From 8f779c9cd2145142d209779eed764d968136d7a4 Mon Sep 17 00:00:00 2001 From: Dennis Kehrig Date: Wed, 13 Mar 2013 12:19:49 +0100 Subject: [PATCH 1/4] Add support for complex file extensions like "coffee.md" or "html.erb" The entries in the fileNames field of a language definition can now also contain extensions --- src/language/LanguageManager.js | 56 ++++++++++++++++++++----------- test/spec/LanguageManager-test.js | 15 +++++++++ 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/src/language/LanguageManager.js b/src/language/LanguageManager.js index 153d8eaaaea..9d40768b6a9 100644 --- a/src/language/LanguageManager.js +++ b/src/language/LanguageManager.js @@ -47,6 +47,13 @@ * console.log("Language " + language.getName() + " is now available!"); * }); * + * The extension can also contain dots: + * LanguageManager.defineLanguage("literatecoffeescript", { + * name: "Literate CoffeeScript", + * mode: "coffeescript", + * fileExtensions: ["litcoffee", "coffee.md"], + * }); + * * You can also specify file names: * LanguageManager.defineLanguage("makefile", { * name: "Make", @@ -133,21 +140,6 @@ define(function (require, exports, module) { } } - /** - * Lowercases the file extension and ensures it doesn't start with a dot. - * @param {!string} extension The file extension - * @return {string} The normalized file extension - */ - function _normalizeFileExtension(extension) { - // Remove a leading dot if present - if (extension.charAt(0) === ".") { - extension = extension.substr(1); - } - - // Make checks below case-INsensitive - return extension.toLowerCase(); - } - /** * Monkey-patch CodeMirror to prevent modes from being overwritten by extensions. * We may rely on the tokens provided by some of these modes. @@ -188,16 +180,33 @@ define(function (require, exports, module) { /** * Resolves a file path to a Language object. + * File names have a higher priority than file extensions. * @param {!string} path Path to the file to find a language for * @return {Language} The language for the provided file type or the fallback language */ function getLanguageForPath(path) { - var extension = _normalizeFileExtension(PathUtils.filenameExtension(path)), - filename = PathUtils.filename(path).toLowerCase(), - language = extension ? _fileExtensionToLanguageMap[extension] : _fileNameToLanguageMap[filename]; + var fileName = PathUtils.filename(path).toLowerCase(), + language = _fileNameToLanguageMap[fileName], + extension, + extensions, + parts, + i, + l; if (!language) { - console.log("Called LanguageManager.getLanguageForPath with an unhandled " + (extension ? "file extension" : "file name") + ":", extension || filename); + // Split file.coffee.md into ["file", "coffee", "md"] + parts = fileName.split("."); + extensions = []; + for (i = 1, l = parts.length; i < l && !language; i++) { + // E.g. "coffee.md", then "md", unless "coffee.md" resolves to a language + extension = parts.slice(i).join("."); + language = _fileExtensionToLanguageMap[extension]; + extensions.push(extension); + } + } + + if (!language) { + console.log("No language found for file name \"" + fileName + "\" or extensions " + extensions.map(JSON.stringify).join(" / ")); } return language || _fallbackLanguage; @@ -383,7 +392,13 @@ define(function (require, exports, module) { * @private */ Language.prototype._addFileExtension = function (extension) { - extension = _normalizeFileExtension(extension); + // Remove a leading dot if present + if (extension.charAt(0) === ".") { + extension = extension.substr(1); + } + + // Make checks below case-INsensitive + extension = extension.toLowerCase(); if (this._fileExtensions.indexOf(extension) === -1) { this._fileExtensions.push(extension); @@ -410,6 +425,7 @@ define(function (require, exports, module) { * @private */ Language.prototype._addFileName = function (name) { + // Make checks below case-INsensitive name = name.toLowerCase(); if (this._fileNames.indexOf(name) === -1) { diff --git a/test/spec/LanguageManager-test.js b/test/spec/LanguageManager-test.js index 6b53bc313e4..287ebc6a8d7 100644 --- a/test/spec/LanguageManager-test.js +++ b/test/spec/LanguageManager-test.js @@ -137,6 +137,21 @@ define(function (require, exports, module) { expect(LanguageManager.getLanguageForPath("foo.doesNotExist")).toBe(unknown); }); + it("should map complex file extensions to languages", function () { + var ruby = LanguageManager.getLanguage("ruby"), + html = LanguageManager.getLanguage("html"), + unknown = LanguageManager.getLanguage("unknown"); + + expect(LanguageManager.getLanguageForPath("foo.html.erb")).toBe(unknown); + expect(LanguageManager.getLanguageForPath("foo.erb")).toBe(unknown); + + html._addFileExtension("html.erb"); + ruby._addFileExtension("erb"); + + expect(LanguageManager.getLanguageForPath("foo.html.erb")).toBe(html); + expect(LanguageManager.getLanguageForPath("foo.erb")).toBe(ruby); + }); + it("should map file names to languages", function () { var coffee = LanguageManager.getLanguage("coffeescript"), unknown = LanguageManager.getLanguage("unknown"); From 53e3d8950503a56d21f011f391b11a166b39bca8 Mon Sep 17 00:00:00 2001 From: Dennis Kehrig Date: Tue, 19 Mar 2013 13:57:58 +0100 Subject: [PATCH 2/4] Ignore leading dots when determining the file extension (".profile" doesn't have a file extension) Don't spam the console when no language could be found for an extension --- src/language/LanguageManager.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/language/LanguageManager.js b/src/language/LanguageManager.js index 2c7c1137922..22be9a2d9d0 100644 --- a/src/language/LanguageManager.js +++ b/src/language/LanguageManager.js @@ -194,21 +194,19 @@ define(function (require, exports, module) { l; if (!language) { - // Split file.coffee.md into ["file", "coffee", "md"] + // Split "foo.coffee.md" into ["foo", "coffee", "md"] + // Split ".profile" into ["", "profile"] parts = fileName.split("."); extensions = []; - for (i = 1, l = parts.length; i < l && !language; i++) { - // E.g. "coffee.md", then "md", unless "coffee.md" resolves to a language + // For file name "foo.coffee.md", consider "coffee.md" and "md" as extensions + // Treat file name ".coffee.md" as a hidden file named "coffee.md" and only consider "md" as extension + for (i = parts[0] === "" ? 2 : 1, l = parts.length; i < l && !language; i++) { extension = parts.slice(i).join("."); language = _fileExtensionToLanguageMap[extension]; extensions.push(extension); } } - if (!language) { - console.log("No language found for file name \"" + fileName + "\" or extensions " + extensions.map(JSON.stringify).join(" / ")); - } - return language || _fallbackLanguage; } From 73a85635f94124d55c3171506caf414e219114ac Mon Sep 17 00:00:00 2001 From: Dennis Kehrig Date: Tue, 19 Mar 2013 14:11:00 +0100 Subject: [PATCH 3/4] Updated the documentation --- src/language/LanguageManager.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/language/LanguageManager.js b/src/language/LanguageManager.js index 22be9a2d9d0..456638e20ab 100644 --- a/src/language/LanguageManager.js +++ b/src/language/LanguageManager.js @@ -51,7 +51,7 @@ * LanguageManager.defineLanguage("literatecoffeescript", { * name: "Literate CoffeeScript", * mode: "coffeescript", - * fileExtensions: ["litcoffee", "coffee.md"], + * fileExtensions: ["litcoffee", "coffee.md"] * }); * * You can also specify file names: @@ -407,8 +407,6 @@ define(function (require, exports, module) { /** * Adds a file extension to this language. - * Private for now since dependent code would need to by kept in sync with such changes. - * See https://github.com/adobe/brackets/issues/2966 for plans to make this public. * @param {!string} extension A file extension used by this language */ Language.prototype.addFileExtension = function (extension) { @@ -436,10 +434,7 @@ define(function (require, exports, module) { /** * Adds a file name to the language which is used to match files that don't have extensions like "Makefile" for example. - * Private for now since dependent code would need to by kept in sync with such changes. - * See https://github.com/adobe/brackets/issues/2966 for plans to make this public. * @param {!string} extension An extensionless file name used by this language - * @private */ Language.prototype.addFileName = function (name) { // Make checks below case-INsensitive From 17d06f01df57b46d6516696062b05590e34a565f Mon Sep 17 00:00:00 2001 From: Dennis Kehrig Date: Tue, 19 Mar 2013 15:14:34 +0100 Subject: [PATCH 4/4] Fixed a unit test --- test/spec/LanguageManager-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/spec/LanguageManager-test.js b/test/spec/LanguageManager-test.js index 9fe3a58a2a9..a011307010f 100644 --- a/test/spec/LanguageManager-test.js +++ b/test/spec/LanguageManager-test.js @@ -144,8 +144,8 @@ define(function (require, exports, module) { expect(LanguageManager.getLanguageForPath("foo.html.erb")).toBe(unknown); expect(LanguageManager.getLanguageForPath("foo.erb")).toBe(unknown); - html._addFileExtension("html.erb"); - ruby._addFileExtension("erb"); + html.addFileExtension("html.erb"); + ruby.addFileExtension("erb"); expect(LanguageManager.getLanguageForPath("foo.html.erb")).toBe(html); expect(LanguageManager.getLanguageForPath("foo.erb")).toBe(ruby);