deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View 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 };
}

View 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
};
}
}

View File

@@ -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) };
}
}

View File

@@ -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();
}
}

View 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';
}
}

View 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);
}

View 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'>;

View 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'
};
}

View 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[]
}
}

View 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[]
}
}

View 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

View 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();
});

View 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);
});

View 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);
}
}

View 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);
}
}

View 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,
};

View File

@@ -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)');
});
});

View File

@@ -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;
}
}

View 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
});
}
}

View 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');
}
}
}

View 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;
}
}
}

View 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');
}
}

File diff suppressed because it is too large Load Diff

View 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);
}
}

View 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[];
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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));
}

View File

@@ -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;
}
}

View File

@@ -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[];
}

View File

@@ -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;
}
}

View 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'>;
}

View 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
};
}

View 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();
}
}
}

View 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;

View 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);
}
}

View 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 };
}
}

View 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));
}
}
}

View 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());
}
}

View File

@@ -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;
}
}

View 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);
}
}
}

View 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')]
};
}
}

View 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);
}
}
}
}

View File

@@ -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')]
};
}
}

View 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;
}
}

View 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));
}
}

View 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;
}
}

View 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 '';
}
}
}

View 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;
}

View 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;
}

View 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
}
}

View 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;
}
}

View 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;
}
}

View 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 : '';
}
}

View 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
});
}

View 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 * 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 });
}
}
}

View 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);
}
}

View File

@@ -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];
}
}
}

View 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;
}
}

View 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;
}
}
}
}

View 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;
}
}

View 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;
}
}

View 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]();
}
}

View 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);
}
}

View 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);
}

View 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;
}

View 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]();
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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';

View 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;
}
}

View 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
}

View 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>;
}

View 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);
}

View 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();
}
}

View 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\+\-\.]+:/;

View 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();
}
}

View 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' });
}

View 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
);
}
}

View 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();
}
}

View 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);
});

View 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();
}
}
}

View 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());
});
}
}