Repository: Kaiser-Yang/blink-cmp-git
Pull request #57: Better async
Author: login: TheLeoP
Created: 2025-05-01T17:40:56Z | Merged: 2025-05-04T06:21:40Z

Problem:

The plugin isn't truly async everywhere. There are calls for `sync` on a plenary job that call `vim.wait` under the hood, which blocks the editor until the jobs has finished it's execution. This may not be a problem in unix-like systems, but it is in Windows.

This, plus the fact that the plugin delays the execution of this jobs until `InsertEnter` results in Neovim being blocked for a couple of seconds on `InsertEnter` the first time after opening Neovim.

Solution:

Ditch the plenary.nvim dependency altogether and use the builtin `vim.system` and lua coroutines to have async code everywhere.

As a consequence of this change I also ended up removing the `async` config option and defaulting to using async always (since there is no cost to doing so).

I would also prefer to eagerly start the cache related jobs earlier, but didn't change the code to do so because that's not my decision to make. Since they are async, they won't block the user in any case. This will allow the plugin to always offer completions results on insert mode, even when defaulting to async always.


Diff:
diff --git a/README.md b/README.md
index 8781ffd..3ceac93 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,6 @@ Add the plugin to your packer managers, and make sure it is loaded before `blink
     dependencies = {
         {
             'Kaiser-Yang/blink-cmp-git',
-            dependencies = { 'nvim-lua/plenary.nvim' }
         }
         -- ... other dependencies
     },
@@ -140,10 +139,10 @@ pre-cache when you enter insert mode or other mode you can input
 > For `github` users, if you customize the `get_token`, you should see those below to know
 > which permissions are required:
 >
-> * [commit (for `octo.nvim` users)](https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#list-commits)
-> * [issue](https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues)
-> * [pull-request](https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests)
-> * [mention](https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repository-contributors)
+> - [commit (for `octo.nvim` users)](https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#list-commits)
+> - [issue](https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues)
+> - [pull-request](https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests)
+> - [mention](https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repository-contributors)
 >
 > For `gitlab` users, see [PAT](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html)
 > to know how to get the token.
@@ -155,11 +154,11 @@ pre-cache when you enter insert mode or other mode you can input
 
 ## Reload Cache
 
-There are many cases will make the cache out of date. For example,
-if your `cwd` is in a repository, later you switch your `cwd` to another repository, the cache
-will use the first repository's result. To solve this problem, there is a command to
-reload the cache: `BlinkCmpGitReloadCache`. This command will clear all the cache and if
-`use_items_pre_cache` is enabled (default to `true`), it will pre-cache again.
+There are many cases that will make the cache invalid. For example, if your
+`cwd` is in a repository, later you switch your `cwd` to another repository,
+the cache will use the first repository's result. To solve this problem, there
+is a command to reload the cache: `BlinkCmpGitReloadCache`. This command will
+clear the cache.
 
 `blink-cmp-git` will create a auto command which uses `should_reload_cache` to determine
 whether or not to reload cache when entering insert mode.
@@ -182,7 +181,7 @@ See [default.lua](./lua/blink-cmp-git/default/init.lua).
 Because all features have same fields, I'll use `commit` as an example.
 
 The `blink-cmp-git` will first run command from `get_command` and `get_command_args`. The standout
-of the command will be passed to `separate_output`. So if you want to customize the completion 
+of the command will be passed to `separate_output`. So if you want to customize the completion
 items, you should be aware of what the output of your command looks like.
 
 In most situations, you just need to customize
@@ -260,7 +259,7 @@ git_centers = {
 >
 > `kind_name` is used by those default options:
 >
-> * `kind_icons`
+> - `kind_icons`
 >
 > Therefore, if you customize the `kind_name`, you should customize them too.
 
@@ -306,13 +305,12 @@ Suppose the item in your completion list is like this below:
 ```
 
 | Highlight Group Name              | Description             |
-|-----------------------------------|-------------------------|
+| --------------------------------- | ----------------------- |
 | `BlinkCmpGitKind<kind_name>`      | For ``                 |
 | `BlinkCmpGitKindIcon<kind_name>`  | For `PR`                |
 | `BlinkCmpGitLabel<kind_name>Id`   | For `#1`                |
 | `BlinkCmpGitLabel<kind_name>Rest` | For `Add a new feature` |
 
-
 > [!NOTE]
 > The `Id` part is got by seperating the label by whitespaces, if you customize the `get_label`,
 > you may need to customize the `label_highlight` too.
@@ -515,7 +513,7 @@ request domain.
 By default, `blink-cmp-git` will request those endpoints below:
 
 | Feature                            | API Endpoint                         |
-|------------------------------------|--------------------------------------|
+| ---------------------------------- | ------------------------------------ |
 | `github.issue`                     | `repos/OWNER/REPO/issues`            |
 | `github.pull_request`              | `repos/OWNER/REPO/pulls`             |
 | `github.mention`                   | `repos/OWNER/REPO/contributors`      |
@@ -589,23 +587,23 @@ end
 
 ## Performance
 
-Once `async` is enabled, the completion will has no effect to your other operations.
-How long it will take to show results depends on the network speed and the response time
-of the git center. But, don't worries, once you enable `use_items_cache`, the items will be
-cached when you first trigger the completion by inputting `@`, `#`, or `:`
-(You can DIY the triggers). Furthermore, once you enable `use_items_pre_cache`, when the
-source is created, it will pre-cache all the items. For the documentation of `mention` feature,
-it will be cached when you hover on one item.
+`blink-cmp-git` is async. This means that completion won't block other
+operations. How long it will take to show results depends on the network speed
+and the response time of the git center. But, don't worry, the items will be
+cached when you first trigger the completion by inputting `@`, `#`, or `:` (You
+can DIY the triggers). Furthermore, when the source is created, it will
+pre-cache all the items. For the documentation of the `mention` feature, it
+will be cached when you hover on one item.
 
 ## Version Introduction
 
 The release versions are something like `major.minor.patch`. When one of these numbers is increased:
 
-* `patch`: bugs are fixed or docs are added. This will not break the compatibility.
-* `minor`: compatible features are added. This may cause some configurations `deprecated`, but
-not break the compatibility.
-* `major`: incompatible features are added. All the `deprecated` configurations will be removed.
-This will break the compatibility.
+- `patch`: bugs are fixed or docs are added. This will not break the compatibility.
+- `minor`: compatible features are added. This may cause some configurations `deprecated`, but
+  not break the compatibility.
+- `major`: incompatible features are added. All the `deprecated` configurations will be removed.
+  This will break the compatibility.
 
 ## Acknowledgment
 
diff --git a/lua/blink-cmp-git/default/github.lua b/lua/blink-cmp-git/default/github.lua
index 7592ee7..0e3be36 100644
--- a/lua/blink-cmp-git/default/github.lua
+++ b/lua/blink-cmp-git/default/github.lua
@@ -1,6 +1,7 @@
 local utils = require('blink-cmp-git.utils')
 local common = require('blink-cmp-git.default.common')
 
+--- @async
 local function default_github_enable()
     if
         not utils.command_found('git')
diff --git a/lua/blink-cmp-git/default/init.lua b/lua/blink-cmp-git/default/init.lua
index d497b07..bb17ba3 100644
--- a/lua/blink-cmp-git/default/init.lua
+++ b/lua/blink-cmp-git/default/init.lua
@@ -2,8 +2,13 @@ local utils = require('blink-cmp-git.utils')
 local log = require('blink-cmp-git.log')
 log.setup({ title = 'blink-cmp-git' })
 
-local last_git_repo
+local last_git_repo ---@type string?
+
+--- @async
 local function default_should_reload_cache()
+    local co = coroutine.running()
+    assert(co, 'This function should run inside a coroutine')
+
     -- Do not reload cache when the buffer is a prompt buffer
     -- or the source provider is disabled
     if vim.bo.buftype == 'prompt' or not utils.source_provider_enabled() then return false end
@@ -66,10 +71,6 @@ end
 
 --- @type blink-cmp-git.Options
 return {
-    async = true,
-    use_items_cache = true,
-    -- Whether or not cache the triggers when the source is loaded
-    use_items_pre_cache = true,
     should_reload_cache = default_should_reload_cache,
     before_reload_cache = default_before_reload_cache,
     kind_icons = {
diff --git a/lua/blink-cmp-git/init.lua b/lua/blink-cmp-git/init.lua
index 27f7de0..3795ea3 100644
--- a/lua/blink-cmp-git/init.lua
+++ b/lua/blink-cmp-git/init.lua
@@ -3,7 +3,6 @@
 
 local default = require('blink-cmp-git.default')
 local utils = require('blink-cmp-git.utils')
-local Job = require('plenary.job')
 local log = require('blink-cmp-git.log')
 local blink_cmp_git_reload_cache_command = 'BlinkCmpGitReloadCache'
 local blink_cmp_git_autocmd_group = 'blink-cmp-git-autocmd'
@@ -12,25 +11,12 @@ log.setup({ title = 'blink-cmp-git' })
 --- @class blink.cmp.Source
 --- @field git_source_config blink-cmp-git.Options
 --- @field cache blink-cmp-git.Cache
---- @field pre_cache_jobs table<string, {items: table, jobs: Job[]}>
+--- @field running_pre_cache_jobs vim.SystemObj[]
+--- @field _after_cache? fun()
 local GitSource = {}
 
---- @param documentation_command blink-cmp-git.DocumentationCommand
---- @return Job
-local function create_job_from_documentation_command(documentation_command)
-    local command = utils.get_option(documentation_command.get_command)
-    local token = utils.get_option(documentation_command.get_token)
-    ---@diagnostic disable-next-line: missing-fields
-    return Job:new({
-        command = command,
-        args = utils.get_option(documentation_command.get_command_args, command, token),
-        cwd = utils.get_cwd(),
-        env = utils.get_job_default_env(),
-    })
-end
-
 --- @param context blink.cmp.Context
---- @return lsp.TextEdit
+--- @return lsp.Range
 local function get_text_edit_range(context)
     return {
         -- This range make it possible to remove the trigger character
@@ -46,131 +32,113 @@ local function get_text_edit_range(context)
 end
 
 --- @param feature blink-cmp-git.GCSCompletionOptions
---- @param result string[]
+--- @param result string
 --- @return blink-cmp-git.CompletionItem[]
 local function assemble_completion_items_from_output(feature, result)
-    local items = {}
-    for i, v in ipairs(feature.separate_output(table.concat(result, '\n'))) do
-        items[i] = {
+    local items = {} ---@type blink-cmp-git.CompletionItem[]
+    for _, v in pairs(feature.separate_output(result)) do
+        table.insert(items, {
             label = feature.get_label(v),
             kind_name = feature.get_kind_name(v),
             insert_text = feature.get_insert_text(v),
             documentation = feature.get_documentation(v),
-        }
+        })
     end
     feature.configure_score_offset(items)
     return items
 end
 
 --- @param feature blink-cmp-git.GCSCompletionOptions
---- @param items table
---- @return Job
-local function create_job_from_feature(feature, items)
-    local command = utils.get_option(feature.get_command)
-    local token = utils.get_option(feature.get_token)
-    ---@diagnostic disable-next-line: missing-fields
-    return Job:new({
-        command = command,
-        args = utils.get_option(feature.get_command_args, command, token),
-        cwd = utils.get_cwd(),
-        env = utils.get_job_default_env(),
-        on_exit = function(j, return_value, signal)
-            if signal == 9 then return end
-            if return_value ~= 0 or utils.truthy(j:stderr_result()) then
-                if feature.on_error(return_value, table.concat(j:stderr_result(), '\n')) then
-                    return
-                end
-            end
-            if utils.truthy(j:result()) then
-                local match_list = assemble_completion_items_from_output(feature, j:result())
-                vim.iter(match_list):each(
-                    function(match)
-                        items[match] = {
-                            label = match.label,
-                            kind = require('blink.cmp.types').CompletionItemKind[match.kind_name]
-                                or 0,
-                            textEdit = {
-                                newText = match.insert_text
-                                    .. (utils.get_option(feature.insert_text_trailing) or ''),
-                            },
-                            score_offset = match.score_offset,
-                            documentation = match.documentation,
-                        }
-                    end
-                )
-            else
-                log.trace('search command return empty result')
-            end
-        end,
-    })
-end
+--- @return blink.cmp.CompletionItem[]
+--- @async
+local function items_for_feature(feature, running_jobs)
+    local co = coroutine.running()
+    assert(co, 'This function should run inside a coroutine')
 
---- Clear the content in items but do not delete the table so that the table can be reused
---- @param items table
-local function clear_items(items)
-    for key, _ in pairs(items) do
-        items[key] = nil
+    local command = utils.get_option(feature.get_command) ---@type string
+    local token = utils.get_option(feature.get_token) ---@type string
+    local args = utils.get_option(feature.get_command_args, command, token) ---@type string[]
+
+    local job = vim.system(
+        vim.list_extend({ command }, args),
+        {
+            text = true,
+            cwd = utils.get_cwd(),
+            env = utils.get_job_default_env(),
+        },
+        vim.schedule_wrap(function(out)
+            local ok, err = coroutine.resume(co, out)
+            if not ok then vim.notify(debug.traceback(co, err), vim.log.levels.ERROR) end
+        end)
+    )
+    table.insert(running_jobs, job)
+    local out = coroutine.yield() --[[@as vim.SystemCompleted]]
+
+    local signal = out.signal
+    local return_value = out.code
+    if signal == 9 then return {} end
+    if return_value ~= 0 or utils.truthy(out.stderr) then
+        if feature.on_error(return_value, out.stderr) then return {} end
+    end
+    if out.stdout == '' then
+        log.trace('search command return empty result')
+        return {}
     end
+
+    local match_list = assemble_completion_items_from_output(feature, out.stdout)
+    return vim.iter(match_list)
+        :map(
+            function(match)
+                return {
+                    label = match.label,
+                    kind = require('blink.cmp.types').CompletionItemKind[match.kind_name] or 0,
+                    textEdit = {
+                        newText = match.insert_text
+                            .. (utils.get_option(feature.insert_text_trailing) or ''),
+                    },
+                    score_offset = match.score_offset,
+                    documentation = match.documentation,
+                }
+            end
+        )
+        :totable()
 end
 
+--- @async
 function GitSource:create_pre_cache_jobs()
-    local async = utils.get_option(self.git_source_config.async)
-    self.pre_cache_jobs = {}
+    local co = coroutine.running()
+    assert(co, 'This function should run inside a coroutine')
+
+    local all_items = {} ---@type table<string, lsp.CompletionItem[]>
+    local n_coroutines = 0
+
     for _, feature in pairs(self:get_enabled_features()) do
-        for _, trigger in pairs(utils.get_option(feature.triggers)) do
-            if not self.pre_cache_jobs[trigger] then
-                self.pre_cache_jobs[trigger] = {
-                    items = {},
-                    jobs = {},
-                }
-            end
-            local jobs_and_items = self.pre_cache_jobs[trigger]
-            local next_job = create_job_from_feature(feature, jobs_and_items.items)
-            if utils.truthy(jobs_and_items.jobs) then
-                jobs_and_items.jobs[#jobs_and_items.jobs]:after(function(_, code, signal)
-                    -- shutdown
-                    if signal == 9 then return end
-                    if async then
-                        next_job:start()
-                    else
-                        -- this must in a schedule
-                        vim.schedule(function() next_job:sync() end)
-                    end
-                end)
-            end
-            table.insert(jobs_and_items.jobs, next_job)
+        local triggers = utils.get_option(feature.triggers) ---@type string[]
+        for _, trigger in pairs(triggers) do
+            if not all_items[trigger] then all_items[trigger] = {} end
+            local items = all_items[trigger]
+
+            coroutine.wrap(function()
+                n_coroutines = n_coroutines + 1
+                local new_items = items_for_feature(feature, self.running_pre_cache_jobs)
+                vim.list_extend(items, new_items)
+
+                local ok, err = coroutine.resume(co)
+                if not ok then vim.notify(debug.traceback(co, err), vim.log.levels.ERROR) end
+            end)()
         end
     end
-    for trigger, job_and_items in pairs(self.pre_cache_jobs) do
-        -- let the last job set the cache
-        job_and_items.jobs[#job_and_items.jobs]:after(function(_, _, signal)
-            if signal == 9 then return end
-            for key, item in pairs(job_and_items.items) do
-                self.cache:set({ trigger, item.label }, item)
-            end
-            clear_items(job_and_items.items)
-        end)
+    for _ = 1, n_coroutines do
+        coroutine.yield()
     end
-end
 
-function GitSource:shutdown_pre_cache_jobs()
-    for _, job_and_items in pairs(self.pre_cache_jobs) do
-        for _, job in pairs(job_and_items.jobs) do
-            job:shutdown(0, 9)
+    for trigger, items in pairs(all_items) do
+        for _, item in ipairs(items) do
+            self.cache:set({ trigger, item.label }, item)
         end
-        clear_items(job_and_items.items)
     end
-end
 
-function GitSource:run_pre_cache_jobs()
-    local async = utils.get_option(self.git_source_config.async)
-    for _, job_and_items in pairs(self.pre_cache_jobs) do
-        if async then
-            job_and_items.jobs[1]:start()
-        else
-            job_and_items.jobs[1]:sync()
-        end
-    end
+    if self._after_cache then self._after_cache() end
 end
 
 --- @type blink-cmp-git.Options
@@ -227,6 +195,7 @@ end
 function GitSource.new(opts, config)
     local self = setmetatable({}, { __index = GitSource })
     self.git_source_config = vim.tbl_deep_extend('force', default, opts or {})
+    self.running_pre_cache_jobs = {}
     latest_git_source_config = self.git_source_config
     latest_source_provider_config = config
 
@@ -275,40 +244,42 @@ function GitSource.new(opts, config)
         end
     end
 
-    -- cache and pre-cache jobs
     self.cache = require('blink-cmp-git.cache').new()
-    self.pre_cache_jobs = {}
-    local use_items_cache = utils.get_option(self.git_source_config.use_items_cache)
-    local use_items_pre_cache = utils.get_option(self.git_source_config.use_items_pre_cache)
-    if use_items_cache and use_items_pre_cache then
-        self:create_pre_cache_jobs()
-        self:run_pre_cache_jobs()
-    end
+    coroutine.wrap(function() self:create_pre_cache_jobs() end)()
 
     -- reload cache command and autocmd
     vim.api.nvim_create_user_command(blink_cmp_git_reload_cache_command, function()
         self.git_source_config.before_reload_cache()
-        if utils.get_option(self.git_source_config.use_items_cache) then
-            self.cache:clear()
-            if utils.get_option(self.git_source_config.use_items_pre_cache) then
-                self:shutdown_pre_cache_jobs()
-                self:create_pre_cache_jobs()
-                self:run_pre_cache_jobs()
-            end
-        end
+
+        self.cache:clear()
+
+        vim.iter(self.running_pre_cache_jobs):each(function(job) job:kill('TERM') end)
+        self.running_pre_cache_jobs = {}
+        self._after_cache = nil
+        coroutine.wrap(function() self:create_pre_cache_jobs() end)()
     end, { nargs = 0 })
     vim.api.nvim_create_augroup(blink_cmp_git_autocmd_group, { clear = true })
     vim.api.nvim_create_autocmd({ 'InsertEnter' }, {
         group = blink_cmp_git_autocmd_group,
         callback = function()
-            if self.git_source_config.should_reload_cache() then
+            coroutine.wrap(function()
+                if not self.git_source_config.should_reload_cache() then return end
                 vim.cmd(blink_cmp_git_reload_cache_command)
-            end
+            end)()
         end,
     })
-    -- call the function so the default last_git_repo is set
-    -- defer the call to avoid getting empty last_git_repo even in a git repo
-    vim.schedule_wrap(self.git_source_config.should_reload_cache)()
+
+    -- call `should_reload_cache` so the default `last_git_repo` is set
+    coroutine.wrap(function()
+        local co = coroutine.running()
+        -- defer the call to avoid getting empty `last_git_repo` even in a git repo
+        vim.schedule(function()
+            local ok, err = coroutine.resume(co)
+            if not ok then vim.notify(debug.traceback(co, err), vim.log.levels.ERROR) end
+        end)
+        coroutine.yield()
+        self.git_source_config.should_reload_cache()
+    end)()
 
     return self
 end
@@ -331,177 +302,55 @@ function GitSource:get_trigger_characters()
     return result
 end
 
-function GitSource:handle_items_pre_cache(context, callback)
-    if
-        not utils.get_option(self.git_source_config.use_items_pre_cache)
-        or not utils.get_option(self.git_source_config.use_items_cache)
-    then
-        return nil
-    end
-    local items = {}
-    local trigger = context.trigger.initial_character
-    local transformed_callback = function()
-        for _, item in pairs(items) do
-            item.textEdit.range = get_text_edit_range(context)
-            item.gitSourceTrigger = trigger
-        end
-        callback({
-            is_incomplete_backward = false,
-            is_incomplete_forward = false,
-            items = vim.tbl_values(items),
-        })
-    end
-    local job_and_items = self.pre_cache_jobs[trigger]
-    -- no enabled features
-    if not job_and_items then
-        return function() end
-    end
-    local cancel_fun = function()
-        for _, job in pairs(job_and_items.jobs) do
-            job:shutdown(0, 9)
-        end
-    end
-    local async = utils.get_option(self.git_source_config.async)
-    -- This happens when the async is true in new, but changed to false in the future
-    if not async then
-        -- Wait for jobs to finish
-        local timeout = false
-        for _, job in pairs(job_and_items.jobs) do
-            job:wait()
-            if not job.is_shutdown then
-                timeout = true
-                break
-            end
+---@param context blink.cmp.Context
+---@param callback function
+function GitSource:handle_items_pre_cache(context, callback) end
+
+function GitSource:get_completions(context, callback)
+    coroutine.wrap(function()
+        local co = coroutine.running()
+
+        local trigger = context.trigger.initial_character ---@type string
+        if
+            ---@diagnostic disable-next-line: missing-parameter
+            not self:should_show_items(context)
+            or not context.line
+                :sub(1, context.cursor[2])
+                :match(context.trigger.initial_character .. '$')
+        then
+            callback()
+            return
         end
-        -- any job timeouts in 5 seconds, cancel jobs
-        if timeout then
-            for _, job in pairs(job_and_items.jobs) do
-                job:shutdown(0, 9)
+
+        local items = self.cache:get(trigger) ---@type table<string, lsp.CompletionItem>
+        if not items then
+            self._after_cache = function()
+                self._after_cache = nil
+                local ok, err = coroutine.resume(co)
+                if not ok then vim.notify(debug.traceback(co, err), vim.log.levels.ERROR) end
             end
-            clear_items(job_and_items.items)
-            -- completion already canceled, return a function that does nothing
-            return function() end
-        end
-    elseif not job_and_items.jobs[#job_and_items.jobs].is_shutdown then
-        -- Re-run jobs to make sure it is asynchronous
-        for _, job in pairs(job_and_items.jobs) do
-            job:shutdown(0, 9)
+            coroutine.yield()
+            items = self.cache:get(trigger) ---@type table<string, lsp.CompletionItem>
         end
-        clear_items(job_and_items.items)
-        job_and_items.jobs[#job_and_items.jobs]:after(function(j, _, signal)
-            -- HACK: there may be a elegant way to do this
-            -- Remove this callback to avoid running it again
-            -- Make sure this callback is the last one
-            table.remove(j._additional_on_exit_callbacks)
-            if signal == 9 then
-                vim.schedule(function() callback() end)
-                return
-            end
-            items = self.cache:get(trigger) or {}
-            vim.schedule(function() transformed_callback() end)
-        end)
-        job_and_items.jobs[1]:start()
-        return cancel_fun
-    end
-    items = self.cache:get(trigger) or {}
-    transformed_callback()
-    -- completion finished, return a function that does nothing
-    return function() end
-end
 
---- @param context blink.cmp.Context
---- @param callback fun(response?: blink.cmp.CompletionResponse)
-function GitSource:get_completions(context, callback)
-    local items = {}
-    local trigger = context.trigger.initial_character
-    local transformed_callback = function()
-        for _, item in pairs(items) do
-            item.textEdit.range = get_text_edit_range(context)
-            item.gitSourceTrigger = trigger
-        end
+        items = vim.iter(items)
+            :map(function(key, item)
+                item.textEdit.range = get_text_edit_range(context)
+                item.gitSourceTrigger = trigger
+                return key, item
+            end)
+            :fold({}, function(acc, key, item)
+                acc[key] = item
+                return acc
+            end)
         callback({
             is_incomplete_backward = false,
             is_incomplete_forward = false,
             items = vim.tbl_values(items),
         })
-    end
-    ---@diagnostic disable-next-line: missing-parameter
-    if
-        not self:should_show_items(context)
-        or not context.line
-            :sub(1, context.cursor[2])
-            :match(context.trigger.initial_character .. '$')
-    then
-        transformed_callback()
-        return function() end
-    end
-    local cancel_fun = self:handle_items_pre_cache(context, callback)
-    if cancel_fun then return cancel_fun end
-    cancel_fun = function() end
-    local async = utils.get_option(self.git_source_config.async)
-    local use_items_cache = utils.get_option(self.git_source_config.use_items_cache)
-    local cached_items
-    if use_items_cache then
-        ---@diagnostic disable-next-line: param-type-mismatch
-        cached_items = self.cache:get(trigger)
-    end
-    if cached_items then
-        items = cached_items
-        transformed_callback()
-        return cancel_fun
-    end
-    --- @type Job[]
-    local jobs = {}
-    local trigger_pos = context.cursor[2] - 1
-    local enabled_features = self:get_enabled_features()
-    for _, feature in pairs(enabled_features) do
-        local feature_triggers = utils.get_option(feature.triggers)
-        if utils.truthy(feature_triggers) and vim.tbl_contains(feature_triggers, trigger) then
-            local next_job = create_job_from_feature(feature, items)
-            if utils.truthy(jobs) then
-                jobs[#jobs]:after(function(_, code, signal)
-                    -- shutdown
-                    if signal == 9 then return end
-                    if async then
-                        next_job:start()
-                    else
-                        -- this must in a schedule
-                        vim.schedule(function() next_job:sync() end)
-                    end
-                end)
-            end
-            table.insert(jobs, next_job)
-        end
-    end
-    -- the feature for the trigger may not be enabled
-    if not utils.truthy(jobs) then
-        transformed_callback()
-        return cancel_fun
-    end
-    -- let the last job call the transformed_callback
-    jobs[#jobs]:after(function(_, _, signal)
-        if signal == 9 then
-            vim.schedule(function() callback() end)
-            return
-        end
-        if use_items_cache then
-            for key, item in pairs(items) do
-                self.cache:set({ trigger, item.label }, item)
-            end
-        end
-        vim.schedule(function() transformed_callback() end)
-    end)
-    if async then
-        cancel_fun = function()
-            for _, job in pairs(jobs) do
-                job:shutdown(0, 9)
-            end
-        end
-        jobs[1]:start()
-    else
-        jobs[1]:sync()
-    end
-    return cancel_fun
+    end)()
+
+    return function() end
 end
 
 --- @param context blink.cmp.Context
@@ -511,71 +360,79 @@ function GitSource:should_show_items(context, _)
         and vim.tbl_contains(self:get_trigger_characters(), context.trigger.initial_character)
 end
 
---- @param item blink.cmp.CompletionItem
---- @param callback fun(item: blink.cmp.CompletionItem)
 function GitSource:resolve(item, callback)
-    ---@diagnostic disable-next-line: undefined-field
-    local trigger = item.gitSourceTrigger
-    local transformed_callback = function() callback(item) end
-    local use_items_cache = utils.get_option(self.git_source_config.use_items_cache)
-    local cached_item_documentation
-    if use_items_cache then
-        cached_item_documentation = self.cache:get({ trigger, item.label, 'documentation' })
-    end
-    if cached_item_documentation then
-        item.documentation = cached_item_documentation
-        if item.documentation == '' then item.documentation = nil end
-        transformed_callback()
-        return
-    end
-    if type(item.documentation) == 'string' or not item.documentation then
-        ---@diagnostic disable-next-line: param-type-mismatch
-        if item.documentation and item.documentation:match('^%s*$') then item.documentation = '' end
-        if use_items_cache then
+    local job ---@type vim.SystemObj | nil
+    coroutine.wrap(function()
+        local documentation = item.documentation
+        ---@cast documentation +blink-cmp-git.DocumentationCommand
+
+        ---@diagnostic disable-next-line: undefined-field
+        local trigger = item.gitSourceTrigger ---@type string
+        local cached_item_documentation = self.cache:get({ trigger, item.label, 'documentation' })
+        if cached_item_documentation then
+            documentation = cached_item_documentation
+            if documentation == '' then documentation = nil end
+            callback(item)
+            return
+        end
+        if type(documentation) == 'string' or not documentation then
+            if documentation and documentation:match('^%s*$') then documentation = '' end
             -- cache empty string
-            self.cache:set({ trigger, item.label, 'documentation' }, item.documentation or '')
+            self.cache:set({ trigger, item.label, 'documentation' }, documentation or '')
+            if documentation == '' then documentation = nil end
+            callback(item)
+            return
         end
-        if item.documentation == '' then item.documentation = nil end
-        transformed_callback()
-        return
-    end
-    ---@diagnostic disable-next-line: param-type-mismatch
-    local job = create_job_from_documentation_command(item.documentation)
-    job:after(function(j, return_value, signal)
-        if return_value ~= 0 or utils.truthy(j:stderr_result()) then
-            ---@diagnostic disable-next-line: undefined-field
-            if item.documentation.on_error(return_value, table.concat(j:stderr_result(), '\n')) then
-                item.documentation = nil
+
+        local co = coroutine.running()
+
+        local command = utils.get_option(documentation.get_command) ---@type string
+        local token = utils.get_option(documentation.get_token) ---@type string
+        local args = utils.get_option(documentation.get_command_args, command, token) ---@type string[]
+
+        job = vim.system(
+            vim.list_extend({ command }, args),
+            {
+                text = true,
+                cwd = utils.get_cwd(),
+                env = utils.get_job_default_env(),
+            },
+            vim.schedule_wrap(function(out)
+                local ok, err = coroutine.resume(co, out)
+                if not ok then vim.notify(debug.traceback(co, err), vim.log.levels.ERROR) end
+            end)
+        )
+        local out = coroutine.yield() --[[@as vim.SystemCompleted]]
+
+        local return_value = out.code
+        if return_value ~= 0 or utils.truthy(out.stderr) then
+            if documentation.on_error(return_value, out.stderr) then
+                documentation = nil
                 goto cache_handler
             end
         end
-        if utils.truthy(job:result()) then
-            item.documentation =
-                ---@diagnostic disable-next-line: undefined-field
-                item.documentation.resolve_documentation(table.concat(job:result(), '\n'))
-            ---@diagnostic disable-next-line: param-type-mismatch
-            if item.documentation and item.documentation:match('^%s*$') then
-                item.documentation = ''
-            end
+        if utils.truthy(out.stdout) then
+            documentation = documentation.resolve_documentation(out.stdout)
+            if documentation and documentation:match('^%s*$') then documentation = '' end
         else
-            item.documentation = nil
+            documentation = nil
         end
         ::cache_handler::
-        if use_items_cache then
-            -- cache empty string
-            self.cache:set({ trigger, item.label, 'documentation' }, item.documentation or '')
-        end
-        if item.documentation == '' then item.documentation = nil end
-        vim.schedule(function() transformed_callback() end)
-    end)
-    if utils.get_option(self.git_source_config.async) then
-        job:start()
-    else
-        job:sync()
+        -- cache empty string
+        self.cache:set({ trigger, item.label, 'documentation' }, documentation or '')
+        if documentation == '' then documentation = nil end
+        callback(item)
+    end)()
+
+    return function()
+        if not job then return end
+        job:kill('TERM')
+        job = nil
     end
 end
 
 --- @return blink-cmp-git.GCSCompletionOptions[]
+--- @async
 function GitSource:get_enabled_features()
     local result = {}
     for _, git_center in pairs(vim.tbl_values(self.git_source_config.git_centers)) do
diff --git a/lua/blink-cmp-git/types.lua b/lua/blink-cmp-git/types.lua
index 53eac88..4b48491 100644
--- a/lua/blink-cmp-git/types.lua
+++ b/lua/blink-cmp-git/types.lua
@@ -19,7 +19,7 @@
 --- @field get_command? string|fun(): string
 --- @field get_command_args? string[]|fun(command:string, token: string): string[]
 --- @field insert_text_trailing? string|fun(): string
---- @field separate_output? fun(output: string): any[]
+--- @field separate_output? fun(output: string): [integer, any]
 --- @field get_label? fun(item: any): string
 --- @field get_kind_name? fun(item: any): string
 --- @field get_insert_text? fun(item: any): string
@@ -34,12 +34,7 @@
 
 --- @alias blink-cmp-git.GCSGitCenterKeys 'github'|'gitlab'|string
 
--- TODO:
--- remove async and use_items_cache
 --- @class (exact) blink-cmp-git.Options
---- @field async? boolean|fun(): boolean
---- @field use_items_cache? boolean|fun(): boolean
---- @field use_items_pre_cache? boolean|fun(): boolean
 --- @field should_reload_cache? fun(): boolean
 --- @field before_reload_cache? fun()
 --- @field kind_icons? table<string, string>
diff --git a/lua/blink-cmp-git/utils.lua b/lua/blink-cmp-git/utils.lua
index 2b47773..e94325e 100644
--- a/lua/blink-cmp-git/utils.lua
+++ b/lua/blink-cmp-git/utils.lua
@@ -1,18 +1,26 @@
 local M = {}
-local Job = require('plenary.job')
 
+--- @async
 function M.is_inside_git_repo()
+    local co = coroutine.running()
+    assert(co, 'This function should run inside a coroutine')
+
     if not M.command_found('git') then return false end
-    local res = false
-    ---@diagnostic disable-next-line: missing-fields
-    Job:new({
-        command = 'git',
-        args = { 'rev-parse', '--is-inside-work-tree' },
-        cwd = M.get_cwd(),
-        env = M.get_job_default_env(),
-        on_exit = function(_, return_value, _) res = return_value == 0 end,
-    }):sync()
-    return res
+    vim.system(
+        { 'git', 'rev-parse', '--is-inside-work-tree' },
+        {
+            text = true,
+            cwd = M.get_cwd(),
+            env = M.get_job_default_env(),
+        },
+        vim.schedule_wrap(function(out)
+            local ok, err = coroutine.resume(co, out)
+            if not ok then vim.notify(debug.traceback(co, err), vim.log.levels.ERROR) end
+        end)
+    )
+    local out = coroutine.yield() --[[@as vim.SystemCompleted]]
+
+    return out.code == 0
 end
 
 function M.get_option(opt, ...)
@@ -112,20 +120,30 @@ function M.get_remote_name()
     return require('blink-cmp-git').get_latest_git_source_config().get_remote_name()
 end
 
+--- @param remote_name? string
+--- @async
 function M.get_repo_remote_url(remote_name)
+    local co = coroutine.running()
+    assert(co, 'This function should run inside a coroutine')
+
     remote_name = remote_name or M.get_remote_name()
     if not M.command_found('git') then return '' end
-    local output = ''
-    ---@diagnostic disable-next-line: missing-fields
-    Job:new({
-        command = 'git',
-        args = { 'remote', 'get-url', remote_name },
-        cwd = M.get_cwd(),
-        on_exit = function(job, return_value, _)
-            if return_value ~= 0 then return end
-            output = table.concat(job:result(), '\n')
-        end,
-    }):sync()
+
+    vim.system(
+        { 'git', 'remote', 'get-url', remote_name },
+        {
+            text = true,
+            cwd = M.get_cwd(),
+        },
+        vim.schedule_wrap(function(out)
+            local ok, err = coroutine.resume(co, out)
+            if not ok then vim.notify(debug.traceback(co, err), vim.log.levels.ERROR) end
+        end)
+    )
+    local out = coroutine.yield() --[[@as vim.SystemCompleted]]
+
+    local output = out.code == 0 and out.stdout or ''
+    output = output:gsub('\n', '')
     return output
 end
 
@@ -138,23 +156,28 @@ end
 --- Get the absolute path of current git repo
 --- Return nil if not in a git repo
 --- @return string?
+--- @async
 function M.get_git_repo_absolute_path()
+    local co = coroutine.running()
+    assert(co, 'This function should run inside a coroutine')
+
     if vim.bo.filetype == 'octo' then return vim.fn.expand('%:p:h'):match('^octo://[^/]+/[^/]+') end
     if not M.command_found('git') then return nil end
-    local result
-    ---@diagnostic disable-next-line: missing-fields
-    Job:new({
-        command = 'git',
-        args = { 'rev-parse', '--show-toplevel' },
-        cwd = M.get_cwd(),
-        env = M.get_job_default_env(),
-        on_exit = function(j, return_value, _)
-            if return_value == 0 then
-                result = table.concat(j:result(), '\n')
-                if not M.truthy(result) then result = nil end
-            end
-        end,
-    }):sync()
+
+    vim.system(
+        { 'git', 'rev-parse', '--show-toplevel' },
+        {
+            text = true,
+            cwd = M.get_cwd(),
+            env = M.get_job_default_env(),
+        },
+        vim.schedule_wrap(function(out)
+            local ok, err = coroutine.resume(co, out)
+            if not ok then vim.notify(debug.traceback(co, err), vim.log.levels.ERROR) end
+        end)
+    )
+    local out = coroutine.yield() --[[@as vim.SystemCompleted]]
+    local result = out.stdout ~= '' and out.stdout or nil
     return result
 end
 


Reviews:
user: login: Kaiser-Yang
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-02T07:29:41Z
user: login: TheLeoP
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T12:47:37Z
user: login: TheLeoP
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T12:54:27Z
user: login: TheLeoP
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T13:21:42Z
user: login: TheLeoP
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:46:28Z
user: login: Kaiser-Yang
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-02T16:08:54Z
user: login: Kaiser-Yang
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-02T16:17:33Z
user: login: Kaiser-Yang
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-02T16:27:58Z
user: login: TheLeoP
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T17:30:41Z
user: login: Kaiser-Yang
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-03T04:20:46Z
user: login: Kaiser-Yang
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-05-03T04:53:20Z
user: login: TheLeoP
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T00:25:14Z
user: login: TheLeoP
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T00:33:41Z
user: login: Kaiser-Yang
state: APPROVED
author_association: OWNER
submitted_at: 2025-05-04T06:20:39Z

Review comments:
diff_hunk: @@ -277,33 +246,30 @@ function GitSource.new(opts, config)
 
     -- cache and pre-cache jobs
     self.cache = require('blink-cmp-git.cache').new()
-    self.pre_cache_jobs = {}
-    local use_items_cache = utils.get_option(self.git_source_config.use_items_cache)
-    local use_items_pre_cache = utils.get_option(self.git_source_config.use_items_pre_cache)
+    local use_items_cache = utils.get_option(self.git_source_config.use_items_cache) ---@type boolean
+    local use_items_pre_cache = utils.get_option(self.git_source_config.use_items_pre_cache) ---@type boolean
     if use_items_cache and use_items_pre_cache then
-        self:create_pre_cache_jobs()
-        self:run_pre_cache_jobs()
+        coroutine.wrap(function() self:create_pre_cache_jobs() end)()
     end
 
     -- reload cache command and autocmd
     vim.api.nvim_create_user_command(blink_cmp_git_reload_cache_command, function()
         self.git_source_config.before_reload_cache()
-        if utils.get_option(self.git_source_config.use_items_cache) then
-            self.cache:clear()
-            if utils.get_option(self.git_source_config.use_items_pre_cache) then
-                self:shutdown_pre_cache_jobs()
-                self:create_pre_cache_jobs()
-                self:run_pre_cache_jobs()
-            end
-        end
+
+        if not utils.get_option(self.git_source_config.use_items_cache) then return end
+        self.cache:clear()
+
+        if not utils.get_option(self.git_source_config.use_items_pre_cache) then return end
+        vim.iter(self.running_pre_cache_jobs):each(function(sys) sys:kill('TERM') end)
path: lua/blink-cmp-git/init.lua
user: login: Kaiser-Yang
body: To use 'job' to substitute the 'sys' may be better.
created_at: 2025-05-02T05:47:32Z
author_association: OWNER
diff_hunk: @@ -46,129 +32,110 @@ local function get_text_edit_range(context)
 end
 
 --- @param feature blink-cmp-git.GCSCompletionOptions
---- @param result string[]
+--- @param result string
 --- @return blink-cmp-git.CompletionItem[]
 local function assemble_completion_items_from_output(feature, result)
-    local items = {}
-    for i, v in ipairs(feature.separate_output(table.concat(result, '\n'))) do
-        items[i] = {
+    local items = {} ---@type blink-cmp-git.CompletionItem[]
+    for _, v in pairs(feature.separate_output(result)) do
+        table.insert(items, {
             label = feature.get_label(v),
             kind_name = feature.get_kind_name(v),
             insert_text = feature.get_insert_text(v),
             documentation = feature.get_documentation(v),
-        }
+        })
     end
     feature.configure_score_offset(items)
     return items
 end
 
 --- @param feature blink-cmp-git.GCSCompletionOptions
---- @param items table
---- @return Job
-local function create_job_from_feature(feature, items)
-    local command = utils.get_option(feature.get_command)
-    local token = utils.get_option(feature.get_token)
-    ---@diagnostic disable-next-line: missing-fields
-    return Job:new({
-        command = command,
-        args = utils.get_option(feature.get_command_args, command, token),
-        cwd = utils.get_cwd(),
-        env = utils.get_job_default_env(),
-        on_exit = function(j, return_value, signal)
-            if signal == 9 then return end
-            if return_value ~= 0 or utils.truthy(j:stderr_result()) then
-                if feature.on_error(return_value, table.concat(j:stderr_result(), '\n')) then
-                    return
-                end
-            end
-            if utils.truthy(j:result()) then
-                local match_list = assemble_completion_items_from_output(feature, j:result())
-                vim.iter(match_list):each(
-                    function(match)
-                        items[match] = {
-                            label = match.label,
-                            kind = require('blink.cmp.types').CompletionItemKind[match.kind_name]
-                                or 0,
-                            textEdit = {
-                                newText = match.insert_text
-                                    .. (utils.get_option(feature.insert_text_trailing) or ''),
-                            },
-                            score_offset = match.score_offset,
-                            documentation = match.documentation,
-                        }
-                    end
-                )
-            else
-                log.trace('search command return empty result')
-            end
-        end,
-    })
-end
+--- @return blink.cmp.CompletionItem[]
+--- @async
+local function items_for_feature(feature, running_pre_cache_jobs)
+    local co = coroutine.running()
+    assert(co, 'This function should run inside a coroutine')
+
+    local command = utils.get_option(feature.get_command) ---@type string
+    local token = utils.get_option(feature.get_token) ---@type string
+    local args = utils.get_option(feature.get_command_args, command, token) ---@type string[]
+
+    local sys = vim.system(
+        vim.list_extend({ command }, args),
+        {
+            text = true,
+            cwd = utils.get_cwd(),
+            env = utils.get_job_default_env(),
+        },
+        vim.schedule_wrap(function(out)
+            local ok, err = coroutine.resume(co, out)
+            if not ok then vim.notify(debug.traceback(co, err), vim.log.levels.ERROR) end
+        end)
+    )
+    table.insert(running_pre_cache_jobs, sys)
+    local out = coroutine.yield() --[[@as vim.SystemCompleted]]
 
---- Clear the content in items but do not delete the table so that the table can be reused
---- @param items table
-local function clear_items(items)
-    for key, _ in pairs(items) do
-        items[key] = nil
+    local signal = out.signal
+    local return_value = out.code
+    if signal == 9 then return {} end
+    if return_value ~= 0 or utils.truthy(out.stderr) then
+        if feature.on_error(return_value, out.stderr) then return {} end
     end
+    if out.stdout == '' then
+        log.trace('search command return empty result')
+        return {}
+    end
+
+    local match_list = assemble_completion_items_from_output(feature, out.stdout)
+    return vim.iter(match_list)
+        :map(
+            function(match)
+                return {
+                    label = match.label,
+                    kind = require('blink.cmp.types').CompletionItemKind[match.kind_name] or 0,
+                    textEdit = {
+                        newText = match.insert_text
+                            .. (utils.get_option(feature.insert_text_trailing) or ''),
+                    },
+                    score_offset = match.score_offset,
+                    documentation = match.documentation,
+                }
+            end
+        )
+        :totable()
 end
 
+--- @async
 function GitSource:create_pre_cache_jobs()
-    local async = utils.get_option(self.git_source_config.async)
-    self.pre_cache_jobs = {}
+    local co = coroutine.running()
+
+    local all_items = {} ---@type table<string, {items: table}>
+    local n_coroutines = 0
+
     for _, feature in pairs(self:get_enabled_features()) do
-        for _, trigger in pairs(utils.get_option(feature.triggers)) do
-            if not self.pre_cache_jobs[trigger] then
-                self.pre_cache_jobs[trigger] = {
+        local triggers = utils.get_option(feature.triggers) ---@type string[]
+        for _, trigger in pairs(triggers) do
+            if not all_items[trigger] then
+                all_items[trigger] = {
path: lua/blink-cmp-git/init.lua
user: login: Kaiser-Yang
body: There is only one key in `all_items[trigger]`, to let `all_items[trigger]` be a list may be better: `all_items[trigger] = {}`
created_at: 2025-05-02T05:58:12Z
author_association: OWNER
diff_hunk: @@ -46,129 +32,110 @@ local function get_text_edit_range(context)
 end
 
 --- @param feature blink-cmp-git.GCSCompletionOptions
---- @param result string[]
+--- @param result string
 --- @return blink-cmp-git.CompletionItem[]
 local function assemble_completion_items_from_output(feature, result)
-    local items = {}
-    for i, v in ipairs(feature.separate_output(table.concat(result, '\n'))) do
-        items[i] = {
+    local items = {} ---@type blink-cmp-git.CompletionItem[]
+    for _, v in pairs(feature.separate_output(result)) do
+        table.insert(items, {
             label = feature.get_label(v),
             kind_name = feature.get_kind_name(v),
             insert_text = feature.get_insert_text(v),
             documentation = feature.get_documentation(v),
-        }
+        })
     end
     feature.configure_score_offset(items)
     return items
 end
 
 --- @param feature blink-cmp-git.GCSCompletionOptions
---- @param items table
---- @return Job
-local function create_job_from_feature(feature, items)
-    local command = utils.get_option(feature.get_command)
-    local token = utils.get_option(feature.get_token)
-    ---@diagnostic disable-next-line: missing-fields
-    return Job:new({
-        command = command,
-        args = utils.get_option(feature.get_command_args, command, token),
-        cwd = utils.get_cwd(),
-        env = utils.get_job_default_env(),
-        on_exit = function(j, return_value, signal)
-            if signal == 9 then return end
-            if return_value ~= 0 or utils.truthy(j:stderr_result()) then
-                if feature.on_error(return_value, table.concat(j:stderr_result(), '\n')) then
-                    return
-                end
-            end
-            if utils.truthy(j:result()) then
-                local match_list = assemble_completion_items_from_output(feature, j:result())
-                vim.iter(match_list):each(
-                    function(match)
-                        items[match] = {
-                            label = match.label,
-                            kind = require('blink.cmp.types').CompletionItemKind[match.kind_name]
-                                or 0,
-                            textEdit = {
-                                newText = match.insert_text
-                                    .. (utils.get_option(feature.insert_text_trailing) or ''),
-                            },
-                            score_offset = match.score_offset,
-                            documentation = match.documentation,
-                        }
-                    end
-                )
-            else
-                log.trace('search command return empty result')
-            end
-        end,
-    })
-end
+--- @return blink.cmp.CompletionItem[]
+--- @async
+local function items_for_feature(feature, running_pre_cache_jobs)
+    local co = coroutine.running()
+    assert(co, 'This function should run inside a coroutine')
+
+    local command = utils.get_option(feature.get_command) ---@type string
+    local token = utils.get_option(feature.get_token) ---@type string
+    local args = utils.get_option(feature.get_command_args, command, token) ---@type string[]
+
+    local sys = vim.system(
+        vim.list_extend({ command }, args),
+        {
+            text = true,
+            cwd = utils.get_cwd(),
+            env = utils.get_job_default_env(),
+        },
+        vim.schedule_wrap(function(out)
+            local ok, err = coroutine.resume(co, out)
+            if not ok then vim.notify(debug.traceback(co, err), vim.log.levels.ERROR) end
+        end)
+    )
+    table.insert(running_pre_cache_jobs, sys)
+    local out = coroutine.yield() --[[@as vim.SystemCompleted]]
 
---- Clear the content in items but do not delete the table so that the table can be reused
---- @param items table
-local function clear_items(items)
-    for key, _ in pairs(items) do
-        items[key] = nil
+    local signal = out.signal
+    local return_value = out.code
+    if signal == 9 then return {} end
+    if return_value ~= 0 or utils.truthy(out.stderr) then
+        if feature.on_error(return_value, out.stderr) then return {} end
     end
+    if out.stdout == '' then
+        log.trace('search command return empty result')
+        return {}
+    end
+
+    local match_list = assemble_completion_items_from_output(feature, out.stdout)
+    return vim.iter(match_list)
+        :map(
+            function(match)
+                return {
+                    label = match.label,
+                    kind = require('blink.cmp.types').CompletionItemKind[match.kind_name] or 0,
+                    textEdit = {
+                        newText = match.insert_text
+                            .. (utils.get_option(feature.insert_text_trailing) or ''),
+                    },
+                    score_offset = match.score_offset,
+                    documentation = match.documentation,
+                }
+            end
+        )
+        :totable()
 end
 
+--- @async
 function GitSource:create_pre_cache_jobs()
-    local async = utils.get_option(self.git_source_config.async)
-    self.pre_cache_jobs = {}
+    local co = coroutine.running()
+
+    local all_items = {} ---@type table<string, {items: table}>
+    local n_coroutines = 0
+
     for _, feature in pairs(self:get_enabled_features()) do
-        for _, trigger in pairs(utils.get_option(feature.triggers)) do
-            if not self.pre_cache_jobs[trigger] then
-                self.pre_cache_jobs[trigger] = {
+        local triggers = utils.get_option(feature.triggers) ---@type string[]
+        for _, trigger in pairs(triggers) do
+            if not all_items[trigger] then
+                all_items[trigger] = {
                     items = {},
-                    jobs = {},
                 }
             end
-            local jobs_and_items = self.pre_cache_jobs[trigger]
-            local next_job = create_job_from_feature(feature, jobs_and_items.items)
-            if utils.truthy(jobs_and_items.jobs) then
-                jobs_and_items.jobs[#jobs_and_items.jobs]:after(function(_, code, signal)
-                    -- shutdown
-                    if signal == 9 then return end
-                    if async then
-                        next_job:start()
-                    else
-                        -- this must in a schedule
-                        vim.schedule(function() next_job:sync() end)
-                    end
-                end)
-            end
-            table.insert(jobs_and_items.jobs, next_job)
-        end
-    end
-    for trigger, job_and_items in pairs(self.pre_cache_jobs) do
-        -- let the last job set the cache
-        job_and_items.jobs[#job_and_items.jobs]:after(function(_, _, signal)
-            if signal == 9 then return end
-            for key, item in pairs(job_and_items.items) do
-                self.cache:set({ trigger, item.label }, item)
-            end
-            clear_items(job_and_items.items)
-        end)
-    end
-end
+            local items = all_items[trigger]
 
-function GitSource:shutdown_pre_cache_jobs()
-    for _, job_and_items in pairs(self.pre_cache_jobs) do
-        for _, job in pairs(job_and_items.jobs) do
-            job:shutdown(0, 9)
+            coroutine.wrap(function()
+                n_coroutines = n_coroutines + 1
+                local new_items = items_for_feature(feature, self.running_pre_cache_jobs)
+                vim.list_extend(items.items, new_items)
+                coroutine.resume(co)
+            end)()
         end
-        clear_items(job_and_items.items)
     end
-end
+    for _ = 1, n_coroutines do
+        coroutine.yield()
+    end
 
-function GitSource:run_pre_cache_jobs()
-    local async = utils.get_option(self.git_source_config.async)
-    for _, job_and_items in pairs(self.pre_cache_jobs) do
-        if async then
-            job_and_items.jobs[1]:start()
-        else
-            job_and_items.jobs[1]:sync()
+    for trigger, job_and_items in pairs(all_items) do
path: lua/blink-cmp-git/init.lua
user: login: Kaiser-Yang
body: `job_and_items` should be renamed when trasfering `all_items[trigger]` to a list.
created_at: 2025-05-02T06:02:03Z
author_association: OWNER
diff_hunk: @@ -46,129 +32,110 @@ local function get_text_edit_range(context)
 end
 
 --- @param feature blink-cmp-git.GCSCompletionOptions
---- @param result string[]
+--- @param result string
 --- @return blink-cmp-git.CompletionItem[]
 local function assemble_completion_items_from_output(feature, result)
-    local items = {}
-    for i, v in ipairs(feature.separate_output(table.concat(result, '\n'))) do
-        items[i] = {
+    local items = {} ---@type blink-cmp-git.CompletionItem[]
+    for _, v in pairs(feature.separate_output(result)) do
+        table.insert(items, {
             label = feature.get_label(v),
             kind_name = feature.get_kind_name(v),
             insert_text = feature.get_insert_text(v),
             documentation = feature.get_documentation(v),
-        }
+        })
     end
     feature.configure_score_offset(items)
     return items
 end
 
 --- @param feature blink-cmp-git.GCSCompletionOptions
---- @param items table
---- @return Job
-local function create_job_from_feature(feature, items)
-    local command = utils.get_option(feature.get_command)
-    local token = utils.get_option(feature.get_token)
-    ---@diagnostic disable-next-line: missing-fields
-    return Job:new({
-        command = command,
-        args = utils.get_option(feature.get_command_args, command, token),
-        cwd = utils.get_cwd(),
-        env = utils.get_job_default_env(),
-        on_exit = function(j, return_value, signal)
-            if signal == 9 then return end
-            if return_value ~= 0 or utils.truthy(j:stderr_result()) then
-                if feature.on_error(return_value, table.concat(j:stderr_result(), '\n')) then
-                    return
-                end
-            end
-            if utils.truthy(j:result()) then
-                local match_list = assemble_completion_items_from_output(feature, j:result())
-                vim.iter(match_list):each(
-                    function(match)
-                        items[match] = {
-                            label = match.label,
-                            kind = require('blink.cmp.types').CompletionItemKind[match.kind_name]
-                                or 0,
-                            textEdit = {
-                                newText = match.insert_text
-                                    .. (utils.get_option(feature.insert_text_trailing) or ''),
-                            },
-                            score_offset = match.score_offset,
-                            documentation = match.documentation,
-                        }
-                    end
-                )
-            else
-                log.trace('search command return empty result')
-            end
-        end,
-    })
-end
+--- @return blink.cmp.CompletionItem[]
+--- @async
+local function items_for_feature(feature, running_pre_cache_jobs)
+    local co = coroutine.running()
+    assert(co, 'This function should run inside a coroutine')
+
+    local command = utils.get_option(feature.get_command) ---@type string
+    local token = utils.get_option(feature.get_token) ---@type string
+    local args = utils.get_option(feature.get_command_args, command, token) ---@type string[]
+
+    local sys = vim.system(
+        vim.list_extend({ command }, args),
+        {
+            text = true,
+            cwd = utils.get_cwd(),
+            env = utils.get_job_default_env(),
+        },
+        vim.schedule_wrap(function(out)
+            local ok, err = coroutine.resume(co, out)
+            if not ok then vim.notify(debug.traceback(co, err), vim.log.levels.ERROR) end
+        end)
+    )
+    table.insert(running_pre_cache_jobs, sys)
+    local out = coroutine.yield() --[[@as vim.SystemCompleted]]
 
---- Clear the content in items but do not delete the table so that the table can be reused
---- @param items table
-local function clear_items(items)
-    for key, _ in pairs(items) do
-        items[key] = nil
+    local signal = out.signal
+    local return_value = out.code
+    if signal == 9 then return {} end
+    if return_value ~= 0 or utils.truthy(out.stderr) then
+        if feature.on_error(return_value, out.stderr) then return {} end
     end
+    if out.stdout == '' then
+        log.trace('search command return empty result')
+        return {}
+    end
+
+    local match_list = assemble_completion_items_from_output(feature, out.stdout)
+    return vim.iter(match_list)
+        :map(
+            function(match)
+                return {
+                    label = match.label,
+                    kind = require('blink.cmp.types').CompletionItemKind[match.kind_name] or 0,
+                    textEdit = {
+                        newText = match.insert_text
+                            .. (utils.get_option(feature.insert_text_trailing) or ''),
+                    },
+                    score_offset = match.score_offset,
+                    documentation = match.documentation,
+                }
+            end
+        )
+        :totable()
 end
 
+--- @async
 function GitSource:create_pre_cache_jobs()
-    local async = utils.get_option(self.git_source_config.async)
-    self.pre_cache_jobs = {}
+    local co = coroutine.running()
+
path: lua/blink-cmp-git/init.lua
user: login: Kaiser-Yang
body: Add `assert(co)` here.
created_at: 2025-05-02T06:04:51Z
author_association: OWNER
diff_hunk: @@ -46,129 +32,110 @@ local function get_text_edit_range(context)
 end
 
 --- @param feature blink-cmp-git.GCSCompletionOptions
---- @param result string[]
+--- @param result string
 --- @return blink-cmp-git.CompletionItem[]
 local function assemble_completion_items_from_output(feature, result)
-    local items = {}
-    for i, v in ipairs(feature.separate_output(table.concat(result, '\n'))) do
-        items[i] = {
+    local items = {} ---@type blink-cmp-git.CompletionItem[]
+    for _, v in pairs(feature.separate_output(result)) do
+        table.insert(items, {
             label = feature.get_label(v),
             kind_name = feature.get_kind_name(v),
             insert_text = feature.get_insert_text(v),
             documentation = feature.get_documentation(v),
-        }
+        })
     end
     feature.configure_score_offset(items)
     return items
 end
 
 --- @param feature blink-cmp-git.GCSCompletionOptions
---- @param items table
---- @return Job
-local function create_job_from_feature(feature, items)
-    local command = utils.get_option(feature.get_command)
-    local token = utils.get_option(feature.get_token)
-    ---@diagnostic disable-next-line: missing-fields
-    return Job:new({
-        command = command,
-        args = utils.get_option(feature.get_command_args, command, token),
-        cwd = utils.get_cwd(),
-        env = utils.get_job_default_env(),
-        on_exit = function(j, return_value, signal)
-            if signal == 9 then return end
-            if return_value ~= 0 or utils.truthy(j:stderr_result()) then
-                if feature.on_error(return_value, table.concat(j:stderr_result(), '\n')) then
-                    return
-                end
-            end
-            if utils.truthy(j:result()) then
-                local match_list = assemble_completion_items_from_output(feature, j:result())
-                vim.iter(match_list):each(
-                    function(match)
-                        items[match] = {
-                            label = match.label,
-                            kind = require('blink.cmp.types').CompletionItemKind[match.kind_name]
-                                or 0,
-                            textEdit = {
-                                newText = match.insert_text
-                                    .. (utils.get_option(feature.insert_text_trailing) or ''),
-                            },
-                            score_offset = match.score_offset,
-                            documentation = match.documentation,
-                        }
-                    end
-                )
-            else
-                log.trace('search command return empty result')
-            end
-        end,
-    })
-end
+--- @return blink.cmp.CompletionItem[]
+--- @async
+local function items_for_feature(feature, running_pre_cache_jobs)
+    local co = coroutine.running()
+    assert(co, 'This function should run inside a coroutine')
+
+    local command = utils.get_option(feature.get_command) ---@type string
+    local token = utils.get_option(feature.get_token) ---@type string
+    local args = utils.get_option(feature.get_command_args, command, token) ---@type string[]
+
+    local sys = vim.system(
path: lua/blink-cmp-git/init.lua
user: login: Kaiser-Yang
body: Use `job` as the variable name.
created_at: 2025-05-02T06:07:29Z
author_association: OWNER
diff_hunk: @@ -35,9 +35,8 @@
 --- @alias blink-cmp-git.GCSGitCenterKeys 'github'|'gitlab'|string
 
 -- TODO:
--- remove async and use_items_cache
+--  use_items_cache
path: lua/blink-cmp-git/types.lua
user: login: Kaiser-Yang
body: This should be `remove use_items_cache`
created_at: 2025-05-02T06:13:17Z
author_association: OWNER
diff_hunk: @@ -336,172 +302,98 @@ function GitSource:handle_items_pre_cache(context, callback)
         not utils.get_option(self.git_source_config.use_items_pre_cache)
         or not utils.get_option(self.git_source_config.use_items_cache)
     then
-        return nil
+        return
     end
-    local items = {}
+
+    local items = {} ---@type lsp.CompletionItem[]
     local trigger = context.trigger.initial_character
-    local transformed_callback = function()
-        for _, item in pairs(items) do
-            item.textEdit.range = get_text_edit_range(context)
-            item.gitSourceTrigger = trigger
-        end
-        callback({
-            is_incomplete_backward = false,
-            is_incomplete_forward = false,
-            items = vim.tbl_values(items),
-        })
-    end
-    local job_and_items = self.pre_cache_jobs[trigger]
-    -- no enabled features
-    if not job_and_items then
-        return function() end
-    end
-    local cancel_fun = function()
-        for _, job in pairs(job_and_items.jobs) do
-            job:shutdown(0, 9)
-        end
+
+    items = self.cache:get(trigger) or {}
+    for _, item in pairs(items) do
+        item.textEdit.range = get_text_edit_range(context)
+        item.gitSourceTrigger = trigger
     end
-    local async = utils.get_option(self.git_source_config.async)
-    -- This happens when the async is true in new, but changed to false in the future
-    if not async then
-        -- Wait for jobs to finish
-        local timeout = false
-        for _, job in pairs(job_and_items.jobs) do
-            job:wait()
-            if not job.is_shutdown then
-                timeout = true
-                break
-            end
-        end
-        -- any job timeouts in 5 seconds, cancel jobs
-        if timeout then
-            for _, job in pairs(job_and_items.jobs) do
-                job:shutdown(0, 9)
+    callback({
path: lua/blink-cmp-git/init.lua
user: login: Kaiser-Yang
body: This `callback` here may cause calling `callback` more than once in a single `get_completions`, which may cause problems.

A better way may be just to remove te `use_items_cache` and `use_items_pre_cache` options, and them all the time, therefore there will only be one `callback` to call and this `handle_items_pre_cache` function can be removed.

And if removing the these options, in the new `get_completions` function just need to wait for the pre-cache jobs finish, then use the result. And there is no need to use both `running_jobs` and `running_pre_cache_jobs`, only the `running_pre_cache_jobs` is needed.
created_at: 2025-05-02T06:41:23Z
author_association: OWNER
diff_hunk: @@ -1,18 +1,29 @@
 local M = {}
-local Job = require('plenary.job')
 
+--- @async
 function M.is_inside_git_repo()
+    local co = coroutine.running()
+    assert(co, 'This function should run inside a coroutine')
+
     if not M.command_found('git') then return false end
-    local res = false
-    ---@diagnostic disable-next-line: missing-fields
-    Job:new({
-        command = 'git',
-        args = { 'rev-parse', '--is-inside-work-tree' },
-        cwd = M.get_cwd(),
-        env = M.get_job_default_env(),
-        on_exit = function(_, return_value, _) res = return_value == 0 end,
-    }):sync()
-    return res
+    vim.system(
+        { 'git', 'rev-parse', '--is-inside-work-tree' },
+        {
+            text = true,
+            cwd = M.get_cwd(),
+            env = M.get_job_default_env(),
+        },
+        vim.schedule_wrap(function(out)
+            local ok, err = coroutine.resume(co, out)
+            if not ok then vim.notify(debug.traceback(co, err), vim.log.levels.ERROR) end
+        end)
+    )
+    local out = coroutine.yield() --[[@as vim.SystemCompleted]]
+
+    vim.schedule(function() coroutine.resume(co) end)
path: lua/blink-cmp-git/utils.lua
user: login: Kaiser-Yang
body: Thess two lines seem to be removable.
created_at: 2025-05-02T06:54:50Z
author_association: OWNER
diff_hunk: @@ -1,18 +1,29 @@
 local M = {}
path: lua/blink-cmp-git/utils.lua
user: login: Kaiser-Yang
body: The functions in utils are public to the users, the users may use the function in their code, asserting co context is not suitable in the functions.
created_at: 2025-05-02T07:17:57Z
author_association: OWNER
diff_hunk: @@ -1,18 +1,29 @@
 local M = {}
-local Job = require('plenary.job')
 
+--- @async
 function M.is_inside_git_repo()
+    local co = coroutine.running()
+    assert(co, 'This function should run inside a coroutine')
+
     if not M.command_found('git') then return false end
-    local res = false
-    ---@diagnostic disable-next-line: missing-fields
-    Job:new({
-        command = 'git',
-        args = { 'rev-parse', '--is-inside-work-tree' },
-        cwd = M.get_cwd(),
-        env = M.get_job_default_env(),
-        on_exit = function(_, return_value, _) res = return_value == 0 end,
-    }):sync()
-    return res
+    vim.system(
+        { 'git', 'rev-parse', '--is-inside-work-tree' },
+        {
+            text = true,
+            cwd = M.get_cwd(),
+            env = M.get_job_default_env(),
+        },
+        vim.schedule_wrap(function(out)
+            local ok, err = coroutine.resume(co, out)
+            if not ok then vim.notify(debug.traceback(co, err), vim.log.levels.ERROR) end
+        end)
+    )
+    local out = coroutine.yield() --[[@as vim.SystemCompleted]]
+
+    vim.schedule(function() coroutine.resume(co) end)
path: lua/blink-cmp-git/utils.lua
user: login: TheLeoP
body: Yeah, sorry. I was trying some different things and forgot to remove this. Good catch
created_at: 2025-05-02T12:47:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,18 +1,29 @@
 local M = {}
path: lua/blink-cmp-git/utils.lua
user: login: TheLeoP
body: These functions wouldn't work if they aren't inside of a coroutine even if the `assert` is removed (that's why I added the `assert`, it makes the error more understandable).

Users will simply need to call these functions inside of a coroutine

```lua
-- I prefer this style
coroutine.wrap(function()
  local foo = require('blink-cmp-git').utils.is_inside_git_repo()
  -- the code in here can be written as if it were synchronous code
end)()

-- This can also be done
coroutine.resume(coroutine.create(function()
  local foo = require('blink-cmp-git').utils.is_inside_git_repo()
  -- the code in here can be written as if it were synchronous code
end))
```
created_at: 2025-05-02T12:54:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -336,172 +302,98 @@ function GitSource:handle_items_pre_cache(context, callback)
         not utils.get_option(self.git_source_config.use_items_pre_cache)
         or not utils.get_option(self.git_source_config.use_items_cache)
     then
-        return nil
+        return
     end
-    local items = {}
+
+    local items = {} ---@type lsp.CompletionItem[]
     local trigger = context.trigger.initial_character
-    local transformed_callback = function()
-        for _, item in pairs(items) do
-            item.textEdit.range = get_text_edit_range(context)
-            item.gitSourceTrigger = trigger
-        end
-        callback({
-            is_incomplete_backward = false,
-            is_incomplete_forward = false,
-            items = vim.tbl_values(items),
-        })
-    end
-    local job_and_items = self.pre_cache_jobs[trigger]
-    -- no enabled features
-    if not job_and_items then
-        return function() end
-    end
-    local cancel_fun = function()
-        for _, job in pairs(job_and_items.jobs) do
-            job:shutdown(0, 9)
-        end
+
+    items = self.cache:get(trigger) or {}
+    for _, item in pairs(items) do
+        item.textEdit.range = get_text_edit_range(context)
+        item.gitSourceTrigger = trigger
     end
-    local async = utils.get_option(self.git_source_config.async)
-    -- This happens when the async is true in new, but changed to false in the future
-    if not async then
-        -- Wait for jobs to finish
-        local timeout = false
-        for _, job in pairs(job_and_items.jobs) do
-            job:wait()
-            if not job.is_shutdown then
-                timeout = true
-                break
-            end
-        end
-        -- any job timeouts in 5 seconds, cancel jobs
-        if timeout then
-            for _, job in pairs(job_and_items.jobs) do
-                job:shutdown(0, 9)
+    callback({
path: lua/blink-cmp-git/init.lua
user: login: TheLeoP
body: > This callback here may cause calling callback more than once in a single get_completions, which may cause problems.

Oh, that makes sense. I missed that.

> A better way may be just to remove te use_items_cache and use_items_pre_cache options, and them all the time, therefore there will only be one callback to call and this handle_items_pre_cache function can be removed.

> And if removing the these options, in the new get_completions function just need to wait for the pre-cache jobs finish, then use the result. And there is no need to use both running_jobs and running_pre_cache_jobs, only the running_pre_cache_jobs is needed.

I could try doing that.
created_at: 2025-05-02T13:21:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -336,172 +302,98 @@ function GitSource:handle_items_pre_cache(context, callback)
         not utils.get_option(self.git_source_config.use_items_pre_cache)
         or not utils.get_option(self.git_source_config.use_items_cache)
     then
-        return nil
+        return
     end
-    local items = {}
+
+    local items = {} ---@type lsp.CompletionItem[]
     local trigger = context.trigger.initial_character
-    local transformed_callback = function()
-        for _, item in pairs(items) do
-            item.textEdit.range = get_text_edit_range(context)
-            item.gitSourceTrigger = trigger
-        end
-        callback({
-            is_incomplete_backward = false,
-            is_incomplete_forward = false,
-            items = vim.tbl_values(items),
-        })
-    end
-    local job_and_items = self.pre_cache_jobs[trigger]
-    -- no enabled features
-    if not job_and_items then
-        return function() end
-    end
-    local cancel_fun = function()
-        for _, job in pairs(job_and_items.jobs) do
-            job:shutdown(0, 9)
-        end
+
+    items = self.cache:get(trigger) or {}
+    for _, item in pairs(items) do
+        item.textEdit.range = get_text_edit_range(context)
+        item.gitSourceTrigger = trigger
     end
-    local async = utils.get_option(self.git_source_config.async)
-    -- This happens when the async is true in new, but changed to false in the future
-    if not async then
-        -- Wait for jobs to finish
-        local timeout = false
-        for _, job in pairs(job_and_items.jobs) do
-            job:wait()
-            if not job.is_shutdown then
-                timeout = true
-                break
-            end
-        end
-        -- any job timeouts in 5 seconds, cancel jobs
-        if timeout then
-            for _, job in pairs(job_and_items.jobs) do
-                job:shutdown(0, 9)
+    callback({
path: lua/blink-cmp-git/init.lua
user: login: TheLeoP
body: > I could try doing that.

Done. I removed a lot of code, but everything seems to be working, at least on my end
created_at: 2025-05-02T15:46:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,18 +1,29 @@
 local M = {}
path: lua/blink-cmp-git/utils.lua
user: login: Kaiser-Yang
body: Maybe you can check in the utils if the co is nil, if it is, you can use coroutine.wrap in the utils' functions.
created_at: 2025-05-02T16:08:54Z
author_association: OWNER
diff_hunk: @@ -35,11 +35,7 @@
 --- @alias blink-cmp-git.GCSGitCenterKeys 'github'|'gitlab'|string
 
 -- TODO:
path: lua/blink-cmp-git/types.lua
user: login: Kaiser-Yang
body: This TODO should be removed, too.
created_at: 2025-05-02T16:11:45Z
author_association: OWNER
diff_hunk: @@ -275,35 +244,25 @@ function GitSource.new(opts, config)
         end
     end
 
-    -- cache and pre-cache jobs
     self.cache = require('blink-cmp-git.cache').new()
-    self.pre_cache_jobs = {}
-    local use_items_cache = utils.get_option(self.git_source_config.use_items_cache)
-    local use_items_pre_cache = utils.get_option(self.git_source_config.use_items_pre_cache)
-    if use_items_cache and use_items_pre_cache then
-        self:create_pre_cache_jobs()
-        self:run_pre_cache_jobs()
-    end
+    coroutine.wrap(function() self:create_pre_cache_jobs() end)()
 
     -- reload cache command and autocmd
     vim.api.nvim_create_user_command(blink_cmp_git_reload_cache_command, function()
         self.git_source_config.before_reload_cache()
-        if utils.get_option(self.git_source_config.use_items_cache) then
-            self.cache:clear()
-            if utils.get_option(self.git_source_config.use_items_pre_cache) then
-                self:shutdown_pre_cache_jobs()
-                self:create_pre_cache_jobs()
-                self:run_pre_cache_jobs()
-            end
-        end
+
+        self.cache:clear()
+
+        vim.iter(self.running_pre_cache_jobs):each(function(job) job:kill('TERM') end)
+        self.running_pre_cache_jobs = {}
path: lua/blink-cmp-git/init.lua
user: login: Kaiser-Yang
body: set _after_cache to nil.
created_at: 2025-05-02T16:24:36Z
author_association: OWNER
diff_hunk: @@ -1,18 +1,29 @@
 local M = {}
path: lua/blink-cmp-git/utils.lua
user: login: TheLeoP
body: This wouldn't allow the functions to return any values. You can only "return" values from a coroutine if: 

- you use `coroutine.yield`, which requires the function to be called inside of a coroutine anyway (so, it's not suitable for this situation) 
- you receive a callback as an argument and call it with the "return" values 
created_at: 2025-05-02T17:30:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,20 +123,33 @@ function M.get_remote_name()
     return require('blink-cmp-git').get_latest_git_source_config().get_remote_name()
 end
 
+--- @param remote_name? string
+--- @async
 function M.get_repo_remote_url(remote_name)
+    local co = coroutine.running()
+    assert(co, 'This function should run inside a coroutine')
+
     remote_name = remote_name or M.get_remote_name()
     if not M.command_found('git') then return '' end
-    local output = ''
-    ---@diagnostic disable-next-line: missing-fields
-    Job:new({
-        command = 'git',
-        args = { 'remote', 'get-url', remote_name },
-        cwd = M.get_cwd(),
-        on_exit = function(job, return_value, _)
-            if return_value ~= 0 then return end
-            output = table.concat(job:result(), '\n')
-        end,
-    }):sync()
+
+    vim.schedule(function() coroutine.resume(co) end)
path: lua/blink-cmp-git/utils.lua
user: login: Kaiser-Yang
body: These two lines seem to be removable.
created_at: 2025-05-03T04:07:18Z
author_association: OWNER
diff_hunk: @@ -4,20 +4,22 @@ log.setup({ title = 'blink-cmp-git' })
 
 local last_git_repo
 local function default_should_reload_cache()
-    -- Do not reload cache when the buffer is a prompt buffer
-    -- or the source provider is disabled
-    if vim.bo.buftype == 'prompt' or not utils.source_provider_enabled() then return false end
-    if last_git_repo == nil then
-        last_git_repo = utils.get_git_repo_absolute_path()
-        if not last_git_repo then last_git_repo = '' end
+    coroutine.wrap(function()
path: lua/blink-cmp-git/default/init.lua
user: login: Kaiser-Yang
body: In this wrap, there is a problem which makes the auto load autocmd not work.

In the original code, blink-cmp-git created an autocmd which uses this function's return value to check whether or not to reload the cache when InsertEnter event emits. In this coroutine.wrap, the function will return nil, which makes the autocmd not work.
created_at: 2025-05-03T04:53:14Z
author_association: OWNER
diff_hunk: @@ -4,20 +4,22 @@ log.setup({ title = 'blink-cmp-git' })
 
 local last_git_repo
 local function default_should_reload_cache()
-    -- Do not reload cache when the buffer is a prompt buffer
-    -- or the source provider is disabled
-    if vim.bo.buftype == 'prompt' or not utils.source_provider_enabled() then return false end
-    if last_git_repo == nil then
-        last_git_repo = utils.get_git_repo_absolute_path()
-        if not last_git_repo then last_git_repo = '' end
+    coroutine.wrap(function()
path: lua/blink-cmp-git/default/init.lua
user: login: TheLeoP
body: Sorry, I missed that. Will fix
created_at: 2025-05-04T00:25:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,22 @@ log.setup({ title = 'blink-cmp-git' })
 
 local last_git_repo
 local function default_should_reload_cache()
-    -- Do not reload cache when the buffer is a prompt buffer
-    -- or the source provider is disabled
-    if vim.bo.buftype == 'prompt' or not utils.source_provider_enabled() then return false end
-    if last_git_repo == nil then
-        last_git_repo = utils.get_git_repo_absolute_path()
-        if not last_git_repo then last_git_repo = '' end
+    coroutine.wrap(function()
path: lua/blink-cmp-git/default/init.lua
user: login: TheLeoP
body: Should be fixed now
created_at: 2025-05-04T00:33:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: Kaiser-Yang
created_at: 2025-05-02T07:16:02Z
body: Actually, you can not async everywhere. In neovim, you can only be async when calling vim.system or something to create a new process, any lua code in the neovim is sync. Using plenary is to avoid the callback hell, in this plugin, it sems that using plenary or not are both OK, because this plugin is simple enough.

In the original code, I tried to make all the time consuming jobs async, there jobs are mainly fetching resources from internet. The jobs using sync should be executed very quickly, such as `git log`, it actually block the neovim, but you should not feel it, can you give a more specific explanation of the blocking in Windows? Is the repository really huge?

Coroutine is a good way to write code for this plugin, but this actually not solve the problem you mentioned. Or if i misunderstand  something, can you give a more detailed explanation?
author_association: OWNER
user: login: TheLeoP
created_at: 2025-05-02T10:46:05Z
body: > Actually, you can not async everywhere. In neovim, you can only be async when calling vim.system or something to create a new process, any lua code in the neovim is sync.

I know. Maybe my word choice wasn't the best. This PR makes all calls to external process async, it completely avoids the user of any blocking `vim.wait`.

> Using plenary is to avoid the callback hell, in this plugin, it sems that using plenary or not are both OK, because this plugin is simple enough.

I know. But it's not necessary. Under the hood, plenary uses lua coroutines to avoid callback hell with luv. This PR does the same while removing the dependency on plenary.nvim.

> In the original code, I tried to make all the time consuming jobs async, there jobs are mainly fetching resources from internet. The jobs using sync should be executed very quickly, such as git log, it actually block the neovim, but you should not feel it, can you give a more specific explanation of the blocking in Windows? Is the repository really huge?

It was not a huge repo, it was happening on my nvim-config repo and only on windows. Git it's just that much slower in windows, it has nothing to do with the repo size. The same repository on a unix-like system felt instantaneous, which means that making the code sync shouldn't be a problem in those cases and it's a win-win situation. 

> Coroutine is a good way to write code for this plugin, but this actually not solve the problem you mentioned. Or if i misunderstand something, can you give a more detailed explanation?

It does. That's how plenary (or any lua plugin's async code) works under the hood. It removes the dependency in plenary.nvim and, using it everywhere there are external processes being executed, stops Neovim from being blocked. 

You can take a look at this post to learn more about how coroutines work with Neovim's async code: https://gregorias.github.io/posts/using-coroutines-in-neovim-lua/
author_association: CONTRIBUTOR
user: login: TheLeoP
created_at: 2025-05-02T15:47:38Z
body: I implemented the changes you requested. Let me know if you would like me to modify anything else. I could rebase the commits into a single one if you prefer after your review.
author_association: CONTRIBUTOR
user: login: Kaiser-Yang
created_at: 2025-05-04T06:21:56Z
body: Nice work!

Thank you very much!
author_association: OWNER
user: login: Kaiser-Yang
created_at: 2025-05-07T04:56:35Z
body: This update seems to be causing problems of 'too many open files'.

I'm not sure if the problem is caused by this update, if it is I think this may be something related with the autocmd.

Update: this issue is not related with this update.
author_association: OWNER