// Drops in the editor (spec §8.4): the addresses of drop files, the names // shared files take, and the words the Drops view shows. No vscode imports, // so it is tested with plain Node. import type * as api from "./hubapi"; import { ago } from "./flowview"; export const DROP_SCHEME = "oxygenhub-drop"; /** Which drop file a document is: the hub, the drop's id, and the file's name ("" for the drop itself). */ export interface DropRef { base: string; id: string; name: string; } /** A drop file's URI parts: `/ID/NAME`, the hub in the query. */ export function dropUriParts(r: DropRef): { path: string; query: string } { return { path: `/${r.id}${r.name ? `/${r.name}` : ""}`, query: new URLSearchParams({ hub: r.base }).toString() }; } /** Parse a drop file's URI parts back; undefined if they are not one. */ export function parseDropUri(path: string, query: string): DropRef | undefined { const base = new URLSearchParams(query).get("hub"); const m = /^\/([0-9a-f]{24})(?:\/(.+))?$/.exec(path); if (!base || !m) { return undefined; } return { base, id: m[1], name: m[2] ?? "" }; } /** * A drop's id from what someone pastes: the id, or its page on a hub * (`https://hub/OWNER/drops/ID` or `https://hub/drops/ID`). */ export function dropIdOf(text: string): string | undefined { const t = text.trim(); if (/^[0-9a-f]{24}$/.test(t)) { return t; } const m = /\/drops\/([0-9a-f]{24})(?:[/?#].*)?$/.exec(t); return m ? m[1] : undefined; } /** The hub a pasted drop page is on, if it is a URL. */ export function dropBaseOf(text: string): string | undefined { const m = /^(https?:\/\/[^/]+)\//.exec(text.trim()); return m ? m[1] : undefined; } /** * Names for files shared as one drop: each file's path below the folder * they all sit in, so two `index.ts` in different folders stay apart and a * single file keeps just its name. Paths use `/`. */ export function shareNames(paths: string[]): string[] { const parts = paths.map((p) => p.split(/[\\/]/).filter(Boolean)); if (parts.length === 1) { return [parts[0][parts[0].length - 1]]; } let common = 0; const shortest = Math.min(...parts.map((p) => p.length - 1)); while (common < shortest && parts.every((p) => p[common] === parts[0][common])) { common++; } return parts.map((p) => p.slice(common).join("/")); } /** File extensions for a language with no file behind it (an untitled editor). */ const EXTENSIONS: Record = { typescript: "ts", typescriptreact: "tsx", javascript: "js", javascriptreact: "jsx", rust: "rs", python: "py", go: "go", java: "java", c: "c", cpp: "cpp", csharp: "cs", shellscript: "sh", markdown: "md", json: "json", yaml: "yaml", toml: "toml", html: "html", css: "css", sql: "sql", ruby: "rb", php: "php", swift: "swift", kotlin: "kt", plaintext: "txt", }; /** The name a selection is shared under: the file's own name, else `snippet.EXT` by its language. */ export function snippetName(fileName: string | undefined, languageId: string): string { if (fileName) { const base = fileName.split(/[\\/]/).pop(); if (base) { return base; } } return `snippet.${EXTENSIONS[languageId] ?? "txt"}`; } /** A shared selection's title: the file and its lines. */ export function snippetTitle(name: string, startLine: number, endLine: number, whole: boolean): string { if (whole) { return name; } return startLine === endLine ? `${name}, line ${startLine}` : `${name}, lines ${startLine}–${endLine}`; } /** The text a selection shares: the selected lines' text with its shared indentation removed. */ export function dedent(text: string): string { const lines = text.replace(/\r\n/g, "\n").split("\n"); const indents = lines.filter((l) => l.trim()).map((l) => /^[ \t]*/.exec(l)![0].length); const cut = indents.length ? Math.min(...indents) : 0; return lines.map((l) => l.slice(cut)).join("\n"); } /** The HTML that embeds a drop in another page. */ export function embedSnippet(d: Pick): string { const title = (d.title || d.id).replace(/"/g, """); return ``; } /** A drop's line in the Drops view: its title, and what it holds. */ export function dropLabel(d: api.Drop): { label: string; description: string } { const files = d.files.length; const bits = [d.visibility === "secret" ? "secret" : "", `${files} file${files === 1 ? "" : "s"}`, d.stars ? `★ ${d.stars}` : "", d.forked_from ? "fork" : ""]; return { label: d.title || d.files[0]?.name || d.id, description: bits.filter(Boolean).join(" · ") }; } /** A comment's line, for the comments pick. */ export function commentLine(c: api.DropComment, now = Date.now()): { label: string; description: string; detail: string } { return { label: c.author, description: ago(c.created_ms, now), detail: c.body.replace(/\s+/g, " ").trim() }; }