Skip to content
This repository was archived by the owner on Sep 6, 2021. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 4 additions & 13 deletions src/document/Document.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ define(function (require, exports, module) {
*
* __deleted__ -- When the file for this document has been deleted. All views onto the document should
* be closed. The document will no longer be editable or dispatch "change" events.
*
* __languageChanged__ -- When the value of getLanguage() has changed. 2nd argument is the old value,
* 3rd argument is the new value.
*
* @constructor
* @param {!File} file Need not lie within the project.
Expand Down Expand Up @@ -672,19 +675,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
39 changes: 31 additions & 8 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,14 @@ 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) {
var label = _.escape(StringUtils.format(Strings.STATUSBAR_SET_DEFAULT_LANG, FileUtils.getSmartFileExtension(document.file.fullPath)));
return { html: label, enabled: document.getLanguage() !== defaultLang };
}

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 +375,22 @@ 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);
fullPath = document.file.fullPath;

if (lang === LANGUAGE_SET_AS_DEFAULT) {
// Set file's current language in preferences as a file extension override (only enabled if not default already)
var fileExtensionMap = PreferencesManager.get("language.fileExtensions");
Copy link
Contributor

Choose a reason for hiding this comment

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

This is why I can't get this to work for me. You need to handle undefined as a return value from PreferenecesManager.get()

Uncaught TypeError: Cannot set property 'jst' of undefined EditorStatusBar.js:385

Copy link
Member Author

Choose a reason for hiding this comment

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

Yikes, good catch indeed,

@dangoor The docs for LanguageManager.definePreference() make it appear as if the initial value arg is required, so is the bug in this case that LanguageManager didn't set the default for "language.fileExtensions" to {}? (I found only a couple more cases throughout the codebase where a default isn't explicitly specified).

Copy link
Contributor

Choose a reason for hiding this comment

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

You're right. What LanguageManager does is less than ideal because _updateFromPrefs does:

        var newMapping = PreferencesManager.get(pref) || {},

rather than just having a default set via definePreference.

fileExtensionMap[FileUtils.getSmartFileExtension(fullPath)] = document.getLanguage().getId();
PreferencesManager.set("language.fileExtensions", fileExtensionMap);
Copy link
Contributor

Choose a reason for hiding this comment

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

"language.fileExtensions" was a declared constant _EXTENSION_MAP_PREF in LanguageManager.js -- we should probably use that here as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

or actually, why isn't this functionality just an API on language manager?

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 guess that's an interesting architecture philosophy question here (CC @dangoor :-) ...should prefs be considered an encapsulated, private detail of a module (hidden behind programmatic APIs)? Or are they a public API in themselves, since any user can touch them too?

Personally, I'd lean toward the latter. Preferences have to be treated much like a frozen API, because we document them publicly and we need backwards compatibility with existing user .json files. Also, prefs definers need to listen to changes in the underlying prefs since users can edit the file at any time -- and making programmatic changes all go through an API might subtly encourage forgetting to handle the external-change case properly.

Copy link
Contributor

Choose a reason for hiding this comment

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

that sounds reasonable. It's just interesting the design of languageManger encapsulates so much of the language management features -- even the prefs seem to be defined for internal use only. The constants are even declared as "private" as if we shouldn't use them outside of the module.

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 semi-private consts LanguageManager uses for those two prefs keys do seem a little odd -- almost everywhere else, our code just uses string literals...

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, this makes sense philosophically: prefs are fundamentally public. I vaguely remember creating some pref or view state value that had a leading underscore, but otherwise they are values that need to have some level of compatibility guarantee just like a public API.


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

$statusOverwrite.on("click", _updateEditorOverwriteMode);
Expand Down
2 changes: 1 addition & 1 deletion src/extensibility/Package.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ define(function (require, exports, module) {
NodeConnection = require("utils/NodeConnection"),
PreferencesManager = require("preferences/PreferencesManager");

PreferencesManager.definePreference("proxy", "string");
PreferencesManager.definePreference("proxy", "string", undefined);

var Errors = {
ERROR_LOADING: "ERROR_LOADING",
Expand Down
2 changes: 1 addition & 1 deletion src/extensions/default/JSLint/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ define(function (require, exports, module) {
*/
var _lastRunOptions;

prefs.definePreference("options", "object")
prefs.definePreference("options", "object", undefined)
.on("change", function (e, data) {
var options = prefs.get("options");
if (!_.isEqual(options, _lastRunOptions)) {
Expand Down
14 changes: 7 additions & 7 deletions src/extensions/default/JavaScriptCodeHints/ScopeManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -1036,9 +1036,9 @@ define(function (require, exports, module) {
/**
* Do the work to initialize a code hinting session.
*
* @param {Session} session - the active hinting session
* @param {Document} document - the document the editor has changed to
* @param {Document} previousDocument - the document the editor has changed from
* @param {Session} session - the active hinting session (TODO: currently unused)
* @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 @@ -1136,9 +1136,9 @@ define(function (require, exports, module) {
/**
* Called each time a new editor becomes active.
*
* @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 {Session} session - the active hinting session (TODO: currently unused by doEditorChange())
* @param {!Document} document - the document of the editor that has changed
* @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
28 changes: 12 additions & 16 deletions src/extensions/default/JavaScriptCodeHints/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -563,8 +563,8 @@ define(function (require, exports, module) {
* When the editor is changed, reset the hinting session and cached
* information, and reject any pending deferred requests.
*
* @param {Editor} editor - editor context to be initialized.
* @param {Editor} previousEditor - the previous editor.
* @param {!Editor} editor - editor context to be initialized.
* @param {?Editor} previousEditor - the previous editor.
*/
function initializeSession(editor, previousEditor) {
session = new Session(editor);
Expand All @@ -575,11 +575,11 @@ define(function (require, exports, module) {
}

/*
* Install editor change listeners
* Connects to the given editor, creating a new Session & adding listeners
*
* @param {Editor} editor - editor context on which to listen for
* changes
* @param {Editor} previousEditor - the previous editor
* @param {?Editor} editor - editor context on which to listen for
* changes. If null, 'session' is cleared.
* @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
80 changes: 47 additions & 33 deletions src/language/LanguageManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@
* 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";
Expand Down Expand Up @@ -227,28 +233,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 +244,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 +369,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 @@ -989,7 +1006,7 @@ define(function (require, exports, module) {
* }
*/
function _updateFromPrefs(pref) {
var newMapping = PreferencesManager.get(pref) || {},
var newMapping = PreferencesManager.get(pref),
newNames = Object.keys(newMapping),
state = _prefState[pref],
last = state.last,
Expand Down Expand Up @@ -1086,25 +1103,21 @@ define(function (require, exports, module) {
// depends on FileUtils) here. Using the async form of require fixes this.
require(["preferences/PreferencesManager"], function (pm) {
PreferencesManager = pm;
_updateFromPrefs(_EXTENSION_MAP_PREF);
_updateFromPrefs(_NAME_MAP_PREF);
pm.definePreference(_EXTENSION_MAP_PREF, "object").on("change", function () {
pm.definePreference(_EXTENSION_MAP_PREF, "object", {}).on("change", function () {
_updateFromPrefs(_EXTENSION_MAP_PREF);
});
pm.definePreference(_NAME_MAP_PREF, "object").on("change", function () {
pm.definePreference(_NAME_MAP_PREF, "object", {}).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._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 +1126,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
Loading