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