@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.
Files changed (104) hide show
  1. package/.serena/memories/conventions.md +6 -0
  2. package/.serena/memories/core.md +8 -0
  3. package/.serena/memories/memory_maintenance.md +33 -0
  4. package/.serena/memories/suggested_commands.md +4 -0
  5. package/.serena/memories/task_completion.md +7 -0
  6. package/.serena/memories/tech_stack.md +6 -0
  7. package/.serena/project.yml +132 -0
  8. package/AGENTS.md +63 -0
  9. package/README.md +137 -0
  10. package/dist/src/asm/diagnostics.js +202 -0
  11. package/dist/src/asm/document.js +26 -0
  12. package/dist/src/asm/expression.js +163 -0
  13. package/dist/src/asm/lexer.js +122 -0
  14. package/dist/src/asm/local-labels.js +140 -0
  15. package/dist/src/asm/metadata.js +101 -0
  16. package/dist/src/asm/parser.js +118 -0
  17. package/dist/src/asm/symbols.js +40 -0
  18. package/dist/src/asm/syntax.js +44 -0
  19. package/dist/src/asm/workspace.js +73 -0
  20. package/dist/src/cli.js +21 -0
  21. package/dist/src/index.js +4 -0
  22. package/dist/src/lsp/completion.js +32 -0
  23. package/dist/src/lsp/diagnostics.js +63 -0
  24. package/dist/src/lsp/document-symbols.js +80 -0
  25. package/dist/src/lsp/hover.js +75 -0
  26. package/dist/src/lsp/symbol-navigation.js +181 -0
  27. package/dist/src/lsp/workspace-symbols.js +17 -0
  28. package/dist/src/server.js +77 -0
  29. package/dist/test/bootstrap.test.js +11 -0
  30. package/dist/test/cli-contract.test.js +74 -0
  31. package/dist/test/coc-config.test.js +21 -0
  32. package/dist/test/completion.test.js +126 -0
  33. package/dist/test/definition-references.test.js +126 -0
  34. package/dist/test/diagnostics.test.js +66 -0
  35. package/dist/test/document-model.test.js +30 -0
  36. package/dist/test/document-symbol.test.js +107 -0
  37. package/dist/test/expression.test.js +100 -0
  38. package/dist/test/fixture-corpus.test.js +33 -0
  39. package/dist/test/hover.test.js +142 -0
  40. package/dist/test/lexer.test.js +53 -0
  41. package/dist/test/line-parser.test.js +67 -0
  42. package/dist/test/local-labels.test.js +43 -0
  43. package/dist/test/metadata.test.js +27 -0
  44. package/dist/test/publish-diagnostics.test.js +137 -0
  45. package/dist/test/run-tests.js +132 -0
  46. package/dist/test/server-entrypoint.test.js +14 -0
  47. package/dist/test/server-initialize.test.js +77 -0
  48. package/dist/test/symbols.test.js +37 -0
  49. package/dist/test/syntax-shape.test.js +18 -0
  50. package/dist/test/workspace-symbol.test.js +113 -0
  51. package/dist/test/workspace.test.js +24 -0
  52. package/examples/coc-settings.json +18 -0
  53. package/package.json +26 -0
  54. package/publish.ps1 +9 -0
  55. package/src/asm/diagnostics.ts +294 -0
  56. package/src/asm/document.ts +43 -0
  57. package/src/asm/expression.ts +242 -0
  58. package/src/asm/lexer.ts +197 -0
  59. package/src/asm/local-labels.ts +204 -0
  60. package/src/asm/metadata.ts +150 -0
  61. package/src/asm/parser.ts +197 -0
  62. package/src/asm/symbols.ts +55 -0
  63. package/src/asm/syntax.ts +76 -0
  64. package/src/asm/workspace.ts +105 -0
  65. package/src/cli.ts +24 -0
  66. package/src/index.ts +1 -0
  67. package/src/lsp/completion.ts +42 -0
  68. package/src/lsp/diagnostics.ts +82 -0
  69. package/src/lsp/document-symbols.ts +111 -0
  70. package/src/lsp/hover.ts +90 -0
  71. package/src/lsp/symbol-navigation.ts +244 -0
  72. package/src/lsp/workspace-symbols.ts +24 -0
  73. package/src/server.ts +121 -0
  74. package/test/bootstrap.test.ts +7 -0
  75. package/test/cli-contract.test.ts +94 -0
  76. package/test/coc-config.test.ts +28 -0
  77. package/test/completion.test.ts +151 -0
  78. package/test/definition-references.test.ts +152 -0
  79. package/test/diagnostics.test.ts +129 -0
  80. package/test/document-model.test.ts +29 -0
  81. package/test/document-symbol.test.ts +131 -0
  82. package/test/expression.test.ts +111 -0
  83. package/test/fixture-corpus.test.ts +33 -0
  84. package/test/fixtures/invalid/65816-bank-ops.asm +17 -0
  85. package/test/fixtures/invalid/65816-long-addressing.asm +26 -0
  86. package/test/fixtures/valid/merlin32-linkscript.asm +16 -0
  87. package/test/fixtures/valid/merlin32-main-6502.asm +103 -0
  88. package/test/fixtures/valid/smoke-test.asm +7 -0
  89. package/test/hover.test.ts +175 -0
  90. package/test/lexer.test.ts +87 -0
  91. package/test/line-parser.test.ts +69 -0
  92. package/test/local-labels.test.ts +47 -0
  93. package/test/metadata.test.ts +27 -0
  94. package/test/publish-diagnostics.test.ts +206 -0
  95. package/test/run-tests.ts +139 -0
  96. package/test/server-entrypoint.test.ts +11 -0
  97. package/test/server-initialize.test.ts +101 -0
  98. package/test/smoke/run-smoke.ps1 +177 -0
  99. package/test/smoke/vimrc +17 -0
  100. package/test/symbols.test.ts +41 -0
  101. package/test/syntax-shape.test.ts +18 -0
  102. package/test/workspace-symbol.test.ts +139 -0
  103. package/test/workspace.test.ts +29 -0
  104. package/tsconfig.json +16 -0
@@ -0,0 +1,33 @@
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.runFixtureCorpusTest = runFixtureCorpusTest;
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 validFixturePaths = [
11
+ "test/fixtures/valid/merlin32-linkscript.asm",
12
+ "test/fixtures/valid/merlin32-main-6502.asm"
13
+ ];
14
+ const invalidFixturePaths = [
15
+ "test/fixtures/invalid/65816-bank-ops.asm",
16
+ "test/fixtures/invalid/65816-long-addressing.asm"
17
+ ];
18
+ function runFixtureCorpusTest() {
19
+ for (const fixturePath of validFixturePaths) {
20
+ const absolutePath = node_path_1.default.resolve(process.cwd(), fixturePath);
21
+ strict_1.default.equal(node_fs_1.default.existsSync(absolutePath), true, `${fixturePath} should exist`);
22
+ const content = node_fs_1.default.readFileSync(absolutePath, "utf8");
23
+ strict_1.default.match(content, /Source: apple2accumulator\/merlin32/);
24
+ strict_1.default.ok(content.trim().length > 0, `${fixturePath} should not be empty`);
25
+ }
26
+ for (const fixturePath of invalidFixturePaths) {
27
+ const absolutePath = node_path_1.default.resolve(process.cwd(), fixturePath);
28
+ strict_1.default.equal(node_fs_1.default.existsSync(absolutePath), true, `${fixturePath} should exist`);
29
+ const content = node_fs_1.default.readFileSync(absolutePath, "utf8");
30
+ strict_1.default.match(content, /Source: apple2accumulator\/merlin32/);
31
+ strict_1.default.match(content, /65816-only/);
32
+ }
33
+ }
@@ -0,0 +1,142 @@
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.runHoverTest = runHoverTest;
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
+ function positionOfInMatch(text, needle, offset) {
48
+ const base = positionOf(text, needle);
49
+ return {
50
+ line: base.line,
51
+ character: base.character + offset
52
+ };
53
+ }
54
+ async function runHoverTest() {
55
+ const serverPath = node_path_1.default.resolve(__dirname, "../src/server.js");
56
+ const mainPath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-main-6502.asm");
57
+ const mainUri = `file://${mainPath.replace(/\\/g, "/")}`;
58
+ const text = node_fs_1.default.readFileSync(mainPath, "utf8");
59
+ const child = (0, node_child_process_1.spawn)(process.execPath, [serverPath], {
60
+ stdio: ["pipe", "pipe", "pipe"]
61
+ });
62
+ let stdout = "";
63
+ let nextId = 1;
64
+ const pending = new Map();
65
+ child.stdout.setEncoding("utf8");
66
+ child.stdout.on("data", (chunk) => {
67
+ stdout += chunk;
68
+ const decoded = decodeMessages(stdout);
69
+ stdout = decoded.rest;
70
+ for (const message of decoded.messages) {
71
+ if (message.id !== undefined) {
72
+ pending.get(message.id)?.(message);
73
+ pending.delete(message.id);
74
+ }
75
+ }
76
+ });
77
+ function sendRequest(method, params) {
78
+ const id = nextId++;
79
+ child.stdin.write(encodeMessage({
80
+ id,
81
+ jsonrpc: "2.0",
82
+ method,
83
+ params
84
+ }));
85
+ return new Promise((resolve) => {
86
+ pending.set(id, resolve);
87
+ });
88
+ }
89
+ function sendNotification(method, params) {
90
+ child.stdin.write(encodeMessage({
91
+ jsonrpc: "2.0",
92
+ method,
93
+ params
94
+ }));
95
+ }
96
+ try {
97
+ await sendRequest("initialize", {
98
+ capabilities: {},
99
+ processId: process.pid,
100
+ rootUri: `file://${node_path_1.default.resolve(process.cwd()).replace(/\\/g, "/")}`
101
+ });
102
+ sendNotification("initialized", {});
103
+ sendNotification("textDocument/didOpen", {
104
+ textDocument: {
105
+ uri: mainUri,
106
+ languageId: "asm",
107
+ version: 1,
108
+ text
109
+ }
110
+ });
111
+ const opcodeHover = await sendRequest("textDocument/hover", {
112
+ textDocument: { uri: mainUri },
113
+ position: positionOf(text, "adc (_tmp")
114
+ });
115
+ const opcodeResult = opcodeHover.result;
116
+ const opcodeText = typeof opcodeResult.contents === "string"
117
+ ? opcodeResult.contents
118
+ : opcodeResult.contents.value;
119
+ strict_1.default.equal(opcodeText.includes("adc"), true);
120
+ const directiveHover = await sendRequest("textDocument/hover", {
121
+ textDocument: { uri: mainUri },
122
+ position: positionOf(text, "DUM 0")
123
+ });
124
+ const directiveResult = directiveHover.result;
125
+ const directiveText = typeof directiveResult.contents === "string"
126
+ ? directiveResult.contents
127
+ : directiveResult.contents.value;
128
+ strict_1.default.equal(directiveText.includes("dum"), true);
129
+ const symbolHover = await sendRequest("textDocument/hover", {
130
+ textDocument: { uri: mainUri },
131
+ position: positionOfInMatch(text, "bpl GetKey", 4)
132
+ });
133
+ const symbolResult = symbolHover.result;
134
+ const symbolText = typeof symbolResult.contents === "string"
135
+ ? symbolResult.contents
136
+ : symbolResult.contents.value;
137
+ strict_1.default.equal(symbolText.includes("GetKey"), true);
138
+ }
139
+ finally {
140
+ child.kill();
141
+ }
142
+ }
@@ -0,0 +1,53 @@
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.runLexerTest = runLexerTest;
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 lexer_1 = require("../src/asm/lexer");
11
+ function summarizeTokens(tokens) {
12
+ return tokens.map((token) => [token.kind, token.lexeme]);
13
+ }
14
+ function findLine(lines, source) {
15
+ const line = lines.find((candidate) => candidate.text === source);
16
+ strict_1.default.ok(line, `expected to find line: ${source}`);
17
+ return line;
18
+ }
19
+ function runLexerTest() {
20
+ const mainFixturePath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-main-6502.asm");
21
+ const linkFixturePath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-linkscript.asm");
22
+ const mainFixture = node_fs_1.default.readFileSync(mainFixturePath, "utf8");
23
+ const linkFixture = node_fs_1.default.readFileSync(linkFixturePath, "utf8");
24
+ const mainLines = (0, lexer_1.lexSource)(mainFixture).lines;
25
+ const linkLines = (0, lexer_1.lexSource)(linkFixture).lines;
26
+ strict_1.default.deepEqual(summarizeTokens(findLine(mainLines, "; Source: apple2accumulator/merlin32").tokens), [["comment", "; Source: apple2accumulator/merlin32"]]);
27
+ strict_1.default.deepEqual(summarizeTokens(findLine(mainLines, "TEXT = $FB39").tokens), [
28
+ ["label", "TEXT"],
29
+ ["expressionOperator", "="],
30
+ ["numericLiteral", "$FB39"]
31
+ ]);
32
+ strict_1.default.deepEqual(summarizeTokens(findLine(mainLines, " DUM 0").tokens), [
33
+ ["directive", "DUM"],
34
+ ["numericLiteral", "0"]
35
+ ]);
36
+ strict_1.default.deepEqual(summarizeTokens(findLine(mainLines, " adc ($80,x)").tokens), [
37
+ ["mnemonic", "adc"],
38
+ ["expressionOperator", "("],
39
+ ["numericLiteral", "$80"],
40
+ ["expressionOperator", ","],
41
+ ["identifier", "x"],
42
+ ["expressionOperator", ")"]
43
+ ]);
44
+ strict_1.default.deepEqual(summarizeTokens(findLine(mainLines, "GetKey ldx $C000").tokens), [
45
+ ["label", "GetKey"],
46
+ ["mnemonic", "ldx"],
47
+ ["numericLiteral", "$C000"]
48
+ ]);
49
+ strict_1.default.deepEqual(summarizeTokens(findLine(linkLines, " asm \"merlin32-main-6502.asm\"").tokens), [
50
+ ["directive", "asm"],
51
+ ["string", "\"merlin32-main-6502.asm\""]
52
+ ]);
53
+ }
@@ -0,0 +1,67 @@
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.runLineParserTest = runLineParserTest;
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const parser_1 = require("../src/asm/parser");
9
+ function runLineParserTest() {
10
+ const source = [
11
+ "TEXT = $FB39",
12
+ " adc (_tmp+dum1+1,x)",
13
+ "dum0 ds 1",
14
+ " hex 2C",
15
+ " adc ("
16
+ ].join("\n");
17
+ const lines = (0, parser_1.parseSourceLines)(source);
18
+ strict_1.default.deepEqual(lines[0], {
19
+ shape: "equate",
20
+ text: "TEXT = $FB39",
21
+ label: "TEXT",
22
+ expression: {
23
+ kind: "numericLiteral",
24
+ value: "$FB39"
25
+ }
26
+ });
27
+ strict_1.default.deepEqual(lines[1], {
28
+ shape: "instruction",
29
+ text: " adc (_tmp+dum1+1,x)",
30
+ label: null,
31
+ mnemonic: "adc",
32
+ operand: {
33
+ immediate: false,
34
+ indirect: true,
35
+ indexRegister: "x",
36
+ expression: {
37
+ kind: "binary",
38
+ operator: "+",
39
+ left: {
40
+ kind: "binary",
41
+ operator: "+",
42
+ left: { kind: "identifier", value: "_tmp" },
43
+ right: { kind: "identifier", value: "dum1" }
44
+ },
45
+ right: { kind: "numericLiteral", value: "1" }
46
+ }
47
+ }
48
+ });
49
+ strict_1.default.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
+ strict_1.default.deepEqual(lines[3], {
60
+ shape: "data",
61
+ text: " hex 2C",
62
+ label: null,
63
+ directive: "hex",
64
+ payload: "2C"
65
+ });
66
+ strict_1.default.equal(lines[4]?.shape, "malformed");
67
+ }
@@ -0,0 +1,43 @@
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.runLocalLabelScopeTest = runLocalLabelScopeTest;
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 local_labels_1 = require("../src/asm/local-labels");
12
+ function runLocalLabelScopeTest() {
13
+ const fixturePath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-main-6502.asm");
14
+ const source = node_fs_1.default.readFileSync(fixturePath, "utf8");
15
+ const document = (0, document_1.parseDocument)(source);
16
+ const scope = (0, local_labels_1.resolveLocalLabels)(document);
17
+ strict_1.default.deepEqual(scope.definitions.get("]loop@69"), {
18
+ name: "]loop",
19
+ line: 71,
20
+ anchor: "GetKey",
21
+ qualifiedName: "]loop@69"
22
+ });
23
+ strict_1.default.deepEqual(scope.references.get("]loop@73"), {
24
+ name: "]loop",
25
+ line: 73,
26
+ anchor: "GetKey",
27
+ qualifiedName: "]loop@69",
28
+ targetLine: 71
29
+ });
30
+ strict_1.default.deepEqual(scope.definitions.get(":err@69"), {
31
+ name: ":err",
32
+ line: 82,
33
+ anchor: "GetKey",
34
+ qualifiedName: ":err@69"
35
+ });
36
+ strict_1.default.deepEqual(scope.references.get(":good@81"), {
37
+ name: ":good",
38
+ line: 81,
39
+ anchor: "GetKey",
40
+ qualifiedName: ":good@69",
41
+ targetLine: 84
42
+ });
43
+ }
@@ -0,0 +1,27 @@
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.runMetadataTableTest = runMetadataTableTest;
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const metadata_1 = require("../src/asm/metadata");
9
+ function runMetadataTableTest() {
10
+ strict_1.default.equal(metadata_1.opcodeTable.size, 56);
11
+ strict_1.default.deepEqual(metadata_1.opcodeTable.get("lda")?.modes, [
12
+ "immediate",
13
+ "zeroPage",
14
+ "zeroPageX",
15
+ "absolute",
16
+ "absoluteX",
17
+ "absoluteY",
18
+ "indexedIndirect",
19
+ "indirectIndexed"
20
+ ]);
21
+ strict_1.default.equal(metadata_1.opcodeTable.has("mvn"), false);
22
+ strict_1.default.equal(metadata_1.directiveTable.get("org")?.supported, true);
23
+ strict_1.default.equal(metadata_1.directiveTable.get("dum")?.supported, true);
24
+ strict_1.default.equal(metadata_1.directiveTable.get("xc")?.supported, false);
25
+ strict_1.default.equal(metadata_1.directiveTable.get("mx")?.supported, false);
26
+ strict_1.default.equal(metadata_1.directiveTable.get("put")?.kind, "include");
27
+ }
@@ -0,0 +1,137 @@
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.runPublishDiagnosticsTest = runPublishDiagnosticsTest;
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_child_process_1 = require("node:child_process");
10
+ function encodeMessage(message) {
11
+ const body = JSON.stringify(message);
12
+ return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
13
+ }
14
+ function decodeMessages(streamBuffer) {
15
+ const messages = [];
16
+ let buffer = streamBuffer;
17
+ for (;;) {
18
+ const separator = buffer.indexOf("\r\n\r\n");
19
+ if (separator === -1) {
20
+ return { messages, rest: buffer };
21
+ }
22
+ const header = buffer.slice(0, separator);
23
+ const match = /Content-Length: (\d+)/i.exec(header);
24
+ if (!match) {
25
+ throw new Error(`Missing Content-Length header: ${header}`);
26
+ }
27
+ const length = Number(match[1]);
28
+ const body = buffer.slice(separator + 4);
29
+ if (Buffer.byteLength(body, "utf8") < length) {
30
+ return { messages, rest: buffer };
31
+ }
32
+ messages.push(JSON.parse(body.slice(0, length)));
33
+ buffer = body.slice(length);
34
+ }
35
+ }
36
+ async function runPublishDiagnosticsTest() {
37
+ const serverPath = node_path_1.default.resolve(__dirname, "../src/server.js");
38
+ const documentPath = node_path_1.default.resolve(process.cwd(), "test/fixtures/invalid/publish-diagnostics.asm");
39
+ const documentUri = `file://${documentPath.replace(/\\/g, "/")}`;
40
+ const brokenText = ["dup equ 1", " lda missing", "dup equ 2", " adc ("].join("\n");
41
+ const fixedText = ["dup equ 1", " lda dup", " adc #1"].join("\n");
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
+ const diagnosticWaiters = [];
49
+ child.stdout.setEncoding("utf8");
50
+ child.stdout.on("data", (chunk) => {
51
+ stdout += chunk;
52
+ const decoded = decodeMessages(stdout);
53
+ stdout = decoded.rest;
54
+ for (const message of decoded.messages) {
55
+ if (message.id !== undefined) {
56
+ pending.get(message.id)?.(message);
57
+ pending.delete(message.id);
58
+ continue;
59
+ }
60
+ if (message.method === "textDocument/publishDiagnostics") {
61
+ const params = message.params;
62
+ const waiter = diagnosticWaiters.shift();
63
+ waiter?.(params);
64
+ }
65
+ }
66
+ });
67
+ function sendRequest(method, params) {
68
+ const id = nextId++;
69
+ child.stdin.write(encodeMessage({
70
+ id,
71
+ jsonrpc: "2.0",
72
+ method,
73
+ params
74
+ }));
75
+ return new Promise((resolve) => {
76
+ pending.set(id, resolve);
77
+ });
78
+ }
79
+ function sendNotification(method, params) {
80
+ child.stdin.write(encodeMessage({
81
+ jsonrpc: "2.0",
82
+ method,
83
+ params
84
+ }));
85
+ }
86
+ function waitForDiagnostics() {
87
+ return new Promise((resolve) => {
88
+ diagnosticWaiters.push(resolve);
89
+ });
90
+ }
91
+ try {
92
+ await sendRequest("initialize", {
93
+ capabilities: {},
94
+ processId: process.pid,
95
+ rootUri: `file://${node_path_1.default.resolve(process.cwd()).replace(/\\/g, "/")}`
96
+ });
97
+ sendNotification("initialized", {});
98
+ const openedDiagnostics = waitForDiagnostics();
99
+ sendNotification("textDocument/didOpen", {
100
+ textDocument: {
101
+ uri: documentUri,
102
+ languageId: "asm",
103
+ version: 1,
104
+ text: brokenText
105
+ }
106
+ });
107
+ const firstPublish = await openedDiagnostics;
108
+ strict_1.default.equal(firstPublish.uri, documentUri);
109
+ strict_1.default.equal(firstPublish.diagnostics.some((diagnostic) => diagnostic.message.includes("Unresolved reference missing") &&
110
+ diagnostic.severity === 1 &&
111
+ diagnostic.range.start.line === 1), true);
112
+ strict_1.default.equal(firstPublish.diagnostics.some((diagnostic) => diagnostic.message.includes("Duplicate symbol dup") &&
113
+ diagnostic.severity === 1 &&
114
+ diagnostic.range.start.line === 2), true);
115
+ strict_1.default.equal(firstPublish.diagnostics.some((diagnostic) => diagnostic.message.includes("expected expression token") &&
116
+ diagnostic.severity === 1 &&
117
+ diagnostic.range.start.line === 3), true);
118
+ const changedDiagnostics = waitForDiagnostics();
119
+ sendNotification("textDocument/didChange", {
120
+ textDocument: {
121
+ uri: documentUri,
122
+ version: 2
123
+ },
124
+ contentChanges: [
125
+ {
126
+ text: fixedText
127
+ }
128
+ ]
129
+ });
130
+ const secondPublish = await changedDiagnostics;
131
+ strict_1.default.equal(secondPublish.uri, documentUri);
132
+ strict_1.default.deepEqual(secondPublish.diagnostics, []);
133
+ }
134
+ finally {
135
+ child.kill();
136
+ }
137
+ }
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const bootstrap_test_1 = require("./bootstrap.test");
4
+ const cli_contract_test_1 = require("./cli-contract.test");
5
+ const coc_config_test_1 = require("./coc-config.test");
6
+ const completion_test_1 = require("./completion.test");
7
+ const definition_references_test_1 = require("./definition-references.test");
8
+ const diagnostics_test_1 = require("./diagnostics.test");
9
+ const document_model_test_1 = require("./document-model.test");
10
+ const document_symbol_test_1 = require("./document-symbol.test");
11
+ const expression_test_1 = require("./expression.test");
12
+ const fixture_corpus_test_1 = require("./fixture-corpus.test");
13
+ const hover_test_1 = require("./hover.test");
14
+ const lexer_test_1 = require("./lexer.test");
15
+ const line_parser_test_1 = require("./line-parser.test");
16
+ const local_labels_test_1 = require("./local-labels.test");
17
+ const metadata_test_1 = require("./metadata.test");
18
+ const publish_diagnostics_test_1 = require("./publish-diagnostics.test");
19
+ const server_initialize_test_1 = require("./server-initialize.test");
20
+ const server_entrypoint_test_1 = require("./server-entrypoint.test");
21
+ const symbols_test_1 = require("./symbols.test");
22
+ const syntax_shape_test_1 = require("./syntax-shape.test");
23
+ const workspace_test_1 = require("./workspace.test");
24
+ const workspace_symbol_test_1 = require("./workspace-symbol.test");
25
+ const tests = [
26
+ {
27
+ name: "workspace bootstrap exposes the project name",
28
+ run: bootstrap_test_1.runBootstrapTest
29
+ },
30
+ {
31
+ name: "cli contract exposes a packaged stdio entrypoint",
32
+ run: cli_contract_test_1.runCliContractTest
33
+ },
34
+ {
35
+ name: "coc.nvim example targets the packaged stdio CLI contract",
36
+ run: coc_config_test_1.runCocConfigTest
37
+ },
38
+ {
39
+ name: "server entrypoint exposes callable startup helpers",
40
+ run: server_entrypoint_test_1.runServerEntrypointTest
41
+ },
42
+ {
43
+ name: "server process answers initialize",
44
+ run: server_initialize_test_1.runInitializeHandshakeTest
45
+ },
46
+ {
47
+ name: "positive fixture corpus includes upstream Merlin32 samples",
48
+ run: fixture_corpus_test_1.runFixtureCorpusTest
49
+ },
50
+ {
51
+ name: "shared opcode and directive metadata covers 6502 and Merlin syntax",
52
+ run: metadata_test_1.runMetadataTableTest
53
+ },
54
+ {
55
+ name: "token kinds and line shapes cover Merlin syntax categories",
56
+ run: syntax_shape_test_1.runSyntaxShapeTest
57
+ },
58
+ {
59
+ name: "lexer tokenizes fixture comments labels mnemonics directives and literals",
60
+ run: lexer_test_1.runLexerTest
61
+ },
62
+ {
63
+ name: "expression parser handles numeric forms modifiers arithmetic and indexed operands",
64
+ run: expression_test_1.runExpressionTest
65
+ },
66
+ {
67
+ name: "line parser recognizes equates instructions directives data and malformed lines",
68
+ run: line_parser_test_1.runLineParserTest
69
+ },
70
+ {
71
+ name: "document model preserves line structure and tolerates malformed lines",
72
+ run: document_model_test_1.runDocumentModelTest
73
+ },
74
+ {
75
+ name: "symbol collection indexes labels equates and named data definitions",
76
+ run: symbols_test_1.runSymbolsTest
77
+ },
78
+ {
79
+ name: "local label scope resolves Merlin local definitions and references",
80
+ run: local_labels_test_1.runLocalLabelScopeTest
81
+ },
82
+ {
83
+ name: "workspace indexing follows Merlin include directives and merges symbols",
84
+ run: workspace_test_1.runWorkspaceGraphTest
85
+ },
86
+ {
87
+ name: "diagnostics report duplicates unresolved refs malformed lines and unsupported 65816 syntax",
88
+ run: diagnostics_test_1.runDiagnosticsTest
89
+ },
90
+ {
91
+ name: "server returns document symbols for Merlin labels equates and data definitions",
92
+ run: document_symbol_test_1.runDocumentSymbolTest
93
+ },
94
+ {
95
+ name: "server returns workspace symbols across open Merlin documents",
96
+ run: workspace_symbol_test_1.runWorkspaceSymbolTest
97
+ },
98
+ {
99
+ name: "server resolves definitions and references for Merlin symbols",
100
+ run: definition_references_test_1.runDefinitionReferencesTest
101
+ },
102
+ {
103
+ name: "server returns hover information for opcodes directives and symbols",
104
+ run: hover_test_1.runHoverTest
105
+ },
106
+ {
107
+ name: "server returns opcode directive and symbol completions",
108
+ run: completion_test_1.runCompletionTest
109
+ },
110
+ {
111
+ name: "server publishes and clears diagnostics for open Merlin documents",
112
+ run: publish_diagnostics_test_1.runPublishDiagnosticsTest
113
+ }
114
+ ];
115
+ async function main() {
116
+ let failures = 0;
117
+ for (const test of tests) {
118
+ try {
119
+ await test.run();
120
+ console.log(`PASS ${test.name}`);
121
+ }
122
+ catch (error) {
123
+ failures += 1;
124
+ console.error(`FAIL ${test.name}`);
125
+ console.error(error);
126
+ }
127
+ }
128
+ if (failures > 0) {
129
+ process.exitCode = 1;
130
+ }
131
+ }
132
+ void main();
@@ -0,0 +1,14 @@
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.runServerEntrypointTest = runServerEntrypointTest;
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const server_1 = require("../src/server");
9
+ function runServerEntrypointTest() {
10
+ strict_1.default.equal(typeof server_1.startServer, "function");
11
+ const connection = (0, server_1.createServerConnection)();
12
+ strict_1.default.equal(typeof connection.listen, "function");
13
+ connection.dispose();
14
+ }