├── LICENSE
├── README.md
├── demo.gif
├── doc
├── cmp_go_deep.txt
└── tags
├── lua
└── cmp_go_deep
│ ├── db.lua
│ ├── gopls_requests.lua
│ ├── init.lua
│ ├── test.lua
│ ├── treesitter_implementations.lua
│ └── utils.lua
└── plugin
└── cmp_go_deep.lua
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Samiul Islam
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cmp-go-deep
2 |
3 | A Go ```deep-completion``` source for [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) / [blink.cmp](https://github.com/Saghen/blink.cmp), that works alongside [cmp-nvim-lsp](https://github.com/hrsh7th/cmp-nvim-lsp) / [blink.cmp](https://github.com/Saghen/blink.cmp)'s LSP source and provides completion suggestions for "UNIMPORTED LOCAL, INTERNAL, AND VENDORED PACKAGES ONLY".
4 |
5 | #### Why?
6 |
7 | At the time of writing, the GoLang Language Server (```gopls@v0.18.1```) doesn't seem to support deep completions for unimported packages. For example, with deep completion enabled, typing ```'cha'``` could suggest ```'rand.NewChaCha8()'``` as a possible completion option - but that is not the case no matter how high the completion budget is set for ```gopls```.
8 |
9 |
10 | #### How?
11 |
12 |
13 | Query ```gopls's``` ```workspace/symbol``` endpoint, cache the results using ```sqlite```, convert the resulting ```SymbolInformation``` into ```completionItemKinds```, filter the results to only include the ones that are unimported, then finally feed them back into ```nvim-cmp``` / ```blink.cmp```
14 |
15 | ---
16 | ⚠️ it might take a while for the packages to be indexed by gopls in huge codebases
17 | #### Demo
18 |
19 | * Note: Due to how gopls indexes packages, completions for standard library packages are not available until at least one of them is manually imported.
20 |
21 |
22 |
23 |
24 | ---
25 | ## Requirements
26 | - [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) OR [blink.cmp](https://github.com/saghen/blink.cmp)
27 | - [sqlite.lua](https://github.com/kkharji/sqlite.lua)
28 |
29 | ## Setup
30 | #### Lazy.nvim
31 | ##### - nvim-cmp
32 | ```lua
33 | {
34 | "hrsh7th/nvim-cmp",
35 | dependencies = {
36 | { "samiulsami/cmp-go-deep", dependencies = { "kkharji/sqlite.lua" } },
37 | },
38 | ...
39 | require("cmp").setup({
40 | sources = {{
41 | name = "go_deep",
42 | keyword_length = 3,
43 | max_item_count = 5,
44 | ---@module "cmp_go_deep"
45 | ---@type cmp_go_deep.Options
46 | option = {
47 | -- See below for configuration options
48 | },
49 | }},
50 | })
51 | }
52 | ```
53 | ##### - blink.cmp (requires saghen/blink.compat)
54 | ```lua
55 | {
56 | "saghen/blink.cmp",
57 | dependencies = {
58 | { "samiulsami/cmp-go-deep", dependencies = { "kkharji/sqlite.lua" } },
59 | { "saghen/blink.compat" },
60 | },
61 | opts = {
62 | sources = {
63 | default = {
64 | "go_deep",
65 | },
66 | providers = {
67 | go_deep = {
68 | name = "go_deep",
69 | module = "blink.compat.source",
70 | min_keyword_length = 3,
71 | max_items = 5,
72 | ---@module "cmp_go_deep"
73 | ---@type cmp_go_deep.Options
74 | opts = {
75 | -- See below for configuration options
76 | },
77 | },
78 | },
79 | },
80 | },
81 | }
82 | ```
83 | ### Default options
84 | ```lua
85 | {
86 | -- Enable/disable notifications.
87 | notifications = true,
88 |
89 | -- Filetypes to enable the source for.
90 | filetypes = { "go" },
91 |
92 | -- How to get documentation for Go symbols.
93 | -- options:
94 | -- "hover" - LSP 'textDocument/hover'. Prettier.
95 | -- "regex" - faster and simpler.
96 | get_documentation_implementation = "regex",
97 |
98 | -- How to get the package names.
99 | -- options:
100 | -- "treesitter" - accurate but slower.
101 | -- "regex" - faster but can fail in edge cases.
102 | get_package_name_implementation = "regex",
103 |
104 | -- Whether to exclude vendored packages from completions.
105 | exclude_vendored_packages = false,
106 |
107 | -- Timeout in milliseconds for fetching documentation.
108 | -- Controls how long to wait for documentation to load.
109 | documentation_wait_timeout_ms = 100,
110 |
111 | -- Maximum time (in milliseconds) to wait before "locking-in" the current request and sending it to gopls.
112 | debounce_gopls_requests_ms = 250
113 |
114 | -- Maximum time (in milliseconds) to wait before "locking-in" the current request and loading data from cache.
115 | debounce_cache_requests_ms = 50
116 |
117 | -- Path to store the SQLite database
118 | -- Default: "~/.local/share/nvim/cmp_go_deep.sqlite3"
119 | db_path = vim.fn.stdpath("data") .. "/cmp_go_deep.sqlite3",
120 |
121 | -- Maximum size for the SQLite database in bytes.
122 | db_size_limit_bytes = 200 * 1024 * 1024, -- 200MB
123 | }
124 | ```
125 | ---
126 | #### TODO
127 | - [x] Cache results for faster completions.
128 | - [x] Cross-project cache sharing for internal packages.
129 | - [x] Better memory usage.
130 | - [ ] ~~Remove the indirect dependency on ```cmp-nvim-lsp``` or ```blink.cmp's``` LSP source.~~
131 | - [ ] Don't ignore package names while matching symbols.
132 | - [ ] Archive after [this issue](https://github.com/golang/go/issues/38528) is properly addressed.
133 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samiulsami/cmp-go-deep/8dcaaf3a450c6c06d67e6c305e346fbf247b6ded/demo.gif
--------------------------------------------------------------------------------
/doc/cmp_go_deep.txt:
--------------------------------------------------------------------------------
1 | cmp_go_deep Options *cmp_go_deep-options*
2 | ==============================================================================
3 | DESCRIPTION *cmp_go_deep-description*
4 |
5 | This section describes the default options for the
6 | |cmp_go_deep| Go source that provides deep symbol
7 | completions for unimported packages.
8 |
9 | =============================================================================
10 | SETUP-nvim-cmp *cmp_go_deep-setup-nvim-cmp*
11 | >lua
12 | {
13 | "hrsh7th/nvim-cmp",
14 | dependencies = {
15 | { "samiulsami/cmp-go-deep", dependencies = { "kkharji/sqlite.lua" } },
16 | },
17 | ...
18 | require("cmp").setup({
19 | sources = {{
20 | name = "go_deep",
21 | keyword_length = 3,
22 | max_item_count = 5,
23 | ---@module "cmp_go_deep"
24 | ---@type cmp_go_deep.Options
25 | option = {
26 | -- See below for configuration options
27 | },
28 | }},
29 | })
30 | }
31 | <
32 |
33 | ==============================================================================
34 | SETUP-blink.cmp *cmp_go_deep-setup-blink*
35 | >lua
36 | {
37 | "saghen/blink.cmp",
38 | dependencies = {
39 | { "samiulsami/cmp-go-deep", dependencies = { "kkharji/sqlite.lua" } },
40 | { "saghen/blink.compat" },
41 | },
42 | opts = {
43 | sources = {
44 | default = {
45 | "go_deep",
46 | },
47 | providers = {
48 | go_deep = {
49 | name = "go_deep",
50 | module = "blink.compat.source",
51 | min_keyword_length = 3,
52 | max_items = 5,
53 | ---@module "cmp_go_deep"
54 | ---@type cmp_go_deep.Options
55 | opts = {
56 | -- See below for configuration options
57 | },
58 | },
59 | },
60 | },
61 | },
62 | }
63 | <
64 |
65 | ==============================================================================
66 | DEFAULT OPTIONS *cmp_go_deep-default-options*
67 |
68 | The following options can be configured:
69 |
70 | • `notifications`
71 | Enable/disable notifications.
72 | Default: `true`
73 |
74 | • `filetypes`
75 | Filetypes to enable the source for.
76 | Default: `{"go"}`
77 |
78 | • `get_documentation_implementation`
79 | How to get documentation for Go symbols.
80 | Options:
81 | "hover" - LSP 'textDocument/hover'. Prettier.
82 | "regex" - faster and simpler.
83 | Default: `"regex"`
84 |
85 | • `get_package_name_implementation`
86 | How to get the package names.
87 | Options:
88 | "treesitter" - accurate but slower.
89 | "regex" - faster but can fail in edge cases.
90 | Default: `"regex"`
91 |
92 | • `exclude_vendored_packages`
93 | Whether to exclude vendored packages from completions.
94 | Default: `false`
95 |
96 | • `documentation_wait_timeout_ms`
97 | Timeout in milliseconds for fetching documentation.
98 | Controls how long to wait for documentation to load.
99 | Default: `100` (0.5 seconds)
100 |
101 | • `debounce_gopls_requests_ms`
102 | Timeout in milliseconds for "locking-in" the current request and sending it to gopls.
103 | Default: `250`
104 |
105 | • `debounce_cache_requests_ms`
106 | Timeout in milliseconds for "locking-in" the current request and loading data from cache.
107 | Default: `50`
108 |
109 | • `db_path`
110 | Path to store the SQLite database.
111 | Default: `vim.fn.stdpath("data") .. "/cmp_go_deep.sqlite3"`
112 |
113 | • `db_size_limit_bytes`
114 | Maximum size for the SQLite database in bytes.
115 | Default: `200 * 1024 * 1024` (200MB)
116 | ==============================================================================
117 |
118 | vim:tw=80:ts=4:ft=help:norl:
119 |
--------------------------------------------------------------------------------
/doc/tags:
--------------------------------------------------------------------------------
1 | cmp_go_deep-default-options cmp_go_deep.txt /*cmp_go_deep-default-options*
2 | cmp_go_deep-description cmp_go_deep.txt /*cmp_go_deep-description*
3 | cmp_go_deep-options cmp_go_deep.txt /*cmp_go_deep-options*
4 | cmp_go_deep-setup-blink cmp_go_deep.txt /*cmp_go_deep-setup-blink*
5 | cmp_go_deep-setup-nvim-cmp cmp_go_deep.txt /*cmp_go_deep-setup-nvim-cmp*
6 |
--------------------------------------------------------------------------------
/lua/cmp_go_deep/db.lua:
--------------------------------------------------------------------------------
1 | local sqlstmt = require("sqlite.stmt")
2 | ---@type sqlite_db
3 | local sqlite = require("sqlite.db")
4 | local math = require("math")
5 |
6 | ---@class cmp_go_deep.DB
7 | ---@field public setup fun(opts: cmp_go_deep.Options): cmp_go_deep.DB?
8 | ---@field public load fun(self, query_string: string): table
9 | ---@field public save fun(self, utils: cmp_go_deep.utils, symbol_information: table): nil
10 | ---@field private db sqlite_db
11 | ---@field private db_path string
12 | ---@field private max_db_size_bytes number
13 | ---@field private total_rows_estimate number -- pessimistic overestimation
14 | ---@field private notifications boolean
15 | ---@field private MAX_ROWS_THRESHOLD number
16 | local DB = {}
17 | local SCHEMA_VERSION = "0.0.6"
18 |
19 | ---@param opts cmp_go_deep.Options
20 | ---@return cmp_go_deep.DB?
21 | function DB.setup(opts)
22 | DB.notifications = opts.notifications
23 | DB.db_path = opts.db_path
24 | DB.max_db_size_bytes = opts.db_size_limit_bytes
25 | DB.db = sqlite:open(opts.db_path)
26 | DB.MAX_ROWS_THRESHOLD = math.min(100000, math.floor(opts.db_size_limit_bytes / 1024))
27 | DB.MAX_ROWS_THRESHOLD = math.max(DB.MAX_ROWS_THRESHOLD, 10000)
28 |
29 | ---TODO: rtfm and fine-tune these
30 | local result = DB.db:eval("PRAGMA journal_mode = WAL")
31 | if type(result) == "table" and result[1] and result[1].journal_mode ~= "wal" then
32 | if DB.notifications then
33 | vim.notify("Failed to set journal_mode to WAL", vim.log.levels.WARN)
34 | end
35 | end
36 | DB.db:eval("PRAGMA synchronous = NORMAL")
37 | DB.db:eval("PRAGMA temp_store = MEMORY")
38 | DB.db:eval("PRAGMA cache_size = -10000")
39 | DB.db:eval("PRAGMA wal_autocheckpoint = 1000")
40 | DB.db:eval("PRAGMA page_size = 4096")
41 | DB.db:eval("PRAGMA auto_vacuum = incremental")
42 | DB.db:eval("PRAGMA max_page_count = " .. math.ceil(DB.max_db_size_bytes / 4096))
43 |
44 | DB.db:eval([[
45 | CREATE TABLE IF NOT EXISTS meta (
46 | version TEXT PRIMARY KEY
47 | );
48 | ]])
49 |
50 | local res = DB.db:eval("SELECT version FROM meta;")
51 | if type(res) ~= "table" or #res == 0 or res[1].version ~= SCHEMA_VERSION then
52 | DB.db:eval("DELETE FROM meta")
53 | DB.db:eval("INSERT INTO meta (version) VALUES ('" .. SCHEMA_VERSION .. "')")
54 | DB.db:eval("DROP TABLE IF EXISTS gosymbols")
55 | DB.db:eval("DROP TABLE IF EXISTS gosymbols_fts")
56 | DB.db:eval("DROP TABLE IF EXISTS gosymbol_cache")
57 | end
58 |
59 | local tables = {
60 | [[
61 | CREATE TABLE IF NOT EXISTS gosymbols (
62 | id INTEGER PRIMARY KEY AUTOINCREMENT,
63 | hash TEXT UNIQUE NOT NULL,
64 | name TEXT NOT NULL,
65 | data TEXT NOT NULL,
66 | last_modified INTEGER NOT NULL
67 | );
68 | ]],
69 |
70 | [[
71 | CREATE VIRTUAL TABLE IF NOT EXISTS gosymbols_fts
72 | USING fts5(name, id UNINDEXED, tokenize='trigram', detail='none');
73 | ]],
74 | }
75 |
76 | for _, sql in ipairs(tables) do
77 | if not DB.db:eval(sql) then
78 | if DB.notifications then
79 | vim.notify("[sqlite] failed to create table", vim.log.levels.ERROR)
80 | end
81 | return nil
82 | end
83 | end
84 |
85 | res = DB.db:eval("SELECT COUNT(*) as count FROM gosymbols")
86 | if type(res) ~= "table" or #res == 0 then
87 | if DB.notifications then
88 | vim.notify("[sqlite] error reading db row count", vim.log.levels.ERROR)
89 | end
90 | return nil
91 | end
92 | DB.total_rows_estimate = res[1].count
93 |
94 | if opts.debug then
95 | vim.notify("[sqlite] db row count: " .. DB.total_rows_estimate, vim.log.levels.INFO)
96 | end
97 |
98 | DB.db:eval("CREATE INDEX IF NOT EXISTS idx_last_modified ON gosymbols (last_modified DESC);")
99 | DB.db:eval("CREATE INDEX IF NOT EXISTS idx_hash ON gosymbols (hash DESC);")
100 |
101 | return DB
102 | end
103 |
104 | ---@param query_string string
105 | ---@return table
106 | function DB:load(query_string)
107 | local res = self.db:eval(
108 | [[
109 | SELECT gosymbols.data FROM gosymbols
110 | JOIN gosymbols_fts ON gosymbols.id = gosymbols_fts.id
111 | WHERE gosymbols_fts.name LIKE '%' || ? || '%'
112 | ORDER BY gosymbols.last_modified DESC
113 | LIMIT 200;
114 | ]],
115 | { query_string }
116 | )
117 |
118 | if type(res) ~= "table" or #res == 0 then
119 | return {}
120 | end
121 |
122 | local ret = {}
123 | for _, row in ipairs(res) do
124 | ret[#ret + 1] = vim.json.decode(row.data)
125 | end
126 |
127 | return ret
128 | end
129 |
130 | ---@return nil
131 | function DB:prune()
132 | if self.total_rows_estimate < self.MAX_ROWS_THRESHOLD then
133 | return
134 | end
135 |
136 | local res = self.db:eval("SELECT COUNT(*) as count FROM gosymbols")
137 | if type(res) ~= "table" or #res == 0 then
138 | if self.notifications then
139 | vim.notify("[sqlite] error reading db row count while pruning", vim.log.levels.ERROR)
140 | end
141 | return
142 | end
143 | self.total_rows_estimate = res[1].count
144 |
145 | local to_delete = math.floor(self.total_rows_estimate * 0.2)
146 | to_delete = math.min(to_delete, 5000)
147 |
148 | if to_delete == 0 then
149 | return
150 | end
151 |
152 | local delete_fts = sqlstmt:parse(
153 | self.db.conn,
154 | [[
155 | DELETE FROM gosymbols_fts
156 | WHERE id IN (
157 | SELECT id FROM gosymbols ORDER BY last_modified ASC LIMIT ?
158 | );
159 | ]]
160 | )
161 |
162 | local delete_gosymbols = sqlstmt:parse(
163 | self.db.conn,
164 | [[
165 | DELETE FROM gosymbols
166 | WHERE id IN (
167 | SELECT id FROM gosymbols ORDER BY last_modified ASC LIMIT ?
168 | );
169 | ]]
170 | )
171 |
172 | delete_gosymbols:bind({ to_delete })
173 | delete_fts:bind({ to_delete })
174 |
175 | if not self.db:eval("BEGIN TRANSACTION;") then
176 | if self.notifications then
177 | vim.notify("[sqlite] failed to begin transaction", vim.log.levels.ERROR)
178 | end
179 | return
180 | end
181 |
182 | ---@param msg string
183 | local rollback = function(msg)
184 | self.db:eval("ROLLBACK;")
185 | if self.notifications then
186 | vim.notify("[sqlite] " .. msg, vim.log.levels.ERROR)
187 | end
188 | end
189 |
190 | if delete_fts:step() ~= sqlite.flags["done"] then
191 | return rollback("failed to perform deletion of gosymbols_fts")
192 | end
193 | if not delete_fts:finalize() then
194 | return rollback("failed to finalize deletion of gosymbols_fts")
195 | end
196 |
197 | if delete_gosymbols:step() ~= sqlite.flags["done"] then
198 | return rollback("failed to perform deletion of gosymbols")
199 | end
200 | if not delete_gosymbols:finalize() then
201 | return rollback("failed to finalize deletion of gosymbols")
202 | end
203 |
204 | if not self.db:eval("END TRANSACTION;") then
205 | return rollback("failed to end transaction")
206 | end
207 |
208 | self.total_rows_estimate = self.total_rows_estimate - to_delete
209 |
210 | if not self.db:eval("PRAGMA incremental_vacuum(0)") then
211 | if self.notifications then
212 | vim.notify("[sqlite] failed to perform incremental vacuum", vim.log.levels.ERROR)
213 | end
214 | end
215 | end
216 |
217 | ---@param utils cmp_go_deep.utils
218 | ---@param symbol_information table
219 | function DB:save(utils, symbol_information) --- assumes that gopls doesn't return more than 100 workspace symbols
220 | local insert_gosymbols = sqlstmt:parse(
221 | self.db.conn,
222 | [[
223 | INSERT OR REPLACE INTO gosymbols (name, data, hash, last_modified)
224 | VALUES (?, ?, ?, ?);
225 | ]]
226 | )
227 | local last_insert_rowid_stmt = sqlstmt:parse(
228 | self.db.conn,
229 | [[
230 | SELECT last_insert_rowid();
231 | ]]
232 | )
233 | local insert_gosymbols_fts = sqlstmt:parse(
234 | self.db.conn,
235 | [[
236 | INSERT OR REPLACE INTO gosymbols_fts (name, id )
237 | VALUES (?, ?);
238 | ]]
239 | )
240 |
241 | local last_modified = os.time()
242 | if not self.db:eval("BEGIN TRANSACTION;") then
243 | if self.notifications then
244 | vim.notify("[sqlite] failed to begin transaction", vim.log.levels.ERROR)
245 | end
246 | return
247 | end
248 |
249 | ---@param msg string
250 | local rollback = function(msg)
251 | self.db:eval("ROLLBACK;")
252 | if self.notifications then
253 | vim.notify("[sqlite] " .. msg, vim.log.levels.ERROR)
254 | end
255 | end
256 |
257 | for _, symbol in ipairs(symbol_information) do
258 | local encoded = vim.json.encode(symbol)
259 | local hash = utils.deterministic_symbol_hash(symbol)
260 |
261 | insert_gosymbols:bind({ symbol.name, encoded, hash, last_modified })
262 | if insert_gosymbols:step() ~= sqlite.flags["done"] then
263 | return rollback("failed to insert gosymbols row")
264 | end
265 | if insert_gosymbols:reset() ~= sqlite.flags["ok"] then
266 | return rollback("failed to reset insert_gosymbols")
267 | end
268 |
269 | if last_insert_rowid_stmt:step() ~= sqlite.flags["row"] then
270 | return rollback("failed to select id")
271 | end
272 | local id = last_insert_rowid_stmt:val(0)
273 | if last_insert_rowid_stmt:reset() ~= sqlite.flags["ok"] then
274 | return rollback("failed to reset last_insert_rowid_stmt")
275 | end
276 |
277 | insert_gosymbols_fts:bind({ symbol.name, id })
278 | if insert_gosymbols_fts:step() ~= sqlite.flags["done"] then
279 | return rollback("failed to insert gosymbols_fts row")
280 | end
281 | if insert_gosymbols_fts:reset() ~= sqlite.flags["ok"] then
282 | return rollback("failed to reset insert_gosymbols_fts")
283 | end
284 | end
285 |
286 | if not insert_gosymbols:finalize() then
287 | return rollback("failed to finalize insert_gosymbols")
288 | end
289 | if not last_insert_rowid_stmt:finalize() then
290 | return rollback("failed to finalize last_insert_rowid_stmt")
291 | end
292 | if not insert_gosymbols_fts:finalize() then
293 | return rollback("failed to finalize insert_gosymbols_fts")
294 | end
295 |
296 | if not self.db:eval("END TRANSACTION;") then
297 | return rollback("failed to end transaction")
298 | end
299 |
300 | self.total_rows_estimate = self.total_rows_estimate + #symbol_information
301 |
302 | vim.schedule(function()
303 | self:prune()
304 | end)
305 | end
306 |
307 | return DB
308 |
--------------------------------------------------------------------------------
/lua/cmp_go_deep/gopls_requests.lua:
--------------------------------------------------------------------------------
1 | ---@class cmp_go_deep.gopls_requests
2 | ---@field get_documentation fun(opts: cmp_go_deep.Options, gopls_client: vim.lsp.Client | nil, uri: string, range: lsp.Range): string|nil
3 | ---@field debounced_workspace_symbols fun(opts:cmp_go_deep.Options, gopls_client: vim.lsp.Client, bufnr: integer, cursor_prefix_word: string, callback: fun(items: lsp.SymbolInformation[]): nil): nil
4 | ---@field public workspace_symbols fun(opts:cmp_go_deep.Options, gopls_client: vim.lsp.Client, bufnr: integer, cursor_prefix_word: string, callback: fun(items: lsp.SymbolInformation[]): nil): nil
5 | local gopls_requests = {}
6 |
7 | ---@param opts cmp_go_deep.Options
8 | ---@param gopls_client vim.lsp.Client | nil
9 | ---@param uri string
10 | ---@param range lsp.Range
11 | ---@return string | nil
12 | ---TODO: try completionItem/resolve instead
13 | gopls_requests.get_documentation = function(opts, gopls_client, uri, range)
14 | if gopls_client == nil then
15 | if opts.notifications then
16 | vim.notify("gopls client is nil", vim.log.levels.WARN)
17 | end
18 | return nil
19 | end
20 |
21 | local params = {
22 | textDocument = { uri = uri },
23 | position = range.start,
24 | }
25 |
26 | local markdown = ""
27 |
28 | gopls_client:request("textDocument/hover", params, function(err, result)
29 | if err then
30 | if opts.notifications then
31 | vim.notify("failed to get documentation: " .. err.message, vim.log.levels.WARN)
32 | end
33 | return
34 | end
35 |
36 | if result and result.contents then
37 | markdown = result.contents.value
38 | end
39 | end)
40 |
41 | vim.wait(opts.documentation_wait_timeout_ms, function()
42 | return markdown ~= ""
43 | end, 10)
44 |
45 | if markdown == "" and opts.notifications then
46 | if opts.notifications then
47 | vim.notify("timed out waiting for documentation", vim.log.levels.WARN)
48 | end
49 | end
50 |
51 | return markdown
52 | end
53 |
54 | ---@param opts cmp_go_deep.Options
55 | ---@param gopls_client vim.lsp.Client
56 | ---@param bufnr integer
57 | ---@param cursor_prefix_word string
58 | ---@param callback fun(items: lsp.SymbolInformation[]): nil
59 | -- stylua: ignore
60 | gopls_requests.workspace_symbols = function(opts, gopls_client, bufnr, cursor_prefix_word, callback)
61 | local success, _ = gopls_client:request("workspace/symbol", { query = cursor_prefix_word }, function(_, result)
62 | if not result then
63 | return
64 | end
65 | callback(result)
66 | end, bufnr)
67 |
68 | if not success then
69 | if opts.notifications then
70 | vim.notify("failed to get workspace symbols", vim.log.levels.WARN)
71 | end
72 | return
73 | end
74 | end
75 |
76 | return gopls_requests
77 |
--------------------------------------------------------------------------------
/lua/cmp_go_deep/init.lua:
--------------------------------------------------------------------------------
1 | local utils = require("cmp_go_deep.utils")
2 | local gopls_requests = require("cmp_go_deep.gopls_requests")
3 |
4 | ---@class cmp_go_deep.Options
5 | ---@field public notifications boolean | nil -- whether to show notifications. default: true
6 | ---@field public filetypes string[] | nil -- filetypes to enable the source for
7 | ---@field public get_documentation_implementation "hover" | "regex" | nil -- how to get documentation. default: "regex"
8 | ---@field public get_package_name_implementation "treesitter" | "regex" | nil -- how to get package name (treesitter = slow but accurate | regex = fast but fails edge cases). default: "regex"
9 | ---@field public exclude_vendored_packages boolean | nil -- whether to exclude vendored packages. default: false
10 | ---@field public documentation_wait_timeout_ms integer | nil -- maximum time (in milliseconds) to wait for fetching documentation. default: 100
11 | ---@field public debounce_gopls_requests_ms integer | nil -- time to wait before "locking-in" the current request and sending it to gopls. default: 350
12 | ---@field public debounce_cache_requests_ms integer | nil -- time to wait before "locking-in" the current request and loading data from cache. default: 50
13 | ---@field public db_path string | nil -- where to store the sqlite db. default: ~/.local/share/nvim/cmp_go_deep.sqlite3
14 | ---@field public db_size_limit_bytes number | nil -- max db size in bytes. default: 200MB
15 | ---@field public debug boolean | nil -- whether to enable debug logging. default: false
16 |
17 | ---@type cmp_go_deep.Options
18 | local default_options = {
19 | notifications = true,
20 | filetypes = { "go" },
21 | get_documentation_implementation = "regex",
22 | get_package_name_implementation = "regex",
23 | exclude_vendored_packages = false,
24 | documentation_wait_timeout_ms = 100,
25 | debounce_gopls_requests_ms = 250,
26 | debounce_cache_requests_ms = 50,
27 | db_path = vim.fn.stdpath("data") .. "/cmp_go_deep.sqlite3",
28 | db_size_limit_bytes = 200 * 1024 * 1024,
29 | debug = false,
30 | }
31 |
32 | local source = {}
33 |
34 | source.new = function()
35 | return setmetatable({}, { __index = source })
36 | end
37 |
38 | source.is_available = function()
39 | if utils.get_gopls_client() == nil then
40 | return false
41 | end
42 | return true
43 | end
44 |
45 | source.get_trigger_characters = function()
46 | return { "[%w_]" }
47 | end
48 |
49 | source.complete = function(_, params, callback)
50 | local gopls_client = utils.get_gopls_client()
51 | if not gopls_client then
52 | return callback({ items = {}, isIncomplete = false })
53 | end
54 |
55 | ---@type cmp_go_deep.Options
56 | source.opts = vim.tbl_deep_extend("force", default_options, params.option or params.opts or {})
57 |
58 | local allowed_filetype = false
59 | for _, key in pairs(source.opts.filetypes) do
60 | if vim.bo.filetype == key then
61 | allowed_filetype = true
62 | break
63 | end
64 | end
65 | if not allowed_filetype then
66 | return callback({ items = {}, isIncomplete = false })
67 | end
68 |
69 | local cursor_prefix_word = utils.get_cursor_prefix_word(0)
70 | if cursor_prefix_word:match("[%.]") or cursor_prefix_word:match("[^%w_]") then
71 | return callback({ items = {}, isIncomplete = false })
72 | end
73 |
74 | if not source.cache then
75 | source.cache = require("cmp_go_deep.db").setup(source.opts)
76 | end
77 |
78 | if not gopls_requests.debounced_workspace_symbols then
79 | gopls_requests.debounced_workspace_symbols =
80 | utils.debounce(gopls_requests.workspace_symbols, source.opts.debounce_gopls_requests_ms)
81 | end
82 |
83 | if not utils.debounced_process_symbols then
84 | utils.debounced_process_symbols = utils.debounce(utils.process_symbols, source.opts.debounce_cache_requests_ms)
85 | end
86 |
87 | ---@type table
88 | local processed_items = {}
89 |
90 | local bufnr = vim.api.nvim_get_current_buf()
91 | local project_path = vim.fn.getcwd()
92 | local vendor_path_prefix = "file://" .. project_path .. "/vendor/"
93 | local project_path_prefix = "file://" .. project_path .. "/"
94 |
95 | utils:debounced_process_symbols(
96 | source.opts,
97 | bufnr,
98 | callback,
99 | vendor_path_prefix,
100 | project_path_prefix,
101 | source.cache:load(cursor_prefix_word),
102 | processed_items,
103 | true
104 | )
105 |
106 | gopls_requests.debounced_workspace_symbols(source.opts, gopls_client, bufnr, cursor_prefix_word, function(result)
107 | if not result or #result == 0 then
108 | return callback({ items = {}, isIncomplete = false })
109 | end
110 |
111 | local filtered_result = {}
112 | for _, symbol in ipairs(result) do
113 | if
114 | utils.symbol_to_completion_kind(symbol.kind)
115 | and symbol.name:match("^[A-Z]")
116 | and not symbol.location.uri:match("_test%.go$")
117 | and (#cursor_prefix_word > 2 or symbol.name:find(cursor_prefix_word, 1, true))
118 | then
119 | if string.sub(symbol.location.uri, 1, #vendor_path_prefix) == vendor_path_prefix then
120 | symbol.isVendored = true
121 | symbol.location.uri = symbol.location.uri:sub(#vendor_path_prefix + 1)
122 | elseif string.sub(symbol.location.uri, 1, #project_path_prefix) == project_path_prefix then
123 | symbol.isLocal = true
124 | symbol.location.uri = symbol.location.uri:sub(#project_path_prefix + 1)
125 | end
126 | table.insert(filtered_result, symbol)
127 | end
128 | end
129 |
130 | source.cache:save(utils, filtered_result)
131 |
132 | local toProcess = filtered_result
133 | if #cursor_prefix_word > 2 then
134 | toProcess = source.cache:load(cursor_prefix_word)
135 | end
136 |
137 | utils:debounced_process_symbols(
138 | source.opts,
139 | bufnr,
140 | callback,
141 | vendor_path_prefix,
142 | project_path_prefix,
143 | toProcess,
144 | processed_items,
145 | true
146 | )
147 | end)
148 | end
149 |
150 | ---@param completion_item lsp.CompletionItem
151 | ---@param callback fun(completion_item: lsp.CompletionItem|nil)
152 | function source:resolve(completion_item, callback)
153 | local symbol = completion_item.data
154 |
155 | if symbol == nil then
156 | return callback(nil)
157 | end
158 |
159 | ---@type cmp_go_deep.Options|nil
160 | local opts = symbol.opts
161 | if not opts then
162 | vim.notify("Warning: symbol data is missing options", vim.log.levels.WARN)
163 | return callback(completion_item)
164 | end
165 |
166 | if not type(symbol.location) == "table" then
167 | if opts.notifications then
168 | vim.notify("Warning: symbol location is missing", vim.log.levels.WARN)
169 | end
170 | return callback(completion_item)
171 | end
172 |
173 | ---@type lsp.Location
174 | local location = symbol.location
175 | if not location or not location.uri or not location.range then
176 | if opts.notifications then
177 | vim.notify("Warning: symbol location is missing", vim.log.levels.WARN)
178 | end
179 | return callback(completion_item)
180 | end
181 |
182 | vim.schedule(function()
183 | ---@type string|nil
184 | local documentation = utils.get_documentation(opts, location.uri, location.range)
185 | completion_item.documentation = {
186 | kind = "markdown",
187 | value = documentation or "",
188 | }
189 | callback(completion_item)
190 | end)
191 | end
192 |
193 | ---@param completion_item lsp.CompletionItem
194 | ---@param callback fun(completion_item: lsp.CompletionItem|nil)
195 | function source:execute(completion_item, callback)
196 | if vim.bo.filetype ~= "go" then
197 | return
198 | end
199 |
200 | local symbol = completion_item.data
201 | if not symbol then
202 | return
203 | end
204 |
205 | ---@type cmp_go_deep.Options|nil
206 | local opts = symbol.opts
207 | if not opts then
208 | return
209 | end
210 |
211 | local import_path = symbol.containerName
212 | local package_name = symbol.package_alias
213 |
214 | if not import_path then
215 | if opts.notifications then
216 | vim.notify("import path not found", vim.log.levels.WARN)
217 | end
218 | return
219 | end
220 |
221 | utils.add_import_statement(opts, symbol.bufnr, package_name, import_path)
222 | callback(completion_item)
223 | end
224 |
225 | return source
226 |
--------------------------------------------------------------------------------
/lua/cmp_go_deep/test.lua:
--------------------------------------------------------------------------------
1 | --TODO: add this to ci
2 |
3 | local DB = require("cmp_go_deep.db")
4 | local utils = require("cmp_go_deep.utils")
5 | local math = math
6 | local os = os
7 |
8 | -- Random string generator
9 | local function random_string(length)
10 | local chars = "abcdefghijklmnopqrstuvwxyz"
11 | local result = {}
12 | for _ = 1, length do
13 | local index = math.random(1, #chars)
14 | table.insert(result, chars:sub(index, index))
15 | end
16 | return table.concat(result)
17 | end
18 |
19 | local function generate_fake_symbols(n)
20 | local symbols = {}
21 | for i = 1, n do
22 | local name = "Sym_" .. random_string(math.random(3, 60)) .. tostring(i)
23 | local symbol = {
24 | name = name,
25 | kind = math.random(1, 25), -- LSP SymbolKind (1–25)
26 | containerName = "pkg" .. tostring(i % 100),
27 | location = {
28 | uri = "file:///fake/path/file" .. tostring(i % 500) .. ".go",
29 | range = {
30 | start = { line = 0, character = 0 },
31 | ["end"] = { line = 0, character = 0 },
32 | },
33 | },
34 | }
35 | table.insert(symbols, symbol)
36 | end
37 | return symbols
38 | end
39 |
40 | -- Main population script
41 | local function populate()
42 | math.randomseed(os.time())
43 |
44 | local opts = {
45 | db_path = vim.fn.stdpath("data") .. "/cmp_go_deep.sqlite3",
46 | db_size_limit_bytes = 1024 * 1024 * 1024,
47 | }
48 | local db = DB.setup(opts)
49 |
50 | local total = 100000
51 | for _ = 1, total / 100 do
52 | local total = 100
53 | local data = generate_fake_symbols(total)
54 | db:save(utils, data)
55 | end
56 | vim.notify("[populate] Done! Inserted " .. total .. " symbols.", vim.log.levels.INFO)
57 | end
58 |
59 | populate()
60 |
--------------------------------------------------------------------------------
/lua/cmp_go_deep/treesitter_implementations.lua:
--------------------------------------------------------------------------------
1 | local treesitter_implementations = {}
2 |
3 | ---@param bufnr (integer)
4 | ---@return TSNode | nil
5 | local function get_root_node(bufnr)
6 | if bufnr == nil then
7 | return nil
8 | end
9 |
10 | if not vim.api.nvim_buf_is_loaded(bufnr) then
11 | vim.fn.bufload(bufnr)
12 | end
13 |
14 | local parser = vim.treesitter.get_parser(bufnr, "go")
15 | if parser == nil then
16 | return nil
17 | end
18 | local root = nil
19 | parser = parser:parse()
20 | if parser ~= nil then
21 | root = parser[1]:root()
22 | end
23 | return root
24 | end
25 |
26 | ---@param opts cmp_go_deep.Options
27 | ---@param bufnr (integer)
28 | ---@param package_alias string | nil
29 | ---@param import_path string
30 | treesitter_implementations.add_import_statement = function(opts, bufnr, package_alias, import_path)
31 | local root = get_root_node(bufnr)
32 | if root == nil then
33 | return
34 | end
35 |
36 | if not package_alias then
37 | package_alias = ""
38 | else
39 | package_alias = package_alias .. " "
40 | end
41 |
42 | ---@type TSNode | nil
43 | local import_node = nil
44 | for node in root:iter_children() do
45 | if node:type() == "import_declaration" then
46 | import_node = node
47 | break
48 | end
49 | end
50 |
51 | if import_node then
52 | local start_row, _, end_row, _ = import_node:range()
53 | if import_node:named_child_count() == 1 then
54 | local child = import_node:named_child(0)
55 | if not child then
56 | if opts.notifications then
57 | vim.notify("could not parse import line with treesitter", vim.log.levels.WARN)
58 | end
59 | return
60 | end
61 |
62 | local type = child:type()
63 | if type == "interpreted_string_literal" or type == "raw_string_literal" or type == "import_spec" then
64 | vim.api.nvim_buf_set_lines(bufnr, start_row, end_row + 1, false, {
65 | "import (",
66 | "\t" .. vim.treesitter.get_node_text(child, bufnr),
67 | "\t" .. package_alias .. '"' .. import_path .. '"',
68 | ")",
69 | })
70 | return
71 | end
72 | end
73 |
74 | local lines = vim.api.nvim_buf_get_lines(bufnr, start_row, end_row + 1, false)
75 | if not lines[#lines]:match("^%s*%)") then
76 | if opts.notifications then
77 | vim.notify("could not parse import block with treesitter", vim.log.levels.WARN)
78 | end
79 | return
80 | end
81 |
82 | table.insert(lines, #lines, "\t" .. package_alias .. '"' .. import_path .. '"')
83 | vim.api.nvim_buf_set_lines(bufnr, start_row, end_row + 1, false, lines)
84 | else
85 | local insert_line = 0
86 | for i, line in ipairs(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) do
87 | if line:match("^package%s+") then
88 | insert_line = i
89 | break
90 | end
91 | end
92 |
93 | vim.api.nvim_buf_set_lines(bufnr, insert_line, insert_line, false, {
94 | "",
95 | "import (",
96 | "\t" .. package_alias .. '"' .. import_path .. '"',
97 | ")",
98 | "",
99 | })
100 | end
101 | end
102 |
103 | ---@param str string
104 | ---@return string
105 | ---@return integer count
106 | local function trim_quotes(str)
107 | return str:gsub('^["`]+', ""):gsub('["`]+$', "")
108 | end
109 |
110 | ---@param opts cmp_go_deep.Options
111 | ---@param bufnr (integer)
112 | ---@return table -- key: import path, value: package alias
113 | treesitter_implementations.get_imported_paths = function(opts, bufnr)
114 | local root = get_root_node(bufnr)
115 | if root == nil then
116 | return {}
117 | end
118 |
119 | ---@type TSNode | nil
120 | local import_node = nil
121 | for i = 0, root:named_child_count() - 1 do
122 | local node = root:named_child(i)
123 | if node and node:type() == "import_declaration" then
124 | import_node = node
125 | break
126 | end
127 | end
128 | if import_node == nil then
129 | return {}
130 | end
131 |
132 | local imported_paths = {}
133 | ---@param spec TSNode?
134 | local process_import_spec = function(spec)
135 | local path_node = spec and spec:field("path")[1]
136 | local name_node = spec and spec:field("name")[1]
137 | if path_node then
138 | local text = vim.treesitter.get_node_text(path_node, bufnr)
139 | if text then
140 | text = trim_quotes(text)
141 | local package_alias = name_node and vim.treesitter.get_node_text(name_node, bufnr)
142 | package_alias = package_alias or text:match("([^/]+)$")
143 | if package_alias == nil and opts.notifications then
144 | vim.notify("could not parse import line with treesitter " .. text, vim.log.levels.WARN)
145 | end
146 | imported_paths[text] = package_alias
147 | end
148 | end
149 | end
150 |
151 | for j = 0, import_node:named_child_count() - 1 do
152 | local child = import_node:named_child(j)
153 | if not child then
154 | goto continue
155 | end
156 |
157 | local type = child:type()
158 | if type == "import_spec" and child:named_child_count() > 0 then -- single line import
159 | process_import_spec(child:named_child(0))
160 | goto continue
161 | end
162 |
163 | if type == "import_spec_list" then -- multiline import
164 | for k = 0, child:child_count() - 1 do
165 | process_import_spec(child:named_child(k))
166 | end
167 | break
168 | end
169 |
170 | ::continue::
171 | end
172 |
173 | return imported_paths
174 | end
175 |
176 | ---@param uri string
177 | ---@return string|nil
178 | treesitter_implementations.get_package_name = function(uri)
179 | local filepath = vim.uri_to_fname(uri)
180 | local bufnr = vim.fn.bufadd(filepath)
181 | local root = get_root_node(bufnr)
182 | if not root then
183 | return nil
184 | end
185 |
186 | for node in root:iter_children() do
187 | if node:type() == "package_clause" then
188 | for child in node:iter_children() do
189 | if child:type() == "package_identifier" then
190 | return vim.treesitter.get_node_text(child, bufnr)
191 | end
192 | end
193 | end
194 | end
195 |
196 | return nil
197 | end
198 |
199 | return treesitter_implementations
200 |
--------------------------------------------------------------------------------
/lua/cmp_go_deep/utils.lua:
--------------------------------------------------------------------------------
1 | local gopls_requests = require("cmp_go_deep.gopls_requests")
2 | local treesitter_implementations = require("cmp_go_deep.treesitter_implementations")
3 | local completionItemKind = vim.lsp.protocol.CompletionItemKind
4 |
5 | ---@class cmp_go_deep.utils
6 | ---@field debounce fun(fn: fun(...), delay_ms: integer): fun(...)
7 | ---@field symbol_to_completion_kind fun(lspKind: lsp.SymbolKind): integer
8 | ---@field get_cursor_prefix_word fun(win_id: integer): string
9 | ---@field get_unique_package_alias fun(used_aliases: table, package_alias: string): string
10 | ---@field get_gopls_client fun(): vim.lsp.Client|nil
11 | ---@field get_documentation fun(opts: cmp_go_deep.Options, uri: string, range: lsp.Range): string|nil
12 | ---@field get_imported_paths fun(opts: cmp_go_deep.Options, bufnr: integer): tablebufnr: integer): table
13 | ---@field add_import_statement fun(opts: cmp_go_deep.Options, bufnr: integer, package_name: string | nil, import_path: string): nil
14 | ---@field get_package_name fun(opts: cmp_go_deep.Options, uri: string, package_name_cache: table): string|nil, boolean
15 | ---@field deterministic_symbol_hash fun(symbol: lsp.SymbolInformation): string
16 | ---@field process_symbols fun(self, opts: cmp_go_deep.Options, bufnr: integer, callback: any, vendor_prefix: string, project_path_prefix: string, symbols: table, processed_items: table, isIncomplete: boolean): nil
17 | ---@field debounced_process_symbols fun(self, opts: cmp_go_deep.Options, bufnr: integer, callback: any, vendor_prefix: string, project_path_prefix: string, symbols: table, processed_items: table, isIncomplete: boolean): nil
18 | local utils = {}
19 |
20 | local symbol_to_completion_kind = {
21 | [10] = completionItemKind.Enum,
22 | [11] = completionItemKind.Interface,
23 | [12] = completionItemKind.Function,
24 | [13] = completionItemKind.Variable,
25 | [14] = completionItemKind.Constant,
26 | [23] = completionItemKind.Struct,
27 | [26] = completionItemKind.TypeParameter,
28 | }
29 |
30 | ---@param fn fun( ...)
31 | ---@param delay_ms integer
32 | ---@return fun(...)
33 | utils.debounce = function(fn, delay_ms)
34 | local timer = vim.uv.new_timer()
35 |
36 | return function(...)
37 | timer:stop()
38 | local args = { ... }
39 | timer:start(delay_ms, 0, function()
40 | vim.schedule(function()
41 | local cur_args = args
42 | fn(unpack(cur_args))
43 | end)
44 | end)
45 | end
46 | end
47 |
48 | ---@param lspKind lsp.SymbolKind
49 | ---@return integer
50 | utils.symbol_to_completion_kind = function(lspKind)
51 | return symbol_to_completion_kind[lspKind]
52 | end
53 |
54 | ---@param win_id integer
55 | ---@return string
56 | utils.get_cursor_prefix_word = function(win_id)
57 | local pos = vim.api.nvim_win_get_cursor(win_id)
58 | if #pos < 2 then
59 | return ""
60 | end
61 |
62 | local col = pos[2]
63 | local start_col = col
64 | local end_col = col
65 |
66 | local line = vim.api.nvim_get_current_line()
67 |
68 | while start_col > 0 and not line:sub(start_col - 1, start_col - 1):match("%s") do
69 | start_col = start_col - 1
70 | end
71 |
72 | return line:sub(start_col, end_col)
73 | end
74 |
75 | ---@return vim.lsp.Client | nil
76 | utils.get_gopls_client = function()
77 | local gopls_clients = vim.lsp.get_clients({ name = "gopls" })
78 | if #gopls_clients > 0 then
79 | return gopls_clients[1]
80 | end
81 | return nil
82 | end
83 |
84 | ---@param opts cmp_go_deep.Options
85 | ---@param uri string
86 | ---@param range lsp.Range
87 | ---@return string | nil
88 | utils.get_documentation = function(opts, uri, range)
89 | if opts.get_documentation_implementation == "hover" then
90 | return gopls_requests.get_documentation(opts, utils.get_gopls_client(), uri, range)
91 | end
92 |
93 | --default to regex
94 | local filepath = vim.uri_to_fname(uri)
95 | local bufnr = vim.fn.bufadd(filepath)
96 | vim.fn.bufload(bufnr)
97 |
98 | local doc_lines = {}
99 | local start_line = range.start.line
100 |
101 | for i = start_line - 1, 0, -1 do
102 | local line = vim.api.nvim_buf_get_lines(bufnr, i, i + 1, false)[1]
103 | if not line or line:match("^%s*$") then
104 | break
105 | end
106 |
107 | local comment = line:match("^%s*//(.*)")
108 | if comment then
109 | table.insert(doc_lines, 1, vim.trim(comment))
110 | else
111 | break
112 | end
113 | end
114 |
115 | if vim.tbl_isempty(doc_lines) then
116 | return nil
117 | end
118 |
119 | local ft = vim.bo[bufnr].filetype
120 | return string.format("```%s\n%s\n```", ft, table.concat(doc_lines, "\n"))
121 | end
122 |
123 | ---@param opts cmp_go_deep.Options
124 | ---@param bufnr (integer)
125 | ---@return table
126 | utils.get_imported_paths = function(opts, bufnr)
127 | return treesitter_implementations.get_imported_paths(opts, bufnr)
128 | end
129 |
130 | ---@param opts cmp_go_deep.Options
131 | ---@param bufnr (integer)
132 | ---@param package_alias string | nil
133 | ---@param import_path string
134 | utils.add_import_statement = function(opts, bufnr, package_alias, import_path)
135 | treesitter_implementations.add_import_statement(opts, bufnr, package_alias, import_path)
136 | end
137 |
138 | ---@param used_aliases table
139 | ---@param package_alias string
140 | ---@return string
141 | utils.get_unique_package_alias = function(used_aliases, package_alias)
142 | local alias = package_alias
143 | local i = 2
144 | while used_aliases[alias] do
145 | alias = package_alias .. i
146 | i = i + 1
147 | end
148 | return alias
149 | end
150 |
151 | ---@param opts cmp_go_deep.Options
152 | ---@param uri string
153 | ---@param package_name_cache table
154 | ---@return string|nil, boolean
155 | --- TODO: consider asking gopls for the package name, but this is probably faster
156 | utils.get_package_name = function(opts, uri, package_name_cache)
157 | local cached = package_name_cache[uri]
158 | if cached then
159 | if cached == "" then
160 | return nil, true
161 | end
162 | return cached, true
163 | end
164 |
165 | local stat = vim.uv.fs_stat(vim.uri_to_fname(uri))
166 | if not (stat and stat.type == "file") then
167 | return nil, false
168 | end
169 |
170 | if opts.get_package_name_implementation == "treesitter" then
171 | local pkg = treesitter_implementations.get_package_name(uri)
172 | if pkg then
173 | package_name_cache[uri] = pkg
174 | return pkg, true
175 | end
176 | package_name_cache[uri] = ""
177 | return "", true
178 | end
179 |
180 | --default to regex
181 | --- FIXME: regex implementation doesn't work for package declarations like: "/* hehe */ package xd"
182 | local fname = vim.uri_to_fname(uri)
183 | if not fname then
184 | if opts.notifications then
185 | vim.notify("could not get file name from uri: " .. uri, vim.log.levels.WARN)
186 | end
187 | package_name_cache[uri] = ""
188 | return nil, true
189 | end
190 |
191 | local file, err = io.open(fname, "r")
192 | if not file then
193 | if opts.notifications then
194 | vim.notify("could not open file: " .. err, vim.log.levels.WARN)
195 | end
196 | package_name_cache[uri] = ""
197 | return nil, true
198 | end
199 |
200 | local in_block = false
201 | for line in file:lines() do
202 | local ln = line:match("^%s*(.-)%s*$")
203 | if not in_block and ln:find("^/%*") then
204 | in_block = true
205 | if ln:find("%*/") then
206 | in_block = false
207 | end
208 | elseif in_block then
209 | if ln:find("%*/") then
210 | in_block = false
211 | end
212 | elseif ln == "" or ln:find("^//") then
213 | -- ignore
214 | else
215 | local pkg = ln:match("^package%s+([%a_][%w_]*)")
216 | file:close()
217 | package_name_cache[uri] = pkg
218 | return pkg, true
219 | end
220 | end
221 |
222 | file:close()
223 | package_name_cache[uri] = ""
224 | return nil, true
225 | end
226 |
227 | ---@param symbol lsp.SymbolInformation
228 | ---@return string
229 | utils.deterministic_symbol_hash = function(symbol)
230 | local ordered = symbol.name
231 | .. " #"
232 | .. symbol.kind
233 | .. " #"
234 | .. symbol.containerName
235 | .. " #"
236 | .. string.format(
237 | "%d-%d,%d-%d",
238 | symbol.location.range.start.character,
239 | symbol.location.range.start.line,
240 | symbol.location.range["end"].character,
241 | symbol.location.range["end"].line
242 | )
243 | return vim.fn.sha256(ordered)
244 | end
245 |
246 | ---@param opts cmp_go_deep.Options
247 | ---@param bufnr integer
248 | ---@param callback any
249 | ---@param vendor_path_prefix string
250 | ---@param project_path_prefix string
251 | ---@param symbols table
252 | ---@param processed_items table
253 | ---@param isIncomplete boolean
254 | function utils:process_symbols(
255 | opts,
256 | bufnr,
257 | callback,
258 | vendor_path_prefix,
259 | project_path_prefix,
260 | symbols,
261 | processed_items,
262 | isIncomplete
263 | )
264 | local items = {}
265 | local package_name_cache = {}
266 | local imported_paths = utils.get_imported_paths(opts, bufnr)
267 |
268 | ---@type table
269 | local used_aliases = {}
270 | for _, v in pairs(imported_paths) do
271 | used_aliases[v] = true
272 | end
273 |
274 | local current_buf_uri = vim.uri_from_bufnr(bufnr)
275 | local current_buf_dir = vim.fn.fnamemodify(current_buf_uri, ":h")
276 |
277 | ---TODO: better type checking and error handling
278 | for _, symbol in ipairs(symbols) do
279 | local kind = utils.symbol_to_completion_kind(symbol.kind)
280 | local hash = self.deterministic_symbol_hash(symbol)
281 | if processed_items[hash] then
282 | goto continue
283 | end
284 |
285 | processed_items[hash] = true
286 |
287 | if symbol.isVendored then
288 | symbol.location.uri = vendor_path_prefix .. symbol.location.uri
289 | elseif symbol.isLocal then
290 | symbol.location.uri = project_path_prefix .. symbol.location.uri
291 | end
292 |
293 | local symbol_dir = vim.fn.fnamemodify(symbol.location.uri, ":h")
294 |
295 | if
296 | kind
297 | and not imported_paths[symbol.containerName]
298 | and symbol.location.uri ~= current_buf_uri
299 | and symbol_dir ~= current_buf_dir
300 | and not (opts.exclude_vendored_packages and symbol.isVendored)
301 | then
302 | local package_name, file_exists = utils.get_package_name(opts, symbol.location.uri, package_name_cache)
303 | if not file_exists then
304 | goto continue
305 | end
306 |
307 | if package_name == nil then
308 | package_name = symbol.containerName:match("([^/]+)$"):gsub("-", "_")
309 | end
310 | if not package_name then
311 | goto continue
312 | end
313 |
314 | local package_alias = utils.get_unique_package_alias(used_aliases, package_name)
315 | if package_alias ~= package_name then
316 | symbol.package_alias = package_alias
317 | end
318 |
319 | symbol.bufnr = bufnr
320 | symbol.opts = opts
321 |
322 | table.insert(items, {
323 | label = package_alias .. "." .. symbol.name,
324 | sortText = symbol.name,
325 | kind = kind,
326 | detail = '"' .. symbol.containerName .. '"',
327 | data = symbol,
328 | })
329 | end
330 |
331 | ::continue::
332 | end
333 |
334 | return callback({ items = items, isIncomplete = isIncomplete })
335 | end
336 |
337 | return utils
338 |
--------------------------------------------------------------------------------
/plugin/cmp_go_deep.lua:
--------------------------------------------------------------------------------
1 | require("cmp").register_source("go_deep", require("cmp_go_deep").new())
2 |
--------------------------------------------------------------------------------