Skip to content

Commit ffcd81c

Browse files
committed
improve caching
1 parent 37f853d commit ffcd81c

4 files changed

Lines changed: 396 additions & 79 deletions

File tree

src/core/config.zig

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ pub const UI = struct {
1212
pub const TEXT_COLOR = 0x00303030;
1313
/// Selected item text color (white)
1414
pub const SELECTED_TEXT_COLOR = 0x00FFFFFF;
15+
/// Average character width as a proportion of font height (for width estimation)
16+
pub const AVG_CHAR_WIDTH_RATIO = 0.6;
17+
/// Default popup width (pixels)
18+
pub const DEFAULT_POPUP_WIDTH = 200;
19+
/// Default popup height (pixels)
20+
pub const DEFAULT_POPUP_HEIGHT = 150;
21+
/// Screen edge padding (pixels)
22+
pub const SCREEN_EDGE_PADDING = 10;
23+
/// Base DPI value for scaling calculations
24+
pub const BASE_DPI = 96.0;
25+
/// Vertical offset below caret (pixels)
26+
pub const CARET_VERTICAL_OFFSET = 20;
1527
};
1628

1729
/// Text handling configuration
@@ -33,3 +45,23 @@ pub const BEHAVIOR = struct {
3345
/// Maximum edit distance for spelling corrections
3446
pub const MAX_EDIT_DISTANCE = 2;
3547
};
48+
49+
/// Performance configuration
50+
pub const PERFORMANCE = struct {
51+
/// Position cache lifetime in milliseconds (how long to use cached positions)
52+
pub const POSITION_CACHE_LIFETIME_MS = 200;
53+
/// Whether to use caching for suggestions
54+
pub const USE_SUGGESTION_CACHE = true;
55+
/// Whether to use caching for positions
56+
pub const USE_POSITION_CACHE = true;
57+
/// Maximum number of user words to check for suggestions
58+
pub const MAX_USER_WORDS_TO_CHECK = 1000;
59+
};
60+
61+
/// Window class specific adjustments
62+
pub const WINDOW_CLASS_ADJUSTMENTS = struct {
63+
/// Vertical offset adjustment for Edit controls
64+
pub const EDIT_CONTROL_OFFSET = 5;
65+
/// Vertical offset adjustment for RichEdit controls
66+
pub const RICHEDIT_CONTROL_OFFSET = -5;
67+
};

src/text/autocomplete.zig

Lines changed: 179 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,62 @@ const sysinput = @import("root").sysinput;
44
const dict = sysinput.text.dictionary;
55
const insertion = sysinput.text.insertion;
66
const config = sysinput.core.config;
7+
const debug = sysinput.core.debug;
78

89
/// Maximum number of suggestions to generate
910
const 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
1264
pub 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
};

src/text/edit_distance.zig

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,48 +28,31 @@ pub fn compareByScore(context: void, a: Suggestion, b: Suggestion) bool {
2828

2929
/// Calculate Levenshtein edit distance between two strings with enhancements
3030
pub fn enhancedEditDistance(a: []const u8, b: []const u8) usize {
31-
// Handle edge cases
31+
// Handle simple cases immediately
3232
if (a.len == 0) return b.len;
3333
if (b.len == 0) return a.len;
34+
if (std.mem.eql(u8, a, b)) return 0; // Identical strings
3435

35-
// Early exit for identical strings
36-
if (std.mem.eql(u8, a, b)) return 0;
37-
38-
// More aggressive early termination for very different lengths
36+
// Early termination for strings with very different lengths
3937
const len_diff = if (a.len > b.len) a.len - b.len else b.len - a.len;
4038
if (len_diff > MAX_EDIT_DISTANCE) {
41-
return len_diff; // Will exceed max distance, so return early
42-
}
43-
44-
// Quick check: if first and last characters don't match, that's already 2 operations
45-
if (a.len > 1 and b.len > 1) {
46-
var different_chars: usize = 0;
47-
if (a[0] != b[0]) different_chars += 1;
48-
if (a[a.len - 1] != b[b.len - 1]) different_chars += 1;
49-
50-
// If both ends differ and strings are long, we can often skip full calculation
51-
if (different_chars == 2 and a.len > 5 and b.len > 5) {
52-
// If first 2 chars also differ, this is likely not a close match
53-
if (a.len > 2 and b.len > 2 and a[1] != b[1]) {
54-
return MAX_EDIT_DISTANCE + 1; // Return a value above our threshold
55-
}
56-
}
39+
return len_diff; // Will exceed max distance anyway, so return early
5740
}
5841

59-
// If first two characters don't match and words are long enough,
60-
// that's another signal they might be quite different
61-
if (a.len > 1 and b.len > 1 and a[0] != b[0] and a[1] != b[1]) {
62-
// Count additional differences in the first 4 chars
63-
var initial_diff_count: usize = 0;
64-
const check_len = @min(4, @min(a.len, b.len));
42+
// Quick check of first few characters
43+
if (a.len > 2 and b.len > 2) {
44+
var diff_count: usize = 0;
45+
const check_len = @min(3, @min(a.len, b.len));
6546

6647
for (0..check_len) |i| {
67-
if (a[i] != b[i]) initial_diff_count += 1;
48+
if (a[i] != b[i]) {
49+
diff_count += 1;
50+
}
6851
}
6952

70-
// If we have 3+ differences in the first 4 chars, likely exceeds threshold
71-
if (initial_diff_count > 2 and a.len > 5 and b.len > 5) {
72-
return MAX_EDIT_DISTANCE + 1;
53+
// If first few chars are very different, likely a poor match
54+
if (diff_count >= 2) {
55+
return MAX_EDIT_DISTANCE + 1; // Return value that exceeds our threshold
7356
}
7457
}
7558

0 commit comments

Comments
 (0)