-
Notifications
You must be signed in to change notification settings - Fork 7.6k
Add support for complex file extensions like ".coffee.md" or ".html.erb" #3122
Changes from all commits
8f779c9
94d3e3d
53e3d89
1981677
73a8563
17d06f0
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 |
---|---|---|
|
@@ -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,31 @@ 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 "foo.coffee.md" into ["foo", "coffee", "md"] | ||
// Split ".profile" into ["", "profile"] | ||
parts = fileName.split("."); | ||
extensions = []; | ||
// 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++) { | ||
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. As cool as this oneliner is, I find it a little bit hard to read. Even more now that we've added the condition inside the declarations part. How would you feel about extracting the declarations and using a while loop instead? 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. Definitely seems warranted :) |
||
extension = parts.slice(i).join("."); | ||
language = _fileExtensionToLanguageMap[extension]; | ||
extensions.push(extension); | ||
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. Would this logic cause a file named "html" (with no extension) to get resolved to HTML since we're treatig it like ".html"? Ideally, it seems like we should distinguish filenames from extensions more clearly... 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. "html" would be a file name. The extension logic only kicks in when there's actually a dot present (due to i=1 instead of i=0). 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've checked, and it works allright. It also works nicely with other filenames such as .bashrc or .profile, detecting first filename and then extension. @peterflynn was your concern fully addressed, or is there something else? 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. Hidden files are an interesting edge case... it probably would treat ".profile" first as a file name, and then use the extension "profile". I don't think that's what we want since the leading dot has different semantics. I will change the code to take this into account. 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. Yes, that's exactly what happens. First it looks for .profile as filename and then for profile as extension. I thought that would work, but now that you mention it, I agree that we'd want it to work slightly different. |
||
} | ||
} | ||
|
||
return language || _fallbackLanguage; | ||
|
@@ -400,12 +407,16 @@ 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) { | ||
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); | ||
|
@@ -423,12 +434,10 @@ 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 | ||
name = name.toLowerCase(); | ||
|
||
if (this._fileNames.indexOf(name) === -1) { | ||
|
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.
This seems a little bit inconsistent. Above,
fileName
returned fromPathUtils.filename
for a file named .coffee.md includes the dot, and here it points to that it is only a marker for hidden files. This is enforced in that you need to use .coffe.md inlanguages.json
as a filename and not coffee.md. I think the behavior is correct as is, so maybe we could just change the comment... what do you think?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.
I see your point... I'll try to rephrase it better, so that it's clear that this is just the rationale for ignoring a leading dot.