From dd49a7d22644171d717051e886e44c38bb38a3a0 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Mon, 10 Dec 2018 15:58:28 +0000 Subject: [PATCH] WIP fix #3312: complete support of source breakpoints Signed-off-by: Anton Kosyakov --- .../browser/breakpoint/breakpoint-manager.ts | 10 +- ...debug-frontend-application-contribution.ts | 178 ++++++++++++++++- .../src/browser/debug-frontend-module.ts | 3 +- .../src/browser/debug-keybinding-contexts.ts | 18 ++ .../editor/debug-breakpoint-widget.tsx | 189 ++++++++++++++++++ .../src/browser/editor/debug-editor-model.ts | 79 ++++---- .../browser/editor/debug-editor-service.ts | 73 ++++++- .../src/browser/model/debug-breakpoint.tsx | 115 ++++++++++- .../style/breakpoint-conditional-disabled.svg | 1 + .../breakpoint-conditional-unverified.svg | 1 + .../browser/style/breakpoint-conditional.svg | 1 + .../browser/style/breakpoint-log-disabled.svg | 1 + .../style/breakpoint-log-unverified.svg | 1 + .../src/browser/style/breakpoint-log.svg | 1 + .../browser/style/breakpoint-unsupported.svg | 1 + packages/debug/src/browser/style/index.css | 56 +++++- .../browser/view/debug-breakpoints-widget.ts | 5 +- .../src/browser/monaco-editor-zone-widget.ts | 173 ++++++++++++++++ packages/monaco/src/browser/style/index.css | 15 ++ 19 files changed, 850 insertions(+), 71 deletions(-) create mode 100644 packages/debug/src/browser/editor/debug-breakpoint-widget.tsx create mode 100644 packages/debug/src/browser/style/breakpoint-conditional-disabled.svg create mode 100644 packages/debug/src/browser/style/breakpoint-conditional-unverified.svg create mode 100644 packages/debug/src/browser/style/breakpoint-conditional.svg create mode 100644 packages/debug/src/browser/style/breakpoint-log-disabled.svg create mode 100644 packages/debug/src/browser/style/breakpoint-log-unverified.svg create mode 100644 packages/debug/src/browser/style/breakpoint-log.svg create mode 100644 packages/debug/src/browser/style/breakpoint-unsupported.svg create mode 100644 packages/monaco/src/browser/monaco-editor-zone-widget.ts 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..9e5fadea7e659 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, @@ -145,11 +155,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, @@ -237,15 +262,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'); @@ -456,8 +510,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, @@ -467,9 +526,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' } ); } @@ -608,6 +674,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 @@ -616,6 +690,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; @@ -623,7 +717,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(), @@ -669,14 +774,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 +807,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 +888,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(); @@ -828,10 +986,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 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..d68c86f29855a --- /dev/null +++ b/packages/debug/src/browser/editor/debug-breakpoint-widget.tsx @@ -0,0 +1,189 @@ +/******************************************************************************** + * 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.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.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(); + } + } +} + +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..d8990c62414be 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)), @@ -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 { @@ -241,8 +236,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 +264,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(); } } @@ -352,27 +368,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..5cf6c310336fc 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,24 +83,39 @@ 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); } } @@ -118,4 +135,42 @@ 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(): 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(); + } + } + } diff --git a/packages/debug/src/browser/model/debug-breakpoint.tsx b/packages/debug/src/browser/model/debug-breakpoint.tsx index 622098df01359..2882d10c41b3d 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; @@ -81,6 +86,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 +127,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 +163,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 +179,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 +189,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/style/breakpoint-conditional-disabled.svg b/packages/debug/src/browser/style/breakpoint-conditional-disabled.svg new file mode 100644 index 0000000000000..61eb04a0818f0 --- /dev/null +++ b/packages/debug/src/browser/style/breakpoint-conditional-disabled.svg @@ -0,0 +1 @@ +breakpoint-conditional-disabled \ No newline at end of file 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..8e5f753968a05 --- /dev/null +++ b/packages/debug/src/browser/style/breakpoint-conditional-unverified.svg @@ -0,0 +1 @@ +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..db2c251350be3 --- /dev/null +++ b/packages/debug/src/browser/style/breakpoint-conditional.svg @@ -0,0 +1 @@ +breakpoint-conditional \ No newline at end of file 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..d1dc5cd6560c4 --- /dev/null +++ b/packages/debug/src/browser/style/breakpoint-log-disabled.svg @@ -0,0 +1 @@ +breakpoint-log-disabled \ No newline at end of file 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..31aede64f864f --- /dev/null +++ b/packages/debug/src/browser/style/breakpoint-log-unverified.svg @@ -0,0 +1 @@ +breakpoint-log-unverified \ No newline at end of file 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..51e2e70edb367 --- /dev/null +++ b/packages/debug/src/browser/style/breakpoint-log.svg @@ -0,0 +1 @@ +breakpoint-log \ No newline at end of file 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..49a195c47c1e1 --- /dev/null +++ b/packages/debug/src/browser/style/breakpoint-unsupported.svg @@ -0,0 +1 @@ +breakpoint-unsupported \ No newline at end of file diff --git a/packages/debug/src/browser/style/index.css b/packages/debug/src/browser/style/index.css index 2516195313a55..121c8b7df7e5d 100644 --- a/packages/debug/src/browser/style/index.css +++ b/packages/debug/src/browser/style/index.css @@ -195,15 +195,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; @@ -302,3 +333,22 @@ 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/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..810ec8e1e0b9f --- /dev/null +++ b/packages/monaco/src/browser/monaco-editor-zone-widget.ts @@ -0,0 +1,173 @@ +/******************************************************************************** + * 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(); + } + + 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; +}