Pārlūkot izejas kodu

Phase 3 (3/5): preferences and tasks

- Modula-2 preference section (compiler path/options, dialect);
  modula2.json keeps precedence, preferences fill the gaps
- Task provider contributes Build/Clean/Rebuild/Run shell tasks
  from modula2.json, resolved to shell execution via a task resolver
- Pure task/project logic headlessly tested; provider wiring kept thin

Verified end-to-end in headless Chrome: Modula-2 preferences
render in Settings; all 4 tasks listed and Build executes gm2 in
the terminal with real compiler output; zero console errors.
Eric Streit 2 nedēļas atpakaļ
vecāks
revīzija
ec459a30e0

+ 1 - 1
apps/browser/package.json

@@ -15,7 +15,7 @@
     "@theia/process": "1.75.0",
     "@theia/terminal": "1.75.0",
     "@theia/workspace": "1.75.0",
-    "modula2-compiler": "0.1.0",
+    "modula2-compiler": "0.2.0",
     "modula2-workbench": "0.1.0",
     "react": "^19.0.0",
     "react-dom": "^19.0.0"

+ 1 - 1
apps/electron/package.json

@@ -16,7 +16,7 @@
     "@theia/process": "1.75.0",
     "@theia/terminal": "1.75.0",
     "@theia/workspace": "1.75.0",
-    "modula2-compiler": "0.1.0",
+    "modula2-compiler": "0.2.0",
     "modula2-workbench": "0.1.0",
     "react": "^19.0.0",
     "react-dom": "^19.0.0"

+ 2 - 1
extensions/modula2-compiler/package.json

@@ -1,7 +1,7 @@
 {
   "name": "modula2-compiler",
   "displayName": "Modula-2 Compiler Support",
-  "version": "0.1.0",
+  "version": "0.2.0",
   "keywords": ["theia-extension"],
   "files": ["lib", "src"],
   "dependencies": {
@@ -9,6 +9,7 @@
     "@theia/editor": "1.75.0",
     "@theia/filesystem": "1.75.0",
     "@theia/markers": "1.75.0",
+    "@theia/task": "1.75.0",
     "@theia/workspace": "1.75.0"
   },
   "devDependencies": {"rimraf": "^6.0.1", "typescript": "^5.9.2"},

+ 25 - 13
extensions/modula2-compiler/src/browser/modula2-compiler-frontend-module.ts

@@ -9,11 +9,17 @@ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service
 import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
 import { ProblemManager } from '@theia/markers/lib/browser/problem/problem-manager';
 import { Diagnostic, DiagnosticSeverity } from '@theia/core/shared/vscode-languageserver-protocol';
+import { PreferenceContribution } from '@theia/core/lib/common/preferences/preference-schema';
+import { PreferenceService } from '@theia/core/lib/common/preferences/preference-service';
+import { TaskContribution } from '@theia/task/lib/browser/task-contribution';
 import URI from '@theia/core/lib/common/uri';
 import {
   BuildDiagnostic, BuildResult, Modula2Compiler, Modula2CompilerServicePath,
-  Modula2Project, parseProjectFile,
+  Modula2Project,
 } from '../common/compiler';
+import { COMPILER_OPTIONS, COMPILER_PATH, modula2PreferenceSchema } from './modula2-preferences';
+import { loadWorkspaceProject } from './modula2-project';
+import { Modula2TaskContribution, Modula2TaskProvider, Modula2TaskResolver } from './modula2-tasks';
 
 const CATEGORY = 'Modula-2';
 
@@ -45,6 +51,7 @@ class CompilerCommands implements CommandContribution {
   @inject(EditorManager) protected readonly editors: EditorManager;
   @inject(MessageService) protected readonly messages: MessageService;
   @inject(ProblemManager) protected readonly problems: ProblemManager;
+  @inject(PreferenceService) protected readonly prefs: PreferenceService;
   /** URIs currently carrying our markers, so stale ones can be cleared. */
   protected readonly markedUris = new Set<string>();
 
@@ -59,21 +66,21 @@ class CompilerCommands implements CommandContribution {
     commands.registerCommand(Commands.run, { execute: () => this.withProject(p => this.run(p)) });
   }
 
-  /** Load modula2.json from the first workspace root. */
+  /** Load modula2.json from the first workspace root, applying preferences. */
   protected async loadProject(): Promise<Modula2Project | undefined> {
-    const roots = await this.workspace.roots;
-    if (roots.length === 0) {
+    const loaded = await loadWorkspaceProject(this.files, this.workspace);
+    if (!loaded) {
       this.messages.warn('Modula-2: open a folder with a modula2.json project first.');
       return undefined;
     }
-    const rootUri = roots[0].resource;
-    try {
-      const content = await this.files.read(rootUri.resolve('modula2.json'));
-      return parseProjectFile(rootUri.path.fsPath(), content.value);
-    } catch {
-      this.messages.warn('Modula-2: no readable modula2.json in the workspace root.');
-      return undefined;
+    const project = loaded.project;
+    // Preferences fill the gaps: an explicit modula2.json path always wins.
+    if (!project.compilerPathExplicit) {
+      project.compiler.path = this.prefs.get<string>(COMPILER_PATH, 'gm2');
     }
+    const extra = this.prefs.get<string[]>(COMPILER_OPTIONS, []);
+    project.compiler.options = [...project.compiler.options, ...extra];
+    return project;
   }
 
   protected async withProject(op: (project: Modula2Project) => Promise<void>): Promise<void> {
@@ -171,19 +178,24 @@ class CompilerCommands implements CommandContribution {
   }
 
   protected async detect(): Promise<void> {
-    const info = await this.compiler.detect('gm2');
+    const configured = this.prefs.get<string>(COMPILER_PATH, 'gm2');
+    const info = await this.compiler.detect(configured);
     if (info) {
       this.messages.info(`GNU Modula-2 detected: ${info.version} (${info.path})`);
     } else {
-      this.messages.warn('Modula-2: no working gm2 executable found on PATH.');
+      this.messages.warn(`Modula-2: no working compiler at "${configured}".`);
     }
   }
 }
 
 export default new ContainerModule(bind => {
+  bind(PreferenceContribution).toConstantValue({ schema: modula2PreferenceSchema });
   bind(Modula2Compiler).toDynamicValue(ctx => {
     const provider = ctx.container.get<ServiceConnectionProvider>(RemoteConnectionProvider);
     return provider.createProxy<Modula2Compiler>(Modula2CompilerServicePath);
   }).inSingletonScope();
   bind(CommandContribution).to(CompilerCommands).inSingletonScope();
+  bind(Modula2TaskProvider).toSelf().inSingletonScope();
+  bind(Modula2TaskResolver).toSelf().inSingletonScope();
+  bind(TaskContribution).to(Modula2TaskContribution).inSingletonScope();
 });

+ 28 - 0
extensions/modula2-compiler/src/browser/modula2-preferences.ts

@@ -0,0 +1,28 @@
+import { PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema';
+
+export const COMPILER_PATH = 'modula2.compiler.path';
+export const COMPILER_OPTIONS = 'modula2.compiler.options';
+export const DIALECT = 'modula2.dialect';
+
+export const modula2PreferenceSchema: PreferenceSchema = {
+  title: 'Modula-2',
+  properties: {
+    [COMPILER_PATH]: {
+      type: 'string',
+      default: 'gm2',
+      description: 'Modula-2 compiler executable (used when modula2.json sets no explicit path).',
+    },
+    [COMPILER_OPTIONS]: {
+      type: 'array',
+      items: { type: 'string' },
+      default: [],
+      description: 'Extra compiler options appended after modula2.json options.',
+    },
+    [DIALECT]: {
+      type: 'string',
+      enum: ['gnu', 'iso', 'pim'],
+      default: 'gnu',
+      description: 'Modula-2 dialect assumed by language services (reserved for the parser/LSP).',
+    },
+  },
+};

+ 19 - 0
extensions/modula2-compiler/src/browser/modula2-project.ts

@@ -0,0 +1,19 @@
+import { FileService } from '@theia/filesystem/lib/browser/file-service';
+import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
+import { Modula2Project, parseProjectFile } from '../common/compiler';
+
+/** Load modula2.json from the first workspace root (shared by commands/tasks). */
+export async function loadWorkspaceProject(
+  files: FileService,
+  workspace: WorkspaceService,
+): Promise<{ project: Modula2Project; rootUri: string } | undefined> {
+  const roots = await workspace.roots;
+  if (roots.length === 0) return undefined;
+  const rootUri = roots[0].resource;
+  try {
+    const content = await files.read(rootUri.resolve('modula2.json'));
+    return { project: parseProjectFile(rootUri.path.fsPath(), content.value), rootUri: rootUri.toString() };
+  } catch {
+    return undefined;
+  }
+}

+ 61 - 0
extensions/modula2-compiler/src/browser/modula2-tasks.ts

@@ -0,0 +1,61 @@
+import { inject, injectable } from '@theia/core/shared/inversify';
+import { FileService } from '@theia/filesystem/lib/browser/file-service';
+import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
+import {
+  TaskContribution, TaskProvider, TaskProviderRegistry,
+  TaskResolver, TaskResolverRegistry,
+} from '@theia/task/lib/browser/task-contribution';
+import { TaskConfiguration } from '@theia/task/lib/common/task-protocol';
+import { PanelKind, RevealKind } from '@theia/task/lib/common/task-protocol';
+import { defaultOutput, buildProvidedTasks } from '../common/compiler';
+import { loadWorkspaceProject } from './modula2-project';
+
+/** Task type for Modula-2 tasks; resolved to shell execution below. */
+export const MODULA2_TASK_TYPE = 'modula2';
+
+/** Provides Build/Clean/Rebuild/Run tasks from modula2.json. */
+@injectable()
+export class Modula2TaskProvider implements TaskProvider {
+  @inject(FileService) protected readonly files: FileService;
+  @inject(WorkspaceService) protected readonly workspace: WorkspaceService;
+
+  async provideTasks(): Promise<TaskConfiguration[]> {
+    const loaded = await loadWorkspaceProject(this.files, this.workspace);
+    if (!loaded) return [];
+    const { project, rootUri } = loaded;
+    return buildProvidedTasks(project).map(t => ({
+      label: t.label,
+      group: t.group === undefined ? 'none' : t.group === 'clean' ? 'clean' : { kind: t.group.kind, isDefault: t.group.isDefault ?? false },
+      command: t.command,
+      args: t.args,
+      dependsOn: t.dependsOn,
+      type: MODULA2_TASK_TYPE,
+      executionType: 'shell' as const,
+      options: { cwd: project.root },
+      presentation: { echo: true, reveal: RevealKind.Always, focus: false, panel: PanelKind.Shared },
+      _scope: rootUri,
+    }));
+  }
+}
+
+/** Resolves Modula-2 tasks to plain shell execution. */
+@injectable()
+export class Modula2TaskResolver implements TaskResolver {
+  async resolveTask(taskConfig: TaskConfiguration): Promise<TaskConfiguration> {
+    return { ...taskConfig, type: 'shell' };
+  }
+}
+
+@injectable()
+export class Modula2TaskContribution implements TaskContribution {
+  @inject(Modula2TaskProvider) protected readonly provider: Modula2TaskProvider;
+  @inject(Modula2TaskResolver) protected readonly resolver: Modula2TaskResolver;
+
+  registerProviders(providers: TaskProviderRegistry): void {
+    providers.register(MODULA2_TASK_TYPE, this.provider);
+  }
+
+  registerResolvers(resolvers: TaskResolverRegistry): void {
+    resolvers.registerTaskResolver(MODULA2_TASK_TYPE, this.resolver);
+  }
+}

+ 25 - 1
extensions/modula2-compiler/src/common/compiler.ts

@@ -46,6 +46,8 @@ export interface Modula2Project {
   /** Built executable, relative to root. Defaults to bin/<name>. */
   output?: string;
   compiler: CompilerOptions;
+  /** True when modula2.json sets an explicit compiler path. */
+  compilerPathExplicit: boolean;
 }
 
 export interface CompilerInfo {
@@ -81,15 +83,17 @@ export function parseProjectFile(root: string, jsonText: string): Modula2Project
   const options = Array.isArray(compiler.options)
     ? compiler.options.filter((o): o is string => typeof o === 'string')
     : [];
+  const explicitPath = typeof compiler.path === 'string' && compiler.path.length > 0;
   return {
     name: raw.name,
     root,
     main: raw.main,
     output: typeof raw.output === 'string' ? raw.output : undefined,
     compiler: {
-      path: typeof compiler.path === 'string' && compiler.path.length > 0 ? compiler.path : 'gm2',
+      path: explicitPath ? (compiler.path as string) : 'gm2',
       options,
     },
+    compilerPathExplicit: explicitPath,
   };
 }
 
@@ -97,3 +101,23 @@ export function parseProjectFile(root: string, jsonText: string): Modula2Project
 export function defaultOutput(project: Modula2Project): string {
   return project.output ?? `bin/${project.name}`;
 }
+
+export interface ProvidedTask {
+  label: string;
+  group?: { kind: 'build' | 'rebuild'; isDefault?: boolean } | 'clean';
+  command: string;
+  args: string[];
+  dependsOn?: string;
+}
+
+/** Pure task list for a project (tested headlessly; the Theia provider wraps these). */
+export function buildProvidedTasks(project: Modula2Project): ProvidedTask[] {
+  const out = defaultOutput(project);
+  const buildArgs = ['-fiso', ...project.compiler.options, '-o', out, project.main];
+  return [
+    { label: 'Modula-2: Build', group: { kind: 'build', isDefault: true }, command: project.compiler.path, args: buildArgs },
+    { label: 'Modula-2: Clean', group: 'clean', command: 'rm', args: ['-f', out] },
+    { label: 'Modula-2: Rebuild', group: { kind: 'rebuild' }, command: project.compiler.path, args: buildArgs },
+    { label: 'Modula-2: Run', command: `./${out}`, args: [], dependsOn: 'Modula-2: Build' },
+  ];
+}