src/language/LanguageManager.js
/*
* Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*unittests: LanguageManager*/
/**
* LanguageManager provides access to the languages supported by Brackets
*
* To find out which languages we support by default, have a look at languages.json.
*
* To get access to an existing language, call getLanguage():
*
* var language = LanguageManager.getLanguage("<id>");
*
* To define your own languages, call defineLanguage():
*
* LanguageManager.defineLanguage("haskell", {
* name: "Haskell",
* mode: "haskell",
* fileExtensions: ["hs"],
* blockComment: ["{-", "-}"],
* lineComment: ["--"]
* });
*
* To use that language and its related mode, wait for the returned promise to be resolved:
*
* LanguageManager.defineLanguage("haskell", definition).done(function (language) {
* 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",
* mode: ["null", "text/plain"],
* fileNames: ["Makefile"]
* });
*
* You can combine file names and extensions, or not define them at all.
*
* You can also refine an existing language:
*
* var language = LanguageManager.getLanguage("haskell");
* language.setLineCommentSyntax(["--"]);
* language.setBlockCommentSyntax("{-", "-}");
* language.addFileExtension("lhs");
*
* Some CodeMirror modes define variations of themselves. They are called MIME modes.
* To find existing MIME modes, search for "CodeMirror.defineMIME" in thirdparty/CodeMirror/mode
* For instance, C++, C# and Java all use the clike (C-like) mode with different settings and a different MIME name.
* You can refine the mode definition by specifying the MIME mode as well:
*
* LanguageManager.defineLanguage("csharp", {
* name: "C#",
* mode: ["clike", "text/x-csharp"],
* ...
* });
*
* Defining the base mode is still necessary to know which file to load.
* However, language.getMode() will return just the MIME mode if one was
* specified.
*
* If you need to configure a mode, you can just create a new MIME mode and use that:
*
* CodeMirror.defineMIME("text/x-brackets-html", {
* "name": "htmlmixed",
* "scriptTypes": [{"matches": /\/x-handlebars-template|\/x-mustache/i,
* "mode": null}]
* });
*
* LanguageManager.defineLanguage("html", {
* name: "HTML",
* mode: ["htmlmixed", "text/x-brackets-html"],
* ...
* });
*
* If a mode is not shipped with our CodeMirror distribution, you need to first load it yourself.
* If the mode is part of our CodeMirror distribution, it gets loaded automatically.
*
* You can also defines binary file types, i.e. Brackets supports image files by default,
* such as *.jpg, *.png etc.
* Binary files do not require mode because modes are specific to CodeMirror, which
* only handles text based file types.
* To register a binary language the isBinary flag must be set, i.e.
*
* LanguageManager.defineLanguage("audio", {
* name: "Audio",
* fileExtensions: ["mp3", "wav", "aif", "aiff", "ogg"],
* isBinary: true
* });
*
*
* LanguageManager dispatches two events:
*
* - languageAdded -- When any new Language is added. 2nd arg is the new Language.
* - languageModified -- When the attributes of a Language change, or when the Language gains or loses
* file extension / filename mappings. 2nd arg is the modified Language.
*/
define(function (require, exports, module) {
"use strict";
// Dependencies
var CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"),
EventDispatcher = require("utils/EventDispatcher"),
Async = require("utils/Async"),
FileUtils = require("file/FileUtils"),
Strings = require("strings"),
_defaultLanguagesJSON = require("text!language/languages.json"),
_ = require("thirdparty/lodash"),
// PreferencesManager is loaded near the end of the file
PreferencesManager;
// State
var _fallbackLanguage = null,
_pendingLanguages = {},
_languages = {},
_baseFileExtensionToLanguageMap = {},
_fileExtensionToLanguageMap = Object.create(_baseFileExtensionToLanguageMap),
_fileNameToLanguageMap = {},
_filePathToLanguageMap = {},
_modeToLanguageMap = {},
_ready;
// Constants
var _EXTENSION_MAP_PREF = "language.fileExtensions",
_NAME_MAP_PREF = "language.fileNames";
// Tracking for changes to mappings made by preferences
var _prefState = {};
_prefState[_EXTENSION_MAP_PREF] = {
last: {},
overridden: {},
add: "addFileExtension",
remove: "removeFileExtension",
get: "getLanguageForExtension"
};
_prefState[_NAME_MAP_PREF] = {
last: {},
overridden: {},
add: "addFileName",
remove: "removeFileName",
get: "getLanguageForPath"
};
// Helper functions
/**
* Checks whether value is a non-empty string. Reports an error otherwise.
* If no deferred is passed, console.error is called.
* Otherwise the deferred is rejected with the error message.
* @param {*} value The value to validate
* @param {!string} description A helpful identifier for value
* @param {?jQuery.Deferred} deferred A deferred to reject with the error message in case of an error
* @return {boolean} True if the value is a non-empty string, false otherwise
*/
function _validateNonEmptyString(value, description, deferred) {
var reportError = deferred ? deferred.reject : console.error;
// http://stackoverflow.com/questions/1303646/check-whether-variable-is-number-or-string-in-javascript
if (Object.prototype.toString.call(value) !== "[object String]") {
reportError(description + " must be a string");
return false;
}
if (value === "") {
reportError(description + " must not be empty");
return false;
}
return true;
}
/**
* Monkey-patch CodeMirror to prevent modes from being overwritten by extensions.
* We may rely on the tokens provided by some of these modes.
*/
function _patchCodeMirror() {
var _original_CodeMirror_defineMode = CodeMirror.defineMode;
function _wrapped_CodeMirror_defineMode(name) {
if (CodeMirror.modes[name]) {
console.error("There already is a CodeMirror mode with the name \"" + name + "\"");
return;
}
_original_CodeMirror_defineMode.apply(CodeMirror, arguments);
}
CodeMirror.defineMode = _wrapped_CodeMirror_defineMode;
}
/**
* Adds a global mode-to-language association.
* @param {!string} mode The mode to associate the language with
* @param {!Language} language The language to associate with the mode
*/
function _setLanguageForMode(mode, language) {
if (_modeToLanguageMap[mode]) {
console.warn("CodeMirror mode \"" + mode + "\" is already used by language " + _modeToLanguageMap[mode]._name + " - cannot fully register language " + language._name +
" using the same mode. Some features will treat all content with this mode as language " + _modeToLanguageMap[mode]._name);
return;
}
_modeToLanguageMap[mode] = language;
}
/**
* Resolves a language ID to a Language object.
* File names have a higher priority than file extensions.
* @param {!string} id Identifier for this language: lowercase letters, digits, and _ separators (e.g. "cpp", "foo_bar", "c99")
* @return {Language} The language with the provided identifier or undefined
*/
function getLanguage(id) {
return _languages[id];
}
/**
* Resolves a file extension to a Language object.
* *Warning:* it is almost always better to use getLanguageForPath(), since Language can depend
* on file name and even full path. Use this API only if no relevant file/path exists.
* @param {!string} extension Extension that language should be resolved for
* @return {?Language} The language for the provided extension or null if none exists
*/
function getLanguageForExtension(extension) {
return _fileExtensionToLanguageMap[extension.toLowerCase()];
}
/**
* Resolves a file path to a Language object.
* @param {!string} path Path to the file to find a language for
* @param {?boolean} ignoreOverride If set to true will cause the lookup to ignore any
* overrides and return default binding. By default override is not ignored.
*
* @return {Language} The language for the provided file type or the fallback language
*/
function getLanguageForPath(path, ignoreOverride) {
var fileName,
language = _filePathToLanguageMap[path],
extension,
parts;
// if there's an override, return it
if (!ignoreOverride && language) {
return language;
}
fileName = FileUtils.getBaseName(path).toLowerCase();
language = _fileNameToLanguageMap[fileName];
// If no language was found for the file name, use the file extension instead
if (!language) {
// Split the file name into parts:
// "foo.coffee.md" => ["foo", "coffee", "md"]
// ".profile.bak" => ["", "profile", "bak"]
// "1. Vacation.txt" => ["1", " Vacation", "txt"]
parts = fileName.split(".");
// A leading dot does not indicate a file extension, but marks the file as hidden => remove it
if (parts[0] === "") {
// ["", "profile", "bak"] => ["profile", "bak"]
parts.shift();
}
// The first part is assumed to be the title, not the extension => remove it
// ["foo", "coffee", "md"] => ["coffee", "md"]
// ["profile", "bak"] => ["bak"]
// ["1", " Vacation", "txt"] => [" Vacation", "txt"]
parts.shift();
// Join the remaining parts into a file extension until none are left or a language was found
while (!language && parts.length) {
// First iteration:
// ["coffee", "md"] => "coffee.md"
// ["bak"] => "bak"
// [" Vacation", "txt"] => " Vacation.txt"
// Second iteration (assuming no language was found for "coffee.md"):
// ["md"] => "md"
// ["txt"] => "txt"
extension = parts.join(".");
language = _fileExtensionToLanguageMap[extension];
// Remove the first part
// First iteration:
// ["coffee", "md"] => ["md"]
// ["bak"] => []
// [" Vacation", "txt"] => ["txt"]
// Second iteration:
// ["md"] => []
// ["txt"] => []
parts.shift();
}
}
return language || _fallbackLanguage;
}
/**
* Returns a map of all the languages currently defined in the LanguageManager. The key to
* the map is the language id and the value is the language object.
*
* @return {Object.<string, Language>} A map containing all of the
* languages currently defined.
*/
function getLanguages() {
return $.extend({}, _languages); // copy to prevent modification
}
/**
* Resolves a CodeMirror mode to a Language object.
* @param {!string} mode CodeMirror mode
* @return {Language} The language for the provided mode or the fallback language
*/
function _getLanguageForMode(mode) {
var language = _modeToLanguageMap[mode];
if (language) {
return language;
}
// In case of unsupported languages
console.log("Called LanguageManager._getLanguageForMode with a mode for which no language has been registered:", mode);
return _fallbackLanguage;
}
/**
* @private
* Notify listeners when a language is added
* @param {!Language} language The new language
*/
function _triggerLanguageAdded(language) {
// finally, store language to _language map
_languages[language.getId()] = language;
exports.trigger("languageAdded", language);
}
/**
* @private
* Notify listeners when a language is modified
* @param {!Language} language The modified language
*/
function _triggerLanguageModified(language) {
exports.trigger("languageModified", language);
}
/**
* Adds a language mapping for the specified fullPath. If language is falsy (null or undefined), the mapping
* is removed. The override is NOT persisted across Brackets sessions.
*
* @param {!fullPath} fullPath absolute path of the file
* @param {?object} language language to associate the file with or falsy value to remove any existing override
*/
function setLanguageOverrideForPath(fullPath, language) {
var oldLang = getLanguageForPath(fullPath);
if (!language) {
delete _filePathToLanguageMap[fullPath];
} else {
_filePathToLanguageMap[fullPath] = language;
}
var newLang = getLanguageForPath(fullPath);
// Old language changed since this path is no longer mapped to it
_triggerLanguageModified(oldLang);
// New language changed since a path is now mapped to it that wasn't before
_triggerLanguageModified(newLang);
}
/**
* Resets all the language overrides for file paths. Used by unit tests only.
*/
function _resetPathLanguageOverrides() {
_filePathToLanguageMap = {};
}
/**
* Get the file extension (excluding ".") given a path OR a bare filename.
* Returns "" for names with no extension.
* If the only `.` in the file is the first character,
* returns "" as this is not considered an extension.
* This method considers known extensions which include `.` in them.
*
* @param {string} fullPath full path to a file or directory
* @return {string} Returns the extension of a filename or empty string if
* the argument is a directory or a filename with no extension
*/
function getCompoundFileExtension(fullPath) {
var baseName = FileUtils.getBaseName(fullPath),
parts = baseName.split(".");
// get rid of file name
parts.shift();
if (baseName[0] === ".") {
// if starts with a `.`, then still consider it as file name
parts.shift();
}
var extension = [parts.pop()], // last part is always an extension
i = parts.length;
while (i--) {
if (getLanguageForExtension(parts[i])) {
extension.unshift(parts[i]);
} else {
break;
}
}
return extension.join(".");
}
/**
* Model for a language.
* @constructor
*/
function Language() {
this._fileExtensions = [];
this._fileNames = [];
this._modeToLanguageMap = {};
this._lineCommentSyntax = [];
}
/**
* Identifier for this language
* @type {string}
*/
Language.prototype._id = null;
/**
* Human-readable name of this language
* @type {string}
*/
Language.prototype._name = null;
/**
* CodeMirror mode for this language
* @type {string}
*/
Language.prototype._mode = null;
/**
* File extensions that use this language
* @type {Array.<string>}
*/
Language.prototype._fileExtensions = null;
/**
* File names for extensionless files that use this language
* @type {Array.<string>}
*/
Language.prototype._fileNames = null;
/**
* Line comment syntax
* @type {Array.<string>}
*/
Language.prototype._lineCommentSyntax = null;
/**
* Which language to use for what CodeMirror mode
* @type {Object.<string,Language>}
*/
Language.prototype._modeToLanguageMap = null;
/**
* Block comment syntax
* @type {{ prefix: string, suffix: string }}
*/
Language.prototype._blockCommentSyntax = null;
/**
* Whether or not the language is binary
* @type {boolean}
*/
Language.prototype._isBinary = false;
/**
* Returns the identifier for this language.
* @return {string} The identifier
*/
Language.prototype.getId = function () {
return this._id;
};
/**
* Sets the identifier for this language or prints an error to the console.
* @param {!string} id Identifier for this language: lowercase letters, digits, and _ separators (e.g. "cpp", "foo_bar", "c99")
* @return {boolean} Whether the ID was valid and set or not
*/
Language.prototype._setId = function (id) {
if (!_validateNonEmptyString(id, "Language ID")) {
return false;
}
// Make sure the ID is a string that can safely be used universally by the computer - as a file name, as an object key, as part of a URL, etc.
// Hence we use "_" instead of "." since the latter often has special meaning
if (!id.match(/^[a-z0-9]+(_[a-z0-9]+)*$/)) {
console.error("Invalid language ID \"" + id + "\": Only groups of lower case letters and numbers are allowed, separated by underscores.");
return false;
}
this._id = id;
return true;
};
/**
* Returns the human-readable name of this language.
* @return {string} The name
*/
Language.prototype.getName = function () {
return this._name;
};
/**
* Sets the human-readable name of this language or prints an error to the console.
* @param {!string} name Human-readable name of the language, as it's commonly referred to (e.g. "C++")
* @return {boolean} Whether the name was valid and set or not
*/
Language.prototype._setName = function (name) {
if (!_validateNonEmptyString(name, "name")) {
return false;
}
this._name = name;
return true;
};
/**
* Returns the CodeMirror mode for this language.
* @return {string} The mode
*/
Language.prototype.getMode = function () {
return this._mode;
};
/**
* Loads a mode and sets it for this language.
*
* @param {(string|Array.<string>)} mode CodeMirror mode (e.g. "htmlmixed"), optionally paired with a MIME mode defined by
* that mode (e.g. ["clike", "text/x-c++src"]). Unless the mode is located in thirdparty/CodeMirror/mode/<name>/<name>.js,
* you need to first load it yourself.
* @return {$.Promise} A promise object that will be resolved when the mode is loaded and set
*/
Language.prototype._loadAndSetMode = function (mode) {
var result = new $.Deferred(),
self = this,
mimeMode; // Mode can be an array specifying a mode plus a MIME mode defined by that mode ["clike", "text/x-c++src"]
if (Array.isArray(mode)) {
if (mode.length !== 2) {
result.reject("Mode must either be a string or an array containing two strings");
return result.promise();
}
mimeMode = mode[1];
mode = mode[0];
}
// mode must not be empty. Use "null" (the string "null") mode for plain text
if (!_validateNonEmptyString(mode, "mode", result)) {
result.reject();
return result.promise();
}
var finish = function () {
if (!CodeMirror.modes[mode]) {
result.reject("CodeMirror mode \"" + mode + "\" is not loaded");
return;
}
if (mimeMode) {
var modeConfig = CodeMirror.mimeModes[mimeMode];
if (!modeConfig) {
result.reject("CodeMirror MIME mode \"" + mimeMode + "\" not found");
return;
}
}
// This mode is now only about what to tell CodeMirror
// The base mode was only necessary to load the proper mode file
self._mode = mimeMode || mode;
self._wasModified();
result.resolve(self);
};
if (CodeMirror.modes[mode]) {
finish();
} else {
require(["thirdparty/CodeMirror/mode/" + mode + "/" + mode], finish);
}
return result.promise();
};
/**
* Returns an array of file extensions for this language.
* @return {Array.<string>} File extensions used by this language
*/
Language.prototype.getFileExtensions = function () {
// 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 one or more file extensions to this language.
* @param {!string|Array.<string>} extension A file extension (or array thereof) used by this language
*/
Language.prototype.addFileExtension = function (extension) {
if (Array.isArray(extension)) {
extension.forEach(this._addFileExtension.bind(this));
} else {
this._addFileExtension(extension);
}
};
Language.prototype._addFileExtension = function (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);
var language = _fileExtensionToLanguageMap[extension];
if (language) {
console.warn("Cannot register file extension \"" + extension + "\" for " + this._name + ", it already belongs to " + language._name);
} else {
_fileExtensionToLanguageMap[extension] = this;
}
this._wasModified();
} else if(!_fileExtensionToLanguageMap[extension]) {
// Language should be in the extension map but isn't
_fileExtensionToLanguageMap[extension] = this;
this._wasModified();
}
};
/**
* Unregisters one or more file extensions from this language.
* @param {!string|Array.<string>} extension File extension (or array thereof) to stop using for this language
*/
Language.prototype.removeFileExtension = function (extension) {
if (Array.isArray(extension)) {
extension.forEach(this._removeFileExtension.bind(this));
} else {
this._removeFileExtension(extension);
}
};
Language.prototype._removeFileExtension = function (extension) {
// Remove a leading dot if present
if (extension.charAt(0) === ".") {
extension = extension.substr(1);
}
// Make checks below case-INsensitive
extension = extension.toLowerCase();
var index = this._fileExtensions.indexOf(extension);
if (index !== -1) {
this._fileExtensions.splice(index, 1);
delete _fileExtensionToLanguageMap[extension];
this._wasModified();
}
};
/**
* Adds one or more file names to the language which is used to match files that don't have extensions like "Makefile" for example.
* @param {!string|Array.<string>} extension An extensionless file name (or array thereof) used by this language
*/
Language.prototype.addFileName = function (name) {
if (Array.isArray(name)) {
name.forEach(this._addFileName.bind(this));
} else {
this._addFileName(name);
}
};
Language.prototype._addFileName = function (name) {
// Make checks below case-INsensitive
name = name.toLowerCase();
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;
}
this._wasModified();
}
};
/**
* Unregisters one or more file names from this language.
* @param {!string|Array.<string>} extension An extensionless file name (or array thereof) used by this language
*/
Language.prototype.removeFileName = function (name) {
if (Array.isArray(name)) {
name.forEach(this._removeFileName.bind(this));
} else {
this._removeFileName(name);
}
};
Language.prototype._removeFileName = function (name) {
// Make checks below case-INsensitive
name = name.toLowerCase();
var index = this._fileNames.indexOf(name);
if (index !== -1) {
this._fileNames.splice(index, 1);
delete _fileNameToLanguageMap[name];
this._wasModified();
}
};
/**
* Returns whether the line comment syntax is defined for this language.
* @return {boolean} Whether line comments are supported
*/
Language.prototype.hasLineCommentSyntax = function () {
return this._lineCommentSyntax.length > 0;
};
/**
* Returns an array of prefixes to use for line comments.
* @return {Array.<string>} The prefixes
*/
Language.prototype.getLineCommentPrefixes = function () {
return this._lineCommentSyntax;
};
/**
* Sets the prefixes to use for line comments in this language or prints an error to the console.
* @param {!(string|Array.<string>)} prefix Prefix string or an array of prefix strings
* to use for line comments (e.g. "//" or ["//", "#"])
* @return {boolean} Whether the syntax was valid and set or not
*/
Language.prototype.setLineCommentSyntax = function (prefix) {
var prefixes = Array.isArray(prefix) ? prefix : [prefix];
var i;
if (prefixes.length) {
this._lineCommentSyntax = [];
for (i = 0; i < prefixes.length; i++) {
_validateNonEmptyString(String(prefixes[i]), Array.isArray(prefix) ? "prefix[" + i + "]" : "prefix");
this._lineCommentSyntax.push(prefixes[i]);
}
this._wasModified();
} else {
console.error("The prefix array should not be empty");
}
return true;
};
/**
* Returns whether the block comment syntax is defined for this language.
* @return {boolean} Whether block comments are supported
*/
Language.prototype.hasBlockCommentSyntax = function () {
return Boolean(this._blockCommentSyntax);
};
/**
* Returns the prefix to use for block comments.
* @return {string} The prefix
*/
Language.prototype.getBlockCommentPrefix = function () {
return this._blockCommentSyntax && this._blockCommentSyntax.prefix;
};
/**
* Returns the suffix to use for block comments.
* @return {string} The suffix
*/
Language.prototype.getBlockCommentSuffix = function () {
return this._blockCommentSyntax && this._blockCommentSyntax.suffix;
};
/**
* Sets the prefix and suffix to use for blocks comments in this language or prints an error to the console.
* @param {!string} prefix Prefix string to use for block comments (e.g. "<!--")
* @param {!string} suffix Suffix string to use for block comments (e.g. "-->")
* @return {boolean} Whether the syntax was valid and set or not
*/
Language.prototype.setBlockCommentSyntax = function (prefix, suffix) {
if (!_validateNonEmptyString(prefix, "prefix") || !_validateNonEmptyString(suffix, "suffix")) {
return false;
}
this._blockCommentSyntax = { prefix: prefix, suffix: suffix };
this._wasModified();
return true;
};
/**
* Returns either a language associated with the mode or the fallback language.
* Used to disambiguate modes used by multiple languages.
* @param {!string} mode The mode to associate the language with
* @return {Language} This language if it uses the mode, or whatever {@link #_getLanguageForMode} returns
*/
Language.prototype.getLanguageForMode = function (mode) {
if (mode === this._mode) {
return this;
}
return this._modeToLanguageMap[mode] || _getLanguageForMode(mode);
};
/**
* Overrides a mode-to-language association for this particular language only or prints an error to the console.
* Used to disambiguate modes used by multiple languages.
* @param {!string} mode The mode to associate the language with
* @param {!Language} language The language to associate with the mode
* @return {boolean} Whether the mode-to-language association was valid and set or not
* @private
*/
Language.prototype._setLanguageForMode = function (mode, language) {
if (mode === this._mode && language !== this) {
console.error("A language must always map its mode to itself");
return false;
}
this._modeToLanguageMap[mode] = language;
this._wasModified();
return true;
};
/**
* Determines whether this is the fallback language or not
* @return {boolean} True if this is the fallback language, false otherwise
*/
Language.prototype.isFallbackLanguage = function () {
return this === _fallbackLanguage;
};
/**
* Trigger the "languageModified" event if this language is registered already
* @see #_triggerLanguageModified
* @private
*/
Language.prototype._wasModified = function () {
if (_languages[this._id]) {
_triggerLanguageModified(this);
}
};
/**
* Indicates whether or not the language is binary (e.g., image or audio).
* @return {boolean}
*/
Language.prototype.isBinary = function () {
return this._isBinary;
};
/**
* Sets whether or not the language is binary
* @param {!boolean} isBinary
*/
Language.prototype._setBinary = function (isBinary) {
this._isBinary = isBinary;
};
/**
* Defines a language.
*
* @param {!string} id Unique identifier for this language: lowercase letters, digits, and _ separators (e.g. "cpp", "foo_bar", "c99")
* @param {!Object} definition An object describing the language
* @param {!string} definition.name Human-readable name of the language, as it's commonly referred to (e.g. "C++")
* @param {Array.<string>} definition.fileExtensions List of file extensions used by this language (e.g. ["php", "php3"] or ["coffee.md"] - may contain dots)
* @param {Array.<string>} definition.fileNames List of exact file names (e.g. ["Makefile"] or ["package.json]). Higher precedence than file extension.
* @param {Array.<string>} definition.blockComment Array with two entries defining the block comment prefix and suffix (e.g. ["<!--", "-->"])
* @param {(string|Array.<string>)} definition.lineComment Line comment prefixes (e.g. "//" or ["//", "#"])
* @param {(string|Array.<string>)} definition.mode CodeMirror mode (e.g. "htmlmixed"), optionally with a MIME mode defined by that mode ["clike", "text/x-c++src"]
* Unless the mode is located in thirdparty/CodeMirror/mode/<name>/<name>.js, you need to first load it yourself.
*
* @return {$.Promise} A promise object that will be resolved with a Language object
**/
function defineLanguage(id, definition) {
var result = new $.Deferred();
if (_pendingLanguages[id]) {
result.reject("Language \"" + id + "\" is waiting to be resolved.");
return result.promise();
}
if (_languages[id]) {
result.reject("Language \"" + id + "\" is already defined");
return result.promise();
}
var language = new Language(),
name = definition.name,
fileExtensions = definition.fileExtensions,
fileNames = definition.fileNames,
blockComment = definition.blockComment,
lineComment = definition.lineComment,
i,
l;
function _finishRegisteringLanguage() {
if (fileExtensions) {
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]);
}
}
language._setBinary(!!definition.isBinary);
// store language to language map
_languages[language.getId()] = language;
// restore any preferences for non-default languages
if(PreferencesManager) {
_updateFromPrefs(_EXTENSION_MAP_PREF);
_updateFromPrefs(_NAME_MAP_PREF);
}
}
if (!language._setId(id) || !language._setName(name) ||
(blockComment && !language.setBlockCommentSyntax(blockComment[0], blockComment[1])) ||
(lineComment && !language.setLineCommentSyntax(lineComment))) {
result.reject();
return result.promise();
}
if (definition.isBinary) {
// add file extensions and store language to language map
_finishRegisteringLanguage();
result.resolve(language);
// Not notifying DocumentManager via event LanguageAdded, because DocumentManager
// does not care about binary files.
} else {
// track languages that are currently loading
_pendingLanguages[id] = language;
language._loadAndSetMode(definition.mode).done(function () {
// globally associate mode to language
_setLanguageForMode(language.getMode(), language);
// add file extensions and store language to language map
_finishRegisteringLanguage();
// fire an event to notify DocumentManager of the new language
_triggerLanguageAdded(language);
result.resolve(language);
}).fail(function (error) {
console.error(error);
result.reject(error);
}).always(function () {
// delete from pending languages after success and failure
delete _pendingLanguages[id];
});
}
return result.promise();
}
/**
* @private
*
* If a default file extension or name was overridden by a pref, restore it.
*
* @param {string} name Extension or filename that should be restored
* @param {{overridden: string, add: string}} prefState object for the pref that is currently being updated
*/
function _restoreOverriddenDefault(name, state) {
if (state.overridden[name]) {
var language = getLanguage(state.overridden[name]);
language[state.add](name);
delete state.overridden[name];
}
}
/**
* @private
*
* Updates extension and filename mappings from languages based on the current preferences values.
*
* The preferences look like this in a prefs file:
*
* Map *.foo to javascript, *.vm to html
*
* "language.fileExtensions": {
* "foo": "javascript",
* "vm": "html"
* }
*
* Map "Gemfile" to ruby:
*
* "language.fileNames": {
* "Gemfile": "ruby"
* }
*/
function _updateFromPrefs(pref) {
var newMapping = PreferencesManager.get(pref),
newNames = Object.keys(newMapping),
state = _prefState[pref],
last = state.last,
overridden = state.overridden;
// Look for added and changed names (extensions or filenames)
newNames.forEach(function (name) {
var language;
if (newMapping[name] !== last[name]) {
if (last[name]) {
language = getLanguage(last[name]);
if (language) {
language[state.remove](name);
// If this name that was previously mapped was overriding a default
// restore it now.
_restoreOverriddenDefault(name, state);
}
}
language = exports[state.get](name);
if (language) {
language[state.remove](name);
// We're removing a name that was defined in Brackets or an extension,
// so keep track of how it used to be mapped.
if (!overridden[name]) {
overridden[name] = language.getId();
}
}
language = getLanguage(newMapping[name]);
if (language) {
language[state.add](name);
}
}
if(!getLanguage(newMapping[name])) {
// If the language doesn't exist, restore any overrides and remove it
// from the state.
if(overridden[name]) {
_restoreOverriddenDefault(name, state);
}
delete newMapping[name];
}
});
// Look for removed names (extensions or filenames)
_.difference(Object.keys(last), newNames).forEach(function (name) {
var language = getLanguage(last[name]);
if (language) {
language[state.remove](name);
_restoreOverriddenDefault(name, state);
}
});
state.last = newMapping;
}
EventDispatcher.makeEventDispatcher(exports);
// Prevent modes from being overwritten by extensions
_patchCodeMirror();
// Define a custom MIME mode here instead of putting it directly into languages.json
// because JSON files can't contain regular expressions. Also, all other modes so
// far were strings, so we spare us the trouble of allowing more complex mode values.
CodeMirror.defineMIME("text/x-brackets-html", {
"name": "htmlmixed",
"scriptTypes": [
{
"matches": /\/x-handlebars|\/x-mustache|\/ng-template$|^text\/html$/i,
"mode": "htmlmixed"
},
{
"matches": /^text\/(babel|jsx)$/i,
"mode": "jsx"
}
]
});
// Define SVG MIME type so an SVG language can be defined for SVG-specific code hints.
// Currently, SVG uses XML mode so it has generic XML syntax highlighting. This can
// be removed when SVG gets its own CodeMirror mode with SVG syntax highlighting.
CodeMirror.defineMIME("image/svg+xml", "xml");
// Load the default languages
_defaultLanguagesJSON = JSON.parse(_defaultLanguagesJSON);
_ready = Async.doInParallel(Object.keys(_defaultLanguagesJSON), function (key) {
return defineLanguage(key, _defaultLanguagesJSON[key]);
}, false);
// Get the object for HTML
_ready.always(function () {
var html = getLanguage("html");
// The htmlmixed mode uses the xml mode internally for the HTML parts, so we map it to HTML
html._setLanguageForMode("xml", html);
// Currently we override the above mentioned "xml" in TokenUtils.getModeAt, instead returning "html".
// When the CSSInlineEditor and the hint providers are no longer based on modes, this can be changed.
// But for now, we need to associate this madeup "html" mode with our HTML language object.
_setLanguageForMode("html", html);
// Similarly, the php mode uses clike internally for the PHP parts
var php = getLanguage("php");
php._setLanguageForMode("clike", php);
// Similar hack to the above for dealing with SCSS/CSS.
var scss = getLanguage("scss");
scss._setLanguageForMode("css", scss);
// Map stylus mode to the stylus Brackets language, fixes #13378
var stylus = getLanguage("stylus");
_setLanguageForMode("stylus", stylus);
// The fallback language for unknown modes and file extensions
_fallbackLanguage = getLanguage("unknown");
// There is a circular dependency between FileUtils and LanguageManager which
// was introduced in 254b01e2f2eebea4416026d0f40d017b8ca6dbc9
// and may be preventing us from importing PreferencesManager (which also
// depends on FileUtils) here. Using the async form of require fixes this.
require(["preferences/PreferencesManager"], function (pm) {
PreferencesManager = pm;
pm.definePreference(_EXTENSION_MAP_PREF, "object", {}, {
description: Strings.DESCRIPTION_LANGUAGE_FILE_EXTENSIONS
}).on("change", function () {
_updateFromPrefs(_EXTENSION_MAP_PREF);
});
pm.definePreference(_NAME_MAP_PREF, "object", {}, {
description: Strings.DESCRIPTION_LANGUAGE_FILE_NAMES
}).on("change", function () {
_updateFromPrefs(_NAME_MAP_PREF);
});
_updateFromPrefs(_EXTENSION_MAP_PREF);
_updateFromPrefs(_NAME_MAP_PREF);
});
});
// Private for unit tests
exports._EXTENSION_MAP_PREF = _EXTENSION_MAP_PREF;
exports._NAME_MAP_PREF = _NAME_MAP_PREF;
exports._resetPathLanguageOverrides = _resetPathLanguageOverrides;
// Public methods
exports.ready = _ready;
exports.defineLanguage = defineLanguage;
exports.getLanguage = getLanguage;
exports.getLanguageForExtension = getLanguageForExtension;
exports.getLanguageForPath = getLanguageForPath;
exports.getLanguages = getLanguages;
exports.setLanguageOverrideForPath = setLanguageOverrideForPath;
exports.getCompoundFileExtension = getCompoundFileExtension;
});