mirror of
https://github.com/nvimdev/lspsaga.nvim.git
synced 2024-09-16 14:24:08 +02:00
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:
parent
4f075452c4
commit
a17c975ac2
51 changed files with 5512 additions and 7722 deletions
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
@ -1,2 +1,3 @@
|
|||
github: glepnir
|
||||
open_collective: nvimdev
|
||||
custom: ['https://www.paypal.me/bobbyhub']
|
||||
|
|
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
67
Changelog.md
67
Changelog.md
|
@ -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
687
README.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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 Lspsaga’s Appearance|lspsaga.nvim-customizing-lspsaga’s-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-lspsaga’s-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 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 *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 won’t see a title.**
|
||||
|
||||
**If you are using Neovim 0.9 and want to disable the title, see
|
||||
|lspsaga.nvim-customizing-lspsaga’s-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 *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 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 })
|
||||
<
|
||||
|
||||
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 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",
|
||||
},
|
||||
},
|
||||
<
|
||||
|
||||
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 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.
|
||||
|
||||
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 LSPSAGA’S APPEARANCE*lspsaga.nvim-customizing-lspsaga’s-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 folder’s icon color, you could do this: `ui
|
||||
= { kind = { ["Folder"] = { " ", "@comment" }, }, },`.
|
||||
|
||||
|
||||
==============================================================================
|
||||
3. Donate *lspsaga.nvim-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://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
62
lua/lspsaga/beacon.lua
Normal 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,
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
379
lua/lspsaga/codeaction/init.lua
Normal file
379
lua/lspsaga/codeaction/init.lua
Normal 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)
|
127
lua/lspsaga/codeaction/lightbulb.lua
Normal file
127
lua/lspsaga/codeaction/lightbulb.lua
Normal 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,
|
||||
}
|
166
lua/lspsaga/codeaction/preview.lua
Normal file
166
lua/lspsaga/codeaction/preview.lua
Normal 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,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
454
lua/lspsaga/diagnostic/init.lua
Normal file
454
lua/lspsaga/diagnostic/init.lua
Normal 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)
|
339
lua/lspsaga/diagnostic/show.lua
Normal file
339
lua/lspsaga/diagnostic/show.lua
Normal 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)
|
45
lua/lspsaga/diagnostic/virt.lua
Normal file
45
lua/lspsaga/diagnostic/virt.lua
Normal 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,
|
||||
}
|
|
@ -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
146
lua/lspsaga/finder/box.lua
Normal 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
406
lua/lspsaga/finder/init.lua
Normal 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)
|
|
@ -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, {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(' ') then
|
||||
line = line:gsub(' ', 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
|
||||
|
|
205
lua/lspsaga/implement/init.lua
Normal file
205
lua/lspsaga/implement/init.lua
Normal 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,
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
68
lua/lspsaga/layout/float.lua
Normal file
68
lua/lspsaga/layout/float.lua
Normal 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
|
93
lua/lspsaga/layout/init.lua
Normal file
93
lua/lspsaga/layout/init.lua
Normal 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
|
31
lua/lspsaga/layout/normal.lua
Normal file
31
lua/lspsaga/layout/normal.lua
Normal 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
|
|
@ -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
|
|
@ -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
47
lua/lspsaga/logger.lua
Normal 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
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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
207
lua/lspsaga/rename/init.lua
Normal 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)
|
190
lua/lspsaga/rename/project.lua
Normal file
190
lua/lspsaga/rename/project.lua
Normal 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
|
|
@ -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
72
lua/lspsaga/slist.lua
Normal 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
199
lua/lspsaga/symbol/init.lua
Normal 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)
|
430
lua/lspsaga/symbol/outline.lua
Normal file
430
lua/lspsaga/symbol/outline.lua
Normal 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)
|
221
lua/lspsaga/symbol/winbar.lua
Normal file
221
lua/lspsaga/symbol/winbar.lua
Normal 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,
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
66
test/helper.lua
Normal 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
97
test/layout_spec.lua
Normal 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
103
test/slist_spec.lua
Normal 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
58
test/util_spec.lua
Normal 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
110
test/window_spec.lua
Normal 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)
|
Loading…
Reference in a new issue