Ver Fonte

Project explorer: definition-only modules and new folders

New Module Here gains a Definition only kind (.def skeleton alone);
new New Folder Here command (validated name prompt, info feedback)
joins it in the explorer context menu. Target folder resolution falls
back to the workspace root, so group/project selections work instead
of warning.

Validation: GUI end-to-end (folder created, Helper.def skeleton
written, .mod correctly skipped for def-only).
Eric Streit há 6 dias atrás
pai
commit
8ffb237e79
2 ficheiros alterados com 49 adições e 11 exclusões
  1. 0 1
      PLAN.md
  2. 49 10
      extensions/modula2-project/src/browser/modula2-explorer-widget.ts

+ 0 - 1
PLAN.md

@@ -19,4 +19,3 @@ this file; all tags local.
 - Push commits + tags to `origin` (all local so far, through v0.25.0).
 - Clean superseded artifacts from `apps/electron/dist/`.
 - Coco/R grammar differential testing against `gm2`.
-- Right-click context menu visual check (CDP cannot open context menus).

+ 49 - 10
extensions/modula2-project/src/browser/modula2-explorer-widget.ts

@@ -237,6 +237,10 @@ export const NewModuleHereCommand: Command = Command.toLocalizedCommand({
   id: 'modula2.newModuleHere', label: 'New Module Here...', category: 'Modula-2',
 });
 
+export const NewFolderHereCommand: Command = Command.toLocalizedCommand({
+  id: 'modula2.newFolderHere', label: 'New Folder Here...', category: 'Modula-2',
+});
+
 @injectable()
 export class Modula2ExplorerCommands implements CommandContribution {
   @inject(WidgetManager) protected readonly widgets: WidgetManager;
@@ -255,25 +259,26 @@ export class Modula2ExplorerCommands implements CommandContribution {
       },
     });
     commands.registerCommand(NewModuleHereCommand, { execute: () => this.newModuleHere() });
+    commands.registerCommand(NewFolderHereCommand, { execute: () => this.newFolderHere() });
   }
 
-  /** Folder of the selected explorer file, or undefined when nothing useful is selected. */
-  protected selectedDir(): URI | undefined {
+  /** Folder of the selected explorer file, else the workspace root. */
+  protected async selectedDir(): Promise<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 new URI(node.uri).parent;
       }
     }
-    return undefined;
+    const roots = await this.workspace.roots;
+    return roots[0]?.resource;
   }
 
   protected async newModuleHere(): Promise<void> {
-    const dir = this.selectedDir();
+    const dir = await 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.');
+      this.messages.warn('Modula-2: open a folder or workspace first.');
       return;
     }
     const raw = await this.quickInput.input({
@@ -285,7 +290,8 @@ export class Modula2ExplorerCommands implements CommandContribution {
     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' }],
+       { label: 'Implementation only', description: 'MyModule.mod', value: 'mod' },
+       { label: 'Definition only', description: 'MyModule.def', value: 'def' }],
       { placeholder: 'What to create?' },
     );
     if (!kind) return;
@@ -303,20 +309,53 @@ export class Modula2ExplorerCommands implements CommandContribution {
         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 (kind.value !== 'def') await write(`${name}.mod`, renderImplModule(name));
+    if (kind.value !== 'mod') 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 */ }
   }
+
+  protected async newFolderHere(): Promise<void> {
+    const dir = await this.selectedDir();
+    if (!dir) {
+      this.messages.warn('Modula-2: 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('Modula-2: that folder already exists.');
+        return;
+      }
+      await this.files.createFolder(uri);
+      this.messages.info(`Modula-2: created folder ${raw.trim()}.`);
+    } catch (error) {
+      this.messages.error(`Modula-2: cannot create folder: ${error instanceof Error ? error.message : String(error)}`);
+    }
+  }
 }
 
 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: NewFolderHereCommand.id, order: '11' });
     menus.registerMenuAction(path, { commandId: 'modula2.build', order: '20' });
     menus.registerMenuAction(path, { commandId: RefreshExplorerCommand.id, order: '30' });
   }