Skip to content
Merged
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
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ To open oil.code:
| `ctrl+p` | `alt+p` | `oil-code.preview` | Toggle preview window of entry under the cursor |
| `ctrl+l` | `alt+l` | `oil-code.refresh` | Refresh directory listing from disk |
| <code>\`</code> | <code>alt+\`</code> | `oil-code.cd` | Change Directory to current |
| `gd` | `alt+shift+d` | `oil-code.toggleDetails` | Toggle detail columns visibility |
| -- No Default -- | `alt+shift+h` | `oil-code.help` | Display oil.code default keymaps |

### [vscode-neovim](https://github.com/vscode-neovim/vscode-neovim) Keymaps
Expand Down Expand Up @@ -67,6 +68,7 @@ if vim.g.vscode then
map("n", "<C-t>", function() vscode.action('oil-code.selectTab') end)
map("n", "<C-l>", function() vscode.action('oil-code.refresh') end)
map("n", "`", function() vscode.action('oil-code.cd') end)
map("n", "gd", function() vscode.action('oil-code.toggleDetails') end)
end,
})
end
Expand Down Expand Up @@ -96,6 +98,14 @@ If you're using VSCodeVim and want to customize the keymaps for oil.code:
"command": "oil-code.select"
}
]
},
{
"before": ["g", "d"],
"commands": [
{
"command": "oil-code.toggleDetails"
}
]
}
]
```
Expand Down Expand Up @@ -170,6 +180,23 @@ You can set the VSCode text editor to use an installed Nerd Font by setting `"ed

Once you have a Nerd Font set for your editor font, to use these icons in your oil view, set `"oil-code.hasNerdFont": true`.

## Detail Columns

By default, oil.code shows file icons only. You can add metadata columns to the directory listing with `"oil-code.columns"`:

```json
"oil-code.columns": ["icon", "permissions", "size", "mtime"]
```

Supported columns:

- `icon` — file or directory icon
- `permissions` — Unix-style permissions such as `-rw-r--r--`
- `size` — human-readable file size
- `mtime` — last modified time

The metadata is rendered as non-editable decoration text, so the oil buffer keeps the same `/NNN filename` format for file operations. Use `gd` in Vim mode or `alt+shift+d` to toggle detail column visibility for the current session.

## Confirmation Dialog

By default, oil.code uses a modal confirmation dialog when you save file operations. You can enable an alternate confirmation interface by setting `"oil-code.enableAlternateConfirmation": true`.
Expand Down
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 @@ -175,6 +184,21 @@
"type": "boolean",
"default": false,
"description": "Enable preview automatically when opening oil. 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/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export async function help() {
"alt+`",
"Change VSCode working directory to current oil directory",
],
["toggleDetails", "gd", "alt+shift+d", "Toggle detail columns visibility"],
];

// Display the message in a Markdown preview panel
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
140 changes: 86 additions & 54 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 @@ -108,8 +113,30 @@ function getFileIcon(fileName: string, isDirectory: boolean): string {
}
}

// Decoration types for different file types (created on demand)
const fileIconDecorations = new Map<string, vscode.TextEditorDecorationType>();
export function buildEntryDecorationContent({
icon,
metadataText,
}: {
icon: string;
metadataText?: string;
}): string {
if (metadataText) {
return `${icon.trimEnd()}${metadataText}`;
}

return /\s$/.test(icon) ? icon : `${icon} `;
}

// Single decoration type for all entry before-text. The icon and metadata columns
// are emitted together so VS Code cannot reorder multiple same-position
// decorations while navigating between directories.
let entryDecorationType: vscode.TextEditorDecorationType | null = null;
function getEntryDecorationType(): vscode.TextEditorDecorationType {
if (!entryDecorationType) {
entryDecorationType = vscode.window.createTextEditorDecorationType({});
}
return entryDecorationType;
}

// Apply decorations to hide prefixes
export function updateDecorations(editor: vscode.TextEditor | undefined) {
Expand All @@ -120,15 +147,33 @@ export function updateDecorations(editor: vscode.TextEditor | undefined) {
const document = editor.document;
const hiddenRanges: vscode.Range[] = [];

// Clear previous icon decorations
fileIconDecorations.forEach((decoration) => {
editor.setDecorations(decoration, []);
});
// Clear previous entry decorations
if (entryDecorationType) {
editor.setDecorations(entryDecorationType, []);
}

// 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 entryDecorations: vscode.DecorationOptions[] = [];

// Add icon after the prefix and space
// Add icon and optional metadata after the hidden prefix
// Get appropriate icon based on configuration
const config = vscode.workspace.getConfiguration("oil-code");
const hasNerdFont = config.get("hasNerdFont") === true;
Expand All @@ -150,51 +195,40 @@ export function updateDecorations(editor: vscode.TextEditor | undefined) {
const endPos = new vscode.Position(i, prefixLength);
hiddenRanges.push(new vscode.Range(startPos, endPos));

let icon;
let fontColor = "inherit";
let iconKey = "";
let icon: string;
let fontColor: string | vscode.ThemeColor | undefined;
if (hasNerdFont) {
const {
icon: nerdIcon,
color,
extension,
} = getNerdFontFileIcon(fileName, isDirectory);
const { icon: nerdIcon, color } = getNerdFontFileIcon(fileName, isDirectory);
icon = nerdIcon;
fontColor = color;
iconKey = extension;
} else {
icon = getFileIcon(fileName, isDirectory);
iconKey = isDirectory ? "directory" : path.extname(fileName) || "file";
}

// Create decoration type if it doesn't exist
if (!fileIconDecorations.has(iconKey)) {
fileIconDecorations.set(
iconKey,
vscode.window.createTextEditorDecorationType({
before: {
contentText: icon,
width: "1.5em",
color: fontColor,
},
})
);
}

// Get or create the ranges array for this icon type
if (!iconDecorations.has(iconKey)) {
iconDecorations.set(iconKey, []);
let metadataText = "";
if (showMetadata && metadataMap) {
const meta = metadataMap.get(fileName);
if (meta) {
metadataText = formatMetadataColumns(meta, metaColumns);
if (metadataText) {
// A combined icon+metadata decoration must use one color. Prefer the
// metadata/inlay hint color when details are visible so the data
// columns read as subdued metadata instead of file-type glyphs.
fontColor = new vscode.ThemeColor("editorInlayHint.foreground");
}
}
}

// Add decoration range at the beginning of visible part
iconDecorations
.get(iconKey)!
.push(
new vscode.Range(
new vscode.Position(i, prefixLength),
new vscode.Position(i, prefixLength)
)
);
const filenameStartPos = new vscode.Position(i, prefixLength);
entryDecorations.push({
range: new vscode.Range(filenameStartPos, filenameStartPos),
renderOptions: {
before: {
contentText: buildEntryDecorationContent({ icon, metadataText }),
color: fontColor,
},
},
});

// If cursor is within the prefix area, move it to the first visible character
for (let selection of editor.selections) {
Expand All @@ -219,13 +253,9 @@ export function updateDecorations(editor: vscode.TextEditor | undefined) {
// Apply the hidden prefix decoration
editor.setDecorations(hiddenPrefixDecoration, hiddenRanges);

// Apply file type icon decorations
for (const [iconKey, ranges] of iconDecorations.entries()) {
const decoration = fileIconDecorations.get(iconKey);
if (decoration) {
editor.setDecorations(decoration, ranges);
}
}
// Apply entry before-text decorations after hiding prefixes. Icon and metadata
// share one per-line decoration to keep VS Code from reordering columns.
editor.setDecorations(getEntryDecorationType(), entryDecorations);
}

// Disposable for cleanup
Expand Down Expand Up @@ -343,8 +373,10 @@ export function activateDecorations(context: vscode.ExtensionContext) {
context.subscriptions.push(decorationUpdateListener, {
dispose: () => {
hiddenPrefixDecoration.dispose();
fileIconDecorations.forEach((decoration) => decoration.dispose());
fileIconDecorations.clear();
if (entryDecorationType) {
entryDecorationType.dispose();
entryDecorationType = null;
}

if (decorationUpdateListener) {
decorationUpdateListener.dispose();
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
Loading
Loading