Skip to content

Commit f96d677

Browse files
committed
Implement undo/redo functionality for prompt field input changes and refactor keyword deletion logic to use the new update_value helper
1 parent 325b3b4 commit f96d677

2 files changed

Lines changed: 103 additions & 97 deletions

File tree

packages/ui/src/components/editor/panel/PromptField/PromptField.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,40 @@ export const PromptField: React.FC<PromptFieldProps> = (props) => {
398398
}
399399
}
400400

401+
const handle_copy = (e: React.ClipboardEvent<HTMLDivElement>) => {
402+
const selection = window.getSelection()
403+
if (!selection || selection.rangeCount === 0 || selection.isCollapsed)
404+
return
405+
406+
const range = selection.getRangeAt(0)
407+
const input_element = input_ref.current
408+
if (!input_element || !input_element.contains(range.startContainer)) return
409+
410+
e.preventDefault()
411+
412+
const pre_selection_range = document.createRange()
413+
pre_selection_range.selectNodeContents(input_element)
414+
pre_selection_range.setEnd(range.startContainer, range.startOffset)
415+
const display_start = pre_selection_range.toString().length
416+
417+
pre_selection_range.setEnd(range.endContainer, range.endOffset)
418+
const display_end = pre_selection_range.toString().length
419+
420+
const raw_start = map_display_pos_to_raw_pos(
421+
display_start,
422+
props.value,
423+
props.context_file_paths ?? []
424+
)
425+
const raw_end = map_display_pos_to_raw_pos(
426+
display_end,
427+
props.value,
428+
props.context_file_paths ?? []
429+
)
430+
431+
const raw_text_slice = props.value.substring(raw_start, raw_end)
432+
e.clipboardData.setData('text/plain', raw_text_slice)
433+
}
434+
401435
const has_no_context =
402436
!props.context_file_paths || props.context_file_paths.length == 0
403437

@@ -453,6 +487,7 @@ export const PromptField: React.FC<PromptFieldProps> = (props) => {
453487
suppressContentEditableWarning={true}
454488
onInput={handle_input_change}
455489
onKeyDown={handle_key_down}
490+
onCopy={handle_copy}
456491
onClick={handle_input_click}
457492
className={cn(styles.input, {
458493
[styles['input-with-tab-hint']]: show_tab_hint,

packages/ui/src/components/editor/panel/PromptField/hooks/use-handlers.ts

Lines changed: 68 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { RefObject, useState } from 'react'
1+
import { RefObject, useState, useEffect } from 'react'
22
import { PromptFieldProps } from '../PromptField'
3-
import { get_display_text } from '../utils/get-display-text'
43
import {
54
get_caret_position_from_div,
65
set_caret_position_for_div
@@ -84,15 +83,31 @@ export const use_handlers = (
8483
) => {
8584
const [history_index, set_history_index] = useState(-1)
8685
const [is_history_enabled, set_is_history_enabled] = useState(!props.value)
87-
const [saved_value_before_at_sign, set_saved_value_before_at_sign] = useState<
88-
string | null
89-
>(null)
86+
const [undo_stack, set_undo_stack] = useState<string[]>([])
87+
const [redo_stack, set_redo_stack] = useState<string[]>([])
88+
89+
useEffect(() => {
90+
set_is_history_enabled(history_index >= 0 || !props.value)
91+
}, [history_index, props.value])
92+
93+
const update_value = (new_value: string, caret_pos?: number) => {
94+
if (new_value === props.value) return
95+
set_undo_stack((prev) => [...prev, props.value])
96+
set_redo_stack([])
97+
props.on_change(new_value)
98+
if (caret_pos !== undefined) {
99+
set_caret_position_after_change(
100+
input_ref,
101+
caret_pos,
102+
new_value,
103+
props.context_file_paths ?? []
104+
)
105+
}
106+
}
90107

91108
const handle_clear = () => {
92-
props.on_change('')
109+
update_value('')
93110
set_history_index(-1)
94-
set_is_history_enabled(true)
95-
set_saved_value_before_at_sign(null)
96111
}
97112

98113
const handle_input_change = (e: React.FormEvent<HTMLDivElement>) => {
@@ -102,17 +117,14 @@ export const use_handlers = (
102117
if (new_value === props.value) {
103118
return
104119
}
105-
106-
props.on_change(new_value)
120+
update_value(new_value)
107121
set_history_index(-1)
108-
set_saved_value_before_at_sign(null)
109122

110123
const new_display_value = currentTarget.textContent ?? ''
111124
const caret_position = get_caret_position_from_div(currentTarget)
112125
const char_before_caret = new_display_value.charAt(caret_position - 1)
113126

114127
if (char_before_caret === '@') {
115-
set_saved_value_before_at_sign(props.value)
116128
setTimeout(() => {
117129
props.on_at_sign_click()
118130
}, 150)
@@ -121,10 +133,6 @@ export const use_handlers = (
121133
props.on_hash_sign_click()
122134
}, 150)
123135
}
124-
125-
if (!new_value) {
126-
set_is_history_enabled(true)
127-
}
128136
}
129137

130138
const handle_submit = (
@@ -169,14 +177,7 @@ export const use_handlers = (
169177
props.value.substring(0, start_of_path) +
170178
props.value.substring(end_of_path + 1)
171179
const new_raw_cursor_pos = start_of_path
172-
props.on_change(new_value)
173-
174-
set_caret_position_after_change(
175-
input_ref,
176-
new_raw_cursor_pos,
177-
new_value,
178-
context_file_paths
179-
)
180+
update_value(new_value, new_raw_cursor_pos)
180181
return true
181182
}
182183
}
@@ -196,14 +197,7 @@ export const use_handlers = (
196197
props.value.substring(0, start_of_match) +
197198
props.value.substring(raw_pos)
198199
const new_raw_cursor_pos = start_of_match
199-
props.on_change(new_value)
200-
201-
set_caret_position_after_change(
202-
input_ref,
203-
new_raw_cursor_pos,
204-
new_value,
205-
context_file_paths
206-
)
200+
update_value(new_value, new_raw_cursor_pos)
207201
return true
208202
}
209203
return false
@@ -224,14 +218,7 @@ export const use_handlers = (
224218
props.value.substring(0, start_of_match) +
225219
props.value.substring(raw_pos)
226220
const new_raw_cursor_pos = start_of_match
227-
props.on_change(new_value)
228-
229-
set_caret_position_after_change(
230-
input_ref,
231-
new_raw_cursor_pos,
232-
new_value,
233-
context_file_paths
234-
)
221+
update_value(new_value, new_raw_cursor_pos)
235222
return true
236223
}
237224
return false
@@ -250,14 +237,7 @@ export const use_handlers = (
250237
props.value.substring(0, start_of_match) +
251238
props.value.substring(raw_pos)
252239
const new_raw_cursor_pos = start_of_match
253-
props.on_change(new_value)
254-
255-
set_caret_position_after_change(
256-
input_ref,
257-
new_raw_cursor_pos,
258-
new_value,
259-
context_file_paths
260-
)
240+
update_value(new_value, new_raw_cursor_pos)
261241
return true
262242
}
263243
return false
@@ -276,14 +256,7 @@ export const use_handlers = (
276256
props.value.substring(0, start_of_match) +
277257
props.value.substring(raw_pos)
278258
const new_raw_cursor_pos = start_of_match
279-
props.on_change(new_value)
280-
281-
set_caret_position_after_change(
282-
input_ref,
283-
new_raw_cursor_pos,
284-
new_value,
285-
context_file_paths
286-
)
259+
update_value(new_value, new_raw_cursor_pos)
287260
return true
288261
}
289262
return false
@@ -393,22 +366,14 @@ export const use_handlers = (
393366
const handle_history_navigation = (
394367
e: React.KeyboardEvent<HTMLDivElement>
395368
) => {
396-
set_saved_value_before_at_sign(null)
397-
398369
const active_history = props.chat_history
399370

400371
if (active_history.length === 0) return
401372

402373
e.preventDefault()
403374

404375
const update_and_set_caret = (value: string) => {
405-
props.on_change(value)
406-
set_caret_position_after_change(
407-
input_ref,
408-
value.length,
409-
value,
410-
props.context_file_paths ?? []
411-
)
376+
update_value(value, value.length)
412377
}
413378

414379
if (e.key === 'ArrowUp') {
@@ -429,27 +394,45 @@ export const use_handlers = (
429394
}
430395
}
431396

432-
const handle_undo_at_sign = (e: React.KeyboardEvent<HTMLDivElement>) => {
433-
if (saved_value_before_at_sign === null) return
434-
435-
e.preventDefault()
436-
const restored_value = saved_value_before_at_sign
437-
set_saved_value_before_at_sign(null)
438-
props.on_change(restored_value)
439-
440-
// Restore caret position
441-
setTimeout(() => {
442-
const display_restored = get_display_text(
443-
restored_value,
444-
props.context_file_paths ?? []
445-
)
446-
set_caret_position_for_div(e.currentTarget, display_restored.length)
447-
}, 0)
448-
}
449-
450397
const handle_key_down = (e: React.KeyboardEvent<HTMLDivElement>) => {
451-
const { key, shiftKey, ctrlKey, metaKey } = e
452-
398+
const { key, shiftKey } = e
399+
if ((e.ctrlKey || e.metaKey) && !shiftKey && key.toLowerCase() === 'z') {
400+
e.preventDefault()
401+
if (undo_stack.length > 0) {
402+
const prev_value = undo_stack[undo_stack.length - 1]
403+
set_undo_stack((prev) => prev.slice(0, -1))
404+
set_redo_stack((prev) => [...prev, props.value])
405+
props.on_change(prev_value)
406+
set_caret_position_after_change(
407+
input_ref,
408+
prev_value.length,
409+
prev_value,
410+
props.context_file_paths ?? []
411+
)
412+
set_history_index(-1)
413+
}
414+
return
415+
}
416+
if (
417+
((e.ctrlKey || e.metaKey) && key.toLowerCase() === 'y') ||
418+
((e.ctrlKey || e.metaKey) && shiftKey && key.toLowerCase() === 'z')
419+
) {
420+
e.preventDefault()
421+
if (redo_stack.length > 0) {
422+
const next_value = redo_stack[redo_stack.length - 1]
423+
set_redo_stack((prev) => prev.slice(0, -1))
424+
set_undo_stack((prev) => [...prev, props.value])
425+
props.on_change(next_value)
426+
set_caret_position_after_change(
427+
input_ref,
428+
next_value.length,
429+
next_value,
430+
props.context_file_paths ?? []
431+
)
432+
set_history_index(-1)
433+
}
434+
return
435+
}
453436
if (key == 'Tab' && !shiftKey) {
454437
handle_tab_key(e)
455438
return
@@ -470,18 +453,6 @@ export const use_handlers = (
470453
handle_history_navigation(e)
471454
return
472455
}
473-
474-
if (key == 'z' && (ctrlKey || metaKey) && !shiftKey) {
475-
if (saved_value_before_at_sign !== null) {
476-
handle_undo_at_sign(e)
477-
return
478-
}
479-
// Otherwise, we don't return, allowing the default undo behavior
480-
}
481-
482-
if (props.value) {
483-
set_is_history_enabled(false)
484-
}
485456
}
486457

487458
return {

0 commit comments

Comments
 (0)