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 = $("