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