OXYGENHUB
◆ v0.13.0 oxygenhub-vscode / src / dropkit.ts
4.9 KiB · last changed OxygenHub 0.13.0: Drops in the editor with Share as a Drop (closes #7); Flow approvals, expedite, reports, timings and test results (closes #8); ready/draft, landable, trust reports, asset downloads and new repositories (closes #9) 6 days ago source blame history raw
991516a1 Andy McCall ↶ 1
// Drops in the editor (spec §8.4): the addresses of drop files, the names
2
// shared files take, and the words the Drops view shows. No vscode imports,
3
// so it is tested with plain Node.
4
5
import type * as api from "./hubapi";
6
import { ago } from "./flowview";
7
8
export const DROP_SCHEME = "oxygenhub-drop";
9
10
/** Which drop file a document is: the hub, the drop's id, and the file's name ("" for the drop itself). */
11
export interface DropRef {
12
  base: string;
13
  id: string;
14
  name: string;
15
}
16
17
/** A drop file's URI parts: `/ID/NAME`, the hub in the query. */
18
export function dropUriParts(r: DropRef): { path: string; query: string } {
19
  return { path: `/${r.id}${r.name ? `/${r.name}` : ""}`, query: new URLSearchParams({ hub: r.base }).toString() };
20
}
21
22
/** Parse a drop file's URI parts back; undefined if they are not one. */
23
export function parseDropUri(path: string, query: string): DropRef | undefined {
24
  const base = new URLSearchParams(query).get("hub");
25
  const m = /^\/([0-9a-f]{24})(?:\/(.+))?$/.exec(path);
26
  if (!base || !m) {
27
    return undefined;
28
  }
29
  return { base, id: m[1], name: m[2] ?? "" };
30
}
31
32
/**
33
 * A drop's id from what someone pastes: the id, or its page on a hub
34
 * (`https://hub/OWNER/drops/ID` or `https://hub/drops/ID`).
35
 */
36
export function dropIdOf(text: string): string | undefined {
37
  const t = text.trim();
38
  if (/^[0-9a-f]{24}$/.test(t)) {
39
    return t;
40
  }
41
  const m = /\/drops\/([0-9a-f]{24})(?:[/?#].*)?$/.exec(t);
42
  return m ? m[1] : undefined;
43
}
44
45
/** The hub a pasted drop page is on, if it is a URL. */
46
export function dropBaseOf(text: string): string | undefined {
47
  const m = /^(https?:\/\/[^/]+)\//.exec(text.trim());
48
  return m ? m[1] : undefined;
49
}
50
51
/**
52
 * Names for files shared as one drop: each file's path below the folder
53
 * they all sit in, so two `index.ts` in different folders stay apart and a
54
 * single file keeps just its name. Paths use `/`.
55
 */
56
export function shareNames(paths: string[]): string[] {
57
  const parts = paths.map((p) => p.split(/[\\/]/).filter(Boolean));
58
  if (parts.length === 1) {
59
    return [parts[0][parts[0].length - 1]];
60
  }
61
  let common = 0;
62
  const shortest = Math.min(...parts.map((p) => p.length - 1));
63
  while (common < shortest && parts.every((p) => p[common] === parts[0][common])) {
64
    common++;
65
  }
66
  return parts.map((p) => p.slice(common).join("/"));
67
}
68
69
/** File extensions for a language with no file behind it (an untitled editor). */
70
const EXTENSIONS: Record<string, string> = {
71
  typescript: "ts",
72
  typescriptreact: "tsx",
73
  javascript: "js",
74
  javascriptreact: "jsx",
75
  rust: "rs",
76
  python: "py",
77
  go: "go",
78
  java: "java",
79
  c: "c",
80
  cpp: "cpp",
81
  csharp: "cs",
82
  shellscript: "sh",
83
  markdown: "md",
84
  json: "json",
85
  yaml: "yaml",
86
  toml: "toml",
87
  html: "html",
88
  css: "css",
89
  sql: "sql",
90
  ruby: "rb",
91
  php: "php",
92
  swift: "swift",
93
  kotlin: "kt",
94
  plaintext: "txt",
95
};
96
97
/** The name a selection is shared under: the file's own name, else `snippet.EXT` by its language. */
98
export function snippetName(fileName: string | undefined, languageId: string): string {
99
  if (fileName) {
100
    const base = fileName.split(/[\\/]/).pop();
101
    if (base) {
102
      return base;
103
    }
104
  }
105
  return `snippet.${EXTENSIONS[languageId] ?? "txt"}`;
106
}
107
108
/** A shared selection's title: the file and its lines. */
109
export function snippetTitle(name: string, startLine: number, endLine: number, whole: boolean): string {
110
  if (whole) {
111
    return name;
112
  }
113
  return startLine === endLine ? `${name}, line ${startLine}` : `${name}, lines ${startLine}–${endLine}`;
114
}
115
116
/** The text a selection shares: the selected lines' text with its shared indentation removed. */
117
export function dedent(text: string): string {
118
  const lines = text.replace(/\r\n/g, "\n").split("\n");
119
  const indents = lines.filter((l) => l.trim()).map((l) => /^[ \t]*/.exec(l)![0].length);
120
  const cut = indents.length ? Math.min(...indents) : 0;
121
  return lines.map((l) => l.slice(cut)).join("\n");
122
}
123
124
/** The HTML that embeds a drop in another page. */
125
export function embedSnippet(d: Pick<api.Drop, "embed_url" | "title" | "id">): string {
126
  const title = (d.title || d.id).replace(/"/g, "&quot;");
127
  return `<iframe src="${d.embed_url ?? ""}" title="${title}" width="100%" height="400" style="border:0" loading="lazy"></iframe>`;
128
}
129
130
/** A drop's line in the Drops view: its title, and what it holds. */
131
export function dropLabel(d: api.Drop): { label: string; description: string } {
132
  const files = d.files.length;
133
  const bits = [d.visibility === "secret" ? "secret" : "", `${files} file${files === 1 ? "" : "s"}`, d.stars ? `★ ${d.stars}` : "", d.forked_from ? "fork" : ""];
134
  return { label: d.title || d.files[0]?.name || d.id, description: bits.filter(Boolean).join(" · ") };
135
}
136
137
/** A comment's line, for the comments pick. */
138
export function commentLine(c: api.DropComment, now = Date.now()): { label: string; description: string; detail: string } {
139
  return { label: c.author, description: ago(c.created_ms, now), detail: c.body.replace(/\s+/g, " ").trim() };
140
}