Skip to content
This repository was archived by the owner on Sep 6, 2021. It is now read-only.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ src/extensions/disabled

# unit test working directory
test/temp
test/results
test/results

# Netbeans
/nbproject
3 changes: 1 addition & 2 deletions src/document/DocumentManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -952,8 +952,7 @@ define(function (require, exports, module) {
*/
Document.prototype._updateLanguage = function () {
var oldLanguage = this.language;
var ext = PathUtils.filenameExtension(this.file.fullPath);
this.language = LanguageManager.getLanguageForFileExtension(ext);
this.language = LanguageManager.getLanguageForPath(this.file.fullPath);

if (oldLanguage && oldLanguage !== this.language) {
$(this).triggerHandler("languageChanged", [oldLanguage, this.language]);
Expand Down
4 changes: 2 additions & 2 deletions src/editor/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -1214,7 +1214,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 LanguageManager#getLanguageForFileExtension()} and {@link Language#getMode()}.
* See {@link LanguageManager#getLanguageForPath()} and {@link Language#getMode()}.
*/
Editor.prototype.getModeForSelection = function () {
// Check for mixed mode info
Expand Down Expand Up @@ -1247,7 +1247,7 @@ define(function (require, exports, module) {
/**
* Gets the syntax-highlighting mode for the document.
*
* @return {Object|String} Object or Name of syntax-highlighting mode; see {@link LanguageManager#getLanguageForFileExtension()} and {@link Language#getMode()}.
* @return {Object|String} Object or Name of syntax-highlighting mode; see {@link LanguageManager#getLanguageForPath()} and {@link Language#getMode()}.
*/
Editor.prototype.getModeForDocument = function () {
return this._codeMirror.getOption("mode");
Expand Down
2 changes: 1 addition & 1 deletion src/extensions/default/JavaScriptCodeHints/ScopeManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ define(function (require, exports, module) {
file = split.file;

if (file.indexOf(".") > 1) { // ignore /.dotfiles
var mode = LanguageManager.getLanguageForFileExtension(entry.fullPath).getMode();
var mode = LanguageManager.getLanguageForPath(entry.fullPath).getMode();
if (mode === HintUtils.MODE_NAME) {
DocumentManager.getDocumentForPath(path).done(function (document) {
refreshOuterScope(dir, file, document.getText());
Expand Down
71 changes: 61 additions & 10 deletions src/language/LanguageManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ define(function (require, exports, module) {
_pendingLanguages = {},
_languages = {},
_fileExtensionToLanguageMap = {},
_fileNameToLanguageMap = {},
_modeToLanguageMap = {},
_ready;

Expand Down Expand Up @@ -178,16 +179,19 @@ define(function (require, exports, module) {
}

/**
* Resolves a file extension to a Language object.
* @param {!string} path Path to or extension of the file to find a language for
* Resolves a file path to a Language object.
* @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 getLanguageForFileExtension(path) {
function getLanguageForPath(path) {
var extension = _normalizeFileExtension(PathUtils.filenameExtension(path)),
language = _fileExtensionToLanguageMap[extension];
filename = PathUtils.filename(path),
language = extension ? _fileExtensionToLanguageMap[extension]
: _fileNameToLanguageMap[filename];

if (!language) {
console.log("Called LanguageManager.getLanguageForFileExtension with an unhandled file extension:", extension);
extension ? console.log("Called LanguageManager.getLanguageForPath with an unhandled file extension:", extension)
: console.log("Called LanguageManager.getLanguageForPath with an unhandled file name:", filename);
}

return language || _fallbackLanguage;
Expand Down Expand Up @@ -233,6 +237,7 @@ define(function (require, exports, module) {
this._name = name;

this._fileExtensions = [];
this._fileNames = [];
this._modeToLanguageMap = {};
}

Expand All @@ -249,6 +254,9 @@ define(function (require, exports, module) {
/** @type {Array.<string>} File extensions that use this language */
Language.prototype._fileExtensions = null;

/** @type {Array.<string>} File names for extensionless files that use this language */
Language.prototype._fileNames = null;

/** @type {{ prefix: string }} Line comment syntax */
Language.prototype._lineCommentSyntax = null;

Expand Down Expand Up @@ -351,6 +359,15 @@ define(function (require, exports, module) {
// Use concat to create a copy of this array, preventing external modification
return this._fileExtensions.concat();
};

/**
* Returns an array of file names for extensionless files that use this language.
* @return {Array.<string>} Extensionless file names used by this language
*/
Language.prototype.getFileNames = function () {
// Use concat to create a copy of this array, preventing external modification
return this._fileNames.concat();
};

/**
* Adds a file extension to this language.
Expand Down Expand Up @@ -379,6 +396,31 @@ 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) {
if (this._fileNames.indexOf(name) === -1) {
this._fileNames.push(name);

var language = _fileNameToLanguageMap[name];
if (language) {
console.warn("Cannot register file name \"" + name + "\" for " + this._name + ", it already belongs to " + language._name);
} else {
_fileNameToLanguageMap[name] = this;

// TODO (issue #2966) Allow extensions to add new file names to existing languages
// Notify on the Language and on LanguageManager?
// $(this).triggerHandler("fileNameAdded", [name]);
// $(exports).triggerHandler("fileNameAdded", [name, this]);
}
}
};

/**
* Returns whether the line comment syntax is defined for this language.
* @return {boolean} Whether line comments are supported
Expand Down Expand Up @@ -504,6 +546,8 @@ define(function (require, exports, module) {

var language = new Language(id, definition.name),
fileExtensions = definition.fileExtensions,
fileNames = definition.fileNames,
l,
i;

var blockComment = definition.blockComment;
Expand All @@ -522,10 +566,17 @@ define(function (require, exports, module) {
language._loadAndSetMode(definition.mode).done(function () {
// register language file extensions after mode has loaded
if (fileExtensions) {
for (i = 0; i < fileExtensions.length; i++) {
for (i = 0, l = fileExtensions.length; i < l; i++) {
language._addFileExtension(fileExtensions[i]);
}
}

// register language file names after mode has loaded
if (fileNames) {
for (i = 0, l = fileNames.length; i < l; i++) {
language._addFileName(fileNames[i]);
}
}

// globally associate mode to language
_setLanguageForMode(language.getMode(), language);
Expand Down Expand Up @@ -581,8 +632,8 @@ define(function (require, exports, module) {
});

// Public methods
exports.ready = _ready;
exports.defineLanguage = defineLanguage;
exports.getLanguage = getLanguage;
exports.getLanguageForFileExtension = getLanguageForFileExtension;
exports.ready = _ready;
exports.defineLanguage = defineLanguage;
exports.getLanguage = getLanguage;
exports.getLanguageForPath = getLanguageForPath;
});
9 changes: 5 additions & 4 deletions src/language/languages.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"xml": {
"name": "XML",
"mode": "xml",
"fileExtensions": ["svg", "xml", "wxs", "wxl"],
"fileExtensions": ["svg", "xml", "wxs", "wxl", "wsdl", "rss", "atom", "rdf", "xslt", "xul", "xbl", "mathml"],
Copy link
Member

Choose a reason for hiding this comment

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

Not sure we need to do anything about this now, but it occurs to me that we don't have a good story for how an extension would offer improved support for these non-generic forms of XML. If the generic "xml" language has already claimed that file extension, no one can add a new language for it.

So for example, if I wanted to make an extension that offers offers tag & attribute hinting for SVG I'd ideally be able to un-register ".svg" from the "xml" language and declare a new "svg" language to take over it. (For code hinting specifically, you could do hacks to avoid defining a new language, but ultimately that approach will probably hit a wall).

This is relevant here since the more specific XML formats we add to our generic XML language, the more likely this is to become an issue. But as with my SVG example above, it's possible to hit this issue even on current master.

Copy link
Contributor

Choose a reason for hiding this comment

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

You're absolutely right. I wonder whether we'd want extensions to be able to simply add a file extension, and have them remove the file extension from some other language, or whether extensions have to prove some kind of awareness of the issue, like calling a special replaceFileExtension method somewhere, or having to provide a language from which they want to take a file extension, to indicate that this is a conscious specialization of a file extension.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. Started a new issue around this. Lets move the discussion there: Issue #3044

"blockComment": ["<!--", "-->"]
},

Expand Down Expand Up @@ -85,7 +85,8 @@
"coffeescript": {
"name": "CoffeeScript",
"mode": "coffeescript",
"fileExtensions": ["coffee"]
"fileExtensions": ["coffee", "cf", "cson"],
"fileNames": ["Cakefile"]
},

"clojure": {
Expand All @@ -103,7 +104,7 @@
"ruby": {
"name": "Ruby",
"mode": "ruby",
"fileExtensions": ["rb"]
"fileExtensions": ["rb", "ru", "gemspec", "rake"]
},

"python": {
Expand Down Expand Up @@ -134,7 +135,7 @@
"name": "Diff",
"mode": "diff",
"fileExtensions": ["diff", "patch"]
},
},

"markdown": {
"name": "Markdown",
Expand Down
8 changes: 4 additions & 4 deletions test/spec/Editor-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,25 +117,25 @@ define(function (require, exports, module) {

it("should switch to the HTML mode for files ending in .html", function () {
// verify editor content
var mode = LanguageManager.getLanguageForFileExtension("file:///only/testing/the/path.html").getMode();
var mode = LanguageManager.getLanguageForPath("file:///only/testing/the/path.html").getMode();
expect(mode).toSpecifyModeNamed("text/x-brackets-html");
});

it("should switch modes even if the url has a query string", function () {
// verify editor content
var mode = LanguageManager.getLanguageForFileExtension("http://only.org/testing/the/path.css?v=2").getMode();
var mode = LanguageManager.getLanguageForPath("http://only.org/testing/the/path.css?v=2").getMode();
expect(mode).toSpecifyModeNamed(langNames.css.mode);
});

it("should accept just a file name too", function () {
// verify editor content
var mode = LanguageManager.getLanguageForFileExtension("path.js").getMode();
var mode = LanguageManager.getLanguageForPath("path.js").getMode();
expect(mode).toSpecifyModeNamed(langNames.javascript.mode);
});

it("should default to plain text for unknown file extensions", function () {
// verify editor content
var mode = LanguageManager.getLanguageForFileExtension("test.foo").getMode();
var mode = LanguageManager.getLanguageForPath("test.foo").getMode();

// "unknown" mode uses it's MIME type instead
expect(mode).toBe("text/plain");
Expand Down
14 changes: 7 additions & 7 deletions test/spec/LanguageManager-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ define(function (require, exports, module) {
var html = LanguageManager.getLanguage("html"),
unknown = LanguageManager.getLanguage("unknown");

expect(LanguageManager.getLanguageForFileExtension("foo.html")).toBe(html);
expect(LanguageManager.getLanguageForFileExtension("INDEX.HTML")).toBe(html);
expect(LanguageManager.getLanguageForFileExtension("foo.doesNotExist")).toBe(unknown);
expect(LanguageManager.getLanguageForPath("foo.html")).toBe(html);
expect(LanguageManager.getLanguageForPath("INDEX.HTML")).toBe(html);
expect(LanguageManager.getLanguageForPath("foo.doesNotExist")).toBe(unknown);
});

});
Expand Down Expand Up @@ -176,7 +176,7 @@ define(function (require, exports, module) {
}, "The language should be resolved", 50);

runs(function () {
expect(LanguageManager.getLanguageForFileExtension("file.p")).toBe(language);
expect(LanguageManager.getLanguageForPath("file.p")).toBe(language);
validateLanguage(def, language);
});
});
Expand All @@ -202,8 +202,8 @@ define(function (require, exports, module) {

runs(function () {
expect(xmlBefore).toBe(xmlAfter);
expect(LanguageManager.getLanguageForFileExtension("file.wix")).toBe(lang);
expect(LanguageManager.getLanguageForFileExtension("file.xml")).toBe(xmlAfter);
expect(LanguageManager.getLanguageForPath("file.wix")).toBe(lang);
expect(LanguageManager.getLanguageForPath("file.xml")).toBe(xmlAfter);

validateLanguage(def, lang);
});
Expand Down Expand Up @@ -272,7 +272,7 @@ define(function (require, exports, module) {
}, "The language should be resolved", 50);

runs(function () {
expect(LanguageManager.getLanguageForFileExtension("file.erlang")).toBe(language);
expect(LanguageManager.getLanguageForPath("file.erlang")).toBe(language);
validateLanguage(def, language);
});

Expand Down