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
58 changes: 54 additions & 4 deletions app/frontend/src/components/GpuEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export interface GpuEditorHandle {
selectAll: () => void
openFind: (mode: 'find' | 'replace') => void
goToLine: (line: number) => void
expandSmartSelect: () => void
shrinkSmartSelect: () => void
}

interface Props {
Expand Down Expand Up @@ -481,6 +483,7 @@ const GpuEditor = forwardRef<GpuEditorHandle, Props>(function GpuEditor({
const leftColRef = useRef<number>(0)
const wheelAccumRef = useRef<number>(0)
const cursorsRef = useRef<Cursor[]>([{ line: 0, col: 0 }])
const smartSelectHistoryRef = useRef<Cursor[][]>([])
const bracketMatchRef = useRef<[[number, number], [number, number]] | null>(null)
const visibleRowsRef = useRef<number>(1)
const visibleColsRef = useRef<number>(1)
Expand Down Expand Up @@ -1505,6 +1508,7 @@ const GpuEditor = forwardRef<GpuEditorHandle, Props>(function GpuEditor({

const moveCursor = useCallback(async (dl: number, dc: number, extend: boolean) => {
closeCompletions()
smartSelectHistoryRef.current = []
const cursors = cursorsRef.current
const next: Cursor[] = []
for (const c of cursors) {
Expand Down Expand Up @@ -1661,6 +1665,46 @@ const GpuEditor = forwardRef<GpuEditorHandle, Props>(function GpuEditor({
void updateBracketMatch()
}, [closeCompletions, draw, ensureCursorVisible, ensureLine, fetchVisible, notifyCursor, updateBracketMatch])

// Shift+Alt+Right/Left — expand or shrink the selection by AST node.
const expandSmartSelect = useCallback(async () => {
if (!bufferIdRef.current) return
const c = cursorsRef.current[0]
// Normalize current selection: [anchorLine/Col, line/col] ordered by position.
let sl = c.anchorLine ?? c.line
let sc = c.anchorCol ?? c.col
let el = c.line
let ec = c.col
if (el < sl || (el === sl && ec < sc)) {
[sl, sc, el, ec] = [el, ec, sl, sc]
}
const resp = await invoke<{
noop?: boolean; startLine: number; startCol: number; endLine: number; endCol: number
}>('editor.smartSelect', { bufferId: bufferIdRef.current, startLine: sl, startCol: sc, endLine: el, endCol: ec })
if (resp.noop) return
// Save current cursors so shrink can restore them.
smartSelectHistoryRef.current.push(cursorsRef.current.map(x => ({ ...x })))
// Anchor at start, active end at end of expanded node.
cursorsRef.current = [{ line: resp.endLine, col: resp.endCol, anchorLine: resp.startLine, anchorCol: resp.startCol }]
cursorVisibleRef.current = true
notifyCursor()
ensureCursorVisible()
fetchVisible()
draw()
void updateBracketMatch()
}, [draw, ensureCursorVisible, fetchVisible, notifyCursor, updateBracketMatch])

const shrinkSmartSelect = useCallback(() => {
const prev = smartSelectHistoryRef.current.pop()
if (!prev) return
cursorsRef.current = prev
cursorVisibleRef.current = true
notifyCursor()
ensureCursorVisible()
fetchVisible()
draw()
void updateBracketMatch()
}, [draw, ensureCursorVisible, fetchVisible, notifyCursor, updateBracketMatch])

// Triple-click — select the entire line.
const selectLineAt = useCallback(async (line: number) => {
closeCompletions()
Expand Down Expand Up @@ -1924,8 +1968,12 @@ const GpuEditor = forwardRef<GpuEditorHandle, Props>(function GpuEditor({
case 'ArrowDown':
if (e.ctrlKey && e.altKey) { e.preventDefault(); void addCursorVertical(1); return }
e.preventDefault(); void moveCursor(1, 0, shift); return
case 'ArrowLeft': e.preventDefault(); void moveCursor(0, -1, shift); return
case 'ArrowRight': e.preventDefault(); void moveCursor(0, 1, shift); return
case 'ArrowLeft':
if (e.shiftKey && e.altKey) { e.preventDefault(); shrinkSmartSelect(); return }
e.preventDefault(); void moveCursor(0, -1, shift); return
case 'ArrowRight':
if (e.shiftKey && e.altKey) { e.preventDefault(); void expandSmartSelect(); return }
e.preventDefault(); void moveCursor(0, 1, shift); return
case 'PageUp': e.preventDefault(); void moveCursor(-visibleRowsRef.current, 0, shift); return
case 'PageDown': e.preventDefault(); void moveCursor(visibleRowsRef.current, 0, shift); return
case 'Home': e.preventDefault(); void moveCursorsTo(c => ({ line: c.line, col: 0 }), shift); return
Expand Down Expand Up @@ -1989,7 +2037,7 @@ const GpuEditor = forwardRef<GpuEditorHandle, Props>(function GpuEditor({
}
return
}
}, [acceptCompletion, addCursorVertical, advanceSnippetStop, closeCompletions, closeFind, copySelection, deleteBackward, deleteForward, draw, handleEnter, handleTypedChar, insertText, moveCursor, moveCursorsTo, openFind, redo, requestCompletions, save, selectAll, undo])
}, [acceptCompletion, addCursorVertical, advanceSnippetStop, closeCompletions, closeFind, copySelection, deleteBackward, deleteForward, draw, expandSmartSelect, handleEnter, handleTypedChar, insertText, moveCursor, moveCursorsTo, openFind, redo, requestCompletions, save, selectAll, shrinkSmartSelect, undo])

const onInput = useCallback((e: React.FormEvent<HTMLTextAreaElement>) => {
const ta = e.currentTarget
Expand Down Expand Up @@ -2219,7 +2267,9 @@ const GpuEditor = forwardRef<GpuEditorHandle, Props>(function GpuEditor({
textareaRef.current?.focus()
})
},
}), [save, undo, redo, selectAll, openFind, setCursorTo])
expandSmartSelect: () => { void expandSmartSelect() },
shrinkSmartSelect,
}), [save, undo, redo, selectAll, openFind, setCursorTo, expandSmartSelect, shrinkSmartSelect])

return (
<div className="h-full flex flex-col bg-[var(--app-bg)] overflow-hidden">
Expand Down
30 changes: 20 additions & 10 deletions app/frontend/src/fullscreen/DeleteConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import React, { useEffect } from 'react'

interface Props {
name: string
isDir: boolean
targets: Array<{ name: string; isDir: boolean }>
onConfirm: () => void
onCancel: () => void
}

export default function DeleteConfirmDialog({ name, isDir, onConfirm, onCancel }: Props) {
// Close on Escape
export default function DeleteConfirmDialog({ targets, onConfirm, onCancel }: Props) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onCancel()
Expand All @@ -18,16 +16,28 @@ export default function DeleteConfirmDialog({ name, isDir, onConfirm, onCancel }
return () => document.removeEventListener('keydown', handler)
}, [onConfirm, onCancel])

const isSingle = targets.length === 1
const title = isSingle
? `Delete ${targets[0].isDir ? 'Folder' : 'File'}`
: `Delete ${targets.length} Items`

return (
<div className="del-dialog-overlay" onMouseDown={onCancel}>
<div className="del-dialog" onMouseDown={e => e.stopPropagation()}>
<div className="del-dialog__title">
Delete {isDir ? 'Folder' : 'File'}
</div>
<div className="del-dialog__title">{title}</div>
<div className="del-dialog__body">
Are you sure you want to delete <strong>"{name}"</strong>?
{isDir && (
<span className="del-dialog__warn"> All contents will be removed.</span>
{isSingle ? (
<>
Are you sure you want to delete <strong>"{targets[0].name}"</strong>?
{targets[0].isDir && (
<span className="del-dialog__warn"> All contents will be removed.</span>
)}
</>
) : (
<>
Are you sure you want to delete <strong>{targets.length} items</strong>?
<span className="del-dialog__warn"> Folders and their contents will be removed.</span>
</>
)}
<span className="del-dialog__note"> This cannot be undone.</span>
</div>
Expand Down
Loading
Loading