Skip to content

Commit 087cab1

Browse files
Alberto IannacconeAkos Kitta
Alberto Iannaccone
and
Akos Kitta
authored
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 <[email protected]> * i18n fixup. Signed-off-by: Akos Kitta <[email protected]> * minimized scope of hacky code. Signed-off-by: Akos Kitta <[email protected]> * clean up OPEN_NEW_WINDOW command * add comment on workspace-service.ts * reveal node with URI Co-authored-by: Akos Kitta <[email protected]>
1 parent 5da558d commit 087cab1

File tree

7 files changed

+226
-10
lines changed

7 files changed

+226
-10
lines changed

Diff for: arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ import { CoreErrorHandler } from './contributions/core-error-handler';
302302
import { CompilerErrors } from './contributions/compiler-errors';
303303
import { WidgetManager } from './theia/core/widget-manager';
304304
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
305+
import { StartupTask } from './widgets/sketchbook/startup-task';
305306

306307
MonacoThemingService.register({
307308
id: 'arduino-theme',
@@ -698,6 +699,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
698699
Contribution.configure(bind, PlotterFrontendContribution);
699700
Contribution.configure(bind, Format);
700701
Contribution.configure(bind, CompilerErrors);
702+
Contribution.configure(bind, StartupTask);
701703

702704
// Disabled the quick-pick customization from Theia when multiple formatters are available.
703705
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.

Diff for: arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts

+69-2
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@ import { FrontendApplication } from '@theia/core/lib/browser/frontend-applicatio
99
import { FocusTracker, Widget } from '@theia/core/lib/browser';
1010
import { DEFAULT_WINDOW_HASH } from '@theia/core/lib/common/window';
1111
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
12-
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
12+
import {
13+
WorkspaceInput,
14+
WorkspaceService as TheiaWorkspaceService,
15+
} from '@theia/workspace/lib/browser/workspace-service';
1316
import { ConfigService } from '../../../common/protocol/config-service';
1417
import {
1518
SketchesService,
1619
Sketch,
1720
} from '../../../common/protocol/sketches-service';
1821
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
1922
import { BoardsConfig } from '../../boards/boards-config';
23+
import { FileStat } from '@theia/filesystem/lib/common/files';
24+
import { StartupTask } from '../../widgets/sketchbook/startup-task';
2025

2126
@injectable()
2227
export class WorkspaceService extends TheiaWorkspaceService {
@@ -82,13 +87,75 @@ export class WorkspaceService extends TheiaWorkspaceService {
8287
}
8388
}
8489

85-
protected override openNewWindow(workspacePath: string): void {
90+
/**
91+
* Copied from Theia as-is to be able to pass the original `options` down.
92+
*/
93+
protected override async doOpen(
94+
uri: URI,
95+
options?: WorkspaceInput
96+
): Promise<URI | undefined> {
97+
const stat = await this.toFileStat(uri);
98+
if (stat) {
99+
if (!stat.isDirectory && !this.isWorkspaceFile(stat)) {
100+
const message = `Not a valid workspace: ${uri.path.toString()}`;
101+
this.messageService.error(message);
102+
throw new Error(message);
103+
}
104+
// 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.
105+
// Option passed as parameter has the highest priority (for api developers), then the preference, then the default.
106+
await this.roots;
107+
const { preserveWindow } = {
108+
preserveWindow:
109+
this.preferences['workspace.preserveWindow'] || !this.opened,
110+
...options,
111+
};
112+
await this.server.setMostRecentlyUsedWorkspace(uri.toString());
113+
if (preserveWindow) {
114+
this._workspace = stat;
115+
}
116+
this.openWindow(stat, Object.assign(options ?? {}, { preserveWindow })); // Unlike Theia, IDE2 passes the whole `input` downstream and not only { preserveWindow }
117+
return;
118+
}
119+
throw new Error(
120+
'Invalid workspace root URI. Expected an existing directory or workspace file.'
121+
);
122+
}
123+
124+
/**
125+
* Copied from Theia. Can pass the `options` further down the chain.
126+
*/
127+
protected override openWindow(uri: FileStat, options?: WorkspaceInput): void {
128+
const workspacePath = uri.resource.path.toString();
129+
if (this.shouldPreserveWindow(options)) {
130+
this.reloadWindow();
131+
} else {
132+
try {
133+
this.openNewWindow(workspacePath, options); // Unlike Theia, IDE2 passes the `input` downstream.
134+
} catch (error) {
135+
// Fall back to reloading the current window in case the browser has blocked the new window
136+
this._workspace = uri;
137+
this.logger.error(error.toString()).then(() => this.reloadWindow());
138+
}
139+
}
140+
}
141+
142+
protected override openNewWindow(
143+
workspacePath: string,
144+
options?: WorkspaceInput
145+
): void {
86146
const { boardsConfig } = this.boardsServiceProvider;
87147
const url = BoardsConfig.Config.setConfig(
88148
boardsConfig,
89149
new URL(window.location.href)
90150
); // Set the current boards config for the new browser window.
91151
url.hash = workspacePath;
152+
if (StartupTask.WorkspaceInput.is(options)) {
153+
url.searchParams.set(
154+
StartupTask.QUERY_STRING,
155+
encodeURIComponent(JSON.stringify(options.tasks))
156+
);
157+
}
158+
92159
this.windowService.openNewWindow(url.toString());
93160
}
94161

Diff for: arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-contributions.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ import {
2323
} from '@theia/core/lib/browser/preferences/preference-service';
2424
import { ArduinoMenus, PlaceholderMenuNode } from '../../menu/arduino-menus';
2525
import { SketchbookCommands } from '../sketchbook/sketchbook-commands';
26-
import { CurrentSketch, SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
26+
import {
27+
CurrentSketch,
28+
SketchesServiceClientImpl,
29+
} from '../../../common/protocol/sketches-service-client-impl';
2730
import { Contribution } from '../../contributions/contribution';
2831
import { ArduinoPreferences } from '../../arduino-preferences';
2932
import { MainMenuManager } from '../../../common/main-menu-manager';

Diff for: arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-commands.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { Command } from '@theia/core/lib/common/command';
22

33
export namespace SketchbookCommands {
4+
export const TOGGLE_SKETCHBOOK_WIDGET: Command = {
5+
id: 'arduino-sketchbook-widget:toggle',
6+
};
7+
8+
export const REVEAL_SKETCH_NODE: Command = {
9+
id: 'arduino-sketchbook--reveal-sketch-node',
10+
};
11+
412
export const OPEN_NEW_WINDOW = Command.toLocalizedCommand(
513
{
614
id: 'arduino-sketchbook--open-sketch-new-window',

Diff for: arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget-contribution.ts

+43-6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
} from '../../../common/protocol/sketches-service-client-impl';
3030
import { FileService } from '@theia/filesystem/lib/browser/file-service';
3131
import { URI } from '../../contributions/contribution';
32+
import { WorkspaceInput } from '@theia/workspace/lib/browser';
3233

3334
export const SKETCHBOOK__CONTEXT = ['arduino-sketchbook--context'];
3435

@@ -77,7 +78,7 @@ export class SketchbookWidgetContribution
7778
area: 'left',
7879
rank: 1,
7980
},
80-
toggleCommandId: 'arduino-sketchbook-widget:toggle',
81+
toggleCommandId: SketchbookCommands.TOGGLE_SKETCHBOOK_WIDGET.id,
8182
toggleKeybinding: 'CtrlCmd+Shift+B',
8283
});
8384
}
@@ -100,11 +101,12 @@ export class SketchbookWidgetContribution
100101

101102
override registerCommands(registry: CommandRegistry): void {
102103
super.registerCommands(registry);
103-
104+
registry.registerCommand(SketchbookCommands.REVEAL_SKETCH_NODE, {
105+
execute: (treeWidgetId: string, nodeUri: string) =>
106+
this.revealSketchNode(treeWidgetId, nodeUri),
107+
});
104108
registry.registerCommand(SketchbookCommands.OPEN_NEW_WINDOW, {
105-
execute: async (arg) => {
106-
return this.workspaceService.open(arg.node.uri);
107-
},
109+
execute: (arg) => this.openNewWindow(arg.node),
108110
isEnabled: (arg) =>
109111
!!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
110112
isVisible: (arg) =>
@@ -197,7 +199,7 @@ export class SketchbookWidgetContribution
197199

198200
// unregister main menu action
199201
registry.unregisterMenuAction({
200-
commandId: 'arduino-sketchbook-widget:toggle',
202+
commandId: SketchbookCommands.TOGGLE_SKETCHBOOK_WIDGET.id,
201203
});
202204

203205
registry.registerMenuAction(SKETCHBOOK__CONTEXT__MAIN_GROUP, {
@@ -207,6 +209,28 @@ export class SketchbookWidgetContribution
207209
});
208210
}
209211

212+
private openNewWindow(node: SketchbookTree.SketchDirNode): void {
213+
const widget = this.tryGetWidget();
214+
if (widget) {
215+
const treeWidgetId = widget.activeTreeWidgetId();
216+
if (!treeWidgetId) {
217+
console.warn(`Could not retrieve active sketchbook tree ID.`);
218+
return;
219+
}
220+
const nodeUri = node.uri.toString();
221+
const options: WorkspaceInput = {};
222+
Object.assign(options, {
223+
tasks: [
224+
{
225+
command: SketchbookCommands.REVEAL_SKETCH_NODE.id,
226+
args: [treeWidgetId, nodeUri],
227+
},
228+
],
229+
});
230+
return this.workspaceService.open(node.uri, options);
231+
}
232+
}
233+
210234
/**
211235
* Reveals and selects node in the file navigator to which given widget is related.
212236
* Does nothing if given widget undefined or doesn't have related resource.
@@ -230,4 +254,17 @@ export class SketchbookWidgetContribution
230254
protected onCurrentWidgetChangedHandler(): void {
231255
this.selectWidgetFileNode(this.shell.currentWidget);
232256
}
257+
258+
private async revealSketchNode(
259+
treeWidgetId: string,
260+
nodeUIri: string
261+
): Promise<void> {
262+
return this.widget
263+
.then((widget) => this.shell.activateWidget(widget.id))
264+
.then((widget) => {
265+
if (widget instanceof SketchbookWidget) {
266+
return widget.revealSketchNode(treeWidgetId, nodeUIri);
267+
}
268+
});
269+
}
233270
}

Diff for: arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget.tsx

+58-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
1+
import {
2+
inject,
3+
injectable,
4+
postConstruct,
5+
} from '@theia/core/shared/inversify';
26
import { toArray } from '@theia/core/shared/@phosphor/algorithm';
37
import { IDragEvent } from '@theia/core/shared/@phosphor/dragdrop';
48
import { DockPanel, Widget } from '@theia/core/shared/@phosphor/widgets';
@@ -7,6 +11,8 @@ import { Disposable } from '@theia/core/lib/common/disposable';
711
import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
812
import { SketchbookTreeWidget } from './sketchbook-tree-widget';
913
import { nls } from '@theia/core/lib/common';
14+
import { CloudSketchbookCompositeWidget } from '../cloud-sketchbook/cloud-sketchbook-composite-widget';
15+
import { URI } from '../../contributions/contribution';
1016

1117
@injectable()
1218
export class SketchbookWidget extends BaseWidget {
@@ -45,6 +51,57 @@ export class SketchbookWidget extends BaseWidget {
4551
return this.localSketchbookTreeWidget;
4652
}
4753

54+
activeTreeWidgetId(): string | undefined {
55+
const selectedTreeWidgets = toArray(
56+
this.sketchbookTreesContainer.selectedWidgets()
57+
).map(({ id }) => id);
58+
if (selectedTreeWidgets.length > 1) {
59+
console.warn(
60+
`Found multiple selected tree widgets: ${JSON.stringify(
61+
selectedTreeWidgets
62+
)}. Expected only one.`
63+
);
64+
}
65+
return selectedTreeWidgets.shift();
66+
}
67+
68+
async revealSketchNode(treeWidgetId: string, nodeUri: string): Promise<void> {
69+
const widget = toArray(this.sketchbookTreesContainer.widgets())
70+
.filter(({ id }) => id === treeWidgetId)
71+
.shift();
72+
if (!widget) {
73+
console.warn(`Could not find tree widget with ID: ${widget}`);
74+
return;
75+
}
76+
// TODO: remove this when the remote/local sketchbooks and their widgets are cleaned up.
77+
const findTreeWidget = (
78+
widget: Widget | undefined
79+
): SketchbookTreeWidget | undefined => {
80+
if (widget instanceof SketchbookTreeWidget) {
81+
return widget;
82+
}
83+
if (widget instanceof CloudSketchbookCompositeWidget) {
84+
return widget.getTreeWidget();
85+
}
86+
return undefined;
87+
};
88+
const treeWidget = findTreeWidget(
89+
toArray(this.sketchbookTreesContainer.widgets())
90+
.filter(({ id }) => id === treeWidgetId)
91+
.shift()
92+
);
93+
if (!treeWidget) {
94+
console.warn(`Could not find tree widget with ID: ${treeWidget}`);
95+
return;
96+
}
97+
this.sketchbookTreesContainer.activateWidget(widget);
98+
99+
const treeNode = await treeWidget.model.revealFile(new URI(nodeUri));
100+
if (!treeNode) {
101+
console.warn(`Could not find tree node with URI: ${nodeUri}`);
102+
}
103+
}
104+
48105
protected override onActivateRequest(message: Message): void {
49106
super.onActivateRequest(message);
50107

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { injectable } from '@theia/core/shared/inversify';
2+
import { WorkspaceInput as TheiaWorkspaceInput } from '@theia/workspace/lib/browser';
3+
import { Contribution } from '../../contributions/contribution';
4+
5+
export interface Task {
6+
command: string;
7+
/**
8+
* This must be JSON serializable.
9+
*/
10+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11+
args?: any[];
12+
}
13+
14+
@injectable()
15+
export class StartupTask extends Contribution {
16+
override onReady(): void {
17+
const params = new URLSearchParams(window.location.search);
18+
const encoded = params.get(StartupTask.QUERY_STRING);
19+
if (!encoded) return;
20+
21+
const commands = JSON.parse(decodeURIComponent(encoded));
22+
23+
if (Array.isArray(commands)) {
24+
commands.forEach(({ command, args }) => {
25+
this.commandService.executeCommand(command, ...args);
26+
});
27+
}
28+
}
29+
}
30+
export namespace StartupTask {
31+
export const QUERY_STRING = 'startupTasks';
32+
export interface WorkspaceInput extends TheiaWorkspaceInput {
33+
tasks: Task[];
34+
}
35+
export namespace WorkspaceInput {
36+
export function is(
37+
input: (TheiaWorkspaceInput & Partial<WorkspaceInput>) | undefined
38+
): input is WorkspaceInput {
39+
return !!input && !!input.tasks;
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)