OXYGENHUB
# 810a1c538e78 oxygen-vscode / src / extension.ts
8.5 KiB · last changed Diagnostics: log from activation, report when no repository is found, Initialize Repository command and Source Control welcome 1 month ago source blame history raw
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
  output.appendLine(`Oxygen extension ${context.extension.packageJSON.version} activating; folders: ${(vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath).join(", ") || "(none)"}`);
23
  oxy = new Oxy(findBinary(vscode.workspace.getConfiguration("oxygen").get<string>("path")), (line) => output.appendLine(line));
24
25
  try {
26
    const v = await oxy.version();
27
    output.appendLine(`oxy ${v.version} (format ${v.format}, protocol ${v.protocol}) at ${oxy.binary}`);
28
    if (v.format < MIN_VERSION.format || v.protocol < MIN_VERSION.protocol) {
29
      void vscode.window.showWarningMessage(`Oxygen: oxy ${v.version} is older than this extension supports.`);
30
    }
31
  } catch (e) {
32
    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");
33
    if (pick === "Open Settings") {
34
      void vscode.commands.executeCommand("workbench.action.openSettings", "oxygen.path");
35
    }
36
    return;
37
  }
38
39
  contentProvider = new OxyContentProvider(oxy);
40
  context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(SCHEME, contentProvider));
41
  decorations = new OxyDecorationProvider(() => [...repositories.values()]);
42
  context.subscriptions.push(decorations);
43
44
  context.subscriptions.push(
45
    vscode.commands.registerCommand("oxygen.refresh", async () => {
46
      await Promise.all([...repositories.values()].map((r) => r.refresh()));
47
    }),
48
    vscode.commands.registerCommand("oxygen.showOutput", () => output.show()),
49
    vscode.commands.registerCommand("oxygen.openFile", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", state.resourceUri)),
50
    vscode.commands.registerCommand("oxygen.openChanges", (state?: OxyResourceState) => state && openChanges(state)),
51
    vscode.commands.registerCommand("oxygen.openBaseVersion", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", baseUri(state.repository.root, state.path))),
52
    vscode.commands.registerCommand("oxygen.commit", (arg?: Repository | vscode.SourceControl) => commit(arg)),
53
    vscode.commands.registerCommand("oxygen.showLog", () => showLog()),
54
    vscode.commands.registerCommand("oxygen.init", () => initRepository()),
55
    vscode.workspace.onDidChangeWorkspaceFolders(() => void discover()),
56
    vscode.workspace.onDidChangeConfiguration((e) => {
57
      if (e.affectsConfiguration("oxygen.path")) {
58
        void vscode.window.showInformationMessage("Oxygen: reload the window to use the new oxy path.");
59
      }
60
      if (e.affectsConfiguration("oxygen")) {
61
        decorations.refresh();
62
        for (const r of repositories.values()) {
63
          r.scheduleRefresh();
64
        }
65
      }
66
    }),
67
  );
68
  context.subscriptions.push({ dispose: () => repositories.forEach((r) => r.dispose()) });
69
70
  await discover();
71
}
72
73
export function deactivate(): void {
74
  for (const r of repositories.values()) {
75
    r.dispose();
76
  }
77
  repositories.clear();
78
}
79
80
/** One Repository per distinct oxy root among the workspace folders. */
81
async function discover(): Promise<void> {
82
  const wanted = new Map<string, vscode.WorkspaceFolder>();
83
  for (const folder of vscode.workspace.workspaceFolders ?? []) {
84
    const info = await oxy.root(folder.uri.fsPath);
85
    if (info) {
86
      wanted.set(info.root, folder);
87
    }
88
  }
89
  for (const [root, repo] of repositories) {
90
    if (!wanted.has(root)) {
91
      repo.dispose();
92
      repositories.delete(root);
93
    }
94
  }
95
  if (wanted.size === 0 && (vscode.workspace.workspaceFolders ?? []).length > 0) {
96
    output.appendLine("no Oxygen repository in the open folders (oxy root found none)");
97
  }
98
  for (const [root, folder] of wanted) {
99
    if (!repositories.has(root)) {
100
      const repo = new Repository(oxy, root, folder);
101
      repo.onDidChangeStatus(() => {
102
        decorations.refresh();
103
        contentProvider.invalidate(root);
104
      });
105
      repositories.set(root, repo);
106
      output.appendLine(`repository ${root}`);
107
      await repo.refresh();
108
    }
109
  }
110
  void vscode.commands.executeCommand("setContext", "oxygen.hasRepository", repositories.size > 0);
111
}
112
113
function repositoryFor(arg: Repository | vscode.SourceControl | undefined): Repository | undefined {
114
  if (arg instanceof Repository) {
115
    return arg;
116
  }
117
  if (arg && "rootUri" in arg && arg.rootUri) {
118
    return repositories.get(arg.rootUri.fsPath);
119
  }
120
  const active = vscode.window.activeTextEditor?.document.uri;
121
  for (const repo of repositories.values()) {
122
    if (active && repo.relativePath(active) !== undefined) {
123
      return repo;
124
    }
125
  }
126
  return repositories.size === 1 ? [...repositories.values()][0] : undefined;
127
}
128
129
async function openChanges(state: OxyResourceState): Promise<void> {
130
  const repo = state.repository;
131
  const title = `${basename(state.path)} (base ↔ working copy)`;
132
  if (state.status === "Added") {
133
    await vscode.commands.executeCommand("vscode.open", state.resourceUri);
134
    return;
135
  }
136
  if (state.status === "Removed") {
137
    await vscode.commands.executeCommand("vscode.open", baseUri(repo.root, state.path));
138
    return;
139
  }
140
  await vscode.commands.executeCommand("vscode.diff", baseUri(repo.root, state.path), state.resourceUri, title);
141
}
142
143
async function commit(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
144
  const repo = repositoryFor(arg);
145
  if (!repo) {
146
    void vscode.window.showErrorMessage("Oxygen: no repository for the active file.");
147
    return;
148
  }
149
  let message = repo.sourceControl.inputBox.value.trim();
150
  if (!message) {
151
    message = (await vscode.window.showInputBox({ prompt: "Commit message", placeHolder: "What did you change?" }))?.trim() ?? "";
152
  }
153
  if (!message) {
154
    return;
155
  }
156
  try {
157
    await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: "Committing" }, () => repo.oxy.commit(repo.root, message));
158
    repo.sourceControl.inputBox.value = "";
159
    await repo.refresh();
160
  } catch (e) {
161
    showError(e);
162
  }
163
}
164
165
async function initRepository(): Promise<void> {
166
  const folders = vscode.workspace.workspaceFolders ?? [];
167
  if (folders.length === 0) {
168
    void vscode.window.showErrorMessage("Oxygen: open a folder first.");
169
    return;
170
  }
171
  const folder = folders.length === 1 ? folders[0] : await vscode.window.showWorkspaceFolderPick({ placeHolder: "Folder to initialize as an Oxygen repository" });
172
  if (!folder) {
173
    return;
174
  }
175
  try {
176
    await oxy.check(folder.uri.fsPath, ["init"]);
177
    await discover();
178
    void vscode.window.showInformationMessage(`Oxygen: initialized ${folder.name}.`);
179
  } catch (e) {
180
    showError(e);
181
  }
182
}
183
184
async function showLog(): Promise<void> {
185
  const repo = repositoryFor(undefined);
186
  if (!repo) {
187
    return;
188
  }
189
  try {
190
    const log = await repo.oxy.log(repo.root, 50);
191
    const items = log.changes
192
      .filter((c) => !c.working_copy)
193
      .map((c) => ({
194
        label: `${c.head ? "$(circle-filled)" : "$(circle-outline)"} ${c.message.split("\n")[0]}`,
195
        description: `${c.change_id.slice(0, 8)} · ${c.author}`,
196
        detail: `${new Date(c.when_ms).toLocaleString()} · ${c.signature?.status ?? "unsigned"}${c.signature?.revoked ? " (key revoked)" : ""}`,
197
        change: c,
198
      }));
199
    const pick = await vscode.window.showQuickPick(items, { placeHolder: `${log.bookmark}: ${log.total - 1} changes`, matchOnDescription: true, matchOnDetail: true });
200
    if (pick) {
201
      const doc = await vscode.workspace.openTextDocument({ content: pick.change.message, language: "markdown" });
202
      await vscode.window.showTextDocument(doc, { preview: true });
203
    }
204
  } catch (e) {
205
    showError(e);
206
  }
207
}
208
209
function showError(e: unknown): void {
210
  const message = e instanceof OxyError ? e.message : String(e);
211
  void vscode.window.showErrorMessage(`Oxygen: ${message}`, "Show Output").then((pick) => pick && output.show());
212
}