Skip to content
This repository has been archived by the owner on Sep 6, 2021. It is now read-only.

LESS Refactoring - add LanguageManager #2844

Merged
merged 57 commits into from
Feb 27, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
d3e8c1f
Fixed a typo
DennisKehrig Dec 11, 2012
7674e1e
Allow setting which strings to use for line and block comments (bad, …
DennisKehrig Dec 11, 2012
0781d8b
Support for MORE, a renamed version of LESS, as an extension
DennisKehrig Dec 11, 2012
da92090
Removed journal, it's in the wiki now: https://github.com/adobe/brack…
DennisKehrig Dec 18, 2012
2547cca
LESS extension: reloading the editors is no longer necessary since ex…
DennisKehrig Dec 18, 2012
ec5efa0
Added the Languages module to have a centralized place for adding new…
DennisKehrig Dec 19, 2012
3c3764f
Redesigned the language API with a fluent interface to allow for late…
DennisKehrig Jan 7, 2013
b62be1e
The MoreSupport extension now show cases more of the API features
DennisKehrig Jan 9, 2013
6912ba7
Documents and editor now provide more direct APIs to access the used …
DennisKehrig Jan 9, 2013
87183a3
Added a default language with ID "unknown" so that documents always h…
DennisKehrig Jan 9, 2013
f64a9d0
Remove LESS parser, this would be needed for LiveDevelopment later, b…
DennisKehrig Jan 9, 2013
d830642
Stripped out existing LESS support and renamed MORE extension to LESS
DennisKehrig Jan 9, 2013
5dc6907
Added on comment and fixed a couple of JSLint complains
DennisKehrig Jan 9, 2013
3661fed
Language API: removed MIME type property
DennisKehrig Jan 21, 2013
0b26e09
Renamed LessSupport to LESSSupport since the other extensions are cap…
DennisKehrig Jan 21, 2013
c913e39
Removed Document.setLanguage
DennisKehrig Jan 21, 2013
ba0426d
Removed Editor.getLanguageForDocument
DennisKehrig Jan 21, 2013
68e596a
Removed Editor.setModeForDocument - when renaming the current documen…
DennisKehrig Jan 21, 2013
a59ed7c
Default languages are now defined via language/languages.json
DennisKehrig Jan 21, 2013
0b9f572
Removed a debugging statement
DennisKehrig Jan 21, 2013
486b699
Restrict refining languages to setting the mode
DennisKehrig Jan 21, 2013
7484ca7
Removed EditorUtils#getModeFromFileExtension (and therefore EditorUti…
DennisKehrig Jan 21, 2013
b49ac8a
Set the document language in the constructor
DennisKehrig Jan 21, 2013
fbd3f77
Removed StatusBar/getModeDisplayString
DennisKehrig Jan 21, 2013
b9d4883
Don't expose the defaultLanguage anymore, also making it read-only th…
DennisKehrig Jan 21, 2013
3fa1b02
Bugfix: delete the document's language cache when its file is renamed
DennisKehrig Jan 21, 2013
35c93d9
Added documentation to Languages.js
DennisKehrig Jan 21, 2013
b5c46cd
Fixed a couple of JSLint complains
DennisKehrig Jan 21, 2013
dd708d1
Use the require.js text plugin to load languages.json seemingly synch…
DennisKehrig Jan 23, 2013
be7b8cb
Added a convenience function to load and set a built-in mode for a la…
DennisKehrig Jan 23, 2013
093856c
Use 4 spaces instead of tabs everywhere
DennisKehrig Jan 23, 2013
c1a6e28
Make setLineComment and setBlockComment public after all, since those…
DennisKehrig Jan 28, 2013
c590931
Load modes via require after all, allowing language.setMode to be pri…
DennisKehrig Jan 28, 2013
d63fe7d
Allow extensions to be dependent on other extensions
DennisKehrig Jan 28, 2013
56df18d
Bugfix: error during merge
DennisKehrig Feb 11, 2013
4c3d51e
Reflect the transition from modes to languages with regards to the st…
DennisKehrig Feb 18, 2013
40dee60
Bugfix: directory renames were not propagated
DennisKehrig Feb 20, 2013
59cf95f
Bugfix: renaming file "foo" should not affect file "foobar/baz" even …
DennisKehrig Feb 20, 2013
c00bfb1
The language is now detected when constructing the document and when …
DennisKehrig Feb 20, 2013
7ccdbfa
Fix JSDoc syntax
DennisKehrig Feb 20, 2013
cb5e41a
Restore separation of commands that are just proxies for CM behavior …
DennisKehrig Feb 20, 2013
bc499ef
Document the "extension" plugin for require.js
DennisKehrig Feb 20, 2013
c69a8e6
Removed JSLint's reference to CodeMirror in ExtensionUtils.js
DennisKehrig Feb 20, 2013
70d8028
Allow language IDs like foo_bar for package-style naming
DennisKehrig Feb 20, 2013
0949dc0
Removed the require.js to allow extension dependencies. A better appr…
DennisKehrig Feb 21, 2013
b14a9b3
Renamed language/Languages.js to languages/LanguageManager.js
DennisKehrig Feb 21, 2013
d3579a2
Check properly whether an object is a string and cite thine sources
DennisKehrig Feb 21, 2013
69b14b9
User lower case file extensions
DennisKehrig Feb 21, 2013
87494b4
Better naming of variables
DennisKehrig Feb 21, 2013
ccaccd1
Removed language aliases because the special "html" case was actually…
DennisKehrig Feb 21, 2013
1b286c1
Adjust the tests to reflect that EditorUtils is now gone
DennisKehrig Feb 21, 2013
41f72a1
Added new MIME mode text/x-brackets-html for HTML files
DennisKehrig Feb 21, 2013
7049c60
Bring the unit tests up to speed with the language API
DennisKehrig Feb 21, 2013
d7411eb
Forgot to finish a sentence
DennisKehrig Feb 21, 2013
1c8a15d
Code cleanup after reading @jasonsanjose's helpful suggestions
DennisKehrig Feb 25, 2013
bebb6cf
Merge branch 'master' into dk/less-refactoring
jasonsanjose Feb 26, 2013
5a026ec
Update JS code hints (including unit tests) to work with new language…
peterflynn Feb 26, 2013
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions src/document/DocumentManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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?
Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spun off as #2961

$(this.file).on("rename", this._updateLanguage.bind(this));

// This is a good point to clean up any old dangling Documents
_gcDocuments();
}
Expand All @@ -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}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should really be {?Language} given that this remains null until the first call to getLanguage(). Perhaps it'd be cleaner to not initialize this lazily though -- have something like a _updateLanguage() function that's called in the ctor and upon rename, and then have a dumb getter that merely returns the field.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
55 changes: 31 additions & 24 deletions src/editor/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address how?

Copy link
Member

Choose a reason for hiding this comment

The 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:

mode = this._getModeFromDocument() || mode;

Not the cleanest idea but at the moment I'm drawing a blank.

Copy link
Member

Choose a reason for hiding this comment

The 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);
Expand All @@ -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)

Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -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();
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
65 changes: 28 additions & 37 deletions src/editor/EditorCommandHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ define(function (require, exports, module) {
StringUtils = require("utils/StringUtils"),
TokenUtils = require("utils/TokenUtils");


/**
* List of constants
*/
Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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});
}
}
}
Expand Down Expand Up @@ -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(),
Expand All @@ -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,
Expand All @@ -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});

Expand All @@ -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
Expand Down Expand Up @@ -328,7 +327,7 @@ define(function (require, exports, module) {
return;

} else if (lineUncomment) {
lineCommentSlashSlash(editor);
lineCommentPrefix(editor, linePrefix);

} else {
doc.batchOperation(function () {
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
}

Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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);

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
Expand Down
Loading