OXYGENHUB
# aeb4e32ee7a0 oxygen-vscode / src / extension.ts
7.5 KiB · last changed Milestone 0.1: source control view, diffs over the oxy: scheme, quick diff, decorations, status bar, commit, quick-pick log; process wrapper with a Node test; packaging 1 month ago source blame history raw
bed495a9 Andy McCall ↶ 1
// Activation: find oxy, find repositories, wire the source control view,
2
// decorations, the content provider, and the commands (spec §3).
3
4
import * as vscode from "vscode";
5
import { basename } from "node:path";
6
import { Oxy, OxyError, findBinary } from "./oxy";
7
import { Repository, OxyResourceState } from "./repository";
8
import { OxyContentProvider, SCHEME, baseUri } from "./contentProvider";
9
import { OxyDecorationProvider } from "./decorations";
10
11
const MIN_VERSION = { format: 2, protocol: 1 };
12
13
let output: vscode.OutputChannel;
14
let oxy: Oxy;
15
let contentProvider: OxyContentProvider;
16
let decorations: OxyDecorationProvider;
17
const repositories = new Map<string, Repository>();
18
19
export async function activate(context: vscode.ExtensionContext): Promise<void> {
20
  output = vscode.window.createOutputChannel("Oxygen");
21
  context.subscriptions.push(output);
22
  oxy = new Oxy(findBinary(vscode.workspace.getConfiguration("oxygen").get<string>("path")), (line) => output.appendLine(line));
23
24
  try {
25
    const v = await oxy.version();
26
    output.appendLine(`oxy ${v.version} (format ${v.format}, protocol ${v.protocol}) at ${oxy.binary}`);
27
    if (v.format < MIN_VERSION.format || v.protocol < MIN_VERSION.protocol) {
28
      void vscode.window.showWarningMessage(`Oxygen: oxy ${v.version} is older than this extension supports.`);
29
    }
30
  } catch (e) {
31
    const pick = await vscode.window.showErrorMessage(`Oxygen: could not run oxy (${(e as Error).message}). Set "oxygen.path" or put oxy on your PATH.`, "Open Settings");
32
    if (pick === "Open Settings") {
33
      void vscode.commands.executeCommand("workbench.action.openSettings", "oxygen.path");
34
    }
35
    return;
36
  }
37
38
  contentProvider = new OxyContentProvider(oxy);
39
  context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(SCHEME, contentProvider));
40
  decorations = new OxyDecorationProvider(() => [...repositories.values()]);
41
  context.subscriptions.push(decorations);
42
43
  context.subscriptions.push(
44
    vscode.commands.registerCommand("oxygen.refresh", async () => {
45
      await Promise.all([...repositories.values()].map((r) => r.refresh()));
46
    }),
47
    vscode.commands.registerCommand("oxygen.showOutput", () => output.show()),
48
    vscode.commands.registerCommand("oxygen.openFile", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", state.resourceUri)),
49
    vscode.commands.registerCommand("oxygen.openChanges", (state?: OxyResourceState) => state && openChanges(state)),
50
    vscode.commands.registerCommand("oxygen.openBaseVersion", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", baseUri(state.repository.root, state.path))),
51
    vscode.commands.registerCommand("oxygen.commit", (arg?: Repository | vscode.SourceControl) => commit(arg)),
52
    vscode.commands.registerCommand("oxygen.showLog", () => showLog()),
53
    vscode.workspace.onDidChangeWorkspaceFolders(() => void discover()),
54
    vscode.workspace.onDidChangeConfiguration((e) => {
55
      if (e.affectsConfiguration("oxygen.path")) {
56
        void vscode.window.showInformationMessage("Oxygen: reload the window to use the new oxy path.");
57
      }
58
      if (e.affectsConfiguration("oxygen")) {
59
        decorations.refresh();
60
        for (const r of repositories.values()) {
61
          r.scheduleRefresh();
62
        }
63
      }
64
    }),
65
  );
66
  context.subscriptions.push({ dispose: () => repositories.forEach((r) => r.dispose()) });
67
68
  await discover();
69
}
70
71
export function deactivate(): void {
72
  for (const r of repositories.values()) {
73
    r.dispose();
74
  }
75
  repositories.clear();
76
}
77
78
/** One Repository per distinct oxy root among the workspace folders. */
79
async function discover(): Promise<void> {
80
  const wanted = new Map<string, vscode.WorkspaceFolder>();
81
  for (const folder of vscode.workspace.workspaceFolders ?? []) {
82
    const info = await oxy.root(folder.uri.fsPath);
83
    if (info) {
84
      wanted.set(info.root, folder);
85
    }
86
  }
87
  for (const [root, repo] of repositories) {
88
    if (!wanted.has(root)) {
89
      repo.dispose();
90
      repositories.delete(root);
91
    }
92
  }
93
  for (const [root, folder] of wanted) {
94
    if (!repositories.has(root)) {
95
      const repo = new Repository(oxy, root, folder);
96
      repo.onDidChangeStatus(() => {
97
        decorations.refresh();
98
        contentProvider.invalidate(root);
99
      });
100
      repositories.set(root, repo);
101
      output.appendLine(`repository ${root}`);
102
      await repo.refresh();
103
    }
104
  }
105
  void vscode.commands.executeCommand("setContext", "oxygen.hasRepository", repositories.size > 0);
106
}
107
108
function repositoryFor(arg: Repository | vscode.SourceControl | undefined): Repository | undefined {
109
  if (arg instanceof Repository) {
110
    return arg;
111
  }
112
  if (arg && "rootUri" in arg && arg.rootUri) {
113
    return repositories.get(arg.rootUri.fsPath);
114
  }
115
  const active = vscode.window.activeTextEditor?.document.uri;
116
  for (const repo of repositories.values()) {
117
    if (active && repo.relativePath(active) !== undefined) {
118
      return repo;
119
    }
120
  }
121
  return repositories.size === 1 ? [...repositories.values()][0] : undefined;
122
}
123
124
async function openChanges(state: OxyResourceState): Promise<void> {
125
  const repo = state.repository;
126
  const title = `${basename(state.path)} (base ↔ working copy)`;
127
  if (state.status === "Added") {
128
    await vscode.commands.executeCommand("vscode.open", state.resourceUri);
129
    return;
130
  }
131
  if (state.status === "Removed") {
132
    await vscode.commands.executeCommand("vscode.open", baseUri(repo.root, state.path));
133
    return;
134
  }
135
  await vscode.commands.executeCommand("vscode.diff", baseUri(repo.root, state.path), state.resourceUri, title);
136
}
137
138
async function commit(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
139
  const repo = repositoryFor(arg);
140
  if (!repo) {
141
    void vscode.window.showErrorMessage("Oxygen: no repository for the active file.");
142
    return;
143
  }
144
  let message = repo.sourceControl.inputBox.value.trim();
145
  if (!message) {
146
    message = (await vscode.window.showInputBox({ prompt: "Commit message", placeHolder: "What did you change?" }))?.trim() ?? "";
147
  }
148
  if (!message) {
149
    return;
150
  }
151
  try {
152
    await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: "Committing" }, () => repo.oxy.commit(repo.root, message));
153
    repo.sourceControl.inputBox.value = "";
154
    await repo.refresh();
155
  } catch (e) {
156
    showError(e);
157
  }
158
}
159
160
async function showLog(): Promise<void> {
161
  const repo = repositoryFor(undefined);
162
  if (!repo) {
163
    return;
164
  }
165
  try {
166
    const log = await repo.oxy.log(repo.root, 50);
167
    const items = log.changes
168
      .filter((c) => !c.working_copy)
169
      .map((c) => ({
170
        label: `${c.head ? "$(circle-filled)" : "$(circle-outline)"} ${c.message.split("\n")[0]}`,
171
        description: `${c.change_id.slice(0, 8)} · ${c.author}`,
172
        detail: `${new Date(c.when_ms).toLocaleString()} · ${c.signature?.status ?? "unsigned"}${c.signature?.revoked ? " (key revoked)" : ""}`,
173
        change: c,
174
      }));
175
    const pick = await vscode.window.showQuickPick(items, { placeHolder: `${log.bookmark}: ${log.total - 1} changes`, matchOnDescription: true, matchOnDetail: true });
176
    if (pick) {
177
      const doc = await vscode.workspace.openTextDocument({ content: pick.change.message, language: "markdown" });
178
      await vscode.window.showTextDocument(doc, { preview: true });
179
    }
180
  } catch (e) {
181
    showError(e);
182
  }
183
}
184
185
function showError(e: unknown): void {
186
  const message = e instanceof OxyError ? e.message : String(e);
187
  void vscode.window.showErrorMessage(`Oxygen: ${message}`, "Show Output").then((pick) => pick && output.show());
188
}