|
|
@@ -7,13 +7,14 @@ import {
|
|
|
import { TextDocument } from 'vscode-languageserver-textdocument';
|
|
|
import * as path from 'path';
|
|
|
import * as os from 'os';
|
|
|
-import { promises as fs } from 'fs';
|
|
|
+import { promises as fs, readFileSync } from 'fs';
|
|
|
import { spawn } from 'child_process';
|
|
|
import { pathToFileURL } from 'url';
|
|
|
import { lex, Token } from './m2/lexer';
|
|
|
import { M2Symbol, M2Unit, parseUnitText } from './m2/parser';
|
|
|
import {
|
|
|
moduleExports, recordFieldsOf, resolveName, toLocation, visibleSymbols,
|
|
|
+ findReferencesInText, LocatedOccurrence,
|
|
|
} from './m2/resolve';
|
|
|
|
|
|
const connection = createConnection(ProposedFeatures.all);
|
|
|
@@ -32,6 +33,8 @@ connection.onInitialize((params: InitializeParams): InitializeResult => {
|
|
|
hoverProvider: true,
|
|
|
definitionProvider: true,
|
|
|
documentSymbolProvider: true,
|
|
|
+ referencesProvider: true,
|
|
|
+ renameProvider: { prepareProvider: true },
|
|
|
},
|
|
|
};
|
|
|
});
|
|
|
@@ -188,9 +191,7 @@ function toDocumentSymbol(s: M2Symbol): DocumentSymbol {
|
|
|
end: { line: s.nameRange.endLine, character: s.nameRange.endCh },
|
|
|
},
|
|
|
detail: s.detail,
|
|
|
- children: s.children
|
|
|
- .filter(c => c.kind === 'procedure' || c.kind === 'module' || c.kind === 'type')
|
|
|
- .map(toDocumentSymbol),
|
|
|
+ children: s.children.map(toDocumentSymbol),
|
|
|
};
|
|
|
}
|
|
|
|
|
|
@@ -307,5 +308,124 @@ connection.onDefinition(params => {
|
|
|
return location;
|
|
|
});
|
|
|
|
|
|
+/** Resolve the declaration for a rename/references request, or null. */
|
|
|
+function resolveRequest(
|
|
|
+ document: TextDocument, line: number, character: number,
|
|
|
+): { unit: M2Unit; filePath: string; qualifier: string | null; name: string } | null {
|
|
|
+ const { unit, tokens } = parseDocument(document);
|
|
|
+ const offset = document.offsetAt({ line, character });
|
|
|
+ const found = nameAt(tokens, offset);
|
|
|
+ if (!found) return null;
|
|
|
+ return { unit, filePath: uriToFilePath(document.uri), qualifier: found.qualifier, name: found.name };
|
|
|
+}
|
|
|
+
|
|
|
+/** Reference occurrences across the open documents plus the declaration file. */
|
|
|
+function collectReferences(
|
|
|
+ documents: TextDocument[], currentUri: string,
|
|
|
+ unit: M2Unit, filePath: string, line: number, character: number,
|
|
|
+ qualifier: string | null, name: string,
|
|
|
+ includeDeclaration: boolean,
|
|
|
+): { uri: string; occurrences: LocatedOccurrence[] }[] {
|
|
|
+ const decl = resolveName(unit, path.dirname(filePath), line, character, qualifier, name);
|
|
|
+ if (!decl) return [];
|
|
|
+ const seen = new Map<string, TextDocument>();
|
|
|
+ for (const doc of documents) {
|
|
|
+ if (!seen.has(doc.uri)) seen.set(doc.uri, doc);
|
|
|
+ }
|
|
|
+ // The declaration may live in a .def that is not open.
|
|
|
+ let extra: { uri: string; text: string } | null = null;
|
|
|
+ const declOpen = seen.has(pathToFileURL(decl.filePath).toString());
|
|
|
+ if (!declOpen) {
|
|
|
+ try {
|
|
|
+ extra = { uri: pathToFileURL(decl.filePath).toString(), text: readFileSync(decl.filePath, 'utf8') };
|
|
|
+ } catch {
|
|
|
+ extra = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ void currentUri;
|
|
|
+ const out: { uri: string; occurrences: LocatedOccurrence[] }[] = [];
|
|
|
+ for (const doc of seen.values()) {
|
|
|
+ const fp = uriToFilePath(doc.uri);
|
|
|
+ const occurrences = findReferencesInText(
|
|
|
+ doc.getText(), fp, path.dirname(fp), decl, includeDeclaration,
|
|
|
+ );
|
|
|
+ if (occurrences.length > 0) out.push({ uri: doc.uri, occurrences });
|
|
|
+ }
|
|
|
+ if (extra) {
|
|
|
+ const occurrences = findReferencesInText(
|
|
|
+ extra.text, decl.filePath, path.dirname(decl.filePath), decl, includeDeclaration,
|
|
|
+ );
|
|
|
+ if (occurrences.length > 0) out.push({ uri: extra.uri, occurrences });
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
+connection.onReferences(params => {
|
|
|
+ const document = documents.get(params.textDocument.uri);
|
|
|
+ if (!document) return [];
|
|
|
+ const req = resolveRequest(document, params.position.line, params.position.character);
|
|
|
+ if (!req) return [];
|
|
|
+ const locations: Location[] = [];
|
|
|
+ for (const { uri, occurrences } of collectReferences(
|
|
|
+ documents.all(), document.uri, req.unit, req.filePath,
|
|
|
+ params.position.line, params.position.character,
|
|
|
+ req.qualifier, req.name, params.context.includeDeclaration,
|
|
|
+ )) {
|
|
|
+ for (const occ of occurrences) {
|
|
|
+ locations.push({
|
|
|
+ uri,
|
|
|
+ range: {
|
|
|
+ start: { line: occ.line, character: occ.ch },
|
|
|
+ end: { line: occ.endLine, character: occ.endCh },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return locations;
|
|
|
+});
|
|
|
+
|
|
|
+connection.onPrepareRename(params => {
|
|
|
+ const document = documents.get(params.textDocument.uri);
|
|
|
+ if (!document) return null;
|
|
|
+ const req = resolveRequest(document, params.position.line, params.position.character);
|
|
|
+ if (!req) return null;
|
|
|
+ const decl = resolveName(
|
|
|
+ req.unit, path.dirname(req.filePath),
|
|
|
+ params.position.line, params.position.character, req.qualifier, req.name,
|
|
|
+ );
|
|
|
+ if (!decl) return null;
|
|
|
+ const offset = document.offsetAt(params.position);
|
|
|
+ const { tokens } = parseDocument(document);
|
|
|
+ const token = tokenAt(tokens, offset);
|
|
|
+ if (!token || token.kind !== 'ident') return null;
|
|
|
+ return {
|
|
|
+ start: { line: token.line, character: token.ch },
|
|
|
+ end: { line: token.endLine, character: token.endCh },
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+connection.onRenameRequest(params => {
|
|
|
+ const document = documents.get(params.textDocument.uri);
|
|
|
+ if (!document) return null;
|
|
|
+ const req = resolveRequest(document, params.position.line, params.position.character);
|
|
|
+ if (!req) return null;
|
|
|
+ const changes: { [uri: string]: { range: Location['range']; newText: string }[] } = {};
|
|
|
+ for (const { uri, occurrences } of collectReferences(
|
|
|
+ documents.all(), document.uri, req.unit, req.filePath,
|
|
|
+ params.position.line, params.position.character,
|
|
|
+ req.qualifier, req.name, true,
|
|
|
+ )) {
|
|
|
+ changes[uri] = occurrences.map(occ => ({
|
|
|
+ range: {
|
|
|
+ start: { line: occ.line, character: occ.ch },
|
|
|
+ end: { line: occ.endLine, character: occ.endCh },
|
|
|
+ },
|
|
|
+ newText: params.newName,
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ if (Object.keys(changes).length === 0) return null;
|
|
|
+ return { changes };
|
|
|
+});
|
|
|
+
|
|
|
documents.listen(connection);
|
|
|
connection.listen();
|