@razdolbai/merls 1.1.1 → 1.2.1
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/core.md +6 -7
- package/.serena/memories/suggested_commands.md +4 -4
- package/AGENTS.md +1 -0
- package/README.md +19 -17
- package/dist/src/lsp/semantic-tokens.js +44 -0
- package/dist/src/server.js +12 -0
- package/dist/test/coc-config.test.js +2 -3
- package/dist/test/run-tests.js +5 -0
- package/dist/test/semantic-tokens.test.js +104 -0
- package/package.json +1 -1
- package/src/lsp/semantic-tokens.ts +52 -0
- package/src/server.ts +12 -0
- package/test/coc-config.test.ts +2 -3
- package/test/run-tests.ts +5 -0
- package/test/semantic-tokens.test.ts +128 -0
package/.serena/memories/core.md
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
- Completed MVP repo; current durable docs are \README.md\, \
|
|
2
|
-
- Purpose: standalone LSP server for Merlin-style 6502 assembly, primary editor target coc.nvim, stdio transport.
|
|
3
|
-
- Scope invariant: 6502-only. 65816 syntax is out of scope and should be diagnosed as unsupported.
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
- For
|
|
7
|
-
- For code/test/doc conventions, read \mem:conventions\.
|
|
1
|
+
- Completed MVP repo; current durable docs are \README.md\, \AGENTS.md\.
|
|
2
|
+
- Purpose: standalone LSP server for Merlin-style 6502 assembly, primary editor target coc.nvim, stdio transport. Published to npm as `@razdolbai/merls`.
|
|
3
|
+
- Scope invariant: 6502-only. 65816 syntax is out of scope and should be diagnosed as unsupported.
|
|
4
|
+
- Contributor rules and doc-maintenance requirement live in \AGENTS.md\.
|
|
5
|
+
- For stack and workspace layout, read \mem:tech_stack\.
|
|
6
|
+
- For code/test/doc conventions, read \mem:conventions\.
|
|
8
7
|
- For done criteria and required verification/docs updates, read \mem:task_completion\.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
- Windows shell is PowerShell; use PowerShell forms when documenting local commands.
|
|
2
|
-
- Current useful repo commands are read-only/doc-oriented: `Get-ChildItem`, `Get-Content README.md`, `Get-Content
|
|
3
|
-
-
|
|
4
|
-
-
|
|
1
|
+
- Windows shell is PowerShell; use PowerShell forms when documenting local commands.
|
|
2
|
+
- Current useful repo commands are read-only/doc-oriented: `Get-ChildItem`, `Get-Content README.md`, `Get-Content AGENTS.md`.
|
|
3
|
+
- Available commands: `npm install`, `npm run build`, `npm test`, `npm run dev` (run the TypeScript compiler in watch mode).
|
|
4
|
+
- A local publishing script is available at `.\publish.ps1`.
|
package/AGENTS.md
CHANGED
|
@@ -25,6 +25,7 @@ The current `workspace/symbol` provider is wired through `src/server.ts` and `sr
|
|
|
25
25
|
The current `textDocument/definition` and `textDocument/references` handlers are wired through `src/server.ts` and `src/lsp/symbol-navigation.ts`.
|
|
26
26
|
The current `textDocument/hover` handler is wired through `src/server.ts` and `src/lsp/hover.ts`.
|
|
27
27
|
The current `textDocument/completion` handler is wired through `src/server.ts` and `src/lsp/completion.ts`.
|
|
28
|
+
The current `textDocument/semanticTokens` handler is wired through `src/server.ts` and `src/lsp/semantic-tokens.ts`.
|
|
28
29
|
The current `textDocument/publishDiagnostics` path is wired through `src/server.ts` and `src/lsp/diagnostics.ts`, with full-document sync on open/change so editor clients receive live parser and resolver diagnostics.
|
|
29
30
|
The current packaged CLI entrypoint lives under `src/cli.ts` and is covered by a compiled stdio launch-contract integration test.
|
|
30
31
|
The coc.nvim smoke test now lives under `test/smoke/` with a headless Vim runner (`run-smoke.ps1`) and a minimal vimrc that isolates the test from the user's real Vim configuration.
|
package/README.md
CHANGED
|
@@ -15,25 +15,27 @@ The MVP LSP feature set for Merlin-style 6502 assembly is complete and published
|
|
|
15
15
|
|
|
16
16
|
## Scope
|
|
17
17
|
|
|
18
|
-
### In scope
|
|
19
|
-
|
|
20
|
-
- 6502-only Merlin-style assembly
|
|
21
|
-
- parser-based diagnostics for the MVP
|
|
22
|
-
- core LSP features such as:
|
|
23
|
-
- diagnostics
|
|
24
|
-
- hover
|
|
25
|
-
- completion
|
|
26
|
-
- go to definition
|
|
27
|
-
- find references
|
|
28
|
-
- document symbols
|
|
29
|
-
- workspace symbols
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
18
|
+
### In scope
|
|
19
|
+
|
|
20
|
+
- 6502-only Merlin-style assembly
|
|
21
|
+
- parser-based diagnostics for the MVP
|
|
22
|
+
- core LSP features such as:
|
|
23
|
+
- diagnostics
|
|
24
|
+
- hover
|
|
25
|
+
- completion
|
|
26
|
+
- go to definition
|
|
27
|
+
- find references
|
|
28
|
+
- document symbols
|
|
29
|
+
- workspace symbols
|
|
30
|
+
- semantic tokens (syntax highlighting)
|
|
31
|
+
|
|
32
|
+
### Out of scope
|
|
33
|
+
|
|
34
|
+
- 65816 support
|
|
35
|
+
- assembler-backed diagnostics in the first implementation pass
|
|
35
36
|
- coc.nvim-specific plugin code for the MVP
|
|
36
37
|
|
|
38
|
+
|
|
37
39
|
## Implementation
|
|
38
40
|
|
|
39
41
|
- **runtime:** Node.js
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.semanticTokensLegend = void 0;
|
|
4
|
+
exports.buildSemanticTokens = buildSemanticTokens;
|
|
5
|
+
const vscode_languageserver_1 = require("vscode-languageserver");
|
|
6
|
+
const lexer_1 = require("../asm/lexer");
|
|
7
|
+
const tokenTypesList = [
|
|
8
|
+
vscode_languageserver_1.SemanticTokenTypes.comment,
|
|
9
|
+
vscode_languageserver_1.SemanticTokenTypes.function,
|
|
10
|
+
vscode_languageserver_1.SemanticTokenTypes.macro,
|
|
11
|
+
vscode_languageserver_1.SemanticTokenTypes.keyword,
|
|
12
|
+
vscode_languageserver_1.SemanticTokenTypes.string,
|
|
13
|
+
vscode_languageserver_1.SemanticTokenTypes.number,
|
|
14
|
+
vscode_languageserver_1.SemanticTokenTypes.operator,
|
|
15
|
+
vscode_languageserver_1.SemanticTokenTypes.variable
|
|
16
|
+
];
|
|
17
|
+
exports.semanticTokensLegend = {
|
|
18
|
+
tokenTypes: tokenTypesList,
|
|
19
|
+
tokenModifiers: []
|
|
20
|
+
};
|
|
21
|
+
const tokenTypeMap = {
|
|
22
|
+
comment: tokenTypesList.indexOf(vscode_languageserver_1.SemanticTokenTypes.comment),
|
|
23
|
+
label: tokenTypesList.indexOf(vscode_languageserver_1.SemanticTokenTypes.function),
|
|
24
|
+
localLabel: tokenTypesList.indexOf(vscode_languageserver_1.SemanticTokenTypes.function),
|
|
25
|
+
directive: tokenTypesList.indexOf(vscode_languageserver_1.SemanticTokenTypes.macro),
|
|
26
|
+
mnemonic: tokenTypesList.indexOf(vscode_languageserver_1.SemanticTokenTypes.keyword),
|
|
27
|
+
string: tokenTypesList.indexOf(vscode_languageserver_1.SemanticTokenTypes.string),
|
|
28
|
+
numericLiteral: tokenTypesList.indexOf(vscode_languageserver_1.SemanticTokenTypes.number),
|
|
29
|
+
modifier: tokenTypesList.indexOf(vscode_languageserver_1.SemanticTokenTypes.operator),
|
|
30
|
+
expressionOperator: tokenTypesList.indexOf(vscode_languageserver_1.SemanticTokenTypes.operator),
|
|
31
|
+
identifier: tokenTypesList.indexOf(vscode_languageserver_1.SemanticTokenTypes.variable)
|
|
32
|
+
};
|
|
33
|
+
function buildSemanticTokens(source) {
|
|
34
|
+
const lexedSource = (0, lexer_1.lexSource)(source);
|
|
35
|
+
const builder = new vscode_languageserver_1.SemanticTokensBuilder();
|
|
36
|
+
for (const line of lexedSource.lines) {
|
|
37
|
+
for (const token of line.tokens) {
|
|
38
|
+
const typeIndex = tokenTypeMap[token.kind];
|
|
39
|
+
const length = token.end - token.start;
|
|
40
|
+
builder.push(line.line, token.start, length, typeIndex, 0);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return builder.build();
|
|
44
|
+
}
|
package/dist/src/server.js
CHANGED
|
@@ -9,6 +9,7 @@ const diagnostics_1 = require("./lsp/diagnostics");
|
|
|
9
9
|
const hover_1 = require("./lsp/hover");
|
|
10
10
|
const symbol_navigation_1 = require("./lsp/symbol-navigation");
|
|
11
11
|
const workspace_symbols_1 = require("./lsp/workspace-symbols");
|
|
12
|
+
const semantic_tokens_1 = require("./lsp/semantic-tokens");
|
|
12
13
|
function createServerConnection(inputStream = process.stdin, outputStream = process.stdout) {
|
|
13
14
|
return (0, node_1.createConnection)(node_1.ProposedFeatures.all, inputStream, outputStream);
|
|
14
15
|
}
|
|
@@ -23,6 +24,10 @@ function startServer(inputStream = process.stdin, outputStream = process.stdout)
|
|
|
23
24
|
hoverProvider: true,
|
|
24
25
|
referencesProvider: true,
|
|
25
26
|
workspaceSymbolProvider: true,
|
|
27
|
+
semanticTokensProvider: {
|
|
28
|
+
legend: semantic_tokens_1.semanticTokensLegend,
|
|
29
|
+
full: true
|
|
30
|
+
},
|
|
26
31
|
textDocumentSync: {
|
|
27
32
|
openClose: true,
|
|
28
33
|
change: node_1.TextDocumentSyncKind.Full
|
|
@@ -61,6 +66,13 @@ function startServer(inputStream = process.stdin, outputStream = process.stdout)
|
|
|
61
66
|
connection.onReferences((params) => (0, symbol_navigation_1.findReferences)(openDocuments, params.textDocument.uri, params.position.line, params.context.includeDeclaration));
|
|
62
67
|
connection.onHover((params) => (0, hover_1.buildHover)(openDocuments, params.textDocument.uri, params.position.line, params.position.character));
|
|
63
68
|
connection.onCompletion((params) => (0, completion_1.buildCompletionItems)(openDocuments, params.textDocument.uri, params.position.line));
|
|
69
|
+
connection.languages.semanticTokens.on((params) => {
|
|
70
|
+
const source = openDocuments.get(params.textDocument.uri);
|
|
71
|
+
if (source === undefined) {
|
|
72
|
+
return { data: [] };
|
|
73
|
+
}
|
|
74
|
+
return (0, semantic_tokens_1.buildSemanticTokens)(source);
|
|
75
|
+
});
|
|
64
76
|
function publishDiagnostics() {
|
|
65
77
|
for (const [uri, diagnostics] of (0, diagnostics_1.collectDiagnosticsByUri)(openDocuments).entries()) {
|
|
66
78
|
connection.sendDiagnostics({
|
|
@@ -11,11 +11,10 @@ function runCocConfigTest() {
|
|
|
11
11
|
const settingsPath = node_path_1.default.resolve(process.cwd(), "examples/coc-settings.json");
|
|
12
12
|
const settings = JSON.parse(node_fs_1.default.readFileSync(settingsPath, "utf8"));
|
|
13
13
|
const serverConfig = settings.languageserver?.merls;
|
|
14
|
-
strict_1.default.equal(serverConfig?.command, "
|
|
14
|
+
strict_1.default.equal(serverConfig?.command, "merls");
|
|
15
15
|
strict_1.default.deepEqual(serverConfig?.args, [
|
|
16
|
-
"C:/Users/alexe/Projects/merls/dist/src/cli.js",
|
|
17
16
|
"--stdio"
|
|
18
17
|
]);
|
|
19
18
|
strict_1.default.deepEqual(serverConfig?.filetypes, ["asm"]);
|
|
20
|
-
strict_1.default.deepEqual(serverConfig?.rootPatterns, [".git"
|
|
19
|
+
strict_1.default.deepEqual(serverConfig?.rootPatterns, [".git"]);
|
|
21
20
|
}
|
package/dist/test/run-tests.js
CHANGED
|
@@ -22,6 +22,7 @@ const symbols_test_1 = require("./symbols.test");
|
|
|
22
22
|
const syntax_shape_test_1 = require("./syntax-shape.test");
|
|
23
23
|
const workspace_test_1 = require("./workspace.test");
|
|
24
24
|
const workspace_symbol_test_1 = require("./workspace-symbol.test");
|
|
25
|
+
const semantic_tokens_test_1 = require("./semantic-tokens.test");
|
|
25
26
|
const tests = [
|
|
26
27
|
{
|
|
27
28
|
name: "workspace bootstrap exposes the project name",
|
|
@@ -110,6 +111,10 @@ const tests = [
|
|
|
110
111
|
{
|
|
111
112
|
name: "server publishes and clears diagnostics for open Merlin documents",
|
|
112
113
|
run: publish_diagnostics_test_1.runPublishDiagnosticsTest
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "server returns semantic tokens for highlighting",
|
|
117
|
+
run: semantic_tokens_test_1.runSemanticTokensTest
|
|
113
118
|
}
|
|
114
119
|
];
|
|
115
120
|
async function main() {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runSemanticTokensTest = runSemanticTokensTest;
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
function encodeMessage(message) {
|
|
12
|
+
const body = JSON.stringify(message);
|
|
13
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
14
|
+
}
|
|
15
|
+
function decodeMessages(streamBuffer) {
|
|
16
|
+
const messages = [];
|
|
17
|
+
let buffer = streamBuffer;
|
|
18
|
+
for (;;) {
|
|
19
|
+
const separator = buffer.indexOf("\r\n\r\n");
|
|
20
|
+
if (separator === -1) {
|
|
21
|
+
return { messages, rest: buffer };
|
|
22
|
+
}
|
|
23
|
+
const header = buffer.slice(0, separator);
|
|
24
|
+
const match = /Content-Length: (\d+)/i.exec(header);
|
|
25
|
+
if (!match) {
|
|
26
|
+
throw new Error(`Missing Content-Length header: ${header}`);
|
|
27
|
+
}
|
|
28
|
+
const length = Number(match[1]);
|
|
29
|
+
const body = buffer.slice(separator + 4);
|
|
30
|
+
if (Buffer.byteLength(body, "utf8") < length) {
|
|
31
|
+
return { messages, rest: buffer };
|
|
32
|
+
}
|
|
33
|
+
messages.push(JSON.parse(body.slice(0, length)));
|
|
34
|
+
buffer = body.slice(length);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function runSemanticTokensTest() {
|
|
38
|
+
const serverPath = node_path_1.default.resolve(__dirname, "../src/server.js");
|
|
39
|
+
const mainPath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-main-6502.asm");
|
|
40
|
+
const mainUri = `file://${mainPath.replace(/\\/g, "/")}`;
|
|
41
|
+
const text = node_fs_1.default.readFileSync(mainPath, "utf8");
|
|
42
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [serverPath], {
|
|
43
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
44
|
+
});
|
|
45
|
+
let stdout = "";
|
|
46
|
+
let nextId = 1;
|
|
47
|
+
const pending = new Map();
|
|
48
|
+
child.stdout.setEncoding("utf8");
|
|
49
|
+
child.stdout.on("data", (chunk) => {
|
|
50
|
+
stdout += chunk;
|
|
51
|
+
const decoded = decodeMessages(stdout);
|
|
52
|
+
stdout = decoded.rest;
|
|
53
|
+
for (const message of decoded.messages) {
|
|
54
|
+
if (message.id !== undefined) {
|
|
55
|
+
pending.get(message.id)?.(message);
|
|
56
|
+
pending.delete(message.id);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
function sendRequest(method, params) {
|
|
61
|
+
const id = nextId++;
|
|
62
|
+
child.stdin.write(encodeMessage({
|
|
63
|
+
id,
|
|
64
|
+
jsonrpc: "2.0",
|
|
65
|
+
method,
|
|
66
|
+
params
|
|
67
|
+
}));
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
pending.set(id, resolve);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function sendNotification(method, params) {
|
|
73
|
+
child.stdin.write(encodeMessage({
|
|
74
|
+
jsonrpc: "2.0",
|
|
75
|
+
method,
|
|
76
|
+
params
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await sendRequest("initialize", {
|
|
81
|
+
capabilities: {},
|
|
82
|
+
processId: process.pid,
|
|
83
|
+
rootUri: `file://${node_path_1.default.resolve(process.cwd()).replace(/\\/g, "/")}`
|
|
84
|
+
});
|
|
85
|
+
sendNotification("initialized", {});
|
|
86
|
+
sendNotification("textDocument/didOpen", {
|
|
87
|
+
textDocument: {
|
|
88
|
+
uri: mainUri,
|
|
89
|
+
languageId: "asm",
|
|
90
|
+
version: 1,
|
|
91
|
+
text
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const semanticTokensResponse = await sendRequest("textDocument/semanticTokens/full", {
|
|
95
|
+
textDocument: { uri: mainUri }
|
|
96
|
+
});
|
|
97
|
+
const result = semanticTokensResponse.result;
|
|
98
|
+
strict_1.default.ok(Array.isArray(result.data), "Expected data to be an array");
|
|
99
|
+
strict_1.default.ok(result.data.length > 0, "Expected non-empty semantic tokens array");
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
child.kill();
|
|
103
|
+
}
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SemanticTokens, SemanticTokensBuilder, SemanticTokensLegend, SemanticTokenTypes } from "vscode-languageserver";
|
|
2
|
+
import { lexSource, type TokenKind } from "../asm/lexer";
|
|
3
|
+
|
|
4
|
+
const tokenTypesList = [
|
|
5
|
+
SemanticTokenTypes.comment,
|
|
6
|
+
SemanticTokenTypes.function,
|
|
7
|
+
SemanticTokenTypes.macro,
|
|
8
|
+
SemanticTokenTypes.keyword,
|
|
9
|
+
SemanticTokenTypes.string,
|
|
10
|
+
SemanticTokenTypes.number,
|
|
11
|
+
SemanticTokenTypes.operator,
|
|
12
|
+
SemanticTokenTypes.variable
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export const semanticTokensLegend: SemanticTokensLegend = {
|
|
16
|
+
tokenTypes: tokenTypesList,
|
|
17
|
+
tokenModifiers: []
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const tokenTypeMap: Record<TokenKind, number> = {
|
|
21
|
+
comment: tokenTypesList.indexOf(SemanticTokenTypes.comment),
|
|
22
|
+
label: tokenTypesList.indexOf(SemanticTokenTypes.function),
|
|
23
|
+
localLabel: tokenTypesList.indexOf(SemanticTokenTypes.function),
|
|
24
|
+
directive: tokenTypesList.indexOf(SemanticTokenTypes.macro),
|
|
25
|
+
mnemonic: tokenTypesList.indexOf(SemanticTokenTypes.keyword),
|
|
26
|
+
string: tokenTypesList.indexOf(SemanticTokenTypes.string),
|
|
27
|
+
numericLiteral: tokenTypesList.indexOf(SemanticTokenTypes.number),
|
|
28
|
+
modifier: tokenTypesList.indexOf(SemanticTokenTypes.operator),
|
|
29
|
+
expressionOperator: tokenTypesList.indexOf(SemanticTokenTypes.operator),
|
|
30
|
+
identifier: tokenTypesList.indexOf(SemanticTokenTypes.variable)
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function buildSemanticTokens(source: string): SemanticTokens {
|
|
34
|
+
const lexedSource = lexSource(source);
|
|
35
|
+
const builder = new SemanticTokensBuilder();
|
|
36
|
+
|
|
37
|
+
for (const line of lexedSource.lines) {
|
|
38
|
+
for (const token of line.tokens) {
|
|
39
|
+
const typeIndex = tokenTypeMap[token.kind];
|
|
40
|
+
const length = token.end - token.start;
|
|
41
|
+
builder.push(
|
|
42
|
+
line.line,
|
|
43
|
+
token.start,
|
|
44
|
+
length,
|
|
45
|
+
typeIndex,
|
|
46
|
+
0
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return builder.build();
|
|
52
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { collectDiagnosticsByUri } from "./lsp/diagnostics";
|
|
|
11
11
|
import { buildHover } from "./lsp/hover";
|
|
12
12
|
import { findDefinition, findReferences } from "./lsp/symbol-navigation";
|
|
13
13
|
import { buildWorkspaceSymbols } from "./lsp/workspace-symbols";
|
|
14
|
+
import { buildSemanticTokens, semanticTokensLegend } from "./lsp/semantic-tokens";
|
|
14
15
|
|
|
15
16
|
export function createServerConnection(
|
|
16
17
|
inputStream: NodeJS.ReadableStream = process.stdin,
|
|
@@ -34,6 +35,10 @@ export function startServer(
|
|
|
34
35
|
hoverProvider: true,
|
|
35
36
|
referencesProvider: true,
|
|
36
37
|
workspaceSymbolProvider: true,
|
|
38
|
+
semanticTokensProvider: {
|
|
39
|
+
legend: semanticTokensLegend,
|
|
40
|
+
full: true
|
|
41
|
+
},
|
|
37
42
|
textDocumentSync: {
|
|
38
43
|
openClose: true,
|
|
39
44
|
change: TextDocumentSyncKind.Full
|
|
@@ -102,6 +107,13 @@ export function startServer(
|
|
|
102
107
|
params.position.line
|
|
103
108
|
)
|
|
104
109
|
);
|
|
110
|
+
connection.languages.semanticTokens.on((params) => {
|
|
111
|
+
const source = openDocuments.get(params.textDocument.uri);
|
|
112
|
+
if (source === undefined) {
|
|
113
|
+
return { data: [] };
|
|
114
|
+
}
|
|
115
|
+
return buildSemanticTokens(source);
|
|
116
|
+
});
|
|
105
117
|
|
|
106
118
|
function publishDiagnostics(): void {
|
|
107
119
|
for (const [uri, diagnostics] of collectDiagnosticsByUri(openDocuments).entries()) {
|
package/test/coc-config.test.ts
CHANGED
|
@@ -18,11 +18,10 @@ export function runCocConfigTest(): void {
|
|
|
18
18
|
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")) as CocSettings;
|
|
19
19
|
const serverConfig = settings.languageserver?.merls;
|
|
20
20
|
|
|
21
|
-
assert.equal(serverConfig?.command, "
|
|
21
|
+
assert.equal(serverConfig?.command, "merls");
|
|
22
22
|
assert.deepEqual(serverConfig?.args, [
|
|
23
|
-
"C:/Users/alexe/Projects/merls/dist/src/cli.js",
|
|
24
23
|
"--stdio"
|
|
25
24
|
]);
|
|
26
25
|
assert.deepEqual(serverConfig?.filetypes, ["asm"]);
|
|
27
|
-
assert.deepEqual(serverConfig?.rootPatterns, [".git"
|
|
26
|
+
assert.deepEqual(serverConfig?.rootPatterns, [".git"]);
|
|
28
27
|
}
|
package/test/run-tests.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { runSymbolsTest } from "./symbols.test";
|
|
|
20
20
|
import { runSyntaxShapeTest } from "./syntax-shape.test";
|
|
21
21
|
import { runWorkspaceGraphTest } from "./workspace.test";
|
|
22
22
|
import { runWorkspaceSymbolTest } from "./workspace-symbol.test";
|
|
23
|
+
import { runSemanticTokensTest } from "./semantic-tokens.test";
|
|
23
24
|
|
|
24
25
|
type TestCase = {
|
|
25
26
|
name: string;
|
|
@@ -114,6 +115,10 @@ const tests: TestCase[] = [
|
|
|
114
115
|
{
|
|
115
116
|
name: "server publishes and clears diagnostics for open Merlin documents",
|
|
116
117
|
run: runPublishDiagnosticsTest
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "server returns semantic tokens for highlighting",
|
|
121
|
+
run: runSemanticTokensTest
|
|
117
122
|
}
|
|
118
123
|
];
|
|
119
124
|
|
|
@@ -0,0 +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
|
+
}
|