|
@@ -0,0 +1,396 @@
|
|
|
|
|
+import * as React from 'react';
|
|
|
|
|
+import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
|
|
|
|
|
+import {
|
|
|
|
|
+ AbstractViewContribution, codicon, CompositeTreeNode,
|
|
|
|
|
+ ContextMenuRenderer, createTreeContainer,
|
|
|
|
|
+ ExpandableTreeNode, NodeProps, SelectableTreeNode,
|
|
|
|
|
+ TreeModel, TreeNode, TreeProps, TreeWidget, ViewContainer, ViewContainerTitleOptions,
|
|
|
|
|
+ Widget, WidgetFactory, WidgetManager,
|
|
|
|
|
+} from '@theia/core/lib/browser';
|
|
|
|
|
+import { Command, CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry, MessageService } from '@theia/core/lib/common';
|
|
|
|
|
+import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
|
|
|
+import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
|
|
|
|
+import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
|
|
|
|
+import { QuickInputService } from '@theia/core/lib/common/quick-pick-service';
|
|
|
|
|
+import { BinaryBuffer } from '@theia/core/lib/common/buffer';
|
|
|
|
|
+import URI from '@theia/core/lib/common/uri';
|
|
|
|
|
+import { Message } from '@lumino/messaging';
|
|
|
|
|
+import { groupExplorerFiles } from './oberon-explorer-model';
|
|
|
|
|
+import { isValidModuleName, renderModule } from './oberon-project';
|
|
|
|
|
+
|
|
|
|
|
+export const OBERON_EXPLORER_ID = 'oberon-explorer';
|
|
|
|
|
+
|
|
|
|
|
+/** Context menu path rendered by the explorer tree on right-click. */
|
|
|
|
|
+export const OBERON_EXPLORER_CONTEXT_MENU = ['oberon-explorer-context-menu'];
|
|
|
|
|
+
|
|
|
|
|
+export interface OberonTreeNode extends CompositeTreeNode, SelectableTreeNode, ExpandableTreeNode {
|
|
|
|
|
+ kind: 'project' | 'group' | 'file';
|
|
|
|
|
+ uri?: string;
|
|
|
|
|
+ openable: boolean;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function folderNode(id: string, name: string, kind: 'project' | 'group'): OberonTreeNode {
|
|
|
|
|
+ return {
|
|
|
|
|
+ id, name, kind, openable: false,
|
|
|
|
|
+ parent: undefined as unknown as CompositeTreeNode,
|
|
|
|
|
+ children: [],
|
|
|
|
|
+ expanded: true,
|
|
|
|
|
+ selected: false,
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function fileNode(
|
|
|
|
|
+ uri: string, name: string, openable: boolean,
|
|
|
|
|
+): OberonTreeNode {
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: uri, name, kind: 'file', uri, openable,
|
|
|
|
|
+ parent: undefined as unknown as CompositeTreeNode,
|
|
|
|
|
+ children: [],
|
|
|
|
|
+ expanded: false,
|
|
|
|
|
+ selected: false,
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@injectable()
|
|
|
|
|
+export class OberonExplorerWidget extends TreeWidget {
|
|
|
|
|
+ static readonly ID = OBERON_EXPLORER_ID;
|
|
|
|
|
+
|
|
|
|
|
+ @inject(FileService) protected readonly files: FileService;
|
|
|
|
|
+ @inject(WorkspaceService) protected readonly workspace: WorkspaceService;
|
|
|
|
|
+ @inject(EditorManager) protected readonly editors: EditorManager;
|
|
|
|
|
+
|
|
|
|
|
+ protected refreshTimer: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
|
+ protected attached = false;
|
|
|
|
|
+
|
|
|
|
|
+ constructor(
|
|
|
|
|
+ props: TreeProps,
|
|
|
|
|
+ model: TreeModel,
|
|
|
|
|
+ contextMenuRenderer: ContextMenuRenderer,
|
|
|
|
|
+ ) {
|
|
|
|
|
+ super(props, model, contextMenuRenderer);
|
|
|
|
|
+ this.id = OberonExplorerWidget.ID;
|
|
|
|
|
+ this.title.label = 'Oberon Project';
|
|
|
|
|
+ this.title.caption = 'Oberon project files';
|
|
|
|
|
+ this.title.closable = false;
|
|
|
|
|
+ // Fill the view-container part: without an explicit height the
|
|
|
|
|
+ // virtualized tree measures zero and renders nothing (a dock panel
|
|
|
|
|
+ // would size it instead).
|
|
|
|
|
+ this.node.style.height = '100%';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @postConstruct()
|
|
|
|
|
+ protected initialize(): void {
|
|
|
|
|
+ // NOTE: inversify only invokes the most-derived @postConstruct, so the
|
|
|
|
|
+ // base TreeWidget.init() (row-update wiring) must be called explicitly.
|
|
|
|
|
+ super.init();
|
|
|
|
|
+ // Injections are complete here (never touch them in the constructor).
|
|
|
|
|
+ this.toDispose.push(this.model.onSelectionChanged(nodes => this.openSelected(nodes)));
|
|
|
|
|
+ this.toDispose.push(this.files.onDidFilesChange(() => this.scheduleRefresh()));
|
|
|
|
|
+ this.toDispose.push(this.workspace.onWorkspaceChanged(() => this.refresh()));
|
|
|
|
|
+ this.toDispose.push(this.workspace.onWorkspaceLocationChanged(() => this.refresh()));
|
|
|
|
|
+ // The view may be created before the workspace is ready (fresh view
|
|
|
|
|
+ // container at startup): refresh once roots resolve, in addition to
|
|
|
|
|
+ // the event-driven refreshes, so the tree never stays empty.
|
|
|
|
|
+ void this.workspace.roots.then(() => this.scheduleRefresh(), () => undefined);
|
|
|
|
|
+ void this.refresh();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected override onAfterAttach(msg: Message): void {
|
|
|
|
|
+ super.onAfterAttach(msg);
|
|
|
|
|
+ if (!this.attached) {
|
|
|
|
|
+ this.attached = true;
|
|
|
|
|
+ void this.refresh();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected scheduleRefresh(): void {
|
|
|
|
|
+ if (this.refreshTimer) clearTimeout(this.refreshTimer);
|
|
|
|
|
+ this.refreshTimer = setTimeout(() => {
|
|
|
|
|
+ this.refreshTimer = undefined;
|
|
|
|
|
+ void this.refresh();
|
|
|
|
|
+ }, 300);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async refresh(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const roots = await this.workspace.roots;
|
|
|
|
|
+ if (roots.length === 0) {
|
|
|
|
|
+ this.model.root = folderNode('oberonroot', 'No folder open', 'project');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const root = roots[0].resource;
|
|
|
|
|
+ const project = await this.readProject(root);
|
|
|
|
|
+ const found = await this.collectFiles(root, root, 0, [], project.output);
|
|
|
|
|
+ const groups = groupExplorerFiles(found, project.main);
|
|
|
|
|
+ const rootNode = folderNode(
|
|
|
|
|
+ 'oberonroot',
|
|
|
|
|
+ project.name ?? `${root.path.base} (no oberon.json)`,
|
|
|
|
|
+ 'project',
|
|
|
|
|
+ );
|
|
|
|
|
+ for (const group of groups) {
|
|
|
|
|
+ const groupNode = folderNode(`oberongroup:${group.id}`, group.label, 'group');
|
|
|
|
|
+ const fileNodes = group.files.map(f => fileNode(
|
|
|
|
|
+ f.uri,
|
|
|
|
|
+ f.main ? `${f.name} (main)` : f.name,
|
|
|
|
|
+ f.openable,
|
|
|
|
|
+ ));
|
|
|
|
|
+ CompositeTreeNode.addChildren(groupNode, fileNodes);
|
|
|
|
|
+ CompositeTreeNode.addChild(rootNode, groupNode);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (rootNode.children.length === 0) {
|
|
|
|
|
+ const hint = folderNode('oberonempty', 'No Oberon files found', 'group');
|
|
|
|
|
+ hint.expanded = false;
|
|
|
|
|
+ CompositeTreeNode.addChild(rootNode, hint);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.model.root = rootNode;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const message = error instanceof Error ? error.message : String(error);
|
|
|
|
|
+ console.error('Oberon explorer refresh failed:', message);
|
|
|
|
|
+ const errNode = folderNode('oberonerror', 'Explorer error', 'group');
|
|
|
|
|
+ const detail = fileNode('oberonerror:detail', message.slice(0, 120), false);
|
|
|
|
|
+ CompositeTreeNode.addChild(errNode, detail);
|
|
|
|
|
+ this.model.root = errNode;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected async readProject(root: URI): Promise<{ name?: string; main?: string; output?: string }> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const raw = JSON.parse((await this.files.read(root.resolve('oberon.json'))).value) as unknown;
|
|
|
|
|
+ if (typeof raw !== 'object' || raw === null) return {};
|
|
|
|
|
+ const r = raw as { name?: unknown; main?: unknown; output?: unknown };
|
|
|
|
|
+ const out: { name?: string; main?: string; output?: string } = {};
|
|
|
|
|
+ if (typeof r.name === 'string' && r.name.length > 0) out.name = r.name;
|
|
|
|
|
+ if (typeof r.main === 'string' && r.main.length > 0) out.main = r.main;
|
|
|
|
|
+ const configured = (typeof r.output === 'string' && r.output.length > 0)
|
|
|
|
|
+ ? r.output : (out.name ? `bin/${out.name}` : undefined);
|
|
|
|
|
+ if (configured) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (await this.files.exists(root.resolve(configured))) out.output = configured;
|
|
|
|
|
+ } catch { /* listed only when present */ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return out;
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return {};
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected async collectFiles(
|
|
|
|
|
+ root: URI, dir: URI, depth: number,
|
|
|
|
|
+ out: Array<{ uri: string; relative: string; output?: boolean }>,
|
|
|
|
|
+ outputRel?: string,
|
|
|
|
|
+ ): Promise<Array<{ uri: string; relative: string; output?: boolean }>> {
|
|
|
|
|
+ let stat;
|
|
|
|
|
+ try {
|
|
|
|
|
+ stat = await this.files.resolve(dir);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return out;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (const child of stat.children ?? []) {
|
|
|
|
|
+ const name = child.resource.path.base;
|
|
|
|
|
+ if (child.isDirectory) {
|
|
|
|
|
+ if (name.startsWith('.') || name === 'node_modules') continue;
|
|
|
|
|
+ if (depth < 8) await this.collectFiles(root, child.resource, depth + 1, out, outputRel);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ const rel = root.relative(child.resource)?.toString() ?? name;
|
|
|
|
|
+ if (name === 'oberon.json' || name.endsWith('.Mod') || name.endsWith('.mod') || name.endsWith('.om') || name.endsWith('.def') || name.endsWith('.od')) {
|
|
|
|
|
+ out.push({ uri: child.resource.toString(), relative: rel });
|
|
|
|
|
+ } else if (outputRel && rel === outputRel) {
|
|
|
|
|
+ out.push({ uri: child.resource.toString(), relative: rel, output: true });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return out;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected openSelected(nodes: readonly Readonly<SelectableTreeNode>[]): void {
|
|
|
|
|
+ const node = nodes[0] as OberonTreeNode | undefined;
|
|
|
|
|
+ if (node && node.kind === 'file' && node.uri && node.openable) {
|
|
|
|
|
+ this.editors.open(new URI(node.uri), { mode: 'reveal' }).catch(() => undefined);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected override renderIcon(node: TreeNode, props: NodeProps): React.ReactNode {
|
|
|
|
|
+ const n = node as Partial<OberonTreeNode>;
|
|
|
|
|
+ if (n.kind === 'project') return React.createElement('div', { className: codicon('folder-library') });
|
|
|
|
|
+ if (n.kind === 'group') return React.createElement('div', { className: codicon('folder') });
|
|
|
|
|
+ if (n.kind === 'file') {
|
|
|
|
|
+ if ((n.uri ?? '').endsWith('oberon.json')) {
|
|
|
|
|
+ return React.createElement('div', { className: codicon('json') });
|
|
|
|
|
+ }
|
|
|
|
|
+ return React.createElement('div', { className: codicon('file-code') });
|
|
|
|
|
+ }
|
|
|
|
|
+ return super.renderIcon(node, props);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export const OBERON_EXPLORER_VIEW_CONTAINER_ID = 'oberon-explorer-view-container';
|
|
|
|
|
+export const OBERON_EXPLORER_VIEW_CONTAINER_TITLE_OPTIONS: ViewContainerTitleOptions = {
|
|
|
|
|
+ label: 'Oberon Project',
|
|
|
|
|
+ iconClass: codicon('folder-library'),
|
|
|
|
|
+ closeable: true,
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** Dedicated activity-bar container for the project view (with its icon). */
|
|
|
|
|
+@injectable()
|
|
|
|
|
+export class OberonExplorerContainerFactory {
|
|
|
|
|
+ static ID = OBERON_EXPLORER_VIEW_CONTAINER_ID;
|
|
|
|
|
+ readonly id = OberonExplorerContainerFactory.ID;
|
|
|
|
|
+ @inject(ViewContainer.Factory) protected readonly viewContainerFactory: ViewContainer.Factory;
|
|
|
|
|
+ @inject(WidgetManager) protected readonly widgetManager: WidgetManager;
|
|
|
|
|
+ async createWidget(): Promise<Widget> {
|
|
|
|
|
+ const viewContainer = this.viewContainerFactory({
|
|
|
|
|
+ id: OBERON_EXPLORER_VIEW_CONTAINER_ID,
|
|
|
|
|
+ progressLocationId: 'oberon-explorer',
|
|
|
|
|
+ });
|
|
|
|
|
+ viewContainer.setTitleOptions(OBERON_EXPLORER_VIEW_CONTAINER_TITLE_OPTIONS);
|
|
|
|
|
+ const explorer = await this.widgetManager.getOrCreateWidget(OBERON_EXPLORER_ID);
|
|
|
|
|
+ viewContainer.addWidget(explorer, {
|
|
|
|
|
+ order: 0, canHide: false, initiallyCollapsed: false, weight: 100,
|
|
|
|
|
+ });
|
|
|
|
|
+ return viewContainer;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export class OberonExplorerContribution extends AbstractViewContribution<OberonExplorerWidget> {
|
|
|
|
|
+ constructor() {
|
|
|
|
|
+ super({
|
|
|
|
|
+ viewContainerId: OBERON_EXPLORER_VIEW_CONTAINER_ID,
|
|
|
|
|
+ widgetId: OBERON_EXPLORER_ID,
|
|
|
|
|
+ widgetName: 'Oberon Project',
|
|
|
|
|
+ defaultWidgetOptions: { area: 'left', rank: 300 },
|
|
|
|
|
+ toggleCommandId: 'oberonExplorer:toggle',
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async initializeLayout(): Promise<void> {
|
|
|
|
|
+ await this.openView({ activate: false, reveal: true });
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export const RefreshExplorerCommand: Command = Command.toLocalizedCommand({
|
|
|
|
|
+ id: 'oberon.refreshExplorer', label: 'Refresh Project Explorer', category: 'Oberon',
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+export const NewModuleHereCommand: Command = Command.toLocalizedCommand({
|
|
|
|
|
+ id: 'oberon.newModuleHere', label: 'New Module Here...', category: 'Oberon',
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+export const NewFolderHereCommand: Command = Command.toLocalizedCommand({
|
|
|
|
|
+ id: 'oberon.newFolderHere', label: 'New Folder Here...', category: 'Oberon',
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+@injectable()
|
|
|
|
|
+export class OberonExplorerCommands implements CommandContribution {
|
|
|
|
|
+ @inject(WidgetManager) protected readonly widgets: WidgetManager;
|
|
|
|
|
+ @inject(FileService) protected readonly files: FileService;
|
|
|
|
|
+ @inject(WorkspaceService) protected readonly workspace: WorkspaceService;
|
|
|
|
|
+ @inject(EditorManager) protected readonly editors: EditorManager;
|
|
|
|
|
+ @inject(MessageService) protected readonly messages: MessageService;
|
|
|
|
|
+ @inject(QuickInputService) protected readonly quickInput: QuickInputService;
|
|
|
|
|
+
|
|
|
|
|
+ registerCommands(commands: CommandRegistry): void {
|
|
|
|
|
+ commands.registerCommand(RefreshExplorerCommand, {
|
|
|
|
|
+ execute: () => {
|
|
|
|
|
+ for (const widget of this.widgets.getWidgets(OBERON_EXPLORER_ID)) {
|
|
|
|
|
+ void (widget as OberonExplorerWidget).refresh();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ commands.registerCommand(NewModuleHereCommand, { execute: () => this.newModuleHere() });
|
|
|
|
|
+ commands.registerCommand(NewFolderHereCommand, { execute: () => this.newFolderHere() });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Folder of the selected explorer file, else the workspace root. */
|
|
|
|
|
+ protected async selectedDir(): Promise<URI | undefined> {
|
|
|
|
|
+ for (const widget of this.widgets.getWidgets(OBERON_EXPLORER_ID)) {
|
|
|
|
|
+ const view = widget as OberonExplorerWidget;
|
|
|
|
|
+ const node = view.model.selectedNodes[0] as OberonTreeNode | undefined;
|
|
|
|
|
+ if (node?.kind === 'file' && node.uri) {
|
|
|
|
|
+ return new URI(node.uri).parent;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const roots = await this.workspace.roots;
|
|
|
|
|
+ return roots[0]?.resource;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected async newModuleHere(): Promise<void> {
|
|
|
|
|
+ const dir = await this.selectedDir();
|
|
|
|
|
+ if (!dir) {
|
|
|
|
|
+ this.messages.warn('Oberon: open a folder or workspace first.');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const raw = await this.quickInput.input({
|
|
|
|
|
+ prompt: 'Module name (an Oberon identifier, e.g. MyModule)',
|
|
|
|
|
+ placeHolder: 'MyModule',
|
|
|
|
|
+ validateInput: async value => isValidModuleName(value.trim()) ?? null,
|
|
|
|
|
+ });
|
|
|
|
|
+ if (raw === undefined) return;
|
|
|
|
|
+ const name = raw.trim();
|
|
|
|
|
+ const file = `${name}.Mod`;
|
|
|
|
|
+ const uri = dir.resolve(file);
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (await this.files.exists(uri)) {
|
|
|
|
|
+ this.messages.warn(`Oberon: ${file} already exists, skipped.`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ await this.files.writeFile(uri, BinaryBuffer.fromString(renderModule(name)));
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ this.messages.error(`Oberon: cannot write ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.messages.info(`Oberon: created ${file}.`);
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.editors.open(uri, { mode: 'activate' });
|
|
|
|
|
+ } catch { /* created is enough */ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected async newFolderHere(): Promise<void> {
|
|
|
|
|
+ const dir = await this.selectedDir();
|
|
|
|
|
+ if (!dir) {
|
|
|
|
|
+ this.messages.warn('Oberon: open a folder or workspace first.');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const raw = await this.quickInput.input({
|
|
|
|
|
+ prompt: 'Folder name (e.g. sources)',
|
|
|
|
|
+ placeHolder: 'sources',
|
|
|
|
|
+ validateInput: async value => {
|
|
|
|
|
+ const name = value.trim();
|
|
|
|
|
+ if (name.length === 0) return 'Enter a folder name.';
|
|
|
|
|
+ if (name === '.' || name === '..' || name.includes('/') || name.includes('\\')) {
|
|
|
|
|
+ return 'A single folder name, without path separators.';
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ if (raw === undefined) return;
|
|
|
|
|
+ const uri = dir.resolve(raw.trim());
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (await this.files.exists(uri)) {
|
|
|
|
|
+ this.messages.warn('Oberon: that folder already exists.');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ await this.files.createFolder(uri);
|
|
|
|
|
+ this.messages.info(`Oberon: created folder ${raw.trim()}.`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ this.messages.error(`Oberon: cannot create folder: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export class OberonExplorerMenus implements MenuContribution {
|
|
|
|
|
+ registerMenus(menus: MenuModelRegistry): void {
|
|
|
|
|
+ const path = OBERON_EXPLORER_CONTEXT_MENU;
|
|
|
|
|
+ menus.registerMenuAction(path, { commandId: NewModuleHereCommand.id, order: '10' });
|
|
|
|
|
+ menus.registerMenuAction(path, { commandId: NewFolderHereCommand.id, order: '11' });
|
|
|
|
|
+ menus.registerMenuAction(path, { commandId: 'oberon.build', order: '20' });
|
|
|
|
|
+ menus.registerMenuAction(path, { commandId: RefreshExplorerCommand.id, order: '30' });
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function createOberonExplorerWidget(parent: interfaces.Container): OberonExplorerWidget {
|
|
|
|
|
+ const child = createTreeContainer(parent, {
|
|
|
|
|
+ widget: OberonExplorerWidget,
|
|
|
|
|
+ props: { contextMenuPath: OBERON_EXPLORER_CONTEXT_MENU },
|
|
|
|
|
+ });
|
|
|
|
|
+ return child.get(OberonExplorerWidget);
|
|
|
|
|
+}
|