@razdolbai/merls 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.serena/memories/conventions.md +6 -0
- package/.serena/memories/core.md +8 -0
- package/.serena/memories/memory_maintenance.md +33 -0
- package/.serena/memories/suggested_commands.md +4 -0
- package/.serena/memories/task_completion.md +7 -0
- package/.serena/memories/tech_stack.md +6 -0
- package/.serena/project.yml +132 -0
- package/AGENTS.md +63 -0
- package/README.md +137 -0
- package/dist/src/asm/diagnostics.js +202 -0
- package/dist/src/asm/document.js +26 -0
- package/dist/src/asm/expression.js +163 -0
- package/dist/src/asm/lexer.js +122 -0
- package/dist/src/asm/local-labels.js +140 -0
- package/dist/src/asm/metadata.js +101 -0
- package/dist/src/asm/parser.js +118 -0
- package/dist/src/asm/symbols.js +40 -0
- package/dist/src/asm/syntax.js +44 -0
- package/dist/src/asm/workspace.js +73 -0
- package/dist/src/cli.js +21 -0
- package/dist/src/index.js +4 -0
- package/dist/src/lsp/completion.js +32 -0
- package/dist/src/lsp/diagnostics.js +63 -0
- package/dist/src/lsp/document-symbols.js +80 -0
- package/dist/src/lsp/hover.js +75 -0
- package/dist/src/lsp/symbol-navigation.js +181 -0
- package/dist/src/lsp/workspace-symbols.js +17 -0
- package/dist/src/server.js +77 -0
- package/dist/test/bootstrap.test.js +11 -0
- package/dist/test/cli-contract.test.js +74 -0
- package/dist/test/coc-config.test.js +21 -0
- package/dist/test/completion.test.js +126 -0
- package/dist/test/definition-references.test.js +126 -0
- package/dist/test/diagnostics.test.js +66 -0
- package/dist/test/document-model.test.js +30 -0
- package/dist/test/document-symbol.test.js +107 -0
- package/dist/test/expression.test.js +100 -0
- package/dist/test/fixture-corpus.test.js +33 -0
- package/dist/test/hover.test.js +142 -0
- package/dist/test/lexer.test.js +53 -0
- package/dist/test/line-parser.test.js +67 -0
- package/dist/test/local-labels.test.js +43 -0
- package/dist/test/metadata.test.js +27 -0
- package/dist/test/publish-diagnostics.test.js +137 -0
- package/dist/test/run-tests.js +132 -0
- package/dist/test/server-entrypoint.test.js +14 -0
- package/dist/test/server-initialize.test.js +77 -0
- package/dist/test/symbols.test.js +37 -0
- package/dist/test/syntax-shape.test.js +18 -0
- package/dist/test/workspace-symbol.test.js +113 -0
- package/dist/test/workspace.test.js +24 -0
- package/examples/coc-settings.json +18 -0
- package/package.json +26 -0
- package/publish.ps1 +9 -0
- package/src/asm/diagnostics.ts +294 -0
- package/src/asm/document.ts +43 -0
- package/src/asm/expression.ts +242 -0
- package/src/asm/lexer.ts +197 -0
- package/src/asm/local-labels.ts +204 -0
- package/src/asm/metadata.ts +150 -0
- package/src/asm/parser.ts +197 -0
- package/src/asm/symbols.ts +55 -0
- package/src/asm/syntax.ts +76 -0
- package/src/asm/workspace.ts +105 -0
- package/src/cli.ts +24 -0
- package/src/index.ts +1 -0
- package/src/lsp/completion.ts +42 -0
- package/src/lsp/diagnostics.ts +82 -0
- package/src/lsp/document-symbols.ts +111 -0
- package/src/lsp/hover.ts +90 -0
- package/src/lsp/symbol-navigation.ts +244 -0
- package/src/lsp/workspace-symbols.ts +24 -0
- package/src/server.ts +121 -0
- package/test/bootstrap.test.ts +7 -0
- package/test/cli-contract.test.ts +94 -0
- package/test/coc-config.test.ts +28 -0
- package/test/completion.test.ts +151 -0
- package/test/definition-references.test.ts +152 -0
- package/test/diagnostics.test.ts +129 -0
- package/test/document-model.test.ts +29 -0
- package/test/document-symbol.test.ts +131 -0
- package/test/expression.test.ts +111 -0
- package/test/fixture-corpus.test.ts +33 -0
- package/test/fixtures/invalid/65816-bank-ops.asm +17 -0
- package/test/fixtures/invalid/65816-long-addressing.asm +26 -0
- package/test/fixtures/valid/merlin32-linkscript.asm +16 -0
- package/test/fixtures/valid/merlin32-main-6502.asm +103 -0
- package/test/fixtures/valid/smoke-test.asm +7 -0
- package/test/hover.test.ts +175 -0
- package/test/lexer.test.ts +87 -0
- package/test/line-parser.test.ts +69 -0
- package/test/local-labels.test.ts +47 -0
- package/test/metadata.test.ts +27 -0
- package/test/publish-diagnostics.test.ts +206 -0
- package/test/run-tests.ts +139 -0
- package/test/server-entrypoint.test.ts +11 -0
- package/test/server-initialize.test.ts +101 -0
- package/test/smoke/run-smoke.ps1 +177 -0
- package/test/smoke/vimrc +17 -0
- package/test/symbols.test.ts +41 -0
- package/test/syntax-shape.test.ts +18 -0
- package/test/workspace-symbol.test.ts +139 -0
- package/test/workspace.test.ts +29 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DiagnosticSeverity,
|
|
5
|
+
type Diagnostic as LspDiagnostic
|
|
6
|
+
} from "vscode-languageserver/node";
|
|
7
|
+
|
|
8
|
+
import { parseDocument } from "../asm/document";
|
|
9
|
+
import {
|
|
10
|
+
collectWorkspaceDiagnostics,
|
|
11
|
+
type Diagnostic as AsmDiagnostic,
|
|
12
|
+
type DocumentEntry
|
|
13
|
+
} from "../asm/diagnostics";
|
|
14
|
+
|
|
15
|
+
export function collectDiagnosticsByUri(
|
|
16
|
+
openDocuments: ReadonlyMap<string, string>
|
|
17
|
+
): Map<string, readonly LspDiagnostic[]> {
|
|
18
|
+
const sourcesByFilePath = new Map<string, string>();
|
|
19
|
+
const uriByFilePath = new Map<string, string>();
|
|
20
|
+
const entries: DocumentEntry[] = [];
|
|
21
|
+
|
|
22
|
+
for (const [uri, source] of openDocuments.entries()) {
|
|
23
|
+
const filePath = uriToFilePath(uri);
|
|
24
|
+
sourcesByFilePath.set(filePath, source);
|
|
25
|
+
uriByFilePath.set(filePath, uri);
|
|
26
|
+
entries.push({
|
|
27
|
+
filePath,
|
|
28
|
+
document: parseDocument(source)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const diagnosticsByFilePath = new Map<string, LspDiagnostic[]>();
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
diagnosticsByFilePath.set(entry.filePath, []);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const diagnostic of collectWorkspaceDiagnostics(entries)) {
|
|
38
|
+
const source = sourcesByFilePath.get(diagnostic.filePath);
|
|
39
|
+
const diagnostics = diagnosticsByFilePath.get(diagnostic.filePath);
|
|
40
|
+
if (source === undefined || diagnostics === undefined) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
diagnostics.push(toLspDiagnostic(source, diagnostic));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const diagnosticsByUri = new Map<string, readonly LspDiagnostic[]>();
|
|
48
|
+
for (const [filePath, uri] of uriByFilePath.entries()) {
|
|
49
|
+
diagnosticsByUri.set(uri, diagnosticsByFilePath.get(filePath) ?? []);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return diagnosticsByUri;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toLspDiagnostic(source: string, diagnostic: AsmDiagnostic): LspDiagnostic {
|
|
56
|
+
const lineText = source.split(/\r?\n/)[diagnostic.line] ?? "";
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
severity: DiagnosticSeverity.Error,
|
|
60
|
+
message: diagnostic.message,
|
|
61
|
+
source: "merls",
|
|
62
|
+
code: diagnostic.code,
|
|
63
|
+
range: {
|
|
64
|
+
start: {
|
|
65
|
+
line: diagnostic.line,
|
|
66
|
+
character: 0
|
|
67
|
+
},
|
|
68
|
+
end: {
|
|
69
|
+
line: diagnostic.line,
|
|
70
|
+
character: lineText.length
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function uriToFilePath(uri: string): string {
|
|
77
|
+
if (uri.startsWith("file://")) {
|
|
78
|
+
return fileURLToPath(uri);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return uri;
|
|
82
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Location,
|
|
3
|
+
type Range,
|
|
4
|
+
type SymbolInformation,
|
|
5
|
+
SymbolKind
|
|
6
|
+
} from "vscode-languageserver/node";
|
|
7
|
+
|
|
8
|
+
import { parseDocument } from "../asm/document";
|
|
9
|
+
import { type ParsedLine } from "../asm/parser";
|
|
10
|
+
|
|
11
|
+
export function buildDocumentSymbols(
|
|
12
|
+
uri: string,
|
|
13
|
+
source: string
|
|
14
|
+
): SymbolInformation[] {
|
|
15
|
+
const document = parseDocument(source);
|
|
16
|
+
const symbols: SymbolInformation[] = [];
|
|
17
|
+
|
|
18
|
+
for (const line of document.lines) {
|
|
19
|
+
const symbol = toSymbolInformation(uri, line.node, line.line);
|
|
20
|
+
if (symbol !== null) {
|
|
21
|
+
symbols.push(symbol);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return symbols;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toSymbolInformation(
|
|
29
|
+
uri: string,
|
|
30
|
+
node: ParsedLine,
|
|
31
|
+
line: number
|
|
32
|
+
): SymbolInformation | null {
|
|
33
|
+
const name = getNodeName(node);
|
|
34
|
+
const kind = getNodeKind(node);
|
|
35
|
+
|
|
36
|
+
if (name === null || kind === null) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
kind,
|
|
43
|
+
location: createLocation(uri, line, name)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getNodeName(node: ParsedLine): string | null {
|
|
48
|
+
if (node.shape === "equate") {
|
|
49
|
+
return node.label;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (node.shape === "labelOnly") {
|
|
53
|
+
return node.label;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (node.shape === "instruction" && node.label !== null) {
|
|
57
|
+
return node.label;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (node.shape === "directive" && node.label !== null) {
|
|
61
|
+
return node.label;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (node.shape === "data" && node.label !== null) {
|
|
65
|
+
return node.label;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getNodeKind(node: ParsedLine): SymbolKind | null {
|
|
72
|
+
if (node.shape === "equate") {
|
|
73
|
+
return SymbolKind.Variable;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (node.shape === "labelOnly") {
|
|
77
|
+
return SymbolKind.Method;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (node.shape === "instruction" && node.label !== null) {
|
|
81
|
+
return SymbolKind.Method;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (node.shape === "directive" && node.label !== null) {
|
|
85
|
+
return SymbolKind.Field;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (node.shape === "data" && node.label !== null) {
|
|
89
|
+
return SymbolKind.Field;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createLocation(uri: string, line: number, name: string): Location {
|
|
96
|
+
const range: Range = {
|
|
97
|
+
start: {
|
|
98
|
+
line,
|
|
99
|
+
character: 0
|
|
100
|
+
},
|
|
101
|
+
end: {
|
|
102
|
+
line,
|
|
103
|
+
character: name.length
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
uri,
|
|
109
|
+
range
|
|
110
|
+
};
|
|
111
|
+
}
|
package/src/lsp/hover.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type Hover } from "vscode-languageserver/node";
|
|
2
|
+
|
|
3
|
+
import { directiveTable, opcodeTable } from "../asm/metadata";
|
|
4
|
+
import { parseDocument } from "../asm/document";
|
|
5
|
+
import { lexSource, type Token } from "../asm/lexer";
|
|
6
|
+
import { findDefinition } from "./symbol-navigation";
|
|
7
|
+
|
|
8
|
+
export function buildHover(
|
|
9
|
+
openDocuments: ReadonlyMap<string, string>,
|
|
10
|
+
uri: string,
|
|
11
|
+
line: number,
|
|
12
|
+
character: number
|
|
13
|
+
): Hover | null {
|
|
14
|
+
const source = openDocuments.get(uri);
|
|
15
|
+
if (source === undefined) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const documentLine = parseDocument(source).lines[line];
|
|
20
|
+
const lexedLine = lexSource(source).lines[line];
|
|
21
|
+
if (documentLine === undefined || lexedLine === undefined) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const token = tokenAtCharacter(lexedLine.tokens, character);
|
|
26
|
+
if (token?.kind === "mnemonic") {
|
|
27
|
+
const definition = opcodeTable.get(token.lexeme.toLowerCase());
|
|
28
|
+
if (definition !== undefined) {
|
|
29
|
+
return {
|
|
30
|
+
contents: `Opcode ${definition.mnemonic}: ${definition.modes.join(", ")}`
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (token?.kind === "directive") {
|
|
36
|
+
const definition = directiveTable.get(token.lexeme.toLowerCase());
|
|
37
|
+
if (definition !== undefined) {
|
|
38
|
+
return {
|
|
39
|
+
contents: `Directive ${definition.name}: ${definition.summary}`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (token?.kind === "identifier" || token?.kind === "label" || token?.kind === "localLabel") {
|
|
45
|
+
const lexemeLower = token.lexeme.toLowerCase();
|
|
46
|
+
if (lexemeLower === "a" || lexemeLower === "x" || lexemeLower === "y") {
|
|
47
|
+
return {
|
|
48
|
+
contents: `Register ${token.lexeme.toUpperCase()}`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const definition = findDefinition(openDocuments, uri, line);
|
|
53
|
+
if (definition !== null) {
|
|
54
|
+
return {
|
|
55
|
+
contents: `Symbol ${token.lexeme} defined at line ${definition.range.start.line}`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const node = documentLine.node;
|
|
61
|
+
if (node.shape === "instruction") {
|
|
62
|
+
const definition = opcodeTable.get(node.mnemonic);
|
|
63
|
+
if (definition !== undefined) {
|
|
64
|
+
return {
|
|
65
|
+
contents: `Opcode ${definition.mnemonic}: ${definition.modes.join(", ")}`
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (node.shape === "directive") {
|
|
71
|
+
const definition = directiveTable.get(node.directive);
|
|
72
|
+
if (definition !== undefined) {
|
|
73
|
+
return {
|
|
74
|
+
contents: `Directive ${definition.name}: ${definition.summary}`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function tokenAtCharacter(tokens: readonly Token[], character: number): Token | null {
|
|
83
|
+
for (const token of tokens) {
|
|
84
|
+
if (character >= token.start && character < token.end) {
|
|
85
|
+
return token;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { type Location } from "vscode-languageserver/node";
|
|
2
|
+
|
|
3
|
+
import { parseDocument } from "../asm/document";
|
|
4
|
+
import { type Expression, type Operand } from "../asm/expression";
|
|
5
|
+
import { type ParsedLine } from "../asm/parser";
|
|
6
|
+
|
|
7
|
+
type SymbolDefinition = {
|
|
8
|
+
name: string;
|
|
9
|
+
location: Location;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type SymbolReference = {
|
|
13
|
+
name: string;
|
|
14
|
+
location: Location;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function findDefinition(
|
|
18
|
+
openDocuments: ReadonlyMap<string, string>,
|
|
19
|
+
uri: string,
|
|
20
|
+
line: number
|
|
21
|
+
): Location | null {
|
|
22
|
+
const targetName = getSymbolAtLine(openDocuments.get(uri), line);
|
|
23
|
+
if (targetName === null) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const [documentUri, source] of openDocuments.entries()) {
|
|
28
|
+
for (const definition of collectDefinitions(documentUri, source)) {
|
|
29
|
+
if (definition.name === targetName) {
|
|
30
|
+
return definition.location;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function findReferences(
|
|
39
|
+
openDocuments: ReadonlyMap<string, string>,
|
|
40
|
+
uri: string,
|
|
41
|
+
line: number,
|
|
42
|
+
includeDeclaration: boolean
|
|
43
|
+
): Location[] {
|
|
44
|
+
const targetName = getSymbolAtLine(openDocuments.get(uri), line);
|
|
45
|
+
if (targetName === null) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const locations: Location[] = [];
|
|
50
|
+
|
|
51
|
+
for (const [documentUri, source] of openDocuments.entries()) {
|
|
52
|
+
if (includeDeclaration) {
|
|
53
|
+
for (const definition of collectDefinitions(documentUri, source)) {
|
|
54
|
+
if (definition.name === targetName) {
|
|
55
|
+
locations.push(definition.location);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const reference of collectReferences(documentUri, source)) {
|
|
61
|
+
if (reference.name === targetName) {
|
|
62
|
+
locations.push(reference.location);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return locations;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getSymbolAtLine(source: string | undefined, line: number): string | null {
|
|
71
|
+
if (source === undefined) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const node = parseDocument(source).lines[line]?.node;
|
|
76
|
+
if (node === undefined) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (node.shape === "labelOnly") {
|
|
81
|
+
return node.label;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (node.shape === "instruction") {
|
|
85
|
+
if (node.label !== null) {
|
|
86
|
+
return node.label;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return readOperandIdentifier(node.operand);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (node.shape === "directive") {
|
|
93
|
+
if (node.label !== null) {
|
|
94
|
+
return node.label;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return readExpressionIdentifier(node.operand);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (node.shape === "equate") {
|
|
101
|
+
return node.label;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (node.shape === "data" && node.label !== null) {
|
|
105
|
+
return node.label;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function collectDefinitions(uri: string, source: string): readonly SymbolDefinition[] {
|
|
112
|
+
const document = parseDocument(source);
|
|
113
|
+
const definitions: SymbolDefinition[] = [];
|
|
114
|
+
|
|
115
|
+
for (const line of document.lines) {
|
|
116
|
+
const name = getDefinedName(line.node);
|
|
117
|
+
if (name === null) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
definitions.push({
|
|
122
|
+
name,
|
|
123
|
+
location: createLocation(uri, line.line, name)
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return definitions;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function collectReferences(uri: string, source: string): readonly SymbolReference[] {
|
|
131
|
+
const document = parseDocument(source);
|
|
132
|
+
const references: SymbolReference[] = [];
|
|
133
|
+
|
|
134
|
+
for (const line of document.lines) {
|
|
135
|
+
for (const name of getReferencedNames(line.node)) {
|
|
136
|
+
references.push({
|
|
137
|
+
name,
|
|
138
|
+
location: createLocation(uri, line.line, name)
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return references;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getDefinedName(node: ParsedLine): string | null {
|
|
147
|
+
if (node.shape === "equate") {
|
|
148
|
+
return node.label;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (node.shape === "labelOnly") {
|
|
152
|
+
return node.label;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (node.shape === "instruction" && node.label !== null) {
|
|
156
|
+
return node.label;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (node.shape === "directive" && node.label !== null) {
|
|
160
|
+
return node.label;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (node.shape === "data" && node.label !== null) {
|
|
164
|
+
return node.label;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getReferencedNames(node: ParsedLine): readonly string[] {
|
|
171
|
+
if (node.shape === "instruction" && node.operand !== null) {
|
|
172
|
+
return collectExpressionIdentifiers(node.operand.expression);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (node.shape === "directive" && node.operand !== null) {
|
|
176
|
+
return collectExpressionIdentifiers(node.operand);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (node.shape === "equate") {
|
|
180
|
+
return collectExpressionIdentifiers(node.expression);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function readOperandIdentifier(operand: Operand | null): string | null {
|
|
187
|
+
if (operand === null) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return readExpressionIdentifier(operand.expression);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readExpressionIdentifier(expression: Expression | null): string | null {
|
|
195
|
+
if (expression === null) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (expression.kind === "identifier") {
|
|
200
|
+
return expression.value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (expression.kind === "modifier") {
|
|
204
|
+
return readExpressionIdentifier(expression.expression);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (expression.kind === "binary") {
|
|
208
|
+
return readExpressionIdentifier(expression.left);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function collectExpressionIdentifiers(expression: Expression): readonly string[] {
|
|
215
|
+
switch (expression.kind) {
|
|
216
|
+
case "identifier":
|
|
217
|
+
return [expression.value];
|
|
218
|
+
case "modifier":
|
|
219
|
+
return collectExpressionIdentifiers(expression.expression);
|
|
220
|
+
case "binary":
|
|
221
|
+
return [
|
|
222
|
+
...collectExpressionIdentifiers(expression.left),
|
|
223
|
+
...collectExpressionIdentifiers(expression.right)
|
|
224
|
+
];
|
|
225
|
+
default:
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function createLocation(uri: string, line: number, name: string): Location {
|
|
231
|
+
return {
|
|
232
|
+
uri,
|
|
233
|
+
range: {
|
|
234
|
+
start: {
|
|
235
|
+
line,
|
|
236
|
+
character: 0
|
|
237
|
+
},
|
|
238
|
+
end: {
|
|
239
|
+
line,
|
|
240
|
+
character: name.length
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type SymbolInformation } from "vscode-languageserver/node";
|
|
2
|
+
|
|
3
|
+
import { buildDocumentSymbols } from "./document-symbols";
|
|
4
|
+
|
|
5
|
+
export function buildWorkspaceSymbols(
|
|
6
|
+
openDocuments: ReadonlyMap<string, string>,
|
|
7
|
+
query: string
|
|
8
|
+
): SymbolInformation[] {
|
|
9
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
10
|
+
const symbols: SymbolInformation[] = [];
|
|
11
|
+
|
|
12
|
+
for (const [uri, source] of openDocuments.entries()) {
|
|
13
|
+
for (const symbol of buildDocumentSymbols(uri, source)) {
|
|
14
|
+
if (
|
|
15
|
+
normalizedQuery.length === 0 ||
|
|
16
|
+
symbol.name.toLowerCase().includes(normalizedQuery)
|
|
17
|
+
) {
|
|
18
|
+
symbols.push(symbol);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return symbols;
|
|
24
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Connection,
|
|
3
|
+
ProposedFeatures,
|
|
4
|
+
TextDocumentSyncKind,
|
|
5
|
+
createConnection
|
|
6
|
+
} from "vscode-languageserver/node";
|
|
7
|
+
|
|
8
|
+
import { buildDocumentSymbols } from "./lsp/document-symbols";
|
|
9
|
+
import { buildCompletionItems } from "./lsp/completion";
|
|
10
|
+
import { collectDiagnosticsByUri } from "./lsp/diagnostics";
|
|
11
|
+
import { buildHover } from "./lsp/hover";
|
|
12
|
+
import { findDefinition, findReferences } from "./lsp/symbol-navigation";
|
|
13
|
+
import { buildWorkspaceSymbols } from "./lsp/workspace-symbols";
|
|
14
|
+
|
|
15
|
+
export function createServerConnection(
|
|
16
|
+
inputStream: NodeJS.ReadableStream = process.stdin,
|
|
17
|
+
outputStream: NodeJS.WritableStream = process.stdout
|
|
18
|
+
): Connection {
|
|
19
|
+
return createConnection(ProposedFeatures.all, inputStream, outputStream);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function startServer(
|
|
23
|
+
inputStream: NodeJS.ReadableStream = process.stdin,
|
|
24
|
+
outputStream: NodeJS.WritableStream = process.stdout
|
|
25
|
+
): Connection {
|
|
26
|
+
const connection = createServerConnection(inputStream, outputStream);
|
|
27
|
+
const openDocuments = new Map<string, string>();
|
|
28
|
+
|
|
29
|
+
connection.onInitialize(() => ({
|
|
30
|
+
capabilities: {
|
|
31
|
+
completionProvider: {},
|
|
32
|
+
definitionProvider: true,
|
|
33
|
+
documentSymbolProvider: true,
|
|
34
|
+
hoverProvider: true,
|
|
35
|
+
referencesProvider: true,
|
|
36
|
+
workspaceSymbolProvider: true,
|
|
37
|
+
textDocumentSync: {
|
|
38
|
+
openClose: true,
|
|
39
|
+
change: TextDocumentSyncKind.Full
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
connection.onDidOpenTextDocument((params) => {
|
|
44
|
+
openDocuments.set(params.textDocument.uri, params.textDocument.text);
|
|
45
|
+
publishDiagnostics();
|
|
46
|
+
});
|
|
47
|
+
connection.onDidChangeTextDocument((params) => {
|
|
48
|
+
const nextText = params.contentChanges.at(-1)?.text;
|
|
49
|
+
if (nextText === undefined) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
openDocuments.set(params.textDocument.uri, nextText);
|
|
54
|
+
publishDiagnostics();
|
|
55
|
+
});
|
|
56
|
+
connection.onDidCloseTextDocument((params) => {
|
|
57
|
+
openDocuments.delete(params.textDocument.uri);
|
|
58
|
+
connection.sendDiagnostics({
|
|
59
|
+
uri: params.textDocument.uri,
|
|
60
|
+
diagnostics: []
|
|
61
|
+
});
|
|
62
|
+
publishDiagnostics();
|
|
63
|
+
});
|
|
64
|
+
connection.onDocumentSymbol((params) => {
|
|
65
|
+
const source = openDocuments.get(params.textDocument.uri);
|
|
66
|
+
if (source === undefined) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return buildDocumentSymbols(params.textDocument.uri, source);
|
|
71
|
+
});
|
|
72
|
+
connection.onWorkspaceSymbol((params) =>
|
|
73
|
+
buildWorkspaceSymbols(openDocuments, params.query)
|
|
74
|
+
);
|
|
75
|
+
connection.onDefinition((params) =>
|
|
76
|
+
findDefinition(
|
|
77
|
+
openDocuments,
|
|
78
|
+
params.textDocument.uri,
|
|
79
|
+
params.position.line
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
connection.onReferences((params) =>
|
|
83
|
+
findReferences(
|
|
84
|
+
openDocuments,
|
|
85
|
+
params.textDocument.uri,
|
|
86
|
+
params.position.line,
|
|
87
|
+
params.context.includeDeclaration
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
connection.onHover((params) =>
|
|
91
|
+
buildHover(
|
|
92
|
+
openDocuments,
|
|
93
|
+
params.textDocument.uri,
|
|
94
|
+
params.position.line,
|
|
95
|
+
params.position.character
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
connection.onCompletion((params) =>
|
|
99
|
+
buildCompletionItems(
|
|
100
|
+
openDocuments,
|
|
101
|
+
params.textDocument.uri,
|
|
102
|
+
params.position.line
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
function publishDiagnostics(): void {
|
|
107
|
+
for (const [uri, diagnostics] of collectDiagnosticsByUri(openDocuments).entries()) {
|
|
108
|
+
connection.sendDiagnostics({
|
|
109
|
+
uri,
|
|
110
|
+
diagnostics: [...diagnostics]
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
connection.listen();
|
|
116
|
+
return connection;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (require.main === module) {
|
|
120
|
+
startServer();
|
|
121
|
+
}
|