Skip to content

Commit e338de8

Browse files
committed
Extract undo-redo hook and enable undo/redo in templates
- in templates, enable type history e.g. cmd+z, similarly to prompt field - place undo/redo in a hook ├── apps │ └── editor │ └── src │ └── views │ └── settings │ └── frontend │ ├── Settings.tsx │ ├── forms │ │ └── EditTemplateForm │ │ └── EditTemplateForm.tsx │ └── hooks │ └── use-template-editing.ts └── packages └── ui └── src └── components └── editor └── common └── prompts ├── PromptField │ ├── PromptField.module.scss │ ├── PromptField.stories.tsx │ ├── PromptField.tsx │ ├── components │ │ ├── CompletedTasks │ │ │ ├── CompletedTasks.module.scss │ │ │ ├── CompletedTasks.tsx │ │ │ └── index.ts │ │ ├── Footer │ │ │ ├── Footer.module.scss │ │ │ ├── Footer.tsx │ │ │ └── index.ts │ │ └── Tabs │ │ ├── Tabs.module.scss │ │ ├── Tabs.tsx │ │ └── index.ts │ ├── hooks │ │ ├── use-drag-drop.ts │ │ ├── use-dropdown.ts │ │ ├── use-ghost-text.ts │ │ ├── use-handlers │ │ │ ├── handle-copy.ts │ │ │ ├── handle-cut.ts │ │ │ ├── handle-input-change.ts │ │ │ ├── handle-input-click.ts │ │ │ ├── handle-key-down.ts │ │ │ ├── handle-paste.ts │ │ │ ├── handle-submit.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── use-handlers.ts │ │ │ └── utils.ts │ │ └── use-keyboard-shortcuts.ts │ └── index.ts ├── Template │ ├── Template.module.scss │ ├── Template.tsx │ └── index.ts └── shared ├── hooks │ └── use-undo-redo.ts └── symbols ├── caret.ts ├── get-highlighted-text.tsx ├── index.ts ├── position-mapping.ts ├── prompt-symbols.module.scss ├── reconstruct-raw-value.ts └── use-symbol-deletion.ts
1 parent 2eef21a commit e338de8

6 files changed

Lines changed: 219 additions & 83 deletions

File tree

‎apps/editor/src/views/settings/backend/message-handlers/handle-hash-sign-quick-pick.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const handle_hash_sign_quick_pick = async (
1414
const replacement = await hash_sign_quick_pick({
1515
extension_context: provider.extension_context,
1616
on_insert,
17-
exclude: ['commit']
17+
exclude: ['commit', 'changes', 'saved-context']
1818
})
1919

2020
if (replacement) {

‎packages/ui/src/components/editor/common/prompts/PromptField/hooks/use-handlers/handle-key-down.ts‎

Lines changed: 3 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,7 @@ export const create_handle_key_down = (
8282
value.substring(0, current_raw_pos - 1) +
8383
value.substring(current_raw_pos)
8484

85-
state.set_undo_stack((prev) => [
86-
...prev,
87-
{ value, raw_caret_pos: current_raw_pos }
88-
])
89-
state.set_redo_stack([])
85+
utils.save_history(value, current_raw_pos)
9086
refs.props_ref.current.on_change(new_value)
9187
refs.has_modified_current_entry_ref.current = new_value !== ''
9288
set_caret_position_after_change({
@@ -226,50 +222,12 @@ export const create_handle_key_down = (
226222

227223
const handle_undo_key = (e: React.KeyboardEvent<HTMLDivElement>) => {
228224
e.preventDefault()
229-
if (state.undo_stack.length > 0) {
230-
const prev_entry = state.undo_stack[state.undo_stack.length - 1]
231-
state.set_undo_stack((prev) => prev.slice(0, -1))
232-
state.set_redo_stack((prev) => [
233-
...prev,
234-
{
235-
value: props.value,
236-
raw_caret_pos: refs.raw_caret_pos_ref.current
237-
}
238-
])
239-
props.on_change(prev_entry.value)
240-
set_caret_position_after_change({
241-
input_ref: params.input_ref,
242-
new_raw_cursor_pos: prev_entry.raw_caret_pos,
243-
new_value: prev_entry.value,
244-
selected_files: props.selected_files ?? []
245-
})
246-
state.set_history_index(-1)
247-
refs.has_modified_current_entry_ref.current = true
248-
}
225+
utils.undo()
249226
}
250227

251228
const handle_redo_key = (e: React.KeyboardEvent<HTMLDivElement>) => {
252229
e.preventDefault()
253-
if (state.redo_stack.length > 0) {
254-
const next_entry = state.redo_stack[state.redo_stack.length - 1]
255-
state.set_redo_stack((prev) => prev.slice(0, -1))
256-
state.set_undo_stack((prev) => [
257-
...prev,
258-
{
259-
value: props.value,
260-
raw_caret_pos: refs.raw_caret_pos_ref.current
261-
}
262-
])
263-
props.on_change(next_entry.value)
264-
set_caret_position_after_change({
265-
input_ref: params.input_ref,
266-
new_raw_cursor_pos: next_entry.raw_caret_pos,
267-
new_value: next_entry.value,
268-
selected_files: props.selected_files ?? []
269-
})
270-
state.set_history_index(-1)
271-
refs.has_modified_current_entry_ref.current = true
272-
}
230+
utils.redo()
273231
}
274232

275233
const handle_tab_key = (e: React.KeyboardEvent<HTMLDivElement>) => {

‎packages/ui/src/components/editor/common/prompts/PromptField/hooks/use-handlers/types.ts‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import { RefObject, MutableRefObject, Dispatch, SetStateAction } from 'react'
22
import type { PromptField } from '../../PromptField'
33

4-
export type HistoryEntry = {
5-
value: string
6-
raw_caret_pos: number
7-
}
8-
94
export type HandlerContext = {
105
props: PromptField.Props
116
params: {
@@ -27,15 +22,14 @@ export type HandlerContext = {
2722
history_index: number
2823
set_history_index: Dispatch<SetStateAction<number>>
2924
is_history_enabled: boolean
30-
undo_stack: HistoryEntry[]
31-
set_undo_stack: Dispatch<SetStateAction<HistoryEntry[]>>
32-
redo_stack: HistoryEntry[]
33-
set_redo_stack: Dispatch<SetStateAction<HistoryEntry[]>>
3425
}
3526
utils: {
3627
update_value: (new_value: string, caret_pos?: number) => void
3728
handle_symbol_deletion_by_click: (element: HTMLElement) => void
3829
handle_backspace_key: (e: React.KeyboardEvent<HTMLDivElement>) => void
3930
perform_paste: (text: string) => void
31+
save_history: (custom_value?: string, custom_caret_pos?: number) => void
32+
undo: () => void
33+
redo: () => void
4034
}
4135
}

‎packages/ui/src/components/editor/common/prompts/PromptField/hooks/use-handlers/use-handlers.ts‎

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
map_raw_pos_to_display_pos,
88
use_symbol_deletion
99
} from '../../../shared/symbols'
10-
import { HandlerContext, HistoryEntry } from './types'
10+
import { use_undo_redo } from '../../../shared/hooks/use-undo-redo'
11+
import { HandlerContext } from './types'
1112
import { set_caret_position_after_change } from './utils'
1213
import { create_handle_copy } from './handle-copy'
1314
import { create_handle_cut } from './handle-cut'
@@ -31,8 +32,6 @@ export const use_handlers = (
3132

3233
const [history_index, set_history_index] = useState(-1)
3334
const [is_history_enabled, set_is_history_enabled] = useState(!props.value)
34-
const [undo_stack, set_undo_stack] = useState<HistoryEntry[]>([])
35-
const [redo_stack, set_redo_stack] = useState<HistoryEntry[]>([])
3635
const raw_caret_pos_ref = useRef(0)
3736
const has_modified_current_entry_ref = useRef(false)
3837
const is_shift_pressed_ref = useRef(false)
@@ -206,13 +205,25 @@ export const use_handlers = (
206205
}
207206
}, [props.focus_and_select_key])
208207

208+
const { save_history, undo, redo } = use_undo_redo({
209+
value: props.value,
210+
raw_caret_pos_ref,
211+
on_change: props.on_change,
212+
on_restore: (entry) => {
213+
set_caret_position_after_change({
214+
input_ref: params.input_ref,
215+
new_raw_cursor_pos: entry.raw_caret_pos,
216+
new_value: entry.value,
217+
selected_files: props.selected_files ?? []
218+
})
219+
set_history_index(-1)
220+
has_modified_current_entry_ref.current = true
221+
}
222+
})
223+
209224
const update_value = (new_value: string, caret_pos?: number) => {
210225
if (new_value === props.value) return
211-
set_undo_stack((prev) => [
212-
...prev,
213-
{ value: props.value, raw_caret_pos: raw_caret_pos_ref.current }
214-
])
215-
set_redo_stack([])
226+
save_history()
216227
props.on_change(new_value)
217228
if (caret_pos !== undefined) {
218229
set_caret_position_after_change({
@@ -250,17 +261,16 @@ export const use_handlers = (
250261
state: {
251262
history_index,
252263
set_history_index,
253-
is_history_enabled,
254-
undo_stack,
255-
set_undo_stack,
256-
redo_stack,
257-
set_redo_stack
264+
is_history_enabled
258265
},
259266
utils: {
260267
update_value,
261268
handle_symbol_deletion_by_click,
262269
handle_backspace_key,
263-
perform_paste: () => {}
270+
perform_paste: () => {},
271+
save_history,
272+
undo,
273+
redo
264274
}
265275
}
266276

‎packages/ui/src/components/editor/common/prompts/Template/Template.tsx‎

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
useRef,
44
useImperativeHandle,
55
useEffect,
6-
useMemo
6+
useMemo,
7+
useState
78
} from 'react'
89
import styles from './Template.module.scss'
910
import { Icon } from '../../Icon'
@@ -16,6 +17,7 @@ import {
1617
map_display_pos_to_raw_pos,
1718
use_symbol_deletion
1819
} from '../shared/symbols'
20+
import { use_undo_redo } from '../shared/hooks/use-undo-redo'
1921

2022
export type TemplateHandle = {
2123
focus: () => void
@@ -36,6 +38,54 @@ export const Template = forwardRef<TemplateHandle, TemplateProps>(
3638
(props, ref) => {
3739
const input_ref = useRef<HTMLDivElement>(null)
3840

41+
const raw_caret_pos_ref = useRef(0)
42+
43+
const { save_history, undo, redo } = use_undo_redo({
44+
value: props.value || '',
45+
raw_caret_pos_ref,
46+
on_change: props.on_change,
47+
on_restore: (entry) => {
48+
setTimeout(() => {
49+
if (input_ref.current) {
50+
const display_pos = map_raw_pos_to_display_pos({
51+
raw_pos: entry.raw_caret_pos,
52+
raw_text: entry.value,
53+
context_file_paths: []
54+
})
55+
set_caret_position_for_div(input_ref.current, display_pos)
56+
}
57+
}, 0)
58+
}
59+
})
60+
61+
useEffect(() => {
62+
const handle_selection_change = () => {
63+
if (
64+
document.activeElement === input_ref.current &&
65+
input_ref.current
66+
) {
67+
const display_pos = get_caret_position_from_div(input_ref.current)
68+
const raw_pos = map_display_pos_to_raw_pos({
69+
display_pos,
70+
raw_text: props.value || '',
71+
context_file_paths: []
72+
})
73+
raw_caret_pos_ref.current = raw_pos
74+
}
75+
}
76+
77+
document.addEventListener('selectionchange', handle_selection_change)
78+
if (input_ref.current) {
79+
input_ref.current.addEventListener('focus', handle_selection_change)
80+
}
81+
return () => {
82+
document.removeEventListener('selectionchange', handle_selection_change)
83+
if (input_ref.current) {
84+
input_ref.current.removeEventListener('focus', handle_selection_change)
85+
}
86+
}
87+
}, [props.value])
88+
3989
useImperativeHandle(ref, () => ({
4090
focus: () => input_ref.current?.focus(),
4191
get_caret_position: () => {
@@ -63,23 +113,31 @@ export const Template = forwardRef<TemplateHandle, TemplateProps>(
63113
}
64114
}))
65115

116+
const update_value = (new_value: string, caret_pos?: number) => {
117+
if (new_value === (props.value || '')) return
118+
save_history()
119+
props.on_change(new_value)
120+
if (caret_pos !== undefined) {
121+
setTimeout(() => {
122+
if (input_ref.current) {
123+
const display_pos = map_raw_pos_to_display_pos({
124+
raw_pos: caret_pos,
125+
raw_text: new_value,
126+
context_file_paths: []
127+
})
128+
set_caret_position_for_div(input_ref.current, display_pos)
129+
}
130+
}, 0)
131+
}
132+
}
133+
66134
const { handle_symbol_deletion_by_click, handle_backspace_key } =
67135
use_symbol_deletion({
68136
value: props.value || '',
69137
context_file_paths: [],
70138
input_ref,
71139
on_delete: (new_value, new_caret_pos) => {
72-
props.on_change(new_value)
73-
setTimeout(() => {
74-
if (input_ref.current) {
75-
const display_pos = map_raw_pos_to_display_pos({
76-
raw_pos: new_caret_pos,
77-
raw_text: new_value,
78-
context_file_paths: []
79-
})
80-
set_caret_position_for_div(input_ref.current, display_pos)
81-
}
82-
}, 0)
140+
update_value(new_value, new_caret_pos)
83141
}
84142
})
85143

@@ -113,7 +171,7 @@ export const Template = forwardRef<TemplateHandle, TemplateProps>(
113171
const new_raw_value = reconstruct_raw_value_from_node(currentTarget)
114172

115173
if (new_raw_value !== props.value) {
116-
props.on_change(new_raw_value)
174+
update_value(new_raw_value)
117175
}
118176

119177
const native_event = e.nativeEvent as unknown as { inputType?: string }
@@ -155,8 +213,33 @@ export const Template = forwardRef<TemplateHandle, TemplateProps>(
155213
}
156214
}
157215

216+
const handle_undo = (e: React.KeyboardEvent<HTMLDivElement>) => {
217+
e.preventDefault()
218+
undo()
219+
}
220+
221+
const handle_redo = (e: React.KeyboardEvent<HTMLDivElement>) => {
222+
e.preventDefault()
223+
redo()
224+
}
225+
158226
const handle_key_down = (e: React.KeyboardEvent<HTMLDivElement>) => {
159-
if (e.key === 'Backspace') {
227+
const { key, shiftKey } = e
228+
229+
if ((e.ctrlKey || e.metaKey) && !shiftKey && key.toLowerCase() === 'z') {
230+
handle_undo(e)
231+
return
232+
}
233+
234+
if (
235+
((e.ctrlKey || e.metaKey) && key.toLowerCase() === 'y') ||
236+
((e.ctrlKey || e.metaKey) && shiftKey && key.toLowerCase() === 'z')
237+
) {
238+
handle_redo(e)
239+
return
240+
}
241+
242+
if (key === 'Backspace') {
160243
handle_backspace_key(e)
161244
}
162245
}

0 commit comments

Comments
 (0)