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

Commit fee1311

Browse files
committed
Merge pull request #2844 from adobe/dk/less-refactoring
Add language extensibility APIs; refactor LESS support out into a default extension using those APIs
2 parents 30c2751 + 5a026ec commit fee1311

File tree

22 files changed

+798
-376
lines changed

22 files changed

+798
-376
lines changed

src/document/DocumentManager.js

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424

2525
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
26-
/*global define, $ */
26+
/*global define, $, PathUtils */
2727

2828
/**
2929
* 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) {
9292
Async = require("utils/Async"),
9393
CollectionUtils = require("utils/CollectionUtils"),
9494
PerfUtils = require("utils/PerfUtils"),
95-
Commands = require("command/Commands");
95+
Commands = require("command/Commands"),
96+
LanguageManager = require("language/LanguageManager");
9697

9798
/**
9899
* Unique PreferencesManager clientID
@@ -598,6 +599,11 @@ define(function (require, exports, module) {
598599
this.file = file;
599600
this.refreshText(rawText, initialTimestamp);
600601

602+
this._updateLanguage();
603+
// TODO: remove this listener when the document object is obsolete.
604+
// But when is this the case? When _refCount === 0?
605+
$(this.file).on("rename", this._updateLanguage.bind(this));
606+
601607
// This is a good point to clean up any old dangling Documents
602608
_gcDocuments();
603609
}
@@ -613,6 +619,12 @@ define(function (require, exports, module) {
613619
* @type {!FileEntry}
614620
*/
615621
Document.prototype.file = null;
622+
623+
/**
624+
* The Language for this document. Will be resolved by file extension in the constructor
625+
* @type {!Language}
626+
*/
627+
Document.prototype.language = null;
616628

617629
/**
618630
* Whether this document has unsaved changes or not.
@@ -929,6 +941,28 @@ define(function (require, exports, module) {
929941
return "[Document " + this.file.fullPath + dirtyInfo + editorInfo + refInfo + "]";
930942
};
931943

944+
/**
945+
* Returns the language this document is written in.
946+
* The language returned is based on the file extension.
947+
* @return {Language} An object describing the language used in this document
948+
*/
949+
Document.prototype.getLanguage = function () {
950+
return this.language;
951+
};
952+
953+
/**
954+
* Updates the language according to the file extension
955+
*/
956+
Document.prototype._updateLanguage = function () {
957+
var oldLanguage = this.language;
958+
var ext = PathUtils.filenameExtension(this.file.fullPath);
959+
this.language = LanguageManager.getLanguageForFileExtension(ext);
960+
961+
if (oldLanguage && oldLanguage !== this.language) {
962+
$(this).triggerHandler("languageChanged", [oldLanguage, this.language]);
963+
}
964+
};
965+
932966
/**
933967
* Gets an existing open Document for the given file, or creates a new one if the Document is
934968
* not currently open ('open' means referenced by the UI somewhere). Always use this method to
@@ -1167,7 +1201,7 @@ define(function (require, exports, module) {
11671201
// Send a "fileNameChanged" event. This will trigger the views to update.
11681202
$(exports).triggerHandler("fileNameChange", [oldName, newName]);
11691203
}
1170-
1204+
11711205
// Define public API
11721206
exports.Document = Document;
11731207
exports.getCurrentDocument = getCurrentDocument;

src/editor/Editor.js

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -284,15 +284,11 @@ define(function (require, exports, module) {
284284
* @param {!boolean} makeMasterEditor If true, this Editor will set itself as the (secret) "master"
285285
* Editor for the Document. If false, this Editor will attach to the Document as a "slave"/
286286
* secondary editor.
287-
* @param {!(string|Object)} mode Syntax-highlighting language mode; "" means plain-text mode.
288-
* May either be a string naming the mode, or an object containing a "name" property
289-
* naming the mode along with configuration options required by the mode.
290-
* See {@link EditorUtils#getModeFromFileExtension()}.
291287
* @param {!jQueryObject} container Container to add the editor to.
292288
* @param {{startLine: number, endLine: number}=} range If specified, range of lines within the document
293289
* to display in this editor. Inclusive.
294290
*/
295-
function Editor(document, makeMasterEditor, mode, container, range) {
291+
function Editor(document, makeMasterEditor, container, range) {
296292
var self = this;
297293

298294
_instances.push(this);
@@ -308,8 +304,12 @@ define(function (require, exports, module) {
308304
// store this-bound version of listeners so we can remove them later
309305
this._handleDocumentChange = this._handleDocumentChange.bind(this);
310306
this._handleDocumentDeleted = this._handleDocumentDeleted.bind(this);
307+
this._handleDocumentLanguageChanged = this._handleDocumentLanguageChanged.bind(this);
311308
$(document).on("change", this._handleDocumentChange);
312309
$(document).on("deleted", this._handleDocumentDeleted);
310+
$(document).on("languageChanged", this._handleDocumentLanguageChanged);
311+
312+
var mode = this._getModeFromDocument();
313313

314314
// (if makeMasterEditor, we attach the Doc back to ourselves below once we're fully initialized)
315315

@@ -346,13 +346,6 @@ define(function (require, exports, module) {
346346
"Cmd-Left": "goLineStartSmart"
347347
};
348348

349-
// We'd like null/"" to mean plain text mode. CodeMirror defaults to plaintext for any
350-
// unrecognized mode, but it complains on the console in that fallback case: so, convert
351-
// here so we're always explicit, avoiding console noise.
352-
if (!mode) {
353-
mode = "text/plain";
354-
}
355-
356349
// Create the CodeMirror instance
357350
// (note: CodeMirror doesn't actually require using 'new', but jslint complains without it)
358351
this._codeMirror = new CodeMirror(container, {
@@ -436,6 +429,7 @@ define(function (require, exports, module) {
436429
this.document.releaseRef();
437430
$(this.document).off("change", this._handleDocumentChange);
438431
$(this.document).off("deleted", this._handleDocumentDeleted);
432+
$(this.document).off("languageChanged", this._handleDocumentLanguageChanged);
439433

440434
if (this._visibleRange) { // TextRange also refs the Document
441435
this._visibleRange.dispose();
@@ -453,6 +447,18 @@ define(function (require, exports, module) {
453447
});
454448
};
455449

450+
/**
451+
* Determine the mode to use from the document's language
452+
* Uses "text/plain" if the language does not define a mode
453+
* @return string The mode to use
454+
*/
455+
Editor.prototype._getModeFromDocument = function () {
456+
// We'd like undefined/null/"" to mean plain text mode. CodeMirror defaults to plaintext for any
457+
// unrecognized mode, but it complains on the console in that fallback case: so, convert
458+
// here so we're always explicit, avoiding console noise.
459+
return this.document.getLanguage().mode || "text/plain";
460+
};
461+
456462

457463
/**
458464
* Selects all text and maintains the current scroll position.
@@ -598,6 +604,13 @@ define(function (require, exports, module) {
598604
$(this).triggerHandler("lostContent", [event]);
599605
};
600606

607+
/**
608+
* Responds to language changes, for instance when the file extension is changed.
609+
*/
610+
Editor.prototype._handleDocumentLanguageChanged = function (event) {
611+
this._codeMirror.setOption("mode", this._getModeFromDocument());
612+
};
613+
601614

602615
/**
603616
* Install event handlers on the CodeMirror instance, translating them into
@@ -1195,7 +1208,7 @@ define(function (require, exports, module) {
11951208
*
11961209
* @return {?(Object|string)} Name of syntax-highlighting mode, or object containing a "name" property
11971210
* naming the mode along with configuration options required by the mode.
1198-
* See {@link EditorUtils#getModeFromFileExtension()}.
1211+
* See {@link Languages#getLanguageForFileExtension()} and {@link Language#mode}.
11991212
*/
12001213
Editor.prototype.getModeForSelection = function () {
12011214
// Check for mixed mode info
@@ -1221,25 +1234,19 @@ define(function (require, exports, module) {
12211234
}
12221235
};
12231236

1237+
Editor.prototype.getLanguageForSelection = function () {
1238+
return this.document.getLanguage().getLanguageForMode(this.getModeForSelection());
1239+
};
1240+
12241241
/**
12251242
* Gets the syntax-highlighting mode for the document.
12261243
*
1227-
* @return {Object|String} Object or Name of syntax-highlighting mode; see {@link EditorUtils#getModeFromFileExtension()}.
1244+
* @return {Object|String} Object or Name of syntax-highlighting mode; see {@link Languages#getLanguageForFileExtension()} and {@link Language#mode}.
12281245
*/
12291246
Editor.prototype.getModeForDocument = function () {
12301247
return this._codeMirror.getOption("mode");
12311248
};
12321249

1233-
/**
1234-
* Sets the syntax-highlighting mode for the document.
1235-
*
1236-
* @param {(string|Object)} mode Name of syntax highlighting mode, or object containing a "name"
1237-
* property naming the mode along with configuration options required by the mode.
1238-
*/
1239-
Editor.prototype.setModeForDocument = function (mode) {
1240-
this._codeMirror.setOption("mode", mode);
1241-
};
1242-
12431250
/**
12441251
* The Document we're bound to
12451252
* @type {!Document}

src/editor/EditorCommandHandlers.js

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ define(function (require, exports, module) {
3939
StringUtils = require("utils/StringUtils"),
4040
TokenUtils = require("utils/TokenUtils");
4141

42-
4342
/**
4443
* List of constants
4544
*/
@@ -55,14 +54,15 @@ define(function (require, exports, module) {
5554
* @param {!number} endLine - valid line inside the document
5655
* @return {boolean} true if there is at least one uncommented line
5756
*/
58-
function _containsUncommented(editor, startLine, endLine) {
57+
function _containsUncommented(editor, startLine, endLine, prefix) {
58+
var lineExp = new RegExp("^\\s*" + StringUtils.regexEscape(prefix));
5959
var containsUncommented = false;
6060
var i;
6161
var line;
6262
for (i = startLine; i <= endLine; i++) {
6363
line = editor.document.getLine(i);
6464
// A line is commented out if it starts with 0-N whitespace chars, then "//"
65-
if (!line.match(/^\s*\/\//) && line.match(/\S/)) {
65+
if (!line.match(lineExp) && line.match(/\S/)) {
6666
containsUncommented = true;
6767
break;
6868
}
@@ -75,11 +75,10 @@ define(function (require, exports, module) {
7575
* and cursor position. Applies to currently focused Editor.
7676
*
7777
* If all non-whitespace lines are already commented out, then we uncomment; otherwise we comment
78-
* out. Commenting out adds "//" to at column 0 of every line. Uncommenting removes the first "//"
78+
* out. Commenting out adds the prefix at column 0 of every line. Uncommenting removes the first prefix
7979
* on each line (if any - empty lines might not have one).
8080
*/
81-
function lineCommentSlashSlash(editor) {
82-
81+
function lineCommentPrefix(editor, prefix) {
8382
var doc = editor.document;
8483
var sel = editor.getSelection();
8584
var startLine = sel.start.line;
@@ -96,7 +95,7 @@ define(function (require, exports, module) {
9695
// Decide if we're commenting vs. un-commenting
9796
// Are there any non-blank lines that aren't commented out? (We ignore blank lines because
9897
// some editors like Sublime don't comment them out)
99-
var containsUncommented = _containsUncommented(editor, startLine, endLine);
98+
var containsUncommented = _containsUncommented(editor, startLine, endLine, prefix);
10099
var i;
101100
var line;
102101
var updateSelection = false;
@@ -107,7 +106,7 @@ define(function (require, exports, module) {
107106
if (containsUncommented) {
108107
// Comment out - prepend "//" to each line
109108
for (i = startLine; i <= endLine; i++) {
110-
doc.replaceRange("//", {line: i, ch: 0});
109+
doc.replaceRange(prefix, {line: i, ch: 0});
111110
}
112111

113112
// Make sure selection includes "//" that was added at start of range
@@ -119,9 +118,9 @@ define(function (require, exports, module) {
119118
// Uncomment - remove first "//" on each line (if any)
120119
for (i = startLine; i <= endLine; i++) {
121120
line = doc.getLine(i);
122-
var commentI = line.indexOf("//");
121+
var commentI = line.indexOf(prefix);
123122
if (commentI !== -1) {
124-
doc.replaceRange("", {line: i, ch: commentI}, {line: i, ch: commentI + 2});
123+
doc.replaceRange("", {line: i, ch: commentI}, {line: i, ch: commentI + prefix.length});
125124
}
126125
}
127126
}
@@ -205,11 +204,11 @@ define(function (require, exports, module) {
205204
* the lines in the selection are line-commented.
206205
*
207206
* @param {!Editor} editor
208-
* @param {!String} prefix
209-
* @param {!String} suffix
210-
* @param {boolean=} slashComment - true if the mode also supports "//" comments
207+
* @param {!string} prefix, e.g. "<!--"
208+
* @param {!string} suffix, e.g. "-->"
209+
* @param {?string} linePrefix, e.g. "//"
211210
*/
212-
function blockCommentPrefixSuffix(editor, prefix, suffix, slashComment) {
211+
function blockCommentPrefixSuffix(editor, prefix, suffix, linePrefix) {
213212

214213
var doc = editor.document,
215214
sel = editor.getSelection(),
@@ -218,7 +217,7 @@ define(function (require, exports, module) {
218217
endCtx = TokenUtils.getInitialContext(editor._codeMirror, {line: sel.end.line, ch: sel.end.ch}),
219218
prefixExp = new RegExp("^" + StringUtils.regexEscape(prefix), "g"),
220219
suffixExp = new RegExp(StringUtils.regexEscape(suffix) + "$", "g"),
221-
lineExp = new RegExp("^\/\/"),
220+
lineExp = linePrefix ? new RegExp("^" + StringUtils.regexEscape(linePrefix)) : null,
222221
prefixPos = null,
223222
suffixPos = null,
224223
canComment = false,
@@ -234,7 +233,7 @@ define(function (require, exports, module) {
234233
}
235234

236235
// Check if we should just do a line uncomment (if all lines in the selection are commented).
237-
if (slashComment && (ctx.token.string.match(lineExp) || endCtx.token.string.match(lineExp))) {
236+
if (lineExp && (ctx.token.string.match(lineExp) || endCtx.token.string.match(lineExp))) {
238237
var startCtxIndex = editor.indexFromPos({line: ctx.pos.line, ch: ctx.token.start});
239238
var endCtxIndex = editor.indexFromPos({line: endCtx.pos.line, ch: endCtx.token.start + endCtx.token.string.length});
240239

@@ -256,7 +255,7 @@ define(function (require, exports, module) {
256255
}
257256

258257
// Find if all the lines are line-commented.
259-
if (!_containsUncommented(editor, sel.start.line, endLine)) {
258+
if (!_containsUncommented(editor, sel.start.line, endLine, linePrefix)) {
260259
lineUncomment = true;
261260

262261
// Block-comment in all the other cases
@@ -328,7 +327,7 @@ define(function (require, exports, module) {
328327
return;
329328

330329
} else if (lineUncomment) {
331-
lineCommentSlashSlash(editor);
330+
lineCommentPrefix(editor, linePrefix);
332331

333332
} else {
334333
doc.batchOperation(function () {
@@ -438,12 +437,11 @@ define(function (require, exports, module) {
438437
result = result && _findNextBlockComment(ctx, selEnd, prefixExp);
439438

440439
if (className === "comment" || result || isLineSelection) {
441-
blockCommentPrefixSuffix(editor, prefix, suffix, false);
442-
440+
blockCommentPrefixSuffix(editor, prefix, suffix);
443441
} else {
444442
// Set the new selection and comment it
445443
editor.setSelection(selStart, selEnd);
446-
blockCommentPrefixSuffix(editor, prefix, suffix, false);
444+
blockCommentPrefixSuffix(editor, prefix, suffix);
447445

448446
// Restore the old selection taking into account the prefix change
449447
if (isMultipleLine) {
@@ -468,14 +466,10 @@ define(function (require, exports, module) {
468466
return;
469467
}
470468

471-
var mode = editor.getModeForSelection();
469+
var language = editor.getLanguageForSelection();
472470

473-
if (mode === "javascript" || mode === "less") {
474-
blockCommentPrefixSuffix(editor, "/*", "*/", true);
475-
} else if (mode === "css") {
476-
blockCommentPrefixSuffix(editor, "/*", "*/", false);
477-
} else if (mode === "html") {
478-
blockCommentPrefixSuffix(editor, "<!--", "-->", false);
471+
if (language.blockComment) {
472+
blockCommentPrefixSuffix(editor, language.blockComment.prefix, language.blockComment.suffix, language.lineComment ? language.lineComment.prefix : null);
479473
}
480474
}
481475

@@ -489,15 +483,12 @@ define(function (require, exports, module) {
489483
return;
490484
}
491485

492-
var mode = editor.getModeForSelection();
486+
var language = editor.getLanguageForSelection();
493487

494-
// Currently we only support languages with "//" commenting
495-
if (mode === "javascript" || mode === "less") {
496-
lineCommentSlashSlash(editor);
497-
} else if (mode === "css") {
498-
lineCommentPrefixSuffix(editor, "/*", "*/");
499-
} else if (mode === "html") {
500-
lineCommentPrefixSuffix(editor, "<!--", "-->");
488+
if (language.lineComment) {
489+
lineCommentPrefix(editor, language.lineComment.prefix);
490+
} else if (language.blockComment) {
491+
lineCommentPrefixSuffix(editor, language.blockComment.prefix, language.blockComment.suffix);
501492
}
502493
}
503494

@@ -733,7 +724,7 @@ define(function (require, exports, module) {
733724
CommandManager.register(Strings.CMD_LINE_UP, Commands.EDIT_LINE_UP, moveLineUp);
734725
CommandManager.register(Strings.CMD_LINE_DOWN, Commands.EDIT_LINE_DOWN, moveLineDown);
735726
CommandManager.register(Strings.CMD_SELECT_LINE, Commands.EDIT_SELECT_LINE, selectLine);
736-
727+
737728
CommandManager.register(Strings.CMD_UNDO, Commands.EDIT_UNDO, handleUndo);
738729
CommandManager.register(Strings.CMD_REDO, Commands.EDIT_REDO, handleRedo);
739730
CommandManager.register(Strings.CMD_CUT, Commands.EDIT_CUT, ignoreCommand);

0 commit comments

Comments
 (0)