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