minimal extui fuzzy finder for neovim

feat: init

+1000
+287
LICENSE
··· 1 + EUROPEAN UNION PUBLIC LICENCE v. 1.2 2 + EUPL © the European Union 2007, 2016 3 + 4 + This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined 5 + below) which is provided under the terms of this Licence. Any use of the Work, 6 + other than as authorised under this Licence is prohibited (to the extent such 7 + use is covered by a right of the copyright holder of the Work). 8 + 9 + The Work is provided under the terms of this Licence when the Licensor (as 10 + defined below) has placed the following notice immediately following the 11 + copyright notice for the Work: 12 + 13 + Licensed under the EUPL 14 + 15 + or has expressed by any other means his willingness to license under the EUPL. 16 + 17 + 1. Definitions 18 + 19 + In this Licence, the following terms have the following meaning: 20 + 21 + - ‘The Licence’: this Licence. 22 + 23 + - ‘The Original Work’: the work or software distributed or communicated by the 24 + Licensor under this Licence, available as Source Code and also as Executable 25 + Code as the case may be. 26 + 27 + - ‘Derivative Works’: the works or software that could be created by the 28 + Licensee, based upon the Original Work or modifications thereof. This Licence 29 + does not define the extent of modification or dependence on the Original Work 30 + required in order to classify a work as a Derivative Work; this extent is 31 + determined by copyright law applicable in the country mentioned in Article 15. 32 + 33 + - ‘The Work’: the Original Work or its Derivative Works. 34 + 35 + - ‘The Source Code’: the human-readable form of the Work which is the most 36 + convenient for people to study and modify. 37 + 38 + - ‘The Executable Code’: any code which has generally been compiled and which is 39 + meant to be interpreted by a computer as a program. 40 + 41 + - ‘The Licensor’: the natural or legal person that distributes or communicates 42 + the Work under the Licence. 43 + 44 + - ‘Contributor(s)’: any natural or legal person who modifies the Work under the 45 + Licence, or otherwise contributes to the creation of a Derivative Work. 46 + 47 + - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of 48 + the Work under the terms of the Licence. 49 + 50 + - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, 51 + renting, distributing, communicating, transmitting, or otherwise making 52 + available, online or offline, copies of the Work or providing access to its 53 + essential functionalities at the disposal of any other natural or legal 54 + person. 55 + 56 + 2. Scope of the rights granted by the Licence 57 + 58 + The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, 59 + sublicensable licence to do the following, for the duration of copyright vested 60 + in the Original Work: 61 + 62 + - use the Work in any circumstance and for all usage, 63 + - reproduce the Work, 64 + - modify the Work, and make Derivative Works based upon the Work, 65 + - communicate to the public, including the right to make available or display 66 + the Work or copies thereof to the public and perform publicly, as the case may 67 + be, the Work, 68 + - distribute the Work or copies thereof, 69 + - lend and rent the Work or copies thereof, 70 + - sublicense rights in the Work or copies thereof. 71 + 72 + Those rights can be exercised on any media, supports and formats, whether now 73 + known or later invented, as far as the applicable law permits so. 74 + 75 + In the countries where moral rights apply, the Licensor waives his right to 76 + exercise his moral right to the extent allowed by law in order to make effective 77 + the licence of the economic rights here above listed. 78 + 79 + The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to 80 + any patents held by the Licensor, to the extent necessary to make use of the 81 + rights granted on the Work under this Licence. 82 + 83 + 3. Communication of the Source Code 84 + 85 + The Licensor may provide the Work either in its Source Code form, or as 86 + Executable Code. If the Work is provided as Executable Code, the Licensor 87 + provides in addition a machine-readable copy of the Source Code of the Work 88 + along with each copy of the Work that the Licensor distributes or indicates, in 89 + a notice following the copyright notice attached to the Work, a repository where 90 + the Source Code is easily and freely accessible for as long as the Licensor 91 + continues to distribute or communicate the Work. 92 + 93 + 4. Limitations on copyright 94 + 95 + Nothing in this Licence is intended to deprive the Licensee of the benefits from 96 + any exception or limitation to the exclusive rights of the rights owners in the 97 + Work, of the exhaustion of those rights or of other applicable limitations 98 + thereto. 99 + 100 + 5. Obligations of the Licensee 101 + 102 + The grant of the rights mentioned above is subject to some restrictions and 103 + obligations imposed on the Licensee. Those obligations are the following: 104 + 105 + Attribution right: The Licensee shall keep intact all copyright, patent or 106 + trademarks notices and all notices that refer to the Licence and to the 107 + disclaimer of warranties. The Licensee must include a copy of such notices and a 108 + copy of the Licence with every copy of the Work he/she distributes or 109 + communicates. The Licensee must cause any Derivative Work to carry prominent 110 + notices stating that the Work has been modified and the date of modification. 111 + 112 + Copyleft clause: If the Licensee distributes or communicates copies of the 113 + Original Works or Derivative Works, this Distribution or Communication will be 114 + done under the terms of this Licence or of a later version of this Licence 115 + unless the Original Work is expressly distributed only under this version of the 116 + Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee 117 + (becoming Licensor) cannot offer or impose any additional terms or conditions on 118 + the Work or Derivative Work that alter or restrict the terms of the Licence. 119 + 120 + Compatibility clause: If the Licensee Distributes or Communicates Derivative 121 + Works or copies thereof based upon both the Work and another work licensed under 122 + a Compatible Licence, this Distribution or Communication can be done under the 123 + terms of this Compatible Licence. For the sake of this clause, ‘Compatible 124 + Licence’ refers to the licences listed in the appendix attached to this Licence. 125 + Should the Licensee's obligations under the Compatible Licence conflict with 126 + his/her obligations under this Licence, the obligations of the Compatible 127 + Licence shall prevail. 128 + 129 + Provision of Source Code: When distributing or communicating copies of the Work, 130 + the Licensee will provide a machine-readable copy of the Source Code or indicate 131 + a repository where this Source will be easily and freely available for as long 132 + as the Licensee continues to distribute or communicate the Work. 133 + 134 + Legal Protection: This Licence does not grant permission to use the trade names, 135 + trademarks, service marks, or names of the Licensor, except as required for 136 + reasonable and customary use in describing the origin of the Work and 137 + reproducing the content of the copyright notice. 138 + 139 + 6. Chain of Authorship 140 + 141 + The original Licensor warrants that the copyright in the Original Work granted 142 + hereunder is owned by him/her or licensed to him/her and that he/she has the 143 + power and authority to grant the Licence. 144 + 145 + Each Contributor warrants that the copyright in the modifications he/she brings 146 + to the Work are owned by him/her or licensed to him/her and that he/she has the 147 + power and authority to grant the Licence. 148 + 149 + Each time You accept the Licence, the original Licensor and subsequent 150 + Contributors grant You a licence to their contributions to the Work, under the 151 + terms of this Licence. 152 + 153 + 7. Disclaimer of Warranty 154 + 155 + The Work is a work in progress, which is continuously improved by numerous 156 + Contributors. It is not a finished work and may therefore contain defects or 157 + ‘bugs’ inherent to this type of development. 158 + 159 + For the above reason, the Work is provided under the Licence on an ‘as is’ basis 160 + and without warranties of any kind concerning the Work, including without 161 + limitation merchantability, fitness for a particular purpose, absence of defects 162 + or errors, accuracy, non-infringement of intellectual property rights other than 163 + copyright as stated in Article 6 of this Licence. 164 + 165 + This disclaimer of warranty is an essential part of the Licence and a condition 166 + for the grant of any rights to the Work. 167 + 168 + 8. Disclaimer of Liability 169 + 170 + Except in the cases of wilful misconduct or damages directly caused to natural 171 + persons, the Licensor will in no event be liable for any direct or indirect, 172 + material or moral, damages of any kind, arising out of the Licence or of the use 173 + of the Work, including without limitation, damages for loss of goodwill, work 174 + stoppage, computer failure or malfunction, loss of data or any commercial 175 + damage, even if the Licensor has been advised of the possibility of such damage. 176 + However, the Licensor will be liable under statutory product liability laws as 177 + far such laws apply to the Work. 178 + 179 + 9. Additional agreements 180 + 181 + While distributing the Work, You may choose to conclude an additional agreement, 182 + defining obligations or services consistent with this Licence. However, if 183 + accepting obligations, You may act only on your own behalf and on your sole 184 + responsibility, not on behalf of the original Licensor or any other Contributor, 185 + and only if You agree to indemnify, defend, and hold each Contributor harmless 186 + for any liability incurred by, or claims asserted against such Contributor by 187 + the fact You have accepted any warranty or additional liability. 188 + 189 + 10. Acceptance of the Licence 190 + 191 + The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ 192 + placed under the bottom of a window displaying the text of this Licence or by 193 + affirming consent in any other similar way, in accordance with the rules of 194 + applicable law. Clicking on that icon indicates your clear and irrevocable 195 + acceptance of this Licence and all of its terms and conditions. 196 + 197 + Similarly, you irrevocably accept this Licence and all of its terms and 198 + conditions by exercising any rights granted to You by Article 2 of this Licence, 199 + such as the use of the Work, the creation by You of a Derivative Work or the 200 + Distribution or Communication by You of the Work or copies thereof. 201 + 202 + 11. Information to the public 203 + 204 + In case of any Distribution or Communication of the Work by means of electronic 205 + communication by You (for example, by offering to download the Work from a 206 + remote location) the distribution channel or media (for example, a website) must 207 + at least provide to the public the information requested by the applicable law 208 + regarding the Licensor, the Licence and the way it may be accessible, concluded, 209 + stored and reproduced by the Licensee. 210 + 211 + 12. Termination of the Licence 212 + 213 + The Licence and the rights granted hereunder will terminate automatically upon 214 + any breach by the Licensee of the terms of the Licence. 215 + 216 + Such a termination will not terminate the licences of any person who has 217 + received the Work from the Licensee under the Licence, provided such persons 218 + remain in full compliance with the Licence. 219 + 220 + 13. Miscellaneous 221 + 222 + Without prejudice of Article 9 above, the Licence represents the complete 223 + agreement between the Parties as to the Work. 224 + 225 + If any provision of the Licence is invalid or unenforceable under applicable 226 + law, this will not affect the validity or enforceability of the Licence as a 227 + whole. Such provision will be construed or reformed so as necessary to make it 228 + valid and enforceable. 229 + 230 + The European Commission may publish other linguistic versions or new versions of 231 + this Licence or updated versions of the Appendix, so far this is required and 232 + reasonable, without reducing the scope of the rights granted by the Licence. New 233 + versions of the Licence will be published with a unique version number. 234 + 235 + All linguistic versions of this Licence, approved by the European Commission, 236 + have identical value. Parties can take advantage of the linguistic version of 237 + their choice. 238 + 239 + 14. Jurisdiction 240 + 241 + Without prejudice to specific agreement between parties, 242 + 243 + - any litigation resulting from the interpretation of this License, arising 244 + between the European Union institutions, bodies, offices or agencies, as a 245 + Licensor, and any Licensee, will be subject to the jurisdiction of the Court 246 + of Justice of the European Union, as laid down in article 272 of the Treaty on 247 + the Functioning of the European Union, 248 + 249 + - any litigation arising between other parties and resulting from the 250 + interpretation of this License, will be subject to the exclusive jurisdiction 251 + of the competent court where the Licensor resides or conducts its primary 252 + business. 253 + 254 + 15. Applicable Law 255 + 256 + Without prejudice to specific agreement between parties, 257 + 258 + - this Licence shall be governed by the law of the European Union Member State 259 + where the Licensor has his seat, resides or has his registered office, 260 + 261 + - this licence shall be governed by Belgian law if the Licensor has no seat, 262 + residence or registered office inside a European Union Member State. 263 + 264 + Appendix 265 + 266 + ‘Compatible Licences’ according to Article 5 EUPL are: 267 + 268 + - GNU General Public License (GPL) v. 2, v. 3 269 + - GNU Affero General Public License (AGPL) v. 3 270 + - Open Software License (OSL) v. 2.1, v. 3.0 271 + - Eclipse Public License (EPL) v. 1.0 272 + - CeCILL v. 2.0, v. 2.1 273 + - Mozilla Public Licence (MPL) v. 2 274 + - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 275 + - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for 276 + works other than software 277 + - European Union Public Licence (EUPL) v. 1.1, v. 1.2 278 + - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong 279 + Reciprocity (LiLiQ-R+). 280 + 281 + The European Commission may update this Appendix to later versions of the above 282 + licences without producing a new version of the EUPL, as long as they provide 283 + the rights granted in Article 2 of this Licence and protect the covered Source 284 + Code from exclusive appropriation. 285 + 286 + All other changes or additions to this Appendix require the production of a new 287 + EUPL version.
+98
lua/artio/config.lua
··· 1 + ---@module 'artio.config' 2 + 3 + ---@class artio.config 4 + ---@field opts artio.config.opts 5 + ---@field win artio.config.win 6 + 7 + ---@class artio.config.opts 8 + ---@field preselect boolean 9 + ---@field bottom boolean 10 + ---@field promptprefix string 11 + ---@field pointer string 12 + 13 + ---@class artio.config.win 14 + ---@field height? integer|number 15 + ---@field hidestatusline? boolean 16 + 17 + local M = {} 18 + 19 + ---@type artio.config 20 + ---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) 21 + ---@text # Default ~ 22 + M.default = { 23 + opts = { 24 + preselect = true, 25 + bottom = true, 26 + promptprefix = "", 27 + pointer = "", 28 + }, 29 + win = { 30 + height = 12, 31 + hidestatusline = false, -- works best with laststatus=3 32 + }, 33 + } 34 + 35 + ---@type artio.config 36 + ---@diagnostic disable-next-line: missing-fields 37 + M.config = {} 38 + 39 + ---@private 40 + ---@generic T: table|any[] 41 + ---@param tdefault T 42 + ---@param toverride T 43 + ---@return T 44 + local function tmerge(tdefault, toverride) 45 + if toverride == nil then 46 + return tdefault 47 + end 48 + 49 + if vim.islist(tdefault) then 50 + return toverride 51 + end 52 + if vim.tbl_isempty(tdefault) then 53 + return toverride 54 + end 55 + 56 + return vim.iter(pairs(tdefault)):fold({}, function(tnew, k, v) 57 + if toverride[k] == nil or type(v) ~= type(toverride[k]) then 58 + tnew[k] = v 59 + return tnew 60 + end 61 + if type(v) == "table" then 62 + tnew[k] = tmerge(v, toverride[k]) 63 + return tnew 64 + end 65 + 66 + tnew[k] = toverride[k] 67 + return tnew 68 + end) 69 + end 70 + 71 + ---@param tdefault artio.config 72 + ---@param toverride artio.config 73 + ---@return artio.config 74 + function M.merge(tdefault, toverride) 75 + if vim.fn.has("nvim-0.11.0") == 1 then 76 + toverride = 77 + vim.tbl_deep_extend("keep", toverride, { editor = { float = { solid_border = vim.o.winborder == "solid" } } }) 78 + end 79 + return tmerge(tdefault, toverride) 80 + end 81 + 82 + ---@return artio.config 83 + function M.get() 84 + return M.merge(M.default, M.config) 85 + end 86 + 87 + ---@param cfg artio.config 88 + ---@return artio.config 89 + function M.override(cfg) 90 + return M.merge(M.default, cfg) 91 + end 92 + 93 + ---@param cfg artio.config 94 + function M.set(cfg) 95 + M.config = cfg 96 + end 97 + 98 + return M
+69
lua/artio/init.lua
··· 1 + local artio = {} 2 + 3 + local function lzrq(modname) 4 + return setmetatable({}, { 5 + __index = function(_, key) 6 + return require(modname)[key] 7 + end, 8 + }) 9 + end 10 + 11 + local Picker = lzrq("artio.picker") 12 + 13 + local findprg = "fd -p -t f --color=never" 14 + 15 + local function find_files(match) 16 + if not findprg then 17 + return {} 18 + end 19 + local farg = string.format("'%s'", match) 20 + local findcmd, n = findprg:gsub("%$%*", farg) 21 + if n == 0 then 22 + findcmd = findcmd .. " " .. farg 23 + end 24 + local fn = function(o) 25 + local src = o.stderr 26 + if o.code == 0 then 27 + src = o.stdout 28 + end 29 + src = src 30 + local lines = vim.split(src, "\n", { trimempty = true }) 31 + return lines 32 + end 33 + return fn(vim 34 + .system({ vim.o.shell, "-c", findcmd }, { 35 + text = true, 36 + }) 37 + :wait()) 38 + end 39 + 40 + artio.files = function() 41 + return artio.pick({ 42 + prompt = "files", 43 + fn = function(input) 44 + local lst = find_files(input) 45 + if not lst or #lst == 0 then 46 + return {} 47 + end 48 + 49 + local matches = vim.fn.matchfuzzypos(lst, input) 50 + return vim 51 + .iter(ipairs(matches[1])) 52 + :map(function(index, v) 53 + return { v, matches[2][index] } 54 + end) 55 + :totable() 56 + end, 57 + on_close = function(text, _) 58 + vim.schedule(function() 59 + vim.cmd.edit(text) 60 + end) 61 + end, 62 + }) 63 + end 64 + 65 + artio.pick = function(...) 66 + return Picker:new(...):open() 67 + end 68 + 69 + return artio
+108
lua/artio/picker.lua
··· 1 + local View = require("artio.view") 2 + 3 + ---@class artio.Picker.proto 4 + ---@field idx? integer 1-indexed 5 + ---@field fn? fun(input: string): [string, integer][] 6 + ---@field on_close? fun(text: string, idx: integer) 7 + ---@field opts? artio.config.opts 8 + ---@field win? artio.config.win 9 + ---@field prompt? string 10 + ---@field defaulttext? string 11 + ---@field prompttext? string 12 + 13 + ---@class artio.Picker : artio.Picker.proto 14 + ---@field idx integer 15 + local Picker = {} 16 + Picker.__index = Picker 17 + 18 + ---@param props artio.Picker.proto 19 + function Picker:new(props) 20 + vim.validate("fn", props.fn, "function") 21 + vim.validate("on_close", props.on_close, "function") 22 + 23 + local t = vim.tbl_deep_extend("force", { 24 + prompt = "", 25 + idx = 1, 26 + items = {}, 27 + }, require("artio.config").get(), props) 28 + 29 + t.prompttext = t.prompttext or ("%s %s"):format(t.prompt, t.opts.promptprefix) 30 + 31 + return setmetatable(t, Picker) 32 + end 33 + 34 + function Picker:open() 35 + if not self.fn or not self.on_close then 36 + vim.notify("Picker must have `fn` and `on_close`", vim.log.levels.ERROR) 37 + return 38 + end 39 + 40 + local accepted 41 + local cancelled 42 + 43 + local view = View:new() 44 + view.picker = self 45 + 46 + coroutine.wrap(function() 47 + view:open() 48 + 49 + local co, ismain = coroutine.running() 50 + assert(not ismain, "must be called from a coroutine") 51 + 52 + local key_ns = vim.on_key(function(_, typed) 53 + if view.closed then 54 + coroutine.resume(co) 55 + return 56 + end 57 + 58 + typed = string.lower(vim.fn.keytrans(typed)) 59 + if typed == "<down>" then 60 + self.idx = self.idx + 1 61 + self:fix() 62 + view:hlselect() 63 + return "" 64 + elseif typed == "<up>" then 65 + self.idx = self.idx - 1 66 + self:fix() 67 + view:hlselect() 68 + return "" 69 + elseif typed == "<cr>" then 70 + accepted = true 71 + coroutine.resume(co) 72 + return "" 73 + elseif typed == "<esc>" then 74 + cancelled = true 75 + coroutine.resume(co) 76 + return "" 77 + end 78 + end) 79 + 80 + coroutine.yield() 81 + 82 + vim.on_key(nil, key_ns) 83 + view:close() 84 + 85 + if cancelled or not accepted then 86 + return 87 + end 88 + 89 + local current = self.items[self.idx] 90 + if not current then 91 + return 92 + end 93 + 94 + self.on_close(current[1], self.idx) 95 + end)() 96 + end 97 + 98 + function Picker:fix() 99 + self.idx = math.max(self.idx, self.opts.preselect and 1 or 0) 100 + self.idx = math.min(self.idx, self.win.height - 1, #self.items) 101 + end 102 + 103 + function Picker:getitems(input) 104 + self.items = self.fn(input) 105 + return self.items 106 + end 107 + 108 + return Picker
+333
lua/artio/view.lua
··· 1 + local cmdline = require("vim._extui.cmdline") 2 + local ext = require("vim._extui.shared") 3 + 4 + local prompt_hl_id = vim.api.nvim_get_hl_id_by_name("ArtioPrompt") 5 + 6 + --- Set the 'cmdheight' and cmdline window height. Reposition message windows. 7 + --- 8 + ---@param win integer Cmdline window in the current tabpage. 9 + ---@param hide boolean Whether to hide or show the window. 10 + ---@param height integer (Text)height of the cmdline window. 11 + local function win_config(win, hide, height) 12 + if ext.cmdheight == 0 and vim.api.nvim_win_get_config(win).hide ~= hide then 13 + vim.api.nvim_win_set_config(win, { hide = hide, height = not hide and height or nil }) 14 + elseif vim.api.nvim_win_get_height(win) ~= height then 15 + vim.api.nvim_win_set_height(win, height) 16 + end 17 + if vim.o.cmdheight ~= height then 18 + -- Avoid moving the cursor with 'splitkeep' = "screen", and altering the user 19 + -- configured value with noautocmd. 20 + vim._with({ noautocmd = true, o = { splitkeep = "screen" } }, function() 21 + vim.o.cmdheight = height 22 + end) 23 + ext.msg.set_pos() 24 + end 25 + end 26 + 27 + local cmdbuff = "" ---@type string Stored cmdline used to calculate translation offset. 28 + local promptlen = 0 -- Current length of the last line in the prompt. 29 + local promptwidth = 0 -- Current width of the prompt in the cmdline buffer. 30 + local promptidx = 0 31 + --- Concatenate content chunks and set the text for the current row in the cmdline buffer. 32 + --- 33 + ---@param content CmdContent 34 + ---@param prompt string 35 + local function set_text(content, prompt) 36 + local lines = {} ---@type string[] 37 + for line in (prompt .. "\n"):gmatch("(.-)\n") do 38 + lines[#lines + 1] = vim.fn.strtrans(line) 39 + end 40 + 41 + promptlen = #lines[#lines] 42 + promptwidth = vim.fn.strdisplaywidth(lines[#lines]) 43 + 44 + cmdbuff = "" 45 + for _, chunk in ipairs(content) do 46 + cmdbuff = cmdbuff .. chunk[2] 47 + end 48 + lines[#lines] = ("%s%s"):format(lines[#lines], vim.fn.strtrans(cmdbuff)) 49 + vim.api.nvim_buf_set_lines(ext.bufs.cmd, promptidx, promptidx + 1, false, lines) 50 + end 51 + 52 + ---@class artio.View 53 + ---@field picker artio.Picker 54 + ---@field closed boolean 55 + ---@field win artio.View.win 56 + local View = {} 57 + View.__index = View 58 + 59 + function View:new() 60 + return setmetatable({ 61 + closed = false, 62 + win = { 63 + height = 0, 64 + }, 65 + }, View) 66 + end 67 + 68 + ---@class artio.View.win 69 + ---@field height integer 70 + 71 + --- Set the cmdline buffer text and cursor position. 72 + --- 73 + ---@param content CmdContent 74 + ---@param pos? integer 75 + ---@param firstc string 76 + ---@param prompt string 77 + ---@param indent integer 78 + ---@param level integer 79 + ---@param hl_id integer 80 + function View:show(content, pos, firstc, prompt, indent, level, hl_id) 81 + cmdline.level, cmdline.indent, cmdline.prompt = level, indent, cmdline.prompt or #prompt > 0 82 + if cmdline.highlighter and cmdline.highlighter.active then 83 + cmdline.highlighter.active[ext.bufs.cmd] = nil 84 + end 85 + if ext.msg.cmd.msg_row ~= -1 then 86 + ext.msg.msg_clear() 87 + end 88 + ext.msg.virt.last = { {}, {}, {}, {} } 89 + 90 + self:clear() 91 + 92 + local cmd_text = "" 93 + for _, chunk in ipairs(content) do 94 + cmd_text = cmd_text .. chunk[2] 95 + end 96 + 97 + self.picker:getitems(cmd_text) 98 + self:showitems() 99 + 100 + self:promptpos() 101 + set_text(content, ("%s%s%s"):format(firstc, prompt, (" "):rep(indent))) 102 + 103 + local height = math.max(1, vim.api.nvim_win_text_height(ext.wins.cmd, {}).all) 104 + height = math.min(height, self.win.height) 105 + win_config(ext.wins.cmd, false, height) 106 + 107 + self:updatecursor(pos) 108 + 109 + if promptlen > 0 and hl_id > 0 then 110 + vim.api.nvim_buf_set_extmark(ext.bufs.cmd, ext.ns, promptidx, 0, { hl_group = hl_id, end_col = promptlen }) 111 + end 112 + self:hlselect() 113 + end 114 + 115 + function View:saveview() 116 + self.save = vim.fn.winsaveview() 117 + self.prevwin = vim.api.nvim_get_current_win() 118 + end 119 + 120 + function View:restoreview() 121 + vim.api.nvim_set_current_win(self.prevwin) 122 + vim.fn.winrestview(self.save) 123 + end 124 + 125 + local ext_winhl = "Search:MsgArea,CurSearch:MsgArea,IncSearch:MsgArea" 126 + 127 + function View:setopts() 128 + local opts = { 129 + eventignorewin = "all,-FileType,-TextChangedI,-CursorMovedI", 130 + winhighlight = "Normal:ArtioNormal," .. ext_winhl, 131 + laststatus = self.picker.win.hidestatusline and 0 or nil, 132 + } 133 + 134 + self.opts = {} 135 + 136 + for name, value in pairs(opts) do 137 + self.opts[name] = vim.api.nvim_get_option_value(name, { scope = "local" }) 138 + vim.api.nvim_set_option_value(name, value, { scope = "local" }) 139 + end 140 + end 141 + 142 + function View:revertopts() 143 + for name, value in pairs(self.opts) do 144 + vim.api.nvim_set_option_value(name, value, { scope = "local" }) 145 + end 146 + end 147 + 148 + function View:on_resized() 149 + if self.picker.win.height > 0 then 150 + self.win.height = self.picker.win.height 151 + else 152 + self.win.height = vim.o.lines * self.picker.win.height 153 + end 154 + self.win.height = math.max(math.ceil(self.win.height), 1) 155 + end 156 + 157 + function View:open() 158 + if not self.picker then 159 + return 160 + end 161 + 162 + ext.check_targets() 163 + 164 + self.prev_show = cmdline.cmdline_show 165 + 166 + self.augroup = vim.api.nvim_create_augroup("artio:view", {}) 167 + 168 + vim.schedule(function() 169 + vim.api.nvim_create_autocmd({ "CmdlineLeave", "ModeChanged" }, { 170 + group = self.augroup, 171 + once = true, 172 + callback = function() 173 + self:close() 174 + end, 175 + }) 176 + 177 + vim.api.nvim_create_autocmd("VimResized", { 178 + group = self.augroup, 179 + callback = function() 180 + self:on_resized() 181 + end, 182 + }) 183 + 184 + vim.api.nvim_create_autocmd("TextChangedI", { 185 + group = self.augroup, 186 + callback = function() 187 + self:update() 188 + end, 189 + }) 190 + 191 + vim.api.nvim_create_autocmd("CursorMovedI", { 192 + group = self.augroup, 193 + callback = function() 194 + self:updatecursor() 195 + end, 196 + }) 197 + end) 198 + 199 + self:on_resized() 200 + 201 + cmdline.cmdline_show = function(...) 202 + return self:show(...) 203 + end 204 + 205 + self:saveview() 206 + 207 + cmdline.cmdline_show( 208 + { self.picker.defaulttext and { 0, self.picker.defaulttext } or nil }, 209 + nil, 210 + "", 211 + self.picker.prompttext, 212 + 1, 213 + 0, 214 + prompt_hl_id 215 + ) 216 + 217 + vim._with({ noautocmd = true }, function() 218 + vim.api.nvim_set_current_win(ext.wins.cmd) 219 + end) 220 + 221 + self:setopts() 222 + 223 + vim._with({ noautocmd = true }, function() 224 + vim.cmd.startinsert() 225 + end) 226 + 227 + vim.schedule(function() 228 + self:clear() 229 + self:updatecursor() 230 + end) 231 + 232 + vim._with({ win = ext.wins.cmd, wo = { eventignorewin = "" } }, function() 233 + vim.api.nvim_exec_autocmds("WinEnter", {}) 234 + end) 235 + end 236 + 237 + function View:close() 238 + if self.closed then 239 + return 240 + end 241 + self.closed = true 242 + cmdline.cmdline_show = self.prev_show 243 + vim.schedule(function() 244 + vim.cmd.stopinsert() 245 + self:revertopts() 246 + self:clear() 247 + cmdline.srow = 0 248 + cmdline.erow = 0 249 + win_config(ext.wins.cmd, true, ext.cmdheight) 250 + self:restoreview() 251 + cmdline.cmdline_block_hide() 252 + pcall(vim.api.nvim_del_augroup_by_id, self.augroup) 253 + end) 254 + end 255 + 256 + function View:update() 257 + local text = vim.api.nvim_get_current_line() 258 + text = text:sub(promptlen + 1) 259 + 260 + cmdline.cmdline_show({ { 0, text } }, nil, "", self.picker.prompttext, cmdline.indent, cmdline.level, prompt_hl_id) 261 + end 262 + 263 + local curpos = { 0, 0 } -- Last drawn cursor position. absolute 264 + ---@param pos? integer relative to prompt 265 + function View:updatecursor(pos) 266 + self:promptpos() 267 + 268 + if not pos then 269 + local cursorpos = vim.api.nvim_win_get_cursor(ext.wins.cmd) 270 + pos = cursorpos[2] - promptlen 271 + end 272 + 273 + curpos[2] = math.max(curpos[2], promptlen) 274 + 275 + if curpos[1] == promptidx + 1 and curpos[2] == promptlen + pos then 276 + return 277 + end 278 + 279 + if pos < 0 then 280 + -- reset to last known position 281 + pos = curpos[2] - promptlen 282 + end 283 + 284 + curpos[1], curpos[2] = promptidx + 1, promptlen + pos 285 + 286 + vim._with({ noautocmd = true }, function() 287 + vim.api.nvim_win_set_cursor(ext.wins.cmd, curpos) 288 + end) 289 + end 290 + 291 + function View:clear() 292 + cmdline.srow = self.picker.opts.bottom and 0 or 1 293 + cmdline.erow = 0 294 + vim.api.nvim_buf_set_lines(ext.bufs.cmd, 0, -1, false, {}) 295 + end 296 + 297 + function View:promptpos() 298 + promptidx = self.picker.opts.bottom and cmdline.erow or 0 299 + end 300 + 301 + function View:showitems() 302 + local prefix = (" "):rep(vim.fn.strdisplaywidth(self.picker.opts.pointer) + 1) 303 + 304 + local lines = {} ---@type string[] 305 + for i = 1, math.min(#self.picker.items, self.win.height - 1) do 306 + lines[#lines + 1] = ("%s%s"):format(prefix, self.picker.items[i][1]) 307 + end 308 + cmdline.erow = cmdline.srow + #lines 309 + vim.api.nvim_buf_set_lines(ext.bufs.cmd, cmdline.srow, cmdline.erow, false, lines) 310 + end 311 + 312 + local view_ns = vim.api.nvim_create_namespace("artio:view:ns") 313 + 314 + function View:hlselect() 315 + if self.select_ext then 316 + vim.api.nvim_buf_del_extmark(ext.bufs.cmd, view_ns, self.select_ext) 317 + end 318 + 319 + self.picker:fix() 320 + local idx = self.picker.idx 321 + if idx == 0 then 322 + return 323 + end 324 + 325 + self.select_ext = vim.api.nvim_buf_set_extmark(ext.bufs.cmd, view_ns, cmdline.srow + idx - 1, 0, { 326 + virt_text = { { self.picker.opts.pointer, "ArtioPointer" } }, 327 + hl_mode = "combine", 328 + virt_text_pos = "overlay", 329 + line_hl_group = "ArtioSel", 330 + }) 331 + end 332 + 333 + return View
+37
plugin/artio.lua
··· 1 + if vim.g.loaded_artio then 2 + return 3 + end 4 + 5 + vim.g.loaded_artio = true 6 + 7 + local augroup = vim.api.nvim_create_augroup("artio:hl", {}) 8 + 9 + vim.api.nvim_create_autocmd("ColorScheme", { 10 + group = augroup, 11 + callback = function() 12 + local normal_hl = vim.api.nvim_get_hl(0, { name = "Normal" }) 13 + local msgarea_hl = vim.api.nvim_get_hl(0, { name = "MsgArea" }) 14 + 15 + vim.api.nvim_set_hl(0, "ArtioNormal", { fg = normal_hl.fg, bg = msgarea_hl.bg, default = true }) 16 + vim.api.nvim_set_hl(0, "ArtioPrompt", { link = "Title", default = true }) 17 + 18 + local cursor_hl = vim.api.nvim_get_hl(0, { name = "Cursor" }) 19 + local cursorline_hl = vim.api.nvim_get_hl(0, { name = "CursorLine" }) 20 + vim.api.nvim_set_hl(0, "ArtioSel", { fg = cursor_hl.bg, bg = cursorline_hl.bg, default = true }) 21 + vim.api.nvim_set_hl(0, "ArtioPointer", { fg = cursor_hl.bg, default = true }) 22 + end, 23 + }) 24 + 25 + vim.api.nvim_create_autocmd("ColorSchemePre", { 26 + group = augroup, 27 + callback = function() 28 + vim.api.nvim_set_hl(0, "ArtioNormal", {}) 29 + vim.api.nvim_set_hl(0, "ArtioPrompt", {}) 30 + vim.api.nvim_set_hl(0, "ArtioSel", {}) 31 + vim.api.nvim_set_hl(0, "ArtioPointer", {}) 32 + end, 33 + }) 34 + 35 + vim.keymap.set("n", "<Plug>(picker-find)", function() 36 + return require("artio").files() 37 + end)
+4
selene.toml
··· 1 + std="vim" 2 + 3 + [rules] 4 + mixed_table = "allow"
+9
stylua.toml
··· 1 + indent_type = "Spaces" 2 + indent_width = 2 3 + column_width = 120 4 + quote_style = "AutoPreferDouble" 5 + call_parentheses = "Always" 6 + line_endings = "Unix" 7 + 8 + [sort_requires] 9 + enabled = true
+55
vim.toml
··· 1 + [selene] 2 + base = "lua51" 3 + name = "vim" 4 + 5 + [vim] 6 + any = true 7 + 8 + [[describe.args]] 9 + type = "string" 10 + [[describe.args]] 11 + type = "function" 12 + 13 + [[it.args]] 14 + type = "string" 15 + [[it.args]] 16 + type = "function" 17 + 18 + [[before_each.args]] 19 + type = "function" 20 + [[after_each.args]] 21 + type = "function" 22 + 23 + [assert.is_not] 24 + any = true 25 + 26 + [assert.matches] 27 + any = true 28 + 29 + [assert.has_error] 30 + any = true 31 + 32 + [[assert.equals.args]] 33 + type = "any" 34 + [[assert.equals.args]] 35 + type = "any" 36 + [[assert.equals.args]] 37 + type = "any" 38 + required = false 39 + 40 + [[assert.same.args]] 41 + type = "any" 42 + [[assert.same.args]] 43 + type = "any" 44 + 45 + [[assert.truthy.args]] 46 + type = "any" 47 + 48 + [[assert.falsy.args]] 49 + type = "any" 50 + 51 + [[assert.spy.args]] 52 + type = "any" 53 + 54 + [[assert.stub.args]] 55 + type = "any"