diff --git a/packages/core/src/common/objects.ts b/packages/core/src/common/objects.ts index 54e650adee82d..9375f36be16af 100644 --- a/packages/core/src/common/objects.ts +++ b/packages/core/src/common/objects.ts @@ -14,6 +14,27 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +export function deepClone(obj: T): T { + if (!obj || typeof obj !== 'object') { + return obj; + } + if (obj instanceof RegExp) { + return obj; + } + // tslint:disable-next-line:no-any + const result: any = Array.isArray(obj) ? [] : {}; + Object.keys(obj).forEach((key: string) => { + // tslint:disable-next-line:no-any + const prop = (obj)[key]; + if (prop && typeof prop === 'object') { + result[key] = deepClone(prop); + } else { + result[key] = prop; + } + }); + return result; +} + export function deepFreeze(obj: T): T { if (!obj || typeof obj !== 'object') { return obj; diff --git a/packages/core/src/common/resource.ts b/packages/core/src/common/resource.ts index 7fba8a9408c09..e2506cb27b431 100644 --- a/packages/core/src/common/resource.ts +++ b/packages/core/src/common/resource.ts @@ -18,7 +18,7 @@ import { injectable, inject, named } from 'inversify'; import { TextDocumentContentChangeEvent } from 'vscode-languageserver-types'; import URI from '../common/uri'; import { ContributionProvider } from './contribution-provider'; -import { Event } from './event'; +import { Event, Emitter } from './event'; import { Disposable } from './disposable'; import { MaybePromise } from './types'; import { CancellationToken } from './cancellation'; @@ -117,26 +117,52 @@ export class DefaultResourceProvider { } +export class MutableResource implements Resource { + private contents: string; + + constructor(readonly uri: URI, contents: string, readonly dispose: () => void) { + this.contents = contents; + } + + async readContents(): Promise { + return this.contents; + } + + async saveContents(contents: string): Promise { + this.contents = contents; + this.fireDidChangeContents(); + } + + protected readonly onDidChangeContentsEmitter = new Emitter(); + onDidChangeContents = this.onDidChangeContentsEmitter.event; + protected fireDidChangeContents(): void { + this.onDidChangeContentsEmitter.fire(undefined); + } +} + @injectable() export class InMemoryResources implements ResourceResolver { - private resources = new Map(); + private resources = new Map(); add(uri: URI, contents: string): Resource { - const stringUri = uri.toString(); - if (this.resources.has(stringUri)) { - throw new Error(`Cannot add already existing in-memory resource '${stringUri}'`); + const resourceUri = uri.toString(); + if (this.resources.has(resourceUri)) { + throw new Error(`Cannot add already existing in-memory resource '${resourceUri}'`); } - const resource: Resource = { - uri, - async readContents(): Promise { - return contents; - }, - dispose: () => { - this.resources.delete(stringUri); - } - }; - this.resources.set(stringUri, resource); + + const resource = new MutableResource(uri, contents, () => this.resources.delete(resourceUri)); + this.resources.set(resourceUri, resource); + return resource; + } + + update(uri: URI, contents: string): Resource { + const resourceUri = uri.toString(); + const resource = this.resources.get(resourceUri); + if (!resource) { + throw new Error(`Cannot update non-existed in-memory resource '${resourceUri}'`); + } + resource.saveContents(contents); return resource; } @@ -146,5 +172,4 @@ export class InMemoryResources implements ResourceResolver { } return this.resources.get(uri.toString())!; } - } diff --git a/packages/debug-nodejs/src/node/debug-nodejs-backend-module.ts b/packages/debug-nodejs/src/node/debug-nodejs-backend-module.ts index b3c0d2c06916d..0d97442754b9e 100644 --- a/packages/debug-nodejs/src/node/debug-nodejs-backend-module.ts +++ b/packages/debug-nodejs/src/node/debug-nodejs-backend-module.ts @@ -16,7 +16,7 @@ import { ContainerModule } from 'inversify'; import { NodeDebugAdapterContribution, Node2DebugAdapterContribution } from './node-debug-adapter-contribution'; -import { DebugAdapterContribution } from '@theia/debug/lib/node/debug-model'; +import { DebugAdapterContribution } from '@theia/debug/lib/common/debug-model'; export default new ContainerModule(bind => { bind(DebugAdapterContribution).to(NodeDebugAdapterContribution).inSingletonScope(); diff --git a/packages/debug-nodejs/src/node/node-debug-adapter-contribution.ts b/packages/debug-nodejs/src/node/node-debug-adapter-contribution.ts index e7334958a4b86..756f070bf23ee 100644 --- a/packages/debug-nodejs/src/node/node-debug-adapter-contribution.ts +++ b/packages/debug-nodejs/src/node/node-debug-adapter-contribution.ts @@ -27,7 +27,6 @@ export const LEGACY_PORT_DEFAULT = 5858; @injectable() export class NodeDebugAdapterContribution extends AbstractVSCodeDebugAdapterContribution { - constructor() { super( 'node', @@ -94,12 +93,10 @@ export class NodeDebugAdapterContribution extends AbstractVSCodeDebugAdapterCont @injectable() export class Node2DebugAdapterContribution extends AbstractVSCodeDebugAdapterContribution { - constructor() { super( 'node2', path.join(__dirname, '../../download/node-debug2/extension') ); } - } diff --git a/packages/debug/src/browser/breakpoint/breakpoint-manager.ts b/packages/debug/src/browser/breakpoint/breakpoint-manager.ts index 790cf00cdc49d..c5daf762c2515 100644 --- a/packages/debug/src/browser/breakpoint/breakpoint-manager.ts +++ b/packages/debug/src/browser/breakpoint/breakpoint-manager.ts @@ -40,7 +40,7 @@ export class BreakpointManager extends MarkerManager { return marker && marker.data; } - getBreakpoints(uri: URI): SourceBreakpoint[] { + getBreakpoints(uri?: URI): SourceBreakpoint[] { return this.findMarkers({ uri }).map(marker => marker.data); } @@ -64,6 +64,12 @@ export class BreakpointManager extends MarkerManager { } } + deleteBreakpoint(uri: URI, line: number, column?: number): void { + const breakpoints = this.getBreakpoints(uri); + const newBreakpoints = breakpoints.filter(({ raw }) => raw.line !== line); + this.setBreakpoints(uri, newBreakpoints); + } + enableAllBreakpoints(enabled: boolean): void { for (const uriString of this.getUris()) { let didChange = false; diff --git a/packages/debug/src/browser/console/debug-console-session.ts b/packages/debug/src/browser/console/debug-console-session.ts index 167f3e36f5d5c..0ff4ec9d5d312 100644 --- a/packages/debug/src/browser/console/debug-console-session.ts +++ b/packages/debug/src/browser/console/debug-console-session.ts @@ -33,6 +33,9 @@ export class DebugConsoleSession extends ConsoleSession { readonly id = 'debug'; protected items: ConsoleItem[] = []; + // content buffer for [append](#append) method + protected uncompletedItemContent: string | undefined; + @inject(DebugSessionManager) protected readonly manager: DebugSessionManager; @@ -125,6 +128,28 @@ export class DebugConsoleSession extends ConsoleSession { 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, MessageType.Info)); + this.fireDidChange(); + } + + appendLine(value: string): void { + this.items.push(new AnsiConsoleItem(value, MessageType.Info)); + this.fireDidChange(); + } + protected async logOutput(session: DebugSession, event: DebugProtocol.OutputEvent): Promise { const body = event.body; const { category, variablesReference } = body; diff --git a/packages/debug/src/browser/debug-configuration-manager.ts b/packages/debug/src/browser/debug-configuration-manager.ts index d8b9cabbe0e8f..b6109a1f280fe 100644 --- a/packages/debug/src/browser/debug-configuration-manager.ts +++ b/packages/debug/src/browser/debug-configuration-manager.ts @@ -28,10 +28,10 @@ import { EditorManager, EditorWidget } from '@theia/editor/lib/browser'; import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { QuickPickService, StorageService } from '@theia/core/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; -import { DebugService } from '../common/debug-service'; import { DebugConfiguration } from '../common/debug-configuration'; import { DebugConfigurationModel } from './debug-configuration-model'; import { DebugSessionOptions } from './debug-session-options'; +import { DebugService } from '../common/debug-service'; @injectable() export class DebugConfigurationManager { diff --git a/packages/debug/src/browser/debug-frontend-application-contribution.ts b/packages/debug/src/browser/debug-frontend-application-contribution.ts index 45334e137f418..aadd428506c55 100644 --- a/packages/debug/src/browser/debug-frontend-application-contribution.ts +++ b/packages/debug/src/browser/debug-frontend-application-contribution.ts @@ -16,14 +16,10 @@ import { AbstractViewContribution, ApplicationShell, KeybindingRegistry } from '@theia/core/lib/browser'; import { injectable, inject } from 'inversify'; -import { JsonSchemaStore } from '@theia/core/lib/browser/json-schema-store'; import { ThemeService } from '@theia/core/lib/browser/theming'; -import { InMemoryResources, MenuModelRegistry, CommandRegistry, MAIN_MENU_BAR, Command } from '@theia/core/lib/common'; -import { DebugService } from '../common/debug-service'; +import { MenuModelRegistry, CommandRegistry, MAIN_MENU_BAR, Command } from '@theia/core/lib/common'; import { DebugViewLocation } from '../common/debug-configuration'; -import { IJSONSchema } from '@theia/core/lib/common/json-schema'; import { EditorKeybindingContexts } from '@theia/editor/lib/browser'; -import URI from '@theia/core/lib/common/uri'; import { DebugSessionManager } from './debug-session-manager'; import { DebugWidget } from './view/debug-widget'; import { BreakpointManager } from './breakpoint/breakpoint-manager'; @@ -42,6 +38,8 @@ import { DebugKeybindingContexts } from './debug-keybinding-contexts'; import { DebugEditorModel } from './editor/debug-editor-model'; import { DebugEditorService } from './editor/debug-editor-service'; import { DebugConsoleContribution } from './console/debug-console-contribution'; +import { DebugService } from '../common/debug-service'; +import { DebugSchemaUpdater } from './debug-schema-updater'; export namespace DebugMenus { export const DEBUG = [...MAIN_MENU_BAR, '6_debug']; @@ -267,9 +265,8 @@ ThemeService.get().onThemeChange(() => updateTheme()); @injectable() export class DebugFrontendApplicationContribution extends AbstractViewContribution { - @inject(JsonSchemaStore) protected readonly jsonSchemaStore: JsonSchemaStore; - @inject(InMemoryResources) protected readonly inmemoryResources: InMemoryResources; - @inject(DebugService) protected readonly debugService: DebugService; + @inject(DebugService) + protected readonly debug: DebugService; @inject(DebugSessionManager) protected readonly manager: DebugSessionManager; @@ -292,6 +289,9 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi @inject(DebugConsoleContribution) protected readonly console: DebugConsoleContribution; + @inject(DebugSchemaUpdater) + protected readonly schemaUpdater: DebugSchemaUpdater; + constructor() { super({ widgetId: DebugWidget.ID, @@ -336,43 +336,8 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi this.openSession(session); } }); - this.debugService.debugTypes().then(async types => { - const launchSchemaUrl = new URI('vscode://debug/launch.json'); - const attributePromises = types.map(type => this.debugService.getSchemaAttributes(type)); - const schema: IJSONSchema = { - ...launchSchema - }; - const items = (launchSchema!.properties!['configurations'].items); - for (const attributes of await Promise.all(attributePromises)) { - for (const attribute of attributes) { - attribute.properties = { - 'debugViewLocation': { - enum: ['default', 'left', 'right', 'bottom'], - default: 'default', - description: 'Controls the location of the debug view.' - }, - 'openDebug': { - enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart', 'openOnDebugBreak'], - default: 'openOnSessionStart', - description: 'Controls when the debug view should open.' - }, - 'internalConsoleOptions': { - enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart'], - default: 'openOnFirstSessionStart', - description: 'Controls when the internal debug console should open.' - }, - ...attribute.properties - }; - items.oneOf!.push(attribute); - } - } - items.defaultSnippets!.push(...await this.debugService.getConfigurationSnippets()); - this.inmemoryResources.add(launchSchemaUrl, JSON.stringify(schema)); - this.jsonSchemaStore.registerSchema({ - fileMatch: ['launch.json'], - url: launchSchemaUrl.toString() - }); - }); + + this.schemaUpdater.update(); this.configurations.load(); await this.breakpointManager.load(); } @@ -843,73 +808,3 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi } } - -// debug general schema -const defaultCompound = { name: 'Compound', configurations: [] }; - -const launchSchemaId = 'vscode://schemas/launch'; -const launchSchema: IJSONSchema = { - id: launchSchemaId, - type: 'object', - title: 'Launch', - required: [], - default: { version: '0.2.0', configurations: [], compounds: [] }, - properties: { - version: { - type: 'string', - description: 'Version of this file format.', - default: '0.2.0' - }, - configurations: { - type: 'array', - description: 'List of configurations. Add new configurations or edit existing ones by using IntelliSense.', - items: { - defaultSnippets: [], - 'type': 'object', - oneOf: [] - } - }, - compounds: { - type: 'array', - description: 'List of compounds. Each compound references multiple configurations which will get launched together.', - items: { - type: 'object', - required: ['name', 'configurations'], - properties: { - name: { - type: 'string', - description: 'Name of compound. Appears in the launch configuration drop down menu.' - }, - configurations: { - type: 'array', - default: [], - items: { - oneOf: [{ - enum: [], - description: 'Please use unique configuration names.' - }, { - type: 'object', - required: ['name'], - properties: { - name: { - enum: [], - description: 'Name of compound. Appears in the launch configuration drop down menu.' - }, - folder: { - enum: [], - description: 'Name of folder in which the compound is located.' - } - } - }] - }, - description: 'Names of configurations that will be started as part of this compound.' - } - }, - default: defaultCompound - }, - default: [ - defaultCompound - ] - } - } -}; diff --git a/packages/debug/src/browser/debug-frontend-module.ts b/packages/debug/src/browser/debug-frontend-module.ts index 91bce20b863d6..f1cb498838010 100644 --- a/packages/debug/src/browser/debug-frontend-module.ts +++ b/packages/debug/src/browser/debug-frontend-module.ts @@ -23,7 +23,13 @@ import { DebugPath, DebugService } from '../common/debug-service'; import { WidgetFactory, WebSocketConnectionProvider, FrontendApplicationContribution, bindViewContribution, KeybindingContext } from '@theia/core/lib/browser'; import { DebugSessionManager } from './debug-session-manager'; import { DebugResourceResolver } from './debug-resource'; -import { DebugSessionContribution, DebugSessionFactory, DefaultDebugSessionFactory } from './debug-session-contribution'; +import { + DebugSessionContribution, + DebugSessionFactory, + DefaultDebugSessionFactory, + DebugSessionContributionRegistry, + DebugSessionContributionRegistryImpl +} from './debug-session-contribution'; import { bindContributionProvider, ResourceResolver } from '@theia/core'; import { DebugFrontendApplicationContribution } from './debug-frontend-application-contribution'; import { DebugConsoleContribution } from './console/debug-console-contribution'; @@ -35,6 +41,7 @@ import { InDebugModeContext } from './debug-keybinding-contexts'; import { DebugEditorModelFactory, DebugEditorModel } from './editor/debug-editor-model'; import './debug-monaco-contribution'; import { bindDebugPreferences } from './debug-preferences'; +import { DebugSchemaUpdater } from './debug-schema-updater'; export default new ContainerModule((bind: interfaces.Bind) => { bindContributionProvider(bind, DebugSessionContribution); @@ -56,6 +63,7 @@ export default new ContainerModule((bind: interfaces.Bind) => { })).inSingletonScope(); DebugConsoleContribution.bindContribution(bind); + bind(DebugSchemaUpdater).toSelf().inSingletonScope(); bind(DebugConfigurationManager).toSelf().inSingletonScope(); bind(DebugService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, DebugPath)).inSingletonScope(); @@ -66,5 +74,8 @@ export default new ContainerModule((bind: interfaces.Bind) => { bindViewContribution(bind, DebugFrontendApplicationContribution); bind(FrontendApplicationContribution).toService(DebugFrontendApplicationContribution); + bind(DebugSessionContributionRegistryImpl).toSelf().inSingletonScope(); + bind(DebugSessionContributionRegistry).toService(DebugSessionContributionRegistryImpl); + bindDebugPreferences(bind); }); diff --git a/packages/debug/src/browser/debug-schema-updater.ts b/packages/debug/src/browser/debug-schema-updater.ts new file mode 100644 index 0000000000000..7b18d64721af1 --- /dev/null +++ b/packages/debug/src/browser/debug-schema-updater.ts @@ -0,0 +1,144 @@ +/******************************************************************************** + * Copyright (C) 2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { JsonSchemaStore } from '@theia/core/lib/browser/json-schema-store'; +import { InMemoryResources, deepClone } 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'; + +@injectable() +export class DebugSchemaUpdater { + + @inject(JsonSchemaStore) protected readonly jsonSchemaStore: JsonSchemaStore; + @inject(InMemoryResources) protected readonly inmemoryResources: InMemoryResources; + @inject(DebugService) protected readonly debug: DebugService; + + async update(): Promise { + const types = await this.debug.debugTypes(); + const launchSchemaUrl = new URI('vscode://debug/launch.json'); + const schema = { ...deepClone(launchSchema) }; + const items = (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) { + attribute.properties = { + 'debugViewLocation': { + enum: ['default', 'left', 'right', 'bottom'], + default: 'default', + description: 'Controls the location of the debug view.' + }, + 'openDebug': { + enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart', 'openOnDebugBreak'], + default: 'openOnSessionStart', + description: 'Controls when the debug view should open.' + }, + 'internalConsoleOptions': { + enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart'], + default: 'openOnFirstSessionStart', + description: 'Controls when the internal debug console should open.' + }, + ...attribute.properties + }; + items.oneOf!.push(attribute); + } + } + items.defaultSnippets!.push(...await this.debug.getConfigurationSnippets()); + + const contents = JSON.stringify(schema); + try { + await this.inmemoryResources.update(launchSchemaUrl, contents); + } catch (e) { + this.inmemoryResources.add(launchSchemaUrl, contents); + this.jsonSchemaStore.registerSchema({ + fileMatch: ['launch.json'], + url: launchSchemaUrl.toString() + }); + } + } +} + +// debug general schema +const defaultCompound = { name: 'Compound', configurations: [] }; + +const launchSchemaId = 'vscode://schemas/launch'; +const launchSchema: IJSONSchema = { + id: launchSchemaId, + type: 'object', + title: 'Launch', + required: [], + default: { version: '0.2.0', configurations: [], compounds: [] }, + properties: { + version: { + type: 'string', + description: 'Version of this file format.', + default: '0.2.0' + }, + configurations: { + type: 'array', + description: 'List of configurations. Add new configurations or edit existing ones by using IntelliSense.', + items: { + defaultSnippets: [], + 'type': 'object', + oneOf: [] + } + }, + compounds: { + type: 'array', + description: 'List of compounds. Each compound references multiple configurations which will get launched together.', + items: { + type: 'object', + required: ['name', 'configurations'], + properties: { + name: { + type: 'string', + description: 'Name of compound. Appears in the launch configuration drop down menu.' + }, + configurations: { + type: 'array', + default: [], + items: { + oneOf: [{ + enum: [], + description: 'Please use unique configuration names.' + }, { + type: 'object', + required: ['name'], + properties: { + name: { + enum: [], + description: 'Name of compound. Appears in the launch configuration drop down menu.' + }, + folder: { + enum: [], + description: 'Name of folder in which the compound is located.' + } + } + }] + }, + description: 'Names of configurations that will be started as part of this compound.' + } + }, + default: defaultCompound + }, + default: [ + defaultCompound + ] + } + } +}; diff --git a/packages/debug/src/browser/debug-session-connection.ts b/packages/debug/src/browser/debug-session-connection.ts index bf882e987a014..001325011d556 100644 --- a/packages/debug/src/browser/debug-session-connection.ts +++ b/packages/debug/src/browser/debug-session-connection.ts @@ -16,13 +16,11 @@ // tslint:disable:no-any -import { WebSocketConnectionProvider } from '@theia/core/lib/browser'; import { DebugProtocol } from 'vscode-debugprotocol'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { Event, Emitter, DisposableCollection, Disposable } from '@theia/core'; -import { WebSocketChannel } from '@theia/core/lib/common/messaging/web-socket-channel'; -import { DebugAdapterPath } from '../common/debug-service'; import { OutputChannel } from '@theia/output/lib/common/output-channel'; +import { IWebSocket } from 'vscode-ws-jsonrpc/lib/socket/socket'; export interface DebugExitEvent { code?: number @@ -102,7 +100,7 @@ export class DebugSessionConnection implements Disposable { private sequence = 1; protected readonly pendingRequests = new Map void>(); - protected readonly connection: Promise; + protected readonly connection: Promise; protected readonly requestHandlers = new Map(); @@ -117,7 +115,7 @@ export class DebugSessionConnection implements Disposable { constructor( readonly sessionId: string, - protected readonly connectionProvider: WebSocketConnectionProvider, + protected readonly connectionFactory: (sessionId: string) => Promise, protected readonly traceOutputChannel: OutputChannel | undefined ) { this.connection = this.createConnection(); @@ -136,22 +134,18 @@ export class DebugSessionConnection implements Disposable { this.toDispose.dispose(); } - protected createConnection(): Promise { - return new Promise(resolve => - this.connectionProvider.openChannel(`${DebugAdapterPath}/${this.sessionId}`, channel => { - if (this.disposed) { - channel.close(); - } else { - const closeChannel = this.toDispose.push(Disposable.create(() => channel.close())); - channel.onClose((code, reason) => { - closeChannel.dispose(); - this.fire('exited', { code, reason }); - }); - channel.onMessage(data => this.handleMessage(data)); - resolve(channel); - } - }, { reconnecting: false }) - ); + protected async createConnection(): Promise { + if (this.disposed) { + throw new Error('Connection has been already disposed.'); + } else { + const connection = await this.connectionFactory(this.sessionId); + connection.onClose((code, reason) => { + connection.dispose(); + this.fire('exited', { code, reason }); + }); + connection.onMessage(data => this.handleMessage(data)); + return connection; + } } protected allThreadsContinued = true; diff --git a/packages/debug/src/browser/debug-session-contribution.ts b/packages/debug/src/browser/debug-session-contribution.ts index 9ddde5c04ef3b..9d7d0bbc88c35 100644 --- a/packages/debug/src/browser/debug-session-contribution.ts +++ b/packages/debug/src/browser/debug-session-contribution.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject } from 'inversify'; +import { injectable, inject, named, postConstruct } from 'inversify'; import { MessageClient } from '@theia/core/lib/common'; import { LabelProvider } from '@theia/core/lib/browser'; import { EditorManager } from '@theia/editor/lib/browser'; @@ -25,6 +25,10 @@ import { BreakpointManager } from './breakpoint/breakpoint-manager'; import { DebugSessionOptions } from './debug-session-options'; import { OutputChannelManager, OutputChannel } from '@theia/output/lib/common/output-channel'; import { DebugPreferences } from './debug-preferences'; +import { DebugSessionConnection } from './debug-session-connection'; +import { IWebSocket } from 'vscode-ws-jsonrpc/lib/socket/socket'; +import { DebugAdapterPath } from '../common/debug-service'; +import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; /** * DebugSessionContribution symbol for DI. @@ -46,10 +50,41 @@ export interface DebugSessionContribution { 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(); + + @inject(ContributionProvider) @named(DebugSessionContribution) + protected readonly contributions: ContributionProvider; + + @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. */ @@ -62,47 +97,45 @@ export class DefaultDebugSessionFactory implements DebugSessionFactory { @inject(WebSocketConnectionProvider) protected readonly connectionProvider: WebSocketConnectionProvider; - @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; - protected traceOutputChannel: OutputChannel | undefined; - get(sessionId: string, options: DebugSessionOptions): DebugSession { - let traceOutputChannel: OutputChannel | undefined; - - if (this.debugPreferences['debug.trace']) { - traceOutputChannel = this.outputChannelManager.getChannel('Debug adapters'); - } + const connection = new DebugSessionConnection( + sessionId, + () => new Promise(resolve => + this.connectionProvider.openChannel(`${DebugAdapterPath}/${sessionId}`, channel => { + resolve(channel); + }, { reconnecting: false }) + ), + this.getTraceOutputChannel()); return new DebugSession( sessionId, options, - this.connectionProvider, + connection, this.terminalService, this.editorManager, this.breakpoints, this.labelProvider, - this.messages, - traceOutputChannel, - ); + this.messages); + } + + protected getTraceOutputChannel(): OutputChannel | undefined { + if (this.debugPreferences['debug.trace']) { + return this.outputChannelManager.getChannel('Debug adapters'); + } } } diff --git a/packages/debug/src/browser/debug-session-manager.ts b/packages/debug/src/browser/debug-session-manager.ts index f5e401868717d..9efe01e1cfd89 100644 --- a/packages/debug/src/browser/debug-session-manager.ts +++ b/packages/debug/src/browser/debug-session-manager.ts @@ -16,13 +16,13 @@ // tslint:disable:no-any -import { injectable, inject, named, postConstruct } from 'inversify'; -import { Emitter, Event, ContributionProvider, DisposableCollection, MessageService } from '@theia/core'; +import { injectable, inject, postConstruct } from 'inversify'; +import { Emitter, Event, DisposableCollection, MessageService } from '@theia/core'; import { LabelProvider } from '@theia/core/lib/browser'; import { EditorManager } from '@theia/editor/lib/browser'; -import { DebugService, DebugError } from '../common/debug-service'; +import { DebugError, DebugService } from '../common/debug-service'; import { DebugState, DebugSession } from './debug-session'; -import { DebugSessionContribution, DebugSessionFactory } from './debug-session-contribution'; +import { DebugSessionFactory, DebugSessionContributionRegistry } from './debug-session-contribution'; import { DebugThread } from './model/debug-thread'; import { DebugStackFrame } from './model/debug-stack-frame'; import { DebugBreakpoint } from './model/debug-breakpoint'; @@ -50,7 +50,6 @@ export interface DebugSessionCustomEvent { @injectable() export class DebugSessionManager { protected readonly _sessions = new Map(); - protected readonly contribs = new Map(); protected readonly onDidCreateDebugSessionEmitter = new Emitter(); readonly onDidCreateDebugSession: Event = this.onDidCreateDebugSessionEmitter.event; @@ -85,11 +84,8 @@ export class DebugSessionManager { @inject(DebugSessionFactory) protected readonly debugSessionFactory: DebugSessionFactory; - @inject(ContributionProvider) @named(DebugSessionContribution) - protected readonly contributions: ContributionProvider; - @inject(DebugService) - protected readonly debugService: DebugService; + protected readonly debug: DebugService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @@ -106,18 +102,18 @@ export class DebugSessionManager { @inject(MessageService) protected readonly messageService: MessageService; + @inject(DebugSessionContributionRegistry) + protected readonly sessionContributionRegistry: DebugSessionContributionRegistry; + @postConstruct() protected init(): void { - for (const contrib of this.contributions.getContributions()) { - this.contribs.set(contrib.debugType, contrib); - } this.breakpoints.onDidChangeMarkers(uri => this.fireDidChangeBreakpoints({ uri })); } async start(options: DebugSessionOptions): Promise { try { const resolved = await this.resolveConfiguration(options); - const sessionId = await this.debugService.create(resolved.configuration); + const sessionId = await this.debug.createDebugSession(resolved.configuration); return this.doStart(sessionId, resolved); } catch (e) { if (DebugError.NotFound.is(e)) { @@ -132,7 +128,7 @@ export class DebugSessionManager { return options; } const { workspaceFolderUri } = options; - const resolvedConfiguration = await this.debugService.resolveDebugConfiguration(options.configuration, workspaceFolderUri); + const resolvedConfiguration = await this.debug.resolveDebugConfiguration(options.configuration, workspaceFolderUri); const configuration = await this.variableResolver.resolve(resolvedConfiguration); const key = configuration.name + workspaceFolderUri; const id = this.configurationIds.has(key) ? this.configurationIds.get(key)! + 1 : 0; @@ -144,7 +140,7 @@ export class DebugSessionManager { }; } protected async doStart(sessionId: string, options: DebugSessionOptions): Promise { - const contrib = this.contribs.get(options.configuration.type); + const contrib = this.sessionContributionRegistry.get(options.configuration.type); const sessionFactory = contrib ? contrib.debugSessionFactory() : this.debugSessionFactory; const session = sessionFactory.get(sessionId, options); this._sessions.set(sessionId, session); @@ -296,7 +292,7 @@ export class DebugSessionManager { } private doDestroy(session: DebugSession): void { - this.debugService.stop(session.id); + this.debug.terminateDebugSession(session.id); session.dispose(); this.remove(session.id); @@ -322,4 +318,17 @@ export class DebugSessionManager { return origin && new DebugBreakpoint(origin, this.labelProvider, this.breakpoints, this.editorManager); } + addBreakpoints(breakpoints: DebugBreakpoint[]): void { + breakpoints.forEach(breakpoint => { + this.breakpoints.addBreakpoint(breakpoint.uri, breakpoint.line, breakpoint.column); + this.fireDidChangeBreakpoints({ uri: breakpoint.uri }); + }); + } + + deleteBreakpoints(breakpoints: DebugBreakpoint[]): void { + breakpoints.forEach(breakpoint => { + this.breakpoints.deleteBreakpoint(breakpoint.uri, breakpoint.line, breakpoint.column); + this.fireDidChangeBreakpoints({ uri: breakpoint.uri }); + }); + } } diff --git a/packages/debug/src/browser/debug-session.tsx b/packages/debug/src/browser/debug-session.tsx index dc97d9ce79fdb..bac3d931f0efb 100644 --- a/packages/debug/src/browser/debug-session.tsx +++ b/packages/debug/src/browser/debug-session.tsx @@ -17,7 +17,7 @@ // tslint:disable:no-any import * as React from 'react'; -import { WebSocketConnectionProvider, LabelProvider } from '@theia/core/lib/browser'; +import { LabelProvider } from '@theia/core/lib/browser'; import { DebugProtocol } from 'vscode-debugprotocol'; import { Emitter, Event, DisposableCollection, Disposable, MessageClient, MessageType, Mutable } from '@theia/core/lib/common'; import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; @@ -34,7 +34,6 @@ import URI from '@theia/core/lib/common/uri'; import { BreakpointManager } from './breakpoint/breakpoint-manager'; import { DebugSessionOptions, InternalDebugSessionOptions } from './debug-session-options'; import { DebugConfiguration } from '../common/debug-common'; -import { OutputChannel } from '@theia/output/lib/common/output-channel'; export enum DebugState { Inactive, @@ -46,8 +45,6 @@ export enum DebugState { // FIXME: make injectable to allow easily inject services export class DebugSession implements CompositeTreeElement { - protected readonly connection: DebugSessionConnection; - protected readonly onDidChangeEmitter = new Emitter(); readonly onDidChange: Event = this.onDidChangeEmitter.event; protected fireDidChange(): void { @@ -65,15 +62,12 @@ export class DebugSession implements CompositeTreeElement { constructor( readonly id: string, readonly options: DebugSessionOptions, - connectionProvider: WebSocketConnectionProvider, + protected readonly connection: DebugSessionConnection, protected readonly terminalServer: TerminalService, protected readonly editorManager: EditorManager, protected readonly breakpoints: BreakpointManager, protected readonly labelProvider: LabelProvider, - protected readonly messages: MessageClient, - protected readonly traceOutputChannel: OutputChannel | undefined, - ) { - this.connection = new DebugSessionConnection(id, connectionProvider, traceOutputChannel); + protected readonly messages: MessageClient) { this.connection.onRequest('runInTerminal', (request: DebugProtocol.RunInTerminalRequest) => this.runInTerminal(request)); this.toDispose.pushAll([ this.onDidChangeEmitter, diff --git a/packages/debug/src/common/debug-configuration.ts b/packages/debug/src/common/debug-configuration.ts index fe2b975c56a05..615716ce898f0 100644 --- a/packages/debug/src/common/debug-configuration.ts +++ b/packages/debug/src/common/debug-configuration.ts @@ -37,6 +37,11 @@ export interface DebugConfiguration { */ [key: string]: any; + /** + * The request type of the debug adapter session. + */ + request: string; + /** * If noDebug is true the launch request should launch the program without enabling debugging. */ @@ -60,6 +65,6 @@ export interface DebugConfiguration { } export namespace DebugConfiguration { export function is(arg: DebugConfiguration | any): arg is DebugConfiguration { - return !!arg && 'type' in arg && 'name' in arg; + return !!arg && 'type' in arg && 'name' in arg && 'request' in arg; } } diff --git a/packages/debug/src/node/debug-model.ts b/packages/debug/src/common/debug-model.ts similarity index 96% rename from packages/debug/src/node/debug-model.ts rename to packages/debug/src/common/debug-model.ts index 123da759cdea7..31bd1829461ad 100644 --- a/packages/debug/src/node/debug-model.ts +++ b/packages/debug/src/common/debug-model.ts @@ -23,10 +23,11 @@ // Some entities copied and modified from https://github.com/Microsoft/vscode/blob/master/src/vs/workbench/parts/debug/common/debug.ts import * as stream from 'stream'; -import { Disposable, MaybePromise } from '@theia/core'; import { WebSocketChannel } from '@theia/core/lib/common/messaging/web-socket-channel'; -import { DebugConfiguration } from '../common/debug-configuration'; +import { DebugConfiguration } from './debug-configuration'; import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; +import { Disposable } from '@theia/core/lib/common/disposable'; +import { MaybePromise } from '@theia/core/lib/common/types'; // FXIME: break down this file to debug adapter and debug adapter contribution (see Theia file naming conventions) @@ -151,7 +152,7 @@ export interface DebugAdapterContribution { * @param config The resolved [debug configuration](#DebugConfiguration). * @returns The [debug adapter executable](#DebugAdapterExecutable). */ - provideDebugAdapterExecutable?(config: DebugConfiguration): MaybePromise; + provideDebugAdapterExecutable?(config: DebugConfiguration): MaybePromise; /** * Provides initial [debug configuration](#DebugConfiguration). diff --git a/packages/debug/src/common/debug-service.ts b/packages/debug/src/common/debug-service.ts index 3c18e0e5caf1f..705276c71c1fb 100644 --- a/packages/debug/src/common/debug-service.ts +++ b/packages/debug/src/common/debug-service.ts @@ -83,17 +83,12 @@ export interface DebugService extends Disposable { * @param config The resolved [debug configuration](#DebugConfiguration). * @returns The identifier of the created [debug adapter session](#DebugAdapterSession). */ - create(config: DebugConfiguration): Promise; + createDebugSession(config: DebugConfiguration): Promise; /** * Stop a running session for the given session id. */ - stop(sessionId: string): Promise; - - /** - * Stop all running sessions. - */ - stop(): Promise; + terminateDebugSession(sessionId: string): Promise; } /** diff --git a/packages/debug/src/node/debug-adapter-contribution-registry.ts b/packages/debug/src/node/debug-adapter-contribution-registry.ts index b7616c870727a..bd9a30538e667 100644 --- a/packages/debug/src/node/debug-adapter-contribution-registry.ts +++ b/packages/debug/src/node/debug-adapter-contribution-registry.ts @@ -19,7 +19,7 @@ import { ContributionProvider } from '@theia/core'; import { DebugConfiguration } from '../common/debug-configuration'; import { DebuggerDescription, DebugError } from '../common/debug-service'; -import { DebugAdapterContribution, DebugAdapterExecutable, DebugAdapterSessionFactory } from './debug-model'; +import { DebugAdapterContribution, DebugAdapterExecutable, DebugAdapterSessionFactory } from '../common/debug-model'; import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; /** diff --git a/packages/debug/src/node/debug-adapter-factory.ts b/packages/debug/src/node/debug-adapter-factory.ts new file mode 100644 index 0000000000000..fe9cd04c337a2 --- /dev/null +++ b/packages/debug/src/node/debug-adapter-factory.ts @@ -0,0 +1,103 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Some entities copied and modified from https://github.com/Microsoft/vscode-debugadapter-node/blob/master/adapter/src/protocol.ts + +import * as net from 'net'; +import { injectable, inject } from 'inversify'; +import { + RawProcessFactory, + ProcessManager, + RawProcess, + RawForkOptions, + RawProcessOptions +} from '@theia/process/lib/node'; +import { + DebugAdapterExecutable, + CommunicationProvider, + DebugAdapterSession, + DebugAdapterSessionFactory, + DebugAdapterFactory +} from '../common/debug-model'; +import { DebugAdapterSessionImpl } from './debug-adapter-session'; + +/** + * [DebugAdapterFactory](#DebugAdapterFactory) implementation based on + * launching the debug adapter as separate process. + */ +@injectable() +export class LaunchBasedDebugAdapterFactory implements DebugAdapterFactory { + @inject(RawProcessFactory) + protected readonly processFactory: RawProcessFactory; + @inject(ProcessManager) + protected readonly processManager: ProcessManager; + + start(executable: DebugAdapterExecutable): CommunicationProvider { + const process = this.childProcess(executable); + + // FIXME: propagate onError + onExit + return { + input: process.input, + output: process.output, + dispose: () => process.kill() + }; + } + + private childProcess(executable: DebugAdapterExecutable): RawProcess { + // tslint:disable-next-line:no-any + const isForkOptions = (forkOptions: RawForkOptions | any): forkOptions is RawForkOptions => + !!forkOptions && !!forkOptions.modulePath; + + const processOptions: RawProcessOptions | RawForkOptions = { ...executable }; + const options = { stdio: ['pipe', 'pipe', 2] }; + + if (isForkOptions(processOptions)) { + options.stdio.push('ipc'); + } + + processOptions.options = options; + return this.processFactory(processOptions); + } + + connect(debugServerPort: number): CommunicationProvider { + const socket = net.createConnection(debugServerPort); + // FIXME: propagate socket.on('error', ...) + socket.on('close', ...) + return { + input: socket, + output: socket, + dispose: () => socket.end() + }; + } +} + +/** + * [DebugAdapterSessionFactory](#DebugAdapterSessionFactory) implementation. + */ +@injectable() +export class DebugAdapterSessionFactoryImpl implements DebugAdapterSessionFactory { + + get(sessionId: string, communicationProvider: CommunicationProvider): DebugAdapterSession { + return new DebugAdapterSessionImpl( + sessionId, + communicationProvider + ); + } +} diff --git a/packages/debug/src/node/debug-adapter-session-manager.ts b/packages/debug/src/node/debug-adapter-session-manager.ts index d29a39b0fa24a..730104eb0211a 100644 --- a/packages/debug/src/node/debug-adapter-session-manager.ts +++ b/packages/debug/src/node/debug-adapter-session-manager.ts @@ -20,7 +20,7 @@ import { MessagingService } from '@theia/core/lib/node/messaging/messaging-servi import { DebugAdapterPath } from '../common/debug-service'; import { DebugConfiguration } from '../common/debug-configuration'; -import { DebugAdapterSession, DebugAdapterSessionFactory, DebugAdapterFactory } from './debug-model'; +import { DebugAdapterSession, DebugAdapterSessionFactory, DebugAdapterFactory } from '../common/debug-model'; import { DebugAdapterContributionRegistry } from './debug-adapter-contribution-registry'; /** diff --git a/packages/debug/src/node/debug-adapter.ts b/packages/debug/src/node/debug-adapter-session.ts similarity index 65% rename from packages/debug/src/node/debug-adapter.ts rename to packages/debug/src/node/debug-adapter-session.ts index 90e7ff3509af9..f559dd8b27a67 100644 --- a/packages/debug/src/node/debug-adapter.ts +++ b/packages/debug/src/node/debug-adapter-session.ts @@ -21,74 +21,13 @@ // 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 'inversify'; -import { Disposable, DisposableCollection } from '@theia/core'; import { - RawProcessFactory, - ProcessManager, - RawProcess, - RawProcessOptions, - RawForkOptions -} from '@theia/process/lib/node'; -import { - DebugAdapterExecutable, CommunicationProvider, DebugAdapterSession, - DebugAdapterSessionFactory, - DebugAdapterFactory -} from './debug-model'; +} from '../common/debug-model'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { WebSocketChannel } from '@theia/core/lib/common/messaging/web-socket-channel'; - -/** - * [DebugAdapterFactory](#DebugAdapterFactory) implementation based on - * launching the debug adapter as separate process. - */ -@injectable() -export class LaunchBasedDebugAdapterFactory implements DebugAdapterFactory { - @inject(RawProcessFactory) - protected readonly processFactory: RawProcessFactory; - @inject(ProcessManager) - protected readonly processManager: ProcessManager; - - start(executable: DebugAdapterExecutable): CommunicationProvider { - const process = this.childProcess(executable); - - // FIXME: propagate onError + onExit - return { - input: process.input, - output: process.output, - dispose: () => process.kill() - }; - } - - private childProcess(executable: DebugAdapterExecutable): RawProcess { - // tslint:disable-next-line:no-any - const isForkOptions = (forkOptions: any): forkOptions is RawForkOptions => - !!forkOptions && !!forkOptions.modulePath; - - const processOptions: RawProcessOptions | RawForkOptions = { ...executable }; - const options = { stdio: ['pipe', 'pipe', 2] }; - - if (isForkOptions(processOptions)) { - options.stdio.push('ipc'); - } - - processOptions.options = options; - return this.processFactory(processOptions); - } - - connect(debugServerPort: number): CommunicationProvider { - const socket = net.createConnection(debugServerPort); - // FIXME: propagate socket.on('error', ...) + socket.on('close', ...) - return { - input: socket, - output: socket, - dispose: () => socket.end() - }; - } -} +import { IWebSocket } from 'vscode-ws-jsonrpc/lib/socket/socket'; +import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; /** * [DebugAdapterSession](#DebugAdapterSession) implementation. @@ -96,9 +35,10 @@ export class LaunchBasedDebugAdapterFactory implements DebugAdapterFactory { export class DebugAdapterSessionImpl implements DebugAdapterSession { private static TWO_CRLF = '\r\n\r\n'; + private static CONTENT_LENGTH = 'Content-Length'; private readonly toDispose = new DisposableCollection(); - private channel: WebSocketChannel | undefined; + private channel: IWebSocket | undefined; private contentLength: number; private buffer: Buffer; @@ -115,7 +55,7 @@ export class DebugAdapterSessionImpl implements DebugAdapterSession { ]); } - async start(channel: WebSocketChannel): Promise { + async start(channel: IWebSocket): Promise { if (this.channel) { throw new Error('The session has already been started, id: ' + this.id); } @@ -167,13 +107,22 @@ export class DebugAdapterSessionImpl implements DebugAdapterSession { continue; // there may be more complete messages to process } } else { - const idx = this.buffer.indexOf(DebugAdapterSessionImpl.TWO_CRLF); + let idx = this.buffer.indexOf(DebugAdapterSessionImpl.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(DebugAdapterSessionImpl.TWO_CRLF); if (idx !== -1) { const header = this.buffer.toString('utf8', 0, idx); const lines = header.split('\r\n'); for (let i = 0; i < lines.length; i++) { const pair = lines[i].split(/: +/); - if (pair[0] === 'Content-Length') { + if (pair[0] === DebugAdapterSessionImpl.CONTENT_LENGTH) { this.contentLength = +pair[1]; } } @@ -199,17 +148,3 @@ export class DebugAdapterSessionImpl implements DebugAdapterSession { this.toDispose.dispose(); } } - -/** - * [DebugAdapterSessionFactory](#DebugAdapterSessionFactory) implementation. - */ -@injectable() -export class DebugAdapterSessionFactoryImpl implements DebugAdapterSessionFactory { - - get(sessionId: string, communicationProvider: CommunicationProvider): DebugAdapterSession { - return new DebugAdapterSessionImpl( - sessionId, - communicationProvider - ); - } -} diff --git a/packages/debug/src/node/debug-backend-module.ts b/packages/debug/src/node/debug-backend-module.ts index fcdbd2d695f68..ff8475887bc46 100644 --- a/packages/debug/src/node/debug-backend-module.ts +++ b/packages/debug/src/node/debug-backend-module.ts @@ -23,14 +23,14 @@ import { import { LaunchBasedDebugAdapterFactory, DebugAdapterSessionFactoryImpl -} from './debug-adapter'; +} 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 './debug-model'; +} from '../common/debug-model'; import { DebugServiceImpl } from './debug-service-impl'; import { DebugAdapterContributionRegistry } from './debug-adapter-contribution-registry'; import { DebugAdapterSessionManager } from './debug-adapter-session-manager'; diff --git a/packages/debug/src/node/debug-service-impl.ts b/packages/debug/src/node/debug-service-impl.ts index 8b8c648a5977d..001b9e9bfb723 100644 --- a/packages/debug/src/node/debug-service-impl.ts +++ b/packages/debug/src/node/debug-service-impl.ts @@ -35,7 +35,7 @@ export class DebugServiceImpl implements DebugService { protected readonly registry: DebugAdapterContributionRegistry; dispose(): void { - this.stop(); + this.terminateDebugSession(); } async debugTypes(): Promise { @@ -62,13 +62,13 @@ export class DebugServiceImpl implements DebugService { } protected readonly sessions = new Set(); - async create(config: DebugConfiguration): Promise { + async createDebugSession(config: DebugConfiguration): Promise { const session = await this.sessionManager.create(config, this.registry); this.sessions.add(session.id); return session.id; } - async stop(sessionId?: string): Promise { + async terminateDebugSession(sessionId?: string): Promise { if (sessionId) { await this.doStop(sessionId); } else { diff --git a/packages/debug/src/node/vscode/vscode-debug-adapter-contribution.ts b/packages/debug/src/node/vscode/vscode-debug-adapter-contribution.ts index fdb888ce71096..a9a8194f39a56 100644 --- a/packages/debug/src/node/vscode/vscode-debug-adapter-contribution.ts +++ b/packages/debug/src/node/vscode/vscode-debug-adapter-contribution.ts @@ -16,10 +16,11 @@ import * as fs from 'fs-extra'; import * as path from 'path'; -import { injectable, unmanaged } from 'inversify'; -import { DebugAdapterContribution, DebugAdapterExecutable } from '../debug-model'; +import { DebugAdapterExecutable, DebugAdapterContribution } from '../../common/debug-model'; import { isWindows, isOSX } from '@theia/core/lib/common/os'; import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; +import { deepClone } from '@theia/core/lib/common/objects'; +import { injectable, unmanaged } from 'inversify'; namespace nls { export function localize(key: string, _default: string) { @@ -134,7 +135,7 @@ export abstract class AbstractVSCodeDebugAdapterContribution implements DebugAda const taskSchema = {}; // TODO const { configurationAttributes } = debuggerContribution; return Object.keys(configurationAttributes).map(request => { - const attributes: IJSONSchema = configurationAttributes[request]; + const attributes: IJSONSchema = deepClone(configurationAttributes[request]); const defaultRequired = ['name', 'type', 'request']; attributes.required = attributes.required && attributes.required.length ? defaultRequired.concat(attributes.required) : defaultRequired; attributes.additionalProperties = false; @@ -236,5 +237,4 @@ export abstract class AbstractVSCodeDebugAdapterContribution implements DebugAda args }; } - } diff --git a/packages/java-debug/src/node/java-debug-adapter-contribution.ts b/packages/java-debug/src/node/java-debug-adapter-contribution.ts index c74d7d3bc5444..c25c4740744d4 100644 --- a/packages/java-debug/src/node/java-debug-adapter-contribution.ts +++ b/packages/java-debug/src/node/java-debug-adapter-contribution.ts @@ -76,7 +76,6 @@ export class JavaDebugExtensionContribution extends AbstractVSCodeDebugAdapterCo path.resolve(this.extensionPath, javaExtPath) ); } - } @injectable() diff --git a/packages/java-debug/src/node/java-debug-backend-module.ts b/packages/java-debug/src/node/java-debug-backend-module.ts index 68d7cbb4348e3..ae3cca42c2805 100644 --- a/packages/java-debug/src/node/java-debug-backend-module.ts +++ b/packages/java-debug/src/node/java-debug-backend-module.ts @@ -16,8 +16,8 @@ import { ContainerModule } from 'inversify'; import { JavaExtensionContribution } from '@theia/java/lib/node'; -import { DebugAdapterContribution } from '@theia/debug/lib/node/debug-model'; import { JavaDebugAdapterContribution, JavaDebugExtensionContribution } from './java-debug-adapter-contribution'; +import { DebugAdapterContribution } from '@theia/debug/lib/common/debug-model'; export default new ContainerModule(bind => { /* explcit inTransientScope because it is very important, that diff --git a/packages/plugin-ext/package.json b/packages/plugin-ext/package.json index d693beee45203..3087f3d2541d2 100644 --- a/packages/plugin-ext/package.json +++ b/packages/plugin-ext/package.json @@ -14,11 +14,14 @@ "@theia/plugin": "^0.3.18", "@theia/task": "^0.3.18", "@theia/workspace": "^0.3.18", + "@theia/debug": "^0.3.18", "decompress": "^4.2.0", "jsonc-parser": "^2.0.2", "lodash.clonedeep": "^4.5.0", "ps-tree": "1.1.0", - "vscode-uri": "^1.0.1" + "vscode-uri": "^1.0.1", + "uuid": "^3.2.1", + "vscode-debugprotocol": "^1.32.0" }, "publishConfig": { "access": "public" diff --git a/packages/plugin-ext/src/api/model.ts b/packages/plugin-ext/src/api/model.ts index 25c59ba858bc3..ebb2a86a94dc9 100644 --- a/packages/plugin-ext/src/api/model.ts +++ b/packages/plugin-ext/src/api/model.ts @@ -407,3 +407,12 @@ export interface WorkspaceFolder { name: string; index: number; } + +export interface Breakpoint { + readonly enabled: boolean; + readonly condition?: string; + readonly hitCondition?: string; + readonly logMessage?: string; + readonly location?: Location; + readonly functionName?: string; +} diff --git a/packages/plugin-ext/src/api/plugin-api.ts b/packages/plugin-ext/src/api/plugin-api.ts index 13d2c0b27153e..9836bee0b4153 100644 --- a/packages/plugin-ext/src/api/plugin-api.ts +++ b/packages/plugin-ext/src/api/plugin-api.ts @@ -46,14 +46,18 @@ import { TextEdit, DocumentSymbol, ReferenceContext, - Location, FileWatcherSubscriberOptions, FileChangeEvent, TextDocumentShowOptions, - WorkspaceRootsChangeEvent + WorkspaceRootsChangeEvent, + Location, + Breakpoint } from './model'; import { ExtPluginApi } from '../common/plugin-ext-api-contribution'; import { CancellationToken, Progress, ProgressOptions } from '@theia/plugin'; +import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; +import { DebuggerDescription } from '@theia/debug/lib/common/debug-service'; +import { DebugProtocol } from 'vscode-debugprotocol'; export interface PluginInitData { plugins: PluginMetadata[]; @@ -156,15 +160,17 @@ export interface TerminalServiceExt { } export interface ConnectionMain { + $createConnection(id: string): Promise; + $deleteConnection(id: string): Promise; $sendMessage(id: string, message: string): void; $createConnection(id: string): Promise; $deleteConnection(id: string): Promise; } export interface ConnectionExt { - $sendMessage(id: string, message: string): void; $createConnection(id: string): Promise; $deleteConnection(id: string): Promise + $sendMessage(id: string, message: string): void; } export interface TerminalServiceMain { @@ -903,6 +909,32 @@ export interface WebviewsMain { $unregisterSerializer(viewType: string): void; } +export interface DebugExt { + $onSessionCustomEvent(sessionId: string, event: string, body?: any): void; + $breakpointsDidChange(all: Breakpoint[], added: Breakpoint[], removed: Breakpoint[], changed: Breakpoint[]): void; + $sessionDidCreate(sessionId: string): void; + $sessionDidDestroy(sessionId: string): void; + $sessionDidChange(sessionId: string | undefined): void; + $provideDebugConfigurations(contributionId: string, folder: string | undefined): Promise; + $resolveDebugConfigurations(contributionId: string, debugConfiguration: theia.DebugConfiguration, folder: string | undefined): Promise; + $getSupportedLanguages(contributionId: string): Promise; + $getSchemaAttributes(contributionId: string): Promise; + $getConfigurationSnippets(contributionId: string): Promise; + $createDebugSession(contributionId: string, debugConfiguration: theia.DebugConfiguration): Promise; + $terminateDebugSession(sessionId: string): Promise; +} + +export interface DebugMain { + $appendToDebugConsole(value: string): Promise; + $appendLineToDebugConsole(value: string): Promise; + $registerDebugConfigurationProvider(contributorId: string, description: DebuggerDescription): Promise; + $unregisterDebugConfigurationProvider(contributorId: string): Promise; + $addBreakpoints(breakpoints: Breakpoint[]): Promise; + $removeBreakpoints(breakpoints: Breakpoint[]): Promise; + $startDebugging(folder: theia.WorkspaceFolder | undefined, nameOrConfiguration: string | theia.DebugConfiguration): Promise; + $customRequest(command: string, args?: any): Promise; +} + export const PLUGIN_RPC_CONTEXT = { COMMAND_REGISTRY_MAIN: >createProxyIdentifier('CommandRegistryMain'), QUICK_OPEN_MAIN: createProxyIdentifier('QuickOpenMain'), @@ -923,6 +955,7 @@ export const PLUGIN_RPC_CONTEXT = { WEBVIEWS_MAIN: createProxyIdentifier('WebviewsMain'), TASKS_MAIN: createProxyIdentifier('TasksMain'), LANGUAGES_CONTRIBUTION_MAIN: createProxyIdentifier('LanguagesContributionMain'), + DEBUG_MAIN: createProxyIdentifier('DebugMain') }; export const MAIN_RPC_CONTEXT = { @@ -943,6 +976,7 @@ export const MAIN_RPC_CONTEXT = { WEBVIEWS_EXT: createProxyIdentifier('WebviewsExt'), TASKS_EXT: createProxyIdentifier('TasksExt'), LANGUAGES_CONTRIBUTION_EXT: createProxyIdentifier('LanguagesContributionExt'), + DEBUG_EXT: createProxyIdentifier('DebugExt') }; export interface TasksExt { diff --git a/packages/plugin-ext/src/common/connection.ts b/packages/plugin-ext/src/common/connection.ts index 2d878207812b6..b806b74988186 100644 --- a/packages/plugin-ext/src/common/connection.ts +++ b/packages/plugin-ext/src/common/connection.ts @@ -17,6 +17,7 @@ import { Disposable } from './disposable-util'; import { PluginMessageReader } from './plugin-message-reader'; import { PluginMessageWriter } from './plugin-message-writer'; import { MessageReader, MessageWriter, Message } from 'vscode-jsonrpc'; +import { IWebSocket } from 'vscode-ws-jsonrpc/lib/socket/socket'; /** * The interface for describing the connection between plugins and main side. @@ -37,16 +38,10 @@ export interface Connection extends Disposable { * The container for message reader and writer which can be used to create connection between plugins and main side. */ export class PluginConnection implements Connection { - reader: PluginMessageReader; - writer: PluginMessageWriter; - clearConnection: () => void; - - constructor(protected readonly pluginMessageReader: PluginMessageReader, - protected readonly pluginMessageWriter: PluginMessageWriter, - dispose: () => void) { - this.reader = pluginMessageReader; - this.writer = pluginMessageWriter; - this.clearConnection = dispose; + constructor( + readonly reader: PluginMessageReader, + readonly writer: PluginMessageWriter, + readonly dispose: () => void) { } forward(to: Connection, map: (message: Message) => Message = message => message): void { @@ -55,11 +50,33 @@ export class PluginConnection implements Connection { to.writer.write(output); }); } +} + +/** + * [IWebSocket](#IWebSocket) implementation over RPC. + */ +export class PluginWebSocketChannel implements IWebSocket { + constructor(protected readonly connection: PluginConnection) { } + + send(content: string): void { + this.connection.writer.write(content); + } + + // tslint:disable-next-line:no-any + onMessage(cb: (data: any) => void): void { + this.connection.reader.listen(cb); + } + + // tslint:disable-next-line:no-any + onError(cb: (reason: any) => void): void { + this.connection.reader.onError(e => cb(e)); + } + + onClose(cb: (code: number, reason: string) => void): void { + this.connection.reader.onClose(() => cb(-1, 'closed')); + } - /** - * Has to be called when the connection was closed. - */ dispose(): void { - this.clearConnection(); + this.connection.dispose(); } } diff --git a/packages/plugin-ext/src/common/plugin-message-writer.ts b/packages/plugin-ext/src/common/plugin-message-writer.ts index 47b25631291ae..8394d0ea8aa75 100644 --- a/packages/plugin-ext/src/common/plugin-message-writer.ts +++ b/packages/plugin-ext/src/common/plugin-message-writer.ts @@ -14,20 +14,24 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { AbstractMessageWriter } from 'vscode-jsonrpc/lib/messageWriter'; +import { AbstractMessageWriter, MessageWriter } from 'vscode-jsonrpc/lib/messageWriter'; import { ConnectionMain, ConnectionExt } from '../api/plugin-api'; import { Message } from 'vscode-jsonrpc'; /** * Support for writing string message through RPC protocol. */ -export class PluginMessageWriter extends AbstractMessageWriter { - constructor(protected readonly id: string, protected readonly proxy: ConnectionMain | ConnectionExt) { +export class PluginMessageWriter extends AbstractMessageWriter implements MessageWriter { + constructor( + protected readonly id: string, + protected readonly proxy: ConnectionMain | ConnectionExt) { super(); } - write(message: Message): void { - const content = JSON.stringify(message); + write(message: string): void; + write(message: Message): void; + write(arg: string | Message): void { + const content = JSON.stringify(arg); this.proxy.$sendMessage(this.id, content); } } diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 9209aeacebedc..5fc1d3b4e2eef 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -20,6 +20,7 @@ import { LogPart } from './types'; import { CharacterPair, CommentRule, PluginAPIFactory, Plugin } from '../api/plugin-api'; import { PreferenceSchema } from '@theia/core/lib/browser/preferences'; import { ExtPluginApi } from './plugin-ext-api-contribution'; +import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; export const hostedServicePath = '/services/hostedPlugin'; @@ -60,6 +61,7 @@ export interface PluginPackageContribution { views?: { [location: string]: PluginPackageView[] }; menus?: { [location: string]: PluginPackageMenu[] }; keybindings?: PluginPackageKeybinding[]; + debuggers?: PluginPackageDebuggersContribution[]; } export interface PluginPackageViewContainer { @@ -98,6 +100,32 @@ export interface ScopeMap { [scopeName: string]: string; } +export interface PlatformSpecificAdapterContribution { + program?: string; + args?: string[]; + runtime?: string; + runtimeArgs?: string[]; +} + +/** + * This interface describes a package.json debuggers contribution section object. + */ +export interface PluginPackageDebuggersContribution extends PlatformSpecificAdapterContribution { + type: string; + label?: string; + languages?: string[]; + enableBreakpointsFor?: { languageIds: string[] }; + configurationAttributes: { [request: string]: IJSONSchema }; + configurationSnippets: IJSONSchemaSnippet[]; + variables?: ScopeMap; + adapterExecutableCommand?: string; + win?: PlatformSpecificAdapterContribution; + winx86?: PlatformSpecificAdapterContribution; + windows?: PlatformSpecificAdapterContribution; + osx?: PlatformSpecificAdapterContribution; + linux?: PlatformSpecificAdapterContribution; +} + /** * This interface describes a package.json languages contribution section object. */ @@ -311,6 +339,7 @@ export interface PluginContribution { views?: { [location: string]: View[] }; menus?: { [location: string]: Menu[] }; keybindings?: Keybinding[]; + debuggers?: DebuggerContribution[]; } export interface GrammarsContribution { @@ -347,6 +376,27 @@ export interface LanguageConfiguration { wordPattern?: string; } +/** + * This interface describes a package.json debuggers contribution section object. + */ +export interface DebuggerContribution extends PlatformSpecificAdapterContribution { + type: string, + label?: string, + languages?: string[], + enableBreakpointsFor?: { + languageIds: string[] + }, + configurationAttributes?: IJSONSchema[], + configurationSnippets?: IJSONSchemaSnippet[], + variables?: ScopeMap, + adapterExecutableCommand?: string + win?: PlatformSpecificAdapterContribution; + winx86?: PlatformSpecificAdapterContribution; + windows?: PlatformSpecificAdapterContribution; + osx?: PlatformSpecificAdapterContribution; + linux?: PlatformSpecificAdapterContribution; +} + export interface IndentationRules { increaseIndentPattern: string; decreaseIndentPattern: string; diff --git a/packages/plugin-ext/src/hosted/browser/worker/debug-stub.ts b/packages/plugin-ext/src/hosted/browser/worker/debug-stub.ts new file mode 100644 index 0000000000000..8f8e44b884d11 --- /dev/null +++ b/packages/plugin-ext/src/hosted/browser/worker/debug-stub.ts @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { DebugExtImpl } from '../../../plugin/node/debug/debug'; + +export function createDebugExtStub(): DebugExtImpl { + const err = new Error('Debug API works only in plugin container'); + + return new Proxy({}, { + get: function (obj, prop) { + throw err; + }, + + set(obj, prop, value) { + throw err; + }, + + apply: function (target, that, args) { + throw err; + } + }) as DebugExtImpl; +} diff --git a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts index d45efbc9b8aae..e2fc74302246a 100644 --- a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts +++ b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts @@ -24,7 +24,7 @@ import * as theia from '@theia/plugin'; import { EnvExtImpl } from '../../../plugin/env'; import { PreferenceRegistryExtImpl } from '../../../plugin/preference-registry'; import { ExtPluginApi } from '../../../common/plugin-ext-api-contribution'; -import { ConnectionExtImpl } from '../../../plugin/connection-ext'; +import { createDebugExtStub } from './debug-stub'; // tslint:disable-next-line:no-any const ctx = self as any; @@ -47,8 +47,8 @@ function initialize(contextPath: string, pluginMetadata: PluginMetadata): void { ctx.importScripts('/context/' + contextPath); } const envExt = new EnvExtImpl(rpc); -const connectionExt = new ConnectionExtImpl(rpc); const preferenceRegistryExt = new PreferenceRegistryExtImpl(rpc); +const debugExt = createDebugExtStub(); const pluginManager = new PluginManagerExtImpl({ // tslint:disable-next-line:no-any @@ -119,10 +119,11 @@ const pluginManager = new PluginManagerExtImpl({ } }, envExt, preferenceRegistryExt); -const apiFactory = createAPIFactory(rpc, +const apiFactory = createAPIFactory( + rpc, pluginManager, envExt, - connectionExt, + debugExt, preferenceRegistryExt); let defaultApi: typeof theia; diff --git a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts index 3481c59598d24..1fa8993631e2a 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts @@ -21,7 +21,7 @@ import { createAPIFactory } from '../../plugin/plugin-context'; import { EnvExtImpl } from '../../plugin/env'; import { PreferenceRegistryExtImpl } from '../../plugin/preference-registry'; import { ExtPluginApi } from '../../common/plugin-ext-api-contribution'; -import { ConnectionExtImpl } from '../../plugin/connection-ext'; +import { DebugExtImpl } from '../../plugin/node/debug/debug'; /** * Handle the RPC calls. @@ -38,15 +38,17 @@ export class PluginHostRPC { initialize() { const envExt = new EnvExtImpl(this.rpc); - const connectionExt = new ConnectionExtImpl(this.rpc); + const debugExt = new DebugExtImpl(this.rpc); const preferenceRegistryExt = new PreferenceRegistryExtImpl(this.rpc); this.pluginManager = this.createPluginManager(envExt, preferenceRegistryExt, this.rpc); this.rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, this.pluginManager); this.rpc.set(MAIN_RPC_CONTEXT.PREFERENCE_REGISTRY_EXT, preferenceRegistryExt); - PluginHostRPC.apiFactory = createAPIFactory(this.rpc, + + PluginHostRPC.apiFactory = createAPIFactory( + this.rpc, this.pluginManager, envExt, - connectionExt, + debugExt, preferenceRegistryExt); } diff --git a/packages/plugin-ext/src/hosted/node/plugin-reader.ts b/packages/plugin-ext/src/hosted/node/plugin-reader.ts index 1cbe426ab3e0e..9dc623f6662bd 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-reader.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-reader.ts @@ -75,7 +75,10 @@ export class HostedPluginReader implements BackendApplicationContribution { return undefined; } - const plugin: PluginPackage = require(packageJsonPath); + let rawData = fs.readFileSync(packageJsonPath).toString(); + rawData = this.localize(rawData, path); + + const plugin: PluginPackage = JSON.parse(rawData); plugin.packagePath = path; const pluginMetadata = this.scanner.getPluginMetadata(plugin); if (pluginMetadata.model.entryPoint.backend) { @@ -95,6 +98,21 @@ export class HostedPluginReader implements BackendApplicationContribution { return pluginMetadata; } + private localize(rawData: string, pluginPath: string): string { + const nlsPath = pluginPath + 'package.nls.json'; + if (fs.existsSync(nlsPath)) { + const nlsMap: { + [key: string]: string + } = require(nlsPath); + for (const key of Object.keys(nlsMap)) { + const value = nlsMap[key].replace(/\"/g, '\\"'); + rawData = rawData.split('%' + key + '%').join(value); + } + } + + return rawData; + } + getPlugin(): PluginMetadata | undefined { return this.plugin; } diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index 8c43a1722aacc..e4593709adafb 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -36,7 +36,9 @@ import { View, PluginPackageView, Menu, - PluginPackageMenu + PluginPackageMenu, + PluginPackageDebuggersContribution, + DebuggerContribution } from '../../../common/plugin-protocol'; import * as fs from 'fs'; import * as path from 'path'; @@ -44,6 +46,20 @@ import { isObject } from 'util'; import { GrammarsReader } from './grammars-reader'; import { CharacterPair } from '../../../api/plugin-api'; import * as jsoncparser from 'jsonc-parser'; +import { IJSONSchema } from '@theia/core/lib/common/json-schema'; +import { deepClone } from '@theia/core/lib/common/objects'; + +namespace nls { + export function localize(key: string, _default: string) { + return _default; + } +} + +const INTERNAL_CONSOLE_OPTIONS_SCHEMA = { + enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart'], + default: 'openOnFirstSessionStart', + description: nls.localize('internalConsoleOptions', 'Controls when the internal debug console should open.') +}; @injectable() export class TheiaPluginScanner implements PluginScanner { @@ -146,6 +162,12 @@ export class TheiaPluginScanner implements PluginScanner { if (rawPlugin.contributes && rawPlugin.contributes.keybindings) { contributions.keybindings = rawPlugin.contributes.keybindings.map(rawKeybinding => this.readKeybinding(rawKeybinding)); } + + if (rawPlugin.contributes!.debuggers) { + const debuggers = this.readDebuggers(rawPlugin.contributes.debuggers!); + contributions.debuggers = debuggers; + } + return contributions; } @@ -243,6 +265,115 @@ export class TheiaPluginScanner implements PluginScanner { } + private readDebuggers(rawDebuggers: PluginPackageDebuggersContribution[]): DebuggerContribution[] { + return rawDebuggers.map(rawDebug => this.readDebugger(rawDebug)); + } + + private readDebugger(rawDebugger: PluginPackageDebuggersContribution): DebuggerContribution { + const result: DebuggerContribution = { + type: rawDebugger.type, + label: rawDebugger.label, + languages: rawDebugger.languages, + enableBreakpointsFor: rawDebugger.enableBreakpointsFor, + variables: rawDebugger.variables, + adapterExecutableCommand: rawDebugger.adapterExecutableCommand, + configurationSnippets: rawDebugger.configurationSnippets, + win: rawDebugger.win, + winx86: rawDebugger.winx86, + windows: rawDebugger.windows, + osx: rawDebugger.osx, + linux: rawDebugger.linux, + program: rawDebugger.program, + args: rawDebugger.args, + runtime: rawDebugger.runtime, + runtimeArgs: rawDebugger.runtimeArgs + }; + + result.configurationAttributes = rawDebugger.configurationAttributes + && this.resolveSchemaAttributes(rawDebugger.type, rawDebugger.configurationAttributes); + + return result; + } + + protected resolveSchemaAttributes(type: string, configurationAttributes: { [request: string]: IJSONSchema }): IJSONSchema[] { + const taskSchema = {}; + return Object.keys(configurationAttributes).map(request => { + const attributes: IJSONSchema = deepClone(configurationAttributes[request]); + const defaultRequired = ['name', 'type', 'request']; + attributes.required = attributes.required && attributes.required.length ? defaultRequired.concat(attributes.required) : defaultRequired; + attributes.additionalProperties = false; + attributes.type = 'object'; + if (!attributes.properties) { + attributes.properties = {}; + } + const properties = attributes.properties; + properties['type'] = { + enum: [type], + description: nls.localize('debugType', 'Type of configuration.'), + pattern: '^(?!node2)', + errorMessage: nls.localize('debugTypeNotRecognised', + 'The debug type is not recognized. Make sure that you have a corresponding debug extension installed and that it is enabled.'), + patternErrorMessage: nls.localize('node2NotSupported', + '"node2" is no longer supported, use "node" instead and set the "protocol" attribute to "inspector".') + }; + properties['name'] = { + type: 'string', + description: nls.localize('debugName', 'Name of configuration; appears in the launch configuration drop down menu.'), + default: 'Launch' + }; + properties['request'] = { + enum: [request], + description: nls.localize('debugRequest', 'Request type of configuration. Can be "launch" or "attach".'), + }; + properties['debugServer'] = { + type: 'number', + description: nls.localize('debugServer', + 'For debug extension development only: if a port is specified VS Code tries to connect to a debug adapter running in server mode'), + default: 4711 + }; + properties['preLaunchTask'] = { + anyOf: [taskSchema, { + type: ['string', 'null'], + }], + default: '', + description: nls.localize('debugPrelaunchTask', 'Task to run before debug session starts.') + }; + properties['postDebugTask'] = { + anyOf: [taskSchema, { + type: ['string', 'null'], + }], + default: '', + description: nls.localize('debugPostDebugTask', 'Task to run after debug session ends.') + }; + properties['internalConsoleOptions'] = INTERNAL_CONSOLE_OPTIONS_SCHEMA; + + const osProperties = Object.assign({}, properties); + properties['windows'] = { + type: 'object', + description: nls.localize('debugWindowsConfiguration', 'Windows specific launch configuration attributes.'), + properties: osProperties + }; + properties['osx'] = { + type: 'object', + description: nls.localize('debugOSXConfiguration', 'OS X specific launch configuration attributes.'), + properties: osProperties + }; + properties['linux'] = { + type: 'object', + description: nls.localize('debugLinuxConfiguration', 'Linux specific launch configuration attributes.'), + properties: osProperties + }; + Object.keys(attributes.properties).forEach(name => { + // Use schema allOf property to get independent error reporting #21113 + attributes!.properties![name].pattern = attributes!.properties![name].pattern || '^(?!.*\\$\\{(env|config|command)\\.)'; + attributes!.properties![name].patternErrorMessage = attributes!.properties![name].patternErrorMessage || + nls.localize('deprecatedVariables', "'env.', 'config.' and 'command.' are deprecated, use 'env:', 'config:' and 'command:' instead."); + }); + + return attributes; + }); + } + private extractValidAutoClosingPairs(langId: string, configuration: PluginPackageLanguageContributionConfiguration): AutoClosingPairConditional[] | undefined { const source = configuration.autoClosingPairs; if (typeof source === 'undefined') { diff --git a/packages/plugin-ext/src/main/browser/connection-main.ts b/packages/plugin-ext/src/main/browser/connection-main.ts index 640d21f63bcca..5dd543bfda4b5 100644 --- a/packages/plugin-ext/src/main/browser/connection-main.ts +++ b/packages/plugin-ext/src/main/browser/connection-main.ts @@ -25,6 +25,7 @@ import { PluginMessageWriter } from '../../common/plugin-message-writer'; * Creates holds the connections to the plugins. Allows to send a message to the plugin by getting already created connection via id. */ export class ConnectionMainImpl implements ConnectionMain { + private proxy: ConnectionExt; private connections = new Map(); constructor(rpc: RPCProtocol) { diff --git a/packages/plugin-ext/src/main/browser/debug/debug-main.ts b/packages/plugin-ext/src/main/browser/debug/debug-main.ts new file mode 100644 index 0000000000000..a998454980291 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/debug/debug-main.ts @@ -0,0 +1,247 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +// tslint:disable:no-any + +import { interfaces } from 'inversify'; +import { RPCProtocol } from '../../../api/rpc-protocol'; +import { + DebugMain, + DebugExt, + MAIN_RPC_CONTEXT +} from '../../../api/plugin-api'; +import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager'; +import { Breakpoint, WorkspaceFolder } from '../../../api/model'; +import { LabelProvider } from '@theia/core/lib/browser'; +import { EditorManager } from '@theia/editor/lib/browser'; +import { BreakpointManager } from '@theia/debug/lib/browser/breakpoint/breakpoint-manager'; +import { DebugBreakpoint } from '@theia/debug/lib/browser/model/debug-breakpoint'; +import URI from 'vscode-uri'; +import { DebugConsoleSession } from '@theia/debug/lib/browser/console/debug-console-session'; +import { SourceBreakpoint } from '@theia/debug/lib/browser/breakpoint/breakpoint-marker'; +import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration'; +import { ConnectionMainImpl } from '../connection-main'; +import { DebuggerDescription } from '@theia/debug/lib/common/debug-service'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { DebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager'; +import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; +import { MessageClient } from '@theia/core/lib/common/message-service-protocol'; +import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; +import { DebugPreferences } from '@theia/debug/lib/browser/debug-preferences'; +import { PluginDebugAdapterContribution } from './plugin-debug-adapter-contribution'; +import { PluginDebugSessionContributionRegistrator, PluginDebugSessionContributionRegistry } from './plugin-debug-session-contribution-registry'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { PluginDebugSessionFactory } from './plugin-debug-session-factory'; +import { PluginWebSocketChannel } from '../../../common/connection'; +import { PluginDebugAdapterContributionRegistrator, PluginDebugService } from './plugin-debug-service'; +import { DebugSchemaUpdater } from '@theia/debug/lib/browser/debug-schema-updater'; + +export class DebugMainImpl implements DebugMain { + private readonly debugExt: DebugExt; + + private readonly sessionManager: DebugSessionManager; + private readonly labelProvider: LabelProvider; + private readonly editorManager: EditorManager; + private readonly breakpointsManager: BreakpointManager; + private readonly debugConsoleSession: DebugConsoleSession; + private readonly configurationManager: DebugConfigurationManager; + private readonly terminalService: TerminalService; + private readonly messages: MessageClient; + private readonly outputChannelManager: OutputChannelManager; + private readonly debugPreferences: DebugPreferences; + private readonly sessionContributionRegistrator: PluginDebugSessionContributionRegistrator; + private readonly adapterContributionRegistrator: PluginDebugAdapterContributionRegistrator; + private readonly debugSchemaUpdater: DebugSchemaUpdater; + + // registered plugins per contributorId + private readonly toDispose = new Map(); + + constructor(rpc: RPCProtocol, readonly connectionMain: ConnectionMainImpl, container: interfaces.Container) { + this.debugExt = rpc.getProxy(MAIN_RPC_CONTEXT.DEBUG_EXT); + this.sessionManager = container.get(DebugSessionManager); + this.labelProvider = container.get(LabelProvider); + this.editorManager = container.get(EditorManager); + this.breakpointsManager = container.get(BreakpointManager); + this.debugConsoleSession = container.get(DebugConsoleSession); + this.configurationManager = container.get(DebugConfigurationManager); + this.terminalService = container.get(TerminalService); + this.messages = container.get(MessageClient); + this.outputChannelManager = container.get(OutputChannelManager); + this.debugPreferences = container.get(DebugPreferences); + this.adapterContributionRegistrator = container.get(PluginDebugService); + this.sessionContributionRegistrator = container.get(PluginDebugSessionContributionRegistry); + this.debugSchemaUpdater = container.get(DebugSchemaUpdater); + + // TODO: distinguish added/deleted breakpoints + this.breakpointsManager.onDidChangeMarkers(uri => { + const all = this.breakpointsManager.getBreakpoints(); + const affected = this.breakpointsManager.getBreakpoints(uri); + this.debugExt.$breakpointsDidChange(this.toTheiaPluginApiBreakpoints(all), [], [], this.toTheiaPluginApiBreakpoints(affected)); + }); + + this.sessionManager.onDidCreateDebugSession(debugSession => this.debugExt.$sessionDidCreate(debugSession.id)); + this.sessionManager.onDidDestroyDebugSession(debugSession => this.debugExt.$sessionDidDestroy(debugSession.id)); + this.sessionManager.onDidChangeActiveDebugSession(event => this.debugExt.$sessionDidChange(event.current && event.current.id)); + this.sessionManager.onDidReceiveDebugSessionCustomEvent(event => this.debugExt.$onSessionCustomEvent(event.session.id, event.event, event.body)); + } + + async $appendToDebugConsole(value: string): Promise { + this.debugConsoleSession.append(value); + } + + async $appendLineToDebugConsole(value: string): Promise { + this.debugConsoleSession.appendLine(value); + } + + async $registerDebugConfigurationProvider(contributorId: string, description: DebuggerDescription): Promise { + const disposable = new DisposableCollection(); + this.toDispose.set(contributorId, disposable); + + const debugAdapterContributor = new PluginDebugAdapterContribution( + description.type, + description.label, + this.debugExt.$getSupportedLanguages(contributorId), + contributorId, + this.debugExt); + + const debugSessionFactory = new PluginDebugSessionFactory( + this.terminalService, + this.editorManager, + this.breakpointsManager, + this.labelProvider, + this.messages, + this.outputChannelManager, + this.debugPreferences, + async (sessionId: string) => { + const connection = await this.connectionMain.ensureConnection(sessionId); + return new PluginWebSocketChannel(connection); + } + ); + + disposable.push(this.adapterContributionRegistrator.registerDebugAdapterContribution(debugAdapterContributor)); + disposable.push( + this.sessionContributionRegistrator.registerDebugSessionContribution( + { + debugType: description.type, + debugSessionFactory: () => debugSessionFactory + }) + ); + + this.debugSchemaUpdater.update(); + } + + async $unregisterDebugConfigurationProvider(contributorId: string): Promise { + const disposable = this.toDispose.get(contributorId); + if (disposable) { + disposable.dispose(); + this.toDispose.delete(contributorId); + this.debugSchemaUpdater.update(); + } + } + + async $addBreakpoints(breakpoints: Breakpoint[]): Promise { + this.sessionManager.addBreakpoints(this.toInternalBreakpoints(breakpoints)); + } + + async $removeBreakpoints(breakpoints: Breakpoint[]): Promise { + this.sessionManager.deleteBreakpoints(this.toInternalBreakpoints(breakpoints)); + } + + async $customRequest(sessionId: string, command: string, args?: any): Promise { + const session = this.sessionManager.getSession(sessionId); + if (session) { + return session.sendCustomRequest(command, args); + } + + throw new Error(`Debug session '${sessionId}' not found`); + } + + async $startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration): Promise { + let configuration: DebugConfiguration | undefined; + + if (typeof nameOrConfiguration === 'string') { + for (const options of this.configurationManager.all) { + if (options.configuration.name === nameOrConfiguration) { + configuration = options.configuration; + } + } + } else { + configuration = nameOrConfiguration; + } + + if (!configuration) { + console.error(`There is no debug configuration for ${nameOrConfiguration}`); + return false; + } + + const session = await this.sessionManager.start({ + configuration, + workspaceFolderUri: folder && URI.revive(folder.uri).toString() + }); + + return !!session; + } + + private toInternalBreakpoints(breakpoints: Breakpoint[]): DebugBreakpoint[] { + return breakpoints + .filter(breakpoint => !!breakpoint.location) + .map(breakpoint => { + const location = breakpoint.location!; + const uri = URI.revive(location.uri); + const uriString = uri.toString(); + + const origin = { + uri: uriString, + enabled: true, + raw: { + line: location.range.startLineNumber, + column: location.range.startColumn, + condition: breakpoint.condition, + hitCondition: breakpoint.hitCondition, + logMessage: breakpoint.logMessage + } + }; + + return new DebugBreakpoint(origin, + this.labelProvider, + this.breakpointsManager, + this.editorManager, + this.sessionManager.currentSession); + }); + } + + private toTheiaPluginApiBreakpoints(sourceBreakpoints: SourceBreakpoint[]): Breakpoint[] { + return sourceBreakpoints.map(b => { + const breakpoint = { + enabled: b.enabled, + condition: b.raw.condition, + hitCondition: b.raw.hitCondition, + logMessage: b.raw.logMessage, + location: { + uri: URI.parse(b.uri), + range: { + startLineNumber: b.raw.line, + startColumn: b.raw.column || 0, + endLineNumber: b.raw.line, + endColumn: b.raw.column || 0 + } + } + }; + + return breakpoint; + }); + } +} diff --git a/packages/plugin-ext/src/main/browser/debug/plugin-debug-adapter-contribution.ts b/packages/plugin-ext/src/main/browser/debug/plugin-debug-adapter-contribution.ts new file mode 100644 index 0000000000000..1031a4547775e --- /dev/null +++ b/packages/plugin-ext/src/main/browser/debug/plugin-debug-adapter-contribution.ts @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { DebugExt, } from '../../../api/plugin-api'; +import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration'; +import { IJSONSchemaSnippet, IJSONSchema } from '@theia/core/lib/common/json-schema'; +import { MaybePromise } from '@theia/core/lib/common/types'; +import { DebugAdapterContribution } from '@theia/debug/lib/common/debug-model'; + +/** + * Plugin [DebugAdapterContribution](#DebugAdapterContribution) with functionality + * to create / terminated debug adapter session. + */ +export class PluginDebugAdapterContribution implements DebugAdapterContribution { + constructor( + readonly type: string, + readonly label: MaybePromise, + readonly languages: MaybePromise, + protected readonly contributorId: string, + protected readonly debugExt: DebugExt) { } + + async provideDebugConfigurations(workspaceFolderUri: string | undefined): Promise { + return this.debugExt.$provideDebugConfigurations(this.contributorId, workspaceFolderUri); + } + + async resolveDebugConfiguration(config: DebugConfiguration, workspaceFolderUri: string | undefined): Promise { + return this.debugExt.$resolveDebugConfigurations(this.contributorId, config, workspaceFolderUri); + } + + async getSchemaAttributes(): Promise { + return this.debugExt.$getSchemaAttributes(this.contributorId); + } + + async getConfigurationSnippets(): Promise { + return this.debugExt.$getConfigurationSnippets(this.contributorId); + } + + async createDebugSession(debugConfiguration: DebugConfiguration): Promise { + return this.debugExt.$createDebugSession(this.contributorId, debugConfiguration); + } + + async terminateDebugSession(sessionId: string): Promise { + return this.debugExt.$terminateDebugSession(sessionId); + } +} diff --git a/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts b/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts new file mode 100644 index 0000000000000..9c9740058964f --- /dev/null +++ b/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts @@ -0,0 +1,174 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { DebugService, DebuggerDescription, DebugPath } from '@theia/debug/lib/common/debug-service'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration'; +import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; +import { PluginDebugAdapterContribution } from './plugin-debug-adapter-contribution'; +import { injectable, inject, postConstruct } from 'inversify'; +import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider'; + +/** + * Debug adapter contribution registrator. + */ +export interface PluginDebugAdapterContributionRegistrator { + /** + * Registers [PluginDebugAdapterContribution](#PluginDebugAdapterContribution). + * @param contrib contribution + */ + registerDebugAdapterContribution(contrib: PluginDebugAdapterContribution): Disposable; + + /** + * Unregisters [PluginDebugAdapterContribution](#PluginDebugAdapterContribution). + * @param debugType the debug type + */ + unregisterDebugAdapterContribution(debugType: string): void; +} + +/** + * Debug service to work with plugin and extension contributions. + */ +@injectable() +export class PluginDebugService implements DebugService, PluginDebugAdapterContributionRegistrator { + protected readonly contributors = new Map(); + protected readonly toDispose = new DisposableCollection(); + + // maps session and contribution identifiers. + protected readonly sessionId2contrib = new Map(); + protected delegated: DebugService; + + @inject(WebSocketConnectionProvider) + protected readonly connectionProvider: WebSocketConnectionProvider; + + @postConstruct() + protected init(): void { + this.delegated = this.connectionProvider.createProxy(DebugPath); + this.toDispose.push(Disposable.create(() => this.delegated.dispose())); + this.toDispose.push(Disposable.create(() => { + for (const sessionId of this.sessionId2contrib.keys()) { + const contrib = this.sessionId2contrib.get(sessionId)!; + contrib.terminateDebugSession(sessionId); + } + })); + } + + registerDebugAdapterContribution(contrib: PluginDebugAdapterContribution): Disposable { + const { type } = contrib; + + if (this.contributors.has(type)) { + console.warn(`Debugger with type '${type}' already registered.`); + return Disposable.NULL; + } + + this.contributors.set(type, contrib); + return Disposable.create(() => this.unregisterDebugAdapterContribution(type)); + } + + unregisterDebugAdapterContribution(debugType: string): void { + this.contributors.delete(debugType); + } + + async debugTypes(): Promise { + const debugTypes = await this.delegated.debugTypes(); + return debugTypes.concat(Array.from(this.contributors.keys())); + } + + async provideDebugConfigurations(debugType: string, workspaceFolderUri: string | undefined): Promise { + const contributor = this.contributors.get(debugType); + if (contributor) { + return contributor.provideDebugConfigurations && contributor.provideDebugConfigurations(workspaceFolderUri) || []; + } else { + return this.delegated.provideDebugConfigurations(debugType, workspaceFolderUri); + } + } + + async resolveDebugConfiguration(config: DebugConfiguration, workspaceFolderUri: string | undefined): Promise { + let resolved = config; + + for (const contributor of this.contributors.values()) { + if (contributor.resolveDebugConfiguration) { + try { + resolved = await contributor.resolveDebugConfiguration(config, workspaceFolderUri) || resolved; + } catch (e) { + console.error(e); + } + } + } + + return this.delegated.resolveDebugConfiguration(resolved, workspaceFolderUri); + } + + async getDebuggersForLanguage(language: string): Promise { + const debuggers = await this.delegated.getDebuggersForLanguage(language); + + for (const contributor of this.contributors.values()) { + const languages = await contributor.languages; + if (languages && languages.indexOf(language) !== -1) { + const { type } = contributor; + debuggers.push({ type, label: await contributor.label || type }); + } + } + + return debuggers; + } + + async getSchemaAttributes(debugType: string): Promise { + const contributor = this.contributors.get(debugType); + if (contributor) { + return contributor.getSchemaAttributes && contributor.getSchemaAttributes() || []; + } else { + return this.delegated.getSchemaAttributes(debugType); + } + } + + async getConfigurationSnippets(): Promise { + let snippets = await this.delegated.getConfigurationSnippets(); + + for (const contributor of this.contributors.values()) { + if (contributor.getConfigurationSnippets) { + snippets = snippets.concat(await contributor.getConfigurationSnippets()); + } + } + + return snippets; + } + + async createDebugSession(config: DebugConfiguration): Promise { + const contributor = this.contributors.get(config.type); + if (contributor) { + const sessionId = await contributor.createDebugSession(config); + this.sessionId2contrib.set(sessionId, contributor); + return sessionId; + } else { + return this.delegated.createDebugSession(config); + } + } + + async terminateDebugSession(sessionId: string): Promise { + const contributor = this.sessionId2contrib.get(sessionId); + if (contributor) { + this.sessionId2contrib.delete(sessionId); + return contributor.terminateDebugSession(sessionId); + } else { + return this.delegated.terminateDebugSession(sessionId); + } + } + + dispose(): void { + this.toDispose.dispose(); + } +} diff --git a/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-contribution-registry.ts b/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-contribution-registry.ts new file mode 100644 index 0000000000000..b6d8dc7dd4819 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-contribution-registry.ts @@ -0,0 +1,76 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { DebugSessionContributionRegistry, DebugSessionContribution } from '@theia/debug/lib/browser/debug-session-contribution'; +import { injectable, inject, named, postConstruct } from 'inversify'; +import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; +import { Disposable } from '@theia/core/lib/common/disposable'; + +/** + * Debug session contribution registrator. + */ +export interface PluginDebugSessionContributionRegistrator { + /** + * Registers [DebugSessionContribution](#DebugSessionContribution). + * @param contrib contribution + */ + registerDebugSessionContribution(contrib: DebugSessionContribution): Disposable; + + /** + * Unregisters [DebugSessionContribution](#DebugSessionContribution). + * @param debugType the debug type + */ + unregisterDebugSessionContribution(debugType: string): void; +} + +/** + * Plugin debug session contribution registry implementation with functionality + * to register / unregister plugin contributions. + */ +@injectable() +export class PluginDebugSessionContributionRegistry implements DebugSessionContributionRegistry, PluginDebugSessionContributionRegistrator { + protected readonly contribs = new Map(); + + @inject(ContributionProvider) @named(DebugSessionContribution) + protected readonly contributions: ContributionProvider; + + @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); + } + + registerDebugSessionContribution(contrib: DebugSessionContribution): Disposable { + const { debugType } = contrib; + + if (this.contribs.has(debugType)) { + console.warn(`Debug session contribution already registered for ${debugType}`); + return Disposable.NULL; + } + + this.contribs.set(debugType, contrib); + return Disposable.create(() => this.unregisterDebugSessionContribution(debugType)); + } + + unregisterDebugSessionContribution(debugType: string): void { + this.contribs.delete(debugType); + } +} diff --git a/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts b/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts new file mode 100644 index 0000000000000..310e824b6303a --- /dev/null +++ b/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts @@ -0,0 +1,64 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { DefaultDebugSessionFactory, } from '@theia/debug/lib/browser/debug-session-contribution'; +import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; +import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; +import { BreakpointManager } from '@theia/debug/lib/browser/breakpoint/breakpoint-manager'; +import { LabelProvider } from '@theia/core/lib/browser/label-provider'; +import { MessageClient } from '@theia/core/lib/common/message-service-protocol'; +import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; +import { DebugPreferences } from '@theia/debug/lib/browser/debug-preferences'; +import { DebugSessionOptions } from '@theia/debug/lib/browser/debug-session-options'; +import { DebugSession } from '@theia/debug/lib/browser/debug-session'; +import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection'; +import { IWebSocket } from 'vscode-ws-jsonrpc/lib/socket/socket'; + +/** + * Session factory for a client debug session that communicates with debug adapter contributed as plugin. + * The main difference is to use a connection factory that creates [IWebSocket](#IWebSocket) over Rpc channel. + */ +export class PluginDebugSessionFactory extends DefaultDebugSessionFactory { + constructor( + protected readonly terminalService: TerminalService, + protected readonly editorManager: EditorManager, + protected readonly breakpoints: BreakpointManager, + protected readonly labelProvider: LabelProvider, + protected readonly messages: MessageClient, + protected readonly outputChannelManager: OutputChannelManager, + protected readonly debugPreferences: DebugPreferences, + protected readonly connectionFactory: (sessionId: string) => Promise + ) { + super(); + } + + get(sessionId: string, options: DebugSessionOptions): DebugSession { + const connection = new DebugSessionConnection( + sessionId, + this.connectionFactory, + this.getTraceOutputChannel()); + + return new DebugSession( + sessionId, + options, + connection, + this.terminalService, + this.editorManager, + this.breakpoints, + this.labelProvider, + this.messages); + } +} diff --git a/packages/plugin-ext/src/main/browser/keybindings/keybindings-contribution-handler.ts b/packages/plugin-ext/src/main/browser/keybindings/keybindings-contribution-handler.ts index e812f35dae9b1..f51d8cda152ea 100644 --- a/packages/plugin-ext/src/main/browser/keybindings/keybindings-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/keybindings/keybindings-contribution-handler.ts @@ -36,10 +36,13 @@ export class KeybindingsContributionPointHandler { const keybindings = contributions.keybindings; keybindings.forEach(keybinding => { - const keybindingResult = this.keybindingRegistry.getKeybindingsForKeySequence(KeySequence.parse(keybinding.keybinding)); - - this.handleShadingKeybindings(keybinding, keybindingResult.shadow); - this.handlePartialKeybindings(keybinding, keybindingResult.partial); + try { + const keybindingResult = this.keybindingRegistry.getKeybindingsForKeySequence(KeySequence.parse(keybinding.keybinding)); + this.handleShadingKeybindings(keybinding, keybindingResult.shadow); + this.handlePartialKeybindings(keybinding, keybindingResult.partial); + } catch (e) { + this.logger.error(e.message || e); + } }); this.keybindingRegistry.setKeymap(KeybindingScope.USER, keybindings); diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index 1151c1f963da9..95bcbc515aaec 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -35,6 +35,7 @@ import { ConnectionMainImpl } from './connection-main'; import { WebviewsMainImpl } from './webviews-main'; import { TasksMainImpl } from './tasks-main'; import { LanguagesContributionMainImpl } from './languages-contribution-main'; +import { DebugMainImpl } from './debug/debug-main'; export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { const commandRegistryMain = new CommandRegistryMainImpl(rpc, container); @@ -92,4 +93,10 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const languagesContribution = new LanguagesContributionMainImpl(rpc, container, pluginConnection); rpc.set(PLUGIN_RPC_CONTEXT.LANGUAGES_CONTRIBUTION_MAIN, languagesContribution); + + const connectionMain = new ConnectionMainImpl(rpc); + rpc.set(PLUGIN_RPC_CONTEXT.CONNECTION_MAIN, connectionMain); + + const debugMain = new DebugMainImpl(rpc, connectionMain, container); + rpc.set(PLUGIN_RPC_CONTEXT.DEBUG_MAIN, debugMain); } diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index bad748343d972..4aa4d22121083 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -54,6 +54,10 @@ import { LanguageClientProviderImpl } from './language-provider/plugin-language- import { LanguageClientContributionProviderImpl } from './language-provider/language-client-contribution-provider-impl'; import { LanguageClientContributionProvider } from './language-provider/language-client-contribution-provider'; import { StoragePathService } from './storage-path-service'; +import { DebugSessionContributionRegistry } from '@theia/debug/lib/browser/debug-session-contribution'; +import { PluginDebugSessionContributionRegistry } from './debug/plugin-debug-session-contribution-registry'; +import { PluginDebugService } from './debug/plugin-debug-service'; +import { DebugService } from '@theia/debug/lib/common/debug-service'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bindHostedPluginPreferences(bind); @@ -125,4 +129,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(LanguageClientProviderImpl).toSelf().inSingletonScope(); rebind(LanguageClientProvider).toService(LanguageClientProviderImpl); bind(ContextKeyService).to(ContextKeyServiceImpl).inSingletonScope(); + + bind(PluginDebugService).toSelf().inSingletonScope(); + rebind(DebugService).toService(PluginDebugService); + + bind(PluginDebugSessionContributionRegistry).toSelf().inSingletonScope(); + rebind(DebugSessionContributionRegistry).toService(PluginDebugSessionContributionRegistry); }); diff --git a/packages/plugin-ext/src/plugin/command-registry.ts b/packages/plugin-ext/src/plugin/command-registry.ts index e84bbdb24dad9..d7c3f1c7100d7 100644 --- a/packages/plugin-ext/src/plugin/command-registry.ts +++ b/packages/plugin-ext/src/plugin/command-registry.ts @@ -31,9 +31,15 @@ export class CommandRegistryImpl implements CommandRegistryExt { private readonly converter: CommandsConverter; + // tslint:disable-next-line:no-any + private static EMPTY_HANDLER(...args: any[]): Promise { return Promise.resolve(undefined); } + constructor(rpc: RPCProtocol) { this.proxy = rpc.getProxy(Ext.COMMAND_REGISTRY_MAIN); this.converter = new CommandsConverter(this); + + // register internal VS Code commands + this.registerHandler('vscode.previewHtml', CommandRegistryImpl.EMPTY_HANDLER); } getConverter(): CommandsConverter { @@ -50,6 +56,7 @@ export class CommandRegistryImpl implements CommandRegistryExt { this.proxy.$registerCommand(command); return Disposable.create(() => { + this.commands.delete(command.id); this.proxy.$unregisterCommand(command.id); }); @@ -61,6 +68,7 @@ export class CommandRegistryImpl implements CommandRegistryExt { } this.commands.set(commandId, handler); return Disposable.create(() => { + this.commands.delete(commandId); this.proxy.$unregisterCommand(commandId); }); } diff --git a/packages/plugin-ext/src/plugin/node/debug/debug.ts b/packages/plugin-ext/src/plugin/node/debug/debug.ts new file mode 100644 index 0000000000000..12684d0017a09 --- /dev/null +++ b/packages/plugin-ext/src/plugin/node/debug/debug.ts @@ -0,0 +1,287 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { Emitter } from '@theia/core/lib/common/event'; +import { Disposable } from '../../types-impl'; +import { Breakpoint } from '../../../api/model'; +import { RPCProtocol } from '../../../api/rpc-protocol'; +import { + PLUGIN_RPC_CONTEXT as Ext, + DebugMain, + DebugExt +} from '../../../api/plugin-api'; +import * as theia from '@theia/plugin'; +import uuid = require('uuid'); +import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; +import { DebuggerDescription } from '@theia/debug/lib/common/debug-service'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { DebuggerContribution } from '../../../common'; +import { DebugAdapterSessionImpl } from '@theia/debug/lib/node/debug-adapter-session'; +import { ChildProcess, spawn, fork } from 'child_process'; +import { ConnectionExtImpl } from '../../connection-ext'; +import { CommandRegistryImpl } from '../../command-registry'; +import { PluginWebSocketChannel } from '../../../common/connection'; +import { PluginDebugAdapterContribution } from './plugin-debug-adapter-contribution'; +import { CommunicationProvider, DebugAdapterExecutable } from '@theia/debug/lib/common/debug-model'; + +// tslint:disable:no-any + +/** + * It is supposed to work at node. + */ +export class DebugExtImpl implements DebugExt { + // debug sessions by sessionId + private debugSessions = new Map(); + + // contributions by contributorId + private contributions = new Map(); + + private connectionExt: ConnectionExtImpl; + private commandRegistryExt: CommandRegistryImpl; + + private proxy: DebugMain; + private readonly onDidChangeBreakpointsEmitter = new Emitter(); + private readonly onDidChangeActiveDebugSessionEmitter = new Emitter(); + private readonly onDidTerminateDebugSessionEmitter = new Emitter(); + private readonly onDidStartDebugSessionEmitter = new Emitter(); + private readonly onDidReceiveDebugSessionCustomEmitter = new Emitter(); + + activeDebugSession: theia.DebugSession | undefined; + activeDebugConsole: theia.DebugConsole; + breakpoints: theia.Breakpoint[] = []; + + constructor(rpc: RPCProtocol) { + this.proxy = rpc.getProxy(Ext.DEBUG_MAIN); + this.activeDebugConsole = { + append: (value: string) => this.proxy.$appendToDebugConsole(value), + appendLine: (value: string) => this.proxy.$appendLineToDebugConsole(value) + }; + } + + inject(connectionExt: ConnectionExtImpl, commandRegistryExt: CommandRegistryImpl) { + this.connectionExt = connectionExt; + this.commandRegistryExt = commandRegistryExt; + } + + get onDidReceiveDebugSessionCustomEvent(): theia.Event { + return this.onDidReceiveDebugSessionCustomEmitter.event; + } + + get onDidChangeActiveDebugSession(): theia.Event { + return this.onDidChangeActiveDebugSessionEmitter.event; + } + + get onDidTerminateDebugSession(): theia.Event { + return this.onDidTerminateDebugSessionEmitter.event; + } + + get onDidStartDebugSession(): theia.Event { + return this.onDidStartDebugSessionEmitter.event; + } + + get onDidChangeBreakpoints(): theia.Event { + return this.onDidChangeBreakpointsEmitter.event; + } + + addBreakpoints(breakpoints: theia.Breakpoint[]): void { + this.proxy.$addBreakpoints(breakpoints); + } + + removeBreakpoints(breakpoints: theia.Breakpoint[]): void { + this.proxy.$removeBreakpoints(breakpoints); + } + + startDebugging(folder: theia.WorkspaceFolder | undefined, nameOrConfiguration: string | theia.DebugConfiguration): PromiseLike { + return this.proxy.$startDebugging(folder, nameOrConfiguration); + } + + registerDebugConfigurationProvider( + debugType: string, + provider: theia.DebugConfigurationProvider, + debuggerContribution: DebuggerContribution, + pluginPath: string): Disposable { + + const contributionId = uuid.v4(); + const adapterContribution = new PluginDebugAdapterContribution( + debugType, + provider, + debuggerContribution, + this.commandRegistryExt, + pluginPath); + this.contributions.set(contributionId, adapterContribution); + + const description: DebuggerDescription = { type: debugType, label: debuggerContribution.label || debugType }; + this.proxy.$registerDebugConfigurationProvider(contributionId, description); + + return Disposable.create(() => { + this.contributions.delete(contributionId); + this.proxy.$unregisterDebugConfigurationProvider(contributionId); + }); + } + + // tslint:disable-next-line:no-any + $onSessionCustomEvent(sessionId: string, event: string, body?: any): void { + const session = this.debugSessions.get(sessionId); + if (session) { + this.onDidReceiveDebugSessionCustomEmitter.fire({ event, body, session }); + } + } + + $sessionDidCreate(sessionId: string): void { + const session = this.debugSessions.get(sessionId); + if (session) { + this.onDidStartDebugSessionEmitter.fire(session); + } + } + + $sessionDidDestroy(sessionId: string): void { + const session = this.debugSessions.get(sessionId); + if (session) { + this.onDidTerminateDebugSessionEmitter.fire(session); + } + } + + $sessionDidChange(sessionId: string | undefined): void { + const activeDebugSession = sessionId ? this.debugSessions.get(sessionId) : undefined; + this.onDidChangeActiveDebugSessionEmitter.fire(activeDebugSession); + } + + $breakpointsDidChange(all: Breakpoint[], added: Breakpoint[], removed: Breakpoint[], changed: Breakpoint[]): void { + this.breakpoints = all; + this.onDidChangeBreakpointsEmitter.fire({ added, removed, changed }); + } + + async $createDebugSession(contributionId: string, debugConfiguration: theia.DebugConfiguration): Promise { + const adapterContribution = this.contributions.get(contributionId); + if (!adapterContribution) { + throw new Error(`Debug adapter contribution '${contributionId}' not found.`); + } + + const executable = await adapterContribution.provideDebugAdapterExecutable(debugConfiguration); + const communicationProvider = startDebugAdapter(executable); + + const sessionId = uuid.v4(); + const session = new PluginDebugAdapterSession( + sessionId, + debugConfiguration, + communicationProvider, + (command: string, args?: any) => this.proxy.$customRequest(command, args)); + this.debugSessions.set(sessionId, session); + + const connection = await this.connectionExt!.ensureConnection(sessionId); + session.start(new PluginWebSocketChannel(connection)); + + return sessionId; + } + + async $terminateDebugSession(sessionId: string): Promise { + const debugAdapterSession = this.debugSessions.get(sessionId); + if (debugAdapterSession) { + this.debugSessions.delete(sessionId); + return debugAdapterSession.stop(); + } + } + + async $getSupportedLanguages(contributionId: string): Promise { + const contribution = this.contributions.get(contributionId); + if (!contribution) { + throw new Error(`Debug adapter contribution '${contributionId}' not found.`); + } + + return contribution.getSupportedLanguages(); + } + + async $getSchemaAttributes(contributionId: string): Promise { + const contribution = this.contributions.get(contributionId); + if (!contribution) { + throw new Error(`Debug adapter contribution '${contributionId}' not found.`); + } + + return contribution.getSchemaAttributes(); + } + + async $getConfigurationSnippets(contributionId: string): Promise { + const contribution = this.contributions.get(contributionId); + if (!contribution) { + throw new Error(`Debug adapter contribution '${contributionId}' not found.`); + } + + return contribution.getConfigurationSnippets(); + } + + async $provideDebugConfigurations(contributionId: string, folder: string | undefined): Promise { + const contribution = this.contributions.get(contributionId); + if (!contribution) { + throw new Error(`Debug adapter contribution '${contributionId}' not found.`); + } + + return contribution.provideDebugConfigurations(undefined); + } + + async $resolveDebugConfigurations( + contributionId: string, + debugConfiguration: theia.DebugConfiguration, + folder: string | undefined): Promise { + + const contribution = this.contributions.get(contributionId); + if (!contribution) { + throw new Error(`Debug adapter contribution '${contributionId}' not found.`); + } + + return contribution.resolveDebugConfiguration(debugConfiguration, folder); + } +} + +/** + * Server debug session for plugin contribution. + */ +class PluginDebugAdapterSession extends DebugAdapterSessionImpl implements theia.DebugSession { + readonly type: string; + readonly name: string; + + constructor( + readonly id: string, + readonly configuration: theia.DebugConfiguration, + readonly communicationProvider: CommunicationProvider, + readonly customRequest: (command: string, args?: any) => Promise) { + + super(id, communicationProvider); + + this.type = configuration.type; + this.name = configuration.name; + } +} + +/** + * Starts debug adapter process. + */ +function startDebugAdapter(executable: DebugAdapterExecutable): CommunicationProvider { + let childProcess: ChildProcess; + if ('command' in executable) { + const { command, args } = executable; + childProcess = spawn(command, args, { stdio: ['pipe', 'pipe', 2] }) as ChildProcess; + } else if ('modulePath' in executable) { + const { modulePath, args } = executable; + childProcess = fork(modulePath, args, { stdio: ['pipe', 'pipe', 2, 'ipc'] }); + } else { + throw new Error(`It is not possible to launch debug adapter with the command: ${JSON.stringify(executable)}`); + } + + return { + input: childProcess.stdin, + output: childProcess.stdout, + dispose: () => childProcess.kill() + }; +} diff --git a/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-contribution.ts b/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-contribution.ts new file mode 100644 index 0000000000000..36ad7a067dc94 --- /dev/null +++ b/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-contribution.ts @@ -0,0 +1,100 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as theia from '@theia/plugin'; +import * as path from 'path'; +import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration'; +import { PlatformSpecificAdapterContribution, DebuggerContribution } from '../../../common'; +import { CommandRegistryImpl } from '../../command-registry'; +import { IJSONSchemaSnippet, IJSONSchema } from '@theia/core/lib/common/json-schema'; +import { isWindows, isOSX } from '@theia/core/lib/common/os'; +import { DebugAdapterExecutable } from '@theia/debug/lib/common/debug-model'; + +export class PluginDebugAdapterContribution { + constructor( + protected readonly debugType: string, + protected readonly provider: theia.DebugConfigurationProvider, + protected readonly debuggerContribution: DebuggerContribution, + protected readonly commandRegistryExt: CommandRegistryImpl, + protected readonly pluginPath: string) { + } + + async provideDebugConfigurations(workspaceFolderUri?: string): Promise { + if (this.provider.provideDebugConfigurations) { + return await this.provider.provideDebugConfigurations(undefined) || []; + } + + return []; + } + + async resolveDebugConfiguration(config: DebugConfiguration, workspaceFolderUri?: string): Promise { + if (this.provider.resolveDebugConfiguration) { + return this.provider.resolveDebugConfiguration(undefined, config); + } + + return config; + } + + async getSupportedLanguages(): Promise { + return this.debuggerContribution.languages || []; + } + + async provideDebugAdapterExecutable(debugConfiguration: theia.DebugConfiguration): Promise { + if (this.debuggerContribution.adapterExecutableCommand) { + return await this.commandRegistryExt.executeCommand(this.debuggerContribution.adapterExecutableCommand, []) as DebugAdapterExecutable; + } + + const info = this.toPlatformInfo(this.debuggerContribution); + let program = (info && info.program || this.debuggerContribution.program); + if (!program) { + throw new Error('It is not possible to provide debug adapter executable. Program not found.'); + } + program = path.join(this.pluginPath, program); + const programArgs = info && info.args || this.debuggerContribution.args || []; + let runtime = info && info.runtime || this.debuggerContribution.runtime; + if (runtime && runtime.indexOf('./') === 0) { + runtime = path.join(this.pluginPath, runtime); + } + const runtimeArgs = info && info.runtimeArgs || this.debuggerContribution.runtimeArgs || []; + const command = runtime ? runtime : program; + const args = runtime ? [...runtimeArgs, program, ...programArgs] : programArgs; + return { + command, + args + }; + } + + async getSchemaAttributes(): Promise { + return this.debuggerContribution.configurationAttributes || []; + } + + async getConfigurationSnippets(): Promise { + return this.debuggerContribution.configurationSnippets || []; + } + + protected toPlatformInfo(executable: DebuggerContribution): PlatformSpecificAdapterContribution | undefined { + if (isWindows && !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) { + return executable.winx86 || executable.win || executable.windows; + } + if (isWindows) { + return executable.win || executable.windows; + } + if (isOSX) { + return executable.osx; + } + return executable.linux; + } +} diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 3b5945319f842..649105818ebc0 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -84,7 +84,10 @@ import { TaskPanelKind, TaskRevealKind, TaskGroup, - Task + Task, + Breakpoint, + SourceBreakpoint, + FunctionBreakpoint } from './types-impl'; import { SymbolKind } from '../api/model'; import { EditorsAndDocumentsExtImpl } from './editors-and-documents'; @@ -107,12 +110,14 @@ import { LanguagesContributionExtImpl } from './languages-contribution-ext'; import { ConnectionExtImpl } from './connection-ext'; import { WebviewsExtImpl } from './webviews'; import { TasksExtImpl } from './tasks/tasks'; +import { DebugExtImpl } from './node/debug/debug'; +import { DebuggerContribution } from '../common'; export function createAPIFactory( rpc: RPCProtocol, pluginManager: PluginManager, envExt: EnvExtImpl, - connectionExt: ConnectionExtImpl, + debugExt: DebugExtImpl, preferenceRegistryExt: PreferenceRegistryExtImpl): PluginAPIFactory { const commandRegistry = rpc.set(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT, new CommandRegistryImpl(rpc)); @@ -133,10 +138,12 @@ export function createAPIFactory( const treeViewsExt = rpc.set(MAIN_RPC_CONTEXT.TREE_VIEWS_EXT, new TreeViewsExtImpl(rpc, commandRegistry)); const webviewExt = rpc.set(MAIN_RPC_CONTEXT.WEBVIEWS_EXT, new WebviewsExtImpl(rpc)); const tasksExt = rpc.set(MAIN_RPC_CONTEXT.TASKS_EXT, new TasksExtImpl(rpc)); - rpc.set(MAIN_RPC_CONTEXT.CONNECTION_EXT, new ConnectionExtImpl(rpc)); - rpc.set(MAIN_RPC_CONTEXT.CONNECTION_EXT, connectionExt); + const connectionExt = rpc.set(MAIN_RPC_CONTEXT.CONNECTION_EXT, new ConnectionExtImpl(rpc)); const languagesContributionExt = rpc.set(MAIN_RPC_CONTEXT.LANGUAGES_CONTRIBUTION_EXT, new LanguagesContributionExtImpl(rpc, connectionExt)); + debugExt.inject(connectionExt, commandRegistry); + rpc.set(MAIN_RPC_CONTEXT.DEBUG_EXT, debugExt); + return function (plugin: InternalPlugin): typeof theia { const commands: typeof theia.commands = { // tslint:disable-next-line:no-any @@ -497,17 +504,51 @@ export function createAPIFactory( }; const debug: typeof theia.debug = { - onDidChangeActiveDebugSession(listener, thisArg?, disposables?) { - // FIXME: to implement - return new Disposable(() => { }); + get activeDebugSession(): theia.DebugSession | undefined { + return debugExt.activeDebugSession; }, - onDidTerminateDebugSession(listener, thisArg?, disposables?) { - // FIXME: to implement - return new Disposable(() => { }); + get activeDebugConsole(): theia.DebugConsole { + return debugExt.activeDebugConsole; }, - registerDebugConfigurationProvider(debugType: string, provider: theia.DebugConfigurationProvider): theia.Disposable { - // FIXME: to implement - return new Disposable(() => { }); + get breakpoints(): theia.Breakpoint[] { + return debugExt.breakpoints; + }, + get onDidChangeActiveDebugSession(): theia.Event { + return debugExt.onDidChangeActiveDebugSession; + }, + get onDidStartDebugSession(): theia.Event { + return debugExt.onDidStartDebugSession; + }, + get onDidReceiveDebugSessionCustomEvent(): theia.Event { + return debugExt.onDidReceiveDebugSessionCustomEvent; + }, + get onDidTerminateDebugSession(): theia.Event { + return debugExt.onDidTerminateDebugSession; + }, + get onDidChangeBreakpoints(): theia.Event { + return debugExt.onDidChangeBreakpoints; + }, + registerDebugConfigurationProvider(debugType: string, provider: theia.DebugConfigurationProvider): Disposable { + const debuggersContribution = plugin.model.contributes && plugin.model.contributes.debuggers; + if (debuggersContribution) { + const contribution = debuggersContribution.filter((value: DebuggerContribution) => value.type === debugType)[0]; + if (contribution) { + console.info(`Registered debug contribution provider: '${debugType}'`); + return debugExt.registerDebugConfigurationProvider(debugType, provider, contribution, plugin.pluginFolder); + } + } + + console.warn(`There is no package contribution with type ${debugType}`); + return Disposable.create(() => { }); + }, + startDebugging(folder: theia.WorkspaceFolder | undefined, nameOrConfiguration: string | theia.DebugConfiguration): Thenable { + return debugExt.startDebugging(folder, nameOrConfiguration); + }, + addBreakpoints(breakpoints: theia.Breakpoint[]): void { + debugExt.addBreakpoints(breakpoints); + }, + removeBreakpoints(breakpoints: theia.Breakpoint[]): void { + debugExt.removeBreakpoints(breakpoints); } }; @@ -592,7 +633,10 @@ export function createAPIFactory( TaskRevealKind, TaskPanelKind, TaskGroup, - Task + Task, + Breakpoint, + SourceBreakpoint, + FunctionBreakpoint }; }; } diff --git a/packages/plugin-ext/src/plugin/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts index bdc379c6a3240..e65c2343144cb 100644 --- a/packages/plugin-ext/src/plugin/plugin-manager.ts +++ b/packages/plugin-ext/src/plugin/plugin-manager.ts @@ -121,6 +121,8 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { const pluginContext: theia.PluginContext = { extensionPath: plugin.pluginFolder, subscriptions: subscriptions, + globalState: new MementoImpl(), + workspaceState: new MementoImpl(), asAbsolutePath: asAbsolutePath, logPath: logPath, storagePath: storagePath, @@ -181,3 +183,17 @@ function getGlobal() { // tslint:disable-next-line:no-null-keyword return typeof self === 'undefined' ? typeof global === 'undefined' ? null : global : self; } + +class MementoImpl implements theia.Memento { + private readonly m = new Map(); + + get(key: string, defaultValue?: T): T | undefined { + const value = this.m.get(key); + return value || defaultValue; + } + + update(key: string, value: T): PromiseLike { + this.m.set(key, value); + return Promise.resolve(undefined); + } +} diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 833df5f0ed284..a7e77c51ae2c4 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -1652,3 +1652,68 @@ export class Task { } } } + +/** + * The base class of all breakpoint types. + */ +export class Breakpoint { + /** + * Is breakpoint enabled. + */ + enabled: boolean; + /** + * An optional expression for conditional breakpoints. + */ + condition?: string; + /** + * An optional expression that controls how many hits of the breakpoint are ignored. + */ + hitCondition?: string; + /** + * An optional message that gets logged when this breakpoint is hit. Embedded expressions within {} are interpolated by the debug adapter. + */ + logMessage?: string; + + protected constructor(enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { + this.enabled = enabled || false; + this.condition = condition; + this.hitCondition = hitCondition; + this.logMessage = logMessage; + } +} + +/** + * A breakpoint specified by a source location. + */ +export class SourceBreakpoint extends Breakpoint { + /** + * The source and line position of this breakpoint. + */ + location: Location; + + /** + * Create a new breakpoint for a source location. + */ + constructor(location: Location, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { + super(enabled, condition, hitCondition, logMessage); + this.location = location; + } +} + +/** + * A breakpoint specified by a function name. + */ +export class FunctionBreakpoint extends Breakpoint { + /** + * The name of the function to which this breakpoint is attached. + */ + functionName: string; + + /** + * Create a new function breakpoint. + */ + constructor(functionName: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { + super(enabled, condition, hitCondition, logMessage); + this.functionName = functionName; + } +} diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index b4e70106ecb42..0005e7176597d 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -612,22 +612,22 @@ declare module '@theia/plugin' { provideDefinition(document: TextDocument, position: Position, token: CancellationToken | undefined): ProviderResult; } - /** - * The implementation provider interface defines the contract between extensions and - * the go to implementation feature. - */ - export interface ImplementationProvider { + /** + * The implementation provider interface defines the contract between extensions and + * the go to implementation feature. + */ + export interface ImplementationProvider { - /** - * Provide the implementations of the symbol at the given position and document. - * - * @param document The document in which the command was invoked. - * @param position The position at which the command was invoked. - * @param token A cancellation token. - * @return A definition or a thenable that resolves to such. The lack of a result can be - * signaled by returning `undefined` or `null`. - */ - provideImplementation(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + /** + * Provide the implementations of the symbol at the given position and document. + * + * @param document The document in which the command was invoked. + * @param position The position at which the command was invoked. + * @param token A cancellation token. + * @return A definition or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined` or `null`. + */ + provideImplementation(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } /** @@ -2352,6 +2352,18 @@ declare module '@theia/plugin' { */ subscriptions: { dispose(): any }[]; + /** + * A memento object that stores state in the context + * of the currently opened [workspace](#workspace.workspaceFolders). + */ + workspaceState: Memento; + + /** + * A memento object that stores state independent + * of the current opened [workspace](#workspace.workspaceFolders). + */ + globalState: Memento; + /** * The absolute file path of the directory containing the extension. */ @@ -5905,14 +5917,17 @@ declare module '@theia/plugin' { * The type of the debug session. */ type: string; + /** * The name of the debug session. */ name: string; + /** * The request type of the debug session. */ request: string; + /** * Additional debug type specific properties. */ @@ -5920,8 +5935,8 @@ declare module '@theia/plugin' { } /** - * A debug session. - */ + * A debug session. + */ export interface DebugSession { /** @@ -5945,6 +5960,26 @@ declare module '@theia/plugin' { customRequest(command: string, args?: any): PromiseLike; } + /** + * A custom Debug Adapter Protocol event received from a [debug session](#DebugSession). + */ + export interface DebugSessionCustomEvent { + /** + * The [debug session](#DebugSession) for which the custom event was received. + */ + session: DebugSession; + + /** + * Type of event. + */ + event: string; + + /** + * Event specific information. + */ + body?: any; + } + /** * A debug configuration provider allows to add the initial debug configurations to a newly created launch.json * and to resolve a launch configuration before it is used to start a new debug session. @@ -5960,6 +5995,7 @@ declare module '@theia/plugin' { * @return An array of [debug configurations](#DebugConfiguration). */ provideDebugConfigurations?(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult; + /** * Resolves a [debug configuration](#DebugConfiguration) by filling in missing values or by adding/changing/removing attributes. * If more than one debug configuration provider is registered for the same type, the resolveDebugConfiguration calls are chained @@ -5975,11 +6011,122 @@ declare module '@theia/plugin' { resolveDebugConfiguration?(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult; } + /** + * Represents the debug console. + */ + export interface DebugConsole { + /** + * Append the given value to the debug console. + * + * @param value A string, falsy values will not be printed. + */ + append(value: string): void; + + /** + * Append the given value and a line feed character + * to the debug console. + * + * @param value A string, falsy values will be printed. + */ + appendLine(value: string): void; + } + + /** + * An event describing the changes to the set of [breakpoints](#Breakpoint). + */ + export interface BreakpointsChangeEvent { + /** + * Added breakpoints. + */ + readonly added: Breakpoint[]; + + /** + * Removed breakpoints. + */ + readonly removed: Breakpoint[]; + + /** + * Changed breakpoints. + */ + readonly changed: Breakpoint[]; + } + + /** + * The base class of all breakpoint types. + */ + export class Breakpoint { + /** + * Is breakpoint enabled. + */ + readonly enabled: boolean; + /** + * An optional expression for conditional breakpoints. + */ + readonly condition?: string; + /** + * An optional expression that controls how many hits of the breakpoint are ignored. + */ + readonly hitCondition?: string; + /** + * An optional message that gets logged when this breakpoint is hit. Embedded expressions within {} are interpolated by the debug adapter. + */ + readonly logMessage?: string; + + protected constructor(enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string); + } + + /** + * A breakpoint specified by a source location. + */ + export class SourceBreakpoint extends Breakpoint { + /** + * The source and line position of this breakpoint. + */ + readonly location: Location; + + /** + * Create a new breakpoint for a source location. + */ + constructor(location: Location, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string); + } + + /** + * A breakpoint specified by a function name. + */ + export class FunctionBreakpoint extends Breakpoint { + /** + * The name of the function to which this breakpoint is attached. + */ + readonly functionName: string; + + /** + * Create a new function breakpoint. + */ + constructor(functionName: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string); + } + /** * Namespace for debug functionality. */ export namespace debug { + /** + * The currently active [debug session](#DebugSession) or `undefined`. The active debug session is the one + * represented by the debug action floating window or the one currently shown in the drop down menu of the debug action floating window. + * If no debug session is active, the value is `undefined`. + */ + export let activeDebugSession: DebugSession | undefined; + + /** + * The currently active [debug console](#DebugConsole). + */ + export let activeDebugConsole: DebugConsole; + + /** + * List of breakpoints. + */ + export let breakpoints: Breakpoint[]; + /** * An [event](#Event) which fires when the [active debug session](#debug.activeDebugSession) * has changed. *Note* that the event also fires when the active debug session changes @@ -5987,11 +6134,26 @@ declare module '@theia/plugin' { */ export const onDidChangeActiveDebugSession: Event; + /** + * An [event](#Event) which fires when a new [debug session](#DebugSession) has been started. + */ + export const onDidStartDebugSession: Event; + + /** + * An [event](#Event) which fires when a custom DAP event is received from the [debug session](#DebugSession). + */ + export const onDidReceiveDebugSessionCustomEvent: Event; + /** * An [event](#Event) which fires when a [debug session](#DebugSession) has terminated. */ export const onDidTerminateDebugSession: Event; + /** + * An [event](#Event) that is emitted when the set of breakpoints is added, removed, or changed. + */ + export const onDidChangeBreakpoints: Event; + /** * Register a [debug configuration provider](#DebugConfigurationProvider) for a specific debug type. * More than one provider can be registered for the same type. @@ -6001,6 +6163,30 @@ declare module '@theia/plugin' { * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerDebugConfigurationProvider(debugType: string, provider: DebugConfigurationProvider): Disposable; + + /** + * Start debugging by using either a named launch or named compound configuration, + * or by directly passing a [DebugConfiguration](#DebugConfiguration). + * The named configurations are looked up in '.vscode/launch.json' found in the given folder. + * Before debugging starts, all unsaved files are saved and the launch configurations are brought up-to-date. + * Folder specific variables used in the configuration (e.g. '${workspaceFolder}') are resolved against the given folder. + * @param folder The [workspace folder](#WorkspaceFolder) for looking up named configurations and resolving variables or `undefined` for a non-folder setup. + * @param nameOrConfiguration Either the name of a debug or compound configuration or a [DebugConfiguration](#DebugConfiguration) object. + * @return A thenable that resolves when debugging could be successfully started. + */ + export function startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration): PromiseLike; + + /** + * Add breakpoints. + * @param breakpoints The breakpoints to add. + */ + export function addBreakpoints(breakpoints: Breakpoint[]): void; + + /** + * Remove breakpoints. + * @param breakpoints The breakpoints to remove. + */ + export function removeBreakpoints(breakpoints: Breakpoint[]): void; } /** @@ -6273,7 +6459,7 @@ declare module '@theia/plugin' { Never = 3 } - /** Controls how the task channel is used between tasks */ + /** Controls how the task channel is used between tasks */ export enum TaskPanelKind { /** Shares a panel with other tasks. This is the default. */ @@ -6408,4 +6594,37 @@ declare module '@theia/plugin' { */ export function registerTaskProvider(type: string, provider: TaskProvider): Disposable; } + + /** + * A memento represents a storage utility. It can store and retrieve + * values. + */ + export interface Memento { + + /** + * Return a value. + * + * @param key A string. + * @return The stored value or `undefined`. + */ + get(key: string): T | undefined; + + /** + * Return a value. + * + * @param key A string. + * @param defaultValue A value that should be returned when there is no + * value (`undefined`) with the given key. + * @return The stored value or the defaultValue. + */ + get(key: string, defaultValue: T): T; + + /** + * Store a value. The value must be JSON-stringifyable. + * + * @param key A string. + * @param value A value. MUST not contain cyclic references. + */ + update(key: string, value: any): PromiseLike; + } }