Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement crate name autocompletion #120

Merged
merged 6 commits into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docgen/shared/plain_text_config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ popup = {
loading = " ...",
},
},
src = {
completion = {
text = {
prerelease = " pre-release ",
yanked = " yanked ",
Expand Down
28 changes: 23 additions & 5 deletions docgen/templates/documentation.md.in
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
Documentation for `crates.nvim` `<VERSION>`

# Features
- Complete crate versions and features using one of:
- Complete crate names, versions and features using one of:
- In-process language server (`lsp`)
- [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) source (`src.cmp`)
- [coq.nvim](https://github.com/ms-jpq/coq_nvim) source (`src.coq`)
- [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) source (`completion.cmp`)
- [coq.nvim](https://github.com/ms-jpq/coq_nvim) source (`completion.coq`)
- Code actions using one of:
- In-process language server (`lsp`)
- [null-ls.nvim](https://github.com/jose-elias-alvarez/null-ls.nvim)/[none-ls.nvim](https://github.com/nvimtools/none-ls.nvim)
Expand Down Expand Up @@ -61,7 +61,7 @@ Enable it in the setup.
```lua
require("crates").setup {
...
src = {
completion = {
...
cmp = {
enabled = true,
Expand Down Expand Up @@ -103,7 +103,7 @@ Enable it in the setup, and optionally change the display name.
```lua
require("crates").setup {
...
src = {
completion = {
...
coq = {
enabled = true,
Expand All @@ -113,6 +113,24 @@ require("crates").setup {
}
```

### Crate name completion

Crate names in dependencies can be completed from searches on `crates.io`. This has to be
enabled seperately:

```lua
require("crates").setup {
...
completion = {
crates = {
enabled = true -- disabled by default
max_results = 8 -- The maximum number of search results to display
min_chars = 3 -- The minimum number of charaters to type before completions begin appearing
}
}
}
```

## Code actions
Code actions are supported in a few different ways, either by the [in-process language server],
which also supports completion, or by the null-ls/none-ls source.
Expand Down
159 changes: 159 additions & 0 deletions lua/crates/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ local M = {
crate_jobs = {},
---@type table<string,DepsJob>
deps_jobs = {},
---@type table<string,SearchJob>
search_jobs = {},
---@type QueuedJob[]
queued_jobs = {},
---@type QueuedSearchJob[]
search_queue = {},
---@type integer
num_requests = 0,
}
Expand All @@ -29,13 +33,21 @@ local M = {
---@field job Job
---@field callbacks fun(deps: ApiDependency[]|nil, cancelled: boolean)[]

---@class SearchJob
---@field job Job
---@field callbacks fun(search: ApiCrateSummary[]?, cancelled: boolean)[]

---@class QueuedJob
---@field kind JobKind
---@field name string
---@field crate_callbacks fun(crate: ApiCrate|nil, cancelled: boolean)[]
---@field version string
---@field deps_callbacks fun(deps: ApiDependency[]|nil, cancelled: boolean)[]

---@class QueuedSearchJob
---@field name string
---@field callbacks fun(deps: ApiCrateSummary[]?, cancelled: boolean)[]

---@enum JobKind
local JobKind = {
CRATE = 1,
Expand Down Expand Up @@ -184,6 +196,110 @@ local function enqueue_deps_job(name, version, callbacks)
})
end

---@param name string
---@param callbacks fun(search: ApiCrateSummary[]?, cancelled: boolean)[]
local function enqueue_search_job(name, callbacks)
for _, j in ipairs(M.search_queue) do
if j.name == name then
vim.list_extend(j.callbacks, callbacks)
return
end
end

table.insert(M.search_queue, {
name = name,
callbacks = callbacks,
})
end

---@param json_str string
---@return ApiCrateSummary[]?
function M.parse_search(json_str)
local json = parse_json(json_str)
if not (json and json.crates) then
return
end

---@type ApiCrateSummary[]
local search = {}
---@diagnostic disable-next-line: no-unknown
for _, c in ipairs(json.crates) do
---@type ApiCrateSummary
local result = {
name = c.name,
description = c.description,
newest_version = c.newest_version,
}
table.insert(search, result)
end

return search
end

---@param name string
---@param callbacks fun(search: ApiCrateSummary[]?, cancelled: boolean)[]
local function fetch_search(name, callbacks)
local existing = M.search_jobs[name]
if existing then
vim.list_extend(existing.callbacks, callbacks)
return
end

if M.num_requests >= state.cfg.max_parallel_requests then
enqueue_search_job(name, callbacks)
return
end

local url = string.format(
"%s/crates?q=%s&per_page=%s",
ENDPOINT,
name,
state.cfg.completion.crates.max_results
)

---@param json_str string?
---@param cancelled boolean
local function on_exit(json_str, cancelled)
---@type ApiCrateSummary[]?
local search
if not cancelled and json_str then
local ok, s = pcall(M.parse_search, json_str)
if ok then
search = s
end
end
for _, c in ipairs(callbacks) do
c(search, cancelled)
end

M.search_jobs[name] = nil
M.num_requests = M.num_requests - 1

M.run_queued_jobs()
end

local job = start_job(url, on_exit)
if job then
M.num_requests = M.num_requests + 1
M.search_jobs[name] = {
job = job,
callbacks = callbacks,
}
else
for _, c in ipairs(callbacks) do
c(nil, false)
end
end
end

---@param name string
---@return ApiCrateSummary[]?, boolean
function M.fetch_search(name)
---@param resolve fun(search: ApiCrateSummary[]?, cancelled: boolean)
return coroutine.yield(function(resolve)
fetch_search(name, { resolve })
end)
end

---@param json_str string
---@return ApiCrate|nil
Expand Down Expand Up @@ -449,6 +565,12 @@ function M.is_fetching_deps(name, version)
return M.deps_jobs[name .. ":" .. version] ~= nil
end

---@param name string
---@return boolean
function M.is_fetching_search(name)
return M.search_jobs[name] ~= nil
end

---@param name string
---@param callback fun(crate: ApiCrate|nil, cancelled: boolean)
local function add_crate_callback(name, callback)
Expand Down Expand Up @@ -487,7 +609,32 @@ function M.await_deps(name, version)
end)
end

---@param name string
---@param callback fun(deps: ApiCrateSummary[]?, cancelled: boolean)
local function add_search_callback(name, callback)
table.insert(
M.search_jobs[name].callbacks,
callback
)
end

---@param name string
---@return ApiCrateSummary[]?, boolean
function M.await_search(name)
---@param resolve fun(crate: ApiCrateSummary[]?, cancelled: boolean)
return coroutine.yield(function(resolve)
add_search_callback(name, resolve)
end)
end

function M.run_queued_jobs()
-- Prioritise crate searches
if #M.search_queue > 0 then
local job = table.remove(M.search_queue, 1)
fetch_search(job.name, job.search_callbacks)
return
end

if #M.queued_jobs == 0 then
return
end
Expand All @@ -507,8 +654,20 @@ function M.cancel_jobs()
for _, r in pairs(M.deps_jobs) do
cancel_job(r.job)
end
for _, r in pairs(M.search_jobs) do
cancel_job(r.job)
end

M.crate_jobs = {}
M.deps_jobs = {}
M.search_jobs = {}
end

function M.cancel_search_jobs()
for _, r in pairs(M.search_jobs) do
cancel_job(r.job)
end
M.search_jobs = {}
end

return M
6 changes: 3 additions & 3 deletions lua/crates/src/cmp.lua → lua/crates/completion/cmp.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
local src = require("crates.src.common")
local completion = require("crates.completion.common")

---@class Cmp
---@field register_source fun(name: string, src: CmpCompletionSource)
Expand Down Expand Up @@ -92,15 +92,15 @@ end
---@param _params CmpSourceBaseApiParams
---@return string[]
function M:get_trigger_characters(_params)
return src.trigger_characters
return completion.trigger_characters
end

---Invoke completion (required).
--- If you want to abort completion, just call the callback without arguments.
---@param _params CmpSourceBaseApiParams
---@param callback fun(list: CompletionList|nil)
function M:complete(_params, callback)
src.complete(callback)
completion.complete(callback)
end

function M.setup()
Expand Down
Loading
Loading