| 1 | ! ============================================================================== |
| 2 | ! Module: suggestions |
| 3 | ! Pure suggestion selection logic for autosuggestions (shadow text). |
| 4 | ! Separated from readline for testability — no I/O, no terminal ops. |
| 5 | ! ============================================================================== |
| 6 | module suggestions |
| 7 | implicit none |
| 8 | private |
| 9 | |
| 10 | ! Buffer size — matches readline's MAX_LINE_LEN on Linux/gfortran |
| 11 | integer, parameter, public :: SUGGEST_BUF_LEN = 1024 |
| 12 | integer, parameter, public :: MAX_SUGGEST_COMPLETIONS = 40 |
| 13 | |
| 14 | ! Suggestion source identifiers |
| 15 | integer, parameter, public :: SUGGEST_NONE = 0 |
| 16 | integer, parameter, public :: SUGGEST_PATH = 1 |
| 17 | integer, parameter, public :: SUGGEST_HISTORY = 2 |
| 18 | |
| 19 | type, public :: suggestion_result_t |
| 20 | character(len=SUGGEST_BUF_LEN) :: text = '' |
| 21 | integer :: length = 0 |
| 22 | integer :: source = 0 ! SUGGEST_NONE, SUGGEST_PATH, SUGGEST_HISTORY |
| 23 | end type suggestion_result_t |
| 24 | |
| 25 | public :: compute_path_suggestion, compute_history_suggestion |
| 26 | |
| 27 | contains |
| 28 | |
| 29 | ! -------------------------------------------------------------------------- |
| 30 | ! Compute a path-based suggestion from completion results. |
| 31 | ! |
| 32 | ! Fish-style: pick the FIRST prefix-matching completion and suggest its |
| 33 | ! full remainder. This works for both single and multiple completions, |
| 34 | ! and correctly handles paths ending in / (where common prefix = last_word). |
| 35 | ! -------------------------------------------------------------------------- |
| 36 | function compute_path_suggestion(last_word, last_word_len, & |
| 37 | completions, num_completions) result(res) |
| 38 | character(len=*), intent(in) :: last_word |
| 39 | integer, intent(in) :: last_word_len |
| 40 | character(len=*), intent(in) :: completions(:) |
| 41 | integer, intent(in) :: num_completions |
| 42 | type(suggestion_result_t) :: res |
| 43 | |
| 44 | integer :: i, j, comp_len |
| 45 | logical :: is_prefix |
| 46 | |
| 47 | res%text = '' |
| 48 | res%length = 0 |
| 49 | res%source = SUGGEST_NONE |
| 50 | |
| 51 | if (last_word_len == 0 .or. num_completions == 0) return |
| 52 | |
| 53 | ! Find the first completion that is a strict prefix extension of last_word |
| 54 | do i = 1, num_completions |
| 55 | comp_len = len_trim(completions(i)) |
| 56 | if (comp_len <= last_word_len) cycle |
| 57 | |
| 58 | ! Verify prefix match character-by-character |
| 59 | is_prefix = .true. |
| 60 | do j = 1, last_word_len |
| 61 | if (completions(i)(j:j) /= last_word(j:j)) then |
| 62 | is_prefix = .false. |
| 63 | exit |
| 64 | end if |
| 65 | end do |
| 66 | |
| 67 | if (is_prefix) then |
| 68 | res%length = min(comp_len - last_word_len, SUGGEST_BUF_LEN) |
| 69 | do j = 1, res%length |
| 70 | res%text(j:j) = completions(i)(last_word_len + j : last_word_len + j) |
| 71 | end do |
| 72 | res%source = SUGGEST_PATH |
| 73 | return |
| 74 | end if |
| 75 | end do |
| 76 | end function compute_path_suggestion |
| 77 | |
| 78 | ! -------------------------------------------------------------------------- |
| 79 | ! Compute a history-based suggestion. |
| 80 | ! |
| 81 | ! Searches history_lines backwards for the first entry that starts with |
| 82 | ! current_input. Returns the remaining suffix (truncated at newlines). |
| 83 | ! -------------------------------------------------------------------------- |
| 84 | function compute_history_suggestion(current_input, input_len, & |
| 85 | history_lines, history_count) result(res) |
| 86 | character(len=*), intent(in) :: current_input |
| 87 | integer, intent(in) :: input_len |
| 88 | character(len=*), intent(in) :: history_lines(:) |
| 89 | integer, intent(in) :: history_count |
| 90 | type(suggestion_result_t) :: res |
| 91 | |
| 92 | integer :: i, j, hist_len, remainder_len, newline_pos |
| 93 | logical :: matches |
| 94 | |
| 95 | res%text = '' |
| 96 | res%length = 0 |
| 97 | res%source = SUGGEST_NONE |
| 98 | |
| 99 | if (input_len == 0 .or. history_count == 0) return |
| 100 | |
| 101 | do i = history_count, 1, -1 |
| 102 | hist_len = len_trim(history_lines(i)) |
| 103 | if (hist_len > input_len) then |
| 104 | ! Check prefix match character-by-character |
| 105 | matches = .true. |
| 106 | do j = 1, input_len |
| 107 | if (history_lines(i)(j:j) /= current_input(j:j)) then |
| 108 | matches = .false. |
| 109 | exit |
| 110 | end if |
| 111 | end do |
| 112 | |
| 113 | if (matches) then |
| 114 | ! Extract remainder |
| 115 | remainder_len = min(hist_len - input_len, SUGGEST_BUF_LEN) |
| 116 | |
| 117 | ! Find first newline in remainder |
| 118 | newline_pos = 0 |
| 119 | do j = 1, remainder_len |
| 120 | if (history_lines(i)(input_len + j : input_len + j) == char(10) .or. & |
| 121 | history_lines(i)(input_len + j : input_len + j) == char(13)) then |
| 122 | newline_pos = j |
| 123 | exit |
| 124 | end if |
| 125 | end do |
| 126 | |
| 127 | if (newline_pos == 1) then |
| 128 | ! Newline is first char of remainder — no useful suggestion |
| 129 | cycle |
| 130 | end if |
| 131 | |
| 132 | ! Truncate at newline if found |
| 133 | if (newline_pos > 1) then |
| 134 | remainder_len = newline_pos - 1 |
| 135 | end if |
| 136 | |
| 137 | ! Copy remainder character-by-character |
| 138 | res%text = '' |
| 139 | do j = 1, remainder_len |
| 140 | res%text(j:j) = history_lines(i)(input_len + j : input_len + j) |
| 141 | end do |
| 142 | res%length = remainder_len |
| 143 | res%source = SUGGEST_HISTORY |
| 144 | return |
| 145 | end if |
| 146 | end if |
| 147 | end do |
| 148 | end function compute_history_suggestion |
| 149 | |
| 150 | end module suggestions |
| 151 |