/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import lifecycle = require('vs/base/common/lifecycle'); import editorcommon = require('vs/editor/common/editorCommon'); import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IDebugService, ModelEvents, ViewModelEvents, IBreakpoint, IRawBreakpoint, State } from 'vs/workbench/parts/debug/common/debug'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IModelService } from 'vs/editor/common/services/modelService'; function toMap(arr: string[]): { [key: string]: boolean; } { const result: { [key: string]: boolean; } = {}; for (var i = 0, len = arr.length; i < len; i++) { result[arr[i]] = true; } return result; } function createRange(startLineNUmber: number, startColumn: number, endLineNumber: number, endColumn: number): editorcommon.IRange { return { startLineNumber: startLineNUmber, startColumn: startColumn, endLineNumber: endLineNumber, endColumn: endColumn }; } interface IDebugEditorModelData { model: editorcommon.IModel; toDispose: lifecycle.IDisposable[]; breakpointDecorationIds: string[]; breakpointLines: number[]; breakpointDecorationsAsMap: { [decorationId: string]: boolean; }; currentStackDecorations: string[]; topStackFrameRange: editorcommon.IRange; } export class DebugEditorModelManager implements IWorkbenchContribution { static ID = 'breakpointManager'; private modelData: { [modelUrl: string]: IDebugEditorModelData; }; private toDispose: lifecycle.IDisposable[]; constructor( @IModelService private modelService: IModelService, @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @IDebugService private debugService: IDebugService ) { this.modelData = {}; this.toDispose = []; this.registerListeners(); } public getId(): string { return DebugEditorModelManager.ID; } public dispose(): void { this.modelService.onModelAdded.remove(this.onModelAdded, this); this.modelService.onModelAdded.remove(this.onModelRemoved, this); var modelUrlStr: string; for (modelUrlStr in this.modelData) { if (this.modelData.hasOwnProperty(modelUrlStr)) { var modelData = this.modelData[modelUrlStr]; lifecycle.disposeAll(modelData.toDispose); modelData.model.deltaDecorations(modelData.breakpointDecorationIds, []); modelData.model.deltaDecorations(modelData.currentStackDecorations, []); } } this.toDispose = lifecycle.disposeAll(this.toDispose); this.modelData = null; } private registerListeners(): void { this.modelService.onModelAdded.add(this.onModelAdded, this); this.modelService.getModels().forEach(model => this.onModelAdded(model)); this.modelService.onModelRemoved.add(this.onModelRemoved, this); this.toDispose.push(this.debugService.getModel().addListener2(ModelEvents.BREAKPOINTS_UPDATED, () => this.onBreakpointsChanged())); this.toDispose.push(this.debugService.getViewModel().addListener2(ViewModelEvents.FOCUSED_STACK_FRAME_UPDATED, () => this.onFocusedStackFrameUpdated())); } private onModelAdded(model: editorcommon.IModel): void { const modelUrlStr = model.getAssociatedResource().toString(); const breakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp.source.uri.toString() === modelUrlStr); var currentStackDecorations = model.deltaDecorations([], this.createCallStackDecorations(modelUrlStr)); var breakPointDecorations = model.deltaDecorations([], this.createBreakpointDecorations(breakpoints)); const toDispose: lifecycle.IDisposable[] = [model.addListener2(editorcommon.EventType.ModelDecorationsChanged, (e: editorcommon.IModelDecorationsChangedEvent) => this.onModelDecorationsChanged(modelUrlStr, e))]; this.modelData[modelUrlStr] = { model: model, toDispose: toDispose, breakpointDecorationIds: breakPointDecorations, breakpointLines: breakpoints.map(bp => bp.lineNumber), breakpointDecorationsAsMap: toMap(breakPointDecorations), currentStackDecorations: currentStackDecorations, topStackFrameRange: null }; } private onModelRemoved(model: editorcommon.IModel): void { const modelUrlStr = model.getAssociatedResource().toString(); if (this.modelData.hasOwnProperty(modelUrlStr)) { const modelData = this.modelData[modelUrlStr]; delete this.modelData[modelUrlStr]; lifecycle.disposeAll(modelData.toDispose); } } // Call stack management. Represent data coming from the debug service. private onFocusedStackFrameUpdated(): void { Object.keys(this.modelData).forEach(modelUrlStr => { const modelData = this.modelData[modelUrlStr]; modelData.currentStackDecorations = modelData.model.deltaDecorations(modelData.currentStackDecorations, this.createCallStackDecorations(modelUrlStr)); }); } private createCallStackDecorations(modelUrlStr: string): editorcommon.IModelDeltaDecoration[] { const result: editorcommon.IModelDeltaDecoration[] = []; const focusedStackFrame = this.debugService.getViewModel().getFocusedStackFrame(); const allThreads = this.debugService.getModel().getThreads(); if (!focusedStackFrame || !allThreads[focusedStackFrame.threadId] || !allThreads[focusedStackFrame.threadId].callStack) { return result; } // Only show decorations for the currently focussed thread. const thread = allThreads[focusedStackFrame.threadId]; thread.callStack.filter(sf => sf.source.uri.toString() === modelUrlStr).forEach(sf => { const wholeLineRange = createRange(sf.lineNumber, sf.column, sf.lineNumber, Number.MAX_VALUE); // Compute how to decorate the editor. Different decorations are used if this is a top stack frame, focussed stack frame, // an exception or a stack frame that did not change the line number (we only decorate the columns, not the whole line). if (sf === thread.callStack[0]) { result.push({ options: DebugEditorModelManager.TOP_STACK_FRAME_MARGIN, range: createRange(sf.lineNumber, sf.column, sf.lineNumber, sf.column + 1) }); if (thread.exception) { result.push({ options: DebugEditorModelManager.TOP_STACK_FRAME_EXCEPTION_DECORATION, range: wholeLineRange }); } else { result.push({ options: DebugEditorModelManager.TOP_STACK_FRAME_DECORATION, range: wholeLineRange }); if (this.modelData[modelUrlStr]) { if (this.modelData[modelUrlStr].topStackFrameRange && this.modelData[modelUrlStr].topStackFrameRange.startLineNumber === wholeLineRange.startLineNumber && this.modelData[modelUrlStr].topStackFrameRange.startColumn !== wholeLineRange.startColumn) { result.push({ options: DebugEditorModelManager.TOP_STACK_FRAME_COLUMN_DECORATION, range: wholeLineRange }); } this.modelData[modelUrlStr].topStackFrameRange = wholeLineRange; } } } else if (sf === focusedStackFrame) { result.push({ options: DebugEditorModelManager.FOCUSED_STACK_FRAME_MARGIN, range: createRange(sf.lineNumber, sf.column, sf.lineNumber, sf.column + 1) }); result.push({ options: DebugEditorModelManager.FOCUSED_STACK_FRAME_DECORATION, range: wholeLineRange }); } }); return result; } // Breakpoints management. Represent data coming from the debug service and also send data back. private onModelDecorationsChanged(modelUrlStr: string, e: editorcommon.IModelDecorationsChangedEvent): void { const modelData = this.modelData[modelUrlStr]; if (!e.addedOrChangedDecorations.some(d => modelData.breakpointDecorationsAsMap.hasOwnProperty(d.id))) { // Nothing to do, my decorations did not change. return; } const data: IRawBreakpoint[] = []; const enabledAndConditions: { [key: number]: { enabled: boolean, condition: string } } = {}; this.debugService.getModel().getBreakpoints().filter(bp => bp.source.uri.toString() === modelUrlStr).forEach(bp => { enabledAndConditions[bp.lineNumber] = { enabled: bp.enabled, condition: bp.condition }; }); const modelUrl = modelData.model.getAssociatedResource(); for (let i = 0, len = modelData.breakpointDecorationIds.length; i < len; i++) { const decorationRange = modelData.model.getDecorationRange(modelData.breakpointDecorationIds[i]); // Check if the line got deleted. if (decorationRange.endColumn - decorationRange.startColumn > 0) { // Since we know it is collapsed, it cannot grow to multiple lines data.push({ uri: modelUrl, lineNumber: decorationRange.startLineNumber, enabled: enabledAndConditions[modelData.breakpointLines[i]].enabled, condition: enabledAndConditions[modelData.breakpointLines[i]].condition }); } } this.debugService.setBreakpointsForModel(modelUrl, data); } private onBreakpointsChanged(): void { const breakpointsMap: { [key: string]: IBreakpoint[] } = {}; this.debugService.getModel().getBreakpoints().forEach(bp => { const uriStr = bp.source.uri.toString(); if (breakpointsMap[uriStr]) { breakpointsMap[uriStr].push(bp); } else { breakpointsMap[uriStr] = [bp]; } }); Object.keys(breakpointsMap).forEach(modelUriStr => { if (this.modelData.hasOwnProperty(modelUriStr)) { this.updateBreakpoints(this.modelData[modelUriStr], breakpointsMap[modelUriStr]); } }); Object.keys(this.modelData).forEach(modelUriStr => { if (!breakpointsMap.hasOwnProperty(modelUriStr)) { this.updateBreakpoints(this.modelData[modelUriStr], []); } }); } private updateBreakpoints(modelData: IDebugEditorModelData, newBreakpoints: IBreakpoint[]): void { modelData.breakpointDecorationIds = modelData.model.deltaDecorations(modelData.breakpointDecorationIds, this.createBreakpointDecorations(newBreakpoints)); modelData.breakpointDecorationsAsMap = toMap(modelData.breakpointDecorationIds); modelData.breakpointLines = newBreakpoints.map(bp => bp.lineNumber); } private createBreakpointDecorations(breakpoints: IBreakpoint[]): editorcommon.IModelDeltaDecoration[] { const activated = this.debugService.getModel().areBreakpointsActivated(); const debugActive = this.debugService.getState() === State.Running || this.debugService.getState() === State.Stopped; return breakpoints.map((breakpoint) => { return { options: (!breakpoint.enabled || !activated) ? DebugEditorModelManager.BREAKPOINT_DISABLED_DECORATION : debugActive && !breakpoint.verified ? DebugEditorModelManager.BREAKPOINT_UNVERIFIED_DECORATION : DebugEditorModelManager.BREAKPOINT_DECORATION, range: createRange(breakpoint.lineNumber, 1, breakpoint.lineNumber, 2) }; }); } // Editor decorations private static BREAKPOINT_DECORATION: editorcommon.IModelDecorationOptions = { glyphMarginClassName: 'debug-breakpoint-glyph', stickiness: editorcommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; private static BREAKPOINT_DISABLED_DECORATION: editorcommon.IModelDecorationOptions = { glyphMarginClassName: 'debug-breakpoint-glyph-disabled', stickiness: editorcommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; private static BREAKPOINT_UNVERIFIED_DECORATION: editorcommon.IModelDecorationOptions = { glyphMarginClassName: 'debug-breakpoint-glyph-unverified', stickiness: editorcommon.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: editorcommon.IModelDecorationOptions = { glyphMarginClassName: 'debug-top-stack-frame-glyph', stickiness: editorcommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } private static FOCUSED_STACK_FRAME_MARGIN: editorcommon.IModelDecorationOptions = { glyphMarginClassName: 'debug-focused-stack-frame-glyph', stickiness: editorcommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } private static TOP_STACK_FRAME_DECORATION: editorcommon.IModelDecorationOptions = { isWholeLine: true, className: 'debug-top-stack-frame-line', stickiness: editorcommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; private static TOP_STACK_FRAME_EXCEPTION_DECORATION: editorcommon.IModelDecorationOptions = { isWholeLine: true, className: 'debug-top-stack-frame-exception-line', stickiness: editorcommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; private static TOP_STACK_FRAME_COLUMN_DECORATION: editorcommon.IModelDecorationOptions = { isWholeLine: false, className: 'debug-top-stack-frame-column', stickiness: editorcommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; private static FOCUSED_STACK_FRAME_DECORATION: editorcommon.IModelDecorationOptions = { isWholeLine: true, className: 'debug-focused-stack-frame-line', stickiness: editorcommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; }