UNPKG

chrome-devtools-frontend

Version:
360 lines (316 loc) • 14.4 kB
// 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. import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Root from '../../core/root/root.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Formatter from '../../models/formatter/formatter.js'; import * as SourceMapScopes from '../../models/source_map_scopes/source_map_scopes.js'; import * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js'; import * as IconButton from '../../ui/components/icon_button/icon_button.js'; import * as TextEditor from '../../ui/components/text_editor/text_editor.js'; import * as ObjectUI from '../../ui/legacy/components/object_ui/object_ui.js'; import * as UI from '../../ui/legacy/legacy.js'; import {ConsolePanel} from './ConsolePanel.js'; import consolePromptStyles from './consolePrompt.css.js'; const {Direction} = TextEditor.TextEditorHistory; const UIStrings = { /** *@description Text in Console Prompt of the Console panel */ consolePrompt: 'Console prompt', }; const str_ = i18n.i18n.registerUIStrings('panels/console/ConsolePrompt.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class ConsolePrompt extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.Widget>( UI.Widget.Widget) { private addCompletionsFromHistory: boolean; private historyInternal: TextEditor.AutocompleteHistory.AutocompleteHistory; private initialText: string; private editor: TextEditor.TextEditor.TextEditor; private readonly eagerPreviewElement: HTMLDivElement; private textChangeThrottler: Common.Throttler.Throttler; private readonly formatter: ObjectUI.RemoteObjectPreviewFormatter.RemoteObjectPreviewFormatter; private requestPreviewBound: () => Promise<void>; private requestPreviewCurrent = 0; private readonly innerPreviewElement: HTMLElement; private readonly promptIcon: IconButton.Icon.Icon; private readonly iconThrottler: Common.Throttler.Throttler; private readonly eagerEvalSetting: Common.Settings.Setting<boolean>; private previewRequestForTest: Promise<void>|null; private highlightingNode: boolean; // The CodeMirror state field that controls whether the argument hints are showing. // If they are, the escape key will clear them. However, if they aren't, then the // console drawer should be hidden as a whole. #argumentHintsState: CodeMirror.StateField<CodeMirror.Tooltip|null>; #editorHistory: TextEditor.TextEditorHistory.TextEditorHistory; constructor() { super(); this.addCompletionsFromHistory = true; this.historyInternal = new TextEditor.AutocompleteHistory.AutocompleteHistory( Common.Settings.Settings.instance().createLocalSetting('consoleHistory', [])); this.initialText = ''; 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'); const previewIcon = new IconButton.Icon.Icon(); previewIcon.data = {iconName: 'chevron-left-dot', color: 'var(--icon-default)', width: '16px', height: '16px'}; previewIcon.classList.add('preview-result-icon'); this.eagerPreviewElement.appendChild(previewIcon); const editorContainerElement = this.element.createChild('div', 'console-prompt-editor-container'); this.element.appendChild(this.eagerPreviewElement); this.promptIcon = new IconButton.Icon.Icon(); this.promptIcon.data = {iconName: 'chevron-right', color: 'var(--icon-action)', width: '16px', height: '16px'}; this.promptIcon.classList.add('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.highlightingNode = false; const argumentHints = TextEditor.JavaScript.argumentHints(); this.#argumentHintsState = argumentHints[0]; const autocompleteOnEnter = TextEditor.Config.DynamicSetting.bool( 'consoleAutocompleteOnEnter', [], TextEditor.Config.conservativeCompletion); const extensions = [ CodeMirror.keymap.of(this.editorKeymap()), CodeMirror.EditorView.updateListener.of(update => this.editorUpdate(update)), argumentHints, autocompleteOnEnter.instance(), TextEditor.Config.showCompletionHint, TextEditor.Config.baseConfiguration(this.initialText), TextEditor.Config.autocompletion.instance(), CodeMirror.javascript.javascriptLanguage.data.of({ autocomplete: (context: CodeMirror.CompletionContext): CodeMirror.CompletionResult | null => this.addCompletionsFromHistory ? this.#editorHistory.historyCompletions(context) : null, }), CodeMirror.EditorView.contentAttributes.of({'aria-label': i18nString(UIStrings.consolePrompt)}), CodeMirror.EditorView.lineWrapping, CodeMirror.autocompletion({aboveCursor: true}), ]; if (Root.Runtime.Runtime.queryParam('noJavaScriptCompletion') !== 'true') { extensions.push( CodeMirror.javascript.javascript(), TextEditor.JavaScript.completion(), ); } else { extensions.push(CodeMirror.javascript.javascriptLanguage); } const doc = this.initialText; const editorState = CodeMirror.EditorState.create({doc, extensions}); this.editor = new TextEditor.TextEditor.TextEditor(editorState); this.editor.addEventListener('keydown', (event): void => { if (event.defaultPrevented) { event.stopPropagation(); } }); editorContainerElement.appendChild(this.editor); this.#editorHistory = new TextEditor.TextEditorHistory.TextEditorHistory(this.editor, this.historyInternal); if (this.hasFocus()) { this.focus(); } this.element.removeAttribute('tabindex'); this.editorSetForTest(); // Record the console tool load time after the console prompt constructor is complete. Host.userMetrics.panelLoaded('console', 'DevTools.Launch.Console'); } private eagerSettingChanged(): void { const enabled = this.eagerEvalSetting.get(); this.eagerPreviewElement.classList.toggle('hidden', !enabled); if (enabled) { void this.requestPreview(); } } belowEditorElement(): Element { return this.eagerPreviewElement; } private 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 = !TextEditor.Config.contentIncludingHint(this.editor.editor); this.previewRequestForTest = this.textChangeThrottler.schedule(this.requestPreviewBound, asSoonAsPossible); } this.updatePromptIcon(); this.dispatchEventToListeners(Events.TextChanged); } private async requestPreview(): Promise<void> { const id = ++this.requestPreviewCurrent; const text = TextEditor.Config.contentIncludingHint(this.editor.editor).trim(); const executionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext); const {preview, result} = await ObjectUI.JavaScriptREPL.JavaScriptREPL.evaluateAndBuildPreview( text, true /* throwOnSideEffect */, true /* replMode */, 500 /* timeout */); if (this.requestPreviewCurrent !== id) { return; } this.innerPreviewElement.removeChildren(); if (preview.deepTextContent() !== TextEditor.Config.contentIncludingHint(this.editor.editor).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); } } override wasShown(): void { super.wasShown(); this.registerCSSFiles([consolePromptStyles]); } override willHide(): void { if (this.highlightingNode) { this.highlightingNode = false; SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); } } history(): TextEditor.AutocompleteHistory.AutocompleteHistory { return this.historyInternal; } clearAutocomplete(): void { CodeMirror.closeCompletion(this.editor.editor); } private isCaretAtEndOfPrompt(): boolean { return this.editor.state.selection.main.head === this.editor.state.doc.length; } moveCaretToEndOfPrompt(): void { this.editor.dispatch({ selection: CodeMirror.EditorSelection.cursor(this.editor.state.doc.length), }); } clear(): void { this.editor.dispatch({ changes: {from: 0, to: this.editor.state.doc.length}, }); } text(): string { return this.editor.state.doc.toString(); } setAddCompletionsFromHistory(value: boolean): void { this.addCompletionsFromHistory = value; } private editorKeymap(): readonly CodeMirror.KeyBinding[] { return [ {key: 'ArrowUp', run: (): boolean => this.#editorHistory.moveHistory(Direction.BACKWARD)}, {key: 'ArrowDown', run: (): boolean => this.#editorHistory.moveHistory(Direction.FORWARD)}, {mac: 'Ctrl-p', run: (): boolean => this.#editorHistory.moveHistory(Direction.BACKWARD, true)}, {mac: 'Ctrl-n', run: (): boolean => this.#editorHistory.moveHistory(Direction.FORWARD, true)}, { key: 'Escape', run: (): boolean => { return TextEditor.JavaScript.closeArgumentsHintsTooltip(this.editor.editor, this.#argumentHintsState); }, }, { key: 'Ctrl-Enter', run: (): boolean => { void this.handleEnter(/* forceEvaluate */ true); return true; }, }, { key: 'Enter', run: (): boolean => { void this.handleEnter(); return true; }, shift: CodeMirror.insertNewlineAndIndent, }, ]; } private async enterWillEvaluate(forceEvaluate?: boolean): Promise<boolean> { const {doc, selection} = this.editor.state; if (!doc.length) { return false; } if (forceEvaluate || selection.main.head < doc.length) { return true; } return await TextEditor.JavaScript.isExpressionComplete(doc.toString()); } private async handleEnter(forceEvaluate?: boolean): Promise<void> { if (await this.enterWillEvaluate(forceEvaluate)) { this.appendCommand(this.text(), true); TextEditor.JavaScript.closeArgumentsHintsTooltip(this.editor.editor, this.#argumentHintsState); this.editor.dispatch({ changes: {from: 0, to: this.editor.state.doc.length}, scrollIntoView: true, }); } else if (this.editor.state.doc.length) { CodeMirror.insertNewlineAndIndent(this.editor.editor); } else { this.editor.dispatch({scrollIntoView: true}); } } private updatePromptIcon(): void { void this.iconThrottler.schedule(async () => { this.promptIcon.classList.toggle('console-prompt-incomplete', !(await this.enterWillEvaluate())); }); } private appendCommand(text: string, useCommandLineAPI: boolean): void { const currentExecutionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext); if (currentExecutionContext) { const executionContext = currentExecutionContext; const consoleModel = executionContext.target().model(SDK.ConsoleModel.ConsoleModel); if (consoleModel) { const message = consoleModel.addCommandMessage(executionContext, text); const expression = ObjectUI.JavaScriptREPL.JavaScriptREPL.wrapObjectLiteral(text); void this.evaluateCommandInConsole(executionContext, message, expression, useCommandLineAPI); if (ConsolePanel.instance().isShowing()) { Host.userMetrics.actionTaken(Host.UserMetrics.Action.CommandEvaluatedInConsolePanel); } } } } private async evaluateCommandInConsole( executionContext: SDK.RuntimeModel.ExecutionContext, message: SDK.ConsoleModel.ConsoleMessage, expression: string, useCommandLineAPI: boolean): Promise<void> { if (Root.Runtime.experiments.isEnabled('evaluateExpressionsWithSourceMaps')) { const callFrame = executionContext.debuggerModel.selectedCallFrame(); if (callFrame) { const nameMap = await SourceMapScopes.NamesResolver.allVariablesInCallFrame(callFrame); expression = await this.substituteNames(expression, nameMap); } } await executionContext.target() .model(SDK.ConsoleModel.ConsoleModel) ?.evaluateCommandInConsole(executionContext, message, expression, useCommandLineAPI); } private async substituteNames(expression: string, mapping: Map<string, string>): Promise<string> { try { return await Formatter.FormatterWorkerPool.formatterWorkerPool().javaScriptSubstitute(expression, mapping); } catch { return expression; } } private editorUpdate(update: CodeMirror.ViewUpdate): void { if (update.docChanged || CodeMirror.selectedCompletion(update.state) !== CodeMirror.selectedCompletion(update.startState)) { this.onTextChanged(); } else if (update.selectionSet) { this.updatePromptIcon(); } } override focus(): void { this.editor.focus(); } private editorSetForTest(): void { } } export const enum Events { TextChanged = 'TextChanged', } export type EventTypes = { [Events.TextChanged]: void, };