chrome-devtools-frontend
Version:
Chrome DevTools UI
262 lines (232 loc) • 10.8 kB
text/typescript
// 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.
/* eslint-disable rulesdir/no-imperative-dom-api */
import '../../ui/legacy/legacy.js';
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as BreakpointManager from '../../models/breakpoints/breakpoints.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 UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import breakpointEditDialogStyles from './breakpointEditDialog.css.js';
const {Direction} = TextEditor.TextEditorHistory;
const UIStrings = {
/**
*@description Screen reader label for a select box that chooses the breakpoint type in the Sources panel when editing a breakpoint
*/
breakpointType: 'Breakpoint type',
/**
*@description Text in Breakpoint Edit Dialog of the Sources panel
*/
breakpoint: 'Breakpoint',
/**
*@description Tooltip text in Breakpoint Edit Dialog of the Sources panel that shows up when hovering over the close icon
*/
closeDialog: 'Close edit dialog and save changes',
/**
*@description Text in Breakpoint Edit Dialog of the Sources panel
*/
conditionalBreakpoint: 'Conditional breakpoint',
/**
*@description Text in Breakpoint Edit Dialog of the Sources panel
*/
logpoint: 'Logpoint',
/**
*@description Text in Breakpoint Edit Dialog of the Sources panel
*/
expressionToCheckBeforePausingEg: 'Expression to check before pausing, e.g. x > 5',
/**
*@description Type selector element title in Breakpoint Edit Dialog of the Sources panel
*/
pauseOnlyWhenTheConditionIsTrue: 'Pause only when the condition is true',
/**
* @description Link text in the Breakpoint Edit Dialog of the Sources panel
*/
learnMoreOnBreakpointTypes: 'Learn more: Breakpoint Types',
/**
*@description Text in Breakpoint Edit Dialog of the Sources panel. It is used as
*the placeholder for a text input field before the user enters text. Provides the user with
*an example on how to use Logpoints. 'Log' is a verb and 'message' is a noun.
*See: https://developer.chrome.com/blog/new-in-devtools-73/#logpoints
*/
logMessageEgXIsX: 'Log message, e.g. `\'x is\', x`',
/**
*@description Type selector element title in Breakpoint Edit Dialog of the Sources panel
*/
logAMessageToConsoleDoNotBreak: 'Log a message to Console, do not break',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/sources/BreakpointEditDialog.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export interface BreakpointEditDialogResult {
committed: boolean;
condition: BreakpointManager.BreakpointManager.UserCondition;
isLogpoint: boolean;
}
export class BreakpointEditDialog extends UI.Widget.Widget {
private readonly onFinish: (result: BreakpointEditDialogResult) => void;
private finished: boolean;
private editor: TextEditor.TextEditor.TextEditor;
private readonly typeSelector: UI.Toolbar.ToolbarComboBox;
private placeholderCompartment: CodeMirror.Compartment;
#history: TextEditor.AutocompleteHistory.AutocompleteHistory;
#editorHistory: TextEditor.TextEditorHistory.TextEditorHistory;
constructor(
editorLineNumber: number, oldCondition: string, isLogpoint: boolean,
onFinish: (result: BreakpointEditDialogResult) => void) {
super(true);
this.registerRequiredCSS(breakpointEditDialogStyles);
const editorConfig = [
CodeMirror.javascript.javascriptLanguage,
TextEditor.Config.baseConfiguration(oldCondition || ''),
TextEditor.Config.closeBrackets.instance(),
TextEditor.Config.autocompletion.instance(),
CodeMirror.EditorView.lineWrapping,
TextEditor.Config.showCompletionHint,
TextEditor.Config.conservativeCompletion,
CodeMirror.javascript.javascriptLanguage.data.of({
autocomplete: (context: CodeMirror.CompletionContext) => this.#editorHistory.historyCompletions(context),
}),
CodeMirror.autocompletion(),
TextEditor.JavaScript.argumentHints(),
];
this.onFinish = onFinish;
this.finished = false;
this.element.tabIndex = -1;
this.element.classList.add('sources-edit-breakpoint-dialog');
this.element.setAttribute('jslog', `${VisualLogging.dialog('edit-breakpoint')}`);
const header = this.contentElement.createChild('div', 'dialog-header');
const toolbar = header.createChild('devtools-toolbar', 'source-frame-breakpoint-toolbar');
toolbar.appendText(`Line ${editorLineNumber + 1}:`);
this.typeSelector = new UI.Toolbar.ToolbarComboBox(
this.onTypeChanged.bind(this), i18nString(UIStrings.breakpointType), undefined, 'type');
this.typeSelector.createOption(
i18nString(UIStrings.breakpoint), SDK.DebuggerModel.BreakpointType.REGULAR_BREAKPOINT);
const conditionalOption = this.typeSelector.createOption(
i18nString(UIStrings.conditionalBreakpoint), SDK.DebuggerModel.BreakpointType.CONDITIONAL_BREAKPOINT);
const logpointOption =
this.typeSelector.createOption(i18nString(UIStrings.logpoint), SDK.DebuggerModel.BreakpointType.LOGPOINT);
this.typeSelector.select(isLogpoint ? logpointOption : conditionalOption);
toolbar.appendToolbarItem(this.typeSelector);
const content = oldCondition || '';
const finishIfComplete = (view: CodeMirror.EditorView): boolean => {
void TextEditor.JavaScript.isExpressionComplete(view.state.doc.toString()).then(complete => {
if (complete) {
this.finishEditing(true, this.editor.state.doc.toString());
} else {
CodeMirror.insertNewlineAndIndent(view);
}
});
return true;
};
const keymap = [
{key: 'ArrowUp', run: () => this.#editorHistory.moveHistory(Direction.BACKWARD)},
{key: 'ArrowDown', run: () => this.#editorHistory.moveHistory(Direction.FORWARD)},
{mac: 'Ctrl-p', run: () => this.#editorHistory.moveHistory(Direction.BACKWARD, true)},
{mac: 'Ctrl-n', run: () => this.#editorHistory.moveHistory(Direction.FORWARD, true)},
{
key: 'Mod-Enter',
run: finishIfComplete,
},
{
key: 'Enter',
run: finishIfComplete,
},
{
key: 'Shift-Enter',
run: CodeMirror.insertNewlineAndIndent,
},
{
key: 'Escape',
run: () => {
this.finishEditing(false, '');
return true;
},
},
];
this.placeholderCompartment = new CodeMirror.Compartment();
const editorWrapper = this.contentElement.appendChild(document.createElement('div'));
editorWrapper.classList.add('condition-editor');
editorWrapper.setAttribute('jslog', `${VisualLogging.textField().track({change: true})}`);
this.editor = new TextEditor.TextEditor.TextEditor(CodeMirror.EditorState.create({
doc: content,
selection: {anchor: 0, head: content.length},
extensions: [
this.placeholderCompartment.of(this.getPlaceholder()),
CodeMirror.keymap.of(keymap),
editorConfig,
],
}));
editorWrapper.appendChild(this.editor);
const closeIcon = IconButton.Icon.create('cross');
closeIcon.title = i18nString(UIStrings.closeDialog);
closeIcon.setAttribute('jslog', `${VisualLogging.close().track({click: true})}`);
closeIcon.onclick = () => this.finishEditing(true, this.editor.state.doc.toString());
header.appendChild(closeIcon);
this.#history = new TextEditor.AutocompleteHistory.AutocompleteHistory(
Common.Settings.Settings.instance().createLocalSetting('breakpoint-condition-history', []));
this.#editorHistory = new TextEditor.TextEditorHistory.TextEditorHistory(this.editor, this.#history);
const linkWrapper = this.contentElement.appendChild(document.createElement('div'));
linkWrapper.classList.add('link-wrapper');
const link = UI.Fragment.html`<x-link class="link devtools-link" tabindex="0" href="https://goo.gle/devtools-loc"
jslog="${VisualLogging.link('learn-more')}">${
i18nString(UIStrings.learnMoreOnBreakpointTypes)}</x-link>` as UI.XLink.XLink;
const linkIcon = IconButton.Icon.create('open-externally', 'link-icon');
link.prepend(linkIcon);
linkWrapper.appendChild(link);
this.updateTooltip();
}
saveAndFinish(): void {
this.finishEditing(true, this.editor.state.doc.toString());
}
focusEditor(): void {
this.editor.editor.focus();
}
private onTypeChanged(): void {
if (this.breakpointType === SDK.DebuggerModel.BreakpointType.REGULAR_BREAKPOINT) {
this.finishEditing(true, '');
return;
}
this.focusEditor();
this.editor.dispatch({effects: this.placeholderCompartment.reconfigure(this.getPlaceholder())});
this.updateTooltip();
}
private get breakpointType(): string|null {
const option = this.typeSelector.selectedOption();
return option ? option.value : null;
}
private getPlaceholder(): CodeMirror.Extension {
const type = this.breakpointType;
if (type === SDK.DebuggerModel.BreakpointType.CONDITIONAL_BREAKPOINT) {
return CodeMirror.placeholder(i18nString(UIStrings.expressionToCheckBeforePausingEg));
}
if (type === SDK.DebuggerModel.BreakpointType.LOGPOINT) {
return CodeMirror.placeholder(i18nString(UIStrings.logMessageEgXIsX));
}
return [];
}
private updateTooltip(): void {
const type = this.breakpointType;
if (type === SDK.DebuggerModel.BreakpointType.CONDITIONAL_BREAKPOINT) {
UI.Tooltip.Tooltip.install((this.typeSelector.element), i18nString(UIStrings.pauseOnlyWhenTheConditionIsTrue));
} else if (type === SDK.DebuggerModel.BreakpointType.LOGPOINT) {
UI.Tooltip.Tooltip.install((this.typeSelector.element), i18nString(UIStrings.logAMessageToConsoleDoNotBreak));
}
}
finishEditing(committed: boolean, condition: string): void {
if (this.finished) {
return;
}
this.finished = true;
this.editor.remove();
this.#history.pushHistoryItem(condition);
const isLogpoint = this.breakpointType === SDK.DebuggerModel.BreakpointType.LOGPOINT;
this.onFinish({committed, condition: condition as BreakpointManager.BreakpointManager.UserCondition, isLogpoint});
}
get editorForTest(): TextEditor.TextEditor.TextEditor {
return this.editor;
}
}