@razdolbai/merls 1.3.0 → 1.3.2

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.
@@ -0,0 +1,2 @@
1
+ coc
2
+ js
package/README.md CHANGED
@@ -1,25 +1,25 @@
1
- # merls
2
-
3
- `merls` is a language server for **Merlin-style 6502 assembly** with **coc.nvim** as the primary editor target.
4
-
1
+ # merls
2
+
3
+ `merls` is a language server for **Merlin-style 6502 assembly** with **coc.nvim** as the primary editor target.
4
+
5
5
  ## Status
6
6
 
7
7
  The MVP LSP feature set for Merlin-style 6502 assembly is complete and published to npm as `@razdolbai/merls`. It provides core language server features and is ready to be used with editors like `coc.nvim`.
8
-
9
- ## Goals
10
-
11
- - provide a standalone LSP server over stdio
12
- - work cleanly with coc.nvim through standard language-server configuration
13
- - support Merlin-style 6502 source structure, symbols, directives, and expressions
14
- - deliver useful editing features before deeper assembler integration
15
-
16
- ## Scope
17
-
18
- ### In scope
8
+
9
+ ## Goals
10
+
11
+ - Provide a standalone LSP server over stdio
12
+ - Work cleanly with coc.nvim through standard language-server configuration
13
+ - support Merlin-style 6502 source structure, symbols, directives, and expressions
14
+ - Deliver useful editing features before deeper assembler integration
15
+
16
+ ## Scope
17
+
18
+ ### In Scope
19
19
 
20
20
  - 6502-only Merlin-style assembly
21
- - parser-based diagnostics for the MVP
22
- - core LSP features such as:
21
+ - Parser-based diagnostics for the MVP
22
+ - Core LSP features such as:
23
23
  - diagnostics
24
24
  - hover
25
25
  - completion
@@ -29,13 +29,11 @@ The MVP LSP feature set for Merlin-style 6502 assembly is complete and published
29
29
  - workspace symbols
30
30
  - semantic tokens (syntax highlighting)
31
31
 
32
- ### Out of scope
32
+ ### Out of Scope
33
33
 
34
34
  - 65816 support
35
- - assembler-backed diagnostics in the first implementation pass
36
- - coc.nvim-specific plugin code for the MVP
37
-
38
-
35
+
36
+
39
37
  ## Implementation
40
38
 
41
39
  - **runtime:** Node.js
@@ -52,7 +50,7 @@ Install globally via npm:
52
50
  npm install -g @razdolbai/merls
53
51
  ```
54
52
 
55
- ## Development workflow
53
+ ## Development Workflow
56
54
 
57
55
  - `npm install`: install project dependencies
58
56
  - `npm run build`: compile the TypeScript sources into `dist/`
@@ -60,14 +58,14 @@ npm install -g @razdolbai/merls
60
58
  - `npm run dev`: run the TypeScript compiler in watch mode during bootstrap work
61
59
  - `pwsh test/smoke/run-smoke.ps1`: run the headless Vim + coc.nvim smoke test (requires Vim with coc.nvim installed via vim-plug)
62
60
 
63
- The packaged CLI entrypoint now lives at `dist/src/cli.js`.
61
+ The packaged CLI entry point now lives at `dist/src/cli.js`.
64
62
 
65
- ## CLI contract
63
+ ## CLI Contract
66
64
 
67
65
  The supported stdio launch contract is `merls --stdio`.
68
66
 
69
67
 
70
- ## coc.nvim target
68
+ ## coc.nvim Target
71
69
 
72
70
  The intended integration model is a standard `languageserver` entry in `coc-settings.json` that launches `merls` over stdio.
73
71
 
@@ -75,6 +73,7 @@ If you installed `merls` globally via npm, the `languageserver` shape is:
75
73
 
76
74
  ```json
77
75
  {
76
+ "semanticTokens.enable": true,
78
77
  "languageserver": {
79
78
  "merls": {
80
79
  "command": "merls",
@@ -85,21 +84,23 @@ If you installed `merls` globally via npm, the `languageserver` shape is:
85
84
  ".git"
86
85
  ],
87
86
  "filetypes": [
88
- "asm"
87
+ "6502"
89
88
  ]
90
89
  }
91
90
  }
92
91
  }
93
92
  ```
94
93
 
94
+ To enable semantic tokens for syntax highlighting, ensure `"semanticTokens.enable": true` is set in your `coc-settings.json` and that your Vim buffer is set to the matching filetype (e.g. `set filetype=6502`).
95
+
95
96
  An example configuration lives in `examples/coc-settings.json`.
96
-
97
- ## Development notes
97
+
98
+ ## Development Notes
98
99
 
99
100
  - TDD is mandatory for implementation work in this repository.
100
101
  - Core syntax, parsing, and document model logic is located in `src/asm/`.
101
102
  - LSP handlers (hover, completion, diagnostics, etc.) are located in `src/lsp/`.
102
- - The CLI entrypoint lives in `src/cli.ts` and compiles to `dist/src/cli.js`.
103
+ - The CLI entry point lives in `src/cli.ts` and compiles to `dist/src/cli.js`.
103
104
  - The `test/fixtures/valid/` directory contains supported 6502 Merlin-style syntax samples.
104
105
  - The `test/fixtures/invalid/` directory contains unsupported 65816-only syntax samples for negative testing.
105
106
  - The integration test suite covers stdio `initialize`, the CLI contract, and core LSP features.
@@ -57,7 +57,8 @@ function parseStructuredLine(text, tokens) {
57
57
  text
58
58
  };
59
59
  }
60
- if (token.kind === "expressionOperator" && token.lexeme === "=") {
60
+ if ((token.kind === "expressionOperator" && token.lexeme === "=") ||
61
+ (token.kind === "directive" && token.lexeme.toLowerCase() === "equ")) {
61
62
  if (label === null) {
62
63
  throw new Error("equate requires a label");
63
64
  }
@@ -107,7 +107,7 @@ async function runDefinitionReferencesTest() {
107
107
  });
108
108
  const definition = definitionResponse.result;
109
109
  strict_1.default.equal(definition.uri, mainUri);
110
- strict_1.default.equal(definition.range.start.line, 69);
110
+ strict_1.default.equal(definition.range.start.line, 70);
111
111
  const referencesResponse = await sendRequest("textDocument/references", {
112
112
  textDocument: { uri: mainUri },
113
113
  position: positionOf(text, "GetKey ldx"),
@@ -117,8 +117,8 @@ async function runDefinitionReferencesTest() {
117
117
  });
118
118
  const references = referencesResponse.result;
119
119
  strict_1.default.equal(Array.isArray(references), true);
120
- strict_1.default.equal(references.some((reference) => reference.uri === mainUri && reference.range.start.line === 69), true);
121
120
  strict_1.default.equal(references.some((reference) => reference.uri === mainUri && reference.range.start.line === 70), true);
121
+ strict_1.default.equal(references.some((reference) => reference.uri === mainUri && reference.range.start.line === 71), true);
122
122
  }
123
123
  finally {
124
124
  child.kill();
@@ -14,30 +14,30 @@ function runLocalLabelScopeTest() {
14
14
  const source = node_fs_1.default.readFileSync(fixturePath, "utf8");
15
15
  const document = (0, document_1.parseDocument)(source);
16
16
  const scope = (0, local_labels_1.resolveLocalLabels)(document);
17
- strict_1.default.deepEqual(scope.definitions.get("]loop@69"), {
17
+ strict_1.default.deepEqual(scope.definitions.get("]loop@70"), {
18
18
  name: "]loop",
19
- line: 71,
19
+ line: 72,
20
20
  anchor: "GetKey",
21
- qualifiedName: "]loop@69"
21
+ qualifiedName: "]loop@70"
22
22
  });
23
- strict_1.default.deepEqual(scope.references.get("]loop@73"), {
23
+ strict_1.default.deepEqual(scope.references.get("]loop@74"), {
24
24
  name: "]loop",
25
- line: 73,
25
+ line: 74,
26
26
  anchor: "GetKey",
27
- qualifiedName: "]loop@69",
28
- targetLine: 71
27
+ qualifiedName: "]loop@70",
28
+ targetLine: 72
29
29
  });
30
- strict_1.default.deepEqual(scope.definitions.get(":err@69"), {
30
+ strict_1.default.deepEqual(scope.definitions.get(":err@70"), {
31
31
  name: ":err",
32
- line: 82,
32
+ line: 83,
33
33
  anchor: "GetKey",
34
- qualifiedName: ":err@69"
34
+ qualifiedName: ":err@70"
35
35
  });
36
- strict_1.default.deepEqual(scope.references.get(":good@81"), {
36
+ strict_1.default.deepEqual(scope.references.get(":good@82"), {
37
37
  name: ":good",
38
- line: 81,
38
+ line: 82,
39
39
  anchor: "GetKey",
40
- qualifiedName: ":good@69",
41
- targetLine: 84
40
+ qualifiedName: ":good@70",
41
+ targetLine: 85
42
42
  });
43
43
  }
@@ -22,16 +22,21 @@ function runSymbolsTest() {
22
22
  strict_1.default.deepEqual(symbols.get("TEST_START"), {
23
23
  name: "TEST_START",
24
24
  kind: "label",
25
- line: 31
25
+ line: 32
26
26
  });
27
27
  strict_1.default.deepEqual(symbols.get("dum0"), {
28
28
  name: "dum0",
29
29
  kind: "data",
30
- line: 17
30
+ line: 18
31
31
  });
32
32
  strict_1.default.deepEqual(symbols.get("_num1"), {
33
33
  name: "_num1",
34
34
  kind: "data",
35
- line: 25
35
+ line: 26
36
+ });
37
+ strict_1.default.deepEqual(symbols.get("MY_VAL"), {
38
+ name: "MY_VAL",
39
+ kind: "equate",
40
+ line: 15
36
41
  });
37
42
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "semanticTokens.enable": true,
2
3
  "languageserver": {
3
4
  "merls": {
4
5
  "command": "merls",
@@ -9,7 +10,7 @@
9
10
  ".git"
10
11
  ],
11
12
  "filetypes": [
12
- "asm"
13
+ "6502"
13
14
  ]
14
15
  }
15
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@razdolbai/merls",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Language server for Merlin-style 6502 assembly.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
package/src/asm/parser.ts CHANGED
@@ -127,7 +127,10 @@ function parseStructuredLine(text: string, tokens: readonly Token[]): ParsedLine
127
127
  };
128
128
  }
129
129
 
130
- if (token.kind === "expressionOperator" && token.lexeme === "=") {
130
+ if (
131
+ (token.kind === "expressionOperator" && token.lexeme === "=") ||
132
+ (token.kind === "directive" && token.lexeme.toLowerCase() === "equ")
133
+ ) {
131
134
  if (label === null) {
132
135
  throw new Error("equate requires a label");
133
136
  }
@@ -132,7 +132,7 @@ export async function runDefinitionReferencesTest(): Promise<void> {
132
132
 
133
133
  const definition = definitionResponse.result as { uri: string; range: { start: { line: number } } };
134
134
  assert.equal(definition.uri, mainUri);
135
- assert.equal(definition.range.start.line, 69);
135
+ assert.equal(definition.range.start.line, 70);
136
136
 
137
137
  const referencesResponse = await sendRequest("textDocument/references", {
138
138
  textDocument: { uri: mainUri },
@@ -144,8 +144,8 @@ export async function runDefinitionReferencesTest(): Promise<void> {
144
144
 
145
145
  const references = referencesResponse.result as Array<{ uri: string; range: { start: { line: number } } }>;
146
146
  assert.equal(Array.isArray(references), true);
147
- assert.equal(references.some((reference) => reference.uri === mainUri && reference.range.start.line === 69), true);
148
147
  assert.equal(references.some((reference) => reference.uri === mainUri && reference.range.start.line === 70), true);
148
+ assert.equal(references.some((reference) => reference.uri === mainUri && reference.range.start.line === 71), true);
149
149
  } finally {
150
150
  child.kill();
151
151
  }
@@ -13,6 +13,7 @@ TEXT = $FB39
13
13
  CROUT = $FD8E
14
14
  DOSWARM = $3D0
15
15
  TSTADDR = $1000
16
+ MY_VAL EQU $42
16
17
 
17
18
  DUM 0
18
19
  dum0 ds 1
@@ -15,33 +15,33 @@ export function runLocalLabelScopeTest(): void {
15
15
  const document = parseDocument(source);
16
16
  const scope = resolveLocalLabels(document);
17
17
 
18
- assert.deepEqual(scope.definitions.get("]loop@69"), {
18
+ assert.deepEqual(scope.definitions.get("]loop@70"), {
19
19
  name: "]loop",
20
- line: 71,
20
+ line: 72,
21
21
  anchor: "GetKey",
22
- qualifiedName: "]loop@69"
22
+ qualifiedName: "]loop@70"
23
23
  });
24
24
 
25
- assert.deepEqual(scope.references.get("]loop@73"), {
25
+ assert.deepEqual(scope.references.get("]loop@74"), {
26
26
  name: "]loop",
27
- line: 73,
27
+ line: 74,
28
28
  anchor: "GetKey",
29
- qualifiedName: "]loop@69",
30
- targetLine: 71
29
+ qualifiedName: "]loop@70",
30
+ targetLine: 72
31
31
  });
32
32
 
33
- assert.deepEqual(scope.definitions.get(":err@69"), {
33
+ assert.deepEqual(scope.definitions.get(":err@70"), {
34
34
  name: ":err",
35
- line: 82,
35
+ line: 83,
36
36
  anchor: "GetKey",
37
- qualifiedName: ":err@69"
37
+ qualifiedName: ":err@70"
38
38
  });
39
39
 
40
- assert.deepEqual(scope.references.get(":good@81"), {
40
+ assert.deepEqual(scope.references.get(":good@82"), {
41
41
  name: ":good",
42
- line: 81,
42
+ line: 82,
43
43
  anchor: "GetKey",
44
- qualifiedName: ":good@69",
45
- targetLine: 84
44
+ qualifiedName: ":good@70",
45
+ targetLine: 85
46
46
  });
47
47
  }
@@ -24,18 +24,24 @@ export function runSymbolsTest(): void {
24
24
  assert.deepEqual(symbols.get("TEST_START"), {
25
25
  name: "TEST_START",
26
26
  kind: "label",
27
- line: 31
27
+ line: 32
28
28
  });
29
29
 
30
30
  assert.deepEqual(symbols.get("dum0"), {
31
31
  name: "dum0",
32
32
  kind: "data",
33
- line: 17
33
+ line: 18
34
34
  });
35
35
 
36
36
  assert.deepEqual(symbols.get("_num1"), {
37
37
  name: "_num1",
38
38
  kind: "data",
39
- line: 25
39
+ line: 26
40
+ });
41
+
42
+ assert.deepEqual(symbols.get("MY_VAL"), {
43
+ name: "MY_VAL",
44
+ kind: "equate",
45
+ line: 15
40
46
  });
41
47
  }