feat: use new debug -I -P CLI output

- Can pick a programmer if missing,
 - Can auto-select a programmer on app start,
 - Can edit the `launch.json`,
 - Adjust board discovery to new gRPC API. From now on, it's a client
 read stream, not a duplex.
 - Allow `.cxx` and `.cc` file extensions. (Closes #2265)
 - Drop `debuggingSupported` from `BoardDetails`.
 - Dedicated service endpoint for checking the debugger.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta
2023-11-08 12:04:44 +01:00
committed by Akos Kitta
parent 42bf1a0e99
commit 73b6dc4774
48 changed files with 4697 additions and 3041 deletions

View File

@@ -1,44 +1,44 @@
import debounce from 'p-debounce';
import { inject, injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { DebugConfiguration } from '@theia/debug/lib/common/debug-common';
import { DebugConfigurationModel as TheiaDebugConfigurationModel } from '@theia/debug/lib/browser/debug-configuration-model';
import { Disposable } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
import { DebugConfigurationModel as TheiaDebugConfigurationModel } from '@theia/debug/lib/browser/debug-configuration-model';
import { DebugConfiguration } from '@theia/debug/lib/common/debug-common';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import {
FileOperationError,
FileOperationResult,
} from '@theia/filesystem/lib/common/files';
import debounce from 'p-debounce';
import { SketchesService } from '../../../common/protocol';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../sketches-service-client-impl';
import { maybeUpdateReadOnlyState } from '../monaco/monaco-editor-provider';
import { DebugConfigurationModel } from './debug-configuration-model';
import {
FileOperationError,
FileOperationResult,
} from '@theia/filesystem/lib/common/files';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
@injectable()
export class DebugConfigurationManager extends TheiaDebugConfigurationManager {
@inject(SketchesService)
protected readonly sketchesService: SketchesService;
private readonly sketchesService: SketchesService;
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
private readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
private readonly appStateService: FrontendApplicationStateService;
@inject(FileService)
protected readonly fileService: FileService;
private readonly fileService: FileService;
protected onTempContentDidChangeEmitter =
private onTempContentDidChangeEmitter =
new Emitter<TheiaDebugConfigurationModel.JsonContent>();
get onTempContentDidChange(): Event<TheiaDebugConfigurationModel.JsonContent> {
return this.onTempContentDidChangeEmitter.event;
}
protected override async doInit(): Promise<void> {
this.watchLaunchConfigEditor();
this.appStateService.reachedState('ready').then(async () => {
const tempContent = await this.getTempLaunchJsonContent();
if (!tempContent) {
@@ -75,6 +75,19 @@ export class DebugConfigurationManager extends TheiaDebugConfigurationManager {
return super.doInit();
}
/**
* Sets a listener on current sketch change, and maybe updates the readonly state of the editor showing the debug configuration. aka the `launch.json`.
*/
private watchLaunchConfigEditor(): Disposable {
return this.sketchesServiceClient.onCurrentSketchDidChange(() => {
for (const widget of this.editorManager.all) {
maybeUpdateReadOnlyState(widget, (uri) =>
this.sketchesServiceClient.isReadOnly(uri)
);
}
});
}
protected override updateModels = debounce(async () => {
await this.appStateService.reachedState('ready');
const roots = await this.workspaceService.roots;
@@ -111,7 +124,7 @@ export class DebugConfigurationManager extends TheiaDebugConfigurationManager {
this.updateCurrent();
}, 500);
protected async getTempLaunchJsonContent(): Promise<
private async getTempLaunchJsonContent(): Promise<
(TheiaDebugConfigurationModel.JsonContent & { uri: URI }) | URI | undefined
> {
const sketch = await this.sketchesServiceClient.currentSketch();

View File

@@ -0,0 +1,57 @@
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import React from '@theia/core/shared/react';
import { DebugAction } from '@theia/debug/lib/browser/view/debug-action';
import { DebugConfigurationSelect as TheiaDebugConfigurationSelect } from '@theia/debug/lib/browser/view/debug-configuration-select';
import { DebugConfigurationWidget as TheiaDebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
/**
* Patched to programmatically update the debug config <select> in the widget.
*/
@injectable()
export class DebugConfigurationWidget extends TheiaDebugConfigurationWidget {
override render(): React.ReactNode {
return (
<React.Fragment>
<DebugAction
run={this.start}
label={nls.localizeByDefault('Start Debugging')}
iconClass="debug-start"
ref={this.setStepRef}
/>
{/* The customized select component that will refresh when the config manager did change */}
<DebugConfigurationSelect
manager={this.manager}
quickInputService={this.quickInputService}
isMultiRoot={this.workspaceService.isMultiRootWorkspaceOpened}
/>
<DebugAction
run={this.openConfiguration}
label={nls.localizeByDefault('Open {0}', '"launch.json"')}
iconClass="settings-gear"
/>
<DebugAction
run={this.openConsole}
label={nls.localizeByDefault('Debug Console')}
iconClass="terminal"
/>
</React.Fragment>
);
}
}
class DebugConfigurationSelect extends TheiaDebugConfigurationSelect {
private readonly toDisposeOnUnmount = new DisposableCollection();
override componentDidMount(): void {
super.componentDidMount();
this.toDisposeOnUnmount.push(
this['manager'].onDidChange(() => this.refreshDebugConfigurations())
);
}
override componentWillUnmount(): void {
this.toDisposeOnUnmount.dispose();
}
}

View File

@@ -0,0 +1,39 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { DebugSession } from '@theia/debug/lib/browser/debug-session';
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import deepEqual from 'fast-deep-equal';
@injectable()
export class DebugSessionManager extends TheiaDebugSessionManager {
@inject(WorkspaceService)
private readonly workspaceService: WorkspaceService;
protected override doStart(
sessionId: string,
options: DebugConfigurationSessionOptions
): Promise<DebugSession> {
this.syncCurrentOptions(options);
return super.doStart(sessionId, options);
}
/**
* If the debug config manager knows about the currently started options, and it's not the currently selected one, select it.
*/
private syncCurrentOptions(options: DebugConfigurationSessionOptions): void {
const knownConfigOptions = this.debugConfigurationManager.find(
options.configuration,
options.workspaceFolderUri ??
this.workspaceService
.tryGetRoots()
.map((stat) => stat.resource.toString())[0]
);
if (
knownConfigOptions &&
!deepEqual(knownConfigOptions, this.debugConfigurationManager.current)
) {
this.debugConfigurationManager.current = knownConfigOptions;
}
}
}

View File

@@ -1,17 +1,20 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { LOCKED_CLASS, lock } from '@theia/core/lib/browser/widgets/widget';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import URI from '@theia/core/lib/common/uri';
import { Title, Widget } from '@theia/core/shared/@phosphor/widgets';
import { inject, injectable } from '@theia/core/shared/inversify';
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
import * as monaco from '@theia/monaco-editor-core';
import type { ReferencesModel } from '@theia/monaco-editor-core/esm/vs/editor/contrib/gotoSymbol/browser/referencesModel';
import {
EditorServiceOverrides,
MonacoEditor,
} from '@theia/monaco/lib/browser/monaco-editor';
import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import { SketchesServiceClientImpl } from '../../sketches-service-client-impl';
import * as monaco from '@theia/monaco-editor-core';
import type { ReferencesModel } from '@theia/monaco-editor-core/esm/vs/editor/contrib/gotoSymbol/browser/referencesModel';
type CancelablePromise = Promise<ReferencesModel> & {
cancel: () => void;
@@ -101,3 +104,30 @@ export class MonacoEditorProvider extends TheiaMonacoEditorProvider {
editor.updateOptions({ readOnly });
}
}
// Theia cannot dynamically set an editor to writable once it was readonly.
export function maybeUpdateReadOnlyState(
widget: EditorWidget,
isReadOnly: (uri: string | URI | monaco.Uri) => boolean
): void {
const editor = widget.editor;
if (!(editor instanceof MonacoEditor)) {
return;
}
const model = editor.document;
const oldReadOnly = model.readOnly;
const resource = model['resource'];
const newReadOnly = Boolean(resource.isReadonly) || isReadOnly(resource.uri);
if (oldReadOnly !== newReadOnly) {
editor.getControl().updateOptions({ readOnly: newReadOnly });
if (newReadOnly) {
lock(widget.title);
} else {
unlock(widget.title);
}
}
}
function unlock(title: Title<Widget>): void {
title.className = title.className.replace(LOCKED_CLASS, '').trim();
}

View File

@@ -77,7 +77,7 @@ class MaybeReadonlyMonacoEditorModel extends SilentMonacoEditorModel {
}
this._dirty = dirty;
if (dirty === false) {
(this as any).updateSavedVersionId();
this['updateSavedVersionId']();
}
this.onDirtyChangedEmitter.fire(undefined);
}