@@ -528,4 +528,151 @@ describe("marksman.nvim", function()
528528 assert .is_not_nil (filtered [" helper_util" ])
529529 end )
530530 end )
531+
532+ describe (" UI highlighting" , function ()
533+ local ui
534+
535+ before_each (function ()
536+ -- Load the UI module fresh for each test
537+ package.loaded [" marksman.ui" ] = nil
538+ ui = require (" marksman.ui" )
539+ ui .setup ({ silent = true })
540+ end )
541+
542+ describe (" create_minimal_mark_line highlighting" , function ()
543+ it (" highlights the first character of the mark name correctly" , function ()
544+ -- This test verifies that the first character of the bookmark name
545+ -- gets highlighted properly ISSUE: #23
546+
547+ -- Mock mark data
548+ local mark = {
549+ file = " /path/to/nvim/lua/plugins/nvim-lspconfig.lua" ,
550+ line = 42 ,
551+ col = 1 ,
552+ text = " local function setup()" ,
553+ }
554+
555+ local name = " fn_config"
556+ local index = 1
557+ local line_idx = 5
558+
559+ -- Get the private function via debug (for testing)
560+ local create_minimal_mark_line
561+ for k , v in pairs (debug.getregistry ()) do
562+ if type (v ) == " table" then
563+ for fname , func in pairs (v ) do
564+ if fname == " create_minimal_mark_line" and type (func ) == " function" then
565+ create_minimal_mark_line = func
566+ break
567+ end
568+ end
569+ end
570+ end
571+
572+ -- If we can't access the private function directly, we'll simulate the logic
573+ local filepath = " nvim/lua/plugins/nvim-lspconfig.lua"
574+ local line = string.format (" [%d] %-20s %s" , index , name :sub (1 , 20 ), filepath )
575+
576+ local expected_name_start = 4 + # tostring (index )
577+ local actual_f_position = line :find (" f" )
578+
579+ assert .equals (
580+ expected_name_start + 1 ,
581+ actual_f_position ,
582+ " The first character 'f' should be at position " .. (expected_name_start + 1 ) .. " (1-indexed)"
583+ )
584+
585+ local buggy_name_start = 5 + # tostring (index ) -- This is 6, which is wrong
586+ local correct_name_start = 4 + # tostring (index ) -- This is 5, which is correct
587+
588+ assert .equals (5 , correct_name_start , " Correct name start should be 5 for index=1" )
589+ assert .equals (6 , buggy_name_start , " Buggy calculation gives 6 for index=1" )
590+ assert .equals (expected_name_start , correct_name_start , " Correct calculation matches expected position" )
591+ end )
592+
593+ it (" highlights correctly for double-digit indices" , function ()
594+ -- Test with index=10 to ensure the fix works for multi-digit indices too
595+ local name = " fn_config"
596+ local index = 10
597+ local expected_name_start = 4 + # tostring (index ) -- Should be 6 for index=10
598+ assert .equals (6 , expected_name_start , " Name should start at position 6 for index=10" )
599+
600+ -- Create the line to verify
601+ local filepath = " test.lua"
602+ local line = string.format (" [%d] %-20s %s" , index , name :sub (1 , 20 ), filepath )
603+ local actual_f_position = line :find (" f" )
604+
605+ -- Find returns 1-indexed, so we expect position 7 (which is 0-indexed position 6)
606+ assert .equals (
607+ expected_name_start + 1 ,
608+ actual_f_position ,
609+ " The 'f' should be at 1-indexed position " .. (expected_name_start + 1 )
610+ )
611+ end )
612+
613+ it (" applies highlight to the entire mark name" , function ()
614+ local name = " fn_config"
615+ local index = 1
616+
617+ local name_start = 4 + # tostring (index ) -- Correct calculation
618+ local name_end = name_start + math.min (20 , # name )
619+
620+ -- For "fn_config" (9 chars), the highlight should cover positions 5-13 (0-indexed)
621+ assert .equals (5 , name_start , " Name should start at position 5" )
622+ assert .equals (14 , name_end , " Name should end at position 14 (5 + 9)" )
623+
624+ -- Verify the full name is covered
625+ local expected_length = # name
626+ local actual_length = name_end - name_start
627+ assert .equals (expected_length , actual_length , " Highlight should cover all characters of the name" )
628+ end )
629+
630+ it (" handles name truncation correctly" , function ()
631+ -- Test with a name longer than 20 characters
632+ local long_name = " very_long_function_name_that_exceeds_twenty_chars"
633+ local index = 1
634+
635+ local name_start = 4 + # tostring (index )
636+ local name_end = name_start + math.min (20 , # long_name )
637+
638+ -- Should truncate to 20 characters
639+ assert .equals (5 , name_start )
640+ assert .equals (25 , name_end , " Should highlight exactly 20 characters (5 + 20)" )
641+ end )
642+ end )
643+
644+ describe (" regression test for the reported bug" , function ()
645+ it (" does not skip the first character when highlighting bookmark names" , function ()
646+ -- "[1] fn_config" where the 'f' was not highlighted
647+
648+ local name = " fn_config"
649+ local index = 1
650+
651+ -- Build the line as the code does
652+ local line = string.format (" [%d] %-20s %s" , index , name :sub (1 , 20 ), " somefile.lua" )
653+
654+ -- Calculate highlight position using CORRECT formula
655+ local correct_name_start = 4 + # tostring (index )
656+
657+ -- Extract the substring that should be highlighted
658+ local highlighted_part = line :sub (correct_name_start + 1 , correct_name_start + # name )
659+
660+ -- The highlighted part should start with 'f', not 'n'
661+ assert .equals (
662+ " fn_config" ,
663+ highlighted_part :sub (1 , # name ),
664+ " Highlighted text should start with 'f' (the first character of fn_config)"
665+ )
666+
667+ -- Verify that the buggy calculation would miss the 'f'
668+ local buggy_name_start = 5 + # tostring (index )
669+ local buggy_highlighted_part = line :sub (buggy_name_start + 1 , buggy_name_start + # name )
670+ assert .equals (
671+ " n_config " ,
672+ buggy_highlighted_part :sub (1 , 9 ),
673+ " Buggy calculation would start with 'n' (missing the 'f')"
674+ )
675+ end )
676+ end )
677+ end )
531678end )
0 commit comments