opencode-vim 0.0.23 → 0.0.24

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 (38) hide show
  1. package/README.md +11 -0
  2. package/dist/tui.js +3326 -0
  3. package/package.json +6 -6
  4. package/src/clipboard.tsx +0 -50
  5. package/src/modules/snippets/index.tsx +0 -17
  6. package/src/modules/snippets/loader.ts +0 -147
  7. package/src/modules/snippets/search.ts +0 -118
  8. package/src/modules/snippets/skill-loader.ts +0 -81
  9. package/src/modules/snippets/state.ts +0 -40
  10. package/src/modules/snippets/trigger.ts +0 -68
  11. package/src/modules/snippets/types.ts +0 -50
  12. package/src/modules/snippets/view.tsx +0 -674
  13. package/src/modules/vim/actions.ts +0 -82
  14. package/src/modules/vim/config.ts +0 -137
  15. package/src/modules/vim/edit.test.ts +0 -51
  16. package/src/modules/vim/edit.ts +0 -33
  17. package/src/modules/vim/graphemes.ts +0 -38
  18. package/src/modules/vim/index.tsx +0 -200
  19. package/src/modules/vim/keys.ts +0 -38
  20. package/src/modules/vim/log.ts +0 -37
  21. package/src/modules/vim/map.test.ts +0 -45
  22. package/src/modules/vim/map.ts +0 -90
  23. package/src/modules/vim/state.test.ts +0 -18
  24. package/src/modules/vim/state.tsx +0 -46
  25. package/src/modules/vim/view.tsx +0 -130
  26. package/src/modules/vim/vimee.test.ts +0 -211
  27. package/src/modules/vim/vimee.ts +0 -1211
  28. package/src/plugin.tsx +0 -116
  29. package/src/prompt/host.tsx +0 -38
  30. package/src/prompt/modules.ts +0 -16
  31. package/src/prompt/root.tsx +0 -69
  32. package/src/prompt/types.ts +0 -42
  33. package/src/session.tsx +0 -291
  34. package/src/transcript-items.tsx +0 -168
  35. package/src/transcript.tsx +0 -179
  36. package/src/update.ts +0 -120
  37. package/tui.tsx +0 -340
  38. package/view.tsx +0 -29
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-vim",
4
- "version": "0.0.23",
4
+ "version": "0.0.24",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/Tarquinen/opencode-vim"
9
9
  },
10
10
  "exports": {
11
- "./tui": "./tui.tsx"
11
+ "./tui": "./dist/tui.js"
12
12
  },
13
13
  "scripts": {
14
+ "build": "bun scripts/build.ts",
15
+ "prepack": "bun run build",
14
16
  "typecheck": "tsc --noEmit",
15
17
  "test": "bun test --conditions=browser --preload @opentui/solid/preload",
16
18
  "bench": "bun test/benchmark.ts"
@@ -33,9 +35,7 @@
33
35
  "files": [
34
36
  "assets/",
35
37
  "docs/",
36
- "src/",
37
- "README.md",
38
- "tui.tsx",
39
- "view.tsx"
38
+ "dist/",
39
+ "README.md"
40
40
  ]
41
41
  }
package/src/clipboard.tsx DELETED
@@ -1,50 +0,0 @@
1
- // .tsx lets OpenCode's runtime loader supply its shared OpenTUI imports.
2
- import { createClipboard, createHostClipboard, createRendererClipboardAdapter, type RendererClipboardBoundary } from "@opentui/core"
3
-
4
- export type VimClipboard = ReturnType<typeof createVimClipboard>
5
-
6
- export function createVimClipboard(renderer: RendererClipboardBoundary) {
7
- const terminal = createRendererClipboardAdapter(renderer)
8
- const clipboard = createClipboard({ host: createHostClipboard(), terminal })
9
- let fallback = ""
10
- let revision = 0
11
- let writes = Promise.resolve()
12
- let disposed = false
13
-
14
- return {
15
- write(text: string): Promise<boolean> {
16
- text = text.replaceAll("\0", "")
17
- fallback = text
18
- revision++
19
- // Keep rapid cuts in order, and let an immediate p wait for the last write.
20
- const result = writes.then(async () => {
21
- if (disposed) return false
22
- try {
23
- const result = await clipboard.writeText(text, { destination: "all-available", selection: "clipboard" })
24
- return result.host.status === "written" || result.terminal.status === "attempted"
25
- } catch { return false }
26
- })
27
- writes = result.then(() => {})
28
- return result
29
- },
30
- async read(): Promise<string> {
31
- await writes
32
- // OSC52 supports copying, but OpenTUI cannot read the client's clipboard.
33
- if (disposed || terminal.remote) return fallback
34
- const before = revision
35
- try {
36
- const result = await clipboard.read({ preferredTypes: ["text/plain"], selection: "clipboard" })
37
- if (before !== revision) return fallback
38
- if (result.status === "empty") fallback = ""
39
- else if (result.status === "read" && result.representation.mimeType === "text/plain") {
40
- fallback = new TextDecoder().decode(result.representation.bytes).replace(/\r\n?/g, "\n").replaceAll("\0", "")
41
- }
42
- } catch {}
43
- return fallback
44
- },
45
- dispose() {
46
- disposed = true
47
- return clipboard.dispose()
48
- },
49
- }
50
- }
@@ -1,17 +0,0 @@
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(controller: SnippetController): PromptModule {
7
- return {
8
- id: "snippets",
9
- order: -10,
10
- onSubmit() {
11
- return controller.accept?.() === true
12
- },
13
- renderAbove(ctx) {
14
- return <SnippetAutocomplete ctx={ctx} controller={controller} />
15
- },
16
- }
17
- }
@@ -1,147 +0,0 @@
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
- }
@@ -1,118 +0,0 @@
1
- import type { SkillInfo, SnippetInfo } from "./types"
2
-
3
- export type HighlightPart = {
4
- text: string
5
- match: boolean
6
- }
7
-
8
- function normalizeSearchText(input: string) {
9
- return input.toLowerCase().replace(/[^a-z0-9]/g, "")
10
- }
11
-
12
- function scoreText(input: string, query: string) {
13
- const raw = input.toLowerCase()
14
- const compact = normalizeSearchText(input)
15
- const needle = query.toLowerCase().trim()
16
- const compactNeedle = normalizeSearchText(query)
17
-
18
- if (raw === needle) return 0
19
- if (compactNeedle && compact === compactNeedle) return 1
20
- if (raw.startsWith(needle)) return 2
21
- if (compactNeedle && compact.startsWith(compactNeedle)) return 3
22
- if (raw.includes(needle)) return 4
23
- if (compactNeedle && compact.includes(compactNeedle)) return 5
24
- return Number.POSITIVE_INFINITY
25
- }
26
-
27
- function snippetDescription(snippet: SnippetInfo) {
28
- return (snippet.description || snippet.content).replace(/\s+/g, " ").trim()
29
- }
30
-
31
- function scoreSnippet(snippet: SnippetInfo, query: string) {
32
- if (!query) return 0
33
-
34
- const nameScore = Math.min(scoreText(snippet.name, query), ...snippet.aliases.map((alias) => scoreText(alias, query)))
35
- if (Number.isFinite(nameScore)) return nameScore
36
-
37
- const description = (snippet.description || "").replace(/\s+/g, " ").trim().toLowerCase()
38
- const lowerQuery = query.toLowerCase()
39
- if (description.startsWith(lowerQuery)) return 6
40
- if (description.includes(lowerQuery)) return 7
41
- return Number.POSITIVE_INFINITY
42
- }
43
-
44
- function sourceRank(snippet: SnippetInfo) {
45
- return snippet.source === "project" ? 0 : 1
46
- }
47
-
48
- export function filterSnippets(snippets: SnippetInfo[], query: string) {
49
- const needle = query.trim()
50
- return snippets
51
- .map((snippet) => ({ snippet, score: scoreSnippet(snippet, needle) }))
52
- .filter((entry) => Number.isFinite(entry.score))
53
- .sort((left, right) => left.score - right.score || sourceRank(left.snippet) - sourceRank(right.snippet) || left.snippet.name.localeCompare(right.snippet.name))
54
- .map((entry) => entry.snippet)
55
- }
56
-
57
- export function matchedAliases(snippet: SnippetInfo, query: string) {
58
- const needle = query.trim()
59
- if (!needle) return []
60
- return snippet.aliases.filter((alias) => Number.isFinite(scoreText(alias, needle)))
61
- }
62
-
63
- export function describeSnippet(snippet: SnippetInfo) {
64
- return snippetDescription(snippet)
65
- }
66
-
67
- export function highlightMatches(input: string, query: string): HighlightPart[] {
68
- const needle = query.trim().toLowerCase()
69
- if (!needle) return [{ text: input, match: false }]
70
-
71
- const lower = input.toLowerCase()
72
- const parts: HighlightPart[] = []
73
- let cursor = 0
74
-
75
- while (cursor < input.length) {
76
- const index = lower.indexOf(needle, cursor)
77
- if (index < 0) break
78
- if (index > cursor) parts.push({ text: input.slice(cursor, index), match: false })
79
- parts.push({ text: input.slice(index, index + needle.length), match: true })
80
- cursor = index + needle.length
81
- }
82
-
83
- if (parts.length === 0) return [{ text: input, match: false }]
84
- if (cursor < input.length) parts.push({ text: input.slice(cursor), match: false })
85
- return parts
86
- }
87
-
88
- function skillTag(skill: SkillInfo) {
89
- return `skill(${skill.name})`
90
- }
91
-
92
- function scoreSkill(skill: SkillInfo, query: string) {
93
- if (!query) return 0
94
- const nameScore = Math.min(scoreText(skill.name, query), scoreText(skillTag(skill), query))
95
- if (Number.isFinite(nameScore)) return nameScore
96
- const description = (skill.description || "").replace(/\s+/g, " ").trim().toLowerCase()
97
- const lowerQuery = query.toLowerCase()
98
- if (description.startsWith(lowerQuery)) return 6
99
- if (description.includes(lowerQuery)) return 7
100
- return Number.POSITIVE_INFINITY
101
- }
102
-
103
- function skillSourceRank(skill: SkillInfo) {
104
- return skill.source === "project" ? 0 : 1
105
- }
106
-
107
- export function filterSkills(skills: SkillInfo[], query: string) {
108
- const needle = query.trim()
109
- return skills
110
- .map((skill) => ({ skill, score: scoreSkill(skill, needle) }))
111
- .filter((entry) => Number.isFinite(entry.score))
112
- .sort((left, right) => left.score - right.score || skillSourceRank(left.skill) - skillSourceRank(right.skill) || left.skill.name.localeCompare(right.skill.name))
113
- .map((entry) => entry.skill)
114
- }
115
-
116
- export function describeSkill(skill: SkillInfo) {
117
- return (skill.description || skill.content).replace(/\s+/g, " ").trim()
118
- }
@@ -1,81 +0,0 @@
1
- import { dirname, join, parse } from "node:path"
2
- import type { SkillInfo, SnippetSource } from "./types"
3
- import { parseFrontmatter } from "./loader"
4
-
5
- const SKILL_FILE = "SKILL.md"
6
-
7
- export async function loadSkills(projectDir?: string) {
8
- const registry = new Map<string, SkillInfo>()
9
- const home = process.env.HOME
10
-
11
- if (home) {
12
- await loadSkillDir(registry, join(home, ".config/opencode/skill"), "global")
13
- await loadSkillDir(registry, join(home, ".config/opencode/skills"), "global")
14
- await loadSkillDir(registry, join(home, ".claude/skills"), "global")
15
- await loadSkillDir(registry, join(home, ".agents/skills"), "global")
16
- }
17
-
18
- for (const root of await projectRoots(projectDir)) {
19
- await loadSkillDir(registry, join(root, ".opencode/skill"), "project")
20
- await loadSkillDir(registry, join(root, ".opencode/skills"), "project")
21
- await loadSkillDir(registry, join(root, ".claude/skills"), "project")
22
- await loadSkillDir(registry, join(root, ".agents/skills"), "project")
23
- }
24
-
25
- return [...registry.values()].sort((left, right) => sourceRank(left) - sourceRank(right) || left.name.localeCompare(right.name))
26
- }
27
-
28
- async function projectRoots(projectDir?: string) {
29
- if (!projectDir) return []
30
- const roots: string[] = []
31
- let current = projectDir
32
-
33
- while (true) {
34
- roots.push(current)
35
- if (await Bun.file(join(current, ".git")).exists()) break
36
- const parent = dirname(current)
37
- if (parent === current) break
38
- current = parent
39
- }
40
-
41
- return roots.reverse()
42
- }
43
-
44
- async function loadSkillDir(registry: Map<string, SkillInfo>, dir: string, source: SnippetSource) {
45
- let entries: string[]
46
- try {
47
- entries = await Array.fromAsync(new Bun.Glob(`*/${SKILL_FILE}`).scan({ cwd: dir, onlyFiles: true }))
48
- } catch {
49
- return
50
- }
51
-
52
- for (const entry of entries) {
53
- const skill = await loadSkill(join(dir, entry), source)
54
- if (skill) registry.set(skill.name.toLowerCase(), skill)
55
- }
56
- }
57
-
58
- async function loadSkill(filePath: string, source: SnippetSource) {
59
- let raw: string
60
- try {
61
- raw = await Bun.file(filePath).text()
62
- } catch {
63
- return undefined
64
- }
65
-
66
- const parsed = parseFrontmatter(raw)
67
- const folderName = parse(dirname(filePath)).base
68
- const name = typeof parsed.data.name === "string" && parsed.data.name.trim() ? parsed.data.name.trim() : folderName
69
-
70
- return {
71
- name,
72
- content: parsed.content.trim(),
73
- description: typeof parsed.data.description === "string" ? parsed.data.description : undefined,
74
- filePath,
75
- source,
76
- } satisfies SkillInfo
77
- }
78
-
79
- function sourceRank(skill: SkillInfo) {
80
- return skill.source === "project" ? 0 : 1
81
- }
@@ -1,40 +0,0 @@
1
- import { dirname, join } from "node:path"
2
- import { mkdir, writeFile } from "node:fs/promises"
3
-
4
- const SCOPE_GLOBAL = "__global__"
5
-
6
- export async function addPendingDraft(workspaceDir: string | undefined, name: string) {
7
- const filePath = statePath("pending-drafts.json")
8
- const state = await readState(filePath)
9
- const scope = workspaceDir || SCOPE_GLOBAL
10
- const current = Array.isArray(state[scope]) ? state[scope].filter((item): item is string => typeof item === "string") : []
11
- const next = new Set([...current, name.toLowerCase()])
12
- state[scope] = [...next].sort()
13
- await writeState(filePath, state)
14
- }
15
-
16
- export async function markSnippetReloadRequested(workspaceDir?: string) {
17
- const filePath = statePath("snippet-reload.json")
18
- const state = await readState(filePath)
19
- state[workspaceDir || SCOPE_GLOBAL] = Date.now()
20
- await writeState(filePath, state)
21
- }
22
-
23
- function statePath(name: string) {
24
- const home = process.env.HOME
25
- if (!home) throw new Error("HOME is not set")
26
- return join(home, ".config/opencode/state", name)
27
- }
28
-
29
- async function readState(filePath: string) {
30
- try {
31
- const parsed = JSON.parse(await Bun.file(filePath).text())
32
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record<string, unknown>
33
- } catch {}
34
- return {} as Record<string, unknown>
35
- }
36
-
37
- async function writeState(filePath: string, state: Record<string, unknown>) {
38
- await mkdir(dirname(filePath), { recursive: true })
39
- await writeFile(filePath, `${JSON.stringify(state, null, 2)}\n`)
40
- }
@@ -1,68 +0,0 @@
1
- import type { HashtagTriggerMatch, SnippetInfo } from "./types"
2
-
3
- export function findTrailingHashtagTrigger(input: string): HashtagTriggerMatch | undefined {
4
- let start = -1
5
- for (let i = input.length - 1; i >= 0; i--) {
6
- const char = input[i]
7
- if (char === "#") {
8
- start = i
9
- break
10
- }
11
- if (/\s/.test(char)) return undefined
12
- }
13
-
14
- if (start < 0) return undefined
15
- if (start > 0 && !/\s/.test(input[start - 1])) return undefined
16
-
17
- const query = input.slice(start + 1)
18
- const token = `#${query}`
19
-
20
- return {
21
- start,
22
- end: input.length,
23
- query,
24
- token,
25
- }
26
- }
27
-
28
- export function replaceTrailingHashtag(input: string, name: string) {
29
- const match = findTrailingHashtagTrigger(input)
30
- if (!match) return input
31
- return `${input.slice(0, match.start)}#${name} `
32
- }
33
-
34
- export function insertSnippetTag(input: string, name: string) {
35
- const match = findTrailingHashtagTrigger(input)
36
- if (match) return replaceTrailingHashtag(input, name)
37
- const separator = input.length === 0 || /\s$/.test(input) ? "" : " "
38
- return `${input}${separator}#${name} `
39
- }
40
-
41
- export function insertSkillLoad(input: string, name: string) {
42
- const tag = `skill(${name})`
43
- const match = findTrailingHashtagTrigger(input)
44
- if (match) return replaceTrailingHashtag(input, tag)
45
- const separator = input.length === 0 || /\s$/.test(input) ? "" : " "
46
- return `${input}${separator}#${tag} `
47
- }
48
-
49
- export function preferredSnippetTag(input: string, snippet: SnippetInfo) {
50
- const query = findTrailingHashtagTrigger(input)?.query
51
- if (query && snippet.aliases.some((alias) => alias === query)) return query
52
- return snippet.name
53
- }
54
-
55
- export function insertSnippetTrigger(input: string) {
56
- if (findTrailingHashtagTrigger(input)) return input
57
- const separator = input.length === 0 || /\s$/.test(input) ? "" : " "
58
- return `${input}${separator}#`
59
- }
60
-
61
- export function isReloadCommand(input: string) {
62
- return input.trim() === "/snippets:reload"
63
- }
64
-
65
- export function stepSelection(current: number, total: number, delta: number) {
66
- if (total <= 0) return 0
67
- return Math.max(0, Math.min(total - 1, current + delta))
68
- }
@@ -1,50 +0,0 @@
1
- export type SnippetSource = "global" | "project"
2
-
3
- export type SnippetInfo = {
4
- name: string
5
- content: string
6
- aliases: string[]
7
- description?: string
8
- filePath: string
9
- source: SnippetSource
10
- }
11
-
12
- export type SkillInfo = {
13
- name: string
14
- content: string
15
- description?: string
16
- filePath: string
17
- source: SnippetSource
18
- }
19
-
20
- export type AutocompleteItem =
21
- | {
22
- kind: "snippet"
23
- id: string
24
- label: string
25
- description: string
26
- aliases: string[]
27
- snippet: SnippetInfo
28
- }
29
- | {
30
- kind: "skill"
31
- id: string
32
- label: string
33
- description: string
34
- aliases: string[]
35
- skill: SkillInfo
36
- }
37
-
38
- export type HashtagTriggerMatch = {
39
- start: number
40
- end: number
41
- query: string
42
- token: string
43
- }
44
-
45
- export type SnippetController = {
46
- accept?: () => boolean
47
- reload?: () => void
48
- insertTrigger?: () => void
49
- navigate?: (delta: number) => boolean
50
- }