Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
75b036b
Add CLAUDE.md to provide guidance for Claude Code
lambdalisue Jul 5, 2025
6bdfbe0
feat(source): add window source for listing Vim windows
lambdalisue Jul 6, 2025
ad2d410
feat(source): add tabpage source for listing Vim tab pages
lambdalisue Jul 6, 2025
9004a59
feat(source): add loclist source for listing location list items
lambdalisue Jul 6, 2025
d337686
feat(source): add colorscheme source for listing available colorschemes
lambdalisue Jul 6, 2025
d8a627c
feat(source): add highlight source for listing highlight groups
lambdalisue Jul 6, 2025
33e58cd
feat(source): add jumplist source for listing jump locations
lambdalisue Jul 6, 2025
fd1a7c8
feat(source): add register source for listing Vim registers
lambdalisue Jul 6, 2025
5006534
feat(source): add mark source for listing Vim marks
lambdalisue Jul 6, 2025
1c0e825
feat(source): add command source for listing user-defined commands
lambdalisue Jul 6, 2025
9c39f46
feat(source): add mapping source for listing key mappings
lambdalisue Jul 6, 2025
e8f21f1
feat(source): add git status source for listing modified files
lambdalisue Jul 6, 2025
881a059
feat(source): add grep source for vim's :grep command results
lambdalisue Jul 6, 2025
c3bf51b
feat(source): add vimgrep source for vim's :vimgrep command results
lambdalisue Jul 6, 2025
89e0265
feat(source): add autocmd source for vim autocmds
lambdalisue Jul 6, 2025
c74961d
feat(previewer): add shell command previewer
lambdalisue Jul 6, 2025
bfba569
feat(renderer): add file info renderer for displaying file metadata
lambdalisue Jul 6, 2025
8090011
feat(renderer): add buffer info renderer for displaying buffer metadata
lambdalisue Jul 6, 2025
0f0a8f2
feat(renderer): add smart grep renderer for formatting grep-like results
lambdalisue Jul 6, 2025
e9a3319
feat(refiner): add file info refiner for filtering by file properties
lambdalisue Jul 6, 2025
c166b92
feat(refiner): add buffer info refiner for filtering by buffer proper…
lambdalisue Jul 6, 2025
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
213 changes: 213 additions & 0 deletions builtin/source/git_status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import * as fn from "@denops/std/function";
import { join } from "@std/path/join";

import { defineSource, type Source } from "../../source.ts";

type Detail = {
/**
* File path relative to git root
*/
path: string;

/**
* Absolute file path
*/
absolutePath: string;

/**
* Git status code (e.g., "M", "A", "D", "??")
*/
status: string;

/**
* Human-readable status description
*/
statusDescription: string;

/**
* Whether the file is staged
*/
staged: boolean;

/**
* Whether the file is unstaged
*/
unstaged: boolean;
};

export type GitStatusOptions = {
/**
* Whether to include untracked files.
* @default true
*/
includeUntracked?: boolean;

/**
* Whether to include ignored files.
* @default false
*/
includeIgnored?: boolean;

/**
* Whether to show status in submodules.
* @default false
*/
includeSubmodules?: boolean;
};

// Git status format codes
const STATUS_CODES = {
STAGED: {
M: "modified",
A: "added",
D: "deleted",
R: "renamed",
C: "copied",
},
UNSTAGED: {
M: "modified",
D: "deleted",
},
UNTRACKED: "??",
IGNORED: "!!",
} as const;

/**
* Creates a Source that generates items from git status.
*
* This Source runs `git status` and generates items for each modified,
* staged, or untracked file in the repository.
*
* @param options - Options to customize git status listing.
* @returns A Source that generates items representing git status files.
*/
export function gitStatus(
options: Readonly<GitStatusOptions> = {},
): Source<Detail> {
const includeUntracked = options.includeUntracked ?? true;
const includeIgnored = options.includeIgnored ?? false;
const includeSubmodules = options.includeSubmodules ?? false;

return defineSource(async function* (denops, _params, { signal }) {
// Get current working directory
const cwd = await fn.getcwd(denops);
signal?.throwIfAborted();

// Build git status command
const args = ["status", "--porcelain=v1"];
if (includeUntracked) {
args.push("-u");
} else {
args.push("-uno");
}
if (includeIgnored) {
args.push("--ignored");
}
if (!includeSubmodules) {
args.push("--ignore-submodules");
}

try {
// Run git status
const cmd = new Deno.Command("git", {
args,
cwd,
stdout: "piped",
stderr: "piped",
signal,
});

const { stdout, stderr, success } = await cmd.output();

if (!success) {
// Not a git repository or git command failed
const errorText = new TextDecoder().decode(stderr);
if (errorText.includes("not a git repository")) {
// Silently return empty - not an error condition
return;
}
throw new Error(`git status failed: ${errorText}`);
}

// Parse git status output
const output = new TextDecoder().decode(stdout);
const lines = output.trim().split("\n").filter((line) => line);

const items = lines.map((line, index) => {
// Git status format: XY filename
// X = staged status, Y = unstaged status
const staged = line[0];
const unstaged = line[1];
const filename = line.substring(3);

// Determine status code and description
const status = `${staged}${unstaged}`;
let statusDescription = "";
let isStaged = false;
let isUnstaged = false;

// Parse status codes
if (status === STATUS_CODES.UNTRACKED) {
statusDescription = "untracked";
isUnstaged = true;
} else if (status === STATUS_CODES.IGNORED) {
statusDescription = "ignored";
} else {
// Handle staged status
const stagedDesc =
STATUS_CODES.STAGED[staged as keyof typeof STATUS_CODES.STAGED];
if (stagedDesc) {
statusDescription = stagedDesc;
isStaged = true;
}

// Handle unstaged status
const unstagedDesc = STATUS_CODES
.UNSTAGED[unstaged as keyof typeof STATUS_CODES.UNSTAGED];
if (unstagedDesc) {
statusDescription += isStaged ? `, ${unstagedDesc}` : unstagedDesc;
isUnstaged = true;
}
}

// Create status indicator
const indicator = status === STATUS_CODES.UNTRACKED
? "[?]"
: status === STATUS_CODES.IGNORED
? "[!]"
: `[${status}]`;

// Format display value
const absolutePath = join(cwd, filename);
const displayPath = filename;
const value = `${indicator.padEnd(5)} ${displayPath}`;
Comment on lines +137 to +183
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a parser as a pure function so that we can easily test.


return {
id: index,
value,
detail: {
path: filename,
absolutePath,
status,
statusDescription,
staged: isStaged,
unstaged: isUnstaged,
},
};
});

yield* items;
} catch (err) {
// Handle errors gracefully
if (err instanceof Error) {
if (err.name === "NotFound") {
// Git not installed - silently return empty
return;
}
// Re-throw other errors with context
throw new Error(`Failed to get git status: ${err.message}`);
}
throw err;
}
});
}
1 change: 1 addition & 0 deletions builtin/source/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from "./buffer.ts";
export * from "./colorscheme.ts";
export * from "./command.ts";
export * from "./file.ts";
export * from "./git_status.ts";
export * from "./helptag.ts";
export * from "./highlight.ts";
export * from "./history.ts";
Expand Down