Fortran · 105635 bytes Raw Blame History
1 module renderer_module
2 use iso_fortran_env, only: int32, int64, output_unit
3 use terminal_io_module
4 use text_buffer_module
5 use utf8_module
6 use editor_state_module, only: editor_state_t, cursor_t
7 use bracket_matching_module
8 use file_tree_module
9 use file_tree_renderer_module
10 use syntax_highlighter_module
11 use diagnostics_module, only: diagnostic_t, get_diagnostics_for_line, &
12 get_diagnostic_at_cursor, &
13 SEVERITY_ERROR, SEVERITY_WARNING, SEVERITY_INFO, SEVERITY_HINT
14 use diagnostics_panel_module, only: render_diagnostics_panel
15 use references_panel_module, only: render_references_panel
16 use code_actions_panel_module, only: render_code_actions_panel
17 use symbols_panel_module, only: render_symbols_panel
18 use unified_search_module, only: get_matches_on_line, search_mode_active
19 use lsp_server_installer_panel_module, only: render_lsp_server_installer_panel, &
20 is_lsp_server_installer_panel_visible
21 implicit none
22 private
23
24 public :: render_screen, update_viewport, init_renderer, cleanup_renderer
25 public :: render_status_bar, render_cursor
26 public :: show_line_numbers, LINE_NUMBER_WIDTH
27 public :: render_screen_with_tree, render_screen_with_lsp_panel
28 public :: tree_state
29 public :: update_syntax_highlighter
30 public :: render_cursor_only ! Fast path for cursor-only updates
31 public :: fuss_search_buffer, fuss_search_len, fuss_search_last_time
32 public :: fuss_fuzzy_jump, fuss_reset_search, get_time_ms
33 public :: fuss_git_prefix_active
34
35 ! Configuration
36 logical :: show_line_numbers = .true.
37 logical :: highlight_current_line = .true.
38 integer, parameter :: LINE_NUMBER_WIDTH = 5 ! Width for line number display
39
40 ! Bracket matching state
41 integer :: bracket_line = 0
42 integer :: bracket_col = 0
43 integer :: matching_bracket_line = 0
44 integer :: matching_bracket_col = 0
45
46 ! Screen buffer for double buffering
47 type :: screen_buffer_t
48 character(len=:), allocatable :: lines(:)
49 integer :: rows
50 integer :: cols
51 logical :: needs_full_redraw
52 end type screen_buffer_t
53
54 type(screen_buffer_t) :: screen_buffer
55
56 ! File tree state (for fuss mode)
57 type(tree_state_t) :: tree_state
58
59 ! Fuzzy search state for fuss mode (500ms timeout)
60 character(len=64) :: fuss_search_buffer = ''
61 integer :: fuss_search_len = 0
62 integer(int64) :: fuss_search_last_time = 0
63
64 ! Git prefix mode for fuss (Ctrl+g followed by command key)
65 logical :: fuss_git_prefix_active = .false.
66
67 ! Syntax highlighting state
68 type(syntax_highlighter_t) :: syntax_highlighter
69 character(len=512) :: last_highlighted_filename = ""
70
71 contains
72
73 subroutine init_renderer(rows, cols, filename)
74 integer, intent(in) :: rows, cols
75 character(len=*), intent(in), optional :: filename
76 integer :: i
77
78 screen_buffer%rows = rows
79 screen_buffer%cols = cols
80 screen_buffer%needs_full_redraw = .true.
81
82 allocate(character(len=cols) :: screen_buffer%lines(rows))
83 do i = 1, rows
84 screen_buffer%lines(i) = repeat(' ', cols)
85 end do
86
87 ! Initialize syntax highlighter if filename provided
88 if (present(filename)) then
89 call init_highlighter(syntax_highlighter, filename)
90 last_highlighted_filename = trim(filename)
91 else
92 call init_highlighter(syntax_highlighter)
93 last_highlighted_filename = ""
94 end if
95 end subroutine init_renderer
96
97 subroutine cleanup_renderer()
98 if (allocated(screen_buffer%lines)) deallocate(screen_buffer%lines)
99 call cleanup_highlighter(syntax_highlighter)
100 end subroutine cleanup_renderer
101
102 ! Update syntax highlighter for a new filename/language
103 subroutine update_syntax_highlighter(filename)
104 character(len=*), intent(in) :: filename
105
106 ! Only update if filename has changed
107 if (trim(filename) == trim(last_highlighted_filename)) return
108
109 ! Cleanup old language definition and re-initialize
110 call cleanup_highlighter(syntax_highlighter)
111 call init_highlighter(syntax_highlighter, filename)
112 last_highlighted_filename = trim(filename)
113 end subroutine update_syntax_highlighter
114
115 subroutine render_screen(buffer, editor, match_mode_active, match_case_sens)
116 type(buffer_t), intent(in) :: buffer
117 type(editor_state_t), intent(inout) :: editor
118 logical, intent(in), optional :: match_mode_active
119 logical, intent(in), optional :: match_case_sens
120 integer :: screen_row, buffer_line, line_count
121 character(len=:), allocatable :: line_content
122 character(len=1) :: cursor_char
123 integer :: content_width
124 integer :: start_row, row_offset_val
125 character(len=16) :: line_num_str
126 logical :: found_match
127 type(cursor_t) :: cursor
128
129 ! Auto-update syntax highlighter if filename changed
130 if (allocated(editor%filename)) then
131 call update_syntax_highlighter(editor%filename)
132 end if
133
134 call terminal_hide_cursor()
135
136 ! Render tab bar if there are any tabs
137 call render_tab_bar(editor)
138
139 ! Check if cursor is on a bracket and find its match
140 cursor = editor%cursors(editor%active_cursor)
141 line_content = buffer_get_line(buffer, cursor%line)
142 if (cursor%column >= 1 .and. cursor%column <= len(line_content)) then
143 cursor_char = line_content(cursor%column:cursor%column)
144 if (is_opening_bracket(cursor_char) .or. is_closing_bracket(cursor_char)) then
145 bracket_line = cursor%line
146 bracket_col = cursor%column
147 call find_matching_bracket(buffer, bracket_line, bracket_col, &
148 found_match, matching_bracket_line, matching_bracket_col)
149 if (.not. found_match) then
150 matching_bracket_line = 0
151 matching_bracket_col = 0
152 end if
153 else
154 bracket_line = 0
155 bracket_col = 0
156 matching_bracket_line = 0
157 matching_bracket_col = 0
158 end if
159 else
160 bracket_line = 0
161 bracket_col = 0
162 matching_bracket_line = 0
163 matching_bracket_col = 0
164 end if
165 if (allocated(line_content)) deallocate(line_content)
166
167 ! Get total lines in buffer
168 line_count = buffer_get_line_count(buffer)
169
170 ! Calculate content width (accounting for line numbers)
171 if (show_line_numbers) then
172 content_width = editor%screen_cols - LINE_NUMBER_WIDTH - 1 ! -1 for separator
173 else
174 content_width = editor%screen_cols
175 end if
176
177 ! Determine starting row based on whether tabs exist
178 if (size(editor%tabs) > 0) then
179 start_row = 2 ! Tab bar at row 1
180 row_offset_val = 2
181 else
182 start_row = 1 ! No tab bar
183 row_offset_val = 1
184 end if
185
186 ! Render all panes for the active tab
187 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0 .and. &
188 editor%active_tab_index <= size(editor%tabs)) then
189 if (allocated(editor%tabs(editor%active_tab_index)%panes)) then
190 call render_all_panes(editor)
191 ! Render status bar after panes
192 call render_status_bar(editor, buffer, match_mode_active, match_case_sens)
193
194 ! Render diagnostics panel if visible (for panes path)
195 if (allocated(editor%filename)) then
196 block
197 character(len=:), allocatable :: file_uri, cwd
198 character(len=1024) :: cwd_buffer
199 integer :: cwd_len
200
201 ! Get absolute path for file URI
202 if (editor%filename(1:1) == '/') then
203 ! Already absolute
204 file_uri = 'file:///' // trim(editor%filename)
205 else
206 ! Relative path - get PWD from environment
207 call get_environment_variable("PWD", cwd_buffer, cwd_len)
208
209 if (cwd_len > 0) then
210 cwd = cwd_buffer(1:cwd_len)
211 file_uri = 'file://' // trim(cwd) // '/' // trim(editor%filename)
212 else
213 file_uri = 'file:///' // trim(editor%filename)
214 end if
215 end if
216
217 call render_diagnostics_panel(editor%diagnostics_panel, editor%diagnostics, &
218 file_uri, editor%screen_rows, editor%screen_cols)
219 end block
220 end if
221
222 ! Render references panel if visible (for panes path)
223 call render_references_panel(editor%references_panel, 3)
224
225 ! Render code actions menu if visible (for panes path)
226 call render_code_actions_panel(editor%code_actions_panel, editor%screen_rows, editor%screen_cols)
227
228 ! Render symbols panel if visible (for panes path)
229 call render_symbols_panel(editor%symbols_panel, editor%screen_rows)
230
231 ! Render LSP server installer panel if visible (for panes path)
232 if (is_lsp_server_installer_panel_visible(editor%lsp_installer_panel)) then
233 call render_lsp_server_installer_panel(editor%lsp_installer_panel, &
234 editor%screen_cols)
235 end if
236
237 ! Position cursor for panes
238 call render_cursor_for_panes(editor)
239 return ! Exit after rendering panes
240 end if
241 end if
242
243 ! Fallback to simple rendering if no tabs/panes
244 ! Clear and render each visible line
245 do screen_row = start_row, editor%screen_rows - 1 ! Last row for status bar
246 buffer_line = editor%viewport_line + screen_row - row_offset_val
247
248 call terminal_move_cursor(screen_row, 1)
249
250 ! Render line number if enabled
251 if (show_line_numbers) then
252 if (buffer_line <= line_count) then
253 ! Format line number, right-aligned
254 write(line_num_str, '(i5)') buffer_line
255
256 if (buffer_line == editor%cursors(editor%active_cursor)%line) then
257 ! Highlight current line number
258 call terminal_write(char(27) // '[1;33m' // adjustl(line_num_str(1:LINE_NUMBER_WIDTH)) &
259 // char(27) // '[0m ')
260 else
261 call terminal_write(char(27) // '[90m' // adjustl(line_num_str(1:LINE_NUMBER_WIDTH)) &
262 // char(27) // '[0m ')
263 end if
264 else
265 ! Empty line number area for lines beyond file
266 call terminal_write(repeat(' ', LINE_NUMBER_WIDTH + 1))
267 end if
268 end if
269
270 if (buffer_line <= line_count) then
271 ! Render actual line content with selections
272 call render_line_with_selections(buffer, editor, buffer_line, &
273 editor%viewport_column, content_width)
274 else
275 ! Render empty line indicator
276 if (buffer_line == line_count + 1 .and. line_count == 0) then
277 ! Empty file
278 call terminal_write('~' // repeat(' ', content_width - 1))
279 else
280 ! Beyond file content
281 call terminal_write('~' // repeat(' ', content_width - 1))
282 end if
283 end if
284 ! Clear to end of line to prevent stale content when scrolling
285 call terminal_write(char(27) // '[K')
286 end do
287
288 ! Render status bar
289 call render_status_bar(editor, buffer, match_mode_active, match_case_sens)
290
291 ! Render diagnostics panel if visible
292 if (allocated(editor%filename)) then
293 block
294 character(len=:), allocatable :: file_uri
295 file_uri = 'file://' // trim(editor%filename)
296 call render_diagnostics_panel(editor%diagnostics_panel, editor%diagnostics, &
297 file_uri, editor%screen_rows, editor%screen_cols)
298 end block
299 end if
300
301 ! Render references panel if visible
302 call render_references_panel(editor%references_panel, 3)
303
304 ! Render code actions menu if visible
305 call render_code_actions_panel(editor%code_actions_panel, editor%screen_rows, editor%screen_cols)
306
307 ! Render symbols panel if visible
308 call render_symbols_panel(editor%symbols_panel, editor%screen_rows)
309
310 ! Render LSP server installer panel if visible
311 if (is_lsp_server_installer_panel_visible(editor%lsp_installer_panel)) then
312 call render_lsp_server_installer_panel(editor%lsp_installer_panel, &
313 editor%screen_cols)
314 end if
315
316 ! Position cursor for panes or regular view
317 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0 .and. &
318 editor%active_tab_index <= size(editor%tabs)) then
319 if (allocated(editor%tabs(editor%active_tab_index)%panes)) then
320 call render_cursor_for_panes(editor)
321 else
322 call render_cursor(editor, buffer)
323 end if
324 else
325 call render_cursor(editor, buffer)
326 end if
327 end subroutine render_screen
328
329 subroutine render_line_with_selections(buffer, editor, line_num, start_col, width)
330 type(buffer_t), intent(in) :: buffer
331 type(editor_state_t), intent(in) :: editor
332 integer, intent(in) :: line_num, start_col, width
333 character(len=:), allocatable :: line, utf8_ch
334 integer :: i, char_idx, byte_pos, token_idx, char_count, display_col, char_width
335 integer :: sel_start_line, sel_start_col, sel_end_line, sel_end_col
336 logical :: in_selection, is_bracket_match, is_current_line, is_search_match
337 type(token_t), allocatable :: tokens(:)
338 character(len=:), allocatable :: token_color
339 integer :: search_matches(2, 50) ! Up to 50 matches per line (start, end pairs)
340 integer :: num_search_matches, match_idx
341 integer :: line_byte_len
342
343 line = buffer_get_line(buffer, line_num)
344 line_byte_len = len(line)
345 char_count = utf8_char_count(line)
346
347 ! Get all search matches on this line (these use byte indices)
348 if (search_mode_active) then
349 call get_matches_on_line(line, line_num, search_matches, num_search_matches)
350 else
351 num_search_matches = 0
352 end if
353
354 ! Get syntax tokens for this line (tokens use byte indices)
355 if (syntax_highlighter%enabled) then
356 call tokenize_line(syntax_highlighter, line, tokens)
357 else
358 allocate(tokens(1))
359 tokens(1)%type = TOKEN_PLAIN
360 tokens(1)%start_col = 1
361 tokens(1)%end_col = max(1, line_byte_len)
362 end if
363
364 ! Check if this is the current line
365 is_current_line = (line_num == editor%cursors(editor%active_cursor)%line) .and. highlight_current_line
366
367 ! Render each UTF-8 character with selection highlighting
368 ! char_idx = 1-based character index (for selection logic)
369 ! display_col = screen column position (for width tracking)
370 ! byte_pos = byte position in string (for token lookup)
371 display_col = 0
372 char_idx = start_col
373
374 do while (char_idx <= char_count .and. display_col < width)
375 in_selection = .false.
376 is_bracket_match = .false.
377
378 ! Get the UTF-8 character at this position
379 utf8_ch = utf8_char_at(line, char_idx)
380 char_width = utf8_display_width(utf8_ch)
381
382 ! Get byte position for token lookup
383 byte_pos = utf8_char_to_byte_index(line, char_idx)
384
385 ! Check if this position is in any cursor's selection
386 ! (cursor positions are character indices, not byte indices)
387 do i = 1, size(editor%cursors)
388 if (editor%cursors(i)%has_selection) then
389 ! Determine selection bounds (handle both directions)
390 if (editor%cursors(i)%line < editor%cursors(i)%selection_start_line .or. &
391 (editor%cursors(i)%line == editor%cursors(i)%selection_start_line .and. &
392 editor%cursors(i)%column < editor%cursors(i)%selection_start_col)) then
393 ! Cursor is before selection start (selecting upward)
394 sel_start_line = editor%cursors(i)%line
395 sel_start_col = editor%cursors(i)%column
396 sel_end_line = editor%cursors(i)%selection_start_line
397 sel_end_col = editor%cursors(i)%selection_start_col
398 else
399 ! Cursor is after selection start (selecting downward)
400 sel_start_line = editor%cursors(i)%selection_start_line
401 sel_start_col = editor%cursors(i)%selection_start_col
402 sel_end_line = editor%cursors(i)%line
403 sel_end_col = editor%cursors(i)%column
404 end if
405
406 ! Check if this position is selected (using char_idx)
407 if (line_num > sel_start_line .and. line_num < sel_end_line) then
408 ! Fully selected line (between start and end)
409 in_selection = .true.
410 exit
411 else if (line_num == sel_start_line .and. line_num == sel_end_line) then
412 ! Single-line selection
413 if (char_idx >= sel_start_col .and. char_idx < sel_end_col) then
414 in_selection = .true.
415 exit
416 end if
417 else if (line_num == sel_start_line .and. line_num < sel_end_line) then
418 ! First line of multi-line selection
419 if (char_idx >= sel_start_col) then
420 in_selection = .true.
421 exit
422 end if
423 else if (line_num == sel_end_line .and. line_num > sel_start_line) then
424 ! Last line of multi-line selection
425 if (char_idx < sel_end_col) then
426 in_selection = .true.
427 exit
428 end if
429 end if
430 end if
431 end do
432
433 ! Check if this position is a bracket or its match (using char_idx)
434 if ((line_num == bracket_line .and. char_idx == bracket_col) .or. &
435 (line_num == matching_bracket_line .and. char_idx == matching_bracket_col)) then
436 is_bracket_match = .true.
437 end if
438
439 ! Check if this position is part of a search match (search uses byte indices)
440 is_search_match = .false.
441 if (byte_pos > 0) then
442 do match_idx = 1, num_search_matches
443 if (byte_pos >= search_matches(1, match_idx) .and. byte_pos <= search_matches(2, match_idx)) then
444 is_search_match = .true.
445 exit
446 end if
447 end do
448 end if
449
450 ! Find which token this column belongs to (tokens use byte indices)
451 token_color = ""
452 if (syntax_highlighter%enabled .and. byte_pos > 0) then
453 do token_idx = 1, size(tokens)
454 if (byte_pos >= tokens(token_idx)%start_col .and. byte_pos <= tokens(token_idx)%end_col) then
455 token_color = get_token_color(tokens(token_idx)%type)
456 exit
457 end if
458 end do
459 end if
460
461 ! Render character with or without highlighting
462 if (in_selection) then
463 ! Highlight selected text with reverse video (highest priority)
464 call terminal_write(char(27) // '[7m' // utf8_ch // char(27) // '[0m')
465 else if (is_bracket_match) then
466 ! Highlight matching brackets with cyan background
467 call terminal_write(char(27) // '[46m' // utf8_ch // char(27) // '[0m')
468 else if (is_search_match) then
469 ! Highlight search matches with yellow background
470 if (len(token_color) > 0) then
471 call terminal_write(token_color // char(27) // '[43m' // utf8_ch // char(27) // '[0m')
472 else
473 call terminal_write(char(27) // '[43m' // utf8_ch // char(27) // '[0m')
474 end if
475 else if (is_current_line) then
476 ! Subtle background for current line with syntax color
477 if (len(token_color) > 0) then
478 call terminal_write(token_color // char(27) // '[48;5;236m' // utf8_ch // char(27) // '[0m')
479 else
480 call terminal_write(char(27) // '[48;5;236m' // utf8_ch // char(27) // '[0m')
481 end if
482 else if (len(token_color) > 0) then
483 ! Apply syntax highlighting
484 call terminal_write(token_color // utf8_ch // char(27) // '[0m')
485 else
486 call terminal_write(utf8_ch)
487 end if
488
489 display_col = display_col + char_width
490 char_idx = char_idx + 1
491 end do
492
493 ! Fill remaining width with spaces
494 do while (display_col < width)
495 in_selection = .false.
496
497 ! Check if end of line position is in selection
498 do i = 1, size(editor%cursors)
499 if (editor%cursors(i)%has_selection) then
500 ! Determine selection bounds (handle both directions)
501 if (editor%cursors(i)%line < editor%cursors(i)%selection_start_line .or. &
502 (editor%cursors(i)%line == editor%cursors(i)%selection_start_line .and. &
503 editor%cursors(i)%column < editor%cursors(i)%selection_start_col)) then
504 sel_start_line = editor%cursors(i)%line
505 sel_start_col = editor%cursors(i)%column
506 sel_end_line = editor%cursors(i)%selection_start_line
507 sel_end_col = editor%cursors(i)%selection_start_col
508 else
509 sel_start_line = editor%cursors(i)%selection_start_line
510 sel_start_col = editor%cursors(i)%selection_start_col
511 sel_end_line = editor%cursors(i)%line
512 sel_end_col = editor%cursors(i)%column
513 end if
514
515 ! Check if this position is selected (multi-line aware)
516 ! Use char_idx which is now past end of line content
517 if (line_num > sel_start_line .and. line_num < sel_end_line) then
518 ! Fully selected line
519 in_selection = .true.
520 exit
521 else if (line_num == sel_start_line .and. line_num == sel_end_line) then
522 ! Single-line selection
523 if (char_idx >= sel_start_col .and. char_idx < sel_end_col) then
524 in_selection = .true.
525 exit
526 end if
527 else if (line_num == sel_start_line .and. line_num < sel_end_line) then
528 ! First line of multi-line selection
529 if (char_idx >= sel_start_col) then
530 in_selection = .true.
531 exit
532 end if
533 else if (line_num == sel_end_line .and. line_num > sel_start_line) then
534 ! Last line of multi-line selection
535 if (char_idx < sel_end_col) then
536 in_selection = .true.
537 exit
538 end if
539 end if
540 end if
541 end do
542
543 if (in_selection) then
544 call terminal_write(char(27) // '[7m ' // char(27) // '[0m')
545 else if (is_current_line) then
546 call terminal_write(char(27) // '[48;5;236m ' // char(27) // '[0m')
547 else
548 call terminal_write(' ')
549 end if
550 display_col = display_col + 1
551 char_idx = char_idx + 1
552 end do
553
554 if (allocated(line)) deallocate(line)
555 if (allocated(utf8_ch)) deallocate(utf8_ch)
556 end subroutine render_line_with_selections
557
558 subroutine render_status_bar(editor, buffer, match_mode_active, match_case_sens)
559 type(editor_state_t), intent(in) :: editor
560 type(buffer_t), intent(in) :: buffer
561 logical, intent(in), optional :: match_mode_active
562 logical, intent(in), optional :: match_case_sens
563 character(len=256) :: status_left, status_center, status_right, status_bar
564 integer :: padding_len, left_pad, right_pad
565 type(cursor_t) :: cursor
566 logical :: show_match_hint
567
568 cursor = editor%cursors(editor%active_cursor)
569 show_match_hint = .false.
570 if (present(match_mode_active)) show_match_hint = match_mode_active
571
572 ! Move to status bar position
573 call terminal_move_cursor(editor%screen_rows, 1)
574
575 ! Prepare status bar content
576 if (allocated(editor%filename)) then
577 write(status_left, '(a,a,a,a)') ' ctrl-b:fuss | ', trim(editor%filename), &
578 merge(' [modified]', ' ', buffer%modified), ' '
579 else
580 write(status_left, '(a,a,a)') ' ctrl-b:fuss | [No Name]', &
581 merge(' [modified]', ' ', buffer%modified), ' '
582 end if
583
584 ! Add hint in center - show diagnostic, match mode hint, or help
585 block
586 type(diagnostic_t), allocatable :: line_diagnostics(:)
587 character(len=256) :: diag_msg
588 character(len=:), allocatable :: file_uri
589
590 ! Check for diagnostics at cursor position
591 if (allocated(editor%filename)) then
592 file_uri = 'file://' // trim(editor%filename)
593 line_diagnostics = get_diagnostics_for_line(editor%diagnostics, file_uri, cursor%line)
594 end if
595
596 if (allocated(line_diagnostics) .and. size(line_diagnostics) > 0) then
597 ! Show first diagnostic message (highest severity)
598 diag_msg = line_diagnostics(1)%message
599 ! Truncate if too long
600 if (len_trim(diag_msg) > 50) then
601 status_center = trim(diag_msg(1:47)) // '...'
602 else
603 status_center = trim(diag_msg)
604 end if
605 else if (show_match_hint .and. present(match_case_sens)) then
606 if (match_case_sens) then
607 status_center = '[Cc] alt-c:toggle'
608 else
609 status_center = '[cc] alt-c:toggle'
610 end if
611 else
612 status_center = 'ctrl-/:help'
613 end if
614
615 if (allocated(line_diagnostics)) deallocate(line_diagnostics)
616 end block
617
618 if (size(editor%cursors) > 1) then
619 write(status_right, '(a,i0,a,a,i0,a,i0,a)') '[', size(editor%cursors), ' cursors] ', &
620 'Ln ', cursor%line, ', Col ', cursor%column, ' '
621 else
622 write(status_right, '(a,i0,a,i0,a)') 'Ln ', cursor%line, ', Col ', cursor%column, ' '
623 end if
624
625 ! Create full status bar with center text
626 padding_len = editor%screen_cols - len_trim(status_left) - len_trim(status_center) - len_trim(status_right)
627 if (padding_len > 0) then
628 ! Distribute padding around center text
629 left_pad = padding_len / 2
630 right_pad = padding_len - left_pad
631 status_bar = trim(status_left) // repeat(' ', left_pad) // &
632 trim(status_center) // repeat(' ', right_pad) // trim(status_right)
633 else
634 ! Not enough space for all three sections
635 ! If in match mode, prioritize showing the hint by reducing right side info
636 if (show_match_hint) then
637 ! Show: left + hint + minimal right (just line/col, no cursor count)
638 write(status_right, '(a,i0,a,i0,a)') 'Ln ', cursor%line, ',Col ', cursor%column, ' '
639 padding_len = editor%screen_cols - len_trim(status_left) - len_trim(status_center) - len_trim(status_right)
640 if (padding_len > 0) then
641 left_pad = padding_len / 2
642 right_pad = padding_len - left_pad
643 status_bar = trim(status_left) // repeat(' ', left_pad) // &
644 trim(status_center) // repeat(' ', right_pad) // trim(status_right)
645 else
646 ! Still not enough space, show hint + right only
647 padding_len = editor%screen_cols - len_trim(status_center) - len_trim(status_right)
648 if (padding_len > 0) then
649 status_bar = repeat(' ', padding_len / 2) // trim(status_center) // &
650 repeat(' ', padding_len - padding_len / 2) // trim(status_right)
651 else
652 ! Absolute minimum: just show the hint centered
653 padding_len = editor%screen_cols - len_trim(status_center)
654 if (padding_len > 0) then
655 left_pad = padding_len / 2
656 status_bar = repeat(' ', left_pad) // trim(status_center) // &
657 repeat(' ', padding_len - left_pad)
658 else
659 status_bar = status_center(1:editor%screen_cols)
660 end if
661 end if
662 end if
663 else
664 ! Normal mode: just show left and right
665 padding_len = editor%screen_cols - len_trim(status_left) - len_trim(status_right)
666 if (padding_len > 0) then
667 status_bar = trim(status_left) // repeat(' ', padding_len) // trim(status_right)
668 else
669 status_bar = status_left(1:editor%screen_cols)
670 end if
671 end if
672 end if
673
674 ! Render with inverse video
675 call terminal_write(char(27) // '[7m') ! Inverse video
676 call terminal_write(status_bar(1:editor%screen_cols))
677 call terminal_write(char(27) // '[0m') ! Reset attributes
678 end subroutine render_status_bar
679
680 subroutine render_cursor(editor, buffer)
681 type(editor_state_t), intent(in) :: editor
682 type(buffer_t), intent(in) :: buffer
683 type(cursor_t) :: cursor
684 integer :: screen_row, screen_col
685 integer :: i
686 integer :: col_offset, row_offset, min_row
687 character(len=:), allocatable :: line
688 character :: cursor_char
689
690 ! Calculate column offset for line numbers
691 if (show_line_numbers) then
692 col_offset = LINE_NUMBER_WIDTH + 1 ! +1 for separator space
693 else
694 col_offset = 0
695 end if
696
697 ! Account for tab bar offset - when tabs exist, row 1 is tab bar, content starts at row 2
698 if (size(editor%tabs) > 0) then
699 row_offset = 2 ! Tab bar takes row 1
700 min_row = 2 ! Cursor cannot be in row 1 (tab bar)
701 else
702 row_offset = 1 ! No tab bar
703 min_row = 1 ! Cursor can be in row 1
704 end if
705
706 ! For multiple cursors, show them all with block cursor for inactive ones
707 if (size(editor%cursors) > 1) then
708 ! First draw all inactive cursors
709 do i = 1, size(editor%cursors)
710 if (i /= editor%active_cursor) then
711 cursor = editor%cursors(i)
712
713 ! Calculate screen position from buffer position
714 screen_row = cursor%line - editor%viewport_line + row_offset
715
716 ! Calculate screen column based on display width
717 line = buffer_get_line(buffer, cursor%line)
718 block
719 character(len=:), allocatable :: prefix
720 integer :: byte_pos
721 byte_pos = utf8_char_to_byte_index(line, cursor%column)
722 if (byte_pos > 1) then
723 prefix = line(1:byte_pos-1)
724 screen_col = utf8_display_width(prefix) - editor%viewport_column + 1 + col_offset
725 else
726 screen_col = 1 - editor%viewport_column + col_offset
727 end if
728 if (allocated(prefix)) deallocate(prefix)
729 end block
730
731 ! Ensure cursor is within screen bounds and not in tab bar
732 if (screen_row >= min_row .and. screen_row < editor%screen_rows .and. &
733 screen_col >= 1 .and. screen_col <= editor%screen_cols) then
734 ! Get the character at this cursor position
735 if (cursor%column <= utf8_char_count(line)) then
736 cursor_char = utf8_char_at(line, cursor%column)
737 else
738 cursor_char = ' ' ! End of line
739 end if
740
741 ! Inactive cursor - draw character with reverse video
742 call terminal_move_cursor(screen_row, screen_col)
743 call terminal_write(char(27) // '[7m' // cursor_char) ! Inverse video
744 call terminal_write(char(27) // '[0m') ! Reset
745 end if
746 end if
747 end do
748
749 ! Then position terminal cursor at active cursor location
750 cursor = editor%cursors(editor%active_cursor)
751 screen_row = cursor%line - editor%viewport_line + row_offset
752
753 ! Calculate screen column based on display width
754 line = buffer_get_line(buffer, cursor%line)
755 block
756 character(len=:), allocatable :: prefix2
757 integer :: byte_pos2
758 byte_pos2 = utf8_char_to_byte_index(line, cursor%column)
759 if (byte_pos2 > 1) then
760 prefix2 = line(1:byte_pos2-1)
761 screen_col = utf8_display_width(prefix2) - editor%viewport_column + 1 + col_offset
762 else
763 screen_col = 1 - editor%viewport_column + col_offset
764 end if
765 if (allocated(prefix2)) deallocate(prefix2)
766 end block
767
768 if (screen_row >= min_row .and. screen_row < editor%screen_rows .and. &
769 screen_col >= 1 .and. screen_col <= editor%screen_cols) then
770 call terminal_move_cursor(screen_row, screen_col)
771 call terminal_show_cursor()
772 end if
773 else
774 ! Single cursor mode
775 cursor = editor%cursors(editor%active_cursor)
776
777 ! Calculate screen position from buffer position
778 screen_row = cursor%line - editor%viewport_line + row_offset
779 screen_col = cursor%column - editor%viewport_column + 1 + col_offset
780
781 ! Ensure cursor is within screen bounds and not in tab bar
782 if (screen_row >= min_row .and. screen_row < editor%screen_rows .and. &
783 screen_col >= 1 .and. screen_col <= editor%screen_cols) then
784 call terminal_move_cursor(screen_row, screen_col)
785 call terminal_show_cursor()
786 end if
787 end if
788 end subroutine render_cursor
789
790 ! Fast path for cursor-only updates - just update cursor and status bar
791 ! Use this when only cursor position changed, not buffer content
792 subroutine render_cursor_only(buffer, editor, match_mode_active, match_case_sens)
793 type(buffer_t), intent(in) :: buffer
794 type(editor_state_t), intent(inout) :: editor
795 logical, intent(in), optional :: match_mode_active
796 logical, intent(in), optional :: match_case_sens
797
798 ! Just render the status bar and position cursor
799 call render_status_bar(editor, buffer, match_mode_active, match_case_sens)
800
801 ! Handle panes vs single buffer
802 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0 .and. &
803 editor%active_tab_index <= size(editor%tabs)) then
804 if (allocated(editor%tabs(editor%active_tab_index)%panes)) then
805 if (editor%fuss_mode_active) then
806 call render_cursor_for_panes_with_tree(editor, 31, editor%screen_cols - 30)
807 else
808 call render_cursor_for_panes(editor)
809 end if
810 return
811 end if
812 end if
813
814 ! Single buffer mode
815 if (editor%fuss_mode_active) then
816 call render_cursor_in_pane(editor, 31, editor%screen_cols - 30)
817 else
818 call render_cursor(editor, buffer)
819 end if
820 end subroutine render_cursor_only
821
822 subroutine update_viewport(editor)
823 use editor_state_module, only: pane_t
824 type(editor_state_t), intent(inout) :: editor
825 type(cursor_t) :: cursor
826 integer :: margin = 3 ! Lines to keep visible above/below cursor
827 integer :: tab_idx, pane_idx
828 integer :: pane_height, pane_width
829 integer :: screen_width, screen_height
830
831 ! If we have panes, update the active pane's viewport
832 if (size(editor%tabs) > 0 .and. editor%active_tab_index > 0) then
833 tab_idx = editor%active_tab_index
834 if (allocated(editor%tabs(tab_idx)%panes)) then
835 pane_idx = editor%tabs(tab_idx)%active_pane_index
836 if (pane_idx > 0 .and. pane_idx <= size(editor%tabs(tab_idx)%panes)) then
837
838 if (allocated(editor%tabs(tab_idx)%panes(pane_idx)%cursors) .and. &
839 editor%tabs(tab_idx)%panes(pane_idx)%active_cursor > 0) then
840 cursor = editor%tabs(tab_idx)%panes(pane_idx)%cursors(&
841 editor%tabs(tab_idx)%panes(pane_idx)%active_cursor)
842
843 ! Calculate pane dimensions (account for fuss mode)
844 if (editor%fuss_mode_active) then
845 ! Fuss mode: editor takes ~70% of screen
846 screen_width = editor%screen_cols * 70 / 100
847 else
848 screen_width = editor%screen_cols
849 end if
850 screen_height = editor%screen_rows - 2
851 pane_height = int((editor%tabs(tab_idx)%panes(pane_idx)%y_end - &
852 editor%tabs(tab_idx)%panes(pane_idx)%y_start) * real(screen_height))
853 pane_width = int((editor%tabs(tab_idx)%panes(pane_idx)%x_end - &
854 editor%tabs(tab_idx)%panes(pane_idx)%x_start) * real(screen_width))
855
856 ! Account for line numbers in pane width
857 if (show_line_numbers) then
858 pane_width = pane_width - LINE_NUMBER_WIDTH - 1
859 end if
860
861 ! Vertical scrolling for pane
862 if (cursor%line < editor%tabs(tab_idx)%panes(pane_idx)%viewport_line + margin) then
863 editor%tabs(tab_idx)%panes(pane_idx)%viewport_line = max(1, cursor%line - margin)
864 else if (cursor%line > editor%tabs(tab_idx)%panes(pane_idx)%viewport_line + pane_height - margin - 1) then
865 editor%tabs(tab_idx)%panes(pane_idx)%viewport_line = cursor%line - pane_height + margin + 1
866 end if
867
868 ! Horizontal scrolling for pane
869 if (cursor%column < editor%tabs(tab_idx)%panes(pane_idx)%viewport_column + margin) then
870 editor%tabs(tab_idx)%panes(pane_idx)%viewport_column = max(1, cursor%column - margin)
871 else if (cursor%column > editor%tabs(tab_idx)%panes(pane_idx)%viewport_column + pane_width - margin) then
872 editor%tabs(tab_idx)%panes(pane_idx)%viewport_column = cursor%column - pane_width + margin
873 end if
874
875 ! Also update legacy editor viewport for compatibility
876 editor%viewport_line = editor%tabs(tab_idx)%panes(pane_idx)%viewport_line
877 editor%viewport_column = editor%tabs(tab_idx)%panes(pane_idx)%viewport_column
878 end if
879 return
880 end if
881 end if
882 end if
883
884 ! Fallback to original behavior if no panes
885 cursor = editor%cursors(editor%active_cursor)
886
887 ! Vertical scrolling
888 if (cursor%line < editor%viewport_line + margin) then
889 editor%viewport_line = max(1, cursor%line - margin)
890 else if (cursor%line > editor%viewport_line + editor%screen_rows - margin - 2) then
891 ! -2 for status bar and margin
892 editor%viewport_line = cursor%line - editor%screen_rows + margin + 2
893 end if
894
895 ! Horizontal scrolling (account for fuss mode and line numbers)
896 if (editor%fuss_mode_active) then
897 screen_width = editor%screen_cols * 70 / 100
898 else
899 screen_width = editor%screen_cols
900 end if
901
902 ! Account for line numbers
903 if (show_line_numbers) then
904 screen_width = screen_width - LINE_NUMBER_WIDTH - 1
905 end if
906
907 if (cursor%column < editor%viewport_column + margin) then
908 editor%viewport_column = max(1, cursor%column - margin)
909 else if (cursor%column > editor%viewport_column + screen_width - margin) then
910 editor%viewport_column = cursor%column - screen_width + margin
911 end if
912 end subroutine update_viewport
913
914 ! Render screen with split panes (tree on left, editor on right)
915 subroutine render_screen_with_tree(buffer, editor, match_mode_active, match_case_sens)
916 type(buffer_t), intent(in) :: buffer
917 type(editor_state_t), intent(inout) :: editor
918 logical, intent(in), optional :: match_mode_active
919 logical, intent(in), optional :: match_case_sens
920 integer :: tree_width, editor_start_col, editor_width
921 integer :: separator_col
922 integer :: row
923
924 call terminal_hide_cursor()
925
926 ! Clear screen first to avoid artifacts
927 do row = 1, editor%screen_rows
928 call terminal_move_cursor(row, 1)
929 call terminal_write(repeat(' ', editor%screen_cols))
930 end do
931
932 ! Calculate split: 30% for tree, 70% for editor
933 tree_width = editor%screen_cols * 30 / 100
934 separator_col = tree_width + 1
935 editor_start_col = tree_width + 2
936 editor_width = editor%screen_cols - editor_start_col + 1
937
938 ! Render tab bar if there are any tabs (positioned in editor pane area)
939 call render_tab_bar(editor, editor_start_col, editor_width)
940
941 ! Render file tree in left pane (start at row 2 for tab bar)
942 call render_file_tree(tree_state, 2, editor%screen_rows - 1, 2, tree_width - 2, &
943 editor%fuss_hints_expanded, fuss_git_prefix_active)
944
945 ! Render vertical separator (start at row 2 for tab bar)
946 call render_vertical_separator(separator_col, 2, editor%screen_rows - 1)
947
948 ! Render editor in right pane (check for multiple panes)
949 call render_editor_area_with_tree(editor, editor_start_col, editor_width)
950
951 ! Render status bar (full width)
952 call render_status_bar(editor, buffer, match_mode_active, match_case_sens)
953
954 ! Render diagnostics panel if visible
955 if (allocated(editor%filename)) then
956 block
957 character(len=:), allocatable :: file_uri
958 file_uri = 'file://' // trim(editor%filename)
959 call render_diagnostics_panel(editor%diagnostics_panel, editor%diagnostics, &
960 file_uri, editor%screen_rows, editor%screen_cols)
961 end block
962 end if
963
964 ! Render references panel if visible
965 call render_references_panel(editor%references_panel, 3)
966
967 ! Render code actions menu if visible
968 call render_code_actions_panel(editor%code_actions_panel, editor%screen_rows, editor%screen_cols)
969
970 ! Render symbols panel if visible
971 call render_symbols_panel(editor%symbols_panel, editor%screen_rows)
972
973 ! Render LSP server installer panel if visible
974 if (is_lsp_server_installer_panel_visible(editor%lsp_installer_panel)) then
975 call render_lsp_server_installer_panel(editor%lsp_installer_panel, &
976 editor%screen_cols)
977 end if
978
979 ! Position cursor in editor pane (use appropriate method based on pane count)
980 if (size(editor%tabs(editor%active_tab_index)%panes) > 1) then
981 ! Multiple panes: use pane-aware cursor rendering with tree offset
982 call render_cursor_for_panes_with_tree(editor, editor_start_col, editor_width)
983 else
984 ! Single pane: use simple cursor rendering
985 call render_cursor_in_pane(editor, editor_start_col, editor_width)
986 end if
987
988 call terminal_show_cursor()
989 end subroutine render_screen_with_tree
990
991 subroutine render_vertical_separator(col, start_row, end_row)
992 integer, intent(in) :: col, start_row, end_row
993 integer :: row
994
995 do row = start_row, end_row
996 call terminal_move_cursor(row, col)
997 call terminal_write(char(27) // '[90m│' // char(27) // '[0m') ! Gray vertical line
998 end do
999 end subroutine render_vertical_separator
1000
1001 subroutine render_editor_area_with_tree(editor, start_col, width)
1002 use editor_state_module, only: pane_t
1003 type(editor_state_t), intent(inout) :: editor
1004 integer, intent(in) :: start_col, width
1005 type(pane_t) :: pane
1006 integer :: i, tab_idx, n_panes
1007 integer :: pane_col, pane_row, pane_width, pane_height
1008 integer :: screen_height
1009
1010 ! Get active tab
1011 tab_idx = editor%active_tab_index
1012 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) then
1013 ! No valid tab, render empty
1014 return
1015 end if
1016
1017 if (.not. allocated(editor%tabs(tab_idx)%panes)) then
1018 ! No panes, render empty
1019 return
1020 end if
1021
1022 n_panes = size(editor%tabs(tab_idx)%panes)
1023 if (n_panes == 0) return
1024
1025 screen_height = editor%screen_rows - 2 ! Account for tab bar and status bar
1026
1027 ! If only one pane, use simple rendering
1028 if (n_panes == 1) then
1029 ! Use the pane's buffer, not the passed buffer parameter
1030 call render_editor_pane(editor%tabs(tab_idx)%panes(1)%buffer, editor, start_col, width)
1031 return
1032 end if
1033
1034 ! Multiple panes: render each with adjusted coordinates for tree view
1035 ! Clear the editor area first
1036 do i = 2, editor%screen_rows - 1
1037 call terminal_move_cursor(i, start_col)
1038 call terminal_write(repeat(' ', width))
1039 end do
1040
1041 ! Render each pane with coordinates adjusted for tree offset
1042 do i = 1, n_panes
1043 pane = editor%tabs(tab_idx)%panes(i)
1044
1045 ! Calculate pane position relative to editor area (not full screen)
1046 pane_col = start_col + int(pane%x_start * real(width))
1047 if (i < n_panes) then
1048 pane_width = int((pane%x_end - pane%x_start) * real(width)) - 1
1049 else
1050 pane_width = int((pane%x_end - pane%x_start) * real(width))
1051 end if
1052 pane_row = 2 + int(pane%y_start * real(screen_height))
1053 pane_height = int((pane%y_end - pane%y_start) * real(screen_height))
1054
1055 ! Store the calculated screen coordinates in the pane
1056 editor%tabs(tab_idx)%panes(i)%screen_col = pane_col
1057 editor%tabs(tab_idx)%panes(i)%screen_row = pane_row
1058 editor%tabs(tab_idx)%panes(i)%screen_width = pane_width
1059 editor%tabs(tab_idx)%panes(i)%screen_height = pane_height
1060
1061 ! Render the pane content
1062 call render_single_pane(editor, i, pane_col, pane_row, pane_width, pane_height)
1063
1064 ! Draw vertical separator between panes
1065 if (i < n_panes) then
1066 call render_pane_separator(pane_col + pane_width, pane_row, pane_height)
1067 end if
1068 end do
1069 end subroutine render_editor_area_with_tree
1070
1071 subroutine render_editor_pane(buffer, editor, start_col, width)
1072 type(buffer_t), intent(in) :: buffer
1073 type(editor_state_t), intent(in) :: editor
1074 integer, intent(in) :: start_col, width
1075 integer :: screen_row, buffer_line, line_count
1076 integer :: adjusted_width, line_num_width
1077 integer :: start_row
1078 character(len=16) :: line_num_str
1079 character(len=:), allocatable :: padding
1080
1081 line_count = buffer_get_line_count(buffer)
1082
1083 ! Calculate content width (accounting for line numbers if enabled)
1084 if (show_line_numbers) then
1085 line_num_width = LINE_NUMBER_WIDTH + 1
1086 adjusted_width = width - line_num_width
1087 else
1088 line_num_width = 0
1089 adjusted_width = width
1090 end if
1091
1092 ! Determine starting row (account for tab bar)
1093 if (size(editor%tabs) > 0) then
1094 start_row = 2 ! Tab bar at row 1
1095 else
1096 start_row = 1 ! No tab bar
1097 end if
1098
1099 ! Render each visible line in the editor pane
1100 do screen_row = start_row, editor%screen_rows - 1
1101 buffer_line = editor%viewport_line + screen_row - start_row
1102
1103 ! Position cursor at start of this line in the pane
1104 call terminal_move_cursor(screen_row, start_col)
1105
1106 ! Render line number if enabled
1107 if (show_line_numbers) then
1108 if (buffer_line <= line_count) then
1109 write(line_num_str, '(i5)') buffer_line
1110 if (buffer_line == editor%cursors(editor%active_cursor)%line) then
1111 call terminal_write(char(27) // '[1;33m' // adjustl(line_num_str(1:LINE_NUMBER_WIDTH)) &
1112 // char(27) // '[0m ')
1113 else
1114 call terminal_write(char(27) // '[90m' // adjustl(line_num_str(1:LINE_NUMBER_WIDTH)) &
1115 // char(27) // '[0m ')
1116 end if
1117 else
1118 call terminal_write(repeat(' ', line_num_width))
1119 end if
1120 end if
1121
1122 ! Render content (render_line_with_selections will write exactly adjusted_width chars)
1123 if (buffer_line <= line_count) then
1124 call render_line_with_selections(buffer, editor, buffer_line, &
1125 editor%viewport_column, adjusted_width)
1126 else
1127 ! Empty line beyond file content
1128 padding = '~' // repeat(' ', adjusted_width - 1)
1129 call terminal_write(padding)
1130 end if
1131 ! Clear to end of line to prevent stale content when scrolling
1132 call terminal_write(char(27) // '[K')
1133 end do
1134 end subroutine render_editor_pane
1135
1136 subroutine render_all_panes(editor)
1137 use editor_state_module, only: pane_t
1138 type(editor_state_t), intent(inout) :: editor
1139 type(pane_t) :: pane
1140 integer :: i, tab_idx, n_panes, active_pane_idx
1141 integer :: pane_col, pane_row, pane_width, pane_height
1142 integer :: screen_width, screen_height
1143 character(len=:), allocatable :: line_content
1144 character(len=1) :: cursor_char
1145 logical :: found_match
1146 type(cursor_t) :: active_cursor
1147
1148 ! Get active tab
1149 tab_idx = editor%active_tab_index
1150 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
1151 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
1152
1153 n_panes = size(editor%tabs(tab_idx)%panes)
1154 if (n_panes == 0) return
1155
1156 active_pane_idx = editor%tabs(tab_idx)%active_pane_index
1157
1158 ! Calculate bracket matching for the active pane's cursor
1159 bracket_line = 0
1160 bracket_col = 0
1161 matching_bracket_line = 0
1162 matching_bracket_col = 0
1163
1164 if (active_pane_idx > 0 .and. active_pane_idx <= n_panes) then
1165 pane = editor%tabs(tab_idx)%panes(active_pane_idx)
1166 if (allocated(pane%cursors) .and. size(pane%cursors) > 0) then
1167 active_cursor = pane%cursors(1) ! Use first cursor for bracket matching
1168 line_content = buffer_get_line(pane%buffer, active_cursor%line)
1169 if (active_cursor%column >= 1 .and. active_cursor%column <= len(line_content)) then
1170 cursor_char = line_content(active_cursor%column:active_cursor%column)
1171 if (is_opening_bracket(cursor_char) .or. is_closing_bracket(cursor_char)) then
1172 bracket_line = active_cursor%line
1173 bracket_col = active_cursor%column
1174 call find_matching_bracket(pane%buffer, bracket_line, bracket_col, &
1175 found_match, matching_bracket_line, matching_bracket_col)
1176 if (.not. found_match) then
1177 matching_bracket_line = 0
1178 matching_bracket_col = 0
1179 end if
1180 end if
1181 end if
1182 if (allocated(line_content)) deallocate(line_content)
1183 end if
1184 end if
1185
1186 ! Get screen dimensions
1187 screen_width = editor%screen_cols
1188 screen_height = editor%screen_rows - 2 ! Account for tab bar (row 1) and status bar (last row)
1189
1190 ! Reduce width if diagnostics panel is visible
1191 if (editor%diagnostics_panel%visible) then
1192 screen_width = screen_width - editor%diagnostics_panel%width
1193 end if
1194
1195 ! Reduce width if references panel is visible
1196 if (editor%references_panel%visible) then
1197 screen_width = screen_width - editor%references_panel%width
1198 end if
1199
1200 ! If only one pane, render full screen
1201 if (n_panes == 1) then
1202 ! Set screen coordinates for the single pane
1203 editor%tabs(tab_idx)%panes(1)%screen_col = 1
1204 editor%tabs(tab_idx)%panes(1)%screen_row = 2 ! After tab bar
1205 editor%tabs(tab_idx)%panes(1)%screen_width = screen_width
1206 editor%tabs(tab_idx)%panes(1)%screen_height = screen_height
1207
1208 ! Use the pane's buffer, not the passed buffer parameter
1209 call render_editor_pane(editor%tabs(tab_idx)%panes(1)%buffer, editor, 1, screen_width)
1210 return
1211 end if
1212
1213 ! Clear the editor area first with background
1214 do i = 2, editor%screen_rows - 1
1215 call terminal_move_cursor(i, 1)
1216 call terminal_write(repeat(' ', screen_width))
1217 end do
1218
1219 ! Render each pane with gaps
1220 do i = 1, n_panes
1221 pane = editor%tabs(tab_idx)%panes(i)
1222
1223 ! Calculate actual screen coordinates with gap consideration
1224 ! Add 1 column gap on right side of each pane except the last
1225 pane_col = 1 + int(pane%x_start * real(screen_width))
1226 if (i < n_panes) then
1227 ! Reserve 1 column for the border/gap
1228 pane_width = int((pane%x_end - pane%x_start) * real(screen_width)) - 1
1229 else
1230 ! Last pane uses full width
1231 pane_width = int((pane%x_end - pane%x_start) * real(screen_width))
1232 end if
1233 pane_row = 2 + int(pane%y_start * real(screen_height))
1234 pane_height = int((pane%y_end - pane%y_start) * real(screen_height))
1235
1236 ! Store the calculated screen coordinates in the pane
1237 editor%tabs(tab_idx)%panes(i)%screen_col = pane_col
1238 editor%tabs(tab_idx)%panes(i)%screen_row = pane_row
1239 editor%tabs(tab_idx)%panes(i)%screen_width = pane_width
1240 editor%tabs(tab_idx)%panes(i)%screen_height = pane_height
1241
1242 ! Render the pane content
1243 call render_single_pane(editor, i, pane_col, pane_row, pane_width, pane_height)
1244
1245 ! Draw vertical separator between panes
1246 if (i < n_panes) then
1247 call render_pane_separator(pane_col + pane_width, pane_row, pane_height)
1248 end if
1249 end do
1250 end subroutine render_all_panes
1251
1252 subroutine render_single_pane(editor, pane_idx, col, row, width, height)
1253 use editor_state_module, only: pane_t
1254 type(editor_state_t), intent(in) :: editor
1255 integer, intent(in) :: pane_idx, col, row, width, height
1256 type(pane_t) :: pane
1257 integer :: screen_row, buffer_line, content_start_row, content_height
1258 integer :: tab_idx
1259
1260 tab_idx = editor%active_tab_index
1261 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
1262 if (pane_idx < 1 .or. pane_idx > size(editor%tabs(tab_idx)%panes)) return
1263
1264 pane = editor%tabs(tab_idx)%panes(pane_idx)
1265
1266 ! Draw pane header with filename (if more than one pane exists)
1267 if (size(editor%tabs(tab_idx)%panes) > 1) then
1268 call render_pane_header(pane, col, row, width)
1269 content_start_row = row + 1
1270 content_height = height - 1
1271 else
1272 content_start_row = row
1273 content_height = height
1274 end if
1275
1276 ! Clear the pane area with subtle background for inactive panes
1277 do screen_row = content_start_row, content_start_row + content_height - 1
1278 call terminal_move_cursor(screen_row, col)
1279 if (.not. pane%is_active) then
1280 ! Subtle dark background for inactive panes
1281 call terminal_write(char(27) // '[48;5;234m') ! Very dark gray
1282 end if
1283 call terminal_write(repeat(' ', width))
1284 call terminal_write(char(27) // '[0m')
1285 end do
1286
1287 ! Render buffer content with pane's viewport (use pane's own buffer)
1288 do screen_row = content_start_row, content_start_row + content_height - 1
1289 buffer_line = pane%viewport_line + (screen_row - content_start_row)
1290 if (buffer_line > 0 .and. buffer_line <= buffer_get_line_count(pane%buffer)) then
1291 call render_buffer_line_in_pane(pane%buffer, editor, pane_idx, buffer_line, &
1292 screen_row, col, width)
1293 else
1294 ! Render empty line indicator for lines beyond file
1295 call terminal_move_cursor(screen_row, col)
1296
1297 ! Render empty line number area if line numbers are enabled
1298 if (show_line_numbers) then
1299 if (.not. pane%is_active) then
1300 call terminal_write(char(27) // '[48;5;234m') ! Dark gray for inactive
1301 end if
1302 call terminal_write(repeat(' ', LINE_NUMBER_WIDTH + 1))
1303 end if
1304
1305 ! Render the ~ indicator
1306 if (.not. pane%is_active) then
1307 call terminal_write(char(27) // '[48;5;234m') ! Dark gray for inactive
1308 end if
1309 call terminal_write('~')
1310
1311 ! Calculate remaining width accounting for line numbers
1312 if (show_line_numbers) then
1313 if (width > LINE_NUMBER_WIDTH + 2) then
1314 call terminal_write(repeat(' ', width - LINE_NUMBER_WIDTH - 2))
1315 end if
1316 else
1317 if (width > 1) then
1318 call terminal_write(repeat(' ', width - 1))
1319 end if
1320 end if
1321 call terminal_write(char(27) // '[0m')
1322 end if
1323 end do
1324 end subroutine render_single_pane
1325
1326 subroutine render_pane_header(pane, col, row, width)
1327 use editor_state_module, only: pane_t
1328 type(pane_t), intent(in) :: pane
1329 integer, intent(in) :: col, row, width
1330 character(len=:), allocatable :: filename_display, filename_only
1331 character(len=256) :: temp_display
1332 integer :: slash_pos, display_len, padding_left, padding_right
1333
1334 ! Move to header position
1335 call terminal_move_cursor(row, col)
1336
1337 ! Extract filename from path
1338 if (allocated(pane%filename)) then
1339 ! Find last slash to get just the filename
1340 slash_pos = index(pane%filename, '/', back=.true.)
1341 if (slash_pos > 0) then
1342 filename_only = pane%filename(slash_pos+1:)
1343 else
1344 filename_only = pane%filename
1345 end if
1346
1347 ! Build display string with brackets
1348 write(temp_display, '(A,A,A)') ' [', trim(filename_only), '] '
1349 filename_display = trim(temp_display)
1350 else
1351 filename_display = ' [untitled] '
1352 end if
1353
1354 ! Calculate padding
1355 display_len = len(filename_display)
1356 if (display_len < width) then
1357 padding_left = (width - display_len) / 2
1358 padding_right = width - display_len - padding_left
1359 else
1360 ! Truncate if too long
1361 filename_display = filename_display(1:width)
1362 padding_left = 0
1363 padding_right = 0
1364 end if
1365
1366 ! Draw header with reverse video (like tab bar)
1367 if (pane%is_active) then
1368 ! Active pane: bright reverse video
1369 call terminal_write(char(27) // '[7m') ! Reverse video
1370 else
1371 ! Inactive pane: dimmed reverse video
1372 call terminal_write(char(27) // '[2;7m') ! Dim + reverse video
1373 end if
1374
1375 ! Draw the header line
1376 call terminal_write(repeat('─', padding_left))
1377 call terminal_write(filename_display)
1378 call terminal_write(repeat('─', padding_right))
1379
1380 ! Reset attributes
1381 call terminal_write(char(27) // '[0m')
1382 end subroutine render_pane_header
1383
1384 subroutine render_buffer_line_in_pane(buffer, editor, pane_idx, line_num, screen_row, col, width)
1385 use editor_state_module, only: pane_t
1386 type(buffer_t), intent(in) :: buffer
1387 type(editor_state_t), intent(in) :: editor
1388 integer, intent(in) :: pane_idx, line_num, screen_row, col, width
1389 character(len=:), allocatable :: line, utf8_ch
1390 type(pane_t) :: pane
1391 integer :: tab_idx, i, char_idx, char_count, display_col, char_width
1392 integer :: content_width, content_col
1393 character(len=5) :: line_num_str
1394 logical :: is_current_line, in_selection, is_bracket_match
1395 integer :: sel_start_line, sel_start_col, sel_end_line, sel_end_col
1396 ! Syntax highlighting support
1397 type(token_t), allocatable :: tokens(:)
1398 character(len=:), allocatable :: token_color
1399 integer :: byte_pos, token_idx, line_byte_len
1400
1401 tab_idx = editor%active_tab_index
1402 pane = editor%tabs(tab_idx)%panes(pane_idx)
1403
1404 ! Move to position
1405 call terminal_move_cursor(screen_row, col)
1406
1407 ! Render line number if enabled
1408 if (show_line_numbers) then
1409 write(line_num_str, '(i5)') line_num
1410 ! Check if this line has any cursor
1411 is_current_line = .false.
1412 if (allocated(pane%cursors)) then
1413 do i = 1, size(pane%cursors)
1414 if (pane%cursors(i)%line == line_num) then
1415 is_current_line = .true.
1416 exit
1417 end if
1418 end do
1419 end if
1420
1421 ! Apply pane background for inactive panes
1422 if (.not. pane%is_active) then
1423 call terminal_write(char(27) // '[48;5;234m') ! Dark gray background
1424 end if
1425
1426 if (is_current_line .and. pane%is_active) then
1427 call terminal_write(char(27) // '[1;33m' // adjustl(line_num_str(1:LINE_NUMBER_WIDTH)) &
1428 // char(27) // '[0m ')
1429 else
1430 call terminal_write(char(27) // '[90m' // adjustl(line_num_str(1:LINE_NUMBER_WIDTH)) &
1431 // char(27) // '[0m ')
1432 end if
1433
1434 ! Continue with pane background for content
1435 if (.not. pane%is_active) then
1436 call terminal_write(char(27) // '[48;5;234m') ! Dark gray background
1437 end if
1438
1439 content_width = width - LINE_NUMBER_WIDTH - 1
1440 content_col = col + LINE_NUMBER_WIDTH + 1
1441 else
1442 content_width = width
1443 content_col = col
1444 end if
1445
1446 ! Get the line content
1447 line = buffer_get_line(buffer, line_num)
1448 if (.not. allocated(line)) return
1449
1450 ! Get character count for UTF-8 iteration
1451 char_count = utf8_char_count(line)
1452 line_byte_len = len(line)
1453
1454 ! Get syntax tokens for this line
1455 if (syntax_highlighter%enabled) then
1456 call tokenize_line(syntax_highlighter, line, tokens)
1457 else
1458 allocate(tokens(1))
1459 tokens(1)%type = TOKEN_PLAIN
1460 tokens(1)%start_col = 1
1461 tokens(1)%end_col = max(1, line_byte_len)
1462 end if
1463
1464 ! Check if this is the current line with a cursor
1465 is_current_line = .false.
1466 if (allocated(pane%cursors)) then
1467 do i = 1, size(pane%cursors)
1468 if (pane%cursors(i)%line == line_num) then
1469 is_current_line = .true.
1470 exit
1471 end if
1472 end do
1473 end if
1474
1475 ! Render the line character by character with selection highlighting
1476 ! Using UTF-8 aware iteration
1477 display_col = 0
1478 char_idx = pane%viewport_column ! Start from viewport column (character index)
1479
1480 do while (char_idx <= char_count .and. display_col < content_width)
1481 in_selection = .false.
1482
1483 ! Get the UTF-8 character at this position
1484 utf8_ch = utf8_char_at(line, char_idx)
1485 char_width = utf8_display_width(utf8_ch)
1486
1487 ! Check if this position is in any cursor's selection (use pane's cursors)
1488 if (allocated(pane%cursors)) then
1489 do i = 1, size(pane%cursors)
1490 if (pane%cursors(i)%has_selection) then
1491 ! Determine selection bounds (handle both directions)
1492 if (pane%cursors(i)%line < pane%cursors(i)%selection_start_line .or. &
1493 (pane%cursors(i)%line == pane%cursors(i)%selection_start_line .and. &
1494 pane%cursors(i)%column < pane%cursors(i)%selection_start_col)) then
1495 ! Cursor is before selection start (selecting upward)
1496 sel_start_line = pane%cursors(i)%line
1497 sel_start_col = pane%cursors(i)%column
1498 sel_end_line = pane%cursors(i)%selection_start_line
1499 sel_end_col = pane%cursors(i)%selection_start_col
1500 else
1501 ! Cursor is after selection start (selecting downward)
1502 sel_start_line = pane%cursors(i)%selection_start_line
1503 sel_start_col = pane%cursors(i)%selection_start_col
1504 sel_end_line = pane%cursors(i)%line
1505 sel_end_col = pane%cursors(i)%column
1506 end if
1507
1508 ! Check if this position is selected (using char_idx)
1509 if (line_num > sel_start_line .and. line_num < sel_end_line) then
1510 ! Fully selected line (between start and end)
1511 in_selection = .true.
1512 exit
1513 else if (line_num == sel_start_line .and. line_num == sel_end_line) then
1514 ! Single-line selection
1515 if (char_idx >= sel_start_col .and. char_idx < sel_end_col) then
1516 in_selection = .true.
1517 exit
1518 end if
1519 else if (line_num == sel_start_line .and. line_num < sel_end_line) then
1520 ! First line of multi-line selection
1521 if (char_idx >= sel_start_col) then
1522 in_selection = .true.
1523 exit
1524 end if
1525 else if (line_num == sel_end_line .and. line_num > sel_start_line) then
1526 ! Last line of multi-line selection
1527 if (char_idx < sel_end_col) then
1528 in_selection = .true.
1529 exit
1530 end if
1531 end if
1532 end if
1533 end do
1534 end if
1535
1536 ! Check if this position is a bracket or its match (only for active pane)
1537 is_bracket_match = .false.
1538 if (pane%is_active) then
1539 if ((line_num == bracket_line .and. char_idx == bracket_col) .or. &
1540 (line_num == matching_bracket_line .and. char_idx == matching_bracket_col)) then
1541 is_bracket_match = .true.
1542 end if
1543 end if
1544
1545 ! Get byte position for syntax token lookup
1546 byte_pos = utf8_char_to_byte_index(line, char_idx)
1547
1548 ! Find which token this column belongs to (tokens use byte indices)
1549 token_color = ""
1550 if (syntax_highlighter%enabled .and. byte_pos > 0) then
1551 do token_idx = 1, size(tokens)
1552 if (byte_pos >= tokens(token_idx)%start_col .and. &
1553 byte_pos <= tokens(token_idx)%end_col) then
1554 token_color = get_token_color(tokens(token_idx)%type)
1555 exit
1556 end if
1557 end do
1558 end if
1559
1560 ! Render the character with appropriate highlighting
1561 if (in_selection) then
1562 ! Highlight selected text with reverse video
1563 call terminal_write(char(27) // '[7m' // utf8_ch // char(27) // '[0m')
1564 else if (is_bracket_match) then
1565 ! Highlight matching brackets with cyan background
1566 call terminal_write(char(27) // '[46m' // utf8_ch // char(27) // '[0m')
1567 else if (pane%is_active .and. is_current_line) then
1568 ! Current line with syntax highlighting
1569 if (len(token_color) > 0) then
1570 call terminal_write(token_color // char(27) // '[48;5;237m' // utf8_ch // char(27) // '[0m')
1571 else
1572 call terminal_write(char(27) // '[48;5;237m' // utf8_ch // char(27) // '[0m')
1573 end if
1574 else if (.not. pane%is_active) then
1575 ! Inactive pane with syntax highlighting
1576 if (len(token_color) > 0) then
1577 call terminal_write(token_color // char(27) // '[48;5;234m' // utf8_ch // char(27) // '[0m')
1578 else
1579 call terminal_write(char(27) // '[48;5;234m' // utf8_ch // char(27) // '[0m')
1580 end if
1581 else if (len(token_color) > 0) then
1582 ! Normal text with syntax highlighting
1583 call terminal_write(token_color // utf8_ch // char(27) // '[0m')
1584 else
1585 ! Normal text without highlighting
1586 call terminal_write(utf8_ch)
1587 end if
1588
1589 display_col = display_col + char_width
1590 char_idx = char_idx + 1
1591 end do
1592
1593 ! Fill remaining width with spaces
1594 do while (display_col < content_width)
1595 if (.not. pane%is_active) then
1596 call terminal_write(char(27) // '[48;5;234m ' // char(27) // '[0m')
1597 else if (is_current_line) then
1598 call terminal_write(char(27) // '[48;5;237m ' // char(27) // '[0m')
1599 else
1600 call terminal_write(' ')
1601 end if
1602 display_col = display_col + 1
1603 end do
1604
1605 ! Reset attributes
1606 call terminal_write(char(27) // '[0m')
1607
1608 if (allocated(utf8_ch)) deallocate(utf8_ch)
1609 end subroutine render_buffer_line_in_pane
1610
1611 subroutine render_pane_separator(col, start_row, height)
1612 integer, intent(in) :: col, start_row, height
1613 integer :: row
1614
1615 ! Draw vertical separator with distinct visual
1616 do row = start_row, start_row + height - 1
1617 call terminal_move_cursor(row, col)
1618 ! Use reverse video for a solid separator
1619 call terminal_write(char(27) // '[7m ') ! Reverse video space
1620 call terminal_write(char(27) // '[0m') ! Reset
1621 end do
1622 end subroutine render_pane_separator
1623
1624 subroutine render_cursor_for_panes(editor)
1625 use editor_state_module, only: pane_t
1626 type(editor_state_t), intent(in) :: editor
1627 type(pane_t) :: pane
1628 type(cursor_t) :: cursor
1629 integer :: tab_idx, pane_idx, i
1630 integer :: pane_col, pane_row, pane_width, pane_height
1631 integer :: screen_row, screen_col
1632 integer :: screen_width, screen_height
1633 integer :: col_offset
1634 character(len=:), allocatable :: line
1635 character :: cursor_char
1636
1637 tab_idx = editor%active_tab_index
1638 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
1639 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
1640
1641 pane_idx = editor%tabs(tab_idx)%active_pane_index
1642 if (pane_idx < 1 .or. pane_idx > size(editor%tabs(tab_idx)%panes)) return
1643
1644 pane = editor%tabs(tab_idx)%panes(pane_idx)
1645 if (.not. allocated(pane%cursors)) return
1646 if (pane%active_cursor < 1 .or. pane%active_cursor > size(pane%cursors)) return
1647
1648 ! Calculate column offset for line numbers
1649 if (show_line_numbers) then
1650 col_offset = LINE_NUMBER_WIDTH + 1 ! +1 for separator space
1651 else
1652 col_offset = 0
1653 end if
1654
1655 ! Calculate pane screen coordinates
1656 screen_width = editor%screen_cols
1657 screen_height = editor%screen_rows - 2 ! Account for tab bar (row 1) and status bar (last row)
1658
1659 ! Reduce width if diagnostics panel is visible
1660 if (editor%diagnostics_panel%visible) then
1661 screen_width = screen_width - editor%diagnostics_panel%width
1662 end if
1663
1664 ! Reduce width if references panel is visible
1665 if (editor%references_panel%visible) then
1666 screen_width = screen_width - editor%references_panel%width
1667 end if
1668
1669 pane_col = 1 + int(pane%x_start * real(screen_width))
1670 pane_width = int((pane%x_end - pane%x_start) * real(screen_width))
1671 if (pane_idx < size(editor%tabs(tab_idx)%panes)) then
1672 pane_width = pane_width - 1 ! Reserve space for separator
1673 end if
1674 pane_row = 2 + int(pane%y_start * real(screen_height))
1675 pane_height = int((pane%y_end - pane%y_start) * real(screen_height))
1676
1677 ! Account for pane header when multiple panes exist
1678 if (size(editor%tabs(tab_idx)%panes) > 1) then
1679 pane_row = pane_row + 1 ! Content starts after header
1680 pane_height = pane_height - 1 ! Height reduced by header
1681 end if
1682
1683 ! For multiple cursors, render all inactive ones first
1684 if (size(pane%cursors) > 1) then
1685 do i = 1, size(pane%cursors)
1686 if (i /= pane%active_cursor) then
1687 cursor = pane%cursors(i)
1688
1689 ! Calculate cursor position within the pane
1690 screen_row = pane_row + (cursor%line - pane%viewport_line)
1691 screen_col = pane_col + col_offset + (cursor%column - pane%viewport_column)
1692
1693 ! Ensure cursor is within pane boundaries
1694 if (screen_row >= pane_row .and. screen_row < pane_row + pane_height .and. &
1695 screen_col >= pane_col + col_offset .and. screen_col < pane_col + pane_width) then
1696 ! Get the character at this cursor position
1697 line = buffer_get_line(pane%buffer, cursor%line)
1698 if (cursor%column <= len(line)) then
1699 cursor_char = line(cursor%column:cursor%column)
1700 else
1701 cursor_char = ' ' ! End of line
1702 end if
1703
1704 ! Inactive cursor - draw character with reverse video
1705 call terminal_move_cursor(screen_row, screen_col)
1706 call terminal_write(char(27) // '[7m' // cursor_char) ! Inverse video
1707 call terminal_write(char(27) // '[0m') ! Reset
1708 end if
1709 end if
1710 end do
1711 end if
1712
1713 ! Now render the active cursor
1714 cursor = pane%cursors(pane%active_cursor)
1715
1716 ! Calculate cursor position within the pane, accounting for line numbers
1717 screen_row = pane_row + (cursor%line - pane%viewport_line)
1718 screen_col = pane_col + col_offset + (cursor%column - pane%viewport_column)
1719
1720 ! Ensure cursor is within pane boundaries
1721 if (screen_row >= pane_row .and. screen_row < pane_row + pane_height .and. &
1722 screen_col >= pane_col + col_offset .and. screen_col < pane_col + pane_width) then
1723 call terminal_move_cursor(screen_row, screen_col)
1724 else
1725 ! Cursor is out of view, position at top-left of pane content area
1726 call terminal_move_cursor(pane_row, pane_col + col_offset)
1727 end if
1728
1729 ! Always show cursor
1730 call terminal_show_cursor()
1731 end subroutine render_cursor_for_panes
1732
1733 subroutine render_cursor_for_panes_with_tree(editor, tree_offset, editor_width)
1734 use editor_state_module, only: pane_t
1735 type(editor_state_t), intent(in) :: editor
1736 integer, intent(in) :: tree_offset, editor_width
1737 type(pane_t) :: pane
1738 type(cursor_t) :: cursor
1739 integer :: tab_idx, pane_idx
1740 integer :: pane_col, pane_row, pane_width, pane_height
1741 integer :: screen_row, screen_col, col_offset
1742 integer :: screen_height
1743
1744 tab_idx = editor%active_tab_index
1745 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
1746 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
1747
1748 pane_idx = editor%tabs(tab_idx)%active_pane_index
1749 if (pane_idx < 1 .or. pane_idx > size(editor%tabs(tab_idx)%panes)) return
1750
1751 pane = editor%tabs(tab_idx)%panes(pane_idx)
1752 if (.not. allocated(pane%cursors)) return
1753 if (pane%active_cursor < 1 .or. pane%active_cursor > size(pane%cursors)) return
1754
1755 cursor = pane%cursors(pane%active_cursor)
1756
1757 ! Calculate column offset for line numbers
1758 if (show_line_numbers) then
1759 col_offset = LINE_NUMBER_WIDTH + 1
1760 else
1761 col_offset = 0
1762 end if
1763
1764 ! Calculate pane coordinates (adjusted for tree)
1765 screen_height = editor%screen_rows - 2
1766 pane_col = tree_offset + int(pane%x_start * real(editor_width))
1767 pane_width = int((pane%x_end - pane%x_start) * real(editor_width))
1768 if (pane_idx < size(editor%tabs(tab_idx)%panes)) then
1769 pane_width = pane_width - 1
1770 end if
1771 pane_row = 2 + int(pane%y_start * real(screen_height))
1772 pane_height = int((pane%y_end - pane%y_start) * real(screen_height))
1773
1774 ! Account for pane header
1775 if (size(editor%tabs(tab_idx)%panes) > 1) then
1776 pane_row = pane_row + 1
1777 pane_height = pane_height - 1
1778 end if
1779
1780 ! Calculate cursor screen position
1781 screen_row = pane_row + (cursor%line - pane%viewport_line)
1782 screen_col = pane_col + col_offset + (cursor%column - pane%viewport_column)
1783
1784 ! Ensure cursor is within pane boundaries
1785 if (screen_row >= pane_row .and. screen_row < pane_row + pane_height .and. &
1786 screen_col >= pane_col + col_offset .and. screen_col < pane_col + pane_width) then
1787 call terminal_move_cursor(screen_row, screen_col)
1788 else
1789 ! Cursor out of view, position at top-left of pane
1790 call terminal_move_cursor(pane_row, pane_col + col_offset)
1791 end if
1792
1793 call terminal_show_cursor()
1794 end subroutine render_cursor_for_panes_with_tree
1795
1796 subroutine render_cursor_in_pane(editor, pane_start_col, pane_width)
1797 type(editor_state_t), intent(in) :: editor
1798 integer, intent(in) :: pane_start_col, pane_width
1799 type(cursor_t) :: cursor
1800 integer :: screen_row, screen_col, col_offset, row_offset, min_row
1801
1802 ! Calculate column offset for line numbers
1803 if (show_line_numbers) then
1804 col_offset = LINE_NUMBER_WIDTH + 1
1805 else
1806 col_offset = 0
1807 end if
1808
1809 ! Account for tab bar offset - when tabs exist, row 1 is tab bar, content starts at row 2
1810 if (size(editor%tabs) > 0) then
1811 row_offset = 2 ! Tab bar takes row 1
1812 min_row = 2 ! Cursor cannot be in row 1 (tab bar)
1813 else
1814 row_offset = 1 ! No tab bar
1815 min_row = 1 ! Cursor can be in row 1
1816 end if
1817
1818 cursor = editor%cursors(editor%active_cursor)
1819
1820 ! Calculate screen position within the editor pane
1821 screen_row = cursor%line - editor%viewport_line + row_offset
1822 screen_col = pane_start_col + col_offset + cursor%column - editor%viewport_column
1823
1824 ! Ensure cursor is within pane bounds and not in tab bar
1825 if (screen_row >= min_row .and. screen_row < editor%screen_rows .and. &
1826 screen_col >= pane_start_col .and. screen_col <= pane_start_col + pane_width) then
1827 call terminal_move_cursor(screen_row, screen_col)
1828 call terminal_show_cursor()
1829 end if
1830 end subroutine render_cursor_in_pane
1831
1832 ! Render tab bar at top of screen
1833 ! Optional start_col and width parameters for positioning in split view
1834 subroutine render_tab_bar(editor, start_col, width)
1835 type(editor_state_t), intent(in) :: editor
1836 integer, intent(in), optional :: start_col, width
1837 integer :: i, col, tab_count
1838 character(len=:), allocatable :: tab_label, filename_only
1839 character(len=256) :: temp_label
1840 integer :: slash_pos, last_slash
1841 character(len=1) :: modified_marker
1842 integer :: start_column, max_width
1843
1844 tab_count = size(editor%tabs)
1845 if (tab_count == 0) return ! No tabs to display
1846
1847 ! Use provided start_col and width, or default to full screen
1848 if (present(start_col)) then
1849 start_column = start_col
1850 else
1851 start_column = 1
1852 end if
1853
1854 if (present(width)) then
1855 max_width = width
1856 else
1857 max_width = editor%screen_cols
1858 end if
1859
1860 ! Move to top row at starting column and clear the tab bar area
1861 call terminal_move_cursor(1, start_column)
1862 call terminal_write(repeat(' ', max_width))
1863
1864 ! Render each tab
1865 col = start_column
1866 do i = 1, tab_count
1867 ! Extract filename from full path
1868 filename_only = editor%tabs(i)%filename
1869 last_slash = 0
1870 do slash_pos = len(editor%tabs(i)%filename), 1, -1
1871 if (editor%tabs(i)%filename(slash_pos:slash_pos) == '/') then
1872 last_slash = slash_pos
1873 exit
1874 end if
1875 end do
1876 if (last_slash > 0 .and. last_slash < len(editor%tabs(i)%filename)) then
1877 filename_only = editor%tabs(i)%filename(last_slash+1:)
1878 end if
1879
1880 ! Add modified marker
1881 if (editor%tabs(i)%modified) then
1882 modified_marker = '*'
1883 else
1884 modified_marker = ' '
1885 end if
1886
1887 ! Build tab label: [1: file.txt*]
1888 write(temp_label, '(A,I0,A,A,A,A)') '[', i, ': ', trim(filename_only), modified_marker, ']'
1889 tab_label = trim(temp_label)
1890
1891 ! Check if we have room for this tab
1892 if (col + len(tab_label) > start_column + max_width) exit
1893
1894 ! Position cursor
1895 call terminal_move_cursor(1, col)
1896
1897 ! Apply orphan tab styling (gray foreground)
1898 if (editor%tabs(i)%is_orphan) then
1899 call terminal_write(char(27) // '[90m') ! Bright black/gray
1900 end if
1901
1902 ! Highlight active tab
1903 if (i == editor%active_tab_index) then
1904 call terminal_write(char(27) // '[7m') ! Reverse video
1905 end if
1906
1907 call terminal_write(tab_label)
1908
1909 ! Reset if we applied any styling
1910 if (i == editor%active_tab_index .or. editor%tabs(i)%is_orphan) then
1911 call terminal_write(char(27) // '[0m') ! Reset
1912 end if
1913
1914 col = col + len(tab_label) + 1 ! +1 for space between tabs
1915 end do
1916 end subroutine render_tab_bar
1917
1918 ! UNUSED: Get diagnostic marker and color for a line
1919 ! Kept for potential future use
1920 ! subroutine get_diagnostic_marker(diagnostics, marker, color)
1921 ! type(diagnostic_t), intent(in) :: diagnostics(:)
1922 ! character(len=3), intent(out) :: marker ! UTF-8 characters can be up to 3 bytes
1923 ! character(len=:), allocatable, intent(out) :: color
1924 ! integer :: i, max_severity
1925 !
1926 ! marker = ' '
1927 ! color = ''
1928 !
1929 ! if (size(diagnostics) == 0) return
1930 !
1931 ! ! Find highest severity diagnostic
1932 ! max_severity = SEVERITY_HINT
1933 ! do i = 1, size(diagnostics)
1934 ! if (diagnostics(i)%severity < max_severity) then
1935 ! max_severity = diagnostics(i)%severity
1936 ! end if
1937 ! end do
1938 !
1939 ! ! Set marker and color based on severity
1940 ! select case(max_severity)
1941 ! case(SEVERITY_ERROR)
1942 ! marker = '●' ! Filled circle for errors
1943 ! color = char(27) // '[31m' ! Red
1944 ! case(SEVERITY_WARNING)
1945 ! marker = '▲' ! Triangle for warnings
1946 ! color = char(27) // '[33m' ! Yellow
1947 ! case(SEVERITY_INFO)
1948 ! marker = '◆' ! Diamond for info
1949 ! color = char(27) // '[36m' ! Cyan
1950 ! case(SEVERITY_HINT)
1951 ! marker = '○' ! Empty circle for hints
1952 ! color = char(27) // '[90m' ! Gray
1953 ! end select
1954 ! end subroutine get_diagnostic_marker
1955
1956 ! Render screen with LSP panel on the right (similar to render_screen_with_tree but for right side)
1957 subroutine render_screen_with_lsp_panel(buffer, editor, panel_type, match_mode_active, match_case_sens)
1958 use references_panel_module, only: references_panel_t, is_references_panel_visible
1959 use symbols_panel_module, only: symbols_panel_t, is_symbols_panel_visible
1960 use workspace_symbols_panel_module, only: workspace_symbols_panel_t, is_workspace_symbols_panel_visible
1961 type(buffer_t), intent(in) :: buffer
1962 type(editor_state_t), intent(inout) :: editor
1963 character(len=*), intent(in) :: panel_type ! "references", "symbols", or "workspace_symbols"
1964 logical, intent(in), optional :: match_mode_active
1965 logical, intent(in), optional :: match_case_sens
1966 integer :: panel_width, editor_end_col, editor_width
1967 integer :: separator_col, panel_start_col
1968 integer :: row
1969
1970 call terminal_hide_cursor()
1971
1972 ! Clear screen first to avoid artifacts
1973 do row = 1, editor%screen_rows
1974 call terminal_move_cursor(row, 1)
1975 call terminal_write(repeat(' ', editor%screen_cols))
1976 end do
1977
1978 ! Calculate split: 60% for editor, 40% for LSP panel
1979 panel_width = editor%screen_cols * 40 / 100
1980 editor_width = editor%screen_cols - panel_width - 1 ! -1 for separator
1981 editor_end_col = editor_width
1982 separator_col = editor_width + 1
1983 panel_start_col = separator_col + 1
1984
1985 ! Render tab bar if there are any tabs (positioned in editor pane area)
1986 call render_tab_bar(editor, 1, editor_width)
1987
1988 ! Render editor in left pane (check for multiple panes)
1989 call render_editor_area_for_lsp_panel(editor, 1, editor_width)
1990
1991 ! Render status bar (full width)
1992 call render_status_bar(editor, buffer, match_mode_active, match_case_sens)
1993
1994 ! Render vertical separator (start at row 2 for tab bar)
1995 call render_vertical_separator(separator_col, 2, editor%screen_rows - 1)
1996
1997 ! Render appropriate LSP panel on the right
1998 select case (panel_type)
1999 case ("references")
2000 if (is_references_panel_visible(editor%references_panel)) then
2001 call render_lsp_references_panel(editor%references_panel, panel_start_col, panel_width, &
2002 2, editor%screen_rows - 1)
2003 end if
2004 case ("symbols")
2005 if (is_symbols_panel_visible(editor%symbols_panel)) then
2006 call render_lsp_symbols_panel(editor%symbols_panel, editor%screen_rows - 1)
2007 end if
2008 case ("workspace_symbols")
2009 if (is_workspace_symbols_panel_visible(editor%workspace_symbols_panel)) then
2010 call render_lsp_workspace_symbols_panel(editor%workspace_symbols_panel, editor%screen_rows - 1)
2011 end if
2012 end select
2013
2014 ! Render cursor
2015 call render_cursor_for_lsp_panel(editor, 1, editor_width)
2016
2017 call terminal_show_cursor()
2018 end subroutine render_screen_with_lsp_panel
2019
2020 ! Helper to render editor area when LSP panel is on right
2021 subroutine render_editor_area_for_lsp_panel(editor, start_col, width)
2022 use editor_state_module, only: pane_t
2023 type(editor_state_t), intent(inout) :: editor
2024 integer, intent(in) :: start_col, width
2025 type(pane_t) :: pane
2026 integer :: i, tab_idx, n_panes
2027 integer :: pane_col, pane_row, pane_width, pane_height
2028 integer :: screen_height
2029
2030 ! Get active tab
2031 tab_idx = editor%active_tab_index
2032 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) then
2033 return
2034 end if
2035
2036 if (.not. allocated(editor%tabs(tab_idx)%panes)) then
2037 return
2038 end if
2039
2040 n_panes = size(editor%tabs(tab_idx)%panes)
2041 if (n_panes == 0) return
2042
2043 screen_height = editor%screen_rows - 2 ! Account for tab bar and status bar
2044
2045 ! If only one pane, use simple rendering
2046 if (n_panes == 1) then
2047 call render_editor_pane(editor%tabs(tab_idx)%panes(1)%buffer, editor, start_col, width)
2048 return
2049 end if
2050
2051 ! Multiple panes: render each with adjusted coordinates
2052 do i = 2, editor%screen_rows - 1
2053 call terminal_move_cursor(i, start_col)
2054 call terminal_write(repeat(' ', width))
2055 end do
2056
2057 do i = 1, n_panes
2058 pane = editor%tabs(tab_idx)%panes(i)
2059
2060 pane_col = start_col + int(pane%x_start * real(width))
2061 if (i < n_panes) then
2062 pane_width = int((pane%x_end - pane%x_start) * real(width)) - 1
2063 else
2064 pane_width = int((pane%x_end - pane%x_start) * real(width))
2065 end if
2066 pane_row = 2 + int(pane%y_start * real(screen_height))
2067 pane_height = int((pane%y_end - pane%y_start) * real(screen_height))
2068
2069 editor%tabs(tab_idx)%panes(i)%screen_col = pane_col
2070 editor%tabs(tab_idx)%panes(i)%screen_row = pane_row
2071 editor%tabs(tab_idx)%panes(i)%screen_width = pane_width
2072 editor%tabs(tab_idx)%panes(i)%screen_height = pane_height
2073
2074 call render_single_pane(editor, i, pane_col, pane_row, pane_width, pane_height)
2075
2076 if (i < n_panes) then
2077 call render_pane_separator(pane_col + pane_width, pane_row, pane_height)
2078 end if
2079 end do
2080 end subroutine render_editor_area_for_lsp_panel
2081
2082 ! Helper to render cursor when LSP panel is visible
2083 subroutine render_cursor_for_lsp_panel(editor, start_col, width)
2084 type(editor_state_t), intent(inout) :: editor
2085 integer, intent(in) :: start_col, width
2086 integer :: tab_idx, n_panes
2087
2088 tab_idx = editor%active_tab_index
2089 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
2090 if (.not. allocated(editor%tabs(tab_idx)%panes)) return
2091
2092 n_panes = size(editor%tabs(tab_idx)%panes)
2093 if (n_panes == 0) return
2094
2095 if (n_panes > 1) then
2096 call render_cursor_for_panes_in_lsp_view(editor)
2097 else
2098 call render_cursor_in_pane(editor, start_col, width)
2099 end if
2100 end subroutine render_cursor_for_lsp_panel
2101
2102 ! Helper to render cursor for multiple panes when LSP panel is visible
2103 subroutine render_cursor_for_panes_in_lsp_view(editor)
2104 use editor_state_module, only: pane_t
2105 type(editor_state_t), intent(inout) :: editor
2106 integer :: tab_idx, active_pane
2107 type(pane_t) :: pane
2108 integer :: screen_row, screen_col
2109
2110 tab_idx = editor%active_tab_index
2111 if (tab_idx < 1 .or. tab_idx > size(editor%tabs)) return
2112
2113 active_pane = editor%tabs(tab_idx)%active_pane_index
2114 if (active_pane < 1 .or. active_pane > size(editor%tabs(tab_idx)%panes)) return
2115
2116 pane = editor%tabs(tab_idx)%panes(active_pane)
2117
2118 ! Calculate cursor position relative to pane
2119 screen_row = pane%screen_row + (editor%cursors(editor%active_cursor)%line - pane%viewport_line)
2120 screen_col = pane%screen_col + (editor%cursors(editor%active_cursor)%column - 1)
2121
2122 if (screen_row >= pane%screen_row .and. &
2123 screen_row < pane%screen_row + pane%screen_height .and. &
2124 screen_col >= pane%screen_col .and. &
2125 screen_col < pane%screen_col + pane%screen_width) then
2126 call terminal_move_cursor(screen_row, screen_col)
2127 end if
2128 end subroutine render_cursor_for_panes_in_lsp_view
2129
2130 ! Render references panel in offcanvas mode (right side, full height)
2131 subroutine render_lsp_references_panel(panel, start_col, width, start_row, end_row)
2132 use references_panel_module, only: references_panel_t
2133 type(references_panel_t), intent(in) :: panel
2134 integer, intent(in) :: start_col, width, start_row, end_row
2135 integer :: row, i, visible_index, max_visible
2136 character(len=256) :: line
2137 character(len=100) :: header, location_str
2138 character(len=:), allocatable :: display_text, filename_display
2139 character(len=1), parameter :: ESC = achar(27)
2140
2141 ! Clear panel area
2142 do row = start_row, end_row
2143 call terminal_move_cursor(row, start_col)
2144 call terminal_write(repeat(' ', width))
2145 end do
2146
2147 row = start_row
2148
2149 ! Header with symbol name
2150 call terminal_move_cursor(row, start_col)
2151 call terminal_write(ESC // '[48;5;237m') ! Dark background
2152
2153 if (allocated(panel%symbol_name)) then
2154 write(header, '(A,A,A,I0,A)') " References: ", trim(panel%symbol_name), &
2155 " (", panel%num_references, ") "
2156 else
2157 write(header, '(A,I0,A)') " References (", panel%num_references, ") "
2158 end if
2159
2160 ! Truncate header if too long
2161 if (len_trim(header) > width) then
2162 header = header(1:width-3) // "..."
2163 end if
2164
2165 call terminal_write(ESC // '[1m' // trim(header))
2166 ! Pad rest of header line
2167 if (len_trim(header) < width) then
2168 call terminal_write(repeat(' ', width - len_trim(header)))
2169 end if
2170 call terminal_write(ESC // '[0m')
2171 row = row + 1
2172
2173 ! Separator
2174 call terminal_move_cursor(row, start_col)
2175 call terminal_write(ESC // '[48;5;237m' // repeat("─", width) // ESC // '[0m')
2176 row = row + 1
2177
2178 ! Legend
2179 call terminal_move_cursor(row, start_col)
2180 call terminal_write(ESC // '[90mj/k:nav enter:jump esc:close' // ESC // '[0m')
2181 row = row + 1
2182
2183 ! Separator
2184 call terminal_move_cursor(row, start_col)
2185 call terminal_write(ESC // '[90m' // repeat("─", width) // ESC // '[0m')
2186 row = row + 1
2187
2188 ! Calculate max visible items
2189 max_visible = end_row - row + 1
2190
2191 ! Display references
2192 if (panel%num_references == 0) then
2193 call terminal_move_cursor(row, start_col)
2194 call terminal_write(ESC // '[48;5;235m' // ESC // '[90m')
2195 call terminal_write(" No references found")
2196 if (20 < width) then
2197 call terminal_write(repeat(' ', width - 20))
2198 end if
2199 call terminal_write(ESC // '[0m')
2200 else
2201 do i = 1, min(max_visible, panel%num_references - panel%scroll_offset)
2202 visible_index = panel%scroll_offset + i
2203 if (visible_index > panel%num_references) exit
2204
2205 call terminal_move_cursor(row, start_col)
2206
2207 ! Highlight selected item
2208 if (visible_index == panel%selected_index) then
2209 call terminal_write(ESC // '[48;5;240m') ! Highlight background
2210 else
2211 call terminal_write(ESC // '[48;5;235m') ! Normal background
2212 end if
2213
2214 ! Format location string
2215 if (allocated(panel%references(visible_index)%filename)) then
2216 ! Extract just the filename from full path
2217 filename_display = get_basename_str(panel%references(visible_index)%filename)
2218 write(location_str, '(A,A,I0,A,I0)') &
2219 trim(filename_display), &
2220 ":", panel%references(visible_index)%line, &
2221 ":", panel%references(visible_index)%column
2222 else
2223 write(location_str, '(I0,A,I0)') &
2224 panel%references(visible_index)%line, &
2225 ":", panel%references(visible_index)%column
2226 end if
2227
2228 ! Build line with location and preview
2229 line = " " // trim(adjustl(location_str))
2230
2231 ! Add preview text if available
2232 if (allocated(panel%references(visible_index)%preview_text)) then
2233 display_text = trim(panel%references(visible_index)%preview_text)
2234 if (len(line) + len(display_text) + 2 < width) then
2235 line = trim(line) // " " // display_text
2236 else if (len(line) + 5 < width) then
2237 line = trim(line) // " " // display_text(1:width-len(line)-4) // "..."
2238 end if
2239 end if
2240
2241 ! Write line and pad to width
2242 call terminal_write(trim(line))
2243 if (len_trim(line) < width) then
2244 call terminal_write(repeat(' ', width - len_trim(line)))
2245 end if
2246 call terminal_write(ESC // '[0m')
2247
2248 row = row + 1
2249 if (row > end_row) exit
2250 end do
2251 end if
2252 end subroutine render_lsp_references_panel
2253
2254 ! Render symbols panel in offcanvas mode (right side, full height)
2255 subroutine render_lsp_symbols_panel(panel, screen_height)
2256 use symbols_panel_module, only: symbols_panel_t, render_symbols_panel
2257 type(symbols_panel_t), intent(in) :: panel
2258 integer, intent(in) :: screen_height
2259
2260 ! Delegate to the real symbols panel renderer
2261 ! The panel manages its own positioning via panel_start_col and panel_width
2262 call render_symbols_panel(panel, screen_height)
2263 end subroutine render_lsp_symbols_panel
2264
2265 ! Render workspace symbols panel in offcanvas mode (right side, full height)
2266 subroutine render_lsp_workspace_symbols_panel(panel, screen_height)
2267 use workspace_symbols_panel_module, only: workspace_symbols_panel_t, render_workspace_symbols_panel
2268 type(workspace_symbols_panel_t), intent(in) :: panel
2269 integer, intent(in) :: screen_height
2270
2271 ! Delegate to the real workspace symbols panel renderer
2272 ! The panel manages its own positioning via panel_start_col and panel_width
2273 call render_workspace_symbols_panel(panel, screen_height)
2274 end subroutine render_lsp_workspace_symbols_panel
2275
2276 ! Helper function to extract basename from path
2277 function get_basename_str(path) result(basename)
2278 character(len=*), intent(in) :: path
2279 character(len=:), allocatable :: basename
2280 integer :: i, last_slash
2281
2282 last_slash = 0
2283 do i = len(path), 1, -1
2284 if (path(i:i) == '/' .or. path(i:i) == '\') then
2285 last_slash = i
2286 exit
2287 end if
2288 end do
2289
2290 if (last_slash > 0 .and. last_slash < len(path)) then
2291 basename = path(last_slash+1:len(path))
2292 else
2293 basename = path
2294 end if
2295 end function get_basename_str
2296
2297 ! Get current time in milliseconds (for fuzzy search timeout)
2298 function get_time_ms() result(ms)
2299 integer(int64) :: ms
2300 integer(int64) :: count, count_rate
2301
2302 call system_clock(count, count_rate)
2303 if (count_rate > 0) then
2304 ms = (count * 1000_int64) / count_rate
2305 else
2306 ms = 0
2307 end if
2308 end function get_time_ms
2309
2310 ! Reset fuzzy search buffer
2311 subroutine fuss_reset_search()
2312 fuss_search_buffer = ''
2313 fuss_search_len = 0
2314 fuss_search_last_time = 0
2315 end subroutine fuss_reset_search
2316
2317 ! Fuzzy jump to matching entry in fuss mode
2318 ! Returns true if a match was found and jumped to
2319 function fuss_fuzzy_jump(search_str) result(found)
2320 character(len=*), intent(in) :: search_str
2321 logical :: found
2322 integer :: i, start_idx, search_len
2323 character(len=256) :: item_name, search_lower, name_lower
2324
2325 found = .false.
2326 search_len = len_trim(search_str)
2327 if (search_len == 0) return
2328 if (tree_state%n_selectable == 0) return
2329
2330 ! Convert search string to lowercase for case-insensitive matching
2331 search_lower = to_lower(trim(search_str))
2332
2333 ! Start searching from current position + 1, wrap around
2334 start_idx = tree_state%selected_index
2335 do i = 1, tree_state%n_selectable
2336 ! Wrap around index
2337 start_idx = start_idx + 1
2338 if (start_idx > tree_state%n_selectable) start_idx = 1
2339
2340 ! Get item name (extract basename from path for files)
2341 if (associated(tree_state%selectable_files(start_idx)%node)) then
2342 item_name = trim(tree_state%selectable_files(start_idx)%node%name)
2343 else
2344 item_name = trim(tree_state%selectable_files(start_idx)%path)
2345 end if
2346
2347 ! Convert to lowercase for comparison
2348 name_lower = to_lower(trim(item_name))
2349
2350 ! Check if name starts with search string (prefix match)
2351 if (len_trim(name_lower) >= search_len) then
2352 if (name_lower(1:search_len) == search_lower(1:search_len)) then
2353 tree_state%selected_index = start_idx
2354 found = .true.
2355 return
2356 end if
2357 end if
2358 end do
2359 end function fuss_fuzzy_jump
2360
2361 ! Convert string to lowercase
2362 function to_lower(str) result(lower_str)
2363 character(len=*), intent(in) :: str
2364 character(len=256) :: lower_str
2365 integer :: i, ic
2366
2367 lower_str = str
2368 do i = 1, len_trim(str)
2369 ic = ichar(str(i:i))
2370 if (ic >= ichar('A') .and. ic <= ichar('Z')) then
2371 lower_str(i:i) = char(ic + 32)
2372 end if
2373 end do
2374 end function to_lower
2375
2376 end module renderer_module