@razdolbai/merls 1.2.2 → 1.3.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/AGENTS.md +64 -64
- package/dist/src/lsp/completion.js +14 -11
- package/dist/test/completion.test.js +1 -1
- package/dist/test/definition-references.test.js +1 -1
- package/dist/test/diagnostics.test.js +2 -2
- package/dist/test/document-symbol.test.js +1 -1
- package/dist/test/fixture-corpus.test.js +4 -4
- package/dist/test/hover.test.js +1 -1
- package/dist/test/lexer.test.js +4 -4
- package/dist/test/local-labels.test.js +1 -1
- package/dist/test/publish-diagnostics.test.js +1 -1
- package/dist/test/semantic-tokens.test.js +1 -1
- package/dist/test/symbols.test.js +1 -1
- package/dist/test/workspace-symbol.test.js +2 -2
- package/dist/test/workspace.test.js +2 -2
- package/package.json +1 -1
- package/src/lsp/completion.ts +15 -11
- package/test/completion.test.ts +151 -151
- package/test/definition-references.test.ts +152 -152
- package/test/diagnostics.test.ts +129 -129
- package/test/document-symbol.test.ts +131 -131
- package/test/fixture-corpus.test.ts +33 -33
- package/test/fixtures/valid/{merlin32-linkscript.asm → merlin32-linkscript.S} +16 -16
- package/test/hover.test.ts +175 -175
- package/test/lexer.test.ts +87 -87
- package/test/local-labels.test.ts +47 -47
- package/test/publish-diagnostics.test.ts +206 -206
- package/test/semantic-tokens.test.ts +128 -128
- package/test/smoke/run-smoke.ps1 +177 -177
- package/test/symbols.test.ts +41 -41
- package/test/workspace-symbol.test.ts +139 -139
- package/test/workspace.test.ts +29 -29
- /package/test/fixtures/invalid/{65816-bank-ops.asm → 65816-bank-ops.S} +0 -0
- /package/test/fixtures/invalid/{65816-long-addressing.asm → 65816-long-addressing.S} +0 -0
- /package/test/fixtures/valid/{merlin32-main-6502.asm → merlin32-main-6502.S} +0 -0
- /package/test/fixtures/valid/{smoke-test.asm → smoke-test.S} +0 -0
|
@@ -1,206 +1,206 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
|
|
5
|
-
type JsonRpcMessage = {
|
|
6
|
-
id?: number;
|
|
7
|
-
jsonrpc: "2.0";
|
|
8
|
-
method?: string;
|
|
9
|
-
params?: unknown;
|
|
10
|
-
result?: unknown;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
type PublishedDiagnostic = {
|
|
14
|
-
message: string;
|
|
15
|
-
severity?: number;
|
|
16
|
-
range: {
|
|
17
|
-
start: {
|
|
18
|
-
line: number;
|
|
19
|
-
character: number;
|
|
20
|
-
};
|
|
21
|
-
end: {
|
|
22
|
-
line: number;
|
|
23
|
-
character: number;
|
|
24
|
-
};
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type PublishDiagnosticsParams = {
|
|
29
|
-
uri: string;
|
|
30
|
-
diagnostics: PublishedDiagnostic[];
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
function encodeMessage(message: object): string {
|
|
34
|
-
const body = JSON.stringify(message);
|
|
35
|
-
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function decodeMessages(streamBuffer: string): { messages: JsonRpcMessage[]; rest: string } {
|
|
39
|
-
const messages: JsonRpcMessage[] = [];
|
|
40
|
-
let buffer = streamBuffer;
|
|
41
|
-
|
|
42
|
-
for (;;) {
|
|
43
|
-
const separator = buffer.indexOf("\r\n\r\n");
|
|
44
|
-
if (separator === -1) {
|
|
45
|
-
return { messages, rest: buffer };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const header = buffer.slice(0, separator);
|
|
49
|
-
const match = /Content-Length: (\d+)/i.exec(header);
|
|
50
|
-
if (!match) {
|
|
51
|
-
throw new Error(`Missing Content-Length header: ${header}`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const length = Number(match[1]);
|
|
55
|
-
const body = buffer.slice(separator + 4);
|
|
56
|
-
if (Buffer.byteLength(body, "utf8") < length) {
|
|
57
|
-
return { messages, rest: buffer };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
messages.push(JSON.parse(body.slice(0, length)) as JsonRpcMessage);
|
|
61
|
-
buffer = body.slice(length);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function runPublishDiagnosticsTest(): Promise<void> {
|
|
66
|
-
const serverPath = path.resolve(__dirname, "../src/server.js");
|
|
67
|
-
const documentPath = path.resolve(
|
|
68
|
-
process.cwd(),
|
|
69
|
-
"test/fixtures/invalid/publish-diagnostics.
|
|
70
|
-
);
|
|
71
|
-
const documentUri = `file://${documentPath.replace(/\\/g, "/")}`;
|
|
72
|
-
const brokenText = ["dup equ 1", " lda missing", "dup equ 2", " adc ("].join(
|
|
73
|
-
"\n"
|
|
74
|
-
);
|
|
75
|
-
const fixedText = ["dup equ 1", " lda dup", " adc #1"].join("\n");
|
|
76
|
-
const child = spawn(process.execPath, [serverPath], {
|
|
77
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
let stdout = "";
|
|
81
|
-
let nextId = 1;
|
|
82
|
-
const pending = new Map<number, (message: JsonRpcMessage) => void>();
|
|
83
|
-
const diagnosticWaiters: Array<(params: PublishDiagnosticsParams) => void> = [];
|
|
84
|
-
|
|
85
|
-
child.stdout.setEncoding("utf8");
|
|
86
|
-
child.stdout.on("data", (chunk: string) => {
|
|
87
|
-
stdout += chunk;
|
|
88
|
-
const decoded = decodeMessages(stdout);
|
|
89
|
-
stdout = decoded.rest;
|
|
90
|
-
|
|
91
|
-
for (const message of decoded.messages) {
|
|
92
|
-
if (message.id !== undefined) {
|
|
93
|
-
pending.get(message.id)?.(message);
|
|
94
|
-
pending.delete(message.id);
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (message.method === "textDocument/publishDiagnostics") {
|
|
99
|
-
const params = message.params as PublishDiagnosticsParams;
|
|
100
|
-
const waiter = diagnosticWaiters.shift();
|
|
101
|
-
waiter?.(params);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
function sendRequest(method: string, params: object): Promise<JsonRpcMessage> {
|
|
107
|
-
const id = nextId++;
|
|
108
|
-
child.stdin.write(
|
|
109
|
-
encodeMessage({
|
|
110
|
-
id,
|
|
111
|
-
jsonrpc: "2.0",
|
|
112
|
-
method,
|
|
113
|
-
params
|
|
114
|
-
})
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
return new Promise((resolve) => {
|
|
118
|
-
pending.set(id, resolve);
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function sendNotification(method: string, params: object): void {
|
|
123
|
-
child.stdin.write(
|
|
124
|
-
encodeMessage({
|
|
125
|
-
jsonrpc: "2.0",
|
|
126
|
-
method,
|
|
127
|
-
params
|
|
128
|
-
})
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function waitForDiagnostics(): Promise<PublishDiagnosticsParams> {
|
|
133
|
-
return new Promise((resolve) => {
|
|
134
|
-
diagnosticWaiters.push(resolve);
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
await sendRequest("initialize", {
|
|
140
|
-
capabilities: {},
|
|
141
|
-
processId: process.pid,
|
|
142
|
-
rootUri: `file://${path.resolve(process.cwd()).replace(/\\/g, "/")}`
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
sendNotification("initialized", {});
|
|
146
|
-
|
|
147
|
-
const openedDiagnostics = waitForDiagnostics();
|
|
148
|
-
sendNotification("textDocument/didOpen", {
|
|
149
|
-
textDocument: {
|
|
150
|
-
uri: documentUri,
|
|
151
|
-
languageId: "asm",
|
|
152
|
-
version: 1,
|
|
153
|
-
text: brokenText
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const firstPublish = await openedDiagnostics;
|
|
158
|
-
assert.equal(firstPublish.uri, documentUri);
|
|
159
|
-
assert.equal(
|
|
160
|
-
firstPublish.diagnostics.some(
|
|
161
|
-
(diagnostic) =>
|
|
162
|
-
diagnostic.message.includes("Unresolved reference missing") &&
|
|
163
|
-
diagnostic.severity === 1 &&
|
|
164
|
-
diagnostic.range.start.line === 1
|
|
165
|
-
),
|
|
166
|
-
true
|
|
167
|
-
);
|
|
168
|
-
assert.equal(
|
|
169
|
-
firstPublish.diagnostics.some(
|
|
170
|
-
(diagnostic) =>
|
|
171
|
-
diagnostic.message.includes("Duplicate symbol dup") &&
|
|
172
|
-
diagnostic.severity === 1 &&
|
|
173
|
-
diagnostic.range.start.line === 2
|
|
174
|
-
),
|
|
175
|
-
true
|
|
176
|
-
);
|
|
177
|
-
assert.equal(
|
|
178
|
-
firstPublish.diagnostics.some(
|
|
179
|
-
(diagnostic) =>
|
|
180
|
-
diagnostic.message.includes("expected expression token") &&
|
|
181
|
-
diagnostic.severity === 1 &&
|
|
182
|
-
diagnostic.range.start.line === 3
|
|
183
|
-
),
|
|
184
|
-
true
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
const changedDiagnostics = waitForDiagnostics();
|
|
188
|
-
sendNotification("textDocument/didChange", {
|
|
189
|
-
textDocument: {
|
|
190
|
-
uri: documentUri,
|
|
191
|
-
version: 2
|
|
192
|
-
},
|
|
193
|
-
contentChanges: [
|
|
194
|
-
{
|
|
195
|
-
text: fixedText
|
|
196
|
-
}
|
|
197
|
-
]
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const secondPublish = await changedDiagnostics;
|
|
201
|
-
assert.equal(secondPublish.uri, documentUri);
|
|
202
|
-
assert.deepEqual(secondPublish.diagnostics, []);
|
|
203
|
-
} finally {
|
|
204
|
-
child.kill();
|
|
205
|
-
}
|
|
206
|
-
}
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
type JsonRpcMessage = {
|
|
6
|
+
id?: number;
|
|
7
|
+
jsonrpc: "2.0";
|
|
8
|
+
method?: string;
|
|
9
|
+
params?: unknown;
|
|
10
|
+
result?: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type PublishedDiagnostic = {
|
|
14
|
+
message: string;
|
|
15
|
+
severity?: number;
|
|
16
|
+
range: {
|
|
17
|
+
start: {
|
|
18
|
+
line: number;
|
|
19
|
+
character: number;
|
|
20
|
+
};
|
|
21
|
+
end: {
|
|
22
|
+
line: number;
|
|
23
|
+
character: number;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type PublishDiagnosticsParams = {
|
|
29
|
+
uri: string;
|
|
30
|
+
diagnostics: PublishedDiagnostic[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function encodeMessage(message: object): string {
|
|
34
|
+
const body = JSON.stringify(message);
|
|
35
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function decodeMessages(streamBuffer: string): { messages: JsonRpcMessage[]; rest: string } {
|
|
39
|
+
const messages: JsonRpcMessage[] = [];
|
|
40
|
+
let buffer = streamBuffer;
|
|
41
|
+
|
|
42
|
+
for (;;) {
|
|
43
|
+
const separator = buffer.indexOf("\r\n\r\n");
|
|
44
|
+
if (separator === -1) {
|
|
45
|
+
return { messages, rest: buffer };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const header = buffer.slice(0, separator);
|
|
49
|
+
const match = /Content-Length: (\d+)/i.exec(header);
|
|
50
|
+
if (!match) {
|
|
51
|
+
throw new Error(`Missing Content-Length header: ${header}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const length = Number(match[1]);
|
|
55
|
+
const body = buffer.slice(separator + 4);
|
|
56
|
+
if (Buffer.byteLength(body, "utf8") < length) {
|
|
57
|
+
return { messages, rest: buffer };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
messages.push(JSON.parse(body.slice(0, length)) as JsonRpcMessage);
|
|
61
|
+
buffer = body.slice(length);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function runPublishDiagnosticsTest(): Promise<void> {
|
|
66
|
+
const serverPath = path.resolve(__dirname, "../src/server.js");
|
|
67
|
+
const documentPath = path.resolve(
|
|
68
|
+
process.cwd(),
|
|
69
|
+
"test/fixtures/invalid/publish-diagnostics.S"
|
|
70
|
+
);
|
|
71
|
+
const documentUri = `file://${documentPath.replace(/\\/g, "/")}`;
|
|
72
|
+
const brokenText = ["dup equ 1", " lda missing", "dup equ 2", " adc ("].join(
|
|
73
|
+
"\n"
|
|
74
|
+
);
|
|
75
|
+
const fixedText = ["dup equ 1", " lda dup", " adc #1"].join("\n");
|
|
76
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
77
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let stdout = "";
|
|
81
|
+
let nextId = 1;
|
|
82
|
+
const pending = new Map<number, (message: JsonRpcMessage) => void>();
|
|
83
|
+
const diagnosticWaiters: Array<(params: PublishDiagnosticsParams) => void> = [];
|
|
84
|
+
|
|
85
|
+
child.stdout.setEncoding("utf8");
|
|
86
|
+
child.stdout.on("data", (chunk: string) => {
|
|
87
|
+
stdout += chunk;
|
|
88
|
+
const decoded = decodeMessages(stdout);
|
|
89
|
+
stdout = decoded.rest;
|
|
90
|
+
|
|
91
|
+
for (const message of decoded.messages) {
|
|
92
|
+
if (message.id !== undefined) {
|
|
93
|
+
pending.get(message.id)?.(message);
|
|
94
|
+
pending.delete(message.id);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (message.method === "textDocument/publishDiagnostics") {
|
|
99
|
+
const params = message.params as PublishDiagnosticsParams;
|
|
100
|
+
const waiter = diagnosticWaiters.shift();
|
|
101
|
+
waiter?.(params);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
function sendRequest(method: string, params: object): Promise<JsonRpcMessage> {
|
|
107
|
+
const id = nextId++;
|
|
108
|
+
child.stdin.write(
|
|
109
|
+
encodeMessage({
|
|
110
|
+
id,
|
|
111
|
+
jsonrpc: "2.0",
|
|
112
|
+
method,
|
|
113
|
+
params
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
pending.set(id, resolve);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sendNotification(method: string, params: object): void {
|
|
123
|
+
child.stdin.write(
|
|
124
|
+
encodeMessage({
|
|
125
|
+
jsonrpc: "2.0",
|
|
126
|
+
method,
|
|
127
|
+
params
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function waitForDiagnostics(): Promise<PublishDiagnosticsParams> {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
diagnosticWaiters.push(resolve);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await sendRequest("initialize", {
|
|
140
|
+
capabilities: {},
|
|
141
|
+
processId: process.pid,
|
|
142
|
+
rootUri: `file://${path.resolve(process.cwd()).replace(/\\/g, "/")}`
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
sendNotification("initialized", {});
|
|
146
|
+
|
|
147
|
+
const openedDiagnostics = waitForDiagnostics();
|
|
148
|
+
sendNotification("textDocument/didOpen", {
|
|
149
|
+
textDocument: {
|
|
150
|
+
uri: documentUri,
|
|
151
|
+
languageId: "asm",
|
|
152
|
+
version: 1,
|
|
153
|
+
text: brokenText
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const firstPublish = await openedDiagnostics;
|
|
158
|
+
assert.equal(firstPublish.uri, documentUri);
|
|
159
|
+
assert.equal(
|
|
160
|
+
firstPublish.diagnostics.some(
|
|
161
|
+
(diagnostic) =>
|
|
162
|
+
diagnostic.message.includes("Unresolved reference missing") &&
|
|
163
|
+
diagnostic.severity === 1 &&
|
|
164
|
+
diagnostic.range.start.line === 1
|
|
165
|
+
),
|
|
166
|
+
true
|
|
167
|
+
);
|
|
168
|
+
assert.equal(
|
|
169
|
+
firstPublish.diagnostics.some(
|
|
170
|
+
(diagnostic) =>
|
|
171
|
+
diagnostic.message.includes("Duplicate symbol dup") &&
|
|
172
|
+
diagnostic.severity === 1 &&
|
|
173
|
+
diagnostic.range.start.line === 2
|
|
174
|
+
),
|
|
175
|
+
true
|
|
176
|
+
);
|
|
177
|
+
assert.equal(
|
|
178
|
+
firstPublish.diagnostics.some(
|
|
179
|
+
(diagnostic) =>
|
|
180
|
+
diagnostic.message.includes("expected expression token") &&
|
|
181
|
+
diagnostic.severity === 1 &&
|
|
182
|
+
diagnostic.range.start.line === 3
|
|
183
|
+
),
|
|
184
|
+
true
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const changedDiagnostics = waitForDiagnostics();
|
|
188
|
+
sendNotification("textDocument/didChange", {
|
|
189
|
+
textDocument: {
|
|
190
|
+
uri: documentUri,
|
|
191
|
+
version: 2
|
|
192
|
+
},
|
|
193
|
+
contentChanges: [
|
|
194
|
+
{
|
|
195
|
+
text: fixedText
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const secondPublish = await changedDiagnostics;
|
|
201
|
+
assert.equal(secondPublish.uri, documentUri);
|
|
202
|
+
assert.deepEqual(secondPublish.diagnostics, []);
|
|
203
|
+
} finally {
|
|
204
|
+
child.kill();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -1,128 +1,128 @@
|
|
|
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
|
-
export async function runSemanticTokensTest(): Promise<void> {
|
|
45
|
-
const serverPath = path.resolve(__dirname, "../src/server.js");
|
|
46
|
-
const mainPath = path.resolve(
|
|
47
|
-
process.cwd(),
|
|
48
|
-
"test/fixtures/valid/merlin32-main-6502.
|
|
49
|
-
);
|
|
50
|
-
const mainUri = `file://${mainPath.replace(/\\/g, "/")}`;
|
|
51
|
-
const text = fs.readFileSync(mainPath, "utf8");
|
|
52
|
-
const child = spawn(process.execPath, [serverPath], {
|
|
53
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
let stdout = "";
|
|
57
|
-
let nextId = 1;
|
|
58
|
-
const pending = new Map<number, (message: JsonRpcMessage) => void>();
|
|
59
|
-
|
|
60
|
-
child.stdout.setEncoding("utf8");
|
|
61
|
-
child.stdout.on("data", (chunk: string) => {
|
|
62
|
-
stdout += chunk;
|
|
63
|
-
const decoded = decodeMessages(stdout);
|
|
64
|
-
stdout = decoded.rest;
|
|
65
|
-
|
|
66
|
-
for (const message of decoded.messages) {
|
|
67
|
-
if (message.id !== undefined) {
|
|
68
|
-
pending.get(message.id)?.(message);
|
|
69
|
-
pending.delete(message.id);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
function sendRequest(method: string, params: object): Promise<JsonRpcMessage> {
|
|
75
|
-
const id = nextId++;
|
|
76
|
-
child.stdin.write(
|
|
77
|
-
encodeMessage({
|
|
78
|
-
id,
|
|
79
|
-
jsonrpc: "2.0",
|
|
80
|
-
method,
|
|
81
|
-
params
|
|
82
|
-
})
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
return new Promise((resolve) => {
|
|
86
|
-
pending.set(id, resolve);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function sendNotification(method: string, params: object): void {
|
|
91
|
-
child.stdin.write(
|
|
92
|
-
encodeMessage({
|
|
93
|
-
jsonrpc: "2.0",
|
|
94
|
-
method,
|
|
95
|
-
params
|
|
96
|
-
})
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
await sendRequest("initialize", {
|
|
102
|
-
capabilities: {},
|
|
103
|
-
processId: process.pid,
|
|
104
|
-
rootUri: `file://${path.resolve(process.cwd()).replace(/\\/g, "/")}`
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
sendNotification("initialized", {});
|
|
108
|
-
sendNotification("textDocument/didOpen", {
|
|
109
|
-
textDocument: {
|
|
110
|
-
uri: mainUri,
|
|
111
|
-
languageId: "asm",
|
|
112
|
-
version: 1,
|
|
113
|
-
text
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const semanticTokensResponse = await sendRequest("textDocument/semanticTokens/full", {
|
|
118
|
-
textDocument: { uri: mainUri }
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const result = semanticTokensResponse.result as { data: number[] };
|
|
122
|
-
assert.ok(Array.isArray(result.data), "Expected data to be an array");
|
|
123
|
-
console.log(result.data); assert.ok(result.data.length > 0, "Expected non-empty semantic tokens array");
|
|
124
|
-
|
|
125
|
-
} finally {
|
|
126
|
-
child.kill();
|
|
127
|
-
}
|
|
128
|
-
}
|
|
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
|
+
export async function runSemanticTokensTest(): Promise<void> {
|
|
45
|
+
const serverPath = path.resolve(__dirname, "../src/server.js");
|
|
46
|
+
const mainPath = path.resolve(
|
|
47
|
+
process.cwd(),
|
|
48
|
+
"test/fixtures/valid/merlin32-main-6502.S"
|
|
49
|
+
);
|
|
50
|
+
const mainUri = `file://${mainPath.replace(/\\/g, "/")}`;
|
|
51
|
+
const text = fs.readFileSync(mainPath, "utf8");
|
|
52
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
53
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let stdout = "";
|
|
57
|
+
let nextId = 1;
|
|
58
|
+
const pending = new Map<number, (message: JsonRpcMessage) => void>();
|
|
59
|
+
|
|
60
|
+
child.stdout.setEncoding("utf8");
|
|
61
|
+
child.stdout.on("data", (chunk: string) => {
|
|
62
|
+
stdout += chunk;
|
|
63
|
+
const decoded = decodeMessages(stdout);
|
|
64
|
+
stdout = decoded.rest;
|
|
65
|
+
|
|
66
|
+
for (const message of decoded.messages) {
|
|
67
|
+
if (message.id !== undefined) {
|
|
68
|
+
pending.get(message.id)?.(message);
|
|
69
|
+
pending.delete(message.id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function sendRequest(method: string, params: object): Promise<JsonRpcMessage> {
|
|
75
|
+
const id = nextId++;
|
|
76
|
+
child.stdin.write(
|
|
77
|
+
encodeMessage({
|
|
78
|
+
id,
|
|
79
|
+
jsonrpc: "2.0",
|
|
80
|
+
method,
|
|
81
|
+
params
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
pending.set(id, resolve);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sendNotification(method: string, params: object): void {
|
|
91
|
+
child.stdin.write(
|
|
92
|
+
encodeMessage({
|
|
93
|
+
jsonrpc: "2.0",
|
|
94
|
+
method,
|
|
95
|
+
params
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await sendRequest("initialize", {
|
|
102
|
+
capabilities: {},
|
|
103
|
+
processId: process.pid,
|
|
104
|
+
rootUri: `file://${path.resolve(process.cwd()).replace(/\\/g, "/")}`
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
sendNotification("initialized", {});
|
|
108
|
+
sendNotification("textDocument/didOpen", {
|
|
109
|
+
textDocument: {
|
|
110
|
+
uri: mainUri,
|
|
111
|
+
languageId: "asm",
|
|
112
|
+
version: 1,
|
|
113
|
+
text
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const semanticTokensResponse = await sendRequest("textDocument/semanticTokens/full", {
|
|
118
|
+
textDocument: { uri: mainUri }
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const result = semanticTokensResponse.result as { data: number[] };
|
|
122
|
+
assert.ok(Array.isArray(result.data), "Expected data to be an array");
|
|
123
|
+
console.log(result.data); assert.ok(result.data.length > 0, "Expected non-empty semantic tokens array");
|
|
124
|
+
|
|
125
|
+
} finally {
|
|
126
|
+
child.kill();
|
|
127
|
+
}
|
|
128
|
+
}
|