diff --git a/src/brackets.js b/src/brackets.js index 4af5ad6eb15..7c24c5fc4a8 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -400,14 +400,15 @@ define(function (require, exports, module) { $("html").on("mousedown", ".no-focus", function (e) { // Text fields should always be focusable. var $target = $(e.target), - isTextField = + isFormElement = $target.is("input[type=text]") || $target.is("input[type=number]") || $target.is("input[type=password]") || $target.is("input:not([type])") || // input with no type attribute defaults to text - $target.is("textarea"); + $target.is("textarea") || + $target.is("select"); - if (!isTextField) { + if (!isFormElement) { e.preventDefault(); } }); diff --git a/src/document/Document.js b/src/document/Document.js index 1a48c53be81..47e46dc30f7 100644 --- a/src/document/Document.js +++ b/src/document/Document.js @@ -156,7 +156,7 @@ define(function (require, exports, module) { * @type {FileUtils.LINE_ENDINGS_CRLF|FileUtils.LINE_ENDINGS_LF} */ Document.prototype._lineEndings = null; - + /** Add a ref to keep this Document alive */ Document.prototype.addRef = function () { //console.log("+++REF+++ "+this); @@ -670,14 +670,25 @@ define(function (require, exports, module) { Document.prototype.getLanguage = function () { return this.language; }; + + /** + * Overrides the default language of this document and sets it to the given + * language. This change is not persisted if the document is closed. + * @param {?Language} language The language to be set for this document; if + * null, the language will be set back to the default. + */ + Document.prototype.setLanguageOverride = function (language) { + LanguageManager._setLanguageOverrideForPath(this.file.fullPath, language); + this._updateLanguage(); + }; /** - * Updates the language according to the file extension + * Updates the language according to the file extension. If the current + * language was forced (set manually by user), don't change it. */ Document.prototype._updateLanguage = function () { var oldLanguage = this.language; this.language = LanguageManager.getLanguageForPath(this.file.fullPath); - if (oldLanguage && oldLanguage !== this.language) { $(this).triggerHandler("languageChanged", [oldLanguage, this.language]); } @@ -698,7 +709,6 @@ define(function (require, exports, module) { return this.file instanceof InMemoryFile; }; - // Define public API exports.Document = Document; }); diff --git a/src/document/DocumentManager.js b/src/document/DocumentManager.js index 26aa238155b..ca4ecedd354 100644 --- a/src/document/DocumentManager.js +++ b/src/document/DocumentManager.js @@ -474,9 +474,13 @@ define(function (require, exports, module) { if (_currentDocument === doc) { return; } - + var perfTimerName = PerfUtils.markStart("setCurrentDocument:\t" + doc.file.fullPath); + if (_currentDocument) { + $(_currentDocument).off("languageChanged.DocumentManager"); + } + // If file is untitled or otherwise not within project tree, add it to // working set right now (don't wait for it to become dirty) if (doc.isUntitled() || !ProjectManager.isWithinProject(doc.file.fullPath)) { @@ -491,6 +495,12 @@ define(function (require, exports, module) { // Make it the current document var previousDocument = _currentDocument; _currentDocument = doc; + + // Proxy this doc's languageChange events as long as it's current + $(_currentDocument).on("languageChanged.DocumentManager", function (data) { + $(exports).trigger("currentDocumentLanguageChanged", data); + }); + $(exports).triggerHandler("currentDocumentChange", [_currentDocument, previousDocument]); // (this event triggers EditorManager to actually switch editors in the UI) diff --git a/src/editor/EditorStatusBar.js b/src/editor/EditorStatusBar.js index a76a3847c7f..81ad39de5c9 100644 --- a/src/editor/EditorStatusBar.js +++ b/src/editor/EditorStatusBar.js @@ -32,17 +32,20 @@ define(function (require, exports, module) { "use strict"; // Load dependent modules - var AppInit = require("utils/AppInit"), - AnimationUtils = require("utils/AnimationUtils"), - EditorManager = require("editor/EditorManager"), - Editor = require("editor/Editor").Editor, - KeyEvent = require("utils/KeyEvent"), - StatusBar = require("widgets/StatusBar"), - Strings = require("strings"), - StringUtils = require("utils/StringUtils"); + var _ = require("thirdparty/lodash"), + AnimationUtils = require("utils/AnimationUtils"), + AppInit = require("utils/AppInit"), + DropdownButton = require("widgets/DropdownButton").DropdownButton, + EditorManager = require("editor/EditorManager"), + Editor = require("editor/Editor").Editor, + KeyEvent = require("utils/KeyEvent"), + LanguageManager = require("language/LanguageManager"), + StatusBar = require("widgets/StatusBar"), + Strings = require("strings"), + StringUtils = require("utils/StringUtils"); /* StatusBar indicators */ - var $languageInfo, + var languageSelect, // this is a DropdownButton instance $cursorInfo, $fileInfo, $indentType, @@ -67,7 +70,15 @@ define(function (require, exports, module) { * @param {Editor} editor Current editor */ function _updateLanguageInfo(editor) { - $languageInfo.text(editor.document.getLanguage().getName()); + var doc = editor.document, + lang = doc.getLanguage(); + + // Ensure width isn't left locked by a previous click of the dropdown (which may not have resulted in a "change" event at the time) + languageSelect.$button.css("width", "auto"); + // Setting Untitled documents to non-text mode isn't supported yet, so disable the switcher in that case for now + languageSelect.$button.prop("disabled", doc.isUntitled()); + // Show the current language as button title + languageSelect.$button.text(lang.getName()); } /** @@ -260,7 +271,9 @@ define(function (require, exports, module) { $(current).on("overwriteToggle.statusbar", _updateOverwriteLabel); current.document.addRef(); - $(current.document).on("languageChanged.statusbar", function () { _updateLanguageInfo(current); }); + $(current.document).on("languageChanged.statusbar", function () { + _updateLanguageInfo(current); + }); _updateCursorInfo(null, current); _updateLanguageInfo(current); @@ -271,11 +284,29 @@ define(function (require, exports, module) { } } + /** + * Populate the languageSelect DropdownButton's menu with all registered Languages + */ + function _populateLanguageDropdown() { + // Get all non-binary languages + var languages = _.values(LanguageManager.getLanguages()).filter(function (language) { + return !language.isBinary(); + }); + + // sort dropdown alphabetically + languages.sort(function (a, b) { + return a.getName().toLowerCase().localeCompare(b.getName().toLowerCase()); + }); + + languageSelect.items = languages; + + } + /** * Initialize */ function _init() { - $languageInfo = $("#status-language"); + $cursorInfo = $("#status-cursor"); $fileInfo = $("#status-file"); $indentType = $("#indent-type"); @@ -283,6 +314,25 @@ define(function (require, exports, module) { $indentWidthInput = $("#indent-width-input"); $statusOverwrite = $("#status-overwrite"); + languageSelect = new DropdownButton("", [], function (item, index) { + var document = EditorManager.getActiveEditor().document, + defaultLang = LanguageManager.getLanguageForPath(document.file.fullPath, true), + html = _.escape(item.getName()); + + // Show indicators for currently selected & default languages for the current file + if (item === defaultLang) { + html += " " + Strings.STATUSBAR_DEFAULT_LANG + ""; + } + if (item === document.getLanguage()) { + html = "" + html; + } + return html; + }); + + languageSelect.dropdownExtraClasses = "dropdown-status-bar"; + languageSelect.$button.addClass("btn-status-bar"); + $("#status-language").append(languageSelect.$button); + // indentation event handlers $indentType.on("click", _toggleIndentType); $indentWidthLabel @@ -310,6 +360,16 @@ define(function (require, exports, module) { $indentWidthInput.focus(function () { $indentWidthInput.select(); }); + // Language select change handler + $(languageSelect).on("select", function (e, lang, index) { + var document = EditorManager.getActiveEditor().document, + fullPath = document.file.fullPath, + defaultLang = LanguageManager.getLanguageForPath(fullPath, true); + // if default language selected, don't "force" it + // (passing in null will reset the force flag) + document.setLanguageOverride(lang === defaultLang ? null : lang); + }); + $statusOverwrite.on("click", _updateEditorOverwriteMode); _onActiveEditorChange(null, EditorManager.getActiveEditor(), null); @@ -319,4 +379,5 @@ define(function (require, exports, module) { $(EditorManager).on("activeEditorChange", _onActiveEditorChange); AppInit.htmlReady(_init); + AppInit.appReady(_populateLanguageDropdown); }); diff --git a/src/extensions/default/JavaScriptCodeHints/main.js b/src/extensions/default/JavaScriptCodeHints/main.js index bc188c56192..1aea34e5f46 100644 --- a/src/extensions/default/JavaScriptCodeHints/main.js +++ b/src/extensions/default/JavaScriptCodeHints/main.js @@ -34,12 +34,12 @@ define(function (require, exports, module) { DocumentManager = brackets.getModule("document/DocumentManager"), Commands = brackets.getModule("command/Commands"), CommandManager = brackets.getModule("command/CommandManager"), + LanguageManager = brackets.getModule("language/LanguageManager"), Menus = brackets.getModule("command/Menus"), AppInit = brackets.getModule("utils/AppInit"), ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), PerfUtils = brackets.getModule("utils/PerfUtils"), StringMatch = brackets.getModule("utils/StringMatch"), - LanguageManager = brackets.getModule("language/LanguageManager"), ProjectManager = brackets.getModule("project/ProjectManager"), PreferencesManager = brackets.getModule("preferences/PreferencesManager"), ParameterHintManager = require("ParameterHintManager"), @@ -308,8 +308,7 @@ define(function (require, exports, module) { * @return {boolean} - true if the document is a html file */ function isHTMLFile(document) { - var languageID = LanguageManager.getLanguageForPath(document.file.fullPath).getId(); - return languageID === "html"; + return LanguageManager.getLanguageForPath(document.file.fullPath).getId() === "html"; } function isInlineScript(editor) { @@ -596,7 +595,6 @@ define(function (require, exports, module) { } ignoreChange = false; }); - ParameterHintManager.installListeners(editor); } else { session = null; @@ -626,6 +624,18 @@ define(function (require, exports, module) { * @param {Editor} previous - the previous editor context */ function handleActiveEditorChange(event, current, previous) { + // Uninstall "languageChanged" event listeners on the previous editor's document + if (previous && previous !== current) { + $(previous.document) + .off(HintUtils.eventName("languageChanged")); + } + if (current && current.document !== DocumentManager.getCurrentDocument()) { + $(current.document) + .on(HintUtils.eventName("languageChanged"), function () { + uninstallEditorListeners(current); + installEditorListeners(current); + }); + } uninstallEditorListeners(previous); installEditorListeners(current, previous); } @@ -793,6 +803,13 @@ define(function (require, exports, module) { .on(HintUtils.eventName("activeEditorChange"), handleActiveEditorChange); + $(DocumentManager) + .on("currentDocumentLanguageChanged", function (e) { + var activeEditor = EditorManager.getActiveEditor(); + uninstallEditorListeners(activeEditor); + installEditorListeners(activeEditor); + }); + $(ProjectManager).on("beforeProjectClose", function () { ScopeManager.handleProjectClose(); }); diff --git a/src/language/CodeInspection.js b/src/language/CodeInspection.js index ffc035ba0ea..a17bb3561df 100644 --- a/src/language/CodeInspection.js +++ b/src/language/CodeInspection.js @@ -152,7 +152,7 @@ define(function (require, exports, module) { function _unregisterAll() { _providers = {}; } - + /** * Returns a list of provider for given file path, if available. * Decision is made depending on the file extension. @@ -474,7 +474,7 @@ define(function (require, exports, module) { if (_enabled) { // register our event listeners $(DocumentManager) - .on("currentDocumentChange.codeInspection", function () { + .on("currentDocumentChange.codeInspection currentDocumentLanguageChanged.codeInspection", function () { run(); }) .on("documentSaved.codeInspection documentRefreshed.codeInspection", function (event, document) { diff --git a/src/language/LanguageManager.js b/src/language/LanguageManager.js index a39ca7d0b27..ca66cdb05b3 100644 --- a/src/language/LanguageManager.js +++ b/src/language/LanguageManager.js @@ -142,6 +142,7 @@ define(function (require, exports, module) { _baseFileExtensionToLanguageMap = {}, _fileExtensionToLanguageMap = Object.create(_baseFileExtensionToLanguageMap), _fileNameToLanguageMap = {}, + _filePathToLanguageMap = {}, _modeToLanguageMap = {}, _ready; @@ -225,6 +226,28 @@ define(function (require, exports, module) { _modeToLanguageMap[mode] = language; } + + /** + * Adds a language mapping for the specified fullPath. If language is falsy (null or undefined), the mapping + * is removed. + * + * @param {!fullPath} fullPath absolute path of the file + * @param {?object} language language to associate the file with or falsy value to remove the existing mapping + */ + function _setLanguageOverrideForPath(fullPath, language) { + if (!language) { + delete _filePathToLanguageMap[fullPath]; + } else { + _filePathToLanguageMap[fullPath] = language; + } + } + + /** + * Resets all the language overrides for file paths. Used by unit tests only. + */ + function _resetLanguageOverrides() { + _filePathToLanguageMap = {}; + } /** * Resolves a language ID to a Language object. @@ -248,14 +271,25 @@ define(function (require, exports, module) { /** * 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) { - var fileName = FileUtils.getBaseName(path).toLowerCase(), - language = _fileNameToLanguageMap[fileName], + 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: @@ -302,6 +336,17 @@ define(function (require, exports, module) { 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.} 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 @@ -1053,13 +1098,19 @@ define(function (require, exports, module) { }); // Private for unit tests - exports._EXTENSION_MAP_PREF = _EXTENSION_MAP_PREF; - exports._NAME_MAP_PREF = _NAME_MAP_PREF; + exports._EXTENSION_MAP_PREF = _EXTENSION_MAP_PREF; + exports._NAME_MAP_PREF = _NAME_MAP_PREF; + exports._resetLanguageOverrides = _resetLanguageOverrides; + // Internal use only + // _setLanguageOverrideForPath is used by Document to help LanguageManager keeping track of + // in-document language overrides + exports._setLanguageOverrideForPath = _setLanguageOverrideForPath; // Public methods - exports.ready = _ready; - exports.defineLanguage = defineLanguage; - exports.getLanguage = getLanguage; - exports.getLanguageForExtension = getLanguageForExtension; - exports.getLanguageForPath = getLanguageForPath; + exports.ready = _ready; + exports.defineLanguage = defineLanguage; + exports.getLanguage = getLanguage; + exports.getLanguageForExtension = getLanguageForExtension; + exports.getLanguageForPath = getLanguageForPath; + exports.getLanguages = getLanguages; }); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 59c168e377a..1d315472b08 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -246,6 +246,7 @@ define({ "STATUSBAR_USER_EXTENSIONS_DISABLED" : "Extensions Disabled", "STATUSBAR_INSERT" : "INS", "STATUSBAR_OVERWRITE" : "OVR", + "STATUSBAR_DEFAULT_LANG" : "(default)", // CodeInspection: errors/warnings "ERRORS_PANEL_TITLE_MULTIPLE" : "{0} Problems", diff --git a/src/preferences/PreferencesImpl.js b/src/preferences/PreferencesImpl.js index 0ade1df22a9..89dfae38c98 100644 --- a/src/preferences/PreferencesImpl.js +++ b/src/preferences/PreferencesImpl.js @@ -32,7 +32,6 @@ define(function (require, exports, module) { "use strict"; var PreferencesBase = require("./PreferencesBase"), - ProjectManager = require("project/ProjectManager"), Async = require("utils/Async"), // The SETTINGS_FILENAME is used with a preceding "." within user projects @@ -117,13 +116,13 @@ define(function (require, exports, module) { // Listen for times where we might be unwatching a root that contains one of the user-level prefs files, // and force a re-read of the file in order to ensure we can write to it later (see #7300). - $(ProjectManager).on("projectClose", function (event, rootDir) { + function _reloadUserPrefs(rootDir) { var prefsDir = brackets.app.getApplicationSupportDirectory() + "/"; if (prefsDir.indexOf(rootDir.fullPath) === 0) { manager.fileChanged(userPrefFile); stateManager.fileChanged(userStateFile); } - }); + } // Semi-Public API. Use this at your own risk. The public API is in PreferencesManager. @@ -137,6 +136,7 @@ define(function (require, exports, module) { exports.userPrefFile = userPrefFile; exports.isUserScopeCorrupt = isUserScopeCorrupt; exports.managerReady = _prefManagerReadyDeferred.promise(); + exports.reloadUserPrefs = _reloadUserPrefs; exports.STATE_FILENAME = STATE_FILENAME; exports.SETTINGS_FILENAME = SETTINGS_FILENAME; -}); \ No newline at end of file +}); diff --git a/src/preferences/PreferencesManager.js b/src/preferences/PreferencesManager.js index 901c64cf9d0..4505a946498 100644 --- a/src/preferences/PreferencesManager.js +++ b/src/preferences/PreferencesManager.js @@ -561,6 +561,7 @@ define(function (require, exports, module) { exports._setProjectSettingsFile = _setProjectSettingsFile; exports._smUserScopeLoading = PreferencesImpl.smUserScopeLoading; exports._stateProjectLayer = PreferencesImpl.stateProjectLayer; + exports._reloadUserPrefs = PreferencesImpl.reloadUserPrefs; // Public API diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 709783a75cb..97fba36e307 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1157,6 +1157,7 @@ define(function (require, exports, module) { _unwatchProjectRoot().always(function () { // Done closing old project (if any) if (_projectRoot) { + PreferencesManager._reloadUserPrefs(_projectRoot); $(exports).triggerHandler("projectClose", _projectRoot); } diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 7337af5b76f..dde521cf89e 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -171,7 +171,33 @@ a, img { #status-language { border-right: 1px solid rgba(0, 0, 0, 0.1); + padding: 0px; + margin: 0px; } + + /* dropdown button styling */ + .btn-status-bar { + border: 0; + background-color: inherit; + color: inherit; + font: inherit; + height: inherit; + line-height: inherit; + margin: 0; + padding: 0 20px 0 10px; + vertical-align: top; + width: auto; + cursor: pointer; + &:focus { + outline: 0; + } + &[disabled] { + cursor: inherit; + background: none; + text-decoration: none; + } + } + } #status-indent > * { diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less index 84f3cdd5aa4..86ce5925ddd 100644 --- a/src/styles/brackets_patterns_override.less +++ b/src/styles/brackets_patterns_override.less @@ -514,10 +514,33 @@ a:focus { &.dropdown-menu a:not(.selected):hover { background: none; } - + .divider { margin: 5px 1px; } + +} + +/* Status bar language picker's DropdownButton */ +.dropdownbutton-popup.dropdown-status-bar { + height: auto; + max-height: 80%; + + // Improve how bottom of the dropdown joins with top of status bar button + margin-top: -6px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + + li a .default-language { + font-style: italic; + color: @tc-quiet-text; + } + + li a .checked-language::before { + content: "✓"; + margin-left: -11px; + margin-right: 3px; + } } /* Inline editor stylesheet-picker DropdownButton */ diff --git a/src/styles/images/select-triangles.svg b/src/styles/images/select-triangles.svg new file mode 100644 index 00000000000..751ca6e998f --- /dev/null +++ b/src/styles/images/select-triangles.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/widgets/DropdownButton.js b/src/widgets/DropdownButton.js index baf074e7aca..d18904c6d71 100644 --- a/src/widgets/DropdownButton.js +++ b/src/widgets/DropdownButton.js @@ -228,7 +228,8 @@ define(function (require, exports, module) { Menus.closeAll(); var $dropdown = $("