fortrangoingonforty/fuss / baa024d

Browse files

give the fuzzy matcher a scoring system for better ressutls

Authored by espadonne
SHA
baa024d045014f2174fbcaa6eb874b8df1bc8064
Parents
0da432f
Tree
4f41835

2 changed files

StatusFile+-
M src/display_module.f90 1 1
M src/fuss_main.f90 170 74
src/display_module.f90modified
@@ -183,7 +183,7 @@ contains
183183
                          achar(27) // '[31m✗' // achar(27) // '[0m=modified ' // &
184184
                          achar(27) // '[90m✗' // achar(27) // '[0m=untracked ' // &
185185
                          achar(27) // '[34m↓' // achar(27) // '[0m=incoming' // achar(27) // '[0m'
186
-            print '(A)', achar(27) // '[33mKeys: j/k/↑/↓:nav | ←/→:nav tree | space:toggle | .:hide-dots | a:stage | u:unstage | S:stage-all | U:unstage-all | x:discard | z:stash | Z:unstash | b:switch | n:new-br | R:del-br | G:merge | O:reset | I:rebase | f:fetch | d:diff | c:view | w:blame | h:history | L:reflog | y:cherry-pick | v:revert | r:delete | l:pull | m:commit | M:amend | p:push | t:tag | s:status | q:exit-mode | ESC:exit-mode' // achar(27) // '[0m'
186
+            print '(A)', achar(27) // '[33mKeys: j/k/↑/↓:nav | ←/→:nav tree | space:toggle | .:hide-dots | a:stage | u:unstage | S:stage-all | U:unstage-all | x:discard | z:stash | Z:unstash | b:switch | n:new-br | R:del-br | G:merge | O:reset | I:rebase | f:fetch | d:diff | c:view | w:blame | h:history | L:reflog | y:cherry-pick | v:revert | r:delete | l:pull | m:commit | M:amend | p:push | t:tag | s:status | q:exit-mode | ESC:exit-mode | ctrl-q:quit' // achar(27) // '[0m'
187187
         else
188188
             ! Normal mode help
189189
             print '(A)', 'Legend: ' // achar(27) // '[32m↑' // achar(27) // '[0m=staged ' // &
src/fuss_main.f90modified
@@ -313,6 +313,22 @@ contains
313313
             ! Always use fast blocking read - timeouts are too slow
314314
             call read_key(key)
315315
 
316
+            ! DEBUG: Log all control characters to see what we're getting
317
+            if (ichar(key) < 32) then
318
+                open(99, file='/tmp/fuss_debug.log', position='append')
319
+                write(99, '(A,I0)') 'Control char received: ', ichar(key)
320
+                close(99)
321
+            end if
322
+
323
+            ! Check for ctrl-q to quit (priority over everything)
324
+            if (key == achar(17)) then
325
+                open(99, file='/tmp/fuss_debug.log', position='append')
326
+                write(99, '(A)') 'CTRL-Q detected - quitting!'
327
+                close(99)
328
+                running = .false.
329
+                cycle
330
+            end if
331
+
316332
             ! Check for alt-g to toggle git mode
317333
             ! alt-g is encoded as achar(1 + ichar('g') - ichar('a')) = achar(7)
318334
             if (key == achar(7)) then
@@ -391,11 +407,6 @@ contains
391407
 
392408
                         call fuzzy_jump_to_match(items, n_items, search_buffer(1:search_length), selected)
393409
 
394
-                        ! DEBUG
395
-                        open(99, file='/tmp/fuss_debug.log', position='append')
396
-                        write(99, '(A,I0,A,A)') '  Result: selected=', selected, ' path=', trim(items(selected)%path)
397
-                        close(99)
398
-
399410
                         needs_full_redraw = .true.
400411
                     end if
401412
                     cycle  ! Skip case statement
@@ -692,17 +703,14 @@ contains
692703
                 visible_items = term_height - top_padding - 6
693704
                 if (visible_items < 3) visible_items = 3
694705
                 if (visible_items > n_items) visible_items = n_items
695
-            case ('q', 'Q')  ! Quit or exit git mode
706
+            case ('q', 'Q')  ! Exit git mode
696707
                 if (mode == 'git') then
697708
                     ! In git mode: q exits to normal mode
698709
                     mode = 'normal'
699710
                     needs_full_redraw = .true.
700
-                else
701
-                    ! In normal mode: q quits the application
702
-                    running = .false.
703711
                 end if
704
-            case (achar(17))  ! Ctrl-Q - force quit from any mode
705
-                running = .false.
712
+                ! Note: In normal mode, 'q' is used for fuzzy search
713
+                ! Use ctrl-q to quit from normal mode
706714
             case default
707715
                 ! Unhandled keys - do nothing
708716
                 continue
@@ -1528,102 +1536,190 @@ contains
15281536
     end subroutine refresh_and_rebuild
15291537
 
15301538
     subroutine fuzzy_jump_to_match(items, n_items, pattern, selected)
1531
-        ! Jump to first item that fuzzy matches the pattern
1532
-        ! Prioritizes matching item names (basename) over full paths
1533
-        ! Searches from NEXT position (skips current item to allow cycling)
1539
+        ! Jump to BEST matching item using fzf-style scoring
1540
+        ! Two-pass approach: basename matches first, then path matches
1541
+        ! This ensures "src" matches "src/" directory before "src/file.f90"
15341542
         type(selectable_item), intent(in) :: items(:)
15351543
         integer, intent(in) :: n_items
15361544
         character(len=*), intent(in) :: pattern
15371545
         integer, intent(inout) :: selected
1538
-        integer :: i, start_pos
1546
+        integer :: i, best_idx, best_score, score, current_score
1547
+
1548
+        best_idx = selected  ! Stay at current if no matches
1549
+        best_score = 0
1550
+
1551
+        ! Check current item's basename first - if it's a perfect match, stay on it!
1552
+        if (associated(items(selected)%node)) then
1553
+            current_score = fuzzy_match_score(pattern, items(selected)%node%name)
1554
+            if (current_score >= 10000) then  ! Exact match - stay here!
1555
+                ! DEBUG
1556
+                open(99, file='/tmp/fuss_debug.log', position='append')
1557
+                write(99, '(A,I0,A,A,A,I0,A)') '  EXACT MATCH (current): item=', selected, ' path=', &
1558
+                                              trim(items(selected)%path), ' score=', current_score, ' (basename)'
1559
+                close(99)
1560
+                return
1561
+            end if
1562
+            best_score = current_score
1563
+            best_idx = selected
1564
+        end if
15391565
 
1540
-        ! Start from next item (so repeated searches cycle through matches)
1541
-        start_pos = selected + 1
1542
-        if (start_pos > n_items) start_pos = 1
1566
+        ! PASS 1: Search for basename matches (directories, file names)
1567
+        do i = 1, n_items
1568
+            if (i == selected) cycle  ! Already checked current above
15431569
 
1544
-        ! PASS 1: Try to match item NAME first (e.g., "src" matches "src/" before "src/file.f90")
1545
-        ! Search from next position forward
1546
-        do i = start_pos, n_items
15471570
             if (associated(items(i)%node)) then
1548
-                if (fuzzy_match(pattern, items(i)%node%name)) then
1549
-                    selected = i
1550
-                    return
1571
+                score = fuzzy_match_score(pattern, items(i)%node%name)
1572
+                if (score > best_score) then
1573
+                    best_score = score
1574
+                    best_idx = i
15511575
                 end if
15521576
             end if
15531577
         end do
15541578
 
1555
-        ! Wrap around: search from beginning to current position (inclusive)
1556
-        do i = 1, selected
1557
-            if (associated(items(i)%node)) then
1558
-                if (fuzzy_match(pattern, items(i)%node%name)) then
1559
-                    selected = i
1560
-                    return
1561
-                end if
1562
-            end if
1563
-        end do
1579
+        ! If we found a good basename match, use it
1580
+        if (best_score >= 5000) then  ! Prefix or exact match
1581
+            selected = best_idx
1582
+            ! DEBUG
1583
+            open(99, file='/tmp/fuss_debug.log', position='append')
1584
+            write(99, '(A,I0,A,A,A,I0,A)') '  BASENAME MATCH: item=', best_idx, ' path=', &
1585
+                                          trim(items(best_idx)%path), ' score=', best_score, ' (basename)'
1586
+            close(99)
1587
+            return
1588
+        end if
15641589
 
1565
-        ! PASS 2: If no name match, try matching full path
1566
-        ! Search from next position forward
1567
-        do i = start_pos, n_items
1568
-            if (fuzzy_match(pattern, items(i)%path)) then
1569
-                selected = i
1570
-                return
1571
-            end if
1572
-        end do
1590
+        ! PASS 2: Search full paths if no good basename match
1591
+        do i = 1, n_items
1592
+            if (i == selected) cycle
15731593
 
1574
-        ! Wrap around: search from beginning to current position (inclusive)
1575
-        do i = 1, selected
1576
-            if (fuzzy_match(pattern, items(i)%path)) then
1577
-                selected = i
1578
-                return
1594
+            score = fuzzy_match_score(pattern, items(i)%path)
1595
+            if (score > best_score) then
1596
+                best_score = score
1597
+                best_idx = i
15791598
             end if
15801599
         end do
15811600
 
1582
-        ! No match found - stay at current position
1601
+        ! Jump to best match if any was found
1602
+        if (best_score > 0) then
1603
+            selected = best_idx
1604
+            ! DEBUG
1605
+            open(99, file='/tmp/fuss_debug.log', position='append')
1606
+            write(99, '(A,I0,A,A,A,I0,A)') '  PATH MATCH: item=', best_idx, ' path=', &
1607
+                                          trim(items(best_idx)%path), ' score=', best_score, ' (fullpath)'
1608
+            close(99)
1609
+        end if
15831610
     end subroutine fuzzy_jump_to_match
15841611
 
1585
-    function fuzzy_match(pattern, text) result(matches)
1586
-        ! Fuzzy matching like fzf: pattern chars must appear in order in text
1587
-        ! Case-insensitive matching
1588
-        ! Returns .true. if all pattern chars found in sequence
1612
+    function fuzzy_match_score(pattern, text) result(score)
1613
+        ! Fuzzy matching with fzf-style scoring
1614
+        ! Returns a score (higher is better), 0 means no match
15891615
         character(len=*), intent(in) :: pattern, text
1590
-        logical :: matches
1591
-        integer :: pattern_idx, text_idx
1592
-        character(len=1) :: pattern_char, text_char
1616
+        integer :: score
1617
+        integer :: pattern_idx, text_idx, match_start, consecutive_bonus
1618
+        character(len=256) :: pattern_lower, text_lower
1619
+        logical :: is_consecutive
15931620
 
1594
-        matches = .false.
1621
+        score = 0
15951622
 
1596
-        ! Empty pattern matches everything
1623
+        ! Empty pattern matches everything with score 1
15971624
         if (len_trim(pattern) == 0) then
1598
-            matches = .true.
1625
+            score = 1
15991626
             return
16001627
         end if
16011628
 
1629
+        ! Convert to lowercase once
1630
+        pattern_lower = pattern
1631
+        text_lower = text
1632
+        call to_lowercase(pattern_lower)
1633
+        call to_lowercase(text_lower)
1634
+
1635
+        ! Check for exact match first (highest score)
1636
+        if (trim(pattern_lower) == trim(text_lower)) then
1637
+            score = 10000
1638
+            return
1639
+        end if
1640
+
1641
+        ! Check for prefix match (very high score)
1642
+        if (len_trim(pattern_lower) <= len_trim(text_lower)) then
1643
+            if (text_lower(1:len_trim(pattern_lower)) == trim(pattern_lower)) then
1644
+                score = 5000
1645
+                return
1646
+            end if
1647
+        end if
1648
+
1649
+        ! Fuzzy match with scoring
16021650
         pattern_idx = 1
1651
+        consecutive_bonus = 0
1652
+        is_consecutive = .false.
1653
+        match_start = -1
16031654
 
1604
-        ! Scan through text looking for each pattern character in order
1605
-        do text_idx = 1, len_trim(text)
1606
-            if (pattern_idx > len_trim(pattern)) exit
1655
+        do text_idx = 1, len_trim(text_lower)
1656
+            if (pattern_idx > len_trim(pattern_lower)) exit
16071657
 
1608
-            ! Case-insensitive comparison
1609
-            pattern_char = pattern(pattern_idx:pattern_idx)
1610
-            text_char = text(text_idx:text_idx)
1658
+            if (pattern_lower(pattern_idx:pattern_idx) == text_lower(text_idx:text_idx)) then
1659
+                if (match_start == -1) match_start = text_idx
16111660
 
1612
-            ! Convert to lowercase for comparison
1613
-            if (pattern_char >= 'A' .and. pattern_char <= 'Z') then
1614
-                pattern_char = achar(ichar(pattern_char) + 32)
1615
-            end if
1616
-            if (text_char >= 'A' .and. text_char <= 'Z') then
1617
-                text_char = achar(ichar(text_char) + 32)
1618
-            end if
1661
+                ! Base score for each matched character
1662
+                score = score + 100
1663
+
1664
+                ! Bonus for consecutive characters
1665
+                if (is_consecutive) then
1666
+                    consecutive_bonus = consecutive_bonus + 1
1667
+                    score = score + consecutive_bonus * 50
1668
+                else
1669
+                    consecutive_bonus = 1
1670
+                    is_consecutive = .true.
1671
+                end if
1672
+
1673
+                ! Bonus for matching at start of text
1674
+                if (text_idx == 1) then
1675
+                    score = score + 200
1676
+                end if
1677
+
1678
+                ! Bonus for matching after separator (word boundary)
1679
+                if (text_idx > 1) then
1680
+                    if (text_lower(text_idx-1:text_idx-1) == '/' .or. &
1681
+                        text_lower(text_idx-1:text_idx-1) == '_' .or. &
1682
+                        text_lower(text_idx-1:text_idx-1) == '-' .or. &
1683
+                        text_lower(text_idx-1:text_idx-1) == '.') then
1684
+                        score = score + 150
1685
+                    end if
1686
+                end if
16191687
 
1620
-            if (pattern_char == text_char) then
16211688
                 pattern_idx = pattern_idx + 1
1689
+            else
1690
+                ! Reset consecutive bonus when characters don't match
1691
+                is_consecutive = .false.
1692
+                consecutive_bonus = 0
1693
+                ! Small penalty for gaps
1694
+                if (match_start > 0) then
1695
+                    score = score - 1
1696
+                end if
16221697
             end if
16231698
         end do
16241699
 
1625
-        ! Match succeeds if we found all pattern characters
1626
-        matches = (pattern_idx > len_trim(pattern))
1627
-    end function fuzzy_match
1700
+        ! No match if we didn't find all pattern characters
1701
+        if (pattern_idx <= len_trim(pattern_lower)) then
1702
+            score = 0
1703
+            return
1704
+        end if
1705
+
1706
+        ! Bonus for shorter strings (prefer concise matches)
1707
+        score = score - len_trim(text_lower)
1708
+
1709
+    end function fuzzy_match_score
1710
+
1711
+    subroutine to_lowercase(str)
1712
+        ! Convert string to lowercase in-place
1713
+        character(len=*), intent(inout) :: str
1714
+        integer :: i
1715
+        character(len=1) :: c
1716
+
1717
+        do i = 1, len_trim(str)
1718
+            c = str(i:i)
1719
+            if (c >= 'A' .and. c <= 'Z') then
1720
+                str(i:i) = achar(ichar(c) + 32)
1721
+            end if
1722
+        end do
1723
+    end subroutine to_lowercase
16281724
 
16291725
 end program fuss