@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,87 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type LexedLine,
|
|
7
|
+
type Token,
|
|
8
|
+
lexSource
|
|
9
|
+
} from "../src/asm/lexer";
|
|
10
|
+
|
|
11
|
+
function summarizeTokens(tokens: readonly Token[]): readonly (readonly [string, string])[] {
|
|
12
|
+
return tokens.map((token) => [token.kind, token.lexeme] as const);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function findLine(lines: readonly LexedLine[], source: string): LexedLine {
|
|
16
|
+
const line = lines.find((candidate) => candidate.text === source);
|
|
17
|
+
assert.ok(line, `expected to find line: ${source}`);
|
|
18
|
+
return line;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function runLexerTest(): void {
|
|
22
|
+
const mainFixturePath = path.resolve(
|
|
23
|
+
process.cwd(),
|
|
24
|
+
"test/fixtures/valid/merlin32-main-6502.asm"
|
|
25
|
+
);
|
|
26
|
+
const linkFixturePath = path.resolve(
|
|
27
|
+
process.cwd(),
|
|
28
|
+
"test/fixtures/valid/merlin32-linkscript.asm"
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const mainFixture = fs.readFileSync(mainFixturePath, "utf8");
|
|
32
|
+
const linkFixture = fs.readFileSync(linkFixturePath, "utf8");
|
|
33
|
+
|
|
34
|
+
const mainLines = lexSource(mainFixture).lines;
|
|
35
|
+
const linkLines = lexSource(linkFixture).lines;
|
|
36
|
+
|
|
37
|
+
assert.deepEqual(
|
|
38
|
+
summarizeTokens(findLine(mainLines, "; Source: apple2accumulator/merlin32").tokens),
|
|
39
|
+
[["comment", "; Source: apple2accumulator/merlin32"]]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
assert.deepEqual(
|
|
43
|
+
summarizeTokens(findLine(mainLines, "TEXT = $FB39").tokens),
|
|
44
|
+
[
|
|
45
|
+
["label", "TEXT"],
|
|
46
|
+
["expressionOperator", "="],
|
|
47
|
+
["numericLiteral", "$FB39"]
|
|
48
|
+
]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
summarizeTokens(findLine(mainLines, " DUM 0").tokens),
|
|
53
|
+
[
|
|
54
|
+
["directive", "DUM"],
|
|
55
|
+
["numericLiteral", "0"]
|
|
56
|
+
]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
assert.deepEqual(
|
|
60
|
+
summarizeTokens(findLine(mainLines, " adc ($80,x)").tokens),
|
|
61
|
+
[
|
|
62
|
+
["mnemonic", "adc"],
|
|
63
|
+
["expressionOperator", "("],
|
|
64
|
+
["numericLiteral", "$80"],
|
|
65
|
+
["expressionOperator", ","],
|
|
66
|
+
["identifier", "x"],
|
|
67
|
+
["expressionOperator", ")"]
|
|
68
|
+
]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
assert.deepEqual(
|
|
72
|
+
summarizeTokens(findLine(mainLines, "GetKey ldx $C000").tokens),
|
|
73
|
+
[
|
|
74
|
+
["label", "GetKey"],
|
|
75
|
+
["mnemonic", "ldx"],
|
|
76
|
+
["numericLiteral", "$C000"]
|
|
77
|
+
]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
assert.deepEqual(
|
|
81
|
+
summarizeTokens(findLine(linkLines, " asm \"merlin32-main-6502.asm\"").tokens),
|
|
82
|
+
[
|
|
83
|
+
["directive", "asm"],
|
|
84
|
+
["string", "\"merlin32-main-6502.asm\""]
|
|
85
|
+
]
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
|
|
3
|
+
import { parseSourceLines } from "../src/asm/parser";
|
|
4
|
+
|
|
5
|
+
export function runLineParserTest(): void {
|
|
6
|
+
const source = [
|
|
7
|
+
"TEXT = $FB39",
|
|
8
|
+
" adc (_tmp+dum1+1,x)",
|
|
9
|
+
"dum0 ds 1",
|
|
10
|
+
" hex 2C",
|
|
11
|
+
" adc ("
|
|
12
|
+
].join("\n");
|
|
13
|
+
|
|
14
|
+
const lines = parseSourceLines(source);
|
|
15
|
+
|
|
16
|
+
assert.deepEqual(lines[0], {
|
|
17
|
+
shape: "equate",
|
|
18
|
+
text: "TEXT = $FB39",
|
|
19
|
+
label: "TEXT",
|
|
20
|
+
expression: {
|
|
21
|
+
kind: "numericLiteral",
|
|
22
|
+
value: "$FB39"
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
assert.deepEqual(lines[1], {
|
|
27
|
+
shape: "instruction",
|
|
28
|
+
text: " adc (_tmp+dum1+1,x)",
|
|
29
|
+
label: null,
|
|
30
|
+
mnemonic: "adc",
|
|
31
|
+
operand: {
|
|
32
|
+
immediate: false,
|
|
33
|
+
indirect: true,
|
|
34
|
+
indexRegister: "x",
|
|
35
|
+
expression: {
|
|
36
|
+
kind: "binary",
|
|
37
|
+
operator: "+",
|
|
38
|
+
left: {
|
|
39
|
+
kind: "binary",
|
|
40
|
+
operator: "+",
|
|
41
|
+
left: { kind: "identifier", value: "_tmp" },
|
|
42
|
+
right: { kind: "identifier", value: "dum1" }
|
|
43
|
+
},
|
|
44
|
+
right: { kind: "numericLiteral", value: "1" }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
assert.deepEqual(lines[2], {
|
|
50
|
+
shape: "directive",
|
|
51
|
+
text: "dum0 ds 1",
|
|
52
|
+
label: "dum0",
|
|
53
|
+
directive: "ds",
|
|
54
|
+
operand: {
|
|
55
|
+
kind: "numericLiteral",
|
|
56
|
+
value: "1"
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
assert.deepEqual(lines[3], {
|
|
61
|
+
shape: "data",
|
|
62
|
+
text: " hex 2C",
|
|
63
|
+
label: null,
|
|
64
|
+
directive: "hex",
|
|
65
|
+
payload: "2C"
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
assert.equal(lines[4]?.shape, "malformed");
|
|
69
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { parseDocument } from "../src/asm/document";
|
|
6
|
+
import { resolveLocalLabels } from "../src/asm/local-labels";
|
|
7
|
+
|
|
8
|
+
export function runLocalLabelScopeTest(): void {
|
|
9
|
+
const fixturePath = path.resolve(
|
|
10
|
+
process.cwd(),
|
|
11
|
+
"test/fixtures/valid/merlin32-main-6502.asm"
|
|
12
|
+
);
|
|
13
|
+
const source = fs.readFileSync(fixturePath, "utf8");
|
|
14
|
+
|
|
15
|
+
const document = parseDocument(source);
|
|
16
|
+
const scope = resolveLocalLabels(document);
|
|
17
|
+
|
|
18
|
+
assert.deepEqual(scope.definitions.get("]loop@69"), {
|
|
19
|
+
name: "]loop",
|
|
20
|
+
line: 71,
|
|
21
|
+
anchor: "GetKey",
|
|
22
|
+
qualifiedName: "]loop@69"
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
assert.deepEqual(scope.references.get("]loop@73"), {
|
|
26
|
+
name: "]loop",
|
|
27
|
+
line: 73,
|
|
28
|
+
anchor: "GetKey",
|
|
29
|
+
qualifiedName: "]loop@69",
|
|
30
|
+
targetLine: 71
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
assert.deepEqual(scope.definitions.get(":err@69"), {
|
|
34
|
+
name: ":err",
|
|
35
|
+
line: 82,
|
|
36
|
+
anchor: "GetKey",
|
|
37
|
+
qualifiedName: ":err@69"
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.deepEqual(scope.references.get(":good@81"), {
|
|
41
|
+
name: ":good",
|
|
42
|
+
line: 81,
|
|
43
|
+
anchor: "GetKey",
|
|
44
|
+
qualifiedName: ":good@69",
|
|
45
|
+
targetLine: 84
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
directiveTable,
|
|
5
|
+
opcodeTable
|
|
6
|
+
} from "../src/asm/metadata";
|
|
7
|
+
|
|
8
|
+
export function runMetadataTableTest(): void {
|
|
9
|
+
assert.equal(opcodeTable.size, 56);
|
|
10
|
+
assert.deepEqual(opcodeTable.get("lda")?.modes, [
|
|
11
|
+
"immediate",
|
|
12
|
+
"zeroPage",
|
|
13
|
+
"zeroPageX",
|
|
14
|
+
"absolute",
|
|
15
|
+
"absoluteX",
|
|
16
|
+
"absoluteY",
|
|
17
|
+
"indexedIndirect",
|
|
18
|
+
"indirectIndexed"
|
|
19
|
+
]);
|
|
20
|
+
assert.equal(opcodeTable.has("mvn"), false);
|
|
21
|
+
|
|
22
|
+
assert.equal(directiveTable.get("org")?.supported, true);
|
|
23
|
+
assert.equal(directiveTable.get("dum")?.supported, true);
|
|
24
|
+
assert.equal(directiveTable.get("xc")?.supported, false);
|
|
25
|
+
assert.equal(directiveTable.get("mx")?.supported, false);
|
|
26
|
+
assert.equal(directiveTable.get("put")?.kind, "include");
|
|
27
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
type JsonRpcMessage = {
|
|
6
|
+
id?: number;
|
|
7
|
+
jsonrpc: "2.0";
|
|
8
|
+
method?: string;
|
|
9
|
+
params?: unknown;
|
|
10
|
+
result?: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type PublishedDiagnostic = {
|
|
14
|
+
message: string;
|
|
15
|
+
severity?: number;
|
|
16
|
+
range: {
|
|
17
|
+
start: {
|
|
18
|
+
line: number;
|
|
19
|
+
character: number;
|
|
20
|
+
};
|
|
21
|
+
end: {
|
|
22
|
+
line: number;
|
|
23
|
+
character: number;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type PublishDiagnosticsParams = {
|
|
29
|
+
uri: string;
|
|
30
|
+
diagnostics: PublishedDiagnostic[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function encodeMessage(message: object): string {
|
|
34
|
+
const body = JSON.stringify(message);
|
|
35
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function decodeMessages(streamBuffer: string): { messages: JsonRpcMessage[]; rest: string } {
|
|
39
|
+
const messages: JsonRpcMessage[] = [];
|
|
40
|
+
let buffer = streamBuffer;
|
|
41
|
+
|
|
42
|
+
for (;;) {
|
|
43
|
+
const separator = buffer.indexOf("\r\n\r\n");
|
|
44
|
+
if (separator === -1) {
|
|
45
|
+
return { messages, rest: buffer };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const header = buffer.slice(0, separator);
|
|
49
|
+
const match = /Content-Length: (\d+)/i.exec(header);
|
|
50
|
+
if (!match) {
|
|
51
|
+
throw new Error(`Missing Content-Length header: ${header}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const length = Number(match[1]);
|
|
55
|
+
const body = buffer.slice(separator + 4);
|
|
56
|
+
if (Buffer.byteLength(body, "utf8") < length) {
|
|
57
|
+
return { messages, rest: buffer };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
messages.push(JSON.parse(body.slice(0, length)) as JsonRpcMessage);
|
|
61
|
+
buffer = body.slice(length);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function runPublishDiagnosticsTest(): Promise<void> {
|
|
66
|
+
const serverPath = path.resolve(__dirname, "../src/server.js");
|
|
67
|
+
const documentPath = path.resolve(
|
|
68
|
+
process.cwd(),
|
|
69
|
+
"test/fixtures/invalid/publish-diagnostics.asm"
|
|
70
|
+
);
|
|
71
|
+
const documentUri = `file://${documentPath.replace(/\\/g, "/")}`;
|
|
72
|
+
const brokenText = ["dup equ 1", " lda missing", "dup equ 2", " adc ("].join(
|
|
73
|
+
"\n"
|
|
74
|
+
);
|
|
75
|
+
const fixedText = ["dup equ 1", " lda dup", " adc #1"].join("\n");
|
|
76
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
77
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let stdout = "";
|
|
81
|
+
let nextId = 1;
|
|
82
|
+
const pending = new Map<number, (message: JsonRpcMessage) => void>();
|
|
83
|
+
const diagnosticWaiters: Array<(params: PublishDiagnosticsParams) => void> = [];
|
|
84
|
+
|
|
85
|
+
child.stdout.setEncoding("utf8");
|
|
86
|
+
child.stdout.on("data", (chunk: string) => {
|
|
87
|
+
stdout += chunk;
|
|
88
|
+
const decoded = decodeMessages(stdout);
|
|
89
|
+
stdout = decoded.rest;
|
|
90
|
+
|
|
91
|
+
for (const message of decoded.messages) {
|
|
92
|
+
if (message.id !== undefined) {
|
|
93
|
+
pending.get(message.id)?.(message);
|
|
94
|
+
pending.delete(message.id);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (message.method === "textDocument/publishDiagnostics") {
|
|
99
|
+
const params = message.params as PublishDiagnosticsParams;
|
|
100
|
+
const waiter = diagnosticWaiters.shift();
|
|
101
|
+
waiter?.(params);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
function sendRequest(method: string, params: object): Promise<JsonRpcMessage> {
|
|
107
|
+
const id = nextId++;
|
|
108
|
+
child.stdin.write(
|
|
109
|
+
encodeMessage({
|
|
110
|
+
id,
|
|
111
|
+
jsonrpc: "2.0",
|
|
112
|
+
method,
|
|
113
|
+
params
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
pending.set(id, resolve);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sendNotification(method: string, params: object): void {
|
|
123
|
+
child.stdin.write(
|
|
124
|
+
encodeMessage({
|
|
125
|
+
jsonrpc: "2.0",
|
|
126
|
+
method,
|
|
127
|
+
params
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function waitForDiagnostics(): Promise<PublishDiagnosticsParams> {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
diagnosticWaiters.push(resolve);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await sendRequest("initialize", {
|
|
140
|
+
capabilities: {},
|
|
141
|
+
processId: process.pid,
|
|
142
|
+
rootUri: `file://${path.resolve(process.cwd()).replace(/\\/g, "/")}`
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
sendNotification("initialized", {});
|
|
146
|
+
|
|
147
|
+
const openedDiagnostics = waitForDiagnostics();
|
|
148
|
+
sendNotification("textDocument/didOpen", {
|
|
149
|
+
textDocument: {
|
|
150
|
+
uri: documentUri,
|
|
151
|
+
languageId: "asm",
|
|
152
|
+
version: 1,
|
|
153
|
+
text: brokenText
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const firstPublish = await openedDiagnostics;
|
|
158
|
+
assert.equal(firstPublish.uri, documentUri);
|
|
159
|
+
assert.equal(
|
|
160
|
+
firstPublish.diagnostics.some(
|
|
161
|
+
(diagnostic) =>
|
|
162
|
+
diagnostic.message.includes("Unresolved reference missing") &&
|
|
163
|
+
diagnostic.severity === 1 &&
|
|
164
|
+
diagnostic.range.start.line === 1
|
|
165
|
+
),
|
|
166
|
+
true
|
|
167
|
+
);
|
|
168
|
+
assert.equal(
|
|
169
|
+
firstPublish.diagnostics.some(
|
|
170
|
+
(diagnostic) =>
|
|
171
|
+
diagnostic.message.includes("Duplicate symbol dup") &&
|
|
172
|
+
diagnostic.severity === 1 &&
|
|
173
|
+
diagnostic.range.start.line === 2
|
|
174
|
+
),
|
|
175
|
+
true
|
|
176
|
+
);
|
|
177
|
+
assert.equal(
|
|
178
|
+
firstPublish.diagnostics.some(
|
|
179
|
+
(diagnostic) =>
|
|
180
|
+
diagnostic.message.includes("expected expression token") &&
|
|
181
|
+
diagnostic.severity === 1 &&
|
|
182
|
+
diagnostic.range.start.line === 3
|
|
183
|
+
),
|
|
184
|
+
true
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const changedDiagnostics = waitForDiagnostics();
|
|
188
|
+
sendNotification("textDocument/didChange", {
|
|
189
|
+
textDocument: {
|
|
190
|
+
uri: documentUri,
|
|
191
|
+
version: 2
|
|
192
|
+
},
|
|
193
|
+
contentChanges: [
|
|
194
|
+
{
|
|
195
|
+
text: fixedText
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const secondPublish = await changedDiagnostics;
|
|
201
|
+
assert.equal(secondPublish.uri, documentUri);
|
|
202
|
+
assert.deepEqual(secondPublish.diagnostics, []);
|
|
203
|
+
} finally {
|
|
204
|
+
child.kill();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { runBootstrapTest } from "./bootstrap.test";
|
|
2
|
+
import { runCliContractTest } from "./cli-contract.test";
|
|
3
|
+
import { runCocConfigTest } from "./coc-config.test";
|
|
4
|
+
import { runCompletionTest } from "./completion.test";
|
|
5
|
+
import { runDefinitionReferencesTest } from "./definition-references.test";
|
|
6
|
+
import { runDiagnosticsTest } from "./diagnostics.test";
|
|
7
|
+
import { runDocumentModelTest } from "./document-model.test";
|
|
8
|
+
import { runDocumentSymbolTest } from "./document-symbol.test";
|
|
9
|
+
import { runExpressionTest } from "./expression.test";
|
|
10
|
+
import { runFixtureCorpusTest } from "./fixture-corpus.test";
|
|
11
|
+
import { runHoverTest } from "./hover.test";
|
|
12
|
+
import { runLexerTest } from "./lexer.test";
|
|
13
|
+
import { runLineParserTest } from "./line-parser.test";
|
|
14
|
+
import { runLocalLabelScopeTest } from "./local-labels.test";
|
|
15
|
+
import { runMetadataTableTest } from "./metadata.test";
|
|
16
|
+
import { runPublishDiagnosticsTest } from "./publish-diagnostics.test";
|
|
17
|
+
import { runInitializeHandshakeTest } from "./server-initialize.test";
|
|
18
|
+
import { runServerEntrypointTest } from "./server-entrypoint.test";
|
|
19
|
+
import { runSymbolsTest } from "./symbols.test";
|
|
20
|
+
import { runSyntaxShapeTest } from "./syntax-shape.test";
|
|
21
|
+
import { runWorkspaceGraphTest } from "./workspace.test";
|
|
22
|
+
import { runWorkspaceSymbolTest } from "./workspace-symbol.test";
|
|
23
|
+
|
|
24
|
+
type TestCase = {
|
|
25
|
+
name: string;
|
|
26
|
+
run: () => void | Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const tests: TestCase[] = [
|
|
30
|
+
{
|
|
31
|
+
name: "workspace bootstrap exposes the project name",
|
|
32
|
+
run: runBootstrapTest
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "cli contract exposes a packaged stdio entrypoint",
|
|
36
|
+
run: runCliContractTest
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "coc.nvim example targets the packaged stdio CLI contract",
|
|
40
|
+
run: runCocConfigTest
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "server entrypoint exposes callable startup helpers",
|
|
44
|
+
run: runServerEntrypointTest
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "server process answers initialize",
|
|
48
|
+
run: runInitializeHandshakeTest
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "positive fixture corpus includes upstream Merlin32 samples",
|
|
52
|
+
run: runFixtureCorpusTest
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "shared opcode and directive metadata covers 6502 and Merlin syntax",
|
|
56
|
+
run: runMetadataTableTest
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "token kinds and line shapes cover Merlin syntax categories",
|
|
60
|
+
run: runSyntaxShapeTest
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "lexer tokenizes fixture comments labels mnemonics directives and literals",
|
|
64
|
+
run: runLexerTest
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "expression parser handles numeric forms modifiers arithmetic and indexed operands",
|
|
68
|
+
run: runExpressionTest
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "line parser recognizes equates instructions directives data and malformed lines",
|
|
72
|
+
run: runLineParserTest
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "document model preserves line structure and tolerates malformed lines",
|
|
76
|
+
run: runDocumentModelTest
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "symbol collection indexes labels equates and named data definitions",
|
|
80
|
+
run: runSymbolsTest
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "local label scope resolves Merlin local definitions and references",
|
|
84
|
+
run: runLocalLabelScopeTest
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "workspace indexing follows Merlin include directives and merges symbols",
|
|
88
|
+
run: runWorkspaceGraphTest
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "diagnostics report duplicates unresolved refs malformed lines and unsupported 65816 syntax",
|
|
92
|
+
run: runDiagnosticsTest
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "server returns document symbols for Merlin labels equates and data definitions",
|
|
96
|
+
run: runDocumentSymbolTest
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "server returns workspace symbols across open Merlin documents",
|
|
100
|
+
run: runWorkspaceSymbolTest
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "server resolves definitions and references for Merlin symbols",
|
|
104
|
+
run: runDefinitionReferencesTest
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "server returns hover information for opcodes directives and symbols",
|
|
108
|
+
run: runHoverTest
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "server returns opcode directive and symbol completions",
|
|
112
|
+
run: runCompletionTest
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "server publishes and clears diagnostics for open Merlin documents",
|
|
116
|
+
run: runPublishDiagnosticsTest
|
|
117
|
+
}
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
async function main(): Promise<void> {
|
|
121
|
+
let failures = 0;
|
|
122
|
+
|
|
123
|
+
for (const test of tests) {
|
|
124
|
+
try {
|
|
125
|
+
await test.run();
|
|
126
|
+
console.log(`PASS ${test.name}`);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
failures += 1;
|
|
129
|
+
console.error(`FAIL ${test.name}`);
|
|
130
|
+
console.error(error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (failures > 0) {
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
void main();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
|
|
3
|
+
import { createServerConnection, startServer } from "../src/server";
|
|
4
|
+
|
|
5
|
+
export function runServerEntrypointTest(): void {
|
|
6
|
+
assert.equal(typeof startServer, "function");
|
|
7
|
+
|
|
8
|
+
const connection = createServerConnection();
|
|
9
|
+
assert.equal(typeof connection.listen, "function");
|
|
10
|
+
connection.dispose();
|
|
11
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
type JsonRpcMessage = {
|
|
6
|
+
id?: number;
|
|
7
|
+
jsonrpc: "2.0";
|
|
8
|
+
method?: string;
|
|
9
|
+
result?: {
|
|
10
|
+
capabilities?: {
|
|
11
|
+
definitionProvider?: boolean;
|
|
12
|
+
referencesProvider?: boolean;
|
|
13
|
+
textDocumentSync?: {
|
|
14
|
+
change?: number;
|
|
15
|
+
openClose?: boolean;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function encodeMessage(message: object): string {
|
|
22
|
+
const body = JSON.stringify(message);
|
|
23
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runInitializeHandshakeTest(): Promise<void> {
|
|
27
|
+
const serverPath = path.resolve(__dirname, "../src/server.js");
|
|
28
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
29
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const response = new Promise<JsonRpcMessage>((resolve, reject) => {
|
|
33
|
+
let stdout = "";
|
|
34
|
+
let stderr = "";
|
|
35
|
+
|
|
36
|
+
child.stdout.setEncoding("utf8");
|
|
37
|
+
child.stderr.setEncoding("utf8");
|
|
38
|
+
|
|
39
|
+
child.stdout.on("data", (chunk: string) => {
|
|
40
|
+
stdout += chunk;
|
|
41
|
+
|
|
42
|
+
const separator = stdout.indexOf("\r\n\r\n");
|
|
43
|
+
if (separator === -1) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const header = stdout.slice(0, separator);
|
|
48
|
+
const match = /Content-Length: (\d+)/i.exec(header);
|
|
49
|
+
if (!match) {
|
|
50
|
+
reject(new Error(`Missing Content-Length header in response: ${stdout}`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const length = Number(match[1]);
|
|
55
|
+
const body = stdout.slice(separator + 4);
|
|
56
|
+
if (Buffer.byteLength(body, "utf8") < length) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
resolve(JSON.parse(body.slice(0, length)) as JsonRpcMessage);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
child.stderr.on("data", (chunk: string) => {
|
|
64
|
+
stderr += chunk;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
child.once("error", reject);
|
|
68
|
+
child.once("exit", (code) => {
|
|
69
|
+
reject(new Error(`Server exited before initialize response. code=${code}, stderr=${stderr}`));
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
child.stdin.write(
|
|
74
|
+
encodeMessage({
|
|
75
|
+
id: 1,
|
|
76
|
+
jsonrpc: "2.0",
|
|
77
|
+
method: "initialize",
|
|
78
|
+
params: {
|
|
79
|
+
capabilities: {},
|
|
80
|
+
clientInfo: {
|
|
81
|
+
name: "merls-test"
|
|
82
|
+
},
|
|
83
|
+
processId: process.pid,
|
|
84
|
+
rootUri: null
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const message = await response;
|
|
91
|
+
assert.equal(message.id, 1);
|
|
92
|
+
assert.equal(message.jsonrpc, "2.0");
|
|
93
|
+
assert.equal(typeof message.result?.capabilities, "object");
|
|
94
|
+
assert.equal(message.result?.capabilities?.textDocumentSync?.openClose, true);
|
|
95
|
+
assert.equal(message.result?.capabilities?.textDocumentSync?.change, 1);
|
|
96
|
+
assert.equal(message.result?.capabilities?.definitionProvider, true);
|
|
97
|
+
assert.equal(message.result?.capabilities?.referencesProvider, true);
|
|
98
|
+
} finally {
|
|
99
|
+
child.kill();
|
|
100
|
+
}
|
|
101
|
+
}
|