Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3e54418
fix(tui): route tests through editor actions
fcoury May 8, 2026
39c6523
refactor(tui): centralize active window sync
fcoury May 8, 2026
d459f89
fix(tui): use window buffer line for cursor render
fcoury May 8, 2026
d968bfe
fix(tui): align overlays with rendered cursor
fcoury May 8, 2026
7792f06
fix(tui): make render buffer display-width aware
fcoury May 8, 2026
c6d4026
fix(tui): size overlays by display width
fcoury May 8, 2026
298a796
fix(tui): make commandline unicode safe
fcoury May 8, 2026
8e70e79
fix(tui): make statusline width calculations safe
fcoury May 8, 2026
651dea1
fix(tui): render completion rows by display width
fcoury May 8, 2026
4c345ab
fix(tui): harden picker list navigation
fcoury May 8, 2026
76b37d0
fix(tui): align info tooltip content rendering
fcoury May 8, 2026
dc29e63
fix(tui): center dialog titles by display width
fcoury May 8, 2026
64b775f
fix(tui): place commandline cursor by display width
fcoury May 8, 2026
ab7d3d7
fix(tui): make picker sizing safe on tiny terminals
fcoury May 8, 2026
42507ca
fix(tui): make info tooltip geometry saturating
fcoury May 8, 2026
79335ca
fix(tui): guard chrome rendering on tiny terminals
fcoury May 8, 2026
aeff423
fix(tui): resize windows inside nested splits
fcoury May 9, 2026
fc22ddf
fix(tui): keep sibling when closing first split window
fcoury May 9, 2026
c8bdbae
fix(tui): fit diagnostics to window width
fcoury May 9, 2026
5bb8219
fix(tui): bound completion popup layout
fcoury May 9, 2026
acc5716
refactor(lsp): modularize language server backend
fcoury May 9, 2026
525ea17
feat(tui): add plugin file tree panel
fcoury May 9, 2026
8f06ddb
fix(tui): clamp cursor at end of file
fcoury May 9, 2026
9aef6cd
fix(tui): preserve viewport during word motion
fcoury May 9, 2026
0390feb
fix(tui): handle edge movement and visual selection
fcoury May 9, 2026
19a011f
fix(tui): make line end motion visual-aware
fcoury May 9, 2026
7c62796
fix(tui): clamp insert escape at line end
fcoury May 9, 2026
74a33da
feat(tui): replace undo history with transactions
fcoury May 9, 2026
690f411
chore(tui): merge master keeping branch changes
fcoury May 9, 2026
43b212d
fix(tui): satisfy ci lint and audit checks
fcoury May 9, 2026
6caf312
fix(tui): keep inactive window gutters stable
fcoury May 28, 2026
ce0824a
feat(tui): support ctrl navigation in pickers
fcoury May 28, 2026
a46f016
fix(tui): clamp cursor to rendered buffer lines
fcoury May 28, 2026
e5fd921
fix(tui): preserve insert and visual unicode edits
fcoury May 28, 2026
a7b0707
fix(tui): restore backspace and tab unicode edits
fcoury May 28, 2026
c79db8f
fix(tui): align word and ui grapheme handling
fcoury May 28, 2026
68e0ec3
fix(tui): keep panel and line-open state consistent
fcoury May 28, 2026
2db0adc
fix(tui): reserve right panels and save active edits
fcoury May 28, 2026
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ cursor_line = true
[lsp]
enabled = true

[lsp.servers.typescript]
command = "typescript-language-server"
args = ["--stdio"]
language_id = "typescript"
file_extensions = ["ts", "tsx"]
root_markers = ["package.json", ".git"]

# Plugin settings
[plugins]
enabled = true
Expand Down
8 changes: 7 additions & 1 deletion default_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Esc = { EnterMode = "Normal" }
"n" = [ "FindNext" ]
"N" = [ "FindPrevious" ]
"a" = [ { EnterMode = "Insert" }, "MoveRight" ]
"A" = [ { EnterMode = "Insert" }, "MoveToLineEnd" ]
"A" = [ "MoveToLineEnd", "MoveRight", { EnterMode = "Insert" } ]
"i" = { EnterMode = "Insert" }
"I" = [ { EnterMode = "Insert" }, "MoveToFirstLineChar" ]
";" = { EnterMode = "Command" }
Expand Down Expand Up @@ -94,6 +94,7 @@ Esc = { EnterMode = "Normal" }
# }
"Ctrl-p" = "FilePicker"
"Ctrl-z" = "Suspend"
"Ctrl-e" = { PluginCommand = "NeoTree" }
"K" = "Hover"
# "W" = "ToggleWrap"
# "L" = "DecreaseLeft"
Expand Down Expand Up @@ -138,6 +139,8 @@ Esc = { EnterMode = "Normal" }
"j" = "MoveDown"
"h" = "MoveLeft"
"l" = "MoveRight"
"$" = "MoveToLineEnd"
"End" = "MoveToLineEnd"
"y" = [ "Yank", { EnterMode = "Normal" } ]
"x" = [ "Delete", { EnterMode = "Normal" } ]
"p" = [ "Paste", { EnterMode = "Normal" } ]
Expand All @@ -152,6 +155,8 @@ Esc = { EnterMode = "Normal" }

[keys.visual_block]
Esc = { EnterMode = "Normal" }
"$" = "MoveToLineEnd"
"End" = "MoveToLineEnd"

[keys.visual_line]
Esc = { EnterMode = "Normal" }
Expand All @@ -162,3 +167,4 @@ Esc = { EnterMode = "Normal" }

[plugins]
buffer_picker = "buffer_picker.js"
neotree = "neotree.js"
38 changes: 37 additions & 1 deletion docs/PLUGIN_SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,44 @@ red.openBuffer(name: string)

// Draw text at specific coordinates
red.drawText(x: number, y: number, text: string, style?: object)

// Create and update a persistent side panel
red.createPanel("tree", { side: "left", width: 32, title: "Files" })
red.updatePanel("tree", [{
id: "/repo/src",
label: "src",
path: "/repo/src",
depth: 0,
expanded: true,
kind: "directory"
}])
red.focusPanel("tree")
red.focusEditor()
red.closePanel("tree")
red.onPanelEvent("tree", (event) => {
// event.action is "up", "down", "expand", "collapse", or "activate"
})
```

Panel rows are rendered by the editor and receive focused keyboard input
while the panel is active. Plugins can call `focusEditor()` to return input
to the editor after handling a panel action. Pressing `Esc` also returns
focus to the editor.

#### Filesystem
```javascript
const { entries, error } = await red.listDirectory(".")
const watchId = red.watchDirectory(".", async (snapshot) => {
// snapshot has the same shape as listDirectory()
})
red.unwatchDirectory(watchId)
red.openFile("src/main.rs")
```

`listDirectory` returns entries sorted with directories before files. Plugins
do not receive arbitrary filesystem access; they request directory listings
and directory watches through editor-owned APIs.

#### Buffer Manipulation
```javascript
// Insert text at position
Expand Down Expand Up @@ -489,4 +525,4 @@ Areas identified for potential improvement:

The Red editor's plugin system provides a robust foundation for extending editor functionality while maintaining security and performance. By leveraging Deno's runtime and a well-designed API, developers can create powerful plugins that integrate seamlessly with the editor's core functionality.

For questions or contributions to the plugin system, please refer to the main Red editor repository and its contribution guidelines.
For questions or contributions to the plugin system, please refer to the main Red editor repository and its contribution guidelines.
149 changes: 149 additions & 0 deletions plugins/neotree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
const PANEL_ID = "neotree";
const ROOT = ".";

let redApi = null;
const watches = new Map();

export async function activate(red) {
redApi = red;
const expanded = new Set([ROOT]);
const children = new Map();
let created = false;

async function loadDirectory(path) {
const result = await red.listDirectory(path);
if (result.error) {
red.logWarn("NeoTree failed to list directory", path, result.error);
children.set(path, []);
return [];
}
children.set(path, result.entries);
return result.entries;
}

function watchDirectory(path) {
if (watches.has(path)) return;
const watchId = red.watchDirectory(path, async () => {
await loadDirectory(path);
await refresh();
});
watches.set(path, watchId);
}

async function ensureLoaded(path) {
if (!children.has(path)) {
await loadDirectory(path);
}
watchDirectory(path);
return children.get(path) || [];
}

async function buildRows(path, depth = 0, rows = []) {
const entries = await ensureLoaded(path);
for (const entry of entries) {
if (entry.kind !== "directory" && entry.kind !== "file") {
continue;
}

const isDirectory = entry.kind === "directory";
rows.push({
id: entry.path,
label: entry.name,
path: entry.path,
depth,
expanded: isDirectory ? expanded.has(entry.path) : false,
kind: isDirectory ? "directory" : "file",
});

if (isDirectory && expanded.has(entry.path)) {
await buildRows(entry.path, depth + 1, rows);
}
}
return rows;
}

async function refresh() {
const rows = await buildRows(ROOT);
red.updatePanel(PANEL_ID, rows);
}

function stopWatchingDirectories() {
for (const watchId of watches.values()) {
red.unwatchDirectory(watchId);
}
watches.clear();
}

function close() {
if (!created) return;
stopWatchingDirectories();
red.closePanel(PANEL_ID);
red.focusEditor();
created = false;
}

async function show() {
if (!created) {
red.createPanel(PANEL_ID, {
side: "left",
width: 32,
title: "Files",
});
created = true;
}

await ensureLoaded(ROOT);
await refresh();
red.focusPanel(PANEL_ID);
}

async function toggleDirectory(path, forceExpand = null) {
const shouldExpand = forceExpand ?? !expanded.has(path);
if (shouldExpand) {
expanded.add(path);
await ensureLoaded(path);
} else {
expanded.delete(path);
}
await refresh();
}

red.addCommand("NeoTree", async () => {
if (created) {
close();
} else {
await show();
}
});

red.onPanelEvent(PANEL_ID, async (event) => {
const row = event.row;
if (!row) return;

if (event.action === "activate") {
if (row.kind === "directory") {
await toggleDirectory(row.path);
} else if (row.path) {
red.openFile(row.path);
red.focusEditor();
}
return;
}

if (row.kind === "directory" && event.action === "expand") {
await toggleDirectory(row.path, true);
}

if (row.kind === "directory" && event.action === "collapse") {
await toggleDirectory(row.path, false);
}
});
}

export async function deactivate() {
if (!redApi) return;
for (const watchId of watches.values()) redApi.unwatchDirectory(watchId);
watches.clear();
redApi.closePanel(PANEL_ID);
redApi = null;
}
Loading
Loading