Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

opencode-vim

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

opencode-vim - npm Package Compare versions

Comparing version
0.0.8
to
0.0.9
+1
-1
package.json
{
"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"
}

@@ -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 @@