blob: 1420585297df38b55b88a5432b3f6cce252a115a [file] [log] [blame]
/*
* Copyright (C) 2012 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.
*/
/**
* @unrestricted
* @implements {SDK.SDKModelObserver<!SDK.DebuggerModel>}
* @implements {Bindings.DebuggerSourceMapping}
*/
Snippets.ScriptSnippetModel = class extends Common.Object {
/**
* @param {!Workspace.Workspace} workspace
*/
constructor(workspace) {
super();
this._workspace = workspace;
/** @type {!Object.<string, !Workspace.UISourceCode>} */
this._uiSourceCodeForSnippetId = {};
/** @type {!Map.<!Workspace.UISourceCode, string>} */
this._snippetIdForUISourceCode = new Map();
/** @type {!Map.<!SDK.DebuggerModel, !Snippets.SnippetScriptMapping>} */
this._mappingForDebuggerModel = new Map();
this._snippetStorage = new Snippets.SnippetStorage('script', 'Script snippet #');
this._lastSnippetEvaluationIndexSetting = Common.settings.createSetting('lastSnippetEvaluationIndex', 0);
this._project = new Snippets.SnippetsProject(workspace, this);
this._loadSnippets();
SDK.targetManager.observeModels(SDK.DebuggerModel, this);
Bindings.debuggerWorkspaceBinding.addSourceMapping(this);
}
/**
* @override
* @param {!SDK.DebuggerModel} debuggerModel
*/
modelAdded(debuggerModel) {
this._mappingForDebuggerModel.set(debuggerModel, new Snippets.SnippetScriptMapping(debuggerModel, this));
}
/**
* @override
* @param {!SDK.DebuggerModel} debuggerModel
*/
modelRemoved(debuggerModel) {
this._mappingForDebuggerModel.remove(debuggerModel);
}
/**
* @override
* @param {!SDK.DebuggerModel.Location} rawLocation
* @return {?Workspace.UILocation}
*/
rawLocationToUILocation(rawLocation) {
const mapping = this._mappingForDebuggerModel.get(rawLocation.debuggerModel);
if (!mapping)
return null;
return mapping.rawLocationToUILocation(rawLocation);
}
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
* @param {number} lineNumber
* @param {number} columnNumber
* @return {?SDK.DebuggerModel.Location}
*/
uiLocationToRawLocation(uiSourceCode, lineNumber, columnNumber) {
for (const mapping of this._mappingForDebuggerModel.values()) {
const rawLocation = mapping.uiLocationToRawLocation(uiSourceCode, lineNumber, columnNumber);
if (rawLocation)
return rawLocation;
}
return null;
}
/**
* @param {!SDK.DebuggerModel} debuggerModel
* @return {!Snippets.SnippetScriptMapping|undefined}
*/
snippetScriptMapping(debuggerModel) {
return this._mappingForDebuggerModel.get(debuggerModel);
}
/**
* @return {!Workspace.Project}
*/
project() {
return this._project;
}
_loadSnippets() {
for (const snippet of this._snippetStorage.snippets())
this._addScriptSnippet(snippet);
}
/**
* @param {string} content
* @return {!Workspace.UISourceCode}
*/
createScriptSnippet(content) {
const snippet = this._snippetStorage.createSnippet();
snippet.content = content;
return this._addScriptSnippet(snippet);
}
/**
* @param {!Snippets.Snippet} snippet
* @return {!Workspace.UISourceCode}
*/
_addScriptSnippet(snippet) {
const uiSourceCode = this._project.addSnippet(snippet.name, new Snippets.SnippetContentProvider(snippet));
uiSourceCode.addEventListener(Workspace.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this);
this._snippetIdForUISourceCode.set(uiSourceCode, snippet.id);
const breakpointLocations = this._removeBreakpoints(uiSourceCode);
this._restoreBreakpoints(uiSourceCode, breakpointLocations);
this._uiSourceCodeForSnippetId[snippet.id] = uiSourceCode;
return uiSourceCode;
}
/**
* @param {!Common.Event} event
*/
_workingCopyChanged(event) {
const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data);
this._scriptSnippetEdited(uiSourceCode);
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
*/
deleteScriptSnippet(uiSourceCode) {
const snippetId = this._snippetIdForUISourceCode.get(uiSourceCode) || '';
const snippet = this._snippetStorage.snippetForId(snippetId);
if (!snippet)
return;
this._snippetStorage.deleteSnippet(snippet);
this._removeBreakpoints(uiSourceCode);
this._releaseSnippetScript(uiSourceCode);
delete this._uiSourceCodeForSnippetId[snippet.id];
this._snippetIdForUISourceCode.remove(uiSourceCode);
this._project.removeFile(snippet.name);
}
/**
* @param {string} name
* @param {string} newName
* @param {function(boolean, string=)} callback
*/
renameScriptSnippet(name, newName, callback) {
newName = newName.trim();
if (!newName || newName.indexOf('/') !== -1 || name === newName || this._snippetStorage.snippetForName(newName)) {
callback(false);
return;
}
const snippet = this._snippetStorage.snippetForName(name);
console.assert(snippet, 'Snippet \'' + name + '\' was not found.');
const uiSourceCode = this._uiSourceCodeForSnippetId[snippet.id];
console.assert(uiSourceCode, 'No uiSourceCode was found for snippet \'' + name + '\'.');
const breakpointLocations = this._removeBreakpoints(uiSourceCode);
snippet.name = newName;
this._restoreBreakpoints(uiSourceCode, breakpointLocations);
callback(true, newName);
}
/**
* @param {string} name
* @param {string} newContent
*/
_setScriptSnippetContent(name, newContent) {
const snippet = this._snippetStorage.snippetForName(name);
snippet.content = newContent;
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
*/
_scriptSnippetEdited(uiSourceCode) {
const breakpointLocations = this._removeBreakpoints(uiSourceCode);
this._releaseSnippetScript(uiSourceCode);
this._restoreBreakpoints(uiSourceCode, breakpointLocations);
this._mappingForDebuggerModel.valuesArray().forEach(function(mapping) {
mapping._restoreBreakpoints(uiSourceCode, breakpointLocations);
});
}
/**
* @return {number}
*/
_nextEvaluationIndex() {
const evaluationIndex = this._lastSnippetEvaluationIndexSetting.get() + 1;
this._lastSnippetEvaluationIndexSetting.set(evaluationIndex);
return evaluationIndex;
}
/**
* @param {!SDK.ExecutionContext} executionContext
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {!Promise<undefined>}
*/
async evaluateScriptSnippet(executionContext, uiSourceCode) {
console.assert(uiSourceCode.project().type() === Workspace.projectTypes.Snippets);
let breakpointLocations = this._removeBreakpoints(uiSourceCode);
this._releaseSnippetScript(uiSourceCode);
this._restoreBreakpoints(uiSourceCode, breakpointLocations);
const runtimeModel = executionContext.runtimeModel;
const debuggerModel = executionContext.debuggerModel;
const evaluationIndex = this._nextEvaluationIndex();
const mapping = this._mappingForDebuggerModel.get(debuggerModel);
mapping._setEvaluationIndex(evaluationIndex, uiSourceCode);
const evaluationUrl = mapping._evaluationSourceURL(uiSourceCode);
await uiSourceCode.requestContent();
const expression = uiSourceCode.workingCopy();
Common.console.show();
const result = await runtimeModel.compileScript(expression, '', true, executionContext.id);
if (!result || mapping.evaluationIndex(uiSourceCode) !== evaluationIndex)
return;
const script = /** @type {!SDK.Script} */ (
debuggerModel.scriptForId(/** @type {string} */ (result.scriptId || result.exceptionDetails.scriptId)));
mapping._addScript(script, uiSourceCode);
if (!result.scriptId) {
this._printRunOrCompileScriptResultFailure(
runtimeModel, /** @type {!Protocol.Runtime.ExceptionDetails} */ (result.exceptionDetails), evaluationUrl);
return;
}
breakpointLocations = this._removeBreakpoints(uiSourceCode);
this._restoreBreakpoints(uiSourceCode, breakpointLocations);
this._runScript(script.scriptId, executionContext, evaluationUrl);
}
/**
* @param {!Protocol.Runtime.ScriptId} scriptId
* @param {!SDK.ExecutionContext} executionContext
* @param {?string=} sourceURL
*/
async _runScript(scriptId, executionContext, sourceURL) {
const runtimeModel = executionContext.runtimeModel;
const result = await runtimeModel.runScript(
scriptId, executionContext.id, 'console', /* silent */ false, /* includeCommandLineAPI */ true,
/* returnByValue */ false, /* generatePreview */ true);
if (result.error)
return;
if (!result.exceptionDetails)
this._printRunScriptResult(runtimeModel, result.object || null, scriptId, sourceURL);
else
this._printRunOrCompileScriptResultFailure(runtimeModel, result.exceptionDetails, sourceURL);
}
/**
* @param {!SDK.RuntimeModel} runtimeModel
* @param {?SDK.RemoteObject} result
* @param {!Protocol.Runtime.ScriptId} scriptId
* @param {?string=} sourceURL
*/
_printRunScriptResult(runtimeModel, result, scriptId, sourceURL) {
const consoleMessage = new SDK.ConsoleMessage(
runtimeModel, SDK.ConsoleMessage.MessageSource.JS, SDK.ConsoleMessage.MessageLevel.Info, '',
SDK.ConsoleMessage.MessageType.Result, sourceURL, undefined, undefined, [result], undefined, undefined,
undefined, scriptId);
SDK.consoleModel.addMessage(consoleMessage);
}
/**
* @param {!SDK.RuntimeModel} runtimeModel
* @param {!Protocol.Runtime.ExceptionDetails} exceptionDetails
* @param {?string=} sourceURL
*/
_printRunOrCompileScriptResultFailure(runtimeModel, exceptionDetails, sourceURL) {
SDK.consoleModel.addMessage(
SDK.ConsoleMessage.fromException(runtimeModel, exceptionDetails, undefined, undefined, sourceURL || undefined));
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {!Array.<!{breakpoint: !Bindings.BreakpointManager.Breakpoint, uiLocation: !Workspace.UILocation}>}
*/
_removeBreakpoints(uiSourceCode) {
const breakpointLocations = Bindings.breakpointManager.breakpointLocationsForUISourceCode(uiSourceCode);
for (let i = 0; i < breakpointLocations.length; ++i)
breakpointLocations[i].breakpoint.remove(false /* keepInStorage */);
return breakpointLocations;
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @param {!Array.<!{breakpoint: !Bindings.BreakpointManager.Breakpoint, uiLocation: !Workspace.UILocation}>} breakpointLocations
*/
_restoreBreakpoints(uiSourceCode, breakpointLocations) {
for (let i = 0; i < breakpointLocations.length; ++i) {
const uiLocation = breakpointLocations[i].uiLocation;
const breakpoint = breakpointLocations[i].breakpoint;
Bindings.breakpointManager.setBreakpoint(
uiSourceCode, uiLocation.lineNumber, uiLocation.columnNumber, breakpoint.condition(), breakpoint.enabled());
}
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
*/
_releaseSnippetScript(uiSourceCode) {
this._mappingForDebuggerModel.valuesArray().forEach(function(mapping) {
mapping._releaseSnippetScript(uiSourceCode);
});
}
/**
* @param {string} sourceURL
* @return {?string}
*/
_snippetIdForSourceURL(sourceURL) {
const snippetPrefix = Snippets.ScriptSnippetModel.snippetSourceURLPrefix;
if (!sourceURL.startsWith(snippetPrefix))
return null;
const splitURL = sourceURL.substring(snippetPrefix.length).split('_');
const snippetId = splitURL[0];
return snippetId;
}
};
Snippets.ScriptSnippetModel.snippetSourceURLPrefix = 'snippets:///';
/**
* @unrestricted
*/
Snippets.SnippetScriptMapping = class {
/**
* @param {!SDK.DebuggerModel} debuggerModel
* @param {!Snippets.ScriptSnippetModel} scriptSnippetModel
*/
constructor(debuggerModel, scriptSnippetModel) {
this._debuggerModel = debuggerModel;
this._scriptSnippetModel = scriptSnippetModel;
/** @type {!Object.<string, !Workspace.UISourceCode>} */
this._uiSourceCodeForScriptId = {};
/** @type {!Map.<!Workspace.UISourceCode, !SDK.Script>} */
this._scriptForUISourceCode = new Map();
/** @type {!Map.<!Workspace.UISourceCode, number>} */
this._evaluationIndexForUISourceCode = new Map();
debuggerModel.addEventListener(SDK.DebuggerModel.Events.GlobalObjectCleared, this._reset, this);
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
*/
_releaseSnippetScript(uiSourceCode) {
const script = this._scriptForUISourceCode.get(uiSourceCode);
if (!script)
return;
delete this._uiSourceCodeForScriptId[script.scriptId];
this._scriptForUISourceCode.remove(uiSourceCode);
this._evaluationIndexForUISourceCode.remove(uiSourceCode);
}
/**
* @param {number} evaluationIndex
* @param {!Workspace.UISourceCode} uiSourceCode
*/
_setEvaluationIndex(evaluationIndex, uiSourceCode) {
this._evaluationIndexForUISourceCode.set(uiSourceCode, evaluationIndex);
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {number|undefined}
*/
evaluationIndex(uiSourceCode) {
return this._evaluationIndexForUISourceCode.get(uiSourceCode);
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {string}
*/
_evaluationSourceURL(uiSourceCode) {
const evaluationSuffix = '_' + this._evaluationIndexForUISourceCode.get(uiSourceCode);
const snippetId = this._scriptSnippetModel._snippetIdForUISourceCode.get(uiSourceCode);
return Snippets.ScriptSnippetModel.snippetSourceURLPrefix + snippetId + evaluationSuffix;
}
_reset() {
this._uiSourceCodeForScriptId = {};
this._scriptForUISourceCode.clear();
this._evaluationIndexForUISourceCode.clear();
}
/**
* @param {!SDK.DebuggerModel.Location} rawLocation
* @return {?Workspace.UILocation}
*/
rawLocationToUILocation(rawLocation) {
const debuggerModelLocation = /** @type {!SDK.DebuggerModel.Location} */ (rawLocation);
const uiSourceCode = this._uiSourceCodeForScriptId[debuggerModelLocation.scriptId];
if (!uiSourceCode)
return null;
return uiSourceCode.uiLocation(debuggerModelLocation.lineNumber, debuggerModelLocation.columnNumber || 0);
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @param {number} lineNumber
* @param {number} columnNumber
* @return {?SDK.DebuggerModel.Location}
*/
uiLocationToRawLocation(uiSourceCode, lineNumber, columnNumber) {
const script = this._scriptForUISourceCode.get(uiSourceCode);
if (!script)
return null;
return this._debuggerModel.createRawLocation(script, lineNumber, columnNumber);
}
/**
* @param {!SDK.Script} script
* @param {!Workspace.UISourceCode} uiSourceCode
*/
_addScript(script, uiSourceCode) {
console.assert(!this._scriptForUISourceCode.get(uiSourceCode));
this._uiSourceCodeForScriptId[script.scriptId] = uiSourceCode;
this._scriptForUISourceCode.set(uiSourceCode, script);
Bindings.debuggerWorkspaceBinding.updateLocations(script);
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @param {!Array.<!{breakpoint: !Bindings.BreakpointManager.Breakpoint, uiLocation: !Workspace.UILocation}>} breakpointLocations
*/
_restoreBreakpoints(uiSourceCode, breakpointLocations) {
const script = this._scriptForUISourceCode.get(uiSourceCode);
if (!script)
return;
const rawLocation =
/** @type {!SDK.DebuggerModel.Location} */ (this._debuggerModel.createRawLocation(script, 0, 0));
const uiLocation = Bindings.debuggerWorkspaceBinding.rawLocationToUILocation(rawLocation);
if (uiLocation)
this._scriptSnippetModel._restoreBreakpoints(uiLocation.uiSourceCode, breakpointLocations);
}
};
/**
* @implements {Common.ContentProvider}
* @unrestricted
*/
Snippets.SnippetContentProvider = class {
/**
* @param {!Snippets.Snippet} snippet
*/
constructor(snippet) {
this._snippet = snippet;
}
/**
* @override
* @return {string}
*/
contentURL() {
return '';
}
/**
* @override
* @return {!Common.ResourceType}
*/
contentType() {
return Common.resourceTypes.Snippet;
}
/**
* @override
* @return {!Promise<boolean>}
*/
contentEncoded() {
return Promise.resolve(false);
}
/**
* @override
* @return {!Promise<?string>}
*/
requestContent() {
return Promise.resolve(/** @type {?string} */ (this._snippet.content));
}
/**
* @override
* @param {string} query
* @param {boolean} caseSensitive
* @param {boolean} isRegex
* @return {!Promise<!Array<!Common.ContentProvider.SearchMatch>>}
*/
async searchInContent(query, caseSensitive, isRegex) {
return Common.ContentProvider.performSearchInContent(this._snippet.content, query, caseSensitive, isRegex);
}
};
/**
* @unrestricted
*/
Snippets.SnippetsProject = class extends Bindings.ContentProviderBasedProject {
/**
* @param {!Workspace.Workspace} workspace
* @param {!Snippets.ScriptSnippetModel} model
*/
constructor(workspace, model) {
super(workspace, 'snippets:', Workspace.projectTypes.Snippets, '', false /* isServiceProject */);
this._model = model;
}
/**
* @param {string} name
* @param {!Common.ContentProvider} contentProvider
* @return {!Workspace.UISourceCode}
*/
addSnippet(name, contentProvider) {
return this.addContentProvider(name, contentProvider, 'text/javascript');
}
/**
* @override
* @return {boolean}
*/
canSetFileContent() {
return true;
}
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
* @param {string} newContent
* @param {boolean} isBase64
* @return {!Promise}
*/
async setFileContent(uiSourceCode, newContent, isBase64) {
this._model._setScriptSnippetContent(uiSourceCode.url(), newContent);
}
/**
* @override
* @return {boolean}
*/
canRename() {
return true;
}
/**
* @override
* @param {string} url
* @param {string} newName
* @param {function(boolean, string=)} callback
*/
performRename(url, newName, callback) {
this._model.renameScriptSnippet(url, newName, callback);
}
/**
* @override
* @param {string} url
* @param {?string} name
* @param {string} content
* @param {boolean=} isBase64
* @return {!Promise<?Workspace.UISourceCode>}
*/
createFile(url, name, content, isBase64) {
return /** @type {!Promise<?Workspace.UISourceCode>} */ (Promise.resolve(this._model.createScriptSnippet(content)));
}
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
*/
deleteFile(uiSourceCode) {
this._model.deleteScriptSnippet(uiSourceCode);
}
};
/**
* @type {!Snippets.ScriptSnippetModel}
*/
Snippets.scriptSnippetModel = new Snippets.ScriptSnippetModel(Workspace.workspace);