Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e50af61
Add a new entrypoint
asispts Apr 10, 2026
6c48213
split tests into vitest (tests/) and vscode-test (tests-ext/)
asispts Apr 10, 2026
f553df4
Move findGitRepos into queries dir
asispts Apr 10, 2026
8d6db17
Add lightweight repo watcher
asispts Apr 10, 2026
c878a3d
Fix typecheck issues
asispts Apr 10, 2026
0ebe67d
Use temp dir
asispts Apr 10, 2026
470bda6
Don't mock findGitRepos
asispts Apr 10, 2026
dc47ad9
Can display the panel
asispts Apr 10, 2026
180fb2a
Add diff provider
asispts Apr 10, 2026
bf4d73e
Bring back clearAvatarCache command
asispts Apr 13, 2026
530d1ed
Modify activate function to register the ext commands
asispts Apr 13, 2026
80da7e4
Create a better bootstrap
asispts Apr 13, 2026
e0367fd
Invoke sendRepos when reset the repos
asispts Apr 13, 2026
492bcea
Add configuration listener
asispts Apr 13, 2026
ee042f8
Merge branch 'main' into refactor/initialization
asispts Apr 13, 2026
221e5d2
Fix tests-ext tsconfig
asispts Apr 13, 2026
e2ad753
Fix linter issues
asispts Apr 13, 2026
3841ce8
Fix
asispts Apr 13, 2026
ce62348
Move makeDepth listener to waitForRepo
asispts Apr 13, 2026
f9589a7
Move fallback commands to waitForRepo
asispts Apr 13, 2026
499adc8
Rename to initExtension and watchForRepos
asispts Apr 13, 2026
15fa965
Listen for maxDepth change in initExtension
asispts Apr 13, 2026
6b84dfb
Add maxDepthTracker
asispts Apr 14, 2026
f443912
Add VscodeWorkspace type
asispts Apr 14, 2026
18c9ef2
Listen for on workspace folders changed
asispts Apr 14, 2026
ecde95c
Watch for file system change in initExtension
asispts Apr 14, 2026
11bd2d8
Deprecate the old activate function
asispts Apr 14, 2026
55cfda0
Update barrel
asispts Apr 14, 2026
d66adf2
Update
asispts Apr 15, 2026
655d9ce
Remove global maxDepth initial value
asispts Apr 15, 2026
e1c3ce1
Use path.dirname
asispts Apr 15, 2026
20fd472
Update extensionState when reset the repos
asispts Apr 16, 2026
8fffdd7
Fix N disk writes when calling setRepos
asispts Apr 16, 2026
71572ce
Don't sendRepos unconditionally in onDidCreate event
asispts Apr 16, 2026
33b406b
Return early if already disposed
asispts Apr 16, 2026
d0e8d00
Should call sendRepos manually
asispts Apr 16, 2026
123ce3d
Reformat code
asispts Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@
"import/named": "off"
}
},
{
"files": ["tests-ext/**/*.ts"],
"env": {
"mocha": true
}
},
{
"files": ["src/webview/**/*.ts"],
"env": {
Expand Down
2 changes: 1 addition & 1 deletion .vscode-test.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from "@vscode/test-cli";

export default defineConfig({
files: "tests/out/extension/**/*.test.js",
files: "tests-ext/out/**/*.test.js",
workspaceFolder: ".",
version: "stable"
});
2 changes: 1 addition & 1 deletion esbuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const aliasPlugin = {

async function main() {
const extension = await esbuild.context({
entryPoints: ["src/extension.ts"],
entryPoints: ["src/extension/main.ts"],
bundle: true,
format: "cjs",
minify: production,
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@
"publisher": "asispts",
"main": "./out/extension.js",
"scripts": {
"typecheck": "tsc -p ./src --noEmit && tsc -p ./src/webview --noEmit && tsc -p ./tests --noEmit",
"typecheck": "tsc -p ./src --noEmit && tsc -p ./src/webview --noEmit && tsc -p ./tests --noEmit && tsc -p ./tests-ext --noEmit",
"vscode:prepublish": "pnpm run clean && pnpm run package",
"package": "pnpm run typecheck && pnpm run lint && node esbuild.js --production",
"clean": "node -e \"require('fs').rmSync('out',{recursive:true,force:true})\"",
"compile": "pnpm run clean && node esbuild.js",
"test": "vitest run --project backend && vitest run --project webview",
"test": "vitest run --project backend && vitest run --project extension && vitest run --project webview",
"test:ext": "pnpm run compile && pnpm run compile-tests && vscode-test",
"compile-tests": "tsc -p tests/extension/tsconfig.json && tsc-alias -p tests/extension/tsconfig.json",
"compile-tests": "tsc -p tests-ext/tsconfig.json && tsc-alias -p tests-ext/tsconfig.json",
"watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc -p ./src --noEmit --watch",
"format": "oxfmt --check",
Expand Down
12 changes: 12 additions & 0 deletions src/backend/queries/repoSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { searchDirectoryForRepos } from "@/backend/utils/repoSearch";

export async function findGitRepos(
paths: string[],
gitPath: string,
maxDepth: number
): Promise<string[]> {
const results = await Promise.all(
paths.map((p) => searchDirectoryForRepos(p, maxDepth, gitPath, []))
);
return results.flat();
}
3 changes: 3 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import { initL10n } from "./l10n";
import { RepoFileWatcher } from "./repoFileWatcher";
import { StatusBarItem } from "./statusBarItem";

/**
* @deprecated See src/extension/main.ts
*/
export function activate(context: vscode.ExtensionContext) {
initL10n(context.extensionPath);
const outputChannel = vscode.window.createOutputChannel(l10n.t("outputChannel.text"));
Expand Down
155 changes: 155 additions & 0 deletions src/extension/initExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import * as path from "path";

import * as vscode from "vscode";

import { AvatarManager } from "@/avatarManager";
import { GitClient, gitClientFactory } from "@/backend/gitClient";
import { findGitRepos } from "@/backend/queries/repoSearch";
import { buildExtensionUri } from "@/backend/utils/path";
import { config } from "@/config";
import { DiffDocProvider } from "@/diffDocProvider";
import { createMaxDepthTracker } from "@/extension/maxDepthTracker";
import { registerMessageHandlers } from "@/extension/messageHandler";
import { createRepoManager, RepoManager } from "@/extension/repoManager";
import { WebviewBridge, webviewBridgeFactory } from "@/extension/webviewBridge";
import { createWebviewPanel, WebviewPanel } from "@/extension/webviewPanel";
import { ExtensionState } from "@/extensionState";
import * as l10n from "@/l10n";
import { RepoFileWatcher } from "@/repoFileWatcher";
import { StatusBarItem } from "@/statusBarItem";

export type InitExtension = typeof initExtension;

function registerViewCommand(
ctx: vscode.ExtensionContext,
repoManager: RepoManager,
extensionState: ExtensionState,
avatarManager: AvatarManager,
gitClient: GitClient
) {
let currentPanel: WebviewPanel | undefined;
ctx.subscriptions.push(
vscode.commands.registerCommand("neo-git-graph.view", () => {
if (currentPanel) {
currentPanel.reveal(vscode.window.activeTextEditor?.viewColumn);
return;
}

const vsPanel = vscode.window.createWebviewPanel(
"neo-git-graph",
l10n.t("outputChannel.text"),
vscode.window.activeTextEditor?.viewColumn ?? vscode.ViewColumn.One,
{
enableScripts: true,
localResourceRoots: [
buildExtensionUri(ctx.extensionPath, "media"),
buildExtensionUri(ctx.extensionPath, "out")
]
}
);

let bridge!: WebviewBridge;
const repoFileWatcher = new RepoFileWatcher(() => {
if (vsPanel.visible) bridge.post({ command: "refresh" });
});
bridge = webviewBridgeFactory(vsPanel.webview, repoFileWatcher);
avatarManager.registerBridge(bridge.post.bind(bridge));

const { onPanelShown } = registerMessageHandlers(bridge, {
config,
gitClient,
repoManager,
extensionState,
avatarManager,
repoFileWatcher
});

currentPanel = createWebviewPanel({
panel: vsPanel,
bridge,
config,
repoFileWatcher,
extensionPath: ctx.extensionPath,
extensionState,
avatarManager,
repoManager,
onDispose: () => {
currentPanel = undefined;
},
onPanelShown
});
})
);
}

export function initExtension(ctx: vscode.ExtensionContext, repos: string[]) {
const extensionState = new ExtensionState(ctx);
const avatarManager = new AvatarManager(config.gitPath, extensionState);

ctx.subscriptions.push(
vscode.commands.registerCommand("neo-git-graph.clearAvatarCache", () => {
avatarManager.clearCache();
})
);

const gitClient = gitClientFactory(extensionState.getLastActiveRepo() ?? "", config.gitPath());
ctx.subscriptions.push(
vscode.workspace.registerTextDocumentContentProvider(
DiffDocProvider.scheme,
new DiffDocProvider(gitClient.getInstance)
)
);

const maxDepth = createMaxDepthTracker(config.maxDepthOfRepoSearch());
const statusBarItem = new StatusBarItem(ctx, config);
const repoManager = createRepoManager(extensionState, statusBarItem, config);
repoManager.setRepos(repos);
registerViewCommand(ctx, repoManager, extensionState, avatarManager, gitClient);

const gitWatcher = vscode.workspace.createFileSystemWatcher("**/.git");
ctx.subscriptions.push(
gitWatcher,
gitWatcher.onDidCreate((uri) => {
const repoPath = path.dirname(uri.fsPath);
if (repoManager.addRepo(repoPath)) repoManager.sendRepos();
}),
gitWatcher.onDidDelete((uri) => {
const repoPath = path.dirname(uri.fsPath);
if (repoManager.removeReposWithinFolder(repoPath)) repoManager.sendRepos();
}),
vscode.workspace.onDidChangeWorkspaceFolders(async (e) => {
if (e.added.length > 0) {
const paths = e.added.map((f) => f.uri.fsPath);
const repoDirs = await findGitRepos(paths, config.gitPath(), config.maxDepthOfRepoSearch());
for (const repo of repoDirs) repoManager.addRepo(repo);
if (repoDirs.length > 0) repoManager.sendRepos();
}
if (e.removed.length > 0) {
let changes = false;
for (const folder of e.removed) {
if (repoManager.removeReposWithinFolder(folder.uri.fsPath)) changes = true;
}
if (changes) repoManager.sendRepos();
}
}),
vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration("neo-git-graph.showStatusBarItem")) {
statusBarItem.refresh();
} else if (e.affectsConfiguration("git.path")) {
gitClient.setGitPath(config.gitPath());
} else if (e.affectsConfiguration("neo-git-graph.maxDepthOfRepoSearch")) {
if (maxDepth.increased(config.maxDepthOfRepoSearch())) {
const paths = (vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath);
void findGitRepos(paths, config.gitPath(), config.maxDepthOfRepoSearch()).then(
(repoDirs) => {
if (repoDirs.length > 0) {
repoManager.setRepos(repoDirs);
repoManager.sendRepos();
}
}
);
}
}
})
);
}
23 changes: 23 additions & 0 deletions src/extension/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as vscode from "vscode";

import { findGitRepos } from "@/backend/queries/repoSearch";
import { config } from "@/config";
import { initExtension } from "@/extension/initExtension";
import { watchForRepos } from "@/extension/watchForRepos";
import * as l10n from "@/l10n";

export async function activate(ctx: vscode.ExtensionContext) {
l10n.initL10n(ctx.extensionUri.fsPath);

const paths = (vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath);
const repoDirs = await findGitRepos(paths, config.gitPath(), config.maxDepthOfRepoSearch());

if (repoDirs.length > 0) {
initExtension(ctx, repoDirs);
return;
}

ctx.subscriptions.push(watchForRepos(ctx, initExtension));
}

export function deactivate() {}
10 changes: 10 additions & 0 deletions src/extension/maxDepthTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function createMaxDepthTracker(initialDepth: number) {
let current = initialDepth;
return {
increased: (newDepth: number) => {
const prev = current;
current = newDepth;
return current > prev;
}
};
}
15 changes: 14 additions & 1 deletion src/extension/repoManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ export function createRepoManager(
let repos = extensionState.getRepos();
let viewCallback: ((repos: GitRepoSet, numRepos: number) => void) | null = null;

function setRepos(repoDirs: string[]) {
for (const key of Object.keys(repos)) {
delete repos[key];
}
for (const repo of repoDirs) {
repos[repo] = { columnWidths: null };
}
extensionState.saveRepos(repos);
}

function getRepos() {
return sortRepos(repos);
}
Expand Down Expand Up @@ -58,8 +68,10 @@ export function createRepoManager(
}

function addRepo(repo: string) {
if (repos[repo]) return false;
repos[repo] = { columnWidths: null };
extensionState.saveRepos(repos);
return true;
}

function removeReposWithinFolder(path: string) {
Expand Down Expand Up @@ -123,9 +135,10 @@ export function createRepoManager(
return {
registerViewCallback,
deregisterViewCallback,
getRepos,
isDirectoryWithinRepos,
getRepos,
sendRepos,
setRepos,
addRepo,
removeRepo,
removeReposWithinFolder,
Expand Down
66 changes: 66 additions & 0 deletions src/extension/watchForRepos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as vscode from "vscode";

import { findGitRepos } from "@/backend/queries/repoSearch";
import { config } from "@/config";
import type { InitExtension } from "@/extension/initExtension";
import { createMaxDepthTracker } from "@/extension/maxDepthTracker";
import * as l10n from "@/l10n";

type WatcherState = {
disposed: boolean;
disposables: vscode.Disposable[];
};

function dispose(state: WatcherState) {
state.disposed = true;
for (const d of state.disposables) d.dispose();
state.disposables.length = 0;
}

async function check(
ctx: vscode.ExtensionContext,
state: WatcherState,
onReposFound: InitExtension
) {
if (state.disposed) return;
const paths = (vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath);
const repoDirs = await findGitRepos(paths, config.gitPath(), config.maxDepthOfRepoSearch());
if (repoDirs.length === 0 || state.disposed) return;
dispose(state);
onReposFound(ctx, repoDirs);
}

export function watchForRepos(
ctx: vscode.ExtensionContext,
onReposFound: InitExtension
): { dispose(): void } {
const maxDepth = createMaxDepthTracker(config.maxDepthOfRepoSearch());
const gitWatcher = vscode.workspace.createFileSystemWatcher("**/.git");
const state: WatcherState = { disposed: false, disposables: [gitWatcher] };

state.disposables.push(
gitWatcher.onDidCreate(() => check(ctx, state, onReposFound)),
vscode.workspace.onDidChangeWorkspaceFolders(() => check(ctx, state, onReposFound)),
vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration("neo-git-graph.maxDepthOfRepoSearch")) {
if (maxDepth.increased(config.maxDepthOfRepoSearch())) {
void check(ctx, state, onReposFound);
}
}
}),
vscode.commands.registerCommand("neo-git-graph.view", async () => {
await vscode.window.showErrorMessage(l10n.t("statusBar.text"), {
detail: l10n.t("error.noGitRepository"),
modal: true
});
}),
vscode.commands.registerCommand("neo-git-graph.clearAvatarCache", async () => {
await vscode.window.showErrorMessage(l10n.t("statusBar.text"), {
detail: l10n.t("error.noGitRepository"),
modal: true
});
})
);

return { dispose: () => dispose(state) };
}
File renamed without changes.
File renamed without changes.
6 changes: 3 additions & 3 deletions tests/extension/tsconfig.json → tests-ext/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": false,
"module": "commonjs",
"module": "CommonJS",
"moduleResolution": "node",
"outDir": "../out/extension"
"noEmit": false,
"outDir": "./out"
},
"include": ["./**/*.ts"]
}
Loading
Loading