Path: blob/main/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as dom from '../../../../base/browser/dom.js';6import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';7import { Button } from '../../../../base/browser/ui/button/button.js';8import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';9import { ISelectOptionItem, SelectBox } from '../../../../base/browser/ui/selectBox/selectBox.js';10import { CancellationToken } from '../../../../base/common/cancellation.js';11import { onUnexpectedError } from '../../../../base/common/errors.js';12import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';13import * as lifecycle from '../../../../base/common/lifecycle.js';14import { URI as uri } from '../../../../base/common/uri.js';15import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js';16import { EditorCommand, ServicesAccessor, registerEditorCommand } from '../../../../editor/browser/editorExtensions.js';17import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';18import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';19import { EditorOption, IEditorOptions } from '../../../../editor/common/config/editorOptions.js';20import { IPosition, Position } from '../../../../editor/common/core/position.js';21import { IRange, Range } from '../../../../editor/common/core/range.js';22import { IDecorationOptions } from '../../../../editor/common/editorCommon.js';23import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';24import { CompletionContext, CompletionItemKind, CompletionList } from '../../../../editor/common/languages.js';25import { PLAINTEXT_LANGUAGE_ID } from '../../../../editor/common/languages/modesRegistry.js';26import { ITextModel } from '../../../../editor/common/model.js';27import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';28import { IModelService } from '../../../../editor/common/services/model.js';29import { ITextModelService } from '../../../../editor/common/services/resolverService.js';30import { CompletionOptions, provideSuggestionItems } from '../../../../editor/contrib/suggest/browser/suggest.js';31import { ZoneWidget } from '../../../../editor/contrib/zoneWidget/browser/zoneWidget.js';32import * as nls from '../../../../nls.js';33import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';34import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js';35import { IHoverService } from '../../../../platform/hover/browser/hover.js';36import { IInstantiationService, createDecorator } from '../../../../platform/instantiation/common/instantiation.js';37import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';38import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';39import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';40import { ILabelService } from '../../../../platform/label/common/label.js';41import { defaultButtonStyles, defaultSelectBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';42import { editorForeground } from '../../../../platform/theme/common/colorRegistry.js';43import { IColorTheme, IThemeService } from '../../../../platform/theme/common/themeService.js';44import { hasNativeContextMenu } from '../../../../platform/window/common/window.js';45import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js';46import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, CONTEXT_IN_BREAKPOINT_WIDGET, BreakpointWidgetContext as Context, DEBUG_SCHEME, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugService } from '../common/debug.js';47import './media/breakpointWidget.css';4849const $ = dom.$;50const IPrivateBreakpointWidgetService = createDecorator<IPrivateBreakpointWidgetService>('privateBreakpointWidgetService');51interface IPrivateBreakpointWidgetService {52readonly _serviceBrand: undefined;53close(success: boolean): void;54}55const DECORATION_KEY = 'breakpointwidgetdecoration';5657function isPositionInCurlyBracketBlock(input: IActiveCodeEditor): boolean {58const model = input.getModel();59const bracketPairs = model.bracketPairs.getBracketPairsInRange(Range.fromPositions(input.getPosition()));60return bracketPairs.some(p => p.openingBracketInfo.bracketText === '{');61}6263function createDecorations(theme: IColorTheme, placeHolder: string): IDecorationOptions[] {64const transparentForeground = theme.getColor(editorForeground)?.transparent(0.4);65return [{66range: {67startLineNumber: 0,68endLineNumber: 0,69startColumn: 0,70endColumn: 171},72renderOptions: {73after: {74contentText: placeHolder,75color: transparentForeground ? transparentForeground.toString() : undefined76}77}78}];79}8081export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWidgetService {82declare readonly _serviceBrand: undefined;8384private selectContainer!: HTMLElement;85private inputContainer!: HTMLElement;86private selectBreakpointContainer!: HTMLElement;87private input!: IActiveCodeEditor;88private selectBreakpointBox!: SelectBox;89private selectModeBox?: SelectBox;90private store: lifecycle.DisposableStore;91private conditionInput = '';92private hitCountInput = '';93private logMessageInput = '';94private modeInput?: DebugProtocol.BreakpointMode;95private breakpoint: IBreakpoint | undefined;96private context: Context;97private heightInPx: number | undefined;98private triggeredByBreakpointInput: IBreakpoint | undefined;99100constructor(editor: ICodeEditor, private lineNumber: number, private column: number | undefined, context: Context | undefined,101@IContextViewService private readonly contextViewService: IContextViewService,102@IDebugService private readonly debugService: IDebugService,103@IThemeService private readonly themeService: IThemeService,104@IInstantiationService private readonly instantiationService: IInstantiationService,105@IModelService private readonly modelService: IModelService,106@ICodeEditorService private readonly codeEditorService: ICodeEditorService,107@IConfigurationService private readonly _configurationService: IConfigurationService,108@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,109@IKeybindingService private readonly keybindingService: IKeybindingService,110@ILabelService private readonly labelService: ILabelService,111@ITextModelService private readonly textModelService: ITextModelService,112@IHoverService private readonly hoverService: IHoverService113) {114super(editor, { showFrame: true, showArrow: false, frameWidth: 1, isAccessible: true });115116this.store = new lifecycle.DisposableStore();117const model = this.editor.getModel();118if (model) {119const uri = model.uri;120const breakpoints = this.debugService.getModel().getBreakpoints({ lineNumber: this.lineNumber, column: this.column, uri });121this.breakpoint = breakpoints.length ? breakpoints[0] : undefined;122}123124if (context === undefined) {125if (this.breakpoint && !this.breakpoint.condition && !this.breakpoint.hitCondition && this.breakpoint.logMessage) {126this.context = Context.LOG_MESSAGE;127} else if (this.breakpoint && !this.breakpoint.condition && this.breakpoint.hitCondition) {128this.context = Context.HIT_COUNT;129} else if (this.breakpoint && this.breakpoint.triggeredBy) {130this.context = Context.TRIGGER_POINT;131} else {132this.context = Context.CONDITION;133}134} else {135this.context = context;136}137138this.store.add(this.debugService.getModel().onDidChangeBreakpoints(e => {139if (this.breakpoint && e && e.removed && e.removed.indexOf(this.breakpoint) >= 0) {140this.dispose();141}142}));143this.codeEditorService.registerDecorationType('breakpoint-widget', DECORATION_KEY, {});144145this.create();146}147148private get placeholder(): string {149const acceptString = this.keybindingService.lookupKeybinding(AcceptBreakpointWidgetInputAction.ID)?.getLabel() || 'Enter';150const closeString = this.keybindingService.lookupKeybinding(CloseBreakpointWidgetCommand.ID)?.getLabel() || 'Escape';151switch (this.context) {152case Context.LOG_MESSAGE:153return nls.localize('breakpointWidgetLogMessagePlaceholder', "Message to log when breakpoint is hit. Expressions within {} are interpolated. '{0}' to accept, '{1}' to cancel.", acceptString, closeString);154case Context.HIT_COUNT:155return nls.localize('breakpointWidgetHitCountPlaceholder', "Break when hit count condition is met. '{0}' to accept, '{1}' to cancel.", acceptString, closeString);156default:157return nls.localize('breakpointWidgetExpressionPlaceholder', "Break when expression evaluates to true. '{0}' to accept, '{1}' to cancel.", acceptString, closeString);158}159}160161private getInputValue(breakpoint: IBreakpoint | undefined): string {162switch (this.context) {163case Context.LOG_MESSAGE:164return breakpoint && breakpoint.logMessage ? breakpoint.logMessage : this.logMessageInput;165case Context.HIT_COUNT:166return breakpoint && breakpoint.hitCondition ? breakpoint.hitCondition : this.hitCountInput;167default:168return breakpoint && breakpoint.condition ? breakpoint.condition : this.conditionInput;169}170}171172private rememberInput(): void {173if (this.context !== Context.TRIGGER_POINT) {174const value = this.input.getModel().getValue();175switch (this.context) {176case Context.LOG_MESSAGE:177this.logMessageInput = value;178break;179case Context.HIT_COUNT:180this.hitCountInput = value;181break;182default:183this.conditionInput = value;184}185}186}187188private setInputMode(): void {189if (this.editor.hasModel()) {190// Use plaintext language for log messages, otherwise respect underlying editor language #125619191const languageId = this.context === Context.LOG_MESSAGE ? PLAINTEXT_LANGUAGE_ID : this.editor.getModel().getLanguageId();192this.input.getModel().setLanguage(languageId);193}194}195196override show(rangeOrPos: IRange | IPosition): void {197const lineNum = this.input.getModel().getLineCount();198super.show(rangeOrPos, lineNum + 1);199}200201fitHeightToContent(): void {202const lineNum = this.input.getModel().getLineCount();203this._relayout(lineNum + 1);204}205206protected _fillContainer(container: HTMLElement): void {207this.setCssClass('breakpoint-widget');208const selectBox = this.store.add(new SelectBox([209{ text: nls.localize('expression', "Expression") },210{ text: nls.localize('hitCount', "Hit Count") },211{ text: nls.localize('logMessage', "Log Message") },212{ text: nls.localize('triggeredBy', "Wait for Breakpoint") },213] satisfies ISelectOptionItem[], this.context, this.contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('breakpointType', 'Breakpoint Type'), useCustomDrawn: !hasNativeContextMenu(this._configurationService) }));214this.selectContainer = $('.breakpoint-select-container');215selectBox.render(dom.append(container, this.selectContainer));216selectBox.onDidSelect(e => {217this.rememberInput();218this.context = e.index;219this.updateContextInput();220});221222this.createModesInput(container);223224this.inputContainer = $('.inputContainer');225this.store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.inputContainer, this.placeholder));226this.createBreakpointInput(dom.append(container, this.inputContainer));227228this.input.getModel().setValue(this.getInputValue(this.breakpoint));229this.store.add(this.input.getModel().onDidChangeContent(() => {230this.fitHeightToContent();231}));232this.input.setPosition({ lineNumber: 1, column: this.input.getModel().getLineMaxColumn(1) });233234this.createTriggerBreakpointInput(container);235236this.updateContextInput();237// Due to an electron bug we have to do the timeout, otherwise we do not get focus238setTimeout(() => this.focusInput(), 150);239}240241private createModesInput(container: HTMLElement) {242const modes = this.debugService.getModel().getBreakpointModes('source');243if (modes.length <= 1) {244return;245}246247const sb = this.selectModeBox = new SelectBox(248[249{ text: nls.localize('bpMode', 'Mode'), isDisabled: true },250...modes.map(mode => ({ text: mode.label, description: mode.description })),251],252modes.findIndex(m => m.mode === this.breakpoint?.mode) + 1,253this.contextViewService,254defaultSelectBoxStyles,255{ useCustomDrawn: !hasNativeContextMenu(this._configurationService) }256);257this.store.add(sb);258this.store.add(sb.onDidSelect(e => {259this.modeInput = modes[e.index - 1];260}));261262const modeWrapper = $('.select-mode-container');263const selectionWrapper = $('.select-box-container');264dom.append(modeWrapper, selectionWrapper);265sb.render(selectionWrapper);266dom.append(container, modeWrapper);267}268269private createTriggerBreakpointInput(container: HTMLElement) {270const breakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp !== this.breakpoint && !bp.logMessage);271const breakpointOptions: ISelectOptionItem[] = [272{ text: nls.localize('noTriggerByBreakpoint', 'None'), isDisabled: true },273...breakpoints.map(bp => ({274text: `${this.labelService.getUriLabel(bp.uri, { relative: true })}: ${bp.lineNumber}`,275description: nls.localize('triggerByLoading', 'Loading...')276})),277];278279const index = breakpoints.findIndex((bp) => this.breakpoint?.triggeredBy === bp.getId());280for (const [i, bp] of breakpoints.entries()) {281this.textModelService.createModelReference(bp.uri).then(ref => {282try {283breakpointOptions[i + 1].description = ref.object.textEditorModel.getLineContent(bp.lineNumber).trim();284} finally {285ref.dispose();286}287}).catch(() => {288breakpointOptions[i + 1].description = nls.localize('noBpSource', 'Could not load source.');289});290}291292const selectBreakpointBox = this.selectBreakpointBox = new SelectBox(breakpointOptions, index + 1, this.contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('selectBreakpoint', 'Select breakpoint'), useCustomDrawn: !hasNativeContextMenu(this._configurationService) });293selectBreakpointBox.onDidSelect(e => {294if (e.index === 0) {295this.triggeredByBreakpointInput = undefined;296} else {297this.triggeredByBreakpointInput = breakpoints[e.index - 1];298}299});300this.store.add(selectBreakpointBox);301this.selectBreakpointContainer = $('.select-breakpoint-container');302this.store.add(dom.addDisposableListener(this.selectBreakpointContainer, dom.EventType.KEY_DOWN, e => {303const event = new StandardKeyboardEvent(e);304if (event.equals(KeyCode.Escape)) {305this.close(false);306}307}));308309const selectionWrapper = $('.select-box-container');310dom.append(this.selectBreakpointContainer, selectionWrapper);311selectBreakpointBox.render(selectionWrapper);312313dom.append(container, this.selectBreakpointContainer);314315const closeButton = new Button(this.selectBreakpointContainer, defaultButtonStyles);316closeButton.label = nls.localize('ok', "OK");317this.store.add(closeButton.onDidClick(() => this.close(true)));318this.store.add(closeButton);319}320321private updateContextInput() {322if (this.context === Context.TRIGGER_POINT) {323this.inputContainer.hidden = true;324this.selectBreakpointContainer.hidden = false;325} else {326this.inputContainer.hidden = false;327this.selectBreakpointContainer.hidden = true;328this.setInputMode();329const value = this.getInputValue(this.breakpoint);330this.input.getModel().setValue(value);331this.focusInput();332}333}334335protected override _doLayout(heightInPixel: number, widthInPixel: number): void {336this.heightInPx = heightInPixel;337this.input.layout({ height: heightInPixel, width: widthInPixel - 113 });338this.centerInputVertically();339}340341protected override _onWidth(widthInPixel: number): void {342if (typeof this.heightInPx === 'number') {343this._doLayout(this.heightInPx, widthInPixel);344}345}346347private createBreakpointInput(container: HTMLElement): void {348const scopedInstatiationService = this.instantiationService.createChild(new ServiceCollection(349[IPrivateBreakpointWidgetService, this]350));351this.store.add(scopedInstatiationService);352353const options = this.createEditorOptions();354const codeEditorWidgetOptions = getSimpleCodeEditorWidgetOptions();355this.input = <IActiveCodeEditor>scopedInstatiationService.createInstance(CodeEditorWidget, container, options, codeEditorWidgetOptions);356357CONTEXT_IN_BREAKPOINT_WIDGET.bindTo(this.input.contextKeyService).set(true);358const model = this.modelService.createModel('', null, uri.parse(`${DEBUG_SCHEME}:${this.editor.getId()}:breakpointinput`), true);359if (this.editor.hasModel()) {360model.setLanguage(this.editor.getModel().getLanguageId());361}362this.input.setModel(model);363this.setInputMode();364this.store.add(model);365const setDecorations = () => {366const value = this.input.getModel().getValue();367const decorations = !!value ? [] : createDecorations(this.themeService.getColorTheme(), this.placeholder);368this.input.setDecorationsByType('breakpoint-widget', DECORATION_KEY, decorations);369};370this.input.getModel().onDidChangeContent(() => setDecorations());371this.themeService.onDidColorThemeChange(() => setDecorations());372373this.store.add(this.languageFeaturesService.completionProvider.register({ scheme: DEBUG_SCHEME, hasAccessToAllModels: true }, {374_debugDisplayName: 'breakpointWidget',375provideCompletionItems: (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken): Promise<CompletionList> => {376let suggestionsPromise: Promise<CompletionList>;377const underlyingModel = this.editor.getModel();378if (underlyingModel && (this.context === Context.CONDITION || (this.context === Context.LOG_MESSAGE && isPositionInCurlyBracketBlock(this.input)))) {379suggestionsPromise = provideSuggestionItems(this.languageFeaturesService.completionProvider, underlyingModel, new Position(this.lineNumber, 1), new CompletionOptions(undefined, new Set<CompletionItemKind>().add(CompletionItemKind.Snippet)), _context, token).then(suggestions => {380381let overwriteBefore = 0;382if (this.context === Context.CONDITION) {383overwriteBefore = position.column - 1;384} else {385// Inside the currly brackets, need to count how many useful characters are behind the position so they would all be taken into account386const value = this.input.getModel().getValue();387while ((position.column - 2 - overwriteBefore >= 0) && value[position.column - 2 - overwriteBefore] !== '{' && value[position.column - 2 - overwriteBefore] !== ' ') {388overwriteBefore++;389}390}391392return {393suggestions: suggestions.items.map(s => {394s.completion.range = Range.fromPositions(position.delta(0, -overwriteBefore), position);395return s.completion;396})397};398});399} else {400suggestionsPromise = Promise.resolve({ suggestions: [] });401}402403return suggestionsPromise;404}405}));406407this.store.add(this._configurationService.onDidChangeConfiguration((e) => {408if (e.affectsConfiguration('editor.fontSize') || e.affectsConfiguration('editor.lineHeight')) {409this.input.updateOptions(this.createEditorOptions());410this.centerInputVertically();411}412}));413}414415private createEditorOptions(): IEditorOptions {416const editorConfig = this._configurationService.getValue<IEditorOptions>('editor');417const options = getSimpleEditorOptions(this._configurationService);418options.fontSize = editorConfig.fontSize;419options.fontFamily = editorConfig.fontFamily;420options.lineHeight = editorConfig.lineHeight;421options.fontLigatures = editorConfig.fontLigatures;422options.ariaLabel = this.placeholder;423return options;424}425426private centerInputVertically() {427if (this.container && typeof this.heightInPx === 'number') {428const lineHeight = this.input.getOption(EditorOption.lineHeight);429const lineNum = this.input.getModel().getLineCount();430const newTopMargin = (this.heightInPx - lineNum * lineHeight) / 2;431this.inputContainer.style.marginTop = newTopMargin + 'px';432}433}434435close(success: boolean): void {436if (success) {437// if there is already a breakpoint on this location - remove it.438439let condition: string | undefined = undefined;440let hitCondition: string | undefined = undefined;441let logMessage: string | undefined = undefined;442let triggeredBy: string | undefined = undefined;443let mode: string | undefined = undefined;444let modeLabel: string | undefined = undefined;445446this.rememberInput();447448if (this.conditionInput || this.context === Context.CONDITION) {449condition = this.conditionInput;450}451if (this.hitCountInput || this.context === Context.HIT_COUNT) {452hitCondition = this.hitCountInput;453}454if (this.logMessageInput || this.context === Context.LOG_MESSAGE) {455logMessage = this.logMessageInput;456}457if (this.selectModeBox) {458mode = this.modeInput?.mode;459modeLabel = this.modeInput?.label;460}461if (this.context === Context.TRIGGER_POINT) {462// currently, trigger points don't support additional conditions:463condition = undefined;464hitCondition = undefined;465logMessage = undefined;466triggeredBy = this.triggeredByBreakpointInput?.getId();467}468469if (this.breakpoint) {470const data = new Map<string, IBreakpointUpdateData>();471data.set(this.breakpoint.getId(), {472condition,473hitCondition,474logMessage,475triggeredBy,476mode,477modeLabel,478});479this.debugService.updateBreakpoints(this.breakpoint.originalUri, data, false).then(undefined, onUnexpectedError);480} else {481const model = this.editor.getModel();482if (model) {483this.debugService.addBreakpoints(model.uri, [{484lineNumber: this.lineNumber,485column: this.column,486enabled: true,487condition,488hitCondition,489logMessage,490triggeredBy,491mode,492modeLabel,493}]);494}495}496}497498this.dispose();499}500501private focusInput() {502if (this.context === Context.TRIGGER_POINT) {503this.selectBreakpointBox.focus();504} else {505this.input.focus();506}507}508509override dispose(): void {510super.dispose();511this.input.dispose();512lifecycle.dispose(this.store);513setTimeout(() => this.editor.focus(), 0);514}515}516517class AcceptBreakpointWidgetInputAction extends EditorCommand {518static ID = 'breakpointWidget.action.acceptInput';519constructor() {520super({521id: AcceptBreakpointWidgetInputAction.ID,522precondition: CONTEXT_BREAKPOINT_WIDGET_VISIBLE,523kbOpts: {524kbExpr: CONTEXT_IN_BREAKPOINT_WIDGET,525primary: KeyCode.Enter,526weight: KeybindingWeight.EditorContrib527}528});529}530531runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void {532accessor.get(IPrivateBreakpointWidgetService).close(true);533}534}535536class CloseBreakpointWidgetCommand extends EditorCommand {537static ID = 'closeBreakpointWidget';538constructor() {539super({540id: CloseBreakpointWidgetCommand.ID,541precondition: CONTEXT_BREAKPOINT_WIDGET_VISIBLE,542kbOpts: {543kbExpr: EditorContextKeys.textInputFocus,544primary: KeyCode.Escape,545secondary: [KeyMod.Shift | KeyCode.Escape],546weight: KeybindingWeight.EditorContrib547}548});549}550551runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {552const debugContribution = editor.getContribution<IBreakpointEditorContribution>(BREAKPOINT_EDITOR_CONTRIBUTION_ID);553if (debugContribution) {554// if focus is in outer editor we need to use the debug contribution to close555return debugContribution.closeBreakpointWidget();556}557558accessor.get(IPrivateBreakpointWidgetService).close(false);559}560}561562registerEditorCommand(new AcceptBreakpointWidgetInputAction());563registerEditorCommand(new CloseBreakpointWidgetCommand());564565566