diff --git a/README.md b/README.md index 454f68c..2655f3e 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,16 @@ Cached treesitter navigation on a big projects, an attempt to make navigation in ## Description _hopcsharp_ is a lightweight code navigation tool inspired by [ctags](https://github.com/universal-ctags/ctags), built -for large C# projects. It uses [tree-sitter](https://tree-sitter.github.io/tree-sitter/) to quickly (not blazing fast -but still good) parse code and store marks in a SQLite database for fast access, after that you can navigate freely in -code base using built in methods or writing queries on your own against sqlite database. +for large C# projects. It uses [tree-sitter](https://tree-sitter.github.io/tree-sitter/) to quickly parse code and +store marks in a SQLite database for fast access, after that you can navigate freely in +code base using built in methods or writing queries on your own against sqlite database. Important to understand that this is +not an _LSP server_ this tool is intended for code navigation and read. It won't be as precise as full blown _LSP server_ but +it won't require you to compile code as well :) so basically any c# codebase can be opened and parsed with this plugin. -__This plugin is in its early stages__ expect lots of bugs :D, I hope that there will be people's interest and +I hope that there will be people's interest and contributions as well. I'll try to improve it little by little. -

- -

+[![bvT1Q.gif](https://s12.gifyu.com/images/bvT1Q.gif)](https://gifyu.com/image/bvT1Q) ## How does it work? @@ -39,6 +39,31 @@ Using [packer.nvim](https://github.com/wbthomason/packer.nvim): use({ 'leblocks/hopcsharp.nvim', requires = { { 'kkharji/sqlite.lua' } } }) ``` +## Quick Start + +Example keybinding configuration: + +```lua +local hopcsharp = require('hopcsharp') + +-- database +vim.keymap.set('n', 'hD', hopcsharp.init_database, { desc = 'hopcsharp: init database' }) + +-- navigation +vim.keymap.set('n', 'hd', hopcsharp.hop_to_definition, { desc = 'hopcsharp: go to definition' }) +vim.keymap.set('n', 'hi', hopcsharp.hop_to_implementation, { desc = 'hopcsharp: go to implementation' }) +vim.keymap.set('n', 'hr', hopcsharp.hop_to_reference, { desc = 'hopcsharp: go to reference' }) +vim.keymap.set('n', 'ht', hopcsharp.get_type_hierarchy, { desc = 'hopcsharp: type hierarchy' }) + +-- fzf pickers (requires fzf-lua) +local pickers = require('hopcsharp.pickers.fzf') +vim.keymap.set('n', 'hf', pickers.source_files, { desc = 'hopcsharp: find source files' }) +vim.keymap.set('n', 'ha', pickers.all_definitions, { desc = 'hopcsharp: all definitions' }) +vim.keymap.set('n', 'hc', pickers.class_definitions, { desc = 'hopcsharp: class definitions' }) +vim.keymap.set('n', 'hn', pickers.interface_definitions, { desc = 'hopcsharp: interface definitions' }) +vim.keymap.set('n', 'he', pickers.enum_definitions, { desc = 'hopcsharp: enum definitions' }) +``` + ## API This plugin exposes only a small set of functions, allowing you to build various interfaces and workflows on top of them. @@ -91,56 +116,25 @@ Opens read-only buffer with type hierarchy. require('hopcsharp').get_db() ``` -Returns opened _[sqlite_db](https://github.com/kkharji/sqlite.lua/blob/50092d60feb242602d7578398c6eb53b4a8ffe7b/doc/sqlite.txt#L76)_ object, you can create custom flows querying it with SQL queries from lua. See customization - -## Example customizations +Returns opened _[sqlite_db](https://github.com/kkharji/sqlite.lua/blob/50092d60feb242602d7578398c6eb53b4a8ffe7b/doc/sqlite.txt#L76)_ object, you can create custom flows querying it with SQL queries from lua. See `:h hopcsharp.get_db` for more details. -[Here](https://github.com/leblocks/dotfiles/blob/master/packages/neovim/config/lua/plugins/hopcsharp.lua) (this is my -configuration that I use day to day) you can take a look at example configuration based on _get_db()_ method and _[fzf-lua](https://github.com/ibhagwan/fzf-lua)_, -here is demo usage of it on a [net framework reference source](https://github.com/microsoft/referencesource) repository +## FZF Pickers -

- -

+hopcsharp ships with built-in [fzf-lua](https://github.com/ibhagwan/fzf-lua) pickers for browsing definitions and source files. Requires the `fzf-lua` plugin to be installed. -Create repository _.cs_ files fzf-lua picker that were previously stored in a db: +All pickers are available via `require('hopcsharp.pickers.fzf')`: -

- -

+| Picker | Description | +|---|---| +| `source_files` | Browse all `.cs` source files in the database | +| `all_definitions` | Browse all definitions | +| `class_definitions` | Browse class definitions | +| `interface_definitions` | Browse interface definitions | +| `method_definitions` | Browse method definitions | +| `struct_definitions` | Browse struct definitions | +| `enum_definitions` | Browse enum definitions | +| `record_definitions` | Browse record definitions | +| `attribute_definitions` | Browse attribute definitions | -```lua -local list_files = function() - -- get database (connection is always opened) - fzf_lua.fzf_exec(function(fzf_cb) - coroutine.wrap(function() - local db = hopcsharp.get_db() - local co = coroutine.running() - local items = db:eval([[ SELECT path FROM files ]]) - - if type(items) ~= 'table' then - items = {} - end - - for _, entry in pairs(items) do - fzf_cb(entry.path, function() coroutine.resume(co) end) - coroutine.yield() - end - fzf_cb() - end)() - end, { - actions = { - ["enter"] = actions.file_edit_or_qf, - ["ctrl-s"] = actions.file_split, - ["ctrl-v"] = actions.file_vsplit, - ["ctrl-t"] = actions.file_tabedit, - ["alt-q"] = actions.file_sel_to_qf, - ["alt-Q"] = actions.file_sel_to_ll, - ["alt-i"] = actions.toggle_ignore, - ["alt-h"] = actions.toggle_hidden, - ["alt-f"] = actions.toggle_follow, - } - }) -end -``` +See `:h hopcsharp-fzf-pickers` for more details. diff --git a/doc/hopcsharp.txt b/doc/hopcsharp.txt index 32d33b9..9e1de19 100644 --- a/doc/hopcsharp.txt +++ b/doc/hopcsharp.txt @@ -1,5 +1,5 @@ ============================================================================== -INTRODUCTION *hopcsharp.txt +INTRODUCTION *hopcsharp.txt* Author: a.f.gurevich@gmail.com @@ -14,9 +14,10 @@ CONTENTS *hopcsharp-contents* 3. Installation .............. |hopcsharp-installation| 4. Configuration ............. |hopcsharp-configuration| 5. API ....................... |hopcsharp-api| - 6. Commands .................. |hopcsharp-commands| - 7 Schema .................... |hopcsharp-database| - 8. Troubleshooting ........... |hopcsharp-troubleshooting| + 6. FZF Pickers ............... |hopcsharp-fzf-pickers| + 7. Commands .................. |hopcsharp-commands| + 8. Schema .................... |hopcsharp-database| + 9. Troubleshooting ........... |hopcsharp-troubleshooting| ============================================================================== @@ -232,6 +233,112 @@ Example: > ]], { name = 'MyClassName' }) < +============================================================================== +FZF PICKERS *hopcsharp-fzf-pickers* + +hopcsharp.nvim provides built-in fzf-lua pickers for navigating definitions +and source files. These require the |fzf-lua| plugin to be installed. +https://github.com/ibhagwan/fzf-lua + +All pickers support the following keybindings: +- `enter` - open file +- `ctrl-s` - open in horizontal split +- `ctrl-v` - open in vertical split +- `ctrl-t` - open in new tab +- `alt-q` - send selection to quickfix list + + +------------------------------------------------------------------------------ + *hopcsharp.pickers.fzf.source_files* +hopcsharp.pickers.fzf.source_files() + +Opens fzf-lua picker listing all `.cs` source files stored in the database. + +Example: > + require('hopcsharp.pickers.fzf').source_files() +< + +------------------------------------------------------------------------------ + *hopcsharp.pickers.fzf.all_definitions* +hopcsharp.pickers.fzf.all_definitions() + +Opens fzf-lua picker listing all definitions (classes, interfaces, methods, +structs, enums, records, and attributes). + +Example: > + require('hopcsharp.pickers.fzf').all_definitions() +< + +------------------------------------------------------------------------------ + *hopcsharp.pickers.fzf.class_definitions* +hopcsharp.pickers.fzf.class_definitions() + +Opens fzf-lua picker listing class definitions. + +Example: > + require('hopcsharp.pickers.fzf').class_definitions() +< + +------------------------------------------------------------------------------ + *hopcsharp.pickers.fzf.interface_definitions* +hopcsharp.pickers.fzf.interface_definitions() + +Opens fzf-lua picker listing interface definitions. + +Example: > + require('hopcsharp.pickers.fzf').interface_definitions() +< + +------------------------------------------------------------------------------ + *hopcsharp.pickers.fzf.method_definitions* +hopcsharp.pickers.fzf.method_definitions() + +Opens fzf-lua picker listing method definitions. + +Example: > + require('hopcsharp.pickers.fzf').method_definitions() +< + +------------------------------------------------------------------------------ + *hopcsharp.pickers.fzf.struct_definitions* +hopcsharp.pickers.fzf.struct_definitions() + +Opens fzf-lua picker listing struct definitions. + +Example: > + require('hopcsharp.pickers.fzf').struct_definitions() +< + +------------------------------------------------------------------------------ + *hopcsharp.pickers.fzf.enum_definitions* +hopcsharp.pickers.fzf.enum_definitions() + +Opens fzf-lua picker listing enum definitions. + +Example: > + require('hopcsharp.pickers.fzf').enum_definitions() +< + +------------------------------------------------------------------------------ + *hopcsharp.pickers.fzf.record_definitions* +hopcsharp.pickers.fzf.record_definitions() + +Opens fzf-lua picker listing record definitions. + +Example: > + require('hopcsharp.pickers.fzf').record_definitions() +< + +------------------------------------------------------------------------------ + *hopcsharp.pickers.fzf.attribute_definitions* +hopcsharp.pickers.fzf.attribute_definitions() + +Opens fzf-lua picker listing attribute definitions. + +Example: > + require('hopcsharp.pickers.fzf').attribute_definitions() +< + ============================================================================== DATABASE SCHEMA *hopcsharp-database* diff --git a/lua/hopcsharp/health.lua b/lua/hopcsharp/health.lua index 26be2a2..e4ac117 100644 --- a/lua/hopcsharp/health.lua +++ b/lua/hopcsharp/health.lua @@ -42,10 +42,21 @@ local function check_treesitter_c_sharp_grammar_installation() end end +local function check_fzf_lua_optional() + vim.health.start('fzf-lua [optional]') + local ok, _ = pcall(require, 'fzf-lua') + if ok then + vim.health.ok('fzf-lua is installed') + else + vim.health.error('fzf-lua is not installed') + end +end + M.check = function() check_fd() check_sqlite_installation() check_treesitter_c_sharp_grammar_installation() + check_fzf_lua_optional() end return M diff --git a/lua/hopcsharp/hop/init.lua b/lua/hopcsharp/hop/init.lua index 82882f5..623d78f 100644 --- a/lua/hopcsharp/hop/init.lua +++ b/lua/hopcsharp/hop/init.lua @@ -1,4 +1,3 @@ -local utils = require('hopcsharp.utils') local hop_utils = require('hopcsharp.hop.utils') local dbutils = require('hopcsharp.database.utils') @@ -8,42 +7,6 @@ local implementation_providers = require('hopcsharp.hop.providers.implementation local M = {} -local stop_callback = nil - -local function populate_quickfix(entries, jump_on_quickfix, type_converter) - -- stop previous quickfix population - -- won't work 100% but it much better - -- rathen that nothing - if stop_callback then - stop_callback() - stop_callback = nil - end - - -- remove previous quickfix entries - vim.fn.setqflist({}, 'r') - - utils.__scheduled_iteration(entries, function(i, item, _, stop) - if i == 1 then - stop_callback = stop - end - - vim.fn.setqflist({ - { - filename = item.path, - lnum = item.row + 1, - col = item.col, - text = string.format('%-15s | %s', type_converter(item.type), item.namespace or ''), - }, - }, 'a') - end) - - vim.cmd([[ :copen ]]) - - if jump_on_quickfix then - vim.cmd([[ :cc! ]]) - end -end - local function filter_entry_under_cursor(entries) local filtered_entries = {} local current_line = vim.fn.getcurpos()[2] -- 2 for line number @@ -98,9 +61,8 @@ M.__hop_to = function(hop_providers, config) -- sent to quickfix if there is too much if #filtered_items > 1 then - -- TODO cover this in test local converter = type_converter or dbutils.get_type_name - populate_quickfix(filtered_items, jump_on_quickfix, converter) + hop_utils.__populate_quickfix(filtered_items, jump_on_quickfix, converter) end return diff --git a/lua/hopcsharp/hop/utils.lua b/lua/hopcsharp/hop/utils.lua index 0414def..31bbd4e 100644 --- a/lua/hopcsharp/hop/utils.lua +++ b/lua/hopcsharp/hop/utils.lua @@ -1,5 +1,9 @@ +local utils = require('hopcsharp.utils') + local M = {} +local stop_callback = nil + M.__open_buffer = function(path, exists_callback, not_exists_callback) local buffers = vim.api.nvim_list_bufs() -- check if the file is already open in any buffer @@ -54,4 +58,38 @@ M.__thop = function(path, row, column) vim.fn.setcursorcharpos(row, column + 1) end +M.__populate_quickfix = function(entries, jump_on_quickfix, type_converter) + -- stop previous quickfix population + -- won't work 100% but it much better + -- rathen that nothing + if stop_callback then + stop_callback() + stop_callback = nil + end + + -- remove previous quickfix entries + vim.fn.setqflist({}, 'r') + + utils.__scheduled_iteration(entries, function(i, item, _, stop) + if i == 1 then + stop_callback = stop + end + + vim.fn.setqflist({ + { + filename = item.path, + lnum = item.row + 1, + col = item.col, + text = string.format('%-15s | %s', type_converter(item.type), item.namespace or ''), + }, + }, 'a') + end) + + vim.cmd([[ :copen ]]) + + if jump_on_quickfix then + vim.cmd([[ :cc! ]]) + end +end + return M diff --git a/lua/hopcsharp/pickers/fzf/file.lua b/lua/hopcsharp/pickers/fzf/file.lua new file mode 100644 index 0000000..f508109 --- /dev/null +++ b/lua/hopcsharp/pickers/fzf/file.lua @@ -0,0 +1,42 @@ +local hopcsharp = require('hopcsharp') + +local M = {} + +M.__source_files = function(fzf) + return function() + fzf.fzf_exec(function(fzf_cb) + coroutine.wrap(function() + local db = hopcsharp.get_db() + local co = coroutine.running() + local items = db:eval([[ SELECT path FROM files ]]) + + if type(items) ~= 'table' then + items = {} + end + + for _, entry in pairs(items) do + fzf_cb(entry.path, function() + coroutine.resume(co) + end) + coroutine.yield() + end + fzf_cb() + end)() + end, { + actions = { + ['enter'] = fzf.actions.file_edit_or_qf, + ['ctrl-s'] = fzf.actions.file_split, + ['ctrl-v'] = fzf.actions.file_vsplit, + ['ctrl-t'] = fzf.actions.file_tabedit, + ['alt-q'] = fzf.actions.file_sel_to_qf, + ['alt-Q'] = fzf.actions.file_sel_to_ll, + ['alt-i'] = fzf.actions.toggle_ignore, + ['alt-h'] = fzf.actions.toggle_hidden, + ['alt-f'] = fzf.actions.toggle_follow, + }, + previewer = 'builtin', + }) + end +end + +return M diff --git a/lua/hopcsharp/pickers/fzf/init.lua b/lua/hopcsharp/pickers/fzf/init.lua new file mode 100644 index 0000000..401c80a --- /dev/null +++ b/lua/hopcsharp/pickers/fzf/init.lua @@ -0,0 +1,51 @@ +local ok, _ = pcall(require, 'fzf-lua') +if not ok then + error("hopcsharp.pickers.fzf was required but 'fzf-lua' plugin was not found") +end + +local fzf = require('fzf-lua') +local builtin = require('fzf-lua.previewer.builtin') +local hopcsharp = require('hopcsharp') +local file = require('hopcsharp.pickers.fzf.file') +local utils = require('hopcsharp.pickers.fzf.utils') +local db_query = require('hopcsharp.database.query') +local db_utils = require('hopcsharp.database.utils') + +local M = {} + +local get_items_by_type = function(type) + local db = hopcsharp.get_db() + return function() + return db:eval(db_query.get_definition_by_type, { type = type }) + end +end + +local get_items_by_type_picker = function(item_type) + return utils.__get_picker(fzf, builtin, get_items_by_type(item_type), utils.__format_name_and_namespace) +end + +M.source_files = file.__source_files(fzf) + +M.all_definitions = utils.__get_picker(fzf, builtin, function() + local db = hopcsharp.get_db() + return db:eval(db_query.get_all_definitions) +end, utils.__format_name_and_namespace) + +M.class_definitions = get_items_by_type_picker(db_utils.types.CLASS) + +M.interface_definitions = get_items_by_type_picker(db_utils.types.INTERFACE) + +M.method_definitions = get_items_by_type_picker(db_utils.types.METHOD) + +M.struct_definitions = get_items_by_type_picker(db_utils.types.STRUCT) + +M.enum_definitions = get_items_by_type_picker(db_utils.types.ENUM) + +M.record_definitions = get_items_by_type_picker(db_utils.types.RECORD) + +M.attribute_definitions = utils.__get_picker(fzf, builtin, function() + local db = hopcsharp.get_db() + return db:eval(db_query.get_attributes) +end, utils.__format_name_and_namespace) + +return M diff --git a/lua/hopcsharp/pickers/fzf/utils.lua b/lua/hopcsharp/pickers/fzf/utils.lua new file mode 100644 index 0000000..c04a67a --- /dev/null +++ b/lua/hopcsharp/pickers/fzf/utils.lua @@ -0,0 +1,122 @@ +local hop_utils = require('hopcsharp.hop.utils') +local db_utils = require('hopcsharp.database.utils') + +local M = {} + +local function parse_entry(entry, items) + local id = nil + + -- lookup id of the item in items + for part in string.gmatch(entry, '%[(%d+)%]') do + id = tonumber(part) + end + + local item = items[id] + + return item.path, item.row, item.column, item.type, item.namespace +end + +M.__format_entry_name_type_namespace = function(i, entry) + local type_name = db_utils.get_type_name(entry.type) + return string.format('%-50s %-20s %-30s [%s]', entry.name, type_name, entry.namespace, i) +end + +M.__format_name_and_namespace = function(i, entry) + return string.format('%-70s %-30s [%s]', entry.name, entry.namespace, i) +end + +M.__get_picker = function(fzf, builtin_previewer, items_provider, formatter) + -- store items in a closure + -- so after db call and render + -- we can retrieve info from here + local items = {} + + -- see example here https://github.com/ibhagwan/fzf-lua/wiki/Advanced#preview-nvim-builtin + local custom_previewer = builtin_previewer.buffer_or_file:extend() + + function custom_previewer:new(o, opts, fzf_win) + custom_previewer.super.new(self, o, opts, fzf_win) + setmetatable(self, custom_previewer) + return self + end + + function custom_previewer:parse_entry(entry_str) + local path, line, col = parse_entry(entry_str, items) + return { path = path, line = line + 1, col = col + 1 } + end + + local picker = function() + -- get database (connection is always opened) + fzf.fzf_exec(function(fzf_cb) + coroutine.wrap(function() + local co = coroutine.running() + items = items_provider() + + if type(items) ~= 'table' then + items = {} + end + + for i, entry in ipairs(items) do + fzf_cb(formatter(i, entry), function() + coroutine.resume(co) + end) + coroutine.yield() + end + fzf_cb() + end)() + end, { + actions = { + -- on select hop to definition by path row and column + ['default'] = function(selected) + local path, row, column = parse_entry(selected[1], items) + -- fixing row by + 1 because __hop internally fixies column already + -- TODO do those fixes in a single place + hop_utils.__hop(path, row + 1, column) + end, + + ['ctrl-v'] = function(selected) + local path, row, column = parse_entry(selected[1], items) + hop_utils.__vhop(path, row + 1, column) + end, + + ['ctrl-s'] = function(selected) + local path, row, column = parse_entry(selected[1], items) + hop_utils.__shop(path, row + 1, column) + end, + + ['ctrl-t'] = function(selected) + local path, row, column = parse_entry(selected[1], items) + hop_utils.__thop(path, row + 1, column) + end, + + ['alt-q'] = function(selected) + local quickfix_entries = {} + for _, selected_item in ipairs(selected) do + -- not fixing here row and column by + 1 + -- because populate_quickfix already does that + local path, row, column, type, namespace = parse_entry(selected_item, items) + table.insert(quickfix_entries, { + path = path, + row = row, + column = column, + type = type, + namespace = namespace, + }) + end + + hop_utils.__populate_quickfix(quickfix_entries, true, db_utils.get_type_name) + end, + }, + + previewer = custom_previewer, + + fzf_opts = { + ['--wrap'] = false, + ['--multi'] = true, + }, + }) + end + return picker +end + +return M diff --git a/test/hop/hop_to_spec.lua b/test/hop/hop_to_spec.lua index 309d0c2..b125702 100644 --- a/test/hop/hop_to_spec.lua +++ b/test/hop/hop_to_spec.lua @@ -25,6 +25,37 @@ describe('hop_to', function() assert(called_get_hops) end) + it('__hop_to calls type_converter if providers returns it', function() + local called_can_handle = false + local called_get_hops = false + local called_type_provider = false + + local provider = function(current_word, node) + return { + can_handle = function() + called_can_handle = true + return true + end, + + get_hops = function() + called_get_hops = true + return { + { row = 10, column = 10, path = 'dummy1', name = 'dummy_name' }, + { row = 11, column = 10, path = 'dummy2', name = 'dummy_name' }, + }, function() + called_type_provider = true + end + end, + } + end + + hop.__hop_to({ provider('test', nil) }, {}) + + assert(called_type_provider) + assert(called_can_handle) + assert(called_get_hops) + end) + it("__hop_to calls providers that can handle hop and won't call other providers", function() local called_can_handle1 = false local called_get_hops1 = false diff --git a/test/hop/utils_spec.lua b/test/hop/utils_spec.lua new file mode 100644 index 0000000..328437f --- /dev/null +++ b/test/hop/utils_spec.lua @@ -0,0 +1,28 @@ +local hop_utils = require('hopcsharp.hop.utils') + +describe('hop_utils', function() + it('__populate_quickfix calls type converter', function() + local type_converter_called = false + + local type_converter = function() + type_converter_called = true + end + + local entries = { + { path = 'testpath1', row = 1, col = 1, type = 42, namespace = 'namespace1' }, + { path = 'testpath2', row = 1, col = 1, type = 42, namespace = 'namespace2' }, + { path = 'testpath3', row = 1, col = 1, type = 42, namespace = 'namespace3' }, + { path = 'testpath4', row = 1, col = 1, type = 42, namespace = 'namespace4' }, + } + + -- check that qf is empty + assert(#vim.fn.getqflist() == 0) + + -- this one runs via vim.schedule + -- there is no safe way to assume that it (that I'm aware of) + -- finished something + hop_utils.__populate_quickfix(entries, false, type_converter) + + assert(type_converter_called) + end) +end)