mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-07 04:26:32 +08:00
Currently we don't have a good solution to provide sensible completions for the breakpoint editor, so either the developer ends up with only completions from the global object (debugger isn't paused), or the developer ends up with completions from the currently paused scope, while trying to set a conditional breakpoint or logpoint in a completely different function. Ideally we'd perform some kind of syntactic analysis here, and also pay special attention (and warn developers about potential issues) in the case of sourcemapped code. For now, the autocompletion is primarily an annoyance, and so it's better to disable it for now in this case, until we have a viable solution that doesn't get in the way. Fixed: chromium:1312374 Bug: chromium:1276960 Change-Id: I863ed80dd7cd501c5faa9e88acd1453f0639aece Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3563074 Auto-Submit: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Jaroslav Sevcik <jarin@chromium.org> Commit-Queue: Jaroslav Sevcik <jarin@chromium.org>
234 lines
8.1 KiB
TypeScript
234 lines
8.1 KiB
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.
|
|
|
|
import * as i18n from '../../core/i18n/i18n.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 breakpointEditDialogStyles from './breakpointEditDialog.css.js';
|
|
|
|
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 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 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',
|
|
};
|
|
const str_ = i18n.i18n.registerUIStrings('panels/sources/BreakpointEditDialog.ts', UIStrings);
|
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
|
|
export class BreakpointEditDialog extends UI.Widget.Widget {
|
|
private readonly onFinish: (arg0: {
|
|
committed: boolean,
|
|
condition: string,
|
|
}) => Promise<void>;
|
|
private finished: boolean;
|
|
private editor: TextEditor.TextEditor.TextEditor;
|
|
private isLogpoint: boolean;
|
|
private readonly typeSelector: UI.Toolbar.ToolbarComboBox;
|
|
private placeholderCompartment: CodeMirror.Compartment;
|
|
|
|
constructor(
|
|
editorLineNumber: number,
|
|
oldCondition: string,
|
|
preferLogpoint: boolean,
|
|
onFinish: (arg0: {committed: boolean, condition: string}) => Promise<void>,
|
|
) {
|
|
super(true);
|
|
|
|
const editorConfig = [
|
|
CodeMirror.javascript.javascriptLanguage,
|
|
TextEditor.Config.baseConfiguration(oldCondition || ''),
|
|
TextEditor.Config.closeBrackets,
|
|
TextEditor.Config.autocompletion,
|
|
CodeMirror.EditorView.lineWrapping,
|
|
TextEditor.Config.showCompletionHint,
|
|
TextEditor.JavaScript.argumentHints(),
|
|
];
|
|
|
|
this.onFinish = onFinish;
|
|
this.finished = false;
|
|
this.element.tabIndex = -1;
|
|
|
|
const logpointPrefix = LogpointPrefix;
|
|
const logpointSuffix = LogpointSuffix;
|
|
this.isLogpoint = oldCondition.startsWith(logpointPrefix) && oldCondition.endsWith(logpointSuffix);
|
|
if (this.isLogpoint) {
|
|
oldCondition = oldCondition.substring(logpointPrefix.length, oldCondition.length - logpointSuffix.length);
|
|
}
|
|
this.isLogpoint = this.isLogpoint || preferLogpoint;
|
|
|
|
this.element.classList.add('sources-edit-breakpoint-dialog');
|
|
const toolbar = new UI.Toolbar.Toolbar('source-frame-breakpoint-toolbar', this.contentElement);
|
|
toolbar.appendText(`Line ${editorLineNumber + 1}:`);
|
|
|
|
this.typeSelector =
|
|
new UI.Toolbar.ToolbarComboBox(this.onTypeChanged.bind(this), i18nString(UIStrings.breakpointType));
|
|
this.typeSelector.createOption(i18nString(UIStrings.breakpoint), BreakpointType.Breakpoint);
|
|
const conditionalOption =
|
|
this.typeSelector.createOption(i18nString(UIStrings.conditionalBreakpoint), BreakpointType.Conditional);
|
|
const logpointOption = this.typeSelector.createOption(i18nString(UIStrings.logpoint), BreakpointType.Logpoint);
|
|
this.typeSelector.select(this.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): void => {
|
|
if (complete) {
|
|
this.finishEditing(true, this.editor.state.doc.toString());
|
|
} else {
|
|
CodeMirror.insertNewlineAndIndent(view);
|
|
}
|
|
});
|
|
return true;
|
|
};
|
|
const keymap = [
|
|
{
|
|
key: 'Mod-Enter',
|
|
run: finishIfComplete,
|
|
},
|
|
{
|
|
key: 'Enter',
|
|
run: finishIfComplete,
|
|
},
|
|
{
|
|
key: 'Shift-Enter',
|
|
run: CodeMirror.insertNewlineAndIndent,
|
|
},
|
|
{
|
|
key: 'Escape',
|
|
run: (): boolean => {
|
|
this.finishEditing(false, '');
|
|
return true;
|
|
},
|
|
},
|
|
];
|
|
|
|
this.placeholderCompartment = new CodeMirror.Compartment();
|
|
|
|
const editorWrapper = this.contentElement.appendChild(document.createElement('div'));
|
|
editorWrapper.classList.add('condition-editor');
|
|
|
|
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);
|
|
|
|
this.updateTooltip();
|
|
|
|
this.element.addEventListener('blur', event => {
|
|
if (!event.relatedTarget ||
|
|
(event.relatedTarget && !(event.relatedTarget as Node).isSelfOrDescendant(this.element))) {
|
|
this.finishEditing(true, this.editor.state.doc.toString());
|
|
}
|
|
}, true);
|
|
}
|
|
|
|
focusEditor(): void {
|
|
this.editor.editor.focus();
|
|
}
|
|
private static conditionForLogpoint(condition: string): string {
|
|
return `${LogpointPrefix}${condition}${LogpointSuffix}`;
|
|
}
|
|
|
|
private onTypeChanged(): void {
|
|
const type = this.breakpointType;
|
|
this.isLogpoint = type === BreakpointType.Logpoint;
|
|
if (type === BreakpointType.Breakpoint) {
|
|
this.finishEditing(true, '');
|
|
return;
|
|
}
|
|
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 === BreakpointType.Conditional) {
|
|
return CodeMirror.placeholder(i18nString(UIStrings.expressionToCheckBeforePausingEg));
|
|
}
|
|
if (type === BreakpointType.Logpoint) {
|
|
return CodeMirror.placeholder(i18nString(UIStrings.logMessageEgXIsX));
|
|
}
|
|
return [];
|
|
}
|
|
|
|
private updateTooltip(): void {
|
|
const type = this.breakpointType;
|
|
if (type === BreakpointType.Conditional) {
|
|
UI.Tooltip.Tooltip.install((this.typeSelector.element), i18nString(UIStrings.pauseOnlyWhenTheConditionIsTrue));
|
|
} else if (type === 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();
|
|
if (this.isLogpoint) {
|
|
condition = BreakpointEditDialog.conditionForLogpoint(condition);
|
|
}
|
|
void this.onFinish({committed, condition});
|
|
}
|
|
|
|
wasShown(): void {
|
|
super.wasShown();
|
|
this.registerCSSFiles([breakpointEditDialogStyles]);
|
|
}
|
|
}
|
|
|
|
export const LogpointPrefix = '/** DEVTOOLS_LOGPOINT */ console.log(';
|
|
export const LogpointSuffix = ')';
|
|
|
|
export const BreakpointType = {
|
|
Breakpoint: 'Breakpoint',
|
|
Conditional: 'Conditional',
|
|
Logpoint: 'Logpoint',
|
|
};
|