@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,94 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
type JsonRpcMessage = {
|
|
7
|
+
id?: number;
|
|
8
|
+
jsonrpc: "2.0";
|
|
9
|
+
result?: {
|
|
10
|
+
capabilities?: object;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function encodeMessage(message: object): string {
|
|
15
|
+
const body = JSON.stringify(message);
|
|
16
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runCliContractTest(): Promise<void> {
|
|
20
|
+
const packageJsonPath = path.resolve(process.cwd(), "package.json");
|
|
21
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
|
22
|
+
bin?: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
assert.equal(packageJson.bin?.merls, "dist/src/cli.js");
|
|
26
|
+
|
|
27
|
+
const cliPath = path.resolve(__dirname, "../src/cli.js");
|
|
28
|
+
const child = spawn(process.execPath, [cliPath, "--stdio"], {
|
|
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(`CLI 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
|
+
processId: process.pid,
|
|
81
|
+
rootUri: null
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const message = await response;
|
|
88
|
+
assert.equal(message.id, 1);
|
|
89
|
+
assert.equal(message.jsonrpc, "2.0");
|
|
90
|
+
assert.equal(typeof message.result?.capabilities, "object");
|
|
91
|
+
} finally {
|
|
92
|
+
child.kill();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
type CocSettings = {
|
|
6
|
+
languageserver?: {
|
|
7
|
+
merls?: {
|
|
8
|
+
command?: string;
|
|
9
|
+
args?: string[];
|
|
10
|
+
filetypes?: string[];
|
|
11
|
+
rootPatterns?: string[];
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function runCocConfigTest(): void {
|
|
17
|
+
const settingsPath = path.resolve(process.cwd(), "examples/coc-settings.json");
|
|
18
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")) as CocSettings;
|
|
19
|
+
const serverConfig = settings.languageserver?.merls;
|
|
20
|
+
|
|
21
|
+
assert.equal(serverConfig?.command, "node");
|
|
22
|
+
assert.deepEqual(serverConfig?.args, [
|
|
23
|
+
"C:/Users/alexe/Projects/merls/dist/src/cli.js",
|
|
24
|
+
"--stdio"
|
|
25
|
+
]);
|
|
26
|
+
assert.deepEqual(serverConfig?.filetypes, ["asm"]);
|
|
27
|
+
assert.deepEqual(serverConfig?.rootPatterns, [".git", "package.json"]);
|
|
28
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
type JsonRpcMessage = {
|
|
7
|
+
id?: number;
|
|
8
|
+
jsonrpc: "2.0";
|
|
9
|
+
result?: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function encodeMessage(message: object): string {
|
|
13
|
+
const body = JSON.stringify(message);
|
|
14
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function decodeMessages(streamBuffer: string): { messages: JsonRpcMessage[]; rest: string } {
|
|
18
|
+
const messages: JsonRpcMessage[] = [];
|
|
19
|
+
let buffer = streamBuffer;
|
|
20
|
+
|
|
21
|
+
for (;;) {
|
|
22
|
+
const separator = buffer.indexOf("\r\n\r\n");
|
|
23
|
+
if (separator === -1) {
|
|
24
|
+
return { messages, rest: buffer };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const header = buffer.slice(0, separator);
|
|
28
|
+
const match = /Content-Length: (\d+)/i.exec(header);
|
|
29
|
+
if (!match) {
|
|
30
|
+
throw new Error(`Missing Content-Length header: ${header}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const length = Number(match[1]);
|
|
34
|
+
const body = buffer.slice(separator + 4);
|
|
35
|
+
if (Buffer.byteLength(body, "utf8") < length) {
|
|
36
|
+
return { messages, rest: buffer };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
messages.push(JSON.parse(body.slice(0, length)) as JsonRpcMessage);
|
|
40
|
+
buffer = body.slice(length);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function positionOf(text: string, needle: string): { line: number; character: number } {
|
|
45
|
+
const index = text.indexOf(needle);
|
|
46
|
+
assert.notEqual(index, -1, `expected to find ${needle}`);
|
|
47
|
+
const prefix = text.slice(0, index);
|
|
48
|
+
const lines = prefix.split("\n");
|
|
49
|
+
return {
|
|
50
|
+
line: lines.length - 1,
|
|
51
|
+
character: lines.at(-1)?.length ?? 0
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function runCompletionTest(): Promise<void> {
|
|
56
|
+
const serverPath = path.resolve(__dirname, "../src/server.js");
|
|
57
|
+
const mainPath = path.resolve(
|
|
58
|
+
process.cwd(),
|
|
59
|
+
"test/fixtures/valid/merlin32-main-6502.asm"
|
|
60
|
+
);
|
|
61
|
+
const mainUri = `file://${mainPath.replace(/\\/g, "/")}`;
|
|
62
|
+
const text = `${fs.readFileSync(mainPath, "utf8")}\n ld\n du\n bpl G`;
|
|
63
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
64
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let stdout = "";
|
|
68
|
+
let nextId = 1;
|
|
69
|
+
const pending = new Map<number, (message: JsonRpcMessage) => void>();
|
|
70
|
+
|
|
71
|
+
child.stdout.setEncoding("utf8");
|
|
72
|
+
child.stdout.on("data", (chunk: string) => {
|
|
73
|
+
stdout += chunk;
|
|
74
|
+
const decoded = decodeMessages(stdout);
|
|
75
|
+
stdout = decoded.rest;
|
|
76
|
+
|
|
77
|
+
for (const message of decoded.messages) {
|
|
78
|
+
if (message.id !== undefined) {
|
|
79
|
+
pending.get(message.id)?.(message);
|
|
80
|
+
pending.delete(message.id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function sendRequest(method: string, params: object): Promise<JsonRpcMessage> {
|
|
86
|
+
const id = nextId++;
|
|
87
|
+
child.stdin.write(
|
|
88
|
+
encodeMessage({
|
|
89
|
+
id,
|
|
90
|
+
jsonrpc: "2.0",
|
|
91
|
+
method,
|
|
92
|
+
params
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
pending.set(id, resolve);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sendNotification(method: string, params: object): void {
|
|
102
|
+
child.stdin.write(
|
|
103
|
+
encodeMessage({
|
|
104
|
+
jsonrpc: "2.0",
|
|
105
|
+
method,
|
|
106
|
+
params
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await sendRequest("initialize", {
|
|
113
|
+
capabilities: {},
|
|
114
|
+
processId: process.pid,
|
|
115
|
+
rootUri: `file://${path.resolve(process.cwd()).replace(/\\/g, "/")}`
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
sendNotification("initialized", {});
|
|
119
|
+
sendNotification("textDocument/didOpen", {
|
|
120
|
+
textDocument: {
|
|
121
|
+
uri: mainUri,
|
|
122
|
+
languageId: "asm",
|
|
123
|
+
version: 1,
|
|
124
|
+
text
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const opcodeResponse = await sendRequest("textDocument/completion", {
|
|
129
|
+
textDocument: { uri: mainUri },
|
|
130
|
+
position: positionOf(text, " ld")
|
|
131
|
+
});
|
|
132
|
+
const opcodeItems = opcodeResponse.result as Array<{ label: string }>;
|
|
133
|
+
assert.equal(opcodeItems.some((item) => item.label === "lda"), true);
|
|
134
|
+
|
|
135
|
+
const directiveResponse = await sendRequest("textDocument/completion", {
|
|
136
|
+
textDocument: { uri: mainUri },
|
|
137
|
+
position: positionOf(text, " du")
|
|
138
|
+
});
|
|
139
|
+
const directiveItems = directiveResponse.result as Array<{ label: string }>;
|
|
140
|
+
assert.equal(directiveItems.some((item) => item.label === "dum"), true);
|
|
141
|
+
|
|
142
|
+
const symbolResponse = await sendRequest("textDocument/completion", {
|
|
143
|
+
textDocument: { uri: mainUri },
|
|
144
|
+
position: positionOf(text, " bpl G")
|
|
145
|
+
});
|
|
146
|
+
const symbolItems = symbolResponse.result as Array<{ label: string }>;
|
|
147
|
+
assert.equal(symbolItems.some((item) => item.label === "GetKey"), true);
|
|
148
|
+
} finally {
|
|
149
|
+
child.kill();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
type JsonRpcMessage = {
|
|
7
|
+
id?: number;
|
|
8
|
+
jsonrpc: "2.0";
|
|
9
|
+
result?: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function encodeMessage(message: object): string {
|
|
13
|
+
const body = JSON.stringify(message);
|
|
14
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function decodeMessages(streamBuffer: string): { messages: JsonRpcMessage[]; rest: string } {
|
|
18
|
+
const messages: JsonRpcMessage[] = [];
|
|
19
|
+
let buffer = streamBuffer;
|
|
20
|
+
|
|
21
|
+
for (;;) {
|
|
22
|
+
const separator = buffer.indexOf("\r\n\r\n");
|
|
23
|
+
if (separator === -1) {
|
|
24
|
+
return { messages, rest: buffer };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const header = buffer.slice(0, separator);
|
|
28
|
+
const match = /Content-Length: (\d+)/i.exec(header);
|
|
29
|
+
if (!match) {
|
|
30
|
+
throw new Error(`Missing Content-Length header: ${header}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const length = Number(match[1]);
|
|
34
|
+
const body = buffer.slice(separator + 4);
|
|
35
|
+
if (Buffer.byteLength(body, "utf8") < length) {
|
|
36
|
+
return { messages, rest: buffer };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
messages.push(JSON.parse(body.slice(0, length)) as JsonRpcMessage);
|
|
40
|
+
buffer = body.slice(length);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function positionOf(text: string, needle: string): { line: number; character: number } {
|
|
45
|
+
const index = text.indexOf(needle);
|
|
46
|
+
assert.notEqual(index, -1, `expected to find ${needle}`);
|
|
47
|
+
const prefix = text.slice(0, index);
|
|
48
|
+
const lines = prefix.split("\n");
|
|
49
|
+
return {
|
|
50
|
+
line: lines.length - 1,
|
|
51
|
+
character: lines.at(-1)?.length ?? 0
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function runDefinitionReferencesTest(): Promise<void> {
|
|
56
|
+
const serverPath = path.resolve(__dirname, "../src/server.js");
|
|
57
|
+
const mainPath = path.resolve(
|
|
58
|
+
process.cwd(),
|
|
59
|
+
"test/fixtures/valid/merlin32-main-6502.asm"
|
|
60
|
+
);
|
|
61
|
+
const mainUri = `file://${mainPath.replace(/\\/g, "/")}`;
|
|
62
|
+
const text = fs.readFileSync(mainPath, "utf8");
|
|
63
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
64
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let stdout = "";
|
|
68
|
+
let nextId = 1;
|
|
69
|
+
const pending = new Map<number, (message: JsonRpcMessage) => void>();
|
|
70
|
+
|
|
71
|
+
child.stdout.setEncoding("utf8");
|
|
72
|
+
child.stdout.on("data", (chunk: string) => {
|
|
73
|
+
stdout += chunk;
|
|
74
|
+
const decoded = decodeMessages(stdout);
|
|
75
|
+
stdout = decoded.rest;
|
|
76
|
+
|
|
77
|
+
for (const message of decoded.messages) {
|
|
78
|
+
if (message.id !== undefined) {
|
|
79
|
+
pending.get(message.id)?.(message);
|
|
80
|
+
pending.delete(message.id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function sendRequest(method: string, params: object): Promise<JsonRpcMessage> {
|
|
86
|
+
const id = nextId++;
|
|
87
|
+
child.stdin.write(
|
|
88
|
+
encodeMessage({
|
|
89
|
+
id,
|
|
90
|
+
jsonrpc: "2.0",
|
|
91
|
+
method,
|
|
92
|
+
params
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
pending.set(id, resolve);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sendNotification(method: string, params: object): void {
|
|
102
|
+
child.stdin.write(
|
|
103
|
+
encodeMessage({
|
|
104
|
+
jsonrpc: "2.0",
|
|
105
|
+
method,
|
|
106
|
+
params
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await sendRequest("initialize", {
|
|
113
|
+
capabilities: {},
|
|
114
|
+
processId: process.pid,
|
|
115
|
+
rootUri: `file://${path.resolve(process.cwd()).replace(/\\/g, "/")}`
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
sendNotification("initialized", {});
|
|
119
|
+
sendNotification("textDocument/didOpen", {
|
|
120
|
+
textDocument: {
|
|
121
|
+
uri: mainUri,
|
|
122
|
+
languageId: "asm",
|
|
123
|
+
version: 1,
|
|
124
|
+
text
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const definitionResponse = await sendRequest("textDocument/definition", {
|
|
129
|
+
textDocument: { uri: mainUri },
|
|
130
|
+
position: positionOf(text, "bpl GetKey")
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const definition = definitionResponse.result as { uri: string; range: { start: { line: number } } };
|
|
134
|
+
assert.equal(definition.uri, mainUri);
|
|
135
|
+
assert.equal(definition.range.start.line, 69);
|
|
136
|
+
|
|
137
|
+
const referencesResponse = await sendRequest("textDocument/references", {
|
|
138
|
+
textDocument: { uri: mainUri },
|
|
139
|
+
position: positionOf(text, "GetKey ldx"),
|
|
140
|
+
context: {
|
|
141
|
+
includeDeclaration: true
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const references = referencesResponse.result as Array<{ uri: string; range: { start: { line: number } } }>;
|
|
146
|
+
assert.equal(Array.isArray(references), true);
|
|
147
|
+
assert.equal(references.some((reference) => reference.uri === mainUri && reference.range.start.line === 69), true);
|
|
148
|
+
assert.equal(references.some((reference) => reference.uri === mainUri && reference.range.start.line === 70), true);
|
|
149
|
+
} finally {
|
|
150
|
+
child.kill();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
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 {
|
|
7
|
+
collectWorkspaceDiagnostics,
|
|
8
|
+
type Diagnostic
|
|
9
|
+
} from "../src/asm/diagnostics";
|
|
10
|
+
|
|
11
|
+
export function runDiagnosticsTest(): void {
|
|
12
|
+
const duplicateSource = [
|
|
13
|
+
"dup equ 1",
|
|
14
|
+
" lda missing",
|
|
15
|
+
"dup equ 2",
|
|
16
|
+
" adc (",
|
|
17
|
+
" dsk ../build/WORLD",
|
|
18
|
+
" typ BIN",
|
|
19
|
+
" typ BLAH",
|
|
20
|
+
" end BLAH"
|
|
21
|
+
].join("\n");
|
|
22
|
+
|
|
23
|
+
const bankOpsPath = path.resolve(
|
|
24
|
+
process.cwd(),
|
|
25
|
+
"test/fixtures/invalid/65816-bank-ops.asm"
|
|
26
|
+
);
|
|
27
|
+
const longPath = path.resolve(
|
|
28
|
+
process.cwd(),
|
|
29
|
+
"test/fixtures/invalid/65816-long-addressing.asm"
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const diagnostics = collectWorkspaceDiagnostics([
|
|
33
|
+
{
|
|
34
|
+
filePath: "<memory>",
|
|
35
|
+
document: parseDocument(duplicateSource)
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
filePath: bankOpsPath,
|
|
39
|
+
document: parseDocument(fs.readFileSync(bankOpsPath, "utf8"))
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
filePath: longPath,
|
|
43
|
+
document: parseDocument(fs.readFileSync(longPath, "utf8"))
|
|
44
|
+
}
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
assert.equal(
|
|
48
|
+
diagnostics.some(
|
|
49
|
+
(diagnostic: Diagnostic) =>
|
|
50
|
+
diagnostic.filePath === "<memory>" &&
|
|
51
|
+
diagnostic.code === "duplicate-symbol" &&
|
|
52
|
+
diagnostic.line === 2
|
|
53
|
+
),
|
|
54
|
+
true
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
assert.equal(
|
|
58
|
+
diagnostics.some(
|
|
59
|
+
(diagnostic: Diagnostic) =>
|
|
60
|
+
diagnostic.filePath === "<memory>" &&
|
|
61
|
+
diagnostic.code === "unresolved-reference" &&
|
|
62
|
+
diagnostic.line === 1 &&
|
|
63
|
+
diagnostic.message.includes("missing")
|
|
64
|
+
),
|
|
65
|
+
true
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
assert.equal(
|
|
69
|
+
diagnostics.some(
|
|
70
|
+
(diagnostic: Diagnostic) =>
|
|
71
|
+
diagnostic.filePath === "<memory>" &&
|
|
72
|
+
diagnostic.code === "unresolved-reference" &&
|
|
73
|
+
(diagnostic.line === 4 || diagnostic.line === 5)
|
|
74
|
+
),
|
|
75
|
+
false
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
assert.equal(
|
|
79
|
+
diagnostics.some(
|
|
80
|
+
(diagnostic: Diagnostic) =>
|
|
81
|
+
diagnostic.filePath === "<memory>" &&
|
|
82
|
+
diagnostic.code === "unresolved-reference" &&
|
|
83
|
+
diagnostic.line === 6 &&
|
|
84
|
+
diagnostic.message.includes("BLAH")
|
|
85
|
+
),
|
|
86
|
+
true
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
assert.equal(
|
|
90
|
+
diagnostics.some(
|
|
91
|
+
(diagnostic: Diagnostic) =>
|
|
92
|
+
diagnostic.filePath === "<memory>" &&
|
|
93
|
+
diagnostic.code === "malformed-line" &&
|
|
94
|
+
diagnostic.line === 3
|
|
95
|
+
),
|
|
96
|
+
true
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
assert.equal(
|
|
100
|
+
diagnostics.some(
|
|
101
|
+
(diagnostic: Diagnostic) =>
|
|
102
|
+
diagnostic.filePath === "<memory>" &&
|
|
103
|
+
diagnostic.code === "malformed-line" &&
|
|
104
|
+
diagnostic.line === 7 &&
|
|
105
|
+
diagnostic.message === "unexpected operand for end"
|
|
106
|
+
),
|
|
107
|
+
true
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
assert.equal(
|
|
111
|
+
diagnostics.some(
|
|
112
|
+
(diagnostic: Diagnostic) =>
|
|
113
|
+
diagnostic.filePath === bankOpsPath &&
|
|
114
|
+
diagnostic.code === "unsupported-65816" &&
|
|
115
|
+
diagnostic.message.includes("mvn")
|
|
116
|
+
),
|
|
117
|
+
true
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
assert.equal(
|
|
121
|
+
diagnostics.some(
|
|
122
|
+
(diagnostic: Diagnostic) =>
|
|
123
|
+
diagnostic.filePath === longPath &&
|
|
124
|
+
diagnostic.code === "unsupported-65816" &&
|
|
125
|
+
diagnostic.message.includes("^")
|
|
126
|
+
),
|
|
127
|
+
true
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
|
|
3
|
+
import { parseDocument } from "../src/asm/document";
|
|
4
|
+
|
|
5
|
+
export function runDocumentModelTest(): void {
|
|
6
|
+
const source = [
|
|
7
|
+
"TEXT = $FB39",
|
|
8
|
+
" adc (",
|
|
9
|
+
" lda #1",
|
|
10
|
+
"",
|
|
11
|
+
"; trailing note"
|
|
12
|
+
].join("\n");
|
|
13
|
+
|
|
14
|
+
const document = parseDocument(source);
|
|
15
|
+
|
|
16
|
+
assert.equal(document.lines.length, 5);
|
|
17
|
+
assert.equal(document.errors.length, 1);
|
|
18
|
+
assert.deepEqual(document.errors[0], {
|
|
19
|
+
line: 1,
|
|
20
|
+
text: " adc (",
|
|
21
|
+
message: "expected expression token"
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
assert.equal(document.lines[0]?.node.shape, "equate");
|
|
25
|
+
assert.equal(document.lines[1]?.node.shape, "malformed");
|
|
26
|
+
assert.equal(document.lines[2]?.node.shape, "instruction");
|
|
27
|
+
assert.equal(document.lines[3]?.node.shape, "empty");
|
|
28
|
+
assert.equal(document.lines[4]?.node.shape, "commentOnly");
|
|
29
|
+
}
|