mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
Opentui textarea (#3310)
Co-authored-by: Sebastian Herrlinger <hasta84@gmail.com>
This commit is contained in:
parent
b3a4cbd7ce
commit
4c2d0cf181
2 changed files with 53 additions and 31 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import type { ParsedKey, BoxRenderable, InputRenderable } from "@opentui/core"
|
||||
import type { BoxRenderable, TextareaRenderable, KeyEvent } from "@opentui/core"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { firstBy } from "remeda"
|
||||
import { createMemo, createResource, createEffect, onMount, For, Show } from "solid-js"
|
||||
|
|
@ -12,7 +12,7 @@ import type { PromptInfo } from "./history"
|
|||
|
||||
export type AutocompleteRef = {
|
||||
onInput: (value: string) => void
|
||||
onKeyDown: (e: ParsedKey) => void
|
||||
onKeyDown: (e: KeyEvent) => void
|
||||
visible: false | "@" | "/"
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ export function Autocomplete(props: {
|
|||
sessionID?: string
|
||||
setPrompt: (input: (prompt: PromptInfo) => void) => void
|
||||
anchor: () => BoxRenderable
|
||||
input: () => InputRenderable
|
||||
input: () => TextareaRenderable
|
||||
ref: (ref: AutocompleteRef) => void
|
||||
}) {
|
||||
const sdk = useSDK()
|
||||
|
|
@ -138,8 +138,9 @@ export function Autocomplete(props: {
|
|||
display: "/" + command.name,
|
||||
description: command.description,
|
||||
onSelect: () => {
|
||||
console.log("commands.onSelect", command.name, Bun.stringWidth(props.input().value))
|
||||
props.input().value = "/" + command.name + " "
|
||||
props.input().cursorPosition = props.input().value.length
|
||||
props.input().cursorOffset = Bun.stringWidth(props.input().value)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -238,9 +239,10 @@ export function Autocomplete(props: {
|
|||
}
|
||||
|
||||
function show(mode: "@" | "/") {
|
||||
console.log("show", mode, props.input().visualCursor.offset)
|
||||
setStore({
|
||||
visible: mode,
|
||||
index: props.input().cursorPosition,
|
||||
index: props.input().visualCursor.offset,
|
||||
position: {
|
||||
x: props.anchor().x,
|
||||
y: props.anchor().y,
|
||||
|
|
@ -262,7 +264,7 @@ export function Autocomplete(props: {
|
|||
onInput(value: string) {
|
||||
if (store.visible && value.length <= store.index) hide()
|
||||
},
|
||||
onKeyDown(e: ParsedKey) {
|
||||
onKeyDown(e: KeyEvent) {
|
||||
if (store.visible) {
|
||||
if (e.name === "up") move(-1)
|
||||
if (e.name === "down") move(1)
|
||||
|
|
@ -278,7 +280,7 @@ export function Autocomplete(props: {
|
|||
}
|
||||
|
||||
if (e.name === "/") {
|
||||
if (props.input().cursorPosition === 0) show("/")
|
||||
if (props.input().visualCursor.offset === 0) show("/")
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { InputRenderable, TextAttributes, BoxRenderable } from "@opentui/core"
|
||||
import { TextAttributes, BoxRenderable, TextareaRenderable, MouseEvent, KeyEvent } from "@opentui/core"
|
||||
import { createEffect, createMemo, Match, Switch, type JSX } from "solid-js"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { Theme } from "@tui/context/theme"
|
||||
|
|
@ -34,7 +34,7 @@ export type PromptRef = {
|
|||
}
|
||||
|
||||
export function Prompt(props: PromptProps) {
|
||||
let input: InputRenderable
|
||||
let input: TextareaRenderable
|
||||
let anchor: BoxRenderable
|
||||
let autocomplete: AutocompleteRef
|
||||
|
||||
|
|
@ -69,7 +69,8 @@ export function Prompt(props: PromptProps) {
|
|||
input: content,
|
||||
parts: [],
|
||||
})
|
||||
input.cursorPosition = content.length
|
||||
console.log("editor.open", content, Bun.stringWidth(content))
|
||||
input.cursorOffset = Bun.stringWidth(content)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -141,7 +142,8 @@ export function Prompt(props: PromptProps) {
|
|||
},
|
||||
set(prompt) {
|
||||
setStore("prompt", prompt)
|
||||
input.cursorPosition = prompt.input.length
|
||||
console.log("prompt.set", prompt.input, Bun.stringWidth(prompt.input))
|
||||
input.cursorOffset = Bun.stringWidth(prompt.input)
|
||||
},
|
||||
reset() {
|
||||
setStore("prompt", {
|
||||
|
|
@ -239,7 +241,8 @@ export function Prompt(props: PromptProps) {
|
|||
input={() => input}
|
||||
setPrompt={(cb) => {
|
||||
setStore("prompt", produce(cb))
|
||||
input.cursorPosition = store.prompt.input.length
|
||||
console.log("setPrompt", store.prompt.input, Bun.stringWidth(store.prompt.input))
|
||||
input.cursorOffset = Bun.stringWidth(store.prompt.input)
|
||||
}}
|
||||
value={store.prompt.input}
|
||||
/>
|
||||
|
|
@ -248,18 +251,23 @@ export function Prompt(props: PromptProps) {
|
|||
flexDirection="row"
|
||||
{...SplitBorder}
|
||||
borderColor={keybind.leader ? Theme.accent : store.mode === "shell" ? Theme.secondary : undefined}
|
||||
justifyContent="space-evenly"
|
||||
>
|
||||
<box backgroundColor={Theme.backgroundElement} width={3} justifyContent="center" alignItems="center">
|
||||
<box
|
||||
backgroundColor={Theme.backgroundElement}
|
||||
width={3}
|
||||
height="100%"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<text attributes={TextAttributes.BOLD} fg={Theme.primary}>
|
||||
{store.mode === "normal" ? ">" : "!"}
|
||||
</text>
|
||||
</box>
|
||||
<box paddingTop={1} paddingBottom={2} backgroundColor={Theme.backgroundElement} flexGrow={1}>
|
||||
<input
|
||||
onPaste={async function (text) {
|
||||
this.insertText(text)
|
||||
}}
|
||||
onInput={(value) => {
|
||||
<box paddingTop={1} paddingBottom={1} backgroundColor={Theme.backgroundElement} flexGrow={1}>
|
||||
<textarea
|
||||
onContentChange={() => {
|
||||
const value = input.value
|
||||
let diff = value.length - store.prompt.input.length
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
|
|
@ -268,7 +276,7 @@ export function Prompt(props: PromptProps) {
|
|||
const part = draft.prompt.parts[i]
|
||||
if (!part.source) continue
|
||||
const source = part.type === "agent" ? part.source : part.source.text
|
||||
if (source.start >= input.cursorPosition) {
|
||||
if (source.start >= input.visualCursor.offset) {
|
||||
source.start += diff
|
||||
source.end += diff
|
||||
}
|
||||
|
|
@ -278,7 +286,14 @@ export function Prompt(props: PromptProps) {
|
|||
draft.prompt.input =
|
||||
draft.prompt.input.slice(0, source.start) + draft.prompt.input.slice(source.end)
|
||||
draft.prompt.parts.splice(i, 1)
|
||||
input.cursorPosition = Math.max(0, source.start - 1)
|
||||
console.log(
|
||||
"onContentChange setting cursor offset",
|
||||
value,
|
||||
input.visualCursor.offset,
|
||||
source.start,
|
||||
source.end,
|
||||
)
|
||||
input.cursorOffset = Math.max(0, source.start - 1)
|
||||
i--
|
||||
}
|
||||
}
|
||||
|
|
@ -287,7 +302,7 @@ export function Prompt(props: PromptProps) {
|
|||
autocomplete.onInput(value)
|
||||
}}
|
||||
value={store.prompt.input}
|
||||
onKeyDown={async (e) => {
|
||||
onKeyDown={async (e: KeyEvent) => {
|
||||
if (props.disabled) {
|
||||
e.preventDefault()
|
||||
return
|
||||
|
|
@ -303,29 +318,32 @@ export function Prompt(props: PromptProps) {
|
|||
await exit()
|
||||
return
|
||||
}
|
||||
if (e.name === "!" && input.cursorPosition === 0) {
|
||||
if (e.name === "!" && input.visualCursor.offset === 0) {
|
||||
setStore("mode", "shell")
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
if (store.mode === "shell") {
|
||||
if ((e.name === "backspace" && input.cursorPosition === 0) || e.name === "escape") {
|
||||
if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
|
||||
setStore("mode", "normal")
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
if (store.mode === "normal") autocomplete.onKeyDown(e)
|
||||
if (!autocomplete.visible) {
|
||||
if (!autocomplete.visible && input.visualCursor.offset === 0) {
|
||||
if (e.name === "up" || e.name === "down") {
|
||||
const direction = e.name === "up" ? -1 : 1
|
||||
const item = history.move(direction, input.value)
|
||||
if (item) {
|
||||
setStore("prompt", item)
|
||||
input.cursorPosition = item.input.length
|
||||
e.preventDefault()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!autocomplete.visible) {
|
||||
if (e.name === "escape" && props.sessionID) {
|
||||
sdk.client.session.abort({
|
||||
path: {
|
||||
|
|
@ -335,9 +353,9 @@ export function Prompt(props: PromptProps) {
|
|||
return
|
||||
}
|
||||
}
|
||||
const old = input.cursorPosition
|
||||
const old = input.visualCursor.offset
|
||||
setTimeout(() => {
|
||||
const position = input.cursorPosition
|
||||
const position = input.visualCursor.offset
|
||||
const direction = Math.sign(old - position)
|
||||
for (const part of store.prompt.parts) {
|
||||
const source = iife(() => {
|
||||
|
|
@ -348,10 +366,12 @@ export function Prompt(props: PromptProps) {
|
|||
if (source) {
|
||||
if (position >= source.start && position < source.end) {
|
||||
if (direction === 1) {
|
||||
input.cursorPosition = Math.max(0, source.start - 1)
|
||||
console.log("onKeyDown setting cursor offset", source.start - 1)
|
||||
input.cursorOffset = Math.max(0, source.start - 1)
|
||||
}
|
||||
if (direction === -1) {
|
||||
input.cursorPosition = source.end
|
||||
console.log("onKeyDown setting cursor offset", source.end)
|
||||
input.cursorOffset = source.end
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -359,8 +379,8 @@ export function Prompt(props: PromptProps) {
|
|||
}, 0)
|
||||
}}
|
||||
onSubmit={submit}
|
||||
ref={(r) => (input = r)}
|
||||
onMouseDown={(r) => r.target?.focus()}
|
||||
ref={(r: TextareaRenderable) => (input = r)}
|
||||
onMouseDown={(r: MouseEvent) => r.target?.focus()}
|
||||
focusedBackgroundColor={Theme.backgroundElement}
|
||||
cursorColor={Theme.primary}
|
||||
backgroundColor={Theme.backgroundElement}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue