mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-15 15:26:32 +00:00
Sketchbook sidebar state (#1102)
* add commands to open sketchbook widgets add commands to show sketchbook widgets * enable sending commands via query params * opening sketch in new window will open sketchbook * requested changes * add specific method WorkspaceService to open sketch with commands * add encoded commands contribution * try merge show sketchbook commands * pair session changes. Signed-off-by: Akos Kitta <a.kitta@arduino.cc> * i18n fixup. Signed-off-by: Akos Kitta <a.kitta@arduino.cc> * minimized scope of hacky code. Signed-off-by: Akos Kitta <a.kitta@arduino.cc> * clean up OPEN_NEW_WINDOW command * add comment on workspace-service.ts * reveal node with URI Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
parent
5da558dfd9
commit
087cab177b
@ -302,6 +302,7 @@ import { CoreErrorHandler } from './contributions/core-error-handler';
|
|||||||
import { CompilerErrors } from './contributions/compiler-errors';
|
import { CompilerErrors } from './contributions/compiler-errors';
|
||||||
import { WidgetManager } from './theia/core/widget-manager';
|
import { WidgetManager } from './theia/core/widget-manager';
|
||||||
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
|
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
|
||||||
|
import { StartupTask } from './widgets/sketchbook/startup-task';
|
||||||
|
|
||||||
MonacoThemingService.register({
|
MonacoThemingService.register({
|
||||||
id: 'arduino-theme',
|
id: 'arduino-theme',
|
||||||
@ -698,6 +699,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
Contribution.configure(bind, PlotterFrontendContribution);
|
Contribution.configure(bind, PlotterFrontendContribution);
|
||||||
Contribution.configure(bind, Format);
|
Contribution.configure(bind, Format);
|
||||||
Contribution.configure(bind, CompilerErrors);
|
Contribution.configure(bind, CompilerErrors);
|
||||||
|
Contribution.configure(bind, StartupTask);
|
||||||
|
|
||||||
// Disabled the quick-pick customization from Theia when multiple formatters are available.
|
// Disabled the quick-pick customization from Theia when multiple formatters are available.
|
||||||
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.
|
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.
|
||||||
|
@ -9,7 +9,10 @@ import { FrontendApplication } from '@theia/core/lib/browser/frontend-applicatio
|
|||||||
import { FocusTracker, Widget } from '@theia/core/lib/browser';
|
import { FocusTracker, Widget } from '@theia/core/lib/browser';
|
||||||
import { DEFAULT_WINDOW_HASH } from '@theia/core/lib/common/window';
|
import { DEFAULT_WINDOW_HASH } from '@theia/core/lib/common/window';
|
||||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||||
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
import {
|
||||||
|
WorkspaceInput,
|
||||||
|
WorkspaceService as TheiaWorkspaceService,
|
||||||
|
} from '@theia/workspace/lib/browser/workspace-service';
|
||||||
import { ConfigService } from '../../../common/protocol/config-service';
|
import { ConfigService } from '../../../common/protocol/config-service';
|
||||||
import {
|
import {
|
||||||
SketchesService,
|
SketchesService,
|
||||||
@ -17,6 +20,8 @@ import {
|
|||||||
} from '../../../common/protocol/sketches-service';
|
} from '../../../common/protocol/sketches-service';
|
||||||
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
|
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
|
||||||
import { BoardsConfig } from '../../boards/boards-config';
|
import { BoardsConfig } from '../../boards/boards-config';
|
||||||
|
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||||
|
import { StartupTask } from '../../widgets/sketchbook/startup-task';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class WorkspaceService extends TheiaWorkspaceService {
|
export class WorkspaceService extends TheiaWorkspaceService {
|
||||||
@ -82,13 +87,75 @@ export class WorkspaceService extends TheiaWorkspaceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override openNewWindow(workspacePath: string): void {
|
/**
|
||||||
|
* Copied from Theia as-is to be able to pass the original `options` down.
|
||||||
|
*/
|
||||||
|
protected override async doOpen(
|
||||||
|
uri: URI,
|
||||||
|
options?: WorkspaceInput
|
||||||
|
): Promise<URI | undefined> {
|
||||||
|
const stat = await this.toFileStat(uri);
|
||||||
|
if (stat) {
|
||||||
|
if (!stat.isDirectory && !this.isWorkspaceFile(stat)) {
|
||||||
|
const message = `Not a valid workspace: ${uri.path.toString()}`;
|
||||||
|
this.messageService.error(message);
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
// The same window has to be preserved too (instead of opening a new one), if the workspace root is not yet available and we are setting it for the first time.
|
||||||
|
// Option passed as parameter has the highest priority (for api developers), then the preference, then the default.
|
||||||
|
await this.roots;
|
||||||
|
const { preserveWindow } = {
|
||||||
|
preserveWindow:
|
||||||
|
this.preferences['workspace.preserveWindow'] || !this.opened,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
await this.server.setMostRecentlyUsedWorkspace(uri.toString());
|
||||||
|
if (preserveWindow) {
|
||||||
|
this._workspace = stat;
|
||||||
|
}
|
||||||
|
this.openWindow(stat, Object.assign(options ?? {}, { preserveWindow })); // Unlike Theia, IDE2 passes the whole `input` downstream and not only { preserveWindow }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
'Invalid workspace root URI. Expected an existing directory or workspace file.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copied from Theia. Can pass the `options` further down the chain.
|
||||||
|
*/
|
||||||
|
protected override openWindow(uri: FileStat, options?: WorkspaceInput): void {
|
||||||
|
const workspacePath = uri.resource.path.toString();
|
||||||
|
if (this.shouldPreserveWindow(options)) {
|
||||||
|
this.reloadWindow();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
this.openNewWindow(workspacePath, options); // Unlike Theia, IDE2 passes the `input` downstream.
|
||||||
|
} catch (error) {
|
||||||
|
// Fall back to reloading the current window in case the browser has blocked the new window
|
||||||
|
this._workspace = uri;
|
||||||
|
this.logger.error(error.toString()).then(() => this.reloadWindow());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override openNewWindow(
|
||||||
|
workspacePath: string,
|
||||||
|
options?: WorkspaceInput
|
||||||
|
): void {
|
||||||
const { boardsConfig } = this.boardsServiceProvider;
|
const { boardsConfig } = this.boardsServiceProvider;
|
||||||
const url = BoardsConfig.Config.setConfig(
|
const url = BoardsConfig.Config.setConfig(
|
||||||
boardsConfig,
|
boardsConfig,
|
||||||
new URL(window.location.href)
|
new URL(window.location.href)
|
||||||
); // Set the current boards config for the new browser window.
|
); // Set the current boards config for the new browser window.
|
||||||
url.hash = workspacePath;
|
url.hash = workspacePath;
|
||||||
|
if (StartupTask.WorkspaceInput.is(options)) {
|
||||||
|
url.searchParams.set(
|
||||||
|
StartupTask.QUERY_STRING,
|
||||||
|
encodeURIComponent(JSON.stringify(options.tasks))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.windowService.openNewWindow(url.toString());
|
this.windowService.openNewWindow(url.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,10 @@ import {
|
|||||||
} from '@theia/core/lib/browser/preferences/preference-service';
|
} from '@theia/core/lib/browser/preferences/preference-service';
|
||||||
import { ArduinoMenus, PlaceholderMenuNode } from '../../menu/arduino-menus';
|
import { ArduinoMenus, PlaceholderMenuNode } from '../../menu/arduino-menus';
|
||||||
import { SketchbookCommands } from '../sketchbook/sketchbook-commands';
|
import { SketchbookCommands } from '../sketchbook/sketchbook-commands';
|
||||||
import { CurrentSketch, SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
|
import {
|
||||||
|
CurrentSketch,
|
||||||
|
SketchesServiceClientImpl,
|
||||||
|
} from '../../../common/protocol/sketches-service-client-impl';
|
||||||
import { Contribution } from '../../contributions/contribution';
|
import { Contribution } from '../../contributions/contribution';
|
||||||
import { ArduinoPreferences } from '../../arduino-preferences';
|
import { ArduinoPreferences } from '../../arduino-preferences';
|
||||||
import { MainMenuManager } from '../../../common/main-menu-manager';
|
import { MainMenuManager } from '../../../common/main-menu-manager';
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
import { Command } from '@theia/core/lib/common/command';
|
import { Command } from '@theia/core/lib/common/command';
|
||||||
|
|
||||||
export namespace SketchbookCommands {
|
export namespace SketchbookCommands {
|
||||||
|
export const TOGGLE_SKETCHBOOK_WIDGET: Command = {
|
||||||
|
id: 'arduino-sketchbook-widget:toggle',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const REVEAL_SKETCH_NODE: Command = {
|
||||||
|
id: 'arduino-sketchbook--reveal-sketch-node',
|
||||||
|
};
|
||||||
|
|
||||||
export const OPEN_NEW_WINDOW = Command.toLocalizedCommand(
|
export const OPEN_NEW_WINDOW = Command.toLocalizedCommand(
|
||||||
{
|
{
|
||||||
id: 'arduino-sketchbook--open-sketch-new-window',
|
id: 'arduino-sketchbook--open-sketch-new-window',
|
||||||
|
@ -29,6 +29,7 @@ import {
|
|||||||
} from '../../../common/protocol/sketches-service-client-impl';
|
} from '../../../common/protocol/sketches-service-client-impl';
|
||||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||||
import { URI } from '../../contributions/contribution';
|
import { URI } from '../../contributions/contribution';
|
||||||
|
import { WorkspaceInput } from '@theia/workspace/lib/browser';
|
||||||
|
|
||||||
export const SKETCHBOOK__CONTEXT = ['arduino-sketchbook--context'];
|
export const SKETCHBOOK__CONTEXT = ['arduino-sketchbook--context'];
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ export class SketchbookWidgetContribution
|
|||||||
area: 'left',
|
area: 'left',
|
||||||
rank: 1,
|
rank: 1,
|
||||||
},
|
},
|
||||||
toggleCommandId: 'arduino-sketchbook-widget:toggle',
|
toggleCommandId: SketchbookCommands.TOGGLE_SKETCHBOOK_WIDGET.id,
|
||||||
toggleKeybinding: 'CtrlCmd+Shift+B',
|
toggleKeybinding: 'CtrlCmd+Shift+B',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -100,11 +101,12 @@ export class SketchbookWidgetContribution
|
|||||||
|
|
||||||
override registerCommands(registry: CommandRegistry): void {
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
super.registerCommands(registry);
|
super.registerCommands(registry);
|
||||||
|
registry.registerCommand(SketchbookCommands.REVEAL_SKETCH_NODE, {
|
||||||
|
execute: (treeWidgetId: string, nodeUri: string) =>
|
||||||
|
this.revealSketchNode(treeWidgetId, nodeUri),
|
||||||
|
});
|
||||||
registry.registerCommand(SketchbookCommands.OPEN_NEW_WINDOW, {
|
registry.registerCommand(SketchbookCommands.OPEN_NEW_WINDOW, {
|
||||||
execute: async (arg) => {
|
execute: (arg) => this.openNewWindow(arg.node),
|
||||||
return this.workspaceService.open(arg.node.uri);
|
|
||||||
},
|
|
||||||
isEnabled: (arg) =>
|
isEnabled: (arg) =>
|
||||||
!!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
|
!!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
|
||||||
isVisible: (arg) =>
|
isVisible: (arg) =>
|
||||||
@ -197,7 +199,7 @@ export class SketchbookWidgetContribution
|
|||||||
|
|
||||||
// unregister main menu action
|
// unregister main menu action
|
||||||
registry.unregisterMenuAction({
|
registry.unregisterMenuAction({
|
||||||
commandId: 'arduino-sketchbook-widget:toggle',
|
commandId: SketchbookCommands.TOGGLE_SKETCHBOOK_WIDGET.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.registerMenuAction(SKETCHBOOK__CONTEXT__MAIN_GROUP, {
|
registry.registerMenuAction(SKETCHBOOK__CONTEXT__MAIN_GROUP, {
|
||||||
@ -207,6 +209,28 @@ export class SketchbookWidgetContribution
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openNewWindow(node: SketchbookTree.SketchDirNode): void {
|
||||||
|
const widget = this.tryGetWidget();
|
||||||
|
if (widget) {
|
||||||
|
const treeWidgetId = widget.activeTreeWidgetId();
|
||||||
|
if (!treeWidgetId) {
|
||||||
|
console.warn(`Could not retrieve active sketchbook tree ID.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nodeUri = node.uri.toString();
|
||||||
|
const options: WorkspaceInput = {};
|
||||||
|
Object.assign(options, {
|
||||||
|
tasks: [
|
||||||
|
{
|
||||||
|
command: SketchbookCommands.REVEAL_SKETCH_NODE.id,
|
||||||
|
args: [treeWidgetId, nodeUri],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return this.workspaceService.open(node.uri, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reveals and selects node in the file navigator to which given widget is related.
|
* Reveals and selects node in the file navigator to which given widget is related.
|
||||||
* Does nothing if given widget undefined or doesn't have related resource.
|
* Does nothing if given widget undefined or doesn't have related resource.
|
||||||
@ -230,4 +254,17 @@ export class SketchbookWidgetContribution
|
|||||||
protected onCurrentWidgetChangedHandler(): void {
|
protected onCurrentWidgetChangedHandler(): void {
|
||||||
this.selectWidgetFileNode(this.shell.currentWidget);
|
this.selectWidgetFileNode(this.shell.currentWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async revealSketchNode(
|
||||||
|
treeWidgetId: string,
|
||||||
|
nodeUIri: string
|
||||||
|
): Promise<void> {
|
||||||
|
return this.widget
|
||||||
|
.then((widget) => this.shell.activateWidget(widget.id))
|
||||||
|
.then((widget) => {
|
||||||
|
if (widget instanceof SketchbookWidget) {
|
||||||
|
return widget.revealSketchNode(treeWidgetId, nodeUIri);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
import {
|
||||||
|
inject,
|
||||||
|
injectable,
|
||||||
|
postConstruct,
|
||||||
|
} from '@theia/core/shared/inversify';
|
||||||
import { toArray } from '@theia/core/shared/@phosphor/algorithm';
|
import { toArray } from '@theia/core/shared/@phosphor/algorithm';
|
||||||
import { IDragEvent } from '@theia/core/shared/@phosphor/dragdrop';
|
import { IDragEvent } from '@theia/core/shared/@phosphor/dragdrop';
|
||||||
import { DockPanel, Widget } from '@theia/core/shared/@phosphor/widgets';
|
import { DockPanel, Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||||
@ -7,6 +11,8 @@ import { Disposable } from '@theia/core/lib/common/disposable';
|
|||||||
import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
|
import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
|
||||||
import { SketchbookTreeWidget } from './sketchbook-tree-widget';
|
import { SketchbookTreeWidget } from './sketchbook-tree-widget';
|
||||||
import { nls } from '@theia/core/lib/common';
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { CloudSketchbookCompositeWidget } from '../cloud-sketchbook/cloud-sketchbook-composite-widget';
|
||||||
|
import { URI } from '../../contributions/contribution';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class SketchbookWidget extends BaseWidget {
|
export class SketchbookWidget extends BaseWidget {
|
||||||
@ -45,6 +51,57 @@ export class SketchbookWidget extends BaseWidget {
|
|||||||
return this.localSketchbookTreeWidget;
|
return this.localSketchbookTreeWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activeTreeWidgetId(): string | undefined {
|
||||||
|
const selectedTreeWidgets = toArray(
|
||||||
|
this.sketchbookTreesContainer.selectedWidgets()
|
||||||
|
).map(({ id }) => id);
|
||||||
|
if (selectedTreeWidgets.length > 1) {
|
||||||
|
console.warn(
|
||||||
|
`Found multiple selected tree widgets: ${JSON.stringify(
|
||||||
|
selectedTreeWidgets
|
||||||
|
)}. Expected only one.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return selectedTreeWidgets.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
async revealSketchNode(treeWidgetId: string, nodeUri: string): Promise<void> {
|
||||||
|
const widget = toArray(this.sketchbookTreesContainer.widgets())
|
||||||
|
.filter(({ id }) => id === treeWidgetId)
|
||||||
|
.shift();
|
||||||
|
if (!widget) {
|
||||||
|
console.warn(`Could not find tree widget with ID: ${widget}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: remove this when the remote/local sketchbooks and their widgets are cleaned up.
|
||||||
|
const findTreeWidget = (
|
||||||
|
widget: Widget | undefined
|
||||||
|
): SketchbookTreeWidget | undefined => {
|
||||||
|
if (widget instanceof SketchbookTreeWidget) {
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
if (widget instanceof CloudSketchbookCompositeWidget) {
|
||||||
|
return widget.getTreeWidget();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
const treeWidget = findTreeWidget(
|
||||||
|
toArray(this.sketchbookTreesContainer.widgets())
|
||||||
|
.filter(({ id }) => id === treeWidgetId)
|
||||||
|
.shift()
|
||||||
|
);
|
||||||
|
if (!treeWidget) {
|
||||||
|
console.warn(`Could not find tree widget with ID: ${treeWidget}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.sketchbookTreesContainer.activateWidget(widget);
|
||||||
|
|
||||||
|
const treeNode = await treeWidget.model.revealFile(new URI(nodeUri));
|
||||||
|
if (!treeNode) {
|
||||||
|
console.warn(`Could not find tree node with URI: ${nodeUri}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override onActivateRequest(message: Message): void {
|
protected override onActivateRequest(message: Message): void {
|
||||||
super.onActivateRequest(message);
|
super.onActivateRequest(message);
|
||||||
|
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { WorkspaceInput as TheiaWorkspaceInput } from '@theia/workspace/lib/browser';
|
||||||
|
import { Contribution } from '../../contributions/contribution';
|
||||||
|
|
||||||
|
export interface Task {
|
||||||
|
command: string;
|
||||||
|
/**
|
||||||
|
* This must be JSON serializable.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
args?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class StartupTask extends Contribution {
|
||||||
|
override onReady(): void {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const encoded = params.get(StartupTask.QUERY_STRING);
|
||||||
|
if (!encoded) return;
|
||||||
|
|
||||||
|
const commands = JSON.parse(decodeURIComponent(encoded));
|
||||||
|
|
||||||
|
if (Array.isArray(commands)) {
|
||||||
|
commands.forEach(({ command, args }) => {
|
||||||
|
this.commandService.executeCommand(command, ...args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export namespace StartupTask {
|
||||||
|
export const QUERY_STRING = 'startupTasks';
|
||||||
|
export interface WorkspaceInput extends TheiaWorkspaceInput {
|
||||||
|
tasks: Task[];
|
||||||
|
}
|
||||||
|
export namespace WorkspaceInput {
|
||||||
|
export function is(
|
||||||
|
input: (TheiaWorkspaceInput & Partial<WorkspaceInput>) | undefined
|
||||||
|
): input is WorkspaceInput {
|
||||||
|
return !!input && !!input.tasks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user