mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-09 21:28:43 +08:00
Bug: chromium:1158760 Change-Id: I6d2aadda23109fb8c9581ecb3810c77fd9341b10 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2692147 Commit-Queue: Jan Scheffler <janscheffler@chromium.org> Reviewed-by: Tim van der Lippe <tvanderlippe@chromium.org>
461 lines
15 KiB
TypeScript
461 lines
15 KiB
TypeScript
// Copyright 2016 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.
|
|
|
|
/* eslint-disable rulesdir/no_underscored_properties */
|
|
|
|
import * as Common from '../common/common.js';
|
|
import * as Host from '../host/host.js';
|
|
import * as i18n from '../i18n/i18n.js';
|
|
import * as ObjectUI from '../object_ui/object_ui.js';
|
|
import * as Root from '../root/root.js';
|
|
import * as SDK from '../sdk/sdk.js';
|
|
import * as TextUtils from '../text_utils/text_utils.js';
|
|
import * as UI from '../ui/ui.js';
|
|
|
|
import {ConsolePanel} from './ConsolePanel.js';
|
|
|
|
export const UIStrings = {
|
|
/**
|
|
*@description Text in Console Prompt of the Console panel
|
|
*/
|
|
consolePrompt: 'Console prompt',
|
|
};
|
|
const str_ = i18n.i18n.registerUIStrings('console/ConsolePrompt.ts', UIStrings);
|
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
export class ConsolePrompt extends UI.Widget.Widget {
|
|
_addCompletionsFromHistory: boolean;
|
|
_history: ConsoleHistoryManager;
|
|
_initialText: string;
|
|
_editor: UI.TextEditor.TextEditor|null;
|
|
_eagerPreviewElement: HTMLDivElement;
|
|
_textChangeThrottler: Common.Throttler.Throttler;
|
|
_formatter: ObjectUI.RemoteObjectPreviewFormatter.RemoteObjectPreviewFormatter;
|
|
_requestPreviewBound: () => Promise<void>;
|
|
_innerPreviewElement: HTMLElement;
|
|
_promptIcon: UI.Icon.Icon;
|
|
_iconThrottler: Common.Throttler.Throttler;
|
|
_eagerEvalSetting: Common.Settings.Setting<boolean>;
|
|
_previewRequestForTest: Promise<void>|null;
|
|
_defaultAutocompleteConfig: UI.TextEditor.AutocompleteConfig|null;
|
|
_highlightingNode: boolean;
|
|
|
|
constructor() {
|
|
super();
|
|
this.registerRequiredCSS('console/consolePrompt.css', {enableLegacyPatching: false});
|
|
this._addCompletionsFromHistory = true;
|
|
this._history = new ConsoleHistoryManager();
|
|
|
|
this._initialText = '';
|
|
this._editor = null;
|
|
this._eagerPreviewElement = document.createElement('div');
|
|
this._eagerPreviewElement.classList.add('console-eager-preview');
|
|
this._textChangeThrottler = new Common.Throttler.Throttler(150);
|
|
this._formatter = new ObjectUI.RemoteObjectPreviewFormatter.RemoteObjectPreviewFormatter();
|
|
this._requestPreviewBound = this._requestPreview.bind(this);
|
|
this._innerPreviewElement = this._eagerPreviewElement.createChild('div', 'console-eager-inner-preview');
|
|
this._eagerPreviewElement.appendChild(UI.Icon.Icon.create('smallicon-command-result', 'preview-result-icon'));
|
|
|
|
const editorContainerElement = this.element.createChild('div', 'console-prompt-editor-container');
|
|
this.element.appendChild(this._eagerPreviewElement);
|
|
|
|
this._promptIcon = UI.Icon.Icon.create('smallicon-text-prompt', 'console-prompt-icon');
|
|
this.element.appendChild(this._promptIcon);
|
|
this._iconThrottler = new Common.Throttler.Throttler(0);
|
|
|
|
this._eagerEvalSetting = Common.Settings.Settings.instance().moduleSetting('consoleEagerEval');
|
|
this._eagerEvalSetting.addChangeListener(this._eagerSettingChanged.bind(this));
|
|
this._eagerPreviewElement.classList.toggle('hidden', !this._eagerEvalSetting.get());
|
|
|
|
this.element.tabIndex = 0;
|
|
this._previewRequestForTest = null;
|
|
|
|
this._defaultAutocompleteConfig = null;
|
|
|
|
this._highlightingNode = false;
|
|
|
|
const extension =
|
|
(Root.Runtime.Runtime.instance().extension(UI.TextEditor.TextEditorFactory) as Root.Runtime.Extension);
|
|
extension.instance().then(factory => {
|
|
gotFactory.call(this, (factory as UI.TextEditor.TextEditorFactory));
|
|
});
|
|
|
|
function gotFactory(this: ConsolePrompt, factory: UI.TextEditor.TextEditorFactory): void {
|
|
const options = {
|
|
devtoolsAccessibleName: (i18nString(UIStrings.consolePrompt) as string),
|
|
lineNumbers: false,
|
|
lineWrapping: true,
|
|
mimeType: 'javascript',
|
|
autoHeight: true,
|
|
};
|
|
this._editor = factory.createEditor((options as UI.TextEditor.Options));
|
|
|
|
this._defaultAutocompleteConfig =
|
|
ObjectUI.JavaScriptAutocomplete.JavaScriptAutocompleteConfig.createConfigForEditor(this._editor);
|
|
this._editor.configureAutocomplete(Object.assign({}, this._defaultAutocompleteConfig, {
|
|
suggestionsCallback: this._wordsWithQuery.bind(this),
|
|
anchorBehavior: UI.GlassPane.AnchorBehavior.PreferTop,
|
|
}));
|
|
this._editor.widget().element.addEventListener('keydown', this._editorKeyDown.bind(this), true);
|
|
this._editor.widget().show(editorContainerElement);
|
|
this._editor.addEventListener(UI.TextEditor.Events.CursorChanged, this._updatePromptIcon, this);
|
|
this._editor.addEventListener(UI.TextEditor.Events.TextChanged, this._onTextChanged, this);
|
|
this._editor.addEventListener(UI.TextEditor.Events.SuggestionChanged, this._onTextChanged, this);
|
|
|
|
this.setText(this._initialText);
|
|
this._initialText = '';
|
|
if (this.hasFocus()) {
|
|
this.focus();
|
|
}
|
|
this.element.removeAttribute('tabindex');
|
|
this._editor.widget().element.tabIndex = -1;
|
|
|
|
this._editorSetForTest();
|
|
|
|
// Record the console tool load time after the console prompt constructor is complete.
|
|
Host.userMetrics.panelLoaded('console', 'DevTools.Launch.Console');
|
|
}
|
|
}
|
|
|
|
_eagerSettingChanged(): void {
|
|
const enabled = this._eagerEvalSetting.get();
|
|
this._eagerPreviewElement.classList.toggle('hidden', !enabled);
|
|
if (enabled) {
|
|
this._requestPreview();
|
|
}
|
|
}
|
|
|
|
belowEditorElement(): Element {
|
|
return this._eagerPreviewElement;
|
|
}
|
|
|
|
_onTextChanged(): void {
|
|
// ConsoleView and prompt both use a throttler, so we clear the preview
|
|
// ASAP to avoid inconsistency between a fresh viewport and stale preview.
|
|
if (this._eagerEvalSetting.get()) {
|
|
const asSoonAsPossible = !this._editor || !this._editor.textWithCurrentSuggestion();
|
|
this._previewRequestForTest = this._textChangeThrottler.schedule(this._requestPreviewBound, asSoonAsPossible);
|
|
}
|
|
this._updatePromptIcon();
|
|
this.dispatchEventToListeners(Events.TextChanged);
|
|
}
|
|
|
|
async _requestPreview(): Promise<void> {
|
|
if (!this._editor) {
|
|
return;
|
|
}
|
|
const text = this._editor.textWithCurrentSuggestion().trim();
|
|
const executionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext);
|
|
const {preview, result} =
|
|
await ObjectUI.JavaScriptREPL.JavaScriptREPL.evaluateAndBuildPreview(text, true /* throwOnSideEffect */, 500);
|
|
this._innerPreviewElement.removeChildren();
|
|
if (preview.deepTextContent() !== this._editor.textWithCurrentSuggestion().trim()) {
|
|
this._innerPreviewElement.appendChild(preview);
|
|
}
|
|
if (result && 'object' in result && result.object && result.object.subtype === 'node') {
|
|
this._highlightingNode = true;
|
|
SDK.OverlayModel.OverlayModel.highlightObjectAsDOMNode(result.object);
|
|
} else if (this._highlightingNode) {
|
|
this._highlightingNode = false;
|
|
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
|
|
}
|
|
if (result && executionContext) {
|
|
executionContext.runtimeModel.releaseEvaluationResult(result);
|
|
}
|
|
}
|
|
|
|
willHide(): void {
|
|
if (this._highlightingNode) {
|
|
this._highlightingNode = false;
|
|
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
|
|
}
|
|
}
|
|
|
|
history(): ConsoleHistoryManager {
|
|
return this._history;
|
|
}
|
|
|
|
clearAutocomplete(): void {
|
|
if (this._editor) {
|
|
this._editor.clearAutocomplete();
|
|
}
|
|
}
|
|
|
|
_isCaretAtEndOfPrompt(): boolean {
|
|
return this._editor !== null &&
|
|
this._editor.selection().collapseToEnd().equal(this._editor.fullRange().collapseToEnd());
|
|
}
|
|
|
|
moveCaretToEndOfPrompt(): void {
|
|
if (this._editor) {
|
|
this._editor.setSelection(TextUtils.TextRange.TextRange.createFromLocation(Infinity, Infinity));
|
|
}
|
|
}
|
|
|
|
setText(text: string): void {
|
|
if (this._editor) {
|
|
this._editor.setText(text);
|
|
} else {
|
|
this._initialText = text;
|
|
}
|
|
this.dispatchEventToListeners(Events.TextChanged);
|
|
}
|
|
|
|
text(): string {
|
|
return this._editor ? this._editor.text() : this._initialText;
|
|
}
|
|
|
|
setAddCompletionsFromHistory(value: boolean): void {
|
|
this._addCompletionsFromHistory = value;
|
|
}
|
|
|
|
_editorKeyDown(event: Event): void {
|
|
if (!this._editor) {
|
|
return;
|
|
}
|
|
const keyboardEvent = (event as KeyboardEvent);
|
|
let newText;
|
|
let isPrevious;
|
|
// Check against visual coordinates in case lines wrap.
|
|
const selection = this._editor.selection();
|
|
const cursorY = this._editor.visualCoordinates(selection.endLine, selection.endColumn).y;
|
|
|
|
switch (keyboardEvent.keyCode) {
|
|
case UI.KeyboardShortcut.Keys.Up.code: {
|
|
const startY = this._editor.visualCoordinates(0, 0).y;
|
|
if (keyboardEvent.shiftKey || !selection.isEmpty() || cursorY !== startY) {
|
|
break;
|
|
}
|
|
newText = this._history.previous(this.text());
|
|
isPrevious = true;
|
|
break;
|
|
}
|
|
case UI.KeyboardShortcut.Keys.Down.code: {
|
|
const fullRange = this._editor.fullRange();
|
|
const endY = this._editor.visualCoordinates(fullRange.endLine, fullRange.endColumn).y;
|
|
if (keyboardEvent.shiftKey || !selection.isEmpty() || cursorY !== endY) {
|
|
break;
|
|
}
|
|
newText = this._history.next();
|
|
break;
|
|
}
|
|
case UI.KeyboardShortcut.Keys.P.code: { // Ctrl+P = Previous
|
|
if (Host.Platform.isMac() && keyboardEvent.ctrlKey && !keyboardEvent.metaKey && !keyboardEvent.altKey &&
|
|
!keyboardEvent.shiftKey) {
|
|
newText = this._history.previous(this.text());
|
|
isPrevious = true;
|
|
}
|
|
break;
|
|
}
|
|
case UI.KeyboardShortcut.Keys.N.code: { // Ctrl+N = Next
|
|
if (Host.Platform.isMac() && keyboardEvent.ctrlKey && !keyboardEvent.metaKey && !keyboardEvent.altKey &&
|
|
!keyboardEvent.shiftKey) {
|
|
newText = this._history.next();
|
|
}
|
|
break;
|
|
}
|
|
case UI.KeyboardShortcut.Keys.Enter.code: {
|
|
this._enterKeyPressed(keyboardEvent);
|
|
break;
|
|
}
|
|
case UI.KeyboardShortcut.Keys.Tab.code: {
|
|
if (!this.text()) {
|
|
keyboardEvent.consume();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (newText === undefined) {
|
|
return;
|
|
}
|
|
keyboardEvent.consume(true);
|
|
this.setText(newText);
|
|
|
|
if (isPrevious) {
|
|
this._editor.setSelection(TextUtils.TextRange.TextRange.createFromLocation(0, Infinity));
|
|
} else {
|
|
this.moveCaretToEndOfPrompt();
|
|
}
|
|
}
|
|
|
|
async _enterWillEvaluate(): Promise<boolean> {
|
|
if (!this._isCaretAtEndOfPrompt()) {
|
|
return true;
|
|
}
|
|
return await ObjectUI.JavaScriptAutocomplete.JavaScriptAutocomplete.isExpressionComplete(this.text());
|
|
}
|
|
|
|
_updatePromptIcon(): void {
|
|
this._iconThrottler.schedule(async () => {
|
|
const canComplete = await this._enterWillEvaluate();
|
|
this._promptIcon.classList.toggle('console-prompt-incomplete', !canComplete);
|
|
});
|
|
}
|
|
|
|
async _enterKeyPressed(event: KeyboardEvent): Promise<void> {
|
|
if (event.altKey || event.ctrlKey || event.shiftKey) {
|
|
return;
|
|
}
|
|
|
|
event.consume(true);
|
|
|
|
// Since we prevent default, manually emulate the native "scroll on key input" behavior.
|
|
this.element.scrollIntoView();
|
|
this.clearAutocomplete();
|
|
|
|
const str = this.text();
|
|
if (!str.length) {
|
|
return;
|
|
}
|
|
|
|
if (await this._enterWillEvaluate()) {
|
|
await this._appendCommand(str, true);
|
|
} else if (this._editor) {
|
|
this._editor.newlineAndIndent();
|
|
}
|
|
this._enterProcessedForTest();
|
|
}
|
|
|
|
async _appendCommand(text: string, useCommandLineAPI: boolean): Promise<void> {
|
|
this.setText('');
|
|
const currentExecutionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext);
|
|
if (currentExecutionContext) {
|
|
const executionContext = currentExecutionContext;
|
|
const message = SDK.ConsoleModel.ConsoleModel.instance().addCommandMessage(executionContext, text);
|
|
const expression = ObjectUI.JavaScriptREPL.JavaScriptREPL.preprocessExpression(text);
|
|
SDK.ConsoleModel.ConsoleModel.instance().evaluateCommandInConsole(
|
|
executionContext, message, expression, useCommandLineAPI);
|
|
if (ConsolePanel.instance().isShowing()) {
|
|
Host.userMetrics.actionTaken(Host.UserMetrics.Action.CommandEvaluatedInConsolePanel);
|
|
}
|
|
}
|
|
}
|
|
|
|
_enterProcessedForTest(): void {
|
|
}
|
|
|
|
_historyCompletions(prefix: string, force?: boolean): UI.SuggestBox.Suggestions {
|
|
const text = this.text();
|
|
if (!this._addCompletionsFromHistory || !this._isCaretAtEndOfPrompt() || (!text && !force)) {
|
|
return [];
|
|
}
|
|
const result = [];
|
|
const set = new Set<string>();
|
|
const data = this._history.historyData();
|
|
for (let i = data.length - 1; i >= 0 && result.length < 50; --i) {
|
|
const item = data[i];
|
|
if (!item.startsWith(text)) {
|
|
continue;
|
|
}
|
|
if (set.has(item)) {
|
|
continue;
|
|
}
|
|
set.add(item);
|
|
result.push(
|
|
{text: item.substring(text.length - prefix.length), iconType: 'smallicon-text-prompt', isSecondary: true});
|
|
}
|
|
return result as UI.SuggestBox.Suggestions;
|
|
}
|
|
|
|
focus(): void {
|
|
if (this._editor) {
|
|
this._editor.widget().focus();
|
|
} else {
|
|
this.element.focus();
|
|
}
|
|
}
|
|
|
|
async _wordsWithQuery(
|
|
queryRange: TextUtils.TextRange.TextRange, substituteRange: TextUtils.TextRange.TextRange,
|
|
force?: boolean): Promise<UI.SuggestBox.Suggestions> {
|
|
if (!this._editor || !this._defaultAutocompleteConfig || !this._defaultAutocompleteConfig.suggestionsCallback) {
|
|
return [];
|
|
}
|
|
const query = this._editor.text(queryRange);
|
|
const words = await this._defaultAutocompleteConfig.suggestionsCallback(queryRange, substituteRange, force);
|
|
const historyWords = this._historyCompletions(query, force);
|
|
return words ? words.concat(historyWords) : historyWords;
|
|
}
|
|
|
|
_editorSetForTest(): void {
|
|
}
|
|
}
|
|
|
|
export class ConsoleHistoryManager {
|
|
_data: string[];
|
|
_historyOffset: number;
|
|
_uncommittedIsTop?: boolean;
|
|
constructor() {
|
|
this._data = [];
|
|
|
|
/**
|
|
* 1-based entry in the history stack.
|
|
*/
|
|
this._historyOffset = 1;
|
|
}
|
|
|
|
historyData(): string[] {
|
|
return this._data;
|
|
}
|
|
|
|
setHistoryData(data: string[]): void {
|
|
this._data = data.slice();
|
|
this._historyOffset = 1;
|
|
}
|
|
|
|
/**
|
|
* Pushes a committed text into the history.
|
|
*/
|
|
pushHistoryItem(text: string): void {
|
|
if (this._uncommittedIsTop) {
|
|
this._data.pop();
|
|
delete this._uncommittedIsTop;
|
|
}
|
|
|
|
this._historyOffset = 1;
|
|
if (text === this._currentHistoryItem()) {
|
|
return;
|
|
}
|
|
this._data.push(text);
|
|
}
|
|
|
|
/**
|
|
* Pushes the current (uncommitted) text into the history.
|
|
*/
|
|
_pushCurrentText(currentText: string): void {
|
|
if (this._uncommittedIsTop) {
|
|
this._data.pop();
|
|
} // Throw away obsolete uncommitted text.
|
|
this._uncommittedIsTop = true;
|
|
this._data.push(currentText);
|
|
}
|
|
|
|
previous(currentText: string): string|undefined {
|
|
if (this._historyOffset > this._data.length) {
|
|
return undefined;
|
|
}
|
|
if (this._historyOffset === 1) {
|
|
this._pushCurrentText(currentText);
|
|
}
|
|
++this._historyOffset;
|
|
return this._currentHistoryItem();
|
|
}
|
|
|
|
next(): string|undefined {
|
|
if (this._historyOffset === 1) {
|
|
return undefined;
|
|
}
|
|
--this._historyOffset;
|
|
return this._currentHistoryItem();
|
|
}
|
|
|
|
_currentHistoryItem(): string|undefined {
|
|
return this._data[this._data.length - this._historyOffset];
|
|
}
|
|
}
|
|
|
|
export const enum Events {
|
|
TextChanged = 'TextChanged',
|
|
}
|