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