Files
tirth8205--code-review-graph/code-review-graph-vscode/src/extension.ts
T
2026-07-13 12:42:18 +08:00

984 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as vscode from "vscode";
import * as path from "node:path";
import * as fs from "node:fs";
import { SqliteReader } from "./backend/sqlite";
import type { GraphNode } from "./backend/sqlite";
import { CliWrapper } from "./backend/cli";
import {
CodeGraphTreeProvider,
BlastRadiusTreeProvider,
StatsTreeProvider,
} from "./views/treeView";
import { GraphWebviewPanel } from "./views/graphWebview";
import { Installer } from "./onboarding/installer";
import { registerWalkthroughCommands, showWelcomeIfNeeded } from "./onboarding/welcome";
import { StatusBar } from "./views/statusBar";
import { ScmDecorationProvider } from "./features/scmDecorations";
let sqliteReader: SqliteReader | undefined;
let autoUpdateTimer: ReturnType<typeof setTimeout> | undefined;
let scmDecorationProvider: ScmDecorationProvider | undefined;
/**
* Locate the graph database file in the workspace.
* Checks `.code-review-graph/graph.db` first, then falls back to `.code-review-graph.db`.
*/
function findGraphDb(workspaceRoot: string): string | undefined {
const primary = path.join(workspaceRoot, ".code-review-graph", "graph.db");
if (fs.existsSync(primary)) {
return primary;
}
const fallback = path.join(workspaceRoot, ".code-review-graph.db");
if (fs.existsSync(fallback)) {
return fallback;
}
return undefined;
}
/**
* Get the workspace root folder path, or undefined if no workspace is open.
* Checks all workspace folders for a graph database (multi-root support).
*/
function getWorkspaceRoot(): string | undefined {
const folders = vscode.workspace.workspaceFolders;
if (!folders) { return undefined; }
// Prefer the folder that has a graph database
for (const folder of folders) {
if (findGraphDb(folder.uri.fsPath)) {
return folder.uri.fsPath;
}
}
// Fall back to first folder
return folders[0]?.uri.fsPath;
}
/**
* Navigate to a node's source file location.
*/
async function navigateToNode(node: GraphNode): Promise<void> {
const workspaceRoot = getWorkspaceRoot();
const filePath = workspaceRoot
? path.join(workspaceRoot, node.filePath)
: node.filePath;
const doc = await vscode.workspace.openTextDocument(filePath);
const line = Math.max(0, (node.lineStart ?? 1) - 1);
await vscode.window.showTextDocument(doc, {
selection: new vscode.Range(line, 0, line, 0),
});
}
/**
* Register all extension commands.
*/
function registerCommands(
context: vscode.ExtensionContext,
cli: CliWrapper
): void {
context.subscriptions.push(
vscode.commands.registerCommand(
"codeReviewGraph.buildGraph",
async () => {
const workspaceRoot = getWorkspaceRoot();
if (!workspaceRoot) {
vscode.window.showErrorMessage("No workspace folder is open.");
return;
}
const result = await cli.buildGraph(workspaceRoot);
if (result.success) {
await reinitialize(context);
vscode.window.showInformationMessage("Code Graph: Build complete.");
} else {
vscode.window.showErrorMessage(
`Code Graph: Build failed. ${result.stderr}`
);
}
}
)
);
context.subscriptions.push(
vscode.commands.registerCommand(
"codeReviewGraph.updateGraph",
async () => {
const workspaceRoot = getWorkspaceRoot();
if (!workspaceRoot) {
vscode.window.showErrorMessage("No workspace folder is open.");
return;
}
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: "Code Graph: Updating graph...",
cancellable: false,
},
async () => {
const result = await cli.updateGraph(workspaceRoot);
if (!result.success) {
vscode.window.showErrorMessage(
`Code Graph: Update failed. ${result.stderr}`
);
}
}
);
}
)
);
context.subscriptions.push(
vscode.commands.registerCommand(
"codeReviewGraph.showBlastRadius",
async (qualifiedNameOrUri?: string | vscode.Uri) => {
if (!sqliteReader) {
vscode.window.showWarningMessage(
"Code Graph: No graph database loaded."
);
return;
}
let qualifiedName: string | undefined;
if (typeof qualifiedNameOrUri === "string") {
qualifiedName = qualifiedNameOrUri;
} else {
qualifiedName = await vscode.window.showInputBox({
prompt:
"Enter the qualified name (e.g., my_module.MyClass.my_method)",
placeHolder: "my_module.my_function",
});
}
if (!qualifiedName) {
return;
}
// Find the file for this node and compute impact radius
const node = sqliteReader.getNode(qualifiedName);
if (!node) {
vscode.window.showInformationMessage(
`Code Graph: Node "${qualifiedName}" not found.`
);
return;
}
const config = vscode.workspace.getConfiguration("codeReviewGraph");
const depth = config.get<number>("blastRadiusDepth", 2);
const impact = sqliteReader.getImpactRadius([node.filePath], depth);
if (impact.impactedNodes.length === 0) {
vscode.window.showInformationMessage(
`Code Graph: No blast radius found for "${qualifiedName}".`
);
return;
}
await vscode.commands.executeCommand(
"codeReviewGraph.blastRadius.focus"
);
}
)
);
context.subscriptions.push(
vscode.commands.registerCommand(
"codeReviewGraph.findCallers",
async (qualifiedName?: string) => {
if (!sqliteReader) {
vscode.window.showWarningMessage(
"Code Graph: No graph database loaded."
);
return;
}
if (!qualifiedName) {
qualifiedName = await vscode.window.showInputBox({
prompt: "Enter the qualified name to find callers for",
placeHolder: "my_module.my_function",
});
}
if (!qualifiedName) {
return;
}
const edges = sqliteReader.getEdgesByTarget(qualifiedName);
const callerEdges = edges.filter((e) => e.kind === "CALLS");
if (callerEdges.length === 0) {
vscode.window.showInformationMessage(
`Code Graph: No callers found for "${qualifiedName}".`
);
return;
}
const items = callerEdges.map((e) => {
const callerNode = sqliteReader!.getNode(e.sourceQualified);
return {
label: callerNode?.name ?? e.sourceQualified,
description: callerNode?.filePath ?? e.filePath,
detail: `Line ${callerNode?.lineStart ?? e.line}`,
node: callerNode,
edge: e,
};
});
const selected = await vscode.window.showQuickPick(items, {
placeHolder: `Callers of ${qualifiedName}`,
});
if (selected?.node) {
await navigateToNode(selected.node);
}
}
)
);
context.subscriptions.push(
vscode.commands.registerCommand(
"codeReviewGraph.findTests",
async (qualifiedName?: string) => {
if (!sqliteReader) {
vscode.window.showWarningMessage(
"Code Graph: No graph database loaded."
);
return;
}
if (!qualifiedName) {
qualifiedName = await vscode.window.showInputBox({
prompt: "Enter the qualified name to find tests for",
placeHolder: "my_module.my_function",
});
}
if (!qualifiedName) {
return;
}
// Find tests via TESTED_BY edges
const edges = sqliteReader.getEdgesByTarget(qualifiedName);
const testEdges = edges.filter((e) => e.kind === "TESTED_BY");
// Also check reverse: source is the node, target is the test
const outEdges = sqliteReader.getEdgesBySource(qualifiedName);
const outTestEdges = outEdges.filter((e) => e.kind === "TESTED_BY");
const allTestQualifiedNames = new Set([
...testEdges.map((e) => e.sourceQualified),
...outTestEdges.map((e) => e.targetQualified),
]);
if (allTestQualifiedNames.size === 0) {
vscode.window.showInformationMessage(
`Code Graph: No tests found for "${qualifiedName}".`
);
return;
}
const items: Array<{
label: string;
description: string;
detail: string;
node: GraphNode | undefined;
}> = [];
for (const tqn of allTestQualifiedNames) {
const testNode = sqliteReader.getNode(tqn);
items.push({
label: testNode?.name ?? tqn,
description: testNode?.filePath ?? "",
detail: `Line ${testNode?.lineStart ?? "?"}`,
node: testNode,
});
}
const selected = await vscode.window.showQuickPick(items, {
placeHolder: `Tests for ${qualifiedName}`,
});
if (selected?.node) {
await navigateToNode(selected.node);
}
}
)
);
// -----------------------------------------------------------------
// codeReviewGraph.queryGraph — expose all 8 query patterns
// -----------------------------------------------------------------
context.subscriptions.push(
vscode.commands.registerCommand("codeReviewGraph.queryGraph", async () => {
if (!sqliteReader) {
vscode.window.showWarningMessage("Code Graph: No graph database loaded.");
return;
}
const patterns = [
{ label: "callers_of", description: "Find functions calling the target" },
{ label: "callees_of", description: "Find functions called by the target" },
{ label: "imports_of", description: "Find modules imported by a file" },
{ label: "importers_of", description: "Find files importing from the target" },
{ label: "children_of", description: "Find nodes contained in a file or class" },
{ label: "tests_for", description: "Find tests for a function or class" },
{ label: "inheritors_of", description: "Find classes inheriting/implementing the target" },
{ label: "file_summary", description: "List all nodes in a file" },
];
const pattern = await vscode.window.showQuickPick(patterns, {
placeHolder: "Select a query pattern",
});
if (!pattern) { return; }
const target = await vscode.window.showInputBox({
prompt: `Enter the target for ${pattern.label}`,
placeHolder: "e.g., my_module.py::my_function or path/to/file.py",
});
if (!target) { return; }
// Map pattern to edge kind + direction
type QueryDef = { edgeKind: string; direction: "incoming" | "outgoing"; nodeFilter?: string };
const queryMap: Record<string, QueryDef> = {
callers_of: { edgeKind: "CALLS", direction: "incoming" },
callees_of: { edgeKind: "CALLS", direction: "outgoing" },
imports_of: { edgeKind: "IMPORTS_FROM", direction: "outgoing" },
importers_of: { edgeKind: "IMPORTS_FROM", direction: "incoming" },
children_of: { edgeKind: "CONTAINS", direction: "outgoing" },
tests_for: { edgeKind: "TESTED_BY", direction: "incoming" },
inheritors_of: { edgeKind: "INHERITS", direction: "incoming" },
file_summary: { edgeKind: "CONTAINS", direction: "outgoing" },
};
const qdef = queryMap[pattern.label];
if (!qdef) { return; }
// Try exact match, then search
let node = sqliteReader.getNode(target);
if (!node) {
const matches = sqliteReader.searchNodes(target, 5);
if (matches.length === 1) {
node = matches[0];
} else if (matches.length > 1) {
const selected = await vscode.window.showQuickPick(
matches.map(m => ({
label: m.name,
description: `${m.kind} · ${m.filePath}`,
node: m,
})),
{ placeHolder: `Multiple matches for "${target}" — select one` },
);
if (!selected) { return; }
node = selected.node;
}
}
if (!node) {
vscode.window.showInformationMessage(`Code Graph: "${target}" not found.`);
return;
}
const edges = qdef.direction === "incoming"
? sqliteReader.getEdgesByTarget(node.qualifiedName)
: sqliteReader.getEdgesBySource(node.qualifiedName);
const filtered = edges.filter(e => e.kind === qdef.edgeKind);
if (filtered.length === 0) {
vscode.window.showInformationMessage(
`Code Graph: No ${pattern.label} results for "${node.name}".`
);
return;
}
const items = filtered.map(e => {
const relatedQn = qdef.direction === "incoming" ? e.sourceQualified : e.targetQualified;
const relatedNode = sqliteReader!.getNode(relatedQn);
return {
label: relatedNode?.name ?? relatedQn,
description: relatedNode ? `${relatedNode.kind} · ${relatedNode.filePath}` : "",
detail: `Line ${relatedNode?.lineStart ?? e.line}`,
node: relatedNode,
};
});
const selected = await vscode.window.showQuickPick(items, {
placeHolder: `${pattern.label}: ${node.name} (${filtered.length} results)`,
});
if (selected?.node) {
await navigateToNode(selected.node);
}
})
);
// -----------------------------------------------------------------
// codeReviewGraph.findCallees
// -----------------------------------------------------------------
context.subscriptions.push(
vscode.commands.registerCommand(
"codeReviewGraph.findCallees",
async (qualifiedName?: string) => {
if (!sqliteReader) {
vscode.window.showWarningMessage("Code Graph: No graph database loaded.");
return;
}
if (!qualifiedName) {
qualifiedName = await vscode.window.showInputBox({
prompt: "Enter the qualified name to find callees for",
placeHolder: "my_module.my_function",
});
}
if (!qualifiedName) { return; }
const edges = sqliteReader.getEdgesBySource(qualifiedName);
const calleeEdges = edges.filter(e => e.kind === "CALLS");
if (calleeEdges.length === 0) {
vscode.window.showInformationMessage(
`Code Graph: No callees found for "${qualifiedName}".`
);
return;
}
const items = calleeEdges.map(e => {
const calleeNode = sqliteReader!.getNode(e.targetQualified);
return {
label: calleeNode?.name ?? e.targetQualified,
description: calleeNode?.filePath ?? e.filePath,
detail: `Line ${calleeNode?.lineStart ?? e.line}`,
node: calleeNode,
};
});
const selected = await vscode.window.showQuickPick(items, {
placeHolder: `Callees of ${qualifiedName}`,
});
if (selected?.node) {
await navigateToNode(selected.node);
}
}
)
);
// -----------------------------------------------------------------
// codeReviewGraph.findLargeFunctions
// -----------------------------------------------------------------
context.subscriptions.push(
vscode.commands.registerCommand(
"codeReviewGraph.findLargeFunctions",
async () => {
if (!sqliteReader) {
vscode.window.showWarningMessage("Code Graph: No graph database loaded.");
return;
}
const minLinesStr = await vscode.window.showInputBox({
prompt: "Minimum line count threshold",
placeHolder: "50",
value: "50",
});
if (!minLinesStr) { return; }
const minLines = parseInt(minLinesStr, 10);
if (isNaN(minLines) || minLines < 1) {
vscode.window.showWarningMessage("Code Graph: Invalid line count.");
return;
}
const results = sqliteReader.getNodesBySize(minLines, undefined, undefined, 50);
if (results.length === 0) {
vscode.window.showInformationMessage(
`Code Graph: No functions found with ${minLines}+ lines.`
);
return;
}
const items = results.map(r => ({
label: `${r.name} (${r.lineCount} lines)`,
description: `${r.kind} · ${r.filePath}`,
detail: `Lines ${r.lineStart ?? "?"}${r.lineEnd ?? "?"}`,
node: r as GraphNode,
}));
const selected = await vscode.window.showQuickPick(items, {
placeHolder: `${results.length} nodes with ${minLines}+ lines`,
});
if (selected?.node) {
await navigateToNode(selected.node);
}
}
)
);
// -----------------------------------------------------------------
// codeReviewGraph.embedGraph
// -----------------------------------------------------------------
context.subscriptions.push(
vscode.commands.registerCommand("codeReviewGraph.embedGraph", async () => {
const workspaceRoot = getWorkspaceRoot();
if (!workspaceRoot) {
vscode.window.showErrorMessage("No workspace folder is open.");
return;
}
const result = await cli.embedGraph(workspaceRoot);
if (result.success) {
vscode.window.showInformationMessage("Code Graph: Embeddings computed.");
} else {
const msg = result.stderr.includes("not installed")
? "Install embeddings support: pip install code-review-graph[embeddings]"
: `Embedding failed: ${result.stderr}`;
vscode.window.showErrorMessage(`Code Graph: ${msg}`);
}
})
);
// -----------------------------------------------------------------
// codeReviewGraph.watchGraph
// -----------------------------------------------------------------
context.subscriptions.push(
vscode.commands.registerCommand("codeReviewGraph.watchGraph", async () => {
const workspaceRoot = getWorkspaceRoot();
if (!workspaceRoot) {
vscode.window.showErrorMessage("No workspace folder is open.");
return;
}
vscode.window.showInformationMessage("Code Graph: Watch mode started.");
const result = await cli.watchGraph(workspaceRoot);
if (!result.success) {
vscode.window.showErrorMessage(
`Code Graph: Watch failed. ${result.stderr}`
);
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand("codeReviewGraph.showGraph", async () => {
if (!sqliteReader) {
vscode.window.showWarningMessage(
"Code Graph: No graph database loaded. Run 'Code Graph: Build Graph' first."
);
return;
}
GraphWebviewPanel.createOrShow(context.extensionUri, sqliteReader);
})
);
context.subscriptions.push(
vscode.commands.registerCommand("codeReviewGraph.search", async () => {
if (!sqliteReader) {
vscode.window.showWarningMessage(
"Code Graph: No graph database loaded."
);
return;
}
const query = await vscode.window.showInputBox({
prompt: "Search the code graph",
placeHolder: "Enter a function, class, or module name",
});
if (!query) {
return;
}
const results = sqliteReader.searchNodes(query);
if (results.length === 0) {
vscode.window.showInformationMessage(
`Code Graph: No results found for "${query}".`
);
return;
}
const items = results.map((r) => ({
label: r.name,
description: r.kind,
detail: r.filePath
? `${r.filePath}:${r.lineStart ?? ""}`
: undefined,
result: r,
}));
const selected = await vscode.window.showQuickPick(items, {
placeHolder: `Results for "${query}"`,
});
if (selected?.result) {
await navigateToNode(selected.result);
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand(
"codeReviewGraph.reviewChanges",
async () => {
if (!sqliteReader) {
vscode.window.showWarningMessage(
"Code Graph: No graph database loaded."
);
return;
}
const workspaceRoot = getWorkspaceRoot();
if (!workspaceRoot) {
vscode.window.showErrorMessage("No workspace folder is open.");
return;
}
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: "Code Graph: Analyzing changes...",
cancellable: false,
},
async () => {
const { execFile } = await import("node:child_process");
const { promisify } = await import("node:util");
const execFileAsync = promisify(execFile);
let changedFiles: string[] = [];
try {
const r1 = await execFileAsync(
"git", ["diff", "--name-only", "HEAD"],
{ cwd: workspaceRoot }
);
const r2 = await execFileAsync(
"git", ["diff", "--cached", "--name-only"],
{ cwd: workspaceRoot }
);
changedFiles = [...new Set([
...r1.stdout.trim().split("\n").filter(Boolean),
...r2.stdout.trim().split("\n").filter(Boolean),
])];
} catch {
// git not available or not a git repo
}
if (changedFiles.length === 0) {
vscode.window.showInformationMessage(
"Code Graph: No changes detected."
);
return;
}
const absFiles = changedFiles.map(f => path.join(workspaceRoot, f));
const impact = sqliteReader!.getImpactRadius(absFiles);
// --- Generate review guidance ---
const guidance: string[] = [];
const impactedFileCount = new Set(
impact.impactedNodes.map(n => n.filePath)
).size;
// Test coverage check
const untestedFns: string[] = [];
for (const node of impact.changedNodes) {
if (node.kind !== "Function" || node.isTest) { continue; }
const edges = sqliteReader!.getEdgesByTarget(node.qualifiedName);
const hasCoverage = edges.some(e => e.kind === "TESTED_BY");
if (!hasCoverage) {
const out = sqliteReader!.getEdgesBySource(node.qualifiedName);
if (!out.some(e => e.kind === "TESTED_BY")) {
untestedFns.push(node.name);
}
}
}
if (untestedFns.length > 0) {
guidance.push(
`\u26a0\ufe0f **${untestedFns.length} changed function(s) lack test coverage**: ${untestedFns.slice(0, 5).join(", ")}${untestedFns.length > 5 ? "..." : ""}`
);
}
// Wide blast radius warning
if (impactedFileCount > 10) {
guidance.push(
`\u26a0\ufe0f **Wide blast radius**: ${impactedFileCount} files impacted — consider splitting this change.`
);
}
// Inheritance changes
const inheritanceChanges = impact.edges.filter(
e => e.kind === "INHERITS" || e.kind === "IMPLEMENTS"
);
if (inheritanceChanges.length > 0) {
guidance.push(
`\u26a0\ufe0f **Inheritance chain affected**: ${inheritanceChanges.length} inheritance/implementation edge(s) touched.`
);
}
// Cross-file impacts
if (impact.impactedNodes.length > 0) {
guidance.push(
`\u2139\ufe0f ${impact.impactedNodes.length} nodes in ${impactedFileCount} file(s) may be affected by these changes.`
);
}
// Show guidance in output channel
const channel = vscode.window.createOutputChannel("Code Graph Review", { log: true });
channel.appendLine("# Review Guidance");
channel.appendLine("");
channel.appendLine(`Changed files: ${changedFiles.length}`);
channel.appendLine(`Changed nodes: ${impact.changedNodes.length}`);
channel.appendLine(`Impacted nodes: ${impact.impactedNodes.length}`);
channel.appendLine(`Impacted files: ${impactedFileCount}`);
channel.appendLine("");
if (guidance.length > 0) {
for (const g of guidance) { channel.appendLine(g); }
} else {
channel.appendLine("\u2705 No concerns detected.");
}
channel.show(true);
// Also show in graph
GraphWebviewPanel.createOrShow(
context.extensionUri,
sqliteReader!,
impact
);
// Update SCM decorations
if (scmDecorationProvider && sqliteReader) {
await scmDecorationProvider.update(sqliteReader, workspaceRoot);
}
}
);
}
)
);
}
/**
* Reinitialize the reader and tree providers after a graph rebuild.
*/
async function reinitialize(
context: vscode.ExtensionContext
): Promise<void> {
const workspaceRoot = getWorkspaceRoot();
if (!workspaceRoot) {
return;
}
const dbPath = findGraphDb(workspaceRoot);
if (!dbPath) {
return;
}
sqliteReader?.close();
sqliteReader = new SqliteReader(dbPath);
// Refresh tree views
await vscode.commands.executeCommand(
"codeReviewGraph.codeGraph.refresh"
);
}
/**
* Set up a FileSystemWatcher to detect changes to the graph database.
*/
function watchGraphDb(context: vscode.ExtensionContext): void {
const watcher = vscode.workspace.createFileSystemWatcher(
"**/.code-review-graph/graph.db"
);
const dbPathRef = { current: "" };
const workspaceRoot = getWorkspaceRoot();
if (workspaceRoot) {
const dbPath = findGraphDb(workspaceRoot);
if (dbPath) {
dbPathRef.current = dbPath;
}
}
watcher.onDidChange(() => {
// Close and reopen to pick up external writes
if (sqliteReader && dbPathRef.current) {
sqliteReader.close();
sqliteReader = new SqliteReader(dbPathRef.current);
vscode.commands.executeCommand("codeReviewGraph.codeGraph.refresh");
}
});
watcher.onDidCreate(async () => {
const wsRoot = getWorkspaceRoot();
if (wsRoot && !sqliteReader) {
const dbPath = findGraphDb(wsRoot);
if (dbPath) {
dbPathRef.current = dbPath;
sqliteReader = new SqliteReader(dbPath);
vscode.commands.executeCommand("codeReviewGraph.codeGraph.refresh");
}
}
});
watcher.onDidDelete(() => {
sqliteReader?.close();
sqliteReader = undefined;
dbPathRef.current = "";
});
context.subscriptions.push(watcher);
}
/**
* Set up debounced auto-update on file save.
*/
function setupAutoUpdate(
context: vscode.ExtensionContext,
cli: CliWrapper
): void {
const AUTO_UPDATE_DEBOUNCE_MS = 2000;
const onSave = vscode.workspace.onDidSaveTextDocument(() => {
const config = vscode.workspace.getConfiguration("codeReviewGraph");
if (!config.get<boolean>("autoUpdate", true)) {
return;
}
if (autoUpdateTimer) {
clearTimeout(autoUpdateTimer);
}
autoUpdateTimer = setTimeout(async () => {
const wsRoot = getWorkspaceRoot();
if (!wsRoot || !sqliteReader) {
return;
}
try {
await cli.updateGraph(wsRoot);
} catch {
// Silently ignore update errors on save; user can manually update
}
}, AUTO_UPDATE_DEBOUNCE_MS);
});
context.subscriptions.push(onSave);
}
/**
* Extension activation entry point.
*/
export async function activate(
context: vscode.ExtensionContext
): Promise<void> {
const cli = new CliWrapper();
const installer = new Installer(cli);
// Register walkthrough commands
registerWalkthroughCommands(context, cli, installer);
const workspaceRoot = getWorkspaceRoot();
if (workspaceRoot) {
const dbPath = findGraphDb(workspaceRoot);
if (dbPath) {
// Graph database found - initialize
sqliteReader = new SqliteReader(dbPath);
// Schema compatibility check
const schemaWarning = sqliteReader.checkSchemaCompatibility();
if (schemaWarning) {
const choice = await vscode.window.showWarningMessage(
`Code Graph: ${schemaWarning}`,
"Rebuild Graph",
"Dismiss"
);
if (choice === "Rebuild Graph") {
await vscode.commands.executeCommand("codeReviewGraph.buildGraph");
}
}
// Register tree view providers
const codeGraphProvider = new CodeGraphTreeProvider(
sqliteReader,
workspaceRoot
);
const blastRadiusProvider = new BlastRadiusTreeProvider();
const statsProvider = new StatsTreeProvider(sqliteReader);
context.subscriptions.push(
vscode.window.registerTreeDataProvider(
"codeReviewGraph.codeGraph",
codeGraphProvider
),
vscode.window.registerTreeDataProvider(
"codeReviewGraph.blastRadius",
blastRadiusProvider
),
vscode.window.registerTreeDataProvider(
"codeReviewGraph.stats",
statsProvider
)
);
// Create status bar
const statusBar = new StatusBar();
statusBar.update(sqliteReader);
statusBar.show();
context.subscriptions.push(statusBar);
// Register SCM file decoration provider
scmDecorationProvider = new ScmDecorationProvider();
context.subscriptions.push(
vscode.window.registerFileDecorationProvider(scmDecorationProvider)
);
} else {
// No graph database found - show welcome
showWelcomeIfNeeded(context);
}
}
// Register revealInTree command for bidirectional graph→tree sync
context.subscriptions.push(
vscode.commands.registerCommand(
"codeReviewGraph.revealInTree",
(_qualifiedName: string) => {
// When a node is clicked in the graph, highlight it in the graph
// The graph webview already calls this; the tree view sync relies
// on the file navigation that nodeClicked also triggers.
// This command is a hook for future tree reveal integration.
GraphWebviewPanel.highlightNode(_qualifiedName);
}
)
);
// Register commands (always, even without a database)
registerCommands(context, cli);
// Watch for graph.db changes
watchGraphDb(context);
// Set up auto-update on save
setupAutoUpdate(context, cli);
}
/**
* Extension deactivation cleanup.
*/
export function deactivate(): void {
if (autoUpdateTimer) {
clearTimeout(autoUpdateTimer);
autoUpdateTimer = undefined;
}
sqliteReader?.close();
sqliteReader = undefined;
}