diff --git a/packages/debug/src/browser/breakpoint/breakpoint-manager.ts b/packages/debug/src/browser/breakpoint/breakpoint-manager.ts index 790cf00cdc49d..402fe7c03d447 100644 --- a/packages/debug/src/browser/breakpoint/breakpoint-manager.ts +++ b/packages/debug/src/browser/breakpoint/breakpoint-manager.ts @@ -15,6 +15,7 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; +import { DebugProtocol } from 'vscode-debugprotocol'; import { StorageService } from '@theia/core/lib/browser'; import { MarkerManager } from '@theia/markers/lib/browser/marker-manager'; import URI from '@theia/core/lib/common/uri'; @@ -48,17 +49,14 @@ 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(uri: URI, data: DebugProtocol.SourceBreakpoint): void { const breakpoints = this.getBreakpoints(uri); - const newBreakpoints = breakpoints.filter(({ raw }) => raw.line !== line); + const newBreakpoints = breakpoints.filter(({ raw }) => raw.line !== data.line); if (breakpoints.length === newBreakpoints.length) { newBreakpoints.push({ uri: uri.toString(), enabled: true, - raw: { - line, - column - } + raw: data }); this.setBreakpoints(uri, newBreakpoints); } diff --git a/packages/debug/src/browser/debug-frontend-application-contribution.ts b/packages/debug/src/browser/debug-frontend-application-contribution.ts index 45334e137f418..6dfa6c4f8e206 100644 --- a/packages/debug/src/browser/debug-frontend-application-contribution.ts +++ b/packages/debug/src/browser/debug-frontend-application-contribution.ts @@ -135,6 +135,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, @@ -237,15 +247,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'); @@ -467,9 +506,16 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi registerMenuActions(DebugEditorModel.CONTEXT_MENU, { ...DebugEditorContextCommands.ADD_BREAKPOINT, label: 'Add Breakpoint' }, + { ...DebugEditorContextCommands.ADD_CONDITIONAL_BREAKPOINT, label: DebugCommands.ADD_CONDITIONAL_BREAKPOINT.label }, + { ...DebugEditorContextCommands.ADD_LOGPOINT, label: DebugCommands.ADD_LOGPOINT.label }, { ...DebugEditorContextCommands.REMOVE_BREAKPOINT, label: 'Remove Breakpoint' }, + { ...DebugEditorContextCommands.EDIT_BREAKPOINT, label: 'Edit Breakpoint...' }, { ...DebugEditorContextCommands.ENABLE_BREAKPOINT, label: 'Enable Breakpoint' }, - { ...DebugEditorContextCommands.DISABLE_BREAKPOINT, label: 'Disable Breakpoint' } + { ...DebugEditorContextCommands.DISABLE_BREAKPOINT, label: 'Disable Breakpoint' }, + { ...DebugEditorContextCommands.REMOVE_LOGPOINT, label: 'Remove Logpoint' }, + { ...DebugEditorContextCommands.EDIT_LOGPOINT, label: 'Edit Logpoint...' }, + { ...DebugEditorContextCommands.ENABLE_LOGPOINT, label: 'Enable Logpoint' }, + { ...DebugEditorContextCommands.DISABLE_LOGPOINT, label: 'Disable Logpoint' } ); } @@ -608,6 +654,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 @@ -669,14 +723,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, @@ -687,6 +756,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 { @@ -741,6 +837,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.inBreakpointWidget + }); } protected readonly sessionWidgets = new Map(); diff --git a/packages/debug/src/browser/debug-frontend-module.ts b/packages/debug/src/browser/debug-frontend-module.ts index 91bce20b863d6..3fb7ea230cc48 100644 --- a/packages/debug/src/browser/debug-frontend-module.ts +++ b/packages/debug/src/browser/debug-frontend-module.ts @@ -31,7 +31,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 } from './debug-keybinding-contexts'; import { DebugEditorModelFactory, DebugEditorModel } from './editor/debug-editor-model'; import './debug-monaco-contribution'; import { bindDebugPreferences } from './debug-preferences'; @@ -63,6 +63,7 @@ export default new ContainerModule((bind: interfaces.Bind) => { bind(ResourceResolver).toService(DebugResourceResolver); bind(KeybindingContext).to(InDebugModeContext).inSingletonScope(); + bind(KeybindingContext).to(InBreakpointWidgetContext).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..703915c955bd5 100644 --- a/packages/debug/src/browser/debug-keybinding-contexts.ts +++ b/packages/debug/src/browser/debug-keybinding-contexts.ts @@ -18,11 +18,14 @@ 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'; + } @injectable() @@ -38,3 +41,18 @@ 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; + } + +} 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..ae5c92aab1372 --- /dev/null +++ b/packages/debug/src/browser/editor/debug-breakpoint-widget.tsx @@ -0,0 +1,188 @@ +/******************************************************************************** + * 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: monaco.editor.IStandaloneCodeEditor; + + @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)); + 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); + + // TODO: move input together with breakpoint decorations + // TODO: placeholder + // TODO: completions + 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.renderSelect(); + 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; + } + 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 }); + 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.getId()), node, { + autoSizing: false + }); + } + + protected renderSelect(): void { + if (this.toDispose.disposed) { + return; + } + 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; + if (this._input) { + this._input.getControl().setValue(this._values[this.context] || ''); + } + this.renderSelect(); + } + +} +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..29b0bc847ee2a 100644 --- a/packages/debug/src/browser/editor/debug-editor-model.ts +++ b/packages/debug/src/browser/editor/debug-editor-model.ts @@ -25,6 +25,7 @@ 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; @@ -35,6 +36,7 @@ export class DebugEditorModel implements Disposable { static createContainer(parent: interfaces.Container, editor: monaco.editor.IStandaloneCodeEditor): 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 { @@ -72,11 +74,15 @@ 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.toDispose.pushAll([ this.hover, + this.breakpointWidget, this.editor.onMouseDown(event => this.handleMouseDown(event)), this.editor.onMouseMove(event => this.handleMouseMove(event)), this.editor.onMouseLeave(event => this.handleMouseLeave(event)), @@ -241,8 +247,9 @@ export class DebugEditorModel implements Disposable { uri: uriString, enabled: oldBreakpoint ? oldBreakpoint.enabled : true, raw: { + ...(oldBreakpoint && oldBreakpoint.raw), line, - column: range.startColumn + column: 1 } }); } @@ -268,7 +275,27 @@ export class DebugEditorModel implements Disposable { if (breakpoint) { breakpoint.remove(); } else { - this.breakpoints.addBreakpoint(this.uri, position.lineNumber, position.column); + this.breakpoints.addBreakpoint(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(this.uri, { + line: position.lineNumber, + column: 1, + ...values + }); + } + this.breakpointWidget.hide(); } } diff --git a/packages/debug/src/browser/editor/debug-editor-service.ts b/packages/debug/src/browser/editor/debug-editor-service.ts index b7f74a440efce..643cabcbc8e8a 100644 --- a/packages/debug/src/browser/editor/debug-editor-service.ts +++ b/packages/debug/src/browser/editor/debug-editor-service.ts @@ -23,6 +23,7 @@ import { DebugSessionManager } from '../debug-session-manager'; import { DebugEditorModel, DebugEditorModelFactory } from './debug-editor-model'; import { BreakpointManager } from '../breakpoint/breakpoint-manager'; import { DebugBreakpoint } from '../model/debug-breakpoint'; +import { DebugBreakpointWidget } from './debug-breakpoint-widget'; @injectable() export class DebugEditorService { @@ -73,6 +74,7 @@ export class DebugEditorService { const model = this.models.get(uri.toString()); if (model) { model.render(); + // TODO: hide the breakpoint widget if a breakpoint is removed } } @@ -81,20 +83,35 @@ 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) { @@ -118,4 +135,37 @@ export class DebugEditorService { 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(): void { + const { model, breakpoint } = this; + if (model && breakpoint) { + model.breakpointWidget.show(breakpoint); + } + } + closeBreakpoint(): void { + const { model } = this; + if (model) { + model.breakpointWidget.hide(); + } + } + acceptBreakpoint(): void { + const { model } = this; + if (model) { + model.acceptBreakpoint(); + } + } + } diff --git a/packages/debug/src/browser/model/debug-breakpoint.tsx b/packages/debug/src/browser/model/debug-breakpoint.tsx index 622098df01359..b2b89f44b3df8 100644 --- a/packages/debug/src/browser/model/debug-breakpoint.tsx +++ b/packages/debug/src/browser/model/debug-breakpoint.tsx @@ -81,6 +81,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 +122,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); } diff --git a/packages/debug/src/browser/style/index.css b/packages/debug/src/browser/style/index.css index 2516195313a55..3a03fc4530cad 100644 --- a/packages/debug/src/browser/style/index.css +++ b/packages/debug/src/browser/style/index.css @@ -302,3 +302,23 @@ display: flex; flex: 1; } + +/** 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/monaco/src/browser/monaco-editor-zone-widget.ts b/packages/monaco/src/browser/monaco-editor-zone-widget.ts new file mode 100644 index 0000000000000..ef470d4edbb44 --- /dev/null +++ b/packages/monaco/src/browser/monaco-editor-zone-widget.ts @@ -0,0 +1,153 @@ +/******************************************************************************** + * 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 { 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(); + } + + get options(): Pick | undefined { + return this.viewZone; + } + + hide(): void { + this.toHide.dispose(); + } + + // TODO: show frame option + show({ afterLineNumber, afterColumn, heightInLines }: { + afterLineNumber: number, + afterColumn?: number, + heightInLines: number + }): void { + 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 height = zoneHeight; + this.containerNode.style.height = height + 'px'; + const width = this.computeWidth(); + this.onDidLayoutChangeEmitter.fire({ height, width }); + } + + 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; + } + +} diff --git a/packages/monaco/src/browser/style/index.css b/packages/monaco/src/browser/style/index.css index 1c051b1ef67ca..8c76a0daa62b4 100644 --- a/packages/monaco/src/browser/style/index.css +++ b/packages/monaco/src/browser/style/index.css @@ -59,3 +59,16 @@ 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; + position: relative; +}