Commit 589cdcb3 authored by Erik Luo's avatar Erik Luo Committed by Commit Bot

DevTools: pinned expressions in console

Part 1: introduce pins behind an experiment.
- Prompt has 'pin' button to add current expression to pins
- Pin pane sticks to the top of the Console, with max height
- Pins have multiline editors and a value preview that shows
  "not available" when there is an error

Screenshot: https://imgur.com/a/Ahdtosa

Bug: 849875
Change-Id: Ib0b8bafd97551f823ff8b64d13c5259f42328994
Reviewed-on: https://chromium-review.googlesource.com/1087509
Commit-Queue: Erik Luo <luoe@chromium.org>
Reviewed-by: default avatarDmitry Gozman <dgozman@chromium.org>
Reviewed-by: default avatarJoel Einbinder <einbinder@chromium.org>
Cr-Commit-Position: refs/heads/master@{#574740}
parent 79fba743
......@@ -174,9 +174,11 @@ all_devtools_files = [
"front_end/console/ConsoleFilter.js",
"front_end/console/ConsoleSidebar.js",
"front_end/console/ConsolePanel.js",
"front_end/console/ConsolePinPane.js",
"front_end/console/ConsolePrompt.js",
"front_end/console/consoleView.css",
"front_end/console/consoleContextSelector.css",
"front_end/console/consolePinPane.css",
"front_end/console/consoleSidebar.css",
"front_end/console/ConsoleView.js",
"front_end/console/ConsoleViewMessage.js",
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
Console.ConsolePinPane = class extends UI.VBox {
constructor() {
super(true);
this.registerRequiredCSS('console/consolePinPane.css');
this.contentElement.classList.add('console-pins', 'monospace');
this.contentElement.addEventListener('contextmenu', this._contextMenuEventFired.bind(this), false);
/** @type {!Set<!Console.ConsolePin>} */
this._pins = new Set();
}
/**
* @param {!Event} event
*/
_contextMenuEventFired(event) {
const contextMenu = new UI.ContextMenu(event);
const target = event.deepElementFromPoint();
if (target) {
const targetPinElement = target.enclosingNodeOrSelfWithClass('console-pin');
if (targetPinElement) {
const targetPin = targetPinElement[Console.ConsolePin._PinSymbol];
contextMenu.editSection().appendItem(ls`Edit pin`, targetPin.focus.bind(targetPin));
contextMenu.editSection().appendItem(ls`Remove pin`, this._removePin.bind(this, targetPin));
}
}
contextMenu.editSection().appendItem(ls`Remove all pins`, this._removeAllPins.bind(this));
contextMenu.show();
}
_removeAllPins() {
for (const pin of this._pins)
this._removePin(pin);
}
/**
* @param {!Console.ConsolePin} pin
*/
_removePin(pin) {
pin.element().remove();
this._pins.delete(pin);
}
/**
* @param {string} expression
*/
addPin(expression) {
const pin = new Console.ConsolePin(expression, this._removePin.bind(this));
this.contentElement.appendChild(pin.element());
this._pins.add(pin);
pin.focus();
}
};
Console.ConsolePin = class {
/**
* @param {string} expression
* @param {function(!Console.ConsolePin)} onRemove
*/
constructor(expression, onRemove) {
const deletePinIcon = UI.Icon.create('smallicon-cross', 'console-delete-pin');
deletePinIcon.addEventListener('click', () => onRemove(this));
const fragment = UI.Fragment.build`
<div class='console-pin'>
${deletePinIcon}
<div class='console-pin-name' $='name'></div>
<div class='console-pin-preview'>${ls`not available`}</div>
</div>`;
this._pinElement = fragment.element();
const nameElement = fragment.$('name');
nameElement.title = expression;
this._pinElement[Console.ConsolePin._PinSymbol] = this;
/** @type {?UI.TextEditor} */
this._editor = null;
this._editorPromise = self.runtime.extension(UI.TextEditorFactory).instance().then(factory => {
this._editor = factory.createEditor({
lineNumbers: false,
lineWrapping: true,
mimeType: 'javascript',
autoHeight: true,
placeholder: ls`Expression`
});
this._editor.widget().show(nameElement);
this._editor.widget().element.classList.add('console-pin-editor');
this._editor.widget().element.tabIndex = -1;
this._editor.setText(expression);
this._editor.widget().element.addEventListener('keydown', event => {
if (event.key === 'Tab')
event.consume();
}, true);
});
}
/**
* @return {!Element}
*/
element() {
return this._pinElement;
}
async focus() {
await this._editorPromise;
this._editor.widget().focus();
this._editor.setSelection(TextUtils.TextRange.createFromLocation(Infinity, Infinity));
}
};
Console.ConsolePin._PinSymbol = Symbol('pinSymbol');
......@@ -23,6 +23,10 @@ Console.ConsolePrompt = class extends UI.Widget {
this._eagerEvalSetting.addChangeListener(this._eagerSettingChanged.bind(this));
this._eagerPreviewElement.classList.toggle('hidden', !this._eagerEvalSetting.get());
// TODO(luoe): split out prompt styles into ConsolePrompt.css.
const pinsEnabled = Runtime.experiments.isEnabled('pinnedExpressions');
if (pinsEnabled)
this.element.style.marginRight = '20px';
this.element.tabIndex = 0;
/** @type {?Promise} */
this._previewRequestForTest = null;
......@@ -50,6 +54,13 @@ Console.ConsolePrompt = class extends UI.Widget {
this._editor.widget().show(this.element);
this._editor.addEventListener(UI.TextEditor.Events.TextChanged, this._onTextChanged, this);
this._editor.addEventListener(UI.TextEditor.Events.SuggestionChanged, this._onTextChanged, this);
if (pinsEnabled) {
const pinButton = this.element.createChild('span', 'command-pin-button');
pinButton.title = ls`Pin expression`;
pinButton.addEventListener('click', () => {
this.dispatchEventToListeners(Console.ConsolePrompt.Events.ExpressionPinned, this.text());
});
}
if (this._isBelowPromptEnabled)
this.element.appendChild(this._eagerPreviewElement);
......@@ -447,5 +458,6 @@ Console.ConsoleHistoryManager = class {
};
Console.ConsolePrompt.Events = {
ExpressionPinned: Symbol('ExpressionPinned'),
TextChanged: Symbol('TextChanged')
};
......@@ -153,6 +153,12 @@ Console.ConsoleView = class extends UI.VBox {
this._showSettingsPaneSetting.addChangeListener(
() => settingsPane.element.classList.toggle('hidden', !this._showSettingsPaneSetting.get()));
if (Runtime.experiments.isEnabled('pinnedExpressions')) {
this._pinPane = new Console.ConsolePinPane();
this._pinPane.element.classList.add('console-view-pinpane');
this._pinPane.show(this._contentsElement);
}
this._viewport = new Console.ConsoleViewport(this);
this._viewport.setStickToBottom(true);
this._viewport.contentElement().classList.add('console-group', 'console-group-messages');
......@@ -192,6 +198,7 @@ Console.ConsoleView = class extends UI.VBox {
this._prompt = new Console.ConsolePrompt();
this._prompt.show(this._promptElement);
this._prompt.element.addEventListener('keydown', this._promptKeyDown.bind(this), true);
this._prompt.addEventListener(Console.ConsolePrompt.Events.ExpressionPinned, this._promptExpressionPinned, this);
this._prompt.addEventListener(Console.ConsolePrompt.Events.TextChanged, this._promptTextChanged, this);
this._consoleHistoryAutocompleteSetting.addChangeListener(this._consoleHistoryAutocompleteChanged, this);
......@@ -1148,6 +1155,14 @@ Console.ConsoleView = class extends UI.VBox {
this._updateStickToBottomOnMouseUp();
}
/**
* @param {!Common.Event} event
*/
_promptExpressionPinned(event) {
const text = /** @type {string} */ (event.data);
this._pinPane.addPin(text);
}
_promptTextChanged() {
this._viewport.setStickToBottom(this._isScrolledToBottom());
this._promptTextChangedForTest();
......
/*
* Copyright 2018 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
.console-pins {
overflow-y: auto;
background: var(--toolbar-bg-color);
}
.console-pins:not(:empty) {
border-bottom: 1px solid var(--divider-color);
padding: 5px 28px 0 24px;
}
.console-pin {
margin-bottom: 2px;
position: relative;
user-select: text;
flex: none;
padding-bottom: 6px;
}
.console-pin:not(:last-child) {
border-bottom: 1px solid #e4e4e4;
}
.console-pin-name {
margin-left: -4px;
margin-bottom: 1px;
height: auto;
}
.console-pin-name,
.console-pin-preview {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.console-delete-pin {
position: absolute;
top: 6px;
left: -16px;
opacity: 0.5;
cursor: pointer;
}
.console-delete-pin:hover {
opacity: 1;
}
.console-pin-name:focus-within {
background: #fff;
box-shadow: var(--focus-ring-active-shadow);
}
.console-pin-name:not(:focus-within):not(:hover) {
opacity: 0.6;
}
......@@ -464,3 +464,33 @@
left: -13px;
top: 1px;
}
.console-view-pinpane {
flex: none;
max-height: 200px;
}
.command-pin-button::before {
content: '\1f4cc';
}
.command-pin-button {
cursor: pointer;
position: absolute;
top: 1px;
right: -40px;
opacity: 0;
width: 40px;
}
#console-prompt:hover .command-pin-button {
opacity: 0.12;
}
.-theme-with-dark-background #console-prompt:hover .command-pin-button {
opacity: 0.25;
}
#console-prompt:hover .command-pin-button:hover {
opacity: 0.6;
}
......@@ -188,6 +188,7 @@
"scripts": [
"ConsoleContextSelector.js",
"ConsoleFilter.js",
"ConsolePinPane.js",
"ConsoleSidebar.js",
"ConsoleViewport.js",
"ConsoleViewMessage.js",
......@@ -197,6 +198,7 @@
],
"resources": [
"consoleContextSelector.css",
"consolePinPane.css",
"consoleSidebar.css",
"consoleView.css"
]
......
......@@ -114,6 +114,7 @@ Main.Main = class {
Runtime.experiments.register('nativeHeapProfiler', 'Native memory sampling heap profiler', true);
Runtime.experiments.register('networkSearch', 'Network search');
Runtime.experiments.register('oopifInlineDOM', 'OOPIF: inline DOM ', true);
Runtime.experiments.register('pinnedExpressions', 'Pinned expressions in Console', true);
Runtime.experiments.register('protocolMonitor', 'Protocol Monitor');
Runtime.experiments.register('sourceDiff', 'Source diff');
Runtime.experiments.register('sourcesPrettyPrint', 'Automatically pretty print in the Sources Panel');
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment