fully rewrite version 0.3.0 (#1108)

fully rewrite all . new usage take a look at https://dev.neovim.pro
This commit is contained in:
Raphael 2023-07-09 20:05:56 +08:00 committed by GitHub
parent 4f075452c4
commit a17c975ac2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 5512 additions and 7722 deletions

1
.github/FUNDING.yml vendored
View file

@ -1,2 +1,3 @@
github: glepnir
open_collective: nvimdev
custom: ['https://www.paypal.me/bobbyhub']

View file

@ -37,3 +37,32 @@ jobs:
commit_user_name: "github-actions[bot]"
commit_user_email: "github-actions[bot]@users.noreply.github.com"
commit_author: "github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
test:
name: Run Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- uses: rhysd/action-setup-vim@v1
id: vim
with:
neovim: true
version: nightly
- name: luajit
uses: leafo/gh-actions-lua@v10
with:
luaVersion: "luajit-2.1.0-beta3"
- name: luarocks
uses: leafo/gh-actions-luarocks@v4
- name: run test
shell: bash
run: |
luarocks install luacheck
luarocks install vusted
vusted ./test

View file

@ -1,67 +0,0 @@
## Version 0.2.3 -- 2023-01-13
- Refactor all
## Version 0.2.2 -- 2022--09-09
- fix symbolwinbar high memory usage
- `preview_definition` chang to `peek_definition` support edit definition file in floatwindow and highlight
- `code_action` support in visual mode .
- `finder` preview highlight current word.
- show diagnostic source by default.
- add lspsaga codeaction keymap in diagnostic header
## Remove
- remove `definition_preview_icon` option
- remove `finder_preview_hl_ns`
- remove `diagnostic_source_bracket`
- remove `show_diagnostic_source` .
## Version 0.2.1 -- 2022-08-30
### New
- improve scroll in preview remove old implement of preview
- add new option `scroll_in_preview` use default scroll keymap to scroll `hover` `finder preview`
- use `CursorMoved` with timer in LightBulb instead of using `CursorHold` `CursorHoldI`
- new option `update_time` in code_action_lightbulb
- auto jump into the `preview_definition` window
- `floaterm` can save the state.
### Bug fix
- fix `definition_preview` and `hover` window not closed when `CursorMoved`
- fix `finder` works not well when server has multiple servers
### Remove
- remove function `action.smart_scroll_with_saga(1)` no need this function
## Version 0.2
### BreakChange
Please use command in `vim.keymap.set` rhs . check the README.
### New
- `Finder:` support show `implement` in `Lspsaga lsp_finder`
- `Finder:` add option `imp` in `finder_icons`
- `LightBulb` add api to genreate sign need neovim 0.8 nightly. 0.7 version also use `vim.fn.sign_place/unplace`
- `LightBulb` set the `CursorHold CursorHoldI` only take effect when the buffer has lsp server. and remove them when
buffer delete ,keep the autocmds clean
- `Background of Lspsaga floatwindow` now it will use the colorscheme Normal highlight
- `Notify` add notify for some commands if there has no server give a message
- `Symbolwinbar` when delete the buffer remove the buffer events that symbolwinbar used,keep autocmds clean
- `Outline` remove the patch as [neovim#19458](https://github.com/neovim/neovim/issues/19458#)completed
- `Outline` fix bug
- `Definition` rewrite definition
- Close the `finder` `rename` window when use wincmd shortcut jump to other window like `<C-w>h`
- `Diagnostic` fix the virtual text bug when jump diagnostic
- Better `show_line_diagnostic` and `show_cursor_diagnostic`
- `custom_kind` option change the default lspkind icon and color
### Remove
- remove `Lspsaga implement`.

687
README.md
View file

@ -1,693 +1,18 @@
```
__
/ /________ _________ _____ _____ _
/ / ___/ __ \/ ___/ __ `/ __ `/ __ `/
/ (__ ) /_/ (__ ) /_/ / /_/ / /_/ /
/_/____/ .___/____/\__,_/\__, /\__,_/
/_/ /____/
<center>
<img src="https://github.com/nvimdev/lspsaga.nvim/assets/41671631/682a189e-6571-48f8-af1c-1e52142c7071" width="20%" height="20%"/>
</center>
⚡ Designed for convenience and efficiency ⚡
```
Neovim lsp enhance plugin.
improve lsp experences in neovim
[![](https://img.shields.io/badge/Element-0DBD8B?style=for-the-badge&logo=element&logoColor=white)](https://matrix.to/#/#lspsaga-nvim:matrix.org)
1. [Install](#install)
2. [Example Configuration](#example-configuration)
3. [Using Lspsaga](#using-lspsaga)
4. [Customizing Lspsaga's Appearance](#customizing-lspsagas-appearance)
5. [Backers](#backers)
6. [Donate](#donate)
7. [License](#license)
## Install
You can use plugin managers like `lazy.nvim` and `packer.nvim` to install `lspsaga` and lazy load `lspsaga` using the plugin manager's keyword for lazy loading (`lazy` for `lazy.nvim` and `opt` for `packer.nvim`).
- `cmd` - Load `lspsaga` only when a `lspsaga` command is called.
- `ft` - `lazy.nvim` and `packer.nvim` both provide lazy loading by filetype. This way, you can load `lspsaga` according to the filetypes that you use a LSP in.
- `event` - Only load `lspsaga` on an event like `BufRead` or `BufReadPost`. Do make sure that your LSP plugins, like [lsp-zero](https://github.com/VonHeikemen/lsp-zero.nvim) or [lsp-config](https://github.com/neovim/nvim-lspconfig), are loaded before loading `lspsaga`.
- `dependencies` - For `lazy.nvim` you can set `glepnir/lspsaga.nvim` as a dependency of `nvim-lspconfig` using the `dependencies` keyword and vice versa. For `packer.nvim` you should use `requires` as the keyword instead.
- `after` - For `packer.nvim` you can use `after` keyword to ensure `lspsaga` only loads after your LSP plugins have loaded. This is not necessary for `lazy.nvim`.
- [Lazy](https://github.com/folke/lazy.nvim)
```lua
require("lazy").setup({
"glepnir/lspsaga.nvim",
event = "LspAttach",
config = function()
require("lspsaga").setup({})
end,
dependencies = {
{"nvim-tree/nvim-web-devicons"},
--Please make sure you install markdown and markdown_inline parser
{"nvim-treesitter/nvim-treesitter"}
}
}, opt)
```
- [Packer](https://github.com/wbthomason/packer.nvim)
```lua
use({
"glepnir/lspsaga.nvim",
opt = true,
branch = "main",
event = "LspAttach",
config = function()
require("lspsaga").setup({})
end,
requires = {
{"nvim-tree/nvim-web-devicons"},
--Please make sure you install markdown and markdown_inline parser
{"nvim-treesitter/nvim-treesitter"}
}
})
```
## Example Configuration
```lua
require("lazy").setup({
"glepnir/lspsaga.nvim",
event = "LspAttach",
config = function()
require("lspsaga").setup({})
end,
dependencies = { {"nvim-tree/nvim-web-devicons"} }
})
local keymap = vim.keymap.set
-- LSP finder - Find the symbol's definition
-- If there is no definition, it will instead be hidden
-- When you use an action in finder like "open vsplit",
-- you can use <C-t> to jump back
keymap("n", "gh", "<cmd>Lspsaga lsp_finder<CR>")
-- Code action
keymap({"n","v"}, "<leader>ca", "<cmd>Lspsaga code_action<CR>")
-- Rename all occurrences of the hovered word for the entire file
keymap("n", "gr", "<cmd>Lspsaga rename<CR>")
-- Rename all occurrences of the hovered word for the selected files
keymap("n", "gr", "<cmd>Lspsaga rename ++project<CR>")
-- Peek definition
-- You can edit the file containing the definition in the floating window
-- It also supports open/vsplit/etc operations, do refer to "definition_action_keys"
-- It also supports tagstack
-- Use <C-t> to jump back
keymap("n", "gp", "<cmd>Lspsaga peek_definition<CR>")
-- Go to definition
keymap("n","gd", "<cmd>Lspsaga goto_definition<CR>")
-- Peek type definition
-- You can edit the file containing the type definition in the floating window
-- It also supports open/vsplit/etc operations, do refer to "definition_action_keys"
-- It also supports tagstack
-- Use <C-t> to jump back
keymap("n", "gt", "<cmd>Lspsaga peek_type_definition<CR>")
-- Go to type definition
keymap("n","gt", "<cmd>Lspsaga goto_type_definition<CR>")
-- Show line diagnostics
-- You can pass argument ++unfocus to
-- unfocus the show_line_diagnostics floating window
keymap("n", "<leader>sl", "<cmd>Lspsaga show_line_diagnostics<CR>")
-- Show buffer diagnostics
keymap("n", "<leader>sb", "<cmd>Lspsaga show_buf_diagnostics<CR>")
-- Show workspace diagnostics
keymap("n", "<leader>sw", "<cmd>Lspsaga show_workspace_diagnostics<CR>")
-- Show cursor diagnostics
keymap("n", "<leader>sc", "<cmd>Lspsaga show_cursor_diagnostics<CR>")
-- Diagnostic jump
-- You can use <C-o> to jump back to your previous location
keymap("n", "[e", "<cmd>Lspsaga diagnostic_jump_prev<CR>")
keymap("n", "]e", "<cmd>Lspsaga diagnostic_jump_next<CR>")
-- Diagnostic jump with filters such as only jumping to an error
keymap("n", "[E", function()
require("lspsaga.diagnostic"):goto_prev({ severity = vim.diagnostic.severity.ERROR })
end)
keymap("n", "]E", function()
require("lspsaga.diagnostic"):goto_next({ severity = vim.diagnostic.severity.ERROR })
end)
-- Toggle outline
keymap("n","<leader>o", "<cmd>Lspsaga outline<CR>")
-- Hover Doc
-- If there is no hover doc,
-- there will be a notification stating that
-- there is no information available.
-- To disable it just use ":Lspsaga hover_doc ++quiet"
-- Pressing the key twice will enter the hover window
keymap("n", "K", "<cmd>Lspsaga hover_doc<CR>")
-- If you want to keep the hover window in the top right hand corner,
-- you can pass the ++keep argument
-- Note that if you use hover with ++keep, pressing this key again will
-- close the hover window. If you want to jump to the hover window
-- you should use the wincmd command "<C-w>w"
keymap("n", "K", "<cmd>Lspsaga hover_doc ++keep<CR>")
-- Call hierarchy
keymap("n", "<Leader>ci", "<cmd>Lspsaga incoming_calls<CR>")
keymap("n", "<Leader>co", "<cmd>Lspsaga outgoing_calls<CR>")
-- Floating terminal
keymap({"n", "t"}, "<A-d>", "<cmd>Lspsaga term_toggle<CR>")
```
## Using Lspsaga
**Note that the title in the floating window requires Neovim 0.9 or greater.**
**If you are using Neovim 0.8 you won't see a title.**
\*\*If you are using Neovim 0.9 and want to disable the title, see [Customizing Lspsaga's Appearance](#customizing-lspsagas-appearance)
**You need not copy all of the options into the setup function. Just set the options that you've changed in the setup function and it will be extended with the default options!**
You can find the documentation for Lspsaga in Neovim by using `:h lspsaga`.
## Default options
The top-level default options (command-specific default options below):
```lua
preview = {
lines_above = 0,
lines_below = 10,
},
scroll_preview = {
scroll_down = "<C-f>",
scroll_up = "<C-b>",
},
request_timeout = 2000,
```
Example setup using default options:
```lua
require("lspsaga").setup({
preview = {
lines_above = 0,
lines_below = 10,
},
scroll_preview = {
scroll_down = "<C-f>",
scroll_up = "<C-b>",
},
request_timeout = 2000,
-- See Customizing Lspsaga's Appearance
ui = { ... },
-- For default options for each command, see below
finder = { ... },
code_action = { ... }
-- etc.
})
```
## :Lspsaga lsp_finder
A `finder` to show the definition, reference and implementation (only shown when current hovered word is a function, a type, a class, or an interface).
Default options:
```lua
finder = {
max_height = 0.5,
min_width = 30,
force_max_height = false,
keys = {
jump_to = 'p',
expand_or_jump = 'o',
vsplit = 's',
split = 'i',
tabe = 't',
tabnew = 'r',
quit = { 'q', '<ESC>' },
close_in_preview = '<ESC>',
},
},
```
- `max_height` of the finder window.
- `force_max_height` force window height to max_height
- `keys.jump_to` finder peek window.
- `close_in_preview` will close all finder window in when you in preview window.
- `min_width` is finder preview window min width.
<details>
<summary>lsp_finder showcase</summary>
<img src="https://user-images.githubusercontent.com/41671631/227929557-74ac7d69-2488-46e2-a138-3772a349bcaf.png" height=80% width=80%/>
</details>
## :Lspsaga peek_definition
There are two commands, `:Lspsaga peek_definition` and `:Lspsaga goto_definition`. The `peek_definition`
command works like the VSCode command of the same name, which shows the target file in an editable floating window.
Default options:
```lua
definition = {
edit = "<C-c>o",
vsplit = "<C-c>v",
split = "<C-c>i",
tabe = "<C-c>t",
quit = "q",
}
```
<details>
<summary>peek_definition showcase</summary>
The steps demonstrated in this showcase are:
- Pressing `gp` to run `:Lspsaga peek_definition`
- Editing a comment and using `:w` to save
- Pressing `<C-c>o` to jump to the file in the floating window
- Lspsaga shows a beacon highlight after jumping to the file
<img src="https://user-images.githubusercontent.com/41671631/215719806-0dea0248-4a2c-45df-a258-43a4ba207a43.gif" height=80% width=80%/>
</details>
## :Lspsaga goto_definition
Jumps to the definition of the hovered word and shows a beacon highlight.
## :Lspsaga code_action
Default options:
```lua
code_action = {
num_shortcut = true,
show_server_name = false,
extend_gitsigns = true,
keys = {
-- string | table type
quit = "q",
exec = "<CR>",
},
},
```
- `num_shortcut` - It is `true` by default so you can quickly run a code action by pressing its corresponding number.
- `extend_gitsigns` show gitsings in code action.
<details>
<summary>code_action showcase</summary>
The steps demonstrated in this showcase are:
- Pressing `ga` to run `:Lspsaga code_action`
- Pressing `j` to move within the code action preview window
- Pressing `<Cr>` to run the action
<img src="https://user-images.githubusercontent.com/41671631/215719772-ccebdcba-4e4a-46f7-9af8-61ac8391f2f4.gif" height=80% width=80%/>
</details>
## :Lspsaga Lightbulb
When there are possible code actions to be taken, a lightbulb icon will be shown.
Default options:
```lua
lightbulb = {
enable = true,
enable_in_insert = true,
sign = true,
sign_priority = 40,
virtual_text = true,
},
```
<details>
<summary>lightbulb showcase</summary>
<img src="https://user-images.githubusercontent.com/41671631/212009221-e0fb193e-f69d-4ed6-a4a2-d9ecb589f211.gif" height=80% width=80%/>
</details>
## :Lspasga hover_doc
default options
```lua
hover = {
max_width = 0.6,
open_link = 'gx',
open_browser = '!chrome',
},
```
you can use `open_link` key to open a http link or a file link in hover doc window. the
`open_browser` is `chrome` in default you need config it to your browser
You need install the [treesitter](https://github.com/nvim-treesitter/nvim-treesitter) markdown and markdown_inline parser.
Lspsaga can use it to render the hover window.
You can press the keyboard shortcut for `:Lspsaga hover_doc` twice to enter the hover window.
if you got something wrong in hover please run `:checkhealth`
<details>
<summary>hover_docshow case</summary>
The steps demonstrated in this showcase are:
- Pressing `K` once to run `:Lspsaga hover_doc`
- Pressing `K` again to enter the hover window
- Pressing `q` to quit
<img src="https://user-images.githubusercontent.com/41671631/215719832-37d2f6ab-66ed-4500-b6de-a6c289983ab2.gif" height=80% width=80%/>
</details>
## :Lspsaga diagnostic_jump_next
Jumps to next diagnostic position and show a beacon highlight. Lspsaga will then show the code actions.
Default options:
```lua
diagnostic = {
on_insert = false,
on_insert_follow = false,
insert_winblend = 0,
show_code_action = true,
show_source = true,
jump_num_shortcut = true,
max_width = 0.7,
max_height = 0.6,
max_show_width = 0.9,
max_show_height = 0.6,
text_hl_follow = true,
border_follow = true,
extend_relatedInformation = false,
keys = {
exec_action = 'o',
quit = 'q',
expand_or_jump = '<CR>',
quit_in_show = { 'q', '<ESC>' },
},
},
```
- `jump_num_shortcut` - The default is `true`. After jumping, Lspasga will automatically bind code actions to a number. Afterwards, you can press the number to execute the code action. After the floating window is closed, these numbers will no longer be tied to the same code actions.
- `show_codeaction` default is true it will show available actions in the diagnsotic jump window
- `show_source` default is true extend `source` into the diagnostic message
- `max_width` is the max width for diagnostic jump window. percentage
- `max_height` is the max height of diagnostic jump window percentage
- `text_hl_follow` is false default true that you can define `DiagnostcText` to custom the diagnotic
text color
- `border_follow` the border highlight will follow the diagnostic type. if false it will use the
highlight `DiagnosticBorder`.
- `on_insert` default is true it works like the emacs helix show diagnostic in right but in line.
- `on_insert_follow` true will follow current line. false will on top right
- `insert_winblend` default is 0, when it's to 100 will completely transparent. the color will
changed a little light. 0 will use the `NormalFloat` group. it will link to `Normal` by Lspsaga.
- `max_show_width` is the width of show diagnostic window
- `max_show_height` is the height of show diagnostic widnow
- `extend_relatedInformation` default is false when is true it will extend this message into
diagnostic message
You can also use a filter when using diagnostic jump by using a Lspsaga function. The function takes a table as its argument.
It is functionally identical to `:h vim.diagnostic.get_next`.
```lua
-- This will only jump to an error
-- If no error is found, it executes "goto_next"
require("lspsaga.diagnostic"):goto_prev({ severity = vim.diagnostic.severity.ERROR })
```
<details>
<summary> showcase</summary>
The steps demonstrated in this showcase are:
- Pressing `[e` to jump to the next diagnostic position, which shows the beacon highlight and the code actions in a diagnostic window
- Use `scroll_in_preview` keys to show action preview.
- Pressing the number `2` to execute the code action without needing to enter the floating window
<img src="https://user-images.githubusercontent.com/41671631/227763194-ee0958cf-65f8-4c11-9ee8-956227932114.gif" height=80% width=80%/>
- If you want to see the code action, you can use `<C-w>w` to enter the floating window.
- Press `g` to go to the action line and see the code action preview.
- Press `o` to execute the action.
`on_insert` is true, `on_insert_follow` is false
<img src="https://user-images.githubusercontent.com/41671631/219940539-da554175-dd91-4bca-aaf8-ab39d0ba2a2c.gif" height=80% width=80%/>
`on_insert_follow` is true
<img src="https://user-images.githubusercontent.com/41671631/219909443-f5b4f796-e59d-47cf-9f9a-8a9a69d92449.gif" height=80% width=80%/>
</details>
## :Lspsaga show_diagnostics
`show_line_diagnostics`, `show_buf_diagnostics`, `show_workspace_diagnostics`
`show_cursor_diagnsotics`. and support an
argument `++unfocus` to make it unfocus. like `:Lspsaga show_workspace_diagnostics ++unfocus`
you can press the `expand_or_jump` key to expand on fname line or jump into location on message line.
<details>
<summary>show_diagnostics showcase</summary>
<img src="https://user-images.githubusercontent.com/41671631/227762998-a61c5df3-6a08-4d76-941c-f1cd2aa77f03.png" height=80% width=80%/>
</details>
## :Lspsaga rename
Uses the current LSP to rename the hovered word.
Default options:
```lua
rename = {
quit = "<C-c>",
exec = "<CR>",
mark = "x",
confirm = "<CR>",
in_select = true,
},
```
- `mark` is used for the `++project` argument. It is used to mark the files which you want to rename the hovered word in.
- `confirm` - After you have marked the files, press this key to execute the rename.
<details>
<summary>rename showcase</summary>
The steps demonstrated in this showcase are:
- Pressing `gr` to run `:Lspsaga rename`
- Typing `stesdd` and then pressing `<CR>` to execute the rename
<img src="" height=80% width=80%/>
The steps demonstrated in this showcase are:
- Pressing `gR` to run `:Lspsaga rename ++project`
- Pressing `x` to mark the file
- Pressing `<CR>` to execute rename
<img src="https://user-images.githubusercontent.com/41671631/215719843-7278cc97-399f-48ee-88eb-555647eba42f.gif" height=80% width=80%/>
</details>
## :Lspsaga outline
Default options:
```lua
outline = {
win_position = "right",
win_with = "",
win_width = 30,
preview_width= 0.4,
show_detail = true,
auto_preview = true,
auto_refresh = true,
auto_close = true,
auto_resize = false,
custom_sort = nil,
keys = {
expand_or_jump = 'o',
quit = "q",
},
},
```
<details>
<summary>outline showcase</summary>
The steps demonstrated in this showcase are:
- Pressing `<Leader>o` run `:Lspsaga outline`
<img src="https://user-images.githubusercontent.com/41671631/215719836-25a03774-891b-4dfd-ab2f-0b590ae1c862.gif" height=80% width=80%/>
</details>
## :Lspsaga incoming_calls / outgoing_calls
Runs the LSP's callhierarchy/incoming_calls.
Default options:
```lua
callhierarchy = {
show_detail = false,
keys = {
edit = "e",
vsplit = "s",
split = "i",
tabe = "t",
jump = "o",
quit = "q",
expand_collapse = "u",
},
},
```
<details>
<summary>incoming_calls showcase</summary>
<img src="https://user-images.githubusercontent.com/41671631/215719762-9482e84b-921e-425e-b1a9-7bd1f569a5ce.gif" height=80% width=80%/>
</details>
## :Lspsaga symbols in winbar
This requires Neovim version >= 0.8.
Default options:
```lua
symbol_in_winbar = {
enable = true,
separator = " ",
ignore_patterns={},
hide_keyword = true,
show_file = true,
folder_level = 2,
respect_root = false,
color_mode = true,
},
```
- `hide_keyword` - The default value is `true`. Lspsaga will hide some keywords and temporary variables to make the symbols look cleaner.
- `folder_level` only works when `show_file` is `true`.
- `respect_root` will respect the LSP's root. If this is `true`, Lspsaga will ignore the `folder_level` option. If no LSP client is being used, Lspsaga will fall back to using folder level.
- `color_mode` - The default value is `true`. When it is set to `false`, only icons will have color.
- `ignore_patterns` table type when fileanme matched the pattern will ignore render symbols. if
show_file is true. the file name will still set.
<details>
<summary>Symbols in winbar</summary>
<img src="https://user-images.githubusercontent.com/41671631/212026278-11012b17-209c-4b55-b76c-1c3d8d9a2eb2.gif" height=80% width=80%/>
</details>
## :Lspsaga symbols in a custom winbar/statusline
Lspsaga provides an API that you can use in your custom winbar or statusline.
```lua
vim.wo.winbar / vim.wo.stl = require('lspsaga.symbolwinbar'):get_winbar()
```
## :Lspsaga term_toggle
A simple floating terminal.
<details>
<summary>Toggling the floating terminal</summary>
<img src="https://user-images.githubusercontent.com/41671631/212027060-56d1cebc-c6a8-412e-bd01-620aac3029ed.gif" height=80% width=80%/>
</details>
## :Lspsaga beacon
after jump from float window there will show beacon to remind you where the cursor is.
```lua
beacon = {
enable = true,
frequency = 7,
},
```
`frequency` the blink frequency.
## Customizing Lspsaga's Appearance
## :Lspsaga UI
Default UI options
```lua
ui = {
-- This option only works in Neovim 0.9
title = true,
-- Border type can be single, double, rounded, solid, shadow.
border = "single",
winblend = 0,
expand = "",
collapse = "",
code_action = "💡",
incoming = " ",
outgoing = " ",
hover = ' ',
kind = {},
},
```
# Custom Highlighting
All highlight groups can be found in [highlight.lua](./lua/lspsaga/highlight.lua).
`require('lspsaga.lspkind').get_kind_group()` will return all the SagaWinbar + kind name group . also
include `SagaWinbarFileName SagaWinbarFileIcon SagaWinbarFolderName SagaWinbarSep`. These groups are
special. so if you want use this api to custom the highlight. you need dealwith these 4 groups the
last item is `SagaWinbarSep`.
# Custom Kind
Modify `ui.kind` to change the icons of the kinds.
All kinds used in Lspsaga are defined in [lspkind.lua](./lua/lspsaga/lspkind.lua).
The key in `ui.kind` is the kind name, and the value can either be a string or a table. If a string is passed, it is setting the `icon`. If table is passed, it will be passed as `{ icon, highlight group }`, for example, to change the a folder's icon color, you could do this: `ui = { kind = { ["Folder"] = { " ", "@comment" }, }, },`.
[Usage see the doc website](https://dev.neovim.pro)
# Donate
Currently, I am in need of some donations. If you'd like to support my work financially, please donate through Github Sponsor button or
[PayPal](https://paypal.me/bobbyhub). Thanks!
[![](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://paypal.me/bobbyhub)
[PayPal](https://paypal.me/bobbyhub) [wechat or Alipay](https://user-images.githubusercontent.com/41671631/219828224-8834f48a-0769-45d0-a6b9-1e7f38642fcf.png)
# Backers
Thanks for everyone!
[@Tuo Huang](https://github.com/youngtuotuo)
[@Scott Ming](https://github.com/scottming)
[@Möller Lukas](https://github.com/lmllrjr)
[@HendrikPetertje](https://github.com/HendrikPetertje)
[@Bojan Wilytsch](https://github.com/bwilytsch)
[@zhourrr](https://github.com/zhourrr)
[@Burgess Darrion](https://github.com/ca-mantis-shrimp)
[@Ceserani Alessandro](https://github.com/al-ce)
# License

View file

@ -1,760 +0,0 @@
*lspsaga.nvim.txt* For Nvim 0.8.0 Last change: 2023 June 06
==============================================================================
Table of Contents *lspsaga.nvim-table-of-contents*
- Install |lspsaga.nvim-install|
- Example Configuration |lspsaga.nvim-example-configuration|
- Using Lspsaga |lspsaga.nvim-using-lspsaga|
- Default options |lspsaga.nvim-default-options|
- :Lspsaga lsp_finder |lspsaga.nvim-:lspsaga-lsp_finder|
- :Lspsaga peek_definition |lspsaga.nvim-:lspsaga-peek_definition|
- :Lspsaga goto_definition |lspsaga.nvim-:lspsaga-goto_definition|
- :Lspsaga code_action |lspsaga.nvim-:lspsaga-code_action|
- :Lspsaga Lightbulb |lspsaga.nvim-:lspsaga-lightbulb|
- :Lspasga hover_doc |lspsaga.nvim-:lspasga-hover_doc|
- :Lspsaga diagnostic_jump_next |lspsaga.nvim-:lspsaga-diagnostic_jump_next|
- :Lspsaga show_diagnostics |lspsaga.nvim-:lspsaga-show_diagnostics|
- :Lspsaga rename |lspsaga.nvim-:lspsaga-rename|
- :Lspsaga outline |lspsaga.nvim-:lspsaga-outline|
- :Lspsaga incoming_calls / outgoing_calls|lspsaga.nvim-:lspsaga-incoming_calls-/-outgoing_calls|
- :Lspsaga symbols in winbar |lspsaga.nvim-:lspsaga-symbols-in-winbar|
- :Lspsaga symbols in a custom winbar/statusline|lspsaga.nvim-:lspsaga-symbols-in-a-custom-winbar/statusline|
- :Lspsaga term_toggle |lspsaga.nvim-:lspsaga-term_toggle|
- :Lspsaga beacon |lspsaga.nvim-:lspsaga-beacon|
- Customizing Lspsagas Appearance|lspsaga.nvim-customizing-lspsagas-appearance|
- :Lspsaga UI |lspsaga.nvim-:lspsaga-ui|
1. Custom Highlighting |lspsaga.nvim-custom-highlighting|
2. Custom Kind |lspsaga.nvim-custom-kind|
3. Donate |lspsaga.nvim-donate|
4. Backers |lspsaga.nvim-backers|
5. License |lspsaga.nvim-license|
>
__
/ /________ _________ _____ _____ _
/ / ___/ __ \/ ___/ __ `/ __ `/ __ `/
/ (__ ) /_/ (__ ) /_/ / /_/ / /_/ /
/_/____/ .___/____/\__,_/\__, /\__,_/
/_/ /____/
⚡ Designed for convenience and efficiency ⚡
<
Neovim lsp enhance plugin.
<https://matrix.to/#/#lspsaga-nvim:matrix.org>
1. |lspsaga.nvim-install|
2. |lspsaga.nvim-example-configuration|
3. |lspsaga.nvim-using-lspsaga|
4. |lspsaga.nvim-customizing-lspsagas-appearance|
5. |lspsaga.nvim-backers|
6. |lspsaga.nvim-donate|
7. |lspsaga.nvim-license|
INSTALL *lspsaga.nvim-install*
You can use plugin managers like `lazy.nvim` and `packer.nvim` to install
`lspsaga` and lazy load `lspsaga` using the plugin managers keyword for lazy
loading (`lazy` for `lazy.nvim` and `opt` for `packer.nvim`).
- `cmd` - Load `lspsaga` only when a `lspsaga` command is called.
- `ft` - `lazy.nvim` and `packer.nvim` both provide lazy loading by filetype.
This way, you can load `lspsaga` according to the filetypes that you use a LSP
in.
- `event` - Only load `lspsaga` on an event like `BufRead` or `BufReadPost`. Do
make sure that your LSP plugins, like lsp-zero
<https://github.com/VonHeikemen/lsp-zero.nvim> or lsp-config
<https://github.com/neovim/nvim-lspconfig>, are loaded before loading
`lspsaga`.
- `dependencies` - For `lazy.nvim` you can set `glepnir/lspsaga.nvim` as a
dependency of `nvim-lspconfig` using the `dependencies` keyword and vice versa.
For `packer.nvim` you should use `requires` as the keyword instead.
- `after` - For `packer.nvim` you can use `after` keyword to ensure `lspsaga`
only loads after your LSP plugins have loaded. This is not necessary for
`lazy.nvim`.
- Lazy <https://github.com/folke/lazy.nvim>
>lua
require("lazy").setup({
"glepnir/lspsaga.nvim",
event = "LspAttach",
config = function()
require("lspsaga").setup({})
end,
dependencies = {
{"nvim-tree/nvim-web-devicons"},
--Please make sure you install markdown and markdown_inline parser
{"nvim-treesitter/nvim-treesitter"}
}
}, opt)
<
- Packer <https://github.com/wbthomason/packer.nvim>
>lua
use({
"glepnir/lspsaga.nvim",
opt = true,
branch = "main",
event = "LspAttach",
config = function()
require("lspsaga").setup({})
end,
requires = {
{"nvim-tree/nvim-web-devicons"},
--Please make sure you install markdown and markdown_inline parser
{"nvim-treesitter/nvim-treesitter"}
}
})
<
EXAMPLE CONFIGURATION *lspsaga.nvim-example-configuration*
>lua
require("lazy").setup({
"glepnir/lspsaga.nvim",
event = "LspAttach",
config = function()
require("lspsaga").setup({})
end,
dependencies = { {"nvim-tree/nvim-web-devicons"} }
})
local keymap = vim.keymap.set
-- LSP finder - Find the symbol's definition
-- If there is no definition, it will instead be hidden
-- When you use an action in finder like "open vsplit",
-- you can use <C-t> to jump back
keymap("n", "gh", "<cmd>Lspsaga lsp_finder<CR>")
-- Code action
keymap({"n","v"}, "<leader>ca", "<cmd>Lspsaga code_action<CR>")
-- Rename all occurrences of the hovered word for the entire file
keymap("n", "gr", "<cmd>Lspsaga rename<CR>")
-- Rename all occurrences of the hovered word for the selected files
keymap("n", "gr", "<cmd>Lspsaga rename ++project<CR>")
-- Peek definition
-- You can edit the file containing the definition in the floating window
-- It also supports open/vsplit/etc operations, do refer to "definition_action_keys"
-- It also supports tagstack
-- Use <C-t> to jump back
keymap("n", "gp", "<cmd>Lspsaga peek_definition<CR>")
-- Go to definition
keymap("n","gd", "<cmd>Lspsaga goto_definition<CR>")
-- Peek type definition
-- You can edit the file containing the type definition in the floating window
-- It also supports open/vsplit/etc operations, do refer to "definition_action_keys"
-- It also supports tagstack
-- Use <C-t> to jump back
keymap("n", "gt", "<cmd>Lspsaga peek_type_definition<CR>")
-- Go to type definition
keymap("n","gt", "<cmd>Lspsaga goto_type_definition<CR>")
-- Show line diagnostics
-- You can pass argument ++unfocus to
-- unfocus the show_line_diagnostics floating window
keymap("n", "<leader>sl", "<cmd>Lspsaga show_line_diagnostics<CR>")
-- Show buffer diagnostics
keymap("n", "<leader>sb", "<cmd>Lspsaga show_buf_diagnostics<CR>")
-- Show workspace diagnostics
keymap("n", "<leader>sw", "<cmd>Lspsaga show_workspace_diagnostics<CR>")
-- Show cursor diagnostics
keymap("n", "<leader>sc", "<cmd>Lspsaga show_cursor_diagnostics<CR>")
-- Diagnostic jump
-- You can use <C-o> to jump back to your previous location
keymap("n", "[e", "<cmd>Lspsaga diagnostic_jump_prev<CR>")
keymap("n", "]e", "<cmd>Lspsaga diagnostic_jump_next<CR>")
-- Diagnostic jump with filters such as only jumping to an error
keymap("n", "[E", function()
require("lspsaga.diagnostic"):goto_prev({ severity = vim.diagnostic.severity.ERROR })
end)
keymap("n", "]E", function()
require("lspsaga.diagnostic"):goto_next({ severity = vim.diagnostic.severity.ERROR })
end)
-- Toggle outline
keymap("n","<leader>o", "<cmd>Lspsaga outline<CR>")
-- Hover Doc
-- If there is no hover doc,
-- there will be a notification stating that
-- there is no information available.
-- To disable it just use ":Lspsaga hover_doc ++quiet"
-- Pressing the key twice will enter the hover window
keymap("n", "K", "<cmd>Lspsaga hover_doc<CR>")
-- If you want to keep the hover window in the top right hand corner,
-- you can pass the ++keep argument
-- Note that if you use hover with ++keep, pressing this key again will
-- close the hover window. If you want to jump to the hover window
-- you should use the wincmd command "<C-w>w"
keymap("n", "K", "<cmd>Lspsaga hover_doc ++keep<CR>")
-- Call hierarchy
keymap("n", "<Leader>ci", "<cmd>Lspsaga incoming_calls<CR>")
keymap("n", "<Leader>co", "<cmd>Lspsaga outgoing_calls<CR>")
-- Floating terminal
keymap({"n", "t"}, "<A-d>", "<cmd>Lspsaga term_toggle<CR>")
<
USING LSPSAGA *lspsaga.nvim-using-lspsaga*
**Note that the title in the floating window requires Neovim 0.9 or greater.**
**If you are using Neovim 0.8 you wont see a title.**
**If you are using Neovim 0.9 and want to disable the title, see
|lspsaga.nvim-customizing-lspsagas-appearance|
**You need not copy all of the options into the setup function. Just set the
options that youve changed in the setup function and it will be extended
with the default options!**
You can find the documentation for Lspsaga in Neovim by using `:h lspsaga`.
DEFAULT OPTIONS *lspsaga.nvim-default-options*
The top-level default options (command-specific default options below):
>lua
preview = {
lines_above = 0,
lines_below = 10,
},
scroll_preview = {
scroll_down = "<C-f>",
scroll_up = "<C-b>",
},
request_timeout = 2000,
<
Example setup using default options:
>lua
require("lspsaga").setup({
preview = {
lines_above = 0,
lines_below = 10,
},
scroll_preview = {
scroll_down = "<C-f>",
scroll_up = "<C-b>",
},
request_timeout = 2000,
-- See Customizing Lspsaga's Appearance
ui = { ... },
-- For default options for each command, see below
finder = { ... },
code_action = { ... }
-- etc.
})
<
:LSPSAGA LSP_FINDER *lspsaga.nvim-:lspsaga-lsp_finder*
A `finder` to show the definition, reference and implementation (only shown
when current hovered word is a function, a type, a class, or an interface).
Default options:
>lua
finder = {
max_height = 0.5,
min_width = 30,
force_max_height = false,
keys = {
jump_to = 'p',
expand_or_jump = 'o',
vsplit = 's',
split = 'i',
tabe = 't',
tabnew = 'r',
quit = { 'q', '<ESC>' },
close_in_preview = '<ESC>',
},
},
<
- `max_height` of the finder window.
- `force_max_height` force window height to max_height
- `keys.jump_to` finder peek window.
- `close_in_preview` will close all finder window in when you in preview window.
- `min_width` is finder preview window min width.
lsp_finder showcase ~
:LSPSAGA PEEK_DEFINITION *lspsaga.nvim-:lspsaga-peek_definition*
There are two commands, `:Lspsaga peek_definition` and `:Lspsaga
goto_definition`. The `peek_definition` command works like the VSCode command
of the same name, which shows the target file in an editable floating window.
Default options:
>lua
definition = {
edit = "<C-c>o",
vsplit = "<C-c>v",
split = "<C-c>i",
tabe = "<C-c>t",
quit = "q",
}
<
peek_definition showcase ~
The steps demonstrated in this showcase are:
- Pressing `gp` to run `:Lspsaga peek_definition`
- Editing a comment and using `:w` to save
- Pressing `<C-c>o` to jump to the file in the floating window
- Lspsaga shows a beacon highlight after jumping to the file
:LSPSAGA GOTO_DEFINITION *lspsaga.nvim-:lspsaga-goto_definition*
Jumps to the definition of the hovered word and shows a beacon highlight.
:LSPSAGA CODE_ACTION *lspsaga.nvim-:lspsaga-code_action*
Default options:
>lua
code_action = {
num_shortcut = true,
show_server_name = false,
extend_gitsigns = true,
keys = {
-- string | table type
quit = "q",
exec = "<CR>",
},
},
<
- `num_shortcut` - It is `true` by default so you can quickly run a code action by pressing its corresponding number.
- `extend_gitsigns` show gitsings in code action.
code_action showcase ~
The steps demonstrated in this showcase are:
- Pressing `ga` to run `:Lspsaga code_action`
- Pressing `j` to move within the code action preview window
- Pressing `<Cr>` to run the action
:LSPSAGA LIGHTBULB *lspsaga.nvim-:lspsaga-lightbulb*
When there are possible code actions to be taken, a lightbulb icon will be
shown.
Default options:
>lua
lightbulb = {
enable = true,
enable_in_insert = true,
sign = true,
sign_priority = 40,
virtual_text = true,
},
<
lightbulb showcase ~
:LSPASGA HOVER_DOC *lspsaga.nvim-:lspasga-hover_doc*
default options
>lua
hover = {
max_width = 0.6,
open_link = 'gx',
open_browser = '!chrome',
},
<
you can use `open_link` key to open a http link or a file link in hover doc
window. the `open_browser` is `chrome` in default you need config it to your
browser
You need install the treesitter
<https://github.com/nvim-treesitter/nvim-treesitter> markdown and
markdown_inline parser. Lspsaga can use it to render the hover window. You can
press the keyboard shortcut for `:Lspsaga hover_doc` twice to enter the hover
window.
if you got something wrong in hover please run `:checkhealth`
hover_docshow case ~
The steps demonstrated in this showcase are:
- Pressing `K` once to run `:Lspsaga hover_doc`
- Pressing `K` again to enter the hover window
- Pressing `q` to quit
:LSPSAGA DIAGNOSTIC_JUMP_NEXT *lspsaga.nvim-:lspsaga-diagnostic_jump_next*
Jumps to next diagnostic position and show a beacon highlight. Lspsaga will
then show the code actions.
Default options:
>lua
diagnostic = {
on_insert = false,
on_insert_follow = false,
insert_winblend = 0,
show_code_action = true,
show_source = true,
jump_num_shortcut = true,
max_width = 0.7,
max_height = 0.6,
max_show_width = 0.9,
max_show_height = 0.6,
text_hl_follow = true,
border_follow = true,
extend_relatedInformation = false,
keys = {
exec_action = 'o',
quit = 'q',
expand_or_jump = '<CR>',
quit_in_show = { 'q', '<ESC>' },
},
},
<
- `jump_num_shortcut` - The default is `true`. After jumping, Lspasga will automatically bind code actions to a number. Afterwards, you can press the number to execute the code action. After the floating window is closed, these numbers will no longer be tied to the same code actions.
- `show_codeaction` default is true it will show available actions in the diagnsotic jump window
- `show_source` default is true extend `source` into the diagnostic message
- `max_width` is the max width for diagnostic jump window. percentage
- `max_height` is the max height of diagnostic jump window percentage
- `text_hl_follow` is false default true that you can define `DiagnostcText` to custom the diagnotic
text color
- `border_follow` the border highlight will follow the diagnostic type. if false it will use the
highlight `DiagnosticBorder`.
- `on_insert` default is true it works like the emacs helix show diagnostic in right but in line.
- `on_insert_follow` true will follow current line. false will on top right
- `insert_winblend` default is 0, when its to 100 will completely transparent. the color will
changed a little light. 0 will use the `NormalFloat` group. it will link to `Normal` by Lspsaga.
- `max_show_width` is the width of show diagnostic window
- `max_show_height` is the height of show diagnostic widnow
- `extend_relatedInformation` default is false when is true it will extend this message into
diagnostic message
You can also use a filter when using diagnostic jump by using a Lspsaga
function. The function takes a table as its argument. It is functionally
identical to `:h vim.diagnostic.get_next`.
>lua
-- This will only jump to an error
-- If no error is found, it executes "goto_next"
require("lspsaga.diagnostic"):goto_prev({ severity = vim.diagnostic.severity.ERROR })
<
showcase ~
The steps demonstrated in this showcase are:
- Pressing `[e` to jump to the next diagnostic position, which shows the beacon highlight and the code actions in a diagnostic window
- Use `scroll_in_preview` keys to show action preview.
- Pressing the number `2` to execute the code action without needing to enter the floating window
- If you want to see the code action, you can use `<C-w>w` to enter the floating window.
- Press `g` to go to the action line and see the code action preview.
- Press `o` to execute the action.
`on_insert` is true, `on_insert_follow` is false
`on_insert_follow` is true
:LSPSAGA SHOW_DIAGNOSTICS *lspsaga.nvim-:lspsaga-show_diagnostics*
`show_line_diagnostics`, `show_buf_diagnostics`, `show_workspace_diagnostics`
`show_cursor_diagnsotics`. and support an argument `++unfocus` to make it
unfocus. like `:Lspsaga show_workspace_diagnostics ++unfocus` you can press the
`expand_or_jump` key to expand on fname line or jump into location on message
line.
show_diagnostics showcase ~
:LSPSAGA RENAME *lspsaga.nvim-:lspsaga-rename*
Uses the current LSP to rename the hovered word.
Default options:
>lua
rename = {
quit = "<C-c>",
exec = "<CR>",
mark = "x",
confirm = "<CR>",
in_select = true,
},
<
- `mark` is used for the `++project` argument. It is used to mark the files which you want to rename the hovered word in.
- `confirm` - After you have marked the files, press this key to execute the rename.
rename showcase ~
The steps demonstrated in this showcase are:
- Pressing `gr` to run `:Lspsaga rename`
- Typing `stesdd` and then pressing `<CR>` to execute the rename
The steps demonstrated in this showcase are:
- Pressing `gR` to run `:Lspsaga rename ++project`
- Pressing `x` to mark the file
- Pressing `<CR>` to execute rename
:LSPSAGA OUTLINE *lspsaga.nvim-:lspsaga-outline*
Default options:
>lua
outline = {
win_position = "right",
win_with = "",
win_width = 30,
preview_width= 0.4,
show_detail = true,
auto_preview = true,
auto_refresh = true,
auto_close = true,
auto_resize = false,
custom_sort = nil,
keys = {
expand_or_jump = 'o',
quit = "q",
},
},
<
outline showcase ~
The steps demonstrated in this showcase are:
- Pressing `<Leader>o` run `:Lspsaga outline`
:LSPSAGA INCOMING_CALLS / OUTGOING_CALLS*lspsaga.nvim-:lspsaga-incoming_calls-/-outgoing_calls*
Runs the LSPs callhierarchy/incoming_calls.
Default options:
>lua
callhierarchy = {
show_detail = false,
keys = {
edit = "e",
vsplit = "s",
split = "i",
tabe = "t",
jump = "o",
quit = "q",
expand_collapse = "u",
},
},
<
incoming_calls showcase ~
:LSPSAGA SYMBOLS IN WINBAR *lspsaga.nvim-:lspsaga-symbols-in-winbar*
This requires Neovim version >= 0.8.
Default options:
>lua
symbol_in_winbar = {
enable = true,
separator = " ",
ignore_patterns={},
hide_keyword = true,
show_file = true,
folder_level = 2,
respect_root = false,
color_mode = true,
},
<
- `hide_keyword` - The default value is `true`. Lspsaga will hide some keywords and temporary variables to make the symbols look cleaner.
- `folder_level` only works when `show_file` is `true`.
- `respect_root` will respect the LSPs root. If this is `true`, Lspsaga will ignore the `folder_level` option. If no LSP client is being used, Lspsaga will fall back to using folder level.
- `color_mode` - The default value is `true`. When it is set to `false`, only icons will have color.
- `ignore_patterns` table type when fileanme matched the pattern will ignore render symbols. if
show_file is true. the file name will still set.
Symbols in winbar ~
:LSPSAGA SYMBOLS IN A CUSTOM WINBAR/STATUSLINE*lspsaga.nvim-:lspsaga-symbols-in-a-custom-winbar/statusline*
Lspsaga provides an API that you can use in your custom winbar or statusline.
>lua
vim.wo.winbar / vim.wo.stl = require('lspsaga.symbolwinbar'):get_winbar()
<
:LSPSAGA TERM_TOGGLE *lspsaga.nvim-:lspsaga-term_toggle*
A simple floating terminal.
Toggling the floating terminal ~
:LSPSAGA BEACON *lspsaga.nvim-:lspsaga-beacon*
after jump from float window there will show beacon to remind you where the
cursor is.
>lua
beacon = {
enable = true,
frequency = 7,
},
<
`frequency` the blink frequency.
CUSTOMIZING LSPSAGAS APPEARANCE*lspsaga.nvim-customizing-lspsagas-appearance*
:LSPSAGA UI *lspsaga.nvim-:lspsaga-ui*
Default UI options
>lua
ui = {
-- This option only works in Neovim 0.9
title = true,
-- Border type can be single, double, rounded, solid, shadow.
border = "single",
winblend = 0,
expand = "",
collapse = "",
code_action = "💡",
incoming = " ",
outgoing = " ",
hover = ' ',
kind = {},
},
<
==============================================================================
1. Custom Highlighting *lspsaga.nvim-custom-highlighting*
All highlight groups can be found in highlight.lua
<./lua/lspsaga/highlight.lua>.
`require('lspsaga.lspkind').get_kind_group()` will return all the SagaWinbar +
kind name group. also include `SagaWinbarFileName SagaWinbarFileIcon
SagaWinbarFolderName SagaWinbarSep`. These groups are special. so if you want
use this api to custom the highlight. you need dealwith these 4 groups the last
item is `SagaWinbarSep`.
==============================================================================
2. Custom Kind *lspsaga.nvim-custom-kind*
Modify `ui.kind` to change the icons of the kinds.
All kinds used in Lspsaga are defined in lspkind.lua
<./lua/lspsaga/lspkind.lua>. The key in `ui.kind` is the kind name, and the
value can either be a string or a table. If a string is passed, it is setting
the `icon`. If table is passed, it will be passed as `{ icon, highlight group
}`, for example, to change the a folders icon color, you could do this: `ui
= { kind = { ["Folder"] = { " ", "@comment" }, }, },`.
==============================================================================
3. Donate *lspsaga.nvim-donate*
Currently, I am in need of some donations. If youd like to support my work
financially, please donate through Github Sponsor button or PayPal
<https://paypal.me/bobbyhub>. Thanks! <https://paypal.me/bobbyhub>
==============================================================================
4. Backers *lspsaga.nvim-backers*
Thanks for everyone!
@Tuo Huang <https://github.com/youngtuotuo> @Scott Ming
<https://github.com/scottming> @Möller Lukas <https://github.com/lmllrjr>
@HendrikPetertje <https://github.com/HendrikPetertje> @Bojan Wilytsch
<https://github.com/bwilytsch> @zhourrr <https://github.com/zhourrr> @Burgess
Darrion <https://github.com/ca-mantis-shrimp> @Ceserani Alessandro
<https://github.com/al-ce>
==============================================================================
5. License *lspsaga.nvim-license*
Licensed under the MIT <./LICENSE> license.
==============================================================================
6. Links *lspsaga.nvim-links*
1. **: https://img.shields.io/badge/Element-0DBD8B?style=for-the-badge&logo=element&logoColor=white
2. **: https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white
3. *@Tuo*:
4. *@Scott*:
5. *@Möller*:
6. *@HendrikPetertje*:
7. *@Bojan*:
8. *@zhourrr*:
9. *@Burgess*:
10. *@Ceserani*:
Generated by panvimdoc <https://github.com/kdheepak/panvimdoc>
vim:tw=78:ts=8:noet:ft=help:norl:

62
lua/lspsaga/beacon.lua Normal file
View file

@ -0,0 +1,62 @@
local uv = vim.version().minor >= 10 and vim.uv or vim.loop
local api = vim.api
local config = require('lspsaga').config
local win = require('lspsaga.window')
local function jump_beacon(bufpos, width)
if not config.beacon.enable then
return
end
if width == 0 or not width then
return
end
local float_opt = {
relative = 'win',
bufpos = bufpos,
height = 1,
width = width,
row = 0,
col = 0,
anchor = 'NW',
focusable = false,
noautocmd = true,
border = 'none',
}
local _, winid = win
:new_float(float_opt, false, true)
:bufopt({
['filetype'] = 'beacon',
['bufhidden'] = 'wipe',
['buftype'] = 'nofile',
})
:winopt('winhl', 'NormalFloat:SagaBeacon')
:wininfo()
local timer = uv.new_timer()
timer:start(
0,
60,
vim.schedule_wrap(function()
if not api.nvim_win_is_valid(winid) then
return
end
local blend = vim.wo[winid].winblend + config.beacon.frequency
if blend > 100 then
blend = 100
end
vim.wo[winid].winblend = blend
if vim.wo[winid].winblend == 100 and not timer:is_closing() then
timer:stop()
timer:close()
api.nvim_win_close(winid, true)
end
end)
)
end
return {
jump_beacon = jump_beacon,
}

View file

@ -1,11 +1,15 @@
---@diagnostic disable-next-line: deprecated
local api, fn, lsp, uv = vim.api, vim.fn, vim.lsp, vim.loop
local config = require('lspsaga').config
local libs = require('lspsaga.libs')
local window = require('lspsaga.window')
local util = require('lspsaga.util')
local call_conf, ui = config.callhierarchy, config.ui
local ctx = {}
local slist = require('lspsaga.slist')
local buf_set_lines = api.nvim_buf_set_lines
local buf_set_extmark = api.nvim_buf_set_extmark
local kind = require('lspsaga.lspkind').kind
local ly = require('lspsaga.layout')
local win = require('lspsaga.window')
local beacon = require('lspsaga.beacon').jump_beacon
local ns = api.nvim_create_namespace('SagaCallhierarchy')
local ch = {}
ch.__index = ch
@ -14,9 +18,22 @@ function ch.__newindex(t, k, v)
rawset(t, k, v)
end
local function clean_ctx()
for k, _ in pairs(ctx) do
ctx[k] = nil
function ch:clean()
ly:close()
slist.list_map(self.list, function(node)
if node.value.wipe then
api.nvim_buf_delete(node.value.bufnr, { force = true })
return
end
if node.value.bufnr and api.nvim_buf_is_valid(node.value.bufnr) and node.value.rendered then
api.nvim_buf_del_keymap(node.value.bufnr, 'n', config.finder.keys.close)
end
end)
for key, _ in pairs(self) do
if type(key) ~= 'function' then
self[key] = nil
end
end
end
@ -49,156 +66,314 @@ local function pick_call_hierarchy_item(call_hierarchy_items)
return choice
end
---@private
function ch:call_hierarchy(item, parent)
function ch:spinner(node)
if not node then
return
end
local spinner = { '', '', '', '', '', '', '', '' }
local client = self.client
local frame = 0
local curline = api.nvim_win_get_cursor(0)[1]
if self.bufnr and api.nvim_buf_is_loaded(self.bufnr) and parent then
local timer = uv.new_timer()
local frame = 1
local timer = uv.new_timer()
if self.left_bufnr and api.nvim_buf_is_loaded(self.left_bufnr) then
timer:start(
0,
50,
vim.schedule_wrap(function()
local text = api.nvim_get_current_line()
local replace_icon = text:find(ui.expand) and ui.expand or ui.collapse
if self.pending_request then
self.pending_request = false
local next = frame + 1 == 9 and 1 or frame + 1
if text:find(replace_icon) then
text = text:gsub(replace_icon, spinner[next])
else
text = text:gsub(spinner[frame], spinner[next])
end
local col_start = text:find(spinner[next])
vim.bo[self.bufnr].modifiable = true
api.nvim_buf_set_lines(self.bufnr, curline - 1, curline, false, { text })
frame = frame + 1 == 9 and 1 or frame + 1
api.nvim_buf_add_highlight(
self.bufnr,
0,
'FinderSpinner',
curline - 1,
col_start,
col_start + #spinner[next]
)
if parent then
for group, scope in pairs(parent.highlights) do
if not group:find('Saga') then
api.nvim_buf_add_highlight(self.bufnr, 0, group, curline - 1, scope[1], scope[2])
break
end
end
end
end
if not self.pending_request and not timer:is_closing() then
timer:stop()
timer:close()
text = text:gsub(spinner[frame], replace_icon)
if vim.bo[self.bufnr].modifiable then
api.nvim_buf_set_lines(self.bufnr, curline - 1, curline, false, { text })
end
vim.bo[self.bufnr].modifiable = false
self.pending_request = false
end
vim.bo[self.left_bufnr].modifiable = true
local col = node.value.winline == 1 and 0 or node.value.inlevel - 4
buf_set_extmark(self.left_bufnr, ns, node.value.winline - 1, col, {
id = node.value.virtid,
virt_text = { { spinner[frame], 'SagaSpinner' } },
virt_text_pos = 'overlay',
hl_mode = 'combine',
})
frame = frame + 1 > #spinner and 1 or frame + 1
end)
)
end
return timer
end
function ch:set_toggle_icon(icon, row, col, virtid)
buf_set_extmark(self.left_bufnr, ns, row, col, {
id = virtid,
virt_text = { { icon, 'SagaToggle' } },
virt_text_pos = 'overlay',
})
end
function ch:set_data_icon(curlnum, data, col)
buf_set_extmark(self.left_bufnr, ns, curlnum, col, {
virt_text = { { kind[data.kind][2], 'Saga' .. kind[data.kind][3] } },
virt_text_pos = 'overlay',
})
end
function ch:toggle_or_request()
if self.pending_request then
vim.notify(('[Lspsaga] already have a request for %s'):format(self.method), vim.log.levels.WARN)
return
end
local curlnum = api.nvim_win_get_cursor(0)[1]
local curnode = slist.find_node(self.list, curlnum)
if not curnode then
return
end
local client = vim.lsp.get_client_by_id(curnode.value.client_id)
local next = curnode.next
if not next or next.value.inlevel <= curnode.value.inlevel then
local timer = self:spinner(curnode)
local item = self.method == get_method(2) and curnode.value.from or curnode.value.to
self:call_hierarchy(item, client, timer, curlnum)
return
end
local level = curnode.value.inlevel
if curnode.value.expand == true then
local row = curlnum
while true do
row = row + 1
local l = fn.indent(row)
if l <= level or l == -1 then
break
end
end
local count = row - curlnum - 1
self:set_toggle_icon(
config.ui.expand,
curlnum - 1,
curnode.value.inlevel - 4,
curnode.value.virtid
)
vim.bo[self.left_bufnr].modifiable = true
buf_set_lines(self.left_bufnr, curlnum, curlnum + count, false, {})
vim.bo[self.left_bufnr].modifiable = false
curnode.value.expand = false
slist.update_winline(curnode, -count)
return
end
if curnode.value.expand == false then
curnode.value.expand = true
self:set_toggle_icon(
config.ui.collapse,
curlnum - 1,
curnode.value.inlevel - 4,
curnode.value.virtid
)
local tmp = curnode.next
local count = 0
vim.bo[self.left_bufnr].modifiable = true
while tmp do
local data = self.method == get_method(2) and tmp.value.from or tmp.value.to
local indent = (' '):rep(tmp.value.inlevel)
buf_set_lines(self.left_bufnr, curlnum, curlnum, false, { indent .. data.name })
self:set_toggle_icon(config.ui.expand, curlnum, #indent - 4, tmp.value.virtid)
self:set_data_icon(curlnum, data, #indent - 2)
self:render_virtline(curlnum, tmp.value.inlevel)
curlnum = curlnum + 1
tmp.value.winline = curlnum
if tmp.value.expand == false then
tmp.value.expand = true
end
count = count + 1
if not tmp or (tmp.next and tmp.next.value.inlevel <= level) then
break
end
tmp = tmp.next
end
vim.bo[self.left_bufnr].modifiable = false
if tmp then
slist.update_winline(tmp, count)
end
end
end
local function window_shuttle(winid, right_winid)
local curwin = api.nvim_get_current_win()
local target
if curwin == winid then
target = right_winid
elseif curwin == right_winid then
target = winid
end
if target then
api.nvim_set_current_win(target)
end
end
function ch:keymap()
util.map_keys(self.left_bufnr, config.callhierarchy.keys.close, function()
util.close_win({ self.left_winid, self.right_winid })
self:clean()
end)
util.map_keys(self.left_bufnr, config.callhierarchy.keys.toggle_or_req, function()
self:toggle_or_request()
end)
util.map_keys(self.left_bufnr, config.callhierarchy.keys.shuttle, function()
window_shuttle(self.left_winid, self.right_winid)
end)
local tbl = { 'edit', 'vsplit', 'split', 'tabe' }
for _, action in ipairs(tbl) do
util.map_keys(self.left_bufnr, config.callhierarchy.keys[action], function()
local curlnum = api.nvim_win_get_cursor(0)[1]
local curnode = slist.find_node(self.list, curlnum)
if not curnode then
return
end
local data = self.method == get_method(2) and curnode.value.from or curnode.value.to
local fname = vim.uri_to_fname(data.uri)
local pos = { data.selectionRange.start.line + 1, data.selectionRange.start.character }
self:clean()
local restore = win:minimal_restore()
vim.cmd[action](fname)
restore()
api.nvim_win_set_cursor(0, pos)
beacon({ pos[1], 0 }, #api.nvim_get_current_line())
end)
end
end
function ch:peek_view()
api.nvim_create_autocmd('CursorMoved', {
group = api.nvim_create_augroup('SagaCallhierarchy', { clear = true }),
buffer = self.left_bufnr,
callback = function()
if not self.left_winid or not api.nvim_win_is_valid(self.left_winid) then
return
end
local curlnum = api.nvim_win_get_cursor(self.left_winid)[1]
local curnode = slist.find_node(self.list, curlnum)
if not curnode then
return
end
local data = self.method == get_method(2) and curnode.value.from or curnode.value.to
curnode.value.bufnr = vim.uri_to_bufnr(data.uri)
if not api.nvim_buf_is_loaded(curnode.value.bufnr) then
fn.bufload(curnode.value.bufnr)
curnode.value.wipe = true
end
local range = data.selectionRange
api.nvim_win_set_buf(self.right_winid, curnode.value.bufnr)
curnode.value.rendered = true
vim.bo[curnode.value.bufnr].filetype = vim.bo[self.main_buf].filetype
api.nvim_win_set_cursor(self.right_winid, { range.start.line + 1, range.start.character + 1 })
util.map_keys(curnode.value.bufnr, config.callhierarchy.keys.shuttle, function()
window_shuttle(self.left_winid, self.right_winid)
end)
util.map_keys(curnode.value.bufnr, config.callhierarchy.keys.close, function()
ly:close()
self:clean()
end)
end,
desc = '[Lspsaga] callhierarchy peek preview',
})
end
function ch:render_virtline(row, inlevel)
for i = 1, inlevel - 4, 2 do
local virt = {}
if i + 2 > inlevel - 4 then
virt = {
{ config.ui.lines[2], 'SagaVirtLine' },
{ config.ui.lines[4], 'SagaVirtLine' },
}
else
virt = {
{ config.ui.lines[3], 'SagaVirtLine' },
}
end
buf_set_extmark(self.left_bufnr, ns, row, i - 1, {
virt_text = virt,
virt_text_pos = 'overlay',
})
end
end
function ch:call_hierarchy(item, client, timer, curlnum)
self.pending_request = true
client.request(self.method, { item = item }, function(_, res)
self.pending_request = false
curlnum = curlnum or 0
local inlevel = curlnum == 0 and 2 or fn.indent(curlnum)
local curnode = slist.find_node(self.list, curlnum)
if curnode and timer and timer:is_active() then
local icon = (res and #res > 0) and config.ui.expand or config.ui.collapse
self:set_toggle_icon(icon, curlnum - 1, curnode.value.inlevel - 4, curnode.value.virtid)
timer:stop()
timer:close()
end
if not res or vim.tbl_isempty(res) then
return
end
if not self.left_winid or not api.nvim_win_is_valid(self.left_winid) then
local height = bit.rshift(vim.o.lines, 1) - 4
self.left_bufnr, self.left_winid, self.right_bufnr, self.right_winid = ly:new(self.layout)
:left(height, 20)
:bufopt({
['filetype'] = 'sagacallhierarchy',
['buftype'] = 'nofile',
['bufhidden'] = 'wipe',
})
:right(20)
:bufopt({
['buftype'] = 'nofile',
['bufhidden'] = 'wipe',
})
:done()
self:peek_view()
self:keymap()
end
local kind = require('lspsaga.lspkind').get_kind()
if not parent then
local icons = {}
for i, v in pairs(res) do
local target = v.from and v.from or v.to
icons[#icons + 1] = kind[target.kind]
local expand_collapse = ' ' .. ui.expand
local icon = kind[target.kind][2]
local indent = (' '):rep(inlevel + 2)
-- Variable can be used to append name of class to the respective method in the outgoing preview popup.
local classNamePart = ''
if curnode then
curnode.value.expand = true
self:set_toggle_icon(config.ui.collapse, curlnum - 1, inlevel - 4, curnode.value.virtid)
end
local tmp = curnode
vim.bo[self.left_bufnr].modifiable = true
-- Class name resolution for Java
if vim.bo[self.main_buf].filetype == 'java' then
local projectClassPattern = '.+/([^/]+)[.]java'
local jdtClassPattern = '/([^/?=]+)[.]class'
local projectClass = target.uri:match(projectClassPattern)
local jdtClass = target.uri:match(jdtClassPattern)
local className = projectClass or jdtClass
if className ~= nil then
classNamePart = ' @ ' .. (className and className or '')
end
end
self.data[#self.data + 1] = {
target = target,
name = expand_collapse .. icon .. target.name .. classNamePart,
highlights = {
['SagaCollapse'] = { 0, #expand_collapse },
['SagaWinbar' .. kind[target.kind][1]] = { #expand_collapse, #expand_collapse + #icon },
},
winline = i + 1,
expand = false,
children = {},
requested = false,
}
for _, val in ipairs(res) do
local data = self.method == get_method(2) and val.from or val.to
val.client_id = client.id
val.inlevel = #indent
buf_set_lines(
self.left_bufnr,
curlnum,
curlnum == 0 and -1 or curlnum,
false,
{ indent .. data.name }
)
val.virtid = uv.hrtime()
self:set_toggle_icon(config.ui.expand, curlnum, #indent - 4, val.virtid)
self:set_data_icon(curlnum, data, #indent - 2)
if curlnum ~= 0 then
self:render_virtline(curlnum, #indent)
else
api.nvim_win_set_cursor(self.left_winid, { 1, 4 })
end
self:render_win()
return
end
vim.bo.modifiable = true
parent.requested = true
parent.expand = true
parent.name = parent.name:gsub(ui.expand, ui.collapse)
api.nvim_buf_set_lines(self.bufnr, parent.winline - 1, parent.winline, false, {
parent.name,
})
local _, level = parent.name:find('%s+')
local indent = string.rep(' ', level + 1)
local tbl = {}
for i, v in pairs(res) do
local target = v.from and v.from or v.to
local expand_collapse = indent .. ui.expand
local icon = kind[target.kind][2]
parent.children[#parent + 1] = {
target = target,
name = expand_collapse .. icon .. target.name,
highlights = {
['SagaCollapse'] = { 0, #expand_collapse },
['SagaWinbar' .. kind[target.kind][1]] = { #expand_collapse, #expand_collapse + #icon },
},
winline = parent.winline + i,
expand = false,
children = {},
requested = false,
}
tbl[#tbl + 1] = expand_collapse .. icon .. target.name
end
api.nvim_buf_set_lines(self.bufnr, parent.winline, parent.winline, false, tbl)
for group, scope in pairs(parent.highlights) do
api.nvim_buf_add_highlight(self.bufnr, 0, group, parent.winline - 1, scope[1], scope[2])
end
for _, v in pairs(parent.children) do
for group, scopes in pairs(v.highlights) do
api.nvim_buf_add_highlight(self.bufnr, 0, group, v.winline - 1, scopes[1], scopes[2])
curlnum = curlnum + 1
val.winline = curlnum
if not curnode then
slist.tail_push(self.list, val)
else
slist.insert_node(curnode, val)
curnode = curnode.next
end
end
vim.bo.modifiable = false
self:change_node_winline(parent, #res)
vim.bo[self.left_bufnr].modifiable = false
if curnode and curnode.next then
slist.update_winline(curnode, #res)
end
end)
end
@ -208,397 +383,47 @@ function ch:send_prepare_call()
return
end
self.main_buf = api.nvim_get_current_buf()
local clients = util.get_client_by_method(get_method(1))
if #clients == 0 then
vim.notify('[Lspsaga] all clients of this buffer not support callhierarchy')
return
end
local client
if #clients == 1 then
client = clients[1]
else
local client_name = vim.tbl_map(function(item)
return item.name
end, clients)
local choice = vim.fn.inputlist('select client:', unpack(client_name))
if choice == 0 or choice > #clients then
api.nvim_err_writeln('[Lspsaga] wrong choice for select client')
return
end
client = clients[choice]
end
self.list = slist.new()
local params = lsp.util.make_position_params()
lsp.buf_request(0, get_method(1), params, function(_, result, data)
local call_hierarchy_item = pick_call_hierarchy_item(result)
self.client = lsp.get_client_by_id(data.client_id)
self:call_hierarchy(call_hierarchy_item)
end)
end
function ch:expand_collapse()
local node = self:get_node_at_cursor()
if not node then
return
end
if not node.expand then
if not node.requested and not self.pending_request then
self:call_hierarchy(node.target, node)
else
node.name = node.name:gsub(ui.expand, ui.collapse)
node.highlights['SagaCollapse'] = { unpack(node.highlights['SagaExpand']) }
node.highlights['SagaExpand'] = nil
vim.bo.modifiable = true
api.nvim_buf_set_lines(self.bufnr, node.winline - 1, node.winline, false, {
node.name,
})
local tbl = {}
for i, v in ipairs(node.children) do
v.winline = node.winline + i
tbl[#tbl + 1] = v.name
end
node.expand = true
api.nvim_buf_set_lines(self.bufnr, node.winline, node.winline, false, tbl)
for group, scope in pairs(node.highlights) do
api.nvim_buf_add_highlight(self.bufnr, 0, group, node.winline - 1, scope[1], scope[2])
end
vim.bo.modifiable = false
for _, child in pairs(node.children) do
for group, scope in pairs(child.highlights) do
api.nvim_buf_add_highlight(self.bufnr, 0, group, child.winline - 1, scope[1], scope[2])
end
end
self:change_node_winline(node, #node.children)
client.request(get_method(1), params, function(_, result, ctx)
if api.nvim_get_current_buf() ~= ctx.bufnr then
return
end
return
end
local cur_line = api.nvim_win_get_cursor(0)[1]
local text = api.nvim_get_current_line()
text = text:gsub(ui.collapse, ui.expand)
vim.bo[self.bufnr].modifiable = true
api.nvim_buf_set_lines(self.bufnr, cur_line - 1, cur_line + #node.children, false, { text })
node.expand = false
vim.bo[self.bufnr].modifiable = false
node.highlights['SagaExpand'] = { unpack(node.highlights['SagaCollapse']) }
node.highlights['SagaCollapse'] = nil
for group, scope in pairs(node.highlights) do
api.nvim_buf_add_highlight(self.bufnr, 0, group, cur_line - 1, scope[1], scope[2])
end
for _, v in pairs(node.children) do
v.winline = -1
end
self:change_node_winline(node, -#node.children)
local item = pick_call_hierarchy_item(result)
self:call_hierarchy(item, client)
end, self.main_buf)
end
function ch:apply_map()
local opts = { nowait = true }
util.map_keys(self.bufnr, 'n', call_conf.keys.quit, function()
if self.winid and api.nvim_win_is_valid(self.winid) then
api.nvim_win_close(self.winid, true)
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
api.nvim_win_close(self.preview_winid, true)
end
clean_ctx()
end
end, opts)
util.map_keys(self.bufnr, 'n', call_conf.keys.expand_collapse, function()
self:expand_collapse()
end, opts)
util.map_keys(self.bufnr, 'n', call_conf.keys.jump, function()
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
local node = self:get_node_at_cursor()
if not node then
return
end
api.nvim_set_current_win(self.preview_winid)
api.nvim_win_set_cursor(
self.preview_winid,
{ node.target.selectionRange.start.line + 1, node.target.selectionRange.start.character }
)
end
end, opts)
for action, keys in pairs({
edit = call_conf.keys.edit,
vsplit = call_conf.keys.vsplit,
split = call_conf.keys.split,
tabe = call_conf.keys.tabe,
}) do
util.map_keys(self.bufnr, 'n', keys, function()
local node = self:get_node_at_cursor()
if not node then
return
end
if api.nvim_win_is_valid(self.winid) then
api.nvim_win_close(self.winid, true)
end
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
api.nvim_win_close(self.preview_winid, true)
end
vim.cmd(action .. ' ' .. vim.uri_to_fname(node.target.uri))
api.nvim_win_set_cursor(
0,
{ node.target.selectionRange.start.line + 1, node.target.selectionRange.start.character }
)
local width = #api.nvim_get_current_line()
libs.jump_beacon({ node.target.selectionRange.start.line, 0 }, width)
clean_ctx()
end, opts)
function ch:send_method(t, args)
self.method = get_method(t)
self.layout = config.callhierarchy.layout
if vim.tbl_contains(args, '++normal') then
self.layout = 'normal'
elseif vim.tbl_contains(args, '++float') then
self.layout = 'float'
end
end
function ch:render_win()
local content = { self.cword }
for _, v in pairs(self.data) do
content[#content + 1] = v.name
end
local side_char = window.border_chars()['top'][config.ui.border]
local content_opt = {
contents = content,
filetype = 'lspsagacallhierarchy',
buftype = 'nofile',
enter = true,
border_side = {
['right'] = ' ',
['righttop'] = side_char,
['rightbottom'] = side_char,
},
highlight = {
normal = 'CallHierarchyNormal',
border = 'CallHierarchyBorder',
},
}
local cur_winline = fn.winline()
local max_height = math.floor(vim.o.lines * 0.4)
if vim.o.lines - cur_winline - 6 < max_height then
vim.cmd('normal! zz')
local keycode = api.nvim_replace_termcodes('5<C-e>', true, false, true)
api.nvim_feedkeys(keycode, 'x', false)
end
local opt = {
relative = 'editor',
row = fn.winline() + 1,
col = 10,
height = math.floor(vim.o.lines * 0.4),
width = math.floor(vim.o.columns * 0.3),
no_size_override = true,
}
if fn.has('nvim-0.9') == 1 and config.ui.title then
local icon = self.method == 'callHierarchy/incomingCalls' and ui.incoming or ui.outgoing
opt.title = {
{ icon, 'ArrowIcon' },
}
opt.title_pos = 'center'
api.nvim_set_hl(0, 'ArrowIcon', { link = 'CallHierarchyBorder' })
end
self.bufnr, self.winid = window.create_win_with_border(content_opt, opt)
api.nvim_win_set_cursor(self.winid, { 2, 9 })
api.nvim_create_autocmd('CursorMoved', {
buffer = self.bufnr,
callback = function()
self:preview()
end,
})
api.nvim_create_autocmd('BufWipeOut', {
buffer = self.bufnr,
once = true,
callback = function()
window.nvim_close_valid_window({ self.winid, self.preview_winid })
clean_ctx()
end,
})
api.nvim_buf_add_highlight(self.bufnr, 0, 'LSOutlinePackage', 0, 0, -1)
for i, items in pairs(self.data) do
for group, scope in pairs(items.highlights) do
api.nvim_buf_add_highlight(self.bufnr, 0, group, i, scope[1], scope[2])
end
end
self:apply_map()
end
---@private
local function node_in_parent(parent, node)
for _, v in pairs(parent.children) do
if v.name == node.name then
return true
end
end
return false
end
function ch:change_node_winline(node, factor)
local found = false
local function get_node(data)
for _, v in pairs(data) do
if found and not node_in_parent(node, v) then
v.winline = v.winline + factor
end
if v.name == node.name then
found = true
end
if v.children then
get_node(v.children)
end
end
end
get_node(self.data)
end
function ch:get_node_at_cursor()
local cur_line = api.nvim_win_get_cursor(0)[1]
if cur_line == 1 then
return
end
local node
local function get_node(data)
for _, v in pairs(data) do
if v.winline == cur_line then
node = v
end
if v.children then
get_node(v.children)
end
end
end
get_node(self.data)
return node
end
local function load_jdt_preview(bufnr, uri)
vim.bo[bufnr].modifiable = true
vim.bo[bufnr].swapfile = false
vim.bo[bufnr].buftype = 'nofile'
-- This triggers FileType event which should fire up the lsp client if not already running.
vim.bo[bufnr].filetype = 'java'
vim.wait(config.request_timeout, function()
return next(lsp.get_active_clients({ name = 'jdtls', bufnr = bufnr })) ~= nil
end)
local client = lsp.get_active_clients({ name = 'jdtls', bufnr = bufnr })[1]
assert(client, 'Must have a `jdtls` client to load class file or jdt uri')
local content
local function handler(err, result)
assert(not err, vim.inspect(err))
content = result
api.nvim_buf_set_lines(bufnr, 0, -1, false, vim.split(result, '\n', { plain = true }))
vim.bo[bufnr].modifiable = false
end
local params = {
uri = uri,
}
client.request('java/classFileContents', params, handler, bufnr)
-- Need to block. Otherwise logic could run that sets the cursor to a position
-- that's still missing.
vim.wait(config.request_timeout, function()
return content ~= nil
end)
end
local function get_preview_data(node)
if not node or vim.tbl_count(node) == 0 then
return
end
local uri = node.target.uri
local range = node.target.range
local bufnr = vim.uri_to_bufnr(uri)
if string.sub(uri, 1, 4) == 'jdt:' then
load_jdt_preview(bufnr, uri)
end
if not api.nvim_buf_is_loaded(bufnr) then
fn.bufload(bufnr)
end
return { bufnr = bufnr, range = range }
end
local function create_preview_window(winid)
local winconfig = api.nvim_win_get_config(winid)
local opt = {
relative = 'editor',
row = winconfig.row[false],
height = winconfig.height,
col = winconfig.col[false] + winconfig.width + 2,
no_size_override = true,
}
opt.width = vim.o.columns - opt.col - 6
local rtop = window.combine_char()['top'][config.ui.border]
local rbottom = window.combine_char()['bottom'][config.ui.border]
local content_opt = {
contents = {},
border_side = {
['lefttop'] = rtop,
['leftbottom'] = rbottom,
},
enter = false,
highlight = {
normal = 'CallHierarchyNormal',
border = 'CallHierarchyBorder',
},
}
return window.create_win_with_border(content_opt, opt)
end
function ch:preview()
local node = self:get_node_at_cursor()
if not node then
return
end
local data = get_preview_data(node)
if not data or not data.bufnr then
return
end
if not self.preview_winid or not api.nvim_win_is_valid(self.preview_winid) then
self.preview_bufnr, self.preview_winid = create_preview_window(self.winid)
end
api.nvim_win_set_buf(self.preview_winid, data.bufnr)
if fn.has('nvim-0.9') == 1 and config.ui.title then
local path = vim.split(api.nvim_buf_get_name(data.bufnr), libs.path_sep, { trimempty = true })
local icon = libs.icon_from_devicon(vim.bo[self.main_buf].filetype)
api.nvim_win_set_config(self.preview_winid, {
title = {
{ icon[1], icon[2] or 'TitleString' },
{ path[#path], 'TitleString' },
},
title_pos = 'center',
})
end
api.nvim_set_option_value(
'winhl',
'Normal:finderNormal,FloatBorder:finderPreviewBorder',
{ scope = 'local', win = self.preview_winid }
)
-- Check if the cursor position is within the buffer's valid range.
local buf_line_count = api.nvim_buf_line_count(data.bufnr)
local cursor_row = data.range.start.line + 1
local cursor_column = data.range.start.character
if cursor_row >= 1 and cursor_row <= buf_line_count then
api.nvim_win_set_cursor(self.preview_winid, { cursor_row, cursor_column })
vim.bo[data.bufnr].filetype = vim.bo[self.main_buf].filetype
end
api.nvim_set_option_value('winbar', '', { scope = 'local', win = self.preview_winid })
end
function ch:send_method(type)
self.cword = fn.expand('<cword>')
self.method = get_method(type)
self.data = {}
self:send_prepare_call()
end
return setmetatable(ctx, ch)
return setmetatable({}, ch)

View file

@ -1,486 +0,0 @@
local api, lsp_util, fn, lsp = vim.api, vim.lsp.util, vim.fn, vim.lsp
local config = require('lspsaga').config
local window = require('lspsaga.window')
local util = require('lspsaga.util')
local nvim_buf_set_keymap = api.nvim_buf_set_keymap
local act = {}
local ctx = {}
act.__index = act
function act.__newindex(t, k, v)
rawset(t, k, v)
end
local function clean_ctx()
for k, _ in pairs(ctx) do
ctx[k] = nil
end
end
local function clean_msg(msg)
if msg:find('%(.+%)%S$') then
return msg:gsub('%(.+%)%S$', '')
end
return msg
end
function act:action_callback()
local contents = {}
for index, client_with_actions in pairs(self.action_tuples) do
local action_title = ''
if #client_with_actions ~= 2 then
vim.notify('There is something wrong in aciton_tuples')
return
end
if client_with_actions[2].title then
action_title = '[' .. index .. '] ' .. clean_msg(client_with_actions[2].title)
end
if config.code_action.show_server_name == true then
if type(client_with_actions[1]) == 'string' then
action_title = action_title .. ' (' .. client_with_actions[1] .. ')'
else
action_title = action_title
.. ' ('
.. lsp.get_client_by_id(client_with_actions[1]).name
.. ')'
end
end
contents[#contents + 1] = action_title
end
local content_opts = {
contents = contents,
filetype = 'sagacodeaction',
buftype = 'nofile',
enter = true,
highlight = {
normal = 'CodeActionNormal',
border = 'CodeActionBorder',
},
}
local opt = {}
local max_height = math.floor(vim.o.lines * 0.5)
opt.height = max_height < #contents and max_height or #contents
local max_width = math.floor(vim.o.columns * 0.7)
local max_len = window.get_max_content_length(contents)
opt.width = max_len + 10 < max_width and max_len + 5 or max_width
opt.no_size_override = true
if fn.has('nvim-0.9') == 1 and config.ui.title then
opt.title = {
{ config.ui.code_action .. ' CodeActions', 'TitleString' },
}
end
self.action_bufnr, self.action_winid = window.create_win_with_border(content_opts, opt)
vim.wo[self.action_winid].conceallevel = 2
vim.wo[self.action_winid].concealcursor = 'niv'
-- initial position in code action window
api.nvim_win_set_cursor(self.action_winid, { 1, 1 })
api.nvim_create_autocmd('CursorMoved', {
buffer = self.action_bufnr,
callback = function()
self:set_cursor()
end,
})
for i = 1, #contents, 1 do
local row = i - 1
local col = contents[i]:find('%]')
api.nvim_buf_add_highlight(self.action_bufnr, -1, 'CodeActionText', row, 0, -1)
api.nvim_buf_add_highlight(self.action_bufnr, 0, 'CodeActionNumber', row, 0, col)
end
self:apply_action_keys()
if config.code_action.num_shortcut then
self:num_shortcut(self.action_bufnr)
end
end
---@private
---@param bufnr integer
---@param mode "v"|"V"
---@return table {start={row, col}, end={row, col}} using (1, 0) indexing
local function range_from_selection(bufnr, mode)
-- TODO: Use `vim.region()` instead https://github.com/neovim/neovim/pull/13896
-- [bufnum, lnum, col, off]; both row and column 1-indexed
local start = vim.fn.getpos('v')
local end_ = vim.fn.getpos('.')
local start_row = start[2]
local start_col = start[3]
local end_row = end_[2]
local end_col = end_[3]
-- A user can start visual selection at the end and move backwards
-- Normalize the range to start < end
if start_row == end_row and end_col < start_col then
end_col, start_col = start_col, end_col
elseif end_row < start_row then
start_row, end_row = end_row, start_row
start_col, end_col = end_col, start_col
end
if mode == 'V' then
start_col = 1
local lines = api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true)
end_col = #lines[1]
end
return {
['start'] = { start_row, start_col - 1 },
['end'] = { end_row, end_col - 1 },
}
end
function act:apply_action_keys()
util.map_keys(self.action_bufnr, 'n', config.code_action.keys.exec, function()
self:do_code_action()
end)
util.map_keys(self.action_bufnr, 'n', config.code_action.keys.quit, function()
self:close_action_window()
clean_ctx()
end)
end
function act:send_code_action_request(main_buf, options, cb)
local diagnostics = lsp.diagnostic.get_line_diagnostics(main_buf)
self.bufnr = main_buf
local ctx_diags = { diagnostics = diagnostics }
local params
local mode = api.nvim_get_mode().mode
options = options or {}
if options.range then
assert(type(options.range) == 'table', 'code_action range must be a table')
local start = assert(options.range.start, 'range must have a `start` property')
local end_ = assert(options.range['end'], 'range must have an `end` property')
params = lsp_util.make_given_range_params(start, end_)
elseif mode == 'v' or mode == 'V' then
local range = range_from_selection(0, mode)
params = lsp_util.make_given_range_params(range.start, range['end'])
else
params = lsp_util.make_range_params()
end
params.context = ctx_diags
if not self.enriched_ctx then
self.enriched_ctx = { bufnr = main_buf, method = 'textDocument/codeAction', params = params }
end
lsp.buf_request_all(main_buf, 'textDocument/codeAction', params, function(results)
self.pending_request = false
self.action_tuples = {}
for client_id, result in pairs(results) do
for _, action in pairs(result.result or {}) do
self.action_tuples[#self.action_tuples + 1] = { client_id, action }
end
end
if config.code_action.extend_gitsigns then
local res = self:extend_gitsing(params)
if res then
for _, action in pairs(res) do
self.action_tuples[#self.action_tuples + 1] = { 'gitsigns', action }
end
end
end
if #self.action_tuples == 0 and not options.silent then
vim.notify('No code actions available', vim.log.levels.INFO)
return
end
if cb then
cb(vim.deepcopy(self.action_tuples), vim.deepcopy(self.enriched_ctx))
end
end)
end
function act:set_cursor()
local col = 1
local current_line = api.nvim_win_get_cursor(self.action_winid)[1]
if current_line == #self.action_tuples + 1 then
api.nvim_win_set_cursor(self.action_winid, { 1, col })
else
api.nvim_win_set_cursor(self.action_winid, { current_line, col })
end
self:action_preview(self.action_winid, self.bufnr)
end
function act:num_shortcut(bufnr)
for num, _ in pairs(self.action_tuples or {}) do
nvim_buf_set_keymap(bufnr, 'n', tostring(num), '', {
noremap = true,
nowait = true,
callback = function()
self:do_code_action(num)
end,
})
end
end
function act:code_action(options)
if self.pending_request then
vim.notify(
'[lspsaga.nvim] there is already a code action request please wait',
vim.log.levels.WARN
)
return
end
self.pending_request = true
options = options or {}
self:send_code_action_request(api.nvim_get_current_buf(), options, function()
self:action_callback()
end)
end
function act:apply_action(action, client, enriched_ctx)
if action.edit then
lsp_util.apply_workspace_edit(action.edit, client.offset_encoding)
end
if action.command then
local command = type(action.command) == 'table' and action.command or action
local func = client.commands[command.command] or lsp.commands[command.command]
if func then
enriched_ctx.client_id = client.id
func(command, enriched_ctx)
else
local params = {
command = command.command,
arguments = command.arguments,
workDoneToken = command.workDoneToken,
}
client.request('workspace/executeCommand', params, nil, enriched_ctx.bufnr)
end
end
clean_ctx()
end
function act:do_code_action(num, tuple, enriched_ctx)
local number
if num then
number = tonumber(num)
else
local cur_text = api.nvim_get_current_line()
number = cur_text:match('%[(%d+)%]%s+%S')
number = tonumber(number)
end
if not number and not tuple then
vim.notify('[Lspsaga] no action number choice', vim.log.levels.WARN)
return
end
local action = tuple and tuple[2] or vim.deepcopy(self.action_tuples[number][2])
local id = tuple and tuple[1] or self.action_tuples[number][1]
local client = lsp.get_client_by_id(id)
local curbuf = api.nvim_get_current_buf()
self:close_action_window(curbuf)
enriched_ctx = enriched_ctx or vim.deepcopy(self.enriched_ctx)
if
not action.edit
and client
and vim.tbl_get(client.server_capabilities, 'codeActionProvider', 'resolveProvider')
then
client.request('codeAction/resolve', action, function(err, resolved_action)
if err then
vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR)
return
end
self:apply_action(resolved_action, client, enriched_ctx)
end)
elseif action.action and type(action.action) == 'function' then
action.action()
else
self:apply_action(action, client, enriched_ctx)
end
end
function act:get_action_diff(num, main_buf, tuple)
local action = tuple and tuple[2] or self.action_tuples[tonumber(num)][2]
if not action then
return
end
local id = tuple and tuple[1] or self.action_tuples[tonumber(num)][1]
local client = lsp.get_client_by_id(id)
if
not action.edit
and client
and vim.tbl_get(client.server_capabilities, 'codeActionProvider', 'resolveProvider')
then
local results = lsp.buf_request_sync(main_buf, 'codeAction/resolve', action, 1000)
---@diagnostic disable-next-line: need-check-nil
action = results[client.id].result
if not action then
return
end
if tuple then
tuple[tonumber(num)][2] = action
elseif self.action_tuples then
self.action_tuples[tonumber(num)][2] = action
end
end
if not action.edit then
return
end
local all_changes = {}
if action.edit.documentChanges then
for _, item in pairs(action.edit.documentChanges) do
if item.textDocument then
if not all_changes[item.textDocument.uri] then
all_changes[item.textDocument.uri] = {}
end
for _, edit in pairs(item.edits) do
table.insert(all_changes[item.textDocument.uri], edit)
end
end
end
elseif action.edit.changes then
all_changes = action.edit.changes
end
if not (all_changes and not vim.tbl_isempty(all_changes)) then
return
end
local tmp_buf = api.nvim_create_buf(false, false)
vim.bo[tmp_buf].bufhidden = 'wipe'
local lines = api.nvim_buf_get_lines(main_buf, 0, -1, false)
api.nvim_buf_set_lines(tmp_buf, 0, -1, false, lines)
for _, changes in pairs(all_changes) do
lsp_util.apply_text_edits(changes, tmp_buf, client.offset_encoding)
end
local data = api.nvim_buf_get_lines(tmp_buf, 0, -1, false)
api.nvim_buf_delete(tmp_buf, { force = true })
local diff = vim.diff(table.concat(lines, '\n') .. '\n', table.concat(data, '\n') .. '\n')
return diff
end
function act:action_preview(main_winid, main_buf, border_hi, tuple)
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
api.nvim_win_close(self.preview_winid, true)
self.preview_winid = nil
end
local line = api.nvim_get_current_line()
local num = line:match('%[(%d+)%]')
if not num then
return
end
local tbl = self:get_action_diff(num, main_buf, tuple)
if not tbl or #tbl == 0 then
return
end
tbl = vim.split(tbl, '\n')
table.remove(tbl, 1)
local win_conf = api.nvim_win_get_config(main_winid)
local max_height
local opt = {
relative = win_conf.relative,
win = win_conf.win,
width = win_conf.width,
no_size_override = true,
col = win_conf.col[false],
anchor = win_conf.anchor,
focusable = false,
}
local winheight = api.nvim_win_get_height(win_conf.win)
if win_conf.anchor:find('^S') then
opt.row = win_conf.row[false] - win_conf.height - 2
max_height = win_conf.row[false] - win_conf.height
elseif win_conf.anchor:find('^N') then
opt.row = win_conf.row[false] + win_conf.height + 2
max_height = winheight - opt.row
end
opt.height = #tbl > max_height and max_height or #tbl
if fn.has('nvim-0.9') == 1 and config.ui.title then
opt.title = { { 'Action Preview', 'ActionPreviewTitle' } }
end
local content_opts = {
contents = tbl,
filetype = 'diff',
bufhidden = 'wipe',
highlight = {
normal = 'ActionPreviewNormal',
border = border_hi or 'ActionPreviewBorder',
},
}
local preview_buf
preview_buf, self.preview_winid = window.create_win_with_border(content_opts, opt)
vim.bo[preview_buf].syntax = 'on'
return self.preview_winid
end
function act:close_action_window(bufnr)
if self.action_winid and api.nvim_win_is_valid(self.action_winid) then
api.nvim_win_close(self.action_winid, true)
end
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
api.nvim_win_close(self.preview_winid, true)
end
if config.code_action.num_shortcut and self.action_tuples and #self.action_tuples > 1 then
for i = 1, #self.action_tuples do
pcall(api.nvim_buf_del_keymap, bufnr, 'n', tostring(i))
end
end
end
function act:clean_context()
clean_ctx()
end
function act:extend_gitsing(params)
local ok, gitsigns = pcall(require, 'gitsigns')
if not ok then
return
end
local gitsigns_actions = gitsigns.get_actions()
if not gitsigns_actions or vim.tbl_isempty(gitsigns_actions) then
return
end
local name_to_title = function(name)
return name:sub(1, 1):upper() .. name:gsub('_', ' '):sub(2)
end
local actions = {}
local range_actions = { ['reset_hunk'] = true, ['stage_hunk'] = true }
local mode = vim.api.nvim_get_mode().mode
for name, action in pairs(gitsigns_actions) do
local title = name_to_title(name)
local cb = action
if (mode == 'v' or mode == 'V') and range_actions[name] then
title = title:gsub('hunk', 'selection')
cb = function()
action({ params.range.start.line, params.range['end'].line })
end
end
actions[#actions + 1] = {
title = title,
action = function()
local bufnr = vim.uri_to_bufnr(params.textDocument.uri)
vim.api.nvim_buf_call(bufnr, cb)
end,
}
end
return actions
end
return setmetatable(ctx, act)

View file

@ -0,0 +1,379 @@
local api, lsp = vim.api, vim.lsp
local config = require('lspsaga').config
local win = require('lspsaga.window')
local preview = require('lspsaga.codeaction.preview')
local util = require('lspsaga.util')
local act = {}
local ctx = {}
act.__index = act
function act.__newindex(t, k, v)
rawset(t, k, v)
end
local function clean_ctx()
for k, _ in pairs(ctx) do
ctx[k] = nil
end
end
local function clean_msg(msg)
if msg:find('%(.+%)%S$') then
return msg:gsub('%(.+%)%S$', '')
end
return msg
end
function act:action_callback(tuples, enriched_ctx)
local content = {}
for index, client_with_actions in ipairs(tuples) do
local action_title = ''
if #client_with_actions ~= 2 then
vim.notify('There is something wrong in aciton_tuples')
return
end
if client_with_actions[2].title then
action_title = '[' .. index .. '] ' .. clean_msg(client_with_actions[2].title)
end
if config.code_action.show_server_name == true then
if type(client_with_actions[1]) == 'string' then
action_title = action_title .. ' (' .. client_with_actions[1] .. ')'
else
action_title = action_title
.. ' ('
.. lsp.get_client_by_id(client_with_actions[1]).name
.. ')'
end
end
content[#content + 1] = action_title
end
local float_opt = {
height = #content,
width = util.get_max_content_length(content),
}
if config.ui.title then
float_opt.title = {
{ config.ui.code_action .. ' CodeActions', 'Title' },
}
end
self.action_bufnr, self.action_winid = win
:new_float(float_opt, true)
:setlines(content)
:bufopt({
['filetype'] = 'saga_codeaction',
['buftype'] = 'nofile',
['bufhidden'] = 'wipe',
})
:winopt({
['conceallevel'] = 2,
['concealcursor'] = 'niv',
['modifiable'] = false,
})
:wininfo()
-- initial position in code action window
api.nvim_win_set_cursor(self.action_winid, { 1, 1 })
api.nvim_create_autocmd('CursorMoved', {
buffer = self.action_bufnr,
callback = function()
self:set_cursor(tuples)
end,
})
for i = 1, #content, 1 do
local row = i - 1
local col = content[i]:find('%]')
api.nvim_buf_add_highlight(self.action_bufnr, -1, 'CodeActionText', row, 0, -1)
api.nvim_buf_add_highlight(self.action_bufnr, 0, 'CodeActionNumber', row, 0, col)
end
self:apply_action_keys(tuples, enriched_ctx)
if config.code_action.num_shortcut then
self:num_shortcut(self.action_bufnr, tuples, enriched_ctx)
end
end
local function map_keys(mode, keys, action, options)
if type(keys) == 'string' then
keys = { keys }
end
for _, key in ipairs(keys) do
vim.keymap.set(mode, key, action, options)
end
end
---@private
---@param bufnr integer
---@param mode "v"|"V"
---@return table {start={row, col}, end={row, col}} using (1, 0) indexing
local function range_from_selection(bufnr, mode)
-- TODO: Use `vim.region()` instead https://github.com/neovim/neovim/pull/13896
-- [bufnum, lnum, col, off]; both row and column 1-indexed
local start = vim.fn.getpos('v')
local end_ = vim.fn.getpos('.')
local start_row = start[2]
local start_col = start[3]
local end_row = end_[2]
local end_col = end_[3]
-- A user can start visual selection at the end and move backwards
-- Normalize the range to start < end
if start_row == end_row and end_col < start_col then
end_col, start_col = start_col, end_col
elseif end_row < start_row then
start_row, end_row = end_row, start_row
start_col, end_col = end_col, start_col
end
if mode == 'V' then
start_col = 1
local lines = api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true)
end_col = #lines[1]
end
return {
['start'] = { start_row, start_col - 1 },
['end'] = { end_row, end_col - 1 },
}
end
function act:send_request(main_buf, options, callback)
self.bufnr = main_buf
local params
local mode = api.nvim_get_mode().mode
if options.range then
assert(type(options.range) == 'table', 'code_action range must be a table')
local start = assert(options.range.start, 'range must have a `start` property')
local end_ = assert(options.range['end'], 'range must have an `end` property')
params = lsp.util.make_given_range_params(start, end_)
elseif mode == 'v' or mode == 'V' then
local range = range_from_selection(0, mode)
params = lsp.util.make_given_range_params(range.start, range['end'])
else
params = lsp.util.make_range_params()
end
params.context = options.context
local enriched_ctx = { bufnr = main_buf, method = 'textDocument/codeAction', params = params }
lsp.buf_request_all(main_buf, 'textDocument/codeAction', params, function(results)
self.pending_request = false
local action_tuples = {}
for client_id, item in pairs(results) do
for _, action in ipairs(item.result or {}) do
action_tuples[#action_tuples + 1] = { client_id, action }
end
end
if config.code_action.extend_gitsigns then
local res = self:extend_gitsign(params)
if res then
for _, action in ipairs(res) do
action_tuples[#action_tuples + 1] = { 'gitsigns', action }
end
end
end
if #action_tuples == 0 then
vim.notify('No code actions available', vim.log.levels.INFO)
return
end
if callback then
callback(action_tuples, enriched_ctx)
end
end)
end
local function get_num()
local num
local cur_text = api.nvim_get_current_line()
num = cur_text:match('%[(%d+)%]%s+%S')
if num then
num = tonumber(num)
end
return num
end
function act:set_cursor(action_tuples)
local col = 1
local current_line = api.nvim_win_get_cursor(self.action_winid)[1]
if current_line == #action_tuples + 1 then
api.nvim_win_set_cursor(self.action_winid, { 1, col })
else
api.nvim_win_set_cursor(self.action_winid, { current_line, col })
end
local num = get_num()
if not num or not action_tuples[num] then
return
end
local tuple = action_tuples[num]
preview.action_preview(self.action_winid, self.bufnr, tuple)
end
local function apply_action(action, client, enriched_ctx)
if action.edit then
lsp.util.apply_workspace_edit(action.edit, client.offset_encoding)
end
if action.command then
local command = type(action.command) == 'table' and action.command or action
local func = client.commands[command.command] or lsp.commands[command.command]
if func then
enriched_ctx.client_id = client.id
func(command, enriched_ctx)
else
local params = {
command = command.command,
arguments = command.arguments,
workDoneToken = command.workDoneToken,
}
client.request('workspace/executeCommand', params, nil, enriched_ctx.bufnr)
end
end
clean_ctx()
end
function act:support_resolve(client)
if vim.version().minor >= 10 then
local reg = client.dynamic_capabilities:get('textDocument/codeAction', { bufnr = ctx.bufnr })
return vim.tbl_get(reg or {}, 'registerOptions', 'resolveProvider')
or client.supports_method('codeAction/resolve')
end
return vim.tbl_get(client.server_capabilities, 'codeActionProvider', 'resolveProvider')
end
function act:get_resolve_action(client, action, bufnr)
if not self:support_resolve(client) then
return
end
return client.request_sync('codeAction/resolve', action, 1500, bufnr).result
end
function act:do_code_action(action, client, enriched_ctx)
if not action.edit and client and self:support_resolve(client) then
client.request('codeAction/resolve', action, function(err, resolved_action)
if err then
vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR)
return
end
apply_action(resolved_action, client, enriched_ctx)
end)
elseif action.action and type(action.action) == 'function' then
action.action()
else
apply_action(action, client, enriched_ctx)
end
end
function act:apply_action_keys(action_tuples, enriched_ctx)
map_keys('n', config.code_action.keys.exec, function()
local num = get_num()
if not num then
return
end
local action = action_tuples[num][2]
local client = lsp.get_client_by_id(action_tuples[num][1])
self:close_action_window()
self:do_code_action(action, client, enriched_ctx)
end, { buffer = self.action_bufnr })
map_keys('n', config.code_action.keys.quit, function()
self:close_action_window()
clean_ctx()
end, { buffer = self.action_bufnr })
end
function act:num_shortcut(bufnr, action_tuples, enriched_ctx)
for num, _ in pairs(action_tuples or {}) do
util.map_keys(bufnr, tostring(num), function()
if not action_tuples or not action_tuples[num] then
return
end
local action = action_tuples[num][2]
local client = lsp.get_client_by_id(action_tuples[num][1])
self:close_action_window()
self:do_code_action(action, client, enriched_ctx)
end)
end
end
function act:code_action(options)
if self.pending_request then
vim.notify(
'[lspsaga.nvim] there is already a code action request please wait',
vim.log.levels.WARN
)
return
end
self.pending_request = true
options = options or {}
if not options.context then
options.context = {
diagnostics = require('lspsaga.diagnostic'):get_cursor_diagnostic(),
}
end
self:send_request(api.nvim_get_current_buf(), options, function(tuples)
self.pending_request = false
self:action_callback(tuples)
end)
end
function act:close_action_window()
if self.action_winid and api.nvim_win_is_valid(self.action_winid) then
api.nvim_win_close(self.action_winid, true)
end
preview.preview_win_close()
end
function act:clean_context()
clean_ctx()
end
function act:extend_gitsign(params)
local ok, gitsigns = pcall(require, 'gitsigns')
if not ok then
return
end
local gitsigns_actions = gitsigns.get_actions()
if not gitsigns_actions or vim.tbl_isempty(gitsigns_actions) then
return
end
local name_to_title = function(name)
return name:sub(1, 1):upper() .. name:gsub('_', ' '):sub(2)
end
local actions = {}
local range_actions = { ['reset_hunk'] = true, ['stage_hunk'] = true }
local mode = vim.api.nvim_get_mode().mode
for name, action in pairs(gitsigns_actions) do
local title = name_to_title(name)
local cb = action
if (mode == 'v' or mode == 'V') and range_actions[name] then
title = title:gsub('hunk', 'selection')
cb = function()
action({ params.range.start.line, params.range['end'].line })
end
end
actions[#actions + 1] = {
title = title,
action = function()
local bufnr = vim.uri_to_bufnr(params.textDocument.uri)
vim.api.nvim_buf_call(bufnr, cb)
end,
}
end
return actions
end
return setmetatable(ctx, act)

View file

@ -0,0 +1,127 @@
local api, lsp, fn = vim.api, vim.lsp, vim.fn
---@diagnostic disable-next-line: deprecated
local uv = vim.version().minor >= 10 and vim.uv or vim.loop
local config = require('lspsaga').config
local nvim_buf_set_extmark = api.nvim_buf_set_extmark
local inrender_row = -1
local function get_name()
return 'SagaLightBulb'
end
local namespace = api.nvim_create_namespace(get_name())
local defined = false
if not defined then
fn.sign_define(get_name(), { text = config.ui.code_action, texthl = get_name() })
defined = true
end
local function update_lightbulb(bufnr, row)
api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
local name = get_name()
pcall(fn.sign_unplace, name, { id = inrender_row, buffer = bufnr })
if not row then
return
end
if config.lightbulb.sign then
fn.sign_place(
row + 1,
name,
name,
bufnr,
{ lnum = row + 1, priority = config.lightbulb.sign_priority }
)
end
if config.lightbulb.virtual_text then
nvim_buf_set_extmark(bufnr, namespace, row, -1, {
virt_text = { { config.ui.code_action, name } },
virt_text_pos = 'overlay',
hl_mode = 'combine',
})
end
inrender_row = row + 1
end
local function render(bufnr)
local row = api.nvim_win_get_cursor(0)[1] - 1
local params = lsp.util.make_range_params()
params.context = {
diagnostics = lsp.diagnostic.get_line_diagnostics(bufnr),
}
lsp.buf_request(bufnr, 'textDocument/codeAction', params, function(_, result, _)
if api.nvim_get_current_buf() ~= bufnr then
return
end
if result and #result > 0 then
update_lightbulb(bufnr, row)
else
update_lightbulb(bufnr, nil)
end
end)
end
local timer = uv.new_timer()
local function update(buf)
timer:start(config.lightbulb.debounce, 0, function()
timer:stop()
vim.schedule(function()
render(buf)
end)
end)
end
local function lb_autocmd()
local name = 'SagaLightBulb'
api.nvim_create_autocmd('LspAttach', {
group = api.nvim_create_augroup(name, { clear = true }),
callback = function(opt)
local client = lsp.get_client_by_id(opt.data.client_id)
if not client.supports_method('textDocument/codeAction') then
return
end
local buf = opt.buf
local group_name = name .. tostring(buf)
local ok = pcall(api.nvim_get_autocmds, { group = group_name })
if ok then
return
end
local group = api.nvim_create_augroup(group_name, { clear = true })
api.nvim_create_autocmd('CursorMoved', {
group = group,
buffer = buf,
callback = function()
update(buf)
end,
})
api.nvim_create_autocmd('InsertEnter', {
group = group,
buffer = buf,
callback = function()
update_lightbulb(buf, nil)
end,
})
api.nvim_create_autocmd('BufLeave', {
group = group,
buffer = buf,
callback = function()
update_lightbulb(buf, nil)
end,
})
end,
})
end
return {
lb_autocmd = lb_autocmd,
}

View file

@ -0,0 +1,166 @@
local api, lsp = vim.api, vim.lsp
local config = require('lspsaga').config
local win = require('lspsaga.window')
local function get_action_diff(main_buf, tuple)
local act = require('lspsaga.codeaction.init')
local action = tuple[2]
if not action then
return
end
local id = tuple[1]
local client = lsp.get_client_by_id(id)
if not action.edit and client and act:support_resolve(client) then
action = act:get_resolve_action(client, action, main_buf)
if not action then
return
end
tuple[2] = action
end
if not action.edit then
return
end
local all_changes = {}
if action.edit.documentChanges then
for _, item in pairs(action.edit.documentChanges) do
if item.textDocument then
if not all_changes[item.textDocument.uri] then
all_changes[item.textDocument.uri] = {}
end
for _, edit in pairs(item.edits) do
all_changes[item.textDocument.uri][#all_changes[item.textDocument.uri] + 1] = edit
end
end
end
elseif action.edit.changes then
all_changes = action.edit.changes
end
if not (all_changes and not vim.tbl_isempty(all_changes)) then
return
end
local tmp_buf = api.nvim_create_buf(false, false)
vim.bo[tmp_buf].bufhidden = 'wipe'
local lines = api.nvim_buf_get_lines(main_buf, 0, -1, false)
api.nvim_buf_set_lines(tmp_buf, 0, -1, false, lines)
local srow = 0
local erow = 0
for _, changes in pairs(all_changes) do
lsp.util.apply_text_edits(changes, tmp_buf, client.offset_encoding)
vim.tbl_map(function(item)
srow = srow == 0 and item.range.start.line or srow
erow = erow == 0 and item.range['end'].line or erow
srow = math.min(srow, item.range.start.line)
erow = math.max(erow, item.range['end'].line)
end, changes)
end
local data = api.nvim_buf_get_lines(tmp_buf, srow - 1, erow, false)
data = vim.tbl_map(function(line)
return line .. '\n'
end, data)
lines = vim.list_slice(lines, srow, erow + 1)
lines = vim.tbl_map(function(line)
return line .. '\n'
end, lines)
api.nvim_buf_delete(tmp_buf, { force = true })
local diff = vim.diff(table.concat(lines), table.concat(data), {
algorithm = 'minimal',
ctxlen = 0,
})
diff = vim.tbl_filter(function(item)
return not item:find('@@%s')
end, vim.split(diff, '\n'))
return diff
end
local preview_buf, preview_winid
---create a preview window according given window
---default is under the given window
local function create_preview_win(content, main_winid)
local win_conf = api.nvim_win_get_config(main_winid)
local max_height
local opt = {
relative = win_conf.relative,
win = win_conf.win,
width = win_conf.width,
col = win_conf.col[false],
anchor = win_conf.anchor,
focusable = false,
}
local winheight = api.nvim_win_get_height(win_conf.win)
if win_conf.anchor:find('^S') then
opt.row = win_conf.row[false] - win_conf.height - 2
max_height = win_conf.row[false] - win_conf.height
elseif win_conf.anchor:find('^N') then
opt.row = win_conf.row[false] + win_conf.height + 2
max_height = winheight - opt.row
end
opt.height = math.min(max_height, #content)
if config.ui.title then
opt.title = { { 'Action Preview', 'ActionPreviewTitle' } }
opt.title_pos = 'center'
end
preview_buf, preview_winid = win
:new_float(opt, false, true)
:setlines(content)
:bufopt({
['filetype'] = 'diff',
['bufhidden'] = 'wipe',
['buftype'] = 'nofile',
['modifiable'] = false,
})
:winhl('ActionPreviewNormal', 'ActionPreviewBorder')
:wininfo()
end
local function action_preview(main_winid, main_buf, tuple)
local diff = get_action_diff(main_buf, tuple)
if not diff or #diff == 0 then
if preview_winid and api.nvim_win_is_valid(preview_winid) then
api.nvim_win_close(preview_winid, true)
preview_buf = nil
preview_winid = nil
end
return
end
if not preview_winid or not api.nvim_win_is_valid(preview_winid) then
create_preview_win(diff, main_winid)
else
--reuse before window
vim.bo[preview_buf].modifiable = true
api.nvim_buf_set_lines(preview_buf, 0, -1, false, diff)
vim.bo[preview_buf].modifiable = false
local win_conf = api.nvim_win_get_config(preview_winid)
win_conf.height = #diff
api.nvim_win_set_config(preview_winid, win_conf)
end
return preview_buf, preview_winid
end
local function preview_win_close()
if preview_winid and api.nvim_win_is_valid(preview_winid) then
api.nvim_win_close(preview_winid, true)
preview_winid = nil
preview_buf = nil
end
end
return {
action_preview = action_preview,
preview_win_close = preview_win_close,
}

View file

@ -1,8 +1,8 @@
local command = {}
local subcommands = {
lsp_finder = function()
require('lspsaga.finder'):lsp_finder()
finder = function(args)
require('lspsaga.finder'):new(args)
end,
peek_definition = function()
require('lspsaga.definition'):peek_definition(1)
@ -16,23 +16,26 @@ local subcommands = {
goto_type_definition = function()
require('lspsaga.definition'):goto_definition(2)
end,
rename = function(arg)
require('lspsaga.rename'):lsp_rename(arg)
rename = function(args)
require('lspsaga.rename'):lsp_rename(args)
end,
hover_doc = function(arg)
require('lspsaga.hover'):render_hover_doc(arg)
project_replace = function(args)
require('lspsaga.rename.project'):new(args)
end,
show_workspace_diagnostics = function(arg)
require('lspsaga.showdiag'):show_diagnostics({ workspace = true, arg = arg })
hover_doc = function(args)
require('lspsaga.hover'):render_hover_doc(args)
end,
show_line_diagnostics = function(arg)
require('lspsaga.showdiag'):show_diagnostics({ line = true, arg = arg })
show_workspace_diagnostics = function(args)
require('lspsaga.diagnostic.show'):show_diagnostics({ workspace = true, args = args })
end,
show_buf_diagnostics = function(arg)
require('lspsaga.showdiag'):show_diagnostics({ buffer = true, arg = arg })
show_line_diagnostics = function(args)
require('lspsaga.diagnostic.show'):show_diagnostics({ line = true, args = args })
end,
show_cursor_diagnostics = function(arg)
require('lspsaga.showdiag'):show_diagnostics({ cursor = true, arg = arg })
show_buf_diagnostics = function(args)
require('lspsaga.diagnostic.show'):show_diagnostics({ buffer = true, args = args })
end,
show_cursor_diagnostics = function(args)
require('lspsaga.diagnostic.show'):show_diagnostics({ cursor = true, args = args })
end,
diagnostic_jump_next = function()
require('lspsaga.diagnostic'):goto_next()
@ -44,16 +47,19 @@ local subcommands = {
require('lspsaga.codeaction'):code_action()
end,
outline = function()
require('lspsaga.outline'):outline()
require('lspsaga.symbol'):outline()
end,
incoming_calls = function()
require('lspsaga.callhierarchy'):send_method(2)
incoming_calls = function(args)
require('lspsaga.callhierarchy'):send_method(2, args)
end,
outgoing_calls = function()
require('lspsaga.callhierarchy'):send_method(3)
outgoing_calls = function(args)
require('lspsaga.callhierarchy'):send_method(3, args)
end,
term_toggle = function(cmd)
require('lspsaga.floaterm'):open_float_terminal(cmd)
term_toggle = function(args)
require('lspsaga.floaterm'):open_float_terminal(args)
end,
open_log = function()
require('lspsaga.logger'):open()
end,
}

View file

@ -1,10 +1,12 @@
local config = require('lspsaga').config
local lsp, fn, api = vim.lsp, vim.fn, vim.api
local libs = require('lspsaga.libs')
local window = require('lspsaga.window')
local log = require('lspsaga.logger')
local util = require('lspsaga.util')
local win = require('lspsaga.window')
local buf_del_keymap = api.nvim_buf_del_keymap
local beacon = require('lspsaga.beacon').jump_beacon
local def = {}
def.__index = def
-- a double linked list for store the node infor
local ctx = {}
@ -15,182 +17,149 @@ local function clean_ctx()
end
end
local function find_node(bufnr)
for i, node in pairs(ctx) do
if type(node) == 'table' and node.bufnr == bufnr then
return i
end
end
end
local function push(node)
ctx[#ctx + 1] = node
end
local function title_text(fname)
if not fname then
return
end
local title = {}
local data = libs.icon_from_devicon(vim.bo.filetype)
title[#title + 1] = { data[1], data[2] or 'TitleString' }
title[#title + 1] = { fn.fnamemodify(fname, ':t'), 'TitleString' }
return title
end
local function get_uri_data(result)
local res = {}
local range
if type(result[1]) == 'table' then
res.uri = result[1].uri or result[1].targetUri
range = result[1].range or result[1].targetSelectionRange
else
res.uri = result.uri or result.targetUri
range = result.range or result.targetSelectionRange
end
if not res.uri or not range then
vim.notify('[Lspsaga] Did not find target uri', vim.log.levels.WARN)
return
end
res.pos = { range.start.line, range.start.character }
res.bufnr = vim.uri_to_bufnr(res.uri)
if not api.nvim_buf_is_loaded(res.bufnr) then
fn.bufload(res.bufnr)
res.wipe = true
end
return res
end
function def:has_peek_win()
if self.winid and api.nvim_win_is_valid(self.winid) then
return true
end
return false
end
function def:apply_action_keys(bufnr, main_bufnr)
local opts = { nowait = true }
local function find_node_index()
local curbuf = api.nvim_get_current_buf()
local index = find_node(curbuf)
if not index then
return
end
return index
end
local function unpack_map()
local map = {}
for k, v in pairs(config.definition) do
if k ~= 'width' and k ~= 'height' and k ~= 'quit' then
map[k] = v
end
end
return map
end
for action, keys in pairs(unpack_map()) do
util.map_keys(bufnr, 'n', keys, function()
local index = find_node_index()
if not index then
return
end
local node = ctx[index]
api.nvim_win_close(self.winid, true)
-- if buffer same as normal buffer write it first
if node.bufnr == main_bufnr and vim.bo[node.bufnr].modified then
vim.cmd('write!')
end
if bufnr == main_bufnr then
if action ~= 'edit' then
vim.cmd(action .. ' ' .. vim.uri_to_fname(node.uri))
end
else
vim.cmd(action .. ' ' .. vim.uri_to_fname(node.uri))
end
if not node.wipe then
self.restore_opts.restore()
end
api.nvim_win_set_cursor(0, { node.pos[1] + 1, node.pos[2] })
local width = #api.nvim_get_current_line()
libs.jump_beacon({ node.pos[1], node.pos[2] }, width)
clean_ctx()
end, opts)
end
local function quit_fn()
local index = find_node_index()
if not index or not self:has_peek_win() then
return
end
api.nvim_win_close(self.winid, true)
for _, node in pairs(ctx) do
if type(node) == 'table' then
vim.tbl_map(function(k)
pcall(api.nvim_buf_del_keymap, node.bufnr, 'n', k)
end, config.definition)
end
end
clean_ctx()
end
util.map_keys(bufnr, 'n', config.definition.quit, quit_fn, opts)
end
local function get_method(index)
local tbl = { 'textDocument/definition', 'textDocument/typeDefinition' }
return tbl[index]
end
local function create_window(node)
local cur_winline = fn.winline()
local max_height = math.floor(vim.o.lines * config.definition.height)
local max_width = math.floor(vim.o.columns * config.definition.width)
def.restore_opts = window.restore_option()
local opt = {
relative = 'cursor',
no_override_size = true,
height = max_height,
width = max_width,
}
if vim.o.lines - opt.height - cur_winline < 0 then
vim.cmd('normal! zz')
local keycode = api.nvim_replace_termcodes('5<C-e>', true, false, true)
api.nvim_feedkeys(keycode, 'x', false)
local function get_node_idx(list, winid)
for i, node in ipairs(list) do
if node.winid == winid then
return i
end
end
local content_opts = {
contents = {},
enter = true,
highlight = {
border = 'DefinitionBorder',
normal = 'DefinitionNormal',
},
}
--@deprecated when 0.9 release
if fn.has('nvim-0.9') == 1 and config.ui.title then
opt.title = title_text(vim.uri_to_fname(node.uri))
end
return window.create_win_with_border(content_opts, opt)
end
local in_process = 0
local function in_def_wins(list, bufnr)
local wins = fn.win_findbuf(bufnr)
local in_def = false
for _, id in ipairs(wins) do
if get_node_idx(list, id) then
in_def = true
break
end
end
return in_def
end
function def:close_all()
vim.opt.eventignore:append('WinClosed')
local function recursive(tbl)
local node = tbl[#tbl]
if api.nvim_win_is_valid(node.winid) then
api.nvim_win_close(node.winid, true)
end
if not node.wipe and not in_def_wins(tbl, node.bufnr) then
self:delete_maps(node.bufnr)
end
table.remove(tbl, #tbl)
if #tbl ~= 0 then
recursive(tbl)
end
end
recursive(self.list)
clean_ctx()
vim.opt.eventignore:remove('WinClosed')
end
function def:apply_maps(bufnr)
for action, map in pairs(config.definition.keys) do
if action ~= 'close' then
util.map_keys(bufnr, map, function()
local fname = api.nvim_buf_get_name(0)
local index = get_node_idx(self.list, api.nvim_get_current_win())
local pos = {
self.list[index].selectionRange.start.line + 1,
self.list[index].selectionRange.start.character,
}
if action == 'quit' then
vim.cmd[action]()
return
end
self:close_all()
vim.cmd[action](fname)
api.nvim_win_set_cursor(0, pos)
beacon({ pos[1] - 1, 0 }, #api.nvim_get_current_line())
end)
else
util.map_keys(bufnr, map, function()
self:close_all()
end)
end
end
end
function def:delete_maps(bufnr)
for _, map in pairs(config.definition.keys) do
buf_del_keymap(bufnr, 'n', map)
end
end
function def:create_win(bufnr, root_dir)
local fname = api.nvim_buf_get_name(bufnr)
fname = fname:sub(#root_dir + 2)
if not self.list or vim.tbl_isempty(self.list) then
local float_opt = {
width = math.floor(api.nvim_win_get_width(0) * config.definition.width),
height = math.floor(api.nvim_win_get_height(0) * config.definition.height),
bufnr = bufnr,
}
if config.ui.title then
float_opt.title = fname
float_opt.title_pos = 'center'
end
return win
:new_float(float_opt, true)
:winopt('winbar', '')
:winhl('SagaNormal', 'SagaBorder')
:wininfo()
end
local win_conf = api.nvim_win_get_config(self.list[#self.list].winid)
win_conf.bufnr = bufnr
win_conf.title = fname
win_conf.row = win_conf.row[false] + 1
win_conf.col = win_conf.col[false] + 1
win_conf.height = win_conf.height - 1
win_conf.width = win_conf.width - 2
return win:new_float(win_conf, true, true):wininfo()
end
function def:clean_event()
api.nvim_create_autocmd('WinClosed', {
group = api.nvim_create_augroup('SagaPeekdefinition', { clear = true }),
callback = function(args)
local curwin = tonumber(args.file)
local index = get_node_idx(self.list or {}, curwin)
if not index then
return
end
if self.list[index].restore then
self.opt_restore()
end
local prev = self.list[index - 1] and self.list[index - 1] or nil
table.remove(self.list, index)
if prev then
api.nvim_set_current_win(prev.winid)
end
if api.nvim_buf_is_loaded(args.buf) then
if not in_def_wins(self.list, args.buf) then
self:delete_maps(args.buf)
end
end
if not self.list or #self.list == 0 then
clean_ctx()
api.nvim_del_autocmd(args.id)
end
end,
desc = '[Lspsaga] peek definition clean data event',
})
end
function def:peek_definition(method)
local cur_winid = api.nvim_get_current_win()
if in_process == cur_winid then
if self.pending_reqeust then
vim.notify(
'[Lspsaga] There is already a peek_definition request, please wait for the response.',
vim.log.levels.WARN
@ -198,7 +167,11 @@ function def:peek_definition(method)
return
end
in_process = cur_winid
if not self.list then
self.list = {}
self:clean_event()
end
local current_buf = api.nvim_get_current_buf()
-- push a tag stack
@ -210,86 +183,40 @@ function def:peek_definition(method)
local params = lsp.util.make_position_params()
local method_name = get_method(method)
self.opt_restore = win:minimal_restore()
lsp.buf_request_all(current_buf, method_name, params, function(results)
in_process = 0
if not results or next(results) == nil then
self.pending_request = true
lsp.buf_request(current_buf, method_name, params, function(_, result, context)
self.pending_request = false
if not result or next(result) == nil then
vim.notify(
'[Lspsaga] response of request method ' .. method_name .. ' is nil',
'[Lspsaga] response of request method ' .. method_name .. ' is empty',
vim.log.levels.WARN
)
return
end
local result
for _, res in pairs(results) do
if res and res.result and not vim.tbl_isempty(res.result) then
result = res.result
end
end
if not result then
vim.notify(
'[Lspsaga] response of request method ' .. method_name .. ' is nil',
vim.log.levels.WARN
)
return
end
local node = get_uri_data(result)
if not node or not node.bufnr then
return
end
if not self.winid or not api.nvim_win_is_valid(self.winid) then
_, self.winid = create_window(node)
end
api.nvim_win_set_buf(self.winid, node.bufnr)
api.nvim_set_option_value(
'winhl',
'Normal:DefinitionNormal,FloatBorder:DefinitionBorder',
{ scope = 'local', win = self.winid }
)
api.nvim_set_option_value('winbar', '', { scope = 'local', win = self.winid })
if node.wipe then
local node = {
bufnr = vim.uri_to_bufnr(result[1].targetUri or result[1].uri),
selectionRange = result[1].targetSelectionRange or result[1].range,
}
if not api.nvim_buf_is_loaded(node.bufnr) then
fn.bufload(node.bufnr)
api.nvim_set_option_value('bufhidden', 'wipe', { buf = node.bufnr })
node.wipe = true
end
vim.bo[node.bufnr].modifiable = true
--set the initail cursor pos
api.nvim_win_set_cursor(self.winid, { node.pos[1] + 1, node.pos[2] })
vim.cmd('normal! zt')
push(node)
self:apply_action_keys(node.bufnr, current_buf)
api.nvim_create_autocmd({ 'WinLeave' }, {
buffer = self.bufnr,
callback = function(opt)
window.nvim_close_valid_window(self.winid)
api.nvim_del_autocmd(opt.id)
end,
})
api.nvim_create_autocmd('WinClosed', {
once = true,
buffer = node.bufnr,
callback = function(opt)
local curwin = api.nvim_get_current_win()
if curwin == self.winid then
api.nvim_del_autocmd(opt.id)
for _, item in pairs(ctx) do
if type(item) == 'table' then
vim.tbl_map(function(k)
pcall(api.nvim_buf_del_keymap, item.bufnr, 'n', k)
end, config.definition)
end
end
end
end,
})
local root_dir = lsp.get_client_by_id(context.client_id).config.root_dir
_, node.winid = self:create_win(node.bufnr, root_dir)
api.nvim_win_set_cursor(
node.winid,
{ node.selectionRange.start.line + 1, node.selectionRange.start.character }
)
beacon(
{ node.selectionRange.start.line, node.selectionRange.start.character },
#api.nvim_get_current_line()
)
self:apply_maps(node.bufnr)
self.list[#self.list + 1] = node
end)
end
@ -328,7 +255,7 @@ function def:goto_definition(method)
api.nvim_win_set_cursor(0, { res.range.start.line + 1, res.range.start.character })
local width = #api.nvim_get_current_line()
libs.jump_beacon({ res.range.start.line, res.range.start.character }, width)
beacon({ res.range.start.line, res.range.start.character }, width)
end
if method == 1 then
lsp.buf.definition()
@ -337,13 +264,4 @@ function def:goto_definition(method)
end
end
def = setmetatable(def, {
__newindex = function(_, k, v)
ctx[k] = v
end,
__index = function(_, k, _)
return ctx[k]
end,
})
return def
return setmetatable(ctx, def)

View file

@ -1,687 +0,0 @@
local config = require('lspsaga').config
local act = require('lspsaga.codeaction')
local window = require('lspsaga.window')
local libs = require('lspsaga.libs')
local util = require('lspsaga.util')
local diag_conf = config.diagnostic
local diagnostic = vim.diagnostic
local api, fn = vim.api, vim.fn
local ns = api.nvim_create_namespace('DiagnosticJump')
local nvim_buf_set_keymap = api.nvim_buf_set_keymap
local nvim_buf_del_keymap = api.nvim_buf_del_keymap
local diag = {}
local ctx = {}
function diag.__newindex(t, k, v)
rawset(t, k, v)
end
diag.__index = diag
--- clean ctx table data
---@private
local function clean_ctx()
for k, _ in pairs(ctx) do
ctx[k] = nil
end
end
function diag:get_diagnostic_sign(severity)
local type = self:get_diag_type(severity)
local prefix = 'DiagnosticSign'
local sign_conf = fn.sign_getdefined(prefix .. type)
if not sign_conf or vim.tbl_isempty(sign_conf) then
return type:gsub(1, 1)
end
local icon = (sign_conf[1] and sign_conf[1].text) and sign_conf[1].text or type:gsub(1, 1)
return icon
end
function diag:get_diag_type(severity)
local type = { 'Error', 'Warn', 'Info', 'Hint' }
return type[severity]
end
local function clean_msg(msg)
local pattern = '%(.+%)%S$'
if msg:find(pattern) then
return msg:gsub(pattern, '')
end
return msg
end
function diag:code_action_cb(hi_name)
if not self.bufnr or not api.nvim_buf_is_loaded(self.bufnr) then
return
end
if not self.action_tuples or next(self.action_tuples) == nil then
return
end
local win_conf = api.nvim_win_get_config(self.winid)
local contents = {
libs.gen_truncate_line(win_conf.width),
config.ui.actionfix .. 'Actions',
}
for index, client_with_actions in pairs(self.action_tuples) do
if #client_with_actions ~= 2 then
vim.notify('There is something wrong in aciton_tuples')
return
end
if client_with_actions[2].title then
local title = clean_msg(client_with_actions[2].title)
local action_title = '[[' .. index .. ']] ' .. title
contents[#contents + 1] = action_title
end
end
local increase = window.win_height_increase(contents, math.abs(win_conf.width / vim.o.columns))
local start_line = api.nvim_buf_line_count(self.bufnr) + 1
api.nvim_win_set_config(self.winid, { height = win_conf.height + increase + #contents })
api.nvim_buf_set_option(self.bufnr, 'modifiable', true)
api.nvim_buf_set_lines(self.bufnr, -1, -1, false, contents)
api.nvim_buf_set_option(self.bufnr, 'modifiable', false)
api.nvim_buf_add_highlight(self.bufnr, 0, hi_name, start_line - 1, 0, -1)
api.nvim_buf_add_highlight(self.bufnr, 0, 'ActionFix', start_line, 0, #config.ui.actionfix)
api.nvim_buf_add_highlight(self.bufnr, 0, 'TitleString', start_line, #config.ui.actionfix, -1)
for i = 3, #contents do
local row = start_line + i - 2
api.nvim_buf_add_highlight(self.bufnr, 0, 'CodeActionText', row, 6, -1)
end
if diag_conf.jump_num_shortcut then
for num, _ in pairs(self.action_tuples or {}) do
nvim_buf_set_keymap(self.main_buf, 'n', tostring(num), '', {
noremap = true,
nowait = true,
callback = function()
act:do_code_action(nil, self.action_tuples[num], self.enriched_ctx)
self:clean_data()
end,
})
end
end
local function get_num()
local line = api.nvim_get_current_line()
local num = line:match('%[(%d+)%]')
if num then
num = tonumber(num)
end
return num
end
api.nvim_create_autocmd('CursorMoved', {
buffer = self.bufnr,
callback = function()
local curline = api.nvim_win_get_cursor(self.winid)[1]
if curline > 3 then
local num = get_num()
if not num then
return
end
local tuple = vim.deepcopy(self.action_tuples[num])
self.preview_winid = act:action_preview(self.winid, self.main_buf, hi_name, tuple)
end
end,
desc = 'Lspsaga show code action preview in diagnostic window',
})
local function scroll_with_preview(direction)
api.nvim_win_call(self.winid, function()
local curlnum = api.nvim_win_get_cursor(self.winid)[1]
local lines = api.nvim_buf_line_count(self.bufnr)
local col = 6
if curlnum < 4 then
curlnum = 4
elseif curlnum >= 4 then
curlnum = curlnum + direction > lines and 4 or curlnum + direction
end
api.nvim_win_set_cursor(self.winid, { curlnum, col })
api.nvim_buf_clear_namespace(self.bufnr, ns, 0, -1)
if curlnum > 3 then
api.nvim_buf_add_highlight(self.bufnr, ns, 'FinderSelection', curlnum - 1, 6, -1)
end
local num = get_num()
if not num then
return
end
local tuple = vim.deepcopy(self.action_tuples[num])
if tuple then
self.preview_winid = act:action_preview(self.winid, self.main_buf, hi_name, tuple)
end
end)
end
nvim_buf_set_keymap(self.main_buf, 'n', config.scroll_preview.scroll_down, '', {
noremap = true,
nowait = true,
callback = function()
scroll_with_preview(1)
end,
})
nvim_buf_set_keymap(self.main_buf, 'n', config.scroll_preview.scroll_up, '', {
noremap = true,
nowait = true,
callback = function()
scroll_with_preview(-1)
end,
})
end
local function cursor_diagnostic()
local diags = require('lspsaga.showdiag'):get_diagnostic({ cursor = true })
local res = {}
for _, entry in ipairs(diags) do
res[#res + 1] = {
message = entry.message,
code = entry.code or nil,
codeDescription = entry.codeDescription or nil,
data = entry.data or nil,
tags = entry.tags or nil,
relatedInformation = entry.relatedInformation or nil,
source = entry.source or nil,
severity = entry.severity or nil,
range = {
start = {
line = entry.lnum,
},
['end'] = {
line = entry.end_lnum,
},
},
}
end
return res
end
function diag:do_code_action()
local line = api.nvim_get_current_line()
local num = line:match('%[(%d+)%]')
if not num then
return
end
num = tonumber(num)
local action
action = self.action_tuples[num] and vim.deepcopy(self.action_tuples[num]) or nil
local enriched_ctx = vim.deepcopy(self.enriched_ctx)
self:clean_data()
if action then
act:do_code_action(num, action, enriched_ctx)
end
end
function diag:clean_data()
window.nvim_close_valid_window({ self.winid, self.preview_winid })
libs.delete_scroll_map(self.main_buf)
for num, _ in pairs(self.action_tuples or {}) do
pcall(nvim_buf_del_keymap, self.main_buf, 'n', tostring(num))
end
clean_ctx()
end
function diag:apply_map()
local opts = { nowait = true }
util.map_keys(self.bufnr, 'n', diag_conf.keys.exec_action, function()
self:do_code_action()
end, opts)
util.map_keys(self.bufnr, 'n', diag_conf.keys.quit, function()
self:clean_data()
end, opts)
end
function diag:get_diag_counts(entrys)
--E W I W
local counts = { 0, 0, 0, 0 }
for _, item in ipairs(entrys) do
counts[item.severity] = counts[item.severity] + 1
end
return counts
end
local function source_clean(source)
if source == 'typescript' then
return 'ts'
end
return source
end
function diag:render_diagnostic_window(entry, option)
option = option or {}
self.main_buf = api.nvim_get_current_buf()
local diag_type = self:get_diag_type(entry.severity)
local sign = self:get_diagnostic_sign(entry.severity)
local source = ''
if entry.source then
source = source .. source_clean(entry.source)
end
if entry.code then
source = source .. '(' .. entry.code .. ')'
end
local content = {}
content = vim.split(entry.message, '\n', { trimempty = true })
content[1] = sign .. ' ' .. content[1]
local source_col
if #source > 0 then
source_col = #content[1] + 1
content[1] = content[1] .. ' ' .. source
end
if diag_conf.extend_relatedInformation then
if entry.user_data.lsp.relatedInformation and #entry.user_data.lsp.relatedInformation > 0 then
vim.tbl_map(function(item)
if item.location and item.location.range then
local fname
if item.location.uri then
fname = fn.fnamemodify(vim.uri_to_fname(item.location.uri), ':t')
end
local range = '('
.. item.location.range.start.line + 1
.. ':'
.. item.location.range.start.character
.. '): '
item.message = fname and fname .. range .. item.message or range .. item.message
end
content[#content + 1] = (' '):rep(3) .. item.message
end, entry.user_data.lsp.relatedInformation)
end
end
local hi_name = 'Diagnostic' .. diag_type
if diag_conf.show_code_action and libs.get_client_by_cap('codeActionProvider') then
local cursor_diags = cursor_diagnostic()
act:send_code_action_request(self.main_buf, {
context = { diagnostics = cursor_diags },
range = {
start = { entry.lnum + 1, entry.col },
['end'] = { entry.lnum + 1, entry.col },
},
silent = true,
}, function(action_tuples, enriched_ctx)
self.action_tuples = action_tuples
self.enriched_ctx = enriched_ctx
act:clean_context()
self:code_action_cb(hi_name)
end)
end
local max_width = math.floor(vim.o.columns * diag_conf.max_width)
local max_len = window.get_max_content_length(content)
if max_len < max_width then
max_width = max_len
elseif max_width - max_len > 15 then
max_width = max_len + 10
end
local increase = window.win_height_increase(content, diag_conf.max_width)
local content_opts = {
contents = content,
filetype = 'markdown',
wrap = true,
highlight = {
border = diag_conf.border_follow and hi_name or 'DiagnosticBorder',
normal = 'DiagnosticNormal',
},
}
local opts = {
relative = 'cursor',
style = 'minimal',
width = max_width,
height = #content + increase,
no_size_override = true,
focusable = true,
}
self.bufnr, self.winid = window.create_win_with_border(content_opts, opts)
vim.wo[self.winid].conceallevel = 2
vim.wo[self.winid].concealcursor = 'niv'
vim.wo[self.winid].showbreak = 'NONE'
vim.wo[self.winid].breakindent = true
vim.wo[self.winid].breakindentopt = 'shift:0'
vim.wo[self.winid].linebreak = false
api.nvim_buf_add_highlight(self.bufnr, 0, hi_name, 0, 0, #sign)
for i, _ in ipairs(content) do
local start = i == 1 and #sign or 3
api.nvim_buf_add_highlight(
self.bufnr,
0,
diag_conf.text_hl_follow and hi_name or 'DiagnosticText',
i - 1,
start,
-1
)
end
if source_col then
api.nvim_buf_add_highlight(self.bufnr, 0, 'DiagnosticSource', 0, source_col, -1)
end
local current_buffer = api.nvim_get_current_buf()
api.nvim_create_autocmd('BufLeave', {
buffer = self.bufnr,
once = true,
callback = function()
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
api.nvim_win_close(self.preview_winid, true)
self.preview_winid = nil
self.preview_bufnr = nil
end
end,
})
api.nvim_create_autocmd('BufLeave', {
buffer = current_buffer,
once = true,
callback = function()
vim.defer_fn(function()
local cur = api.nvim_get_current_buf()
if
cur ~= current_buffer
and cur ~= self.bufnr
and self.bufnr
and api.nvim_buf_is_loaded(self.bufnr)
then
api.nvim_win_close(self.winid, true)
clean_ctx()
end
end, 0)
end,
})
self:apply_map()
local close_autocmds = { 'CursorMoved', 'InsertEnter' }
local winid = self.winid
vim.defer_fn(function()
libs.close_preview_autocmd(
current_buffer,
{ self.winid, self.preview_winid or nil },
close_autocmds,
function()
if winid == self.winid then
self:clean_data()
end
end
)
end, 0)
end
function diag:move_cursor(entry)
local current_winid = api.nvim_get_current_win()
api.nvim_win_call(current_winid, function()
-- Save position in the window's jumplist
vim.cmd("normal! m'")
if entry.col == 0 then
local text = api.nvim_buf_get_text(entry.bufnr, entry.lnum, 0, entry.lnum, -1, {})[1]
local scol = text:find('%S')
if scol ~= 0 then
entry.col = scol
end
end
api.nvim_win_set_cursor(current_winid, { entry.lnum + 1, entry.col })
local width = entry.end_col - entry.col
if width <= 0 then
width = #api.nvim_get_current_line()
end
libs.jump_beacon({ entry.lnum, entry.col }, width)
-- Open folds under the cursor
vim.cmd('normal! zv')
end)
self:render_diagnostic_window(entry)
end
function diag:goto_next(opts)
local incursor = require('lspsaga.showdiag'):get_diagnostic({ cursor = true })
local entry
if next(incursor) ~= nil and not (self.winid and api.nvim_win_is_valid(self.winid)) then
entry = incursor[1]
else
entry = diagnostic.get_next(opts)
end
if not entry then
return
end
self:move_cursor(entry)
end
function diag:goto_prev(opts)
local incursor = require('lspsaga.showdiag'):get_diagnostic({ cursor = true })
local entry
if next(incursor) ~= nil and not (self.winid and api.nvim_win_is_valid(self.winid)) then
entry = incursor[1]
else
entry = diagnostic.get_prev(opts)
end
if not entry then
return
end
self:move_cursor(entry)
end
function diag:close_exist_win()
local has = false
if self.winid and api.nvim_win_is_valid(self.winid) then
has = true
api.nvim_win_close(self.winid, true)
act:clean_context()
end
clean_ctx()
return has
end
local function on_top_right(content)
local width = window.get_max_content_length(content)
if width >= math.floor(vim.o.columns * 0.75) then
width = math.floor(vim.o.columns * 0.5)
end
local opt = {
relative = 'editor',
row = 1,
col = vim.o.columns - width,
height = #content,
width = width,
focusable = false,
}
return opt
end
local function get_row_col(content)
local res = {}
local curwin = api.nvim_get_current_win()
local max_len = window.get_max_content_length(content)
local current_col = api.nvim_win_get_cursor(curwin)[2]
local end_col = api.nvim_strwidth(api.nvim_get_current_line())
local winwidth = api.nvim_win_get_width(curwin)
if current_col < end_col then
current_col = end_col
end
if winwidth - max_len > current_col + 20 then
res.row = fn.winline() - 1
res.col = current_col + 20
else
res.row = fn.winline() + 1
res.col = current_col + 20
end
return res
end
local function theme_bg()
local conf = api.nvim_get_hl_by_name('Normal', true)
if conf.background then
return conf.background
end
return 'NONE'
end
function diag:on_insert()
local winid, bufnr
local function max_width(content)
local width = window.get_max_content_length(content)
if width == vim.o.columns - 10 then
width = vim.o.columns * 0.6
end
return width
end
local function create_window(content, buf)
local float_opt
if not config.diagnostic.on_insert_follow then
float_opt = on_top_right(content)
else
local res = get_row_col(content)
float_opt = {
relative = 'win',
win = api.nvim_get_current_win(),
width = max_width(content),
height = #content,
row = res.row,
col = res.col,
focusable = false,
}
end
return window.create_win_with_border({
contents = content,
bufnr = buf or nil,
winblend = config.diagnostic.insert_winblend,
highlight = {
normal = 'DiagnosticInsertNormal',
},
noborder = true,
}, float_opt)
end
local function set_lines(content)
if bufnr and api.nvim_buf_is_loaded(bufnr) then
api.nvim_buf_set_lines(bufnr, 0, -1, false, content)
end
end
local function reduce_width()
if not winid or not api.nvim_win_is_valid(winid) then
return
end
api.nvim_win_hide(winid)
end
local group = api.nvim_create_augroup('Lspsaga Diagnostic on insert', { clear = true })
api.nvim_create_autocmd('DiagnosticChanged', {
group = group,
callback = function(opt)
if api.nvim_get_mode().mode ~= 'i' then
set_lines({})
return
end
local content = {}
local hi = {}
local diagnostics = opt.data.diagnostics
local lnum = api.nvim_win_get_cursor(0)[1] - 1
for _, item in pairs(diagnostics) do
if item.lnum == lnum then
hi[#hi + 1] = 'Diagnostic' .. self:get_diag_type(item.severity)
if item.message:find('\n') then
item.message = item.message:gsub('\n', '')
end
content[#content + 1] = item.message
end
end
if #content == 0 then
set_lines({})
reduce_width()
return
end
if not winid or not api.nvim_win_is_valid(winid) then
bufnr, winid =
create_window(content, (bufnr and api.nvim_buf_is_valid(bufnr)) and bufnr or nil)
vim.bo[bufnr].modifiable = true
vim.wo[winid].wrap = true
if fn.has('nvim-0.9') == 1 then
api.nvim_set_option_value('fillchars', 'lastline: ', { scope = 'local', win = winid })
end
end
set_lines(content)
if bufnr and api.nvim_buf_is_loaded(bufnr) then
for i = 1, #hi do
api.nvim_buf_add_highlight(bufnr, 0, hi[i], i - 1, 0, -1)
end
end
api.nvim_set_hl(0, 'DiagnosticInsertNormal', {
background = theme_bg(),
default = true,
})
if not diag_conf.on_insert_follow then
api.nvim_win_set_config(winid, on_top_right(content))
return
end
local curwin = api.nvim_get_current_win()
local res = get_row_col(content)
api.nvim_win_set_config(winid, {
relative = 'win',
win = curwin,
height = #content,
width = max_width(content),
row = res.row,
col = res.col,
})
end,
})
api.nvim_create_autocmd('ModeChanged', {
group = group,
callback = function()
if winid and api.nvim_win_is_valid(winid) then
set_lines({})
reduce_width()
end
end,
})
api.nvim_create_user_command('DiagnosticInsertDisable', function()
if winid and api.nvim_win_is_valid(winid) then
api.nvim_win_close(winid, true)
winid = nil
bufnr = nil
end
api.nvim_del_augroup_by_id(group)
end, {})
end
return setmetatable(ctx, diag)

View file

@ -0,0 +1,454 @@
local api, fn = vim.api, vim.fn
local config = require('lspsaga').config
local act = require('lspsaga.codeaction')
local win = require('lspsaga.window')
local util = require('lspsaga.util')
local diag_conf = config.diagnostic
local diagnostic = vim.diagnostic
local ns = api.nvim_create_namespace('DiagnosticJump')
local jump_beacon = require('lspsaga.beacon').jump_beacon
local nvim_buf_del_keymap = api.nvim_buf_del_keymap
local action_preview = require('lspsaga.codeaction.preview').action_preview
local preview_win_close = require('lspsaga.codeaction.preview').preview_win_close
local diag = {}
local ctx = {}
function diag.__newindex(t, k, v)
rawset(t, k, v)
end
diag.__index = diag
--- clean ctx table data
---@private
local function clean_ctx()
for k, _ in pairs(ctx) do
ctx[k] = nil
end
end
local function get_num()
local line = api.nvim_get_current_line()
return line:match('%[(%d+)%]')
end
---get the line or cursor diagnostics
---@param opt table
function diag:get_diagnostic(opt)
local cur_buf = api.nvim_get_current_buf()
if opt.buffer then
return vim.diagnostic.get(cur_buf)
end
local line, col = unpack(api.nvim_win_get_cursor(0))
local entrys = vim.diagnostic.get(cur_buf, { lnum = line - 1 })
if opt.line then
return entrys
end
if opt.cursor then
local res = {}
for _, v in pairs(entrys) do
if v.col <= col and v.end_col >= col then
res[#res + 1] = v
end
end
return res
end
return vim.diagnostic.get()
end
local function clean_msg(msg)
local pattern = '%(.+%)%S$'
if msg:find(pattern) then
return msg:gsub(pattern, '')
end
return msg
end
function diag:code_action_cb(action_tuples, enriched_ctx)
if not self.bufnr or not api.nvim_buf_is_loaded(self.bufnr) then
return
end
local win_conf = api.nvim_win_get_config(self.winid)
local contents = {
util.gen_truncate_line(win_conf.width),
config.ui.actionfix .. 'Actions',
}
for index, client_with_actions in pairs(action_tuples) do
if #client_with_actions ~= 2 then
vim.notify('There is something wrong in aciton_tuples')
return
end
if client_with_actions[2].title then
local title = clean_msg(client_with_actions[2].title)
local action_title = '[[' .. index .. ']] ' .. title
contents[#contents + 1] = action_title
end
end
local increase = util.win_height_increase(contents, math.abs(win_conf.width / vim.o.columns))
local start_line = api.nvim_buf_line_count(self.bufnr) + 1
win
:from_exist(self.bufnr, self.winid)
:winsetconf({ height = win_conf.height + increase + #contents })
:bufopt('modifiable', true)
:setlines(contents, -1, -1)
:bufopt('modifiable', false)
api.nvim_buf_add_highlight(self.bufnr, 0, 'Comment', start_line - 1, 0, -1)
api.nvim_buf_add_highlight(self.bufnr, 0, 'ActionFix', start_line, 0, #config.ui.actionfix)
api.nvim_buf_add_highlight(self.bufnr, 0, 'SagaTitle', start_line, #config.ui.actionfix, -1)
for i = 3, #contents do
local row = start_line + i - 2
api.nvim_buf_add_highlight(self.bufnr, 0, 'CodeActionText', row, 6, -1)
end
if diag_conf.jump_num_shortcut then
for num, _ in pairs(action_tuples or {}) do
util.map_keys(self.main_buf, tostring(num), function()
local action = action_tuples[num][2]
local client = vim.lsp.get_client_by_id(action_tuples[num][1])
act:do_code_action(action, client, enriched_ctx)
end)
end
end
api.nvim_create_autocmd('CursorMoved', {
buffer = self.bufnr,
callback = function()
local curline = api.nvim_win_get_cursor(self.winid)[1]
if curline > 4 then
local tuple = action_tuples[tonumber(get_num())]
action_preview(self.winid, self.main_buf, tuple)
end
end,
desc = 'Lspsaga show code action preview in diagnostic window',
})
local function scroll_with_preview(direction)
api.nvim_win_call(self.winid, function()
local curlnum = api.nvim_win_get_cursor(self.winid)[1]
local lines = api.nvim_buf_line_count(self.bufnr)
local sline = start_line + 2
local col = 6
if curlnum < sline then
curlnum = sline
elseif curlnum >= sline then
curlnum = curlnum + direction > lines and sline or curlnum + direction
end
api.nvim_win_set_cursor(self.winid, { curlnum, col })
api.nvim_buf_clear_namespace(self.bufnr, ns, sline, -1)
if curlnum >= sline then
api.nvim_buf_add_highlight(self.bufnr, ns, 'SagaSelect', curlnum - 1, 6, -1)
end
local tuple = action_tuples[tonumber(get_num())]
if tuple then
action_preview(self.winid, self.main_buf, tuple)
end
end)
end
util.map_keys(self.bufnr, diag_conf.keys.exec_action, function()
self:close_win()
self:do_code_action(action_tuples, enriched_ctx)
end)
util.map_keys(self.main_buf, config.scroll_preview.scroll_down, function()
scroll_with_preview(1)
end)
util.map_keys(self.main_buf, config.scroll_preview.scroll_up, function()
scroll_with_preview(-1)
end)
end
---get original lsp diagnostic
function diag:get_cursor_diagnostic()
local diags = diag:get_diagnostic({ cursor = true })
local res = {}
for _, entry in ipairs(diags) do
res[#res + 1] = {
message = entry.message,
code = entry.code or nil,
codeDescription = entry.codeDescription or nil,
data = entry.data or nil,
tags = entry.tags or nil,
relatedInformation = entry.relatedInformation or nil,
source = entry.source or nil,
severity = entry.severity or nil,
range = {
start = {
line = entry.lnum,
},
['end'] = {
line = entry.end_lnum,
},
},
}
end
return res
end
function diag:do_code_action(action_tuples, enriched_ctx)
local num = get_num()
if not num then
return
end
if action_tuples[num] then
act:do_code_action(num, action_tuples[num], enriched_ctx)
self:close_win()
end
self:clean_data()
end
function diag:clean_data()
util.close_win(self.winid)
pcall(util.delete_scroll_map, self.main_buf)
for num, _ in pairs(self.action_tuples or {}) do
nvim_buf_del_keymap(self.main_buf, 'n', tostring(num))
end
clean_ctx()
end
function diag:get_diag_counts(entrys)
--E W I W
local counts = { 0, 0, 0, 0 }
for _, item in ipairs(entrys) do
counts[item.severity] = counts[item.severity] + 1
end
return counts
end
function diag:render_diagnostic_window(entry, option)
option = option or {}
self.main_buf = api.nvim_get_current_buf()
local hi_name = 'Diagnostic' .. vim.diagnostic.severity[entry.severity]
local content = vim.split(entry.message, '\n', { trimempty = true })
if diag_conf.extend_relatedInformation then
if entry.user_data.lsp.relatedInformation and #entry.user_data.lsp.relatedInformation > 0 then
vim.tbl_map(function(item)
if item.location and item.location.range then
local fname
if item.location.uri then
fname = fn.fnamemodify(vim.uri_to_fname(item.location.uri), ':t')
end
local range = '('
.. item.location.range.start.line + 1
.. ':'
.. item.location.range.start.character
.. '): '
content[#content + 1] = fname and fname .. range .. item.message or range .. item.message
end
end, entry.user_data.lsp.relatedInformation)
end
end
if diag_conf.show_code_action then
act:send_request(self.main_buf, {
context = { diagnostics = self:get_cursor_diagnostic() },
range = {
start = { entry.lnum + 1, entry.col },
['end'] = { entry.lnum + 1, entry.col },
},
}, function(action_tuples, enriched_ctx)
self:code_action_cb(action_tuples, enriched_ctx)
end)
end
local virt = {}
if entry.source then
virt[#virt + 1] = { entry.source, 'Comment' }
end
if entry.code then
virt[#virt + 1] = { ' ' .. entry.code, 'Comment' }
end
local max_width = math.floor(vim.o.columns * diag_conf.max_width)
local max_len = util.get_max_content_length(content)
+ (entry.source and #entry.source or 0)
+ (entry.code and #tostring(entry.code) or 0)
+ 2
local increase = util.win_height_increase(content, diag_conf.max_width)
local float_opt = {
relative = 'cursor',
width = math.min(max_width, max_len),
height = #content + increase,
focusable = true,
}
if config.ui.title then
float_opt.title = { { vim.diagnostic.severity[entry.severity], hi_name } }
end
self.bufnr, self.winid = win
:new_float(float_opt)
:setlines(content)
:bufopt({
['filetype'] = 'markdown',
['modifiable'] = false,
['bufhidden'] = 'wipe',
['buftype'] = 'nofile',
})
:winopt({
['conceallevel'] = 2,
['concealcursor'] = 'niv',
['showbreak'] = 'NONE',
['breakindent'] = true,
['breakindentopt'] = 'shift:0',
['linebreak'] = false,
})
:winhl('DiagnosticNormal', diag_conf.border_follow and hi_name or 'DiagnosticBorder')
:wininfo()
api.nvim_buf_set_extmark(self.bufnr, ns, #content - 1, 0, {
virt_text = virt,
hl_mode = 'combine',
})
for i, _ in ipairs(content) do
api.nvim_buf_add_highlight(
self.bufnr,
0,
diag_conf.text_hl_follow and hi_name or 'DiagnosticText',
i - 1,
0,
-1
)
end
api.nvim_create_autocmd('BufLeave', {
buffer = self.bufnr,
once = true,
callback = function()
preview_win_close()
end,
})
api.nvim_create_autocmd('BufLeave', {
buffer = self.main_buf,
once = true,
callback = function()
vim.defer_fn(function()
local cur = api.nvim_get_current_buf()
if
cur ~= self.main_buf
and cur ~= self.bufnr
and self.bufnr
and api.nvim_buf_is_loaded(self.bufnr)
then
api.nvim_win_close(self.winid, true)
clean_ctx()
end
end, 0)
end,
})
util.map_keys(self.bufnr, diag_conf.keys.quit, function()
self:clean_data()
end)
local close_autocmds = { 'CursorMoved', 'InsertEnter' }
vim.defer_fn(function()
api.nvim_create_autocmd(close_autocmds, {
buffer = self.main_buf,
once = true,
callback = function(args)
preview_win_close()
if self.before_winid then
api.nvim_win_close(self.before_winid, true)
self.before_winid = nil
elseif self.winid then
self:clean_data()
end
api.nvim_del_autocmd(args.id)
end,
})
end, 0)
end
function diag:move_cursor(entry)
local current_winid = api.nvim_get_current_win()
if self.winid then
self.before_winid = self.winid
end
api.nvim_win_call(current_winid, function()
-- Save position in the window's jumplist
vim.cmd("normal! m'")
if entry.col == 0 then
local text = api.nvim_buf_get_text(entry.bufnr, entry.lnum, 0, entry.lnum, -1, {})[1]
local scol = text:find('%S')
if scol ~= 0 then
entry.col = scol
end
end
api.nvim_win_set_cursor(current_winid, { entry.lnum + 1, entry.col })
local width = entry.end_col - entry.col
if width <= 0 then
width = #api.nvim_get_current_line()
end
jump_beacon({ entry.lnum, entry.col }, width)
-- Open folds under the cursor
vim.cmd('normal! zv')
end)
self:render_diagnostic_window(entry)
end
function diag:goto_next(opts)
local incursor = self:get_diagnostic({ cursor = true })
local entry
if next(incursor) ~= nil and not (self.winid and api.nvim_win_is_valid(self.winid)) then
entry = incursor[1]
else
entry = diagnostic.get_next(opts)
end
if not entry then
return
end
self:move_cursor(entry)
end
function diag:goto_prev(opts)
local incursor = self:get_diagnostic({ cursor = true })
local entry
if next(incursor) ~= nil and not (self.winid and api.nvim_win_is_valid(self.winid)) then
entry = incursor[1]
else
entry = diagnostic.get_prev(opts)
end
if not entry then
return
end
self:move_cursor(entry)
end
function diag:close_exist_win()
local has = false
if self.winid and api.nvim_win_is_valid(self.winid) then
has = true
api.nvim_win_close(self.winid, true)
act:clean_context()
end
clean_ctx()
return has
end
return setmetatable(ctx, diag)

View file

@ -0,0 +1,339 @@
local api, fn = vim.api, vim.fn
local win = require('lspsaga.window')
local util = require('lspsaga.util')
local diag = require('lspsaga.diagnostic')
local config = require('lspsaga').config
local beacon = require('lspsaga.beacon').jump_beacon
local ui = config.ui
local diag_conf = config.diagnostic
local ns = api.nvim_create_namespace('SagaDiagnostic')
local nvim_buf_set_extmark = api.nvim_buf_set_extmark
local nvim_buf_add_highlight = api.nvim_buf_add_highlight
local nvim_buf_set_lines = api.nvim_buf_set_lines
local ctx = {}
local sd = {}
sd.__index = sd
function sd.__newindex(t, k, v)
rawset(t, k, v)
end
--- clean ctx
local function clean_ctx()
for i, _ in pairs(ctx) do
ctx[i] = nil
end
end
local function new_node()
return {
next = nil,
diags = {},
expand = false,
lnum = 0,
}
end
---single linked list
local function generate_list(entrys)
local list = new_node()
local curnode
for _, item in ipairs(entrys) do
if #list.diags == 0 then
curnode = list
elseif item.bufnr ~= curnode.diags[#curnode.diags].bufnr then
if not curnode.next then
curnode.next = new_node()
end
curnode = curnode.next
end
curnode.diags[#curnode.diags + 1] = item
end
return list
end
local function find_node(list, lnum)
local curnode = list
while curnode do
if curnode.lnum == lnum then
return curnode
end
curnode = curnode.next
end
end
local function range_node_winline(node, val)
while node do
node.lnum = node.lnum + val
node = node.next
end
end
function sd:layout_normal()
self.bufnr, self.winid = win
:new_normal('sp', self.bufnr)
:bufopt({
['modifiable'] = false,
['filetype'] = 'sagadiagnostc',
['expandtab'] = false,
})
:winopt({
['number'] = false,
['relativenumber'] = false,
['stc'] = '',
})
:wininfo()
api.nvim_win_set_height(self.winid, 10)
end
function sd:layout_float(opt)
local curbuf = api.nvim_get_current_buf()
local content = api.nvim_buf_get_lines(self.bufnr, 0, -1, false)
local increase = util.win_height_increase(content)
local max_len = util.get_max_content_length(content)
local max_height = math.floor(vim.o.lines * diag_conf.max_show_height)
local max_width = math.floor(vim.o.columns * diag_conf.max_show_width)
local float_opt = {
width = math.min(max_width, max_len),
height = math.min(max_height, #content + increase),
bufnr = self.bufnr,
}
local enter = true
if ui.title then
if opt.buffer then
float_opt.title = 'Buffer'
elseif opt.line then
float_opt.title = 'Line'
elseif opt.cursor then
float_opt.title = 'Cursor'
else
float_opt.title = 'Workspace'
end
float_opt.title_pos = 'center'
end
local close_autocmds =
{ 'CursorMoved', 'CursorMovedI', 'InsertEnter', 'BufDelete', 'WinScrolled' }
if vim.tbl_contains(opt.args, '++unfocus') then
opt.focusable = false
close_autocmds[#close_autocmds] = 'BufLeave'
enter = false
else
opt.focusable = true
api.nvim_create_autocmd('BufEnter', {
callback = function(args)
if not self.winid or not api.nvim_win_is_valid(self.winid) then
pcall(api.nvim_del_autocmd, args.id)
end
local cur_buf = api.nvim_get_current_buf()
if cur_buf ~= self.bufnr and self.winid and api.nvim_win_is_valid(self.winid) then
api.nvim_win_close(self.winid, true)
clean_ctx()
pcall(api.nvim_del_autocmd, args.id)
end
end,
})
end
self.bufnr, self.winid = win
:new_float(float_opt, enter)
:bufopt({
['filetype'] = 'markdown',
['modifiable'] = false,
['buftype'] = 'nofile',
})
:winopt({
['conceallevel'] = 2,
['concealcursor'] = 'niv',
})
:winhl('DiagnosticShowNormal', 'DiagnsoticShowBorder')
:wininfo()
api.nvim_win_set_cursor(self.winid, { 2, 3 })
for _, key in ipairs(diag_conf.keys.quit_in_show) do
util.map_keys(self.bufnr, key, function()
local curwin = api.nvim_get_current_win()
if curwin ~= self.winid then
return
end
if api.nvim_win_is_valid(curwin) then
api.nvim_win_close(curwin, true)
clean_ctx()
end
end)
end
vim.defer_fn(function()
api.nvim_create_autocmd(close_autocmds, {
buffer = curbuf,
once = true,
callback = function(args)
if self.winid and api.nvim_win_is_valid(self.winid) then
api.nvim_win_close(self.winid, true)
end
api.nvim_del_autocmd(args.id)
end,
})
end, 0)
end
function sd:write_line(message, severity, virt_line, srow, erow)
local indent = (' '):rep(3)
srow = srow or -1
erow = erow or -1
if message:find('\n') then
message = vim.split(message, '\n')
message = table.concat(message)
end
nvim_buf_set_lines(self.bufnr, srow, erow, false, { indent .. message })
nvim_buf_add_highlight(
self.bufnr,
0,
'Diagnostic' .. vim.diagnostic.severity[severity],
srow,
0,
-1
)
nvim_buf_set_extmark(self.bufnr, ns, srow, 0, {
virt_text = {
{ virt_line, 'SagaVirtLine' },
{ ui.lines[4], 'SagaVirtLine' },
{ ui.lines[4], 'SagaVirtLine' },
},
virt_text_pos = 'overlay',
hl_mode = 'combine',
})
end
local function msg_fmt(entry)
return entry.message
.. ' '
.. entry.lnum
.. ':'
.. entry.col
.. ':'
.. entry.bufnr
.. ' '
.. (entry.source and entry.source or '')
.. (entry.code and entry.code or '')
end
function sd:toggle_or_jump(entrys_list)
local lnum = api.nvim_win_get_cursor(0)[1]
local node = find_node(entrys_list, lnum)
if not node then
local line = api.nvim_get_current_line()
local info = line:match('%s(%d+:%d+:%d+)')
if not info then
return
end
api.nvim_win_close(self.winid, true)
local ln, col, bn = unpack(vim.split(info, ':'))
local wins = fn.win_findbuf(tonumber(bn))
if #wins == 0 then
---@diagnostic disable-next-line: param-type-mismatch
api.nvim_win_set_buf(0, tonumber(bn))
wins[#wins] = 0
end
api.nvim_win_set_cursor(wins[#wins], { tonumber(ln) + 1, tonumber(col) })
beacon({ tonumber(ln), 0 }, #api.nvim_get_current_line())
clean_ctx()
return
end
vim.bo[self.bufnr].modifiable = true
if node.expand == true then
api.nvim_buf_clear_namespace(self.bufnr, ns, lnum - 1, lnum + #node.diags)
nvim_buf_set_lines(self.bufnr, lnum, lnum + #node.diags, false, {})
node.expand = false
nvim_buf_set_extmark(self.bufnr, ns, lnum - 1, 0, {
virt_text = { { ui.expand, 'SagaToggle' } },
virt_text_pos = 'overlay',
hl_mode = 'combine',
})
range_node_winline(node.next, -#node.diags)
vim.bo[self.bufnr].modifiable = false
return
end
if node.expand == false then
nvim_buf_set_extmark(self.bufnr, ns, lnum - 1, 0, {
virt_text = { { ui.collapse, 'SagaToggle' } },
virt_text_pos = 'overlay',
hl_mode = 'combine',
})
for i, item in ipairs(node.diags) do
local mes = msg_fmt(item)
local virt_start = i == #node.diags and ui.lines[1] or ui.lines[2]
self:write_line(mes, item.severity, virt_start, lnum, lnum)
lnum = lnum + 1
end
node.expand = true
range_node_winline(node.next, #node.diags)
end
vim.bo[self.bufnr].modifiable = false
end
function sd:show(opt)
self.bufnr = api.nvim_create_buf(false, false)
local curnode = opt.entrys_list
local count = 0
while curnode do
curnode.expand = true
for i, entry in ipairs(curnode.diags) do
local virt_start = i == #curnode.diags and ui.lines[1] or ui.lines[2]
local mes = msg_fmt(entry)
if i == 1 then
---@diagnostic disable-next-line: param-type-mismatch
local fname = fn.fnamemodify(api.nvim_buf_get_name(tonumber(entry.bufnr)), ':t')
-- local counts = diag:get_diag_counts(curnode.diags)
local text = ' ' .. fname
nvim_buf_set_lines(self.bufnr, count, -1, false, { text })
nvim_buf_set_extmark(self.bufnr, ns, count, 0, {
virt_text = {
{ ui.collapse, 'SagaCollapse' },
},
virt_text_pos = 'overlay',
hl_mode = 'combine',
})
count = count + 1
curnode.lnum = count
end
self:write_line(mes, entry.severity, virt_start, count)
count = count + 1
end
curnode = curnode.next
end
local layout = diag_conf.show_layout
if vim.tbl_contains(opt.args, '++float') then
layout = 'float'
elseif vim.tbl_contains(opt.args, '++normal') then
layout = 'normal'
end
if layout == 'float' then
self:layout_float(opt)
else
self:layout_normal()
end
api.nvim_win_set_cursor(self.winid, { 2, 3 })
util.map_keys(self.bufnr, diag_conf.keys.toggle_or_jump, function()
self:toggle_or_jump(opt.entrys_list)
end)
end
function sd:show_diagnostics(opt)
local entrys = diag:get_diagnostic(opt)
if next(entrys) == nil then
return
end
opt.entrys_list = generate_list(entrys)
self:show(opt)
end
return setmetatable(ctx, sd)

View file

@ -0,0 +1,45 @@
local api = vim.api
local ns = vim.api.nvim_create_namespace('DiagnosticCurLine')
---render diagnostic virtual text only on current line
---make sure disable neovim builtin diagnostic virtual
---text by using `vim.diagnsotic.config`
---```lua
---vim.diagnostic.config({
--- virtual_text = false
---})
---```
local function changed(bufnr)
api.nvim_create_autocmd({ 'CursorMoved', 'DiagnosticChanged' }, {
buffer = bufnr,
callback = function(args)
if args.buf ~= api.nvim_get_current_buf() then
return
end
vim.api.nvim_buf_clear_namespace(args.buf, ns, 0, -1)
local curline = vim.api.nvim_win_get_cursor(0)[1]
local diagnostics = vim.diagnostic.get(args.buf, { lnum = curline - 1 })
local virt_texts = { { (' '):rep(4) } }
for _, diag in ipairs(diagnostics) do
virt_texts[#virt_texts + 1] =
{ diag.message, 'Diagnostic' .. vim.diagnostic.severity[diag.severity] }
end
api.nvim_buf_set_extmark(args.buf, ns, curline - 1, 0, {
virt_text = virt_texts,
hl_mode = 'combine',
})
end,
})
end
local function diag_on_current()
api.nvim_create_autocmd('LspAttach', {
callback = function(args)
changed(args.buf)
end,
})
end
return {
diag_on_current = diag_on_current,
}

View file

@ -1,934 +0,0 @@
local api, lsp, fn, uv = vim.api, vim.lsp, vim.fn, vim.loop
local config = require('lspsaga').config
local window = require('lspsaga.window')
local libs = require('lspsaga.libs')
local util = require('lspsaga.util')
local ui = config.ui
local nvim_buf_set_extmark = api.nvim_buf_set_extmark
local ns_id = api.nvim_create_namespace('lspsagafinder')
local co = coroutine
local finder = {}
local ctx = {}
local function clean_ctx()
for k, _ in pairs(ctx) do
ctx[k] = nil
end
end
finder.__index = finder
finder.__newindex = function(t, k, v)
rawset(t, k, v)
end
local function get_titles(index)
local t = {
'● Definition',
'● Implements',
'● References',
}
return t[index]
end
local function methods(index)
local t = {
'textDocument/definition',
'textDocument/implementation',
'textDocument/references',
}
return index and t[index] or t
end
local function supports_implement(buf)
local support = false
for _, client in ipairs(lsp.get_active_clients({ bufnr = buf })) do
if client.supports_method('textDocument/implementation') then
support = true
break
end
end
return support
end
function finder:lsp_finder()
-- push a tag stack
local pos = api.nvim_win_get_cursor(0)
self.main_buf = api.nvim_get_current_buf()
self.main_win = api.nvim_get_current_win()
local from = { self.main_buf, pos[1], pos[2], 0 }
local items = { { tagname = fn.expand('<cword>'), from = from } }
fn.settagstack(self.main_win, { items = items }, 't')
self.request_status = {}
self.lspdata = {}
local params = lsp.util.make_position_params()
---@diagnostic disable-next-line: param-type-mismatch
local meths = methods()
if not supports_implement(self.main_buf) then
self.request_status[meths[2]] = true
---@diagnostic disable-next-line: param-type-mismatch
table.remove(meths, 2)
end
---@diagnostic disable-next-line: param-type-mismatch
for _, method in ipairs(meths) do
self:do_request(params, method)
end
-- make a spinner
self:loading_bar()
end
function finder:request_done()
local done = true
---@diagnostic disable-next-line: param-type-mismatch
for _, method in ipairs(methods()) do
if not self.request_status[method] then
done = false
break
end
end
return done
end
function finder:loading_bar()
local opts = {
relative = 'cursor',
height = 2,
width = 20,
}
local content_opts = {
contents = {},
buftype = 'nofile',
border = 'solid',
highlight = {
normal = 'FinderNormal',
border = 'FinderBorder',
},
enter = false,
}
local spin_buf, spin_win = window.create_win_with_border(content_opts, opts)
local spin_config = {
spinner = {
'█▁▁▁▁▁▁▁▁▁',
'██▁▁▁▁▁▁▁▁',
'███▁▁▁▁▁▁▁',
'████▁▁▁▁▁▁',
'█████▁▁▁▁▁',
'██████▁▁▁▁',
'███████▁▁▁',
'████████▁▁ ',
'█████████▁',
'██████████',
},
interval = 50,
timeout = config.request_timeout,
}
api.nvim_buf_set_option(spin_buf, 'modifiable', true)
local spin_frame = 1
local spin_timer = uv.new_timer()
local start_request = uv.now()
spin_timer:start(
0,
spin_config.interval,
vim.schedule_wrap(function()
spin_frame = spin_frame == 11 and 1 or spin_frame
local msg = ' LOADING' .. string.rep('.', spin_frame > 3 and 3 or spin_frame)
local spinner = ' ' .. spin_config.spinner[spin_frame]
pcall(api.nvim_buf_set_lines, spin_buf, 0, -1, false, { msg, spinner })
pcall(api.nvim_buf_add_highlight, spin_buf, 0, 'FinderSpinnerTitle', 0, 0, -1)
pcall(api.nvim_buf_add_highlight, spin_buf, 0, 'FinderSpinner', 1, 0, -1)
spin_frame = spin_frame + 1
if uv.now() - start_request >= spin_config.timeout and not spin_timer:is_closing() then
spin_timer:stop()
spin_timer:close()
if api.nvim_buf_is_loaded(spin_buf) then
api.nvim_buf_delete(spin_buf, { force = true })
end
window.nvim_close_valid_window(spin_win)
vim.notify('request timeout')
return
end
if self:request_done() and not spin_timer:is_closing() then
spin_timer:stop()
spin_timer:close()
if api.nvim_buf_is_loaded(spin_buf) then
api.nvim_buf_delete(spin_buf, { force = true })
end
window.nvim_close_valid_window(spin_win)
self:render_finder()
end
end)
)
end
function finder:do_request(params, method)
if method == methods(3) then
params.context = { includeDeclaration = false }
end
lsp.buf_request_all(self.current_buf, method, params, function(results)
local result = {}
for _, res in pairs(results or {}) do
if res.result and not (res.result.uri or res.result.targetUri) then
libs.merge_table(result, res.result)
elseif res.result and (res.result.uri or res.result.targetUri) then
result[#result + 1] = res.result
end
end
if vim.tbl_isempty(result) then
self.request_status[method] = true
return
end
local uri = result[1].uri or result[1].targetUri
local range = result[1].targetRange or result[1].range
local line = api.nvim_win_get_cursor(0)[1]
if
method == methods(1)
and vim.uri_to_bufnr(uri) == api.nvim_get_current_buf()
and range.start.line == line
then
local col = api.nvim_win_get_cursor(0)[2]
if col >= range.start.character and col <= range['end'].character then
self.request_status[method] = true
return
end
end
self:create_finder_data(result, method)
self.request_status[method] = true
end)
end
function finder:create_finder_data(result, method)
if #result == 1 and result[1].inline then
return
end
if not self.wipe_buffers then
self.wipe_buffers = {}
end
if not self.lspdata[method] then
self.lspdata[method] = {}
local title = get_titles(libs.tbl_index(methods(), method))
self.lspdata[method].title = title .. '' .. #result
self.lspdata[method].count = #result
end
local parent = self.lspdata[method]
parent.data = {}
for i, res in ipairs(result) do
local uri = res.targetUri or res.uri
if not uri then
vim.notify('[Lspsaga] miss uri in server response', vim.log.levels.WARN)
return
end
local bufnr = vim.uri_to_bufnr(uri)
local fname = vim.uri_to_fname(uri) -- returns lowercase drive letters on Windows
local range = res.targetSelectionRange or res.targetRange or res.range
if libs.iswin then
fname = fname:gsub('^%l', fname:sub(1, 1):upper())
end
fname = table.concat(libs.get_path_info(bufnr, 2), libs.path_sep)
local node = {
bufnr = bufnr,
fname = fname,
row = range.start.line,
col = range.start.character,
ecol = range['end'].character,
method = method,
winline = -1,
}
if not api.nvim_buf_is_loaded(bufnr) or not vim.treesitter.highlighter.active[bufnr] then
node.wipe = true
--ignore the FileType event avoid trigger the lsp
vim.opt.eventignore:append({ 'FileType' })
fn.bufload(bufnr)
--restore eventignore
vim.opt.eventignore:remove({ 'FileType' })
if not vim.tbl_contains(self.wipe_buffers, bufnr) then
self.wipe_buffers[#self.wipe_buffers + 1] = bufnr
end
end
if node.ecol < node.col then
local tmp = node.ecol
node.ecol = node.col
node.col = tmp
end
local start_col = 0
--avoid the preview code too long
if node.col > 15 then
start_col = node.col - 10
end
node.word = api.nvim_buf_get_text(node.bufnr, node.row, start_col, node.row, node.ecol, {})[1]
if node.word:find('^%s') then
node.word = node.word:sub(node.word:find('%S'), #node.word)
end
if not parent.data[node.fname] then
parent.data[node.fname] = {
expand = true,
nodes = {},
}
end
if i == #result then
node.tail = true
end
parent.data[node.fname].nodes[#parent.data[node.fname].nodes + 1] = node
end
end
local function get_max_height()
return math.floor(vim.o.lines * config.finder.max_height)
end
function finder:render_finder()
local width = {}
self.bufnr = api.nvim_create_buf(false, false)
local float_height = get_max_height()
self.render_fn = co.create(function(need_yield)
local indent = (' '):rep(2)
local virt_hi = 'Finderlines'
local line_count = 0
---@diagnostic disable-next-line: param-type-mismatch
for i, method in pairs(methods()) do
local meth_data = self.lspdata[method]
if not meth_data then
goto skip
end
local title = { meth_data.title }
if i > 1 and api.nvim_buf_line_count(self.bufnr) ~= 1 then
table.insert(title, 1, '')
end
api.nvim_buf_set_lines(self.bufnr, line_count, line_count, false, title)
width[#width + 1] = #meth_data.title
line_count = line_count + #title
api.nvim_buf_add_highlight(self.bufnr, ns_id, 'FinderType', line_count - 1, 4, 16)
api.nvim_buf_add_highlight(self.bufnr, ns_id, 'FinderIcon', line_count - 1, 0, 4)
api.nvim_buf_add_highlight(self.bufnr, ns_id, 'FinderCount', line_count - 1, 16, -1)
local first = true
for fname, item in pairs(meth_data.data) do
local text = indent .. ui.collapse .. ' ' .. fname .. ' ' .. #item.nodes
indent = (' '):rep(5)
api.nvim_buf_set_lines(self.bufnr, line_count, line_count + 1, false, { text })
width[#width + 1] = #text
line_count = line_count + 1
local start = line_count
api.nvim_buf_add_highlight(self.bufnr, ns_id, 'SagaCollapse', line_count - 1, 0, 5)
api.nvim_buf_add_highlight(self.bufnr, ns_id, 'FinderFname', line_count - 1, 6, -1)
for k, v in pairs(item.nodes) do
local tbl = {
{ k == #item.nodes and ui.lines[1] or ui.lines[2], virt_hi },
{ ui.lines[4]:rep(2), virt_hi },
}
if first then
v.first = true
first = false
meth_data.start = start
end
v.start = start
v.idx = k
v.count = #item.nodes
text = indent .. v.word
api.nvim_buf_set_lines(self.bufnr, line_count, line_count + 1, false, { text })
width[#width + 1] = #text
line_count = line_count + 1
api.nvim_buf_add_highlight(self.bufnr, ns_id, 'FinderCode', line_count - 1, 5, -1)
v.winline = v.winline > -1 and v.winline or line_count
nvim_buf_set_extmark(self.bufnr, ns_id, line_count - 1, 2, {
virt_text = tbl,
virt_text_pos = 'overlay',
})
if line_count > float_height + 10 and need_yield then
table.sort(width)
need_yield = co.yield(width[#width])
end
end
indent = ' '
end
::skip::
end
if api.nvim_buf_line_count(self.bufnr) == 0 then
clean_ctx()
vim.notify('[Lspsaga] finder nothing to show', vim.log.levels.WARN)
return
end
api.nvim_buf_set_lines(self.bufnr, line_count, line_count + 1, false, { '' })
vim.bo[self.bufnr].modifiable = false
end)
self:apply_map()
while true do
local _, float_width = co.resume(self.render_fn, true)
if not float_width and co.status(self.render_fn) == 'dead' then
table.sort(width)
float_width = width[#width]
end
if not float_width then
print('[lspsaga] no data to show')
return
end
self:create_finder_win(float_width)
break
end
end
function finder:create_finder_win(width)
self.group = api.nvim_create_augroup('lspsaga_finder', { clear = true })
local opt = {
relative = 'editor',
width = width,
height = get_max_height(),
no_size_override = true,
}
local winline = fn.winline()
if vim.o.lines - 6 - opt.height - winline <= 0 then
vim.cmd('normal! zz')
local keycode = api.nvim_replace_termcodes('6<C-e>', true, false, true)
api.nvim_feedkeys(keycode, 'x', false)
end
winline = fn.winline()
opt.row = winline + 1
local wincol = fn.wincol()
opt.col = fn.screencol() - math.floor(wincol * 0.4)
local side_char = window.border_chars()['top'][config.ui.border]
local normal_right_side = ' '
local content_opts = {
contents = {},
filetype = 'lspsagafinder',
bufhidden = 'wipe',
bufnr = self.bufnr,
enter = true,
border_side = {
['right'] = config.ui.border == 'shadow' and '' or normal_right_side,
['righttop'] = config.ui.border == 'shadow' and '' or side_char,
['rightbottom'] = config.ui.border == 'shadow' and '' or side_char,
},
highlight = {
border = 'finderBorder',
normal = 'finderNormal',
},
}
vim.bo[self.bufnr].buftype = 'nofile'
self.restore_opts = window.restore_option()
_, self.winid = window.create_win_with_border(content_opts, opt)
-- make sure close preview window by using wincmd
api.nvim_create_autocmd('WinClosed', {
buffer = self.bufnr,
once = true,
callback = function()
local ok, buf = pcall(api.nvim_win_get_buf, self.peek_winid)
if ok then
pcall(api.nvim_buf_clear_namespace, buf, self.preview_hl_ns, 0, -1)
end
pcall(api.nvim_del_augroup_by_id, self.group)
self:close_auto_preview_win()
self:clean_data()
clean_ctx()
end,
})
local before, start = 0, 0
local ns_select = api.nvim_create_namespace('FinderSelect')
api.nvim_create_autocmd('CursorMoved', {
buffer = self.bufnr,
callback = function()
local curline = api.nvim_win_get_cursor(self.winid)[1]
api.nvim_buf_clear_namespace(self.bufnr, ns_select, 0, -1)
local col = 5
local buf_lines = api.nvim_buf_line_count(self.bufnr)
local text = api.nvim_get_current_line()
local in_fname = text:find(ui.expand) or text:find(ui.collapse)
local node
if curline == 1 or curline > buf_lines - 1 then
curline = 3
start = 2
node = self:get_node({ lnum = 3 })
elseif curline == 2 and curline < before then
curline = buf_lines - 1
node = self:get_node({ lnum = curline })
start = node.start
elseif text:find('%sDef') or text:find('%sRef') or text:find('%sImp') or #text == 0 then
local increase = curline > before and 1 or -1
for _, v in ipairs({
curline,
curline + increase,
curline + increase * 2,
curline + increase * 3,
}) do
node = self:get_node({ lnum = v })
if node then
curline = node.winline
start = node.start
break
end
end
elseif not in_fname then
node = self:get_node({ lnum = curline })
start = node.start
end
col = in_fname and 7 or col
before = curline
api.nvim_win_set_cursor(self.winid, { curline, col })
api.nvim_buf_add_highlight(
self.bufnr,
ns_select,
'FinderStart',
start - 1,
#ui.collapse + 2,
-1
)
api.nvim_buf_add_highlight(self.bufnr, ns_select, 'FinderSelection', curline - 1, 5, -1)
if node then
self:open_preview(node)
end
end,
})
if self.render_fn and co.status(self.render_fn) == 'suspended' then
co.resume(self.render_fn, false)
end
end
local function unpack_map()
local map = {}
for k, v in pairs(config.finder.keys) do
if k ~= 'jump_to' and k ~= 'close_in_preview' and k ~= 'expand_or_jump' and k ~= 'quit' then
map[k] = v
end
end
return map
end
function finder:apply_map()
local opts = {
nowait = true,
silent = true,
}
for action, keys in pairs(unpack_map()) do
util.map_keys(self.bufnr, 'n', keys, function()
local curline = api.nvim_win_get_cursor(self.winid)[1]
local node = self:get_node({ lnum = curline })
if not node then
return
end
self:do_action(node, action)
end, opts)
end
util.map_keys(self.bufnr, 'n', config.finder.keys.quit, function()
local ok, buf = pcall(api.nvim_win_get_buf, self.peek_winid)
if ok then
pcall(api.nvim_buf_clear_namespace, buf, self.preview_hl_ns, 0, -1)
end
window.nvim_close_valid_window({ self.winid, self.peek_winid })
self:clean_data()
clean_ctx()
end, opts)
util.map_keys(self.bufnr, 'n', config.finder.keys.jump_to, function()
if self.peek_winid and api.nvim_win_is_valid(self.peek_winid) then
api.nvim_set_current_win(self.peek_winid)
end
end, opts)
local function expand_or_collapse(text, curline)
local fname = text:match(ui.expand .. '%s(.+)%s')
if not fname then
fname = text:match(ui.collapse .. '%s(.+)%s')
end
if not fname then
return
end
local nodes = self:find_nodes_by_fname(fname)
vim.bo[self.bufnr].modifiable = true
if not self.lspdata[nodes[1].method].data[nodes[1].fname].expand then
text = text:gsub(ui.expand, ui.collapse)
local lines = vim.tbl_map(function(i)
return (' '):rep(5) .. i.word
end, nodes)
table.insert(lines, 1, text)
api.nvim_buf_set_lines(self.bufnr, curline - 1, curline, false, lines)
for i = 1, #nodes do
api.nvim_buf_set_extmark(self.bufnr, ns_id, curline - 1 + i, 2, {
virt_text = {
{ i == #nodes and ui.lines[1] or ui.lines[2], 'FinderLines' },
{ ui.lines[4]:rep(2), 'FinderLines' },
},
virt_text_pos = 'overlay',
})
api.nvim_buf_add_highlight(self.bufnr, ns_id, 'FinderCode', curline - 1 + i, 5, -1)
end
self:change_node_winline(function(item)
return item.winline > curline
end, #nodes)
for i, v in ipairs(nodes) do
v.winline = curline + i
end
api.nvim_win_set_cursor(self.winid, { curline + 1, 5 })
api.nvim_buf_add_highlight(self.bufnr, ns_id, 'SagaCollapse', curline - 1, 0, 5)
vim.bo[self.bufnr].modifiable = false
self.lspdata[nodes[1].method].data[nodes[1].fname].expand = true
return
end
text = text:gsub(ui.collapse, ui.expand)
api.nvim_buf_clear_namespace(self.bufnr, ns_id, curline - 1, curline + #nodes)
api.nvim_buf_set_lines(self.bufnr, curline - 1, curline + #nodes, false, { text })
api.nvim_buf_add_highlight(self.bufnr, ns_id, 'SagaExpand', nodes[1].start - 1, 0, 5)
self.lspdata[nodes[1].method].data[fname].expand = false
self:change_node_winline(function(item)
return item.winline > curline + #nodes
end, -#nodes)
for _, v in ipairs(nodes) do
v.winline = -1
end
vim.bo[self.bufnr].modifiable = false
end
util.map_keys(self.bufnr, 'n', config.finder.keys.expand_or_jump, function()
local curline = api.nvim_win_get_cursor(self.winid)[1]
local text = api.nvim_get_current_line()
local in_fname = text:find(ui.expand) or text:find(ui.collapse)
if in_fname then
expand_or_collapse(text, curline)
return
end
local node = self:get_node({ lnum = curline })
if not node then
return
end
self:do_action(node, 'edit')
end, opts)
end
function finder:find_nodes_by_fname(fname)
for _, meth_data in pairs(self.lspdata) do
for f, item in pairs(meth_data.data) do
if f == fname then
return item.nodes
end
end
end
end
function finder:next_node_in_meth(method, cur_fname, lnum)
for fname, item in pairs(self.lspdata[method].data) do
if fname ~= cur_fname then
for i = 1, #item.nodes do
if i == 1 and item.nodes[i].winline > lnum then
return item.nodes[i]
end
end
end
end
end
function finder:change_node_winline(cond, increase)
for _, meth_data in pairs(self.lspdata) do
for _, item in pairs(meth_data.data) do
for _, node in ipairs(item.nodes) do
if cond(node) then
node.winline = node.winline + increase
node.start = node.start + increase
end
end
end
end
end
function finder:get_node(opt)
local node
for meth, meth_data in pairs(self.lspdata) do
if opt.meth and opt.meth ~= meth then
goto skip
end
for _, item in pairs(meth_data.data) do
for _, v in ipairs(item.nodes) do
if
(opt.lnum and v.winline == opt.lnum)
or (opt.first and v.first)
or (opt.tail and v.tail)
then
node = v
break
end
end
end
::skip::
end
return node
end
function finder:node_in_range(range)
for _, lnum in ipairs(range) do
local node = self:get_node({ lnum = lnum })
if node then
return node
end
end
end
local function create_preview_window(finder_winid)
if not finder_winid or not api.nvim_win_is_valid(finder_winid) then
return
end
local opts = {
relative = 'editor',
no_size_override = true,
zindex = 80,
}
local winconfig = api.nvim_win_get_config(finder_winid)
opts.row = winconfig.row[false]
opts.height = winconfig.height
local border_side = {}
local top = window.combine_char()['top'][config.ui.border]
local bottom = window.combine_char()['bottom'][config.ui.border]
--in right
if vim.o.columns - winconfig.col[false] - winconfig.width >= config.finder.min_width then
local adjust = config.ui.border == 'shadow' and -2 or 2
opts.col = winconfig.col[false] + winconfig.width + adjust
opts.width = vim.o.columns - opts.col - 2
border_side = {
['lefttop'] = top,
['leftbottom'] = bottom,
}
--in left
elseif winconfig.col[false] >= config.finder.min_width then
opts.width = math.floor(winconfig.col[false] * 0.8)
local adjust = config.ui.border == 'shadow' and -2 or 0
opts.col = winconfig.col[false] - opts.width - adjust
border_side = {
['righttop'] = top,
['rightbottom'] = bottom,
}
api.nvim_win_set_config(finder_winid, {
border = window.combine_border(config.ui.border, {
['lefttop'] = '',
['left'] = '',
['leftbottom'] = '',
}, 'FinderBorder'),
})
end
if not opts.col then
vim.notify(
'[Lspsaga] finder previee get col failed try change finder.min_width',
vim.log.levels.WARN
)
return
end
local content_opts = {
contents = {},
border_side = border_side,
bufhidden = '',
highlight = {
border = 'FinderPreviewBorder',
normal = 'FinderNormal',
},
}
return window.create_win_with_border(content_opts, opts)
end
local function clear_preview_ns(ns, buf)
pcall(api.nvim_buf_clear_namespace, buf, ns, 0, -1)
end
function finder:open_preview(node)
if self.peek_winid and api.nvim_win_is_valid(self.peek_winid) then
local before_buf = api.nvim_win_get_buf(self.peek_winid)
clear_preview_ns(ns_id, before_buf)
end
if not node then
return
end
if not self.peek_winid or not api.nvim_win_is_valid(self.peek_winid) then
self.preview_bufnr, self.peek_winid = create_preview_window(self.winid)
if not self.peek_winid then
return
end
api.nvim_create_autocmd('WinClosed', {
callback = function(opt)
local curwin = api.nvim_get_current_win()
if curwin == self.peek_winid then
local curbuf
api.nvim_win_call(curwin, function()
curbuf = api.nvim_get_current_buf()
end)
if curbuf then
clear_preview_ns(ns_id, curbuf)
pcall(api.nvim_del_autocmd, opt.id)
end
end
end,
})
api.nvim_win_set_hl_ns(self.peek_winid, ns_id)
end
local function highlight_word()
api.nvim_buf_add_highlight(node.bufnr, ns_id, 'FinderPreview', node.row, node.col, node.ecol)
end
local function apply_node_count_virtual_text()
local opts = {
virt_text = { { '<--' .. node.idx .. '/' .. node.count, 'IncSearch' } },
}
api.nvim_buf_set_extmark(node.bufnr, ns_id, node.row, node.col, opts)
end
local buf_in_peek = api.nvim_win_get_buf(self.peek_winid)
if buf_in_peek == node.bufnr then
api.nvim_win_set_cursor(self.peek_winid, { node.row + 1, node.col })
highlight_word()
apply_node_count_virtual_text()
return
end
api.nvim_win_set_buf(self.peek_winid, node.bufnr)
api.nvim_win_set_cursor(self.peek_winid, { node.row + 1, node.col })
highlight_word()
apply_node_count_virtual_text()
api.nvim_set_option_value('winbar', '', {
scope = 'local',
win = self.peek_winid,
})
api.nvim_set_option_value(
'winhl',
'Normal:finderNormal,FloatBorder:finderPreviewBorder',
{ scope = 'local', win = self.peek_winid }
)
api.nvim_create_autocmd({ 'WinEnter' }, {
buffer = self.bufnr,
callback = function(opt)
local curwin = api.nvim_get_current_win()
if curwin == self.peek_winid or curwin == self.winid then
return
end
window.nvim_close_valid_window(self.winid)
self:close_auto_preview_win()
api.nvim_del_autocmd(opt.id)
clean_ctx()
end,
})
if fn.has('nvim-0.9') == 1 and node.wipe then
local lang = require('nvim-treesitter.parsers').ft_to_lang(vim.bo[self.main_buf].filetype)
vim.defer_fn(function()
vim.treesitter.start(node.bufnr, lang)
end, 5)
node.loaded = true
elseif fn.has('nvim-0.8') == 1 and node.wipe then
vim.schedule(function()
api.nvim_buf_call(node.bufnr, function()
vim.cmd('TSBufEnable highlight')
end)
end)
end
end
function finder:close_auto_preview_win()
if self.peek_winid and api.nvim_win_is_valid(self.peek_winid) then
local buf = api.nvim_win_get_buf(self.peek_winid)
clear_preview_ns(ns_id, buf)
api.nvim_win_close(self.peek_winid, true)
self.peek_winid = nil
end
end
function finder:do_action(node, action)
if self.peek_winid and api.nvim_win_is_valid(self.peek_winid) then
local pbuf = api.nvim_win_get_buf(self.peek_winid)
clear_preview_ns(ns_id, pbuf)
end
local restore_opts
local data = vim.deepcopy(node)
local fname = api.nvim_buf_get_name(data.bufnr)
if not data.wipe then
restore_opts = self.restore_opts
end
window.nvim_close_valid_window({ self.winid, self.peek_winid, self.tip_winid or nil })
self:clean_data()
-- if buffer not saved save it before jump
if fname == api.nvim_buf_get_name(0) and vim.bo.modified then
vim.cmd('write')
end
vim.cmd(action .. ' ' .. fn.fnameescape(fname))
if restore_opts then
restore_opts.restore()
end
if data.row then
api.nvim_win_set_cursor(0, { data.row + 1, data.col })
end
local width = #api.nvim_get_current_line()
if not width or width <= 0 then
width = 10
end
if data.row then
libs.jump_beacon({ data.row, 0 }, width)
end
clean_ctx()
end
function finder:clean_data()
for _, buf in ipairs(self.wipe_buffers or {}) do
api.nvim_buf_delete(buf, { force = true })
pcall(vim.keymap.del, 'n', config.finder.keys.close_in_preview, { buffer = buf })
end
if self.preview_bufnr and api.nvim_buf_is_loaded(self.preview_bufnr) then
api.nvim_buf_delete(self.preview_bufnr, { force = true })
end
if self.group then
pcall(api.nvim_del_augroup_by_id, self.group)
end
end
return setmetatable(ctx, finder)

146
lua/lspsaga/finder/box.lua Normal file
View file

@ -0,0 +1,146 @@
local M = {}
---@diagnostic disable-next-line: deprecated
local api, uv = vim.api, vim.version().minor >= 10 and vim.uv or vim.loop
local win = require('lspsaga.window')
local config = require('lspsaga').config
function M.get_methods(args)
local methods = {
['def'] = 'textDocument/definition',
['ref'] = 'textDocument/references',
['imp'] = 'textDocument/implementation',
}
methods = vim.tbl_extend('force', methods, config.finder.methods)
local keys = vim.tbl_keys(methods)
return vim.tbl_map(function(item)
if vim.tbl_contains(keys, item) then
return methods[item]
end
end, args)
end
function M.parse_argument(args)
local methods = {}
local layout
for _, arg in ipairs(args) do
if arg:find('^%w+$') then
methods[#methods + 1] = arg
elseif arg:find('%w+%+%w+') then
methods = vim.split(arg, '+', { plain = true })
elseif arg:find('%+%+normal') then
layout = 'normal'
elseif arg:find('%+%+float') then
layout = 'float'
end
end
return methods, layout
end
function M.filter(method, results)
if vim.tbl_isempty(config.finder.filter) or not config.finder.filter[method] then
return results
end
local fn = config.finder.filter[method]
if type(fn) ~= 'function' then
vim.notify('[Lspsaga] filter must be function', vim.log.levels.ERROR)
return
end
local retval = {}
for client_id, item in pairs(results) do
retval[client_id] = {}
for _, val in ipairs(item) do
if fn(val) then
retval[client_id][#retval[client_id] + 1] = val
end
end
end
return retval
end
function M.spinner()
local timer = uv.new_timer()
local bufnr, winid = win
:new_float({
width = 10,
height = 1,
border = 'solid',
focusable = false,
noautocmd = true,
}, true)
:bufopt({
['bufhidden'] = 'wipe',
['buftype'] = 'nofile',
})
:wininfo()
local spinner = {
'●∙∙∙∙∙∙∙∙',
' ●∙∙∙∙∙∙∙',
' ●∙∙∙∙∙∙',
' ●∙∙∙∙∙',
' ●∙∙∙∙',
' ●∙∙∙',
' ●∙∙',
' ●∙',
'',
}
local frame = 1
timer:start(0, 50, function()
vim.schedule(function()
api.nvim_buf_set_lines(bufnr, 0, -1, false, { spinner[frame] })
api.nvim_buf_add_highlight(bufnr, 0, 'SagaSpinner', 0, 0, -1)
frame = frame + 1 > #spinner and 1 or frame + 1
end)
end)
return function()
if timer:is_active() and not timer:is_closing() then
timer:stop()
timer:close()
api.nvim_win_close(winid, true)
end
end
end
function M.indent_current(inlevel)
local available = { 0, 2, 4 }
local current = inlevel - 2
vim.tbl_map(function(index)
local hi = index == current and 'Type' or 'Comment'
api.nvim_set_hl(0, 'SagaIndent' .. index, { link = hi })
end, available)
end
function M.indent(ns, lbufnr, lwinid)
api.nvim_set_decoration_provider(ns, {
on_win = function(_, winid, bufnr)
if winid ~= lwinid or lbufnr ~= bufnr then
return false
end
end,
on_start = function()
if api.nvim_get_current_buf() ~= lbufnr then
return false
end
end,
on_line = function(_, winid, bufnr, row)
local inlevel = vim.fn.indent(row + 1)
if bufnr ~= lbufnr or winid ~= lwinid or inlevel == 2 then
return
end
local total = inlevel == 4 and 4 - 2 or inlevel - 1
for i = 1, total, 2 do
api.nvim_buf_set_extmark(bufnr, ns, row, i - 1, {
virt_text = { { config.ui.lines[3], 'SagaIndent' .. (i - 1) } },
virt_text_pos = 'overlay',
ephemeral = true,
})
end
end,
})
end
return M

406
lua/lspsaga/finder/init.lua Normal file
View file

@ -0,0 +1,406 @@
local api, lsp, fn = vim.api, vim.lsp, vim.fn
---@diagnostic disable-next-line: deprecated
local uv = vim.version().minor >= 10 and vim.uv or vim.loop
local ly = require('lspsaga.layout')
local slist = require('lspsaga.slist')
local box = require('lspsaga.finder.box')
local util = require('lspsaga.util')
local buf_set_lines, buf_set_extmark = api.nvim_buf_set_lines, api.nvim_buf_set_extmark
local buf_add_highlight = api.nvim_buf_add_highlight
local config = require('lspsaga').config
local select_ns = api.nvim_create_namespace('SagaSelect')
local win = require('lspsaga.window')
local beacon = require('lspsaga.beacon').jump_beacon
local fd = {}
local ctx = {}
fd.__index = fd
fd.__newindex = function(t, k, v)
rawset(t, k, v)
end
local function clean_ctx()
for key, _ in pairs(ctx) do
if type(ctx) ~= 'function' then
ctx[key] = nil
end
end
end
local ns = api.nvim_create_namespace('SagaFinder')
function fd:init_layout()
local win_width = api.nvim_win_get_width(0)
self.lbufnr, self.lwinid, _, self.rwinid = ly:new(self.layout)
:left(
math.floor(vim.o.lines * config.finder.max_height),
math.floor(win_width * config.finder.left_width)
)
:bufopt({
['filetype'] = 'sagafinder',
['buftype'] = 'nofile',
['bufhidden'] = 'wipe',
})
:right()
:bufopt({
['buftype'] = 'nofile',
['bufhidden'] = 'wipe',
})
:done()
self:apply_maps()
self:event()
end
function fd:set_toggle_icon(icon, virtid, row, col)
api.nvim_buf_set_extmark(self.lbufnr, ns, row, col, {
id = virtid,
-- virt_text_win_col = col,
virt_text = { { icon, 'SagaToggle' } },
virt_text_pos = 'overlay',
})
end
function fd:set_highlight(inlevel, line)
local hl_group, col_start
if inlevel == 2 then
hl_group = 'SagaTitle'
col_start = 2
elseif inlevel == 4 then
hl_group = 'SagaFinderFname'
col_start = 4
else
hl_group = 'SagaText'
col_start = 6
end
buf_add_highlight(self.lbufnr, ns, hl_group, line, col_start, -1)
end
function fd:method_title(method, row)
local title = vim.split(method, '/', { plain = true })[2]
title = title:upper()
local n = {
winline = row + 1,
expand = true,
virtid = uv.hrtime(),
inlevel = 2,
}
buf_set_lines(self.lbufnr, row, -1, false, { (' '):rep(2) .. title })
self:set_highlight(n.inlevel, row)
self:set_toggle_icon(config.ui.collapse, n.virtid, row, 0)
slist.tail_push(self.list, n)
end
function fd:handler(method, results, spin_close, done)
if not results or util.res_isempty(results) then
spin_close()
vim.notify(('[Lspsaga] no response of %s'):format(method), vim.log.levels.WARN)
return
end
local rendered_fname = {}
for client_id, item in pairs(results) do
for i, res in ipairs(item.result or {}) do
if not self.lbufnr then
spin_close()
self:init_layout()
vim.bo[self.lbufnr].modifiable = true
end
local row = api.nvim_buf_line_count(self.lbufnr)
row = row == 1 and row - 1 or row
local uri = res.uri or res.targetUri
if i == 1 then
self:method_title(method, row)
row = row + 1
end
local fname = vim.uri_to_fname(uri)
if not vim.tbl_contains(rendered_fname, fname) then
local node = {
count = #item.result,
expand = true,
virtid = uv.hrtime(),
inlevel = 4,
client_id = client_id,
}
local client = lsp.get_client_by_id(client_id)
node.line = fname:sub(#client.config.root_dir + 2)
buf_set_lines(self.lbufnr, -1, -1, false, { (' '):rep(4) .. node.line })
self:set_toggle_icon(config.ui.collapse, node.virtid, row, 2)
self:set_highlight(node.inlevel, row)
row = row + 1
node.winline = row
slist.tail_push(self.list, node)
end
res.bufnr = vim.uri_to_bufnr(uri)
if not api.nvim_buf_is_loaded(res.bufnr) then
fn.bufload(res.bufnr)
res.wipe = true
end
local range = res.range or res.targetSelectionRange or res.selectionRange
res.line = api.nvim_buf_get_text(
res.bufnr,
range.start.line,
range.start.character,
range['end'].line,
range['end'].character,
{}
)[1]
res.client_id = client_id
res.inlevel = 6
buf_set_lines(self.lbufnr, -1, -1, false, { (' '):rep(6) .. res.line })
rendered_fname[#rendered_fname + 1] = fname
self:set_highlight(res.inlevel, row)
row = row + 1
res.winline = row
slist.tail_push(self.list, res)
end
end
if not done then
buf_set_lines(self.lbufnr, -1, -1, false, {})
end
if done then
vim.bo[self.lbufnr].modifiable = false
spin_close()
api.nvim_win_set_cursor(self.lwinid, { 3, 6 })
box.indent(ns, self.lbufnr, self.lwinid)
api.nvim_create_autocmd('BufEnter', {
callback = function(args)
if args.buf ~= self.lbufnr or args.buf ~= self.rbufnr then
self:clean()
api.nvim_del_autocmd(args.id)
end
end,
})
end
end
function fd:event()
api.nvim_create_autocmd('CursorMoved', {
buffer = self.lbufnr,
callback = function()
if not self.lwinid or not api.nvim_win_is_valid(self.lwinid) then
return
end
local curlnum = api.nvim_win_get_cursor(self.lwinid)[1]
api.nvim_buf_clear_namespace(self.lbufnr, select_ns, 0, -1)
local inlevel = fn.indent(curlnum)
if inlevel == 6 then
buf_add_highlight(self.lbufnr, select_ns, 'String', curlnum - 1, 6, -1)
end
box.indent_current(inlevel)
local node = slist.find_node(self.list, curlnum)
if not node or not node.value.bufnr then
return
end
api.nvim_win_set_buf(self.rwinid, node.value.bufnr)
local range = node.value.range or node.value.targetSelectionRange or node.value.selectionRange
api.nvim_win_set_cursor(self.rwinid, { range.start.line + 1, range.start.character })
api.nvim_set_option_value('winbar', '', { scope = 'local', win = self.rwinid })
local rwin_conf = api.nvim_win_get_config(self.rwinid)
local client = vim.lsp.get_client_by_id(node.value.client_id)
rwin_conf.title =
util.path_sub(api.nvim_buf_get_name(node.value.bufnr), client.config.root_dir)
rwin_conf.title_pos = 'center'
api.nvim_win_set_config(self.rwinid, rwin_conf)
api.nvim_win_call(self.rwinid, function()
fn.winrestview({ topline = range.start.line + 1 })
end)
buf_add_highlight(
node.value.bufnr,
ns,
'SagaSearch',
range.start.line,
range.start.character,
range['end'].character
)
node.value.rendered = true
util.map_keys(node.value.bufnr, config.finder.keys.close, function()
self:clean()
end)
util.map_keys(node.value.bufnr, config.finder.keys.shuttle, function()
if api.nvim_get_current_win() ~= self.rwinid then
return
end
api.nvim_set_current_win(self.lwinid)
end)
end,
})
end
function fd:clean()
ly:close()
slist.list_map(self.list, function(node)
if node.value.wipe then
api.nvim_buf_delete(node.value.bufnr, { force = true })
return
end
if node.value.bufnr and api.nvim_buf_is_valid(node.value.bufnr) and node.value.rendered then
api.nvim_buf_clear_namespace(node.value.bufnr, ns, 0, -1)
pcall(api.nvim_buf_del_keymap, node.value.bufnr, 'n', config.finder.keys.close)
end
end)
clean_ctx()
end
function fd:toggle_or_open()
util.map_keys(self.lbufnr, config.finder.keys['toggle_or_open'], function()
local curlnum = api.nvim_win_get_cursor(self.lwinid)[1]
local node = slist.find_node(self.list, curlnum)
if not node then
return
end
if node.value.expand == nil then
local fname = vim.uri_to_fname(node.value.uri)
local pos = { node.value.range.start.line + 1, node.value.range.start.character }
self:clean()
local restore = win:minimal_restore()
vim.cmd.edit(fname)
restore()
api.nvim_win_set_cursor(0, pos)
beacon({ pos[1] - 1, 0 }, #api.nvim_get_current_line())
return
end
vim.bo[self.lbufnr].modifiable = true
if node.value.expand == true then
local row = curlnum + 1
while true do
local l = fn.indent(row)
if l <= node.value.inlevel or l == 0 or l == -1 then
break
end
row = row + 1
end
local count = row - curlnum - 1
self:set_toggle_icon(config.ui.expand, node.value.virtid, curlnum - 1, node.value.inlevel - 2)
buf_set_lines(self.lbufnr, curlnum, curlnum + count, false, {})
node.value.expand = false
vim.bo[self.lbufnr].modifiable = false
slist.update_winline(node, -count)
return
end
local count = 0
node.value.expand = true
self:set_toggle_icon(config.ui.collapse, node.value.virtid, curlnum - 1, node.value.inlevel - 2)
local tmp = node.next
while tmp do
buf_set_lines(
self.lbufnr,
curlnum,
curlnum,
false,
{ (' '):rep(tmp.value.inlevel) .. tmp.value.line }
)
self:set_highlight(tmp.value.inlevel, curlnum)
local islast = (not tmp.next or tmp.next.value.inlevel <= tmp.value.inlevel) and true or false
if tmp.value.expand == false then
self:set_toggle_icon(config.ui.collapse, tmp.value.virtid, curlnum, tmp.value.inlevel - 2)
tmp.value.expand = true
end
count = count + 1
curlnum = curlnum + 1
tmp.value.winline = curlnum
if not tmp or (tmp.next and tmp.next.value.inlevel <= node.value.inlevel) then
break
end
tmp = tmp.next
end
vim.bo[self.lbufnr].modifiable = false
if tmp then
slist.update_winline(tmp, count)
end
end)
end
function fd:apply_maps()
local black = { 'close', 'toggle_or_open', 'go_peek', 'quit', 'shuttle' }
for action, key in pairs(config.finder.keys) do
util.map_keys(self.lbufnr, key, function()
if not vim.tbl_contains(black, action) then
local curlnum = api.nvim_win_get_cursor(0)[1]
local curnode = slist.find_node(self.list, curlnum)
if not curnode then
return
end
local fname = api.nvim_buf_get_name(curnode.value.bufnr)
local pos = { curnode.value.range.start.line + 1, curnode.value.range.start.character }
self:clean()
local restore = win:minimal_restore()
vim.cmd[action](fname)
restore()
api.nvim_win_set_cursor(0, pos)
beacon({ pos[1], 0 }, #api.nvim_get_current_line())
return
end
if action == 'quit' then
self:clean()
return
end
if action == 'go_peek' then
api.nvim_set_current_win(self.rwinid)
return
end
end)
end
self:toggle_or_open()
util.map_keys(self.lbufnr, config.finder.keys.shuttle, function()
if api.nvim_get_current_win() ~= self.lwinid then
return
end
api.nvim_set_current_win(self.rwinid)
end)
end
function fd:new(args)
local meth, layout = box.parse_argument(args)
self.layout = layout or config.finder.layout
if #meth == 0 then
meth = vim.split(config.finder.default, '+', { plain = true })
end
local methods = box.get_methods(meth)
methods = vim.tbl_filter(function(method)
return #util.get_client_by_method(method) > 0
end, methods)
local curbuf = api.nvim_get_current_buf()
if #methods == 0 then
vim.notify(
('[Lspsaga] all server of %s buffer does not these methods %s'):format(
curbuf,
table.concat(args, ' ')
),
vim.log.levels.WARN
)
return
end
self.list = slist.new()
local params = lsp.util.make_position_params()
params.context = {
includeDeclaration = false,
}
local spin_close = box.spinner()
local count = 0
for _, method in ipairs(methods) do
lsp.buf_request_all(curbuf, method, params, function(results)
count = count + 1
results = box.filter(method, results)
self:handler(method, results, spin_close, count == #methods)
end)
end
end
return setmetatable(ctx, fd)

View file

@ -1,18 +1,14 @@
local api = vim.api
local window = require('lspsaga.window')
local win = require('lspsaga.window')
local term = {}
local ctx = {}
function term:open_float_terminal(command)
function term:open_float_terminal(args)
local cur_buf = api.nvim_get_current_buf()
if not vim.tbl_isempty(ctx) and ctx.term_bufnr == cur_buf then
api.nvim_win_close(ctx.term_winid, true)
if ctx.shadow_winid and api.nvim_win_is_valid(ctx.shadow_winid) then
api.nvim_win_close(ctx.shadow_winid, true)
end
ctx.term_winid = nil
ctx.shadow_winid = nil
if ctx.cur_win and ctx.pos then
api.nvim_set_current_win(ctx.cur_win)
api.nvim_win_set_cursor(0, ctx.pos)
@ -22,8 +18,8 @@ function term:open_float_terminal(command)
return
end
local cmd = command and command
or (require('lspsaga.libs').iswin and 'cmd.exe' or os.getenv('SHELL'))
local cmd = (#args == 1 and args[1]) and args[1]
or (require('lspsaga.util').iswin and 'cmd.exe' or os.getenv('SHELL'))
-- calculate our floating window size
local win_height = math.ceil(vim.o.lines * 0.7)
local win_width = math.ceil(vim.o.columns * 0.7)
@ -33,7 +29,7 @@ function term:open_float_terminal(command)
local col = math.ceil((vim.o.columns - win_width) * 0.5)
-- set some options
local opts = {
local float_opt = {
style = 'minimal',
relative = 'editor',
width = win_width,
@ -42,26 +38,20 @@ function term:open_float_terminal(command)
col = col,
}
local content_opts = {
contents = {},
enter = true,
bufhidden = 'hide',
highlight = {
normal = 'TerminalNormal',
border = 'TerminalBorder',
},
}
local spawn_new = vim.tbl_isempty(ctx) and true or false
if not spawn_new then
content_opts.bufnr = ctx.term_bufnr
float_opt.bufnr = ctx.term_bufnr
api.nvim_buf_set_option(ctx.term_bufnr, 'modified', false)
end
ctx.cur_win = api.nvim_get_current_win()
ctx.pos = api.nvim_win_get_cursor(0)
ctx.term_bufnr, ctx.term_winid, ctx.shadow_bufnr, ctx.shadow_winid =
window.open_shadow_float_win(content_opts, opts)
ctx.term_bufnr, ctx.term_winid = win
:new_float(float_opt, true, true)
:bufopt('bufhidden', 'hide')
:winhl('TerminalNormal', 'TerminalBorder')
:wininfo()
if spawn_new then
vim.fn.termopen(cmd, {

View file

@ -1,31 +1,25 @@
local fn, health, api = vim.fn, vim.health, vim.api
local M = {}
local nvim_09 = vim.fn.has('nvim-0.9') == 1
local start = nvim_09 and health.start or health.report_start
local ok = nvim_09 and health.ok or health.report_ok
local error = nvim_09 and health.error or health.report_error
local warn = nvim_09 and health.warn or health.report_warn
local function treesitter_check()
if fn.executable('tree-sitter') == 0 then
warn('`tree-sitter` executable not found ')
health.report_warn('`tree-sitter` executable not found ')
else
ok('`tree-sitter` found ')
health.report_ok('`tree-sitter` found ')
end
for _, parser in ipairs({ 'markdown', 'markdown_inline' }) do
local installed = #api.nvim_get_runtime_file('parser/' .. parser .. '.so', false)
if installed == 0 then
error('tree-sitter `' .. parser .. '` parser not found')
health.report_error('tree-sitter `' .. parser .. '` parser not found')
else
ok('tree-sitter `' .. parser .. '` parser found')
health.report_ok('tree-sitter `' .. parser .. '` parser found')
end
end
end
M.check = function()
start('Lspsaga.nvim report')
health.report_start('Lspsaga.nvim report')
treesitter_check()
end

View file

@ -1,54 +1,32 @@
local api = vim.api
local function theme_normal()
local conf = api.nvim_get_hl_by_name('Normal', true)
if conf.background then
return conf.background
end
return 'NONE'
end
local kind = require('lspsaga.lspkind').kind
local function hi_define()
local bg = theme_normal()
return {
-- general
TitleString = { link = 'Title' },
TitleIcon = { link = 'Repeat' },
SagaTitle = { link = 'Title' },
SagaBorder = { link = 'FloatBorder' },
SagaNormal = { bg = bg },
SagaExpand = { fg = '#475164' },
SagaCollapse = { fg = '#475164' },
SagaNormal = { link = 'NormalFloat' },
SagaToggle = { link = 'Comment' },
SagaCount = { link = 'Comment' },
SagaBeacon = { bg = '#c43963' },
SagaVirtLine = { link = 'Comment' },
SagaSpinnerTitle = { link = 'Statement' },
SagaSpinner = { link = 'Statement' },
SagaText = { link = 'Comment' },
SagaSelect = { link = 'String' },
SagaSearch = { link = 'Search' },
SagaFinderFname = { link = 'Keyword' },
SagaIndent0 = { link = 'Comment' },
SagaIndent2 = { link = 'Comment' },
SagaIndent4 = { link = 'Comment' },
-- code action
ActionFix = { link = 'Keyword' },
ActionPreviewNormal = { link = 'SagaNormal' },
ActionPreviewBorder = { link = 'SagaBorder' },
ActionPreviewTitle = { link = 'Title' },
CodeActionNormal = { link = 'SagaNormal' },
CodeActionBorder = { link = 'SagaBorder' },
CodeActionText = { link = '@variable' },
CodeActionNumber = { link = 'DiffAdd' },
-- finder
FinderSelection = { link = 'String' },
FinderFName = {},
FinderCode = { link = 'Comment' },
FinderCount = { link = 'Constant' },
FinderIcon = { link = 'Type' },
FinderType = { link = '@property' },
FinderStart = { link = 'Function' },
--finder spinner
FinderSpinnerTitle = { link = 'Statement' },
FinderSpinner = { link = 'Statement' },
FinderPreview = { link = 'Search' },
FinderLines = { link = 'Operator' },
FinderNormal = { link = 'SagaNormal' },
FinderBorder = { link = 'SagaBorder' },
FinderPreviewBorder = { link = 'SagaBorder' },
-- definition
DefinitionBorder = { link = 'SagaBorder' },
DefinitionNormal = { link = 'SagaNormal' },
DefinitionSearch = { link = 'Search' },
-- hover
HoverNormal = { link = 'SagaNormal' },
HoverBorder = { link = 'SagaBorder' },
@ -58,30 +36,21 @@ local function hi_define()
RenameMatch = { link = 'Search' },
-- diagnostic
DiagnosticBorder = { link = 'SagaBorder' },
DiagnosticSource = { link = 'Comment' },
DiagnosticNormal = { link = 'SagaNormal' },
DiagnosticText = {},
DiagnosticBufnr = { link = '@variable' },
DiagnosticFname = { link = 'KeyWord' },
DiagnosticShowNormal = { link = 'SagaNormal' },
DiagnosticShowBorder = { link = '@property' },
-- Call Hierachry
CallHierarchyNormal = { link = 'SagaNormal' },
CallHierarchyBorder = { link = 'SagaBorder' },
CallHierarchyIcon = { link = 'TitleIcon' },
CallHierarchyTitle = { link = 'Title' },
-- lightbulb
SagaLightBulb = { link = 'DiagnosticSignHint' },
-- shadow
SagaShadow = { link = 'FloatShadow' },
-- Outline
OutlineIndent = { fg = '#806d9e' },
OutlinePreviewBorder = { link = 'SagaNormal' },
OutlinePreviewNormal = { link = 'SagaBorder' },
OutlineWinSeparator = { link = 'WinSeparator' },
-- Float term
TerminalBorder = { link = 'SagaBorder' },
TerminalNormal = { link = 'SagaNormal' },
-- Implement
SagaImpIcon = { link = 'PreProc' },
--Winbar
SagaWinbarSep = { link = 'Operator' },
SagaFileName = { link = 'Comment' },
SagaFolderName = { link = 'Comment' },
}
end
@ -89,6 +58,10 @@ local function init_highlight()
for group, conf in pairs(hi_define()) do
api.nvim_set_hl(0, group, vim.tbl_extend('keep', conf, { default = true }))
end
for _, item in pairs(kind) do
api.nvim_set_hl(0, 'Saga' .. item[1], { link = item[3], default = true })
end
end
return {

View file

@ -1,70 +1,66 @@
local api, fn, lsp, util = vim.api, vim.fn, vim.lsp, vim.lsp.util
local api, fn, lsp = vim.api, vim.fn, vim.lsp
local config = require('lspsaga').config
local window = require('lspsaga.window')
local libs = require('lspsaga.libs')
local win = require('lspsaga.window')
local util = require('lspsaga.util')
local treesitter = vim.treesitter
local hover = {}
local function has_arg(args, arg)
local tbl = vim.split(args, '%s')
if vim.tbl_contains(tbl, arg) then
return true
end
return false
function hover:clean()
self.bufnr = nil
self.winid = nil
end
local function open_link()
local ts_utils = require('nvim-treesitter.ts_utils')
local node = ts_utils.get_node_at_cursor()
if node ~= nil and node:type() ~= 'inline_link' then
node = node:parent()
function hover:open_link()
if not self.bufnr or not api.nvim_buf_is_valid(self.bufnr) then
return
end
if node ~= nil and node:type() == 'inline_link' then
local path
local node = treesitter.get_node()
if not node or node:type() ~= 'inline' then
return
end
local text = treesitter.get_node_text(node, self.bufnr)
local link = text:match('%]%((.-)%)')
for i = 0, node:named_child_count() - 1, 1 do
local child = node:named_child(i)
if child:type() == 'link_destination' then
---@diagnostic disable-next-line: undefined-field
path = vim.treesitter.get_node_text(child, 0)
break
end
end
if not link then
return
end
if path:find('#') then
vim.fn.escape(path, '#')
end
local cmd
local cmd
if libs.iswin then
cmd = '!start cmd /cstart /b '
elseif libs.ismac then
cmd = 'silent !open '
else
cmd = config.hover.open_browser .. ' '
end
if vim.fn.has('mac') == 1 then
cmd = '!open'
elseif vim.fn.has('win32') == 1 then
cmd = '!explorer'
elseif vim.fn.executable('wslview') == 1 then
cmd = '!wslview'
elseif vim.fn.executable('xdg-open') == 1 then
cmd = '!xdg-open'
else
cmd = config.hover.open_browser
end
if path and path:find('file://') then
vim.cmd.edit(vim.uri_to_fname(path))
else
fn.execute(cmd .. '"' .. fn.escape(path, '#') .. '"')
end
if link:find('file://') then
vim.cmd.edit(vim.uri_to_fname(link))
else
fn.execute(cmd .. ' ' .. fn.escape(link, '#'))
end
end
function hover:open_floating_preview(res, option_fn)
vim.validate({
res = { res, 't' },
})
local bufnr = api.nvim_get_current_buf()
self.preview_bufnr = api.nvim_create_buf(false, true)
local content = vim.split(res.value, '\n', { trimempty = true })
function hover:open_floating_preview(content, option_fn)
local new = {}
local max_float_width = math.floor(vim.o.columns * config.hover.max_width)
local max_content_len = util.get_max_content_length(content)
local max_height = math.floor(vim.o.lines * config.hover.max_height)
local float_option = {
width = math.min(max_float_width, max_content_len),
zindex = 80,
}
local in_codeblock = false
for _, line in pairs(content) do
for _, line in ipairs(content) do
if line:find('\\') then
line = line:gsub('\\(?![tn])', '')
end
@ -95,177 +91,137 @@ function hover:open_floating_preview(res, option_fn)
if line:find('```') then
in_codeblock = in_codeblock and false or true
end
if line:find('&emsp;') then
line = line:gsub('&emsp;', vim.bo.filetype == 'yaml' and '' or ' ')
if line:find('^%-%-%-$') then
line = util.gen_truncate_line(float_option.width)
end
if #line > 0 then
new[#new + 1] = line
end
end
content = new
local max_float_width = math.floor(vim.o.columns * config.hover.max_width)
local max_content_len = window.get_max_content_length(content)
local increase = window.win_height_increase(content)
local max_height = math.floor(vim.o.lines * 0.8)
local float_option = {
width = max_content_len < max_float_width and max_content_len or max_float_width,
height = #content + increase > max_height and max_height or #content + increase,
no_size_override = true,
zindex = 80,
}
if fn.has('nvim-0.9') == 1 and config.ui.title then
float_option.title = {
{ config.ui.hover, 'Exception' },
{ ' Hover', 'TitleString' },
}
local tuncate_lnum = -1
for i, line in ipairs(new) do
if line:find('^─') then
tuncate_lnum = i
end
end
local increase = util.win_height_increase(new, config.hover.max_width)
float_option.height = math.min(max_height, #new + increase)
if option_fn then
local new_opt = option_fn(float_option.width)
float_option = vim.tbl_extend('keep', float_option, new_opt)
float_option = vim.tbl_extend('keep', float_option, option_fn(float_option.width))
end
local contents_opt = {
contents = content,
filetype = res.kind or 'markdown',
buftype = 'nofile',
wrap = true,
highlight = {
normal = 'HoverNormal',
border = 'HoverBorder',
},
bufnr = self.preview_bufnr,
}
_, self.preview_winid = window.create_win_with_border(contents_opt, float_option)
vim.bo[self.preview_bufnr].modifiable = false
local curbuf = api.nvim_get_current_buf()
vim.wo[self.preview_winid].conceallevel = 2
vim.wo[self.preview_winid].concealcursor = 'niv'
vim.wo[self.preview_winid].showbreak = 'NONE'
if fn.has('nvim-0.9') == 1 then
api.nvim_set_option_value(
'fillchars',
'lastline: ',
{ scope = 'local', win = self.preview_winid }
)
vim.treesitter.start(self.preview_bufnr, 'markdown')
vim.treesitter.query.set(
'markdown',
'highlights',
[[
self.bufnr, self.winid = win
:new_float(float_option, false, option_fn and true or false)
:setlines(new)
:bufopt({
['filetype'] = 'markdown',
['modifiable'] = false,
['buftype'] = 'nofile',
['bufhidden'] = 'wipe',
})
:winopt({
['conceallevel'] = 2,
['concealcursor'] = 'niv',
['showbreak'] = 'NONE',
['wrap'] = true,
})
:winhl('HoverNormal', 'HoverBorder')
:wininfo()
if tuncate_lnum > 0 then
api.nvim_buf_add_highlight(self.bufnr, 0, 'Comment', tuncate_lnum - 1, 0, -1)
end
vim.treesitter.start(self.bufnr, 'markdown')
vim.treesitter.query.set(
'markdown',
'highlights',
[[
([
(info_string)
(fenced_code_block_delimiter)
] @conceal
(#set! conceal ""))
]]
)
end
)
vim.keymap.set('n', 'q', function()
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
api.nvim_win_close(self.preview_winid, true)
self:remove_data()
util.scroll_in_float(curbuf, self.winid)
util.map_keys(self.bufnr, 'q', function()
if self.winid and api.nvim_win_is_valid(self.winid) then
api.nvim_win_close(self.winid, true)
self:clean()
end
end, { buffer = self.preview_bufnr })
end)
if not option_fn then
api.nvim_create_autocmd({ 'CursorMoved', 'InsertEnter', 'BufDelete', 'WinScrolled' }, {
buffer = bufnr,
api.nvim_create_autocmd({ 'CursorMoved', 'InsertEnter', 'BufDelete' }, {
buffer = curbuf,
once = true,
callback = function(opt)
if self.preview_bufnr and api.nvim_buf_is_loaded(self.preview_bufnr) then
libs.delete_scroll_map(bufnr)
api.nvim_buf_delete(self.preview_bufnr, { force = true })
if self.bufnr and api.nvim_buf_is_loaded(self.bufnr) then
util.delete_scroll_map(curbuf)
end
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
api.nvim_win_close(self.preview_winid, true)
self:remove_data()
end
if opt.event == 'WinScrolled' then
vim.cmd('Lspsaga hover_doc')
if self.winid and api.nvim_win_is_valid(self.winid) then
api.nvim_win_close(self.winid, true)
end
self:clean()
api.nvim_del_autocmd(opt.id)
end,
desc = '[Lspsaga] Auto close hover window',
})
self.enter_leave_id = api.nvim_create_autocmd('BufEnter', {
api.nvim_create_autocmd('BufEnter', {
callback = function(opt)
if
opt.buf ~= self.preview_bufnr
and self.preview_winid
and api.nvim_win_is_valid(self.preview_winid)
then
api.nvim_win_close(self.preview_winid, true)
if self.enter_leave_id then
pcall(api.nvim_del_autocmd, self.enter_leave_id)
end
self:remove_data()
if opt.buf ~= self.bufnr and self.winid and api.nvim_win_is_valid(self.winid) then
api.nvim_win_close(self.winid, true)
pcall(api.nvim_del_autocmd, opt.id)
self:clean()
end
end,
})
end
api.nvim_buf_set_keymap(self.preview_bufnr, 'n', config.hover.open_link, '', {
nowait = true,
noremap = true,
callback = function()
open_link()
end,
})
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
libs.scroll_in_preview(bufnr, self.preview_winid)
end
util.map_keys(self.bufnr, config.hover.open_link, function()
self:open_link()
end)
end
local function should_error(args)
-- Never error if we have ++quiet
if args and has_arg(args, '++quiet') then
return false
local function ignore_error(args, can_through)
if vim.tbl_contains(args, '++silent') and can_through then
return true
end
return true
end
local function support_clients()
local count = 0
local clients = lsp.get_active_clients({ bufnr = 0 })
for _, client in ipairs(clients) do
if client.supports_method('textDocument/hover') then
count = count + 1
break
end
end
return count, #clients
end
function hover:do_request(args)
local params = util.make_position_params()
local count, total = support_clients()
if count == 0 and should_error(args) then
local params = lsp.util.make_position_params()
local method = 'textDocument/hover'
local clients = util.get_client_by_method(method)
if #clients == 0 then
self.pending_request = false
vim.notify('[Lspsaga] all server of buffer not support hover request')
return
end
count = 0
local count = 0
local failed = 0
lsp.buf_request(0, 'textDocument/hover', params, function(_, result, ctx)
self.pending_request = false
lsp.buf_request(api.nvim_get_current_buf(), method, params, function(_, result, ctx)
count = count + 1
if count == #clients then
self.pending_request = false
end
if api.nvim_get_current_buf() ~= ctx.bufnr then
return
end
if not result or not result.contents then
failed = failed + 1
if count == total and failed == total and should_error(args) then
if ignore_error(args, count == #clients) then
vim.notify('No information available')
end
return
@ -278,13 +234,9 @@ function hover:do_request(args)
if type(result.contents) == 'string' then -- MarkedString
value = result.contents
elseif result.contents.language then -- MarkedString
if result.contents.language == 'css' then
value = '```css\n' .. result.contents.value .. '\n```'
else
value = result.contents.value
end
value = result.contents.value
elseif vim.tbl_islist(result.contents) then -- MarkedString[]
if vim.tbl_isempty(result.contents) and should_error(args) then
if vim.tbl_isempty(result.contents) and ignore_error(args) then
vim.notify('No information available')
return
end
@ -299,19 +251,39 @@ function hover:do_request(args)
end
if not value or #value == 0 then
if should_error(args) then
if ignore_error(args, count == #clients) then
vim.notify('No information available')
end
return
end
local content = vim.split(value, '\n', { trimempty = true })
local client = vim.lsp.get_client_by_id(ctx.client_id)
content[#content + 1] = '`From: ' .. client.name .. '`'
result.contents = {
kind = 'markdown',
value = value,
}
if
self.bufnr
and api.nvim_buf_is_valid(self.bufnr)
and self.winid
and api.nvim_win_is_valid(self.winid)
then
vim.bo[self.bufnr].modifiable = true
local win_conf = api.nvim_win_get_config(self.winid)
local max_len = util.get_max_content_length(content)
if max_len > win_conf.width then
win_conf.width = max_len
end
local truncate = util.gen_truncate_line(win_conf.width)
content = vim.list_extend({ truncate }, content)
api.nvim_buf_set_lines(self.bufnr, -1, -1, false, content)
vim.bo[self.bufnr].modifiable = false
win_conf.height = win_conf.height + #content + 1
api.nvim_win_set_config(self.winid, win_conf)
return
end
local option_fn
if args and has_arg(args, '++keep') then
if vim.tbl_contains(args, '++keep') then
option_fn = function(width)
local opt = {}
opt.relative = 'editor'
@ -321,22 +293,17 @@ function hover:do_request(args)
end
end
self:open_floating_preview(result.contents, option_fn)
end)
end
function hover:remove_data()
for k, v in pairs(self) do
if type(v) ~= 'function' then
self[k] = nil
if not self.winid then
self:open_floating_preview(content, option_fn)
return
end
end
end)
end
local function check_parser()
local parsers = { 'parser/markdown.so', 'parser/markdown_inline.so' }
local has_parser = true
for _, p in pairs(parsers) do
for _, p in ipairs(parsers) do
if #api.nvim_get_runtime_file(p, true) == 0 then
has_parser = false
break
@ -348,44 +315,31 @@ end
function hover:render_hover_doc(args)
if not check_parser() then
vim.notify(
'[Lspsaga.nvim] Please install markdown and markdown_inline parser in nvim-treesitter',
'[Lpsaga.nvim] Please install markdown and markdown_inline parser in nvim-treesitter',
vim.log.levels.WARN
)
return
end
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
if (args and not has_arg(args, '++keep')) or not args then
api.nvim_set_current_win(self.preview_winid)
return
elseif args and has_arg(args, '++keep') then
libs.delete_scroll_map(api.nvim_get_current_buf())
api.nvim_win_close(self.preview_winid, true)
self.preview_winid = nil
self.preview_bufnr = nil
return
end
end
if vim.bo.filetype == 'help' then
api.nvim_feedkeys('K', 'ni', true)
return
end
if self.pending_request then
print('[Lspsaga] There is already a hover request, please wait for the response.')
return
end
if self.winid and api.nvim_win_is_valid(self.winid) then
if not vim.tbl_contains(args, '++keep') then
api.nvim_set_current_win(self.winid)
return
else
util.delete_scroll_map(api.nvim_get_current_buf())
api.nvim_win_close(self.winid, true)
self:clean()
return
end
end
self.pending_request = true
self:do_request(args)
end
function hover:has_hover()
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
return true
end
return false
end
return hover

View file

@ -0,0 +1,205 @@
local api, fn = vim.api, vim.fn
---@diagnostic disable-next-line: deprecated
local uv = vim.version().minor >= 10 and vim.uv or vim.loop
local config = require('lspsaga').config.implement
local symbol = require('lspsaga.symbol')
local ui = require('lspsaga').config.ui
local ns = api.nvim_create_namespace('SagaImp')
local defined = false
local name = 'SagaImpIcon'
local buffers_cache = {}
if not defined then
fn.sign_define(name, { text = ui.imp_sign, texthl = name })
defined = true
end
local function render_sign(bufnr, row, data)
if not config.sign then
return
end
fn.sign_place(row + 1, name, name, bufnr, { lnum = row + 1, priority = config.priority })
data.sign_id = row + 1
end
local function try_render(client_id, bufnr, pos, data)
local params = {
position = {
character = pos.character,
line = pos.line,
},
textDocument = {
uri = vim.uri_from_bufnr(bufnr),
},
}
local client = vim.lsp.get_client_by_id(client_id)
if not client then
return
end
---@diagnostic disable-next-line: invisible
client.request('textDocument/implementation', params, function(err, result)
if err or api.nvim_get_current_buf() ~= bufnr then
return
end
if not result then
result = {}
end
if data.res_count then
if data.res_count == #result then
return
end
pcall(fn.sign_unplace, name, { buffer = bufnr, id = data.sign_id })
end
if config.sign and #result > 0 then
render_sign(bufnr, pos.line, data)
end
if not config.virtual_text then
return
end
local word = #result > 1 and 'implementations' or 'implementation'
local indent
if vim.bo[bufnr].expandtab then
local level = vim.fn.indent(pos.line + 1) / vim.bo[bufnr].sw
indent = (' '):rep(level * vim.bo[bufnr].sw)
else
local level = vim.fn.indent(pos.line + 1) / vim.bo[bufnr].tabstop
indent = ('\t'):rep(level)
end
if not data.virt_id then
data.virt_id = uv.hrtime()
end
api.nvim_buf_set_extmark(bufnr, ns, pos.line, 0, {
id = data.virt_id,
virt_lines = { { { indent .. #result .. ' ' .. word, 'Comment' } } },
virt_lines_above = true,
hl_mode = 'combine',
})
data.res_count = #result
end, bufnr)
end
local function langmap(bufnr)
local tbl = {
['rust'] = {
kinds = { 10, 11 },
children = true,
},
}
return tbl[vim.bo[bufnr].filetype] or { kinds = { 11 }, children = false }
end
local function range_compare(r1, r2)
for k, v in pairs(r1.start) do
if r2.start[k] ~= v then
return true
end
end
for k, v in pairs(r1['end']) do
if r2['end'][k] ~= v then
return true
end
end
end
local function is_rename(data, range, word)
for before, item in pairs(data) do
if
item.range.start.line == range.start.line
and item.range.start.character == range.start.character
and word ~= before
then
return before
end
end
end
local function clean(buf)
for k, data in pairs(buffers_cache[buf] or {}) do
pcall(api.nvim_buf_del_extmark, buf, ns, data.virt_id)
pcall(fn.sign_unplace, name, { buffer = buf, id = data.sign_id })
buffers_cache[buf][k] = nil
end
end
local function render(client_id, bufnr, symbols)
local langdata = langmap(bufnr)
local hit = buffers_cache[bufnr] and {} or nil
if not buffers_cache[bufnr] then
buffers_cache[bufnr] = {}
end
local function parse_symbol(nodes)
for _, item in ipairs(nodes) do
if vim.tbl_contains(langdata.kinds, item.kind) then
local srow = item.selectionRange.start.line
local scol = item.selectionRange.start.character
local erow = item.selectionRange['end'].line
local ecol = item.selectionRange['end'].character
local word = api.nvim_buf_get_text(bufnr, srow, scol, erow, ecol, {})[1]
if not buffers_cache[bufnr][word] then
buffers_cache[bufnr][word] = {
range = item.range,
}
else
if not range_compare(buffers_cache[bufnr][word].range, item.range) then
buffers_cache[bufnr][word].range = item.range
end
end
if hit then
hit[#hit + 1] = word
end
try_render(client_id, bufnr, item.selectionRange.start, buffers_cache[bufnr][word])
end
if item.children and langdata.children then
parse_symbol(item.children)
end
end
end
parse_symbol(symbols)
if hit and #hit > 0 then
local nonexist = vim.tbl_filter(function(word)
return not vim.tbl_contains(hit, word)
end, vim.tbl_keys(buffers_cache[bufnr]))
for _, word in ipairs(nonexist) do
local data = buffers_cache[bufnr][word]
pcall(api.nvim_buf_del_extmark, bufnr, ns, data.virt_id)
pcall(fn.sign_unplace, name, { buffer = bufnr, id = data.sign_id })
buffers_cache[bufnr][word] = nil
end
end
end
local function start(buf, client_id, symbols)
if symbols then
render(client_id, buf, symbols)
end
api.nvim_create_autocmd({ 'InsertLeave', 'TextChanged' }, {
buffer = buf,
callback = function(args)
local res = symbol:get_buf_symbols(args.buf)
if res and res.symbols and #res.symbols > 0 then
render(client_id, buf, res.symbols)
end
end,
desc = '[Lspsaga] Implement show',
})
end
return {
start = start,
}

View file

@ -5,42 +5,39 @@ saga.saga_augroup = api.nvim_create_augroup('Lspsaga', { clear = true })
local default_config = {
ui = {
border = 'single',
devicon = true,
title = true,
winblend = 0,
expand = '',
collapse = '',
expand = '',
collapse = '',
code_action = '💡',
incoming = '',
outgoing = '',
actionfix = '',
hover = '',
theme = 'arrow',
lines = { '', '', '', '' },
kind = {},
lines = { '', '', '', '', '' },
kind = nil,
imp_sign = '󰳛 ',
},
hover = {
max_width = 0.6,
max_width = 0.9,
max_height = 0.8,
open_link = 'gx',
open_browser = '!chrome',
open_cmd = '!chrome',
},
diagnostic = {
on_insert = false,
on_insert_follow = false,
insert_winblend = 0,
show_code_action = true,
show_source = true,
show_layout = 'float',
show_normal_height = 10,
jump_num_shortcut = true,
max_width = 0.7,
max_width = 0.8,
max_height = 0.6,
max_show_width = 0.9,
max_show_height = 0.6,
text_hl_follow = true,
border_follow = true,
extend_relatedInformation = false,
diagnostic_only_current = false,
keys = {
exec_action = 'o',
quit = 'q',
expand_or_jump = '<CR>',
toggle_or_jump = '<CR>',
quit_in_show = { 'q', '<ESC>' },
},
},
@ -55,15 +52,11 @@ local default_config = {
},
lightbulb = {
enable = true,
enable_in_insert = true,
sign = true,
debounce = 10,
sign_priority = 40,
virtual_text = true,
},
preview = {
lines_above = 0,
lines_below = 10,
},
scroll_preview = {
scroll_down = '<C-f>',
scroll_up = '<C-b>',
@ -71,78 +64,88 @@ local default_config = {
request_timeout = 2000,
finder = {
max_height = 0.5,
min_width = 30,
force_max_height = false,
left_width = 0.3,
methods = {},
default = 'ref+imp',
layout = 'float',
filter = {},
keys = {
jump_to = 'p',
expand_or_jump = 'o',
shuttle = '[w',
toggle_or_open = 'o',
vsplit = 's',
split = 'i',
tabe = 't',
tabnew = 'r',
quit = { 'q', '<ESC>' },
close_in_preview = '<ESC>',
quit = 'q',
close = '<C-c>k',
},
},
definition = {
width = 0.6,
height = 0.5,
edit = '<C-c>o',
vsplit = '<C-c>v',
split = '<C-c>i',
tabe = '<C-c>t',
quit = 'q',
keys = {
edit = '<C-c>o',
vsplit = '<C-c>v',
split = '<C-c>i',
tabe = '<C-c>t',
quit = 'q',
close = '<C-c>k',
},
},
rename = {
quit = '<C-c>',
exec = '<CR>',
mark = 'x',
confirm = '<CR>',
in_select = true,
auto_save = false,
project_max_width = 0.5,
project_max_height = 0.5,
keys = {
quit = '<Esc>',
exec = '<CR>',
select = 'x',
},
},
symbol_in_winbar = {
enable = true,
ignore_patterns = {},
separator = '',
hide_keyword = true,
separator = ' ',
hide_keyword = false,
show_file = true,
folder_level = 2,
respect_root = false,
folder_level = 1,
color_mode = true,
dely = 300,
},
outline = {
win_position = 'right',
win_with = '',
win_width = 30,
auto_preview = true,
auto_refresh = true,
detail = true,
auto_close = true,
auto_resize = false,
custom_sort = nil,
preview_width = 0.4,
close_after_jump = false,
close_after_jump = true,
keys = {
expand_or_jump = 'o',
toggle_or_jump = 'o',
quit = 'q',
},
},
callhierarchy = {
show_detail = false,
layout = 'float',
keys = {
edit = 'e',
vsplit = 's',
split = 'i',
tabe = 't',
jump = 'o',
quit = 'q',
expand_collapse = 'u',
close = '<C-c>k',
shuttle = '[w',
toggle_or_req = 'u',
},
},
implement = {
enable = true,
sign = true,
virtual_text = true,
priority = 100,
},
beacon = {
enable = true,
frequency = 7,
},
server_filetype_map = {},
}
function saga.setup(opts)
@ -150,17 +153,16 @@ function saga.setup(opts)
saga.config = vim.tbl_deep_extend('force', default_config, opts)
require('lspsaga.highlight'):init_highlight()
require('lspsaga.lspkind').init_kind_hl()
if saga.config.lightbulb.enable then
require('lspsaga.lightbulb').lb_autocmd()
require('lspsaga.codeaction.lightbulb').lb_autocmd()
end
if saga.config.symbol_in_winbar.enable then
require('lspsaga.symbolwinbar'):symbol_autocmd()
require('lspsaga.symbol'):register_module()
end
if saga.config.diagnostic.on_insert then
require('lspsaga.diagnostic'):on_insert()
if saga.config.diagnostic.diagnostic_only_current then
require('lspsaga.diagnostic.virt').diag_on_current()
end
end

View file

@ -0,0 +1,68 @@
local api, fn = vim.api, vim.fn
local win = require('lspsaga.window')
local ui = require('lspsaga').config.ui
local M = {}
function M.left(height, width, bufnr)
local curwin = api.nvim_get_current_win()
local pos = api.nvim_win_get_cursor(curwin)
local float_opt = {
width = width,
height = height,
bufnr = bufnr,
offset_x = -pos[2],
focusable = true,
}
local topline = fn.line('w0')
local room = fn.line('w$') - pos[1]
if room <= height + 4 then
fn.winrestview({ topline = topline + (height + 4 - room) })
end
return win
:new_float(float_opt, true)
:bufopt({
['buftype'] = 'nofile',
['bufhidden'] = 'wipe',
})
:winopt({
['winhl'] = 'NormalFloat:SagaNormal,Border:SagaBorder',
})
:wininfo()
end
local function border_map()
return {
['single'] = { '', '' },
['rounded'] = { '', '' },
['double'] = { '', '' },
['solid'] = { '', '' },
['shadow'] = { '', '' },
}
end
function M.right(left_winid)
local win_conf = api.nvim_win_get_config(left_winid)
local original = vim.deepcopy(win_conf)
local map = border_map()
original.border[5] = map[ui.border][1]
original.border[3] = map[ui.border][2]
api.nvim_win_set_config(left_winid, original)
local WIDTH = api.nvim_win_get_width(win_conf.win)
local col = win_conf.col[false] + win_conf.width
local row = win_conf.row[false]
win_conf.width = WIDTH - win_conf.width - 15
win_conf.border[8] = ''
win_conf.border[7] = ''
win_conf.row = row
win_conf.col = col + 2
return win
:new_float(win_conf, false, true)
:winopt({
['winhl'] = 'NormalFloat:SagaNormal,Border:SagaBorder',
['signcolumn'] = 'no',
})
:wininfo()
end
return M

View file

@ -0,0 +1,93 @@
local api = vim.api
local float = require('lspsaga.layout.float')
local normal = require('lspsaga.layout.normal')
local M = {}
function M:arg_layout(args)
local layout
for _, item in ipairs(args) do
if item:find('normal') then
layout = 'normal'
elseif item:find('float') then
layout = 'float'
end
end
return layout
end
function M:new(layout)
self.layout = layout
return self
end
local LEFT = 1
local RIGHT = 2
function M:left(height, width, bufnr)
local fn = self.layout == 'float' and float.left or normal.left
self.left_bufnr, self.left_winid = fn(height, width, bufnr)
self.current = LEFT
return self
end
function M:bufopt(name, value)
local bufnr = self.current == LEFT and self.left_bufnr or self.right_bufnr
if type(name) == 'table' then
for key, val in pairs(name) do
api.nvim_set_option_value(key, val, { buf = bufnr })
end
else
api.nvim_set_option_value(name, value, { buf = bufnr })
end
return self
end
function M:winopt(name, value)
local winid = self.current == LEFT and self.left_winid or self.right_winid
if type(name) == 'table' then
for key, val in pairs(name) do
api.nvim_set_option_value(key, val, { win = winid, scope = 'local' })
end
else
api.nvim_set_option_value(name, value, { win = winid, scope = 'local' })
end
return self
end
function M:setlines(lines)
vim.validate({
lines = { lines, 't' },
})
local bufnr = self.current == LEFT and self.left_bufnr or self.right_bufnr
api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
return self
end
function M:right()
local fn = self.layout == 'float' and float.right or normal.right
self.right_bufnr, self.right_winid = fn(self.left_winid)
self.current = RIGHT
return self
end
function M:done(fn)
vim.validate({
fn = { fn, { 'f' }, true },
})
if fn then
fn(self.left_bufnr, self.left_winid, self.right_bufnr, self.right_winid)
end
return self.left_bufnr, self.left_winid, self.right_bufnr, self.right_winid
end
function M:close()
for _, id in ipairs({ self.left_winid, self.right_winid }) do
if api.nvim_win_is_valid(id) then
api.nvim_win_close(id, true)
end
end
self.left_winid = nil
self.right_winid = nil
end
return M

View file

@ -0,0 +1,31 @@
local api = vim.api
local win = require('lspsaga.window')
local M = {}
function M.left(height, width, bufnr)
M.width = width
return win
:new_normal('sp', bufnr)
:bufopt({
['buftype'] = 'nofile',
})
:winopt({
['number'] = false,
['relativenumber'] = false,
['stc'] = '',
['cursorline'] = false,
['winfixwidth'] = true,
})
:setheight(height)
:wininfo()
end
function M.right(left_winid)
vim.cmd.vsplit('new')
api.nvim_win_set_width(left_winid, M.width)
local rbuf, rwinid = api.nvim_get_current_buf(), api.nvim_get_current_win()
api.nvim_set_current_win(left_winid)
return rbuf, rwinid
end
return M

View file

@ -1,348 +0,0 @@
local api, lsp = vim.api, vim.lsp
local saga_conf = require('lspsaga').config
local libs = {}
local saga_augroup = require('lspsaga').saga_augroup
libs.iswin = vim.loop.os_uname().sysname == 'Windows_NT'
libs.ismac = vim.loop.os_uname().sysname == 'Darwin'
local shellslash = vim.fn.exists('+shellslash') == 1 and vim.opt.shellslash:get() or nil
libs.path_sep = libs.iswin and not shellslash and '\\' or '/'
function libs.get_path_info(buf, level)
level = level or 1
local fname = api.nvim_buf_get_name(buf)
local tbl = vim.split(fname, libs.path_sep, { trimempty = true })
if level == 1 then
return { tbl[#tbl] }
end
local index = level > #tbl and #tbl or level
return { unpack(tbl, #tbl - index + 1, #tbl) }
end
--get icon hlgroup color
function libs.icon_from_devicon(ft, color)
color = color ~= nil and color or false
if not libs.devicons then
local ok, devicons = pcall(require, 'nvim-web-devicons')
if not ok then
return { '' }
end
libs.devicons = devicons
end
local icon, hl = libs.devicons.get_icon_by_filetype(ft)
if color then
local _, rgb = libs.devicons.get_icon_color_by_filetype(ft)
return { icon and icon .. ' ' or '', rgb }
end
return { icon and icon .. ' ' or '', hl }
end
function libs.get_home_dir()
if libs.is_win then
return os.getenv('USERPROFILE')
end
return os.getenv('HOME')
end
function libs.tbl_index(tbl, val)
for index, v in pairs(tbl) do
if v == val then
return index
end
end
end
function libs.has_value(filetypes, val)
if type(filetypes) == 'table' then
for _, v in pairs(filetypes) do
if v == val then
return true
end
end
elseif type(filetypes) == 'string' then
if filetypes == val then
return true
end
end
return false
end
function libs.check_lsp_active(silent)
silent = silent or true
local current_buf = api.nvim_get_current_buf()
local active_clients = lsp.get_active_clients({ bufnr = current_buf })
if next(active_clients) == nil then
if not silent then
vim.notify('[LspSaga] Current buffer does not have any lsp server')
end
return false
end
return true
end
function libs.merge_table(t1, t2)
for _, v in pairs(t2) do
table.insert(t1, v)
end
end
function libs.get_lsp_root_dir()
if not libs.check_lsp_active() then
return
end
local cur_buf = api.nvim_get_current_buf()
local clients = lsp.get_active_clients({ bufnr = cur_buf })
for _, client in pairs(clients) do
if client.config.filetypes and client.config.root_dir then
if libs.has_value(client.config.filetypes, vim.bo[cur_buf].filetype) then
return client.config.root_dir
end
else
for name, fts in pairs(saga_conf.server_filetype_map) do
for _, ft in pairs(fts) do
if ft == vim.bo.filetype and client.config.name == name and client.config.root_dir then
return client.config.root_dir
end
end
end
end
end
return nil
end
function libs.get_config_lsp_filetypes()
local ok, lsp_config = pcall(require, 'lspconfig.configs')
if not ok then
return
end
local filetypes = {}
for _, config in pairs(lsp_config) do
if config.filetypes then
for _, ft in pairs(config.filetypes) do
table.insert(filetypes, ft)
end
end
end
if next(saga_conf.server_filetype_map) == nil then
return filetypes
end
for _, fts in pairs(saga_conf.server_filetype_map) do
if type(fts) == 'table' then
for _, ft in pairs(fts) do
table.insert(filetypes, ft)
end
elseif type(fts) == 'string' then
table.insert(filetypes, fts)
end
end
return filetypes
end
function libs.close_preview_autocmd(bufnr, winids, events, callback)
api.nvim_create_autocmd(events, {
group = saga_augroup,
buffer = bufnr,
once = true,
callback = function()
local window = require('lspsaga.window')
window.nvim_close_valid_window(winids)
if callback then
callback()
end
end,
})
end
function libs.find_buffer_by_filetype(ft)
local all_bufs = vim.fn.getbufinfo()
local filetype = ''
for _, bufinfo in pairs(all_bufs) do
filetype = api.nvim_buf_get_option(bufinfo['bufnr'], 'filetype')
if type(ft) == 'table' and libs.has_value(ft, filetype) then
return true, bufinfo['bufnr']
end
if filetype == ft then
return true, bufinfo['bufnr']
end
end
return false, nil
end
function libs.removeElementByKey(tbl, key)
local tmp = {}
for i in pairs(tbl) do
table.insert(tmp, i)
end
local newTbl = {}
local i = 1
while i <= #tmp do
local val = tmp[i]
if val == key then
table.remove(tmp, i)
else
newTbl[val] = tbl[val]
i = i + 1
end
end
return newTbl
end
function libs.generate_empty_table(length)
local empty_tbl = {}
if length == 0 then
return empty_tbl
end
for _ = 1, length do
table.insert(empty_tbl, ' ')
end
return empty_tbl
end
function libs.add_client_filetypes(client, fts)
if not client.config.filetypes then
client.config.filetypes = fts
end
end
-- get client by capabilities
function libs.get_client_by_cap(caps)
local client_caps = {
['string'] = function(instance)
libs.add_client_filetypes(instance, { vim.bo.filetype })
if
instance.server_capabilities[caps]
and libs.has_value(instance.config.filetypes, vim.bo.filetype)
then
return instance
end
return nil
end,
['table'] = function(instance)
libs.add_client_filetypes(instance, { vim.bo.filetype })
if
vim.tbl_get(instance.server_capabilities, unpack(caps))
and libs.has_value(instance.config.filetypes, vim.bo.filetype)
then
return instance
end
return nil
end,
}
local clients = lsp.get_active_clients({ bufnr = 0 })
local client
for _, instance in pairs(clients) do
client = client_caps[type(caps)](instance)
if client ~= nil then
break
end
end
return client
end
local function feedkeys(key)
local k = api.nvim_replace_termcodes(key, true, false, true)
api.nvim_feedkeys(k, 'x', false)
end
function libs.scroll_in_preview(bufnr, preview_winid)
local config = require('lspsaga').config
if preview_winid and api.nvim_win_is_valid(preview_winid) then
for i, map in ipairs({ config.scroll_preview.scroll_down, config.scroll_preview.scroll_up }) do
api.nvim_buf_set_keymap(bufnr, 'n', map, '', {
noremap = true,
nowait = true,
callback = function()
if api.nvim_win_is_valid(preview_winid) then
api.nvim_win_call(preview_winid, function()
local key = i == 1 and '<C-d>' or '<C-u>'
feedkeys(key)
end)
return
end
libs.delete_scroll_map(bufnr)
end,
})
end
end
end
function libs.delete_scroll_map(bufnr)
local config = require('lspsaga').config
pcall(api.nvim_buf_del_keymap, bufnr, 'n', config.scroll_preview.scroll_down)
pcall(api.nvim_buf_del_keymap, bufnr, 'n', config.scroll_preview.scroll_up)
end
function libs.jump_beacon(bufpos, width)
if not saga_conf.beacon.enable then
return
end
if width == 0 or not width then
return
end
local opts = {
relative = 'win',
bufpos = bufpos,
height = 1,
width = width,
row = 0,
col = 0,
anchor = 'NW',
focusable = false,
no_size_override = true,
noautocmd = true,
}
local window = require('lspsaga.window')
local _, winid = window.create_win_with_border({
contents = { '' },
noborder = true,
winblend = 0,
highlight = {
normal = 'SagaBeacon',
},
}, opts)
local timer = vim.loop.new_timer()
timer:start(
0,
60,
vim.schedule_wrap(function()
if not api.nvim_win_is_valid(winid) then
return
end
local blend = vim.wo[winid].winblend + saga_conf.beacon.frequency
if blend > 100 then
blend = 100
end
vim.wo[winid].winblend = blend
if vim.wo[winid].winblend == 100 and not timer:is_closing() then
timer:stop()
timer:close()
api.nvim_win_close(winid, true)
end
end)
)
end
function libs.gen_truncate_line(width)
local char = ''
return char:rep(math.floor(width / api.nvim_strwidth(char)))
end
return libs

View file

@ -1,190 +0,0 @@
local api, lsp, fn = vim.api, vim.lsp, vim.fn
local config = require('lspsaga').config
local lb = {}
local function get_hl_group()
return 'SagaLightBulb'
end
function lb:init_sign()
self.name = get_hl_group()
if not self.defined_sign then
fn.sign_define(self.name, { text = config.ui.code_action, texthl = self.name })
self.defined_sign = true
end
end
local function check_server_support_codeaction(bufnr)
local libs = require('lspsaga.libs')
local clients = lsp.get_active_clients({ bufnr = bufnr })
for _, client in pairs(clients) do
if not client.config.filetypes and next(config.server_filetype_map) ~= nil then
for _, fts in pairs(config.server_filetype_map) do
if libs.has_value(fts, vim.bo[bufnr].filetype) then
client.config.filetypes = fts
break
end
end
end
if
client.supports_method('textDocument/codeAction')
and libs.has_value(client.config.filetypes, vim.bo[bufnr].filetype)
then
return true
end
end
return false
end
local function _update_virtual_text(bufnr, line)
local namespace = api.nvim_create_namespace('sagalightbulb')
api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
if line then
local icon_with_indent = ' ' .. config.ui.code_action
pcall(api.nvim_buf_set_extmark, bufnr, namespace, line, -1, {
virt_text = { { icon_with_indent, 'SagaLightBulb' } },
virt_text_pos = 'overlay',
hl_mode = 'combine',
})
end
end
local function generate_sign(bufnr, line)
vim.fn.sign_place(
line,
lb.name,
lb.name,
bufnr,
{ lnum = line + 1, priority = config.lightbulb.sign_priority }
)
end
local function _update_sign(bufnr, line)
if vim.w.lightbulb_line == 0 then
vim.w.lightbulb_line = 1
end
if vim.w.lightbulb_line ~= 0 then
fn.sign_unplace(lb.name, { id = vim.w.lightbulb_line, buffer = bufnr })
end
if line then
generate_sign(bufnr, line)
vim.w.lightbulb_line = line
end
end
local function render_action_virtual_text(bufnr, line, has_actions)
if not api.nvim_buf_is_valid(bufnr) then
return
end
if not has_actions then
if config.lightbulb.virtual_text then
_update_virtual_text(bufnr, nil)
end
if config.lightbulb.sign then
_update_sign(bufnr, nil)
end
return
end
if config.lightbulb.sign then
_update_sign(bufnr, line)
end
if config.lightbulb.virtual_text then
_update_virtual_text(bufnr, line)
end
end
local send_request = coroutine.create(function()
local current_buf = api.nvim_get_current_buf()
vim.w.lightbulb_line = vim.w.lightbulb_line or 0
while true do
local diagnostics = lsp.diagnostic.get_line_diagnostics(current_buf)
local context = { diagnostics = diagnostics }
local params = lsp.util.make_range_params()
params.context = context
local line = params.range.start.line
lsp.buf_request_all(current_buf, 'textDocument/codeAction', params, function(results)
local has_actions = false
for _, res in pairs(results or {}) do
if res.result and type(res.result) == 'table' and next(res.result) ~= nil then
has_actions = true
break
end
end
-- if
-- has_actions
-- and config.code_action_lightbulb.enable
-- and config.code_action_lightbulb.cache_code_action
-- then
-- codeaction.action_tuples = nil
-- codeaction:get_clients(results)
-- end
render_action_virtual_text(current_buf, line, has_actions)
end)
current_buf = coroutine.yield()
end
end)
local render_bulb = function(bufnr)
local has_code_action = check_server_support_codeaction(bufnr)
if not has_code_action then
return
end
coroutine.resume(send_request, bufnr)
end
function lb.lb_autocmd()
lb:init_sign()
api.nvim_create_autocmd('LspAttach', {
group = api.nvim_create_augroup('LspSagaLightBulb', { clear = true }),
callback = function(opt)
local buf = opt.buf
local group = api.nvim_create_augroup(lb.name .. tostring(buf), {})
api.nvim_create_autocmd('CursorHold', {
group = group,
buffer = buf,
callback = function()
render_bulb(buf)
end,
})
if not config.lightbulb.enable_in_insert then
api.nvim_create_autocmd('InsertEnter', {
group = group,
buffer = buf,
callback = function()
_update_sign(buf, nil)
_update_virtual_text(buf, nil)
end,
})
end
api.nvim_create_autocmd('BufLeave', {
group = group,
buffer = buf,
callback = function()
_update_sign(buf, nil)
_update_virtual_text(buf, nil)
end,
})
api.nvim_create_autocmd('BufDelete', {
buffer = buf,
once = true,
callback = function()
pcall(api.nvim_del_augroup_by_id, group)
end,
})
end,
})
end
return lb

47
lua/lspsaga/logger.lua Normal file
View file

@ -0,0 +1,47 @@
local fn, uv = vim.fn, vim.version().minor >= 10 and vim.uv or vim.loop
local util = require('lspsaga.util')
local log = {}
local function log_path()
local data_path = fn.stdpath('data')
return util.path_join(data_path, 'lspsaga.log')
end
local function header()
local time = os.date('%m-%d-%H:%M:%S')
return '[Lspsaga] [' .. time .. ']'
end
local function tbl_to_string(tbl)
return vim.json.encode(tbl)
end
function log:new(method, params, result)
self.logfile = log_path()
self.content = header()
.. ' ['
.. method
.. '] [param] '
.. tbl_to_string(params)
.. ' [result] '
.. tbl_to_string(result)
return self
end
function log:write()
local fd = uv.fs_open(self.logfile, 'w', 438)
uv.fs_write(fd, self.content, function(err, bytes)
if err then
error('[Lspsaga] write to log failed')
end
if bytes == 0 then
print('[Lspsaga] write to log file failed bytes: ' .. bytes)
end
end)
end
function log:open()
vim.cmd.edit(log_path())
end
return log

View file

@ -1,5 +1,4 @@
local ui = require('lspsaga').config.ui
local api = vim.api
local function merge_custom(kind)
local function find_index_by_type(k)
@ -8,10 +7,9 @@ local function merge_custom(kind)
return index
end
end
return nil
end
for k, v in pairs(ui.kind) do
for k, v in ipairs(ui.kind or {}) do
local index = find_index_by_type(k)
if not index then
vim.notify('[lspsaga.nvim] could not find kind in default')
@ -23,38 +21,39 @@ local function merge_custom(kind)
kind[index][3] = v
else
vim.notify('[Lspsaga.nvim] value must be string or table')
return
end
end
end
local function get_kind()
local kind = {
[1] = { 'File', '', 'Tag' },
[2] = { 'Module', '', 'Exception' },
[3] = { 'Namespace', '', 'Include' },
[4] = { 'Package', '', 'Label' },
[5] = { 'Class', '', 'Include' },
[6] = { 'Method', '', 'Function' },
[7] = { 'Property', '', '@property' },
[8] = { 'Field', '', '@field' },
[9] = { 'Constructor', '', '@constructor' },
[10] = { 'Enum', '', '@number' },
[11] = { 'Interface', '', 'Type' },
[12] = { 'Function', '󰡱 ', 'Function' },
[13] = { 'Variable', '', '@variable' },
[14] = { 'Constant', '', 'Constant' },
[15] = { 'String', '󰅳 ', 'String' },
[16] = { 'Number', '󰎠 ', 'Number' },
[17] = { 'Boolean', '', 'Boolean' },
[18] = { 'Array', '󰅨 ', 'Type' },
[19] = { 'Object', '', 'Type' },
[20] = { 'Key', '', 'Constant' },
[21] = { 'Null', '󰟢 ', 'Constant' },
[22] = { 'EnumMember', '', 'Number' },
[23] = { 'Struct', '', 'Type' },
[24] = { 'Event', '', 'Constant' },
[25] = { 'Operator', '', 'Operator' },
[26] = { 'TypeParameter', '', 'Type' },
{ 'File', '', 'Tag' },
{ 'Module', '', 'Exception' },
{ 'Namespace', '', 'Include' },
{ 'Package', '', 'Label' },
{ 'Class', '', 'Include' },
{ 'Method', '', 'Function' },
{ 'Property', '', '@property' },
{ 'Field', '', '@field' },
{ 'Constructor', '', '@constructor' },
{ 'Enum', '', '@number' },
{ 'Interface', '', 'Type' },
{ 'Function', '󰡱 ', 'Function' },
{ 'Variable', '', '@variable' },
{ 'Constant', '', 'Constant' },
{ 'String', '󰅳 ', 'String' },
{ 'Number', '󰎠 ', 'Number' },
{ 'Boolean', '', 'Boolean' },
{ 'Array', '󰅨 ', 'Type' },
{ 'Object', '', 'Type' },
{ 'Key', '', 'Constant' },
{ 'Null', '󰟢 ', 'Constant' },
{ 'EnumMember', '', 'Number' },
{ 'Struct', '', 'Type' },
{ 'Event', '', 'Constant' },
{ 'Operator', '', 'Operator' },
{ 'TypeParameter', '', 'Type' },
-- ccls
[252] = { 'TypeAlias', '', 'Type' },
[253] = { 'Parameter', '', '@parameter' },
@ -72,51 +71,8 @@ local function get_kind()
return kind
end
local function other_groups()
local prefix = 'SagaWinbar'
return { prefix .. 'Filename', prefix .. 'FolderName' }
end
local function get_kind_group()
local prefix = 'SagaWinbar'
local res = {}
---@diagnostic disable-next-line: param-type-mismatch
for _, item in pairs(get_kind()) do
res[#res + 1] = prefix .. item[1]
end
res = vim.list_extend(res, other_groups())
res[#res + 1] = 'SagaWinbarFileIcon'
res[#res + 1] = 'SagaWinbarSep'
return res
end
local function find_kind_group(name)
---@diagnostic disable-next-line: param-type-mismatch
for _, v in pairs(get_kind()) do
if name:find(v[1]) then
return v[3]
end
end
end
local function init_kind_hl()
local others = other_groups()
local tbl = get_kind_group()
---@diagnostic disable-next-line: param-type-mismatch
for i, v in pairs(tbl) do
if vim.tbl_contains(others, v) then
api.nvim_set_hl(0, v, { fg = '#bdbfb8', default = true })
elseif i == #tbl then
api.nvim_set_hl(0, v, { link = 'Operator', default = true })
else
local group = find_kind_group(v)
api.nvim_set_hl(0, v, { link = group, default = true })
end
end
end
local kind = get_kind()
return {
init_kind_hl = init_kind_hl,
get_kind = get_kind,
get_kind_group = get_kind_group,
kind = kind,
}

View file

@ -1,609 +0,0 @@
local ot = {}
local api, lsp, fn = vim.api, vim.lsp, vim.fn
local config = require('lspsaga').config
local libs = require('lspsaga.libs')
local symbar = require('lspsaga.symbolwinbar')
local window = require('lspsaga.window')
local util = require('lspsaga.util')
local outline_conf = config.outline
local ctx = {}
function ot.__newindex(t, k, v)
rawset(t, k, v)
end
ot.__index = ot
local function clean_ctx()
if ctx.group then
api.nvim_del_augroup_by_id(ctx.group)
end
for k, _ in pairs(ctx) do
ctx[k] = nil
end
end
local function get_cache_symbols(buf)
if not symbar[buf] then
return
end
local data = symbar[buf]
if not data or data.pending_request then
return
end
if not data.pending_request and data.symbols then
return data.symbols
end
return nil
end
---@private
local function set_local()
local local_options = {
bufhidden = 'wipe',
number = false,
relativenumber = false,
filetype = 'lspsagaoutline',
buftype = 'nofile',
wrap = false,
signcolumn = 'no',
matchpairs = '',
buflisted = false,
list = false,
spell = false,
cursorcolumn = false,
cursorline = false,
winfixwidth = true,
winhl = 'Normal:OutlineNormal',
}
for opt, val in pairs(local_options) do
vim.opt_local[opt] = val
end
---@diagnostic disable-next-line: undefined-field
if fn.has('nvim-0.9') == 1 and #vim.opt_local.stc:get() > 0 then
vim.opt_local.stc = ''
end
end
local function get_hi_prefix()
return 'SagaWinbar'
end
local function get_kind()
return require('lspsaga.lspkind').get_kind()
end
local function find_node(data, line)
for _, node in pairs(data or {}) do
if node.winline == line then
return node
end
end
end
local function parse_symbols(buf, symbols)
local res = {}
local tmp_node = function(node)
local tmp = {}
tmp.winline = -1
for k, v in pairs(node) do
if k ~= 'children' then
tmp[k] = v
end
end
return tmp
end
local function recursive_parse(tbl)
for _, v in ipairs(tbl) do
if not res[v.kind] then
res[v.kind] = {
expand = true,
data = {},
}
end
if not symbar.node_is_keyword(buf, v) then
local tmp = tmp_node(v)
table.insert(res[v.kind].data, tmp)
end
if v.children then
recursive_parse(v.children)
end
end
end
recursive_parse(symbols)
local keys = vim.tbl_keys(res)
table.sort(keys, outline_conf.custom_sort)
local new = {}
for _, v in ipairs(keys) do
new[v] = res[v]
end
-- remove unnecessary data reduce memory usage
for k, v in pairs(new) do
if #v.data == 0 then
new[k] = nil
else
for _, item in ipairs(v.data) do
if item.selectionRange then
item.pos = { item.selectionRange.start.line, item.selectionRange.start.character }
item.selectionRange = nil
end
end
end
end
return new
end
---@private
local function create_outline_window()
local curwin = api.nvim_get_current_win()
vim.wo[curwin].winhl = 'WinSeparator:OutlineWinSeparator'
if #outline_conf.win_with > 0 then
local ok, sp_buf = libs.find_buffer_by_filetype(outline_conf.win_with)
if ok then
local winid = fn.win_findbuf(sp_buf)[1]
api.nvim_set_current_win(winid)
vim.cmd('sp vnew')
return
end
end
local pos = outline_conf.win_position == 'right' and 'botright' or 'topleft'
vim.cmd(pos .. ' vnew')
local winid, bufnr = api.nvim_get_current_win(), api.nvim_get_current_buf()
api.nvim_win_set_width(winid, outline_conf.win_width)
set_local()
return winid, bufnr
end
function ot:apply_map()
local maps = outline_conf.keys
local opts = { nowait = true }
util.map_keys(self.bufnr, 'n', maps.quit, function()
if self.bufnr and api.nvim_buf_is_loaded(self.bufnr) then
api.nvim_buf_delete(self.bufnr, { force = true })
end
if self.winid and api.nvim_win_is_valid(self.winid) then
api.nvim_win_close(self.winid, true)
end
clean_ctx()
end, opts)
local function open()
local curline = api.nvim_win_get_cursor(0)[1]
local node
for _, nodes in pairs(self.data) do
node = find_node(nodes.data, curline)
if node then
break
end
end
if not node then
return
end
local range = node.range and node.range or node.location.range
local winid = fn.bufwinid(self.render_buf)
api.nvim_set_current_win(winid)
if node.pos then
api.nvim_win_set_cursor(winid, { node.pos[1] + 1, node.pos[2] })
else
api.nvim_win_set_cursor(winid, { range.start.line + 1, range.start.character })
end
local width = #api.nvim_get_current_line()
libs.jump_beacon({ range.start.line, range.start.character }, width)
if outline_conf.close_after_jump then
self:close_and_clean()
end
end
util.map_keys(self.bufnr, 'n', maps.expand_or_jump, function()
local text = api.nvim_get_current_line()
if text:find(config.ui.expand) or text:find(config.ui.collapse) then
self:expand_collapse()
return
end
open()
end, opts)
end
function ot:request_and_render(buf)
local params = { textDocument = lsp.util.make_text_document_params(buf) }
local client = libs.get_client_by_cap('documentSymbolProvider')
if not client then
return
end
client.request('textDocument/documentSymbol', params, function(_, result)
self.pending_request = false
if not result or next(result) == nil then
return
end
self:render_outline(buf, result)
if not self.registerd then
self:register_events()
end
end, buf)
end
function ot:expand_collapse()
local curline = api.nvim_win_get_cursor(0)[1]
local node = find_node(self.data, curline)
if not node then
return
end
local prefix = get_hi_prefix()
local kind = get_kind()
local function increase_or_reduce(lnum, num)
for k, v in pairs(self.data) do
if v.winline > lnum then
self.data[k].winline = self.data[k].winline + num
for _, item in pairs(v.data) do
item.winline = item.winline + num
end
end
end
end
if node.expand then
local text = api.nvim_get_current_line()
text = text:gsub(config.ui.collapse, config.ui.expand)
for _, v in ipairs(node.data) do
v.winline = -1
end
vim.bo[self.bufnr].modifiable = true
api.nvim_buf_set_lines(self.bufnr, curline - 1, curline + #node.data, false, { text })
vim.bo[self.bufnr].modifiable = false
node.expand = false
api.nvim_buf_add_highlight(self.bufnr, 0, 'SagaCollapse', curline - 1, 0, 5)
api.nvim_buf_add_highlight(
self.bufnr,
0,
prefix .. kind[node.data[1].kind][1],
curline - 1,
5,
-1
)
increase_or_reduce(node.winline + #node.data, -#node.data)
return
end
local lines = {}
local text = api.nvim_get_current_line()
text = text:gsub(config.ui.expand, config.ui.collapse)
lines[#lines + 1] = text
for i, v in pairs(node.data) do
lines[#lines + 1] = v.name
v.winline = curline + i
end
vim.bo[self.bufnr].modifiable = true
api.nvim_buf_set_lines(self.bufnr, curline - 1, curline, false, lines)
vim.bo[self.bufnr].modifiable = false
node.expand = true
api.nvim_buf_add_highlight(self.bufnr, 0, 'SagaExpand', curline - 1, 0, 5)
api.nvim_buf_add_highlight(
self.bufnr,
0,
prefix .. kind[node.data[1].kind][1],
curline - 1,
5,
-1
)
for _, v in ipairs(node.data) do
for group, scope in pairs(v.hi_scope) do
api.nvim_buf_add_highlight(self.bufnr, 0, group, v.winline - 1, scope[1], scope[2])
end
end
increase_or_reduce(node.winline, #node.data)
end
function ot:auto_refresh()
api.nvim_create_autocmd('BufEnter', {
group = self.group,
callback = function(opt)
local clients = lsp.get_active_clients({ bufnr = opt.buf })
if next(clients) == nil or opt.buf == self.render_buf then
return
end
vim.bo[self.bufnr].modifiable = true
api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {})
self:outline(opt.buf, true)
end,
desc = '[Lspsaga.nvim] outline auto refresh',
})
end
function ot:auto_preview()
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
api.nvim_win_close(self.preview_winid, true)
self.preview_winid = nil
self.preview_bufnr = nil
end
local curline = api.nvim_win_get_cursor(0)[1]
local node
for _, nodes in pairs(self.data) do
node = find_node(nodes.data, curline)
if node then
break
end
end
if not node then
return
end
local range = node.location and node.location.range or node.range
if not range then
return
end
if range.start.line == range['end'].line then
range['end'].line = range['end'].line + 1
end
local content =
api.nvim_buf_get_lines(self.render_buf, range.start.line, range['end'].line, false)
local WIN_WIDTH = vim.o.columns
local max_width = math.floor(WIN_WIDTH * outline_conf.preview_width)
local opts = {
relative = 'editor',
style = 'minimal',
height = #content > 0 and #content or 1,
width = max_width,
no_size_override = true,
}
local winid = fn.bufwinid(self.render_buf)
local _height = fn.winheight(winid)
local win_height
if outline_conf.win_position == 'right' then
opts.anchor = 'NE'
opts.col = WIN_WIDTH - outline_conf.win_width - 1
opts.row = fn.winline() + 2
win_height = fn.winheight(0)
if win_height < _height then
opts.row = (_height - win_height) + fn.winline()
else
opts.row = fn.winline()
end
else
opts.anchor = 'NW'
opts.col = outline_conf.win_width + 1
win_height = fn.winheight(0)
if win_height < _height then
opts.row = (_height - win_height) + vim.fn.winline()
else
opts.row = fn.winline()
end
end
local content_opts = {
contents = content,
buftype = 'nofile',
bufhidden = 'wipe',
highlight = {
normal = 'ActionPreviewNormal',
border = 'ActionPreviewBorder',
},
}
self.preview_bufnr, self.preview_winid = window.create_win_with_border(content_opts, opts)
if fn.has('nvim-0.9') == 1 then
local lang = require('nvim-treesitter.parsers').ft_to_lang(vim.bo[self.render_buf].filetype)
vim.treesitter.start(self.preview_bufnr, lang)
else
-- this is will trigger filetype event
-- when 0.9 release use vim.treesitter.start would be better
vim.bo[self.preview_bufnr].filetype = vim.bo[self.render_buf].filetype
api.nvim_win_set_var(self.preview_winid, 'disable_winbar', true)
end
local events = { 'CursorMoved', 'BufLeave' }
vim.defer_fn(function()
libs.close_preview_autocmd(self.bufnr, self.preview_winid, events)
end, 0)
end
function ot:close_when_last()
api.nvim_create_autocmd('BufEnter', {
group = self.group,
callback = function()
local wins = api.nvim_list_wins()
if #wins > 2 then
return
end
local bufs = api.nvim_list_bufs()
bufs = vim.tbl_filter(function(b)
return fn.buflisted(b) == 0 and #fn.win_findbuf(b) > 0
end, bufs)
if #bufs == 1 and bufs[1] == self.bufnr and #wins > 1 then
return
end
local both_nofile = {}
for _, buf in ipairs(bufs) do
if buf ~= self.bufnr and (vim.bo[buf].buftype == 'nofile' or #vim.bo[buf].buftype == 0) then
table.insert(both_nofile, true)
end
end
if #both_nofile + 1 == #bufs then
api.nvim_buf_delete(self.bufnr, { force = true })
end
if #wins == 1 or (#wins == 2 and vim.tbl_contains(wins, self.preview_winid)) then
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
api.nvim_win_close(self.preview_winid, true)
end
local buffers = api.nvim_list_bufs()
local scratch = true
local setbuf
for _, buf in ipairs(buffers) do
if
api.nvim_buf_is_loaded(buf)
and fn.bufwinid(buf) == -1
and #api.nvim_buf_get_name(buf) > 0
then
scratch = false
setbuf = buf
break
end
end
if scratch then
local bufnr = api.nvim_create_buf(true, true)
api.nvim_win_set_buf(0, bufnr)
else
if setbuf then
api.nvim_win_set_buf(0, setbuf)
end
end
clean_ctx()
end
end,
desc = 'Outline auto close when last one',
})
end
function ot:render_outline(buf, symbols)
if not self.winid and not self.bufnr then
self.winid, self.bufnr = create_outline_window()
end
local res = parse_symbols(buf, symbols)
self.data = res
local lines = {}
local kind = get_kind() or {}
local fname = libs.get_path_info(buf, 1)
local data = libs.icon_from_devicon(vim.bo[buf].filetype)
lines[#lines + 1] = ' ' .. data[1] .. fname[1]
local prefix = get_hi_prefix()
local hi = {}
for k, v in pairs(res) do
local scope = {}
local indent_with_icon = ' ' .. config.ui.collapse
lines[#lines + 1] = indent_with_icon .. ' ' .. kind[k][1] .. ':' .. #v.data
scope['SagaCount'] = { #indent_with_icon + #kind[k][1] + 1, -1 }
scope['SagaCollapse'] = { 0, #indent_with_icon }
scope[prefix .. kind[k][1]] = { #indent_with_icon, -1 }
hi[#hi + 1] = scope
v.winline = #lines
for j, node in pairs(v.data) do
node.hi_scope = {}
local indent = j == #v.data and '' .. '' or '' .. ''
node.name = indent .. kind[node.kind][2] .. node.name
lines[#lines + 1] = node.name
node.hi_scope['OutlineIndent'] = { 0, #indent }
node.hi_scope[prefix .. kind[node.kind][1]] = { #indent, #indent + #kind[node.kind][2] }
hi[#hi + 1] = node.hi_scope
node.winline = #lines
end
lines[#lines + 1] = ''
hi[#hi + 1] = {}
end
if config.outline.auto_resize then
local max_width = config.outline.win_width
for _, line in ipairs(lines) do
local width = vim.api.nvim_strwidth(line)
if width > max_width then
max_width = width
end
end
config.outline.win_width = max_width
api.nvim_win_set_width(self.winid, outline_conf.win_width)
end
api.nvim_buf_set_lines(self.bufnr, 0, -1, false, lines)
vim.bo[self.bufnr].modifiable = false
api.nvim_buf_add_highlight(self.bufnr, 0, data[2], 0, 0, 4)
for k, v in pairs(hi) do
if not vim.tbl_isempty(v) then
for group, scope in pairs(v) do
api.nvim_buf_add_highlight(self.bufnr, 0, group, k, scope[1], scope[2])
end
end
end
self:apply_map()
api.nvim_create_autocmd('WinClosed', {
callback = function(opt)
if api.nvim_get_current_win() == self.winid and opt.buf == self.bufnr then
clean_ctx()
end
end,
desc = '[lspsaga.nvim] clean the outline data after the win closed',
})
end
function ot:register_events()
if outline_conf.auto_close then
self:close_when_last()
end
if outline_conf.auto_refresh then
self:auto_refresh()
end
if outline_conf.auto_preview then
api.nvim_create_autocmd('CursorMoved', {
group = self.group,
buffer = self.bufnr,
callback = function()
self:auto_preview()
end,
})
end
self.registerd = true
end
function ot:close_and_clean()
if self.winid and api.nvim_win_is_valid(self.winid) then
api.nvim_win_close(self.winid, true)
clean_ctx()
end
end
function ot:outline(buf, non_close)
non_close = non_close or false
if self.winid and api.nvim_win_is_valid(self.winid) and not non_close then
self:close_and_clean()
return
end
buf = buf or api.nvim_get_current_buf()
if #lsp.get_active_clients({ bufnr = buf }) == 0 then
vim.notify('[Lspsaga.nvim] there is no server attatched this buffer')
return
end
if self.pending_request then
vim.notify('[lspsaga.nvim] there is already a request for outline please wait')
return
end
local symbols = get_cache_symbols(buf)
self.group = api.nvim_create_augroup('LspsagaOutline', { clear = false })
self.render_buf = buf
if not symbols then
self.pending_request = true
self:request_and_render(buf)
else
self:render_outline(buf, symbols)
if not self.registerd then
self:register_events()
end
end
end
return setmetatable(ctx, ot)

View file

@ -1,526 +0,0 @@
local api, lsp_util, lsp, uv, fn = vim.api, vim.lsp.util, vim.lsp, vim.loop, vim.fn
local ns = api.nvim_create_namespace('LspsagaRename')
local window = require('lspsaga.window')
local libs = require('lspsaga.libs')
local config = require('lspsaga').config
local util = require('lspsaga.util')
local rename = {}
local context = {}
rename.__index = rename
rename.__newindex = function(t, k, v)
rawset(t, k, v)
end
local function clean_context()
for k, _ in pairs(context) do
context[k] = nil
end
end
function rename:close_rename_win()
if api.nvim_get_mode().mode == 'i' then
vim.cmd([[stopinsert]])
end
if self.winid and api.nvim_win_is_valid(self.winid) then
api.nvim_win_close(self.winid, true)
end
api.nvim_win_set_cursor(0, { self.pos[1], self.pos[2] })
api.nvim_buf_clear_namespace(0, ns, 0, -1)
end
function rename:apply_action_keys()
local modes = { 'i', 'n', 'v' }
util.map_keys(self.bufnr, modes, config.rename.quit, function()
self:close_rename_win()
end)
util.map_keys(self.bufnr, modes, config.rename.exec, function()
self:do_rename()
end)
end
function rename:set_local_options()
local opt_locals = {
scrolloff = 0,
sidescrolloff = 0,
modifiable = true,
}
for opt, val in pairs(opt_locals) do
vim.opt_local[opt] = val
end
end
function rename:find_reference()
local bufnr = api.nvim_get_current_buf()
local params = lsp_util.make_position_params()
params.context = { includeDeclaration = true }
local client = libs.get_client_by_cap('referencesProvider')
if client == nil then
return
end
client.request('textDocument/references', params, function(_, result)
if not result then
return
end
for _, v in pairs(result) do
if v.range then
local line = v.range.start.line
local start_char = v.range.start.character
local end_char = v.range['end'].character
api.nvim_buf_add_highlight(bufnr, ns, 'RenameMatch', line, start_char, end_char)
end
end
end, bufnr)
end
local feedkeys = function(keys, mode)
api.nvim_feedkeys(api.nvim_replace_termcodes(keys, true, true, true), mode, true)
end
local function support_change()
local ok, _ = pcall(require, 'nvim-treesitter')
if not ok then
return true
end
local bufnr = api.nvim_get_current_buf()
local queries = require('nvim-treesitter.query')
local ft_to_lang = require('nvim-treesitter.parsers').ft_to_lang
local lang = ft_to_lang(vim.bo[bufnr].filetype)
local is_installed = #api.nvim_get_runtime_file('parser/' .. lang .. '.so', false) > 0
if not is_installed then
return true
end
local query = queries.get_query(lang, 'highlights')
local ts_utils = require('nvim-treesitter.ts_utils')
local current_node = ts_utils.get_node_at_cursor()
if not current_node then
return
end
local start_row, _, end_row, _ = current_node:range()
for id, _, _ in query:iter_captures(current_node, 0, start_row, end_row) do
local name = query.captures[id]
if name:find('builtin') or name:find('keyword') then
return false
end
end
return true
end
function rename:lsp_rename(arg)
if not support_change() then
vim.notify('Current is builtin or keyword,you can not rename it', vim.log.levels.WARN)
return
end
local cword = fn.expand('<cword>')
self.pos = api.nvim_win_get_cursor(0)
self.arg = arg
local opts = {
height = 1,
width = 30,
}
if vim.fn.has('nvim-0.9') == 1 and config.ui.title then
opts.title = {
{ 'Rename', 'TitleString' },
}
end
local content_opts = {
contents = {},
filetype = 'sagarename',
enter = true,
highlight = {
normal = 'RenameNormal',
border = 'RenameBorder',
},
}
self:find_reference()
self.bufnr, self.winid = window.create_win_with_border(content_opts, opts)
self:set_local_options()
api.nvim_buf_set_lines(self.bufnr, -2, -1, false, { cword })
if config.rename.in_select then
vim.cmd([[normal! V]])
feedkeys('<C-g>', 'n')
end
local quit_id, close_unfocus
local group = require('lspsaga').saga_augroup
quit_id = api.nvim_create_autocmd('QuitPre', {
group = group,
buffer = self.bufnr,
once = true,
nested = true,
callback = function()
self:close_rename_win()
if not quit_id then
api.nvim_del_autocmd(quit_id)
quit_id = nil
end
end,
})
close_unfocus = api.nvim_create_autocmd('WinLeave', {
group = group,
buffer = self.bufnr,
callback = function()
api.nvim_win_close(0, true)
if close_unfocus then
api.nvim_del_autocmd(close_unfocus)
close_unfocus = nil
end
end,
})
self:apply_action_keys()
end
function rename:get_lsp_result()
---@diagnostic disable-next-line: duplicate-set-field
lsp.handlers['textDocument/rename'] = function(_, result, ctx, _)
if not result then
vim.notify("Language server couldn't provide rename result", vim.log.levels.INFO)
return
end
local client = vim.lsp.get_client_by_id(ctx.client_id)
lsp.util.apply_workspace_edit(result, client.offset_encoding)
if not self.arg or (self.arg and self.arg ~= '++project') then
return
end
if fn.executable('rg') == 0 then
return
end
if not self.lspres then
self.lspres = {}
end
if result.changes then
for uri, change in pairs(result.changes) do
local fname = vim.uri_to_fname(uri)
if not self.lspres[fname] then
self.lspres[fname] = {}
end
for _, edit in pairs(change) do
self.lspres[fname][#self.lspres[fname] + 1] = edit.range
end
end
elseif result.documentChanges then
for _, change in pairs(result.documentChanges) do
if not change.kind or change.kind == 'rename' then
local fname = vim.uri_to_fname(change.textDocument.uri)
if not self.lspres[fname] then
self.lspres[fname] = {}
end
for _, edit in pairs(change.edits) do
self.lspres[fname][#self.lspres[fname] + 1] = edit.range or edit.location.range
end
end
end
end
end
end
function rename:do_rename()
self.new_name = vim.trim(api.nvim_get_current_line())
self:close_rename_win()
local current_name = vim.fn.expand('<cword>')
local current_buf = api.nvim_get_current_buf()
if not (self.new_name and #self.new_name > 0) or self.new_name == current_name then
return
end
local current_win = api.nvim_get_current_win()
api.nvim_win_set_cursor(current_win, self.pos)
self:get_lsp_result()
lsp.buf.rename(self.new_name)
local lnum, col = unpack(self.pos)
self.pos = nil
api.nvim_win_set_cursor(current_win, { lnum, col + 1 })
if not self.arg or (self.arg and self.arg ~= '++project') then
clean_context()
return
end
if fn.executable('rg') == 0 then
return
end
local root_dir = lsp.get_active_clients({ bufnr = current_buf })[1].config.root_dir
if not root_dir then
return
end
local timer = uv.new_timer()
timer:start(
0,
5,
vim.schedule_wrap(function()
if self.lspres and vim.tbl_count(self.lspres) > 0 and not timer:is_closing() then
self:whole_project(current_name, root_dir)
timer:stop()
timer:close()
end
end)
)
end
function rename:p_preview()
if self.pp_winid and api.nvim_win_is_valid(self.pp_winid) then
api.nvim_win_close(self.pp_winid, true)
end
local current_line = api.nvim_win_get_cursor(0)[1]
local lines = {}
for i, item in pairs(self.rg_data) do
if i == current_line then
local tbl = api.nvim_buf_get_lines(
item.data.bufnr,
item.data.line_number - 1,
item.data.line_number,
false
)
vim.list_extend(lines, tbl)
end
end
local win_conf = api.nvim_win_get_config(self.p_winid)
local opt = {}
opt.relative = 'editor'
if win_conf.anchor:find('^N') then
if win_conf.row[false] - #lines > 0 then
opt.row = win_conf.row[false]
opt.anchor = win_conf.anchor:gsub('N', 'S')
else
opt.row = win_conf.row[false] + win_conf.height + 3
opt.anchor = win_conf.anchor
end
else
if win_conf.row[false] - win_conf.height - #lines - 4 > 0 then
opt.row = win_conf.row[false] - win_conf.height - 4
opt.anchor = win_conf.anchor
else
opt.row = win_conf.row[false]
opt.anchor = win_conf.anchor:gsub('S', 'N')
end
end
opt.col = win_conf.col[false]
local max_width = math.floor(vim.o.columns * 0.4)
opt.width = win_conf.width < max_width and max_width or win_conf.width
opt.height = #lines
opt.no_size_override = true
if fn.has('nvim-0.9') == 1 and config.ui.title then
opt.title = {
{ 'Preview', 'TitleString' },
}
end
self.pp_bufnr, self.pp_winid = window.create_win_with_border({
contents = lines,
buftype = 'nofile',
highlight = {
normal = 'RenameNormal',
border = 'RenameBorder',
},
}, opt)
end
function rename:popup_win(lines)
local opt = {}
opt.width = window.get_max_float_width()
local max_height = math.floor(vim.o.lines * 0.3)
opt.height = max_height > #context and max_height or #context
opt.no_size_override = true
if fn.has('nvim-0.9') == 1 and config.ui.title then
opt.title = {
{ 'Files', 'TitleString' },
}
end
self.p_bufnr, self.p_winid = window.create_win_with_border({
contents = lines,
enter = true,
buftype = 'nofile',
highlight = {
normal = 'RenameNormal',
border = 'RenameBorder',
},
}, opt)
api.nvim_create_autocmd('CursorMoved', {
buffer = self.p_bufnr,
callback = function()
vim.defer_fn(function()
self:p_preview()
end, 10)
end,
})
util.map_keys(self.bufnr, 'n', config.rename.mark, function()
if not self.confirmed then
self.confirmed = {}
end
local line = api.nvim_win_get_cursor(0)[1]
for i, data in pairs(self.confirmed) do
for _, item in pairs(data) do
if item.winline == line then
table.remove(self.confirmed, i)
api.nvim_buf_clear_namespace(0, ns, 0, -1)
return
end
end
end
api.nvim_buf_add_highlight(0, ns, 'FinderSelection', line - 1, 0, -1)
for i, data in pairs(self.rg_data) do
if i == line then
self.confirmed[#self.confirmed + 1] = data
end
end
end, { buffer = self.p_bufnr, nowait = true })
util.map_keys(self.bufnr, 'n', config.rename.confirm, function()
for _, item in pairs(self.confirmed or {}) do
for _, match in pairs(item.data.submatches) do
api.nvim_buf_set_text(
item.data.bufnr,
item.data.line_number - 1,
match.start,
item.data.line_number - 1,
match['end'],
{ self.new_name }
)
api.nvim_buf_call(item.data.bufnr, function()
vim.cmd.write()
end)
end
end
if self.p_winid and api.nvim_win_is_valid(self.p_winid) then
api.nvim_win_close(self.p_winid, true)
end
if self.pp_winid and api.nvim_win_is_valid(self.pp_winid) then
api.nvim_win_close(self.pp_winid, true)
end
clean_context()
end, { buffer = self.p_bufnr, nowait = true })
end
function rename:check_in_lspres(fname, lnum)
if not self.lspres[fname] then
return false
end
for _, range in pairs(self.lspres[fname]) do
if range.start.line + 1 == lnum then
return true
end
end
return false
end
function rename:whole_project(cur_name, root_dir)
local stdout = uv.new_pipe(false)
local stderr = uv.new_pipe(false)
local stdin = uv.new_pipe(false)
local function safe_close(handle)
if not uv.is_closing(handle) then
uv.close(handle)
end
end
local res = {}
local handle, pid
local function parse_result()
local function decode()
local result = {}
for _, v in pairs(res) do
for _, item in pairs(v) do
local tbl = vim.json.decode(item)
table.insert(result, tbl)
end
end
return result
end
local parsed = decode()
if not self.rg_data then
self.rg_data = {}
end
for _, v in ipairs(parsed) do
local path = vim.tbl_get(v, 'data', 'path', 'text')
local lnum = vim.tbl_get(v, 'data', 'line_number')
if v.type == 'match' and path and lnum and not self:check_in_lspres(path, lnum) then
table.insert(self.rg_data, v)
end
end
self.lspres = nil
local lines = {}
for _, item in pairs(self.rg_data) do
local root_parts = vim.split(root_dir, libs.path_sep, { trimempty = true })
local fname_parts = vim.split(item.data.path.text, libs.path_sep, { trimempty = true })
local short = table.concat({ unpack(fname_parts, #root_parts + 1) }, libs.path_sep)
lines[#lines + 1] = short
local uri = vim.uri_from_fname(item.data.path.text)
local bufnr = vim.uri_to_bufnr(uri)
item.data.bufnr = bufnr
if not api.nvim_buf_is_loaded(bufnr) then
-- avoid lsp attached this buffer
vim.opt.eventignore:append({ 'BufRead', 'BufReadPost', 'BufEnter', 'FileType' })
fn.bufload(bufnr)
vim.opt.eventignore:remove({ 'BufRead', 'BufReadPost', 'BufEnter', 'FileType' })
end
end
if #lines == 0 then
return
end
self:popup_win(lines)
end
handle, pid = uv.spawn('rg', {
args = { cur_name, root_dir, '--json' },
stdio = { stdin, stdout, stderr },
}, function(_, _)
print(pid .. ' exit')
uv.read_stop(stdout)
uv.read_stop(stderr)
safe_close(handle)
safe_close(stdout)
safe_close(stderr)
-- parse after close
vim.schedule(parse_result)
end)
uv.read_start(stdout, function(err, data)
assert(not err, err)
if data then
local tbl = vim.split(data, '\n', { trimempty = true })
res[#res + 1] = tbl
end
end)
end
return setmetatable(context, rename)

207
lua/lspsaga/rename/init.lua Normal file
View file

@ -0,0 +1,207 @@
local api, lsp, fn = vim.api, vim.lsp, vim.fn
local ns = api.nvim_create_namespace('LspsagaRename')
local win = require('lspsaga.window')
local util = require('lspsaga.util')
local config = require('lspsaga').config
local rename = {}
local context = {}
rename.__index = rename
rename.__newindex = function(t, k, v)
rawset(t, k, v)
end
local function clean_context()
for k, _ in pairs(context) do
context[k] = nil
end
end
function rename:close_rename_win()
if api.nvim_get_mode().mode == 'i' then
vim.cmd([[stopinsert]])
end
if self.winid and api.nvim_win_is_valid(self.winid) then
api.nvim_win_close(self.winid, true)
end
api.nvim_win_set_cursor(0, { self.pos[1], self.pos[2] })
api.nvim_buf_clear_namespace(0, ns, 0, -1)
end
function rename:apply_action_keys(project)
local modes = { 'i', 'n', 'v' }
for i, mode in ipairs(modes) do
util.map_keys(self.bufnr, config.rename.keys.quit, function()
self:close_rename_win()
end, mode)
if i ~= 3 then
util.map_keys(self.bufnr, config.rename.keys.exec, function()
self:do_rename(project)
end, mode)
end
end
end
function rename:find_reference()
local bufnr = api.nvim_get_current_buf()
local params = lsp.util.make_position_params()
params.context = { includeDeclaration = true }
local clients = util.get_client_by_method('textDocment/references')
if #clients == 0 then
return
end
clients[1].request('textDocument/references', params, function(_, result)
if not result then
return
end
for _, v in ipairs(result) do
if v.range then
local buf = vim.uri_to_bufnr(v.uri)
local line = v.range.start.line
local start_char = v.range.start.character
local end_char = v.range['end'].character
if buf == bufnr then
api.nvim_buf_add_highlight(bufnr, ns, 'RenameMatch', line, start_char, end_char)
end
end
end
end, bufnr)
end
local feedkeys = function(keys, mode)
api.nvim_feedkeys(api.nvim_replace_termcodes(keys, true, true, true), mode, true)
end
local function parse_arugment(args)
local mode, project
for _, arg in ipairs(args) do
if arg:find('mode=') then
mode = vim.split(arg, '=', { trimempty = true })
elseif arg:find('%+%+project') then
project = true
end
end
return mode, project
end
function rename:lsp_rename(args)
local cword = fn.expand('<cword>')
self.pos = api.nvim_win_get_cursor(0)
local mode, project = parse_arugment(args)
local float_opt = {
height = 1,
width = 30,
}
if config.ui.title then
float_opt.title = {
{ 'Rename', 'SagaTitle' },
}
end
self:find_reference()
self.bufnr, self.winid = win
:new_float(float_opt, true)
:setlines({ cword })
:bufopt({
['bufhidden'] = 'wipe',
['buftype'] = 'nofile',
['filetype'] = 'sagarename',
})
:winopt('scrolloff', 0)
:winhl('RenameNormal', 'RenameBorder')
:wininfo()
if mode == 'i' then
vim.cmd.startinsert()
elseif mode == 's' or config.rename.in_select then
vim.cmd([[normal! V]])
feedkeys('<C-g>', 'n')
end
local quit_id, close_unfocus
local group = require('lspsaga').saga_augroup
quit_id = api.nvim_create_autocmd('QuitPre', {
group = group,
buffer = self.bufnr,
once = true,
nested = true,
callback = function()
self:close_rename_win()
if not quit_id then
api.nvim_del_autocmd(quit_id)
quit_id = nil
end
end,
})
close_unfocus = api.nvim_create_autocmd('WinLeave', {
group = group,
buffer = self.bufnr,
callback = function()
api.nvim_win_close(0, true)
if close_unfocus then
api.nvim_del_autocmd(close_unfocus)
close_unfocus = nil
end
end,
})
self:apply_action_keys(project)
end
local function rename_handler(project, curname, new_name)
---@diagnostic disable-next-line: duplicate-set-field
lsp.handlers['textDocument/rename'] = function(err, result, ctx)
if err then
vim.notify(
'[Lspsaga] rename failed err in callback' .. table.concat(err),
vim.log.levels.ERROR
)
return
end
local client = lsp.get_client_by_id(ctx.client_id)
for uri, edits in pairs(result.changes or {}) do
local bufnr = vim.uri_to_bufnr(uri)
lsp.util.apply_text_edits(edits, bufnr, client.offset_encoding)
if config.rename.auto_save then
api.nvim_buf_call(bufnr, function()
vim.cmd('noautocmd write!')
end)
end
end
if project then
require('lspsaga.rename.project'):new({ curname, new_name })
end
end
end
local original = vim.lsp.util.apply_workspace_edit
function rename:do_rename(project)
local new_name = vim.trim(api.nvim_get_current_line())
self:close_rename_win()
local current_name = vim.fn.expand('<cword>')
if not (new_name and #new_name > 0) or new_name == current_name then
return
end
local current_win = api.nvim_get_current_win()
api.nvim_win_set_cursor(current_win, self.pos)
rename_handler(project, current_name, new_name)
lsp.buf.rename(new_name)
local lnum, col = unpack(self.pos)
self.pos = nil
api.nvim_win_set_cursor(current_win, { lnum, col + 1 })
clean_context()
end
return setmetatable(context, rename)

View file

@ -0,0 +1,190 @@
local lsp, fn, api = vim.lsp, vim.fn, vim.api
---@diagnostic disable-next-line: deprecated
local uv = vim.version().minor >= 10 and vim.uv or vim.loop
local config = require('lspsaga').config
local win = require('lspsaga.window')
local ns = api.nvim_create_namespace('SagaProjectRename')
local util = require('lspsaga.util')
--project rename module
local M = {}
local function safe_close(handle)
if not uv.is_closing(handle) then
uv.close(handle)
end
end
local function get_root_dir()
local clients = lsp.get_active_clients({ bufnr = 0 })
for _, client in ipairs(clients) do
if client.config.root_dir then
return client.config.root_dir
end
end
end
local function decode(data)
local t = vim.split(data, '\n', { trimempty = true })
local result = {}
for _, v in pairs(t) do
local tbl = vim.json.decode(v)
if tbl.type == 'match' then
local path = tbl.data.path.text
if not result[path] then
result[path] = {}
end
result[path][#result[path] + 1] = tbl
end
end
return result
end
local function create_win()
local win_height = api.nvim_win_get_height(0)
local win_width = api.nvim_win_get_width(0)
local float_opt = {
height = math.floor(win_height * config.rename.project_max_height),
width = math.floor(win_width * config.rename.project_max_width),
title = config.ui.title and 'Project' or nil,
}
return win
:new_float(float_opt, true)
:bufopt({
['buftype'] = 'nofile',
['bufhidden'] = 'wipe',
})
:winhl('SagaNormal', 'SagaBorder')
:wininfo()
end
local function find_data_by_lnum(data, lnum)
for _, item in pairs(data) do
for _, v in ipairs(item) do
if v.winline == lnum then
return v
end
end
end
end
local function apply_map(bufnr, winid, data, new_name)
util.map_keys(bufnr, config.rename.keys.select, function()
local curlnum = api.nvim_win_get_cursor(winid)[1]
if fn.indent(curlnum) ~= 2 then
return
end
local item = find_data_by_lnum(data, curlnum)
if not item.selected then
item.selected = true
api.nvim_buf_add_highlight(bufnr, ns, 'SagaSelect', curlnum - 1, 0, -1)
return
end
item.selectd = false
api.nvim_buf_add_highlight(bufnr, ns, 'Comment', curlnum - 1, 0, -1)
end)
util.map_keys(bufnr, config.rename.keys.quit, function()
api.nvim_win_close(winid, true)
end)
util.map_keys(bufnr, config.rename.keys.exec, function()
for fname, v in pairs(data) do
for _, item in ipairs(v) do
if item.selected then
local buf = fn.bufadd(fname)
if not api.nvim_buf_is_loaded(buf) then
fn.bufload(buf)
end
for _, match in ipairs(item.data.submatches) do
api.nvim_buf_set_text(
buf,
item.data.line_number - 1,
match.start,
item.data.line_number - 1,
match['end'],
{ new_name }
)
api.nvim_buf_call(buf, function()
vim.cmd.write()
end)
end
end
end
end
api.nvim_win_close(winid, true)
end)
end
local function render(chunks, root_dir, new_name)
local result = decode(chunks, root_dir)
local lines = {}
local bufnr, winid = create_win()
local line = 1
for fname, item in pairs(result) do
fname = fname:sub(#vim.env.HOME + 2)
api.nvim_buf_set_lines(bufnr, line - 1, line - 1, false, { fname })
api.nvim_buf_add_highlight(bufnr, ns, 'SagaFinderFname', line - 1, 0, -1)
line = line + 1
vim.tbl_map(function(val)
local ln = val.data.line_number
local text = 'ln:' .. ln .. (' '):rep(5 - #tostring(ln)) .. vim.trim(val.data.lines.text)
api.nvim_buf_set_lines(bufnr, line - 1, -1, false, { (' '):rep(2) .. text })
api.nvim_buf_add_highlight(bufnr, ns, 'Comment', line - 1, 0, -1)
val.winline = line
line = line + 1
end, item)
end
apply_map(bufnr, winid, result, new_name)
end
function M:new(args)
if fn.executable('rg') == 0 then
vim.notify('[Lspsaga] does not find rg')
return
end
if #args < 2 then
vim.notify('[Lspsaga] missing search pattern or new name')
return
end
local stdout = uv.new_pipe(false)
local stderr = uv.new_pipe(false)
local stdin = uv.new_pipe(false)
local handle
local chunks = {}
local root_dir = get_root_dir()
if not root_dir then
vim.notify('[Lspsaga] buffer run in single file mode')
return
end
handle, _ = uv.spawn('rg', {
args = { args[1], root_dir, '--json' },
stdio = { stdin, stdout, stderr },
}, function(_, _)
uv.read_stop(stdout)
uv.read_stop(stderr)
safe_close(handle)
safe_close(stdout)
safe_close(stderr)
-- parse after close
vim.schedule(function()
render(table.concat(chunks), root_dir, args[2])
end)
end)
uv.read_start(stdout, function(err, data)
assert(not err, err)
if data then
chunks[#chunks + 1] = data
end
end)
end
return M

View file

@ -1,384 +0,0 @@
local api, fn = vim.api, vim.fn
local window = require('lspsaga.window')
local libs = require('lspsaga.libs')
local diag = require('lspsaga.diagnostic')
local config = require('lspsaga').config
local ui = config.ui
local diag_conf = config.diagnostic
local nvim_buf_set_keymap = api.nvim_buf_set_keymap
local ns = api.nvim_create_namespace('SagaDiagnostic')
local nvim_buf_set_extmark = api.nvim_buf_set_extmark
local nvim_buf_add_highlight = api.nvim_buf_add_highlight
local ctx = {}
local sd = {}
sd.__index = sd
function sd.__newindex(t, k, v)
rawset(t, k, v)
end
--- clean ctx
local function clean_ctx()
for i, _ in pairs(ctx) do
ctx[i] = nil
end
end
---get the line or cursor diagnostics
---@param opt table
function sd:get_diagnostic(opt)
local cur_buf = api.nvim_get_current_buf()
if opt.buffer then
return vim.diagnostic.get(cur_buf)
end
local line, col = unpack(api.nvim_win_get_cursor(0))
local entrys = vim.diagnostic.get(cur_buf, { lnum = line - 1 })
if opt.line then
return entrys
end
if opt.cursor then
local res = {}
for _, v in pairs(entrys) do
if v.col <= col and v.end_col >= col then
res[#res + 1] = v
end
end
return res
end
return vim.diagnostic.get()
end
---@private sort table by diagnsotic severity
local function sort_by_severity(entrys)
table.sort(entrys, function(k1, k2)
return k1.severity < k2.severity
end)
end
function sd:create_win(opt, content)
local curbuf = api.nvim_get_current_buf()
local increase = window.win_height_increase(content)
local max_len = window.get_max_content_length(content)
local max_height = math.floor(vim.o.lines * diag_conf.max_show_height)
local max_width = math.floor(vim.o.columns * diag_conf.max_show_width)
local float_opt = {
width = max_len < max_width and max_len or max_width,
height = #content + increase > max_height and max_height or #content + increase,
no_size_override = true,
}
if fn.has('nvim-0.9') == 1 and config.ui.title then
if opt.buffer then
float_opt.title = 'Buffer'
elseif opt.line then
float_opt.title = 'Line'
elseif opt.cursor then
float_opt.title = 'Cursor'
else
float_opt.title = 'Workspace'
end
float_opt.title_pos = 'center'
end
local content_opt = {
contents = {},
filetype = 'markdown',
enter = true,
bufnr = self.bufnr,
wrap = true,
highlight = {
normal = 'DiagnosticShowNormal',
border = 'DiagnosticShowBorder',
},
}
local close_autocmds =
{ 'CursorMoved', 'CursorMovedI', 'InsertEnter', 'BufDelete', 'WinScrolled' }
if opt.arg and opt.arg == '++unfocus' then
opt.focusable = false
close_autocmds[#close_autocmds] = 'BufLeave'
content_opt.enter = false
else
opt.focusable = true
api.nvim_create_autocmd('BufEnter', {
callback = function(args)
if not self.winid or not api.nvim_win_is_valid(self.winid) then
pcall(api.nvim_del_autocmd, args.id)
end
local cur_buf = api.nvim_get_current_buf()
if cur_buf ~= self.bufnr and self.winid and api.nvim_win_is_valid(self.winid) then
api.nvim_win_close(self.winid, true)
clean_ctx()
pcall(api.nvim_del_autocmd, args.id)
end
end,
})
end
_, self.winid = window.create_win_with_border(content_opt, float_opt)
vim.wo[self.winid].conceallevel = 2
vim.wo[self.winid].concealcursor = 'niv'
vim.wo[self.winid].showbreak = ui.lines[3]
vim.wo[self.winid].breakindent = true
vim.wo[self.winid].breakindentopt = 'shift:2,sbr'
vim.wo[self.winid].linebreak = true
api.nvim_win_set_cursor(self.winid, { 2, 3 })
for _, key in ipairs(diag_conf.keys.quit_in_show) do
nvim_buf_set_keymap(self.bufnr, 'n', key, '', {
noremap = true,
nowait = true,
callback = function()
local curwin = api.nvim_get_current_win()
if curwin ~= self.winid then
return
end
if api.nvim_win_is_valid(curwin) then
api.nvim_win_close(curwin, true)
clean_ctx()
end
end,
})
end
vim.defer_fn(function()
libs.close_preview_autocmd(curbuf, self.winid, close_autocmds)
end, 0)
end
local function find_node_by_lnum(lnum, entrys)
for _, items in pairs(entrys) do
for _, item in ipairs(items.diags) do
if item.winline == lnum then
return item
end
end
end
end
local function change_winline(cond, direction, entrys)
for _, items in pairs(entrys) do
for _, item in ipairs(items.diags) do
if cond(item) then
item.winline = item.winline + direction
end
end
end
end
function sd:show(opt)
local indent = ' '
local line_count = 0
local content = {}
local curbuf = api.nvim_get_current_buf()
local icon_data = libs.icon_from_devicon(vim.bo[curbuf].filetype)
self.bufnr = api.nvim_create_buf(false, false)
vim.bo[self.bufnr].buftype = 'nofile'
local titlehi = {}
for bufnr, items in pairs(opt.entrys) do
---@diagnostic disable-next-line: param-type-mismatch
local fname = fn.fnamemodify(api.nvim_buf_get_name(tonumber(bufnr)), ':t')
local counts = diag:get_diag_counts(items.diags)
local text = ui.collapse .. ' ' .. icon_data[1] .. fname .. ' Bufnr[[' .. bufnr .. ']]'
local diaghi = {}
for i, v in ipairs(counts) do
local sign = diag:get_diagnostic_sign(i)
if v > 0 then
local start = #text
text = text .. ' ' .. sign .. v
diaghi[#diaghi + 1] = {
'Diagnostic' .. diag:get_diag_type(i),
start,
#text,
}
end
end
content[#content + 1] = text
api.nvim_buf_set_lines(self.bufnr, line_count, line_count + 1, false, { text })
line_count = line_count + 1
titlehi[tostring(line_count - 1)] = {
{ 'SagaCollapse', 0, #ui.collapse },
icon_data[2] and {
icon_data[2],
#ui.collapse + 1,
#ui.collapse + 1 + #icon_data[1],
} or nil,
{
'DiagnosticFname',
#ui.collapse + 1 + (icon_data[2] and #icon_data[1] or 0),
#ui.collapse + 1 + (icon_data[2] and #icon_data[1] or 0) + #fname,
},
{
'DiagnosticBufnr',
#ui.collapse + 2 + (icon_data[2] and #icon_data[1] or 0) + #fname,
#ui.collapse + 14 + (icon_data[2] and #icon_data[1] or 0) + #fname,
},
unpack(diaghi),
}
for _, v in ipairs(titlehi[tostring(line_count - 1)]) do
nvim_buf_add_highlight(self.bufnr, 0, v[1], line_count - 1, v[2], v[3])
end
items.expand = true
for i, item in ipairs(items.diags) do
if item.message:find('\n') then
item.message = item.message:gsub('\n', '')
end
text = indent .. item.message
api.nvim_buf_set_lines(self.bufnr, line_count, line_count + 1, false, { text })
line_count = line_count + 1
nvim_buf_add_highlight(
self.bufnr,
0,
diag_conf.text_hl_follow and 'Diagnostic' .. diag:get_diag_type(item.severity)
or 'DiagnosticText',
line_count - 1,
3,
-1
)
item.winline = line_count
content[#content + 1] = text
nvim_buf_set_extmark(self.bufnr, ns, line_count - 1, 0, {
virt_text = {
{ i == #items.diags and ui.lines[1] or ui.lines[2], 'FinderLines' },
{ ui.lines[4]:rep(2), 'FinderLines' },
},
virt_text_pos = 'overlay',
})
end
api.nvim_buf_set_lines(self.bufnr, line_count, line_count + 1, false, { '' })
line_count = line_count + 1
end
vim.bo[self.bufnr].modifiable = false
local nontext = api.nvim_get_hl_by_name('NonText', true)
api.nvim_set_hl(ns, 'NonText', {
link = 'FinderLines',
})
local function expand_or_collapse(text)
local change = text:find(ui.expand) and { ui.expand, ui.collapse } or { ui.collapse, ui.expand }
text = text:gsub(change[1], change[2])
local curline = api.nvim_win_get_cursor(self.winid)[1]
vim.bo[self.bufnr].modifiable = true
local bufnr = text:match('%[%[(.+)%]%]')
local data = opt.entrys[tostring(bufnr)]
local hi = titlehi[tostring(curline - 1)]
if data.expand then
api.nvim_buf_clear_namespace(self.bufnr, ns, curline - 1, curline + #data.diags)
api.nvim_buf_set_lines(self.bufnr, curline - 1, curline + #data.diags, false, { text })
for _, v in ipairs(hi) do
nvim_buf_add_highlight(self.bufnr, 0, v[1], curline - 1, v[2], v[3])
end
for _, v in ipairs(data.diags) do
v.winline = -1
end
change_winline(function(item)
return item.winline > curline + #data.diags
end, -#data.diags, opt.entrys)
data.expand = false
else
local lines = {}
vim.tbl_map(function(k)
lines[#lines + 1] = indent .. k.message
end, data.diags)
api.nvim_buf_set_lines(self.bufnr, curline - 1, curline, false, { text, unpack(lines) })
for _, v in ipairs(hi) do
nvim_buf_add_highlight(self.bufnr, 0, v[1], curline - 1, v[2], v[3])
end
for i, v in ipairs(data.diags) do
v.winline = curline + i
nvim_buf_add_highlight(
self.bufnr,
0,
diag_conf.text_hl_follow and 'Diagnostic' .. diag:get_diag_type(v.severity)
or 'DiagnosticText',
v.winline - 1,
3,
-1
)
nvim_buf_set_extmark(self.bufnr, ns, curline + i - 1, 0, {
virt_text = {
{ i == #data.diags and ui.lines[1] or ui.lines[2], 'FinderLines' },
{ ui.lines[4]:rep(2), 'FinderLines' },
},
virt_text_pos = 'overlay',
})
end
data.expand = true
end
vim.bo[self.bufnr].modifiable = false
end
nvim_buf_set_keymap(self.bufnr, 'n', diag_conf.keys.expand_or_jump, '', {
nowait = true,
silent = true,
callback = function()
local text = api.nvim_get_current_line()
if text:find(ui.expand) or text:find(ui.collapse) then
expand_or_collapse(text)
return
end
local winline = api.nvim_win_get_cursor(self.winid)[1]
api.nvim_set_hl(0, 'NonText', {
foreground = nontext.foreground,
background = nontext.background,
})
local entry = find_node_by_lnum(winline, opt.entrys)
if entry then
api.nvim_win_close(self.winid, true)
clean_ctx()
local winid = fn.bufwinid(entry.bufnr)
if winid == -1 then
winid = api.nvim_get_current_win()
end
api.nvim_set_current_win(winid)
api.nvim_win_set_cursor(winid, { entry.lnum + 1, entry.col })
local width = #api.nvim_get_current_line()
libs.jump_beacon({ entry.lnum, entry.col }, width)
end
end,
})
self:create_win(opt, content)
end
---migreate diagnostic to a table that
---use in show function
local function migrate_diagnostics(entrys)
local tbl = {}
for _, item in ipairs(entrys) do
local key = tostring(item.bufnr)
if not tbl[key] then
tbl[key] = {
diags = {},
}
end
tbl[key].diags[#tbl[key].diags + 1] = item
end
return tbl
end
function sd:show_diagnostics(opt)
local entrys = self:get_diagnostic(opt)
if next(entrys) == nil then
return
end
sort_by_severity(entrys)
opt.entrys = migrate_diagnostics(entrys)
self:show(opt)
end
return setmetatable(ctx, sd)

72
lua/lspsaga/slist.lua Normal file
View file

@ -0,0 +1,72 @@
---single linked list module
local M = {}
function M.new()
return { value = nil, next = nil, prev = nil }
end
function M.tail_push(list, node)
local tmp = list
if not tmp.value then
tmp.value = node
return
end
while true do
if not tmp.next then
break
end
tmp = tmp.next
end
tmp.next = { value = node }
end
function M.find_node(list, curlnum)
local tmp = list
if not tmp.value then
return
end
while tmp do
if tmp.value.winline == curlnum then
return tmp
end
tmp = tmp.next
end
end
function M.insert_node(curnode, node)
local tmp = curnode.next
curnode.next = {
value = node,
next = tmp,
}
end
function M.update_winline(node, count)
node = node.next
local total = count < 0 and math.abs(count) or 0
while node do
if total ~= 0 then
node.value.winline = -1
total = total - 1
if node.value.expand then
node.value.expand = false
end
else
if node.value.winline ~= -1 then
node.value.winline = node.value.winline + count
end
end
node = node.next
end
end
function M.list_map(list, fn)
local node = list
while node do
fn(node)
node = node.next
end
end
return M

199
lua/lspsaga/symbol/init.lua Normal file
View file

@ -0,0 +1,199 @@
local api, lsp = vim.api, vim.lsp
local config = require('lspsaga').config
local symbol = {}
local cache = {}
symbol.__index = symbol
function symbol.__newindex(t, k, v)
rawset(t, k, v)
end
local function clean_buf_cache(buf)
buf = buf or api.nvim_get_current_buf()
if buf and cache[buf] then
for k, _ in pairs(cache[buf]) do
cache[buf][k] = nil
end
cache[buf] = nil
end
end
local buf_changedtick = {}
function symbol:buf_watcher(buf, client_id)
local function defer_request(changedtick)
vim.defer_fn(function()
if not self[buf] or not api.nvim_buf_is_valid(buf) then
return
end
self[buf].pending_request = true
self:do_request(buf, client_id, function()
if not api.nvim_buf_is_valid(buf) then
return
end
self[buf].pending_request = false
if changedtick < buf_changedtick[buf] then
changedtick = api.nvim_buf_get_changedtick(buf)
defer_request(changedtick)
else
self[buf].changedtick = changedtick
end
end)
end, 1000)
end
api.nvim_buf_attach(buf, false, {
on_lines = function(_, b, changedtick)
if b ~= buf then
return
end
buf_changedtick[buf] = changedtick
if not self[buf].pending_request then
defer_request(changedtick)
end
end,
})
api.nvim_create_autocmd('BufDelete', {
buffer = buf,
callback = function()
clean_buf_cache(buf)
end,
})
end
function symbol:do_request(buf, client_id, callback)
local params = { textDocument = {
uri = vim.uri_from_bufnr(buf),
} }
local client = vim.lsp.get_client_by_id(client_id)
if not client then
return
end
if not self[buf] then
self[buf] = {}
self:buf_watcher(buf, client.id)
end
self[buf].pending_request = true
---@diagnostic disable-next-line: invisible
client.request('textDocument/documentSymbol', params, function(err, result, ctx)
if not api.nvim_buf_is_loaded(ctx.bufnr) or not self[ctx.bufnr] then
return
end
self[ctx.bufnr].pending_request = false
if callback then
callback(result)
end
if err then
return
end
self[ctx.bufnr].symbols = result
api.nvim_exec_autocmds('User', {
pattern = 'SagaSymbolUpdate',
modeline = false,
data = { symbols = result or {}, client_id = ctx.client_id, bufnr = ctx.bufnr },
})
end, buf)
end
function symbol:get_buf_symbols(buf)
buf = buf or api.nvim_get_current_buf()
local res = {}
if not self[buf] then
return
end
if self[buf].pending_request then
res.pending_request = self[buf].pending_request
return res
end
res.symbols = self[buf].symbols
res.pending_request = self[buf].pending_request
return res
end
function symbol:node_is_keyword(buf, node)
if not node.selectionRange then
return false
end
local tnode = vim.treesitter.get_node({
bufnr = buf,
pos = {
node.selectionRange.start.line,
node.selectionRange.start.character,
},
})
if not tnode then
return
end
local keylist = {
'if_statement',
'for_statement',
'while_statement',
'repeat_statement',
'do_statement',
}
if vim.tbl_contains(keylist, tnode:type()) then
return true
end
return false
end
function symbol:register_module()
local group = api.nvim_create_augroup('LspsagaSymbols', { clear = true })
api.nvim_create_autocmd('LspAttach', {
group = group,
callback = function(args)
if self[args.buf] or api.nvim_get_current_buf() ~= args.buf then
return
end
local client = lsp.get_client_by_id(args.data.client_id)
if not client.supports_method('textDocument/documentSymbol') then
return
end
local winbar = require('lspsaga.symbol.winbar')
winbar.file_bar(args.buf)
self:do_request(args.buf, args.data.client_id, function(result)
if api.nvim_get_current_buf() ~= args.buf then
return
end
winbar.init_winbar(args.buf)
if config.implement.enable and client.supports_method('textDocument/implementation') then
require('lspsaga.implement').start(args.buf, args.data.client_id, result)
end
end)
end,
})
api.nvim_create_autocmd('LspDetach', {
group = group,
callback = function(args)
if self[args.buf] then
self[args.buf] = nil
end
end,
})
end
function symbol:outline()
require('lspsaga.symbol.outline'):outline()
end
return setmetatable(cache, symbol)

View file

@ -0,0 +1,430 @@
local ot = {}
local api, fn = vim.api, vim.fn
---@diagnostic disable-next-line: deprecated
local uv = vim.version().minor >= 10 and vim.uv or vim.loop
local kind = require('lspsaga.lspkind').kind
local config = require('lspsaga').config
local util = require('lspsaga.util')
local symbol = require('lspsaga.symbol')
local win = require('lspsaga.window')
local buf_set_lines = api.nvim_buf_set_lines
local buf_set_extmark = api.nvim_buf_set_extmark
local outline_conf = config.outline
local ns = api.nvim_create_namespace('SagaOutline')
local beacon = require('lspsaga.beacon').jump_beacon
local slist = require('lspsaga.slist')
local ctx = {}
function ot.__newindex(t, k, v)
rawset(t, k, v)
end
ot.__index = ot
local function clean_ctx()
for k, _ in pairs(ctx) do
ctx[k] = nil
end
end
local function create_outline_window()
local pos = outline_conf.win_position == 'right' and 'botright' or 'topleft'
vim.cmd(pos .. ' vnew')
local winid, bufnr = api.nvim_get_current_win(), api.nvim_get_current_buf()
api.nvim_win_set_width(winid, config.outline.win_width)
return win
:from_exist(bufnr, winid)
:bufopt({
['filetype'] = 'sagaoutline',
['bufhidden'] = 'wipe',
['buflisted'] = false,
['buftype'] = 'nofile',
['indentexpr'] = 'indent',
})
:winopt({
['wrap'] = false,
['number'] = false,
['relativenumber'] = false,
['signcolumn'] = 'no',
['list'] = false,
['spell'] = false,
['cursorcolumn'] = false,
['cursorline'] = false,
['winfixwidth'] = true,
['winhl'] = 'Normal:OutlineNormal',
['stc'] = '',
})
:wininfo()
end
function ot:parse(symbols)
local row = 0
self.list = slist.new()
local function recursive_parse(data, level)
for i, node in ipairs(data) do
level = level or 0
local indent = ' ' .. (' '):rep(level)
node.name = node.name == ' ' and '_' or node.name
buf_set_lines(self.bufnr, row, -1, false, { indent .. node.name })
row = row + 1
if level == 0 then
node.winline = row
end
buf_set_extmark(self.bufnr, ns, row - 1, #indent - 2, {
virt_text = { { kind[node.kind][2], 'Saga' .. kind[node.kind][1] } },
virt_text_pos = 'overlay',
})
local inlevel = 4 + 2 * level
if inlevel == 4 and not node.children then
local virt = {
{ row == 1 and config.ui.lines[5] or config.ui.lines[2], 'SagaVirtLine' },
{ config.ui.lines[4]:rep(2), 'SagaVirtLine' },
}
buf_set_extmark(self.bufnr, ns, row - 1, 0, {
virt_text = virt,
virt_text_pos = 'overlay',
})
else
for j = 1, inlevel - 4, 2 do
local virt = {}
if not node.children and j + 2 > inlevel - 4 then
virt[#virt + 1] = i == #data and { config.ui.lines[1], 'SagaVirtLine' }
or { config.ui.lines[2], 'SagaVirtLine' }
virt[#virt + 1] = { config.ui.lines[4]:rep(2), 'SagaVirtLine' }
else
virt = { { config.ui.lines[3], 'SagaVirtLine' } }
end
buf_set_extmark(self.bufnr, ns, row - 1, j - 1, {
virt_text = virt,
virt_text_pos = 'overlay',
})
end
end
if config.outline.detail then
buf_set_extmark(self.bufnr, ns, row - 1, 0, {
virt_text = { { node.detail or '', 'Comment' } },
})
end
local copy = vim.deepcopy(node)
copy.children = nil
copy.winline = row
copy.inlevel = #indent
if node.children then
copy.expand = true
copy.virtid = uv.hrtime()
buf_set_extmark(self.bufnr, ns, row - 1, #indent - 4, {
id = copy.virtid,
virt_text = { { config.ui.collapse, 'SagaToggle' } },
virt_text_pos = 'overlay',
})
slist.tail_push(self.list, copy)
recursive_parse(node.children, level + 1)
else
slist.tail_push(self.list, copy)
end
end
end
recursive_parse(symbols)
api.nvim_set_option_value('modifiable', false, { buf = self.bufnr })
end
function ot:collapse(node, curlnum)
local row = curlnum - 1
local inlevel = fn.indent(curlnum)
local tmp = node.next
while tmp do
local icon = kind[tmp.value.kind][2]
local level = tmp.value.inlevel
buf_set_lines(
self.bufnr,
row + 1,
row + 1,
false,
{ (' '):rep(tmp.value.inlevel) .. tmp.value.name }
)
row = row + 1
tmp.value.winline = row + 1
if tmp.value.expand == false then
tmp.value.expand = true
end
buf_set_extmark(self.bufnr, ns, row, level - 2, {
virt_text = { { icon, 'Saga' .. kind[tmp.value.kind][1] } },
virt_text_pos = 'overlay',
})
local has_child = tmp.next and tmp.next.value.inlevel > level
if has_child then
buf_set_extmark(self.bufnr, ns, row, level - 4, {
virt_text = { { config.ui.collapse, 'SagaToggle' } },
virt_text_pos = 'overlay',
})
end
local islast = not tmp.next or tmp.next.value.inlevel < level
for j = 1, level - 4, 2 do
local virt = {}
if j + 2 > level - 4 and not has_child then
virt[#virt + 1] = islast and { config.ui.lines[1], 'SagaVirtLine' }
or { config.ui.lines[2], 'SagaVirtLine' }
virt[#virt + 1] = { config.ui.lines[4]:rep(2), 'SagaVirtLine' }
else
virt = { { config.ui.lines[3], 'SagaVirtLine' } }
end
buf_set_extmark(self.bufnr, ns, row, j - 1, {
virt_text = virt,
virt_text_pos = 'overlay',
})
end
if config.outline.detail then
buf_set_extmark(self.bufnr, ns, row, 0, {
virt_text = { { tmp.value.detail, 'Comment' } },
})
end
if not tmp or (tmp.next and tmp.next.value.inlevel <= inlevel) then
break
end
tmp = tmp.next
end
if tmp then
slist.update_winline(tmp, row - curlnum + 1, curlnum)
end
end
function ot:expand_or_jump()
local curlnum = unpack(api.nvim_win_get_cursor(self.winid))
local node = slist.find_node(self.list, curlnum)
if not node then
return
end
local count = api.nvim_buf_line_count(self.bufnr)
local inlevel = fn.indent(curlnum)
if node.value.expand then
api.nvim_set_option_value('modifiable', true, { buf = self.bufnr })
local _end
for i = curlnum + 1, count do
if inlevel >= fn.indent(i) then
_end = i - 1
break
end
end
_end = _end or count
buf_set_lines(self.bufnr, curlnum, _end, false, {})
buf_set_extmark(self.bufnr, ns, curlnum - 1, inlevel - 4, {
id = node.value.virtid,
virt_text = { { config.ui.expand, 'SagaToggle' } },
virt_text_pos = 'overlay',
})
slist.update_winline(node, -(_end - curlnum))
node.value.expand = false
api.nvim_set_option_value('modifiable', false, { buf = self.bufnr })
return
end
if node.value.expand == false then
node.value.expand = true
api.nvim_set_option_value('modifiable', true, { buf = self.bufnr })
buf_set_extmark(self.bufnr, ns, curlnum - 1, inlevel - 4, {
id = node.value.virtid,
virt_text = { { config.ui.collapse, 'SagaToggle' } },
virt_text_pos = 'overlay',
})
self:collapse(node, curlnum)
api.nvim_set_option_value('modifiable', false, { buf = self.bufnr })
return
end
api.nvim_win_close(self.winid, true)
local wins = fn.win_findbuf(self.main_buf)
api.nvim_win_set_cursor(
wins[#wins],
{ node.value.selectionRange.start.line + 1, node.value.selectionRange.start.character }
)
local width = #api.nvim_get_current_line()
beacon({ node.value.selectionRange.start.line, 0 }, width)
if config.outline.close_after_jump then
clean_ctx()
end
end
function ot:create_preview_win(lines)
local winid = fn.bufwinid(self.main_buf)
local origianl_win_height = api.nvim_win_get_height(winid)
local original_win_width = api.nvim_win_get_width(winid)
local max_height = bit.rshift(origianl_win_height, 2)
local max_width = bit.rshift(original_win_width, 2)
local float_opt = {
relative = 'editor',
style = 'minimal',
height = math.min(max_height, #lines),
width = math.min(max_width, util.get_max_content_length(lines)),
focusable = false,
noautocmd = true,
}
if outline_conf.win_position == 'right' then
float_opt.anchor = 'NE'
float_opt.col = vim.o.columns - outline_conf.win_width - 1
float_opt.row = fn.winline() + 2
float_opt.row = fn.winline()
else
float_opt.anchor = 'NW'
float_opt.col = outline_conf.win_width + 1
float_opt.row = fn.winline()
end
self.preview_bufnr, self.preview_winid = win
:new_float(float_opt, false, true)
:setlines(lines)
:bufopt({
['bufhidden'] = 'wipe',
['filetype'] = vim.bo[self.main_buf].filetype,
['buftype'] = 'nofile',
})
:winopt({
['winhl'] = 'NormalFloat:SagaNormal,FloatBorder:SagaBorder',
['sidescrolloff'] = 5,
})
:wininfo()
end
function ot:refresh(group)
api.nvim_create_autocmd('User', {
group = group,
pattern = 'SagaSymbolUpdate',
callback = function(args)
if
not self.bufnr
or not api.nvim_buf_is_valid(self.bufnr)
or api.nvim_get_current_buf() ~= args.data.bufnr
then
return
end
api.nvim_set_option_value('modifiable', true, { buf = self.bufnr })
self:parse(args.data.symbols)
end,
})
end
function ot:preview(group)
api.nvim_create_autocmd('CursorMoved', {
group = group,
buffer = self.bufnr,
callback = function()
local curlnum = unpack(api.nvim_win_get_cursor(self.winid))
local node = slist.find_node(self.list, curlnum)
if not node then
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
api.nvim_win_close(self.preview_winid, true)
self.preview_winid = nil
self.preview_bufnr = nil
end
return
end
local range = node.value.range
local lines =
api.nvim_buf_get_lines(self.main_buf, range.start.line, range['end'].line + 1, false)
if not self.preview_winid or not api.nvim_win_is_valid(self.preview_winid) then
self:create_preview_win(lines)
return
end
api.nvim_buf_set_lines(self.preview_bufnr, 0, -1, false, lines)
local win_conf = api.nvim_win_get_config(self.preview_winid)
win_conf.row = fn.winline() - 1
win_conf.height = math.min(#lines, bit.rshift(vim.o.lines, 1))
api.nvim_win_set_config(self.preview_winid, win_conf)
end,
})
api.nvim_create_autocmd('BufLeave', {
buffer = self.bufnr,
callback = function()
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
api.nvim_win_close(self.preview_winid, true)
self.preview_winid = nil
self.preview_bufnr = nil
end
end,
})
end
function ot:auto_close(group)
api.nvim_create_autocmd('WinEnter', {
group = group,
callback = function()
if api.nvim_get_current_win() == self.winid and #api.nvim_list_wins() == 1 then
api.nvim_win_set_buf(self.winid, api.nvim_create_buf(true, true))
api.nvim_del_augroup_by_id(group)
clean_ctx()
end
end,
desc = '[Lspsaga] auto close the outline window when is last',
})
end
function ot:clean_after_close()
api.nvim_create_autocmd('BufDelete', {
buffer = self.bufnr,
callback = function(args)
if args.buf == self.bufnr then
clean_ctx()
end
end,
})
end
function ot:outline(buf)
if self.winid and api.nvim_win_is_valid(self.winid) then
api.nvim_win_close(self.winid, true)
clean_ctx()
return
end
self.main_buf = buf or api.nvim_get_current_buf()
local res = symbol:get_buf_symbols(self.main_buf)
if not res or not res.symbols or #res.symbols == 0 then
vim.inspect(
'[lspsaga] get symbols failed server may not initialed try again later',
vim.log.levels.INFO
)
return
end
if not self.winid or not api.nvim_win_is_valid(self.winid) then
self.bufnr, self.winid = create_outline_window()
end
self:parse(res.symbols)
util.map_keys(self.bufnr, config.outline.keys.toggle_or_jump, function()
self:expand_or_jump()
end)
util.map_keys(self.bufnr, config.outline.keys.quit, function()
api.nvim_win_close(self.winid, true)
clean_ctx()
end)
local group = api.nvim_create_augroup('outline', { clear = true })
self:clean_after_close()
self:refresh(group)
if config.outline.auto_preview then
self:preview(group)
end
if outline_conf.auto_close then
self:auto_close(group)
end
end
return setmetatable(ctx, ot)

View file

@ -0,0 +1,221 @@
local api = vim.api
local config = require('lspsaga').config.symbol_in_winbar
local ui = require('lspsaga').config.ui
local util = require('lspsaga.util')
local symbol = require('lspsaga.symbol')
local kind = require('lspsaga.lspkind').kind
local function bar_prefix()
return {
prefix = '%#Saga',
sep = '%#SagaWinbarSep#' .. config.separator .. '%*',
}
end
local function path_in_bar(buf)
local ft = vim.bo[buf].filetype
local icon, hl
if ui.devicon then
icon, hl = util.icon_from_devicon(ft)
end
local bar = bar_prefix()
local items = {}
local folder = kind[302][2] .. '%*'
for item in util.path_itera(buf) do
item = #items == 0
and '%#' .. (hl or 'SagaFileIcon') .. '#' .. (icon .. ' ' or '') .. '%*' .. bar.prefix .. 'FileName#' .. item .. '%*'
or bar.prefix .. 'Folder#' .. folder .. bar.prefix .. 'FolderName#' .. item .. '%*'
items[#items + 1] = item
if #items > config.folder_level then
break
end
end
local barstr = ''
for i = #items, 1, -1 do
barstr = barstr .. items[i] .. (i > 1 and bar.sep or '')
end
return barstr
end
--@private
local function binary_search(tbl, line)
local left = 1
local right = #tbl
local mid = 0
while true do
mid = bit.rshift(left + right, 1)
if not tbl[mid] then
return
end
local range = tbl[mid].range or tbl[mid].location.range
if not range then
return
end
if line >= range.start.line and line <= range['end'].line then
return mid
elseif line < range.start.line then
right = mid - 1
else
left = mid + 1
end
if left > right then
return
end
end
end
local function stl_escape(str)
return str:gsub('%%', '')
end
local function insert_elements(buf, node, elements)
if config.hide_keyword and symbol:node_is_keyword(buf, node) then
return
end
local type = kind[node.kind][1]
local icon = kind[node.kind][2]
local bar = bar_prefix()
if node.name:find('%%') then
node.name = stl_escape(node.name)
end
if config.color_mode then
local node_context = bar.prefix .. type .. '#' .. icon .. node.name
elements[#elements + 1] = node_context
else
local node_context = bar.prefix
.. type
.. '#'
.. icon
.. bar.prefix
.. 'Word'
.. '#'
.. node.name
elements[#elements + 1] = node_context
end
end
--@private
local function find_in_node(buf, tbl, line, elements)
local mid = binary_search(tbl, line)
if not mid then
return
end
local node = tbl[mid]
insert_elements(buf, tbl[mid], elements)
if node.children ~= nil and next(node.children) ~= nil then
find_in_node(buf, node.children, line, elements)
end
end
--@private
local function render_symbol_winbar(buf, symbols)
if api.nvim_get_current_buf() ~= buf then
return
end
-- don't show in float window.
local cur_win = api.nvim_get_current_win()
local winconf = api.nvim_win_get_config(cur_win)
if #winconf.relative > 0 then
return
end
local current_line = api.nvim_win_get_cursor(cur_win)[1]
local winbar_str = config.show_file and path_in_bar(buf) or ''
local winbar_elements = {}
find_in_node(buf, symbols, current_line - 1, winbar_elements)
local lens, over_idx = 0, 0
local max_width = math.floor(api.nvim_win_get_width(cur_win) * 0.9)
for i, item in pairs(winbar_elements) do
local s = vim.split(item, '#')
lens = lens + api.nvim_strwidth(s[3]) + api.nvim_strwidth(config.separator)
if lens > max_width then
over_idx = i
lens = 0
end
end
if over_idx > 0 then
winbar_elements = { unpack(winbar_elements, over_idx) }
table.insert(winbar_elements, 1, '...')
end
local bar = bar_prefix()
local str = table.concat(winbar_elements, bar.sep)
if config.show_file and next(winbar_elements) ~= nil then
str = bar.sep .. str
end
winbar_str = winbar_str .. str
if config.enable and api.nvim_win_get_height(cur_win) - 1 > 1 then
if #winbar_str == 0 then
winbar_str = bar_prefix().prefix .. ' #'
end
api.nvim_set_option_value('winbar', winbar_str, { scope = 'local', win = cur_win })
end
return winbar_str
end
local function file_bar(buf)
local winid = api.nvim_get_current_win()
if config.show_file then
api.nvim_set_option_value('winbar', path_in_bar(buf), { scope = 'local', win = winid })
else
api.nvim_set_option_value(
'winbar',
bar_prefix().prefix .. ' #',
{ scope = 'local', win = winid }
)
end
end
local function init_winbar(buf)
api.nvim_create_autocmd('User', {
pattern = 'SagaSymbolUpdate',
callback = function(opt)
local curbuf = api.nvim_get_current_buf()
if
vim.bo[opt.buf].buftype == 'nofile'
or curbuf ~= opt.data.bufnr
or #opt.data.symbols == 0
then
return
end
render_symbol_winbar(opt.buf, opt.data.symbols)
end,
desc = 'Lspsaga get and show symbols',
})
api.nvim_create_autocmd({ 'CursorMoved' }, {
buffer = buf,
callback = function()
local res = symbol:get_buf_symbols(buf)
if res and res.symbols then
render_symbol_winbar(buf, res.symbols)
end
end,
desc = 'Lspsaga symbols render and request',
})
end
return {
init_winbar = init_winbar,
file_bar = file_bar,
}

View file

@ -1,474 +0,0 @@
local lsp, api = vim.lsp, vim.api
local config = require('lspsaga').config.symbol_in_winbar
local libs = require('lspsaga.libs')
local symbar = {}
local cache = {}
symbar.__index = symbar
function symbar.__newindex(t, k, v)
rawset(t, k, v)
end
local function bar_prefix()
return {
prefix = '%#SagaWinbar',
sep = '%#SagaWinbarSep#' .. config.separator .. '%*',
}
end
local function get_kind_icon(type, index)
local kind = require('lspsaga.lspkind').get_kind()
return kind[type][index]
end
local function respect_lsp_root(buf)
local clients = lsp.get_active_clients({ bufnr = buf })
if #clients == 0 then
return
end
local root_dir = clients[1].config.root_dir
local bufname = api.nvim_buf_get_name(buf)
local bufname_parts = vim.split(bufname, libs.path_sep, { trimempty = true })
if not root_dir then
return { #bufname_parts }
end
local parts = vim.split(root_dir, libs.path_sep, { trimempty = true })
return { unpack(bufname_parts, #parts + 1) }
end
local function bar_file_name(buf)
local res
if config.respect_root then
res = respect_lsp_root(buf)
end
--fallback to config.folder_level
if not res then
res = libs.get_path_info(buf, config.folder_level)
end
if not res or #res == 0 then
return
end
local data = libs.icon_from_devicon(vim.bo[buf].filetype, true)
local bar = bar_prefix()
local items = {}
for i, v in pairs(res) do
if i == #res then
if #data > 0 then
items[#items + 1] = '%#SagaWinbarFileIcon#' .. data[1] .. '%*'
local ok, conf = pcall(api.nvim_get_hl_by_name, 'SagaWinbarFileIcon', true)
if not ok then
conf = {}
end
for k, _ in pairs(conf) do
if type(k) ~= 'string' then
conf[k] = nil
end
end
api.nvim_set_hl(
0,
'SagaWinbarFileIcon',
vim.tbl_extend('force', conf, {
foreground = data[2],
})
)
end
items[#items + 1] = bar.prefix .. 'FileName#' .. v .. '%*'
else
items[#items + 1] = bar.prefix
.. 'Folder#'
.. get_kind_icon(302, 2)
.. '%*'
.. bar.prefix
.. 'FolderName'
.. '#'
.. v
.. '%*'
.. bar.sep
end
end
return table.concat(items, '')
end
local function get_node_range(node)
if node.location then
return node.location.range
end
if node.range then
return node.range
end
return nil
end
--@private
local function binary_search(tbl, line)
local left = 1
local right = #tbl
local mid = 0
while true do
mid = bit.rshift(left + right, 1)
if mid == 0 then
return nil
end
local range = get_node_range(tbl[mid])
if not range then
return nil
end
if line >= range.start.line and line <= range['end'].line then
return mid
elseif line < range.start.line then
right = mid - 1
if left > right then
return nil
end
else
left = mid + 1
if left > right then
return nil
end
end
end
end
function symbar.node_is_keyword(buf, node)
if not node.selectionRange then
return false
end
if not vim.treesitter.get_captures_at_pos then
return false
end
local captures = vim.treesitter.get_captures_at_pos(
buf,
node.selectionRange.start.line,
node.selectionRange.start.character
)
for _, v in pairs(captures) do
if v.capture == 'keyword' or v.capture == 'conditional' or v.capture == 'repeat' then
return true
end
end
return false
end
local function stl_escape(str)
return str:gsub('%%', '')
end
local function insert_elements(buf, node, elements)
if config.hide_keyword and symbar.node_is_keyword(buf, node) then
return
end
local type = get_kind_icon(node.kind, 1)
local icon = get_kind_icon(node.kind, 2)
local bar = bar_prefix()
if node.name:find('%%') then
node.name = stl_escape(node.name)
end
if config.color_mode then
local node_context = bar.prefix .. type .. '#' .. icon .. node.name
elements[#elements + 1] = node_context
else
local node_context = bar.prefix
.. type
.. '#'
.. icon
.. bar.prefix
.. 'Word'
.. '#'
.. node.name
elements[#elements + 1] = node_context
end
end
--@private
local function find_in_node(buf, tbl, line, elements)
local mid = binary_search(tbl, line)
if mid == nil then
return
end
local node = tbl[mid]
local range = get_node_range(tbl[mid]) or {}
if mid > 1 then
for i = 1, mid - 1 do
local prev_range = get_node_range(tbl[i]) or {}
if -- not sure there should be 6 or other kind can be used in here
tbl[i].kind == 6
and range.start.line > prev_range.start.line
and range['end'].line <= prev_range['end'].line
then
insert_elements(buf, tbl[i], elements)
end
end
end
insert_elements(buf, tbl[mid], elements)
if node.children ~= nil and next(node.children) ~= nil then
find_in_node(buf, node.children, line, elements)
end
end
--@private
local render_symbol_winbar = function(buf, symbols)
local cur_buf = api.nvim_get_current_buf()
if cur_buf ~= buf then
return
end
-- don't show in float window.
local cur_win = api.nvim_get_current_win()
local winconf = api.nvim_win_get_config(cur_win)
if #winconf.relative > 0 then
return
end
local current_line = api.nvim_win_get_cursor(cur_win)[1]
local winbar_str = config.show_file and bar_file_name(buf) or ''
local winbar_elements = {}
find_in_node(buf, symbols, current_line - 1, winbar_elements)
local lens, over_idx = 0, 0
local max_width = math.floor(api.nvim_win_get_width(cur_win) * 0.9)
for i, item in pairs(winbar_elements) do
local s = vim.split(item, '#')
lens = lens + api.nvim_strwidth(s[3]) + api.nvim_strwidth(config.separator)
if lens > max_width then
over_idx = i
lens = 0
end
end
if over_idx > 0 then
winbar_elements = { unpack(winbar_elements, over_idx) }
table.insert(winbar_elements, 1, '...')
end
local bar = bar_prefix()
local str = table.concat(winbar_elements, bar.sep)
if config.show_file and next(winbar_elements) ~= nil then
str = bar.sep .. str
end
winbar_str = winbar_str .. str
if config.enable and api.nvim_win_get_height(cur_win) - 1 > 1 then
if #winbar_str == 0 then
winbar_str = bar_prefix().prefix .. ' #'
end
api.nvim_set_option_value('winbar', winbar_str, { scope = 'local', win = cur_win })
end
return winbar_str
end
--- Get buffer symbols from cache
---@private
local function get_buf_symbol(buf)
buf = buf or api.nvim_get_current_buf()
local res = {}
if not cache[buf] then
return res
end
if cache[buf].pending_request then
res.pending_request = cache[buf].pending_request
return res
end
res.symbols = cache[buf].symbols
res.pending_request = cache[buf].pending_request
return res
end
function symbar:do_symbol_request(buf, callback)
local params = { textDocument = lsp.util.make_text_document_params() }
local client = libs.get_client_by_cap('documentSymbolProvider')
if not client then
return
end
self[buf].pending_request = true
client.request('textDocument/documentSymbol', params, callback, buf)
end
function symbar:refresh_symbol_cache(buf, render_fn)
local function handler(_, result, ctx)
if api.nvim_get_current_buf() ~= buf or not self[ctx.bufnr] then
return
end
self[ctx.bufnr].pending_request = false
if not result then
return
end
if render_fn then
render_fn(buf, result)
end
self[ctx.bufnr].symbols = result
end
self:do_symbol_request(buf, handler)
end
function symbar:init_buf_symbols(buf, render_fn)
local res = get_buf_symbol(buf)
if res.pending_request then
return
end
if not res.symbols or (next(res.symbols) == nil and not res.pending_request) then
self:refresh_symbol_cache(buf, render_fn)
else
render_fn(buf, res.symbols)
end
end
local function clean_buf_cache(buf)
buf = buf or api.nvim_get_current_buf()
if buf and cache[buf] then
for k, _ in pairs(cache[buf]) do
cache[buf][k] = nil
end
cache[buf] = nil
end
end
function symbar:register_events(buf)
local augroup = api.nvim_create_augroup('LspsagaSymbol' .. tostring(buf), { clear = true })
self[buf].group = augroup
api.nvim_create_autocmd('CursorMoved', {
group = augroup,
buffer = buf,
callback = function()
self:init_buf_symbols(buf, render_symbol_winbar)
end,
desc = 'Lspsaga symbols render and request',
})
api.nvim_create_autocmd('InsertLeave', {
group = augroup,
buffer = buf,
callback = function()
if not config.enable then
self:refresh_symbol_cache(buf, render_symbol_winbar)
else
self:refresh_symbol_cache(buf)
end
end,
desc = 'Lspsaga update symbols',
})
api.nvim_buf_attach(buf, false, {
on_detach = function()
if self[buf] and self[buf].group then
pcall(api.nvim_del_augroup_by_id, self[buf].group)
end
clean_buf_cache(buf)
end,
})
end
local function match_ignore(buf)
local fname = api.nvim_buf_get_name(buf)
for _, pattern in pairs(config.ignore_patterns) do
if fname:find(pattern) then
return true
end
end
return false
end
function symbar:symbol_autocmd()
api.nvim_create_autocmd('LspAttach', {
group = api.nvim_create_augroup('LspsagaSymbols', { clear = false }),
callback = function(opt)
if vim.bo[opt.buf].buftype == 'nofile' then
return
end
local winid = api.nvim_get_current_win()
if api.nvim_get_current_buf() ~= opt.buf then
return
end
local ok, _ = pcall(api.nvim_win_get_var, winid, 'disable_winbar')
if ok then
return
end
if config.show_file then
api.nvim_set_option_value(
'winbar',
bar_file_name(opt.buf),
{ scope = 'local', win = winid }
)
else
api.nvim_set_option_value(
'winbar',
bar_prefix().prefix .. ' #',
{ scope = 'local', win = winid }
)
end
--ignored after folder file prefix set
if match_ignore(opt.buf) then
return
end
if not self[opt.buf] then
self[opt.buf] = {}
end
self:init_buf_symbols(opt.buf, render_symbol_winbar)
self:register_events(opt.buf)
end,
desc = 'Lspsaga get and show symbols',
})
end
---Get buffer symbols
---@return string | nil
function symbar:get_winbar()
local buf = api.nvim_get_current_buf()
if not self[buf] then
self[buf] = {}
end
local res = get_buf_symbol(buf)
if vim.tbl_isempty(res) or not res.symbols then
self:refresh_symbol_cache(buf)
return
end
if res.pending_request then
return
end
self:register_events(buf)
if res.symbols then
return render_symbol_winbar(buf, res.symbols)
end
end
return setmetatable(cache, symbar)

View file

@ -1,17 +1,172 @@
local api, lsp = vim.api, vim.lsp
---@diagnostic disable-next-line: deprecated
local uv = vim.version().minor >= 10 and vim.uv or vim.loop
local M = {}
M.iswin = uv.os_uname().sysname:match('Windows')
M.ismac = uv.os_uname().sysname == 'Darwin'
M.path_sep = M.iswin and '\\' or '/'
function M.path_join(...)
return table.concat({ ... }, M.path_sep)
end
function M.path_itera(buf)
local parts = vim.split(api.nvim_buf_get_name(buf), M.path_sep, { trimempty = true })
local index = #parts + 1
return function()
index = index - 1
if index > 0 then
return parts[index]
end
end
end
function M.path_sub(fname, root)
root = (root and fname:sub(1, #root) == root) and root or vim.env.HOME
return fname:sub(#root + 2)
end
--get icon hlgroup color
function M.icon_from_devicon(ft)
local ok, devicons = pcall(require, 'nvim-web-devicons')
if not ok then
return ''
end
return devicons.get_icon_by_filetype(ft)
end
---get index from a list-like table
function M.tbl_index(tbl, val)
for index, v in ipairs(tbl) do
if v == val then
return index
end
end
end
-- get client by methods
function M.get_client_by_method(methods)
local clients = lsp.get_active_clients({ bufnr = 0 })
clients = vim.tbl_filter(function(client)
return client.name ~= 'null-ls'
end, clients)
local supports = {}
for _, client in ipairs(clients or {}) do
for _, method in ipairs(M.as_table(methods)) do
if client.supports_method(method) then
supports[#supports + 1] = client
end
end
end
return supports
end
local function feedkeys(key)
local k = api.nvim_replace_termcodes(key, true, false, true)
api.nvim_feedkeys(k, 'x', false)
end
function M.scroll_in_float(bufnr, winid)
local config = require('lspsaga').config
if not api.nvim_win_is_valid(winid) or not api.nvim_buf_is_valid(bufnr) then
return
end
for i, map in ipairs({ config.scroll_preview.scroll_down, config.scroll_preview.scroll_up }) do
M.map_keys(bufnr, map, function()
if api.nvim_win_is_valid(winid) then
api.nvim_win_call(winid, function()
local key = i == 1 and '<C-d>' or '<C-u>'
feedkeys(key)
end)
end
end)
end
end
function M.delete_scroll_map(bufnr)
local config = require('lspsaga').config
api.nvim_buf_del_keymap(bufnr, 'n', config.scroll_preview.scroll_down)
api.nvim_buf_del_keymap(bufnr, 'n', config.scroll_preview.scroll_up)
end
function M.gen_truncate_line(width)
return (''):rep(width)
end
function M.get_max_content_length(contents)
vim.validate({
contents = { contents, 't' },
})
local cells = {}
for _, v in pairs(contents) do
if v:find('\n.') then
local tbl = vim.split(v, '\n')
vim.tbl_map(function(s)
table.insert(cells, #s)
end, tbl)
else
table.insert(cells, #v)
end
end
table.sort(cells)
return cells[#cells]
end
function M.close_win(winid)
for _, id in ipairs(M.as_table(winid)) do
if api.nvim_win_is_valid(id) then
api.nvim_win_close(id, true)
end
end
end
function M.get_max_float_width(percent)
percent = percent or 0.6
return math.floor(vim.o.columns * percent)
end
function M.win_height_increase(content, percent)
local increase = 0
local max_width = M.get_max_float_width(percent)
local max_len = M.get_max_content_length(content)
local new = {}
for _, v in pairs(content) do
if v:find('\n.') then
vim.list_extend(new, vim.split(v, '\n'))
else
new[#new + 1] = v
end
end
if max_len > max_width then
vim.tbl_map(function(s)
local cols = vim.fn.strdisplaywidth(s)
if cols > max_width then
increase = increase + math.floor(cols / max_width)
end
end, new)
end
return increase
end
function M.as_table(value)
return type(value) == 'string' and { value } or value
return type(value) ~= 'table' and { value } or value
end
--- Creates a buffer local mapping.
---@param buffer number
---@param modes string|table<string>
---@param keys string|table<string>
---@param rhs string|function
---@param opts? table
function M.map_keys(buffer, modes, keys, rhs, opts)
---@param modes string|table<string>|nil
---@param opts table|nil
function M.map_keys(buffer, keys, rhs, modes, opts)
opts = opts or {}
opts.nowait = true
opts.noremap = true
modes = modes or 'n'
if type(rhs) == 'function' then
opts.callback = rhs
@ -20,9 +175,22 @@ function M.map_keys(buffer, modes, keys, rhs, opts)
for _, mode in ipairs(M.as_table(modes)) do
for _, lhs in ipairs(M.as_table(keys)) do
vim.api.nvim_buf_set_keymap(buffer, mode, lhs, rhs, opts)
api.nvim_buf_set_keymap(buffer, mode, lhs, rhs, opts)
end
end
end
function M.res_isempty(results)
-- handle {{}}
if vim.tbl_isempty(results) then
return true
end
for i, res in ipairs(results) do
if res.result and #res.result > 0 then
return false
end
end
return true
end
return M

View file

@ -1,103 +1,9 @@
local vim, api, lsp = vim, vim.api, vim.lsp
local M = {}
local vim, api = vim, vim.api
local validate = vim.validate
local ui = require('lspsaga').config.ui
local win = {}
function M.border_chars()
return {
lefttop = {
['single'] = '',
['double'] = '',
['rounded'] = '',
['solid'] = ' ',
['shadow'] = '',
},
top = {
['single'] = '',
['double'] = '',
['rounded'] = '',
['solid'] = ' ',
['shadow'] = '',
},
righttop = {
['single'] = '',
['double'] = '',
['rounded'] = '',
['solid'] = ' ',
['shadow'] = ' ',
},
right = {
['single'] = '',
['double'] = '',
['rounded'] = '',
['solid'] = ' ',
['shadow'] = ' ',
},
rightbottom = {
['single'] = '',
['double'] = '',
['rounded'] = '',
['solid'] = ' ',
['shadow'] = ' ',
},
bottom = {
['single'] = '',
['double'] = '',
['rounded'] = '',
['solid'] = ' ',
['shadow'] = ' ',
},
leftbottom = {
['single'] = '',
['double'] = '',
['rounded'] = '',
['solid'] = ' ',
['shadow'] = ' ',
},
left = {
['single'] = '',
['double'] = '',
['rounded'] = '',
['solid'] = ' ',
['shadow'] = '',
},
}
end
function M.combine_char()
return {
['top'] = {
['single'] = '',
['rounded'] = '',
['double'] = '',
['solid'] = ' ',
},
['bottom'] = {
['single'] = '',
['rounded'] = '',
['double'] = '',
['solid'] = ' ',
},
}
end
function M.combine_border(style, side, hi)
local border_chars = M.border_chars()
local order =
{ 'lefttop', 'top', 'righttop', 'right', 'rightbottom', 'bottom', 'leftbottom', 'left' }
local res = {}
for _, pos in ipairs(order) do
if not vim.tbl_isempty(side) and vim.tbl_contains(vim.tbl_keys(side), pos) then
res[#res + 1] = { side[pos], hi }
else
res[#res + 1] = { border_chars[pos][style], hi }
end
end
return res
end
local function make_floating_popup_options(width, height, opts)
local function make_floating_popup_options(opts)
vim.validate({
opts = { opts, 't', true },
})
@ -106,272 +12,167 @@ local function make_floating_popup_options(width, height, opts)
['opts.offset_x'] = { opts.offset_x, 'n', true },
['opts.offset_y'] = { opts.offset_y, 'n', true },
})
local new_option = {}
new_option.style = opts.style or 'minimal'
new_option.width = width
new_option.height = height
local anchor = ''
local row, col
if opts.focusable ~= nil then
new_option.focusable = opts.focusable
end
local lines_above = opts.relative == 'mouse' and vim.fn.getmousepos().line - 1
or vim.fn.winline() - 1
local lines_below = vim.fn.winheight(0) - lines_above
new_option.noautocmd = opts.noautocmd or true
new_option.relative = opts.relative and opts.relative or 'cursor'
new_option.anchor = opts.anchor or nil
if new_option.relative == 'win' then
new_option.bufpos = opts.bufpos or nil
new_option.win = opts.win or nil
end
if opts.title then
new_option.title = opts.title
new_option.title_pos = opts.title_pos or 'center'
end
new_option.zindex = opts.zindex or nil
if not opts.row and not opts.col and not opts.bufpos then
local lines_above = vim.fn.winline() - 1
local lines_below = vim.fn.winheight(0) - lines_above
new_option.anchor = ''
local pum_pos = vim.fn.pum_getpos()
local pum_vis = not vim.tbl_isempty(pum_pos) -- pumvisible() can be true and pum_pos() returns {}
if pum_vis and vim.fn.line('.') >= pum_pos.row or not pum_vis and lines_above < lines_below then
new_option.anchor = 'N'
new_option.row = 1
else
new_option.anchor = 'S'
new_option.row = 0
end
if vim.fn.wincol() + width <= vim.o.columns then
new_option.anchor = new_option.anchor .. 'W'
new_option.col = 0
else
new_option.anchor = new_option.anchor .. 'E'
new_option.col = 1
end
if lines_above < lines_below then
anchor = anchor .. 'N'
opts.height = math.min(lines_below, opts.height)
row = 1
else
new_option.row = opts.row
new_option.col = opts.col
anchor = anchor .. 'S'
opts.height = math.min(lines_above, opts.height)
row = 0
end
return new_option
end
local wincol = opts.relative == 'mouse' and vim.fn.getmousepos().column or vim.fn.wincol()
local function generate_win_opts(contents, opts)
opts = opts or {}
local win_width, win_height
if opts.no_size_override and opts.width and opts.height then
win_width, win_height = opts.width, opts.height
if wincol + opts.width + (opts.offset_x or 0) <= vim.o.columns then
anchor = anchor .. 'W'
col = 0
else
win_width, win_height = lsp.util._make_floating_popup_size(contents, opts)
anchor = anchor .. 'E'
col = 1
end
opts = make_floating_popup_options(win_width, win_height, opts)
return opts
end
local border = opts.border or ui.border
local function get_shadow_config()
local opts = {
relative = 'editor',
local title = (border and border ~= 'none' and opts.title) and opts.title or nil
local title_pos
if title then
title_pos = opts.title_pos or 'center'
end
return {
anchor = anchor,
bufpos = opts.relative == 'win' and opts.bufpos or nil,
col = col + (opts.offset_x or 0),
height = opts.height,
focusable = opts.focusable,
relative = opts.relative or 'cursor',
row = row + (opts.offset_y or 0),
style = 'minimal',
width = vim.o.columns,
height = vim.o.lines,
row = 0,
col = 0,
width = opts.width,
border = border,
zindex = opts.zindex or 50,
title = title,
title_pos = title_pos,
noautocmd = opts.noautocmd or false,
}
return opts
end
local function open_shadow_win()
local opts = get_shadow_config()
local shadow_winhl = 'Normal:SagaShadow'
local shadow_bufnr = api.nvim_create_buf(false, false)
local shadow_winid = api.nvim_open_win(shadow_bufnr, true, opts)
api.nvim_set_option_value('winhl', shadow_winhl, { scope = 'local', win = shadow_winid })
api.nvim_set_option_value('winblend', 70, { scope = 'local', win = shadow_winid })
api.nvim_set_option_value('bufhidden', 'wipe', { buf = shadow_bufnr })
return shadow_bufnr, shadow_winid
local function default()
return {
style = 'minimal',
border = ui.border,
noautocmd = false,
}
end
-- content_opts a table with filed
-- contents table type
-- filetype string type
-- enter boolean into window or not
-- highlight border highlight string type
function M.create_win_with_border(content_opts, opts)
local config = require('lspsaga').config
vim.validate({
content_opts = { content_opts, 't' },
contents = { content_opts.content, 't', true },
opts = { opts, 't', true },
})
local obj = {}
obj.__index = obj
local contents, filetype = content_opts.contents, content_opts.filetype
local enter = content_opts.enter or false
opts = opts or {}
opts = generate_win_opts(contents, opts)
local highlight = content_opts.highlight or {}
local normal = highlight.normal or 'LspNormal'
local border_hl = highlight.border or 'LspBorder'
if content_opts.noborder then
opts.border = 'none'
function obj:bufopt(name, value)
if type(name) == 'table' then
for key, val in pairs(name) do
api.nvim_set_option_value(key, val, { buf = self.bufnr })
end
else
opts.border = content_opts.border_side
and M.combine_border(config.ui.border, content_opts.border_side, border_hl)
or config.ui.border
api.nvim_set_option_value(name, value, { buf = self.bufnr })
end
return self
end
-- create contents buffer
local bufnr = content_opts.bufnr or api.nvim_create_buf(false, false)
-- buffer settings for contents buffer
-- Clean up input: trim empty lines from the end, pad
---@diagnostic disable-next-line: missing-parameter
local content = lsp.util._trim(contents)
if filetype then
api.nvim_buf_set_option(bufnr, 'filetype', filetype)
end
content = vim.tbl_flatten(vim.tbl_map(function(line)
if string.find(line, '\n') then
return vim.split(line, '\n')
function obj:winopt(name, value)
if type(name) == 'table' then
for key, val in pairs(name) do
api.nvim_set_option_value(key, val, { scope = 'local', win = self.winid })
end
return line
end, content))
if not vim.tbl_isempty(content) then
api.nvim_buf_set_lines(bufnr, 0, -1, true, content)
else
api.nvim_set_option_value(name, value, { scope = 'local', win = self.winid })
end
if not content_opts.bufnr then
api.nvim_set_option_value('modifiable', false, { buf = bufnr })
api.nvim_set_option_value('bufhidden', content_opts.bufhidden or 'wipe', { buf = bufnr })
api.nvim_set_option_value('buftype', content_opts.buftype or 'nofile', { buf = bufnr })
end
local winid = api.nvim_open_win(bufnr, enter, opts)
api.nvim_set_option_value(
'winblend',
content_opts.winblend or config.ui.winblend,
{ scope = 'local', win = winid }
)
api.nvim_set_option_value('wrap', content_opts.wrap or false, { scope = 'local', win = winid })
api.nvim_set_option_value(
'winhl',
'Normal:' .. normal .. ',FloatBorder:' .. border_hl,
{ scope = 'local', win = winid }
)
api.nvim_set_option_value('winbar', '', { scope = 'local', win = winid })
return bufnr, winid
return self
end
function M.open_shadow_float_win(content_opts, opts)
local shadow_bufnr, shadow_winid = open_shadow_win()
local contents_bufnr, contents_winid = M.create_win_with_border(content_opts, opts)
return contents_bufnr, contents_winid, shadow_bufnr, shadow_winid
end
function M.get_max_float_width(percent)
percent = percent or 0.6
return math.floor(vim.o.columns * percent)
end
function M.get_max_content_length(contents)
vim.validate({
contents = { contents, 't' },
function obj:winhl(normal, border)
api.nvim_set_option_value('winhl', 'Normal:' .. normal .. ',FloatBorder:' .. border, {
scope = 'local',
win = self.winid,
})
local cells = {}
for _, v in pairs(contents) do
if v:find('\n.') then
local tbl = vim.split(v, '\n')
vim.tbl_map(function(s)
table.insert(cells, #s)
end, tbl)
else
table.insert(cells, #v)
end
end
table.sort(cells)
return cells[#cells]
return self
end
function M.nvim_close_valid_window(winid)
if winid == nil then
return
end
local close_win = function(win_id)
if not winid or win_id == 0 then
return
end
if vim.api.nvim_win_is_valid(win_id) then
api.nvim_win_close(win_id, true)
end
end
local _switch = {
['table'] = function()
for _, id in ipairs(winid) do
close_win(id)
end
end,
['number'] = function()
close_win(winid)
end,
}
local _switch_metatable = {
__index = function(_, t)
error(string.format('Wrong type %s of winid', t))
end,
}
setmetatable(_switch, _switch_metatable)
_switch[type(winid)]()
function obj:wininfo()
return self.bufnr, self.winid
end
function M.nvim_win_try_close()
local has_var, line_diag_winids = pcall(api.nvim_win_get_var, 0, 'show_line_diag_winids')
if has_var and line_diag_winids ~= nil then
M.nvim_close_valid_window(line_diag_winids)
end
function obj:setlines(lines, row, erow)
row = row or 0
erow = erow or -1
api.nvim_buf_set_lines(self.bufnr, row, erow, false, lines)
return self
end
function M.win_height_increase(content, percent)
local increase = 0
local max_width = M.get_max_float_width(percent)
local max_len = M.get_max_content_length(content)
local new = {}
for _, v in pairs(content) do
if v:find('\n.') then
vim.list_extend(new, vim.split(v, '\n'))
else
new[#new + 1] = v
end
end
if max_len > max_width then
vim.tbl_map(function(s)
local cols = vim.fn.strdisplaywidth(s)
if cols > max_width then
increase = increase + math.floor(cols / max_width)
end
end, new)
end
return increase
--float window only
function obj:winsetconf(config)
validate({
config = { config, 't' },
})
api.nvim_win_set_config(self.winid, config)
return self
end
function M.restore_option()
--normal window only
function obj:setwidth(width)
api.nvim_win_set_width(self.winid, width)
return self
end
--normal window only
function obj:setheight(height)
api.nvim_win_set_height(self.winid, height)
return self
end
function win:new_float(float_opt, enter, force)
vim.validate({
float_opt = { float_opt, 't', true },
})
enter = enter or false
self.bufnr = float_opt.bufnr or api.nvim_create_buf(false, false)
float_opt.bufnr = nil
float_opt = not force and make_floating_popup_options(float_opt)
or vim.tbl_extend('force', default(), float_opt)
self.winid = api.nvim_open_win(self.bufnr, enter, float_opt)
return setmetatable(win, obj)
end
function win:new_normal(direct, bufnr)
local user_val = vim.opt.splitbelow
vim.opt.splitbelow = true
vim.cmd[direct]('new')
vim.opt.splitbelow = user_val
self.bufnr = bufnr or api.nvim_create_buf(false, false)
self.winid = api.nvim_get_current_win()
api.nvim_win_set_buf(self.winid, self.bufnr)
return setmetatable(win, obj)
end
function win:from_exist(bufnr, winid)
self.bufnr = bufnr
self.winid = winid
return setmetatable(win, obj)
end
function win:minimal_restore()
local minimal_opts = {
['number'] = vim.opt.number,
['relativenumber'] = vim.opt.relativenumber,
@ -383,21 +184,17 @@ function M.restore_option()
['signcolumn'] = vim.opt.signcolumn,
['colorcolumn'] = vim.opt.colorcolumn,
['fillchars'] = vim.opt.fillchars,
['statuscolumn'] = vim.opt.statuscolumn,
}
if vim.fn.has('nvim-0.9') == 1 then
minimal_opts['statuscolumn'] = vim.opt.statuscolumn
end
function minimal_opts.restore()
local restore = function()
for opt, val in pairs(minimal_opts) do
if type(val) ~= 'function' then
vim.opt[opt] = val
end
end
end
return minimal_opts
return restore
end
return M
return win

View file

@ -2,10 +2,10 @@ if vim.g.lspsaga_version then
return
end
vim.g.lspsaga_version = '0.2.9'
vim.g.lspsaga_version = '0.3.1'
vim.api.nvim_create_user_command('Lspsaga', function(args)
require('lspsaga.command').load_command(args.fargs[1], args.fargs[2])
require('lspsaga.command').load_command(args.fargs[1], vim.list_slice(args.fargs, 2))
end, {
range = true,
nargs = '+',
@ -16,7 +16,3 @@ end, {
end, list)
end,
})
vim.api.nvim_create_user_command('DiagnosticInsertEnable', function()
require('lspsaga.diagnostic'):on_insert()
end, {})

66
test/helper.lua Normal file
View file

@ -0,0 +1,66 @@
local function t(s)
return vim.api.nvim_replace_termcodes(s, true, true, true)
end
local function feedkey(key)
vim.api.nvim_feedkeys(t('a' .. key), 'x', false)
end
local function join_paths(...)
local result = table.concat({ ... }, '/')
return result
end
local function test_dir()
local data_path = vim.fn.stdpath('data')
local package_root = join_paths(data_path, 'test')
return os.getenv('SAGATEST') or package_root
end
local function treesitter_dep()
local package_root = test_dir()
local treesitter_path = join_paths(package_root, 'nvim-treesitter')
vim.opt.runtimepath:append(treesitter_path)
if vim.fn.isdirectory(treesitter_path) ~= 1 then
vim.fn.system({
'git',
'clone',
'https://github.com/nvim-treesitter/nvim-treesitter',
treesitter_path,
})
end
local parser_dir = join_paths(treesitter_path, 'parser')
require('nvim-treesitter').setup({
ensure_installed = { 'rust' },
sync_install = true,
highlight = { enable = true },
parser_install_dir = parser_dir,
})
if vim.fn.filereadable(join_paths(parser_dir, 'lua.so')) == 0 then
vim.cmd('TSInstallSync rust')
end
end
local function lspconfig_dep()
local package_root = test_dir()
local lspconfig_path = join_paths(package_root, 'nvim-lspconfig')
vim.opt.runtimepath:append(lspconfig_path)
if vim.fn.isdirectory(lspconfig_path) ~= 1 then
vim.fn.system({
'git',
'clone',
'https://github.com/neovim/nvim-lspconfig',
lspconfig_path,
})
end
local lspconfig = require('lspconfig')
lspconfig.lua_ls.setup({})
end
return {
test_dir = test_dir,
feedkey = feedkey,
treesitter_dep = treesitter_dep,
lspconfig_dep = lspconfig_dep,
join_paths = join_paths,
}

97
test/layout_spec.lua Normal file
View file

@ -0,0 +1,97 @@
require('lspsaga').setup({})
local eq = assert.is_equal
local ly = require('lspsaga.layout')
vim.opt.swapfile = false
describe('layout module', function()
local lbufnr, lwinid, rbufnr, rwinid
it('can create float layout', function()
lbufnr, lwinid, rbufnr, rwinid = ly:new('float'):left(20, 20):right():done()
assert.is_true(lbufnr ~= nil)
assert.is_true(lwinid ~= nil)
assert.is_true(rbufnr ~= nil)
assert.is_true(rwinid ~= nil)
end)
after_each(function()
for _, id in ipairs({ lwinid, rwinid }) do
if vim.api.nvim_win_is_valid(id) then
vim.api.nvim_win_close(id, true)
end
end
end)
it('can close float layout', function()
ly:close()
local wins = vim.api.nvim_list_wins()
assert.is_equal(1, #wins)
end)
it('can create normal layout', function()
lbufnr, lwinid, rbufnr, rwinid = ly:new('normal'):left(20, 20):right():done()
assert.is_true(lbufnr ~= nil)
assert.is_true(lwinid ~= nil)
assert.is_true(rbufnr ~= nil)
assert.is_true(rwinid ~= nil)
local wins = vim.api.nvim_list_wins()
local has_float = false
for _, win in ipairs(wins) do
local conf = vim.api.nvim_win_get_config(win)
if #conf.relative ~= 0 then
has_float = true
end
end
assert.is_false(has_float)
end)
it('can close normal layout', function()
ly:close()
local wins = vim.api.nvim_list_wins()
assert.is_equal(1, #wins)
end)
it('can set buffer options', function()
lbufnr, lwinid, rbufnr, rwinid = ly:new('float')
:left(20, 20)
:bufopt({
['filetype'] = 'lspsaga_test',
['buftype'] = 'nofile',
['bufhidden'] = 'wipe',
})
:right()
:bufopt({
['filetype'] = 'lspsaga_test',
['buftype'] = 'nofile',
['bufhidden'] = 'wipe',
})
:done()
eq('lspsaga_test', vim.api.nvim_get_option_value('filetype', { buf = lbufnr }))
eq('nofile', vim.api.nvim_get_option_value('buftype', { buf = lbufnr }))
eq('wipe', vim.api.nvim_get_option_value('bufhidden', { buf = lbufnr }))
--right
eq('lspsaga_test', vim.api.nvim_get_option_value('filetype', { buf = rbufnr }))
eq('nofile', vim.api.nvim_get_option_value('buftype', { buf = rbufnr }))
eq('wipe', vim.api.nvim_get_option_value('bufhidden', { buf = rbufnr }))
end)
it('can wipe out a wipe layout buffer', function()
ly:close()
assert.is_true(vim.api.nvim_buf_is_valid(lbufnr) == false)
assert.is_true(vim.api.nvim_buf_is_valid(rbufnr) == false)
end)
it('can set window local options', function()
lbufnr, lwinid, rbufnr, rwinid = ly:new('flaot')
:left(20, 20)
:winopt({
['number'] = false,
})
:right()
:done()
end)
assert.is_false(vim.api.nvim_get_option_value('number', { scope = 'local', win = lwinid }))
end)

103
test/slist_spec.lua Normal file
View file

@ -0,0 +1,103 @@
local eq, same = assert.equal, assert.same
local slist = require('lspsaga.slist')
describe('single linked list module ', function()
local list = slist.new()
it('can tail insert node to list', function()
slist.tail_push(
list,
{ name = 'test', range = { start = { line = 1, character = 1 } }, winline = 1 }
)
slist.tail_push(
list,
{ name = 'test2', range = { start = { line = 2, character = 2 } }, winline = 2 }
)
same({
value = {
name = 'test',
winline = 1,
range = {
start = {
line = 1,
character = 1,
},
},
},
next = {
value = {
name = 'test2',
winline = 2,
range = {
start = {
line = 2,
character = 2,
},
},
},
},
}, list)
end)
it('can insert node', function()
local node = slist.find_node(list, 1)
local tmp = {
name = 'test_insert',
winline = -1,
range = {
start = {
line = 2,
character = 2,
},
},
}
slist.insert_node(node, tmp)
same({
value = {
name = 'test_insert',
winline = -1,
range = {
start = {
line = 2,
character = 2,
},
},
},
next = {
value = {
name = 'test2',
winline = 2,
range = {
start = {
line = 2,
character = 2,
},
},
},
},
}, slist.find_node(list, -1))
end)
it('can find node', function()
local node = slist.find_node(list, 2)
same({
value = {
name = 'test2',
winline = 2,
range = {
start = {
line = 2,
character = 2,
},
},
},
}, node)
end)
it('can map all node', function()
local result = {}
slist.list_map(list, function(node)
result[#result + 1] = node.value.name
end)
same({ 'test', 'test_insert', 'test2' }, result)
end)
end)

58
test/util_spec.lua Normal file
View file

@ -0,0 +1,58 @@
local helper = require('test.helper')
local api = vim.api
local util = require('lspsaga.util')
local eq, is_true = assert.equal, assert.is_true
---util module unit test
describe('lspsaga util', function()
local bufnr
before_each(function()
bufnr = api.nvim_create_buf(true, false)
api.nvim_win_set_buf(0, bufnr)
end)
it('util.path_itera', function()
api.nvim_buf_set_name(bufnr, 'test.lua')
local result = {}
for part in util.path_itera(bufnr) do
result[#result + 1] = part
end
eq('test.lua', result[1])
end)
it('util.tbl_index', function()
local case = { 1, 2, 3, 8 }
eq(4, util.tbl_index(case, 8))
end)
it('util.close_win', function()
vim.cmd.split()
util.close_win(api.nvim_get_current_win())
assert.is_true(true, #api.nvim_list_wins() == 1)
end)
it('util.as_table', function()
assert.same({ 10 }, util.as_table(10))
assert.same({ 10 }, util.as_table({ 10 }))
end)
it('util.map_keys', function()
util.map_keys(bufnr, 'gq', function()
return '<Nop>'
end)
local maps = api.nvim_buf_get_keymap(bufnr, 'n')
local created = false
for _, item in ipairs(maps) do
if item.lhs == 'gq' then
created = true
break
end
end
is_true(true, created)
end)
it('util.res_isempty', function()
local client_results = { { result = {} } }
assert.is_true(util.res_isempty(client_results))
end)
end)

110
test/window_spec.lua Normal file
View file

@ -0,0 +1,110 @@
local api = vim.api
require('lspsaga').setup({})
local win = require('lspsaga.window')
local eq = assert.equal
vim.opt.swapfile = false
describe('window module', function()
local bufnr, winid
local float_opt = {
relative = 'editor',
row = 10,
col = 10,
border = 'single',
height = 10,
width = 10,
}
before_each(function()
if winid and api.nvim_win_is_valid(winid) then
api.nvim_win_close(winid, true)
end
pcall(api.nvim_delete_buf, bufnr, { force = true })
end)
it('can create float window', function()
assert.equal(1, #api.nvim_list_wins())
bufnr, winid = win:new_float(float_opt):wininfo()
eq(2, #api.nvim_list_wins())
end)
it('can create float window and enter float window', function()
bufnr, winid = win:new_float(float_opt, true):wininfo()
eq(winid, api.nvim_get_current_win())
end)
it('can set float window buffer options', function()
bufnr, winid = win:new_float(float_opt):bufopt('bufhidden', 'wipe'):wininfo()
eq('wipe', vim.bo[bufnr].bufhidden)
end)
it('can set float window buffer options in table param', function()
bufnr, winid = win
:new_float(float_opt)
:bufopt({
['bufhidden'] = 'wipe',
['filetype'] = 'saga_unitest',
})
:wininfo()
eq('wipe', vim.bo[bufnr].bufhidden)
eq('saga_unitest', vim.bo[bufnr].filetype)
end)
it('can set float window win-local options', function()
bufnr, winid = win:new_float(float_opt):winopt('number', true):wininfo()
assert.is_true(vim.wo[winid].number)
end)
it('can set float window win-local options by using table param', function()
bufnr, winid = win
:new_float(float_opt)
:winopt({
['number'] = true,
['signcolumn'] = 'no',
})
:wininfo()
assert.is_true(vim.wo[winid].number)
eq('no', vim.wo[winid].signcolumn)
end)
it('can create normal window', function()
bufnr, winid = win:new_normal('sp'):wininfo()
eq(2, #api.nvim_list_wins())
end)
it('can set normal win-local options', function()
bufnr, winid = win:new_normal('sp'):winopt('number', true):wininfo()
assert.is_true(vim.wo[winid].number)
end)
it('can restore options after close ', function()
vim.opt.number = true
vim.opt.swapfile = false
local restore = win:minimal_restore()
vim.cmd('enew')
bufnr = vim.api.nvim_get_current_buf()
local curwin = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(curwin, bufnr)
bufnr, winid = win
:new_float({
relative = 'editor',
row = 10,
col = 10,
height = 20,
width = 20,
style = 'minimal',
bufnr = bufnr,
}, true)
:wininfo()
vim.api.nvim_create_autocmd('WinClosed', {
callback = function()
restore()
end,
})
vim.api.nvim_win_close(winid, true)
vim.api.nvim_set_current_win(curwin)
assert.is_true(vim.api.nvim_get_option_value('number', { win = curwin }))
end)
end)