UNPKG

@dcloudio/uni-debugger

Version:

uni-app debugger

839 lines (735 loc) 24.8 kB
/* * Copyright (C) 2011 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. AND ITS CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE INC. * OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @interface */ Sources.TabbedEditorContainerDelegate = function() {}; Sources.TabbedEditorContainerDelegate.prototype = { /** * @param {!Workspace.UISourceCode} uiSourceCode * @return {!UI.Widget} */ viewForFile(uiSourceCode) {}, /** * @param {!Sources.UISourceCodeFrame} sourceFrame * @param {!Workspace.UISourceCode} uiSourceCode */ recycleUISourceCodeFrame(sourceFrame, uiSourceCode) {}, }; /** * @unrestricted */ Sources.TabbedEditorContainer = class extends Common.Object { /** * @param {!Sources.TabbedEditorContainerDelegate} delegate * @param {!Common.Setting} setting * @param {!Element} placeholderElement */ constructor(delegate, setting, placeholderElement) { super(); this._delegate = delegate; this._tabbedPane = new UI.TabbedPane(); this._tabbedPane.setPlaceholderElement(placeholderElement); this._tabbedPane.setTabDelegate(new Sources.EditorContainerTabDelegate(this)); this._tabbedPane.setCloseableTabs(true); this._tabbedPane.setAllowTabReorder(true, true); this._tabbedPane.addEventListener(UI.TabbedPane.Events.TabClosed, this._tabClosed, this); this._tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected, this._tabSelected, this); Persistence.persistence.addEventListener( Persistence.Persistence.Events.BindingCreated, this._onBindingCreated, this); Persistence.persistence.addEventListener( Persistence.Persistence.Events.BindingRemoved, this._onBindingRemoved, this); this._tabIds = new Map(); this._files = {}; this._previouslyViewedFilesSetting = setting; this._history = Sources.TabbedEditorContainer.History.fromObject(this._previouslyViewedFilesSetting.get()); } /** * @param {!Common.Event} event */ _onBindingCreated(event) { const binding = /** @type {!Persistence.PersistenceBinding} */ (event.data); this._updateFileTitle(binding.fileSystem); const networkTabId = this._tabIds.get(binding.network); let fileSystemTabId = this._tabIds.get(binding.fileSystem); const wasSelectedInNetwork = this._currentFile === binding.network; const currentSelectionRange = this._history.selectionRange(binding.network.url()); const currentScrollLineNumber = this._history.scrollLineNumber(binding.network.url()); this._history.remove(binding.network.url()); if (!networkTabId) return; if (!fileSystemTabId) { const networkView = this._tabbedPane.tabView(networkTabId); const tabIndex = this._tabbedPane.tabIndex(networkTabId); if (networkView instanceof Sources.UISourceCodeFrame) { this._delegate.recycleUISourceCodeFrame(networkView, binding.fileSystem); fileSystemTabId = this._appendFileTab(binding.fileSystem, false, tabIndex, networkView); } else { fileSystemTabId = this._appendFileTab(binding.fileSystem, false, tabIndex); const fileSystemTabView = /** @type {!UI.Widget} */ (this._tabbedPane.tabView(fileSystemTabId)); this._restoreEditorProperties(fileSystemTabView, currentSelectionRange, currentScrollLineNumber); } } this._closeTabs([networkTabId], true); if (wasSelectedInNetwork) this._tabbedPane.selectTab(fileSystemTabId, false); this._updateHistory(); } /** * @param {!Common.Event} event */ _onBindingRemoved(event) { const binding = /** @type {!Persistence.PersistenceBinding} */ (event.data); this._updateFileTitle(binding.fileSystem); } /** * @return {!UI.Widget} */ get view() { return this._tabbedPane; } /** * @return {?UI.Widget} */ get visibleView() { return this._tabbedPane.visibleView; } /** * @return {!Array.<!UI.Widget>} */ fileViews() { return /** @type {!Array.<!UI.Widget>} */ (this._tabbedPane.tabViews()); } /** * @return {!UI.Toolbar} */ leftToolbar() { return this._tabbedPane.leftToolbar(); } /** * @return {!UI.Toolbar} */ rightToolbar() { return this._tabbedPane.rightToolbar(); } /** * @param {!Element} parentElement */ show(parentElement) { this._tabbedPane.show(parentElement); } /** * @param {!Workspace.UISourceCode} uiSourceCode */ showFile(uiSourceCode) { this._innerShowFile(uiSourceCode, true); } /** * @param {!Workspace.UISourceCode} uiSourceCode */ closeFile(uiSourceCode) { const tabId = this._tabIds.get(uiSourceCode); if (!tabId) return; this._closeTabs([tabId]); } closeAllFiles() { this._closeTabs(this._tabbedPane.allTabs()); } /** * @return {!Array.<!Workspace.UISourceCode>} */ historyUISourceCodes() { // FIXME: there should be a way to fetch UISourceCode for its uri. const uriToUISourceCode = {}; for (const id in this._files) { const uiSourceCode = this._files[id]; uriToUISourceCode[uiSourceCode.url()] = uiSourceCode; } const result = []; const uris = this._history._urls(); for (let i = 0; i < uris.length; ++i) { const uiSourceCode = uriToUISourceCode[uris[i]]; if (uiSourceCode) result.push(uiSourceCode); } return result; } _addViewListeners() { if (!this._currentView || !this._currentView.textEditor) return; this._currentView.textEditor.addEventListener( SourceFrame.SourcesTextEditor.Events.ScrollChanged, this._scrollChanged, this); this._currentView.textEditor.addEventListener( SourceFrame.SourcesTextEditor.Events.SelectionChanged, this._selectionChanged, this); } _removeViewListeners() { if (!this._currentView || !this._currentView.textEditor) return; this._currentView.textEditor.removeEventListener( SourceFrame.SourcesTextEditor.Events.ScrollChanged, this._scrollChanged, this); this._currentView.textEditor.removeEventListener( SourceFrame.SourcesTextEditor.Events.SelectionChanged, this._selectionChanged, this); } /** * @param {!Common.Event} event */ _scrollChanged(event) { if (this._scrollTimer) clearTimeout(this._scrollTimer); const lineNumber = /** @type {number} */ (event.data); this._scrollTimer = setTimeout(saveHistory.bind(this), 100); this._history.updateScrollLineNumber(this._currentFile.url(), lineNumber); /** * @this {Sources.TabbedEditorContainer} */ function saveHistory() { this._history.save(this._previouslyViewedFilesSetting); } } /** * @param {!Common.Event} event */ _selectionChanged(event) { const range = /** @type {!TextUtils.TextRange} */ (event.data); this._history.updateSelectionRange(this._currentFile.url(), range); this._history.save(this._previouslyViewedFilesSetting); Extensions.extensionServer.sourceSelectionChanged(this._currentFile.url(), range); } /** * @param {!Workspace.UISourceCode} uiSourceCode * @param {boolean=} userGesture */ _innerShowFile(uiSourceCode, userGesture) { const binding = Persistence.persistence.binding(uiSourceCode); uiSourceCode = binding ? binding.fileSystem : uiSourceCode; if (this._currentFile === uiSourceCode) return; this._removeViewListeners(); this._currentFile = uiSourceCode; const tabId = this._tabIds.get(uiSourceCode) || this._appendFileTab(uiSourceCode, userGesture); this._tabbedPane.selectTab(tabId, userGesture); if (userGesture) this._editorSelectedByUserAction(); const previousView = this._currentView; this._currentView = this.visibleView; this._addViewListeners(); const eventData = { currentFile: this._currentFile, currentView: this._currentView, previousView: previousView, userGesture: userGesture }; this.dispatchEventToListeners(Sources.TabbedEditorContainer.Events.EditorSelected, eventData); } /** * @param {!Workspace.UISourceCode} uiSourceCode * @return {string} */ _titleForFile(uiSourceCode) { const maxDisplayNameLength = 30; let title = uiSourceCode.displayName(true).trimMiddle(maxDisplayNameLength); if (uiSourceCode.isDirty()) title += '*'; return title; } /** * @param {string} id * @param {string} nextTabId */ _maybeCloseTab(id, nextTabId) { const uiSourceCode = this._files[id]; const shouldPrompt = uiSourceCode.isDirty() && uiSourceCode.project().canSetFileContent(); // FIXME: this should be replaced with common Save/Discard/Cancel dialog. if (!shouldPrompt || confirm(Common.UIString('Are you sure you want to close unsaved file: %s?', uiSourceCode.name()))) { uiSourceCode.resetWorkingCopy(); if (nextTabId) this._tabbedPane.selectTab(nextTabId, true); this._tabbedPane.closeTab(id, true); return true; } return false; } /** * @param {!Array.<string>} ids * @param {boolean=} forceCloseDirtyTabs */ _closeTabs(ids, forceCloseDirtyTabs) { const dirtyTabs = []; const cleanTabs = []; for (let i = 0; i < ids.length; ++i) { const id = ids[i]; const uiSourceCode = this._files[id]; if (!forceCloseDirtyTabs && uiSourceCode.isDirty()) dirtyTabs.push(id); else cleanTabs.push(id); } if (dirtyTabs.length) this._tabbedPane.selectTab(dirtyTabs[0], true); this._tabbedPane.closeTabs(cleanTabs, true); for (let i = 0; i < dirtyTabs.length; ++i) { const nextTabId = i + 1 < dirtyTabs.length ? dirtyTabs[i + 1] : null; if (!this._maybeCloseTab(dirtyTabs[i], nextTabId)) break; } } /** * @param {string} tabId * @param {!UI.ContextMenu} contextMenu */ _onContextMenu(tabId, contextMenu) { const uiSourceCode = this._files[tabId]; if (uiSourceCode) contextMenu.appendApplicableItems(uiSourceCode); } /** * @param {!Workspace.UISourceCode} uiSourceCode */ addUISourceCode(uiSourceCode) { const binding = Persistence.persistence.binding(uiSourceCode); uiSourceCode = binding ? binding.fileSystem : uiSourceCode; if (this._currentFile === uiSourceCode) return; const uri = uiSourceCode.url(); const index = this._history.index(uri); if (index === -1) return; if (!this._tabIds.has(uiSourceCode)) this._appendFileTab(uiSourceCode, false); // Select tab if this file was the last to be shown. if (!index) { this._innerShowFile(uiSourceCode, false); return; } if (!this._currentFile) return; const currentProjectType = this._currentFile.project().type(); const addedProjectType = uiSourceCode.project().type(); const snippetsProjectType = Workspace.projectTypes.Snippets; if (this._history.index(this._currentFile.url()) && currentProjectType === snippetsProjectType && addedProjectType !== snippetsProjectType) this._innerShowFile(uiSourceCode, false); } /** * @param {!Workspace.UISourceCode} uiSourceCode */ removeUISourceCode(uiSourceCode) { this.removeUISourceCodes([uiSourceCode]); } /** * @param {!Array.<!Workspace.UISourceCode>} uiSourceCodes */ removeUISourceCodes(uiSourceCodes) { const tabIds = []; for (let i = 0; i < uiSourceCodes.length; ++i) { const uiSourceCode = uiSourceCodes[i]; const tabId = this._tabIds.get(uiSourceCode); if (tabId) tabIds.push(tabId); } this._tabbedPane.closeTabs(tabIds); } /** * @param {!Workspace.UISourceCode} uiSourceCode */ _editorClosedByUserAction(uiSourceCode) { this._history.remove(uiSourceCode.url()); this._updateHistory(); } _editorSelectedByUserAction() { this._updateHistory(); } _updateHistory() { const tabIds = this._tabbedPane.lastOpenedTabIds(Sources.TabbedEditorContainer.maximalPreviouslyViewedFilesCount); /** * @param {string} tabId * @this {Sources.TabbedEditorContainer} */ function tabIdToURI(tabId) { return this._files[tabId].url(); } this._history.update(tabIds.map(tabIdToURI.bind(this))); this._history.save(this._previouslyViewedFilesSetting); } /** * @param {!Workspace.UISourceCode} uiSourceCode * @return {string} */ _tooltipForFile(uiSourceCode) { uiSourceCode = Persistence.persistence.network(uiSourceCode) || uiSourceCode; return uiSourceCode.url(); } /** * @param {!Workspace.UISourceCode} uiSourceCode * @param {boolean=} userGesture * @param {number=} index * @param {!UI.Widget=} replaceView * @return {string} */ _appendFileTab(uiSourceCode, userGesture, index, replaceView) { const view = replaceView || this._delegate.viewForFile(uiSourceCode); const title = this._titleForFile(uiSourceCode); const tooltip = this._tooltipForFile(uiSourceCode); const tabId = this._generateTabId(); this._tabIds.set(uiSourceCode, tabId); this._files[tabId] = uiSourceCode; if (!replaceView) { const savedSelectionRange = this._history.selectionRange(uiSourceCode.url()); const savedScrollLineNumber = this._history.scrollLineNumber(uiSourceCode.url()); this._restoreEditorProperties(view, savedSelectionRange, savedScrollLineNumber); } this._tabbedPane.appendTab(tabId, title, view, tooltip, userGesture, undefined, index); this._updateFileTitle(uiSourceCode); this._addUISourceCodeListeners(uiSourceCode); return tabId; } /** * @param {!UI.Widget} editorView * @param {!TextUtils.TextRange=} selection * @param {number=} firstLineNumber */ _restoreEditorProperties(editorView, selection, firstLineNumber) { const sourceFrame = editorView instanceof SourceFrame.SourceFrame ? /** @type {!SourceFrame.SourceFrame} */ (editorView) : null; if (!sourceFrame) return; if (selection) sourceFrame.setSelection(selection); if (typeof firstLineNumber === 'number') sourceFrame.scrollToLine(firstLineNumber); } /** * @param {!Common.Event} event */ _tabClosed(event) { const tabId = /** @type {string} */ (event.data.tabId); const userGesture = /** @type {boolean} */ (event.data.isUserGesture); const uiSourceCode = this._files[tabId]; if (this._currentFile === uiSourceCode) { this._removeViewListeners(); delete this._currentView; delete this._currentFile; } this._tabIds.remove(uiSourceCode); delete this._files[tabId]; this._removeUISourceCodeListeners(uiSourceCode); this.dispatchEventToListeners(Sources.TabbedEditorContainer.Events.EditorClosed, uiSourceCode); if (userGesture) this._editorClosedByUserAction(uiSourceCode); } /** * @param {!Common.Event} event */ _tabSelected(event) { const tabId = /** @type {string} */ (event.data.tabId); const userGesture = /** @type {boolean} */ (event.data.isUserGesture); const uiSourceCode = this._files[tabId]; this._innerShowFile(uiSourceCode, userGesture); } /** * @param {!Workspace.UISourceCode} uiSourceCode */ _addUISourceCodeListeners(uiSourceCode) { uiSourceCode.addEventListener(Workspace.UISourceCode.Events.TitleChanged, this._uiSourceCodeTitleChanged, this); uiSourceCode.addEventListener( Workspace.UISourceCode.Events.WorkingCopyChanged, this._uiSourceCodeWorkingCopyChanged, this); uiSourceCode.addEventListener( Workspace.UISourceCode.Events.WorkingCopyCommitted, this._uiSourceCodeWorkingCopyCommitted, this); } /** * @param {!Workspace.UISourceCode} uiSourceCode */ _removeUISourceCodeListeners(uiSourceCode) { uiSourceCode.removeEventListener(Workspace.UISourceCode.Events.TitleChanged, this._uiSourceCodeTitleChanged, this); uiSourceCode.removeEventListener( Workspace.UISourceCode.Events.WorkingCopyChanged, this._uiSourceCodeWorkingCopyChanged, this); uiSourceCode.removeEventListener( Workspace.UISourceCode.Events.WorkingCopyCommitted, this._uiSourceCodeWorkingCopyCommitted, this); } /** * @param {!Workspace.UISourceCode} uiSourceCode */ _updateFileTitle(uiSourceCode) { const tabId = this._tabIds.get(uiSourceCode); if (tabId) { const title = this._titleForFile(uiSourceCode); this._tabbedPane.changeTabTitle(tabId, title); let icon = null; if (Persistence.persistence.hasUnsavedCommittedChanges(uiSourceCode)) { icon = UI.Icon.create('smallicon-warning'); icon.title = Common.UIString('Changes to this file were not saved to file system.'); } else { icon = Persistence.PersistenceUtils.iconForUISourceCode(uiSourceCode); } this._tabbedPane.setTabIcon(tabId, icon); } } /** * @param {!Common.Event} event */ _uiSourceCodeTitleChanged(event) { const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data); this._updateFileTitle(uiSourceCode); this._updateHistory(); } /** * @param {!Common.Event} event */ _uiSourceCodeWorkingCopyChanged(event) { const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data); this._updateFileTitle(uiSourceCode); } /** * @param {!Common.Event} event */ _uiSourceCodeWorkingCopyCommitted(event) { const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data.uiSourceCode); this._updateFileTitle(uiSourceCode); } /** * @return {string} */ _generateTabId() { return 'tab_' + (Sources.TabbedEditorContainer._tabId++); } /** * @return {?Workspace.UISourceCode} uiSourceCode */ currentFile() { return this._currentFile || null; } }; /** @enum {symbol} */ Sources.TabbedEditorContainer.Events = { EditorSelected: Symbol('EditorSelected'), EditorClosed: Symbol('EditorClosed') }; Sources.TabbedEditorContainer._tabId = 0; Sources.TabbedEditorContainer.maximalPreviouslyViewedFilesCount = 30; /** * @unrestricted */ Sources.TabbedEditorContainer.HistoryItem = class { /** * @param {string} url * @param {!TextUtils.TextRange=} selectionRange * @param {number=} scrollLineNumber */ constructor(url, selectionRange, scrollLineNumber) { /** @const */ this.url = url; /** @const */ this._isSerializable = url.length < Sources.TabbedEditorContainer.HistoryItem.serializableUrlLengthLimit; this.selectionRange = selectionRange; this.scrollLineNumber = scrollLineNumber; } /** * @param {!Object} serializedHistoryItem * @return {!Sources.TabbedEditorContainer.HistoryItem} */ static fromObject(serializedHistoryItem) { const selectionRange = serializedHistoryItem.selectionRange ? TextUtils.TextRange.fromObject(serializedHistoryItem.selectionRange) : undefined; return new Sources.TabbedEditorContainer.HistoryItem( serializedHistoryItem.url, selectionRange, serializedHistoryItem.scrollLineNumber); } /** * @return {?Object} */ serializeToObject() { if (!this._isSerializable) return null; const serializedHistoryItem = {}; serializedHistoryItem.url = this.url; serializedHistoryItem.selectionRange = this.selectionRange; serializedHistoryItem.scrollLineNumber = this.scrollLineNumber; return serializedHistoryItem; } }; Sources.TabbedEditorContainer.HistoryItem.serializableUrlLengthLimit = 4096; /** * @unrestricted */ Sources.TabbedEditorContainer.History = class { /** * @param {!Array.<!Sources.TabbedEditorContainer.HistoryItem>} items */ constructor(items) { this._items = items; this._rebuildItemIndex(); } /** * @param {!Array.<!Object>} serializedHistory * @return {!Sources.TabbedEditorContainer.History} */ static fromObject(serializedHistory) { const items = []; for (let i = 0; i < serializedHistory.length; ++i) items.push(Sources.TabbedEditorContainer.HistoryItem.fromObject(serializedHistory[i])); return new Sources.TabbedEditorContainer.History(items); } /** * @param {string} url * @return {number} */ index(url) { return this._itemsIndex.has(url) ? /** @type {number} */ (this._itemsIndex.get(url)) : -1; } _rebuildItemIndex() { /** @type {!Map<string, number>} */ this._itemsIndex = new Map(); for (let i = 0; i < this._items.length; ++i) { console.assert(!this._itemsIndex.has(this._items[i].url)); this._itemsIndex.set(this._items[i].url, i); } } /** * @param {string} url * @return {!TextUtils.TextRange|undefined} */ selectionRange(url) { const index = this.index(url); return index !== -1 ? this._items[index].selectionRange : undefined; } /** * @param {string} url * @param {!TextUtils.TextRange=} selectionRange */ updateSelectionRange(url, selectionRange) { if (!selectionRange) return; const index = this.index(url); if (index === -1) return; this._items[index].selectionRange = selectionRange; } /** * @param {string} url * @return {number|undefined} */ scrollLineNumber(url) { const index = this.index(url); return index !== -1 ? this._items[index].scrollLineNumber : undefined; } /** * @param {string} url * @param {number} scrollLineNumber */ updateScrollLineNumber(url, scrollLineNumber) { const index = this.index(url); if (index === -1) return; this._items[index].scrollLineNumber = scrollLineNumber; } /** * @param {!Array.<string>} urls */ update(urls) { for (let i = urls.length - 1; i >= 0; --i) { const index = this.index(urls[i]); let item; if (index !== -1) { item = this._items[index]; this._items.splice(index, 1); } else { item = new Sources.TabbedEditorContainer.HistoryItem(urls[i]); } this._items.unshift(item); this._rebuildItemIndex(); } } /** * @param {string} url */ remove(url) { const index = this.index(url); if (index !== -1) { this._items.splice(index, 1); this._rebuildItemIndex(); } } /** * @param {!Common.Setting} setting */ save(setting) { setting.set(this._serializeToObject()); } /** * @return {!Array.<!Object>} */ _serializeToObject() { const serializedHistory = []; for (let i = 0; i < this._items.length; ++i) { const serializedItem = this._items[i].serializeToObject(); if (serializedItem) serializedHistory.push(serializedItem); if (serializedHistory.length === Sources.TabbedEditorContainer.maximalPreviouslyViewedFilesCount) break; } return serializedHistory; } /** * @return {!Array.<string>} */ _urls() { const result = []; for (let i = 0; i < this._items.length; ++i) result.push(this._items[i].url); return result; } }; /** * @implements {UI.TabbedPaneTabDelegate} * @unrestricted */ Sources.EditorContainerTabDelegate = class { /** * @param {!Sources.TabbedEditorContainer} editorContainer */ constructor(editorContainer) { this._editorContainer = editorContainer; } /** * @override * @param {!UI.TabbedPane} tabbedPane * @param {!Array.<string>} ids */ closeTabs(tabbedPane, ids) { this._editorContainer._closeTabs(ids); } /** * @override * @param {string} tabId * @param {!UI.ContextMenu} contextMenu */ onContextMenu(tabId, contextMenu) { this._editorContainer._onContextMenu(tabId, contextMenu); } };