blob: f5455cacf5e2219ac4a6d31f1c1cd45bffc231aa [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @implements {Sources.TabbedEditorContainerDelegate}
* @implements {UI.Searchable}
* @implements {UI.Replaceable}
* @unrestricted
*/
Sources.SourcesView = class extends UI.VBox {
/**
* @suppressGlobalPropertiesCheck
*/
constructor() {
super();
this.registerRequiredCSS('sources/sourcesView.css');
this.element.id = 'sources-panel-sources-view';
this.setMinimumAndPreferredSizes(50, 52, 150, 100);
var workspace = Workspace.workspace;
this._searchableView = new UI.SearchableView(this, 'sourcesViewSearchConfig');
this._searchableView.setMinimalSearchQuerySize(0);
this._searchableView.show(this.element);
/** @type {!Map.<!Workspace.UISourceCode, !UI.Widget>} */
this._sourceViewByUISourceCode = new Map();
var tabbedEditorPlaceholderText =
Host.isMac() ? Common.UIString('Hit \u2318+P to open a file') : Common.UIString('Hit Ctrl+P to open a file');
this._editorContainer = new Sources.TabbedEditorContainer(
this, Common.settings.createLocalSetting('previouslyViewedFiles', []), tabbedEditorPlaceholderText);
this._editorContainer.show(this._searchableView.element);
this._editorContainer.addEventListener(
Sources.TabbedEditorContainer.Events.EditorSelected, this._editorSelected, this);
this._editorContainer.addEventListener(Sources.TabbedEditorContainer.Events.EditorClosed, this._editorClosed, this);
this._historyManager = new Sources.EditingLocationHistoryManager(this, this.currentSourceFrame.bind(this));
this._toolbarContainerElement = this.element.createChild('div', 'sources-toolbar');
this._toolbarEditorActions = new UI.Toolbar('', this._toolbarContainerElement);
self.runtime.allInstances(Sources.SourcesView.EditorAction).then(appendButtonsForExtensions.bind(this));
/**
* @param {!Array.<!Sources.SourcesView.EditorAction>} actions
* @this {Sources.SourcesView}
*/
function appendButtonsForExtensions(actions) {
for (var i = 0; i < actions.length; ++i)
this._toolbarEditorActions.appendToolbarItem(actions[i].button(this));
}
this._scriptViewToolbar = new UI.Toolbar('', this._toolbarContainerElement);
this._scriptViewToolbar.element.style.flex = 'auto';
this._bottomToolbar = new UI.Toolbar('', this._toolbarContainerElement);
UI.startBatchUpdate();
workspace.uiSourceCodes().forEach(this._addUISourceCode.bind(this));
UI.endBatchUpdate();
workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAdded, this);
workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeRemoved, this._uiSourceCodeRemoved, this);
workspace.addEventListener(Workspace.Workspace.Events.ProjectRemoved, this._projectRemoved.bind(this), this);
/**
* @param {!Event} event
*/
function handleBeforeUnload(event) {
if (event.returnValue)
return;
var unsavedSourceCodes = [];
var projects = Workspace.workspace.projectsForType(Workspace.projectTypes.FileSystem);
for (var i = 0; i < projects.length; ++i)
unsavedSourceCodes = unsavedSourceCodes.concat(projects[i].uiSourceCodes().filter(isUnsaved));
if (!unsavedSourceCodes.length)
return;
event.returnValue = Common.UIString('DevTools have unsaved changes that will be permanently lost.');
UI.viewManager.showView('sources');
for (var i = 0; i < unsavedSourceCodes.length; ++i)
Common.Revealer.reveal(unsavedSourceCodes[i]);
/**
* @param {!Workspace.UISourceCode} sourceCode
* @return {boolean}
*/
function isUnsaved(sourceCode) {
var binding = Persistence.persistence.binding(sourceCode);
if (binding)
return binding.network.isDirty();
return sourceCode.isDirty();
}
}
if (!window.opener)
window.addEventListener('beforeunload', handleBeforeUnload, true);
this._shortcuts = {};
this.element.addEventListener('keydown', this._handleKeyDown.bind(this), false);
}
/**
* @param {function(!Array.<!UI.KeyboardShortcut.Descriptor>, function(!Event=):boolean)} registerShortcutDelegate
*/
registerShortcuts(registerShortcutDelegate) {
/**
* @this {Sources.SourcesView}
* @param {!Array.<!UI.KeyboardShortcut.Descriptor>} shortcuts
* @param {function(!Event=):boolean} handler
*/
function registerShortcut(shortcuts, handler) {
registerShortcutDelegate(shortcuts, handler);
this._registerShortcuts(shortcuts, handler);
}
registerShortcut.call(
this, Components.ShortcutsScreen.SourcesPanelShortcuts.JumpToPreviousLocation,
this._onJumpToPreviousLocation.bind(this));
registerShortcut.call(
this, Components.ShortcutsScreen.SourcesPanelShortcuts.JumpToNextLocation,
this._onJumpToNextLocation.bind(this));
registerShortcut.call(
this, Components.ShortcutsScreen.SourcesPanelShortcuts.CloseEditorTab, this._onCloseEditorTab.bind(this));
registerShortcut.call(
this, Components.ShortcutsScreen.SourcesPanelShortcuts.GoToLine, this._showGoToLineDialog.bind(this));
registerShortcut.call(
this, Components.ShortcutsScreen.SourcesPanelShortcuts.GoToMember, this._showOutlineDialog.bind(this));
registerShortcut.call(
this, Components.ShortcutsScreen.SourcesPanelShortcuts.ToggleBreakpoint, this._toggleBreakpoint.bind(this));
registerShortcut.call(this, Components.ShortcutsScreen.SourcesPanelShortcuts.Save, this._save.bind(this));
registerShortcut.call(this, Components.ShortcutsScreen.SourcesPanelShortcuts.SaveAll, this._saveAll.bind(this));
}
/**
* @return {!UI.Toolbar}
*/
leftToolbar() {
return this._editorContainer.leftToolbar();
}
/**
* @return {!UI.Toolbar}
*/
rightToolbar() {
return this._editorContainer.rightToolbar();
}
/**
* @return {!UI.Toolbar}
*/
bottomToolbar() {
return this._bottomToolbar;
}
/**
* @param {!Array.<!UI.KeyboardShortcut.Descriptor>} keys
* @param {function(!Event=):boolean} handler
*/
_registerShortcuts(keys, handler) {
for (var i = 0; i < keys.length; ++i)
this._shortcuts[keys[i].key] = handler;
}
_handleKeyDown(event) {
var shortcutKey = UI.KeyboardShortcut.makeKeyFromEvent(event);
var handler = this._shortcuts[shortcutKey];
if (handler && handler())
event.consume(true);
}
/**
* @override
*/
wasShown() {
super.wasShown();
UI.context.setFlavor(Sources.SourcesView, this);
}
/**
* @override
*/
willHide() {
UI.context.setFlavor(Sources.SourcesView, null);
super.willHide();
}
/**
* @return {!Element}
*/
toolbarContainerElement() {
return this._toolbarContainerElement;
}
/**
* @return {!UI.SearchableView}
*/
searchableView() {
return this._searchableView;
}
/**
* @return {?UI.Widget}
*/
visibleView() {
return this._editorContainer.visibleView;
}
/**
* @return {?Sources.UISourceCodeFrame}
*/
currentSourceFrame() {
var view = this.visibleView();
if (!(view instanceof Sources.UISourceCodeFrame))
return null;
return /** @type {!Sources.UISourceCodeFrame} */ (view);
}
/**
* @return {?Workspace.UISourceCode}
*/
currentUISourceCode() {
return this._editorContainer.currentFile();
}
/**
* @param {!Event=} event
*/
_onCloseEditorTab(event) {
var uiSourceCode = this._editorContainer.currentFile();
if (!uiSourceCode)
return false;
this._editorContainer.closeFile(uiSourceCode);
return true;
}
/**
* @param {!Event=} event
*/
_onJumpToPreviousLocation(event) {
this._historyManager.rollback();
return true;
}
/**
* @param {!Event=} event
*/
_onJumpToNextLocation(event) {
this._historyManager.rollover();
return true;
}
/**
* @param {!Common.Event} event
*/
_uiSourceCodeAdded(event) {
var uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data);
this._addUISourceCode(uiSourceCode);
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
*/
_addUISourceCode(uiSourceCode) {
if (uiSourceCode.isFromServiceProject())
return;
this._editorContainer.addUISourceCode(uiSourceCode);
}
_uiSourceCodeRemoved(event) {
var uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data);
this._removeUISourceCodes([uiSourceCode]);
}
/**
* @param {!Array.<!Workspace.UISourceCode>} uiSourceCodes
*/
_removeUISourceCodes(uiSourceCodes) {
this._editorContainer.removeUISourceCodes(uiSourceCodes);
for (var i = 0; i < uiSourceCodes.length; ++i) {
this._removeSourceFrame(uiSourceCodes[i]);
this._historyManager.removeHistoryForSourceCode(uiSourceCodes[i]);
}
}
_projectRemoved(event) {
var project = event.data;
var uiSourceCodes = project.uiSourceCodes();
this._removeUISourceCodes(uiSourceCodes);
}
_updateScriptViewToolbarItems() {
this._scriptViewToolbar.removeToolbarItems();
var view = this.visibleView();
if (view instanceof UI.SimpleView) {
for (var item of (/** @type {?UI.SimpleView} */ (view)).syncToolbarItems())
this._scriptViewToolbar.appendToolbarItem(item);
}
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @param {number=} lineNumber 0-based
* @param {number=} columnNumber
* @param {boolean=} omitFocus
* @param {boolean=} omitHighlight
*/
showSourceLocation(uiSourceCode, lineNumber, columnNumber, omitFocus, omitHighlight) {
this._historyManager.updateCurrentState();
this._editorContainer.showFile(uiSourceCode);
var currentSourceFrame = this.currentSourceFrame();
if (currentSourceFrame && typeof lineNumber === 'number')
currentSourceFrame.revealPosition(lineNumber, columnNumber, !omitHighlight);
this._historyManager.pushNewState();
if (!omitFocus)
this.visibleView().focus();
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {!UI.Widget}
*/
_createSourceView(uiSourceCode) {
var sourceFrame;
var sourceView;
var contentType = uiSourceCode.contentType();
if (contentType.hasScripts())
sourceFrame = new Sources.JavaScriptSourceFrame(uiSourceCode);
else if (contentType.isStyleSheet())
sourceFrame = new Sources.CSSSourceFrame(uiSourceCode);
else if (contentType === Common.resourceTypes.Image)
sourceView = new SourceFrame.ImageView(Bindings.NetworkProject.uiSourceCodeMimeType(uiSourceCode), uiSourceCode);
else if (contentType === Common.resourceTypes.Font)
sourceView = new SourceFrame.FontView(Bindings.NetworkProject.uiSourceCodeMimeType(uiSourceCode), uiSourceCode);
else
sourceFrame = new Sources.UISourceCodeFrame(uiSourceCode);
if (sourceFrame) {
sourceFrame.setHighlighterType(Bindings.NetworkProject.uiSourceCodeMimeType(uiSourceCode));
this._historyManager.trackSourceFrameCursorJumps(sourceFrame);
}
var widget = /** @type {!UI.Widget} */ (sourceFrame || sourceView);
this._sourceViewByUISourceCode.set(uiSourceCode, widget);
uiSourceCode.addEventListener(Workspace.UISourceCode.Events.TitleChanged, this._uiSourceCodeTitleChanged, this);
return widget;
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {!UI.Widget}
*/
_getOrCreateSourceView(uiSourceCode) {
return this._sourceViewByUISourceCode.get(uiSourceCode) || this._createSourceView(uiSourceCode);
}
/**
* @param {!Sources.UISourceCodeFrame} sourceFrame
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {boolean}
*/
_sourceFrameMatchesUISourceCode(sourceFrame, uiSourceCode) {
if (uiSourceCode.contentType().hasScripts())
return sourceFrame instanceof Sources.JavaScriptSourceFrame;
if (uiSourceCode.contentType().isStyleSheet())
return sourceFrame instanceof Sources.CSSSourceFrame;
return !(sourceFrame instanceof Sources.JavaScriptSourceFrame);
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
*/
_recreateSourceFrameIfNeeded(uiSourceCode) {
var oldSourceView = this._sourceViewByUISourceCode.get(uiSourceCode);
if (!oldSourceView || !(oldSourceView instanceof Sources.UISourceCodeFrame))
return;
var oldSourceFrame = /** @type {!Sources.UISourceCodeFrame} */ (oldSourceView);
if (this._sourceFrameMatchesUISourceCode(oldSourceFrame, uiSourceCode)) {
oldSourceFrame.setHighlighterType(Bindings.NetworkProject.uiSourceCodeMimeType(uiSourceCode));
} else {
this._editorContainer.removeUISourceCode(uiSourceCode);
this._removeSourceFrame(uiSourceCode);
}
}
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {!UI.Widget}
*/
viewForFile(uiSourceCode) {
return this._getOrCreateSourceView(uiSourceCode);
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
*/
_removeSourceFrame(uiSourceCode) {
var sourceView = this._sourceViewByUISourceCode.get(uiSourceCode);
this._sourceViewByUISourceCode.remove(uiSourceCode);
uiSourceCode.removeEventListener(Workspace.UISourceCode.Events.TitleChanged, this._uiSourceCodeTitleChanged, this);
if (sourceView && sourceView instanceof Sources.UISourceCodeFrame)
/** @type {!Sources.UISourceCodeFrame} */ (sourceView).dispose();
}
clearCurrentExecutionLine() {
if (this._executionSourceFrame)
this._executionSourceFrame.clearExecutionLine();
delete this._executionSourceFrame;
}
/**
* @param {!Workspace.UILocation} uiLocation
*/
setExecutionLocation(uiLocation) {
var sourceView = this._getOrCreateSourceView(uiLocation.uiSourceCode);
if (sourceView instanceof Sources.UISourceCodeFrame) {
var sourceFrame = /** @type {!Sources.UISourceCodeFrame} */ (sourceView);
sourceFrame.setExecutionLocation(uiLocation);
this._executionSourceFrame = sourceFrame;
}
}
/**
* @param {!Common.Event} event
*/
_editorClosed(event) {
var uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data);
this._historyManager.removeHistoryForSourceCode(uiSourceCode);
var wasSelected = false;
if (!this._editorContainer.currentFile())
wasSelected = true;
// SourcesNavigator does not need to update on EditorClosed.
this._updateScriptViewToolbarItems();
this._searchableView.resetSearch();
var data = {};
data.uiSourceCode = uiSourceCode;
data.wasSelected = wasSelected;
this.dispatchEventToListeners(Sources.SourcesView.Events.EditorClosed, data);
}
/**
* @param {!Common.Event} event
*/
_editorSelected(event) {
var previousSourceFrame =
event.data.previousView instanceof Sources.UISourceCodeFrame ? event.data.previousView : null;
if (previousSourceFrame)
previousSourceFrame.setSearchableView(null);
var currentSourceFrame =
event.data.currentView instanceof Sources.UISourceCodeFrame ? event.data.currentView : null;
if (currentSourceFrame)
currentSourceFrame.setSearchableView(this._searchableView);
this._searchableView.setReplaceable(!!currentSourceFrame && currentSourceFrame.canEditSource());
this._searchableView.refreshSearch();
this._updateScriptViewToolbarItems();
this.dispatchEventToListeners(Sources.SourcesView.Events.EditorSelected, this._editorContainer.currentFile());
}
/**
* @param {!Common.Event} event
*/
_uiSourceCodeTitleChanged(event) {
this._recreateSourceFrameIfNeeded(/** @type {!Workspace.UISourceCode} */ (event.target));
}
/**
* @override
*/
searchCanceled() {
if (this._searchView)
this._searchView.searchCanceled();
delete this._searchView;
delete this._searchConfig;
}
/**
* @override
* @param {!UI.SearchableView.SearchConfig} searchConfig
* @param {boolean} shouldJump
* @param {boolean=} jumpBackwards
*/
performSearch(searchConfig, shouldJump, jumpBackwards) {
var sourceFrame = this.currentSourceFrame();
if (!sourceFrame)
return;
this._searchView = sourceFrame;
this._searchConfig = searchConfig;
this._searchView.performSearch(this._searchConfig, shouldJump, jumpBackwards);
}
/**
* @override
*/
jumpToNextSearchResult() {
if (!this._searchView)
return;
if (this._searchView !== this.currentSourceFrame()) {
this.performSearch(this._searchConfig, true);
return;
}
this._searchView.jumpToNextSearchResult();
}
/**
* @override
*/
jumpToPreviousSearchResult() {
if (!this._searchView)
return;
if (this._searchView !== this.currentSourceFrame()) {
this.performSearch(this._searchConfig, true);
if (this._searchView)
this._searchView.jumpToLastSearchResult();
return;
}
this._searchView.jumpToPreviousSearchResult();
}
/**
* @override
* @return {boolean}
*/
supportsCaseSensitiveSearch() {
return true;
}
/**
* @override
* @return {boolean}
*/
supportsRegexSearch() {
return true;
}
/**
* @override
* @param {!UI.SearchableView.SearchConfig} searchConfig
* @param {string} replacement
*/
replaceSelectionWith(searchConfig, replacement) {
var sourceFrame = this.currentSourceFrame();
if (!sourceFrame) {
console.assert(sourceFrame);
return;
}
sourceFrame.replaceSelectionWith(searchConfig, replacement);
}
/**
* @override
* @param {!UI.SearchableView.SearchConfig} searchConfig
* @param {string} replacement
*/
replaceAllWith(searchConfig, replacement) {
var sourceFrame = this.currentSourceFrame();
if (!sourceFrame) {
console.assert(sourceFrame);
return;
}
sourceFrame.replaceAllWith(searchConfig, replacement);
}
/**
* @param {!Event=} event
* @return {boolean}
*/
_showOutlineDialog(event) {
var uiSourceCode = this._editorContainer.currentFile();
if (!uiSourceCode)
return false;
if (uiSourceCode.contentType().hasScripts()) {
Sources.JavaScriptOutlineDialog.show(uiSourceCode, this.showSourceLocation.bind(this, uiSourceCode));
return true;
}
if (uiSourceCode.contentType().isStyleSheet()) {
Sources.StyleSheetOutlineDialog.show(uiSourceCode, this.showSourceLocation.bind(this, uiSourceCode));
return true;
}
// We don't want default browser shortcut to be executed, so pretend to handle this event.
return true;
}
/**
* @param {string=} query
*/
showOpenResourceDialog(query) {
var uiSourceCodes = this._editorContainer.historyUISourceCodes();
/** @type {!Map.<!Workspace.UISourceCode, number>} */
var defaultScores = new Map();
for (var i = 1; i < uiSourceCodes.length; ++i) // Skip current element
defaultScores.set(uiSourceCodes[i], uiSourceCodes.length - i);
if (!this._openResourceDialogHistory)
this._openResourceDialogHistory = [];
Sources.OpenResourceDialog.show(this, query || '', defaultScores, this._openResourceDialogHistory);
}
/**
* @param {!Event=} event
* @return {boolean}
*/
_showGoToLineDialog(event) {
if (this._editorContainer.currentFile())
this.showOpenResourceDialog(':');
return true;
}
/**
* @return {boolean}
*/
_save() {
this._saveSourceFrame(this.currentSourceFrame());
return true;
}
/**
* @return {boolean}
*/
_saveAll() {
var sourceFrames = this._editorContainer.fileViews();
sourceFrames.forEach(this._saveSourceFrame.bind(this));
return true;
}
/**
* @param {?UI.Widget} sourceFrame
*/
_saveSourceFrame(sourceFrame) {
if (!(sourceFrame instanceof Sources.UISourceCodeFrame))
return;
var uiSourceCodeFrame = /** @type {!Sources.UISourceCodeFrame} */ (sourceFrame);
uiSourceCodeFrame.commitEditing();
}
/**
* @return {boolean}
*/
_toggleBreakpoint() {
var sourceFrame = this.currentSourceFrame();
if (!sourceFrame)
return false;
if (sourceFrame instanceof Sources.JavaScriptSourceFrame) {
var javaScriptSourceFrame = /** @type {!Sources.JavaScriptSourceFrame} */ (sourceFrame);
javaScriptSourceFrame.toggleBreakpointOnCurrentLine();
return true;
}
return false;
}
/**
* @param {boolean} active
*/
toggleBreakpointsActiveState(active) {
this._editorContainer.view.element.classList.toggle('breakpoints-deactivated', !active);
}
};
/** @enum {symbol} */
Sources.SourcesView.Events = {
EditorClosed: Symbol('EditorClosed'),
EditorSelected: Symbol('EditorSelected'),
};
/**
* @interface
*/
Sources.SourcesView.EditorAction = function() {};
Sources.SourcesView.EditorAction.prototype = {
/**
* @param {!Sources.SourcesView} sourcesView
* @return {!UI.ToolbarButton}
*/
button: function(sourcesView) {}
};
/**
* @implements {UI.ActionDelegate}
* @unrestricted
*/
Sources.SourcesView.SwitchFileActionDelegate = class {
/**
* @param {!Workspace.UISourceCode} currentUISourceCode
* @return {?Workspace.UISourceCode}
*/
static _nextFile(currentUISourceCode) {
/**
* @param {string} name
* @return {string}
*/
function fileNamePrefix(name) {
var lastDotIndex = name.lastIndexOf('.');
var namePrefix = name.substr(0, lastDotIndex !== -1 ? lastDotIndex : name.length);
return namePrefix.toLowerCase();
}
var uiSourceCodes = currentUISourceCode.project().uiSourceCodes();
var candidates = [];
var url = currentUISourceCode.parentURL();
var name = currentUISourceCode.name();
var namePrefix = fileNamePrefix(name);
for (var i = 0; i < uiSourceCodes.length; ++i) {
var uiSourceCode = uiSourceCodes[i];
if (url !== uiSourceCode.parentURL())
continue;
if (fileNamePrefix(uiSourceCode.name()) === namePrefix)
candidates.push(uiSourceCode.name());
}
candidates.sort(String.naturalOrderComparator);
var index = mod(candidates.indexOf(name) + 1, candidates.length);
var fullURL = (url ? url + '/' : '') + candidates[index];
var nextUISourceCode = currentUISourceCode.project().uiSourceCodeForURL(fullURL);
return nextUISourceCode !== currentUISourceCode ? nextUISourceCode : null;
}
/**
* @override
* @param {!UI.Context} context
* @param {string} actionId
* @return {boolean}
*/
handleAction(context, actionId) {
var sourcesView = UI.context.flavor(Sources.SourcesView);
var currentUISourceCode = sourcesView.currentUISourceCode();
if (!currentUISourceCode)
return false;
var nextUISourceCode = Sources.SourcesView.SwitchFileActionDelegate._nextFile(currentUISourceCode);
if (!nextUISourceCode)
return false;
sourcesView.showSourceLocation(nextUISourceCode);
return true;
}
};
/**
* @implements {UI.ActionDelegate}
* @unrestricted
*/
Sources.SourcesView.CloseAllActionDelegate = class {
/**
* @override
* @param {!UI.Context} context
* @param {string} actionId
* @return {boolean}
*/
handleAction(context, actionId) {
var sourcesView = UI.context.flavor(Sources.SourcesView);
if (!sourcesView)
return false;
sourcesView._editorContainer.closeAllFiles();
return true;
}
};