-- 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