From 4ace80eb6a18634b16f261e3f2c44cc917106aa8 Mon Sep 17 00:00:00 2001 From: isidor Date: Mon, 16 Sep 2019 16:58:38 +0200 Subject: [PATCH] debug: move all breakpoint editor decoration to breakpointEditorContribution --- .../browser/breakpointEditorContribution.ts | 136 ++++++- .../debug/browser/debug.contribution.ts | 4 +- .../browser/debugCallStackContribution.ts | 182 ++++++++++ .../debug/browser/debugEditorModelManager.ts | 334 ------------------ 4 files changed, 312 insertions(+), 344 deletions(-) create mode 100644 src/vs/workbench/contrib/debug/browser/debugCallStackContribution.ts delete mode 100644 src/vs/workbench/contrib/debug/browser/debugEditorModelManager.ts diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 3e56bc41f7d44..37109a1ba9fe5 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -8,20 +8,75 @@ import * as env from 'vs/base/common/platform'; import { URI as uri } from 'vs/base/common/uri'; import severity from 'vs/base/common/severity'; import { IAction, Action } from 'vs/base/common/actions'; +import { Range } from 'vs/editor/common/core/range'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness, ITextModel } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { RemoveBreakpointAction } from 'vs/workbench/contrib/debug/browser/debugActions'; -import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, BreakpointWidgetContext, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, BreakpointWidgetContext, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, IBreakpointUpdateData } from 'vs/workbench/contrib/debug/common/debug'; import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ContextSubMenu } from 'vs/base/browser/contextmenu'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { BreakpointWidget } from 'vs/workbench/contrib/debug/browser/breakpointWidget'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { getBreakpointMessageAndClassName } from 'vs/workbench/contrib/debug/browser/breakpointsView'; + +interface IBreakpointDecoration { + decorationId: string; + breakpointId: string; + range: Range; +} + +const breakpointHelperDecoration: IModelDecorationOptions = { + glyphMarginClassName: 'debug-breakpoint-hint', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges +}; + +function createBreakpointDecorations(model: ITextModel, breakpoints: ReadonlyArray, debugService: IDebugService): { range: Range; options: IModelDecorationOptions; }[] { + const result: { range: Range; options: IModelDecorationOptions; }[] = []; + breakpoints.forEach((breakpoint) => { + if (breakpoint.lineNumber <= model.getLineCount()) { + const column = model.getLineFirstNonWhitespaceColumn(breakpoint.lineNumber); + const range = model.validateRange( + breakpoint.column ? new Range(breakpoint.lineNumber, breakpoint.column, breakpoint.lineNumber, breakpoint.column + 1) + : new Range(breakpoint.lineNumber, column, breakpoint.lineNumber, column + 1) // Decoration has to have a width #20688 + ); + + result.push({ + options: getBreakpointDecorationOptions(model, breakpoint, debugService), + range + }); + } + }); + + return result; +} + +function getBreakpointDecorationOptions(model: ITextModel, breakpoint: IBreakpoint, debugService: IDebugService): IModelDecorationOptions { + const { className, message } = getBreakpointMessageAndClassName(debugService, breakpoint); + let glyphMarginHoverMessage: MarkdownString | undefined; + + if (message) { + if (breakpoint.condition || breakpoint.hitCondition) { + const modeId = model.getLanguageIdentifier().language; + glyphMarginHoverMessage = new MarkdownString().appendCodeblock(modeId, message); + } else { + glyphMarginHoverMessage = new MarkdownString().appendText(message); + } + } + + return { + glyphMarginClassName: className, + glyphMarginHoverMessage, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + beforeContentClassName: breakpoint.column ? `debug-breakpoint-column ${className}-column` : undefined + }; +} class BreakpointEditorContribution implements IBreakpointEditorContribution { @@ -29,6 +84,8 @@ class BreakpointEditorContribution implements IBreakpointEditorContribution { private breakpointWidget: BreakpointWidget | undefined; private breakpointWidgetVisible: IContextKey; private toDispose: IDisposable[] = []; + private ignoreDecorationsChangedEvent = false; + private breakpointDecorations: IBreakpointDecoration[] = []; constructor( private readonly editor: ICodeEditor, @@ -132,7 +189,10 @@ class BreakpointEditorContribution implements IBreakpointEditorContribution { this.toDispose.push(this.editor.onDidChangeModel(() => { this.closeBreakpointWidget(); + this.setDecorations(); })); + this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.setDecorations())); + this.toDispose.push(this.editor.onDidChangeModelDecorations(() => this.onModelDecorationsChanged())); } private getContextMenuActions(breakpoints: ReadonlyArray, uri: uri, lineNumber: number): Array { @@ -226,7 +286,7 @@ class BreakpointEditorContribution implements IBreakpointEditorContribution { const newDecoration: IModelDeltaDecoration[] = []; if (showBreakpointHintAtLineNumber !== -1) { newDecoration.push({ - options: BreakpointEditorContribution.BREAKPOINT_HELPER_DECORATION, + options: breakpointHelperDecoration, range: { startLineNumber: showBreakpointHintAtLineNumber, startColumn: 1, @@ -239,6 +299,70 @@ class BreakpointEditorContribution implements IBreakpointEditorContribution { this.breakpointHintDecoration = this.editor.deltaDecorations(this.breakpointHintDecoration, newDecoration); } + private setDecorations(): void { + if (!this.editor.hasModel()) { + return; + } + + const model = this.editor.getModel(); + const breakpoints = this.debugService.getModel().getBreakpoints({ uri: model.uri }); + const desiredDecorations = createBreakpointDecorations(model, breakpoints, this.debugService); + + try { + this.ignoreDecorationsChangedEvent = true; + const decorationIds = this.editor.deltaDecorations(this.breakpointDecorations.map(bpd => bpd.decorationId), desiredDecorations); + this.breakpointDecorations = decorationIds.map((decorationId, index) => ({ + decorationId, + breakpointId: breakpoints[index].getId(), + range: desiredDecorations[index].range + })); + } finally { + this.ignoreDecorationsChangedEvent = false; + } + } + + private async onModelDecorationsChanged(): Promise { + if (this.breakpointDecorations.length === 0 || this.ignoreDecorationsChangedEvent || !this.editor.hasModel()) { + // I have no decorations + return; + } + let somethingChanged = false; + const model = this.editor.getModel(); + this.breakpointDecorations.forEach(breakpointDecoration => { + if (somethingChanged) { + return; + } + const newBreakpointRange = model.getDecorationRange(breakpointDecoration.decorationId); + if (newBreakpointRange && (!breakpointDecoration.range.equalsRange(newBreakpointRange))) { + somethingChanged = true; + } + }); + if (!somethingChanged) { + // nothing to do, my decorations did not change. + return; + } + + const data = new Map(); + const breakpoints = this.debugService.getModel().getBreakpoints(); + for (let i = 0, len = this.breakpointDecorations.length; i < len; i++) { + const breakpointDecoration = this.breakpointDecorations[i]; + const decorationRange = model.getDecorationRange(breakpointDecoration.decorationId); + // check if the line got deleted. + if (decorationRange) { + const breakpoint = breakpoints.filter(bp => bp.getId() === breakpointDecoration.breakpointId).pop(); + // since we know it is collapsed, it cannot grow to multiple lines + if (breakpoint) { + data.set(breakpoint.getId(), { + lineNumber: decorationRange.startLineNumber, + column: breakpoint.column ? decorationRange.startColumn : undefined, + }); + } + } + } + + await this.debugService.updateBreakpoints(model.uri, data, true); + } + // breakpoint widget showBreakpointWidget(lineNumber: number, context?: BreakpointWidgetContext): void { if (this.breakpointWidget) { @@ -263,13 +387,9 @@ class BreakpointEditorContribution implements IBreakpointEditorContribution { if (this.breakpointWidget) { this.breakpointWidget.dispose(); } + this.editor.deltaDecorations(this.breakpointDecorations.map(bpd => bpd.decorationId), []); dispose(this.toDispose); } - - private static BREAKPOINT_HELPER_DECORATION: IModelDecorationOptions = { - glyphMarginClassName: 'debug-breakpoint-hint', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges - }; } registerEditorContribution(BreakpointEditorContribution); diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 252c52e2b3ad5..5e3c41d36e6c8 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -24,7 +24,6 @@ import { } from 'vs/workbench/contrib/debug/common/debug'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { DebugEditorModelManager } from 'vs/workbench/contrib/debug/browser/debugEditorModelManager'; import { StartAction, AddFunctionBreakpointAction, ConfigureAction, DisableAllBreakpointsAction, EnableAllBreakpointsAction, RemoveAllBreakpointsAction, RunAction, ReapplyBreakpointsAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; import * as service from 'vs/workbench/contrib/debug/browser/debugService'; @@ -49,6 +48,7 @@ import { VariablesView } from 'vs/workbench/contrib/debug/browser/variablesView' import { ClearReplAction, Repl } from 'vs/workbench/contrib/debug/browser/repl'; import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider'; import { registerAndGetAmdImageURL } from 'vs/base/common/amd'; +import { DebugCallStackContribution } from 'vs/workbench/contrib/debug/browser/debugCallStackContribution'; class OpenDebugViewletAction extends ShowViewletAction { public static readonly ID = VIEWLET_ID; @@ -120,7 +120,7 @@ const registry = Registry.as(WorkbenchActionRegistryEx registry.registerWorkbenchAction(new SyncActionDescriptor(OpenDebugPanelAction, OpenDebugPanelAction.ID, OpenDebugPanelAction.LABEL, openPanelKb), 'View: Debug Console', nls.localize('view', "View")); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenDebugViewletAction, OpenDebugViewletAction.ID, OpenDebugViewletAction.LABEL, openViewletKb), 'View: Show Debug', nls.localize('view', "View")); -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugEditorModelManager, LifecyclePhase.Restored); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugCallStackContribution, LifecyclePhase.Restored); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugToolBar, LifecyclePhase.Restored); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugContentProvider, LifecyclePhase.Eventually); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(StatusBarColorProvider, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/debug/browser/debugCallStackContribution.ts b/src/vs/workbench/contrib/debug/browser/debugCallStackContribution.ts new file mode 100644 index 0000000000000..68cf65a7931ec --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/debugCallStackContribution.ts @@ -0,0 +1,182 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Constants } from 'vs/editor/common/core/uint'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel, TrackedRangeStickiness, IModelDeltaDecoration, IModelDecorationOptions } from 'vs/editor/common/model'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { localize } from 'vs/nls'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; + +interface IDebugEditorModelData { + model: ITextModel; + currentStackDecorations: string[]; + topStackFrameRange: Range | undefined; +} + +const stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; + +export class DebugCallStackContribution implements IWorkbenchContribution { + private modelDataMap = new Map(); + private toDispose: IDisposable[] = []; + + constructor( + @IModelService private readonly modelService: IModelService, + @IDebugService private readonly debugService: IDebugService, + ) { + this.registerListeners(); + } + + private registerListeners(): void { + this.toDispose.push(this.modelService.onModelAdded(this.onModelAdded, this)); + this.modelService.getModels().forEach(model => this.onModelAdded(model)); + this.toDispose.push(this.modelService.onModelRemoved(this.onModelRemoved, this)); + + this.toDispose.push(this.debugService.getViewModel().onDidFocusStackFrame(() => this.onFocusStackFrame())); + this.toDispose.push(this.debugService.onDidChangeState(state => { + if (state === State.Inactive) { + this.modelDataMap.forEach(modelData => { + modelData.topStackFrameRange = undefined; + }); + } + })); + } + + private onModelAdded(model: ITextModel): void { + const modelUriStr = model.uri.toString(); + const currentStackDecorations = model.deltaDecorations([], this.createCallStackDecorations(modelUriStr)); + + this.modelDataMap.set(modelUriStr, { + model: model, + currentStackDecorations: currentStackDecorations, + topStackFrameRange: undefined + }); + } + + private onModelRemoved(model: ITextModel): void { + const modelUriStr = model.uri.toString(); + const data = this.modelDataMap.get(modelUriStr); + if (data) { + this.modelDataMap.delete(modelUriStr); + } + } + + private onFocusStackFrame(): void { + this.modelDataMap.forEach((modelData, uri) => { + modelData.currentStackDecorations = modelData.model.deltaDecorations(modelData.currentStackDecorations, this.createCallStackDecorations(uri)); + }); + } + + private createCallStackDecorations(modelUriStr: string): IModelDeltaDecoration[] { + const result: IModelDeltaDecoration[] = []; + const stackFrame = this.debugService.getViewModel().focusedStackFrame; + if (!stackFrame || stackFrame.source.uri.toString() !== modelUriStr) { + return result; + } + + // only show decorations for the currently focused thread. + const columnUntilEOLRange = new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn, stackFrame.range.startLineNumber, Constants.MAX_SAFE_SMALL_INTEGER); + const range = new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn, stackFrame.range.startLineNumber, stackFrame.range.startColumn + 1); + + // compute how to decorate the editor. Different decorations are used if this is a top stack frame, focused stack frame, + // an exception or a stack frame that did not change the line number (we only decorate the columns, not the whole line). + const callStack = stackFrame.thread.getCallStack(); + if (callStack && callStack.length && stackFrame === callStack[0]) { + result.push({ + options: DebugCallStackContribution.TOP_STACK_FRAME_MARGIN, + range + }); + + result.push({ + options: DebugCallStackContribution.TOP_STACK_FRAME_DECORATION, + range: columnUntilEOLRange + }); + + const modelData = this.modelDataMap.get(modelUriStr); + if (modelData) { + if (modelData.topStackFrameRange && modelData.topStackFrameRange.startLineNumber === stackFrame.range.startLineNumber && modelData.topStackFrameRange.startColumn !== stackFrame.range.startColumn) { + result.push({ + options: DebugCallStackContribution.TOP_STACK_FRAME_INLINE_DECORATION, + range: columnUntilEOLRange + }); + } + modelData.topStackFrameRange = columnUntilEOLRange; + } + } else { + result.push({ + options: DebugCallStackContribution.FOCUSED_STACK_FRAME_MARGIN, + range + }); + + result.push({ + options: DebugCallStackContribution.FOCUSED_STACK_FRAME_DECORATION, + range: columnUntilEOLRange + }); + } + + return result; + } + + // editor decorations + + static readonly STICKINESS = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; + // we need a separate decoration for glyph margin, since we do not want it on each line of a multi line statement. + private static TOP_STACK_FRAME_MARGIN: IModelDecorationOptions = { + glyphMarginClassName: 'debug-top-stack-frame', + stickiness + }; + + private static FOCUSED_STACK_FRAME_MARGIN: IModelDecorationOptions = { + glyphMarginClassName: 'debug-focused-stack-frame', + stickiness + }; + + private static TOP_STACK_FRAME_DECORATION: IModelDecorationOptions = { + isWholeLine: true, + inlineClassName: 'debug-remove-token-colors', + className: 'debug-top-stack-frame-line', + stickiness + }; + + private static TOP_STACK_FRAME_INLINE_DECORATION: IModelDecorationOptions = { + beforeContentClassName: 'debug-top-stack-frame-column' + }; + + private static FOCUSED_STACK_FRAME_DECORATION: IModelDecorationOptions = { + isWholeLine: true, + inlineClassName: 'debug-remove-token-colors', + className: 'debug-focused-stack-frame-line', + stickiness + }; + + dispose(): void { + this.modelDataMap.forEach(modelData => { + modelData.model.deltaDecorations(modelData.currentStackDecorations, []); + }); + this.toDispose = dispose(this.toDispose); + + this.modelDataMap.clear(); + } +} + +registerThemingParticipant((theme, collector) => { + const topStackFrame = theme.getColor(topStackFrameColor); + if (topStackFrame) { + collector.addRule(`.monaco-editor .view-overlays .debug-top-stack-frame-line { background: ${topStackFrame}; }`); + collector.addRule(`.monaco-editor .view-overlays .debug-top-stack-frame-line { background: ${topStackFrame}; }`); + } + + const focusedStackFrame = theme.getColor(focusedStackFrameColor); + if (focusedStackFrame) { + collector.addRule(`.monaco-editor .view-overlays .debug-focused-stack-frame-line { background: ${focusedStackFrame}; }`); + } +}); + +const topStackFrameColor = registerColor('editor.stackFrameHighlightBackground', { dark: '#ffff0033', light: '#ffff6673', hc: '#fff600' }, localize('topStackFrameLineHighlight', 'Background color for the highlight of line at the top stack frame position.')); +const focusedStackFrameColor = registerColor('editor.focusedStackFrameHighlightBackground', { dark: '#7abd7a4d', light: '#cee7ce73', hc: '#cee7ce' }, localize('focusedStackFrameLineHighlight', 'Background color for the highlight of line at focused stack frame position.')); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorModelManager.ts b/src/vs/workbench/contrib/debug/browser/debugEditorModelManager.ts deleted file mode 100644 index 84dd90e705604..0000000000000 --- a/src/vs/workbench/contrib/debug/browser/debugEditorModelManager.ts +++ /dev/null @@ -1,334 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as lifecycle from 'vs/base/common/lifecycle'; -import { Constants } from 'vs/editor/common/core/uint'; -import { Range } from 'vs/editor/common/core/range'; -import { ITextModel, TrackedRangeStickiness, IModelDeltaDecoration, IModelDecorationOptions } from 'vs/editor/common/model'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IDebugService, IBreakpoint, State, IBreakpointUpdateData } from 'vs/workbench/contrib/debug/common/debug'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { MarkdownString } from 'vs/base/common/htmlContent'; -import { getBreakpointMessageAndClassName } from 'vs/workbench/contrib/debug/browser/breakpointsView'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { registerColor } from 'vs/platform/theme/common/colorRegistry'; -import { localize } from 'vs/nls'; -import { onUnexpectedError } from 'vs/base/common/errors'; - -interface IBreakpointDecoration { - decorationId: string; - modelId: string; - range: Range; -} - -interface IDebugEditorModelData { - model: ITextModel; - toDispose: lifecycle.IDisposable[]; - breakpointDecorations: IBreakpointDecoration[]; - currentStackDecorations: string[]; - topStackFrameRange: Range | undefined; -} - -export class DebugEditorModelManager implements IWorkbenchContribution { - static readonly ID = 'breakpointManager'; - static readonly STICKINESS = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; - private modelDataMap: Map; - private toDispose: lifecycle.IDisposable[]; - private ignoreDecorationsChangedEvent = false; - - constructor( - @IModelService private readonly modelService: IModelService, - @IDebugService private readonly debugService: IDebugService, - ) { - this.modelDataMap = new Map(); - this.toDispose = []; - this.registerListeners(); - } - - public dispose(): void { - this.modelDataMap.forEach(modelData => { - lifecycle.dispose(modelData.toDispose); - modelData.model.deltaDecorations(modelData.breakpointDecorations.map(bpd => bpd.decorationId), []); - modelData.model.deltaDecorations(modelData.currentStackDecorations, []); - }); - this.toDispose = lifecycle.dispose(this.toDispose); - - this.modelDataMap.clear(); - } - - private registerListeners(): void { - this.toDispose.push(this.modelService.onModelAdded(this.onModelAdded, this)); - this.modelService.getModels().forEach(model => this.onModelAdded(model)); - this.toDispose.push(this.modelService.onModelRemoved(this.onModelRemoved, this)); - - this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); - this.toDispose.push(this.debugService.getViewModel().onDidFocusStackFrame(() => this.onFocusStackFrame())); - this.toDispose.push(this.debugService.onDidChangeState(state => { - if (state === State.Inactive) { - this.modelDataMap.forEach(modelData => { - modelData.topStackFrameRange = undefined; - }); - } - })); - } - - private onModelAdded(model: ITextModel): void { - const modelUriStr = model.uri.toString(); - const breakpoints = this.debugService.getModel().getBreakpoints({ uri: model.uri }); - - const currentStackDecorations = model.deltaDecorations([], this.createCallStackDecorations(modelUriStr)); - const desiredDecorations = this.createBreakpointDecorations(model, breakpoints); - const breakpointDecorationIds = model.deltaDecorations([], desiredDecorations); - const toDispose: lifecycle.IDisposable[] = [model.onDidChangeDecorations((e) => this.onModelDecorationsChanged(modelUriStr))]; - - this.modelDataMap.set(modelUriStr, { - model: model, - toDispose: toDispose, - breakpointDecorations: breakpointDecorationIds.map((decorationId, index) => ({ decorationId, modelId: breakpoints[index].getId(), range: desiredDecorations[index].range })), - currentStackDecorations: currentStackDecorations, - topStackFrameRange: undefined - }); - } - - private onModelRemoved(model: ITextModel): void { - const modelUriStr = model.uri.toString(); - const data = this.modelDataMap.get(modelUriStr); - if (data) { - lifecycle.dispose(data.toDispose); - this.modelDataMap.delete(modelUriStr); - } - } - - // call stack management. Represent data coming from the debug service. - - private onFocusStackFrame(): void { - this.modelDataMap.forEach((modelData, uri) => { - modelData.currentStackDecorations = modelData.model.deltaDecorations(modelData.currentStackDecorations, this.createCallStackDecorations(uri)); - }); - } - - private createCallStackDecorations(modelUriStr: string): IModelDeltaDecoration[] { - const result: IModelDeltaDecoration[] = []; - const stackFrame = this.debugService.getViewModel().focusedStackFrame; - if (!stackFrame || stackFrame.source.uri.toString() !== modelUriStr) { - return result; - } - - // only show decorations for the currently focused thread. - const columnUntilEOLRange = new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn, stackFrame.range.startLineNumber, Constants.MAX_SAFE_SMALL_INTEGER); - const range = new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn, stackFrame.range.startLineNumber, stackFrame.range.startColumn + 1); - - // compute how to decorate the editor. Different decorations are used if this is a top stack frame, focused stack frame, - // an exception or a stack frame that did not change the line number (we only decorate the columns, not the whole line). - const callStack = stackFrame.thread.getCallStack(); - if (callStack && callStack.length && stackFrame === callStack[0]) { - result.push({ - options: DebugEditorModelManager.TOP_STACK_FRAME_MARGIN, - range - }); - - result.push({ - options: DebugEditorModelManager.TOP_STACK_FRAME_DECORATION, - range: columnUntilEOLRange - }); - - const modelData = this.modelDataMap.get(modelUriStr); - if (modelData) { - if (modelData.topStackFrameRange && modelData.topStackFrameRange.startLineNumber === stackFrame.range.startLineNumber && modelData.topStackFrameRange.startColumn !== stackFrame.range.startColumn) { - result.push({ - options: DebugEditorModelManager.TOP_STACK_FRAME_INLINE_DECORATION, - range: columnUntilEOLRange - }); - } - modelData.topStackFrameRange = columnUntilEOLRange; - } - } else { - result.push({ - options: DebugEditorModelManager.FOCUSED_STACK_FRAME_MARGIN, - range - }); - - result.push({ - options: DebugEditorModelManager.FOCUSED_STACK_FRAME_DECORATION, - range: columnUntilEOLRange - }); - } - - return result; - } - - // breakpoints management. Represent data coming from the debug service and also send data back. - private onModelDecorationsChanged(modelUrlStr: string): void { - const modelData = this.modelDataMap.get(modelUrlStr); - if (!modelData || modelData.breakpointDecorations.length === 0 || this.ignoreDecorationsChangedEvent) { - // I have no decorations - return; - } - let somethingChanged = false; - modelData.breakpointDecorations.forEach(breakpointDecoration => { - if (somethingChanged) { - return; - } - const newBreakpointRange = modelData.model.getDecorationRange(breakpointDecoration.decorationId); - if (newBreakpointRange && (!breakpointDecoration.range.equalsRange(newBreakpointRange))) { - somethingChanged = true; - } - }); - if (!somethingChanged) { - // nothing to do, my decorations did not change. - return; - } - - const data = new Map(); - const breakpoints = this.debugService.getModel().getBreakpoints(); - const modelUri = modelData.model.uri; - for (let i = 0, len = modelData.breakpointDecorations.length; i < len; i++) { - const breakpointDecoration = modelData.breakpointDecorations[i]; - const decorationRange = modelData.model.getDecorationRange(breakpointDecoration.decorationId); - // check if the line got deleted. - if (decorationRange) { - const breakpoint = breakpoints.filter(bp => bp.getId() === breakpointDecoration.modelId).pop(); - // since we know it is collapsed, it cannot grow to multiple lines - if (breakpoint) { - data.set(breakpoint.getId(), { - lineNumber: decorationRange.startLineNumber, - column: breakpoint.column ? decorationRange.startColumn : undefined, - }); - } - } - } - - this.debugService.updateBreakpoints(modelUri, data, true).then(undefined, onUnexpectedError); - } - - private onBreakpointsChange(): void { - const breakpointsMap = new Map(); - this.debugService.getModel().getBreakpoints().forEach(bp => { - const uriStr = bp.uri.toString(); - const breakpoints = breakpointsMap.get(uriStr); - if (breakpoints) { - breakpoints.push(bp); - } else { - breakpointsMap.set(uriStr, [bp]); - } - }); - - breakpointsMap.forEach((bps, uri) => { - const data = this.modelDataMap.get(uri); - if (data) { - this.updateBreakpoints(data, breakpointsMap.get(uri)!); - } - }); - this.modelDataMap.forEach((modelData, uri) => { - if (!breakpointsMap.has(uri)) { - this.updateBreakpoints(modelData, []); - } - }); - } - - private updateBreakpoints(modelData: IDebugEditorModelData, newBreakpoints: IBreakpoint[]): void { - const desiredDecorations = this.createBreakpointDecorations(modelData.model, newBreakpoints); - try { - this.ignoreDecorationsChangedEvent = true; - const breakpointDecorationIds = modelData.model.deltaDecorations(modelData.breakpointDecorations.map(bpd => bpd.decorationId), desiredDecorations); - modelData.breakpointDecorations = breakpointDecorationIds.map((decorationId, index) => ({ - decorationId, - modelId: newBreakpoints[index].getId(), - range: desiredDecorations[index].range - })); - } finally { - this.ignoreDecorationsChangedEvent = false; - } - } - - private createBreakpointDecorations(model: ITextModel, breakpoints: ReadonlyArray): { range: Range; options: IModelDecorationOptions; }[] { - const result: { range: Range; options: IModelDecorationOptions; }[] = []; - breakpoints.forEach((breakpoint) => { - if (breakpoint.lineNumber <= model.getLineCount()) { - const column = model.getLineFirstNonWhitespaceColumn(breakpoint.lineNumber); - const range = model.validateRange( - breakpoint.column ? new Range(breakpoint.lineNumber, breakpoint.column, breakpoint.lineNumber, breakpoint.column + 1) - : new Range(breakpoint.lineNumber, column, breakpoint.lineNumber, column + 1) // Decoration has to have a width #20688 - ); - - result.push({ - options: this.getBreakpointDecorationOptions(breakpoint), - range - }); - } - }); - - return result; - } - - private getBreakpointDecorationOptions(breakpoint: IBreakpoint): IModelDecorationOptions { - const { className, message } = getBreakpointMessageAndClassName(this.debugService, breakpoint); - let glyphMarginHoverMessage: MarkdownString | undefined; - - if (message) { - if (breakpoint.condition || breakpoint.hitCondition) { - const modelData = this.modelDataMap.get(breakpoint.uri.toString()); - const modeId = modelData ? modelData.model.getLanguageIdentifier().language : ''; - glyphMarginHoverMessage = new MarkdownString().appendCodeblock(modeId, message); - } else { - glyphMarginHoverMessage = new MarkdownString().appendText(message); - } - } - - return { - glyphMarginClassName: className, - glyphMarginHoverMessage, - stickiness: DebugEditorModelManager.STICKINESS, - beforeContentClassName: breakpoint.column ? `debug-breakpoint-column ${className}-column` : undefined - }; - } - - // editor decorations - - // we need a separate decoration for glyph margin, since we do not want it on each line of a multi line statement. - private static TOP_STACK_FRAME_MARGIN: IModelDecorationOptions = { - glyphMarginClassName: 'debug-top-stack-frame', - stickiness: DebugEditorModelManager.STICKINESS - }; - - private static FOCUSED_STACK_FRAME_MARGIN: IModelDecorationOptions = { - glyphMarginClassName: 'debug-focused-stack-frame', - stickiness: DebugEditorModelManager.STICKINESS - }; - - private static TOP_STACK_FRAME_DECORATION: IModelDecorationOptions = { - isWholeLine: true, - inlineClassName: 'debug-remove-token-colors', - className: 'debug-top-stack-frame-line', - stickiness: DebugEditorModelManager.STICKINESS - }; - - private static TOP_STACK_FRAME_INLINE_DECORATION: IModelDecorationOptions = { - beforeContentClassName: 'debug-top-stack-frame-column' - }; - - private static FOCUSED_STACK_FRAME_DECORATION: IModelDecorationOptions = { - isWholeLine: true, - inlineClassName: 'debug-remove-token-colors', - className: 'debug-focused-stack-frame-line', - stickiness: DebugEditorModelManager.STICKINESS - }; -} - -registerThemingParticipant((theme, collector) => { - const topStackFrame = theme.getColor(topStackFrameColor); - if (topStackFrame) { - collector.addRule(`.monaco-editor .view-overlays .debug-top-stack-frame-line { background: ${topStackFrame}; }`); - collector.addRule(`.monaco-editor .view-overlays .debug-top-stack-frame-line { background: ${topStackFrame}; }`); - } - - const focusedStackFrame = theme.getColor(focusedStackFrameColor); - if (focusedStackFrame) { - collector.addRule(`.monaco-editor .view-overlays .debug-focused-stack-frame-line { background: ${focusedStackFrame}; }`); - } -}); - -const topStackFrameColor = registerColor('editor.stackFrameHighlightBackground', { dark: '#ffff0033', light: '#ffff6673', hc: '#fff600' }, localize('topStackFrameLineHighlight', 'Background color for the highlight of line at the top stack frame position.')); -const focusedStackFrameColor = registerColor('editor.focusedStackFrameHighlightBackground', { dark: '#7abd7a4d', light: '#cee7ce73', hc: '#cee7ce' }, localize('focusedStackFrameLineHighlight', 'Background color for the highlight of line at focused stack frame position.'));