@tabnas/lsp 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.
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ /* Copyright (c) 2026 Richard Rodger, MIT License */
3
+ 'use strict'
4
+
5
+ // Generate data/registry.json from fleet tabnas.plugin.json
6
+ // descriptors (the mcp/ts/tools/gen-data.js pattern: sibling
7
+ // checkouts, one and two levels up, dedupe by name, sort), merged with
8
+ // the overrides table below for the fields descriptors do not carry
9
+ // yet (plan C1). Each override is deleted as C1 lands per repo.
10
+
11
+ const fs = require('fs')
12
+ const path = require('path')
13
+
14
+ // languageId / kind / capability overrides (design §3). enabled:false
15
+ // = editor-collision policy default-off (entrenched incumbents).
16
+ const OVERRIDES = {
17
+ '@tabnas/json': { enabled: false },
18
+ '@tabnas/jsonc': { enabled: false },
19
+ '@tabnas/css': { enabled: false },
20
+ '@tabnas/markdown': { enabled: false, grammarKind: 'external' },
21
+ '@tabnas/yaml': { enabled: false },
22
+ '@tabnas/c': { enabled: false, grammarKind: 'external' },
23
+ '@tabnas/toml': { enabled: false },
24
+ '@tabnas/xml': { enabled: false },
25
+ '@tabnas/ini': { enabled: false },
26
+ '@tabnas/proto': { enabled: false, grammarKind: 'compiled' },
27
+ '@tabnas/chess': { languageId: 'pgn' },
28
+ '@tabnas/csv': {},
29
+ '@tabnas/json5': {},
30
+ '@tabnas/jsonl': {},
31
+ '@tabnas/zon': {},
32
+ '@tabnas/feed': { pluginKind: 'grammar' },
33
+ '@tabnas/abnf': { pluginKind: 'compiler' },
34
+ '@tabnas/ebnf': { pluginKind: 'compiler' },
35
+ '@tabnas/gbnf': { pluginKind: 'compiler' },
36
+ '@tabnas/bnf': { pluginKind: 'compiler' },
37
+ '@tabnas/expr': { pluginKind: 'modifier', grammarKind: 'imperative' },
38
+ '@tabnas/hoover': { pluginKind: 'modifier', grammarKind: 'imperative' },
39
+ '@tabnas/debug': { pluginKind: 'modifier', grammarKind: 'imperative' },
40
+ '@tabnas/directive': { pluginKind: 'modifier' },
41
+ '@tabnas/multisource': { pluginKind: 'modifier' },
42
+ '@tabnas/path': { pluginKind: 'modifier' },
43
+ }
44
+
45
+ function findDescriptors(root) {
46
+ const found = new Map()
47
+ const levels = [root, path.join(root, '..')]
48
+ for (const base of levels) {
49
+ let names = []
50
+ try {
51
+ names = fs.readdirSync(base)
52
+ } catch (e) {
53
+ continue
54
+ }
55
+ for (const n of names) {
56
+ const p = path.join(base, n, 'tabnas.plugin.json')
57
+ try {
58
+ const d = JSON.parse(fs.readFileSync(p, 'utf8'))
59
+ if (d.name && !found.has(d.name)) found.set(d.name, d)
60
+ } catch (e) {
61
+ // not a plugin repo
62
+ }
63
+ }
64
+ }
65
+ return [...found.values()].sort((a, b) => a.name.localeCompare(b.name))
66
+ }
67
+
68
+ // Fields a descriptor may carry once plan C1 has landed for its repo
69
+ // (tasks/ax-descriptor.sh preserves exactly these through
70
+ // regeneration). A descriptor that carries one is AUTHORITATIVE for it:
71
+ // the override table is interim scaffolding, and ADR-10 says derive,
72
+ // never duplicate. Without this the table would silently shadow the
73
+ // descriptor the moment C1 rolled out, which is the failure that would
74
+ // look like C1 having no effect.
75
+ const LSP_FIELDS = [
76
+ 'languageId',
77
+ 'grammarKind',
78
+ 'pluginKind',
79
+ 'syncGroups',
80
+ 'semanticTokens',
81
+ 'lexStream',
82
+ 'optionsSchema',
83
+ ]
84
+
85
+ // `enabled` is deliberately NOT in that list. It encodes the editor
86
+ // collision policy — whether tabnas should claim a language an
87
+ // entrenched extension already owns — which is a host decision, not a
88
+ // fact about the grammar. A plugin must not be able to enable itself.
89
+
90
+ function entryFor(d, redundant) {
91
+ const out = {
92
+ name: d.name,
93
+ extensions: d.extensions || [],
94
+ mediaTypes: d.mediaTypes || [],
95
+ base: d.base || null,
96
+ errorCodes: d.errorCodes || [],
97
+ }
98
+ const ov = OVERRIDES[d.name] || {}
99
+ for (const k of Object.keys(ov)) out[k] = ov[k]
100
+ for (const f of LSP_FIELDS) {
101
+ if (undefined === d[f]) continue
102
+ if (f in ov) {
103
+ redundant.push(d.name + '.' + f)
104
+ }
105
+ out[f] = d[f]
106
+ }
107
+ return out
108
+ }
109
+
110
+ function main() {
111
+ // Default to the promoted layout — this package is the `ts/` half of
112
+ // the `lsp` repo, sitting beside its sibling repos in a fleet
113
+ // checkout, so the fleet root is two levels above the package
114
+ // (ts/tools -> ts -> lsp -> fleet). On a lone checkout with no
115
+ // sibling repos the guard below makes that a clear error rather than
116
+ // an empty registry.
117
+ const root = process.argv[2] || path.join(__dirname, '..', '..', '..')
118
+ const descriptors = findDescriptors(root)
119
+
120
+ // Finding nothing means the fleet is not where we looked, not that
121
+ // the fleet is empty — a wrong root is the likely cause, and this
122
+ // staging layout is not where the promoted repo will sit. Writing
123
+ // the empty result would replace a good registry with one that
124
+ // routes no language at all, and the server would then serve
125
+ // nothing while reporting no error. Refuse instead.
126
+ if (0 === descriptors.length) {
127
+ console.error(
128
+ 'gen-registry: no tabnas.plugin.json found under ' + root + '\n' +
129
+ 'Pass the fleet root explicitly: node tools/gen-registry.js <root>\n' +
130
+ 'Refusing to overwrite data/registry.json with an empty registry.',
131
+ )
132
+ process.exitCode = 1
133
+ return
134
+ }
135
+
136
+ const redundant = []
137
+ const entries = descriptors.map((d) => entryFor(d, redundant))
138
+ const out = { generated: 'tools/gen-registry.js', count: entries.length, entries }
139
+ const file = path.join(__dirname, '..', 'data', 'registry.json')
140
+ fs.writeFileSync(file, JSON.stringify(out, null, 2) + '\n')
141
+ console.log('wrote ' + file + ' (' + entries.length + ' entries)')
142
+ // Naming them is how the interim table actually gets deleted rather
143
+ // than quietly outliving the migration it was meant to bridge.
144
+ if (0 < redundant.length) {
145
+ console.log(
146
+ 'descriptors now carry these — delete from OVERRIDES: ' +
147
+ redundant.join(', '),
148
+ )
149
+ }
150
+ }
151
+
152
+ if (require.main === module) main()
153
+ module.exports = { findDescriptors, entryFor, OVERRIDES, LSP_FIELDS }