| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- import { ContainerModule } from '@theia/core/shared/inversify';
- import { inject, injectable } from '@theia/core/shared/inversify';
- import {
- Command, CommandContribution, CommandRegistry, MenuContribution,
- MenuModelRegistry, MessageService,
- } from '@theia/core/lib/common';
- import { WidgetFactory, bindViewContribution, FrontendApplicationContribution } from '@theia/core/lib/browser';
- import { QuickInputService } from '@theia/core/lib/common/quick-pick-service';
- import { StorageService } from '@theia/core/lib/browser/storage-service';
- import { BinaryBuffer } from '@theia/core/lib/common/buffer';
- import { FileService } from '@theia/filesystem/lib/browser/file-service';
- import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
- import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
- import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
- import URI from '@theia/core/lib/common/uri';
- import {
- ProjectTemplate, TEMPLATES, declaredSourceDirs, isValidModuleName,
- isValidProjectName, moduleNameOf, renderDefModule, renderImplModule,
- scaffoldFiles, validateProjectFile,
- } from './modula2-project';
- import {
- MODULA2_EXPLORER_ID, Modula2ExplorerCommands, Modula2ExplorerContainerFactory,
- Modula2ExplorerContribution, Modula2ExplorerMenus, createModula2ExplorerWidget,
- } from './modula2-explorer-widget';
- const CATEGORY = 'Modula-2';
- const Commands = {
- newModule: Command.toLocalizedCommand({ id: 'modula2.newModule', label: 'New Module...', category: CATEGORY }),
- newDefModule: Command.toLocalizedCommand({ id: 'modula2.newDefModule', label: 'New Definition Module...', category: CATEGORY }),
- newProject: Command.toLocalizedCommand({ id: 'modula2.newProject', label: 'New Modula-2 Project...', category: CATEGORY }),
- openProject: Command.toLocalizedCommand({ id: 'modula2.openProject', label: 'Open Project...', category: CATEGORY }),
- switchProject: Command.toLocalizedCommand({ id: 'modula2.switchProject', label: 'Switch Project...', category: CATEGORY }),
- closeProject: Command.toLocalizedCommand({ id: 'modula2.closeProject', label: 'Close Project', category: CATEGORY }),
- validate: Command.toLocalizedCommand({ id: 'modula2.validateProject', label: 'Validate Project', category: CATEGORY }),
- };
- @injectable()
- class ProjectCommands implements CommandContribution {
- @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;
- @inject(CommandRegistry) protected readonly commands: CommandRegistry;
- @inject(StorageService) protected readonly storage: StorageService;
- registerCommands(commands: CommandRegistry): void {
- commands.registerCommand(Commands.newModule, { execute: () => this.newModule(false) });
- commands.registerCommand(Commands.newDefModule, { execute: () => this.newModule(true) });
- commands.registerCommand(Commands.newProject, { execute: () => this.newProject() });
- commands.registerCommand(Commands.openProject, { execute: () => this.openProject() });
- commands.registerCommand(Commands.switchProject, { execute: () => this.switchProject() });
- commands.registerCommand(Commands.closeProject, { execute: () => this.closeProject() });
- commands.registerCommand(Commands.validate, { execute: () => this.validateProject() });
- }
- protected async root(): Promise<URI | undefined> {
- const roots = await this.workspace.roots;
- if (roots.length === 0) {
- this.messages.warn('Modula-2: open a folder first (File > Open Folder).');
- return undefined;
- }
- return roots[0].resource;
- }
- /** Candidate source directories: declared ones that exist, then src/, then the root. */
- protected async sourceDirChoices(root: URI): Promise<string[]> {
- const declared: string[] = [];
- try {
- const content = await this.files.read(root.resolve('modula2.json'));
- declared.push(...declaredSourceDirs(content.value));
- } catch {
- // No project file: fall through to conventions.
- }
- const out: string[] = [];
- for (const d of [...declared, 'src']) {
- if (d === '.' || d === '') continue;
- try {
- if (await this.files.exists(root.resolve(d))) out.push(d);
- } catch { /* ignore */ }
- }
- out.push('.');
- return [...new Set(out)];
- }
- protected async askModuleName(): Promise<string | undefined> {
- return this.quickInput.input({
- prompt: 'Module name (a Modula-2 identifier, e.g. MyModule)',
- placeHolder: 'MyModule',
- validateInput: async value => isValidModuleName(value.trim()) ?? null,
- });
- }
- protected async newModule(defOnly: boolean): Promise<void> {
- const root = await this.root();
- if (!root) return;
- const raw = await this.askModuleName();
- if (raw === undefined) return;
- const name = raw.trim();
- let makeDef = defOnly;
- let makeMod = !defOnly;
- if (!defOnly) {
- 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: 'Definition only', description: 'MyModule.def', value: 'def' }],
- { placeholder: 'What to create?' },
- );
- if (!kind) return;
- makeDef = kind.value !== 'mod';
- makeMod = kind.value !== 'def';
- }
- const choices = await this.sourceDirChoices(root);
- let dir = choices[0];
- if (choices.length > 1) {
- const picked = await this.quickInput.showQuickPick(
- choices.map(c => ({ label: c === '.' ? '(project root)' : c, value: c })),
- { placeholder: 'Where to create the module?' },
- );
- if (!picked) return;
- dir = picked.value;
- }
- const created: string[] = [];
- const write = async (file: string, content: string): Promise<boolean> => {
- const uri = dir === '.' ? root.resolve(file) : root.resolve(`${dir}/${file}`);
- try {
- if (await this.files.exists(uri)) {
- this.messages.warn(`Modula-2: ${file} already exists, skipped.`);
- return false;
- }
- await this.files.writeFile(uri, BinaryBuffer.fromString(content));
- created.push(file);
- return true;
- } catch (error) {
- this.messages.error(`Modula-2: cannot write ${file}: ${error instanceof Error ? error.message : String(error)}`);
- return false;
- }
- };
- if (makeMod) await write(`${name}.mod`, renderImplModule(name));
- if (makeDef) await write(`${name}.def`, renderDefModule(name));
- if (created.length === 0) return;
- this.messages.info(`Modula-2: created ${created.join(', ')}.`);
- const first = dir === '.' ? root.resolve(created[0]) : root.resolve(`${dir}/${created[0]}`);
- try {
- await this.editors.open(first, { mode: 'activate' });
- } catch { /* created is enough */ }
- }
- protected async newProject(): Promise<void> {
- const root = await this.root();
- if (!root) return;
- const rawName = await this.quickInput.input({
- prompt: 'Project name (used for the folder and modula2.json)',
- placeHolder: 'HelloWorld',
- validateInput: async value => isValidProjectName(value.trim()) ?? null,
- });
- if (rawName === undefined) return;
- const name = rawName.trim();
- const template = await this.quickInput.showQuickPick(
- TEMPLATES.map(t => ({ label: t.label, description: t.description, value: t.id as ProjectTemplate })),
- { placeholder: 'Project template' },
- );
- if (!template) return;
- // Scaffold at the root when it is empty, else in a subfolder.
- let target = root;
- try {
- const stat = await this.files.resolve(root);
- if ((stat.children ?? []).length > 0) target = root.resolve(name);
- } catch {
- target = root.resolve(name);
- }
- const files = scaffoldFiles(name, template.value);
- try {
- await this.files.createFolder(target);
- for (const f of files) {
- const uri = target.resolve(f.path);
- const slash = f.path.lastIndexOf('/');
- if (slash > 0) await this.files.createFolder(uri.parent);
- if (await this.files.exists(uri)) {
- this.messages.warn(`Modula-2: ${f.path} already exists, skipped.`);
- continue;
- }
- await this.files.writeFile(uri, BinaryBuffer.fromString(f.content));
- }
- } catch (error) {
- this.messages.error(`Modula-2: cannot scaffold project: ${error instanceof Error ? error.message : String(error)}`);
- return;
- }
- const where = target.toString() === root.toString() ? 'the open folder' : `the "${name}" folder`;
- this.messages.info(`Modula-2: created ${template.label} "${name}" in ${where}. Open modula2.json to review, then Modula-2: Build.`);
- }
- /** Open a project folder (delegates to the workspace folder dialog). */
- protected async openProject(): Promise<void> {
- await this.commands.executeCommand(WorkspaceCommands.OPEN_FOLDER.id);
- }
- /** Switch to a recently opened project (delegates to the recent picker). */
- protected async switchProject(): Promise<void> {
- await this.recordCurrentProject();
- const recent = await this.recentProjects();
- const picks = recent.map(r => ({
- label: r.name, description: r.path, value: r.path,
- }));
- picks.push({ label: 'Browse for folder...', description: 'Open any folder as a project', value: '' });
- const choice = await this.quickInput.showQuickPick(picks, { placeholder: 'Switch to project' });
- if (!choice) return;
- if (!choice.value) {
- await this.commands.executeCommand(WorkspaceCommands.OPEN_FOLDER.id);
- return;
- }
- this.workspace.open(new URI(choice.value));
- }
- /** Remembered Modula-2 projects (most recent first, max 12). */
- protected async recentProjects(): Promise<{ name: string; path: string }[]> {
- try {
- const raw = await this.storage.getData<{ name: string; path: string }[]>('modula2.recentProjects', []);
- if (!Array.isArray(raw)) return [];
- const live: { name: string; path: string }[] = [];
- for (const r of raw) {
- if (!r || typeof r.name !== 'string' || typeof r.path !== 'string') continue;
- try {
- if (await this.files.exists(new URI(r.path))) live.push({ name: r.name, path: r.path });
- } catch { /* drop dead entries */ }
- }
- return live.slice(0, 12);
- } catch {
- return [];
- }
- }
- /** Record the current workspace when it holds a Modula-2 project. */
- protected async recordCurrentProject(): Promise<void> {
- try {
- const roots = await this.workspace.roots;
- if (roots.length === 0) return;
- const root = roots[0].resource;
- const raw = await this.files.read(root.resolve('modula2.json')).catch(() => undefined);
- if (!raw) return;
- const json = JSON.parse(raw.value) as { name?: unknown };
- if (typeof json.name !== 'string' || json.name.length === 0) return;
- const entry = { name: json.name, path: root.toString() };
- const prev = await this.recentProjects();
- const next = [entry, ...prev.filter(r => r.path !== entry.path)].slice(0, 12);
- await this.storage.setData('modula2.recentProjects', next);
- } catch { /* best-effort history */ }
- }
- /** Close the current project (delegates to workspace close). */
- protected async closeProject(): Promise<void> {
- await this.commands.executeCommand(WorkspaceCommands.CLOSE.id);
- }
- protected async validateProject(): Promise<void> {
- const root = await this.root();
- if (!root) return;
- const uri = root.resolve('modula2.json');
- let text: string;
- try {
- text = (await this.files.read(uri)).value;
- } catch {
- this.messages.warn('Modula-2: no modula2.json in the open folder. Create one with Modula-2: New Modula-2 Project.');
- return;
- }
- const problems = validateProjectFile(text);
- if (problems.length > 0) {
- this.messages.error(`Modula-2: invalid project.\n${problems.join('\n')}`);
- return;
- }
- // Existence checks against the workspace.
- let raw: { name: string; main: string; sourceDirectories?: string[]; definitionDirectories?: string[] };
- try {
- raw = JSON.parse(text);
- } catch {
- this.messages.error('Modula-2: invalid project.\nmodula2.json: invalid JSON.');
- return;
- }
- const missing: string[] = [];
- try {
- if (!await this.files.exists(root.resolve(raw.main))) missing.push(`main file "${raw.main}" does not exist`);
- } catch { /* ignore */ }
- for (const key of ['sourceDirectories', 'definitionDirectories'] as const) {
- for (const d of raw[key] ?? []) {
- try {
- if (!await this.files.exists(root.resolve(d))) missing.push(`"${key}" entry "${d}" does not exist`);
- } catch { /* ignore */ }
- }
- }
- if (missing.length > 0) {
- this.messages.error(`Modula-2: project "${raw.name}" has problems.\n${missing.join('\n')}`);
- return;
- }
- this.messages.info(`Modula-2: project "${raw.name}" is valid (main: ${raw.main}).`);
- }
- }
- class ProjectMenus implements MenuContribution {
- registerMenus(menus: MenuModelRegistry): void {
- const submenu = ['menubar', 'modula2'];
- menus.registerMenuAction(submenu, { commandId: Commands.newModule.id, order: '20' });
- menus.registerMenuAction(submenu, { commandId: Commands.newDefModule.id, order: '21' });
- menus.registerMenuAction(submenu, { commandId: Commands.newProject.id, order: '22' });
- menus.registerMenuAction(submenu, { commandId: Commands.validate.id, order: '23' });
- const project = ['menubar', 'project'];
- menus.registerSubmenu(project, 'Project');
- menus.registerMenuAction(project, { commandId: Commands.newProject.id, order: '10' });
- menus.registerMenuAction(project, { commandId: Commands.newModule.id, order: '11' });
- menus.registerMenuAction(project, { commandId: Commands.newDefModule.id, order: '12' });
- menus.registerMenuAction(project, { commandId: Commands.openProject.id, order: '13' });
- menus.registerMenuAction(project, { commandId: Commands.switchProject.id, order: '14' });
- menus.registerMenuAction(project, { commandId: Commands.closeProject.id, order: '15' });
- }
- }
- export default new ContainerModule(bind => {
- bind(CommandContribution).to(ProjectCommands).inSingletonScope();
- bind(MenuContribution).to(ProjectMenus).inSingletonScope();
- bindViewContribution(bind, Modula2ExplorerContribution);
- bind(FrontendApplicationContribution).toService(Modula2ExplorerContribution);
- bind(WidgetFactory).toDynamicValue(context => ({
- id: MODULA2_EXPLORER_ID,
- createWidget: () => createModula2ExplorerWidget(context.container),
- }));
- bind(Modula2ExplorerContainerFactory).toSelf().inSingletonScope();
- bind(WidgetFactory).toService(Modula2ExplorerContainerFactory);
- bind(CommandContribution).to(Modula2ExplorerCommands).inSingletonScope();
- bind(MenuContribution).to(Modula2ExplorerMenus).inSingletonScope();
- });
|