Skip to content

Sketchbook sidebar state #1102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 4, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ import { CoreErrorHandler } from './contributions/core-error-handler';
import { CompilerErrors } from './contributions/compiler-errors';
import { WidgetManager } from './theia/core/widget-manager';
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
import { StartupTask } from './widgets/sketchbook/startup-task';

MonacoThemingService.register({
id: 'arduino-theme',
Expand Down Expand Up @@ -698,6 +699,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, PlotterFrontendContribution);
Contribution.configure(bind, Format);
Contribution.configure(bind, CompilerErrors);
Contribution.configure(bind, StartupTask);

// 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ import { FrontendApplication } from '@theia/core/lib/browser/frontend-applicatio
import { FocusTracker, Widget } from '@theia/core/lib/browser';
import { DEFAULT_WINDOW_HASH } from '@theia/core/lib/common/window';
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 {
SketchesService,
Sketch,
} from '../../../common/protocol/sketches-service';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { BoardsConfig } from '../../boards/boards-config';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { StartupTask } from '../../widgets/sketchbook/startup-task';

@injectable()
export class WorkspaceService extends TheiaWorkspaceService {
Expand Down Expand Up @@ -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 url = BoardsConfig.Config.setConfig(
boardsConfig,
new URL(window.location.href)
); // Set the current boards config for the new browser window.
url.hash = workspacePath;
if (StartupTask.WorkspaceInput.is(options)) {
url.searchParams.set(
StartupTask.QUERY_STRING,
encodeURIComponent(JSON.stringify(options.tasks))
);
}

this.windowService.openNewWindow(url.toString());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import {
} from '@theia/core/lib/browser/preferences/preference-service';
import { ArduinoMenus, PlaceholderMenuNode } from '../../menu/arduino-menus';
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 { ArduinoPreferences } from '../../arduino-preferences';
import { MainMenuManager } from '../../../common/main-menu-manager';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { Command } from '@theia/core/lib/common/command';

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(
{
id: 'arduino-sketchbook--open-sketch-new-window',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from '../../../common/protocol/sketches-service-client-impl';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { URI } from '../../contributions/contribution';
import { WorkspaceInput } from '@theia/workspace/lib/browser';

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

Expand Down Expand Up @@ -77,7 +78,7 @@ export class SketchbookWidgetContribution
area: 'left',
rank: 1,
},
toggleCommandId: 'arduino-sketchbook-widget:toggle',
toggleCommandId: SketchbookCommands.TOGGLE_SKETCHBOOK_WIDGET.id,
toggleKeybinding: 'CtrlCmd+Shift+B',
});
}
Expand All @@ -100,11 +101,12 @@ export class SketchbookWidgetContribution

override registerCommands(registry: CommandRegistry): void {
super.registerCommands(registry);

registry.registerCommand(SketchbookCommands.REVEAL_SKETCH_NODE, {
execute: (treeWidgetId: string, nodeUri: string) =>
this.revealSketchNode(treeWidgetId, nodeUri),
});
registry.registerCommand(SketchbookCommands.OPEN_NEW_WINDOW, {
execute: async (arg) => {
return this.workspaceService.open(arg.node.uri);
},
execute: (arg) => this.openNewWindow(arg.node),
isEnabled: (arg) =>
!!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
isVisible: (arg) =>
Expand Down Expand Up @@ -197,7 +199,7 @@ export class SketchbookWidgetContribution

// unregister main menu action
registry.unregisterMenuAction({
commandId: 'arduino-sketchbook-widget:toggle',
commandId: SketchbookCommands.TOGGLE_SKETCHBOOK_WIDGET.id,
});

registry.registerMenuAction(SKETCHBOOK__CONTEXT__MAIN_GROUP, {
Expand All @@ -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.
* Does nothing if given widget undefined or doesn't have related resource.
Expand All @@ -230,4 +254,17 @@ export class SketchbookWidgetContribution
protected onCurrentWidgetChangedHandler(): void {
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);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -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 { IDragEvent } from '@theia/core/shared/@phosphor/dragdrop';
import { DockPanel, Widget } from '@theia/core/shared/@phosphor/widgets';
Expand All @@ -7,6 +11,8 @@ import { Disposable } from '@theia/core/lib/common/disposable';
import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
import { SketchbookTreeWidget } from './sketchbook-tree-widget';
import { nls } from '@theia/core/lib/common';
import { CloudSketchbookCompositeWidget } from '../cloud-sketchbook/cloud-sketchbook-composite-widget';
import { URI } from '../../contributions/contribution';

@injectable()
export class SketchbookWidget extends BaseWidget {
Expand Down Expand Up @@ -45,6 +51,57 @@ export class SketchbookWidget extends BaseWidget {
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 {
super.onActivateRequest(message);

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}