@@ -4,10 +4,62 @@ const sysinput = @import("root").sysinput;
44const dict = sysinput .text .dictionary ;
55const insertion = sysinput .text .insertion ;
66const config = sysinput .core .config ;
7+ const debug = sysinput .core .debug ;
78
89/// Maximum number of suggestions to generate
910const MAX_SUGGESTIONS = config .TEXT .MAX_SUGGESTIONS ;
1011
12+ /// Recent suggestion cache to avoid recalculating
13+ const SuggestionCache = struct {
14+ /// The input word that generated these suggestions
15+ input : [config .TEXT .MAX_SUGGESTION_LEN ]u8 ,
16+ /// Input length (since input might contain garbage past this)
17+ input_len : usize ,
18+ /// The cached suggestions
19+ suggestions : [MAX_SUGGESTIONS ][]const u8 ,
20+ /// Number of valid suggestions in the cache
21+ count : usize ,
22+ /// Whether the cache is valid
23+ valid : bool ,
24+
25+ pub fn init () SuggestionCache {
26+ return .{
27+ .input = [_ ]u8 {0 } ** config .TEXT .MAX_SUGGESTION_LEN ,
28+ .input_len = 0 ,
29+ .suggestions = [_ ][]const u8 {"" } ** MAX_SUGGESTIONS ,
30+ .count = 0 ,
31+ .valid = false ,
32+ };
33+ }
34+
35+ pub fn isMatch (self : * const SuggestionCache , input : []const u8 ) bool {
36+ // If caching is disabled in config, always return false
37+ if (! config .PERFORMANCE .USE_SUGGESTION_CACHE ) return false ;
38+
39+ if (! self .valid or input .len != self .input_len ) return false ;
40+ return std .mem .eql (u8 , input , self .input [0.. self .input_len ]);
41+ }
42+
43+ pub fn update (self : * SuggestionCache , input : []const u8 , new_suggestions : [][]const u8 ) void {
44+ if (input .len >= self .input .len ) return ; // Too long for our cache
45+
46+ @memcpy (self .input [0.. input .len ], input );
47+ self .input_len = input .len ;
48+
49+ self .count = @min (new_suggestions .len , MAX_SUGGESTIONS );
50+ for (0.. self .count ) | i | {
51+ self .suggestions [i ] = new_suggestions [i ];
52+ }
53+
54+ self .valid = true ;
55+ }
56+
57+ pub fn invalidate (self : * SuggestionCache ) void {
58+ self .valid = false ;
59+ self .count = 0 ;
60+ }
61+ };
62+
1163/// Autocompletion engine structure
1264pub const AutocompleteEngine = struct {
1365 /// Dictionary for base vocabulary
@@ -18,6 +70,8 @@ pub const AutocompleteEngine = struct {
1870 allocator : std.mem.Allocator ,
1971 /// Current partial word being typed
2072 current_word : []const u8 ,
73+ /// Suggestion cache for performance
74+ cache : SuggestionCache ,
2175
2276 /// Initialize a new autocompletion engine
2377 pub fn init (allocator : std.mem.Allocator , dictionary : * dict.Dictionary ) ! AutocompleteEngine {
@@ -26,6 +80,7 @@ pub const AutocompleteEngine = struct {
2680 .user_words = std .StringHashMap (u32 ).init (allocator ),
2781 .allocator = allocator ,
2882 .current_word = "" ,
83+ .cache = SuggestionCache .init (),
2984 };
3085 }
3186
@@ -40,11 +95,11 @@ pub const AutocompleteEngine = struct {
4095
4196 /// Add a word to the user's vocabulary
4297 pub fn addWord (self : * AutocompleteEngine , word : []const u8 ) ! void {
43- // Don't add short words or empty strings
44- if (word .len < 2 ) return ;
98+ // Don't add words that are too short (using config value)
99+ if (word .len < config . BEHAVIOR . MIN_TRIGGER_LEN ) return ;
45100
46101 // Convert to lowercase
47- var buf : [256 ]u8 = undefined ;
102+ var buf : [config . TEXT . MAX_SUGGESTION_LEN ]u8 = undefined ;
48103 if (word .len > buf .len ) return ;
49104
50105 var i : usize = 0 ;
@@ -64,66 +119,159 @@ pub const AutocompleteEngine = struct {
64119 // Add it to the user's words with a count of 1
65120 try self .user_words .put (owned_word , 1 );
66121 }
122+
123+ // Invalidate suggestion cache whenever vocabulary changes
124+ self .cache .invalidate ();
67125 }
68126
69127 /// Set the current partial word being typed
70128 pub fn setCurrentWord (self : * AutocompleteEngine , word : []const u8 ) void {
71- self .current_word = word ;
129+ // Only update if the word has changed
130+ if (! std .mem .eql (u8 , self .current_word , word )) {
131+ self .current_word = word ;
132+ }
133+ }
134+
135+ /// Check if a string starts with a prefix (case-insensitive)
136+ fn startsWithInsensitive (haystack : []const u8 , needle : []const u8 ) bool {
137+ if (needle .len > haystack .len ) return false ;
138+
139+ for (needle , 0.. ) | n_char , i | {
140+ const h_char = haystack [i ];
141+ if (std .ascii .toLower (h_char ) != std .ascii .toLower (n_char )) {
142+ return false ;
143+ }
144+ }
145+
146+ return true ;
72147 }
73148
74149 /// Get suggestions based on the current partial word
75150 pub fn getSuggestions (self : * AutocompleteEngine , results : * std .ArrayList ([]const u8 )) ! void {
76- // Don't provide suggestions for very short partial words
77- if (self .current_word .len < 1 ) return ;
151+ // Clear existing suggestions
152+ for (results .items ) | item | {
153+ self .allocator .free (item );
154+ }
155+ results .clearRetainingCapacity ();
156+
157+ // Don't provide suggestions for very short partial words (using config)
158+ if (self .current_word .len < config .BEHAVIOR .MIN_TRIGGER_LEN ) return ;
159+
160+ // Check cache first
161+ if (self .cache .isMatch (self .current_word )) {
162+ debug .debugPrint ("Using cached suggestions for '{s}'\n " , .{self .current_word });
163+
164+ // Copy cached suggestions to results
165+ for (0.. self .cache .count ) | i | {
166+ const cached_suggestion = self .cache .suggestions [i ];
167+ const owned_suggestion = try self .allocator .dupe (u8 , cached_suggestion );
168+ try results .append (owned_suggestion );
169+ }
170+ return ;
171+ }
78172
79173 // Convert to lowercase for matching
80- var buf : [256 ]u8 = undefined ;
174+ var buf : [config . TEXT . MAX_SUGGESTION_LEN ]u8 = undefined ;
81175 if (self .current_word .len > buf .len ) return ;
82176
83177 var i : usize = 0 ;
84178 while (i < self .current_word .len ) : (i += 1 ) {
85179 buf [i ] = std .ascii .toLower (self .current_word [i ]);
86180 }
181+ const lower_word = buf [0.. self .current_word .len ];
182+
183+ // Pre-allocate a buffer to track highest frequency user words
184+ var top_user_words = std .BoundedArray (struct { word : []const u8 , freq : u32 }, MAX_SUGGESTIONS ).init (0 ) catch unreachable ;
87185
88- // First try user's words
89- var user_words_added : usize = 0 ;
186+ // First scan user's words - We'll collect top N by frequency instead of just taking the first N
90187 var it = self .user_words .iterator ();
188+ var words_checked : usize = 0 ;
189+ const max_words_to_check = config .PERFORMANCE .MAX_USER_WORDS_TO_CHECK ;
190+
91191 while (it .next ()) | entry | {
92192 const word = entry .key_ptr .* ;
193+ const freq = entry .value_ptr .* ;
93194
94- // If the word starts with the current partial word
95- if (std .mem .startsWith (u8 , word , buf [0.. self .current_word .len ]) and
96- ! std .mem .eql (u8 , word , buf [0.. self .current_word .len ]))
97- {
98- // Create a copy of the word that WE OWN
99- const owned_suggestion = try self .allocator .dupe (u8 , word );
100- errdefer self .allocator .free (owned_suggestion );
101-
102- try results .append (owned_suggestion );
195+ // Limit the number of words we check for performance
196+ words_checked += 1 ;
197+ if (words_checked > max_words_to_check ) break ;
103198
104- user_words_added += 1 ;
105- if (user_words_added >= MAX_SUGGESTIONS ) break ;
199+ // If the word starts with the current partial word (and isn't exactly the same)
200+ if (startsWithInsensitive (word , lower_word ) and
201+ ! std .mem .eql (u8 , word , lower_word ))
202+ {
203+ // Add to our bounded array, maintaining sorting by frequency
204+ if (top_user_words .len < MAX_SUGGESTIONS ) {
205+ // Just add it if we have room
206+ try top_user_words .append (.{ .word = word , .freq = freq });
207+ // Insertion sort to keep it sorted by frequency (highest first)
208+ var j = top_user_words .len - 1 ;
209+ while (j > 0 and top_user_words .get (j ).freq > top_user_words .get (j - 1 ).freq ) {
210+ const temp = top_user_words .get (j );
211+ top_user_words .set (j , top_user_words .get (j - 1 ));
212+ top_user_words .set (j - 1 , temp );
213+ j -= 1 ;
214+ }
215+ } else if (freq > top_user_words .get (top_user_words .len - 1 ).freq ) {
216+ // Replace lowest frequency word if this one is higher
217+ top_user_words .set (top_user_words .len - 1 , .{ .word = word , .freq = freq });
218+ // Bubble up to maintain sorting
219+ var j = top_user_words .len - 1 ;
220+ while (j > 0 and top_user_words .get (j ).freq > top_user_words .get (j - 1 ).freq ) {
221+ const temp = top_user_words .get (j );
222+ top_user_words .set (j , top_user_words .get (j - 1 ));
223+ top_user_words .set (j - 1 , temp );
224+ j -= 1 ;
225+ }
226+ }
106227 }
107228 }
108229
230+ // Add top user words to results
231+ for (0.. top_user_words .len ) | idx | {
232+ const word = top_user_words .get (idx ).word ;
233+ const owned_suggestion = try self .allocator .dupe (u8 , word );
234+ try results .append (owned_suggestion );
235+ }
236+
109237 // If we don't have enough user words, add suggestions from dictionary
110- if (user_words_added < MAX_SUGGESTIONS ) {
238+ if (top_user_words .len < MAX_SUGGESTIONS ) {
239+ // Track how many more suggestions we need
240+ const needed = MAX_SUGGESTIONS - top_user_words .len ;
241+
111242 var dict_iter = self .dictionary .word_map .keyIterator ();
243+ var dict_count : usize = 0 ;
244+
112245 while (dict_iter .next ()) | dict_word | {
113246 // If the dictionary word starts with the current partial word
114- if (std . mem . startsWith ( u8 , dict_word .* , buf [0 .. self . current_word . len ] ) and
115- ! std .mem .eql (u8 , dict_word .* , buf [0 .. self . current_word . len ] ))
247+ if (startsWithInsensitive ( dict_word .* , lower_word ) and
248+ ! std .mem .eql (u8 , dict_word .* , lower_word ))
116249 {
117- // Create a copy of the word that WE OWN
118- const owned_suggestion = try self .allocator .dupe (u8 , dict_word .* );
119- errdefer self .allocator .free (owned_suggestion );
250+ // Skip if this word is already in our results (from user words)
251+ var skip = false ;
252+ for (0.. top_user_words .len ) | j | {
253+ if (std .mem .eql (u8 , dict_word .* , top_user_words .get (j ).word )) {
254+ skip = true ;
255+ break ;
256+ }
257+ }
120258
121- try results .append (owned_suggestion );
259+ if (! skip ) {
260+ // Create a copy of the word
261+ const owned_suggestion = try self .allocator .dupe (u8 , dict_word .* );
262+ try results .append (owned_suggestion );
122263
123- if (results .items .len >= MAX_SUGGESTIONS ) break ;
264+ dict_count += 1 ;
265+ if (dict_count >= needed ) break ;
266+ }
124267 }
125268 }
126269 }
270+
271+ // Update cache with these new suggestions
272+ if (results .items .len > 0 ) {
273+ self .cache .update (self .current_word , results .items );
274+ }
127275 }
128276
129277 /// Process text to extract and learn words
@@ -154,5 +302,8 @@ pub const AutocompleteEngine = struct {
154302 pub fn completeWord (self : * AutocompleteEngine , word : []const u8 ) ! void {
155303 try self .addWord (word );
156304 self .current_word = "" ;
305+
306+ // Reset cache when a word is completed
307+ self .cache .invalidate ();
157308 }
158309};
0 commit comments