UNPKG

debug-server-next

Version:

Dev server for hippy-core.

662 lines (661 loc) 27.2 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: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 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. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT * OWNER OR 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. */ /* eslint-disable rulesdir/no_underscored_properties */ import * as Common from '../../core/common/common.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as Extensions from '../../models/extensions/extensions.js'; import * as Persistence from '../../models/persistence/persistence.js'; import * as TextUtils from '../../models/text_utils/text_utils.js'; import * as Workspace from '../../models/workspace/workspace.js'; import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as Snippets from '../snippets/snippets.js'; import { SourcesView } from './SourcesView.js'; import { UISourceCodeFrame } from './UISourceCodeFrame.js'; const UIStrings = { /** *@description Text in Tabbed Editor Container of the Sources panel *@example {example.file} PH1 */ areYouSureYouWantToCloseUnsaved: 'Are you sure you want to close unsaved file: {PH1}?', /** *@description Error message for tooltip showing that a file in Sources could not be loaded */ unableToLoadThisContent: 'Unable to load this content.', /** *@description Icon title in Tabbed Editor Container of the Sources panel */ changesToThisFileWereNotSavedTo: 'Changes to this file were not saved to file system.', }; const str_ = i18n.i18n.registerUIStrings('panels/sources/TabbedEditorContainer.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class TabbedEditorContainer extends Common.ObjectWrapper.ObjectWrapper { _delegate; _tabbedPane; _tabIds; _files; _previouslyViewedFilesSetting; _history; _uriToUISourceCode; _currentFile; _currentView; _scrollTimer; constructor(delegate, setting, placeholderElement, focusedPlaceholderElement) { super(); this._delegate = delegate; this._tabbedPane = new UI.TabbedPane.TabbedPane(); this._tabbedPane.setPlaceholderElement(placeholderElement, focusedPlaceholderElement); this._tabbedPane.setTabDelegate(new 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.PersistenceImpl.instance().addEventListener(Persistence.Persistence.Events.BindingCreated, this._onBindingCreated, this); Persistence.Persistence.PersistenceImpl.instance().addEventListener(Persistence.Persistence.Events.BindingRemoved, this._onBindingRemoved, this); this._tabIds = new Map(); this._files = new Map(); this._previouslyViewedFilesSetting = setting; this._history = History.fromObject(this._previouslyViewedFilesSetting.get()); this._uriToUISourceCode = new Map(); } _onBindingCreated(event) { const binding = 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 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 = this._tabbedPane.tabView(fileSystemTabId); this._restoreEditorProperties(fileSystemTabView, currentSelectionRange, currentScrollLineNumber); } } this._closeTabs([networkTabId], true); if (wasSelectedInNetwork) { this._tabbedPane.selectTab(fileSystemTabId, false); } this._updateHistory(); } _onBindingRemoved(event) { const binding = event.data; this._updateFileTitle(binding.fileSystem); } get view() { return this._tabbedPane; } get visibleView() { return this._tabbedPane.visibleView; } fileViews() { return /** @type {!Array.<!UI.Widget.Widget>} */ this._tabbedPane.tabViews(); } leftToolbar() { return this._tabbedPane.leftToolbar(); } rightToolbar() { return this._tabbedPane.rightToolbar(); } show(parentElement) { this._tabbedPane.show(parentElement); } showFile(uiSourceCode) { const binding = Persistence.Persistence.PersistenceImpl.instance().binding(uiSourceCode); uiSourceCode = binding ? binding.fileSystem : uiSourceCode; const frame = UI.Context.Context.instance().flavor(SourcesView); // If the content has already been set and the current frame is showing // the incoming uiSourceCode, then fire the event that the file has been loaded. // Otherwise, this event will fire as soon as the content has been set. if (frame?.currentSourceFrame()?.contentSet && this._currentFile === uiSourceCode && frame?.currentUISourceCode() === uiSourceCode) { Common.EventTarget.fireEvent('source-file-loaded', uiSourceCode.displayName(true)); } else { this._innerShowFile(this._canonicalUISourceCode(uiSourceCode), true); } } closeFile(uiSourceCode) { const tabId = this._tabIds.get(uiSourceCode); if (!tabId) { return; } this._closeTabs([tabId]); } closeAllFiles() { this._closeTabs(this._tabbedPane.tabIds()); } historyUISourceCodes() { const result = []; const uris = this._history._urls(); for (const uri of uris) { const uiSourceCode = this._uriToUISourceCode.get(uri); if (uiSourceCode) { result.push(uiSourceCode); } } return result; } _addViewListeners() { if (!this._currentView || !(this._currentView instanceof SourceFrame.SourceFrame.SourceFrameImpl)) { 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 instanceof SourceFrame.SourceFrame.SourceFrameImpl)) { return; } this._currentView.textEditor.removeEventListener(SourceFrame.SourcesTextEditor.Events.ScrollChanged, this._scrollChanged, this); this._currentView.textEditor.removeEventListener(SourceFrame.SourcesTextEditor.Events.SelectionChanged, this._selectionChanged, this); } _scrollChanged(event) { if (this._scrollTimer) { clearTimeout(this._scrollTimer); } const lineNumber = event.data; this._scrollTimer = window.setTimeout(saveHistory.bind(this), 100); if (this._currentFile) { this._history.updateScrollLineNumber(this._currentFile.url(), lineNumber); } function saveHistory() { this._history.save(this._previouslyViewedFilesSetting); } } _selectionChanged(event) { const range = event.data; if (this._currentFile) { this._history.updateSelectionRange(this._currentFile.url(), range); } this._history.save(this._previouslyViewedFilesSetting); if (this._currentFile) { Extensions.ExtensionServer.ExtensionServer.instance().sourceSelectionChanged(this._currentFile.url(), range); } } _innerShowFile(uiSourceCode, userGesture) { const binding = Persistence.Persistence.PersistenceImpl.instance().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(Events.EditorSelected, eventData); } _titleForFile(uiSourceCode) { const maxDisplayNameLength = 30; let title = Platform.StringUtilities.trimMiddle(uiSourceCode.displayName(true), maxDisplayNameLength); if (uiSourceCode.isDirty()) { title += '*'; } return title; } _maybeCloseTab(id, nextTabId) { const uiSourceCode = this._files.get(id); if (!uiSourceCode) { return false; } const shouldPrompt = uiSourceCode.isDirty() && uiSourceCode.project().canSetFileContent(); // FIXME: this should be replaced with common Save/Discard/Cancel dialog. if (!shouldPrompt || confirm(i18nString(UIStrings.areYouSureYouWantToCloseUnsaved, { PH1: uiSourceCode.name() }))) { uiSourceCode.resetWorkingCopy(); if (nextTabId) { this._tabbedPane.selectTab(nextTabId, true); } this._tabbedPane.closeTab(id, true); return true; } return false; } _closeTabs(ids, forceCloseDirtyTabs) { const dirtyTabs = []; const cleanTabs = []; for (let i = 0; i < ids.length; ++i) { const id = ids[i]; const uiSourceCode = this._files.get(id); if (uiSourceCode) { 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; } } } _onContextMenu(tabId, contextMenu) { const uiSourceCode = this._files.get(tabId); if (uiSourceCode) { contextMenu.appendApplicableItems(uiSourceCode); } } _canonicalUISourceCode(uiSourceCode) { // Check if we have already a UISourceCode for this url const existingSourceCode = this._uriToUISourceCode.get(uiSourceCode.url()); if (existingSourceCode) { // Ignore incoming uiSourceCode, we already have this file. return existingSourceCode; } this._uriToUISourceCode.set(uiSourceCode.url(), uiSourceCode); return uiSourceCode; } addUISourceCode(uiSourceCode) { const canonicalSourceCode = this._canonicalUISourceCode(uiSourceCode); const duplicated = canonicalSourceCode !== uiSourceCode; const binding = Persistence.Persistence.PersistenceImpl.instance().binding(canonicalSourceCode); uiSourceCode = binding ? binding.fileSystem : canonicalSourceCode; if (duplicated && uiSourceCode.project().type() !== Workspace.Workspace.projectTypes.FileSystem) { uiSourceCode.disableEdit(); } 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 currentProjectIsSnippets = Snippets.ScriptSnippetFileSystem.isSnippetsUISourceCode(this._currentFile); const addedProjectIsSnippets = Snippets.ScriptSnippetFileSystem.isSnippetsUISourceCode(uiSourceCode); if (this._history.index(this._currentFile.url()) && currentProjectIsSnippets && !addedProjectIsSnippets) { this._innerShowFile(uiSourceCode, false); } } removeUISourceCode(uiSourceCode) { this.removeUISourceCodes([uiSourceCode]); } removeUISourceCodes(uiSourceCodes) { const tabIds = []; for (const uiSourceCode of uiSourceCodes) { const tabId = this._tabIds.get(uiSourceCode); if (tabId) { tabIds.push(tabId); } if (this._uriToUISourceCode.get(uiSourceCode.url()) === uiSourceCode) { this._uriToUISourceCode.delete(uiSourceCode.url()); } } this._tabbedPane.closeTabs(tabIds); } _editorClosedByUserAction(uiSourceCode) { this._history.remove(uiSourceCode.url()); this._updateHistory(); } _editorSelectedByUserAction() { this._updateHistory(); } _updateHistory() { const tabIds = this._tabbedPane.lastOpenedTabIds(maximalPreviouslyViewedFilesCount); function tabIdToURI(tabId) { const tab = this._files.get(tabId); if (!tab) { return ''; } return tab.url(); } this._history.update(tabIds.map(tabIdToURI.bind(this))); this._history.save(this._previouslyViewedFilesSetting); } _tooltipForFile(uiSourceCode) { uiSourceCode = Persistence.Persistence.PersistenceImpl.instance().network(uiSourceCode) || uiSourceCode; return uiSourceCode.url(); } _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.set(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); if (uiSourceCode.loadError()) { this._addLoadErrorIcon(tabId); } else if (!uiSourceCode.contentLoaded()) { uiSourceCode.requestContent().then(_content => { if (uiSourceCode.loadError()) { this._addLoadErrorIcon(tabId); } }); } return tabId; } _addLoadErrorIcon(tabId) { const icon = UI.Icon.Icon.create('smallicon-error'); UI.Tooltip.Tooltip.install(icon, i18nString(UIStrings.unableToLoadThisContent)); if (this._tabbedPane.tabView(tabId)) { this._tabbedPane.setTabIcon(tabId, icon); } } _restoreEditorProperties(editorView, selection, firstLineNumber) { const sourceFrame = editorView instanceof SourceFrame.SourceFrame.SourceFrameImpl ? editorView : null; if (!sourceFrame) { return; } if (selection) { sourceFrame.setSelection(selection); } if (typeof firstLineNumber === 'number') { sourceFrame.scrollToLine(firstLineNumber); } } _tabClosed(event) { const tabId = event.data.tabId; const userGesture = event.data.isUserGesture; const uiSourceCode = this._files.get(tabId); if (this._currentFile === uiSourceCode) { this._removeViewListeners(); this._currentView = null; this._currentFile = null; } if (uiSourceCode) { this._tabIds.delete(uiSourceCode); } this._files.delete(tabId); if (uiSourceCode) { this._removeUISourceCodeListeners(uiSourceCode); this.dispatchEventToListeners(Events.EditorClosed, uiSourceCode); if (userGesture) { this._editorClosedByUserAction(uiSourceCode); } } } _tabSelected(event) { const tabId = event.data.tabId; const userGesture = event.data.isUserGesture; const uiSourceCode = this._files.get(tabId); if (uiSourceCode) { this._innerShowFile(uiSourceCode, userGesture); } } _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); } _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); } _updateFileTitle(uiSourceCode) { const tabId = this._tabIds.get(uiSourceCode); if (tabId) { const title = this._titleForFile(uiSourceCode); const tooltip = this._tooltipForFile(uiSourceCode); this._tabbedPane.changeTabTitle(tabId, title, tooltip); let icon = null; if (uiSourceCode.loadError()) { icon = UI.Icon.Icon.create('smallicon-error'); UI.Tooltip.Tooltip.install(icon, i18nString(UIStrings.unableToLoadThisContent)); } else if (Persistence.Persistence.PersistenceImpl.instance().hasUnsavedCommittedChanges(uiSourceCode)) { icon = UI.Icon.Icon.create('smallicon-warning'); UI.Tooltip.Tooltip.install(icon, i18nString(UIStrings.changesToThisFileWereNotSavedTo)); } else { icon = Persistence.PersistenceUtils.PersistenceUtils.iconForUISourceCode(uiSourceCode); } this._tabbedPane.setTabIcon(tabId, icon); } } _uiSourceCodeTitleChanged(event) { const uiSourceCode = event.data; this._updateFileTitle(uiSourceCode); this._updateHistory(); } _uiSourceCodeWorkingCopyChanged(event) { const uiSourceCode = event.data; this._updateFileTitle(uiSourceCode); } _uiSourceCodeWorkingCopyCommitted(event) { const uiSourceCode = event.data.uiSourceCode; this._updateFileTitle(uiSourceCode); } _generateTabId() { return 'tab_' + (tabId++); } /** uiSourceCode */ currentFile() { return this._currentFile || null; } } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export var Events; (function (Events) { Events["EditorSelected"] = "EditorSelected"; Events["EditorClosed"] = "EditorClosed"; })(Events || (Events = {})); export let tabId = 0; export const maximalPreviouslyViewedFilesCount = 30; export class HistoryItem { url; _isSerializable; selectionRange; scrollLineNumber; constructor(url, selectionRange, scrollLineNumber) { this.url = url; this._isSerializable = url.length < HistoryItem.serializableUrlLengthLimit; this.selectionRange = selectionRange; this.scrollLineNumber = scrollLineNumber; } static fromObject(serializedHistoryItem) { const selectionRange = serializedHistoryItem.selectionRange ? TextUtils.TextRange.TextRange.fromObject(serializedHistoryItem.selectionRange) : undefined; return new HistoryItem(serializedHistoryItem.url, selectionRange, serializedHistoryItem.scrollLineNumber); } serializeToObject() { if (!this._isSerializable) { return null; } const serializedHistoryItem = { url: this.url, selectionRange: this.selectionRange, scrollLineNumber: this.scrollLineNumber, }; return serializedHistoryItem; } static serializableUrlLengthLimit = 4096; } export class History { _items; _itemsIndex; constructor(items) { this._items = items; this._itemsIndex = new Map(); this._rebuildItemIndex(); } static fromObject(serializedHistory) { const items = []; for (let i = 0; i < serializedHistory.length; ++i) { // crbug.com/876265 Old versions of DevTools don't have urls set in their localStorage if ('url' in serializedHistory[i] && serializedHistory[i].url) { items.push(HistoryItem.fromObject(serializedHistory[i])); } } return new History(items); } index(url) { const index = this._itemsIndex.get(url); if (index !== undefined) { return index; } return -1; } _rebuildItemIndex() { 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); } } selectionRange(url) { const index = this.index(url); return index !== -1 ? this._items[index].selectionRange : undefined; } updateSelectionRange(url, selectionRange) { if (!selectionRange) { return; } const index = this.index(url); if (index === -1) { return; } this._items[index].selectionRange = selectionRange; } scrollLineNumber(url) { const index = this.index(url); return index !== -1 ? this._items[index].scrollLineNumber : undefined; } updateScrollLineNumber(url, scrollLineNumber) { const index = this.index(url); if (index === -1) { return; } this._items[index].scrollLineNumber = scrollLineNumber; } 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 HistoryItem(urls[i]); } this._items.unshift(item); this._rebuildItemIndex(); } } remove(url) { const index = this.index(url); if (index !== -1) { this._items.splice(index, 1); this._rebuildItemIndex(); } } save(setting) { setting.set(this._serializeToObject()); } _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 === maximalPreviouslyViewedFilesCount) { break; } } return serializedHistory; } _urls() { const result = []; for (let i = 0; i < this._items.length; ++i) { result.push(this._items[i].url); } return result; } } export class EditorContainerTabDelegate { _editorContainer; constructor(editorContainer) { this._editorContainer = editorContainer; } closeTabs(_tabbedPane, ids) { this._editorContainer._closeTabs(ids); } onContextMenu(tabId, contextMenu) { this._editorContainer._onContextMenu(tabId, contextMenu); } }