/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
release-1.12
src/language/LanguageManager.js
1 206 строк
44 KB
opl-
Add mapping for stylus mode in LanguageManager, fixes #13378 (#13380)
12 сен 2017, 16:37
12 сен 2017, 16:37
8fafdf0
Код
Авторство
О чём код?
/* * 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; });