|
|
@@ -0,0 +1,185 @@
|
|
|
+/** Search paths for definition modules (`.def`).
|
|
|
+ *
|
|
|
+ * `loadDefModule` used to look only in the open file's directory, so
|
|
|
+ * go-to-definition (and hover, completion, references) silently failed
|
|
|
+ * for every import whose `.def` lived anywhere else — other project
|
|
|
+ * folders or gm2's own libraries (`InOut`, `TextIO`, ...). The search
|
|
|
+ * order is now: the document's directory, the project's
|
|
|
+ * `definitionDirectories`/`sourceDirectories`/`libraryDirectories`
|
|
|
+ * from the nearest `modula2.json`, then gm2's library sources (`m2iso` first, matching
|
|
|
+ * the default `-fiso` build; a `-fpim`/`-flog`/`-fmin` flag in the
|
|
|
+ * project's compiler options promotes its dialect instead).
|
|
|
+ *
|
|
|
+ * Best-effort throughout: missing files, unparsable configs and gm2
|
|
|
+ * probe failures shrink the list to what is known, never throw.
|
|
|
+ */
|
|
|
+
|
|
|
+import { execFileSync } from 'child_process';
|
|
|
+import * as fs from 'fs';
|
|
|
+import * as path from 'path';
|
|
|
+
|
|
|
+const PROJECT_FILE = 'modula2.json';
|
|
|
+const MAX_UP = 8;
|
|
|
+
|
|
|
+/** Dialect library folders in default search order (ISO first). */
|
|
|
+const DIALECTS = ['m2iso', 'm2pim', 'm2log', 'm2min', 'm2cor'];
|
|
|
+
|
|
|
+interface ProjectEntry {
|
|
|
+ file: string;
|
|
|
+ mtimeMs: number;
|
|
|
+ dirs: string[];
|
|
|
+ dialectFirst: string | null;
|
|
|
+}
|
|
|
+
|
|
|
+const projectCache = new Map<string, ProjectEntry>();
|
|
|
+
|
|
|
+let gm2Cache: { at: number; key: string; dirs: string[] } | null = null;
|
|
|
+const GM2_TTL_MS = 5 * 60 * 1000;
|
|
|
+
|
|
|
+/** Extra `.def` search directories for a document (project folders). */
|
|
|
+export function projectSearchDirs(docDir: string): string[] {
|
|
|
+ const found = findProjectFile(docDir);
|
|
|
+ if (!found) return [];
|
|
|
+ try {
|
|
|
+ const mtimeMs = fs.statSync(found).mtimeMs;
|
|
|
+ const hit = projectCache.get(docDir);
|
|
|
+ if (hit && hit.file === found && hit.mtimeMs === mtimeMs) return hit.dirs;
|
|
|
+ const { dirs, dialectFirst } = readProjectDirs(found);
|
|
|
+ projectCache.set(docDir, { file: found, mtimeMs, dirs, dialectFirst });
|
|
|
+ return dirs;
|
|
|
+ } catch {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** Preferred gm2 dialect folder for a document (`m2iso` default). */
|
|
|
+export function preferredDialect(docDir: string): string | null {
|
|
|
+ const found = findProjectFile(docDir);
|
|
|
+ if (!found) return null;
|
|
|
+ try {
|
|
|
+ const mtimeMs = fs.statSync(found).mtimeMs;
|
|
|
+ const hit = projectCache.get(docDir);
|
|
|
+ if (hit && hit.file === found && hit.mtimeMs === mtimeMs) return hit.dialectFirst;
|
|
|
+ const { dirs, dialectFirst } = readProjectDirs(found);
|
|
|
+ projectCache.set(docDir, { file: found, mtimeMs, dirs, dialectFirst });
|
|
|
+ return dialectFirst;
|
|
|
+ } catch {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** gm2 library source folders (`.../m2/m2iso`, ...), cached for 5 minutes. */
|
|
|
+export function gm2LibraryDirs(docDir: string): string[] {
|
|
|
+ const now = Date.now();
|
|
|
+ const key = `${docDir}`;
|
|
|
+ if (gm2Cache && now - gm2Cache.at < GM2_TTL_MS && gm2Cache.key === key) return gm2Cache.dirs;
|
|
|
+ const dirs = probeGm2Libs(docDir);
|
|
|
+ gm2Cache = { at: now, key, dirs };
|
|
|
+ return dirs;
|
|
|
+}
|
|
|
+
|
|
|
+/** Nearest `modula2.json` walking up from `startDir`, or null. */
|
|
|
+function findProjectFile(startDir: string): string | null {
|
|
|
+ let dir = path.resolve(startDir);
|
|
|
+ for (let k = 0; k < MAX_UP; k++) {
|
|
|
+ const candidate = path.join(dir, PROJECT_FILE);
|
|
|
+ try {
|
|
|
+ if (fs.statSync(candidate).isFile()) return candidate;
|
|
|
+ } catch {
|
|
|
+ // absent: keep climbing
|
|
|
+ }
|
|
|
+ const parent = path.dirname(dir);
|
|
|
+ if (parent === dir) return null;
|
|
|
+ dir = parent;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+function readProjectDirs(file: string): { dirs: string[]; dialectFirst: string | null } {
|
|
|
+ const root = path.dirname(file);
|
|
|
+ let json: {
|
|
|
+ sourceDirectories?: unknown; definitionDirectories?: unknown;
|
|
|
+ libraryDirectories?: unknown;
|
|
|
+ compiler?: { path?: unknown; options?: unknown };
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ json = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
|
+ } catch {
|
|
|
+ return { dirs: [], dialectFirst: null };
|
|
|
+ }
|
|
|
+ const dirs: string[] = [];
|
|
|
+ for (const key of ['definitionDirectories', 'sourceDirectories', 'libraryDirectories'] as const) {
|
|
|
+ const raw = json[key];
|
|
|
+ if (!Array.isArray(raw)) continue;
|
|
|
+ for (const entry of raw) {
|
|
|
+ if (typeof entry !== 'string' || !entry) continue;
|
|
|
+ const abs = path.resolve(root, entry);
|
|
|
+ try {
|
|
|
+ if (!fs.statSync(abs).isDirectory()) continue;
|
|
|
+ } catch {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (!dirs.includes(abs)) dirs.push(abs);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return { dirs, dialectFirst: dialectFromOptions(json.compiler?.options) };
|
|
|
+}
|
|
|
+
|
|
|
+/** `m2pim` for `-fpim*`, `m2log` for `-flog`, ...; null keeps the default. */
|
|
|
+function dialectFromOptions(options: unknown): string | null {
|
|
|
+ if (!Array.isArray(options)) return null;
|
|
|
+ for (const opt of options) {
|
|
|
+ if (typeof opt !== 'string') continue;
|
|
|
+ const m = /^-f(pim\d*|iso|log|min)\b/.exec(opt.trim());
|
|
|
+ if (m) return 'm2' + m[1].replace(/\d+$/, '');
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+function compilerBinary(docDir: string): string {
|
|
|
+ const found = findProjectFile(docDir);
|
|
|
+ if (found) {
|
|
|
+ try {
|
|
|
+ const json = JSON.parse(fs.readFileSync(found, 'utf8')) as {
|
|
|
+ compiler?: { path?: unknown };
|
|
|
+ };
|
|
|
+ const p = json.compiler?.path;
|
|
|
+ if (typeof p === 'string' && p) return p;
|
|
|
+ } catch {
|
|
|
+ // fall through to PATH lookup
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return 'gm2';
|
|
|
+}
|
|
|
+
|
|
|
+function probeGm2Libs(docDir: string): string[] {
|
|
|
+ let base: string;
|
|
|
+ try {
|
|
|
+ const out = execFileSync(compilerBinary(docDir), ['-print-search-dirs'], {
|
|
|
+ timeout: 15000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
|
+ });
|
|
|
+ const line = out.split('\n').map(l => l.trim()).find(l => /^install(és)?:/.test(l));
|
|
|
+ if (!line) return [];
|
|
|
+ const install = line.slice(line.indexOf(':') + 1).trim().replace(/\/+$/, '');
|
|
|
+ if (!install) return [];
|
|
|
+ base = path.join(install, 'm2');
|
|
|
+ } catch {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ const order = orderedDialects(preferredDialect(docDir));
|
|
|
+ const out: string[] = [];
|
|
|
+ for (const d of order) {
|
|
|
+ const abs = path.join(base, d);
|
|
|
+ try {
|
|
|
+ if (fs.statSync(abs).isDirectory()) out.push(abs);
|
|
|
+ } catch {
|
|
|
+ // dialect not installed: skip
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
+function orderedDialects(first: string | null): string[] {
|
|
|
+ if (!first || !DIALECTS.includes(first)) return [...DIALECTS];
|
|
|
+ return [first, ...DIALECTS.filter(d => d !== first)];
|
|
|
+}
|