Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Code Explainer is a VS Code extension scaffold for branch-aware code comprehensi
- Compare the current branch with `main`
- Explain the current selection with surrounding context
- Trace relationships between files, symbols, tests, and configuration
- Generate a reviewable GitHub PR title and description for the current branch

## Development

Expand Down
34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"onCommand:codeExplainer.explainRepo",
"onCommand:codeExplainer.explainSelection",
"onCommand:codeExplainer.compareBranch",
"onCommand:codeExplainer.generatePrDescription",
"onCommand:codeExplainer.traceRelationships",
"onCommand:codeExplainer.drawFlowChart",
"onView:codeExplainer.sidebar"
Expand All @@ -38,6 +39,10 @@
"command": "codeExplainer.traceRelationships",
"title": "Code Explainer: Trace Relationships"
},
{
"command": "codeExplainer.generatePrDescription",
"title": "Code Explainer: Generate PR Description"
},
{
"command": "codeExplainer.drawFlowChart",
"title": "Code Explainer: Draw Current Branch Diagram"
Expand Down Expand Up @@ -77,12 +82,20 @@
{
"command": "codeExplainer.drawFlowChart",
"group": "navigation@102"
},
{
"command": "codeExplainer.generatePrDescription",
"group": "navigation@103"
}
],
"explorer/context": [
{
"command": "codeExplainer.explainRepo",
"group": "navigation@100"
},
{
"command": "codeExplainer.generatePrDescription",
"group": "navigation@101"
}
]
},
Expand Down Expand Up @@ -114,6 +127,27 @@
"default": true,
"description": "Render custom flow-chart visualizations in the explanation panel when available."
},
"codeExplainer.prDescription.defaultStyle": {
"type": "string",
"enum": [
"business-stakeholder",
"code-collaborator",
"manager",
"other"
],
"default": "manager",
"description": "Default audience/style for generated PR descriptions."
},
"codeExplainer.prDescription.defaultGuidelines": {
"type": "string",
"default": "",
"description": "Optional default team guidance or house rules to apply when generating PR descriptions."
},
"codeExplainer.prDescription.defaultTemplate": {
"type": "string",
"default": "",
"description": "Optional default markdown template or preferred section structure for generated PR descriptions."
},
"codeExplainer.openai.apiKey": {
"type": "string",
"default": "",
Expand Down
2 changes: 1 addition & 1 deletion src/commands/compareBranch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function createCompareBranchCommand(
getFresh: () => analysisService.analyze(),
render: (result, refresh) =>
panel.show(result, {
onAction: (action) => void handlePanelAction(action),
onAction: (action) => void handlePanelAction(action, panel),
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: refresh,
}),
Expand Down
47 changes: 47 additions & 0 deletions src/commands/compareFileWithBranch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as path from "path";
import * as vscode from "vscode";
import { BranchAnalysisService } from "../services/analysis/BranchAnalysisService";
import { GitService } from "../services/git/GitService";
import { CacheService } from "../storage/CacheService";
import { CodeExplainerProvider } from "../ui/sidebar/CodeExplainerProvider";
import { ResultsPanel } from "../ui/webview/panel";
import { handlePanelAction, openFileRef } from "./shared";

export function createCompareFileWithBranchCommand(
panel: ResultsPanel,
analysisService: BranchAnalysisService,
cache: CacheService,
sidebarProvider: CodeExplainerProvider
) {
return async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
throw new Error("Open a file before comparing it with the base branch.");
}

const filePath = editor.document.uri.fsPath;
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;

if (!workspaceRoot) {
throw new Error("Open a workspace folder before comparing.");
}

const config = vscode.workspace.getConfiguration("codeExplainer");
const baseBranch = config.get<string>("baseBranch", "main");
const git = new GitService(workspaceRoot);

if (!(await git.isGitRepo())) {
throw new Error("The current workspace is not a git repository.");
}

// Get the changes for this specific file
const result = await analysisService.analyzeFile(filePath, baseBranch);
const fileName = path.basename(filePath);

panel.show(result, {
onAction: (action) => void handlePanelAction(action, panel),
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: () => void createCompareFileWithBranchCommand(panel, analysisService, cache, sidebarProvider)(),
});
};
}
27 changes: 20 additions & 7 deletions src/commands/drawFlowChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function createDrawFlowChartCommand(
cache: CacheService,
sidebarProvider: CodeExplainerProvider
) {
return async (forceRefresh = false) => {
return async (directoryPath?: string, forceRefresh = false) => {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;

if (!workspaceRoot) {
Expand All @@ -25,22 +25,35 @@ export function createDrawFlowChartCommand(
}

const branchName = await git.getCurrentBranch();
const cacheKey = `flow:${workspaceRoot}:${branchName}`;
const label = `Diagram: ${branchName}`;

let cacheKey: string;
let label: string;
let loadingMessage: string;

if (directoryPath) {
const dirName = directoryPath.split("/").pop() || "directory";
cacheKey = `flow:${workspaceRoot}:${branchName}:${directoryPath}`;
label = `Diagram: ${dirName}`;
loadingMessage = `Building a diagram for the ${dirName} directory.`;
} else {
cacheKey = `flow:${workspaceRoot}:${branchName}`;
label = `Diagram: ${branchName}`;
loadingMessage = `Building an overall diagram for ${branchName}.`;
}

await showCachedOrFresh({
panel,
cache,
sidebarProvider,
cacheKey,
label,
source: { kind: "flow", branchName },
loadingMessage: `Building an overall diagram for ${branchName}.`,
source: { kind: "flow", branchName, directoryPath },
loadingMessage,
forceRefresh,
getFresh: () => analysisService.analyze(),
getFresh: () => analysisService.analyze(directoryPath),
render: (result, refresh) =>
panel.show(result, {
onAction: (action) => void handlePanelAction(action),
onAction: (action) => void handlePanelAction(action, panel),
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: refresh,
}),
Expand Down
63 changes: 63 additions & 0 deletions src/commands/explainDirectory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as path from "path";
import * as vscode from "vscode";
import { DirectoryAnalysisService } from "../services/analysis/DirectoryAnalysisService";
import { CacheService } from "../storage/CacheService";
import { CodeExplainerProvider } from "../ui/sidebar/CodeExplainerProvider";
import { ResultsPanel } from "../ui/webview/panel";
import { handlePanelAction, openFileRef, showCachedOrFresh } from "./shared";

export function createExplainDirectoryCommand(
panel: ResultsPanel,
analysisService: DirectoryAnalysisService,
cache: CacheService,
sidebarProvider: CodeExplainerProvider
) {
return async (uri?: vscode.Uri, forceRefresh = false) => {
let directoryPath: string;

if (uri) {
// Called from context menu on a folder
const stat = await vscode.workspace.fs.stat(uri);
if (stat.type === vscode.FileType.Directory) {
directoryPath = uri.fsPath;
} else {
// If it's a file, use its parent directory
directoryPath = path.dirname(uri.fsPath);
}
} else {
// Called from command palette - use active editor's directory
const editor = vscode.window.activeTextEditor;
if (!editor) {
throw new Error("Open a file or select a folder before using Explain Directory.");
}
directoryPath = path.dirname(editor.document.uri.fsPath);
}

const folder = vscode.workspace.workspaceFolders?.[0];
if (!folder) {
throw new Error("Open a workspace folder before using Code Explainer.");
}

const relativePath = path.relative(folder.uri.fsPath, directoryPath);
const dirName = path.basename(directoryPath);
const cacheKey = `directory:${folder.uri.fsPath}:${relativePath}`;

await showCachedOrFresh({
panel,
cache,
sidebarProvider,
cacheKey,
label: `Directory: ${dirName}`,
source: { kind: "directory", directoryPath },
loadingMessage: `Analyzing the ${dirName} directory.`,
forceRefresh,
getFresh: () => analysisService.analyze(directoryPath),
render: (result, refresh, source) =>
panel.show(result, {
onAction: (action) => void handlePanelAction(action, panel),
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: refresh,
}, source),
});
};
}
2 changes: 1 addition & 1 deletion src/commands/explainRepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function createExplainRepoCommand(
getFresh: () => analysisService.analyze(),
render: (result, refresh) =>
panel.show(result, {
onAction: (action) => void handlePanelAction(action),
onAction: (action) => void handlePanelAction(action, panel),
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: refresh,
}),
Expand Down
2 changes: 1 addition & 1 deletion src/commands/explainSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function createExplainSelectionCommand(
getFresh: () => analysisService.explainSelection({ filePath, startLine, endLine }),
render: (result, refresh) =>
panel.show(result, {
onAction: (action) => void handlePanelAction(action),
onAction: (action) => void handlePanelAction(action, panel),
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: refresh,
}),
Expand Down
106 changes: 106 additions & 0 deletions src/commands/generatePrDescription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as vscode from "vscode";
import { PrDescriptionExplanation, PrDescriptionStyle } from "../models/types";
import { PrDescriptionAnalysisService } from "../services/analysis/PrDescriptionAnalysisService";
import { ResultsPanel } from "../ui/webview/panel";
import { openFileRef } from "./shared";

type DraftPanelMessage =
| {
type: "prRegenerate";
title: string;
body: string;
style: PrDescriptionStyle;
customInstructions: string;
}
| {
type: "prApply";
title: string;
body: string;
style: PrDescriptionStyle;
customInstructions: string;
};

export function createGeneratePrDescriptionCommand(
panel: ResultsPanel,
analysisService: PrDescriptionAnalysisService
) {
return async () => {
const renderResult = (result: PrDescriptionExplanation) => {
panel.show(result, {
onAction: () => undefined,
onFileRef: (fileRef) => void openFileRef(fileRef),
onRefresh: () => {
void runAnalysis();
},
onMessage: (message) => {
void handlePanelMessage(message as DraftPanelMessage, result, renderResult);
},
});
};

const runAnalysis = async (options?: { style?: PrDescriptionStyle; customInstructions?: string }) => {
panel.showLoading("Generate PR Description", "Reviewing the current branch and preparing a PR description draft.");
const result = await analysisService.analyze(options);
renderResult(result);
};

const handlePanelMessage = async (
message: DraftPanelMessage,
currentResult: PrDescriptionExplanation,
render: (result: PrDescriptionExplanation) => void
) => {
if (message.type === "prRegenerate") {
await runAnalysis({
style: message.style,
customInstructions: message.customInstructions,
});
return;
}

if (message.type !== "prApply") {
return;
}

panel.showLoading("Generate PR Description", "Applying the reviewed draft to GitHub.");
const applied = await analysisService.applyDraft({
draft: currentResult,
title: message.title,
body: message.body,
});

if (applied.status === "cancelled") {
render({
...currentResult,
draftTitle: message.title,
draftBody: message.body,
style: message.style,
customInstructions: message.customInstructions,
});
return;
}

const updatedResult = analysisService.withAppliedDraft(
{
...currentResult,
style: message.style,
customInstructions: message.customInstructions,
},
message.title,
message.body,
applied.pullRequest
);
render(updatedResult);

const messageText = applied.status === "created"
? `Created pull request #${applied.pullRequest?.number}.`
: `Updated pull request #${applied.pullRequest?.number}.`;
const openAction = "Open PR";
const selection = await vscode.window.showInformationMessage(messageText, openAction);
if (selection === openAction && applied.pullRequest?.url) {
await vscode.env.openExternal(vscode.Uri.parse(applied.pullRequest.url));
}
};

await runAnalysis();
};
}
Loading