opencode-vim
Advanced tools
+1
-1
| { | ||
| "name": "opencode-vim", | ||
| "version": "0.0.8", | ||
| "version": "0.0.9", | ||
| "exports": { | ||
@@ -5,0 +5,0 @@ "./tui": { |
@@ -0,1 +1,2 @@ | ||
| import type { RGBA } from "@opentui/core" | ||
| import type { TuiPromptInfo, TuiPromptRef } from "@opencode-ai/plugin/tui" | ||
@@ -9,4 +10,6 @@ import type { PromptContext } from "../../prompt/types" | ||
| visualCursor?: VisualCursorLike | ||
| editorView?: { getVisualEOL?: () => VisualCursorLike | undefined } | ||
| editorView?: { getVisualEOL?: () => VisualCursorLike | undefined; setSelection?: (start: number, end: number, bgColor?: RGBA, fgColor?: RGBA) => void; resetSelection?: () => void } | ||
| cursorStyle?: VimCursorStyle | ||
| selectionBg?: RGBA | ||
| selectionFg?: RGBA | ||
| moveCursorLeft?: () => boolean | ||
@@ -16,2 +19,5 @@ moveCursorRight?: () => boolean | ||
| moveCursorDown?: () => boolean | ||
| setSelection?: (start: number, end: number) => void | ||
| setSelectionInclusive?: (start: number, end: number) => void | ||
| clearSelection?: () => void | ||
| gotoVisualLineEnd?: () => boolean | ||
@@ -18,0 +24,0 @@ gotoLineEnd?: () => void |
@@ -34,2 +34,4 @@ import type { VimMode } from "./state" | ||
| normal: { style: "block", blinking: true }, | ||
| visual: { style: "block", blinking: true }, | ||
| "visual-line": { style: "block", blinking: true }, | ||
| } | ||
@@ -46,2 +48,4 @@ | ||
| normal: { ...DEFAULT_CURSOR_STYLES.normal, ...input.cursorStyles?.normal }, | ||
| visual: { ...DEFAULT_CURSOR_STYLES.visual, ...input.cursorStyles?.visual }, | ||
| "visual-line": { ...DEFAULT_CURSOR_STYLES["visual-line"], ...input.cursorStyles?.["visual-line"] }, | ||
| }, | ||
@@ -76,3 +80,3 @@ debug: input.debug ?? process.env.VIM_PROMPT_DEBUG === "1", | ||
| for (const mode of ["insert", "normal"] as const) { | ||
| for (const mode of ["insert", "normal", "visual", "visual-line"] as const) { | ||
| const raw = source[mode] | ||
@@ -95,2 +99,4 @@ if (!raw || typeof raw !== "object") continue | ||
| normal: readCursorStyle(source.normal), | ||
| visual: readCursorStyle(source.visual), | ||
| "visual-line": readCursorStyle(source["visual-line"]), | ||
| } | ||
@@ -114,3 +120,3 @@ } | ||
| function isMode(value: unknown): value is VimMode { | ||
| return value === "insert" || value === "normal" | ||
| return value === "insert" || value === "normal" || value === "visual" || value === "visual-line" | ||
| } | ||
@@ -117,0 +123,0 @@ |
@@ -18,2 +18,3 @@ import type { CursorPosition } from "@vimee/core" | ||
| const suffix = commonSuffix(map.vimText, vimText, prefix) | ||
| const inserted = vimText.slice(prefix, vimText.length - suffix) | ||
| const synthetic = new Set<number>() | ||
@@ -24,3 +25,3 @@ let hostText = "" | ||
| const oldOffset = previousOffset(map, vimOffset, vimText.length, prefix, suffix) | ||
| if (oldOffset !== undefined && map.vimToHost[oldOffset] === undefined) { | ||
| if (oldOffset !== undefined && map.vimToHost[oldOffset] === undefined && preserveSynthetic(vimOffset, prefix, suffix, vimText.length, inserted)) { | ||
| synthetic.add(vimOffset) | ||
@@ -36,2 +37,7 @@ continue | ||
| function preserveSynthetic(vimOffset: number, prefix: number, suffix: number, vimLength: number, inserted: string) { | ||
| if (!inserted.includes("\n")) return true | ||
| return vimOffset !== prefix - 1 && vimOffset !== vimLength - suffix | ||
| } | ||
| export function hostPosition(map: PromptMap, hostOffset: number): CursorPosition { | ||
@@ -38,0 +44,0 @@ return positionFromOffset(map.vimText, map.hostToVim[clamp(hostOffset, 0, map.hostText.length)] ?? 0) |
| import { createSignal } from "solid-js" | ||
| import type { VimLog } from "./log" | ||
| export type VimMode = "normal" | "insert" | ||
| export type VimMode = "normal" | "insert" | "visual" | "visual-line" | ||
@@ -6,0 +6,0 @@ export function createVimState(defaultMode: VimMode, log: VimLog = () => {}) { |
@@ -48,3 +48,3 @@ /** @jsxImportSource @opentui/solid */ | ||
| {pending() ? <text fg={props.theme.info}>{pending()} </text> : undefined} | ||
| <text fg={props.disabled ? props.theme.textMuted : mode() === "normal" ? props.theme.warning : props.theme.success}>{mode() === "normal" ? "NORMAL" : "INSERT"}</text> | ||
| <text fg={props.disabled ? props.theme.textMuted : mode() === "insert" ? props.theme.success : props.theme.warning}>{modeLabel(mode())}</text> | ||
| </box> | ||
@@ -88,1 +88,7 @@ ) | ||
| } | ||
| function modeLabel(mode: VimMode) { | ||
| if (mode === "visual") return "VISUAL" | ||
| if (mode === "visual-line") return "VISUAL LINE" | ||
| return mode === "normal" ? "NORMAL" : "INSERT" | ||
| } |
+196
-6
@@ -13,3 +13,6 @@ import type { KeyEvent } from "@opentui/core" | ||
| type HostAction = VimeeAction | { type: "submit" } | ||
| type HostRange = { start: number; end: number } | ||
| const YANK_FLASH_MS = 250 | ||
| export type VimeeAdapter = ReturnType<typeof createVimeeAdapter> | ||
@@ -24,2 +27,4 @@ | ||
| let timer: ReturnType<typeof setTimeout> | undefined | ||
| let yankTimer: ReturnType<typeof setTimeout> | undefined | ||
| let yankFlashActive = false | ||
| let pendingInsert = "" | ||
@@ -68,6 +73,19 @@ let nativeInsertUndoSaved = false | ||
| const hostEnd = vimeeKey === "e" ? endMotionOffset(map.hostText, offset, vim.count || 1) : undefined | ||
| const shouldFlashYank = shouldFlashYankFor(vimeeKey) | ||
| const visualYankRange = visualYankRangeFor(map) | ||
| const result = processKeystroke(vimeeKey, vim, buffer, event.ctrl, false, keybinds) | ||
| vim = result.newCtx | ||
| if (hostEnd !== undefined && result.actions.every((action) => action.type === "cursor-move") && hostEnd > hostOffset(map, vim.cursor, "previous")) vim = { ...vim, cursor: hostPosition(map, hostEnd) } | ||
| applyActions(result.actions as HostAction[], ctx, map) | ||
| let clampFinalCursor = true | ||
| const content = result.actions.find((action) => action.type === "content-change")?.content | ||
| if (vimeeKey === "x" && content !== undefined) { | ||
| const next = nextMap(map, content) | ||
| const target = clamp(offset, 0, Math.max(0, next.hostText.length - 1)) | ||
| if (offset < map.hostText.length - 1 && map.hostText[offset + 1] !== "\n" && hostOffset(next, vim.cursor, "previous") < target) { | ||
| vim = { ...vim, cursor: hostPosition(next, target) } | ||
| clampFinalCursor = false | ||
| } | ||
| } | ||
| applyActions(result.actions as HostAction[], ctx, map, clampFinalCursor) | ||
| if (shouldFlashYank) flashYank(ctx, activeMap, yankAction(result.actions), visualYankRange) | ||
| syncMode(state, vim.mode) | ||
@@ -84,2 +102,3 @@ const keybindPending = keybinds?.isPending() ?? false | ||
| if (timer) clearTimeout(timer) | ||
| if (yankTimer) clearTimeout(yankTimer) | ||
| }, | ||
@@ -122,3 +141,3 @@ } | ||
| function applyActions(actions: HostAction[], ctx: PromptContext, map: PromptMap) { | ||
| function applyActions(actions: HostAction[], ctx: PromptContext, map: PromptMap, clampFinalCursor = true) { | ||
| const ref = ctx.prompt() | ||
@@ -154,3 +173,4 @@ const input = focusedInput(ctx) | ||
| setCursor(input, currentMap, vim.cursor) | ||
| setCursor(input, currentMap, vim.cursor, clampFinalCursor) | ||
| syncVisualSelection(input, currentMap, ctx) | ||
| } | ||
@@ -264,6 +284,6 @@ | ||
| function setCursor(input: EditBufferLike | undefined, map: PromptMap, position: CursorPosition) { | ||
| function setCursor(input: EditBufferLike | undefined, map: PromptMap, position: CursorPosition, clampCursor = true) { | ||
| if (!input) return | ||
| input.cursorOffset = cursorOffset(map, position) | ||
| if (vim.mode !== "insert") clampNormalCursor(input) | ||
| if (clampCursor && vim.mode !== "insert") clampNormalCursor(input) | ||
| } | ||
@@ -273,2 +293,3 @@ | ||
| if (!input?.gotoVisualLineEnd) return false | ||
| clearVisualSelection(input) | ||
| input.gotoVisualLineEnd() | ||
@@ -280,4 +301,173 @@ vim = { ...vim, cursor: hostPosition(map, input.cursorOffset ?? map.hostText.length), mode: "insert", phase: "idle", count: 0, operator: null, statusMessage: "-- INSERT --" } | ||
| } | ||
| function syncVisualSelection(input: EditBufferLike | undefined, map: PromptMap, ctx: PromptContext) { | ||
| if (!input) return | ||
| if (!isVisualMode(vim.mode) || !vim.visualAnchor) { | ||
| if (!yankFlashActive) clearVisualSelection(input) | ||
| return | ||
| } | ||
| cancelYankFlash() | ||
| const range = vim.mode === "visual-line" ? visualLineRange(map, vim.visualAnchor, vim.cursor) : visualCharRange(map, vim.visualAnchor, vim.cursor) | ||
| if (!range) { | ||
| clearVisualSelection(input) | ||
| return | ||
| } | ||
| setSelection(input, range.start, range.end, ctx) | ||
| } | ||
| function visualYankRangeFor(map: PromptMap) { | ||
| if (!isVisualMode(vim.mode) || !vim.visualAnchor) return undefined | ||
| return vim.mode === "visual-line" ? visualLineRange(map, vim.visualAnchor, vim.cursor) : visualCharRange(map, vim.visualAnchor, vim.cursor) | ||
| } | ||
| function flashYank(ctx: PromptContext, map: PromptMap, action: YankAction | undefined, visualRange: HostRange | undefined) { | ||
| if (!action) return | ||
| const input = focusedInput(ctx) | ||
| if (!input) return | ||
| const range = visualRange ?? yankedTextRange(map, vim.cursor, action.text) | ||
| if (!range) return | ||
| cancelYankFlash() | ||
| yankFlashActive = true | ||
| setYankSelection(input, range.start, range.end, ctx) | ||
| ctx.requestRender() | ||
| yankTimer = setTimeout(() => { | ||
| yankTimer = undefined | ||
| if (!yankFlashActive) return | ||
| yankFlashActive = false | ||
| clearVisualSelection(input) | ||
| ctx.requestRender() | ||
| }, YANK_FLASH_MS) | ||
| } | ||
| function cancelYankFlash() { | ||
| if (yankTimer) clearTimeout(yankTimer) | ||
| yankTimer = undefined | ||
| yankFlashActive = false | ||
| } | ||
| function shouldFlashYankFor(key: string) { | ||
| if (isVisualMode(vim.mode)) return key === "y" | ||
| return vim.operator === "y" | ||
| } | ||
| } | ||
| function visualCharRange(map: PromptMap, anchor: CursorPosition, cursor: CursorPosition): HostRange | undefined { | ||
| const anchorOffset = hostOffset(map, anchor, "next") | ||
| const cursorOffset = hostOffset(map, cursor, "previous") | ||
| return hostRange(map, anchorOffset, cursorOffset) | ||
| } | ||
| function visualLineRange(map: PromptMap, anchor: CursorPosition, cursor: CursorPosition): HostRange | undefined { | ||
| const startLine = Math.min(anchor.line, cursor.line) | ||
| const endLine = Math.max(anchor.line, cursor.line) | ||
| const start = hostOffset(map, { line: startLine, col: 0 }, "next") | ||
| const end = hostOffset(map, { line: endLine, col: vimLineLength(map.vimText, endLine) }, "previous") | ||
| return hostRange(map, start, end) | ||
| } | ||
| function yankedTextRange(map: PromptMap, cursor: CursorPosition, text: string): HostRange | undefined { | ||
| if (!text) return undefined | ||
| if (text.endsWith("\n")) { | ||
| const lineCount = text.split("\n").length - 1 | ||
| return visualLineRange(map, cursor, { line: cursor.line + Math.max(0, lineCount - 1), col: 0 }) | ||
| } | ||
| const start = vimOffsetFromPosition(map.vimText, cursor) | ||
| return vimOffsetRange(map, start, start + text.length - 1) | ||
| } | ||
| function vimOffsetRange(map: PromptMap, left: number, right: number): HostRange | undefined { | ||
| const start = hostFromVimOffset(map, Math.min(left, right), "next") | ||
| const end = hostFromVimOffset(map, Math.max(left, right), "previous") | ||
| return hostRange(map, start, end) | ||
| } | ||
| function hostRange(map: PromptMap, left: number, right: number): HostRange | undefined { | ||
| if (!map.hostText) return undefined | ||
| const start = clamp(Math.min(left, right), 0, Math.max(0, map.hostText.length - 1)) | ||
| const end = clamp(Math.max(left, right), 0, Math.max(0, map.hostText.length - 1)) | ||
| return { start, end } | ||
| } | ||
| type YankAction = Extract<VimeeAction, { type: "yank" }> | ||
| function yankAction(actions: VimeeAction[]): YankAction | undefined { | ||
| return actions.find((action): action is YankAction => action.type === "yank") | ||
| } | ||
| function setSelection(input: EditBufferLike, start: number, end: number, ctx: PromptContext) { | ||
| setSelectionColors(input, start, end, ctx.api.theme.current.warning, ctx.api.theme.current.background) | ||
| } | ||
| function setYankSelection(input: EditBufferLike, start: number, end: number, ctx: PromptContext) { | ||
| setSelectionColors(input, start, end, ctx.api.theme.current.info, ctx.api.theme.current.background) | ||
| } | ||
| function setSelectionColors(input: EditBufferLike, start: number, end: number, background: PromptContext["api"]["theme"]["current"]["warning"], foreground: PromptContext["api"]["theme"]["current"]["background"]) { | ||
| input.selectionBg = background | ||
| input.selectionFg = foreground | ||
| if (input.setSelectionInclusive) { | ||
| input.setSelectionInclusive(start, end) | ||
| return | ||
| } | ||
| const exclusiveEnd = end + 1 | ||
| if (input.setSelection) { | ||
| input.setSelection(start, exclusiveEnd) | ||
| return | ||
| } | ||
| input.editorView?.setSelection?.(start, exclusiveEnd, background, foreground) | ||
| } | ||
| function clearVisualSelection(input: EditBufferLike) { | ||
| if (input.clearSelection) { | ||
| input.clearSelection() | ||
| return | ||
| } | ||
| input.editorView?.resetSelection?.() | ||
| } | ||
| function isVisualMode(mode: VimContext["mode"]): mode is "visual" | "visual-line" { | ||
| return mode === "visual" || mode === "visual-line" | ||
| } | ||
| function vimLineLength(text: string, line: number) { | ||
| return text.split("\n")[line]?.length ?? 0 | ||
| } | ||
| function vimOffsetFromPosition(text: string, position: CursorPosition) { | ||
| const lines = text.split("\n") | ||
| const line = clamp(position.line, 0, Math.max(0, lines.length - 1)) | ||
| let offset = 0 | ||
| for (let index = 0; index < line; index++) offset += lines[index].length + 1 | ||
| return offset + clamp(position.col, 0, lines[line]?.length ?? 0) | ||
| } | ||
| function hostFromVimOffset(map: PromptMap, offset: number, bias: "previous" | "next") { | ||
| const current = clamp(offset, 0, map.vimText.length) | ||
| if (current === map.vimText.length) return map.hostText.length | ||
| const host = map.vimToHost[current] | ||
| if (host !== undefined) return host | ||
| if (bias === "previous") { | ||
| for (let previous = current - 1; previous >= 0; previous--) { | ||
| const previousHost = map.vimToHost[previous] | ||
| if (previousHost !== undefined) return previousHost | ||
| } | ||
| } | ||
| for (let next = current + 1; next < map.vimToHost.length; next++) { | ||
| const nextHost = map.vimToHost[next] | ||
| if (nextHost !== undefined) return nextHost | ||
| } | ||
| return map.hostText.length | ||
| } | ||
| function clampNormalCursor(input: EditBufferLike) { | ||
@@ -373,3 +563,3 @@ const cursor = input.visualCursor | ||
| function syncMode(state: VimState, mode: VimContext["mode"]) { | ||
| state.setMode(mode === "insert" ? "insert" : "normal") | ||
| state.setMode(mode === "insert" || mode === "visual" || mode === "visual-line" ? mode : "normal") | ||
| } | ||
@@ -376,0 +566,0 @@ |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
383011
2.47%2437
7.98%