@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,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,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
|
+
}
|