@@ -40,7 +40,7 @@ local default = {
4040 scanner = {
4141 scan_batch_size = 1000 , -- Scan 1000 items per batch
4242 cache_duration_ms = 5000 , -- Cache results for 5s
43- throttle_delay_ms = 5000 , -- Wait 5000ms between updates..otherwise we store lots of part of words..
43+ throttle_delay_ms = 5000 , -- Wait 5000ms between updates
4444 ignore_patterns = {}, -- No ignore patterns by default
4545 },
4646 snippet = ' ' ,
@@ -84,6 +84,44 @@ function Trie.insert(root, word, timestamp)
8484 return was_new
8585end
8686
87+ --- Remove a word from the trie, pruning empty branches
88+ --- @return boolean true if the word existed and was removed
89+ function Trie .remove (root , word )
90+ local nodes = { root }
91+ local chars = {}
92+ local node = root
93+
94+ for i = 1 , # word do
95+ local char = word :sub (i , i )
96+ if not node .children [char ] then
97+ return false
98+ end
99+ node = node .children [char ]
100+ table.insert (nodes , node )
101+ table.insert (chars , char )
102+ end
103+
104+ if not node .is_end then
105+ return false
106+ end
107+
108+ node .is_end = false
109+ node .frequency = 0
110+ node .last_used = 0
111+
112+ -- Prune empty branches bottom-up
113+ for i = # nodes , 2 , - 1 do
114+ local child = nodes [i ]
115+ if not child .is_end and not next (child .children ) then
116+ nodes [i - 1 ].children [chars [i - 1 ]] = nil
117+ else
118+ break
119+ end
120+ end
121+
122+ return true
123+ end
124+
87125function Trie .search_prefix (root , prefix )
88126 local node = root
89127 for i = 1 , # prefix do
@@ -426,12 +464,6 @@ local function cleanup_dict()
426464 end )
427465end
428466
429- local function visible_range (content )
430- local top = vim .fn .line (' w0' )
431- local bot = vim .fn .line (' w$' )
432- return vim .iter (vim .split (content , ' \n ' )):slice (top , bot ):join (' \n ' )
433- end
434-
435467-- Core word processing function
436468local function process_words (line , seen , dict_config )
437469 local new_words = 0
@@ -639,12 +671,81 @@ local function find_last_occurrence(str, patterns)
639671 return nil
640672end
641673
674+ -- Track words from previous didChange snapshot so we can diff
675+ local prev_word_set = {}
676+
677+ -- Debounced dictionary update for didChange
678+ -- Only fires after user stops typing for throttle_delay_ms
679+ local change_timer = vim .uv .new_timer ()
680+
681+ local function debounced_update_dict (text )
682+ if not change_timer or change_timer :is_closing () then
683+ return
684+ end
685+ change_timer :stop ()
686+ change_timer :start (Config .scanner .throttle_delay_ms , 0 , function ()
687+ change_timer :stop ()
688+ vim .schedule (function ()
689+ local dict_config = Config .dict
690+ local cursor_lnum = vim .api .nvim_win_get_cursor (0 )[1 ]
691+ local lines = vim .split (text , ' \n ' )
692+
693+ -- Build current word set, skipping cursor line
694+ local current_words = {}
695+ for i , line in ipairs (lines ) do
696+ if i ~= cursor_lnum then
697+ for word in line :gmatch (dict_config .word_pattern ) do
698+ if # word >= dict_config .min_word_length then
699+ current_words [word ] = true
700+ end
701+ end
702+ end
703+ end
704+
705+ -- Remove words that existed in prev snapshot but not in current
706+ -- These are partial words or deleted words
707+ for word in pairs (prev_word_set ) do
708+ if not current_words [word ] then
709+ if Trie .remove (dict .trie , word ) then
710+ dict .word_count = dict .word_count - 1
711+ end
712+ end
713+ end
714+
715+ -- Insert new words that appear in current but not prev
716+ local now = vim .uv .now ()
717+ for word in pairs (current_words ) do
718+ if not prev_word_set [word ] then
719+ if Trie .insert (dict .trie , word , now ) then
720+ dict .word_count = dict .word_count + 1
721+ end
722+ end
723+ end
724+
725+ prev_word_set = current_words
726+
727+ if dict .word_count > dict .max_words then
728+ cleanup_dict ()
729+ end
730+ end )
731+ end )
732+ end
733+
642734-- LSP handler functions
643735local function handle_document_open (params )
644736 local text = params .textDocument .text
645737 if # text == 0 then
646738 return
647739 end
740+
741+ -- Build initial prev_word_set from opened document
742+ local dict_config = Config .dict
743+ for word in text :gmatch (dict_config .word_pattern ) do
744+ if # word >= dict_config .min_word_length then
745+ prev_word_set [word ] = true
746+ end
747+ end
748+
648749 local content = vim .split (text , ' %s' , { trimempty = true })
649750 if # content == 0 then
650751 return
@@ -658,32 +759,12 @@ local function handle_document_change(params)
658759 return
659760 end
660761
661- -- Process only the changed text
662762 local change = params .contentChanges [1 ]
663763 if not change or not change .text then
664764 return
665765 end
666766
667- -- Process just the changed text
668- local lines = vim .split (change .text , ' \n ' )
669- if # lines == 0 then
670- return
671- end
672-
673- -- Process the new text directly without comparing to old version
674- local seen = {}
675- local dict_config = Config .dict
676- local new_words = 0
677-
678- for _ , line in ipairs (lines ) do
679- new_words = new_words + process_words (line , seen , dict_config )
680- end
681-
682- dict .word_count = dict .word_count + new_words
683-
684- if dict .word_count > dict .max_words then
685- cleanup_dict ()
686- end
767+ debounced_update_dict (change .text )
687768end
688769
689770function server .create ()
0 commit comments