-
Notifications
You must be signed in to change notification settings - Fork 7.6k
LESS Refactoring - add LanguageManager #2844
Changes from all commits
d3e8c1f
7674e1e
0781d8b
da92090
2547cca
ec5efa0
3c3764f
b62be1e
6912ba7
87183a3
f64a9d0
d830642
5dc6907
3661fed
0b26e09
c913e39
ba0426d
68e596a
a59ed7c
0b9f572
486b699
7484ca7
b49ac8a
fbd3f77
b9d4883
3fa1b02
35c93d9
b5c46cd
dd708d1
be7b8cb
093856c
c1a6e28
c590931
d63fe7d
56df18d
4c3d51e
40dee60
59cf95f
c00bfb1
7ccdbfa
cb5e41a
bc499ef
c69a8e6
70d8028
0949dc0
b14a9b3
d3579a2
69b14b9
87494b4
ccaccd1
1b286c1
41f72a1
7049c60
d7411eb
1c8a15d
bebb6cf
5a026ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,7 @@ | |
|
||
|
||
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ | ||
/*global define, $ */ | ||
/*global define, $, PathUtils */ | ||
|
||
/** | ||
* DocumentManager maintains a list of currently 'open' Documents. It also owns the list of files in | ||
|
@@ -92,7 +92,8 @@ define(function (require, exports, module) { | |
Async = require("utils/Async"), | ||
CollectionUtils = require("utils/CollectionUtils"), | ||
PerfUtils = require("utils/PerfUtils"), | ||
Commands = require("command/Commands"); | ||
Commands = require("command/Commands"), | ||
LanguageManager = require("language/LanguageManager"); | ||
|
||
/** | ||
* Unique PreferencesManager clientID | ||
|
@@ -598,6 +599,11 @@ define(function (require, exports, module) { | |
this.file = file; | ||
this.refreshText(rawText, initialTimestamp); | ||
|
||
this._updateLanguage(); | ||
// TODO: remove this listener when the document object is obsolete. | ||
// But when is this the case? When _refCount === 0? | ||
$(this.file).on("rename", this._updateLanguage.bind(this)); | ||
|
||
// This is a good point to clean up any old dangling Documents | ||
_gcDocuments(); | ||
} | ||
|
@@ -613,6 +619,12 @@ define(function (require, exports, module) { | |
* @type {!FileEntry} | ||
*/ | ||
Document.prototype.file = null; | ||
|
||
/** | ||
* The Language for this document. Will be resolved by file extension in the constructor | ||
* @type {!Language} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should really be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something like _updateLanguage() feels better, I agree. Having things break early is easier to debug. |
||
*/ | ||
Document.prototype.language = null; | ||
|
||
/** | ||
* Whether this document has unsaved changes or not. | ||
|
@@ -929,6 +941,28 @@ define(function (require, exports, module) { | |
return "[Document " + this.file.fullPath + dirtyInfo + editorInfo + refInfo + "]"; | ||
}; | ||
|
||
/** | ||
* Returns the language this document is written in. | ||
* The language returned is based on the file extension. | ||
* @return {Language} An object describing the language used in this document | ||
*/ | ||
Document.prototype.getLanguage = function () { | ||
return this.language; | ||
}; | ||
|
||
/** | ||
* Updates the language according to the file extension | ||
*/ | ||
Document.prototype._updateLanguage = function () { | ||
var oldLanguage = this.language; | ||
var ext = PathUtils.filenameExtension(this.file.fullPath); | ||
this.language = LanguageManager.getLanguageForFileExtension(ext); | ||
|
||
if (oldLanguage && oldLanguage !== this.language) { | ||
$(this).triggerHandler("languageChanged", [oldLanguage, this.language]); | ||
} | ||
}; | ||
|
||
/** | ||
* Gets an existing open Document for the given file, or creates a new one if the Document is | ||
* not currently open ('open' means referenced by the UI somewhere). Always use this method to | ||
|
@@ -1167,7 +1201,7 @@ define(function (require, exports, module) { | |
// Send a "fileNameChanged" event. This will trigger the views to update. | ||
$(exports).triggerHandler("fileNameChange", [oldName, newName]); | ||
} | ||
|
||
// Define public API | ||
exports.Document = Document; | ||
exports.getCurrentDocument = getCurrentDocument; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -284,15 +284,11 @@ define(function (require, exports, module) { | |
* @param {!boolean} makeMasterEditor If true, this Editor will set itself as the (secret) "master" | ||
* Editor for the Document. If false, this Editor will attach to the Document as a "slave"/ | ||
* secondary editor. | ||
* @param {!(string|Object)} mode Syntax-highlighting language mode; "" means plain-text mode. | ||
* May either be a string naming the mode, or an object containing a "name" property | ||
* naming the mode along with configuration options required by the mode. | ||
* See {@link EditorUtils#getModeFromFileExtension()}. | ||
* @param {!jQueryObject} container Container to add the editor to. | ||
* @param {{startLine: number, endLine: number}=} range If specified, range of lines within the document | ||
* to display in this editor. Inclusive. | ||
*/ | ||
function Editor(document, makeMasterEditor, mode, container, range) { | ||
function Editor(document, makeMasterEditor, container, range) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't believe that existing extensions will call this constructor, but we should address this API change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Address how? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question. :) @peterflynn maybe you can also chime in. Since we can't overload the constructor, maybe we can just leave the argument in and change line 312 to:
Not the cleanest idea but at the moment I'm drawing a blank. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case, I suggest we just make the change and document it as a breaking API change in the release notes. It's not really that supported to create Editors without going through EditorManager anyway, and I suspect there aren't any extensions doing so... |
||
var self = this; | ||
|
||
_instances.push(this); | ||
|
@@ -308,8 +304,12 @@ define(function (require, exports, module) { | |
// store this-bound version of listeners so we can remove them later | ||
this._handleDocumentChange = this._handleDocumentChange.bind(this); | ||
this._handleDocumentDeleted = this._handleDocumentDeleted.bind(this); | ||
this._handleDocumentLanguageChanged = this._handleDocumentLanguageChanged.bind(this); | ||
$(document).on("change", this._handleDocumentChange); | ||
$(document).on("deleted", this._handleDocumentDeleted); | ||
$(document).on("languageChanged", this._handleDocumentLanguageChanged); | ||
|
||
var mode = this._getModeFromDocument(); | ||
|
||
// (if makeMasterEditor, we attach the Doc back to ourselves below once we're fully initialized) | ||
|
||
|
@@ -346,13 +346,6 @@ define(function (require, exports, module) { | |
"Cmd-Left": "goLineStartSmart" | ||
}; | ||
|
||
// We'd like null/"" to mean plain text mode. CodeMirror defaults to plaintext for any | ||
// unrecognized mode, but it complains on the console in that fallback case: so, convert | ||
// here so we're always explicit, avoiding console noise. | ||
if (!mode) { | ||
mode = "text/plain"; | ||
} | ||
|
||
// Create the CodeMirror instance | ||
// (note: CodeMirror doesn't actually require using 'new', but jslint complains without it) | ||
this._codeMirror = new CodeMirror(container, { | ||
|
@@ -436,6 +429,7 @@ define(function (require, exports, module) { | |
this.document.releaseRef(); | ||
$(this.document).off("change", this._handleDocumentChange); | ||
$(this.document).off("deleted", this._handleDocumentDeleted); | ||
$(this.document).off("languageChanged", this._handleDocumentLanguageChanged); | ||
|
||
if (this._visibleRange) { // TextRange also refs the Document | ||
this._visibleRange.dispose(); | ||
|
@@ -453,6 +447,18 @@ define(function (require, exports, module) { | |
}); | ||
}; | ||
|
||
/** | ||
* Determine the mode to use from the document's language | ||
* Uses "text/plain" if the language does not define a mode | ||
* @return string The mode to use | ||
*/ | ||
Editor.prototype._getModeFromDocument = function () { | ||
// We'd like undefined/null/"" to mean plain text mode. CodeMirror defaults to plaintext for any | ||
// unrecognized mode, but it complains on the console in that fallback case: so, convert | ||
// here so we're always explicit, avoiding console noise. | ||
return this.document.getLanguage().mode || "text/plain"; | ||
}; | ||
|
||
|
||
/** | ||
* Selects all text and maintains the current scroll position. | ||
|
@@ -598,6 +604,13 @@ define(function (require, exports, module) { | |
$(this).triggerHandler("lostContent", [event]); | ||
}; | ||
|
||
/** | ||
* Responds to language changes, for instance when the file extension is changed. | ||
*/ | ||
Editor.prototype._handleDocumentLanguageChanged = function (event) { | ||
this._codeMirror.setOption("mode", this._getModeFromDocument()); | ||
}; | ||
|
||
|
||
/** | ||
* Install event handlers on the CodeMirror instance, translating them into | ||
|
@@ -1195,7 +1208,7 @@ define(function (require, exports, module) { | |
* | ||
* @return {?(Object|string)} Name of syntax-highlighting mode, or object containing a "name" property | ||
* naming the mode along with configuration options required by the mode. | ||
* See {@link EditorUtils#getModeFromFileExtension()}. | ||
* See {@link Languages#getLanguageForFileExtension()} and {@link Language#mode}. | ||
*/ | ||
Editor.prototype.getModeForSelection = function () { | ||
// Check for mixed mode info | ||
|
@@ -1221,25 +1234,19 @@ define(function (require, exports, module) { | |
} | ||
}; | ||
|
||
Editor.prototype.getLanguageForSelection = function () { | ||
return this.document.getLanguage().getLanguageForMode(this.getModeForSelection()); | ||
}; | ||
|
||
/** | ||
* Gets the syntax-highlighting mode for the document. | ||
* | ||
* @return {Object|String} Object or Name of syntax-highlighting mode; see {@link EditorUtils#getModeFromFileExtension()}. | ||
* @return {Object|String} Object or Name of syntax-highlighting mode; see {@link Languages#getLanguageForFileExtension()} and {@link Language#mode}. | ||
*/ | ||
Editor.prototype.getModeForDocument = function () { | ||
return this._codeMirror.getOption("mode"); | ||
}; | ||
|
||
/** | ||
* Sets the syntax-highlighting mode for the document. | ||
* | ||
* @param {(string|Object)} mode Name of syntax highlighting mode, or object containing a "name" | ||
* property naming the mode along with configuration options required by the mode. | ||
*/ | ||
Editor.prototype.setModeForDocument = function (mode) { | ||
this._codeMirror.setOption("mode", mode); | ||
}; | ||
|
||
/** | ||
* The Document we're bound to | ||
* @type {!Document} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,7 +39,6 @@ define(function (require, exports, module) { | |
StringUtils = require("utils/StringUtils"), | ||
TokenUtils = require("utils/TokenUtils"); | ||
|
||
|
||
/** | ||
* List of constants | ||
*/ | ||
|
@@ -55,14 +54,15 @@ define(function (require, exports, module) { | |
* @param {!number} endLine - valid line inside the document | ||
* @return {boolean} true if there is at least one uncommented line | ||
*/ | ||
function _containsUncommented(editor, startLine, endLine) { | ||
function _containsUncommented(editor, startLine, endLine, prefix) { | ||
var lineExp = new RegExp("^\\s*" + StringUtils.regexEscape(prefix)); | ||
var containsUncommented = false; | ||
var i; | ||
var line; | ||
for (i = startLine; i <= endLine; i++) { | ||
line = editor.document.getLine(i); | ||
// A line is commented out if it starts with 0-N whitespace chars, then "//" | ||
if (!line.match(/^\s*\/\//) && line.match(/\S/)) { | ||
if (!line.match(lineExp) && line.match(/\S/)) { | ||
containsUncommented = true; | ||
break; | ||
} | ||
|
@@ -75,11 +75,10 @@ define(function (require, exports, module) { | |
* and cursor position. Applies to currently focused Editor. | ||
* | ||
* If all non-whitespace lines are already commented out, then we uncomment; otherwise we comment | ||
* out. Commenting out adds "//" to at column 0 of every line. Uncommenting removes the first "//" | ||
* out. Commenting out adds the prefix at column 0 of every line. Uncommenting removes the first prefix | ||
* on each line (if any - empty lines might not have one). | ||
*/ | ||
function lineCommentSlashSlash(editor) { | ||
|
||
function lineCommentPrefix(editor, prefix) { | ||
var doc = editor.document; | ||
var sel = editor.getSelection(); | ||
var startLine = sel.start.line; | ||
|
@@ -96,7 +95,7 @@ define(function (require, exports, module) { | |
// Decide if we're commenting vs. un-commenting | ||
// Are there any non-blank lines that aren't commented out? (We ignore blank lines because | ||
// some editors like Sublime don't comment them out) | ||
var containsUncommented = _containsUncommented(editor, startLine, endLine); | ||
var containsUncommented = _containsUncommented(editor, startLine, endLine, prefix); | ||
var i; | ||
var line; | ||
var updateSelection = false; | ||
|
@@ -107,7 +106,7 @@ define(function (require, exports, module) { | |
if (containsUncommented) { | ||
// Comment out - prepend "//" to each line | ||
for (i = startLine; i <= endLine; i++) { | ||
doc.replaceRange("//", {line: i, ch: 0}); | ||
doc.replaceRange(prefix, {line: i, ch: 0}); | ||
} | ||
|
||
// Make sure selection includes "//" that was added at start of range | ||
|
@@ -119,9 +118,9 @@ define(function (require, exports, module) { | |
// Uncomment - remove first "//" on each line (if any) | ||
for (i = startLine; i <= endLine; i++) { | ||
line = doc.getLine(i); | ||
var commentI = line.indexOf("//"); | ||
var commentI = line.indexOf(prefix); | ||
if (commentI !== -1) { | ||
doc.replaceRange("", {line: i, ch: commentI}, {line: i, ch: commentI + 2}); | ||
doc.replaceRange("", {line: i, ch: commentI}, {line: i, ch: commentI + prefix.length}); | ||
} | ||
} | ||
} | ||
|
@@ -205,11 +204,11 @@ define(function (require, exports, module) { | |
* the lines in the selection are line-commented. | ||
* | ||
* @param {!Editor} editor | ||
* @param {!String} prefix | ||
* @param {!String} suffix | ||
* @param {boolean=} slashComment - true if the mode also supports "//" comments | ||
* @param {!string} prefix, e.g. "<!--" | ||
* @param {!string} suffix, e.g. "-->" | ||
* @param {?string} linePrefix, e.g. "//" | ||
*/ | ||
function blockCommentPrefixSuffix(editor, prefix, suffix, slashComment) { | ||
function blockCommentPrefixSuffix(editor, prefix, suffix, linePrefix) { | ||
|
||
var doc = editor.document, | ||
sel = editor.getSelection(), | ||
|
@@ -218,7 +217,7 @@ define(function (require, exports, module) { | |
endCtx = TokenUtils.getInitialContext(editor._codeMirror, {line: sel.end.line, ch: sel.end.ch}), | ||
prefixExp = new RegExp("^" + StringUtils.regexEscape(prefix), "g"), | ||
suffixExp = new RegExp(StringUtils.regexEscape(suffix) + "$", "g"), | ||
lineExp = new RegExp("^\/\/"), | ||
lineExp = linePrefix ? new RegExp("^" + StringUtils.regexEscape(linePrefix)) : null, | ||
prefixPos = null, | ||
suffixPos = null, | ||
canComment = false, | ||
|
@@ -234,7 +233,7 @@ define(function (require, exports, module) { | |
} | ||
|
||
// Check if we should just do a line uncomment (if all lines in the selection are commented). | ||
if (slashComment && (ctx.token.string.match(lineExp) || endCtx.token.string.match(lineExp))) { | ||
if (lineExp && (ctx.token.string.match(lineExp) || endCtx.token.string.match(lineExp))) { | ||
var startCtxIndex = editor.indexFromPos({line: ctx.pos.line, ch: ctx.token.start}); | ||
var endCtxIndex = editor.indexFromPos({line: endCtx.pos.line, ch: endCtx.token.start + endCtx.token.string.length}); | ||
|
||
|
@@ -256,7 +255,7 @@ define(function (require, exports, module) { | |
} | ||
|
||
// Find if all the lines are line-commented. | ||
if (!_containsUncommented(editor, sel.start.line, endLine)) { | ||
if (!_containsUncommented(editor, sel.start.line, endLine, linePrefix)) { | ||
lineUncomment = true; | ||
|
||
// Block-comment in all the other cases | ||
|
@@ -328,7 +327,7 @@ define(function (require, exports, module) { | |
return; | ||
|
||
} else if (lineUncomment) { | ||
lineCommentSlashSlash(editor); | ||
lineCommentPrefix(editor, linePrefix); | ||
|
||
} else { | ||
doc.batchOperation(function () { | ||
|
@@ -438,12 +437,11 @@ define(function (require, exports, module) { | |
result = result && _findNextBlockComment(ctx, selEnd, prefixExp); | ||
|
||
if (className === "comment" || result || isLineSelection) { | ||
blockCommentPrefixSuffix(editor, prefix, suffix, false); | ||
|
||
blockCommentPrefixSuffix(editor, prefix, suffix); | ||
} else { | ||
// Set the new selection and comment it | ||
editor.setSelection(selStart, selEnd); | ||
blockCommentPrefixSuffix(editor, prefix, suffix, false); | ||
blockCommentPrefixSuffix(editor, prefix, suffix); | ||
|
||
// Restore the old selection taking into account the prefix change | ||
if (isMultipleLine) { | ||
|
@@ -468,14 +466,10 @@ define(function (require, exports, module) { | |
return; | ||
} | ||
|
||
var mode = editor.getModeForSelection(); | ||
var language = editor.getLanguageForSelection(); | ||
|
||
if (mode === "javascript" || mode === "less") { | ||
blockCommentPrefixSuffix(editor, "/*", "*/", true); | ||
} else if (mode === "css") { | ||
blockCommentPrefixSuffix(editor, "/*", "*/", false); | ||
} else if (mode === "html") { | ||
blockCommentPrefixSuffix(editor, "<!--", "-->", false); | ||
if (language.blockComment) { | ||
blockCommentPrefixSuffix(editor, language.blockComment.prefix, language.blockComment.suffix, language.lineComment ? language.lineComment.prefix : null); | ||
} | ||
} | ||
|
||
|
@@ -489,15 +483,12 @@ define(function (require, exports, module) { | |
return; | ||
} | ||
|
||
var mode = editor.getModeForSelection(); | ||
var language = editor.getLanguageForSelection(); | ||
|
||
// Currently we only support languages with "//" commenting | ||
if (mode === "javascript" || mode === "less") { | ||
lineCommentSlashSlash(editor); | ||
} else if (mode === "css") { | ||
lineCommentPrefixSuffix(editor, "/*", "*/"); | ||
} else if (mode === "html") { | ||
lineCommentPrefixSuffix(editor, "<!--", "-->"); | ||
if (language.lineComment) { | ||
lineCommentPrefix(editor, language.lineComment.prefix); | ||
} else if (language.blockComment) { | ||
lineCommentPrefixSuffix(editor, language.blockComment.prefix, language.blockComment.suffix); | ||
} | ||
} | ||
|
||
|
@@ -733,7 +724,7 @@ define(function (require, exports, module) { | |
CommandManager.register(Strings.CMD_LINE_UP, Commands.EDIT_LINE_UP, moveLineUp); | ||
CommandManager.register(Strings.CMD_LINE_DOWN, Commands.EDIT_LINE_DOWN, moveLineDown); | ||
CommandManager.register(Strings.CMD_SELECT_LINE, Commands.EDIT_SELECT_LINE, selectLine); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, there it is! |
||
CommandManager.register(Strings.CMD_UNDO, Commands.EDIT_UNDO, handleUndo); | ||
CommandManager.register(Strings.CMD_REDO, Commands.EDIT_REDO, handleRedo); | ||
CommandManager.register(Strings.CMD_CUT, Commands.EDIT_CUT, ignoreCommand); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, yes this is a little tricky given the lack of weak references (/ weak listeners) in JS... The FileEntry will probably be a lot less permanent than the DocumentManager singleton this code used to listen to, but nonetheless we probably still do need to clean up the listener.
How about this -- on the first addRef() we add this listener, and on the last releaseRef() we clean it up. Anyone keeping a Document around for an asynchronous length of time is required to addRef() it, so the listener only matters when the refcount is non-zero.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spun off as #2961