chrome-devtools-frontend
Version:
Chrome DevTools UI
330 lines (306 loc) • 12.6 kB
text/typescript
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-imperative-dom-api */
import '../../ui/kit/kit.js';
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 TextEditor from '../../ui/components/text_editor/text_editor.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import breakpointEditDialogStyles from './breakpointEditDialog.css.js';
const {ref} = Directives;
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;
}
interface ViewInput {
state: CodeMirror.EditorState;
breakpointType: SDK.DebuggerModel.BreakpointType.LOGPOINT|SDK.DebuggerModel.BreakpointType.CONDITIONAL_BREAKPOINT;
editorLineNumber: number;
onTypeChanged(breakpointType: SDK.DebuggerModel.BreakpointType): void;
saveAndFinish(): void;
}
interface ViewOutput {
editor: TextEditor.TextEditor.TextEditor|undefined;
}
type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
export const DEFAULT_VIEW: View = (input, output, target) => {
const editorRef = (e: Element|undefined): void => {
output.editor = e as TextEditor.TextEditor.TextEditor;
};
const onTypeChanged = (event: Event): void => {
if (event.target instanceof HTMLSelectElement && event.target.selectedOptions.length === 1) {
input.onTypeChanged(event.target.selectedOptions.item(0)?.value as SDK.DebuggerModel.BreakpointType);
}
output.editor?.focus();
};
// clang-format off
render(html`
<style>${breakpointEditDialogStyles}</style>
<div class=dialog-header>
<devtools-toolbar class=source-frame-breakpoint-toolbar>Line ${input.editorLineNumber + 1}:
<select
class=type-selector
title=${input.breakpointType === SDK.DebuggerModel.BreakpointType.LOGPOINT
? i18nString(UIStrings.logAMessageToConsoleDoNotBreak)
: i18nString(UIStrings.pauseOnlyWhenTheConditionIsTrue)}
aria-label=${i18nString(UIStrings.breakpointType)}
jslog=${VisualLogging.dropDown('type').track({change: true})}
@change=${onTypeChanged}>
<option value=${SDK.DebuggerModel.BreakpointType.REGULAR_BREAKPOINT}>
${i18nString(UIStrings.breakpoint)}
</option>
<option
value=${SDK.DebuggerModel.BreakpointType.CONDITIONAL_BREAKPOINT}
.selected=${input.breakpointType === SDK.DebuggerModel.BreakpointType.CONDITIONAL_BREAKPOINT}>
${i18nString(UIStrings.conditionalBreakpoint)}
</option>
<option
value=${SDK.DebuggerModel.BreakpointType.LOGPOINT}
.selected=${input.breakpointType === SDK.DebuggerModel.BreakpointType.LOGPOINT}>
${i18nString(UIStrings.logpoint)}
</option>
</select>
</devtools-toolbar>
<devtools-icon
name=cross
title=${i18nString(UIStrings.closeDialog)}
jslog=${VisualLogging.close().track({click: true})}
@click=${input.saveAndFinish}>
</devtools-icon>
</div>
<div class=condition-editor jslog=${VisualLogging.textField().track({change: true})}>
<devtools-text-editor
${ref(editorRef)}
autofocus
.state=${input.state}
@focus=${() => output.editor?.focus()}></devtools-text-editor>
</div>
<div class=link-wrapper>
<devtools-icon name=open-externally class=link-icon></devtools-icon>
<devtools-link class="devtools-link" href="https://goo.gle/devtools-loc"
jslogcontext="learn-more">${
i18nString(UIStrings.learnMoreOnBreakpointTypes)}</devtools-link>
</div>
`,
// clang-format on
target);
};
export class BreakpointEditDialog extends UI.Widget.Widget {
readonly #view: View;
readonly #history = new TextEditor.AutocompleteHistory.AutocompleteHistory(
Common.Settings.Settings.instance().createLocalSetting('breakpoint-condition-history', []));
#finished = false;
#editorLineNumber = 0;
#oldCondition = '';
#breakpointType: SDK.DebuggerModel.BreakpointType.LOGPOINT|SDK.DebuggerModel.BreakpointType.CONDITIONAL_BREAKPOINT =
SDK.DebuggerModel.BreakpointType.CONDITIONAL_BREAKPOINT;
#onFinish: (result: BreakpointEditDialogResult) => void = () => {};
#editor?: TextEditor.TextEditor.TextEditor;
#state?: CodeMirror.EditorState;
constructor(target?: HTMLElement, view = DEFAULT_VIEW) {
super({
jslog: `${VisualLogging.dialog('edit-breakpoint')}`,
useShadowDom: true,
delegatesFocus: true,
classes: ['sources-edit-breakpoint-dialog'],
});
this.#view = view;
this.element.tabIndex = -1;
}
get editorLineNumber(): number {
return this.#editorLineNumber;
}
set editorLineNumber(editorLineNumber: number) {
this.#editorLineNumber = editorLineNumber;
this.requestUpdate();
}
get oldCondition(): string {
return this.#oldCondition;
}
set oldCondition(oldCondition: string) {
this.#state = undefined;
this.#oldCondition = oldCondition;
this.requestUpdate();
}
get breakpointType(): SDK.DebuggerModel.BreakpointType {
return this.#breakpointType;
}
set breakpointType(
breakpointType: SDK.DebuggerModel.BreakpointType.LOGPOINT|
SDK.DebuggerModel.BreakpointType.CONDITIONAL_BREAKPOINT) {
this.#breakpointType = breakpointType;
this.requestUpdate();
}
get onFinish(): (result: BreakpointEditDialogResult) => void {
return this.#onFinish;
}
set onFinish(onFinish: (result: BreakpointEditDialogResult) => void) {
this.#onFinish = onFinish;
this.requestUpdate();
}
override performUpdate(): void {
const input: ViewInput = {
state: this.#getEditorState(),
breakpointType: this.#breakpointType,
editorLineNumber: this.#editorLineNumber,
onTypeChanged: type => this.#typeChanged(type),
saveAndFinish: () => this.saveAndFinish(),
};
const that = this;
const output = {
get editor() {
return that.#editor;
},
set editor(editor) {
that.#editor = editor;
}
};
this.#view(input, output, this.contentElement);
}
#getEditorState(): CodeMirror.EditorState {
if (this.#state) {
return this.#state;
}
const getPlaceholder = (): CodeMirror.Extension => {
if (this.#breakpointType === SDK.DebuggerModel.BreakpointType.CONDITIONAL_BREAKPOINT) {
return CodeMirror.placeholder(i18nString(UIStrings.expressionToCheckBeforePausingEg));
}
if (this.#breakpointType === SDK.DebuggerModel.BreakpointType.LOGPOINT) {
return CodeMirror.placeholder(i18nString(UIStrings.logMessageEgXIsX));
}
return [];
};
const history = (): TextEditor.TextEditorHistory.TextEditorHistory|undefined =>
this.#editor && new TextEditor.TextEditorHistory.TextEditorHistory(this.#editor, this.#history);
const autocomplete = (context: CodeMirror.CompletionContext): CodeMirror.CompletionResult|null =>
history()?.historyCompletions(context) ?? null;
const historyBack = (force: boolean): boolean => history()?.moveHistory(Direction.BACKWARD, force) ?? false;
const historyForward = (force: boolean): boolean => history()?.moveHistory(Direction.FORWARD, force) ?? false;
const finishIfComplete = (view: CodeMirror.EditorView): boolean => {
void TextEditor.JavaScript.isExpressionComplete(view.state.doc.toString()).then(complete => {
if (complete) {
this.finishEditing(true, view.state.doc.toString());
} else {
CodeMirror.insertNewlineAndIndent(view);
}
});
return true;
};
const keymap = [
{key: 'ArrowUp', run: () => historyBack(false)},
{key: 'ArrowDown', run: () => historyForward(false)},
{mac: 'Ctrl-p', run: () => historyBack(true)},
{mac: 'Ctrl-n', run: () => historyForward(true)},
{key: 'Mod-Enter', run: finishIfComplete},
{key: 'Enter', run: finishIfComplete},
{key: 'Shift-Enter', run: CodeMirror.insertNewlineAndIndent},
{
key: 'Escape',
run: () => {
this.finishEditing(false, '');
return true;
}
},
];
const editorConfig = [
CodeMirror.javascript.javascriptLanguage,
TextEditor.Config.baseConfiguration(this.oldCondition),
TextEditor.Config.closeBrackets.instance(),
TextEditor.Config.autocompletion.instance(),
CodeMirror.EditorView.lineWrapping,
TextEditor.Config.showCompletionHint,
TextEditor.Config.conservativeCompletion,
CodeMirror.javascript.javascriptLanguage.data.of({autocomplete}),
CodeMirror.autocompletion(),
TextEditor.JavaScript.argumentHints(),
];
this.#state = CodeMirror.EditorState.create({
doc: this.oldCondition,
selection: {anchor: 0, head: this.oldCondition.length},
extensions: [
new CodeMirror.Compartment().of(getPlaceholder()),
CodeMirror.keymap.of(keymap),
editorConfig,
],
});
return this.#state;
}
#typeChanged(breakpointType: SDK.DebuggerModel.BreakpointType): void {
if (breakpointType === SDK.DebuggerModel.BreakpointType.REGULAR_BREAKPOINT) {
this.finishEditing(true, '');
return;
}
this.breakpointType = breakpointType;
this.requestUpdate();
}
finishEditing(committed: boolean, condition: string): void {
if (this.#finished) {
return;
}
this.#finished = true;
this.#history.pushHistoryItem(condition);
const isLogpoint = this.breakpointType === SDK.DebuggerModel.BreakpointType.LOGPOINT;
this.onFinish({committed, condition: condition as BreakpointManager.BreakpointManager.UserCondition, isLogpoint});
}
saveAndFinish(): void {
if (this.#editor) {
this.finishEditing(true, this.#editor.state.doc.toString());
}
}
}