completeInScratch
uses tcmts.studio.editableCodebase, which uses tcmts.share.servedCodebase, which uses process
Completions at offset in a scratch file (text being what it holds; trigger, the character typed that asked,
a "." say): TypeScript's, from its language service, and after them the codebase's names the word begins (where a
name, not a member, goes), each with the import it needs. From where the word they complete starts.
import { analyzedScratch, editableCodebase, importEdit, scratchPath } from "#codebase/tcmts/studio"; import type { ScratchCompletion } from "#codebase/tcmts/studio"; export async function completeInScratch( file: string, text: string, offset: number, trigger?: string, ): Promise<{ from: number; options: ScratchCompletion[] }> { const cb = editableCodebase(); const abs = scratchPath(cb, file); let from = offset; while (from > 0 && /[\w$]/.test(text[from - 1]!)) from--; const word = text.slice(from, offset); const a = analyzedScratch(cb, abs, text); let entries: readonly { name: string; kind?: number; sortText?: string; insertText?: string; detail?: string; labelDetails?: { description?: string; detail?: string }; }[] = []; try { entries = a.service?.getCompletionsAtPosition(abs, offset, trigger ? { triggerCharacter: trigger } : {}) ?.entries ?? []; } catch { entries = []; } // (the language server's kinds, as the editor's icons are named) const KINDS: Record<number, string> = { 2: "method", 3: "function", 4: "function", 5: "property", 6: "variable", 7: "class", 8: "interface", 9: "namespace", 10: "property", 13: "enum", 14: "keyword", 20: "enum", 21: "constant", 22: "class", 25: "type", }; const ts = [...entries] .sort( (x, y) => (x.sortText ?? "").localeCompare(y.sortText ?? "") || x.name.localeCompare(y.name), ) .slice(0, 500); const options: ScratchCompletion[] = ts.map((e, i) => { const detail = e.labelDetails?.description ?? e.detail; return { label: e.name, ...(e.kind && KINDS[e.kind] ? { type: KINDS[e.kind] } : {}), ...(detail ? { detail } : {}), boost: Math.max(-99, -Math.floor(i / 50)), ...(e.insertText && e.insertText !== e.name ? { apply: e.insertText } : {}), }; }); // the codebase's names: not after a dot (a member goes there), not one the file has in scope already if (word && text[from - 1] !== "." && trigger !== ".") { const have = new Set(ts.map((e) => e.name)); const lower = word.toLowerCase(); let added = 0; for (const n of cb.store.names()) { const dot = n.path.lastIndexOf("."); const leaf = n.path.slice(dot + 1); if ( !leaf.toLowerCase().startsWith(lower) || have.has(leaf) || !/^[A-Za-z_$][\w$]*$/.test(leaf) ) continue; const rec = cb.store.getDef(n.hash); if (!rec || rec.kind === "file") continue; const ns = dot > 0 ? n.path.slice(0, dot) : ""; const imports = importEdit( text, ns ? `#codebase/${ns.replaceAll(".", "/")}` : "#codebase", leaf, rec.kind === "type", ); options.push({ label: leaf, type: rec.kind === "type" ? "type" : rec.kind === "class" ? "class" : /^(<[^>]*>)?\(/.test(rec.signature) ? "function" : "variable", detail: n.path, boost: -1, ...(imports ? { imports } : {}), }); if (++added >= 60) break; } } return { from, options }; }
uses
- tcmts.studio.ScratchCompletion
- tcmts.studio.analyzedScratch
- tcmts.studio.editableCodebase
- tcmts.studio.importEdit
- tcmts.studio.scratchPath