├── .gitignore ├── .npmignore ├── Readme.md ├── esbuild.js ├── nvimstart ├── package-lock.json ├── package.json ├── src ├── buffers.ts ├── changes.ts ├── cmdhistory.ts ├── colors.ts ├── commands.ts ├── files.ts ├── filetypes.ts ├── functions.ts ├── grep.ts ├── helptags.ts ├── index.ts ├── lines.ts ├── locationlist.ts ├── maps.ts ├── marks.ts ├── mru.ts ├── quickfix.ts ├── registers.ts ├── searchhistory.ts ├── session.ts ├── tags.ts ├── util │ ├── ansiparse.ts │ ├── index.ts │ └── option.ts ├── windows.ts └── words.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tsconfig.json 3 | tslint.json 4 | *.map 5 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # coc-lists 2 | 3 | Some basic list sources for [coc.nvim](https://github.com/neoclide/coc.nvim/) 4 | 5 | Including: 6 | 7 | - [x] `buffers` current buffer list. 8 | - [x] `changes` changes list. 9 | - [x] `cmdhistory` history of commands. 10 | - [x] `colors` colors schemes. 11 | - [x] `files` search files from current cwd. 12 | - [x] `filetypes` file types. 13 | - [x] `grep` grep text from current cwd. 14 | - [x] `helptags` helptags of vim. 15 | - [x] `lines` search lines by regex patterns. 16 | - [x] `locationlist` items from vim's location list. 17 | - [x] `maps` key mappings. 18 | - [x] `marks` marks of vim. 19 | - [x] `mru` most recent used files. 20 | - [x] `quickfix` items from vim's quickfix list. 21 | - [x] `registers` registers of vim. 22 | - [x] `searchhistory` history of search. 23 | - [x] `sessions` session list. 24 | - [x] `tags` search tag files. 25 | - [x] `vimcommands` available vim commands. 26 | - [x] `windows` windows of vim. 27 | - [x] `words` search word in current buffer. 28 | - [x] `functions` available vim functions. 29 | 30 | For snippets list, use [coc-snippets](https://github.com/neoclide/coc-snippets). 31 | 32 | For git related list, use [coc-git](https://github.com/neoclide/coc-git). 33 | 34 | For yank history, use [coc-yank](https://github.com/neoclide/coc-yank). 35 | 36 | ## Install 37 | 38 | In your vim/neovim, run command: 39 | 40 | ``` 41 | :CocInstall coc-lists 42 | ``` 43 | 44 | Checkout `:h coc-list` for usage. 45 | 46 | ## Options 47 | 48 | **Tip:** type `?` on normal mode to get detail help of current list. 49 | 50 | Available options for coc-lists: 51 | 52 | - `session.saveOnVimLeave` Save session on VimLeavePre., default to `true` 53 | - `session.directory` directory for session files, default to `~/.vim/sessions` 54 | - `session.restartOnSessionLoad` Restart vim with cwd changed on session load, support neovim on iTerm2 only. 55 | - `lists.disabledLists`: List names to disable form load., default: `[]` 56 | - `list.source.files.command`: Command used for search for files, default: `""` 57 | - `list.source.files.args`: Arguments for search command, default: `[]` 58 | - `list.source.files.excludePatterns`: Minimatch patterns that should be excluded., default: `[]` 59 | - `list.source.mru.maxLength`: Max length of mru list., default: `1000` 60 | - `list.source.mru.ignoreGitIgnore`: Ignore git ignored files., default: `false` 61 | - `list.source.mru.excludePatterns`: Minimatch patterns that should be excluded., default: `["**/.git/*","/tmp/*"]` 62 | - `list.source.grep.useLiteral`: Use literal match unless specified regex options, default: true., default: `true` 63 | - `list.source.grep.command`: Command used for grep, default to 'rg'., default: `"rg"` could be `rg` or `ag`. 64 | - `list.source.grep.maxColumns`: Don't print lines longer than this limit in bytes, ripgrep only.. 65 | - `list.source.tags.command`: Command used for generate tags., default: `"ctags -R ."` 66 | - `list.source.grep.args`: Arguments for grep command, always used for grep, default: `[]` 67 | - `list.source.grep.excludePatterns`: Minimatch patterns of files that should be excluded, use .ignore file is recommended., default: `[]` 68 | 69 | ## Commands 70 | 71 | - `mru.validate` remove none exists files from mru list. 72 | - `tags.generate` generate tags of current project (in current cwd). 73 | - `session.save` save current vim state to session file. 74 | - `session.load` load exists session file. 75 | 76 | ## F.A.Q 77 | 78 | Q: Hidden files not exists using files source. 79 | 80 | A: You have to pass `--hidden` to `ripgrep` by using configuration: 81 | 82 | `list.source.files.args`: ['--hidden', '--files'] 83 | 84 | Q: How to ignore files using files/grep source. 85 | 86 | A: You can add `.ignore` file in your project root, which would be respected by 87 | `ripgrep` or use `list.sourcefiles.excludePatterns` configuration. 88 | 89 | Q: How to make grep easier? 90 | 91 | A: Create custom command like: 92 | 93 | ```vim 94 | " grep word under cursor 95 | command! -nargs=+ -complete=custom,s:GrepArgs Rg exe 'CocList grep '. 96 | 97 | function! s:GrepArgs(...) 98 | let list = ['-S', '-smartcase', '-i', '-ignorecase', '-w', '-word', 99 | \ '-e', '-regex', '-u', '-skip-vcs-ignores', '-t', '-extension'] 100 | return join(list, "\n") 101 | endfunction 102 | 103 | " Keymapping for grep word under cursor with interactive mode 104 | nnoremap cf :exe 'CocList -I --input='.expand('').' grep' 105 | ``` 106 | 107 | Q: How to grep by motion? 108 | 109 | A: Create custom keymappings like: 110 | 111 | ```vim 112 | vnoremap g :call GrepFromSelected(visualmode()) 113 | nnoremap g :set operatorfunc=GrepFromSelectedg@ 114 | 115 | function! s:GrepFromSelected(type) 116 | let saved_unnamed_register = @@ 117 | if a:type ==# 'v' 118 | normal! `y 119 | elseif a:type ==# 'char' 120 | normal! `[v`]y 121 | else 122 | return 123 | endif 124 | let word = substitute(@@, '\n$', '', 'g') 125 | let word = escape(word, '| ') 126 | let @@ = saved_unnamed_register 127 | execute 'CocList grep '.word 128 | endfunction 129 | ``` 130 | 131 | Q: How to grep current word in current buffer? 132 | 133 | A: Create kep-mapping like: 134 | 135 | ```vim 136 | nnoremap w :exe 'CocList -I --normal --input='.expand('').' words' 137 | ``` 138 | 139 | Q: How to grep word in a specific folder? 140 | 141 | A: Pass `-- /folder/to/search/from` to `CocList grep` 142 | 143 | ```vim 144 | :CocList grep word -- /folder/to/search/from 145 | ``` 146 | 147 | ## License 148 | 149 | MIT 150 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | async function start() { 2 | await require('esbuild').build({ 3 | entryPoints: ['src/index.ts'], 4 | bundle: true, 5 | minify: process.env.NODE_ENV === 'production', 6 | sourcemap: process.env.NODE_ENV === 'development', 7 | mainFields: ['module', 'main'], 8 | external: ['coc.nvim'], 9 | platform: 'node', 10 | target: 'node10.12', 11 | outfile: 'lib/index.js' 12 | }) 13 | } 14 | 15 | start().catch(e => { 16 | console.error(e) 17 | }) 18 | -------------------------------------------------------------------------------- /nvimstart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | session=$1 3 | cwd=$2 4 | 5 | cat << EOF | osascript - 6 | tell application "iTerm2" 7 | tell current window 8 | tell current session 9 | delay 0.2 10 | write text "clear" 11 | write text "cd $cwd" 12 | write text "nvim -c 'silent! source $session'" 13 | end tell 14 | end tell 15 | end tell 16 | EOF 17 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coc-lists", 3 | "version": "1.5.3", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "coc-lists", 9 | "version": "1.5.3", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@types/colors": "^1.2.4", 13 | "@types/minimatch": "^5.1.2", 14 | "@types/node": "16.18", 15 | "@types/which": "^3.0.4", 16 | "coc.nvim": "0.0.83-next.19", 17 | "colors": "^1.4.0", 18 | "esbuild": "^0.25.0", 19 | "minimatch": "5.1.2", 20 | "typescript": "^5.7.3", 21 | "which": "^5.0.0" 22 | }, 23 | "engines": { 24 | "coc": "^0.0.80" 25 | } 26 | }, 27 | "node_modules/@esbuild/aix-ppc64": { 28 | "version": "0.25.0", 29 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", 30 | "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", 31 | "cpu": [ 32 | "ppc64" 33 | ], 34 | "dev": true, 35 | "license": "MIT", 36 | "optional": true, 37 | "os": [ 38 | "aix" 39 | ], 40 | "engines": { 41 | "node": ">=18" 42 | } 43 | }, 44 | "node_modules/@esbuild/android-arm": { 45 | "version": "0.25.0", 46 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", 47 | "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", 48 | "cpu": [ 49 | "arm" 50 | ], 51 | "dev": true, 52 | "license": "MIT", 53 | "optional": true, 54 | "os": [ 55 | "android" 56 | ], 57 | "engines": { 58 | "node": ">=18" 59 | } 60 | }, 61 | "node_modules/@esbuild/android-arm64": { 62 | "version": "0.25.0", 63 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", 64 | "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", 65 | "cpu": [ 66 | "arm64" 67 | ], 68 | "dev": true, 69 | "license": "MIT", 70 | "optional": true, 71 | "os": [ 72 | "android" 73 | ], 74 | "engines": { 75 | "node": ">=18" 76 | } 77 | }, 78 | "node_modules/@esbuild/android-x64": { 79 | "version": "0.25.0", 80 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", 81 | "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", 82 | "cpu": [ 83 | "x64" 84 | ], 85 | "dev": true, 86 | "license": "MIT", 87 | "optional": true, 88 | "os": [ 89 | "android" 90 | ], 91 | "engines": { 92 | "node": ">=18" 93 | } 94 | }, 95 | "node_modules/@esbuild/darwin-arm64": { 96 | "version": "0.25.0", 97 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", 98 | "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", 99 | "cpu": [ 100 | "arm64" 101 | ], 102 | "dev": true, 103 | "license": "MIT", 104 | "optional": true, 105 | "os": [ 106 | "darwin" 107 | ], 108 | "engines": { 109 | "node": ">=18" 110 | } 111 | }, 112 | "node_modules/@esbuild/darwin-x64": { 113 | "version": "0.25.0", 114 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", 115 | "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", 116 | "cpu": [ 117 | "x64" 118 | ], 119 | "dev": true, 120 | "license": "MIT", 121 | "optional": true, 122 | "os": [ 123 | "darwin" 124 | ], 125 | "engines": { 126 | "node": ">=18" 127 | } 128 | }, 129 | "node_modules/@esbuild/freebsd-arm64": { 130 | "version": "0.25.0", 131 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", 132 | "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", 133 | "cpu": [ 134 | "arm64" 135 | ], 136 | "dev": true, 137 | "license": "MIT", 138 | "optional": true, 139 | "os": [ 140 | "freebsd" 141 | ], 142 | "engines": { 143 | "node": ">=18" 144 | } 145 | }, 146 | "node_modules/@esbuild/freebsd-x64": { 147 | "version": "0.25.0", 148 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", 149 | "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", 150 | "cpu": [ 151 | "x64" 152 | ], 153 | "dev": true, 154 | "license": "MIT", 155 | "optional": true, 156 | "os": [ 157 | "freebsd" 158 | ], 159 | "engines": { 160 | "node": ">=18" 161 | } 162 | }, 163 | "node_modules/@esbuild/linux-arm": { 164 | "version": "0.25.0", 165 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", 166 | "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", 167 | "cpu": [ 168 | "arm" 169 | ], 170 | "dev": true, 171 | "license": "MIT", 172 | "optional": true, 173 | "os": [ 174 | "linux" 175 | ], 176 | "engines": { 177 | "node": ">=18" 178 | } 179 | }, 180 | "node_modules/@esbuild/linux-arm64": { 181 | "version": "0.25.0", 182 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", 183 | "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", 184 | "cpu": [ 185 | "arm64" 186 | ], 187 | "dev": true, 188 | "license": "MIT", 189 | "optional": true, 190 | "os": [ 191 | "linux" 192 | ], 193 | "engines": { 194 | "node": ">=18" 195 | } 196 | }, 197 | "node_modules/@esbuild/linux-ia32": { 198 | "version": "0.25.0", 199 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", 200 | "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", 201 | "cpu": [ 202 | "ia32" 203 | ], 204 | "dev": true, 205 | "license": "MIT", 206 | "optional": true, 207 | "os": [ 208 | "linux" 209 | ], 210 | "engines": { 211 | "node": ">=18" 212 | } 213 | }, 214 | "node_modules/@esbuild/linux-loong64": { 215 | "version": "0.25.0", 216 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", 217 | "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", 218 | "cpu": [ 219 | "loong64" 220 | ], 221 | "dev": true, 222 | "license": "MIT", 223 | "optional": true, 224 | "os": [ 225 | "linux" 226 | ], 227 | "engines": { 228 | "node": ">=18" 229 | } 230 | }, 231 | "node_modules/@esbuild/linux-mips64el": { 232 | "version": "0.25.0", 233 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", 234 | "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", 235 | "cpu": [ 236 | "mips64el" 237 | ], 238 | "dev": true, 239 | "license": "MIT", 240 | "optional": true, 241 | "os": [ 242 | "linux" 243 | ], 244 | "engines": { 245 | "node": ">=18" 246 | } 247 | }, 248 | "node_modules/@esbuild/linux-ppc64": { 249 | "version": "0.25.0", 250 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", 251 | "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", 252 | "cpu": [ 253 | "ppc64" 254 | ], 255 | "dev": true, 256 | "license": "MIT", 257 | "optional": true, 258 | "os": [ 259 | "linux" 260 | ], 261 | "engines": { 262 | "node": ">=18" 263 | } 264 | }, 265 | "node_modules/@esbuild/linux-riscv64": { 266 | "version": "0.25.0", 267 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", 268 | "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", 269 | "cpu": [ 270 | "riscv64" 271 | ], 272 | "dev": true, 273 | "license": "MIT", 274 | "optional": true, 275 | "os": [ 276 | "linux" 277 | ], 278 | "engines": { 279 | "node": ">=18" 280 | } 281 | }, 282 | "node_modules/@esbuild/linux-s390x": { 283 | "version": "0.25.0", 284 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", 285 | "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", 286 | "cpu": [ 287 | "s390x" 288 | ], 289 | "dev": true, 290 | "license": "MIT", 291 | "optional": true, 292 | "os": [ 293 | "linux" 294 | ], 295 | "engines": { 296 | "node": ">=18" 297 | } 298 | }, 299 | "node_modules/@esbuild/linux-x64": { 300 | "version": "0.25.0", 301 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", 302 | "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", 303 | "cpu": [ 304 | "x64" 305 | ], 306 | "dev": true, 307 | "license": "MIT", 308 | "optional": true, 309 | "os": [ 310 | "linux" 311 | ], 312 | "engines": { 313 | "node": ">=18" 314 | } 315 | }, 316 | "node_modules/@esbuild/netbsd-arm64": { 317 | "version": "0.25.0", 318 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", 319 | "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", 320 | "cpu": [ 321 | "arm64" 322 | ], 323 | "dev": true, 324 | "license": "MIT", 325 | "optional": true, 326 | "os": [ 327 | "netbsd" 328 | ], 329 | "engines": { 330 | "node": ">=18" 331 | } 332 | }, 333 | "node_modules/@esbuild/netbsd-x64": { 334 | "version": "0.25.0", 335 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", 336 | "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", 337 | "cpu": [ 338 | "x64" 339 | ], 340 | "dev": true, 341 | "license": "MIT", 342 | "optional": true, 343 | "os": [ 344 | "netbsd" 345 | ], 346 | "engines": { 347 | "node": ">=18" 348 | } 349 | }, 350 | "node_modules/@esbuild/openbsd-arm64": { 351 | "version": "0.25.0", 352 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", 353 | "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", 354 | "cpu": [ 355 | "arm64" 356 | ], 357 | "dev": true, 358 | "license": "MIT", 359 | "optional": true, 360 | "os": [ 361 | "openbsd" 362 | ], 363 | "engines": { 364 | "node": ">=18" 365 | } 366 | }, 367 | "node_modules/@esbuild/openbsd-x64": { 368 | "version": "0.25.0", 369 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", 370 | "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", 371 | "cpu": [ 372 | "x64" 373 | ], 374 | "dev": true, 375 | "license": "MIT", 376 | "optional": true, 377 | "os": [ 378 | "openbsd" 379 | ], 380 | "engines": { 381 | "node": ">=18" 382 | } 383 | }, 384 | "node_modules/@esbuild/sunos-x64": { 385 | "version": "0.25.0", 386 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", 387 | "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", 388 | "cpu": [ 389 | "x64" 390 | ], 391 | "dev": true, 392 | "license": "MIT", 393 | "optional": true, 394 | "os": [ 395 | "sunos" 396 | ], 397 | "engines": { 398 | "node": ">=18" 399 | } 400 | }, 401 | "node_modules/@esbuild/win32-arm64": { 402 | "version": "0.25.0", 403 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", 404 | "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", 405 | "cpu": [ 406 | "arm64" 407 | ], 408 | "dev": true, 409 | "license": "MIT", 410 | "optional": true, 411 | "os": [ 412 | "win32" 413 | ], 414 | "engines": { 415 | "node": ">=18" 416 | } 417 | }, 418 | "node_modules/@esbuild/win32-ia32": { 419 | "version": "0.25.0", 420 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", 421 | "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", 422 | "cpu": [ 423 | "ia32" 424 | ], 425 | "dev": true, 426 | "license": "MIT", 427 | "optional": true, 428 | "os": [ 429 | "win32" 430 | ], 431 | "engines": { 432 | "node": ">=18" 433 | } 434 | }, 435 | "node_modules/@esbuild/win32-x64": { 436 | "version": "0.25.0", 437 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", 438 | "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", 439 | "cpu": [ 440 | "x64" 441 | ], 442 | "dev": true, 443 | "license": "MIT", 444 | "optional": true, 445 | "os": [ 446 | "win32" 447 | ], 448 | "engines": { 449 | "node": ">=18" 450 | } 451 | }, 452 | "node_modules/@types/colors": { 453 | "version": "1.2.4", 454 | "resolved": "https://registry.npmjs.org/@types/colors/-/colors-1.2.4.tgz", 455 | "integrity": "sha512-oSQxEVIDcYisAzWLa+wr50GSIPu8ml4PsKNJzgrDX3SmEHVBBqbaUurqsUceFauNlCRxNtENKkQm3yOe3m3nfg==", 456 | "deprecated": "This is a stub types definition. colors provides its own type definitions, so you do not need this installed.", 457 | "dev": true, 458 | "license": "MIT", 459 | "dependencies": { 460 | "colors": "*" 461 | } 462 | }, 463 | "node_modules/@types/minimatch": { 464 | "version": "5.1.2", 465 | "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", 466 | "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", 467 | "dev": true, 468 | "license": "MIT" 469 | }, 470 | "node_modules/@types/node": { 471 | "version": "16.18.126", 472 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", 473 | "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", 474 | "dev": true, 475 | "license": "MIT" 476 | }, 477 | "node_modules/@types/which": { 478 | "version": "3.0.4", 479 | "resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.4.tgz", 480 | "integrity": "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==", 481 | "dev": true, 482 | "license": "MIT" 483 | }, 484 | "node_modules/balanced-match": { 485 | "version": "1.0.2", 486 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 487 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 488 | "dev": true, 489 | "license": "MIT" 490 | }, 491 | "node_modules/brace-expansion": { 492 | "version": "2.0.1", 493 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 494 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 495 | "dev": true, 496 | "license": "MIT", 497 | "dependencies": { 498 | "balanced-match": "^1.0.0" 499 | } 500 | }, 501 | "node_modules/coc.nvim": { 502 | "version": "0.0.83-next.19", 503 | "resolved": "https://registry.npmjs.org/coc.nvim/-/coc.nvim-0.0.83-next.19.tgz", 504 | "integrity": "sha512-OuIti7VPDecpaumEx+2M7v6G0SCOIxskbMKPD3G0b3z+3lqx/fzP/WJvDdPJTJ6LvEimlmvIsRKWoyTncqhmHg==", 505 | "dev": true, 506 | "license": "MIT", 507 | "engines": { 508 | "node": ">=16.18.0" 509 | }, 510 | "funding": { 511 | "type": "opencollective", 512 | "url": "https://opencollective.com/cocnvim" 513 | }, 514 | "peerDependencies": { 515 | "@types/node": "^16.18.0" 516 | } 517 | }, 518 | "node_modules/colors": { 519 | "version": "1.4.0", 520 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", 521 | "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", 522 | "dev": true, 523 | "license": "MIT", 524 | "engines": { 525 | "node": ">=0.1.90" 526 | } 527 | }, 528 | "node_modules/esbuild": { 529 | "version": "0.25.0", 530 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", 531 | "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", 532 | "dev": true, 533 | "hasInstallScript": true, 534 | "license": "MIT", 535 | "bin": { 536 | "esbuild": "bin/esbuild" 537 | }, 538 | "engines": { 539 | "node": ">=18" 540 | }, 541 | "optionalDependencies": { 542 | "@esbuild/aix-ppc64": "0.25.0", 543 | "@esbuild/android-arm": "0.25.0", 544 | "@esbuild/android-arm64": "0.25.0", 545 | "@esbuild/android-x64": "0.25.0", 546 | "@esbuild/darwin-arm64": "0.25.0", 547 | "@esbuild/darwin-x64": "0.25.0", 548 | "@esbuild/freebsd-arm64": "0.25.0", 549 | "@esbuild/freebsd-x64": "0.25.0", 550 | "@esbuild/linux-arm": "0.25.0", 551 | "@esbuild/linux-arm64": "0.25.0", 552 | "@esbuild/linux-ia32": "0.25.0", 553 | "@esbuild/linux-loong64": "0.25.0", 554 | "@esbuild/linux-mips64el": "0.25.0", 555 | "@esbuild/linux-ppc64": "0.25.0", 556 | "@esbuild/linux-riscv64": "0.25.0", 557 | "@esbuild/linux-s390x": "0.25.0", 558 | "@esbuild/linux-x64": "0.25.0", 559 | "@esbuild/netbsd-arm64": "0.25.0", 560 | "@esbuild/netbsd-x64": "0.25.0", 561 | "@esbuild/openbsd-arm64": "0.25.0", 562 | "@esbuild/openbsd-x64": "0.25.0", 563 | "@esbuild/sunos-x64": "0.25.0", 564 | "@esbuild/win32-arm64": "0.25.0", 565 | "@esbuild/win32-ia32": "0.25.0", 566 | "@esbuild/win32-x64": "0.25.0" 567 | } 568 | }, 569 | "node_modules/isexe": { 570 | "version": "3.1.1", 571 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", 572 | "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", 573 | "dev": true, 574 | "license": "ISC", 575 | "engines": { 576 | "node": ">=16" 577 | } 578 | }, 579 | "node_modules/minimatch": { 580 | "version": "5.1.2", 581 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", 582 | "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", 583 | "dev": true, 584 | "license": "ISC", 585 | "dependencies": { 586 | "brace-expansion": "^2.0.1" 587 | }, 588 | "engines": { 589 | "node": ">=10" 590 | } 591 | }, 592 | "node_modules/typescript": { 593 | "version": "5.7.3", 594 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", 595 | "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", 596 | "dev": true, 597 | "license": "Apache-2.0", 598 | "bin": { 599 | "tsc": "bin/tsc", 600 | "tsserver": "bin/tsserver" 601 | }, 602 | "engines": { 603 | "node": ">=14.17" 604 | } 605 | }, 606 | "node_modules/which": { 607 | "version": "5.0.0", 608 | "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", 609 | "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", 610 | "dev": true, 611 | "license": "ISC", 612 | "dependencies": { 613 | "isexe": "^3.1.1" 614 | }, 615 | "bin": { 616 | "node-which": "bin/which.js" 617 | }, 618 | "engines": { 619 | "node": "^18.17.0 || >=20.5.0" 620 | } 621 | } 622 | } 623 | } 624 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coc-lists", 3 | "version": "1.5.4", 4 | "description": "Basic list sources for coc.nvim", 5 | "main": "lib/index.js", 6 | "publisher": "chemzqm", 7 | "module": "commonjs", 8 | "keywords": [ 9 | "coc.nvim", 10 | "list" 11 | ], 12 | "engines": { 13 | "coc": "^0.0.80" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/neoclide/coc-lists.git" 18 | }, 19 | "scripts": { 20 | "prepare": "node esbuild.js" 21 | }, 22 | "activationEvents": [ 23 | "*" 24 | ], 25 | "contributes": { 26 | "configuration": { 27 | "type": "object", 28 | "properties": { 29 | "session.directory": { 30 | "type": "string", 31 | "default": "", 32 | "description": "Directory for store session files, default to ~/.vim/sessions when empty." 33 | }, 34 | "session.restartOnSessionLoad": { 35 | "type": "boolean", 36 | "default": false, 37 | "description": "Restart vim with cwd changed on session load, support neovim on iTerm2 only." 38 | }, 39 | "session.saveOnVimLeave": { 40 | "type": "boolean", 41 | "default": true, 42 | "description": "Save session on VimLeavePre." 43 | }, 44 | "lists.disabledLists": { 45 | "type": "array", 46 | "default": [], 47 | "description": "List names to disable form load.", 48 | "items": { 49 | "type": "string" 50 | } 51 | }, 52 | "list.source.files.command": { 53 | "type": "string", 54 | "default": "", 55 | "description": "Command used for search for files" 56 | }, 57 | "list.source.files.args": { 58 | "type": "array", 59 | "default": [], 60 | "description": "Arguments for search command", 61 | "items": { 62 | "type": "string" 63 | } 64 | }, 65 | "list.source.files.excludePatterns": { 66 | "type": "array", 67 | "default": [], 68 | "description": "Minimatch patterns that should be excluded.", 69 | "items": { 70 | "type": "string" 71 | } 72 | }, 73 | "list.source.files.filterByName": { 74 | "type": "boolean", 75 | "default": false, 76 | "description": "Filter files by name only" 77 | }, 78 | "list.source.mru.maxLength": { 79 | "type": "number", 80 | "default": 1000, 81 | "description": "Max length of mru list." 82 | }, 83 | "list.source.mru.ignoreGitIgnore": { 84 | "type": "boolean", 85 | "default": false, 86 | "description": "Ignore git ignored files." 87 | }, 88 | "list.source.mru.excludePatterns": { 89 | "type": "array", 90 | "default": [ 91 | "**/.git/*", 92 | "/tmp/*" 93 | ], 94 | "description": "Minimatch patterns that should be excluded.", 95 | "items": { 96 | "type": "string" 97 | } 98 | }, 99 | "list.source.mru.filterByName": { 100 | "type": "boolean", 101 | "default": false, 102 | "description": "Filter files by name only" 103 | }, 104 | "list.source.grep.useLiteral": { 105 | "type": "boolean", 106 | "default": true, 107 | "description": "Use literal match unless specified regex options, default: true." 108 | }, 109 | "list.source.grep.command": { 110 | "type": "string", 111 | "default": "rg", 112 | "description": "Command used for grep, default to 'rg'.", 113 | "enum": [ 114 | "rg", 115 | "ag" 116 | ] 117 | }, 118 | "list.source.grep.maxColumns": { 119 | "type": "number", 120 | "default": 300, 121 | "description": "Don't print lines longer than this limit in bytes, ripgrep only." 122 | }, 123 | "list.source.grep.maxLines": { 124 | "type": "number", 125 | "default": 0, 126 | "description": "Limit the number of matching lines in interactive mode, 0 is not limited." 127 | }, 128 | "list.source.tags.command": { 129 | "type": "string", 130 | "default": "ctags -R .", 131 | "description": "Command used for generate tags." 132 | }, 133 | "list.source.grep.args": { 134 | "type": "array", 135 | "default": [], 136 | "description": "Arguments for grep command, always used for grep", 137 | "items": { 138 | "type": "string" 139 | } 140 | }, 141 | "list.source.grep.excludePatterns": { 142 | "type": "array", 143 | "default": [], 144 | "description": "Minimatch patterns of files that should be excluded, use .ignore file is recommended.", 145 | "items": { 146 | "type": "string" 147 | } 148 | } 149 | } 150 | }, 151 | "commands": [ 152 | { 153 | "command": "session.save", 154 | "title": "Save current vim state to session file." 155 | }, 156 | { 157 | "command": "session.load", 158 | "title": "Load session by name." 159 | }, 160 | { 161 | "command": "session.restart", 162 | "title": "Restart vim with current session." 163 | }, 164 | { 165 | "command": "mru.validate", 166 | "title": "Validate mru files list, which remove files that doesn't exist." 167 | } 168 | ] 169 | }, 170 | "author": "chemzqm@gmail.com", 171 | "license": "MIT", 172 | "devDependencies": { 173 | "@types/colors": "^1.2.4", 174 | "@types/minimatch": "^5.1.2", 175 | "@types/node": "16.18", 176 | "@types/which": "^3.0.4", 177 | "coc.nvim": "0.0.83-next.19", 178 | "colors": "^1.4.0", 179 | "esbuild": "^0.25.0", 180 | "minimatch": "5.1.2", 181 | "typescript": "^5.7.3", 182 | "which": "^5.0.0" 183 | }, 184 | "dependencies": {} 185 | } 186 | -------------------------------------------------------------------------------- /src/buffers.ts: -------------------------------------------------------------------------------- 1 | import { BasicList, ListContext, ListItem, Neovim } from 'coc.nvim' 2 | import colors from 'colors/safe' 3 | const regex = /^\s*(\d+)(.+?)"(.+?)".*/ 4 | 5 | export default class BufferList extends BasicList { 6 | public readonly name = 'buffers' 7 | public readonly defaultAction = 'open' 8 | public description = 'get buffer list' 9 | 10 | constructor(nvim: Neovim) { 11 | super(nvim) 12 | this.addAction('open', async (item: ListItem) => { 13 | let { bufnr } = item.data 14 | await nvim.command(`buffer ${bufnr}`) 15 | }) 16 | this.addAction('drop', async (item: ListItem) => { 17 | let { bufnr, visible } = item.data 18 | if (visible) { 19 | let info = await nvim.call('getbufinfo', bufnr) as any[] 20 | if (info.length && info[0].windows.length) { 21 | let winid = info[0].windows[0] 22 | await nvim.call('win_gotoid', winid) 23 | return 24 | } 25 | } 26 | await nvim.command(`buffer ${bufnr}`) 27 | }) 28 | this.addAction('split', async (item: ListItem) => { 29 | let { bufnr } = item.data 30 | await nvim.command(`sb ${bufnr}`) 31 | }) 32 | this.addAction('tabe', async (item: ListItem) => { 33 | let { bufname } = item.data 34 | let escaped = await nvim.call('fnameescape', bufname) 35 | await nvim.command(`tabe ${escaped}`) 36 | }) 37 | this.addAction('vsplit', async (item: ListItem) => { 38 | let { bufname } = item.data 39 | let escaped = await nvim.call('fnameescape', bufname) 40 | await nvim.command(`vs ${escaped}`) 41 | }) 42 | 43 | // unload buffer 44 | this.addAction('delete', async item => { 45 | let { bufnr, bufname, isArgs } = item.data 46 | await nvim.command(`bdelete ${bufnr}`) 47 | if (isArgs) { 48 | await nvim.command(`argdelete ${bufname}`) 49 | } 50 | }, { persist: true, reload: true }) 51 | 52 | this.addAction('wipe', async item => { 53 | let { bufnr } = item.data 54 | await nvim.command(`bwipeout ${bufnr}`) 55 | }, { persist: true, reload: true }) 56 | 57 | this.addAction('preview', async (item, context) => { 58 | let { nvim } = this 59 | let { bufname, bufnr } = item.data 60 | let lines = await nvim.call('getbufline', [bufnr, 1, 200]) as string[] 61 | let filetype = await nvim.call('getbufvar', [bufnr, '&filetype', 'txt']) as string 62 | if (lines.length == 0) { 63 | lines = [`Unable to get lines for buffer ${bufname}, add 'set hidden' in your vimrc.`] 64 | } 65 | await this.preview({ 66 | filetype, 67 | bufname, 68 | lines, 69 | sketch: true 70 | }, context) 71 | }) 72 | } 73 | 74 | public async loadItems(context: ListContext): Promise { 75 | const { nvim } = this 76 | const bufnrAlt = Number(await nvim.call('bufnr', '#')) 77 | const content = await nvim.call('execute', 'ls') as string 78 | const isArgs = context.args.indexOf('--args') !== -1 79 | const isPWD = context.args.indexOf('--pwd') !== -1 80 | const args = isArgs ? await nvim.eval("map(argv(), 'bufnr(v:val)')") as number[] : [] 81 | 82 | return content.split(/\n/).reduce((res, line) => { 83 | const ms = line.match(regex) 84 | if (!ms) return res 85 | const bufnr = Number(ms[1]) 86 | const bufname = ms[3] 87 | if (isArgs && args.indexOf(bufnr) === -1) return res 88 | if (isPWD && (bufname[0] === '/' || bufname[0] === '~')) return res 89 | 90 | const item = { 91 | label: ` ${colors.magenta(ms[1])}${colors.america(ms[2])}${ms[3]}`, 92 | filterText: bufname, 93 | sortText: ms[1], 94 | data: { 95 | bufnr, 96 | bufname, 97 | visible: ms[2].indexOf('a') !== -1, 98 | isArgs, 99 | } 100 | } as ListItem 101 | 102 | return bufnr === bufnrAlt 103 | ? [item, ...res] 104 | : [...res, item] 105 | }, []) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/changes.ts: -------------------------------------------------------------------------------- 1 | import { IList, ListAction, ListContext, ListItem, Neovim } from 'coc.nvim' 2 | import colors from 'colors/safe' 3 | const regex = /^[^\d]*(\d+)[^\d]+(\d+)[^\d]+(\d+)[^\w]+(.*)$/ 4 | 5 | export default class ChangeList implements IList { 6 | public readonly name = 'changes' 7 | public readonly description = 'changes list' 8 | public readonly defaultAction = 'jump' 9 | public actions: ListAction[] = [] 10 | 11 | constructor(private nvim: Neovim) { 12 | this.actions.push({ 13 | name: 'jump', 14 | execute: item => { 15 | if (Array.isArray(item)) return 16 | nvim.command(`normal! ${item.data.lnum}G${item.data.col}|zz`, true) 17 | } 18 | }) 19 | } 20 | 21 | public async loadItems(_context: ListContext): Promise { 22 | const { nvim } = this 23 | let list = await nvim.eval('split(execute("changes"), "\n")') as string[] 24 | list = list.slice(1) 25 | list = list.filter(s => regex.test(s)) 26 | 27 | return list.map(line => { 28 | let [, change, lnum, col, text] = line.match(regex) 29 | return { 30 | label: `${colors.magenta(change)}\t${colors.grey(lnum)}\t${colors.grey(col)}\t${text}`, 31 | filterText: text, 32 | data: { lnum, col } 33 | } 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/cmdhistory.ts: -------------------------------------------------------------------------------- 1 | import { IList, ListAction, ListContext, ListItem, Neovim } from 'coc.nvim' 2 | 3 | const regex = /^\s*\d+\s+(.*)$/ 4 | 5 | export default class Cmdhistory implements IList { 6 | public readonly name = 'cmdhistory' 7 | public readonly description = 'history of vim commands' 8 | public readonly defaultAction = 'execute' 9 | public actions: ListAction[] = [] 10 | 11 | constructor(private nvim: Neovim) { 12 | this.actions.push({ 13 | name: 'execute', 14 | execute: async item => { 15 | if (Array.isArray(item)) return 16 | nvim.command(item.data.cmd, true) 17 | } 18 | }) 19 | } 20 | 21 | public async loadItems(_context: ListContext): Promise { 22 | let { nvim } = this 23 | let list = await nvim.eval(`split(execute("history cmd"),"\n")`) as string[] 24 | list = list.slice(1) 25 | let res: ListItem[] = [] 26 | for (let line of list) { 27 | let ms = line.match(regex) 28 | if (ms) { 29 | let [, cmd] = ms 30 | res.push({ 31 | label: cmd, 32 | filterText: cmd, 33 | data: { 34 | cmd 35 | } 36 | }) 37 | } 38 | } 39 | return res 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/colors.ts: -------------------------------------------------------------------------------- 1 | import { BasicList, ListAction, ListContext, ListItem, Neovim, Uri } from 'coc.nvim' 2 | import path from 'path' 3 | 4 | export default class Colors extends BasicList { 5 | public readonly name = 'colors' 6 | public readonly description = 'color schemes' 7 | public readonly defaultAction = 'set' 8 | public actions: ListAction[] = [] 9 | 10 | constructor(nvim: Neovim) { 11 | super(nvim) 12 | this.addLocationActions() 13 | this.addAction('set', item => { 14 | if (Array.isArray(item)) return 15 | nvim.command(`colorscheme ${item.data.name}`, true) 16 | }) 17 | } 18 | 19 | public async loadItems(_context: ListContext): Promise { 20 | let { nvim } = this 21 | let colors = await nvim.eval('split(globpath(&rtp, "colors/*.vim"),"\n")') as string[] 22 | let hasPackages = await nvim.call('has', ['packages']) 23 | if (hasPackages) { 24 | let packageColors = await nvim.eval('split(globpath(&packpath, "pack/*/opt/*/colors/*.vim"),"\n")') as string[] 25 | colors.push(...packageColors) 26 | } 27 | return colors.map(file => { 28 | let name = path.basename(file, '.vim') 29 | return { 30 | label: `${name}\t${file}`, 31 | filterText: name, 32 | data: { name }, 33 | location: Uri.file(file).toString() 34 | } 35 | }) 36 | } 37 | 38 | public doHighlight(): void { 39 | let { nvim } = this 40 | nvim.pauseNotification() 41 | nvim.command('syntax match CocColorsName /\\v^[^\\t]+/ contained containedin=CocColorsLine', true) 42 | nvim.command('syntax match CocColorsFile /\\t.*$/ contained containedin=CocColorsLine', true) 43 | nvim.command('highlight default link CocColorsName Identifier', true) 44 | nvim.command('highlight default link CocColorsFile Comment', true) 45 | nvim.resumeNotification(false, true) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | import { IList, ListAction, ListContext, ListItem, Neovim } from 'coc.nvim' 2 | import colors from 'colors/safe' 3 | import fs from 'fs' 4 | import util from 'util' 5 | import { pad } from './util/index' 6 | 7 | const regex = /^\|:(\S+)\|\t(\S+)\t(.*)$/ 8 | 9 | export default class Commands implements IList { 10 | public readonly name = 'vimcommands' 11 | public readonly description = 'command list' 12 | public readonly defaultAction = 'execute' 13 | public actions: ListAction[] = [] 14 | 15 | constructor(private nvim: Neovim) { 16 | this.actions.push({ 17 | name: 'execute', 18 | execute: async item => { 19 | if (Array.isArray(item)) return 20 | let { command, shabang, hasArgs } = item.data 21 | if (!hasArgs) { 22 | nvim.command(command, true) 23 | } else { 24 | const feedableCommand = `:${command}${shabang ? '' : ' '}` 25 | const mode = await nvim.call('mode') 26 | const isInsertMode = mode.startsWith('i') 27 | if (isInsertMode) { 28 | // For some reason `nvim.feedkeys` doesn't support CSI escapes, even though the 29 | // docs say it should. So we force the escape here with double backslashes. 30 | nvim.command(`call feedkeys("\\${feedableCommand}", 'n')`, true) 31 | } else { 32 | await nvim.feedKeys(feedableCommand, 'n', true) 33 | } 34 | } 35 | } 36 | }) 37 | this.actions.push({ 38 | name: 'open', 39 | execute: async item => { 40 | if (Array.isArray(item)) return 41 | let { command } = item.data 42 | if (!/^[A-Z]/.test(command)) return 43 | let res = await nvim.eval(`split(execute("verbose command ${command}"),"\n")[2]`) as string 44 | if (/Last\sset\sfrom/.test(res)) { 45 | let [filepath, _ ,line] = res.replace(/^\s+Last\sset\sfrom\s+/, '').split(/\s+/) 46 | if (line) { 47 | nvim.command(`edit +${line} ${filepath}`, true) 48 | } else { 49 | nvim.command(`edit +/${command} ${filepath}`, true) 50 | } 51 | } 52 | } 53 | }) 54 | } 55 | 56 | public async loadItems(_context: ListContext): Promise { 57 | let { nvim } = this 58 | let list = await nvim.eval('split(execute("command"),"\n")') as string[] 59 | list = list.slice(1) 60 | let res: ListItem[] = [] 61 | for (let str of list) { 62 | let name = str.slice(4).match(/\S+/)[0] 63 | let end = str.slice(4 + name.length) 64 | res.push({ 65 | label: str.slice(0, 4) + colors.magenta(name) + end, 66 | filterText: name, 67 | data: { 68 | command: name, 69 | shabang: str.startsWith('!'), 70 | hasArgs: !end.match(/^\s*0\s/) 71 | } 72 | }) 73 | } 74 | let help = await nvim.eval(`globpath($VIMRUNTIME, 'doc/index.txt')`) as string 75 | if (help && fs.existsSync(help)) { 76 | let content = await util.promisify(fs.readFile)(help, 'utf8') 77 | for (let line of content.split(/\r?\n/)) { 78 | let ms = line.match(regex) 79 | if (ms) { 80 | let [, cmd, chars, description] = ms 81 | res.push({ 82 | label: ` ${colors.magenta(cmd)}${pad(cmd, 20)}${chars}${pad(chars, 30)}${description}`, 83 | filterText: cmd, 84 | data: { 85 | command: cmd, 86 | shabang: false, 87 | hasArgs: true 88 | } 89 | }) 90 | } 91 | } 92 | } 93 | return res 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/files.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, spawn } from 'child_process' 2 | import { BasicList, ListContext, ListTask, Location, Neovim, Range, Uri, workspace } from 'coc.nvim' 3 | import { EventEmitter } from 'events' 4 | import fs from 'fs' 5 | import minimatch from 'minimatch' 6 | import path from 'path' 7 | import readline from 'readline' 8 | import { executable } from './util' 9 | 10 | class Task extends EventEmitter implements ListTask { 11 | private processes: ChildProcess[] = [] 12 | 13 | public start(cmd: string, args: string[], cwds: string[], patterns: string[]): void { 14 | let remain = cwds.length 15 | let config = workspace.getConfiguration('list.source.files') 16 | let filterByName = config.get('filterByName', false) 17 | for (let cwd of cwds) { 18 | let process = spawn(cmd, args, { cwd }) 19 | this.processes.push(process) 20 | process.on('error', e => { 21 | this.emit('error', e.message) 22 | }) 23 | const rl = readline.createInterface(process.stdout) 24 | const range = Range.create(0, 0, 0, 0) 25 | let hasPattern = patterns.length > 0 26 | process.stderr.on('data', chunk => { 27 | console.error(chunk.toString('utf8')) // tslint:disable-line 28 | }) 29 | 30 | rl.on('line', line => { 31 | let file = line 32 | if (file.indexOf(cwd) < 0) { 33 | file = path.join(cwd, line) 34 | } 35 | if (hasPattern && patterns.some(p => minimatch(file, p))) return 36 | let location = Location.create(Uri.file(file).toString(), range) 37 | if (!filterByName) { 38 | this.emit('data', { 39 | label: line, 40 | sortText: file, 41 | location 42 | }) 43 | } else { 44 | let name = path.basename(file) 45 | this.emit('data', { 46 | label: `${name}\t${line}`, 47 | sortText: file, 48 | filterText: name, 49 | location 50 | }) 51 | } 52 | }) 53 | rl.on('close', () => { 54 | remain = remain - 1 55 | if (remain == 0) { 56 | this.emit('end') 57 | } 58 | }) 59 | } 60 | } 61 | 62 | public dispose(): void { 63 | for (let process of this.processes) { 64 | if (!process.killed) { 65 | process.kill() 66 | } 67 | } 68 | } 69 | } 70 | 71 | export default class FilesList extends BasicList { 72 | public readonly name = 'files' 73 | public readonly defaultAction = 'open' 74 | public description = 'Search files by rg or ag' 75 | public readonly detail = `Install ripgrep in your $PATH to have best experience. 76 | Files is searched from current cwd by default. 77 | Provide directory names as arguments to search other directories. 78 | Use 'list.source.files.command' configuration for custom search command. 79 | Use 'list.source.files.args' configuration for custom command arguments. 80 | Note that rg ignore hidden files by default.` 81 | public options = [{ 82 | name: '-F, -folder', 83 | description: 'Search files from current workspace folder instead of cwd.' 84 | }, { 85 | name: '-W, -workspace', 86 | description: 'Search files from all workspace folders instead of cwd.' 87 | }] 88 | 89 | constructor(nvim: Neovim) { 90 | super(nvim) 91 | this.addLocationActions() 92 | } 93 | 94 | private getArgs(args: string[], defaultArgs: string[]): string[] { 95 | return args.length ? args : defaultArgs 96 | } 97 | 98 | public getCommand(): { cmd: string, args: string[] } { 99 | let config = workspace.getConfiguration('list.source.files') 100 | let cmd = config.get('command', '') 101 | let args = config.get('args', []) 102 | if (!cmd) { 103 | if (executable('rg')) { 104 | return { cmd: 'rg', args: this.getArgs(args, ['--color', 'never', '--files']) } 105 | } else if (executable('ag')) { 106 | return { cmd: 'ag', args: this.getArgs(args, ['-f', '-g', '.', '--nocolor']) } 107 | } else if (process.platform == 'win32') { 108 | return { cmd: 'dir', args: this.getArgs(args, ['/a-D', '/S', '/B']) } 109 | } else if (executable('find')) { 110 | return { cmd: 'find', args: this.getArgs(args, ['.', '-type', 'f']) } 111 | } else { 112 | throw new Error('Unable to find command for files list.') 113 | return null 114 | } 115 | } else { 116 | return { cmd, args } 117 | } 118 | } 119 | 120 | public async loadItems(context: ListContext): Promise { 121 | let { nvim } = this 122 | let { window, args } = context 123 | let options = this.parseArguments(args) 124 | let res = this.getCommand() 125 | if (!res) return null 126 | let used = res.args.concat(['-F', '-folder', '-W', '-workspace']) 127 | let extraArgs = args.filter(s => used.indexOf(s) == -1) 128 | let cwds: string[] 129 | let dirArgs = [] 130 | let searchArgs = [] 131 | if (options.folder) { 132 | cwds = [workspace.rootPath] 133 | } else if (options.workspace) { 134 | cwds = workspace.workspaceFolders.map(f => Uri.parse(f.uri).fsPath) 135 | } else { 136 | if (extraArgs.length > 0) { 137 | // tslint:disable-next-line: prefer-for-of 138 | for (let i = 0; i < extraArgs.length; i++) { 139 | let d = await nvim.call('expand', extraArgs[i]) 140 | try { 141 | if (fs.lstatSync(d).isDirectory()) { 142 | dirArgs.push(d) 143 | } else { 144 | searchArgs.push(d) 145 | } 146 | } catch (e) { 147 | searchArgs.push(d) 148 | } 149 | } 150 | } 151 | if (dirArgs.length > 0) { 152 | cwds = dirArgs 153 | } else { 154 | let valid = await window.valid 155 | if (valid) { 156 | cwds = [await nvim.call('getcwd', window.id)] 157 | } else { 158 | cwds = [await nvim.call('getcwd')] 159 | } 160 | } 161 | } 162 | let task = new Task() 163 | let excludePatterns = this.getConfig().get('excludePatterns', []) 164 | task.start(res.cmd, res.args.concat(searchArgs), cwds, excludePatterns) 165 | return task 166 | } 167 | 168 | public doHighlight(): void { 169 | let config = workspace.getConfiguration('list.source.files') 170 | let filterByName = config.get('filterByName', false) 171 | if (filterByName) { 172 | let { nvim } = this 173 | nvim.pauseNotification() 174 | nvim.command('syntax match CocFilesFile /\\t.*$/ contained containedin=CocFilesLine', true) 175 | nvim.command('highlight default link CocFilesFile Comment', true) 176 | nvim.resumeNotification(false, true) 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/filetypes.ts: -------------------------------------------------------------------------------- 1 | import { IList, ListContext, ListItem, Neovim, ListAction } from 'coc.nvim' 2 | import { distinct } from './util' 3 | 4 | export default class Filetypes implements IList { 5 | public readonly name = 'filetypes' 6 | public readonly description = 'available filetypes' 7 | public readonly defaultAction = 'set' 8 | public actions: ListAction[] = [] 9 | 10 | constructor(private nvim: Neovim) { 11 | this.actions.push({ 12 | name: 'set', 13 | execute: item => { 14 | if (Array.isArray(item)) { 15 | for (let i of item) { 16 | nvim.command(`setf ${i.label}`, true) 17 | } 18 | } else { 19 | nvim.command(`setf ${item.label}`, true) 20 | } 21 | } 22 | }) 23 | } 24 | 25 | public async loadItems(_context: ListContext): Promise { 26 | let { nvim } = this 27 | let filetypes = await nvim.eval(`sort(map(split(globpath(&rtp, 'syntax/*.vim'), '\n'),'fnamemodify(v:val, ":t:r")'))`) as string[] 28 | filetypes = distinct(filetypes) 29 | return filetypes.map(filetype => { 30 | return { label: filetype } 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/functions.ts: -------------------------------------------------------------------------------- 1 | import { IList, ListAction, ListContext, ListItem, Neovim } from 'coc.nvim' 2 | import colors from 'colors/safe' 3 | 4 | export default class Functions implements IList { 5 | public readonly name = 'functions' 6 | public readonly description = 'function list' 7 | public readonly defaultAction = 'open' 8 | public actions: ListAction[] = [] 9 | 10 | constructor(private nvim: Neovim) { 11 | this.actions.push({ 12 | name: 'open', 13 | execute: async item => { 14 | if (Array.isArray(item)) return 15 | let { funcname } = item.data 16 | let res = await nvim.eval(`split(execute("verbose function ${funcname}"),"\n")[1]`) as string 17 | 18 | let [filepath, _ ,line] = res.replace(/^\s+Last\sset\sfrom\s+/, '').split(/\s+/) 19 | if (line) { 20 | nvim.command(`edit +${line} ${filepath}`, true) 21 | } else { 22 | nvim.command(`edit +/${funcname} ${filepath}`, true) 23 | } 24 | } 25 | }) 26 | } 27 | 28 | public async loadItems(_context: ListContext): Promise { 29 | let { nvim } = this 30 | let list = await nvim.eval('split(execute("function"),"\n")') as string[] 31 | list = list.slice(1) 32 | let res: ListItem[] = [] 33 | for (let str of list) { 34 | let name = str.slice(8).split(/\(/)[0] 35 | let end = str.slice(8 + name.length) 36 | res.push({ 37 | label: str.slice(0, 8) + colors.magenta(name) + end, 38 | filterText: name, 39 | data: { 40 | funcname: name 41 | } 42 | }) 43 | } 44 | return res 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/grep.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, spawn } from 'child_process' 2 | import { BasicList, ListContext, ListItem, ListTask, Neovim, workspace } from 'coc.nvim' 3 | import { EventEmitter } from 'events' 4 | import minimatch from 'minimatch' 5 | import path from 'path' 6 | import readline from 'readline' 7 | import { Uri as URI, } from 'coc.nvim' 8 | import { executable } from './util' 9 | import { ansiparse } from './util/ansiparse' 10 | import { convertOptions } from './util/option' 11 | 12 | const lineRegex = /^(.+?):(\d+):(\d+):(.*)/ 13 | const controlCode = '\x1b' 14 | 15 | class Task extends EventEmitter implements ListTask { 16 | private processes: ChildProcess[] = [] 17 | private lines: number = 0 18 | constructor(private interactive: boolean) { 19 | super() 20 | } 21 | 22 | public start(text: string, cmd: string, args: string[], cwds: string[], patterns: string[], maxLines: number): void { 23 | for (let cwd of cwds) { 24 | let remain = cwds.length 25 | let process = spawn(cmd, args, { cwd }) 26 | process.on('error', e => { 27 | this.emit('error', e.message) 28 | }) 29 | process.stderr.on('data', chunk => { 30 | let parts = ansiparse(chunk.toString('utf8')) 31 | let escaped = parts.reduce((s, curr) => s + curr.text, '') 32 | console.error(escaped) // tslint:disable-line 33 | }) 34 | const rl = readline.createInterface(process.stdout) 35 | let hasPattern = patterns.length > 0 36 | rl.on('line', line => { 37 | if (this.interactive && (maxLines > 0) && (this.lines >= maxLines)) return 38 | let ms: RegExpMatchArray 39 | let escaped: string 40 | if (line.indexOf(controlCode) !== -1) { 41 | let parts = ansiparse(line) 42 | escaped = parts.reduce((s, curr) => s + curr.text, '') 43 | ms = escaped.match(lineRegex) 44 | } else { 45 | ms = line.match(lineRegex) 46 | escaped = line 47 | } 48 | if (!ms) return 49 | let file = ms[1] 50 | if (!path.isAbsolute(file)) { 51 | file = path.join(cwd, ms[1]) 52 | } 53 | if (hasPattern && patterns.some(p => minimatch(file, p))) return 54 | this.emit('data', { 55 | label: line, 56 | filterText: this.interactive ? '' : escaped, 57 | location: { 58 | text, 59 | uri: file, 60 | line: ms[4], 61 | } 62 | }) 63 | this.lines++; 64 | }) 65 | rl.on('close', () => { 66 | remain = remain - 1 67 | if (remain == 0) { 68 | this.emit('end') 69 | } 70 | }) 71 | } 72 | } 73 | 74 | public dispose(): void { 75 | for (let process of this.processes) { 76 | if (!process.killed) { 77 | process.kill() 78 | } 79 | } 80 | } 81 | } 82 | 83 | export default class GrepList extends BasicList { 84 | public readonly interactive = true 85 | public readonly description = 'grep text by rg or ag' 86 | public readonly name = 'grep' 87 | public readonly defaultAction = 'open' 88 | public readonly detail = `Literal match is used by default. 89 | To use interactive mode, add '-I' or '--interactive' to LIST OPTIONS. 90 | To change colors, checkout 'man rg' or 'man ag'. 91 | To search from workspace folders instead of cwd, use '-folder' or '-workspace' argument. 92 | Grep source provide some uniformed options to ease differences between rg and ag.` 93 | public options = [{ 94 | name: '-S, -smartcase', 95 | description: 'Use smartcase match.' 96 | }, { 97 | name: '-i, -ignorecase', 98 | description: 'Use ignorecase match.' 99 | }, { 100 | name: '-l, -literal', 101 | description: 'Treat the pattern as a literal string, used when -regex is not used.' 102 | }, { 103 | name: '-w, -word', 104 | description: 'Use word match.' 105 | }, { 106 | name: '-e, -regex', 107 | description: 'Use regex match.' 108 | }, { 109 | name: '-u, -skip-vcs-ignores', 110 | description: 'Don\'t respect version control ignore files(.gitignore, etc.)' 111 | }, { 112 | name: '-t, -extension EXTENSION', 113 | description: 'Grep files with specified extension only, could be used multiple times.', 114 | hasValue: true 115 | }, { 116 | name: '-F, -folder', 117 | description: 'Grep files from current workspace folder instead of cwd.' 118 | }, { 119 | name: '-W, -workspace', 120 | description: 'Grep files from all workspace folders instead of cwd.' 121 | }] 122 | 123 | constructor(nvim: Neovim) { 124 | super(nvim) 125 | this.addLocationActions() 126 | } 127 | 128 | public async loadItems(context: ListContext): Promise { 129 | let { nvim } = this 130 | let { interactive } = context.options 131 | let config = workspace.getConfiguration('list.source.grep') 132 | let cmd = config.get('command', 'rg') 133 | let args = config.get('args', []).slice() 134 | let useLiteral = config.get('useLiteral', true) 135 | let maxLines = config.get("maxLines", 0) 136 | if (cmd == 'rg') { 137 | let maxColumns = config.get('maxColumns', 300) 138 | args.push('--color', 'always', '--max-columns', maxColumns.toString(), '--vimgrep') 139 | } else if (cmd == 'ag') { 140 | args.push('--color', '--vimgrep') 141 | } 142 | if (!executable(cmd)) throw new Error(`Command '${cmd}' not found on $PATH`) 143 | if (interactive && !context.input) return [] 144 | args.push(...context.args) 145 | if (context.input) { 146 | if (interactive && context.input.indexOf(' ') != -1) { 147 | let input = context.input.split(/\s+/).join('.*') 148 | if (!args.includes('-regex') && !args.includes('-e')) { 149 | args.push('-regex') 150 | } 151 | args.push(input) 152 | } else { 153 | args.push(context.input) 154 | } 155 | } 156 | 157 | let patterns = config.get('excludePatterns', []) 158 | let { window } = context 159 | let cwds: string[] 160 | if (args.indexOf('-F') != -1 || args.indexOf('-folder') != -1) { 161 | cwds = [workspace.rootPath] 162 | } else if (args.indexOf('-W') != -1 || args.indexOf('-workspace') != -1) { 163 | cwds = workspace.workspaceFolders.map(f => URI.parse(f.uri).fsPath) 164 | } else { 165 | let valid = await window.valid 166 | if (valid) { 167 | cwds = [await nvim.call('getcwd', window.id)] 168 | } else { 169 | cwds = [await nvim.call('getcwd')] 170 | } 171 | } 172 | let task = new Task(interactive) 173 | if (cmd == 'rg' || cmd == 'ag') { 174 | args = convertOptions(args, cmd, useLiteral) 175 | args = args.filter(s => ['-F', '-folder', '-W', '-workspace'].indexOf(s) == -1) 176 | } 177 | 178 | let text = '' 179 | if (!interactive) { 180 | // no text in interactive mode, no text if search item contains regex 181 | const regexPattern = /[\\^$.|?*+(){}[\]]/; 182 | const last = args.slice(-1)[0] 183 | if (!regexPattern.test(last)) { 184 | text = last 185 | } 186 | } 187 | if (!args.includes('--')) { 188 | args.push('--', './') 189 | } 190 | task.start(text, cmd, args, cwds, patterns, maxLines) 191 | return task 192 | } 193 | } 194 | 195 | function byteSlice(content: string, start: number, end?: number): string { 196 | let buf = Buffer.from(content, 'utf8') 197 | return buf.slice(start, end).toString('utf8') 198 | } 199 | -------------------------------------------------------------------------------- /src/helptags.ts: -------------------------------------------------------------------------------- 1 | import { BasicList, Uri, ListContext, ListItem, Neovim, workspace } from 'coc.nvim' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import { isParentFolder } from './util' 5 | 6 | export default class Helptags extends BasicList { 7 | public readonly name = 'helptags' 8 | public readonly description = 'helptags of vim' 9 | public readonly defaultAction = 'show' 10 | 11 | constructor(nvim: Neovim) { 12 | super(nvim) 13 | this.addAction('show', item => { 14 | nvim.command(`help ${item.data.name}`, true) 15 | }) 16 | this.addLocationActions() 17 | } 18 | 19 | public async loadItems(_context: ListContext): Promise { 20 | let rtp = workspace.env.runtimepath 21 | if (!rtp) return [] 22 | let folders = rtp.split(',') 23 | let result: ListItem[] = [] 24 | let cwd = await this.nvim.call('getcwd') 25 | await Promise.all(folders.map(folder => { 26 | return new Promise(resolve => { 27 | let file = path.join(folder, 'doc/tags') 28 | fs.readFile(file, 'utf8', (err, content) => { 29 | if (err) return resolve() 30 | let lines = content.split(/\r?\n/) 31 | for (let line of lines) { 32 | if (line) { 33 | let [name, filepath, regex] = line.split('\t') 34 | let fullpath = path.join(folder, 'doc', filepath) 35 | let uri = Uri.file(fullpath).toString() 36 | let file = isParentFolder(cwd, fullpath) ? path.relative(cwd, fullpath) : fullpath 37 | result.push({ 38 | label: `${name}\t${file}`, 39 | filterText: name, 40 | location: { 41 | uri, 42 | line: regex.replace(/^\//, '').replace(/\$\//, ''), 43 | text: name 44 | }, 45 | data: { name } 46 | }) 47 | } 48 | } 49 | resolve() 50 | }) 51 | }) 52 | })) 53 | return result 54 | } 55 | 56 | public doHighlight(): void { 57 | let { nvim } = this 58 | nvim.pauseNotification() 59 | nvim.command('syntax match CocHelptagsName /\\v^[^\\t]+/ contained containedin=CocHelptagsLine', true) 60 | nvim.command('syntax match CocHelptagsFile /\\t.*$/ contained containedin=CocHelptagsLine', true) 61 | nvim.command('highlight default link CocHelptagsName Identifier', true) 62 | nvim.command('highlight default link CocHelptagsFile Comment', true) 63 | nvim.resumeNotification(false, true) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, listManager, workspace } from 'coc.nvim' 2 | import BufferList from './buffers' 3 | import ChangeList from './changes' 4 | import Cmdhistory from './cmdhistory' 5 | import Colors from './colors' 6 | import Commands from './commands' 7 | import FilesList from './files' 8 | import Filetypes from './filetypes' 9 | import GrepList from './grep' 10 | import Helptags from './helptags' 11 | import LinesList from './lines' 12 | import LocationList from './locationlist' 13 | import Maps from './maps' 14 | import Marks from './marks' 15 | import MruList from './mru' 16 | import QuickfixList from './quickfix' 17 | import RegisterList from './registers' 18 | import Searchhistory from './searchhistory' 19 | import SessionList from './session' 20 | import Tags from './tags' 21 | import Windows from './windows' 22 | import Words from './words' 23 | import Functions from './functions' 24 | 25 | export async function activate(context: ExtensionContext): Promise { 26 | let { subscriptions } = context 27 | let config = workspace.getConfiguration(undefined, null) 28 | let disabled = config.get('lists.disabledLists', []) 29 | let { nvim } = workspace 30 | 31 | function isDisabled(name): boolean { 32 | return disabled.indexOf(name) !== -1 33 | } 34 | if (!isDisabled('lines')) { 35 | subscriptions.push(listManager.registerList(new LinesList(nvim))) 36 | } 37 | if (!isDisabled('session')) { 38 | let saveOnVimLeave = config.get('session.saveOnVimLeave', true) 39 | subscriptions.push(listManager.registerList(new SessionList(nvim, context.extensionPath, saveOnVimLeave))) 40 | } 41 | if (!isDisabled('cmdhistory')) { 42 | subscriptions.push(listManager.registerList(new Cmdhistory(nvim))) 43 | } 44 | if (!isDisabled('searchhistory')) { 45 | subscriptions.push(listManager.registerList(new Searchhistory(nvim))) 46 | } 47 | if (!isDisabled('vimcommands')) { 48 | subscriptions.push(listManager.registerList(new Commands(nvim))) 49 | } 50 | if (!isDisabled('maps')) { 51 | subscriptions.push(listManager.registerList(new Maps(nvim))) 52 | } 53 | if (!isDisabled('colors')) { 54 | subscriptions.push(listManager.registerList(new Colors(nvim))) 55 | } 56 | if (!isDisabled('windows')) { 57 | subscriptions.push(listManager.registerList(new Windows(nvim))) 58 | } 59 | if (!isDisabled('marks')) { 60 | subscriptions.push(listManager.registerList(new Marks(nvim))) 61 | } 62 | if (!isDisabled('filetypes')) { 63 | subscriptions.push(listManager.registerList(new Filetypes(nvim))) 64 | } 65 | if (!isDisabled('files')) { 66 | subscriptions.push(listManager.registerList(new FilesList(nvim))) 67 | } 68 | if (!isDisabled('tags')) { 69 | subscriptions.push(listManager.registerList(new Tags(nvim))) 70 | } 71 | if (!isDisabled('helptags')) { 72 | subscriptions.push(listManager.registerList(new Helptags(nvim))) 73 | } 74 | if (!isDisabled('registers')) { 75 | subscriptions.push(listManager.registerList(new RegisterList(nvim))) 76 | } 77 | if (!isDisabled('buffers')) { 78 | subscriptions.push(listManager.registerList(new BufferList(nvim))) 79 | } 80 | if (!isDisabled('changes')) { 81 | subscriptions.push(listManager.registerList(new ChangeList(nvim))) 82 | } 83 | if (!isDisabled('grep')) { 84 | subscriptions.push(listManager.registerList(new GrepList(nvim))) 85 | } 86 | if (!isDisabled('LocationList')) { 87 | subscriptions.push(listManager.registerList(new LocationList(nvim))) 88 | } 89 | if (!isDisabled('mru')) { 90 | subscriptions.push(listManager.registerList(new MruList(nvim))) 91 | } 92 | if (!isDisabled('quickfix')) { 93 | subscriptions.push(listManager.registerList(new QuickfixList(nvim))) 94 | } 95 | if (!isDisabled('words')) { 96 | subscriptions.push(listManager.registerList(new Words(nvim))) 97 | } 98 | if (!isDisabled('functions')) { 99 | subscriptions.push(listManager.registerList(new Functions(nvim))) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/lines.ts: -------------------------------------------------------------------------------- 1 | import { AnsiHighlight, BasicList, ListContext, ListItem, Location, Neovim, Range, workspace } from 'coc.nvim' 2 | import { pad } from './util' 3 | 4 | export default class Lines extends BasicList { 5 | public readonly name = 'lines' 6 | public readonly searchHighlight = false 7 | public readonly interactive = true 8 | public readonly description = 'match lines of current buffer by regexp' 9 | public readonly detail = `Patterns are separated by space, pattern starts with '!' means nagitive.` 10 | 11 | constructor(nvim: Neovim) { 12 | super(nvim) 13 | this.addLocationActions() 14 | } 15 | 16 | public async loadItems(context: ListContext): Promise { 17 | let { input, buffer } = context 18 | if (!context.options.interactive) { 19 | throw new Error('lines list works on interactive mode only.') 20 | } 21 | if (input.trim() === '') return [] 22 | let doc = workspace.getDocument(buffer.id) 23 | if (!doc) return [] 24 | let lines = doc.getLines() 25 | let result: ListItem[] = [] 26 | let inputs = input.trim().split(/\s+/) 27 | let flags = context.options.ignorecase ? 'iu' : 'u' 28 | let patterns: RegExp[] = [] 29 | let nagitives: RegExp[] = [] 30 | for (let s of inputs) { 31 | try { 32 | let nagitive = s.startsWith('!') 33 | let re = new RegExp(nagitive ? s.slice(1) : s, flags) 34 | if (nagitive) { 35 | nagitives.push(re) 36 | } else { 37 | patterns.push(re) 38 | } 39 | } catch (_e) { 40 | // noop invalid 41 | } 42 | } 43 | if (patterns.length == 0 && nagitives.length == 0) return [] 44 | let hasNagitive = nagitives.length > 0 45 | let total = lines.length.toString().length 46 | let lnum = 0 47 | for (let line of lines) { 48 | lnum = lnum + 1 49 | if (hasNagitive && nagitives.some(r => r.test(line))) { 50 | continue 51 | } 52 | let ranges: [number, number][] = [] 53 | for (let pattern of patterns) { 54 | let ms = line.match(pattern) 55 | if (ms == null) continue 56 | ranges.push([ms.index, ms.index + ms[0].length]) 57 | } 58 | if (ranges.length != patterns.length) { 59 | continue 60 | } 61 | let range = Range.create(lnum - 1, ranges[0][0], lnum - 1, ranges[0][1]) 62 | let pre = `${lnum}${pad(lnum.toString(), total)}` 63 | let pl = pre.length 64 | let ansiHighlights: AnsiHighlight[] = ranges.map(r => { 65 | return { 66 | span: [byteIndex(line, r[0]) + pl + 1, byteIndex(line, r[1]) + pl + 1], 67 | hlGroup: 'CocListFgRed' 68 | } 69 | }) 70 | ansiHighlights.push({ 71 | span: [0, pl], 72 | hlGroup: 'CocListFgMagenta' 73 | }) 74 | // let text = line.replace(regex, colors.red(input)) 75 | result.push({ 76 | ansiHighlights, 77 | label: `${pre} ${line}`, 78 | location: Location.create(doc.uri, range), 79 | filterText: '' 80 | }) 81 | } 82 | return result 83 | } 84 | } 85 | 86 | function byteIndex(content: string, index: number): number { 87 | let s = content.slice(0, index) 88 | return Buffer.byteLength(s) 89 | } 90 | -------------------------------------------------------------------------------- /src/locationlist.ts: -------------------------------------------------------------------------------- 1 | import { BasicList, ListContext, Uri as URI, ListItem, Location, Neovim, Position, Range, workspace } from 'coc.nvim' 2 | import { characterIndex } from './util' 3 | 4 | export default class LocationList extends BasicList { 5 | public readonly name = 'locationlist' 6 | public readonly defaultAction = 'open' 7 | public description = 'locationlist of current window' 8 | 9 | constructor(nvim: Neovim) { 10 | super(nvim) 11 | this.addLocationActions() 12 | } 13 | 14 | public async loadItems(context: ListContext): Promise { 15 | let { nvim } = this 16 | let { window } = context 17 | let valid = await window.valid 18 | if (!valid) return [] 19 | let list = await nvim.call('getloclist', [window.id]) 20 | if (list.length == 0) return [] 21 | let res: ListItem[] = [] 22 | let buf = await context.window.buffer 23 | let bufnr = buf.id 24 | 25 | let ignoreFilepath = list.every(o => o.bufnr && bufnr && o.bufnr == bufnr) 26 | for (let item of list) { 27 | let { bufnr, col, text, type, lnum } = item 28 | if (type == 'E') { 29 | type = 'Error' 30 | } else if (type == 'W') { 31 | type = 'Warning' 32 | } 33 | let bufname = await nvim.call('bufname', bufnr) 34 | let fullpath = await nvim.call('fnamemodify', [bufname, ':p']) 35 | let uri = URI.file(fullpath).toString() 36 | let line = await workspace.getLine(uri, lnum - 1) 37 | let pos = Position.create(lnum - 1, characterIndex(line, col - 1)) 38 | res.push({ 39 | label: `${ignoreFilepath ? '' : bufname} |${type ? type + ' ' : ''}${lnum} col ${col}| ${text}`, 40 | location: Location.create(uri, Range.create(pos, pos)), 41 | filterText: `${ignoreFilepath ? '' : bufname}${text}` 42 | }) 43 | } 44 | return res 45 | } 46 | 47 | public doHighlight(): void { 48 | let { nvim } = this 49 | nvim.pauseNotification() 50 | nvim.command('syntax match CocLocationlistName /\\v^[^|]+/ contained containedin=CocLocationlistLine', true) 51 | nvim.command('syntax match CocLocationlistPosition /\\v\\|\\w*\\s?\\d+\\scol\\s\\d+\\|/ contained containedin=CocLocationlistLine', true) 52 | nvim.command('syntax match CocLocationlistError /Error/ contained containedin=CocLocationlistPosition', true) 53 | nvim.command('syntax match CocLocationlistWarning /Warning/ contained containedin=CocLocationlistPosition', true) 54 | nvim.command('highlight default link CocLocationlistName Directory', true) 55 | nvim.command('highlight default link CocLocationlistPosition LineNr', true) 56 | nvim.command('highlight default link CocLocationlistError Error', true) 57 | nvim.command('highlight default link CocLocationlistWarning WarningMsg', true) 58 | nvim.resumeNotification().catch(_e => { 59 | // noop 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/maps.ts: -------------------------------------------------------------------------------- 1 | import { IList, ListAction, ListContext, ListItem, Neovim } from 'coc.nvim' 2 | import colors from 'colors/safe' 3 | 4 | const regex = /^(\S+)\s+(\S+)\s+(.*)$/ 5 | 6 | export default class Maps implements IList { 7 | public readonly name = 'maps' 8 | public readonly description = 'key mappings' 9 | public readonly defaultAction = 'open' 10 | public actions: ListAction[] = [] 11 | public options = [{ 12 | name: '-mode=[i,n,v,x,s]', 13 | description: 'Filter mappings by mode.' 14 | }] 15 | 16 | constructor(private nvim: Neovim) { 17 | this.actions.push({ 18 | name: 'open', 19 | execute: async item => { 20 | if (Array.isArray(item)) return 21 | let { mode, key } = item.data 22 | let cmd = JSON.stringify(`verbose ${mode}map ${key}`) 23 | let res = await nvim.eval(`split(execute(${cmd}),"\n")[-1]`) as string 24 | if (/Last\sset\sfrom/.test(res)) { 25 | // the format of the latest vim and neovim is: 26 | // Last set from ~/dotfiles/vimrc/remap.vim line 183 27 | let [filepath, _ ,line] = res.replace(/^\s+Last\sset\sfrom\s+/, '').split(/\s+/) 28 | if (line) { 29 | nvim.command(`edit +${line} ${filepath}`, true) 30 | } else { 31 | nvim.command(`edit +/${key} ${filepath}`, true) 32 | } 33 | } 34 | } 35 | }) 36 | } 37 | 38 | public async loadItems(context: ListContext): Promise { 39 | let { nvim } = this 40 | let mode = '' 41 | for (let arg of context.args) { 42 | if (arg.startsWith('-mode=')) { 43 | mode = arg.replace('-mode=', '') 44 | } 45 | } 46 | let list = await nvim.eval(`split(execute("verbose ${mode}map"),"\n")`) as string[] 47 | let res: ListItem[] = [] 48 | for (let line of list) { 49 | let ms = line.match(regex) 50 | if (ms) { 51 | let [, mode, key, more] = ms 52 | res.push({ 53 | label: ` ${colors.magenta(mode)}\t${colors.blue(key)}\t${more}`, 54 | filterText: `${key} ${more}`, 55 | data: { 56 | mode, 57 | key, 58 | } 59 | }) 60 | } 61 | } 62 | return res 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/marks.ts: -------------------------------------------------------------------------------- 1 | import { IList, ListAction, ListContext, ListItem, Neovim } from 'coc.nvim' 2 | import colors from 'colors/safe' 3 | 4 | const regex = /^\s*(\S)\s+(\d+)\s+(\d+)\s*(.*)$/ 5 | 6 | export default class Marks implements IList { 7 | public readonly name = 'marks' 8 | public readonly description = 'marks list' 9 | public readonly defaultAction = 'jump' 10 | public actions: ListAction[] = [] 11 | 12 | constructor(private nvim: Neovim) { 13 | this.actions.push({ 14 | name: 'jump', 15 | execute: item => { 16 | if (Array.isArray(item)) return 17 | nvim.command('normal! `' + item.data.name + 'zz', true) 18 | } 19 | }) 20 | } 21 | 22 | public async loadItems(_context: ListContext): Promise { 23 | let { nvim } = this 24 | let list = await nvim.eval('split(execute("marks"), "\n")') as string[] 25 | list = list.slice(1) 26 | list = list.filter(s => regex.test(s)) 27 | return list.map(line => { 28 | let [, name, lnum, col, text] = line.match(regex) 29 | return { 30 | label: `${colors.magenta(name)}\t${text}\t${colors.grey(lnum)}\t${colors.grey(col)}`, 31 | filterText: name + ' ' + text, 32 | data: { name } 33 | } 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/mru.ts: -------------------------------------------------------------------------------- 1 | import { BasicList, Uri as URI, commands, Location, Range, Document, events, ListContext, ListItem, Mru, Neovim, workspace } from 'coc.nvim' 2 | import fs from 'fs' 3 | import minimatch from 'minimatch' 4 | import path from 'path' 5 | import { isParentFolder, wait } from './util' 6 | 7 | export default class MruList extends BasicList { 8 | public readonly name = 'mru' 9 | public readonly defaultAction = 'open' 10 | public description = 'most recent used files in current cwd' 11 | public detail = `Use command 'mru.validate' to remove files that not exists any more.` 12 | public options = [{ 13 | name: '-A', 14 | description: 'Show all recent files instead of filter by cwd.' 15 | }] 16 | private promise: Promise = Promise.resolve(undefined) 17 | private mru: Mru 18 | 19 | constructor(nvim: Neovim) { 20 | super(nvim) 21 | this.mru = workspace.createMru('mru') 22 | this.addLocationActions() 23 | this.addAction( 24 | 'delete', 25 | async (item, _context) => { 26 | let filepath = URI.parse(item.data.uri).fsPath 27 | await this.mru.remove(filepath) 28 | }, 29 | { reload: true, persist: true } 30 | ) 31 | this.addAction( 32 | 'clean', 33 | async () => { 34 | await this.mru.clean() 35 | }, 36 | { reload: true, persist: true } 37 | ) 38 | 39 | this.disposables.push(commands.registerCommand('mru.validate', async () => { 40 | let files = await this.mru.load() 41 | for (let file of files) { 42 | if (!fs.existsSync(file)) { 43 | await this.mru.remove(file) 44 | } 45 | } 46 | })) 47 | 48 | for (let doc of workspace.documents) { 49 | this.addRecentFile(doc) 50 | } 51 | 52 | workspace.onDidOpenTextDocument(async textDocument => { 53 | await wait(50) 54 | let doc = workspace.getDocument(textDocument.uri) 55 | if (doc) this.addRecentFile(doc) 56 | }, null, this.disposables) 57 | 58 | events.on('BufEnter', bufnr => { 59 | let doc = workspace.getDocument(bufnr) 60 | if (doc) this.addRecentFile(doc) 61 | }, null, this.disposables) 62 | } 63 | 64 | private addRecentFile(doc: Document): void { 65 | this.promise = this.promise.then(() => { 66 | return this._addRecentFile(doc) 67 | }, () => { 68 | return this._addRecentFile(doc) 69 | }) 70 | } 71 | 72 | private async _addRecentFile(doc: Document): Promise { 73 | let uri = URI.parse(doc.uri) 74 | if (uri.scheme !== 'file' || doc.buftype != '') return 75 | if (doc.filetype == 'netrw') return 76 | if (doc.uri.indexOf('NERD_tree') !== -1) return 77 | let parts = uri.fsPath.split(path.sep) 78 | if (parts.indexOf('.git') !== -1 || parts.length == 0) return 79 | let preview = await this.nvim.eval(`getwinvar(bufwinnr(${doc.bufnr}), '&previewwindow')`) 80 | if (preview == 1) return 81 | let filepath = uri.fsPath 82 | let patterns = this.config.get('source.mru.excludePatterns', []) 83 | if (patterns.some(p => minimatch(filepath, p))) return 84 | await this.mru.add(filepath) 85 | } 86 | 87 | public async loadItems(context: ListContext): Promise { 88 | let cwd = await this.nvim.call('getcwd') 89 | let findAll = context.args.indexOf('-A') !== -1 90 | let files = await this.mru.load() 91 | let config = workspace.getConfiguration('list.source.mru') 92 | let filterByName = config.get('filterByName', false) 93 | const range = Range.create(0, 0, 0, 0) 94 | if (!findAll) files = files.filter(file => isParentFolder(cwd, file)) 95 | return files.map((file, i) => { 96 | let uri = URI.file(file).toString() 97 | let location = Location.create(uri.toString(), range) 98 | if (!filterByName) { 99 | return { 100 | label: findAll ? file : path.relative(cwd, file), 101 | data: { uri }, 102 | sortText: String.fromCharCode(i), 103 | location 104 | } 105 | } else { 106 | let name = path.basename(file) 107 | file = findAll ? file : path.relative(cwd, file) 108 | return { 109 | label: `${name}\t${file}`, 110 | data: { uri }, 111 | sortText: String.fromCharCode(i), 112 | filterText: name, 113 | location 114 | } 115 | } 116 | }) 117 | } 118 | 119 | public doHighlight(): void { 120 | let config = workspace.getConfiguration('list.source.mru') 121 | let filterByName = config.get('filterByName', false) 122 | if (filterByName) { 123 | let { nvim } = this 124 | nvim.pauseNotification() 125 | nvim.command('syntax match CocMruFile /\\t.*$/ contained containedin=CocMruLine', true) 126 | nvim.command('highlight default link CocMruFile Comment', true) 127 | nvim.resumeNotification(false, true) 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/quickfix.ts: -------------------------------------------------------------------------------- 1 | import { BasicList, ListContext, Uri as URI, ListItem, Location, Neovim, Position, Range, workspace } from 'coc.nvim' 2 | import { characterIndex } from './util' 3 | 4 | export default class QuickfixList extends BasicList { 5 | public readonly name = 'quickfix' 6 | public readonly defaultAction = 'open' 7 | public description = 'quickfix list' 8 | 9 | constructor(nvim: Neovim) { 10 | super(nvim) 11 | this.addLocationActions() 12 | } 13 | 14 | public async loadItems(context: ListContext): Promise { 15 | let { nvim } = this 16 | let { window } = context 17 | let list = await nvim.call('getqflist') 18 | if (list.length == 0) return [] 19 | let res: ListItem[] = [] 20 | let bufnr: number 21 | let valid = await window.valid 22 | if (valid) { 23 | let buf = await window.buffer 24 | bufnr = buf.id 25 | } 26 | 27 | let ignoreFilepath = list.every(o => o.bufnr && bufnr && o.bufnr == bufnr) 28 | for (let item of list) { 29 | let { bufnr, col, text, type, lnum } = item 30 | if (type == 'E') { 31 | type = 'Error' 32 | } else if (type == 'W') { 33 | type = 'Warning' 34 | } 35 | let bufname = await nvim.call('bufname', bufnr) 36 | let fullpath = await nvim.call('fnamemodify', [bufname, ':p']) 37 | let uri = URI.file(fullpath).toString() 38 | let line = await workspace.getLine(uri, lnum - 1) 39 | let pos = Position.create(lnum - 1, characterIndex(line, col - 1)) 40 | res.push({ 41 | label: `${ignoreFilepath ? '' : bufname} |${type ? type + ' ' : ''}${lnum} col ${col}| ${text}`, 42 | location: Location.create(uri, Range.create(pos, pos)), 43 | filterText: `${ignoreFilepath ? '' : bufname}${text}` 44 | }) 45 | } 46 | return res 47 | } 48 | 49 | public doHighlight(): void { 50 | let { nvim } = this 51 | nvim.pauseNotification() 52 | nvim.command('syntax match CocQuickfixName /\\v^[^|]+/ contained containedin=CocQuickfixLine', true) 53 | nvim.command('syntax match CocQuickfixPosition /\\v\\|\\w*\\s?\\d+\\scol\\s\\d+\\|/ contained containedin=CocQuickfixLine', true) 54 | nvim.command('syntax match CocQuickfixError /Error/ contained containedin=CocQuickfixPosition', true) 55 | nvim.command('syntax match CocQuickfixWarning /Warning/ contained containedin=CocQuickfixPosition', true) 56 | nvim.command('highlight default link CocQuickfixName Directory', true) 57 | nvim.command('highlight default link CocQuickfixPosition LineNr', true) 58 | nvim.command('highlight default link CocQuickfixError Error', true) 59 | nvim.command('highlight default link CocQuickfixWarning WarningMsg', true) 60 | nvim.resumeNotification().catch(_e => { 61 | // noop 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/registers.ts: -------------------------------------------------------------------------------- 1 | import { IList, ListAction, ListContext, ListItem, Neovim } from 'coc.nvim' 2 | import colors from 'colors/safe' 3 | const regex = /^[^\w]*(\w)[^\w|"]+(".)[^\w]+(.*)$/ 4 | 5 | export default class Registers implements IList { 6 | public readonly name = 'registers' 7 | public readonly description = 'registers list' 8 | public readonly defaultAction = 'append' 9 | public actions: ListAction[] = [] 10 | 11 | constructor(private nvim: Neovim) { 12 | this.actions.push({ 13 | name: 'append', 14 | execute: item => { 15 | if (Array.isArray(item)) return 16 | nvim.command('normal! ' + item.data.name + 'p', true) 17 | } 18 | }) 19 | this.actions.push({ 20 | name: 'prepend', 21 | execute: item => { 22 | if (Array.isArray(item)) return 23 | nvim.command('normal! ' + item.data.name + 'P', true) 24 | } 25 | }) 26 | } 27 | 28 | public async loadItems(_context: ListContext): Promise { 29 | const { nvim } = this 30 | let list = await nvim.eval('split(execute("registers"), "\n")') as string[] 31 | list = list.slice(1) 32 | list = list.filter(s => regex.test(s)) 33 | 34 | return list.map(line => { 35 | let [, type, name, content] = line.match(regex) 36 | return { 37 | label: `${colors.grey(type)}\t${colors.magenta(name)}\t${content}`, 38 | filterText: content, 39 | data: { name } 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/searchhistory.ts: -------------------------------------------------------------------------------- 1 | import { IList, ListAction, ListContext, ListItem, Neovim } from 'coc.nvim' 2 | 3 | const regex = /^\s*\d+\s+(.*)$/ 4 | 5 | export default class Searchhistory implements IList { 6 | public readonly name = 'searchhistory' 7 | public readonly description = 'search history' 8 | public readonly defaultAction = 'feedkeys' 9 | public actions: ListAction[] = [] 10 | 11 | constructor(private nvim: Neovim) { 12 | this.actions.push({ 13 | name: 'feedkeys', 14 | execute: async item => { 15 | if (Array.isArray(item)) return 16 | nvim.call('feedkeys', [`/${item.data.cmd}`, 'n'], true) 17 | } 18 | }) 19 | } 20 | 21 | public async loadItems(_context: ListContext): Promise { 22 | let { nvim } = this 23 | let list = await nvim.eval(`split(execute("history search"),"\n")`) as string[] 24 | list = list.slice(1) 25 | let res: ListItem[] = [] 26 | for (let line of list) { 27 | let ms = line.match(regex) 28 | if (ms) { 29 | let [, cmd] = ms 30 | res.push({ 31 | label: cmd, 32 | filterText: cmd, 33 | data: { 34 | cmd 35 | } 36 | }) 37 | } 38 | } 39 | return res 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/session.ts: -------------------------------------------------------------------------------- 1 | import { BasicList, commands, ListContext, ListItem, Location, Mru, Neovim, Range, Uri, window, workspace } from 'coc.nvim' 2 | import fs from 'fs' 3 | import os from 'os' 4 | import path from 'path' 5 | import { promisify } from 'util' 6 | 7 | export default class SessionList extends BasicList { 8 | public readonly name = 'sessions' 9 | public readonly defaultAction = 'load' 10 | public description = 'session list' 11 | public detail = `After session load, coc service would be restarted.` 12 | private mru: Mru 13 | 14 | constructor(nvim: Neovim, private extensionPath: string, saveOnVimLeave: boolean) { 15 | super(nvim) 16 | this.mru = workspace.createMru('sessions') 17 | this.addLocationActions() 18 | 19 | this.addAction('delete', async item => { 20 | let filepath = item.data.filepath 21 | await this.mru.remove(filepath) 22 | await promisify(fs.unlink)(filepath) 23 | }, { reload: true, persist: true }) 24 | 25 | this.addAction('load', async (item, _context) => { 26 | let filepath = item.data.filepath 27 | await this.loadSession(filepath) 28 | }) 29 | 30 | this.disposables.push(commands.registerCommand('session.save', async (name?: string) => { 31 | setTimeout(async () => { 32 | if (!name) { 33 | name = await nvim.getVvar('this_session') as string 34 | if (!name) { 35 | let defaultValue = path.basename(workspace.rootPath) 36 | name = await window.requestInput('session name', defaultValue) 37 | if (!name) return 38 | } 39 | } 40 | if (!name.endsWith('.vim')) name = name + '.vim' 41 | let escaped: string 42 | if (!path.isAbsolute(name)) { 43 | let folder = this.getSessionFolder() 44 | escaped = await nvim.call('fnameescape', [path.join(folder, name)]) 45 | } else { 46 | escaped = await nvim.call('fnameescape', [name]) 47 | name = path.basename(name, '.vim') 48 | } 49 | await nvim.command(`silent mksession! ${escaped}`) 50 | window.showMessage(`Saved session: ${name}`, 'more') 51 | }, 50) 52 | })) 53 | 54 | this.disposables.push(commands.registerCommand('session.load', async (name?: string) => { 55 | if (!name) { 56 | let folder = this.getSessionFolder() 57 | let files = await promisify(fs.readdir)(folder, { encoding: 'utf8' }) 58 | files = files.filter(p => p.endsWith('.vim')) 59 | files = files.map(p => path.basename(p, '.vim')) 60 | let idx = await window.showQuickpick(files, 'choose session:') 61 | if (idx == -1) return 62 | name = files[idx] 63 | } 64 | let filepath: string 65 | if (path.isAbsolute(name)) { 66 | filepath = name 67 | } else { 68 | let folder = this.getSessionFolder() 69 | filepath = path.join(folder, name.endsWith('.vim') ? name : `${name}.vim`) 70 | } 71 | setTimeout(async () => { 72 | await this.loadSession(filepath) 73 | }, 30) 74 | })) 75 | 76 | this.disposables.push(commands.registerCommand('session.restart', async () => { 77 | if (!workspace.isNvim || process.env.TERM_PROGRAM != 'iTerm.app') { 78 | window.showMessage('Sorry, restart support iTerm and neovim only.', 'warning') 79 | return 80 | } 81 | let filepath = await this.nvim.getVvar('this_session') as string 82 | if (!filepath) { 83 | let folder = this.getSessionFolder() 84 | filepath = path.join(folder, 'default.vim') 85 | } 86 | await nvim.command(`silent mksession! ${filepath}`) 87 | let cwd = await nvim.call('getcwd') 88 | let cmd = `${path.join(this.extensionPath, 'nvimstart')} ${filepath} ${cwd}` 89 | nvim.call('jobstart', [cmd, { detach: 1 }], true) 90 | nvim.command('silent! wa | silent quitall!', true) 91 | })) 92 | 93 | if (saveOnVimLeave) { 94 | this.disposables.push(workspace.registerAutocmd({ 95 | event: 'VimLeavePre', 96 | request: true, 97 | callback: async () => { 98 | let curr = await this.nvim.getVvar('this_session') as string 99 | if (!curr) { 100 | let folder = this.getSessionFolder() 101 | curr = path.join(folder, 'default.vim') 102 | } else { 103 | if (!path.isAbsolute(curr)) return 104 | // check if folder of curr exists 105 | let folder = path.dirname(curr) 106 | if (!fs.existsSync(folder)) return 107 | } 108 | nvim.command(`silent! mksession! ${curr}`, true) 109 | } 110 | })) 111 | } 112 | } 113 | 114 | private async loadSession(filepath: string): Promise { 115 | let { nvim } = this 116 | let config = workspace.getConfiguration('session') 117 | let restart = config.get('restartOnSessionLoad', false) 118 | if (restart && workspace.isNvim && process.env.TERM_PROGRAM.startsWith('iTerm.app')) { 119 | let content = await promisify(fs.readFile)(filepath, 'utf8') 120 | let line = content.split(/\r?\n/).find(s => s.startsWith('cd ')) 121 | let cwd = line.replace(/^cd\s+/, '') 122 | let cmd = `${path.join(this.extensionPath, 'nvimstart')} ${filepath} ${cwd}` 123 | nvim.call('jobstart', [cmd, { detach: 1 }], true) 124 | nvim.command('silent! wa | silent quitall!', true) 125 | } else { 126 | await this.mru.add(filepath) 127 | let escaped = await nvim.call('fnameescape', [filepath]) 128 | nvim.pauseNotification() 129 | nvim.command('noautocmd silent! %bwipeout!', true) 130 | nvim.command(`silent! source ${escaped}`, true) 131 | nvim.command('CocRestart', true) 132 | await nvim.resumeNotification(false, true) 133 | } 134 | } 135 | 136 | private getSessionFolder(): string { 137 | let config = workspace.getConfiguration('session') 138 | let directory = config.get('directory', '') 139 | directory = directory.replace(/^~/, os.homedir()) 140 | const isWin = process.platform === 'win32' 141 | if (!directory) { 142 | const folder = isWin ? 'vimfiles/sessions' : '.vim/sessions' 143 | directory = path.join(os.homedir(), folder) 144 | } 145 | if (!fs.existsSync(directory)) { 146 | fs.mkdirSync(directory, { recursive: true }) 147 | if (isWin) { 148 | let folder = path.join(os.homedir(), '.vim/sessions') 149 | if (fs.existsSync(folder)) { 150 | let stat = fs.lstatSync(folder) 151 | if (stat && stat.isDirectory()) { 152 | let files = fs.readdirSync(folder) 153 | for (let file of files) { 154 | if (file.endsWith('.vim')) { 155 | let dest = path.join(os.homedir(), 'vimfiles/sessions', file) 156 | fs.copyFileSync(path.join(folder, file), dest) 157 | } 158 | } 159 | } 160 | } 161 | } 162 | } 163 | return directory 164 | } 165 | 166 | public async loadItems(_context: ListContext): Promise { 167 | let folder = this.getSessionFolder() 168 | let files = await promisify(fs.readdir)(folder, { encoding: 'utf8' }) 169 | files = files.filter(p => p.endsWith('.vim')) 170 | let range = Range.create(0, 0, 0, 0) 171 | let curr = await this.nvim.getVvar('this_session') as string 172 | let arr = await Promise.all(files.map(file => { 173 | let filepath = path.join(folder, file) 174 | return promisify(fs.stat)(filepath).then(stat => { 175 | return { 176 | atime: stat.atime, 177 | filepath 178 | } 179 | }) 180 | })) 181 | arr.sort((a, b) => { 182 | return a.atime.getTime() - b.atime.getTime() 183 | }) 184 | files = arr.map(o => o.filepath) 185 | return files.map(filepath => { 186 | let uri = Uri.file(filepath).toString() 187 | let location = Location.create(uri, range) 188 | let name = path.basename(filepath, '.vim') 189 | let active = curr && curr == filepath 190 | return { 191 | label: `${active ? '*' : ' '} ${name}\t${filepath}`, 192 | data: { filepath }, 193 | location 194 | } 195 | }) 196 | } 197 | 198 | public doHighlight(): void { 199 | let { nvim } = this 200 | nvim.pauseNotification() 201 | nvim.command('syntax match CocSessionsActivited /\\v^\\*/ contained containedin=CocSessionsLine', true) 202 | nvim.command('syntax match CocSessionsName /\\v%3c\\S+/ contained containedin=CocSessionsLine', true) 203 | nvim.command('syntax match CocSessionsRoot /\\v\\t[^\\t]*$/ contained containedin=CocSessionsLine', true) 204 | nvim.command('highlight default link CocSessionsActivited Special', true) 205 | nvim.command('highlight default link CocSessionsName Type', true) 206 | nvim.command('highlight default link CocSessionsRoot Comment', true) 207 | nvim.resumeNotification().catch(_e => { 208 | // noop 209 | }) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/tags.ts: -------------------------------------------------------------------------------- 1 | import { BasicList, commands, Uri as URI, ListContext, ListTask, Neovim, window, workspace } from 'coc.nvim' 2 | import colors from 'colors/safe' 3 | import { EventEmitter } from 'events' 4 | import fs, { ReadStream } from 'fs' 5 | import path from 'path' 6 | import readline from 'readline' 7 | import { isParentFolder } from './util' 8 | 9 | class FileTask extends EventEmitter implements ListTask { 10 | private streams: ReadStream[] = [] 11 | constructor() { 12 | super() 13 | } 14 | 15 | public start(files: string[], cwd: string): void { 16 | let count = files.length 17 | for (let file of files) { 18 | let filepath = path.isAbsolute(file) ? file : path.join(cwd, file) 19 | if (!fs.existsSync(filepath)) continue 20 | let stream = fs.createReadStream(filepath, { encoding: 'utf8' }) 21 | this.streams.push(stream) 22 | const rl = readline.createInterface({ 23 | input: stream 24 | }) 25 | let dirname = path.dirname(filepath) 26 | rl.on('line', line => { 27 | if (line.startsWith('!')) return 28 | let [name, file, pattern] = line.split('\t') 29 | if (!pattern) return 30 | let fullpath = path.join(dirname, file) 31 | let uri = URI.file(fullpath).toString() 32 | let relativeFile = isParentFolder(cwd, fullpath) ? path.relative(cwd, fullpath) : fullpath 33 | this.emit('data', { 34 | label: `${colors.blue(name)} ${colors.grey(relativeFile)}`, 35 | filterText: name, 36 | location: { 37 | uri, 38 | line: pattern.replace(/^\/\^/, '').replace(/\$\/;?"?$/, ''), 39 | text: name 40 | } 41 | }) 42 | }) 43 | rl.on('error', e => { 44 | count = count - 1 45 | this.emit('error', e.message) 46 | }) 47 | rl.on('close', () => { 48 | count = count - 1 49 | if (count == 0) { 50 | this.emit('end') 51 | } 52 | }) 53 | } 54 | } 55 | 56 | public dispose(): void { 57 | for (let stream of this.streams) { 58 | stream.close() 59 | } 60 | } 61 | } 62 | 63 | export default class Helptags extends BasicList { 64 | public readonly name = 'tags' 65 | public readonly description = 'search from tags' 66 | public readonly defaultAction = 'open' 67 | 68 | constructor(nvim: Neovim) { 69 | super(nvim) 70 | this.addLocationActions() 71 | 72 | this.disposables.push(commands.registerCommand('tags.generate', async () => { 73 | let config = workspace.getConfiguration('list.source.tags') 74 | let cmd = config.get('command', 'ctags -R .') 75 | let res = await window.runTerminalCommand(cmd) 76 | if (res.success) window.showMessage('tagfile generated') 77 | })) 78 | } 79 | 80 | public async loadItems(_context: ListContext): Promise { 81 | let { nvim } = this 82 | let cwd = await nvim.call('getcwd') as string 83 | let tagfiles = await nvim.call('tagfiles') as string[] 84 | if (!tagfiles || tagfiles.length == 0) { 85 | throw new Error('no tag files found, use ":CocCommand tags.generate" to generate tagfile.') 86 | } 87 | let task = new FileTask() 88 | task.start(tagfiles, cwd) 89 | return task 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/util/ansiparse.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface AnsiItem { 3 | foreground?: string 4 | background?: string 5 | bold?: boolean 6 | italic?: boolean 7 | underline?: boolean 8 | text: string 9 | } 10 | 11 | const foregroundColors = { 12 | 30: 'black', 13 | 31: 'red', 14 | 32: 'green', 15 | 33: 'yellow', 16 | 34: 'blue', 17 | 35: 'magenta', 18 | 36: 'cyan', 19 | 37: 'white', 20 | 90: 'grey' 21 | } 22 | 23 | const backgroundColors = { 24 | 40: 'black', 25 | 41: 'red', 26 | 42: 'green', 27 | 43: 'yellow', 28 | 44: 'blue', 29 | 45: 'magenta', 30 | 46: 'cyan', 31 | 47: 'white' 32 | } 33 | 34 | const styles = { 35 | 1: 'bold', 36 | 3: 'italic', 37 | 4: 'underline' 38 | } 39 | 40 | export function ansiparse(str: string): AnsiItem[] { 41 | // 42 | // I'm terrible at writing parsers. 43 | // 44 | let matchingControl = null 45 | let matchingData = null 46 | let matchingText = '' 47 | let ansiState = [] 48 | let result = [] 49 | let state: Partial = {} 50 | let eraseChar 51 | 52 | // 53 | // General workflow for this thing is: 54 | // \033\[33mText 55 | // | | | 56 | // | | matchingText 57 | // | matchingData 58 | // matchingControl 59 | // 60 | // \033\[K 61 | // 62 | // In further steps we hope it's all going to be fine. It usually is. 63 | // 64 | 65 | // 66 | // Erases a char from the output 67 | // 68 | eraseChar = () => { 69 | let index 70 | let text 71 | if (matchingText.length) { 72 | matchingText = matchingText.substr(0, matchingText.length - 1) 73 | } 74 | else if (result.length) { 75 | index = result.length - 1 76 | text = result[index].text 77 | if (text.length === 1) { 78 | // 79 | // A result bit was fully deleted, pop it out to simplify the final output 80 | // 81 | result.pop() 82 | } 83 | else { 84 | result[index].text = text.substr(0, text.length - 1) 85 | } 86 | } 87 | } 88 | 89 | for (let i = 0; i < str.length; i++) { // tslint:disable-line 90 | if (matchingControl != null) { 91 | if (matchingControl == '\x1b' && str[i] == '\[') { 92 | // 93 | // We've matched full control code. Lets start matching formating data. 94 | // 95 | 96 | // 97 | // "emit" matched text with correct state 98 | // 99 | if (matchingText) { 100 | state.text = matchingText 101 | result.push(state) 102 | state = {} 103 | matchingText = '' 104 | } 105 | 106 | matchingControl = null 107 | matchingData = '' 108 | } else { 109 | // 110 | // We failed to match anything - most likely a bad control code. We 111 | // go back to matching regular strings. 112 | // 113 | matchingText += matchingControl + str[i] 114 | matchingControl = null 115 | } 116 | continue 117 | } else if (matchingData != null) { 118 | if (str[i] == ';') { 119 | // 120 | // `;` separates many formatting codes, for example: `\033[33;43m` 121 | // means that both `33` and `43` should be applied. 122 | // 123 | // TODO: this can be simplified by modifying state here. 124 | // 125 | ansiState.push(matchingData) 126 | matchingData = '' 127 | } else if (str[i] == 'm' || str[i] == 'K') { 128 | // 129 | // `m` finished whole formatting code. We can proceed to matching 130 | // formatted text. 131 | // 132 | ansiState.push(matchingData) 133 | matchingData = null 134 | matchingText = '' 135 | 136 | // 137 | // Convert matched formatting data into user-friendly state object. 138 | // 139 | // TODO: DRY. 140 | // 141 | ansiState.forEach(ansiCode => { 142 | if (foregroundColors[ansiCode]) { 143 | state.foreground = foregroundColors[ansiCode] 144 | } else if (backgroundColors[ansiCode]) { 145 | state.background = backgroundColors[ansiCode] 146 | } else if (ansiCode == 39) { 147 | delete state.foreground 148 | } else if (ansiCode == 49) { 149 | delete state.background 150 | } else if (styles[ansiCode]) { 151 | state[styles[ansiCode]] = true 152 | } else if (ansiCode == 22) { 153 | state.bold = false 154 | } else if (ansiCode == 23) { 155 | state.italic = false 156 | } else if (ansiCode == 24) { 157 | state.underline = false 158 | } 159 | }) 160 | ansiState = [] 161 | } 162 | else { 163 | matchingData += str[i] 164 | } 165 | continue 166 | } 167 | 168 | if (str[i] == '\x1b') { 169 | matchingControl = str[i] 170 | } else if (str[i] == '\u0008') { 171 | eraseChar() 172 | } else { 173 | matchingText += str[i] 174 | } 175 | } 176 | 177 | if (matchingText) { 178 | state.text = matchingText + (matchingControl ? matchingControl : '') 179 | result.push(state) 180 | } 181 | return result 182 | } 183 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | import which from 'which' 2 | import path from 'path' 3 | 4 | export function executable(cmd: string): boolean { 5 | try { 6 | which.sync(cmd) 7 | } catch (e) { 8 | return false 9 | } 10 | return true 11 | } 12 | 13 | export function characterIndex(content: string, byteIndex: number): number { 14 | let buf = Buffer.from(content, 'utf8') 15 | return buf.slice(0, byteIndex).toString('utf8').length 16 | } 17 | 18 | export function wait(ms: number): Promise { 19 | return new Promise(resolve => { 20 | setTimeout(() => { 21 | resolve(undefined) 22 | }, ms) 23 | }) 24 | } 25 | 26 | export function pad(n: string, total: number): string { 27 | let l = total - n.length 28 | if (l <= 0) return '' 29 | return ((new Array(l)).fill(' ').join('')) 30 | } 31 | 32 | /** 33 | * Removes duplicates from the given array. The optional keyFn allows to specify 34 | * how elements are checked for equalness by returning a unique string for each. 35 | */ 36 | export function distinct(array: T[], keyFn?: (t: T) => string): T[] { 37 | if (!keyFn) { 38 | return array.filter((element, position) => { 39 | return array.indexOf(element) === position 40 | }) 41 | } 42 | 43 | const seen: { [key: string]: boolean } = Object.create(null) 44 | return array.filter(elem => { 45 | const key = keyFn(elem) 46 | if (seen[key]) { 47 | return false 48 | } 49 | 50 | seen[key] = true 51 | 52 | return true 53 | }) 54 | } 55 | 56 | export function isParentFolder(folder: string, filepath: string): boolean { 57 | let rel = path.relative(folder, filepath) 58 | return !rel.startsWith('..') 59 | } 60 | -------------------------------------------------------------------------------- /src/util/option.ts: -------------------------------------------------------------------------------- 1 | const options = [] 2 | 3 | function defineOptions(s: string, l: string, ag: string, rg: string): void { 4 | options.push([s, l, ag, rg]) 5 | } 6 | 7 | defineOptions('-S', '-smartcase', '--smart-case', '--smart-case') 8 | defineOptions('-i', '-ignorecase', '--ignore-case', '--ignore-case') 9 | defineOptions('-l', '-literal', '--fixed-strings', '--fixed-strings') 10 | defineOptions('-w', '-word', '--word-regexp', '--word-regexp') 11 | defineOptions('-e', '-regex', '', '--regexp') 12 | defineOptions('-u', '-skip-vcs-ignores', '--skip-vcs-ignores', '--no-ignore-vcs') 13 | defineOptions('-t', '-extension', null, null) 14 | 15 | function getOption(opt: string, command: string): string { 16 | let idx = opt.length == 2 ? 0 : 1 17 | let o = options.find(o => o[idx] == opt) 18 | if (!o) return opt 19 | return command === 'ag' ? o[2] : o[3] 20 | } 21 | 22 | // export const optionList = options.map(o => o[1]) 23 | 24 | export function convertOptions(list: string[], command: string, defaultLiteral = false): string[] { 25 | let useLiteral = defaultLiteral && list.find(o => { 26 | return ['-e', '-regex', '-w', '-word', '-l', '-literal'].indexOf(o) != -1 27 | }) == null 28 | let res: string[] = [] 29 | if (useLiteral) list.unshift('-l') 30 | for (let idx = 0; idx < list.length; idx++) { 31 | let s = list[idx] 32 | if (/^-/.test(s)) { 33 | if (s === '-t' || s === '-extension') { 34 | let f = list[idx + 1] 35 | if (!f || /^-/.test(f)) continue 36 | if (command == 'rg') { 37 | res.push('--glob', `*.${f}`) 38 | } else { 39 | res.push('-G', `*.${f}`) 40 | } 41 | idx++ 42 | } else { 43 | res.push(getOption(s, command)) 44 | } 45 | } else { 46 | res.push(s) 47 | } 48 | } 49 | // ag use smartcase by default, we don't like that 50 | if (command === 'ag' 51 | && list.indexOf('-S') === -1 52 | && list.indexOf('-smartcase') === -1) { 53 | res.unshift('-s') 54 | } 55 | return res 56 | } 57 | -------------------------------------------------------------------------------- /src/windows.ts: -------------------------------------------------------------------------------- 1 | import { IList, ListAction, ListContext, ListItem, Neovim } from 'coc.nvim' 2 | import colors from 'colors/safe' 3 | 4 | export default class Windows implements IList { 5 | public readonly name = 'windows' 6 | public readonly description = 'windows list' 7 | public readonly defaultAction = 'jump' 8 | public actions: ListAction[] = [] 9 | 10 | constructor(private nvim: Neovim) { 11 | this.actions.push({ 12 | name: 'jump', 13 | execute: item => { 14 | if (Array.isArray(item)) return 15 | nvim.call('win_gotoid', item.data.id, true) 16 | } 17 | }) 18 | } 19 | 20 | public async loadItems(_context: ListContext): Promise { 21 | let { nvim } = this 22 | let wins = await nvim.call('getwininfo') as any[] 23 | let res: ListItem[] = [] 24 | for (let win of wins) { 25 | let bufname = await nvim.call('bufname', win.bufnr) 26 | res.push({ 27 | label: `${colors.yellow(win.tabnr.toString())}\t${colors.yellow(win.winnr.toString())}\t${bufname}`, 28 | data: { id: win.winid } 29 | }) 30 | } 31 | return res 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/words.ts: -------------------------------------------------------------------------------- 1 | import { BasicList, ListContext, ListItem, Location, Neovim, Range, workspace } from 'coc.nvim' 2 | import colors from 'colors/safe' 3 | import { pad } from './util' 4 | 5 | const matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g 6 | 7 | export default class Words extends BasicList { 8 | public readonly name = 'words' 9 | public readonly searchHighlight = false 10 | public readonly interactive = true 11 | public readonly description = 'word matches of current buffer' 12 | public options = [{ 13 | name: '-w, -word', 14 | description: 'Match word boundary.' 15 | }] 16 | 17 | constructor(nvim: Neovim) { 18 | super(nvim) 19 | this.addLocationActions() 20 | } 21 | 22 | public async loadItems(context: ListContext): Promise { 23 | let { input, window } = context 24 | if (!input) return [] 25 | let valid = await window.valid 26 | if (!valid) return [] 27 | let buf = await window.buffer 28 | let doc = workspace.getDocument(buf.id) 29 | if (!doc) return 30 | let { args } = context 31 | let wordMatch = args.indexOf('-w') !== -1 || args.indexOf('-word') !== -1 32 | let content = doc.getDocumentContent() 33 | let result: ListItem[] = [] 34 | let lnum = 1 35 | let total = doc.lineCount.toString().length 36 | let flags = context.options.ignorecase ? 'iu' : 'u' 37 | let source = input.replace(matchOperatorsRe, '\\$&') 38 | if (wordMatch) source = `\\b${source}\\b` 39 | let regex = new RegExp(source, flags) 40 | let replaceRegex = new RegExp(source, flags + 'g') 41 | for (let line of content.split('\n')) { 42 | let ms = line.match(regex) 43 | if (ms) { 44 | let range = Range.create(lnum - 1, ms.index, lnum - 1, ms.index + input.length) 45 | let pre = `${colors.magenta(lnum.toString())}${pad(lnum.toString(), total)}` 46 | let text = line.replace(replaceRegex, match => { 47 | return colors.red(match) 48 | }) 49 | result.push({ 50 | label: `${pre} ${text}`, 51 | location: Location.create(doc.uri, range), 52 | filterText: '' 53 | }) 54 | } 55 | lnum = lnum + 1 56 | } 57 | return result 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "resolveJsonModule": true, 4 | "esModuleInterop": true, 5 | "strictNullChecks": false, 6 | "strictFunctionTypes": false, 7 | "allowUnreachableCode": true, 8 | "allowUnusedLabels": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noImplicitAny": false, 11 | "noImplicitReturns": false, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "strictPropertyInitialization": false, 15 | "declaration": false, 16 | "outDir": "lib", 17 | "target": "es2017", 18 | "module": "commonjs", 19 | "moduleResolution": "node", 20 | "lib": ["es2018"], 21 | "plugins": [] 22 | }, 23 | "include": ["src"], 24 | "exclude": [] 25 | } 26 | --------------------------------------------------------------------------------