diff --git a/packages/debug/src/browser/breakpoint/breakpoint-manager.ts b/packages/debug/src/browser/breakpoint/breakpoint-manager.ts index c5daf762c2515..13632a3d664c5 100644 --- a/packages/debug/src/browser/breakpoint/breakpoint-manager.ts +++ b/packages/debug/src/browser/breakpoint/breakpoint-manager.ts @@ -15,11 +15,20 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; +import { Emitter, Event } from '@theia/core/lib/common'; import { StorageService } from '@theia/core/lib/browser'; +import { Marker } from '@theia/markers/lib/common/marker'; import { MarkerManager } from '@theia/markers/lib/browser/marker-manager'; import URI from '@theia/core/lib/common/uri'; import { SourceBreakpoint, BREAKPOINT_KIND } from './breakpoint-marker'; +export interface BreakpointsChangeEvent { + uri: URI + added: SourceBreakpoint[] + removed: SourceBreakpoint[] + changed: SourceBreakpoint[] +} + @injectable() export class BreakpointManager extends MarkerManager { @@ -32,6 +41,33 @@ export class BreakpointManager extends MarkerManager { return BREAKPOINT_KIND; } + protected readonly onDidChangeBreakpointsEmitter = new Emitter(); + readonly onDidChangeBreakpoints: Event = this.onDidChangeBreakpointsEmitter.event; + + setMarkers(uri: URI, owner: string, newMarkers: SourceBreakpoint[]): Marker[] { + const result = super.setMarkers(uri, owner, newMarkers); + const added: SourceBreakpoint[] = []; + const removed: SourceBreakpoint[] = []; + const changed: SourceBreakpoint[] = []; + const oldMarkers = new Map(result.map(({ data }) => [data.id, data] as [string, SourceBreakpoint])); + const ids = new Set(); + for (const newMarker of newMarkers) { + ids.add(newMarker.id); + if (oldMarkers.has(newMarker.id)) { + changed.push(newMarker); + } else { + added.push(newMarker); + } + } + for (const [id, data] of oldMarkers.entries()) { + if (!ids.has(id)) { + removed.push(data); + } + } + this.onDidChangeBreakpointsEmitter.fire({ uri, added, removed, changed }); + return result; + } + getBreakpoint(uri: URI, line: number): SourceBreakpoint | undefined { const marker = this.findMarkers({ uri, @@ -48,26 +84,16 @@ export class BreakpointManager extends MarkerManager { this.setMarkers(uri, this.owner, breakpoints.sort((a, b) => a.raw.line - b.raw.line)); } - addBreakpoint(uri: URI, line: number, column?: number): void { + addBreakpoint(breakpoint: SourceBreakpoint): boolean { + const uri = new URI(breakpoint.uri); const breakpoints = this.getBreakpoints(uri); - const newBreakpoints = breakpoints.filter(({ raw }) => raw.line !== line); + const newBreakpoints = breakpoints.filter(({ raw }) => raw.line !== breakpoint.raw.line); if (breakpoints.length === newBreakpoints.length) { - newBreakpoints.push({ - uri: uri.toString(), - enabled: true, - raw: { - line, - column - } - }); + newBreakpoints.push(breakpoint); this.setBreakpoints(uri, newBreakpoints); + return true; } - } - - deleteBreakpoint(uri: URI, line: number, column?: number): void { - const breakpoints = this.getBreakpoints(uri); - const newBreakpoints = breakpoints.filter(({ raw }) => raw.line !== line); - this.setBreakpoints(uri, newBreakpoints); + return false; } enableAllBreakpoints(enabled: boolean): void { diff --git a/packages/debug/src/browser/breakpoint/breakpoint-marker.ts b/packages/debug/src/browser/breakpoint/breakpoint-marker.ts index f0ab01c5bc032..a60e9cd112dc8 100644 --- a/packages/debug/src/browser/breakpoint/breakpoint-marker.ts +++ b/packages/debug/src/browser/breakpoint/breakpoint-marker.ts @@ -14,21 +14,36 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { UUID } from '@phosphor/coreutils'; +import URI from '@theia/core/lib/common/uri'; import { Marker } from '@theia/markers/lib/common/marker'; import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; export const BREAKPOINT_KIND = 'breakpoint'; export interface SourceBreakpoint { + id: string; uri: string; enabled: boolean; raw: DebugProtocol.SourceBreakpoint } +export namespace SourceBreakpoint { + export function create(uri: URI, data: DebugProtocol.SourceBreakpoint, origin?: SourceBreakpoint): SourceBreakpoint { + return { + id: origin ? origin.id : UUID.uuid4(), + uri: uri.toString(), + enabled: origin ? origin.enabled : true, + raw: { + ...(origin && origin.raw), + ...data + } + }; + } +} export interface BreakpointMarker extends Marker { kind: 'breakpoint' } - export namespace BreakpointMarker { export function is(node: Marker): node is BreakpointMarker { return 'kind' in node && node.kind === BREAKPOINT_KIND; diff --git a/packages/debug/src/browser/debug-frontend-application-contribution.ts b/packages/debug/src/browser/debug-frontend-application-contribution.ts index aadd428506c55..896c5b89c7689 100644 --- a/packages/debug/src/browser/debug-frontend-application-contribution.ts +++ b/packages/debug/src/browser/debug-frontend-application-contribution.ts @@ -133,6 +133,16 @@ export namespace DebugCommands { category: DEBUG_CATEGORY, label: 'Toggle Breakpoint', }; + export const ADD_CONDITIONAL_BREAKPOINT: Command = { + id: 'debug.breakpoint.add.conditional', + category: DEBUG_CATEGORY, + label: 'Add Conditional Breakpoint...', + }; + export const ADD_LOGPOINT: Command = { + id: 'debug.breakpoint.add.logpoint', + category: DEBUG_CATEGORY, + label: 'Add Logpoint...', + }; export const ENABLE_ALL_BREAKPOINTS: Command = { id: 'debug.breakpoint.enableAll', category: DEBUG_CATEGORY, @@ -143,11 +153,26 @@ export namespace DebugCommands { category: DEBUG_CATEGORY, label: 'Disable All Breakpoints', }; + export const EDIT_BREAKPOINT: Command = { + id: 'debug.breakpoint.edit', + category: DEBUG_CATEGORY, + label: 'Edit Breakpoint...', + }; + export const EDIT_LOGPOINT: Command = { + id: 'debug.logpoint.edit', + category: DEBUG_CATEGORY, + label: 'Edit Logpoint...', + }; export const REMOVE_BREAKPOINT: Command = { id: 'debug.breakpoint.remove', category: DEBUG_CATEGORY, label: 'Remove Breakpoint', }; + export const REMOVE_LOGPOINT: Command = { + id: 'debug.logpoint.remove', + category: DEBUG_CATEGORY, + label: 'Remove Logpoint', + }; export const REMOVE_ALL_BREAKPOINTS: Command = { id: 'debug.breakpoint.removeAll', category: DEBUG_CATEGORY, @@ -235,15 +260,44 @@ export namespace DebugEditorContextCommands { export const ADD_BREAKPOINT = { id: 'debug.editor.context.addBreakpoint' }; + export const ADD_CONDITIONAL_BREAKPOINT = { + id: 'debug.editor.context.addBreakpoint.conditional' + }; + export const ADD_LOGPOINT = { + id: 'debug.editor.context.add.logpoint' + }; export const REMOVE_BREAKPOINT = { id: 'debug.editor.context.removeBreakpoint' }; + export const EDIT_BREAKPOINT = { + id: 'debug.editor.context.edit.breakpoint' + }; export const ENABLE_BREAKPOINT = { id: 'debug.editor.context.enableBreakpoint' }; export const DISABLE_BREAKPOINT = { id: 'debug.editor.context.disableBreakpoint' }; + export const REMOVE_LOGPOINT = { + id: 'debug.editor.context.logpoint.remove' + }; + export const EDIT_LOGPOINT = { + id: 'debug.editor.context.logpoint.edit' + }; + export const ENABLE_LOGPOINT = { + id: 'debug.editor.context.logpoint.enable' + }; + export const DISABLE_LOGPOINT = { + id: 'debug.editor.context.logpoint.disable' + }; +} +export namespace DebugBreakpointWidgetCommands { + export const ACCEPT = { + id: 'debug.breakpointWidget.accept' + }; + export const CLOSE = { + id: 'debug.breakpointWidget.close' + }; } const darkCss = require('../../src/browser/style/debug-dark.useable.css'); @@ -421,8 +475,13 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi DebugCommands.COPY_VAIRABLE_AS_EXPRESSION ); + registerMenuActions(DebugBreakpointsWidget.EDIT_MENU, + DebugCommands.EDIT_BREAKPOINT, + DebugCommands.EDIT_LOGPOINT + ); registerMenuActions(DebugBreakpointsWidget.REMOVE_MENU, DebugCommands.REMOVE_BREAKPOINT, + DebugCommands.REMOVE_LOGPOINT, DebugCommands.REMOVE_ALL_BREAKPOINTS ); registerMenuActions(DebugBreakpointsWidget.ENABLE_MENU, @@ -432,9 +491,16 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi registerMenuActions(DebugEditorModel.CONTEXT_MENU, { ...DebugEditorContextCommands.ADD_BREAKPOINT, label: 'Add Breakpoint' }, - { ...DebugEditorContextCommands.REMOVE_BREAKPOINT, label: 'Remove Breakpoint' }, + { ...DebugEditorContextCommands.ADD_CONDITIONAL_BREAKPOINT, label: DebugCommands.ADD_CONDITIONAL_BREAKPOINT.label }, + { ...DebugEditorContextCommands.ADD_LOGPOINT, label: DebugCommands.ADD_LOGPOINT.label }, + { ...DebugEditorContextCommands.REMOVE_BREAKPOINT, label: DebugCommands.REMOVE_BREAKPOINT.label }, + { ...DebugEditorContextCommands.EDIT_BREAKPOINT, label: DebugCommands.EDIT_BREAKPOINT.label }, { ...DebugEditorContextCommands.ENABLE_BREAKPOINT, label: 'Enable Breakpoint' }, - { ...DebugEditorContextCommands.DISABLE_BREAKPOINT, label: 'Disable Breakpoint' } + { ...DebugEditorContextCommands.DISABLE_BREAKPOINT, label: 'Disable Breakpoint' }, + { ...DebugEditorContextCommands.REMOVE_LOGPOINT, label: DebugCommands.REMOVE_LOGPOINT.label }, + { ...DebugEditorContextCommands.EDIT_LOGPOINT, label: DebugCommands.EDIT_LOGPOINT.label }, + { ...DebugEditorContextCommands.ENABLE_LOGPOINT, label: 'Enable Logpoint' }, + { ...DebugEditorContextCommands.DISABLE_LOGPOINT, label: 'Disable Logpoint' } ); } @@ -573,6 +639,14 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi execute: () => this.editors.toggleBreakpoint(), isEnabled: () => !!this.editors.model }); + registry.registerCommand(DebugCommands.ADD_CONDITIONAL_BREAKPOINT, { + execute: () => this.editors.addBreakpoint('condition'), + isEnabled: () => !!this.editors.model && !this.editors.anyBreakpoint + }); + registry.registerCommand(DebugCommands.ADD_LOGPOINT, { + execute: () => this.editors.addBreakpoint('logMessage'), + isEnabled: () => !!this.editors.model && !this.editors.anyBreakpoint + }); registry.registerCommand(DebugCommands.ENABLE_ALL_BREAKPOINTS, { execute: () => this.breakpointManager.enableAllBreakpoints(true), isEnabled: () => !!this.breakpointManager.getUris().next().value @@ -581,6 +655,26 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi execute: () => this.breakpointManager.enableAllBreakpoints(false), isEnabled: () => !!this.breakpointManager.getUris().next().value }); + registry.registerCommand(DebugCommands.EDIT_BREAKPOINT, { + execute: async () => { + const { selectedBreakpoint } = this; + if (selectedBreakpoint) { + await this.editors.editBreakpoint(selectedBreakpoint); + } + }, + isEnabled: () => !!this.selectedBreakpoint, + isVisible: () => !!this.selectedBreakpoint + }); + registry.registerCommand(DebugCommands.EDIT_LOGPOINT, { + execute: async () => { + const { selectedLogpoint } = this; + if (selectedLogpoint) { + await this.editors.editBreakpoint(selectedLogpoint); + } + }, + isEnabled: () => !!this.selectedLogpoint, + isVisible: () => !!this.selectedLogpoint + }); registry.registerCommand(DebugCommands.REMOVE_BREAKPOINT, { execute: () => { const { selectedBreakpoint } = this; @@ -588,7 +682,18 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi selectedBreakpoint.remove(); } }, - isEnabled: () => !!this.selectedBreakpoint + isEnabled: () => !!this.selectedBreakpoint, + isVisible: () => !!this.selectedBreakpoint + }); + registry.registerCommand(DebugCommands.REMOVE_LOGPOINT, { + execute: () => { + const { selectedLogpoint } = this; + if (selectedLogpoint) { + selectedLogpoint.remove(); + } + }, + isEnabled: () => !!this.selectedLogpoint, + isVisible: () => !!this.selectedLogpoint }); registry.registerCommand(DebugCommands.REMOVE_ALL_BREAKPOINTS, { execute: () => this.breakpointManager.cleanAllMarkers(), @@ -634,14 +739,29 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi registry.registerCommand(DebugEditorContextCommands.ADD_BREAKPOINT, { execute: () => this.editors.toggleBreakpoint(), - isEnabled: () => !this.editors.breakpoint, - isVisible: () => !this.editors.breakpoint + isEnabled: () => !this.editors.anyBreakpoint, + isVisible: () => !this.editors.anyBreakpoint + }); + registry.registerCommand(DebugEditorContextCommands.ADD_CONDITIONAL_BREAKPOINT, { + execute: () => this.editors.addBreakpoint('condition'), + isEnabled: () => !this.editors.anyBreakpoint, + isVisible: () => !this.editors.anyBreakpoint + }); + registry.registerCommand(DebugEditorContextCommands.ADD_LOGPOINT, { + execute: () => this.editors.addBreakpoint('logMessage'), + isEnabled: () => !this.editors.anyBreakpoint, + isVisible: () => !this.editors.anyBreakpoint }); registry.registerCommand(DebugEditorContextCommands.REMOVE_BREAKPOINT, { execute: () => this.editors.toggleBreakpoint(), isEnabled: () => !!this.editors.breakpoint, isVisible: () => !!this.editors.breakpoint }); + registry.registerCommand(DebugEditorContextCommands.EDIT_BREAKPOINT, { + execute: () => this.editors.editBreakpoint(), + isEnabled: () => !!this.editors.breakpoint, + isVisible: () => !!this.editors.breakpoint + }); registry.registerCommand(DebugEditorContextCommands.ENABLE_BREAKPOINT, { execute: () => this.editors.setBreakpointEnabled(true), isEnabled: () => this.editors.breakpointEnabled === false, @@ -652,6 +772,33 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi isEnabled: () => !!this.editors.breakpointEnabled, isVisible: () => !!this.editors.breakpointEnabled }); + registry.registerCommand(DebugEditorContextCommands.REMOVE_LOGPOINT, { + execute: () => this.editors.toggleBreakpoint(), + isEnabled: () => !!this.editors.logpoint, + isVisible: () => !!this.editors.logpoint + }); + registry.registerCommand(DebugEditorContextCommands.EDIT_LOGPOINT, { + execute: () => this.editors.editBreakpoint(), + isEnabled: () => !!this.editors.logpoint, + isVisible: () => !!this.editors.logpoint + }); + registry.registerCommand(DebugEditorContextCommands.ENABLE_LOGPOINT, { + execute: () => this.editors.setBreakpointEnabled(true), + isEnabled: () => this.editors.logpointEnabled === false, + isVisible: () => this.editors.logpointEnabled === false + }); + registry.registerCommand(DebugEditorContextCommands.DISABLE_LOGPOINT, { + execute: () => this.editors.setBreakpointEnabled(false), + isEnabled: () => !!this.editors.logpointEnabled, + isVisible: () => !!this.editors.logpointEnabled + }); + + registry.registerCommand(DebugBreakpointWidgetCommands.ACCEPT, { + execute: () => this.editors.acceptBreakpoint() + }); + registry.registerCommand(DebugBreakpointWidgetCommands.CLOSE, { + execute: () => this.editors.closeBreakpoint() + }); } registerKeybindings(keybindings: KeybindingRegistry): void { @@ -706,6 +853,17 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi keybinding: 'f9', context: EditorKeybindingContexts.editorTextFocus }); + + keybindings.registerKeybinding({ + command: DebugBreakpointWidgetCommands.ACCEPT.id, + keybinding: 'enter', + context: DebugKeybindingContexts.inBreakpointWidget + }); + keybindings.registerKeybinding({ + command: DebugBreakpointWidgetCommands.CLOSE.id, + keybinding: 'esc', + context: DebugKeybindingContexts.inStrictBreakpointWidget + }); } protected readonly sessionWidgets = new Map(); @@ -793,10 +951,18 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi const { currentWidget } = this.shell; return currentWidget instanceof DebugBreakpointsWidget && currentWidget || undefined; } - get selectedBreakpoint(): DebugBreakpoint | undefined { + get selectedAnyBreakpoint(): DebugBreakpoint | undefined { const { breakpoints } = this; return breakpoints && breakpoints.selectedElement instanceof DebugBreakpoint && breakpoints.selectedElement || undefined; } + get selectedBreakpoint(): DebugBreakpoint | undefined { + const breakpoint = this.selectedAnyBreakpoint; + return breakpoint && !breakpoint.logMessage ? breakpoint : undefined; + } + get selectedLogpoint(): DebugBreakpoint | undefined { + const breakpoint = this.selectedAnyBreakpoint; + return breakpoint && !!breakpoint.logMessage ? breakpoint : undefined; + } get variables(): DebugVariablesWidget | undefined { const { currentWidget } = this.shell; diff --git a/packages/debug/src/browser/debug-frontend-module.ts b/packages/debug/src/browser/debug-frontend-module.ts index f1cb498838010..bc23cd709b072 100644 --- a/packages/debug/src/browser/debug-frontend-module.ts +++ b/packages/debug/src/browser/debug-frontend-module.ts @@ -37,7 +37,7 @@ import { BreakpointManager } from './breakpoint/breakpoint-manager'; import { DebugEditorService } from './editor/debug-editor-service'; import { DebugViewOptions } from './view/debug-view-model'; import { DebugSessionWidget, DebugSessionWidgetFactory } from './view/debug-session-widget'; -import { InDebugModeContext } from './debug-keybinding-contexts'; +import { InDebugModeContext, InBreakpointWidgetContext, InStrictBreakpointWidgetContext } from './debug-keybinding-contexts'; import { DebugEditorModelFactory, DebugEditorModel } from './editor/debug-editor-model'; import './debug-monaco-contribution'; import { bindDebugPreferences } from './debug-preferences'; @@ -71,6 +71,8 @@ export default new ContainerModule((bind: interfaces.Bind) => { bind(ResourceResolver).toService(DebugResourceResolver); bind(KeybindingContext).to(InDebugModeContext).inSingletonScope(); + bind(KeybindingContext).to(InBreakpointWidgetContext).inSingletonScope(); + bind(KeybindingContext).to(InStrictBreakpointWidgetContext).inSingletonScope(); bindViewContribution(bind, DebugFrontendApplicationContribution); bind(FrontendApplicationContribution).toService(DebugFrontendApplicationContribution); diff --git a/packages/debug/src/browser/debug-keybinding-contexts.ts b/packages/debug/src/browser/debug-keybinding-contexts.ts index f0b273e6f6f68..bd5d3cfa66dd8 100644 --- a/packages/debug/src/browser/debug-keybinding-contexts.ts +++ b/packages/debug/src/browser/debug-keybinding-contexts.ts @@ -18,11 +18,16 @@ import { injectable, inject } from 'inversify'; import { KeybindingContext } from '@theia/core/lib/browser'; import { DebugSessionManager } from './debug-session-manager'; import { DebugState } from './debug-session'; +import { DebugEditorService } from './editor/debug-editor-service'; export namespace DebugKeybindingContexts { export const inDebugMode = 'inDebugMode'; + export const inBreakpointWidget = 'inBreakpointWidget'; + + export const inStrictBreakpointWidget = 'inStrictBreakpointWidget'; + } @injectable() @@ -38,3 +43,30 @@ export class InDebugModeContext implements KeybindingContext { } } + +@injectable() +export class InBreakpointWidgetContext implements KeybindingContext { + + readonly id: string = DebugKeybindingContexts.inBreakpointWidget; + + @inject(DebugEditorService) + protected readonly editors: DebugEditorService; + + isEnabled(): boolean { + const model = this.editors.model; + return !!model && !!model.breakpointWidget.position && !model.editor.isFocused(); + } + +} + +@injectable() +export class InStrictBreakpointWidgetContext extends InBreakpointWidgetContext { + + readonly id: string = DebugKeybindingContexts.inStrictBreakpointWidget; + + isEnabled(): boolean { + const model = this.editors.model; + return !!model && !!model.breakpointWidget.position && (!model.editor.isFocused() || model.editor.isFocused({ strict: true })); + } + +} diff --git a/packages/debug/src/browser/debug-session-manager.ts b/packages/debug/src/browser/debug-session-manager.ts index ca0f6b0272b32..62fe77d27ac0e 100644 --- a/packages/debug/src/browser/debug-session-manager.ts +++ b/packages/debug/src/browser/debug-session-manager.ts @@ -322,17 +322,4 @@ export class DebugSessionManager { return origin && new DebugBreakpoint(origin, this.labelProvider, this.breakpoints, this.editorManager); } - addBreakpoints(breakpoints: DebugBreakpoint[]): void { - breakpoints.forEach(breakpoint => { - this.breakpoints.addBreakpoint(breakpoint.uri, breakpoint.line, breakpoint.column); - this.fireDidChangeBreakpoints({ uri: breakpoint.uri }); - }); - } - - deleteBreakpoints(breakpoints: DebugBreakpoint[]): void { - breakpoints.forEach(breakpoint => { - this.breakpoints.deleteBreakpoint(breakpoint.uri, breakpoint.line, breakpoint.column); - this.fireDidChangeBreakpoints({ uri: breakpoint.uri }); - }); - } } diff --git a/packages/debug/src/browser/debug-session.tsx b/packages/debug/src/browser/debug-session.tsx index bac3d931f0efb..f5ea36d889a0a 100644 --- a/packages/debug/src/browser/debug-session.tsx +++ b/packages/debug/src/browser/debug-session.tsx @@ -34,6 +34,7 @@ import URI from '@theia/core/lib/common/uri'; import { BreakpointManager } from './breakpoint/breakpoint-manager'; import { DebugSessionOptions, InternalDebugSessionOptions } from './debug-session-options'; import { DebugConfiguration } from '../common/debug-common'; +import { SourceBreakpoint } from './breakpoint/breakpoint-marker'; export enum DebugState { Inactive, @@ -452,15 +453,20 @@ export class DebugSession implements CompositeTreeElement { try { const raw = body.breakpoint; if (body.reason === 'new') { - const breakpoint = this.toBreakpoint(raw); - if (breakpoint) { - const breakpoints = this.getBreakpoints(breakpoint.uri); - breakpoints.push(breakpoint); - this.setBreakpoints(breakpoint.uri, breakpoints); + if (raw.source && typeof raw.line === 'number') { + const uri = DebugSource.toUri(raw.source); + const origin = SourceBreakpoint.create(uri, { line: raw.line, column: 1 }); + if (this.breakpoints.addBreakpoint(origin)) { + const breakpoints = this.getBreakpoints(uri); + const breakpoint = new DebugBreakpoint(origin, this.labelProvider, this.breakpoints, this.editorManager, this); + breakpoint.update({ raw }); + breakpoints.push(breakpoint); + this.setBreakpoints(uri, breakpoints); + } } } if (body.reason === 'removed' && raw.id) { - const toRemove = this.findBreakpoint(b => b.id === raw.id); + const toRemove = this.findBreakpoint(b => b.idFromAdapter === raw.id); if (toRemove) { toRemove.remove(); const breakpoints = this.getBreakpoints(toRemove.uri); @@ -472,7 +478,7 @@ export class DebugSession implements CompositeTreeElement { } } if (body.reason === 'changed' && raw.id) { - const toUpdate = this.findBreakpoint(b => b.id === raw.id); + const toUpdate = this.findBreakpoint(b => b.idFromAdapter === raw.id); if (toUpdate) { toUpdate.update({ raw }); this.fireDidChangeBreakpoints(toUpdate.uri); @@ -492,21 +498,6 @@ export class DebugSession implements CompositeTreeElement { } return undefined; } - protected toBreakpoint(raw: DebugProtocol.Breakpoint): DebugBreakpoint | undefined { - if (!raw.source || !raw.line) { - return undefined; - } - const breakpoint = new DebugBreakpoint({ - uri: DebugSource.toUri(raw.source).toString(), - enabled: true, - raw: { - line: raw.line, - column: raw.column - } - }, this.labelProvider, this.breakpoints, this.editorManager, this); - breakpoint.update({ raw }); - return breakpoint; - } protected async updateBreakpoints(options: { uri?: URI, sourceModified: boolean diff --git a/packages/debug/src/browser/editor/debug-breakpoint-widget.tsx b/packages/debug/src/browser/editor/debug-breakpoint-widget.tsx new file mode 100644 index 0000000000000..8b2c559a0fecf --- /dev/null +++ b/packages/debug/src/browser/editor/debug-breakpoint-widget.tsx @@ -0,0 +1,222 @@ +/******************************************************************************** + * Copyright (C) 2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { injectable, postConstruct, inject } from 'inversify'; +import { Disposable, DisposableCollection } from '@theia/core'; +import URI from '@theia/core/lib/common/uri'; +import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider'; +import { MonacoEditorZoneWidget } from '@theia/monaco/lib/browser/monaco-editor-zone-widget'; +import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; +import { DebugEditor } from './debug-editor'; +import { DebugBreakpoint } from '../model/debug-breakpoint'; + +export type ShowDebugBreakpointOptions = DebugBreakpoint | { + position: monaco.Position, + context: DebugBreakpointWidget.Context +} | { + breakpoint: DebugBreakpoint, + context: DebugBreakpointWidget.Context +}; + +@injectable() +export class DebugBreakpointWidget implements Disposable { + + @inject(DebugEditor) + readonly editor: DebugEditor; + + @inject(MonacoEditorProvider) + protected readonly editorProvider: MonacoEditorProvider; + + protected selectNode: HTMLDivElement; + + protected zone: MonacoEditorZoneWidget; + + protected readonly toDispose = new DisposableCollection(); + + protected context: DebugBreakpointWidget.Context = 'condition'; + protected _values: { + [context in DebugBreakpointWidget.Context]?: string + } = {}; + get values(): { + [context in DebugBreakpointWidget.Context]?: string + } | undefined { + if (!this._input) { + return undefined; + } + return { + ...this._values, + [this.context]: this._input.getControl().getValue() + }; + } + + protected _input: MonacoEditor | undefined; + get input(): MonacoEditor | undefined { + return this._input; + } + + @postConstruct() + protected async init(): Promise { + this.toDispose.push(this.zone = new MonacoEditorZoneWidget(this.editor.getControl())); + this.zone.containerNode.classList.add('theia-debug-breakpoint-widget'); + + const selectNode = this.selectNode = document.createElement('div'); + selectNode.classList.add('theia-debug-breakpoint-select'); + this.zone.containerNode.appendChild(selectNode); + + const inputNode = document.createElement('div'); + inputNode.classList.add('theia-debug-breakpoint-input'); + this.zone.containerNode.appendChild(inputNode); + + const input = this._input = await this.createInput(inputNode); + if (this.toDispose.disposed) { + input.dispose(); + return; + } + this.toDispose.push(input); + this.toDispose.push(this.zone.onDidLayoutChange(dimension => this.layout(dimension))); + this.toDispose.push(input.getControl().onDidChangeModelContent(() => { + const heightInLines = input.getControl().getModel().getLineCount() + 1; + this.zone.layout(heightInLines); + this.updatePlaceholder(); + })); + this.toDispose.push(Disposable.create(() => ReactDOM.unmountComponentAtNode(selectNode))); + } + + dispose(): void { + this.toDispose.dispose(); + } + + get position(): monaco.Position | undefined { + const options = this.zone.options; + return options && new monaco.Position(options.afterLineNumber, options.afterColumn || -1); + } + + show(options: ShowDebugBreakpointOptions): void { + if (!this._input) { + return; + } + const breakpoint = options instanceof DebugBreakpoint ? options : 'breakpoint' in options ? options.breakpoint : undefined; + this._values = breakpoint ? { + condition: breakpoint.condition, + hitCondition: breakpoint.hitCondition, + logMessage: breakpoint.logMessage + } : {}; + if (options instanceof DebugBreakpoint) { + if (options.logMessage) { + this.context = 'logMessage'; + } else if (options.hitCondition && !options.condition) { + this.context = 'hitCondition'; + } else { + this.context = 'condition'; + } + } else { + this.context = options.context; + } + this.render(); + const position = 'position' in options ? options.position : undefined; + const afterLineNumber = breakpoint ? breakpoint.line : position!.lineNumber; + const afterColumn = breakpoint ? breakpoint.column : position!.column; + const editor = this._input.getControl(); + const heightInLines = editor.getModel().getLineCount() + 1; + this.zone.show({ afterLineNumber, afterColumn, heightInLines, frameWidth: 1 }); + editor.setPosition(editor.getModel().getPositionAt(editor.getModel().getValueLength())); + this._input.focus(); + } + + hide(): void { + this.zone.hide(); + this.editor.focus(); + } + + protected layout(dimension: monaco.editor.IDimension): void { + if (this._input) { + this._input.getControl().layout(dimension); + } + } + + protected createInput(node: HTMLElement): Promise { + return this.editorProvider.createInline(new URI().withScheme('breakpointinput').withPath(this.editor.getControl().getId()), node, { + autoSizing: false + }); + } + + protected render(): void { + if (this._input) { + this._input.getControl().setValue(this._values[this.context] || ''); + } + ReactDOM.render(, this.selectNode); + } + + protected renderOption(context: DebugBreakpointWidget.Context, label: string): JSX.Element { + return ; + } + protected readonly updateInput = (e: React.ChangeEvent) => { + if (this._input) { + this._values[this.context] = this._input.getControl().getValue(); + } + this.context = e.currentTarget.value as DebugBreakpointWidget.Context; + this.render(); + if (this._input) { + this._input.focus(); + } + } + + static PLACEHOLDER_DECORATION = 'placeholderDecoration'; + protected updatePlaceholder(): void { + if (!this._input) { + return; + } + const value = this._input.getControl().getValue(); + const decorations: monaco.editor.IDecorationOptions[] = !!value ? [] : [{ + range: { + startLineNumber: 0, + endLineNumber: 0, + startColumn: 0, + endColumn: 1 + }, + renderOptions: { + after: { + contentText: this.placeholder, + opacity: '0.4' + } + } + }]; + this._input.getControl().setDecorations(DebugBreakpointWidget.PLACEHOLDER_DECORATION, decorations); + } + protected get placeholder(): string { + if (this.context === 'logMessage') { + return "Message to log when breakpoint is hit. Expressions within {} are interpolated. 'Enter' to accept, 'esc' to cancel."; + } + if (this.context === 'hitCondition') { + return "Break when hit count condition is met. 'Enter' to accept, 'esc' to cancel."; + } + return "Break when expression evaluates to true. 'Enter' to accept, 'esc' to cancel."; + } + +} + +monaco.services.StaticServices.codeEditorService.get().registerDecorationType(DebugBreakpointWidget.PLACEHOLDER_DECORATION, {}); + +export namespace DebugBreakpointWidget { + export type Context = keyof Pick; +} diff --git a/packages/debug/src/browser/editor/debug-editor-model.ts b/packages/debug/src/browser/editor/debug-editor-model.ts index 62e1b34550eff..7267b96197870 100644 --- a/packages/debug/src/browser/editor/debug-editor-model.ts +++ b/packages/debug/src/browser/editor/debug-editor-model.ts @@ -25,19 +25,21 @@ import { DebugSessionManager } from '../debug-session-manager'; import { SourceBreakpoint } from '../breakpoint/breakpoint-marker'; import { DebugEditor } from './debug-editor'; import { DebugHoverWidget, createDebugHoverWidgetContainer } from './debug-hover-widget'; +import { DebugBreakpointWidget } from './debug-breakpoint-widget'; export const DebugEditorModelFactory = Symbol('DebugEditorModelFactory'); -export type DebugEditorModelFactory = (editor: monaco.editor.IStandaloneCodeEditor) => DebugEditorModel; +export type DebugEditorModelFactory = (editor: DebugEditor) => DebugEditorModel; @injectable() export class DebugEditorModel implements Disposable { - static createContainer(parent: interfaces.Container, editor: monaco.editor.IStandaloneCodeEditor): Container { + static createContainer(parent: interfaces.Container, editor: DebugEditor): Container { const child = createDebugHoverWidgetContainer(parent, editor); child.bind(DebugEditorModel).toSelf(); + child.bind(DebugBreakpointWidget).toSelf(); return child; } - static createModel(parent: interfaces.Container, editor: monaco.editor.IStandaloneCodeEditor): DebugEditorModel { + static createModel(parent: interfaces.Container, editor: DebugEditor): DebugEditorModel { return DebugEditorModel.createContainer(parent, editor).get(DebugEditorModel); } @@ -61,7 +63,7 @@ export class DebugEditorModel implements Disposable { readonly hover: DebugHoverWidget; @inject(DebugEditor) - readonly editor: monaco.editor.IStandaloneCodeEditor; + readonly editor: DebugEditor; @inject(BreakpointManager) readonly breakpoints: BreakpointManager; @@ -72,16 +74,20 @@ export class DebugEditorModel implements Disposable { @inject(ContextMenuRenderer) readonly contextMenu: ContextMenuRenderer; + @inject(DebugBreakpointWidget) + readonly breakpointWidget: DebugBreakpointWidget; + @postConstruct() protected init(): void { - this.uri = new URI(this.editor.getModel().uri.toString()); + this.uri = new URI(this.editor.getControl().getModel().uri.toString()); this.toDispose.pushAll([ this.hover, - this.editor.onMouseDown(event => this.handleMouseDown(event)), - this.editor.onMouseMove(event => this.handleMouseMove(event)), - this.editor.onMouseLeave(event => this.handleMouseLeave(event)), - this.editor.onKeyDown(() => this.hover.hide({ immediate: false })), - this.editor.getModel().onDidChangeDecorations(() => this.updateBreakpoints()), + this.breakpointWidget, + this.editor.getControl().onMouseDown(event => this.handleMouseDown(event)), + this.editor.getControl().onMouseMove(event => this.handleMouseMove(event)), + this.editor.getControl().onMouseLeave(event => this.handleMouseLeave(event)), + this.editor.getControl().onKeyDown(() => this.hover.hide({ immediate: false })), + this.editor.getControl().getModel().onDidChangeDecorations(() => this.updateBreakpoints()), this.sessions.onDidChange(() => this.renderFrames()) ]); this.renderFrames(); @@ -170,7 +176,7 @@ export class DebugEditorModel implements Disposable { protected updateBreakpointRanges(): void { this.breakpointRanges.clear(); for (const decoration of this.breakpointDecorations) { - const range = this.editor.getModel().getDecorationRange(decoration); + const range = this.editor.getControl().getModel().getDecorationRange(decoration); this.breakpointRanges.set(decoration, range); } } @@ -186,26 +192,15 @@ export class DebugEditorModel implements Disposable { protected createCurrentBreakpointDecoration(breakpoint: DebugBreakpoint): monaco.editor.IModelDeltaDecoration { const lineNumber = breakpoint.line; const range = new monaco.Range(lineNumber, 1, lineNumber, 1); - const options = this.createCurrentBreakpointDecorationOptions(breakpoint); - return { range, options }; - } - protected createCurrentBreakpointDecorationOptions(breakpoint: DebugBreakpoint): monaco.editor.IModelDecorationOptions { - if (breakpoint.installed) { - const decoration = breakpoint.verified ? DebugEditorModel.BREAKPOINT_DECORATION : DebugEditorModel.BREAKPOINT_UNVERIFIED_DECORATION; - if (breakpoint.message) { - return { - ...decoration, - glyphMarginHoverMessage: { - value: breakpoint.message - } - }; + const { className, message } = breakpoint.getDecoration(); + return { + range, + options: { + glyphMarginClassName: className, + glyphMarginHoverMessage: message.map(value => ({ value })), + stickiness: DebugEditorModel.STICKINESS } - return decoration; - } - if (breakpoint.enabled) { - return DebugEditorModel.BREAKPOINT_DECORATION; - } - return DebugEditorModel.BREAKPOINT_DISABLED_DECORATION; + }; } protected updateBreakpoints(): void { @@ -215,11 +210,11 @@ export class DebugEditorModel implements Disposable { } } protected areBreakpointsAffected(): boolean { - if (this.updatingDecorations || !this.editor.getModel()) { + if (this.updatingDecorations || !this.editor.getControl().getModel()) { return false; } for (const decoration of this.breakpointDecorations) { - const range = this.editor.getModel().getDecorationRange(decoration); + const range = this.editor.getControl().getModel().getDecorationRange(decoration); const oldRange = this.breakpointRanges.get(decoration)!; if (!range || !range.equalsRange(oldRange)) { return true; @@ -229,30 +224,25 @@ export class DebugEditorModel implements Disposable { } protected createBreakpoints(): SourceBreakpoint[] { const { uri } = this; - const uriString = uri.toString(); - const breakpoints = new Map(); + const lines = new Set(); + const breakpoints: SourceBreakpoint[] = []; for (const decoration of this.breakpointDecorations) { - const range = this.editor.getModel().getDecorationRange(decoration); - if (range && !breakpoints.has(range.startLineNumber)) { + const range = this.editor.getControl().getModel().getDecorationRange(decoration); + if (range && !lines.has(range.startLineNumber)) { const line = range.startLineNumber; const oldRange = this.breakpointRanges.get(decoration); const oldBreakpoint = oldRange && this.breakpoints.getBreakpoint(uri, oldRange.startLineNumber); - breakpoints.set(line, { - uri: uriString, - enabled: oldBreakpoint ? oldBreakpoint.enabled : true, - raw: { - line, - column: range.startColumn - } - }); + const breakpoint = SourceBreakpoint.create(uri, { line, column: 1 }, oldBreakpoint); + breakpoints.push(breakpoint); + lines.add(line); } } - return [...breakpoints.values()]; + return breakpoints; } protected _position: monaco.Position | undefined; get position(): monaco.Position { - return this._position || this.editor.getPosition(); + return this._position || this.editor.getControl().getPosition(); } get breakpoint(): DebugBreakpoint | undefined { return this.getBreakpoint(); @@ -268,7 +258,27 @@ export class DebugEditorModel implements Disposable { if (breakpoint) { breakpoint.remove(); } else { - this.breakpoints.addBreakpoint(this.uri, position.lineNumber, position.column); + this.breakpoints.addBreakpoint(SourceBreakpoint.create(this.uri, { + line: position.lineNumber, + column: 1 + })); + } + } + + acceptBreakpoint(): void { + const { position, values } = this.breakpointWidget; + if (position && values) { + const breakpoint = this.getBreakpoint(position); + if (breakpoint) { + breakpoint.updateOrigins(values); + } else { + this.breakpoints.addBreakpoint(SourceBreakpoint.create(this.uri, { + line: position.lineNumber, + column: 1, + ...values + })); + } + this.breakpointWidget.hide(); } } @@ -344,7 +354,7 @@ export class DebugEditorModel implements Disposable { protected deltaDecorations(oldDecorations: string[], newDecorations: monaco.editor.IModelDeltaDecoration[]): string[] { this.updatingDecorations = true; try { - return this.editor.getModel().deltaDecorations(oldDecorations, newDecorations); + return this.editor.getControl().getModel().deltaDecorations(oldDecorations, newDecorations); } finally { this.updatingDecorations = false; } @@ -352,27 +362,6 @@ export class DebugEditorModel implements Disposable { static STICKINESS = monaco.editor.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; - static BREAKPOINT_DECORATION: monaco.editor.IModelDecorationOptions = { - glyphMarginClassName: 'theia-debug-breakpoint', - glyphMarginHoverMessage: { - value: 'Breakpoint' - }, - stickiness: DebugEditorModel.STICKINESS - }; - static BREAKPOINT_DISABLED_DECORATION: monaco.editor.IModelDecorationOptions = { - glyphMarginClassName: 'theia-debug-breakpoint-disabled', - glyphMarginHoverMessage: { - value: 'Disabled Breakpoint' - }, - stickiness: DebugEditorModel.STICKINESS - }; - static BREAKPOINT_UNVERIFIED_DECORATION: monaco.editor.IModelDecorationOptions = { - glyphMarginClassName: 'theia-debug-breakpoint-unverified', - glyphMarginHoverMessage: { - value: 'Unverified Breakpoint' - }, - stickiness: DebugEditorModel.STICKINESS - }; static BREAKPOINT_HINT_DECORATION: monaco.editor.IModelDecorationOptions = { glyphMarginClassName: 'theia-debug-breakpoint-hint', stickiness: DebugEditorModel.STICKINESS diff --git a/packages/debug/src/browser/editor/debug-editor-service.ts b/packages/debug/src/browser/editor/debug-editor-service.ts index b7f74a440efce..40adbeaa9ffe7 100644 --- a/packages/debug/src/browser/editor/debug-editor-service.ts +++ b/packages/debug/src/browser/editor/debug-editor-service.ts @@ -21,8 +21,9 @@ import { ContextMenuRenderer } from '@theia/core/lib/browser'; import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { DebugSessionManager } from '../debug-session-manager'; import { DebugEditorModel, DebugEditorModelFactory } from './debug-editor-model'; -import { BreakpointManager } from '../breakpoint/breakpoint-manager'; +import { BreakpointManager, BreakpointsChangeEvent } from '../breakpoint/breakpoint-manager'; import { DebugBreakpoint } from '../model/debug-breakpoint'; +import { DebugBreakpointWidget } from './debug-breakpoint-widget'; @injectable() export class DebugEditorService { @@ -53,6 +54,7 @@ export class DebugEditorService { this.render(uri); } }); + this.breakpoints.onDidChangeBreakpoints(event => this.closeBreakpointIfAffected(event)); } protected push(widget: EditorWidget): void { @@ -61,7 +63,7 @@ export class DebugEditorService { return; } const uri = editor.getControl().getModel().uri.toString(); - const debugModel = this.factory(editor.getControl()); + const debugModel = this.factory(editor); this.models.set(uri, debugModel); editor.getControl().onDidDispose(() => { debugModel.dispose(); @@ -81,41 +83,109 @@ export class DebugEditorService { const uri = currentEditor && currentEditor.getResourceUri(); return uri && this.models.get(uri.toString()); } + + get logpoint(): DebugBreakpoint | undefined { + const logpoint = this.anyBreakpoint; + return logpoint && logpoint.logMessage ? logpoint : undefined; + } + get logpointEnabled(): boolean | undefined { + const { logpoint } = this; + return logpoint && logpoint.enabled; + } + get breakpoint(): DebugBreakpoint | undefined { - const { model } = this; - return model && model.breakpoint; + const breakpoint = this.anyBreakpoint; + return breakpoint && breakpoint.logMessage ? undefined : breakpoint; + } + get breakpointEnabled(): boolean | undefined { + const { breakpoint } = this; + return breakpoint && breakpoint.enabled; + } + + get anyBreakpoint(): DebugBreakpoint | undefined { + return this.model && this.model.breakpoint; } + toggleBreakpoint(): void { const { model } = this; if (model) { model.toggleBreakpoint(); } } - get breakpointEnabled(): boolean | undefined { - const { breakpoint } = this; - return breakpoint && breakpoint.enabled; - } setBreakpointEnabled(enabled: boolean): void { - const { breakpoint } = this; - if (breakpoint) { - breakpoint.setEnabled(enabled); + const { anyBreakpoint } = this; + if (anyBreakpoint) { + anyBreakpoint.setEnabled(enabled); } } showHover(): void { const { model } = this; if (model) { - const selection = model.editor.getSelection(); + const selection = model.editor.getControl().getSelection(); model.hover.show({ selection, focus: true }); } } canShowHover(): boolean { const { model } = this; if (model) { - const selection = model.editor.getSelection(); - return !!model.editor.getModel().getWordAtPosition(selection.getStartPosition()); + const selection = model.editor.getControl().getSelection(); + return !!model.editor.getControl().getModel().getWordAtPosition(selection.getStartPosition()); } return false; } + addBreakpoint(context: DebugBreakpointWidget.Context): void { + const { model } = this; + if (model) { + const { breakpoint } = model; + if (breakpoint) { + model.breakpointWidget.show({ breakpoint, context }); + } else { + model.breakpointWidget.show({ + position: model.position, + context + }); + } + } + } + editBreakpoint(): Promise; + editBreakpoint(breakpoint: DebugBreakpoint): Promise; + async editBreakpoint(breakpoint: DebugBreakpoint | undefined = this.anyBreakpoint): Promise { + if (breakpoint) { + await breakpoint.open(); + const model = this.models.get(breakpoint.uri.toString()); + if (model) { + model.breakpointWidget.show(breakpoint); + } + } + } + closeBreakpoint(): void { + const { model } = this; + if (model) { + model.breakpointWidget.hide(); + } + } + acceptBreakpoint(): void { + const { model } = this; + if (model) { + model.acceptBreakpoint(); + } + } + protected closeBreakpointIfAffected({ uri, removed }: BreakpointsChangeEvent) { + const model = this.models.get(uri.toString()); + if (!model) { + return; + } + const position = model.breakpointWidget.position; + if (!position) { + return; + } + for (const breakpoint of removed) { + if (breakpoint.raw.line === position.lineNumber) { + model.breakpointWidget.hide(); + } + } + } + } diff --git a/packages/debug/src/browser/editor/debug-editor.ts b/packages/debug/src/browser/editor/debug-editor.ts index 66f7ca356a1cf..549ed88ec31e1 100644 --- a/packages/debug/src/browser/editor/debug-editor.ts +++ b/packages/debug/src/browser/editor/debug-editor.ts @@ -14,4 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; + export const DebugEditor = Symbol('DebugEditor'); +export type DebugEditor = MonacoEditor; diff --git a/packages/debug/src/browser/editor/debug-hover-widget.ts b/packages/debug/src/browser/editor/debug-hover-widget.ts index 6570ef1d2fb4a..d85913439ca30 100644 --- a/packages/debug/src/browser/editor/debug-hover-widget.ts +++ b/packages/debug/src/browser/editor/debug-hover-widget.ts @@ -40,7 +40,7 @@ export interface HideDebugHoverOptions { immediate?: boolean } -export function createDebugHoverWidgetContainer(parent: interfaces.Container, editor: monaco.editor.IStandaloneCodeEditor): Container { +export function createDebugHoverWidgetContainer(parent: interfaces.Container, editor: DebugEditor): Container { const child = SourceTreeWidget.createContainer(parent, { virtualized: false }); @@ -58,7 +58,7 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. protected readonly toDispose = new DisposableCollection(); @inject(DebugEditor) - protected readonly editor: monaco.editor.IStandaloneCodeEditor; + protected readonly editor: DebugEditor; @inject(DebugSessionManager) protected readonly sessions: DebugSessionManager; @@ -92,11 +92,11 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. this.contentNode.className = 'theia-debug-hover-content'; this.domNode.appendChild(this.contentNode); - this.editor.addContentWidget(this); + this.editor.getControl().addContentWidget(this); this.source = this.hoverSource; this.toDispose.pushAll([ this.hoverSource, - Disposable.create(() => this.editor.removeContentWidget(this)), + Disposable.create(() => this.editor.getControl().removeContentWidget(this)), Disposable.create(() => this.hide()), this.sessions.onDidChange(() => { if (!this.isEditorFrame()) { @@ -132,7 +132,7 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. return; } if (this.domNode.contains(document.activeElement)) { - this.editor.focus(); + this.editor.getControl().focus(); } if (this.isAttached) { Widget.detach(this); @@ -140,7 +140,7 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. this.hoverSource.reset(); super.hide(); this.options = undefined; - this.editor.layoutContentWidget(this); + this.editor.getControl().layoutContentWidget(this); } protected async doShow(options: ShowDebugHoverOptions | undefined = this.options): Promise { if (!this.isEditorFrame()) { @@ -159,7 +159,7 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. } super.show(); this.options = options; - const expression = this.expressionProvider.get(this.editor.getModel(), options.selection); + const expression = this.expressionProvider.get(this.editor.getControl().getModel(), options.selection); if (!expression) { this.hide(); return; @@ -176,12 +176,12 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. this.hide(); return; } - this.editor.layoutContentWidget(this); + this.editor.getControl().layoutContentWidget(this); } protected isEditorFrame(): boolean { const { currentFrame } = this.sessions; return !!currentFrame && !!currentFrame.source && - this.editor.getModel().uri.toString() === currentFrame.source.uri.toString(); + this.editor.getControl().getModel().uri.toString() === currentFrame.source.uri.toString(); } getPosition(): monaco.editor.IContentWidgetPosition { @@ -189,7 +189,7 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. return undefined!; } const position = this.options && this.options.selection.getStartPosition(); - const word = position && this.editor.getModel().getWordAtPosition(position); + const word = position && this.editor.getControl().getModel().getWordAtPosition(position); return position && word ? { position: new monaco.Position(position.lineNumber, word.startColumn), preference: [ diff --git a/packages/debug/src/browser/model/debug-breakpoint.tsx b/packages/debug/src/browser/model/debug-breakpoint.tsx index 622098df01359..feb54482535b2 100644 --- a/packages/debug/src/browser/model/debug-breakpoint.tsx +++ b/packages/debug/src/browser/model/debug-breakpoint.tsx @@ -31,6 +31,11 @@ export class DebugBreakpointData { readonly origins: SourceBreakpoint[]; } +export class DebugBreakpointDecoration { + readonly className: string; + readonly message: string[]; +} + export class DebugBreakpoint extends DebugBreakpointData implements TreeElement { readonly uri: URI; @@ -54,7 +59,11 @@ export class DebugBreakpoint extends DebugBreakpointData implements TreeElement return this.origins[0]; } - get id(): number | undefined { + get id(): string { + return this.origin.id; + } + + get idFromAdapter(): number | undefined { return this.raw && this.raw.id; } @@ -81,6 +90,22 @@ export class DebugBreakpoint extends DebugBreakpointData implements TreeElement } } + updateOrigins(data: Partial): void { + const breakpoints = this.breakpoints.getBreakpoints(this.uri); + let shouldUpdate = false; + const originLines = new Set(); + this.origins.forEach(origin => originLines.add(origin.raw.line)); + for (const breakpoint of breakpoints) { + if (originLines.has(breakpoint.raw.line)) { + Object.assign(breakpoint.raw, data); + shouldUpdate = true; + } + } + if (shouldUpdate) { + this.breakpoints.setBreakpoints(this.uri, breakpoints); + } + } + get installed(): boolean { return !!this.raw; } @@ -106,6 +131,16 @@ export class DebugBreakpoint extends DebugBreakpointData implements TreeElement return this.raw && this.raw.endColumn; } + get condition(): string | undefined { + return this.origin.raw.condition; + } + get hitCondition(): string | undefined { + return this.origin.raw.hitCondition; + } + get logMessage(): string | undefined { + return this.origin.raw.logMessage; + } + get source(): DebugSource | undefined { return this.raw && this.raw.source && this.session && this.session.getSource(this.raw.source); } @@ -132,7 +167,7 @@ export class DebugBreakpoint extends DebugBreakpointData implements TreeElement selection }); } else { - this.editorManager.open(this.uri, { + await this.editorManager.open(this.uri, { ...options, selection }); @@ -148,7 +183,9 @@ export class DebugBreakpoint extends DebugBreakpointData implements TreeElement if (!this.breakpoints.breakpointsEnabled || !this.verified) { classNames.push(DISABLED_CLASS); } - return
+ const decoration = this.getDecoration(); + return
+ {this.labelProvider.getName(this.uri)} {this.labelProvider.getLongName(this.uri.parent)} @@ -156,6 +193,84 @@ export class DebugBreakpoint extends DebugBreakpointData implements TreeElement
; } + getDecoration(): DebugBreakpointDecoration { + if (!this.enabled) { + return this.getDisabledBreakpointDecoration(); + } + if (this.installed && !this.verified) { + return this.getUnverifiedBreakpointDecoration(); + } + const messages: string[] = []; + if (this.logMessage || this.condition || this.hitCondition) { + const { session } = this; + if (this.logMessage) { + if (session && !session.capabilities.supportsLogPoints) { + return this.getUnsupportedBreakpointDecoration('Logpoints not supported by this debug type'); + } + messages.push('Log Message: ' + this.logMessage); + } + if (this.condition) { + if (session && !session.capabilities.supportsConditionalBreakpoints) { + return this.getUnsupportedBreakpointDecoration('Conditional breakpoints not supported by this debug type'); + } + messages.push('Expression: ' + this.condition); + } + if (this.hitCondition) { + if (session && !session.capabilities.supportsHitConditionalBreakpoints) { + return this.getUnsupportedBreakpointDecoration('Hit conditional breakpoints not supported by this debug type'); + } + messages.push('Hit Count: ' + this.hitCondition); + } + } + if (this.message) { + if (messages.length) { + messages[messages.length - 1].concat(', ' + this.message); + } else { + messages.push(this.message); + } + } + return this.getBreakpointDecoration(messages); + } + protected getUnverifiedBreakpointDecoration(): DebugBreakpointDecoration { + const decoration = this.getBreakpointDecoration(); + return { + className: decoration.className + '-unverified', + message: [this.message || 'Unverified ' + decoration.message[0]] + }; + } + protected getDisabledBreakpointDecoration(): DebugBreakpointDecoration { + const decoration = this.getBreakpointDecoration(); + return { + className: decoration.className + '-disabled', + message: ['Disabled ' + decoration.message[0]] + }; + } + + protected getBreakpointDecoration(message?: string[]): DebugBreakpointDecoration { + if (this.logMessage) { + return { + className: 'theia-debug-logpoint', + message: message || ['Logpoint'] + }; + } + if (this.condition || this.hitCondition) { + return { + className: 'theia-debug-conditional-breakpoint', + message: message || ['Conditional Breakpoint'] + }; + } + return { + className: 'theia-debug-breakpoint', + message: message || ['Breakpoint'] + }; + } + protected getUnsupportedBreakpointDecoration(message: string): DebugBreakpointDecoration { + return { + className: 'theia-debug-breakpoint-unsupported', + message: [message] + }; + } + remove(): void { const breakpoints = this.doRemove(this.origins); if (breakpoints) { diff --git a/packages/debug/src/browser/monaco.d.ts b/packages/debug/src/browser/monaco.d.ts new file mode 100644 index 0000000000000..0460bfc958810 --- /dev/null +++ b/packages/debug/src/browser/monaco.d.ts @@ -0,0 +1,17 @@ +/******************************************************************************** + * Copyright (C) 2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/// diff --git a/packages/debug/src/browser/style/breakpoint-conditional-disabled.svg b/packages/debug/src/browser/style/breakpoint-conditional-disabled.svg new file mode 100644 index 0000000000000..56bfda5b1f6c6 --- /dev/null +++ b/packages/debug/src/browser/style/breakpoint-conditional-disabled.svg @@ -0,0 +1,4 @@ + + + +breakpoint-conditional-disabled diff --git a/packages/debug/src/browser/style/breakpoint-conditional-unverified.svg b/packages/debug/src/browser/style/breakpoint-conditional-unverified.svg new file mode 100644 index 0000000000000..878dc370c1036 --- /dev/null +++ b/packages/debug/src/browser/style/breakpoint-conditional-unverified.svg @@ -0,0 +1,4 @@ + + + +breakpoint-conditional-unverified diff --git a/packages/debug/src/browser/style/breakpoint-conditional.svg b/packages/debug/src/browser/style/breakpoint-conditional.svg new file mode 100644 index 0000000000000..0083ea775cbc3 --- /dev/null +++ b/packages/debug/src/browser/style/breakpoint-conditional.svg @@ -0,0 +1,4 @@ + + + +breakpoint-conditional diff --git a/packages/debug/src/browser/style/breakpoint-log-disabled.svg b/packages/debug/src/browser/style/breakpoint-log-disabled.svg new file mode 100644 index 0000000000000..f55ccf53a5da1 --- /dev/null +++ b/packages/debug/src/browser/style/breakpoint-log-disabled.svg @@ -0,0 +1,4 @@ + + + +breakpoint-log-disabled diff --git a/packages/debug/src/browser/style/breakpoint-log-unverified.svg b/packages/debug/src/browser/style/breakpoint-log-unverified.svg new file mode 100644 index 0000000000000..56ab5bd800ede --- /dev/null +++ b/packages/debug/src/browser/style/breakpoint-log-unverified.svg @@ -0,0 +1,4 @@ + + + +breakpoint-log-unverified diff --git a/packages/debug/src/browser/style/breakpoint-log.svg b/packages/debug/src/browser/style/breakpoint-log.svg new file mode 100644 index 0000000000000..43ffb51fcb139 --- /dev/null +++ b/packages/debug/src/browser/style/breakpoint-log.svg @@ -0,0 +1,4 @@ + + + +breakpoint-log diff --git a/packages/debug/src/browser/style/breakpoint-unsupported.svg b/packages/debug/src/browser/style/breakpoint-unsupported.svg new file mode 100644 index 0000000000000..109a7e71990ab --- /dev/null +++ b/packages/debug/src/browser/style/breakpoint-unsupported.svg @@ -0,0 +1,4 @@ + + + +breakpoint-unsupported diff --git a/packages/debug/src/browser/style/index.css b/packages/debug/src/browser/style/index.css index c6c4068348537..d4303c3d41ec7 100644 --- a/packages/debug/src/browser/style/index.css +++ b/packages/debug/src/browser/style/index.css @@ -196,15 +196,46 @@ .monaco-editor .theia-debug-breakpoint-hint { background: url('breakpoint-hint.svg') center center no-repeat; } -.monaco-editor .theia-debug-breakpoint { + +.theia-debug-breakpoint-icon { + width: 19px; + height: 19px; + min-width: 19px; + margin-left: 0px !important; +} + +.theia-debug-breakpoint { background: url('breakpoint.svg') center center no-repeat; } -.monaco-editor .theia-debug-breakpoint-disabled { +.theia-debug-breakpoint-disabled { background: url('breakpoint-disabled.svg') center center no-repeat; } -.monaco-editor .theia-debug-breakpoint-unverified { +.theia-debug-breakpoint-unverified { background: url('breakpoint-unverified.svg') center center no-repeat; } +.theia-debug-breakpoint-unsupported { + background: url('breakpoint-unsupported.svg') center center no-repeat; +} + +.theia-debug-conditional-breakpoint { + background: url('breakpoint-conditional.svg') center center no-repeat; +} +.theia-debug-conditional-breakpoint-disabled { + background: url('breakpoint-conditional-disabled.svg') center center no-repeat; +} +.theia-debug-conditional-breakpoint-unverified { + background: url('breakpoint-conditional-unverified.svg') center center no-repeat; +} + +.theia-debug-logpoint { + background: url('breakpoint-log.svg') center center no-repeat; +} +.theia-debug-logpoint-disabled { + background: url('breakpoint-log-disabled.svg') center center no-repeat; +} +.theia-debug-logpoint-unverified { + background: url('breakpoint-log-unverified.svg') center center no-repeat; +} .monaco-editor .theia-debug-top-stack-frame { background: url('current-arrow.svg') center center no-repeat; @@ -307,3 +338,22 @@ .theia-debug-widget-container .theia-header { margin-top: 5px; } + +/** Breakpoint Widget */ +.theia-debug-breakpoint-widget { + display: flex; +} + +.theia-debug-breakpoint-select { + display: flex; + justify-content: center; + flex-direction: column; + padding: 0 10px; + flex-shrink: 0; +} + +.theia-debug-breakpoint-input { + flex: 1; + margin-top: var(--theia-ui-padding); + margin-bottom: var(--theia-ui-padding); +} diff --git a/packages/debug/src/browser/view/debug-breakpoints-widget.ts b/packages/debug/src/browser/view/debug-breakpoints-widget.ts index 6bdb98c97eb74..6adb32a25dffb 100644 --- a/packages/debug/src/browser/view/debug-breakpoints-widget.ts +++ b/packages/debug/src/browser/view/debug-breakpoints-widget.ts @@ -27,8 +27,9 @@ import { DebugViewModel } from './debug-view-model'; export class DebugBreakpointsWidget extends SourceTreeWidget implements ViewContainerPartWidget { static CONTEXT_MENU: MenuPath = ['debug-breakpoints-context-menu']; - static REMOVE_MENU = [...DebugBreakpointsWidget.CONTEXT_MENU, 'a_remove']; - static ENABLE_MENU = [...DebugBreakpointsWidget.CONTEXT_MENU, 'b_enable']; + static EDIT_MENU = [...DebugBreakpointsWidget.CONTEXT_MENU, 'a_edit']; + static REMOVE_MENU = [...DebugBreakpointsWidget.CONTEXT_MENU, 'b_remove']; + static ENABLE_MENU = [...DebugBreakpointsWidget.CONTEXT_MENU, 'c_enable']; static createContainer(parent: interfaces.Container): Container { const child = SourceTreeWidget.createContainer(parent, { contextMenuPath: DebugBreakpointsWidget.CONTEXT_MENU, diff --git a/packages/monaco/src/browser/monaco-editor-zone-widget.ts b/packages/monaco/src/browser/monaco-editor-zone-widget.ts new file mode 100644 index 0000000000000..e5d5831d81551 --- /dev/null +++ b/packages/monaco/src/browser/monaco-editor-zone-widget.ts @@ -0,0 +1,177 @@ +/******************************************************************************** + * Copyright (C) 2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + /*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation and others. All rights reserved. + * Licensed under the MIT License. See https://github.com/Microsoft/vscode/blob/master/LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableCollection, Event, Emitter } from '@theia/core'; + +export interface MonacoEditorViewZone extends monaco.editor.IViewZone { + id: number +} + +export class MonacoEditorZoneWidget implements Disposable { + + readonly zoneNode = document.createElement('div'); + readonly containerNode = document.createElement('div'); + + protected readonly onDidLayoutChangeEmitter = new Emitter(); + readonly onDidLayoutChange: Event = this.onDidLayoutChangeEmitter.event; + + protected viewZone: MonacoEditorViewZone | undefined; + + protected readonly toHide = new DisposableCollection(); + + protected readonly toDispose = new DisposableCollection( + this.onDidLayoutChangeEmitter, + this.toHide + ); + + constructor( + readonly editor: monaco.editor.IStandaloneCodeEditor + ) { + this.zoneNode.classList.add('zone-widget'); + this.containerNode.classList.add('zone-widget-container'); + this.zoneNode.appendChild(this.containerNode); + this.updateWidth(); + this.toDispose.push(this.editor.onDidLayoutChange(info => this.updateWidth(info))); + } + + dispose(): void { + this.toDispose.dispose(); + } + + protected _options: MonacoEditorZoneWidget.Options | undefined; + get options(): MonacoEditorZoneWidget.Options | undefined { + return this.viewZone ? this._options : undefined; + } + + hide(): void { + this.toHide.dispose(); + } + + show(options: MonacoEditorZoneWidget.Options): void { + let { afterLineNumber, afterColumn, heightInLines } = this._options = { showFrame: true, ...options }; + const lineHeight = this.editor.getConfiguration().lineHeight; + const maxHeightInLines = (this.editor.getLayoutInfo().height / lineHeight) * .8; + if (heightInLines >= maxHeightInLines) { + heightInLines = maxHeightInLines; + } + this.toHide.dispose(); + this.editor.changeViewZones(accessor => { + this.zoneNode.style.top = '-1000px'; + const domNode = document.createElement('div'); + domNode.style.overflow = 'hidden'; + const zone: monaco.editor.IViewZone = { + domNode, + afterLineNumber, + afterColumn, + heightInLines, + onDomNodeTop: zoneTop => this.updateTop(zoneTop), + onComputedHeight: zoneHeight => this.updateHeight(zoneHeight) + }; + this.viewZone = Object.assign(zone, { + id: accessor.addZone(zone) + }); + const id = this.viewZone.id; + this.toHide.push(Disposable.create(() => { + this.editor.changeViewZones(a => a.removeZone(id)); + this.viewZone = undefined; + })); + const widget: monaco.editor.IOverlayWidget = { + getId: () => 'editor-zone-widget-' + id, + getDomNode: () => this.zoneNode, + // tslint:disable-next-line:no-null-keyword + getPosition: () => null! + }; + this.editor.addOverlayWidget(widget); + this.toHide.push(Disposable.create(() => this.editor.removeOverlayWidget(widget))); + }); + + this.containerNode.style.top = 0 + 'px'; + this.containerNode.style.overflow = 'hidden'; + this.updateContainerHeight(heightInLines * lineHeight); + + const model = this.editor.getModel(); + if (model) { + const revealLineNumber = Math.min(model.getLineCount(), Math.max(1, afterLineNumber + 1)); + this.editor.revealLine(revealLineNumber, monaco.editor.ScrollType.Smooth); + } + } + + layout(heightInLines: number): void { + if (this.viewZone && this.viewZone.heightInLines !== heightInLines) { + this.viewZone.heightInLines = heightInLines; + const id = this.viewZone.id; + this.editor.changeViewZones(accessor => accessor.layoutZone(id)); + } + } + + protected updateTop(top: number): void { + this.zoneNode.style.top = top + 'px'; + } + protected updateHeight(zoneHeight: number): void { + this.zoneNode.style.height = zoneHeight + 'px'; + this.updateContainerHeight(zoneHeight); + } + protected updateContainerHeight(zoneHeight: number): void { + const { frameWidth, height } = this.computeContainerHeight(zoneHeight); + this.containerNode.style.height = height + 'px'; + this.containerNode.style.borderTopWidth = frameWidth + 'px'; + this.containerNode.style.borderBottomWidth = frameWidth + 'px'; + const width = this.computeWidth(); + this.onDidLayoutChangeEmitter.fire({ height, width }); + } + protected computeContainerHeight(zoneHeight: number): { + height: number, + frameWidth: number + } { + const lineHeight = this.editor.getConfiguration().lineHeight; + const frameWidth = this._options && this._options.frameWidth; + const frameThickness = this._options && this._options.showFrame ? Math.round(lineHeight / 9) : 0; + return { + frameWidth: frameWidth !== undefined ? frameWidth : frameThickness, + height: zoneHeight - 2 * frameThickness + }; + } + + protected updateWidth(info: monaco.editor.EditorLayoutInfo = this.editor.getLayoutInfo()): void { + const width = this.computeWidth(info); + this.zoneNode.style.width = width + 'px'; + this.zoneNode.style.left = this.computeLeft(info) + 'px'; + } + protected computeWidth(info: monaco.editor.EditorLayoutInfo = this.editor.getLayoutInfo()): number { + return info.width - info.minimapWidth - info.verticalScrollbarWidth; + } + protected computeLeft(info: monaco.editor.EditorLayoutInfo = this.editor.getLayoutInfo()): number { + // If minimap is to the left, we move beyond it + if (info.minimapWidth > 0 && info.minimapLeft === 0) { + return info.minimapWidth; + } + return 0; + } + +} +export namespace MonacoEditorZoneWidget { + export interface Options { + afterLineNumber: number, + afterColumn?: number, + heightInLines: number, + showFrame?: boolean, + frameWidth?: number + } +} diff --git a/packages/monaco/src/browser/style/index.css b/packages/monaco/src/browser/style/index.css index 1c051b1ef67ca..11be890cc101c 100644 --- a/packages/monaco/src/browser/style/index.css +++ b/packages/monaco/src/browser/style/index.css @@ -59,3 +59,18 @@ color: var(--theia-ui-font-color2); font-size: calc(var(--theia-ui-font-size0) * 0.95); } + +.monaco-editor .zone-widget { + position: absolute; + z-index: 10; +} + +.monaco-editor .zone-widget .zone-widget-container { + border-top-style: solid; + border-bottom-style: solid; + border-top-width: 0; + border-bottom-width: 0; + border-top-color: var(--theia-accent-color2); + border-bottom-color: var(--theia-accent-color2); + position: relative; +} diff --git a/packages/monaco/src/typings/monaco/index.d.ts b/packages/monaco/src/typings/monaco/index.d.ts index b6060d0ae234d..b0a2d2b10f7bd 100644 --- a/packages/monaco/src/typings/monaco/index.d.ts +++ b/packages/monaco/src/typings/monaco/index.d.ts @@ -213,6 +213,7 @@ declare module monaco.editor { fontWeight?: string; textDecoration?: string; color?: string | ThemeColor; + opacity?: string; backgroundColor?: string | ThemeColor; margin?: string; diff --git a/packages/plugin-ext/src/api/model.ts b/packages/plugin-ext/src/api/model.ts index d23ada4c754d0..7280a01c0bb0a 100644 --- a/packages/plugin-ext/src/api/model.ts +++ b/packages/plugin-ext/src/api/model.ts @@ -410,6 +410,7 @@ export interface WorkspaceFolder { } export interface Breakpoint { + readonly id: string; readonly enabled: boolean; readonly condition?: string; readonly hitCondition?: string; diff --git a/packages/plugin-ext/src/main/browser/debug/debug-main.ts b/packages/plugin-ext/src/main/browser/debug/debug-main.ts index a998454980291..2901032799d0c 100644 --- a/packages/plugin-ext/src/main/browser/debug/debug-main.ts +++ b/packages/plugin-ext/src/main/browser/debug/debug-main.ts @@ -29,7 +29,7 @@ import { LabelProvider } from '@theia/core/lib/browser'; import { EditorManager } from '@theia/editor/lib/browser'; import { BreakpointManager } from '@theia/debug/lib/browser/breakpoint/breakpoint-manager'; import { DebugBreakpoint } from '@theia/debug/lib/browser/model/debug-breakpoint'; -import URI from 'vscode-uri'; +import Uri from 'vscode-uri'; import { DebugConsoleSession } from '@theia/debug/lib/browser/console/debug-console-session'; import { SourceBreakpoint } from '@theia/debug/lib/browser/breakpoint/breakpoint-marker'; import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration'; @@ -85,11 +85,15 @@ export class DebugMainImpl implements DebugMain { this.sessionContributionRegistrator = container.get(PluginDebugSessionContributionRegistry); this.debugSchemaUpdater = container.get(DebugSchemaUpdater); - // TODO: distinguish added/deleted breakpoints - this.breakpointsManager.onDidChangeMarkers(uri => { + this.breakpointsManager.onDidChangeBreakpoints(({ added, removed, changed }) => { + // TODO can we get rid of all to reduce amount of data set each time, should not it be possible to recover on another side from deltas? const all = this.breakpointsManager.getBreakpoints(); - const affected = this.breakpointsManager.getBreakpoints(uri); - this.debugExt.$breakpointsDidChange(this.toTheiaPluginApiBreakpoints(all), [], [], this.toTheiaPluginApiBreakpoints(affected)); + this.debugExt.$breakpointsDidChange( + this.toTheiaPluginApiBreakpoints(all), + this.toTheiaPluginApiBreakpoints(added), + this.toTheiaPluginApiBreakpoints(removed), + this.toTheiaPluginApiBreakpoints(changed) + ); }); this.sessionManager.onDidCreateDebugSession(debugSession => this.debugExt.$sessionDidCreate(debugSession.id)); @@ -153,11 +157,43 @@ export class DebugMainImpl implements DebugMain { } async $addBreakpoints(breakpoints: Breakpoint[]): Promise { - this.sessionManager.addBreakpoints(this.toInternalBreakpoints(breakpoints)); + const newBreakpoints = new Map(); + breakpoints.forEach(b => newBreakpoints.set(b.id, b)); + this.breakpointsManager.findMarkers({ + dataFilter: data => { + // install only new breakpoints + if (newBreakpoints.has(data.id)) { + newBreakpoints.delete(data.id); + } + return false; + } + }); + for (const breakpoint of newBreakpoints.values()) { + if (breakpoint.location) { + const location = breakpoint.location; + this.breakpointsManager.addBreakpoint({ + id: breakpoint.id, + uri: Uri.revive(location.uri).toString(), + enabled: true, + raw: { + line: breakpoint.location.range.startLineNumber + 1, + column: 1, + condition: breakpoint.condition, + hitCondition: breakpoint.hitCondition, + logMessage: breakpoint.logMessage + } + }); + } + } } async $removeBreakpoints(breakpoints: Breakpoint[]): Promise { - this.sessionManager.deleteBreakpoints(this.toInternalBreakpoints(breakpoints)); + const ids = new Set(); + breakpoints.forEach(b => ids.add(b.id)); + for (const origin of this.breakpointsManager.findMarkers({ dataFilter: data => ids.has(data.id) })) { + const breakpoint = new DebugBreakpoint(origin.data, this.labelProvider, this.breakpointsManager, this.editorManager, this.sessionManager.currentSession); + breakpoint.remove(); + } } async $customRequest(sessionId: string, command: string, args?: any): Promise { @@ -189,59 +225,28 @@ export class DebugMainImpl implements DebugMain { const session = await this.sessionManager.start({ configuration, - workspaceFolderUri: folder && URI.revive(folder.uri).toString() + workspaceFolderUri: folder && Uri.revive(folder.uri).toString() }); return !!session; } - private toInternalBreakpoints(breakpoints: Breakpoint[]): DebugBreakpoint[] { - return breakpoints - .filter(breakpoint => !!breakpoint.location) - .map(breakpoint => { - const location = breakpoint.location!; - const uri = URI.revive(location.uri); - const uriString = uri.toString(); - - const origin = { - uri: uriString, - enabled: true, - raw: { - line: location.range.startLineNumber, - column: location.range.startColumn, - condition: breakpoint.condition, - hitCondition: breakpoint.hitCondition, - logMessage: breakpoint.logMessage - } - }; - - return new DebugBreakpoint(origin, - this.labelProvider, - this.breakpointsManager, - this.editorManager, - this.sessionManager.currentSession); - }); - } - private toTheiaPluginApiBreakpoints(sourceBreakpoints: SourceBreakpoint[]): Breakpoint[] { - return sourceBreakpoints.map(b => { - const breakpoint = { - enabled: b.enabled, - condition: b.raw.condition, - hitCondition: b.raw.hitCondition, - logMessage: b.raw.logMessage, - location: { - uri: URI.parse(b.uri), - range: { - startLineNumber: b.raw.line, - startColumn: b.raw.column || 0, - endLineNumber: b.raw.line, - endColumn: b.raw.column || 0 - } + return sourceBreakpoints.map(b => ({ + id: b.id, + enabled: b.enabled, + condition: b.raw.condition, + hitCondition: b.raw.hitCondition, + logMessage: b.raw.logMessage, + location: { + uri: Uri.parse(b.uri), + range: { + startLineNumber: b.raw.line - 1, + startColumn: (b.raw.column || 1) - 1, + endLineNumber: b.raw.line - 1, + endColumn: (b.raw.column || 1) - 1 } - }; - - return breakpoint; - }); + } + })); } } diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index eb8e10cec1e9a..652d3a44d3362 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -14,6 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { UUID } from '@phosphor/coreutils/lib/uuid'; import { illegalArgument } from '../common/errors'; import * as theia from '@theia/plugin'; import * as crypto from 'crypto'; @@ -1681,6 +1682,18 @@ export class Breakpoint { this.hitCondition = hitCondition; this.logMessage = logMessage; } + + private _id: string | undefined; + /** + * The unique ID of the breakpoint. + */ + get id(): string { + if (!this._id) { + this._id = UUID.uuid4(); + } + return this._id; + } + } /** diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index f0ff3441d7594..38f89e663943b 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -6196,6 +6196,10 @@ declare module '@theia/plugin' { * The base class of all breakpoint types. */ export class Breakpoint { + /** + * The unique ID of the breakpoint. + */ + readonly id: string; /** * Is breakpoint enabled. */