/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/editor/EditorManager.js
765 строк
31 KB
Subhash Jha
Language Server Protocol Support for Brackets (#14606)
02 апр 2019, 12:35
02 апр 2019, 12:35
df7aea4
Код
Авторство
О чём код?
/* * 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. * */ /** * EditorManager owns the UI for the editor area. This essentially mirrors the 'current document' * property maintained by DocumentManager's model. * * Note that there is a little bit of unusual overlap between EditorManager and DocumentManager: * because the Document state is actually stored in the CodeMirror editor UI, DocumentManager is * not a pure headless model. Each Document encapsulates an editor instance, and thus EditorManager * must have some knowledge about Document's internal state (we access its _editor property). * * This module dispatches the following events: * - activeEditorChange -- Fires after the active editor (full or inline). * * Doesn't fire when editor temporarily loses focus to a non-editor * control (e.g. search toolbar or modal dialog, or window deactivation). * * Does fire when focus moves between inline editor and its full-size container. * * This event tracks `MainViewManagers's `currentFileChange` event and all editor * objects "focus" event. * * (e, editorGainingFocus:editor, editorLosingFocus:editor) * * The 2nd arg to the listener is which Editor became active; the 3rd arg is * which Editor is deactivated as a result. Either one may be null. * NOTE (#1257): `getFocusedEditor()` sometimes lags behind this event. Listeners * should use the arguments or call `getActiveEditor()` to reliably see which Editor * just gained focus. */ define(function (require, exports, module) { "use strict"; // Load dependent modules var Commands = require("command/Commands"), EventDispatcher = require("utils/EventDispatcher"), WorkspaceManager = require("view/WorkspaceManager"), PreferencesManager = require("preferences/PreferencesManager"), CommandManager = require("command/CommandManager"), DocumentManager = require("document/DocumentManager"), MainViewManager = require("view/MainViewManager"), ViewStateManager = require("view/ViewStateManager"), PerfUtils = require("utils/PerfUtils"), Editor = require("editor/Editor").Editor, InlineTextEditor = require("editor/InlineTextEditor").InlineTextEditor, Strings = require("strings"), LanguageManager = require("language/LanguageManager"), DeprecationWarning = require("utils/DeprecationWarning"); /** * Currently focused Editor (full-size, inline, or otherwise) * @type {?Editor} * @private */ var _lastFocusedEditor = null; /** * Registered inline-editor widget providers sorted descending by priority. * @see {@link #registerInlineEditProvider}. * @type {Array.<{priority:number, provider:function(...)}>} * @private */ var _inlineEditProviders = []; /** * Registered inline documentation widget providers sorted descending by priority. * @see {@link #registerInlineDocsProvider}. * @type {Array.<{priority:number, provider:function(...)}>} * @private */ var _inlineDocsProviders = []; /** * DOM element to house any hidden editors created soley for inline widgets * @private * @type {jQuery} */ var _$hiddenEditorsContainer; /** * Retrieves the visible full-size Editor for the currently opened file in the ACTIVE_PANE * @return {?Editor} editor of the current view or null */ function getCurrentFullEditor() { var currentPath = MainViewManager.getCurrentlyViewedPath(MainViewManager.ACTIVE_PANE), doc = currentPath && DocumentManager.getOpenDocumentForPath(currentPath); return doc && doc._masterEditor; } /** * Updates _viewStateCache from the given editor's actual current state * @private * @param {!Editor} editor - editor to cache data for */ function _saveEditorViewState(editor) { ViewStateManager.updateViewState(editor); } /** * Updates _viewStateCache from the given editor's actual current state * @param {!Editor} editor - editor restore cached data * @private */ function _restoreEditorViewState(editor) { // We want to ignore the current state of the editor, so don't call __getViewState() var viewState = ViewStateManager.getViewState(editor.document.file); if (viewState) { editor.restoreViewState(viewState); } } /** * Editor focus handler to change the currently active editor * @private * @param {?Editor} current - the editor that will be the active editor */ function _notifyActiveEditorChanged(current) { // Skip if the Editor that gained focus was already the most recently focused editor. // This may happen e.g. if the window loses then regains focus. if (_lastFocusedEditor === current) { return; } var previous = _lastFocusedEditor; _lastFocusedEditor = current; exports.trigger("activeEditorChange", current, previous); } /** * Current File Changed handler * MainViewManager dispatches a "currentFileChange" event whenever the currently viewed * file changes. Which could mean that the previously viewed file has been closed or a * non-editor view (image) has been given focus. _notifyAcitveEditorChanged is also hooked * up to editor.focus to handle focus events for editors which handles changing focus between * two editors but, because editormanager maintains a "_lastFocusedEditor" state, we have to * "nullify" that state whenever the focus goes to a non-editor or when the current editor is closed * @private * @param {!jQuery.Event} e - event * @param {?File} file - current file (can be null) */ function _handleCurrentFileChange(e, file) { var doc = file && DocumentManager.getOpenDocumentForPath(file.fullPath); _notifyActiveEditorChanged(doc && doc._masterEditor); } /** * Creates a new Editor bound to the given Document. * The editor is appended to the given container as a visible child. * @private * @param {!Document} doc Document for the Editor's content * @param {!boolean} makeMasterEditor If true, the Editor will set itself as the private "master" * Editor for the Document. If false, the Editor will attach to the Document as a "slave." * @param {!jQueryObject} container Container to add the editor to. * @param {{startLine: number, endLine: number}=} range If specified, range of lines within the document * to display in this editor. Inclusive. * @param {!Object} editorOptions If specified, contains editor options that can be passed to CodeMirror * @return {Editor} the newly created editor. */ function _createEditorForDocument(doc, makeMasterEditor, container, range, editorOptions) { var editor = new Editor(doc, makeMasterEditor, container, range, editorOptions); editor.on("focus", function () { _notifyActiveEditorChanged(editor); }); editor.on("beforeDestroy", function () { if (editor.$el.is(":visible")) { _saveEditorViewState(editor); } }); return editor; } /** * @private * Finds an inline widget provider from the given list that can offer a widget for the current cursor * position, and once the widget has been created inserts it into the editor. * * @param {!Editor} editor The host editor * @param {Array.<{priority:number, provider:function(...)}>} providers * prioritized list of providers * @param {string=} defaultErrorMsg Default message to display if no providers return non-null * @return {$.Promise} a promise that will be resolved when an InlineWidget * is created or rejected if no inline providers have offered one. */ function _openInlineWidget(editor, providers, defaultErrorMsg) { PerfUtils.markStart(PerfUtils.INLINE_WIDGET_OPEN); // Run through inline-editor providers until one responds var pos = editor.getCursorPos(), inlinePromise, i, result = new $.Deferred(), errorMsg, providerRet; // Query each provider in priority order. Provider may return: // 1. `null` to indicate it does not apply to current cursor position // 2. promise that should resolve to an InlineWidget // 3. string which indicates provider does apply to current cursor position, // but reason it could not create InlineWidget // // Keep looping until a provider is found. If a provider is not found, // display the highest priority error message that was found, otherwise display // default error message for (i = 0; i < providers.length && !inlinePromise; i++) { var provider = providers[i].provider; providerRet = provider(editor, pos); if (providerRet) { if (providerRet.hasOwnProperty("done")) { inlinePromise = providerRet; } else if (!errorMsg && typeof (providerRet) === "string") { errorMsg = providerRet; } } } // Use default error message if none other provided errorMsg = errorMsg || defaultErrorMsg; // If one of them will provide a widget, show it inline once ready if (inlinePromise) { inlinePromise.done(function (inlineWidget) { editor.addInlineWidget(pos, inlineWidget).done(function () { PerfUtils.addMeasurement(PerfUtils.INLINE_WIDGET_OPEN); result.resolve(); }); }).fail(function () { // terminate timer that was started above PerfUtils.finalizeMeasurement(PerfUtils.INLINE_WIDGET_OPEN); editor.displayErrorMessageAtCursor(errorMsg); result.reject(); }); } else { // terminate timer that was started above PerfUtils.finalizeMeasurement(PerfUtils.INLINE_WIDGET_OPEN); editor.displayErrorMessageAtCursor(errorMsg); result.reject(); } return result.promise(); } /** * Closes any focused inline widget. Else, asynchronously asks providers to create one. * * @param {Array.<{priority:number, provider:function(...)}>} providers * prioritized list of providers * @param {string=} errorMsg Default message to display if no providers return non-null * @return {!Promise} A promise resolved with true if an inline widget is opened or false * when closed. Rejected if there is neither an existing widget to close nor a provider * willing to create a widget (or if no editor is open). */ function _toggleInlineWidget(providers, errorMsg) { var result = new $.Deferred(); var currentEditor = getCurrentFullEditor(); if (currentEditor) { var inlineWidget = currentEditor.getFocusedInlineWidget(); if (inlineWidget) { // an inline widget's editor has focus, so close it PerfUtils.markStart(PerfUtils.INLINE_WIDGET_CLOSE); inlineWidget.close().done(function () { PerfUtils.addMeasurement(PerfUtils.INLINE_WIDGET_CLOSE); // return a resolved promise to CommandManager result.resolve(false); }); } else { // main editor has focus, so create an inline editor _openInlineWidget(currentEditor, providers, errorMsg).done(function () { result.resolve(true); }).fail(function () { result.reject(); }); } } else { // Can not open an inline editor without a host editor result.reject(); } return result.promise(); } /** * Inserts a prioritized provider object into the array in sorted (descending) order. * @private * @param {Array.<{priority:number, provider:function(...)}>} array * @param {number} priority * @param {function(...)} provider */ function _insertProviderSorted(array, provider, priority) { var index, prioritizedProvider = { priority: priority, provider: provider }; for (index = 0; index < array.length; index++) { if (array[index].priority < priority) { break; } } array.splice(index, 0, prioritizedProvider); } /** * Creates a hidden, unattached master editor that is needed when a document is created for the * sole purpose of creating an inline editor so operations that require a master editor can be performed * Only called from Document._ensureMasterEditor() * The editor view is placed in a hidden part of the DOM but can later be moved to a visible pane * when the document is opened using pane.addView() * @param {!Document} doc - document to create a hidden editor for */ function _createUnattachedMasterEditor(doc) { // attach to the hidden containers DOM node if necessary if (!_$hiddenEditorsContainer) { _$hiddenEditorsContainer = $("#hidden-editors"); } // Create an editor var editor = _createEditorForDocument(doc, true, _$hiddenEditorsContainer); // and hide it editor.setVisible(false); } /** * Removes the given widget UI from the given hostEditor (agnostic of what the widget's content * is). The widget's onClosed() callback will be run as a result. * @param {!Editor} hostEditor The editor containing the widget. * @param {!InlineWidget} inlineWidget The inline widget to close. * @return {$.Promise} A promise that's resolved when the widget is fully closed. */ function closeInlineWidget(hostEditor, inlineWidget) { // If widget has focus, return it to the hostEditor & move the cursor to where the inline used to be if (inlineWidget.hasFocus()) { // Place cursor back on the line just above the inline (the line from which it was opened) // If cursor's already on that line, leave it be to preserve column position var widgetLine = hostEditor._codeMirror.getLineNumber(inlineWidget.info.line); var cursorLine = hostEditor.getCursorPos().line; if (cursorLine !== widgetLine) { hostEditor.setCursorPos({ line: widgetLine, pos: 0 }); } hostEditor.focus(); } return hostEditor.removeInlineWidget(inlineWidget); } /** * Registers a new inline editor provider. When Quick Edit is invoked each registered provider is * asked if it wants to provide an inline editor given the current editor and cursor location. * An optional priority parameter is used to give providers with higher priority an opportunity * to provide an inline editor before providers with lower priority. * * @param {function(!Editor, !{line:number, ch:number}):?($.Promise|string)} provider * @param {number=} priority * The provider returns a promise that will be resolved with an InlineWidget, or returns a string * indicating why the provider cannot respond to this case (or returns null to indicate no reason). */ function registerInlineEditProvider(provider, priority) { if (priority === undefined) { priority = 0; } _insertProviderSorted(_inlineEditProviders, provider, priority); } /** * Registers a new inline docs provider. When Quick Docs is invoked each registered provider is * asked if it wants to provide inline docs given the current editor and cursor location. * An optional priority parameter is used to give providers with higher priority an opportunity * to provide an inline editor before providers with lower priority. * * @param {function(!Editor, !{line:number, ch:number}):?($.Promise|string)} provider * @param {number=} priority * The provider returns a promise that will be resolved with an InlineWidget, or returns a string * indicating why the provider cannot respond to this case (or returns null to indicate no reason). */ function registerInlineDocsProvider(provider, priority) { if (priority === undefined) { priority = 0; } _insertProviderSorted(_inlineDocsProviders, provider, priority); } /** * @private * Given a host editor, return a list of all Editors in all its open inline widgets. (Ignoring * any other inline widgets that might be open but don't contain Editors). * @param {!Editor} hostEditor * @return {Array.<Editor>} * */ function getInlineEditors(hostEditor) { var inlineEditors = []; if (hostEditor) { hostEditor.getInlineWidgets().forEach(function (widget) { if (widget instanceof InlineTextEditor && widget.editor) { inlineEditors.push(widget.editor); } }); } return inlineEditors; } /** * @private * Creates a new "full-size" (not inline) Editor for the given Document, and sets it as the * Document's master backing editor. The editor is not yet visible; * Semi-private: should only be called within this module or by Document. * @param {!Document} document Document whose main/full Editor to create * @param {!Pane} pane Pane in which the editor will be hosted * @param {!Object} editorOptions If specified, contains editor options that * can be passed to CodeMirror * @return {!Editor} */ function _createFullEditorForDocument(document, pane, editorOptions) { // Create editor; make it initially invisible var editor = _createEditorForDocument(document, true, pane.$content, undefined, editorOptions); editor.setVisible(false); pane.addView(editor); exports.trigger("_fullEditorCreatedForDocument", document, editor, pane.id); return editor; } /** * Creates a new inline Editor instance for the given Document. * The editor is not yet visible or attached to a host editor. * @param {!Document} doc Document for the Editor's content * @param {?{startLine:Number, endLine:Number}} range If specified, all lines outside the given * range are hidden from the editor. Range is inclusive. Line numbers start at 0. * @param {HTMLDivContainer} inlineContent * @param {function(inlineWidget)} closeThisInline * * @return {{content:DOMElement, editor:Editor}} */ function createInlineEditorForDocument(doc, range, inlineContent) { // Hide the container for the editor before creating it so that CodeMirror doesn't do extra work // when initializing the document. When we construct the editor, we have to set its text and then // set the (small) visible range that we show in the editor. If the editor is visible, CM has to // render a large portion of the document before setting the visible range. By hiding the editor // first and showing it after the visible range is set, we avoid that initial render. $(inlineContent).hide(); var inlineEditor = _createEditorForDocument(doc, false, inlineContent, range); inlineEditor._hostEditor = getCurrentFullEditor(); $(inlineContent).show(); return { content: inlineContent, editor: inlineEditor }; } /** * Returns focus to the last visible editor that had focus. If no editor visible, does nothing. * This function should be called to restore editor focus after it has been temporarily * removed. For example, after a dialog with editable text is closed. */ function focusEditor() { DeprecationWarning.deprecationWarning("Use MainViewManager.focusActivePane() instead of EditorManager.focusEditor().", true); MainViewManager.focusActivePane(); } /** * @deprecated * resizes the editor */ function resizeEditor() { DeprecationWarning.deprecationWarning("Use WorkspaceManager.recomputeLayout() instead of EditorManager.resizeEditor().", true); WorkspaceManager.recomputeLayout(); } /** * Create and/or show the editor for the specified document * @param {!Document} document - document to edit * @param {!Pane} pane - pane to show it in * @param {!Object} editorOptions - If specified, contains * editor options that can be passed to CodeMirror * @private */ function _showEditor(document, pane, editorOptions) { // Ensure a main editor exists for this document to show in the UI var createdNewEditor = false, editor = document._masterEditor; // Check if a master editor is not set already or the current master editor doesn't belong // to the pane container requested - to support creation of multiple full editors // This check is required as _masterEditor is the active full editor for the document // and there can be existing full editor created for other panes if (editor && editor._paneId && editor._paneId !== pane.id) { editor = document._checkAssociatedEditorForPane(pane.id); } if (!editor) { // Performance (see #4757) Chrome wastes time messing with selection // that will just be changed at end, so clear it for now if (window.getSelection && window.getSelection().empty) { // Chrome window.getSelection().empty(); } // Editor doesn't exist: populate a new Editor with the text editor = _createFullEditorForDocument(document, pane, editorOptions); createdNewEditor = true; } else if (editor.$el.parent()[0] !== pane.$content[0]) { // editor does exist but is not a child of the pane so add it to the // pane (which will switch the view's container as well) pane.addView(editor); } // show the view pane.showView(editor); if (MainViewManager.getActivePaneId() === pane.id) { // give it focus editor.focus(); } if (createdNewEditor) { _restoreEditorViewState(editor); } } /** * @deprecated use MainViewManager.getCurrentlyViewedFile() instead * @return {string=} path of the file currently viewed in the active, full sized editor or null when there is no active editor */ function getCurrentlyViewedPath() { DeprecationWarning.deprecationWarning("Use MainViewManager.getCurrentlyViewedFile() instead of EditorManager.getCurrentlyViewedPath().", true); // We only want to return a path of a document object // not other things like images, etc... var currentPath = MainViewManager.getCurrentlyViewedPath(MainViewManager.ACTIVE_PANE), doc; if (currentPath) { doc = DocumentManager.getOpenDocumentForPath(currentPath); } if (doc) { return currentPath; } return null; } /** * @deprecated There is no equivalent API moving forward. * Use MainViewManager._initialize() from a unit test to create a Main View attached to a specific DOM element */ function setEditorHolder() { throw new Error("EditorManager.setEditorHolder() has been removed."); } /** * @deprecated Register a View Factory instead * @see MainViewFactory::#registerViewFactory */ function registerCustomViewer() { throw new Error("EditorManager.registerCustomViewer() has been removed."); } /** * Determines if the file can be opened in an editor * @param {!string} fullPath - file to be opened * @return {boolean} true if the file can be opened in an editor, false if not */ function canOpenPath(fullPath) { return !LanguageManager.getLanguageForPath(fullPath).isBinary(); } /** * Opens the specified document in the given pane * @param {!Document} doc - the document to open * @param {!Pane} pane - the pane to open the document in * @param {!Object} editorOptions - If specified, contains * editor options that can be passed to CodeMirror * @return {boolean} true if the file can be opened, false if not */ function openDocument(doc, pane, editorOptions) { var perfTimerName = PerfUtils.markStart("EditorManager.openDocument():\t" + (!doc || doc.file.fullPath)); if (doc && pane) { _showEditor(doc, pane, editorOptions); } PerfUtils.addMeasurement(perfTimerName); } /** * Returns the currently focused inline widget, if any. * @return {?InlineWidget} */ function getFocusedInlineWidget() { var currentEditor = getCurrentFullEditor(); if (currentEditor) { return currentEditor.getFocusedInlineWidget(); } return null; } /** * Returns the focused Editor within an inline text editor, or null if something else has focus * @return {?Editor} */ function _getFocusedInlineEditor() { var focusedWidget = getFocusedInlineWidget(); if (focusedWidget instanceof InlineTextEditor) { return focusedWidget.getFocusedEditor(); } return null; } /** * Returns the currently focused editor instance (full-sized OR inline editor). * This function is similar to getActiveEditor(), with one main difference: this * function will only return editors that currently have focus, whereas * getActiveEditor() will return the last visible editor that was given focus (but * may not currently have focus because, for example, a dialog with editable text * is open). * @return {?Editor} */ function getFocusedEditor() { var currentEditor = getCurrentFullEditor(); if (currentEditor) { // See if any inlines have focus var focusedInline = _getFocusedInlineEditor(); if (focusedInline) { return focusedInline; } // otherwise, see if full-sized editor has focus if (currentEditor.hasFocus()) { return currentEditor; } } return null; } /** * Returns the current active editor (full-sized OR inline editor). This editor may not * have focus at the moment, but it is visible and was the last editor that was given * focus. Returns null if no editors are active. * @see #getFocusedEditor * @return {?Editor} */ function getActiveEditor() { return _lastFocusedEditor; } /** * file removed from pane handler. * @param {jQuery.Event} e * @param {File|Array.<File>} removedFiles - file, path or array of files or paths that are being removed */ function _handleRemoveFromPaneView(e, removedFiles) { var handleFileRemoved = function (file) { var doc = DocumentManager.getOpenDocumentForPath(file.fullPath); if (doc) { MainViewManager._destroyEditorIfNotNeeded(doc); } }; // when files are removed from a pane then // we should destroy any unnecssary views if ($.isArray(removedFiles)) { removedFiles.forEach(function (removedFile) { handleFileRemoved(removedFile); }); } else { handleFileRemoved(removedFiles); } } // Set up event dispatching EventDispatcher.makeEventDispatcher(exports); // File-based preferences handling exports.on("activeEditorChange", function (e, current) { if (current && current.document && current.document.file) { PreferencesManager._setCurrentFile(current.document.file.fullPath); } }); // Initialize: command handlers CommandManager.register(Strings.CMD_TOGGLE_QUICK_EDIT, Commands.TOGGLE_QUICK_EDIT, function () { return _toggleInlineWidget(_inlineEditProviders, Strings.ERROR_QUICK_EDIT_PROVIDER_NOT_FOUND); }); CommandManager.register(Strings.CMD_TOGGLE_QUICK_DOCS, Commands.TOGGLE_QUICK_DOCS, function () { return _toggleInlineWidget(_inlineDocsProviders, Strings.ERROR_QUICK_DOCS_PROVIDER_NOT_FOUND); }); MainViewManager.on("currentFileChange", _handleCurrentFileChange); MainViewManager.on("workingSetRemove workingSetRemoveList", _handleRemoveFromPaneView); // For unit tests and internal use only exports._createFullEditorForDocument = _createFullEditorForDocument; exports._notifyActiveEditorChanged = _notifyActiveEditorChanged; // Internal Use only exports._saveEditorViewState = _saveEditorViewState; exports._createUnattachedMasterEditor = _createUnattachedMasterEditor; // Define public API exports.createInlineEditorForDocument = createInlineEditorForDocument; exports.getFocusedInlineWidget = getFocusedInlineWidget; exports.getInlineEditors = getInlineEditors; exports.closeInlineWidget = closeInlineWidget; exports.openDocument = openDocument; exports.canOpenPath = canOpenPath; // Convenience Methods exports.getActiveEditor = getActiveEditor; exports.getCurrentFullEditor = getCurrentFullEditor; exports.getFocusedEditor = getFocusedEditor; exports.registerInlineEditProvider = registerInlineEditProvider; exports.registerInlineDocsProvider = registerInlineDocsProvider; // Deprecated exports.registerCustomViewer = registerCustomViewer; exports.resizeEditor = resizeEditor; exports.focusEditor = focusEditor; exports.getCurrentlyViewedPath = getCurrentlyViewedPath; exports.setEditorHolder = setEditorHolder; });