| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- -- ido.lua
- -- ============================================================================
- -- Interactive Do - Enhanced completion with fuzzy matching and inline display.
- -- Similar to Emacs ido-mode.
- -- ============================================================================
- local ido = {}
- -- Configuration
- ido.config = {
- enabled = true,
- separator = " | ",
- max_candidates = 10,
- show_count = true,
- fuzzy = true,
- case_sensitive = false,
- -- Display format: "inline" shows in minibuffer, "vertical" would need popup
- display = "inline",
- }
- -- State
- ido.active = false
- ido.candidates = {}
- ido.filtered = {}
- ido.selected_index = 1
- ido.input = ""
- -- Fuzzy match score function
- -- Returns score (higher is better) or nil for no match
- function ido.fuzzy_match(pattern, candidate)
- if pattern == "" then
- return 1000 -- Empty pattern matches everything
- end
- local p = ido.config.case_sensitive and pattern or pattern:lower()
- local c = ido.config.case_sensitive and candidate or candidate:lower()
- -- Exact match is best
- if c == p then
- return 10000
- end
- -- Prefix match is very good
- if c:sub(1, #p) == p then
- return 5000 + (1000 / #c)
- end
- -- Substring match
- local substr_pos = c:find(p, 1, true)
- if substr_pos then
- return 3000 - substr_pos + (1000 / #c)
- end
- -- Fuzzy match: all chars in pattern appear in order in candidate
- if ido.config.fuzzy then
- local score = 0
- local j = 1
- local consecutive = 0
- local last_match = -1
- for i = 1, #p do
- local char = p:sub(i, i)
- local found = false
- while j <= #c do
- if c:sub(j, j) == char then
- found = true
- -- Bonus for consecutive matches
- if j == last_match + 1 then
- consecutive = consecutive + 1
- score = score + consecutive * 10
- else
- consecutive = 0
- end
- -- Bonus for matching at word boundaries
- if j == 1 or c:sub(j-1, j-1):match("[%s_%-/\\.]") then
- score = score + 50
- end
- last_match = j
- j = j + 1
- break
- end
- j = j + 1
- end
- if not found then
- return nil -- No match
- end
- end
- -- Penalty for length difference
- score = score + 1000 - (#c - #p) * 5
- return math.max(1, score)
- end
- return nil
- end
- -- Filter and sort candidates based on input
- function ido.filter_candidates(input)
- local results = {}
- for _, candidate in ipairs(ido.candidates) do
- local text = type(candidate) == "table" and candidate.text or candidate
- local score = ido.fuzzy_match(input, text)
- if score then
- table.insert(results, {
- candidate = candidate,
- text = text,
- score = score
- })
- end
- end
- -- Sort by score (descending)
- table.sort(results, function(a, b)
- return a.score > b.score
- end)
- -- Extract candidates
- ido.filtered = {}
- for i, r in ipairs(results) do
- if i > ido.config.max_candidates * 2 then break end
- table.insert(ido.filtered, r.candidate)
- end
- return ido.filtered
- end
- -- Format candidates for display in minibuffer
- function ido.format_display(input)
- local parts = {}
- -- Show input
- if input ~= "" then
- table.insert(parts, input)
- end
- -- Show candidates
- local shown = 0
- for i, candidate in ipairs(ido.filtered) do
- if shown >= ido.config.max_candidates then
- if #ido.filtered > ido.config.max_candidates then
- table.insert(parts, "...")
- end
- break
- end
- local text = type(candidate) == "table" and candidate.text or candidate
- if i == ido.selected_index then
- text = "[" .. text .. "]"
- end
- table.insert(parts, text)
- shown = shown + 1
- end
- -- Add count if enabled
- if ido.config.show_count and #ido.filtered > ido.config.max_candidates then
- table.insert(parts, "(" .. #ido.filtered .. " total)")
- end
- return table.concat(parts, ido.config.separator)
- end
- -- Select next candidate
- function ido.next()
- if #ido.filtered > 0 then
- ido.selected_index = (ido.selected_index % #ido.filtered) + 1
- end
- end
- -- Select previous candidate
- function ido.prev()
- if #ido.filtered > 0 then
- ido.selected_index = ((ido.selected_index - 2) % #ido.filtered) + 1
- end
- end
- -- Get selected candidate
- function ido.get_selected()
- if ido.selected_index <= #ido.filtered then
- return ido.filtered[ido.selected_index]
- end
- return nil
- end
- -- Complete buffer names with ido
- function ido.complete_buffer()
- local buf_names = editor:get_buffer_names()
- ido.candidates = buf_names
- ido.filtered = ido.filter_candidates("")
- ido.selected_index = 1
- local display = ido.format_display("")
- editor:message("Switch to buffer: " .. display)
- end
- -- Complete commands with ido
- function ido.complete_command()
- local cmd_names = get_command_names()
- ido.candidates = cmd_names
- ido.filtered = ido.filter_candidates("")
- ido.selected_index = 1
- local display = ido.format_display("")
- editor:message("M-x " .. display)
- end
- -- Register commands
- editor:register_command("ido-switch-buffer", "Switch buffer with ido completion", function(args)
- ido.complete_buffer()
- return {success = true}
- end)
- editor:register_command("ido-execute-command", "Execute command with ido completion", function(args)
- ido.complete_command()
- return {success = true}
- end)
- editor:register_command("ido-mode", "Toggle ido-mode for enhanced completion", function(args)
- ido.config.enabled = not ido.config.enabled
- if ido.config.enabled then
- editor:message("ido-mode enabled")
- else
- editor:message("ido-mode disabled")
- end
- return {success = true}
- end)
- editor:register_command("ido-toggle-fuzzy", "Toggle fuzzy matching in ido", function(args)
- ido.config.fuzzy = not ido.config.fuzzy
- editor:message("Fuzzy matching: " .. (ido.config.fuzzy and "on" or "off"))
- return {success = true}
- end)
- -- Utility: simple buffer selection demo
- editor:register_command("ido-demo", "Demonstrate ido buffer selection", function(args)
- local buf_names = editor:get_buffer_names()
- ido.candidates = buf_names
- local results = ido.filter_candidates(args[1] or "")
- ido.selected_index = 1
- if #results > 0 then
- local display = ido.format_display(args[1] or "")
- return {success = true, message = display}
- else
- return {success = false, message = "No matching buffers"}
- end
- end, {}, true, "s")
- -- Store in lumacs namespace
- lumacs.ido = ido
- print("[ido] Package loaded")
- return ido
|