Skip to content
This repository has been archived by the owner on Sep 6, 2021. It is now read-only.

Language switcher: connect to prefs & clean up some code #8444

Merged
merged 5 commits into from
Aug 6, 2014
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 1 addition & 13 deletions src/document/Document.js
Original file line number Diff line number Diff line change
Expand Up @@ -672,19 +672,7 @@ define(function (require, exports, module) {
};

/**
* 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. If the current
* language was forced (set manually by user), don't change it.
* Updates the language to match the current mapping given by LanguageManager
Copy link
Contributor

Choose a reason for hiding this comment

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

The original Language Switcher pr went through and we didn't update the events in Document.js -- specifically the LanguageChanged event (which is a past-tense event name and we normally use current-tense event names)

Actually it appears that there are NO events documented for document objects for some reason. is there a reason why we aren't documenting them? Are they documented somewhere else? I couldn't find them documented anywhere.

We also should notify lint extension authors that they should update their extensions to listen to the "languageChanged" event since most of them leave cruft behind after changing the language of a javascript file to "text". We might want to change it to "languageChange" to match the rest of the events if no one is using it yet as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

The "languageChanged" event was added much earlier than the 0.42 language switcher PR -- it dates back to Sprint 21 (c00bfb1), so I'd rather not change it as part of this diff.

There are some events documented for Document (see docs on the constructor at the top of Document.js), but not this one. I'll add it.

*/
Document.prototype._updateLanguage = function () {
var oldLanguage = this.language;
Expand Down
37 changes: 31 additions & 6 deletions src/editor/EditorStatusBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ define(function (require, exports, module) {
DropdownButton = require("widgets/DropdownButton").DropdownButton,
EditorManager = require("editor/EditorManager"),
Editor = require("editor/Editor").Editor,
FileUtils = require("file/FileUtils"),
KeyEvent = require("utils/KeyEvent"),
LanguageManager = require("language/LanguageManager"),
PreferencesManager = require("preferences/PreferencesManager"),
StatusBar = require("widgets/StatusBar"),
Strings = require("strings"),
StringUtils = require("utils/StringUtils");
Expand All @@ -53,6 +55,9 @@ define(function (require, exports, module) {
$indentWidthInput,
$statusOverwrite;

/** Special list item for the 'set as default' gesture in language switcher dropdown */
var LANGUAGE_SET_AS_DEFAULT = {};


/**
* Determine string based on count
Expand Down Expand Up @@ -300,6 +305,9 @@ define(function (require, exports, module) {

languageSelect.items = languages;

// Add option to top of menu for persisting the override
languageSelect.items.unshift("---");
languageSelect.items.unshift(LANGUAGE_SET_AS_DEFAULT);
}

/**
Expand All @@ -316,8 +324,13 @@ define(function (require, exports, module) {

languageSelect = new DropdownButton("", [], function (item, index) {
var document = EditorManager.getActiveEditor().document,
defaultLang = LanguageManager.getLanguageForPath(document.file.fullPath, true),
html = _.escape(item.getName());
defaultLang = LanguageManager.getLanguageForPath(document.file.fullPath, true);

if (item === LANGUAGE_SET_AS_DEFAULT) {
return _.escape(StringUtils.format(Strings.STATUSBAR_SET_DEFAULT_LANG, FileUtils.getSmartFileExtension(document.file.fullPath)));
}

var html = _.escape(item.getName());

// Show indicators for currently selected & default languages for the current file
if (item === defaultLang) {
Expand Down Expand Up @@ -361,13 +374,25 @@ define(function (require, exports, module) {
$indentWidthInput.focus(function () { $indentWidthInput.select(); });

// Language select change handler
$(languageSelect).on("select", function (e, lang, index) {
$(languageSelect).on("select", function (e, lang) {
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);

if (lang === LANGUAGE_SET_AS_DEFAULT) {
lang = document.getLanguage();
if (lang !== defaultLang) {
// Set file's current language in preferences as a file extension override
var fileExtensionMap = PreferencesManager.get("language.fileExtensions");
fileExtensionMap[FileUtils.getSmartFileExtension(fullPath)] = lang.getId();
PreferencesManager.set("language.fileExtensions", fileExtensionMap);
}

} else {
// Set selected language as a path override for just this one file (not persisted)
// if default language selected, pass null to clear the override
LanguageManager.setLanguageOverrideForPath(fullPath, lang === defaultLang ? null : lang);
}
});

$statusOverwrite.on("click", _updateEditorOverwriteMode);
Expand Down
8 changes: 4 additions & 4 deletions src/extensions/default/JavaScriptCodeHints/ScopeManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -1037,8 +1037,8 @@ define(function (require, exports, module) {
* Do the work to initialize a code hinting session.
*
* @param {Session} session - the active hinting session
Copy link
Contributor

Choose a reason for hiding this comment

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

Session can't be null here either

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, actually it looks entirely unused...

Copy link
Contributor

Choose a reason for hiding this comment

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

we should make that note in the doc and type it as optional

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd rather not explicitly indicate it's ok to write code which passes null for that arg, unless we've verified it's correct for it to be ignoring the arg :-) And that seems outside the scope of this PR -- but I'll add a TODO to draw attention to it.

* @param {Document} document - the document the editor has changed to
* @param {Document} previousDocument - the document the editor has changed from
* @param {!Document} document - the document the editor has changed to
* @param {?Document} previousDocument - the document the editor has changed from
*/
function doEditorChange(session, document, previousDocument) {
var file = document.file,
Expand Down Expand Up @@ -1138,7 +1138,7 @@ define(function (require, exports, module) {
*
* @param {Session} session - the active hinting session
* @param {Document} document - the document of the editor that has changed
Copy link
Contributor

Choose a reason for hiding this comment

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

document and session are non-nullable as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure about session since the caller never uses it, but document should be, yep.

* @param {Document} previousDocument - the document of the editor is changing from
* @param {?Document} previousDocument - the document of the editor is changing from
*/
function handleEditorChange(session, document, previousDocument) {
if (addFilesPromise === null) {
Expand Down Expand Up @@ -1376,7 +1376,7 @@ define(function (require, exports, module) {
*
* @param {Session} session - the active hinting session
* @param {Document} document - the document of the editor that has changed
* @param {Document} previousDocument - the document of the editor is changing from
* @param {?Document} previousDocument - the document of the editor is changing from
*/
function handleEditorChange(session, document, previousDocument) {

Expand Down
20 changes: 8 additions & 12 deletions src/extensions/default/JavaScriptCodeHints/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ define(function (require, exports, module) {
* information, and reject any pending deferred requests.
*
* @param {Editor} editor - editor context to be initialized.
Copy link
Contributor

Choose a reason for hiding this comment

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

editor is required here

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

* @param {Editor} previousEditor - the previous editor.
* @param {?Editor} previousEditor - the previous editor.
*/
function initializeSession(editor, previousEditor) {
session = new Session(editor);
Expand All @@ -579,7 +579,7 @@ define(function (require, exports, module) {
*
* @param {Editor} editor - editor context on which to listen for
Copy link
Contributor

Choose a reason for hiding this comment

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

This can also be null in the case that all editors are destroyed (files close all, etc...) it will still reset the cache hint context and do nothing more

Copy link
Member Author

Choose a reason for hiding this comment

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

Will fix

* changes
* @param {Editor} previousEditor - the previous editor
* @param {?Editor} previousEditor - the previous editor
*/
function installEditorListeners(editor, previousEditor) {
// always clean up cached scope and hint info
Expand Down Expand Up @@ -624,18 +624,21 @@ 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) {
// Uninstall "languageChanged" event listeners on previous editor's document & put them on current editor's doc
if (previous) {
Copy link
Member Author

Choose a reason for hiding this comment

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

previous !== current is guaranteed to be true for all "activeEditorChange" events, so I removed that check

$(previous.document)
.off(HintUtils.eventName("languageChanged"));
}
if (current && current.document !== DocumentManager.getCurrentDocument()) {
if (current) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Removing the second check here is what allows us to remove the "currentDocumentLanguageChanged" listener further below, since this code now covers that case too.

$(current.document)
.on(HintUtils.eventName("languageChanged"), function () {
// If current doc's language changed, reset our state by treating it as if the user switched to a
// different document altogether
uninstallEditorListeners(current);
installEditorListeners(current);
});
}

uninstallEditorListeners(previous);
installEditorListeners(current, previous);
}
Expand Down Expand Up @@ -803,13 +806,6 @@ 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();
});
Expand Down
64 changes: 36 additions & 28 deletions src/language/LanguageManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,28 +227,6 @@ 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.
* File names have a higher priority than file extensions.
Expand All @@ -260,7 +238,9 @@ define(function (require, exports, module) {
}

/**
* Resolves a language to a file extension
* 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
*/
Expand Down Expand Up @@ -383,6 +363,37 @@ define(function (require, exports, module) {
$(exports).triggerHandler("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 = {};
}




/**
* Model for a language.
Expand Down Expand Up @@ -1100,11 +1111,7 @@ define(function (require, exports, module) {
// Private for unit tests
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;
exports._resetPathLanguageOverrides = _resetPathLanguageOverrides;

// Public methods
exports.ready = _ready;
Expand All @@ -1113,4 +1120,5 @@ define(function (require, exports, module) {
exports.getLanguageForExtension = getLanguageForExtension;
exports.getLanguageForPath = getLanguageForPath;
exports.getLanguages = getLanguages;
exports.setLanguageOverrideForPath = setLanguageOverrideForPath;
});
1 change: 1 addition & 0 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ define({
"STATUSBAR_INSERT" : "INS",
"STATUSBAR_OVERWRITE" : "OVR",
"STATUSBAR_DEFAULT_LANG" : "(default)",
"STATUSBAR_SET_DEFAULT_LANG" : "Set as Default for .{0} Files",

// CodeInspection: errors/warnings
"ERRORS_PANEL_TITLE_MULTIPLE" : "{0} Problems",
Expand Down
7 changes: 4 additions & 3 deletions src/project/ProjectManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -1117,8 +1117,8 @@ define(function (require, exports, module) {
}

/**
* Loads the given folder as a project. Normally, you would call openProject() instead to let the
* user choose a folder.
* Loads the given folder as a project. Does NOT prompt about any unsaved changes - use openProject()
* instead to check for unsaved changes and (optionally) let the user choose the folder to open.
*
* @param {!string} rootPath Absolute path to the root folder of the project.
* A trailing "/" on the path is optional (unlike many Brackets APIs that assume a trailing "/").
Expand Down Expand Up @@ -1155,8 +1155,9 @@ define(function (require, exports, module) {
DocumentManager.closeAll();

_unwatchProjectRoot().always(function () {
// Done closing old project (if any)
// Finish closing old project (if any)
if (_projectRoot) {
LanguageManager._resetPathLanguageOverrides();
PreferencesManager._reloadUserPrefs(_projectRoot);
$(exports).triggerHandler("projectClose", _projectRoot);
}
Expand Down
4 changes: 3 additions & 1 deletion src/widgets/DropdownButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ define(function (require, exports, module) {
* - "select" - when an option in the dropdown is clicked. Passed item object and index.
*
* @param {!string} label Label to display on the button
* @param {!Array.<*>} items Items in the dropdown list
* @param {!Array.<*>} items Items in the dropdown list. It generally doesn't matter what type/value the
* items have, except that any item === "---" will be treated as a divider. Such items are not
* clickable and itemRenderer() will not be called for them.
* @param {?function(*, number):!string} itemRenderer Optional function to convert a single item to HTML
* (see itemRenderer() docs below). If not provided, items are assumed to be plain text strings.
*/
Expand Down
14 changes: 7 additions & 7 deletions test/spec/LanguageManager-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ define(function (require, exports, module) {
});

afterEach(function () {
LanguageManager._resetLanguageOverrides();
LanguageManager._resetPathLanguageOverrides();
});

function defineLanguage(definition) {
Expand Down Expand Up @@ -544,7 +544,7 @@ define(function (require, exports, module) {

var renameDeferred = $.Deferred();
runs(function () {
DocumentManager.setCurrentDocument(doc);
DocumentManager.setCurrentDocument(doc);
javascript = LanguageManager.getLanguage("javascript");

// sanity check language
Expand Down Expand Up @@ -705,7 +705,7 @@ define(function (require, exports, module) {
// make active
doc.addRef();

doc.setLanguageOverride(phpLang);
LanguageManager.setLanguageOverrideForPath(doc.file.fullPath, phpLang);

// language should change
expect(doc.getLanguage()).toBe(phpLang);
Expand Down Expand Up @@ -747,7 +747,7 @@ define(function (require, exports, module) {
// make active
doc.addRef();

doc.setLanguageOverride(phpLang);
LanguageManager.setLanguageOverrideForPath(doc.file.fullPath, phpLang);

// language should change
expect(doc.getLanguage()).toBe(phpLang);
Expand All @@ -756,7 +756,7 @@ define(function (require, exports, module) {
expect(spy.mostRecentCall.args[2]).toBe(phpLang);
expect(LanguageManager.getLanguageForPath(doc.file.fullPath)).toBe(phpLang);

doc.setLanguageOverride(null);
LanguageManager.setLanguageOverrideForPath(doc.file.fullPath, null);

// language should revert
expect(doc.getLanguage()).toBe(unknownLang);
Expand All @@ -777,7 +777,7 @@ define(function (require, exports, module) {
expect(LanguageManager.getLanguageForPath(doc.file.fullPath)).toBe(modifiedLanguage);

// override again
doc.setLanguageOverride(phpLang);
LanguageManager.setLanguageOverrideForPath(doc.file.fullPath, phpLang);

expect(doc.getLanguage()).toBe(phpLang);
expect(spy.callCount).toBe(4);
Expand All @@ -786,7 +786,7 @@ define(function (require, exports, module) {
expect(LanguageManager.getLanguageForPath(doc.file.fullPath)).toBe(phpLang);

// remove override, should restore to modifiedLanguage
doc.setLanguageOverride(null);
LanguageManager.setLanguageOverrideForPath(doc.file.fullPath, null);

expect(doc.getLanguage()).toBe(modifiedLanguage);
expect(spy.callCount).toBe(5);
Expand Down