@plaited/development-skills 0.3.5
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/.claude/commands/lsp-analyze.md +66 -0
- package/.claude/commands/lsp-find.md +51 -0
- package/.claude/commands/lsp-hover.md +48 -0
- package/.claude/commands/lsp-refs.md +55 -0
- package/.claude/commands/scaffold-rules.md +221 -0
- package/.claude/commands/validate-skill.md +29 -0
- package/.claude/rules/accuracy.md +64 -0
- package/.claude/rules/bun-apis.md +80 -0
- package/.claude/rules/code-review.md +276 -0
- package/.claude/rules/git-workflow.md +66 -0
- package/.claude/rules/github.md +154 -0
- package/.claude/rules/testing.md +125 -0
- package/.claude/settings.local.json +47 -0
- package/.claude/skills/code-documentation/SKILL.md +47 -0
- package/.claude/skills/code-documentation/references/internal-templates.md +113 -0
- package/.claude/skills/code-documentation/references/maintenance.md +164 -0
- package/.claude/skills/code-documentation/references/public-api-templates.md +100 -0
- package/.claude/skills/code-documentation/references/type-documentation.md +116 -0
- package/.claude/skills/code-documentation/references/workflow.md +60 -0
- package/.claude/skills/scaffold-rules/SKILL.md +97 -0
- package/.claude/skills/typescript-lsp/SKILL.md +239 -0
- package/.claude/skills/validate-skill/SKILL.md +105 -0
- package/LICENSE +15 -0
- package/README.md +149 -0
- package/bin/cli.ts +109 -0
- package/package.json +57 -0
- package/src/lsp-analyze.ts +223 -0
- package/src/lsp-client.ts +400 -0
- package/src/lsp-find.ts +100 -0
- package/src/lsp-hover.ts +87 -0
- package/src/lsp-references.ts +83 -0
- package/src/lsp-symbols.ts +73 -0
- package/src/resolve-file-path.ts +28 -0
- package/src/scaffold-rules.ts +435 -0
- package/src/tests/fixtures/sample.ts +27 -0
- package/src/tests/lsp-client.spec.ts +180 -0
- package/src/tests/resolve-file-path.spec.ts +33 -0
- package/src/tests/scaffold-rules.spec.ts +286 -0
- package/src/tests/validate-skill.spec.ts +231 -0
- package/src/validate-skill.ts +492 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Batch analysis script for TypeScript/JavaScript files
|
|
4
|
+
*
|
|
5
|
+
* Performs multiple LSP queries in a single session for efficiency.
|
|
6
|
+
* Useful for understanding a file before making changes.
|
|
7
|
+
*
|
|
8
|
+
* Usage: bun lsp-analyze.ts <file> [options]
|
|
9
|
+
*
|
|
10
|
+
* Options:
|
|
11
|
+
* --symbols, -s List all symbols in the file
|
|
12
|
+
* --exports, -e List only exported symbols
|
|
13
|
+
* --hover <line:char> Get type info at position (can be repeated)
|
|
14
|
+
* --refs <line:char> Find references at position (can be repeated)
|
|
15
|
+
* --all Run all analyses (symbols + exports)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { parseArgs } from 'node:util'
|
|
19
|
+
import { LspClient } from './lsp-client.ts'
|
|
20
|
+
import { resolveFilePath } from './resolve-file-path.ts'
|
|
21
|
+
|
|
22
|
+
type SymbolInfo = {
|
|
23
|
+
name: string
|
|
24
|
+
kind: number
|
|
25
|
+
range: { start: { line: number; character: number }; end: { line: number; character: number } }
|
|
26
|
+
children?: SymbolInfo[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type AnalysisResult = {
|
|
30
|
+
file: string
|
|
31
|
+
symbols?: Array<{ name: string; kind: string; line: number }>
|
|
32
|
+
exports?: Array<{ name: string; kind: string; line: number }>
|
|
33
|
+
hovers?: Array<{ position: string; content: unknown }>
|
|
34
|
+
references?: Array<{ position: string; locations: unknown }>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const symbolKindNames: Record<number, string> = {
|
|
38
|
+
1: 'File',
|
|
39
|
+
2: 'Module',
|
|
40
|
+
3: 'Namespace',
|
|
41
|
+
4: 'Package',
|
|
42
|
+
5: 'Class',
|
|
43
|
+
6: 'Method',
|
|
44
|
+
7: 'Property',
|
|
45
|
+
8: 'Field',
|
|
46
|
+
9: 'Constructor',
|
|
47
|
+
10: 'Enum',
|
|
48
|
+
11: 'Interface',
|
|
49
|
+
12: 'Function',
|
|
50
|
+
13: 'Variable',
|
|
51
|
+
14: 'Constant',
|
|
52
|
+
15: 'String',
|
|
53
|
+
16: 'Number',
|
|
54
|
+
17: 'Boolean',
|
|
55
|
+
18: 'Array',
|
|
56
|
+
19: 'Object',
|
|
57
|
+
20: 'Key',
|
|
58
|
+
21: 'Null',
|
|
59
|
+
22: 'EnumMember',
|
|
60
|
+
23: 'Struct',
|
|
61
|
+
24: 'Event',
|
|
62
|
+
25: 'Operator',
|
|
63
|
+
26: 'TypeParameter',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const extractSymbols = (symbols: SymbolInfo[], prefix = ''): Array<{ name: string; kind: string; line: number }> => {
|
|
67
|
+
const result: Array<{ name: string; kind: string; line: number }> = []
|
|
68
|
+
for (const sym of symbols) {
|
|
69
|
+
result.push({
|
|
70
|
+
name: prefix ? `${prefix}.${sym.name}` : sym.name,
|
|
71
|
+
kind: symbolKindNames[sym.kind] || `Unknown(${sym.kind})`,
|
|
72
|
+
line: sym.range.start.line,
|
|
73
|
+
})
|
|
74
|
+
if (sym.children) {
|
|
75
|
+
result.push(...extractSymbols(sym.children, sym.name))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Batch analysis for TypeScript/JavaScript files
|
|
83
|
+
*
|
|
84
|
+
* @param args - Command line arguments
|
|
85
|
+
*/
|
|
86
|
+
export const lspAnalyze = async (args: string[]) => {
|
|
87
|
+
const { values, positionals } = parseArgs({
|
|
88
|
+
args,
|
|
89
|
+
options: {
|
|
90
|
+
symbols: { type: 'boolean', short: 's' },
|
|
91
|
+
exports: { type: 'boolean', short: 'e' },
|
|
92
|
+
hover: { type: 'string', multiple: true },
|
|
93
|
+
refs: { type: 'string', multiple: true },
|
|
94
|
+
all: { type: 'boolean' },
|
|
95
|
+
help: { type: 'boolean', short: 'h' },
|
|
96
|
+
},
|
|
97
|
+
allowPositionals: true,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (values.help || positionals.length === 0) {
|
|
101
|
+
console.log(`
|
|
102
|
+
LSP Analyze - Batch analysis for TypeScript/JavaScript files
|
|
103
|
+
|
|
104
|
+
Usage: lsp-analyze <file> [options]
|
|
105
|
+
|
|
106
|
+
Options:
|
|
107
|
+
--symbols, -s List all symbols in the file
|
|
108
|
+
--exports, -e List only exported symbols
|
|
109
|
+
--hover <line:char> Get type info at position (can be repeated)
|
|
110
|
+
--refs <line:char> Find references at position (can be repeated)
|
|
111
|
+
--all Run all analyses (symbols + exports)
|
|
112
|
+
--help, -h Show this help
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
lsp-analyze src/app.ts --all
|
|
116
|
+
lsp-analyze src/app.ts --symbols
|
|
117
|
+
lsp-analyze src/app.ts --hover 50:15 --hover 60:20
|
|
118
|
+
lsp-analyze src/app.ts --refs 10:8
|
|
119
|
+
`)
|
|
120
|
+
process.exit(0)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const filePath = positionals[0]
|
|
124
|
+
if (!filePath) {
|
|
125
|
+
console.error('Error: File path required')
|
|
126
|
+
process.exit(1)
|
|
127
|
+
}
|
|
128
|
+
const absolutePath = await resolveFilePath(filePath)
|
|
129
|
+
const uri = `file://${absolutePath}`
|
|
130
|
+
const rootUri = `file://${process.cwd()}`
|
|
131
|
+
|
|
132
|
+
const client = new LspClient({ rootUri })
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
await client.start()
|
|
136
|
+
|
|
137
|
+
const file = Bun.file(absolutePath)
|
|
138
|
+
if (!(await file.exists())) {
|
|
139
|
+
console.error(`Error: File not found: ${absolutePath}`)
|
|
140
|
+
process.exit(1)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const text = await file.text()
|
|
144
|
+
const languageId = absolutePath.endsWith('.tsx')
|
|
145
|
+
? 'typescriptreact'
|
|
146
|
+
: absolutePath.endsWith('.ts')
|
|
147
|
+
? 'typescript'
|
|
148
|
+
: absolutePath.endsWith('.jsx')
|
|
149
|
+
? 'javascriptreact'
|
|
150
|
+
: 'javascript'
|
|
151
|
+
|
|
152
|
+
client.openDocument(uri, languageId, 1, text)
|
|
153
|
+
|
|
154
|
+
const result: AnalysisResult = { file: filePath }
|
|
155
|
+
|
|
156
|
+
// Get symbols if requested
|
|
157
|
+
if (values.symbols || values.exports || values.all) {
|
|
158
|
+
const symbols = (await client.documentSymbols(uri)) as SymbolInfo[]
|
|
159
|
+
const extracted = extractSymbols(symbols)
|
|
160
|
+
|
|
161
|
+
if (values.symbols || values.all) {
|
|
162
|
+
result.symbols = extracted
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (values.exports || values.all) {
|
|
166
|
+
// Filter to only top-level exports (items that start with export in source)
|
|
167
|
+
const lines = text.split('\n')
|
|
168
|
+
result.exports = extracted.filter((sym) => {
|
|
169
|
+
const line = lines[sym.line]
|
|
170
|
+
return line?.includes('export')
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Get hover info if requested
|
|
176
|
+
if (values.hover?.length) {
|
|
177
|
+
result.hovers = []
|
|
178
|
+
for (const pos of values.hover) {
|
|
179
|
+
const parts = pos.split(':')
|
|
180
|
+
const lineStr = parts[0] ?? ''
|
|
181
|
+
const charStr = parts[1] ?? ''
|
|
182
|
+
const line = parseInt(lineStr, 10)
|
|
183
|
+
const char = parseInt(charStr, 10)
|
|
184
|
+
|
|
185
|
+
if (!Number.isNaN(line) && !Number.isNaN(char)) {
|
|
186
|
+
const hover = await client.hover(uri, line, char)
|
|
187
|
+
result.hovers.push({ position: pos, content: hover })
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Get references if requested
|
|
193
|
+
if (values.refs?.length) {
|
|
194
|
+
result.references = []
|
|
195
|
+
for (const pos of values.refs) {
|
|
196
|
+
const parts = pos.split(':')
|
|
197
|
+
const lineStr = parts[0] ?? ''
|
|
198
|
+
const charStr = parts[1] ?? ''
|
|
199
|
+
const line = parseInt(lineStr, 10)
|
|
200
|
+
const char = parseInt(charStr, 10)
|
|
201
|
+
|
|
202
|
+
if (!Number.isNaN(line) && !Number.isNaN(char)) {
|
|
203
|
+
const refs = await client.references(uri, line, char, true)
|
|
204
|
+
result.references.push({ position: pos, locations: refs })
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
client.closeDocument(uri)
|
|
210
|
+
await client.stop()
|
|
211
|
+
|
|
212
|
+
console.log(JSON.stringify(result, null, 2))
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error(`Error: ${error}`)
|
|
215
|
+
await client.stop()
|
|
216
|
+
process.exit(1)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Keep executable entry point for direct execution
|
|
221
|
+
if (import.meta.main) {
|
|
222
|
+
await lspAnalyze(Bun.argv.slice(2))
|
|
223
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript Language Server client using Bun.spawn
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Spawns typescript-language-server as a subprocess and communicates via LSP JSON-RPC protocol.
|
|
6
|
+
* Uses Bun's native spawn API for process management.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Subprocess } from 'bun'
|
|
12
|
+
|
|
13
|
+
type JsonRpcRequest = {
|
|
14
|
+
jsonrpc: '2.0'
|
|
15
|
+
id: number
|
|
16
|
+
method: string
|
|
17
|
+
params?: unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type JsonRpcResponse = {
|
|
21
|
+
jsonrpc: '2.0'
|
|
22
|
+
id: number
|
|
23
|
+
result?: unknown
|
|
24
|
+
error?: { code: number; message: string; data?: unknown }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type JsonRpcNotification = {
|
|
28
|
+
jsonrpc: '2.0'
|
|
29
|
+
method: string
|
|
30
|
+
params?: unknown
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type PendingRequest = {
|
|
34
|
+
resolve: (value: unknown) => void
|
|
35
|
+
reject: (error: Error) => void
|
|
36
|
+
timer?: ReturnType<typeof setTimeout>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* LSP Client that manages a typescript-language-server subprocess
|
|
41
|
+
*/
|
|
42
|
+
export class LspClient {
|
|
43
|
+
#process: Subprocess | null = null
|
|
44
|
+
#requestId = 0
|
|
45
|
+
#pendingRequests = new Map<number, PendingRequest>()
|
|
46
|
+
#buffer = new Uint8Array(0)
|
|
47
|
+
#contentLength = -1
|
|
48
|
+
#initialized = false
|
|
49
|
+
#rootUri: string
|
|
50
|
+
#serverCommand: string[]
|
|
51
|
+
#requestTimeout: number
|
|
52
|
+
|
|
53
|
+
constructor({
|
|
54
|
+
rootUri,
|
|
55
|
+
command = ['bun', 'typescript-language-server', '--stdio'],
|
|
56
|
+
requestTimeout = 30000,
|
|
57
|
+
}: {
|
|
58
|
+
rootUri: string
|
|
59
|
+
command?: string[]
|
|
60
|
+
requestTimeout?: number
|
|
61
|
+
}) {
|
|
62
|
+
this.#rootUri = rootUri
|
|
63
|
+
this.#serverCommand = command
|
|
64
|
+
this.#requestTimeout = requestTimeout
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Start the LSP server subprocess
|
|
69
|
+
*/
|
|
70
|
+
async start(): Promise<void> {
|
|
71
|
+
if (this.#process) {
|
|
72
|
+
throw new Error('LSP server already running')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.#process = Bun.spawn(this.#serverCommand, {
|
|
76
|
+
stdin: 'pipe',
|
|
77
|
+
stdout: 'pipe',
|
|
78
|
+
stderr: 'pipe',
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Start reading stdout
|
|
82
|
+
this.#readOutput()
|
|
83
|
+
|
|
84
|
+
// Initialize the LSP connection
|
|
85
|
+
await this.#initialize()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Stop the LSP server subprocess
|
|
90
|
+
*/
|
|
91
|
+
async stop(): Promise<void> {
|
|
92
|
+
if (!this.#process) return
|
|
93
|
+
|
|
94
|
+
// Send shutdown request
|
|
95
|
+
try {
|
|
96
|
+
await this.request('shutdown', null)
|
|
97
|
+
this.notify('exit')
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore errors during shutdown
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.#process.kill()
|
|
103
|
+
this.#process = null
|
|
104
|
+
this.#initialized = false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if the LSP server is running
|
|
109
|
+
*/
|
|
110
|
+
isRunning(): boolean {
|
|
111
|
+
return this.#process !== null && this.#initialized
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Send a request to the LSP server and wait for response
|
|
116
|
+
*/
|
|
117
|
+
async request<T = unknown>(method: string, params: unknown): Promise<T> {
|
|
118
|
+
if (!this.#process) {
|
|
119
|
+
throw new Error('LSP server not running')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.#requestId += 1
|
|
123
|
+
const id = this.#requestId
|
|
124
|
+
const request: JsonRpcRequest = {
|
|
125
|
+
jsonrpc: '2.0',
|
|
126
|
+
id,
|
|
127
|
+
method,
|
|
128
|
+
params: params ?? undefined,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
this.#pendingRequests.delete(id)
|
|
134
|
+
reject(new Error(`LSP request timeout: ${method} (id=${id})`))
|
|
135
|
+
}, this.#requestTimeout)
|
|
136
|
+
|
|
137
|
+
this.#pendingRequests.set(id, {
|
|
138
|
+
resolve: resolve as (value: unknown) => void,
|
|
139
|
+
reject,
|
|
140
|
+
timer,
|
|
141
|
+
})
|
|
142
|
+
this.#send(request)
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Send a notification to the LSP server (no response expected)
|
|
148
|
+
*/
|
|
149
|
+
notify(method: string, params?: unknown): void {
|
|
150
|
+
if (!this.#process) {
|
|
151
|
+
throw new Error('LSP server not running')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const notification: JsonRpcNotification = {
|
|
155
|
+
jsonrpc: '2.0',
|
|
156
|
+
method,
|
|
157
|
+
params,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.#send(notification)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// LSP Methods
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* textDocument/hover - Get hover information at a position
|
|
167
|
+
*/
|
|
168
|
+
async hover(uri: string, line: number, character: number): Promise<unknown> {
|
|
169
|
+
return this.request('textDocument/hover', {
|
|
170
|
+
textDocument: { uri },
|
|
171
|
+
position: { line, character },
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* textDocument/definition - Go to definition
|
|
177
|
+
*/
|
|
178
|
+
async definition(uri: string, line: number, character: number): Promise<unknown> {
|
|
179
|
+
return this.request('textDocument/definition', {
|
|
180
|
+
textDocument: { uri },
|
|
181
|
+
position: { line, character },
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* textDocument/references - Find all references
|
|
187
|
+
*/
|
|
188
|
+
async references(uri: string, line: number, character: number, includeDeclaration = true): Promise<unknown> {
|
|
189
|
+
return this.request('textDocument/references', {
|
|
190
|
+
textDocument: { uri },
|
|
191
|
+
position: { line, character },
|
|
192
|
+
context: { includeDeclaration },
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* textDocument/completion - Get completions at a position
|
|
198
|
+
*/
|
|
199
|
+
async completion(uri: string, line: number, character: number): Promise<unknown> {
|
|
200
|
+
return this.request('textDocument/completion', {
|
|
201
|
+
textDocument: { uri },
|
|
202
|
+
position: { line, character },
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* textDocument/signatureHelp - Get signature help
|
|
208
|
+
*/
|
|
209
|
+
async signatureHelp(uri: string, line: number, character: number): Promise<unknown> {
|
|
210
|
+
return this.request('textDocument/signatureHelp', {
|
|
211
|
+
textDocument: { uri },
|
|
212
|
+
position: { line, character },
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* textDocument/documentSymbol - Get document symbols
|
|
218
|
+
*/
|
|
219
|
+
async documentSymbols(uri: string): Promise<unknown> {
|
|
220
|
+
return this.request('textDocument/documentSymbol', {
|
|
221
|
+
textDocument: { uri },
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* workspace/symbol - Search for symbols across workspace
|
|
227
|
+
*/
|
|
228
|
+
async workspaceSymbols(query: string): Promise<unknown> {
|
|
229
|
+
return this.request('workspace/symbol', { query })
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Open a document in the LSP server
|
|
234
|
+
*/
|
|
235
|
+
openDocument(uri: string, languageId: string, version: number, text: string): void {
|
|
236
|
+
this.notify('textDocument/didOpen', {
|
|
237
|
+
textDocument: {
|
|
238
|
+
uri,
|
|
239
|
+
languageId,
|
|
240
|
+
version,
|
|
241
|
+
text,
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Close a document in the LSP server
|
|
248
|
+
*/
|
|
249
|
+
closeDocument(uri: string): void {
|
|
250
|
+
this.notify('textDocument/didClose', {
|
|
251
|
+
textDocument: { uri },
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async #initialize(): Promise<void> {
|
|
256
|
+
const result = await this.request('initialize', {
|
|
257
|
+
processId: process.pid,
|
|
258
|
+
rootUri: this.#rootUri,
|
|
259
|
+
capabilities: {
|
|
260
|
+
textDocument: {
|
|
261
|
+
hover: { contentFormat: ['markdown', 'plaintext'] },
|
|
262
|
+
definition: { linkSupport: true },
|
|
263
|
+
references: {},
|
|
264
|
+
completion: {
|
|
265
|
+
completionItem: {
|
|
266
|
+
snippetSupport: true,
|
|
267
|
+
documentationFormat: ['markdown', 'plaintext'],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
signatureHelp: {
|
|
271
|
+
signatureInformation: {
|
|
272
|
+
documentationFormat: ['markdown', 'plaintext'],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
documentSymbol: {
|
|
276
|
+
hierarchicalDocumentSymbolSupport: true,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
workspace: {
|
|
280
|
+
symbol: { symbolKind: {} },
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
this.notify('initialized', {})
|
|
286
|
+
this.#initialized = true
|
|
287
|
+
|
|
288
|
+
return result as Promise<void>
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#send(message: JsonRpcRequest | JsonRpcNotification): void {
|
|
292
|
+
const stdin = this.#process?.stdin
|
|
293
|
+
if (!stdin || typeof stdin === 'number') {
|
|
294
|
+
throw new Error('LSP server stdin not available')
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const content = JSON.stringify(message)
|
|
298
|
+
const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`
|
|
299
|
+
|
|
300
|
+
stdin.write(header + content)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async #readOutput(): Promise<void> {
|
|
304
|
+
const stdout = this.#process?.stdout
|
|
305
|
+
if (!stdout || typeof stdout === 'number') return
|
|
306
|
+
|
|
307
|
+
const reader = stdout.getReader()
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
while (true) {
|
|
311
|
+
const { done, value } = await reader.read()
|
|
312
|
+
if (done) break
|
|
313
|
+
|
|
314
|
+
// Append new bytes to buffer
|
|
315
|
+
const newBuffer = new Uint8Array(this.#buffer.length + value.length)
|
|
316
|
+
newBuffer.set(this.#buffer)
|
|
317
|
+
newBuffer.set(value, this.#buffer.length)
|
|
318
|
+
this.#buffer = newBuffer
|
|
319
|
+
|
|
320
|
+
this.#processBuffer()
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
// Stream closed
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#processBuffer(): void {
|
|
328
|
+
const decoder = new TextDecoder()
|
|
329
|
+
|
|
330
|
+
while (true) {
|
|
331
|
+
// Parse header if we don't have content length yet
|
|
332
|
+
if (this.#contentLength === -1) {
|
|
333
|
+
// Look for header end sequence: \r\n\r\n
|
|
334
|
+
const headerEndIndex = this.#findHeaderEnd()
|
|
335
|
+
if (headerEndIndex === -1) break
|
|
336
|
+
|
|
337
|
+
const headerBytes = this.#buffer.slice(0, headerEndIndex)
|
|
338
|
+
const header = decoder.decode(headerBytes)
|
|
339
|
+
const match = header.match(/Content-Length: (\d+)/)
|
|
340
|
+
if (!match?.[1]) {
|
|
341
|
+
// Skip invalid header
|
|
342
|
+
this.#buffer = this.#buffer.slice(headerEndIndex + 4)
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
this.#contentLength = parseInt(match[1], 10)
|
|
347
|
+
this.#buffer = this.#buffer.slice(headerEndIndex + 4)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check if we have enough content (now comparing bytes to bytes)
|
|
351
|
+
if (this.#buffer.length < this.#contentLength) break
|
|
352
|
+
|
|
353
|
+
const contentBytes = this.#buffer.slice(0, this.#contentLength)
|
|
354
|
+
const content = decoder.decode(contentBytes)
|
|
355
|
+
this.#buffer = this.#buffer.slice(this.#contentLength)
|
|
356
|
+
this.#contentLength = -1
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
const message = JSON.parse(content) as JsonRpcResponse
|
|
360
|
+
this.#handleMessage(message)
|
|
361
|
+
} catch {
|
|
362
|
+
// Skip invalid JSON
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#findHeaderEnd(): number {
|
|
368
|
+
// Look for \r\n\r\n sequence in buffer
|
|
369
|
+
const CRLF = [13, 10, 13, 10] // \r\n\r\n
|
|
370
|
+
for (let i = 0; i <= this.#buffer.length - 4; i++) {
|
|
371
|
+
if (
|
|
372
|
+
this.#buffer[i] === CRLF[0] &&
|
|
373
|
+
this.#buffer[i + 1] === CRLF[1] &&
|
|
374
|
+
this.#buffer[i + 2] === CRLF[2] &&
|
|
375
|
+
this.#buffer[i + 3] === CRLF[3]
|
|
376
|
+
) {
|
|
377
|
+
return i
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return -1
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
#handleMessage(message: JsonRpcResponse): void {
|
|
384
|
+
if ('id' in message && message.id !== undefined) {
|
|
385
|
+
const pending = this.#pendingRequests.get(message.id)
|
|
386
|
+
if (pending) {
|
|
387
|
+
if (pending.timer) {
|
|
388
|
+
clearTimeout(pending.timer)
|
|
389
|
+
}
|
|
390
|
+
this.#pendingRequests.delete(message.id)
|
|
391
|
+
if (message.error) {
|
|
392
|
+
pending.reject(new Error(`LSP Error: ${message.error.message}`))
|
|
393
|
+
} else {
|
|
394
|
+
pending.resolve(message.result)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// Notifications and server requests are ignored for now
|
|
399
|
+
}
|
|
400
|
+
}
|
package/src/lsp-find.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Search for symbols across the workspace by name
|
|
4
|
+
*
|
|
5
|
+
* Usage: bun lsp-find.ts <query> [file]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parseArgs } from 'node:util'
|
|
9
|
+
import { LspClient } from './lsp-client.ts'
|
|
10
|
+
import { resolveFilePath } from './resolve-file-path.ts'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Find a default context file when none is provided
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* Checks common TypeScript entry points in order of preference
|
|
17
|
+
*/
|
|
18
|
+
const findDefaultContextFile = async (): Promise<string | null> => {
|
|
19
|
+
const candidates = [`${process.cwd()}/src/index.ts`, `${process.cwd()}/src/main.ts`, `${process.cwd()}/index.ts`]
|
|
20
|
+
for (const candidate of candidates) {
|
|
21
|
+
if (await Bun.file(candidate).exists()) {
|
|
22
|
+
return candidate
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Search for symbols across the workspace by name
|
|
30
|
+
*
|
|
31
|
+
* @param args - Command line arguments [query, file?]
|
|
32
|
+
*/
|
|
33
|
+
export const lspFind = async (args: string[]) => {
|
|
34
|
+
const { positionals } = parseArgs({
|
|
35
|
+
args,
|
|
36
|
+
allowPositionals: true,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const [query, filePath] = positionals
|
|
40
|
+
|
|
41
|
+
if (!query) {
|
|
42
|
+
console.error('Usage: lsp-find <query> [file]')
|
|
43
|
+
console.error(' query: Symbol name or partial name to search')
|
|
44
|
+
console.error(' file: Optional file to open for project context')
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const rootUri = `file://${process.cwd()}`
|
|
49
|
+
const client = new LspClient({ rootUri })
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await client.start()
|
|
53
|
+
|
|
54
|
+
// Open a file to establish project context if provided, otherwise find a default
|
|
55
|
+
const contextFile = filePath ? await resolveFilePath(filePath) : await findDefaultContextFile()
|
|
56
|
+
|
|
57
|
+
if (!contextFile) {
|
|
58
|
+
console.error('Error: No context file found.')
|
|
59
|
+
console.error('Provide a file path or ensure src/index.ts, src/main.ts, or index.ts exists.')
|
|
60
|
+
await client.stop()
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const file = Bun.file(contextFile)
|
|
65
|
+
if (!(await file.exists())) {
|
|
66
|
+
console.error(`Error: Context file not found: ${contextFile}`)
|
|
67
|
+
console.error('Workspace symbol search requires at least one open document.')
|
|
68
|
+
await client.stop()
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const text = await file.text()
|
|
73
|
+
const uri = `file://${contextFile}`
|
|
74
|
+
const languageId = contextFile.endsWith('.tsx')
|
|
75
|
+
? 'typescriptreact'
|
|
76
|
+
: contextFile.endsWith('.ts')
|
|
77
|
+
? 'typescript'
|
|
78
|
+
: contextFile.endsWith('.jsx')
|
|
79
|
+
? 'javascriptreact'
|
|
80
|
+
: 'javascript'
|
|
81
|
+
|
|
82
|
+
client.openDocument(uri, languageId, 1, text)
|
|
83
|
+
|
|
84
|
+
const result = await client.workspaceSymbols(query)
|
|
85
|
+
|
|
86
|
+
client.closeDocument(uri)
|
|
87
|
+
await client.stop()
|
|
88
|
+
|
|
89
|
+
console.log(JSON.stringify(result, null, 2))
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error(`Error: ${error}`)
|
|
92
|
+
await client.stop()
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Keep executable entry point for direct execution
|
|
98
|
+
if (import.meta.main) {
|
|
99
|
+
await lspFind(Bun.argv.slice(2))
|
|
100
|
+
}
|