@razdolbai/merls 1.2.1 → 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.
Files changed (36) hide show
  1. package/AGENTS.md +64 -64
  2. package/dist/src/lsp/completion.js +14 -11
  3. package/dist/test/completion.test.js +1 -1
  4. package/dist/test/definition-references.test.js +1 -1
  5. package/dist/test/diagnostics.test.js +2 -2
  6. package/dist/test/document-symbol.test.js +1 -1
  7. package/dist/test/fixture-corpus.test.js +4 -4
  8. package/dist/test/hover.test.js +1 -1
  9. package/dist/test/lexer.test.js +4 -4
  10. package/dist/test/local-labels.test.js +1 -1
  11. package/dist/test/publish-diagnostics.test.js +1 -1
  12. package/dist/test/semantic-tokens.test.js +2 -1
  13. package/dist/test/symbols.test.js +1 -1
  14. package/dist/test/workspace-symbol.test.js +2 -2
  15. package/dist/test/workspace.test.js +2 -2
  16. package/package.json +1 -1
  17. package/src/lsp/completion.ts +15 -11
  18. package/test/completion.test.ts +151 -151
  19. package/test/definition-references.test.ts +152 -152
  20. package/test/diagnostics.test.ts +129 -129
  21. package/test/document-symbol.test.ts +131 -131
  22. package/test/fixture-corpus.test.ts +33 -33
  23. package/test/fixtures/valid/{merlin32-linkscript.asm → merlin32-linkscript.S} +16 -16
  24. package/test/hover.test.ts +175 -175
  25. package/test/lexer.test.ts +87 -87
  26. package/test/local-labels.test.ts +47 -47
  27. package/test/publish-diagnostics.test.ts +206 -206
  28. package/test/semantic-tokens.test.ts +128 -128
  29. package/test/smoke/run-smoke.ps1 +177 -177
  30. package/test/symbols.test.ts +41 -41
  31. package/test/workspace-symbol.test.ts +139 -139
  32. package/test/workspace.test.ts +29 -29
  33. /package/test/fixtures/invalid/{65816-bank-ops.asm → 65816-bank-ops.S} +0 -0
  34. /package/test/fixtures/invalid/{65816-long-addressing.asm → 65816-long-addressing.S} +0 -0
  35. /package/test/fixtures/valid/{merlin32-main-6502.asm → merlin32-main-6502.S} +0 -0
  36. /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.asm"
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.asm"
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
- 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
+ }