Forráskód Böngészése

Explorer context menu and build-before-debug

- right-click in the Modula-2 Project view: New Module Here (in the
  selected file's folder), Build, Refresh
- example launch.json gains preLaunchTask Modula-2: Build so F5
  always debugs a fresh binary; tutorial documents both
Eric Streit 1 hete
szülő
commit
451556b3a4

+ 6 - 1
TUTORIAL.md

@@ -70,7 +70,12 @@ Build errors appear in Problems panel, click to jump to source.
 2. Set breakpoint in gutter.
 3. `Run > Start Debugging`, type `modula2`. `program` defaults to `bin/<name>`.
 
-You get: continue/step over/into/out, call stack, Locals + Module scopes, hover/evaluate.
+The example `launch.json` sets `"preLaunchTask": "Modula-2: Build"`, so
+debugging always builds first. You get: continue/step over/into/out, call
+stack, Locals + Module scopes, hover/evaluate.
+
+Right-click any file in the Modula-2 Project view for `New Module Here`,
+`Build` and `Refresh` without leaving the explorer.
 
 ## 7. Troubleshooting
 

+ 2 - 1
examples/hello/.vscode/launch.json

@@ -7,7 +7,8 @@
       "request": "launch",
       "name": "Debug Hello",
       "program": "${workspaceFolder}/bin/Hello",
-      "cwd": "${workspaceFolder}"
+      "cwd": "${workspaceFolder}",
+      "preLaunchTask": "Modula-2: Build"
     }
   ]
 }

+ 85 - 2
extensions/modula2-project/src/browser/modula2-explorer-widget.ts

@@ -6,16 +6,22 @@ import {
   ExpandableTreeNode, NodeProps, SelectableTreeNode,
   TreeModel, TreeNode, TreeProps, TreeWidget, WidgetFactory, WidgetManager,
 } from '@theia/core/lib/browser';
-import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common';
+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 './modula2-explorer-model';
+import { isValidModuleName, renderDefModule, renderImplModule } from './modula2-project';
 
 export const MODULA2_EXPLORER_ID = 'modula2-explorer';
 
+/** Context menu path rendered by the explorer tree on right-click. */
+export const MODULA2_EXPLORER_CONTEXT_MENU = ['modula2-explorer-context-menu'];
+
 export interface Modula2TreeNode extends CompositeTreeNode, SelectableTreeNode, ExpandableTreeNode {
   kind: 'project' | 'group' | 'file';
   uri?: string;
@@ -227,9 +233,18 @@ export const RefreshExplorerCommand: Command = Command.toLocalizedCommand({
   id: 'modula2.refreshExplorer', label: 'Refresh Project Explorer', category: 'Modula-2',
 });
 
+export const NewModuleHereCommand: Command = Command.toLocalizedCommand({
+  id: 'modula2.newModuleHere', label: 'New Module Here...', category: 'Modula-2',
+});
+
 @injectable()
 export class Modula2ExplorerCommands 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, {
@@ -239,10 +254,78 @@ export class Modula2ExplorerCommands implements CommandContribution {
         }
       },
     });
+    commands.registerCommand(NewModuleHereCommand, { execute: () => this.newModuleHere() });
+  }
+
+  /** Folder of the selected explorer file, or undefined when nothing useful is selected. */
+  protected selectedDir(): URI | undefined {
+    for (const widget of this.widgets.getWidgets(MODULA2_EXPLORER_ID)) {
+      const view = widget as Modula2ExplorerWidget;
+      const node = view.model.selectedNodes[0] as Modula2TreeNode | undefined;
+      if (node?.kind === 'file' && node.uri) {
+        const uri = new URI(node.uri);
+        return uri.parent;
+      }
+    }
+    return undefined;
+  }
+
+  protected async newModuleHere(): Promise<void> {
+    const dir = this.selectedDir();
+    if (!dir) {
+      this.messages.warn('Modula-2: select a file in the Modula-2 Project view first, or use Modula-2: New Module.');
+      return;
+    }
+    const raw = await this.quickInput.input({
+      prompt: 'Module name (a Modula-2 identifier, e.g. MyModule)',
+      placeHolder: 'MyModule',
+      validateInput: async value => isValidModuleName(value.trim()) ?? null,
+    });
+    if (raw === undefined) return;
+    const name = raw.trim();
+    const kind = await this.quickInput.showQuickPick(
+      [{ label: 'Implementation + definition', description: 'MyModule.mod and MyModule.def', value: 'both' },
+       { label: 'Implementation only', description: 'MyModule.mod', value: 'mod' }],
+      { placeholder: 'What to create?' },
+    );
+    if (!kind) return;
+    const created: string[] = [];
+    const write = async (file: string, content: string): Promise<void> => {
+      const uri = dir.resolve(file);
+      try {
+        if (await this.files.exists(uri)) {
+          this.messages.warn(`Modula-2: ${file} already exists, skipped.`);
+          return;
+        }
+        await this.files.writeFile(uri, BinaryBuffer.fromString(content));
+        created.push(file);
+      } catch (error) {
+        this.messages.error(`Modula-2: cannot write ${file}: ${error instanceof Error ? error.message : String(error)}`);
+      }
+    };
+    await write(`${name}.mod`, renderImplModule(name));
+    if (kind.value === 'both') await write(`${name}.def`, renderDefModule(name));
+    if (created.length === 0) return;
+    this.messages.info(`Modula-2: created ${created.join(', ')}.`);
+    try {
+      await this.editors.open(dir.resolve(created[0]), { mode: 'activate' });
+    } catch { /* created is enough */ }
+  }
+}
+
+export class Modula2ExplorerMenus implements MenuContribution {
+  registerMenus(menus: MenuModelRegistry): void {
+    const path = MODULA2_EXPLORER_CONTEXT_MENU;
+    menus.registerMenuAction(path, { commandId: NewModuleHereCommand.id, order: '10' });
+    menus.registerMenuAction(path, { commandId: 'modula2.build', order: '20' });
+    menus.registerMenuAction(path, { commandId: RefreshExplorerCommand.id, order: '30' });
   }
 }
 
 export function createModula2ExplorerWidget(parent: interfaces.Container): Modula2ExplorerWidget {
-  const child = createTreeContainer(parent, { widget: Modula2ExplorerWidget });
+  const child = createTreeContainer(parent, {
+    widget: Modula2ExplorerWidget,
+    props: { contextMenuPath: MODULA2_EXPLORER_CONTEXT_MENU },
+  });
   return child.get(Modula2ExplorerWidget);
 }

+ 2 - 1
extensions/modula2-project/src/browser/modula2-project-frontend-module.ts

@@ -18,7 +18,7 @@ import {
 } from './modula2-project';
 import {
   MODULA2_EXPLORER_ID, Modula2ExplorerCommands, Modula2ExplorerContribution,
-  createModula2ExplorerWidget,
+  Modula2ExplorerMenus, createModula2ExplorerWidget,
 } from './modula2-explorer-widget';
 
 const CATEGORY = 'Modula-2';
@@ -243,4 +243,5 @@ export default new ContainerModule(bind => {
     createWidget: () => createModula2ExplorerWidget(context.container),
   }));
   bind(CommandContribution).to(Modula2ExplorerCommands).inSingletonScope();
+  bind(MenuContribution).to(Modula2ExplorerMenus).inSingletonScope();
 });