Fortran · 60858 bytes Raw Blame History
1 module editor_state_module
2 use iso_fortran_env, only: int32, int64
3 use text_buffer_module, only: buffer_t, copy_buffer, init_buffer
4 use lsp_server_manager_module, only: lsp_manager_t, init_lsp_manager, cleanup_lsp_manager, &
5 get_or_start_server, process_server_messages, &
6 start_lsp_for_file, start_all_lsp_servers_for_file, &
7 get_server_with_capability, notify_file_opened, &
8 notify_file_changed, notify_file_closed, &
9 request_completion, request_hover
10 use completion_popup_module, only: completion_popup_t, init_completion_popup, &
11 cleanup_completion_popup
12 use hover_tooltip_module, only: hover_tooltip_t, init_hover_tooltip, &
13 cleanup_hover_tooltip
14 use diagnostics_module, only: diagnostics_store_t, init_diagnostics_store, &
15 cleanup_diagnostics_store
16 use diagnostics_panel_module, only: diagnostics_panel_t, init_diagnostics_panel, &
17 cleanup_diagnostics_panel
18 use references_panel_module, only: references_panel_t, init_references_panel, &
19 cleanup_references_panel
20 use code_actions_panel_module, only: code_actions_panel_t, init_code_actions_panel, &
21 cleanup_code_actions_panel
22 use symbols_panel_module, only: symbols_panel_t, init_symbols_panel, &
23 cleanup_symbols_panel
24 use signature_tooltip_module, only: signature_tooltip_t, init_signature_tooltip, &
25 cleanup_signature_tooltip
26 use command_palette_module, only: command_palette_t, init_command_palette, &
27 cleanup_command_palette
28 use workspace_symbols_panel_module, only: workspace_symbols_panel_t, init_workspace_symbols_panel, &
29 cleanup_workspace_symbols_panel
30 use document_sync_module, only: document_sync_t, init_document_sync, &
31 cleanup_document_sync
32 use jump_stack_module, only: jump_stack_t, init_jump_stack, &
33 cleanup_jump_stack
34 use lsp_server_installer_panel_module, only: lsp_server_installer_panel_t, &
35 init_lsp_server_installer_panel, &
36 cleanup_lsp_server_installer_panel
37 implicit none
38 private
39
40 public :: editor_state_t, cursor_t, pane_t, tab_t
41 public :: init_editor, cleanup_editor
42 public :: create_tab, switch_to_tab, switch_to_tab_with_buffer, get_active_tab_index, close_tab
43 public :: split_pane_vertical, split_pane_horizontal, close_pane, get_active_pane_indices
44 public :: navigate_to_pane_left, navigate_to_pane_right, navigate_to_pane_up, navigate_to_pane_down
45 public :: sync_pane_to_editor, sync_editor_to_pane, switch_to_pane, switch_to_pane_with_buffer
46 public :: sync_buffer_to_all_instances
47
48 ! Cursor position and selection
49 ! Cursor type - positions are UTF-8 CHARACTER indices (not byte indices)
50 !
51 ! IMPORTANT: All column values are 1-based CHARACTER positions, NOT byte positions
52 ! For example, in the string "├──", column=2 refers to the second character (─),
53 ! even though that character starts at byte 4.
54 type :: cursor_t
55 integer(int32) :: line = 1 ! Line number (1-based)
56 integer(int32) :: column = 1 ! UTF-8 character position (1-based), NOT byte index
57 integer(int32) :: desired_column = 1 ! For vertical movement (character position)
58 logical :: has_selection = .false.
59 integer(int32) :: selection_start_line = 1
60 integer(int32) :: selection_start_col = 1 ! UTF-8 character position
61 end type cursor_t
62
63 ! Pane - represents a view within a tab
64 type :: pane_t
65 ! Position within tab (0.0 to 1.0 normalized coordinates)
66 real :: x_start = 0.0
67 real :: y_start = 0.0
68 real :: x_end = 1.0
69 real :: y_end = 1.0
70
71 ! Actual screen coordinates (calculated during render)
72 integer :: screen_col = 1
73 integer :: screen_row = 1
74 integer :: screen_width = 80
75 integer :: screen_height = 24
76
77 ! Each pane has its own buffer and file
78 type(buffer_t) :: buffer
79 character(len=:), allocatable :: filename
80
81 ! Independent view state
82 integer(int32) :: viewport_line = 1
83 integer(int32) :: viewport_column = 1
84 type(cursor_t), allocatable :: cursors(:)
85 integer(int32) :: active_cursor = 1
86
87 ! State
88 logical :: is_active = .false.
89 end type pane_t
90
91 ! Tab - represents a single file buffer with one or more panes
92 type :: tab_t
93 character(len=:), allocatable :: filename
94 type(buffer_t) :: buffer
95
96 ! Panes within this tab
97 type(pane_t), allocatable :: panes(:)
98 integer(int32) :: active_pane_index = 1
99
100 logical :: modified = .false.
101 logical :: is_orphan = .false. ! True if file is outside workspace (uses absolute path)
102
103 ! LSP support - multiple servers per file
104 integer, allocatable :: lsp_server_indices(:) ! Indices of LSP servers handling this file
105 integer :: num_lsp_servers = 0 ! Number of active LSP servers
106 type(document_sync_t) :: document_sync ! Document synchronization for LSP
107 end type tab_t
108
109 ! Main editor state
110 type :: editor_state_t
111 ! Legacy fields (kept for backward compatibility during transition)
112 type(cursor_t), allocatable :: cursors(:)
113 integer(int32) :: active_cursor = 1
114 integer(int32) :: viewport_line = 1
115 integer(int32) :: viewport_column = 1
116 integer(int32) :: screen_rows = 24
117 integer(int32) :: screen_cols = 80
118 character(len=:), allocatable :: filename
119 character(len=:), allocatable :: workspace_path ! Current working directory
120 logical :: modified = .false.
121 logical :: fuss_mode_active = .false. ! Toggle for file tree mode
122 logical :: fuss_hints_expanded = .false. ! Toggle for expanded fuss legend
123
124 ! Tab management
125 type(tab_t), allocatable :: tabs(:)
126 integer(int32) :: active_tab_index = 1
127 integer(int32) :: max_tabs = 10
128
129 ! LSP support
130 type(lsp_manager_t) :: lsp_manager
131 type(completion_popup_t) :: completion_popup
132 type(hover_tooltip_t) :: hover_tooltip
133 type(diagnostics_store_t) :: diagnostics
134 type(diagnostics_panel_t) :: diagnostics_panel
135 type(references_panel_t) :: references_panel
136 type(code_actions_panel_t) :: code_actions_panel
137 type(symbols_panel_t) :: symbols_panel
138 type(signature_tooltip_t) :: signature_tooltip
139 type(command_palette_t) :: command_palette
140 type(workspace_symbols_panel_t) :: workspace_symbols_panel
141 type(lsp_server_installer_panel_t) :: lsp_installer_panel
142
143 ! Navigation
144 type(jump_stack_t) :: jump_stack
145 end type editor_state_t
146
147 contains
148
149 subroutine init_editor(editor)
150 type(editor_state_t), intent(out) :: editor
151
152 ! Initialize with single cursor
153 allocate(editor%cursors(1))
154 editor%cursors(1)%line = 1
155 editor%cursors(1)%column = 1
156 editor%cursors(1)%desired_column = 1
157 editor%active_cursor = 1
158
159 ! Default screen size (will be updated by terminal query)
160 editor%screen_rows = 24
161 editor%screen_cols = 80
162 editor%viewport_line = 1
163 editor%viewport_column = 1
164
165 editor%modified = .false.
166
167 ! Initialize tabs array (empty initially)
168 allocate(editor%tabs(0))
169 editor%active_tab_index = 0
170
171 ! Initialize LSP manager
172 call init_lsp_manager(editor%lsp_manager)
173
174 ! Initialize completion popup
175 call init_completion_popup(editor%completion_popup)
176
177 ! Initialize hover tooltip
178 call init_hover_tooltip(editor%hover_tooltip)
179
180 ! Initialize diagnostics store
181 call init_diagnostics_store(editor%diagnostics)
182
183 ! Initialize diagnostics panel
184 call init_diagnostics_panel(editor%diagnostics_panel)
185
186 ! Initialize references panel
187 call init_references_panel(editor%references_panel)
188
189 ! Initialize code actions menu
190 call init_code_actions_panel(editor%code_actions_panel)
191
192 ! Initialize symbols panel
193 call init_symbols_panel(editor%symbols_panel)
194
195 ! Initialize signature tooltip
196 call init_signature_tooltip(editor%signature_tooltip)
197
198 ! Initialize command palette
199 call init_command_palette(editor%command_palette)
200
201 ! Initialize workspace symbols panel
202 call init_workspace_symbols_panel(editor%workspace_symbols_panel)
203
204 ! Initialize jump stack
205 call init_jump_stack(editor%jump_stack)
206
207 ! Initialize LSP server installer panel
208 call init_lsp_server_installer_panel(editor%lsp_installer_panel)
209 end subroutine init_editor
210
211 subroutine cleanup_editor(editor)
212 type(editor_state_t), intent(inout) :: editor
213 integer :: i
214
215 if (allocated(editor%cursors)) deallocate(editor%cursors)
216 if (allocated(editor%filename)) deallocate(editor%filename)
217 if (allocated(editor%workspace_path)) deallocate(editor%workspace_path)
218
219 ! Cleanup tabs
220 if (allocated(editor%tabs)) then
221 do i = 1, size(editor%tabs)
222 call cleanup_tab(editor%tabs(i))
223 end do
224 deallocate(editor%tabs)
225 end if
226
227 ! Cleanup LSP manager
228 call cleanup_lsp_manager(editor%lsp_manager)
229
230 ! Cleanup completion popup
231 call cleanup_completion_popup(editor%completion_popup)
232
233 ! Cleanup hover tooltip
234 call cleanup_hover_tooltip(editor%hover_tooltip)
235
236 ! Cleanup diagnostics store
237 call cleanup_diagnostics_store(editor%diagnostics)
238
239 ! Cleanup diagnostics panel
240 call cleanup_diagnostics_panel(editor%diagnostics_panel)
241
242 ! Cleanup references panel
243 call cleanup_references_panel(editor%references_panel)
244
245 ! Cleanup code actions menu
246 call cleanup_code_actions_panel(editor%code_actions_panel)
247
248 ! Cleanup symbols panel
249 call cleanup_symbols_panel(editor%symbols_panel)
250
251 ! Cleanup signature tooltip
252 call cleanup_signature_tooltip(editor%signature_tooltip)
253
254 ! Cleanup command palette
255 call cleanup_command_palette(editor%command_palette)
256
257 ! Cleanup workspace symbols panel
258 call cleanup_workspace_symbols_panel(editor%workspace_symbols_panel)
259
260 ! Cleanup jump stack
261 call cleanup_jump_stack(editor%jump_stack)
262
263 ! Cleanup LSP server installer panel
264 call cleanup_lsp_server_installer_panel(editor%lsp_installer_panel)
265 end subroutine cleanup_editor
266
267 ! Helper to cleanup a single tab
268 subroutine cleanup_tab(tab)
269 use text_buffer_module, only: cleanup_buffer
270 type(tab_t), intent(inout) :: tab
271 integer :: i
272
273 if (allocated(tab%filename)) deallocate(tab%filename)
274
275 ! Cleanup panes
276 if (allocated(tab%panes)) then
277 do i = 1, size(tab%panes)
278 if (allocated(tab%panes(i)%cursors)) deallocate(tab%panes(i)%cursors)
279 end do
280 deallocate(tab%panes)
281 end if
282
283 call cleanup_buffer(tab%buffer)
284
285 ! Cleanup LSP server indices
286 if (allocated(tab%lsp_server_indices)) deallocate(tab%lsp_server_indices)
287 tab%num_lsp_servers = 0
288
289 ! Cleanup document sync
290 call cleanup_document_sync(tab%document_sync)
291 end subroutine cleanup_tab
292
293 ! Create a new tab with the given filename
294 subroutine create_tab(editor, filename)
295 use text_buffer_module, only: init_buffer
296 type(editor_state_t), intent(inout) :: editor
297 character(len=*), intent(in) :: filename
298 type(tab_t), allocatable :: temp_tabs(:)
299 integer :: n_tabs, new_index
300
301 n_tabs = size(editor%tabs)
302
303 ! Check max tabs limit
304 if (n_tabs >= editor%max_tabs) then
305 ! Could add error handling here
306 return
307 end if
308
309 ! Resize tabs array
310 allocate(temp_tabs(n_tabs + 1))
311 if (n_tabs > 0) then
312 temp_tabs(1:n_tabs) = editor%tabs(1:n_tabs)
313 end if
314
315 ! Initialize new tab
316 new_index = n_tabs + 1
317 allocate(character(len=len_trim(filename)) :: temp_tabs(new_index)%filename)
318 temp_tabs(new_index)%filename = trim(filename)
319 call init_buffer(temp_tabs(new_index)%buffer)
320
321 ! Create default pane (full screen)
322 allocate(temp_tabs(new_index)%panes(1))
323 temp_tabs(new_index)%panes(1)%x_start = 0.0
324 temp_tabs(new_index)%panes(1)%y_start = 0.0
325 temp_tabs(new_index)%panes(1)%x_end = 1.0
326 temp_tabs(new_index)%panes(1)%y_end = 1.0
327 temp_tabs(new_index)%panes(1)%viewport_line = 1
328 temp_tabs(new_index)%panes(1)%viewport_column = 1
329 temp_tabs(new_index)%panes(1)%is_active = .true.
330
331 ! Initialize screen coordinates for the default pane
332 ! These will be updated during rendering, but set reasonable defaults
333 temp_tabs(new_index)%panes(1)%screen_col = 1
334 temp_tabs(new_index)%panes(1)%screen_row = 2 ! After tab bar
335 temp_tabs(new_index)%panes(1)%screen_width = 80 ! Default width
336 temp_tabs(new_index)%panes(1)%screen_height = 22 ! Default height (24 - 2)
337
338 ! Initialize cursor in the default pane
339 allocate(temp_tabs(new_index)%panes(1)%cursors(1))
340 temp_tabs(new_index)%panes(1)%cursors(1)%line = 1
341 temp_tabs(new_index)%panes(1)%cursors(1)%column = 1
342 temp_tabs(new_index)%panes(1)%cursors(1)%desired_column = 1
343 temp_tabs(new_index)%panes(1)%cursors(1)%has_selection = .false.
344 temp_tabs(new_index)%panes(1)%active_cursor = 1
345
346 ! Initialize pane's buffer and filename (copy from tab)
347 call init_buffer(temp_tabs(new_index)%panes(1)%buffer)
348 call copy_buffer(temp_tabs(new_index)%panes(1)%buffer, temp_tabs(new_index)%buffer)
349 allocate(character(len=len_trim(filename)) :: temp_tabs(new_index)%panes(1)%filename)
350 temp_tabs(new_index)%panes(1)%filename = trim(filename)
351
352 temp_tabs(new_index)%active_pane_index = 1
353 temp_tabs(new_index)%modified = .false.
354
355 ! Start ALL LSP servers for this file (multi-server support)
356 call start_all_lsp_servers_for_file(editor%lsp_manager, filename, &
357 temp_tabs(new_index)%lsp_server_indices, &
358 temp_tabs(new_index)%num_lsp_servers)
359
360 ! Initialize document sync for LSP if we have servers
361 if (temp_tabs(new_index)%num_lsp_servers > 0) then
362 block
363 character(len=:), allocatable :: file_uri
364 file_uri = 'file://' // trim(filename)
365 ! Use first server for document sync (primary server)
366 call init_document_sync(temp_tabs(new_index)%document_sync, &
367 file_uri, temp_tabs(new_index)%lsp_server_indices(1))
368 end block
369 end if
370
371 ! Replace tabs array
372 call move_alloc(temp_tabs, editor%tabs)
373 editor%active_tab_index = new_index
374 end subroutine create_tab
375
376 ! Switch to a specific tab index (1-based)
377 subroutine switch_to_tab(editor, tab_index)
378 type(editor_state_t), intent(inout) :: editor
379 integer(int32), intent(in) :: tab_index
380 integer :: pane_idx
381
382 if (tab_index < 1 .or. tab_index > size(editor%tabs)) return
383
384 ! Save current tab state (if any)
385 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
386 ! Save to active pane of current tab
387 pane_idx = editor%tabs(editor%active_tab_index)%active_pane_index
388 if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. &
389 pane_idx > 0 .and. pane_idx <= size(editor%tabs(editor%active_tab_index)%panes)) then
390 editor%tabs(editor%active_tab_index)%panes(pane_idx)%cursors = editor%cursors
391 editor%tabs(editor%active_tab_index)%panes(pane_idx)%active_cursor = editor%active_cursor
392 editor%tabs(editor%active_tab_index)%panes(pane_idx)%viewport_line = editor%viewport_line
393 editor%tabs(editor%active_tab_index)%panes(pane_idx)%viewport_column = editor%viewport_column
394 end if
395 editor%tabs(editor%active_tab_index)%modified = editor%modified
396 end if
397
398 ! Load new tab state
399 editor%active_tab_index = tab_index
400
401 ! Load from active pane of new tab
402 pane_idx = editor%tabs(tab_index)%active_pane_index
403 if (allocated(editor%tabs(tab_index)%panes) .and. &
404 pane_idx > 0 .and. pane_idx <= size(editor%tabs(tab_index)%panes)) then
405
406 if (allocated(editor%cursors)) deallocate(editor%cursors)
407 allocate(editor%cursors(size(editor%tabs(tab_index)%panes(pane_idx)%cursors)))
408 editor%cursors = editor%tabs(tab_index)%panes(pane_idx)%cursors
409 editor%active_cursor = editor%tabs(tab_index)%panes(pane_idx)%active_cursor
410 editor%viewport_line = editor%tabs(tab_index)%panes(pane_idx)%viewport_line
411 editor%viewport_column = editor%tabs(tab_index)%panes(pane_idx)%viewport_column
412 end if
413
414 if (allocated(editor%filename)) deallocate(editor%filename)
415 allocate(character(len=len(editor%tabs(tab_index)%filename)) :: editor%filename)
416 editor%filename = editor%tabs(tab_index)%filename
417 editor%modified = editor%tabs(tab_index)%modified
418 end subroutine switch_to_tab
419
420 ! Switch to a tab with buffer synchronization
421 subroutine switch_to_tab_with_buffer(editor, tab_index, buffer)
422 use text_buffer_module, only: copy_buffer
423 type(editor_state_t), intent(inout) :: editor
424 integer(int32), intent(in) :: tab_index
425 type(buffer_t), intent(inout) :: buffer
426 integer :: pane_idx
427
428 if (tab_index < 1 .or. tab_index > size(editor%tabs)) return
429
430 ! Save current buffer to current tab's active pane (if any)
431 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
432 ! Save to active pane of current tab
433 pane_idx = editor%tabs(editor%active_tab_index)%active_pane_index
434 if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. &
435 pane_idx > 0 .and. pane_idx <= size(editor%tabs(editor%active_tab_index)%panes)) then
436 ! Save buffer to pane's buffer
437 call copy_buffer(editor%tabs(editor%active_tab_index)%panes(pane_idx)%buffer, buffer)
438
439 ! Sync buffer to all other instances of this file
440 if (allocated(editor%tabs(editor%active_tab_index)%panes(pane_idx)%filename)) then
441 call sync_buffer_to_all_instances(editor, &
442 editor%tabs(editor%active_tab_index)%panes(pane_idx)%filename, buffer)
443 end if
444
445 ! Save cursor and viewport state
446 editor%tabs(editor%active_tab_index)%panes(pane_idx)%cursors = editor%cursors
447 editor%tabs(editor%active_tab_index)%panes(pane_idx)%active_cursor = editor%active_cursor
448 editor%tabs(editor%active_tab_index)%panes(pane_idx)%viewport_line = editor%viewport_line
449 editor%tabs(editor%active_tab_index)%panes(pane_idx)%viewport_column = editor%viewport_column
450 end if
451 ! Also save to tab's buffer for backwards compatibility
452 call copy_buffer(editor%tabs(editor%active_tab_index)%buffer, buffer)
453 editor%tabs(editor%active_tab_index)%modified = editor%modified
454 end if
455
456 ! Switch to new tab
457 editor%active_tab_index = tab_index
458
459 ! Load new tab's active pane buffer
460 pane_idx = editor%tabs(tab_index)%active_pane_index
461 if (allocated(editor%tabs(tab_index)%panes) .and. &
462 pane_idx > 0 .and. pane_idx <= size(editor%tabs(tab_index)%panes)) then
463 call copy_buffer(buffer, editor%tabs(tab_index)%panes(pane_idx)%buffer)
464 else
465 ! Fallback to tab buffer if no pane
466 call copy_buffer(buffer, editor%tabs(tab_index)%buffer)
467 end if
468
469 ! Load from active pane of new tab
470 pane_idx = editor%tabs(tab_index)%active_pane_index
471 if (allocated(editor%tabs(tab_index)%panes) .and. &
472 pane_idx > 0 .and. pane_idx <= size(editor%tabs(tab_index)%panes)) then
473
474 if (allocated(editor%cursors)) deallocate(editor%cursors)
475 allocate(editor%cursors(size(editor%tabs(tab_index)%panes(pane_idx)%cursors)))
476 editor%cursors = editor%tabs(tab_index)%panes(pane_idx)%cursors
477 editor%active_cursor = editor%tabs(tab_index)%panes(pane_idx)%active_cursor
478 editor%viewport_line = editor%tabs(tab_index)%panes(pane_idx)%viewport_line
479 editor%viewport_column = editor%tabs(tab_index)%panes(pane_idx)%viewport_column
480 end if
481
482 if (allocated(editor%filename)) deallocate(editor%filename)
483 allocate(character(len=len(editor%tabs(tab_index)%filename)) :: editor%filename)
484 editor%filename = editor%tabs(tab_index)%filename
485 editor%modified = editor%tabs(tab_index)%modified
486 end subroutine switch_to_tab_with_buffer
487
488 ! Get the active tab index
489 function get_active_tab_index(editor) result(index)
490 type(editor_state_t), intent(in) :: editor
491 integer(int32) :: index
492 index = editor%active_tab_index
493 end function get_active_tab_index
494
495 ! Close a tab
496 subroutine close_tab(editor, tab_index)
497 type(editor_state_t), intent(inout) :: editor
498 integer(int32), intent(in) :: tab_index
499 type(tab_t), allocatable :: temp_tabs(:)
500 integer :: n_tabs, i, j
501
502 n_tabs = size(editor%tabs)
503 if (tab_index < 1 .or. tab_index > n_tabs) return
504
505 ! Cleanup the tab being closed
506 call cleanup_tab(editor%tabs(tab_index))
507
508 if (n_tabs == 1) then
509 ! Last tab - just deallocate the array
510 deallocate(editor%tabs)
511 allocate(editor%tabs(0))
512 editor%active_tab_index = 0
513 return
514 end if
515
516 ! Create new array without this tab
517 allocate(temp_tabs(n_tabs - 1))
518 j = 1
519 do i = 1, n_tabs
520 if (i /= tab_index) then
521 temp_tabs(j) = editor%tabs(i)
522 j = j + 1
523 end if
524 end do
525
526 ! Replace tabs array
527 call move_alloc(temp_tabs, editor%tabs)
528
529 ! Adjust active tab index
530 if (editor%active_tab_index > n_tabs - 1) then
531 editor%active_tab_index = n_tabs - 1
532 else if (editor%active_tab_index >= tab_index) then
533 editor%active_tab_index = max(1, editor%active_tab_index - 1)
534 end if
535
536 ! Switch to the new active tab
537 if (editor%active_tab_index > 0) then
538 call switch_to_tab(editor, editor%active_tab_index)
539 end if
540 end subroutine close_tab
541
542 ! Split the active pane vertically
543 subroutine split_pane_vertical(editor)
544 type(editor_state_t), intent(inout) :: editor
545 type(pane_t), allocatable :: temp_panes(:)
546 integer :: tab_idx, pane_idx, n_panes, new_idx, i
547 real :: mid_x
548
549 ! Get active tab
550 tab_idx = editor%active_tab_index
551 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
552 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
553
554 ! Get active pane
555 pane_idx = editor%tabs(tab_idx)%active_pane_index
556 n_panes = size(editor%tabs(tab_idx)%panes)
557 if (pane_idx < 1 .or. pane_idx > n_panes) return
558
559 ! Check pane limit (maximum 6 panes per tab)
560 if (n_panes >= 6) return
561
562 ! Ensure active pane has cursors from editor state
563 if (.not. allocated(editor%tabs(tab_idx)%panes(pane_idx)%cursors)) then
564 allocate(editor%tabs(tab_idx)%panes(pane_idx)%cursors(size(editor%cursors)))
565 editor%tabs(tab_idx)%panes(pane_idx)%cursors = editor%cursors
566 editor%tabs(tab_idx)%panes(pane_idx)%active_cursor = editor%active_cursor
567 end if
568
569 ! Calculate split point
570 associate(active_pane => editor%tabs(tab_idx)%panes(pane_idx))
571 mid_x = (active_pane%x_start + active_pane%x_end) / 2.0
572
573 ! Check minimum size (20 columns minimum)
574 ! Assuming screen is ~80 cols, 20 cols = 0.25 of width
575 if ((mid_x - active_pane%x_start) < 0.25 .or. &
576 (active_pane%x_end - mid_x) < 0.25) then
577 ! Too small to split
578 return
579 end if
580
581 ! Resize array
582 allocate(temp_panes(n_panes + 1))
583 temp_panes(1:n_panes) = editor%tabs(tab_idx)%panes(1:n_panes)
584
585 ! Setup new pane (right half)
586 new_idx = n_panes + 1
587 temp_panes(new_idx)%x_start = mid_x
588 temp_panes(new_idx)%x_end = active_pane%x_end
589 temp_panes(new_idx)%y_start = active_pane%y_start
590 temp_panes(new_idx)%y_end = active_pane%y_end
591
592 ! Copy viewport and cursor state
593 temp_panes(new_idx)%viewport_line = active_pane%viewport_line
594 temp_panes(new_idx)%viewport_column = active_pane%viewport_column
595 if (allocated(active_pane%cursors)) then
596 allocate(temp_panes(new_idx)%cursors(size(active_pane%cursors)))
597 ! Deep copy each cursor to ensure all fields are copied
598 do i = 1, size(active_pane%cursors)
599 temp_panes(new_idx)%cursors(i)%line = active_pane%cursors(i)%line
600 temp_panes(new_idx)%cursors(i)%column = active_pane%cursors(i)%column
601 temp_panes(new_idx)%cursors(i)%desired_column = active_pane%cursors(i)%desired_column
602 temp_panes(new_idx)%cursors(i)%has_selection = active_pane%cursors(i)%has_selection
603 temp_panes(new_idx)%cursors(i)%selection_start_line = active_pane%cursors(i)%selection_start_line
604 temp_panes(new_idx)%cursors(i)%selection_start_col = active_pane%cursors(i)%selection_start_col
605 end do
606 temp_panes(new_idx)%active_cursor = active_pane%active_cursor
607 else
608 ! Initialize cursor if not already allocated
609 allocate(temp_panes(new_idx)%cursors(1))
610 temp_panes(new_idx)%cursors(1)%line = 1
611 temp_panes(new_idx)%cursors(1)%column = 1
612 temp_panes(new_idx)%cursors(1)%desired_column = 1
613 temp_panes(new_idx)%cursors(1)%has_selection = .false.
614 temp_panes(new_idx)%active_cursor = 1
615 end if
616
617 ! Initialize and copy buffer from active pane
618 call init_buffer(temp_panes(new_idx)%buffer)
619 ! Copy from active pane's buffer - make sure it's initialized
620 if (active_pane%buffer%size > 0) then
621 call copy_buffer(temp_panes(new_idx)%buffer, active_pane%buffer)
622 else
623 ! Active pane buffer not initialized, copy from tab buffer
624 call copy_buffer(temp_panes(new_idx)%buffer, editor%tabs(tab_idx)%buffer)
625 end if
626 if (allocated(active_pane%filename)) then
627 temp_panes(new_idx)%filename = active_pane%filename
628 end if
629
630 temp_panes(new_idx)%is_active = .false.
631
632 ! Update active pane (left half)
633 temp_panes(pane_idx)%x_end = mid_x
634
635 ! Replace panes array
636 call move_alloc(temp_panes, editor%tabs(tab_idx)%panes)
637
638 ! Clear all is_active flags first
639 do i = 1, size(editor%tabs(tab_idx)%panes)
640 editor%tabs(tab_idx)%panes(i)%is_active = .false.
641 end do
642
643 ! Set new pane as active
644 editor%tabs(tab_idx)%panes(new_idx)%is_active = .true.
645 editor%tabs(tab_idx)%active_pane_index = new_idx
646
647 ! Sync new pane to editor state
648 call sync_pane_to_editor(editor, tab_idx, new_idx)
649 end associate
650 end subroutine split_pane_vertical
651
652 ! Split the active pane horizontally
653 subroutine split_pane_horizontal(editor)
654 type(editor_state_t), intent(inout) :: editor
655 type(pane_t), allocatable :: temp_panes(:)
656 integer :: tab_idx, pane_idx, n_panes, new_idx, i
657 real :: mid_y
658
659 ! Get active tab
660 tab_idx = editor%active_tab_index
661 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
662 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
663
664 ! Get active pane
665 pane_idx = editor%tabs(tab_idx)%active_pane_index
666 n_panes = size(editor%tabs(tab_idx)%panes)
667 if (pane_idx < 1 .or. pane_idx > n_panes) return
668
669 ! Check pane limit (maximum 6 panes per tab)
670 if (n_panes >= 6) return
671
672 ! Ensure active pane has cursors from editor state
673 if (.not. allocated(editor%tabs(tab_idx)%panes(pane_idx)%cursors)) then
674 allocate(editor%tabs(tab_idx)%panes(pane_idx)%cursors(size(editor%cursors)))
675 editor%tabs(tab_idx)%panes(pane_idx)%cursors = editor%cursors
676 editor%tabs(tab_idx)%panes(pane_idx)%active_cursor = editor%active_cursor
677 end if
678
679 ! Calculate split point
680 associate(active_pane => editor%tabs(tab_idx)%panes(pane_idx))
681 mid_y = (active_pane%y_start + active_pane%y_end) / 2.0
682
683 ! Check minimum size (5 rows minimum)
684 ! Assuming screen is ~24 rows, 5 rows = 0.21 of height
685 if ((mid_y - active_pane%y_start) < 0.21 .or. &
686 (active_pane%y_end - mid_y) < 0.21) then
687 ! Too small to split
688 return
689 end if
690
691 ! Resize array
692 allocate(temp_panes(n_panes + 1))
693 temp_panes(1:n_panes) = editor%tabs(tab_idx)%panes(1:n_panes)
694
695 ! Setup new pane (bottom half)
696 new_idx = n_panes + 1
697 temp_panes(new_idx)%x_start = active_pane%x_start
698 temp_panes(new_idx)%x_end = active_pane%x_end
699 temp_panes(new_idx)%y_start = mid_y
700 temp_panes(new_idx)%y_end = active_pane%y_end
701
702 ! Copy viewport and cursor state
703 temp_panes(new_idx)%viewport_line = active_pane%viewport_line
704 temp_panes(new_idx)%viewport_column = active_pane%viewport_column
705 if (allocated(active_pane%cursors)) then
706 allocate(temp_panes(new_idx)%cursors(size(active_pane%cursors)))
707 ! Deep copy each cursor to ensure all fields are copied
708 do i = 1, size(active_pane%cursors)
709 temp_panes(new_idx)%cursors(i)%line = active_pane%cursors(i)%line
710 temp_panes(new_idx)%cursors(i)%column = active_pane%cursors(i)%column
711 temp_panes(new_idx)%cursors(i)%desired_column = active_pane%cursors(i)%desired_column
712 temp_panes(new_idx)%cursors(i)%has_selection = active_pane%cursors(i)%has_selection
713 temp_panes(new_idx)%cursors(i)%selection_start_line = active_pane%cursors(i)%selection_start_line
714 temp_panes(new_idx)%cursors(i)%selection_start_col = active_pane%cursors(i)%selection_start_col
715 end do
716 temp_panes(new_idx)%active_cursor = active_pane%active_cursor
717 else
718 ! Initialize cursor if not already allocated
719 allocate(temp_panes(new_idx)%cursors(1))
720 temp_panes(new_idx)%cursors(1)%line = 1
721 temp_panes(new_idx)%cursors(1)%column = 1
722 temp_panes(new_idx)%cursors(1)%desired_column = 1
723 temp_panes(new_idx)%cursors(1)%has_selection = .false.
724 temp_panes(new_idx)%active_cursor = 1
725 end if
726
727 ! Initialize and copy buffer from active pane
728 call init_buffer(temp_panes(new_idx)%buffer)
729 ! Copy from active pane's buffer - make sure it's initialized
730 if (active_pane%buffer%size > 0) then
731 call copy_buffer(temp_panes(new_idx)%buffer, active_pane%buffer)
732 else
733 ! Active pane buffer not initialized, copy from tab buffer
734 call copy_buffer(temp_panes(new_idx)%buffer, editor%tabs(tab_idx)%buffer)
735 end if
736 if (allocated(active_pane%filename)) then
737 temp_panes(new_idx)%filename = active_pane%filename
738 end if
739
740 temp_panes(new_idx)%is_active = .false.
741
742 ! Update active pane (top half)
743 temp_panes(pane_idx)%y_end = mid_y
744
745 ! Replace panes array
746 call move_alloc(temp_panes, editor%tabs(tab_idx)%panes)
747
748 ! Clear all is_active flags first
749 do i = 1, size(editor%tabs(tab_idx)%panes)
750 editor%tabs(tab_idx)%panes(i)%is_active = .false.
751 end do
752
753 ! Set new pane as active
754 editor%tabs(tab_idx)%panes(new_idx)%is_active = .true.
755 editor%tabs(tab_idx)%active_pane_index = new_idx
756
757 ! Sync new pane to editor state
758 call sync_pane_to_editor(editor, tab_idx, new_idx)
759 end associate
760 end subroutine split_pane_horizontal
761
762 ! Close the active pane
763 subroutine close_pane(editor)
764 type(editor_state_t), intent(inout) :: editor
765 type(pane_t), allocatable :: temp_panes(:)
766 integer :: tab_idx, pane_idx, n_panes, i, j
767
768 ! Get active tab
769 tab_idx = editor%active_tab_index
770 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
771 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
772
773 ! Get active pane
774 pane_idx = editor%tabs(tab_idx)%active_pane_index
775 n_panes = size(editor%tabs(tab_idx)%panes)
776 if (pane_idx < 1 .or. pane_idx > n_panes) return
777
778 ! If only one pane, check if it's the last tab
779 if (n_panes == 1) then
780 ! If this is the last tab, create an UNTITLED.txt tab instead of closing
781 if (size(editor%tabs) == 1) then
782 ! Create a new untitled tab
783 call create_untitled_tab(editor)
784 return
785 else
786 ! Multiple tabs exist, close this tab normally
787 call close_tab(editor, tab_idx)
788 return
789 end if
790 end if
791
792 ! Remove the pane
793 allocate(temp_panes(n_panes - 1))
794 j = 1
795 do i = 1, n_panes
796 if (i /= pane_idx) then
797 temp_panes(j) = editor%tabs(tab_idx)%panes(i)
798 j = j + 1
799 else
800 ! Clean up the pane being removed
801 if (allocated(editor%tabs(tab_idx)%panes(i)%cursors)) then
802 deallocate(editor%tabs(tab_idx)%panes(i)%cursors)
803 end if
804 end if
805 end do
806
807 ! Replace panes array
808 call move_alloc(temp_panes, editor%tabs(tab_idx)%panes)
809
810 ! Recalculate layout for remaining panes
811 call recalculate_pane_layout(editor%tabs(tab_idx)%panes)
812
813 ! Determine new active pane index
814 if (pane_idx > size(editor%tabs(tab_idx)%panes)) then
815 ! Was the last pane, activate the new last pane
816 editor%tabs(tab_idx)%active_pane_index = size(editor%tabs(tab_idx)%panes)
817 else if (pane_idx > 1) then
818 ! Activate the previous pane
819 editor%tabs(tab_idx)%active_pane_index = pane_idx - 1
820 else
821 ! Was the first pane, activate what is now the first pane
822 editor%tabs(tab_idx)%active_pane_index = 1
823 end if
824
825 ! Clear all is_active flags first
826 do i = 1, size(editor%tabs(tab_idx)%panes)
827 editor%tabs(tab_idx)%panes(i)%is_active = .false.
828 end do
829
830 ! Set new active pane
831 editor%tabs(tab_idx)%panes(editor%tabs(tab_idx)%active_pane_index)%is_active = .true.
832
833 ! Sync to editor state
834 call sync_pane_to_editor(editor, tab_idx, editor%tabs(tab_idx)%active_pane_index)
835 end subroutine close_pane
836
837 ! Get the active pane indices
838 subroutine get_active_pane_indices(editor, tab_idx, pane_idx)
839 type(editor_state_t), intent(in) :: editor
840 integer, intent(out) :: tab_idx, pane_idx
841
842 tab_idx = editor%active_tab_index
843 pane_idx = -1
844
845 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) then
846 tab_idx = -1
847 return
848 end if
849
850 if (.not. allocated(editor%tabs(tab_idx)%panes)) then
851 tab_idx = -1
852 return
853 end if
854
855 pane_idx = editor%tabs(tab_idx)%active_pane_index
856 if (pane_idx < 1 .or. pane_idx > size(editor%tabs(tab_idx)%panes)) then
857 pane_idx = -1
858 end if
859 end subroutine get_active_pane_indices
860
861 ! Helper to sync pane state to editor
862 subroutine sync_pane_to_editor(editor, tab_idx, pane_idx)
863 use text_buffer_module, only: buffer_get_line, buffer_get_line_count
864 type(editor_state_t), intent(inout) :: editor
865 integer, intent(in) :: tab_idx, pane_idx
866 integer :: i, line_count
867 character(len=:), allocatable :: line
868
869 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
870 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
871 if (pane_idx < 1 .or. pane_idx > size(editor%tabs(tab_idx)%panes)) return
872
873 associate(pane => editor%tabs(tab_idx)%panes(pane_idx))
874 ! Copy pane state to editor
875 if (allocated(editor%cursors)) deallocate(editor%cursors)
876 if (allocated(pane%cursors) .and. size(pane%cursors) > 0) then
877 allocate(editor%cursors(size(pane%cursors)))
878 editor%cursors = pane%cursors
879 editor%active_cursor = min(pane%active_cursor, size(pane%cursors))
880 if (editor%active_cursor < 1) editor%active_cursor = 1
881 else
882 ! Initialize with single cursor if pane has no cursors
883 allocate(editor%cursors(1))
884 editor%cursors(1)%line = 1
885 editor%cursors(1)%column = 1
886 editor%cursors(1)%desired_column = 1
887 editor%cursors(1)%has_selection = .false.
888 editor%cursors(1)%selection_start_line = 1
889 editor%cursors(1)%selection_start_col = 1
890 editor%active_cursor = 1
891 end if
892
893 ! Validate cursor positions are within buffer bounds
894 line_count = buffer_get_line_count(editor%tabs(tab_idx)%buffer)
895 if (line_count > 0 .and. allocated(editor%cursors)) then
896 do i = 1, size(editor%cursors)
897 ! Clamp line to valid range
898 if (editor%cursors(i)%line > line_count) then
899 editor%cursors(i)%line = line_count
900 end if
901 if (editor%cursors(i)%line < 1) then
902 editor%cursors(i)%line = 1
903 end if
904
905 ! Clamp column to valid range for the line
906 line = buffer_get_line(editor%tabs(tab_idx)%buffer, editor%cursors(i)%line)
907 if (editor%cursors(i)%column > len(line) + 1) then
908 editor%cursors(i)%column = len(line) + 1
909 end if
910 if (editor%cursors(i)%column < 1) then
911 editor%cursors(i)%column = 1
912 end if
913 editor%cursors(i)%desired_column = editor%cursors(i)%column
914
915 if (allocated(line)) deallocate(line)
916 end do
917 end if
918 editor%viewport_line = pane%viewport_line
919 editor%viewport_column = pane%viewport_column
920
921 ! Sync filename from pane to editor
922 if (allocated(editor%filename)) deallocate(editor%filename)
923 if (allocated(pane%filename)) then
924 allocate(character(len=len(pane%filename)) :: editor%filename)
925 editor%filename = pane%filename
926 end if
927 end associate
928 end subroutine sync_pane_to_editor
929
930 ! Helper to sync editor state back to the active pane
931 subroutine sync_editor_to_pane(editor)
932 type(editor_state_t), intent(inout) :: editor
933 integer :: tab_idx, pane_idx
934
935 tab_idx = editor%active_tab_index
936 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
937 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
938
939 pane_idx = editor%tabs(tab_idx)%active_pane_index
940 if (pane_idx < 1 .or. pane_idx > size(editor%tabs(tab_idx)%panes)) return
941
942 associate(pane => editor%tabs(tab_idx)%panes(pane_idx))
943 ! Copy editor state back to pane
944 if (allocated(pane%cursors)) deallocate(pane%cursors)
945 if (allocated(editor%cursors) .and. size(editor%cursors) > 0) then
946 allocate(pane%cursors(size(editor%cursors)))
947 pane%cursors = editor%cursors
948 pane%active_cursor = min(editor%active_cursor, size(editor%cursors))
949 if (pane%active_cursor < 1) pane%active_cursor = 1
950 else
951 ! Should not happen, but ensure we have at least one cursor
952 allocate(pane%cursors(1))
953 pane%cursors(1)%line = 1
954 pane%cursors(1)%column = 1
955 pane%cursors(1)%desired_column = 1
956 pane%cursors(1)%has_selection = .false.
957 pane%cursors(1)%selection_start_line = 1
958 pane%cursors(1)%selection_start_col = 1
959 pane%active_cursor = 1
960 end if
961 pane%viewport_line = editor%viewport_line
962 pane%viewport_column = editor%viewport_column
963 end associate
964 end subroutine sync_editor_to_pane
965
966 ! Navigate to pane on the left
967 subroutine navigate_to_pane_left(editor)
968 type(editor_state_t), intent(inout) :: editor
969 integer :: tab_idx, current_idx, i
970 real :: current_x, best_x
971 integer :: best_idx
972
973 tab_idx = editor%active_tab_index
974 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
975 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
976
977 current_idx = editor%tabs(tab_idx)%active_pane_index
978 if (current_idx < 1 .or. current_idx > size(editor%tabs(tab_idx)%panes)) return
979
980 current_x = (editor%tabs(tab_idx)%panes(current_idx)%x_start + &
981 editor%tabs(tab_idx)%panes(current_idx)%x_end) / 2.0
982
983 best_idx = -1
984 best_x = -1.0
985
986 ! Find the nearest pane to the left
987 do i = 1, size(editor%tabs(tab_idx)%panes)
988 if (i /= current_idx) then
989 ! Check if pane is to the left
990 if (editor%tabs(tab_idx)%panes(i)%x_end <= current_x) then
991 if (best_idx == -1 .or. editor%tabs(tab_idx)%panes(i)%x_end > best_x) then
992 best_idx = i
993 best_x = editor%tabs(tab_idx)%panes(i)%x_end
994 end if
995 end if
996 end if
997 end do
998
999 if (best_idx > 0) then
1000 call switch_to_pane(editor, tab_idx, best_idx)
1001 end if
1002 end subroutine navigate_to_pane_left
1003
1004 ! Navigate to pane on the right
1005 subroutine navigate_to_pane_right(editor)
1006 type(editor_state_t), intent(inout) :: editor
1007 integer :: tab_idx, current_idx, i
1008 real :: current_x, best_x
1009 integer :: best_idx
1010
1011 tab_idx = editor%active_tab_index
1012 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
1013 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
1014
1015 current_idx = editor%tabs(tab_idx)%active_pane_index
1016 if (current_idx < 1 .or. current_idx > size(editor%tabs(tab_idx)%panes)) return
1017
1018 current_x = (editor%tabs(tab_idx)%panes(current_idx)%x_start + &
1019 editor%tabs(tab_idx)%panes(current_idx)%x_end) / 2.0
1020
1021 best_idx = -1
1022 best_x = 2.0 ! Start with value beyond max
1023
1024 ! Find the nearest pane to the right
1025 do i = 1, size(editor%tabs(tab_idx)%panes)
1026 if (i /= current_idx) then
1027 ! Check if pane is to the right
1028 if (editor%tabs(tab_idx)%panes(i)%x_start >= current_x) then
1029 if (best_idx == -1 .or. editor%tabs(tab_idx)%panes(i)%x_start < best_x) then
1030 best_idx = i
1031 best_x = editor%tabs(tab_idx)%panes(i)%x_start
1032 end if
1033 end if
1034 end if
1035 end do
1036
1037 if (best_idx > 0) then
1038 call switch_to_pane(editor, tab_idx, best_idx)
1039 end if
1040 end subroutine navigate_to_pane_right
1041
1042 ! Navigate to pane above
1043 subroutine navigate_to_pane_up(editor)
1044 type(editor_state_t), intent(inout) :: editor
1045 integer :: tab_idx, current_idx, i
1046 real :: current_y, best_y
1047 integer :: best_idx
1048
1049 tab_idx = editor%active_tab_index
1050 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
1051 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
1052
1053 current_idx = editor%tabs(tab_idx)%active_pane_index
1054 if (current_idx < 1 .or. current_idx > size(editor%tabs(tab_idx)%panes)) return
1055
1056 current_y = (editor%tabs(tab_idx)%panes(current_idx)%y_start + &
1057 editor%tabs(tab_idx)%panes(current_idx)%y_end) / 2.0
1058
1059 best_idx = -1
1060 best_y = -1.0
1061
1062 ! Find the nearest pane above
1063 do i = 1, size(editor%tabs(tab_idx)%panes)
1064 if (i /= current_idx) then
1065 ! Check if pane is above
1066 if (editor%tabs(tab_idx)%panes(i)%y_end <= current_y) then
1067 if (best_idx == -1 .or. editor%tabs(tab_idx)%panes(i)%y_end > best_y) then
1068 best_idx = i
1069 best_y = editor%tabs(tab_idx)%panes(i)%y_end
1070 end if
1071 end if
1072 end if
1073 end do
1074
1075 if (best_idx > 0) then
1076 call switch_to_pane(editor, tab_idx, best_idx)
1077 end if
1078 end subroutine navigate_to_pane_up
1079
1080 ! Navigate to pane below
1081 subroutine navigate_to_pane_down(editor)
1082 type(editor_state_t), intent(inout) :: editor
1083 integer :: tab_idx, current_idx, i
1084 real :: current_y, best_y
1085 integer :: best_idx
1086
1087 tab_idx = editor%active_tab_index
1088 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
1089 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
1090
1091 current_idx = editor%tabs(tab_idx)%active_pane_index
1092 if (current_idx < 1 .or. current_idx > size(editor%tabs(tab_idx)%panes)) return
1093
1094 current_y = (editor%tabs(tab_idx)%panes(current_idx)%y_start + &
1095 editor%tabs(tab_idx)%panes(current_idx)%y_end) / 2.0
1096
1097 best_idx = -1
1098 best_y = 2.0 ! Start with value beyond max
1099
1100 ! Find the nearest pane below
1101 do i = 1, size(editor%tabs(tab_idx)%panes)
1102 if (i /= current_idx) then
1103 ! Check if pane is below
1104 if (editor%tabs(tab_idx)%panes(i)%y_start >= current_y) then
1105 if (best_idx == -1 .or. editor%tabs(tab_idx)%panes(i)%y_start < best_y) then
1106 best_idx = i
1107 best_y = editor%tabs(tab_idx)%panes(i)%y_start
1108 end if
1109 end if
1110 end if
1111 end do
1112
1113 if (best_idx > 0) then
1114 call switch_to_pane(editor, tab_idx, best_idx)
1115 end if
1116 end subroutine navigate_to_pane_down
1117
1118 ! Helper to switch to a specific pane
1119 subroutine switch_to_pane(editor, tab_idx, pane_idx)
1120 type(editor_state_t), intent(inout) :: editor
1121 integer, intent(in) :: tab_idx, pane_idx
1122 integer :: i, old_pane_idx
1123
1124 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
1125 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
1126 if (pane_idx < 1 .or. pane_idx > size(editor%tabs(tab_idx)%panes)) return
1127
1128 ! Don't do anything if we're already in this pane
1129 if (pane_idx == editor%tabs(tab_idx)%active_pane_index) return
1130
1131 ! Save current editor state to the old pane before switching
1132 old_pane_idx = editor%tabs(tab_idx)%active_pane_index
1133 if (old_pane_idx > 0 .and. old_pane_idx <= size(editor%tabs(tab_idx)%panes)) then
1134 call sync_editor_to_pane(editor)
1135 end if
1136
1137 ! Clear all is_active flags
1138 do i = 1, size(editor%tabs(tab_idx)%panes)
1139 editor%tabs(tab_idx)%panes(i)%is_active = .false.
1140 end do
1141
1142 ! Set new active pane
1143 editor%tabs(tab_idx)%panes(pane_idx)%is_active = .true.
1144 editor%tabs(tab_idx)%active_pane_index = pane_idx
1145
1146 ! Load the new pane's state to editor
1147 call sync_pane_to_editor(editor, tab_idx, pane_idx)
1148 end subroutine switch_to_pane
1149
1150 ! Helper to switch to a specific pane with buffer synchronization
1151 subroutine switch_to_pane_with_buffer(editor, tab_idx, pane_idx, buffer)
1152 type(editor_state_t), intent(inout) :: editor
1153 integer, intent(in) :: tab_idx, pane_idx
1154 type(buffer_t), intent(inout) :: buffer
1155 integer :: i, old_pane_idx
1156
1157 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
1158 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
1159 if (pane_idx < 1 .or. pane_idx > size(editor%tabs(tab_idx)%panes)) return
1160
1161 ! Don't do anything if we're already in this pane
1162 if (pane_idx == editor%tabs(tab_idx)%active_pane_index) return
1163
1164 ! Save current buffer and editor state to the old pane before switching
1165 old_pane_idx = editor%tabs(tab_idx)%active_pane_index
1166 if (old_pane_idx > 0 .and. old_pane_idx <= size(editor%tabs(tab_idx)%panes)) then
1167 ! Save buffer to old pane
1168 call copy_buffer(editor%tabs(tab_idx)%panes(old_pane_idx)%buffer, buffer)
1169
1170 ! Sync buffer to all other instances of this file
1171 if (allocated(editor%tabs(tab_idx)%panes(old_pane_idx)%filename)) then
1172 call sync_buffer_to_all_instances(editor, editor%tabs(tab_idx)%panes(old_pane_idx)%filename, buffer)
1173 end if
1174
1175 ! Save cursor/viewport state
1176 call sync_editor_to_pane(editor)
1177 end if
1178
1179 ! Clear all is_active flags
1180 do i = 1, size(editor%tabs(tab_idx)%panes)
1181 editor%tabs(tab_idx)%panes(i)%is_active = .false.
1182 end do
1183
1184 ! Set new active pane
1185 editor%tabs(tab_idx)%panes(pane_idx)%is_active = .true.
1186 editor%tabs(tab_idx)%active_pane_index = pane_idx
1187
1188 ! Load the new pane's buffer
1189 call copy_buffer(buffer, editor%tabs(tab_idx)%panes(pane_idx)%buffer)
1190
1191 ! Load the new pane's cursor/viewport state to editor
1192 call sync_pane_to_editor(editor, tab_idx, pane_idx)
1193
1194 ! Update editor filename if pane has different file
1195 if (allocated(editor%tabs(tab_idx)%panes(pane_idx)%filename)) then
1196 if (allocated(editor%filename)) deallocate(editor%filename)
1197 allocate(character(len=len(editor%tabs(tab_idx)%panes(pane_idx)%filename)) :: editor%filename)
1198 editor%filename = editor%tabs(tab_idx)%panes(pane_idx)%filename
1199 end if
1200 end subroutine switch_to_pane_with_buffer
1201
1202 ! Sync buffer to all panes/tabs that have the same file open
1203 ! This enables live updates when the same file is open in multiple locations
1204 subroutine sync_buffer_to_all_instances(editor, filename, buffer)
1205 type(editor_state_t), intent(inout) :: editor
1206 character(len=*), intent(in) :: filename
1207 type(buffer_t), intent(in) :: buffer
1208 integer :: tab_idx, pane_idx
1209 character(len=:), allocatable :: normalized_filename
1210
1211 ! Normalize filename for comparison (trim whitespace)
1212 normalized_filename = trim(filename)
1213 if (len_trim(normalized_filename) == 0) return
1214
1215 ! Loop through all tabs
1216 do tab_idx = 1, size(editor%tabs)
1217 ! Update tab's buffer if it matches
1218 if (allocated(editor%tabs(tab_idx)%filename)) then
1219 if (trim(editor%tabs(tab_idx)%filename) == normalized_filename) then
1220 call copy_buffer(editor%tabs(tab_idx)%buffer, buffer)
1221 editor%tabs(tab_idx)%modified = .true.
1222 end if
1223 end if
1224
1225 ! Loop through all panes in this tab
1226 if (allocated(editor%tabs(tab_idx)%panes)) then
1227 do pane_idx = 1, size(editor%tabs(tab_idx)%panes)
1228 ! Check if this pane has the same file open
1229 if (allocated(editor%tabs(tab_idx)%panes(pane_idx)%filename)) then
1230 if (trim(editor%tabs(tab_idx)%panes(pane_idx)%filename) == normalized_filename) then
1231 ! Skip the currently active pane (already has the latest buffer)
1232 if (tab_idx == editor%active_tab_index .and. &
1233 pane_idx == editor%tabs(tab_idx)%active_pane_index) then
1234 cycle
1235 end if
1236
1237 ! Copy buffer to this pane (preserves cursor/viewport)
1238 call copy_buffer(editor%tabs(tab_idx)%panes(pane_idx)%buffer, buffer)
1239 end if
1240 end if
1241 end do
1242 end if
1243 end do
1244 end subroutine sync_buffer_to_all_instances
1245
1246 ! Create an untitled tab (replaces current tab)
1247 subroutine create_untitled_tab(editor)
1248 use text_buffer_module, only: init_buffer, cleanup_buffer
1249 type(editor_state_t), intent(inout) :: editor
1250 integer :: tab_idx
1251
1252 tab_idx = editor%active_tab_index
1253 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
1254
1255 ! Clean up the old tab's buffer
1256 call cleanup_buffer(editor%tabs(tab_idx)%buffer)
1257
1258 ! Reinitialize as untitled
1259 if (allocated(editor%tabs(tab_idx)%filename)) deallocate(editor%tabs(tab_idx)%filename)
1260 allocate(character(len=12) :: editor%tabs(tab_idx)%filename)
1261 editor%tabs(tab_idx)%filename = "UNTITLED.txt"
1262 editor%tabs(tab_idx)%modified = .false.
1263
1264 ! Initialize empty buffer
1265 call init_buffer(editor%tabs(tab_idx)%buffer)
1266
1267 ! Reset panes
1268 if (allocated(editor%tabs(tab_idx)%panes)) then
1269 deallocate(editor%tabs(tab_idx)%panes)
1270 end if
1271 allocate(editor%tabs(tab_idx)%panes(1))
1272 editor%tabs(tab_idx)%panes(1)%x_start = 0.0
1273 editor%tabs(tab_idx)%panes(1)%y_start = 0.0
1274 editor%tabs(tab_idx)%panes(1)%x_end = 1.0
1275 editor%tabs(tab_idx)%panes(1)%y_end = 1.0
1276 editor%tabs(tab_idx)%panes(1)%is_active = .true.
1277 editor%tabs(tab_idx)%panes(1)%viewport_line = 1
1278 editor%tabs(tab_idx)%panes(1)%viewport_column = 1
1279
1280 ! Initialize cursor
1281 allocate(editor%tabs(tab_idx)%panes(1)%cursors(1))
1282 editor%tabs(tab_idx)%panes(1)%cursors(1)%line = 1
1283 editor%tabs(tab_idx)%panes(1)%cursors(1)%column = 1
1284 editor%tabs(tab_idx)%panes(1)%cursors(1)%desired_column = 1
1285 editor%tabs(tab_idx)%panes(1)%cursors(1)%has_selection = .false.
1286 editor%tabs(tab_idx)%panes(1)%active_cursor = 1
1287 editor%tabs(tab_idx)%active_pane_index = 1
1288
1289 ! Initialize screen coordinates for the pane
1290 editor%tabs(tab_idx)%panes(1)%screen_col = 1
1291 editor%tabs(tab_idx)%panes(1)%screen_row = 2 ! After tab bar
1292 editor%tabs(tab_idx)%panes(1)%screen_width = 80 ! Default width
1293 editor%tabs(tab_idx)%panes(1)%screen_height = 22 ! Default height
1294
1295 ! Sync to editor
1296 call sync_pane_to_editor(editor, tab_idx, 1)
1297 end subroutine create_untitled_tab
1298
1299 ! Recalculate pane layout after closing a pane
1300 subroutine recalculate_pane_layout(panes)
1301 type(pane_t), intent(inout) :: panes(:)
1302 integer :: n_panes, i
1303 real :: x_min, x_max, y_min, y_max
1304 logical :: is_vertical_split, is_horizontal_split
1305 real :: pane_width, pane_height
1306
1307 n_panes = size(panes)
1308
1309 ! If only one pane, make it full screen
1310 if (n_panes == 1) then
1311 panes(1)%x_start = 0.0
1312 panes(1)%x_end = 1.0
1313 panes(1)%y_start = 0.0
1314 panes(1)%y_end = 1.0
1315 return
1316 end if
1317
1318 ! Determine the layout type by checking if panes share x or y coordinates
1319 is_vertical_split = .false.
1320 is_horizontal_split = .false.
1321
1322 ! Check if all panes share same y coordinates (vertical split - side by side)
1323 y_min = panes(1)%y_start
1324 y_max = panes(1)%y_end
1325 is_vertical_split = .true.
1326 do i = 2, n_panes
1327 if (abs(panes(i)%y_start - y_min) > 0.01 .or. abs(panes(i)%y_end - y_max) > 0.01) then
1328 is_vertical_split = .false.
1329 exit
1330 end if
1331 end do
1332
1333 ! Check if all panes share same x coordinates (horizontal split - top/bottom)
1334 if (.not. is_vertical_split) then
1335 x_min = panes(1)%x_start
1336 x_max = panes(1)%x_end
1337 is_horizontal_split = .true.
1338 do i = 2, n_panes
1339 if (abs(panes(i)%x_start - x_min) > 0.01 .or. abs(panes(i)%x_end - x_max) > 0.01) then
1340 is_horizontal_split = .false.
1341 exit
1342 end if
1343 end do
1344 end if
1345
1346 ! Recalculate based on layout type
1347 if (is_vertical_split) then
1348 ! Panes are side by side - redistribute horizontally
1349 pane_width = 1.0 / real(n_panes)
1350 do i = 1, n_panes
1351 panes(i)%x_start = real(i - 1) * pane_width
1352 panes(i)%x_end = real(i) * pane_width
1353 panes(i)%y_start = 0.0
1354 panes(i)%y_end = 1.0
1355 end do
1356 else if (is_horizontal_split) then
1357 ! Panes are top/bottom - redistribute vertically
1358 pane_height = 1.0 / real(n_panes)
1359 do i = 1, n_panes
1360 panes(i)%x_start = 0.0
1361 panes(i)%x_end = 1.0
1362 panes(i)%y_start = real(i - 1) * pane_height
1363 panes(i)%y_end = real(i) * pane_height
1364 end do
1365 else
1366 ! Mixed layout - try to expand panes to fill gaps
1367 ! For now, just ensure at least the first pane is properly sized
1368 ! This is a simplified approach - a more sophisticated algorithm
1369 ! would detect and fill gaps properly
1370
1371 ! Find the overall bounds
1372 x_min = 1.0
1373 x_max = 0.0
1374 y_min = 1.0
1375 y_max = 0.0
1376 do i = 1, n_panes
1377 x_min = min(x_min, panes(i)%x_start)
1378 x_max = max(x_max, panes(i)%x_end)
1379 y_min = min(y_min, panes(i)%y_start)
1380 y_max = max(y_max, panes(i)%y_end)
1381 end do
1382
1383 ! If we have exactly 2 panes, try to expand them smartly
1384 if (n_panes == 2) then
1385 ! Check if they're adjacent horizontally
1386 if (abs(panes(1)%x_end - panes(2)%x_start) < 0.01 .or. &
1387 abs(panes(2)%x_end - panes(1)%x_start) < 0.01) then
1388 ! Expand vertically
1389 panes(1)%y_start = 0.0
1390 panes(1)%y_end = 1.0
1391 panes(2)%y_start = 0.0
1392 panes(2)%y_end = 1.0
1393 ! Check if they're adjacent vertically
1394 else if (abs(panes(1)%y_end - panes(2)%y_start) < 0.01 .or. &
1395 abs(panes(2)%y_end - panes(1)%y_start) < 0.01) then
1396 ! Expand horizontally
1397 panes(1)%x_start = 0.0
1398 panes(1)%x_end = 1.0
1399 panes(2)%x_start = 0.0
1400 panes(2)%x_end = 1.0
1401 end if
1402 end if
1403 end if
1404 end subroutine recalculate_pane_layout
1405
1406 end module editor_state_module