A Neovim plugin for viewing and navigating C# and Go code structure using LSP symbols with a TUI interface.
- Multi-Language Support: Works with both C# and Go files with automatic language detection
- Symbol Tree View: Display namespaces, classes, methods, properties, functions, structs, interfaces, and more in a structured preview window
- Namespace-Wide View: Toggle between file-only and namespace-wide symbol viewing to see all symbols across an entire namespace/package
- LSP Integration:
- C#: OmniSharp or csharp-ls
- Go: gopls
- Language-Specific Features:
- C#: Async/await indicators, Task return types, access modifiers
- Go: Method receivers, channel directions, exported/unexported symbols, goroutine detection, error returns
- Quick Navigation: Jump to symbol definitions and navigate through references (works across files in namespace mode)
- Fuzzy Search: Search symbols using Telescope or FZF
- Symbol Highlighting: Highlight all occurrences of a symbol in the buffer
- Quickfix Integration: Add symbol references to the quickfix list
- Customizable Display: Configure window style, icons, symbol path depth, and file separator styles
- Neovim >= 0.8.0
- At least one LSP server:
- C#: OmniSharp or csharp-ls
- Go: gopls
- Optional: telescope.nvim or fzf-lua for fuzzy finding
- Optional: nvim-treesitter with C# (
c_sharp) or Go (go) parser (fallback)
Using lazy.nvim
Important: The plugin name is sharpie.nvim (not sharpier). The plugin fully supports lazy.nvim's opts parameter.
{
'yourusername/sharpie.nvim',
ft = { 'cs', 'csharp', 'go' }, -- Lazy load on C# and Go files
dependencies = { 'nvim-telescope/telescope.nvim' },
opts = {
fuzzy_finder = "telescope",
display = {
style = "bottom",
height = 20,
},
logging = {
enabled = true,
level = "INFO",
},
},
}{
'yourusername/sharpie.nvim',
ft = { 'cs', 'csharp', 'go' },
dependencies = { 'nvim-telescope/telescope.nvim' },
opts = {}, -- Use all defaults
}{
'yourusername/sharpie.nvim',
ft = { 'cs', 'csharp', 'go' },
dependencies = { 'nvim-telescope/telescope.nvim' },
config = function()
require('sharpie').setup({
fuzzy_finder = "telescope",
display = { style = "bottom", height = 20 },
-- Language-specific features
language = {
go = {
show_receiver_types = true,
show_channel_direction = true,
},
},
})
-- Add custom keybindings or additional setup
vim.keymap.set('n', '<leader>cs', '<cmd>SharpieShow<cr>', { desc = 'Show symbols' })
end,
}{
'yourusername/sharpie.nvim',
event = 'LspAttach', -- Load when LSP attaches
dependencies = { 'nvim-telescope/telescope.nvim' },
opts = {
display = { style = "bottom" },
},
}{
'yourusername/sharpie.nvim',
cmd = { 'SharpieShow', 'SharpieHide', 'SharpieSearch' },
ft = { 'cs', 'csharp', 'go' },
dependencies = { 'nvim-telescope/telescope.nvim' },
opts = {
logging = { level = "DEBUG" },
},
}{
'yourusername/sharpie.nvim',
ft = { 'cs', 'csharp' },
dependencies = { 'ibhagwan/fzf-lua' },
opts = {
fuzzy_finder = "fzf",
},
}Using packer.nvim
use {
'yourusername/sharpie.nvim',
ft = { 'cs', 'csharp' },
requires = {
'nvim-telescope/telescope.nvim', -- or 'ibhagwan/fzf-lua'
},
config = function()
require('sharpie').setup()
end
}require('sharpie').setup({
-- Fuzzy finder to use: "telescope" or "fzf"
fuzzy_finder = "telescope",
-- Display settings for the preview window
display = {
style = "bottom", -- left|right|top|bottom|float
width = 60,
height = 20,
y_offset = 1,
x_offset = 1,
auto_reload = true, -- Automatically reload preview when buffer changes
auto_reload_debounce = 500, -- Debounce time in ms for auto-reload
filter_prompt = "> ", -- Prompt shown when in interactive filtering mode
},
-- Cursor positioning after jump (nil = center like 'zz')
cursor_offset = nil,
-- Style settings
style = {
icon_set = {
namespace = "",
class = "",
method = "",
property = "",
field = "",
constructor = "",
enum = "",
interface = "",
struct = "",
event = "",
operator = "",
type_parameter = "",
search = "",
integer = "",
string = "",
boolean = "",
array = "",
number = "",
null = "",
void = "",
object = "",
dictionary = "",
key = "",
task = "⏳", -- Hourglass for Task/async methods
}
},
-- Symbol display options
symbol_options = {
namespace = true, -- Enable namespace-wide view (when toggled)
path = 2, -- 0-3, controls symbol path depth
workspace_symbols = true, -- Required for namespace mode
show_file_location = true, -- Show file path for symbols from other files
namespace_mode_separator_style = "line", -- "line" | "box" | "bold"
},
-- Keybinding settings
keybindings = {
sharpie_local_leader = '+',
disable_default_keybindings = false,
overrides = {
show_preview = "<localleader>s",
hide_preview = "<localleader>h",
step_to_next_symbol = "<localleader>n",
step_to_prev_symbol = "<localleader>p",
step_to_next_reference = "<localleader>N",
step_to_prev_reference = "<localleader>P",
search_symbols = "<localleader>f",
toggle_highlight = "<localleader>H",
toggle_namespace_mode = "<localleader>t",
start_filtering = "<localleader>s.",
},
-- Preview window keybindings (buffer-local)
preview = {
jump_to_symbol = "<CR>", -- Jump to symbol under cursor
next_symbol = "n", -- Navigate to next symbol
prev_symbol = "p", -- Navigate to previous symbol
close = "q", -- Close preview window
filter = "/", -- Start filtering/searching
}
}
})-- Show preview window with symbols
require('sharpie').show(bufnr)
-- Hide preview window
require('sharpie').hide(bufnr)
-- Toggle between file-only and namespace-wide view
require('sharpie').toggle_namespace_mode()
-- Navigate symbols
require('sharpie').step_to_next_symbol(bufnr)
require('sharpie').step_to_prev_symbol(bufnr)
-- Navigate references
require('sharpie').step_to_next_reference(bufnr)
require('sharpie').step_to_prev_reference(bufnr)
-- Search symbols with fuzzy finder
require('sharpie').search_symbols(query, bufnr)
-- Go to reference/definition
require('sharpie').search_go_to_reference(symbol_id, bufnr)
require('sharpie').search_go_to_definition(symbol_id, bufnr)
-- Highlight symbol occurrences
-- on: true/false/nil (nil = toggle), hl_group can be custom, bg/fg can be #RRGGBB or HL group name
require('sharpie').highlight_symbol_occurrences(symbol_id, hl_group, bufnr, bg, fg, on)
-- Add occurrences to quickfix list
require('sharpie').add_occurences_to_qflist(symbol_id, bufnr)
-- Run health check
require('sharpie').checkhealth()With default configuration (using + as local leader prefix):
+s- Show the preview window+h- Hide the preview window+n- Step to the next symbol+p- Step to the previous symbol+N- Step to the next reference+P- Step to the previous reference+H- Toggle highlighting+t- Toggle namespace mode (file-only ↔ namespace-wide)+s.- Start filtering symbols+f- Search for symbols
When focused on the preview window (fully configurable via keybindings.preview):
<CR>- Jump to symbol under cursorn- Next symbolp- Previous symbolq- Close preview window/- Start filtering symbols (live filtering in preview)<Esc>- Clear filter and show all symbols
Two-Mode System:
The preview window operates in two distinct modes:
Browse and explore symbols with full navigation:
n/p: Navigate to next/previous symbol<CR>: Jump to symbol under cursor/: Enter Filter Modeq: Close preview window<Esc>: Clear any active filter (if present)
Visual indicator:
- No filter: Clean symbol list
- Filter active:
Filter: query (X/Y matches)at top
Build a filter query by typing directly in the preview (dired-style):
- Type any character: Add to filter query
n/p: Navigate through filtered results (while typing!)<Backspace>: Remove last character<Enter>: Exit to Navigate Mode (keeps filter)<Esc>: Clear filter and exit to Navigate Modeq: Exit to Navigate Mode (keeps filter)
Visual indicator: Input line with prompt at top: > query
Navigate Mode ──[/]──> Filter Mode ──[Enter/q/Esc]──> Navigate Mode
↑ |
└──────────────────────────────────────────────────────┘
Customize Filter Prompt:
require('sharpie').setup({
display = {
filter_prompt = "🔍 ", -- Use any icon or text
}
})Example Workflow:
1. Navigate Mode (browsing all symbols):
──────────────────────────────
Program
User
UserService
GetUser(int id)
GetUserAsync(int id) ← Press '/' to enter Filter Mode
UpdateUser()
DeleteUser()
2. Filter Mode (type "Get"):
> Get ← FILTER MODE - typing query
──────────────────────────────
GetUser(int id) ← Press 'n' to navigate while typing
→ GetUserAsync(int id)
3. Navigate Mode (after pressing Enter):
Filter: Get (2/50 matches) ← NAVIGATE MODE - filter applied
──────────────────────────────
→ GetUser(int id) ← Press 'n'/'p' to navigate
GetUserAsync(int id) ← Press Enter to jump to symbol
4. Back to Navigate Mode (press Esc to clear filter):
────────────────────────────── ← NAVIGATE MODE - all symbols
Program
User
→ UserService ← Back to browsing full tree
GetUser(int id)
GetUserAsync(int id)
Customizing Preview Keybindings:
require('sharpie').setup({
keybindings = {
preview = {
jump_to_symbol = "<CR>", -- Change to any key
next_symbol = "j", -- Use j instead of n
prev_symbol = "k", -- Use k instead of p
close = "<Esc>", -- Use Esc instead of q
filter = "?", -- Use ? instead of /
clear_filter = "c", -- Use c to clear filter
}
}
})To disable a preview keybinding, set it to an empty string:
preview = {
filter = "", -- Disable the filter keybinding
}The preview window automatically reloads in two scenarios:
-
Buffer Content Changes: When you edit the current C# file (add/remove methods, etc.), the preview updates automatically after a short debounce period (default 500ms). This is triggered immediately when you save the file.
-
Buffer Switching: When you switch to a different C# file (e.g., via
:bnext,:bprev, or opening a new file), the preview window automatically shows the symbols for the new file.
Configuration:
require('sharpie').setup({
display = {
auto_reload = true, -- Enable/disable auto-reload (default: true)
auto_reload_debounce = 500, -- Debounce time in ms (default: 500)
}
})Disable auto-reload:
require('sharpie').setup({
display = {
auto_reload = false, -- Disable auto-reload
}
})Adjust debounce time (for faster/slower updates during editing):
require('sharpie').setup({
display = {
auto_reload_debounce = 1000, -- Wait 1 second after typing stops
}
})Behavior Details:
- When you edit a file, the preview waits
auto_reload_debouncemilliseconds after your last change before refreshing (to avoid constant updates while typing) - When you save a file (
:w), the preview refreshes immediately - When you switch to a different C# file, the preview refreshes immediately and any active filter is cleared
- Auto-reload only works when the preview window is already open - it won't open the preview automatically
sharpie.nvim supports two viewing modes that you can toggle between:
Shows symbols only from the current file, just like traditional symbol viewers.
Shows all symbols from the current file's namespace/package across all files in your workspace. This is perfect for exploring large namespaces or packages without switching between files.
How it works:
-
Auto-detection: Automatically detects the namespace from your current file
- C#: Supports both file-scoped (
namespace MyApp.Services;) and block-scoped (namespace MyApp.Services { }) namespaces - Go: Detects package name (
package mypackage)
- C#: Supports both file-scoped (
-
Workspace Symbols: Queries your LSP server for all symbols in the detected namespace
-
File Grouping: Groups symbols by file with visual separators:
Namespace: MyApp.Services (42 symbols across 5 files) ══════════════════════════════════════════════════════════ ─── src/Services/UserService.cs (8 symbols) ─── UserService GetUser(int id) CreateUser(User user) ─── src/Services/EmailService.cs (6 symbols) ─── EmailService SendEmail(string to, string subject) -
Seamless Navigation: Navigate with
n/p(automatically skips file headers) and jump with<CR>to symbols in any file
Toggle between modes:
- Press
+t(or run:SharpieToggleNamespaceMode) - The preview window updates immediately with symbols from the current mode
- Mode persists until you toggle again
Configuration:
require('sharpie').setup({
symbol_options = {
namespace = true, -- Enable namespace mode capability
workspace_symbols = true, -- Required for namespace mode
namespace_mode_separator_style = "line", -- "line" | "box" | "bold"
}
})Separator Styles:
-- Line style (default)
─── src/Services/UserService.cs (8 symbols) ───
-- Box style
┌─── src/Services/UserService.cs (8 symbols)
-- Bold style
▶ src/Services/UserService.cs (8 symbols)Features in Namespace Mode:
- ✅ Auto-reload when editing files
- ✅ Interactive filtering with
/ - ✅ Cross-file navigation
- ✅ Relative file paths for cleaner display
- ✅ Symbol count per file
Limitations:
- Requires LSP server with workspace symbol support (OmniSharp, csharp-ls, and gopls all support this)
- Query performance depends on workspace size and LSP server
- For Go, packages spanning multiple directories are shown separately per directory
Understanding the Modes:
sharpie.nvim has two independent mode systems:
- View Modes (File-Only vs Namespace-Wide)
- Preview Modes (Navigate vs Filter)
-
File-Only Mode (Default)
- Press
+sto show symbols from the current file - Navigate with
n/pthrough all symbols - Press
<CR>to jump to any symbol
- Press
-
Switch to Namespace-Wide Mode
- Press
+tto toggle namespace mode - View updates to show all symbols from the current namespace across all files
- File headers separate symbols by file
- Press
-
Navigate Across Files
- Use
n/pto browse symbols (automatically skips file headers) - Press
<CR>to jump to a symbol in any file - Press
+tagain to return to file-only mode
- Use
-
Navigate Mode (Default)
- Browse symbols with
n/p - Press
<CR>to jump to any symbol
- Browse symbols with
-
Filter Symbols (Enter Filter Mode)
- Press
/to enter Filter Mode - Type directly in the preview: "Get"
- Navigate filtered results with
n/pwhile typing - Press
<Enter>to return to Navigate Mode (filter stays active)
- Press
-
Navigate Filtered Results (Navigate Mode with filter)
- Use
n/pto browse only matching symbols - Press
<CR>to jump to a filtered symbol - Press
<Esc>to clear filter and see all symbols again
- Use
- Open a C# file and press
+sto show symbols (file-only mode) - Press
+tto switch to namespace-wide view - Press
/and type "User" to filter symbols containing "User" - Navigate filtered results with
n/pacross all files in the namespace - Press
<CR>to jump to a symbol in another file - Edit your code - preview automatically refreshes
- Press
+Hto highlight all occurrences of the symbol under cursor - Use
+N/+Pto cycle through references
The symbol_options.path setting controls how much of the symbol path is displayed:
0: Just the symbol name -Main(string[] args)1: Class.Symbol -Program.Main(string[] args)2: Namespace.Class.Symbol -MyNamespace.Program.Main(string[] args)(default)3: Full path -FullNamespace.Leading.To.MyNamespace.Program.Main(string[] args)
MyNamespace
MyNamespace.Program
( ) MyNamespace.Program.Main(string[] args)
MyNamespace.MyClass
( ) MyNamespace.MyClass.MyProperty
( ) MyNamespace.MyClass.MyStaticInt()
⏳ ( ) MyNamespace.MyClass.MyAsyncMethod() # Task return type
⏳ ( <> ) MyNamespace.MyClass.MyAsyncMethod2() # Task<T> return type
Smart Icon Detection:
- Methods returning
Task(no generic) get the hourglass icon (⏳) - Methods returning
Task<T>show the icon for typeT:Task<int>→ (integer icon)Task<string>→ (string icon)Task<bool>→ (boolean icon)Task<User>→ (class icon)Task<List<T>>→ (array icon)
- Classes, structs, and interfaces get the class icon ()
- Async methods are detected and marked appropriately
Run :checkhealth sharpie to verify:
- Neovim version compatibility (>= 0.8.0)
- LSP is active and configured
- C# LSP client is available (OmniSharp or csharp-ls)
- LSP server capabilities (symbols, references, definitions)
- Current buffer filetype
- Treesitter is available (optional)
- C# treesitter parser installed (optional)
- Configured fuzzy finder is installed
- Plugin configuration validity
- Logging setup and log directory
sharpie.nvim includes a comprehensive logging system for troubleshooting and debugging.
require('sharpie').setup({
logging = {
enabled = true, -- Enable/disable logging
level = "INFO", -- TRACE, DEBUG, INFO, WARN, ERROR, FATAL
file = vim.fn.stdpath('data') .. '/sharpie.log',
max_file_size = 10 * 1024 * 1024, -- 10MB, auto-rotates
include_timestamp = true, -- Include timestamps in logs
include_location = true, -- Include file:line in logs
console_output = false, -- Also output to vim.notify
format = "default", -- "default" or "json"
}
}):SharpieLog- View log file in a split window:SharpieLog tail- View log file in follow mode (auto-updates):SharpieLogClear- Clear the log file:SharpieLogStats- Show logging statistics in a floating window:SharpieLogLevel [LEVEL]- Get or set the log level
- TRACE: Detailed execution flow (function entry/exit)
- DEBUG: Detailed debugging information (LSP requests/responses)
- INFO: General informational messages (default)
- WARN: Warning messages for potential issues
- ERROR: Error messages for failures
- FATAL: Critical errors
" Enable debug logging
:SharpieLogLevel DEBUG
" Try to show symbols
:SharpieShow
" View the log to see detailed LSP communication
:SharpieLog
" Check statistics
:SharpieLogStatsIn addition to the API functions, sharpie.nvim provides user commands:
:SharpieShow- Show the preview window:SharpieHide- Hide the preview window:SharpieSearch- Search symbols with fuzzy finder:SharpieToggleHighlight- Toggle symbol highlighting:SharpieToggleNamespaceMode- Toggle between file-only and namespace-wide view:SharpieNextSymbol- Jump to next symbol:SharpiePrevSymbol- Jump to previous symbol:SharpieNextReference- Jump to next reference:SharpiePrevReference- Jump to previous reference:SharpieFilterClear- Clear symbol filter in preview
If you see an error like Lua module not found for config of sharpier.nvim, ensure:
- Correct plugin name: It's
sharpie.nvimnotsharpier.nvim - Using
dirwith different directory name: If your directory is named differently than the module, explicitly set thename:{ dir = '/path/to/sharpier.nvim', -- Directory name is sharpier.nvim name = 'sharpie.nvim', -- But module name is sharpie ft = { 'cs', 'csharp' }, dependencies = { 'nvim-telescope/telescope.nvim' }, opts = {}, } - Use a
configfunction: Instead of a string, use:config = function() require('sharpie').setup() end
- Check the module name: The Lua module is
sharpie(without.nvim)
- Check that a C# LSP server is installed and running:
:LspInfo
- Run the health check:
:checkhealth sharpie
- Enable debug logging:
:SharpieLogLevel DEBUG :SharpieShow :SharpieLog
- Ensure you're in a C# file (
.csextension) - Check that LSP is attached to the buffer
- Verify the LSP server supports
textDocument/documentSymbol::checkhealth sharpie
Check log file for slow operations:
:SharpieLogLook for lines with high duration_ms values. Consider:
- Reducing
symbol_options.pathdepth - Disabling logging in production:
logging.enabled = false
See the examples/ directory for complete configuration examples:
examples/lazy.lua- lazy.nvim configuration with all optionsexamples/packer.lua- packer.nvim configuration
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details.
Inspired by the dired interface in Emacs and various LSP symbol viewers.