fortrangoingonforty/facsimile / d12ed9f

Browse files

Fix UTF-8 rendering in editor

Authored by espadonne
SHA
d12ed9fd1b1149e1fd97ef90d3685a290388137b
Parents
42f9bdb
Tree
3eeb67a

1 changed file

StatusFile+-
M src/terminal/renderer_module.f90 111 79
src/terminal/renderer_module.f90modified
@@ -316,45 +316,60 @@ contains
316
         type(buffer_t), intent(in) :: buffer
316
         type(buffer_t), intent(in) :: buffer
317
         type(editor_state_t), intent(in) :: editor
317
         type(editor_state_t), intent(in) :: editor
318
         integer, intent(in) :: line_num, start_col, width
318
         integer, intent(in) :: line_num, start_col, width
319
-        character(len=:), allocatable :: line
319
+        character(len=:), allocatable :: line, utf8_ch
320
-        integer :: i, col, line_len, token_idx
320
+        integer :: i, char_idx, byte_pos, token_idx, char_count, display_col, char_width
321
         integer :: sel_start_line, sel_start_col, sel_end_line, sel_end_col
321
         integer :: sel_start_line, sel_start_col, sel_end_line, sel_end_col
322
         logical :: in_selection, is_bracket_match, is_current_line, is_search_match
322
         logical :: in_selection, is_bracket_match, is_current_line, is_search_match
323
-        character :: ch
324
         type(token_t), allocatable :: tokens(:)
323
         type(token_t), allocatable :: tokens(:)
325
         character(len=:), allocatable :: token_color
324
         character(len=:), allocatable :: token_color
326
         integer :: search_matches(2, 50)  ! Up to 50 matches per line (start, end pairs)
325
         integer :: search_matches(2, 50)  ! Up to 50 matches per line (start, end pairs)
327
         integer :: num_search_matches, match_idx
326
         integer :: num_search_matches, match_idx
327
+        integer :: line_byte_len
328
 
328
 
329
         line = buffer_get_line(buffer, line_num)
329
         line = buffer_get_line(buffer, line_num)
330
-        line_len = len(line)
330
+        line_byte_len = len(line)
331
+        char_count = utf8_char_count(line)
331
 
332
 
332
-        ! Get all search matches on this line
333
+        ! Get all search matches on this line (these use byte indices)
333
         if (search_mode_active) then
334
         if (search_mode_active) then
334
             call get_matches_on_line(line, line_num, search_matches, num_search_matches)
335
             call get_matches_on_line(line, line_num, search_matches, num_search_matches)
335
         else
336
         else
336
             num_search_matches = 0
337
             num_search_matches = 0
337
         end if
338
         end if
338
 
339
 
339
-        ! Get syntax tokens for this line
340
+        ! Get syntax tokens for this line (tokens use byte indices)
340
         if (syntax_highlighter%enabled) then
341
         if (syntax_highlighter%enabled) then
341
             call tokenize_line(syntax_highlighter, line, tokens)
342
             call tokenize_line(syntax_highlighter, line, tokens)
342
         else
343
         else
343
             allocate(tokens(1))
344
             allocate(tokens(1))
344
             tokens(1)%type = TOKEN_PLAIN
345
             tokens(1)%type = TOKEN_PLAIN
345
             tokens(1)%start_col = 1
346
             tokens(1)%start_col = 1
346
-            tokens(1)%end_col = max(1, line_len)
347
+            tokens(1)%end_col = max(1, line_byte_len)
347
         end if
348
         end if
348
 
349
 
349
         ! Check if this is the current line
350
         ! Check if this is the current line
350
         is_current_line = (line_num == editor%cursors(editor%active_cursor)%line) .and. highlight_current_line
351
         is_current_line = (line_num == editor%cursors(editor%active_cursor)%line) .and. highlight_current_line
351
 
352
 
352
-        ! Render each character with selection highlighting
353
+        ! Render each UTF-8 character with selection highlighting
353
-        do col = start_col, min(start_col + width - 1, line_len + 1)
354
+        ! char_idx = 1-based character index (for selection logic)
355
+        ! display_col = screen column position (for width tracking)
356
+        ! byte_pos = byte position in string (for token lookup)
357
+        display_col = 0
358
+        char_idx = start_col
359
+
360
+        do while (char_idx <= char_count .and. display_col < width)
354
             in_selection = .false.
361
             in_selection = .false.
355
             is_bracket_match = .false.
362
             is_bracket_match = .false.
356
 
363
 
364
+            ! Get the UTF-8 character at this position
365
+            utf8_ch = utf8_char_at(line, char_idx)
366
+            char_width = utf8_display_width(utf8_ch)
367
+
368
+            ! Get byte position for token lookup
369
+            byte_pos = utf8_char_to_byte_index(line, char_idx)
370
+
357
             ! Check if this position is in any cursor's selection
371
             ! Check if this position is in any cursor's selection
372
+            ! (cursor positions are character indices, not byte indices)
358
             do i = 1, size(editor%cursors)
373
             do i = 1, size(editor%cursors)
359
                 if (editor%cursors(i)%has_selection) then
374
                 if (editor%cursors(i)%has_selection) then
360
                     ! Determine selection bounds (handle both directions)
375
                     ! Determine selection bounds (handle both directions)
@@ -374,26 +389,26 @@ contains
374
                         sel_end_col = editor%cursors(i)%column
389
                         sel_end_col = editor%cursors(i)%column
375
                     end if
390
                     end if
376
 
391
 
377
-                    ! Check if this position is selected
392
+                    ! Check if this position is selected (using char_idx)
378
                     if (line_num > sel_start_line .and. line_num < sel_end_line) then
393
                     if (line_num > sel_start_line .and. line_num < sel_end_line) then
379
                         ! Fully selected line (between start and end)
394
                         ! Fully selected line (between start and end)
380
                         in_selection = .true.
395
                         in_selection = .true.
381
                         exit
396
                         exit
382
                     else if (line_num == sel_start_line .and. line_num == sel_end_line) then
397
                     else if (line_num == sel_start_line .and. line_num == sel_end_line) then
383
                         ! Single-line selection
398
                         ! Single-line selection
384
-                        if (col >= sel_start_col .and. col < sel_end_col) then
399
+                        if (char_idx >= sel_start_col .and. char_idx < sel_end_col) then
385
                             in_selection = .true.
400
                             in_selection = .true.
386
                             exit
401
                             exit
387
                         end if
402
                         end if
388
                     else if (line_num == sel_start_line .and. line_num < sel_end_line) then
403
                     else if (line_num == sel_start_line .and. line_num < sel_end_line) then
389
                         ! First line of multi-line selection
404
                         ! First line of multi-line selection
390
-                        if (col >= sel_start_col) then
405
+                        if (char_idx >= sel_start_col) then
391
                             in_selection = .true.
406
                             in_selection = .true.
392
                             exit
407
                             exit
393
                         end if
408
                         end if
394
                     else if (line_num == sel_end_line .and. line_num > sel_start_line) then
409
                     else if (line_num == sel_end_line .and. line_num > sel_start_line) then
395
                         ! Last line of multi-line selection
410
                         ! Last line of multi-line selection
396
-                        if (col < sel_end_col) then
411
+                        if (char_idx < sel_end_col) then
397
                             in_selection = .true.
412
                             in_selection = .true.
398
                             exit
413
                             exit
399
                         end if
414
                         end if
@@ -401,26 +416,28 @@ contains
401
                 end if
416
                 end if
402
             end do
417
             end do
403
 
418
 
404
-            ! Check if this position is a bracket or its match
419
+            ! Check if this position is a bracket or its match (using char_idx)
405
-            if ((line_num == bracket_line .and. col == bracket_col) .or. &
420
+            if ((line_num == bracket_line .and. char_idx == bracket_col) .or. &
406
-                (line_num == matching_bracket_line .and. col == matching_bracket_col)) then
421
+                (line_num == matching_bracket_line .and. char_idx == matching_bracket_col)) then
407
                 is_bracket_match = .true.
422
                 is_bracket_match = .true.
408
             end if
423
             end if
409
 
424
 
410
-            ! Check if this position is part of a search match
425
+            ! Check if this position is part of a search match (search uses byte indices)
411
             is_search_match = .false.
426
             is_search_match = .false.
412
-            do match_idx = 1, num_search_matches
427
+            if (byte_pos > 0) then
413
-                if (col >= search_matches(1, match_idx) .and. col <= search_matches(2, match_idx)) then
428
+                do match_idx = 1, num_search_matches
414
-                    is_search_match = .true.
429
+                    if (byte_pos >= search_matches(1, match_idx) .and. byte_pos <= search_matches(2, match_idx)) then
415
-                    exit
430
+                        is_search_match = .true.
416
-                end if
431
+                        exit
417
-            end do
432
+                    end if
433
+                end do
434
+            end if
418
 
435
 
419
-            ! Find which token this column belongs to
436
+            ! Find which token this column belongs to (tokens use byte indices)
420
             token_color = ""
437
             token_color = ""
421
-            if (syntax_highlighter%enabled) then
438
+            if (syntax_highlighter%enabled .and. byte_pos > 0) then
422
                 do token_idx = 1, size(tokens)
439
                 do token_idx = 1, size(tokens)
423
-                    if (col >= tokens(token_idx)%start_col .and. col <= tokens(token_idx)%end_col) then
440
+                    if (byte_pos >= tokens(token_idx)%start_col .and. byte_pos <= tokens(token_idx)%end_col) then
424
                         token_color = get_token_color(tokens(token_idx)%type)
441
                         token_color = get_token_color(tokens(token_idx)%type)
425
                         exit
442
                         exit
426
                     end if
443
                     end if
@@ -428,42 +445,39 @@ contains
428
             end if
445
             end if
429
 
446
 
430
             ! Render character with or without highlighting
447
             ! Render character with or without highlighting
431
-            if (col <= line_len) then
432
-                ch = line(col:col)
433
-            else
434
-                ch = ' '
435
-            end if
436
-
437
             if (in_selection) then
448
             if (in_selection) then
438
                 ! Highlight selected text with reverse video (highest priority)
449
                 ! Highlight selected text with reverse video (highest priority)
439
-                call terminal_write(char(27) // '[7m' // ch // char(27) // '[0m')
450
+                call terminal_write(char(27) // '[7m' // utf8_ch // char(27) // '[0m')
440
             else if (is_bracket_match) then
451
             else if (is_bracket_match) then
441
                 ! Highlight matching brackets with cyan background
452
                 ! Highlight matching brackets with cyan background
442
-                call terminal_write(char(27) // '[46m' // ch // char(27) // '[0m')
453
+                call terminal_write(char(27) // '[46m' // utf8_ch // char(27) // '[0m')
443
             else if (is_search_match) then
454
             else if (is_search_match) then
444
                 ! Highlight search matches with yellow background
455
                 ! Highlight search matches with yellow background
445
                 if (len(token_color) > 0) then
456
                 if (len(token_color) > 0) then
446
-                    call terminal_write(token_color // char(27) // '[43m' // ch // char(27) // '[0m')
457
+                    call terminal_write(token_color // char(27) // '[43m' // utf8_ch // char(27) // '[0m')
447
                 else
458
                 else
448
-                    call terminal_write(char(27) // '[43m' // ch // char(27) // '[0m')
459
+                    call terminal_write(char(27) // '[43m' // utf8_ch // char(27) // '[0m')
449
                 end if
460
                 end if
450
             else if (is_current_line) then
461
             else if (is_current_line) then
451
                 ! Subtle background for current line with syntax color
462
                 ! Subtle background for current line with syntax color
452
                 if (len(token_color) > 0) then
463
                 if (len(token_color) > 0) then
453
-                    call terminal_write(token_color // char(27) // '[48;5;236m' // ch // char(27) // '[0m')
464
+                    call terminal_write(token_color // char(27) // '[48;5;236m' // utf8_ch // char(27) // '[0m')
454
                 else
465
                 else
455
-                    call terminal_write(char(27) // '[48;5;236m' // ch // char(27) // '[0m')
466
+                    call terminal_write(char(27) // '[48;5;236m' // utf8_ch // char(27) // '[0m')
456
                 end if
467
                 end if
457
             else if (len(token_color) > 0) then
468
             else if (len(token_color) > 0) then
458
                 ! Apply syntax highlighting
469
                 ! Apply syntax highlighting
459
-                call terminal_write(token_color // ch // char(27) // '[0m')
470
+                call terminal_write(token_color // utf8_ch // char(27) // '[0m')
460
             else
471
             else
461
-                call terminal_write(ch)
472
+                call terminal_write(utf8_ch)
462
             end if
473
             end if
474
+
475
+            display_col = display_col + char_width
476
+            char_idx = char_idx + 1
463
         end do
477
         end do
464
 
478
 
465
         ! Fill remaining width with spaces
479
         ! Fill remaining width with spaces
466
-        do col = max(line_len + 1, start_col), start_col + width - 1
480
+        do while (display_col < width)
467
             in_selection = .false.
481
             in_selection = .false.
468
 
482
 
469
             ! Check if end of line position is in selection
483
             ! Check if end of line position is in selection
@@ -485,25 +499,26 @@ contains
485
                     end if
499
                     end if
486
 
500
 
487
                     ! Check if this position is selected (multi-line aware)
501
                     ! Check if this position is selected (multi-line aware)
502
+                    ! Use char_idx which is now past end of line content
488
                     if (line_num > sel_start_line .and. line_num < sel_end_line) then
503
                     if (line_num > sel_start_line .and. line_num < sel_end_line) then
489
                         ! Fully selected line
504
                         ! Fully selected line
490
                         in_selection = .true.
505
                         in_selection = .true.
491
                         exit
506
                         exit
492
                     else if (line_num == sel_start_line .and. line_num == sel_end_line) then
507
                     else if (line_num == sel_start_line .and. line_num == sel_end_line) then
493
                         ! Single-line selection
508
                         ! Single-line selection
494
-                        if (col >= sel_start_col .and. col < sel_end_col) then
509
+                        if (char_idx >= sel_start_col .and. char_idx < sel_end_col) then
495
                             in_selection = .true.
510
                             in_selection = .true.
496
                             exit
511
                             exit
497
                         end if
512
                         end if
498
                     else if (line_num == sel_start_line .and. line_num < sel_end_line) then
513
                     else if (line_num == sel_start_line .and. line_num < sel_end_line) then
499
                         ! First line of multi-line selection
514
                         ! First line of multi-line selection
500
-                        if (col >= sel_start_col) then
515
+                        if (char_idx >= sel_start_col) then
501
                             in_selection = .true.
516
                             in_selection = .true.
502
                             exit
517
                             exit
503
                         end if
518
                         end if
504
                     else if (line_num == sel_end_line .and. line_num > sel_start_line) then
519
                     else if (line_num == sel_end_line .and. line_num > sel_start_line) then
505
                         ! Last line of multi-line selection
520
                         ! Last line of multi-line selection
506
-                        if (col < sel_end_col) then
521
+                        if (char_idx < sel_end_col) then
507
                             in_selection = .true.
522
                             in_selection = .true.
508
                             exit
523
                             exit
509
                         end if
524
                         end if
@@ -518,9 +533,12 @@ contains
518
             else
533
             else
519
                 call terminal_write(' ')
534
                 call terminal_write(' ')
520
             end if
535
             end if
536
+            display_col = display_col + 1
537
+            char_idx = char_idx + 1
521
         end do
538
         end do
522
 
539
 
523
         if (allocated(line)) deallocate(line)
540
         if (allocated(line)) deallocate(line)
541
+        if (allocated(utf8_ch)) deallocate(utf8_ch)
524
     end subroutine render_line_with_selections
542
     end subroutine render_line_with_selections
525
 
543
 
526
     subroutine render_status_bar(editor, buffer, match_mode_active, match_case_sens)
544
     subroutine render_status_bar(editor, buffer, match_mode_active, match_case_sens)
@@ -1319,14 +1337,13 @@ contains
1319
         type(buffer_t), intent(in) :: buffer
1337
         type(buffer_t), intent(in) :: buffer
1320
         type(editor_state_t), intent(in) :: editor
1338
         type(editor_state_t), intent(in) :: editor
1321
         integer, intent(in) :: pane_idx, line_num, screen_row, col, width
1339
         integer, intent(in) :: pane_idx, line_num, screen_row, col, width
1322
-        character(len=:), allocatable :: line
1340
+        character(len=:), allocatable :: line, utf8_ch
1323
         type(pane_t) :: pane
1341
         type(pane_t) :: pane
1324
-        integer :: tab_idx, start_col, end_col, i, char_col
1342
+        integer :: tab_idx, i, char_idx, char_count, display_col, char_width
1325
         integer :: content_width, content_col
1343
         integer :: content_width, content_col
1326
         character(len=5) :: line_num_str
1344
         character(len=5) :: line_num_str
1327
         logical :: is_current_line, in_selection, is_bracket_match
1345
         logical :: is_current_line, in_selection, is_bracket_match
1328
         integer :: sel_start_line, sel_start_col, sel_end_line, sel_end_col
1346
         integer :: sel_start_line, sel_start_col, sel_end_line, sel_end_col
1329
-        character(len=1) :: ch
1330
 
1347
 
1331
         tab_idx = editor%active_tab_index
1348
         tab_idx = editor%active_tab_index
1332
         pane = editor%tabs(tab_idx)%panes(pane_idx)
1349
         pane = editor%tabs(tab_idx)%panes(pane_idx)
@@ -1377,9 +1394,8 @@ contains
1377
         line = buffer_get_line(buffer, line_num)
1394
         line = buffer_get_line(buffer, line_num)
1378
         if (.not. allocated(line)) return
1395
         if (.not. allocated(line)) return
1379
 
1396
 
1380
-        ! Calculate visible portion based on horizontal scroll
1397
+        ! Get character count for UTF-8 iteration
1381
-        start_col = pane%viewport_column
1398
+        char_count = utf8_char_count(line)
1382
-        end_col = min(start_col + content_width - 1, len(line))
1383
 
1399
 
1384
         ! Check if this is the current line with a cursor
1400
         ! Check if this is the current line with a cursor
1385
         is_current_line = .false.
1401
         is_current_line = .false.
@@ -1393,9 +1409,17 @@ contains
1393
         end if
1409
         end if
1394
 
1410
 
1395
         ! Render the line character by character with selection highlighting
1411
         ! Render the line character by character with selection highlighting
1396
-        do char_col = start_col, start_col + content_width - 1
1412
+        ! Using UTF-8 aware iteration
1413
+        display_col = 0
1414
+        char_idx = pane%viewport_column  ! Start from viewport column (character index)
1415
+
1416
+        do while (char_idx <= char_count .and. display_col < content_width)
1397
             in_selection = .false.
1417
             in_selection = .false.
1398
 
1418
 
1419
+            ! Get the UTF-8 character at this position
1420
+            utf8_ch = utf8_char_at(line, char_idx)
1421
+            char_width = utf8_display_width(utf8_ch)
1422
+
1399
             ! Check if this position is in any cursor's selection (use pane's cursors)
1423
             ! Check if this position is in any cursor's selection (use pane's cursors)
1400
             if (allocated(pane%cursors)) then
1424
             if (allocated(pane%cursors)) then
1401
                 do i = 1, size(pane%cursors)
1425
                 do i = 1, size(pane%cursors)
@@ -1417,26 +1441,26 @@ contains
1417
                             sel_end_col = pane%cursors(i)%column
1441
                             sel_end_col = pane%cursors(i)%column
1418
                         end if
1442
                         end if
1419
 
1443
 
1420
-                        ! Check if this position is selected
1444
+                        ! Check if this position is selected (using char_idx)
1421
                         if (line_num > sel_start_line .and. line_num < sel_end_line) then
1445
                         if (line_num > sel_start_line .and. line_num < sel_end_line) then
1422
                             ! Fully selected line (between start and end)
1446
                             ! Fully selected line (between start and end)
1423
                             in_selection = .true.
1447
                             in_selection = .true.
1424
                             exit
1448
                             exit
1425
                         else if (line_num == sel_start_line .and. line_num == sel_end_line) then
1449
                         else if (line_num == sel_start_line .and. line_num == sel_end_line) then
1426
                             ! Single-line selection
1450
                             ! Single-line selection
1427
-                            if (char_col >= sel_start_col .and. char_col < sel_end_col) then
1451
+                            if (char_idx >= sel_start_col .and. char_idx < sel_end_col) then
1428
                                 in_selection = .true.
1452
                                 in_selection = .true.
1429
                                 exit
1453
                                 exit
1430
                             end if
1454
                             end if
1431
                         else if (line_num == sel_start_line .and. line_num < sel_end_line) then
1455
                         else if (line_num == sel_start_line .and. line_num < sel_end_line) then
1432
                             ! First line of multi-line selection
1456
                             ! First line of multi-line selection
1433
-                            if (char_col >= sel_start_col) then
1457
+                            if (char_idx >= sel_start_col) then
1434
                                 in_selection = .true.
1458
                                 in_selection = .true.
1435
                                 exit
1459
                                 exit
1436
                             end if
1460
                             end if
1437
                         else if (line_num == sel_end_line .and. line_num > sel_start_line) then
1461
                         else if (line_num == sel_end_line .and. line_num > sel_start_line) then
1438
                             ! Last line of multi-line selection
1462
                             ! Last line of multi-line selection
1439
-                            if (char_col < sel_end_col) then
1463
+                            if (char_idx < sel_end_col) then
1440
                                 in_selection = .true.
1464
                                 in_selection = .true.
1441
                                 exit
1465
                                 exit
1442
                             end if
1466
                             end if
@@ -1448,40 +1472,50 @@ contains
1448
             ! Check if this position is a bracket or its match (only for active pane)
1472
             ! Check if this position is a bracket or its match (only for active pane)
1449
             is_bracket_match = .false.
1473
             is_bracket_match = .false.
1450
             if (pane%is_active) then
1474
             if (pane%is_active) then
1451
-                if ((line_num == bracket_line .and. char_col == bracket_col) .or. &
1475
+                if ((line_num == bracket_line .and. char_idx == bracket_col) .or. &
1452
-                    (line_num == matching_bracket_line .and. char_col == matching_bracket_col)) then
1476
+                    (line_num == matching_bracket_line .and. char_idx == matching_bracket_col)) then
1453
                     is_bracket_match = .true.
1477
                     is_bracket_match = .true.
1454
                 end if
1478
                 end if
1455
             end if
1479
             end if
1456
 
1480
 
1457
-            ! Get the character at this position
1458
-            if (char_col <= len(line)) then
1459
-                ch = line(char_col:char_col)
1460
-            else
1461
-                ch = ' '
1462
-            end if
1463
-
1464
             ! Render the character with appropriate highlighting
1481
             ! Render the character with appropriate highlighting
1465
             if (in_selection) then
1482
             if (in_selection) then
1466
                 ! Highlight selected text with reverse video
1483
                 ! Highlight selected text with reverse video
1467
-                call terminal_write(char(27) // '[7m' // ch // char(27) // '[0m')
1484
+                call terminal_write(char(27) // '[7m' // utf8_ch // char(27) // '[0m')
1468
             else if (is_bracket_match) then
1485
             else if (is_bracket_match) then
1469
                 ! Highlight matching brackets with cyan background
1486
                 ! Highlight matching brackets with cyan background
1470
-                call terminal_write(char(27) // '[46m' // ch // char(27) // '[0m')
1487
+                call terminal_write(char(27) // '[46m' // utf8_ch // char(27) // '[0m')
1471
             else if (pane%is_active .and. is_current_line) then
1488
             else if (pane%is_active .and. is_current_line) then
1472
                 ! Subtle background for current line in active pane
1489
                 ! Subtle background for current line in active pane
1473
-                call terminal_write(char(27) // '[48;5;237m' // ch // char(27) // '[0m')
1490
+                call terminal_write(char(27) // '[48;5;237m' // utf8_ch // char(27) // '[0m')
1474
             else if (.not. pane%is_active) then
1491
             else if (.not. pane%is_active) then
1475
                 ! Inactive pane background
1492
                 ! Inactive pane background
1476
-                call terminal_write(char(27) // '[48;5;234m' // ch // char(27) // '[0m')
1493
+                call terminal_write(char(27) // '[48;5;234m' // utf8_ch // char(27) // '[0m')
1477
             else
1494
             else
1478
                 ! Normal text
1495
                 ! Normal text
1479
-                call terminal_write(ch)
1496
+                call terminal_write(utf8_ch)
1497
+            end if
1498
+
1499
+            display_col = display_col + char_width
1500
+            char_idx = char_idx + 1
1501
+        end do
1502
+
1503
+        ! Fill remaining width with spaces
1504
+        do while (display_col < content_width)
1505
+            if (.not. pane%is_active) then
1506
+                call terminal_write(char(27) // '[48;5;234m ' // char(27) // '[0m')
1507
+            else if (is_current_line) then
1508
+                call terminal_write(char(27) // '[48;5;237m ' // char(27) // '[0m')
1509
+            else
1510
+                call terminal_write(' ')
1480
             end if
1511
             end if
1512
+            display_col = display_col + 1
1481
         end do
1513
         end do
1482
 
1514
 
1483
         ! Reset attributes
1515
         ! Reset attributes
1484
         call terminal_write(char(27) // '[0m')
1516
         call terminal_write(char(27) // '[0m')
1517
+
1518
+        if (allocated(utf8_ch)) deallocate(utf8_ch)
1485
     end subroutine render_buffer_line_in_pane
1519
     end subroutine render_buffer_line_in_pane
1486
 
1520
 
1487
     subroutine render_pane_separator(col, start_row, height)
1521
     subroutine render_pane_separator(col, start_row, height)
@@ -1879,13 +1913,11 @@ contains
1879
             end if
1913
             end if
1880
         case ("symbols")
1914
         case ("symbols")
1881
             if (is_symbols_panel_visible(editor%symbols_panel)) then
1915
             if (is_symbols_panel_visible(editor%symbols_panel)) then
1882
-                call render_lsp_symbols_panel(editor%symbols_panel, panel_start_col, panel_width, &
1916
+                call render_lsp_symbols_panel(editor%symbols_panel, editor%screen_rows - 1)
1883
-                                              2, editor%screen_rows - 1)
1884
             end if
1917
             end if
1885
         case ("workspace_symbols")
1918
         case ("workspace_symbols")
1886
             if (is_workspace_symbols_panel_visible(editor%workspace_symbols_panel)) then
1919
             if (is_workspace_symbols_panel_visible(editor%workspace_symbols_panel)) then
1887
-                call render_lsp_workspace_symbols_panel(editor%workspace_symbols_panel, panel_start_col, &
1920
+                call render_lsp_workspace_symbols_panel(editor%workspace_symbols_panel, editor%screen_rows - 1)
1888
-                                                        panel_width, 2, editor%screen_rows - 1)
1889
             end if
1921
             end if
1890
         end select
1922
         end select
1891
 
1923
 
@@ -2130,25 +2162,25 @@ contains
2130
     end subroutine render_lsp_references_panel
2162
     end subroutine render_lsp_references_panel
2131
 
2163
 
2132
     ! Render symbols panel in offcanvas mode (right side, full height)
2164
     ! Render symbols panel in offcanvas mode (right side, full height)
2133
-    subroutine render_lsp_symbols_panel(panel, start_col, width, start_row, end_row)
2165
+    subroutine render_lsp_symbols_panel(panel, screen_height)
2134
         use symbols_panel_module, only: symbols_panel_t, render_symbols_panel
2166
         use symbols_panel_module, only: symbols_panel_t, render_symbols_panel
2135
         type(symbols_panel_t), intent(in) :: panel
2167
         type(symbols_panel_t), intent(in) :: panel
2136
-        integer, intent(in) :: start_col, width, start_row, end_row
2168
+        integer, intent(in) :: screen_height
2137
 
2169
 
2138
         ! Delegate to the real symbols panel renderer
2170
         ! Delegate to the real symbols panel renderer
2139
         ! The panel manages its own positioning via panel_start_col and panel_width
2171
         ! The panel manages its own positioning via panel_start_col and panel_width
2140
-        call render_symbols_panel(panel, end_row)
2172
+        call render_symbols_panel(panel, screen_height)
2141
     end subroutine render_lsp_symbols_panel
2173
     end subroutine render_lsp_symbols_panel
2142
 
2174
 
2143
     ! Render workspace symbols panel in offcanvas mode (right side, full height)
2175
     ! Render workspace symbols panel in offcanvas mode (right side, full height)
2144
-    subroutine render_lsp_workspace_symbols_panel(panel, start_col, width, start_row, end_row)
2176
+    subroutine render_lsp_workspace_symbols_panel(panel, screen_height)
2145
         use workspace_symbols_panel_module, only: workspace_symbols_panel_t, render_workspace_symbols_panel
2177
         use workspace_symbols_panel_module, only: workspace_symbols_panel_t, render_workspace_symbols_panel
2146
         type(workspace_symbols_panel_t), intent(in) :: panel
2178
         type(workspace_symbols_panel_t), intent(in) :: panel
2147
-        integer, intent(in) :: start_col, width, start_row, end_row
2179
+        integer, intent(in) :: screen_height
2148
 
2180
 
2149
         ! Delegate to the real workspace symbols panel renderer
2181
         ! Delegate to the real workspace symbols panel renderer
2150
         ! The panel manages its own positioning via panel_start_col and panel_width
2182
         ! The panel manages its own positioning via panel_start_col and panel_width
2151
-        call render_workspace_symbols_panel(panel, end_row)
2183
+        call render_workspace_symbols_panel(panel, screen_height)
2152
     end subroutine render_lsp_workspace_symbols_panel
2184
     end subroutine render_lsp_workspace_symbols_panel
2153
 
2185
 
2154
     ! Helper function to extract basename from path
2186
     ! Helper function to extract basename from path