From e3e233c97bde2aaa0f3f26997ed89527e9b09350 Mon Sep 17 00:00:00 2001 From: Goutham-AR Date: Sat, 21 Mar 2026 19:55:56 +0530 Subject: [PATCH 1/9] feat: add data model for configurable metadata columns - Add MetadataColumn type and FileMetadata interface to constants.ts. - Extend OilState with a metadataCache field. Add metadataUtils.ts with fixed-width formatting helpers (formatSize, formatMtime, formatPermissions, formatMetadataColumns) and populateMetadataCache. - Add getColumnsSettings() to settings.ts. - Add peekOilState() to oilState.ts for use in the decoration layer. - Initialize metadataCache in both OilState init functions. --- src/constants.ts | 9 +++ src/state/initState.ts | 2 + src/state/oilState.ts | 4 ++ src/utils/metadataUtils.ts | 113 +++++++++++++++++++++++++++++++++++++ src/utils/settings.ts | 6 ++ 5 files changed, 134 insertions(+) create mode 100644 src/utils/metadataUtils.ts diff --git a/src/constants.ts b/src/constants.ts index 3001984..dc2f7fb 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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; @@ -13,6 +21,7 @@ export interface OilState { identifierCounter: number; visitedPaths: Map; editedPaths: Map; + metadataCache: Map>; openAfterSave?: string; } diff --git a/src/state/initState.ts b/src/state/initState.ts index 5fdc8bf..2411dd1 100644 --- a/src/state/initState.ts +++ b/src/state/initState.ts @@ -24,6 +24,7 @@ export function initOilState(): OilState { identifierCounter: 1, visitedPaths: new Map(), editedPaths: new Map(), + metadataCache: new Map(), }; return newState; @@ -39,6 +40,7 @@ export function initOilStateWithPath(path: string): OilState { identifierCounter: 1, visitedPaths: new Map(), editedPaths: new Map(), + metadataCache: new Map(), }; return newState; diff --git a/src/state/oilState.ts b/src/state/oilState.ts index e9c4ac7..1fe0efc 100644 --- a/src/state/oilState.ts +++ b/src/state/oilState.ts @@ -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) { diff --git a/src/utils/metadataUtils.ts b/src/utils/metadataUtils.ts new file mode 100644 index 0000000..1b52e92 --- /dev/null +++ b/src/utils/metadataUtils.ts @@ -0,0 +1,113 @@ +import * as fs from "fs"; +import * as path from "path"; +import { FileMetadata, MetadataColumn, OilState } from "../constants"; +import { normalizePathToUri, removeTrailingSlash } from "./pathUtils"; + +const MONTHS = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", +]; + +// Returns at most 5 chars (e.g. "999B", " 1.0K", "99.9M") so padStart(5) always gives +// a fixed-width right-aligned string. +export function formatSize(bytes: number): string { + if (bytes < 1000) { + return `${bytes}B`; // max "999B" = 4 chars + } + if (bytes < 1000 * 1024) { + const k = bytes / 1024; + return k >= 10 ? `${Math.round(k)}K` : `${k.toFixed(1)}K`; // max "999K" = 4 chars + } + if (bytes < 1000 * 1024 * 1024) { + const m = bytes / (1024 * 1024); + return m >= 10 ? `${Math.round(m)}M` : `${m.toFixed(1)}M`; // max "999M" = 4 chars + } + const g = bytes / (1024 * 1024 * 1024); + return g >= 10 ? `${Math.round(g)}G` : `${g.toFixed(1)}G`; // max "999G" = 4 chars +} + +export function formatMtime(mtime: Date): string { + const month = MONTHS[mtime.getMonth()]; + const day = String(mtime.getDate()).padStart(2, " "); + const now = new Date(); + if (mtime.getFullYear() === now.getFullYear()) { + const hours = String(mtime.getHours()).padStart(2, "0"); + const minutes = String(mtime.getMinutes()).padStart(2, "0"); + return `${month} ${day} ${hours}:${minutes}`; + } + return `${month} ${day} ${mtime.getFullYear()}`; +} + +export function formatPermissions(stat: fs.Stats): string { + const typeChar = stat.isDirectory() ? "d" : stat.isSymbolicLink() ? "l" : "-"; + + if (process.platform === "win32") { + const readonly = !(stat.mode & 0o200); + return readonly ? `${typeChar}r--r--r--` : `${typeChar}rw-rw-rw-`; + } + + const mode = stat.mode & 0o777; + const rwx = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"]; + return `${typeChar}${rwx[(mode >> 6) & 7]}${rwx[(mode >> 3) & 7]}${rwx[mode & 7]}`; +} + +export function getFileMetadata(filePath: string, stat: fs.Stats): FileMetadata { + return { + permissions: formatPermissions(stat), + size: formatSize(stat.size), + mtime: formatMtime(stat.mtime), + }; +} + +// Column widths (monospace chars): +// permissions : exactly 10 (e.g. "-rw-r--r--") +// size : right-aligned in 5 (e.g. " 12K", "1023B") +// mtime : exactly 12 (e.g. "Mar 14 14:23", "Mar 14 2024") +// Separator between columns: 2 spaces. Trailing 2 spaces before filename. +// All rows in the same directory with the same column set are identical width. +export function formatMetadataColumns( + meta: FileMetadata, + columns: MetadataColumn[] +): string { + const parts: string[] = []; + for (const col of columns) { + switch (col) { + case "permissions": + parts.push(meta.permissions); // always 10 chars + break; + case "size": + parts.push(meta.size.padStart(5)); // right-aligned, always 5 chars + break; + case "mtime": + parts.push(meta.mtime); // always 12 chars + break; + // "icon" is not a metadata column — silently ignored + } + } + // Leading 2 spaces (gap from icon), 2 spaces between columns, 2 spaces before filename + return parts.length > 0 ? " " + parts.join(" ") + " " : ""; +} + +export function populateMetadataCache( + folderPath: string, + listings: string[], + oilState: OilState +): void { + const folderPathUri = removeTrailingSlash(normalizePathToUri(folderPath)); + const fileMap = new Map(); + + for (const name of listings) { + if (name === "../") { + continue; + } + try { + const fullPath = path.join(folderPath, name.replace(/\/$/, "")); + const stat = fs.statSync(fullPath); + fileMap.set(name, getFileMetadata(fullPath, stat)); + } catch { + // Skip entries we can't stat (broken symlinks, permission errors, etc.) + } + } + + oilState.metadataCache.set(folderPathUri, fileMap); +} diff --git a/src/utils/settings.ts b/src/utils/settings.ts index 17edb07..46709cc 100644 --- a/src/utils/settings.ts +++ b/src/utils/settings.ts @@ -1,4 +1,5 @@ import * as vscode from "vscode"; +import { MetadataColumn } from "../constants"; export function getDisableVimKeymapsSetting(): boolean { const config = vscode.workspace.getConfiguration("oil-code"); @@ -25,6 +26,11 @@ export function getEnableAlternateConfirmationSetting(): boolean { return config.get("enableAlternateConfirmation") || false; } +export function getColumnsSettings(): MetadataColumn[] { + const config = vscode.workspace.getConfiguration("oil-code"); + return config.get("columns") ?? ["icon"]; +} + let restoreAutoSave = false; export async function checkAndDisableAutoSave() { From 4c685a87704ce7aca869e280870ad5f504404c86 Mon Sep 17 00:00:00 2001 From: Goutham-AR Date: Sat, 21 Mar 2026 19:56:35 +0530 Subject: [PATCH 2/9] feat: populate metadata cache and add oil-code.columns setting - Call populateMetadataCache at the end of getDirectoryListing when at least one non-icon column is configured. Clear the cache entry for the current directory on refresh, alongside visitedPaths. - Add oil-code.columns setting (array, default ["icon"]) with enum values icon/permissions/size/mtime. Register oil-code.toggleDetails command and alt+shift+d keybinding. --- package.json | 24 ++++++++++++++++++++++++ src/commands/refresh.ts | 1 + src/utils/fileUtils.ts | 7 +++++++ 3 files changed, 32 insertions(+) diff --git a/package.json b/package.json index 2e60d74..50e9b4b 100644 --- a/package.json +++ b/package.json @@ -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": [ @@ -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": [ @@ -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." } } } diff --git a/src/commands/refresh.ts b/src/commands/refresh.ts index b66ebd4..bf4efa7 100644 --- a/src/commands/refresh.ts +++ b/src/commands/refresh.ts @@ -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( diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts index 59a3576..3f8cbe5 100644 --- a/src/utils/fileUtils.ts +++ b/src/utils/fileUtils.ts @@ -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, @@ -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); } From 7af0d890089dbe85e70c7f11244e793d8f0ad206 Mon Sep 17 00:00:00 2001 From: Goutham-AR Date: Sat, 21 Mar 2026 19:57:19 +0530 Subject: [PATCH 3/9] feat: render metadata as before-decorations and add toggleDetails command Add columnState.ts with session-level getDetailsVisible/toggleDetailsVisible. - Add toggleDetails command that flips the flag and redraws all visible oil editors. - Update updateDecorations to inject metadata as before: virtual text at the filename start position when non-icon columns are configured and details are visible. Metadata is never written to buffer text, so the /NNN filename format and the save/diff pipeline are entirely unaffected. - Register oil-code.toggleDetails in extension.ts. --- src/commands/toggleDetails.ts | 14 ++++++++ src/decorations.ts | 64 +++++++++++++++++++++++++++++++++++ src/extension.ts | 4 ++- src/state/columnState.ts | 13 +++++++ 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/commands/toggleDetails.ts create mode 100644 src/state/columnState.ts diff --git a/src/commands/toggleDetails.ts b/src/commands/toggleDetails.ts new file mode 100644 index 0000000..c8cfb91 --- /dev/null +++ b/src/commands/toggleDetails.ts @@ -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); + } + } +} diff --git a/src/decorations.ts b/src/decorations.ts index d2b229e..7c8cea9 100644 --- a/src/decorations.ts +++ b/src/decorations.ts @@ -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({ @@ -111,6 +116,21 @@ function getFileIcon(fileName: string, isDirectory: boolean): string { // Decoration types for different file types (created on demand) const fileIconDecorations = new Map(); +// 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"), + fontStyle: "italic", + }, + }); + } + return metadataDecorationType; +} + // Apply decorations to hide prefixes export function updateDecorations(editor: vscode.TextEditor | undefined) { if (!editor || editor.document.languageId !== "oil") { @@ -125,9 +145,33 @@ export function updateDecorations(editor: vscode.TextEditor | undefined) { editor.setDecorations(decoration, []); }); + // Clear previous metadata decorations + editor.setDecorations(getMetadataDecorationType(), []); + // Track icon decorations for this update const iconDecorations = new Map(); + // 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 | 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"); @@ -196,6 +240,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 ( @@ -226,6 +287,9 @@ export function updateDecorations(editor: vscode.TextEditor | undefined) { editor.setDecorations(decoration, ranges); } } + + // Apply metadata after-decorations + editor.setDecorations(getMetadataDecorationType(), metaDecorations); } // Disposable for cleanup diff --git a/src/extension.ts b/src/extension.ts index 8869552..5d6a8dd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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 @@ -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 diff --git a/src/state/columnState.ts b/src/state/columnState.ts new file mode 100644 index 0000000..53a7f3a --- /dev/null +++ b/src/state/columnState.ts @@ -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; +} From a3ec4e6056643099313022cddb5a5989832e8323 Mon Sep 17 00:00:00 2001 From: Goutham-AR Date: Sat, 21 Mar 2026 19:57:37 +0530 Subject: [PATCH 4/9] feat: add toggleDetails vim keymaps and tests - Map to oil-code.toggleDetails in both the neovim Lua autocmd block and the VSCodeVim normal-mode keymap registration. - Add three tests: toggleDetails executes without error, rename with metadata columns enabled does not corrupt file operations, and buffer text always stays in /NNN filename format regardless of columns setting. --- src/test/extension.test.ts | 76 ++++++++++++++++++++++++++++++++++++++ src/vim/oil.code.lua | 1 + src/vim/vscodeVim.ts | 14 +++++++ 3 files changed, 91 insertions(+) diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 0894f58..694938e 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -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}` + ); + } + }); }); diff --git a/src/vim/oil.code.lua b/src/vim/oil.code.lua index 18b2ea8..04f6aca 100644 --- a/src/vim/oil.code.lua +++ b/src/vim/oil.code.lua @@ -22,5 +22,6 @@ vim.api.nvim_create_autocmd({'FileType'}, { map("n", "", function() vscode.action('oil-code.selectTab') end) map("n", "", function() vscode.action('oil-code.refresh') end) map("n", "`", function() vscode.action('oil-code.cd') end) + map("n", "", function() vscode.action('oil-code.toggleDetails') end) end, }) diff --git a/src/vim/vscodeVim.ts b/src/vim/vscodeVim.ts index 67406b9..6ad817d 100644 --- a/src/vim/vscodeVim.ts +++ b/src/vim/vscodeVim.ts @@ -104,6 +104,20 @@ export async function registerVSCodeVimKeymap(): Promise { keymapChanged = true; } + // Check for and add the Oil toggleDetails binding if not present + const hasOilToggleDetailsBinding = normalModeKeymap.some((binding) => + binding.commands?.some( + (cmd: { command: string }) => cmd.command === "oil-code.toggleDetails" + ) + ); + if (!hasOilToggleDetailsBinding) { + updatedKeymap.push({ + before: [""], + commands: [{ command: "oil-code.toggleDetails" }], + }); + keymapChanged = true; + } + // Update the configuration if changes were made if (keymapChanged) { await vimConfig.update( From 9409ba6e2acabb7f0af43e53813d7432cd8f2821 Mon Sep 17 00:00:00 2001 From: Goutham-AR Date: Sat, 21 Mar 2026 20:03:20 +0530 Subject: [PATCH 5/9] fix: correct size column padding and update documentation --- src/utils/metadataUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/metadataUtils.ts b/src/utils/metadataUtils.ts index 1b52e92..98fd562 100644 --- a/src/utils/metadataUtils.ts +++ b/src/utils/metadataUtils.ts @@ -61,7 +61,7 @@ export function getFileMetadata(filePath: string, stat: fs.Stats): FileMetadata // Column widths (monospace chars): // permissions : exactly 10 (e.g. "-rw-r--r--") -// size : right-aligned in 5 (e.g. " 12K", "1023B") +// size : right-aligned in 4, padded left with non-breaking spaces (e.g. "\u00A012K", "999B") // mtime : exactly 12 (e.g. "Mar 14 14:23", "Mar 14 2024") // Separator between columns: 2 spaces. Trailing 2 spaces before filename. // All rows in the same directory with the same column set are identical width. @@ -76,7 +76,7 @@ export function formatMetadataColumns( parts.push(meta.permissions); // always 10 chars break; case "size": - parts.push(meta.size.padStart(5)); // right-aligned, always 5 chars + parts.push(meta.size.padStart(4, "\u00A0")); // right-aligned, always 4 chars, NBSP padding break; case "mtime": parts.push(meta.mtime); // always 12 chars From 0a4c57bf7fc78032f6acbbb791f924e1395d7be2 Mon Sep 17 00:00:00 2001 From: Goutham-AR Date: Sat, 21 Mar 2026 23:58:24 +0530 Subject: [PATCH 6/9] feat: update toggleDetails key binding to use 'gd' instead of '' --- src/vim/oil.code.lua | 2 +- src/vim/vscodeVim.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vim/oil.code.lua b/src/vim/oil.code.lua index 04f6aca..e6728a6 100644 --- a/src/vim/oil.code.lua +++ b/src/vim/oil.code.lua @@ -22,6 +22,6 @@ vim.api.nvim_create_autocmd({'FileType'}, { map("n", "", function() vscode.action('oil-code.selectTab') end) map("n", "", function() vscode.action('oil-code.refresh') end) map("n", "`", function() vscode.action('oil-code.cd') end) - map("n", "", function() vscode.action('oil-code.toggleDetails') end) + map("n", "gd", function() vscode.action('oil-code.toggleDetails') end) end, }) diff --git a/src/vim/vscodeVim.ts b/src/vim/vscodeVim.ts index 6ad817d..4851dec 100644 --- a/src/vim/vscodeVim.ts +++ b/src/vim/vscodeVim.ts @@ -112,7 +112,7 @@ export async function registerVSCodeVimKeymap(): Promise { ); if (!hasOilToggleDetailsBinding) { updatedKeymap.push({ - before: [""], + before: ["g", "d"], commands: [{ command: "oil-code.toggleDetails" }], }); keymapChanged = true; From 991c34baeb6abf28943edd8a9418c0b8d90a388d Mon Sep 17 00:00:00 2001 From: Goutham-AR Date: Sun, 22 Mar 2026 00:10:49 +0530 Subject: [PATCH 7/9] feat: remove italic font style from metadata decoration type --- src/decorations.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/decorations.ts b/src/decorations.ts index 7c8cea9..e391ac4 100644 --- a/src/decorations.ts +++ b/src/decorations.ts @@ -124,7 +124,6 @@ function getMetadataDecorationType(): vscode.TextEditorDecorationType { metadataDecorationType = vscode.window.createTextEditorDecorationType({ before: { color: new vscode.ThemeColor("editorInlayHint.foreground"), - fontStyle: "italic", }, }); } From 386a0b93743a2571068c9ec4d0fce05e07e776ee Mon Sep 17 00:00:00 2001 From: Goutham-AR Date: Sun, 22 Mar 2026 12:11:12 +0530 Subject: [PATCH 8/9] feat: update mtime formatting to use non-breaking spaces for better alignment --- src/utils/metadataUtils.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/utils/metadataUtils.ts b/src/utils/metadataUtils.ts index 98fd562..0cce26a 100644 --- a/src/utils/metadataUtils.ts +++ b/src/utils/metadataUtils.ts @@ -28,14 +28,14 @@ export function formatSize(bytes: number): string { export function formatMtime(mtime: Date): string { const month = MONTHS[mtime.getMonth()]; - const day = String(mtime.getDate()).padStart(2, " "); + const day = String(mtime.getDate()).padStart(2, "0"); const now = new Date(); if (mtime.getFullYear() === now.getFullYear()) { const hours = String(mtime.getHours()).padStart(2, "0"); const minutes = String(mtime.getMinutes()).padStart(2, "0"); - return `${month} ${day} ${hours}:${minutes}`; + return `${month}\u00A0${day}\u00A0${hours}:${minutes}`; } - return `${month} ${day} ${mtime.getFullYear()}`; + return `${month}\u00A0${day}\u00A0\u00A0${mtime.getFullYear()}`; } export function formatPermissions(stat: fs.Stats): string { @@ -59,11 +59,11 @@ export function getFileMetadata(filePath: string, stat: fs.Stats): FileMetadata }; } -// Column widths (monospace chars): +// Column widths (monospace chars), all padding uses non-breaking spaces (\u00A0): // permissions : exactly 10 (e.g. "-rw-r--r--") -// size : right-aligned in 4, padded left with non-breaking spaces (e.g. "\u00A012K", "999B") -// mtime : exactly 12 (e.g. "Mar 14 14:23", "Mar 14 2024") -// Separator between columns: 2 spaces. Trailing 2 spaces before filename. +// size : right-aligned in 4, padded left with NBSP (e.g. "\u00A012K", "999B") +// mtime : exactly 12 (e.g. "Mar\u00A014\u00A014:23", "Mar\u00A014\u00A0\u00A02024") +// Separator between columns: 2 NBSP. Leading 2 NBSP (gap from icon), trailing 2 NBSP before filename. // All rows in the same directory with the same column set are identical width. export function formatMetadataColumns( meta: FileMetadata, @@ -84,8 +84,8 @@ export function formatMetadataColumns( // "icon" is not a metadata column — silently ignored } } - // Leading 2 spaces (gap from icon), 2 spaces between columns, 2 spaces before filename - return parts.length > 0 ? " " + parts.join(" ") + " " : ""; + // Leading 2 NBSP (gap from icon), 2 NBSP between columns, 2 NBSP before filename + return parts.length > 0 ? "\u00A0\u00A0" + parts.join("\u00A0\u00A0") + "\u00A0\u00A0" : ""; } export function populateMetadataCache( @@ -98,6 +98,11 @@ export function populateMetadataCache( for (const name of listings) { if (name === "../") { + fileMap.set(name, { + permissions: "-".padStart(10, "\u00A0"), + size: "-", + mtime: "-".padStart(12, "\u00A0"), + }); continue; } try { From 1cdc5e662fa3f2478b509fb1b998803b6f7695c6 Mon Sep 17 00:00:00 2001 From: Goutham-AR Date: Sun, 22 Mar 2026 12:14:22 +0530 Subject: [PATCH 9/9] feat: ensure metadata decorations are cleared only if the decoration type exists --- src/decorations.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/decorations.ts b/src/decorations.ts index e391ac4..6a6b13a 100644 --- a/src/decorations.ts +++ b/src/decorations.ts @@ -144,8 +144,10 @@ export function updateDecorations(editor: vscode.TextEditor | undefined) { editor.setDecorations(decoration, []); }); - // Clear previous metadata decorations - editor.setDecorations(getMetadataDecorationType(), []); + // 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();