OXYGENHUB
◆ main oxygenhub-vscode / src / refs.ts
3.1 KiB · last changed New Issue from Selection (0.5.0) 1 month ago source blame history raw
f902d5b8 Andy McCall ↶ 1
// `#N` and `@user` in text the user is typing (spec §6.2): what is being
2
// completed at the cursor, and which reference a position sits on. No
3
// vscode imports, so it is tested with plain Node.
4
5
/** A reference being typed: `#12` (issue or proposal) or `@an` (a user). */
6
export type Typing = { kind: "number"; prefix: string; start: number } | { kind: "user"; prefix: string; start: number };
7
8
/**
9
 * What the text before the cursor is in the middle of typing, if anything.
10
 * A reference starts a word: after the line start, whitespace or an
11
 * opening bracket, so `a#1` and `mail@host` are not references.
12
 */
13
export function typingAt(before: string): Typing | undefined {
14
  const n = /(^|[\s([{])#(\d*)$/.exec(before);
15
  if (n) {
16
    return { kind: "number", prefix: n[2], start: before.length - n[2].length - 1 };
17
  }
18
  const u = /(^|[\s([{])@([A-Za-z0-9][A-Za-z0-9_-]*|)$/.exec(before);
19
  if (u) {
20
    return { kind: "user", prefix: u[2], start: before.length - u[2].length - 1 };
21
  }
22
  return undefined;
23
}
24
25
/** The `#N` reference covering `column` of `line`, with its span. */
26
export function numberAt(line: string, column: number): { number: number; start: number; end: number } | undefined {
27
  const re = /(^|[\s([{])#(\d+)\b/g;
28
  for (let m = re.exec(line); m; m = re.exec(line)) {
29
    const start = m.index + m[1].length;
30
    const end = start + 1 + m[2].length;
31
    if (column >= start && column <= end) {
32
      return { number: Number(m[2]), start, end };
33
    }
34
  }
35
  return undefined;
36
}
37
38
/** A repository root named by an SCM input box's URI query (`rootUri=file:///…`). */
39
export function rootFromScmQuery(query: string): string | undefined {
40
  const raw = new URLSearchParams(query).get("rootUri");
41
  if (!raw) {
42
    return undefined;
43
  }
44
  try {
45
    return decodeURIComponent(new URL(raw).pathname);
46
  } catch {
47
    return undefined;
48
  }
49
}
95f69b4a Andy McCall ↶ 50
51
/** The note of a `TODO`/`FIXME`/`XXX`/`HACK` comment line, as an issue title. */
52
export function todoTitle(line: string): string | undefined {
53
  const m = /\b(TODO|FIXME|XXX|HACK)\b(?:\([^)]*\))?[:\s-]*(.*)$/.exec(line);
54
  if (!m) {
55
    return undefined;
56
  }
57
  const text = m[2].replace(/\s*(\*\/|-->|#\}|\*\)|;)\s*$/, "").trim();
58
  return text ? text : `${m[1]} in the code`;
59
}
60
61
/** The TODO line with the new issue's number recorded: `TODO(#12): …`. */
62
export function annotateTodo(line: string, number: number): string {
63
  return line.replace(/\b(TODO|FIXME|XXX|HACK)\b(?!\()/, `$1(#${number})`);
64
}
65
66
/** An issue body quoting the code, with a permalink to it on the hub. */
67
export function issueBody(opts: { permalink: string; path: string; startLine: number; endLine: number; language: string; code: string; pinned: boolean }): string {
68
  const where = opts.startLine === opts.endLine ? `line ${opts.startLine}` : `lines ${opts.startLine}–${opts.endLine}`;
69
  const fence = opts.code.includes("```") ? "~~~" : "```";
70
  const note = opts.pinned ? "" : "\n\n_The link follows the bookmark, which has several heads, so the lines may move._";
71
  return `From [\`${opts.path}\`, ${where}](${opts.permalink}):\n\n${fence}${opts.language}\n${opts.code.replace(/\n$/, "")}\n${fence}${note}\n`;
72
}