Fortran · 282165 bytes Raw Blame History
1 module command_handler_module
2 use iso_fortran_env, only: int32, error_unit
3 use iso_c_binding, only: c_int
4 use editor_state_module, only: editor_state_t, cursor_t, switch_to_tab_with_buffer, &
5 close_tab, create_tab, close_pane, split_pane_vertical, split_pane_horizontal, &
6 navigate_to_pane_left, navigate_to_pane_right, navigate_to_pane_up, navigate_to_pane_down, &
7 sync_editor_to_pane, tab_t
8 use text_buffer_module
9 use renderer_module, only: update_viewport, render_screen, tree_state
10 use yank_stack_module
11 use clipboard_module
12 use help_display_module, only: show_help
13 use goto_prompt_module, only: show_goto_prompt
14 use search_prompt_module, only: show_search_prompt, search_forward, search_backward, &
15 current_search_pattern
16 use replace_prompt_module, only: show_replace_prompt
17 use unified_search_module, only: show_unified_search_prompt
18 use undo_stack_module
19 use terminal_io_module, only: terminal_move_cursor, terminal_write, terminal_clear_screen
20 use bracket_matching_module, only: find_matching_bracket
21 use file_tree_module
22 use git_ops_module
23 use text_prompt_module, only: show_text_prompt, show_yes_no_prompt
24 use fortress_navigator_module, only: open_fortress_navigator
25 use binary_prompt_module, only: binary_file_prompt
26 use lsp_server_manager_module, only: request_completion, request_hover, request_definition, &
27 request_references, request_code_actions, request_document_symbols, &
28 request_signature_help, request_formatting, request_rename
29 use rename_prompt_module, only: show_rename_prompt
30 use completion_popup_module, only: show_completion_popup, hide_completion_popup, &
31 handle_completion_response, navigate_completion_up, &
32 navigate_completion_down, get_selected_completion, &
33 is_completion_visible
34 use hover_tooltip_module, only: show_hover_tooltip, hide_hover_tooltip, &
35 handle_hover_response, is_hover_visible
36 use diagnostics_panel_module, only: toggle_panel => toggle_diagnostics_panel, &
37 is_diagnostics_panel_visible, &
38 diagnostics_panel_handle_key
39 use references_panel_module, only: toggle_references_panel, &
40 is_references_panel_visible, &
41 references_panel_handle_key, &
42 get_selected_reference_location, &
43 hide_references_panel, &
44 show_references_panel, &
45 set_references, reference_location_t
46 use code_actions_menu_module, only: code_actions_menu_t, init_code_actions_menu, &
47 cleanup_code_actions_menu, show_code_actions_menu, &
48 hide_code_actions_menu, is_code_actions_menu_visible, &
49 code_actions_menu_handle_key, set_code_actions, &
50 clear_code_actions, get_selected_action
51 use symbols_panel_module, only: symbols_panel_t, document_symbol_t, &
52 toggle_symbols_panel, is_symbols_panel_visible, &
53 symbols_panel_handle_key, get_selected_symbol_location, &
54 hide_symbols_panel, show_symbols_panel, &
55 set_symbols, clear_symbols
56 use signature_tooltip_module, only: signature_tooltip_t, show_signature_tooltip, &
57 hide_signature_tooltip, is_signature_tooltip_visible, &
58 handle_signature_response
59 use jump_stack_module, only: push_jump_location, pop_jump_location, &
60 is_jump_stack_empty
61 implicit none
62 private
63
64 public :: handle_key_command, init_command_handler, cleanup_command_handler
65 public :: save_initial_state_for_undo
66 public :: search_pattern, match_case_sensitive ! Exposed for status bar hint
67
68 type(yank_stack_t) :: yank_stack
69 type(undo_stack_t) :: undo_stack
70 character(len=:), allocatable :: search_pattern ! For ctrl-d functionality
71 logical :: match_case_sensitive = .true. ! Case sensitivity for ctrl-d match mode
72 logical :: last_action_was_edit = .false.
73
74 ! Module-level storage for LSP callbacks
75 type(editor_state_t), allocatable, save :: saved_editor_for_callback
76
77
78 contains
79
80 subroutine init_command_handler()
81 call init_yank_stack(yank_stack)
82 call init_undo_stack(undo_stack)
83 last_action_was_edit = .false.
84 end subroutine init_command_handler
85
86 subroutine save_initial_state_for_undo(buffer, editor)
87 use undo_stack_module, only: save_initial_undo_state
88 type(buffer_t), intent(in) :: buffer
89 type(editor_state_t), intent(in) :: editor
90 call save_initial_undo_state(undo_stack, buffer, editor%cursors(editor%active_cursor))
91 end subroutine save_initial_state_for_undo
92
93 subroutine cleanup_command_handler()
94 call cleanup_yank_stack(yank_stack)
95 call cleanup_undo_stack(undo_stack)
96 if (allocated(search_pattern)) deallocate(search_pattern)
97 end subroutine cleanup_command_handler
98
99 subroutine save_undo_state(buffer, editor)
100 type(buffer_t), intent(in) :: buffer
101 type(editor_state_t), intent(in) :: editor
102
103 ! Save current state to undo stack
104 call push_undo_state(undo_stack, buffer, editor%cursors(editor%active_cursor))
105 end subroutine save_undo_state
106
107 subroutine handle_key_command(key_str, editor, buffer, should_quit)
108 character(len=*), intent(in) :: key_str
109 type(editor_state_t), intent(inout) :: editor
110 type(buffer_t), intent(inout) :: buffer
111 logical, intent(out) :: should_quit
112 integer :: line_count, i, j, insert_line, pane_idx
113 logical :: is_edit_action
114 type(cursor_t), allocatable :: new_cursors(:)
115 integer, allocatable :: original_lines(:)
116 character(len=:), allocatable :: line
117
118 should_quit = .false.
119 line_count = buffer_get_line_count(buffer)
120 is_edit_action = .false.
121
122 ! DEBUG: Log ALL keys to file only (not screen to avoid overwriting status messages)
123 block
124 integer :: debug_unit
125 open(newunit=debug_unit, file='/tmp/fac_keys.log', position='append', action='write')
126 write(debug_unit, '(A)') '[KEY] "' // trim(key_str) // '"'
127 close(debug_unit)
128 end block
129
130 ! Ignore empty key strings (from terminal position reports, etc)
131 if (len_trim(key_str) == 0 .and. key_str(1:1) /= ' ') then
132 return
133 end if
134
135 ! Reset search_pattern for any key except ctrl-d and alt-c
136 ! This ensures ctrl-d always starts fresh when not in an active match sequence
137 ! alt-c is preserved to allow toggling case sensitivity during match mode
138 if (trim(key_str) /= 'ctrl-d' .and. trim(key_str) /= 'alt-c' .and. allocated(search_pattern)) then
139 deallocate(search_pattern)
140 match_case_sensitive = .true. ! Reset to default
141 end if
142
143 ! Route input when in fuss mode (except ctrl-b/ctrl-shift-b/F-keys/Alt-keys/ctrl-q which work in both modes)
144 if (editor%fuss_mode_active .and. trim(key_str) /= 'ctrl-b' .and. &
145 trim(key_str) /= 'ctrl-shift-b' .and. trim(key_str) /= 'f2' .and. &
146 trim(key_str) /= 'f3' .and. trim(key_str) /= 'f4' .and. &
147 trim(key_str) /= 'f6' .and. trim(key_str) /= 'f8' .and. &
148 trim(key_str) /= 'f12' .and. trim(key_str) /= 'shift-f12' .and. &
149 trim(key_str) /= 'alt-g' .and. trim(key_str) /= 'alt-o' .and. &
150 trim(key_str) /= 'alt-p' .and. trim(key_str) /= 'alt-e' .and. &
151 trim(key_str) /= 'alt-r' .and. trim(key_str) /= 'ctrl-\\' .and. &
152 trim(key_str) /= 'ctrl-q') then
153 call handle_fuss_input(key_str, editor, buffer)
154 return
155 end if
156
157 select case(trim(key_str))
158 ! File operations
159 case('ctrl-q')
160 should_quit = .true.
161
162 case('ctrl-b', 'ctrl-shift-b', 'f3')
163 ! Toggle fuss mode (file tree)
164 ! ctrl-b: Original binding (conflicts with tmux prefix)
165 ! ctrl-shift-b: Alternative (tmux may still catch this)
166 ! f3: Tmux/terminal-safe alternative (recommended)
167 call toggle_fuss_mode(editor)
168
169 case('ctrl-o')
170 ! Open fortress navigator (file/directory picker)
171 call handle_fortress_navigator(editor, buffer)
172
173 case('esc')
174 ! If completion popup is visible, hide it
175 if (is_completion_visible(editor%completion_popup)) then
176 call hide_completion_popup(editor%completion_popup)
177 return
178 end if
179
180 ! If hover tooltip is visible, hide it
181 if (is_hover_visible(editor%hover_tooltip)) then
182 call hide_hover_tooltip(editor%hover_tooltip)
183 return
184 end if
185
186 ! If diagnostics panel is visible, hide it
187 if (is_diagnostics_panel_visible(editor%diagnostics_panel)) then
188 if (diagnostics_panel_handle_key(editor%diagnostics_panel, trim(key_str))) then
189 return
190 end if
191 end if
192
193 ! If references panel is visible, hide it
194 if (is_references_panel_visible(editor%references_panel)) then
195 if (references_panel_handle_key(editor%references_panel, trim(key_str))) then
196 return
197 end if
198 end if
199
200 ! If code actions menu is visible, hide it
201 if (is_code_actions_menu_visible(editor%code_actions_menu)) then
202 if (code_actions_menu_handle_key(editor%code_actions_menu, trim(key_str))) then
203 return
204 end if
205 end if
206
207 ! If symbols panel is visible, hide it
208 if (is_symbols_panel_visible(editor%symbols_panel)) then
209 if (symbols_panel_handle_key(editor%symbols_panel, trim(key_str))) then
210 return
211 end if
212 end if
213
214 ! ESC - Clear selections and return to single cursor mode
215 if (size(editor%cursors) > 1) then
216 ! Keep only the active cursor
217 allocate(new_cursors(1))
218 new_cursors(1) = editor%cursors(editor%active_cursor)
219 new_cursors(1)%has_selection = .false.
220 deallocate(editor%cursors)
221 editor%cursors = new_cursors
222 editor%active_cursor = 1
223 else
224 ! Single cursor - just clear selection
225 editor%cursors(editor%active_cursor)%has_selection = .false.
226 end if
227 call sync_editor_to_pane(editor)
228 call update_viewport(editor)
229
230 case('ctrl-?', 'ctrl-/')
231 ! Show help menu
232 ! Both ctrl-? and ctrl-/ supported for compatibility
233 ! ctrl-/ is more reliable across terminals
234 call show_help(editor)
235 ! Screen will be redrawn automatically by main loop
236
237 case('ctrl-g')
238 ! Go to line:column
239 call show_goto_prompt(editor, buffer)
240 call sync_editor_to_pane(editor)
241 call update_viewport(editor)
242
243 case('ctrl-l')
244 ! Clear and redraw screen
245 call terminal_clear_screen()
246 ! Screen will be redrawn automatically by main loop
247
248 ! Undo/Redo
249 case('ctrl-z')
250 ! Undo
251 if (can_undo(undo_stack)) then
252 call perform_undo(undo_stack, buffer, editor%cursors(editor%active_cursor))
253 ! Sync even for single cursor case since undo changes cursor position
254 call sync_editor_to_pane(editor)
255 ! If we have multiple cursors, reset to single cursor mode
256 ! (Undo only tracks one cursor's state)
257 if (size(editor%cursors) > 1) then
258 allocate(new_cursors(1))
259 new_cursors(1) = editor%cursors(editor%active_cursor)
260 ! Clamp cursor to actual line length after undo
261 line = buffer_get_line(buffer, new_cursors(1)%line)
262 if (new_cursors(1)%column > len(line) + 1) then
263 new_cursors(1)%column = len(line) + 1
264 end if
265 new_cursors(1)%desired_column = new_cursors(1)%column
266 if (allocated(line)) deallocate(line)
267 deallocate(editor%cursors)
268 editor%cursors = new_cursors
269 editor%active_cursor = 1
270 end if
271 call sync_editor_to_pane(editor)
272 call update_viewport(editor)
273 end if
274
275 case('ctrl-shift-z', 'ctrl-]')
276 ! Redo
277 ! ctrl-shift-z: Standard redo (WezTerm may intercept - disable in config)
278 ! ctrl-]: Alternative redo binding
279 if (can_redo(undo_stack)) then
280 call perform_redo(undo_stack, buffer, editor%cursors(editor%active_cursor))
281 ! Sync even for single cursor case since redo changes cursor position
282 call sync_editor_to_pane(editor)
283 ! If we have multiple cursors, reset to single cursor mode
284 ! (Undo only tracks one cursor's state)
285 if (size(editor%cursors) > 1) then
286 allocate(new_cursors(1))
287 new_cursors(1) = editor%cursors(editor%active_cursor)
288 deallocate(editor%cursors)
289 editor%cursors = new_cursors
290 editor%active_cursor = 1
291 end if
292 call sync_editor_to_pane(editor)
293 call update_viewport(editor)
294 end if
295
296 case('ctrl-y')
297 ! Yank (paste from yank stack)
298 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
299 if (size(editor%cursors) > 1) then
300 ! Apply yank to all cursors
301 do i = 1, size(editor%cursors)
302 call yank_text(editor%cursors(i), buffer)
303 end do
304 else
305 call yank_text(editor%cursors(editor%active_cursor), buffer)
306 end if
307 call sync_editor_to_pane(editor)
308 is_edit_action = .true.
309
310 ! Navigation
311 case('up')
312 ! If completion popup is visible, navigate it instead
313 if (is_completion_visible(editor%completion_popup)) then
314 call navigate_completion_up(editor%completion_popup)
315 return
316 end if
317
318 ! If diagnostics panel is visible, navigate it
319 if (is_diagnostics_panel_visible(editor%diagnostics_panel)) then
320 if (diagnostics_panel_handle_key(editor%diagnostics_panel, trim(key_str))) then
321 return
322 end if
323 end if
324
325 ! If references panel is visible, navigate it
326 if (is_references_panel_visible(editor%references_panel)) then
327 if (references_panel_handle_key(editor%references_panel, trim(key_str))) then
328 return
329 end if
330 end if
331
332 ! If code actions menu is visible, navigate it
333 if (is_code_actions_menu_visible(editor%code_actions_menu)) then
334 if (code_actions_menu_handle_key(editor%code_actions_menu, trim(key_str))) then
335 return
336 end if
337 end if
338
339 ! If symbols panel is visible, navigate it
340 if (is_symbols_panel_visible(editor%symbols_panel)) then
341 if (symbols_panel_handle_key(editor%symbols_panel, trim(key_str))) then
342 return
343 end if
344 end if
345
346 if (size(editor%cursors) > 1) then
347 ! Move all cursors
348 do i = 1, size(editor%cursors)
349 call move_cursor_up(editor%cursors(i), buffer)
350 end do
351 ! Remove duplicate cursors that ended up at same position
352 call deduplicate_cursors(editor)
353 else
354 call move_cursor_up(editor%cursors(editor%active_cursor), buffer)
355 end if
356 call sync_editor_to_pane(editor)
357 call update_viewport(editor)
358
359 case('down')
360 ! If completion popup is visible, navigate it instead
361 if (is_completion_visible(editor%completion_popup)) then
362 call navigate_completion_down(editor%completion_popup)
363 return
364 end if
365
366 ! If diagnostics panel is visible, navigate it
367 if (is_diagnostics_panel_visible(editor%diagnostics_panel)) then
368 if (diagnostics_panel_handle_key(editor%diagnostics_panel, trim(key_str))) then
369 return
370 end if
371 end if
372
373 ! If references panel is visible, navigate it
374 if (is_references_panel_visible(editor%references_panel)) then
375 if (references_panel_handle_key(editor%references_panel, trim(key_str))) then
376 return
377 end if
378 end if
379
380 ! If code actions menu is visible, navigate it
381 if (is_code_actions_menu_visible(editor%code_actions_menu)) then
382 if (code_actions_menu_handle_key(editor%code_actions_menu, trim(key_str))) then
383 return
384 end if
385 end if
386
387 ! If symbols panel is visible, navigate it
388 if (is_symbols_panel_visible(editor%symbols_panel)) then
389 if (symbols_panel_handle_key(editor%symbols_panel, trim(key_str))) then
390 return
391 end if
392 end if
393
394 if (size(editor%cursors) > 1) then
395 ! Move all cursors
396 do i = 1, size(editor%cursors)
397 call move_cursor_down(editor%cursors(i), buffer, line_count)
398 end do
399 ! Remove duplicate cursors that ended up at same position
400 call deduplicate_cursors(editor)
401 else
402 call move_cursor_down(editor%cursors(editor%active_cursor), buffer, line_count)
403 end if
404 call sync_editor_to_pane(editor)
405 call update_viewport(editor)
406
407 case('left')
408 ! Hide hover tooltip on movement
409 if (is_hover_visible(editor%hover_tooltip)) then
410 call hide_hover_tooltip(editor%hover_tooltip)
411 end if
412
413 if (size(editor%cursors) > 1) then
414 ! Move all cursors
415 do i = 1, size(editor%cursors)
416 call move_cursor_left(editor%cursors(i), buffer)
417 end do
418 ! Remove duplicate cursors that ended up at same position
419 call deduplicate_cursors(editor)
420 else
421 call move_cursor_left(editor%cursors(editor%active_cursor), buffer)
422 end if
423 call sync_editor_to_pane(editor)
424 call update_viewport(editor)
425
426 case('right')
427 ! Hide hover tooltip on movement
428 if (is_hover_visible(editor%hover_tooltip)) then
429 call hide_hover_tooltip(editor%hover_tooltip)
430 end if
431
432 if (size(editor%cursors) > 1) then
433 ! Move all cursors
434 do i = 1, size(editor%cursors)
435 call move_cursor_right(editor%cursors(i), buffer)
436 end do
437 ! Remove duplicate cursors that ended up at same position
438 call deduplicate_cursors(editor)
439 else
440 call move_cursor_right(editor%cursors(editor%active_cursor), buffer)
441 end if
442 call sync_editor_to_pane(editor)
443 call update_viewport(editor)
444
445 ! Selection with shift+motion
446 case('shift-up')
447 call extend_selection_up(editor%cursors(editor%active_cursor), buffer)
448 call sync_editor_to_pane(editor)
449 call update_viewport(editor)
450
451 case('shift-down')
452 call extend_selection_down(editor%cursors(editor%active_cursor), buffer, line_count)
453 call sync_editor_to_pane(editor)
454 call update_viewport(editor)
455
456 case('shift-left')
457 call extend_selection_left(editor%cursors(editor%active_cursor), buffer)
458 call sync_editor_to_pane(editor)
459 call update_viewport(editor)
460
461 case('shift-right')
462 call extend_selection_right(editor%cursors(editor%active_cursor), buffer)
463 call sync_editor_to_pane(editor)
464 call update_viewport(editor)
465
466 case('home', 'ctrl-a')
467 if (size(editor%cursors) > 1) then
468 ! Move all cursors
469 do i = 1, size(editor%cursors)
470 call move_cursor_smart_home(editor%cursors(i), buffer)
471 end do
472 else
473 call move_cursor_smart_home(editor%cursors(editor%active_cursor), buffer)
474 end if
475 call sync_editor_to_pane(editor)
476 call update_viewport(editor)
477
478 case('end', 'ctrl-e')
479 if (size(editor%cursors) > 1) then
480 ! Move all cursors
481 do i = 1, size(editor%cursors)
482 call move_cursor_end(editor%cursors(i), buffer)
483 end do
484 else
485 call move_cursor_end(editor%cursors(editor%active_cursor), buffer)
486 end if
487 call sync_editor_to_pane(editor)
488 call update_viewport(editor)
489
490 case('shift-home', 'ctrl-shift-a')
491 call extend_selection_home(editor%cursors(editor%active_cursor))
492 call sync_editor_to_pane(editor)
493 call update_viewport(editor)
494
495 case('shift-end', 'ctrl-shift-e')
496 call extend_selection_end(editor%cursors(editor%active_cursor), buffer)
497 call sync_editor_to_pane(editor)
498 call update_viewport(editor)
499
500 case('pageup')
501 if (size(editor%cursors) > 1) then
502 ! Move all cursors
503 do i = 1, size(editor%cursors)
504 call move_cursor_page_up(editor%cursors(i), editor)
505 end do
506 else
507 call move_cursor_page_up(editor%cursors(editor%active_cursor), editor)
508 end if
509 call sync_editor_to_pane(editor)
510 call update_viewport(editor)
511
512 case('pagedown')
513 if (size(editor%cursors) > 1) then
514 ! Move all cursors
515 do i = 1, size(editor%cursors)
516 call move_cursor_page_down(editor%cursors(i), editor, line_count)
517 end do
518 else
519 call move_cursor_page_down(editor%cursors(editor%active_cursor), editor, line_count)
520 end if
521 call sync_editor_to_pane(editor)
522 call update_viewport(editor)
523
524 case('mouse-scroll-up')
525 ! Scroll viewport up by 3 lines (don't move cursor)
526 editor%viewport_line = max(1, editor%viewport_line - 3)
527 call sync_editor_to_pane(editor)
528
529 case('mouse-scroll-down')
530 ! Scroll viewport down by 3 lines (don't move cursor)
531 editor%viewport_line = min(max(1, line_count - editor%screen_rows + 2), &
532 editor%viewport_line + 3)
533 call sync_editor_to_pane(editor)
534
535 case('ctrl-home')
536 ! Jump to beginning of file
537 editor%cursors(editor%active_cursor)%line = 1
538 editor%cursors(editor%active_cursor)%column = 1
539 editor%cursors(editor%active_cursor)%desired_column = 1
540 editor%cursors(editor%active_cursor)%has_selection = .false.
541 call sync_editor_to_pane(editor)
542 call update_viewport(editor)
543
544 case('ctrl-end')
545 ! Jump to end of file
546 line_count = buffer_get_line_count(buffer)
547 editor%cursors(editor%active_cursor)%line = line_count
548 call move_cursor_end(editor%cursors(editor%active_cursor), buffer)
549 call sync_editor_to_pane(editor)
550 call update_viewport(editor)
551
552 case('shift-pageup')
553 call extend_selection_page_up(editor%cursors(editor%active_cursor), editor)
554 call sync_editor_to_pane(editor)
555 call update_viewport(editor)
556
557 case('shift-pagedown')
558 call extend_selection_page_down(editor%cursors(editor%active_cursor), editor, line_count)
559 call sync_editor_to_pane(editor)
560 call update_viewport(editor)
561
562 case('alt-left')
563 if (size(editor%cursors) > 1) then
564 ! Move all cursors
565 do i = 1, size(editor%cursors)
566 call move_cursor_word_left(editor%cursors(i), buffer)
567 end do
568 else
569 call move_cursor_word_left(editor%cursors(editor%active_cursor), buffer)
570 end if
571 call sync_editor_to_pane(editor)
572 call update_viewport(editor)
573
574 case('alt-right')
575 if (size(editor%cursors) > 1) then
576 ! Move all cursors
577 do i = 1, size(editor%cursors)
578 call move_cursor_word_right(editor%cursors(i), buffer)
579 end do
580 else
581 call move_cursor_word_right(editor%cursors(editor%active_cursor), buffer)
582 end if
583 call sync_editor_to_pane(editor)
584 call update_viewport(editor)
585
586 case('alt-shift-left')
587 call extend_selection_word_left(editor%cursors(editor%active_cursor), buffer)
588 call sync_editor_to_pane(editor)
589 call update_viewport(editor)
590
591 case('alt-shift-right')
592 call extend_selection_word_right(editor%cursors(editor%active_cursor), buffer)
593 call sync_editor_to_pane(editor)
594 call update_viewport(editor)
595
596 case('alt-up')
597 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
598 call move_line_up(editor%cursors(editor%active_cursor), buffer)
599 call sync_editor_to_pane(editor)
600 call update_viewport(editor)
601 is_edit_action = .true.
602
603 case('alt-down')
604 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
605 call move_line_down(editor%cursors(editor%active_cursor), buffer)
606 call sync_editor_to_pane(editor)
607 call update_viewport(editor)
608 is_edit_action = .true.
609
610 case('alt-shift-up')
611 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
612 call duplicate_line_up(editor%cursors(editor%active_cursor), buffer)
613 call sync_editor_to_pane(editor)
614 call update_viewport(editor)
615 is_edit_action = .true.
616
617 case('alt-shift-down')
618 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
619 call duplicate_line_down(editor%cursors(editor%active_cursor), buffer)
620 call sync_editor_to_pane(editor)
621 call update_viewport(editor)
622 is_edit_action = .true.
623
624 ! Tab navigation
625 case('alt-1', 'ctrl-1')
626 ! Switch to tab 1 (alt-1 or ctrl-1)
627 if (size(editor%tabs) >= 1) call switch_to_tab_with_buffer(editor, 1, buffer)
628 case('alt-2', 'ctrl-2')
629 ! Switch to tab 2
630 if (size(editor%tabs) >= 2) call switch_to_tab_with_buffer(editor, 2, buffer)
631 case('alt-3', 'ctrl-3')
632 ! Switch to tab 3
633 if (size(editor%tabs) >= 3) call switch_to_tab_with_buffer(editor, 3, buffer)
634 case('alt-4', 'ctrl-4')
635 ! Switch to tab 4
636 if (size(editor%tabs) >= 4) call switch_to_tab_with_buffer(editor, 4, buffer)
637 case('alt-5', 'ctrl-5')
638 ! Switch to tab 5
639 if (size(editor%tabs) >= 5) call switch_to_tab_with_buffer(editor, 5, buffer)
640 case('alt-6', 'ctrl-6')
641 ! Switch to tab 6
642 if (size(editor%tabs) >= 6) call switch_to_tab_with_buffer(editor, 6, buffer)
643 case('alt-7', 'ctrl-7')
644 ! Switch to tab 7
645 if (size(editor%tabs) >= 7) call switch_to_tab_with_buffer(editor, 7, buffer)
646 case('alt-8', 'ctrl-8')
647 ! Switch to tab 8
648 if (size(editor%tabs) >= 8) call switch_to_tab_with_buffer(editor, 8, buffer)
649 case('alt-9', 'ctrl-9')
650 ! Switch to tab 9
651 if (size(editor%tabs) >= 9) call switch_to_tab_with_buffer(editor, 9, buffer)
652 case('alt-0', 'ctrl-0')
653 ! Switch to tab 10
654 if (size(editor%tabs) >= 10) call switch_to_tab_with_buffer(editor, 10, buffer)
655
656 case('ctrl-alt-left', 'ctrl-pageup')
657 ! Previous tab (ctrl-alt-left or ctrl-pageup)
658 if (size(editor%tabs) > 0) then
659 if (editor%active_tab_index > 1) then
660 call switch_to_tab_with_buffer(editor, editor%active_tab_index - 1, buffer)
661 else
662 call switch_to_tab_with_buffer(editor, size(editor%tabs), buffer) ! Wrap to last tab
663 end if
664 end if
665
666 case('ctrl-alt-right', 'ctrl-pagedown')
667 ! Next tab (ctrl-alt-right or ctrl-pagedown)
668 if (size(editor%tabs) > 0) then
669 if (editor%active_tab_index < size(editor%tabs)) then
670 call switch_to_tab_with_buffer(editor, editor%active_tab_index + 1, buffer)
671 else
672 call switch_to_tab_with_buffer(editor, 1, buffer) ! Wrap to first tab
673 end if
674 end if
675
676 ! Text modification
677 case('backspace')
678 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
679 if (size(editor%cursors) > 1) then
680 ! Apply to all cursors
681 do i = 1, size(editor%cursors)
682 call handle_backspace(editor%cursors(i), buffer)
683 end do
684 else
685 call handle_backspace(editor%cursors(editor%active_cursor), buffer)
686 end if
687 call sync_editor_to_pane(editor)
688 is_edit_action = .true.
689
690 case('delete')
691 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
692 if (size(editor%cursors) > 1) then
693 ! Apply to all cursors
694 do i = 1, size(editor%cursors)
695 call handle_delete(editor%cursors(i), buffer)
696 end do
697 else
698 call handle_delete(editor%cursors(editor%active_cursor), buffer)
699 end if
700 call sync_editor_to_pane(editor)
701 is_edit_action = .true.
702
703 case('enter')
704 ! If code actions menu is visible, apply selected action
705 if (is_code_actions_menu_visible(editor%code_actions_menu)) then
706 block
707 character(len=:), allocatable :: action_json
708
709 if (get_selected_action(editor%code_actions_menu, action_json)) then
710 ! TODO: Apply the selected code action
711 ! This will involve sending workspace/executeCommand or workspace/applyEdit
712 call terminal_move_cursor(editor%screen_rows, 1)
713 call terminal_write('Applying code action... ')
714
715 ! Hide menu after selection
716 call hide_code_actions_menu(editor%code_actions_menu)
717 end if
718 end block
719 return
720 end if
721
722 ! If symbols panel is visible, jump to selected symbol
723 if (is_symbols_panel_visible(editor%symbols_panel)) then
724 block
725 integer(int32) :: line, col
726
727 if (get_selected_symbol_location(editor%symbols_panel, line, col)) then
728 ! Jump to the symbol location
729 editor%cursors(editor%active_cursor)%line = line
730 editor%cursors(editor%active_cursor)%column = col
731 call update_viewport(editor)
732
733 ! Hide panel after jump
734 call hide_symbols_panel(editor%symbols_panel)
735 end if
736 end block
737 return
738 end if
739
740 ! If references panel is visible, jump to selected reference
741 if (is_references_panel_visible(editor%references_panel)) then
742 block
743 character(len=:), allocatable :: uri
744 integer(int32) :: line, col
745
746 if (get_selected_reference_location(editor%references_panel, uri, line, col)) then
747 ! Convert URI to file path
748 if (uri(1:7) == "file://") then
749 ! Jump to the reference location
750 ! TODO: Handle cross-file navigation
751 editor%cursors(editor%active_cursor)%line = line
752 editor%cursors(editor%active_cursor)%column = col
753 call update_viewport(editor)
754 call hide_references_panel(editor%references_panel)
755 end if
756 end if
757 end block
758 return
759 end if
760
761 ! If completion popup is visible, insert selected completion
762 if (is_completion_visible(editor%completion_popup)) then
763 block
764 character(len=:), allocatable :: completion_text
765 integer :: text_i
766 completion_text = get_selected_completion(editor%completion_popup)
767 if (len(completion_text) > 0) then
768 ! Insert the completion text at cursor
769 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
770 do text_i = 1, len(completion_text)
771 call buffer_insert_char(buffer, editor%cursors(editor%active_cursor), &
772 completion_text(text_i:text_i))
773 editor%cursors(editor%active_cursor)%column = &
774 editor%cursors(editor%active_cursor)%column + 1
775 end do
776 end if
777 call hide_completion_popup(editor%completion_popup)
778 end block
779 is_edit_action = .true.
780 return
781 end if
782
783 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
784 if (size(editor%cursors) > 1) then
785 ! Sort cursors and apply from bottom to top to avoid position shifts
786 call sort_cursors_by_position(editor)
787 ! Save original line numbers before any insertions
788 allocate(original_lines(size(editor%cursors)))
789 do i = 1, size(editor%cursors)
790 original_lines(i) = editor%cursors(i)%line
791 end do
792
793 ! Process in reverse order (bottom to top)
794 do i = size(editor%cursors), 1, -1
795 ! Save the line where we're inserting
796 insert_line = original_lines(i)
797
798 call handle_enter(editor%cursors(i), buffer)
799
800 ! Adjust ALL other cursors that were BELOW where we inserted
801 ! (cursors at same line are handled by their own handle_enter)
802 do j = 1, size(editor%cursors)
803 if (j /= i .and. original_lines(j) > insert_line) then
804 ! This cursor was below where we inserted, shift it down
805 editor%cursors(j)%line = editor%cursors(j)%line + 1
806 end if
807 end do
808 end do
809 deallocate(original_lines)
810 else
811 call handle_enter(editor%cursors(editor%active_cursor), buffer)
812 end if
813 call sync_editor_to_pane(editor)
814 is_edit_action = .true.
815
816 case('tab')
817 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
818 if (size(editor%cursors) > 1) then
819 ! Apply to all cursors
820 do i = 1, size(editor%cursors)
821 if (editor%cursors(i)%has_selection) then
822 call indent_selection(editor%cursors(i), buffer)
823 else
824 call handle_tab(editor%cursors(i), buffer)
825 end if
826 end do
827 else
828 if (editor%cursors(editor%active_cursor)%has_selection) then
829 call indent_selection(editor%cursors(editor%active_cursor), buffer)
830 else
831 call handle_tab(editor%cursors(editor%active_cursor), buffer)
832 end if
833 end if
834 is_edit_action = .true.
835
836 case('shift-tab')
837 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
838 if (editor%cursors(editor%active_cursor)%has_selection) then
839 call dedent_selection(editor%cursors(editor%active_cursor), buffer)
840 else
841 call dedent_current_line(editor%cursors(editor%active_cursor), buffer)
842 end if
843 is_edit_action = .true.
844
845 ! Editing keybinds
846 case('ctrl-k')
847 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
848 if (size(editor%cursors) > 1) then
849 ! Apply to all cursors
850 do i = 1, size(editor%cursors)
851 call kill_line_forward(editor%cursors(i), buffer)
852 end do
853 else
854 call kill_line_forward(editor%cursors(editor%active_cursor), buffer)
855 end if
856 call sync_editor_to_pane(editor)
857 is_edit_action = .true.
858
859 case('ctrl-u')
860 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
861 if (size(editor%cursors) > 1) then
862 ! Apply to all cursors
863 do i = 1, size(editor%cursors)
864 call kill_line_backward(editor%cursors(i), buffer)
865 end do
866 else
867 call kill_line_backward(editor%cursors(editor%active_cursor), buffer)
868 end if
869 call sync_editor_to_pane(editor)
870 is_edit_action = .true.
871
872 case('alt-v')
873 ! Split pane vertically
874 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0) then
875 ! Sync current buffer to active pane before splitting
876 call sync_editor_to_pane(editor)
877 if (allocated(editor%tabs(editor%active_tab_index)%panes)) then
878 pane_idx = editor%tabs(editor%active_tab_index)%active_pane_index
879 if (pane_idx > 0 .and. pane_idx <= size(editor%tabs(editor%active_tab_index)%panes)) then
880 call copy_buffer(editor%tabs(editor%active_tab_index)%panes(pane_idx)%buffer, buffer)
881 end if
882 end if
883 call split_pane_vertical(editor)
884 end if
885
886 case('alt-s')
887 ! Split pane horizontally
888 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0) then
889 ! Sync current buffer to active pane before splitting
890 call sync_editor_to_pane(editor)
891 if (allocated(editor%tabs(editor%active_tab_index)%panes)) then
892 pane_idx = editor%tabs(editor%active_tab_index)%active_pane_index
893 if (pane_idx > 0 .and. pane_idx <= size(editor%tabs(editor%active_tab_index)%panes)) then
894 call copy_buffer(editor%tabs(editor%active_tab_index)%panes(pane_idx)%buffer, buffer)
895 end if
896 end if
897 call split_pane_horizontal(editor)
898 end if
899
900 case('alt-q')
901 ! Close current pane (creates UNTITLED.txt if last pane of last tab)
902 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0) then
903 call close_pane(editor)
904
905 ! Always copy the buffer (either new tab or UNTITLED.txt)
906 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0) then
907 call copy_buffer(buffer, editor%tabs(editor%active_tab_index)%buffer)
908 editor%modified = editor%tabs(editor%active_tab_index)%modified
909 if (allocated(editor%filename)) deallocate(editor%filename)
910 allocate(character(len=len(editor%tabs(editor%active_tab_index)%filename)) :: editor%filename)
911 editor%filename = editor%tabs(editor%active_tab_index)%filename
912 ! Should not happen with new logic
913 else
914 editor%fuss_mode_active = .true.
915 if (allocated(editor%workspace_path)) then
916 call init_tree_state(tree_state, editor%workspace_path)
917 end if
918 end if
919 end if
920
921 case('ctrl-w')
922 ! Close current tab (prompts to save if modified)
923 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0) then
924 ! Check if tab is modified - prompt before closing
925 if (editor%tabs(editor%active_tab_index)%modified) then
926 call prompt_save_before_close_tab(editor, buffer)
927 else
928 call close_tab_without_prompt(editor, buffer)
929 end if
930 end if
931
932 case('ctrl-shift-left', 'alt-h')
933 ! Navigate to pane on the left (Vim-style hjkl with alt)
934 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0) then
935 call navigate_to_pane_left(editor)
936 end if
937
938 case('ctrl-shift-right', 'alt-l')
939 ! Navigate to pane on the right
940 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0) then
941 call navigate_to_pane_right(editor)
942 end if
943
944 case('ctrl-shift-up', 'alt-k')
945 ! Navigate to pane above
946 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0) then
947 call navigate_to_pane_up(editor)
948 end if
949
950 case('ctrl-shift-down', 'alt-j')
951 ! Navigate to pane below
952 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0) then
953 call navigate_to_pane_down(editor)
954 end if
955
956 case('alt-d', 'alt-delete')
957 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
958 if (size(editor%cursors) > 1) then
959 ! Apply to all cursors
960 do i = 1, size(editor%cursors)
961 call delete_word_forward(editor%cursors(i), buffer)
962 end do
963 else
964 call delete_word_forward(editor%cursors(editor%active_cursor), buffer)
965 end if
966 call sync_editor_to_pane(editor)
967 call update_viewport(editor)
968 is_edit_action = .true.
969
970 case('alt-backspace')
971 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
972 if (size(editor%cursors) > 1) then
973 ! Apply to all cursors
974 do i = 1, size(editor%cursors)
975 call delete_word_backward(editor%cursors(i), buffer)
976 end do
977 else
978 call delete_word_backward(editor%cursors(editor%active_cursor), buffer)
979 end if
980 call sync_editor_to_pane(editor)
981 call update_viewport(editor)
982 is_edit_action = .true.
983
984 case('ctrl-t')
985 ! Create new empty tab with unique name
986 block
987 integer :: untitled_counter, i, name_len, max_untitled, dash_pos, num_start
988 character(len=32) :: untitled_name, num_str
989 logical :: name_exists
990 integer :: ios
991
992 ! Scan existing tabs to find highest untitled number
993 max_untitled = 0
994 if (allocated(editor%tabs)) then
995 do i = 1, size(editor%tabs)
996 if (allocated(editor%tabs(i)%filename)) then
997 ! Check if it's an untitled tab
998 if (index(editor%tabs(i)%filename, '[Untitled') == 1) then
999 ! Check for plain [Untitled]
1000 if (trim(editor%tabs(i)%filename) == '[Untitled]') then
1001 max_untitled = max(max_untitled, 1)
1002 else
1003 ! Check for [Untitled-N]
1004 dash_pos = index(editor%tabs(i)%filename, '-')
1005 if (dash_pos > 0) then
1006 num_start = dash_pos + 1
1007 num_str = editor%tabs(i)%filename(num_start:len_trim(editor%tabs(i)%filename)-1)
1008 read(num_str, *, iostat=ios) untitled_counter
1009 if (ios == 0) then
1010 max_untitled = max(max_untitled, untitled_counter)
1011 end if
1012 end if
1013 end if
1014 end if
1015 end if
1016 end do
1017 end if
1018
1019 ! Start checking from max_untitled + 1, but check backwards too in case of gaps
1020 untitled_counter = max(1, max_untitled)
1021 do
1022 if (untitled_counter == 1) then
1023 write(untitled_name, '(A)') '[Untitled]'
1024 else
1025 write(untitled_name, '(A,I0,A)') '[Untitled-', untitled_counter, ']'
1026 end if
1027
1028 ! Check if this name already exists
1029 name_exists = .false.
1030 if (allocated(editor%tabs)) then
1031 do i = 1, size(editor%tabs)
1032 if (allocated(editor%tabs(i)%filename)) then
1033 if (trim(editor%tabs(i)%filename) == trim(untitled_name)) then
1034 name_exists = .true.
1035 exit
1036 end if
1037 end if
1038 end do
1039 end if
1040
1041 if (.not. name_exists) exit
1042 untitled_counter = untitled_counter + 1
1043 end do
1044
1045 call create_tab(editor, trim(untitled_name))
1046
1047 ! Switch to the new tab (it's already active after create_tab)
1048 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
1049 ! Clear the display buffer and copy from the new tab's pane (which is empty)
1050 call cleanup_buffer(buffer)
1051 call init_buffer(buffer)
1052
1053 ! Copy empty buffer to the new tab's pane
1054 if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. &
1055 size(editor%tabs(editor%active_tab_index)%panes) > 0) then
1056 call copy_buffer(editor%tabs(editor%active_tab_index)%panes(1)%buffer, buffer)
1057 call copy_buffer(editor%tabs(editor%active_tab_index)%buffer, buffer)
1058 end if
1059
1060 ! Update editor state with the new tab
1061 name_len = len_trim(untitled_name)
1062 if (allocated(editor%filename)) deallocate(editor%filename)
1063 allocate(character(len=name_len) :: editor%filename)
1064 editor%filename = trim(untitled_name)
1065
1066 ! Reset cursor to top
1067 editor%cursors(editor%active_cursor)%line = 1
1068 editor%cursors(editor%active_cursor)%column = 1
1069 editor%cursors(editor%active_cursor)%desired_column = 1
1070 editor%viewport_line = 1
1071 editor%viewport_column = 1
1072 editor%modified = .false.
1073 end if
1074 end block
1075
1076 case('ctrl-j')
1077 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
1078 if (size(editor%cursors) > 1) then
1079 ! Apply to all cursors
1080 do i = 1, size(editor%cursors)
1081 call join_lines(editor%cursors(i), buffer)
1082 end do
1083 else
1084 call join_lines(editor%cursors(editor%active_cursor), buffer)
1085 end if
1086 is_edit_action = .true.
1087
1088 case('ctrl-x')
1089 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
1090 if (size(editor%cursors) > 1) then
1091 ! Apply to all cursors in reverse order (bottom to top)
1092 do i = size(editor%cursors), 1, -1
1093 call cut_selection_or_line(editor%cursors(i), buffer)
1094 end do
1095 else
1096 call cut_selection_or_line(editor%cursors(editor%active_cursor), buffer)
1097 end if
1098 call sync_editor_to_pane(editor)
1099 is_edit_action = .true.
1100
1101 case('ctrl-c')
1102 ! Copy only needs active cursor (copies to shared clipboard)
1103 call copy_selection_or_line(editor%cursors(editor%active_cursor), buffer)
1104
1105 case('ctrl-v')
1106 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
1107 if (size(editor%cursors) > 1) then
1108 ! Apply to all cursors in reverse order (bottom to top)
1109 ! This prevents position shifts as we insert text
1110 do i = size(editor%cursors), 1, -1
1111 call paste_clipboard(editor%cursors(i), buffer)
1112 end do
1113 else
1114 call paste_clipboard(editor%cursors(editor%active_cursor), buffer)
1115 end if
1116 call sync_editor_to_pane(editor)
1117 is_edit_action = .true.
1118
1119 case('ctrl-s')
1120 ! Save the active pane/tab's buffer
1121 ! (All panes in a tab share the same buffer, so saving saves the entire tab)
1122 call save_file(editor, buffer)
1123
1124 ! LSP features
1125 case('ctrl-space')
1126 ! Trigger code completion
1127 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
1128 if (editor%tabs(editor%active_tab_index)%lsp_server_index > 0) then
1129 ! Request completion at current cursor position
1130 ! LSP uses 0-based positions
1131 block
1132 integer :: request_id, lsp_line, lsp_char
1133 lsp_line = editor%cursors(editor%active_cursor)%line - 1
1134 lsp_char = editor%cursors(editor%active_cursor)%column - 1
1135
1136 request_id = request_completion(editor%lsp_manager, &
1137 editor%tabs(editor%active_tab_index)%lsp_server_index, &
1138 editor%tabs(editor%active_tab_index)%filename, &
1139 lsp_line, lsp_char)
1140
1141 if (request_id > 0) then
1142 ! Show popup at cursor position (will populate when response arrives)
1143 call show_completion_popup(editor%completion_popup, &
1144 editor%cursors(editor%active_cursor)%line - editor%viewport_line + 2, &
1145 editor%cursors(editor%active_cursor)%column - editor%viewport_column + 1)
1146 end if
1147 end block
1148 end if
1149 end if
1150
1151 case('ctrl-h')
1152 ! Trigger hover information
1153 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
1154 if (editor%tabs(editor%active_tab_index)%lsp_server_index > 0) then
1155 ! Request hover at current cursor position
1156 block
1157 integer :: request_id, lsp_line, lsp_char
1158 lsp_line = editor%cursors(editor%active_cursor)%line - 1
1159 lsp_char = editor%cursors(editor%active_cursor)%column - 1
1160
1161 request_id = request_hover(editor%lsp_manager, &
1162 editor%tabs(editor%active_tab_index)%lsp_server_index, &
1163 editor%tabs(editor%active_tab_index)%filename, &
1164 lsp_line, lsp_char)
1165
1166 if (request_id > 0) then
1167 ! Show tooltip at cursor position (will populate when response arrives)
1168 call show_hover_tooltip(editor%hover_tooltip, &
1169 editor%cursors(editor%active_cursor)%line - editor%viewport_line + 2, &
1170 editor%cursors(editor%active_cursor)%column - editor%viewport_column + 1)
1171 end if
1172 end block
1173 end if
1174 end if
1175
1176 case('ctrl-.')
1177 ! Trigger code actions
1178 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
1179 if (editor%tabs(editor%active_tab_index)%lsp_server_index > 0) then
1180 ! Request code actions at current cursor position
1181 block
1182 integer :: request_id, lsp_line, lsp_char
1183 lsp_line = editor%cursors(editor%active_cursor)%line - 1
1184 lsp_char = editor%cursors(editor%active_cursor)%column - 1
1185
1186 ! Save editor state for callback
1187 if (allocated(saved_editor_for_callback)) then
1188 deallocate(saved_editor_for_callback)
1189 end if
1190 allocate(saved_editor_for_callback)
1191 saved_editor_for_callback = editor
1192
1193 request_id = request_code_actions(editor%lsp_manager, &
1194 editor%tabs(editor%active_tab_index)%lsp_server_index, &
1195 editor%tabs(editor%active_tab_index)%filename, &
1196 lsp_line, lsp_char, &
1197 lsp_line, lsp_char, &
1198 handle_code_actions_response_wrapper)
1199
1200 if (request_id > 0) then
1201 ! Show menu placeholder (will populate when response arrives)
1202 call show_code_actions_menu(editor%code_actions_menu, &
1203 editor%cursors(editor%active_cursor)%line - editor%viewport_line + 2, &
1204 editor%cursors(editor%active_cursor)%column - editor%viewport_column + 1)
1205 end if
1206 end block
1207 end if
1208 end if
1209
1210 case('f12', 'ctrl-\\', 'alt-g')
1211 ! Go to definition (F12, Ctrl+\, or Alt+G)
1212 block
1213 integer :: debug_unit
1214 open(newunit=debug_unit, file='/tmp/fac_keys.log', position='append', action='write')
1215 write(debug_unit, '(A)') '>>> INSIDE F12/ALT-G HANDLER <<<'
1216 write(debug_unit, '(A,I0)') 'active_tab_index = ', editor%active_tab_index
1217 write(debug_unit, '(A,I0)') 'size(tabs) = ', size(editor%tabs)
1218 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
1219 write(debug_unit, '(A,I0)') 'lsp_server_index = ', editor%tabs(editor%active_tab_index)%lsp_server_index
1220 end if
1221 close(debug_unit)
1222 end block
1223 call terminal_move_cursor(editor%screen_rows, 1)
1224 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
1225 if (editor%tabs(editor%active_tab_index)%lsp_server_index > 0) then
1226 ! Save current location to jump stack
1227 if (allocated(editor%filename)) then
1228 call push_jump_location(editor%jump_stack, &
1229 trim(editor%filename), &
1230 editor%cursors(editor%active_cursor)%line, &
1231 editor%cursors(editor%active_cursor)%column)
1232 end if
1233
1234 ! Request definition at current cursor position
1235 block
1236 integer :: request_id, lsp_line, lsp_char
1237 lsp_line = editor%cursors(editor%active_cursor)%line - 1
1238 lsp_char = editor%cursors(editor%active_cursor)%column - 1
1239
1240 ! Save editor state for callback
1241 if (.not. allocated(saved_editor_for_callback)) then
1242 allocate(saved_editor_for_callback)
1243 end if
1244 saved_editor_for_callback = editor
1245
1246 request_id = request_definition(editor%lsp_manager, &
1247 editor%tabs(editor%active_tab_index)%lsp_server_index, &
1248 editor%tabs(editor%active_tab_index)%filename, &
1249 lsp_line, lsp_char, handle_definition_response_wrapper)
1250
1251 block
1252 integer :: debug_unit
1253 open(newunit=debug_unit, file='/tmp/fac_keys.log', position='append', action='write')
1254 write(debug_unit, '(A,I0)') 'request_definition returned request_id = ', request_id
1255 close(debug_unit)
1256 end block
1257
1258 if (request_id > 0) then
1259 ! Response will be handled by callback
1260 call terminal_write('Searching for definition... ')
1261 else
1262 call terminal_write('[F12] LSP request failed ')
1263 end if
1264 end block
1265 else
1266 call terminal_write('[F12] No LSP server for this file ')
1267 end if
1268 else
1269 call terminal_write('[F12] No active tab ')
1270 end if
1271
1272 case('shift-f12', 'alt-r')
1273 ! Find all references (Shift+F12 or Alt+R)
1274 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
1275 if (editor%tabs(editor%active_tab_index)%lsp_server_index > 0) then
1276 ! Request references at current cursor position
1277 block
1278 integer :: request_id, lsp_line, lsp_char
1279 lsp_line = editor%cursors(editor%active_cursor)%line - 1
1280 lsp_char = editor%cursors(editor%active_cursor)%column - 1
1281
1282 ! Save editor state for callback
1283 if (.not. allocated(saved_editor_for_callback)) then
1284 allocate(saved_editor_for_callback)
1285 end if
1286 saved_editor_for_callback = editor
1287
1288 request_id = request_references(editor%lsp_manager, &
1289 editor%tabs(editor%active_tab_index)%lsp_server_index, &
1290 editor%tabs(editor%active_tab_index)%filename, &
1291 lsp_line, lsp_char, handle_references_response_wrapper)
1292
1293 if (request_id > 0) then
1294 ! Response will be handled by callback
1295 call terminal_move_cursor(editor%screen_rows, 1)
1296 call terminal_write('Searching for references... ')
1297 ! Show panel (will be populated when response arrives)
1298 call show_references_panel(editor%references_panel, editor%screen_cols, editor%screen_rows)
1299 end if
1300 end block
1301 end if
1302 end if
1303
1304 case('f2')
1305 ! Rename symbol
1306 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
1307 if (editor%tabs(editor%active_tab_index)%lsp_server_index > 0) then
1308 ! Get word under cursor as old name
1309 block
1310 character(len=:), allocatable :: line, old_name, new_name
1311 integer :: word_start, word_end, lsp_line, lsp_char, request_id
1312 logical :: cancelled
1313
1314 line = buffer_get_line(buffer, editor%cursors(editor%active_cursor)%line)
1315 call find_word_boundaries(line, editor%cursors(editor%active_cursor)%column, &
1316 word_start, word_end)
1317
1318 if (word_start > 0 .and. word_end >= word_start) then
1319 old_name = line(word_start:word_end)
1320
1321 ! Show rename prompt
1322 call show_rename_prompt(editor%screen_rows, old_name, new_name, cancelled)
1323
1324 if (.not. cancelled .and. allocated(new_name)) then
1325 ! Send rename request
1326 lsp_line = editor%cursors(editor%active_cursor)%line - 1
1327 lsp_char = editor%cursors(editor%active_cursor)%column - 1
1328
1329 ! Save editor state for callback
1330 if (.not. allocated(saved_editor_for_callback)) then
1331 allocate(saved_editor_for_callback)
1332 end if
1333 saved_editor_for_callback = editor
1334
1335 request_id = request_rename(editor%lsp_manager, &
1336 editor%tabs(editor%active_tab_index)%lsp_server_index, &
1337 editor%tabs(editor%active_tab_index)%filename, &
1338 lsp_line, lsp_char, new_name, handle_rename_response_wrapper)
1339
1340 if (request_id > 0) then
1341 call terminal_move_cursor(editor%screen_rows, 1)
1342 call terminal_write('Renaming symbol... ')
1343 end if
1344
1345 deallocate(new_name)
1346 end if
1347
1348 if (allocated(old_name)) deallocate(old_name)
1349 else
1350 call terminal_move_cursor(editor%screen_rows, 1)
1351 call terminal_write('No symbol under cursor ')
1352 end if
1353
1354 if (allocated(line)) deallocate(line)
1355 end block
1356 end if
1357 end if
1358
1359 case('shift-alt-f')
1360 ! Format document
1361 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
1362 if (editor%tabs(editor%active_tab_index)%lsp_server_index > 0) then
1363 block
1364 integer :: request_id
1365
1366 ! Save editor state for callback
1367 if (.not. allocated(saved_editor_for_callback)) then
1368 allocate(saved_editor_for_callback)
1369 end if
1370 saved_editor_for_callback = editor
1371
1372 ! Request formatting with 4 spaces (configurable later)
1373 request_id = request_formatting(editor%lsp_manager, &
1374 editor%tabs(editor%active_tab_index)%lsp_server_index, &
1375 editor%tabs(editor%active_tab_index)%filename, &
1376 4, .true., handle_formatting_response_wrapper)
1377
1378 if (request_id > 0) then
1379 call terminal_move_cursor(editor%screen_rows, 1)
1380 call terminal_write('Formatting document... ')
1381 end if
1382 end block
1383 end if
1384 end if
1385
1386 case('f4', 'alt-o')
1387 ! Document symbols outline (F4 or Alt+O)
1388 block
1389 integer :: debug_unit
1390 open(newunit=debug_unit, file='/tmp/fac_keys.log', position='append', action='write')
1391 write(debug_unit, '(A)') '>>> INSIDE F4/ALT-O HANDLER <<<'
1392 close(debug_unit)
1393 end block
1394 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
1395 if (editor%tabs(editor%active_tab_index)%lsp_server_index > 0) then
1396 ! Request document symbols
1397 block
1398 integer :: request_id
1399
1400 ! Save editor state for callback
1401 if (.not. allocated(saved_editor_for_callback)) then
1402 allocate(saved_editor_for_callback)
1403 end if
1404 saved_editor_for_callback = editor
1405
1406 request_id = request_document_symbols(editor%lsp_manager, &
1407 editor%tabs(editor%active_tab_index)%lsp_server_index, &
1408 editor%tabs(editor%active_tab_index)%filename, &
1409 handle_symbols_response_wrapper)
1410
1411 if (request_id > 0) then
1412 ! Response will be handled by callback
1413 call terminal_move_cursor(editor%screen_rows, 1)
1414 call terminal_write('Loading document symbols... ')
1415 ! Show panel (will be populated when response arrives)
1416 call show_symbols_panel(editor%symbols_panel, editor%screen_cols, editor%screen_rows)
1417 end if
1418 end block
1419 end if
1420 end if
1421
1422 case('ctrl-p')
1423 ! Command palette (Ctrl+P - VSCode standard)
1424 ! Note: ctrl-shift-p doesn't work - terminals can't distinguish ctrl-p from ctrl-shift-p
1425 block
1426 use command_palette_module, only: show_command_palette_interactive
1427 character(len=:), allocatable :: cmd_id
1428
1429 cmd_id = show_command_palette_interactive(editor%command_palette, editor%screen_rows)
1430
1431 if (allocated(cmd_id) .and. len_trim(cmd_id) > 0) then
1432 ! Execute the command by re-processing as a key
1433 call execute_palette_command(editor, buffer, cmd_id, should_quit)
1434 end if
1435
1436 ! Redraw screen after palette
1437 call render_screen(buffer, editor)
1438 end block
1439
1440 case('f6', 'alt-p')
1441 ! Workspace symbols (F6 or Alt+P for project)
1442 ! Workspace symbols (fuzzy search across project)
1443 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0) then
1444 block
1445 use workspace_symbols_panel_module, only: show_workspace_symbols_panel, &
1446 render_workspace_symbols_panel, &
1447 workspace_symbols_panel_handle_key, &
1448 hide_workspace_symbols_panel, &
1449 get_selected_symbol, &
1450 workspace_symbol_t
1451 use input_handler_module, only: get_key_input
1452 use lsp_server_manager_module, only: request_workspace_symbols
1453 integer :: request_id, status
1454 character(len=32) :: key_input
1455 logical :: handled
1456 type(workspace_symbol_t) :: selected_symbol
1457
1458 ! Show panel
1459 call show_workspace_symbols_panel(editor%workspace_symbols_panel)
1460
1461 ! Send initial empty query to get all symbols
1462 if (editor%tabs(editor%active_tab_index)%lsp_server_index > 0) then
1463 request_id = request_workspace_symbols(editor%lsp_manager, &
1464 editor%tabs(editor%active_tab_index)%lsp_server_index, &
1465 '', handle_workspace_symbols_response_wrapper)
1466 end if
1467
1468 call render_workspace_symbols_panel(editor%workspace_symbols_panel, editor%screen_rows)
1469
1470 ! Interactive loop
1471 do
1472 call get_key_input(key_input, status)
1473 if (status /= 0) cycle
1474
1475 ! Handle special keys
1476 if (key_input == 'enter') then
1477 selected_symbol = get_selected_symbol(editor%workspace_symbols_panel)
1478 if (allocated(selected_symbol%file_uri) .and. len_trim(selected_symbol%file_uri) > 0) then
1479 ! Navigate to the symbol location
1480 call navigate_to_workspace_symbol(editor, buffer, selected_symbol, should_quit)
1481 end if
1482 call hide_workspace_symbols_panel(editor%workspace_symbols_panel)
1483 call render_screen(buffer, editor)
1484 exit
1485 else if (key_input == 'esc') then
1486 call hide_workspace_symbols_panel(editor%workspace_symbols_panel)
1487 call render_screen(buffer, editor)
1488 exit
1489 else if (key_input == 'backspace') then
1490 if (editor%workspace_symbols_panel%search_pos > 0) then
1491 editor%workspace_symbols_panel%search_query(editor%workspace_symbols_panel%search_pos:editor%workspace_symbols_panel%search_pos) = ' '
1492 editor%workspace_symbols_panel%search_pos = editor%workspace_symbols_panel%search_pos - 1
1493 ! Send new query to LSP
1494 if (editor%tabs(editor%active_tab_index)%lsp_server_index > 0) then
1495 request_id = request_workspace_symbols(editor%lsp_manager, &
1496 editor%tabs(editor%active_tab_index)%lsp_server_index, &
1497 trim(editor%workspace_symbols_panel%search_query(1:editor%workspace_symbols_panel%search_pos)), &
1498 handle_workspace_symbols_response_wrapper)
1499 end if
1500 end if
1501 else
1502 ! Try navigation keys
1503 call workspace_symbols_panel_handle_key(editor%workspace_symbols_panel, key_input, handled)
1504 if (.not. handled) then
1505 ! Regular character - add to search
1506 if (len_trim(key_input) == 1) then
1507 if (iachar(key_input(1:1)) >= 32 .and. iachar(key_input(1:1)) < 127 .and. &
1508 editor%workspace_symbols_panel%search_pos < 255) then
1509 editor%workspace_symbols_panel%search_pos = editor%workspace_symbols_panel%search_pos + 1
1510 editor%workspace_symbols_panel%search_query(editor%workspace_symbols_panel%search_pos:editor%workspace_symbols_panel%search_pos) = key_input(1:1)
1511 ! Send new query to LSP
1512 if (editor%tabs(editor%active_tab_index)%lsp_server_index > 0) then
1513 request_id = request_workspace_symbols(editor%lsp_manager, &
1514 editor%tabs(editor%active_tab_index)%lsp_server_index, &
1515 trim(editor%workspace_symbols_panel%search_query(1:editor%workspace_symbols_panel%search_pos)), &
1516 handle_workspace_symbols_response_wrapper)
1517 end if
1518 end if
1519 end if
1520 end if
1521 end if
1522
1523 call render_workspace_symbols_panel(editor%workspace_symbols_panel, editor%screen_rows)
1524 end do
1525 end block
1526 end if
1527
1528 case('alt-comma')
1529 ! Jump back in navigation history (Alt+,)
1530 if (.not. is_jump_stack_empty(editor%jump_stack)) then
1531 block
1532 character(len=:), allocatable :: jump_filename
1533 integer(int32) :: jump_line, jump_column
1534 logical :: success
1535
1536 success = pop_jump_location(editor%jump_stack, jump_filename, jump_line, jump_column)
1537 if (success) then
1538 ! Check if we need to open a different file
1539 if (allocated(editor%filename)) then
1540 if (trim(jump_filename) /= trim(editor%filename)) then
1541 ! TODO: Open the file
1542 call terminal_move_cursor(editor%screen_rows, 1)
1543 call terminal_write('Opening: ' // trim(jump_filename))
1544 ! For now, just jump if same file
1545 end if
1546 end if
1547
1548 ! Jump to the location
1549 editor%cursors(editor%active_cursor)%line = jump_line
1550 editor%cursors(editor%active_cursor)%column = jump_column
1551 editor%cursors(editor%active_cursor)%desired_column = jump_column
1552 call sync_editor_to_pane(editor)
1553 call update_viewport(editor)
1554 end if
1555 end block
1556 end if
1557
1558 case("ctrl-'", "ctrl-apostrophe", "alt-'")
1559 ! Cycle quotes: " -> ' -> `
1560 ! ctrl-': Doesn't work (terminals send plain apostrophe)
1561 ! alt-': Alternative binding (Option+' on Mac)
1562 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
1563 call cycle_quotes(editor%cursors(editor%active_cursor), buffer)
1564 is_edit_action = .true.
1565
1566 case('ctrl-opt-backspace', 'ctrl-alt-backspace', 'alt-shift-backspace', 'alt-shift-apostrophe')
1567 ! Remove surrounding brackets/quotes
1568 ! ctrl-alt-backspace: Doesn't work (terminals send alt-backspace)
1569 ! alt-shift-backspace: Doesn't work (terminals send alt-backspace)
1570 ! alt-shift-': Alternative binding (Alt+Shift+' = Alt+")
1571 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
1572 call remove_brackets(editor%cursors(editor%active_cursor), buffer)
1573 is_edit_action = .true.
1574
1575 case('ctrl-d')
1576 call select_next_match(editor, buffer)
1577 call sync_editor_to_pane(editor)
1578 call update_viewport(editor)
1579
1580 case('f8', 'alt-e')
1581 ! Toggle diagnostics panel (F8 or Alt+E for errors)
1582 block
1583 integer :: debug_unit
1584 open(newunit=debug_unit, file='/tmp/fac_keys.log', position='append', action='write')
1585 write(debug_unit, '(A)') '>>> INSIDE F8/ALT-E HANDLER <<<'
1586 write(debug_unit, '(A)') 'Calling toggle_diagnostics_panel...'
1587 close(debug_unit)
1588 end block
1589 call toggle_diagnostics_panel(editor)
1590 block
1591 integer :: debug_unit
1592 open(newunit=debug_unit, file='/tmp/fac_keys.log', position='append', action='write')
1593 write(debug_unit, '(A)') 'toggle_diagnostics_panel returned'
1594 close(debug_unit)
1595 end block
1596
1597 case('alt-c')
1598 ! Toggle case sensitivity for match mode (ctrl-d)
1599 ! Only has effect when in active match mode (search_pattern allocated)
1600 if (allocated(search_pattern)) then
1601 match_case_sensitive = .not. match_case_sensitive
1602 end if
1603
1604 case('alt-[', 'alt-]')
1605 ! Jump to matching bracket
1606 call jump_to_matching_bracket(editor, buffer)
1607
1608 case('opt-meta-up', 'ctrl-alt-up', 'alt-ctrl-up')
1609 ! Add cursor on line above
1610 ! opt-meta-up: Doesn't work (terminals don't send Cmd)
1611 ! ctrl-alt-up: Alternative binding that works
1612 call add_cursor_above(editor)
1613 call sync_editor_to_pane(editor)
1614 call update_viewport(editor)
1615
1616 case('opt-meta-down', 'ctrl-alt-down', 'alt-ctrl-down')
1617 ! Add cursor on line below
1618 ! opt-meta-down: Doesn't work (terminals don't send Cmd)
1619 ! ctrl-alt-down: Alternative binding that works
1620 call add_cursor_below(editor, buffer)
1621 call sync_editor_to_pane(editor)
1622 call update_viewport(editor)
1623
1624 ! Search commands
1625 case('ctrl-f')
1626 ! Unified search and replace (Ctrl+F)
1627 call show_unified_search_prompt(editor, buffer)
1628 call update_viewport(editor)
1629
1630 case('ctrl-r')
1631 ! Find and replace
1632 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
1633 call show_replace_prompt(editor, buffer)
1634 call update_viewport(editor)
1635 is_edit_action = .true.
1636
1637 case('n')
1638 ! Only use 'n' for search navigation if we have an active search
1639 if (allocated(current_search_pattern)) then
1640 call search_forward(editor, buffer)
1641 call update_viewport(editor)
1642 else
1643 ! No active search, treat as regular character with multi-cursor support
1644 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
1645 if (size(editor%cursors) > 1) then
1646 call insert_char_multiple_cursors(editor, buffer, 'n')
1647 else
1648 call insert_char(editor%cursors(editor%active_cursor), buffer, 'n')
1649 end if
1650 call sync_editor_to_pane(editor)
1651 is_edit_action = .true.
1652 end if
1653
1654 case('N')
1655 ! Only use 'N' for search navigation if we have an active search
1656 if (allocated(current_search_pattern)) then
1657 call search_backward(editor, buffer)
1658 call update_viewport(editor)
1659 else
1660 ! No active search, treat as regular character with multi-cursor support
1661 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
1662 if (size(editor%cursors) > 1) then
1663 call insert_char_multiple_cursors(editor, buffer, 'N')
1664 else
1665 call insert_char(editor%cursors(editor%active_cursor), buffer, 'N')
1666 end if
1667 call sync_editor_to_pane(editor)
1668 is_edit_action = .true.
1669 end if
1670
1671 case default
1672 ! DEBUG: Show unhandled function keys
1673 if (index(key_str, 'f') == 1 .or. index(key_str, 'shift-f') == 1) then
1674 call terminal_move_cursor(editor%screen_rows, 1)
1675 call terminal_write('[DEBUG] Unhandled key: ' // trim(key_str) // ' ')
1676 end if
1677
1678 ! Check for mouse events
1679 if (index(key_str, 'mouse-') == 1) then
1680 call handle_mouse_event_action(key_str, editor, buffer)
1681 call sync_editor_to_pane(editor)
1682 ! Regular character input (including space)
1683 ! Check for single char: either len_trim=1, or it's a space (trim removes it)
1684 else if (len_trim(key_str) == 1 .or. (len_trim(key_str) == 0 .and. key_str(1:1) == ' ')) then
1685 if (.not. last_action_was_edit) call save_undo_state(buffer, editor)
1686 ! Handle character input for all cursors
1687 if (size(editor%cursors) > 1) then
1688 call insert_char_multiple_cursors(editor, buffer, key_str(1:1))
1689 else
1690 call insert_char(editor%cursors(editor%active_cursor), buffer, key_str(1:1))
1691 end if
1692 call sync_editor_to_pane(editor)
1693 is_edit_action = .true.
1694
1695 ! Auto-trigger signature help on '(' or ','
1696 if (key_str(1:1) == '(' .or. key_str(1:1) == ',') then
1697 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
1698 if (editor%tabs(editor%active_tab_index)%lsp_server_index > 0) then
1699 block
1700 integer :: request_id, lsp_line, lsp_char
1701 lsp_line = editor%cursors(editor%active_cursor)%line - 1
1702 lsp_char = editor%cursors(editor%active_cursor)%column - 1
1703
1704 ! Save editor state for callback
1705 if (.not. allocated(saved_editor_for_callback)) then
1706 allocate(saved_editor_for_callback)
1707 end if
1708 saved_editor_for_callback = editor
1709
1710 request_id = request_signature_help(editor%lsp_manager, &
1711 editor%tabs(editor%active_tab_index)%lsp_server_index, &
1712 editor%tabs(editor%active_tab_index)%filename, &
1713 lsp_line, lsp_char, handle_signature_response_wrapper)
1714
1715 if (request_id > 0) then
1716 ! Show tooltip placeholder
1717 call show_signature_tooltip(editor%signature_tooltip, &
1718 editor%cursors(editor%active_cursor)%line - editor%viewport_line + 1, &
1719 editor%cursors(editor%active_cursor)%column - editor%viewport_column + 1)
1720 end if
1721 end block
1722 end if
1723 end if
1724 end if
1725
1726 ! Hide signature help on ')'
1727 if (key_str(1:1) == ')') then
1728 call hide_signature_tooltip(editor%signature_tooltip)
1729 end if
1730 end if
1731 end select
1732
1733 ! Update edit action state
1734 last_action_was_edit = is_edit_action
1735
1736 ! Notify LSP of document changes if buffer was modified
1737 if (is_edit_action) then
1738 call notify_buffer_change(editor, buffer)
1739 end if
1740 end subroutine handle_key_command
1741
1742 subroutine move_cursor_up(cursor, buffer)
1743 type(cursor_t), intent(inout) :: cursor
1744 type(buffer_t), intent(in) :: buffer
1745 character(len=:), allocatable :: current_line, target_line
1746
1747 cursor%has_selection = .false. ! Clear selection
1748 if (cursor%line > 1) then
1749 current_line = buffer_get_line(buffer, cursor%line)
1750 cursor%line = cursor%line - 1
1751 target_line = buffer_get_line(buffer, cursor%line)
1752
1753 ! If on empty line with no goal column established, go to end of target line
1754 ! Otherwise, use the goal column
1755 if (len(current_line) == 0 .and. cursor%desired_column == 1) then
1756 cursor%column = len(target_line) + 1
1757 cursor%desired_column = cursor%column
1758 else
1759 ! Use goal column, clamped to line bounds
1760 cursor%column = cursor%desired_column
1761 if (cursor%column > len(target_line) + 1) then
1762 cursor%column = len(target_line) + 1
1763 end if
1764 end if
1765
1766 if (allocated(current_line)) deallocate(current_line)
1767 if (allocated(target_line)) deallocate(target_line)
1768 end if
1769 end subroutine move_cursor_up
1770
1771 subroutine move_cursor_down(cursor, buffer, line_count)
1772 type(cursor_t), intent(inout) :: cursor
1773 type(buffer_t), intent(in) :: buffer
1774 integer, intent(in) :: line_count
1775 character(len=:), allocatable :: current_line, target_line
1776
1777 cursor%has_selection = .false. ! Clear selection
1778 if (cursor%line < line_count) then
1779 current_line = buffer_get_line(buffer, cursor%line)
1780 cursor%line = cursor%line + 1
1781 target_line = buffer_get_line(buffer, cursor%line)
1782
1783 ! If on empty line with no goal column established, go to col 1 of target line
1784 ! Otherwise, use the goal column
1785 if (len(current_line) == 0 .and. cursor%desired_column == 1) then
1786 cursor%column = 1
1787 ! desired_column stays 1
1788 else
1789 ! Use goal column, clamped to line bounds
1790 cursor%column = cursor%desired_column
1791 if (cursor%column > len(target_line) + 1) then
1792 cursor%column = len(target_line) + 1
1793 end if
1794 end if
1795
1796 if (allocated(current_line)) deallocate(current_line)
1797 if (allocated(target_line)) deallocate(target_line)
1798 end if
1799 end subroutine move_cursor_down
1800
1801 subroutine move_cursor_left(cursor, buffer)
1802 type(cursor_t), intent(inout) :: cursor
1803 type(buffer_t), intent(in) :: buffer
1804 integer :: char_count
1805
1806 ! If we have a selection, move to START of selection (leftmost/earliest position)
1807 if (cursor%has_selection) then
1808 ! Find which end is further left (start of selection)
1809 if (cursor%selection_start_line < cursor%line .or. &
1810 (cursor%selection_start_line == cursor%line .and. cursor%selection_start_col < cursor%column)) then
1811 ! selection_start is the start - move there
1812 cursor%line = cursor%selection_start_line
1813 cursor%column = cursor%selection_start_col
1814 end if
1815 ! Otherwise cursor is already at the start
1816 cursor%has_selection = .false.
1817 cursor%desired_column = cursor%column
1818 return
1819 end if
1820
1821 if (cursor%column > 1) then
1822 cursor%column = cursor%column - 1
1823 cursor%desired_column = cursor%column
1824 else if (cursor%line > 1) then
1825 ! Move to end of previous line
1826 cursor%line = cursor%line - 1
1827 char_count = buffer_get_line_char_count(buffer, cursor%line)
1828 cursor%column = char_count + 1
1829 cursor%desired_column = cursor%column
1830 end if
1831 end subroutine move_cursor_left
1832
1833 subroutine move_cursor_right(cursor, buffer)
1834 type(cursor_t), intent(inout) :: cursor
1835 type(buffer_t), intent(in) :: buffer
1836 integer :: line_count, char_count
1837
1838 ! If we have a selection, move to END of selection (rightmost/latest position)
1839 if (cursor%has_selection) then
1840 ! Find which end is further right (end of selection)
1841 if (cursor%selection_start_line > cursor%line .or. &
1842 (cursor%selection_start_line == cursor%line .and. cursor%selection_start_col > cursor%column)) then
1843 ! selection_start is the end - move there
1844 cursor%line = cursor%selection_start_line
1845 cursor%column = cursor%selection_start_col
1846 end if
1847 ! Otherwise cursor is already at the end
1848 cursor%has_selection = .false.
1849 cursor%desired_column = cursor%column
1850 return
1851 end if
1852
1853 char_count = buffer_get_line_char_count(buffer, cursor%line)
1854 line_count = buffer_get_line_count(buffer)
1855
1856 if (cursor%column <= char_count) then
1857 cursor%column = cursor%column + 1
1858 cursor%desired_column = cursor%column
1859 else if (cursor%line < line_count) then
1860 ! Move to start of next line
1861 cursor%line = cursor%line + 1
1862 cursor%column = 1
1863 cursor%desired_column = cursor%column
1864 end if
1865 end subroutine move_cursor_right
1866
1867 subroutine move_cursor_smart_home(cursor, buffer)
1868 type(cursor_t), intent(inout) :: cursor
1869 type(buffer_t), intent(in) :: buffer
1870 character(len=:), allocatable :: line
1871 integer :: first_non_whitespace, i
1872
1873 cursor%has_selection = .false. ! Clear selection
1874
1875 ! Get the current line
1876 line = buffer_get_line(buffer, cursor%line)
1877
1878 ! Find the first non-whitespace character
1879 first_non_whitespace = 1
1880 do i = 1, len(line)
1881 if (line(i:i) /= ' ' .and. line(i:i) /= char(9)) then ! Not space or tab
1882 first_non_whitespace = i
1883 exit
1884 end if
1885 end do
1886
1887 ! Smart home behavior:
1888 ! If we're already at the first non-whitespace, go to column 1
1889 ! If we're at column 1, go to first non-whitespace
1890 ! Otherwise, go to first non-whitespace
1891 if (cursor%column == first_non_whitespace .and. first_non_whitespace > 1) then
1892 cursor%column = 1
1893 else
1894 cursor%column = first_non_whitespace
1895 end if
1896
1897 cursor%desired_column = cursor%column
1898
1899 if (allocated(line)) deallocate(line)
1900 end subroutine move_cursor_smart_home
1901
1902 subroutine move_cursor_end(cursor, buffer)
1903 type(cursor_t), intent(inout) :: cursor
1904 type(buffer_t), intent(in) :: buffer
1905 character(len=:), allocatable :: line
1906
1907 cursor%has_selection = .false. ! Clear selection
1908 line = buffer_get_line(buffer, cursor%line)
1909 cursor%column = len(line) + 1
1910 cursor%desired_column = cursor%column
1911 if (allocated(line)) deallocate(line)
1912 end subroutine move_cursor_end
1913
1914 subroutine move_cursor_page_up(cursor, editor)
1915 type(cursor_t), intent(inout) :: cursor
1916 type(editor_state_t), intent(in) :: editor
1917 integer :: page_size
1918
1919 cursor%has_selection = .false. ! Clear selection
1920 page_size = editor%screen_rows - 2 ! Leave room for status bar
1921 cursor%line = max(1, cursor%line - page_size)
1922 cursor%column = cursor%desired_column
1923 end subroutine move_cursor_page_up
1924
1925 subroutine move_cursor_page_down(cursor, editor, line_count)
1926 type(cursor_t), intent(inout) :: cursor
1927 type(editor_state_t), intent(in) :: editor
1928 integer, intent(in) :: line_count
1929 integer :: page_size
1930
1931 cursor%has_selection = .false. ! Clear selection
1932 page_size = editor%screen_rows - 2 ! Leave room for status bar
1933 cursor%line = min(line_count, cursor%line + page_size)
1934 cursor%column = cursor%desired_column
1935 end subroutine move_cursor_page_down
1936
1937 subroutine move_cursor_word_left(cursor, buffer)
1938 type(cursor_t), intent(inout) :: cursor
1939 type(buffer_t), intent(in) :: buffer
1940 character(len=:), allocatable :: line
1941 integer :: pos, line_len
1942
1943 cursor%has_selection = .false. ! Clear selection
1944 line = buffer_get_line(buffer, cursor%line)
1945 line_len = len(line)
1946 pos = cursor%column
1947
1948 ! Handle empty lines
1949 if (line_len == 0) then
1950 if (cursor%line > 1) then
1951 ! Move to end of previous line
1952 cursor%line = cursor%line - 1
1953 if (allocated(line)) deallocate(line)
1954 line = buffer_get_line(buffer, cursor%line)
1955 cursor%column = len(line) + 1
1956 else
1957 cursor%column = 1
1958 end if
1959 cursor%desired_column = cursor%column
1960 if (allocated(line)) deallocate(line)
1961 return
1962 end if
1963
1964 if (pos > 1 .and. line_len > 0) then
1965 ! Simple algorithm: move left one position at a time until we find a word start
1966 ! A word start is: a word char that's either at position 1 OR preceded by a non-word char
1967
1968 pos = pos - 1 ! Move left one position
1969
1970 ! Skip any whitespace
1971 do while (pos > 1 .and. pos <= line_len)
1972 if (line(pos:pos) /= ' ') exit
1973 pos = pos - 1
1974 end do
1975
1976 ! If we're on a word character, go to the start of this word
1977 if (pos >= 1 .and. pos <= line_len) then
1978 if (is_word_char(line(pos:pos))) then
1979 ! Move to the start of the current word
1980 do while (pos > 1)
1981 if (pos-1 < 1) exit ! Safety check
1982 if (.not. is_word_char(line(pos-1:pos-1))) exit
1983 pos = pos - 1
1984 end do
1985 end if
1986 end if
1987
1988 ! Clamp to valid range
1989 if (pos < 1) pos = 1
1990 if (pos > line_len + 1) pos = line_len + 1
1991
1992 cursor%column = pos
1993 else if (cursor%line > 1) then
1994 ! Move to end of previous line
1995 cursor%line = cursor%line - 1
1996 if (allocated(line)) deallocate(line)
1997 line = buffer_get_line(buffer, cursor%line)
1998 cursor%column = len(line) + 1
1999 else
2000 cursor%column = 1
2001 end if
2002
2003 cursor%desired_column = cursor%column
2004 if (allocated(line)) deallocate(line)
2005 end subroutine move_cursor_word_left
2006
2007 subroutine move_cursor_word_right(cursor, buffer)
2008 type(cursor_t), intent(inout) :: cursor
2009 type(buffer_t), intent(in) :: buffer
2010 character(len=:), allocatable :: line
2011 integer :: pos, line_count, line_len
2012
2013 cursor%has_selection = .false. ! Clear selection
2014 line = buffer_get_line(buffer, cursor%line)
2015 line_count = buffer_get_line_count(buffer)
2016 line_len = len(line)
2017 pos = cursor%column
2018
2019 if (pos <= line_len) then
2020 ! VSCode-style word navigation: stop after each word OR punctuation group
2021 if (line(pos:pos) == ' ') then
2022 ! On whitespace - skip all whitespace
2023 do while (pos < line_len)
2024 if (pos+1 <= line_len .and. line(pos+1:pos+1) == ' ') then
2025 pos = pos + 1
2026 else
2027 exit
2028 end if
2029 end do
2030 pos = pos + 1 ! Move past whitespace
2031 else if (is_word_char(line(pos:pos))) then
2032 ! We're on a word character - skip to end of this word
2033 do while (pos < line_len)
2034 if (pos+1 <= line_len) then
2035 if (.not. is_word_char(line(pos+1:pos+1))) exit
2036 end if
2037 pos = pos + 1
2038 end do
2039 pos = pos + 1 ! Move past the word
2040 else
2041 ! We're on punctuation - skip to end of punctuation group
2042 ! e.g., "##" should be treated as one group
2043 do while (pos < line_len)
2044 if (pos+1 <= line_len) then
2045 ! Stop if next char is word char or space
2046 if (is_word_char(line(pos+1:pos+1)) .or. line(pos+1:pos+1) == ' ') exit
2047 end if
2048 pos = pos + 1
2049 end do
2050 pos = pos + 1 ! Move past punctuation
2051 end if
2052
2053 cursor%column = pos
2054 else if (cursor%line < line_count) then
2055 ! Move to start of next line
2056 cursor%line = cursor%line + 1
2057 cursor%column = 1
2058 else
2059 cursor%column = len(line) + 1
2060 end if
2061
2062 cursor%desired_column = cursor%column
2063 if (allocated(line)) deallocate(line)
2064 end subroutine move_cursor_word_right
2065
2066 function is_word_char(ch) result(is_word)
2067 character, intent(in) :: ch
2068 logical :: is_word
2069
2070 is_word = (ch >= 'a' .and. ch <= 'z') .or. &
2071 (ch >= 'A' .and. ch <= 'Z') .or. &
2072 (ch >= '0' .and. ch <= '9') .or. &
2073 ch == '_'
2074 end function is_word_char
2075
2076 subroutine handle_backspace(cursor, buffer)
2077 type(cursor_t), intent(inout) :: cursor
2078 type(buffer_t), intent(inout) :: buffer
2079
2080 ! Delete selection if one exists
2081 if (cursor%has_selection) then
2082 call delete_selection(cursor, buffer)
2083 return
2084 end if
2085
2086 if (cursor%column > 1) then
2087 ! Delete character before cursor
2088 cursor%column = cursor%column - 1
2089 call buffer_delete_at_cursor(buffer, cursor)
2090 cursor%desired_column = cursor%column
2091 else if (cursor%line > 1) then
2092 ! Join with previous line
2093 call join_line_with_previous(cursor, buffer)
2094 end if
2095 end subroutine handle_backspace
2096
2097 subroutine handle_delete(cursor, buffer)
2098 type(cursor_t), intent(inout) :: cursor
2099 type(buffer_t), intent(inout) :: buffer
2100 character(len=:), allocatable :: line
2101 integer :: line_count
2102
2103 ! Delete selection if one exists
2104 if (cursor%has_selection) then
2105 call delete_selection(cursor, buffer)
2106 return
2107 end if
2108
2109 line = buffer_get_line(buffer, cursor%line)
2110 line_count = buffer_get_line_count(buffer)
2111
2112 if (cursor%column <= len(line)) then
2113 ! Delete character at cursor
2114 call buffer_delete_at_cursor(buffer, cursor)
2115 else if (cursor%line < line_count) then
2116 ! Join with next line
2117 call join_line_with_next(cursor, buffer)
2118 end if
2119
2120 if (allocated(line)) deallocate(line)
2121 end subroutine handle_delete
2122
2123 subroutine handle_enter(cursor, buffer)
2124 type(cursor_t), intent(inout) :: cursor
2125 type(buffer_t), intent(inout) :: buffer
2126 character(len=:), allocatable :: current_line
2127 integer :: indent_level, i
2128
2129 ! Delete selection if one exists
2130 if (cursor%has_selection) then
2131 call delete_selection(cursor, buffer)
2132 end if
2133
2134 ! Get the current line to determine indentation
2135 current_line = buffer_get_line(buffer, cursor%line)
2136
2137 ! Count leading spaces/tabs for auto-indent
2138 indent_level = 0
2139 do i = 1, len(current_line)
2140 if (current_line(i:i) == ' ') then
2141 indent_level = indent_level + 1
2142 else if (current_line(i:i) == char(9)) then ! Tab
2143 indent_level = indent_level + 4 ! Treat tab as 4 spaces
2144 else
2145 exit ! Found non-whitespace character
2146 end if
2147 end do
2148
2149 ! Insert the newline
2150 call buffer_insert_newline(buffer, cursor)
2151 cursor%line = cursor%line + 1
2152 cursor%column = 1
2153
2154 ! Insert the same indentation on the new line
2155 do i = 1, indent_level
2156 call buffer_insert_char(buffer, cursor, ' ')
2157 cursor%column = cursor%column + 1
2158 end do
2159
2160 cursor%desired_column = cursor%column
2161
2162 if (allocated(current_line)) deallocate(current_line)
2163 end subroutine handle_enter
2164
2165 subroutine handle_tab(cursor, buffer)
2166 type(cursor_t), intent(inout) :: cursor
2167 type(buffer_t), intent(inout) :: buffer
2168 integer :: i
2169
2170 ! Insert 4 spaces
2171 do i = 1, 4
2172 call buffer_insert_char(buffer, cursor, ' ')
2173 cursor%column = cursor%column + 1
2174 end do
2175 cursor%desired_column = cursor%column
2176 end subroutine handle_tab
2177
2178 subroutine indent_selection(cursor, buffer)
2179 type(cursor_t), intent(inout) :: cursor
2180 type(buffer_t), intent(inout) :: buffer
2181 integer :: start_line, end_line, i
2182 character(len=:), allocatable :: line
2183
2184 if (.not. cursor%has_selection) return
2185
2186 ! Get the range of lines to indent
2187 start_line = min(cursor%selection_start_line, cursor%line)
2188 end_line = max(cursor%selection_start_line, cursor%line)
2189
2190 ! Indent each line in the selection
2191 do i = start_line, end_line
2192 line = buffer_get_line(buffer, i)
2193 ! Insert 4 spaces at the beginning of the line
2194 call buffer_insert_text_at(buffer, i, 1, " ")
2195 if (allocated(line)) deallocate(line)
2196 end do
2197
2198 ! Adjust cursor position if needed
2199 if (cursor%column > 1) then
2200 cursor%column = cursor%column + 4
2201 end if
2202 if (cursor%selection_start_col > 1) then
2203 cursor%selection_start_col = cursor%selection_start_col + 4
2204 end if
2205 end subroutine indent_selection
2206
2207 subroutine dedent_selection(cursor, buffer)
2208 type(cursor_t), intent(inout) :: cursor
2209 type(buffer_t), intent(inout) :: buffer
2210 integer :: start_line, end_line, i, spaces_to_remove
2211 character(len=:), allocatable :: line
2212
2213 if (.not. cursor%has_selection) return
2214
2215 ! Get the range of lines to dedent
2216 start_line = min(cursor%selection_start_line, cursor%line)
2217 end_line = max(cursor%selection_start_line, cursor%line)
2218
2219 ! Dedent each line in the selection
2220 do i = start_line, end_line
2221 line = buffer_get_line(buffer, i)
2222 spaces_to_remove = 0
2223
2224 ! Count how many spaces we can remove (max 4)
2225 do while (spaces_to_remove < 4 .and. spaces_to_remove < len(line))
2226 if (line(spaces_to_remove + 1:spaces_to_remove + 1) == ' ') then
2227 spaces_to_remove = spaces_to_remove + 1
2228 else
2229 exit
2230 end if
2231 end do
2232
2233 ! Remove the spaces
2234 if (spaces_to_remove > 0) then
2235 call buffer_delete_range(buffer, i, 1, i, spaces_to_remove + 1)
2236
2237 ! Adjust cursor position for current line
2238 if (i == cursor%line .and. cursor%column > spaces_to_remove) then
2239 cursor%column = cursor%column - spaces_to_remove
2240 else if (i == cursor%line .and. cursor%column <= spaces_to_remove) then
2241 cursor%column = 1
2242 end if
2243
2244 if (i == cursor%selection_start_line .and. cursor%selection_start_col > spaces_to_remove) then
2245 cursor%selection_start_col = cursor%selection_start_col - spaces_to_remove
2246 else if (i == cursor%selection_start_line .and. cursor%selection_start_col <= spaces_to_remove) then
2247 cursor%selection_start_col = 1
2248 end if
2249 end if
2250
2251 if (allocated(line)) deallocate(line)
2252 end do
2253 end subroutine dedent_selection
2254
2255 subroutine dedent_current_line(cursor, buffer)
2256 type(cursor_t), intent(inout) :: cursor
2257 type(buffer_t), intent(inout) :: buffer
2258 character(len=:), allocatable :: line
2259 integer :: spaces_to_remove
2260
2261 line = buffer_get_line(buffer, cursor%line)
2262 spaces_to_remove = 0
2263
2264 ! Count how many spaces we can remove (max 4)
2265 do while (spaces_to_remove < 4 .and. spaces_to_remove < len(line))
2266 if (line(spaces_to_remove + 1:spaces_to_remove + 1) == ' ') then
2267 spaces_to_remove = spaces_to_remove + 1
2268 else
2269 exit
2270 end if
2271 end do
2272
2273 ! Remove the spaces
2274 if (spaces_to_remove > 0) then
2275 call buffer_delete_range(buffer, cursor%line, 1, cursor%line, spaces_to_remove + 1)
2276
2277 ! Adjust cursor position
2278 if (cursor%column > spaces_to_remove) then
2279 cursor%column = cursor%column - spaces_to_remove
2280 else
2281 cursor%column = 1
2282 end if
2283 cursor%desired_column = cursor%column
2284 end if
2285
2286 if (allocated(line)) deallocate(line)
2287 end subroutine dedent_current_line
2288
2289 subroutine insert_char(cursor, buffer, ch)
2290 type(cursor_t), intent(inout) :: cursor
2291 type(buffer_t), intent(inout) :: buffer
2292 character, intent(in) :: ch
2293 character :: closing_char
2294 logical :: should_auto_close, should_wrap
2295 integer :: start_line, start_col, end_line, end_col
2296
2297 ! Check if we should auto-close or wrap brackets/quotes
2298 should_auto_close = .false.
2299 should_wrap = .false.
2300 select case(ch)
2301 case('(')
2302 closing_char = ')'
2303 should_auto_close = .true.
2304 if (cursor%has_selection) should_wrap = .true.
2305 case('[')
2306 closing_char = ']'
2307 should_auto_close = .true.
2308 if (cursor%has_selection) should_wrap = .true.
2309 case('{')
2310 closing_char = '}'
2311 should_auto_close = .true.
2312 if (cursor%has_selection) should_wrap = .true.
2313 case('"')
2314 closing_char = '"'
2315 should_auto_close = .true.
2316 if (cursor%has_selection) should_wrap = .true.
2317 case("'")
2318 closing_char = "'"
2319 should_auto_close = .true.
2320 if (cursor%has_selection) should_wrap = .true.
2321 case('`')
2322 closing_char = '`'
2323 should_auto_close = .true.
2324 if (cursor%has_selection) should_wrap = .true.
2325 end select
2326
2327 ! If we should wrap, don't delete - wrap the selection instead
2328 if (should_wrap) then
2329 ! Find selection bounds
2330 if (cursor%line < cursor%selection_start_line .or. &
2331 (cursor%line == cursor%selection_start_line .and. cursor%column < cursor%selection_start_col)) then
2332 start_line = cursor%line
2333 start_col = cursor%column
2334 end_line = cursor%selection_start_line
2335 end_col = cursor%selection_start_col
2336 else
2337 start_line = cursor%selection_start_line
2338 start_col = cursor%selection_start_col
2339 end_line = cursor%line
2340 end_col = cursor%column
2341 end if
2342
2343 ! Insert closing character at end
2344 cursor%line = end_line
2345 cursor%column = end_col
2346 call buffer_insert_char(buffer, cursor, closing_char)
2347
2348 ! Insert opening character at start
2349 cursor%line = start_line
2350 cursor%column = start_col
2351 call buffer_insert_char(buffer, cursor, ch)
2352
2353 ! Position cursor after the opening bracket (inside the wrapped text)
2354 cursor%column = start_col + 1
2355 cursor%has_selection = .false.
2356 cursor%desired_column = cursor%column
2357 return
2358 end if
2359
2360 ! Delete selection if one exists (normal behavior)
2361 if (cursor%has_selection) then
2362 call delete_selection(cursor, buffer)
2363 end if
2364
2365 ! Insert the character
2366 call buffer_insert_char(buffer, cursor, ch)
2367 cursor%column = cursor%column + 1
2368
2369 ! If auto-close is enabled, insert the closing character
2370 if (should_auto_close) then
2371 call buffer_insert_char(buffer, cursor, closing_char)
2372 ! Don't move cursor forward - stay between the brackets/quotes
2373 end if
2374
2375 cursor%desired_column = cursor%column
2376 end subroutine insert_char
2377
2378 subroutine insert_char_multiple_cursors(editor, buffer, ch)
2379 type(editor_state_t), intent(inout) :: editor
2380 type(buffer_t), intent(inout) :: buffer
2381 character, intent(in) :: ch
2382 integer :: i
2383 integer :: offset_adjust
2384
2385 ! Sort cursors by position to handle offset adjustments
2386 call sort_cursors_by_position(editor)
2387
2388 offset_adjust = 0
2389 do i = 1, size(editor%cursors)
2390 ! For cursors with selection, delete selection first
2391 if (editor%cursors(i)%has_selection) then
2392 call delete_selection(editor%cursors(i), buffer)
2393 editor%cursors(i)%has_selection = .false.
2394 end if
2395
2396 ! Insert character
2397 call buffer_insert_char(buffer, editor%cursors(i), ch)
2398 editor%cursors(i)%column = editor%cursors(i)%column + 1
2399 editor%cursors(i)%desired_column = editor%cursors(i)%column
2400 end do
2401 end subroutine insert_char_multiple_cursors
2402
2403 subroutine delete_selection(cursor, buffer)
2404 type(cursor_t), intent(inout) :: cursor
2405 type(buffer_t), intent(inout) :: buffer
2406 integer :: start_line, start_col, end_line, end_col
2407 integer :: i
2408 character(len=:), allocatable :: line
2409
2410 if (.not. cursor%has_selection) return
2411
2412 ! Determine start and end of selection
2413 if (cursor%line < cursor%selection_start_line .or. &
2414 (cursor%line == cursor%selection_start_line .and. &
2415 cursor%column < cursor%selection_start_col)) then
2416 start_line = cursor%line
2417 start_col = cursor%column
2418 end_line = cursor%selection_start_line
2419 end_col = cursor%selection_start_col
2420 else
2421 start_line = cursor%selection_start_line
2422 start_col = cursor%selection_start_col
2423 end_line = cursor%line
2424 end_col = cursor%column
2425 end if
2426
2427 ! Delete the selection
2428 if (start_line == end_line) then
2429 ! Single-line selection
2430 line = buffer_get_line(buffer, start_line)
2431 cursor%line = start_line
2432 cursor%column = start_col
2433 do i = start_col, end_col - 1
2434 call buffer_delete_at_cursor(buffer, cursor)
2435 end do
2436 if (allocated(line)) deallocate(line)
2437 else
2438 ! Multi-line selection
2439 ! Delete from start_col to end of first line
2440 cursor%line = start_line
2441 cursor%column = start_col
2442 line = buffer_get_line(buffer, start_line)
2443 do i = start_col, len(line)
2444 call buffer_delete_at_cursor(buffer, cursor)
2445 end do
2446 if (allocated(line)) deallocate(line)
2447
2448 ! Delete entire lines in between
2449 do i = start_line + 1, end_line - 1
2450 ! After deleting from first line, the next line moves up
2451 ! So we keep deleting line at position start_line + 1
2452 if (buffer_get_line_count(buffer) > start_line) then
2453 ! Delete the newline to join with next line
2454 line = buffer_get_line(buffer, start_line)
2455 cursor%column = len(line) + 1
2456 call buffer_delete_at_cursor(buffer, cursor) ! Delete newline
2457 if (allocated(line)) deallocate(line)
2458
2459 ! Delete all content of the joined line
2460 line = buffer_get_line(buffer, start_line)
2461 cursor%column = len(line)
2462 do while (cursor%column > start_col .and. cursor%column > 0)
2463 call buffer_delete_at_cursor(buffer, cursor)
2464 cursor%column = cursor%column - 1
2465 end do
2466 if (allocated(line)) deallocate(line)
2467 end if
2468 end do
2469
2470 ! Delete from beginning of last line to end_col
2471 if (buffer_get_line_count(buffer) > start_line) then
2472 line = buffer_get_line(buffer, start_line)
2473 cursor%column = len(line) + 1
2474 call buffer_delete_at_cursor(buffer, cursor) ! Delete newline
2475 if (allocated(line)) deallocate(line)
2476
2477 ! Delete from start to end_col
2478 cursor%column = start_col
2479 do i = 1, end_col - 1
2480 if (cursor%column <= buffer_get_line_count(buffer)) then
2481 call buffer_delete_at_cursor(buffer, cursor)
2482 end if
2483 end do
2484 end if
2485
2486 cursor%line = start_line
2487 cursor%column = start_col
2488 end if
2489
2490 cursor%has_selection = .false.
2491 end subroutine delete_selection
2492
2493 function get_selection_text(cursor, buffer) result(text)
2494 type(cursor_t), intent(in) :: cursor
2495 type(buffer_t), intent(in) :: buffer
2496 character(len=:), allocatable :: text
2497 integer :: start_line, start_col, end_line, end_col
2498 integer :: i
2499 character(len=:), allocatable :: line
2500
2501 if (.not. cursor%has_selection) then
2502 allocate(character(len=0) :: text)
2503 return
2504 end if
2505
2506 ! Determine start and end of selection
2507 if (cursor%line < cursor%selection_start_line .or. &
2508 (cursor%line == cursor%selection_start_line .and. &
2509 cursor%column < cursor%selection_start_col)) then
2510 start_line = cursor%line
2511 start_col = cursor%column
2512 end_line = cursor%selection_start_line
2513 end_col = cursor%selection_start_col
2514 else
2515 start_line = cursor%selection_start_line
2516 start_col = cursor%selection_start_col
2517 end_line = cursor%line
2518 end_col = cursor%column
2519 end if
2520
2521 ! Extract text based on selection
2522 if (start_line == end_line) then
2523 ! Single-line selection
2524 line = buffer_get_line(buffer, start_line)
2525 if (allocated(line) .and. start_col <= len(line) + 1 .and. end_col <= len(line) + 1) then
2526 if (end_col > start_col) then
2527 allocate(character(len=end_col - start_col) :: text)
2528 text = line(start_col:end_col - 1)
2529 else
2530 allocate(character(len=0) :: text)
2531 end if
2532 else
2533 allocate(character(len=0) :: text)
2534 end if
2535 if (allocated(line)) deallocate(line)
2536 else
2537 ! Multi-line selection
2538 text = ""
2539
2540 ! First line (from start_col to end)
2541 line = buffer_get_line(buffer, start_line)
2542 if (allocated(line)) then
2543 if (start_col <= len(line)) then
2544 text = text // line(start_col:)
2545 end if
2546 text = text // char(10) ! newline
2547 deallocate(line)
2548 end if
2549
2550 ! Middle lines (complete lines)
2551 do i = start_line + 1, end_line - 1
2552 line = buffer_get_line(buffer, i)
2553 if (allocated(line)) then
2554 text = text // line // char(10)
2555 deallocate(line)
2556 end if
2557 end do
2558
2559 ! Last line (from beginning to end_col)
2560 line = buffer_get_line(buffer, end_line)
2561 if (allocated(line)) then
2562 if (end_col > 1 .and. end_col <= len(line) + 1) then
2563 text = text // line(1:end_col - 1)
2564 end if
2565 deallocate(line)
2566 end if
2567 end if
2568 end function get_selection_text
2569
2570 subroutine sort_cursors_by_position(editor)
2571 type(editor_state_t), intent(inout) :: editor
2572 type(cursor_t) :: temp
2573 integer :: i, j
2574 logical :: swapped
2575
2576 ! Simple bubble sort for small number of cursors
2577 do i = 1, size(editor%cursors) - 1
2578 swapped = .false.
2579 do j = 1, size(editor%cursors) - i
2580 if (editor%cursors(j)%line > editor%cursors(j+1)%line .or. &
2581 (editor%cursors(j)%line == editor%cursors(j+1)%line .and. &
2582 editor%cursors(j)%column > editor%cursors(j+1)%column)) then
2583 temp = editor%cursors(j)
2584 editor%cursors(j) = editor%cursors(j+1)
2585 editor%cursors(j+1) = temp
2586 if (editor%active_cursor == j) then
2587 editor%active_cursor = j + 1
2588 else if (editor%active_cursor == j + 1) then
2589 editor%active_cursor = j
2590 end if
2591 swapped = .true.
2592 end if
2593 end do
2594 if (.not. swapped) exit
2595 end do
2596 end subroutine sort_cursors_by_position
2597
2598 subroutine deduplicate_cursors(editor)
2599 type(editor_state_t), intent(inout) :: editor
2600 type(cursor_t), allocatable :: unique_cursors(:)
2601 integer :: i, j, unique_count, duplicate_of
2602 logical :: is_duplicate
2603 integer, allocatable :: old_to_new_map(:)
2604
2605 if (size(editor%cursors) <= 1) return
2606
2607 ! Allocate mapping from old cursor indices to new indices
2608 allocate(old_to_new_map(size(editor%cursors)))
2609 old_to_new_map = 0
2610
2611 ! Count unique cursors
2612 unique_count = 0
2613 do i = 1, size(editor%cursors)
2614 is_duplicate = .false.
2615 do j = 1, i-1
2616 if (editor%cursors(i)%line == editor%cursors(j)%line .and. &
2617 editor%cursors(i)%column == editor%cursors(j)%column) then
2618 is_duplicate = .true.
2619 exit
2620 end if
2621 end do
2622 if (.not. is_duplicate) then
2623 unique_count = unique_count + 1
2624 end if
2625 end do
2626
2627 ! If we have duplicates, create new array with only unique cursors
2628 if (unique_count < size(editor%cursors)) then
2629 allocate(unique_cursors(unique_count))
2630 unique_count = 0
2631 do i = 1, size(editor%cursors)
2632 is_duplicate = .false.
2633 duplicate_of = 0
2634 do j = 1, i-1
2635 if (editor%cursors(i)%line == editor%cursors(j)%line .and. &
2636 editor%cursors(i)%column == editor%cursors(j)%column) then
2637 is_duplicate = .true.
2638 duplicate_of = j
2639 exit
2640 end if
2641 end do
2642 if (.not. is_duplicate) then
2643 unique_count = unique_count + 1
2644 unique_cursors(unique_count) = editor%cursors(i)
2645 old_to_new_map(i) = unique_count
2646 else
2647 ! This cursor is a duplicate of an earlier one
2648 ! Map it to the same new index as the earlier cursor
2649 old_to_new_map(i) = old_to_new_map(duplicate_of)
2650 end if
2651 end do
2652
2653 ! Update active cursor using the mapping
2654 if (editor%active_cursor > 0 .and. editor%active_cursor <= size(old_to_new_map)) then
2655 editor%active_cursor = old_to_new_map(editor%active_cursor)
2656 end if
2657 ! Ensure active_cursor is valid
2658 if (editor%active_cursor < 1 .or. editor%active_cursor > unique_count) then
2659 editor%active_cursor = 1
2660 end if
2661
2662 deallocate(editor%cursors)
2663 editor%cursors = unique_cursors
2664 deallocate(old_to_new_map)
2665 end if
2666 end subroutine deduplicate_cursors
2667
2668 subroutine join_line_with_previous(cursor, buffer)
2669 type(cursor_t), intent(inout) :: cursor
2670 type(buffer_t), intent(inout) :: buffer
2671 character(len=:), allocatable :: prev_line
2672 integer :: new_column
2673
2674 prev_line = buffer_get_line(buffer, cursor%line - 1)
2675 new_column = len(prev_line) + 1
2676
2677 ! Move to end of previous line
2678 cursor%line = cursor%line - 1
2679 cursor%column = new_column
2680
2681 ! Delete the newline
2682 call buffer_delete_at_cursor(buffer, cursor)
2683
2684 cursor%desired_column = cursor%column
2685 if (allocated(prev_line)) deallocate(prev_line)
2686 end subroutine join_line_with_previous
2687
2688 subroutine join_line_with_next(cursor, buffer)
2689 type(cursor_t), intent(inout) :: cursor
2690 type(buffer_t), intent(inout) :: buffer
2691
2692 ! Delete the newline at end of current line
2693 call buffer_delete_at_cursor(buffer, cursor)
2694 end subroutine join_line_with_next
2695
2696 subroutine kill_line_forward(cursor, buffer)
2697 type(cursor_t), intent(inout) :: cursor
2698 type(buffer_t), intent(inout) :: buffer
2699 character(len=:), allocatable :: line
2700 character(len=:), allocatable :: killed_text
2701 integer :: i
2702
2703 cursor%has_selection = .false. ! Clear selection
2704
2705 line = buffer_get_line(buffer, cursor%line)
2706
2707 if (cursor%column <= len(line)) then
2708 ! Kill from cursor to end of line
2709 killed_text = line(cursor%column:)
2710 do i = cursor%column, len(line)
2711 call buffer_delete_at_cursor(buffer, cursor)
2712 end do
2713 else
2714 ! At end of line - kill the newline
2715 killed_text = char(10) ! newline
2716 call buffer_delete_at_cursor(buffer, cursor)
2717 end if
2718
2719 ! Add to yank stack
2720 if (len(killed_text) > 0) then
2721 call push_yank(yank_stack, killed_text)
2722 end if
2723
2724 buffer%modified = .true.
2725 if (allocated(line)) deallocate(line)
2726 if (allocated(killed_text)) deallocate(killed_text)
2727 end subroutine kill_line_forward
2728
2729 subroutine kill_line_backward(cursor, buffer)
2730 type(cursor_t), intent(inout) :: cursor
2731 type(buffer_t), intent(inout) :: buffer
2732 character(len=:), allocatable :: line
2733 character(len=:), allocatable :: killed_text
2734 integer :: i, start_col
2735
2736 cursor%has_selection = .false. ! Clear selection
2737
2738 line = buffer_get_line(buffer, cursor%line)
2739 start_col = cursor%column
2740
2741 if (cursor%column > 1) then
2742 ! Kill from start of line to cursor
2743 killed_text = line(1:cursor%column-1)
2744 cursor%column = 1
2745 do i = 1, start_col - 1
2746 call buffer_delete_at_cursor(buffer, cursor)
2747 end do
2748 cursor%desired_column = 1
2749 end if
2750
2751 ! Add to yank stack
2752 if (allocated(killed_text)) then
2753 if (len(killed_text) > 0) then
2754 call push_yank(yank_stack, killed_text)
2755 end if
2756 end if
2757
2758 buffer%modified = .true.
2759 if (allocated(line)) deallocate(line)
2760 if (allocated(killed_text)) deallocate(killed_text)
2761 end subroutine kill_line_backward
2762
2763 subroutine yank_text(cursor, buffer)
2764 type(cursor_t), intent(inout) :: cursor
2765 type(buffer_t), intent(inout) :: buffer
2766 character(len=:), allocatable :: text
2767 integer :: i
2768
2769 text = pop_yank(yank_stack)
2770 if (allocated(text)) then
2771 do i = 1, len(text)
2772 if (text(i:i) == char(10)) then
2773 call buffer_insert_newline(buffer, cursor)
2774 cursor%line = cursor%line + 1
2775 cursor%column = 1
2776 else
2777 call buffer_insert_char(buffer, cursor, text(i:i))
2778 cursor%column = cursor%column + 1
2779 end if
2780 end do
2781 cursor%desired_column = cursor%column
2782 buffer%modified = .true.
2783 deallocate(text)
2784 end if
2785 end subroutine yank_text
2786
2787 subroutine move_line_up(cursor, buffer)
2788 type(cursor_t), intent(inout) :: cursor
2789 type(buffer_t), intent(inout) :: buffer
2790 character(len=:), allocatable :: current_line, prev_line
2791 integer :: saved_column, original_line, total_lines
2792
2793 if (cursor%line <= 1) return
2794
2795 ! Save state
2796 saved_column = cursor%column
2797 original_line = cursor%line
2798 total_lines = buffer_get_line_count(buffer)
2799
2800 ! Get both lines
2801 current_line = buffer_get_line(buffer, cursor%line)
2802 prev_line = buffer_get_line(buffer, cursor%line - 1)
2803
2804 ! Delete current line entirely (including newline)
2805 cursor%column = 1
2806 call delete_entire_line(buffer, cursor)
2807
2808 ! Move to previous line (now current_line position after delete)
2809 cursor%line = cursor%line - 1
2810 cursor%column = 1
2811
2812 ! Delete previous line entirely (including newline)
2813 call delete_entire_line(buffer, cursor)
2814
2815 ! Now insert current_line first, then prev_line
2816 cursor%column = 1
2817 call insert_line_text(buffer, cursor, current_line)
2818 call buffer_insert_newline(buffer, cursor)
2819
2820 cursor%line = cursor%line + 1
2821 cursor%column = 1
2822 call insert_line_text(buffer, cursor, prev_line)
2823 ! Add newline if we're not at the last line
2824 if (original_line < total_lines) then
2825 call buffer_insert_newline(buffer, cursor)
2826 end if
2827
2828 ! Restore cursor to moved line
2829 cursor%line = cursor%line - 1
2830 cursor%column = min(saved_column, len(current_line) + 1)
2831 cursor%desired_column = cursor%column
2832
2833 buffer%modified = .true.
2834 if (allocated(current_line)) deallocate(current_line)
2835 if (allocated(prev_line)) deallocate(prev_line)
2836 end subroutine move_line_up
2837
2838 subroutine move_line_down(cursor, buffer)
2839 type(cursor_t), intent(inout) :: cursor
2840 type(buffer_t), intent(inout) :: buffer
2841 character(len=:), allocatable :: current_line, next_line
2842 integer :: line_count, saved_column, original_line, total_lines
2843
2844 line_count = buffer_get_line_count(buffer)
2845 if (cursor%line >= line_count) return
2846
2847 ! Save state
2848 saved_column = cursor%column
2849 original_line = cursor%line
2850 total_lines = line_count
2851
2852 ! Get both lines
2853 current_line = buffer_get_line(buffer, cursor%line)
2854 next_line = buffer_get_line(buffer, cursor%line + 1)
2855
2856 ! Delete current line entirely (including newline)
2857 cursor%column = 1
2858 call delete_entire_line(buffer, cursor)
2859
2860 ! Delete next line entirely (including newline)
2861 ! After deleting current line, next line is now at cursor%line
2862 cursor%column = 1
2863 call delete_entire_line(buffer, cursor)
2864
2865 ! Now insert next_line first, then current_line
2866 cursor%column = 1
2867 call insert_line_text(buffer, cursor, next_line)
2868 call buffer_insert_newline(buffer, cursor)
2869
2870 cursor%line = cursor%line + 1
2871 cursor%column = 1
2872 call insert_line_text(buffer, cursor, current_line)
2873 ! Add newline if we're not at the last line
2874 if (original_line + 1 < total_lines) then
2875 call buffer_insert_newline(buffer, cursor)
2876 end if
2877
2878 ! Restore cursor position on moved line
2879 ! Current line is now at cursor%line (which is original_line + 1)
2880 ! So cursor is already on the moved line, just need to fix column
2881 cursor%column = min(saved_column, len(current_line) + 1)
2882 cursor%desired_column = cursor%column
2883
2884 buffer%modified = .true.
2885 if (allocated(current_line)) deallocate(current_line)
2886 if (allocated(next_line)) deallocate(next_line)
2887 end subroutine move_line_down
2888
2889 subroutine duplicate_line_up(cursor, buffer)
2890 type(cursor_t), intent(inout) :: cursor
2891 type(buffer_t), intent(inout) :: buffer
2892 character(len=:), allocatable :: line
2893
2894 line = buffer_get_line(buffer, cursor%line)
2895
2896 ! Move to start of line
2897 cursor%column = 1
2898 ! Insert newline before
2899 call buffer_insert_newline(buffer, cursor)
2900 ! Insert the duplicated text
2901 call insert_line_text(buffer, cursor, line)
2902 ! Stay on original line
2903 cursor%line = cursor%line + 1
2904
2905 buffer%modified = .true.
2906 if (allocated(line)) deallocate(line)
2907 end subroutine duplicate_line_up
2908
2909 subroutine duplicate_line_down(cursor, buffer)
2910 type(cursor_t), intent(inout) :: cursor
2911 type(buffer_t), intent(inout) :: buffer
2912 character(len=:), allocatable :: line
2913 integer :: saved_column
2914
2915 line = buffer_get_line(buffer, cursor%line)
2916 saved_column = cursor%column
2917
2918 ! Move to end of line
2919 cursor%column = len(line) + 1
2920 ! Insert newline
2921 call buffer_insert_newline(buffer, cursor)
2922 cursor%line = cursor%line + 1
2923 cursor%column = 1
2924 ! Insert the duplicated text
2925 call insert_line_text(buffer, cursor, line)
2926
2927 ! Return to original position
2928 cursor%line = cursor%line - 1
2929 cursor%column = saved_column
2930 cursor%desired_column = saved_column
2931
2932 buffer%modified = .true.
2933 if (allocated(line)) deallocate(line)
2934 end subroutine duplicate_line_down
2935
2936 subroutine delete_entire_line(buffer, cursor)
2937 type(buffer_t), intent(inout) :: buffer
2938 type(cursor_t), intent(inout) :: cursor
2939 character(len=:), allocatable :: line
2940 integer :: i
2941
2942 line = buffer_get_line(buffer, cursor%line)
2943 cursor%column = 1
2944
2945 ! Delete all characters in line
2946 do i = 1, len(line)
2947 call buffer_delete_at_cursor(buffer, cursor)
2948 end do
2949
2950 ! Delete the newline if not the last line
2951 if (cursor%line < buffer_get_line_count(buffer)) then
2952 call buffer_delete_at_cursor(buffer, cursor)
2953 end if
2954
2955 if (allocated(line)) deallocate(line)
2956 end subroutine delete_entire_line
2957
2958 subroutine insert_line_text(buffer, cursor, text)
2959 type(buffer_t), intent(inout) :: buffer
2960 type(cursor_t), intent(inout) :: cursor
2961 character(len=*), intent(in) :: text
2962 integer :: i
2963
2964 do i = 1, len(text)
2965 call buffer_insert_char(buffer, cursor, text(i:i))
2966 cursor%column = cursor%column + 1
2967 end do
2968 end subroutine insert_line_text
2969
2970 function buffer_get_char_at(buffer, pos) result(ch)
2971 type(buffer_t), intent(in) :: buffer
2972 integer, intent(in) :: pos
2973 character :: ch
2974
2975 if (pos < buffer%gap_start) then
2976 ch = buffer%data(pos:pos)
2977 else
2978 ch = buffer%data(pos + (buffer%gap_end - buffer%gap_start):&
2979 pos + (buffer%gap_end - buffer%gap_start))
2980 end if
2981 end function buffer_get_char_at
2982
2983 subroutine cut_selection_or_line(cursor, buffer)
2984 type(cursor_t), intent(inout) :: cursor
2985 type(buffer_t), intent(inout) :: buffer
2986 character(len=:), allocatable :: text
2987
2988 if (cursor%has_selection) then
2989 ! Get selected text
2990 text = get_selection_text(cursor, buffer)
2991
2992 ! Copy to clipboard
2993 if (allocated(text)) then
2994 call copy_to_clipboard(text)
2995 end if
2996
2997 ! Delete the selection
2998 call delete_selection(cursor, buffer)
2999 else
3000 ! Get current line
3001 text = buffer_get_line(buffer, cursor%line)
3002
3003 ! Copy to clipboard
3004 call copy_to_clipboard(text)
3005
3006 ! Delete the line
3007 cursor%column = 1
3008 call delete_entire_line(buffer, cursor)
3009 end if
3010
3011 buffer%modified = .true.
3012 if (allocated(text)) deallocate(text)
3013 end subroutine cut_selection_or_line
3014
3015 subroutine copy_selection_or_line(cursor, buffer)
3016 type(cursor_t), intent(in) :: cursor
3017 type(buffer_t), intent(in) :: buffer
3018 character(len=:), allocatable :: text
3019
3020 if (cursor%has_selection) then
3021 ! Get selected text (only what's selected, no automatic newlines)
3022 text = get_selection_text(cursor, buffer)
3023 else
3024 ! Get current line - don't add newline, user can select it if they want it
3025 text = buffer_get_line(buffer, cursor%line)
3026 end if
3027
3028 ! Copy to clipboard
3029 if (allocated(text)) then
3030 call copy_to_clipboard(text)
3031 deallocate(text)
3032 end if
3033 end subroutine copy_selection_or_line
3034
3035 subroutine paste_clipboard(cursor, buffer)
3036 type(cursor_t), intent(inout) :: cursor
3037 type(buffer_t), intent(inout) :: buffer
3038 character(len=:), allocatable :: text
3039 integer :: i
3040
3041 ! Get text from clipboard
3042 text = paste_from_clipboard()
3043
3044 if (allocated(text)) then
3045 ! Insert text at cursor position
3046 do i = 1, len(text)
3047 if (text(i:i) == char(10)) then
3048 call buffer_insert_newline(buffer, cursor)
3049 cursor%line = cursor%line + 1
3050 cursor%column = 1
3051 else
3052 call buffer_insert_char(buffer, cursor, text(i:i))
3053 cursor%column = cursor%column + 1
3054 end if
3055 end do
3056 cursor%desired_column = cursor%column
3057 buffer%modified = .true.
3058 deallocate(text)
3059 end if
3060 end subroutine paste_clipboard
3061
3062 subroutine save_file(editor, buffer)
3063 use text_prompt_module, only: show_text_prompt
3064 use lsp_server_manager_module, only: notify_file_saved
3065 use text_buffer_module, only: buffer_to_string
3066 type(editor_state_t), intent(inout) :: editor
3067 type(buffer_t), intent(inout) :: buffer
3068 integer :: ios, tab_idx
3069 character(len=256) :: temp_filename, command
3070 character(len=512) :: new_filename
3071 logical :: file_exists, cancelled
3072
3073 if (.not. allocated(editor%filename)) return
3074
3075 ! Check if this is an untitled file - prompt for filename
3076 if (index(editor%filename, '[Untitled') == 1) then
3077 call show_text_prompt('Save as: ', new_filename, cancelled, editor%screen_rows)
3078
3079 if (cancelled .or. len_trim(new_filename) == 0) then
3080 ! User cancelled or entered empty filename
3081 call terminal_move_cursor(editor%screen_rows, 1)
3082 call terminal_write('Save cancelled')
3083 return
3084 end if
3085
3086 ! Update editor filename
3087 if (allocated(editor%filename)) deallocate(editor%filename)
3088 allocate(character(len=len_trim(new_filename)) :: editor%filename)
3089 editor%filename = trim(new_filename)
3090
3091 ! Update tab filename if in workspace mode
3092 if (allocated(editor%tabs) .and. editor%active_tab_index > 0) then
3093 tab_idx = editor%active_tab_index
3094 if (tab_idx <= size(editor%tabs)) then
3095 if (allocated(editor%tabs(tab_idx)%filename)) then
3096 deallocate(editor%tabs(tab_idx)%filename)
3097 end if
3098 allocate(character(len=len_trim(new_filename)) :: editor%tabs(tab_idx)%filename)
3099 editor%tabs(tab_idx)%filename = trim(new_filename)
3100
3101 ! Update pane filename
3102 if (allocated(editor%tabs(tab_idx)%panes)) then
3103 if (size(editor%tabs(tab_idx)%panes) > 0) then
3104 if (allocated(editor%tabs(tab_idx)%panes(1)%filename)) then
3105 deallocate(editor%tabs(tab_idx)%panes(1)%filename)
3106 end if
3107 allocate(character(len=len_trim(new_filename)) :: &
3108 editor%tabs(tab_idx)%panes(1)%filename)
3109 editor%tabs(tab_idx)%panes(1)%filename = trim(new_filename)
3110 end if
3111 end if
3112 end if
3113 end if
3114 end if
3115
3116 ! First try normal save
3117 call buffer_save_file(buffer, editor%filename, ios)
3118
3119 if (ios == 0) then
3120 buffer%modified = .false.
3121
3122 ! Send LSP didSave notification if LSP is active
3123 if (allocated(editor%tabs) .and. editor%active_tab_index > 0) then
3124 tab_idx = editor%active_tab_index
3125 if (tab_idx <= size(editor%tabs)) then
3126 if (editor%tabs(tab_idx)%lsp_server_index > 0) then
3127 call notify_file_saved(editor%lsp_manager, &
3128 editor%tabs(tab_idx)%lsp_server_index, &
3129 trim(editor%filename), buffer_to_string(buffer))
3130 end if
3131 end if
3132 end if
3133
3134 return
3135 end if
3136
3137 ! Check if file exists and we have write permission
3138 inquire(file=editor%filename, exist=file_exists)
3139
3140 ! If save failed, try sudo save
3141 write(temp_filename, '(a,i0)') '/tmp/facsimile_sudo_', get_process_id()
3142
3143 ! Save to temporary file
3144 call buffer_save_file(buffer, temp_filename, ios)
3145 if (ios /= 0) then
3146 ! Can't even save to /tmp, serious problem
3147 write(error_unit, *) 'Error: Cannot save file even to /tmp'
3148 return
3149 end if
3150
3151 ! Use sudo to move the file
3152 write(command, '(a,a,a,a,a)') 'sudo mv ', trim(temp_filename), ' ', &
3153 trim(editor%filename), ' 2>/dev/null'
3154
3155 ! Show message to user
3156 call terminal_move_cursor(editor%screen_rows, 1)
3157 call terminal_write('[sudo] password required to save file')
3158
3159 ! Execute sudo command
3160 call execute_command_line(command, exitstat=ios)
3161
3162 if (ios == 0) then
3163 buffer%modified = .false.
3164 call terminal_move_cursor(editor%screen_rows, 1)
3165 call terminal_write('File saved with sudo ')
3166
3167 ! Send LSP didSave notification if LSP is active
3168 if (allocated(editor%tabs) .and. editor%active_tab_index > 0) then
3169 tab_idx = editor%active_tab_index
3170 if (tab_idx <= size(editor%tabs)) then
3171 if (editor%tabs(tab_idx)%lsp_server_index > 0) then
3172 call notify_file_saved(editor%lsp_manager, &
3173 editor%tabs(tab_idx)%lsp_server_index, &
3174 trim(editor%filename), buffer_to_string(buffer))
3175 end if
3176 end if
3177 end if
3178 else
3179 ! Clean up temp file
3180 write(command, '(a,a)') 'rm -f ', trim(temp_filename)
3181 call execute_command_line(command)
3182 call terminal_move_cursor(editor%screen_rows, 1)
3183 call terminal_write('Save failed - permission denied ')
3184 end if
3185 end subroutine save_file
3186
3187 function get_process_id() result(pid)
3188 integer :: pid
3189 interface
3190 function c_getpid() bind(C, name="getpid")
3191 use iso_c_binding, only: c_int
3192 integer(c_int) :: c_getpid
3193 end function
3194 end interface
3195 pid = c_getpid()
3196 end function get_process_id
3197
3198 subroutine cycle_quotes(cursor, buffer)
3199 type(cursor_t), intent(inout) :: cursor
3200 type(buffer_t), intent(inout) :: buffer
3201 character(len=:), allocatable :: line
3202 integer :: quote_start, quote_end
3203 character :: current_quote, new_quote
3204
3205 line = buffer_get_line(buffer, cursor%line)
3206
3207 ! Find surrounding quotes
3208 call find_surrounding_quotes(line, cursor%column, quote_start, quote_end, current_quote)
3209
3210 if (quote_start > 0 .and. quote_end > 0) then
3211 ! Determine next quote type
3212 select case(current_quote)
3213 case('"')
3214 new_quote = "'"
3215 case("'")
3216 new_quote = '`'
3217 case('`')
3218 new_quote = '"'
3219 case default
3220 return
3221 end select
3222
3223 ! Replace quotes
3224 cursor%column = quote_start
3225 call buffer_delete_at_cursor(buffer, cursor)
3226 call buffer_insert_char(buffer, cursor, new_quote)
3227
3228 cursor%column = quote_end
3229 call buffer_delete_at_cursor(buffer, cursor)
3230 call buffer_insert_char(buffer, cursor, new_quote)
3231
3232 ! Restore cursor position
3233 cursor%column = quote_end
3234 buffer%modified = .true.
3235 end if
3236
3237 if (allocated(line)) deallocate(line)
3238 end subroutine cycle_quotes
3239
3240 subroutine find_surrounding_quotes(line, pos, start_pos, end_pos, quote_char)
3241 character(len=*), intent(in) :: line
3242 integer, intent(in) :: pos
3243 integer, intent(out) :: start_pos, end_pos
3244 character, intent(out) :: quote_char
3245 integer :: i
3246
3247 start_pos = 0
3248 end_pos = 0
3249 quote_char = ' '
3250
3251 ! Search backward for opening quote
3252 do i = pos - 1, 1, -1
3253 if (line(i:i) == '"' .or. line(i:i) == "'" .or. line(i:i) == '`') then
3254 start_pos = i
3255 quote_char = line(i:i)
3256 exit
3257 end if
3258 end do
3259
3260 if (start_pos > 0) then
3261 ! Search forward for closing quote
3262 do i = pos, len(line)
3263 if (line(i:i) == quote_char) then
3264 end_pos = i
3265 exit
3266 end if
3267 end do
3268 end if
3269 end subroutine find_surrounding_quotes
3270
3271 subroutine remove_brackets(cursor, buffer)
3272 type(cursor_t), intent(inout) :: cursor
3273 type(buffer_t), intent(inout) :: buffer
3274 character(len=:), allocatable :: line
3275 integer :: bracket_start, bracket_end
3276 character :: open_bracket, close_bracket
3277
3278 line = buffer_get_line(buffer, cursor%line)
3279
3280 ! Find surrounding brackets
3281 call find_surrounding_brackets(line, cursor%column, bracket_start, bracket_end, &
3282 open_bracket, close_bracket)
3283
3284 if (bracket_start > 0 .and. bracket_end > 0) then
3285 ! Delete closing bracket first (to maintain positions)
3286 cursor%column = bracket_end
3287 call buffer_delete_at_cursor(buffer, cursor)
3288
3289 ! Delete opening bracket
3290 cursor%column = bracket_start
3291 call buffer_delete_at_cursor(buffer, cursor)
3292
3293 buffer%modified = .true.
3294 end if
3295
3296 if (allocated(line)) deallocate(line)
3297 end subroutine remove_brackets
3298
3299 subroutine find_surrounding_brackets(line, pos, start_pos, end_pos, open_br, close_br)
3300 character(len=*), intent(in) :: line
3301 integer, intent(in) :: pos
3302 integer, intent(out) :: start_pos, end_pos
3303 character, intent(out) :: open_br, close_br
3304 integer :: i
3305
3306 start_pos = 0
3307 end_pos = 0
3308 open_br = ' '
3309 close_br = ' '
3310
3311 ! Search backward for opening bracket
3312 do i = pos - 1, 1, -1
3313 select case(line(i:i))
3314 case('(')
3315 start_pos = i
3316 open_br = '('
3317 close_br = ')'
3318 exit
3319 case('[')
3320 start_pos = i
3321 open_br = '['
3322 close_br = ']'
3323 exit
3324 case('{')
3325 start_pos = i
3326 open_br = '{'
3327 close_br = '}'
3328 exit
3329 end select
3330 end do
3331
3332 if (start_pos > 0) then
3333 ! Search forward for matching closing bracket
3334 do i = pos, len(line)
3335 if (line(i:i) == close_br) then
3336 end_pos = i
3337 exit
3338 end if
3339 end do
3340 end if
3341 end subroutine find_surrounding_brackets
3342
3343 subroutine init_cursor(cursor)
3344 type(cursor_t), intent(out) :: cursor
3345 cursor%line = 1
3346 cursor%column = 1
3347 cursor%desired_column = 1
3348 cursor%has_selection = .false.
3349 cursor%selection_start_line = 1
3350 cursor%selection_start_col = 1
3351 end subroutine init_cursor
3352
3353 subroutine handle_mouse_event_action(key_str, editor, buffer)
3354 use editor_state_module, only: get_active_pane_indices
3355 character(len=*), intent(in) :: key_str
3356 type(editor_state_t), intent(inout) :: editor
3357 type(buffer_t), intent(in) :: buffer
3358 integer :: button, row, col
3359 integer :: colon1, colon2, colon3
3360 character(len=100) :: event_type
3361 integer :: ios, line_count
3362 type(cursor_t), allocatable :: new_cursors(:)
3363 integer :: i, cursor_exists
3364 ! Variables for mouse drag handling
3365 logical :: had_selection, in_active_pane
3366 integer :: selection_start_line, selection_start_col
3367 integer :: tab_idx, pane_idx
3368
3369 line_count = buffer_get_line_count(buffer)
3370
3371 ! Parse the mouse event string format: "mouse-type:button:row:col"
3372 colon1 = index(key_str, ':')
3373 if (colon1 == 0) return
3374
3375 event_type = key_str(1:colon1-1)
3376 colon2 = index(key_str(colon1+1:), ':') + colon1
3377 if (colon2 == colon1) return
3378
3379 colon3 = index(key_str(colon2+1:), ':') + colon2
3380 if (colon3 == colon2) return
3381
3382 ! Parse button, row, and col
3383 read(key_str(colon1+1:colon2-1), '(i10)', iostat=ios) button
3384 if (ios /= 0) return
3385
3386 read(key_str(colon2+1:colon3-1), '(i10)', iostat=ios) row
3387 if (ios /= 0) return
3388
3389 read(key_str(colon3+1:), '(i10)', iostat=ios) col
3390 if (ios /= 0) return
3391
3392 ! Handle different mouse event types
3393 select case(trim(event_type))
3394 case('mouse-click')
3395 ! Regular click - move cursor to position
3396 if (button == 0) then ! Left click
3397 ! Clear other cursors first (single cursor mode)
3398 if (allocated(editor%cursors)) then
3399 if (size(editor%cursors) > 1) then
3400 deallocate(editor%cursors)
3401 allocate(editor%cursors(1))
3402 call init_cursor(editor%cursors(1))
3403 editor%active_cursor = 1
3404 end if
3405 end if
3406
3407 ! Now position cursor (this might switch panes and update cursors)
3408 call position_cursor_at_screen(editor%active_cursor, &
3409 editor, buffer, row, col)
3410
3411 ! Clear selection after positioning (cursor array is now stable)
3412 if (allocated(editor%cursors) .and. editor%active_cursor > 0 .and. &
3413 editor%active_cursor <= size(editor%cursors)) then
3414 editor%cursors(editor%active_cursor)%has_selection = .false.
3415 end if
3416 end if
3417
3418 case('mouse-drag')
3419 ! Mouse drag - extend selection within current pane only
3420 ! We shouldn't switch panes while dragging, only move cursor within current pane
3421
3422 ! Check if we're in the current active pane - don't switch panes during drag
3423 call get_active_pane_indices(editor, tab_idx, pane_idx)
3424 in_active_pane = .false.
3425
3426 if (tab_idx > 0 .and. pane_idx > 0 .and. allocated(editor%tabs(tab_idx)%panes)) then
3427 associate(pane => editor%tabs(tab_idx)%panes(pane_idx))
3428 ! Check if mouse is still in the active pane
3429 if (row >= pane%screen_row .and. &
3430 row < pane%screen_row + pane%screen_height .and. &
3431 col >= pane%screen_col .and. &
3432 col < pane%screen_col + pane%screen_width) then
3433 in_active_pane = .true.
3434 end if
3435 end associate
3436 end if
3437
3438 ! Only process drag if within active pane
3439 if (in_active_pane) then
3440 had_selection = editor%cursors(editor%active_cursor)%has_selection
3441 if (.not. had_selection) then
3442 ! Start selection from current position
3443 selection_start_line = editor%cursors(editor%active_cursor)%line
3444 selection_start_col = editor%cursors(editor%active_cursor)%column
3445 else
3446 selection_start_line = editor%cursors(editor%active_cursor)%selection_start_line
3447 selection_start_col = editor%cursors(editor%active_cursor)%selection_start_col
3448 end if
3449
3450 ! Move cursor to drag position (won't switch panes since we're in active pane)
3451 call position_cursor_at_screen(editor%active_cursor, &
3452 editor, buffer, row, col)
3453
3454 ! Restore/set selection state after positioning
3455 if (allocated(editor%cursors) .and. editor%active_cursor > 0 .and. &
3456 editor%active_cursor <= size(editor%cursors)) then
3457 editor%cursors(editor%active_cursor)%has_selection = .true.
3458 editor%cursors(editor%active_cursor)%selection_start_line = selection_start_line
3459 editor%cursors(editor%active_cursor)%selection_start_col = selection_start_col
3460 end if
3461
3462 call update_viewport(editor)
3463 end if
3464
3465 case('mouse-release')
3466 ! Mouse button released - nothing special to do
3467 continue
3468
3469 case('mouse-scroll-up')
3470 ! Scroll up by 3 lines
3471 editor%viewport_line = max(1, editor%viewport_line - 3)
3472 call sync_editor_to_pane(editor)
3473
3474 case('mouse-scroll-down')
3475 ! Scroll down by 3 lines
3476 editor%viewport_line = min(buffer_get_line_count(buffer) - editor%screen_rows + 2, &
3477 editor%viewport_line + 3)
3478 call sync_editor_to_pane(editor)
3479
3480 case('mouse-alt')
3481 ! Alt+click - add or remove cursor
3482 if (button == 8) then ! Alt + left click (button code includes alt modifier)
3483 ! Check if cursor already exists at this position
3484 cursor_exists = 0
3485 do i = 1, size(editor%cursors)
3486 if (is_cursor_at_screen_pos(editor%cursors(i), editor, row, col)) then
3487 cursor_exists = i
3488 exit
3489 end if
3490 end do
3491
3492 if (cursor_exists > 0) then
3493 ! Remove the cursor
3494 if (size(editor%cursors) > 1) then
3495 allocate(new_cursors(size(editor%cursors) - 1))
3496 do i = 1, cursor_exists - 1
3497 new_cursors(i) = editor%cursors(i)
3498 end do
3499 do i = cursor_exists + 1, size(editor%cursors)
3500 new_cursors(i-1) = editor%cursors(i)
3501 end do
3502 deallocate(editor%cursors)
3503 editor%cursors = new_cursors
3504 if (editor%active_cursor >= cursor_exists) then
3505 editor%active_cursor = max(1, editor%active_cursor - 1)
3506 end if
3507 end if
3508 else
3509 ! Add a new cursor
3510 allocate(new_cursors(size(editor%cursors) + 1))
3511 do i = 1, size(editor%cursors)
3512 new_cursors(i) = editor%cursors(i)
3513 end do
3514 call init_cursor(new_cursors(size(new_cursors)))
3515 ! First move the new cursors to editor
3516 deallocate(editor%cursors)
3517 editor%cursors = new_cursors
3518 editor%active_cursor = size(editor%cursors)
3519 ! Then position the new cursor using its index
3520 call position_cursor_at_screen(editor%active_cursor, &
3521 editor, buffer, row, col)
3522 end if
3523 end if
3524
3525 end select
3526 end subroutine handle_mouse_event_action
3527
3528 subroutine position_cursor_at_screen(cursor_idx, editor, buffer, screen_row, screen_col)
3529 use renderer_module, only: show_line_numbers, LINE_NUMBER_WIDTH
3530 use editor_state_module, only: get_active_pane_indices, switch_to_pane, sync_editor_to_pane
3531 integer, intent(inout) :: cursor_idx ! Use index instead of reference
3532 type(editor_state_t), intent(inout) :: editor
3533 type(buffer_t), intent(in) :: buffer
3534 integer, intent(in) :: screen_row, screen_col
3535 integer :: target_line, target_col, col_offset, row_offset
3536 character(len=:), allocatable :: line
3537 integer :: line_count
3538 integer :: tab_idx, pane_idx, i
3539 integer :: pane_row, pane_col
3540 logical :: in_pane
3541
3542 line_count = buffer_get_line_count(buffer)
3543 in_pane = .false.
3544
3545 ! Account for tab bar offset - when tabs exist, row 1 is tab bar, content starts at row 2
3546 if (size(editor%tabs) > 0) then
3547 row_offset = 2 ! Tab bar takes row 1
3548 else
3549 row_offset = 1 ! No tab bar
3550 end if
3551
3552 ! Ignore clicks on the tab bar
3553 if (size(editor%tabs) > 0 .and. screen_row < row_offset) then
3554 return ! Don't move cursor if clicking on tab bar
3555 end if
3556
3557 ! Account for line number display offset
3558 if (show_line_numbers) then
3559 col_offset = LINE_NUMBER_WIDTH + 1 ! +1 for separator space
3560 else
3561 col_offset = 0
3562 end if
3563
3564 ! Check if we're in a pane system
3565 call get_active_pane_indices(editor, tab_idx, pane_idx)
3566 if (tab_idx > 0 .and. pane_idx > 0 .and. allocated(editor%tabs(tab_idx)%panes)) then
3567 ! Find which pane was clicked
3568 do i = 1, size(editor%tabs(tab_idx)%panes)
3569 ! Check if click is within this pane's boundaries
3570 if (screen_row >= editor%tabs(tab_idx)%panes(i)%screen_row .and. &
3571 screen_row < editor%tabs(tab_idx)%panes(i)%screen_row + &
3572 editor%tabs(tab_idx)%panes(i)%screen_height .and. &
3573 screen_col >= editor%tabs(tab_idx)%panes(i)%screen_col .and. &
3574 screen_col < editor%tabs(tab_idx)%panes(i)%screen_col + &
3575 editor%tabs(tab_idx)%panes(i)%screen_width) then
3576
3577 ! If clicking on a different pane, switch to it first
3578 if (i /= pane_idx) then
3579 call switch_to_pane(editor, tab_idx, i)
3580 pane_idx = i
3581 ! Update cursor index after switching (might have changed)
3582 cursor_idx = editor%active_cursor
3583 end if
3584
3585 ! Now use the active pane's data
3586 associate(pane => editor%tabs(tab_idx)%panes(pane_idx))
3587 ! Calculate position relative to pane
3588 ! Note: screen_row is where the click occurred, pane%screen_row is top of pane
3589 ! We want 0-based offset into the pane
3590 pane_row = screen_row - pane%screen_row
3591 pane_col = screen_col - pane%screen_col + 1
3592
3593 ! Convert pane position to buffer position using pane's viewport
3594 target_line = pane%viewport_line + pane_row
3595 target_col = pane%viewport_column + max(1, pane_col - col_offset)
3596 in_pane = .true.
3597 end associate
3598 exit
3599 end if
3600 end do
3601
3602 if (.not. in_pane) then
3603 return ! Click outside of any pane
3604 end if
3605 else
3606 ! No panes, use editor viewport
3607 target_line = editor%viewport_line + screen_row - row_offset
3608 target_col = editor%viewport_column + max(1, screen_col - col_offset)
3609 end if
3610
3611 ! Clamp to valid range
3612 if (target_line < 1) target_line = 1
3613 if (target_line > line_count) target_line = line_count
3614
3615 ! Get line and adjust column to valid positions only
3616 line = buffer_get_line(buffer, target_line)
3617 if (target_col < 1) target_col = 1
3618 ! Clamp column to actual line length + 1 (position after last char)
3619 if (target_col > len(line) + 1) target_col = len(line) + 1
3620
3621 ! Set cursor position (ensure cursor_idx is valid)
3622 if (allocated(editor%cursors) .and. cursor_idx > 0 .and. cursor_idx <= size(editor%cursors)) then
3623 editor%cursors(cursor_idx)%line = target_line
3624 editor%cursors(cursor_idx)%column = target_col
3625 editor%cursors(cursor_idx)%desired_column = target_col
3626 end if
3627
3628 if (allocated(line)) deallocate(line)
3629
3630 ! Sync the updated cursor position back to the active pane
3631 call sync_editor_to_pane(editor)
3632 end subroutine position_cursor_at_screen
3633
3634 function is_cursor_at_screen_pos(cursor, editor, screen_row, screen_col) result(at_pos)
3635 type(cursor_t), intent(in) :: cursor
3636 type(editor_state_t), intent(in) :: editor
3637 integer, intent(in) :: screen_row, screen_col
3638 logical :: at_pos
3639 integer :: cursor_screen_row, cursor_screen_col, row_offset
3640
3641 ! Account for tab bar - when tabs exist, content starts at row 2
3642 if (size(editor%tabs) > 0) then
3643 row_offset = 2
3644 else
3645 row_offset = 1
3646 end if
3647
3648 cursor_screen_row = cursor%line - editor%viewport_line + row_offset
3649 cursor_screen_col = cursor%column - editor%viewport_column + 1
3650 at_pos = (cursor_screen_row == screen_row .and. cursor_screen_col == screen_col)
3651 end function is_cursor_at_screen_pos
3652
3653 subroutine select_next_match(editor, buffer)
3654 type(editor_state_t), intent(inout) :: editor
3655 type(buffer_t), intent(inout) :: buffer
3656 type(cursor_t), allocatable :: new_cursors(:)
3657 character(len=:), allocatable :: word
3658 integer :: i
3659 integer :: found_line, found_col
3660 logical :: found
3661
3662 ! If no pattern selected yet, select word at cursor
3663 if (.not. allocated(search_pattern)) then
3664 call select_word_at_cursor(editor%cursors(editor%active_cursor), buffer)
3665 word = get_selected_text(editor%cursors(editor%active_cursor), buffer)
3666 if (allocated(word)) then
3667 search_pattern = word
3668 end if
3669 else
3670 ! Search for next occurrence
3671 call find_next_occurrence(buffer, search_pattern, &
3672 editor%cursors(size(editor%cursors))%line, &
3673 editor%cursors(size(editor%cursors))%column, &
3674 found, found_line, found_col)
3675
3676 if (found) then
3677 ! Add a new cursor at the found position
3678 allocate(new_cursors(size(editor%cursors) + 1))
3679 do i = 1, size(editor%cursors)
3680 new_cursors(i) = editor%cursors(i)
3681 end do
3682
3683 ! Initialize new cursor
3684 call init_cursor(new_cursors(size(new_cursors)))
3685 new_cursors(size(new_cursors))%line = found_line
3686 new_cursors(size(new_cursors))%column = found_col
3687 new_cursors(size(new_cursors))%has_selection = .true.
3688 new_cursors(size(new_cursors))%selection_start_line = found_line
3689 new_cursors(size(new_cursors))%selection_start_col = found_col
3690 new_cursors(size(new_cursors))%column = found_col + len(search_pattern)
3691
3692 deallocate(editor%cursors)
3693 editor%cursors = new_cursors
3694 editor%active_cursor = size(editor%cursors)
3695 end if
3696 end if
3697 end subroutine select_next_match
3698
3699 subroutine select_word_at_cursor(cursor, buffer)
3700 type(cursor_t), intent(inout) :: cursor
3701 type(buffer_t), intent(in) :: buffer
3702 character(len=:), allocatable :: line
3703 integer :: word_start, word_end
3704
3705 line = buffer_get_line(buffer, cursor%line)
3706
3707 ! Find word boundaries
3708 call find_word_boundaries(line, cursor%column, word_start, word_end)
3709
3710 if (word_start > 0 .and. word_end >= word_start) then
3711 ! Select the word
3712 cursor%has_selection = .true.
3713 cursor%selection_start_line = cursor%line
3714 cursor%selection_start_col = word_start
3715 cursor%column = word_end + 1
3716 cursor%desired_column = cursor%column
3717 end if
3718
3719 if (allocated(line)) deallocate(line)
3720 end subroutine select_word_at_cursor
3721
3722 function get_selected_text(cursor, buffer) result(text)
3723 type(cursor_t), intent(in) :: cursor
3724 type(buffer_t), intent(in) :: buffer
3725 character(len=:), allocatable :: text
3726 character(len=:), allocatable :: line
3727 integer :: start_col, end_col
3728
3729 if (.not. cursor%has_selection) then
3730 allocate(character(len=0) :: text)
3731 return
3732 end if
3733
3734 ! For single-line selection only (for now)
3735 if (cursor%selection_start_line == cursor%line) then
3736 line = buffer_get_line(buffer, cursor%line)
3737 start_col = min(cursor%selection_start_col, cursor%column)
3738 end_col = max(cursor%selection_start_col, cursor%column) - 1
3739
3740 if (start_col <= len(line) .and. end_col <= len(line)) then
3741 text = line(start_col:end_col)
3742 else
3743 allocate(character(len=0) :: text)
3744 end if
3745 if (allocated(line)) deallocate(line)
3746 else
3747 allocate(character(len=0) :: text)
3748 end if
3749 end function get_selected_text
3750
3751 subroutine find_word_boundaries(line, pos, word_start, word_end)
3752 character(len=*), intent(in) :: line
3753 integer, intent(in) :: pos
3754 integer, intent(out) :: word_start, word_end
3755 integer :: i
3756
3757 word_start = 0
3758 word_end = 0
3759
3760 ! Check if we're on a word character
3761 if (pos <= len(line)) then
3762 if (.not. is_word_char(line(pos:pos))) then
3763 return
3764 end if
3765
3766 ! Find start of word
3767 word_start = pos
3768 do i = pos - 1, 1, -1
3769 if (is_word_char(line(i:i))) then
3770 word_start = i
3771 else
3772 exit
3773 end if
3774 end do
3775
3776 ! Find end of word
3777 word_end = pos
3778 do i = pos + 1, len(line)
3779 if (is_word_char(line(i:i))) then
3780 word_end = i
3781 else
3782 exit
3783 end if
3784 end do
3785 end if
3786 end subroutine find_word_boundaries
3787
3788 subroutine find_next_occurrence(buffer, pattern, start_line, start_col, &
3789 found, found_line, found_col)
3790 type(buffer_t), intent(in) :: buffer
3791 character(len=*), intent(in) :: pattern
3792 integer, intent(in) :: start_line, start_col
3793 logical, intent(out) :: found
3794 integer, intent(out) :: found_line, found_col
3795 character(len=:), allocatable :: line
3796 character(len=:), allocatable :: search_line, search_pattern
3797 integer :: line_count, current_line, pos
3798 integer :: search_col
3799
3800 found = .false.
3801 found_line = 0
3802 found_col = 0
3803 line_count = buffer_get_line_count(buffer)
3804
3805 ! Search from current position to end
3806 do current_line = start_line, line_count
3807 line = buffer_get_line(buffer, current_line)
3808
3809 if (current_line == start_line) then
3810 search_col = start_col + 1
3811 else
3812 search_col = 1
3813 end if
3814
3815 ! Perform case-sensitive or case-insensitive search
3816 if (match_case_sensitive) then
3817 pos = index(line(search_col:), pattern)
3818 else
3819 search_line = to_lower(line(search_col:))
3820 search_pattern = to_lower(pattern)
3821 pos = index(search_line, search_pattern)
3822 if (allocated(search_line)) deallocate(search_line)
3823 end if
3824
3825 if (pos > 0) then
3826 found = .true.
3827 found_line = current_line
3828 found_col = search_col + pos - 1
3829 if (allocated(line)) deallocate(line)
3830 if (allocated(search_pattern)) deallocate(search_pattern)
3831 return
3832 end if
3833 if (allocated(line)) deallocate(line)
3834 end do
3835
3836 ! Wrap around to beginning
3837 do current_line = 1, start_line
3838 line = buffer_get_line(buffer, current_line)
3839
3840 if (current_line == start_line) then
3841 ! Search only up to start position
3842 if (start_col > 1) then
3843 if (match_case_sensitive) then
3844 pos = index(line(1:start_col-1), pattern)
3845 else
3846 search_line = to_lower(line(1:start_col-1))
3847 search_pattern = to_lower(pattern)
3848 pos = index(search_line, search_pattern)
3849 if (allocated(search_line)) deallocate(search_line)
3850 end if
3851 else
3852 pos = 0
3853 end if
3854 else
3855 if (match_case_sensitive) then
3856 pos = index(line, pattern)
3857 else
3858 search_line = to_lower(line)
3859 search_pattern = to_lower(pattern)
3860 pos = index(search_line, search_pattern)
3861 if (allocated(search_line)) deallocate(search_line)
3862 end if
3863 end if
3864
3865 if (pos > 0) then
3866 found = .true.
3867 found_line = current_line
3868 found_col = pos
3869 if (allocated(line)) deallocate(line)
3870 if (allocated(search_pattern)) deallocate(search_pattern)
3871 return
3872 end if
3873
3874 if (allocated(line)) deallocate(line)
3875 end do
3876
3877 if (allocated(search_pattern)) deallocate(search_pattern)
3878 end subroutine find_next_occurrence
3879
3880 ! Helper function to convert a string to lowercase for case-insensitive comparison
3881 function to_lower(str) result(lower_str)
3882 character(len=*), intent(in) :: str
3883 character(len=:), allocatable :: lower_str
3884 integer :: i
3885
3886 allocate(character(len=len(str)) :: lower_str)
3887
3888 do i = 1, len(str)
3889 if (iachar(str(i:i)) >= iachar('A') .and. &
3890 iachar(str(i:i)) <= iachar('Z')) then
3891 lower_str(i:i) = char(iachar(str(i:i)) + 32)
3892 else
3893 lower_str(i:i) = str(i:i)
3894 end if
3895 end do
3896 end function to_lower
3897
3898 ! ========================================================================
3899 ! Buffer Helper Functions - Wrappers for cursor-based operations
3900 ! ========================================================================
3901
3902 subroutine buffer_delete_at_cursor(buffer, cursor)
3903 type(buffer_t), intent(inout) :: buffer
3904 type(cursor_t), intent(in) :: cursor
3905 integer :: pos
3906
3907 ! Convert cursor position to buffer position
3908 pos = get_buffer_position(buffer, cursor%line, cursor%column)
3909 if (pos > 0 .and. pos <= get_buffer_content_size(buffer)) then
3910 call buffer_delete(buffer, pos, 1)
3911 end if
3912 end subroutine buffer_delete_at_cursor
3913
3914 subroutine buffer_insert_char(buffer, cursor, ch)
3915 type(buffer_t), intent(inout) :: buffer
3916 type(cursor_t), intent(in) :: cursor
3917 character, intent(in) :: ch
3918 integer :: pos
3919
3920 ! Convert cursor position to buffer position
3921 pos = get_buffer_position(buffer, cursor%line, cursor%column)
3922 call buffer_insert(buffer, pos, ch)
3923 end subroutine buffer_insert_char
3924
3925 subroutine buffer_insert_newline(buffer, cursor)
3926 type(buffer_t), intent(inout) :: buffer
3927 type(cursor_t), intent(in) :: cursor
3928 integer :: pos
3929
3930 ! Convert cursor position to buffer position
3931 pos = get_buffer_position(buffer, cursor%line, cursor%column)
3932 call buffer_insert(buffer, pos, char(10))
3933 end subroutine buffer_insert_newline
3934
3935 subroutine buffer_insert_text_at(buffer, line, column, text)
3936 type(buffer_t), intent(inout) :: buffer
3937 integer, intent(in) :: line, column
3938 character(len=*), intent(in) :: text
3939 integer :: pos
3940
3941 ! Convert line/column to buffer position
3942 pos = get_buffer_position(buffer, line, column)
3943 call buffer_insert(buffer, pos, text)
3944 end subroutine buffer_insert_text_at
3945
3946 subroutine buffer_delete_range(buffer, start_line, start_col, end_line, end_col)
3947 type(buffer_t), intent(inout) :: buffer
3948 integer, intent(in) :: start_line, start_col, end_line, end_col
3949 integer :: start_pos, end_pos, count
3950
3951 ! Convert positions to buffer positions
3952 start_pos = get_buffer_position(buffer, start_line, start_col)
3953 end_pos = get_buffer_position(buffer, end_line, end_col)
3954 count = end_pos - start_pos
3955
3956 if (count > 0) then
3957 call buffer_delete(buffer, start_pos, count)
3958 end if
3959 end subroutine buffer_delete_range
3960
3961 function get_buffer_position(buffer, line, column) result(pos)
3962 type(buffer_t), intent(in) :: buffer
3963 integer, intent(in) :: line, column
3964 integer :: pos
3965 integer :: current_line, i, col_in_line
3966 character :: ch
3967
3968 pos = 1
3969 current_line = 1
3970 col_in_line = 1
3971
3972 ! Find the position for the given line and column
3973 do i = 1, get_buffer_content_size(buffer)
3974 if (current_line == line .and. col_in_line == column) then
3975 pos = i
3976 return
3977 end if
3978
3979 ch = buffer_get_char(buffer, i)
3980 if (ch == char(10)) then
3981 if (current_line == line) then
3982 ! We're at the end of the target line
3983 pos = i
3984 return
3985 end if
3986 current_line = current_line + 1
3987 col_in_line = 1
3988 else
3989 col_in_line = col_in_line + 1
3990 end if
3991 end do
3992
3993 ! If we reach here, we're at the end of the buffer
3994 pos = get_buffer_content_size(buffer) + 1
3995 end function get_buffer_position
3996
3997 function get_buffer_content_size(buffer) result(size)
3998 type(buffer_t), intent(in) :: buffer
3999 integer :: size
4000
4001 size = buffer%size - (buffer%gap_end - buffer%gap_start)
4002 end function get_buffer_content_size
4003
4004 ! ========================================================================
4005 ! Multiple Cursor Addition Above/Below
4006 ! ========================================================================
4007
4008 subroutine add_cursor_above(editor)
4009 type(editor_state_t), intent(inout) :: editor
4010 type(cursor_t), allocatable :: new_cursors(:)
4011 type(cursor_t) :: active_cursor
4012 integer :: i, new_line
4013
4014 active_cursor = editor%cursors(editor%active_cursor)
4015 new_line = active_cursor%line - 1
4016
4017 ! Check if we can add a cursor above
4018 if (new_line < 1) return
4019
4020 ! Allocate space for additional cursor
4021 allocate(new_cursors(size(editor%cursors) + 1))
4022
4023 ! Copy existing cursors
4024 do i = 1, size(editor%cursors)
4025 new_cursors(i) = editor%cursors(i)
4026 end do
4027
4028 ! Add new cursor above
4029 new_cursors(size(new_cursors))%line = new_line
4030 new_cursors(size(new_cursors))%column = active_cursor%column
4031 new_cursors(size(new_cursors))%desired_column = active_cursor%desired_column
4032 new_cursors(size(new_cursors))%has_selection = .false.
4033
4034 ! Replace cursors array
4035 call move_alloc(new_cursors, editor%cursors)
4036
4037 ! Set the new cursor as active
4038 editor%active_cursor = size(editor%cursors)
4039 end subroutine add_cursor_above
4040
4041 subroutine add_cursor_below(editor, buffer)
4042 type(editor_state_t), intent(inout) :: editor
4043 type(buffer_t), intent(in) :: buffer
4044 type(cursor_t), allocatable :: new_cursors(:)
4045 type(cursor_t) :: active_cursor
4046 integer :: i, new_line, line_count
4047
4048 active_cursor = editor%cursors(editor%active_cursor)
4049 line_count = buffer_get_line_count(buffer)
4050 new_line = active_cursor%line + 1
4051
4052 ! Check if we can add a cursor below
4053 if (new_line > line_count) return
4054
4055 ! Allocate space for additional cursor
4056 allocate(new_cursors(size(editor%cursors) + 1))
4057
4058 ! Copy existing cursors
4059 do i = 1, size(editor%cursors)
4060 new_cursors(i) = editor%cursors(i)
4061 end do
4062
4063 ! Add new cursor below
4064 new_cursors(size(new_cursors))%line = new_line
4065 new_cursors(size(new_cursors))%column = active_cursor%column
4066 new_cursors(size(new_cursors))%desired_column = active_cursor%desired_column
4067 new_cursors(size(new_cursors))%has_selection = .false.
4068
4069 ! Replace cursors array
4070 call move_alloc(new_cursors, editor%cursors)
4071
4072 ! Set the new cursor as active
4073 editor%active_cursor = size(editor%cursors)
4074 end subroutine add_cursor_below
4075
4076 subroutine jump_to_matching_bracket(editor, buffer)
4077 type(editor_state_t), intent(inout) :: editor
4078 type(buffer_t), intent(in) :: buffer
4079 logical :: found
4080 integer :: match_line, match_col
4081
4082 ! Find matching bracket from current cursor position
4083 call find_matching_bracket(buffer, &
4084 editor%cursors(editor%active_cursor)%line, &
4085 editor%cursors(editor%active_cursor)%column, &
4086 found, match_line, match_col)
4087
4088 if (found) then
4089 ! Jump to the matching bracket
4090 editor%cursors(editor%active_cursor)%line = match_line
4091 editor%cursors(editor%active_cursor)%column = match_col
4092 editor%cursors(editor%active_cursor)%desired_column = match_col
4093
4094 ! Update viewport to ensure cursor is visible
4095 call update_viewport(editor)
4096 end if
4097 end subroutine jump_to_matching_bracket
4098
4099 ! ========================================================================
4100 ! Selection Extension Subroutines
4101 ! ========================================================================
4102
4103 subroutine extend_selection_up(cursor, buffer)
4104 type(cursor_t), intent(inout) :: cursor
4105 type(buffer_t), intent(in) :: buffer
4106 character(len=:), allocatable :: current_line, target_line
4107
4108 ! Initialize selection if not already started
4109 if (.not. cursor%has_selection) then
4110 cursor%has_selection = .true.
4111 cursor%selection_start_line = cursor%line
4112 cursor%selection_start_col = cursor%column
4113 end if
4114
4115 ! Move cursor up
4116 if (cursor%line > 1) then
4117 current_line = buffer_get_line(buffer, cursor%line)
4118 cursor%line = cursor%line - 1
4119 target_line = buffer_get_line(buffer, cursor%line)
4120
4121 ! If coming from empty line, go to end of target line
4122 if (len(current_line) == 0) then
4123 cursor%column = len(target_line) + 1
4124 cursor%desired_column = cursor%column
4125 else
4126 cursor%column = cursor%desired_column
4127 if (cursor%column > len(target_line) + 1) then
4128 cursor%column = len(target_line) + 1
4129 end if
4130 end if
4131
4132 if (allocated(current_line)) deallocate(current_line)
4133 if (allocated(target_line)) deallocate(target_line)
4134 end if
4135 end subroutine extend_selection_up
4136
4137 subroutine extend_selection_down(cursor, buffer, line_count)
4138 type(cursor_t), intent(inout) :: cursor
4139 type(buffer_t), intent(in) :: buffer
4140 integer, intent(in) :: line_count
4141 character(len=:), allocatable :: current_line, target_line
4142
4143 ! Initialize selection if not already started
4144 if (.not. cursor%has_selection) then
4145 cursor%has_selection = .true.
4146 cursor%selection_start_line = cursor%line
4147 cursor%selection_start_col = cursor%column
4148 end if
4149
4150 ! Move cursor down
4151 if (cursor%line < line_count) then
4152 current_line = buffer_get_line(buffer, cursor%line)
4153 cursor%line = cursor%line + 1
4154 target_line = buffer_get_line(buffer, cursor%line)
4155
4156 ! If coming from empty line, go to column 1 of target line
4157 if (len(current_line) == 0) then
4158 cursor%column = 1
4159 cursor%desired_column = 1
4160 else
4161 cursor%column = cursor%desired_column
4162 if (cursor%column > len(target_line) + 1) then
4163 cursor%column = len(target_line) + 1
4164 end if
4165 end if
4166
4167 if (allocated(current_line)) deallocate(current_line)
4168 if (allocated(target_line)) deallocate(target_line)
4169 end if
4170 end subroutine extend_selection_down
4171
4172 subroutine extend_selection_left(cursor, buffer)
4173 type(cursor_t), intent(inout) :: cursor
4174 type(buffer_t), intent(in) :: buffer
4175 character(len=:), allocatable :: line
4176
4177 ! Initialize selection if not already started
4178 if (.not. cursor%has_selection) then
4179 cursor%has_selection = .true.
4180 cursor%selection_start_line = cursor%line
4181 cursor%selection_start_col = cursor%column
4182 end if
4183
4184 ! Move cursor left
4185 if (cursor%column > 1) then
4186 cursor%column = cursor%column - 1
4187 cursor%desired_column = cursor%column
4188 else if (cursor%line > 1) then
4189 ! Move to end of previous line
4190 cursor%line = cursor%line - 1
4191 line = buffer_get_line(buffer, cursor%line)
4192 cursor%column = len(line) + 1
4193 cursor%desired_column = cursor%column
4194 if (allocated(line)) deallocate(line)
4195 end if
4196 end subroutine extend_selection_left
4197
4198 subroutine extend_selection_right(cursor, buffer)
4199 type(cursor_t), intent(inout) :: cursor
4200 type(buffer_t), intent(in) :: buffer
4201 character(len=:), allocatable :: line
4202 integer :: line_count
4203
4204 ! Initialize selection if not already started
4205 if (.not. cursor%has_selection) then
4206 cursor%has_selection = .true.
4207 cursor%selection_start_line = cursor%line
4208 cursor%selection_start_col = cursor%column
4209 end if
4210
4211 line = buffer_get_line(buffer, cursor%line)
4212 line_count = buffer_get_line_count(buffer)
4213
4214 ! Move cursor right
4215 if (cursor%column <= len(line)) then
4216 cursor%column = cursor%column + 1
4217 cursor%desired_column = cursor%column
4218 else if (cursor%line < line_count) then
4219 ! Move to start of next line
4220 cursor%line = cursor%line + 1
4221 cursor%column = 1
4222 cursor%desired_column = cursor%column
4223 end if
4224
4225 if (allocated(line)) deallocate(line)
4226 end subroutine extend_selection_right
4227
4228 subroutine extend_selection_home(cursor)
4229 type(cursor_t), intent(inout) :: cursor
4230
4231 ! Initialize selection if not already started
4232 if (.not. cursor%has_selection) then
4233 cursor%has_selection = .true.
4234 cursor%selection_start_line = cursor%line
4235 cursor%selection_start_col = cursor%column
4236 end if
4237
4238 cursor%column = 1
4239 cursor%desired_column = 1
4240 end subroutine extend_selection_home
4241
4242 subroutine extend_selection_end(cursor, buffer)
4243 type(cursor_t), intent(inout) :: cursor
4244 type(buffer_t), intent(in) :: buffer
4245 character(len=:), allocatable :: line
4246
4247 ! Initialize selection if not already started
4248 if (.not. cursor%has_selection) then
4249 cursor%has_selection = .true.
4250 cursor%selection_start_line = cursor%line
4251 cursor%selection_start_col = cursor%column
4252 end if
4253
4254 line = buffer_get_line(buffer, cursor%line)
4255 cursor%column = len(line) + 1
4256 cursor%desired_column = cursor%column
4257 if (allocated(line)) deallocate(line)
4258 end subroutine extend_selection_end
4259
4260 subroutine extend_selection_page_up(cursor, editor)
4261 type(cursor_t), intent(inout) :: cursor
4262 type(editor_state_t), intent(in) :: editor
4263 integer :: page_size
4264
4265 ! Initialize selection if not already started
4266 if (.not. cursor%has_selection) then
4267 cursor%has_selection = .true.
4268 cursor%selection_start_line = cursor%line
4269 cursor%selection_start_col = cursor%column
4270 end if
4271
4272 page_size = editor%screen_rows - 2 ! Leave room for status bar
4273 cursor%line = max(1, cursor%line - page_size)
4274 cursor%column = cursor%desired_column
4275 end subroutine extend_selection_page_up
4276
4277 subroutine extend_selection_page_down(cursor, editor, line_count)
4278 type(cursor_t), intent(inout) :: cursor
4279 type(editor_state_t), intent(in) :: editor
4280 integer, intent(in) :: line_count
4281 integer :: page_size
4282
4283 ! Initialize selection if not already started
4284 if (.not. cursor%has_selection) then
4285 cursor%has_selection = .true.
4286 cursor%selection_start_line = cursor%line
4287 cursor%selection_start_col = cursor%column
4288 end if
4289
4290 page_size = editor%screen_rows - 2 ! Leave room for status bar
4291 cursor%line = min(line_count, cursor%line + page_size)
4292 cursor%column = cursor%desired_column
4293 end subroutine extend_selection_page_down
4294
4295 subroutine extend_selection_word_left(cursor, buffer)
4296 type(cursor_t), intent(inout) :: cursor
4297 type(buffer_t), intent(in) :: buffer
4298 character(len=:), allocatable :: line
4299 integer :: pos, line_len
4300
4301 ! Initialize selection if not already started
4302 if (.not. cursor%has_selection) then
4303 cursor%has_selection = .true.
4304 cursor%selection_start_line = cursor%line
4305 cursor%selection_start_col = cursor%column
4306 end if
4307
4308 line = buffer_get_line(buffer, cursor%line)
4309 line_len = len(line)
4310 pos = cursor%column
4311
4312 ! Handle empty lines
4313 if (line_len == 0) then
4314 if (cursor%line > 1) then
4315 cursor%line = cursor%line - 1
4316 if (allocated(line)) deallocate(line)
4317 line = buffer_get_line(buffer, cursor%line)
4318 cursor%column = len(line) + 1
4319 else
4320 cursor%column = 1
4321 end if
4322 cursor%desired_column = cursor%column
4323 if (allocated(line)) deallocate(line)
4324 return
4325 end if
4326
4327 if (pos > 1 .and. line_len > 0) then
4328 ! Simple algorithm: move left one position at a time until we find a word start
4329 pos = pos - 1 ! Move left one position
4330
4331 ! Skip any whitespace
4332 do while (pos > 1 .and. pos <= line_len)
4333 if (line(pos:pos) /= ' ') exit
4334 pos = pos - 1
4335 end do
4336
4337 ! If we're on a word character, go to the start of this word
4338 if (pos >= 1 .and. pos <= line_len) then
4339 if (is_word_char(line(pos:pos))) then
4340 ! Move to the start of the current word
4341 do while (pos > 1)
4342 if (pos-1 < 1) exit ! Safety check
4343 if (.not. is_word_char(line(pos-1:pos-1))) exit
4344 pos = pos - 1
4345 end do
4346 end if
4347 end if
4348
4349 ! Clamp to valid range
4350 if (pos < 1) pos = 1
4351 if (pos > line_len + 1) pos = line_len + 1
4352
4353 cursor%column = pos
4354 else if (cursor%line > 1) then
4355 ! Move to end of previous line
4356 cursor%line = cursor%line - 1
4357 if (allocated(line)) deallocate(line)
4358 line = buffer_get_line(buffer, cursor%line)
4359 cursor%column = len(line) + 1
4360 else
4361 cursor%column = 1
4362 end if
4363
4364 cursor%desired_column = cursor%column
4365 if (allocated(line)) deallocate(line)
4366 end subroutine extend_selection_word_left
4367
4368 subroutine extend_selection_word_right(cursor, buffer)
4369 type(cursor_t), intent(inout) :: cursor
4370 type(buffer_t), intent(in) :: buffer
4371 character(len=:), allocatable :: line
4372 integer :: pos, line_count, line_len
4373
4374 ! Initialize selection if not already started
4375 if (.not. cursor%has_selection) then
4376 cursor%has_selection = .true.
4377 cursor%selection_start_line = cursor%line
4378 cursor%selection_start_col = cursor%column
4379 end if
4380
4381 line = buffer_get_line(buffer, cursor%line)
4382 line_count = buffer_get_line_count(buffer)
4383 line_len = len(line)
4384 pos = cursor%column
4385
4386 ! Clamp position to valid range
4387 if (pos > line_len + 1) pos = line_len + 1
4388 if (pos < 1) pos = 1
4389
4390 if (line_len == 0 .or. pos > line_len) then
4391 ! At end of line or empty line - move to next line
4392 if (cursor%line < line_count) then
4393 cursor%line = cursor%line + 1
4394 cursor%column = 1
4395 else
4396 cursor%column = line_len + 1
4397 end if
4398 else if (pos >= 1 .and. pos <= line_len) then
4399 ! Check what we're currently on (with bounds checking)
4400 if (is_word_char(line(pos:pos))) then
4401 ! We're on a word character - skip to end of word
4402 do while (pos < line_len)
4403 if (pos+1 <= line_len) then
4404 if (.not. is_word_char(line(pos+1:pos+1))) exit
4405 end if
4406 pos = pos + 1
4407 end do
4408 pos = pos + 1 ! Move past the word
4409 else
4410 ! We're on whitespace or punctuation - skip to next word
4411 ! Skip non-word characters
4412 do while (pos < line_len)
4413 if (pos+1 <= line_len) then
4414 if (is_word_char(line(pos+1:pos+1))) exit
4415 end if
4416 pos = pos + 1
4417 end do
4418
4419 ! If we found a word, move to its end
4420 if (pos < line_len .and. pos+1 <= line_len) then
4421 pos = pos + 1 ! Move to start of word
4422 do while (pos < line_len)
4423 if (pos+1 <= line_len) then
4424 if (.not. is_word_char(line(pos+1:pos+1))) exit
4425 end if
4426 pos = pos + 1
4427 end do
4428 pos = pos + 1 ! Move past the word
4429 else
4430 pos = line_len + 1 ! At end of line
4431 end if
4432 end if
4433
4434 cursor%column = pos
4435 else if (cursor%line < line_count) then
4436 ! Move to start of next line
4437 cursor%line = cursor%line + 1
4438 cursor%column = 1
4439 else
4440 cursor%column = line_len + 1
4441 end if
4442
4443 cursor%desired_column = cursor%column
4444 if (allocated(line)) deallocate(line)
4445 end subroutine extend_selection_word_right
4446
4447 ! ========================================================================
4448 ! Word Deletion Subroutines
4449 ! ========================================================================
4450
4451 subroutine delete_word_forward(cursor, buffer)
4452 type(cursor_t), intent(inout) :: cursor
4453 type(buffer_t), intent(inout) :: buffer
4454 character(len=:), allocatable :: line
4455 integer :: start_col, end_col, line_len
4456 logical :: in_word
4457
4458 line = buffer_get_line(buffer, cursor%line)
4459 line_len = len(line)
4460 start_col = cursor%column
4461 end_col = start_col
4462
4463 ! If cursor is past end of line, do nothing (can't delete forward from past the line end)
4464 if (start_col > line_len + 1) then
4465 if (allocated(line)) deallocate(line)
4466 return
4467 end if
4468
4469 if (end_col <= line_len) then
4470 ! Skip current word (use nested ifs to avoid bounds issues)
4471 in_word = is_word_char(line(end_col:end_col))
4472 do while (end_col < line_len)
4473 if (is_word_char(line(end_col:end_col)) .eqv. in_word) then
4474 end_col = end_col + 1
4475 else
4476 exit
4477 end if
4478 end do
4479
4480 ! Check if we're still on the same word type at end_col
4481 if (end_col <= line_len) then
4482 if (is_word_char(line(end_col:end_col)) .eqv. in_word) then
4483 end_col = end_col + 1
4484 end if
4485 end if
4486
4487 ! Skip trailing whitespace
4488 do while (end_col <= line_len)
4489 if (line(end_col:end_col) == ' ') then
4490 end_col = end_col + 1
4491 else
4492 exit
4493 end if
4494 end do
4495
4496 ! Delete from cursor to end position
4497 if (end_col > start_col) then
4498 call delete_range(buffer, cursor%line, start_col, cursor%line, end_col - 1)
4499 buffer%modified = .true.
4500 end if
4501 end if
4502
4503 if (allocated(line)) deallocate(line)
4504 end subroutine delete_word_forward
4505
4506 subroutine delete_word_backward(cursor, buffer)
4507 type(cursor_t), intent(inout) :: cursor
4508 type(buffer_t), intent(inout) :: buffer
4509 character(len=:), allocatable :: line
4510 integer :: start_col, end_col, line_len
4511 logical :: in_word
4512
4513 line = buffer_get_line(buffer, cursor%line)
4514 line_len = len(line)
4515 end_col = cursor%column - 1
4516 start_col = end_col
4517
4518 ! Skip whitespace to the left (use nested ifs for safety)
4519 do while (start_col > 0 .and. start_col <= line_len)
4520 if (line(start_col:start_col) == ' ') then
4521 start_col = start_col - 1
4522 else
4523 exit
4524 end if
4525 end do
4526
4527 ! Delete word to the left
4528 if (start_col > 0 .and. start_col <= line_len) then
4529 in_word = is_word_char(line(start_col:start_col))
4530 do while (start_col > 1)
4531 if (is_word_char(line(start_col-1:start_col-1)) .eqv. in_word) then
4532 start_col = start_col - 1
4533 else
4534 exit
4535 end if
4536 end do
4537
4538 ! Delete from start position to cursor
4539 if (start_col <= end_col) then
4540 call delete_range(buffer, cursor%line, start_col, cursor%line, end_col)
4541 cursor%column = start_col
4542 cursor%desired_column = cursor%column
4543 buffer%modified = .true.
4544 end if
4545 end if
4546
4547 if (allocated(line)) deallocate(line)
4548 end subroutine delete_word_backward
4549
4550 ! ========================================================================
4551 ! Character Transpose Subroutine
4552 ! ========================================================================
4553
4554 subroutine join_lines(cursor, buffer)
4555 type(cursor_t), intent(inout) :: cursor
4556 type(buffer_t), intent(inout) :: buffer
4557 character(len=:), allocatable :: current_line, next_line
4558 integer :: line_count, current_len, leading_spaces
4559
4560 line_count = buffer_get_line_count(buffer)
4561
4562 ! Can't join if we're on the last line
4563 if (cursor%line >= line_count) return
4564
4565 ! Get the current line and next line
4566 current_line = buffer_get_line(buffer, cursor%line)
4567 next_line = buffer_get_line(buffer, cursor%line + 1)
4568 current_len = len(current_line)
4569
4570 ! Count leading whitespace in next line
4571 leading_spaces = 0
4572 do while (leading_spaces < len(next_line) .and. &
4573 (next_line(leading_spaces + 1:leading_spaces + 1) == ' ' .or. &
4574 next_line(leading_spaces + 1:leading_spaces + 1) == char(9)))
4575 leading_spaces = leading_spaces + 1
4576 end do
4577
4578 ! Delete the newline and leading whitespace from next line
4579 if (leading_spaces > 0) then
4580 call buffer_delete_range(buffer, cursor%line, current_len + 1, cursor%line + 1, leading_spaces + 1)
4581 else
4582 call buffer_delete_range(buffer, cursor%line, current_len + 1, cursor%line + 1, 1)
4583 end if
4584
4585 ! If the next line had non-whitespace content, insert a space between the lines
4586 if (leading_spaces < len(next_line)) then
4587 ! Insert a space if current line doesn't end with space
4588 if (current_len > 0) then
4589 if (current_line(current_len:current_len) /= ' ') then
4590 call buffer_insert_text_at(buffer, cursor%line, current_len + 1, ' ')
4591 end if
4592 end if
4593 end if
4594
4595 if (allocated(current_line)) deallocate(current_line)
4596 if (allocated(next_line)) deallocate(next_line)
4597 end subroutine join_lines
4598
4599 function get_line_start_pos(buffer, line_num) result(pos)
4600 type(buffer_t), intent(in) :: buffer
4601 integer, intent(in) :: line_num
4602 integer :: pos
4603 integer :: i, current_line
4604
4605 pos = 1
4606 current_line = 1
4607
4608 ! Find the start position of the given line
4609 do i = 1, buffer%size
4610 if (current_line == line_num) then
4611 return
4612 end if
4613
4614 if (buffer_get_char_at(buffer, i) == char(10)) then ! Newline
4615 current_line = current_line + 1
4616 pos = i + 1
4617 end if
4618 end do
4619
4620 ! If line_num is beyond the last line
4621 if (current_line < line_num) then
4622 pos = buffer%size + 1
4623 end if
4624 end function get_line_start_pos
4625
4626 subroutine delete_range(buffer, start_line, start_col, end_line, end_col)
4627 type(buffer_t), intent(inout) :: buffer
4628 integer, intent(in) :: start_line, start_col, end_line, end_col
4629 integer :: pos
4630
4631 ! For now, handle single-line deletions
4632 if (start_line == end_line) then
4633 ! Calculate buffer position
4634 pos = get_line_start_pos(buffer, start_line) + start_col - 1
4635
4636 ! Move gap to deletion point
4637 call buffer_move_gap(buffer, pos)
4638
4639 ! Extend gap to delete characters
4640 buffer%gap_end = buffer%gap_end + (end_col - start_col + 1)
4641 end if
4642 end subroutine delete_range
4643
4644 ! UNUSED: subroutine insert_char_at(buffer, line_num, col, ch)
4645 ! UNUSED: type(buffer_t), intent(inout) :: buffer
4646 ! UNUSED: integer, intent(in) :: line_num, col
4647 ! UNUSED: character, intent(in) :: ch
4648 ! UNUSED: integer :: pos
4649 ! UNUSED:
4650 ! UNUSED: ! Calculate buffer position
4651 ! UNUSED: pos = get_line_start_pos(buffer, line_num) + col - 1
4652 ! UNUSED:
4653 ! UNUSED: ! Move gap to insertion point
4654 ! UNUSED: call buffer_move_gap(buffer, pos)
4655 ! UNUSED:
4656 ! UNUSED: ! Insert character
4657 ! UNUSED: buffer%data(buffer%gap_start:buffer%gap_start) = ch
4658 ! UNUSED: buffer%gap_start = buffer%gap_start + 1
4659 ! UNUSED: end subroutine insert_char_at
4660
4661 ! Handle input when in fuss mode
4662 subroutine handle_fuss_input(key_str, editor, buffer)
4663 character(len=*), intent(in) :: key_str
4664 type(editor_state_t), intent(inout) :: editor
4665 type(buffer_t), intent(inout) :: buffer
4666 character(len=:), allocatable :: selected_path
4667 integer :: i
4668
4669 select case(trim(key_str))
4670 case('j', 'down')
4671 ! Move down in tree
4672 call tree_move_down(tree_state)
4673 ! Update viewport to keep selection visible (estimate ~18 visible lines)
4674 call update_tree_viewport(tree_state, 18)
4675
4676 case('k', 'up')
4677 ! Move up in tree
4678 call tree_move_up(tree_state)
4679 ! Update viewport to keep selection visible (estimate ~18 visible lines)
4680 call update_tree_viewport(tree_state, 18)
4681
4682 case('left')
4683 ! Move up to parent directory
4684 if (tree_state%selected_index >= 1 .and. tree_state%selected_index <= tree_state%n_selectable) then
4685 if (associated(tree_state%selectable_files(tree_state%selected_index)%node)) then
4686 if (associated(tree_state%selectable_files(tree_state%selected_index)%node%parent)) then
4687 ! Find the parent in the selectable list
4688 do i = 1, tree_state%n_selectable
4689 if (associated(tree_state%selectable_files(i)%node, &
4690 tree_state%selectable_files(tree_state%selected_index)%node%parent)) then
4691 tree_state%selected_index = i
4692 exit
4693 end if
4694 end do
4695 end if
4696 end if
4697 end if
4698
4699 case('right')
4700 ! Move into first child of directory (and expand if needed)
4701 if (tree_state%selected_index >= 1 .and. tree_state%selected_index <= tree_state%n_selectable) then
4702 if (tree_state%selectable_files(tree_state%selected_index)%is_directory .and. &
4703 associated(tree_state%selectable_files(tree_state%selected_index)%node)) then
4704 ! Expand if collapsed
4705 if (.not. tree_state%selectable_files(tree_state%selected_index)%node%expanded) then
4706 tree_state%selectable_files(tree_state%selected_index)%node%expanded = .true.
4707 ! Rebuild selectable list
4708 if (allocated(tree_state%selectable_files)) deallocate(tree_state%selectable_files)
4709 call build_selectable_list(tree_state%root, tree_state%selectable_files, tree_state%n_selectable)
4710 end if
4711 ! Find first child in selectable list (look for item whose parent is current node)
4712 do i = tree_state%selected_index + 1, tree_state%n_selectable
4713 if (associated(tree_state%selectable_files(i)%node)) then
4714 if (associated(tree_state%selectable_files(i)%node%parent, &
4715 tree_state%selectable_files(tree_state%selected_index)%node)) then
4716 tree_state%selected_index = i
4717 exit
4718 end if
4719 end if
4720 end do
4721 end if
4722 end if
4723
4724 case(' ', 'space')
4725 ! Toggle directory expand/collapse
4726 if (tree_state%selected_index >= 1 .and. tree_state%selected_index <= tree_state%n_selectable) then
4727 if (.not. tree_state%selectable_files(tree_state%selected_index)%is_directory) then
4728 ! Not a directory - do nothing
4729 else if (associated(tree_state%selectable_files(tree_state%selected_index)%node)) then
4730 ! Toggle expanded
4731 tree_state%selectable_files(tree_state%selected_index)%node%expanded = &
4732 .not. tree_state%selectable_files(tree_state%selected_index)%node%expanded
4733 ! Rebuild selectable list
4734 if (allocated(tree_state%selectable_files)) deallocate(tree_state%selectable_files)
4735 call build_selectable_list(tree_state%root, tree_state%selectable_files, tree_state%n_selectable)
4736 ! Clamp selection
4737 if (tree_state%selected_index > tree_state%n_selectable .and. tree_state%n_selectable > 0) then
4738 tree_state%selected_index = tree_state%n_selectable
4739 end if
4740 end if
4741 end if
4742
4743 case('a')
4744 ! Stage file
4745 if (allocated(editor%workspace_path)) then
4746 call tree_stage_file(tree_state, editor%workspace_path)
4747 end if
4748
4749 case('u')
4750 ! Unstage file
4751 if (allocated(editor%workspace_path)) then
4752 call tree_unstage_file(tree_state, editor%workspace_path)
4753 end if
4754
4755 case('m')
4756 ! Git commit with message
4757 if (allocated(editor%workspace_path)) then
4758 call handle_git_commit(editor)
4759 end if
4760
4761 case('p')
4762 ! Git push
4763 if (allocated(editor%workspace_path)) then
4764 call handle_git_push(editor)
4765 end if
4766
4767 case('f')
4768 ! Git fetch
4769 if (allocated(editor%workspace_path)) then
4770 call handle_git_fetch(editor)
4771 end if
4772
4773 case('l')
4774 ! Git pull
4775 if (allocated(editor%workspace_path)) then
4776 call handle_git_pull(editor)
4777 end if
4778
4779 case('t')
4780 ! Git tag
4781 if (allocated(editor%workspace_path)) then
4782 call handle_git_tag(editor)
4783 end if
4784
4785 case('d')
4786 ! Git diff
4787 if (allocated(editor%workspace_path)) then
4788 call handle_git_diff(editor, buffer)
4789 end if
4790
4791 case('enter', 'o')
4792 ! Open file in editor (only for files, not directories)
4793 if (tree_state%selected_index >= 1 .and. tree_state%selected_index <= tree_state%n_selectable) then
4794 if (.not. tree_state%selectable_files(tree_state%selected_index)%is_directory) then
4795 selected_path = get_selected_item_path(tree_state)
4796 if (len_trim(selected_path) > 0) then
4797 call open_file_in_editor(selected_path, editor, buffer)
4798 end if
4799 end if
4800 end if
4801
4802 case('v')
4803 ! Open file in vertical split (only for files, not directories)
4804 if (tree_state%selected_index >= 1 .and. tree_state%selected_index <= tree_state%n_selectable) then
4805 if (.not. tree_state%selectable_files(tree_state%selected_index)%is_directory) then
4806 selected_path = get_selected_item_path(tree_state)
4807 if (len_trim(selected_path) > 0) then
4808 call open_file_in_vertical_split(selected_path, editor, buffer)
4809 end if
4810 end if
4811 end if
4812
4813 case('s')
4814 ! Open file in horizontal split (only for files, not directories)
4815 if (tree_state%selected_index >= 1 .and. tree_state%selected_index <= tree_state%n_selectable) then
4816 if (.not. tree_state%selectable_files(tree_state%selected_index)%is_directory) then
4817 selected_path = get_selected_item_path(tree_state)
4818 if (len_trim(selected_path) > 0) then
4819 call open_file_in_horizontal_split(selected_path, editor, buffer)
4820 end if
4821 end if
4822 end if
4823
4824 case('.')
4825 ! Toggle hiding dotfiles/gitignored files
4826 tree_state%hide_dotfiles = .not. tree_state%hide_dotfiles
4827
4828 case('ctrl-/')
4829 ! Toggle fuss mode hints expansion
4830 editor%fuss_hints_expanded = .not. editor%fuss_hints_expanded
4831
4832 case('esc')
4833 ! Exit fuss mode
4834 editor%fuss_mode_active = .false.
4835 editor%fuss_hints_expanded = .false. ! Reset to collapsed
4836 call cleanup_tree_state(tree_state)
4837
4838 end select
4839 end subroutine handle_fuss_input
4840
4841 ! Open a file in the editor
4842 subroutine open_file_in_editor(file_path, editor, buffer)
4843 use editor_state_module, only: create_tab
4844 use text_buffer_module, only: copy_buffer
4845 character(len=*), intent(in) :: file_path
4846 type(editor_state_t), intent(inout) :: editor
4847 type(buffer_t), intent(inout) :: buffer
4848 character(len=:), allocatable :: full_path
4849 integer :: status
4850
4851 ! Build full path
4852 if (allocated(editor%workspace_path)) then
4853 full_path = trim(editor%workspace_path) // '/' // trim(file_path)
4854 else
4855 full_path = trim(file_path)
4856 end if
4857
4858 ! Create a new tab for this file
4859 call create_tab(editor, full_path)
4860
4861 ! Load file into the new tab's buffer
4862 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
4863 call buffer_load_file(editor%tabs(editor%active_tab_index)%buffer, full_path, status)
4864
4865 ! Handle binary files
4866 if (status == -2) then
4867 ! Binary file detected - prompt user
4868 if (binary_file_prompt(full_path)) then
4869 ! User wants to view in hex mode
4870 call buffer_load_file_as_hex(editor%tabs(editor%active_tab_index)%buffer, full_path, status)
4871 if (status /= 0) then
4872 ! Failed to load hex view - close the tab and return
4873 call close_tab(editor, editor%active_tab_index)
4874 return
4875 end if
4876 ! Mark as hex view in filename
4877 full_path = trim(full_path) // ' [HEX]'
4878 else
4879 ! User cancelled - close the tab and return
4880 call close_tab(editor, editor%active_tab_index)
4881 return
4882 end if
4883 end if
4884
4885 if (status == 0) then
4886 ! Copy tab's buffer to pane's buffer
4887 if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. &
4888 editor%tabs(editor%active_tab_index)%active_pane_index > 0) then
4889 call copy_buffer(editor%tabs(editor%active_tab_index)%panes( &
4890 editor%tabs(editor%active_tab_index)%active_pane_index)%buffer, &
4891 editor%tabs(editor%active_tab_index)%buffer)
4892 end if
4893
4894 ! Copy tab's buffer to main buffer so it's displayed
4895 call copy_buffer(buffer, editor%tabs(editor%active_tab_index)%buffer)
4896
4897 ! Update editor state with the new tab's info
4898 if (allocated(editor%filename)) deallocate(editor%filename)
4899 allocate(character(len=len_trim(full_path)) :: editor%filename)
4900 editor%filename = full_path
4901
4902 ! Reset cursor to top of file
4903 editor%cursors(editor%active_cursor)%line = 1
4904 editor%cursors(editor%active_cursor)%column = 1
4905 editor%cursors(editor%active_cursor)%desired_column = 1
4906 editor%viewport_line = 1
4907 editor%viewport_column = 1
4908
4909 ! Also update tab's active pane state
4910 if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. &
4911 editor%tabs(editor%active_tab_index)%active_pane_index > 0) then
4912 associate (pane => &
4913 editor%tabs(editor%active_tab_index)%panes( &
4914 editor%tabs(editor%active_tab_index)%active_pane_index))
4915 if (allocated(pane%cursors) .and. size(pane%cursors) > 0) then
4916 pane%cursors(1)%line = 1
4917 pane%cursors(1)%column = 1
4918 pane%cursors(1)%desired_column = 1
4919 end if
4920 pane%viewport_line = 1
4921 pane%viewport_column = 1
4922 end associate
4923 end if
4924 end if
4925 end if
4926 ! Note: fuss mode stays active - user must press ctrl-b to exit
4927 end subroutine open_file_in_editor
4928
4929 ! Open a file in a vertical split
4930 subroutine open_file_in_vertical_split(file_path, editor, buffer)
4931 use editor_state_module, only: split_pane_vertical, sync_editor_to_pane
4932 use text_buffer_module, only: copy_buffer
4933 character(len=*), intent(in) :: file_path
4934 type(editor_state_t), intent(inout) :: editor
4935 type(buffer_t), intent(inout) :: buffer
4936 character(len=:), allocatable :: full_path
4937 integer :: status, tab_idx, pane_idx
4938
4939 ! Build full path
4940 if (allocated(editor%workspace_path)) then
4941 full_path = trim(editor%workspace_path) // '/' // trim(file_path)
4942 else
4943 full_path = trim(file_path)
4944 end if
4945
4946 ! Exit fuss mode
4947 editor%fuss_mode_active = .false.
4948 editor%fuss_hints_expanded = .false.
4949 call cleanup_tree_state(tree_state)
4950
4951 ! If no tabs exist, create one first
4952 if (size(editor%tabs) == 0 .or. editor%active_tab_index == 0) then
4953 call open_file_in_editor(file_path, editor, buffer)
4954 return
4955 end if
4956
4957 ! Split the current pane vertically
4958 call split_pane_vertical(editor)
4959
4960 ! Get the new pane index (it becomes the active pane)
4961 tab_idx = editor%active_tab_index
4962 if (tab_idx > 0 .and. tab_idx <= size(editor%tabs)) then
4963 pane_idx = editor%tabs(tab_idx)%active_pane_index
4964
4965 ! Load the file into the new pane's buffer
4966 if (allocated(editor%tabs(tab_idx)%panes) .and. pane_idx > 0) then
4967 call buffer_load_file(editor%tabs(tab_idx)%panes(pane_idx)%buffer, full_path, status)
4968
4969 ! Handle binary files
4970 if (status == -2) then
4971 ! Binary file detected - prompt user
4972 if (binary_file_prompt(full_path)) then
4973 ! User wants to view in hex mode
4974 call buffer_load_file_as_hex(editor%tabs(tab_idx)%panes(pane_idx)%buffer, full_path, status)
4975 if (status /= 0) then
4976 ! Failed to load hex view - close the pane and return
4977 call close_pane(editor)
4978 return
4979 end if
4980 ! Mark as hex view in filename
4981 full_path = trim(full_path) // ' [HEX]'
4982 else
4983 ! User cancelled - close the pane and return
4984 call close_pane(editor)
4985 return
4986 end if
4987 end if
4988
4989 if (status == 0) then
4990 ! Update filename for the pane
4991 if (allocated(editor%tabs(tab_idx)%panes(pane_idx)%filename)) &
4992 deallocate(editor%tabs(tab_idx)%panes(pane_idx)%filename)
4993 allocate(character(len=len_trim(full_path)) :: editor%tabs(tab_idx)%panes(pane_idx)%filename)
4994 editor%tabs(tab_idx)%panes(pane_idx)%filename = full_path
4995
4996 ! Copy to main buffer
4997 call copy_buffer(buffer, editor%tabs(tab_idx)%panes(pane_idx)%buffer)
4998
4999 ! Update editor filename
5000 if (allocated(editor%filename)) deallocate(editor%filename)
5001 allocate(character(len=len_trim(full_path)) :: editor%filename)
5002 editor%filename = full_path
5003
5004 ! Reset cursor in the new pane
5005 if (allocated(editor%tabs(tab_idx)%panes(pane_idx)%cursors) .and. &
5006 size(editor%tabs(tab_idx)%panes(pane_idx)%cursors) > 0) then
5007 editor%tabs(tab_idx)%panes(pane_idx)%cursors(1)%line = 1
5008 editor%tabs(tab_idx)%panes(pane_idx)%cursors(1)%column = 1
5009 editor%tabs(tab_idx)%panes(pane_idx)%cursors(1)%desired_column = 1
5010 end if
5011 editor%tabs(tab_idx)%panes(pane_idx)%viewport_line = 1
5012 editor%tabs(tab_idx)%panes(pane_idx)%viewport_column = 1
5013
5014 ! Sync editor state with the new pane
5015 call sync_editor_to_pane(editor)
5016 end if
5017 end if
5018 end if
5019 end subroutine open_file_in_vertical_split
5020
5021 ! Open a file in a horizontal split
5022 subroutine open_file_in_horizontal_split(file_path, editor, buffer)
5023 use editor_state_module, only: split_pane_horizontal, sync_editor_to_pane
5024 use text_buffer_module, only: copy_buffer
5025 character(len=*), intent(in) :: file_path
5026 type(editor_state_t), intent(inout) :: editor
5027 type(buffer_t), intent(inout) :: buffer
5028 character(len=:), allocatable :: full_path
5029 integer :: status, tab_idx, pane_idx
5030
5031 ! Build full path
5032 if (allocated(editor%workspace_path)) then
5033 full_path = trim(editor%workspace_path) // '/' // trim(file_path)
5034 else
5035 full_path = trim(file_path)
5036 end if
5037
5038 ! Exit fuss mode
5039 editor%fuss_mode_active = .false.
5040 editor%fuss_hints_expanded = .false.
5041 call cleanup_tree_state(tree_state)
5042
5043 ! If no tabs exist, create one first
5044 if (size(editor%tabs) == 0 .or. editor%active_tab_index == 0) then
5045 call open_file_in_editor(file_path, editor, buffer)
5046 return
5047 end if
5048
5049 ! Split the current pane horizontally
5050 call split_pane_horizontal(editor)
5051
5052 ! Get the new pane index (it becomes the active pane)
5053 tab_idx = editor%active_tab_index
5054 if (tab_idx > 0 .and. tab_idx <= size(editor%tabs)) then
5055 pane_idx = editor%tabs(tab_idx)%active_pane_index
5056
5057 ! Load the file into the new pane's buffer
5058 if (allocated(editor%tabs(tab_idx)%panes) .and. pane_idx > 0) then
5059 call buffer_load_file(editor%tabs(tab_idx)%panes(pane_idx)%buffer, full_path, status)
5060
5061 ! Handle binary files
5062 if (status == -2) then
5063 ! Binary file detected - prompt user
5064 if (binary_file_prompt(full_path)) then
5065 ! User wants to view in hex mode
5066 call buffer_load_file_as_hex(editor%tabs(tab_idx)%panes(pane_idx)%buffer, full_path, status)
5067 if (status /= 0) then
5068 ! Failed to load hex view - close the pane and return
5069 call close_pane(editor)
5070 return
5071 end if
5072 ! Mark as hex view in filename
5073 full_path = trim(full_path) // ' [HEX]'
5074 else
5075 ! User cancelled - close the pane and return
5076 call close_pane(editor)
5077 return
5078 end if
5079 end if
5080
5081 if (status == 0) then
5082 ! Update filename for the pane
5083 if (allocated(editor%tabs(tab_idx)%panes(pane_idx)%filename)) &
5084 deallocate(editor%tabs(tab_idx)%panes(pane_idx)%filename)
5085 allocate(character(len=len_trim(full_path)) :: editor%tabs(tab_idx)%panes(pane_idx)%filename)
5086 editor%tabs(tab_idx)%panes(pane_idx)%filename = full_path
5087
5088 ! Copy to main buffer
5089 call copy_buffer(buffer, editor%tabs(tab_idx)%panes(pane_idx)%buffer)
5090
5091 ! Update editor filename
5092 if (allocated(editor%filename)) deallocate(editor%filename)
5093 allocate(character(len=len_trim(full_path)) :: editor%filename)
5094 editor%filename = full_path
5095
5096 ! Reset cursor in the new pane
5097 if (allocated(editor%tabs(tab_idx)%panes(pane_idx)%cursors) .and. &
5098 size(editor%tabs(tab_idx)%panes(pane_idx)%cursors) > 0) then
5099 editor%tabs(tab_idx)%panes(pane_idx)%cursors(1)%line = 1
5100 editor%tabs(tab_idx)%panes(pane_idx)%cursors(1)%column = 1
5101 editor%tabs(tab_idx)%panes(pane_idx)%cursors(1)%desired_column = 1
5102 end if
5103 editor%tabs(tab_idx)%panes(pane_idx)%viewport_line = 1
5104 editor%tabs(tab_idx)%panes(pane_idx)%viewport_column = 1
5105
5106 ! Sync editor state with the new pane
5107 call sync_editor_to_pane(editor)
5108 end if
5109 end if
5110 end if
5111 end subroutine open_file_in_horizontal_split
5112
5113 ! Toggle fuss mode (file tree)
5114 subroutine toggle_fuss_mode(editor)
5115 type(editor_state_t), intent(inout) :: editor
5116
5117 editor%fuss_mode_active = .not. editor%fuss_mode_active
5118
5119 if (editor%fuss_mode_active) then
5120 ! Entering fuss mode - initialize tree state
5121 if (allocated(editor%workspace_path)) then
5122 call init_tree_state(tree_state, editor%workspace_path)
5123 end if
5124 else
5125 ! Exiting fuss mode - cleanup tree state
5126 call cleanup_tree_state(tree_state)
5127 end if
5128 end subroutine toggle_fuss_mode
5129
5130 ! Toggle diagnostics panel
5131 subroutine toggle_diagnostics_panel(editor)
5132 type(editor_state_t), intent(inout) :: editor
5133 call toggle_panel(editor%diagnostics_panel)
5134 end subroutine toggle_diagnostics_panel
5135
5136 ! Handle git commit with message prompt
5137 subroutine handle_git_commit(editor)
5138 type(editor_state_t), intent(inout) :: editor
5139 character(len=512) :: commit_message
5140 logical :: cancelled, success
5141
5142 ! Show prompt for commit message
5143 call show_text_prompt('Commit message (ESC to cancel): ', commit_message, cancelled, editor%screen_rows)
5144
5145 if (.not. cancelled .and. len_trim(commit_message) > 0) then
5146 call git_commit(editor%workspace_path, commit_message, success)
5147
5148 ! Show feedback message
5149 call terminal_move_cursor(editor%screen_rows, 1)
5150 call terminal_write(repeat(' ', 200))
5151 call terminal_move_cursor(editor%screen_rows, 1)
5152 if (success) then
5153 call terminal_write(char(27) // '[32m✓ Committed successfully!' // char(27) // '[0m')
5154 else
5155 call terminal_write(char(27) // '[31m✗ Commit failed (nothing staged?)' // char(27) // '[0m')
5156 end if
5157
5158 ! Brief pause
5159 call execute_command_line('sleep 1')
5160
5161 ! Refresh tree
5162 call refresh_tree_state(tree_state, editor%workspace_path)
5163 end if
5164 end subroutine handle_git_commit
5165
5166 ! Handle git push
5167 subroutine handle_git_push(editor)
5168 type(editor_state_t), intent(inout) :: editor
5169 logical :: success
5170
5171 ! Show progress message
5172 call terminal_move_cursor(editor%screen_rows, 1)
5173 call terminal_write(repeat(' ', 200))
5174 call terminal_move_cursor(editor%screen_rows, 1)
5175 call terminal_write('Pushing to remote...')
5176
5177 call git_push(editor%workspace_path, success)
5178
5179 ! Show result
5180 call terminal_move_cursor(editor%screen_rows, 1)
5181 call terminal_write(repeat(' ', 200))
5182 call terminal_move_cursor(editor%screen_rows, 1)
5183 if (success) then
5184 call terminal_write(char(27) // '[32m✓ Pushed successfully!' // char(27) // '[0m')
5185 else
5186 call terminal_write(char(27) // '[31m✗ Push failed (check remote/branch)' // char(27) // '[0m')
5187 end if
5188
5189 ! Brief pause
5190 call execute_command_line('sleep 1')
5191
5192 ! Refresh tree
5193 call refresh_tree_state(tree_state, editor%workspace_path)
5194 end subroutine handle_git_push
5195
5196 ! Handle git fetch
5197 subroutine handle_git_fetch(editor)
5198 type(editor_state_t), intent(inout) :: editor
5199 logical :: success
5200
5201 ! Show progress message
5202 call terminal_move_cursor(editor%screen_rows, 1)
5203 call terminal_write(repeat(' ', 200))
5204 call terminal_move_cursor(editor%screen_rows, 1)
5205 call terminal_write('Fetching from remote...')
5206
5207 call git_fetch(editor%workspace_path, success)
5208
5209 ! Show result
5210 call terminal_move_cursor(editor%screen_rows, 1)
5211 call terminal_write(repeat(' ', 200))
5212 call terminal_move_cursor(editor%screen_rows, 1)
5213 if (success) then
5214 call terminal_write(char(27) // '[32m✓ Fetch completed!' // char(27) // '[0m')
5215 else
5216 call terminal_write(char(27) // '[31m✗ Fetch failed!' // char(27) // '[0m')
5217 end if
5218
5219 ! Brief pause
5220 call execute_command_line('sleep 1')
5221
5222 ! Refresh tree
5223 call refresh_tree_state(tree_state, editor%workspace_path)
5224 end subroutine handle_git_fetch
5225
5226 ! Handle git pull
5227 subroutine handle_git_pull(editor)
5228 type(editor_state_t), intent(inout) :: editor
5229 logical :: success
5230
5231 ! Show progress message
5232 call terminal_move_cursor(editor%screen_rows, 1)
5233 call terminal_write(repeat(' ', 200))
5234 call terminal_move_cursor(editor%screen_rows, 1)
5235 call terminal_write('Pulling from remote...')
5236
5237 call git_pull(editor%workspace_path, success)
5238
5239 ! Show result
5240 call terminal_move_cursor(editor%screen_rows, 1)
5241 call terminal_write(repeat(' ', 200))
5242 call terminal_move_cursor(editor%screen_rows, 1)
5243 if (success) then
5244 call terminal_write(char(27) // '[32m✓ Pull completed!' // char(27) // '[0m')
5245 else
5246 call terminal_write(char(27) // '[31m✗ Pull failed!' // char(27) // '[0m')
5247 end if
5248
5249 ! Brief pause
5250 call execute_command_line('sleep 1')
5251
5252 ! Refresh tree
5253 call refresh_tree_state(tree_state, editor%workspace_path)
5254 end subroutine handle_git_pull
5255
5256 ! Handle git tag
5257 subroutine handle_git_tag(editor)
5258 use help_display_module, only: display_tags_header
5259 type(editor_state_t), intent(inout) :: editor
5260 character(len=256) :: tag_name, tag_message
5261 character(len=256), allocatable :: existing_tags(:)
5262 integer :: n_tags
5263 logical :: cancelled, success, push_tag
5264
5265 ! Fetch and display existing tags (keeps them visible during prompts)
5266 call git_list_tags(editor%workspace_path, existing_tags, n_tags)
5267 call display_tags_header(editor, existing_tags, n_tags)
5268
5269 ! Show prompt for tag name (tags remain visible above)
5270 call show_text_prompt('Tag name (ESC to cancel): ', tag_name, cancelled, editor%screen_rows)
5271
5272 if (allocated(existing_tags)) deallocate(existing_tags)
5273
5274 if (.not. cancelled .and. len_trim(tag_name) > 0) then
5275 ! Show prompt for tag message (optional)
5276 call show_text_prompt('Tag message (optional, ESC to skip): ', tag_message, cancelled, editor%screen_rows)
5277
5278 if (.not. cancelled) then
5279 call git_tag(editor%workspace_path, tag_name, tag_message, success)
5280
5281 ! Show result
5282 call terminal_move_cursor(editor%screen_rows, 1)
5283 call terminal_write(repeat(' ', 200))
5284 call terminal_move_cursor(editor%screen_rows, 1)
5285 if (success) then
5286 call terminal_write(char(27) // '[32m✓ Tag created: ' // trim(tag_name) // char(27) // '[0m')
5287
5288 ! Brief pause
5289 call execute_command_line('sleep 1')
5290
5291 ! Ask if user wants to push the tag to origin (auto-submit on y/n)
5292 call show_yes_no_prompt('Push tag to origin? (y/n, ESC to skip): ', push_tag, cancelled, editor%screen_rows)
5293
5294 if (.not. cancelled .and. push_tag) then
5295 call git_push_tag(editor%workspace_path, tag_name, success)
5296
5297 ! Show push result
5298 call terminal_move_cursor(editor%screen_rows, 1)
5299 call terminal_write(repeat(' ', 200))
5300 call terminal_move_cursor(editor%screen_rows, 1)
5301 if (success) then
5302 call terminal_write(char(27) // '[32m✓ Tag pushed to origin' // char(27) // '[0m')
5303 else
5304 call terminal_write(char(27) // '[31m✗ Failed to push tag (check remote)' // char(27) // '[0m')
5305 end if
5306
5307 call execute_command_line('sleep 1')
5308 end if
5309 else
5310 call terminal_write(char(27) // '[31m✗ Failed to create tag' // char(27) // '[0m')
5311 call execute_command_line('sleep 1')
5312 end if
5313
5314 ! Refresh tree
5315 call refresh_tree_state(tree_state, editor%workspace_path)
5316 end if
5317 end if
5318 end subroutine handle_git_tag
5319
5320 subroutine handle_git_diff(editor, buffer)
5321 use editor_state_module, only: create_tab
5322 use text_buffer_module, only: buffer_insert, copy_buffer
5323 use file_tree_module, only: get_selected_item_path
5324 type(editor_state_t), intent(inout) :: editor
5325 type(buffer_t), intent(inout) :: buffer
5326 character(len=:), allocatable :: selected_path, diff_content, tab_name
5327 character(len=256) :: branch_name
5328 logical :: success
5329
5330 ! Get selected file from tree
5331 if (tree_state%selected_index < 1 .or. tree_state%selected_index > tree_state%n_selectable) return
5332 if (tree_state%selectable_files(tree_state%selected_index)%is_directory) return
5333
5334 selected_path = get_selected_item_path(tree_state)
5335 if (len_trim(selected_path) == 0) return
5336
5337 ! Get diff content
5338 call git_diff_file(editor%workspace_path, selected_path, diff_content, branch_name, success)
5339
5340 if (.not. success) then
5341 call terminal_move_cursor(editor%screen_rows, 1)
5342 call terminal_write(repeat(' ', 200))
5343 call terminal_move_cursor(editor%screen_rows, 1)
5344 call terminal_write(char(27) // '[31m✗ Failed to get diff' // char(27) // '[0m')
5345 call execute_command_line('sleep 1')
5346 return
5347 end if
5348
5349 ! Create tab name: diff:<filename>:<branch>
5350 if (len_trim(branch_name) > 0) then
5351 tab_name = 'diff:' // trim(selected_path) // ':' // trim(branch_name)
5352 else
5353 tab_name = 'diff:' // trim(selected_path)
5354 end if
5355
5356 ! Create new tab
5357 call create_tab(editor, tab_name)
5358
5359 ! Load diff content into the tab's buffer
5360 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
5361 ! Insert diff content at the beginning of the buffer
5362 call buffer_insert(editor%tabs(editor%active_tab_index)%buffer, 1, diff_content)
5363
5364 ! Copy tab's buffer to main buffer so it's displayed
5365 call copy_buffer(buffer, editor%tabs(editor%active_tab_index)%buffer)
5366
5367 ! Update editor state with the new tab's info
5368 if (allocated(editor%filename)) deallocate(editor%filename)
5369 allocate(character(len=len_trim(tab_name)) :: editor%filename)
5370 editor%filename = tab_name
5371
5372 ! Reset cursor to top of file
5373 editor%cursors(editor%active_cursor)%line = 1
5374 editor%cursors(editor%active_cursor)%column = 1
5375 editor%cursors(editor%active_cursor)%desired_column = 1
5376
5377 ! Exit fuss mode and show diff
5378 editor%fuss_mode_active = .false.
5379 end if
5380 end subroutine handle_git_diff
5381
5382 subroutine handle_fortress_navigator(editor, buffer)
5383 use workspace_module, only: workspace_is_file_in_workspace, workspace_switch
5384 use save_prompt_module, only: save_prompt, save_prompt_result_t
5385 use input_handler_module, only: get_key_input
5386 type(editor_state_t), intent(inout) :: editor
5387 type(buffer_t), intent(inout) :: buffer
5388 character(len=:), allocatable :: selected_path
5389 character(len=32) :: key_input
5390 logical :: is_directory, cancelled, is_in_workspace, switch_success
5391 logical :: should_switch
5392 integer :: load_status, tab_idx, status
5393
5394 ! Call fortress navigator (start in workspace if available)
5395 if (allocated(editor%workspace_path)) then
5396 call open_fortress_navigator(selected_path, is_directory, cancelled, editor%workspace_path)
5397 else
5398 call open_fortress_navigator(selected_path, is_directory, cancelled)
5399 end if
5400
5401 ! If user selected something, open it
5402 if (.not. cancelled .and. allocated(selected_path)) then
5403 if (len_trim(selected_path) > 0) then
5404 if (.not. is_directory) then
5405 ! Selected a file - create a new tab for it
5406 ! Check if file is within workspace
5407 if (allocated(editor%workspace_path)) then
5408 is_in_workspace = workspace_is_file_in_workspace(selected_path, editor%workspace_path)
5409 else
5410 is_in_workspace = .false.
5411 end if
5412
5413 ! Create new tab
5414 call create_tab(editor, trim(selected_path))
5415 tab_idx = editor%active_tab_index
5416
5417 ! Mark as orphan if outside workspace
5418 if (allocated(editor%tabs) .and. tab_idx > 0 .and. tab_idx <= size(editor%tabs)) then
5419 editor%tabs(tab_idx)%is_orphan = .not. is_in_workspace
5420
5421 ! Load file into tab's buffer
5422 call buffer_load_file(editor%tabs(tab_idx)%buffer, selected_path, load_status)
5423
5424 ! Also load into first pane's buffer
5425 if (allocated(editor%tabs(tab_idx)%panes) .and. &
5426 size(editor%tabs(tab_idx)%panes) > 0) then
5427 call buffer_load_file(editor%tabs(tab_idx)%panes(1)%buffer, selected_path, load_status)
5428 ! Copy to main buffer for rendering
5429 call copy_buffer(buffer, editor%tabs(tab_idx)%panes(1)%buffer)
5430 else
5431 ! Copy tab buffer to main buffer
5432 call copy_buffer(buffer, editor%tabs(tab_idx)%buffer)
5433 end if
5434
5435 ! Update editor filename
5436 if (allocated(editor%filename)) deallocate(editor%filename)
5437 allocate(character(len=len_trim(selected_path)) :: editor%filename)
5438 editor%filename = selected_path
5439
5440 ! Reset cursor to top
5441 editor%cursors(editor%active_cursor)%line = 1
5442 editor%cursors(editor%active_cursor)%column = 1
5443 editor%cursors(editor%active_cursor)%desired_column = 1
5444 end if
5445 else
5446 ! Selected a directory - switch workspace (Phase 6)
5447 should_switch = .true.
5448
5449 ! Check for dirty buffers and prompt to save
5450 if (allocated(editor%tabs)) then
5451 call handle_dirty_buffers_before_switch(editor, should_switch)
5452 end if
5453
5454 ! If user didn't cancel, perform the switch
5455 if (should_switch) then
5456 call workspace_switch(editor, selected_path, switch_success)
5457
5458 if (.not. switch_success) then
5459 ! Show error message
5460 call terminal_move_cursor(1, 1)
5461 call terminal_write("Error: Could not switch to workspace: " // trim(selected_path))
5462 call terminal_write("Press any key to continue...")
5463 ! Wait for keypress (simple implementation)
5464 call get_key_input(key_input, status)
5465 else
5466 ! Phase 7: Update file tree if it's active after successful workspace switch
5467 if (editor%fuss_mode_active .and. allocated(editor%workspace_path)) then
5468 call refresh_tree_state(tree_state, editor%workspace_path)
5469 end if
5470 end if
5471 end if
5472 end if
5473 end if
5474 end if
5475
5476 ! Re-render after returning from fortress
5477 call terminal_clear_screen()
5478 end subroutine handle_fortress_navigator
5479
5480 !> Handle dirty buffers before workspace switch
5481 subroutine handle_dirty_buffers_before_switch(editor, should_continue)
5482 use save_prompt_module, only: save_prompt, save_prompt_result_t
5483 use text_buffer_module, only: buffer_save_file
5484 type(editor_state_t), intent(inout) :: editor
5485 logical, intent(inout) :: should_continue
5486 type(save_prompt_result_t) :: prompt_result
5487 integer :: i, save_status
5488
5489 should_continue = .true.
5490
5491 ! Check each tab for modified buffers
5492 do i = 1, size(editor%tabs)
5493 if (editor%tabs(i)%modified .and. allocated(editor%tabs(i)%filename)) then
5494 ! Prompt user for this file
5495 call save_prompt(editor%tabs(i)%filename, prompt_result)
5496
5497 select case (prompt_result%action)
5498 case ('y')
5499 ! Save the file
5500 if (allocated(editor%tabs(i)%panes) .and. size(editor%tabs(i)%panes) > 0) then
5501 call buffer_save_file(editor%tabs(i)%panes(1)%buffer, &
5502 editor%tabs(i)%filename, save_status)
5503 else
5504 call buffer_save_file(editor%tabs(i)%buffer, &
5505 editor%tabs(i)%filename, save_status)
5506 end if
5507
5508 if (save_status == 0) then
5509 editor%tabs(i)%modified = .false.
5510 end if
5511
5512 case ('n')
5513 ! Skip saving - continue
5514
5515 case ('c')
5516 ! Cancel the workspace switch
5517 should_continue = .false.
5518 return
5519 end select
5520 end if
5521 end do
5522 end subroutine handle_dirty_buffers_before_switch
5523
5524 !> Prompt to save before closing tab
5525 subroutine prompt_save_before_close_tab(editor, buffer)
5526 use save_prompt_module, only: save_prompt, save_prompt_result_t
5527 use text_prompt_module, only: show_text_prompt
5528 type(editor_state_t), intent(inout) :: editor
5529 type(buffer_t), intent(inout) :: buffer
5530 type(save_prompt_result_t) :: prompt_result
5531 integer :: save_status, tab_idx
5532 character(len=512) :: new_filename
5533 logical :: cancelled
5534
5535 tab_idx = editor%active_tab_index
5536
5537 ! Prompt user to save
5538 call save_prompt(editor%tabs(tab_idx)%filename, prompt_result)
5539
5540 if (prompt_result%action == 's') then
5541 ! User wants to save
5542 ! Check if untitled - need filename
5543 if (index(editor%tabs(tab_idx)%filename, '[Untitled') == 1) then
5544 call show_text_prompt('Save as: ', new_filename, cancelled, editor%screen_rows)
5545 if (.not. cancelled .and. len_trim(new_filename) > 0) then
5546 ! Update filename and save
5547 if (allocated(editor%tabs(tab_idx)%filename)) deallocate(editor%tabs(tab_idx)%filename)
5548 allocate(character(len=len_trim(new_filename)) :: editor%tabs(tab_idx)%filename)
5549 editor%tabs(tab_idx)%filename = trim(new_filename)
5550
5551 call buffer_save_file(buffer, new_filename, save_status)
5552 if (save_status == 0) then
5553 buffer%modified = .false.
5554 editor%tabs(tab_idx)%modified = .false.
5555 end if
5556 else
5557 ! User cancelled filename prompt - don't close tab
5558 return
5559 end if
5560 else
5561 ! Not untitled - just save
5562 call buffer_save_file(buffer, editor%tabs(tab_idx)%filename, save_status)
5563 if (save_status == 0) then
5564 buffer%modified = .false.
5565 editor%tabs(tab_idx)%modified = .false.
5566 end if
5567 end if
5568
5569 ! After saving, close the tab
5570 call close_tab_without_prompt(editor, buffer)
5571
5572 else if (prompt_result%action == 'd') then
5573 ! User wants to discard - just close
5574 call close_tab_without_prompt(editor, buffer)
5575
5576 ! else if 'c' (cancel) - do nothing, don't close tab
5577 end if
5578 end subroutine prompt_save_before_close_tab
5579
5580 !> Close tab without prompting
5581 subroutine close_tab_without_prompt(editor, buffer)
5582 type(editor_state_t), intent(inout) :: editor
5583 type(buffer_t), intent(inout) :: buffer
5584 integer :: tab_idx
5585
5586 tab_idx = editor%active_tab_index
5587
5588 ! If this is the last tab, close it and clear editor
5589 if (size(editor%tabs) == 1) then
5590 call close_tab(editor, tab_idx)
5591
5592 ! Clear the buffer and open fuss mode
5593 call cleanup_buffer(buffer)
5594 call init_buffer(buffer)
5595 editor%fuss_mode_active = .true.
5596 if (allocated(editor%filename)) deallocate(editor%filename)
5597 editor%modified = .false.
5598 if (allocated(editor%workspace_path)) then
5599 call init_tree_state(tree_state, editor%workspace_path)
5600 end if
5601 else
5602 ! Multiple tabs - close current tab normally
5603 call close_tab(editor, tab_idx)
5604
5605 ! Copy new active tab's buffer
5606 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0) then
5607 call copy_buffer(buffer, editor%tabs(editor%active_tab_index)%buffer)
5608 editor%modified = editor%tabs(editor%active_tab_index)%modified
5609 if (allocated(editor%filename)) deallocate(editor%filename)
5610 allocate(character(len=len(editor%tabs(editor%active_tab_index)%filename)) :: editor%filename)
5611 editor%filename = editor%tabs(editor%active_tab_index)%filename
5612 end if
5613 end if
5614 end subroutine close_tab_without_prompt
5615
5616 ! Notify LSP server of buffer changes
5617 subroutine notify_buffer_change(editor, buffer)
5618 use document_sync_module, only: notify_document_change
5619 type(editor_state_t), intent(inout) :: editor
5620 type(buffer_t), intent(in) :: buffer
5621 character(len=:), allocatable :: full_content
5622 integer :: i, line_count
5623
5624 ! Only notify if we have an active tab with LSP support
5625 if (editor%active_tab_index < 1 .or. editor%active_tab_index > size(editor%tabs)) return
5626 if (editor%tabs(editor%active_tab_index)%lsp_server_index <= 0) return
5627
5628 ! Build full document content
5629 line_count = buffer_get_line_count(buffer)
5630 full_content = ''
5631 do i = 1, line_count
5632 if (i > 1) then
5633 full_content = full_content // char(10) ! LF
5634 end if
5635 full_content = full_content // buffer_get_line(buffer, i)
5636 end do
5637
5638 ! Notify document sync of the change
5639 call notify_document_change(editor%tabs(editor%active_tab_index)%document_sync, &
5640 full_content)
5641 end subroutine notify_buffer_change
5642
5643 ! TODO: Handle LSP textDocument/definition response
5644 ! This needs to be integrated with the main event loop callback system
5645 ! The response parsing logic is ready but needs proper callback integration
5646
5647 ! Wrapper callback that matches the LSP callback signature
5648 subroutine handle_references_response_wrapper(request_id, response)
5649 use lsp_protocol_module, only: lsp_message_t
5650 integer, intent(in) :: request_id
5651 type(lsp_message_t), intent(in) :: response
5652
5653 ! Call the actual handler with saved editor state
5654 if (allocated(saved_editor_for_callback)) then
5655 call handle_references_response_impl(saved_editor_for_callback, response)
5656 end if
5657 end subroutine handle_references_response_wrapper
5658
5659 ! Handle LSP textDocument/references response implementation
5660 subroutine handle_references_response_impl(editor, response)
5661 use lsp_protocol_module, only: lsp_message_t
5662 use json_module, only: json_value_t, json_get_array, json_get_object, &
5663 json_get_string, json_get_number, json_array_size, &
5664 json_get_array_element, json_has_key
5665 type(editor_state_t), intent(inout) :: editor
5666 type(lsp_message_t), intent(in) :: response
5667 type(json_value_t) :: result_array, location_obj, range_obj
5668 type(json_value_t) :: start_obj, end_obj
5669 type(reference_location_t), allocatable :: references(:)
5670 integer :: num_refs, i
5671 character(len=:), allocatable :: uri
5672 real(8) :: line_real, col_real
5673
5674 ! The result is directly in response%result for LSP responses
5675 result_array = response%result
5676 num_refs = json_array_size(result_array)
5677
5678 if (num_refs == 0) then
5679 ! No references found
5680 allocate(references(0))
5681 call set_references(editor%references_panel, references, 0)
5682 return
5683 end if
5684
5685 ! Allocate references array
5686 allocate(references(num_refs))
5687
5688 ! Initialize all fields
5689 do i = 1, num_refs
5690 references(i)%line = 1
5691 references(i)%column = 1
5692 references(i)%end_line = 1
5693 references(i)%end_column = 1
5694 end do
5695
5696 ! Parse each reference location
5697 do i = 1, num_refs
5698 location_obj = json_get_array_element(result_array, i - 1)
5699
5700 ! Get URI
5701 uri = json_get_string(location_obj, 'uri', '')
5702 if (len(uri) > 0) then
5703 allocate(character(len=len(uri)) :: references(i)%uri)
5704 references(i)%uri = uri
5705
5706 ! Extract filename from URI
5707 if (len(uri) > 7) then
5708 if (uri(1:7) == "file://") then
5709 allocate(character(len=len(uri)-7) :: references(i)%filename)
5710 references(i)%filename = uri(8:)
5711 end if
5712 end if
5713 end if
5714
5715 ! Get range
5716 if (json_has_key(location_obj, 'range')) then
5717 range_obj = json_get_object(location_obj, 'range')
5718
5719 ! Get start position
5720 if (json_has_key(range_obj, 'start')) then
5721 start_obj = json_get_object(range_obj, 'start')
5722 line_real = json_get_number(start_obj, 'line', 0.0d0)
5723 references(i)%line = int(line_real) + 1 ! Convert from 0-based to 1-based
5724 col_real = json_get_number(start_obj, 'character', 0.0d0)
5725 references(i)%column = int(col_real) + 1 ! Convert from 0-based to 1-based
5726 end if
5727
5728 ! Get end position
5729 if (json_has_key(range_obj, 'end')) then
5730 end_obj = json_get_object(range_obj, 'end')
5731 line_real = json_get_number(end_obj, 'line', 0.0d0)
5732 references(i)%end_line = int(line_real) + 1
5733 col_real = json_get_number(end_obj, 'character', 0.0d0)
5734 references(i)%end_column = int(col_real) + 1
5735 end if
5736 end if
5737
5738 ! TODO: Load preview text from the file if available
5739 allocate(character(len=50) :: references(i)%preview_text)
5740 references(i)%preview_text = "..." ! Placeholder
5741 end do
5742
5743 ! Update the references panel
5744 call set_references(editor%references_panel, references, num_refs)
5745
5746 ! Clean up
5747 do i = 1, num_refs
5748 if (allocated(references(i)%uri)) deallocate(references(i)%uri)
5749 if (allocated(references(i)%filename)) deallocate(references(i)%filename)
5750 if (allocated(references(i)%preview_text)) deallocate(references(i)%preview_text)
5751 end do
5752 deallocate(references)
5753
5754 end subroutine handle_references_response_impl
5755
5756 ! Wrapper callback that matches the LSP callback signature for code actions
5757 subroutine handle_code_actions_response_wrapper(request_id, response)
5758 use lsp_protocol_module, only: lsp_message_t
5759 integer, intent(in) :: request_id
5760 type(lsp_message_t), intent(in) :: response
5761
5762 ! Call the actual handler with saved editor state
5763 if (allocated(saved_editor_for_callback)) then
5764 call handle_code_actions_response_impl(saved_editor_for_callback, response)
5765 end if
5766 end subroutine handle_code_actions_response_wrapper
5767
5768 ! Handle LSP textDocument/codeAction response implementation
5769 subroutine handle_code_actions_response_impl(editor, response)
5770 use lsp_protocol_module, only: lsp_message_t
5771 use json_module, only: json_value_t, json_get_array, json_get_object, &
5772 json_get_string, json_get_bool, json_array_size, &
5773 json_get_array_element, json_has_key, json_stringify
5774 use code_actions_menu_module, only: code_action_t
5775 type(editor_state_t), intent(inout) :: editor
5776 type(lsp_message_t), intent(in) :: response
5777 type(json_value_t) :: result_array, action_obj, edit_obj
5778 type(code_action_t), allocatable :: actions(:)
5779 integer :: num_actions, i
5780 character(len=:), allocatable :: title, kind, action_json
5781 logical :: is_preferred
5782
5783 ! The result is directly in response%result for LSP responses
5784 result_array = response%result
5785 num_actions = json_array_size(result_array)
5786
5787 if (num_actions == 0) then
5788 call hide_code_actions_menu(editor%code_actions_menu)
5789 call terminal_move_cursor(editor%screen_rows, 1)
5790 call terminal_write('No code actions available at this position ')
5791 return
5792 end if
5793
5794 ! Allocate and fill actions array
5795 allocate(actions(num_actions))
5796
5797 do i = 1, num_actions
5798 action_obj = json_get_array_element(result_array, i)
5799
5800 ! Get title (required)
5801 if (json_has_key(action_obj, 'title')) then
5802 title = json_get_string(action_obj, 'title', '')
5803 if (allocated(actions(i)%title)) deallocate(actions(i)%title)
5804 allocate(character(len=len(title)) :: actions(i)%title)
5805 actions(i)%title = title
5806 end if
5807
5808 ! Get kind (optional)
5809 if (json_has_key(action_obj, 'kind')) then
5810 kind = json_get_string(action_obj, 'kind', '')
5811 if (allocated(actions(i)%kind)) deallocate(actions(i)%kind)
5812 allocate(character(len=len(kind)) :: actions(i)%kind)
5813 actions(i)%kind = kind
5814 end if
5815
5816 ! Get isPreferred (optional)
5817 if (json_has_key(action_obj, 'isPreferred')) then
5818 actions(i)%is_preferred = json_get_bool(action_obj, 'isPreferred', .false.)
5819 else
5820 actions(i)%is_preferred = .false.
5821 end if
5822
5823 ! Store the entire action as JSON for later application
5824 action_json = json_stringify(action_obj)
5825 if (allocated(actions(i)%action_json)) deallocate(actions(i)%action_json)
5826 allocate(character(len=len(action_json)) :: actions(i)%action_json)
5827 actions(i)%action_json = action_json
5828 end do
5829
5830 ! Update the code actions menu
5831 call set_code_actions(editor%code_actions_menu, actions, num_actions)
5832
5833 ! Show success message
5834 call terminal_move_cursor(editor%screen_rows, 1)
5835 if (num_actions == 1) then
5836 call terminal_write('1 code action available ')
5837 else
5838 block
5839 character(len=50) :: msg
5840 write(msg, '(I0,A)') num_actions, ' code actions available '
5841 call terminal_write(trim(msg))
5842 end block
5843 end if
5844
5845 ! Clean up
5846 do i = 1, num_actions
5847 if (allocated(actions(i)%title)) deallocate(actions(i)%title)
5848 if (allocated(actions(i)%kind)) deallocate(actions(i)%kind)
5849 if (allocated(actions(i)%action_json)) deallocate(actions(i)%action_json)
5850 end do
5851 deallocate(actions)
5852
5853 end subroutine handle_code_actions_response_impl
5854
5855 ! Wrapper callback that matches the LSP callback signature for symbols
5856 subroutine handle_symbols_response_wrapper(request_id, response)
5857 use lsp_protocol_module, only: lsp_message_t
5858 integer, intent(in) :: request_id
5859 type(lsp_message_t), intent(in) :: response
5860
5861 ! Call the actual handler with saved editor state
5862 if (allocated(saved_editor_for_callback)) then
5863 call handle_symbols_response_impl(saved_editor_for_callback, response)
5864 end if
5865 end subroutine handle_symbols_response_wrapper
5866
5867 ! Handle LSP textDocument/documentSymbol response implementation
5868 subroutine handle_symbols_response_impl(editor, response)
5869 use lsp_protocol_module, only: lsp_message_t
5870 use json_module, only: json_value_t, json_get_array, json_get_object, &
5871 json_get_string, json_get_number, json_array_size, &
5872 json_get_array_element, json_has_key
5873 type(editor_state_t), intent(inout) :: editor
5874 type(lsp_message_t), intent(in) :: response
5875 type(json_value_t) :: result_array, symbol_obj, location_obj, range_obj
5876 type(json_value_t) :: start_obj, end_obj, children_array
5877 type(document_symbol_t), allocatable :: symbols(:)
5878 integer :: num_symbols, i
5879 character(len=:), allocatable :: name, detail
5880 real(8) :: kind_real, line_real, col_real
5881
5882 ! The result is directly in response%result for LSP responses
5883 result_array = response%result
5884 num_symbols = json_array_size(result_array)
5885
5886 if (num_symbols == 0) then
5887 call clear_symbols(editor%symbols_panel)
5888 call terminal_move_cursor(editor%screen_rows, 1)
5889 call terminal_write('No symbols found in document ')
5890 return
5891 end if
5892
5893 ! Allocate symbols array
5894 allocate(symbols(num_symbols))
5895
5896 ! Parse each symbol
5897 do i = 1, num_symbols
5898 symbol_obj = json_get_array_element(result_array, i)
5899
5900 ! Get symbol name (required)
5901 if (json_has_key(symbol_obj, 'name')) then
5902 name = json_get_string(symbol_obj, 'name', '')
5903 if (allocated(symbols(i)%name)) deallocate(symbols(i)%name)
5904 allocate(character(len=len(name)) :: symbols(i)%name)
5905 symbols(i)%name = name
5906 end if
5907
5908 ! Get detail (optional)
5909 if (json_has_key(symbol_obj, 'detail')) then
5910 detail = json_get_string(symbol_obj, 'detail', '')
5911 if (len(detail) > 0) then
5912 if (allocated(symbols(i)%detail)) deallocate(symbols(i)%detail)
5913 allocate(character(len=len(detail)) :: symbols(i)%detail)
5914 symbols(i)%detail = detail
5915 end if
5916 end if
5917
5918 ! Get kind (required)
5919 if (json_has_key(symbol_obj, 'kind')) then
5920 kind_real = json_get_number(symbol_obj, 'kind', 13.0d0) ! Default to Variable
5921 symbols(i)%kind = int(kind_real)
5922 else
5923 symbols(i)%kind = 13 ! Variable
5924 end if
5925
5926 ! Get range or location
5927 if (json_has_key(symbol_obj, 'range')) then
5928 ! DocumentSymbol format (hierarchical)
5929 range_obj = json_get_object(symbol_obj, 'range')
5930
5931 ! Get start position
5932 if (json_has_key(range_obj, 'start')) then
5933 start_obj = json_get_object(range_obj, 'start')
5934 line_real = json_get_number(start_obj, 'line', 0.0d0)
5935 symbols(i)%line = int(line_real) + 1
5936 col_real = json_get_number(start_obj, 'character', 0.0d0)
5937 symbols(i)%column = int(col_real) + 1
5938 end if
5939
5940 ! Get end position
5941 if (json_has_key(range_obj, 'end')) then
5942 end_obj = json_get_object(range_obj, 'end')
5943 line_real = json_get_number(end_obj, 'line', 0.0d0)
5944 symbols(i)%end_line = int(line_real) + 1
5945 col_real = json_get_number(end_obj, 'character', 0.0d0)
5946 symbols(i)%end_column = int(col_real) + 1
5947 end if
5948
5949 ! Check for children (hierarchical symbols)
5950 if (json_has_key(symbol_obj, 'children')) then
5951 children_array = json_get_array(symbol_obj, 'children')
5952 symbols(i)%num_children = json_array_size(children_array)
5953 ! TODO: Parse children recursively
5954 end if
5955
5956 else if (json_has_key(symbol_obj, 'location')) then
5957 ! SymbolInformation format (flat)
5958 location_obj = json_get_object(symbol_obj, 'location')
5959
5960 if (json_has_key(location_obj, 'range')) then
5961 range_obj = json_get_object(location_obj, 'range')
5962
5963 ! Get start position
5964 if (json_has_key(range_obj, 'start')) then
5965 start_obj = json_get_object(range_obj, 'start')
5966 line_real = json_get_number(start_obj, 'line', 0.0d0)
5967 symbols(i)%line = int(line_real) + 1
5968 col_real = json_get_number(start_obj, 'character', 0.0d0)
5969 symbols(i)%column = int(col_real) + 1
5970 end if
5971
5972 ! Get end position
5973 if (json_has_key(range_obj, 'end')) then
5974 end_obj = json_get_object(range_obj, 'end')
5975 line_real = json_get_number(end_obj, 'line', 0.0d0)
5976 symbols(i)%end_line = int(line_real) + 1
5977 col_real = json_get_number(end_obj, 'character', 0.0d0)
5978 symbols(i)%end_column = int(col_real) + 1
5979 end if
5980 end if
5981 end if
5982
5983 symbols(i)%depth = 0 ! Top level
5984 symbols(i)%is_expanded = .true.
5985 end do
5986
5987 ! Update the symbols panel
5988 call set_symbols(editor%symbols_panel, symbols, num_symbols)
5989
5990 ! Show success message
5991 call terminal_move_cursor(editor%screen_rows, 1)
5992 if (num_symbols == 1) then
5993 call terminal_write('1 symbol found ')
5994 else
5995 block
5996 character(len=50) :: msg
5997 write(msg, '(I0,A)') num_symbols, ' symbols found '
5998 call terminal_write(trim(msg))
5999 end block
6000 end if
6001
6002 ! Clean up
6003 do i = 1, num_symbols
6004 if (allocated(symbols(i)%name)) deallocate(symbols(i)%name)
6005 if (allocated(symbols(i)%detail)) deallocate(symbols(i)%detail)
6006 if (allocated(symbols(i)%children)) deallocate(symbols(i)%children)
6007 end do
6008 deallocate(symbols)
6009
6010 end subroutine handle_symbols_response_impl
6011
6012 ! Wrapper callback that matches the LSP callback signature for signature help
6013 subroutine handle_signature_response_wrapper(request_id, response)
6014 use lsp_protocol_module, only: lsp_message_t
6015 integer, intent(in) :: request_id
6016 type(lsp_message_t), intent(in) :: response
6017
6018 ! Call the actual handler with saved editor state
6019 if (allocated(saved_editor_for_callback)) then
6020 call handle_signature_response(saved_editor_for_callback%signature_tooltip, response)
6021 end if
6022 end subroutine handle_signature_response_wrapper
6023
6024 ! Wrapper callback that matches the LSP callback signature for rename
6025 subroutine handle_rename_response_wrapper(request_id, response)
6026 use lsp_protocol_module, only: lsp_message_t
6027 use json_module, only: json_value_t, json_stringify
6028 integer, intent(in) :: request_id
6029 type(lsp_message_t), intent(in) :: response
6030
6031 character(len=:), allocatable :: result_str
6032 integer :: changes_applied
6033
6034 if (.not. allocated(saved_editor_for_callback)) return
6035
6036 ! Convert result to string for apply_workspace_edit
6037 result_str = json_stringify(response%result)
6038 if (.not. allocated(result_str) .or. result_str == 'null' .or. len_trim(result_str) == 0) then
6039 call terminal_move_cursor(saved_editor_for_callback%screen_rows, 1)
6040 call terminal_write('Rename failed or not supported ')
6041 if (allocated(result_str)) deallocate(result_str)
6042 return
6043 end if
6044
6045 ! Apply workspace edit
6046 call apply_workspace_edit(saved_editor_for_callback, result_str, changes_applied)
6047
6048 call terminal_move_cursor(saved_editor_for_callback%screen_rows, 1)
6049 if (changes_applied > 0) then
6050 block
6051 character(len=64) :: msg
6052 write(msg, '(A,I0,A)') 'Renamed symbol (', changes_applied, ' changes applied)'
6053 call terminal_write(trim(msg) // ' ')
6054 end block
6055 else
6056 call terminal_write('No changes applied ')
6057 end if
6058
6059 if (allocated(result_str)) deallocate(result_str)
6060 end subroutine handle_rename_response_wrapper
6061
6062 ! Wrapper callback for formatting response
6063 subroutine handle_formatting_response_wrapper(request_id, response)
6064 use lsp_protocol_module, only: lsp_message_t
6065 use json_module, only: json_value_t, json_array_size, json_get_array_element, &
6066 json_get_object, json_get_string, json_get_number, json_has_key
6067 integer, intent(in) :: request_id
6068 type(lsp_message_t), intent(in) :: response
6069
6070 type(json_value_t) :: edits_array, edit_obj, range_obj, start_obj, end_obj
6071 character(len=:), allocatable :: new_text
6072 integer :: num_edits, i, tab_idx
6073 integer :: start_line, start_char, end_line, end_char
6074 integer :: changes_applied
6075
6076 if (.not. allocated(saved_editor_for_callback)) return
6077
6078 tab_idx = saved_editor_for_callback%active_tab_index
6079 if (tab_idx < 1 .or. tab_idx > size(saved_editor_for_callback%tabs)) return
6080
6081 ! The result is an array of TextEdit objects
6082 edits_array = response%result
6083 num_edits = json_array_size(edits_array)
6084
6085 if (num_edits == 0) then
6086 call terminal_move_cursor(saved_editor_for_callback%screen_rows, 1)
6087 call terminal_write('No formatting changes needed ')
6088 return
6089 end if
6090
6091 changes_applied = 0
6092
6093 ! Apply edits in reverse order (to preserve positions)
6094 do i = num_edits - 1, 0, -1
6095 edit_obj = json_get_array_element(edits_array, i)
6096
6097 if (.not. json_has_key(edit_obj, 'range')) cycle
6098 range_obj = json_get_object(edit_obj, 'range')
6099
6100 if (json_has_key(range_obj, 'start') .and. json_has_key(range_obj, 'end')) then
6101 start_obj = json_get_object(range_obj, 'start')
6102 end_obj = json_get_object(range_obj, 'end')
6103
6104 start_line = int(json_get_number(start_obj, 'line', 0.0d0)) + 1
6105 start_char = int(json_get_number(start_obj, 'character', 0.0d0)) + 1
6106 end_line = int(json_get_number(end_obj, 'line', 0.0d0)) + 1
6107 end_char = int(json_get_number(end_obj, 'character', 0.0d0)) + 1
6108
6109 new_text = json_get_string(edit_obj, 'newText')
6110
6111 if (allocated(new_text)) then
6112 call apply_single_edit(saved_editor_for_callback%tabs(tab_idx)%buffer, &
6113 start_line, start_char, end_line, end_char, new_text)
6114 changes_applied = changes_applied + 1
6115 deallocate(new_text)
6116 end if
6117 end if
6118 end do
6119
6120 call terminal_move_cursor(saved_editor_for_callback%screen_rows, 1)
6121 if (changes_applied > 0) then
6122 block
6123 character(len=64) :: msg
6124 write(msg, '(A,I0,A)') 'Formatted (', changes_applied, ' edits applied)'
6125 call terminal_write(trim(msg) // ' ')
6126 end block
6127 else
6128 call terminal_write('No formatting changes applied ')
6129 end if
6130 end subroutine handle_formatting_response_wrapper
6131
6132 ! Apply a workspace edit from LSP
6133 subroutine apply_workspace_edit(editor, edit_json, changes_applied)
6134 use json_module, only: json_parse, json_value_t, json_get_array, json_array_size, &
6135 json_get_array_element, json_get_object, json_get_string, &
6136 json_get_number, json_has_key
6137 type(editor_state_t), intent(inout) :: editor
6138 character(len=*), intent(in) :: edit_json
6139 integer, intent(out) :: changes_applied
6140
6141 type(json_value_t) :: edit_obj, doc_changes_arr, file_change_obj
6142 type(json_value_t) :: text_doc_obj, edits_arr
6143 character(len=:), allocatable :: uri
6144 integer :: num_files, i
6145
6146 changes_applied = 0
6147
6148 ! Parse the edit JSON
6149 edit_obj = json_parse(edit_json)
6150
6151 ! Try to get documentChanges first (newer format)
6152 if (json_has_key(edit_obj, 'documentChanges')) then
6153 doc_changes_arr = json_get_array(edit_obj, 'documentChanges')
6154 num_files = json_array_size(doc_changes_arr)
6155
6156 do i = 0, num_files - 1 ! 0-based index
6157 file_change_obj = json_get_array_element(doc_changes_arr, i)
6158
6159 ! Get text document URI
6160 if (json_has_key(file_change_obj, 'textDocument')) then
6161 text_doc_obj = json_get_object(file_change_obj, 'textDocument')
6162 uri = json_get_string(text_doc_obj, 'uri')
6163 end if
6164
6165 ! Get edits array
6166 if (json_has_key(file_change_obj, 'edits') .and. allocated(uri)) then
6167 edits_arr = json_get_array(file_change_obj, 'edits')
6168 call apply_file_edits_obj(editor, uri, edits_arr, changes_applied)
6169 deallocate(uri)
6170 end if
6171 end do
6172 return
6173 end if
6174
6175 ! Fall back to changes format (older format - map of URI to edits)
6176 if (json_has_key(edit_obj, 'changes')) then
6177 call terminal_move_cursor(editor%screen_rows, 1)
6178 call terminal_write('Workspace edit (changes format) not fully supported')
6179 return
6180 end if
6181
6182 end subroutine apply_workspace_edit
6183
6184 ! Apply edits to a specific file (using json_value_t)
6185 subroutine apply_file_edits_obj(editor, uri, edits_arr, changes_applied)
6186 use json_module, only: json_value_t, json_array_size, json_get_array_element, &
6187 json_get_object, json_get_string, json_get_number, json_has_key
6188 type(editor_state_t), intent(inout) :: editor
6189 character(len=*), intent(in) :: uri
6190 type(json_value_t), intent(in) :: edits_arr
6191 integer, intent(inout) :: changes_applied
6192
6193 type(json_value_t) :: edit_obj, range_obj, start_obj, end_obj
6194 character(len=:), allocatable :: filename, new_text
6195 integer :: num_edits, i, j, tab_idx
6196 integer :: start_line, start_char, end_line, end_char
6197
6198 ! Convert URI to filename
6199 if (len(uri) >= 7 .and. uri(1:7) == 'file://') then
6200 filename = uri(8:)
6201 else
6202 filename = uri
6203 end if
6204
6205 ! Find the tab with this file
6206 tab_idx = 0
6207 do j = 1, size(editor%tabs)
6208 if (allocated(editor%tabs(j)%filename)) then
6209 if (trim(editor%tabs(j)%filename) == trim(filename)) then
6210 tab_idx = j
6211 exit
6212 end if
6213 end if
6214 end do
6215
6216 if (tab_idx == 0) then
6217 ! File not open - skip for now
6218 if (allocated(filename)) deallocate(filename)
6219 return
6220 end if
6221
6222 ! Apply edits in reverse order (to preserve line numbers)
6223 num_edits = json_array_size(edits_arr)
6224
6225 do i = num_edits - 1, 0, -1 ! 0-based index, reverse order
6226 edit_obj = json_get_array_element(edits_arr, i)
6227
6228 ! Get range
6229 if (.not. json_has_key(edit_obj, 'range')) cycle
6230 range_obj = json_get_object(edit_obj, 'range')
6231
6232 if (json_has_key(range_obj, 'start') .and. json_has_key(range_obj, 'end')) then
6233 start_obj = json_get_object(range_obj, 'start')
6234 end_obj = json_get_object(range_obj, 'end')
6235
6236 start_line = int(json_get_number(start_obj, 'line', 0.0d0)) + 1
6237 start_char = int(json_get_number(start_obj, 'character', 0.0d0)) + 1
6238 end_line = int(json_get_number(end_obj, 'line', 0.0d0)) + 1
6239 end_char = int(json_get_number(end_obj, 'character', 0.0d0)) + 1
6240
6241 ! Get new text
6242 new_text = json_get_string(edit_obj, 'newText')
6243
6244 if (allocated(new_text)) then
6245 ! Apply the edit to the buffer
6246 call apply_single_edit(editor%tabs(tab_idx)%buffer, &
6247 start_line, start_char, end_line, end_char, new_text)
6248 changes_applied = changes_applied + 1
6249 deallocate(new_text)
6250 end if
6251 end if
6252 end do
6253
6254 if (allocated(filename)) deallocate(filename)
6255 end subroutine apply_file_edits_obj
6256
6257 ! Apply a single text edit to a buffer
6258 subroutine apply_single_edit(buffer, start_line, start_char, end_line, end_char, new_text)
6259 type(buffer_t), intent(inout) :: buffer
6260 integer, intent(in) :: start_line, start_char, end_line, end_char
6261 character(len=*), intent(in) :: new_text
6262
6263 integer :: start_pos, end_pos, delete_count
6264
6265 ! Calculate buffer positions
6266 start_pos = get_buffer_position(buffer, start_line, start_char)
6267 end_pos = get_buffer_position(buffer, end_line, end_char)
6268
6269 if (start_pos <= 0 .or. end_pos <= 0) return
6270
6271 ! Delete the old text
6272 delete_count = end_pos - start_pos
6273 if (delete_count > 0) then
6274 call buffer_delete(buffer, start_pos, delete_count)
6275 end if
6276
6277 ! Insert the new text
6278 if (len(new_text) > 0) then
6279 call buffer_insert(buffer, start_pos, new_text)
6280 end if
6281 end subroutine apply_single_edit
6282
6283 ! Execute a command from the command palette
6284 subroutine execute_palette_command(editor, buffer, cmd_id, should_quit)
6285 type(editor_state_t), intent(inout) :: editor
6286 type(buffer_t), intent(inout) :: buffer
6287 character(len=*), intent(in) :: cmd_id
6288 logical, intent(out) :: should_quit
6289
6290 ! For now, just display the selected command
6291 ! TODO: Implement full command execution in next iteration
6292 call terminal_move_cursor(editor%screen_rows, 1)
6293 call terminal_write('Selected: ' // trim(cmd_id) // ' ')
6294 should_quit = .false.
6295 end subroutine execute_palette_command
6296
6297 ! Handle workspace symbols LSP response
6298 subroutine handle_workspace_symbols_response_wrapper(request_id, response)
6299 use lsp_protocol_module, only: lsp_message_t
6300 use json_module
6301 use workspace_symbols_panel_module, only: workspace_symbol_t, set_workspace_symbols
6302 integer, intent(in) :: request_id
6303 type(lsp_message_t), intent(in) :: response
6304 type(json_value_t) :: result_array, symbol_obj, location_obj, range_obj, start_obj
6305 integer :: num_symbols, i
6306 type(workspace_symbol_t), allocatable :: symbols(:)
6307 character(len=:), allocatable :: name, kind_str, container, uri
6308 real(8) :: line_num, char_num, kind_num
6309
6310 num_symbols = json_array_size(response%result)
6311 if (num_symbols == 0) return
6312 allocate(symbols(num_symbols))
6313
6314 do i = 0, num_symbols - 1
6315 symbol_obj = json_get_array_element(response%result, i)
6316
6317 ! Get name
6318 name = json_get_string(symbol_obj, 'name', '')
6319 if (allocated(name)) then
6320 symbols(i+1)%name = name
6321 end if
6322
6323 ! Get kind (as number) and convert to string
6324 kind_num = json_get_number(symbol_obj, 'kind', 0.0d0)
6325 symbols(i+1)%kind_name = symbol_kind_to_string(int(kind_num))
6326
6327 ! Get container name (optional)
6328 container = json_get_string(symbol_obj, 'containerName', '')
6329 if (allocated(container)) then
6330 symbols(i+1)%container_name = container
6331 end if
6332
6333 ! Get location
6334 location_obj = json_get_object(symbol_obj, 'location')
6335 uri = json_get_string(location_obj, 'uri', '')
6336 if (allocated(uri) .and. len_trim(uri) > 0) then
6337 symbols(i+1)%file_uri = uri
6338
6339 ! Get range -> start -> line/character
6340 range_obj = json_get_object(location_obj, 'range')
6341 start_obj = json_get_object(range_obj, 'start')
6342 line_num = json_get_number(start_obj, 'line', 0.0d0)
6343 char_num = json_get_number(start_obj, 'character', 0.0d0)
6344 symbols(i+1)%line = int(line_num)
6345 symbols(i+1)%character = int(char_num)
6346 end if
6347 end do
6348
6349 ! Update the panel
6350 if (allocated(saved_editor_for_callback)) then
6351 call set_workspace_symbols(saved_editor_for_callback%workspace_symbols_panel, symbols, num_symbols)
6352 end if
6353
6354 if (allocated(symbols)) deallocate(symbols)
6355 end subroutine handle_workspace_symbols_response_wrapper
6356
6357 ! Helper to convert LSP symbol kind number to string
6358 function symbol_kind_to_string(kind) result(kind_str)
6359 integer, intent(in) :: kind
6360 character(len=:), allocatable :: kind_str
6361
6362 select case(kind)
6363 case(1); kind_str = "File"
6364 case(2); kind_str = "Module"
6365 case(3); kind_str = "Namespace"
6366 case(4); kind_str = "Package"
6367 case(5); kind_str = "Class"
6368 case(6); kind_str = "Method"
6369 case(7); kind_str = "Property"
6370 case(8); kind_str = "Field"
6371 case(9); kind_str = "Constructor"
6372 case(10); kind_str = "Enum"
6373 case(11); kind_str = "Interface"
6374 case(12); kind_str = "Function"
6375 case(13); kind_str = "Variable"
6376 case(14); kind_str = "Constant"
6377 case(15); kind_str = "String"
6378 case(16); kind_str = "Number"
6379 case(17); kind_str = "Boolean"
6380 case(18); kind_str = "Array"
6381 case default; kind_str = "Unknown"
6382 end select
6383 end function symbol_kind_to_string
6384
6385 ! Navigate to a workspace symbol
6386 subroutine navigate_to_workspace_symbol(editor, buffer, symbol, should_quit)
6387 use workspace_symbols_panel_module, only: workspace_symbol_t
6388 use jump_stack_module, only: push_jump_location
6389 use editor_state_module, only: switch_to_tab, create_tab, sync_pane_to_editor, sync_editor_to_pane
6390 use text_buffer_module, only: buffer_load_file, copy_buffer
6391 type(editor_state_t), intent(inout) :: editor
6392 type(buffer_t), intent(inout) :: buffer
6393 type(workspace_symbol_t), intent(in) :: symbol
6394 logical, intent(out) :: should_quit
6395 character(len=:), allocatable :: filepath
6396 integer :: i
6397
6398 should_quit = .false.
6399
6400 ! Convert file:// URI to filepath
6401 if (index(symbol%file_uri, "file://") == 1) then
6402 filepath = symbol%file_uri(8:) ! Remove "file://"
6403 else
6404 filepath = symbol%file_uri
6405 end if
6406
6407 ! Push current location to jump stack
6408 if (allocated(editor%filename)) then
6409 call push_jump_location(editor%jump_stack, editor%filename, &
6410 editor%cursors(editor%active_cursor)%line, &
6411 editor%cursors(editor%active_cursor)%column)
6412 end if
6413
6414 ! Check if file is already open in a tab
6415 do i = 1, size(editor%tabs)
6416 if (allocated(editor%tabs(i)%filename)) then
6417 if (trim(editor%tabs(i)%filename) == trim(filepath)) then
6418 call switch_to_tab(editor, i)
6419 ! Jump to the symbol's position
6420 editor%cursors(editor%active_cursor)%line = symbol%line + 1 ! LSP is 0-based
6421 editor%cursors(editor%active_cursor)%column = symbol%character + 1
6422 editor%cursors(editor%active_cursor)%desired_column = symbol%character + 1
6423 editor%viewport_line = max(1, symbol%line + 1 - editor%screen_rows / 2)
6424 return
6425 end if
6426 end if
6427 end do
6428
6429 ! File not open - create a new tab and load the file
6430 call create_tab(editor, filepath)
6431
6432 ! Load file content into the new tab's buffer
6433 block
6434 integer :: status, new_tab_idx
6435
6436 new_tab_idx = size(editor%tabs) ! The tab we just created
6437
6438 call buffer_load_file(editor%tabs(new_tab_idx)%buffer, filepath, status)
6439
6440 if (status == 0) then
6441 ! File loaded successfully
6442 ! Copy buffer to the pane's buffer
6443 if (allocated(editor%tabs(new_tab_idx)%panes)) then
6444 call copy_buffer(editor%tabs(new_tab_idx)%panes(1)%buffer, editor%tabs(new_tab_idx)%buffer)
6445 end if
6446
6447 ! Switch to the new tab
6448 call switch_to_tab(editor, new_tab_idx)
6449
6450 ! Sync the pane to editor state (this updates editor%cursors, etc.)
6451 call sync_pane_to_editor(editor, new_tab_idx, 1)
6452
6453 ! Navigate to the symbol's position
6454 editor%cursors(editor%active_cursor)%line = symbol%line + 1 ! LSP is 0-based
6455 editor%cursors(editor%active_cursor)%column = symbol%character + 1
6456 editor%cursors(editor%active_cursor)%desired_column = symbol%character + 1
6457 editor%viewport_line = max(1, symbol%line + 1 - editor%screen_rows / 2)
6458
6459 ! Sync editor state back to pane
6460 call sync_editor_to_pane(editor)
6461 else
6462 ! File load failed - could show error message
6463 ! For now, just don't navigate
6464 continue
6465 end if
6466 end block
6467 end subroutine navigate_to_workspace_symbol
6468
6469 ! ==================================================
6470 ! LSP Definition Response Handler
6471 ! ==================================================
6472
6473 ! Wrapper callback for go to definition
6474 subroutine handle_definition_response_wrapper(request_id, response)
6475 use lsp_protocol_module, only: lsp_message_t
6476 integer, intent(in) :: request_id
6477 type(lsp_message_t), intent(in) :: response
6478
6479 ! Call actual handler with saved editor state
6480 if (allocated(saved_editor_for_callback)) then
6481 call handle_definition_response_impl(saved_editor_for_callback, response)
6482 end if
6483 end subroutine handle_definition_response_wrapper
6484
6485 ! Handle LSP textDocument/definition response
6486 subroutine handle_definition_response_impl(editor, response)
6487 use lsp_protocol_module, only: lsp_message_t
6488 use json_module, only: json_value_t, json_get_object, json_get_string, &
6489 json_get_number, json_array_size, json_get_array_element, &
6490 json_has_key
6491 type(editor_state_t), intent(inout) :: editor
6492 type(lsp_message_t), intent(in) :: response
6493 type(json_value_t) :: location_obj, range_obj, start_obj
6494 character(len=:), allocatable :: uri, filepath
6495 real(8) :: line_real, col_real
6496 integer :: target_line, target_col, i, num_locations
6497 logical :: found_file
6498
6499 ! Log response for debugging
6500 block
6501 integer :: debug_unit
6502 open(newunit=debug_unit, file='/tmp/fac_keys.log', position='append', action='write')
6503 write(debug_unit, '(A)') '>>> DEFINITION RESPONSE RECEIVED <<<'
6504 close(debug_unit)
6505 end block
6506
6507 ! Try to treat result as array first
6508 num_locations = json_array_size(response%result)
6509
6510 if (num_locations > 0) then
6511 ! Array of locations - take first one
6512 location_obj = json_get_array_element(response%result, 0)
6513 else if (json_has_key(response%result, "uri")) then
6514 ! Single location object
6515 location_obj = response%result
6516 else
6517 ! No definition found
6518 call terminal_move_cursor(editor%screen_rows, 1)
6519 call terminal_write('No definition found ')
6520 return
6521 end if
6522
6523 ! Extract URI
6524 uri = json_get_string(location_obj, 'uri', '')
6525 if (len(uri) == 0) then
6526 call terminal_move_cursor(editor%screen_rows, 1)
6527 call terminal_write('Invalid definition response ')
6528 return
6529 end if
6530
6531 ! Convert URI to filepath (remove file:// prefix)
6532 if (len(uri) > 7 .and. uri(1:7) == 'file://') then
6533 filepath = uri(8:)
6534 else
6535 filepath = uri
6536 end if
6537
6538 ! Get range
6539 range_obj = json_get_object(location_obj, 'range')
6540 start_obj = json_get_object(range_obj, 'start')
6541
6542 line_real = json_get_number(start_obj, 'line', 0.0d0)
6543 col_real = json_get_number(start_obj, 'character', 0.0d0)
6544
6545 ! Convert from 0-based LSP to 1-based editor coordinates
6546 target_line = int(line_real) + 1
6547 target_col = int(col_real) + 1
6548
6549 ! Log details
6550 block
6551 integer :: debug_unit
6552 open(newunit=debug_unit, file='/tmp/fac_keys.log', position='append', action='write')
6553 write(debug_unit, '(A)') 'File: ' // trim(filepath)
6554 write(debug_unit, '(A,I0,A,I0)') 'Position: line=', target_line, ', col=', target_col
6555 close(debug_unit)
6556 end block
6557
6558 ! Check if the file is already open in a tab
6559 found_file = .false.
6560 do i = 1, size(editor%tabs)
6561 if (allocated(editor%tabs(i)%filename)) then
6562 if (trim(editor%tabs(i)%filename) == trim(filepath)) then
6563 ! Switch to this tab
6564 editor%active_tab_index = i
6565 found_file = .true.
6566 exit
6567 end if
6568 end if
6569 end do
6570
6571 ! If file not found in tabs, report location
6572 if (.not. found_file) then
6573 call terminal_move_cursor(editor%screen_rows, 1)
6574 call terminal_write('Found in: ' // trim(filepath) // ' ')
6575 return
6576 end if
6577
6578 ! Jump to the line and column in current tab
6579 editor%cursors(editor%active_cursor)%line = target_line
6580 editor%cursors(editor%active_cursor)%column = target_col
6581
6582 ! Center viewport on target
6583 editor%viewport_line = max(1, target_line - editor%screen_rows / 2)
6584
6585 call terminal_move_cursor(editor%screen_rows, 1)
6586 call terminal_write('Jumped to definition ')
6587 end subroutine handle_definition_response_impl
6588
6589 end module command_handler_module
6590