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
24 changes: 24 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
"command": "oil-code.cd",
"title": "Oil: cd - Change directory to the currently opened directory",
"when": "editorTextFocus && editorLangId == oil"
},
{
"command": "oil-code.toggleDetails",
"title": "Oil: Toggle Detail Columns"
}
],
"languages": [
Expand Down Expand Up @@ -129,6 +133,11 @@
"command": "oil-code.cd",
"key": "alt+`",
"when": "editorTextFocus && editorLangId == oil"
},
{
"command": "oil-code.toggleDetails",
"key": "alt+shift+d",
"when": "editorTextFocus && editorLangId == oil"
}
],
"grammars": [
Expand Down Expand Up @@ -170,6 +179,21 @@
"type": "boolean",
"default": false,
"description": "Enable alternate confirmation dialog for file operations. When enabled, uses a QuickPick interface instead of the default modal confirmation dialog. Default is false."
},
"oil-code.columns": {
"type": "array",
"items": {
"type": "string",
"enum": ["icon", "permissions", "size", "mtime"],
"enumDescriptions": [
"File type icon",
"Unix-style file permissions (e.g. -rw-r--r--)",
"File size in human-readable form (e.g. 12K)",
"Last modified time (e.g. Mar 14 14:23)"
]
},
"default": ["icon"],
"description": "Columns to display in oil directory listings. 'icon' controls the file type icon. Reload the oil buffer after changing this setting."
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/commands/refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export async function refresh() {
try {
// Clear the visited path cache for the current directory to force refresh from disk
oilState.visitedPaths.delete(currentPath);
oilState.metadataCache.delete(currentPath);

// Get updated directory content from disk
const directoryContent = await getDirectoryListing(
Expand Down
14 changes: 14 additions & 0 deletions src/commands/toggleDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as vscode from "vscode";
import { toggleDetailsVisible } from "../state/columnState";
import { updateDecorations } from "../decorations";

export function toggleDetails(): void {
toggleDetailsVisible();

// Redraw decorations on all visible oil editors
for (const editor of vscode.window.visibleTextEditors) {
if (editor.document.languageId === "oil") {
updateDecorations(editor);
}
}
}
9 changes: 9 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import * as vscode from "vscode";

export type MetadataColumn = "icon" | "permissions" | "size" | "mtime";

export interface FileMetadata {
permissions: string;
size: string;
mtime: string;
}

export interface OilEntry {
identifier: string;
value: string;
Expand All @@ -13,6 +21,7 @@ export interface OilState {
identifierCounter: number;
visitedPaths: Map<string, string[]>;
editedPaths: Map<string, string[]>;
metadataCache: Map<string, Map<string, FileMetadata>>;
openAfterSave?: string;
}

Expand Down
65 changes: 65 additions & 0 deletions src/decorations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import * as vscode from "vscode";
import * as path from "path";
import { getNerdFontFileIcon } from "./nerd-fonts";
import { peekOilState } from "./state/oilState";
import { getDetailsVisible } from "./state/columnState";
import { getColumnsSettings } from "./utils/settings";
import { formatMetadataColumns } from "./utils/metadataUtils";
import { oilUriToDiskPath, normalizePathToUri, removeTrailingSlash } from "./utils/pathUtils";

// Create decoration type for hidden prefix
const hiddenPrefixDecoration = vscode.window.createTextEditorDecorationType({
Expand Down Expand Up @@ -111,6 +116,20 @@ function getFileIcon(fileName: string, isDirectory: boolean): string {
// Decoration types for different file types (created on demand)
const fileIconDecorations = new Map<string, vscode.TextEditorDecorationType>();

// Single decoration type for all metadata before-text (style only; contentText is per-instance).
// Applied AFTER icon decorations so VSCode stacks it between the icon and the filename text.
let metadataDecorationType: vscode.TextEditorDecorationType | null = null;
function getMetadataDecorationType(): vscode.TextEditorDecorationType {
if (!metadataDecorationType) {
metadataDecorationType = vscode.window.createTextEditorDecorationType({
before: {
color: new vscode.ThemeColor("editorInlayHint.foreground"),
},
});
}
return metadataDecorationType;
}

// Apply decorations to hide prefixes
export function updateDecorations(editor: vscode.TextEditor | undefined) {
if (!editor || editor.document.languageId !== "oil") {
Expand All @@ -125,9 +144,35 @@ export function updateDecorations(editor: vscode.TextEditor | undefined) {
editor.setDecorations(decoration, []);
});

// Clear previous metadata decorations (avoid creating the decoration type too early)
if (metadataDecorationType) {
editor.setDecorations(metadataDecorationType, []);
}

// Track icon decorations for this update
const iconDecorations = new Map<string, vscode.Range[]>();

// Determine if metadata columns should be rendered
const oilState = peekOilState();
const columns = getColumnsSettings();
const detailsVisible = getDetailsVisible();
const metaColumns = columns.filter((c) => c !== "icon");
const showMetadata = oilState !== undefined && detailsVisible && metaColumns.length > 0;

// Resolve the metadata map for the current directory (once, before the line loop)
let metadataMap: Map<string, import("./constants").FileMetadata> | undefined;
if (showMetadata) {
try {
const diskPath = oilUriToDiskPath(document.uri);
const folderPathUri = removeTrailingSlash(normalizePathToUri(diskPath));
metadataMap = oilState!.metadataCache.get(folderPathUri);
} catch {
// Not a valid oil URI β€” skip metadata rendering
}
}

const metaDecorations: vscode.DecorationOptions[] = [];

// Add icon after the prefix and space
// Get appropriate icon based on configuration
const config = vscode.workspace.getConfiguration("oil-code");
Expand Down Expand Up @@ -196,6 +241,23 @@ export function updateDecorations(editor: vscode.TextEditor | undefined) {
)
);

// Add metadata before-decoration for this line (renders between icon and filename)
if (showMetadata && metadataMap) {
const meta = metadataMap.get(fileName);
if (meta) {
const metaText = formatMetadataColumns(meta, metaColumns);
if (metaText) {
const filenameStartPos = new vscode.Position(i, prefixLength);
metaDecorations.push({
range: new vscode.Range(filenameStartPos, filenameStartPos),
renderOptions: {
before: { contentText: metaText },
},
});
}
}
}

// If cursor is within the prefix area, move it to the first visible character
for (let selection of editor.selections) {
if (
Expand Down Expand Up @@ -226,6 +288,9 @@ export function updateDecorations(editor: vscode.TextEditor | undefined) {
editor.setDecorations(decoration, ranges);
}
}

// Apply metadata after-decorations
editor.setDecorations(getMetadataDecorationType(), metaDecorations);
}

// Disposable for cleanup
Expand Down
4 changes: 3 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { openCwd } from "./commands/openCwd";
import { preview } from "./commands/preview";
import { refresh } from "./commands/refresh";
import { cd } from "./commands/cd";
import { toggleDetails } from "./commands/toggleDetails";
import { logger } from "./logger";

// In your extension's activate function
Expand Down Expand Up @@ -79,7 +80,8 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand("oil-code.openCwd", openCwd),
vscode.commands.registerCommand("oil-code.preview", preview),
vscode.commands.registerCommand("oil-code.refresh", refresh),
vscode.commands.registerCommand("oil-code.cd", cd)
vscode.commands.registerCommand("oil-code.cd", cd),
vscode.commands.registerCommand("oil-code.toggleDetails", toggleDetails)
);

// Make initial attempt to register Vim keymaps with retries
Expand Down
13 changes: 13 additions & 0 deletions src/state/columnState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
let _detailsVisible = true;

export function getDetailsVisible(): boolean {
return _detailsVisible;
}

export function setDetailsVisible(v: boolean): void {
_detailsVisible = v;
}

export function toggleDetailsVisible(): void {
_detailsVisible = !_detailsVisible;
}
2 changes: 2 additions & 0 deletions src/state/initState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function initOilState(): OilState {
identifierCounter: 1,
visitedPaths: new Map(),
editedPaths: new Map(),
metadataCache: new Map(),
};

return newState;
Expand All @@ -39,6 +40,7 @@ export function initOilStateWithPath(path: string): OilState {
identifierCounter: 1,
visitedPaths: new Map(),
editedPaths: new Map(),
metadataCache: new Map(),
};

return newState;
Expand Down
4 changes: 4 additions & 0 deletions src/state/oilState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export function setOilState(state: OilState | undefined) {
oilState = state;
}

export function peekOilState(): OilState | undefined {
return oilState;
}

export function getCurrentPath(): string | undefined {
const activeEditor = vscode.window.activeTextEditor;
if (activeEditor && oilState) {
Expand Down
76 changes: 76 additions & 0 deletions src/test/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -879,4 +879,80 @@ suite("oil.code", () => {
"Content of moved file does not match expected content"
);
});

test("toggleDetails command executes without error", async () => {
// Run the command twice (toggle on, toggle off) to verify it
// completes without throwing in both directions.
// Direct state inspection is not possible here because the extension
// is bundled by esbuild, giving it a separate module instance from
// the test's compiled output.
await vscode.commands.executeCommand("oil-code.toggleDetails");
await vscode.commands.executeCommand("oil-code.toggleDetails");
});

test("Rename with metadata columns enabled does not corrupt file operations", async () => {
// Open oil in icon+size+mtime mode
await vscode.workspace
.getConfiguration("oil-code")
.update("columns", ["icon", "size", "mtime"], vscode.ConfigurationTarget.Global);

await vscode.commands.executeCommand("oil-code.open");
await waitForDocumentText("/000 ../");

const editor = vscode.window.activeTextEditor;
assert.ok(editor, "No active editor");

// Create a file
await editor.edit((editBuilder) => {
editBuilder.insert(new vscode.Position(1, 0), `${newline}metadata-rename-test.md`);
});
await saveFile();
await waitForDocumentText(["/000 ../", "/001 metadata-rename-test.md"]);

// Rename the file by editing only the filename portion (after the identifier)
const line = editor.document.lineAt(1).text; // "/001 metadata-rename-test.md"
const prefixLength = 5; // "/001 "
await editor.edit((editBuilder) => {
editBuilder.replace(
new vscode.Range(
new vscode.Position(1, prefixLength),
new vscode.Position(1, line.length)
),
"metadata-renamed.md"
);
});
await saveFile();

await assertProjectFileStructure(["metadata-renamed.md"]);

// Restore columns setting
await vscode.workspace
.getConfiguration("oil-code")
.update("columns", ["icon"], vscode.ConfigurationTarget.Global);
});

test("When columns is icon-only, buffer text stays plain /NNN filename format", async () => {
// Verifies that metadata is never embedded in buffer text (always decoration-only).
// Cross-bundle internal state (metadataCache) cannot be read from test code because
// the extension is bundled by esbuild into a separate module instance.
await vscode.workspace
.getConfiguration("oil-code")
.update("columns", ["icon"], vscode.ConfigurationTarget.Global);

await vscode.commands.executeCommand("oil-code.open");
await waitForDocumentText("/000 ../");

const editor = vscode.window.activeTextEditor;
assert.ok(editor, "Oil editor should be active");

const text = editor.document.getText();
const lines = text.split(newline).filter((l) => l.trim().length > 0);
for (const line of lines) {
assert.match(
line,
/^\/\d{3} /,
`Each line must start with /NNN prefix only β€” no metadata in buffer text. Got: ${line}`
);
}
});
});
7 changes: 7 additions & 0 deletions src/utils/fileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import * as path from "path";
import { GO_UP_IDENTIFIER, OilState } from "../constants";
import { removeTrailingSlash, normalizePathToUri } from "./pathUtils";
import { newline } from "../newline";
import { populateMetadataCache } from "./metadataUtils";
import { getColumnsSettings } from "./settings";

export async function getDirectoryListing(
folderPath: string,
Expand Down Expand Up @@ -79,6 +81,11 @@ export async function getDirectoryListing(

oilState.visitedPaths.set(folderPathUri, listingsWithIds);

const activeColumns = getColumnsSettings();
if (activeColumns.some((c) => c !== "icon")) {
populateMetadataCache(folderPath, listings, oilState);
}

return listingsWithIds.join(newline);
}

Expand Down
Loading
Loading