diff --git a/.travis.yml b/.travis.yml index 5b672a6e68769..92f2b60be3386 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,8 @@ cache: - packages/bunyan/node_modules - packages/callhierarchy/node_modules - packages/core/node_modules + - packages/debug/node_modules + - packages/debug-nodejs/node_modules - packages/cpp/node_modules - packages/editorconfig/node_modules - packages/editor/node_modules diff --git a/examples/browser/package.json b/examples/browser/package.json index 13eeb6bb7f207..4fbe0603da852 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -6,6 +6,8 @@ "@theia/callhierarchy": "^0.3.13", "@theia/core": "^0.3.13", "@theia/cpp": "^0.3.13", + "@theia/debug": "^0.3.13", + "@theia/debug-nodejs": "^0.3.13", "@theia/editor": "^0.3.13", "@theia/editorconfig": "^0.3.13", "@theia/extension-manager": "^0.3.13", diff --git a/examples/electron/package.json b/examples/electron/package.json index 6542c98e62118..f0c89f2217d26 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -9,6 +9,8 @@ "@theia/callhierarchy": "^0.3.13", "@theia/core": "^0.3.13", "@theia/cpp": "^0.3.13", + "@theia/debug": "^0.3.13", + "@theia/debug-nodejs": "^0.3.13", "@theia/editor": "^0.3.13", "@theia/editorconfig": "^0.3.13", "@theia/extension-manager": "^0.3.13", @@ -55,4 +57,4 @@ "devDependencies": { "@theia/cli": "^0.3.13" } -} +} \ No newline at end of file diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts index 6c144d5d63ea8..51994bec6a1ec 100644 --- a/packages/core/src/browser/shell/tab-bars.ts +++ b/packages/core/src/browser/shell/tab-bars.ts @@ -104,7 +104,7 @@ export class TabBarRenderer extends TabBar.Renderer { const iconSize = data.iconSize; let height: string | undefined; if (labelSize || iconSize) { - const labelHeight = labelSize ? labelSize.width : 0; + const labelHeight = labelSize ? (this.tabBar && this.tabBar.orientation === 'horizontal' ? labelSize.height : labelSize.width) : 0; const iconHeight = iconSize ? iconSize.height : 0; let paddingTop = data.paddingTop || 0; if (labelHeight > 0 && iconHeight > 0) { diff --git a/packages/debug-nodejs/README.md b/packages/debug-nodejs/README.md new file mode 100644 index 0000000000000..d0e072908449e --- /dev/null +++ b/packages/debug-nodejs/README.md @@ -0,0 +1,7 @@ +# Theia - NodeJS Debug Extension + +See [here](https://github.com/theia-ide/theia) for a detailed documentation. + +## License +- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/) +- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp) diff --git a/packages/debug-nodejs/compile.tsconfig.json b/packages/debug-nodejs/compile.tsconfig.json new file mode 100644 index 0000000000000..a23513b5e6b13 --- /dev/null +++ b/packages/debug-nodejs/compile.tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ] +} diff --git a/packages/debug-nodejs/package.json b/packages/debug-nodejs/package.json new file mode 100644 index 0000000000000..98cbefd0f7e8d --- /dev/null +++ b/packages/debug-nodejs/package.json @@ -0,0 +1,52 @@ +{ + "name": "@theia/debug-nodejs", + "version": "0.3.13", + "description": "Theia - NodeJS Debug Extension", + "dependencies": { + "@theia/debug": "^0.3.13", + "vscode-debugprotocol": "^1.26.0" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "backend": "lib/node/debug-nodejs-backend-module" + } + ], + "keywords": [ + "theia-extension, debug, nodejs" + ], + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/theia-ide/theia.git" + }, + "bugs": { + "url": "https://github.com/theia-ide/theia/issues" + }, + "homepage": "https://github.com/theia-ide/theia", + "files": [ + "lib", + "src", + "scripts" + ], + "scripts": { + "prepare": "yarn run clean && yarn run build", + "clean": "theiaext clean", + "build": "concurrently -n download,build -c red,blue \"node ./scripts/download-vscode-node-debug.js\" \"theiaext build\"", + "watch": "theiaext watch", + "test": "theiaext test", + "docs": "theiaext docs" + }, + "devDependencies": { + "@theia/ext-scripts": "^0.3.13" + }, + "nyc": { + "extends": "../../configs/nyc.json" + }, + "debugAdapter": { + "downloadUrl": "https://github.com/tolusha/node-debug/releases/download/v1.23.5/vscode-node-debug.tar.gz", + "dir": "lib/adapter" + } +} \ No newline at end of file diff --git a/packages/debug-nodejs/scripts/download-vscode-node-debug.js b/packages/debug-nodejs/scripts/download-vscode-node-debug.js new file mode 100644 index 0000000000000..64a6f404ef4a9 --- /dev/null +++ b/packages/debug-nodejs/scripts/download-vscode-node-debug.js @@ -0,0 +1,92 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +const fs = require('fs'); +const https = require('https'); +const http = require('http'); +const path = require('path'); +const zlib = require('zlib'); +const tar = require('tar'); +const mkdirp = require('mkdirp'); +const packageJson = require('../package.json'); +const downloadUrl = packageJson['debugAdapter']['downloadUrl']; +const downloadPath = path.join(__dirname, '../node_modules/download'); +const archivePath = path.join(downloadPath, path.basename(downloadUrl)); +const targetPath = packageJson['debugAdapter']['dir']; + +function downloadDap() { + return new Promise((resolve, reject) => { + if (fs.existsSync(archivePath)) { + resolve(); + return; + } + + if (!fs.existsSync(downloadPath)) { + fs.mkdirSync(downloadPath); + } + + const file = fs.createWriteStream(archivePath); + const downloadWithRedirect = url => { + const h = url.toString().startsWith('https') ? https : http; + h.get(url, response => { + const statusCode = response.statusCode; + const redirectLocation = response.headers.location; + if (statusCode >= 300 && statusCode < 400 && redirectLocation) { + console.log('Redirect location: ' + redirectLocation); + downloadWithRedirect(redirectLocation); + } else if (statusCode === 200) { + response.on('end', () => resolve()); + response.on('error', e => { + file.destroy(); + reject(e); + }); + response.pipe(file); + } else { + file.destroy(); + reject(new Error(`Failed to download 'VSCode Node Debug' with error code: ${statusCode}`)); + } + }) + }; + + downloadWithRedirect(downloadUrl); + }); +} + +decompressArchive = function () { + return new Promise((resolve, reject) => { + if (!fs.existsSync(archivePath)) { + reject(new Error(`The archive was not found at ${archivePath}.`)); + return; + } + + if (!fs.existsSync(targetPath)) { + mkdirp.sync(targetPath); + } + + const gunzip = zlib.createGunzip({ finishFlush: zlib.Z_SYNC_FLUSH, flush: zlib.Z_SYNC_FLUSH }); + const untar = tar.x({ cwd: targetPath }); + fs.createReadStream(archivePath).pipe(gunzip).pipe(untar) + .on('error', e => reject(e)) + .on('end', () => resolve()); + }); +} + +downloadDap().then(() => { + decompressArchive(); +}).catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/packages/debug-nodejs/src/node/debug-nodejs-backend-module.ts b/packages/debug-nodejs/src/node/debug-nodejs-backend-module.ts new file mode 100644 index 0000000000000..f3e934b5db536 --- /dev/null +++ b/packages/debug-nodejs/src/node/debug-nodejs-backend-module.ts @@ -0,0 +1,23 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { ContainerModule } from 'inversify'; +import { NodeJsDebugAdapterContribution } from './debug-nodejs'; +import { DebugAdapterContribution } from '@theia/debug/lib/node/debug-model'; + +export default new ContainerModule(bind => { + bind(DebugAdapterContribution).to(NodeJsDebugAdapterContribution).inSingletonScope(); +}); diff --git a/packages/debug-nodejs/src/node/debug-nodejs.spec.ts b/packages/debug-nodejs/src/node/debug-nodejs.spec.ts new file mode 100644 index 0000000000000..412687575cd9b --- /dev/null +++ b/packages/debug-nodejs/src/node/debug-nodejs.spec.ts @@ -0,0 +1,15 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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-nodejs/src/node/debug-nodejs.ts b/packages/debug-nodejs/src/node/debug-nodejs.ts new file mode 100644 index 0000000000000..0a3525186ca6b --- /dev/null +++ b/packages/debug-nodejs/src/node/debug-nodejs.ts @@ -0,0 +1,64 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 + ********************************************************************************/ + +const path = require('path'); +const packageJson = require('../../package.json'); +const debugAdapterDir = packageJson['debugAdapter']['dir']; + +import { injectable } from 'inversify'; +import { DebugConfiguration } from '@theia/debug/lib/common/debug-common'; +import { DebugAdapterContribution, DebugAdapterExecutable } from '@theia/debug/lib/node/debug-model'; + +@injectable() +export class NodeJsDebugAdapterContribution implements DebugAdapterContribution { + readonly debugType = 'node'; + + provideDebugConfigurations = [{ + type: this.debugType, + breakpoints: { filePatterns: ['[.]js$', '[.]ts$'] }, + request: 'attach', + name: 'Attach by PID', + processId: '' + }]; + + resolveDebugConfiguration(config: DebugConfiguration): DebugConfiguration { + config.breakpoints = { filePatterns: ['[.]js$', '[.]ts$'] }; + + if (!config.request) { + throw new Error('Debug request type is not provided.'); + } + + switch (config.request) { + case 'attach': this.validateAttachConfig(config); + } + + return config; + } + + provideDebugAdapterExecutable(config: DebugConfiguration): DebugAdapterExecutable { + const program = path.join(__dirname, `../../${debugAdapterDir}/out/src/nodeDebug.js`); + return { + program, + runtime: 'node' + }; + } + + private validateAttachConfig(config: DebugConfiguration) { + if (!config.processId) { + throw new Error('PID is not provided.'); + } + } +} diff --git a/packages/debug/README.md b/packages/debug/README.md new file mode 100644 index 0000000000000..9395201412bf0 --- /dev/null +++ b/packages/debug/README.md @@ -0,0 +1,37 @@ +## Architecture +`DebugService` is used to initialize a new `DebugSession`. This service provides functionality to configure and to start a new debug session. The workflow is the following. If user wants to debug an application and there is no debug configuration associated with the application then the list of available debuggers is requested to create a suitable debug configuration. When configuration is chosen it is possible to alter the configuration by filling in missing values or by adding/changing/removing attributes. + +In most cases the default behavior of the `DebugSession` is enough. But it is possible to provide its own implementation. The `DebugSessionFactory` is used for this purpose via `DebugSessionContribution`. Documented model objects are located [here](https://github.com/theia-ide/theia/tree/master/packages/debug/src/browser/debug-model.ts) + +### Debug Session life-cycle API +`DebugSession` life-cycle is controlled and can be tracked as follows: +* An `onDidPreCreateDebugSession` event indicates that a debug session is going to be created. +* An `onDidCreateDebugSession` event indicates that a debug session has been created. +* An `onDidDestroyDebugSession` event indicates that a debug session has terminated. +* An `onDidChangeActiveDebugSession` event indicates that an active debug session has been changed + +### Breakpoints API +`ExtDebugProtocol.AggregatedBreakpoint` is used to handle breakpoints on the client side. It covers all three breakpoint types: `DebugProtocol.SourceBreakpoint`, `DebugProtocol.FunctionBreakpoint` and `ExtDebugProtocol.ExceptionBreakpoint`. It is possible to identify a breakpoint type with help of `DebugUtils`. Notification about added, removed, or changed breakpoints is received via `onDidChangeBreakpoints`. + +### Server side +At the back-end we start a debug adapter using `DebugAdapterFactory` and then a `DebugAdapterSession` is instantiated which works as a proxy between client and debug adapter. If a default implementation of the debug adapter session does not fit needs, it is possible to provide its own implementation using `DebugAdapterSessionFactory`. If so, it is recommended to extend the default implementation of the `DebugAdapterSession`. Documented model objects are located [here](https://github.com/theia-ide/theia/tree/master/packages/debug/src/node/debug-model.ts) + +`DebugSessionState` accumulates debug adapter events and is used to restore debug session on the client side when page is refreshed. + +## How to contribute a new debugger +`DebugAdapterContribution` is a contribution point for all debug adapters to provide and resolve debug configuration. + +Here is an example of [debug adapter contribution for node](https://github.com/theia-ide/theia/tree/master/packages/debug-nodejs/src/node/debug-nodejs.ts) + +## References +* [Debug Adapter Protocol](https://github.com/Microsoft/vscode-debugadapter-node/blob/master/protocol/src/debugProtocol.ts) +* [VS Code debug API](https://code.visualstudio.com/docs/extensionAPI/api-debugging) +* [Debug adapter example for VS Code](https://code.visualstudio.com/docs/extensions/example-debuggers) + +## Debug adapter implementations for VS Code +* [Node debugger](https://github.com/Microsoft/vscode-node-debug) +* [Java debugger](https://github.com/Microsoft/vscode-java-debug) + +## License +- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/) +- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp) diff --git a/packages/debug/compile.tsconfig.json b/packages/debug/compile.tsconfig.json new file mode 100644 index 0000000000000..a23513b5e6b13 --- /dev/null +++ b/packages/debug/compile.tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ] +} diff --git a/packages/debug/package.json b/packages/debug/package.json new file mode 100644 index 0000000000000..9d0c1d576d0d6 --- /dev/null +++ b/packages/debug/package.json @@ -0,0 +1,50 @@ +{ + "name": "@theia/debug", + "version": "0.3.13", + "description": "Theia - Debug Extension", + "dependencies": { + "@theia/core": "^0.3.13", + "@theia/editor": "^0.3.13", + "@theia/monaco": "^0.3.13", + "@theia/process": "^0.3.13", + "vscode-debugprotocol": "^1.26.0" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/debug-frontend-module", + "backend": "lib/node/debug-backend-module" + } + ], + "keywords": [ + "theia-extension, debug" + ], + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/theia-ide/theia.git" + }, + "bugs": { + "url": "https://github.com/theia-ide/theia/issues" + }, + "homepage": "https://github.com/theia-ide/theia", + "files": [ + "lib" + ], + "scripts": { + "prepare": "yarn run clean && yarn run build", + "clean": "theiaext clean", + "build": "theiaext build", + "watch": "theiaext watch", + "test": "theiaext test", + "docs": "theiaext docs" + }, + "devDependencies": { + "@theia/ext-scripts": "^0.3.13" + }, + "nyc": { + "extends": "../../configs/nyc.json" + } +} \ No newline at end of file diff --git a/packages/debug/src/browser/breakpoint/breakpoint-applier.ts b/packages/debug/src/browser/breakpoint/breakpoint-applier.ts new file mode 100644 index 0000000000000..c8c287939149e --- /dev/null +++ b/packages/debug/src/browser/breakpoint/breakpoint-applier.ts @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { injectable, inject } from 'inversify'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { BreakpointStorage } from './breakpoint-marker'; +import { DebugUtils } from '../debug-utils'; +import { DebugSession } from '../debug-model'; + +/** + * Applies session breakpoints. + */ +@injectable() +export class BreakpointsApplier { + constructor(@inject(BreakpointStorage) protected readonly storage: BreakpointStorage) { } + + applySessionBreakpoints(debugSession: DebugSession, source?: DebugProtocol.Source): Promise { + const promises: Promise[] = []; + + const breakpoints = this.storage.get(DebugUtils.isSourceBreakpoint) + .filter(b => b.sessionId === debugSession.sessionId) + .filter(b => source ? DebugUtils.checkUri(b, DebugUtils.toUri(source)) : true); + + for (const breakpointsBySource of DebugUtils.groupBySource(breakpoints).values()) { + const args: DebugProtocol.SetBreakpointsArguments = { + source: breakpointsBySource[0].source!, + breakpoints: breakpointsBySource.map(b => b.origin as DebugProtocol.SourceBreakpoint), + // Although marked as deprecated, some debug adapters still use lines + lines: breakpointsBySource.map(b => (b.origin as DebugProtocol.SourceBreakpoint).line) + }; + + // The array elements are in the same order as the elements + // of the 'breakpoints' in the SetBreakpointsArguments. + promises.push(debugSession.setBreakpoints(args) + .then(response => { + for (const i in breakpointsBySource) { + if (breakpointsBySource) { + if (response.body.breakpoints) { + breakpointsBySource[i].created = response.body.breakpoints[i]; + } + } + } + return breakpointsBySource; + }).then(result => this.storage.update(result))); + } + + return Promise.all(promises).then(() => { }); + } +} diff --git a/packages/debug/src/browser/breakpoint/breakpoint-decorators.ts b/packages/debug/src/browser/breakpoint/breakpoint-decorators.ts new file mode 100644 index 0000000000000..d3a5f88561b1d --- /dev/null +++ b/packages/debug/src/browser/breakpoint/breakpoint-decorators.ts @@ -0,0 +1,129 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { injectable, inject } from 'inversify'; +import { + Range, + EditorDecorator, + EditorDecorationOptions, + TextEditor, + Position, + EditorManager +} from '@theia/editor/lib/browser'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { DebugSessionManager } from '../debug-session'; +import { DebugUtils } from '../debug-utils'; +import { BreakpointStorage } from './breakpoint-marker'; + +const ActiveLineDecoration = { + isWholeLine: true, + className: 'theia-debug-active-line', +}; + +/** + * Highlight active debug line in the editors. + */ +@injectable() +export class ActiveLineDecorator extends EditorDecorator { + constructor( + @inject(DebugSessionManager) protected readonly debugSessionManager: DebugSessionManager, + @inject(EditorManager) protected readonly editorManager: EditorManager) { + super(); + } + + applyDecorations(): void { + this.editorManager.all.map(widget => this.setDecorations(widget.editor, [])); + + const session = this.debugSessionManager.getActiveDebugSession(); + if (!session) { + return; + } + + for (const threadId of session.state.stoppedThreadIds) { + session.stacks({ threadId, levels: 1 }).then(response => { + const frame = response.body.stackFrames[0]; + if (!frame || !frame.source) { + return; + } + + const uri = DebugUtils.toUri(frame.source); + this.editorManager.getByUri(uri).then(widget => { + if (widget) { + this.doShow(widget.editor, frame); + } + }); + }); + } + } + + private doShow(editor: TextEditor, frame: DebugProtocol.StackFrame): void { + const decoration = { + range: this.toRange(frame), + options: ActiveLineDecoration + }; + this.setDecorations(editor, [decoration]); + } + + private toRange(frame: DebugProtocol.StackFrame): Range { + const start = Position.create( + frame.line - 1, + frame.endColumn ? frame.column - 1 : 0); + const end = Position.create( + frame.endLine ? frame.endLine - 1 : frame.line - 1, + frame.endColumn ? frame.endColumn - 1 : 0); + return Range.create(start, end); + } +} + +const InactiveBreakpointDecoration = { + isWholeLine: false, + glyphMarginClassName: 'theia-debug-inactive-breakpoint', +}; + +const ActiveBreakpointDecoration = { + isWholeLine: false, + glyphMarginClassName: 'theia-debug-active-breakpoint', +}; + +/** + * Shows breakpoints. + */ +@injectable() +export class BreakpointDecorator extends EditorDecorator { + constructor( + @inject(BreakpointStorage) protected readonly breakpointStorage: BreakpointStorage, + @inject(EditorManager) protected readonly editorManager: EditorManager) { + super(); + } + + applyDecorations(editor?: TextEditor): void { + const editors = editor ? [editor] : this.editorManager.all.map(widget => widget.editor); + + editors.forEach(e => { + const decorations = this.breakpointStorage.get(DebugUtils.isSourceBreakpoint) + .filter(b => DebugUtils.checkUri(b, e.uri)) + .map(b => ({ + range: this.toRange(b.origin as DebugProtocol.SourceBreakpoint), + options: !!b.created && !!b.created.verified ? ActiveBreakpointDecoration : InactiveBreakpointDecoration + })); + this.setDecorations(e, decorations); + }); + } + + private toRange(breakpoint: DebugProtocol.SourceBreakpoint): Range { + return Range.create(Position.create(breakpoint.line - 1, 0), Position.create(breakpoint.line - 1, 0)); + } +} diff --git a/packages/debug/src/browser/breakpoint/breakpoint-manager.ts b/packages/debug/src/browser/breakpoint/breakpoint-manager.ts new file mode 100644 index 0000000000000..f2ca7a60a5d51 --- /dev/null +++ b/packages/debug/src/browser/breakpoint/breakpoint-manager.ts @@ -0,0 +1,289 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { DebugSession } from '../debug-model'; +import { DebugSessionManager } from '../debug-session'; +import { injectable, inject } from 'inversify'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { SourceOpener, DebugUtils } from '../debug-utils'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { ActiveLineDecorator, BreakpointDecorator } from './breakpoint-decorators'; +import { BreakpointStorage } from './breakpoint-marker'; +import { + EditorManager, + EditorWidget, + Position, + TextEditor, + MouseTargetType +} from '@theia/editor/lib/browser'; +import { ExtDebugProtocol, DebugService } from '../../common/debug-common'; +import { Emitter, Event } from '@theia/core'; +import { BreakpointsApplier } from './breakpoint-applier'; + +/** + * The breakpoint manager implementation. + */ +@injectable() +export class BreakpointsManager implements FrontendApplicationContribution { + protected readonly supportedFilePatterns: string[] = []; + protected readonly onDidChangeBreakpointsEmitter = new Emitter(); + + constructor( + @inject(DebugService) protected readonly debugService: DebugService, + @inject(DebugSessionManager) protected readonly debugSessionManager: DebugSessionManager, + @inject(SourceOpener) protected readonly sourceOpener: SourceOpener, + @inject(ActiveLineDecorator) protected readonly lineDecorator: ActiveLineDecorator, + @inject(BreakpointDecorator) protected readonly breakpointDecorator: BreakpointDecorator, + @inject(BreakpointStorage) protected readonly breakpointStorage: BreakpointStorage, + @inject(BreakpointsApplier) protected readonly breakpointApplier: BreakpointsApplier, + @inject(EditorManager) protected readonly editorManager: EditorManager + ) { } + + onStart(): void { + this.debugSessionManager.onDidCreateDebugSession(debugSession => this.onDebugSessionCreated(debugSession)); + this.debugSessionManager.onDidChangeActiveDebugSession( + ([oldDebugSession, newDebugSession]) => this.onActiveDebugSessionChanged(oldDebugSession, newDebugSession)); + this.editorManager.onCreated(widget => this.onEditorCreated(widget.editor)); + this.editorManager.onActiveEditorChanged(widget => this.onActiveEditorChanged(widget)); + this.editorManager.onCurrentEditorChanged(widget => this.onCurrentEditorChanged(widget)); + + this.debugService.debugTypes() + .then(debugTypes => debugTypes.forEach(debugType => + this.debugService.provideDebugConfigurations(debugType).then(configs => + configs.forEach(config => config.breakpoints.filePatterns.forEach(pattern => + this.supportedFilePatterns.push(pattern))) + ) + )); + } + + get onDidChangeBreakpoints(): Event { + return this.onDidChangeBreakpointsEmitter.event; + } + + /** + * Toggles breakpoint in the given editor. + * @param editor the active text editor + * @param position the mouse position in the editor + */ + async toggleBreakpoint(editor: TextEditor, position: Position): Promise { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + + const srcBreakpoint = this.createSourceBreakpoint(debugSession, editor, position); + const id = DebugUtils.makeBreakpointId(srcBreakpoint); + + if (this.breakpointStorage.exists(id)) { + this.breakpointStorage.delete(srcBreakpoint); + } else { + this.breakpointStorage.add(srcBreakpoint); + } + + if (debugSession) { + const source = DebugUtils.toSource(editor.uri, debugSession); + await this.breakpointApplier.applySessionBreakpoints(debugSession, source); + } + + this.breakpointDecorator.applyDecorations(editor); + this.onDidChangeBreakpointsEmitter.fire(undefined); + } + + /** + * Returns all breakpoints for the given debug session. + * @param sessionId the debug session identifier + */ + async get(sessionId: string | undefined): Promise { + return this.breakpointStorage.get().filter(b => b.sessionId === sessionId); + } + + /** + * Returns all breakpoints. + */ + async getAll(): Promise { + return this.breakpointStorage.get(); + } + + /** + * Creates a source breakpoint for the given editor and active session. + * @param session the current active session + * @param editor the text editor + * @param position the mouse position in the editor + * @returns breakpoint + */ + private createSourceBreakpoint(debugSession: DebugSession | undefined, editor: TextEditor, position: Position): ExtDebugProtocol.AggregatedBreakpoint { + const source = DebugUtils.toSource(editor.uri, debugSession); + const sessionId = debugSession + ? (DebugUtils.checkPattern(source, debugSession.configuration.breakpoints.filePatterns) + ? debugSession.sessionId + : undefined) + : undefined; + + return { + source, sessionId, + origin: { line: position.line + 1 } + }; + } + + private onDebugSessionCreated(debugSession: DebugSession) { + debugSession.on('stopped', event => this.onThreadStopped(debugSession, event)); + debugSession.on('continued', event => this.onThreadContinued(debugSession, event)); + debugSession.on('terminated', event => this.onTerminated(debugSession, event)); + debugSession.on('configurationDone', event => this.onConfigurationDone(debugSession, event)); + debugSession.on('breakpoint', event => this.onBreakpoint(debugSession, event)); + + const breakpoints = this.breakpointStorage.get(DebugUtils.isSourceBreakpoint) + .filter(b => b.sessionId === undefined) + .filter(b => DebugUtils.checkPattern(b.source!, debugSession.configuration.breakpoints.filePatterns)) + .map(b => { + b.sessionId = debugSession.sessionId; + b.created = undefined; + return b; + }); + + this.breakpointStorage.update(breakpoints); + this.onDidChangeBreakpointsEmitter.fire(undefined); + } + + private onConfigurationDone(debugSession: DebugSession, event: ExtDebugProtocol.ConfigurationDoneEvent): void { + this.breakpointDecorator.applyDecorations(); + this.lineDecorator.applyDecorations(); + } + + private onTerminated(debugSession: DebugSession, event: DebugProtocol.TerminatedEvent): void { + this.lineDecorator.applyDecorations(); + + const breakpoints = this.breakpointStorage.get(DebugUtils.isSourceBreakpoint) + .filter(b => b.sessionId === debugSession.sessionId) + .map(b => { + b.created = undefined; + b.sessionId = undefined; + return b; + }); + + this.breakpointStorage.update(breakpoints); + this.breakpointDecorator.applyDecorations(); + this.onDidChangeBreakpointsEmitter.fire(undefined); + } + + private onActiveDebugSessionChanged(oldDebugSession: DebugSession | undefined, newDebugSession: DebugSession | undefined) { + this.lineDecorator.applyDecorations(); + this.breakpointDecorator.applyDecorations(); + } + + private onBreakpoint(debugSession: DebugSession, event: DebugProtocol.BreakpointEvent): void { + const breakpoint = event.body.breakpoint; + + const breakpoints = this.breakpointStorage.get(DebugUtils.isSourceBreakpoint) + .filter(b => b.sessionId === debugSession.sessionId) + .filter(b => { + if (breakpoint.id && b.created && b.created.id === breakpoint.id) { + return true; + } + + if (!breakpoint.source) { + return false; + } + + const srcBrk = b.origin as DebugProtocol.SourceBreakpoint; + return DebugUtils.checkUri(b, DebugUtils.toUri(breakpoint.source)) + && srcBrk.line === breakpoint.line + && srcBrk.column === breakpoint.column; + }); + + const sourceBreakpoint = breakpoints[0]; + switch (event.body.reason) { + case 'new': + case 'changed': { + if (sourceBreakpoint) { + sourceBreakpoint.created = breakpoint; + return this.breakpointStorage.update(sourceBreakpoint); + } else { + return this.breakpointStorage.update({ + sessionId: debugSession.sessionId, + source: breakpoint.source, + created: breakpoint, + origin: { + line: breakpoint.line!, + column: breakpoint.column + } + }); + } + } + case 'removed': { + if (sourceBreakpoint) { + return this.breakpointStorage.delete(sourceBreakpoint); + } + } + } + + this.breakpointDecorator.applyDecorations(); + this.onDidChangeBreakpointsEmitter.fire(undefined); + } + + private onThreadContinued(debugSession: DebugSession, event: DebugProtocol.ContinuedEvent): void { + this.lineDecorator.applyDecorations(); + } + + private onThreadStopped(debugSession: DebugSession, event: DebugProtocol.StoppedEvent): void { + const body = event.body; + + if (body.threadId) { + switch (body.reason) { + case 'exception': + case 'breakpoint': + case 'entry': + case 'step': { + const activeDebugSession = this.debugSessionManager.getActiveDebugSession(); + if (activeDebugSession && activeDebugSession.sessionId === debugSession.sessionId) { + const args: DebugProtocol.StackTraceArguments = { + threadId: body.threadId, + startFrame: 0, + levels: 1 + }; + + debugSession.stacks(args).then(response => { + const frame = response.body.stackFrames[0]; + if (frame) { + this.sourceOpener.open(frame).then(() => this.lineDecorator.applyDecorations()); + } + }); + } + break; + } + } + } + } + + private async onEditorCreated(editor: TextEditor): Promise { + this.lineDecorator.applyDecorations(); + this.breakpointDecorator.applyDecorations(editor); + + editor.onMouseDown(event => { + switch (event.target.type) { + case MouseTargetType.GUTTER_GLYPH_MARGIN: + case MouseTargetType.GUTTER_VIEW_ZONE: { + const source = DebugUtils.toSource(editor.uri, undefined); + if (DebugUtils.checkPattern(source, this.supportedFilePatterns)) { + this.toggleBreakpoint(editor, event.target.position); + } + break; + } + } + }); + } + + private onActiveEditorChanged(widget: EditorWidget | undefined): void { } + + private onCurrentEditorChanged(widget: EditorWidget | undefined): void { } +} diff --git a/packages/debug/src/browser/breakpoint/breakpoint-marker.ts b/packages/debug/src/browser/breakpoint/breakpoint-marker.ts new file mode 100644 index 0000000000000..fc35e1baf6487 --- /dev/null +++ b/packages/debug/src/browser/breakpoint/breakpoint-marker.ts @@ -0,0 +1,117 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { injectable } from 'inversify'; +import { ExtDebugProtocol } from '../../common/debug-common'; +import { DebugUtils } from '../debug-utils'; +import { MarkerManager } from '@theia/markers/lib/browser/marker-manager'; +import URI from '@theia/core/lib/common/uri'; +import { Marker } from '@theia/markers/lib/common/marker'; + +export const BREAKPOINT_KIND = 'breakpoint'; +const BREAKPOINT_OWNER = 'breakpoint'; + +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; + } +} + +@injectable() +export class BreakpointStorage extends MarkerManager { + + public getKind(): string { + return BREAKPOINT_KIND; + } + + /** + * Updates an existed breakpoint. + * @param breakpoint the breakpoint to update + */ + update(data: ExtDebugProtocol.AggregatedBreakpoint | ExtDebugProtocol.AggregatedBreakpoint[]): void { + const breakpoints = Array.isArray(data) ? data : [data]; + + breakpoints.map(breakpoint => { + const uri = this.toUri(breakpoint); + const id = DebugUtils.makeBreakpointId(breakpoint); + + const newBreakpoints = super.findMarkers({ uri }).map(m => DebugUtils.makeBreakpointId(m.data) === id ? breakpoint : m.data); + super.setMarkers(uri, BREAKPOINT_OWNER, newBreakpoints); + }); + } + + /** + * Adds a given breakpoint. + * @param breakpoint the breakpoint to add + */ + add(breakpoint: ExtDebugProtocol.AggregatedBreakpoint): void { + const uri = this.toUri(breakpoint); + const existedBreakpoints = super.findMarkers({ uri }).map(m => m.data); + existedBreakpoints.push(breakpoint); + super.setMarkers(uri, BREAKPOINT_OWNER, existedBreakpoints); + } + + /** + * Deletes a given breakpoint. + * @param breakpoint the breakpoint to delete + */ + delete(breakpoint: ExtDebugProtocol.AggregatedBreakpoint): void { + const uri = this.toUri(breakpoint); + + const id = DebugUtils.makeBreakpointId(breakpoint); + const breakpoints = super.findMarkers({ uri, dataFilter: b => DebugUtils.makeBreakpointId(b) !== id }).map(m => m.data); + + super.setMarkers(uri, BREAKPOINT_OWNER, breakpoints); + } + + /** + * Gets breakpoints by the given criteria. + * @param dataFilter the filter + * @returns the list of breakpoints + */ + get(dataFilter?: (breakpoint: ExtDebugProtocol.AggregatedBreakpoint) => boolean): ExtDebugProtocol.AggregatedBreakpoint[] { + return super.findMarkers({ dataFilter }).map(m => m.data); + } + + /** + * Indicates if breakpoint with given id exists. + * @param id the breakpoint id + * @returns true if breakpoint exists and false otherwise + */ + exists(id: string): boolean { + return super.findMarkers().some(m => DebugUtils.makeBreakpointId(m.data) === id); + } + + private toUri(breakpoint: ExtDebugProtocol.AggregatedBreakpoint): URI { + if (DebugUtils.isSourceBreakpoint(breakpoint)) { + return DebugUtils.toUri(breakpoint.source!); + } + + if (DebugUtils.isFunctionBreakpoint(breakpoint)) { + return new URI().withScheme('brk').withPath('function'); + } + + if (DebugUtils.isExceptionBreakpoint(breakpoint)) { + return new URI().withScheme('brk').withPath('exception'); + } + + throw new Error('Unrecognized breakpoint type: ' + JSON.stringify(breakpoint)); + } +} diff --git a/packages/debug/src/browser/debug-command.ts b/packages/debug/src/browser/debug-command.ts new file mode 100644 index 0000000000000..15d57622cb50e --- /dev/null +++ b/packages/debug/src/browser/debug-command.ts @@ -0,0 +1,423 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { injectable, inject } from 'inversify'; +import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry } from '@theia/core/lib/common'; +import { MAIN_MENU_BAR, MenuPath } from '@theia/core/lib/common/menu'; +import { DebugService } from '../common/debug-common'; +import { DebugSessionManager } from './debug-session'; +import { DebugConfigurationManager } from './debug-configuration'; +import { DebugSelectionService } from './view/debug-selection-service'; +import { SingleTextInputDialog } from '@theia/core/lib/browser/dialogs'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { BreakpointsDialog } from './view/debug-breakpoints-widget'; + +export const DEBUG_SESSION_CONTEXT_MENU: MenuPath = ['debug-session-context-menu']; +export const DEBUG_SESSION_THREAD_CONTEXT_MENU: MenuPath = ['debug-session-thread-context-menu']; +export const DEBUG_VARIABLE_CONTEXT_MENU: MenuPath = ['debug-variable-context-menu']; + +export namespace DebugSessionContextMenu { + export const STOP = [...DEBUG_SESSION_CONTEXT_MENU, '1_stop']; +} + +export namespace DebugThreadContextMenu { + export const RESUME_THREAD = [...DEBUG_SESSION_THREAD_CONTEXT_MENU, '2_resume']; + export const SUSPEND_THREAD = [...RESUME_THREAD, '1_suspend']; + + export const STEPOUT_THREAD = [...DEBUG_SESSION_THREAD_CONTEXT_MENU, '5_stepout']; + export const STEPIN_THREAD = [...STEPOUT_THREAD, '4_stepin']; + export const STEP_THREAD = [...STEPIN_THREAD, '3_next']; +} + +export namespace DebugVariableContextMenu { + export const MODIFY = [...DEBUG_VARIABLE_CONTEXT_MENU, '1_modify']; +} + +export namespace DebugMenus { + export const DEBUG = [...MAIN_MENU_BAR, '4_debug']; + export const DEBUG_STOP = [...DEBUG, '2_stop']; + export const DEBUG_START = [...DEBUG_STOP, '1_start']; + + export const SUSPEND_ALL_THREADS = [...DEBUG, '4_suspend_all_threads']; + export const RESUME_ALL_THREADS = [...SUSPEND_ALL_THREADS, '3_resume_all_threads']; + + export const STEPOUT_THREAD = [...DEBUG, '7_stepout']; + export const STEPIN_THREAD = [...STEPOUT_THREAD, '6_stepin']; + export const STEP_THREAD = [...STEPIN_THREAD, '5_next']; + + export const ADD_CONFIGURATION = [...DEBUG, '9_add_configuration']; + export const OPEN_CONFIGURATION = [...ADD_CONFIGURATION, '8_open_configuration']; + export const SHOW_BREAKPOINTS = [...OPEN_CONFIGURATION, '10_breakpoinst']; +} + +export namespace DEBUG_COMMANDS { + export const START = { + id: 'debug.start', + label: 'Start' + }; + + export const STOP = { + id: 'debug.stop', + label: 'Stop' + }; + + export const OPEN_CONFIGURATION = { + id: 'debug.configuration.open', + label: 'Open configuration' + }; + + export const ADD_CONFIGURATION = { + id: 'debug.configuration.add', + label: 'Add configuration' + }; + + export const SUSPEND_THREAD = { + id: 'debug.thread.suspend', + label: 'Suspend thread' + }; + + export const RESUME_THREAD = { + id: 'debug.thread.resume', + label: 'Resume thread' + }; + export const SUSPEND_ALL_THREADS = { + id: 'debug.thread.suspend.all', + label: 'Suspend' + }; + + export const RESUME_ALL_THREADS = { + id: 'debug.thread.resume.all', + label: 'Resume' + }; + + export const MODIFY_VARIABLE = { + id: 'debug.variable.modify', + label: 'Modify' + }; + + export const SHOW_BREAKPOINTS = { + id: 'debug.breakpoints.show', + label: 'Breakpoints' + }; + + export const STEP = { + id: 'debug.thread.next', + label: 'Step' + }; + + export const STEPIN = { + id: 'debug.thread.stepin', + label: 'Step in' + }; + + export const STEPOUT = { + id: 'debug.thread.stepout', + label: 'Step out' + }; +} + +@injectable() +export class DebugCommandHandlers implements MenuContribution, CommandContribution { + constructor( + @inject(DebugService) protected readonly debug: DebugService, + @inject(DebugSessionManager) protected readonly debugSessionManager: DebugSessionManager, + @inject(DebugConfigurationManager) protected readonly debugConfigurationManager: DebugConfigurationManager, + @inject(DebugSelectionService) protected readonly debugSelectionHandler: DebugSelectionService, + @inject(BreakpointsDialog) protected readonly breakpointsDialog: BreakpointsDialog) { } + + registerMenus(menus: MenuModelRegistry): void { + menus.registerSubmenu(DebugMenus.DEBUG, 'Debug'); + menus.registerMenuAction(DebugMenus.DEBUG_START, { + commandId: DEBUG_COMMANDS.START.id + }); + menus.registerMenuAction(DebugMenus.DEBUG_STOP, { + commandId: DEBUG_COMMANDS.STOP.id + }); + menus.registerMenuAction(DebugMenus.OPEN_CONFIGURATION, { + commandId: DEBUG_COMMANDS.OPEN_CONFIGURATION.id + }); + menus.registerMenuAction(DebugMenus.ADD_CONFIGURATION, { + commandId: DEBUG_COMMANDS.ADD_CONFIGURATION.id + }); + menus.registerMenuAction(DebugSessionContextMenu.STOP, { + commandId: DEBUG_COMMANDS.STOP.id + }); + menus.registerMenuAction(DebugMenus.STEP_THREAD, { + commandId: DEBUG_COMMANDS.STEP.id + }); + menus.registerMenuAction(DebugMenus.STEPIN_THREAD, { + commandId: DEBUG_COMMANDS.STEPIN.id + }); + menus.registerMenuAction(DebugMenus.STEPOUT_THREAD, { + commandId: DEBUG_COMMANDS.STEPOUT.id + }); + menus.registerMenuAction(DebugMenus.SUSPEND_ALL_THREADS, { + commandId: DEBUG_COMMANDS.SUSPEND_ALL_THREADS.id + }); + menus.registerMenuAction(DebugMenus.RESUME_ALL_THREADS, { + commandId: DEBUG_COMMANDS.RESUME_ALL_THREADS.id + }); + menus.registerMenuAction(DebugMenus.SHOW_BREAKPOINTS, { + commandId: DEBUG_COMMANDS.SHOW_BREAKPOINTS.id + }); + menus.registerMenuAction(DebugThreadContextMenu.SUSPEND_THREAD, { + commandId: DEBUG_COMMANDS.SUSPEND_THREAD.id + }); + menus.registerMenuAction(DebugThreadContextMenu.RESUME_THREAD, { + commandId: DEBUG_COMMANDS.RESUME_THREAD.id + }); + menus.registerMenuAction(DebugThreadContextMenu.STEP_THREAD, { + commandId: DEBUG_COMMANDS.STEP.id + }); + menus.registerMenuAction(DebugThreadContextMenu.STEPIN_THREAD, { + commandId: DEBUG_COMMANDS.STEPIN.id + }); + menus.registerMenuAction(DebugThreadContextMenu.STEPOUT_THREAD, { + commandId: DEBUG_COMMANDS.STEPOUT.id + }); + menus.registerMenuAction(DebugVariableContextMenu.MODIFY, { + commandId: DEBUG_COMMANDS.MODIFY_VARIABLE.id + }); + } + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(DEBUG_COMMANDS.START); + registry.registerHandler(DEBUG_COMMANDS.START.id, { + execute: () => { + this.debugConfigurationManager.selectConfiguration() + .then(configuration => this.debug.resolveDebugConfiguration(configuration)) + .then(configuration => this.debug.start(configuration).then(sessionId => ({ sessionId, configuration }))) + .then(({ sessionId, configuration }) => this.debugSessionManager.create(sessionId, configuration)) + .catch(error => console.log(error)); + }, + isEnabled: () => true, + isVisible: () => true + }); + + registry.registerCommand(DEBUG_COMMANDS.STOP); + registry.registerHandler(DEBUG_COMMANDS.STOP.id, { + execute: () => { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (debugSession) { + debugSession.disconnect(); + } + }, + isEnabled: () => this.debugSessionManager.getActiveDebugSession() !== undefined, + isVisible: () => true + }); + + registry.registerCommand(DEBUG_COMMANDS.OPEN_CONFIGURATION); + registry.registerHandler(DEBUG_COMMANDS.OPEN_CONFIGURATION.id, { + execute: () => this.debugConfigurationManager.openConfigurationFile(), + isEnabled: () => true, + isVisible: () => true + }); + + registry.registerCommand(DEBUG_COMMANDS.ADD_CONFIGURATION); + registry.registerHandler(DEBUG_COMMANDS.ADD_CONFIGURATION.id, { + execute: () => this.debugConfigurationManager.addConfiguration(), + isEnabled: () => true, + isVisible: () => true + }); + + registry.registerCommand(DEBUG_COMMANDS.SHOW_BREAKPOINTS); + registry.registerHandler(DEBUG_COMMANDS.SHOW_BREAKPOINTS.id, { + execute: () => this.breakpointsDialog.open(), + isEnabled: () => true, + isVisible: () => true + }); + + registry.registerCommand(DEBUG_COMMANDS.SUSPEND_ALL_THREADS); + registry.registerHandler(DEBUG_COMMANDS.RESUME_ALL_THREADS.id, { + execute: () => { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (debugSession) { + debugSession.resumeAll(); + } + }, + isEnabled: () => { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (!debugSession) { + return false; + } + + const state = debugSession.state; + return !!state.isConnected && !state.allThreadsContinued; + }, + isVisible: () => true + }); + + registry.registerCommand(DEBUG_COMMANDS.RESUME_ALL_THREADS); + registry.registerHandler(DEBUG_COMMANDS.SUSPEND_ALL_THREADS.id, { + execute: () => { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (debugSession) { + debugSession.pauseAll(); + } + }, + isEnabled: () => { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (!debugSession) { + return false; + } + + const state = debugSession.state; + return !!state.isConnected && !state.allThreadsStopped; + }, + isVisible: () => true + }); + + registry.registerCommand(DEBUG_COMMANDS.STEP); + registry.registerHandler(DEBUG_COMMANDS.STEP.id, { + execute: () => { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (debugSession) { + const threadId = this.getSelectedThreadId(debugSession.sessionId); + if (threadId) { + debugSession.next({ threadId }); + } + } + }, + isEnabled: () => this.isSelectedThreadSuspended(), + isVisible: () => true + }); + + registry.registerCommand(DEBUG_COMMANDS.STEPIN); + registry.registerHandler(DEBUG_COMMANDS.STEPIN.id, { + execute: () => { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (debugSession) { + const threadId = this.getSelectedThreadId(debugSession.sessionId); + if (threadId) { + debugSession.stepIn({ threadId }); + } + } + }, + isEnabled: () => this.isSelectedThreadSuspended(), + isVisible: () => true + }); + + registry.registerCommand(DEBUG_COMMANDS.STEPOUT); + registry.registerHandler(DEBUG_COMMANDS.STEPOUT.id, { + execute: () => { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (debugSession) { + const threadId = this.getSelectedThreadId(debugSession.sessionId); + if (threadId) { + debugSession.stepOut({ threadId }); + } + } + }, + isEnabled: () => this.isSelectedThreadSuspended(), + isVisible: () => true + }); + + registry.registerCommand(DEBUG_COMMANDS.SUSPEND_THREAD); + registry.registerHandler(DEBUG_COMMANDS.SUSPEND_THREAD.id, { + execute: () => { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (debugSession) { + const threadId = this.getSelectedThreadId(debugSession.sessionId); + if (threadId) { + debugSession.pause({ threadId }); + } + } + }, + isEnabled: () => true, + isVisible: () => this.isSelectedThreadResumed() + }); + + registry.registerCommand(DEBUG_COMMANDS.RESUME_THREAD); + registry.registerHandler(DEBUG_COMMANDS.RESUME_THREAD.id, { + execute: () => { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (debugSession) { + const threadId = this.getSelectedThreadId(debugSession.sessionId); + if (threadId) { + debugSession.resume({ threadId }); + } + } + }, + isEnabled: () => true, + isVisible: () => this.isSelectedThreadSuspended() + }); + + registry.registerCommand(DEBUG_COMMANDS.MODIFY_VARIABLE); + registry.registerHandler(DEBUG_COMMANDS.MODIFY_VARIABLE.id, { + execute: () => { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (debugSession) { + const selection = this.debugSelectionHandler.get(debugSession.sessionId); + if (selection.variable) { + const variable = selection.variable; + const dialog = new SingleTextInputDialog({ + title: `Modify: ${variable.name}`, + initialValue: variable.value + }); + + dialog.open().then(newValue => { + if (newValue) { + const args: DebugProtocol.SetVariableArguments = { + variablesReference: variable.parentVariablesReference, + name: variable.name, + value: newValue + }; + debugSession.setVariable(args); + } + }); + } + } + }, + isEnabled: () => true, + isVisible: () => { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (!debugSession) { + return false; + } + + const selection = this.debugSelectionHandler.get(debugSession.sessionId); + return !!selection && !!selection.variable; + } + }); + } + + private isSelectedThreadSuspended(): boolean { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (!debugSession) { + return false; + } + + const selection = this.debugSelectionHandler.get(debugSession.sessionId); + return !!selection && !!selection.thread && !!debugSession.state.stoppedThreadIds.has(selection.thread.id); + } + + private isSelectedThreadResumed(): boolean { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (!debugSession) { + return false; + } + + const selection = this.debugSelectionHandler.get(debugSession.sessionId); + return !!selection && !!selection.thread && !debugSession.state.stoppedThreadIds.has(selection.thread.id); + } + + private getSelectedThreadId(sessionId: string): number | undefined { + const selection = this.debugSelectionHandler.get(sessionId); + if (!!selection && !!selection.thread) { + return selection.thread.id; + } + } +} diff --git a/packages/debug/src/browser/debug-configuration.ts b/packages/debug/src/browser/debug-configuration.ts new file mode 100644 index 0000000000000..3da11f461fa2e --- /dev/null +++ b/packages/debug/src/browser/debug-configuration.ts @@ -0,0 +1,179 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { injectable, inject } from 'inversify'; +import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; +import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import URI from '@theia/core/lib/common/uri'; +import { EditorManager, EditorWidget } from '@theia/editor/lib/browser'; +import { QuickOpenService, QuickOpenItem, QuickOpenMode } from '@theia/core/lib/browser'; +import { DebugService, DebugConfiguration } from '../common/debug-common'; + +@injectable() +export class DebugConfigurationManager { + private static readonly CONFIG = '.theia/launch.json'; + + @inject(FileSystem) + protected readonly fileSystem: FileSystem; + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + @inject(EditorManager) + protected readonly editorManager: EditorManager; + @inject(DebugService) + protected readonly debug: DebugService; + @inject(QuickOpenService) + protected readonly quickOpenService: QuickOpenService; + + /** + * Opens configuration file in the editor. + */ + openConfigurationFile(): Promise { + return this.resolveConfigurationFile().then(configFile => this.editorManager.open(new URI(configFile.uri))); + } + + /** + * Adds a new configuration to the configuration file. + */ + addConfiguration(): Promise { + return this.provideDebugTypes() + .then(debugType => this.provideDebugConfigurations(debugType)) + .then(newDebugConfiguration => this.readConfigurations().then(configurations => configurations.concat(newDebugConfiguration))) + .then(configurations => this.writeConfigurations(configurations)) + .then(() => this.openConfigurationFile()) + .then(() => { }); + } + + /** + * Selects the debug configuration to start debug adapter. + */ + selectConfiguration(): Promise { + const result = new Deferred(); + + return this.readConfigurations() + .then(configurations => { + if (configurations.length !== 0) { + const items = configurations.map(configuration => this.toQuickOpenItem(configuration.type + ' : ' + configuration.name, result, configuration)); + return Promise.resolve(items); + } + return Promise.reject('There are no provided debug configurations.'); + }) + .then(items => this.doOpen(items)) + .then(() => result.promise); + } + + readConfigurations(): Promise { + return this.resolveConfigurationFile() + .then(configFile => this.fileSystem.resolveContent(configFile.uri)) + .then(({ stat, content }) => { + if (content.length === 0) { + return []; + } + + try { + return JSON.parse(content); + } catch (error) { + return Promise.reject('Configuration file bad format.'); + } + }); + } + + writeConfigurations(configurations: DebugConfiguration[]): Promise { + return this.resolveConfigurationFile() + .then(configFile => { + const jsonPretty = JSON.stringify(configurations, (key, value) => value, 2); + return this.fileSystem.setContent(configFile, jsonPretty); + }) + .then(() => { }); + } + + /** + * Creates and returns configuration file. + * @returns [configuration file](#FileStat). + */ + resolveConfigurationFile(): Promise { + const root = this.workspaceService.tryGetRoots()[0]; + if (!root) { + return Promise.reject('Workspace is not opened yet.'); + } + + const uri = root.uri + '/' + DebugConfigurationManager.CONFIG; + return this.fileSystem.exists(uri) + .then(exists => { + if (exists) { + return this.fileSystem.getFileStat(uri); + } else { + return this.fileSystem.createFile(uri, { encoding: 'utf8' }); + } + }).then(configFile => { + if (configFile) { + return Promise.resolve(configFile); + } + return Promise.reject(`Configuration file '${DebugConfigurationManager.CONFIG}' not found.`); + }); + } + + private provideDebugTypes(): Promise { + const result = new Deferred(); + + return this.debug.debugTypes() + .then(debugTypes => { + if (debugTypes.length !== 0) { + const items = debugTypes.map(debugType => this.toQuickOpenItem(debugType, result, debugType)); + return Promise.resolve(items); + } + return Promise.reject('There are no registered debug adapters.'); + }) + .then(items => this.doOpen(items)) + .then(() => result.promise); + } + + private provideDebugConfigurations(debugType: string): Promise { + const result = new Deferred(); + + return this.debug.provideDebugConfigurations(debugType) + .then(configurations => { + if (configurations) { + const items = configurations.map(configuration => this.toQuickOpenItem(configuration.name, result, configuration)); + return Promise.resolve(items); + } + return Promise.reject(`There are no provided debug configurations for ${debugType}`); + }) + .then(items => this.doOpen(items)) + .then(() => result.promise); + } + + private toQuickOpenItem(label: string, result: Deferred, value: T): QuickOpenItem { + return new QuickOpenItem({ + label: label, + run(mode: QuickOpenMode): boolean { + if (mode === QuickOpenMode.OPEN) { + result.resolve(value); + return true; + } + return false; + } + }); + } + + private doOpen(items: QuickOpenItem[]): void { + this.quickOpenService.open({ + onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void { + acceptor(items); + } + }); + } +} diff --git a/packages/debug/src/browser/debug-frontend-module.ts b/packages/debug/src/browser/debug-frontend-module.ts new file mode 100644 index 0000000000000..f28075417b2ed --- /dev/null +++ b/packages/debug/src/browser/debug-frontend-module.ts @@ -0,0 +1,150 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { ContainerModule, interfaces, Container } from 'inversify'; +import { DebugCommandHandlers, DEBUG_VARIABLE_CONTEXT_MENU } from './debug-command'; +import { DebugConfigurationManager } from './debug-configuration'; +import { + DebugViewContribution, + DebugWidget, + DEBUG_FACTORY_ID, + DebugTargetWidget, +} from './view/debug-view-contribution'; +import { DebugPath, DebugService } from '../common/debug-common'; +import { MenuContribution } from '@theia/core/lib/common/menu'; +import { CommandContribution } from '@theia/core/lib/common/command'; +import { + WidgetFactory, + bindViewContribution, + WebSocketConnectionProvider, + createTreeContainer, + TreeImpl, + Tree, + TreeWidget, + TreeProps, + defaultTreeProps, + TreeModelImpl, + TreeModel, + FrontendApplicationContribution +} from '@theia/core/lib/browser'; +import { + DebugSession, + DebugSessionContribution, + DebugSessionFactory +} from './debug-model'; +import { + DebugSessionManager, + DefaultDebugSessionFactory, + DebugResourceResolver +} from './debug-session'; +import { + DebugVariablesWidget, DebugVariableModel, DebugVariablesTree, +} from './view/debug-variables-widget'; +import '../../src/browser/style/index.css'; +import { DebugThreadsWidget } from './view/debug-threads-widget'; +import { DebugStackFramesWidget } from './view/debug-stack-frames-widget'; +import { DebugBreakpointsWidget, BreakpointsDialog } from './view/debug-breakpoints-widget'; +import { DebugSelectionService, DebugSelection } from './view/debug-selection-service'; +import { bindContributionProvider, ResourceResolver } from '@theia/core'; +import { ActiveLineDecorator, BreakpointDecorator } from './breakpoint/breakpoint-decorators'; +import { BreakpointsManager } from './breakpoint/breakpoint-manager'; +import { BreakpointStorage } from './breakpoint/breakpoint-marker'; +import { SourceOpener } from './debug-utils'; +import { BreakpointsApplier } from './breakpoint/breakpoint-applier'; + +export const DEBUG_VARIABLES_PROPS = { + ...defaultTreeProps, + contextMenuPath: DEBUG_VARIABLE_CONTEXT_MENU, + multiSelect: false +}; + +export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => { + bindDebugSession(bind); + bindBreakpointsManager(bind); + bindDebugView(bind); + + bind(MenuContribution).to(DebugCommandHandlers); + bind(CommandContribution).to(DebugCommandHandlers); + bind(DebugConfigurationManager).toSelf().inSingletonScope(); + + bind(DebugService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, DebugPath)).inSingletonScope(); + bind(DebugResourceResolver).toSelf().inSingletonScope(); + bind(ResourceResolver).toService(DebugResourceResolver); +}); + +function bindDebugView(bind: interfaces.Bind): void { + bind(DebugWidget).toSelf(); + bind(WidgetFactory).toDynamicValue(context => ({ + id: DEBUG_FACTORY_ID, + createWidget: () => context.container.get(DebugWidget) + })).inSingletonScope(); + + bindViewContribution(bind, DebugViewContribution); + bind(DebugTargetWidget).toSelf(); + bind(DebugSelectionService).toSelf().inSingletonScope(); + + bind>('Factory').toFactory(context => + (debugSession: DebugSession) => { + const container = createDebugTargetContainer(context, debugSession); + return container.get(DebugTargetWidget); + } + ); +} + +function bindBreakpointsManager(bind: interfaces.Bind): void { + bind(BreakpointsDialog).toSelf().inSingletonScope(); + bind(ActiveLineDecorator).toSelf().inSingletonScope(); + bind(BreakpointDecorator).toSelf().inSingletonScope(); + bind(BreakpointStorage).toSelf().inSingletonScope(); + bind(BreakpointsApplier).toSelf().inSingletonScope(); + bind(BreakpointsManager).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toDynamicValue(ctx => ctx.container.get(BreakpointsManager)); + bind(SourceOpener).toSelf().inSingletonScope(); +} + +function bindDebugSession(bind: interfaces.Bind): void { + bindContributionProvider(bind, DebugSessionContribution); + bind(DebugSessionFactory).to(DefaultDebugSessionFactory).inSingletonScope(); + bind(DebugSessionManager).toSelf().inSingletonScope(); +} + +function createDebugTargetContainer(context: interfaces.Context, debugSession: DebugSession): Container { + const child = createTreeContainer(context.container); + + const debugSelectionService = context.container.get(DebugSelectionService); + const selection = debugSelectionService.get(debugSession.sessionId); + + child.bind(DebugSession).toConstantValue(debugSession); + child.bind(DebugSelection).toConstantValue(selection); + child.bind(DebugThreadsWidget).toSelf(); + child.bind(DebugStackFramesWidget).toSelf(); + child.bind(DebugBreakpointsWidget).toSelf(); + + child.rebind(TreeProps).toConstantValue(DEBUG_VARIABLES_PROPS); + + child.unbind(TreeModelImpl); + child.bind(DebugVariableModel).toSelf(); + child.rebind(TreeModel).toDynamicValue(ctx => ctx.container.get(DebugVariableModel)); + + child.unbind(TreeImpl); + child.bind(DebugVariablesTree).toSelf(); + child.rebind(Tree).toDynamicValue(ctx => ctx.container.get(DebugVariablesTree)); + + child.unbind(TreeWidget); + child.bind(DebugVariablesWidget).toSelf(); + + return child; +} diff --git a/packages/debug/src/browser/debug-model.ts b/packages/debug/src/browser/debug-model.ts new file mode 100644 index 0000000000000..9f5e974c9d6f5 --- /dev/null +++ b/packages/debug/src/browser/debug-model.ts @@ -0,0 +1,87 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { DebugConfiguration, DebugSessionState } from '../common/debug-common'; +import { Disposable } from '@theia/core'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +/** + * DebugSession symbol for DI. + */ +export const DebugSession = Symbol('DebugSession'); + +/** + * The debug session. + */ +export interface DebugSession extends Disposable, NodeJS.EventEmitter { + readonly sessionId: string; + readonly configuration: DebugConfiguration; + readonly state: DebugSessionState; + + initialize(args: DebugProtocol.InitializeRequestArguments): Promise; + configurationDone(): Promise; + attach(args: DebugProtocol.AttachRequestArguments): Promise; + launch(args: DebugProtocol.LaunchRequestArguments): Promise; + threads(): Promise; + stacks(args: DebugProtocol.StackTraceArguments): Promise; + pause(args: DebugProtocol.PauseArguments): Promise; + pauseAll(): Promise; + resume(args: DebugProtocol.ContinueArguments): Promise; + resumeAll(): Promise; + disconnect(): Promise; + scopes(args: DebugProtocol.ScopesArguments): Promise; + variables(args: DebugProtocol.VariablesArguments): Promise; + setVariable(args: DebugProtocol.SetVariableArguments): Promise; + evaluate(args: DebugProtocol.EvaluateArguments): Promise; + source(args: DebugProtocol.SourceArguments): Promise; + setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): Promise; + next(args: DebugProtocol.NextArguments): Promise; + stepIn(args: DebugProtocol.StepInArguments): Promise; + stepOut(args: DebugProtocol.StepOutArguments): Promise; +} + +/** + * DebugSessionFactory symbol for DI. + */ +export const DebugSessionFactory = Symbol('DebugSessionFactory'); + +/** + * The [debug session](#DebugSession) factory. + */ +export interface DebugSessionFactory { + get(sessionId: string, debugConfiguration: DebugConfiguration): DebugSession; +} + +/** + * DebugSessionContribution symbol for DI. + */ +export const DebugSessionContribution = Symbol('DebugSessionContribution'); + +/** + * The [debug session](#DebugSession) contribution. + * Can be used to instantiate a specific debug sessions. + */ +export interface DebugSessionContribution { + /** + * The debug type. + */ + debugType: string; + + /** + * The [debug session](#DebugSession) factory. + */ + debugSessionFactory(): DebugSessionFactory; +} diff --git a/packages/debug/src/browser/debug-session.spec.ts b/packages/debug/src/browser/debug-session.spec.ts new file mode 100644 index 0000000000000..412687575cd9b --- /dev/null +++ b/packages/debug/src/browser/debug-session.spec.ts @@ -0,0 +1,15 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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/debug-session.ts b/packages/debug/src/browser/debug-session.ts new file mode 100644 index 0000000000000..4b24f15c28565 --- /dev/null +++ b/packages/debug/src/browser/debug-session.ts @@ -0,0 +1,480 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { injectable, inject, named } from 'inversify'; +import { Endpoint } from '@theia/core/lib/browser'; +import { + DebugAdapterPath, + DebugConfiguration, + DebugSessionState, + DebugSessionStateAccumulator +} from '../common/debug-common'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { Emitter, Event, DisposableCollection, ContributionProvider, Resource, ResourceResolver } from '@theia/core'; +import { EventEmitter } from 'events'; +import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; +import { DebugSession, DebugSessionFactory, DebugSessionContribution } from './debug-model'; +import URI from '@theia/core/lib/common/uri'; +import { BreakpointsApplier } from './breakpoint/breakpoint-applier'; + +/** + * Stack frame format. + */ +const DEFAULT_STACK_FRAME_FORMAT: DebugProtocol.StackFrameFormat = { + parameters: true, + parameterTypes: true, + parameterNames: true, + parameterValues: true, + line: true, + module: true, + includeAll: true, + hex: false +}; + +/** + * Initialize requests arguments. + */ +const INITIALIZE_ARGUMENTS = { + clientID: 'Theia', + locale: '', + linesStartAt1: true, + columnsStartAt1: true, + pathFormat: 'path', + supportsVariableType: false, + supportsVariablePaging: false, + supportsRunInTerminalRequest: false +}; + +/** + * DebugSession implementation. + */ +export class DebugSessionImpl extends EventEmitter implements DebugSession { + protected readonly toDispose = new DisposableCollection(); + protected readonly callbacks = new Map void>(); + + protected websocket: Promise; + + private sequence: number; + + constructor( + public readonly sessionId: string, + public readonly configuration: DebugConfiguration, + public readonly state: DebugSessionState) { + + super(); + this.state = new DebugSessionStateAccumulator(this, state); + this.websocket = this.createWebSocket(); + this.sequence = 1; + } + + private createWebSocket(): Promise { + const path = DebugAdapterPath + '/' + this.sessionId; + const url = new Endpoint({ path }).getWebSocketUrl().toString(); + const websocket = new WebSocket(url); + + const initialized = new Deferred(); + + websocket.onopen = () => initialized.resolve(websocket); + websocket.onclose = () => this.onClose(); + websocket.onerror = () => initialized.reject(`Failed to establish connection with debug adapter by url: '${url}'`); + websocket.onmessage = (event: MessageEvent): void => this.handleMessage(event); + + return initialized.promise; + } + + initialize(args: DebugProtocol.InitializeRequestArguments): Promise { + return this.proceedRequest('initialize', args); + } + + attach(args: DebugProtocol.AttachRequestArguments): Promise { + return this.proceedRequest('attach', args); + } + + launch(args: DebugProtocol.LaunchRequestArguments): Promise { + return this.proceedRequest('launch', args); + } + + threads(): Promise { + return this.proceedRequest('threads'); + } + + pauseAll(): Promise { + return this.threads().then(response => Promise.all(response.body.threads.map((thread: DebugProtocol.Thread) => this.pause({ threadId: thread.id })))); + } + + pause(args: DebugProtocol.PauseArguments): Promise { + return this.proceedRequest('pause', args); + } + + resumeAll(): Promise { + return this.threads().then(response => Promise.all(response.body.threads.map((thread: DebugProtocol.Thread) => this.resume({ threadId: thread.id })))); + } + + resume(args: DebugProtocol.ContinueArguments): Promise { + return this.proceedRequest('continue', args); + } + + stacks(args: DebugProtocol.StackTraceArguments): Promise { + if (!args.format) { + args.format = DEFAULT_STACK_FRAME_FORMAT; + } + return this.proceedRequest('stackTrace', args); + } + + configurationDone(): Promise { + return this.proceedRequest('configurationDone'); + } + + disconnect(): Promise { + return this.proceedRequest('disconnect', { terminateDebuggee: true }); + } + + scopes(args: DebugProtocol.ScopesArguments): Promise { + return this.proceedRequest('scopes', args); + } + + variables(args: DebugProtocol.VariablesArguments): Promise { + return this.proceedRequest('variables', args); + } + + setVariable(args: DebugProtocol.SetVariableArguments): Promise { + return this.proceedRequest('setVariable', args); + } + + evaluate(args: DebugProtocol.EvaluateArguments): Promise { + return this.proceedRequest('evaluate', args); + } + + source(args: DebugProtocol.SourceArguments): Promise { + return this.proceedRequest('source', args); + } + + setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): Promise { + return this.proceedRequest('setBreakpoints', args); + } + + next(args: DebugProtocol.NextArguments): Promise { + return this.proceedRequest('next', args); + } + + stepIn(args: DebugProtocol.StepInArguments): Promise { + return this.proceedRequest('stepIn', args); + } + + stepOut(args: DebugProtocol.StepOutArguments): Promise { + return this.proceedRequest('stepOut', args); + } + + protected handleMessage(event: MessageEvent) { + const message: DebugProtocol.ProtocolMessage = JSON.parse(event.data); + if (message.type === 'response') { + this.proceedResponse(message as DebugProtocol.Response); + } else if (message.type === 'event') { + this.proceedEvent(message as DebugProtocol.Event); + } + } + + protected proceedRequest(command: string, args?: {}): Promise { + const result = new Deferred(); + + const request: DebugProtocol.Request = { + seq: this.sequence++, + type: 'request', + command: command, + arguments: args + }; + + this.callbacks.set(request.seq, (response: T) => { + if (!response.success) { + result.reject(response); + } else { + result.resolve(response); + } + }); + + return this.websocket + .then(websocket => websocket.send(JSON.stringify(request))) + .then(() => result.promise); + } + + protected proceedResponse(response: DebugProtocol.Response): void { + const callback = this.callbacks.get(response.request_seq); + if (callback) { + this.callbacks.delete(response.request_seq); + callback(response); + } + } + + protected proceedEvent(event: DebugProtocol.Event): void { + this.emit(event.event, event); + } + + protected onClose(): void { + if (this.state.isConnected) { + const event: DebugProtocol.TerminatedEvent = { + event: 'terminated', + type: 'event', + seq: -1, + }; + this.proceedEvent(event); + } + } + + dispose() { + this.callbacks.clear(); + this.websocket + .then(websocket => websocket.close()) + .catch(error => console.error(error)); + } +} + +@injectable() +export class DefaultDebugSessionFactory implements DebugSessionFactory { + get(sessionId: string, debugConfiguration: DebugConfiguration): DebugSession { + const state: DebugSessionState = { + isConnected: false, + sources: new Map(), + stoppedThreadIds: new Set(), + allThreadsContinued: false, + allThreadsStopped: false, + capabilities: {} + }; + return new DebugSessionImpl(sessionId, debugConfiguration, state); + } +} + +/** It is intended to manage active debug sessions. */ +@injectable() +export class DebugSessionManager { + private activeDebugSessionId: string | undefined; + + protected readonly sessions = new Map(); + protected readonly contribs = new Map(); + protected readonly onDidPreCreateDebugSessionEmitter = new Emitter(); + protected readonly onDidCreateDebugSessionEmitter = new Emitter(); + protected readonly onDidChangeActiveDebugSessionEmitter = new Emitter<[DebugSession | undefined, DebugSession | undefined]>(); + protected readonly onDidDestroyDebugSessionEmitter = new Emitter(); + + constructor( + @inject(DebugSessionFactory) protected readonly debugSessionFactory: DebugSessionFactory, + @inject(OutputChannelManager) protected readonly outputChannelManager: OutputChannelManager, + @inject(ContributionProvider) @named(DebugSessionContribution) protected readonly contributions: ContributionProvider, + @inject(BreakpointsApplier) protected readonly breakpointApplier: BreakpointsApplier) { + + for (const contrib of this.contributions.getContributions()) { + this.contribs.set(contrib.debugType, contrib); + } + } + + /** + * Creates a new [debug session](#DebugSession). + * @param sessionId The session identifier + * @param configuration The debug configuration + * @returns The debug session + */ + create(sessionId: string, debugConfiguration: DebugConfiguration): Promise { + this.onDidPreCreateDebugSessionEmitter.fire(sessionId); + + const contrib = this.contribs.get(debugConfiguration.type); + const sessionFactory = contrib ? contrib.debugSessionFactory() : this.debugSessionFactory; + const session = sessionFactory.get(sessionId, debugConfiguration); + this.sessions.set(sessionId, session); + + this.onDidCreateDebugSessionEmitter.fire(session); + + const channel = this.outputChannelManager.getChannel(debugConfiguration.name); + session.on('output', event => { + const outputEvent = (event as DebugProtocol.OutputEvent); + channel.appendLine(outputEvent.body.output); + }); + session.on('terminated', () => this.destroy(sessionId)); + + const initializeArgs: DebugProtocol.InitializeRequestArguments = { + ...INITIALIZE_ARGUMENTS, + adapterID: debugConfiguration.type + }; + + return session.initialize(initializeArgs) + .then(() => { + const request = debugConfiguration.request; + switch (request) { + case 'attach': { + const attachArgs: DebugProtocol.AttachRequestArguments = Object.assign(debugConfiguration, { __restart: false }); + return session.attach(attachArgs); + } + case 'launch': { + const launchArgs: DebugProtocol.LaunchRequestArguments = Object.assign(debugConfiguration, { __restart: false, noDebug: false }); + return session.launch(launchArgs); + } + default: return Promise.reject(`Unsupported request '${request}' type.`); + } + }) + .then(() => this.breakpointApplier.applySessionBreakpoints(session)) + .then(() => session.configurationDone()) + .then(() => session); + } + + /** + * Removes the [debug session](#DebugSession). + * @param sessionId The session identifier + */ + remove(sessionId: string): void { + this.sessions.delete(sessionId); + if (this.activeDebugSessionId) { + if (this.activeDebugSessionId === sessionId) { + if (this.sessions.size !== 0) { + this.setActiveDebugSession(this.sessions.keys().next().value); + } else { + this.setActiveDebugSession(undefined); + } + } + } + } + + /** + * Finds a debug session by its identifier. + * @returns The debug sessions + */ + find(sessionId: string): DebugSession | undefined { + return this.sessions.get(sessionId); + } + + /** + * Finds all instantiated debug sessions. + * @returns An array of debug sessions + */ + findAll(): DebugSession[] { + return Array.from(this.sessions.values()); + } + + /** + * Sets the active debug session. + * @param sessionId The session identifier + */ + setActiveDebugSession(sessionId: string | undefined) { + const oldActiveSessionSession = this.activeDebugSessionId ? this.find(this.activeDebugSessionId) : undefined; + + if (this.activeDebugSessionId !== sessionId) { + this.activeDebugSessionId = sessionId; + this.onDidChangeActiveDebugSessionEmitter.fire([oldActiveSessionSession, this.getActiveDebugSession()]); + } + } + + /** + * Returns the active debug session. + * @returns the [debug session](#DebugSession) + */ + getActiveDebugSession(): DebugSession | undefined { + if (this.activeDebugSessionId) { + return this.sessions.get(this.activeDebugSessionId); + } + } + + /** + * Destroy the debug session. If session identifier isn't provided then + * all active debug session will be destroyed. + * @param sessionId The session identifier + */ + destroy(sessionId?: string): void { + if (sessionId) { + const session = this.sessions.get(sessionId); + if (session) { + this.doDestroy(session); + } + } else { + this.sessions.forEach(session => this.doDestroy(session)); + } + } + + private doDestroy(session: DebugSession): void { + session.dispose(); + this.remove(session.sessionId); + this.onDidDestroyDebugSessionEmitter.fire(session); + } + + get onDidChangeActiveDebugSession(): Event<[DebugSession | undefined, DebugSession | undefined]> { + return this.onDidChangeActiveDebugSessionEmitter.event; + } + + get onDidPreCreateDebugSession(): Event { + return this.onDidPreCreateDebugSessionEmitter.event; + } + + get onDidCreateDebugSession(): Event { + return this.onDidCreateDebugSessionEmitter.event; + } + + get onDidDestroyDebugSession(): Event { + return this.onDidDestroyDebugSessionEmitter.event; + } +} + +/** + * DAP resource. + */ +export const DAP_SCHEME = 'dap'; + +export class DebugResource implements Resource { + + constructor( + public uri: URI, + protected readonly debugSessionManager: DebugSessionManager, + ) { } + + dispose(): void { } + + readContents(options: { encoding?: string }): Promise { + const debugSession = this.debugSessionManager.getActiveDebugSession(); + if (!debugSession) { + throw new Error(`There is no active debug session to load content '${this.uri}'`); + } + + const sourceReference = this.uri.query; + if (sourceReference) { + return debugSession.source({ sourceReference: Number.parseInt(sourceReference) }).then(response => response.body.content); + } + + const path = this.uri.path.toString(); + const source = debugSession.state.sources.get(path); + if (!source) { + throw new Error(`There is no loaded source for '${this.uri}'`); + } + + if (!source.sourceReference) { + throw new Error(`sourceReference isn't specified '${this.uri}'`); + } + + return debugSession.source({ sourceReference: source.sourceReference }).then(response => response.body.content); + } +} + +@injectable() +export class DebugResourceResolver implements ResourceResolver { + + constructor( + @inject(DebugSessionManager) + protected readonly debugSessionManager: DebugSessionManager + ) { } + + resolve(uri: URI): DebugResource { + if (uri.scheme !== DAP_SCHEME) { + throw new Error('The given URI is not a valid dap uri: ' + uri); + } + + return new DebugResource(uri, this.debugSessionManager); + } +} diff --git a/packages/debug/src/browser/debug-utils.ts b/packages/debug/src/browser/debug-utils.ts new file mode 100644 index 0000000000000..fbb59384d897c --- /dev/null +++ b/packages/debug/src/browser/debug-utils.ts @@ -0,0 +1,210 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { injectable, inject } from 'inversify'; +import { EditorManager, EditorOpenerOptions, Position } from '@theia/editor/lib/browser'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { EditorWidget } from '@theia/editor/lib/browser/editor-widget'; +import URI from '@theia/core/lib/common/uri'; +import { ExtDebugProtocol } from '../common/debug-common'; +import { DebugSession } from './debug-model'; + +@injectable() +export class SourceOpener { + constructor(@inject(EditorManager) protected readonly editorManager: EditorManager) { } + + open(frame: DebugProtocol.StackFrame): Promise { + if (!frame.source) { + return Promise.reject(`The source '${frame.name}' to open is not specified.`); + } + + const uri = DebugUtils.toUri(frame.source); + return this.editorManager.open(uri, this.toEditorOpenerOption(frame)); + } + + private toEditorOpenerOption(frame: DebugProtocol.StackFrame): EditorOpenerOptions { + return { + selection: { start: Position.create(frame.line - 1, frame.column - 1) } + }; + } +} + +export namespace DebugUtils { + /** + * Creates a unique breakpoint identifier based on its origin. + * @param breakpoint the breakpoint + * @returns the breakpoint unique identifier + */ + export function makeBreakpointId(breakpoint: ExtDebugProtocol.AggregatedBreakpoint | DebugProtocol.Breakpoint): string { + if ('origin' in breakpoint) { + if (isSourceBreakpoint(breakpoint)) { + return makeSourceBrkId(breakpoint.source!, breakpoint.origin as DebugProtocol.SourceBreakpoint); + } else if (isFunctionBreakpoint(breakpoint)) { + return makeFunctionBrkId(breakpoint.origin as DebugProtocol.FunctionBreakpoint); + } else if (isExceptionBreakpoint(breakpoint)) { + return makeExceptionBrkId(breakpoint.origin as ExtDebugProtocol.ExceptionBreakpoint); + } + } else if (!!breakpoint.source && !!breakpoint.line) { + const sourceBreakpoint = { + line: breakpoint.line, + column: breakpoint.column + }; + + return makeSourceBrkId(breakpoint.source, sourceBreakpoint); + } + + throw new Error('Unrecognized breakpoint type: ' + JSON.stringify(breakpoint)); + } + + export function isSourceBreakpoint(breakpoint: ExtDebugProtocol.AggregatedBreakpoint): boolean { + return !!breakpoint.source; + } + + export function isFunctionBreakpoint(breakpoint: ExtDebugProtocol.AggregatedBreakpoint): boolean { + return 'name' in breakpoint.origin; + } + + export function isExceptionBreakpoint(breakpoint: ExtDebugProtocol.AggregatedBreakpoint): boolean { + return 'filter' in breakpoint.origin; + } + + function makeExceptionBrkId(breakpoint: ExtDebugProtocol.ExceptionBreakpoint): string { + return 'brk-exception-' + breakpoint.filter; + } + + function makeFunctionBrkId(breakpoint: DebugProtocol.FunctionBreakpoint): string { + return 'brk-function-' + breakpoint.name; + } + + function makeSourceBrkId(source: DebugProtocol.Source, breakpoint: DebugProtocol.SourceBreakpoint): string { + return 'brk-source-' + // Accordingly to the spec either path or reference ought to be specified. + + (source.path || source.sourceReference!) + + `-${breakpoint.line}` + + (breakpoint.column ? `:${breakpoint.column}` : ''); + } + + /** + * Indicates if two entities has the same id. + * @param left the left entity + * @param right the right entity + * @returns true if two entities have the same id otherwise it returns false + */ + export function isEqual(left: { id: number } | number | undefined, right: { id: number } | number | undefined): boolean { + return getId(left) === getId(right); + } + + function getId(entity: { id: number } | number | undefined): number | undefined { + if (typeof entity === 'number') { + return entity; + } + return entity && entity.id; + } + + /** + * Converts the [source](#DebugProtocol.Source) to a [uri](#URI). + * @param source the debug source + * @returns an [uri](#URI) referring to the source + */ + export function toUri(source: DebugProtocol.Source): URI { + if (source.sourceReference && source.sourceReference > 0) { + // Every source returned from the debug adapter has a name + return new URI().withScheme('dap').withPath(source.name!).withQuery(source.sourceReference.toString()); + } + + if (source.path) { + return new URI().withScheme('file').withPath(source.path); + } + + throw new Error('Unrecognized source type: ' + JSON.stringify(source)); + } + + /** + * Converts the [uri](#URI) to [debug source](#DebugProtocol.Source). + * @param uri an [uri](#URI) referring to the source + * @param debugSession [debug session](#DebugSession) + * @returns an [debug source](#DebugProtocol.Source) referred by the uri + */ + export function toSource(uri: URI, debugSession: DebugSession | undefined): DebugProtocol.Source { + const sourceReference = uri.query; + + if (debugSession) { + const source = sourceReference + ? debugSession.state.sources.get(sourceReference) + : debugSession.state.sources.get(uri.path.toString()); + if (source) { + return source; + } + } + + if (sourceReference) { + return { + sourceReference: Number.parseInt(sourceReference), + name: uri.path.toString() + }; + } + + return { + name: uri.displayName, + path: uri.path.toString() + }; + } + + /** + * Groups breakpoints by their source. + * @param breakpoints the breakpoints to group + * @return grouped breakpoints by their source + */ + export function groupBySource(breakpoints: ExtDebugProtocol.AggregatedBreakpoint[]): Map { + return breakpoints + .filter(breakpoint => isSourceBreakpoint(breakpoint)) + .reduce((sourced, breakpoint) => { + const uri = toUri(breakpoint.source!).toString(); + + const arr = sourced.get(uri) || []; + arr.push(breakpoint); + sourced.set(uri, arr); + + return sourced; + }, new Map()); + } + + /** + * Indicates if the breakpoint has a source that refers to the same uri as provided. + * @param breakpoint (breakpoint)[#ExtDebugProtocol.AggregatedBreakpoint] + * @param uri (URI)[#URI] + * @returns true breakpoint has a source that refers to the same uri otherwise function returns false + */ + export function checkUri(breakpoint: ExtDebugProtocol.AggregatedBreakpoint, uri: URI): boolean { + return toUri(breakpoint.source!).toString() === uri.toString(); + } + + /** + * Indicates if given source fits any of patterns. + */ + export function checkPattern(source: DebugProtocol.Source, filePatterns: string[]): boolean { + for (const pattern of filePatterns) { + // Every source returned from the debug adapter has a name + const name = source.name!; + + if (new RegExp(pattern).test(name)) { + return true; + } + } + + return false; + } +} diff --git a/packages/debug/src/browser/style/index.css b/packages/debug/src/browser/style/index.css new file mode 100644 index 0000000000000..dd7cccff97742 --- /dev/null +++ b/packages/debug/src/browser/style/index.css @@ -0,0 +1,126 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 + ********************************************************************************/ + +.theia-debug-panel { } + +.theia-debug-panel:focus { + outline: 0; + box-shadow: none; +} + +.theia-debug-target { + display: inline-table; + color: var(--theia-ui-font-color1); + background: var(--theia-layout-color0); + font-size: var(--theia-ui-font-size1); + width: 100%; +} + +.theia-debug-entry { } + +.theia-debug-entry:focus .theia-mod-selected { + background: var(--theia-accent-color3); +} + +.theia-debug-entry:not(:focus) .theia-mod-selected { + background: var(--theia-accent-color4); +} + +.theia-debug-threads { + overflow: auto; + height: 180px; +} + +.theia-debug-thread-item { + line-height: var(--theia-private-horizontal-tab-height); + align-items: baseline; + padding-left: 5px; +} + +.theia-debug-thread-item:hover { + background: var(--theia-accent-color4); + cursor: pointer; +} + +.theia-debug-frames { + overflow: auto; + height: 200px; +} + +.theia-debug-frame-item { + line-height: var(--theia-private-horizontal-tab-height); + align-items: baseline; + padding-left: 5px; +} + +.theia-debug-frame-item:hover { + background: var(--theia-accent-color4); + cursor: pointer; +} + +.theia-debug-breakpoints { + overflow: auto; + height: 150px; +} + +.theia-debug-breakpoint-item { + line-height: var(--theia-private-horizontal-tab-height); + align-items: baseline; + padding-left: 5px; +} + +.theia-debug-breakpoint-item:hover { + background: var(--theia-accent-color4); + cursor: pointer; +} + +.theia-debug-breakpoint-dialog { + height: 400px; +} + +.theia-debug-breakpoint-dialog .theia-debug-breakpoints { + background: var(--theia-layout-color0); + height: 380px +} + +.theia-debug-variables { + overflow: hidden; + height: 200px; + white-space: nowrap; +} + +.theia-debug-active-line { + background-color: rgba(255, 120, 100, 0.5); +} + +.theia-debug-header { + background: var(--theia-layout-color3); + text-transform: uppercase; + font-size: var(--theia-ui-font-size0); + font-weight: 800; + border: 4px solid; + border-color: var(--theia-layout-color3); +} + +.theia-debug-inactive-breakpoint { + transform: scale(0.5); + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgdmVyc2lvbj0iMS4xIiAgIGlkPSJicmVha3BvaW50X3g1Rl9pbmFjdGl2ZSIgICB4PSIwcHgiICAgeT0iMHB4IiAgIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiAgIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAyNTYgMjU2IDI1NjsiICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjkxIHIxMzcyNSIgICBzb2RpcG9kaTpkb2NuYW1lPSJicmVha3BvaW50X2luYWN0aXZlLnN2ZyI+PG1ldGFkYXRhICAgICBpZD0ibWV0YWRhdGEzMzk3Ij48cmRmOlJERj48Y2M6V29yayAgICAgICAgIHJkZjphYm91dD0iIj48ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD48ZGM6dHlwZSAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48ZGM6dGl0bGU+PC9kYzp0aXRsZT48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMgICAgIGlkPSJkZWZzMzM5NSIgLz48c29kaXBvZGk6bmFtZWR2aWV3ICAgICBwYWdlY29sb3I9IiNmZmZmZmYiICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIgICAgIGJvcmRlcm9wYWNpdHk9IjEiICAgICBvYmplY3R0b2xlcmFuY2U9IjEwIiAgICAgZ3JpZHRvbGVyYW5jZT0iMTAiICAgICBndWlkZXRvbGVyYW5jZT0iMTAiICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMCIgICAgIGlua3NjYXBlOnBhZ2VzaGFkb3c9IjIiICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjE5MjAiICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDI3IiAgICAgaWQ9Im5hbWVkdmlldzMzOTMiICAgICBzaG93Z3JpZD0idHJ1ZSIgICAgIGlua3NjYXBlOnpvb209IjMuMjkyOTY4OCIgICAgIGlua3NjYXBlOmN4PSIxMjgiICAgICBpbmtzY2FwZTpjeT0iMTQwLjE0NzA5IiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjAiICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIgICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjEiICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJicmVha3BvaW50X3g1Rl9pbmFjdGl2ZSI+PGlua3NjYXBlOmdyaWQgICAgICAgdHlwZT0ieHlncmlkIiAgICAgICBpZD0iZ3JpZDMzOTkiIC8+PC9zb2RpcG9kaTpuYW1lZHZpZXc+PHBhdGggICAgIGlkPSJkb3R0ZWRfeDVGX3JpbmciICAgICBkPSJNODQuNSw0Mi4zbC05LDUuMmMtNS4yLDMtMTEuOCwxLjItMTQuOC00bC01LjItOWMtMy01LjItMS4yLTExLjgsNC0xNC44bDktNS4yYzUuMi0zLDExLjgtMS4yLDE0LjgsNCAgbDUuMiw5QzkxLjQsMzIuNiw4OS42LDM5LjMsODQuNSw0Mi4zeiBNMTk2LjUsMjM2LjNsLTksNS4yYy01LjIsMy0xMS44LDEuMi0xNC44LTRsLTUuMi05Yy0zLTUuMi0xLjItMTEuOCw0LTE0LjhsOS01LjIgIGM1LjItMywxMS44LTEuMiwxNC44LDRsNS4yLDlDMjAzLjQsMjI2LjYsMjAxLjYsMjMzLjMsMTk2LjUsMjM2LjN6IE00Ny41LDc1LjVsLTUuMiw5Yy0zLDUuMi05LjYsNi45LTE0LjgsNGwtOS01LjIgIGMtNS4yLTMtNi45LTkuNi00LTE0LjhsNS4yLTljMy01LjIsOS42LTYuOSwxNC44LTRsOSw1LjJDNDguNiw2My43LDUwLjQsNzAuNCw0Ny41LDc1LjV6IE0yNDEuNCwxODcuNWwtNS4yLDkgIGMtMyw1LjItOS42LDYuOS0xNC44LDRsLTktNS4yYy01LjItMy02LjktOS42LTQtMTQuOGw1LjItOWMzLTUuMiw5LjYtNi45LDE0LjgtNGw5LDUuMkMyNDIuNiwxNzUuNywyNDQuNCwxODIuNCwyNDEuNCwxODcuNXogICBNMzIsMTIyLjh2MTAuNGMwLDYtNC45LDEwLjgtMTAuOCwxMC44SDEwLjhjLTYsMC0xMC44LTQuOS0xMC44LTEwLjh2LTEwLjRjMC02LDQuOS0xMC44LDEwLjgtMTAuOGgxMC40ICBDMjcuMSwxMTIsMzIsMTE2LjksMzIsMTIyLjh6IE0yNTYsMTIyLjh2MTAuNGMwLDYtNC45LDEwLjgtMTAuOCwxMC44aC0xMC40Yy02LDAtMTAuOC00LjktMTAuOC0xMC44di0xMC40YzAtNiw0LjktMTAuOCwxMC44LTEwLjggIGgxMC40QzI1MS4xLDExMiwyNTYsMTE2LjksMjU2LDEyMi44eiBNNDIuMywxNzEuNWw1LjIsOWMzLDUuMiwxLjIsMTEuOC00LDE0LjhsLTksNS4yYy01LjIsMy0xMS44LDEuMi0xNC44LTRsLTUuMi05ICBjLTMtNS4yLTEuMi0xMS44LDQtMTQuOGw5LTUuMkMzMi42LDE2NC42LDM5LjMsMTY2LjQsNDIuMywxNzEuNXogTTIzNi4zLDU5LjVsNS4yLDljMyw1LjIsMS4yLDExLjgtNCwxNC44bC05LDUuMiAgYy01LjIsMy0xMS44LDEuMi0xNC44LTRsLTUuMi05Yy0zLTUuMi0xLjItMTEuOCw0LTE0LjhsOS01LjJDMjI2LjYsNTIuNiwyMzMuMyw1NC40LDIzNi4zLDU5LjV6IE03NS41LDIwOC41bDksNS4yICBjNS4yLDMsNi45LDkuNiw0LDE0LjhsLTUuMiw5Yy0zLDUuMi05LjYsNi45LTE0LjgsNGwtOS01LjJjLTUuMi0zLTYuOS05LjYtNC0xNC44bDUuMi05QzYzLjcsMjA3LjQsNzAuNCwyMDUuNiw3NS41LDIwOC41eiAgIE0xODcuNSwxNC42bDksNS4yYzUuMiwzLDYuOSw5LjYsNCwxNC44bC01LjIsOWMtMyw1LjItOS42LDYuOS0xNC44LDRsLTktNS4yYy01LjItMy02LjktOS42LTQtMTQuOGw1LjItOSAgQzE3NS43LDEzLjQsMTgyLjQsMTEuNiwxODcuNSwxNC42eiBNMTIyLjgsMjI0aDEwLjRjNiwwLDEwLjgsNC45LDEwLjgsMTAuOHYxMC40YzAsNi00LjksMTAuOC0xMC44LDEwLjhoLTEwLjQgIGMtNiwwLTEwLjgtNC45LTEwLjgtMTAuOHYtMTAuNEMxMTIsMjI4LjksMTE2LjksMjI0LDEyMi44LDIyNHogTTEyMi44LDBoMTAuNGM2LDAsMTAuOCw0LjksMTAuOCwxMC44djEwLjRjMCw2LTQuOSwxMC44LTEwLjgsMTAuOCAgaC0xMC40Yy02LDAtMTAuOC00LjktMTAuOC0xMC44VjEwLjhDMTEyLDQuOSwxMTYuOSwwLDEyMi44LDB6IiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIgLz48L3N2Zz4=); +} + +.theia-debug-active-breakpoint { + transform: scale(0.5); + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgdmVyc2lvbj0iMS4xIiAgIGlkPSJicmVha3BvaW50X3g1Rl9lbmFibGVkIiAgIHg9IjBweCIgICB5PSIwcHgiICAgdmlld0JveD0iMCAwIDI1NiAyNTYiICAgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMjU2IDI1NjsiICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjkxIHIxMzcyNSIgICBzb2RpcG9kaTpkb2NuYW1lPSJicmVha3BvaW50X2VuYWJsZWQuc3ZnIj48bWV0YWRhdGEgICAgIGlkPSJtZXRhZGF0YTMzODIiPjxyZGY6UkRGPjxjYzpXb3JrICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcyAgICAgaWQ9ImRlZnMzMzgwIiAvPjxzb2RpcG9kaTpuYW1lZHZpZXcgICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIgICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IiAgICAgYm9yZGVyb3BhY2l0eT0iMSIgICAgIG9iamVjdHRvbGVyYW5jZT0iMTAiICAgICBncmlkdG9sZXJhbmNlPSIxMCIgICAgIGd1aWRldG9sZXJhbmNlPSIxMCIgICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIgICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIgICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjEwMjciICAgICBpZD0ibmFtZWR2aWV3MzM3OCIgICAgIHNob3dncmlkPSJmYWxzZSIgICAgIGlua3NjYXBlOnpvb209IjEuNjQ2NDg0NCIgICAgIGlua3NjYXBlOmN4PSI2OS4zOTMzMTgiICAgICBpbmtzY2FwZTpjeT0iMTI1LjQyODY5IiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjAiICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIgICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjEiICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJicmVha3BvaW50X3g1Rl9lbmFibGVkIiAvPjxzdHlsZSAgICAgdHlwZT0idGV4dC9jc3MiICAgICBpZD0ic3R5bGUzMzc1Ij4uc3Qwe2ZpbGw6IzIzMUYyMDt9PC9zdHlsZT48Y2lyY2xlICAgICBpZD0iY2lyY2xlIiAgICAgY2xhc3M9InN0MCIgICAgIGN4PSIxMjgiICAgICBjeT0iMTI4IiAgICAgcj0iMTI4IiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIgLz48L3N2Zz4=); +} diff --git a/packages/debug/src/browser/view/debug-breakpoints-widget.ts b/packages/debug/src/browser/view/debug-breakpoints-widget.ts new file mode 100644 index 0000000000000..aed583ecb68ea --- /dev/null +++ b/packages/debug/src/browser/view/debug-breakpoints-widget.ts @@ -0,0 +1,177 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { + VirtualWidget, SELECTED_CLASS, AbstractDialog, Widget, Message, +} from '@theia/core/lib/browser'; +import { DebugSession } from '../debug-model'; +import { h } from '@phosphor/virtualdom'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { Emitter, Event } from '@theia/core'; +import { injectable, inject, postConstruct } from 'inversify'; +import { BreakpointsManager } from '../breakpoint/breakpoint-manager'; +import { ExtDebugProtocol } from '../../common/debug-common'; +import { DebugUtils } from '../debug-utils'; +import { Disposable } from '@theia/core'; + +/** + * Is it used to display breakpoints. + */ +@injectable() +export class DebugBreakpointsWidget extends VirtualWidget { + private readonly onDidClickBreakpointEmitter = new Emitter(); + private readonly onDidDblClickBreakpointEmitter = new Emitter(); + private breakpoints: ExtDebugProtocol.AggregatedBreakpoint[] = []; + + constructor( + @inject(BreakpointsManager) protected readonly breakpointManager: BreakpointsManager, + @inject(DebugSession) protected readonly debugSession: DebugSession | undefined) { + super(); + + this.id = 'debug-breakpoints' + (debugSession ? `-${debugSession.sessionId}` : ''); + this.addClass('theia-debug-entry'); + this.node.setAttribute('tabIndex', '0'); + + } + + @postConstruct() + protected init() { + this.toDisposeOnDetach.push(this.breakpointManager.onDidChangeBreakpoints(() => this.refreshBreakpoints())); + if (this.debugSession) { + const configurationDoneListener = () => this.refreshBreakpoints(); + + this.debugSession.on('configurationDone', configurationDoneListener); + this.toDisposeOnDetach.push(Disposable.create(() => this.debugSession!.removeListener('configurationDone', configurationDoneListener))); + } + } + + get onDidClickBreakpoint(): Event { + return this.onDidClickBreakpointEmitter.event; + } + + get onDidDblClickBreakpoint(): Event { + return this.onDidDblClickBreakpointEmitter.event; + } + + async refreshBreakpoints(): Promise { + if (this.debugSession) { + this.breakpoints = await this.breakpointManager.get(this.debugSession.sessionId); + } else { + this.breakpoints = await this.breakpointManager.getAll(); + } + this.breakpoints.sort(); + + super.update(); + } + + protected render(): h.Child { + const header = h.div({ className: 'theia-debug-header' }, 'Breakpoints'); + const items: h.Child = []; + + for (const breakpoint of this.breakpoints) { + const item = + h.div({ + id: DebugUtils.makeBreakpointId(breakpoint), + className: Styles.BREAKPOINT_ITEM, + onclick: event => { + const selected = this.node.getElementsByClassName(SELECTED_CLASS)[0]; + if (selected) { + selected.className = Styles.BREAKPOINT_ITEM; + } + (event.target as HTMLDivElement).className = `${Styles.BREAKPOINT_ITEM} ${SELECTED_CLASS}`; + + this.onDidClickBreakpointEmitter.fire(breakpoint); + }, + ondblclick: () => this.onDidDblClickBreakpointEmitter.fire(breakpoint), + }, this.toDisplayName(breakpoint)); + items.push(item); + } + + return [header, h.div({ className: Styles.BREAKPOINTS }, items)]; + } + + private toDisplayName(breakpoint: ExtDebugProtocol.AggregatedBreakpoint): string { + if ('origin' in breakpoint) { + if (DebugUtils.isSourceBreakpoint(breakpoint)) { + return this.toDisplayNameSourceBrk(breakpoint.source!, breakpoint.origin as DebugProtocol.SourceBreakpoint); + + } else if (DebugUtils.isFunctionBreakpoint(breakpoint)) { + return (breakpoint.origin as DebugProtocol.FunctionBreakpoint).name; + + } else if (DebugUtils.isExceptionBreakpoint(breakpoint)) { + return (breakpoint.origin as ExtDebugProtocol.ExceptionBreakpoint).filter; + } + } + + throw new Error('Unrecognized breakpoint type: ' + JSON.stringify(breakpoint)); + } + + private toDisplayNameSourceBrk(source: DebugProtocol.Source, breakpoint: DebugProtocol.SourceBreakpoint): string { + return source.name! + `:${breakpoint.line}` + (breakpoint.column ? `:${breakpoint.column}` : ''); + } +} + +@injectable() +export class BreakpointsDialog extends AbstractDialog { + private readonly breakpointsWidget: DebugBreakpointsWidget; + + constructor(@inject(BreakpointsManager) protected readonly breakpointManager: BreakpointsManager) { + super({ + title: 'Breakpoints' + }); + + this.breakpointsWidget = new DebugBreakpointsWidget(breakpointManager, undefined); + this.breakpointsWidget.addClass(Styles.BREAKPOINT_DIALOG); + this.toDispose.push(this.breakpointsWidget); + } + + @postConstruct() + protected init() { + this.appendCloseButton('Close'); + } + + protected onAfterAttach(msg: Message): void { + Widget.attach(this.breakpointsWidget, this.contentNode); + super.onAfterAttach(msg); + } + + protected onBeforeDetach(msg: Message): void { + super.onBeforeDetach(msg); + Widget.detach(this.breakpointsWidget); + } + + protected onUpdateRequest(msg: Message): void { + super.onUpdateRequest(msg); + this.breakpointsWidget.update(); + } + + protected onActivateRequest(msg: Message): void { + this.breakpointsWidget.activate(); + } + + open(): Promise { + this.breakpointsWidget.refreshBreakpoints(); + return super.open(); + } + + get value(): void { return undefined; } +} + +namespace Styles { + export const BREAKPOINTS = 'theia-debug-breakpoints'; + export const BREAKPOINT_ITEM = 'theia-debug-breakpoint-item'; + export const BREAKPOINT_DIALOG = 'theia-debug-breakpoint-dialog'; +} diff --git a/packages/debug/src/browser/view/debug-selection-service.ts b/packages/debug/src/browser/view/debug-selection-service.ts new file mode 100644 index 0000000000000..cf5878a873e2d --- /dev/null +++ b/packages/debug/src/browser/view/debug-selection-service.ts @@ -0,0 +1,97 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { injectable, inject, postConstruct } from 'inversify'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { DebugSessionManager } from '../debug-session'; +import { Emitter, Event } from '@theia/core'; +import { ExtDebugProtocol } from '../../common/debug-common'; + +/** + * Contains debug panel selections. + */ +@injectable() +export class DebugSelection { + private _thread: DebugProtocol.Thread | undefined; + private _frame: DebugProtocol.StackFrame | undefined; + private _variable: ExtDebugProtocol.Variable | undefined; + + private readonly onDidSelectThreadEmitter = new Emitter(); + private readonly onDidSelectFrameEmitter = new Emitter(); + private readonly onDidSelectVariableEmitter = new Emitter(); + + get thread(): DebugProtocol.Thread | undefined { + return this._thread; + } + + set thread(thread: DebugProtocol.Thread | undefined) { + this._thread = thread; + this.onDidSelectThreadEmitter.fire(thread); + } + + get frame(): DebugProtocol.StackFrame | undefined { + return this._frame; + } + + set frame(frame: DebugProtocol.StackFrame | undefined) { + this._frame = frame; + this.onDidSelectFrameEmitter.fire(frame); + } + + get variable(): ExtDebugProtocol.Variable | undefined { + return this._variable; + } + + set variable(variable: ExtDebugProtocol.Variable | undefined) { + this._variable = variable; + this.onDidSelectVariableEmitter.fire(variable); + } + + get onDidSelectThread(): Event { + return this.onDidSelectThreadEmitter.event; + } + + get onDidSelectFrame(): Event { + return this.onDidSelectFrameEmitter.event; + } + + get onDidSelectVariable(): Event { + return this.onDidSelectVariableEmitter.event; + } +} + +@injectable() +export class DebugSelectionService { + private readonly selections = new Map(); + + constructor( + @inject(DebugSessionManager) protected readonly debugSessionManager: DebugSessionManager) { } + + @postConstruct() + protected init() { + this.debugSessionManager.onDidPreCreateDebugSession(sessionId => this.selections.set(sessionId, new DebugSelection())); + this.debugSessionManager.onDidDestroyDebugSession(debugSession => this.selections.delete(debugSession.sessionId)); + } + + get(sessionId: string): DebugSelection { + const selection = this.selections.get(sessionId); + if (!selection) { + throw new Error(`Selection is not initialized for the debug session: '${sessionId}'`); + } + + return selection; + } +} diff --git a/packages/debug/src/browser/view/debug-stack-frames-widget.ts b/packages/debug/src/browser/view/debug-stack-frames-widget.ts new file mode 100644 index 0000000000000..1c4f7071cc10a --- /dev/null +++ b/packages/debug/src/browser/view/debug-stack-frames-widget.ts @@ -0,0 +1,168 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { + VirtualWidget, + SELECTED_CLASS, +} from '@theia/core/lib/browser'; +import { DebugSession } from '../debug-model'; +import { h } from '@phosphor/virtualdom'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { injectable, inject, postConstruct } from 'inversify'; +import { DebugSelection } from './debug-selection-service'; +import { SourceOpener, DebugUtils } from '../debug-utils'; +import { Disposable } from '@theia/core'; + +/** + * Is it used to display call stack. + */ +@injectable() +export class DebugStackFramesWidget extends VirtualWidget { + private _frames: DebugProtocol.StackFrame[] = []; + + constructor( + @inject(DebugSession) protected readonly debugSession: DebugSession, + @inject(DebugSelection) protected readonly debugSelection: DebugSelection, + @inject(SourceOpener) protected readonly sourceOpener: SourceOpener) { + super(); + + this.id = this.createId(); + this.addClass('theia-debug-entry'); + this.node.setAttribute('tabIndex', '0'); + } + + @postConstruct() + protected init() { + this.toDisposeOnDetach.push(this.debugSelection.onDidSelectThread(thread => this.onThreadSelected(thread))); + + const stoppedEventListener = (event: DebugProtocol.StoppedEvent) => this.onStoppedEvent(event); + const continuedEventListener = (event: DebugProtocol.ContinuedEvent) => this.onContinuedEvent(event); + + this.debugSession.on('stopped', stoppedEventListener); + this.debugSession.on('continued', continuedEventListener); + + this.toDisposeOnDetach.push(Disposable.create(() => this.debugSession.removeListener('stopped', stoppedEventListener))); + this.toDisposeOnDetach.push(Disposable.create(() => this.debugSession.removeListener('continued', continuedEventListener))); + } + + get frames(): DebugProtocol.StackFrame[] { + return this._frames; + } + + set frames(frames: DebugProtocol.StackFrame[]) { + this._frames = frames; + this.update(); + } + + protected render(): h.Child { + const header = h.div({ className: 'theia-debug-header' }, 'Call stack'); + const items: h.Child = []; + + const selectedFrame = this.debugSelection.frame; + for (const frame of this._frames) { + const className = Styles.FRAME_ITEM + (DebugUtils.isEqual(selectedFrame, frame) ? ` ${SELECTED_CLASS}` : ''); + const id = this.createId(frame); + + const item = + h.div({ + id, className, + onclick: () => { + this.selectFrame(frame); + this.sourceOpener.open(frame); + } + }, this.toDisplayName(frame)); + + items.push(item); + } + + return [header, h.div({ className: Styles.FRAMES }, items)]; + } + + protected onThreadSelected(thread: DebugProtocol.Thread | undefined) { + this.updateFrames(thread ? thread.id : undefined); + } + + protected selectFrame(newFrame: DebugProtocol.StackFrame | undefined) { + const currentFrame = this.debugSelection.frame; + + if (DebugUtils.isEqual(currentFrame, newFrame)) { + return; + } + + if (currentFrame) { + const element = document.getElementById(this.createId(currentFrame)); + if (element) { + element.className = Styles.FRAME_ITEM; + } + } + + if (newFrame) { + const element = document.getElementById(this.createId(newFrame)); + if (element) { + element.className = `${Styles.FRAME_ITEM} ${SELECTED_CLASS}`; + } + } + + this.debugSelection.frame = newFrame; + } + + private toDisplayName(frame: DebugProtocol.StackFrame): string { + return frame.name; + } + + private createId(frame?: DebugProtocol.StackFrame): string { + return `debug-stack-frames-${this.debugSession.sessionId}` + (frame ? `-${frame.id}` : ''); + } + + private onContinuedEvent(event: DebugProtocol.ContinuedEvent): void { + const currentThread = this.debugSelection.thread; + if (currentThread) { + if (DebugUtils.isEqual(currentThread, event.body.threadId) || event.body.allThreadsContinued) { + this.selectFrame(undefined); + this.frames = []; + } + } + } + + private onStoppedEvent(event: DebugProtocol.StoppedEvent): void { + const currentThread = this.debugSelection.thread; + if (currentThread) { + if (DebugUtils.isEqual(currentThread, event.body.threadId) || event.body.allThreadsStopped) { + this.updateFrames(currentThread.id); + } + } + } + + private updateFrames(threadId: number | undefined) { + this.selectFrame(undefined); + this.frames = []; + + if (threadId) { + const args: DebugProtocol.StackTraceArguments = { threadId }; + this.debugSession.stacks(args).then(response => { + if (DebugUtils.isEqual(this.debugSelection.thread, threadId)) { // still the same thread remains selected + this.selectFrame(response.body.stackFrames[0]); + this.frames = response.body.stackFrames; + } + }); + } + } +} + +namespace Styles { + export const FRAMES = 'theia-debug-frames'; + export const FRAME_ITEM = 'theia-debug-frame-item'; +} diff --git a/packages/debug/src/browser/view/debug-threads-widget.ts b/packages/debug/src/browser/view/debug-threads-widget.ts new file mode 100644 index 0000000000000..624c6b0db3fec --- /dev/null +++ b/packages/debug/src/browser/view/debug-threads-widget.ts @@ -0,0 +1,145 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { VirtualWidget, SELECTED_CLASS, ContextMenuRenderer } from '@theia/core/lib/browser'; +import { DebugSession } from '../debug-model'; +import { h } from '@phosphor/virtualdom'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { injectable, inject, postConstruct } from 'inversify'; +import { DEBUG_SESSION_THREAD_CONTEXT_MENU } from '../debug-command'; +import { DebugSelection } from './debug-selection-service'; +import { DebugUtils } from '../debug-utils'; +import { Disposable } from '@theia/core'; + +/** + * Is it used to display list of threads. + */ +@injectable() +export class DebugThreadsWidget extends VirtualWidget { + private _threads: DebugProtocol.Thread[] = []; + + constructor( + @inject(DebugSession) protected readonly debugSession: DebugSession, + @inject(DebugSelection) protected readonly debugSelection: DebugSelection, + @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer) { + super(); + + this.id = this.createId(); + this.addClass('theia-debug-entry'); + this.node.setAttribute('tabIndex', '0'); + } + + @postConstruct() + protected init() { + const threadEventListener = (event: DebugProtocol.ThreadEvent) => this.onThreadEvent(event); + const connectedEventListener = () => this.updateThreads(); + + this.debugSession.on('thread', threadEventListener); + this.debugSession.on('connected', connectedEventListener); + + this.toDisposeOnDetach.push(Disposable.create(() => this.debugSession.removeListener('thread', threadEventListener))); + this.toDisposeOnDetach.push(Disposable.create(() => this.debugSession.removeListener('connected', connectedEventListener))); + + if (this.debugSession.state.isConnected) { + this.updateThreads(); + } + } + + get threads(): DebugProtocol.Thread[] { + return this._threads; + } + + set threads(threads: DebugProtocol.Thread[]) { + this._threads = threads; + this.update(); + } + + protected render(): h.Child { + const header = h.div({ className: 'theia-debug-header' }, 'Threads'); + const items: h.Child = []; + + for (const thread of this.threads) { + const className = Styles.THREAD_ITEM + (DebugUtils.isEqual(this.debugSelection.thread, thread) ? ` ${SELECTED_CLASS}` : ''); + const id = this.createId(thread); + + const item = + h.div({ + id, className, + onclick: () => this.selectThread(thread), + oncontextmenu: event => { + event.preventDefault(); + event.stopPropagation(); + this.selectThread(thread); + this.contextMenuRenderer.render(DEBUG_SESSION_THREAD_CONTEXT_MENU, event); + } + }, thread.name); + items.push(item); + } + + return [header, h.div({ className: Styles.THREADS }, items)]; + } + + protected selectThread(newThread: DebugProtocol.Thread | undefined) { + const currentThread = this.debugSelection.thread; + + if (DebugUtils.isEqual(currentThread, newThread)) { + return; + } + + if (currentThread) { + const element = document.getElementById(this.createId(currentThread)); + if (element) { + element.className = Styles.THREAD_ITEM; + } + } + + if (newThread) { + const element = document.getElementById(this.createId(newThread)); + if (element) { + element.className = `${Styles.THREAD_ITEM} ${SELECTED_CLASS}`; + } + } + + this.debugSelection.thread = newThread; + } + + private createId(thread?: DebugProtocol.Thread): string { + return `debug-threads-${this.debugSession.sessionId}` + (thread ? `-${thread.id}` : ''); + } + + private onThreadEvent(event: DebugProtocol.ThreadEvent): void { + this.updateThreads(); + } + + private updateThreads(): void { + const currentThread = this.debugSelection.thread; + + this.threads = []; + this.selectThread(undefined); + + this.debugSession.threads().then(response => { + this.threads = response.body.threads; + + const currentThreadExists = this.threads.some(thread => DebugUtils.isEqual(thread, currentThread)); + this.selectThread(currentThreadExists ? currentThread : this.threads[0]); + }); + } +} + +namespace Styles { + export const THREADS = 'theia-debug-threads'; + export const THREAD_ITEM = 'theia-debug-thread-item'; +} diff --git a/packages/debug/src/browser/view/debug-variables-widget.tsx b/packages/debug/src/browser/view/debug-variables-widget.tsx new file mode 100644 index 0000000000000..7029bb4e9a828 --- /dev/null +++ b/packages/debug/src/browser/view/debug-variables-widget.tsx @@ -0,0 +1,261 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { + TreeWidget, + ContextMenuRenderer, + TreeModel, + TreeNode, + NodeProps, + TreeProps, + SelectableTreeNode, + ExpandableTreeNode, + CompositeTreeNode, + TreeModelImpl, + TreeImpl, +} from '@theia/core/lib/browser'; +import { injectable, inject, postConstruct } from 'inversify'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { MenuModelRegistry } from '@theia/core/lib/common/menu'; +import { CommandRegistry } from '@theia/core'; +import { DebugSession } from '../debug-model'; +import { DebugSelection } from '../view/debug-selection-service'; +import { ExtDebugProtocol } from '../../common/debug-common'; +import * as React from 'react'; +import { Disposable } from '@theia/core'; + +/** + * Is it used to display variables. + */ +@injectable() +export class DebugVariablesWidget extends TreeWidget { + constructor( + @inject(DebugSession) protected readonly debugSession: DebugSession, + @inject(DebugSelection) protected readonly debugSelection: DebugSelection, + @inject(TreeModel) readonly model: TreeModel, + @inject(TreeProps) readonly treeProps: TreeProps, + @inject(ContextMenuRenderer) readonly contextMenuRenderer: ContextMenuRenderer, + @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry, + @inject(MenuModelRegistry) protected readonly menuModelRegistry: MenuModelRegistry) { + super(treeProps, model, contextMenuRenderer); + + this.id = `debug-variables-${debugSession.sessionId}`; + this.title.label = 'Variables'; + this.addClass('theia-debug-entry'); + } + + @postConstruct() + protected init() { + super.init(); + + this.toDisposeOnDetach.push(Disposable.create(() => this.debugSession.removeListener('variableUpdated', variableUpdateListener))); + + const variableUpdateListener = (event: ExtDebugProtocol.VariableUpdatedEvent) => this.onVariableUpdated(event); + this.debugSession.on('variableUpdated', variableUpdateListener); + this.toDisposeOnDetach.push(this.debugSelection.onDidSelectFrame(frame => this.onFrameSelected(frame))); + } + + protected onFrameSelected(frame: DebugProtocol.StackFrame | undefined) { + if (frame) { + this.debugSelection.variable = undefined; + this.model.root = FrameNode.create(this.debugSession.sessionId, frame.id); + } + } + + protected onVariableUpdated(event: ExtDebugProtocol.VariableUpdatedEvent) { + const id = VariableNode.getId(this.debugSession.sessionId, event.body.name, event.body.parentVariablesReference); + const variableNode = this.model.getNode(id) as VariableNode; + Object.assign(variableNode.extVariable, event.body); + this.model.refresh(variableNode); + } + + protected renderTree(model: TreeModel): React.ReactNode { + return
Variables
{super.renderTree(model)}
; + } + + protected renderCaption(node: TreeNode, props: NodeProps): React.ReactNode { + if (VariableNode.is(node)) { + return this.decorateVariableCaption(node); + } else if (ScopeNode.is(node)) { + return this.decorateScopeCaption(node); + } + return super.renderCaption(node, props); + } + + protected decorateVariableCaption(node: VariableNode): React.ReactNode { + return
{node.extVariable.name} = {node.extVariable.value}
; + } + + protected decorateScopeCaption(node: ScopeNode): React.ReactNode { + return
{node.scope.name}
; + } +} + +@injectable() +export class DebugVariableModel extends TreeModelImpl { + constructor(@inject(DebugSelection) protected readonly debugSelection: DebugSelection) { + super(); + } + + @postConstruct() + protected init() { + super.init(); + + this.selectionService.onSelectionChanged((nodes: SelectableTreeNode[]) => { + const node = nodes[0]; + if (VariableNode.is(node)) { + this.debugSelection.variable = node.extVariable; + } else { + this.debugSelection.variable = undefined; + } + }); + } +} + +@injectable() +export class DebugVariablesTree extends TreeImpl { + constructor(@inject(DebugSession) protected readonly debugSession: DebugSession) { + super(); + } + + protected resolveChildren(parent: CompositeTreeNode): Promise { + if (FrameNode.is(parent)) { + const frameId = parent.frameId; + if (frameId) { + return this.debugSession.scopes({ frameId }).then(response => { + const scopes = response.body.scopes; + return scopes.map(scope => ScopeNode.create(this.debugSession.sessionId, scope, parent)); + }); + } else { + return Promise.resolve([]); + } + } + + if (ScopeNode.is(parent)) { + const parentVariablesReference = parent.scope.variablesReference; + const args: DebugProtocol.VariablesArguments = { variablesReference: parentVariablesReference }; + return this.debugSession.variables(args).then(response => { + const variables = response.body.variables; + return variables.map(variable => { + const extVariable = { ...variable, parentVariablesReference }; + return VariableNode.create(this.debugSession.sessionId, extVariable, parent); + }); + }); + } + + if (VariableNode.is(parent)) { + const parentVariablesReference = parent.extVariable.variablesReference; + if (parentVariablesReference > 0) { + const args: DebugProtocol.VariablesArguments = { variablesReference: parentVariablesReference }; + return this.debugSession.variables(args).then(response => { + const variables = response.body.variables; + return variables.map(variable => { + const extVariable = { ...variable, parentVariablesReference }; + return VariableNode.create(this.debugSession.sessionId, extVariable, parent); + }); + }); + } else { + return Promise.resolve([]); + } + } + + return super.resolveChildren(parent); + + } +} + +export interface VariableNode extends SelectableTreeNode, ExpandableTreeNode, CompositeTreeNode { + extVariable: ExtDebugProtocol.Variable; +} + +export interface ScopeNode extends SelectableTreeNode, ExpandableTreeNode, CompositeTreeNode { + scope: DebugProtocol.Scope; +} + +export interface FrameNode extends SelectableTreeNode, ExpandableTreeNode, CompositeTreeNode { + frameId: number | undefined; +} + +namespace VariableNode { + export function is(node: TreeNode | undefined): node is VariableNode { + return !!node && 'extVariable' in node; + } + + export function create(sessionId: string, extVariable: ExtDebugProtocol.Variable, parent: CompositeTreeNode | undefined): VariableNode { + const name = extVariable.name; + const id = createId(sessionId, extVariable.name, extVariable.parentVariablesReference); + return { + id, extVariable, name, parent, + visible: true, + expanded: false, + selected: false, + children: [] + }; + } + + export function getId(sessionId: string, name: string, parentVariablesReference: number): string { + return createId(sessionId, name, parentVariablesReference); + } +} + +namespace ScopeNode { + export function is(node: TreeNode | undefined): node is ScopeNode { + return !!node && 'scope' in node; + } + + export function create(sessionId: string, scope: DebugProtocol.Scope, parent: CompositeTreeNode | undefined): ScopeNode { + const name = scope.name; + const id = getId(sessionId, scope.name); + return { + id, name, parent, scope, + visible: true, + expanded: false, + selected: false, + children: [] + }; + } + + export function getId(sessionId: string, name: string): string { + return createId(sessionId, name); + } +} + +namespace FrameNode { + export function is(node: TreeNode | undefined): node is FrameNode { + return !!node && 'frameId' in node; + } + + export function create(sessionId: string, frameId: number): FrameNode { + const id = createId(sessionId, `frame-${frameId}`); + return { + id, frameId, + parent: undefined, + name: 'Debug variable', + visible: false, + expanded: true, + selected: false, + children: [] + }; + } + + export function getId(sessionId: string, frameId: number): string { + return createId(sessionId, `frame-${frameId}`); + } +} + +function createId(sessionId: string, itemId: string | number, parentId?: string | number): string { + return `debug-variables-${sessionId}` + (parentId && `-${parentId}`) + `-${itemId}`; +} diff --git a/packages/debug/src/browser/view/debug-view-contribution.ts b/packages/debug/src/browser/view/debug-view-contribution.ts new file mode 100644 index 0000000000000..950ffa34395b6 --- /dev/null +++ b/packages/debug/src/browser/view/debug-view-contribution.ts @@ -0,0 +1,236 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { + AbstractViewContribution, + TabBar, + Panel, + TabBarRenderer, + TabBarRendererFactory, + SideTabBar, + Widget, + Message, + VirtualWidget, +} from '@theia/core/lib/browser'; +import { DebugSessionManager } from '../debug-session'; +import { DebugSession } from '../debug-model'; +import { DEBUG_SESSION_CONTEXT_MENU } from '../debug-command'; +import { inject, injectable, postConstruct } from 'inversify'; +import { DebugThreadsWidget } from './debug-threads-widget'; +import { DebugStackFramesWidget } from './debug-stack-frames-widget'; +import { DebugBreakpointsWidget } from './debug-breakpoints-widget'; +import { DebugVariablesWidget } from './debug-variables-widget'; + +export const DEBUG_FACTORY_ID = 'debug'; + +/** + * The panel which contains all debug target widgets. + */ +@injectable() +export class DebugWidget extends Panel { + private readonly tabBar: SideTabBar; + + constructor( + @inject(DebugSessionManager) + protected readonly debugSessionManager: DebugSessionManager, + @inject(TabBarRendererFactory) + protected readonly tabBarRendererFactory: () => TabBarRenderer, + @inject('Factory') + protected readonly debugTargetWidgetFactory: (debugSession: DebugSession) => DebugTargetWidget) { + + super(); + this.id = DEBUG_FACTORY_ID; + this.title.label = 'Debug'; + this.title.closable = true; + this.title.iconClass = 'fa fa-bug'; + this.tabBar = this.createTabBar(); + this.addClass(Styles.DEBUG_PANEL); + } + + @postConstruct() + protected init() { + this.debugSessionManager.onDidCreateDebugSession(debugSession => this.onDebugSessionCreated(debugSession)); + this.debugSessionManager.onDidDestroyDebugSession(debugSession => this.onDebugSessionDestroyed(debugSession)); + + this.debugSessionManager.findAll().forEach(debugSession => { + this.onDebugSessionCreated(debugSession); + this.tabBar.titles + .filter(title => (title.owner as DebugTargetWidget).sessionId === debugSession.sessionId) + .forEach(title => title.owner.update()); + }); + } + + protected onAfterAttach(msg: Message): void { + Widget.attach(this.tabBar, this.node); + this.tabBar.titles.forEach(title => Widget.attach(title.owner, this.node)); + super.onAfterAttach(msg); + } + + protected onBeforeDetach(msg: Message): void { + this.tabBar.titles.forEach(title => Widget.detach(title.owner)); + Widget.detach(this.tabBar); + super.onBeforeDetach(msg); + } + + protected onActivateRequest(msg: Message) { + super.onActivateRequest(msg); + const currentTitle = this.tabBar.currentTitle; + this.tabBar.update(); // to redraw to tab + if (currentTitle) { + currentTitle.owner.activate(); + } + } + + protected onUpdateRequest(msg: Message): void { + super.onUpdateRequest(msg); + this.tabBar.update(); + const currentTitle = this.tabBar.currentTitle; + if (currentTitle) { + currentTitle.owner.update(); + } + } + + protected onTabCloseRequested(sender: SideTabBar, { title }: TabBar.ITabCloseRequestedArgs): void { + const session = this.debugSessionManager.find(title.owner.sessionId); + if (session) { + session.disconnect(); + } + } + + protected onCurrentTabChanged(sender: SideTabBar, { previousTitle, currentTitle }: TabBar.ICurrentChangedArgs): void { + if (previousTitle) { + previousTitle.owner.hide(); + } + + if (currentTitle) { + currentTitle.owner.show(); + this.debugSessionManager.setActiveDebugSession(currentTitle.owner.sessionId); + } + } + + private onDebugSessionCreated(debugSession: DebugSession): void { + const widget = this.debugTargetWidgetFactory(debugSession); + if (this.isAttached) { + Widget.attach(widget, this.node); + } + this.tabBar.addTab(widget.title); + this.tabBar.currentTitle = widget.title; + + debugSession.on('connected', () => { + this.tabBar.titles + .filter(title => (title.owner as DebugTargetWidget).sessionId === debugSession.sessionId) + .forEach(title => title.owner.update()); + }); + } + + private onDebugSessionDestroyed(debugSession: DebugSession) { + this.tabBar.titles + .filter(title => (title.owner as DebugTargetWidget).sessionId === debugSession.sessionId) + .forEach(title => { + Widget.detach(title.owner); + this.tabBar.removeTab(title); + }); + } + + private createTabBar(): SideTabBar { + const renderer = this.tabBarRendererFactory(); + const tabBar = new SideTabBar({ + orientation: 'horizontal', + insertBehavior: 'none', + removeBehavior: 'select-previous-tab', + allowDeselect: false, + tabsMovable: false, + renderer: renderer, + handlers: ['drag-thumb', 'keyboard', 'wheel', 'touch'], + useBothWheelAxes: true, + scrollYMarginOffset: 8, + suppressScrollX: true + }); + renderer.tabBar = tabBar; + renderer.contextMenuPath = DEBUG_SESSION_CONTEXT_MENU; + tabBar.addClass('theia-app-centers'); + tabBar.currentChanged.connect(this.onCurrentTabChanged, this); + tabBar.tabCloseRequested.connect(this.onTabCloseRequested, this); + return tabBar; + } +} + +/** + * The debug target widget. It is used as a container + * for the rest of widgets for the specific debug target. + */ +@injectable() +export class DebugTargetWidget extends VirtualWidget { + readonly sessionId: string; + private readonly widgets: Widget[]; + + constructor( + @inject(DebugSession) protected readonly debugSession: DebugSession, + @inject(DebugThreadsWidget) protected readonly threads: DebugThreadsWidget, + @inject(DebugStackFramesWidget) protected readonly frames: DebugStackFramesWidget, + @inject(DebugBreakpointsWidget) protected readonly breakpoints: DebugBreakpointsWidget, + @inject(DebugVariablesWidget) protected readonly variables: DebugVariablesWidget + ) { + super(); + + this.title.label = debugSession.configuration.name; + this.title.closable = true; + this.addClass(Styles.DEBUG_TARGET); + this.sessionId = debugSession.sessionId; + this.widgets = [this.variables, this.threads, this.frames, this.breakpoints]; + } + + protected onUpdateRequest(msg: Message): void { + super.onUpdateRequest(msg); + this.widgets.forEach(w => w.update()); + } + + protected onAfterAttach(msg: Message): void { + this.widgets.forEach(w => Widget.attach(w, this.node)); + super.onAfterAttach(msg); + } + + protected onBeforeDetach(msg: Message): void { + super.onBeforeDetach(msg); + this.widgets.forEach(w => Widget.detach(w)); + } + + protected onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + this.widgets.forEach(w => w.activate()); + } +} + +@injectable() +export class DebugViewContribution extends AbstractViewContribution { + constructor() { + super({ + widgetId: DEBUG_FACTORY_ID, + widgetName: 'Debug', + defaultWidgetOptions: { + area: 'left', + rank: 500 + }, + toggleCommandId: 'debug.view.toggle', + toggleKeybinding: 'ctrlcmd+alt+d' + }); + } +} + +namespace Styles { + export const DEBUG_PANEL = 'theia-debug-panel'; + export const DEBUG_TARGET = 'theia-debug-target'; +} diff --git a/packages/debug/src/common/debug-common.ts b/packages/debug/src/common/debug-common.ts new file mode 100644 index 0000000000000..4f94a28abe4de --- /dev/null +++ b/packages/debug/src/common/debug-common.ts @@ -0,0 +1,307 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Some entities copied and modified from https://github.com/Microsoft/vscode/blob/master/src/vs/vscode.d.ts +// Some entities copied and modified from https://github.com/Microsoft/vscode/blob/master/src/vs/workbench/parts/debug/common/debug.ts + +import { Disposable } from '@theia/core'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +/** + * The WS endpoint path to the Debug service. + */ +export const DebugPath = '/services/debug'; + +/** + * DebugService symbol for DI. + */ +export const DebugService = Symbol('DebugService'); + +/** + * This service provides functionality to configure and to start a new debug adapter session. + * The workflow is the following. If user wants to debug an application and + * there is no debug configuration associated with the application then + * the list of available providers is requested to create suitable debug configuration. + * When configuration is chosen it is possible to alter the configuration + * by filling in missing values or by adding/changing/removing attributes. For this purpose the + * #resolveDebugConfiguration method is invoked. After that the debug adapter session will be started. + */ +export interface DebugService extends Disposable { + /** + * Finds and returns an array of registered debug types. + * @returns An array of registered debug types + */ + debugTypes(): Promise; + + /** + * Provides initial [debug configuration](#DebugConfiguration). + * @param debugType The registered debug type + * @returns An array of [debug configurations](#DebugConfiguration) + */ + provideDebugConfigurations(debugType: string): Promise; + + /** + * Resolves a [debug configuration](#DebugConfiguration) by filling in missing values + * or by adding/changing/removing attributes. + * @param debugConfiguration The [debug configuration](#DebugConfiguration) to resolve. + * @returns The resolved debug configuration. + */ + resolveDebugConfiguration(config: DebugConfiguration): Promise; + + /** + * Starts a new [debug adapter session](#DebugAdapterSession). + * Returning the value 'undefined' means the debug adapter session can't be started. + * @param config The resolved [debug configuration](#DebugConfiguration). + * @returns The identifier of the created [debug adapter session](#DebugAdapterSession). + */ + start(config: DebugConfiguration): Promise; +} + +/** + * Configuration for a debug adapter session. + */ +export interface DebugConfiguration { + /** + * The type of the debug adapter session. + */ + type: string; + + /** + * The name of the debug adapter session. + */ + name: string; + + /** + * Supported file patterns for breakpoints. + */ + breakpoints: { + filePatterns: string[]; + } + + /** + * Additional debug type specific properties. + */ + [key: string]: any; +} + +/** + * The endpoint path to the debug adapter session. + */ +export const DebugAdapterPath = '/services/debug-adapter'; + +/** + * The debug session state. + */ +export interface DebugSessionState { + /** + * Indicates if debug session is connected to the debug adapter. + */ + isConnected: boolean; + + /** + * Indicates if all threads are continued. + */ + allThreadsContinued: boolean | undefined; + + /** + * Indicates if all threads are stopped. + */ + allThreadsStopped: boolean | undefined; + + /** + * Stopped threads Ids. + */ + stoppedThreadIds: Set; + + /** + * Debug adapter protocol capabilities. + */ + capabilities: DebugProtocol.Capabilities; + + /** + * Loaded sources. + */ + sources: Map; +} + +/** + * Extension to the vscode debug protocol. + */ +export namespace ExtDebugProtocol { + + export interface Variable extends DebugProtocol.Variable { + /** Parent variables reference. */ + parentVariablesReference: number; + } + + /** + * Event message for 'connected' event type. + */ + export interface ConnectedEvent extends DebugProtocol.Event { } + + /** + * Event message for 'configurationDone' event type. + */ + export interface ConfigurationDoneEvent extends DebugProtocol.Event { } + + /** + * Event message for 'variableUpdated' event type. + */ + export interface VariableUpdatedEvent extends DebugProtocol.Event { + body: { + /** The variable's name. */ + name: string; + /** The new value of the variable. */ + value: string; + /** The type of the new value. Typically shown in the UI when hovering over the value. */ + type?: string; + /** If variablesReference is > 0, the new value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. */ + variablesReference?: number; + /** The number of named child variables. The client can use this optional information to present the variables in a paged UI and fetch them in chunks. */ + namedVariables?: number; + /** The number of indexed child variables. The client can use this optional information to present the variables in a paged UI and fetch them in chunks. */ + indexedVariables?: number; + /** Parent variables reference. */ + parentVariablesReference: number; + } + } + + /** + * Exceptional breakpoint. + */ + export interface ExceptionBreakpoint { + /** ID of checked exception options returned via the 'exceptionBreakpointFilters' capability. */ + filter: string; + /** Configuration options for exception. */ + exceptionOptions?: DebugProtocol.ExceptionOptions; + } + + /** + * The aggregated breakpoint. + */ + export interface AggregatedBreakpoint { + /** + * Indicates that breakpoint is attached to the specific debug session. + */ + sessionId?: string + /** + * Breakpoint created in setBreakpoints or setFunctionBreakpoints. + */ + created?: DebugProtocol.Breakpoint; + /** + * A Source is a descriptor for source code. + * If source is defined then breakpoint is a [SourceBreakpoint](#DebugProtocol.SourceBreakpoint) + */ + source?: DebugProtocol.Source; + /** + * One of possible breakpoints. + */ + origin: DebugProtocol.SourceBreakpoint | DebugProtocol.FunctionBreakpoint | ExtDebugProtocol.ExceptionBreakpoint; + } +} + +/** + * Accumulates session states since some data are available only through events + * and are needed in different components. + */ +export class DebugSessionStateAccumulator implements DebugSessionState { + isConnected: boolean; + allThreadsContinued: boolean | undefined; + allThreadsStopped: boolean | undefined; + stoppedThreadIds = new Set(); + capabilities: DebugProtocol.Capabilities = {}; + sources = new Map(); + + constructor(eventEmitter: NodeJS.EventEmitter, currentState?: DebugSessionState) { + if (currentState) { + this.stoppedThreadIds = new Set(currentState.stoppedThreadIds); + this.sources = new Map(currentState.sources); + this.isConnected = currentState.isConnected; + this.allThreadsContinued = currentState.allThreadsContinued; + this.allThreadsStopped = currentState.allThreadsStopped; + } + + eventEmitter.on('connected', event => this.onConnected(event)); + eventEmitter.on('terminated', event => this.onTerminated(event)); + eventEmitter.on('stopped', event => this.onStopped(event)); + eventEmitter.on('continued', event => this.onContinued(event)); + eventEmitter.on('thread', event => this.onThread(event)); + eventEmitter.on('capabilities', event => this.onCapabilitiesEvent(event)); + eventEmitter.on('loadedSource', event => this.onLoadedSource(event)); + } + + private onConnected(event: ExtDebugProtocol.ConnectedEvent): void { + this.isConnected = true; + } + + private onTerminated(event: DebugProtocol.TerminatedEvent): void { + this.isConnected = false; + } + + private onContinued(event: DebugProtocol.ContinuedEvent): void { + const body = event.body; + + this.allThreadsContinued = body.allThreadsContinued; + if (this.allThreadsContinued) { + this.stoppedThreadIds.clear(); + } else { + this.stoppedThreadIds.delete(body.threadId); + } + } + + private onStopped(event: DebugProtocol.StoppedEvent): void { + const body = event.body; + + this.allThreadsStopped = body.allThreadsStopped; + if (body.threadId) { + this.stoppedThreadIds.add(body.threadId); + } + } + + private onThread(event: DebugProtocol.ThreadEvent): void { + switch (event.body.reason) { + case 'exited': { + this.stoppedThreadIds.delete(event.body.threadId); + break; + } + } + } + + private onLoadedSource(event: DebugProtocol.LoadedSourceEvent): void { + const source = event.body.source; + switch (event.body.reason) { + case 'new': + case 'changed': { + if (source.path) { + this.sources.set(source.path, source); + } if (source.sourceReference) { + this.sources.set(source.sourceReference.toString(), source); + } + + break; + } + } + } + + private onCapabilitiesEvent(event: DebugProtocol.CapabilitiesEvent): void { + Object.assign(this.capabilities, event.body.capabilities); + } +} diff --git a/packages/debug/src/node/debug-adapter.ts b/packages/debug/src/node/debug-adapter.ts new file mode 100644 index 0000000000000..9150c9193a3cb --- /dev/null +++ b/packages/debug/src/node/debug-adapter.ts @@ -0,0 +1,327 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Some entities copied and modified from https://github.com/Microsoft/vscode-debugadapter-node/blob/master/adapter/src/protocol.ts + +import * as WebSocket from 'ws'; +import { injectable, inject } from 'inversify'; +import { ILogger, DisposableCollection, Disposable } from '@theia/core'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { + DebugAdapterPath, + DebugSessionState, + DebugSessionStateAccumulator, + ExtDebugProtocol +} from '../common/debug-common'; +import { + RawProcessFactory, + ProcessManager, + RawProcess +} from '@theia/process/lib/node'; +import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service'; +import { + DebugAdapterExecutable, + CommunicationProvider, + DebugAdapterSession, + DebugAdapterSessionFactory, + DebugAdapterFactory +} from './debug-model'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { EventEmitter } from 'events'; + +/** + * The container for [Messaging Service](#MessagingService). + */ +@injectable() +export class MessagingServiceContainer implements MessagingService.Contribution { + protected service = new Deferred(); + + getService(): Promise { + return this.service.promise; + } + + configure(service: MessagingService): void { + this.service.resolve(service); + } +} + +/** + * [DebugAdapterFactory](#DebugAdapterFactory) implementation based on + * launching the debug adapter as separate process. + */ +@injectable() +export class LaunchBasedDebugAdapterFactory implements DebugAdapterFactory { + @inject(RawProcessFactory) + protected readonly processFactory: RawProcessFactory; + @inject(ProcessManager) + protected readonly processManager: ProcessManager; + + start(executable: DebugAdapterExecutable): CommunicationProvider { + const process = this.spawnProcess(executable); + + return { + input: process.input, + output: process.output, + dispose: () => process.kill() + }; + } + + private spawnProcess(executable: DebugAdapterExecutable): RawProcess { + const command = executable.runtime + ? executable.runtime + : executable.program; + + const args = executable.runtime + ? [executable.program].concat(executable.args ? executable.args : []) + : executable.args; + + return this.processFactory({ command: command, args: args }); + } +} + +/** + * [DebugAdapterSession](#DebugAdapterSession) implementation. + */ +export class DebugAdapterSessionImpl extends EventEmitter implements DebugAdapterSession { + readonly state: DebugSessionState; + + private static TWO_CRLF = '\r\n\r\n'; + + private readonly toDispose = new DisposableCollection(); + private pendingRequests = new Map(); + private ws: WebSocket; + private contentLength: number; + private buffer: Buffer; + + constructor( + readonly id: string, + protected readonly communicationProvider: CommunicationProvider, + protected readonly logger: ILogger, + protected readonly messagingServiceContainer: MessagingServiceContainer) { + super(); + + this.contentLength = -1; + this.buffer = new Buffer(0); + this.state = new DebugSessionStateAccumulator(this); + this.toDispose.push(this.communicationProvider); + this.toDispose.push(Disposable.create(() => this.pendingRequests.clear())); + } + + start(): Promise { + const path = DebugAdapterPath + '/' + this.id; + return this.messagingServiceContainer.getService().then(service => { + service.ws(path, (params: MessagingService.PathParams, ws: WebSocket) => { + this.ws = ws; + this.toDispose.push(Disposable.create(() => this.ws.close())); + + this.communicationProvider.output.on('data', (data: Buffer) => this.handleData(data)); + this.communicationProvider.output.on('close', () => this.onDebugAdapterClosed()); + this.communicationProvider.output.on('error', (error: Error) => this.onDebugAdapterError(error)); + this.communicationProvider.input.on('error', (error: Error) => this.onDebugAdapterError(error)); + + this.ws.on('message', (data: string) => this.proceedRequest(data)); + }); + }); + } + + protected onDebugAdapterClosed(): void { + const event: DebugProtocol.Event = { + type: 'event', + event: 'terminated', + seq: -1 + }; + this.proceedEvent(JSON.stringify(event), event); + } + + protected onDebugAdapterError(error: Error): void { + const event: DebugProtocol.Event = { + type: 'event', + event: 'error', + seq: -1, + body: error + }; + this.proceedEvent(JSON.stringify(event), event); + } + + protected handleData(data: Buffer): void { + this.buffer = Buffer.concat([this.buffer, data]); + + while (true) { + if (this.contentLength >= 0) { + if (this.buffer.length >= this.contentLength) { + const rawData = this.buffer.toString('utf8', 0, this.contentLength); + this.buffer = this.buffer.slice(this.contentLength); + this.contentLength = -1; + + if (rawData.length > 0) { + const message = JSON.parse(rawData) as DebugProtocol.ProtocolMessage; + if (message.type === 'event') { + this.proceedEvent(rawData, message as DebugProtocol.Event); + } else if (message.type === 'response') { + this.proceedResponse(rawData, message as DebugProtocol.Response); + } + } + continue; // there may be more complete messages to process + } + } else { + const idx = this.buffer.indexOf(DebugAdapterSessionImpl.TWO_CRLF); + if (idx !== -1) { + const header = this.buffer.toString('utf8', 0, idx); + const lines = header.split('\r\n'); + for (let i = 0; i < lines.length; i++) { + const pair = lines[i].split(/: +/); + if (pair[0] === 'Content-Length') { + this.contentLength = +pair[1]; + } + } + this.buffer = this.buffer.slice(idx + DebugAdapterSessionImpl.TWO_CRLF.length); + continue; + } + } + break; + } + } + + protected proceedEvent(rawData: string, event: DebugProtocol.Event): void { + this.logger.debug(`DAP event: ${rawData}`); + + this.emit(event.event, event); + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.send(rawData); + } + } + + protected proceedResponse(rawData: string, response: DebugProtocol.Response): void { + this.logger.debug(`DAP Response: ${rawData}`); + + const request = this.pendingRequests.get(response.request_seq); + + this.pendingRequests.delete(response.request_seq); + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.send(rawData); + } + + if (response.success) { + switch (response.command) { + case 'attach': + case 'launch': { + const event: ExtDebugProtocol.ConnectedEvent = { + type: 'event', + seq: -1, + event: 'connected' + }; + this.proceedEvent(JSON.stringify(event), event); + break; + } + + case 'configurationDone': { + const event: ExtDebugProtocol.ConfigurationDoneEvent = { + type: 'event', + seq: -1, + event: 'configurationDone' + }; + this.proceedEvent(JSON.stringify(event), event); + break; + } + + case 'setVariable': { + const setVariableRequest = request as DebugProtocol.SetVariableRequest; + const event: ExtDebugProtocol.VariableUpdatedEvent = { + type: 'event', + seq: -1, + event: 'variableUpdated', + body: { + ...response.body, + name: setVariableRequest.arguments.name, + parentVariablesReference: setVariableRequest.arguments.variablesReference, + } + }; + this.proceedEvent(JSON.stringify(event), event); + break; + } + + case 'continue': { + const continueRequest = request as DebugProtocol.ContinueRequest; + const continueResponse = response as DebugProtocol.ContinueResponse; + const event: DebugProtocol.ContinuedEvent = { + type: 'event', + seq: -1, + event: 'continued', + body: { + threadId: continueRequest.arguments.threadId, + allThreadsContinued: continueResponse.body && continueResponse.body.allThreadsContinued + } + }; + this.proceedEvent(JSON.stringify(event), event); + break; + } + + case 'initialized': { + const initializeResponse = response as DebugProtocol.InitializeResponse; + const event: DebugProtocol.CapabilitiesEvent = { + type: 'event', + seq: -1, + event: 'capabilities', + body: { + capabilities: initializeResponse.body || {} + } + }; + this.proceedEvent(JSON.stringify(event), event); + break; + } + } + } + } + + protected proceedRequest(data: string): void { + this.logger.debug(`DAP Request: ${data}`); + + const request = JSON.parse(data) as DebugProtocol.Request; + this.pendingRequests.set(request.seq, request); + + this.communicationProvider.input.write(`Content-Length: ${Buffer.byteLength(data, 'utf8')}\r\n\r\n${data}`, 'utf8'); + } + + stop(): Promise { + return Promise.resolve(this.toDispose.dispose()); + } +} + +/** + * [DebugAdapterSessionFactory](#DebugAdapterSessionFactory) implementation. + */ +@injectable() +export class DebugAdapterSessionFactoryImpl implements DebugAdapterSessionFactory { + + constructor( + @inject(ILogger) + protected readonly logger: ILogger, + @inject(MessagingServiceContainer) + protected readonly messagingServiceContainer: MessagingServiceContainer) { } + + get(sessionId: string, communicationProvider: CommunicationProvider): DebugAdapterSession { + return new DebugAdapterSessionImpl( + sessionId, + communicationProvider, + this.logger, + this.messagingServiceContainer); + } +} diff --git a/packages/debug/src/node/debug-backend-module.ts b/packages/debug/src/node/debug-backend-module.ts new file mode 100644 index 0000000000000..41486777fae4c --- /dev/null +++ b/packages/debug/src/node/debug-backend-module.ts @@ -0,0 +1,60 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { ConnectionHandler, JsonRpcConnectionHandler, bindContributionProvider } from '@theia/core/lib/common'; +import { ContainerModule } from 'inversify'; +import { + DebugServiceImpl, + DebugAdapterSessionManager, + DebugAdapterContributionRegistry +} from './debug-service'; +import { + DebugPath, + DebugService +} from '../common/debug-common'; +import { + MessagingServiceContainer, + LaunchBasedDebugAdapterFactory, + DebugAdapterSessionImpl, + DebugAdapterSessionFactoryImpl +} from './debug-adapter'; +import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service'; +import { + DebugAdapterContribution, + DebugAdapterSessionFactory, + DebugAdapterSession, + DebugAdapterFactory +} from './debug-model'; + +export default new ContainerModule(bind => { + bind(DebugService).to(DebugServiceImpl).inSingletonScope(); + bind(DebugAdapterSession).to(DebugAdapterSessionImpl); + bind(DebugAdapterSessionFactory).to(DebugAdapterSessionFactoryImpl).inSingletonScope(); + bind(DebugAdapterFactory).to(LaunchBasedDebugAdapterFactory).inSingletonScope(); + bind(DebugAdapterContributionRegistry).toSelf().inSingletonScope(); + bind(DebugAdapterSessionManager).toSelf().inSingletonScope(); + bind(MessagingServiceContainer).toSelf().inSingletonScope(); + bind(MessagingService.Contribution).toDynamicValue(c => c.container.get(MessagingServiceContainer)); + bindContributionProvider(bind, DebugAdapterContribution); + + bind(ConnectionHandler).toDynamicValue(context => + new JsonRpcConnectionHandler(DebugPath, client => { + const service = context.container.get(DebugService); + client.onDidCloseConnection(() => service.dispose()); + return service; + }) + ).inSingletonScope(); +}); diff --git a/packages/debug/src/node/debug-model.ts b/packages/debug/src/node/debug-model.ts new file mode 100644 index 0000000000000..5aaa55b344772 --- /dev/null +++ b/packages/debug/src/node/debug-model.ts @@ -0,0 +1,137 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Some entities copied and modified from https://github.com/Microsoft/vscode/blob/master/src/vs/vscode.d.ts +// Some entities copied and modified from https://github.com/Microsoft/vscode/blob/master/src/vs/workbench/parts/debug/common/debug.ts + +import { Disposable } from '@theia/core'; +import * as stream from 'stream'; +import { DebugConfiguration, DebugSessionState } from '../common/debug-common'; + +/** + * DebugAdapterSession symbol for DI. + */ +export const DebugAdapterSession = Symbol('DebugAdapterSession'); + +/** + * The debug adapter session. + */ +export interface DebugAdapterSession { + id: string; + state: DebugSessionState; + + start(): Promise + stop(): Promise +} + +/** + * DebugAdapterSessionFactory symbol for DI. + */ +export const DebugAdapterSessionFactory = Symbol('DebugAdapterSessionFactory'); + +/** + * The [debug session](#DebugSession) factory. + */ +export interface DebugAdapterSessionFactory { + get(sessionId: string, communicationProvider: CommunicationProvider): DebugAdapterSession; +} + +/** + * Debug adapter executable. + */ +export interface DebugAdapterExecutable { + /** + * Parameters to instantiate the debug adapter. In case of launching adapter + * the parameters contain a command and arguments. For instance: + * {'program' : 'COMMAND_TO_LAUNCH_DEBUG_ADAPTER', args : [ { 'arg1', 'arg2' } ] } + */ + [key: string]: any; +} + +/** + * Provides some way we can communicate with the running debug adapter. In general there is + * no obligation as of how to launch/initialize local or remote debug adapter + * process/server, it can be done separately and it is not required that this interface covers the + * procedure, however it is also not disallowed. + */ +export interface CommunicationProvider extends Disposable { + output: stream.Readable; + input: stream.Writable; +} + +/** + * DebugAdapterFactory symbol for DI. + */ +export const DebugAdapterFactory = Symbol('DebugAdapterFactory'); + +/** + * Factory to start debug adapter. + */ +export interface DebugAdapterFactory { + start(executable: DebugAdapterExecutable): CommunicationProvider; +} + +/** + * DebugAdapterContribution symbol for DI. + */ +export const DebugAdapterContribution = Symbol('DebugAdapterContribution'); + +/** + * A contribution point for debug adapters. + */ +export interface DebugAdapterContribution { + /** + * The debug type. Should be a unique value among all debug adapters. + */ + readonly debugType: string; + + /** + * The [debug adapter session](#DebugAdapterSession) factory. + * If a default implementation of the debug adapter session does not + * fit all needs it is possible to provide its own implementation using + * this factory. But it is strongly recommended to extend the default + * implementation if so. + */ + debugAdapterSessionFactory?: DebugAdapterSessionFactory; + + /** + * Provides initial [debug configuration](#DebugConfiguration). + * @returns An array of [debug configurations](#DebugConfiguration). + */ + provideDebugConfigurations: DebugConfiguration[]; + + /** + * Resolves a [debug configuration](#DebugConfiguration) by filling in missing values + * or by adding/changing/removing attributes. + * @param config The [debug configuration](#DebugConfiguration) to resolve. + * @returns The resolved debug configuration. + */ + resolveDebugConfiguration(config: DebugConfiguration): DebugConfiguration; + + /** + * Provides a [debug adapter executable](#DebugAdapterExecutable) + * based on [debug configuration](#DebugConfiguration) to launch a new debug adapter + * or to connect to existed one. + * @param config The resolved [debug configuration](#DebugConfiguration). + * @returns The [debug adapter executable](#DebugAdapterExecutable). + */ + provideDebugAdapterExecutable(config: DebugConfiguration): DebugAdapterExecutable; +} diff --git a/packages/debug/src/node/debug-service.ts b/packages/debug/src/node/debug-service.ts new file mode 100644 index 0000000000000..e87cb95c31d5b --- /dev/null +++ b/packages/debug/src/node/debug-service.ts @@ -0,0 +1,204 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. 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 { injectable, inject, named } from 'inversify'; +import { ContributionProvider } from '@theia/core'; +import { + DebugService, + DebugConfiguration +} from '../common/debug-common'; + +import { UUID } from '@phosphor/coreutils'; +import { DebugAdapterContribution, DebugAdapterExecutable, DebugAdapterSession, DebugAdapterSessionFactory, DebugAdapterFactory } from './debug-model'; + +/** + * Contributions registry. + */ +@injectable() +export class DebugAdapterContributionRegistry { + protected readonly contribs = new Map(); + + constructor( + @inject(ContributionProvider) @named(DebugAdapterContribution) + protected readonly contributions: ContributionProvider + ) { + for (const contrib of this.contributions.getContributions()) { + this.contribs.set(contrib.debugType, contrib); + } + } + + /** + * Finds and returns an array of registered debug types. + * @returns An array of registered debug types + */ + debugTypes(): string[] { + return Array.from(this.contribs.keys()); + } + + /** + * Provides initial [debug configuration](#DebugConfiguration). + * @param debugType The registered debug type + * @returns An array of [debug configurations](#DebugConfiguration) + */ + provideDebugConfigurations(debugType: string): DebugConfiguration[] { + const contrib = this.contribs.get(debugType); + if (contrib) { + return contrib.provideDebugConfigurations; + } + throw new Error(`Debug adapter '${debugType}' isn't registered.`); + } + + /** + * Resolves a [debug configuration](#DebugConfiguration) by filling in missing values + * or by adding/changing/removing attributes. + * @param debugConfiguration The [debug configuration](#DebugConfiguration) to resolve. + * @returns The resolved debug configuration. + */ + resolveDebugConfiguration(config: DebugConfiguration): DebugConfiguration { + const contrib = this.contribs.get(config.type); + if (contrib) { + return contrib.resolveDebugConfiguration(config); + } + throw new Error(`Debug adapter '${config.type}' isn't registered.`); + } + + /** + * Provides a [debug adapter executable](#DebugAdapterExecutable) + * based on [debug configuration](#DebugConfiguration) to launch a new debug adapter. + * @param config The resolved [debug configuration](#DebugConfiguration). + * @returns The [debug adapter executable](#DebugAdapterExecutable). + */ + provideDebugAdapterExecutable(config: DebugConfiguration): DebugAdapterExecutable { + const contrib = this.contribs.get(config.type); + if (contrib) { + return contrib.provideDebugAdapterExecutable(config); + } + throw new Error(`Debug adapter '${config.type}' isn't registered.`); + } + + /** + * Returns a [debug adapter session factory](#DebugAdapterSessionFactory). + * @param debugType The registered debug type + * @returns An [debug adapter session factory](#DebugAdapterSessionFactory) + */ + debugAdapterSessionFactory(debugType: string): DebugAdapterSessionFactory | undefined { + const contrib = this.contribs.get(debugType); + if (contrib) { + return contrib.debugAdapterSessionFactory; + } + } +} + +/** + * Debug adapter session manager. + */ +@injectable() +export class DebugAdapterSessionManager { + protected readonly sessions = new Map(); + + constructor( + @inject(DebugAdapterContributionRegistry) + protected readonly registry: DebugAdapterContributionRegistry, + @inject(DebugAdapterSessionFactory) + protected readonly debugAdapterSessionFactory: DebugAdapterSessionFactory, + @inject(DebugAdapterFactory) + protected readonly debugAdapterFactory: DebugAdapterFactory + ) { } + + /** + * Creates a new [debug adapter session](#DebugAdapterSession). + * @param config The [DebugConfiguration](#DebugConfiguration) + * @returns The debug adapter session + */ + create(config: DebugConfiguration): DebugAdapterSession { + const sessionId = UUID.uuid4(); + + const executable = this.registry.provideDebugAdapterExecutable(config); + const communicationProvider = this.debugAdapterFactory.start(executable); + + const sessionFactory = this.registry.debugAdapterSessionFactory(config.type) || this.debugAdapterSessionFactory; + const session = sessionFactory.get(sessionId, communicationProvider); + this.sessions.set(sessionId, session); + return session; + } + + /** + * Removes [debug adapter session](#DebugAdapterSession) from the list of the instantiated sessions. + * Is invoked when session is terminated and isn't needed anymore. + * @param sessionId The session identifier + */ + remove(sessionId: string): void { + this.sessions.delete(sessionId); + } + + /** + * Finds the debug adapter session by its id. + * Returning the value 'undefined' means the session isn't found. + * @param sessionId The session identifier + * @returns The debug adapter session + */ + find(sessionId: string): DebugAdapterSession | undefined { + return this.sessions.get(sessionId); + } + + /** + * Finds all instantiated debug adapter sessions. + * @returns An array of debug adapter sessions + */ + findAll(): DebugAdapterSession[] { + return Array.from(this.sessions.values()); + } +} + +/** + * DebugService implementation. + */ +@injectable() +export class DebugServiceImpl implements DebugService { + constructor( + @inject(DebugAdapterSessionManager) + protected readonly sessionManager: DebugAdapterSessionManager, + @inject(DebugAdapterContributionRegistry) + protected readonly registry: DebugAdapterContributionRegistry) { } + + async debugTypes(): Promise { + return this.registry.debugTypes(); + } + + async provideDebugConfigurations(debugType: string): Promise { + return this.registry.provideDebugConfigurations(debugType); + } + + async resolveDebugConfiguration(config: DebugConfiguration): Promise { + return this.registry.resolveDebugConfiguration(config); + } + + async start(config: DebugConfiguration): Promise { + const session = this.sessionManager.create(config); + return session.start().then(() => session.id); + } + + async dispose(sessionId?: string): Promise { + if (sessionId) { + const debugSession = this.sessionManager.find(sessionId); + if (debugSession) { + debugSession.stop(); + } + } else { + this.sessionManager.findAll().forEach(debugSession => debugSession.stop()); + } + } +} diff --git a/packages/editor/src/browser/editor.ts b/packages/editor/src/browser/editor.ts index 781b626ca0cd2..0f046f9f86eef 100644 --- a/packages/editor/src/browser/editor.ts +++ b/packages/editor/src/browser/editor.ts @@ -51,6 +51,101 @@ export interface TextDocumentChangeEvent { readonly contentChanges: TextDocumentContentChangeDelta[]; } +/** + * Type of hit element with the mouse in the editor. + * Copied from monaco editor. + */ +export enum MouseTargetType { + /** + * Mouse is on top of an unknown element. + */ + UNKNOWN = 0, + /** + * Mouse is on top of the textarea used for input. + */ + TEXTAREA = 1, + /** + * Mouse is on top of the glyph margin + */ + GUTTER_GLYPH_MARGIN = 2, + /** + * Mouse is on top of the line numbers + */ + GUTTER_LINE_NUMBERS = 3, + /** + * Mouse is on top of the line decorations + */ + GUTTER_LINE_DECORATIONS = 4, + /** + * Mouse is on top of the whitespace left in the gutter by a view zone. + */ + GUTTER_VIEW_ZONE = 5, + /** + * Mouse is on top of text in the content. + */ + CONTENT_TEXT = 6, + /** + * Mouse is on top of empty space in the content (e.g. after line text or below last line) + */ + CONTENT_EMPTY = 7, + /** + * Mouse is on top of a view zone in the content. + */ + CONTENT_VIEW_ZONE = 8, + /** + * Mouse is on top of a content widget. + */ + CONTENT_WIDGET = 9, + /** + * Mouse is on top of the decorations overview ruler. + */ + OVERVIEW_RULER = 10, + /** + * Mouse is on top of a scrollbar. + */ + SCROLLBAR = 11, + /** + * Mouse is on top of an overlay widget. + */ + OVERLAY_WIDGET = 12, + /** + * Mouse is outside of the editor. + */ + OUTSIDE_EDITOR = 13, +} + +export interface MouseTarget { + /** + * The target element + */ + readonly element: Element; + /** + * The target type + */ + readonly type: MouseTargetType; + /** + * The 'approximate' editor position + */ + readonly position: Position; + /** + * Desired mouse column (e.g. when position.column gets clamped to text length -- clicking after text on a line). + */ + readonly mouseColumn: number; + /** + * The 'approximate' editor range + */ + readonly range: Range; + /** + * Some extra detail. + */ + readonly detail: any; +} + +export interface EditorMouseEvent { + readonly event: MouseEvent; + readonly target: MouseTarget; +} + export interface TextEditor extends Disposable, TextEditorSelection { readonly node: HTMLElement; @@ -69,6 +164,8 @@ export interface TextEditor extends Disposable, TextEditorSelection { isFocused(): boolean; readonly onFocusChanged: Event; + readonly onMouseDown: Event; + revealPosition(position: Position, options?: RevealPositionOptions): void; revealRange(range: Range, options?: RevealRangeOptions): void; diff --git a/packages/monaco/src/browser/monaco-editor.ts b/packages/monaco/src/browser/monaco-editor.ts index dcd3629d9c028..afc64a6b9ec75 100644 --- a/packages/monaco/src/browser/monaco-editor.ts +++ b/packages/monaco/src/browser/monaco-editor.ts @@ -31,7 +31,8 @@ import { RevealPositionOptions, DeltaDecorationParams, ReplaceTextParams, - EditorDecoration + EditorDecoration, + EditorMouseEvent } from '@theia/editor/lib/browser'; import { MonacoEditorModel } from './monaco-editor-model'; @@ -58,6 +59,7 @@ export class MonacoEditor implements TextEditor, IEditorReference { protected readonly onSelectionChangedEmitter = new Emitter(); protected readonly onFocusChangedEmitter = new Emitter(); protected readonly onDocumentContentChangedEmitter = new Emitter(); + protected readonly onMouseDownEmitter = new Emitter(); readonly documents = new Set(); @@ -74,7 +76,8 @@ export class MonacoEditor implements TextEditor, IEditorReference { this.onCursorPositionChangedEmitter, this.onSelectionChangedEmitter, this.onFocusChangedEmitter, - this.onDocumentContentChangedEmitter + this.onDocumentContentChangedEmitter, + this.onMouseDownEmitter ]); this.documents.add(document); this.autoSizing = options && options.autoSizing !== undefined ? options.autoSizing : false; @@ -117,6 +120,19 @@ export class MonacoEditor implements TextEditor, IEditorReference { this.toDispose.push(codeEditor.onDidBlurEditor(() => this.onFocusChangedEmitter.fire(this.isFocused()) )); + this.toDispose.push(codeEditor.onMouseDown(e => { + const { lineNumber, column } = e.target.position; + const event = { + target: { + ...e.target, + mouseColumn: this.m2p.asPosition(undefined, e.target.mouseColumn).character, + range: this.m2p.asRange(e.target.range), + position: this.m2p.asPosition(lineNumber, column), + }, + event: e.event.browserEvent + }; + this.onMouseDownEmitter.fire(event); + })); } protected mapModelContentChange(change: monaco.editor.IModelContentChange): TextDocumentContentChangeDelta { @@ -213,6 +229,10 @@ export class MonacoEditor implements TextEditor, IEditorReference { return this.onFocusChangedEmitter.event; } + get onMouseDown(): Event { + return this.onMouseDownEmitter.event; + } + /** * `true` if the suggest widget is visible in the editor. Otherwise, `false`. */ diff --git a/tsconfig.json b/tsconfig.json index 29b0057a98be5..561953cf28e48 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -58,6 +58,12 @@ "@theia/java/lib/*": [ "packages/java/src/*" ], + "@theia/debug/lib/*": [ + "packages/debug/src/*" + ], + "@theia/debug-nodejs/lib/*": [ + "packages/debug-nodejs/src/*" + ], "@theia/python/lib/*": [ "packages/python/src/*" ], diff --git a/yarn.lock b/yarn.lock index a6dd0c01ff8c3..8474bade241d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4542,6 +4542,13 @@ har-validator@~5.1.0: ajv "^5.3.0" har-schema "^2.0.0" +har-validator@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" + dependencies: + ajv "^5.3.0" + har-schema "^2.0.0" + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -9847,6 +9854,10 @@ vscode-base-languageclient@4.4.0: dependencies: vscode-languageserver-protocol "^3.10.0" +vscode-debugprotocol@^1.26.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.31.0.tgz#8467eeabeea65f52da5ac03b03c18e10e8b95eb4" + vscode-jsonrpc@^3.6.0, vscode-jsonrpc@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-3.6.2.tgz#3b5eef691159a15556ecc500e9a8a0dd143470c8"