| // 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()); |
| } |
| } |
| } |