Files
kit/examples/extensions/custom-editor-demo.go
T
Ed Zynda 864230bd0a feat: add editor interceptor system for extensions
Extensions can now intercept key events and wrap the editor's rendered
output via ctx.SetEditor/ctx.ResetEditor, enabling vim-like modal
editing, custom key bindings, and visual decorators.

Key fixes during development:
- Yaegi requires closure wrappers for struct function fields (bare
  function references return zero values across the interpreter boundary)
- SetEditor/ResetEditor use async NotifyWidgetUpdate to avoid deadlocking
  BubbleTea's event loop when called from HandleKey callbacks
- distributeHeight now uses renderInput() to account for interceptor
  Render wrapper in height calculations
2026-02-28 17:46:41 +03:00

137 lines
4.1 KiB
Go

//go:build ignore
package main
import (
"fmt"
"strings"
"kit/ext"
)
// normalMode tracks whether the vim-like normal mode is active.
// When false, all keys pass through to the default editor (insert mode).
var normalMode bool
// savedCtx holds the extension context for use in the HandleKey callback.
var savedCtx ext.Context
// Init demonstrates the editor interceptor system. Extensions can intercept
// key events before they reach the built-in editor and wrap the editor's
// rendered output. This example implements a simple vim-like modal editor
// with normal/insert mode switching.
//
// Slash commands:
// - /vim — toggle vim mode (normal ↔ insert)
// - /vim-info — show current editor mode
func Init(api ext.API) {
// /vim — toggle vim-like normal/insert mode.
api.RegisterCommand(ext.CommandDef{
Name: "vim",
Description: "Toggle vim-like normal/insert mode",
Execute: func(args string, ctx ext.Context) (string, error) {
savedCtx = ctx
if normalMode {
// Switch to insert mode (remove interceptor).
normalMode = false
ctx.ResetEditor()
return "Switched to INSERT mode (default editor).", nil
}
// Switch to normal mode (install interceptor).
normalMode = true
ctx.SetEditor(ext.EditorConfig{
HandleKey: func(key string, currentText string) ext.EditorKeyAction {
return handleVimKey(key, currentText)
},
Render: func(width int, defaultContent string) string {
return renderVimMode(width, defaultContent)
},
})
return "Switched to NORMAL mode. Press 'i' to insert, 'h/j/k/l' to navigate.", nil
},
})
// /vim-info — show the current editor mode.
api.RegisterCommand(ext.CommandDef{
Name: "vim-info",
Description: "Show current vim mode",
Execute: func(args string, ctx ext.Context) (string, error) {
if normalMode {
return "Current mode: NORMAL (vim interceptor active)", nil
}
return "Current mode: INSERT (default editor)", nil
},
})
}
// handleVimKey processes keys in vim normal mode.
func handleVimKey(key string, currentText string) ext.EditorKeyAction {
switch key {
// Navigation: remap hjkl to arrow keys.
case "h":
return ext.EditorKeyAction{Type: ext.EditorKeyRemap, RemappedKey: "left"}
case "j":
return ext.EditorKeyAction{Type: ext.EditorKeyRemap, RemappedKey: "down"}
case "k":
return ext.EditorKeyAction{Type: ext.EditorKeyRemap, RemappedKey: "up"}
case "l":
return ext.EditorKeyAction{Type: ext.EditorKeyRemap, RemappedKey: "right"}
// Mode switching.
case "i":
// Enter insert mode.
normalMode = false
if savedCtx.ResetEditor != nil {
savedCtx.ResetEditor()
}
return ext.EditorKeyAction{Type: ext.EditorKeyConsumed}
// Editing shortcuts.
case "x":
// Delete character under cursor (remap to delete key).
return ext.EditorKeyAction{Type: ext.EditorKeyRemap, RemappedKey: "delete"}
case "0":
// Jump to beginning of line.
return ext.EditorKeyAction{Type: ext.EditorKeyRemap, RemappedKey: "home"}
case "$":
// Jump to end of line.
return ext.EditorKeyAction{Type: ext.EditorKeyRemap, RemappedKey: "end"}
// Submission.
case "enter":
// In normal mode, Enter submits the current text.
if strings.TrimSpace(currentText) != "" {
return ext.EditorKeyAction{Type: ext.EditorKeySubmit}
}
return ext.EditorKeyAction{Type: ext.EditorKeyConsumed}
// Block most printable keys in normal mode.
default:
// Let control sequences and special keys through (e.g., ctrl+c, esc).
if len(key) > 1 && key != "space" {
return ext.EditorKeyAction{Type: ext.EditorKeyPassthrough}
}
// Consume single printable characters — don't insert in normal mode.
return ext.EditorKeyAction{Type: ext.EditorKeyConsumed}
}
}
// renderVimMode wraps the default editor rendering with a mode indicator.
func renderVimMode(width int, defaultContent string) string {
mode := "-- NORMAL --"
if !normalMode {
mode = "-- INSERT --"
}
// Build a mode indicator line.
indicator := fmt.Sprintf(" %s", mode)
// Pad to fill width.
padding := width - len(indicator)
if padding > 0 {
indicator += strings.Repeat(" ", padding)
}
return indicator + "\n" + defaultContent
}