|  | /* | 
|  | * 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 | 
|  | * @param {!Element=} focusedPlaceholderElement | 
|  | */ | 
|  | constructor(delegate, setting, placeholderElement, focusedPlaceholderElement) { | 
|  | super(); | 
|  | this._delegate = delegate; | 
|  |  | 
|  | this._tabbedPane = new UI.TabbedPane(); | 
|  | this._tabbedPane.setPlaceholderElement(placeholderElement, focusedPlaceholderElement); | 
|  | 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.tabIds()); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @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 currentProjectIsSnippets = Snippets.isSnippetsUISourceCode(this._currentFile); | 
|  | const addedProjectIsSnippets = Snippets.isSnippetsUISourceCode(uiSourceCode); | 
|  | if (this._history.index(this._currentFile.url()) && currentProjectIsSnippets && !addedProjectIsSnippets) { | 
|  | 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); | 
|  | if (uiSourceCode.loadError()) { | 
|  | this._addLoadErrorIcon(tabId); | 
|  | } else if (!uiSourceCode.contentLoaded()) { | 
|  | uiSourceCode.requestContent().then(content => { | 
|  | if (uiSourceCode.loadError()) { | 
|  | this._addLoadErrorIcon(tabId); | 
|  | } | 
|  | }); | 
|  | } | 
|  | return tabId; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {string} tabId | 
|  | */ | 
|  | _addLoadErrorIcon(tabId) { | 
|  | const icon = UI.Icon.create('smallicon-error'); | 
|  | icon.title = ls`Unable to load this content.`; | 
|  | if (this._tabbedPane.tabView(tabId)) { | 
|  | this._tabbedPane.setTabIcon(tabId, icon); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @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); | 
|  | const tooltip = this._tooltipForFile(uiSourceCode); | 
|  | this._tabbedPane.changeTabTitle(tabId, title, tooltip); | 
|  | let icon = null; | 
|  | if (uiSourceCode.loadError()) { | 
|  | icon = UI.Icon.create('smallicon-error'); | 
|  | icon.title = ls`Unable to load this content.`; | 
|  | } else 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); | 
|  | } | 
|  | }; |