@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,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runCliContractTest = runCliContractTest;
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
function encodeMessage(message) {
|
|
12
|
+
const body = JSON.stringify(message);
|
|
13
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
14
|
+
}
|
|
15
|
+
async function runCliContractTest() {
|
|
16
|
+
const packageJsonPath = node_path_1.default.resolve(process.cwd(), "package.json");
|
|
17
|
+
const packageJson = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
|
|
18
|
+
strict_1.default.equal(packageJson.bin?.merls, "dist/src/cli.js");
|
|
19
|
+
const cliPath = node_path_1.default.resolve(__dirname, "../src/cli.js");
|
|
20
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [cliPath, "--stdio"], {
|
|
21
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
22
|
+
});
|
|
23
|
+
const response = new Promise((resolve, reject) => {
|
|
24
|
+
let stdout = "";
|
|
25
|
+
let stderr = "";
|
|
26
|
+
child.stdout.setEncoding("utf8");
|
|
27
|
+
child.stderr.setEncoding("utf8");
|
|
28
|
+
child.stdout.on("data", (chunk) => {
|
|
29
|
+
stdout += chunk;
|
|
30
|
+
const separator = stdout.indexOf("\r\n\r\n");
|
|
31
|
+
if (separator === -1) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const header = stdout.slice(0, separator);
|
|
35
|
+
const match = /Content-Length: (\d+)/i.exec(header);
|
|
36
|
+
if (!match) {
|
|
37
|
+
reject(new Error(`Missing Content-Length header in response: ${stdout}`));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const length = Number(match[1]);
|
|
41
|
+
const body = stdout.slice(separator + 4);
|
|
42
|
+
if (Buffer.byteLength(body, "utf8") < length) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
resolve(JSON.parse(body.slice(0, length)));
|
|
46
|
+
});
|
|
47
|
+
child.stderr.on("data", (chunk) => {
|
|
48
|
+
stderr += chunk;
|
|
49
|
+
});
|
|
50
|
+
child.once("error", reject);
|
|
51
|
+
child.once("exit", (code) => {
|
|
52
|
+
reject(new Error(`CLI exited before initialize response. code=${code}, stderr=${stderr}`));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
child.stdin.write(encodeMessage({
|
|
56
|
+
id: 1,
|
|
57
|
+
jsonrpc: "2.0",
|
|
58
|
+
method: "initialize",
|
|
59
|
+
params: {
|
|
60
|
+
capabilities: {},
|
|
61
|
+
processId: process.pid,
|
|
62
|
+
rootUri: null
|
|
63
|
+
}
|
|
64
|
+
}));
|
|
65
|
+
try {
|
|
66
|
+
const message = await response;
|
|
67
|
+
strict_1.default.equal(message.id, 1);
|
|
68
|
+
strict_1.default.equal(message.jsonrpc, "2.0");
|
|
69
|
+
strict_1.default.equal(typeof message.result?.capabilities, "object");
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
child.kill();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runCocConfigTest = runCocConfigTest;
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
function runCocConfigTest() {
|
|
11
|
+
const settingsPath = node_path_1.default.resolve(process.cwd(), "examples/coc-settings.json");
|
|
12
|
+
const settings = JSON.parse(node_fs_1.default.readFileSync(settingsPath, "utf8"));
|
|
13
|
+
const serverConfig = settings.languageserver?.merls;
|
|
14
|
+
strict_1.default.equal(serverConfig?.command, "node");
|
|
15
|
+
strict_1.default.deepEqual(serverConfig?.args, [
|
|
16
|
+
"C:/Users/alexe/Projects/merls/dist/src/cli.js",
|
|
17
|
+
"--stdio"
|
|
18
|
+
]);
|
|
19
|
+
strict_1.default.deepEqual(serverConfig?.filetypes, ["asm"]);
|
|
20
|
+
strict_1.default.deepEqual(serverConfig?.rootPatterns, [".git", "package.json"]);
|
|
21
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runCompletionTest = runCompletionTest;
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
function encodeMessage(message) {
|
|
12
|
+
const body = JSON.stringify(message);
|
|
13
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
14
|
+
}
|
|
15
|
+
function decodeMessages(streamBuffer) {
|
|
16
|
+
const messages = [];
|
|
17
|
+
let buffer = streamBuffer;
|
|
18
|
+
for (;;) {
|
|
19
|
+
const separator = buffer.indexOf("\r\n\r\n");
|
|
20
|
+
if (separator === -1) {
|
|
21
|
+
return { messages, rest: buffer };
|
|
22
|
+
}
|
|
23
|
+
const header = buffer.slice(0, separator);
|
|
24
|
+
const match = /Content-Length: (\d+)/i.exec(header);
|
|
25
|
+
if (!match) {
|
|
26
|
+
throw new Error(`Missing Content-Length header: ${header}`);
|
|
27
|
+
}
|
|
28
|
+
const length = Number(match[1]);
|
|
29
|
+
const body = buffer.slice(separator + 4);
|
|
30
|
+
if (Buffer.byteLength(body, "utf8") < length) {
|
|
31
|
+
return { messages, rest: buffer };
|
|
32
|
+
}
|
|
33
|
+
messages.push(JSON.parse(body.slice(0, length)));
|
|
34
|
+
buffer = body.slice(length);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function positionOf(text, needle) {
|
|
38
|
+
const index = text.indexOf(needle);
|
|
39
|
+
strict_1.default.notEqual(index, -1, `expected to find ${needle}`);
|
|
40
|
+
const prefix = text.slice(0, index);
|
|
41
|
+
const lines = prefix.split("\n");
|
|
42
|
+
return {
|
|
43
|
+
line: lines.length - 1,
|
|
44
|
+
character: lines.at(-1)?.length ?? 0
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function runCompletionTest() {
|
|
48
|
+
const serverPath = node_path_1.default.resolve(__dirname, "../src/server.js");
|
|
49
|
+
const mainPath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-main-6502.asm");
|
|
50
|
+
const mainUri = `file://${mainPath.replace(/\\/g, "/")}`;
|
|
51
|
+
const text = `${node_fs_1.default.readFileSync(mainPath, "utf8")}\n ld\n du\n bpl G`;
|
|
52
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [serverPath], {
|
|
53
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
54
|
+
});
|
|
55
|
+
let stdout = "";
|
|
56
|
+
let nextId = 1;
|
|
57
|
+
const pending = new Map();
|
|
58
|
+
child.stdout.setEncoding("utf8");
|
|
59
|
+
child.stdout.on("data", (chunk) => {
|
|
60
|
+
stdout += chunk;
|
|
61
|
+
const decoded = decodeMessages(stdout);
|
|
62
|
+
stdout = decoded.rest;
|
|
63
|
+
for (const message of decoded.messages) {
|
|
64
|
+
if (message.id !== undefined) {
|
|
65
|
+
pending.get(message.id)?.(message);
|
|
66
|
+
pending.delete(message.id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
function sendRequest(method, params) {
|
|
71
|
+
const id = nextId++;
|
|
72
|
+
child.stdin.write(encodeMessage({
|
|
73
|
+
id,
|
|
74
|
+
jsonrpc: "2.0",
|
|
75
|
+
method,
|
|
76
|
+
params
|
|
77
|
+
}));
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
pending.set(id, resolve);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function sendNotification(method, params) {
|
|
83
|
+
child.stdin.write(encodeMessage({
|
|
84
|
+
jsonrpc: "2.0",
|
|
85
|
+
method,
|
|
86
|
+
params
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await sendRequest("initialize", {
|
|
91
|
+
capabilities: {},
|
|
92
|
+
processId: process.pid,
|
|
93
|
+
rootUri: `file://${node_path_1.default.resolve(process.cwd()).replace(/\\/g, "/")}`
|
|
94
|
+
});
|
|
95
|
+
sendNotification("initialized", {});
|
|
96
|
+
sendNotification("textDocument/didOpen", {
|
|
97
|
+
textDocument: {
|
|
98
|
+
uri: mainUri,
|
|
99
|
+
languageId: "asm",
|
|
100
|
+
version: 1,
|
|
101
|
+
text
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const opcodeResponse = await sendRequest("textDocument/completion", {
|
|
105
|
+
textDocument: { uri: mainUri },
|
|
106
|
+
position: positionOf(text, " ld")
|
|
107
|
+
});
|
|
108
|
+
const opcodeItems = opcodeResponse.result;
|
|
109
|
+
strict_1.default.equal(opcodeItems.some((item) => item.label === "lda"), true);
|
|
110
|
+
const directiveResponse = await sendRequest("textDocument/completion", {
|
|
111
|
+
textDocument: { uri: mainUri },
|
|
112
|
+
position: positionOf(text, " du")
|
|
113
|
+
});
|
|
114
|
+
const directiveItems = directiveResponse.result;
|
|
115
|
+
strict_1.default.equal(directiveItems.some((item) => item.label === "dum"), true);
|
|
116
|
+
const symbolResponse = await sendRequest("textDocument/completion", {
|
|
117
|
+
textDocument: { uri: mainUri },
|
|
118
|
+
position: positionOf(text, " bpl G")
|
|
119
|
+
});
|
|
120
|
+
const symbolItems = symbolResponse.result;
|
|
121
|
+
strict_1.default.equal(symbolItems.some((item) => item.label === "GetKey"), true);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
child.kill();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runDefinitionReferencesTest = runDefinitionReferencesTest;
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
function encodeMessage(message) {
|
|
12
|
+
const body = JSON.stringify(message);
|
|
13
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
14
|
+
}
|
|
15
|
+
function decodeMessages(streamBuffer) {
|
|
16
|
+
const messages = [];
|
|
17
|
+
let buffer = streamBuffer;
|
|
18
|
+
for (;;) {
|
|
19
|
+
const separator = buffer.indexOf("\r\n\r\n");
|
|
20
|
+
if (separator === -1) {
|
|
21
|
+
return { messages, rest: buffer };
|
|
22
|
+
}
|
|
23
|
+
const header = buffer.slice(0, separator);
|
|
24
|
+
const match = /Content-Length: (\d+)/i.exec(header);
|
|
25
|
+
if (!match) {
|
|
26
|
+
throw new Error(`Missing Content-Length header: ${header}`);
|
|
27
|
+
}
|
|
28
|
+
const length = Number(match[1]);
|
|
29
|
+
const body = buffer.slice(separator + 4);
|
|
30
|
+
if (Buffer.byteLength(body, "utf8") < length) {
|
|
31
|
+
return { messages, rest: buffer };
|
|
32
|
+
}
|
|
33
|
+
messages.push(JSON.parse(body.slice(0, length)));
|
|
34
|
+
buffer = body.slice(length);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function positionOf(text, needle) {
|
|
38
|
+
const index = text.indexOf(needle);
|
|
39
|
+
strict_1.default.notEqual(index, -1, `expected to find ${needle}`);
|
|
40
|
+
const prefix = text.slice(0, index);
|
|
41
|
+
const lines = prefix.split("\n");
|
|
42
|
+
return {
|
|
43
|
+
line: lines.length - 1,
|
|
44
|
+
character: lines.at(-1)?.length ?? 0
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function runDefinitionReferencesTest() {
|
|
48
|
+
const serverPath = node_path_1.default.resolve(__dirname, "../src/server.js");
|
|
49
|
+
const mainPath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-main-6502.asm");
|
|
50
|
+
const mainUri = `file://${mainPath.replace(/\\/g, "/")}`;
|
|
51
|
+
const text = node_fs_1.default.readFileSync(mainPath, "utf8");
|
|
52
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [serverPath], {
|
|
53
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
54
|
+
});
|
|
55
|
+
let stdout = "";
|
|
56
|
+
let nextId = 1;
|
|
57
|
+
const pending = new Map();
|
|
58
|
+
child.stdout.setEncoding("utf8");
|
|
59
|
+
child.stdout.on("data", (chunk) => {
|
|
60
|
+
stdout += chunk;
|
|
61
|
+
const decoded = decodeMessages(stdout);
|
|
62
|
+
stdout = decoded.rest;
|
|
63
|
+
for (const message of decoded.messages) {
|
|
64
|
+
if (message.id !== undefined) {
|
|
65
|
+
pending.get(message.id)?.(message);
|
|
66
|
+
pending.delete(message.id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
function sendRequest(method, params) {
|
|
71
|
+
const id = nextId++;
|
|
72
|
+
child.stdin.write(encodeMessage({
|
|
73
|
+
id,
|
|
74
|
+
jsonrpc: "2.0",
|
|
75
|
+
method,
|
|
76
|
+
params
|
|
77
|
+
}));
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
pending.set(id, resolve);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function sendNotification(method, params) {
|
|
83
|
+
child.stdin.write(encodeMessage({
|
|
84
|
+
jsonrpc: "2.0",
|
|
85
|
+
method,
|
|
86
|
+
params
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await sendRequest("initialize", {
|
|
91
|
+
capabilities: {},
|
|
92
|
+
processId: process.pid,
|
|
93
|
+
rootUri: `file://${node_path_1.default.resolve(process.cwd()).replace(/\\/g, "/")}`
|
|
94
|
+
});
|
|
95
|
+
sendNotification("initialized", {});
|
|
96
|
+
sendNotification("textDocument/didOpen", {
|
|
97
|
+
textDocument: {
|
|
98
|
+
uri: mainUri,
|
|
99
|
+
languageId: "asm",
|
|
100
|
+
version: 1,
|
|
101
|
+
text
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const definitionResponse = await sendRequest("textDocument/definition", {
|
|
105
|
+
textDocument: { uri: mainUri },
|
|
106
|
+
position: positionOf(text, "bpl GetKey")
|
|
107
|
+
});
|
|
108
|
+
const definition = definitionResponse.result;
|
|
109
|
+
strict_1.default.equal(definition.uri, mainUri);
|
|
110
|
+
strict_1.default.equal(definition.range.start.line, 69);
|
|
111
|
+
const referencesResponse = await sendRequest("textDocument/references", {
|
|
112
|
+
textDocument: { uri: mainUri },
|
|
113
|
+
position: positionOf(text, "GetKey ldx"),
|
|
114
|
+
context: {
|
|
115
|
+
includeDeclaration: true
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
const references = referencesResponse.result;
|
|
119
|
+
strict_1.default.equal(Array.isArray(references), true);
|
|
120
|
+
strict_1.default.equal(references.some((reference) => reference.uri === mainUri && reference.range.start.line === 69), true);
|
|
121
|
+
strict_1.default.equal(references.some((reference) => reference.uri === mainUri && reference.range.start.line === 70), true);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
child.kill();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runDiagnosticsTest = runDiagnosticsTest;
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const document_1 = require("../src/asm/document");
|
|
11
|
+
const diagnostics_1 = require("../src/asm/diagnostics");
|
|
12
|
+
function runDiagnosticsTest() {
|
|
13
|
+
const duplicateSource = [
|
|
14
|
+
"dup equ 1",
|
|
15
|
+
" lda missing",
|
|
16
|
+
"dup equ 2",
|
|
17
|
+
" adc (",
|
|
18
|
+
" dsk ../build/WORLD",
|
|
19
|
+
" typ BIN",
|
|
20
|
+
" typ BLAH",
|
|
21
|
+
" end BLAH"
|
|
22
|
+
].join("\n");
|
|
23
|
+
const bankOpsPath = node_path_1.default.resolve(process.cwd(), "test/fixtures/invalid/65816-bank-ops.asm");
|
|
24
|
+
const longPath = node_path_1.default.resolve(process.cwd(), "test/fixtures/invalid/65816-long-addressing.asm");
|
|
25
|
+
const diagnostics = (0, diagnostics_1.collectWorkspaceDiagnostics)([
|
|
26
|
+
{
|
|
27
|
+
filePath: "<memory>",
|
|
28
|
+
document: (0, document_1.parseDocument)(duplicateSource)
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
filePath: bankOpsPath,
|
|
32
|
+
document: (0, document_1.parseDocument)(node_fs_1.default.readFileSync(bankOpsPath, "utf8"))
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
filePath: longPath,
|
|
36
|
+
document: (0, document_1.parseDocument)(node_fs_1.default.readFileSync(longPath, "utf8"))
|
|
37
|
+
}
|
|
38
|
+
]);
|
|
39
|
+
strict_1.default.equal(diagnostics.some((diagnostic) => diagnostic.filePath === "<memory>" &&
|
|
40
|
+
diagnostic.code === "duplicate-symbol" &&
|
|
41
|
+
diagnostic.line === 2), true);
|
|
42
|
+
strict_1.default.equal(diagnostics.some((diagnostic) => diagnostic.filePath === "<memory>" &&
|
|
43
|
+
diagnostic.code === "unresolved-reference" &&
|
|
44
|
+
diagnostic.line === 1 &&
|
|
45
|
+
diagnostic.message.includes("missing")), true);
|
|
46
|
+
strict_1.default.equal(diagnostics.some((diagnostic) => diagnostic.filePath === "<memory>" &&
|
|
47
|
+
diagnostic.code === "unresolved-reference" &&
|
|
48
|
+
(diagnostic.line === 4 || diagnostic.line === 5)), false);
|
|
49
|
+
strict_1.default.equal(diagnostics.some((diagnostic) => diagnostic.filePath === "<memory>" &&
|
|
50
|
+
diagnostic.code === "unresolved-reference" &&
|
|
51
|
+
diagnostic.line === 6 &&
|
|
52
|
+
diagnostic.message.includes("BLAH")), true);
|
|
53
|
+
strict_1.default.equal(diagnostics.some((diagnostic) => diagnostic.filePath === "<memory>" &&
|
|
54
|
+
diagnostic.code === "malformed-line" &&
|
|
55
|
+
diagnostic.line === 3), true);
|
|
56
|
+
strict_1.default.equal(diagnostics.some((diagnostic) => diagnostic.filePath === "<memory>" &&
|
|
57
|
+
diagnostic.code === "malformed-line" &&
|
|
58
|
+
diagnostic.line === 7 &&
|
|
59
|
+
diagnostic.message === "unexpected operand for end"), true);
|
|
60
|
+
strict_1.default.equal(diagnostics.some((diagnostic) => diagnostic.filePath === bankOpsPath &&
|
|
61
|
+
diagnostic.code === "unsupported-65816" &&
|
|
62
|
+
diagnostic.message.includes("mvn")), true);
|
|
63
|
+
strict_1.default.equal(diagnostics.some((diagnostic) => diagnostic.filePath === longPath &&
|
|
64
|
+
diagnostic.code === "unsupported-65816" &&
|
|
65
|
+
diagnostic.message.includes("^")), true);
|
|
66
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runDocumentModelTest = runDocumentModelTest;
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const document_1 = require("../src/asm/document");
|
|
9
|
+
function runDocumentModelTest() {
|
|
10
|
+
const source = [
|
|
11
|
+
"TEXT = $FB39",
|
|
12
|
+
" adc (",
|
|
13
|
+
" lda #1",
|
|
14
|
+
"",
|
|
15
|
+
"; trailing note"
|
|
16
|
+
].join("\n");
|
|
17
|
+
const document = (0, document_1.parseDocument)(source);
|
|
18
|
+
strict_1.default.equal(document.lines.length, 5);
|
|
19
|
+
strict_1.default.equal(document.errors.length, 1);
|
|
20
|
+
strict_1.default.deepEqual(document.errors[0], {
|
|
21
|
+
line: 1,
|
|
22
|
+
text: " adc (",
|
|
23
|
+
message: "expected expression token"
|
|
24
|
+
});
|
|
25
|
+
strict_1.default.equal(document.lines[0]?.node.shape, "equate");
|
|
26
|
+
strict_1.default.equal(document.lines[1]?.node.shape, "malformed");
|
|
27
|
+
strict_1.default.equal(document.lines[2]?.node.shape, "instruction");
|
|
28
|
+
strict_1.default.equal(document.lines[3]?.node.shape, "empty");
|
|
29
|
+
strict_1.default.equal(document.lines[4]?.node.shape, "commentOnly");
|
|
30
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runDocumentSymbolTest = runDocumentSymbolTest;
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
function encodeMessage(message) {
|
|
12
|
+
const body = JSON.stringify(message);
|
|
13
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
14
|
+
}
|
|
15
|
+
function decodeMessages(streamBuffer) {
|
|
16
|
+
const messages = [];
|
|
17
|
+
let buffer = streamBuffer;
|
|
18
|
+
for (;;) {
|
|
19
|
+
const separator = buffer.indexOf("\r\n\r\n");
|
|
20
|
+
if (separator === -1) {
|
|
21
|
+
return { messages, rest: buffer };
|
|
22
|
+
}
|
|
23
|
+
const header = buffer.slice(0, separator);
|
|
24
|
+
const match = /Content-Length: (\d+)/i.exec(header);
|
|
25
|
+
if (!match) {
|
|
26
|
+
throw new Error(`Missing Content-Length header: ${header}`);
|
|
27
|
+
}
|
|
28
|
+
const length = Number(match[1]);
|
|
29
|
+
const body = buffer.slice(separator + 4);
|
|
30
|
+
if (Buffer.byteLength(body, "utf8") < length) {
|
|
31
|
+
return { messages, rest: buffer };
|
|
32
|
+
}
|
|
33
|
+
messages.push(JSON.parse(body.slice(0, length)));
|
|
34
|
+
buffer = body.slice(length);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function runDocumentSymbolTest() {
|
|
38
|
+
const serverPath = node_path_1.default.resolve(__dirname, "../src/server.js");
|
|
39
|
+
const fixturePath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-main-6502.asm");
|
|
40
|
+
const uri = `file://${fixturePath.replace(/\\/g, "/")}`;
|
|
41
|
+
const text = node_fs_1.default.readFileSync(fixturePath, "utf8");
|
|
42
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [serverPath], {
|
|
43
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
44
|
+
});
|
|
45
|
+
let stdout = "";
|
|
46
|
+
let nextId = 1;
|
|
47
|
+
const pending = new Map();
|
|
48
|
+
child.stdout.setEncoding("utf8");
|
|
49
|
+
child.stdout.on("data", (chunk) => {
|
|
50
|
+
stdout += chunk;
|
|
51
|
+
const decoded = decodeMessages(stdout);
|
|
52
|
+
stdout = decoded.rest;
|
|
53
|
+
for (const message of decoded.messages) {
|
|
54
|
+
if (message.id !== undefined) {
|
|
55
|
+
pending.get(message.id)?.(message);
|
|
56
|
+
pending.delete(message.id);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
function sendRequest(method, params) {
|
|
61
|
+
const id = nextId++;
|
|
62
|
+
child.stdin.write(encodeMessage({
|
|
63
|
+
id,
|
|
64
|
+
jsonrpc: "2.0",
|
|
65
|
+
method,
|
|
66
|
+
params
|
|
67
|
+
}));
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
pending.set(id, resolve);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function sendNotification(method, params) {
|
|
73
|
+
child.stdin.write(encodeMessage({
|
|
74
|
+
jsonrpc: "2.0",
|
|
75
|
+
method,
|
|
76
|
+
params
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const initialize = await sendRequest("initialize", {
|
|
81
|
+
capabilities: {},
|
|
82
|
+
processId: process.pid,
|
|
83
|
+
rootUri: null
|
|
84
|
+
});
|
|
85
|
+
strict_1.default.equal(initialize.id, 1);
|
|
86
|
+
sendNotification("initialized", {});
|
|
87
|
+
sendNotification("textDocument/didOpen", {
|
|
88
|
+
textDocument: {
|
|
89
|
+
uri,
|
|
90
|
+
languageId: "asm",
|
|
91
|
+
version: 1,
|
|
92
|
+
text
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const response = await sendRequest("textDocument/documentSymbol", {
|
|
96
|
+
textDocument: { uri }
|
|
97
|
+
});
|
|
98
|
+
const symbols = response.result;
|
|
99
|
+
strict_1.default.equal(Array.isArray(symbols), true);
|
|
100
|
+
strict_1.default.equal(symbols.some((symbol) => symbol.name === "TEXT" && symbol.kind === 13), true);
|
|
101
|
+
strict_1.default.equal(symbols.some((symbol) => symbol.name === "TEST_START" && symbol.kind === 6), true);
|
|
102
|
+
strict_1.default.equal(symbols.some((symbol) => symbol.name === "dum0" && symbol.kind === 8), true);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
child.kill();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runExpressionTest = runExpressionTest;
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const lexer_1 = require("../src/asm/lexer");
|
|
9
|
+
const expression_1 = require("../src/asm/expression");
|
|
10
|
+
function summarizeExpression(expression) {
|
|
11
|
+
switch (expression.kind) {
|
|
12
|
+
case "binary":
|
|
13
|
+
return {
|
|
14
|
+
kind: expression.kind,
|
|
15
|
+
operator: expression.operator,
|
|
16
|
+
left: summarizeExpression(expression.left),
|
|
17
|
+
right: summarizeExpression(expression.right)
|
|
18
|
+
};
|
|
19
|
+
case "modifier":
|
|
20
|
+
return {
|
|
21
|
+
kind: expression.kind,
|
|
22
|
+
operator: expression.operator,
|
|
23
|
+
expression: summarizeExpression(expression.expression)
|
|
24
|
+
};
|
|
25
|
+
default:
|
|
26
|
+
return {
|
|
27
|
+
kind: expression.kind,
|
|
28
|
+
value: expression.value
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function operandTokens(sourceLine) {
|
|
33
|
+
const line = (0, lexer_1.lexSource)(sourceLine).lines[0];
|
|
34
|
+
strict_1.default.ok(line, "expected a lexed line");
|
|
35
|
+
return line.tokens.slice(1);
|
|
36
|
+
}
|
|
37
|
+
function runExpressionTest() {
|
|
38
|
+
const arithmeticTokens = (0, lexer_1.lexSource)("_tmp+dum1+1").lines[0]?.tokens ?? [];
|
|
39
|
+
const arithmetic = (0, expression_1.parseExpression)(arithmeticTokens);
|
|
40
|
+
strict_1.default.equal(arithmetic.nextTokenIndex, arithmeticTokens.length);
|
|
41
|
+
strict_1.default.deepEqual(summarizeExpression(arithmetic.expression), {
|
|
42
|
+
kind: "binary",
|
|
43
|
+
operator: "+",
|
|
44
|
+
left: {
|
|
45
|
+
kind: "binary",
|
|
46
|
+
operator: "+",
|
|
47
|
+
left: { kind: "identifier", value: "_tmp" },
|
|
48
|
+
right: { kind: "identifier", value: "dum1" }
|
|
49
|
+
},
|
|
50
|
+
right: { kind: "numericLiteral", value: "1" }
|
|
51
|
+
});
|
|
52
|
+
const numericForms = ["$10", "%1010", "42"];
|
|
53
|
+
for (const numericForm of numericForms) {
|
|
54
|
+
const tokens = (0, lexer_1.lexSource)(numericForm).lines[0]?.tokens ?? [];
|
|
55
|
+
const parsed = (0, expression_1.parseExpression)(tokens);
|
|
56
|
+
strict_1.default.deepEqual(summarizeExpression(parsed.expression), {
|
|
57
|
+
kind: "numericLiteral",
|
|
58
|
+
value: numericForm
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const modifierTokens = (0, lexer_1.lexSource)("<value+1").lines[0]?.tokens ?? [];
|
|
62
|
+
const modifier = (0, expression_1.parseExpression)(modifierTokens);
|
|
63
|
+
strict_1.default.deepEqual(summarizeExpression(modifier.expression), {
|
|
64
|
+
kind: "binary",
|
|
65
|
+
operator: "+",
|
|
66
|
+
left: {
|
|
67
|
+
kind: "modifier",
|
|
68
|
+
operator: "<",
|
|
69
|
+
expression: { kind: "identifier", value: "value" }
|
|
70
|
+
},
|
|
71
|
+
right: { kind: "numericLiteral", value: "1" }
|
|
72
|
+
});
|
|
73
|
+
const immediateOperand = (0, expression_1.parseOperand)(operandTokens(" ldx #_LFT"));
|
|
74
|
+
strict_1.default.equal(immediateOperand.operand.immediate, true);
|
|
75
|
+
strict_1.default.equal(immediateOperand.operand.indirect, false);
|
|
76
|
+
strict_1.default.equal(immediateOperand.operand.indexRegister, null);
|
|
77
|
+
strict_1.default.deepEqual(summarizeExpression(immediateOperand.operand.expression), {
|
|
78
|
+
kind: "identifier",
|
|
79
|
+
value: "_LFT"
|
|
80
|
+
});
|
|
81
|
+
const indexedOperand = (0, expression_1.parseOperand)(operandTokens(" sta TSTADDR+_num1+dum0,x"));
|
|
82
|
+
strict_1.default.equal(indexedOperand.operand.immediate, false);
|
|
83
|
+
strict_1.default.equal(indexedOperand.operand.indirect, false);
|
|
84
|
+
strict_1.default.equal(indexedOperand.operand.indexRegister, "x");
|
|
85
|
+
const indirectIndexedOperand = (0, expression_1.parseOperand)(operandTokens(" adc (_tmp+dum1+1,x)"));
|
|
86
|
+
strict_1.default.equal(indirectIndexedOperand.operand.immediate, false);
|
|
87
|
+
strict_1.default.equal(indirectIndexedOperand.operand.indirect, true);
|
|
88
|
+
strict_1.default.equal(indirectIndexedOperand.operand.indexRegister, "x");
|
|
89
|
+
strict_1.default.deepEqual(summarizeExpression(indirectIndexedOperand.operand.expression), {
|
|
90
|
+
kind: "binary",
|
|
91
|
+
operator: "+",
|
|
92
|
+
left: {
|
|
93
|
+
kind: "binary",
|
|
94
|
+
operator: "+",
|
|
95
|
+
left: { kind: "identifier", value: "_tmp" },
|
|
96
|
+
right: { kind: "identifier", value: "dum1" }
|
|
97
|
+
},
|
|
98
|
+
right: { kind: "numericLiteral", value: "1" }
|
|
99
|
+
});
|
|
100
|
+
}
|