|
|
@@ -82,6 +82,7 @@ function pastedFilepath(value: string, platform: string) {
|
|
|
|
|
|
export type PromptRef = {
|
|
|
focused: boolean
|
|
|
+ empty: boolean
|
|
|
current: PromptInfo
|
|
|
set(prompt: PromptInfo): void
|
|
|
reset(): void
|
|
|
@@ -91,20 +92,6 @@ export type PromptRef = {
|
|
|
}
|
|
|
|
|
|
const DRAFT_RETENTION_MIN_CHARS = 20
|
|
|
-const PROMPT_SYNC_COMMANDS = [
|
|
|
- "app.exit",
|
|
|
- "prompt.clear",
|
|
|
- "prompt.submit",
|
|
|
- "prompt.editor",
|
|
|
- "prompt.stash",
|
|
|
- "prompt.stash.pop",
|
|
|
- "prompt.stash.list",
|
|
|
- "prompt.autocomplete.prev",
|
|
|
- "prompt.autocomplete.next",
|
|
|
- "prompt.autocomplete.hide",
|
|
|
- "prompt.autocomplete.select",
|
|
|
- "prompt.autocomplete.complete",
|
|
|
-]
|
|
|
|
|
|
function randomIndex(count: number) {
|
|
|
if (count <= 0) return 0
|
|
|
@@ -159,8 +146,6 @@ export function Prompt(props: PromptProps) {
|
|
|
let input: TextareaRenderable
|
|
|
let anchor: BoxRenderable
|
|
|
let promptSyncQueued = false
|
|
|
- let promptContentChanged = false
|
|
|
- let promptTextInputPending = false
|
|
|
const [inputTarget, setInputTarget] = createSignal<TextareaRenderable | undefined>()
|
|
|
|
|
|
const leader = Keymap.useLeaderActive()
|
|
|
@@ -182,53 +167,7 @@ export function Prompt(props: PromptProps) {
|
|
|
const history = usePromptHistory()
|
|
|
const stash = usePromptStash()
|
|
|
const keymap = Keymap.use()
|
|
|
- const activeKeys = Keymap.useActiveKeys()
|
|
|
- const commandKeys = Keymap.useCommandKeys(() => PROMPT_SYNC_COMMANDS)
|
|
|
- // Commands must see earlier burst text, while native textarea edits remain frame-batched.
|
|
|
- const stopPromptSyncInterceptor = keymap.intercept("key", ({ event }) => {
|
|
|
- const code = event.sequence.charCodeAt(0)
|
|
|
- const textInput =
|
|
|
- !event.ctrl &&
|
|
|
- !event.meta &&
|
|
|
- !event.super &&
|
|
|
- !event.hyper &&
|
|
|
- (event.name === "space" || (code >= 32 && code !== 127))
|
|
|
- const pending = promptSyncQueued || promptTextInputPending
|
|
|
- const rawBase = event.baseCode === undefined ? undefined : String.fromCodePoint(event.baseCode)
|
|
|
- const base = rawBase && rawBase >= "A" && rawBase <= "Z" ? rawBase.toLowerCase() : rawBase
|
|
|
- const configured = commandKeys().some(
|
|
|
- (stroke) =>
|
|
|
- (stroke.name === event.name || stroke.name === base) &&
|
|
|
- stroke.ctrl === event.ctrl &&
|
|
|
- stroke.shift === event.shift &&
|
|
|
- stroke.meta === event.meta &&
|
|
|
- stroke.super === !!event.super &&
|
|
|
- (stroke.hyper ?? false) === !!event.hyper,
|
|
|
- )
|
|
|
- const matched = pending
|
|
|
- ? activeKeys().filter(
|
|
|
- (key) =>
|
|
|
- (key.stroke.name === event.name || key.stroke.name === base) &&
|
|
|
- key.stroke.ctrl === event.ctrl &&
|
|
|
- key.stroke.shift === event.shift &&
|
|
|
- key.stroke.meta === event.meta &&
|
|
|
- key.stroke.super === !!event.super &&
|
|
|
- (key.stroke.hyper ?? false) === !!event.hyper,
|
|
|
- )
|
|
|
- : []
|
|
|
- const bound = matched.some((key) => typeof key.command !== "string" || !key.command.startsWith("input."))
|
|
|
- if (textInput && (!input?.focused || !pending || (!bound && !configured))) {
|
|
|
- if (input?.focused) promptTextInputPending = true
|
|
|
- return
|
|
|
- }
|
|
|
- if (!textInput && matched.length > 0 && !bound && !configured) return
|
|
|
- const value = promptTextInputPending && input && !input.isDestroyed ? input.plainText : undefined
|
|
|
- promptTextInputPending = false
|
|
|
- flushPromptSync(value)
|
|
|
- if (textInput && input?.focused) promptTextInputPending = true
|
|
|
- })
|
|
|
const renderer = useRenderer()
|
|
|
- const flushPromptSyncFrame = async () => flushPromptSync()
|
|
|
const exit = useExit()
|
|
|
const dimensions = useTerminalDimensions()
|
|
|
const theme = useTheme()
|
|
|
@@ -405,6 +344,7 @@ export function Prompt(props: PromptProps) {
|
|
|
category: "Prompt",
|
|
|
palette: undefined,
|
|
|
run: () => {
|
|
|
+ if (input.getTextRange(0, 1) === "") return false
|
|
|
clearPrompt()
|
|
|
dialog.clear()
|
|
|
},
|
|
|
@@ -509,6 +449,7 @@ export function Prompt(props: PromptProps) {
|
|
|
name: "prompt.editor",
|
|
|
slash: { name: "editor" },
|
|
|
run: async () => {
|
|
|
+ if (promptSyncQueued) await flushPromptSync()
|
|
|
dialog.clear()
|
|
|
|
|
|
const editorPrompt = expandPromptInputPastedText(store.prompt, store.prompt.pasted)
|
|
|
@@ -601,6 +542,9 @@ export function Prompt(props: PromptProps) {
|
|
|
get focused() {
|
|
|
return input.focused
|
|
|
},
|
|
|
+ get empty() {
|
|
|
+ return input.getTextRange(0, 1) === ""
|
|
|
+ },
|
|
|
get current() {
|
|
|
return store.prompt
|
|
|
},
|
|
|
@@ -640,11 +584,7 @@ export function Prompt(props: PromptProps) {
|
|
|
})
|
|
|
|
|
|
onCleanup(() => {
|
|
|
- stopPromptSyncInterceptor()
|
|
|
- flushPromptSync(!input || input.isDestroyed ? undefined : input.plainText)
|
|
|
- if (promptSyncQueued) flushPromptSync(!input || input.isDestroyed ? undefined : input.plainText)
|
|
|
- renderer.removeFrameCallback(flushPromptSyncFrame)
|
|
|
- promptSyncQueued = false
|
|
|
+ if (promptSyncQueued) void flushPromptSync()
|
|
|
if (store.prompt.text) {
|
|
|
stashed = { prompt: unwrap(store.prompt), cursor: input.cursorOffset }
|
|
|
}
|
|
|
@@ -770,9 +710,9 @@ export function Prompt(props: PromptProps) {
|
|
|
title: "Stash prompt",
|
|
|
name: "prompt.stash",
|
|
|
category: "Prompt",
|
|
|
- enabled: !!store.prompt.text,
|
|
|
run: () => {
|
|
|
- if (!store.prompt.text) return
|
|
|
+ if (input.getTextRange(0, 1) === "") return false
|
|
|
+ void flushPromptSync()
|
|
|
stash.push({ prompt: store.prompt })
|
|
|
input.extmarks.clear()
|
|
|
input.clear()
|
|
|
@@ -843,7 +783,7 @@ export function Prompt(props: PromptProps) {
|
|
|
Keymap.createLayer(() => {
|
|
|
return {
|
|
|
target: inputTarget,
|
|
|
- enabled: inputTarget() !== undefined && !props.disabled && store.prompt.text !== "",
|
|
|
+ enabled: () => inputTarget() !== undefined && !props.disabled,
|
|
|
bindings: ["prompt.clear"],
|
|
|
}
|
|
|
})
|
|
|
@@ -867,6 +807,10 @@ export function Prompt(props: PromptProps) {
|
|
|
title: "Shell mode",
|
|
|
group: "Prompt",
|
|
|
run: () => {
|
|
|
+ if (input.visualCursor.offset !== 0) {
|
|
|
+ input.insertText("!")
|
|
|
+ return
|
|
|
+ }
|
|
|
setStore("placeholder", randomIndex(shell().length))
|
|
|
setStore("mode", "shell")
|
|
|
},
|
|
|
@@ -989,6 +933,7 @@ export function Prompt(props: PromptProps) {
|
|
|
}
|
|
|
|
|
|
async function submitInner() {
|
|
|
+ if (promptSyncQueued) await flushPromptSync()
|
|
|
// IME: double-defer may fire before onContentChange flushes the last
|
|
|
// composed character (e.g. Korean hangul) to the store, so read
|
|
|
// plainText directly and sync before any downstream reads.
|
|
|
@@ -1291,30 +1236,20 @@ export function Prompt(props: PromptProps) {
|
|
|
}, 0)
|
|
|
}
|
|
|
|
|
|
- function flushPromptSync(value?: string) {
|
|
|
- const contentChanged = value !== undefined && value !== store.prompt.text
|
|
|
- if (!promptSyncQueued && !contentChanged) return
|
|
|
+ async function flushPromptSync() {
|
|
|
promptSyncQueued = false
|
|
|
- renderer.removeFrameCallback(flushPromptSyncFrame)
|
|
|
- const syncContent = promptContentChanged || contentChanged
|
|
|
- promptContentChanged = false
|
|
|
+ renderer.removeFrameCallback(flushPromptSync)
|
|
|
if (!input || input.isDestroyed) return
|
|
|
- if (syncContent) {
|
|
|
- const text = value ?? input.plainText
|
|
|
- setStore("prompt", "text", text)
|
|
|
- auto()?.onInput(text)
|
|
|
- syncExtmarksWithPromptParts()
|
|
|
- }
|
|
|
- setCursorVersion((value) => value + 1)
|
|
|
+ const value = input.plainText
|
|
|
+ setStore("prompt", "text", value)
|
|
|
+ auto()?.onInput(value)
|
|
|
+ syncExtmarksWithPromptParts()
|
|
|
}
|
|
|
|
|
|
- function queuePromptSync(contentChanged: boolean) {
|
|
|
- promptContentChanged ||= contentChanged
|
|
|
- if (contentChanged) promptTextInputPending = false
|
|
|
+ function queuePromptSync() {
|
|
|
if (promptSyncQueued) return
|
|
|
promptSyncQueued = true
|
|
|
- // Keep derived prompt state to one update per rendered frame across split stdin chunks.
|
|
|
- renderer.setFrameCallback(flushPromptSyncFrame)
|
|
|
+ renderer.setFrameCallback(flushPromptSync)
|
|
|
}
|
|
|
|
|
|
async function pasteAttachment(file: { filename?: string; uri: string }) {
|
|
|
@@ -1358,6 +1293,7 @@ export function Prompt(props: PromptProps) {
|
|
|
}
|
|
|
|
|
|
function clearPrompt() {
|
|
|
+ if (promptSyncQueued) void flushPromptSync()
|
|
|
if (
|
|
|
store.prompt.text.trim().length >= DRAFT_RETENTION_MIN_CHARS ||
|
|
|
store.prompt.pasted.length > 0 ||
|
|
|
@@ -1484,8 +1420,8 @@ export function Prompt(props: PromptProps) {
|
|
|
focusedTextColor={leader() ? theme.text.subdued : theme.text.default}
|
|
|
minHeight={1}
|
|
|
maxHeight={maxHeight()}
|
|
|
- onContentChange={() => queuePromptSync(true)}
|
|
|
- onCursorChange={() => queuePromptSync(false)}
|
|
|
+ onContentChange={queuePromptSync}
|
|
|
+ onCursorChange={() => setCursorVersion((value) => value + 1)}
|
|
|
onKeyDown={(e: { preventDefault(): void }) => {
|
|
|
if (props.disabled) {
|
|
|
e.preventDefault()
|