@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,131 @@
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
+ method?: string;
10
+ result?: unknown;
11
+ };
12
+
13
+ function encodeMessage(message: object): string {
14
+ const body = JSON.stringify(message);
15
+ return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
16
+ }
17
+
18
+ function decodeMessages(streamBuffer: string): { messages: JsonRpcMessage[]; rest: string } {
19
+ const messages: JsonRpcMessage[] = [];
20
+ let buffer = streamBuffer;
21
+
22
+ for (;;) {
23
+ const separator = buffer.indexOf("\r\n\r\n");
24
+ if (separator === -1) {
25
+ return { messages, rest: buffer };
26
+ }
27
+
28
+ const header = buffer.slice(0, separator);
29
+ const match = /Content-Length: (\d+)/i.exec(header);
30
+ if (!match) {
31
+ throw new Error(`Missing Content-Length header: ${header}`);
32
+ }
33
+
34
+ const length = Number(match[1]);
35
+ const body = buffer.slice(separator + 4);
36
+ if (Buffer.byteLength(body, "utf8") < length) {
37
+ return { messages, rest: buffer };
38
+ }
39
+
40
+ messages.push(JSON.parse(body.slice(0, length)) as JsonRpcMessage);
41
+ buffer = body.slice(length);
42
+ }
43
+ }
44
+
45
+ export async function runDocumentSymbolTest(): Promise<void> {
46
+ const serverPath = path.resolve(__dirname, "../src/server.js");
47
+ const fixturePath = path.resolve(
48
+ process.cwd(),
49
+ "test/fixtures/valid/merlin32-main-6502.asm"
50
+ );
51
+ const uri = `file://${fixturePath.replace(/\\/g, "/")}`;
52
+ const text = fs.readFileSync(fixturePath, "utf8");
53
+ const child = spawn(process.execPath, [serverPath], {
54
+ stdio: ["pipe", "pipe", "pipe"]
55
+ });
56
+
57
+ let stdout = "";
58
+ let nextId = 1;
59
+ const pending = new Map<number, (message: JsonRpcMessage) => void>();
60
+
61
+ child.stdout.setEncoding("utf8");
62
+ child.stdout.on("data", (chunk: string) => {
63
+ stdout += chunk;
64
+ const decoded = decodeMessages(stdout);
65
+ stdout = decoded.rest;
66
+
67
+ for (const message of decoded.messages) {
68
+ if (message.id !== undefined) {
69
+ pending.get(message.id)?.(message);
70
+ pending.delete(message.id);
71
+ }
72
+ }
73
+ });
74
+
75
+ function sendRequest(method: string, params: object): Promise<JsonRpcMessage> {
76
+ const id = nextId++;
77
+ child.stdin.write(
78
+ encodeMessage({
79
+ id,
80
+ jsonrpc: "2.0",
81
+ method,
82
+ params
83
+ })
84
+ );
85
+
86
+ return new Promise((resolve) => {
87
+ pending.set(id, resolve);
88
+ });
89
+ }
90
+
91
+ function sendNotification(method: string, params: object): void {
92
+ child.stdin.write(
93
+ encodeMessage({
94
+ jsonrpc: "2.0",
95
+ method,
96
+ params
97
+ })
98
+ );
99
+ }
100
+
101
+ try {
102
+ const initialize = await sendRequest("initialize", {
103
+ capabilities: {},
104
+ processId: process.pid,
105
+ rootUri: null
106
+ });
107
+ assert.equal(initialize.id, 1);
108
+
109
+ sendNotification("initialized", {});
110
+ sendNotification("textDocument/didOpen", {
111
+ textDocument: {
112
+ uri,
113
+ languageId: "asm",
114
+ version: 1,
115
+ text
116
+ }
117
+ });
118
+
119
+ const response = await sendRequest("textDocument/documentSymbol", {
120
+ textDocument: { uri }
121
+ });
122
+
123
+ const symbols = response.result as Array<{ name: string; kind: number }>;
124
+ assert.equal(Array.isArray(symbols), true);
125
+ assert.equal(symbols.some((symbol) => symbol.name === "TEXT" && symbol.kind === 13), true);
126
+ assert.equal(symbols.some((symbol) => symbol.name === "TEST_START" && symbol.kind === 6), true);
127
+ assert.equal(symbols.some((symbol) => symbol.name === "dum0" && symbol.kind === 8), true);
128
+ } finally {
129
+ child.kill();
130
+ }
131
+ }
@@ -0,0 +1,111 @@
1
+ import assert from "node:assert/strict";
2
+
3
+ import { lexSource } from "../src/asm/lexer";
4
+ import {
5
+ type Expression,
6
+ parseExpression,
7
+ parseOperand
8
+ } from "../src/asm/expression";
9
+
10
+ function summarizeExpression(expression: Expression): unknown {
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
+
33
+ function operandTokens(sourceLine: string) {
34
+ const line = lexSource(sourceLine).lines[0];
35
+ assert.ok(line, "expected a lexed line");
36
+ return line.tokens.slice(1);
37
+ }
38
+
39
+ export function runExpressionTest(): void {
40
+ const arithmeticTokens = lexSource("_tmp+dum1+1").lines[0]?.tokens ?? [];
41
+ const arithmetic = parseExpression(arithmeticTokens);
42
+ assert.equal(arithmetic.nextTokenIndex, arithmeticTokens.length);
43
+ assert.deepEqual(summarizeExpression(arithmetic.expression), {
44
+ kind: "binary",
45
+ operator: "+",
46
+ left: {
47
+ kind: "binary",
48
+ operator: "+",
49
+ left: { kind: "identifier", value: "_tmp" },
50
+ right: { kind: "identifier", value: "dum1" }
51
+ },
52
+ right: { kind: "numericLiteral", value: "1" }
53
+ });
54
+
55
+ const numericForms = ["$10", "%1010", "42"];
56
+ for (const numericForm of numericForms) {
57
+ const tokens = lexSource(numericForm).lines[0]?.tokens ?? [];
58
+ const parsed = parseExpression(tokens);
59
+ assert.deepEqual(summarizeExpression(parsed.expression), {
60
+ kind: "numericLiteral",
61
+ value: numericForm
62
+ });
63
+ }
64
+
65
+ const modifierTokens = lexSource("<value+1").lines[0]?.tokens ?? [];
66
+ const modifier = parseExpression(modifierTokens);
67
+ assert.deepEqual(summarizeExpression(modifier.expression), {
68
+ kind: "binary",
69
+ operator: "+",
70
+ left: {
71
+ kind: "modifier",
72
+ operator: "<",
73
+ expression: { kind: "identifier", value: "value" }
74
+ },
75
+ right: { kind: "numericLiteral", value: "1" }
76
+ });
77
+
78
+ const immediateOperand = parseOperand(operandTokens(" ldx #_LFT"));
79
+ assert.equal(immediateOperand.operand.immediate, true);
80
+ assert.equal(immediateOperand.operand.indirect, false);
81
+ assert.equal(immediateOperand.operand.indexRegister, null);
82
+ assert.deepEqual(summarizeExpression(immediateOperand.operand.expression), {
83
+ kind: "identifier",
84
+ value: "_LFT"
85
+ });
86
+
87
+ const indexedOperand = parseOperand(
88
+ operandTokens(" sta TSTADDR+_num1+dum0,x")
89
+ );
90
+ assert.equal(indexedOperand.operand.immediate, false);
91
+ assert.equal(indexedOperand.operand.indirect, false);
92
+ assert.equal(indexedOperand.operand.indexRegister, "x");
93
+
94
+ const indirectIndexedOperand = parseOperand(
95
+ operandTokens(" adc (_tmp+dum1+1,x)")
96
+ );
97
+ assert.equal(indirectIndexedOperand.operand.immediate, false);
98
+ assert.equal(indirectIndexedOperand.operand.indirect, true);
99
+ assert.equal(indirectIndexedOperand.operand.indexRegister, "x");
100
+ assert.deepEqual(summarizeExpression(indirectIndexedOperand.operand.expression), {
101
+ kind: "binary",
102
+ operator: "+",
103
+ left: {
104
+ kind: "binary",
105
+ operator: "+",
106
+ left: { kind: "identifier", value: "_tmp" },
107
+ right: { kind: "identifier", value: "dum1" }
108
+ },
109
+ right: { kind: "numericLiteral", value: "1" }
110
+ });
111
+ }
@@ -0,0 +1,33 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const validFixturePaths = [
6
+ "test/fixtures/valid/merlin32-linkscript.asm",
7
+ "test/fixtures/valid/merlin32-main-6502.asm"
8
+ ];
9
+
10
+ const invalidFixturePaths = [
11
+ "test/fixtures/invalid/65816-bank-ops.asm",
12
+ "test/fixtures/invalid/65816-long-addressing.asm"
13
+ ];
14
+
15
+ export function runFixtureCorpusTest(): void {
16
+ for (const fixturePath of validFixturePaths) {
17
+ const absolutePath = path.resolve(process.cwd(), fixturePath);
18
+ assert.equal(fs.existsSync(absolutePath), true, `${fixturePath} should exist`);
19
+
20
+ const content = fs.readFileSync(absolutePath, "utf8");
21
+ assert.match(content, /Source: apple2accumulator\/merlin32/);
22
+ assert.ok(content.trim().length > 0, `${fixturePath} should not be empty`);
23
+ }
24
+
25
+ for (const fixturePath of invalidFixturePaths) {
26
+ const absolutePath = path.resolve(process.cwd(), fixturePath);
27
+ assert.equal(fs.existsSync(absolutePath), true, `${fixturePath} should exist`);
28
+
29
+ const content = fs.readFileSync(absolutePath, "utf8");
30
+ assert.match(content, /Source: apple2accumulator\/merlin32/);
31
+ assert.match(content, /65816-only/);
32
+ }
33
+ }
@@ -0,0 +1,17 @@
1
+ ; Source: apple2accumulator/merlin32
2
+ ; Upstream file: Test/main.s
3
+ ; 65816-only fixture: block move, MX flags, and PEA forms are intentionally unsupported.
4
+
5
+ xc
6
+ xc
7
+ org $018200
8
+
9
+ bank02 equ $020000
10
+ bank03 equ $030000
11
+
12
+ mx %00
13
+ start nop
14
+ pea ^start
15
+ pea start
16
+ mvn bank02,bank03
17
+ mvp bank03,bank02
@@ -0,0 +1,26 @@
1
+ ; Source: apple2accumulator/merlin32
2
+ ; Upstream file: Test/main.s
3
+ ; 65816-only fixture: long-addressing and bank-byte modifiers are intentionally unsupported.
4
+
5
+ xc
6
+ xc
7
+ org $018200
8
+
9
+ dp equ $A5
10
+ long equ $020304
11
+
12
+ lda dp
13
+ lda <dp
14
+ lda >dp
15
+ lda ^dp
16
+ lda |dp
17
+
18
+ lda #long
19
+ lda #<long
20
+ lda #>long
21
+ lda #^long
22
+
23
+ lda long
24
+ lda <long
25
+ lda >long
26
+ lda ^long
@@ -0,0 +1,16 @@
1
+ ; Source: apple2accumulator/merlin32
2
+ ; Upstream file: Test/linkscript.s
3
+ ; Transcribed for a 6502-only positive fixture corpus.
4
+
5
+ * linkscript.s
6
+ * Merlin32 Test
7
+ *
8
+ * Created by Lane Roathe on 8/21/19.
9
+
10
+ typ $06
11
+
12
+ dsk Merlin32Test
13
+ org $800
14
+
15
+ asm "merlin32-main-6502.asm"
16
+ sna main
@@ -0,0 +1,103 @@
1
+ ; Source: apple2accumulator/merlin32
2
+ ; Upstream file: Test/main.s
3
+ ; Transcribed from the 6502-safe portion and trimmed to exclude 65816-only cases.
4
+
5
+ * main.s
6
+ * Merlin32 Test
7
+ *
8
+ * Created by Lane Roathe on 8/26/19.
9
+
10
+ ]XCODESTART
11
+
12
+ TEXT = $FB39
13
+ CROUT = $FD8E
14
+ DOSWARM = $3D0
15
+ TSTADDR = $1000
16
+
17
+ DUM 0
18
+ dum0 ds 1
19
+ dum1 ds 1
20
+ dumSize = *
21
+ DEND
22
+
23
+ DUM 0
24
+ _ptr ds 2
25
+ _tmp ds 2
26
+ _num1 ds dumSize
27
+
28
+ ORG $20
29
+ _LFT ds 1
30
+ DEND
31
+
32
+ TEST_START
33
+ adc (0,x)
34
+ adc ($80,x)
35
+ adc (_tmp,x)
36
+ adc (_tmp+0,x)
37
+ adc (_tmp+$10,x)
38
+ adc ($10+_tmp,x)
39
+ adc (_tmp+dum0,x)
40
+ adc (_tmp+dum1,x)
41
+ adc (_tmp+dum1+1,x)
42
+ adc (_tmp+dum0+dum1,x)
43
+
44
+ adc 0
45
+ adc $80
46
+ adc _tmp
47
+ adc #0
48
+ adc $1111
49
+
50
+ sta TSTADDR+dum0
51
+ sta TSTADDR+_num1+dum0
52
+ sta TSTADDR+_num1+dum0,x
53
+
54
+ lda _num1+dum0
55
+ adc _num1+dum1
56
+ sbc _num1+dum1
57
+ bit _num1+dum0
58
+ sta _num1+dum0
59
+
60
+ lda _num1+dum0,x
61
+ adc _num1+dum0,x
62
+ sbc _num1+dum0,x
63
+ sta _num1+dum0,x
64
+
65
+ lda _num1+dum0,y
66
+ adc _num1+dum0,y
67
+ sbc _num1+dum0,y
68
+ sta _num1+dum0,y
69
+
70
+ GetKey ldx $C000
71
+ bpl GetKey
72
+ ]loop
73
+ dex
74
+ bne ]loop
75
+
76
+ tya
77
+ and #1
78
+ beq :err
79
+
80
+ tya
81
+ and #1
82
+ bne :good
83
+ :err
84
+ lda #0
85
+ :good
86
+ bne myQuit
87
+ nop
88
+ hex 2C
89
+ lda #1
90
+ myQuit
91
+ jmp DOSWARM
92
+
93
+ org $2000
94
+
95
+ lda _LFT
96
+ ldx #_LFT
97
+ cpx #$20
98
+
99
+ org
100
+
101
+ stx $bc,y
102
+
103
+ ]XCODEEND
@@ -0,0 +1,7 @@
1
+ ; Smoke test fixture for coc.nvim integration.
2
+ ; This file intentionally contains one unresolved reference to exercise diagnostics.
3
+
4
+ GetKey ldx $C000
5
+ bpl GetKey
6
+ jmp NoSuchLabel
7
+ END
@@ -0,0 +1,175 @@
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
+ function positionOfInMatch(
56
+ text: string,
57
+ needle: string,
58
+ offset: number
59
+ ): { line: number; character: number } {
60
+ const base = positionOf(text, needle);
61
+ return {
62
+ line: base.line,
63
+ character: base.character + offset
64
+ };
65
+ }
66
+
67
+ export async function runHoverTest(): Promise<void> {
68
+ const serverPath = path.resolve(__dirname, "../src/server.js");
69
+ const mainPath = path.resolve(
70
+ process.cwd(),
71
+ "test/fixtures/valid/merlin32-main-6502.asm"
72
+ );
73
+ const mainUri = `file://${mainPath.replace(/\\/g, "/")}`;
74
+ const text = fs.readFileSync(mainPath, "utf8");
75
+ const child = spawn(process.execPath, [serverPath], {
76
+ stdio: ["pipe", "pipe", "pipe"]
77
+ });
78
+
79
+ let stdout = "";
80
+ let nextId = 1;
81
+ const pending = new Map<number, (message: JsonRpcMessage) => void>();
82
+
83
+ child.stdout.setEncoding("utf8");
84
+ child.stdout.on("data", (chunk: string) => {
85
+ stdout += chunk;
86
+ const decoded = decodeMessages(stdout);
87
+ stdout = decoded.rest;
88
+
89
+ for (const message of decoded.messages) {
90
+ if (message.id !== undefined) {
91
+ pending.get(message.id)?.(message);
92
+ pending.delete(message.id);
93
+ }
94
+ }
95
+ });
96
+
97
+ function sendRequest(method: string, params: object): Promise<JsonRpcMessage> {
98
+ const id = nextId++;
99
+ child.stdin.write(
100
+ encodeMessage({
101
+ id,
102
+ jsonrpc: "2.0",
103
+ method,
104
+ params
105
+ })
106
+ );
107
+
108
+ return new Promise((resolve) => {
109
+ pending.set(id, resolve);
110
+ });
111
+ }
112
+
113
+ function sendNotification(method: string, params: object): void {
114
+ child.stdin.write(
115
+ encodeMessage({
116
+ jsonrpc: "2.0",
117
+ method,
118
+ params
119
+ })
120
+ );
121
+ }
122
+
123
+ try {
124
+ await sendRequest("initialize", {
125
+ capabilities: {},
126
+ processId: process.pid,
127
+ rootUri: `file://${path.resolve(process.cwd()).replace(/\\/g, "/")}`
128
+ });
129
+
130
+ sendNotification("initialized", {});
131
+ sendNotification("textDocument/didOpen", {
132
+ textDocument: {
133
+ uri: mainUri,
134
+ languageId: "asm",
135
+ version: 1,
136
+ text
137
+ }
138
+ });
139
+
140
+ const opcodeHover = await sendRequest("textDocument/hover", {
141
+ textDocument: { uri: mainUri },
142
+ position: positionOf(text, "adc (_tmp")
143
+ });
144
+ const opcodeResult = opcodeHover.result as { contents: string | { value: string } };
145
+ const opcodeText =
146
+ typeof opcodeResult.contents === "string"
147
+ ? opcodeResult.contents
148
+ : opcodeResult.contents.value;
149
+ assert.equal(opcodeText.includes("adc"), true);
150
+
151
+ const directiveHover = await sendRequest("textDocument/hover", {
152
+ textDocument: { uri: mainUri },
153
+ position: positionOf(text, "DUM 0")
154
+ });
155
+ const directiveResult = directiveHover.result as { contents: string | { value: string } };
156
+ const directiveText =
157
+ typeof directiveResult.contents === "string"
158
+ ? directiveResult.contents
159
+ : directiveResult.contents.value;
160
+ assert.equal(directiveText.includes("dum"), true);
161
+
162
+ const symbolHover = await sendRequest("textDocument/hover", {
163
+ textDocument: { uri: mainUri },
164
+ position: positionOfInMatch(text, "bpl GetKey", 4)
165
+ });
166
+ const symbolResult = symbolHover.result as { contents: string | { value: string } };
167
+ const symbolText =
168
+ typeof symbolResult.contents === "string"
169
+ ? symbolResult.contents
170
+ : symbolResult.contents.value;
171
+ assert.equal(symbolText.includes("GetKey"), true);
172
+ } finally {
173
+ child.kill();
174
+ }
175
+ }