From 23c03d7c6c3a03eca6396e8b563ec86f880a0dff Mon Sep 17 00:00:00 2001 From: Devin Rousso Date: Mon, 19 Sep 2022 12:09:07 -0700 Subject: [PATCH] Web Inspector: add support for console snippets https://bugs.webkit.org/show_bug.cgi?id=245161 Reviewed by Patrick Angle. Console Snippets can be very useful for developers that frequently run the same JS over and over. Previously, the developer would have to either repeatedly type the same thing, use the up arrow to find the JS in the Console history, or even use the pasteboard (likely from another app/file). With Console Snippets, the developer can rely on Web Inspector to organize and maintain the "library" of JS that the developer might want to invoke, as well as having quick ways to actually do so. * Source/WebInspectorUI/UserInterface/Models/ConsoleSnippet.js: Added. (WI.ConsoleSnippet): (WI.ConsoleSnippet.createDefaultWithTitle): (WI.ConsoleSnippet.prototype.get title): (WI.ConsoleSnippet.prototype.run): (WI.ConsoleSnippet.fromJSON): (WI.ConsoleSnippet.prototype.toJSON): * Source/WebInspectorUI/UserInterface/Controllers/ConsoleManager.js: (WI.ConsoleManager): (WI.ConsoleManager.prototype.get snippets): Added. (WI.ConsoleManager.prototype.addSnippet): Added. (WI.ConsoleManager.prototype.removeSnippet): Added. (WI.ConsoleManager.prototype._handleSnippetContentChanged): Added. * Source/WebInspectorUI/UserInterface/Views/ResourceIcons.css: (:is(.anonymous-script-icon, .resource-icon.resource-type-script.console-snippet) .icon): Renamed from `.anonymous-script-icon .icon`. (@media (prefers-color-scheme: dark) :is(.anonymous-script-icon, .resource-icon.resource-type-script.console-snippet) .icon): Renamed from `@media (prefers-color-scheme: dark) .anonymous-script-icon .icon`. Force Console Snippets to use the JS clipping icon. * Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.js: Added. (WI.ConsoleSnippetTreeElement): (WI.ConsoleSnippetTreeElement.prototype.onattach): (WI.ConsoleSnippetTreeElement.prototype.ondelete): (WI.ConsoleSnippetTreeElement.prototype.onspace): (WI.ConsoleSnippetTreeElement.prototype.canSelectOnMouseDown): (WI.ConsoleSnippetTreeElement.prototype.updateStatus): * Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.css: Added. (.tree-outline .item.console-snippet > .status): (.tree-outline .item.console-snippet > .status > img): (.tree-outline .item.console-snippet:not(:hover) > .status > img): (body:not(.window-inactive, .window-docked-inactive) .tree-outline:focus-within .item.console-snippet.selected:hover > .status > img): (@media (prefers-color-scheme: dark) .tree-outline .item.console-snippet:hover > .status > img): * Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.js: (WI.SourcesNavigationSidebarPanel): (WI.SourcesNavigationSidebarPanel.prototype.closed): (WI.SourcesNavigationSidebarPanel.prototype.createContentTreeOutline): (WI.SourcesNavigationSidebarPanel.prototype.willDismissPopover): (WI.SourcesNavigationSidebarPanel.prototype._willDismissConsoleSnippetPopover): Added. (WI.SourcesNavigationSidebarPanel.prototype._compareTreeElements): (WI.SourcesNavigationSidebarPanel.prototype._addConsoleSnippet): Added. (WI.SourcesNavigationSidebarPanel.prototype._removeConsoleSnippet): Added. (WI.SourcesNavigationSidebarPanel.prototype._handleCreateConsoleSnippetButtonClicked): Added. (WI.SourcesNavigationSidebarPanel.prototype.async _populateCreateResourceContextMenu): (WI.SourcesNavigationSidebarPanel.prototype._handleConsoleSnippetAdded): Added. (WI.SourcesNavigationSidebarPanel.prototype._handleConsoleSnippetRemoved): Added. * Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.css: (.sidebar > .panel.navigation.sources > .content > :matches(.pause-reason-container, .call-stack-container, .breakpoints-container, .local-overrides-container, .console-snippets-container)): Renamed from `.sidebar > .panel.navigation.sources > .content > :matches(.pause-reason-container, .call-stack-container, .breakpoints-container, .local-overrides-container)`. (@media (min-height: 650px) .sidebar > .panel.navigation.sources > .content > :matches(.call-stack-container, .breakpoints-container, .resources-container, .local-overrides-container, .console-snippets-container)): Renamed from `@media (min-height: 650px) .sidebar > .panel.navigation.sources > .content > :matches(.call-stack-container, .breakpoints-container, .resources-container, .local-overrides-container)`. (@media (min-height: 650px) .sidebar > .panel.navigation.sources > .content > .console-snippets-container): Added. (@media (min-height: 650px) .sidebar > .panel.navigation.sources > .content > .console-snippets-container > .details-section): Added. (@media (min-height: 650px) .sidebar > .panel.navigation.sources > .content > .console-snippets-container > .details-section > .content): Added. (.sidebar > .panel.navigation.sources > .content > .local-overrides): Deleted. Add a "Console Snippets" section to the navigation sidebar in the Sources Tab. * Source/WebInspectorUI/UserInterface/Views/LogContentView.js: (WI.LogContentView): (WI.LogContentView.prototype.get navigationItems): (WI.LogContentView.prototype.willDismissPopover): (WI.LogContentView.prototype._handleDrop): (WI.LogContentView.prototype._handleSnippetsNavigationItemContextMenu): Added. * Source/WebInspectorUI/UserInterface/Views/LogContentView.css: (.navigation-bar > .item.console-snippets > img): Added. (.console-user-command:not(:hover) > .console-snippet-options): Added. (.create-snippet-popover > .label): Added. (@media (prefers-color-scheme: dark) .navigation-bar > .item.console-snippets > img): Add a snippets button navigation item to the Console for quick access when not in the Sources Tab. * Source/WebInspectorUI/UserInterface/Views/OpenResourceDialog.js: (WI.OpenResourceDialog.prototype.didDismissDialog): (WI.OpenResourceDialog.prototype.didPresentDialog): (WI.OpenResourceDialog.prototype._handleConsoleSnippetAdded): Added. (WI.OpenResourceDialog.prototype._handleConsoleSnippetRemoved): Added. * Source/WebInspectorUI/UserInterface/Views/SearchSidebarPanel.js: (WI.SearchSidebarPanel.prototype.performSearch): Support jumping to Console Snippets. * Source/WebInspectorUI/UserInterface/Base/ObjectStore.js: (WI.ObjectStore._open): Bump the version for the new `console-snippets` store. * Source/WebInspectorUI/UserInterface/Views/ButtonNavigationItem.js: (WI.ButtonNavigationItem): (WI.ButtonNavigationItem.prototype._update): Wait to set the `title` until we know we know what `buttonStyle` we're using. * Source/WebInspectorUI/UserInterface/Views/ConsoleCommandView.js: (WI.ConsoleCommandView): (WI.ConsoleCommandView.prototype.render): Add a way for clients to add a `"click"` event listener after the command has `render()`. * Source/WebInspectorUI/UserInterface/Views/ConsoleDrawer.css: (.console-drawer > .navigation-bar > .item > :is(.glyph, img), .console-drawer > .navigation-bar > .log-scope-bar > li): Renamed from `.console-drawer > .navigation-bar > .item > .glyph, .console-drawer > .navigation-bar > .log-scope-bar > li`. Allow `WI.ButtonNavigationItem.ImageType.IMG` to be clickable. * Source/WebInspectorUI/UserInterface/Views/InputPopover.js: (WI.InputPopover): (WI.InputPopover.prototype.get identifier): Added. (WI.InputPopover.prototype.get value): (WI.InputPopover.prototype.show): (WI.InputPopover.prototype.get result): Deleted. It should be possible to get the `value` even if the developer has not hit enter/return. * Source/WebInspectorUI/UserInterface/Views/ScriptContentView.js: (WI.ScriptContentView.prototype._handleTextEditorContentDidChange): Debounce how often we save the current contents after modifications (to prevent swamping IndexedDB). * Source/WebInspectorUI/UserInterface/Base/Utilities.js: (Set.prototype.some): Added. Add utility function that behaves just like `Array.prototype.some`. * Source/WebInspectorUI/UserInterface/Controllers/JavaScriptLogViewController.js: (WI.JavaScriptLogViewController): (WI.JavaScriptLogViewController.prototype.startNewSession): (WI.JavaScriptLogViewController.prototype.appendImmediateExecutionWithResult): (WI.JavaScriptLogViewController.prototype.consolePromptTextCommitted): (WI.JavaScriptLogViewController.prototype._appendConsoleMessageView): * Source/WebInspectorUI/UserInterface/Controllers/DOMManager.js: (WI.DOMManager.prototype.inspectNodeObject.nodeAvailable): * Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js: (WI.AnimationContentView.prototype._populateAnimationTargetButtonContextMenu): * Source/WebInspectorUI/UserInterface/Views/CanvasContentView.js: (WI.CanvasContentView.prototype._populateCanvasElementButtonContextMenu): * Source/WebInspectorUI/UserInterface/Views/CanvasTreeElement.js: (WI.CanvasTreeElement.prototype.populateContextMenu): * Source/WebInspectorUI/UserInterface/Views/ContextMenuUtilities.js: * Source/WebInspectorUI/UserInterface/Views/HeapSnapshotInstanceDataGridNode.js: (WI.HeapSnapshotInstanceDataGridNode.logHeapSnapshotNode.node.shortestGCRootPath.): (WI.HeapSnapshotInstanceDataGridNode.logHeapSnapshotNode): * Source/WebInspectorUI/UserInterface/Views/ObjectPreviewView.js: (WI.ObjectPreviewView.prototype._contextMenuHandler): * Source/WebInspectorUI/UserInterface/Views/ObjectTreeBaseTreeElement.js: (WI.ObjectTreeBaseTreeElement.prototype._logSymbolProperty): (WI.ObjectTreeBaseTreeElement.prototype._logValue): * Source/WebInspectorUI/UserInterface/Views/QuickConsole.js: (WI.QuickConsole.prototype._handleDrop): * Source/WebInspectorUI/UserInterface/Views/WebSocketDataGridNode.js: (WI.WebSocketDataGridNode.prototype.appendContextMenuItems): * Source/WebInspectorUI/UserInterface/Views/WebSocketResourceTreeElement.js: (WI.WebSocketResourceTreeElement.prototype.populateContextMenu): Drive-by: Rework parameters of `appendImmediateExecutionWithResult` to use an `options = {}`. * Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js: * Source/WebInspectorUI/UserInterface/Images/Start.svg: Added. * Source/WebInspectorUI/UserInterface/Main.html: * Source/WebInspectorUI/UserInterface/Test.html: Canonical link: https://commits.webkit.org/254636@main --- .../unit-tests/set-utilities-expected.txt | 7 + .../inspector/unit-tests/set-utilities.html | 18 +++ .../en.lproj/localizedStrings.js | 13 ++ .../UserInterface/Base/ObjectStore.js | 5 +- .../UserInterface/Base/Utilities.js | 12 ++ .../Controllers/ConsoleManager.js | 61 +++++++- .../UserInterface/Controllers/DOMManager.js | 4 +- .../JavaScriptLogViewController.js | 23 ++- .../UserInterface/Images/Start.svg | 5 + Source/WebInspectorUI/UserInterface/Main.html | 3 + .../UserInterface/Models/ConsoleSnippet.js | 110 ++++++++++++++ Source/WebInspectorUI/UserInterface/Test.html | 1 + .../Views/AnimationContentView.js | 3 +- .../Views/ButtonNavigationItem.js | 19 ++- .../UserInterface/Views/CanvasContentView.js | 3 +- .../UserInterface/Views/CanvasTreeElement.js | 3 +- .../UserInterface/Views/ConsoleCommandView.js | 11 +- .../UserInterface/Views/ConsoleDrawer.css | 2 +- .../Views/ConsoleSnippetTreeElement.css | 51 +++++++ .../Views/ConsoleSnippetTreeElement.js | 76 ++++++++++ .../Views/ContextMenuUtilities.js | 3 +- .../Views/HeapSnapshotInstanceDataGridNode.js | 4 +- .../UserInterface/Views/InputPopover.js | 29 ++-- .../UserInterface/Views/LogContentView.css | 23 +++ .../UserInterface/Views/LogContentView.js | 57 ++++++- .../UserInterface/Views/ObjectPreviewView.js | 2 +- .../Views/ObjectTreeBaseTreeElement.js | 4 +- .../UserInterface/Views/OpenResourceDialog.js | 17 +++ .../UserInterface/Views/QuickConsole.js | 3 +- .../UserInterface/Views/ResourceIcons.css | 4 +- .../UserInterface/Views/ScriptContentView.js | 5 +- .../UserInterface/Views/SearchSidebarPanel.js | 2 + .../Views/SourcesNavigationSidebarPanel.css | 24 ++- .../Views/SourcesNavigationSidebarPanel.js | 142 +++++++++++++++++- .../Views/WebSocketDataGridNode.js | 4 +- .../Views/WebSocketResourceTreeElement.js | 3 +- 36 files changed, 679 insertions(+), 77 deletions(-) create mode 100644 Source/WebInspectorUI/UserInterface/Images/Start.svg create mode 100644 Source/WebInspectorUI/UserInterface/Models/ConsoleSnippet.js create mode 100644 Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.css create mode 100644 Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.js diff --git a/LayoutTests/inspector/unit-tests/set-utilities-expected.txt b/LayoutTests/inspector/unit-tests/set-utilities-expected.txt index d2169d125e9f..d49b9293f180 100644 --- a/LayoutTests/inspector/unit-tests/set-utilities-expected.txt +++ b/LayoutTests/inspector/unit-tests/set-utilities-expected.txt @@ -12,6 +12,13 @@ PASS: Set can filter based on the set object. PASS: Set can filter with a different this. PASS: Set can filter based on the set object with a different this. +-- Running test case: Set.prototype.some +PASS: Set can 'some' based on the value. +PASS: Set can 'some' based on the key. +PASS: Set can 'some' based on the set object. +PASS: Set can 'some' with a different this. +PASS: Set can 'some' based on the set object with a different this. + -- Running test case: Set.prototype.pushAll Array: [1,2] => [1,2,"a1","a2"] diff --git a/LayoutTests/inspector/unit-tests/set-utilities.html b/LayoutTests/inspector/unit-tests/set-utilities.html index 830db46aeb43..6caf1ab5b910 100644 --- a/LayoutTests/inspector/unit-tests/set-utilities.html +++ b/LayoutTests/inspector/unit-tests/set-utilities.html @@ -35,6 +35,24 @@ }, }); + suite.addTestCase({ + name: "Set.prototype.some", + test() { + function test(set, callback, thisArg, expected, message) { + InspectorTest.expectEqual(set.some(callback, thisArg), expected, message); + } + + const set1 = new Set([1, 2, 3, 4]); + const set2 = new Set([2, 4]); + + test(set1, function(value) { return value % 2; }, null, true, "Set can 'some' based on the value."); + test(set1, function(value, key) { return key === 5; }, null, false, "Set can 'some' based on the key."); + test(set1, function(value, key, set) { return set === set1; }, null, true, "Set can 'some' based on the set object."); + test(set1, function(value, key, set) { return this.has(key); }, set2, true, "Set can 'some' with a different this."); + test(set1, function(value, key, set) { return this === set; }, set2, false, "Set can 'some' based on the set object with a different this."); + }, + }); + suite.addTestCase({ name: "Set.prototype.pushAll", test() { diff --git a/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js b/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js index eb545021e768..594a3d533324 100644 --- a/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js +++ b/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js @@ -197,6 +197,7 @@ localizedStrings["Approximate Number"] = "~%s"; localizedStrings["Area"] = "Area"; /* Label for option to toggle the area names setting for CSS grid overlays */ localizedStrings["Area names @ Layout Panel Overlay Options"] = "Area Names"; +localizedStrings["As such, the contents will be run as though it was typed into the Console."] = "As such, the contents will be run as though it was typed into the Console."; localizedStrings["Assertion"] = "Assertion"; localizedStrings["Assertion Failed"] = "Assertion Failed"; localizedStrings["Assertion Failed: %s"] = "Assertion Failed: %s"; @@ -397,6 +398,10 @@ localizedStrings["Console"] = "Console"; localizedStrings["Console Evaluation"] = "Console Evaluation"; localizedStrings["Console Evaluation %d"] = "Console Evaluation %d"; localizedStrings["Console Profile Recorded"] = "Console Profile Recorded"; +localizedStrings["Console Snippet..."] = "Console Snippet..."; +localizedStrings["Console Snippets"] = "Console Snippets"; +localizedStrings["Console Snippets are an easy way to save and evaluate JavaScript in the Console."] = "Console Snippets are an easy way to save and evaluate JavaScript in the Console."; +localizedStrings["Console Snippets..."] = "Console Snippets..."; /* Name of Console Tab */ localizedStrings["Console Tab Name"] = "Console"; localizedStrings["Console cleared at %s"] = "Console cleared at %s"; @@ -434,6 +439,8 @@ localizedStrings["Create %s Rule"] = "Create %s Rule"; /* Title of button that creates a new audit. */ localizedStrings["Create @ Audit Tab Navigation Sidebar"] = "Create"; localizedStrings["Create Breakpoint"] = "Create Breakpoint"; +localizedStrings["Create Console Snippet"] = "Create Console Snippet"; +localizedStrings["Create Console Snippet..."] = "Create Console Snippet..."; localizedStrings["Create Local Override"] = "Create Local Override"; localizedStrings["Create Request Local Override"] = "Create Request Local Override"; localizedStrings["Create Resource"] = "Create Resource"; @@ -993,8 +1000,10 @@ localizedStrings["Microtask Dispatched"] = "Microtask Dispatched"; localizedStrings["Microtask Fired"] = "Microtask Fired"; localizedStrings["Missing result level"] = "Missing result level"; localizedStrings["Mixed"] = "Mixed"; +localizedStrings["Modifications are saved automatically and will apply the next time the Console Snippet is run."] = "Modifications are saved automatically and will apply the next time the Console Snippet is run."; localizedStrings["Modifications made here will take effect on the next load of any page or sub-frame."] = "Modifications made here will take effect on the next load of any page or sub-frame."; localizedStrings["Module Code"] = "Module Code"; +localizedStrings["More information is available at ."] = "More information is available at ."; localizedStrings["More information is available at ."] = "More information is available at ."; localizedStrings["Multi-Entry"] = "Multi-Entry"; localizedStrings["Name"] = "Name"; @@ -1027,6 +1036,7 @@ localizedStrings["No Canvas Contexts"] = "No Canvas Contexts"; localizedStrings["No Canvas Selected"] = "No Canvas Selected"; localizedStrings["No Chart Available"] = "No Chart Available"; localizedStrings["No Child Layers"] = "No Child Layers"; +localizedStrings["No Console Snippets"] = "No Console Snippets"; localizedStrings["No Data Bindings"] = "No Data Bindings"; localizedStrings["No Enabled Audits"] = "No Enabled Audits"; localizedStrings["No Entries"] = "No Entries"; @@ -1330,6 +1340,7 @@ localizedStrings["Reveal in Style Sheet"] = "Reveal in Style Sheet"; localizedStrings["Role"] = "Role"; /* Property value for `font-variant-alternates: ruby`. */ localizedStrings["Ruby Glyphs @ Font Details Sidebar Property Value"] = "Ruby Glyphs"; +localizedStrings["Run"] = "Run"; localizedStrings["Run %d"] = "Run %d"; localizedStrings["Run console commands as if inside a user gesture"] = "Run console commands as if inside a user gesture"; localizedStrings["Running the \u201C%s\u201D audit"] = "Running the \u201C%s\u201D audit"; @@ -1577,6 +1588,7 @@ localizedStrings["The \u201C%s\u201D audit resulted in a warning"] = "The \u201C localizedStrings["The \u201C%s\u201D audit threw an error"] = "The \u201C%s\u201D audit threw an error"; localizedStrings["The \u201C%s\u201D\ntable is empty."] = "The \u201C%s\u201D\ntable is empty."; localizedStrings["The contents and enabled state will be preserved across Web Inspector sessions."] = "The contents and enabled state will be preserved across Web Inspector sessions."; +localizedStrings["The contents will be preserved across Web Inspector sessions."] = "The contents will be preserved across Web Inspector sessions."; localizedStrings["The page's content has changed"] = "The page's content has changed"; localizedStrings["The resource was requested insecurely."] = "The resource was requested insecurely."; /* Message displayed in a banner when one or more snapshots that the user has not yet seen are being filtered. */ @@ -1619,6 +1631,7 @@ localizedStrings["This is what the result of a passing test with no data looks l localizedStrings["This is what the result of a test that threw an error with no data looks like."] = "This is what the result of a test that threw an error with no data looks like."; localizedStrings["This is what the result of a warning test with no data looks like."] = "This is what the result of a warning test with no data looks like."; localizedStrings["This is what the result of an unsupported test with no data looks like."] = "This is what the result of an unsupported test with no data looks like."; +localizedStrings["This means all of the Console Command Line API is available ."] = "This means all of the Console Command Line API is available ."; localizedStrings["This object is a root"] = "This object is a root"; localizedStrings["This object is referenced by internal objects"] = "This object is referenced by internal objects"; localizedStrings["This resource came from a local override"] = "This resource came from a local override"; diff --git a/Source/WebInspectorUI/UserInterface/Base/ObjectStore.js b/Source/WebInspectorUI/UserInterface/Base/ObjectStore.js index 19620428feb8..a644ca944676 100644 --- a/Source/WebInspectorUI/UserInterface/Base/ObjectStore.js +++ b/Source/WebInspectorUI/UserInterface/Base/ObjectStore.js @@ -67,7 +67,7 @@ WI.ObjectStore = class ObjectStore WI.ObjectStore._databaseCallbacks = [callback]; - const version = 7; // Increment this for every edit to `WI.objectStores`. + const version = 8; // Increment this for every edit to `WI.objectStores`. let databaseRequest = window.indexedDB.open(WI.ObjectStore._databaseName, version); databaseRequest.addEventListener("upgradeneeded", (event) => { @@ -278,4 +278,7 @@ WI.objectStores = { // Version 7 symbolicBreakpoints: new WI.ObjectStore("debugger-symbolic-breakpoints", {keyPath: "__id"}), + + // Version 8 + consoleSnippets: new WI.ObjectStore("console-snippets", {keyPath: "__id"}), }; diff --git a/Source/WebInspectorUI/UserInterface/Base/Utilities.js b/Source/WebInspectorUI/UserInterface/Base/Utilities.js index 71b3442f4e71..186b1af8a307 100644 --- a/Source/WebInspectorUI/UserInterface/Base/Utilities.js +++ b/Source/WebInspectorUI/UserInterface/Base/Utilities.js @@ -208,6 +208,18 @@ Object.defineProperty(Set.prototype, "filter", }, }); +Object.defineProperty(Set.prototype, "some", + { + value(predicate, thisArg) + { + for (let item of this) { + if (predicate.call(thisArg, item, item, this)) + return true; + } + return false; + }, + }); + Object.defineProperty(Set.prototype, "addAll", { value(iterable) diff --git a/Source/WebInspectorUI/UserInterface/Controllers/ConsoleManager.js b/Source/WebInspectorUI/UserInterface/Controllers/ConsoleManager.js index 7ebebc1270f8..27e35d226afe 100644 --- a/Source/WebInspectorUI/UserInterface/Controllers/ConsoleManager.js +++ b/Source/WebInspectorUI/UserInterface/Controllers/ConsoleManager.js @@ -39,9 +39,29 @@ WI.ConsoleManager = class ConsoleManager extends WI.Object this._isNewPageOrReload = false; this._remoteObjectsToRelease = null; + this._customLoggingChannels = []; + + this._snippets = new Set; + this._restoringSnippets = false; + + WI.ConsoleSnippet.addEventListener(WI.SourceCode.Event.ContentDidChange, this._handleSnippetContentChanged, this); + WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); - this._customLoggingChannels = []; + WI.Target.registerInitializationPromise((async () => { + let serializedSnippets = await WI.objectStores.consoleSnippets.getAll(); + + this._restoringSnippets = true; + for (let serializedSnippet of serializedSnippets) { + let snippet = WI.ConsoleSnippet.fromJSON(serializedSnippet); + + const key = null; + WI.objectStores.consoleSnippets.associateObject(snippet, key, serializedSnippet); + + this.addSnippet(snippet); + } + this._restoringSnippets = false; + })()); } // Static @@ -66,6 +86,7 @@ WI.ConsoleManager = class ConsoleManager extends WI.Object get warningCount() { return this._warningCount; } get errorCount() { return this._errorCount; } + get snippets() { return this._snippets; } get customLoggingChannels() { return this._customLoggingChannels; } issuesForSourceCode(sourceCode) @@ -88,6 +109,33 @@ WI.ConsoleManager = class ConsoleManager extends WI.Object this._remoteObjectsToRelease.add(remoteObject); } + addSnippet(snippet) + { + console.assert(snippet instanceof WI.ConsoleSnippet, snippet); + console.assert(!this._snippets.has(snippet), snippet); + console.assert(!this._snippets.some((existingSnippet) => snippet.contentIdentifier === existingSnippet.contentIdentifier), snippet); + + this._snippets.add(snippet); + + if (!this._restoringSnippets) + WI.objectStores.consoleSnippets.putObject(snippet); + + this.dispatchEventToListeners(WI.ConsoleManager.Event.SnippetAdded, {snippet}); + } + + removeSnippet(snippet) + { + console.assert(snippet instanceof WI.ConsoleSnippet, snippet); + console.assert(this._snippets.has(snippet), snippet); + + this._snippets.delete(snippet); + + if (!this._restoringSnippets) + WI.objectStores.consoleSnippets.deleteObject(snippet); + + this.dispatchEventToListeners(WI.ConsoleManager.Event.SnippetRemoved, {snippet}); + } + // ConsoleObserver messageWasAdded(target, source, level, text, type, url, line, column, repeatCount, parameters, stackTrace, requestId, timestamp) @@ -216,6 +264,15 @@ WI.ConsoleManager = class ConsoleManager extends WI.Object this.dispatchEventToListeners(WI.ConsoleManager.Event.Cleared); } + _handleSnippetContentChanged(event) + { + let snippet = event.target; + + console.assert(this._snippets.has(snippet), snippet); + + WI.objectStores.consoleSnippets.putObject(snippet); + } + _mainResourceDidChange(event) { console.assert(event.target instanceof WI.Frame); @@ -239,4 +296,6 @@ WI.ConsoleManager.Event = { MessageAdded: "console-manager-message-added", IssueAdded: "console-manager-issue-added", PreviousMessageRepeatCountUpdated: "console-manager-previous-message-repeat-count-updated", + SnippetAdded: "console-manager-snippet-added", + SnippetRemoved: "console-manager-snippet-removed", }; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/DOMManager.js b/Source/WebInspectorUI/UserInterface/Controllers/DOMManager.js index d860306d10ee..9f99194edd92 100644 --- a/Source/WebInspectorUI/UserInterface/Controllers/DOMManager.js +++ b/Source/WebInspectorUI/UserInterface/Controllers/DOMManager.js @@ -604,9 +604,7 @@ WI.DOMManager = class DOMManager extends WI.Object // Re-resolve the node in the console's object group when adding to the console. let domNode = this.nodeForId(nodeId); WI.RemoteObject.resolveNode(domNode, WI.RuntimeManager.ConsoleObjectGroup).then((remoteObject) => { - const specialLogStyles = true; - const shouldRevealConsole = false; - WI.consoleLogViewController.appendImmediateExecutionWithResult(WI.UIString("Selected Element"), remoteObject, specialLogStyles, shouldRevealConsole); + WI.consoleLogViewController.appendImmediateExecutionWithResult(WI.UIString("Selected Element"), remoteObject, {addSpecialUserLogClass: true}); }); } diff --git a/Source/WebInspectorUI/UserInterface/Controllers/JavaScriptLogViewController.js b/Source/WebInspectorUI/UserInterface/Controllers/JavaScriptLogViewController.js index ce4242c68830..df8112fa1e83 100644 --- a/Source/WebInspectorUI/UserInterface/Controllers/JavaScriptLogViewController.js +++ b/Source/WebInspectorUI/UserInterface/Controllers/JavaScriptLogViewController.js @@ -45,7 +45,7 @@ WI.JavaScriptLogViewController = class JavaScriptLogViewController extends WI.Ob this._cleared = true; this._previousMessageView = null; - this._lastCommitted = ""; + this._lastCommitted = {text: "", special: false}; this._repeatCountWasInterrupted = false; this._sessions = []; @@ -105,7 +105,7 @@ WI.JavaScriptLogViewController = class JavaScriptLogViewController extends WI.Ob let consoleSession = new WI.ConsoleSession(data); this._previousMessageView = null; - this._lastCommitted = ""; + this._lastCommitted = {text: "", special: false}; this._repeatCountWasInterrupted = false; this._sessions.push(consoleSession); @@ -117,12 +117,19 @@ WI.JavaScriptLogViewController = class JavaScriptLogViewController extends WI.Ob consoleSession.element.scrollIntoView(); } - appendImmediateExecutionWithResult(text, result, addSpecialUserLogClass, shouldRevealConsole) + appendImmediateExecutionWithResult(text, result, {addSpecialUserLogClass, shouldRevealConsole, handleClick} = {}) { console.assert(result instanceof WI.RemoteObject); - var commandMessageView = new WI.ConsoleCommandView(text, addSpecialUserLogClass ? "special-user-log" : null); - this._appendConsoleMessageView(commandMessageView, true); + if (this._lastCommitted.text !== text || this._lastCommitted.special !== addSpecialUserLogClass) { + let classNames = []; + if (addSpecialUserLogClass) + classNames.push("special-user-log"); + + let commandMessageView = new WI.ConsoleCommandView(text, {classNames, handleClick}); + this._appendConsoleMessageView(commandMessageView, true); + this._lastCommitted = {text, special: addSpecialUserLogClass}; + } function saveResultCallback(savedResultIndex) { @@ -218,10 +225,10 @@ WI.JavaScriptLogViewController = class JavaScriptLogViewController extends WI.Ob { console.assert(text); - if (this._lastCommitted !== text) { + if (this._lastCommitted.text !== text || this._lastCommitted.special) { let commandMessageView = new WI.ConsoleCommandView(text); this._appendConsoleMessageView(commandMessageView, true); - this._lastCommitted = text; + this._lastCommitted = {text, special: false}; } function printResult(result, wasThrown, savedResultIndex) @@ -277,7 +284,7 @@ WI.JavaScriptLogViewController = class JavaScriptLogViewController extends WI.Ob this._previousMessageView = messageView; if (messageView.message && messageView.message.source !== WI.ConsoleMessage.MessageSource.JS) - this._lastCommitted = ""; + this._lastCommitted = {test: "", special: false}; if (WI.consoleContentView.isAttached) this.renderPendingMessagesSoon(); diff --git a/Source/WebInspectorUI/UserInterface/Images/Start.svg b/Source/WebInspectorUI/UserInterface/Images/Start.svg new file mode 100644 index 000000000000..52126ecba61d --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Images/Start.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Source/WebInspectorUI/UserInterface/Main.html b/Source/WebInspectorUI/UserInterface/Main.html index 23a29d79ae25..92a6e527b4de 100644 --- a/Source/WebInspectorUI/UserInterface/Main.html +++ b/Source/WebInspectorUI/UserInterface/Main.html @@ -85,6 +85,7 @@ + @@ -423,6 +424,7 @@ + @@ -698,6 +700,7 @@ + diff --git a/Source/WebInspectorUI/UserInterface/Models/ConsoleSnippet.js b/Source/WebInspectorUI/UserInterface/Models/ConsoleSnippet.js new file mode 100644 index 000000000000..d26c68340072 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Models/ConsoleSnippet.js @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2022 Apple 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 APPLE 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 APPLE 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. + */ + +WI.ConsoleSnippet = class ConsoleSnippet extends WI.LocalScript +{ + constructor(title, source) + { + console.assert(typeof title === "string" && title.trim().length, title); + + const target = null; + const sourceURL = null; + super(target, title, sourceURL, WI.Script.SourceType.Program, source, {editable: true}); + } + + // Static + + static createDefaultWithTitle(title) + { + const source = ` +/* + * ${WI.UIString("Console Snippets are an easy way to save and evaluate JavaScript in the Console.")} + * + * ${WI.UIString("As such, the contents will be run as though it was typed into the Console.")} + * ${WI.UIString("This means all of the Console Command Line API is available .")} + * + * ${WI.UIString("Modifications are saved automatically and will apply the next time the Console Snippet is run.")} + * ${WI.UIString("The contents will be preserved across Web Inspector sessions.")} + * + * ${WI.UIString("More information is available at .")} + */ + +"Hello World!" +`.trimStart(); + return new WI.ConsoleSnippet(title, source); + } + + // Public + + get title() + { + return this.url; + } + + run() + { + WI.runtimeManager.evaluateInInspectedWindow(this.content, { + objectGroup: WI.RuntimeManager.ConsoleObjectGroup, + includeCommandLineAPI: true, + doNotPauseOnExceptionsAndMuteConsole: true, + generatePreview: true, + saveResult: true, + }, (result, wasThrown) => { + WI.consoleLogViewController.appendImmediateExecutionWithResult(this.title, result, { + addSpecialUserLogClass: true, + shouldRevealConsole: true, + handleClick: (event) => { + if (!WI.consoleManager.snippets.has(this)) + return; + + + const cookie = null; + WI.showRepresentedObject(this, cookie, { + ignoreNetworkTab: true, + ignoreSearchTab: true, + }); + }, + }); + }); + } + + // Import / Export + + static fromJSON(json) + { + return new WI.ConsoleSnippet(json.title, json.source); + } + + toJSON(key) + { + let json = { + title: this.title, + source: this.content, + }; + if (key === WI.ObjectStore.toJSONSymbol) + json[WI.objectStores.breakpoints.keyPath] = this.title; + return json; + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Test.html b/Source/WebInspectorUI/UserInterface/Test.html index 808d945d4242..f289516904c3 100644 --- a/Source/WebInspectorUI/UserInterface/Test.html +++ b/Source/WebInspectorUI/UserInterface/Test.html @@ -149,6 +149,7 @@ + diff --git a/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js b/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js index 1d135e488f84..4a02006f7414 100644 --- a/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js +++ b/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js @@ -397,8 +397,7 @@ WI.AnimationContentView = class AnimationContentView extends WI.ContentView return; const text = WI.UIString("Selected Animation", "Appears as a label when a given web animation is logged to the Console"); - const addSpecialUserLogClass = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); }); }); diff --git a/Source/WebInspectorUI/UserInterface/Views/ButtonNavigationItem.js b/Source/WebInspectorUI/UserInterface/Views/ButtonNavigationItem.js index b54c857d5e55..f83430093ad6 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ButtonNavigationItem.js +++ b/Source/WebInspectorUI/UserInterface/Views/ButtonNavigationItem.js @@ -59,9 +59,6 @@ WI.ButtonNavigationItem = class ButtonNavigationItem extends WI.NavigationItem this.buttonStyle = this._image ? WI.ButtonNavigationItem.Style.Image : WI.ButtonNavigationItem.Style.Text; this.imageType = this._image ? WI.ButtonNavigationItem.ImageType.SVG : null; - - if (this.buttonStyle === WI.ButtonNavigationItem.Style.Image) - this.tooltip = toolTipOrLabel; } // Public @@ -221,16 +218,19 @@ WI.ButtonNavigationItem = class ButtonNavigationItem extends WI.NavigationItem switch (this._imageType) { case null: case WI.ButtonNavigationItem.ImageType.SVG: { - let glyphElement = WI.ImageUtilities.useSVGSymbol(this._image, "glyph"); - glyphElement.style.width = this._imageWidth + "px"; - glyphElement.style.height = this._imageHeight + "px"; - this.element.appendChild(glyphElement); + if (this._image) { + let glyphElement = WI.ImageUtilities.useSVGSymbol(this._image, "glyph"); + glyphElement.style.width = this._imageWidth + "px"; + glyphElement.style.height = this._imageHeight + "px"; + this.element.appendChild(glyphElement); + } break; } case WI.ButtonNavigationItem.ImageType.IMG: { let img = this.element.appendChild(document.createElement("img")); - img.src = this._image; + if (this._image) + img.src = this._image; break; } } @@ -240,6 +240,9 @@ WI.ButtonNavigationItem = class ButtonNavigationItem extends WI.NavigationItem labelElement.textContent = this._label; } } + + if (this._buttonStyle === WI.ButtonNavigationItem.Style.Image) + this.tooltip ||= this._label; } _updateTabIndex() diff --git a/Source/WebInspectorUI/UserInterface/Views/CanvasContentView.js b/Source/WebInspectorUI/UserInterface/Views/CanvasContentView.js index 7d870d0872a1..de35abcc0172 100644 --- a/Source/WebInspectorUI/UserInterface/Views/CanvasContentView.js +++ b/Source/WebInspectorUI/UserInterface/Views/CanvasContentView.js @@ -308,8 +308,7 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView return; const text = WI.UIString("Selected Canvas Context"); - const addSpecialUserLogClass = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); }); }); diff --git a/Source/WebInspectorUI/UserInterface/Views/CanvasTreeElement.js b/Source/WebInspectorUI/UserInterface/Views/CanvasTreeElement.js index 91f26a7c301c..613cd1450469 100644 --- a/Source/WebInspectorUI/UserInterface/Views/CanvasTreeElement.js +++ b/Source/WebInspectorUI/UserInterface/Views/CanvasTreeElement.js @@ -93,8 +93,7 @@ WI.CanvasTreeElement = class CanvasTreeElement extends WI.FolderizedTreeElement return; const text = WI.UIString("Selected Canvas Context"); - const addSpecialUserLogClass = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); }); }); diff --git a/Source/WebInspectorUI/UserInterface/Views/ConsoleCommandView.js b/Source/WebInspectorUI/UserInterface/Views/ConsoleCommandView.js index 8379ed7f419e..300760462b41 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ConsoleCommandView.js +++ b/Source/WebInspectorUI/UserInterface/Views/ConsoleCommandView.js @@ -29,10 +29,11 @@ WI.ConsoleCommandView = class ConsoleCommandView { - constructor(commandText, className) + constructor(commandText, {classNames, handleClick} = {}) { this._commandText = commandText; - this._className = className || ""; + this._classNames = classNames || null; + this._handleClick = handleClick || null; } // Public @@ -44,12 +45,14 @@ WI.ConsoleCommandView = class ConsoleCommandView this._element.dir = "ltr"; this._element.setAttribute("data-labelprefix", WI.UIString("Input: ")); - if (this._className) - this._element.classList.add(this._className); + if (this._classNames) + this._element.classList.add(...this._classNames); this._formattedCommandElement = this._element.appendChild(document.createElement("span")); this._formattedCommandElement.classList.add("console-message-body"); this._formattedCommandElement.textContent = this._commandText; + if (this._handleClick) + this._formattedCommandElement.addEventListener("click", this._handleClick); // FIXME: Web Inspector: LogContentView should use higher level objects this._element.__commandView = this; diff --git a/Source/WebInspectorUI/UserInterface/Views/ConsoleDrawer.css b/Source/WebInspectorUI/UserInterface/Views/ConsoleDrawer.css index 80583d732b77..1f9713faa125 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ConsoleDrawer.css +++ b/Source/WebInspectorUI/UserInterface/Views/ConsoleDrawer.css @@ -44,7 +44,7 @@ pointer-events: none; } -.console-drawer > .navigation-bar > .item > .glyph, +.console-drawer > .navigation-bar > .item > :is(.glyph, img), .console-drawer > .navigation-bar > .log-scope-bar > li { pointer-events: all; } diff --git a/Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.css b/Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.css new file mode 100644 index 000000000000..a83058c0ff7c --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.css @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 Apple 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 APPLE 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 APPLE 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. + */ + + .tree-outline .item.console-snippet > .status { + display: flex; + align-items: center; + width: 16px; + height: 16px; + } + + .tree-outline .item.console-snippet > .status > img { + width: 100%; + height: 100%; + content: url(../Images/Start.svg); + } + +.tree-outline .item.console-snippet:not(:hover) > .status > img { + display: none; +} + +body:not(.window-inactive, .window-docked-inactive) .tree-outline:focus-within .item.console-snippet.selected:hover > .status > img { + filter: var(--filter-invert); +} + +@media (prefers-color-scheme: dark) { + .tree-outline .item.console-snippet:hover > .status > img { + filter: var(--filter-invert) brightness(90%); + } +} diff --git a/Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.js b/Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.js new file mode 100644 index 000000000000..4c8de43e3078 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Views/ConsoleSnippetTreeElement.js @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 Apple 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 APPLE 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 APPLE 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. + */ + +WI.ConsoleSnippetTreeElement = class ConsoleSnippetTreeElement extends WI.ScriptTreeElement +{ + constructor(consoleSnippet) + { + console.assert(consoleSnippet instanceof WI.ConsoleSnippet, consoleSnippet); + + super(consoleSnippet); + + this.addClassName("console-snippet"); + } + + // Protected + + onattach() + { + super.onattach(); + + this.status = document.createElement("img"); + this.status.title = WI.UIString("Run"); + this.status.addEventListener("click", (event) => { + this.representedObject.run(); + }); + } + + ondelete() + { + WI.consoleManager.removeSnippet(this.representedObject); + + return true; + } + + onspace() + { + this.representedObject.run(); + + return true; + } + + canSelectOnMouseDown(event) + { + if (this.status.contains(event.target)) + return false; + + return super.canSelectOnMouseDown(event); + } + + updateStatus() + { + // Do nothing. Do not allow ScriptTreeElement / SourceCodeTreeElement to modify our status element. + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Views/ContextMenuUtilities.js b/Source/WebInspectorUI/UserInterface/Views/ContextMenuUtilities.js index c9a486e97ede..0553e52c94cd 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ContextMenuUtilities.js +++ b/Source/WebInspectorUI/UserInterface/Views/ContextMenuUtilities.js @@ -363,8 +363,7 @@ WI.appendContextMenuItemsForDOMNode = function(contextMenu, domNode, options = { contextMenu.appendItem(label, () => { WI.RemoteObject.resolveNode(domNode, WI.RuntimeManager.ConsoleObjectGroup).then((remoteObject) => { let text = isElement ? WI.UIString("Selected Element", "Selected DOM element") : WI.UIString("Selected Node", "Selected DOM node"); - const addSpecialUserLogClass = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); }); }); } diff --git a/Source/WebInspectorUI/UserInterface/Views/HeapSnapshotInstanceDataGridNode.js b/Source/WebInspectorUI/UserInterface/Views/HeapSnapshotInstanceDataGridNode.js index 10185b8a0ad0..54a9d4fa666b 100644 --- a/Source/WebInspectorUI/UserInterface/Views/HeapSnapshotInstanceDataGridNode.js +++ b/Source/WebInspectorUI/UserInterface/Views/HeapSnapshotInstanceDataGridNode.js @@ -81,12 +81,12 @@ WI.HeapSnapshotInstanceDataGridNode = class HeapSnapshotInstanceDataGridNode ext if (node.className === "string") { WI.heapManager.getPreview(node, function(error, string, functionDetails, objectPreviewPayload) { let remoteObject = error ? WI.RemoteObject.fromPrimitiveValue(undefined) : WI.RemoteObject.fromPrimitiveValue(string); - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass, shouldRevealConsole); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass, shouldRevealConsole}); }); } else { WI.heapManager.getRemoteObject(node, WI.RuntimeManager.ConsoleObjectGroup, function(error, remoteObjectPayload) { let remoteObject = error ? WI.RemoteObject.fromPrimitiveValue(undefined) : WI.RemoteObject.fromPayload(remoteObjectPayload, WI.assumingMainTarget()); - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass, shouldRevealConsole); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass, shouldRevealConsole}); }); } }); diff --git a/Source/WebInspectorUI/UserInterface/Views/InputPopover.js b/Source/WebInspectorUI/UserInterface/Views/InputPopover.js index 01a31ee52d55..d8eb4ed5a96e 100644 --- a/Source/WebInspectorUI/UserInterface/Views/InputPopover.js +++ b/Source/WebInspectorUI/UserInterface/Views/InputPopover.js @@ -25,13 +25,12 @@ WI.InputPopover = class InputPopover extends WI.Popover { - constructor(message, delegate) + constructor(identifier, message, delegate) { super(delegate); + this._identifier = identifier; this._message = message; - this._value = null; - this._result = WI.InputPopover.Result.None; this._targetElement = null; this._preferredEdges = null; @@ -39,8 +38,11 @@ WI.InputPopover = class InputPopover extends WI.Popover this.windowResizeHandler = this._presentOverTargetElement.bind(this); } - get value() { return this._value; } - get result() { return this._result; } + get identifier() { return this._identifier; } + get value() + { + return this._inputElement?.value ?? ""; + } show(targetElement, preferredEdges) { @@ -48,7 +50,7 @@ WI.InputPopover = class InputPopover extends WI.Popover this._preferredEdges = preferredEdges; let contentElement = document.createElement("div"); - contentElement.classList.add("input-popover-content"); + contentElement.classList.add("input-popover-content", this._identifier); if (this._message) { let label = document.createElement("div"); @@ -61,13 +63,8 @@ WI.InputPopover = class InputPopover extends WI.Popover this._inputElement.spellcheck = false; this._inputElement.addEventListener("keydown", (event) => { - if (!isEnterKey(event)) - return; - - this._result = WI.InputPopover.Result.Committed; - this._value = event.target.value; - - this.dismiss(); + if (isEnterKey(event)) + this.dismiss(); }); contentElement.appendChild(this._inputElement); @@ -89,9 +86,3 @@ WI.InputPopover = class InputPopover extends WI.Popover this._inputElement.select(); } }; - -WI.InputPopover.Result = { - None: Symbol("result-none"), - Cancelled: Symbol("result-cancelled"), - Committed: Symbol("result-committed"), -}; diff --git a/Source/WebInspectorUI/UserInterface/Views/LogContentView.css b/Source/WebInspectorUI/UserInterface/Views/LogContentView.css index 31604e705ae8..44f6622f1d84 100644 --- a/Source/WebInspectorUI/UserInterface/Views/LogContentView.css +++ b/Source/WebInspectorUI/UserInterface/Views/LogContentView.css @@ -61,6 +61,12 @@ color: var(--glyph-color-active-pressed); } +.navigation-bar > .item.console-snippets > img { + width: 16px; + height: 16px; + content: url(../Images/ClippingIcons.svg#js-light); +} + .content-view.log { display: flex; flex-direction: column; @@ -267,7 +273,24 @@ body[dir=rtl] .console-group-title::before { alt: attr(data-labelprefix); } +.console-user-command:not(:hover) > .console-snippet-options { + display: none; +} + +.create-snippet-popover > .label { + display: inline-block; + margin-inline-end: 6px; + font-weight: bold; + line-height: 23px; + white-space: nowrap; + color: hsl(0, 0%, 34%); +} + @media (prefers-color-scheme: dark) { + .navigation-bar > .item.console-snippets > img { + content: url(../Images/ClippingIcons.svg#js-dark); + } + .console-messages { background-color: var(--background-color-content); } diff --git a/Source/WebInspectorUI/UserInterface/Views/LogContentView.js b/Source/WebInspectorUI/UserInterface/Views/LogContentView.js index 7bb2d5c7dc86..99cea9c5f83d 100644 --- a/Source/WebInspectorUI/UserInterface/Views/LogContentView.js +++ b/Source/WebInspectorUI/UserInterface/Views/LogContentView.js @@ -116,6 +116,12 @@ WI.LogContentView = class LogContentView extends WI.ContentView this._messageSourceBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._messageSourceBarSelectionDidChange, this); } + const consoleSnippetsImage = ""; // This is set in CSS to have dark mode support. + this._consoleSnippetsNavigationItem = new WI.ButtonNavigationItem("console-snippets", WI.UIString("Console Snippets..."), consoleSnippetsImage); + this._consoleSnippetsNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.Image; + this._consoleSnippetsNavigationItem.imageType = WI.ButtonNavigationItem.ImageType.IMG; + WI.addMouseDownContextMenuHandlers(this._consoleSnippetsNavigationItem.element, this._handleSnippetsNavigationItemContextMenu.bind(this)); + this._garbageCollectNavigationItem = new WI.ButtonNavigationItem("garbage-collect", WI.UIString("Collect garbage"), "Images/NavigationItemGarbageCollect.svg", 16, 16); this._garbageCollectNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._garbageCollect, this); @@ -156,6 +162,8 @@ WI.LogContentView = class LogContentView extends WI.ContentView if (this._emulateUserGestureNavigationItemGroup) navigationItems.push(this._emulateUserGestureNavigationItemGroup); + navigationItems.push(this._consoleSnippetsNavigationItem); + if (InspectorBackend.hasCommand("Heap.gc")) navigationItems.push(this._garbageCollectNavigationItem); @@ -377,6 +385,37 @@ WI.LogContentView = class LogContentView extends WI.ContentView this.prompt.focus(); } + // Popover delegate + + willDismissPopover(popover) + { + if (popover instanceof WI.InputPopover) { + let title = popover.value?.trim(); + if (!title) { + InspectorFrontendHost.beep(); + return; + } + + // Do not conflict with an existing console snippet. + if (WI.consoleManager.snippets.some((consoleSnippet) => consoleSnippet.title === title)) { + InspectorFrontendHost.beep(); + return; + } + + let consoleSnippet = WI.ConsoleSnippet.createDefaultWithTitle(title); + WI.consoleManager.addSnippet(consoleSnippet); + + const cookie = null; + WI.showRepresentedObject(consoleSnippet, cookie, { + ignoreNetworkTab: true, + ignoreSearchTab: true, + }); + return; + } + + console.assert(false, "not reached", popover); + } + // Private _formatMessagesAsData(onlySelected) @@ -668,8 +707,7 @@ WI.LogContentView = class LogContentView extends WI.ContentView WI.RemoteObject.resolveNode(domNode, WI.RuntimeManager.ConsoleObjectGroup) .then((remoteObject) => { let text = domNode.nodeType() === Node.ELEMENT_NODE ? WI.UIString("Dropped Element") : WI.UIString("Dropped Node"); - const addSpecialUserLogClass = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); this.prompt.focus(); }); @@ -926,6 +964,21 @@ WI.LogContentView = class LogContentView extends WI.ContentView }, !WI.settings.clearLogOnNavigate.value); } + _handleSnippetsNavigationItemContextMenu(contextMenu) { + for (let consoleSnippet of WI.consoleManager.snippets) { + contextMenu.appendItem(consoleSnippet.displayName, () => { + consoleSnippet.run(); + }); + } + + contextMenu.appendSeparator(); + + contextMenu.appendItem(WI.UIString("Create Console Snippet..."), () => { + let popover = new WI.InputPopover("create-snippet-popover", WI.UIString("Name"), this); + popover.show(this._consoleSnippetsNavigationItem.element, [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X]); + }); + } + _handleClearLogOnNavigateSettingChanged() { this._updateOtherFiltersNavigationItemState(); diff --git a/Source/WebInspectorUI/UserInterface/Views/ObjectPreviewView.js b/Source/WebInspectorUI/UserInterface/Views/ObjectPreviewView.js index 48ff08c63856..042a23c2f47c 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ObjectPreviewView.js +++ b/Source/WebInspectorUI/UserInterface/Views/ObjectPreviewView.js @@ -295,7 +295,7 @@ WI.ObjectPreviewView = class ObjectPreviewView extends WI.Object if (!isImpossible) WI.quickConsole.prompt.pushHistoryItem(text); - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, this._remoteObject, isImpossible); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, this._remoteObject, {addSpecialUserLogClass: isImpossible}); }); } }; diff --git a/Source/WebInspectorUI/UserInterface/Views/ObjectTreeBaseTreeElement.js b/Source/WebInspectorUI/UserInterface/Views/ObjectTreeBaseTreeElement.js index c36d44f62380..b00f12044ff4 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ObjectTreeBaseTreeElement.js +++ b/Source/WebInspectorUI/UserInterface/Views/ObjectTreeBaseTreeElement.js @@ -196,7 +196,7 @@ WI.ObjectTreeBaseTreeElement = class ObjectTreeBaseTreeElement extends WI.Genera return; var text = WI.UIString("Selected Symbol"); - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, symbol, true); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, symbol, {addSpecialUserLogClass: true}); } _logValue(value) @@ -212,7 +212,7 @@ WI.ObjectTreeBaseTreeElement = class ObjectTreeBaseTreeElement extends WI.Genera if (!isImpossible) WI.quickConsole.prompt.pushHistoryItem(text); - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, resolvedValue, isImpossible); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, resolvedValue, {addSpecialUserLogClass: isImpossible}); } _appendMenusItemsForObject(contextMenu, resolvedValue) diff --git a/Source/WebInspectorUI/UserInterface/Views/OpenResourceDialog.js b/Source/WebInspectorUI/UserInterface/Views/OpenResourceDialog.js index c24067b467bc..52867a2d82f5 100644 --- a/Source/WebInspectorUI/UserInterface/Views/OpenResourceDialog.js +++ b/Source/WebInspectorUI/UserInterface/Views/OpenResourceDialog.js @@ -165,6 +165,8 @@ WI.OpenResourceDialog = class OpenResourceDialog extends WI.Dialog WI.debuggerManager.removeEventListener(WI.DebuggerManager.Event.ScriptRemoved, this._scriptRemoved, this); WI.cssManager.removeEventListener(WI.CSSManager.Event.StyleSheetAdded, this._handleStyleSheetAdded, this); WI.cssManager.removeEventListener(WI.CSSManager.Event.StyleSheetRemoved, this._handleStyleSheetRemoved, this); + WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.SnippetAdded, this._handleConsoleSnippetAdded, this); + WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.SnippetRemoved, this._handleConsoleSnippetRemoved, this); this._queryController.reset(); this._updateFilterThrottler.cancel(); @@ -180,6 +182,8 @@ WI.OpenResourceDialog = class OpenResourceDialog extends WI.Dialog WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.ScriptRemoved, this._scriptRemoved, this); WI.cssManager.addEventListener(WI.CSSManager.Event.StyleSheetAdded, this._handleStyleSheetAdded, this); WI.cssManager.addEventListener(WI.CSSManager.Event.StyleSheetRemoved, this._handleStyleSheetRemoved, this); + WI.consoleManager.addEventListener(WI.ConsoleManager.Event.SnippetAdded, this._handleConsoleSnippetAdded, this); + WI.consoleManager.addEventListener(WI.ConsoleManager.Event.SnippetRemoved, this._handleConsoleSnippetRemoved, this); if (WI.networkManager.mainFrame) this._addResourcesForFrame(WI.networkManager.mainFrame); @@ -206,6 +210,9 @@ WI.OpenResourceDialog = class OpenResourceDialog extends WI.Dialog this._addResource(styleSheet); } + for (let consoleSnippet of WI.consoleManager.snippets) + this._addResource(consoleSnippet); + this._updateFilterThrottler.force(); this._inputElement.focus(); @@ -469,6 +476,16 @@ WI.OpenResourceDialog = class OpenResourceDialog extends WI.Dialog this._removeResource(styleSheet); } + + _handleConsoleSnippetAdded(event) + { + this._addResource(event.data.snippet); + } + + _handleConsoleSnippetRemoved(event) + { + this._removeResource(event.data.snippet); + } }; WI.OpenResourceDialog.ResourceMatchCookieDataSymbol = Symbol("open-resource-dialog-resource-match-cookie-data"); diff --git a/Source/WebInspectorUI/UserInterface/Views/QuickConsole.js b/Source/WebInspectorUI/UserInterface/Views/QuickConsole.js index 5c51a3a4c0be..420d6e6bfe6f 100644 --- a/Source/WebInspectorUI/UserInterface/Views/QuickConsole.js +++ b/Source/WebInspectorUI/UserInterface/Views/QuickConsole.js @@ -336,8 +336,7 @@ WI.QuickConsole = class QuickConsole extends WI.View WI.RemoteObject.resolveNode(domNode, WI.RuntimeManager.ConsoleObjectGroup) .then((remoteObject) => { let text = domNode.nodeType() === Node.ELEMENT_NODE ? WI.UIString("Dropped Element") : WI.UIString("Dropped Node"); - const addSpecialUserLogClass = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); this.prompt.focus(); }); diff --git a/Source/WebInspectorUI/UserInterface/Views/ResourceIcons.css b/Source/WebInspectorUI/UserInterface/Views/ResourceIcons.css index d5c3e2dc94ba..33453e54af4f 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ResourceIcons.css +++ b/Source/WebInspectorUI/UserInterface/Views/ResourceIcons.css @@ -71,7 +71,7 @@ content: url(../Images/DocumentIcons.svg#js-light-override); } -.anonymous-script-icon .icon { +:is(.anonymous-script-icon, .resource-icon.resource-type-script.console-snippet) .icon { content: url(../Images/ClippingIcons.svg#js-light); } @@ -162,7 +162,7 @@ body:not(.window-inactive, .window-docked-inactive) .tree-outline:focus-within . content: url(../Images/DocumentIcons.svg#js-dark-override); } - .anonymous-script-icon .icon { + :is(.anonymous-script-icon, .resource-icon.resource-type-script.console-snippet) .icon { content: url(../Images/ClippingIcons.svg#js-dark); } diff --git a/Source/WebInspectorUI/UserInterface/Views/ScriptContentView.js b/Source/WebInspectorUI/UserInterface/Views/ScriptContentView.js index a8bd0f178a8d..64e10d0cb84a 100644 --- a/Source/WebInspectorUI/UserInterface/Views/ScriptContentView.js +++ b/Source/WebInspectorUI/UserInterface/Views/ScriptContentView.js @@ -248,7 +248,10 @@ WI.ScriptContentView = class ScriptContentView extends WI.ContentView _handleTextEditorContentDidChange(event) { - this._script.editableRevision.updateRevisionContent(this._textEditor.string); + this._updateRevisionContentDebouncer ||= new Debouncer(() => { + this._script.editableRevision.updateRevisionContent(this._textEditor.string); + }); + this._updateRevisionContentDebouncer.delayForTime(250); } _togglePrettyPrint(event) diff --git a/Source/WebInspectorUI/UserInterface/Views/SearchSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/SearchSidebarPanel.js index a065be94f06c..aa1ef3016022 100644 --- a/Source/WebInspectorUI/UserInterface/Views/SearchSidebarPanel.js +++ b/Source/WebInspectorUI/UserInterface/Views/SearchSidebarPanel.js @@ -343,6 +343,8 @@ WI.SearchSidebarPanel = class SearchSidebarPanel extends WI.NavigationSidebarPan // FIXME: Resource search should work with Local Overrides if enabled. + // FIXME: Resource search should work with Console Snippets. + // FIXME: Resource search should work in JSContext inspection. // Web Inspector: JSContext inspection Resource search does not work } diff --git a/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.css b/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.css index 1a0c3036f51d..ecdfc510b049 100644 --- a/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.css +++ b/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.css @@ -66,14 +66,10 @@ display: none; } -.sidebar > .panel.navigation.sources > .content > :matches(.pause-reason-container, .call-stack-container, .breakpoints-container, .local-overrides-container) { +.sidebar > .panel.navigation.sources > .content > :matches(.pause-reason-container, .call-stack-container, .breakpoints-container, .local-overrides-container, .console-snippets-container) { border-bottom: 1px solid var(--border-color); } -.sidebar > .panel.navigation.sources > .content > .local-overrides { - border-width: 1px !important; -} - .sidebar > .panel.navigation.sources > .content .details-section { font-size: 11px; --details-section-border-bottom: none; @@ -130,7 +126,7 @@ --details-section-header-top: 0; } - .sidebar > .panel.navigation.sources > .content > :matches(.call-stack-container, .breakpoints-container, .resources-container, .local-overrides-container) { + .sidebar > .panel.navigation.sources > .content > :matches(.call-stack-container, .breakpoints-container, .resources-container, .local-overrides-container, .console-snippets-container) { height: 100%; overflow-y: auto; } @@ -153,6 +149,22 @@ max-height: fit-content; } + .sidebar > .panel.navigation.sources > .content > .console-snippets-container { + flex-grow: 0; + flex-shrink: 0; + height: auto; + } + + .sidebar > .panel.navigation.sources > .content > .console-snippets-container > .details-section { + height: 100%; + } + + .sidebar > .panel.navigation.sources > .content > .console-snippets-container > .details-section > .content { + height: 100%; + max-height: 90px; + overflow-y: auto; + } + .sidebar > .panel.navigation.sources > .content > .resources-container { flex-grow: 1; flex-shrink: 3; diff --git a/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.js index a864eb314e32..607f99a03dde 100644 --- a/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.js +++ b/Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.js @@ -246,6 +246,28 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W this._localOverridesContainer.hidden = true; this._localOverridesContainer.appendChild(this._localOverridesSection.element); + this._consoleSnippetsTreeOutline = this.createContentTreeOutline({suppressFiltering: true}); + this._consoleSnippetsTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._handleTreeSelectionDidChange, this); + + this._consoleSnippetsRow = new WI.DetailsSectionRow(WI.UIString("No Console Snippets")); + + let consoleSnippetNavigationBarWrapper = document.createElement("div"); + + let consoleSnippetNavigationBar = new WI.NavigationBar; + consoleSnippetNavigationBarWrapper.appendChild(consoleSnippetNavigationBar.element); + + this._createConsoleSnippetButton = new WI.ButtonNavigationItem("create-console-snippet", WI.UIString("Create Console Snippet"), "Images/Plus13.svg", 13, 13); + this._createConsoleSnippetButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleCreateConsoleSnippetButtonClicked, this); + consoleSnippetNavigationBar.addNavigationItem(this._createConsoleSnippetButton); + + let consoleSnippetsGroup = new WI.DetailsSectionGroup([this._consoleSnippetsRow]); + this._consoleSnippetsSection = new WI.DetailsSection("console-snippets", WI.UIString("Console Snippets"), [consoleSnippetsGroup], consoleSnippetNavigationBarWrapper); + + this._consoleSnippetsContainer = this.contentView.element.appendChild(document.createElement("div")); + this._consoleSnippetsContainer.classList.add("console-snippets-container"); + this._consoleSnippetsContainer.hidden = true; + this._consoleSnippetsContainer.appendChild(this._consoleSnippetsSection.element); + this._resourcesNavigationBar = new WI.NavigationBar; this.contentView.addSubview(this._resourcesNavigationBar); @@ -344,6 +366,8 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W WI.consoleManager.addEventListener(WI.ConsoleManager.Event.IssueAdded, this._handleConsoleIssueAdded, this); WI.consoleManager.addEventListener(WI.ConsoleManager.Event.Cleared, this._handleConsoleCleared, this); + WI.consoleManager.addEventListener(WI.ConsoleManager.Event.SnippetAdded, this._handleConsoleSnippetAdded, this); + WI.consoleManager.addEventListener(WI.ConsoleManager.Event.SnippetRemoved, this._handleConsoleSnippetRemoved, this); WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this); @@ -438,6 +462,9 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W this._handleAuditManagerTestScheduled(); } + for (let consoleSnippet of WI.consoleManager.snippets) + this._addConsoleSnippet(consoleSnippet); + this._updateBreakpointsDisabledBanner(); } @@ -507,6 +534,8 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.IssueAdded, this._handleConsoleIssueAdded, this); WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.Cleared, this._handleConsoleCleared, this); + WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.SnippetAdded, this._handleConsoleSnippetAdded, this); + WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.SnippetRemoved, this._handleConsoleSnippetRemoved, this); WI.timelineManager.removeEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this); @@ -552,6 +581,9 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W if (representedObject instanceof WI.Script && representedObject === WI.networkManager.bootstrapScript) return this._localOverridesTreeOutline.findTreeElement(representedObject); + if (representedObject instanceof WI.ConsoleSnippet) + return this._consoleSnippetsTreeOutline.findTreeElement(representedObject); + if (!this._mainFrameTreeElement && (representedObject instanceof WI.Resource || representedObject instanceof WI.Frame || representedObject instanceof WI.Collection)) { // All resources are under the main frame, so we need to return early if we don't have the main frame yet. return null; @@ -761,7 +793,13 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W treeOutline.addEventListener(WI.TreeOutline.Event.ElementRevealed, function(event) { let treeElement = event.data.element; - let detailsSections = [this._pauseReasonSection, this._callStackSection, this._breakpointsSection, this._localOverridesSection]; + let detailsSections = [ + this._pauseReasonSection, + this._callStackSection, + this._breakpointsSection, + this._localOverridesSection, + this._consoleSnippetsSection, + ]; let detailsSection = detailsSections.find((detailsSection) => detailsSection.element.contains(treeElement.listItemElement)); if (!detailsSection) return; @@ -845,6 +883,11 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W return; } + if (popover instanceof WI.InputPopover) { + this._willDismissConsoleSnippetPopover(popover); + return; + } + if (popover instanceof WI.EventBreakpointPopover) { this._willDismissEventBreakpointPopover(popover); return; @@ -887,6 +930,33 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W WI.showLocalResourceOverride(localResourceOverride); } + _willDismissConsoleSnippetPopover(popover) + { + let title = popover.value?.trim(); + if (!title) { + if (!this._consoleSnippetsTreeOutline.children.length) + this._consoleSnippetsContainer.hidden = true; + + InspectorFrontendHost.beep(); + return; + } + + // Do not conflict with an existing consoleSnippet. + if (WI.consoleManager.snippets.some((consoleSnippet) => consoleSnippet.title === title)) { + InspectorFrontendHost.beep(); + return; + } + + let consoleSnippet = WI.ConsoleSnippet.createDefaultWithTitle(title); + WI.consoleManager.addSnippet(consoleSnippet); + + const cookie = null; + WI.showRepresentedObject(consoleSnippet, cookie, { + ignoreNetworkTab: true, + ignoreSearchTab: true, + }); + } + _willDismissEventBreakpointPopover(popover) { let breakpoint = popover.breakpoint; @@ -945,6 +1015,7 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W const rankFunctions = [ (treeElement) => treeElement instanceof WI.ScriptTreeElement && treeElement.representedObject === WI.networkManager.bootstrapScript, (treeElement) => treeElement instanceof WI.LocalResourceOverrideTreeElement, + (treeElement) => treeElement instanceof WI.ScriptTreeElement && treeElement.representedObject instanceof WI.ConsoleSnippet, (treeElement) => treeElement instanceof WI.CSSStyleSheetTreeElement && treeElement.representedObject.isInspectorStyleSheet(), (treeElement) => treeElement === this._mainFrameTreeElement, (treeElement) => treeElement instanceof WI.FrameTreeElement, @@ -1547,6 +1618,46 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W } } + _addConsoleSnippet(consoleSnippet) + { + console.assert(consoleSnippet instanceof WI.ConsoleSnippet, consoleSnippet); + + if (this._consoleSnippetsTreeOutline.findTreeElement(consoleSnippet)) + return; + + let parentTreeElement = this._consoleSnippetsTreeOutline; + + let consoleSnippetTreeElement = new WI.ConsoleSnippetTreeElement(consoleSnippet); + + let index = insertionIndexForObjectInListSortedByFunction(consoleSnippetTreeElement, parentTreeElement.children, this._boundCompareTreeElements); + parentTreeElement.insertChild(consoleSnippetTreeElement, index); + + this._consoleSnippetsRow.hideEmptyMessage(); + this._consoleSnippetsRow.element.appendChild(this._consoleSnippetsTreeOutline.element); + this._consoleSnippetsContainer.hidden = false; + } + + _removeConsoleSnippet(consoleSnippet) + { + console.assert(consoleSnippet instanceof WI.ConsoleSnippet, consoleSnippet); + + let consoleSnippetTreeElement = this._consoleSnippetsTreeOutline.findTreeElement(consoleSnippet); + if (!consoleSnippetTreeElement) + return; + + let wasSelected = consoleSnippetTreeElement.selected; + + let parentTreeElement = this._consoleSnippetsTreeOutline; + parentTreeElement.removeChild(consoleSnippetTreeElement); + + if (!parentTreeElement.children.length) { + this._consoleSnippetsContainer.hidden = true; + + if (wasSelected && WI.networkManager.mainFrame && WI.networkManager.mainFrame.mainResource) + WI.showRepresentedObject(WI.networkManager.mainFrame.mainResource); + } + } + _updateTemporarilyDisabledBreakpointsButtons() { let breakpointsDisabledTemporarily = WI.debuggerManager.breakpointsDisabledTemporarily; @@ -2159,6 +2270,12 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W } } + _handleCreateConsoleSnippetButtonClicked(event) + { + let popover = new WI.InputPopover("create-snippet-popover", WI.UIString("Name"), this); + popover.show(this._createConsoleSnippetButton.element, [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X]); + } + _populateCreateResourceContextMenu(contextMenu) { if (WI.NetworkManager.supportsOverridingResponses()) { @@ -2176,6 +2293,19 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W }); } + contextMenu.appendItem(WI.UIString("Console Snippet..."), () => { + if (!this._consoleSnippetsTreeOutline.children.length) + this._consoleSnippetsRow.showEmptyMessage(); + + this._consoleSnippetsContainer.hidden = false; + + this._createConsoleSnippetButton.element.scrollIntoViewIfNeeded(false); + requestAnimationFrame(() => { + let popover = new WI.InputPopover("create-snippet-popover", WI.UIString("Name"), this); + popover.show(this._createConsoleSnippetButton.element, [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X]); + }); + }); + if (WI.NetworkManager.supportsBootstrapScript()) { contextMenu.appendItem(WI.UIString("Inspector Bootstrap Script"), async () => { await WI.networkManager.createBootstrapScript(); @@ -2546,6 +2676,16 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W issueTreeElements.forEach((treeElement) => treeElement.parent.removeChild(treeElement)); } + _handleConsoleSnippetAdded(event) + { + this._addConsoleSnippet(event.data.snippet); + } + + _handleConsoleSnippetRemoved(event) + { + this._removeConsoleSnippet(event.data.snippet); + } + _handleTimelineCapturingStateChanged(event) { this._updateTemporarilyDisabledBreakpointsButtons(); diff --git a/Source/WebInspectorUI/UserInterface/Views/WebSocketDataGridNode.js b/Source/WebInspectorUI/UserInterface/Views/WebSocketDataGridNode.js index 2f904c6d1892..3e6c6b192ad1 100644 --- a/Source/WebInspectorUI/UserInterface/Views/WebSocketDataGridNode.js +++ b/Source/WebInspectorUI/UserInterface/Views/WebSocketDataGridNode.js @@ -53,9 +53,7 @@ WI.WebSocketDataGridNode = class WebSocketDataGridNode extends WI.DataGridNode console.assert(!wasThrown); const title = WI.UIString("Selected Frame"); - const addSpecialUserLogClass = true; - const shouldRevealConsole = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(title, result, addSpecialUserLogClass, shouldRevealConsole); + WI.consoleLogViewController.appendImmediateExecutionWithResult(title, result, {addSpecialUserLogClass: true, shouldRevealConsole: true}); }; if (this._data.isText) { diff --git a/Source/WebInspectorUI/UserInterface/Views/WebSocketResourceTreeElement.js b/Source/WebInspectorUI/UserInterface/Views/WebSocketResourceTreeElement.js index c13d5090a81e..8ff00a92cc1e 100644 --- a/Source/WebInspectorUI/UserInterface/Views/WebSocketResourceTreeElement.js +++ b/Source/WebInspectorUI/UserInterface/Views/WebSocketResourceTreeElement.js @@ -51,8 +51,7 @@ WI.WebSocketResourceTreeElement = class WebSocketResourceTreeElement extends WI. return; const text = WI.UIString("Selected WebSocket"); - const addSpecialUserLogClass = true; - WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); + WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); }); });