From 56df844d3c39ec494dacc69eae34272b27db185a Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Tue, 8 Apr 2025 17:44:49 +0200 Subject: [PATCH 1/1] Squashed '.vim/bundle/asyncomplete/' content from commit 016590d2 git-subtree-dir: .vim/bundle/asyncomplete git-subtree-split: 016590d2ca73cefe45712430e319a0ef004e2215 --- .gitattributes | 1 + .github/FUNDING.yml | 12 + .github/stale.yml | 17 + .github/workflows/linux_neovim.yml | 44 ++ .github/workflows/linux_vim.yml | 46 ++ .github/workflows/mac_neovim.yml | 44 ++ .github/workflows/reviewdog.yml | 22 + .github/workflows/windows_neovim.yml | 46 ++ .github/workflows/windows_vim.yml | 52 ++ .gitignore | 1 + .vintrc.yaml | 10 + LICENSE | 21 + README.md | 245 ++++++++ autoload/asyncomplete.vim | 540 ++++++++++++++++++ autoload/asyncomplete/utils.vim | 21 + .../utils/_on_change/textchangedp.vim | 81 +++ .../asyncomplete/utils/_on_change/timer.vim | 83 +++ doc/asyncomplete.txt | 191 +++++++ plugin/asyncomplete.vim | 25 + renovate.json | 6 + test/.themisrc | 3 + test/asyncomplete.vimspec | 2 + 22 files changed, 1513 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/FUNDING.yml create mode 100644 .github/stale.yml create mode 100644 .github/workflows/linux_neovim.yml create mode 100644 .github/workflows/linux_vim.yml create mode 100644 .github/workflows/mac_neovim.yml create mode 100644 .github/workflows/reviewdog.yml create mode 100644 .github/workflows/windows_neovim.yml create mode 100644 .github/workflows/windows_vim.yml create mode 100644 .gitignore create mode 100644 .vintrc.yaml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 autoload/asyncomplete.vim create mode 100644 autoload/asyncomplete/utils.vim create mode 100644 autoload/asyncomplete/utils/_on_change/textchangedp.vim create mode 100644 autoload/asyncomplete/utils/_on_change/timer.vim create mode 100644 doc/asyncomplete.txt create mode 100644 plugin/asyncomplete.vim create mode 100644 renovate.json create mode 100644 test/.themisrc create mode 100644 test/asyncomplete.vimspec diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..176a458f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..eaf41dd9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: asyncomplete +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..dc90e5a1 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/linux_neovim.yml b/.github/workflows/linux_neovim.yml new file mode 100644 index 00000000..3d902a81 --- /dev/null +++ b/.github/workflows/linux_neovim.yml @@ -0,0 +1,44 @@ +name: linux_neovim + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + name: [neovim-v04-x64,neovim-nightly-x64] + include: + - name: neovim-v04-x64 + os: ubuntu-latest + neovim_version: v0.4.3 + - name: neovim-nightly-x64 + os: ubuntu-latest + neovim_version: nightly + runs-on: ${{matrix.os}} + steps: + - uses: actions/checkout@v4 + - name: Download neovim + shell: bash + run: | + mkdir -p ~/nvim/bin + curl -L https://github.com/neovim/neovim/releases/download/${{matrix.neovim_version}}/nvim.appimage -o ~/nvim/bin/nvim + chmod u+x ~/nvim/bin/nvim + - name: Download test runner + shell: bash + run: git clone --depth 1 --branch v1.5.4 --single-branch https://github.com/thinca/vim-themis ~/themis + - name: Run tests + shell: bash + run: | + export PATH=~/nvim/bin:$PATH + export PATH=~/themis/bin:$PATH + export THEMIS_VIM=nvim + nvim --version + themis --reporter spec diff --git a/.github/workflows/linux_vim.yml b/.github/workflows/linux_vim.yml new file mode 100644 index 00000000..e50e66cf --- /dev/null +++ b/.github/workflows/linux_vim.yml @@ -0,0 +1,46 @@ +name: linux_vim + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + name: [vim-v82-x64, vim-v81-x64] + include: + - name: vim-v82-x64 + os: ubuntu-latest + vim_version: 8.2.0813 + glibc_version: 2.15 + - name: vim-v81-x64 + os: ubuntu-latest + vim_version: 8.1.2414 + glibc_version: 2.15 + runs-on: ${{matrix.os}} + steps: + - uses: actions/checkout@v4 + - name: Download vim + shell: bash + run: | + mkdir -p ~/vim/bin + curl -L https://github.com/vim/vim-appimage/releases/download/v${{matrix.vim_version}}/GVim-v${{matrix.vim_version}}.glibc${{matrix.glibc_version}}-x86_64.AppImage -o ~/vim/bin/vim + chmod u+x ~/vim/bin/vim + - name: Download test runner + shell: bash + run: git clone --depth 1 --branch v1.5.4 --single-branch https://github.com/thinca/vim-themis ~/themis + - name: Run tests + shell: bash + run: | + export PATH=~/vim/bin:$PATH + export PATH=~/themis/bin:$PATH + export THEMIS_VIM=vim + vim --version + themis --reporter spec diff --git a/.github/workflows/mac_neovim.yml b/.github/workflows/mac_neovim.yml new file mode 100644 index 00000000..894b636e --- /dev/null +++ b/.github/workflows/mac_neovim.yml @@ -0,0 +1,44 @@ +name: mac_neovim + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [macos-latest] + name: [neovim-v04-x64,neovim-nightly-x64] + include: + - name: neovim-v04-x64 + os: macos-latest + neovim_version: v0.4.3 + - name: neovim-nightly-x64 + os: macos-latest + neovim_version: nightly + runs-on: ${{matrix.os}} + steps: + - uses: actions/checkout@v4 + - name: Download neovim + shell: bash + run: curl -L https://github.com/neovim/neovim/releases/download/${{matrix.neovim_version}}/nvim-macos.tar.gz -o ~/nvim.tar.gz + - name: Extract neovim + shell: bash + run: tar xzf ~/nvim.tar.gz -C ~/ + - name: Download test runner + shell: bash + run: git clone --depth 1 --branch v1.5.4 --single-branch https://github.com/thinca/vim-themis ~/themis + - name: Run tests + shell: bash + run: | + export PATH=~/nvim-osx64/bin:$PATH + export PATH=~/themis/bin:$PATH + export THEMIS_VIM=nvim + nvim --version + themis --reporter spec diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml new file mode 100644 index 00000000..48f5b082 --- /dev/null +++ b/.github/workflows/reviewdog.yml @@ -0,0 +1,22 @@ +name: reviewdog + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + vimlint: + name: runner / vint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: vint + uses: reviewdog/action-vint@v1 + with: + github_token: ${{ secrets.github_token }} + level: error + reporter: github-pr-review diff --git a/.github/workflows/windows_neovim.yml b/.github/workflows/windows_neovim.yml new file mode 100644 index 00000000..06013f6d --- /dev/null +++ b/.github/workflows/windows_neovim.yml @@ -0,0 +1,46 @@ +name: windows_neovim + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [windows-latest] + name: [neovim-v04-x64,neovim-nightly-x64] + include: + - name: neovim-v04-x64 + os: windows-latest + neovim_version: v0.4.3 + neovim_arch: win64 + - name: neovim-nightly-x64 + os: windows-latest + neovim_version: nightly + neovim_arch: win64 + runs-on: ${{matrix.os}} + steps: + - uses: actions/checkout@v4 + - name: Download neovim + shell: PowerShell + run: Invoke-WebRequest -Uri https://github.com/neovim/neovim/releases/download/${{matrix.neovim_version}}/nvim-${{matrix.neovim_arch}}.zip -OutFile neovim.zip + - name: Extract neovim + shell: PowerShell + run: Expand-Archive -Path neovim.zip -DestinationPath $env:USERPROFILE + - name: Download test runner + shell: PowerShell + run: git clone --depth 1 --branch v1.5.4 --single-branch https://github.com/thinca/vim-themis $env:USERPROFILE\themis + - name: Run tests + shell: cmd + run: | + SET PATH=%USERPROFILE%\Neovim\bin;%PATH%; + SET PATH=%USERPROFILE%\themis\bin;%PATH%; + SET THEMIS_VIM=nvim + nvim --version + themis --reporter spec diff --git a/.github/workflows/windows_vim.yml b/.github/workflows/windows_vim.yml new file mode 100644 index 00000000..26b2641e --- /dev/null +++ b/.github/workflows/windows_vim.yml @@ -0,0 +1,52 @@ +name: windows_vim + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [windows-latest] + name: [vim-v82-x64, vim-v81-x64, vim-v80-x64] + include: + - name: vim-v82-x64 + os: windows-latest + vim_version: 8.2.0813 + vim_arch: x64 + vim_ver_path: vim82 + - name: vim-v81-x64 + os: windows-latest + vim_version: 8.1.2414 + vim_arch: x64 + vim_ver_path: vim81 + - name: vim-v80-x64 + os: windows-latest + vim_version: 8.0.1567 + vim_arch: x64 + vim_ver_path: vim80 + runs-on: ${{matrix.os}} + steps: + - uses: actions/checkout@v4 + - name: Download vim + shell: PowerShell + run: Invoke-WebRequest -Uri https://github.com/vim/vim-win32-installer/releases/download/v${{matrix.vim_version}}/gvim_${{matrix.vim_version}}_${{matrix.vim_arch}}.zip -OutFile vim.zip + - name: Extract vim + shell: PowerShell + run: Expand-Archive -Path vim.zip -DestinationPath $env:USERPROFILE + - name: Download test runner + shell: PowerShell + run: git clone --depth 1 --branch v1.5.4 --single-branch https://github.com/thinca/vim-themis $env:USERPROFILE\themis + - name: Run tests + shell: cmd + run: | + SET PATH=%USERPROFILE%\vim\${{matrix.vim_ver_path}};%PATH%; + SET PATH=%USERPROFILE%\themis\bin;%PATH%; + vim --version + themis --reporter spec diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6e92f57d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tags diff --git a/.vintrc.yaml b/.vintrc.yaml new file mode 100644 index 00000000..c96daa07 --- /dev/null +++ b/.vintrc.yaml @@ -0,0 +1,10 @@ +cmdargs: + severity: style_problem + +policies: + ProhibitUnusedVariable: + enabled: false + ProhibitImplicitScopeVariable: + enabled: true + ProhibitNoAbortFunction: + enabled: true diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..cdd1845c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Prabir Shrestha + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..9b7711f8 --- /dev/null +++ b/README.md @@ -0,0 +1,245 @@ +asyncomplete.vim +================ + +Async autocompletion for Vim 8 and Neovim with |timers|. + +This is inspired by [nvim-complete-manager](https://github.com/roxma/nvim-complete-manager) but written +in pure Vim Script. + +### Installing + +```viml +Plug 'prabirshrestha/asyncomplete.vim' +``` + +#### Tab completion + +```vim +inoremap pumvisible() ? "\" : "\" +inoremap pumvisible() ? "\" : "\" +inoremap pumvisible() ? asyncomplete#close_popup() : "\" +``` + +If you prefer the enter key to always insert a new line (even if the popup menu is visible) then +you can amend the above mapping as follows: + +```vim +inoremap pumvisible() ? asyncomplete#close_popup() . "\" : "\" +``` + +### Force refresh completion + +```vim +imap (asyncomplete_force_refresh) +" For Vim 8 ( corresponds to ): +" imap (asyncomplete_force_refresh) +``` + +### Auto popup +By default asyncomplete will automatically show the autocomplete popup menu as you start typing. +If you would like to disable the default behavior set `g:asyncomplete_auto_popup` to 0. + +```vim +let g:asyncomplete_auto_popup = 0 +``` + +You can use the above `(asyncomplete_force_refresh)` to show the popup +or you can tab to show the autocomplete. + +```vim +let g:asyncomplete_auto_popup = 0 + +function! s:check_back_space() abort + let col = col('.') - 1 + return !col || getline('.')[col - 1] =~ '\s' +endfunction + +inoremap + \ pumvisible() ? "\" : + \ check_back_space() ? "\" : + \ asyncomplete#force_refresh() +inoremap pumvisible() ? "\" : "\" +``` + +#### Preview Window + +To enable preview window: + +```vim +" allow modifying the completeopt variable, or it will +" be overridden all the time +let g:asyncomplete_auto_completeopt = 0 + +set completeopt=menuone,noinsert,noselect,preview +``` + +To auto close preview window when completion is done. + +```vim +autocmd! CompleteDone * if pumvisible() == 0 | pclose | endif +``` + +### Sources + +asyncomplete.vim deliberately does not contain any sources. Please use one of the following sources or create your own. + +#### Language Server Protocol (LSP) +[Language Server Protocol](https://github.com/Microsoft/language-server-protocol) via [vim-lsp](https://github.com/prabirshrestha/vim-lsp) and [asyncomplete-lsp.vim](https://github.com/prabirshrestha/asyncomplete-lsp.vim) + +**Please note** that vim-lsp setup for neovim requires neovim v0.2.0 or higher, since it uses lambda setup. + +```vim +Plug 'prabirshrestha/asyncomplete.vim' +Plug 'prabirshrestha/vim-lsp' +Plug 'prabirshrestha/asyncomplete-lsp.vim' + +if executable('pyls') + " pip install python-language-server + au User lsp_setup call lsp#register_server({ + \ 'name': 'pyls', + \ 'cmd': {server_info->['pyls']}, + \ 'allowlist': ['python'], + \ }) +endif +``` + +**Refer to [vim-lsp wiki](https://github.com/prabirshrestha/vim-lsp/wiki/Servers) for configuring other language servers.** Besides auto-complete language server support other features such as go to definition, find references, renaming symbols, document symbols, find workspace symbols, formatting and so on. + +*in alphabetical order* + +| Languages/FileType/Source | Links | +|-------------------------------|----------------------------------------------------------------------------------------------------| +| [Ale][ale] | [asyncomplete-ale.vim](https://github.com/andreypopp/asyncomplete-ale.vim) | +| Buffer | [asyncomplete-buffer.vim](https://github.com/prabirshrestha/asyncomplete-buffer.vim) | +| C/C++ | [asyncomplete-clang.vim](https://github.com/keremc/asyncomplete-clang.vim) | +| Clojure | [async-clj-omni](https://github.com/clojure-vim/async-clj-omni) | +| Common Lisp (vlime) | [vlime](https://github.com/vlime/vlime) | +| Dictionary (look) | [asyncomplete-look](https://github.com/htlsne/asyncomplete-look) | +| [Emmet][emmet-vim] | [asyncomplete-emmet.vim](https://github.com/prabirshrestha/asyncomplete-emmet.vim) | +| English | [asyncomplete-nextword.vim](https://github.com/high-moctane/asyncomplete-nextword.vim) | +| Emoji | [asyncomplete-emoji.vim](https://github.com/prabirshrestha/asyncomplete-emoji.vim) | +| Filenames / directories | [asyncomplete-file.vim](https://github.com/prabirshrestha/asyncomplete-file.vim) | +| [NeoInclude][neoinclude] | [asyncomplete-neoinclude.vim](https://github.com/kyouryuukunn/asyncomplete-neoinclude.vim) | +| Go | [asyncomplete-gocode.vim](https://github.com/prabirshrestha/asyncomplete-gocode.vim) | +| Git commit message | [asyncomplete-gitcommit](https://github.com/laixintao/asyncomplete-gitcommit) | +| JavaScript (Flow) | [asyncomplete-flow.vim](https://github.com/prabirshrestha/asyncomplete-flow.vim) | +| [Neosnippet][neosnippet] | [asyncomplete-neosnippet.vim](https://github.com/prabirshrestha/asyncomplete-neosnippet.vim) | +| Omni | [asyncomplete-omni.vim](https://github.com/yami-beta/asyncomplete-omni.vim) | +| PivotalTracker stories | [asyncomplete-pivotaltracker.vim](https://github.com/hauleth/asyncomplete-pivotaltracker.vim) | +| Rust (racer) | [asyncomplete-racer.vim](https://github.com/keremc/asyncomplete-racer.vim) | +| [TabNine][TabNine] powered by AI | [asyncomplete-tabnine.vim](https://github.com/kitagry/asyncomplete-tabnine.vim) | +| [tmux complete][tmuxcomplete] | [tmux-complete.vim][tmuxcomplete] | +| Typescript | [asyncomplete-tscompletejob.vim](https://github.com/prabirshrestha/asyncomplete-tscompletejob.vim) | +| [UltiSnips][ultisnips] | [asyncomplete-ultisnips.vim](https://github.com/prabirshrestha/asyncomplete-ultisnips.vim) | +| User (compl-function) | [asyncomplete-user.vim](https://github.com/jsit/asyncomplete-user.vim) | +| Vim Syntax | [asyncomplete-necosyntax.vim](https://github.com/prabirshrestha/asyncomplete-necosyntax.vim) | +| Vim tags | [asyncomplete-tags.vim](https://github.com/prabirshrestha/asyncomplete-tags.vim) | +| Vim | [asyncomplete-necovim.vim](https://github.com/prabirshrestha/asyncomplete-necovim.vim) | + +[ale]: https://github.com/dense-analysis/ale +[emmet-vim]: https://github.com/mattn/emmet-vim +[neosnippet]: https://github.com/Shougo/neosnippet.vim +[neoinclude]: https://github.com/Shougo/neoinclude.vim +[TabNine]: https://www.tabnine.com/ +[tmuxcomplete]: https://github.com/wellle/tmux-complete.vim +[ultisnips]: https://github.com/SirVer/ultisnips + +*can't find what you are looking for? write one instead an send a PR to be included here or search github topics tagged with asyncomplete at https://github.com/topics/asyncomplete.* + +#### Using existing vim plugin sources + +Rather than writing your own completion source from scratch you could also suggests other plugin authors to provide a async completion api that works for asyncomplete.vim or any other async autocomplete libraries without taking a dependency on asyncomplete.vim. The plugin can provide a function that takes a callback which returns the list of candidates and the startcol from where it must show the popup. Candidates can be list of words or vim's `complete-items`. + +```vim +function s:completor(opt, ctx) + call mylanguage#get_async_completions({candidates, startcol -> asyncomplete#complete(a:opt['name'], a:ctx, startcol, candidates) }) +endfunction + +au User asyncomplete_setup call asyncomplete#register_source({ + \ 'name': 'mylanguage', + \ 'allowlist': ['*'], + \ 'completor': function('s:completor'), + \ }) +``` + +### Example + +```vim +function! s:js_completor(opt, ctx) abort + let l:col = a:ctx['col'] + let l:typed = a:ctx['typed'] + + let l:kw = matchstr(l:typed, '\v\S+$') + let l:kwlen = len(l:kw) + + let l:startcol = l:col - l:kwlen + + let l:matches = [ + \ "do", "if", "in", "for", "let", "new", "try", "var", "case", "else", "enum", "eval", "null", "this", "true", + \ "void", "with", "await", "break", "catch", "class", "const", "false", "super", "throw", "while", "yield", + \ "delete", "export", "import", "public", "return", "static", "switch", "typeof", "default", "extends", + \ "finally", "package", "private", "continue", "debugger", "function", "arguments", "interface", "protected", + \ "implements", "instanceof" + \ ] + + call asyncomplete#complete(a:opt['name'], a:ctx, l:startcol, l:matches) +endfunction + +au User asyncomplete_setup call asyncomplete#register_source({ + \ 'name': 'javascript', + \ 'allowlist': ['javascript'], + \ 'completor': function('s:js_completor'), + \ }) +``` + +The above sample shows synchronous completion. If you would like to make it async just call `asyncomplete#complete` whenever you have the results ready. + +```vim +call timer_start(2000, {timer-> asyncomplete#complete(a:opt['name'], a:ctx, l:startcol, l:matches)}) +``` + +If you are returning incomplete results and would like to trigger completion on the next keypress pass `1` as the fifth parameter to `asyncomplete#complete` +which signifies the result is incomplete. + +```vim +call asyncomplete#complete(a:opt['name'], a:ctx, l:startcol, l:matches, 1) +``` + +As a source author you do not have to worry about synchronization issues in case the server returns the async completion after the user has typed more +characters. asyncomplete.vim uses partial caching as well as ignores if the context changes when calling `asyncomplete#complete`. +This is one of the core reason why the original context must be passed when calling `asyncomplete#complete`. + +### Credits +All the credit goes to the following projects +* [https://github.com/roxma/nvim-complete-manager](https://github.com/roxma/nvim-complete-manager) +* [https://github.com/maralla/completor.vim](https://github.com/maralla/completor.vim) + +## Contributors + +### Code Contributors + +This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. + + +### Financial Contributors + +Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/asyncomplete/contribute)] + +#### Individuals + + + +#### Organizations + +Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/asyncomplete/contribute)] + + + + + + + + + + + diff --git a/autoload/asyncomplete.vim b/autoload/asyncomplete.vim new file mode 100644 index 00000000..d5e4e4bb --- /dev/null +++ b/autoload/asyncomplete.vim @@ -0,0 +1,540 @@ +function! asyncomplete#log(...) abort + if !empty(g:asyncomplete_log_file) + call writefile([json_encode(a:000)], g:asyncomplete_log_file, 'a') + endif +endfunction + +" do nothing, place it here only to avoid the message +augroup asyncomplete_silence_messages + au! + autocmd User asyncomplete_setup silent +augroup END + +if !has('timers') + echohl ErrorMsg + echomsg 'Vim/Neovim compiled with timers required for asyncomplete.vim.' + echohl NONE + if has('nvim') + call asyncomplete#log('neovim compiled with timers required.') + else + call asyncomplete#log('vim compiled with timers required.') + endif + " Clear augroup so this message is only displayed once. + au! asyncomplete_enable * + finish +endif + +let s:already_setup = 0 +let s:sources = {} +let s:matches = {} " { server_name: { incomplete: 1, startcol: 0, items: [], refresh: 0, status: 'idle|pending|success|failure', ctx: ctx } } +let s:has_complete_info = exists('*complete_info') +let s:has_matchfuzzypos = exists('*matchfuzzypos') + +function! s:setup_if_required() abort + if !s:already_setup + " register asyncomplete change manager + for l:change_manager in g:asyncomplete_change_manager + call asyncomplete#log('core', 'initializing asyncomplete change manager', l:change_manager) + if type(l:change_manager) == type('') + execute 'let s:on_change_manager = function("'. l:change_manager .'")()' + else + let s:on_change_manager = l:change_manager() + endif + if has_key(s:on_change_manager, 'error') + call asyncomplete#log('core', 'initializing asyncomplete change manager failed', s:on_change_manager['name'], s:on_change_manager['error']) + else + call s:on_change_manager.register(function('s:on_change')) + call asyncomplete#log('core', 'initializing asyncomplete change manager complete', s:on_change_manager['name']) + break + endif + endfor + + augroup asyncomplete + autocmd! + autocmd InsertEnter * call s:on_insert_enter() + autocmd InsertLeave * call s:on_insert_leave() + augroup END + + doautocmd User asyncomplete_setup + let s:already_setup = 1 + endif +endfunction + +function! asyncomplete#enable_for_buffer() abort + call s:setup_if_required() + let b:asyncomplete_enable = 1 +endfunction + +function! asyncomplete#disable_for_buffer() abort + let b:asyncomplete_enable = 0 +endfunction + +function! asyncomplete#get_source_names() abort + return keys(s:sources) +endfunction + +function! asyncomplete#get_source_info(source_name) abort + return s:sources[a:source_name] +endfunction + +function! asyncomplete#register_source(info) abort + if has_key(s:sources, a:info['name']) + call asyncomplete#log('core', 'duplicate asyncomplete#register_source', a:info['name']) + return -1 + else + let s:sources[a:info['name']] = a:info + if has_key(a:info, 'events') && has_key(a:info, 'on_event') + execute 'augroup asyncomplete_source_event_' . a:info['name'] + for l:event in a:info['events'] + let l:exec = 'if get(b:,"asyncomplete_enable",0) | call s:notify_event_to_source("' . a:info['name'] . '", "'.l:event.'",asyncomplete#context()) | endif' + if type(l:event) == type('') + execute 'au ' . l:event . ' * ' . l:exec + elseif type(l:event) == type([]) + execute 'au ' . join(l:event,' ') .' ' . l:exec + endif + endfor + execute 'augroup end' + endif + + if exists('b:asyncomplete_active_sources') + unlet b:asyncomplete_active_sources + call s:get_active_sources_for_buffer() + endif + + if exists('b:asyncomplete_triggers') + unlet b:asyncomplete_triggers + call s:update_trigger_characters() + endif + + return 1 + endif +endfunction + +function! asyncomplete#unregister_source(info_or_server_name) abort + if type(a:info_or_server_name) == type({}) + let l:server_name = a:info_or_server_name['name'] + else + let l:server_name = a:info_or_server_name + endif + if has_key(s:sources, l:server_name) + let l:server = s:sources[l:server_name] + if has_key(l:server, 'unregister') + call l:server.unregister() + endif + unlet s:sources[l:server_name] + return 1 + else + return -1 + endif +endfunction + +function! asyncomplete#context() abort + let l:ret = {'bufnr':bufnr('%'), 'curpos':getcurpos(), 'changedtick':b:changedtick} + let l:ret['lnum'] = l:ret['curpos'][1] + let l:ret['col'] = l:ret['curpos'][2] + let l:ret['filetype'] = &filetype + let l:ret['filepath'] = expand('%:p') + let l:ret['typed'] = strpart(getline(l:ret['lnum']),0,l:ret['col']-1) + return l:ret +endfunction + +function! s:on_insert_enter() abort + call s:get_active_sources_for_buffer() " call to cache + call s:update_trigger_characters() +endfunction + +function! s:on_insert_leave() abort + let s:matches = {} + if exists('s:update_pum_timer') + call timer_stop(s:update_pum_timer) + unlet s:update_pum_timer + endif +endfunction + +function! s:get_active_sources_for_buffer() abort + if exists('b:asyncomplete_active_sources') + " active sources were cached for buffer + return b:asyncomplete_active_sources + endif + + call asyncomplete#log('core', 'computing active sources for buffer', bufnr('%')) + let b:asyncomplete_active_sources = [] + for [l:name, l:info] in items(s:sources) + let l:blocked = 0 + + if has_key(l:info, 'blocklist') + let l:blocklistkey = 'blocklist' + else + let l:blocklistkey = 'blacklist' + endif + if has_key(l:info, l:blocklistkey) + for l:filetype in l:info[l:blocklistkey] + if l:filetype == &filetype || l:filetype is# '*' + let l:blocked = 1 + break + endif + endfor + endif + + if l:blocked + continue + endif + + if has_key(l:info, 'allowlist') + let l:allowlistkey = 'allowlist' + else + let l:allowlistkey = 'whitelist' + endif + if has_key(l:info, l:allowlistkey) + for l:filetype in l:info[l:allowlistkey] + if l:filetype == &filetype || l:filetype is# '*' + let b:asyncomplete_active_sources += [l:name] + break + endif + endfor + endif + endfor + + call asyncomplete#log('core', 'active source for buffer', bufnr('%'), b:asyncomplete_active_sources) + + return b:asyncomplete_active_sources +endfunction + +function! s:update_trigger_characters() abort + if exists('b:asyncomplete_triggers') + " triggers were cached for buffer + return b:asyncomplete_triggers + endif + let b:asyncomplete_triggers = {} " { char: { 'sourcea': 1, 'sourceb': 2 } } + + for l:source_name in s:get_active_sources_for_buffer() + let l:source_info = s:sources[l:source_name] + if has_key(l:source_info, 'triggers') && has_key(l:source_info['triggers'], &filetype) + let l:triggers = l:source_info['triggers'][&filetype] + elseif has_key(l:source_info, 'triggers') && has_key(l:source_info['triggers'], '*') + let l:triggers = l:source_info['triggers']['*'] + elseif has_key(g:asyncomplete_triggers, &filetype) + let l:triggers = g:asyncomplete_triggers[&filetype] + elseif has_key(g:asyncomplete_triggers, '*') + let l:triggers = g:asyncomplete_triggers['*'] + else + let l:triggers = [] + endif + + for l:trigger in l:triggers + let l:last_char = l:trigger[len(l:trigger) -1] + if !has_key(b:asyncomplete_triggers, l:last_char) + let b:asyncomplete_triggers[l:last_char] = {} + endif + if !has_key(b:asyncomplete_triggers[l:last_char], l:source_name) + let b:asyncomplete_triggers[l:last_char][l:source_name] = [] + endif + call add(b:asyncomplete_triggers[l:last_char][l:source_name], l:trigger) + endfor + endfor + call asyncomplete#log('core', 'trigger characters for buffer', bufnr('%'), b:asyncomplete_triggers) +endfunction + +function! s:should_skip() abort + if mode() isnot# 'i' || !get(b:, 'asyncomplete_enable', 0) + return 1 + else + return 0 + endif +endfunction + +function! asyncomplete#close_popup() abort + return pumvisible() ? "\" : '' +endfunction + +function! asyncomplete#cancel_popup() abort + return pumvisible() ? "\" : '' +endfunction + +function! s:get_min_chars(source_name) abort + if exists('b:asyncomplete_min_chars') + return b:asyncomplete_min_chars + elseif has_key(s:sources, a:source_name) + return get(s:sources[a:source_name], 'min_chars', g:asyncomplete_min_chars) + endif + return g:asyncomplete_min_chars +endfunction + +function! s:on_change() abort + if s:should_skip() | return | endif + + if !g:asyncomplete_auto_popup + return + endif + + let l:ctx = asyncomplete#context() + let l:last_char = l:ctx['typed'][l:ctx['col'] - 2] " col is 1-indexed, but str 0-indexed + if exists('b:asyncomplete_triggers') + let l:triggered_sources = get(b:asyncomplete_triggers, l:last_char, {}) + else + let l:triggered_sources = {} + endif + let l:refresh_pattern = get(b:, 'asyncomplete_refresh_pattern', '\(\k\+$\)') + let [l:_, l:startidx, l:endidx] = asyncomplete#utils#matchstrpos(l:ctx['typed'], l:refresh_pattern) + + for l:source_name in get(b:, 'asyncomplete_active_sources', []) + " match sources based on the last character if it is a trigger character + " TODO: also check for multiple chars instead of just last chars for + " languages such as cpp which uses -> and :: + if has_key(l:triggered_sources, l:source_name) + let l:startcol = l:ctx['col'] + elseif l:startidx > -1 + let l:startcol = l:startidx + 1 " col is 1-indexed, but str 0-indexed + endif + " here we use the existence of `l:startcol` to determine whether to + " use this completion source. If `l:startcol` exists, we use the + " source. If it does not exist, it means that we cannot get a + " meaningful starting point for the current source, and this implies + " that we cannot use this source for completion. Therefore, we remove + " the matches from the source. + if exists('l:startcol') && l:endidx - l:startidx >= s:get_min_chars(l:source_name) + if !has_key(s:matches, l:source_name) || s:matches[l:source_name]['ctx']['lnum'] !=# l:ctx['lnum'] || s:matches[l:source_name]['startcol'] !=# l:startcol + let s:matches[l:source_name] = { 'startcol': l:startcol, 'status': 'idle', 'items': [], 'refresh': 0, 'ctx': l:ctx } + endif + else + if has_key(s:matches, l:source_name) + unlet s:matches[l:source_name] + endif + endif + endfor + + call s:trigger(l:ctx) + call s:update_pum() +endfunction + +function! s:trigger(ctx) abort + " send cancellation request if supported + for [l:source_name, l:matches] in items(s:matches) + call asyncomplete#log('core', 's:trigger', l:matches) + if l:matches['refresh'] || l:matches['status'] ==# 'idle' || l:matches['status'] ==# 'failure' + let l:matches['status'] = 'pending' + try + " TODO: check for min chars + call asyncomplete#log('core', 's:trigger.completor()', l:source_name, s:matches[l:source_name], a:ctx) + call s:sources[l:source_name].completor(s:sources[l:source_name], a:ctx) + catch + let l:matches['status'] = 'failure' + call asyncomplete#log('core', 's:trigger', 'error', v:exception) + continue + endtry + endif + endfor +endfunction + +function! asyncomplete#complete(name, ctx, startcol, items, ...) abort + let l:refresh = a:0 > 0 ? a:1 : 0 + let l:ctx = asyncomplete#context() + if !has_key(s:matches, a:name) || l:ctx['lnum'] != a:ctx['lnum'] " TODO: handle more context changes + call asyncomplete#log('core', 'asyncomplete#log', 'ignoring due to context chnages', a:name, a:ctx, a:startcol, l:refresh, a:items) + call s:update_pum() + return + endif + + call asyncomplete#log('asyncomplete#complete', a:name, a:ctx, a:startcol, l:refresh, a:items) + + let l:matches = s:matches[a:name] + let l:matches['items'] = s:normalize_items(a:items) + let l:matches['refresh'] = l:refresh + let l:matches['startcol'] = a:startcol + let l:matches['status'] = 'success' + + call s:update_pum() +endfunction + +function! s:normalize_items(items) abort + if len(a:items) > 0 && type(a:items[0]) ==# type('') + let l:items = [] + for l:item in a:items + let l:items += [{'word': l:item }] + endfor + return l:items + else + return a:items + endif +endfunction + +function! asyncomplete#force_refresh() abort + return asyncomplete#menu_selected() ? "\\=asyncomplete#_force_refresh()\" : "\=asyncomplete#_force_refresh()\" +endfunction + +function! asyncomplete#_force_refresh() abort + if s:should_skip() | return | endif + + let l:ctx = asyncomplete#context() + let l:startcol = l:ctx['col'] + let l:last_char = l:ctx['typed'][l:startcol - 2] + + " loop left and find the start of the word or trigger chars and set it as the startcol for the source instead of refresh_pattern + let l:refresh_pattern = get(b:, 'asyncomplete_refresh_pattern', '\(\k\+$\)') + let [l:_, l:startidx, l:endidx] = asyncomplete#utils#matchstrpos(l:ctx['typed'], l:refresh_pattern) + " When no word here, startcol is current col + let l:startcol = l:startidx == -1 ? col('.') : l:startidx + 1 + + let s:matches = {} + + for l:source_name in get(b:, 'asyncomplete_active_sources', []) + let s:matches[l:source_name] = { 'startcol': l:startcol, 'status': 'idle', 'items': [], 'refresh': 0, 'ctx': l:ctx } + endfor + + call s:trigger(l:ctx) + call s:update_pum() + return '' +endfunction + +function! s:update_pum() abort + if exists('s:update_pum_timer') + call timer_stop(s:update_pum_timer) + unlet s:update_pum_timer + endif + call asyncomplete#log('core', 's:update_pum') + let s:update_pum_timer = timer_start(g:asyncomplete_popup_delay, function('s:recompute_pum')) +endfunction + +function! s:recompute_pum(...) abort + if s:should_skip() | return | endif + + " TODO: add support for remote recomputation of complete items, + " Ex: heavy computation such as fuzzy search can happen in a python thread + + call asyncomplete#log('core', 's:recompute_pum') + + if asyncomplete#menu_selected() + call asyncomplete#log('core', 's:recomputed_pum', 'ignorning refresh pum due to menu selection') + return + endif + + let l:ctx = asyncomplete#context() + + let l:startcols = [] + let l:matches_to_filter = {} + + for [l:source_name, l:match] in items(s:matches) + " ignore sources that have been unregistered + if !has_key(s:sources, l:source_name) | continue | endif + let l:startcol = l:match['startcol'] + let l:startcols += [l:startcol] + let l:curitems = l:match['items'] + + if l:startcol > l:ctx['col'] + call asyncomplete#log('core', 's:recompute_pum', 'ignoring due to wrong start col', l:startcol, l:ctx['col']) + continue + else + let l:matches_to_filter[l:source_name] = l:match + endif + endfor + + let l:startcol = min(l:startcols) + let l:base = l:ctx['typed'][l:startcol - 1:] " col is 1-indexed, but str 0-indexed + + let l:filter_ctx = extend({ + \ 'base': l:base, + \ 'startcol': l:startcol, + \ }, l:ctx) + + let l:mode = s:has_complete_info ? complete_info(['mode'])['mode'] : 'unknown' + if l:mode ==# '' || l:mode ==# 'eval' || l:mode ==# 'unknown' + let l:Preprocessor = empty(g:asyncomplete_preprocessor) ? function('s:default_preprocessor') : g:asyncomplete_preprocessor[0] + call l:Preprocessor(l:filter_ctx, l:matches_to_filter) + endif +endfunction + +let s:pair = { +\ '"': '"', +\ '''': '''', +\} + +function! s:default_preprocessor(options, matches) abort + let l:items = [] + let l:startcols = [] + for [l:source_name, l:matches] in items(a:matches) + let l:startcol = l:matches['startcol'] + let l:base = a:options['typed'][l:startcol - 1:] + if has_key(s:sources[l:source_name], 'filter') + let l:result = s:sources[l:source_name].filter(l:matches, l:startcol, l:base) + let l:items += l:result[0] + let l:startcols += l:result[1] + else + if empty(l:base) + for l:item in l:matches['items'] + call add(l:items, s:strip_pair_characters(l:base, l:item)) + let l:startcols += [l:startcol] + endfor + elseif s:has_matchfuzzypos && g:asyncomplete_matchfuzzy + for l:item in matchfuzzypos(l:matches['items'], l:base, {'key':'word'})[0] + call add(l:items, s:strip_pair_characters(l:base, l:item)) + let l:startcols += [l:startcol] + endfor + else + for l:item in l:matches['items'] + if stridx(l:item['word'], l:base) == 0 + call add(l:items, s:strip_pair_characters(l:base, l:item)) + let l:startcols += [l:startcol] + endif + endfor + endif + endif + endfor + + let a:options['startcol'] = min(l:startcols) + + call asyncomplete#preprocess_complete(a:options, l:items) +endfunction + +function! s:strip_pair_characters(base, item) abort + " Strip pair characters. If pre-typed text is '"', candidates + " should have '"' suffix. + let l:item = a:item + if has_key(s:pair, a:base[0]) + let [l:lhs, l:rhs, l:str] = [a:base[0], s:pair[a:base[0]], l:item['word']] + if len(l:str) > 1 && l:str[0] ==# l:lhs && l:str[-1:] ==# l:rhs + let l:item = extend({}, l:item) + let l:item['word'] = l:str[:-2] + endif + endif + return l:item +endfunction + +function! asyncomplete#preprocess_complete(ctx, items) abort + " TODO: handle cases where this is called asynchronsouly. Currently not supported + if s:should_skip() | return | endif + + call asyncomplete#log('core', 'asyncomplete#preprocess_complete') + + if asyncomplete#menu_selected() + call asyncomplete#log('core', 'asyncomplete#preprocess_complete', 'ignorning pum update due to menu selection') + return + endif + + if (g:asyncomplete_auto_completeopt == 1) + setl completeopt=menuone,noinsert,noselect + endif + + let l:startcol = a:ctx['startcol'] + call asyncomplete#log('core', 'asyncomplete#preprocess_complete calling complete()', l:startcol, a:items) + if l:startcol > 0 " Prevent E578: Not allowed to change text here + call complete(l:startcol, a:items) + endif +endfunction + +function! asyncomplete#menu_selected() abort + " when the popup menu is visible, v:completed_item will be the + " current_selected item + " if v:completed_item is empty, no item is selected + return pumvisible() && !empty(v:completed_item) +endfunction + +function! s:notify_event_to_source(name, event, ctx) abort + try + if has_key(s:sources, a:name) + call s:sources[a:name].on_event(s:sources[a:name], a:ctx, a:event) + endif + catch + call asyncomplete#log('core', 's:notify_event_to_source', 'error', v:exception) + return + endtry +endfunction diff --git a/autoload/asyncomplete/utils.vim b/autoload/asyncomplete/utils.vim new file mode 100644 index 00000000..0a1c24f9 --- /dev/null +++ b/autoload/asyncomplete/utils.vim @@ -0,0 +1,21 @@ +" Find a nearest to a `path` parent directory `directoryname` by traversing the +" filesystem upwards +function! asyncomplete#utils#find_nearest_parent_directory(path, directoryname) abort + let l:relative_path = finddir(a:directoryname, a:path . ';') + + if !empty(l:relative_path) + return fnamemodify(l:relative_path, ':p') + else + return '' + endif +endfunction + +if exists('*matchstrpos') + function! asyncomplete#utils#matchstrpos(expr, pattern) abort + return matchstrpos(a:expr, a:pattern) + endfunction +else + function! asyncomplete#utils#matchstrpos(expr, pattern) abort + return [matchstr(a:expr, a:pattern), match(a:expr, a:pattern), matchend(a:expr, a:pattern)] + endfunction +endif diff --git a/autoload/asyncomplete/utils/_on_change/textchangedp.vim b/autoload/asyncomplete/utils/_on_change/textchangedp.vim new file mode 100644 index 00000000..61c49bdc --- /dev/null +++ b/autoload/asyncomplete/utils/_on_change/textchangedp.vim @@ -0,0 +1,81 @@ +let s:callbacks = [] + +function! asyncomplete#utils#_on_change#textchangedp#init() abort + if exists('##TextChangedP') + call s:setup_if_required() + return { + \ 'name': 'TextChangedP', + \ 'register': function('s:register'), + \ 'unregister': function('s:unregister'), + \ } + else + return { 'name': 'TextChangedP', 'error': 'Requires vim with TextChangedP support' } + endif +endfunction + +function! s:setup_if_required() abort + augroup asyncomplete_utils_on_change_text_changed_p + autocmd! + autocmd InsertEnter * call s:on_insert_enter() + autocmd InsertLeave * call s:on_insert_leave() + autocmd TextChangedI * call s:on_text_changed_i() + autocmd TextChangedP * call s:on_text_changed_p() + augroup END +endfunction + +function! s:register(cb) abort + call add(s:callbacks , a:cb) +endfunction + +function! s:unregister(obj, cb) abort + " TODO: remove from s:callbacks +endfunction + +function! s:on_insert_enter() abort + let l:context = asyncomplete#context() + let s:previous_context = { + \ 'lnum': l:context['lnum'], + \ 'col': l:context['col'], + \ 'typed': l:context['typed'], + \ } +endfunction + +function! s:on_insert_leave() abort + unlet! s:previous_context +endfunction + +function! s:on_text_changed_i() abort + call s:maybe_notify_on_change() +endfunction + +function! s:on_text_changed_p() abort + call s:maybe_notify_on_change() +endfunction + +function! s:maybe_notify_on_change() abort + if !exists('s:previous_context') + return + endif + " We notify on_change callbacks only when the cursor position + " has changed. + " Unfortunatelly we need this check because in insert mode it + " is possible to have TextChangedI triggered when the completion + " context is not changed at all: When we close the completion + " popup menu via or . If we still let on_change + " do the completion in this case we never close the menu. + " Vim doesn't allow programmatically changing buffer content + " in insert mode, so by comparing the cursor's position and the + " completion base we know whether the context has changed. + let l:context = asyncomplete#context() + let l:previous_context = s:previous_context + let s:previous_context = { + \ 'lnum': l:context['lnum'], + \ 'col': l:context['col'], + \ 'typed': l:context['typed'], + \ } + if l:previous_context !=# s:previous_context + for l:Cb in s:callbacks + call l:Cb() + endfor + endif +endfunction diff --git a/autoload/asyncomplete/utils/_on_change/timer.vim b/autoload/asyncomplete/utils/_on_change/timer.vim new file mode 100644 index 00000000..2a0c1fc8 --- /dev/null +++ b/autoload/asyncomplete/utils/_on_change/timer.vim @@ -0,0 +1,83 @@ +let s:callbacks = [] + +let s:change_timer = -1 +let s:last_tick = [] + +function! asyncomplete#utils#_on_change#timer#init() abort + call s:setup_if_required() + return { + \ 'name': 'timer', + \ 'register': function('s:register'), + \ 'unregister': function('s:unregister'), + \ } +endfunction + +function! s:setup_if_required() abort + augroup asyncomplete_utils_on_change_timer + autocmd! + autocmd InsertEnter * call s:on_insert_enter() + autocmd InsertLeave * call s:on_insert_leave() + autocmd TextChangedI * call s:on_text_changed_i() + augroup END +endfunction + +function! s:register(cb) abort + call add(s:callbacks , a:cb) +endfunction + +function! s:unregister(obj, cb) abort + " TODO: remove from s:callbacks +endfunction + +function! s:on_insert_enter() abort + let s:previous_position = getcurpos() + call s:change_tick_start() +endfunction + +function! s:on_insert_leave() abort + unlet s:previous_position + call s:change_tick_stop() +endfunction + +function! s:on_text_changed_i() abort + call s:check_changes() +endfunction + +function! s:change_tick_start() abort + if !exists('s:change_timer') + let s:last_tick = s:change_tick() + " changes every 30ms, which is 0.03s, it should be fast enough + let s:change_timer = timer_start(30, function('s:check_changes'), { 'repeat': -1 }) + endif +endfunction + +function! s:change_tick_stop() abort + if exists('s:change_timer') + call timer_stop(s:change_timer) + unlet s:change_timer + let s:last_tick = [] + endif +endfunction + +function! s:check_changes(...) abort + let l:tick = s:change_tick() + if l:tick != s:last_tick + let s:last_tick = l:tick + call s:maybe_notify_on_change() + endif +endfunction + +function! s:maybe_notify_on_change() abort + " enter to new line or backspace to previous line shouldn't cause change trigger + let l:previous_position = s:previous_position + let s:previous_position = getcurpos() + if l:previous_position[1] ==# getcurpos()[1] + for l:Cb in s:callbacks + call l:Cb() + endfor + endif +endfunction + +function! s:change_tick() abort + return [b:changedtick, getcurpos()] +endfunction diff --git a/doc/asyncomplete.txt b/doc/asyncomplete.txt new file mode 100644 index 00000000..1e15fbac --- /dev/null +++ b/doc/asyncomplete.txt @@ -0,0 +1,191 @@ +*asyncomplete.vim.txt* Async autocompletion for Vim 8 and Neovim. +*asyncomplete* + + +=============================================================================== +CONTENTS *asyncomplete-contents* + + 1. Introduction |asyncomplete-introduction| + 2. Options |asyncomplete-options| + 3. Functions |asyncomplete-functions| + 4. Global vim configuration |asyncomplete-global-config| + 5. Known Issues |asyncomplete-known-issues| + +=============================================================================== +1. Introduction *asyncomplete-introduction* + +Async autocompletion for Vim 8 and Neovim with |timers|. + +This is inspired by https://github.com/roxma/nvim-complete-manager but written +in pure Vim Script. + +=============================================================================== +2. Options *asyncomplete-options* + + +g:asyncomplete_enable_for_all *g:asyncomplete_enable_for_all* + + Type |Number| + Default: 1 + + Enable asyncomplete for all buffers. Can be overriden with + `b:asyncomplete_enable` on a per-buffer basis. Setting this to 0 prevents + asyncomplete from loading upon entering a buffer. + +b:asyncomplete_enable *b:asyncomplete_enable* + + Type |Number| + Default: 1 + + Setting this variable to 0 disables asyncomplete for the current buffer + and overrides `g:asyncomplete_enable_for_all` . + +g:asyncomplete_auto_popup *g:asyncomplete_auto_popup* + + Type: |Number| + Default: `1` + + Automatically show the autocomplete popup menu as you start typing. + +g:asyncomplete_log_file *g:asyncomplete_log_file* + + Type: |String| + Default: null + + Path to log file. + +g:asyncomplete_popup_delay *g:asyncomplete_popup_delay* + + Type: |Number| + Default: 30 + + Milliseconds to wait before opening the popup menu + +g:asyncomplete_auto_completeopt *g:asyncomplete_auto_completeopt* + + Type: |Number| + Default: 1 + + Set default `completeopt` options. These are `menuone,noinsert,noselect`. + This effectively overwrites what ever the user has in their config file. + + Set to 0 to disable. + +g:asyncomplete_preprocessor *g:asyncomplete_preprocessor* + + Type: |Array| for zero or one |Function| + Default: [] + + Set a function to allow custom filtering or sorting. + Below example implements removing duplicates. + + function! s:my_asyncomplete_preprocessor(options, matches) abort + let l:visited = {} + let l:items = [] + for [l:source_name, l:matches] in items(a:matches) + for l:item in l:matches['items'] + if stridx(l:item['word'], a:options['base']) == 0 + if !has_key(l:visited, l:item['word']) + call add(l:items, l:item) + let l:visited[l:item['word']] = 1 + endif + endif + endfor + endfor + + call asyncomplete#preprocess_complete(a:options, l:items) + endfunction + + let g:asyncomplete_preprocessor = [function('s:my_asyncomplete_preprocessor')] + + Note: + asyncomplete#preprocess_complete() must be called synchronously. + Plans to support async preprocessing will be supported in the future. + + context and matches in arguments in preprecessor function should be treated + as immutable. + +g:asyncomplete_min_chars *g:asyncomplete_min_chars* + + Type: |Number| + Default: 0 + + Minimum consecutive characters to trigger auto-popup. Overridden by buffer + variable if set (`b:asyncomplete_min_chars`) + +g:asyncomplete_matchfuzzy *g:asyncomplete_matchfuzzy* + + Type: |Number| + Default: `exists('*matchfuzzypos')` + + Use |matchfuzzypos| to support fuzzy matching of 'word' when completing + items. Requires vim with `matchfuzzypos()` function to exists. + + Set to `0` to disable fuzzy matching. + +=============================================================================== +3. Functions *asyncomplete-functions* + +asyncomplete#close_popup() *asyncomplete#close_popup()* + + Insert selected candidate and close popup menu. + Following example prevents popup menu from re-opening after insertion. +> + inoremap pumvisible() ? asyncomplete#close_popup() : "\" +< +asyncomplete#cancel_popup() *asyncomplete#cancel_popup()* + + Cancel completion and close popup menu. + Following example prevents popup menu from re-opening after cancellation. +> + inoremap pumvisible() ? asyncomplete#cancel_popup() : "\" +< +asyncomplete#get_source_info({source-name}) *asyncomplete#get_source_info()* + + Get the source configuration info as dict. + Below example implements a priority sort function. +> + function! s:sort_by_priority_preprocessor(options, matches) abort + let l:items = [] + for [l:source_name, l:matches] in items(a:matches) + for l:item in l:matches['items'] + if stridx(l:item['word'], a:options['base']) == 0 + let l:item['priority'] = + \ get(asyncomplete#get_source_info(l:source_name),'priority',0) + call add(l:items, l:item) + endif + endfor + endfor + + let l:items = sort(l:items, {a, b -> b['priority'] - a['priority']}) + + call asyncomplete#preprocess_complete(a:options, l:items) + endfunction + + let g:asyncomplete_preprocessor = [function('s:sort_by_priority_preprocessor')] +< +asyncomplete#get_source_names() *asyncomplete#get_source_names()* + + Get the registered source names list. + +=============================================================================== +4. Global vim configuration *asyncomplete-global-config* + +If you notice messages like 'Pattern not found' or 'Match 1 of ' printed in +red colour in vim command line and in `:messages` history and you are annoyed +with them, try setting `shortmess` vim option in your `.vimrc` like so: +> + set shortmess+=c +< +See `:help shortmess` for details and description. + +=============================================================================== +5. Known Issues *asyncomplete-known-issues* + +Builtin complete such as omni func, file func flickers and closes. + You need vim with patch v8.1.1068. + https://github.com/vim/vim/commit/fd133323d4e1cc9c0e61c0ce357df4d36ea148e3 + +=============================================================================== + + vim:tw=78:ts=4:sts=4:sw=4:ft=help:norl: diff --git a/plugin/asyncomplete.vim b/plugin/asyncomplete.vim new file mode 100644 index 00000000..69bde828 --- /dev/null +++ b/plugin/asyncomplete.vim @@ -0,0 +1,25 @@ +if exists('g:asyncomplete_loaded') + finish +endif +let g:asyncomplete_loaded = 1 + +if get(g:, 'asyncomplete_enable_for_all', 1) + augroup asyncomplete_enable + au! + au BufEnter * if exists('b:asyncomplete_enable') == 0 | call asyncomplete#enable_for_buffer() | endif + augroup END +endif + +let g:asyncomplete_manager = get(g:, 'asyncomplete_manager', 'asyncomplete#managers#vim#init') +let g:asyncomplete_change_manager = get(g:, 'asyncomplete_change_manager', ['asyncomplete#utils#_on_change#textchangedp#init', 'asyncomplete#utils#_on_change#timer#init']) +let g:asyncomplete_triggers = get(g:, 'asyncomplete_triggers', {'*': ['.', '>', ':'] }) +let g:asyncomplete_min_chars = get(g:, 'asyncomplete_min_chars', 0) + +let g:asyncomplete_auto_completeopt = get(g:, 'asyncomplete_auto_completeopt', 1) +let g:asyncomplete_auto_popup = get(g:, 'asyncomplete_auto_popup', 1) +let g:asyncomplete_popup_delay = get(g:, 'asyncomplete_popup_delay', 30) +let g:asyncomplete_log_file = get(g:, 'asyncomplete_log_file', '') +let g:asyncomplete_preprocessor = get(g:, 'asyncomplete_preprocessor', []) +let g:asyncomplete_matchfuzzy = get(g:, 'asyncomplete_matchfuzzy', exists('*matchfuzzypos')) + +inoremap (asyncomplete_force_refresh) asyncomplete#force_refresh() diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..39a2b6e9 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} diff --git a/test/.themisrc b/test/.themisrc new file mode 100644 index 00000000..d73cbf9d --- /dev/null +++ b/test/.themisrc @@ -0,0 +1,3 @@ +set encoding=utf-8 +call themis#option('recursive', 1) +call themis#helper('command').with(themis#helper('assert')) diff --git a/test/asyncomplete.vimspec b/test/asyncomplete.vimspec new file mode 100644 index 00000000..eaf2b54e --- /dev/null +++ b/test/asyncomplete.vimspec @@ -0,0 +1,2 @@ +Describe asyncomplete +End -- 2.39.5