/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/extensions/default/NavigationAndHistory/NavigationProvider.js
562 строки
21 KB
Swagatam Mitra
Fix for external change handling (#13545)
03 авг 2017, 08:27
03 авг 2017, 08:27
7211d7f
Код
Авторство
О чём код?
/* * Copyright (c) 2017 - 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. * */ /** * Manages Editor navigation history to aid back/fwd movement between the edit positions * in the active project context. The navigation history is purely in-memory and not * persisted to file system when a project is being closed. */ define(function (require, exports, module) { "use strict"; var Strings = brackets.getModule("strings"), MainViewManager = brackets.getModule("view/MainViewManager"), Document = brackets.getModule("document/Document"), DocumentManager = brackets.getModule("document/DocumentManager"), DocumentCommandHandlers = brackets.getModule("document/DocumentCommandHandlers"), EditorManager = brackets.getModule("editor/EditorManager"), Editor = brackets.getModule("editor/Editor"), ProjectManager = brackets.getModule("project/ProjectManager"), CommandManager = brackets.getModule("command/CommandManager"), Commands = brackets.getModule("command/Commands"), Menus = brackets.getModule("command/Menus"), KeyBindingManager = brackets.getModule("command/KeyBindingManager"), FileSystem = brackets.getModule("filesystem/FileSystem"); var KeyboardPrefs = JSON.parse(require("text!keyboard.json")); // Command constants for navigation history var NAVIGATION_JUMP_BACK = "navigation.jump.back", NAVIGATION_JUMP_FWD = "navigation.jump.fwd"; // The latency time to capture an explicit cursor movement as a navigation frame var NAV_FRAME_CAPTURE_LATENCY = 2000, MAX_NAV_FRAMES_COUNT = 30; /** * Contains list of most recently known cursor positions. * @private * @type {Array.<Object>} */ var jumpBackwardStack = []; /** * Contains list of most recently traversed cursor positions using NAVIGATION_JUMP_BACK command. * @private * @type {Array.<Object>} */ var jumpForwardStack = [], activePosNotSynced = false, captureTimer = null, currentEditPos = null, jumpInProgress = false, commandJumpBack, commandJumpFwd; /** * Function to check if there are any navigatable frame backward. * @private */ function _hasNavBackFrames() { return (jumpForwardStack.length > 0 && jumpBackwardStack.length > 0) || (!jumpForwardStack.length && jumpBackwardStack.length > 1); } /** * Function to enable/disable navigation command based on cursor positions availability. * @private */ function _validateNavigationCmds() { commandJumpBack.setEnabled(_hasNavBackFrames()); commandJumpFwd.setEnabled(jumpForwardStack.length > 0); } /** * Function to check existence of a file entry, validity of markers * @private */ function _validateFrame(entry) { var deferred = new $.Deferred(), fileEntry = FileSystem.getFileForPath(entry.filePath); if (entry.inMem) { var indexInWS = MainViewManager.findInWorkingSet(entry.paneId, entry.filePath); // Remove entry if InMemoryFile is not found in Working set if (indexInWS === -1) { deferred.reject(); } else { deferred.resolve(); } } else { fileEntry.exists(function (err, exists) { if (!err && exists) { // Additional check to handle external modification and mutation of the doc text affecting markers if (fileEntry._hash !== entry._hash) { deferred.reject(); } else if (!entry._validateMarkers()) { deferred.reject(); } else { deferred.resolve(); } } else { deferred.reject(); } }); } return deferred.promise(); } /** * Prototype to capture a navigation frame and it's various data/functional attributues */ function NavigationFrame(editor, selectionObj) { this.cm = editor._codeMirror; this.filePath = editor.document.file._path; this.inMem = editor.document.file.constructor.name === "InMemoryFile"; this.paneId = editor._paneId; this._hash = editor.document.file._hash; this.uId = (new Date()).getTime(); this.selections = []; this.bookMarkIds = []; this._createMarkers(selectionObj.ranges); } /** * Lifecycle event handler of the editor for which this frame is captured */ NavigationFrame.prototype._handleEditorDestroy = function (editor) { this._backupSelectionRanges(); this._clearMarkers(); this.cm = null; this.bookMarkIds = null; }; /** * Function to re-create CM TextMarkers for previously backed up ranges * This logic is required to ensure that the captured navigation positions * stay valid and contextual even when the actual document text mutates. * The mutations which are handled here : * -> Addition/Deletion of lines before the captured position * -> Addition/Updation of characters in the captured selection */ NavigationFrame.prototype._reinstateMarkers = function (editor) { this.cm = editor._codeMirror; this.paneId = editor._paneId; this._createMarkers(this.selections); }; /** * Function to validate an existing frame against a file '_hash' to detect * external change so that the frame can be discarded */ NavigationFrame.prototype._validateFileHash = function (file) { return this.filePath === file._path ? this._hash === file._hash : true; }; /** * Function to create CM TextMarkers for the navigated positions/selections. * This logic is required to ensure that the captured navigation positions * stay valid and contextual even when the actual document text mutates. * The mutations which are handled here : * -> Addition/Deletion of lines before the captured position * -> Addition/Updation of characters in the captured selection */ NavigationFrame.prototype._createMarkers = function (ranges) { var range, rangeStart, rangeEnd, index, bookMark; this.bookMarkIds = []; for (index in ranges) { range = ranges[index]; rangeStart = range.anchor || range.start; rangeEnd = range.head || range.end; // 'markText' has to used for a non-zero length position, if current selection is // of zero length use bookmark instead. if (rangeStart.line === rangeEnd.line && rangeStart.ch === rangeEnd.ch) { bookMark = this.cm.setBookmark(rangeStart, rangeEnd); this.bookMarkIds.push(bookMark.id); } else { this.cm.markText(rangeStart, rangeEnd, {className: (this.uId)}); } } }; /** * Function to actually convert the CM markers to CM positions which can be used to * set selections or cursor positions in Editor. */ NavigationFrame.prototype._backupSelectionRanges = function () { if (!this.cm) { return; } var marker, selection, index; // Reset selections first. this.selections = []; var self = this; // Collate only the markers we used to mark selections/cursors var markers = this.cm.getAllMarks().filter(function (entry) { if (entry.className === self.uId || self.bookMarkIds.indexOf(entry.id) !== -1) { return entry; } }); // Iterate over CM textmarkers and collate the updated(if?) positions for (index in markers) { marker = markers[index]; selection = marker.find(); if (marker.type === "bookmark") { this.selections.push({start: selection, end: selection}); } else { this.selections.push({start: selection.from, end: selection.to}); } } }; /** * Function to clean up the markers in cm */ NavigationFrame.prototype._clearMarkers = function () { if (!this.cm) { return; } var self = this; // clear only the markers we used to mark selections/cursors this.cm.getAllMarks().filter(function (entry) { if (entry.className === self.uId || self.bookMarkIds.indexOf(entry.id) !== -1) { entry.clear(); } }); }; /** * Function to check if we have valid markers in cm for this frame */ NavigationFrame.prototype._validateMarkers = function () { this._backupSelectionRanges(); return this.selections.length; }; /** * Function to actually navigate to the position(file,selections) captured in this frame */ NavigationFrame.prototype.goTo = function () { var self = this; this._backupSelectionRanges(); jumpInProgress = true; // To ensure we don't reopen the same doc in the last known pane // rather bring it to the same pane where user has opened it var thisDoc = DocumentManager.getOpenDocumentForPath(this.filePath); if (thisDoc && thisDoc._masterEditor) { this.paneId = thisDoc._masterEditor._paneId; } CommandManager.execute(Commands.FILE_OPEN, {fullPath: this.filePath, paneId: this.paneId}).done(function () { EditorManager.getCurrentFullEditor().setSelections(self.selections, true); _validateNavigationCmds(); }).always(function () { jumpInProgress = false; }); }; /** * Function to capture a non-zero set of selections as a navigation frame. * The assumptions behind capturing a frame as a navigation frame are : * * -> If it's set by user explicitly (using mouse click or jump to definition) * -> By clicking on search results * -> Change of cursor by keyboard navigation keys or actual edits are not captured. * * @private */ function _recordJumpDef(event, selectionObj) { // Don't capture frames if we are navigating or document text is being refreshed(fileSync in progress) if (jumpInProgress || (event.target && event.target.document._refreshInProgress)) { return; } // Reset forward navigation stack if we are capturing a new event jumpForwardStack = []; if (captureTimer) { window.clearTimeout(captureTimer); captureTimer = null; } // Ensure cursor activity has not happened because of arrow keys or edit if (selectionObj.origin !== "+move" && (!window.event || window.event.type !== "input")) { captureTimer = window.setTimeout(function () { // Check if we have reached MAX_NAV_FRAMES_COUNT // If yes, control overflow if (jumpBackwardStack.length === MAX_NAV_FRAMES_COUNT) { var navFrame = jumpBackwardStack.shift(); navFrame._clearMarkers(); } currentEditPos = new NavigationFrame(event.target, selectionObj); jumpBackwardStack.push(currentEditPos); _validateNavigationCmds(); activePosNotSynced = false; }, NAV_FRAME_CAPTURE_LATENCY); } else { activePosNotSynced = true; } } /** * Command handler to navigate backward */ function _navigateBack() { if (!jumpForwardStack.length) { if (activePosNotSynced) { currentEditPos = new NavigationFrame(EditorManager.getCurrentFullEditor(), {ranges: EditorManager.getCurrentFullEditor()._codeMirror.listSelections()}); jumpForwardStack.push(currentEditPos); } } var navFrame = jumpBackwardStack.pop(); // Check if the poped frame is the current active frame or doesn't have any valid marker information // if true, jump again if (navFrame && navFrame === currentEditPos) { jumpForwardStack.push(navFrame); _validateNavigationCmds(); CommandManager.execute(NAVIGATION_JUMP_BACK); return; } if (navFrame) { // We will check for the file existence now, if it doesn't exist we will jump back again // but discard the popped frame as invalid. _validateFrame(navFrame).done(function () { jumpForwardStack.push(navFrame); navFrame.goTo(); currentEditPos = navFrame; }).fail(function () { CommandManager.execute(NAVIGATION_JUMP_BACK); }).always(function () { _validateNavigationCmds(); }); } } /** * Command handler to navigate forward */ function _navigateForward() { var navFrame = jumpForwardStack.pop(); if (!navFrame) { return; } // Check if the poped frame is the current active frame or doesn't have any valid marker information // if true, jump again if (navFrame === currentEditPos) { jumpBackwardStack.push(navFrame); _validateNavigationCmds(); CommandManager.execute(NAVIGATION_JUMP_FWD); return; } // We will check for the file existence now, if it doesn't exist we will jump back again // but discard the popped frame as invalid. _validateFrame(navFrame).done(function () { jumpBackwardStack.push(navFrame); navFrame.goTo(); currentEditPos = navFrame; }).fail(function () { _validateNavigationCmds(); CommandManager.execute(NAVIGATION_JUMP_FWD); }).always(function () { _validateNavigationCmds(); }); } /** * Function to initialize navigation menu items. * @private */ function _initNavigationMenuItems() { var menu = Menus.getMenu(Menus.AppMenuBar.NAVIGATE_MENU); menu.addMenuItem(NAVIGATION_JUMP_BACK, "", Menus.AFTER, Commands.NAVIGATE_PREV_DOC); menu.addMenuItem(NAVIGATION_JUMP_FWD, "", Menus.AFTER, NAVIGATION_JUMP_BACK); } /** * Function to initialize navigation commands and it's keyboard shortcuts. * @private */ function _initNavigationCommands() { CommandManager.register(Strings.CMD_NAVIGATE_BACKWARD, NAVIGATION_JUMP_BACK, _navigateBack); CommandManager.register(Strings.CMD_NAVIGATE_FORWARD, NAVIGATION_JUMP_FWD, _navigateForward); commandJumpBack = CommandManager.get(NAVIGATION_JUMP_BACK); commandJumpFwd = CommandManager.get(NAVIGATION_JUMP_FWD); commandJumpBack.setEnabled(false); commandJumpFwd.setEnabled(false); KeyBindingManager.addBinding(NAVIGATION_JUMP_BACK, KeyboardPrefs[NAVIGATION_JUMP_BACK]); KeyBindingManager.addBinding(NAVIGATION_JUMP_FWD, KeyboardPrefs[NAVIGATION_JUMP_FWD]); _initNavigationMenuItems(); } /** * Function to request a navigation frame creation explicitly. * @private */ function _captureFrame(editor) { // Capture the active position now if it was not captured earlier if ((activePosNotSynced || !jumpBackwardStack.length) && !jumpInProgress) { jumpBackwardStack.push(new NavigationFrame(editor, {ranges: editor._codeMirror.listSelections()})); } } /** * Create snapshot of last known live markers. * @private */ function _backupLiveMarkers(frames, editor) { var index, frame; for (index in frames) { frame = frames[index]; if (frame.cm === editor._codeMirror) { frame._handleEditorDestroy(); } } } /** * Handle Editor destruction to create backup of live marker positions * @private */ function _handleEditorCleanup(event, editor) { _backupLiveMarkers(jumpBackwardStack, editor); _backupLiveMarkers(jumpForwardStack, editor); } /** * Removes all frames from backward navigation stack for the given file only if the file is changed on disk. * @private */ function _removeBackwardFramesForFile(file) { jumpBackwardStack = jumpBackwardStack.filter(function (frame) { return frame._validateFileHash(file); }); } /** * Removes all frames from forward navigation stack for the given file only if the file is changed on disk. * @private */ function _removeForwardFramesForFile(file) { jumpForwardStack = jumpForwardStack.filter(function (frame) { return frame._validateFileHash(file); }); } /** * Handles explicit content reset for a document caused by external changes * @private */ function _handleExternalChange(evt, doc) { if (doc) { _removeBackwardFramesForFile(doc.file); _removeForwardFramesForFile(doc.file); _validateNavigationCmds(); } } function _handleProjectOpen() { jumpBackwardStack = []; jumpForwardStack = []; } /** * Required to make offline markers alive again to track document mutation * @private */ function _reinstateMarkers(editor, frames) { var index, frame; for (index in frames) { frame = frames[index]; if (!frame.cm && frame.filePath === editor.document.file._path) { frame._reinstateMarkers(editor); } } } /** * Handle Active Editor change to update navigation information * @private */ function _handleActiveEditorChange(event, current, previous) { if (previous && previous._paneId) { // Handle only full editors previous.off("beforeSelectionChange", _recordJumpDef); _captureFrame(previous); _validateNavigationCmds(); } if (current && current._paneId) { // Handle only full editors activePosNotSynced = true; current.off("beforeSelectionChange", _recordJumpDef); current.on("beforeSelectionChange", _recordJumpDef); current.off("beforeDestroy", _handleEditorCleanup); current.on("beforeDestroy", _handleEditorCleanup); } } function _initHandlers() { EditorManager.on("activeEditorChange", _handleActiveEditorChange); ProjectManager.on("projectOpen", _handleProjectOpen); EditorManager.on("_fullEditorCreatedForDocument", function (event, document, editor) { _handleExternalChange(event, {file: document.file}); _reinstateMarkers(editor, jumpBackwardStack); _reinstateMarkers(editor, jumpForwardStack); }); FileSystem.on("change", function (event, entry) { if (entry) { _handleExternalChange(event, {file: entry}); } }); Document.on("_documentRefreshed", function (event, doc) { _handleExternalChange(event, {file: doc.file}); }); } function init() { _initNavigationCommands(); _initHandlers(); } exports.init = init; });