Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 27 additions & 23 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,13 @@ import {
dim,
fg,
} from "@opentui/core"
import {
createEffect,
createMemo,
onMount,
createSignal,
onCleanup,
on,
Show,
Switch,
Match,
} from "solid-js"
import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js"
import "opentui-spinner/solid"
import path from "path"
import { fileURLToPath } from "url"
import { Filesystem } from "@/util/filesystem"
import { useLocal } from "@tui/context/local"
import { tint, useTheme } from "@tui/context/theme"
import { selectedForeground, tint, useTheme } from "@tui/context/theme"
import { EmptyBorder, SplitBorder } from "@tui/component/border"
import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route"
Expand Down Expand Up @@ -290,10 +280,13 @@ export function Prompt(props: PromptProps) {
if (props.disabled || vimState.isCopy()) {
input.cursorColor = theme.backgroundElement
input.showCursor = false
} else {
input.cursorColor = theme.text
input.showCursor = true
return
}
const visual = vimState.isVisual()
input.cursorColor = theme.text
input.showCursor = !visual
input.selectionBg = visual ? theme.secondary : undefined
input.selectionFg = visual ? selectedForeground(theme, theme.secondary) : undefined
})

createEffect((prev: boolean | undefined) => {
Expand Down Expand Up @@ -1755,14 +1748,25 @@ export function Prompt(props: PromptProps) {
input.scrollY,
input.height,
)
if (!rows.length) return
const bg = input.selectionBg ?? input.textColor
const fg =
input.selectionFg ??
(input.backgroundColor.a > 0 ? input.backgroundColor : RGBA.fromInts(0, 0, 0))
rows.forEach((row) => {
buffer.setCell(input.x, input.y + row, " ", fg, bg)
})
if (rows.length) {
const bg = input.selectionBg ?? input.textColor
const fg =
input.selectionFg ??
(input.backgroundColor.a > 0 ? input.backgroundColor : RGBA.fromInts(0, 0, 0))
rows.forEach((row) => {
buffer.setCell(input.x, input.y + row, " ", fg, bg)
})
}
if (input.visualCursor.visualRow < 0 || input.visualCursor.visualRow >= input.height) return
if (input.visualCursor.visualCol < 0 || input.visualCursor.visualCol >= input.width) return
// recolor the cursor cell in place; setCell would clobber the underlying glyph
const cursorOffset =
((input.y + input.visualCursor.visualRow) * buffer.width +
input.x +
input.visualCursor.visualCol) *
4
buffer.buffers.fg.set(selectedForeground(theme, theme.text).buffer.subarray(0, 4), cursorOffset)
buffer.buffers.bg.set(theme.text.buffer.subarray(0, 4), cursorOffset)
}
}
props.ref?.(ref)
Expand Down
10 changes: 3 additions & 7 deletions packages/opencode/src/cli/cmd/tui/component/vim/vim-motions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,7 @@ function nextCharwiseSpan(text: string, cursor: number, c: NextClassification):
return { start: cursor, end }
}

export function nextParagraphOperation(
textarea: TextareaRenderable,
operation: ParagraphOperation,
): ParagraphResult {
export function nextParagraphOperation(textarea: TextareaRenderable, operation: ParagraphOperation): ParagraphResult {
const text = textarea.plainText
const cursor = textarea.cursorOffset
if (text.length === 0) return { span: null, register: null }
Expand Down Expand Up @@ -319,8 +316,7 @@ export function wordEnd(text: string, offset: number, big: boolean) {
if (pos >= text.length) pos = text.length - 1

const startClass = wordClass(text[pos], big)
const atRunEnd =
startClass === "blank" || pos + 1 >= text.length || wordClass(text[pos + 1], big) !== startClass
const atRunEnd = startClass === "blank" || pos + 1 >= text.length || wordClass(text[pos + 1], big) !== startClass

if (atRunEnd) {
pos++
Expand Down Expand Up @@ -771,7 +767,7 @@ export function syncSelection(textarea: TextareaRenderable, anchor: number, line
textarea.cursorOffset = forward ? hi : lo
ta.updateSelectionForMovement(true, false)
textarea.cursorOffset = cursor
textarea.editorView.setSelection(lo, hi)
textarea.editorView.setSelection(lo, hi, textarea.selectionBg, textarea.selectionFg)
}

export function clearSelection(textarea: TextareaRenderable) {
Expand Down
56 changes: 42 additions & 14 deletions packages/opencode/src/cli/cmd/tui/routes/session/copy-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ const empty: CopyState = {

const segmenter = new Intl.Segmenter()

type Endpoint = { idx: number; col: number }
function orderEndpoints(a: Endpoint, b: Endpoint): { start: Endpoint; end: Endpoint } {
const aFirst = a.idx < b.idx || (a.idx === b.idx && a.col <= b.col)
return aFirst ? { start: a, end: b } : { start: b, end: a }
}

export function createCopyMode(input: {
scroll: () => ScrollBoxRenderable
messages: Accessor<{ id: string; role: string }[]>
Expand Down Expand Up @@ -449,10 +455,8 @@ export function createCopyMode(input: {
.getChildren()
.map((c) => [c.id, c]),
)
const a = s.anchor
const h = { idx: s.idx, col: s.col }
const start = a.idx <= h.idx ? a : h
const end = a.idx <= h.idx ? h : a
const { start, end } = orderEndpoints(s.anchor, h)
if (s.visual === "line") {
return Array.from({ length: end.idx - start.idx + 1 }, (_, i) => list[start.idx + i])
.filter((row): row is CopyRow => !!row)
Expand Down Expand Up @@ -601,11 +605,21 @@ export function createCopyMode(input: {
.getChildren()
.map((c) => [c.id, c]),
)
const a = s.anchor
const h = { idx: s.idx, col: s.col }
const start = a.idx <= h.idx ? a : h
const end = a.idx <= h.idx ? h : a
const { start, end } = orderEndpoints(s.anchor, h)
const out = new Map<string, CopyHighlight[]>()
const addHighlight = (row: CopyRow, min: number, text: string, left: number, right: number) => {
if (left > right) return
const entry = {
line: row.line,
left,
right,
text: text.slice(Math.max(0, left - min), Math.max(0, right - min + 1)),
}
const arr = out.get(row.id)
if (arr) arr.push(entry)
else out.set(row.id, [entry])
}
for (let i = start.idx; i <= end.idx; i++) {
const r = list[i]
if (!r) continue
Expand All @@ -616,18 +630,31 @@ export function createCopyMode(input: {
s.visual === "line" ? min : i === start.idx && i === end.idx ? start.col : i === start.idx ? start.col : min
const right =
s.visual === "line" ? max : i === start.idx && i === end.idx ? end.col : i === end.idx ? end.col : max
const cur = out.get(r.id) ?? []
cur.push({
line: r.line,
left,
right,
text: text.slice(Math.max(0, left - min), Math.max(0, right - min + 1)),
})
out.set(r.id, cur)
if (i !== h.idx) {
addHighlight(r, min, text, left, right)
continue
}
// cursor cell is painted separately by CopyOverlay so the cursor keeps its theme.text color
addHighlight(r, min, text, left, h.col - 1)
addHighlight(r, min, text, h.col + 1, right)
}
return out
})

const cursorText = createMemo(() => {
const s = state()
if (!s.active) return " "
const row = rows()[s.idx]
if (!row) return " "
const text = copyText()
let col = 0
for (const seg of segmenter.segment(text)) {
if (col >= s.col) return seg.segment
col += Bun.stringWidth(seg.segment)
}
return " "
})

return {
prompt: {
enter,
Expand Down Expand Up @@ -656,5 +683,6 @@ export function createCopyMode(input: {
active: () => state().active,
clamp,
state,
cursorText,
}
}
Loading
Loading