@razdolbai/merls 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.serena/memories/conventions.md +6 -0
  2. package/.serena/memories/core.md +8 -0
  3. package/.serena/memories/memory_maintenance.md +33 -0
  4. package/.serena/memories/suggested_commands.md +4 -0
  5. package/.serena/memories/task_completion.md +7 -0
  6. package/.serena/memories/tech_stack.md +6 -0
  7. package/.serena/project.yml +132 -0
  8. package/AGENTS.md +63 -0
  9. package/README.md +137 -0
  10. package/dist/src/asm/diagnostics.js +202 -0
  11. package/dist/src/asm/document.js +26 -0
  12. package/dist/src/asm/expression.js +163 -0
  13. package/dist/src/asm/lexer.js +122 -0
  14. package/dist/src/asm/local-labels.js +140 -0
  15. package/dist/src/asm/metadata.js +101 -0
  16. package/dist/src/asm/parser.js +118 -0
  17. package/dist/src/asm/symbols.js +40 -0
  18. package/dist/src/asm/syntax.js +44 -0
  19. package/dist/src/asm/workspace.js +73 -0
  20. package/dist/src/cli.js +21 -0
  21. package/dist/src/index.js +4 -0
  22. package/dist/src/lsp/completion.js +32 -0
  23. package/dist/src/lsp/diagnostics.js +63 -0
  24. package/dist/src/lsp/document-symbols.js +80 -0
  25. package/dist/src/lsp/hover.js +75 -0
  26. package/dist/src/lsp/symbol-navigation.js +181 -0
  27. package/dist/src/lsp/workspace-symbols.js +17 -0
  28. package/dist/src/server.js +77 -0
  29. package/dist/test/bootstrap.test.js +11 -0
  30. package/dist/test/cli-contract.test.js +74 -0
  31. package/dist/test/coc-config.test.js +21 -0
  32. package/dist/test/completion.test.js +126 -0
  33. package/dist/test/definition-references.test.js +126 -0
  34. package/dist/test/diagnostics.test.js +66 -0
  35. package/dist/test/document-model.test.js +30 -0
  36. package/dist/test/document-symbol.test.js +107 -0
  37. package/dist/test/expression.test.js +100 -0
  38. package/dist/test/fixture-corpus.test.js +33 -0
  39. package/dist/test/hover.test.js +142 -0
  40. package/dist/test/lexer.test.js +53 -0
  41. package/dist/test/line-parser.test.js +67 -0
  42. package/dist/test/local-labels.test.js +43 -0
  43. package/dist/test/metadata.test.js +27 -0
  44. package/dist/test/publish-diagnostics.test.js +137 -0
  45. package/dist/test/run-tests.js +132 -0
  46. package/dist/test/server-entrypoint.test.js +14 -0
  47. package/dist/test/server-initialize.test.js +77 -0
  48. package/dist/test/symbols.test.js +37 -0
  49. package/dist/test/syntax-shape.test.js +18 -0
  50. package/dist/test/workspace-symbol.test.js +113 -0
  51. package/dist/test/workspace.test.js +24 -0
  52. package/examples/coc-settings.json +18 -0
  53. package/package.json +26 -0
  54. package/publish.ps1 +9 -0
  55. package/src/asm/diagnostics.ts +294 -0
  56. package/src/asm/document.ts +43 -0
  57. package/src/asm/expression.ts +242 -0
  58. package/src/asm/lexer.ts +197 -0
  59. package/src/asm/local-labels.ts +204 -0
  60. package/src/asm/metadata.ts +150 -0
  61. package/src/asm/parser.ts +197 -0
  62. package/src/asm/symbols.ts +55 -0
  63. package/src/asm/syntax.ts +76 -0
  64. package/src/asm/workspace.ts +105 -0
  65. package/src/cli.ts +24 -0
  66. package/src/index.ts +1 -0
  67. package/src/lsp/completion.ts +42 -0
  68. package/src/lsp/diagnostics.ts +82 -0
  69. package/src/lsp/document-symbols.ts +111 -0
  70. package/src/lsp/hover.ts +90 -0
  71. package/src/lsp/symbol-navigation.ts +244 -0
  72. package/src/lsp/workspace-symbols.ts +24 -0
  73. package/src/server.ts +121 -0
  74. package/test/bootstrap.test.ts +7 -0
  75. package/test/cli-contract.test.ts +94 -0
  76. package/test/coc-config.test.ts +28 -0
  77. package/test/completion.test.ts +151 -0
  78. package/test/definition-references.test.ts +152 -0
  79. package/test/diagnostics.test.ts +129 -0
  80. package/test/document-model.test.ts +29 -0
  81. package/test/document-symbol.test.ts +131 -0
  82. package/test/expression.test.ts +111 -0
  83. package/test/fixture-corpus.test.ts +33 -0
  84. package/test/fixtures/invalid/65816-bank-ops.asm +17 -0
  85. package/test/fixtures/invalid/65816-long-addressing.asm +26 -0
  86. package/test/fixtures/valid/merlin32-linkscript.asm +16 -0
  87. package/test/fixtures/valid/merlin32-main-6502.asm +103 -0
  88. package/test/fixtures/valid/smoke-test.asm +7 -0
  89. package/test/hover.test.ts +175 -0
  90. package/test/lexer.test.ts +87 -0
  91. package/test/line-parser.test.ts +69 -0
  92. package/test/local-labels.test.ts +47 -0
  93. package/test/metadata.test.ts +27 -0
  94. package/test/publish-diagnostics.test.ts +206 -0
  95. package/test/run-tests.ts +139 -0
  96. package/test/server-entrypoint.test.ts +11 -0
  97. package/test/server-initialize.test.ts +101 -0
  98. package/test/smoke/run-smoke.ps1 +177 -0
  99. package/test/smoke/vimrc +17 -0
  100. package/test/symbols.test.ts +41 -0
  101. package/test/syntax-shape.test.ts +18 -0
  102. package/test/workspace-symbol.test.ts +139 -0
  103. package/test/workspace.test.ts +29 -0
  104. package/tsconfig.json +16 -0
@@ -0,0 +1,177 @@
1
+ # coc.nvim smoke test for merls language server.
2
+ #
3
+ # Launches Vim in headless mode with a minimal coc.nvim configuration,
4
+ # opens an .asm fixture file, waits for the language server to start and
5
+ # publish diagnostics, then exits with 0 on success / 1 on failure.
6
+ #
7
+ # Usage: pwsh test/smoke/run-smoke.ps1
8
+
9
+ $ErrorActionPreference = "Stop"
10
+
11
+ $projectRoot = (Resolve-Path "$PSScriptRoot/../..").Path
12
+ $smokeDir = "$projectRoot/test/smoke"
13
+ $fixture = "$projectRoot/test/fixtures/valid/smoke-test.asm"
14
+ $cliPath = "$projectRoot/dist/src/cli.js"
15
+
16
+ # Ensure the project is built
17
+ if (-not (Test-Path $cliPath)) {
18
+ Write-Host "Building project..."
19
+ Push-Location $projectRoot
20
+ npm run build
21
+ Pop-Location
22
+ }
23
+
24
+ # Create an isolated temporary directory for coc.nvim config/data
25
+ $tmpBase = "$projectRoot/test/smoke/tmp"
26
+ $configDir = "$tmpBase/config"
27
+ $dataDir = "$tmpBase/data"
28
+
29
+ if (Test-Path $tmpBase) { Remove-Item -Recurse -Force $tmpBase }
30
+ New-Item -ItemType Directory -Force -Path $configDir | Out-Null
31
+ New-Item -ItemType Directory -Force -Path $dataDir | Out-Null
32
+
33
+ # Write the coc-settings.json into the temporary config directory
34
+ $cocSettings = @{
35
+ languageserver = @{
36
+ merls = @{
37
+ command = "node"
38
+ args = @($cliPath.Replace("\", "/"), "--stdio")
39
+ rootPatterns = @(".git", "package.json")
40
+ filetypes = @("asm")
41
+ }
42
+ }
43
+ } | ConvertTo-Json -Depth 5
44
+
45
+ Set-Content -Path "$configDir/coc-settings.json" -Value $cocSettings
46
+
47
+ # Write a tiny Vim script that will:
48
+ # 1. Open the fixture file
49
+ # 2. Wait for coc.nvim to attach the language server
50
+ # 3. Check CocAction('diagnosticList') for expected diagnostics
51
+ # 4. Write results to a file and quit
52
+ $checkScript = @"
53
+ " Wait for coc.nvim to be ready, then run checks.
54
+ function! s:RunChecks(timer)
55
+ " Check if coc.nvim is running
56
+ try
57
+ let l:services = CocAction('services')
58
+ catch
59
+ " coc.nvim not ready yet, retry
60
+ call timer_start(1000, function('s:RunChecks'))
61
+ return
62
+ endtry
63
+
64
+ let l:result_file = '$($tmpBase.Replace("\", "/"))/result.txt'
65
+ let l:lines = []
66
+
67
+ " Check that the merls service is running
68
+ let l:found = 0
69
+ for l:svc in l:services
70
+ if l:svc.id =~# 'merls'
71
+ let l:found = 1
72
+ let l:state = l:svc.state
73
+ call add(l:lines, 'SERVICE: merls ' . l:state)
74
+ endif
75
+ endfor
76
+
77
+ if !l:found
78
+ call add(l:lines, 'SERVICE: merls NOT FOUND')
79
+ call writefile(l:lines, l:result_file)
80
+ qall!
81
+ return
82
+ endif
83
+
84
+ " Wait a moment for diagnostics to arrive, then collect them
85
+ call timer_start(2000, {-> s:CollectDiagnostics(l:lines, l:result_file)})
86
+ endfunction
87
+
88
+ function! s:CollectDiagnostics(lines, result_file)
89
+ try
90
+ let l:diags = CocAction('diagnosticList')
91
+ call add(a:lines, 'DIAGNOSTICS_COUNT: ' . len(l:diags))
92
+ for l:d in l:diags
93
+ call add(a:lines, 'DIAG: ' . l:d.severity . ' | ' . l:d.message)
94
+ endfor
95
+ catch
96
+ call add(a:lines, 'DIAGNOSTICS_ERROR: ' . v:exception)
97
+ endtry
98
+
99
+ call writefile(a:lines, a:result_file)
100
+ qall!
101
+ endfunction
102
+
103
+ " Start the check timer after a 3 second delay to let coc.nvim initialize
104
+ call timer_start(3000, function('s:RunChecks'))
105
+ "@
106
+
107
+ Set-Content -Path "$smokeDir/check.vim" -Value $checkScript
108
+
109
+ # Run Vim in headless mode
110
+ Write-Host "Starting Vim headless smoke test..."
111
+ $env:MERLS_SMOKE_CONFIG = $configDir
112
+ $env:MERLS_SMOKE_DATA = $dataDir
113
+
114
+ $vimArgs = @(
115
+ "-u", "$smokeDir/vimrc",
116
+ "-N", # nocompatible
117
+ "--not-a-term", # headless
118
+ "-S", "$smokeDir/check.vim",
119
+ $fixture
120
+ )
121
+
122
+ $vimProcess = Start-Process -FilePath "vim" -ArgumentList $vimArgs `
123
+ -PassThru -NoNewWindow -Wait -ErrorAction Stop
124
+
125
+ # Read results
126
+ $resultFile = "$tmpBase/result.txt"
127
+ if (-not (Test-Path $resultFile)) {
128
+ Write-Host "FAIL: Vim exited without writing results."
129
+ exit 1
130
+ }
131
+
132
+ $results = Get-Content $resultFile
133
+ Write-Host ""
134
+ Write-Host "=== Smoke test results ==="
135
+ $results | ForEach-Object { Write-Host " $_" }
136
+ Write-Host ""
137
+
138
+ # Validate
139
+ $passed = $true
140
+
141
+ # Check service was found and running
142
+ $serviceLine = $results | Where-Object { $_ -match "^SERVICE:" }
143
+ if ($serviceLine -match "running") {
144
+ Write-Host "PASS: merls language server is running in coc.nvim"
145
+ } else {
146
+ Write-Host "FAIL: merls language server not running. Got: $serviceLine"
147
+ $passed = $false
148
+ }
149
+
150
+ # Check diagnostics were received
151
+ $diagCountLine = $results | Where-Object { $_ -match "^DIAGNOSTICS_COUNT:" }
152
+ if ($diagCountLine) {
153
+ $count = [int]($diagCountLine -replace "DIAGNOSTICS_COUNT:\s*", "")
154
+ if ($count -gt 0) {
155
+ Write-Host "PASS: received $count diagnostic(s) from merls"
156
+ } else {
157
+ Write-Host "FAIL: expected at least 1 diagnostic, got 0"
158
+ $passed = $false
159
+ }
160
+ } else {
161
+ Write-Host "FAIL: no diagnostic count in results"
162
+ $passed = $false
163
+ }
164
+
165
+ # Cleanup
166
+ Remove-Item -Recurse -Force $tmpBase -ErrorAction SilentlyContinue
167
+ Remove-Item -Force "$smokeDir/check.vim" -ErrorAction SilentlyContinue
168
+
169
+ if ($passed) {
170
+ Write-Host ""
171
+ Write-Host "Smoke test PASSED."
172
+ exit 0
173
+ } else {
174
+ Write-Host ""
175
+ Write-Host "Smoke test FAILED."
176
+ exit 1
177
+ }
@@ -0,0 +1,17 @@
1
+ " Minimal vimrc for the coc.nvim smoke test.
2
+ " Loads only coc.nvim from the user's vim-plug directory,
3
+ " configures the merls language server, and runs assertions.
4
+
5
+ set nocompatible
6
+ filetype off
7
+
8
+ " Load coc.nvim from the user's vim-plug directory
9
+ set runtimepath+=$USERPROFILE/vimfiles/local/plugged/coc.nvim
10
+
11
+ filetype plugin indent on
12
+ syntax on
13
+
14
+ " Point coc.nvim at a temporary config directory so it does not
15
+ " interfere with the user's real configuration.
16
+ let g:coc_config_home = $MERLS_SMOKE_CONFIG
17
+ let g:coc_data_home = $MERLS_SMOKE_DATA
@@ -0,0 +1,41 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { parseDocument } from "../src/asm/document";
6
+ import { collectSymbols } from "../src/asm/symbols";
7
+
8
+ export function runSymbolsTest(): void {
9
+ const fixturePath = path.resolve(
10
+ process.cwd(),
11
+ "test/fixtures/valid/merlin32-main-6502.asm"
12
+ );
13
+ const source = fs.readFileSync(fixturePath, "utf8");
14
+
15
+ const document = parseDocument(source);
16
+ const symbols = collectSymbols(document);
17
+
18
+ assert.deepEqual(symbols.get("TEXT"), {
19
+ name: "TEXT",
20
+ kind: "equate",
21
+ line: 11
22
+ });
23
+
24
+ assert.deepEqual(symbols.get("TEST_START"), {
25
+ name: "TEST_START",
26
+ kind: "label",
27
+ line: 31
28
+ });
29
+
30
+ assert.deepEqual(symbols.get("dum0"), {
31
+ name: "dum0",
32
+ kind: "data",
33
+ line: 17
34
+ });
35
+
36
+ assert.deepEqual(symbols.get("_num1"), {
37
+ name: "_num1",
38
+ kind: "data",
39
+ line: 25
40
+ });
41
+ }
@@ -0,0 +1,18 @@
1
+ import assert from "node:assert/strict";
2
+
3
+ import {
4
+ lineShapeTable,
5
+ tokenKindTable
6
+ } from "../src/asm/syntax";
7
+
8
+ export function runSyntaxShapeTest(): void {
9
+ assert.equal(tokenKindTable.get("comment")?.captureExamples[0], "; trailing note");
10
+ assert.equal(tokenKindTable.get("localLabel")?.captureExamples[0], "]loop");
11
+ assert.equal(tokenKindTable.get("modifier")?.captureExamples.includes("<value"), true);
12
+ assert.equal(tokenKindTable.has("expressionOperator"), true);
13
+
14
+ assert.equal(lineShapeTable.get("instruction")?.allowsLabel, true);
15
+ assert.equal(lineShapeTable.get("directive")?.requiresOperand, false);
16
+ assert.equal(lineShapeTable.get("equate")?.allowsExpression, true);
17
+ assert.equal(lineShapeTable.get("malformed")?.terminal, true);
18
+ }
@@ -0,0 +1,139 @@
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 runWorkspaceSymbolTest(): Promise<void> {
45
+ const serverPath = path.resolve(__dirname, "../src/server.js");
46
+ const linkPath = path.resolve(
47
+ process.cwd(),
48
+ "test/fixtures/valid/merlin32-linkscript.asm"
49
+ );
50
+ const mainPath = path.resolve(
51
+ process.cwd(),
52
+ "test/fixtures/valid/merlin32-main-6502.asm"
53
+ );
54
+ const linkUri = `file://${linkPath.replace(/\\/g, "/")}`;
55
+ const mainUri = `file://${mainPath.replace(/\\/g, "/")}`;
56
+ const child = spawn(process.execPath, [serverPath], {
57
+ stdio: ["pipe", "pipe", "pipe"]
58
+ });
59
+
60
+ let stdout = "";
61
+ let nextId = 1;
62
+ const pending = new Map<number, (message: JsonRpcMessage) => void>();
63
+
64
+ child.stdout.setEncoding("utf8");
65
+ child.stdout.on("data", (chunk: string) => {
66
+ stdout += chunk;
67
+ const decoded = decodeMessages(stdout);
68
+ stdout = decoded.rest;
69
+
70
+ for (const message of decoded.messages) {
71
+ if (message.id !== undefined) {
72
+ pending.get(message.id)?.(message);
73
+ pending.delete(message.id);
74
+ }
75
+ }
76
+ });
77
+
78
+ function sendRequest(method: string, params: object): Promise<JsonRpcMessage> {
79
+ const id = nextId++;
80
+ child.stdin.write(
81
+ encodeMessage({
82
+ id,
83
+ jsonrpc: "2.0",
84
+ method,
85
+ params
86
+ })
87
+ );
88
+
89
+ return new Promise((resolve) => {
90
+ pending.set(id, resolve);
91
+ });
92
+ }
93
+
94
+ function sendNotification(method: string, params: object): void {
95
+ child.stdin.write(
96
+ encodeMessage({
97
+ jsonrpc: "2.0",
98
+ method,
99
+ params
100
+ })
101
+ );
102
+ }
103
+
104
+ try {
105
+ await sendRequest("initialize", {
106
+ capabilities: {},
107
+ processId: process.pid,
108
+ rootUri: `file://${path.resolve(process.cwd()).replace(/\\/g, "/")}`
109
+ });
110
+
111
+ sendNotification("initialized", {});
112
+ sendNotification("textDocument/didOpen", {
113
+ textDocument: {
114
+ uri: linkUri,
115
+ languageId: "asm",
116
+ version: 1,
117
+ text: fs.readFileSync(linkPath, "utf8")
118
+ }
119
+ });
120
+ sendNotification("textDocument/didOpen", {
121
+ textDocument: {
122
+ uri: mainUri,
123
+ languageId: "asm",
124
+ version: 1,
125
+ text: fs.readFileSync(mainPath, "utf8")
126
+ }
127
+ });
128
+
129
+ const response = await sendRequest("workspace/symbol", {
130
+ query: "Get"
131
+ });
132
+
133
+ const symbols = response.result as Array<{ name: string; location: { uri: string } }>;
134
+ assert.equal(Array.isArray(symbols), true);
135
+ assert.equal(symbols.some((symbol) => symbol.name === "GetKey" && symbol.location.uri === mainUri), true);
136
+ } finally {
137
+ child.kill();
138
+ }
139
+ }
@@ -0,0 +1,29 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+
4
+ import { indexWorkspace } from "../src/asm/workspace";
5
+
6
+ export function runWorkspaceGraphTest(): void {
7
+ const entryPath = path.resolve(
8
+ process.cwd(),
9
+ "test/fixtures/valid/merlin32-linkscript.asm"
10
+ );
11
+ const mainPath = path.resolve(
12
+ process.cwd(),
13
+ "test/fixtures/valid/merlin32-main-6502.asm"
14
+ );
15
+
16
+ const workspace = indexWorkspace(entryPath);
17
+
18
+ assert.deepEqual(workspace.loadOrder, [entryPath, mainPath]);
19
+ assert.deepEqual(workspace.dependencies.get(entryPath), [mainPath]);
20
+ assert.equal(workspace.documents.has(entryPath), true);
21
+ assert.equal(workspace.documents.has(mainPath), true);
22
+
23
+ assert.deepEqual(workspace.symbols.get("TEXT"), {
24
+ name: "TEXT",
25
+ kind: "equate",
26
+ line: 11,
27
+ filePath: mainPath
28
+ });
29
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "Node",
6
+ "lib": ["ES2022"],
7
+ "rootDir": ".",
8
+ "outDir": "dist",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "skipLibCheck": true,
13
+ "types": ["node"]
14
+ },
15
+ "include": ["src/**/*.ts", "test/**/*.ts"]
16
+ }