Procházet zdrojové kódy

WITH completion for Modula-2 editors

Guard-record fields are offered bare inside WITH bodies
(withFieldSymbols, innermost guard first, shadowed names dropped),
via declaration identity so types invisible under their own name and
cross-file .def records work too. Merged into the completion list
alongside scope symbols.

Validation: headless lists (in-body, nested, imported-record
guards, silent outside); live LSP field items with details;
GUI-verified suggest widget; 21-file corpora at zero errors.
Eric Streit před 6 dny
rodič
revize
9705c7352d

+ 6 - 8
PLAN.md

@@ -1,20 +1,18 @@
-# Plan — next steps (from v0.26.0)
+# Plan — next steps (from v0.27.0)
 
-Status: IDE complete through v0.26.0, distributables fresh at v0.24.0
+Status: IDE complete through v0.27.0, distributables fresh at v0.24.0
 (AppImage + deb in gitignored `apps/electron/dist/`). Tree clean except
 this file; all tags local.
 
 ## Done since the last plan
 
-- v0.26.0 WITH hover support (hover, go-to-definition, references and
-  rename for bare field names in `WITH` bodies, innermost guard first;
-  reference search matches field declarations by position without
-  cross-symbol leakage; GUI-verified with squiggle on unknown field).
+- v0.27.0 WITH completion (guard-record fields offered bare inside
+  `WITH` bodies, innermost first; GUI-verified suggest widget).
 
 ## Next, in priority order
 
-1. **WITH completion** — field-name completion inside `WITH` bodies
-   (hover/definition/references done in v0.26.0).
+1. **Coco/R differential** — differential testing of the Coco/R grammar
+   path against `gm2` (last remaining listed item).
 
 ## Also pending (undecided)
 

+ 46 - 1
extensions/modula2-language/src/m2/resolve.ts

@@ -11,7 +11,7 @@ import { lex, Token } from './lexer';
 import { M2Import, M2Symbol, M2Unit, parseUnitText, rangeContains } from './parser';
 import { gm2LibraryDirs, projectSearchDirs } from './defpaths';
 import {
-  findFieldInType, findRecordField, resolveDeclaredType, resolveType, unitOfFile,
+  findFieldInType, findRecordField, findTypeByDecl, resolveDeclaredType, resolveType, unitOfFile,
 } from './types';
 import type { M2Type } from './types';
 
@@ -501,6 +501,51 @@ function skipBalanced(tokens: Token[], from: number, open: string, close: string
   return -1;
 }
 
+/** Field declarations visible bare through enclosing WITH guards.
+ *
+ *  Innermost guard first, shadowed names dropped — mirroring what the
+ *  checker accepts and what hover resolves, for completion lists.
+ *  Unknowable guards contribute nothing. */
+export function withFieldSymbols(
+  unit: M2Unit, docDir: string, tokens: Token[], line: number, ch: number,
+): M2Symbol[] {
+  const blocks = withBlocksOf(tokens)
+    .filter(b => posInBody(b, tokens, line, ch))
+    .sort((a, b) => (a.endIdx - a.bodyFromIdx) - (b.endIdx - b.bodyFromIdx));
+  const seen = new Set<string>();
+  const out: M2Symbol[] = [];
+  for (const b of blocks) {
+    const gt = guardRecord(unit, docDir, tokens, b, 0);
+    if (!gt) continue;
+    for (const f of recordFieldDecls(unit, docDir, gt, line, ch)) {
+      if (f.name && !seen.has(f.name)) { seen.add(f.name); out.push(f); }
+    }
+  }
+  return out;
+}
+
+/** All field declarations of a record type: declaration identity first
+ *  (reaches types invisible under their own name), name lookup after. */
+function recordFieldDecls(
+  unit: M2Unit, docDir: string, t: M2Type, line: number, ch: number,
+): M2Symbol[] {
+  const b = t.kind === 'subrange' ? t.base : t;
+  if (b.kind !== 'record' || !b.name) return [];
+  const fields = (s: M2Symbol): M2Symbol[] => s.children.filter(c => c.kind === 'field');
+  if (b.decl) {
+    const r = findTypeByDecl(unit, b.decl);
+    if (r && r.sym.kind === 'type') return fields(r.sym);
+  }
+  const dot = b.name.lastIndexOf('.');
+  const r = resolveName(
+    unit, docDir, line, ch,
+    dot >= 0 ? b.name.slice(0, dot) : null,
+    dot >= 0 ? b.name.slice(dot + 1) : b.name,
+  );
+  if (r && r.sym.kind === 'type') return fields(r.sym);
+  return [];
+}
+
 /** Exported symbols of a module (for `Module.` completion). */
 export function moduleExports(docDir: string, moduleName: string): M2Symbol[] {
   const def = loadDefModule(docDir, moduleName);

+ 3 - 2
extensions/modula2-language/src/m2/types.ts

@@ -709,8 +709,9 @@ export function findFieldInType(
 }
 
 /** Locate a type symbol through a stamped declaration identity
- *  (`filePath:line:ch` as built by `attachName`). */
-function findTypeByDecl(
+ *  (`filePath:line:ch` as built by `attachName`). Exported for WITH
+ *  field enumeration, which needs all fields rather than one lookup. */
+export function findTypeByDecl(
   hostUnit: M2Unit, decl: string,
 ): { sym: M2Symbol; filePath: string } | null {
   const m = /^(.*):(\d+):(\d+)$/.exec(decl);

+ 6 - 2
extensions/modula2-language/src/server.ts

@@ -17,7 +17,7 @@ import { computeSemanticTokens, semanticTokensLegend } from './m2/semantic';
 import { DEFAULT_FORMAT_OPTIONS, FormatOptions, formatDocument, formatRangeEdits, normalizeFormatOptions } from './m2/format';
 import {
   moduleExports, recordFieldsOf, resolveName, resolveNameWith, toLocation, visibleSymbols,
-  findReferencesInText, LocatedOccurrence,
+  withFieldSymbols, findReferencesInText, LocatedOccurrence,
 } from './m2/resolve';
 
 const connection = createConnection(ProposedFeatures.all);
@@ -248,7 +248,7 @@ connection.onDocumentSymbol(params => {
 connection.onCompletion(params => {
   const document = documents.get(params.textDocument.uri);
   if (!document) return [];
-  const { unit } = parseDocument(document);
+  const { unit, tokens } = parseDocument(document);
   const filePath = uriToFilePath(document.uri);
   const docDir = path.dirname(filePath);
   const offset = document.offsetAt(params.position);
@@ -294,6 +294,10 @@ connection.onCompletion(params => {
 
   const items: CompletionItem[] = visibleSymbols(unit, params.position.line, params.position.character)
     .map(s => ({ label: s.name, kind: completionKind(s.kind), detail: s.detail } as CompletionItem));
+  // WITH guards in scope: their fields are visible bare (innermost first).
+  for (const f of withFieldSymbols(unit, docDir, tokens, params.position.line, params.position.character)) {
+    items.push({ label: f.name, kind: CompletionItemKind.Field, detail: f.detail } as CompletionItem);
+  }
   for (const kw of SERVER_KEYWORDS) {
     items.push({ label: kw, kind: CompletionItemKind.Keyword, detail: 'Modula-2 keyword' });
   }