opencode-vim 0.0.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.
- package/README.md +13 -0
- package/bun.lock +15 -0
- package/docs/configuration.md +403 -0
- package/package.json +12 -0
- package/src/modules/snippets/index.tsx +19 -0
- package/src/modules/snippets/loader.ts +147 -0
- package/src/modules/snippets/search.ts +118 -0
- package/src/modules/snippets/skill-loader.ts +81 -0
- package/src/modules/snippets/state.ts +40 -0
- package/src/modules/snippets/trigger.ts +60 -0
- package/src/modules/snippets/types.ts +49 -0
- package/src/modules/snippets/view.tsx +626 -0
- package/src/modules/vim/actions.ts +54 -0
- package/src/modules/vim/config.ts +119 -0
- package/src/modules/vim/index.tsx +96 -0
- package/src/modules/vim/keys.ts +34 -0
- package/src/modules/vim/log.ts +37 -0
- package/src/modules/vim/map.ts +159 -0
- package/src/modules/vim/state.ts +24 -0
- package/src/modules/vim/view.tsx +86 -0
- package/src/modules/vim/vimee.ts +364 -0
- package/src/plugin.tsx +69 -0
- package/src/prompt/host.tsx +38 -0
- package/src/prompt/modules.ts +16 -0
- package/src/prompt/ref.ts +17 -0
- package/src/prompt/root.tsx +56 -0
- package/src/prompt/types.ts +42 -0
- package/tsconfig.json +12 -0
- package/tui.json.example +37 -0
- package/tui.tsx +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# opencode-vim
|
|
2
|
+
|
|
3
|
+
Adds Vim-style insert and normal mode editing to the OpenCode prompt.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install from the CLI:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
opencode plugin opencode-vim@latest --global
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
See [docs/configuration.md](./docs/configuration.md) for configuration options and keymap examples.
|
package/bun.lock
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "opencode-vim",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@vimee/core": "^0.3.0",
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
"packages": {
|
|
13
|
+
"@vimee/core": ["@vimee/core@0.3.0", "", {}, "sha512-aC44fw6HhmjrbJMafAk3oUZIDpFbgNebbDsWwMIFD0WwnOUZPETgzU3HYQetmwz0e38JY3I/g8CJXRQjspgJZw=="],
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# Configuration Guide
|
|
2
|
+
|
|
3
|
+
This guide shows how to enable `opencode-vim` in OpenCode and configure its Vim prompt behavior.
|
|
4
|
+
|
|
5
|
+
## Basic Setup
|
|
6
|
+
|
|
7
|
+
Add `opencode-vim` to the `plugin` array in your OpenCode `tui.jsonc` file:
|
|
8
|
+
|
|
9
|
+
```jsonc
|
|
10
|
+
{
|
|
11
|
+
"$schema": "https://opencode.ai/tui.json",
|
|
12
|
+
"plugin": [
|
|
13
|
+
[
|
|
14
|
+
"./plugin/opencode-vim",
|
|
15
|
+
{
|
|
16
|
+
"vim": {
|
|
17
|
+
"defaultMode": "insert"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If your plugin is installed somewhere else, change the plugin path to match your setup.
|
|
26
|
+
|
|
27
|
+
## Full Example
|
|
28
|
+
|
|
29
|
+
```jsonc
|
|
30
|
+
{
|
|
31
|
+
"$schema": "https://opencode.ai/tui.json",
|
|
32
|
+
"plugin": [
|
|
33
|
+
[
|
|
34
|
+
"./plugin/opencode-vim",
|
|
35
|
+
{
|
|
36
|
+
"vim": {
|
|
37
|
+
"defaultMode": "insert",
|
|
38
|
+
"keymapTimeout": 500,
|
|
39
|
+
"pendingDisplayDelay": 120,
|
|
40
|
+
"cursorStyles": {
|
|
41
|
+
"insert": {
|
|
42
|
+
"style": "line",
|
|
43
|
+
"blinking": true
|
|
44
|
+
},
|
|
45
|
+
"normal": {
|
|
46
|
+
"style": "block",
|
|
47
|
+
"blinking": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"debug": false,
|
|
51
|
+
"debugPath": "/home/you/.cache/opencode/opencode-vim.log",
|
|
52
|
+
"keymaps": {
|
|
53
|
+
"insert": {
|
|
54
|
+
"kj": "normal"
|
|
55
|
+
},
|
|
56
|
+
"normal": {
|
|
57
|
+
"<CR>": "submit",
|
|
58
|
+
"Y": "y$"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Options
|
|
69
|
+
|
|
70
|
+
### `defaultMode`
|
|
71
|
+
|
|
72
|
+
The mode the prompt starts in.
|
|
73
|
+
|
|
74
|
+
Allowed values:
|
|
75
|
+
|
|
76
|
+
- `"insert"`
|
|
77
|
+
- `"normal"`
|
|
78
|
+
|
|
79
|
+
Default:
|
|
80
|
+
|
|
81
|
+
```jsonc
|
|
82
|
+
"defaultMode": "insert"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
|
|
87
|
+
```jsonc
|
|
88
|
+
"defaultMode": "normal"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `keymapTimeout`
|
|
92
|
+
|
|
93
|
+
How long, in milliseconds, the prompt waits for the next key when a configured keymap has only been partially typed.
|
|
94
|
+
|
|
95
|
+
Default:
|
|
96
|
+
|
|
97
|
+
```jsonc
|
|
98
|
+
"keymapTimeout": 500
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
|
|
103
|
+
```jsonc
|
|
104
|
+
"keymapTimeout": 250
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```jsonc
|
|
108
|
+
"keymapTimeout": 1000
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Use a shorter timeout for faster fallback after partial mappings. Use a longer timeout if you type multi-key mappings slowly.
|
|
112
|
+
|
|
113
|
+
### `pendingDisplayDelay`
|
|
114
|
+
|
|
115
|
+
How long, in milliseconds, the prompt waits before showing a pending key sequence in the status area.
|
|
116
|
+
|
|
117
|
+
Default:
|
|
118
|
+
|
|
119
|
+
```jsonc
|
|
120
|
+
"pendingDisplayDelay": 120
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Examples:
|
|
124
|
+
|
|
125
|
+
```jsonc
|
|
126
|
+
"pendingDisplayDelay": 0
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```jsonc
|
|
130
|
+
"pendingDisplayDelay": 300
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This only affects display. It does not change how long keymaps wait for more input.
|
|
134
|
+
|
|
135
|
+
### `cursorStyles`
|
|
136
|
+
|
|
137
|
+
The cursor style to use in each mode.
|
|
138
|
+
|
|
139
|
+
Allowed styles:
|
|
140
|
+
|
|
141
|
+
- `"block"`
|
|
142
|
+
- `"line"`
|
|
143
|
+
- `"underline"`
|
|
144
|
+
- `"default"`
|
|
145
|
+
|
|
146
|
+
Default:
|
|
147
|
+
|
|
148
|
+
```jsonc
|
|
149
|
+
"cursorStyles": {
|
|
150
|
+
"insert": {
|
|
151
|
+
"style": "line",
|
|
152
|
+
"blinking": true
|
|
153
|
+
},
|
|
154
|
+
"normal": {
|
|
155
|
+
"style": "block",
|
|
156
|
+
"blinking": true
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Examples:
|
|
162
|
+
|
|
163
|
+
```jsonc
|
|
164
|
+
"cursorStyles": {
|
|
165
|
+
"insert": {
|
|
166
|
+
"style": "line",
|
|
167
|
+
"blinking": false
|
|
168
|
+
},
|
|
169
|
+
"normal": {
|
|
170
|
+
"style": "block",
|
|
171
|
+
"blinking": false
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```jsonc
|
|
177
|
+
"cursorStyles": {
|
|
178
|
+
"insert": {
|
|
179
|
+
"style": "underline"
|
|
180
|
+
},
|
|
181
|
+
"normal": {
|
|
182
|
+
"style": "default"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
You can configure only one mode if you want. Any omitted values use the defaults.
|
|
188
|
+
|
|
189
|
+
### `debug`
|
|
190
|
+
|
|
191
|
+
Enables debug logging.
|
|
192
|
+
|
|
193
|
+
Default:
|
|
194
|
+
|
|
195
|
+
```jsonc
|
|
196
|
+
"debug": false
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Example:
|
|
200
|
+
|
|
201
|
+
```jsonc
|
|
202
|
+
"debug": true
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
You can also enable debug logging with this environment variable:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
VIM_PROMPT_DEBUG=1
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### `debugPath`
|
|
212
|
+
|
|
213
|
+
The file path used for debug logs when debug logging is enabled.
|
|
214
|
+
|
|
215
|
+
Default:
|
|
216
|
+
|
|
217
|
+
```txt
|
|
218
|
+
~/.cache/opencode/opencode-vim.log
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Example:
|
|
222
|
+
|
|
223
|
+
```jsonc
|
|
224
|
+
"debugPath": "/tmp/opencode-vim.log"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Use an absolute path in config. `~` is not expanded inside `debugPath`.
|
|
228
|
+
|
|
229
|
+
### `keymaps`
|
|
230
|
+
|
|
231
|
+
Custom keymaps for insert mode and normal mode.
|
|
232
|
+
|
|
233
|
+
Allowed modes:
|
|
234
|
+
|
|
235
|
+
- `"insert"`
|
|
236
|
+
- `"normal"`
|
|
237
|
+
|
|
238
|
+
Each keymap entry maps a key sequence to an action:
|
|
239
|
+
|
|
240
|
+
```jsonc
|
|
241
|
+
"keymaps": {
|
|
242
|
+
"insert": {
|
|
243
|
+
"kj": "normal"
|
|
244
|
+
},
|
|
245
|
+
"normal": {
|
|
246
|
+
"<CR>": "submit"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Supported built-in actions:
|
|
252
|
+
|
|
253
|
+
- `"normal"` exits insert mode and enters normal mode.
|
|
254
|
+
- `"insert"` enters insert mode.
|
|
255
|
+
- `"submit"` submits the OpenCode prompt.
|
|
256
|
+
|
|
257
|
+
Any other action string is treated as a Vim key sequence. For example, this maps `Y` to yank from the cursor to the end of the line:
|
|
258
|
+
|
|
259
|
+
```jsonc
|
|
260
|
+
"keymaps": {
|
|
261
|
+
"normal": {
|
|
262
|
+
"Y": "y$"
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Keymap Syntax
|
|
268
|
+
|
|
269
|
+
Key sequences can contain printable ASCII characters, except literal spaces. Use `<Space>` for the space key.
|
|
270
|
+
|
|
271
|
+
Examples:
|
|
272
|
+
|
|
273
|
+
```jsonc
|
|
274
|
+
"x": "d"
|
|
275
|
+
"gg": "0"
|
|
276
|
+
"Y": "y$"
|
|
277
|
+
"\\r": "<C-r>"
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Supported special keys:
|
|
281
|
+
|
|
282
|
+
- `<Esc>`
|
|
283
|
+
- `<CR>`
|
|
284
|
+
- `<Tab>`
|
|
285
|
+
- `<BS>`
|
|
286
|
+
- `<Del>`
|
|
287
|
+
- `<Space>`
|
|
288
|
+
- `<C-a>` through `<C-z>`
|
|
289
|
+
|
|
290
|
+
Ctrl key names must be lowercase. Use `<C-s>`, not `<C-S>`.
|
|
291
|
+
|
|
292
|
+
Unsupported examples:
|
|
293
|
+
|
|
294
|
+
```jsonc
|
|
295
|
+
"<C-S>": "submit"
|
|
296
|
+
"<C-1>": "submit"
|
|
297
|
+
"<Up>": "k"
|
|
298
|
+
"a b": "normal"
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Invalid keymaps are skipped. Enable debug logging if you need to troubleshoot keymap registration.
|
|
302
|
+
|
|
303
|
+
## Keymap Examples
|
|
304
|
+
|
|
305
|
+
Use `kj` or `jk` to leave insert mode:
|
|
306
|
+
|
|
307
|
+
```jsonc
|
|
308
|
+
"keymaps": {
|
|
309
|
+
"insert": {
|
|
310
|
+
"kj": "normal",
|
|
311
|
+
"jk": "normal"
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Submit the prompt with Enter in normal mode:
|
|
317
|
+
|
|
318
|
+
```jsonc
|
|
319
|
+
"keymaps": {
|
|
320
|
+
"normal": {
|
|
321
|
+
"<CR>": "submit"
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Submit the prompt with Ctrl-S in insert mode:
|
|
327
|
+
|
|
328
|
+
```jsonc
|
|
329
|
+
"keymaps": {
|
|
330
|
+
"insert": {
|
|
331
|
+
"<C-s>": "submit"
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Make `Y` yank to the end of the line:
|
|
337
|
+
|
|
338
|
+
```jsonc
|
|
339
|
+
"keymaps": {
|
|
340
|
+
"normal": {
|
|
341
|
+
"Y": "y$"
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Make `D` delete to the beginning of the line:
|
|
347
|
+
|
|
348
|
+
```jsonc
|
|
349
|
+
"keymaps": {
|
|
350
|
+
"normal": {
|
|
351
|
+
"D": "d0"
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Make `H` move to the beginning and `L` move to the end:
|
|
357
|
+
|
|
358
|
+
```jsonc
|
|
359
|
+
"keymaps": {
|
|
360
|
+
"normal": {
|
|
361
|
+
"H": "0",
|
|
362
|
+
"L": "$"
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Use `q` to enter insert mode from normal mode:
|
|
368
|
+
|
|
369
|
+
```jsonc
|
|
370
|
+
"keymaps": {
|
|
371
|
+
"normal": {
|
|
372
|
+
"q": "insert"
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Use a leader-style sequence:
|
|
378
|
+
|
|
379
|
+
```jsonc
|
|
380
|
+
"keymaps": {
|
|
381
|
+
"normal": {
|
|
382
|
+
"\\s": "submit",
|
|
383
|
+
"\\r": "<C-r>"
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Troubleshooting
|
|
389
|
+
|
|
390
|
+
If a keymap does not work, check these first:
|
|
391
|
+
|
|
392
|
+
- The mode is either `insert` or `normal`.
|
|
393
|
+
- The key sequence does not contain a literal space.
|
|
394
|
+
- Special keys use one of the supported names exactly.
|
|
395
|
+
- Ctrl keys use lowercase letters, such as `<C-s>`.
|
|
396
|
+
- The action string is not empty.
|
|
397
|
+
|
|
398
|
+
To debug configuration problems, enable logging:
|
|
399
|
+
|
|
400
|
+
```jsonc
|
|
401
|
+
"debug": true,
|
|
402
|
+
"debugPath": "/tmp/opencode-vim.log"
|
|
403
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { PromptModule } from "../../prompt/types"
|
|
3
|
+
import type { SnippetController } from "./types"
|
|
4
|
+
import { SnippetAutocomplete } from "./view"
|
|
5
|
+
|
|
6
|
+
export function createSnippetsModule(): PromptModule {
|
|
7
|
+
const controller: SnippetController = {}
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
id: "snippets",
|
|
11
|
+
order: -10,
|
|
12
|
+
onSubmit() {
|
|
13
|
+
return controller.accept?.() === true
|
|
14
|
+
},
|
|
15
|
+
renderAbove(ctx) {
|
|
16
|
+
return <SnippetAutocomplete ctx={ctx} controller={controller} />
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { basename, join } from "node:path"
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises"
|
|
3
|
+
import type { SnippetInfo, SnippetSource } from "./types"
|
|
4
|
+
|
|
5
|
+
const SNIPPET_EXT = ".md"
|
|
6
|
+
const EMPTY_SNIPPET = `---
|
|
7
|
+
description: ""
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
`
|
|
11
|
+
|
|
12
|
+
export async function loadSnippets(projectDir?: string) {
|
|
13
|
+
const registry = new Map<string, SnippetInfo>()
|
|
14
|
+
const home = process.env.HOME
|
|
15
|
+
|
|
16
|
+
if (home) {
|
|
17
|
+
await loadDir(registry, join(home, ".config/opencode/snippets"), "global")
|
|
18
|
+
await loadDir(registry, join(home, ".config/opencode/snippet"), "global")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (projectDir) {
|
|
22
|
+
await loadDir(registry, join(projectDir, ".opencode/snippets"), "project")
|
|
23
|
+
await loadDir(registry, join(projectDir, ".opencode/snippet"), "project")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return listSnippets(registry).sort((left, right) => sourceRank(left) - sourceRank(right) || left.name.localeCompare(right.name))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function loadDir(registry: Map<string, SnippetInfo>, dir: string, source: SnippetSource) {
|
|
30
|
+
let entries: string[]
|
|
31
|
+
try {
|
|
32
|
+
entries = await Array.fromAsync(new Bun.Glob(`*${SNIPPET_EXT}`).scan({ cwd: dir, onlyFiles: true }))
|
|
33
|
+
} catch {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const snippet = await loadFile(join(dir, entry), source)
|
|
39
|
+
if (snippet) registerSnippet(registry, snippet)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function loadFile(filePath: string, source: SnippetSource) {
|
|
44
|
+
let raw: string
|
|
45
|
+
try {
|
|
46
|
+
raw = await Bun.file(filePath).text()
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const parsed = parseFrontmatter(raw)
|
|
52
|
+
return {
|
|
53
|
+
name: basename(filePath, SNIPPET_EXT),
|
|
54
|
+
content: parsed.content.trim(),
|
|
55
|
+
aliases: normalizeAliases(parsed.data.aliases ?? parsed.data.alias),
|
|
56
|
+
description: typeof parsed.data.description === "string" ? parsed.data.description : undefined,
|
|
57
|
+
filePath,
|
|
58
|
+
source,
|
|
59
|
+
} satisfies SnippetInfo
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function ensureSnippetDraft(name: string, projectDir?: string) {
|
|
63
|
+
const dir = await ensureSnippetsDir(projectDir)
|
|
64
|
+
const filePath = join(dir, `${name}${SNIPPET_EXT}`)
|
|
65
|
+
if (!(await Bun.file(filePath).exists())) await writeFile(filePath, EMPTY_SNIPPET)
|
|
66
|
+
return filePath
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function ensureSnippetsDir(projectDir?: string) {
|
|
70
|
+
const home = process.env.HOME
|
|
71
|
+
const dir = projectDir ? join(projectDir, ".opencode/snippet") : home ? join(home, ".config/opencode/snippet") : undefined
|
|
72
|
+
if (!dir) throw new Error("Unable to resolve snippets directory")
|
|
73
|
+
await mkdir(dir, { recursive: true })
|
|
74
|
+
return dir
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function parseFrontmatter(raw: string) {
|
|
78
|
+
if (!raw.startsWith("---\n")) return { data: {} as Record<string, unknown>, content: raw }
|
|
79
|
+
|
|
80
|
+
const end = raw.indexOf("\n---", 4)
|
|
81
|
+
if (end < 0) return { data: {} as Record<string, unknown>, content: raw }
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
data: parseYamlish(raw.slice(4, end)),
|
|
85
|
+
content: raw.slice(end + 4).replace(/^\r?\n/, ""),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseYamlish(input: string) {
|
|
90
|
+
const data: Record<string, unknown> = {}
|
|
91
|
+
const lines = input.split(/\r?\n/)
|
|
92
|
+
let currentList: string | undefined
|
|
93
|
+
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
const listItem = /^\s*-\s*(.+)$/.exec(line)
|
|
96
|
+
if (listItem && currentList) {
|
|
97
|
+
const value = String(listItem[1]).trim()
|
|
98
|
+
data[currentList] = [...asArray(data[currentList]), unquote(value)]
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const pair = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line)
|
|
103
|
+
if (!pair) continue
|
|
104
|
+
|
|
105
|
+
currentList = undefined
|
|
106
|
+
const key = pair[1]
|
|
107
|
+
const value = pair[2].trim()
|
|
108
|
+
if (!value) {
|
|
109
|
+
data[key] = []
|
|
110
|
+
currentList = key
|
|
111
|
+
} else {
|
|
112
|
+
data[key] = unquote(value)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return data
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function normalizeAliases(value: unknown) {
|
|
120
|
+
return asArray(value).filter((entry) => typeof entry === "string" && entry.length > 0) as string[]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function asArray(value: unknown) {
|
|
124
|
+
return Array.isArray(value) ? value : typeof value === "string" ? [value] : []
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function unquote(value: string) {
|
|
128
|
+
return value.replace(/^['"]|['"]$/g, "")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function registerSnippet(registry: Map<string, SnippetInfo>, snippet: SnippetInfo) {
|
|
132
|
+
const old = registry.get(snippet.name.toLowerCase())
|
|
133
|
+
if (old) {
|
|
134
|
+
for (const alias of old.aliases) registry.delete(alias.toLowerCase())
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
registry.set(snippet.name.toLowerCase(), snippet)
|
|
138
|
+
for (const alias of snippet.aliases) registry.set(alias.toLowerCase(), snippet)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function listSnippets(registry: Map<string, SnippetInfo>) {
|
|
142
|
+
return [...new Map([...registry.values()].map((snippet) => [snippet.name, snippet])).values()]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sourceRank(snippet: SnippetInfo) {
|
|
146
|
+
return snippet.source === "project" ? 0 : 1
|
|
147
|
+
}
|