deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/debug/.eslintrc.js
Normal file
10
packages/debug/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
1
packages/debug/.gitignore
vendored
Normal file
1
packages/debug/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*-test-temp
|
||||
65
packages/debug/README.md
Normal file
65
packages/debug/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
<div align='center'>
|
||||
|
||||
<br />
|
||||
|
||||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
|
||||
|
||||
<h2>ECLIPSE THEIA - DEBUG EXTENSION</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## 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/eclipse-theia/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/eclipse-theia/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.
|
||||
|
||||
## Additional Information
|
||||
|
||||
* [API documentation for `@theia/debug`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_debug.html)
|
||||
* [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
* [Theia - Website](https://theia-ide.org/)
|
||||
* [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)
|
||||
* [Node Debugger 2](https://github.com/microsoft/vscode-node-debug2)
|
||||
* [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)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
68
packages/debug/package.json
Normal file
68
packages/debug/package.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "@theia/debug",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia - Debug Extension",
|
||||
"dependencies": {
|
||||
"@theia/console": "1.68.0",
|
||||
"@theia/core": "1.68.0",
|
||||
"@theia/editor": "1.68.0",
|
||||
"@theia/filesystem": "1.68.0",
|
||||
"@theia/markers": "1.68.0",
|
||||
"@theia/monaco": "1.68.0",
|
||||
"@theia/monaco-editor-core": "1.96.302",
|
||||
"@theia/output": "1.68.0",
|
||||
"@theia/process": "1.68.0",
|
||||
"@theia/task": "1.68.0",
|
||||
"@theia/terminal": "1.68.0",
|
||||
"@theia/test": "1.68.0",
|
||||
"@theia/variable-resolver": "1.68.0",
|
||||
"@theia/workspace": "1.68.0",
|
||||
"@vscode/debugprotocol": "^1.51.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"jsonc-parser": "^2.2.0",
|
||||
"p-debounce": "^2.1.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"frontend": "lib/browser/debug-frontend-module",
|
||||
"secondaryWindow": "lib/browser/debug-frontend-module",
|
||||
"backend": "lib/node/debug-backend-module"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"theia-extension",
|
||||
"debug"
|
||||
],
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/eclipse-theia/theia.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/eclipse-theia/theia/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eclipse-theia/theia",
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "theiaext build",
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"test": "theiaext test",
|
||||
"watch": "theiaext watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/ext-scripts": "1.68.0"
|
||||
},
|
||||
"nyc": {
|
||||
"extends": "../../configs/nyc.json"
|
||||
},
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
422
packages/debug/src/browser/breakpoint/breakpoint-manager.ts
Normal file
422
packages/debug/src/browser/breakpoint/breakpoint-manager.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as deepEqual from 'fast-deep-equal';
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { Emitter } from '@theia/core/lib/common';
|
||||
import { StorageService } from '@theia/core/lib/browser';
|
||||
import { Marker } from '@theia/markers/lib/common/marker';
|
||||
import { MarkerManager } from '@theia/markers/lib/browser/marker-manager';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { SourceBreakpoint, BREAKPOINT_KIND, ExceptionBreakpoint, FunctionBreakpoint, BaseBreakpoint, InstructionBreakpoint, DataBreakpoint } from './breakpoint-marker';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol';
|
||||
|
||||
export interface BreakpointsChangeEvent<T extends BaseBreakpoint> {
|
||||
uri: URI
|
||||
added: T[]
|
||||
removed: T[]
|
||||
changed: T[]
|
||||
}
|
||||
export type SourceBreakpointsChangeEvent = BreakpointsChangeEvent<SourceBreakpoint>;
|
||||
export type FunctionBreakpointsChangeEvent = BreakpointsChangeEvent<FunctionBreakpoint>;
|
||||
export type InstructionBreakpointsChangeEvent = BreakpointsChangeEvent<InstructionBreakpoint>;
|
||||
export type DataBreakpointsChangeEvent = BreakpointsChangeEvent<DataBreakpoint>;
|
||||
|
||||
@injectable()
|
||||
export class BreakpointManager extends MarkerManager<SourceBreakpoint> {
|
||||
|
||||
static EXCEPTION_URI = new URI('debug:exception://');
|
||||
|
||||
static FUNCTION_URI = new URI('debug:function://');
|
||||
|
||||
static INSTRUCTION_URI = new URI('debug:instruction://');
|
||||
|
||||
static DATA_URI = new URI('debug:data://');
|
||||
|
||||
protected readonly owner = 'breakpoint';
|
||||
|
||||
@inject(StorageService)
|
||||
protected readonly storage: StorageService;
|
||||
|
||||
getKind(): string {
|
||||
return BREAKPOINT_KIND;
|
||||
}
|
||||
|
||||
protected readonly onDidChangeBreakpointsEmitter = new Emitter<SourceBreakpointsChangeEvent>();
|
||||
readonly onDidChangeBreakpoints = this.onDidChangeBreakpointsEmitter.event;
|
||||
|
||||
protected readonly onDidChangeFunctionBreakpointsEmitter = new Emitter<FunctionBreakpointsChangeEvent>();
|
||||
readonly onDidChangeFunctionBreakpoints = this.onDidChangeFunctionBreakpointsEmitter.event;
|
||||
|
||||
protected readonly onDidChangeInstructionBreakpointsEmitter = new Emitter<InstructionBreakpointsChangeEvent>();
|
||||
readonly onDidChangeInstructionBreakpoints = this.onDidChangeInstructionBreakpointsEmitter.event;
|
||||
|
||||
protected readonly onDidChangeDataBreakpointsEmitter = new Emitter<DataBreakpointsChangeEvent>();
|
||||
readonly onDidChangeDataBreakpoints = this.onDidChangeDataBreakpointsEmitter.event;
|
||||
|
||||
override setMarkers(uri: URI, owner: string, newMarkers: SourceBreakpoint[]): Marker<SourceBreakpoint>[] {
|
||||
const result = this.findMarkers({ uri, owner });
|
||||
const added: SourceBreakpoint[] = [];
|
||||
const removed: SourceBreakpoint[] = [];
|
||||
const changed: SourceBreakpoint[] = [];
|
||||
const oldMarkers = new Map(result.map(({ data }) => [data.id, data]));
|
||||
const ids = new Set<string>();
|
||||
let didChangeMarkers = false;
|
||||
for (const newMarker of newMarkers) {
|
||||
ids.add(newMarker.id);
|
||||
const oldMarker = oldMarkers.get(newMarker.id);
|
||||
if (!oldMarker) {
|
||||
added.push(newMarker);
|
||||
} else {
|
||||
// We emit all existing markers as 'changed', but we only fire an event if something really did change.
|
||||
// We also fire an event if oldMarker === newMarker, as we cannot actually detect a change in this case
|
||||
// (https://github.com/eclipse-theia/theia/issues/12546).
|
||||
didChangeMarkers ||= !!added.length || oldMarker === newMarker || !deepEqual(oldMarker, newMarker);
|
||||
changed.push(newMarker);
|
||||
}
|
||||
}
|
||||
for (const [id, data] of oldMarkers.entries()) {
|
||||
if (!ids.has(id)) {
|
||||
removed.push(data);
|
||||
}
|
||||
}
|
||||
if (added.length || removed.length || didChangeMarkers) {
|
||||
super.setMarkers(uri, owner, newMarkers);
|
||||
this.onDidChangeBreakpointsEmitter.fire({ uri, added, removed, changed });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getLineBreakpoints(uri: URI, line: number): SourceBreakpoint[] {
|
||||
return this.findMarkers({
|
||||
uri,
|
||||
dataFilter: breakpoint => breakpoint.raw.line === line
|
||||
}).map(({ data }) => data);
|
||||
}
|
||||
|
||||
getInlineBreakpoint(uri: URI, line: number, column: number): SourceBreakpoint | undefined {
|
||||
const marker = this.findMarkers({
|
||||
uri,
|
||||
dataFilter: breakpoint => breakpoint.raw.line === line && breakpoint.raw.column === column
|
||||
})[0];
|
||||
return marker && marker.data;
|
||||
}
|
||||
|
||||
getBreakpoints(uri?: URI): SourceBreakpoint[] {
|
||||
return this.findMarkers({ uri }).map(marker => marker.data);
|
||||
}
|
||||
|
||||
setBreakpoints(uri: URI, breakpoints: SourceBreakpoint[]): void {
|
||||
this.setMarkers(uri, this.owner, breakpoints.sort((a, b) => (a.raw.line - b.raw.line) || ((a.raw.column || 0) - (b.raw.column || 0))));
|
||||
}
|
||||
|
||||
addBreakpoint(breakpoint: SourceBreakpoint): boolean {
|
||||
const uri = new URI(breakpoint.uri);
|
||||
const breakpoints = this.getBreakpoints(uri);
|
||||
const newBreakpoints = breakpoints.filter(({ raw }) => !(raw.line === breakpoint.raw.line && raw.column === breakpoint.raw.column));
|
||||
if (breakpoints.length === newBreakpoints.length) {
|
||||
newBreakpoints.push(breakpoint);
|
||||
this.setBreakpoints(uri, newBreakpoints);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
enableAllBreakpoints(enabled: boolean): void {
|
||||
for (const uriString of this.getUris()) {
|
||||
let didChange = false;
|
||||
const uri = new URI(uriString);
|
||||
const markers = this.findMarkers({ uri });
|
||||
for (const marker of markers) {
|
||||
if (marker.data.enabled !== enabled) {
|
||||
marker.data.enabled = enabled;
|
||||
didChange = true;
|
||||
}
|
||||
}
|
||||
if (didChange) {
|
||||
this.fireOnDidChangeMarkers(uri);
|
||||
}
|
||||
}
|
||||
let didChangeFunction = false;
|
||||
for (const breakpoint of (this.getFunctionBreakpoints() as BaseBreakpoint[]).concat(this.getInstructionBreakpoints())) {
|
||||
if (breakpoint.enabled !== enabled) {
|
||||
breakpoint.enabled = enabled;
|
||||
didChangeFunction = true;
|
||||
|
||||
}
|
||||
}
|
||||
if (didChangeFunction) {
|
||||
this.fireOnDidChangeMarkers(BreakpointManager.FUNCTION_URI);
|
||||
}
|
||||
}
|
||||
|
||||
protected _breakpointsEnabled = true;
|
||||
get breakpointsEnabled(): boolean {
|
||||
return this._breakpointsEnabled;
|
||||
}
|
||||
set breakpointsEnabled(breakpointsEnabled: boolean) {
|
||||
if (this._breakpointsEnabled !== breakpointsEnabled) {
|
||||
this._breakpointsEnabled = breakpointsEnabled;
|
||||
for (const uri of this.getUris()) {
|
||||
this.fireOnDidChangeMarkers(new URI(uri));
|
||||
}
|
||||
this.fireOnDidChangeMarkers(BreakpointManager.FUNCTION_URI);
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly exceptionBreakpoints = new Map<string, ExceptionBreakpoint>();
|
||||
|
||||
getExceptionBreakpoint(filter: string): ExceptionBreakpoint | undefined {
|
||||
return this.exceptionBreakpoints.get(filter);
|
||||
}
|
||||
|
||||
getExceptionBreakpoints(): IterableIterator<ExceptionBreakpoint> {
|
||||
return this.exceptionBreakpoints.values();
|
||||
}
|
||||
|
||||
setExceptionBreakpoints(exceptionBreakpoints: ExceptionBreakpoint[]): void {
|
||||
const toRemove = new Set(this.exceptionBreakpoints.keys());
|
||||
for (const exceptionBreakpoint of exceptionBreakpoints) {
|
||||
const filter = exceptionBreakpoint.raw.filter;
|
||||
toRemove.delete(filter);
|
||||
this.exceptionBreakpoints.set(filter, exceptionBreakpoint);
|
||||
}
|
||||
for (const filter of toRemove) {
|
||||
this.exceptionBreakpoints.delete(filter);
|
||||
}
|
||||
if (toRemove.size || exceptionBreakpoints.length) {
|
||||
this.fireOnDidChangeMarkers(BreakpointManager.EXCEPTION_URI);
|
||||
}
|
||||
}
|
||||
|
||||
toggleExceptionBreakpoint(filter: string): void {
|
||||
const breakpoint = this.getExceptionBreakpoint(filter);
|
||||
if (breakpoint) {
|
||||
breakpoint.enabled = !breakpoint.enabled;
|
||||
this.fireOnDidChangeMarkers(BreakpointManager.EXCEPTION_URI);
|
||||
}
|
||||
}
|
||||
|
||||
updateExceptionBreakpoint(filter: string, options: Partial<Pick<ExceptionBreakpoint, 'condition' | 'enabled'>>): void {
|
||||
const breakpoint = this.getExceptionBreakpoint(filter);
|
||||
if (breakpoint) {
|
||||
Object.assign(breakpoint, options);
|
||||
this.fireOnDidChangeMarkers(BreakpointManager.EXCEPTION_URI);
|
||||
}
|
||||
}
|
||||
|
||||
protected functionBreakpoints: FunctionBreakpoint[] = [];
|
||||
|
||||
getFunctionBreakpoints(): FunctionBreakpoint[] {
|
||||
return this.functionBreakpoints;
|
||||
}
|
||||
|
||||
setFunctionBreakpoints(functionBreakpoints: FunctionBreakpoint[]): void {
|
||||
const oldBreakpoints = new Map(this.functionBreakpoints.map(b => [b.id, b] as [string, FunctionBreakpoint]));
|
||||
|
||||
this.functionBreakpoints = functionBreakpoints;
|
||||
this.fireOnDidChangeMarkers(BreakpointManager.FUNCTION_URI);
|
||||
|
||||
const added: FunctionBreakpoint[] = [];
|
||||
const removed: FunctionBreakpoint[] = [];
|
||||
const changed: FunctionBreakpoint[] = [];
|
||||
const ids = new Set<string>();
|
||||
for (const newBreakpoint of functionBreakpoints) {
|
||||
ids.add(newBreakpoint.id);
|
||||
if (oldBreakpoints.has(newBreakpoint.id)) {
|
||||
changed.push(newBreakpoint);
|
||||
} else {
|
||||
added.push(newBreakpoint);
|
||||
}
|
||||
}
|
||||
for (const [id, breakpoint] of oldBreakpoints.entries()) {
|
||||
if (!ids.has(id)) {
|
||||
removed.push(breakpoint);
|
||||
}
|
||||
}
|
||||
this.onDidChangeFunctionBreakpointsEmitter.fire({ uri: BreakpointManager.FUNCTION_URI, added, removed, changed });
|
||||
}
|
||||
|
||||
protected instructionBreakpoints: InstructionBreakpoint[] = [];
|
||||
|
||||
getInstructionBreakpoints(): ReadonlyArray<InstructionBreakpoint> {
|
||||
return Object.freeze(this.instructionBreakpoints.slice());
|
||||
}
|
||||
|
||||
hasBreakpoints(): boolean {
|
||||
return Boolean(this.getUris().next().value || this.functionBreakpoints.length || this.instructionBreakpoints.length);
|
||||
}
|
||||
|
||||
protected setInstructionBreakpoints(newBreakpoints: InstructionBreakpoint[]): void {
|
||||
const { added, removed, changed } = diff(this.instructionBreakpoints, newBreakpoints, bp => bp.id);
|
||||
this.instructionBreakpoints = newBreakpoints.slice();
|
||||
this.fireOnDidChangeMarkers(BreakpointManager.INSTRUCTION_URI);
|
||||
this.onDidChangeInstructionBreakpointsEmitter.fire({ uri: BreakpointManager.INSTRUCTION_URI, added, removed, changed });
|
||||
}
|
||||
|
||||
addInstructionBreakpoint(address: string, offset: number, condition?: string, hitCondition?: string): void {
|
||||
this.setInstructionBreakpoints(this.instructionBreakpoints.concat(InstructionBreakpoint.create({
|
||||
instructionReference: address,
|
||||
offset,
|
||||
condition,
|
||||
hitCondition,
|
||||
})));
|
||||
}
|
||||
|
||||
updateInstructionBreakpoint(id: string, options: Partial<Pick<InstructionBreakpoint, 'condition' | 'hitCondition' | 'enabled'>>): void {
|
||||
const breakpoint = this.instructionBreakpoints.find(candidate => id === candidate.id);
|
||||
if (breakpoint) {
|
||||
Object.assign(breakpoint, options);
|
||||
this.fireOnDidChangeMarkers(BreakpointManager.INSTRUCTION_URI);
|
||||
this.onDidChangeInstructionBreakpointsEmitter.fire({ uri: BreakpointManager.INSTRUCTION_URI, changed: [breakpoint], added: [], removed: [] });
|
||||
}
|
||||
}
|
||||
|
||||
removeInstructionBreakpoint(address?: string): void {
|
||||
if (!address) {
|
||||
this.clearInstructionBreakpoints();
|
||||
}
|
||||
const breakpointIndex = this.instructionBreakpoints.findIndex(breakpoint => breakpoint.instructionReference === address);
|
||||
if (breakpointIndex !== -1) {
|
||||
const removed = this.instructionBreakpoints.splice(breakpointIndex, 1);
|
||||
this.fireOnDidChangeMarkers(BreakpointManager.INSTRUCTION_URI);
|
||||
this.onDidChangeInstructionBreakpointsEmitter.fire({ uri: BreakpointManager.INSTRUCTION_URI, added: [], changed: [], removed });
|
||||
}
|
||||
}
|
||||
|
||||
clearInstructionBreakpoints(): void {
|
||||
this.setInstructionBreakpoints([]);
|
||||
}
|
||||
|
||||
protected dataBreakpoints: DataBreakpoint[] = [];
|
||||
|
||||
getDataBreakpoints(): readonly DataBreakpoint[] {
|
||||
return Object.freeze(this.dataBreakpoints.slice());
|
||||
}
|
||||
|
||||
setDataBreakpoints(breakpoints: DataBreakpoint[]): void {
|
||||
const { added, removed, changed } = diff(this.dataBreakpoints, breakpoints, ({ id }) => id);
|
||||
this.dataBreakpoints = breakpoints.slice();
|
||||
this.fireOnDidChangeMarkers(BreakpointManager.DATA_URI);
|
||||
this.onDidChangeDataBreakpointsEmitter.fire({ uri: BreakpointManager.DATA_URI, added, removed, changed });
|
||||
}
|
||||
|
||||
addDataBreakpoint(breakpoint: DataBreakpoint): void {
|
||||
this.setDataBreakpoints(this.dataBreakpoints.concat(breakpoint));
|
||||
}
|
||||
|
||||
updateDataBreakpoint(id: string, options: { enabled?: boolean; raw?: Partial<Omit<DebugProtocol.DataBreakpoint, 'dataId'>> }): void {
|
||||
const breakpoint = this.dataBreakpoints.find(bp => bp.id === id);
|
||||
if (!breakpoint) { return; }
|
||||
Object.assign(breakpoint.raw, options);
|
||||
breakpoint.enabled = options.enabled ?? breakpoint.enabled;
|
||||
this.fireOnDidChangeMarkers(BreakpointManager.DATA_URI);
|
||||
this.onDidChangeDataBreakpointsEmitter.fire({ uri: BreakpointManager.DATA_URI, added: [], removed: [], changed: [breakpoint] });
|
||||
}
|
||||
|
||||
removeDataBreakpoint(id: string): void {
|
||||
const index = this.dataBreakpoints.findIndex(bp => bp.id === id);
|
||||
if (index < 0) { return; }
|
||||
const removed = this.dataBreakpoints.splice(index);
|
||||
this.fireOnDidChangeMarkers(BreakpointManager.DATA_URI);
|
||||
this.onDidChangeDataBreakpointsEmitter.fire({ uri: BreakpointManager.DATA_URI, added: [], removed, changed: [] });
|
||||
}
|
||||
|
||||
removeBreakpoints(): void {
|
||||
this.cleanAllMarkers();
|
||||
this.setFunctionBreakpoints([]);
|
||||
this.setInstructionBreakpoints([]);
|
||||
this.setDataBreakpoints([]);
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
const data = await this.storage.getData<BreakpointManager.Data>('breakpoints', {
|
||||
breakpointsEnabled: true,
|
||||
breakpoints: {}
|
||||
});
|
||||
this._breakpointsEnabled = data.breakpointsEnabled;
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const uri in data.breakpoints) {
|
||||
this.setBreakpoints(new URI(uri), data.breakpoints[uri]);
|
||||
}
|
||||
if (data.functionBreakpoints) {
|
||||
this.setFunctionBreakpoints(data.functionBreakpoints);
|
||||
}
|
||||
if (data.exceptionBreakpoints) {
|
||||
this.setExceptionBreakpoints(data.exceptionBreakpoints);
|
||||
}
|
||||
if (data.instructionBreakpoints) {
|
||||
this.setInstructionBreakpoints(data.instructionBreakpoints);
|
||||
}
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const data: BreakpointManager.Data = {
|
||||
breakpointsEnabled: this._breakpointsEnabled,
|
||||
breakpoints: {}
|
||||
};
|
||||
const uris = this.getUris();
|
||||
for (const uri of uris) {
|
||||
data.breakpoints[uri] = this.findMarkers({ uri: new URI(uri) }).map(marker => marker.data);
|
||||
}
|
||||
if (this.functionBreakpoints.length) {
|
||||
data.functionBreakpoints = this.functionBreakpoints;
|
||||
}
|
||||
if (this.exceptionBreakpoints.size) {
|
||||
data.exceptionBreakpoints = [...this.exceptionBreakpoints.values()];
|
||||
}
|
||||
if (this.instructionBreakpoints.length) {
|
||||
data.instructionBreakpoints = this.instructionBreakpoints;
|
||||
}
|
||||
const dataBreakpointsToStore = this.dataBreakpoints.filter(candidate => candidate.info.canPersist);
|
||||
if (dataBreakpointsToStore.length) {
|
||||
data.dataBreakpoints = dataBreakpointsToStore;
|
||||
}
|
||||
|
||||
this.storage.setData('breakpoints', data);
|
||||
}
|
||||
|
||||
}
|
||||
export namespace BreakpointManager {
|
||||
export interface Data {
|
||||
breakpointsEnabled: boolean;
|
||||
breakpoints: {
|
||||
[uri: string]: SourceBreakpoint[];
|
||||
}
|
||||
exceptionBreakpoints?: ExceptionBreakpoint[];
|
||||
functionBreakpoints?: FunctionBreakpoint[];
|
||||
instructionBreakpoints?: InstructionBreakpoint[];
|
||||
dataBreakpoints?: DataBreakpoint[];
|
||||
}
|
||||
}
|
||||
|
||||
export function diff<T>(prevs: T[], nexts: T[], toKey: (member: T) => string): { added: T[], removed: T[], changed: T[] } {
|
||||
const old = new Map(prevs.map(item => [toKey(item), item]));
|
||||
const current = new Map(nexts.map(item => [toKey(item), item]));
|
||||
const added = [];
|
||||
const changed = [];
|
||||
for (const [id, next] of current.entries()) {
|
||||
const prev = old.get(id);
|
||||
if (prev) {
|
||||
changed.push(prev);
|
||||
} else {
|
||||
added.push(next);
|
||||
}
|
||||
old.delete(id);
|
||||
}
|
||||
const removed = Array.from(old.values());
|
||||
return { added, removed, changed };
|
||||
}
|
||||
142
packages/debug/src/browser/breakpoint/breakpoint-marker.ts
Normal file
142
packages/debug/src/browser/breakpoint/breakpoint-marker.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { UUID } from '@theia/core/shared/@lumino/coreutils';
|
||||
import { Marker } from '@theia/markers/lib/common/marker';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol/lib/debugProtocol';
|
||||
import { isObject, isString, URI } from '@theia/core/lib/common';
|
||||
|
||||
export const BREAKPOINT_KIND = 'breakpoint';
|
||||
|
||||
export interface BaseBreakpoint {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface SourceBreakpoint extends BaseBreakpoint {
|
||||
uri: string;
|
||||
raw: DebugProtocol.SourceBreakpoint;
|
||||
}
|
||||
export namespace SourceBreakpoint {
|
||||
export function create(uri: URI, data: DebugProtocol.SourceBreakpoint, origin?: SourceBreakpoint): SourceBreakpoint {
|
||||
return {
|
||||
id: origin ? origin.id : UUID.uuid4(),
|
||||
uri: uri.toString(),
|
||||
enabled: origin ? origin.enabled : true,
|
||||
raw: {
|
||||
...(origin && origin.raw),
|
||||
...data
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface BreakpointMarker extends Marker<SourceBreakpoint> {
|
||||
kind: 'breakpoint'
|
||||
}
|
||||
export namespace BreakpointMarker {
|
||||
export function is(node: Marker<object>): node is BreakpointMarker {
|
||||
return 'kind' in node && node.kind === BREAKPOINT_KIND;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ExceptionBreakpoint {
|
||||
enabled: boolean;
|
||||
condition?: string;
|
||||
raw: DebugProtocol.ExceptionBreakpointsFilter;
|
||||
}
|
||||
export namespace ExceptionBreakpoint {
|
||||
export function create(data: DebugProtocol.ExceptionBreakpointsFilter, origin?: ExceptionBreakpoint): ExceptionBreakpoint {
|
||||
return {
|
||||
enabled: origin ? origin.enabled : false,
|
||||
condition: origin ? origin.condition : undefined,
|
||||
raw: {
|
||||
...(origin && origin.raw),
|
||||
...data
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface FunctionBreakpoint extends BaseBreakpoint {
|
||||
raw: DebugProtocol.FunctionBreakpoint;
|
||||
}
|
||||
export namespace FunctionBreakpoint {
|
||||
export function create(data: DebugProtocol.FunctionBreakpoint, origin?: FunctionBreakpoint): FunctionBreakpoint {
|
||||
return {
|
||||
id: origin ? origin.id : UUID.uuid4(),
|
||||
enabled: origin ? origin.enabled : true,
|
||||
raw: {
|
||||
...(origin && origin.raw),
|
||||
...data
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface InstructionBreakpoint extends BaseBreakpoint, DebugProtocol.InstructionBreakpoint { }
|
||||
|
||||
export namespace InstructionBreakpoint {
|
||||
export function create(raw: DebugProtocol.InstructionBreakpoint, existing?: InstructionBreakpoint): InstructionBreakpoint {
|
||||
return {
|
||||
...raw,
|
||||
id: existing?.id ?? UUID.uuid4(),
|
||||
enabled: existing?.enabled ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
export function is(arg: BaseBreakpoint): arg is InstructionBreakpoint {
|
||||
return isObject<InstructionBreakpoint>(arg) && isString(arg.instructionReference);
|
||||
}
|
||||
}
|
||||
|
||||
export type DataBreakpointInfo = DebugProtocol.DataBreakpointInfoResponse['body'];
|
||||
|
||||
export interface DataBreakpointAddressSource {
|
||||
type: DataBreakpointSourceType.Address;
|
||||
address: string;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
export interface DataBreakpointVariableSource {
|
||||
type: DataBreakpointSourceType.Variable;
|
||||
variable: string;
|
||||
}
|
||||
|
||||
export const enum DataBreakpointSourceType {
|
||||
Variable,
|
||||
Address,
|
||||
}
|
||||
|
||||
export type DataBreakpointSource = | DataBreakpointAddressSource | DataBreakpointVariableSource;
|
||||
|
||||
export interface DataBreakpoint extends BaseBreakpoint {
|
||||
raw: DebugProtocol.DataBreakpoint;
|
||||
info: DataBreakpointInfo;
|
||||
source: DataBreakpointSource;
|
||||
}
|
||||
|
||||
export namespace DataBreakpoint {
|
||||
export function create(raw: DebugProtocol.DataBreakpoint, info: DataBreakpointInfo, source: DataBreakpointSource, ref?: DataBreakpoint): DataBreakpoint {
|
||||
return {
|
||||
raw,
|
||||
info,
|
||||
id: ref?.id ?? UUID.uuid4(),
|
||||
enabled: ref?.enabled ?? true,
|
||||
source
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 EclipseSource GmbH 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { nls, CommandHandler, DisposableCollection, MessageService, QuickInputService, Disposable } from '@theia/core';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { DebugViewModel } from '../view/debug-view-model';
|
||||
import { TreeElementNode } from '@theia/core/lib/browser/source-tree';
|
||||
import { DebugDataBreakpoint } from '../model/debug-data-breakpoint';
|
||||
import { DataBreakpoint, DataBreakpointSource, DataBreakpointSourceType } from './breakpoint-marker';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol';
|
||||
import { BreakpointManager } from './breakpoint-manager';
|
||||
import { TreeNode, Widget } from '@theia/core/lib/browser';
|
||||
import { DebugBreakpointsWidget } from '../view/debug-breakpoints-widget';
|
||||
|
||||
// Adapted from https://github.com/microsoft/vscode/blob/9c883243a89e7ec3b730d3746fbb1e836d5e4f52/src/vs/workbench/contrib/debug/browser/breakpointsView.ts#L1506-L1625
|
||||
|
||||
@injectable()
|
||||
export class AddOrEditDataBreakpointAddress implements CommandHandler {
|
||||
@inject(DebugViewModel)
|
||||
protected readonly viewModel: DebugViewModel;
|
||||
@inject(QuickInputService)
|
||||
protected readonly quickInputService: QuickInputService;
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
@inject(BreakpointManager)
|
||||
protected readonly breakpointManager: BreakpointManager;
|
||||
|
||||
isEnabled(node?: TreeElementNode): boolean {
|
||||
return !!this.viewModel.currentSession?.capabilities.supportsDataBreakpoints
|
||||
&& this.viewModel.currentSession?.capabilities.supportsDataBreakpointBytes !== false
|
||||
&& this.isAddressBreakpointOrDebugWidget(node);
|
||||
}
|
||||
|
||||
isVisible(node?: TreeElementNode): boolean {
|
||||
return this.isEnabled(node);
|
||||
}
|
||||
|
||||
protected isAddressBreakpointOrDebugWidget(candidate?: unknown): boolean {
|
||||
return !candidate ? true // Probably command palette
|
||||
: TreeNode.is(candidate) && TreeElementNode.is(candidate)
|
||||
? candidate.element instanceof DebugDataBreakpoint && candidate.element.origin.source.type === DataBreakpointSourceType.Address
|
||||
: candidate instanceof Widget
|
||||
? candidate instanceof DebugBreakpointsWidget
|
||||
: false;
|
||||
}
|
||||
|
||||
async execute(node?: TreeElementNode): Promise<void> {
|
||||
const existingBreakpoint = TreeElementNode.is(node) && node.element instanceof DebugDataBreakpoint ? node.element : undefined;
|
||||
const session = this.viewModel.currentSession;
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
let defaultValue = undefined;
|
||||
if (existingBreakpoint?.origin.source.type === DataBreakpointSourceType.Address) {
|
||||
defaultValue = `${existingBreakpoint.origin.source.address} + ${existingBreakpoint.origin.source.bytes}`;
|
||||
}
|
||||
|
||||
const quickInput = this.quickInputService;
|
||||
const range = await this.getRange(defaultValue);
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = await session.sendRequest('dataBreakpointInfo', { asAddress: true, name: range.address, bytes: range.bytes })
|
||||
.then(({ body }) => body)
|
||||
.catch(e => { this.messageService.error(nls.localizeByDefault('Failed to set data breakpoint at {0}: {1}', range.address, e.message)); });
|
||||
if (!info?.dataId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let accessType: DebugProtocol.DataBreakpointAccessType = 'write';
|
||||
if (info.accessTypes && info.accessTypes?.length > 1) {
|
||||
const accessTypes = info.accessTypes.map(type => ({ label: type }));
|
||||
const selectedAccessType = await quickInput.pick(accessTypes, { placeHolder: nls.localizeByDefault('Select the access type to monitor') });
|
||||
if (!selectedAccessType) {
|
||||
return;
|
||||
}
|
||||
|
||||
accessType = selectedAccessType.label;
|
||||
}
|
||||
|
||||
const src: DataBreakpointSource = { type: DataBreakpointSourceType.Address, ...range };
|
||||
if (existingBreakpoint) {
|
||||
this.breakpointManager.removeDataBreakpoint(existingBreakpoint.id);
|
||||
}
|
||||
|
||||
this.breakpointManager.addDataBreakpoint(DataBreakpoint.create({ dataId: info.dataId, accessType }, { ...info, canPersist: true }, src));
|
||||
}
|
||||
|
||||
private getRange(defaultValue?: string): Promise<{ address: string, bytes: number } | undefined> {
|
||||
return new Promise(resolve => {
|
||||
const disposables = new DisposableCollection();
|
||||
const addDisposable = <T extends Disposable>(disposable: T): T => {
|
||||
disposables.push(disposable);
|
||||
return disposable;
|
||||
};
|
||||
const input = addDisposable(this.quickInputService.createInputBox());
|
||||
input.prompt = nls.localizeByDefault('Enter a memory range in which to break');
|
||||
input.placeholder = nls.localizeByDefault('Absolute range (0x1234 - 0x1300) or range of bytes after an address (0x1234 + 0xff)');
|
||||
if (defaultValue) {
|
||||
input.value = defaultValue;
|
||||
input.valueSelection = [0, defaultValue.length];
|
||||
}
|
||||
addDisposable(input.onDidChangeValue(e => {
|
||||
const err = this.parseAddress(e, false);
|
||||
input.validationMessage = err?.error;
|
||||
}));
|
||||
addDisposable(input.onDidAccept(() => {
|
||||
const r = this.parseAddress(input.value ?? '', true);
|
||||
if ('error' in r) {
|
||||
input.validationMessage = r.error;
|
||||
} else {
|
||||
resolve(r);
|
||||
}
|
||||
input.dispose();
|
||||
}));
|
||||
addDisposable(input.onDidHide(() => {
|
||||
resolve(undefined);
|
||||
disposables.dispose();
|
||||
}));
|
||||
input.ignoreFocusOut = true;
|
||||
input.show();
|
||||
});
|
||||
}
|
||||
|
||||
private parseAddress(range: string, isFinal: false): { error: string } | undefined;
|
||||
private parseAddress(range: string, isFinal: true): { error: string } | { address: string; bytes: number };
|
||||
private parseAddress(range: string, isFinal: boolean): { error: string } | { address: string; bytes: number } | undefined {
|
||||
const parts = /^(\S+)\s*(?:([+-])\s*(\S+))?/.exec(range);
|
||||
if (!parts) {
|
||||
return { error: nls.localizeByDefault('Address should be a range of numbers the form "[Start] - [End]" or "[Start] + [Bytes]"') };
|
||||
}
|
||||
|
||||
const isNum = (e: string) => isFinal ? /^0x[0-9a-f]*|[0-9]*$/i.test(e) : /^0x[0-9a-f]+|[0-9]+$/i.test(e);
|
||||
const [, startStr, sign = '+', endStr = '1'] = parts;
|
||||
|
||||
for (const n of [startStr, endStr]) {
|
||||
if (!isNum(n)) {
|
||||
return { error: nls.localizeByDefault('Number must be a decimal integer or hex value starting with \"0x\", got {0}', n) };
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFinal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = BigInt(startStr);
|
||||
const end = BigInt(endStr);
|
||||
const address = `0x${start.toString(16)}`;
|
||||
if (sign === '-') {
|
||||
return { address, bytes: Number(start - end) };
|
||||
}
|
||||
|
||||
return { address, bytes: Number(end) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import '../../../src/browser/style/debug.css';
|
||||
|
||||
import { ConsoleSessionManager } from '@theia/console/lib/browser/console-session-manager';
|
||||
import { ConsoleOptions, ConsoleWidget } from '@theia/console/lib/browser/console-widget';
|
||||
import { AbstractViewContribution, bindViewContribution, codicon, HoverService, Widget, WidgetFactory } from '@theia/core/lib/browser';
|
||||
import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { Command, CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { Severity } from '@theia/core/lib/common/severity';
|
||||
import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component';
|
||||
import { DebugSession } from '../debug-session';
|
||||
import { DebugSessionManager, DidChangeActiveDebugSession } from '../debug-session-manager';
|
||||
import { DebugConsoleSession, DebugConsoleSessionFactory } from './debug-console-session';
|
||||
import { Disposable, DisposableCollection, Emitter, Event, InMemoryResources } from '@theia/core';
|
||||
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
|
||||
import debounce = require('@theia/core/shared/lodash.debounce');
|
||||
|
||||
export type InDebugReplContextKey = ContextKey<boolean>;
|
||||
export const InDebugReplContextKey = Symbol('inDebugReplContextKey');
|
||||
|
||||
export namespace DebugConsoleCommands {
|
||||
|
||||
export const DEBUG_CATEGORY = 'Debug';
|
||||
|
||||
export const CLEAR = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.console.clear',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Clear Console',
|
||||
iconClass: codicon('clear-all')
|
||||
});
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DebugConsoleContribution extends AbstractViewContribution<ConsoleWidget> implements TabBarToolbarContribution, Disposable {
|
||||
|
||||
@inject(ConsoleSessionManager)
|
||||
protected consoleSessionManager: ConsoleSessionManager;
|
||||
|
||||
@inject(DebugConsoleSessionFactory)
|
||||
protected debugConsoleSessionFactory: DebugConsoleSessionFactory;
|
||||
|
||||
@inject(DebugSessionManager)
|
||||
protected debugSessionManager: DebugSessionManager;
|
||||
|
||||
@inject(InMemoryResources)
|
||||
protected readonly resources: InMemoryResources;
|
||||
|
||||
@inject(HoverService)
|
||||
protected readonly hoverService: HoverService;
|
||||
|
||||
protected readonly DEBUG_CONSOLE_SEVERITY_ID = 'debugConsoleSeverity';
|
||||
|
||||
protected filterInputRef: HTMLInputElement | undefined;
|
||||
protected currentFilterValue = '';
|
||||
protected readonly filterChangedEmitter = new Emitter<void>();
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: DebugConsoleContribution.options.id,
|
||||
widgetName: DebugConsoleContribution.options.title!.label!,
|
||||
defaultWidgetOptions: {
|
||||
area: 'bottom'
|
||||
},
|
||||
toggleCommandId: 'debug:console:toggle',
|
||||
toggleKeybinding: 'ctrlcmd+shift+y'
|
||||
});
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.resources.add(DebugConsoleSession.uri, '');
|
||||
this.toDispose.pushAll([
|
||||
this.debugSessionManager.onDidCreateDebugSession(session => {
|
||||
const consoleParent = session.findConsoleParent();
|
||||
if (consoleParent) {
|
||||
const parentConsoleSession = this.consoleSessionManager.get(consoleParent.id);
|
||||
if (parentConsoleSession instanceof DebugConsoleSession) {
|
||||
session.on('output', event => parentConsoleSession.logOutput(parentConsoleSession.debugSession, event));
|
||||
}
|
||||
} else {
|
||||
const consoleSession = this.debugConsoleSessionFactory(session);
|
||||
this.consoleSessionManager.add(consoleSession);
|
||||
session.on('output', event => consoleSession.logOutput(session, event));
|
||||
}
|
||||
}),
|
||||
this.debugSessionManager.onDidChangeActiveDebugSession(event => this.handleActiveDebugSessionChanged(event)),
|
||||
this.debugSessionManager.onDidDestroyDebugSession(session => {
|
||||
const consoleSession = this.consoleSessionManager.get(session.id);
|
||||
if (consoleSession instanceof DebugConsoleSession) {
|
||||
consoleSession.markTerminated();
|
||||
}
|
||||
}),
|
||||
this.consoleSessionManager.onDidChangeSelectedSession(() => {
|
||||
const session = this.consoleSessionManager.selectedSession;
|
||||
if (session && this.filterInputRef) {
|
||||
const filterValue = session.filterText || '';
|
||||
this.filterInputRef.value = filterValue;
|
||||
this.currentFilterValue = filterValue;
|
||||
this.filterChangedEmitter.fire();
|
||||
}
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
protected handleActiveDebugSessionChanged(event: DidChangeActiveDebugSession): void {
|
||||
if (!event.current) {
|
||||
return;
|
||||
} else {
|
||||
const topSession = event.current.findConsoleParent() || event.current;
|
||||
const consoleSession = topSession ? this.consoleSessionManager.get(topSession.id) : undefined;
|
||||
this.consoleSessionManager.selectedSession = consoleSession;
|
||||
const consoleSelector = document.getElementById('debugConsoleSelector');
|
||||
if (consoleSession && consoleSelector instanceof HTMLSelectElement) {
|
||||
consoleSelector.value = consoleSession.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override registerCommands(commands: CommandRegistry): void {
|
||||
super.registerCommands(commands);
|
||||
commands.registerCommand(DebugConsoleCommands.CLEAR, {
|
||||
isEnabled: widget => this.withWidget(widget, () => true),
|
||||
isVisible: widget => this.withWidget(widget, () => true),
|
||||
execute: widget => this.withWidget(widget, () => {
|
||||
this.clearConsole();
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
|
||||
toolbarRegistry.registerItem({
|
||||
id: 'debug-console-severity',
|
||||
render: widget => this.renderSeveritySelector(widget),
|
||||
isVisible: widget => this.withWidget(widget, () => true),
|
||||
onDidChange: this.consoleSessionManager.onDidChangeSeverity,
|
||||
priority: -3,
|
||||
});
|
||||
|
||||
toolbarRegistry.registerItem({
|
||||
id: 'debug-console-filter',
|
||||
render: () => this.renderFilterInput(),
|
||||
isVisible: widget => this.withWidget(widget, () => true),
|
||||
onDidChange: this.filterChangedEmitter.event,
|
||||
priority: -2,
|
||||
});
|
||||
|
||||
toolbarRegistry.registerItem({
|
||||
id: 'debug-console-session-selector',
|
||||
render: widget => this.renderDebugConsoleSelector(widget),
|
||||
isVisible: widget => this.withWidget(widget, () => this.consoleSessionManager.all.length >= 1),
|
||||
onDidChange: Event.any(
|
||||
this.consoleSessionManager.onDidAddSession,
|
||||
this.consoleSessionManager.onDidDeleteSession,
|
||||
this.consoleSessionManager.onDidChangeSelectedSession
|
||||
) as Event<void>,
|
||||
priority: -1,
|
||||
});
|
||||
|
||||
toolbarRegistry.registerItem({
|
||||
id: DebugConsoleCommands.CLEAR.id,
|
||||
command: DebugConsoleCommands.CLEAR.id,
|
||||
tooltip: DebugConsoleCommands.CLEAR.label,
|
||||
priority: 0,
|
||||
});
|
||||
}
|
||||
|
||||
static options: ConsoleOptions = {
|
||||
id: 'debug-console',
|
||||
title: {
|
||||
label: nls.localizeByDefault('Debug Console'),
|
||||
iconClass: codicon('debug-console')
|
||||
},
|
||||
input: {
|
||||
uri: DebugConsoleSession.uri,
|
||||
options: {
|
||||
autoSizing: true,
|
||||
minHeight: 1,
|
||||
maxHeight: 10
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static async create(parent: interfaces.Container): Promise<ConsoleWidget> {
|
||||
const inputFocusContextKey = parent.get<InDebugReplContextKey>(InDebugReplContextKey);
|
||||
const child = ConsoleWidget.createContainer(parent, {
|
||||
...DebugConsoleContribution.options,
|
||||
inputFocusContextKey
|
||||
});
|
||||
const widget = child.get(ConsoleWidget);
|
||||
await widget.ready;
|
||||
return widget;
|
||||
}
|
||||
|
||||
static bindContribution(bind: interfaces.Bind): void {
|
||||
bind(InDebugReplContextKey).toDynamicValue(({ container }) =>
|
||||
container.get<ContextKeyService>(ContextKeyService).createKey('inDebugRepl', false)
|
||||
).inSingletonScope();
|
||||
bind(DebugConsoleSession).toSelf().inRequestScope();
|
||||
bind(DebugConsoleSessionFactory).toFactory(context => (session: DebugSession) => {
|
||||
const consoleSession = context.container.get(DebugConsoleSession);
|
||||
consoleSession.debugSession = session;
|
||||
return consoleSession;
|
||||
});
|
||||
bind(ConsoleSessionManager).toSelf().inSingletonScope();
|
||||
bindViewContribution(bind, DebugConsoleContribution);
|
||||
bind(TabBarToolbarContribution).toService(DebugConsoleContribution);
|
||||
bind(WidgetFactory).toDynamicValue(({ container }) => ({
|
||||
id: DebugConsoleContribution.options.id,
|
||||
createWidget: () => DebugConsoleContribution.create(container)
|
||||
}));
|
||||
}
|
||||
|
||||
protected renderSeveritySelector(widget: Widget | undefined): React.ReactNode {
|
||||
const severityElements: SelectOption[] = Severity.toArray().map(e => ({
|
||||
value: e,
|
||||
label: Severity.toLocaleString(e)
|
||||
}));
|
||||
|
||||
return <div onMouseEnter={this.handleSeverityMouseEnter}>
|
||||
<SelectComponent
|
||||
id={this.DEBUG_CONSOLE_SEVERITY_ID}
|
||||
key="debugConsoleSeverity"
|
||||
options={severityElements}
|
||||
defaultValue={this.consoleSessionManager.severity || Severity.Ignore}
|
||||
onChange={this.changeSeverity}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected handleSeverityMouseEnter = (e: React.MouseEvent<HTMLDivElement>): void => {
|
||||
const tooltipContent = new MarkdownStringImpl();
|
||||
tooltipContent.appendMarkdown(nls.localize(
|
||||
'theia/debug/consoleSeverityTooltip',
|
||||
'Filter console output by severity level. Only messages with the selected severity will be shown.'
|
||||
));
|
||||
this.hoverService.requestHover({
|
||||
content: tooltipContent,
|
||||
target: e.currentTarget,
|
||||
position: 'bottom'
|
||||
});
|
||||
};
|
||||
|
||||
protected renderDebugConsoleSelector(widget: Widget | undefined): React.ReactNode {
|
||||
const availableConsoles: SelectOption[] = [];
|
||||
const sortedSessions = this.consoleSessionManager.all
|
||||
.filter((e): e is DebugConsoleSession => e instanceof DebugConsoleSession)
|
||||
.sort((a, b) => {
|
||||
if (a.terminated !== b.terminated) {
|
||||
return a.terminated ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
sortedSessions.forEach(session => {
|
||||
let label = session.debugSession.label;
|
||||
if (session.terminated) {
|
||||
label = `${label} (${nls.localizeByDefault('Stopped')})`;
|
||||
}
|
||||
availableConsoles.push({
|
||||
value: session.id,
|
||||
label
|
||||
});
|
||||
});
|
||||
|
||||
const selectedId = this.consoleSessionManager.selectedSession?.id;
|
||||
|
||||
return <div onMouseEnter={this.handleSessionSelectorMouseEnter}><SelectComponent
|
||||
key="debugConsoleSelector"
|
||||
options={availableConsoles}
|
||||
defaultValue={selectedId}
|
||||
onChange={this.changeDebugConsole} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected handleSessionSelectorMouseEnter = (e: React.MouseEvent<HTMLDivElement>): void => {
|
||||
const tooltipContent = new MarkdownStringImpl();
|
||||
tooltipContent.appendMarkdown(nls.localize(
|
||||
'theia/debug/consoleSessionSelectorTooltip',
|
||||
'Switch between debug sessions. Each debug session has its own console output.'
|
||||
));
|
||||
this.hoverService.requestHover({
|
||||
content: tooltipContent,
|
||||
target: e.currentTarget,
|
||||
position: 'bottom'
|
||||
});
|
||||
};
|
||||
|
||||
protected renderFilterInput(): React.ReactNode {
|
||||
return (
|
||||
<div className="item enabled debug-console-filter-container">
|
||||
<input
|
||||
type="text"
|
||||
className="theia-input"
|
||||
placeholder={nls.localize('theia/debug/consoleFilter', 'Filter (e.g. text, !exclude)')}
|
||||
aria-label={nls.localize('theia/debug/consoleFilterAriaLabel', 'Filter debug console output')}
|
||||
ref={ref => { this.filterInputRef = ref ?? undefined; }}
|
||||
onChange={this.handleFilterInputChange}
|
||||
onMouseEnter={this.handleFilterMouseEnter}
|
||||
/>
|
||||
{this.currentFilterValue && <span
|
||||
className="debug-console-filter-btn codicon codicon-close action-label"
|
||||
role="button"
|
||||
aria-label={nls.localizeByDefault('Clear')}
|
||||
onClick={this.handleFilterClear}
|
||||
title={nls.localizeByDefault('Clear')}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
protected handleFilterInputChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const value = e.target.value;
|
||||
this.currentFilterValue = value;
|
||||
this.filterChangedEmitter.fire();
|
||||
this.applyFilterDebounced(value);
|
||||
};
|
||||
|
||||
protected applyFilterDebounced = debounce((value: string) => {
|
||||
const session = this.consoleSessionManager.selectedSession;
|
||||
if (session) {
|
||||
session.filterText = value;
|
||||
}
|
||||
}, 150);
|
||||
|
||||
protected handleFilterClear = (): void => {
|
||||
if (this.filterInputRef) {
|
||||
this.filterInputRef.value = '';
|
||||
}
|
||||
this.currentFilterValue = '';
|
||||
const session = this.consoleSessionManager.selectedSession;
|
||||
if (session) {
|
||||
session.filterText = '';
|
||||
}
|
||||
this.filterChangedEmitter.fire();
|
||||
};
|
||||
|
||||
protected handleFilterMouseEnter = (e: React.MouseEvent<HTMLInputElement>): void => {
|
||||
const tooltipContent = new MarkdownStringImpl();
|
||||
tooltipContent.appendMarkdown(nls.localize(
|
||||
'theia/debug/consoleFilterTooltip',
|
||||
'Filter console output by text. Separate multiple terms with commas. Prefix with `!` to exclude a term.\n\n' +
|
||||
'Examples:\n\n' +
|
||||
'- `text` - show lines containing "text"\n' +
|
||||
'- `text, other` - show lines containing "text" or "other"\n' +
|
||||
'- `!text` - hide lines containing "text"\n' +
|
||||
'- `text, !other` - show "text" but hide "other"'
|
||||
));
|
||||
this.hoverService.requestHover({
|
||||
content: tooltipContent,
|
||||
target: e.currentTarget,
|
||||
position: 'bottom'
|
||||
});
|
||||
};
|
||||
|
||||
protected changeDebugConsole = (option: SelectOption) => {
|
||||
const id = option.value!;
|
||||
const session = this.consoleSessionManager.get(id);
|
||||
this.consoleSessionManager.selectedSession = session;
|
||||
};
|
||||
|
||||
protected changeSeverity = (option: SelectOption) => {
|
||||
this.consoleSessionManager.severity = Severity.fromValue(option.value);
|
||||
};
|
||||
|
||||
protected withWidget<T>(widget: Widget | undefined = this.tryGetWidget(), fn: (widget: ConsoleWidget) => T): T | false {
|
||||
if (widget instanceof ConsoleWidget && widget.id === DebugConsoleContribution.options.id) {
|
||||
return fn(widget);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the console widget.
|
||||
*/
|
||||
protected async clearConsole(): Promise<void> {
|
||||
const widget = await this.widget;
|
||||
widget.clear();
|
||||
const selectedSession = this.consoleSessionManager.selectedSession;
|
||||
if (selectedSession instanceof DebugConsoleSession && selectedSession.terminated) {
|
||||
this.consoleSessionManager.delete(selectedSession.id);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
481
packages/debug/src/browser/console/debug-console-items.tsx
Normal file
481
packages/debug/src/browser/console/debug-console-items.tsx
Normal file
@@ -0,0 +1,481 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol/lib/debugProtocol';
|
||||
import { codicon, SingleTextInputDialog } from '@theia/core/lib/browser';
|
||||
import { ConsoleItem, CompositeConsoleItem } from '@theia/console/lib/browser/console-session';
|
||||
import { DebugSession, formatMessage } from '../debug-session';
|
||||
import { Severity } from '@theia/core/lib/common/severity';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { generateUuid, nls } from '@theia/core';
|
||||
|
||||
export type DebugSessionProvider = () => DebugSession | undefined;
|
||||
|
||||
export class ExpressionContainer implements CompositeConsoleItem {
|
||||
|
||||
private static readonly BASE_CHUNK_SIZE = 100;
|
||||
|
||||
protected readonly sessionProvider: DebugSessionProvider;
|
||||
protected get session(): DebugSession | undefined {
|
||||
return this.sessionProvider();
|
||||
}
|
||||
|
||||
readonly id: string | number;
|
||||
protected variablesReference: number;
|
||||
protected namedVariables: number | undefined;
|
||||
protected indexedVariables: number | undefined;
|
||||
protected presentationHint: DebugProtocol.VariablePresentationHint | undefined;
|
||||
protected readonly startOfVariables: number;
|
||||
|
||||
constructor(options: ExpressionContainer.Options) {
|
||||
this.sessionProvider = options.session;
|
||||
this.id = options.id ?? generateUuid();
|
||||
this.variablesReference = options.variablesReference || 0;
|
||||
this.namedVariables = options.namedVariables;
|
||||
this.indexedVariables = options.indexedVariables;
|
||||
this.startOfVariables = options.startOfVariables || 0;
|
||||
this.presentationHint = options.presentationHint;
|
||||
if (this.lazy) {
|
||||
(this as CompositeConsoleItem).expandByDefault = () => !this.lazy && !this.session?.autoExpandLazyVariables;
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get reference(): number | undefined {
|
||||
return this.variablesReference;
|
||||
}
|
||||
|
||||
get hasElements(): boolean {
|
||||
return !!this.variablesReference && !this.lazy;
|
||||
}
|
||||
|
||||
get lazy(): boolean {
|
||||
return !!this.presentationHint?.lazy;
|
||||
}
|
||||
|
||||
async resolveLazy(): Promise<void> {
|
||||
const { session, variablesReference, lazy } = this;
|
||||
if (!session || !variablesReference || !lazy) {
|
||||
return;
|
||||
}
|
||||
const response = await session.sendRequest('variables', { variablesReference });
|
||||
const { variables } = response.body;
|
||||
if (variables.length !== 1) {
|
||||
return;
|
||||
}
|
||||
this.handleResolvedLazy(variables[0]);
|
||||
}
|
||||
|
||||
protected handleResolvedLazy(resolved: DebugProtocol.Variable): void {
|
||||
this.variablesReference = resolved.variablesReference;
|
||||
this.namedVariables = resolved.namedVariables;
|
||||
this.indexedVariables = resolved.indexedVariables;
|
||||
this.presentationHint = resolved.presentationHint;
|
||||
}
|
||||
|
||||
protected elements: Promise<ExpressionContainer[]> | undefined;
|
||||
async getElements(): Promise<IterableIterator<ExpressionContainer>> {
|
||||
if (!this.hasElements || !this.session) {
|
||||
return [][Symbol.iterator]();
|
||||
}
|
||||
if (!this.elements) {
|
||||
this.elements = this.doResolve();
|
||||
}
|
||||
return (await this.elements)[Symbol.iterator]();
|
||||
}
|
||||
protected async doResolve(): Promise<ExpressionContainer[]> {
|
||||
const result: ExpressionContainer[] = [];
|
||||
if (this.namedVariables) {
|
||||
await this.fetch(result, 'named');
|
||||
}
|
||||
if (this.indexedVariables) {
|
||||
let chunkSize = ExpressionContainer.BASE_CHUNK_SIZE;
|
||||
while (this.indexedVariables > chunkSize * ExpressionContainer.BASE_CHUNK_SIZE) {
|
||||
chunkSize *= ExpressionContainer.BASE_CHUNK_SIZE;
|
||||
}
|
||||
if (this.indexedVariables > chunkSize) {
|
||||
const numberOfChunks = Math.ceil(this.indexedVariables / chunkSize);
|
||||
for (let i = 0; i < numberOfChunks; i++) {
|
||||
const start = this.startOfVariables + i * chunkSize;
|
||||
const count = Math.min(chunkSize, this.indexedVariables - i * chunkSize);
|
||||
const { variablesReference } = this;
|
||||
const name = `[${start}..${start + count - 1}]`;
|
||||
result.push(new DebugVirtualVariable({
|
||||
session: this.sessionProvider,
|
||||
id: `${this.id}:${name}`,
|
||||
variablesReference,
|
||||
namedVariables: 0,
|
||||
indexedVariables: count,
|
||||
startOfVariables: start,
|
||||
name
|
||||
}));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
await this.fetch(result, 'indexed', this.startOfVariables, this.indexedVariables);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected fetch(result: ConsoleItem[], filter: 'named'): Promise<void>;
|
||||
protected fetch(result: ConsoleItem[], filter: 'indexed', start: number, count?: number): Promise<void>;
|
||||
protected async fetch(result: ConsoleItem[], filter: 'indexed' | 'named', start?: number, count?: number): Promise<void> {
|
||||
try {
|
||||
const { session } = this;
|
||||
if (session) {
|
||||
const { variablesReference } = this;
|
||||
const response = await session.sendRequest('variables', { variablesReference, filter, start, count });
|
||||
const { variables } = response.body;
|
||||
const names = new Set<string>();
|
||||
const debugVariables: DebugVariable[] = [];
|
||||
for (const variable of variables) {
|
||||
if (!names.has(variable.name)) {
|
||||
const v = new DebugVariable(this.sessionProvider, variable, this);
|
||||
debugVariables.push(v);
|
||||
result.push(v);
|
||||
names.add(variable.name);
|
||||
}
|
||||
}
|
||||
if (session.autoExpandLazyVariables) {
|
||||
await Promise.all(debugVariables.map(v => v.lazy && v.resolveLazy()));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
result.push({
|
||||
severity: Severity.Error,
|
||||
visible: !!e.message,
|
||||
render: () => e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
export namespace ExpressionContainer {
|
||||
export interface Options {
|
||||
session: DebugSessionProvider,
|
||||
id?: string | number,
|
||||
variablesReference?: number
|
||||
namedVariables?: number
|
||||
indexedVariables?: number
|
||||
startOfVariables?: number
|
||||
presentationHint?: DebugProtocol.VariablePresentationHint
|
||||
}
|
||||
}
|
||||
|
||||
export class DebugVariable extends ExpressionContainer {
|
||||
|
||||
static booleanRegex = /^true|false$/i;
|
||||
static stringRegex = /^(['"]).*\1$/;
|
||||
|
||||
constructor(
|
||||
session: DebugSessionProvider,
|
||||
protected readonly variable: DebugProtocol.Variable,
|
||||
readonly parent: ExpressionContainer
|
||||
) {
|
||||
super({
|
||||
session,
|
||||
id: `${parent.id}:${variable.name}`,
|
||||
variablesReference: variable.variablesReference,
|
||||
namedVariables: variable.namedVariables,
|
||||
indexedVariables: variable.indexedVariables,
|
||||
presentationHint: variable.presentationHint
|
||||
});
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.variable.name;
|
||||
}
|
||||
get evaluateName(): string | undefined {
|
||||
return this.variable.evaluateName;
|
||||
}
|
||||
protected _type: string | undefined;
|
||||
get type(): string | undefined {
|
||||
return this._type || this.variable.type;
|
||||
}
|
||||
protected _value: string | undefined;
|
||||
get value(): string {
|
||||
return this._value || this.variable.value;
|
||||
}
|
||||
|
||||
get readOnly(): boolean {
|
||||
return this.presentationHint?.attributes?.includes('readOnly') || this.lazy;
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const { type, value, name, lazy } = this;
|
||||
return <div className={this.variableClassName}>
|
||||
<span title={type || name} className='name' ref={this.setNameRef}>{name}{(value || lazy) && ': '}</span>
|
||||
{lazy && <span title={nls.localizeByDefault('Click to expand')} className={codicon('eye') + ' lazy-button'} onClick={this.handleLazyButtonClick} />}
|
||||
<span title={value} className='value' ref={this.setValueRef}>{value}</span>
|
||||
</div>;
|
||||
}
|
||||
|
||||
private readonly handleLazyButtonClick = () => this.resolveLazy();
|
||||
|
||||
protected get variableClassName(): string {
|
||||
const { type, value } = this;
|
||||
const classNames = ['theia-debug-console-variable'];
|
||||
if (type === 'number' || type === 'boolean' || type === 'string') {
|
||||
classNames.push(type);
|
||||
} else if (!isNaN(+value)) {
|
||||
classNames.push('number');
|
||||
} else if (DebugVariable.booleanRegex.test(value)) {
|
||||
classNames.push('boolean');
|
||||
} else if (DebugVariable.stringRegex.test(value)) {
|
||||
classNames.push('string');
|
||||
}
|
||||
return classNames.join(' ');
|
||||
}
|
||||
|
||||
protected override handleResolvedLazy(resolved: DebugProtocol.Variable): void {
|
||||
this._value = resolved.value;
|
||||
this._type = resolved.type || this._type;
|
||||
super.handleResolvedLazy(resolved);
|
||||
this.session?.['onDidResolveLazyVariableEmitter'].fire(this);
|
||||
}
|
||||
|
||||
get supportSetVariable(): boolean {
|
||||
return !!this.session && !!this.session.capabilities.supportsSetVariable;
|
||||
}
|
||||
async setValue(value: string): Promise<void> {
|
||||
if (!this.session || value === this.value) {
|
||||
return;
|
||||
}
|
||||
const { name, parent } = this;
|
||||
const variablesReference = parent['variablesReference'];
|
||||
const response = await this.session.sendRequest('setVariable', { variablesReference, name, value });
|
||||
this._value = response.body.value;
|
||||
this._type = response.body.type;
|
||||
this.variablesReference = response.body.variablesReference || 0;
|
||||
this.namedVariables = response.body.namedVariables;
|
||||
this.indexedVariables = response.body.indexedVariables;
|
||||
this.elements = undefined;
|
||||
this.session['fireDidChange']();
|
||||
}
|
||||
|
||||
get supportCopyValue(): boolean {
|
||||
return !!this.valueRef && document.queryCommandSupported('copy');
|
||||
}
|
||||
copyValue(): void {
|
||||
const selection = document.getSelection();
|
||||
if (this.valueRef && selection) {
|
||||
selection.selectAllChildren(this.valueRef);
|
||||
document.execCommand('copy');
|
||||
}
|
||||
}
|
||||
protected valueRef: HTMLSpanElement | undefined;
|
||||
protected setValueRef = (valueRef: HTMLSpanElement | null) => this.valueRef = valueRef || undefined;
|
||||
|
||||
get supportCopyAsExpression(): boolean {
|
||||
return !!this.nameRef && document.queryCommandSupported('copy');
|
||||
}
|
||||
copyAsExpression(): void {
|
||||
const selection = document.getSelection();
|
||||
if (this.nameRef && selection) {
|
||||
selection.selectAllChildren(this.nameRef);
|
||||
document.execCommand('copy');
|
||||
}
|
||||
}
|
||||
protected nameRef: HTMLSpanElement | undefined;
|
||||
protected setNameRef = (nameRef: HTMLSpanElement | null) => this.nameRef = nameRef || undefined;
|
||||
|
||||
async open(): Promise<void> {
|
||||
if (!this.supportSetVariable || this.readOnly) {
|
||||
return;
|
||||
}
|
||||
const input = new SingleTextInputDialog({
|
||||
title: nls.localize('theia/debug/debugVariableInput', 'Set {0} Value', this.name),
|
||||
initialValue: this.value,
|
||||
placeholder: nls.localizeByDefault('Value'),
|
||||
validate: async (value, mode) => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
if (mode === 'open') {
|
||||
try {
|
||||
await this.setValue(value);
|
||||
} catch (error) {
|
||||
console.error('setValue failed:', error);
|
||||
if (error.body?.error) {
|
||||
const errorMessage: DebugProtocol.Message = error.body.error;
|
||||
if (errorMessage.showUser) {
|
||||
return formatMessage(errorMessage.format, errorMessage.variables);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
await input.open();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class DebugVirtualVariable extends ExpressionContainer {
|
||||
|
||||
constructor(
|
||||
protected readonly options: VirtualVariableItem.Options
|
||||
) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
return this.options.name;
|
||||
}
|
||||
}
|
||||
export namespace VirtualVariableItem {
|
||||
export interface Options extends ExpressionContainer.Options {
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpressionItem extends ExpressionContainer {
|
||||
|
||||
severity?: Severity;
|
||||
static notAvailable = nls.localizeByDefault('not available');
|
||||
|
||||
protected _value = ExpressionItem.notAvailable;
|
||||
get value(): string {
|
||||
return this._value;
|
||||
}
|
||||
protected _type: string | undefined;
|
||||
get type(): string | undefined {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
protected _available = false;
|
||||
get available(): boolean {
|
||||
return this._available;
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected _expression: string,
|
||||
session: DebugSessionProvider,
|
||||
id?: string | number
|
||||
) {
|
||||
super({ session, id });
|
||||
}
|
||||
|
||||
get expression(): string {
|
||||
return this._expression;
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const valueClassNames: string[] = [];
|
||||
if (!this._available) {
|
||||
valueClassNames.push(ConsoleItem.errorClassName);
|
||||
valueClassNames.push('theia-debug-console-unavailable');
|
||||
}
|
||||
return <div className={'theia-debug-console-expression'}>
|
||||
<div>{this._expression}</div>
|
||||
<div className={valueClassNames.join(' ')}>{this._value}</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
async evaluate(context: string = 'repl', resolveLazy = true): Promise<void> {
|
||||
const session = this.session;
|
||||
if (!session?.currentFrame) {
|
||||
this.setResult(undefined, ExpressionItem.notAvailable);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await session.evaluate(this._expression, context);
|
||||
this.setResult(body);
|
||||
if (this.lazy && resolveLazy) {
|
||||
await this.resolveLazy();
|
||||
}
|
||||
} catch (err) {
|
||||
this.setResult(undefined, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
protected setResult(body?: DebugProtocol.EvaluateResponse['body'], error: string = ExpressionItem.notAvailable): void {
|
||||
if (body) {
|
||||
this._value = body.result;
|
||||
this._type = body.type;
|
||||
this._available = true;
|
||||
this.variablesReference = body.variablesReference;
|
||||
this.namedVariables = body.namedVariables;
|
||||
this.indexedVariables = body.indexedVariables;
|
||||
this.presentationHint = body.presentationHint;
|
||||
this.severity = Severity.Log;
|
||||
} else {
|
||||
this._value = error;
|
||||
this._type = undefined;
|
||||
this._available = false;
|
||||
this.variablesReference = 0;
|
||||
this.namedVariables = undefined;
|
||||
this.indexedVariables = undefined;
|
||||
this.presentationHint = undefined;
|
||||
this.severity = Severity.Error;
|
||||
}
|
||||
this.elements = undefined;
|
||||
}
|
||||
|
||||
protected override handleResolvedLazy(resolved: DebugProtocol.Variable): void {
|
||||
this._value = resolved.value;
|
||||
this._type = resolved.type || this._type;
|
||||
super.handleResolvedLazy(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
export class DebugScope extends ExpressionContainer {
|
||||
|
||||
constructor(
|
||||
protected readonly raw: DebugProtocol.Scope,
|
||||
session: DebugSessionProvider,
|
||||
id: number
|
||||
) {
|
||||
super({
|
||||
session,
|
||||
id: `${raw.name}:${id}`,
|
||||
variablesReference: raw.variablesReference,
|
||||
namedVariables: raw.namedVariables,
|
||||
indexedVariables: raw.indexedVariables
|
||||
});
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
get expensive(): boolean {
|
||||
return this.raw.expensive;
|
||||
}
|
||||
|
||||
get range(): monaco.Range | undefined {
|
||||
const { line, column, endLine, endColumn } = this.raw;
|
||||
if (line !== undefined && column !== undefined && endLine !== undefined && endColumn !== undefined) {
|
||||
return new monaco.Range(line, column, endLine, endColumn);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.raw.name;
|
||||
}
|
||||
|
||||
expandByDefault(): boolean {
|
||||
return this.raw.presentationHint === 'locals';
|
||||
}
|
||||
|
||||
}
|
||||
270
packages/debug/src/browser/console/debug-console-session.ts
Normal file
270
packages/debug/src/browser/console/debug-console-session.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import throttle = require('@theia/core/shared/lodash.throttle');
|
||||
import { DebugProtocol } from '@vscode/debugprotocol/lib/debugProtocol';
|
||||
import { ConsoleSession, ConsoleItem } from '@theia/console/lib/browser/console-session';
|
||||
import { AnsiConsoleItem } from '@theia/console/lib/browser/ansi-console-item';
|
||||
import { DebugSession } from '../debug-session';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ExpressionContainer, ExpressionItem } from './debug-console-items';
|
||||
import { Severity } from '@theia/core/lib/common/severity';
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { DebugSessionManager } from '../debug-session-manager';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { LanguageSelector } from '@theia/monaco-editor-core/esm/vs/editor/common/languageSelector';
|
||||
import { Disposable } from '@theia/core';
|
||||
|
||||
export const DebugConsoleSessionFactory = Symbol('DebugConsoleSessionFactory');
|
||||
|
||||
export type DebugConsoleSessionFactory = (debugSession: DebugSession) => DebugConsoleSession;
|
||||
|
||||
@injectable()
|
||||
export class DebugConsoleSession extends ConsoleSession {
|
||||
|
||||
static uri = new URI().withScheme('debugconsole');
|
||||
|
||||
@inject(DebugSessionManager) protected readonly sessionManager: DebugSessionManager;
|
||||
|
||||
protected items: ConsoleItem[] = [];
|
||||
|
||||
protected _terminated = false;
|
||||
|
||||
protected _debugSession: DebugSession;
|
||||
|
||||
// content buffer for [append](#append) method
|
||||
protected uncompletedItemContent: string | undefined;
|
||||
|
||||
protected readonly completionKinds = new Map<DebugProtocol.CompletionItemType | undefined, monaco.languages.CompletionItemKind>();
|
||||
|
||||
get debugSession(): DebugSession {
|
||||
return this._debugSession;
|
||||
}
|
||||
|
||||
set debugSession(value: DebugSession) {
|
||||
this._debugSession = value;
|
||||
this.id = value.id;
|
||||
}
|
||||
|
||||
get terminated(): boolean {
|
||||
return this._terminated;
|
||||
}
|
||||
|
||||
markTerminated(): void {
|
||||
if (!this._terminated) {
|
||||
this._terminated = true;
|
||||
this.fireDidChange();
|
||||
}
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
init(): void {
|
||||
this.completionKinds.set('method', monaco.languages.CompletionItemKind.Method);
|
||||
this.completionKinds.set('function', monaco.languages.CompletionItemKind.Function);
|
||||
this.completionKinds.set('constructor', monaco.languages.CompletionItemKind.Constructor);
|
||||
this.completionKinds.set('field', monaco.languages.CompletionItemKind.Field);
|
||||
this.completionKinds.set('variable', monaco.languages.CompletionItemKind.Variable);
|
||||
this.completionKinds.set('class', monaco.languages.CompletionItemKind.Class);
|
||||
this.completionKinds.set('interface', monaco.languages.CompletionItemKind.Interface);
|
||||
this.completionKinds.set('module', monaco.languages.CompletionItemKind.Module);
|
||||
this.completionKinds.set('property', monaco.languages.CompletionItemKind.Property);
|
||||
this.completionKinds.set('unit', monaco.languages.CompletionItemKind.Unit);
|
||||
this.completionKinds.set('value', monaco.languages.CompletionItemKind.Value);
|
||||
this.completionKinds.set('enum', monaco.languages.CompletionItemKind.Enum);
|
||||
this.completionKinds.set('keyword', monaco.languages.CompletionItemKind.Keyword);
|
||||
this.completionKinds.set('snippet', monaco.languages.CompletionItemKind.Snippet);
|
||||
this.completionKinds.set('text', monaco.languages.CompletionItemKind.Text);
|
||||
this.completionKinds.set('color', monaco.languages.CompletionItemKind.Color);
|
||||
this.completionKinds.set('file', monaco.languages.CompletionItemKind.File);
|
||||
this.completionKinds.set('reference', monaco.languages.CompletionItemKind.Reference);
|
||||
this.completionKinds.set('customcolor', monaco.languages.CompletionItemKind.Color);
|
||||
this.toDispose.push((monaco.languages.registerCompletionItemProvider as (languageId: LanguageSelector, provider: monaco.languages.CompletionItemProvider) => Disposable)({
|
||||
scheme: DebugConsoleSession.uri.scheme,
|
||||
hasAccessToAllModels: true
|
||||
}, {
|
||||
triggerCharacters: ['.'],
|
||||
provideCompletionItems: (model, position) => this.completions(model, position),
|
||||
}));
|
||||
this.toDispose.push(this.sessionManager.onDidResolveLazyVariable(() => this.fireDidChange()));
|
||||
}
|
||||
|
||||
getElements(): IterableIterator<ConsoleItem> {
|
||||
return this.items.filter(e => this.matchesFilter(e))[Symbol.iterator]();
|
||||
}
|
||||
|
||||
protected matchesFilter(item: ConsoleItem): boolean {
|
||||
if (this.severity && item.severity !== this.severity) {
|
||||
return false;
|
||||
}
|
||||
if (this.filterText) {
|
||||
const text = this.getItemText(item).toLowerCase();
|
||||
const parsedFilters = this.parseFilterText(this.filterText.toLowerCase());
|
||||
|
||||
if (parsedFilters.include.length > 0) {
|
||||
const matchesAnyInclude = parsedFilters.include.some(filter => text.includes(filter));
|
||||
if (!matchesAnyInclude) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const filter of parsedFilters.exclude) {
|
||||
if (text.includes(filter)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected parseFilterText(filterText: string): { include: string[]; exclude: string[] } {
|
||||
const include: string[] = [];
|
||||
const exclude: string[] = [];
|
||||
|
||||
const terms = filterText.split(',').map(term => term.trim()).filter(term => term.length > 0);
|
||||
|
||||
for (const term of terms) {
|
||||
if (term.startsWith('!') && term.length > 1) {
|
||||
exclude.push(term.substring(1));
|
||||
} else {
|
||||
include.push(term);
|
||||
}
|
||||
}
|
||||
|
||||
return { include, exclude };
|
||||
}
|
||||
|
||||
protected getItemText(item: ConsoleItem): string {
|
||||
if (item instanceof AnsiConsoleItem) {
|
||||
return item.content;
|
||||
}
|
||||
if (item instanceof ExpressionItem) {
|
||||
return `${item.expression} ${item.value}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
protected async completions(model: monaco.editor.ITextModel, position: monaco.Position): Promise<monaco.languages.CompletionList | undefined> {
|
||||
const completionSession = this.findCompletionSession();
|
||||
if (completionSession) {
|
||||
const column = position.column;
|
||||
const lineNumber = position.lineNumber;
|
||||
const word = model.getWordAtPosition({ column, lineNumber });
|
||||
const overwriteBefore = word ? word.word.length : 0;
|
||||
const text = model.getValue();
|
||||
const items = await completionSession.completions(text, column, lineNumber);
|
||||
const suggestions = items.map(item => this.asCompletionItem(text, position, overwriteBefore, item));
|
||||
return { suggestions };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected findCurrentSession(): DebugSession | undefined {
|
||||
const currentSession = this.sessionManager.currentSession;
|
||||
if (!currentSession) {
|
||||
return undefined;
|
||||
}
|
||||
if (currentSession.id === this.debugSession.id) {
|
||||
// perfect match
|
||||
return this.debugSession;
|
||||
}
|
||||
const parentSession = currentSession.findConsoleParent();
|
||||
if (parentSession?.id === this.debugSession.id) {
|
||||
// child of our session
|
||||
return currentSession;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected findCompletionSession(): DebugSession | undefined {
|
||||
let completionSession: DebugSession | undefined = this.findCurrentSession();
|
||||
while (completionSession !== undefined) {
|
||||
if (completionSession.capabilities.supportsCompletionsRequest) {
|
||||
return completionSession;
|
||||
}
|
||||
completionSession = completionSession.parentSession;
|
||||
}
|
||||
return completionSession;
|
||||
}
|
||||
|
||||
protected asCompletionItem(text: string, position: monaco.Position, overwriteBefore: number, item: DebugProtocol.CompletionItem): monaco.languages.CompletionItem {
|
||||
return {
|
||||
label: item.label,
|
||||
insertText: item.text || item.label,
|
||||
kind: this.completionKinds.get(item.type) || monaco.languages.CompletionItemKind.Property,
|
||||
filterText: (item.start && item.length) ? text.substring(item.start, item.start + item.length).concat(item.label) : undefined,
|
||||
range: monaco.Range.fromPositions(position.delta(0, -(item.length || overwriteBefore)), position),
|
||||
sortText: item.sortText
|
||||
};
|
||||
}
|
||||
|
||||
async execute(value: string): Promise<void> {
|
||||
const expression = new ExpressionItem(value, () => this.findCurrentSession());
|
||||
this.items.push(expression);
|
||||
await expression.evaluate();
|
||||
this.fireDidChange();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.items = [];
|
||||
this.fireDidChange();
|
||||
}
|
||||
|
||||
append(value: string): void {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastItem = this.items.slice(-1)[0];
|
||||
if (lastItem instanceof AnsiConsoleItem && lastItem.content === this.uncompletedItemContent) {
|
||||
this.items.pop();
|
||||
this.uncompletedItemContent += value;
|
||||
} else {
|
||||
this.uncompletedItemContent = value;
|
||||
}
|
||||
|
||||
this.items.push(new AnsiConsoleItem(this.uncompletedItemContent, Severity.Info));
|
||||
this.fireDidChange();
|
||||
}
|
||||
|
||||
appendLine(value: string): void {
|
||||
this.items.push(new AnsiConsoleItem(value, Severity.Info));
|
||||
this.fireDidChange();
|
||||
}
|
||||
|
||||
async logOutput(session: DebugSession, event: DebugProtocol.OutputEvent): Promise<void> {
|
||||
const body = event.body;
|
||||
const { category, variablesReference } = body;
|
||||
if (category === 'telemetry') {
|
||||
console.debug(`telemetry/${event.body.output}`, event.body.data);
|
||||
return;
|
||||
}
|
||||
const severity = category === 'stderr' ? Severity.Error : event.body.category === 'console' ? Severity.Warning : Severity.Info;
|
||||
if (variablesReference) {
|
||||
const items = await new ExpressionContainer({ session: () => session, variablesReference }).getElements();
|
||||
for (const item of items) {
|
||||
this.items.push(Object.assign(item, { severity }));
|
||||
}
|
||||
} else if (typeof body.output === 'string') {
|
||||
for (const line of body.output.split('\n')) {
|
||||
this.items.push(new AnsiConsoleItem(line, severity));
|
||||
}
|
||||
}
|
||||
this.fireDidChange();
|
||||
}
|
||||
|
||||
protected override fireDidChange = throttle(() => super.fireDidChange(), 50);
|
||||
|
||||
}
|
||||
20
packages/debug/src/browser/debug-call-stack-item-type-key.ts
Normal file
20
packages/debug/src/browser/debug-call-stack-item-type-key.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContextKey } from '@theia/core/lib/browser/context-key-service';
|
||||
|
||||
export const DebugCallStackItemTypeKey = Symbol('DebugCallStackItemTypeKey');
|
||||
export type DebugCallStackItemTypeKey = ContextKey<'session' | 'thread' | 'stackFrame'>;
|
||||
402
packages/debug/src/browser/debug-commands.ts
Normal file
402
packages/debug/src/browser/debug-commands.ts
Normal file
@@ -0,0 +1,402 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Command, MAIN_MENU_BAR } from '@theia/core/lib/common';
|
||||
import { codicon } from '@theia/core/lib/browser';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
export namespace DebugMenus {
|
||||
export const DEBUG = [...MAIN_MENU_BAR, '6_debug'];
|
||||
export const DEBUG_CONTROLS = [...DEBUG, 'a_controls'];
|
||||
export const DEBUG_CONFIGURATION = [...DEBUG, 'b_configuration'];
|
||||
export const DEBUG_THREADS = [...DEBUG, 'c_threads'];
|
||||
export const DEBUG_SESSIONS = [...DEBUG, 'd_sessions'];
|
||||
export const DEBUG_BREAKPOINT = [...DEBUG, 'e_breakpoint'];
|
||||
export const DEBUG_NEW_BREAKPOINT = [...DEBUG_BREAKPOINT, 'a_new_breakpoint'];
|
||||
export const DEBUG_BREAKPOINTS = [...DEBUG, 'f_breakpoints'];
|
||||
}
|
||||
|
||||
function nlsEditBreakpoint(breakpoint: string): string {
|
||||
return nls.localizeByDefault('Edit {0}...', nls.localizeByDefault(breakpoint));
|
||||
}
|
||||
|
||||
function nlsRemoveBreakpoint(breakpoint: string): string {
|
||||
return nls.localizeByDefault('Remove {0}', nls.localizeByDefault(breakpoint));
|
||||
}
|
||||
|
||||
export function nlsEnableBreakpoint(breakpoint: string): string {
|
||||
return nls.localizeByDefault('Enable {0}', nls.localizeByDefault(breakpoint));
|
||||
}
|
||||
|
||||
export function nlsDisableBreakpoint(breakpoint: string): string {
|
||||
return nls.localizeByDefault('Disable {0}', nls.localizeByDefault(breakpoint));
|
||||
}
|
||||
|
||||
export namespace DebugCommands {
|
||||
|
||||
export const DEBUG_CATEGORY = 'Debug';
|
||||
export const DEBUG_CATEGORY_KEY = nls.getDefaultKey(DEBUG_CATEGORY);
|
||||
|
||||
export const START = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.debug.start',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Start Debugging',
|
||||
iconClass: codicon('debug-alt')
|
||||
});
|
||||
export const START_NO_DEBUG = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.debug.run',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Start Without Debugging'
|
||||
});
|
||||
export const STOP = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.debug.stop',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Stop',
|
||||
iconClass: codicon('debug-stop')
|
||||
});
|
||||
export const RESTART = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.debug.restart',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Restart',
|
||||
iconClass: codicon('debug-restart')
|
||||
});
|
||||
|
||||
export const OPEN_CONFIGURATIONS = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.configurations.open',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Open Configurations'
|
||||
});
|
||||
export const ADD_CONFIGURATION = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.configurations.add',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Add Configuration...'
|
||||
});
|
||||
|
||||
export const STEP_OVER = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.debug.stepOver',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Step Over',
|
||||
iconClass: codicon('debug-step-over')
|
||||
});
|
||||
export const STEP_INTO = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.debug.stepInto',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Step Into',
|
||||
iconClass: codicon('debug-step-into')
|
||||
});
|
||||
export const STEP_OUT = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.debug.stepOut',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Step Out',
|
||||
iconClass: codicon('debug-step-out')
|
||||
});
|
||||
export const CONTINUE = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.debug.continue',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Continue',
|
||||
iconClass: codicon('debug-continue')
|
||||
});
|
||||
export const PAUSE = Command.toDefaultLocalizedCommand({
|
||||
id: 'workbench.action.debug.pause',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Pause',
|
||||
iconClass: codicon('debug-pause')
|
||||
});
|
||||
export const CONTINUE_ALL = Command.toLocalizedCommand({
|
||||
id: 'debug.thread.continue.all',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Continue All',
|
||||
iconClass: codicon('debug-continue')
|
||||
}, 'theia/debug/continueAll', DEBUG_CATEGORY_KEY);
|
||||
export const PAUSE_ALL = Command.toLocalizedCommand({
|
||||
id: 'debug.thread.pause.all',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Pause All',
|
||||
iconClass: codicon('debug-pause')
|
||||
}, 'theia/debug/pauseAll', DEBUG_CATEGORY_KEY);
|
||||
|
||||
export const TOGGLE_BREAKPOINT = Command.toDefaultLocalizedCommand({
|
||||
id: 'editor.debug.action.toggleBreakpoint',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Toggle Breakpoint',
|
||||
});
|
||||
export const INLINE_BREAKPOINT = Command.toDefaultLocalizedCommand({
|
||||
id: 'editor.debug.action.inlineBreakpoint',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Inline Breakpoint',
|
||||
});
|
||||
export const ADD_CONDITIONAL_BREAKPOINT = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.breakpoint.add.conditional',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Add Conditional Breakpoint...',
|
||||
});
|
||||
export const ADD_LOGPOINT = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.breakpoint.add.logpoint',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Add Logpoint...',
|
||||
});
|
||||
export const ADD_FUNCTION_BREAKPOINT = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.breakpoint.add.function',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Add Function Breakpoint',
|
||||
});
|
||||
export const ADD_DATA_BREAKPOINT = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.breakpoint.add.data',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Add Data Breakpoint at Address'
|
||||
});
|
||||
export const ENABLE_SELECTED_BREAKPOINTS = Command.toLocalizedCommand({
|
||||
id: 'debug.breakpoint.enableSelected',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Enable Selected Breakpoints',
|
||||
}, 'theia/debug/enableSelectedBreakpoints', DEBUG_CATEGORY_KEY);
|
||||
export const ENABLE_ALL_BREAKPOINTS = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.breakpoint.enableAll',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Enable All Breakpoints',
|
||||
});
|
||||
export const DISABLE_SELECTED_BREAKPOINTS = Command.toLocalizedCommand({
|
||||
id: 'debug.breakpoint.disableSelected',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Disable Selected Breakpoints',
|
||||
}, 'theia/debug/disableSelectedBreakpoints', DEBUG_CATEGORY_KEY);
|
||||
export const DISABLE_ALL_BREAKPOINTS = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.breakpoint.disableAll',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Disable All Breakpoints',
|
||||
});
|
||||
export const EDIT_BREAKPOINT = Command.toLocalizedCommand({
|
||||
id: 'debug.breakpoint.edit',
|
||||
category: DEBUG_CATEGORY,
|
||||
originalLabel: 'Edit Breakpoint...',
|
||||
label: nlsEditBreakpoint('Breakpoint')
|
||||
}, '', DEBUG_CATEGORY_KEY);
|
||||
export const EDIT_LOGPOINT = Command.toLocalizedCommand({
|
||||
id: 'debug.logpoint.edit',
|
||||
category: DEBUG_CATEGORY,
|
||||
originalLabel: 'Edit Logpoint...',
|
||||
label: nlsEditBreakpoint('Logpoint')
|
||||
}, '', DEBUG_CATEGORY_KEY);
|
||||
export const EDIT_BREAKPOINT_CONDITION = Command.toLocalizedCommand({
|
||||
id: 'debug.breakpoint.editCondition',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Edit Condition...'
|
||||
}, '', DEBUG_CATEGORY_KEY);
|
||||
export const REMOVE_BREAKPOINT = Command.toLocalizedCommand({
|
||||
id: 'debug.breakpoint.remove',
|
||||
category: DEBUG_CATEGORY,
|
||||
originalLabel: 'Remove Breakpoint',
|
||||
label: nlsRemoveBreakpoint('Breakpoint')
|
||||
}, '', DEBUG_CATEGORY_KEY);
|
||||
export const REMOVE_LOGPOINT = Command.toLocalizedCommand({
|
||||
id: 'debug.logpoint.remove',
|
||||
category: DEBUG_CATEGORY,
|
||||
originalLabel: 'Remove Logpoint',
|
||||
label: nlsRemoveBreakpoint('Logpoint')
|
||||
}, '', DEBUG_CATEGORY_KEY);
|
||||
export const REMOVE_SELECTED_BREAKPOINTS = Command.toLocalizedCommand({
|
||||
id: 'debug.breakpoint.removeSelected',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Remove Selected Breakpoints',
|
||||
}, '', DEBUG_CATEGORY_KEY);
|
||||
export const REMOVE_ALL_BREAKPOINTS = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.breakpoint.removeAll',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Remove All Breakpoints',
|
||||
});
|
||||
export const TOGGLE_BREAKPOINTS_ENABLED = Command.toLocalizedCommand({
|
||||
id: 'debug.breakpoint.toggleEnabled',
|
||||
label: 'Toggle Enable Breakpoints',
|
||||
});
|
||||
export const SHOW_HOVER = Command.toDefaultLocalizedCommand({
|
||||
id: 'editor.debug.action.showDebugHover',
|
||||
label: 'Debug: Show Hover'
|
||||
});
|
||||
export const EVALUATE_IN_DEBUG_CONSOLE = Command.toDefaultLocalizedCommand({
|
||||
id: 'editor.debug.action.selectionToRepl',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Evaluate in Debug Console'
|
||||
});
|
||||
export const ADD_TO_WATCH = Command.toDefaultLocalizedCommand({
|
||||
id: 'editor.debug.action.selectionToWatch',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Add to Watch'
|
||||
});
|
||||
export const JUMP_TO_CURSOR = Command.toDefaultLocalizedCommand({
|
||||
id: 'editor.debug.action.jumpToCursor',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Jump to Cursor'
|
||||
});
|
||||
export const RUN_TO_CURSOR = Command.toDefaultLocalizedCommand({
|
||||
id: 'editor.debug.action.runToCursor',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Run to Cursor'
|
||||
});
|
||||
export const RUN_TO_LINE = Command.toDefaultLocalizedCommand({
|
||||
id: 'editor.debug.action.runToLine',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Run to Line'
|
||||
});
|
||||
|
||||
export const RESTART_FRAME = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.frame.restart',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Restart Frame',
|
||||
});
|
||||
export const COPY_CALL_STACK = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.callStack.copy',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Copy Call Stack',
|
||||
});
|
||||
|
||||
export const SET_VARIABLE_VALUE = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.variable.setValue',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Set Value',
|
||||
});
|
||||
export const COPY_VARIABLE_VALUE = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.variable.copyValue',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Copy Value',
|
||||
});
|
||||
export const COPY_VARIABLE_AS_EXPRESSION = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.variable.copyAsExpression',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Copy as Expression',
|
||||
});
|
||||
export const WATCH_VARIABLE = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.variable.watch',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Add to Watch',
|
||||
});
|
||||
|
||||
export const ADD_WATCH_EXPRESSION = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.watch.addExpression',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Add Expression'
|
||||
});
|
||||
export const EDIT_WATCH_EXPRESSION = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.watch.editExpression',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Edit Expression'
|
||||
});
|
||||
export const COPY_WATCH_EXPRESSION_VALUE = Command.toLocalizedCommand({
|
||||
id: 'debug.watch.copyExpressionValue',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Copy Expression Value'
|
||||
}, 'theia/debug/copyExpressionValue', DEBUG_CATEGORY_KEY);
|
||||
export const REMOVE_WATCH_EXPRESSION = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.watch.removeExpression',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Remove Expression'
|
||||
});
|
||||
export const COLLAPSE_ALL_WATCH_EXPRESSIONS = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.watch.collapseAllExpressions',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Collapse All'
|
||||
});
|
||||
export const REMOVE_ALL_WATCH_EXPRESSIONS = Command.toDefaultLocalizedCommand({
|
||||
id: 'debug.watch.removeAllExpressions',
|
||||
category: DEBUG_CATEGORY,
|
||||
label: 'Remove All Expressions'
|
||||
});
|
||||
}
|
||||
export namespace DebugThreadContextCommands {
|
||||
export const STEP_OVER = {
|
||||
id: 'debug.thread.context.context.next'
|
||||
};
|
||||
export const STEP_INTO = {
|
||||
id: 'debug.thread.context.stepin'
|
||||
};
|
||||
export const STEP_OUT = {
|
||||
id: 'debug.thread.context.stepout'
|
||||
};
|
||||
export const CONTINUE = {
|
||||
id: 'debug.thread.context.continue'
|
||||
};
|
||||
export const PAUSE = {
|
||||
id: 'debug.thread.context.pause'
|
||||
};
|
||||
export const TERMINATE = {
|
||||
id: 'debug.thread.context.terminate'
|
||||
};
|
||||
}
|
||||
export namespace DebugSessionContextCommands {
|
||||
export const STOP = {
|
||||
id: 'debug.session.context.stop'
|
||||
};
|
||||
export const RESTART = {
|
||||
id: 'debug.session.context.restart'
|
||||
};
|
||||
export const PAUSE_ALL = {
|
||||
id: 'debug.session.context.pauseAll'
|
||||
};
|
||||
export const CONTINUE_ALL = {
|
||||
id: 'debug.session.context.continueAll'
|
||||
};
|
||||
export const REVEAL = {
|
||||
id: 'debug.session.context.reveal'
|
||||
};
|
||||
}
|
||||
export namespace DebugEditorContextCommands {
|
||||
export const ADD_BREAKPOINT = {
|
||||
id: 'debug.editor.context.addBreakpoint'
|
||||
};
|
||||
export const ADD_CONDITIONAL_BREAKPOINT = {
|
||||
id: 'debug.editor.context.addBreakpoint.conditional'
|
||||
};
|
||||
export const ADD_LOGPOINT = {
|
||||
id: 'debug.editor.context.add.logpoint'
|
||||
};
|
||||
export const REMOVE_BREAKPOINT = {
|
||||
id: 'debug.editor.context.removeBreakpoint'
|
||||
};
|
||||
export const EDIT_BREAKPOINT = {
|
||||
id: 'debug.editor.context.edit.breakpoint'
|
||||
};
|
||||
export const ENABLE_BREAKPOINT = {
|
||||
id: 'debug.editor.context.enableBreakpoint'
|
||||
};
|
||||
export const DISABLE_BREAKPOINT = {
|
||||
id: 'debug.editor.context.disableBreakpoint'
|
||||
};
|
||||
export const REMOVE_LOGPOINT = {
|
||||
id: 'debug.editor.context.logpoint.remove'
|
||||
};
|
||||
export const EDIT_LOGPOINT = {
|
||||
id: 'debug.editor.context.logpoint.edit'
|
||||
};
|
||||
export const ENABLE_LOGPOINT = {
|
||||
id: 'debug.editor.context.logpoint.enable'
|
||||
};
|
||||
export const DISABLE_LOGPOINT = {
|
||||
id: 'debug.editor.context.logpoint.disable'
|
||||
};
|
||||
export const JUMP_TO_CURSOR = {
|
||||
id: 'debug.editor.context.jumpToCursor'
|
||||
};
|
||||
export const RUN_TO_LINE = {
|
||||
id: 'debug.editor.context.runToLine'
|
||||
};
|
||||
}
|
||||
export namespace DebugBreakpointWidgetCommands {
|
||||
export const ACCEPT = {
|
||||
id: 'debug.breakpointWidget.accept'
|
||||
};
|
||||
export const CLOSE = {
|
||||
id: 'debug.breakpointWidget.close'
|
||||
};
|
||||
}
|
||||
590
packages/debug/src/browser/debug-configuration-manager.ts
Normal file
590
packages/debug/src/browser/debug-configuration-manager.ts
Normal file
@@ -0,0 +1,590 @@
|
||||
// *****************************************************************************
|
||||
// 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-only 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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import debounce = require('p-debounce');
|
||||
import { visit, parse } from 'jsonc-parser';
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Emitter, Event, WaitUntilEvent } from '@theia/core/lib/common/event';
|
||||
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { LabelProvider, QuickPickValue, StorageService } from '@theia/core/lib/browser';
|
||||
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { DebugConfigurationModel } from './debug-configuration-model';
|
||||
import { DebugSessionOptions, DynamicDebugConfigurationSessionOptions } from './debug-session-options';
|
||||
import { DebugService } from '../common/debug-service';
|
||||
import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
||||
import { DebugConfiguration } from '../common/debug-common';
|
||||
import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution';
|
||||
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { ICommandService } from '@theia/monaco-editor-core/esm/vs/platform/commands/common/commands';
|
||||
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
|
||||
import { nls, PreferenceConfigurations, PreferenceScope, PreferenceService } from '@theia/core';
|
||||
import { DebugCompound } from '../common/debug-compound';
|
||||
|
||||
export interface WillProvideDebugConfiguration extends WaitUntilEvent {
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DebugConfigurationManager {
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
@inject(DebugService)
|
||||
protected readonly debug: DebugService;
|
||||
@inject(QuickPickService)
|
||||
protected readonly quickPickService: QuickPickService;
|
||||
|
||||
@inject(ContextKeyService)
|
||||
protected readonly contextKeyService: ContextKeyService;
|
||||
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
@inject(MonacoTextModelService)
|
||||
protected readonly textModelService: MonacoTextModelService;
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferences: PreferenceService;
|
||||
|
||||
@inject(PreferenceConfigurations)
|
||||
protected readonly preferenceConfigurations: PreferenceConfigurations;
|
||||
|
||||
@inject(WorkspaceVariableContribution)
|
||||
protected readonly workspaceVariables: WorkspaceVariableContribution;
|
||||
|
||||
protected readonly onDidChangeEmitter = new Emitter<void>();
|
||||
readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
|
||||
|
||||
protected readonly onWillProvideDebugConfigurationEmitter = new Emitter<WillProvideDebugConfiguration>();
|
||||
readonly onWillProvideDebugConfiguration: Event<WillProvideDebugConfiguration> = this.onWillProvideDebugConfigurationEmitter.event;
|
||||
|
||||
protected readonly onWillProvideDynamicDebugConfigurationEmitter = new Emitter<WillProvideDebugConfiguration>();
|
||||
get onWillProvideDynamicDebugConfiguration(): Event<WillProvideDebugConfiguration> {
|
||||
return this.onWillProvideDynamicDebugConfigurationEmitter.event;
|
||||
}
|
||||
|
||||
get onDidChangeConfigurationProviders(): Event<void> {
|
||||
return this.debug.onDidChangeDebugConfigurationProviders;
|
||||
}
|
||||
|
||||
protected debugConfigurationTypeKey: ContextKey<string>;
|
||||
|
||||
protected initialized: Promise<void>;
|
||||
|
||||
protected recentDynamicOptionsTracker: DynamicDebugConfigurationSessionOptions[] = [];
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.doInit();
|
||||
}
|
||||
|
||||
protected async doInit(): Promise<void> {
|
||||
this.debugConfigurationTypeKey = this.contextKeyService.createKey<string>('debugConfigurationType', undefined);
|
||||
this.initialized = this.preferences.ready.then(() => {
|
||||
this.workspaceService.onWorkspaceChanged(this.updateModels);
|
||||
this.preferences.onPreferenceChanged(e => {
|
||||
if (e.preferenceName === 'launch') {
|
||||
this.updateModels();
|
||||
}
|
||||
});
|
||||
return this.updateModels();
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly models = new Map<string, DebugConfigurationModel>();
|
||||
protected updateModels = debounce(async () => {
|
||||
const roots = await this.workspaceService.roots;
|
||||
const toDelete = new Set(this.models.keys());
|
||||
for (const rootStat of roots) {
|
||||
const key = rootStat.resource.toString();
|
||||
toDelete.delete(key);
|
||||
if (!this.models.has(key)) {
|
||||
const model = new DebugConfigurationModel(key, this.preferences);
|
||||
model.onDidChange(() => this.updateCurrent());
|
||||
model.onDispose(() => this.models.delete(key));
|
||||
this.models.set(key, model);
|
||||
}
|
||||
}
|
||||
for (const uri of toDelete) {
|
||||
const model = this.models.get(uri);
|
||||
if (model) {
|
||||
model.dispose();
|
||||
}
|
||||
}
|
||||
this.updateCurrent();
|
||||
}, 500);
|
||||
|
||||
/**
|
||||
* All _non-dynamic_ debug configurations.
|
||||
*/
|
||||
get all(): IterableIterator<DebugSessionOptions> {
|
||||
return this.getAll();
|
||||
}
|
||||
protected *getAll(): IterableIterator<DebugSessionOptions> {
|
||||
for (const model of this.models.values()) {
|
||||
for (const configuration of model.configurations) {
|
||||
yield this.configurationToOptions(configuration, model.workspaceFolderUri);
|
||||
}
|
||||
for (const compound of model.compounds) {
|
||||
yield this.compoundToOptions(compound, model.workspaceFolderUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get supported(): Promise<IterableIterator<DebugSessionOptions>> {
|
||||
return this.getSupported();
|
||||
}
|
||||
protected async getSupported(): Promise<IterableIterator<DebugSessionOptions>> {
|
||||
await this.initialized;
|
||||
const debugTypes = await this.debug.debugTypes();
|
||||
return this.doGetSupported(new Set(debugTypes));
|
||||
}
|
||||
protected *doGetSupported(debugTypes: Set<string>): IterableIterator<DebugSessionOptions> {
|
||||
for (const options of this.getAll()) {
|
||||
if (options.configuration && debugTypes.has(options.configuration.type)) {
|
||||
yield options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected _currentOptions: DebugSessionOptions | undefined;
|
||||
get current(): DebugSessionOptions | undefined {
|
||||
return this._currentOptions;
|
||||
}
|
||||
|
||||
async getSelectedConfiguration(): Promise<DebugSessionOptions | undefined> {
|
||||
if (!DebugSessionOptions.isDynamic(this._currentOptions)) {
|
||||
return this._currentOptions;
|
||||
}
|
||||
|
||||
// Refresh a dynamic configuration from the provider.
|
||||
// This allow providers to update properties before the execution e.g. program
|
||||
const { providerType, workspaceFolderUri, configuration: { name } } = this._currentOptions;
|
||||
const configuration = await this.fetchDynamicDebugConfiguration(name, providerType, workspaceFolderUri);
|
||||
|
||||
if (!configuration) {
|
||||
const message = nls.localize(
|
||||
'theia/debug/missingConfiguration',
|
||||
"Dynamic configuration '{0}:{1}' is missing or not applicable", providerType, name);
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return { name, configuration, providerType, workspaceFolderUri };
|
||||
}
|
||||
|
||||
set current(option: DebugSessionOptions | undefined) {
|
||||
this.updateCurrent(option);
|
||||
this.updateRecentlyUsedDynamicConfigurationOptions(option);
|
||||
}
|
||||
|
||||
protected updateRecentlyUsedDynamicConfigurationOptions(option: DebugSessionOptions | undefined): void {
|
||||
if (DebugSessionOptions.isDynamic(option)) {
|
||||
// Removing an item already present in the list
|
||||
const index = this.recentDynamicOptionsTracker.findIndex(item => this.dynamicOptionsMatch(item, option));
|
||||
if (index > -1) {
|
||||
this.recentDynamicOptionsTracker.splice(index, 1);
|
||||
}
|
||||
// Adding new item, most recent at the top of the list
|
||||
const recentMax = 3;
|
||||
if (this.recentDynamicOptionsTracker.unshift(option) > recentMax) {
|
||||
// Keep the latest 3 dynamic configuration options to not clutter the dropdown.
|
||||
this.recentDynamicOptionsTracker.splice(recentMax);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected dynamicOptionsMatch(one: DynamicDebugConfigurationSessionOptions, other: DynamicDebugConfigurationSessionOptions): boolean {
|
||||
return one.providerType !== undefined
|
||||
&& one.configuration.name === other.configuration.name
|
||||
&& one.providerType === other.providerType
|
||||
&& one.workspaceFolderUri === other.workspaceFolderUri;
|
||||
}
|
||||
|
||||
get recentDynamicOptions(): readonly DynamicDebugConfigurationSessionOptions[] {
|
||||
return this.recentDynamicOptionsTracker;
|
||||
}
|
||||
|
||||
protected updateCurrent(options: DebugSessionOptions | undefined = this._currentOptions): void {
|
||||
if (DebugSessionOptions.isCompound(options)) {
|
||||
this._currentOptions = options && this.find(options.compound, options.workspaceFolderUri);
|
||||
} else {
|
||||
this._currentOptions = options && this.find(options.configuration, options.workspaceFolderUri, options.providerType);
|
||||
}
|
||||
|
||||
if (!this._currentOptions) {
|
||||
const model = this.getModel();
|
||||
if (model) {
|
||||
const configuration = model.configurations[0];
|
||||
if (configuration) {
|
||||
this._currentOptions = this.configurationToOptions(configuration, model.workspaceFolderUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.debugConfigurationTypeKey.set(this.current && this.current.configuration?.type);
|
||||
this.onDidChangeEmitter.fire(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since v1.27.0
|
||||
*/
|
||||
find(name: string, workspaceFolderUri: string): DebugSessionOptions | undefined;
|
||||
/**
|
||||
* Find / Resolve DebugSessionOptions from a given target debug configuration
|
||||
*/
|
||||
find(compound: DebugCompound, workspaceFolderUri?: string): DebugSessionOptions | undefined;
|
||||
find(configuration: DebugConfiguration, workspaceFolderUri?: string, providerType?: string): DebugSessionOptions | undefined;
|
||||
find(name: string, workspaceFolderUri?: string, providerType?: string): DebugSessionOptions | undefined;
|
||||
find(nameOrConfigurationOrCompound: string | DebugConfiguration | DebugCompound, workspaceFolderUri?: string, providerType?: string): DebugSessionOptions | undefined {
|
||||
if (DebugConfiguration.is(nameOrConfigurationOrCompound) && providerType) {
|
||||
// providerType is only applicable to dynamic debug configurations and may only be created if we have a configuration given
|
||||
return this.configurationToOptions(nameOrConfigurationOrCompound, workspaceFolderUri, providerType);
|
||||
}
|
||||
const name = typeof nameOrConfigurationOrCompound === 'string' ? nameOrConfigurationOrCompound : nameOrConfigurationOrCompound.name;
|
||||
const configuration = this.findConfiguration(name, workspaceFolderUri);
|
||||
if (configuration) {
|
||||
return this.configurationToOptions(configuration, workspaceFolderUri);
|
||||
}
|
||||
const compound = this.findCompound(name, workspaceFolderUri);
|
||||
if (compound) {
|
||||
return this.compoundToOptions(compound, workspaceFolderUri);
|
||||
}
|
||||
}
|
||||
|
||||
findConfigurations(name: string, workspaceFolderUri?: string): DebugConfiguration[] {
|
||||
const matches = [];
|
||||
for (const model of this.models.values()) {
|
||||
if (model.workspaceFolderUri === workspaceFolderUri) {
|
||||
for (const configuration of model.configurations) {
|
||||
if (configuration.name === name) {
|
||||
matches.push(configuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
findConfiguration(name: string, workspaceFolderUri?: string): DebugConfiguration | undefined {
|
||||
for (const model of this.models.values()) {
|
||||
if (model.workspaceFolderUri === workspaceFolderUri) {
|
||||
for (const configuration of model.configurations) {
|
||||
if (configuration.name === name) {
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findCompound(name: string, workspaceFolderUri?: string): DebugCompound | undefined {
|
||||
for (const model of this.models.values()) {
|
||||
if (model.workspaceFolderUri === workspaceFolderUri) {
|
||||
for (const compound of model.compounds) {
|
||||
if (compound.name === name) {
|
||||
return compound;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async openConfiguration(): Promise<void> {
|
||||
const currentUri = new URI(this.current?.workspaceFolderUri);
|
||||
const model = this.getModel(currentUri);
|
||||
if (model) {
|
||||
await this.doOpen(model);
|
||||
}
|
||||
}
|
||||
|
||||
protected configurationToOptions(configuration: DebugConfiguration, workspaceFolderUri?: string, providerType?: string): DebugSessionOptions {
|
||||
return { name: configuration.name, configuration, providerType, workspaceFolderUri };
|
||||
}
|
||||
|
||||
protected compoundToOptions(compound: DebugCompound, workspaceFolderUri?: string): DebugSessionOptions {
|
||||
return { name: compound.name, compound, workspaceFolderUri };
|
||||
}
|
||||
|
||||
async addConfiguration(): Promise<void> {
|
||||
let rootUri: URI | undefined = undefined;
|
||||
if (this.workspaceService.saved && this.workspaceService.tryGetRoots().length > 1) {
|
||||
rootUri = await this.selectRootUri();
|
||||
// Do not continue if the user explicitly does not choose a location.
|
||||
if (!rootUri) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const model = this.getModel(rootUri);
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
const editor = MonacoEditor.get(await this.doOpen(model))?.getControl();
|
||||
const editorModel = editor && editor.getModel();
|
||||
if (!editorModel) {
|
||||
return;
|
||||
}
|
||||
const commandService = StandaloneServices.get(ICommandService);
|
||||
let position: monaco.Position | undefined;
|
||||
let depthInArray = 0;
|
||||
let lastProperty = '';
|
||||
visit(editor.getValue(), {
|
||||
onObjectProperty: property => {
|
||||
lastProperty = property;
|
||||
},
|
||||
onArrayBegin: offset => {
|
||||
if (lastProperty === 'configurations' && depthInArray === 0) {
|
||||
position = editorModel!.getPositionAt(offset + 1);
|
||||
}
|
||||
depthInArray++;
|
||||
},
|
||||
onArrayEnd: () => {
|
||||
depthInArray--;
|
||||
}
|
||||
});
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
// Check if there are more characters on a line after a "configurations": [, if yes enter a newline
|
||||
if (editorModel.getLineLastNonWhitespaceColumn(position.lineNumber) > position.column) {
|
||||
editor.setPosition(position);
|
||||
editor.trigger('debug', 'lineBreakInsert', undefined);
|
||||
}
|
||||
// Check if there is already an empty line to insert suggest, if yes just place the cursor
|
||||
if (editorModel!.getLineLastNonWhitespaceColumn(position.lineNumber + 1) === 0) {
|
||||
editor.setPosition({ lineNumber: position.lineNumber + 1, column: 1 << 30 });
|
||||
await commandService.executeCommand('editor.action.deleteLines');
|
||||
}
|
||||
editor.setPosition(position);
|
||||
await commandService.executeCommand('editor.action.insertLineAfter');
|
||||
await commandService.executeCommand('editor.action.triggerSuggest');
|
||||
}
|
||||
|
||||
protected async selectRootUri(): Promise<URI | undefined> {
|
||||
const workspaceRoots = this.workspaceService.tryGetRoots();
|
||||
const items: QuickPickValue<URI>[] = [];
|
||||
for (const workspaceRoot of workspaceRoots) {
|
||||
items.push({
|
||||
label: this.labelProvider.getName(workspaceRoot.resource),
|
||||
description: this.labelProvider.getLongName(workspaceRoot.resource),
|
||||
value: workspaceRoot.resource
|
||||
});
|
||||
}
|
||||
const root = await this.quickPickService.show(items, {
|
||||
placeholder: nls.localize('theia/debug/addConfigurationPlaceholder', 'Select workspace root to add configuration to'),
|
||||
});
|
||||
return root?.value;
|
||||
}
|
||||
|
||||
protected getModel(uri?: URI): DebugConfigurationModel | undefined {
|
||||
const workspaceFolderUri = this.workspaceVariables.getWorkspaceRootUri(uri);
|
||||
if (workspaceFolderUri) {
|
||||
const key = workspaceFolderUri.toString();
|
||||
for (const model of this.models.values()) {
|
||||
if (model.workspaceFolderUri === key) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const model of this.models.values()) {
|
||||
if (model.uri) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
return this.models.values().next().value;
|
||||
}
|
||||
|
||||
protected async doOpen(model: DebugConfigurationModel): Promise<EditorWidget> {
|
||||
const uri = await this.doCreate(model);
|
||||
|
||||
return this.editorManager.open(uri, {
|
||||
mode: 'activate'
|
||||
});
|
||||
}
|
||||
|
||||
protected async doCreate(model: DebugConfigurationModel): Promise<URI> {
|
||||
const uri = model.uri ?? this.preferences.getConfigUri(PreferenceScope.Folder, model.workspaceFolderUri, 'launch');
|
||||
if (!uri) { // Since we are requesting information about a known workspace folder, this should never happen.
|
||||
throw new Error('PreferenceService.getConfigUri has returned undefined when a URI was expected.');
|
||||
}
|
||||
const settingsUri = this.preferences.getConfigUri(PreferenceScope.Folder, model.workspaceFolderUri);
|
||||
// Users may have placed their debug configurations in a `settings.json`, in which case we shouldn't modify the file.
|
||||
if (settingsUri && !uri.isEqual(settingsUri)) {
|
||||
await this.ensureContent(uri, model);
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a `launch.json` file contains the minimum necessary content.
|
||||
* If content not found, provides content and populates the file using Monaco.
|
||||
*/
|
||||
protected async ensureContent(uri: URI, model: DebugConfigurationModel): Promise<void> {
|
||||
const textModel = await this.textModelService.createModelReference(uri);
|
||||
const currentContent = textModel.object.valid ? textModel.object.getText() : '';
|
||||
try { // Look for the minimal well-formed launch.json content: {configurations: []}
|
||||
const parsedContent = parse(currentContent);
|
||||
if (Array.isArray(parsedContent.configurations)) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Just keep going
|
||||
}
|
||||
const debugType = await this.selectDebugType();
|
||||
const configurations = debugType ? await this.provideDebugConfigurations(debugType, model.workspaceFolderUri) : [];
|
||||
const content = this.getInitialConfigurationContent(configurations);
|
||||
textModel.object.textEditorModel.setValue(content); // Will clobber anything the user has entered!
|
||||
await textModel.object.save();
|
||||
}
|
||||
|
||||
protected async provideDebugConfigurations(debugType: string, workspaceFolderUri: string | undefined): Promise<DebugConfiguration[]> {
|
||||
await this.fireWillProvideDebugConfiguration();
|
||||
return this.debug.provideDebugConfigurations(debugType, workspaceFolderUri);
|
||||
}
|
||||
protected async fireWillProvideDebugConfiguration(): Promise<void> {
|
||||
await WaitUntilEvent.fire(this.onWillProvideDebugConfigurationEmitter, {});
|
||||
}
|
||||
|
||||
async provideDynamicDebugConfigurations(): Promise<Record<string, DynamicDebugConfigurationSessionOptions[]>> {
|
||||
await this.fireWillProvideDynamicDebugConfiguration();
|
||||
const roots = this.workspaceService.tryGetRoots();
|
||||
const promises = roots.map(async root => {
|
||||
const configsMap = await this.debug.provideDynamicDebugConfigurations!(root.resource.toString());
|
||||
const optionsMap = Object.fromEntries(Object.entries(configsMap).map(([type, configs]) => {
|
||||
const options = configs.map(config => ({
|
||||
name: config.name,
|
||||
providerType: type,
|
||||
configuration: config,
|
||||
workspaceFolderUri: root.resource.toString()
|
||||
}));
|
||||
return [type, options];
|
||||
}));
|
||||
return optionsMap;
|
||||
});
|
||||
|
||||
const typesToOptionsRecords = await Promise.all(promises);
|
||||
const consolidatedTypesToOptions: Record<string, DynamicDebugConfigurationSessionOptions[]> = {};
|
||||
|
||||
for (const typesToOptionsInstance of typesToOptionsRecords) {
|
||||
for (const [providerType, configurationsOptions] of Object.entries(typesToOptionsInstance)) {
|
||||
if (!consolidatedTypesToOptions[providerType]) {
|
||||
consolidatedTypesToOptions[providerType] = [];
|
||||
}
|
||||
consolidatedTypesToOptions[providerType].push(...configurationsOptions);
|
||||
}
|
||||
}
|
||||
return consolidatedTypesToOptions;
|
||||
}
|
||||
|
||||
async fetchDynamicDebugConfiguration(name: string, type: string, folder?: string): Promise<DebugConfiguration | undefined> {
|
||||
await this.fireWillProvideDynamicDebugConfiguration();
|
||||
return this.debug.fetchDynamicDebugConfiguration(name, type, folder);
|
||||
}
|
||||
|
||||
protected async fireWillProvideDynamicDebugConfiguration(): Promise<void> {
|
||||
await this.initialized;
|
||||
await WaitUntilEvent.fire(this.onWillProvideDynamicDebugConfigurationEmitter, {});
|
||||
}
|
||||
|
||||
protected getInitialConfigurationContent(initialConfigurations: DebugConfiguration[]): string {
|
||||
return `{
|
||||
// ${nls.localizeByDefault('Use IntelliSense to learn about possible attributes.')}
|
||||
// ${nls.localizeByDefault('Hover to view descriptions of existing attributes.')}
|
||||
"version": "0.2.0",
|
||||
"configurations": ${JSON.stringify(initialConfigurations, undefined, ' ').split('\n').map(line => ' ' + line).join('\n').trim()}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
protected async selectDebugType(): Promise<string | undefined> {
|
||||
const widget = this.editorManager.currentEditor;
|
||||
if (!widget) {
|
||||
return undefined;
|
||||
}
|
||||
const { languageId } = widget.editor.document;
|
||||
const debuggers = await this.debug.getDebuggersForLanguage(languageId);
|
||||
if (debuggers.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const items: Array<QuickPickValue<string>> = debuggers.map(({ label, type }) => ({ label, value: type }));
|
||||
const selectedItem = await this.quickPickService.show(items, { placeholder: nls.localizeByDefault('Select debugger') });
|
||||
return selectedItem?.value;
|
||||
}
|
||||
|
||||
@inject(StorageService)
|
||||
protected readonly storage: StorageService;
|
||||
|
||||
async load(): Promise<void> {
|
||||
await this.initialized;
|
||||
const data = await this.storage.getData<DebugConfigurationManager.Data>('debug.configurations', {});
|
||||
this.resolveRecentDynamicOptionsFromData(data.recentDynamicOptions);
|
||||
|
||||
// Between versions v1.26 and v1.27, the expected format of the data changed so that old stored data
|
||||
// may not contain the configuration key.
|
||||
if (DebugSessionOptions.isConfiguration(data.current)) {
|
||||
// ensure options name is reflected from old configurations data
|
||||
data.current.name = data.current.name ?? data.current.configuration?.name;
|
||||
this.current = this.find(data.current.configuration, data.current.workspaceFolderUri, data.current.providerType);
|
||||
} else if (DebugSessionOptions.isCompound(data.current)) {
|
||||
this.current = this.find(data.current.name, data.current.workspaceFolderUri);
|
||||
}
|
||||
}
|
||||
|
||||
protected resolveRecentDynamicOptionsFromData(options?: DynamicDebugConfigurationSessionOptions[]): void {
|
||||
if (!options || this.recentDynamicOptionsTracker.length !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure options name is reflected from old configurations data
|
||||
const dynamicOptions = options.map(option => {
|
||||
option.name = option.name ?? option.configuration.name;
|
||||
return option;
|
||||
}).filter(DebugSessionOptions.isDynamic);
|
||||
this.recentDynamicOptionsTracker = dynamicOptions;
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const data: DebugConfigurationManager.Data = {};
|
||||
const { current, recentDynamicOptionsTracker } = this;
|
||||
if (current) {
|
||||
data.current = current;
|
||||
}
|
||||
|
||||
if (this.recentDynamicOptionsTracker.length > 0) {
|
||||
data.recentDynamicOptions = recentDynamicOptionsTracker;
|
||||
}
|
||||
|
||||
if (Object.keys(data).length > 0) {
|
||||
this.storage.setData('debug.configurations', data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace DebugConfigurationManager {
|
||||
export interface Data {
|
||||
current?: DebugSessionOptions,
|
||||
recentDynamicOptions?: DynamicDebugConfigurationSessionOptions[]
|
||||
}
|
||||
}
|
||||
99
packages/debug/src/browser/debug-configuration-model.ts
Normal file
99
packages/debug/src/browser/debug-configuration-model.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { DebugConfiguration } from '../common/debug-common';
|
||||
import { DebugCompound } from '../common/debug-compound';
|
||||
import { isObject, PreferenceService } from '@theia/core/lib/common';
|
||||
|
||||
export class DebugConfigurationModel implements Disposable {
|
||||
|
||||
protected json: DebugConfigurationModel.JsonContent;
|
||||
|
||||
protected readonly onDidChangeEmitter = new Emitter<void>();
|
||||
readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection(
|
||||
this.onDidChangeEmitter
|
||||
);
|
||||
|
||||
constructor(
|
||||
readonly workspaceFolderUri: string,
|
||||
protected readonly preferences: PreferenceService
|
||||
) {
|
||||
this.reconcile();
|
||||
this.toDispose.push(this.preferences.onPreferenceChanged(e => {
|
||||
if (e.preferenceName === 'launch' && e.affects(workspaceFolderUri)) {
|
||||
this.reconcile();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
get uri(): URI | undefined {
|
||||
return this.json.uri;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
get onDispose(): Event<void> {
|
||||
return this.toDispose.onDispose;
|
||||
}
|
||||
|
||||
get configurations(): DebugConfiguration[] {
|
||||
return this.json.configurations;
|
||||
}
|
||||
|
||||
get compounds(): DebugCompound[] {
|
||||
return this.json.compounds;
|
||||
}
|
||||
|
||||
async reconcile(): Promise<void> {
|
||||
this.json = this.parseConfigurations();
|
||||
this.onDidChangeEmitter.fire(undefined);
|
||||
}
|
||||
protected parseConfigurations(): DebugConfigurationModel.JsonContent {
|
||||
const configurations: DebugConfiguration[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { configUri, value } = this.preferences.resolve<any>('launch', undefined, this.workspaceFolderUri);
|
||||
if (isObject(value) && Array.isArray(value.configurations)) {
|
||||
for (const configuration of value.configurations) {
|
||||
if (DebugConfiguration.is(configuration)) {
|
||||
configurations.push(configuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
const compounds: DebugCompound[] = [];
|
||||
if (isObject(value) && Array.isArray(value.compounds)) {
|
||||
for (const compound of value.compounds) {
|
||||
if (DebugCompound.is(compound)) {
|
||||
compounds.push(compound);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { uri: configUri, configurations, compounds };
|
||||
}
|
||||
|
||||
}
|
||||
export namespace DebugConfigurationModel {
|
||||
export interface JsonContent {
|
||||
uri?: URI
|
||||
configurations: DebugConfiguration[]
|
||||
compounds: DebugCompound[]
|
||||
}
|
||||
}
|
||||
43
packages/debug/src/browser/debug-contribution.ts
Normal file
43
packages/debug/src/browser/debug-contribution.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 SAP SE or an SAP affiliate company 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { DebugProtocol } from '@vscode/debugprotocol';
|
||||
import { DebugSessionConnection } from './debug-session-connection';
|
||||
|
||||
export const DebugContribution = Symbol('DebugContribution');
|
||||
|
||||
export interface DebugContribution {
|
||||
register(configType: string, connection: DebugSessionConnection): void;
|
||||
}
|
||||
|
||||
// copied from https://github.com/microsoft/vscode-node-debug2/blob/bcd333ef87642b817ac96d28fde7ab96fee3f6a9/src/nodeDebugInterfaces.d.ts
|
||||
export interface LaunchVSCodeRequest extends DebugProtocol.Request {
|
||||
arguments: LaunchVSCodeArguments;
|
||||
}
|
||||
|
||||
export interface LaunchVSCodeArguments {
|
||||
args: LaunchVSCodeArgument[];
|
||||
env?: { [key: string]: string | null; };
|
||||
}
|
||||
|
||||
export interface LaunchVSCodeArgument {
|
||||
prefix?: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface LaunchVSCodeResult {
|
||||
rendererDebugPort?: number;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
140
packages/debug/src/browser/debug-frontend-module.ts
Normal file
140
packages/debug/src/browser/debug-frontend-module.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import '../../src/browser/style/index.css';
|
||||
|
||||
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
|
||||
import { DebugConfigurationManager } from './debug-configuration-manager';
|
||||
import { DebugWidget } from './view/debug-widget';
|
||||
import { DebugPath, DebugService } from '../common/debug-service';
|
||||
import {
|
||||
WidgetFactory, WebSocketConnectionProvider, FrontendApplicationContribution,
|
||||
bindViewContribution
|
||||
} from '@theia/core/lib/browser';
|
||||
import { DebugSessionManager } from './debug-session-manager';
|
||||
import { DebugResourceResolver } from './debug-resource';
|
||||
import {
|
||||
DebugSessionContribution,
|
||||
DebugSessionFactory,
|
||||
DefaultDebugSessionFactory,
|
||||
DebugSessionContributionRegistry,
|
||||
DebugSessionContributionRegistryImpl
|
||||
} from './debug-session-contribution';
|
||||
import { bindContributionProvider, ResourceResolver } from '@theia/core';
|
||||
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
||||
import { DebugFrontendApplicationContribution } from './debug-frontend-application-contribution';
|
||||
import { DebugConsoleContribution } from './console/debug-console-contribution';
|
||||
import { BreakpointManager } from './breakpoint/breakpoint-manager';
|
||||
import { DebugEditorService } from './editor/debug-editor-service';
|
||||
import { DebugEditorModelFactory, DebugEditorModel } from './editor/debug-editor-model';
|
||||
import { bindDebugPreferences } from '../common/debug-preferences';
|
||||
import { DebugSchemaUpdater } from './debug-schema-updater';
|
||||
import { DebugCallStackItemTypeKey } from './debug-call-stack-item-type-key';
|
||||
import { bindLaunchPreferences } from '../common/launch-preferences';
|
||||
import { DebugPrefixConfiguration } from './debug-prefix-configuration';
|
||||
import { CommandContribution } from '@theia/core/lib/common/command';
|
||||
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
import { DebugWatchManager } from './debug-watch-manager';
|
||||
import { DebugExpressionProvider } from './editor/debug-expression-provider';
|
||||
import { DebugBreakpointWidget } from './editor/debug-breakpoint-widget';
|
||||
import { DebugInlineValueDecorator } from './editor/debug-inline-value-decorator';
|
||||
import { JsonSchemaContribution } from '@theia/core/lib/browser/json-schema-store';
|
||||
import { TabBarDecorator } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||
import { DebugTabBarDecorator } from './debug-tab-bar-decorator';
|
||||
import { DebugContribution } from './debug-contribution';
|
||||
import { QuickAccessContribution } from '@theia/core/lib/browser/quick-input/quick-access';
|
||||
import { DebugViewModel } from './view/debug-view-model';
|
||||
import { DebugToolBar } from './view/debug-toolbar-widget';
|
||||
import { DebugSessionWidget } from './view/debug-session-widget';
|
||||
import { bindDisassemblyView } from './disassembly-view/disassembly-view-contribution';
|
||||
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
|
||||
import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService';
|
||||
import { DebugSessionConfigurationLabelProvider } from './debug-session-configuration-label-provider';
|
||||
import { AddOrEditDataBreakpointAddress } from './breakpoint/debug-data-breakpoint-actions';
|
||||
|
||||
export default new ContainerModule((bind: interfaces.Bind) => {
|
||||
bindContributionProvider(bind, DebugContribution);
|
||||
|
||||
bind(DebugCallStackItemTypeKey).toDynamicValue(({ container }) =>
|
||||
container.get<ContextKeyService>(ContextKeyService).createKey('callStackItemType', undefined)
|
||||
).inSingletonScope();
|
||||
|
||||
bindContributionProvider(bind, DebugSessionContribution);
|
||||
bind(DebugSessionFactory).to(DefaultDebugSessionFactory).inSingletonScope();
|
||||
bind(DebugSessionManager).toSelf().inSingletonScope();
|
||||
|
||||
bind(BreakpointManager).toSelf().inSingletonScope();
|
||||
bind(DebugEditorModelFactory).toDynamicValue(({ container }) => <DebugEditorModelFactory>(editor =>
|
||||
DebugEditorModel.createModel(container, editor)
|
||||
)).inSingletonScope();
|
||||
bind(DebugEditorService).toSelf().inSingletonScope().onActivation((context, service) => {
|
||||
StandaloneServices.get(ICodeEditorService).registerDecorationType('Debug breakpoint placeholder', DebugBreakpointWidget.PLACEHOLDER_DECORATION, {});
|
||||
return service;
|
||||
});
|
||||
|
||||
bind(WidgetFactory).toDynamicValue(({ container }) => ({
|
||||
id: DebugWidget.ID,
|
||||
createWidget: () => DebugWidget.createWidget(container)
|
||||
})).inSingletonScope();
|
||||
DebugConsoleContribution.bindContribution(bind);
|
||||
|
||||
bind(DebugSchemaUpdater).toSelf().inSingletonScope();
|
||||
bind(JsonSchemaContribution).toService(DebugSchemaUpdater);
|
||||
bind(DebugConfigurationManager).toSelf().inSingletonScope();
|
||||
|
||||
bind(DebugInlineValueDecorator).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(DebugInlineValueDecorator);
|
||||
|
||||
bind(DebugService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, DebugPath)).inSingletonScope();
|
||||
bind(DebugResourceResolver).toSelf().inSingletonScope();
|
||||
bind(ResourceResolver).toService(DebugResourceResolver);
|
||||
|
||||
bindViewContribution(bind, DebugFrontendApplicationContribution);
|
||||
bind(FrontendApplicationContribution).toService(DebugFrontendApplicationContribution);
|
||||
bind(TabBarToolbarContribution).toService(DebugFrontendApplicationContribution);
|
||||
bind(ColorContribution).toService(DebugFrontendApplicationContribution);
|
||||
|
||||
bind(DebugSessionContributionRegistryImpl).toSelf().inSingletonScope();
|
||||
bind(DebugSessionContributionRegistry).toService(DebugSessionContributionRegistryImpl);
|
||||
|
||||
bind(DebugPrefixConfiguration).toSelf().inSingletonScope();
|
||||
for (const identifier of [CommandContribution, QuickAccessContribution]) {
|
||||
bind(identifier).toService(DebugPrefixConfiguration);
|
||||
}
|
||||
|
||||
bindDebugPreferences(bind);
|
||||
bindLaunchPreferences(bind);
|
||||
|
||||
bind(DebugWatchManager).toSelf().inSingletonScope();
|
||||
bind(DebugExpressionProvider).toSelf().inSingletonScope();
|
||||
|
||||
bind(DebugTabBarDecorator).toSelf().inSingletonScope();
|
||||
bind(TabBarDecorator).toService(DebugTabBarDecorator);
|
||||
|
||||
bind(DebugViewModel).toSelf().inSingletonScope();
|
||||
bind(DebugToolBar).toSelf().inSingletonScope();
|
||||
for (const subwidget of DebugSessionWidget.subwidgets) {
|
||||
bind(WidgetFactory).toDynamicValue(({ container }) => ({
|
||||
id: subwidget.FACTORY_ID,
|
||||
createWidget: () => subwidget.createWidget(container),
|
||||
}));
|
||||
}
|
||||
bindDisassemblyView(bind);
|
||||
|
||||
bind(DebugSessionConfigurationLabelProvider).toSelf().inSingletonScope();
|
||||
bind(AddOrEditDataBreakpointAddress).toSelf().inSingletonScope();
|
||||
});
|
||||
20
packages/debug/src/browser/debug-package.spec.ts
Normal file
20
packages/debug/src/browser/debug-package.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
describe('debug package', () => {
|
||||
|
||||
it('support code coverage statistics', () => true);
|
||||
});
|
||||
195
packages/debug/src/browser/debug-prefix-configuration.ts
Normal file
195
packages/debug/src/browser/debug-prefix-configuration.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { Command, CommandContribution, CommandHandler, CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { DebugSessionManager } from './debug-session-manager';
|
||||
import { DebugConfigurationManager } from './debug-configuration-manager';
|
||||
import { DebugCommands } from './debug-commands';
|
||||
import { DebugSessionOptions } from './debug-session-options';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { QuickAccessContribution, QuickAccessProvider, QuickAccessRegistry, QuickInputService, StatusBar, StatusBarAlignment } from '@theia/core/lib/browser';
|
||||
import { DebugPreferences } from '../common/debug-preferences';
|
||||
import { filterItems, QuickPickItemOrSeparator, QuickPicks } from '@theia/core/lib/browser/quick-input/quick-input-service';
|
||||
import { CancellationToken, nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class DebugPrefixConfiguration implements CommandContribution, CommandHandler, QuickAccessContribution, QuickAccessProvider {
|
||||
static readonly PREFIX = 'debug ';
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(DebugSessionManager)
|
||||
protected readonly debugSessionManager: DebugSessionManager;
|
||||
|
||||
@inject(DebugPreferences)
|
||||
protected readonly preference: DebugPreferences;
|
||||
|
||||
@inject(DebugConfigurationManager)
|
||||
protected readonly debugConfigurationManager: DebugConfigurationManager;
|
||||
|
||||
@inject(QuickInputService) @optional()
|
||||
protected readonly quickInputService: QuickInputService;
|
||||
|
||||
@inject(QuickAccessRegistry)
|
||||
protected readonly quickAccessRegistry: QuickAccessRegistry;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
@inject(StatusBar)
|
||||
protected readonly statusBar: StatusBar;
|
||||
|
||||
readonly statusBarId = 'select-run-debug-statusbar-item';
|
||||
|
||||
private readonly command = Command.toDefaultLocalizedCommand({
|
||||
id: 'select.debug.configuration',
|
||||
category: DebugCommands.DEBUG_CATEGORY,
|
||||
label: 'Select and Start Debugging'
|
||||
});
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.handleDebugStatusBarVisibility();
|
||||
this.preference.onPreferenceChanged(e => {
|
||||
if (e.preferenceName === 'debug.showInStatusBar') {
|
||||
this.handleDebugStatusBarVisibility();
|
||||
}
|
||||
});
|
||||
const toDisposeOnStart = this.debugSessionManager.onDidStartDebugSession(() => {
|
||||
toDisposeOnStart.dispose();
|
||||
this.handleDebugStatusBarVisibility(true);
|
||||
this.debugConfigurationManager.onDidChange(() => this.handleDebugStatusBarVisibility(true));
|
||||
});
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
this.quickInputService?.open(DebugPrefixConfiguration.PREFIX);
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
isVisible(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
registerCommands(commands: CommandRegistry): void {
|
||||
commands.registerCommand(this.command, this);
|
||||
}
|
||||
|
||||
registerQuickAccessProvider(): void {
|
||||
this.quickAccessRegistry.registerQuickAccessProvider({
|
||||
getInstance: () => this,
|
||||
prefix: DebugPrefixConfiguration.PREFIX,
|
||||
placeholder: '',
|
||||
helpEntries: [{ description: nls.localize('theia/debug/debugConfiguration', 'Debug Configuration'), needsEditor: false }]
|
||||
});
|
||||
}
|
||||
|
||||
protected resolveRootFolderName(uri: string | undefined): string | undefined {
|
||||
return uri && this.workspaceService.isMultiRootWorkspaceOpened
|
||||
? this.labelProvider.getName(new URI(uri))
|
||||
: '';
|
||||
}
|
||||
|
||||
async getPicks(filter: string, token: CancellationToken): Promise<QuickPicks> {
|
||||
const items: QuickPickItemOrSeparator[] = [];
|
||||
const configurations = this.debugConfigurationManager.all;
|
||||
|
||||
for (const config of configurations) {
|
||||
items.push({
|
||||
label: config.name,
|
||||
description: this.resolveRootFolderName(config.workspaceFolderUri),
|
||||
execute: () => this.runConfiguration(config)
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve dynamic configurations from providers
|
||||
const record = await this.debugConfigurationManager.provideDynamicDebugConfigurations();
|
||||
for (const [providerType, configurationOptions] of Object.entries(record)) {
|
||||
if (configurationOptions.length > 0) {
|
||||
items.push({
|
||||
label: providerType,
|
||||
type: 'separator'
|
||||
});
|
||||
}
|
||||
|
||||
for (const options of configurationOptions) {
|
||||
items.push({
|
||||
label: options.name,
|
||||
description: this.resolveRootFolderName(options.workspaceFolderUri),
|
||||
execute: () => this.runConfiguration({ name: options.name, configuration: options.configuration, providerType, workspaceFolderUri: options.workspaceFolderUri })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return filterItems(items, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current debug configuration, and execute debug start command.
|
||||
*
|
||||
* @param configurationOptions the `DebugSessionOptions`.
|
||||
*/
|
||||
protected runConfiguration(configurationOptions: DebugSessionOptions): void {
|
||||
this.debugConfigurationManager.current = configurationOptions;
|
||||
this.commandRegistry.executeCommand(DebugCommands.START.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the visibility of the debug status bar.
|
||||
* @param event the preference change event.
|
||||
*/
|
||||
protected handleDebugStatusBarVisibility(started?: boolean): void {
|
||||
const showInStatusBar = this.preference['debug.showInStatusBar'];
|
||||
if (showInStatusBar === 'never') {
|
||||
return this.removeDebugStatusBar();
|
||||
} else if (showInStatusBar === 'always' || started) {
|
||||
return this.updateStatusBar();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the debug status bar element based on the current configuration.
|
||||
*/
|
||||
protected updateStatusBar(): void {
|
||||
const text: string = this.debugConfigurationManager.current
|
||||
? this.debugConfigurationManager.current.name
|
||||
: '';
|
||||
const icon = '$(codicon-debug-alt-small)';
|
||||
this.statusBar.setElement(this.statusBarId, {
|
||||
alignment: StatusBarAlignment.LEFT,
|
||||
text: text.length ? `${icon} ${text}` : icon,
|
||||
tooltip: this.command.label,
|
||||
command: this.command.id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the debug status bar element.
|
||||
*/
|
||||
protected removeDebugStatusBar(): void {
|
||||
this.statusBar.removeElement(this.statusBarId);
|
||||
}
|
||||
}
|
||||
59
packages/debug/src/browser/debug-resource.ts
Normal file
59
packages/debug/src/browser/debug-resource.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { Resource, ResourceResolver } from '@theia/core';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { DebugSessionManager } from './debug-session-manager';
|
||||
import { DebugSource } from './model/debug-source';
|
||||
|
||||
export class DebugResource implements Resource {
|
||||
|
||||
constructor(
|
||||
public uri: URI,
|
||||
protected readonly manager: DebugSessionManager
|
||||
) { }
|
||||
|
||||
dispose(): void { }
|
||||
|
||||
async readContents(): Promise<string> {
|
||||
const { currentSession } = this.manager;
|
||||
if (!currentSession) {
|
||||
throw new Error(`There is no active debug session to load content '${this.uri}'`);
|
||||
}
|
||||
const source = await currentSession.toSource(this.uri);
|
||||
if (!source) {
|
||||
throw new Error(`There is no source for '${this.uri}'`);
|
||||
}
|
||||
return source.load();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DebugResourceResolver implements ResourceResolver {
|
||||
|
||||
@inject(DebugSessionManager)
|
||||
protected readonly manager: DebugSessionManager;
|
||||
|
||||
resolve(uri: URI): DebugResource {
|
||||
if (uri.scheme !== DebugSource.SCHEME) {
|
||||
throw new Error('The given URI is not a valid debug URI: ' + uri);
|
||||
}
|
||||
return new DebugResource(uri, this.manager);
|
||||
}
|
||||
|
||||
}
|
||||
147
packages/debug/src/browser/debug-schema-updater.ts
Normal file
147
packages/debug/src/browser/debug-schema-updater.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { JsonSchemaRegisterContext, JsonSchemaContribution, JsonSchemaDataStore } from '@theia/core/lib/browser/json-schema-store';
|
||||
import { deepClone, nls } from '@theia/core/lib/common';
|
||||
import { IJSONSchema } from '@theia/core/lib/common/json-schema';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { DebugService } from '../common/debug-service';
|
||||
import { debugPreferencesSchema } from '../common/debug-preferences';
|
||||
import { inputsSchema } from '@theia/variable-resolver/lib/browser/variable-input-schema';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
import { defaultCompound } from '../common/debug-compound';
|
||||
import { launchSchemaId } from '../common/launch-preferences';
|
||||
|
||||
@injectable()
|
||||
export class DebugSchemaUpdater implements JsonSchemaContribution {
|
||||
|
||||
protected readonly uri = new URI(launchSchemaId);
|
||||
|
||||
@inject(JsonSchemaDataStore) protected readonly jsonStorage: JsonSchemaDataStore;
|
||||
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
|
||||
@inject(DebugService) protected readonly debug: DebugService;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.jsonStorage.setSchema(this.uri, '');
|
||||
}
|
||||
|
||||
registerSchemas(context: JsonSchemaRegisterContext): void {
|
||||
context.registerSchema({
|
||||
fileMatch: ['launch.json'],
|
||||
url: this.uri.toString()
|
||||
});
|
||||
this.workspaceService.updateSchema('launch', { $ref: this.uri.toString() });
|
||||
}
|
||||
|
||||
async update(): Promise<void> {
|
||||
const types = await this.debug.debugTypes();
|
||||
const schema = { ...deepClone(launchSchema) };
|
||||
const items = (<IJSONSchema>schema!.properties!['configurations'].items);
|
||||
|
||||
const attributePromises = types.map(type => this.debug.getSchemaAttributes(type));
|
||||
for (const attributes of await Promise.all(attributePromises)) {
|
||||
for (const attribute of attributes) {
|
||||
const properties: typeof attribute['properties'] = {};
|
||||
for (const key of ['debugViewLocation', 'openDebug', 'internalConsoleOptions']) {
|
||||
properties[key] = debugPreferencesSchema.properties[`debug.${key}`];
|
||||
}
|
||||
attribute.properties = Object.assign(properties, attribute.properties);
|
||||
items.oneOf!.push(attribute);
|
||||
}
|
||||
}
|
||||
items.defaultSnippets!.push(...await this.debug.getConfigurationSnippets());
|
||||
this.jsonStorage.setSchema(this.uri, schema);
|
||||
}
|
||||
}
|
||||
|
||||
const launchSchema: IJSONSchema = {
|
||||
$id: launchSchemaId,
|
||||
type: 'object',
|
||||
title: nls.localizeByDefault('Launch'),
|
||||
required: [],
|
||||
default: { version: '0.2.0', configurations: [], compounds: [] },
|
||||
properties: {
|
||||
version: {
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault('Version of this file format.'),
|
||||
default: '0.2.0'
|
||||
},
|
||||
configurations: {
|
||||
type: 'array',
|
||||
description: nls.localizeByDefault('List of configurations. Add new configurations or edit existing ones by using IntelliSense.'),
|
||||
items: {
|
||||
defaultSnippets: [],
|
||||
'type': 'object',
|
||||
oneOf: []
|
||||
}
|
||||
},
|
||||
compounds: {
|
||||
type: 'array',
|
||||
description: nls.localizeByDefault('List of compounds. Each compound references multiple configurations which will get launched together.'),
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['name', 'configurations'],
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault('Name of compound. Appears in the launch configuration drop down menu.')
|
||||
},
|
||||
configurations: {
|
||||
type: 'array',
|
||||
default: [],
|
||||
items: {
|
||||
oneOf: [{
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault('Please use unique configuration names.')
|
||||
}, {
|
||||
type: 'object',
|
||||
required: ['name'],
|
||||
properties: {
|
||||
name: {
|
||||
enum: [],
|
||||
description: nls.localizeByDefault('Name of compound. Appears in the launch configuration drop down menu.')
|
||||
},
|
||||
folder: {
|
||||
enum: [],
|
||||
description: nls.localizeByDefault('Name of folder in which the compound is located.')
|
||||
}
|
||||
}
|
||||
}]
|
||||
},
|
||||
description: nls.localizeByDefault('Names of configurations that will be started as part of this compound.')
|
||||
},
|
||||
stopAll: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: nls.localizeByDefault('Controls whether manually terminating one session will stop all of the compound sessions.')
|
||||
},
|
||||
preLaunchTask: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: nls.localizeByDefault('Task to run before any of the compound configurations start.')
|
||||
}
|
||||
},
|
||||
default: defaultCompound
|
||||
},
|
||||
default: [defaultCompound]
|
||||
},
|
||||
inputs: inputsSchema.definitions!.inputs
|
||||
},
|
||||
allowComments: true,
|
||||
allowTrailingCommas: true,
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
||||
const disableJSDOM = enableJSDOM();
|
||||
FrontendApplicationConfigProvider.set({});
|
||||
|
||||
import { Container } from '@theia/core/shared/inversify';
|
||||
import { type FileStat } from '@theia/filesystem/lib/common/files';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { expect } from 'chai';
|
||||
import { DebugSessionConfigurationLabelProvider } from './debug-session-configuration-label-provider';
|
||||
|
||||
disableJSDOM();
|
||||
|
||||
describe('DebugSessionConfigurationLabelProvider', () => {
|
||||
|
||||
let roots: FileStat[] = [];
|
||||
const tryGetRoots = () => roots;
|
||||
let labelProvider: DebugSessionConfigurationLabelProvider;
|
||||
|
||||
before(() => {
|
||||
const container = new Container();
|
||||
container.bind(WorkspaceService).toConstantValue(<WorkspaceService>{
|
||||
tryGetRoots
|
||||
});
|
||||
container.bind(DebugSessionConfigurationLabelProvider).toSelf();
|
||||
labelProvider = container.get(DebugSessionConfigurationLabelProvider);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
roots = [];
|
||||
});
|
||||
|
||||
it('should return the name', () => {
|
||||
const name = 'name';
|
||||
const label = labelProvider.getLabel({ name });
|
||||
expect(label).to.be.equal(name);
|
||||
});
|
||||
|
||||
it('should return the name with default params', () => {
|
||||
const name = 'name';
|
||||
const label = labelProvider.getLabel({ name, workspaceFolderUri: 'file:///workspace/folder/basename' });
|
||||
expect(label).to.be.equal(name);
|
||||
});
|
||||
|
||||
it('should return the multi-root name ignoring the workspace', () => {
|
||||
const name = 'name';
|
||||
const label = labelProvider.getLabel({ name, workspaceFolderUri: 'file:///workspace/folder/basename' }, true);
|
||||
expect(label).to.be.equal('name (basename)');
|
||||
});
|
||||
|
||||
it('should ignore the workspace and return the name without default params', () => {
|
||||
roots = [
|
||||
{/* irrelevant */ } as FileStat,
|
||||
{/* irrelevant */ } as FileStat,
|
||||
];
|
||||
|
||||
const name = 'name';
|
||||
const label = labelProvider.getLabel({ name }, false);
|
||||
expect(label).to.be.equal(name);
|
||||
});
|
||||
|
||||
it('should handle multi-workspace roots', () => {
|
||||
roots = [
|
||||
{/* irrelevant */ } as FileStat,
|
||||
{/* irrelevant */ } as FileStat,
|
||||
];
|
||||
|
||||
const name = 'name';
|
||||
const label = labelProvider.getLabel({ name, workspaceFolderUri: 'file:///workspace/root1/folder/basename' });
|
||||
expect(label).to.be.equal('name (basename)');
|
||||
});
|
||||
|
||||
it('should handle falsy basename and URI authority wins with multi-workspace roots', () => {
|
||||
roots = [
|
||||
{/* irrelevant */ } as FileStat,
|
||||
{/* irrelevant */ } as FileStat,
|
||||
];
|
||||
|
||||
const label = labelProvider.getLabel({ name: '', workspaceFolderUri: 'http://example.com' });
|
||||
expect(label).to.be.equal(' (example.com)');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
import { type DebugSessionOptions } from './debug-session-options';
|
||||
|
||||
/**
|
||||
* Provides a label for the debug session without the need to create the session.
|
||||
* Debug session labels are used to check if sessions are the "same".
|
||||
*/
|
||||
@injectable()
|
||||
export class DebugSessionConfigurationLabelProvider {
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
// https://github.com/microsoft/vscode/blob/907518a25c6d6b9467cbcc57132c6adb7e7396b0/src/vs/workbench/contrib/debug/browser/debugSession.ts#L253-L256
|
||||
getLabel(
|
||||
params: Pick<DebugSessionOptions, 'name' | 'workspaceFolderUri'>,
|
||||
includeRoot = this.workspaceService.tryGetRoots().length > 1
|
||||
): string {
|
||||
let { name, workspaceFolderUri } = params;
|
||||
if (includeRoot && workspaceFolderUri) {
|
||||
const uri = new URI(workspaceFolderUri);
|
||||
const path = uri.path;
|
||||
const basenameOrAuthority = path.name || uri.authority;
|
||||
name += ` (${basenameOrAuthority})`;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
}
|
||||
357
packages/debug/src/browser/debug-session-connection.ts
Normal file
357
packages/debug/src/browser/debug-session-connection.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { DebugProtocol } from '@vscode/debugprotocol';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { Event, Emitter, DisposableCollection, Disposable, MaybePromise } from '@theia/core';
|
||||
import { OutputChannel } from '@theia/output/lib/browser/output-channel';
|
||||
|
||||
import { DebugChannel } from '../common/debug-service';
|
||||
|
||||
export type DebugRequestHandler = (request: DebugProtocol.Request) => MaybePromise<any>;
|
||||
|
||||
export interface DebugRequestTypes {
|
||||
'attach': [DebugProtocol.AttachRequestArguments, DebugProtocol.AttachResponse]
|
||||
'breakpointLocations': [DebugProtocol.BreakpointLocationsArguments, DebugProtocol.BreakpointLocationsResponse]
|
||||
'cancel': [DebugProtocol.CancelArguments, DebugProtocol.CancelResponse]
|
||||
'completions': [DebugProtocol.CompletionsArguments, DebugProtocol.CompletionsResponse]
|
||||
'configurationDone': [DebugProtocol.ConfigurationDoneArguments, DebugProtocol.ConfigurationDoneResponse]
|
||||
'continue': [DebugProtocol.ContinueArguments, DebugProtocol.ContinueResponse]
|
||||
'dataBreakpointInfo': [DebugProtocol.DataBreakpointInfoArguments, DebugProtocol.DataBreakpointInfoResponse]
|
||||
'disassemble': [DebugProtocol.DisassembleArguments, DebugProtocol.DisassembleResponse]
|
||||
'disconnect': [DebugProtocol.DisconnectArguments, DebugProtocol.DisconnectResponse]
|
||||
'evaluate': [DebugProtocol.EvaluateArguments, DebugProtocol.EvaluateResponse]
|
||||
'exceptionInfo': [DebugProtocol.ExceptionInfoArguments, DebugProtocol.ExceptionInfoResponse]
|
||||
'goto': [DebugProtocol.GotoArguments, DebugProtocol.GotoResponse]
|
||||
'gotoTargets': [DebugProtocol.GotoTargetsArguments, DebugProtocol.GotoTargetsResponse]
|
||||
'initialize': [DebugProtocol.InitializeRequestArguments, DebugProtocol.InitializeResponse]
|
||||
'launch': [DebugProtocol.LaunchRequestArguments, DebugProtocol.LaunchResponse]
|
||||
'loadedSources': [DebugProtocol.LoadedSourcesArguments, DebugProtocol.LoadedSourcesResponse]
|
||||
'modules': [DebugProtocol.ModulesArguments, DebugProtocol.ModulesResponse]
|
||||
'next': [DebugProtocol.NextArguments, DebugProtocol.NextResponse]
|
||||
'pause': [DebugProtocol.PauseArguments, DebugProtocol.PauseResponse]
|
||||
'readMemory': [DebugProtocol.ReadMemoryArguments, DebugProtocol.ReadMemoryResponse]
|
||||
'restart': [DebugProtocol.RestartArguments, DebugProtocol.RestartResponse]
|
||||
'restartFrame': [DebugProtocol.RestartFrameArguments, DebugProtocol.RestartFrameResponse]
|
||||
'reverseContinue': [DebugProtocol.ReverseContinueArguments, DebugProtocol.ReverseContinueResponse]
|
||||
'scopes': [DebugProtocol.ScopesArguments, DebugProtocol.ScopesResponse]
|
||||
'setBreakpoints': [DebugProtocol.SetBreakpointsArguments, DebugProtocol.SetBreakpointsResponse]
|
||||
'setDataBreakpoints': [DebugProtocol.SetDataBreakpointsArguments, DebugProtocol.SetDataBreakpointsResponse]
|
||||
'setExceptionBreakpoints': [DebugProtocol.SetExceptionBreakpointsArguments, DebugProtocol.SetExceptionBreakpointsResponse]
|
||||
'setExpression': [DebugProtocol.SetExpressionArguments, DebugProtocol.SetExpressionResponse]
|
||||
'setFunctionBreakpoints': [DebugProtocol.SetFunctionBreakpointsArguments, DebugProtocol.SetFunctionBreakpointsResponse]
|
||||
'setInstructionBreakpoints': [DebugProtocol.SetInstructionBreakpointsArguments, DebugProtocol.SetInstructionBreakpointsResponse]
|
||||
'setVariable': [DebugProtocol.SetVariableArguments, DebugProtocol.SetVariableResponse]
|
||||
'source': [DebugProtocol.SourceArguments, DebugProtocol.SourceResponse]
|
||||
'stackTrace': [DebugProtocol.StackTraceArguments, DebugProtocol.StackTraceResponse]
|
||||
'stepBack': [DebugProtocol.StepBackArguments, DebugProtocol.StepBackResponse]
|
||||
'stepIn': [DebugProtocol.StepInArguments, DebugProtocol.StepInResponse]
|
||||
'stepInTargets': [DebugProtocol.StepInTargetsArguments, DebugProtocol.StepInTargetsResponse]
|
||||
'stepOut': [DebugProtocol.StepOutArguments, DebugProtocol.StepOutResponse]
|
||||
'terminate': [DebugProtocol.TerminateArguments, DebugProtocol.TerminateResponse]
|
||||
'terminateThreads': [DebugProtocol.TerminateThreadsArguments, DebugProtocol.TerminateThreadsResponse]
|
||||
'threads': [{}, DebugProtocol.ThreadsResponse]
|
||||
'variables': [DebugProtocol.VariablesArguments, DebugProtocol.VariablesResponse]
|
||||
'writeMemory': [DebugProtocol.WriteMemoryArguments, DebugProtocol.WriteMemoryResponse]
|
||||
}
|
||||
|
||||
export interface DebugEventTypes {
|
||||
'breakpoint': DebugProtocol.BreakpointEvent
|
||||
'capabilities': DebugProtocol.CapabilitiesEvent
|
||||
'continued': DebugProtocol.ContinuedEvent
|
||||
'exited': DebugProtocol.ExitedEvent,
|
||||
'initialized': DebugProtocol.InitializedEvent
|
||||
'invalidated': DebugProtocol.InvalidatedEvent
|
||||
'loadedSource': DebugProtocol.LoadedSourceEvent
|
||||
'module': DebugProtocol.ModuleEvent
|
||||
'output': DebugProtocol.OutputEvent
|
||||
'process': DebugProtocol.ProcessEvent
|
||||
'progressEnd': DebugProtocol.ProgressEndEvent
|
||||
'progressStart': DebugProtocol.ProgressStartEvent
|
||||
'progressUpdate': DebugProtocol.ProgressUpdateEvent
|
||||
'stopped': DebugProtocol.StoppedEvent
|
||||
'terminated': DebugProtocol.TerminatedEvent
|
||||
'thread': DebugProtocol.ThreadEvent
|
||||
}
|
||||
|
||||
export type DebugEventNames = keyof DebugEventTypes;
|
||||
|
||||
export namespace DebugEventTypes {
|
||||
export function isStandardEvent(event: string): event is DebugEventNames {
|
||||
return standardDebugEvents.has(event);
|
||||
};
|
||||
}
|
||||
|
||||
const standardDebugEvents = new Set<string>([
|
||||
'breakpoint',
|
||||
'capabilities',
|
||||
'continued',
|
||||
'exited',
|
||||
'initialized',
|
||||
'invalidated',
|
||||
'loadedSource',
|
||||
'module',
|
||||
'output',
|
||||
'process',
|
||||
'progressEnd',
|
||||
'progressStart',
|
||||
'progressUpdate',
|
||||
'stopped',
|
||||
'terminated',
|
||||
'thread'
|
||||
]);
|
||||
|
||||
export class DebugSessionConnection implements Disposable {
|
||||
|
||||
private sequence = 1;
|
||||
|
||||
protected readonly pendingRequests = new Map<number, Deferred<DebugProtocol.Response>>();
|
||||
protected readonly connectionPromise: Promise<DebugChannel>;
|
||||
|
||||
protected readonly requestHandlers = new Map<string, DebugRequestHandler>();
|
||||
|
||||
protected readonly onDidCustomEventEmitter = new Emitter<DebugProtocol.Event>();
|
||||
readonly onDidCustomEvent: Event<DebugProtocol.Event> = this.onDidCustomEventEmitter.event;
|
||||
|
||||
protected readonly onDidCloseEmitter = new Emitter<void>();
|
||||
readonly onDidClose: Event<void> = this.onDidCloseEmitter.event;
|
||||
|
||||
protected isClosed = false;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection(
|
||||
this.onDidCustomEventEmitter,
|
||||
Disposable.create(() => this.pendingRequests.clear()),
|
||||
Disposable.create(() => this.emitters.clear())
|
||||
);
|
||||
|
||||
constructor(
|
||||
readonly sessionId: string,
|
||||
connectionFactory: (sessionId: string) => Promise<DebugChannel>,
|
||||
protected readonly traceOutputChannel: OutputChannel | undefined
|
||||
) {
|
||||
this.connectionPromise = this.createConnection(connectionFactory);
|
||||
}
|
||||
|
||||
get disposed(): boolean {
|
||||
return this.toDispose.disposed;
|
||||
}
|
||||
|
||||
protected checkDisposed(): void {
|
||||
if (this.disposed) {
|
||||
throw new Error('the debug session connection is disposed, id: ' + this.sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
protected async createConnection(connectionFactory: (sessionId: string) => Promise<DebugChannel>): Promise<DebugChannel> {
|
||||
const connection = await connectionFactory(this.sessionId);
|
||||
connection.onClose(() => {
|
||||
this.isClosed = true;
|
||||
this.cancelPendingRequests();
|
||||
this.onDidCloseEmitter.fire();
|
||||
});
|
||||
connection.onMessage(data => this.handleMessage(data));
|
||||
return connection;
|
||||
}
|
||||
|
||||
protected allThreadsContinued = true;
|
||||
async sendRequest<K extends keyof DebugRequestTypes>(command: K, args: DebugRequestTypes[K][0], timeout?: number): Promise<DebugRequestTypes[K][1]> {
|
||||
const result = await this.doSendRequest(command, args, timeout);
|
||||
if (command === 'next' || command === 'stepIn' ||
|
||||
command === 'stepOut' || command === 'stepBack' ||
|
||||
command === 'reverseContinue' || command === 'restartFrame') {
|
||||
this.fireContinuedEvent((args as any).threadId);
|
||||
}
|
||||
if (command === 'continue') {
|
||||
const response = result as DebugProtocol.ContinueResponse;
|
||||
const allThreadsContinued = response && response.body && response.body.allThreadsContinued;
|
||||
if (allThreadsContinued !== undefined) {
|
||||
this.allThreadsContinued = result.body.allThreadsContinued;
|
||||
}
|
||||
this.fireContinuedEvent((args as any).threadId, this.allThreadsContinued);
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
sendCustomRequest<T extends DebugProtocol.Response>(command: string, args?: any): Promise<T> {
|
||||
return this.doSendRequest<T>(command, args);
|
||||
}
|
||||
|
||||
protected cancelPendingRequests(): void {
|
||||
this.pendingRequests.forEach((deferred, requestId) => {
|
||||
deferred.reject(new Error(`Request ${requestId} cancelled on connection close`));
|
||||
});
|
||||
}
|
||||
|
||||
protected doSendRequest<K extends DebugProtocol.Response>(command: string, args?: any, timeout?: number): Promise<K> {
|
||||
const result = new Deferred<K>();
|
||||
|
||||
if (this.isClosed) {
|
||||
result.reject(new Error('Connection is closed'));
|
||||
} else {
|
||||
const request: DebugProtocol.Request = {
|
||||
seq: this.sequence++,
|
||||
type: 'request',
|
||||
command: command,
|
||||
arguments: args
|
||||
};
|
||||
|
||||
this.pendingRequests.set(request.seq, result);
|
||||
if (timeout) {
|
||||
const handle = setTimeout(() => {
|
||||
const pendingRequest = this.pendingRequests.get(request.seq);
|
||||
if (pendingRequest) {
|
||||
// request has not been handled
|
||||
this.pendingRequests.delete(request.seq);
|
||||
const error: DebugProtocol.Response = {
|
||||
type: 'response',
|
||||
seq: 0,
|
||||
request_seq: request.seq,
|
||||
success: false,
|
||||
command,
|
||||
message: `Request #${request.seq}: ${request.command} timed out`
|
||||
};
|
||||
pendingRequest.reject(error);
|
||||
}
|
||||
}, timeout);
|
||||
result.promise.finally(() => clearTimeout(handle));
|
||||
}
|
||||
this.send(request);
|
||||
}
|
||||
return result.promise;
|
||||
}
|
||||
|
||||
protected async send(message: DebugProtocol.ProtocolMessage): Promise<void> {
|
||||
const connection = await this.connectionPromise;
|
||||
const messageStr = JSON.stringify(message);
|
||||
if (this.traceOutputChannel) {
|
||||
const now = new Date();
|
||||
const dateStr = `${now.toLocaleString(undefined, { hour12: false })}.${now.getMilliseconds()}`;
|
||||
this.traceOutputChannel.appendLine(`${this.sessionId.substring(0, 8)} ${dateStr} theia -> adapter: ${JSON.stringify(message, undefined, 4)}`);
|
||||
}
|
||||
connection.send(messageStr);
|
||||
}
|
||||
|
||||
protected handleMessage(data: string): void {
|
||||
const message: DebugProtocol.ProtocolMessage = JSON.parse(data);
|
||||
if (this.traceOutputChannel) {
|
||||
const now = new Date();
|
||||
const dateStr = `${now.toLocaleString(undefined, { hour12: false })}.${now.getMilliseconds()}`;
|
||||
this.traceOutputChannel.appendLine(`${this.sessionId.substring(0, 8)} ${dateStr} theia <- adapter: ${JSON.stringify(message, undefined, 4)}`);
|
||||
}
|
||||
if (message.type === 'request') {
|
||||
this.handleRequest(message as DebugProtocol.Request);
|
||||
} else if (message.type === 'response') {
|
||||
this.handleResponse(message as DebugProtocol.Response);
|
||||
} else if (message.type === 'event') {
|
||||
this.handleEvent(message as DebugProtocol.Event);
|
||||
}
|
||||
}
|
||||
|
||||
protected handleResponse(response: DebugProtocol.Response): void {
|
||||
const pendingRequest = this.pendingRequests.get(response.request_seq);
|
||||
if (pendingRequest) {
|
||||
this.pendingRequests.delete(response.request_seq);
|
||||
if (!response.success) {
|
||||
pendingRequest.reject(response);
|
||||
} else {
|
||||
pendingRequest.resolve(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onRequest(command: string, handler: DebugRequestHandler): void {
|
||||
this.requestHandlers.set(command, handler);
|
||||
}
|
||||
|
||||
protected async handleRequest(request: DebugProtocol.Request): Promise<void> {
|
||||
const response: DebugProtocol.Response = {
|
||||
type: 'response',
|
||||
seq: 0,
|
||||
command: request.command,
|
||||
request_seq: request.seq,
|
||||
success: true,
|
||||
};
|
||||
const handler = this.requestHandlers.get(request.command);
|
||||
if (handler) {
|
||||
try {
|
||||
response.body = await handler(request);
|
||||
} catch (error) {
|
||||
response.success = false;
|
||||
response.message = error.message;
|
||||
}
|
||||
} else {
|
||||
console.error('Unhandled request', request);
|
||||
}
|
||||
await this.send(response);
|
||||
}
|
||||
|
||||
protected handleEvent(event: DebugProtocol.Event): void {
|
||||
if (event.event === 'continued') {
|
||||
this.allThreadsContinued = (<DebugProtocol.ContinuedEvent>event).body.allThreadsContinued === false ? false : true;
|
||||
}
|
||||
if (DebugEventTypes.isStandardEvent(event.event)) {
|
||||
this.doFire(event.event, event);
|
||||
} else {
|
||||
this.onDidCustomEventEmitter.fire(event);
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly emitters = new Map<string, Emitter<DebugProtocol.Event>>();
|
||||
on<K extends keyof DebugEventTypes>(kind: K, listener: (e: DebugEventTypes[K]) => any): Disposable {
|
||||
return this.getEmitter(kind).event(listener);
|
||||
}
|
||||
|
||||
onEvent<K extends keyof DebugEventTypes>(kind: K): Event<DebugEventTypes[K]> {
|
||||
return this.getEmitter(kind).event;
|
||||
}
|
||||
|
||||
protected fire<K extends keyof DebugEventTypes>(kind: K, e: DebugEventTypes[K]): void {
|
||||
this.doFire(kind, e);
|
||||
}
|
||||
protected doFire<K extends keyof DebugEventTypes>(kind: K, e: DebugEventTypes[K]): void {
|
||||
this.getEmitter(kind).fire(e);
|
||||
}
|
||||
protected getEmitter<K extends keyof DebugEventTypes>(kind: K): Emitter<DebugEventTypes[K]> {
|
||||
const emitter = this.emitters.get(kind) || this.newEmitter();
|
||||
this.emitters.set(kind, emitter);
|
||||
return <Emitter<DebugEventTypes[K]>>emitter;
|
||||
}
|
||||
protected newEmitter(): Emitter<DebugProtocol.Event> {
|
||||
const emitter = new Emitter();
|
||||
this.checkDisposed();
|
||||
this.toDispose.push(emitter);
|
||||
return emitter;
|
||||
}
|
||||
|
||||
protected fireContinuedEvent(threadId: number, allThreadsContinued = false): void {
|
||||
this.fire('continued', {
|
||||
type: 'event',
|
||||
event: 'continued',
|
||||
body: {
|
||||
threadId,
|
||||
allThreadsContinued
|
||||
},
|
||||
seq: -1
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
162
packages/debug/src/browser/debug-session-contribution.ts
Normal file
162
packages/debug/src/browser/debug-session-contribution.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, named, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { CommandService, MessageClient } from '@theia/core/lib/common';
|
||||
import { LabelProvider } from '@theia/core/lib/browser';
|
||||
import { EditorManager } from '@theia/editor/lib/browser';
|
||||
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
|
||||
import { DebugSession } from './debug-session';
|
||||
import { BreakpointManager } from './breakpoint/breakpoint-manager';
|
||||
import { DebugConfigurationSessionOptions, DebugSessionOptions } from './debug-session-options';
|
||||
import { OutputChannelManager, OutputChannel } from '@theia/output/lib/browser/output-channel';
|
||||
import { DebugPreferences } from '../common/debug-preferences';
|
||||
import { DebugSessionConnection } from './debug-session-connection';
|
||||
import { DebugChannel, DebugAdapterPath, ForwardingDebugChannel } from '../common/debug-service';
|
||||
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { DebugContribution } from './debug-contribution';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
import { RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider';
|
||||
import { TestService } from '@theia/test/lib/browser/test-service';
|
||||
import { DebugSessionManager } from './debug-session-manager';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
/**
|
||||
* DebugSessionContributionRegistry symbol for DI.
|
||||
*/
|
||||
export const DebugSessionContributionRegistry = Symbol('DebugSessionContributionRegistry');
|
||||
/**
|
||||
* Debug session contribution registry.
|
||||
*/
|
||||
export interface DebugSessionContributionRegistry {
|
||||
get(debugType: string): DebugSessionContribution | undefined;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DebugSessionContributionRegistryImpl implements DebugSessionContributionRegistry {
|
||||
protected readonly contribs = new Map<string, DebugSessionContribution>();
|
||||
|
||||
@inject(ContributionProvider) @named(DebugSessionContribution)
|
||||
protected readonly contributions: ContributionProvider<DebugSessionContribution>;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
for (const contrib of this.contributions.getContributions()) {
|
||||
this.contribs.set(contrib.debugType, contrib);
|
||||
}
|
||||
}
|
||||
|
||||
get(debugType: string): DebugSessionContribution | undefined {
|
||||
return this.contribs.get(debugType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DebugSessionFactory symbol for DI.
|
||||
*/
|
||||
export const DebugSessionFactory = Symbol('DebugSessionFactory');
|
||||
|
||||
/**
|
||||
* The [debug session](#DebugSession) factory.
|
||||
*/
|
||||
export interface DebugSessionFactory {
|
||||
get(manager: DebugSessionManager, sessionId: string, options: DebugSessionOptions, parentSession?: DebugSession): DebugSession;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DefaultDebugSessionFactory implements DebugSessionFactory {
|
||||
@inject(RemoteConnectionProvider)
|
||||
protected readonly connectionProvider: ServiceConnectionProvider;
|
||||
@inject(TerminalService)
|
||||
protected readonly terminalService: TerminalService;
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
@inject(BreakpointManager)
|
||||
protected readonly breakpoints: BreakpointManager;
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
@inject(MessageClient)
|
||||
protected readonly messages: MessageClient;
|
||||
@inject(OutputChannelManager)
|
||||
protected readonly outputChannelManager: OutputChannelManager;
|
||||
@inject(DebugPreferences)
|
||||
protected readonly debugPreferences: DebugPreferences;
|
||||
@inject(FileService)
|
||||
protected readonly fileService: FileService;
|
||||
@inject(ContributionProvider) @named(DebugContribution)
|
||||
protected readonly debugContributionProvider: ContributionProvider<DebugContribution>;
|
||||
@inject(TestService)
|
||||
protected readonly testService: TestService;
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
@inject(CommandService)
|
||||
protected commandService: CommandService;
|
||||
|
||||
get(manager: DebugSessionManager, sessionId: string, options: DebugConfigurationSessionOptions, parentSession?: DebugSession): DebugSession {
|
||||
const connection = new DebugSessionConnection(
|
||||
sessionId,
|
||||
() => new Promise<DebugChannel>(resolve =>
|
||||
this.connectionProvider.listen(`${DebugAdapterPath}/${sessionId}`, (_, wsChannel) => {
|
||||
resolve(new ForwardingDebugChannel(wsChannel));
|
||||
}, false)
|
||||
),
|
||||
this.getTraceOutputChannel());
|
||||
return new DebugSession(
|
||||
sessionId,
|
||||
options,
|
||||
parentSession,
|
||||
this.testService,
|
||||
options.testRun,
|
||||
manager,
|
||||
connection,
|
||||
this.terminalService,
|
||||
this.editorManager,
|
||||
this.breakpoints,
|
||||
this.labelProvider,
|
||||
this.messages,
|
||||
this.fileService,
|
||||
this.debugContributionProvider,
|
||||
this.workspaceService,
|
||||
this.debugPreferences,
|
||||
this.commandService
|
||||
);
|
||||
}
|
||||
|
||||
protected getTraceOutputChannel(): OutputChannel | undefined {
|
||||
if (this.debugPreferences['debug.trace']) {
|
||||
return this.outputChannelManager.getChannel('Debug adapters');
|
||||
}
|
||||
}
|
||||
}
|
||||
778
packages/debug/src/browser/debug-session-manager.ts
Normal file
778
packages/debug/src/browser/debug-session-manager.ts
Normal file
@@ -0,0 +1,778 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { CommandService, DisposableCollection, Emitter, Event, MessageService, nls, ProgressService, WaitUntilEvent } from '@theia/core';
|
||||
import { LabelProvider, ApplicationShell, ConfirmDialog } from '@theia/core/lib/browser';
|
||||
import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { EditorManager } from '@theia/editor/lib/browser';
|
||||
import { QuickOpenTask } from '@theia/task/lib/browser/quick-open-task';
|
||||
import { TaskService, TaskEndedInfo, TaskEndedTypes } from '@theia/task/lib/browser/task-service';
|
||||
import { VariableResolverService } from '@theia/variable-resolver/lib/browser';
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { DebugConfiguration } from '../common/debug-common';
|
||||
import { DebugError, DebugService } from '../common/debug-service';
|
||||
import { BreakpointManager } from './breakpoint/breakpoint-manager';
|
||||
import { DebugConfigurationManager } from './debug-configuration-manager';
|
||||
import { DebugSession, DebugState, debugStateContextValue } from './debug-session';
|
||||
import { DebugSessionContributionRegistry, DebugSessionFactory } from './debug-session-contribution';
|
||||
import { DebugCompoundRoot, DebugCompoundSessionOptions, DebugConfigurationSessionOptions, DebugSessionOptions, InternalDebugSessionOptions } from './debug-session-options';
|
||||
import { DebugStackFrame } from './model/debug-stack-frame';
|
||||
import { DebugThread } from './model/debug-thread';
|
||||
import { TaskIdentifier } from '@theia/task/lib/common';
|
||||
import { DebugSourceBreakpoint } from './model/debug-source-breakpoint';
|
||||
import { DebugFunctionBreakpoint } from './model/debug-function-breakpoint';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { DebugInstructionBreakpoint } from './model/debug-instruction-breakpoint';
|
||||
import { DebugSessionConfigurationLabelProvider } from './debug-session-configuration-label-provider';
|
||||
import { DebugDataBreakpoint } from './model/debug-data-breakpoint';
|
||||
import { DebugVariable } from './console/debug-console-items';
|
||||
|
||||
export interface WillStartDebugSession extends WaitUntilEvent {
|
||||
}
|
||||
|
||||
export interface WillResolveDebugConfiguration extends WaitUntilEvent {
|
||||
debugType: string
|
||||
}
|
||||
|
||||
export interface DidChangeActiveDebugSession {
|
||||
previous: DebugSession | undefined
|
||||
current: DebugSession | undefined
|
||||
}
|
||||
|
||||
export interface DidChangeBreakpointsEvent {
|
||||
session?: DebugSession
|
||||
uri: URI
|
||||
}
|
||||
|
||||
export interface DidResolveLazyVariableEvent {
|
||||
readonly session: DebugSession
|
||||
readonly variable: DebugVariable
|
||||
}
|
||||
|
||||
export interface DebugSessionCustomEvent {
|
||||
readonly body?: any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
readonly event: string
|
||||
readonly session: DebugSession
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DebugSessionManager {
|
||||
protected readonly _sessions = new Map<string, DebugSession>();
|
||||
|
||||
protected readonly onWillStartDebugSessionEmitter = new Emitter<WillStartDebugSession>();
|
||||
readonly onWillStartDebugSession: Event<WillStartDebugSession> = this.onWillStartDebugSessionEmitter.event;
|
||||
|
||||
protected readonly onWillResolveDebugConfigurationEmitter = new Emitter<WillResolveDebugConfiguration>();
|
||||
readonly onWillResolveDebugConfiguration: Event<WillResolveDebugConfiguration> = this.onWillResolveDebugConfigurationEmitter.event;
|
||||
|
||||
protected readonly onDidCreateDebugSessionEmitter = new Emitter<DebugSession>();
|
||||
readonly onDidCreateDebugSession: Event<DebugSession> = this.onDidCreateDebugSessionEmitter.event;
|
||||
|
||||
protected readonly onDidStartDebugSessionEmitter = new Emitter<DebugSession>();
|
||||
readonly onDidStartDebugSession: Event<DebugSession> = this.onDidStartDebugSessionEmitter.event;
|
||||
|
||||
protected readonly onDidStopDebugSessionEmitter = new Emitter<DebugSession>();
|
||||
readonly onDidStopDebugSession: Event<DebugSession> = this.onDidStopDebugSessionEmitter.event;
|
||||
|
||||
protected readonly onDidChangeActiveDebugSessionEmitter = new Emitter<DidChangeActiveDebugSession>();
|
||||
readonly onDidChangeActiveDebugSession: Event<DidChangeActiveDebugSession> = this.onDidChangeActiveDebugSessionEmitter.event;
|
||||
|
||||
protected readonly onDidDestroyDebugSessionEmitter = new Emitter<DebugSession>();
|
||||
readonly onDidDestroyDebugSession: Event<DebugSession> = this.onDidDestroyDebugSessionEmitter.event;
|
||||
|
||||
protected readonly onDidReceiveDebugSessionCustomEventEmitter = new Emitter<DebugSessionCustomEvent>();
|
||||
readonly onDidReceiveDebugSessionCustomEvent: Event<DebugSessionCustomEvent> = this.onDidReceiveDebugSessionCustomEventEmitter.event;
|
||||
|
||||
protected readonly onDidFocusStackFrameEmitter = new Emitter<DebugStackFrame | undefined>();
|
||||
readonly onDidFocusStackFrame = this.onDidFocusStackFrameEmitter.event;
|
||||
|
||||
protected readonly onDidFocusThreadEmitter = new Emitter<DebugThread | undefined>();
|
||||
readonly onDidFocusThread = this.onDidFocusThreadEmitter.event;
|
||||
|
||||
protected readonly onDidChangeBreakpointsEmitter = new Emitter<DidChangeBreakpointsEvent>();
|
||||
readonly onDidChangeBreakpoints = this.onDidChangeBreakpointsEmitter.event;
|
||||
protected fireDidChangeBreakpoints(event: DidChangeBreakpointsEvent): void {
|
||||
this.onDidChangeBreakpointsEmitter.fire(event);
|
||||
}
|
||||
|
||||
protected readonly onDidChangeEmitter = new Emitter<DebugSession | undefined>();
|
||||
readonly onDidChange: Event<DebugSession | undefined> = this.onDidChangeEmitter.event;
|
||||
protected fireDidChange(current: DebugSession | undefined): void {
|
||||
this.debugTypeKey.set(current?.configuration.type);
|
||||
this.inDebugModeKey.set(this.inDebugMode);
|
||||
this.debugStateKey.set(debugStateContextValue(this.state));
|
||||
this.onDidChangeEmitter.fire(current);
|
||||
}
|
||||
|
||||
protected readonly onDidResolveLazyVariableEmitter = new Emitter<DidResolveLazyVariableEvent>();
|
||||
readonly onDidResolveLazyVariable: Event<DidResolveLazyVariableEvent> = this.onDidResolveLazyVariableEmitter.event;
|
||||
|
||||
@inject(DebugSessionFactory)
|
||||
protected readonly debugSessionFactory: DebugSessionFactory;
|
||||
|
||||
@inject(DebugService)
|
||||
protected readonly debug: DebugService;
|
||||
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
@inject(CommandService)
|
||||
protected commandService: CommandService;
|
||||
|
||||
@inject(BreakpointManager)
|
||||
protected readonly breakpoints: BreakpointManager;
|
||||
|
||||
@inject(VariableResolverService)
|
||||
protected readonly variableResolver: VariableResolverService;
|
||||
|
||||
@inject(DebugSessionContributionRegistry)
|
||||
protected readonly sessionContributionRegistry: DebugSessionContributionRegistry;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@inject(ProgressService)
|
||||
protected readonly progressService: ProgressService;
|
||||
|
||||
@inject(ContextKeyService)
|
||||
protected readonly contextKeyService: ContextKeyService;
|
||||
|
||||
@inject(TaskService)
|
||||
protected readonly taskService: TaskService;
|
||||
|
||||
@inject(DebugConfigurationManager)
|
||||
protected readonly debugConfigurationManager: DebugConfigurationManager;
|
||||
|
||||
@inject(QuickOpenTask)
|
||||
protected readonly quickOpenTask: QuickOpenTask;
|
||||
|
||||
@inject(ApplicationShell)
|
||||
protected readonly shell: ApplicationShell;
|
||||
|
||||
@inject(DebugSessionConfigurationLabelProvider)
|
||||
protected readonly sessionConfigurationLabelProvider: DebugSessionConfigurationLabelProvider;
|
||||
|
||||
protected debugTypeKey: ContextKey<string>;
|
||||
protected inDebugModeKey: ContextKey<boolean>;
|
||||
protected debugStateKey: ContextKey<string>;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.debugTypeKey = this.contextKeyService.createKey<string>('debugType', undefined);
|
||||
this.inDebugModeKey = this.contextKeyService.createKey<boolean>('inDebugMode', this.inDebugMode);
|
||||
this.debugStateKey = this.contextKeyService.createKey<string>('debugState', debugStateContextValue(this.state));
|
||||
this.breakpoints.onDidChangeMarkers(uri => this.fireDidChangeBreakpoints({ uri }));
|
||||
this.labelProvider.onDidChange(event => {
|
||||
for (const uriString of this.breakpoints.getUris()) {
|
||||
const uri = new URI(uriString);
|
||||
if (event.affects(uri)) {
|
||||
this.fireDidChangeBreakpoints({ uri });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get inDebugMode(): boolean {
|
||||
return this.state > DebugState.Inactive;
|
||||
}
|
||||
|
||||
isCurrentEditorFrame(uri: URI | string | monaco.Uri): boolean {
|
||||
return this.currentFrame?.source?.uri.toString() === (uri instanceof URI ? uri : new URI(uri.toString())).toString();
|
||||
}
|
||||
|
||||
protected async saveAll(): Promise<boolean> {
|
||||
if (!this.shell.canSaveAll()) {
|
||||
return true; // Nothing to save.
|
||||
}
|
||||
try {
|
||||
await this.shell.saveAll();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('saveAll failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async start(options: DebugCompoundSessionOptions): Promise<boolean | undefined>;
|
||||
async start(options: DebugConfigurationSessionOptions): Promise<DebugSession | undefined>;
|
||||
async start(options: DebugSessionOptions): Promise<DebugSession | boolean | undefined>;
|
||||
async start(name: string): Promise<DebugSession | boolean | undefined>;
|
||||
async start(optionsOrName: DebugSessionOptions | string): Promise<DebugSession | boolean | undefined> {
|
||||
if (typeof optionsOrName === 'string') {
|
||||
const options = this.debugConfigurationManager.find(optionsOrName);
|
||||
return !!options && this.start(options);
|
||||
}
|
||||
return optionsOrName.configuration ? this.startConfiguration(optionsOrName) : this.startCompound(optionsOrName);
|
||||
}
|
||||
|
||||
protected async startConfiguration(options: DebugConfigurationSessionOptions): Promise<DebugSession | undefined> {
|
||||
return this.progressService.withProgress(nls.localizeByDefault('Starting...'), 'debug', async () => {
|
||||
try {
|
||||
// If a parent session is available saving should be handled by the parent
|
||||
if (!options.configuration.parentSessionId && !options.configuration.suppressSaveBeforeStart && !await this.saveAll()) {
|
||||
return undefined;
|
||||
}
|
||||
await this.fireWillStartDebugSession();
|
||||
const resolved = await this.resolveConfiguration(options);
|
||||
|
||||
if (!resolved || !resolved.configuration) {
|
||||
// As per vscode API: https://code.visualstudio.com/api/references/vscode-api#DebugConfigurationProvider
|
||||
// "Returning the value 'undefined' prevents the debug session from starting.
|
||||
// Returning the value 'null' prevents the debug session from starting and opens the
|
||||
// underlying debug configuration instead."
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
if (resolved === null) {
|
||||
this.debugConfigurationManager.openConfiguration();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sessionConfigurationLabel = this.sessionConfigurationLabelProvider.getLabel(resolved);
|
||||
if (options?.startedByUser
|
||||
&& options.configuration.suppressMultipleSessionWarning !== true
|
||||
&& this.sessions.some(s => this.sessionConfigurationLabelProvider.getLabel(s.options) === sessionConfigurationLabel)
|
||||
) {
|
||||
const yes = await new ConfirmDialog({
|
||||
title: nls.localizeByDefault('Debug'),
|
||||
msg: nls.localizeByDefault("'{0}' is already running. Do you want to start another instance?", sessionConfigurationLabel)
|
||||
}).open();
|
||||
if (!yes) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// preLaunchTask isn't run in case of auto restart as well as postDebugTask
|
||||
if (!options.configuration.__restart) {
|
||||
const taskRun = await this.runTask(options.workspaceFolderUri, resolved.configuration.preLaunchTask, true);
|
||||
if (!taskRun) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const sessionId = await this.debug.createDebugSession(resolved.configuration, options.workspaceFolderUri);
|
||||
return this.doStart(sessionId, resolved);
|
||||
} catch (e) {
|
||||
if (DebugError.NotFound.is(e)) {
|
||||
this.messageService.error(nls.localize('theia/debug/debugSessionTypeNotSupported', 'The debug session type "{0}" is not supported.', e.data.type));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.messageService.error(nls.localize('theia/debug/errorStartingDebugSession', 'There was an error starting the debug session, check the logs for more details.'));
|
||||
console.error('Error starting the debug session', e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected async startCompound(options: DebugCompoundSessionOptions): Promise<boolean | undefined> {
|
||||
let configurations: DebugConfigurationSessionOptions[] = [];
|
||||
const compoundRoot = options.compound.stopAll ? new DebugCompoundRoot() : undefined;
|
||||
try {
|
||||
configurations = this.getCompoundConfigurations(options, compoundRoot);
|
||||
} catch (error) {
|
||||
this.messageService.error(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.compound.preLaunchTask) {
|
||||
const taskRun = await this.runTask(options.workspaceFolderUri, options.compound.preLaunchTask, true);
|
||||
if (!taskRun) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Compound launch is a success only if each configuration launched successfully
|
||||
const values = await Promise.all(configurations.map(async configuration => {
|
||||
const newSession = await this.startConfiguration(configuration);
|
||||
if (newSession) {
|
||||
compoundRoot?.onDidSessionStop(() => newSession.stop(false, () => this.debug.terminateDebugSession(newSession.id)));
|
||||
}
|
||||
return newSession;
|
||||
}));
|
||||
const result = values.every(success => !!success);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected getCompoundConfigurations(options: DebugCompoundSessionOptions, compoundRoot: DebugCompoundRoot | undefined): DebugConfigurationSessionOptions[] {
|
||||
const compound = options.compound;
|
||||
if (!compound.configurations) {
|
||||
throw new Error(nls.localizeByDefault('Compound must have "configurations" attribute set in order to start multiple configurations.'));
|
||||
}
|
||||
|
||||
const configurations: DebugConfigurationSessionOptions[] = [];
|
||||
|
||||
for (const configData of compound.configurations) {
|
||||
const name = typeof configData === 'string' ? configData : configData.name;
|
||||
if (name === compound.name) {
|
||||
throw new Error(nls.localize('theia/debug/compound-cycle', "Launch configuration '{0}' contains a cycle with itself", name));
|
||||
}
|
||||
|
||||
const workspaceFolderUri = typeof configData === 'string' ? options.workspaceFolderUri : configData.folder;
|
||||
const matchingOptions = [...this.debugConfigurationManager.all]
|
||||
.filter(option => option.name === name && !!option.configuration && option.workspaceFolderUri === workspaceFolderUri);
|
||||
if (matchingOptions.length === 1) {
|
||||
const match = matchingOptions[0];
|
||||
if (DebugSessionOptions.isConfiguration(match)) {
|
||||
configurations.push({ ...match, compoundRoot, configuration: { ...match.configuration, noDebug: options.noDebug } });
|
||||
} else {
|
||||
throw new Error(nls.localizeByDefault("Could not find launch configuration '{0}' in the workspace.", name));
|
||||
}
|
||||
} else {
|
||||
throw new Error(matchingOptions.length === 0
|
||||
? workspaceFolderUri
|
||||
? nls.localizeByDefault("Can not find folder with name '{0}' for configuration '{1}' in compound '{2}'.", workspaceFolderUri, name, compound.name)
|
||||
: nls.localizeByDefault("Could not find launch configuration '{0}' in the workspace.", name)
|
||||
: nls.localizeByDefault("There are multiple launch configurations '{0}' in the workspace. Use folder name to qualify the configuration.", name));
|
||||
}
|
||||
}
|
||||
return configurations;
|
||||
}
|
||||
|
||||
protected async fireWillStartDebugSession(): Promise<void> {
|
||||
await WaitUntilEvent.fire(this.onWillStartDebugSessionEmitter, {});
|
||||
}
|
||||
|
||||
protected configurationIds = new Map<string, number>();
|
||||
protected async resolveConfiguration(
|
||||
options: Readonly<DebugConfigurationSessionOptions>
|
||||
): Promise<InternalDebugSessionOptions | undefined | null> {
|
||||
if (InternalDebugSessionOptions.is(options)) {
|
||||
return options;
|
||||
}
|
||||
const { workspaceFolderUri } = options;
|
||||
let configuration = await this.resolveDebugConfiguration(options.configuration, workspaceFolderUri);
|
||||
|
||||
if (configuration) {
|
||||
// Resolve command variables provided by the debugger
|
||||
const commandIdVariables = await this.debug.provideDebuggerVariables(configuration.type);
|
||||
configuration = await this.variableResolver.resolve(configuration, {
|
||||
context: options.workspaceFolderUri ? new URI(options.workspaceFolderUri) : undefined,
|
||||
configurationSection: 'launch',
|
||||
commandIdVariables,
|
||||
configuration
|
||||
});
|
||||
|
||||
if (configuration) {
|
||||
configuration = await this.resolveDebugConfigurationWithSubstitutedVariables(
|
||||
configuration,
|
||||
workspaceFolderUri
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!configuration) {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
const key = configuration.name + workspaceFolderUri;
|
||||
const id = this.configurationIds.has(key) ? this.configurationIds.get(key)! + 1 : 0;
|
||||
this.configurationIds.set(key, id);
|
||||
|
||||
return {
|
||||
id,
|
||||
...options,
|
||||
name: configuration.name,
|
||||
configuration
|
||||
};
|
||||
}
|
||||
|
||||
protected async resolveDebugConfiguration(
|
||||
configuration: DebugConfiguration,
|
||||
workspaceFolderUri: string | undefined
|
||||
): Promise<DebugConfiguration | undefined | null> {
|
||||
await this.fireWillResolveDebugConfiguration(configuration.type);
|
||||
return this.debug.resolveDebugConfiguration(configuration, workspaceFolderUri);
|
||||
}
|
||||
|
||||
protected async fireWillResolveDebugConfiguration(debugType: string): Promise<void> {
|
||||
await WaitUntilEvent.fire(this.onWillResolveDebugConfigurationEmitter, { debugType });
|
||||
}
|
||||
|
||||
protected async resolveDebugConfigurationWithSubstitutedVariables(
|
||||
configuration: DebugConfiguration,
|
||||
workspaceFolderUri: string | undefined
|
||||
): Promise<DebugConfiguration | undefined | null> {
|
||||
return this.debug.resolveDebugConfigurationWithSubstitutedVariables(configuration, workspaceFolderUri);
|
||||
}
|
||||
|
||||
protected async doStart(sessionId: string, options: DebugConfigurationSessionOptions): Promise<DebugSession> {
|
||||
const parentSession = options.configuration.parentSessionId ? this._sessions.get(options.configuration.parentSessionId) : undefined;
|
||||
const contrib = this.sessionContributionRegistry.get(options.configuration.type);
|
||||
const sessionFactory = contrib ? contrib.debugSessionFactory() : this.debugSessionFactory;
|
||||
const session = sessionFactory.get(this, sessionId, options, parentSession);
|
||||
this._sessions.set(sessionId, session);
|
||||
|
||||
this.debugTypeKey.set(session.configuration.type);
|
||||
this.onDidCreateDebugSessionEmitter.fire(session);
|
||||
|
||||
let state = DebugState.Inactive;
|
||||
session.onDidChange(() => {
|
||||
if (state !== session.state) {
|
||||
state = session.state;
|
||||
if (state === DebugState.Stopped) {
|
||||
this.onDidStopDebugSessionEmitter.fire(session);
|
||||
// Only switch to this session if a thread actually stopped (not just state change)
|
||||
if (session.currentThread && session.currentThread.stopped) {
|
||||
this.updateCurrentSession(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Always fire change event to update views (threads, variables, etc.)
|
||||
// The selection logic in widgets will handle not jumping to non-stopped threads
|
||||
this.fireDidChange(session);
|
||||
});
|
||||
session.onDidChangeBreakpoints(uri => this.fireDidChangeBreakpoints({ session, uri }));
|
||||
session.on('terminated', async event => {
|
||||
const restart = event.body && event.body.restart;
|
||||
if (restart) {
|
||||
// postDebugTask isn't run in case of auto restart as well as preLaunchTask
|
||||
this.doRestart(session, !!restart);
|
||||
} else {
|
||||
await session.disconnect(false, () => this.debug.terminateDebugSession(session.id));
|
||||
await this.runTask(session.options.workspaceFolderUri, session.configuration.postDebugTask);
|
||||
}
|
||||
});
|
||||
|
||||
session.on('exited', async event => {
|
||||
await session.disconnect(false, () => this.debug.terminateDebugSession(session.id));
|
||||
});
|
||||
|
||||
session.onDispose(() => this.cleanup(session));
|
||||
session.start().then(() => {
|
||||
this.onDidStartDebugSessionEmitter.fire(session);
|
||||
// Set as current session if no current session exists
|
||||
// This ensures the UI shows the running session and buttons are enabled
|
||||
if (!this.currentSession) {
|
||||
this.updateCurrentSession(session);
|
||||
}
|
||||
}).catch(e => {
|
||||
session.stop(false, () => {
|
||||
this.debug.terminateDebugSession(session.id);
|
||||
});
|
||||
});
|
||||
session.onDidCustomEvent(({ event, body }) =>
|
||||
this.onDidReceiveDebugSessionCustomEventEmitter.fire({ event, body, session })
|
||||
);
|
||||
return session;
|
||||
}
|
||||
|
||||
protected cleanup(session: DebugSession): void {
|
||||
// Data breakpoints belonging to this session that can't persist and aren't verified by some other session should be removed.
|
||||
const currentDataBreakpoints = this.breakpoints.getDataBreakpoints();
|
||||
const toRemove = currentDataBreakpoints.filter(candidate => !candidate.info.canPersist && this.sessions.every(otherSession => otherSession !== session
|
||||
&& otherSession.getDataBreakpoints().every(otherSessionBp => otherSessionBp.id !== candidate.id || !otherSessionBp.verified)))
|
||||
.map(bp => bp.id);
|
||||
const toRetain = this.breakpoints.getDataBreakpoints().filter(candidate => !toRemove.includes(candidate.id));
|
||||
if (currentDataBreakpoints.length !== toRetain.length) {
|
||||
this.breakpoints.setDataBreakpoints(toRetain);
|
||||
}
|
||||
if (this.remove(session.id)) {
|
||||
this.onDidDestroyDebugSessionEmitter.fire(session);
|
||||
}
|
||||
}
|
||||
|
||||
protected async doRestart(session: DebugSession, isRestart: boolean): Promise<DebugSession | undefined> {
|
||||
if (session.canRestart()) {
|
||||
await session.restart();
|
||||
return session;
|
||||
}
|
||||
|
||||
const { options, configuration } = session;
|
||||
session.stop(isRestart, () => this.debug.terminateDebugSession(session.id));
|
||||
configuration.__restart = isRestart;
|
||||
return this.start(options);
|
||||
}
|
||||
|
||||
async terminateSession(session?: DebugSession): Promise<void> {
|
||||
if (!session) {
|
||||
this.updateCurrentSession(this._currentSession);
|
||||
session = this._currentSession;
|
||||
}
|
||||
if (session) {
|
||||
if (session.options.compoundRoot) {
|
||||
session.options.compoundRoot.stopSession();
|
||||
} else if (session.parentSession && session.configuration.lifecycleManagedByParent) {
|
||||
this.terminateSession(session.parentSession);
|
||||
} else {
|
||||
session.stop(false, () => this.debug.terminateDebugSession(session!.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async restartSession(session?: DebugSession): Promise<DebugSession | undefined> {
|
||||
if (!session) {
|
||||
this.updateCurrentSession(this._currentSession);
|
||||
session = this._currentSession;
|
||||
}
|
||||
|
||||
if (session) {
|
||||
if (session.parentSession && session.configuration.lifecycleManagedByParent) {
|
||||
return this.restartSession(session.parentSession);
|
||||
} else {
|
||||
return this.doRestart(session, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected remove(sessionId: string): boolean {
|
||||
const existed = this._sessions.delete(sessionId);
|
||||
const { currentSession } = this;
|
||||
if (currentSession && currentSession.id === sessionId) {
|
||||
this.updateCurrentSession(undefined);
|
||||
}
|
||||
return existed;
|
||||
}
|
||||
|
||||
getSession(sessionId: string): DebugSession | undefined {
|
||||
return this._sessions.get(sessionId);
|
||||
}
|
||||
|
||||
get sessions(): DebugSession[] {
|
||||
return Array.from(this._sessions.values()).filter(session => session.state > DebugState.Inactive);
|
||||
}
|
||||
|
||||
protected _currentSession: DebugSession | undefined;
|
||||
protected readonly disposeOnCurrentSessionChanged = new DisposableCollection();
|
||||
get currentSession(): DebugSession | undefined {
|
||||
return this._currentSession;
|
||||
}
|
||||
set currentSession(current: DebugSession | undefined) {
|
||||
if (this._currentSession === current) {
|
||||
return;
|
||||
}
|
||||
this.disposeOnCurrentSessionChanged.dispose();
|
||||
const previous = this.currentSession;
|
||||
this._currentSession = current;
|
||||
this.onDidChangeActiveDebugSessionEmitter.fire({ previous, current });
|
||||
if (current) {
|
||||
this.disposeOnCurrentSessionChanged.push(current.onDidChange(() => {
|
||||
if (this.currentFrame === this.topFrame) {
|
||||
this.open('auto');
|
||||
}
|
||||
this.fireDidChange(current);
|
||||
}));
|
||||
this.disposeOnCurrentSessionChanged.push(current.onDidResolveLazyVariable(variable => this.onDidResolveLazyVariableEmitter.fire({ session: current, variable })));
|
||||
this.disposeOnCurrentSessionChanged.push(current.onDidFocusStackFrame(frame => this.onDidFocusStackFrameEmitter.fire(frame)));
|
||||
this.disposeOnCurrentSessionChanged.push(current.onDidFocusThread(thread => this.onDidFocusThreadEmitter.fire(thread)));
|
||||
const { currentThread } = current;
|
||||
this.onDidFocusThreadEmitter.fire(currentThread);
|
||||
}
|
||||
this.updateBreakpoints(previous, current);
|
||||
this.open();
|
||||
this.fireDidChange(current);
|
||||
}
|
||||
open(revealOption: 'auto' | 'center' = 'center'): void {
|
||||
const { currentFrame } = this;
|
||||
if (currentFrame && currentFrame.thread.stopped) {
|
||||
currentFrame.open({ revealOption });
|
||||
}
|
||||
}
|
||||
protected updateBreakpoints(previous: DebugSession | undefined, current: DebugSession | undefined): void {
|
||||
const affectedUri = new Set();
|
||||
for (const session of [previous, current]) {
|
||||
if (session) {
|
||||
for (const uriString of session.breakpointUris) {
|
||||
if (!affectedUri.has(uriString)) {
|
||||
affectedUri.add(uriString);
|
||||
this.fireDidChangeBreakpoints({
|
||||
session: current,
|
||||
uri: new URI(uriString)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
protected updateCurrentSession(session: DebugSession | undefined): void {
|
||||
this.currentSession = session || this.sessions[0];
|
||||
}
|
||||
|
||||
get currentThread(): DebugThread | undefined {
|
||||
const session = this.currentSession;
|
||||
return session && session.currentThread;
|
||||
}
|
||||
|
||||
get state(): DebugState {
|
||||
const session = this.currentSession;
|
||||
return session ? session.state : DebugState.Inactive;
|
||||
}
|
||||
|
||||
get currentFrame(): DebugStackFrame | undefined {
|
||||
const { currentThread } = this;
|
||||
return currentThread && currentThread.currentFrame;
|
||||
}
|
||||
get topFrame(): DebugStackFrame | undefined {
|
||||
const { currentThread } = this;
|
||||
return currentThread && currentThread.topFrame;
|
||||
}
|
||||
|
||||
getFunctionBreakpoints(session?: DebugSession): DebugFunctionBreakpoint[] {
|
||||
if (session && session.state > DebugState.Initializing) {
|
||||
return session.getFunctionBreakpoints();
|
||||
}
|
||||
const { labelProvider, breakpoints, editorManager } = this;
|
||||
return this.breakpoints.getFunctionBreakpoints().map(origin => new DebugFunctionBreakpoint(origin, { labelProvider, breakpoints, editorManager }));
|
||||
}
|
||||
|
||||
getInstructionBreakpoints(session?: DebugSession): DebugInstructionBreakpoint[] {
|
||||
if (session && session.state > DebugState.Initializing) {
|
||||
return session.getInstructionBreakpoints();
|
||||
}
|
||||
const { labelProvider, breakpoints, editorManager } = this;
|
||||
return this.breakpoints.getInstructionBreakpoints().map(origin => new DebugInstructionBreakpoint(origin, { labelProvider, breakpoints, editorManager }));
|
||||
}
|
||||
|
||||
getDataBreakpoints(session = this.currentSession): DebugDataBreakpoint[] {
|
||||
if (session && session.state > DebugState.Initializing) {
|
||||
return session.getDataBreakpoints();
|
||||
}
|
||||
const { labelProvider, breakpoints, editorManager } = this;
|
||||
return this.breakpoints.getDataBreakpoints().map(origin => new DebugDataBreakpoint(origin, { labelProvider, breakpoints, editorManager }));
|
||||
}
|
||||
|
||||
getBreakpoints(session?: DebugSession): DebugSourceBreakpoint[];
|
||||
getBreakpoints(uri: URI, session?: DebugSession): DebugSourceBreakpoint[];
|
||||
getBreakpoints(arg?: URI | DebugSession, arg2?: DebugSession): DebugSourceBreakpoint[] {
|
||||
const uri = arg instanceof URI ? arg : undefined;
|
||||
const session = arg instanceof DebugSession ? arg : arg2 instanceof DebugSession ? arg2 : undefined;
|
||||
if (session && session.state > DebugState.Initializing) {
|
||||
return session.getSourceBreakpoints(uri);
|
||||
}
|
||||
|
||||
const activeSessions = this.sessions.filter(s => s.state > DebugState.Initializing);
|
||||
|
||||
// Start with all breakpoints from markers (not installed = shows as filled circle)
|
||||
const { labelProvider, breakpoints, editorManager } = this;
|
||||
const breakpointMap = new Map<string, DebugSourceBreakpoint>();
|
||||
const markers = this.breakpoints.findMarkers({ uri });
|
||||
|
||||
for (const { data } of markers) {
|
||||
const bp = new DebugSourceBreakpoint(data, { labelProvider, breakpoints, editorManager }, this.commandService);
|
||||
breakpointMap.set(bp.id, bp);
|
||||
}
|
||||
|
||||
// Overlay with VERIFIED breakpoints from active sessions only
|
||||
// We only replace a marker-based breakpoint if the session has VERIFIED it
|
||||
// This ensures breakpoints show as filled (not installed) rather than hollow (installed but unverified)
|
||||
for (const activeSession of activeSessions) {
|
||||
const sessionBps = activeSession.getSourceBreakpoints(uri);
|
||||
|
||||
for (const bp of sessionBps) {
|
||||
if (bp.verified) {
|
||||
// Session has verified this breakpoint - use the session's version
|
||||
breakpointMap.set(bp.id, bp);
|
||||
}
|
||||
// If not verified, keep the marker-based one (shows as not installed = filled circle)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(breakpointMap.values());
|
||||
}
|
||||
|
||||
getLineBreakpoints(uri: URI, line: number): DebugSourceBreakpoint[] {
|
||||
const session = this.currentSession;
|
||||
if (session && session.state > DebugState.Initializing) {
|
||||
return session.getSourceBreakpoints(uri).filter(breakpoint => breakpoint.line === line);
|
||||
}
|
||||
const { labelProvider, breakpoints, editorManager } = this;
|
||||
return this.breakpoints.getLineBreakpoints(uri, line).map(origin =>
|
||||
new DebugSourceBreakpoint(origin, { labelProvider, breakpoints, editorManager }, this.commandService)
|
||||
);
|
||||
}
|
||||
|
||||
getInlineBreakpoint(uri: URI, line: number, column: number): DebugSourceBreakpoint | undefined {
|
||||
const session = this.currentSession;
|
||||
if (session && session.state > DebugState.Initializing) {
|
||||
return session.getSourceBreakpoints(uri).filter(breakpoint => breakpoint.line === line && breakpoint.column === column)[0];
|
||||
}
|
||||
const origin = this.breakpoints.getInlineBreakpoint(uri, line, column);
|
||||
const { labelProvider, breakpoints, editorManager } = this;
|
||||
return origin && new DebugSourceBreakpoint(origin, { labelProvider, breakpoints, editorManager }, this.commandService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given tasks.
|
||||
* @param taskName the task name to run, see [TaskNameResolver](#TaskNameResolver)
|
||||
* @return true if it allowed to continue debugging otherwise it returns false
|
||||
*/
|
||||
protected async runTask(workspaceFolderUri: string | undefined, taskName: string | TaskIdentifier | undefined, checkErrors?: boolean): Promise<boolean> {
|
||||
if (!taskName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const taskInfo = await this.taskService.runWorkspaceTask(this.taskService.startUserAction(), workspaceFolderUri, taskName);
|
||||
if (!checkErrors) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const taskLabel = typeof taskName === 'string' ? taskName : JSON.stringify(taskName);
|
||||
if (!taskInfo) {
|
||||
return this.doPostTaskAction(nls.localize('theia/debug/couldNotRunTask', "Could not run the task '{0}'.", taskLabel));
|
||||
}
|
||||
|
||||
const getExitCodePromise: Promise<TaskEndedInfo> = this.taskService.getExitCode(taskInfo.taskId).then(result =>
|
||||
({ taskEndedType: TaskEndedTypes.TaskExited, value: result }));
|
||||
const isBackgroundTaskEndedPromise: Promise<TaskEndedInfo> = this.taskService.isBackgroundTaskEnded(taskInfo.taskId).then(result =>
|
||||
({ taskEndedType: TaskEndedTypes.BackgroundTaskEnded, value: result }));
|
||||
|
||||
// After start running the task, we wait for the task process to exit and if it is a background task, we also wait for a feedback
|
||||
// that a background task is active, as soon as one of the promises fulfills, we can continue and analyze the results.
|
||||
const taskEndedInfo: TaskEndedInfo = await Promise.race([getExitCodePromise, isBackgroundTaskEndedPromise]);
|
||||
|
||||
if (taskEndedInfo.taskEndedType === TaskEndedTypes.BackgroundTaskEnded && taskEndedInfo.value) {
|
||||
return true;
|
||||
}
|
||||
if (taskEndedInfo.taskEndedType === TaskEndedTypes.TaskExited && taskEndedInfo.value === 0) {
|
||||
return true;
|
||||
} else if (taskEndedInfo.taskEndedType === TaskEndedTypes.TaskExited && taskEndedInfo.value !== undefined) {
|
||||
return this.doPostTaskAction(nls.localize('theia/debug/taskTerminatedWithExitCode', "Task '{0}' terminated with exit code {1}.", taskLabel, taskEndedInfo.value));
|
||||
} else {
|
||||
const signal = await this.taskService.getTerminateSignal(taskInfo.taskId);
|
||||
if (signal !== undefined) {
|
||||
return this.doPostTaskAction(nls.localize('theia/debug/taskTerminatedBySignal', "Task '{0}' terminated by signal {1}.", taskLabel, signal));
|
||||
} else {
|
||||
return this.doPostTaskAction(nls.localize('theia/debug/taskTerminatedForUnknownReason', "Task '{0}' terminated for unknown reason.", taskLabel));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async doPostTaskAction(errorMessage: string): Promise<boolean> {
|
||||
const actions = [
|
||||
nls.localizeByDefault('Open {0}', 'launch.json'),
|
||||
nls.localizeByDefault('Cancel'),
|
||||
nls.localizeByDefault('Configure Task'),
|
||||
nls.localizeByDefault('Debug Anyway')
|
||||
];
|
||||
const result = await this.messageService.error(errorMessage, ...actions);
|
||||
switch (result) {
|
||||
case actions[0]: // open launch.json
|
||||
this.debugConfigurationManager.openConfiguration();
|
||||
return false;
|
||||
case actions[1]: // cancel
|
||||
return false;
|
||||
case actions[2]: // configure tasks
|
||||
this.quickOpenTask.configure();
|
||||
return false;
|
||||
default: // continue debugging
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
127
packages/debug/src/browser/debug-session-options.ts
Normal file
127
packages/debug/src/browser/debug-session-options.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Emitter } from '@theia/core';
|
||||
import { DebugConfiguration } from '../common/debug-common';
|
||||
import { DebugCompound } from '../common/debug-compound';
|
||||
|
||||
export class DebugCompoundRoot {
|
||||
private stopped = false;
|
||||
private stopEmitter = new Emitter<void>();
|
||||
onDidSessionStop = this.stopEmitter.event;
|
||||
|
||||
stopSession(): void {
|
||||
if (!this.stopped) { // avoid sending extraneous terminate events
|
||||
this.stopped = true;
|
||||
this.stopEmitter.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface TestRunReference {
|
||||
controllerId: string;
|
||||
runId: string;
|
||||
}
|
||||
|
||||
export interface DebugSessionOptionsBase {
|
||||
workspaceFolderUri?: string;
|
||||
testRun?: TestRunReference;
|
||||
startedByUser?: boolean;
|
||||
}
|
||||
|
||||
export interface DebugConfigurationSessionOptions extends DebugSessionOptionsBase {
|
||||
name: string; // derived from the configuration
|
||||
configuration: DebugConfiguration;
|
||||
compound?: never;
|
||||
compoundRoot?: DebugCompoundRoot;
|
||||
providerType?: string; // Applicable to dynamic configurations
|
||||
}
|
||||
|
||||
export type DynamicDebugConfigurationSessionOptions = DebugConfigurationSessionOptions & { providerType: string };
|
||||
|
||||
export interface DebugCompoundSessionOptions extends DebugSessionOptionsBase {
|
||||
name: string; // derived from the compound
|
||||
configuration?: never;
|
||||
compound: DebugCompound;
|
||||
noDebug?: boolean;
|
||||
}
|
||||
|
||||
export type DebugSessionOptions = DebugConfigurationSessionOptions | DebugCompoundSessionOptions;
|
||||
|
||||
export namespace DebugSessionOptions {
|
||||
export function is(options: unknown): options is DebugSessionOptions {
|
||||
return !!options &&
|
||||
typeof options === 'object' &&
|
||||
('configuration' in options || 'compound' in options);
|
||||
}
|
||||
|
||||
export function isConfiguration(options?: DebugSessionOptions): options is DebugConfigurationSessionOptions {
|
||||
return !!options && 'configuration' in options && !!options.configuration;
|
||||
}
|
||||
|
||||
export function isDynamic(options?: DebugSessionOptions): options is DynamicDebugConfigurationSessionOptions {
|
||||
return isConfiguration(options) && 'providerType' in options && !!options.providerType;
|
||||
}
|
||||
|
||||
export function isCompound(options?: DebugSessionOptions): options is DebugCompoundSessionOptions {
|
||||
return !!options && 'compound' in options && !!options.compound;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat and partial version of a debug session options usable to find the options later in the manager.
|
||||
* @deprecated Not needed anymore, the recommended way is to serialize/deserialize the options directly using `JSON.stringify` and `JSON.parse`.
|
||||
*/
|
||||
export type DebugSessionOptionsData = DebugSessionOptionsBase & (DebugConfiguration | DebugCompound);
|
||||
|
||||
export type InternalDebugSessionOptions = DebugSessionOptions & { id: number };
|
||||
|
||||
export namespace InternalDebugSessionOptions {
|
||||
|
||||
const SEPARATOR = '__CONF__';
|
||||
const SEPARATOR_CONFIGS = '__COMP__';
|
||||
|
||||
export function is(options: DebugSessionOptions): options is InternalDebugSessionOptions {
|
||||
return 'id' in options;
|
||||
}
|
||||
|
||||
/** @deprecated Please use `JSON.stringify` to serialize the options. */
|
||||
export function toValue(options: DebugSessionOptions): string {
|
||||
if (DebugSessionOptions.isCompound(options)) {
|
||||
return options.compound.name + SEPARATOR +
|
||||
options.workspaceFolderUri + SEPARATOR +
|
||||
options.compound?.configurations.join(SEPARATOR_CONFIGS);
|
||||
}
|
||||
return options.configuration.name + SEPARATOR +
|
||||
options.configuration.type + SEPARATOR +
|
||||
options.configuration.request + SEPARATOR +
|
||||
options.workspaceFolderUri + SEPARATOR +
|
||||
options.providerType;
|
||||
}
|
||||
|
||||
/** @deprecated Please use `JSON.parse` to restore previously serialized debug session options. */
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
export function parseValue(value: string): DebugSessionOptionsData {
|
||||
const split = value.split(SEPARATOR);
|
||||
if (split.length === 5) {
|
||||
return { name: split[0], type: split[1], request: split[2], workspaceFolderUri: split[3], providerType: split[4] };
|
||||
}
|
||||
if (split.length === 3) {
|
||||
return { name: split[0], workspaceFolderUri: split[1], configurations: split[2].split(SEPARATOR_CONFIGS) };
|
||||
}
|
||||
throw new Error('Unexpected argument, the argument is expected to have been generated by the \'toValue\' function');
|
||||
}
|
||||
}
|
||||
1037
packages/debug/src/browser/debug-session.tsx
Normal file
1037
packages/debug/src/browser/debug-session.tsx
Normal file
File diff suppressed because it is too large
Load Diff
57
packages/debug/src/browser/debug-tab-bar-decorator.ts
Normal file
57
packages/debug/src/browser/debug-tab-bar-decorator.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { DebugSessionManager } from './debug-session-manager';
|
||||
import { DebugWidget } from './view/debug-widget';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { TabBarDecorator } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||
import { Title, Widget } from '@theia/core/lib/browser';
|
||||
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
|
||||
import { DisposableCollection } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class DebugTabBarDecorator implements TabBarDecorator {
|
||||
readonly id = 'theia-debug-tabbar-decorator';
|
||||
|
||||
protected readonly emitter = new Emitter<void>();
|
||||
protected toDispose = new DisposableCollection();
|
||||
|
||||
@inject(DebugSessionManager)
|
||||
protected readonly debugSessionManager: DebugSessionManager;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.debugSessionManager.onDidStartDebugSession(() => this.fireDidChangeDecorations()),
|
||||
this.debugSessionManager.onDidDestroyDebugSession(() => this.fireDidChangeDecorations())
|
||||
]);
|
||||
}
|
||||
|
||||
decorate(title: Title<Widget>): WidgetDecoration.Data[] {
|
||||
return (title.owner.id === DebugWidget.ID)
|
||||
? [{ badge: this.debugSessionManager.sessions.length }]
|
||||
: [];
|
||||
}
|
||||
|
||||
get onDidChangeDecorations(): Event<void> {
|
||||
return this.emitter.event;
|
||||
}
|
||||
|
||||
protected fireDidChangeDecorations(): void {
|
||||
this.emitter.fire(undefined);
|
||||
}
|
||||
}
|
||||
93
packages/debug/src/browser/debug-watch-manager.ts
Normal file
93
packages/debug/src/browser/debug-watch-manager.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { StorageService } from '@theia/core/lib/browser/storage-service';
|
||||
|
||||
@injectable()
|
||||
export class DebugWatchManager {
|
||||
|
||||
@inject(StorageService)
|
||||
protected readonly storage: StorageService;
|
||||
|
||||
protected readonly onDidChangeEmitter = new Emitter<void>();
|
||||
readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
|
||||
protected idSequence = 0;
|
||||
protected readonly _watchExpressions = new Map<number, string>();
|
||||
|
||||
get watchExpressions(): IterableIterator<[number, string]> {
|
||||
return this._watchExpressions.entries();
|
||||
}
|
||||
|
||||
addWatchExpression(expression: string): number {
|
||||
const id = this.idSequence++;
|
||||
this._watchExpressions.set(id, expression);
|
||||
this.onDidChangeEmitter.fire(undefined);
|
||||
return id;
|
||||
}
|
||||
|
||||
removeWatchExpression(id: number): boolean {
|
||||
if (!this._watchExpressions.has(id)) {
|
||||
return false;
|
||||
}
|
||||
this._watchExpressions.delete(id);
|
||||
this.onDidChangeEmitter.fire(undefined);
|
||||
return true;
|
||||
}
|
||||
|
||||
removeWatchExpressions(): void {
|
||||
if (this._watchExpressions.size) {
|
||||
this.idSequence = 0;
|
||||
this._watchExpressions.clear();
|
||||
this.onDidChangeEmitter.fire(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
const data = await this.storage.getData<DebugWatchData>(this.storageKey, {
|
||||
expressions: []
|
||||
});
|
||||
this.restoreState(data);
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const data = this.storeState();
|
||||
this.storage.setData(this.storageKey, data);
|
||||
}
|
||||
|
||||
protected get storageKey(): string {
|
||||
return 'debug:watch';
|
||||
}
|
||||
|
||||
protected storeState(): DebugWatchData {
|
||||
return {
|
||||
expressions: [...this._watchExpressions.values()]
|
||||
};
|
||||
}
|
||||
|
||||
protected restoreState(state: DebugWatchData): void {
|
||||
for (const expression of state.expressions) {
|
||||
this.addWatchExpression(expression);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface DebugWatchData {
|
||||
readonly expressions: string[];
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { nls } from '@theia/core';
|
||||
import { IListAccessibilityProvider } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/list/listWidget';
|
||||
import { DisassembledInstructionEntry } from './disassembly-view-utilities';
|
||||
|
||||
// This file is adapted from https://github.com/microsoft/vscode/blob/c061ce5c24fc480342fbc5f23244289d633c56eb/src/vs/workbench/contrib/debug/browser/disassemblyView.ts
|
||||
|
||||
export class AccessibilityProvider implements IListAccessibilityProvider<DisassembledInstructionEntry> {
|
||||
|
||||
getWidgetAriaLabel(): string {
|
||||
return nls.localizeByDefault('Disassembly View');
|
||||
}
|
||||
|
||||
getAriaLabel(element: DisassembledInstructionEntry): string | null {
|
||||
let label = '';
|
||||
|
||||
const instruction = element.instruction;
|
||||
if (instruction.address !== '-1') {
|
||||
label += `${nls.localizeByDefault('Address')}: ${instruction.address}`;
|
||||
}
|
||||
if (instruction.instructionBytes) {
|
||||
label += `, ${nls.localizeByDefault('Bytes')}: ${instruction.instructionBytes}`;
|
||||
}
|
||||
label += `, ${nls.localizeByDefault('Instruction')}: ${instruction.instruction}`;
|
||||
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { append, $, addStandardDisposableListener } from '@theia/monaco-editor-core/esm/vs/base/browser/dom';
|
||||
import { ITableRenderer } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/table/table';
|
||||
import { dispose } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
|
||||
import { BreakpointManager } from '../breakpoint/breakpoint-manager';
|
||||
import { BreakpointColumnTemplateData, DisassembledInstructionEntry, DisassemblyViewRendererReference } from './disassembly-view-utilities';
|
||||
|
||||
// This file is adapted from https://github.com/microsoft/vscode/blob/c061ce5c24fc480342fbc5f23244289d633c56eb/src/vs/workbench/contrib/debug/browser/disassemblyView.ts
|
||||
|
||||
export class BreakpointRenderer implements ITableRenderer<DisassembledInstructionEntry, BreakpointColumnTemplateData> {
|
||||
|
||||
static readonly TEMPLATE_ID = 'breakpoint';
|
||||
|
||||
templateId: string = BreakpointRenderer.TEMPLATE_ID;
|
||||
|
||||
protected readonly _breakpointIcon = 'codicon-debug-breakpoint';
|
||||
protected readonly _breakpointDisabledIcon = 'codicon-debug-breakpoint-disabled';
|
||||
protected readonly _breakpointHintIcon = 'codicon-debug-hint';
|
||||
protected readonly _debugStackframe = 'codicon-debug-stackframe';
|
||||
protected readonly _debugStackframeFocused = 'codicon-debug-stackframe-focused';
|
||||
|
||||
constructor(
|
||||
protected readonly _disassemblyView: DisassemblyViewRendererReference,
|
||||
protected readonly _debugService: BreakpointManager,
|
||||
) { }
|
||||
|
||||
renderTemplate(container: HTMLElement): BreakpointColumnTemplateData {
|
||||
// align from the bottom so that it lines up with instruction when source code is present.
|
||||
container.style.alignSelf = 'flex-end';
|
||||
|
||||
const icon = append(container, $('.disassembly-view'));
|
||||
icon.classList.add('codicon');
|
||||
icon.style.display = 'flex';
|
||||
icon.style.alignItems = 'center';
|
||||
icon.style.justifyContent = 'center';
|
||||
icon.style.height = this._disassemblyView.fontInfo.lineHeight + 'px';
|
||||
|
||||
const currentElement: { element?: DisassembledInstructionEntry } = { element: undefined };
|
||||
|
||||
const disposables = [
|
||||
this._disassemblyView.onDidChangeStackFrame(() => this.rerenderDebugStackframe(icon, currentElement.element)),
|
||||
addStandardDisposableListener(container, 'mouseover', () => {
|
||||
if (currentElement.element?.allowBreakpoint) {
|
||||
icon.classList.add(this._breakpointHintIcon);
|
||||
}
|
||||
}),
|
||||
addStandardDisposableListener(container, 'mouseout', () => {
|
||||
if (currentElement.element?.allowBreakpoint) {
|
||||
icon.classList.remove(this._breakpointHintIcon);
|
||||
}
|
||||
}),
|
||||
addStandardDisposableListener(container, 'click', () => {
|
||||
if (currentElement.element?.allowBreakpoint) {
|
||||
// click show hint while waiting for BP to resolve.
|
||||
icon.classList.add(this._breakpointHintIcon);
|
||||
if (currentElement.element.isBreakpointSet) {
|
||||
this._debugService.removeInstructionBreakpoint(currentElement.element.instruction.address);
|
||||
|
||||
} else if (currentElement.element.allowBreakpoint && !currentElement.element.isBreakpointSet) {
|
||||
this._debugService.addInstructionBreakpoint(currentElement.element.instruction.address, 0);
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
return { currentElement, icon, disposables };
|
||||
}
|
||||
|
||||
renderElement(element: DisassembledInstructionEntry, index: number, templateData: BreakpointColumnTemplateData, height: number | undefined): void {
|
||||
templateData.currentElement.element = element;
|
||||
this.rerenderDebugStackframe(templateData.icon, element);
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: BreakpointColumnTemplateData): void {
|
||||
dispose(templateData.disposables);
|
||||
templateData.disposables = [];
|
||||
}
|
||||
|
||||
protected rerenderDebugStackframe(icon: HTMLElement, element?: DisassembledInstructionEntry): void {
|
||||
if (element?.instruction.address === this._disassemblyView.focusedCurrentInstructionAddress) {
|
||||
icon.classList.add(this._debugStackframe);
|
||||
} else if (element?.instruction.address === this._disassemblyView.focusedInstructionAddress) {
|
||||
icon.classList.add(this._debugStackframeFocused);
|
||||
} else {
|
||||
icon.classList.remove(this._debugStackframe);
|
||||
icon.classList.remove(this._debugStackframeFocused);
|
||||
}
|
||||
|
||||
icon.classList.remove(this._breakpointHintIcon);
|
||||
|
||||
if (element?.isBreakpointSet) {
|
||||
if (element.isBreakpointEnabled) {
|
||||
icon.classList.add(this._breakpointIcon);
|
||||
icon.classList.remove(this._breakpointDisabledIcon);
|
||||
} else {
|
||||
icon.classList.remove(this._breakpointIcon);
|
||||
icon.classList.add(this._breakpointDisabledIcon);
|
||||
}
|
||||
} else {
|
||||
icon.classList.remove(this._breakpointIcon);
|
||||
icon.classList.remove(this._breakpointDisabledIcon);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { AbstractViewContribution, bindViewContribution, WidgetFactory } from '@theia/core/lib/browser';
|
||||
import { DisassemblyViewWidget } from './disassembly-view-widget';
|
||||
import { Command, CommandRegistry, MenuModelRegistry, nls } from '@theia/core';
|
||||
import { DebugService } from '../../common/debug-service';
|
||||
import { EditorManager, EDITOR_CONTEXT_MENU } from '@theia/editor/lib/browser';
|
||||
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
||||
import { DebugSessionManager } from '../debug-session-manager';
|
||||
import { DebugStackFrame } from '../model/debug-stack-frame';
|
||||
import { DebugSession, DebugState } from '../debug-session';
|
||||
import { DebugStackFramesWidget } from '../view/debug-stack-frames-widget';
|
||||
|
||||
export const OPEN_DISASSEMBLY_VIEW_COMMAND: Command = {
|
||||
id: 'open-disassembly-view',
|
||||
label: nls.localizeByDefault('Open Disassembly View')
|
||||
};
|
||||
|
||||
export const LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST = 'languageSupportsDisassembleRequest';
|
||||
export const FOCUSED_STACK_FRAME_HAS_INSTRUCTION_REFERENCE = 'focusedStackFrameHasInstructionReference';
|
||||
export const DISASSEMBLE_REQUEST_SUPPORTED = 'disassembleRequestSupported';
|
||||
export const DISASSEMBLY_VIEW_FOCUS = 'disassemblyViewFocus';
|
||||
|
||||
@injectable()
|
||||
export class DisassemblyViewContribution extends AbstractViewContribution<DisassemblyViewWidget> {
|
||||
@inject(DebugService) protected readonly debugService: DebugService;
|
||||
@inject(EditorManager) protected readonly editorManager: EditorManager;
|
||||
@inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService;
|
||||
@inject(DebugSessionManager) protected readonly debugSessionManager: DebugSessionManager;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: DisassemblyViewWidget.ID,
|
||||
widgetName: nls.localizeByDefault('Disassembly View'),
|
||||
defaultWidgetOptions: { area: 'main' }
|
||||
});
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
let activeEditorChangeCancellation = { cancelled: false };
|
||||
const updateLanguageSupportsDisassemblyKey = async () => {
|
||||
const editor = this.editorManager.currentEditor;
|
||||
activeEditorChangeCancellation.cancelled = true;
|
||||
const localCancellation = activeEditorChangeCancellation = { cancelled: false };
|
||||
|
||||
const language = editor?.editor.document.languageId;
|
||||
const debuggersForLanguage = language && await this.debugService.getDebuggersForLanguage(language);
|
||||
if (!localCancellation.cancelled) {
|
||||
this.contextKeyService.setContext(LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST, Boolean(debuggersForLanguage?.length));
|
||||
}
|
||||
};
|
||||
this.editorManager.onCurrentEditorChanged(updateLanguageSupportsDisassemblyKey);
|
||||
this.debugService.onDidChangeDebuggers?.(updateLanguageSupportsDisassemblyKey);
|
||||
let lastSession: DebugSession | undefined;
|
||||
let lastFrame: DebugStackFrame | undefined;
|
||||
this.debugSessionManager.onDidChange(() => {
|
||||
const { currentFrame, currentSession } = this.debugSessionManager;
|
||||
if (currentFrame !== lastFrame) {
|
||||
lastFrame = currentFrame;
|
||||
this.contextKeyService.setContext(FOCUSED_STACK_FRAME_HAS_INSTRUCTION_REFERENCE, Boolean(currentFrame?.raw.instructionPointerReference));
|
||||
}
|
||||
if (currentSession !== lastSession) {
|
||||
lastSession = currentSession;
|
||||
this.contextKeyService.setContext(DISASSEMBLE_REQUEST_SUPPORTED, Boolean(currentSession?.capabilities.supportsDisassembleRequest));
|
||||
}
|
||||
});
|
||||
this.shell.onDidChangeCurrentWidget(widget => {
|
||||
this.contextKeyService.setContext(DISASSEMBLY_VIEW_FOCUS, widget instanceof DisassemblyViewWidget);
|
||||
});
|
||||
}
|
||||
|
||||
override registerCommands(commands: CommandRegistry): void {
|
||||
commands.registerCommand(OPEN_DISASSEMBLY_VIEW_COMMAND, {
|
||||
isEnabled: () => this.debugSessionManager.inDebugMode
|
||||
&& this.debugSessionManager.state === DebugState.Stopped
|
||||
&& this.contextKeyService.match('focusedStackFrameHasInstructionReference'),
|
||||
execute: () => this.openView({ activate: true }),
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(menus: MenuModelRegistry): void {
|
||||
menus.registerMenuAction(DebugStackFramesWidget.CONTEXT_MENU,
|
||||
{ commandId: OPEN_DISASSEMBLY_VIEW_COMMAND.id, label: OPEN_DISASSEMBLY_VIEW_COMMAND.label });
|
||||
menus.registerMenuAction([...EDITOR_CONTEXT_MENU, 'a_debug'],
|
||||
{ commandId: OPEN_DISASSEMBLY_VIEW_COMMAND.id, label: OPEN_DISASSEMBLY_VIEW_COMMAND.label, when: LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST });
|
||||
}
|
||||
}
|
||||
|
||||
export function bindDisassemblyView(bind: interfaces.Bind): void {
|
||||
bind(DisassemblyViewWidget).toSelf();
|
||||
bind(WidgetFactory).toDynamicValue(({ container }) => ({ id: DisassemblyViewWidget.ID, createWidget: () => container.get(DisassemblyViewWidget) }));
|
||||
bindViewContribution(bind, DisassemblyViewContribution);
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { open, OpenerService } from '@theia/core/lib/browser';
|
||||
import { URI as TheiaURI } from '@theia/core/lib/common/uri';
|
||||
import { EditorOpenerOptions } from '@theia/editor/lib/browser';
|
||||
import { IDisposable, Uri as URI } from '@theia/monaco-editor-core';
|
||||
import { $, addStandardDisposableListener, append } from '@theia/monaco-editor-core/esm/vs/base/browser/dom';
|
||||
import { ITableRenderer } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/table/table';
|
||||
import { Color } from '@theia/monaco-editor-core/esm/vs/base/common/color';
|
||||
import { Disposable, dispose } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
|
||||
import { isAbsolute } from '@theia/monaco-editor-core/esm/vs/base/common/path';
|
||||
import { Constants } from '@theia/monaco-editor-core/esm/vs/base/common/uint';
|
||||
import { applyFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/browser/config/domFontInfo';
|
||||
import { StringBuilder } from '@theia/monaco-editor-core/esm/vs/editor/common/core/stringBuilder';
|
||||
import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
|
||||
import { ITextModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/resolverService';
|
||||
import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol';
|
||||
import { DebugSource } from '../model/debug-source';
|
||||
import { DisassembledInstructionEntry, DisassemblyViewRendererReference, InstructionColumnTemplateData } from './disassembly-view-utilities';
|
||||
|
||||
// This file is adapted from https://github.com/microsoft/vscode/blob/c061ce5c24fc480342fbc5f23244289d633c56eb/src/vs/workbench/contrib/debug/browser/disassemblyView.ts
|
||||
|
||||
const topStackFrameColor = 'editor.stackFrameHighlightBackground';
|
||||
const focusedStackFrameColor = 'editor.focusedStackFrameHighlightBackground';
|
||||
|
||||
export class InstructionRenderer extends Disposable implements ITableRenderer<DisassembledInstructionEntry, InstructionColumnTemplateData> {
|
||||
|
||||
static readonly TEMPLATE_ID = 'instruction';
|
||||
|
||||
protected static readonly INSTRUCTION_ADDR_MIN_LENGTH = 25;
|
||||
protected static readonly INSTRUCTION_BYTES_MIN_LENGTH = 30;
|
||||
|
||||
templateId: string = InstructionRenderer.TEMPLATE_ID;
|
||||
|
||||
protected _topStackFrameColor: Color | undefined;
|
||||
protected _focusedStackFrameColor: Color | undefined;
|
||||
|
||||
constructor(
|
||||
protected readonly _disassemblyView: DisassemblyViewRendererReference,
|
||||
protected readonly openerService: OpenerService,
|
||||
protected readonly uriService: { asCanonicalUri(uri: URI): URI },
|
||||
@IThemeService themeService: IThemeService,
|
||||
@ITextModelService protected readonly textModelService: ITextModelService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._topStackFrameColor = themeService.getColorTheme().getColor(topStackFrameColor);
|
||||
this._focusedStackFrameColor = themeService.getColorTheme().getColor(focusedStackFrameColor);
|
||||
|
||||
this._register(themeService.onDidColorThemeChange(e => {
|
||||
this._topStackFrameColor = e.getColor(topStackFrameColor);
|
||||
this._focusedStackFrameColor = e.getColor(focusedStackFrameColor);
|
||||
}));
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): InstructionColumnTemplateData {
|
||||
const sourcecode = append(container, $('.sourcecode'));
|
||||
const instruction = append(container, $('.instruction'));
|
||||
this.applyFontInfo(sourcecode);
|
||||
this.applyFontInfo(instruction);
|
||||
const currentElement: { element?: DisassembledInstructionEntry } = { element: undefined };
|
||||
const cellDisposable: IDisposable[] = [];
|
||||
|
||||
const disposables = [
|
||||
this._disassemblyView.onDidChangeStackFrame(() => this.rerenderBackground(instruction, sourcecode, currentElement.element)),
|
||||
addStandardDisposableListener(sourcecode, 'dblclick', () => this.openSourceCode(currentElement.element?.instruction!)),
|
||||
];
|
||||
|
||||
return { currentElement, instruction, sourcecode, cellDisposable, disposables };
|
||||
}
|
||||
|
||||
renderElement(element: DisassembledInstructionEntry, index: number, templateData: InstructionColumnTemplateData, height: number | undefined): void {
|
||||
this.renderElementInner(element, index, templateData, height);
|
||||
}
|
||||
|
||||
protected async renderElementInner(element: DisassembledInstructionEntry, index: number, column: InstructionColumnTemplateData, height: number | undefined): Promise<void> {
|
||||
column.currentElement.element = element;
|
||||
const instruction = element.instruction;
|
||||
column.sourcecode.innerText = '';
|
||||
const sb = new StringBuilder(1000);
|
||||
|
||||
if (this._disassemblyView.isSourceCodeRender && instruction.location?.path && instruction.line) {
|
||||
const sourceURI = this.getUriFromSource(instruction);
|
||||
|
||||
if (sourceURI) {
|
||||
let textModel: ITextModel | undefined = undefined;
|
||||
const sourceSB = new StringBuilder(10000);
|
||||
const ref = await this.textModelService.createModelReference(sourceURI);
|
||||
textModel = ref.object.textEditorModel;
|
||||
column.cellDisposable.push(ref);
|
||||
|
||||
// templateData could have moved on during async. Double check if it is still the same source.
|
||||
if (textModel && column.currentElement.element === element) {
|
||||
let lineNumber = instruction.line;
|
||||
|
||||
while (lineNumber && lineNumber >= 1 && lineNumber <= textModel.getLineCount()) {
|
||||
const lineContent = textModel.getLineContent(lineNumber);
|
||||
sourceSB.appendString(` ${lineNumber}: `);
|
||||
sourceSB.appendString(lineContent + '\n');
|
||||
|
||||
if (instruction.endLine && lineNumber < instruction.endLine) {
|
||||
lineNumber++;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
column.sourcecode.innerText = sourceSB.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let spacesToAppend = 10;
|
||||
|
||||
if (instruction.address !== '-1') {
|
||||
sb.appendString(instruction.address);
|
||||
if (instruction.address.length < InstructionRenderer.INSTRUCTION_ADDR_MIN_LENGTH) {
|
||||
spacesToAppend = InstructionRenderer.INSTRUCTION_ADDR_MIN_LENGTH - instruction.address.length;
|
||||
}
|
||||
for (let i = 0; i < spacesToAppend; i++) {
|
||||
sb.appendString(' ');
|
||||
}
|
||||
}
|
||||
|
||||
if (instruction.instructionBytes) {
|
||||
sb.appendString(instruction.instructionBytes);
|
||||
spacesToAppend = 10;
|
||||
if (instruction.instructionBytes.length < InstructionRenderer.INSTRUCTION_BYTES_MIN_LENGTH) {
|
||||
spacesToAppend = InstructionRenderer.INSTRUCTION_BYTES_MIN_LENGTH - instruction.instructionBytes.length;
|
||||
}
|
||||
for (let i = 0; i < spacesToAppend; i++) {
|
||||
sb.appendString(' ');
|
||||
}
|
||||
}
|
||||
|
||||
sb.appendString(instruction.instruction);
|
||||
column.instruction.innerText = sb.build();
|
||||
|
||||
this.rerenderBackground(column.instruction, column.sourcecode, element);
|
||||
}
|
||||
|
||||
disposeElement(element: DisassembledInstructionEntry, index: number, templateData: InstructionColumnTemplateData, height: number | undefined): void {
|
||||
dispose(templateData.cellDisposable);
|
||||
templateData.cellDisposable = [];
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: InstructionColumnTemplateData): void {
|
||||
dispose(templateData.disposables);
|
||||
templateData.disposables = [];
|
||||
}
|
||||
|
||||
protected rerenderBackground(instruction: HTMLElement, sourceCode: HTMLElement, element?: DisassembledInstructionEntry): void {
|
||||
if (element && this._disassemblyView.currentInstructionAddresses.includes(element.instruction.address)) {
|
||||
instruction.style.background = this._topStackFrameColor?.toString() || 'transparent';
|
||||
} else if (element?.instruction.address === this._disassemblyView.focusedInstructionAddress) {
|
||||
instruction.style.background = this._focusedStackFrameColor?.toString() || 'transparent';
|
||||
} else {
|
||||
instruction.style.background = 'transparent';
|
||||
}
|
||||
}
|
||||
|
||||
protected openSourceCode(instruction: DebugProtocol.DisassembledInstruction | undefined): void {
|
||||
if (instruction) {
|
||||
const sourceURI = this.getUriFromSource(instruction);
|
||||
const selection: EditorOpenerOptions['selection'] = instruction.endLine ? {
|
||||
start: { line: instruction.line!, character: instruction.column ?? 0 },
|
||||
end: { line: instruction.endLine, character: instruction.endColumn ?? Constants.MAX_SAFE_SMALL_INTEGER }
|
||||
} : {
|
||||
start: { line: instruction.line!, character: instruction.column ?? 0 },
|
||||
end: { line: instruction.line, character: instruction.endColumn ?? Constants.MAX_SAFE_SMALL_INTEGER }
|
||||
};
|
||||
|
||||
const openerOptions: EditorOpenerOptions = {
|
||||
selection,
|
||||
mode: 'activate',
|
||||
widgetOptions: { area: 'main' }
|
||||
};
|
||||
open(this.openerService, new TheiaURI(sourceURI.toString()), openerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
protected getUriFromSource(instruction: DebugProtocol.DisassembledInstruction): URI {
|
||||
// Try to resolve path before consulting the debugSession.
|
||||
const path = instruction.location!.path;
|
||||
if (path && isUri(path)) { // path looks like a uri
|
||||
return this.uriService.asCanonicalUri(URI.parse(path));
|
||||
}
|
||||
// assume a filesystem path
|
||||
if (path && isAbsolute(path)) {
|
||||
return this.uriService.asCanonicalUri(URI.file(path));
|
||||
}
|
||||
|
||||
return getUriFromSource(instruction.location!, instruction.location!.path, this._disassemblyView.debugSession!.id, this.uriService);
|
||||
}
|
||||
|
||||
protected applyFontInfo(element: HTMLElement): void {
|
||||
applyFontInfo(element, this._disassemblyView.fontInfo);
|
||||
element.style.whiteSpace = 'pre';
|
||||
}
|
||||
}
|
||||
|
||||
export function getUriFromSource(raw: DebugProtocol.Source, path: string | undefined, sessionId: string, uriIdentityService: { asCanonicalUri(uri: URI): URI }): URI {
|
||||
if (typeof raw.sourceReference === 'number' && raw.sourceReference > 0) {
|
||||
return URI.from({
|
||||
scheme: DebugSource.SCHEME,
|
||||
path,
|
||||
query: `session=${sessionId}&ref=${raw.sourceReference}`
|
||||
});
|
||||
}
|
||||
|
||||
if (path && isUri(path)) { // path looks like a uri
|
||||
return uriIdentityService.asCanonicalUri(URI.parse(path));
|
||||
}
|
||||
// assume a filesystem path
|
||||
if (path && isAbsolute(path)) {
|
||||
return uriIdentityService.asCanonicalUri(URI.file(path));
|
||||
}
|
||||
// path is relative: since VS Code cannot deal with this by itself
|
||||
// create a debug url that will result in a DAP 'source' request when the url is resolved.
|
||||
return uriIdentityService.asCanonicalUri(URI.from({
|
||||
scheme: DebugSource.SCHEME,
|
||||
path,
|
||||
query: `session=${sessionId}`
|
||||
}));
|
||||
}
|
||||
|
||||
function isUri(candidate: string | undefined): boolean {
|
||||
return Boolean(candidate && candidate.match(DebugSource.SCHEME_PATTERN));
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ITableVirtualDelegate } from '@theia/monaco-editor-core/esm/vs/base/browser/ui/table/table';
|
||||
import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo';
|
||||
import { DisassembledInstructionEntry } from './disassembly-view-utilities';
|
||||
|
||||
// This file is adapted from https://github.com/microsoft/vscode/blob/c061ce5c24fc480342fbc5f23244289d633c56eb/src/vs/workbench/contrib/debug/browser/disassemblyView.ts
|
||||
|
||||
export class DisassemblyViewTableDelegate implements ITableVirtualDelegate<DisassembledInstructionEntry> {
|
||||
constructor(protected readonly fontInfoProvider: { fontInfo: BareFontInfo, isSourceCodeRender: boolean }) { }
|
||||
|
||||
headerRowHeight = 0;
|
||||
|
||||
getHeight(row: DisassembledInstructionEntry): number {
|
||||
if (this.fontInfoProvider.isSourceCodeRender && row.instruction.location?.path && row.instruction.line !== undefined) {
|
||||
if (row.instruction.endLine !== undefined) {
|
||||
return this.fontInfoProvider.fontInfo.lineHeight + (row.instruction.endLine - row.instruction.line + 2);
|
||||
} else {
|
||||
return this.fontInfoProvider.fontInfo.lineHeight * 2;
|
||||
}
|
||||
}
|
||||
|
||||
return this.fontInfoProvider.fontInfo.lineHeight;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { IDisposable, IEvent } from '@theia/monaco-editor-core';
|
||||
import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol';
|
||||
|
||||
export interface DisassemblyViewRendererReference {
|
||||
onDidChangeStackFrame: IEvent<void>;
|
||||
isSourceCodeRender: boolean;
|
||||
currentInstructionAddresses: Array<string | undefined>;
|
||||
focusedInstructionAddress: string | undefined;
|
||||
focusedCurrentInstructionAddress: string | undefined;
|
||||
debugSession: { id: string } | undefined;
|
||||
fontInfo: BareFontInfo;
|
||||
}
|
||||
|
||||
// The rest of the file is adapted from https://github.com/microsoft/vscode/blob/c061ce5c24fc480342fbc5f23244289d633c56eb/src/vs/workbench/contrib/debug/browser/disassemblyView.ts
|
||||
export interface DisassembledInstructionEntry {
|
||||
allowBreakpoint: boolean;
|
||||
isBreakpointSet: boolean;
|
||||
isBreakpointEnabled: boolean;
|
||||
instruction: DebugProtocol.DisassembledInstruction;
|
||||
instructionAddress?: bigint;
|
||||
}
|
||||
|
||||
export interface InstructionColumnTemplateData {
|
||||
currentElement: { element?: DisassembledInstructionEntry };
|
||||
// TODO: hover widget?
|
||||
instruction: HTMLElement;
|
||||
sourcecode: HTMLElement;
|
||||
// disposed when cell is closed.
|
||||
cellDisposable: IDisposable[];
|
||||
// disposed when template is closed.
|
||||
disposables: IDisposable[];
|
||||
}
|
||||
|
||||
export interface BreakpointColumnTemplateData {
|
||||
currentElement: { element?: DisassembledInstructionEntry };
|
||||
icon: HTMLElement;
|
||||
disposables: IDisposable[];
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { BaseWidget, LabelProvider, Message, OpenerService, Widget } from '@theia/core/lib/browser';
|
||||
import { ArrayUtils } from '@theia/core/lib/common/types';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol';
|
||||
import { InstructionBreakpoint } from '../breakpoint/breakpoint-marker';
|
||||
import { BreakpointManager } from '../breakpoint/breakpoint-manager';
|
||||
import { DebugSessionManager } from '../debug-session-manager';
|
||||
import { Emitter, IDisposable, IRange, Range, Uri } from '@theia/monaco-editor-core';
|
||||
import { nls } from '@theia/core';
|
||||
import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo';
|
||||
import { WorkbenchTable } from '@theia/monaco-editor-core/esm/vs/platform/list/browser/listService';
|
||||
import { DebugState, DebugSession } from '../debug-session';
|
||||
import { EditorPreferences } from '@theia/editor/lib/common/editor-preferences';
|
||||
import { PixelRatio } from '@theia/monaco-editor-core/esm/vs/base/browser/pixelRatio';
|
||||
import { DebugPreferences } from '../../common/debug-preferences';
|
||||
import { DebugThread } from '../model/debug-thread';
|
||||
import { Event } from '@theia/monaco-editor-core/esm/vs/base/common/event';
|
||||
import { DisassembledInstructionEntry } from './disassembly-view-utilities';
|
||||
import { DisassemblyViewTableDelegate } from './disassembly-view-table-delegate';
|
||||
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
|
||||
import { InstructionRenderer } from './disassembly-view-instruction-renderer';
|
||||
import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation';
|
||||
import { BreakpointRenderer } from './disassembly-view-breakpoint-renderer';
|
||||
import { AccessibilityProvider } from './disassembly-view-accessibility-provider';
|
||||
import { editorBackground } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/colorRegistry';
|
||||
import { Dimension } from '@theia/monaco-editor-core/esm/vs/base/browser/dom';
|
||||
import { URI } from '@theia/core/lib/common/uri';
|
||||
|
||||
// This file is adapted from https://github.com/microsoft/vscode/blob/c061ce5c24fc480342fbc5f23244289d633c56eb/src/vs/workbench/contrib/debug/browser/disassemblyView.ts
|
||||
|
||||
// Special entry as a placeholder when disassembly is not available
|
||||
const disassemblyNotAvailable: DisassembledInstructionEntry = {
|
||||
allowBreakpoint: false,
|
||||
isBreakpointSet: false,
|
||||
isBreakpointEnabled: false,
|
||||
instruction: {
|
||||
address: '-1',
|
||||
instruction: nls.localizeByDefault('Disassembly not available.')
|
||||
},
|
||||
instructionAddress: BigInt(-1)
|
||||
} as const;
|
||||
|
||||
@injectable()
|
||||
export class DisassemblyViewWidget extends BaseWidget {
|
||||
static readonly ID = 'disassembly-view-widget';
|
||||
protected static readonly NUM_INSTRUCTIONS_TO_LOAD = 50;
|
||||
protected readonly iconReferenceUri = new URI().withScheme('file').withPath('disassembly-view.disassembly-view');
|
||||
|
||||
@inject(BreakpointManager) protected readonly breakpointManager: BreakpointManager;
|
||||
@inject(DebugSessionManager) protected readonly debugSessionManager: DebugSessionManager;
|
||||
@inject(EditorPreferences) protected readonly editorPreferences: EditorPreferences;
|
||||
@inject(DebugPreferences) protected readonly debugPreferences: DebugPreferences;
|
||||
@inject(OpenerService) protected readonly openerService: OpenerService;
|
||||
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
|
||||
|
||||
protected _fontInfo: BareFontInfo;
|
||||
protected _disassembledInstructions: WorkbenchTable<DisassembledInstructionEntry> | undefined = undefined;
|
||||
protected _onDidChangeStackFrame = new Emitter<void>();
|
||||
protected _previousDebuggingState: DebugState;
|
||||
protected _instructionBpList: readonly InstructionBreakpoint[] = [];
|
||||
protected _enableSourceCodeRender: boolean = true;
|
||||
protected _loadingLock: boolean = false;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.id = DisassemblyViewWidget.ID;
|
||||
this.addClass(DisassemblyViewWidget.ID);
|
||||
this.title.closable = true;
|
||||
this.title.label = nls.localizeByDefault('Disassembly');
|
||||
const updateIcon = () => this.title.iconClass = this.labelProvider.getIcon(this.iconReferenceUri) + ' file-icon';
|
||||
updateIcon();
|
||||
this.toDispose.push(this.labelProvider.onDidChange(updateIcon));
|
||||
this.node.tabIndex = -1;
|
||||
this.node.style.outline = 'none';
|
||||
this._previousDebuggingState = this.debugSessionManager.currentSession?.state ?? DebugState.Inactive;
|
||||
this._fontInfo = BareFontInfo.createFromRawSettings(this.toFontInfo(), PixelRatio.getInstance(window).value);
|
||||
this.editorPreferences.onPreferenceChanged(() => this._fontInfo = BareFontInfo.createFromRawSettings(this.toFontInfo(), PixelRatio.getInstance(window).value));
|
||||
this.debugPreferences.onPreferenceChanged(e => {
|
||||
if (e.preferenceName === 'debug.disassemblyView.showSourceCode') {
|
||||
const showSourceCode = this.debugPreferences['debug.disassemblyView.showSourceCode'];
|
||||
if (showSourceCode !== this._enableSourceCodeRender) {
|
||||
this._enableSourceCodeRender = showSourceCode;
|
||||
this.reloadDisassembly(undefined);
|
||||
}
|
||||
} else {
|
||||
this._disassembledInstructions?.rerender();
|
||||
}
|
||||
});
|
||||
this.createPane();
|
||||
}
|
||||
|
||||
get fontInfo(): BareFontInfo { return this._fontInfo; }
|
||||
|
||||
get currentInstructionAddresses(): Array<string | undefined> {
|
||||
return this.debugSessionManager.sessions
|
||||
.map(session => session.getThreads(() => true))
|
||||
.reduce<DebugThread[]>((prev, curr) => prev.concat(Array.from(curr)), [])
|
||||
.map(thread => thread.topFrame)
|
||||
.map(frame => frame?.raw.instructionPointerReference);
|
||||
}
|
||||
|
||||
get focusedCurrentInstructionAddress(): string | undefined {
|
||||
return this.debugSessionManager.currentFrame?.thread.topFrame?.raw.instructionPointerReference;
|
||||
}
|
||||
|
||||
get isSourceCodeRender(): boolean { return this._enableSourceCodeRender; }
|
||||
|
||||
get debugSession(): DebugSession | undefined { return this.debugSessionManager.currentSession; }
|
||||
|
||||
get focusedInstructionAddress(): string | undefined {
|
||||
return this.debugSessionManager.currentFrame?.raw.instructionPointerReference;
|
||||
}
|
||||
|
||||
get onDidChangeStackFrame(): Event<void> { return this._onDidChangeStackFrame.event; }
|
||||
|
||||
protected createPane(): void {
|
||||
this._enableSourceCodeRender = this.debugPreferences['debug.disassemblyView.showSourceCode'];
|
||||
const monacoInstantiationService = StandaloneServices.get(IInstantiationService);
|
||||
const tableDelegate = new DisassemblyViewTableDelegate(this);
|
||||
const instructionRenderer = monacoInstantiationService.createInstance(InstructionRenderer, this, this.openerService, { asCanonicalUri(thing: Uri): Uri { return thing; } });
|
||||
this.toDispose.push(instructionRenderer);
|
||||
this.getTable(monacoInstantiationService, tableDelegate, instructionRenderer);
|
||||
this.reloadDisassembly();
|
||||
this._register(this._disassembledInstructions!.onDidScroll(e => {
|
||||
if (this._loadingLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.oldScrollTop > e.scrollTop && e.scrollTop < e.height) {
|
||||
this._loadingLock = true;
|
||||
const topElement = Math.floor(e.scrollTop / this.fontInfo.lineHeight) + DisassemblyViewWidget.NUM_INSTRUCTIONS_TO_LOAD;
|
||||
this.scrollUp_LoadDisassembledInstructions(DisassemblyViewWidget.NUM_INSTRUCTIONS_TO_LOAD).then(success => {
|
||||
if (success) {
|
||||
this._disassembledInstructions!.reveal(topElement, 0);
|
||||
}
|
||||
this._loadingLock = false;
|
||||
});
|
||||
} else if (e.oldScrollTop < e.scrollTop && e.scrollTop + e.height > e.scrollHeight - e.height) {
|
||||
this._loadingLock = true;
|
||||
this.scrollDown_LoadDisassembledInstructions(DisassemblyViewWidget.NUM_INSTRUCTIONS_TO_LOAD).then(() => { this._loadingLock = false; });
|
||||
}
|
||||
}));
|
||||
this._register(this.debugSessionManager.onDidFocusStackFrame(() => {
|
||||
if (this._disassembledInstructions) {
|
||||
this.goToAddress();
|
||||
this._onDidChangeStackFrame.fire();
|
||||
}
|
||||
}));
|
||||
this._register(this.breakpointManager.onDidChangeInstructionBreakpoints(bpEvent => {
|
||||
if (bpEvent && this._disassembledInstructions) {
|
||||
// draw viewable BP
|
||||
let changed = false;
|
||||
bpEvent.added?.forEach(bp => {
|
||||
if (InstructionBreakpoint.is(bp)) {
|
||||
const index = this.getIndexFromAddress(bp.instructionReference);
|
||||
if (index >= 0) {
|
||||
this._disassembledInstructions!.row(index).isBreakpointSet = true;
|
||||
this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bpEvent.removed?.forEach(bp => {
|
||||
if (InstructionBreakpoint.is(bp)) {
|
||||
const index = this.getIndexFromAddress(bp.instructionReference);
|
||||
if (index >= 0) {
|
||||
this._disassembledInstructions!.row(index).isBreakpointSet = false;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bpEvent.changed?.forEach(bp => {
|
||||
if (InstructionBreakpoint.is(bp)) {
|
||||
const index = this.getIndexFromAddress(bp.instructionReference);
|
||||
if (index >= 0) {
|
||||
if (this._disassembledInstructions!.row(index).isBreakpointEnabled !== bp.enabled) {
|
||||
this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// get an updated list so that items beyond the current range would render when reached.
|
||||
this._instructionBpList = this.breakpointManager.getInstructionBreakpoints();
|
||||
|
||||
if (changed) {
|
||||
this._onDidChangeStackFrame.fire();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// This would like to be more specific: onDidChangeState
|
||||
this._register(this.debugSessionManager.onDidChange(() => {
|
||||
const state = this.debugSession?.state;
|
||||
|
||||
if ((state === DebugState.Running || state === DebugState.Stopped) &&
|
||||
(this._previousDebuggingState !== DebugState.Running && this._previousDebuggingState !== DebugState.Stopped)) {
|
||||
// Just started debugging, clear the view
|
||||
this._disassembledInstructions?.splice(0, this._disassembledInstructions.length, [disassemblyNotAvailable]);
|
||||
this._enableSourceCodeRender = this.debugPreferences['debug.disassemblyView.showSourceCode'];
|
||||
}
|
||||
if (state !== undefined && state !== this._previousDebuggingState) {
|
||||
this._previousDebuggingState = state;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
protected getTable(
|
||||
monacoInstantiationService: IInstantiationService,
|
||||
tableDelegate: DisassemblyViewTableDelegate,
|
||||
instructionRenderer: InstructionRenderer
|
||||
): WorkbenchTable<DisassembledInstructionEntry> {
|
||||
return this._disassembledInstructions = this._register(monacoInstantiationService.createInstance(WorkbenchTable,
|
||||
'DisassemblyView', this.node, tableDelegate,
|
||||
[
|
||||
{
|
||||
label: '',
|
||||
tooltip: '',
|
||||
weight: 0,
|
||||
minimumWidth: this.fontInfo.lineHeight,
|
||||
maximumWidth: this.fontInfo.lineHeight,
|
||||
templateId: BreakpointRenderer.TEMPLATE_ID,
|
||||
project(row: DisassembledInstructionEntry): DisassembledInstructionEntry { return row; }
|
||||
},
|
||||
{
|
||||
label: nls.localizeByDefault('instructions'),
|
||||
tooltip: '',
|
||||
weight: 0.3,
|
||||
templateId: InstructionRenderer.TEMPLATE_ID,
|
||||
project(row: DisassembledInstructionEntry): DisassembledInstructionEntry { return row; }
|
||||
},
|
||||
],
|
||||
[
|
||||
new BreakpointRenderer(this, this.breakpointManager),
|
||||
instructionRenderer,
|
||||
],
|
||||
{
|
||||
identityProvider: { getId: (e: DisassembledInstructionEntry) => e.instruction.address },
|
||||
horizontalScrolling: false,
|
||||
overrideStyles: {
|
||||
listBackground: editorBackground
|
||||
},
|
||||
multipleSelectionSupport: false,
|
||||
setRowLineHeight: false,
|
||||
openOnSingleClick: false,
|
||||
accessibilityProvider: new AccessibilityProvider(),
|
||||
mouseSupport: false
|
||||
}
|
||||
)) as WorkbenchTable<DisassembledInstructionEntry>;
|
||||
}
|
||||
|
||||
adjustLayout(dimension: Dimension): void {
|
||||
if (this._disassembledInstructions) {
|
||||
this._disassembledInstructions.layout(dimension.height);
|
||||
}
|
||||
}
|
||||
|
||||
goToAddress(address?: string, focus?: boolean): void {
|
||||
if (!this._disassembledInstructions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!address) {
|
||||
address = this.focusedInstructionAddress;
|
||||
}
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this.getIndexFromAddress(address);
|
||||
if (index >= 0) {
|
||||
this._disassembledInstructions.reveal(index);
|
||||
|
||||
if (focus) {
|
||||
this._disassembledInstructions.domFocus();
|
||||
this._disassembledInstructions.setFocus([index]);
|
||||
}
|
||||
} else if (this.debugSessionManager.state === DebugState.Stopped) {
|
||||
// Address is not provided or not in the table currently, clear the table
|
||||
// and reload if we are in the state where we can load disassembly.
|
||||
this.reloadDisassembly(address);
|
||||
}
|
||||
}
|
||||
|
||||
protected async scrollUp_LoadDisassembledInstructions(instructionCount: number): Promise<boolean> {
|
||||
if (this._disassembledInstructions && this._disassembledInstructions.length > 0) {
|
||||
const address: string | undefined = this._disassembledInstructions?.row(0).instruction.address;
|
||||
return this.loadDisassembledInstructions(address, -instructionCount, instructionCount);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected async scrollDown_LoadDisassembledInstructions(instructionCount: number): Promise<boolean> {
|
||||
if (this._disassembledInstructions && this._disassembledInstructions.length > 0) {
|
||||
const address: string | undefined = this._disassembledInstructions?.row(this._disassembledInstructions?.length - 1).instruction.address;
|
||||
return this.loadDisassembledInstructions(address, 1, instructionCount);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected async loadDisassembledInstructions(memoryReference: string | undefined, instructionOffset: number, instructionCount: number): Promise<boolean> {
|
||||
// if address is null, then use current stack frame.
|
||||
if (!memoryReference || memoryReference === '-1') {
|
||||
memoryReference = this.focusedInstructionAddress;
|
||||
}
|
||||
if (!memoryReference) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const session = this.debugSession;
|
||||
const resultEntries = (await session?.sendRequest('disassemble', {
|
||||
instructionCount,
|
||||
memoryReference,
|
||||
instructionOffset,
|
||||
offset: 0,
|
||||
resolveSymbols: true,
|
||||
}))?.body?.instructions;
|
||||
if (session && resultEntries && this._disassembledInstructions) {
|
||||
const newEntries: DisassembledInstructionEntry[] = [];
|
||||
const allowBreakpoint = Boolean(session.capabilities.supportsInstructionBreakpoints);
|
||||
|
||||
let lastLocation: DebugProtocol.Source | undefined;
|
||||
let lastLine: IRange | undefined;
|
||||
for (let i = 0; i < resultEntries.length; i++) {
|
||||
const found = this._instructionBpList.find(p => p.instructionReference === resultEntries[i].address);
|
||||
const instruction = resultEntries[i];
|
||||
|
||||
// Forward fill the missing location as detailed in the DAP spec.
|
||||
if (instruction.location) {
|
||||
lastLocation = instruction.location;
|
||||
lastLine = undefined;
|
||||
}
|
||||
|
||||
if (instruction.line) {
|
||||
const currentLine: IRange = {
|
||||
startLineNumber: instruction.line,
|
||||
startColumn: instruction.column ?? 0,
|
||||
endLineNumber: instruction.endLine ?? instruction.line!,
|
||||
endColumn: instruction.endColumn ?? 0,
|
||||
};
|
||||
|
||||
// Add location only to the first unique range. This will give the appearance of grouping of instructions.
|
||||
if (!Range.equalsRange(currentLine, lastLine ?? null)) { // eslint-disable-line no-null/no-null
|
||||
lastLine = currentLine;
|
||||
instruction.location = lastLocation;
|
||||
}
|
||||
}
|
||||
|
||||
newEntries.push({ allowBreakpoint, isBreakpointSet: found !== undefined, isBreakpointEnabled: !!found?.enabled, instruction: instruction });
|
||||
}
|
||||
|
||||
const specialEntriesToRemove = this._disassembledInstructions.length === 1 ? 1 : 0;
|
||||
|
||||
// request is either at the start or end
|
||||
if (instructionOffset >= 0) {
|
||||
this._disassembledInstructions.splice(this._disassembledInstructions.length, specialEntriesToRemove, newEntries);
|
||||
} else {
|
||||
this._disassembledInstructions.splice(0, specialEntriesToRemove, newEntries);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected getIndexFromAddress(instructionAddress: string): number {
|
||||
const disassembledInstructions = this._disassembledInstructions;
|
||||
if (disassembledInstructions && disassembledInstructions.length > 0) {
|
||||
const address = BigInt(instructionAddress);
|
||||
if (address) {
|
||||
return ArrayUtils.binarySearch2(disassembledInstructions.length, index => {
|
||||
const row = disassembledInstructions.row(index);
|
||||
|
||||
this.ensureAddressParsed(row);
|
||||
if (row.instructionAddress! > address) {
|
||||
return 1;
|
||||
} else if (row.instructionAddress! < address) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
protected ensureAddressParsed(entry: DisassembledInstructionEntry): void {
|
||||
if (entry.instructionAddress !== undefined) {
|
||||
return;
|
||||
} else {
|
||||
entry.instructionAddress = BigInt(entry.instruction.address);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the table and reload instructions near the target address
|
||||
*/
|
||||
protected reloadDisassembly(targetAddress?: string): void {
|
||||
if (this._disassembledInstructions) {
|
||||
this._loadingLock = true; // stop scrolling during the load.
|
||||
this._disassembledInstructions.splice(0, this._disassembledInstructions.length, [disassemblyNotAvailable]);
|
||||
this._instructionBpList = this.breakpointManager.getInstructionBreakpoints();
|
||||
this.loadDisassembledInstructions(targetAddress, -DisassemblyViewWidget.NUM_INSTRUCTIONS_TO_LOAD * 4, DisassemblyViewWidget.NUM_INSTRUCTIONS_TO_LOAD * 8).then(() => {
|
||||
// on load, set the target instruction in the middle of the page.
|
||||
if (this._disassembledInstructions!.length > 0) {
|
||||
const targetIndex = Math.floor(this._disassembledInstructions!.length / 2);
|
||||
this._disassembledInstructions!.reveal(targetIndex, 0.5);
|
||||
|
||||
// Always focus the target address on reload, or arrow key navigation would look terrible
|
||||
this._disassembledInstructions!.domFocus();
|
||||
this._disassembledInstructions!.setFocus([targetIndex]);
|
||||
}
|
||||
this._loadingLock = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override onResize(msg: Widget.ResizeMessage): void {
|
||||
this.adjustLayout(new Dimension(msg.width, msg.height));
|
||||
}
|
||||
|
||||
protected override onActivateRequest(msg: Message): void {
|
||||
this.node.focus();
|
||||
super.onActivateRequest(msg);
|
||||
}
|
||||
|
||||
protected toFontInfo(): Parameters<typeof BareFontInfo.createFromRawSettings>[0] {
|
||||
return {
|
||||
fontFamily: this.editorPreferences['editor.fontFamily'],
|
||||
fontWeight: String(this.editorPreferences['editor.fontWeight']),
|
||||
fontSize: this.editorPreferences['editor.fontSize'],
|
||||
fontLigatures: this.editorPreferences['editor.fontLigatures'],
|
||||
lineHeight: this.editorPreferences['editor.lineHeight'],
|
||||
letterSpacing: this.editorPreferences['editor.letterSpacing'],
|
||||
};
|
||||
}
|
||||
|
||||
protected _register<T extends IDisposable>(disposable: T): T {
|
||||
this.toDispose.push(disposable);
|
||||
return disposable;
|
||||
}
|
||||
}
|
||||
308
packages/debug/src/browser/editor/debug-breakpoint-widget.tsx
Normal file
308
packages/debug/src/browser/editor/debug-breakpoint-widget.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { createRoot, Root } from '@theia/core/shared/react-dom/client';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol';
|
||||
import { injectable, postConstruct, inject } from '@theia/core/shared/inversify';
|
||||
import { Disposable, DisposableCollection, InMemoryResources, nls } from '@theia/core';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import { MonacoEditorZoneWidget } from '@theia/monaco/lib/browser/monaco-editor-zone-widget';
|
||||
import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
|
||||
import { DebugEditor } from './debug-editor';
|
||||
import { DebugSourceBreakpoint } from '../model/debug-source-breakpoint';
|
||||
import { Dimension } from '@theia/editor/lib/browser';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { LanguageSelector } from '@theia/monaco-editor-core/esm/vs/editor/common/languageSelector';
|
||||
import { provideSuggestionItems, CompletionOptions } from '@theia/monaco-editor-core/esm/vs/editor/contrib/suggest/browser/suggest';
|
||||
import { IDecorationOptions } from '@theia/monaco-editor-core/esm/vs/editor/common/editorCommon';
|
||||
import { CompletionItemKind, CompletionContext } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
|
||||
import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures';
|
||||
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
|
||||
import { TextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model/textModel';
|
||||
import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component';
|
||||
|
||||
export type ShowDebugBreakpointOptions = DebugSourceBreakpoint | {
|
||||
position: monaco.Position,
|
||||
context: DebugBreakpointWidget.Context
|
||||
} | {
|
||||
breakpoint: DebugSourceBreakpoint,
|
||||
context: DebugBreakpointWidget.Context
|
||||
};
|
||||
|
||||
export const BREAKPOINT_INPUT_SCHEME = 'breakpointinput';
|
||||
|
||||
@injectable()
|
||||
export class DebugBreakpointWidget implements Disposable {
|
||||
|
||||
@inject(DebugEditor)
|
||||
readonly editor: DebugEditor;
|
||||
|
||||
@inject(MonacoEditorProvider)
|
||||
protected readonly editorProvider: MonacoEditorProvider;
|
||||
|
||||
@inject(InMemoryResources)
|
||||
protected readonly resources: InMemoryResources;
|
||||
|
||||
protected selectNode: HTMLDivElement;
|
||||
protected selectNodeRoot: Root;
|
||||
protected uri: URI;
|
||||
|
||||
protected zone: MonacoEditorZoneWidget;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
protected context: DebugBreakpointWidget.Context = 'condition';
|
||||
protected _values: {
|
||||
[context in DebugBreakpointWidget.Context]?: string
|
||||
} = {};
|
||||
get values(): {
|
||||
[context in DebugBreakpointWidget.Context]?: string
|
||||
} | undefined {
|
||||
if (!this._input) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...this._values,
|
||||
[this.context]: this._input.getControl().getValue()
|
||||
};
|
||||
}
|
||||
|
||||
protected _input: SimpleMonacoEditor | undefined;
|
||||
get input(): SimpleMonacoEditor | undefined {
|
||||
return this._input;
|
||||
}
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
set inputSize(dimension: Dimension | null) {
|
||||
if (this._input) {
|
||||
if (dimension) {
|
||||
this._input.setSize(dimension);
|
||||
} else {
|
||||
this._input.resizeToFit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly selectComponentRef = React.createRef<SelectComponent>();
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.doInit();
|
||||
}
|
||||
|
||||
protected async doInit(): Promise<void> {
|
||||
this.uri = new URI().withScheme(BREAKPOINT_INPUT_SCHEME).withPath(this.editor.getControl().getId());
|
||||
this.toDispose.push(this.resources.add(this.uri, ''));
|
||||
this.toDispose.push(this.zone = new MonacoEditorZoneWidget(this.editor.getControl()));
|
||||
this.zone.containerNode.classList.add('theia-debug-breakpoint-widget');
|
||||
|
||||
const selectNode = this.selectNode = document.createElement('div');
|
||||
selectNode.classList.add('theia-debug-breakpoint-select');
|
||||
this.zone.containerNode.appendChild(selectNode);
|
||||
this.selectNodeRoot = createRoot(this.selectNode);
|
||||
this.toDispose.push(Disposable.create(() => this.selectNodeRoot.unmount()));
|
||||
|
||||
const inputNode = document.createElement('div');
|
||||
inputNode.classList.add('theia-debug-breakpoint-input');
|
||||
this.zone.containerNode.appendChild(inputNode);
|
||||
|
||||
const input = this._input = await this.createInput(inputNode);
|
||||
if (this.toDispose.disposed) {
|
||||
input.dispose();
|
||||
return;
|
||||
}
|
||||
this.toDispose.push(input);
|
||||
this.toDispose.push((monaco.languages.registerCompletionItemProvider as (languageId: LanguageSelector, provider: monaco.languages.CompletionItemProvider) => Disposable)
|
||||
({ scheme: input.uri.scheme }, {
|
||||
provideCompletionItems: async (model, position, context, token): Promise<monaco.languages.CompletionList> => {
|
||||
const editor = this.editor.getControl();
|
||||
const editorModel = editor.getModel() as unknown as TextModel | undefined;
|
||||
const suggestions: monaco.languages.CompletionItem[] = [];
|
||||
if (editorModel && (this.context === 'condition' || this.context === 'logMessage')
|
||||
&& input.uri.toString() === model.uri.toString()) {
|
||||
const completions = await provideSuggestionItems(
|
||||
StandaloneServices.get(ILanguageFeaturesService).completionProvider,
|
||||
editorModel,
|
||||
new monaco.Position(editor.getPosition()!.lineNumber, 1),
|
||||
new CompletionOptions(undefined, new Set<CompletionItemKind>().add(CompletionItemKind.Snippet)),
|
||||
context as unknown as CompletionContext, token);
|
||||
let overwriteBefore = 0;
|
||||
if (this.context === 'condition') {
|
||||
overwriteBefore = position.column - 1;
|
||||
} else {
|
||||
// Inside the curly brackets, need to count how many useful characters are behind the position so they would all be taken into account
|
||||
const value = editor.getModel()!.getValue();
|
||||
while ((position.column - 2 - overwriteBefore >= 0)
|
||||
&& value[position.column - 2 - overwriteBefore] !== '{' && value[position.column - 2 - overwriteBefore] !== ' ') {
|
||||
overwriteBefore++;
|
||||
}
|
||||
}
|
||||
for (const { completion } of completions.items) {
|
||||
completion.range = monaco.Range.fromPositions(position.delta(0, -overwriteBefore), position);
|
||||
suggestions.push(completion as unknown as monaco.languages.CompletionItem);
|
||||
}
|
||||
}
|
||||
return { suggestions };
|
||||
}
|
||||
}));
|
||||
this.toDispose.push(this.zone.onDidLayoutChange(dimension => this.layout(dimension)));
|
||||
this.toDispose.push(this.editor.getControl().onDidChangeModel(() => {
|
||||
this.zone.hide();
|
||||
}));
|
||||
this.toDispose.push(input.getControl().onDidChangeModelContent(() => {
|
||||
const heightInLines = (input.getControl().getModel()?.getLineCount() || 0) + 1;
|
||||
this.zone.layout(heightInLines);
|
||||
this.updatePlaceholder();
|
||||
}));
|
||||
this._input.getControl().contextKeyService.createKey<boolean>('breakpointWidgetFocus', true);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
get position(): monaco.Position | undefined {
|
||||
const options = this.zone.options;
|
||||
return options && new monaco.Position(options.afterLineNumber, options.afterColumn || -1);
|
||||
}
|
||||
|
||||
show(options: ShowDebugBreakpointOptions): void {
|
||||
if (!this._input) {
|
||||
return;
|
||||
}
|
||||
const breakpoint = options instanceof DebugSourceBreakpoint ? options : 'breakpoint' in options ? options.breakpoint : undefined;
|
||||
this._values = breakpoint ? {
|
||||
condition: breakpoint.condition,
|
||||
hitCondition: breakpoint.hitCondition,
|
||||
logMessage: breakpoint.logMessage
|
||||
} : {};
|
||||
if (options instanceof DebugSourceBreakpoint) {
|
||||
if (options.logMessage) {
|
||||
this.context = 'logMessage';
|
||||
} else if (options.hitCondition && !options.condition) {
|
||||
this.context = 'hitCondition';
|
||||
} else {
|
||||
this.context = 'condition';
|
||||
}
|
||||
} else {
|
||||
this.context = options.context;
|
||||
}
|
||||
this.render();
|
||||
const position = 'position' in options ? options.position : undefined;
|
||||
const afterLineNumber = breakpoint ? breakpoint.line : position!.lineNumber;
|
||||
const afterColumn = breakpoint ? breakpoint.column : position!.column;
|
||||
const editor = this._input.getControl();
|
||||
const editorModel = editor.getModel();
|
||||
const heightInLines = (editorModel?.getLineCount() || 0) + 1;
|
||||
this.zone.show({ afterLineNumber, afterColumn, heightInLines, frameWidth: 1 });
|
||||
if (editorModel) {
|
||||
editor.setPosition(editorModel.getPositionAt(editorModel.getValueLength()));
|
||||
}
|
||||
this._input.focus();
|
||||
this.editor.getControl().createContextKey<boolean>('isBreakpointWidgetVisible', true);
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.zone.hide();
|
||||
this.editor.getControl().createContextKey<boolean>('isBreakpointWidgetVisible', false);
|
||||
this.editor.focus();
|
||||
}
|
||||
|
||||
protected layout(dimension: monaco.editor.IDimension): void {
|
||||
if (this._input) {
|
||||
this._input.getControl().layout(dimension);
|
||||
}
|
||||
}
|
||||
|
||||
protected createInput(node: HTMLElement): Promise<SimpleMonacoEditor> {
|
||||
return this.editorProvider.createSimpleInline(this.uri, node, {
|
||||
autoSizing: false
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): void {
|
||||
const value = this._values[this.context] || '';
|
||||
this.resources.update(this.uri, value);
|
||||
if (this._input) {
|
||||
this._input.getControl().setValue(value);
|
||||
}
|
||||
const selectComponent = this.selectComponentRef.current;
|
||||
if (selectComponent && selectComponent.value !== this.context) {
|
||||
selectComponent.value = this.context;
|
||||
}
|
||||
this.selectNodeRoot.render(<SelectComponent
|
||||
defaultValue={this.context} onChange={this.updateInput}
|
||||
options={[
|
||||
{ value: 'condition', label: nls.localizeByDefault('Expression') },
|
||||
{ value: 'hitCondition', label: nls.localizeByDefault('Hit Count') },
|
||||
{ value: 'logMessage', label: nls.localizeByDefault('Log Message') },
|
||||
]}
|
||||
ref={this.selectComponentRef}
|
||||
/>);
|
||||
}
|
||||
|
||||
protected readonly updateInput = (option: SelectOption) => {
|
||||
if (this._input) {
|
||||
this._values[this.context] = this._input.getControl().getValue();
|
||||
}
|
||||
this.context = option.value as DebugBreakpointWidget.Context;
|
||||
this.render();
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
}
|
||||
};
|
||||
|
||||
static PLACEHOLDER_DECORATION = 'placeholderDecoration';
|
||||
protected updatePlaceholder(): void {
|
||||
if (!this._input) {
|
||||
return;
|
||||
}
|
||||
const value = this._input.getControl().getValue();
|
||||
const decorations: IDecorationOptions[] = !!value ? [] : [{
|
||||
range: {
|
||||
startLineNumber: 0,
|
||||
endLineNumber: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 1
|
||||
},
|
||||
renderOptions: {
|
||||
after: {
|
||||
contentText: this.placeholder,
|
||||
opacity: '0.4'
|
||||
}
|
||||
}
|
||||
}];
|
||||
this._input.getControl().setDecorationsByType('Debug breakpoint placeholder', DebugBreakpointWidget.PLACEHOLDER_DECORATION, decorations);
|
||||
}
|
||||
protected get placeholder(): string {
|
||||
const acceptString = 'Enter';
|
||||
const closeString = 'Escape';
|
||||
if (this.context === 'logMessage') {
|
||||
return nls.localizeByDefault(
|
||||
"Message to log when breakpoint is hit. Expressions within {} are interpolated. '{0}' to accept, '{1}' to cancel.", acceptString, closeString
|
||||
);
|
||||
}
|
||||
if (this.context === 'hitCondition') {
|
||||
return nls.localizeByDefault("Break when hit count condition is met. '{0}' to accept, '{1}' to cancel.", acceptString, closeString);
|
||||
}
|
||||
return nls.localizeByDefault("Break when expression evaluates to true. '{0}' to accept, '{1}' to cancel.", acceptString, closeString);
|
||||
}
|
||||
|
||||
}
|
||||
export namespace DebugBreakpointWidget {
|
||||
export type Context = keyof Pick<DebugProtocol.SourceBreakpoint, 'condition' | 'hitCondition' | 'logMessage'>;
|
||||
}
|
||||
515
packages/debug/src/browser/editor/debug-editor-model.ts
Normal file
515
packages/debug/src/browser/editor/debug-editor-model.ts
Normal file
@@ -0,0 +1,515 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import debounce = require('p-debounce');
|
||||
import { injectable, inject, postConstruct, interfaces, Container } from '@theia/core/shared/inversify';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
|
||||
import { IDecorationOptions } from '@theia/monaco-editor-core/esm/vs/editor/common/editorCommon';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Disposable, DisposableCollection, MenuPath, isOSX } from '@theia/core';
|
||||
import { ContextMenuRenderer } from '@theia/core/lib/browser';
|
||||
import { BreakpointManager, SourceBreakpointsChangeEvent } from '../breakpoint/breakpoint-manager';
|
||||
import { DebugSourceBreakpoint } from '../model/debug-source-breakpoint';
|
||||
import { DebugSessionManager } from '../debug-session-manager';
|
||||
import { SourceBreakpoint } from '../breakpoint/breakpoint-marker';
|
||||
import { DebugEditor } from './debug-editor';
|
||||
import { DebugHoverWidget, createDebugHoverWidgetContainer } from './debug-hover-widget';
|
||||
import { DebugBreakpointWidget } from './debug-breakpoint-widget';
|
||||
import { DebugExceptionWidget } from './debug-exception-widget';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol';
|
||||
import { DebugInlineValueDecorator, INLINE_VALUE_DECORATION_KEY } from './debug-inline-value-decorator';
|
||||
|
||||
export const DebugEditorModelFactory = Symbol('DebugEditorModelFactory');
|
||||
export type DebugEditorModelFactory = (editor: DebugEditor) => DebugEditorModel;
|
||||
|
||||
@injectable()
|
||||
export class DebugEditorModel implements Disposable {
|
||||
|
||||
static createContainer(parent: interfaces.Container, editor: DebugEditor): Container {
|
||||
const child = createDebugHoverWidgetContainer(parent, editor);
|
||||
child.bind(DebugEditorModel).toSelf();
|
||||
child.bind(DebugBreakpointWidget).toSelf();
|
||||
child.bind(DebugExceptionWidget).toSelf();
|
||||
return child;
|
||||
}
|
||||
static createModel(parent: interfaces.Container, editor: DebugEditor): DebugEditorModel {
|
||||
return DebugEditorModel.createContainer(parent, editor).get(DebugEditorModel);
|
||||
}
|
||||
|
||||
static CONTEXT_MENU: MenuPath = ['debug-editor-context-menu'];
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
protected readonly toDisposeOnUpdate = new DisposableCollection();
|
||||
|
||||
protected uri: URI;
|
||||
|
||||
protected breakpointDecorations: string[] = [];
|
||||
protected breakpointRanges = new Map<string, [monaco.Range, SourceBreakpoint]>();
|
||||
|
||||
protected currentBreakpointDecorations: string[] = [];
|
||||
|
||||
protected editorDecorations: string[] = [];
|
||||
|
||||
protected updatingDecorations = false;
|
||||
protected toDisposeOnModelChange = new DisposableCollection();
|
||||
|
||||
@inject(DebugHoverWidget)
|
||||
readonly hover: DebugHoverWidget;
|
||||
|
||||
@inject(DebugEditor)
|
||||
readonly editor: DebugEditor;
|
||||
|
||||
@inject(BreakpointManager)
|
||||
readonly breakpoints: BreakpointManager;
|
||||
|
||||
@inject(DebugSessionManager)
|
||||
readonly sessions: DebugSessionManager;
|
||||
|
||||
@inject(ContextMenuRenderer)
|
||||
readonly contextMenu: ContextMenuRenderer;
|
||||
|
||||
@inject(DebugBreakpointWidget)
|
||||
readonly breakpointWidget: DebugBreakpointWidget;
|
||||
|
||||
@inject(DebugExceptionWidget)
|
||||
readonly exceptionWidget: DebugExceptionWidget;
|
||||
|
||||
@inject(DebugInlineValueDecorator)
|
||||
readonly inlineValueDecorator: DebugInlineValueDecorator;
|
||||
|
||||
@inject(DebugSessionManager)
|
||||
protected readonly sessionManager: DebugSessionManager;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.uri = new URI(this.editor.getResourceUri().toString());
|
||||
this.toDispose.pushAll([
|
||||
this.hover,
|
||||
this.breakpointWidget,
|
||||
this.exceptionWidget,
|
||||
this.editor.getControl().onMouseDown(event => this.handleMouseDown(event)),
|
||||
this.editor.getControl().onMouseMove(event => this.handleMouseMove(event)),
|
||||
this.editor.getControl().onMouseLeave(event => this.handleMouseLeave(event)),
|
||||
this.editor.getControl().onKeyDown(() => this.hover.hide({ immediate: false })),
|
||||
this.editor.getControl().onDidChangeModelContent(() => this.update()),
|
||||
this.editor.getControl().onDidChangeModel(e => this.updateModel()),
|
||||
this.editor.onDidResize(e => this.breakpointWidget.inputSize = e),
|
||||
this.sessions.onDidChange(() => this.update()),
|
||||
this.toDisposeOnUpdate,
|
||||
Disposable.create(() => this.toDisposeOnModelChange.dispose()),
|
||||
this.sessionManager.onDidChangeBreakpoints(({ session, uri }) => {
|
||||
if ((!session || session === this.sessionManager.currentSession) && uri.isEqual(this.uri)) {
|
||||
this.render();
|
||||
}
|
||||
}),
|
||||
this.breakpoints.onDidChangeBreakpoints(event => this.closeBreakpointIfAffected(event)),
|
||||
]);
|
||||
this.updateModel();
|
||||
}
|
||||
|
||||
protected updateModel(): void {
|
||||
this.toDisposeOnModelChange.dispose();
|
||||
this.toDisposeOnModelChange = new DisposableCollection();
|
||||
const model = this.editor.getControl().getModel();
|
||||
if (model) {
|
||||
this.toDisposeOnModelChange.push(model.onDidChangeDecorations(() => this.updateBreakpoints()));
|
||||
}
|
||||
this.update();
|
||||
this.render();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
protected readonly update = debounce(async () => {
|
||||
if (this.toDispose.disposed) {
|
||||
return;
|
||||
}
|
||||
this.toDisposeOnUpdate.dispose();
|
||||
this.toggleExceptionWidget();
|
||||
await this.updateEditorDecorations();
|
||||
}, 100);
|
||||
|
||||
protected async updateEditorDecorations(): Promise<void> {
|
||||
const [newFrameDecorations, inlineValueDecorations] = await Promise.all([
|
||||
this.createFrameDecorations(),
|
||||
this.createInlineValueDecorations()
|
||||
]);
|
||||
const codeEditor = this.editor.getControl() as unknown as StandaloneCodeEditor;
|
||||
codeEditor.removeDecorations([INLINE_VALUE_DECORATION_KEY]);
|
||||
codeEditor.setDecorationsByType('Inline debug decorations', INLINE_VALUE_DECORATION_KEY, inlineValueDecorations);
|
||||
this.editorDecorations = this.deltaDecorations(this.editorDecorations, newFrameDecorations);
|
||||
}
|
||||
|
||||
protected async createInlineValueDecorations(): Promise<IDecorationOptions[]> {
|
||||
if (!this.sessions.isCurrentEditorFrame(this.uri)) {
|
||||
return [];
|
||||
}
|
||||
const { currentFrame } = this.sessions;
|
||||
return this.inlineValueDecorator.calculateDecorations(this, currentFrame);
|
||||
}
|
||||
|
||||
protected createFrameDecorations(): monaco.editor.IModelDeltaDecoration[] {
|
||||
const { currentFrame, topFrame } = this.sessions;
|
||||
if (!currentFrame) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!currentFrame.thread.stopped) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!this.sessions.isCurrentEditorFrame(this.uri)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const decorations: monaco.editor.IModelDeltaDecoration[] = [];
|
||||
const columnUntilEOLRange = new monaco.Range(currentFrame.raw.line, currentFrame.raw.column, currentFrame.raw.line, 1 << 30);
|
||||
const range = new monaco.Range(currentFrame.raw.line, currentFrame.raw.column, currentFrame.raw.line, currentFrame.raw.column + 1);
|
||||
|
||||
if (topFrame === currentFrame) {
|
||||
decorations.push({
|
||||
options: DebugEditorModel.TOP_STACK_FRAME_MARGIN,
|
||||
range
|
||||
});
|
||||
decorations.push({
|
||||
options: DebugEditorModel.TOP_STACK_FRAME_DECORATION,
|
||||
range: columnUntilEOLRange
|
||||
});
|
||||
const firstNonWhitespaceColumn = this.editor.document.textEditorModel.getLineFirstNonWhitespaceColumn(currentFrame.raw.line);
|
||||
if (firstNonWhitespaceColumn !== 0 && currentFrame.raw.column > firstNonWhitespaceColumn) {
|
||||
decorations.push({
|
||||
options: DebugEditorModel.TOP_STACK_FRAME_INLINE_DECORATION,
|
||||
range: columnUntilEOLRange
|
||||
});
|
||||
}
|
||||
} else {
|
||||
decorations.push({
|
||||
options: DebugEditorModel.FOCUSED_STACK_FRAME_MARGIN,
|
||||
range
|
||||
});
|
||||
decorations.push({
|
||||
options: DebugEditorModel.FOCUSED_STACK_FRAME_DECORATION,
|
||||
range: columnUntilEOLRange
|
||||
});
|
||||
}
|
||||
return decorations;
|
||||
}
|
||||
|
||||
protected async toggleExceptionWidget(): Promise<void> {
|
||||
const { currentFrame } = this.sessions;
|
||||
if (!currentFrame) {
|
||||
return;
|
||||
}
|
||||
if (!this.sessions.isCurrentEditorFrame(this.uri)) {
|
||||
this.exceptionWidget.hide();
|
||||
return;
|
||||
}
|
||||
const info = await currentFrame.thread.getExceptionInfo();
|
||||
if (!info) {
|
||||
this.exceptionWidget.hide();
|
||||
return;
|
||||
}
|
||||
this.exceptionWidget.show({
|
||||
info,
|
||||
lineNumber: currentFrame.raw.line,
|
||||
column: currentFrame.raw.column
|
||||
});
|
||||
}
|
||||
|
||||
render(): void {
|
||||
this.renderBreakpoints();
|
||||
this.renderCurrentBreakpoints();
|
||||
}
|
||||
protected renderBreakpoints(): void {
|
||||
const breakpoints = this.breakpoints.getBreakpoints(this.uri);
|
||||
const decorations = this.createBreakpointDecorations(breakpoints);
|
||||
this.breakpointDecorations = this.deltaDecorations(this.breakpointDecorations, decorations);
|
||||
this.updateBreakpointRanges(breakpoints);
|
||||
}
|
||||
protected createBreakpointDecorations(breakpoints: SourceBreakpoint[]): monaco.editor.IModelDeltaDecoration[] {
|
||||
return breakpoints.map(breakpoint => this.createBreakpointDecoration(breakpoint));
|
||||
}
|
||||
protected createBreakpointDecoration(breakpoint: SourceBreakpoint): monaco.editor.IModelDeltaDecoration {
|
||||
const lineNumber = breakpoint.raw.line;
|
||||
const column = breakpoint.raw.column || this.editor.getControl().getModel()?.getLineFirstNonWhitespaceColumn(lineNumber) || 1;
|
||||
const range = new monaco.Range(lineNumber, column, lineNumber, column + 1);
|
||||
return {
|
||||
range,
|
||||
options: {
|
||||
stickiness: DebugEditorModel.STICKINESS
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected updateBreakpointRanges(breakpoints: SourceBreakpoint[]): void {
|
||||
this.breakpointRanges.clear();
|
||||
for (let i = 0; i < this.breakpointDecorations.length; i++) {
|
||||
const decoration = this.breakpointDecorations[i];
|
||||
const breakpoint = breakpoints[i];
|
||||
const range = this.editor.getControl().getModel()?.getDecorationRange(decoration);
|
||||
if (range) {
|
||||
this.breakpointRanges.set(decoration, [range, breakpoint]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected renderCurrentBreakpoints(): void {
|
||||
const decorations = this.createCurrentBreakpointDecorations();
|
||||
this.currentBreakpointDecorations = this.deltaDecorations(this.currentBreakpointDecorations, decorations);
|
||||
}
|
||||
protected createCurrentBreakpointDecorations(): monaco.editor.IModelDeltaDecoration[] {
|
||||
const breakpoints = this.sessions.getBreakpoints(this.uri);
|
||||
return breakpoints.map(breakpoint => this.createCurrentBreakpointDecoration(breakpoint));
|
||||
}
|
||||
protected createCurrentBreakpointDecoration(breakpoint: DebugSourceBreakpoint): monaco.editor.IModelDeltaDecoration {
|
||||
const lineNumber = breakpoint.line;
|
||||
const column = breakpoint.column;
|
||||
const range = typeof column === 'number' ? new monaco.Range(lineNumber, column, lineNumber, column + 1) : new monaco.Range(lineNumber, 1, lineNumber, 1);
|
||||
const { className, message } = breakpoint.getDecoration();
|
||||
const renderInline = typeof column === 'number' && (column > (this.editor.getControl().getModel()?.getLineFirstNonWhitespaceColumn(lineNumber) || 0));
|
||||
return {
|
||||
range,
|
||||
options: {
|
||||
glyphMarginClassName: className,
|
||||
glyphMarginHoverMessage: message.map(value => ({ value })),
|
||||
stickiness: DebugEditorModel.STICKINESS,
|
||||
beforeContentClassName: renderInline ? `theia-debug-breakpoint-column codicon ${className}` : undefined
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected updateBreakpoints(): void {
|
||||
if (this.areBreakpointsAffected()) {
|
||||
const breakpoints = this.createBreakpoints();
|
||||
this.breakpoints.setBreakpoints(this.uri, breakpoints);
|
||||
}
|
||||
}
|
||||
protected areBreakpointsAffected(): boolean {
|
||||
if (this.updatingDecorations || !this.editor.getControl().getModel()) {
|
||||
return false;
|
||||
}
|
||||
for (const decoration of this.breakpointDecorations) {
|
||||
const range = this.editor.getControl().getModel()?.getDecorationRange(decoration);
|
||||
const oldRange = this.breakpointRanges.get(decoration)![0];
|
||||
if (!range || !range.equalsRange(oldRange)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected createBreakpoints(): SourceBreakpoint[] {
|
||||
const { uri } = this;
|
||||
const positions = new Set<string>();
|
||||
const breakpoints: SourceBreakpoint[] = [];
|
||||
for (const decoration of this.breakpointDecorations) {
|
||||
const range = this.editor.getControl().getModel()?.getDecorationRange(decoration);
|
||||
if (range) {
|
||||
const line = range.startLineNumber;
|
||||
const column = range.startColumn;
|
||||
const oldBreakpoint = this.breakpointRanges.get(decoration)?.[1];
|
||||
if (oldBreakpoint) {
|
||||
const isLineBreakpoint = oldBreakpoint.raw.line !== undefined && oldBreakpoint.raw.column === undefined;
|
||||
const position = isLineBreakpoint ? `${line}` : `${line}:${column}`;
|
||||
if (!positions.has(position)) {
|
||||
const change = isLineBreakpoint ? { line } : { line, column };
|
||||
const breakpoint = SourceBreakpoint.create(uri, change, oldBreakpoint);
|
||||
breakpoints.push(breakpoint);
|
||||
positions.add(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return breakpoints;
|
||||
}
|
||||
|
||||
get position(): monaco.Position {
|
||||
return this.editor.getControl().getPosition()!;
|
||||
}
|
||||
getBreakpoint(position: monaco.Position = this.position): DebugSourceBreakpoint | undefined {
|
||||
return this.getInlineBreakpoint(position) || this.getLineBreakpoints(position)[0];
|
||||
}
|
||||
|
||||
getInlineBreakpoint(position: monaco.Position = this.position): DebugSourceBreakpoint | undefined {
|
||||
return this.sessions.getInlineBreakpoint(this.uri, position.lineNumber, position.column);
|
||||
}
|
||||
|
||||
protected getLineBreakpoints(position: monaco.Position = this.position): DebugSourceBreakpoint[] {
|
||||
return this.sessions.getLineBreakpoints(this.uri, position.lineNumber);
|
||||
}
|
||||
|
||||
protected addBreakpoint(raw: DebugProtocol.SourceBreakpoint): void {
|
||||
this.breakpoints.addBreakpoint(SourceBreakpoint.create(this.uri, raw));
|
||||
}
|
||||
|
||||
toggleBreakpoint(position: monaco.Position = this.position): void {
|
||||
const { lineNumber } = position;
|
||||
const breakpoints = this.getLineBreakpoints(position);
|
||||
if (breakpoints.length) {
|
||||
for (const breakpoint of breakpoints) {
|
||||
breakpoint.remove();
|
||||
}
|
||||
} else {
|
||||
this.addBreakpoint({ line: lineNumber });
|
||||
}
|
||||
}
|
||||
|
||||
addInlineBreakpoint(): void {
|
||||
const { position } = this;
|
||||
const { lineNumber, column } = position;
|
||||
const breakpoint = this.getInlineBreakpoint(position);
|
||||
if (breakpoint) {
|
||||
return;
|
||||
}
|
||||
this.addBreakpoint({ line: lineNumber, column });
|
||||
}
|
||||
|
||||
acceptBreakpoint(): void {
|
||||
const { position, values } = this.breakpointWidget;
|
||||
if (position && values) {
|
||||
const breakpoint = position.column > 0 ? this.getInlineBreakpoint(position) : this.getLineBreakpoints(position)[0];
|
||||
if (breakpoint) {
|
||||
breakpoint.updateOrigins(values);
|
||||
} else {
|
||||
const { lineNumber } = position;
|
||||
const column = position.column > 0 ? position.column : undefined;
|
||||
this.addBreakpoint({ line: lineNumber, column, ...values });
|
||||
}
|
||||
this.breakpointWidget.hide();
|
||||
}
|
||||
}
|
||||
|
||||
protected handleMouseDown(event: monaco.editor.IEditorMouseEvent): void {
|
||||
if (event.target && event.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN) {
|
||||
if (!event.event.rightButton) {
|
||||
this.toggleBreakpoint(event.target.position!);
|
||||
}
|
||||
}
|
||||
this.hintBreakpoint(event);
|
||||
}
|
||||
protected handleMouseMove(event: monaco.editor.IEditorMouseEvent): void {
|
||||
this.showHover(event);
|
||||
this.hintBreakpoint(event);
|
||||
}
|
||||
protected handleMouseLeave(event: monaco.editor.IPartialEditorMouseEvent): void {
|
||||
this.hideHover(event);
|
||||
this.deltaHintDecorations([]);
|
||||
}
|
||||
|
||||
protected hintDecorations: string[] = [];
|
||||
protected hintBreakpoint(event: monaco.editor.IEditorMouseEvent): void {
|
||||
const hintDecorations = this.createHintDecorations(event);
|
||||
this.deltaHintDecorations(hintDecorations);
|
||||
}
|
||||
protected deltaHintDecorations(hintDecorations: monaco.editor.IModelDeltaDecoration[]): void {
|
||||
this.hintDecorations = this.deltaDecorations(this.hintDecorations, hintDecorations);
|
||||
}
|
||||
protected createHintDecorations(event: monaco.editor.IEditorMouseEvent): monaco.editor.IModelDeltaDecoration[] {
|
||||
if (event.target && event.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN && event.target.position) {
|
||||
const lineNumber = event.target.position.lineNumber;
|
||||
if (this.getLineBreakpoints(event.target.position).length) {
|
||||
return [];
|
||||
}
|
||||
return [{
|
||||
range: new monaco.Range(lineNumber, 1, lineNumber, 1),
|
||||
options: DebugEditorModel.BREAKPOINT_HINT_DECORATION
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
protected closeBreakpointIfAffected({ uri, removed }: SourceBreakpointsChangeEvent): void {
|
||||
if (!uri.isEqual(this.uri)) {
|
||||
return;
|
||||
}
|
||||
const position = this.breakpointWidget.position;
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
for (const breakpoint of removed) {
|
||||
if (breakpoint.raw.line === position.lineNumber) {
|
||||
this.breakpointWidget.hide();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected showHover(mouseEvent: monaco.editor.IEditorMouseEvent): void {
|
||||
const targetType = mouseEvent.target.type;
|
||||
const stopKey = isOSX ? 'metaKey' : 'ctrlKey';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (targetType === monaco.editor.MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === this.hover.getId() && !(<any>mouseEvent.event)[stopKey]) {
|
||||
// mouse moved on top of debug hover widget
|
||||
return;
|
||||
}
|
||||
if (targetType === monaco.editor.MouseTargetType.CONTENT_TEXT) {
|
||||
this.hover.show({
|
||||
selection: mouseEvent.target.range!,
|
||||
immediate: false
|
||||
});
|
||||
} else {
|
||||
this.hover.hide({ immediate: false });
|
||||
}
|
||||
}
|
||||
protected hideHover({ event }: monaco.editor.IPartialEditorMouseEvent): void {
|
||||
const rect = this.hover.getDomNode().getBoundingClientRect();
|
||||
if (event.posx < rect.left || event.posx > rect.right || event.posy < rect.top || event.posy > rect.bottom) {
|
||||
this.hover.hide({ immediate: false });
|
||||
}
|
||||
}
|
||||
|
||||
protected deltaDecorations(oldDecorations: string[], newDecorations: monaco.editor.IModelDeltaDecoration[]): string[] {
|
||||
this.updatingDecorations = true;
|
||||
try {
|
||||
return this.editor.getControl().deltaDecorations(oldDecorations, newDecorations);
|
||||
} finally {
|
||||
this.updatingDecorations = false;
|
||||
}
|
||||
}
|
||||
|
||||
static STICKINESS = monaco.editor.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges;
|
||||
|
||||
static BREAKPOINT_HINT_DECORATION: monaco.editor.IModelDecorationOptions = {
|
||||
glyphMarginClassName: 'codicon-debug-hint',
|
||||
stickiness: DebugEditorModel.STICKINESS
|
||||
};
|
||||
|
||||
static TOP_STACK_FRAME_MARGIN: monaco.editor.IModelDecorationOptions = {
|
||||
glyphMarginClassName: 'codicon-debug-stackframe',
|
||||
stickiness: DebugEditorModel.STICKINESS
|
||||
};
|
||||
static FOCUSED_STACK_FRAME_MARGIN: monaco.editor.IModelDecorationOptions = {
|
||||
glyphMarginClassName: 'codicon-debug-stackframe-focused',
|
||||
stickiness: DebugEditorModel.STICKINESS
|
||||
};
|
||||
static TOP_STACK_FRAME_DECORATION: monaco.editor.IModelDecorationOptions = {
|
||||
isWholeLine: true,
|
||||
className: 'theia-debug-top-stack-frame-line',
|
||||
stickiness: DebugEditorModel.STICKINESS
|
||||
};
|
||||
static TOP_STACK_FRAME_INLINE_DECORATION: monaco.editor.IModelDecorationOptions = {
|
||||
beforeContentClassName: 'theia-debug-top-stack-frame-column'
|
||||
};
|
||||
static FOCUSED_STACK_FRAME_DECORATION: monaco.editor.IModelDecorationOptions = {
|
||||
isWholeLine: true,
|
||||
className: 'theia-debug-focused-stack-frame-line',
|
||||
stickiness: DebugEditorModel.STICKINESS
|
||||
};
|
||||
|
||||
}
|
||||
180
packages/debug/src/browser/editor/debug-editor-service.ts
Normal file
180
packages/debug/src/browser/editor/debug-editor-service.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { ContextMenuRenderer } from '@theia/core/lib/browser';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { DebugSessionManager } from '../debug-session-manager';
|
||||
import { DebugEditorModel, DebugEditorModelFactory } from './debug-editor-model';
|
||||
import { BreakpointManager } from '../breakpoint/breakpoint-manager';
|
||||
import { DebugSourceBreakpoint } from '../model/debug-source-breakpoint';
|
||||
import { DebugBreakpointWidget } from './debug-breakpoint-widget';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
@injectable()
|
||||
export class DebugEditorService {
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editors: EditorManager;
|
||||
|
||||
@inject(BreakpointManager)
|
||||
protected readonly breakpoints: BreakpointManager;
|
||||
|
||||
@inject(DebugSessionManager)
|
||||
protected readonly sessionManager: DebugSessionManager;
|
||||
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenu: ContextMenuRenderer;
|
||||
|
||||
@inject(DebugEditorModelFactory)
|
||||
protected readonly factory: DebugEditorModelFactory;
|
||||
|
||||
protected readonly models = new Map<MonacoEditor, DebugEditorModel>();
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.editors.all.forEach(widget => this.push(widget));
|
||||
this.editors.onCreated(widget => this.push(widget));
|
||||
}
|
||||
|
||||
protected push(widget: EditorWidget): void {
|
||||
const editor = MonacoEditor.get(widget);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const debugModel = this.factory(editor);
|
||||
this.models.set(editor, debugModel);
|
||||
widget.onDispose(() => {
|
||||
debugModel.dispose();
|
||||
this.models.delete(editor);
|
||||
});
|
||||
}
|
||||
|
||||
get model(): DebugEditorModel | undefined {
|
||||
const { currentEditor } = this.editors;
|
||||
return currentEditor && this.models.get(currentEditor.editor as MonacoEditor);
|
||||
}
|
||||
|
||||
get currentUri(): URI | undefined {
|
||||
const { currentEditor } = this.editors;
|
||||
return currentEditor && currentEditor.getResourceUri();
|
||||
}
|
||||
|
||||
getLogpoint(position: monaco.Position): DebugSourceBreakpoint | undefined {
|
||||
const logpoint = this.anyBreakpoint(position);
|
||||
return logpoint && logpoint.logMessage ? logpoint : undefined;
|
||||
}
|
||||
getLogpointEnabled(position: monaco.Position): boolean | undefined {
|
||||
const logpoint = this.getLogpoint(position);
|
||||
return logpoint && logpoint.enabled;
|
||||
}
|
||||
|
||||
getBreakpoint(position: monaco.Position): DebugSourceBreakpoint | undefined {
|
||||
const breakpoint = this.anyBreakpoint(position);
|
||||
return breakpoint && breakpoint.logMessage ? undefined : breakpoint;
|
||||
}
|
||||
getBreakpointEnabled(position: monaco.Position): boolean | undefined {
|
||||
const breakpoint = this.getBreakpoint(position);
|
||||
return breakpoint && breakpoint.enabled;
|
||||
}
|
||||
|
||||
anyBreakpoint(position?: monaco.Position): DebugSourceBreakpoint | undefined {
|
||||
return this.model && this.model.getBreakpoint(position);
|
||||
}
|
||||
|
||||
getInlineBreakpoint(position?: monaco.Position): DebugSourceBreakpoint | undefined {
|
||||
return this.model && this.model.getInlineBreakpoint(position);
|
||||
}
|
||||
|
||||
toggleBreakpoint(position?: monaco.Position): void {
|
||||
const { model } = this;
|
||||
if (model) {
|
||||
model.toggleBreakpoint(position);
|
||||
}
|
||||
}
|
||||
setBreakpointEnabled(position: monaco.Position, enabled: boolean): void {
|
||||
const breakpoint = this.anyBreakpoint(position);
|
||||
if (breakpoint) {
|
||||
breakpoint.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
addInlineBreakpoint(): void {
|
||||
const { model } = this;
|
||||
if (model) {
|
||||
model.addInlineBreakpoint();
|
||||
}
|
||||
}
|
||||
|
||||
showHover(): void {
|
||||
const { model } = this;
|
||||
if (model) {
|
||||
const selection = model.editor.getControl().getSelection()!;
|
||||
model.hover.show({ selection, focus: true });
|
||||
}
|
||||
}
|
||||
canShowHover(): boolean {
|
||||
const { model } = this;
|
||||
if (model) {
|
||||
const selection = model.editor.getControl().getSelection()!;
|
||||
return !!model.editor.getControl().getModel()?.getWordAtPosition(selection.getStartPosition());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
addBreakpoint(context: DebugBreakpointWidget.Context, position?: monaco.Position): void {
|
||||
const { model } = this;
|
||||
if (model) {
|
||||
position = position || model.position;
|
||||
const breakpoint = model.getBreakpoint(position);
|
||||
if (breakpoint) {
|
||||
model.breakpointWidget.show({ breakpoint, context });
|
||||
} else {
|
||||
model.breakpointWidget.show({
|
||||
position,
|
||||
context
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
async editBreakpoint(breakpointOrPosition?: DebugSourceBreakpoint | monaco.Position): Promise<void> {
|
||||
if (breakpointOrPosition instanceof monaco.Position) {
|
||||
breakpointOrPosition = this.anyBreakpoint(breakpointOrPosition);
|
||||
}
|
||||
|
||||
if (breakpointOrPosition) {
|
||||
const editor = await breakpointOrPosition.open();
|
||||
const model = this.models.get(editor.editor as MonacoEditor);
|
||||
if (model) {
|
||||
model.breakpointWidget.show(breakpointOrPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
closeBreakpoint(): void {
|
||||
const { model } = this;
|
||||
if (model) {
|
||||
model.breakpointWidget.hide();
|
||||
}
|
||||
}
|
||||
acceptBreakpoint(): void {
|
||||
const { model } = this;
|
||||
if (model) {
|
||||
model.acceptBreakpoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
20
packages/debug/src/browser/editor/debug-editor.ts
Normal file
20
packages/debug/src/browser/editor/debug-editor.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
|
||||
export const DebugEditor = Symbol('DebugEditor');
|
||||
export type DebugEditor = MonacoEditor;
|
||||
122
packages/debug/src/browser/editor/debug-exception-widget.tsx
Normal file
122
packages/debug/src/browser/editor/debug-exception-widget.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { createRoot, Root } from '@theia/core/shared/react-dom/client';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { MonacoEditorZoneWidget } from '@theia/monaco/lib/browser/monaco-editor-zone-widget';
|
||||
import { DebugEditor } from './debug-editor';
|
||||
import { DebugExceptionInfo } from '../model/debug-thread';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { codicon } from '@theia/core/lib/browser/widgets';
|
||||
|
||||
export interface ShowDebugExceptionParams {
|
||||
info: DebugExceptionInfo
|
||||
lineNumber: number
|
||||
column: number
|
||||
}
|
||||
|
||||
export class DebugExceptionMonacoEditorZoneWidget extends MonacoEditorZoneWidget {
|
||||
|
||||
protected override computeContainerHeight(zoneHeight: number): {
|
||||
height: number,
|
||||
frameWidth: number
|
||||
} {
|
||||
// reset height to match it to the content
|
||||
this.containerNode.style.height = 'initial';
|
||||
const height = this.containerNode.offsetHeight;
|
||||
const result = super.computeContainerHeight(zoneHeight);
|
||||
result.height = height;
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DebugExceptionWidget implements Disposable {
|
||||
|
||||
@inject(DebugEditor)
|
||||
readonly editor: DebugEditor;
|
||||
|
||||
protected zone: MonacoEditorZoneWidget;
|
||||
protected containerNodeRoot: Root;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.doInit();
|
||||
}
|
||||
|
||||
protected async doInit(): Promise<void> {
|
||||
this.toDispose.push(this.zone = new DebugExceptionMonacoEditorZoneWidget(this.editor.getControl()));
|
||||
this.zone.containerNode.classList.add('theia-debug-exception-widget');
|
||||
this.containerNodeRoot = createRoot(this.zone.containerNode);
|
||||
this.toDispose.push(Disposable.create(() => this.containerNodeRoot.unmount()));
|
||||
this.toDispose.push(this.editor.getControl().onDidLayoutChange(() => this.layout()));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
show({ info, lineNumber, column }: ShowDebugExceptionParams): void {
|
||||
this.render(info, () => {
|
||||
const fontInfo = this.editor.getControl().getOption(monaco.editor.EditorOption.fontInfo);
|
||||
this.zone.containerNode.style.fontSize = `${fontInfo.fontSize}px`;
|
||||
this.zone.containerNode.style.lineHeight = `${fontInfo.lineHeight}px`;
|
||||
|
||||
if (lineNumber !== undefined && column !== undefined) {
|
||||
const afterLineNumber = lineNumber;
|
||||
const afterColumn = column;
|
||||
this.zone.show({ showFrame: true, afterLineNumber, afterColumn, heightInLines: 0, frameWidth: 1 });
|
||||
}
|
||||
|
||||
this.layout();
|
||||
});
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.zone.hide();
|
||||
}
|
||||
|
||||
protected render(info: DebugExceptionInfo, cb: () => void): void {
|
||||
const stackTrace = info.details && info.details.stackTrace;
|
||||
const exceptionTitle = info.id ?
|
||||
nls.localizeByDefault('Exception has occurred: {0}', info.id) :
|
||||
nls.localizeByDefault('Exception has occurred.');
|
||||
this.containerNodeRoot.render(<React.Fragment>
|
||||
<div className='title' ref={cb}>
|
||||
{exceptionTitle}
|
||||
<span id="exception-close" className={codicon('close', true)} onClick={() => this.hide()} title={nls.localizeByDefault('Close')}></span>
|
||||
</div>
|
||||
{info.description && <div className='description'>{info.description}</div>}
|
||||
{stackTrace && <div className='stack-trace'>{stackTrace}</div>}
|
||||
</React.Fragment>);
|
||||
}
|
||||
|
||||
protected layout(): void {
|
||||
// reset height to match it to the content
|
||||
this.zone.containerNode.style.height = 'initial';
|
||||
|
||||
const lineHeight = this.editor.getControl().getOption(monaco.editor.EditorOption.lineHeight);
|
||||
const heightInLines = Math.ceil(this.zone.containerNode.offsetHeight / lineHeight);
|
||||
this.zone.layout(heightInLines);
|
||||
}
|
||||
|
||||
}
|
||||
123
packages/debug/src/browser/editor/debug-expression-provider.ts
Normal file
123
packages/debug/src/browser/editor/debug-expression-provider.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation and others. All rights reserved.
|
||||
* Licensed under the MIT License. See https://github.com/Microsoft/vscode/blob/master/LICENSE.txt for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { ArrayUtils } from '@theia/core';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { CancellationToken } from '@theia/monaco-editor-core/esm/vs/base/common/cancellation';
|
||||
import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures';
|
||||
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
|
||||
import { DebugEditor } from './debug-editor';
|
||||
|
||||
/**
|
||||
* TODO: introduce a new request to LSP to look up an expression range: https://github.com/Microsoft/language-server-protocol/issues/462
|
||||
*/
|
||||
@injectable()
|
||||
export class DebugExpressionProvider {
|
||||
|
||||
async getEvaluatableExpression(
|
||||
editor: DebugEditor,
|
||||
selection: monaco.IRange
|
||||
): Promise<{ matchingExpression: string; range: monaco.IRange } | undefined> {
|
||||
|
||||
const pluginExpressionProvider = StandaloneServices.get(ILanguageFeaturesService).evaluatableExpressionProvider;
|
||||
const textEditorModel = editor.document.textEditorModel;
|
||||
|
||||
if (pluginExpressionProvider && pluginExpressionProvider.has(textEditorModel)) {
|
||||
const registeredProviders = pluginExpressionProvider.ordered(textEditorModel);
|
||||
const position = new monaco.Position(selection.startLineNumber, selection.startColumn);
|
||||
|
||||
const promises = registeredProviders.map(support =>
|
||||
Promise.resolve(support.provideEvaluatableExpression(textEditorModel, position, CancellationToken.None))
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises).then(ArrayUtils.coalesce);
|
||||
if (results.length > 0) {
|
||||
const range = results[0].range;
|
||||
const matchingExpression = results[0].expression || textEditorModel.getValueInRange(range);
|
||||
return { matchingExpression, range };
|
||||
}
|
||||
} else { // use fallback if no provider was registered
|
||||
const model = editor.getControl().getModel();
|
||||
if (model) {
|
||||
const lineContent = model.getLineContent(selection.startLineNumber);
|
||||
const { start, end } = this.getExactExpressionStartAndEnd(lineContent, selection.startColumn, selection.endColumn);
|
||||
const matchingExpression = lineContent.substring(start - 1, end - 1);
|
||||
const range = new monaco.Range(
|
||||
selection.startLineNumber,
|
||||
start,
|
||||
selection.startLineNumber,
|
||||
end
|
||||
);
|
||||
return { matchingExpression, range };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get(model: monaco.editor.IModel, selection: monaco.IRange): string {
|
||||
const lineContent = model.getLineContent(selection.startLineNumber);
|
||||
const { start, end } = this.getExactExpressionStartAndEnd(lineContent, selection.startColumn, selection.endColumn);
|
||||
return lineContent.substring(start - 1, end - 1);
|
||||
}
|
||||
protected getExactExpressionStartAndEnd(lineContent: string, looseStart: number, looseEnd: number): { start: number, end: number } {
|
||||
let matchingExpression: string | undefined = undefined;
|
||||
let startOffset = 1;
|
||||
|
||||
// Some example supported expressions: myVar.prop, a.b.c.d, myVar?.prop, myVar->prop, MyClass::StaticProp, *myVar
|
||||
// Match any character except a set of characters which often break interesting sub-expressions
|
||||
const expression = /([^()\[\]{}<>\s+\-/%~#^;=|,`!]|\->)+/g;
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
let result: RegExpExecArray | null = null;
|
||||
|
||||
// First find the full expression under the cursor
|
||||
while (result = expression.exec(lineContent)) {
|
||||
const start = result.index + 1;
|
||||
const end = start + result[0].length;
|
||||
|
||||
if (start <= looseStart && end >= looseEnd) {
|
||||
matchingExpression = result[0];
|
||||
startOffset = start;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are non-word characters after the cursor, we want to truncate the expression then.
|
||||
// For example in expression 'a.b.c.d', if the focus was under 'b', 'a.b' would be evaluated.
|
||||
if (matchingExpression) {
|
||||
const subExpression: RegExp = /\w+/g;
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
let subExpressionResult: RegExpExecArray | null = null;
|
||||
while (subExpressionResult = subExpression.exec(matchingExpression)) {
|
||||
const subEnd = subExpressionResult.index + 1 + startOffset + subExpressionResult[0].length;
|
||||
if (subEnd >= looseEnd) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (subExpressionResult) {
|
||||
matchingExpression = matchingExpression.substring(0, subExpression.lastIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return matchingExpression ?
|
||||
{ start: startOffset, end: startOffset + matchingExpression.length } :
|
||||
{ start: 1, end: 1 };
|
||||
}
|
||||
}
|
||||
109
packages/debug/src/browser/editor/debug-hover-source.tsx
Normal file
109
packages/debug/src/browser/editor/debug-hover-source.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { TreeSource, TreeElement } from '@theia/core/lib/browser/source-tree';
|
||||
import { ExpressionContainer, ExpressionItem, DebugVariable } from '../console/debug-console-items';
|
||||
import { DebugSessionManager } from '../debug-session-manager';
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
|
||||
@injectable()
|
||||
export class DebugHoverSource extends TreeSource {
|
||||
|
||||
@inject(DebugSessionManager)
|
||||
protected readonly sessions: DebugSessionManager;
|
||||
|
||||
protected _expression: ExpressionItem | DebugVariable | undefined;
|
||||
get expression(): ExpressionItem | DebugVariable | undefined {
|
||||
return this._expression;
|
||||
}
|
||||
|
||||
protected elements: TreeElement[] = [];
|
||||
getElements(): IterableIterator<TreeElement> {
|
||||
return this.elements[Symbol.iterator]();
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
init(): void {
|
||||
this.toDispose.push(this.sessions.onDidResolveLazyVariable(() => this.fireDidChange()));
|
||||
}
|
||||
|
||||
protected renderTitle(element: ExpressionItem | DebugVariable): React.ReactNode {
|
||||
return <div className='theia-debug-hover-title' title={element.value}>{element.value}</div>;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._expression = undefined;
|
||||
this.elements = [];
|
||||
this.fireDidChange();
|
||||
}
|
||||
|
||||
async evaluate(expression: string): Promise<ExpressionItem | DebugVariable | undefined> {
|
||||
const evaluated = await this.doEvaluate(expression);
|
||||
const elements = evaluated && await evaluated.getElements();
|
||||
this._expression = evaluated;
|
||||
this.elements = elements ? [...elements] : [];
|
||||
this.fireDidChange();
|
||||
return evaluated;
|
||||
}
|
||||
protected async doEvaluate(expression: string): Promise<ExpressionItem | DebugVariable | undefined> {
|
||||
const { currentSession } = this.sessions;
|
||||
if (!currentSession) {
|
||||
return undefined;
|
||||
}
|
||||
if (currentSession.capabilities.supportsEvaluateForHovers) {
|
||||
const item = new ExpressionItem(expression, () => currentSession);
|
||||
await item.evaluate('hover');
|
||||
return item.available && item || undefined;
|
||||
}
|
||||
return this.findVariable(expression.split('.').map(word => word.trim()).filter(word => !!word));
|
||||
}
|
||||
protected async findVariable(namesToFind: string[]): Promise<DebugVariable | undefined> {
|
||||
const { currentFrame } = this.sessions;
|
||||
if (!currentFrame) {
|
||||
return undefined;
|
||||
}
|
||||
let variable: DebugVariable | undefined;
|
||||
const scopes = await currentFrame.getScopes();
|
||||
for (const scope of scopes) {
|
||||
const found = await this.doFindVariable(scope, namesToFind);
|
||||
if (!variable) {
|
||||
variable = found;
|
||||
} else if (found && found.value !== variable.value) {
|
||||
// only show if all expressions found have the same value
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return variable;
|
||||
}
|
||||
protected async doFindVariable(owner: ExpressionContainer, namesToFind: string[]): Promise<DebugVariable | undefined> {
|
||||
const elements = await owner.getElements();
|
||||
const variables: DebugVariable[] = [];
|
||||
for (const element of elements) {
|
||||
if (element instanceof DebugVariable && element.name === namesToFind[0]) {
|
||||
variables.push(element);
|
||||
}
|
||||
}
|
||||
if (variables.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
if (namesToFind.length === 1) {
|
||||
return variables[0];
|
||||
} else {
|
||||
return this.doFindVariable(variables[0], namesToFind.slice(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
295
packages/debug/src/browser/editor/debug-hover-widget.ts
Normal file
295
packages/debug/src/browser/editor/debug-hover-widget.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import debounce = require('@theia/core/shared/lodash.debounce');
|
||||
|
||||
import { MenuPath } from '@theia/core';
|
||||
import { Key } from '@theia/core/lib/browser';
|
||||
import { SourceTreeWidget } from '@theia/core/lib/browser/source-tree';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Message } from '@theia/core/shared/@lumino/messaging';
|
||||
import { Widget } from '@theia/core/shared/@lumino/widgets';
|
||||
import { Container, inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { URI as CodeUri } from '@theia/core/shared/vscode-uri';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { IEditorHoverOptions } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions';
|
||||
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
|
||||
import { IConfigurationService } from '@theia/monaco-editor-core/esm/vs/platform/configuration/common/configuration';
|
||||
import { DebugVariable } from '../console/debug-console-items';
|
||||
import { DebugSessionManager } from '../debug-session-manager';
|
||||
import { DebugEditor } from './debug-editor';
|
||||
import { DebugExpressionProvider } from './debug-expression-provider';
|
||||
import { DebugHoverSource } from './debug-hover-source';
|
||||
|
||||
export interface ShowDebugHoverOptions {
|
||||
selection: monaco.Range
|
||||
/** default: false */
|
||||
focus?: boolean
|
||||
/** default: true */
|
||||
immediate?: boolean
|
||||
}
|
||||
|
||||
export interface HideDebugHoverOptions {
|
||||
/** default: true */
|
||||
immediate?: boolean
|
||||
}
|
||||
|
||||
export function createDebugHoverWidgetContainer(parent: interfaces.Container, editor: DebugEditor): Container {
|
||||
const child = SourceTreeWidget.createContainer(parent, {
|
||||
contextMenuPath: DebugHoverWidget.CONTEXT_MENU,
|
||||
virtualized: false
|
||||
});
|
||||
child.bind(DebugEditor).toConstantValue(editor);
|
||||
child.bind(DebugHoverSource).toSelf();
|
||||
child.unbind(SourceTreeWidget);
|
||||
child.bind(DebugHoverWidget).toSelf();
|
||||
return child;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor.IContentWidget {
|
||||
|
||||
static CONTEXT_MENU: MenuPath = ['debug-hover-context-menu'];
|
||||
static EDIT_MENU: MenuPath = [...DebugHoverWidget.CONTEXT_MENU, 'a_edit'];
|
||||
static WATCH_MENU: MenuPath = [...DebugHoverWidget.CONTEXT_MENU, 'b_watch'];
|
||||
|
||||
@inject(DebugEditor)
|
||||
protected readonly editor: DebugEditor;
|
||||
|
||||
@inject(DebugSessionManager)
|
||||
protected readonly sessions: DebugSessionManager;
|
||||
|
||||
@inject(DebugHoverSource)
|
||||
protected readonly hoverSource: DebugHoverSource;
|
||||
|
||||
@inject(DebugExpressionProvider)
|
||||
protected readonly expressionProvider: DebugExpressionProvider;
|
||||
|
||||
allowEditorOverflow = true;
|
||||
|
||||
protected suppressEditorHoverToDispose = new DisposableCollection();
|
||||
|
||||
static ID = 'debug.editor.hover';
|
||||
getId(): string {
|
||||
return DebugHoverWidget.ID;
|
||||
}
|
||||
|
||||
protected readonly domNode = document.createElement('div');
|
||||
protected readonly titleNode = document.createElement('div');
|
||||
protected readonly contentNode = document.createElement('div');
|
||||
getDomNode(): HTMLElement {
|
||||
return this.domNode;
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.domNode.className = 'theia-debug-hover';
|
||||
this.titleNode.className = 'theia-debug-hover-title';
|
||||
this.domNode.appendChild(this.titleNode);
|
||||
this.contentNode.className = 'theia-debug-hover-content';
|
||||
this.domNode.appendChild(this.contentNode);
|
||||
|
||||
// for stopping scroll events from contentNode going to the editor
|
||||
this.contentNode.addEventListener('wheel', e => e.stopPropagation());
|
||||
|
||||
this.editor.getControl().addContentWidget(this);
|
||||
this.source = this.hoverSource;
|
||||
this.toDispose.pushAll([
|
||||
this.hoverSource,
|
||||
Disposable.create(() => this.editor.getControl().removeContentWidget(this)),
|
||||
Disposable.create(() => this.hide()),
|
||||
this.sessions.onDidChange(() => {
|
||||
if (!this.isEditorFrame()) {
|
||||
this.hide();
|
||||
}
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.suppressEditorHoverToDispose.dispose();
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
override show(options?: ShowDebugHoverOptions): void {
|
||||
this.schedule(() => this.doShow(options), options && options.immediate);
|
||||
}
|
||||
|
||||
override hide(options?: HideDebugHoverOptions): void {
|
||||
this.schedule(() => this.doHide(), options && options.immediate);
|
||||
}
|
||||
|
||||
protected readonly doSchedule = debounce((fn: () => void) => fn(), 300);
|
||||
protected schedule(fn: () => void, immediate: boolean = true): void {
|
||||
if (immediate) {
|
||||
this.doSchedule.cancel();
|
||||
fn();
|
||||
} else {
|
||||
this.doSchedule(fn);
|
||||
}
|
||||
}
|
||||
|
||||
protected options: ShowDebugHoverOptions | undefined;
|
||||
protected doHide(): void {
|
||||
if (!this.isVisible) {
|
||||
return;
|
||||
}
|
||||
this.suppressEditorHoverToDispose.dispose();
|
||||
|
||||
if (this.domNode.contains(document.activeElement)) {
|
||||
this.editor.getControl().focus();
|
||||
}
|
||||
if (this.isAttached) {
|
||||
Widget.detach(this);
|
||||
}
|
||||
this.hoverSource.reset();
|
||||
super.hide();
|
||||
this.options = undefined;
|
||||
this.editor.getControl().layoutContentWidget(this);
|
||||
}
|
||||
|
||||
protected async doShow(options: ShowDebugHoverOptions | undefined = this.options): Promise<void> {
|
||||
|
||||
if (!this.isEditorFrame()) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
if (!options) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
if (this.options && this.options.selection.equalsRange(options.selection)) {
|
||||
return;
|
||||
}
|
||||
if (!this.isAttached) {
|
||||
Widget.attach(this, this.contentNode);
|
||||
}
|
||||
|
||||
this.options = options;
|
||||
|
||||
const result = await this.expressionProvider.getEvaluatableExpression(this.editor, options.selection);
|
||||
|
||||
if (!result?.matchingExpression) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this.options.selection = monaco.Range.lift(result.range);
|
||||
|
||||
const toFocus = new DisposableCollection();
|
||||
if (this.options.focus === true) {
|
||||
toFocus.push(this.model.onNodeRefreshed(() => {
|
||||
toFocus.dispose();
|
||||
this.activate();
|
||||
}));
|
||||
}
|
||||
const expression = await this.hoverSource.evaluate(result.matchingExpression);
|
||||
if (!expression) {
|
||||
toFocus.dispose();
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this.contentNode.hidden = false;
|
||||
['number', 'boolean', 'string'].forEach(token => this.titleNode.classList.remove(token));
|
||||
this.domNode.classList.remove('complex-value');
|
||||
if (expression.hasElements) {
|
||||
this.domNode.classList.add('complex-value');
|
||||
} else {
|
||||
this.contentNode.hidden = true;
|
||||
if (expression.type === 'number' || expression.type === 'boolean' || expression.type === 'string') {
|
||||
this.titleNode.classList.add(expression.type);
|
||||
} else if (!isNaN(+expression.value)) {
|
||||
this.titleNode.classList.add('number');
|
||||
} else if (DebugVariable.booleanRegex.test(expression.value)) {
|
||||
this.titleNode.classList.add('boolean');
|
||||
} else if (DebugVariable.stringRegex.test(expression.value)) {
|
||||
this.titleNode.classList.add('string');
|
||||
}
|
||||
}
|
||||
|
||||
this.suppressEditorHover();
|
||||
|
||||
super.show();
|
||||
await new Promise<void>(resolve => {
|
||||
setTimeout(() => window.requestAnimationFrame(() => {
|
||||
this.editor.getControl().layoutContentWidget(this);
|
||||
resolve();
|
||||
}), 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress the default editor-contribution hover from Code.
|
||||
* Otherwise, both `textdocument/hover` and the debug hovers are visible
|
||||
* at the same time when hovering over a symbol.
|
||||
* This will priorize the debug hover over the editor hover.
|
||||
*/
|
||||
protected suppressEditorHover(): void {
|
||||
const codeEditor = this.editor.getControl();
|
||||
codeEditor.updateOptions({ hover: { enabled: false } });
|
||||
this.suppressEditorHoverToDispose.push(Disposable.create(() => {
|
||||
const model = codeEditor.getModel();
|
||||
const overrides = {
|
||||
resource: CodeUri.parse(this.editor.getResourceUri().toString()),
|
||||
overrideIdentifier: model?.getLanguageId(),
|
||||
};
|
||||
const { enabled, delay, sticky } = StandaloneServices.get(IConfigurationService).getValue<IEditorHoverOptions>('editor.hover', overrides);
|
||||
codeEditor.updateOptions({
|
||||
hover: {
|
||||
enabled,
|
||||
delay,
|
||||
sticky
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
protected isEditorFrame(): boolean {
|
||||
return this.sessions.isCurrentEditorFrame(this.editor.getResourceUri());
|
||||
}
|
||||
|
||||
getPosition(): monaco.editor.IContentWidgetPosition {
|
||||
if (!this.isVisible) {
|
||||
return undefined!;
|
||||
}
|
||||
const position = this.options && this.options.selection.getStartPosition();
|
||||
return position
|
||||
? {
|
||||
position: new monaco.Position(position.lineNumber, position.column),
|
||||
preference: [
|
||||
monaco.editor.ContentWidgetPositionPreference.ABOVE,
|
||||
monaco.editor.ContentWidgetPositionPreference.BELOW,
|
||||
],
|
||||
}
|
||||
: undefined!;
|
||||
}
|
||||
|
||||
protected override onUpdateRequest(msg: Message): void {
|
||||
super.onUpdateRequest(msg);
|
||||
const { expression } = this.hoverSource;
|
||||
const value = expression && expression.value || '';
|
||||
this.titleNode.textContent = value;
|
||||
this.titleNode.title = value;
|
||||
}
|
||||
|
||||
protected override onAfterAttach(msg: Message): void {
|
||||
super.onAfterAttach(msg);
|
||||
this.addKeyListener(this.domNode, Key.ESCAPE, () => this.hide());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 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-only 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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// Based on https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts
|
||||
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { CancellationTokenSource } from '@theia/monaco-editor-core/esm/vs/base/common/cancellation';
|
||||
import { DEFAULT_WORD_REGEXP } from '@theia/monaco-editor-core/esm/vs/editor/common/core/wordHelper';
|
||||
import { IDecorationOptions } from '@theia/monaco-editor-core/esm/vs/editor/common/editorCommon';
|
||||
import { StandardTokenType } from '@theia/monaco-editor-core/esm/vs/editor/common/encodedTokenAttributes';
|
||||
import { InlineValueContext } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
|
||||
import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
|
||||
import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures';
|
||||
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
|
||||
import { DebugVariable, ExpressionContainer, ExpressionItem } from '../console/debug-console-items';
|
||||
import { DebugPreferences } from '../../common/debug-preferences';
|
||||
import { DebugStackFrame } from '../model/debug-stack-frame';
|
||||
import { DebugEditorModel } from './debug-editor-model';
|
||||
import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService';
|
||||
|
||||
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L40-L43
|
||||
export const INLINE_VALUE_DECORATION_KEY = 'inlinevaluedecoration';
|
||||
const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We want to limit ourselves for perf reasons
|
||||
const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added
|
||||
const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped
|
||||
|
||||
/**
|
||||
* MAX SMI (SMall Integer) as defined in v8.
|
||||
* one bit is lost for boxing/unboxing flag.
|
||||
* one bit is lost for sign flag.
|
||||
* See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values
|
||||
*/
|
||||
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/base/common/uint.ts#L7-L13
|
||||
const MAX_SAFE_SMALL_INTEGER = 1 << 30;
|
||||
|
||||
class InlineSegment {
|
||||
constructor(public column: number, public text: string) {
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DebugInlineValueDecorator implements FrontendApplicationContribution {
|
||||
@inject(DebugPreferences)
|
||||
protected readonly preferences: DebugPreferences;
|
||||
|
||||
protected enabled = false;
|
||||
protected wordToLineNumbersMap: Map<string, monaco.Position[]> | undefined = new Map();
|
||||
|
||||
onStart(): void {
|
||||
StandaloneServices.get(ICodeEditorService).registerDecorationType('Inline debug decorations', INLINE_VALUE_DECORATION_KEY, {});
|
||||
this.enabled = !!this.preferences['debug.inlineValues'];
|
||||
this.preferences.onPreferenceChanged(({ preferenceName }) => {
|
||||
if (preferenceName === 'debug.inlineValues') {
|
||||
const inlineValues = !!this.preferences['debug.inlineValues'];
|
||||
if (inlineValues !== this.enabled) {
|
||||
this.enabled = inlineValues;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async calculateDecorations(debugEditorModel: DebugEditorModel, stackFrame: DebugStackFrame | undefined): Promise<IDecorationOptions[]> {
|
||||
this.wordToLineNumbersMap = undefined;
|
||||
const model = debugEditorModel.editor.getControl().getModel() || undefined;
|
||||
return this.updateInlineValueDecorations(debugEditorModel, model, stackFrame);
|
||||
}
|
||||
|
||||
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L382-L408
|
||||
protected async updateInlineValueDecorations(
|
||||
debugEditorModel: DebugEditorModel,
|
||||
model: monaco.editor.ITextModel | undefined,
|
||||
stackFrame: DebugStackFrame | undefined): Promise<IDecorationOptions[]> {
|
||||
|
||||
if (!this.enabled || !model || !stackFrame || !stackFrame.source || model.uri.toString() !== stackFrame.source.uri.toString()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// XXX: Here is a difference between the VS Code's `IStackFrame` and the `DebugProtocol.StackFrame`.
|
||||
// In DAP, `source` is optional, hence `range` is optional too.
|
||||
const { range: stackFrameRange } = stackFrame;
|
||||
if (!stackFrameRange) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const scopes = await stackFrame.getMostSpecificScopes(stackFrameRange);
|
||||
// Get all top level children in the scope chain
|
||||
const decorationsPerScope = await Promise.all(scopes.map(async scope => {
|
||||
const children = Array.from(await scope.getElements());
|
||||
let range = new monaco.Range(0, 0, stackFrameRange.startLineNumber, stackFrameRange.startColumn);
|
||||
if (scope.range) {
|
||||
range = range.setStartPosition(scope.range.startLineNumber, scope.range.startColumn);
|
||||
}
|
||||
|
||||
return this.createInlineValueDecorationsInsideRange(children, range, model, debugEditorModel, stackFrame);
|
||||
}));
|
||||
|
||||
return decorationsPerScope.reduce((previous, current) => previous.concat(current), []);
|
||||
}
|
||||
|
||||
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L410-L452
|
||||
private async createInlineValueDecorationsInsideRange(
|
||||
expressions: ReadonlyArray<ExpressionContainer>,
|
||||
range: monaco.Range,
|
||||
model: monaco.editor.ITextModel,
|
||||
debugEditorModel: DebugEditorModel,
|
||||
stackFrame: DebugStackFrame): Promise<IDecorationOptions[]> {
|
||||
|
||||
const decorations: IDecorationOptions[] = [];
|
||||
|
||||
const inlineValuesProvider = StandaloneServices.get(ILanguageFeaturesService).inlineValuesProvider;
|
||||
const textEditorModel = debugEditorModel.editor.document.textEditorModel;
|
||||
|
||||
if (inlineValuesProvider && inlineValuesProvider.has(textEditorModel)) {
|
||||
|
||||
const findVariable = async (variableName: string, caseSensitiveLookup: boolean): Promise<DebugVariable | undefined> => {
|
||||
const scopes = await stackFrame.getMostSpecificScopes(stackFrame.range!);
|
||||
const key = caseSensitiveLookup ? variableName : variableName.toLowerCase();
|
||||
for (const scope of scopes) {
|
||||
const expressionContainers = await scope.getElements();
|
||||
let container = expressionContainers.next();
|
||||
while (!container.done) {
|
||||
const debugVariable = container.value;
|
||||
if (debugVariable && debugVariable instanceof DebugVariable) {
|
||||
if (caseSensitiveLookup) {
|
||||
if (debugVariable.name === key) {
|
||||
return debugVariable;
|
||||
}
|
||||
} else {
|
||||
if (debugVariable.name.toLowerCase() === key) {
|
||||
return debugVariable;
|
||||
}
|
||||
}
|
||||
}
|
||||
container = expressionContainers.next();
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const context: InlineValueContext = {
|
||||
frameId: stackFrame.raw.id,
|
||||
stoppedLocation: range
|
||||
};
|
||||
|
||||
const cancellationToken = new CancellationTokenSource().token;
|
||||
const registeredProviders = inlineValuesProvider.ordered(textEditorModel).reverse();
|
||||
const visibleRanges = debugEditorModel.editor.getControl().getVisibleRanges();
|
||||
|
||||
const lineDecorations = new Map<number, InlineSegment[]>();
|
||||
|
||||
for (const provider of registeredProviders) {
|
||||
for (const visibleRange of visibleRanges) {
|
||||
const result = await provider.provideInlineValues(textEditorModel, visibleRange, context, cancellationToken);
|
||||
if (result) {
|
||||
for (const inlineValue of result) {
|
||||
let text: string | undefined = undefined;
|
||||
switch (inlineValue.type) {
|
||||
case 'text':
|
||||
text = inlineValue.text;
|
||||
break;
|
||||
case 'variable': {
|
||||
let varName = inlineValue.variableName;
|
||||
if (!varName) {
|
||||
const lineContent = model.getLineContent(inlineValue.range.startLineNumber);
|
||||
varName = lineContent.substring(inlineValue.range.startColumn - 1, inlineValue.range.endColumn - 1);
|
||||
}
|
||||
const variable = await findVariable(varName, inlineValue.caseSensitiveLookup);
|
||||
if (variable) {
|
||||
text = this.formatInlineValue(varName, variable.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'expression': {
|
||||
let expr = inlineValue.expression;
|
||||
if (!expr) {
|
||||
const lineContent = model.getLineContent(inlineValue.range.startLineNumber);
|
||||
expr = lineContent.substring(inlineValue.range.startColumn - 1, inlineValue.range.endColumn - 1);
|
||||
}
|
||||
if (expr) {
|
||||
const expression = new ExpressionItem(expr, () => stackFrame.thread.session);
|
||||
await expression.evaluate('watch', false);
|
||||
if (expression.available) {
|
||||
text = this.formatInlineValue(expr, expression.value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (text) {
|
||||
const line = inlineValue.range.startLineNumber;
|
||||
let lineSegments = lineDecorations.get(line);
|
||||
if (!lineSegments) {
|
||||
lineSegments = [];
|
||||
lineDecorations.set(line, lineSegments);
|
||||
}
|
||||
if (!lineSegments.some(segment => segment.text === text)) {
|
||||
lineSegments.push(new InlineSegment(inlineValue.range.startColumn, text));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// sort line segments and concatenate them into a decoration
|
||||
const separator = ', ';
|
||||
lineDecorations.forEach((segments, line) => {
|
||||
if (segments.length > 0) {
|
||||
segments = segments.sort((a, b) => a.column - b.column);
|
||||
const text = segments.map(s => s.text).join(separator);
|
||||
decorations.push(this.createInlineValueDecoration(line, text));
|
||||
}
|
||||
});
|
||||
|
||||
} else { // use fallback if no provider was registered
|
||||
const lineToNamesMap: Map<number, string[]> = new Map<number, string[]>();
|
||||
const nameValueMap = new Map<string, string>();
|
||||
for (const expr of expressions) {
|
||||
if (expr instanceof DebugVariable) { // XXX: VS Code uses `IExpression` that has `name` and `value`.
|
||||
nameValueMap.set(expr.name, expr.value);
|
||||
}
|
||||
// Limit the size of map. Too large can have a perf impact
|
||||
if (nameValueMap.size >= MAX_NUM_INLINE_VALUES) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const wordToPositionsMap = this.getWordToPositionsMap(model);
|
||||
|
||||
// Compute unique set of names on each line
|
||||
nameValueMap.forEach((_, name) => {
|
||||
const positions = wordToPositionsMap.get(name);
|
||||
if (positions) {
|
||||
for (const position of positions) {
|
||||
if (range.containsPosition(position)) {
|
||||
if (!lineToNamesMap.has(position.lineNumber)) {
|
||||
lineToNamesMap.set(position.lineNumber, []);
|
||||
}
|
||||
|
||||
if (lineToNamesMap.get(position.lineNumber)!.indexOf(name) === -1) {
|
||||
lineToNamesMap.get(position.lineNumber)!.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Compute decorators for each line
|
||||
lineToNamesMap.forEach((names, line) => {
|
||||
const contentText = names.sort((first, second) => {
|
||||
const content = model.getLineContent(line);
|
||||
return content.indexOf(first) - content.indexOf(second);
|
||||
}).map(name => `${name} = ${nameValueMap.get(name)}`).join(', ');
|
||||
decorations.push(this.createInlineValueDecoration(line, contentText));
|
||||
});
|
||||
}
|
||||
|
||||
return decorations;
|
||||
}
|
||||
|
||||
protected formatInlineValue(...args: string[]): string {
|
||||
const valuePattern = '{0} = {1}';
|
||||
const formatRegExp = /{(\d+)}/g;
|
||||
if (args.length === 0) {
|
||||
return valuePattern;
|
||||
}
|
||||
return valuePattern.replace(formatRegExp, (match, group) => {
|
||||
const idx = parseInt(group, 10);
|
||||
return isNaN(idx) || idx < 0 || idx >= args.length ?
|
||||
match :
|
||||
args[idx];
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L454-L485
|
||||
private createInlineValueDecoration(lineNumber: number, contentText: string): IDecorationOptions {
|
||||
// If decoratorText is too long, trim and add ellipses. This could happen for minified files with everything on a single line
|
||||
if (contentText.length > MAX_INLINE_DECORATOR_LENGTH) {
|
||||
contentText = contentText.substring(0, MAX_INLINE_DECORATOR_LENGTH) + '...';
|
||||
}
|
||||
|
||||
return {
|
||||
range: {
|
||||
startLineNumber: lineNumber,
|
||||
endLineNumber: lineNumber,
|
||||
startColumn: MAX_SAFE_SMALL_INTEGER,
|
||||
endColumn: MAX_SAFE_SMALL_INTEGER
|
||||
},
|
||||
renderOptions: {
|
||||
after: {
|
||||
contentText,
|
||||
backgroundColor: 'rgba(255, 200, 0, 0.2)',
|
||||
margin: '10px'
|
||||
},
|
||||
dark: {
|
||||
after: {
|
||||
color: 'rgba(255, 255, 255, 0.5)',
|
||||
}
|
||||
},
|
||||
light: {
|
||||
after: {
|
||||
color: 'rgba(0, 0, 0, 0.5)',
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L487-L531
|
||||
private getWordToPositionsMap(model: monaco.editor.ITextModel | ITextModel): Map<string, monaco.Position[]> {
|
||||
model = model as ITextModel;
|
||||
if (!this.wordToLineNumbersMap) {
|
||||
this.wordToLineNumbersMap = new Map<string, monaco.Position[]>();
|
||||
if (!model) {
|
||||
return this.wordToLineNumbersMap;
|
||||
}
|
||||
|
||||
// For every word in every line, map its ranges for fast lookup
|
||||
for (let lineNumber = 1, len = model.getLineCount(); lineNumber <= len; ++lineNumber) {
|
||||
const lineContent = model.getLineContent(lineNumber);
|
||||
|
||||
// If line is too long then skip the line
|
||||
if (lineContent.length > MAX_TOKENIZATION_LINE_LEN) {
|
||||
continue;
|
||||
}
|
||||
|
||||
model.tokenization.forceTokenization(lineNumber);
|
||||
const lineTokens = model.tokenization.getLineTokens(lineNumber);
|
||||
for (let tokenIndex = 0, tokenCount = lineTokens.getCount(); tokenIndex < tokenCount; tokenIndex++) {
|
||||
const tokenStartOffset = lineTokens.getStartOffset(tokenIndex);
|
||||
const tokenEndOffset = lineTokens.getEndOffset(tokenIndex);
|
||||
const tokenType = lineTokens.getStandardTokenType(tokenIndex);
|
||||
const tokenStr = lineContent.substring(tokenStartOffset, tokenEndOffset);
|
||||
|
||||
// Token is a word and not a comment
|
||||
if (tokenType === StandardTokenType.Other) {
|
||||
DEFAULT_WORD_REGEXP.lastIndex = 0; // We assume tokens will usually map 1:1 to words if they match
|
||||
const wordMatch = DEFAULT_WORD_REGEXP.exec(tokenStr);
|
||||
|
||||
if (wordMatch) {
|
||||
const word = wordMatch[0];
|
||||
if (!this.wordToLineNumbersMap.has(word)) {
|
||||
this.wordToLineNumbersMap.set(word, []);
|
||||
}
|
||||
|
||||
this.wordToLineNumbersMap.get(word)!.push(new monaco.Position(lineNumber, tokenStartOffset));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.wordToLineNumbersMap;
|
||||
}
|
||||
|
||||
}
|
||||
161
packages/debug/src/browser/model/debug-breakpoint.tsx
Normal file
161
packages/debug/src/browser/model/debug-breakpoint.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol/lib/debugProtocol';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { EditorManager } from '@theia/editor/lib/browser';
|
||||
import { LabelProvider, DISABLED_CLASS, TreeWidget } from '@theia/core/lib/browser';
|
||||
import { TreeElement } from '@theia/core/lib/browser/source-tree';
|
||||
import { SelectableTreeNode } from '@theia/core/lib/browser/tree/tree-selection';
|
||||
import { DebugSession } from '../debug-session';
|
||||
import { BaseBreakpoint } from '../breakpoint/breakpoint-marker';
|
||||
import { BreakpointManager } from '../breakpoint/breakpoint-manager';
|
||||
import { nls } from '@theia/core';
|
||||
|
||||
export class DebugBreakpointData {
|
||||
readonly raw?: DebugProtocol.Breakpoint;
|
||||
}
|
||||
|
||||
export class DebugBreakpointOptions {
|
||||
readonly labelProvider: LabelProvider;
|
||||
readonly breakpoints: BreakpointManager;
|
||||
readonly editorManager: EditorManager;
|
||||
readonly session?: DebugSession;
|
||||
}
|
||||
|
||||
export class DebugBreakpointDecoration {
|
||||
readonly className: string;
|
||||
readonly message: string[];
|
||||
}
|
||||
|
||||
export abstract class DebugBreakpoint<T extends BaseBreakpoint = BaseBreakpoint> extends DebugBreakpointOptions implements TreeElement {
|
||||
|
||||
readonly raw?: DebugProtocol.Breakpoint;
|
||||
protected treeWidget?: TreeWidget;
|
||||
|
||||
constructor(
|
||||
readonly uri: URI,
|
||||
options: DebugBreakpointOptions
|
||||
) {
|
||||
super();
|
||||
Object.assign(this, options);
|
||||
}
|
||||
|
||||
abstract get origin(): T;
|
||||
|
||||
update(data: DebugBreakpointData): void {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
get idFromAdapter(): number | undefined {
|
||||
return this.raw && this.raw.id;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.origin.id;
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this.breakpoints.breakpointsEnabled && this.origin.enabled;
|
||||
}
|
||||
|
||||
get installed(): boolean {
|
||||
return !!this.raw;
|
||||
}
|
||||
|
||||
get verified(): boolean {
|
||||
return !!this.raw ? this.raw.verified : false;
|
||||
}
|
||||
|
||||
get message(): string {
|
||||
return this.raw && this.raw.message || '';
|
||||
}
|
||||
|
||||
abstract setEnabled(enabled: boolean): void;
|
||||
|
||||
abstract remove(): void;
|
||||
|
||||
protected readonly setBreakpointEnabled = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setEnabled(event.target.checked);
|
||||
};
|
||||
|
||||
render(host: TreeWidget): React.ReactNode {
|
||||
this.treeWidget = host;
|
||||
const classNames = ['theia-source-breakpoint'];
|
||||
if (!this.isEnabled()) {
|
||||
classNames.push(DISABLED_CLASS);
|
||||
}
|
||||
const decoration = this.getDecoration();
|
||||
return <div title={decoration.message.join('\n')} className={classNames.join(' ')}>
|
||||
<span className={'theia-debug-breakpoint-icon codicon ' + decoration.className} />
|
||||
<input className='theia-input' type='checkbox' checked={this.origin.enabled} onChange={this.setBreakpointEnabled} />
|
||||
{this.doRender()}
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected isEnabled(): boolean {
|
||||
return this.breakpoints.breakpointsEnabled && this.verified;
|
||||
}
|
||||
|
||||
protected abstract doRender(): React.ReactNode;
|
||||
|
||||
getDecoration(): DebugBreakpointDecoration {
|
||||
if (!this.enabled) {
|
||||
return this.getDisabledBreakpointDecoration();
|
||||
}
|
||||
if (this.installed && !this.verified) {
|
||||
return this.getUnverifiedBreakpointDecoration();
|
||||
}
|
||||
return this.doGetDecoration();
|
||||
}
|
||||
|
||||
protected getUnverifiedBreakpointDecoration(): DebugBreakpointDecoration {
|
||||
const decoration = this.getBreakpointDecoration();
|
||||
return {
|
||||
className: decoration.className + '-unverified',
|
||||
message: [this.message || nls.localize('theia/debug/unverifiedBreakpoint', 'Unverified {0}', decoration.message[0])]
|
||||
};
|
||||
}
|
||||
|
||||
protected getDisabledBreakpointDecoration(message?: string): DebugBreakpointDecoration {
|
||||
const decoration = this.getBreakpointDecoration();
|
||||
return {
|
||||
className: decoration.className + '-disabled',
|
||||
message: [message || nls.localize('theia/debug/disabledBreakpoint', 'Disabled {0}', decoration.message[0])]
|
||||
};
|
||||
}
|
||||
|
||||
protected doGetDecoration(messages: string[] = []): DebugBreakpointDecoration {
|
||||
if (this.message) {
|
||||
if (messages.length) {
|
||||
messages[messages.length - 1].concat(', ' + this.message);
|
||||
} else {
|
||||
messages.push(this.message);
|
||||
}
|
||||
}
|
||||
return this.getBreakpointDecoration(messages);
|
||||
}
|
||||
|
||||
protected abstract getBreakpointDecoration(message?: string[]): DebugBreakpointDecoration;
|
||||
|
||||
protected async selectInTree(): Promise<void> {
|
||||
if (this.treeWidget?.model && SelectableTreeNode.is(this)) {
|
||||
this.treeWidget.model.selectNode(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
94
packages/debug/src/browser/model/debug-data-breakpoint.tsx
Normal file
94
packages/debug/src/browser/model/debug-data-breakpoint.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 EclipseSource GmbH 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { nls } from '@theia/core';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { codicon } from '@theia/core/lib/browser';
|
||||
import { BreakpointManager } from '../breakpoint/breakpoint-manager';
|
||||
import { DataBreakpoint } from '../breakpoint/breakpoint-marker';
|
||||
import { DebugBreakpoint, DebugBreakpointDecoration, DebugBreakpointOptions } from './debug-breakpoint';
|
||||
|
||||
export class DebugDataBreakpoint extends DebugBreakpoint<DataBreakpoint> {
|
||||
constructor(readonly origin: DataBreakpoint, options: DebugBreakpointOptions) {
|
||||
super(BreakpointManager.DATA_URI, options);
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean): void {
|
||||
if (enabled !== this.origin.enabled) {
|
||||
this.breakpoints.updateDataBreakpoint(this.origin.id, { enabled });
|
||||
}
|
||||
}
|
||||
|
||||
protected override isEnabled(): boolean {
|
||||
return super.isEnabled() && this.isSupported();
|
||||
}
|
||||
|
||||
protected isSupported(): boolean {
|
||||
return Boolean(this.session?.capabilities.supportsDataBreakpoints);
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
this.breakpoints.removeDataBreakpoint(this.origin.id);
|
||||
}
|
||||
|
||||
protected doRender(): React.ReactNode {
|
||||
return <>
|
||||
<span className="line-info theia-data-breakpoint" title={this.origin.info.description}>
|
||||
<span className="name">{this.origin.info.description}</span>
|
||||
<span className="theia-TreeNodeInfo theia-access-type" >{this.getAccessType()}</span>
|
||||
</span>
|
||||
{this.renderActions()}
|
||||
</>;
|
||||
}
|
||||
|
||||
protected renderActions(): React.ReactNode {
|
||||
return <div className='theia-debug-breakpoint-actions'>
|
||||
<div className={codicon('close', true)} title={nls.localizeByDefault('Remove Breakpoint')} onClick={this.onRemove} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected onRemove = async () => {
|
||||
await this.selectInTree();
|
||||
this.remove();
|
||||
};
|
||||
|
||||
protected getAccessType(): string {
|
||||
switch (this.origin.raw.accessType) {
|
||||
case 'read': return 'Read';
|
||||
case 'write': return 'Write';
|
||||
default: return 'Access';
|
||||
}
|
||||
}
|
||||
|
||||
protected getBreakpointDecoration(message?: string[]): DebugBreakpointDecoration {
|
||||
if (!this.isSupported()) {
|
||||
return {
|
||||
className: 'codicon-debug-breakpoint-unsupported',
|
||||
message: message ?? [nls.localizeByDefault('Data Breakpoint')],
|
||||
};
|
||||
}
|
||||
if (this.origin.raw.condition || this.origin.raw.hitCondition) {
|
||||
return {
|
||||
className: 'codicon-debug-breakpoint-conditional',
|
||||
message: message || [nls.localize('theia/debug/conditionalBreakpoint', 'Conditional Breakpoint')]
|
||||
};
|
||||
}
|
||||
return {
|
||||
className: 'codicon-debug-breakpoint-data',
|
||||
message: message || [nls.localizeByDefault('Data Breakpoint')]
|
||||
};
|
||||
}
|
||||
}
|
||||
122
packages/debug/src/browser/model/debug-function-breakpoint.tsx
Normal file
122
packages/debug/src/browser/model/debug-function-breakpoint.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { TreeElement } from '@theia/core/lib/browser/source-tree';
|
||||
import { FunctionBreakpoint } from '../breakpoint/breakpoint-marker';
|
||||
import { BreakpointManager } from '../breakpoint/breakpoint-manager';
|
||||
import { DebugBreakpoint, DebugBreakpointOptions, DebugBreakpointDecoration } from './debug-breakpoint';
|
||||
import { SingleTextInputDialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { nls } from '@theia/core';
|
||||
import { codicon } from '@theia/core/lib/browser';
|
||||
|
||||
export class DebugFunctionBreakpoint extends DebugBreakpoint<FunctionBreakpoint> implements TreeElement {
|
||||
|
||||
constructor(readonly origin: FunctionBreakpoint, options: DebugBreakpointOptions) {
|
||||
super(BreakpointManager.FUNCTION_URI, options);
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean): void {
|
||||
const breakpoints = this.breakpoints.getFunctionBreakpoints();
|
||||
const breakpoint = breakpoints.find(b => b.id === this.id);
|
||||
if (breakpoint && breakpoint.enabled !== enabled) {
|
||||
breakpoint.enabled = enabled;
|
||||
this.breakpoints.setFunctionBreakpoints(breakpoints);
|
||||
}
|
||||
}
|
||||
|
||||
protected override isEnabled(): boolean {
|
||||
return super.isEnabled() && this.isSupported();
|
||||
}
|
||||
|
||||
protected isSupported(): boolean {
|
||||
const { session } = this;
|
||||
return !session || !!session.capabilities.supportsFunctionBreakpoints;
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
const breakpoints = this.breakpoints.getFunctionBreakpoints();
|
||||
const newBreakpoints = breakpoints.filter(b => b.id !== this.id);
|
||||
if (breakpoints.length !== newBreakpoints.length) {
|
||||
this.breakpoints.setFunctionBreakpoints(newBreakpoints);
|
||||
}
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.origin.raw.name;
|
||||
}
|
||||
|
||||
protected doRender(): React.ReactNode {
|
||||
return <React.Fragment>
|
||||
<span className='line-info'>{this.name}</span>
|
||||
{this.renderActions()}
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
protected renderActions(): React.ReactNode {
|
||||
return <div className='theia-debug-breakpoint-actions'>
|
||||
<div className={codicon('edit', true)} title={nls.localizeByDefault('Edit Condition...')} onClick={this.onEdit} />
|
||||
<div className={codicon('close', true)} title={nls.localizeByDefault('Remove Breakpoint')} onClick={this.onRemove} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected onEdit = async () => {
|
||||
await this.selectInTree();
|
||||
this.open();
|
||||
};
|
||||
|
||||
protected onRemove = async () => {
|
||||
await this.selectInTree();
|
||||
this.remove();
|
||||
};
|
||||
|
||||
protected override doGetDecoration(): DebugBreakpointDecoration {
|
||||
if (!this.isSupported()) {
|
||||
return this.getDisabledBreakpointDecoration(nls.localizeByDefault('Function breakpoints are not supported by this debug type'));
|
||||
}
|
||||
return super.doGetDecoration();
|
||||
}
|
||||
|
||||
protected getBreakpointDecoration(message?: string[]): DebugBreakpointDecoration {
|
||||
return {
|
||||
className: 'codicon-debug-breakpoint-function',
|
||||
message: message || [nls.localizeByDefault('Function Breakpoint')]
|
||||
};
|
||||
}
|
||||
|
||||
async open(): Promise<void> {
|
||||
const input = new SingleTextInputDialog({
|
||||
title: nls.localizeByDefault('Add Function Breakpoint'),
|
||||
initialValue: this.name
|
||||
});
|
||||
const newValue = await input.open();
|
||||
if (newValue !== undefined && newValue !== this.name) {
|
||||
const breakpoints = this.breakpoints.getFunctionBreakpoints();
|
||||
const breakpoint = breakpoints.find(b => b.id === this.id);
|
||||
if (breakpoint) {
|
||||
if (breakpoint.raw.name !== newValue) {
|
||||
breakpoint.raw.name = newValue;
|
||||
this.breakpoints.setFunctionBreakpoints(breakpoints);
|
||||
}
|
||||
} else {
|
||||
this.origin.raw.name = newValue;
|
||||
breakpoints.push(this.origin);
|
||||
this.breakpoints.setFunctionBreakpoints(breakpoints);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { nls } from '@theia/core';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { codicon } from '@theia/core/lib/browser';
|
||||
import { BreakpointManager } from '../breakpoint/breakpoint-manager';
|
||||
import { InstructionBreakpoint } from '../breakpoint/breakpoint-marker';
|
||||
import { DebugBreakpoint, DebugBreakpointDecoration, DebugBreakpointOptions } from './debug-breakpoint';
|
||||
|
||||
export class DebugInstructionBreakpoint extends DebugBreakpoint<InstructionBreakpoint> {
|
||||
constructor(readonly origin: InstructionBreakpoint, options: DebugBreakpointOptions) {
|
||||
super(BreakpointManager.INSTRUCTION_URI, options);
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean): void {
|
||||
if (enabled !== this.origin.enabled) {
|
||||
this.breakpoints.updateInstructionBreakpoint(this.origin.id, { enabled });
|
||||
}
|
||||
}
|
||||
|
||||
protected override isEnabled(): boolean {
|
||||
return super.isEnabled() && this.isSupported();
|
||||
}
|
||||
|
||||
protected isSupported(): boolean {
|
||||
return Boolean(this.session?.capabilities.supportsInstructionBreakpoints);
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
this.breakpoints.removeInstructionBreakpoint(this.origin.instructionReference);
|
||||
}
|
||||
|
||||
protected doRender(): React.ReactNode {
|
||||
return <React.Fragment>
|
||||
<span className="line-info">{this.origin.instructionReference}</span>;
|
||||
{this.renderActions()}
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
protected renderActions(): React.ReactNode {
|
||||
return <div className='theia-debug-breakpoint-actions'>
|
||||
<div className={codicon('close', true)} title={nls.localizeByDefault('Remove Breakpoint')} onClick={this.onRemove} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected onRemove = async () => {
|
||||
await this.selectInTree();
|
||||
this.remove();
|
||||
};
|
||||
|
||||
protected getBreakpointDecoration(message?: string[]): DebugBreakpointDecoration {
|
||||
if (!this.isSupported()) {
|
||||
return {
|
||||
className: 'codicon-debug-breakpoint-unsupported',
|
||||
message: message ?? [nls.localize('theia/debug/instruction-breakpoint', 'Instruction Breakpoint')],
|
||||
};
|
||||
}
|
||||
if (this.origin.condition || this.origin.hitCondition) {
|
||||
return {
|
||||
className: 'codicon-debug-breakpoint-conditional',
|
||||
message: message || [nls.localize('theia/debug/conditionalBreakpoint', 'Conditional Breakpoint')]
|
||||
};
|
||||
}
|
||||
return {
|
||||
className: 'codicon-debug-breakpoint',
|
||||
message: message || [nls.localize('theia/debug/instruction-breakpoint', 'Instruction Breakpoint')]
|
||||
};
|
||||
}
|
||||
}
|
||||
263
packages/debug/src/browser/model/debug-source-breakpoint.tsx
Normal file
263
packages/debug/src/browser/model/debug-source-breakpoint.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol/lib/debugProtocol';
|
||||
import { CommandService, nls, RecursivePartial } from '@theia/core';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { EditorWidget, Range } from '@theia/editor/lib/browser';
|
||||
import { TREE_NODE_INFO_CLASS, WidgetOpenerOptions, codicon } from '@theia/core/lib/browser';
|
||||
import { TreeElement } from '@theia/core/lib/browser/source-tree';
|
||||
import { SourceBreakpoint } from '../breakpoint/breakpoint-marker';
|
||||
import { DebugSource } from './debug-source';
|
||||
import { DebugBreakpoint, DebugBreakpointOptions, DebugBreakpointData, DebugBreakpointDecoration } from './debug-breakpoint';
|
||||
import { DebugCommands } from '../debug-commands';
|
||||
|
||||
export class DebugSourceBreakpointData extends DebugBreakpointData {
|
||||
readonly origins: SourceBreakpoint[];
|
||||
}
|
||||
|
||||
export class DebugSourceBreakpoint extends DebugBreakpoint<SourceBreakpoint> implements TreeElement {
|
||||
|
||||
protected readonly commandService: CommandService;
|
||||
readonly origins: SourceBreakpoint[];
|
||||
|
||||
constructor(origin: SourceBreakpoint, options: DebugBreakpointOptions, commandService: CommandService) {
|
||||
super(new URI(origin.uri), options);
|
||||
this.origins = [origin];
|
||||
this.commandService = commandService;
|
||||
}
|
||||
|
||||
override update(data: Partial<DebugSourceBreakpointData>): void {
|
||||
super.update(data);
|
||||
}
|
||||
|
||||
get origin(): SourceBreakpoint {
|
||||
return this.origins[0];
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean): void {
|
||||
const { uri, raw } = this;
|
||||
let shouldUpdate = false;
|
||||
const originLine = this.origin.raw.line;
|
||||
const originColumn = this.origin.raw.column;
|
||||
let breakpoints = raw && this.doRemove(this.origins.filter(origin => !(origin.raw.line === originLine && origin.raw.column === originColumn)));
|
||||
// Check for breakpoints array with at least one entry
|
||||
if (breakpoints && breakpoints.length) {
|
||||
shouldUpdate = true;
|
||||
} else {
|
||||
breakpoints = this.breakpoints.getBreakpoints(uri);
|
||||
}
|
||||
for (const breakpoint of breakpoints) {
|
||||
if (breakpoint.raw.line === this.origin.raw.line && breakpoint.raw.column === this.origin.raw.column && breakpoint.enabled !== enabled) {
|
||||
breakpoint.enabled = enabled;
|
||||
shouldUpdate = true;
|
||||
}
|
||||
}
|
||||
if (shouldUpdate) {
|
||||
this.breakpoints.setBreakpoints(this.uri, breakpoints);
|
||||
}
|
||||
}
|
||||
|
||||
updateOrigins(data: Partial<DebugProtocol.SourceBreakpoint>): void {
|
||||
const breakpoints = this.breakpoints.getBreakpoints(this.uri);
|
||||
let shouldUpdate = false;
|
||||
const originPositions = new Set();
|
||||
this.origins.forEach(origin => originPositions.add(origin.raw.line + ':' + origin.raw.column));
|
||||
for (const breakpoint of breakpoints) {
|
||||
if (originPositions.has(breakpoint.raw.line + ':' + breakpoint.raw.column)) {
|
||||
Object.assign(breakpoint.raw, data);
|
||||
shouldUpdate = true;
|
||||
}
|
||||
}
|
||||
if (shouldUpdate) {
|
||||
this.breakpoints.setBreakpoints(this.uri, breakpoints);
|
||||
}
|
||||
}
|
||||
|
||||
/** 1-based */
|
||||
get line(): number {
|
||||
return this.raw && this.raw.line || this.origins[0].raw.line;
|
||||
}
|
||||
get column(): number | undefined {
|
||||
return this.raw && this.raw.column || this.origins[0].raw.column;
|
||||
}
|
||||
get endLine(): number | undefined {
|
||||
return this.raw && this.raw.endLine;
|
||||
}
|
||||
get endColumn(): number | undefined {
|
||||
return this.raw && this.raw.endColumn;
|
||||
}
|
||||
|
||||
get condition(): string | undefined {
|
||||
return this.origin.raw.condition;
|
||||
}
|
||||
get hitCondition(): string | undefined {
|
||||
return this.origin.raw.hitCondition;
|
||||
}
|
||||
get logMessage(): string | undefined {
|
||||
return this.origin.raw.logMessage;
|
||||
}
|
||||
|
||||
get source(): DebugSource | undefined {
|
||||
return this.raw && this.raw.source && this.session && this.session.getSource(this.raw.source);
|
||||
}
|
||||
|
||||
async open(options: WidgetOpenerOptions = {
|
||||
mode: 'reveal'
|
||||
}): Promise<EditorWidget> {
|
||||
const { line, column, endLine, endColumn } = this;
|
||||
const selection: RecursivePartial<Range> = {
|
||||
start: {
|
||||
line: line - 1,
|
||||
character: typeof column === 'number' ? column - 1 : undefined
|
||||
}
|
||||
};
|
||||
if (typeof endLine === 'number') {
|
||||
selection.end = {
|
||||
line: endLine - 1,
|
||||
character: typeof endColumn === 'number' ? endColumn - 1 : undefined
|
||||
};
|
||||
}
|
||||
if (this.source) {
|
||||
return await this.source.open({
|
||||
...options,
|
||||
selection
|
||||
});
|
||||
} else {
|
||||
return await this.editorManager.open(this.uri, {
|
||||
...options,
|
||||
selection
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override setBreakpointEnabled = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setEnabled(event.target.checked);
|
||||
};
|
||||
|
||||
protected doRender(): React.ReactNode {
|
||||
return <React.Fragment>
|
||||
<span className='line-info' title={this.labelProvider.getLongName(this.uri)}>
|
||||
<span className='name'>{this.labelProvider.getName(this.uri)} </span>
|
||||
<span className={'path ' + TREE_NODE_INFO_CLASS}>{this.labelProvider.getLongName(this.uri.parent)} </span>
|
||||
</span>
|
||||
{this.renderActions()}
|
||||
<span className='line'>{this.renderPosition()}</span>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
protected renderActions(): React.ReactNode {
|
||||
return <div className='theia-debug-breakpoint-actions'>
|
||||
<div className={codicon('edit', true)} title={nls.localizeByDefault('Edit Breakpoint')} onClick={this.onEdit} />
|
||||
<div className={codicon('close', true)} title={nls.localizeByDefault('Remove Breakpoint')} onClick={this.onRemove} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected onEdit = async () => {
|
||||
await this.selectInTree();
|
||||
this.commandService.executeCommand(DebugCommands.EDIT_BREAKPOINT.id, this);
|
||||
};
|
||||
|
||||
protected onRemove = async () => {
|
||||
await this.selectInTree();
|
||||
this.remove();
|
||||
};
|
||||
|
||||
renderPosition(): string {
|
||||
return this.line + (typeof this.column === 'number' ? ':' + this.column : '');
|
||||
}
|
||||
|
||||
override doGetDecoration(messages: string[] = []): DebugBreakpointDecoration {
|
||||
if (this.logMessage || this.condition || this.hitCondition) {
|
||||
const { session } = this;
|
||||
if (this.logMessage) {
|
||||
if (session && !session.capabilities.supportsLogPoints) {
|
||||
return this.getUnsupportedBreakpointDecoration(nls.localize('theia/debug/logpointsNotSupported',
|
||||
'Logpoints not supported by this debug type'));
|
||||
}
|
||||
messages.push(nls.localizeByDefault('Log Message: {0}', this.logMessage));
|
||||
}
|
||||
if (this.condition) {
|
||||
if (session && !session.capabilities.supportsConditionalBreakpoints) {
|
||||
return this.getUnsupportedBreakpointDecoration(nls.localize('theia/debug/conditionalBreakpointsNotSupported',
|
||||
'Conditional breakpoints not supported by this debug type'));
|
||||
}
|
||||
messages.push(nls.localizeByDefault('Condition: {0}', this.condition));
|
||||
}
|
||||
if (this.hitCondition) {
|
||||
if (session && !session.capabilities.supportsHitConditionalBreakpoints) {
|
||||
return this.getUnsupportedBreakpointDecoration(nls.localize('theia/debug/htiConditionalBreakpointsNotSupported',
|
||||
'Hit conditional breakpoints not supported by this debug type'));
|
||||
}
|
||||
messages.push(nls.localizeByDefault('Hit Count: {0}', this.hitCondition));
|
||||
}
|
||||
}
|
||||
return super.doGetDecoration(messages);
|
||||
}
|
||||
|
||||
protected getUnsupportedBreakpointDecoration(message: string): DebugBreakpointDecoration {
|
||||
return {
|
||||
className: 'codicon-debug-breakpoint-unsupported',
|
||||
message: [message]
|
||||
};
|
||||
}
|
||||
|
||||
protected getBreakpointDecoration(message?: string[]): DebugBreakpointDecoration {
|
||||
if (this.logMessage) {
|
||||
return {
|
||||
className: 'codicon-debug-breakpoint-log',
|
||||
message: message || [nls.localizeByDefault('Logpoint')]
|
||||
};
|
||||
}
|
||||
if (this.condition || this.hitCondition) {
|
||||
return {
|
||||
className: 'codicon-debug-breakpoint-conditional',
|
||||
message: message || [nls.localize('theia/debug/conditionalBreakpoint', 'Conditional Breakpoint')]
|
||||
};
|
||||
}
|
||||
return {
|
||||
className: 'codicon-debug-breakpoint',
|
||||
message: message || [nls.localizeByDefault('Breakpoint')]
|
||||
};
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
const breakpoints = this.doRemove(this.origins);
|
||||
if (breakpoints) {
|
||||
this.breakpoints.setBreakpoints(this.uri, breakpoints);
|
||||
}
|
||||
}
|
||||
protected doRemove(origins: SourceBreakpoint[]): SourceBreakpoint[] | undefined {
|
||||
if (!origins.length) {
|
||||
return undefined;
|
||||
}
|
||||
const { uri } = this;
|
||||
const toRemove = new Set();
|
||||
origins.forEach(origin => toRemove.add(origin.raw.line + ':' + origin.raw.column));
|
||||
let shouldUpdate = false;
|
||||
const breakpoints = this.breakpoints.findMarkers({
|
||||
uri,
|
||||
dataFilter: data => {
|
||||
const result = !toRemove.has(data.raw.line + ':' + data.raw.column);
|
||||
shouldUpdate = shouldUpdate || !result;
|
||||
return result;
|
||||
}
|
||||
}).map(({ data }) => data);
|
||||
return shouldUpdate && breakpoints || undefined;
|
||||
}
|
||||
|
||||
}
|
||||
93
packages/debug/src/browser/model/debug-source.ts
Normal file
93
packages/debug/src/browser/model/debug-source.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { LabelProvider } from '@theia/core/lib/browser';
|
||||
import { EditorManager, EditorOpenerOptions, EditorWidget } from '@theia/editor/lib/browser';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol/lib/debugProtocol';
|
||||
import { DebugSession } from '../debug-session';
|
||||
import { URI as Uri } from '@theia/core/shared/vscode-uri';
|
||||
import { DEBUG_SCHEME, SCHEME_PATTERN } from '../../common/debug-uri-utils';
|
||||
|
||||
export class DebugSourceData {
|
||||
readonly raw: DebugProtocol.Source;
|
||||
}
|
||||
|
||||
export class DebugSource extends DebugSourceData {
|
||||
|
||||
constructor(
|
||||
protected readonly session: DebugSession,
|
||||
protected readonly editorManager: EditorManager,
|
||||
protected readonly labelProvider: LabelProvider
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
get uri(): URI {
|
||||
return DebugSource.toUri(this.raw);
|
||||
}
|
||||
|
||||
update(data: Partial<DebugSourceData>): void {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
open(options?: EditorOpenerOptions): Promise<EditorWidget> {
|
||||
return this.editorManager.open(this.uri, options);
|
||||
}
|
||||
|
||||
async load(): Promise<string> {
|
||||
const source = this.raw;
|
||||
const sourceReference = source.sourceReference!;
|
||||
const response = await this.session.sendRequest('source', {
|
||||
sourceReference,
|
||||
source
|
||||
});
|
||||
return response.body.content;
|
||||
}
|
||||
|
||||
get inMemory(): boolean {
|
||||
return this.uri.scheme === DEBUG_SCHEME;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
if (this.inMemory) {
|
||||
return this.raw.name || this.uri.path.base || this.uri.path.fsPath();
|
||||
}
|
||||
return this.labelProvider.getName(this.uri);
|
||||
}
|
||||
|
||||
get longName(): string {
|
||||
if (this.inMemory) {
|
||||
return this.name;
|
||||
}
|
||||
return this.labelProvider.getLongName(this.uri);
|
||||
}
|
||||
|
||||
static SCHEME = DEBUG_SCHEME;
|
||||
static SCHEME_PATTERN = SCHEME_PATTERN;
|
||||
static toUri(raw: DebugProtocol.Source): URI {
|
||||
if (raw.sourceReference && raw.sourceReference > 0) {
|
||||
return new URI().withScheme(DEBUG_SCHEME).withPath(raw.name!).withQuery(String(raw.sourceReference));
|
||||
}
|
||||
if (!raw.path) {
|
||||
throw new Error('Unrecognized source type: ' + JSON.stringify(raw));
|
||||
}
|
||||
if (raw.path.match(SCHEME_PATTERN)) {
|
||||
return new URI(raw.path);
|
||||
}
|
||||
return new URI(Uri.file(raw.path));
|
||||
}
|
||||
}
|
||||
184
packages/debug/src/browser/model/debug-stack-frame.tsx
Normal file
184
packages/debug/src/browser/model/debug-stack-frame.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
// *****************************************************************************
|
||||
// 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-only 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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// Based on https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/common/debugModel.ts
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { DISABLED_CLASS } from '@theia/core/lib/browser';
|
||||
import { EditorWidget, Range, Position, EditorOpenerOptions } from '@theia/editor/lib/browser';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol/lib/debugProtocol';
|
||||
import { TreeElement } from '@theia/core/lib/browser/source-tree';
|
||||
import { DebugScope } from '../console/debug-console-items';
|
||||
import { DebugSource } from './debug-source';
|
||||
import { RecursivePartial } from '@theia/core';
|
||||
import { DebugSession } from '../debug-session';
|
||||
import { DebugThread } from './debug-thread';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { stringHash } from '@theia/core/lib/common/hash';
|
||||
|
||||
export class DebugStackFrameData {
|
||||
readonly raw: DebugProtocol.StackFrame;
|
||||
}
|
||||
|
||||
export class DebugStackFrame extends DebugStackFrameData implements TreeElement {
|
||||
|
||||
constructor(
|
||||
readonly thread: DebugThread,
|
||||
readonly session: DebugSession,
|
||||
readonly id: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the frame identifier from the debug protocol.
|
||||
*/
|
||||
get frameId(): number {
|
||||
return this.raw.id;
|
||||
}
|
||||
|
||||
protected _source: DebugSource | undefined;
|
||||
get source(): DebugSource | undefined {
|
||||
return this._source;
|
||||
}
|
||||
update(data: Partial<DebugStackFrameData>): void {
|
||||
Object.assign(this, data);
|
||||
this._source = this.raw.source && this.session.getSource(this.raw.source);
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
await this.session.sendRequest('restartFrame', this.toArgs({
|
||||
threadId: this.thread.id
|
||||
}));
|
||||
}
|
||||
|
||||
async open(options?: EditorOpenerOptions): Promise<EditorWidget | undefined> {
|
||||
if (!this.source) {
|
||||
return undefined;
|
||||
}
|
||||
const { line, column, endLine, endColumn } = this.raw;
|
||||
const selection: RecursivePartial<Range> = {
|
||||
start: Position.create(this.clampPositive(line - 1), this.clampPositive(column - 1))
|
||||
};
|
||||
if (typeof endLine === 'number') {
|
||||
selection.end = {
|
||||
line: this.clampPositive(endLine - 1),
|
||||
character: typeof endColumn === 'number' ? this.clampPositive(endColumn - 1) : undefined
|
||||
};
|
||||
}
|
||||
this.source.open({
|
||||
mode: 'reveal',
|
||||
...options,
|
||||
selection
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Debugger can send `column: 0` value despite of initializing the debug session with `columnsStartAt1: true`.
|
||||
* This method can be used to ensure that neither `column` nor `column` are negative numbers.
|
||||
* See https://github.com/microsoft/vscode-mock-debug/issues/85.
|
||||
*/
|
||||
protected clampPositive(value: number): number {
|
||||
return Math.max(value, 0);
|
||||
}
|
||||
|
||||
protected scopes: Promise<DebugScope[]> | undefined;
|
||||
getScopes(): Promise<DebugScope[]> {
|
||||
return this.scopes || (this.scopes = this.doGetScopes());
|
||||
}
|
||||
protected async doGetScopes(): Promise<DebugScope[]> {
|
||||
let response;
|
||||
try {
|
||||
response = await this.session.sendRequest('scopes', this.toArgs());
|
||||
} catch {
|
||||
// no-op: ignore debug adapter errors
|
||||
}
|
||||
if (!response) {
|
||||
return [];
|
||||
}
|
||||
const scopeIds = new Set<number>();
|
||||
return response.body.scopes.map(raw => {
|
||||
// just as in VS Code, the id is based on the name and location to retain expansion state across multiple pauses
|
||||
let id = 0;
|
||||
do {
|
||||
id = stringHash(`${raw.name}:${raw.line}:${raw.column}`, id);
|
||||
} while (scopeIds.has(id));
|
||||
scopeIds.add(id);
|
||||
return new DebugScope(raw, () => this.session, id);
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/common/debugModel.ts#L324-L335
|
||||
async getMostSpecificScopes(range: monaco.IRange): Promise<DebugScope[]> {
|
||||
const scopes = await this.getScopes();
|
||||
const nonExpensiveScopes = scopes.filter(s => !s.expensive);
|
||||
const haveRangeInfo = nonExpensiveScopes.some(s => !!s.range);
|
||||
if (!haveRangeInfo) {
|
||||
return nonExpensiveScopes;
|
||||
}
|
||||
|
||||
const scopesContainingRange = nonExpensiveScopes.filter(scope => scope.range && monaco.Range.containsRange(scope.range, range))
|
||||
.sort((first, second) => (first.range!.endLineNumber - first.range!.startLineNumber) - (second.range!.endLineNumber - second.range!.startLineNumber));
|
||||
return scopesContainingRange.length ? scopesContainingRange : nonExpensiveScopes;
|
||||
}
|
||||
|
||||
protected toArgs<T extends object>(arg?: T): { frameId: number } & T {
|
||||
return Object.assign({}, arg, {
|
||||
frameId: this.raw.id
|
||||
});
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const classNames = ['theia-debug-stack-frame'];
|
||||
if (this.raw.presentationHint === 'label') {
|
||||
classNames.push('label');
|
||||
}
|
||||
if (this.raw.presentationHint === 'subtle') {
|
||||
classNames.push('subtle');
|
||||
}
|
||||
if (!this.source || this.source.raw.presentationHint === 'deemphasize') {
|
||||
classNames.push(DISABLED_CLASS);
|
||||
}
|
||||
return <div className={classNames.join(' ')}>
|
||||
<span className='expression' title={this.raw.name}>{this.raw.name}</span>
|
||||
{this.renderFile()}
|
||||
</div>;
|
||||
}
|
||||
protected renderFile(): React.ReactNode {
|
||||
const { source } = this;
|
||||
if (!source) {
|
||||
return undefined;
|
||||
}
|
||||
const origin = source.raw.origin && `\n${source.raw.origin}` || '';
|
||||
return <span className='file' title={source.longName + origin}>
|
||||
<span className='name'>{source.name}</span>
|
||||
<span className='line'>{this.raw.line}:{this.raw.column}</span>
|
||||
</span>;
|
||||
}
|
||||
|
||||
get range(): monaco.IRange | undefined {
|
||||
const { source, line: startLine, column: startColumn, endLine, endColumn } = this.raw;
|
||||
if (source) {
|
||||
return new monaco.Range(startLine, startColumn, endLine || startLine, endColumn || startColumn);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
320
packages/debug/src/browser/model/debug-thread.tsx
Normal file
320
packages/debug/src/browser/model/debug-thread.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { CancellationTokenSource, Emitter, Event, MessageType, nls } from '@theia/core';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol/lib/debugProtocol';
|
||||
import { TreeElement } from '@theia/core/lib/browser/source-tree';
|
||||
import { DebugStackFrame } from './debug-stack-frame';
|
||||
import { DebugSession } from '../debug-session';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
export type StoppedDetails = DebugProtocol.StoppedEvent['body'] & {
|
||||
framesErrorMessage?: string
|
||||
totalFrames?: number
|
||||
};
|
||||
|
||||
export class DebugThreadData {
|
||||
readonly raw: DebugProtocol.Thread;
|
||||
readonly stoppedDetails: StoppedDetails | undefined;
|
||||
}
|
||||
|
||||
export interface DebugExceptionInfo {
|
||||
id?: string
|
||||
description?: string
|
||||
details?: DebugProtocol.ExceptionDetails
|
||||
}
|
||||
|
||||
export class DebugThread extends DebugThreadData implements TreeElement {
|
||||
|
||||
protected readonly onDidChangedEmitter = new Emitter<void>();
|
||||
readonly onDidChanged: Event<void> = this.onDidChangedEmitter.event;
|
||||
protected readonly onDidFocusStackFrameEmitter = new Emitter<DebugStackFrame | undefined>();
|
||||
get onDidFocusStackFrame(): Event<DebugStackFrame | undefined> {
|
||||
return this.onDidFocusStackFrameEmitter.event;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly session: DebugSession
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.session.id + ':' + this.raw.id;
|
||||
}
|
||||
|
||||
get threadId(): number {
|
||||
return this.raw.id;
|
||||
}
|
||||
|
||||
protected _currentFrame: DebugStackFrame | undefined;
|
||||
get currentFrame(): DebugStackFrame | undefined {
|
||||
return this._currentFrame;
|
||||
}
|
||||
set currentFrame(frame: DebugStackFrame | undefined) {
|
||||
if (this._currentFrame?.id === frame?.id) {
|
||||
return;
|
||||
}
|
||||
this._currentFrame = frame;
|
||||
this.onDidChangedEmitter.fire(undefined);
|
||||
this.onDidFocusStackFrameEmitter.fire(frame);
|
||||
}
|
||||
|
||||
get stopped(): boolean {
|
||||
return !!this.stoppedDetails;
|
||||
}
|
||||
|
||||
update(data: Partial<DebugThreadData>): void {
|
||||
Object.assign(this, data);
|
||||
if ('stoppedDetails' in data) {
|
||||
this.clearFrames();
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.update({
|
||||
raw: this.raw,
|
||||
stoppedDetails: undefined
|
||||
});
|
||||
}
|
||||
|
||||
continue(): Promise<DebugProtocol.ContinueResponse> {
|
||||
return this.session.sendRequest('continue', this.toArgs());
|
||||
}
|
||||
|
||||
stepOver(): Promise<DebugProtocol.NextResponse> {
|
||||
return this.session.sendRequest('next', this.toArgs());
|
||||
}
|
||||
|
||||
stepIn(): Promise<DebugProtocol.StepInResponse> {
|
||||
return this.session.sendRequest('stepIn', this.toArgs());
|
||||
}
|
||||
|
||||
stepOut(): Promise<DebugProtocol.StepOutResponse> {
|
||||
return this.session.sendRequest('stepOut', this.toArgs());
|
||||
}
|
||||
|
||||
pause(): Promise<DebugProtocol.PauseResponse> {
|
||||
return this.session.sendRequest('pause', this.toArgs());
|
||||
}
|
||||
|
||||
get supportsGoto(): boolean {
|
||||
return !!this.session.capabilities.supportsGotoTargetsRequest;
|
||||
}
|
||||
|
||||
async jumpToCursor(uri: URI, position: monaco.Position): Promise<DebugProtocol.GotoResponse | undefined> {
|
||||
const source = await this.session?.toDebugSource(uri);
|
||||
|
||||
if (!source) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const response: DebugProtocol.GotoTargetsResponse = await this.session.sendRequest('gotoTargets', { source, line: position.lineNumber, column: position.column });
|
||||
|
||||
if (response && response.body.targets.length === 0) {
|
||||
this.session.showMessage(MessageType.Warning, nls.localizeByDefault('No executable code is associated at the current cursor position.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = response.body.targets[0].id;
|
||||
return this.session.sendRequest('goto', this.toArgs({ targetId }));
|
||||
}
|
||||
|
||||
async getExceptionInfo(): Promise<DebugExceptionInfo | undefined> {
|
||||
if (this.stoppedDetails && this.stoppedDetails.reason === 'exception') {
|
||||
if (this.session.capabilities.supportsExceptionInfoRequest) {
|
||||
const response = await this.session.sendRequest('exceptionInfo', this.toArgs());
|
||||
return {
|
||||
id: response.body.exceptionId,
|
||||
description: response.body.description,
|
||||
details: response.body.details
|
||||
};
|
||||
}
|
||||
return {
|
||||
description: this.stoppedDetails.text
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get supportsTerminate(): boolean {
|
||||
return !!this.session.capabilities.supportsTerminateThreadsRequest;
|
||||
}
|
||||
|
||||
async terminate(): Promise<void> {
|
||||
if (this.supportsTerminate) {
|
||||
await this.session.sendRequest('terminateThreads', {
|
||||
threadIds: [this.raw.id]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly _frames = new Map<string, DebugStackFrame>();
|
||||
get frames(): IterableIterator<DebugStackFrame> {
|
||||
return this._frames.values();
|
||||
}
|
||||
get topFrame(): DebugStackFrame | undefined {
|
||||
return this.frames.next().value;
|
||||
}
|
||||
get frameCount(): number {
|
||||
return this._frames.size;
|
||||
}
|
||||
|
||||
protected pendingFetch = Promise.resolve<DebugStackFrame[]>([]);
|
||||
protected _pendingFetchCount: number = 0;
|
||||
protected pendingFetchCancel = new CancellationTokenSource();
|
||||
async fetchFrames(levels: number = 20): Promise<DebugStackFrame[]> {
|
||||
const cancel = this.pendingFetchCancel.token;
|
||||
this._pendingFetchCount += 1;
|
||||
|
||||
return this.pendingFetch = this.pendingFetch.then(async () => {
|
||||
try {
|
||||
const start = this.frameCount;
|
||||
const frames = await this.doFetchFrames(start, levels);
|
||||
if (cancel.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
return this.doUpdateFrames(start, frames);
|
||||
} catch (e) {
|
||||
console.error('fetchFrames failed:', e);
|
||||
return [];
|
||||
} finally {
|
||||
if (!cancel.isCancellationRequested) {
|
||||
this._pendingFetchCount -= 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
get pendingFrameCount(): number {
|
||||
return this._pendingFetchCount;
|
||||
}
|
||||
protected async doFetchFrames(startFrame: number, levels: number): Promise<DebugProtocol.StackFrame[]> {
|
||||
try {
|
||||
const response = await this.session.sendRequest('stackTrace',
|
||||
this.toArgs<Partial<DebugProtocol.StackTraceArguments>>({ startFrame, levels })
|
||||
);
|
||||
if (this.stoppedDetails) {
|
||||
this.stoppedDetails.totalFrames = response.body.totalFrames;
|
||||
}
|
||||
return response.body.stackFrames;
|
||||
} catch (e) {
|
||||
if (this.stoppedDetails) {
|
||||
this.stoppedDetails.framesErrorMessage = e.message;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
protected doUpdateFrames(startFrame: number, frames: DebugProtocol.StackFrame[]): DebugStackFrame[] {
|
||||
const result = new Set<DebugStackFrame>();
|
||||
frames.forEach((raw, index) => {
|
||||
// Similarly to VS Code, we no longer use `raw.frameId` in computation of the DebugStackFrame id.
|
||||
// The `raw.frameId` was changing in every step and was not allowing us to correlate stack frames between step events.
|
||||
// Refs: https://github.com/microsoft/vscode/commit/18fc2bcf718e22265b5e09bb7fd0e9c090264cd2, https://github.com/microsoft/vscode/issues/93230#issuecomment-642558395
|
||||
const source = raw.source && this.session.getSource(raw.source);
|
||||
const id = `${this.id}:${startFrame + index}:${source?.name}`;
|
||||
const frame = this._frames.get(id) || new DebugStackFrame(this, this.session, id);
|
||||
this._frames.set(id, frame);
|
||||
frame.update({ raw });
|
||||
result.add(frame);
|
||||
});
|
||||
this.updateCurrentFrame();
|
||||
return [...result.values()];
|
||||
}
|
||||
protected clearFrames(): void {
|
||||
// Clear all frames
|
||||
this._frames.clear();
|
||||
|
||||
// Cancel all request promises
|
||||
this.pendingFetchCancel.cancel();
|
||||
this.pendingFetchCancel = new CancellationTokenSource();
|
||||
|
||||
// Empty all current requests
|
||||
this.pendingFetch = Promise.resolve([]);
|
||||
this._pendingFetchCount = 0;
|
||||
|
||||
this.updateCurrentFrame();
|
||||
}
|
||||
protected updateCurrentFrame(): void {
|
||||
const { currentFrame } = this;
|
||||
const id = currentFrame && currentFrame.id;
|
||||
this.currentFrame = typeof id === 'string' &&
|
||||
this._frames.get(id) ||
|
||||
this._frames.values().next().value;
|
||||
}
|
||||
|
||||
protected toArgs<T extends object>(arg?: T): { threadId: number } & T {
|
||||
return Object.assign({}, arg, {
|
||||
threadId: this.raw.id
|
||||
});
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return (
|
||||
<div className="theia-debug-thread" title={nls.localizeByDefault('Session')}>
|
||||
<span className="label">{this.raw.name}</span>
|
||||
<span className="status">{this.threadStatus()}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
protected threadStatus(): string {
|
||||
|
||||
if (!this.stoppedDetails) {
|
||||
return nls.localizeByDefault('Running');
|
||||
}
|
||||
|
||||
const description = this.stoppedDetails.description;
|
||||
|
||||
if (description) {
|
||||
// According to DAP we must show description as is. Translation is made by debug adapter
|
||||
return description;
|
||||
}
|
||||
|
||||
const reason = this.stoppedDetails.reason;
|
||||
const localizedReason = this.getLocalizedReason(reason);
|
||||
|
||||
return reason
|
||||
? nls.localizeByDefault('Paused on {0}', localizedReason)
|
||||
: nls.localizeByDefault('Paused');
|
||||
}
|
||||
|
||||
protected getLocalizedReason(reason: string | undefined): string {
|
||||
switch (reason) {
|
||||
case 'step':
|
||||
return nls.localize('theia/debug/step', 'step');
|
||||
case 'breakpoint':
|
||||
return nls.localize('theia/debug/breakpoint', 'breakpoint');
|
||||
case 'exception':
|
||||
return nls.localize('theia/debug/exception', 'exception');
|
||||
case 'pause':
|
||||
return nls.localize('theia/debug/pause', 'pause');
|
||||
case 'entry':
|
||||
return nls.localize('theia/debug/entry', 'entry');
|
||||
case 'goto':
|
||||
return nls.localize('theia/debug/goto', 'goto');
|
||||
case 'function breakpoint':
|
||||
return nls.localize('theia/debug/functionBreakpoint', 'function breakpoint');
|
||||
case 'data breakpoint':
|
||||
return nls.localize('theia/debug/dataBreakpoint', 'data breakpoint');
|
||||
case 'instruction breakpoint':
|
||||
return nls.localize('theia/debug/instructionBreakpoint', 'instruction breakpoint');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
47
packages/debug/src/browser/style/debug.css
Normal file
47
packages/debug/src/browser/style/debug.css
Normal file
@@ -0,0 +1,47 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2025 M. Kachurin 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-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
#debugConsoleSeverity {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.debug-console-filter-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--theia-input-background);
|
||||
border: solid var(--theia-border-width) var(--theia-dropdown-border);
|
||||
border-radius: var(--theia-search-box-radius);
|
||||
height: var(--theia-content-line-height);
|
||||
}
|
||||
|
||||
.debug-console-filter-container:focus-within {
|
||||
border-color: var(--theia-focusBorder);
|
||||
}
|
||||
|
||||
.debug-console-filter-container input.theia-input {
|
||||
flex: 1;
|
||||
min-width: 175px;
|
||||
box-sizing: border-box;
|
||||
background: none;
|
||||
border: none !important;
|
||||
outline: none;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.debug-console-filter-btn {
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
}
|
||||
495
packages/debug/src/browser/style/index.css
Normal file
495
packages/debug/src/browser/style/index.css
Normal file
@@ -0,0 +1,495 @@
|
||||
/********************************************************************************
|
||||
* 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-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
.theia-debug-container,
|
||||
.theia-session-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.theia-debug-container .theia-select {
|
||||
margin-left: var(--theia-ui-padding);
|
||||
}
|
||||
|
||||
.theia-source-breakpoint,
|
||||
.theia-debug-session,
|
||||
.theia-debug-thread,
|
||||
.theia-debug-stack-frame {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.theia-source-breakpoint > span,
|
||||
.theia-debug-session > span,
|
||||
.theia-debug-thread > span,
|
||||
.theia-debug-stack-frame > span,
|
||||
.theia-debug-stack-frame .file > span {
|
||||
margin-left: calc(var(--theia-ui-padding) / 2);
|
||||
}
|
||||
|
||||
.theia-debug-session .label,
|
||||
.theia-debug-thread .label,
|
||||
.theia-debug-stack-frame .expression {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.theia-source-breakpoint .path {
|
||||
font-size: var(--theia-ui-font-size0);
|
||||
}
|
||||
|
||||
.theia-source-breakpoint .line-info {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.theia-source-breakpoint .line,
|
||||
.theia-debug-stack-frame .line {
|
||||
background: var(--theia-descriptionForeground);
|
||||
color: var(--theia-editor-background);
|
||||
padding: calc(var(--theia-ui-padding) / 3);
|
||||
font-size: var(--theia-ui-font-size0);
|
||||
line-height: calc(var(--theia-private-horizontal-tab-height) / 2);
|
||||
border-radius: 2px;
|
||||
margin-left: calc(var(--theia-ui-padding) / 2);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.theia-data-breakpoint .theia-access-type {
|
||||
margin-left: var(--theia-ui-padding);
|
||||
}
|
||||
|
||||
.theia-debug-session .status,
|
||||
.theia-debug-thread .status {
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.theia-debug-stack-frame .expression {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.theia-debug-stack-frame.label {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.theia-debug-stack-frame.subtle {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.theia-debug-stack-frame .file > span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.theia-load-more-frames {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/** Miscellaneous */
|
||||
|
||||
.debug-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: calc(var(--theia-ui-padding) * 2);
|
||||
padding-right: calc(var(--theia-ui-padding) * 3);
|
||||
padding-bottom: var(--theia-ui-padding);
|
||||
padding-left: calc(var(--theia-ui-padding) * 2);
|
||||
}
|
||||
|
||||
.theia-session-container > .debug-toolbar {
|
||||
padding-top: var(--theia-ui-padding);
|
||||
padding-bottom: var(--theia-ui-padding);
|
||||
border-bottom: 1px solid var(--theia-sideBarSectionHeader-border);
|
||||
}
|
||||
|
||||
.debug-toolbar .theia-select-component {
|
||||
width: 100%;
|
||||
min-width: 40px;
|
||||
margin: 0px 4px;
|
||||
}
|
||||
|
||||
.debug-toolbar .debug-action {
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
padding-left: var(--theia-ui-padding);
|
||||
padding-right: var(--theia-ui-padding);
|
||||
min-width: var(--theia-icon-size);
|
||||
min-height: var(--theia-icon-size);
|
||||
}
|
||||
|
||||
.debug-toolbar .debug-action.theia-mod-disabled {
|
||||
opacity: 0.5 !important;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.debug-toolbar .debug-action:not(.theia-mod-disabled):hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.debug-toolbar .debug-action > div {
|
||||
font-family: var(--theia-ui-font-family);
|
||||
font-size: var(--theia-ui-font-size0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
/** Console */
|
||||
|
||||
#debug-console .theia-console-info {
|
||||
color: var(--theia-debugConsole-infoForeground) !important;
|
||||
}
|
||||
#debug-console .theia-console-warning {
|
||||
color: var(--theia-debugConsole-warningForeground) !important;
|
||||
}
|
||||
#debug-console .theia-console-error {
|
||||
color: var(--theia-debugConsole-errorForeground) !important;
|
||||
}
|
||||
|
||||
.theia-debug-console-unavailable {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.theia-debug-console-expression {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.theia-debug-console-variable {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--theia-variable-value-color);
|
||||
}
|
||||
|
||||
.theia-debug-hover-title.number,
|
||||
.theia-debug-console-variable.number {
|
||||
color: var(--theia-variable-number-variable-color);
|
||||
}
|
||||
|
||||
.theia-debug-hover-title.boolean,
|
||||
.theia-debug-console-variable.boolean {
|
||||
color: var(--theia-variable-boolean-variable-color);
|
||||
}
|
||||
|
||||
.theia-debug-hover-title.string,
|
||||
.theia-debug-console-variable.string {
|
||||
color: var(--theia-variable-string-variable-color);
|
||||
}
|
||||
|
||||
.theia-debug-console-variable .name {
|
||||
color: var(--theia-variable-name-color);
|
||||
}
|
||||
|
||||
.theia-debug-console-variable .lazy-button {
|
||||
margin-left: 3px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
color: var(--theia-icon-foreground);
|
||||
}
|
||||
|
||||
.theia-debug-console-variable .lazy-button:hover {
|
||||
background-color: var(--theia-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.theia-debug-console-variable .value {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.theia-TreeNode:not(:hover) .theia-debug-console-variable .action-label {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.theia-debug-console-variable .action-label {
|
||||
/* Vertically center the button in the tree node */
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.theia-debug-console-variable .watch-error {
|
||||
font-style: italic;
|
||||
color: var(--theia-debugConsole-errorForeground);
|
||||
}
|
||||
|
||||
.theia-debug-console-variable .watch-not-available {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.theia-debug-watch-expression {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.theia-debug-breakpoint-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theia-source-breakpoint:hover .theia-debug-breakpoint-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/** Editor **/
|
||||
|
||||
.theia-debug-breakpoint-icon {
|
||||
font-size: 19px !important;
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
min-width: 19px;
|
||||
margin-left: 0px !important;
|
||||
}
|
||||
|
||||
.codicon-debug-hint {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.codicon-debug-breakpoint,
|
||||
.codicon-debug-breakpoint-conditional,
|
||||
.codicon-debug-breakpoint-log,
|
||||
.codicon-debug-breakpoint-function,
|
||||
.codicon-debug-breakpoint-data,
|
||||
.codicon-debug-breakpoint-unsupported,
|
||||
.codicon-debug-hint:not([class*="codicon-debug-breakpoint"]):not(
|
||||
[class*="codicon-debug-stackframe"]
|
||||
),
|
||||
.codicon-debug-breakpoint.codicon-debug-stackframe-focused::after,
|
||||
.codicon-debug-breakpoint.codicon-debug-stackframe::after {
|
||||
color: var(--theia-debugIcon-breakpointForeground) !important;
|
||||
}
|
||||
|
||||
.codicon[class*="-disabled"] {
|
||||
color: var(--theia-debugIcon-breakpointDisabledForeground) !important;
|
||||
}
|
||||
|
||||
.codicon[class*="-unverified"] {
|
||||
color: var(--theia-debugIcon-breakpointUnverifiedForeground) !important;
|
||||
}
|
||||
|
||||
.codicon-debug-stackframe,
|
||||
.monaco-editor .theia-debug-top-stack-frame-column::before {
|
||||
color: var(
|
||||
--theia-debugIcon-breakpointCurrentStackframeForeground
|
||||
) !important;
|
||||
}
|
||||
|
||||
.codicon-debug-stackframe-focused {
|
||||
color: var(--theia-debugIcon-breakpointStackframeForeground) !important;
|
||||
}
|
||||
|
||||
.codicon-debug-breakpoint.codicon-debug-stackframe-focused::after,
|
||||
.codicon-debug-breakpoint.codicon-debug-stackframe::after {
|
||||
content: "\eb8a";
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.monaco-editor .theia-debug-breakpoint-column {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.monaco-editor .theia-debug-top-stack-frame-column::before {
|
||||
content: " ";
|
||||
width: 0.9em;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
margin-top: -1px; /* TODO @misolori: figure out a way to not use negative margin for alignment */
|
||||
}
|
||||
|
||||
.monaco-editor .theia-debug-top-stack-frame-column {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.monaco-editor .theia-debug-top-stack-frame-column::before {
|
||||
content: "\eb8b";
|
||||
font: normal normal normal 16px/1 codicon;
|
||||
text-rendering: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
margin-left: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.codicon-debug-hint:not([class*="codicon-debug-breakpoint"]):not(
|
||||
[class*="codicon-debug-stackframe"]
|
||||
) {
|
||||
opacity: 0.4 !important;
|
||||
}
|
||||
|
||||
.monaco-editor .view-overlays .theia-debug-top-stack-frame-line {
|
||||
background: var(--theia-editor-stackFrameHighlightBackground);
|
||||
}
|
||||
|
||||
.monaco-editor .view-overlays .theia-debug-focused-stack-frame-line {
|
||||
background: var(--theia-editor-focusedStackFrameHighlightBackground);
|
||||
}
|
||||
|
||||
/** Toolbars */
|
||||
|
||||
.codicon-debug-start {
|
||||
color: var(--theia-debugIcon-startForeground) !important;
|
||||
}
|
||||
.codicon-debug-pause {
|
||||
color: var(--theia-debugIcon-pauseForeground) !important;
|
||||
}
|
||||
|
||||
.codicon-debug-stop {
|
||||
color: var(--theia-debugIcon-stopForeground) !important;
|
||||
}
|
||||
.codicon-debug-disconnect {
|
||||
color: var(--theia-debugIcon-disconnectForeground) !important;
|
||||
}
|
||||
.codicon-debug-restart,
|
||||
.codicon-debug-restart-frame {
|
||||
color: var(--theia-debugIcon-restartForeground) !important;
|
||||
}
|
||||
.codicon-debug-step-over {
|
||||
color: var(--theia-debugIcon-stepOverForeground) !important;
|
||||
}
|
||||
.codicon-debug-step-into {
|
||||
color: var(--theia-debugIcon-stepIntoForeground) !important;
|
||||
}
|
||||
.codicon-debug-step-out {
|
||||
color: var(--theia-debugIcon-stepOutForeground) !important;
|
||||
}
|
||||
.codicon-debug-continue,
|
||||
.codicon-debug-reverse-continue {
|
||||
color: var(--theia-debugIcon-continueForeground) !important;
|
||||
}
|
||||
|
||||
.codicon-debug-step-back {
|
||||
color: var(--theia-debugIcon-stepBackForeground) !important;
|
||||
}
|
||||
|
||||
/** Hover */
|
||||
.theia-debug-hover {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--theia-editorHoverWidget-border);
|
||||
background: var(--theia-editorHoverWidget-background);
|
||||
color: var(--theia-editorHoverWidget-foreground);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.theia-debug-hover.complex-value {
|
||||
min-width: 324px;
|
||||
min-height: 324px;
|
||||
width: 324px;
|
||||
height: 324px;
|
||||
}
|
||||
|
||||
.theia-debug-hover .theia-source-tree {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theia-debug-hover-title {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding: var(--theia-ui-padding);
|
||||
border-bottom: 1px solid var(--theia-editorHoverWidget-border);
|
||||
}
|
||||
|
||||
.theia-debug-hover-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/** Breakpoint Widget */
|
||||
.theia-debug-breakpoint-widget {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.theia-debug-breakpoint-select {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 0 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.theia-debug-breakpoint-input {
|
||||
flex: 1;
|
||||
margin-top: var(--theia-ui-padding);
|
||||
margin-bottom: var(--theia-ui-padding);
|
||||
}
|
||||
|
||||
.theia-debug-breakpoint-input .monaco-editor {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Status Bar */
|
||||
.theia-mod-debugging #theia-statusBar {
|
||||
background: var(--theia-statusBar-debuggingBackground);
|
||||
border-top: var(--theia-border-width) solid
|
||||
var(--theia-statusBar-debuggingBorder);
|
||||
}
|
||||
|
||||
.theia-mod-debugging #theia-statusBar .area .element {
|
||||
color: var(--theia-statusBar-debuggingForeground);
|
||||
}
|
||||
|
||||
/** Exception Widget */
|
||||
.monaco-editor
|
||||
.zone-widget
|
||||
.zone-widget-container.theia-debug-exception-widget {
|
||||
color: var(--theia-editor-foreground);
|
||||
font-size: var(--theia-code-font-size);
|
||||
line-height: var(--theia-code-line-height);
|
||||
font-family: var(--theia-code-font-family);
|
||||
border-top-color: var(--theia-debugExceptionWidget-border);
|
||||
border-bottom-color: var(--theia-debugExceptionWidget-border);
|
||||
background-color: var(--theia-debugExceptionWidget-background);
|
||||
padding: var(--theia-ui-padding) calc(var(--theia-ui-padding) * 1.5);
|
||||
white-space: pre-wrap;
|
||||
user-select: text;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.theia-debug-exception-widget .title {
|
||||
font-family: var(--theia-ui-font-family);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.theia-debug-exception-widget .stack-trace {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.theia-debug-exception-widget #exception-close {
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
59
packages/debug/src/browser/view/debug-action.tsx
Normal file
59
packages/debug/src/browser/view/debug-action.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { codiconArray, DISABLED_CLASS } from '@theia/core/lib/browser';
|
||||
import { MenuPath } from '@theia/core';
|
||||
|
||||
export class DebugAction extends React.Component<DebugAction.Props> {
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const { enabled, label, tooltip, iconClass } = this.props;
|
||||
const classNames = ['debug-action'];
|
||||
if (iconClass) {
|
||||
classNames.push(...codiconArray(iconClass, true));
|
||||
}
|
||||
if (enabled === false) {
|
||||
classNames.push(DISABLED_CLASS);
|
||||
}
|
||||
return <span tabIndex={0}
|
||||
className={classNames.join(' ')}
|
||||
title={tooltip || label}
|
||||
onClick={() => { this.props.run([]); }}
|
||||
ref={this.setRef} >
|
||||
{!iconClass && <div>{label}</div>}
|
||||
</span>;
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
if (this.ref) {
|
||||
this.ref.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected ref: HTMLElement | undefined;
|
||||
protected setRef = (ref: HTMLElement | null) => this.ref = ref || undefined;
|
||||
|
||||
}
|
||||
export namespace DebugAction {
|
||||
export interface Props {
|
||||
label: string
|
||||
tooltip?: string
|
||||
iconClass: string
|
||||
run: (effectiveMenuPath: MenuPath) => void
|
||||
enabled?: boolean
|
||||
}
|
||||
}
|
||||
51
packages/debug/src/browser/view/debug-breakpoints-source.tsx
Normal file
51
packages/debug/src/browser/view/debug-breakpoints-source.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { TreeSource, TreeElement } from '@theia/core/lib/browser/source-tree';
|
||||
import { DebugViewModel } from './debug-view-model';
|
||||
import { BreakpointManager } from '../breakpoint/breakpoint-manager';
|
||||
import { DebugExceptionBreakpoint } from './debug-exception-breakpoint';
|
||||
import { CommandService } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class DebugBreakpointsSource extends TreeSource {
|
||||
|
||||
@inject(DebugViewModel)
|
||||
protected readonly model: DebugViewModel;
|
||||
|
||||
@inject(BreakpointManager)
|
||||
protected readonly breakpoints: BreakpointManager;
|
||||
|
||||
@inject(CommandService)
|
||||
protected readonly commandService: CommandService;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.fireDidChange();
|
||||
this.toDispose.push(this.model.onDidChangeBreakpoints(() => this.fireDidChange()));
|
||||
}
|
||||
|
||||
*getElements(): IterableIterator<TreeElement> {
|
||||
for (const exceptionBreakpoint of this.breakpoints.getExceptionBreakpoints()) {
|
||||
yield new DebugExceptionBreakpoint(exceptionBreakpoint, this.breakpoints, this.commandService);
|
||||
}
|
||||
yield* this.model.dataBreakpoints;
|
||||
yield* this.model.functionBreakpoints;
|
||||
yield* this.model.instructionBreakpoints;
|
||||
yield* this.model.breakpoints;
|
||||
}
|
||||
}
|
||||
72
packages/debug/src/browser/view/debug-breakpoints-widget.ts
Normal file
72
packages/debug/src/browser/view/debug-breakpoints-widget.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct, interfaces, Container } from '@theia/core/shared/inversify';
|
||||
import { MenuPath } from '@theia/core/lib/common';
|
||||
import { TreeNode, NodeProps } from '@theia/core/lib/browser';
|
||||
import { SourceTreeWidget } from '@theia/core/lib/browser/source-tree';
|
||||
import { DebugBreakpointsSource } from './debug-breakpoints-source';
|
||||
import { BreakpointManager } from '../breakpoint/breakpoint-manager';
|
||||
import { DebugViewModel } from './debug-view-model';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export class DebugBreakpointsWidget extends SourceTreeWidget {
|
||||
|
||||
static CONTEXT_MENU: MenuPath = ['debug-breakpoints-context-menu'];
|
||||
static EDIT_MENU = [...DebugBreakpointsWidget.CONTEXT_MENU, 'a_edit'];
|
||||
static REMOVE_MENU = [...DebugBreakpointsWidget.CONTEXT_MENU, 'b_remove'];
|
||||
static ENABLE_MENU = [...DebugBreakpointsWidget.CONTEXT_MENU, 'c_enable'];
|
||||
static FACTORY_ID = 'debug:breakpoints';
|
||||
static override createContainer(parent: interfaces.Container): Container {
|
||||
const child = SourceTreeWidget.createContainer(parent, {
|
||||
contextMenuPath: DebugBreakpointsWidget.CONTEXT_MENU,
|
||||
virtualized: false,
|
||||
scrollIfActive: true,
|
||||
multiSelect: true
|
||||
});
|
||||
child.bind(DebugBreakpointsSource).toSelf();
|
||||
child.unbind(SourceTreeWidget);
|
||||
child.bind(DebugBreakpointsWidget).toSelf();
|
||||
return child;
|
||||
}
|
||||
static createWidget(parent: interfaces.Container): DebugBreakpointsWidget {
|
||||
return DebugBreakpointsWidget.createContainer(parent).get(DebugBreakpointsWidget);
|
||||
}
|
||||
|
||||
@inject(DebugViewModel)
|
||||
protected readonly viewModel: DebugViewModel;
|
||||
|
||||
@inject(BreakpointManager)
|
||||
protected readonly breakpoints: BreakpointManager;
|
||||
|
||||
@inject(DebugBreakpointsSource)
|
||||
protected readonly breakpointsSource: DebugBreakpointsSource;
|
||||
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.id = DebugBreakpointsWidget.FACTORY_ID + ':' + this.viewModel.id;
|
||||
this.title.label = nls.localizeByDefault('Breakpoints');
|
||||
this.toDispose.push(this.breakpointsSource);
|
||||
this.source = this.breakpointsSource;
|
||||
}
|
||||
|
||||
protected override getDefaultNodeStyle(node: TreeNode, props: NodeProps): React.CSSProperties | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
267
packages/debug/src/browser/view/debug-configuration-select.tsx
Normal file
267
packages/debug/src/browser/view/debug-configuration-select.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2021 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { DebugConfigurationManager } from '../debug-configuration-manager';
|
||||
import { DebugSessionOptions } from '../debug-session-options';
|
||||
import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component';
|
||||
import { QuickInputService } from '@theia/core/lib/browser';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { DebugSessionConfigurationLabelProvider } from '../debug-session-configuration-label-provider';
|
||||
|
||||
interface DynamicPickItem { label: string, configurationType: string, request: string, providerType: string, workspaceFolderUri?: string }
|
||||
|
||||
export interface DebugConfigurationSelectProps {
|
||||
manager: DebugConfigurationManager,
|
||||
quickInputService: QuickInputService,
|
||||
labelProvider: DebugSessionConfigurationLabelProvider,
|
||||
isMultiRoot: boolean
|
||||
}
|
||||
|
||||
export interface DebugProviderSelectState {
|
||||
providerTypes: string[],
|
||||
currentValue: string | undefined
|
||||
}
|
||||
|
||||
export class DebugConfigurationSelect extends React.Component<DebugConfigurationSelectProps, DebugProviderSelectState> {
|
||||
protected static readonly SEPARATOR = '──────────';
|
||||
protected static readonly PICK = '__PICK__';
|
||||
protected static readonly NO_CONFIGURATION = '__NO_CONF__';
|
||||
protected static readonly ADD_CONFIGURATION = '__ADD_CONF__';
|
||||
protected static readonly CONFIG_MARKER = '__CONFIG__';
|
||||
|
||||
private readonly selectRef = React.createRef<SelectComponent>();
|
||||
private manager: DebugConfigurationManager;
|
||||
private quickInputService: QuickInputService;
|
||||
|
||||
constructor(props: DebugConfigurationSelectProps) {
|
||||
super(props);
|
||||
this.manager = props.manager;
|
||||
this.quickInputService = props.quickInputService;
|
||||
this.state = {
|
||||
providerTypes: [],
|
||||
currentValue: undefined
|
||||
};
|
||||
this.manager.onDidChangeConfigurationProviders(() => {
|
||||
this.refreshDebugConfigurations();
|
||||
});
|
||||
}
|
||||
|
||||
override componentDidUpdate(): void {
|
||||
// synchronize the currentValue with the selectComponent value
|
||||
if (this.selectRef.current?.value !== this.currentValue) {
|
||||
this.refreshDebugConfigurations();
|
||||
}
|
||||
}
|
||||
|
||||
override componentDidMount(): void {
|
||||
this.refreshDebugConfigurations();
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
return <SelectComponent
|
||||
options={this.renderOptions()}
|
||||
defaultValue={this.state.currentValue}
|
||||
onChange={option => this.setCurrentConfiguration(option)}
|
||||
onFocus={() => this.refreshDebugConfigurations()}
|
||||
onBlur={() => this.refreshDebugConfigurations()}
|
||||
ref={this.selectRef}
|
||||
/>;
|
||||
}
|
||||
|
||||
protected get currentValue(): string {
|
||||
const { current } = this.manager;
|
||||
const matchingOption = this.getCurrentOption(current);
|
||||
return matchingOption ? matchingOption.value! : current ? JSON.stringify(current) : DebugConfigurationSelect.NO_CONFIGURATION;
|
||||
}
|
||||
|
||||
protected getCurrentOption(current: DebugSessionOptions | undefined): SelectOption | undefined {
|
||||
if (!current || !this.selectRef.current) {
|
||||
return;
|
||||
}
|
||||
const matchingOption = this.selectRef.current!.options.find(option =>
|
||||
option.userData === DebugConfigurationSelect.CONFIG_MARKER
|
||||
&& this.matchesOption(JSON.parse(option.value!), current)
|
||||
);
|
||||
return matchingOption;
|
||||
}
|
||||
|
||||
protected matchesOption(sessionOption: DebugSessionOptions, current: DebugSessionOptions): boolean {
|
||||
const matchesNameAndWorkspace = sessionOption.name === current.name && sessionOption.workspaceFolderUri === current.workspaceFolderUri;
|
||||
return DebugSessionOptions.isConfiguration(sessionOption) && DebugSessionOptions.isConfiguration(current)
|
||||
? matchesNameAndWorkspace && sessionOption.providerType === current.providerType
|
||||
: matchesNameAndWorkspace;
|
||||
}
|
||||
|
||||
protected readonly setCurrentConfiguration = (option: SelectOption) => {
|
||||
const value = option.value;
|
||||
if (!value) {
|
||||
return false;
|
||||
} else if (value === DebugConfigurationSelect.ADD_CONFIGURATION) {
|
||||
setTimeout(() => this.manager.addConfiguration());
|
||||
} else if (value.startsWith(DebugConfigurationSelect.PICK)) {
|
||||
const providerType = this.parsePickValue(value);
|
||||
this.selectDynamicConfigFromQuickPick(providerType);
|
||||
} else {
|
||||
const data = JSON.parse(value) as DebugSessionOptions;
|
||||
this.manager.current = data;
|
||||
this.refreshDebugConfigurations();
|
||||
}
|
||||
};
|
||||
|
||||
protected toPickValue(providerType: string): string {
|
||||
return DebugConfigurationSelect.PICK + providerType;
|
||||
}
|
||||
|
||||
protected parsePickValue(value: string): string {
|
||||
return value.slice(DebugConfigurationSelect.PICK.length);
|
||||
}
|
||||
|
||||
protected async resolveDynamicConfigurationPicks(providerType: string): Promise<DynamicPickItem[]> {
|
||||
const configurationsOfProviderType =
|
||||
(await this.manager.provideDynamicDebugConfigurations())[providerType];
|
||||
|
||||
if (!configurationsOfProviderType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return configurationsOfProviderType.map(options => ({
|
||||
label: options.configuration.name,
|
||||
configurationType: options.configuration.type,
|
||||
request: options.configuration.request,
|
||||
providerType: options.providerType,
|
||||
description: this.toBaseName(options.workspaceFolderUri),
|
||||
workspaceFolderUri: options.workspaceFolderUri
|
||||
}));
|
||||
}
|
||||
|
||||
protected async selectDynamicConfigFromQuickPick(providerType: string): Promise<void> {
|
||||
const picks: DynamicPickItem[] = await this.resolveDynamicConfigurationPicks(providerType);
|
||||
|
||||
if (picks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selected: DynamicPickItem | undefined = await this.quickInputService.showQuickPick(
|
||||
picks,
|
||||
{
|
||||
placeholder: nls.localizeByDefault('Select Launch Configuration')
|
||||
}
|
||||
);
|
||||
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedConfiguration = {
|
||||
name: selected.label,
|
||||
type: selected.configurationType,
|
||||
request: selected.request
|
||||
};
|
||||
this.manager.current = this.manager.find(selectedConfiguration, selected.workspaceFolderUri, selected.providerType);
|
||||
this.refreshDebugConfigurations();
|
||||
}
|
||||
|
||||
protected refreshDebugConfigurations = async () => {
|
||||
const configsOptionsPerType = await this.manager.provideDynamicDebugConfigurations();
|
||||
const providerTypes = [];
|
||||
for (const [type, configurationsOptions] of Object.entries(configsOptionsPerType)) {
|
||||
if (configurationsOptions.length > 0) {
|
||||
providerTypes.push(type);
|
||||
}
|
||||
}
|
||||
|
||||
const value = this.currentValue;
|
||||
this.selectRef.current!.value = value;
|
||||
this.setState({ providerTypes, currentValue: value });
|
||||
};
|
||||
|
||||
protected renderOptions(): SelectOption[] {
|
||||
const options: SelectOption[] = [];
|
||||
|
||||
// Add non dynamic debug configurations
|
||||
for (const config of this.manager.all) {
|
||||
const value = JSON.stringify(config);
|
||||
options.push({
|
||||
value,
|
||||
label: this.toName(config, this.props.isMultiRoot),
|
||||
userData: DebugConfigurationSelect.CONFIG_MARKER
|
||||
});
|
||||
}
|
||||
|
||||
// Add recently used dynamic debug configurations
|
||||
const { recentDynamicOptions } = this.manager;
|
||||
if (recentDynamicOptions.length > 0) {
|
||||
if (options.length > 0) {
|
||||
options.push({
|
||||
separator: true
|
||||
});
|
||||
}
|
||||
for (const dynamicOption of recentDynamicOptions) {
|
||||
const value = JSON.stringify(dynamicOption);
|
||||
options.push({
|
||||
value,
|
||||
label: this.toName(dynamicOption, this.props.isMultiRoot) + ' (' + dynamicOption.providerType + ')',
|
||||
userData: DebugConfigurationSelect.CONFIG_MARKER
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Placing a 'No Configuration' entry enables proper functioning of the 'onChange' event, by
|
||||
// having an entry to switch from (E.g. a case where only one dynamic configuration type is available)
|
||||
if (options.length === 0) {
|
||||
const value = DebugConfigurationSelect.NO_CONFIGURATION;
|
||||
options.push({
|
||||
value,
|
||||
label: nls.localizeByDefault('No Configurations')
|
||||
});
|
||||
}
|
||||
|
||||
// Add dynamic configuration types for quick pick selection
|
||||
const types = this.state.providerTypes;
|
||||
if (types.length > 0) {
|
||||
options.push({
|
||||
separator: true
|
||||
});
|
||||
for (const type of types) {
|
||||
const value = this.toPickValue(type);
|
||||
options.push({
|
||||
value,
|
||||
label: type + '...'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
options.push({
|
||||
separator: true
|
||||
});
|
||||
options.push({
|
||||
value: DebugConfigurationSelect.ADD_CONFIGURATION,
|
||||
label: nls.localizeByDefault('Add Configuration...')
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
protected toName(options: DebugSessionOptions, multiRoot: boolean): string {
|
||||
return this.props.labelProvider.getLabel(options, multiRoot);
|
||||
}
|
||||
|
||||
protected toBaseName(uri: string | undefined): string {
|
||||
return uri ? new URI(uri).path.base : '';
|
||||
}
|
||||
}
|
||||
131
packages/debug/src/browser/view/debug-configuration-widget.tsx
Normal file
131
packages/debug/src/browser/view/debug-configuration-widget.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ReactWidget, QuickInputService } from '@theia/core/lib/browser';
|
||||
import { CommandRegistry, Disposable, DisposableCollection, MessageService } from '@theia/core/lib/common';
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
import { DebugConsoleContribution } from '../console/debug-console-contribution';
|
||||
import { DebugConfigurationManager } from '../debug-configuration-manager';
|
||||
import { DebugCommands } from '../debug-commands';
|
||||
import { DebugSessionManager } from '../debug-session-manager';
|
||||
import { DebugAction } from './debug-action';
|
||||
import { DebugConfigurationSelect } from './debug-configuration-select';
|
||||
import { DebugViewModel } from './debug-view-model';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { DebugSessionOptions } from '../debug-session-options';
|
||||
import { DebugSessionConfigurationLabelProvider } from '../debug-session-configuration-label-provider';
|
||||
|
||||
@injectable()
|
||||
export class DebugConfigurationWidget extends ReactWidget {
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(DebugViewModel)
|
||||
protected readonly viewModel: DebugViewModel;
|
||||
|
||||
@inject(DebugConfigurationManager)
|
||||
protected readonly manager: DebugConfigurationManager;
|
||||
|
||||
@inject(DebugSessionManager)
|
||||
protected readonly sessionManager: DebugSessionManager;
|
||||
|
||||
@inject(DebugConsoleContribution)
|
||||
protected readonly debugConsole: DebugConsoleContribution;
|
||||
|
||||
@inject(QuickInputService)
|
||||
protected readonly quickInputService: QuickInputService;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@inject(DebugSessionConfigurationLabelProvider)
|
||||
protected readonly sessionConfigurationLabelProvider: DebugSessionConfigurationLabelProvider;
|
||||
|
||||
protected readonly onRender = new DisposableCollection();
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.addClass('debug-toolbar');
|
||||
this.toDispose.push(this.manager.onDidChange(() => this.update()));
|
||||
this.toDispose.push(this.workspaceService.onWorkspaceChanged(() => this.update()));
|
||||
this.toDispose.push(this.workspaceService.onWorkspaceLocationChanged(() => this.update()));
|
||||
this.scrollOptions = undefined;
|
||||
this.update();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
if (!this.doFocus()) {
|
||||
this.onRender.push(Disposable.create(() => this.doFocus()));
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
protected doFocus(): boolean {
|
||||
if (!this.stepRef) {
|
||||
return false;
|
||||
}
|
||||
this.stepRef.focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected stepRef: DebugAction | undefined;
|
||||
protected setStepRef = (stepRef: DebugAction | null) => {
|
||||
this.stepRef = stepRef || undefined;
|
||||
this.onRender.dispose();
|
||||
};
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <React.Fragment>
|
||||
<DebugAction run={this.start} label={nls.localizeByDefault('Start Debugging')} iconClass='debug-start' ref={this.setStepRef} />
|
||||
<DebugConfigurationSelect
|
||||
manager={this.manager}
|
||||
quickInputService={this.quickInputService}
|
||||
isMultiRoot={this.workspaceService.isMultiRootWorkspaceOpened}
|
||||
labelProvider={this.sessionConfigurationLabelProvider}
|
||||
/>
|
||||
<DebugAction run={this.openConfiguration} label={nls.localizeByDefault('Open {0}', '"launch.json"')}
|
||||
iconClass='settings-gear' />
|
||||
<DebugAction run={this.openConsole} label={nls.localizeByDefault('Debug Console')} iconClass='terminal' />
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
protected readonly start = async () => {
|
||||
let configuration;
|
||||
try {
|
||||
configuration = await this.manager.getSelectedConfiguration();
|
||||
} catch (e) {
|
||||
this.messageService.error(e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (DebugSessionOptions.isConfiguration(configuration)) {
|
||||
configuration.startedByUser = true;
|
||||
}
|
||||
|
||||
this.commandRegistry.executeCommand(DebugCommands.START.id, configuration);
|
||||
};
|
||||
|
||||
protected readonly openConfiguration = () => this.manager.openConfiguration();
|
||||
protected readonly openConsole = () => this.debugConsole.openView({
|
||||
activate: true
|
||||
});
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { TreeElement } from '@theia/core/lib/browser/source-tree';
|
||||
import { BreakpointManager } from '../breakpoint/breakpoint-manager';
|
||||
import { ExceptionBreakpoint } from '../breakpoint/breakpoint-marker';
|
||||
import { SingleTextInputDialog, TREE_NODE_INFO_CLASS, codicon, TreeWidget } from '@theia/core/lib/browser';
|
||||
import { SelectableTreeNode } from '@theia/core/lib/browser/tree/tree-selection';
|
||||
import { nls, CommandService } from '@theia/core';
|
||||
import { DebugCommands } from '../debug-commands';
|
||||
|
||||
export class DebugExceptionBreakpoint implements TreeElement {
|
||||
|
||||
readonly id: string;
|
||||
protected treeWidget?: TreeWidget;
|
||||
|
||||
constructor(
|
||||
readonly data: ExceptionBreakpoint,
|
||||
readonly breakpoints: BreakpointManager,
|
||||
protected readonly commandService: CommandService
|
||||
) {
|
||||
this.id = data.raw.filter + ':' + data.raw.label;
|
||||
}
|
||||
|
||||
render(host: TreeWidget): React.ReactNode {
|
||||
this.treeWidget = host;
|
||||
return <div title={this.data.raw.description || this.data.raw.label} className='theia-source-breakpoint'>
|
||||
<span className='theia-debug-breakpoint-icon' />
|
||||
<input type='checkbox' checked={this.data.enabled} onChange={this.toggle} />
|
||||
<span className='line-info'>
|
||||
<span className='name'>{this.data.raw.label} </span>
|
||||
{this.data.condition &&
|
||||
<span title={nls.localizeByDefault('Expression condition: {0}', this.data.condition)}
|
||||
className={'path ' + TREE_NODE_INFO_CLASS}>{this.data.condition} </span>}
|
||||
</span>
|
||||
{this.renderActions()}
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected renderActions(): React.ReactNode {
|
||||
if (this.data.raw.supportsCondition) {
|
||||
return <div className='theia-debug-breakpoint-actions'>
|
||||
<div className={codicon('edit', true)} title={nls.localizeByDefault('Edit Condition...')} onClick={this.onEdit} />
|
||||
</div>;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected onEdit = async () => {
|
||||
await this.selectInTree();
|
||||
this.commandService.executeCommand(DebugCommands.EDIT_BREAKPOINT_CONDITION.id);
|
||||
};
|
||||
|
||||
protected async selectInTree(): Promise<void> {
|
||||
if (this.treeWidget?.model && SelectableTreeNode.is(this)) {
|
||||
this.treeWidget.model.selectNode(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected toggle = () => this.breakpoints.toggleExceptionBreakpoint(this.data.raw.filter);
|
||||
|
||||
async editCondition(): Promise<void> {
|
||||
const inputDialog = new SingleTextInputDialog({
|
||||
title: this.data.raw.label,
|
||||
placeholder: this.data.raw.conditionDescription,
|
||||
initialValue: this.data.condition
|
||||
});
|
||||
let condition = await inputDialog.open();
|
||||
if (condition === undefined) {
|
||||
return;
|
||||
}
|
||||
if (condition === '') {
|
||||
condition = undefined;
|
||||
}
|
||||
if (condition !== this.data.condition) {
|
||||
this.breakpoints.updateExceptionBreakpoint(this.data.raw.filter, { condition });
|
||||
}
|
||||
}
|
||||
}
|
||||
119
packages/debug/src/browser/view/debug-session-widget.ts
Normal file
119
packages/debug/src/browser/view/debug-session-widget.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, postConstruct, interfaces, Container } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
Message, ApplicationShell, Widget, BaseWidget, PanelLayout, StatefulWidget, ViewContainer, codicon, ViewContainerTitleOptions, WidgetManager
|
||||
} from '@theia/core/lib/browser';
|
||||
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';
|
||||
import { DebugToolBar } from './debug-toolbar-widget';
|
||||
import { DebugViewModel } from './debug-view-model';
|
||||
import { DebugWatchWidget } from './debug-watch-widget';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
|
||||
export const DEBUG_VIEW_CONTAINER_TITLE_OPTIONS: ViewContainerTitleOptions = {
|
||||
label: 'debug',
|
||||
iconClass: codicon('debug-alt'),
|
||||
closeable: true
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class DebugSessionWidget extends BaseWidget implements StatefulWidget, ApplicationShell.TrackableWidgetProvider {
|
||||
|
||||
static createContainer(parent: interfaces.Container): Container {
|
||||
const child = new Container({ defaultScope: 'Singleton' });
|
||||
child.parent = parent;
|
||||
child.bind(DebugViewModel).toSelf();
|
||||
child.bind(DebugToolBar).toSelf();
|
||||
child.bind(DebugSessionWidget).toSelf();
|
||||
return child;
|
||||
}
|
||||
|
||||
static createWidget(parent: interfaces.Container): DebugSessionWidget {
|
||||
return DebugSessionWidget.createContainer(parent).get(DebugSessionWidget);
|
||||
}
|
||||
|
||||
static subwidgets = [DebugThreadsWidget, DebugStackFramesWidget, DebugVariablesWidget, DebugWatchWidget, DebugBreakpointsWidget];
|
||||
|
||||
protected viewContainer: ViewContainer;
|
||||
|
||||
@inject(ViewContainer.Factory)
|
||||
protected readonly viewContainerFactory: ViewContainer.Factory;
|
||||
|
||||
@inject(DebugViewModel)
|
||||
readonly model: DebugViewModel;
|
||||
|
||||
@inject(DebugToolBar)
|
||||
protected readonly toolbar: DebugToolBar;
|
||||
|
||||
@inject(WidgetManager) protected readonly widgetManager: WidgetManager;
|
||||
@inject(FrontendApplicationStateService) protected readonly stateService: FrontendApplicationStateService;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.id = 'debug:session:' + this.model.id;
|
||||
this.title.label = this.model.label;
|
||||
this.title.caption = this.model.label;
|
||||
this.title.closable = true;
|
||||
this.title.iconClass = codicon('debug-alt');
|
||||
this.addClass('theia-session-container');
|
||||
|
||||
this.viewContainer = this.viewContainerFactory({
|
||||
id: 'debug:view-container:' + this.model.id
|
||||
});
|
||||
this.viewContainer.setTitleOptions(DEBUG_VIEW_CONTAINER_TITLE_OPTIONS);
|
||||
this.stateService.reachedState('initialized_layout').then(() => {
|
||||
for (const subwidget of DebugSessionWidget.subwidgets) {
|
||||
const widgetPromises = [];
|
||||
const existingWidget = this.widgetManager.tryGetPendingWidget(subwidget.FACTORY_ID);
|
||||
// No other view container instantiated this widget during startup.
|
||||
if (!existingWidget) {
|
||||
widgetPromises.push(this.widgetManager.getOrCreateWidget(subwidget.FACTORY_ID));
|
||||
}
|
||||
Promise.all(widgetPromises).then(widgets => widgets.forEach(widget => this.viewContainer.addWidget(widget)));
|
||||
}
|
||||
});
|
||||
|
||||
this.toDispose.pushAll([
|
||||
this.toolbar,
|
||||
this.viewContainer
|
||||
]);
|
||||
|
||||
const layout = this.layout = new PanelLayout();
|
||||
layout.addWidget(this.toolbar);
|
||||
layout.addWidget(this.viewContainer);
|
||||
}
|
||||
|
||||
protected override onAfterShow(msg: Message): void {
|
||||
super.onAfterShow(msg);
|
||||
this.getTrackableWidgets().forEach(w => w.update());
|
||||
}
|
||||
|
||||
getTrackableWidgets(): Widget[] {
|
||||
return [this.viewContainer];
|
||||
}
|
||||
|
||||
storeState(): object {
|
||||
return this.viewContainer.storeState();
|
||||
}
|
||||
|
||||
restoreState(oldState: ViewContainer.State): void {
|
||||
this.viewContainer.restoreState(oldState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { TreeSource, TreeElement } from '@theia/core/lib/browser/source-tree';
|
||||
import { DebugThread } from '../model/debug-thread';
|
||||
import { DebugViewModel } from './debug-view-model';
|
||||
import debounce = require('p-debounce');
|
||||
import { nls } from '@theia/core';
|
||||
|
||||
@injectable()
|
||||
export class DebugStackFramesSource extends TreeSource {
|
||||
|
||||
@inject(DebugViewModel)
|
||||
protected readonly model: DebugViewModel;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.refresh();
|
||||
this.toDispose.push(this.model.onDidChange(() => this.refresh()));
|
||||
}
|
||||
|
||||
protected readonly refresh = debounce(() => this.fireDidChange(), 100);
|
||||
|
||||
*getElements(): IterableIterator<TreeElement> {
|
||||
const thread = this.model.currentThread;
|
||||
if (!thread) {
|
||||
return;
|
||||
}
|
||||
yield* thread.frames;
|
||||
if (thread.stoppedDetails) {
|
||||
const { framesErrorMessage, totalFrames } = thread.stoppedDetails;
|
||||
if (framesErrorMessage) {
|
||||
yield {
|
||||
render: () => <span title={framesErrorMessage}>{framesErrorMessage}</span>
|
||||
};
|
||||
}
|
||||
if (totalFrames && totalFrames > thread.frameCount) {
|
||||
yield new LoadMoreStackFrames(thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class LoadMoreStackFrames implements TreeElement {
|
||||
|
||||
constructor(
|
||||
readonly thread: DebugThread
|
||||
) { }
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <span className='theia-load-more-frames'>{nls.localizeByDefault('Load More Stack Frames')}</span>;
|
||||
}
|
||||
|
||||
async open(): Promise<void> {
|
||||
const frames = await this.thread.fetchFrames();
|
||||
if (frames[0]) {
|
||||
this.thread.currentFrame = frames[0];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
139
packages/debug/src/browser/view/debug-stack-frames-widget.ts
Normal file
139
packages/debug/src/browser/view/debug-stack-frames-widget.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct, interfaces, Container } from '@theia/core/shared/inversify';
|
||||
import { MenuPath } from '@theia/core';
|
||||
import { TreeNode, NodeProps, SelectableTreeNode } from '@theia/core/lib/browser';
|
||||
import { SourceTreeWidget, TreeElementNode } from '@theia/core/lib/browser/source-tree';
|
||||
import { DebugStackFramesSource, LoadMoreStackFrames } from './debug-stack-frames-source';
|
||||
import { DebugStackFrame } from '../model/debug-stack-frame';
|
||||
import { DebugViewModel } from './debug-view-model';
|
||||
import { DebugCallStackItemTypeKey } from '../debug-call-stack-item-type-key';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export class DebugStackFramesWidget extends SourceTreeWidget {
|
||||
|
||||
static CONTEXT_MENU: MenuPath = ['debug-frames-context-menu'];
|
||||
static FACTORY_ID = 'debug:frames';
|
||||
static override createContainer(parent: interfaces.Container): Container {
|
||||
const child = SourceTreeWidget.createContainer(parent, {
|
||||
contextMenuPath: DebugStackFramesWidget.CONTEXT_MENU,
|
||||
virtualized: false,
|
||||
scrollIfActive: true
|
||||
});
|
||||
child.bind(DebugStackFramesSource).toSelf();
|
||||
child.unbind(SourceTreeWidget);
|
||||
child.bind(DebugStackFramesWidget).toSelf();
|
||||
return child;
|
||||
}
|
||||
static createWidget(parent: interfaces.Container): DebugStackFramesWidget {
|
||||
return DebugStackFramesWidget.createContainer(parent).get(DebugStackFramesWidget);
|
||||
}
|
||||
|
||||
@inject(DebugStackFramesSource)
|
||||
protected readonly frames: DebugStackFramesSource;
|
||||
|
||||
@inject(DebugViewModel)
|
||||
protected readonly viewModel: DebugViewModel;
|
||||
|
||||
@inject(DebugCallStackItemTypeKey)
|
||||
protected readonly debugCallStackItemTypeKey: DebugCallStackItemTypeKey;
|
||||
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.id = DebugStackFramesWidget.FACTORY_ID + ':' + this.viewModel.id;
|
||||
this.title.label = nls.localizeByDefault('Call Stack');
|
||||
this.toDispose.push(this.frames);
|
||||
this.source = this.frames;
|
||||
|
||||
this.toDispose.push(this.viewModel.onDidChange(() => this.updateWidgetSelection()));
|
||||
this.toDispose.push(this.model.onNodeRefreshed(() => this.updateWidgetSelection()));
|
||||
this.toDispose.push(this.model.onSelectionChanged(() => this.updateModelSelection()));
|
||||
}
|
||||
|
||||
protected updatingSelection = false;
|
||||
protected async updateWidgetSelection(): Promise<void> {
|
||||
if (this.updatingSelection) {
|
||||
return;
|
||||
}
|
||||
this.updatingSelection = true;
|
||||
try {
|
||||
const { currentFrame } = this.viewModel;
|
||||
if (currentFrame) {
|
||||
const node = this.model.getNode(currentFrame.id);
|
||||
if (SelectableTreeNode.is(node)) {
|
||||
this.model.selectNode(node);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.updatingSelection = false;
|
||||
}
|
||||
}
|
||||
protected async updateModelSelection(): Promise<void> {
|
||||
if (this.updatingSelection) {
|
||||
return;
|
||||
}
|
||||
this.updatingSelection = true;
|
||||
try {
|
||||
const node = this.model.selectedNodes[0];
|
||||
if (TreeElementNode.is(node)) {
|
||||
if (node.element instanceof DebugStackFrame) {
|
||||
node.element.thread.currentFrame = node.element;
|
||||
this.debugCallStackItemTypeKey.set('stackFrame');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.updatingSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override toContextMenuArgs(node: SelectableTreeNode): [string | number] | undefined {
|
||||
if (TreeElementNode.is(node)) {
|
||||
if (node.element instanceof DebugStackFrame) {
|
||||
const source = node.element.source;
|
||||
if (source) {
|
||||
if (source.inMemory) {
|
||||
const path = source.raw.path || source.raw.sourceReference;
|
||||
if (path !== undefined) {
|
||||
return [path];
|
||||
}
|
||||
} else {
|
||||
return [source.uri.toString()];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected override tapNode(node?: TreeNode): void {
|
||||
if (TreeElementNode.is(node)) {
|
||||
if (node.element instanceof LoadMoreStackFrames) {
|
||||
node.element.open();
|
||||
} else if (node.element instanceof DebugStackFrame) {
|
||||
node.element.open({ preview: true });
|
||||
}
|
||||
}
|
||||
super.tapNode(node);
|
||||
}
|
||||
|
||||
protected override getDefaultNodeStyle(node: TreeNode, props: NodeProps): React.CSSProperties | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
48
packages/debug/src/browser/view/debug-threads-source.tsx
Normal file
48
packages/debug/src/browser/view/debug-threads-source.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { TreeSource, TreeElement } from '@theia/core/lib/browser/source-tree';
|
||||
import { DebugViewModel } from './debug-view-model';
|
||||
|
||||
@injectable()
|
||||
export class DebugThreadsSource extends TreeSource {
|
||||
|
||||
@inject(DebugViewModel)
|
||||
protected readonly model: DebugViewModel;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.fireDidChange();
|
||||
this.toDispose.push(this.model.onDidChange(() => this.fireDidChange()));
|
||||
}
|
||||
|
||||
get multiSession(): boolean {
|
||||
return this.model.sessionCount > 1;
|
||||
}
|
||||
|
||||
*getElements(): IterableIterator<TreeElement> {
|
||||
if (this.model.sessionCount === 1 && this.model.session && this.model.session.threadCount) {
|
||||
return yield* this.model.session.threads;
|
||||
}
|
||||
for (const session of this.model.sessions) {
|
||||
if (!session.parentSession) {
|
||||
yield session;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
204
packages/debug/src/browser/view/debug-threads-widget.ts
Normal file
204
packages/debug/src/browser/view/debug-threads-widget.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct, interfaces, Container } from '@theia/core/shared/inversify';
|
||||
import { MenuPath } from '@theia/core';
|
||||
import { TreeNode, NodeProps, SelectableTreeNode, CompositeTreeNode } from '@theia/core/lib/browser';
|
||||
import { SourceTreeWidget, TreeElementNode } from '@theia/core/lib/browser/source-tree';
|
||||
import { ExpandableTreeNode } from '@theia/core/lib/browser/tree/tree-expansion';
|
||||
import { DebugThreadsSource } from './debug-threads-source';
|
||||
import { DebugSession } from '../debug-session';
|
||||
import { DebugThread } from '../model/debug-thread';
|
||||
import { DebugViewModel } from '../view/debug-view-model';
|
||||
import { DebugCallStackItemTypeKey } from '../debug-call-stack-item-type-key';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export class DebugThreadsWidget extends SourceTreeWidget {
|
||||
|
||||
static CONTEXT_MENU: MenuPath = ['debug-threads-context-menu'];
|
||||
static CONTROL_MENU = [...DebugThreadsWidget.CONTEXT_MENU, 'a_control'];
|
||||
static TERMINATE_MENU = [...DebugThreadsWidget.CONTEXT_MENU, 'b_terminate'];
|
||||
static OPEN_MENU = [...DebugThreadsWidget.CONTEXT_MENU, 'c_open'];
|
||||
static FACTORY_ID = 'debug:threads';
|
||||
static override createContainer(parent: interfaces.Container): Container {
|
||||
const child = SourceTreeWidget.createContainer(parent, {
|
||||
contextMenuPath: DebugThreadsWidget.CONTEXT_MENU,
|
||||
virtualized: false,
|
||||
scrollIfActive: true
|
||||
});
|
||||
child.bind(DebugThreadsSource).toSelf();
|
||||
child.unbind(SourceTreeWidget);
|
||||
child.bind(DebugThreadsWidget).toSelf();
|
||||
return child;
|
||||
}
|
||||
static createWidget(parent: interfaces.Container): DebugThreadsWidget {
|
||||
return DebugThreadsWidget.createContainer(parent).get(DebugThreadsWidget);
|
||||
}
|
||||
|
||||
@inject(DebugThreadsSource)
|
||||
protected readonly threads: DebugThreadsSource;
|
||||
|
||||
@inject(DebugViewModel)
|
||||
protected readonly viewModel: DebugViewModel;
|
||||
|
||||
@inject(DebugCallStackItemTypeKey)
|
||||
protected readonly debugCallStackItemTypeKey: DebugCallStackItemTypeKey;
|
||||
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.id = DebugThreadsWidget.FACTORY_ID + ':' + this.viewModel.id;
|
||||
this.title.label = nls.localize('theia/debug/threads', 'Threads');
|
||||
this.toDispose.push(this.threads);
|
||||
this.source = this.threads;
|
||||
|
||||
this.toDispose.push(this.viewModel.onDidChange(() => {
|
||||
this.updateWidgetSelection();
|
||||
}));
|
||||
this.toDispose.push(this.model.onSelectionChanged(() => this.updateModelSelection()));
|
||||
}
|
||||
|
||||
protected updatingSelection = false;
|
||||
protected async updateWidgetSelection(): Promise<void> {
|
||||
if (this.updatingSelection) {
|
||||
return;
|
||||
}
|
||||
this.updatingSelection = true;
|
||||
try {
|
||||
await this.model.refresh();
|
||||
|
||||
const { currentThread } = this.viewModel;
|
||||
|
||||
// Check if current selection still exists in the tree
|
||||
const selectedNode = this.model.selectedNodes[0];
|
||||
const selectionStillValid = selectedNode && this.model.getNode(selectedNode.id);
|
||||
|
||||
// Only update selection if:
|
||||
// 1. Current selection is invalid (node no longer in tree), OR
|
||||
// 2. There's a stopped thread to show
|
||||
if (selectionStillValid && (!currentThread || !currentThread.stopped)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to select the current stopped thread, or clear if none
|
||||
if (currentThread && currentThread.stopped) {
|
||||
const node = await this.waitForNode(currentThread);
|
||||
|
||||
// Re-check stopped state after async wait
|
||||
if (!currentThread.stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node && SelectableTreeNode.is(node)) {
|
||||
this.model.selectNode(node);
|
||||
|
||||
// Set context key
|
||||
if (TreeElementNode.is(node)) {
|
||||
if (node.element instanceof DebugThread) {
|
||||
this.debugCallStackItemTypeKey.set('thread');
|
||||
} else if (node.element instanceof DebugSession) {
|
||||
this.debugCallStackItemTypeKey.set('session');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!selectionStillValid) {
|
||||
// Selection is stale and no stopped thread to select
|
||||
this.model.clearSelection();
|
||||
}
|
||||
} finally {
|
||||
this.updatingSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a node to appear in the tree, expanding root children to populate the tree
|
||||
*/
|
||||
protected async waitForNode(thread: DebugThread): Promise<TreeNode | undefined> {
|
||||
const maxAttempts = 10;
|
||||
const delayMs = 50;
|
||||
const threadId = thread.id;
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
// If thread continued during wait, abort
|
||||
if (!thread.stopped) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
await this.model.refresh();
|
||||
|
||||
const root = this.model.root;
|
||||
|
||||
// Expand all root's direct children to populate the tree
|
||||
if (root && CompositeTreeNode.is(root)) {
|
||||
for (const child of root.children) {
|
||||
if (ExpandableTreeNode.is(child) && !child.expanded) {
|
||||
await this.model.expandNode(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now look directly for the thread node
|
||||
const threadNode = this.model.getNode(threadId);
|
||||
if (threadNode) {
|
||||
return threadNode;
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected updateModelSelection(): void {
|
||||
if (this.updatingSelection) {
|
||||
return;
|
||||
}
|
||||
this.updatingSelection = true;
|
||||
try {
|
||||
const node = this.model.selectedNodes[0];
|
||||
if (TreeElementNode.is(node)) {
|
||||
if (node.element instanceof DebugSession) {
|
||||
this.viewModel.currentSession = node.element;
|
||||
this.debugCallStackItemTypeKey.set('session');
|
||||
} else if (node.element instanceof DebugThread) {
|
||||
this.viewModel.currentSession = node.element.session;
|
||||
node.element.session.currentThread = node.element;
|
||||
this.debugCallStackItemTypeKey.set('thread');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.updatingSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override toContextMenuArgs(node: SelectableTreeNode): [number] | undefined {
|
||||
if (TreeElementNode.is(node) && node.element instanceof DebugThread) {
|
||||
return [node.element.raw.id];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected override getDefaultNodeStyle(node: TreeNode, props: NodeProps): React.CSSProperties | undefined {
|
||||
if (this.threads.multiSession) {
|
||||
return super.getDefaultNodeStyle(node, props);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
87
packages/debug/src/browser/view/debug-toolbar-widget.tsx
Normal file
87
packages/debug/src/browser/view/debug-toolbar-widget.tsx
Normal file
@@ -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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { inject, postConstruct, injectable } from '@theia/core/shared/inversify';
|
||||
import { CommandMenu, CompoundMenuNode, MenuModelRegistry, MenuPath } from '@theia/core';
|
||||
import { KeybindingRegistry } from '@theia/core/lib/browser';
|
||||
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
||||
import { ReactWidget } from '@theia/core/lib/browser/widgets';
|
||||
import { DebugViewModel } from './debug-view-model';
|
||||
import { DebugAction } from './debug-action';
|
||||
|
||||
@injectable()
|
||||
export class DebugToolBar extends ReactWidget {
|
||||
|
||||
static readonly MENU: MenuPath = ['debug-toolbar-menu'];
|
||||
static readonly CONTROLS: MenuPath = [...DebugToolBar.MENU, 'z_controls'];
|
||||
|
||||
@inject(MenuModelRegistry) protected readonly menuModelRegistry: MenuModelRegistry;
|
||||
@inject(KeybindingRegistry) protected readonly keybindingRegistry: KeybindingRegistry;
|
||||
@inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService;
|
||||
@inject(DebugViewModel) protected readonly model: DebugViewModel;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.id = 'debug:toolbar:' + this.model.id;
|
||||
this.addClass('debug-toolbar');
|
||||
this.toDispose.push(this.model);
|
||||
this.toDispose.push(this.model.onDidChange(() => this.update()));
|
||||
this.toDispose.push(this.keybindingRegistry.onKeybindingsChanged(() => this.update()));
|
||||
this.scrollOptions = undefined;
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected render(): React.ReactNode {
|
||||
return <React.Fragment>
|
||||
{this.renderContributedCommands()}
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
protected renderContributedCommands(): React.ReactNode {
|
||||
const debugActions: React.ReactNode[] = [];
|
||||
// first, search for CompoundMenuNodes:
|
||||
this.menuModelRegistry.getMenu(DebugToolBar.MENU)!.children.forEach(compoundMenuNode => {
|
||||
if (CompoundMenuNode.is(compoundMenuNode) && compoundMenuNode.isVisible(DebugToolBar.MENU, this.contextKeyService, this.node)) {
|
||||
// second, search for nested CommandMenuNodes:
|
||||
compoundMenuNode.children.forEach(commandMenuNode => {
|
||||
if (CommandMenu.is(commandMenuNode) && commandMenuNode.isVisible(DebugToolBar.MENU, this.contextKeyService, this.node)) {
|
||||
debugActions.push(this.debugAction(commandMenuNode));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return debugActions;
|
||||
}
|
||||
|
||||
protected debugAction(commandMenuNode: CommandMenu): React.ReactNode {
|
||||
const accelerator = this.acceleratorFor(commandMenuNode.id);
|
||||
const run = (effectiveMenuPath: MenuPath) => commandMenuNode.run(effectiveMenuPath).catch(e => console.error(e));
|
||||
return <DebugAction
|
||||
key={commandMenuNode.id}
|
||||
enabled={commandMenuNode.isEnabled(DebugToolBar.MENU)}
|
||||
label={commandMenuNode.label}
|
||||
tooltip={commandMenuNode.label + (accelerator ? ` (${accelerator})` : '')}
|
||||
iconClass={commandMenuNode.icon || ''}
|
||||
run={run} />;
|
||||
}
|
||||
|
||||
protected acceleratorFor(commandId: string): string | undefined {
|
||||
const keybindings = this.keybindingRegistry.getKeybindingsForCommand(commandId);
|
||||
return keybindings.length ? this.keybindingRegistry.acceleratorFor(keybindings[0], '+').join(' ') : undefined;
|
||||
}
|
||||
|
||||
}
|
||||
44
packages/debug/src/browser/view/debug-variables-source.ts
Normal file
44
packages/debug/src/browser/view/debug-variables-source.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { TreeSource } from '@theia/core/lib/browser/source-tree';
|
||||
import { DebugScope } from '../console/debug-console-items';
|
||||
import { DebugViewModel } from './debug-view-model';
|
||||
import debounce = require('p-debounce');
|
||||
|
||||
@injectable()
|
||||
export class DebugVariablesSource extends TreeSource {
|
||||
|
||||
@inject(DebugViewModel)
|
||||
protected readonly model: DebugViewModel;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.refresh();
|
||||
this.toDispose.push(this.model.onDidChange(() => this.refresh()));
|
||||
this.toDispose.push(this.model.onDidResolveLazyVariable(() => this.fireDidChange()));
|
||||
}
|
||||
|
||||
protected readonly refresh = debounce(() => this.fireDidChange(), 400);
|
||||
|
||||
async getElements(): Promise<IterableIterator<DebugScope>> {
|
||||
const { currentSession } = this.model;
|
||||
const scopes = currentSession ? await currentSession.getScopes() : [];
|
||||
return scopes[Symbol.iterator]();
|
||||
}
|
||||
|
||||
}
|
||||
217
packages/debug/src/browser/view/debug-variables-widget.ts
Normal file
217
packages/debug/src/browser/view/debug-variables-widget.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct, interfaces, Container } from '@theia/core/shared/inversify';
|
||||
import { MenuPath, Disposable, CommandRegistry, MenuModelRegistry, DisposableCollection, Command } from '@theia/core/lib/common';
|
||||
import { SourceTreeWidget, TreeElementNode } from '@theia/core/lib/browser/source-tree';
|
||||
import { DebugVariablesSource } from './debug-variables-source';
|
||||
import { DebugViewModel } from './debug-view-model';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { MouseEvent } from '@theia/core/shared/react';
|
||||
import { SelectableTreeNode, TreeNode, TreeSelection } from '@theia/core/lib/browser';
|
||||
import { DebugVariable } from '../console/debug-console-items';
|
||||
import { BreakpointManager } from '../breakpoint/breakpoint-manager';
|
||||
import { DataBreakpoint, DataBreakpointSource, DataBreakpointSourceType } from '../breakpoint/breakpoint-marker';
|
||||
import { DebugSessionManager } from '../debug-session-manager';
|
||||
import { DebugSession } from '../debug-session';
|
||||
import { DebugStackFrame } from '../model/debug-stack-frame';
|
||||
|
||||
@injectable()
|
||||
export class DebugVariablesWidget extends SourceTreeWidget {
|
||||
|
||||
static CONTEXT_MENU: MenuPath = ['debug-variables-context-menu'];
|
||||
static EDIT_MENU: MenuPath = [...DebugVariablesWidget.CONTEXT_MENU, 'a_edit'];
|
||||
static WATCH_MENU: MenuPath = [...DebugVariablesWidget.CONTEXT_MENU, 'b_watch'];
|
||||
static DATA_BREAKPOINT_MENU: MenuPath = [...DebugVariablesWidget.CONTEXT_MENU, 'c_data_breakpoints'];
|
||||
static FACTORY_ID = 'debug:variables';
|
||||
static override createContainer(parent: interfaces.Container): Container {
|
||||
const child = SourceTreeWidget.createContainer(parent, {
|
||||
contextMenuPath: DebugVariablesWidget.CONTEXT_MENU,
|
||||
virtualized: false,
|
||||
scrollIfActive: true
|
||||
});
|
||||
child.bind(DebugVariablesSource).toSelf();
|
||||
child.unbind(SourceTreeWidget);
|
||||
child.bind(DebugVariablesWidget).toSelf();
|
||||
return child;
|
||||
}
|
||||
static createWidget(parent: interfaces.Container): DebugVariablesWidget {
|
||||
return DebugVariablesWidget.createContainer(parent).get(DebugVariablesWidget);
|
||||
}
|
||||
|
||||
@inject(DebugViewModel)
|
||||
protected readonly viewModel: DebugViewModel;
|
||||
|
||||
@inject(DebugVariablesSource)
|
||||
protected readonly variables: DebugVariablesSource;
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(BreakpointManager)
|
||||
protected readonly breakpointManager: BreakpointManager;
|
||||
|
||||
@inject(DebugSessionManager)
|
||||
protected readonly sessionManager: DebugSessionManager;
|
||||
|
||||
protected stackFrame: DebugStackFrame | undefined;
|
||||
protected readonly statePerSession = new Map<string, DebugVariablesWidgetSessionState>();
|
||||
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.id = DebugVariablesWidget.FACTORY_ID + ':' + this.viewModel.id;
|
||||
this.title.label = nls.localizeByDefault('Variables');
|
||||
this.toDispose.push(this.variables);
|
||||
this.source = this.variables;
|
||||
this.toDispose.push(this.sessionManager.onDidFocusStackFrame(stackFrame => this.handleDidFocusStackFrame(stackFrame)));
|
||||
this.toDispose.push(this.sessionManager.onDidDestroyDebugSession(session => this.handleDidDestroyDebugSession(session)));
|
||||
}
|
||||
|
||||
protected handleDidFocusStackFrame(stackFrame: DebugStackFrame | undefined): void {
|
||||
if (this.stackFrame !== stackFrame) {
|
||||
if (this.stackFrame) {
|
||||
const sessionState = this.getOrCreateSessionState(this.stackFrame.session);
|
||||
sessionState.setStateForStackFrame(this.stackFrame, this.superStoreState());
|
||||
}
|
||||
if (stackFrame) {
|
||||
const sessionState = this.statePerSession.get(stackFrame.session.id);
|
||||
if (sessionState) {
|
||||
const state = sessionState.getStateForStackFrame(stackFrame);
|
||||
if (state) {
|
||||
this.superRestoreState(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.stackFrame = stackFrame;
|
||||
}
|
||||
}
|
||||
|
||||
protected getOrCreateSessionState(session: DebugSession): DebugVariablesWidgetSessionState {
|
||||
let sessionState = this.statePerSession.get(session.id);
|
||||
if (!sessionState) {
|
||||
sessionState = this.newSessionState();
|
||||
this.statePerSession.set(session.id, sessionState);
|
||||
}
|
||||
return sessionState;
|
||||
}
|
||||
|
||||
protected newSessionState(): DebugVariablesWidgetSessionState {
|
||||
return new DebugVariablesWidgetSessionState();
|
||||
}
|
||||
|
||||
protected handleDidDestroyDebugSession(session: DebugSession): void {
|
||||
this.statePerSession.delete(session.id);
|
||||
}
|
||||
|
||||
protected override handleContextMenuEvent(node: TreeNode | undefined, event: MouseEvent<HTMLElement>): void {
|
||||
this.doHandleContextMenuEvent(node, event);
|
||||
}
|
||||
|
||||
protected async doHandleContextMenuEvent(node: TreeNode | undefined, event: MouseEvent<HTMLElement>): Promise<void> {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (!SelectableTreeNode.is(node) || !TreeElementNode.is(node)) { return; }
|
||||
// Keep the selection for the context menu, if the widget support multi-selection and the right click happens on an already selected node.
|
||||
if (!this.props.multiSelect || !node.selected) {
|
||||
const type = !!this.props.multiSelect && this.hasCtrlCmdMask(event) ? TreeSelection.SelectionType.TOGGLE : TreeSelection.SelectionType.DEFAULT;
|
||||
this.model.addSelection({ node, type });
|
||||
}
|
||||
this.focusService.setFocus(node);
|
||||
const contextMenuPath = this.props.contextMenuPath;
|
||||
if (contextMenuPath) {
|
||||
const { x, y } = event.nativeEvent;
|
||||
const args = this.toContextMenuArgs(node);
|
||||
const target = event.currentTarget;
|
||||
const toDisposeOnHide = await this.getVariableCommands(node);
|
||||
setTimeout(() => this.contextMenuRenderer.render({
|
||||
menuPath: contextMenuPath,
|
||||
context: target,
|
||||
anchor: { x, y },
|
||||
args,
|
||||
onHide: () => toDisposeOnHide.dispose()
|
||||
}), 10);
|
||||
}
|
||||
}
|
||||
|
||||
protected async getVariableCommands(node: TreeElementNode): Promise<Disposable> {
|
||||
const selectedElement = node.element;
|
||||
const { viewModel: { currentSession } } = this;
|
||||
if (!currentSession?.capabilities.supportsDataBreakpoints || !(selectedElement instanceof DebugVariable)) {
|
||||
return Disposable.NULL;
|
||||
}
|
||||
const { name, parent: { reference } } = selectedElement;
|
||||
const dataBreakpointInfo = (await currentSession.sendRequest('dataBreakpointInfo', { name, variablesReference: reference })).body;
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
if (dataBreakpointInfo.dataId === null) {
|
||||
return Disposable.NULL;
|
||||
}
|
||||
const source: DataBreakpointSource = { type: DataBreakpointSourceType.Variable, variable: name };
|
||||
return new DisposableCollection(
|
||||
this.commandRegistry.registerCommand(Command.toDefaultLocalizedCommand({
|
||||
id: `break-on-access:${currentSession.id}:${name}`,
|
||||
label: 'Break on Value Access'
|
||||
}), {
|
||||
execute: () => this.breakpointManager.addDataBreakpoint(DataBreakpoint.create(
|
||||
{ accessType: 'readWrite', dataId: dataBreakpointInfo.dataId! },
|
||||
dataBreakpointInfo,
|
||||
source
|
||||
)),
|
||||
isEnabled: () => !!dataBreakpointInfo.accessTypes?.includes('readWrite'),
|
||||
}),
|
||||
this.menuRegistry.registerMenuAction(DebugVariablesWidget.DATA_BREAKPOINT_MENU, { commandId: `break-on-access:${currentSession.id}:${name}`, order: 'c' }),
|
||||
this.commandRegistry.registerCommand(Command.toDefaultLocalizedCommand({
|
||||
id: `break-on-read:${currentSession.id}:${name}`,
|
||||
label: 'Break on Value Read'
|
||||
}), {
|
||||
execute: () => this.breakpointManager.addDataBreakpoint(DataBreakpoint.create(
|
||||
{ accessType: 'read', dataId: dataBreakpointInfo.dataId! },
|
||||
dataBreakpointInfo,
|
||||
source
|
||||
)),
|
||||
isEnabled: () => !!dataBreakpointInfo.accessTypes?.includes('read'),
|
||||
}),
|
||||
this.menuRegistry.registerMenuAction(DebugVariablesWidget.DATA_BREAKPOINT_MENU, { commandId: `break-on-read:${currentSession.id}:${name}`, order: 'a' }),
|
||||
this.commandRegistry.registerCommand(Command.toDefaultLocalizedCommand({
|
||||
id: `break-on-write:${currentSession.id}:${name}`,
|
||||
label: 'Break on Value Change'
|
||||
}), {
|
||||
execute: () => this.breakpointManager.addDataBreakpoint(DataBreakpoint.create(
|
||||
{ accessType: 'write', dataId: dataBreakpointInfo.dataId! },
|
||||
dataBreakpointInfo,
|
||||
source
|
||||
)),
|
||||
isEnabled: () => !!dataBreakpointInfo.accessTypes?.includes('write'),
|
||||
}),
|
||||
this.menuRegistry.registerMenuAction(DebugVariablesWidget.DATA_BREAKPOINT_MENU, { commandId: `break-on-write:${currentSession.id}:${name}`, order: 'b' }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class DebugVariablesWidgetSessionState {
|
||||
protected readonly statePerStackFrame = new Map<string, object>();
|
||||
|
||||
setStateForStackFrame(stackFrame: DebugStackFrame, state: object): void {
|
||||
this.statePerStackFrame.set(stackFrame.id, state);
|
||||
}
|
||||
|
||||
getStateForStackFrame(stackFrame: DebugStackFrame): object | undefined {
|
||||
return this.statePerStackFrame.get(stackFrame.id);
|
||||
}
|
||||
}
|
||||
247
packages/debug/src/browser/view/debug-view-model.ts
Normal file
247
packages/debug/src/browser/view/debug-view-model.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import debounce from 'p-debounce';
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { Disposable, DisposableCollection, Event, Emitter, deepClone, nls } from '@theia/core/lib/common';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { DebugSession, DebugState } from '../debug-session';
|
||||
import { DebugSessionManager } from '../debug-session-manager';
|
||||
import { DebugThread } from '../model/debug-thread';
|
||||
import { DebugStackFrame } from '../model/debug-stack-frame';
|
||||
import { DebugSourceBreakpoint } from '../model/debug-source-breakpoint';
|
||||
import { DebugWatchExpression } from './debug-watch-expression';
|
||||
import { DebugWatchManager } from '../debug-watch-manager';
|
||||
import { DebugFunctionBreakpoint } from '../model/debug-function-breakpoint';
|
||||
import { DebugInstructionBreakpoint } from '../model/debug-instruction-breakpoint';
|
||||
import { DebugSessionOptionsBase } from '../debug-session-options';
|
||||
import { DebugDataBreakpoint } from '../model/debug-data-breakpoint';
|
||||
import { DebugVariable } from '../console/debug-console-items';
|
||||
|
||||
@injectable()
|
||||
export class DebugViewModel implements Disposable {
|
||||
|
||||
protected readonly onDidChangeEmitter = new Emitter<void>();
|
||||
readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
|
||||
protected fireDidChange(): void {
|
||||
this.refreshWatchExpressions();
|
||||
this.onDidChangeEmitter.fire(undefined);
|
||||
}
|
||||
|
||||
protected readonly onDidChangeBreakpointsEmitter = new Emitter<URI>();
|
||||
readonly onDidChangeBreakpoints: Event<URI> = this.onDidChangeBreakpointsEmitter.event;
|
||||
protected fireDidChangeBreakpoints(uri: URI): void {
|
||||
this.onDidChangeBreakpointsEmitter.fire(uri);
|
||||
}
|
||||
|
||||
protected readonly onDidResolveLazyVariableEmitter = new Emitter<DebugVariable>();
|
||||
readonly onDidResolveLazyVariable: Event<DebugVariable> = this.onDidResolveLazyVariableEmitter.event;
|
||||
protected fireDidResolveLazyVariable(variable: DebugVariable): void {
|
||||
this.onDidResolveLazyVariableEmitter.fire(variable);
|
||||
}
|
||||
|
||||
protected readonly _watchExpressions = new Map<number, DebugWatchExpression>();
|
||||
|
||||
protected readonly onDidChangeWatchExpressionsEmitter = new Emitter<void>();
|
||||
readonly onDidChangeWatchExpressions = this.onDidChangeWatchExpressionsEmitter.event;
|
||||
protected fireDidChangeWatchExpressions(): void {
|
||||
this.onDidChangeWatchExpressionsEmitter.fire(undefined);
|
||||
}
|
||||
|
||||
protected readonly toDispose = new DisposableCollection(
|
||||
this.onDidChangeEmitter,
|
||||
this.onDidChangeBreakpointsEmitter,
|
||||
this.onDidResolveLazyVariableEmitter,
|
||||
this.onDidChangeWatchExpressionsEmitter,
|
||||
);
|
||||
|
||||
@inject(DebugSessionManager)
|
||||
protected readonly manager: DebugSessionManager;
|
||||
|
||||
@inject(DebugWatchManager)
|
||||
protected readonly watch: DebugWatchManager;
|
||||
|
||||
get sessions(): IterableIterator<DebugSession> {
|
||||
return this.manager.sessions[Symbol.iterator]();
|
||||
}
|
||||
get sessionCount(): number {
|
||||
return this.manager.sessions.length;
|
||||
}
|
||||
get session(): DebugSession | undefined {
|
||||
return this.currentSession;
|
||||
}
|
||||
get id(): string {
|
||||
return this.session && this.session.id || '-1';
|
||||
}
|
||||
get label(): string {
|
||||
return this.session && this.session.label || nls.localize('theia/debug/unknownSession', 'Unknown Session');
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.toDispose.push(this.manager.onDidChangeActiveDebugSession(() => {
|
||||
this.fireDidChange();
|
||||
}));
|
||||
this.toDispose.push(this.manager.onDidChange(current => {
|
||||
// Always fire change to update views, even if session is not current
|
||||
// This ensures threads view updates for all sessions
|
||||
this.fireDidChange();
|
||||
}));
|
||||
this.toDispose.push(this.manager.onDidChangeBreakpoints(({ session, uri }) => {
|
||||
// Fire for all sessions since we now show breakpoints from all active sessions
|
||||
this.fireDidChangeBreakpoints(uri);
|
||||
}));
|
||||
this.toDispose.push(this.manager.onDidResolveLazyVariable(({ session, variable }) => {
|
||||
if (session === this.currentSession) {
|
||||
this.fireDidResolveLazyVariable(variable);
|
||||
}
|
||||
}));
|
||||
this.updateWatchExpressions();
|
||||
this.toDispose.push(this.watch.onDidChange(() => this.updateWatchExpressions()));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
get currentSession(): DebugSession | undefined {
|
||||
const { currentSession } = this.manager;
|
||||
return currentSession;
|
||||
}
|
||||
set currentSession(currentSession: DebugSession | undefined) {
|
||||
this.manager.currentSession = currentSession;
|
||||
}
|
||||
|
||||
get state(): DebugState {
|
||||
const { currentSession } = this;
|
||||
return currentSession && currentSession.state || DebugState.Inactive;
|
||||
}
|
||||
get currentThread(): DebugThread | undefined {
|
||||
const { currentSession } = this;
|
||||
return currentSession && currentSession.currentThread;
|
||||
}
|
||||
get currentFrame(): DebugStackFrame | undefined {
|
||||
const { currentThread } = this;
|
||||
return currentThread && currentThread.currentFrame;
|
||||
}
|
||||
|
||||
get breakpoints(): DebugSourceBreakpoint[] {
|
||||
return this.manager.getBreakpoints();
|
||||
}
|
||||
|
||||
get functionBreakpoints(): DebugFunctionBreakpoint[] {
|
||||
return this.manager.getFunctionBreakpoints();
|
||||
}
|
||||
|
||||
get instructionBreakpoints(): DebugInstructionBreakpoint[] {
|
||||
return this.manager.getInstructionBreakpoints();
|
||||
}
|
||||
|
||||
get dataBreakpoints(): DebugDataBreakpoint[] {
|
||||
return this.manager.getDataBreakpoints(this.currentSession);
|
||||
}
|
||||
|
||||
async start(options: Partial<Pick<DebugSessionOptionsBase, 'startedByUser'>> = {}): Promise<void> {
|
||||
const { session } = this;
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
const optionsCopy = deepClone(session.options);
|
||||
const newSession = await this.manager.start(Object.assign(optionsCopy, options));
|
||||
if (newSession) {
|
||||
this.fireDidChange();
|
||||
}
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
const { session } = this;
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
await this.manager.restartSession(session);
|
||||
this.fireDidChange();
|
||||
}
|
||||
|
||||
async terminate(): Promise<void> {
|
||||
this.manager.terminateSession();
|
||||
}
|
||||
|
||||
get watchExpressions(): IterableIterator<DebugWatchExpression> {
|
||||
return this._watchExpressions.values();
|
||||
}
|
||||
|
||||
async addWatchExpression(expression: string = ''): Promise<DebugWatchExpression | undefined> {
|
||||
const watchExpression: DebugWatchExpression = new DebugWatchExpression({
|
||||
id: Number.MAX_SAFE_INTEGER,
|
||||
expression,
|
||||
session: () => this.currentSession,
|
||||
remove: () => this.removeWatchExpression(watchExpression),
|
||||
onDidChange: () => { /* no-op */ },
|
||||
});
|
||||
await watchExpression.open();
|
||||
if (!watchExpression.expression) {
|
||||
return undefined;
|
||||
}
|
||||
const id = this.watch.addWatchExpression(watchExpression.expression);
|
||||
return this._watchExpressions.get(id);
|
||||
}
|
||||
|
||||
removeWatchExpressions(): void {
|
||||
this.watch.removeWatchExpressions();
|
||||
}
|
||||
|
||||
removeWatchExpression(expression: DebugWatchExpression): void {
|
||||
this.watch.removeWatchExpression(expression.id);
|
||||
}
|
||||
|
||||
protected updateWatchExpressions(): void {
|
||||
let added = false;
|
||||
const toRemove = new Set(this._watchExpressions.keys());
|
||||
for (const [id, expression] of this.watch.watchExpressions) {
|
||||
toRemove.delete(id);
|
||||
if (!this._watchExpressions.has(id)) {
|
||||
added = true;
|
||||
const watchExpression: DebugWatchExpression = new DebugWatchExpression({
|
||||
id,
|
||||
expression,
|
||||
session: () => this.currentSession,
|
||||
remove: () => this.removeWatchExpression(watchExpression),
|
||||
onDidChange: () => this.fireDidChangeWatchExpressions()
|
||||
});
|
||||
this._watchExpressions.set(id, watchExpression);
|
||||
watchExpression.evaluate();
|
||||
}
|
||||
}
|
||||
for (const id of toRemove) {
|
||||
this._watchExpressions.delete(id);
|
||||
}
|
||||
if (added || toRemove.size) {
|
||||
this.fireDidChangeWatchExpressions();
|
||||
}
|
||||
}
|
||||
|
||||
protected refreshWatchExpressionsQueue = Promise.resolve();
|
||||
protected refreshWatchExpressions = debounce(() => {
|
||||
this.refreshWatchExpressionsQueue = this.refreshWatchExpressionsQueue.then(async () => {
|
||||
try {
|
||||
await Promise.all(Array.from(this.watchExpressions).map(expr => expr.evaluate()));
|
||||
} catch (e) {
|
||||
console.error('Failed to refresh watch expressions: ', e);
|
||||
}
|
||||
});
|
||||
}, 50);
|
||||
|
||||
}
|
||||
108
packages/debug/src/browser/view/debug-watch-expression.tsx
Normal file
108
packages/debug/src/browser/view/debug-watch-expression.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { SingleTextInputDialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { ExpressionItem, DebugSessionProvider } from '../console/debug-console-items';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol';
|
||||
import { codicon, TREE_NODE_SEGMENT_GROW_CLASS } from '@theia/core/lib/browser';
|
||||
import { nls } from '@theia/core';
|
||||
|
||||
export class DebugWatchExpression extends ExpressionItem {
|
||||
|
||||
override readonly id: number;
|
||||
protected isError: boolean;
|
||||
protected isNotAvailable: boolean;
|
||||
|
||||
constructor(protected readonly options: {
|
||||
id: number,
|
||||
expression: string,
|
||||
session: DebugSessionProvider,
|
||||
remove: () => void,
|
||||
onDidChange: () => void
|
||||
}) {
|
||||
super(options.expression, options.session, options.id);
|
||||
}
|
||||
|
||||
override async evaluate(): Promise<void> {
|
||||
await super.evaluate('watch');
|
||||
this.options.onDidChange();
|
||||
}
|
||||
|
||||
protected override setResult(body?: DebugProtocol.EvaluateResponse['body'], error?: string): void {
|
||||
const session = this.options.session();
|
||||
this.isNotAvailable = false;
|
||||
this.isError = false;
|
||||
|
||||
// not available must be set regardless of the session's availability.
|
||||
// not available is used when there is no session or the current stack frame is not available.
|
||||
if (error === ExpressionItem.notAvailable) {
|
||||
super.setResult(undefined, error);
|
||||
this.isNotAvailable = true;
|
||||
} else if (session) {
|
||||
super.setResult(body, error);
|
||||
this.isError = !!error;
|
||||
}
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const valueClass = this.valueClass();
|
||||
return <div className='theia-debug-console-variable theia-debug-watch-expression'>
|
||||
<div className={TREE_NODE_SEGMENT_GROW_CLASS}>
|
||||
<span title={this.type || this._expression} className='name'>{this._expression}:</span>
|
||||
<span title={this._value} ref={this.setValueRef} className={valueClass}>{this._value}</span>
|
||||
</div>
|
||||
<div className={codicon('close', true)} title={nls.localizeByDefault('Remove Expression')} onClick={this.options.remove} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected valueClass(): string {
|
||||
if (this.isError) {
|
||||
return 'watch-error';
|
||||
}
|
||||
if (this.isNotAvailable) {
|
||||
return 'watch-not-available';
|
||||
}
|
||||
return 'value';
|
||||
}
|
||||
|
||||
async open(): Promise<void> {
|
||||
const input = new SingleTextInputDialog({
|
||||
title: nls.localizeByDefault('Edit Expression'),
|
||||
initialValue: this.expression,
|
||||
placeholder: nls.localizeByDefault('Expression to watch')
|
||||
});
|
||||
const newValue = await input.open();
|
||||
if (newValue !== undefined) {
|
||||
this._expression = newValue;
|
||||
await this.evaluate();
|
||||
}
|
||||
}
|
||||
|
||||
get supportCopyValue(): boolean {
|
||||
return !!this.valueRef && document.queryCommandSupported('copy');
|
||||
}
|
||||
copyValue(): void {
|
||||
const selection = document.getSelection();
|
||||
if (this.valueRef && selection) {
|
||||
selection.selectAllChildren(this.valueRef);
|
||||
document.execCommand('copy');
|
||||
}
|
||||
}
|
||||
protected valueRef: HTMLSpanElement | undefined;
|
||||
protected setValueRef = (valueRef: HTMLSpanElement | null) => this.valueRef = valueRef || undefined;
|
||||
|
||||
}
|
||||
42
packages/debug/src/browser/view/debug-watch-source.ts
Normal file
42
packages/debug/src/browser/view/debug-watch-source.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { TreeSource } from '@theia/core/lib/browser/source-tree';
|
||||
import { DebugViewModel } from './debug-view-model';
|
||||
import { DebugWatchExpression } from './debug-watch-expression';
|
||||
import debounce = require('p-debounce');
|
||||
|
||||
@injectable()
|
||||
export class DebugWatchSource extends TreeSource {
|
||||
|
||||
@inject(DebugViewModel)
|
||||
protected readonly model: DebugViewModel;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.refresh();
|
||||
this.toDispose.push(this.model.onDidChangeWatchExpressions(() => this.refresh()));
|
||||
this.toDispose.push(this.model.onDidResolveLazyVariable(() => this.refresh()));
|
||||
}
|
||||
|
||||
protected readonly refresh = debounce(() => this.fireDidChange(), 100);
|
||||
|
||||
async getElements(): Promise<IterableIterator<DebugWatchExpression>> {
|
||||
return this.model.watchExpressions[Symbol.iterator]();
|
||||
}
|
||||
|
||||
}
|
||||
61
packages/debug/src/browser/view/debug-watch-widget.ts
Normal file
61
packages/debug/src/browser/view/debug-watch-widget.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct, interfaces, Container } from '@theia/core/shared/inversify';
|
||||
import { MenuPath } from '@theia/core/lib/common';
|
||||
import { SourceTreeWidget } from '@theia/core/lib/browser/source-tree';
|
||||
import { DebugWatchSource } from './debug-watch-source';
|
||||
import { DebugViewModel } from './debug-view-model';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export class DebugWatchWidget extends SourceTreeWidget {
|
||||
|
||||
static CONTEXT_MENU: MenuPath = ['debug-watch-context-menu'];
|
||||
static EDIT_MENU = [...DebugWatchWidget.CONTEXT_MENU, 'a_edit'];
|
||||
static REMOVE_MENU = [...DebugWatchWidget.CONTEXT_MENU, 'b_remove'];
|
||||
static FACTORY_ID = 'debug:watch';
|
||||
static override createContainer(parent: interfaces.Container): Container {
|
||||
const child = SourceTreeWidget.createContainer(parent, {
|
||||
contextMenuPath: DebugWatchWidget.CONTEXT_MENU,
|
||||
virtualized: false,
|
||||
scrollIfActive: true
|
||||
});
|
||||
child.bind(DebugWatchSource).toSelf();
|
||||
child.unbind(SourceTreeWidget);
|
||||
child.bind(DebugWatchWidget).toSelf();
|
||||
return child;
|
||||
}
|
||||
static createWidget(parent: interfaces.Container): DebugWatchWidget {
|
||||
return DebugWatchWidget.createContainer(parent).get(DebugWatchWidget);
|
||||
}
|
||||
|
||||
@inject(DebugViewModel)
|
||||
readonly viewModel: DebugViewModel;
|
||||
|
||||
@inject(DebugWatchSource)
|
||||
protected readonly variables: DebugWatchSource;
|
||||
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.id = DebugWatchWidget.FACTORY_ID + ':' + this.viewModel.id;
|
||||
this.title.label = nls.localizeByDefault('Watch');
|
||||
this.toDispose.push(this.variables);
|
||||
this.source = this.variables;
|
||||
}
|
||||
|
||||
}
|
||||
97
packages/debug/src/browser/view/debug-widget.ts
Normal file
97
packages/debug/src/browser/view/debug-widget.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, postConstruct, inject, interfaces, Container } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
BaseWidget, PanelLayout, Message, ApplicationShell, Widget, StatefulWidget, ViewContainer, codicon
|
||||
} from '@theia/core/lib/browser';
|
||||
import { DebugSessionWidget } from './debug-session-widget';
|
||||
import { DebugConfigurationWidget } from './debug-configuration-widget';
|
||||
import { DebugViewModel } from './debug-view-model';
|
||||
import { DebugSessionManager } from '../debug-session-manager';
|
||||
import { ProgressBarFactory } from '@theia/core/lib/browser/progress-bar-factory';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export class DebugWidget extends BaseWidget implements StatefulWidget, ApplicationShell.TrackableWidgetProvider {
|
||||
|
||||
static createContainer(parent: interfaces.Container): Container {
|
||||
const child = DebugSessionWidget.createContainer(parent);
|
||||
child.bind(DebugConfigurationWidget).toSelf();
|
||||
child.bind(DebugWidget).toSelf();
|
||||
return child;
|
||||
}
|
||||
|
||||
static createWidget(parent: interfaces.Container): DebugWidget {
|
||||
return DebugWidget.createContainer(parent).get(DebugWidget);
|
||||
}
|
||||
|
||||
static ID = 'debug';
|
||||
static LABEL = nls.localizeByDefault('Debug');
|
||||
|
||||
@inject(DebugViewModel)
|
||||
readonly model: DebugViewModel;
|
||||
|
||||
@inject(DebugSessionManager)
|
||||
readonly sessionManager: DebugSessionManager;
|
||||
|
||||
@inject(DebugConfigurationWidget)
|
||||
protected readonly toolbar: DebugConfigurationWidget;
|
||||
|
||||
@inject(DebugSessionWidget)
|
||||
protected readonly sessionWidget: DebugSessionWidget;
|
||||
|
||||
@inject(ProgressBarFactory)
|
||||
protected readonly progressBarFactory: ProgressBarFactory;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.id = DebugWidget.ID;
|
||||
this.title.label = DebugWidget.LABEL;
|
||||
this.title.caption = DebugWidget.LABEL;
|
||||
this.title.closable = true;
|
||||
this.title.iconClass = codicon('debug-alt');
|
||||
this.addClass('theia-debug-container');
|
||||
this.toDispose.pushAll([
|
||||
this.toolbar,
|
||||
this.sessionWidget,
|
||||
]);
|
||||
|
||||
const layout = this.layout = new PanelLayout();
|
||||
layout.addWidget(this.toolbar);
|
||||
layout.addWidget(this.sessionWidget);
|
||||
|
||||
this.toDispose.push(this.progressBarFactory({ container: this.node, insertMode: 'prepend', locationId: 'debug' }));
|
||||
}
|
||||
|
||||
protected override onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.toolbar.focus();
|
||||
}
|
||||
|
||||
getTrackableWidgets(): Widget[] {
|
||||
return [this.sessionWidget];
|
||||
}
|
||||
|
||||
storeState(): object {
|
||||
return this.sessionWidget.storeState();
|
||||
}
|
||||
|
||||
restoreState(oldState: ViewContainer.State): void {
|
||||
this.sessionWidget.restoreState(oldState);
|
||||
}
|
||||
|
||||
}
|
||||
206
packages/debug/src/common/debug-adapter-contribution-registry.ts
Normal file
206
packages/debug/src/common/debug-adapter-contribution-registry.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
||||
import { ContributionProvider } from '@theia/core';
|
||||
import { DebugConfiguration } from './debug-configuration';
|
||||
import { DebuggerDescription, DebugError } from './debug-service';
|
||||
|
||||
import { DebugAdapterContribution, DebugAdapterExecutable, DebugAdapterSessionFactory } from './debug-model';
|
||||
import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema';
|
||||
|
||||
/**
|
||||
* Contributions registry.
|
||||
*/
|
||||
@injectable()
|
||||
export class DebugAdapterContributionRegistry {
|
||||
|
||||
@inject(ContributionProvider) @named(DebugAdapterContribution)
|
||||
protected readonly contributions: ContributionProvider<DebugAdapterContribution>;
|
||||
protected *getContributions(debugType: string): IterableIterator<DebugAdapterContribution> {
|
||||
for (const contribution of this.contributions.getContributions()) {
|
||||
if (contribution.type === debugType || contribution.type === '*' || debugType === '*') {
|
||||
yield contribution;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns an array of registered debug types.
|
||||
* @returns An array of registered debug types
|
||||
*/
|
||||
protected _debugTypes: string[] | undefined;
|
||||
debugTypes(): string[] {
|
||||
if (!this._debugTypes) {
|
||||
const result = new Set<string>();
|
||||
for (const contribution of this.contributions.getContributions()) {
|
||||
result.add(contribution.type);
|
||||
}
|
||||
this._debugTypes = [...result];
|
||||
}
|
||||
return this._debugTypes;
|
||||
}
|
||||
|
||||
async getDebuggersForLanguage(language: string): Promise<DebuggerDescription[]> {
|
||||
const debuggers: DebuggerDescription[] = [];
|
||||
for (const contribution of this.contributions.getContributions()) {
|
||||
if (contribution.languages && contribution.label) {
|
||||
const label = await contribution.label;
|
||||
if (label && (await contribution.languages || []).indexOf(language) !== -1) {
|
||||
debuggers.push({
|
||||
type: contribution.type,
|
||||
label
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return debuggers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides initial [debug configuration](#DebugConfiguration).
|
||||
* @param debugType The registered debug type
|
||||
* @returns An array of [debug configurations](#DebugConfiguration)
|
||||
*/
|
||||
async provideDebugConfigurations(debugType: string, workspaceFolderUri?: string): Promise<DebugConfiguration[]> {
|
||||
const configurations: DebugConfiguration[] = [];
|
||||
for (const contribution of this.getContributions(debugType)) {
|
||||
if (contribution.provideDebugConfigurations) {
|
||||
try {
|
||||
const result = await contribution.provideDebugConfigurations(workspaceFolderUri);
|
||||
configurations.push(...result);
|
||||
} catch (e) {
|
||||
console.error('provideDebugConfigurations failed:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return configurations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a [debug configuration](#DebugConfiguration) by filling in missing values
|
||||
* or by adding/changing/removing attributes before variable substitution.
|
||||
* @param debugConfiguration The [debug configuration](#DebugConfiguration) to resolve.
|
||||
* @returns The resolved debug configuration.
|
||||
*/
|
||||
async resolveDebugConfiguration(config: DebugConfiguration, workspaceFolderUri?: string): Promise<DebugConfiguration> {
|
||||
let current = config;
|
||||
for (const contribution of this.getContributions(config.type)) {
|
||||
if (contribution.resolveDebugConfiguration) {
|
||||
try {
|
||||
const next = await contribution.resolveDebugConfiguration(config, workspaceFolderUri);
|
||||
if (next) {
|
||||
current = next;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('resolveDebugConfiguration failed:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a [debug configuration](#DebugConfiguration) by filling in missing values
|
||||
* or by adding/changing/removing attributes with substituted variables.
|
||||
* @param debugConfiguration The [debug configuration](#DebugConfiguration) to resolve.
|
||||
* @returns The resolved debug configuration.
|
||||
*/
|
||||
async resolveDebugConfigurationWithSubstitutedVariables(config: DebugConfiguration, workspaceFolderUri?: string): Promise<DebugConfiguration> {
|
||||
let current = config;
|
||||
for (const contribution of this.getContributions(config.type)) {
|
||||
if (contribution.resolveDebugConfigurationWithSubstitutedVariables) {
|
||||
try {
|
||||
const next = await contribution.resolveDebugConfigurationWithSubstitutedVariables(config, workspaceFolderUri);
|
||||
if (next) {
|
||||
current = next;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('resolveDebugConfigurationWithSubstitutedVariables failed:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides schema attributes.
|
||||
* @param debugType The registered debug type
|
||||
* @returns Schema attributes for the given debug type
|
||||
*/
|
||||
async getSchemaAttributes(debugType: string): Promise<IJSONSchema[]> {
|
||||
const schemas: IJSONSchema[] = [];
|
||||
for (const contribution of this.getContributions(debugType)) {
|
||||
if (contribution.getSchemaAttributes) {
|
||||
try {
|
||||
schemas.push(...await contribution.getSchemaAttributes());
|
||||
} catch (e) {
|
||||
console.error('getSchemaAttributes failed:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return schemas;
|
||||
}
|
||||
async getConfigurationSnippets(): Promise<IJSONSchemaSnippet[]> {
|
||||
const schemas: IJSONSchemaSnippet[] = [];
|
||||
for (const contribution of this.getContributions('*')) {
|
||||
if (contribution.getConfigurationSnippets) {
|
||||
try {
|
||||
schemas.push(...await contribution.getConfigurationSnippets());
|
||||
} catch (e) {
|
||||
console.error('getConfigurationSnippets failed:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return schemas;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
async provideDebugAdapterExecutable(config: DebugConfiguration): Promise<DebugAdapterExecutable> {
|
||||
for (const contribution of this.getContributions(config.type)) {
|
||||
if (contribution.provideDebugAdapterExecutable) {
|
||||
const executable = await contribution.provideDebugAdapterExecutable(config);
|
||||
if (executable) {
|
||||
return executable;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw DebugError.NotFound(config.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
for (const contribution of this.getContributions(debugType)) {
|
||||
if (contribution.debugAdapterSessionFactory) {
|
||||
return contribution.debugAdapterSessionFactory;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
101
packages/debug/src/common/debug-adapter-session.ts
Normal file
101
packages/debug/src/common/debug-adapter-session.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
// *****************************************************************************
|
||||
// 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-only 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 {
|
||||
DebugAdapter,
|
||||
DebugAdapterSession
|
||||
} from './debug-model';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol';
|
||||
import { DebugChannel } from './debug-service';
|
||||
|
||||
/**
|
||||
* [DebugAdapterSession](#DebugAdapterSession) implementation.
|
||||
*/
|
||||
export class DebugAdapterSessionImpl implements DebugAdapterSession {
|
||||
|
||||
private channel: DebugChannel | undefined;
|
||||
private isClosed: boolean = false;
|
||||
|
||||
constructor(
|
||||
readonly id: string,
|
||||
protected readonly debugAdapter: DebugAdapter
|
||||
) {
|
||||
this.debugAdapter.onMessageReceived((message: string) => this.send(message));
|
||||
this.debugAdapter.onClose(() => this.onDebugAdapterExit());
|
||||
this.debugAdapter.onError(error => this.onDebugAdapterError(error));
|
||||
}
|
||||
|
||||
async start(channel: DebugChannel): Promise<void> {
|
||||
|
||||
console.debug(`starting debug adapter session '${this.id}'`);
|
||||
if (this.channel) {
|
||||
throw new Error('The session has already been started, id: ' + this.id);
|
||||
}
|
||||
this.channel = channel;
|
||||
this.channel.onMessage((message: string) => this.write(message));
|
||||
this.channel.onClose(() => this.channel = undefined);
|
||||
|
||||
}
|
||||
|
||||
protected onDebugAdapterExit(): void {
|
||||
this.isClosed = true;
|
||||
console.debug(`onDebugAdapterExit session: '${this.id}'`);
|
||||
if (this.channel) {
|
||||
this.channel.close();
|
||||
this.channel = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected onDebugAdapterError(error: Error): void {
|
||||
console.debug(`error in debug adapter session: '${this.id}': ${JSON.stringify(error)}`);
|
||||
const event: DebugProtocol.Event = {
|
||||
type: 'event',
|
||||
event: 'error',
|
||||
seq: -1,
|
||||
body: error
|
||||
};
|
||||
this.send(JSON.stringify(event));
|
||||
}
|
||||
|
||||
protected send(message: string): void {
|
||||
if (this.channel) {
|
||||
this.channel.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
protected write(message: string): void {
|
||||
if (!this.isClosed) {
|
||||
this.debugAdapter.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
console.debug(`stopping debug adapter session: '${this.id}'`);
|
||||
|
||||
if (!this.isClosed) {
|
||||
await this.debugAdapter.stop();
|
||||
}
|
||||
this.channel?.close();
|
||||
this.channel = undefined;
|
||||
}
|
||||
}
|
||||
19
packages/debug/src/common/debug-common.ts
Normal file
19
packages/debug/src/common/debug-common.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// FIXME: refactor extensions to get rid of this file and remove it
|
||||
|
||||
export * from './debug-configuration';
|
||||
33
packages/debug/src/common/debug-compound.ts
Normal file
33
packages/debug/src/common/debug-compound.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 EclipseSource 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { isObject } from '@theia/core/lib/common';
|
||||
import { TaskIdentifier } from '@theia/task/lib/common';
|
||||
|
||||
export const defaultCompound: DebugCompound = { name: 'Compound', configurations: [] };
|
||||
|
||||
export interface DebugCompound {
|
||||
name: string;
|
||||
stopAll?: boolean;
|
||||
preLaunchTask?: string | TaskIdentifier;
|
||||
configurations: (string | { name: string, folder: string })[];
|
||||
}
|
||||
|
||||
export namespace DebugCompound {
|
||||
export function is(arg: unknown): arg is DebugCompound {
|
||||
return isObject(arg) && 'name' in arg && 'configurations' in arg;
|
||||
}
|
||||
}
|
||||
115
packages/debug/src/common/debug-configuration.ts
Normal file
115
packages/debug/src/common/debug-configuration.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import { isObject } from '@theia/core/lib/common';
|
||||
import { TaskIdentifier } from '@theia/task/lib/common';
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Additional debug type specific properties.
|
||||
*/
|
||||
[key: string]: any;
|
||||
|
||||
parentSessionId?: string;
|
||||
|
||||
lifecycleManagedByParent?: boolean;
|
||||
|
||||
consoleMode?: DebugConsoleMode;
|
||||
|
||||
compact?: boolean;
|
||||
|
||||
/**
|
||||
* The request type of the debug adapter session.
|
||||
*/
|
||||
request: string;
|
||||
|
||||
/**
|
||||
* If noDebug is true the launch request should launch the program without enabling debugging.
|
||||
*/
|
||||
noDebug?: boolean;
|
||||
|
||||
/**
|
||||
* Optional data from the previous, restarted session.
|
||||
* The data is sent as the 'restart' attribute of the 'terminated' event.
|
||||
* The client should leave the data intact.
|
||||
*/
|
||||
__restart?: boolean;
|
||||
|
||||
/** default: neverOpen */
|
||||
openDebug?: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart' | 'openOnDebugBreak';
|
||||
|
||||
/** default: neverOpen */
|
||||
internalConsoleOptions?: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart'
|
||||
|
||||
/** Task to run before debug session starts */
|
||||
preLaunchTask?: string | TaskIdentifier;
|
||||
|
||||
/** Task to run after debug session ends */
|
||||
postDebugTask?: string | TaskIdentifier;
|
||||
|
||||
/**
|
||||
* When true, a save will not be triggered for open editors when starting a debug session,
|
||||
* regardless of the value of the `debug.saveBeforeStart` setting.
|
||||
*/
|
||||
suppressSaveBeforeStart?: boolean;
|
||||
|
||||
/** When true, the window statusbar color will not be changed for this session. */
|
||||
suppressDebugStatusbar?: boolean;
|
||||
|
||||
/** When true, the debug viewlet will not be automatically revealed for this session. */
|
||||
suppressDebugView?: boolean;
|
||||
|
||||
/** Disable the warning when trying to start the same debug configuration more than once. */
|
||||
suppressMultipleSessionWarning?: boolean;
|
||||
}
|
||||
export namespace DebugConfiguration {
|
||||
export function is(arg: unknown): arg is DebugConfiguration {
|
||||
return isObject(arg) && 'type' in arg && 'name' in arg && 'request' in arg;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DebugSessionOptions {
|
||||
lifecycleManagedByParent?: boolean;
|
||||
parentSessionId?: string;
|
||||
consoleMode?: DebugConsoleMode;
|
||||
noDebug?: boolean;
|
||||
compact?: boolean;
|
||||
suppressSaveBeforeStart?: boolean;
|
||||
suppressDebugStatusbar?: boolean;
|
||||
suppressDebugView?: boolean;
|
||||
testRun?: {
|
||||
controllerId: string,
|
||||
runId: string
|
||||
}
|
||||
}
|
||||
|
||||
export enum DebugConsoleMode {
|
||||
Separate = 0,
|
||||
MergeWithParent = 1
|
||||
}
|
||||
200
packages/debug/src/common/debug-model.ts
Normal file
200
packages/debug/src/common/debug-model.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
// *****************************************************************************
|
||||
// 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-only 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 { DebugConfiguration } from './debug-configuration';
|
||||
import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { Event } from '@theia/core';
|
||||
import { DebugChannel } from './debug-service';
|
||||
|
||||
// FIXME: break down this file to debug adapter and debug adapter contribution (see Theia file naming conventions)
|
||||
|
||||
/**
|
||||
* DebugAdapterSession symbol for DI.
|
||||
*/
|
||||
export const DebugAdapterSession = Symbol('DebugAdapterSession');
|
||||
|
||||
/**
|
||||
* The debug adapter session. The debug adapter session manages the lifecycle of a
|
||||
* debug session: the debug session should be discarded if and only if the debug adapter
|
||||
* session is stopped.
|
||||
*/
|
||||
export interface DebugAdapterSession {
|
||||
id: string;
|
||||
parentSession?: DebugAdapterSession;
|
||||
start(channel: DebugChannel): Promise<void>
|
||||
stop(): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* DebugAdapterSessionFactory symbol for DI.
|
||||
*/
|
||||
export const DebugAdapterSessionFactory = Symbol('DebugAdapterSessionFactory');
|
||||
|
||||
/**
|
||||
* The [debug session](#DebugSession) factory.
|
||||
*/
|
||||
export interface DebugAdapterSessionFactory {
|
||||
get(sessionId: string, debugAdapter: DebugAdapter): DebugAdapterSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug adapter executable for spawning.
|
||||
*/
|
||||
export interface DebugAdapterSpawnExecutable {
|
||||
command: string;
|
||||
args?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug adapter executable for forking.
|
||||
*/
|
||||
export interface DebugAdapterForkExecutable {
|
||||
modulePath: string;
|
||||
execArgv?: string[];
|
||||
args?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug adapter executable.
|
||||
* Parameters to instantiate the debug adapter.
|
||||
*
|
||||
* In case of launching adapter the parameters contain a command and arguments. For instance:
|
||||
* {'command' : 'COMMAND_TO_LAUNCH_DEBUG_ADAPTER', args : [ { 'arg1', 'arg2' } ] }
|
||||
*
|
||||
* In case of forking the node process, contain the modulePath to fork. For instance:
|
||||
* {'modulePath' : 'NODE_COMMAND_TO_LAUNCH_DEBUG_ADAPTER', args : [ { 'arg1', 'arg2' } ] }
|
||||
*/
|
||||
export type DebugAdapterExecutable = DebugAdapterSpawnExecutable | DebugAdapterForkExecutable;
|
||||
|
||||
/**
|
||||
* Implementers stand for the various types of debug adapters the system can talk to.
|
||||
* Creation of debug adapters is not covered in this interface, but handling communication
|
||||
* and the end of life is.
|
||||
*/
|
||||
|
||||
export interface DebugAdapter {
|
||||
/**
|
||||
* A DAP protocol message has been received from the debug adapter
|
||||
*/
|
||||
onMessageReceived: Event<string>;
|
||||
/**
|
||||
* Send a DAP message to the debug adapter
|
||||
* @param message the JSON-encoded DAP message
|
||||
*/
|
||||
send(message: string): void;
|
||||
/**
|
||||
* An error has occurred communicating with the debug adapter. This does not meant the debug adapter
|
||||
* has terminated.
|
||||
*/
|
||||
onError: Event<Error>;
|
||||
/**
|
||||
* The connection to the debug adapter has been lost. This signals the end of life for this
|
||||
* debug adapter instance.
|
||||
*/
|
||||
onClose: Event<void>;
|
||||
/**
|
||||
* Terminate the connection to the debug adapter.
|
||||
*/
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* DebugAdapterFactory symbol for DI.
|
||||
*/
|
||||
export const DebugAdapterFactory = Symbol('DebugAdapterFactory');
|
||||
|
||||
/**
|
||||
* Factory to start debug adapter.
|
||||
*/
|
||||
export interface DebugAdapterFactory {
|
||||
start(executable: DebugAdapterExecutable): DebugAdapter;
|
||||
connect(debugServerPort: number): DebugAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 type: string;
|
||||
|
||||
readonly label?: MaybePromise<string | undefined>;
|
||||
|
||||
readonly languages?: MaybePromise<string[] | undefined>;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* @returns The contributed configuration schema for this debug type.
|
||||
*/
|
||||
getSchemaAttributes?(): MaybePromise<IJSONSchema[]>;
|
||||
|
||||
getConfigurationSnippets?(): MaybePromise<IJSONSchemaSnippet[]>;
|
||||
|
||||
/**
|
||||
* 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): MaybePromise<DebugAdapterExecutable | undefined>;
|
||||
|
||||
/**
|
||||
* Provides initial [debug configuration](#DebugConfiguration).
|
||||
* @returns An array of [debug configurations](#DebugConfiguration).
|
||||
*/
|
||||
provideDebugConfigurations?(workspaceFolderUri?: string): MaybePromise<DebugConfiguration[]>;
|
||||
|
||||
/**
|
||||
* Resolves a [debug configuration](#DebugConfiguration) by filling in missing values
|
||||
* or by adding/changing/removing attributes before variable substitution.
|
||||
* @param config The [debug configuration](#DebugConfiguration) to resolve.
|
||||
* @returns The resolved debug configuration.
|
||||
*/
|
||||
resolveDebugConfiguration?(config: DebugConfiguration, workspaceFolderUri?: string): MaybePromise<DebugConfiguration | undefined>;
|
||||
|
||||
/**
|
||||
* Resolves a [debug configuration](#DebugConfiguration) by filling in missing values
|
||||
* or by adding/changing/removing attributes with substituted variables.
|
||||
* @param config The [debug configuration](#DebugConfiguration) to resolve.
|
||||
* @returns The resolved debug configuration.
|
||||
*/
|
||||
resolveDebugConfigurationWithSubstitutedVariables?(config: DebugConfiguration, workspaceFolderUri?: string): MaybePromise<DebugConfiguration | undefined>;
|
||||
}
|
||||
108
packages/debug/src/common/debug-preferences.ts
Normal file
108
packages/debug/src/common/debug-preferences.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { PreferenceContribution, PreferenceProxy, PreferenceSchema, PreferenceService, createPreferenceProxy } from '@theia/core/lib/common/preferences';
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
|
||||
export const debugPreferencesSchema: PreferenceSchema = {
|
||||
properties: {
|
||||
'debug.trace': {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: nls.localize('theia/debug/toggleTracing', 'Enable/disable tracing communications with debug adapters')
|
||||
},
|
||||
'debug.openDebug': {
|
||||
enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart', 'openOnDebugBreak'],
|
||||
default: 'openOnSessionStart',
|
||||
description: nls.localizeByDefault('Controls when the debug view should open.')
|
||||
},
|
||||
'debug.internalConsoleOptions': {
|
||||
enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart'],
|
||||
default: 'openOnFirstSessionStart',
|
||||
description: nls.localizeByDefault('Controls when the internal Debug Console should open.')
|
||||
},
|
||||
'debug.inlineValues': {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: nls.localizeByDefault('Show variable values inline in editor while debugging.')
|
||||
},
|
||||
'debug.showInStatusBar': {
|
||||
enum: ['never', 'always', 'onFirstSessionStart'],
|
||||
enumDescriptions: [
|
||||
nls.localizeByDefault('Never show debug item in status bar'),
|
||||
nls.localizeByDefault('Always show debug item in status bar'),
|
||||
nls.localizeByDefault('Show debug item in status bar only after debug was started for the first time')
|
||||
],
|
||||
description: nls.localizeByDefault('Controls when the debug status bar item should be visible.'),
|
||||
default: 'onFirstSessionStart'
|
||||
},
|
||||
'debug.confirmOnExit': {
|
||||
description: nls.localizeByDefault('Controls whether to confirm when the window closes if there are active debug sessions.'),
|
||||
type: 'string',
|
||||
enum: ['never', 'always'],
|
||||
enumDescriptions: [
|
||||
nls.localizeByDefault('Never confirm.'),
|
||||
nls.localizeByDefault('Always confirm if there are debug sessions.'),
|
||||
],
|
||||
default: 'never'
|
||||
},
|
||||
'debug.disassemblyView.showSourceCode': {
|
||||
description: nls.localizeByDefault('Show Source Code in Disassembly View.'),
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
'debug.autoExpandLazyVariables': {
|
||||
type: 'string',
|
||||
enum: ['on', 'off'],
|
||||
default: 'off',
|
||||
enumDescriptions: [
|
||||
nls.localizeByDefault('Always automatically expand lazy variables.'),
|
||||
nls.localizeByDefault('Never automatically expand lazy variables.')
|
||||
],
|
||||
description: nls.localizeByDefault('Controls whether variables that are lazily resolved, such as getters, are automatically resolved and expanded by the debugger.')
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export class DebugConfiguration {
|
||||
'debug.trace': boolean;
|
||||
'debug.openDebug': 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart' | 'openOnDebugBreak';
|
||||
'debug.internalConsoleOptions': 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart';
|
||||
'debug.inlineValues': boolean;
|
||||
'debug.showInStatusBar': 'never' | 'always' | 'onFirstSessionStart';
|
||||
'debug.confirmOnExit': 'never' | 'always';
|
||||
'debug.disassemblyView.showSourceCode': boolean;
|
||||
'debug.autoExpandLazyVariables': 'on' | 'off';
|
||||
}
|
||||
|
||||
export const DebugPreferenceContribution = Symbol('DebugPreferenceContribution');
|
||||
export const DebugPreferences = Symbol('DebugPreferences');
|
||||
export type DebugPreferences = PreferenceProxy<DebugConfiguration>;
|
||||
|
||||
export function createDebugPreferences(preferences: PreferenceService, schema: PreferenceSchema = debugPreferencesSchema): DebugPreferences {
|
||||
return createPreferenceProxy(preferences, schema);
|
||||
}
|
||||
|
||||
export function bindDebugPreferences(bind: interfaces.Bind): void {
|
||||
bind(DebugPreferences).toDynamicValue(ctx => {
|
||||
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
|
||||
const contribution = ctx.container.get<PreferenceContribution>(DebugPreferenceContribution);
|
||||
return createDebugPreferences(preferences, contribution.schema);
|
||||
}).inSingletonScope();
|
||||
bind(DebugPreferenceContribution).toConstantValue({ schema: debugPreferencesSchema });
|
||||
bind(PreferenceContribution).toService(DebugPreferenceContribution);
|
||||
}
|
||||
184
packages/debug/src/common/debug-service.ts
Normal file
184
packages/debug/src/common/debug-service.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Channel, Disposable, Emitter, Event } from '@theia/core';
|
||||
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||
import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema';
|
||||
import { CommandIdVariables } from '@theia/variable-resolver/lib/common/variable-types';
|
||||
import { DebugConfiguration } from './debug-configuration';
|
||||
|
||||
export interface DebuggerDescription {
|
||||
type: string
|
||||
label: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
onDidChangeDebuggers?: Event<void>;
|
||||
|
||||
/**
|
||||
* Finds and returns an array of registered debug types.
|
||||
* @returns An array of registered debug types
|
||||
*/
|
||||
debugTypes(): Promise<string[]>;
|
||||
|
||||
getDebuggersForLanguage(language: string): Promise<DebuggerDescription[]>;
|
||||
|
||||
/**
|
||||
* Provide debugger contributed variables
|
||||
* see "variables" at https://code.visualstudio.com/api/references/contribution-points#contributes.debuggers
|
||||
*/
|
||||
provideDebuggerVariables(debugType: string): Promise<CommandIdVariables>;
|
||||
|
||||
/**
|
||||
* Provides the schema attributes.
|
||||
* @param debugType The registered debug type
|
||||
* @returns An JSON Schema describing the configuration attributes for the given debug type
|
||||
*/
|
||||
getSchemaAttributes(debugType: string): Promise<IJSONSchema[]>;
|
||||
|
||||
getConfigurationSnippets(): Promise<IJSONSchemaSnippet[]>;
|
||||
|
||||
/**
|
||||
* Provides initial [debug configuration](#DebugConfiguration).
|
||||
* @param debugType The registered debug type
|
||||
* @returns An array of [debug configurations](#DebugConfiguration)
|
||||
*/
|
||||
provideDebugConfigurations(debugType: string, workspaceFolderUri: string | undefined): Promise<DebugConfiguration[]>;
|
||||
|
||||
/**
|
||||
* @returns A Record of debug configuration provider types and a corresponding dynamic debug configurations array
|
||||
*/
|
||||
provideDynamicDebugConfigurations?(folder?: string): Promise<Record<string, DebugConfiguration[]>>;
|
||||
|
||||
/**
|
||||
* Provides a dynamic debug configuration matching the name and the provider debug type
|
||||
*/
|
||||
fetchDynamicDebugConfiguration(name: string, type: string, folder?: string): Promise<DebugConfiguration | undefined>;
|
||||
|
||||
/**
|
||||
* Resolves a [debug configuration](#DebugConfiguration) by filling in missing values
|
||||
* or by adding/changing/removing attributes before variable substitution.
|
||||
* @param debugConfiguration The [debug configuration](#DebugConfiguration) to resolve.
|
||||
* @returns The resolved debug configuration, undefined or null.
|
||||
*/
|
||||
resolveDebugConfiguration(
|
||||
config: DebugConfiguration,
|
||||
workspaceFolderUri: string | undefined
|
||||
): Promise<DebugConfiguration | undefined | null>;
|
||||
|
||||
/**
|
||||
* Resolves a [debug configuration](#DebugConfiguration) by filling in missing values
|
||||
* or by adding/changing/removing attributes with substituted variables.
|
||||
* @param debugConfiguration The [debug configuration](#DebugConfiguration) to resolve.
|
||||
* @returns The resolved debug configuration, undefined or null.
|
||||
*/
|
||||
resolveDebugConfigurationWithSubstitutedVariables(
|
||||
config: DebugConfiguration,
|
||||
workspaceFolderUri: string | undefined
|
||||
): Promise<DebugConfiguration | undefined | null>;
|
||||
|
||||
/**
|
||||
* Creates a new [debug adapter session](#DebugAdapterSession).
|
||||
* @param config The resolved [debug configuration](#DebugConfiguration).
|
||||
* @param workspaceFolderUri The workspace folder for this sessions or undefined when folderless
|
||||
* @returns The identifier of the created [debug adapter session](#DebugAdapterSession).
|
||||
*/
|
||||
createDebugSession(config: DebugConfiguration, workspaceFolderUri: string | undefined): Promise<string>;
|
||||
|
||||
/**
|
||||
* Stop a running session for the given session id.
|
||||
*/
|
||||
terminateDebugSession(sessionId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Event handle to indicate when one or more dynamic debug configuration providers
|
||||
* have been registered or unregistered.
|
||||
*/
|
||||
onDidChangeDebugConfigurationProviders: Event<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The endpoint path to the debug adapter session.
|
||||
*/
|
||||
export const DebugAdapterPath = '/services/debug-adapter';
|
||||
|
||||
export namespace DebugError {
|
||||
export const NotFound = ApplicationError.declare(-41000, (type: string) => ({
|
||||
message: `'${type}' debugger type is not supported.`,
|
||||
data: { type }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* A closeable channel to send debug protocol messages over with error/close handling
|
||||
*/
|
||||
export interface DebugChannel {
|
||||
send(content: string): void;
|
||||
onMessage(cb: (message: string) => void): void;
|
||||
onError(cb: (reason: unknown) => void): void;
|
||||
onClose(cb: (code: number, reason: string) => void): void;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link DebugChannel} wrapper implementation that sends and receives messages to/from an underlying {@link Channel}.
|
||||
*/
|
||||
export class ForwardingDebugChannel implements DebugChannel {
|
||||
private onMessageEmitter = new Emitter<string>();
|
||||
|
||||
constructor(private readonly underlyingChannel: Channel) {
|
||||
this.underlyingChannel.onMessage(msg => this.onMessageEmitter.fire(msg().readString()));
|
||||
}
|
||||
|
||||
send(content: string): void {
|
||||
this.underlyingChannel.getWriteBuffer().writeString(content).commit();
|
||||
}
|
||||
|
||||
onMessage(cb: (message: string) => void): void {
|
||||
this.onMessageEmitter.event(cb);
|
||||
}
|
||||
onError(cb: (reason: unknown) => void): void {
|
||||
this.underlyingChannel.onError(cb);
|
||||
}
|
||||
onClose(cb: (code: number, reason: string) => void): void {
|
||||
this.underlyingChannel.onClose(event => cb(event.code ?? -1, event.reason));
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.underlyingChannel.close();
|
||||
this.onMessageEmitter.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
24
packages/debug/src/common/debug-uri-utils.ts
Normal file
24
packages/debug/src/common/debug-uri-utils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2022 STMicroelectronics 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-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
/**
|
||||
* The URI scheme for debug URIs.
|
||||
*/
|
||||
export const DEBUG_SCHEME = 'debug';
|
||||
/**
|
||||
* The pattern for URI schemes.
|
||||
*/
|
||||
export const SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/;
|
||||
47
packages/debug/src/common/inline-debug-adapter.ts
Normal file
47
packages/debug/src/common/inline-debug-adapter.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { DebugAdapter } from './debug-model';
|
||||
import * as theia from '@theia/plugin';
|
||||
|
||||
/**
|
||||
* A debug adapter for using the inline implementation from a plugin.
|
||||
*/
|
||||
export class InlineDebugAdapter implements DebugAdapter {
|
||||
private messageReceivedEmitter = new Emitter<string>();
|
||||
onMessageReceived: Event<string> = this.messageReceivedEmitter.event;
|
||||
onError: Event<Error> = Event.None;
|
||||
private closeEmitter = new Emitter<void>();
|
||||
onClose: Event<void> = this.closeEmitter.event;
|
||||
|
||||
constructor(private debugAdapter: theia.DebugAdapter) {
|
||||
this.debugAdapter.onDidSendMessage(msg => {
|
||||
this.messageReceivedEmitter.fire(JSON.stringify(msg));
|
||||
});
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
}
|
||||
|
||||
send(message: string): void {
|
||||
this.debugAdapter.handleMessage(JSON.parse(message));
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.debugAdapter.dispose();
|
||||
}
|
||||
}
|
||||
37
packages/debug/src/common/launch-preferences.ts
Normal file
37
packages/debug/src/common/launch-preferences.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { PreferenceConfiguration, PreferenceContribution, PreferenceSchema, PreferenceScope } from '@theia/core/lib/common';
|
||||
|
||||
export const launchSchemaId = 'vscode://schemas/launch';
|
||||
|
||||
export const launchPreferencesSchema: PreferenceSchema = {
|
||||
scope: PreferenceScope.Folder,
|
||||
properties: {
|
||||
'launch': {
|
||||
$ref: launchSchemaId,
|
||||
description: nls.localizeByDefault("Global debug launch configuration. Should be used as an alternative to 'launch.json' that is shared across workspaces."),
|
||||
default: { configurations: [], compounds: [] }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function bindLaunchPreferences(bind: interfaces.Bind): void {
|
||||
bind(PreferenceContribution).toConstantValue({ schema: launchPreferencesSchema });
|
||||
bind(PreferenceConfiguration).toConstantValue({ name: 'launch' });
|
||||
}
|
||||
107
packages/debug/src/node/debug-adapter-factory.ts
Normal file
107
packages/debug/src/node/debug-adapter-factory.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
// *****************************************************************************
|
||||
// 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-only 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 net from 'net';
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
RawProcessFactory,
|
||||
ProcessManager,
|
||||
RawProcess,
|
||||
RawForkOptions,
|
||||
RawProcessOptions
|
||||
} from '@theia/process/lib/node';
|
||||
import {
|
||||
DebugAdapterExecutable,
|
||||
DebugAdapterSession,
|
||||
DebugAdapterSessionFactory,
|
||||
DebugAdapterFactory,
|
||||
DebugAdapterForkExecutable,
|
||||
DebugAdapter
|
||||
} from '../common/debug-model';
|
||||
import { DebugAdapterSessionImpl } from '../common/debug-adapter-session';
|
||||
import { environment } from '@theia/core/shared/@theia/application-package';
|
||||
import { ProcessDebugAdapter, SocketDebugAdapter } from './stream-debug-adapter';
|
||||
import { isObject } from '@theia/core/lib/common';
|
||||
|
||||
/**
|
||||
* [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): DebugAdapter {
|
||||
const process = this.childProcess(executable);
|
||||
|
||||
if (!process.process) {
|
||||
throw new Error(`Could not start debug adapter process: ${JSON.stringify(executable)}`);
|
||||
}
|
||||
|
||||
// FIXME: propagate onError + onExit
|
||||
const provider = new ProcessDebugAdapter(process.process);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private childProcess(executable: DebugAdapterExecutable): RawProcess {
|
||||
const isForkOptions = (forkOptions: unknown): forkOptions is RawForkOptions =>
|
||||
isObject(forkOptions) && 'modulePath' in forkOptions;
|
||||
|
||||
const processOptions: RawProcessOptions | RawForkOptions = { ...executable };
|
||||
const options: { stdio: (string | number)[], env?: object, execArgv?: string[] } = { stdio: ['pipe', 'pipe', 2] };
|
||||
|
||||
if (isForkOptions(processOptions)) {
|
||||
options.stdio.push('ipc');
|
||||
options.env = environment.electron.runAsNodeEnv();
|
||||
options.execArgv = (executable as DebugAdapterForkExecutable).execArgv;
|
||||
}
|
||||
|
||||
processOptions.options = options;
|
||||
return this.processFactory(processOptions);
|
||||
}
|
||||
|
||||
connect(debugServerPort: number): DebugAdapter {
|
||||
const socket = net.createConnection(debugServerPort);
|
||||
// FIXME: propagate socket.on('error', ...) + socket.on('close', ...)
|
||||
|
||||
const provider = new SocketDebugAdapter(socket);
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [DebugAdapterSessionFactory](#DebugAdapterSessionFactory) implementation.
|
||||
*/
|
||||
@injectable()
|
||||
export class DebugAdapterSessionFactoryImpl implements DebugAdapterSessionFactory {
|
||||
|
||||
get(sessionId: string, debugAdapter: DebugAdapter): DebugAdapterSession {
|
||||
return new DebugAdapterSessionImpl(
|
||||
sessionId,
|
||||
debugAdapter
|
||||
);
|
||||
}
|
||||
}
|
||||
106
packages/debug/src/node/debug-adapter-session-manager.ts
Normal file
106
packages/debug/src/node/debug-adapter-session-manager.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { UUID } from '@theia/core/shared/@lumino/coreutils';
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service';
|
||||
|
||||
import { DebugAdapterPath, ForwardingDebugChannel } from '../common/debug-service';
|
||||
import { DebugConfiguration } from '../common/debug-configuration';
|
||||
import { DebugAdapterSession, DebugAdapterSessionFactory, DebugAdapterFactory } from '../common/debug-model';
|
||||
import { DebugAdapterContributionRegistry } from '../common/debug-adapter-contribution-registry';
|
||||
|
||||
/**
|
||||
* Debug adapter session manager.
|
||||
*/
|
||||
@injectable()
|
||||
export class DebugAdapterSessionManager implements MessagingService.Contribution {
|
||||
protected readonly sessions = new Map<string, DebugAdapterSession>();
|
||||
|
||||
@inject(DebugAdapterSessionFactory)
|
||||
protected readonly debugAdapterSessionFactory: DebugAdapterSessionFactory;
|
||||
|
||||
@inject(DebugAdapterFactory)
|
||||
protected readonly debugAdapterFactory: DebugAdapterFactory;
|
||||
|
||||
configure(service: MessagingService): void {
|
||||
service.registerChannelHandler(`${DebugAdapterPath}/:id`, ({ id }: { id: string }, wsChannel) => {
|
||||
const session = this.find(id);
|
||||
if (!session) {
|
||||
wsChannel.close();
|
||||
return;
|
||||
}
|
||||
session.start(new ForwardingDebugChannel(wsChannel));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new [debug adapter session](#DebugAdapterSession).
|
||||
* @param config The [DebugConfiguration](#DebugConfiguration)
|
||||
* @returns The debug adapter session
|
||||
*/
|
||||
async create(config: DebugConfiguration, registry: DebugAdapterContributionRegistry): Promise<DebugAdapterSession> {
|
||||
const sessionId = UUID.uuid4();
|
||||
|
||||
let communicationProvider;
|
||||
if ('debugServer' in config) {
|
||||
communicationProvider = this.debugAdapterFactory.connect(config.debugServer);
|
||||
} else {
|
||||
const executable = await registry.provideDebugAdapterExecutable(config);
|
||||
communicationProvider = this.debugAdapterFactory.start(executable);
|
||||
}
|
||||
|
||||
const sessionFactory = registry.debugAdapterSessionFactory(config.type) || this.debugAdapterSessionFactory;
|
||||
const session = sessionFactory.get(sessionId, communicationProvider);
|
||||
this.sessions.set(sessionId, session);
|
||||
|
||||
if (config.parentSession) {
|
||||
const parentSession = this.sessions.get(config.parentSession.id);
|
||||
if (parentSession) {
|
||||
session.parentSession = parentSession;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all instantiated debug adapter sessions.
|
||||
* @returns An array of debug adapter sessions
|
||||
*/
|
||||
getAll(): IterableIterator<DebugAdapterSession> {
|
||||
return this.sessions.values();
|
||||
}
|
||||
}
|
||||
57
packages/debug/src/node/debug-backend-module.ts
Normal file
57
packages/debug/src/node/debug-backend-module.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { bindContributionProvider } from '@theia/core/lib/common';
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
DebugPath,
|
||||
DebugService
|
||||
} from '../common/debug-service';
|
||||
import {
|
||||
LaunchBasedDebugAdapterFactory,
|
||||
DebugAdapterSessionFactoryImpl
|
||||
} from './debug-adapter-factory';
|
||||
import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service';
|
||||
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
|
||||
import {
|
||||
DebugAdapterContribution,
|
||||
DebugAdapterSessionFactory,
|
||||
DebugAdapterFactory
|
||||
} from '../common/debug-model';
|
||||
import { DebugServiceImpl } from './debug-service-impl';
|
||||
import { DebugAdapterContributionRegistry } from '../common/debug-adapter-contribution-registry';
|
||||
import { DebugAdapterSessionManager } from './debug-adapter-session-manager';
|
||||
import { bindDebugPreferences } from '../common/debug-preferences';
|
||||
import { bindLaunchPreferences } from '../common/launch-preferences';
|
||||
|
||||
const debugConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bindContributionProvider(bind, DebugAdapterContribution);
|
||||
bind(DebugAdapterContributionRegistry).toSelf().inSingletonScope();
|
||||
|
||||
bind(DebugService).to(DebugServiceImpl).inSingletonScope();
|
||||
bindBackendService(DebugPath, DebugService);
|
||||
});
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(ConnectionContainerModule).toConstantValue(debugConnectionModule);
|
||||
|
||||
bind(DebugAdapterSessionFactory).to(DebugAdapterSessionFactoryImpl).inSingletonScope();
|
||||
bind(DebugAdapterFactory).to(LaunchBasedDebugAdapterFactory).inSingletonScope();
|
||||
bind(DebugAdapterSessionManager).toSelf().inSingletonScope();
|
||||
bind(MessagingService.Contribution).toService(DebugAdapterSessionManager);
|
||||
bindDebugPreferences(bind);
|
||||
bindLaunchPreferences(bind);
|
||||
});
|
||||
119
packages/debug/src/node/debug-service-impl.ts
Normal file
119
packages/debug/src/node/debug-service-impl.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { DebugConfiguration } from '../common/debug-configuration';
|
||||
import { DebugService, DebuggerDescription } from '../common/debug-service';
|
||||
import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema';
|
||||
import { CommandIdVariables } from '@theia/variable-resolver/lib/common/variable-types';
|
||||
import { DebugAdapterSessionManager } from './debug-adapter-session-manager';
|
||||
import { DebugAdapterContributionRegistry } from '../common/debug-adapter-contribution-registry';
|
||||
import { Event } from '@theia/core';
|
||||
|
||||
/**
|
||||
* DebugService implementation.
|
||||
*/
|
||||
@injectable()
|
||||
export class DebugServiceImpl implements DebugService {
|
||||
|
||||
@inject(DebugAdapterSessionManager)
|
||||
protected readonly sessionManager: DebugAdapterSessionManager;
|
||||
|
||||
@inject(DebugAdapterContributionRegistry)
|
||||
protected readonly registry: DebugAdapterContributionRegistry;
|
||||
|
||||
get onDidChangeDebugConfigurationProviders(): Event<void> {
|
||||
return Event.None;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.terminateDebugSession();
|
||||
}
|
||||
|
||||
async debugTypes(): Promise<string[]> {
|
||||
return this.registry.debugTypes();
|
||||
}
|
||||
|
||||
getDebuggersForLanguage(language: string): Promise<DebuggerDescription[]> {
|
||||
return this.registry.getDebuggersForLanguage(language);
|
||||
}
|
||||
|
||||
getSchemaAttributes(debugType: string): Promise<IJSONSchema[]> {
|
||||
return this.registry.getSchemaAttributes(debugType);
|
||||
}
|
||||
|
||||
getConfigurationSnippets(): Promise<IJSONSchemaSnippet[]> {
|
||||
return this.registry.getConfigurationSnippets();
|
||||
}
|
||||
|
||||
async provideDebuggerVariables(debugType: string): Promise<CommandIdVariables> {
|
||||
// TODO: Support resolution of variables map through Theia extensions?
|
||||
return {};
|
||||
}
|
||||
|
||||
async provideDebugConfigurations(debugType: string, workspaceFolderUri?: string): Promise<DebugConfiguration[]> {
|
||||
return this.registry.provideDebugConfigurations(debugType, workspaceFolderUri);
|
||||
}
|
||||
async provideDynamicDebugConfigurations(): Promise<Record<string, DebugConfiguration[]>> {
|
||||
// TODO: Support dynamic debug configurations through Theia extensions?
|
||||
return {};
|
||||
}
|
||||
fetchDynamicDebugConfiguration(name: string, type: string, folder?: string): Promise<DebugConfiguration | undefined> {
|
||||
// TODO: Support dynamic debug configurations through Theia extensions?
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
async resolveDebugConfiguration(config: DebugConfiguration, workspaceFolderUri?: string): Promise<DebugConfiguration> {
|
||||
return this.registry.resolveDebugConfiguration(config, workspaceFolderUri);
|
||||
}
|
||||
async resolveDebugConfigurationWithSubstitutedVariables(config: DebugConfiguration, workspaceFolderUri?: string): Promise<DebugConfiguration> {
|
||||
return this.registry.resolveDebugConfigurationWithSubstitutedVariables(config, workspaceFolderUri);
|
||||
}
|
||||
|
||||
protected readonly sessions = new Set<string>();
|
||||
async createDebugSession(config: DebugConfiguration, _workspaceFolderUri?: string): Promise<string> {
|
||||
const session = await this.sessionManager.create(config, this.registry);
|
||||
this.sessions.add(session.id);
|
||||
return session.id;
|
||||
}
|
||||
|
||||
async terminateDebugSession(sessionId?: string): Promise<void> {
|
||||
if (sessionId) {
|
||||
await this.doStop(sessionId);
|
||||
} else {
|
||||
const promises: Promise<void>[] = [];
|
||||
const sessions = [...this.sessions];
|
||||
this.sessions.clear();
|
||||
for (const session of sessions) {
|
||||
promises.push((async () => {
|
||||
try {
|
||||
await this.doStop(session);
|
||||
} catch (e) {
|
||||
console.error('terminateDebugSession failed:', e);
|
||||
}
|
||||
})());
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
protected async doStop(sessionId: string): Promise<void> {
|
||||
const debugSession = this.sessionManager.find(sessionId);
|
||||
if (debugSession) {
|
||||
this.sessionManager.remove(sessionId);
|
||||
this.sessions.delete(sessionId);
|
||||
await debugSession.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
126
packages/debug/src/node/stream-debug-adapter.ts
Normal file
126
packages/debug/src/node/stream-debug-adapter.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { ChildProcess } from 'child_process';
|
||||
import * as stream from 'stream';
|
||||
import * as net from 'net';
|
||||
import { DebugAdapter } from '../common/debug-model';
|
||||
|
||||
abstract class StreamDebugAdapter extends DisposableCollection {
|
||||
private messageReceivedEmitter = new Emitter<string>();
|
||||
onMessageReceived: Event<string> = this.messageReceivedEmitter.event;
|
||||
private errorEmitter = new Emitter<Error>();
|
||||
onError: Event<Error> = this.errorEmitter.event;
|
||||
private closeEmitter = new Emitter<void>();
|
||||
onClose: Event<void> = this.closeEmitter.event;
|
||||
|
||||
// these constants are for the message header, see: https://microsoft.github.io/debug-adapter-protocol/overview#header-part
|
||||
private static TWO_CRLF = '\r\n\r\n';
|
||||
private static CONTENT_LENGTH = 'Content-Length';
|
||||
private contentLength: number = -1;
|
||||
private buffer: Buffer = Buffer.alloc(0);
|
||||
|
||||
constructor(private fromAdapter: stream.Readable, private toAdapter: stream.Writable) {
|
||||
super();
|
||||
|
||||
this.fromAdapter.on('data', (data: Buffer) => this.handleData(data));
|
||||
this.fromAdapter.on('close', () => this.handleClosed()); // FIXME pass a proper exit code
|
||||
this.fromAdapter.on('error', error => this.errorEmitter.fire(error));
|
||||
this.toAdapter.on('error', error => this.errorEmitter.fire(error));
|
||||
}
|
||||
|
||||
handleClosed(): void {
|
||||
this.closeEmitter.fire();
|
||||
}
|
||||
|
||||
send(message: string): void {
|
||||
const msg = `${StreamDebugAdapter.CONTENT_LENGTH}: ${Buffer.byteLength(message, 'utf8')}${StreamDebugAdapter.TWO_CRLF}${message}`;
|
||||
|
||||
this.toAdapter.write(msg, 'utf8');
|
||||
}
|
||||
|
||||
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 message = this.buffer.toString('utf8', 0, this.contentLength);
|
||||
this.buffer = this.buffer.slice(this.contentLength);
|
||||
this.contentLength = -1;
|
||||
|
||||
if (message.length > 0) {
|
||||
this.messageReceivedEmitter.fire(message);
|
||||
}
|
||||
continue; // there may be more complete messages to process
|
||||
}
|
||||
} else {
|
||||
let idx = this.buffer.indexOf(StreamDebugAdapter.CONTENT_LENGTH);
|
||||
if (idx > 0) {
|
||||
// log unrecognized output
|
||||
const output = this.buffer.slice(0, idx);
|
||||
console.log(output.toString('utf-8'));
|
||||
|
||||
this.buffer = this.buffer.slice(idx);
|
||||
}
|
||||
|
||||
idx = this.buffer.indexOf(StreamDebugAdapter.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] === StreamDebugAdapter.CONTENT_LENGTH) {
|
||||
this.contentLength = +pair[1];
|
||||
}
|
||||
}
|
||||
this.buffer = this.buffer.slice(idx + StreamDebugAdapter.TWO_CRLF.length);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ProcessDebugAdapter extends StreamDebugAdapter implements DebugAdapter {
|
||||
protected readonly process: ChildProcess;
|
||||
constructor(process: ChildProcess) {
|
||||
super(process.stdout!, process.stdin!);
|
||||
this.process = process;
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.process.kill();
|
||||
this.process.stdin?.end();
|
||||
}
|
||||
}
|
||||
|
||||
export class SocketDebugAdapter extends StreamDebugAdapter implements DebugAdapter {
|
||||
private readonly socket: net.Socket;
|
||||
constructor(socket: net.Socket) {
|
||||
super(socket, socket);
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
stop(): Promise<void> {
|
||||
return new Promise<void>(resolve => {
|
||||
this.socket.end(() => resolve());
|
||||
});
|
||||
}
|
||||
}
|
||||
52
packages/debug/tsconfig.json
Normal file
52
packages/debug/tsconfig.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../console"
|
||||
},
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../editor"
|
||||
},
|
||||
{
|
||||
"path": "../filesystem"
|
||||
},
|
||||
{
|
||||
"path": "../markers"
|
||||
},
|
||||
{
|
||||
"path": "../monaco"
|
||||
},
|
||||
{
|
||||
"path": "../output"
|
||||
},
|
||||
{
|
||||
"path": "../process"
|
||||
},
|
||||
{
|
||||
"path": "../task"
|
||||
},
|
||||
{
|
||||
"path": "../terminal"
|
||||
},
|
||||
{
|
||||
"path": "../test"
|
||||
},
|
||||
{
|
||||
"path": "../variable-resolver"
|
||||
},
|
||||
{
|
||||
"path": "../workspace"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user