├── .gitignore ├── LICENSE ├── README.md ├── plugin └── fileselect.vim ├── doc └── fileselect.txt └── autoload └── fileselect.vim /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, Yegappan Lakshmanan 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | The File Selector plugin provides an easy access to edit a file from 3 | the current directory tree. 4 | 5 | This plugin needs Vim 8.2.2261 and above and will work on all the platforms 6 | where Vim is supported. This plugin will work in both console and GUI Vim. 7 | 8 | The command :Fileselect opens a popup menu with a list of file names from the 9 | current directory tree. When you press on a file name, the file is 10 | opened. If the selected file is already opened in a window, the cursor will 11 | move to that window. If the file is not present in any of the windows, then 12 | the selected file will be opened in the current window. You can use the up and 13 | down arrow keys to move the currently selected entry in the popup menu. 14 | 15 | In the popup menu, you can type a series of characters to narrow down the list 16 | of displayed file names. The characters entered so far is displayed in the 17 | command-line. You can press backspace to erase the previously entered set of 18 | characters. The popup menu displays all the file names containing the series of 19 | typed characters. 20 | 21 | You can close the popup menu by pressing the escape key or by pressing CTRL-C. 22 | 23 | In the popup menu, the complete directory path to a file is displayed in 24 | parenthesis after the file name. If this is too long, then the path is 25 | shortened and an abbreviated path is displayed. 26 | 27 | ## Screenshot: 28 | gif 29 | -------------------------------------------------------------------------------- /plugin/fileselect.vim: -------------------------------------------------------------------------------- 1 | if !has('patch-8.2.2261') 2 | finish 3 | endif 4 | " Need Vim 8.2.2261 and higher 5 | 6 | vim9script 7 | 8 | # File: fileselect.vim 9 | # Author: Yegappan Lakshmanan (yegappan AT yahoo DOT com) 10 | # Version: 1.2 11 | # Last Modified: Jan 4, 2021 12 | # 13 | # Plugin to display a list of file names in a popup menu 14 | # 15 | # License: Permission is hereby granted to use and distribute this code, 16 | # with or without modifications, provided that this copyright 17 | # notice is copied with it. Like anything else that's free, 18 | # fileselect plugin is provided *as is* and comes with no warranty 19 | # of any kind, either expressed or implied. In no event will the 20 | # copyright holder be liable for any damages resulting from the use 21 | # of this software. 22 | # 23 | # ========================================================================= 24 | 25 | var fs = {} 26 | if has('patch-8.2.4257') 27 | import autoload 'fileselect.vim' 28 | fs.FileSelectShowMenu = fileselect.FileSelectShowMenu 29 | fs.FileSelectToggle = fileselect.FileSelectToggle 30 | else 31 | import {FileSelectShowMenu, FileSelectToggle} from '../autoload/fileselect.vim' 32 | fs.FileSelectShowMenu = FileSelectShowMenu 33 | fs.FileSelectToggle = FileSelectToggle 34 | endif 35 | 36 | var TshowMenu = fs.FileSelectShowMenu 37 | g:FSToggle = fs.FileSelectToggle 38 | 39 | # User command to open the file select popup menu 40 | command! -nargs=* -complete=dir Fileselect call TshowMenu(, ) 41 | 42 | # key mapping to toggle the file select popup menu 43 | nnoremap (FileselectToggle) g:FSToggle() 44 | 45 | # vim: shiftwidth=2 sts=2 expandtab 46 | -------------------------------------------------------------------------------- /doc/fileselect.txt: -------------------------------------------------------------------------------- 1 | *fileselect.txt* Plugin for selecting a file from the current directory 2 | 3 | Author: Yegappan Lakshmanan (yegappan AT yahoo DOT com) 4 | For Vim version 8.2 and above 5 | Last change: Jan 4, 2021 6 | 7 | ============================================================================== 8 | CONTENTS~ 9 | 10 | 1. Overview |fileselect-overview| 11 | 2. Installation |fileselect-installation| 12 | 3. Usage |fileselect-usage| 13 | 4. Configuration |fileselect-configuration| 14 | 15 | ============================================================================== 16 | 17 | 1. Overview *fileselect-overview* 18 | 19 | The File Selector plugin provides an easy method to select a file for editing 20 | from the current directory tree. 21 | 22 | This plugin needs Vim 8.2.2261 and above and will work on all the platforms 23 | where Vim is supported. This plugin will work in both terminal and GUI Vim. 24 | 25 | The Github repository for the File Selector plugin is available at: 26 | 27 | https://github.com/yegappan/fileselect 28 | 29 | ============================================================================== 30 | 2. Installation *fileselect-installation* 31 | 32 | To install this plugin from the fileselect.zip file, unzip the files to the 33 | ~/.vim/pack/downloads/start/fileselect directory: 34 | > 35 | $ mkdir -p ~/.vim/pack/downloads/start/fileselect 36 | $ cd ~/.vim/pack/downloads/start/fileselect 37 | $ unzip ~/Downloads/fileselect.zip 38 | < 39 | To install this plugin on Linux, MacOS and other Unix-like systems from 40 | Github: 41 | > 42 | $ mkdir -p ~/.vim/pack/downloads/start 43 | $ cd ~/.vim/pack/downloads/start 44 | $ git clone https://github.com/yegappan/fileselect 45 | < 46 | To install this plugin on MS-Windows from Github: 47 | > 48 | C:\> mkdir %HOMEPATH%\vimfiles\pack\downloads\start 49 | C:\> cd %HOMEPATH%\vimfiles\pack\downloads\start 50 | C:\> git clone https://github.com/yegappan/fileselect 51 | < 52 | To uninstall the plugin, remove the fileselect directory from the 53 | $HOME/.vim/pack/downloads/start directory. 54 | 55 | Refer to the Vim |packages| help topic for more information. 56 | 57 | ============================================================================== 58 | 3. Usage *fileselect-usage* *:Fileselect* 59 | 60 | The command :Fileselect opens a popup menu with a list of file names in the 61 | current directory tree. When you press on a file name, the file is 62 | opened. If the selected file is already opened in a window, then the cursor 63 | will move to that window. If the file it not present in any of the windows, 64 | then the selected file will be opened in the current window. You can use the 65 | up and down arrow keys to move the currently selected entry in the popup menu. 66 | 67 | You can also supply a directory as an argument to the :Fileselect command. 68 | 69 | In the popup menu, you can type a series of characters to narrow down the list 70 | of displayed file names. The characters entered so far are displayed in the 71 | command-line. You can press backspace to erase one character from the 72 | previously entered set of characters. The popup menu displays all the file 73 | names containing the series of typed characters. You can press to erase 74 | filter text. 75 | 76 | You can close the popup menu by pressing the escape key or by pressing CTRL-C. 77 | 78 | In the popup menu, the following keys can be used: 79 | 80 | CTRL-F - Scroll one page forward 81 | - idem 82 | CTRL-B - Scroll one page backward 83 | - idem 84 | CTRL-Home - Jump to the first entry 85 | CTRL-End - Jump to the last entry 86 | - Go up one entry 87 | - idem 88 | - Go down one entry 89 | - idem 90 | - Open the selected file 91 | - Close the popup menu 92 | - idem 93 | - Erase one character from the filter text 94 | - idem 95 | - Erase the filter text 96 | 97 | Any other alphanumeric key will be used to narrow down the list of names 98 | displayed in the popup menu. When you type a filter string, then only the 99 | filenames fuzzy matching the string are displayed in the popup menu. 100 | 101 | You can create a key mapping to toggle the file select popup menu. For 102 | example, to use to toggle the file select menu, add the following line 103 | to your .vimrc file: 104 | > 105 | nmap (FileselectToggle) 106 | < 107 | ============================================================================== 108 | 109 | vim:tw=78:ts=8:noet:ft=help: 110 | -------------------------------------------------------------------------------- /autoload/fileselect.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | # File: fileselect.vim 3 | # Author: Yegappan Lakshmanan (yegappan AT yahoo DOT com) 4 | # Version: 1.2 5 | # Last Modified: August 11, 2021 6 | # 7 | # Plugin to display a list of file names in a popup menu. 8 | # 9 | # License: Permission is hereby granted to use and distribute this code, 10 | # with or without modifications, provided that this copyright 11 | # notice is copied with it. Like anything else that's free, 12 | # fileselect plugin is provided *as is* and comes with no warranty 13 | # of any kind, either expressed or implied. In no event will the 14 | # copyright holder be liable for any damages resulting from the use 15 | # of this software. 16 | # 17 | # ========================================================================= 18 | 19 | # Need Vim 8.2.2261 and higher 20 | if v:version < 802 || !has('patch-8.2.2261') 21 | finish 22 | endif 23 | 24 | var popupTitle: string = '' 25 | var popupID: number = -1 26 | var popupText: list = [] 27 | var fileList: list = [] 28 | var filterStr: string = '' 29 | var dirQueue: list = [] 30 | var refreshTimerID: number = 0 31 | # File names matching this pattern are ignored 32 | var ignoreFilePat: string = '\%(^\..\+\)\|\%(^.\+\.o\)\|\%(^.\+\.obj\)' 33 | 34 | def Err(msg: string): void 35 | echohl ErrorMsg 36 | echo msg 37 | echohl None 38 | enddef 39 | 40 | # Edit the file selected from the popup menu 41 | def EditFile(id: number, result: number, mods: string): void 42 | # clear the message displayed at the command-line 43 | echo '' 44 | refreshTimerID->timer_stop() 45 | if result <= 0 46 | return 47 | endif 48 | try 49 | # if the selected file is already present in a window, then jump to it 50 | var fname: string = popupText[result - 1] 51 | var winList: list = fname->bufnr()->win_findbuf() 52 | if winList->empty() 53 | # Not present in any window 54 | if &modified || &buftype != '' 55 | # the current buffer is modified or is not a normal buffer, then open 56 | # the file in a new window 57 | exe mods 'split ' .. popupText[result - 1] 58 | else 59 | var editcmd: string = 'confirm ' 60 | if mods != '' 61 | editcmd ..= mods .. ' split ' 62 | else 63 | editcmd ..= 'edit ' 64 | endif 65 | exe editcmd .. popupText[result - 1] 66 | endif 67 | else 68 | winList[0]->win_gotoid() 69 | endif 70 | catch 71 | # ignore exceptions 72 | endtry 73 | enddef 74 | 75 | # Convert each file name in the items List into () format. 76 | # Make sure the popup does't occupy the entire screen by reducing the width. 77 | def MakeMenuName(items: list): void 78 | var maxwidth: number = popupID->popup_getpos().core_width 79 | 80 | var filename: string 81 | var dirname: string 82 | var flen: number 83 | for i in items->len()->range() 84 | filename = items[i]->fnamemodify(':t') 85 | flen = filename->len() 86 | dirname = items[i]->fnamemodify(':h') 87 | 88 | if items[i]->len() > maxwidth && flen < maxwidth 89 | # keep the full file name and reduce directory name length 90 | # keep some characters at the beginning and end (equally). 91 | # 6 spaces are used for "..." and " ()" 92 | var dirsz: number = (maxwidth - flen - 6) / 2 93 | dirname = dirname[: dirsz] .. '...' .. dirname[-dirsz :] 94 | endif 95 | items[i] = filename 96 | if dirname != '.' 97 | items[i] = items[i] .. ' (' .. dirname .. '/)' 98 | endif 99 | endfor 100 | enddef 101 | 102 | # Handle the keys typed in the popup menu. 103 | # Narrow down the displayed names based on the keys typed so far. 104 | def FilterNames(id: number, key: string): bool 105 | var update_popup: bool = false 106 | var key_handled: bool = false 107 | 108 | # To respond to user key-presses in a timely fashion, restart the background 109 | # timer. 110 | refreshTimerID->timer_stop() 111 | if key != "\" && dirQueue->len() > 0 112 | refreshTimerID = timer_start(1'000, TimerCallback) 113 | endif 114 | 115 | if key == "\" || key == "\" 116 | # Erase one character from the filter text 117 | if filterStr->len() >= 1 118 | filterStr = filterStr[: -2] 119 | update_popup = true 120 | endif 121 | key_handled = true 122 | elseif key == "\" 123 | # clear the filter text 124 | filterStr = '' 125 | update_popup = true 126 | key_handled = true 127 | elseif key == "\" 128 | || key == "\" 129 | || key == "\" 130 | || key == "\" 131 | || key == "\" 132 | || key == "\" 133 | || key == "\" 134 | || key == "\" 135 | # scroll the popup window 136 | var cmd: string = 'normal! ' .. (key == "\" ? 'j' : key == "\" ? 'k' : key) 137 | cmd->win_execute(popupID) 138 | key_handled = true 139 | elseif key == "\" || key == "\" 140 | # Use native Vim handling for these keys 141 | key_handled = false 142 | elseif key =~ '^\f$' || key == "\" 143 | # Filter the names based on the typed key and keys typed before 144 | filterStr ..= key 145 | update_popup = true 146 | key_handled = true 147 | endif 148 | 149 | if update_popup 150 | # Update the popup with the new list of file names 151 | 152 | # Keep the cursor at the current item 153 | var prevSelName: string = '' 154 | if popupText->len() > 0 155 | var curLine: number = line('.', popupID) 156 | prevSelName = popupText[curLine - 1] 157 | endif 158 | 159 | UpdatePopup() 160 | echo 'Filter: ' .. filterStr 161 | 162 | # Select the previously selected entry. If not present, select first entry 163 | var idx: number = popupText->index(prevSelName) 164 | idx = idx == -1 ? 1 : idx + 1 165 | var cmd: string = 'cursor(' .. idx .. ', 1)' 166 | cmd->win_execute(popupID) 167 | endif 168 | 169 | if key_handled 170 | return true 171 | endif 172 | 173 | return id->popup_filter_menu(key) 174 | enddef 175 | 176 | # Update the popup menu with a list of filenames. If the user entered a filter 177 | # string, then fuzzy match and display only the matching filenames. 178 | def UpdatePopup(): void 179 | var matchpos: list> = [] 180 | if filterStr != '' 181 | var matches: list> = fileList->matchfuzzypos(filterStr) 182 | popupText = matches[0] 183 | matchpos = matches[1] 184 | else 185 | popupText = fileList->copy() 186 | endif 187 | 188 | # Populate the popup menu 189 | var items: list = popupText->copy() 190 | 191 | # Split the names into file name and directory path. 192 | # FIXME: Changing how a filename is displayed in the popup menu breaks the 193 | # highlighting of the fuzzy matching positions. For now, display the 194 | # unmodified filename with the full path in the popup menu 195 | #MakeMenuName(items) 196 | 197 | var text: list> 198 | if len(matchpos) > 0 199 | text = items 200 | ->mapnew((i: number, v: string): dict => ({ 201 | text: v, 202 | props: mapnew(matchpos[i], 203 | (_, w: number): dict => ({col: w + 1, length: 1, type: 'fileselect'})) 204 | })) 205 | else 206 | text = items->mapnew((_, v: string): dict => ({text: v})) 207 | endif 208 | popupID->popup_settext(text) 209 | enddef 210 | 211 | def ProcessDir(dir: string): void 212 | var dirname: string = dir 213 | if dirname == '' 214 | if dirQueue->empty() 215 | popup_setoptions(popupID, {title: popupTitle}) 216 | return 217 | endif 218 | dirname = dirQueue->remove(0) 219 | endif 220 | 221 | var start: list = reltime() 222 | while true 223 | var l: list> 224 | 225 | # Due to a bug in Vim, exceptions from readdirex() cannot be caught. 226 | # This is fixed by 8.2.1832. 227 | try 228 | l = dirname->readdirex() 229 | catch 230 | # ignore exceptions in reading directories 231 | endtry 232 | 233 | if l->empty() 234 | if dirQueue->empty() 235 | break 236 | endif 237 | dirname = dirQueue->remove(0) 238 | continue 239 | endif 240 | 241 | for f in l 242 | if f.name =~ ignoreFilePat 243 | continue 244 | endif 245 | 246 | var filename: string = (dirname == '.') ? '' : dirname .. '/' 247 | filename ..= f.name 248 | if f.type == 'dir' 249 | dirQueue->add(filename) 250 | else 251 | fileList->add(filename) 252 | endif 253 | endfor 254 | var elapsed: float = start->reltime()->reltimefloat() 255 | if elapsed > 0.1 || dirQueue->empty() 256 | break 257 | endif 258 | dirname = dirQueue->remove(0) 259 | endwhile 260 | 261 | if dirQueue->empty() && fileList->empty() 262 | Err('No files found') 263 | return 264 | endif 265 | 266 | # Expand the file paths and reduce it relative to the home and current 267 | # directories 268 | fileList = fileList->map((_, v: string): string => fnamemodify(v, ':p:~:.')) 269 | 270 | UpdatePopup() 271 | if dirQueue->len() > 0 272 | refreshTimerID = timer_start(500, TimerCallback) 273 | else 274 | popup_setoptions(popupID, {title: popupTitle}) 275 | endif 276 | enddef 277 | 278 | var signChars: list = ['―', '\', '|', '/'] 279 | var signIdx: number = 0 280 | 281 | def GetNextSign(): string 282 | var sign: string = signChars[signIdx] 283 | signIdx += 1 284 | if signIdx >= len(signChars) 285 | signIdx = 0 286 | endif 287 | return sign 288 | enddef 289 | 290 | def TimerCallback(timer_id: number): void 291 | popup_setoptions(popupID, 292 | {title: popupTitle .. '[' .. GetNextSign() .. ']'}) 293 | ProcessDir('') 294 | enddef 295 | 296 | def GetFiles(start_dir: string): void 297 | dirQueue = [] 298 | fileList = [] 299 | filterStr = '' 300 | ProcessDir(start_dir) 301 | enddef 302 | 303 | export def FileSelectShowMenu(dir_arg: string, mods: string): void 304 | var start_dir: string = dir_arg 305 | if dir_arg == '' 306 | # default is current directory 307 | start_dir = getcwd() 308 | else 309 | # shorten the directory name relative to the current directory 310 | start_dir = start_dir->fnamemodify(':p:.') 311 | endif 312 | if start_dir[-1 :] == '/' 313 | # trim the / at the end of the name 314 | start_dir = start_dir[: -2] 315 | endif 316 | 317 | # make sure a valid directory is specified 318 | if start_dir->getftype() != 'dir' 319 | Err('Error: Invalid directory ' .. start_dir) 320 | return 321 | endif 322 | 323 | # Use the directory name as the popup menu title 324 | if start_dir->len() <= 40 325 | popupTitle = '[' .. start_dir .. ']' 326 | else 327 | # trim the title and show the trailing characters 328 | popupTitle = '[...' .. start_dir[-37 :] .. ']' 329 | endif 330 | 331 | # Create the popup menu 332 | var lnum: number = &lines - &cmdheight - 2 - 10 333 | var popupAttr: dict = { 334 | title: popupTitle, 335 | wrap: 0, 336 | pos: 'topleft', 337 | line: lnum, 338 | col: 2, 339 | minwidth: 60, 340 | minheight: 10, 341 | maxheight: 10, 342 | maxwidth: 60, 343 | mapping: 1, 344 | fixed: 1, 345 | close: 'button', 346 | filter: FilterNames, 347 | callback: (id, result) => EditFile(id, result, mods) 348 | } 349 | popupID = popup_menu([], popupAttr) 350 | prop_type_add('fileselect', {bufnr: popupID->winbufnr(), 351 | highlight: 'Title'}) 352 | 353 | # Get the list of file names to display. 354 | GetFiles(start_dir) 355 | if fileList->empty() 356 | return 357 | endif 358 | enddef 359 | 360 | # Toggle (open or close) the fileselect popup menu 361 | export def FileSelectToggle(): string 362 | if popupID->win_gettype() != 'popup' 363 | # open the file select popup 364 | FileSelectShowMenu('', '') 365 | else 366 | # popup window is present. close it. 367 | popupID->popup_close(-2) 368 | endif 369 | return "\" 370 | enddef 371 | 372 | # vim: shiftwidth=2 sts=2 expandtab 373 | --------------------------------------------------------------------------------