|
|
@@ -0,0 +1,135 @@
|
|
|
+import { ContainerModule } from '@theia/core/shared/inversify';
|
|
|
+import { inject, injectable } from '@theia/core/shared/inversify';
|
|
|
+import {
|
|
|
+ Command, CommandContribution, CommandRegistry, MessageService,
|
|
|
+} from '@theia/core/lib/common';
|
|
|
+import { RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider';
|
|
|
+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 {
|
|
|
+ BuildResult, Modula2Compiler, Modula2CompilerServicePath,
|
|
|
+ Modula2Project, parseProjectFile,
|
|
|
+} from '../common/compiler';
|
|
|
+
|
|
|
+const CATEGORY = 'Modula-2';
|
|
|
+
|
|
|
+const Commands = {
|
|
|
+ build: Command.toLocalizedCommand({ id: 'modula2.build', label: 'Build', category: CATEGORY }),
|
|
|
+ buildFile: Command.toLocalizedCommand({ id: 'modula2.buildFile', label: 'Build File', category: CATEGORY }),
|
|
|
+ clean: Command.toLocalizedCommand({ id: 'modula2.clean', label: 'Clean', category: CATEGORY }),
|
|
|
+ rebuild: Command.toLocalizedCommand({ id: 'modula2.rebuild', label: 'Rebuild', category: CATEGORY }),
|
|
|
+ run: Command.toLocalizedCommand({ id: 'modula2.run', label: 'Run', category: CATEGORY }),
|
|
|
+ detect: Command.toLocalizedCommand({ id: 'modula2.detectCompiler', label: 'Detect GNU Modula-2', category: CATEGORY }),
|
|
|
+};
|
|
|
+
|
|
|
+@injectable()
|
|
|
+class CompilerCommands implements CommandContribution {
|
|
|
+ @inject(Modula2Compiler) protected readonly compiler: Modula2Compiler;
|
|
|
+ @inject(FileService) protected readonly files: FileService;
|
|
|
+ @inject(WorkspaceService) protected readonly workspace: WorkspaceService;
|
|
|
+ @inject(EditorManager) protected readonly editors: EditorManager;
|
|
|
+ @inject(MessageService) protected readonly messages: MessageService;
|
|
|
+
|
|
|
+ registerCommands(commands: CommandRegistry): void {
|
|
|
+ commands.registerCommand(Commands.detect, { execute: () => this.detect() });
|
|
|
+ commands.registerCommand(Commands.build, { execute: () => this.withProject(p => this.build(p)) });
|
|
|
+ commands.registerCommand(Commands.buildFile, { execute: () => this.buildFile() });
|
|
|
+ commands.registerCommand(Commands.clean, { execute: () => this.withProject(p => this.clean(p)) });
|
|
|
+ commands.registerCommand(Commands.rebuild, {
|
|
|
+ execute: () => this.withProject(async p => { await this.compiler.clean(p); await this.build(p); }),
|
|
|
+ });
|
|
|
+ commands.registerCommand(Commands.run, { execute: () => this.withProject(p => this.run(p)) });
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Load modula2.json from the first workspace root. */
|
|
|
+ protected async loadProject(): Promise<Modula2Project | undefined> {
|
|
|
+ const roots = await this.workspace.roots;
|
|
|
+ if (roots.length === 0) {
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ protected async withProject(op: (project: Modula2Project) => Promise<void>): Promise<void> {
|
|
|
+ const project = await this.loadProject();
|
|
|
+ if (project) {
|
|
|
+ try {
|
|
|
+ await op(project);
|
|
|
+ } catch (error) {
|
|
|
+ this.messages.error(`Modula-2: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ protected summarize(result: BuildResult): string {
|
|
|
+ const errors = result.diagnostics.filter(d => d.severity === 'error').length;
|
|
|
+ const warnings = result.diagnostics.filter(d => d.severity === 'warning').length;
|
|
|
+ return `${errors} error(s), ${warnings} warning(s). Command: ${result.command}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected async build(project: Modula2Project): Promise<void> {
|
|
|
+ const result = await this.compiler.build(project);
|
|
|
+ if (result.success) {
|
|
|
+ this.messages.info(`Modula-2: build succeeded. ${this.summarize(result)}`);
|
|
|
+ } else {
|
|
|
+ const first = result.diagnostics.slice(0, 3)
|
|
|
+ .map(d => `${d.file}:${d.line + 1}:${d.character + 1}: ${d.message}`).join('\n');
|
|
|
+ this.messages.error(`Modula-2: build failed. ${this.summarize(result)}${first ? '\n' + first : ''}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Build the Modula-2 file in the active editor instead of the project main. */
|
|
|
+ protected async buildFile(): Promise<void> {
|
|
|
+ const uri = this.editors.currentEditor?.getResourceUri();
|
|
|
+ if (!uri || !(uri.path.ext === '.mod' || uri.path.ext === '.def')) {
|
|
|
+ this.messages.warn('Modula-2: open a .mod or .def file to build it.');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const project = await this.loadProject();
|
|
|
+ if (!project) return;
|
|
|
+ const root = (await this.workspace.roots)[0].resource;
|
|
|
+ const relative = root.relative(uri)?.toString() ?? uri.path.toString();
|
|
|
+ await this.build({ ...project, main: relative });
|
|
|
+ }
|
|
|
+
|
|
|
+ protected async clean(project: Modula2Project): Promise<void> {
|
|
|
+ await this.compiler.clean(project);
|
|
|
+ this.messages.info('Modula-2: cleaned build outputs.');
|
|
|
+ }
|
|
|
+
|
|
|
+ protected async run(project: Modula2Project): Promise<void> {
|
|
|
+ const result = await this.compiler.run(project);
|
|
|
+ if (!result.success && result.diagnostics.length > 0) {
|
|
|
+ await this.build(project);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const output = result.output.length > 2000 ? result.output.slice(0, 2000) + '\n…' : result.output;
|
|
|
+ this.messages.info(`Modula-2: program exited with code ${result.exitCode}.\n${output}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected async detect(): Promise<void> {
|
|
|
+ const info = await this.compiler.detect('gm2');
|
|
|
+ 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.');
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export default new ContainerModule(bind => {
|
|
|
+ bind(Modula2Compiler).toDynamicValue(ctx => {
|
|
|
+ const provider = ctx.container.get<ServiceConnectionProvider>(RemoteConnectionProvider);
|
|
|
+ return provider.createProxy<Modula2Compiler>(Modula2CompilerServicePath);
|
|
|
+ }).inSingletonScope();
|
|
|
+ bind(CommandContribution).to(CompilerCommands).inSingletonScope();
|
|
|
+});
|