├── .circleci ├── Dockerfile ├── README.md └── config.yml ├── .luacheckrc ├── .pre-commit-config.yaml ├── .stylua.toml ├── LICENSE ├── README.md ├── ftplugin ├── code-action-menu-details.vim ├── code-action-menu-diff.vim ├── code-action-menu-menu.vim ├── code-action-menu-warning-message.vim └── code-action-menu.vim ├── lua ├── code_action_menu.lua └── code_action_menu │ ├── enumerations │ ├── text_document_edit_status_enum.lua │ └── window_stack_direction_enum.lua │ ├── lsp_objects │ ├── actions │ │ ├── base_action.lua │ │ ├── code_action.lua │ │ └── command.lua │ └── edits │ │ ├── text_document_edit.lua │ │ └── workspace_edit.lua │ ├── utility_functions │ ├── actions.lua │ └── buffers.lua │ └── windows │ ├── anchor_window.lua │ ├── base_window.lua │ ├── details_window.lua │ ├── diff_window.lua │ ├── menu_window.lua │ ├── stacking_window.lua │ └── warning_message_window.lua ├── plugin └── code_action_menu.vim └── syntax ├── code-action-menu-details.vim ├── code-action-menu-diff.vim ├── code-action-menu-menu.vim └── code-action-menu-warning-message.vim /.circleci/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM archlinux/archlinux:base-devel-20220316.0.50542 2 | 3 | RUN pacman --sync --sysupgrade --refresh --noconfirm 4 | RUN pacman --sync --noconfirm git 5 | RUN useradd --create-home circleci 6 | RUN echo 'circleci ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers 7 | 8 | USER circleci 9 | 10 | RUN git clone https://aur.archlinux.org/pikaur.git /tmp/pikaur 11 | RUN cd /tmp/pikaur && makepkg --syncdeps --install --noconfirm && cd 12 | RUN pikaur --sync --noconfirm python-pre-commit luacheck stylua-bin 13 | 14 | WORKDIR /home/circleci 15 | -------------------------------------------------------------------------------- /.circleci/README.md: -------------------------------------------------------------------------------- 1 | # Continuous Integration Pipeline 2 | 3 | The continuous integration pipeline is responsible to verify the projects code 4 | base in a standardized environment. It runs for every open pull request as 5 | a required check to protect the `main` branch from unmature code. Typical jobs 6 | within this pipeline are linting, formatting, tests etc. 7 | 8 | ## Docker Image 9 | 10 | To save the pipeline a lot of time in getting started, there is a custom Docker 11 | image that includes all the packages that are necessary to run all jobs. Note 12 | that this must not include the installation of dependencies which are defines by 13 | the code of the repository itself. An exception to this are the `pre-commit` 14 | hooks which expect their binaries to be installed by the operation system 15 | (`language: system`). 16 | 17 | The configuration of the pipeline must point to a specific tagged version of the 18 | image. This makes sure that all concurrently open pull requests do still 19 | continue working with their referenced image versions. Only pull requests from 20 | branches which have been forked after the commit which changes the pipeline 21 | configuration will use the new version. Furthermore it must be guaranteed that 22 | any commit in the repositories history could be checked-out and the pipeline 23 | will produce the same result. Therefore the used environment must be 24 | deterministic. Each time there are changes to the Docker image, the tag version 25 | must be increased. 26 | 27 | The image can be simply build and updated like this: 28 | 29 | ```sh 30 | docker build --file="./.circleci/Dockerfile" --tag weilbith/ci-nvim-code-action-menu: . 31 | docker push weilbith/ci-nvim-code-action-menu: 32 | ``` 33 | 34 | Then adapt the version in `./.circleci/config.yaml` at the parameter section. 35 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | parameters: 4 | base_docker_image: 5 | type: string 6 | default: weilbith/ci-nvim-code-action-menu:0.0.2 7 | 8 | working_directory: 9 | type: string 10 | default: ~/repository 11 | 12 | anchor_1: &*attach_options 13 | at: << pipeline.parameters.working_directory >> 14 | 15 | executors: 16 | base_executor: 17 | docker: 18 | - image: << pipeline.parameters.base_docker_image >> 19 | working_directory: << pipeline.parameters.working_directory >> 20 | environment: 21 | PRE_COMMIT_HOME: << pipeline.parameters.working_directory >>/pre-commit 22 | 23 | commands: 24 | run_pre_commit_hook: 25 | parameters: 26 | hook_id: 27 | description: The identifier of the hook to execute 28 | type: string 29 | steps: 30 | - run: 31 | name: Run Pre-commit hook '<< parameters.hook_id >>' 32 | command: > 33 | pre-commit run 34 | --all-files --verbose --show-diff-on-failure 35 | << parameters.hook_id >> 36 | 37 | jobs: 38 | setup: 39 | executor: base_executor 40 | steps: 41 | - checkout 42 | - restore_cache: 43 | key: pre-commit-cache-v2-{{ checksum ".pre-commit-config.yaml" }} 44 | - run: 45 | name: Install pre-commit hooks 46 | command: pre-commit install-hooks 47 | - save_cache: 48 | key: pre-commit-cache-v2-{{ checksum ".pre-commit-config.yaml" }} 49 | paths: 50 | - << pipeline.parameters.working_directory >>/pre-commit 51 | - persist_to_workspace: 52 | root: << pipeline.parameters.working_directory >> 53 | paths: 54 | - ./* 55 | 56 | run_linters: 57 | executor: base_executor 58 | steps: 59 | - attach_workspace: **attach_options 60 | - run_pre_commit_hook: 61 | hook_id: luacheck 62 | - run_pre_commit_hook: 63 | hook_id: vint 64 | - run_pre_commit_hook: 65 | hook_id: check-added-large-files 66 | - run_pre_commit_hook: 67 | hook_id: detect-private-key 68 | 69 | check_formatting: 70 | executor: base_executor 71 | steps: 72 | - attach_workspace: **attach_options 73 | - run_pre_commit_hook: 74 | hook_id: stylua 75 | - run_pre_commit_hook: 76 | hook_id: end-of-file-fixer 77 | - run_pre_commit_hook: 78 | hook_id: trailing-whitespace 79 | 80 | find_fixup_commits: 81 | executor: base_executor 82 | steps: 83 | - attach_workspace: **attach_options 84 | - run: 85 | name: Verify that there are no fixup commits in the git history 86 | command: > 87 | exit $(git log --pretty=format:%s main.. | grep fixup! | wc --lines) 88 | 89 | workflows: 90 | version: 2 91 | default: 92 | jobs: 93 | - setup 94 | - run_linters: 95 | requires: 96 | - setup 97 | - check_formatting: 98 | requires: 99 | - setup 100 | - find_fixup_commits: 101 | requires: 102 | - setup 103 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | read_globals = { 2 | "vim" 3 | } 4 | 5 | ignore = { 6 | "212/self" 7 | } 8 | 9 | -- vim: filetype=lua 10 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: http://github.com/pre-commit/pre-commit-hooks 3 | rev: v3.1.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-merge-conflict 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - id: detect-private-key 10 | 11 | - repo: https://github.com/Calinou/pre-commit-luacheck 12 | rev: v1.0.0 13 | hooks: 14 | - id: luacheck 15 | 16 | - repo: local 17 | hooks: 18 | - id: stylua 19 | name: StyLua 20 | entry: stylua 21 | language: system 22 | files: ^.*\.lua$ 23 | 24 | - repo: https://github.com/syntaqx/git-hooks 25 | rev: v0.0.16 26 | hooks: 27 | - id: circleci-config-validate 28 | 29 | - repo: https://github.com/Vimjas/vint 30 | rev: v0.4a4 31 | hooks: 32 | - id: vint 33 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 80 2 | indent_type = "Spaces" 3 | indent_width = 2 4 | quote_style = "ForceSingle" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Thore Weilbier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NeoVim Code Action Menu 2 | 3 | > [!WARNING] 4 | > My apologies for archiving this repository. Though I guess it doesn't make 5 | > a difference because I left this plugin unmaintained for quite some time now. 6 | > I shared this plugin too early, it gathered attention too quickly and it was 7 | > just too unsophisticated. I failed to rewrite this plugin multiple times to a 8 | > state I like. Finally I got disappointed myself and just left it where is. 9 | > 10 | > By now there are other nice alternatives like support from 11 | > [fzf-lua](https://github.com/ibhagwan/fzf-lua) or 12 | > [actions-preview.nvim](https://github.com/aznhe21/actions-preview.nvim). So 13 | > I finally archive this repository now. It still works as before. But it makes 14 | > kinda official that it was a failed experiment. 15 | 16 | > **NOTE:** 17 | > This is still a beta version. Though it runs quite stable since some months, 18 | > it has not been tested with many language servers. Moreover there is a good 19 | > amount of documentation missing. 20 | 21 | ![gscreenshot_2021-09-28-094359](https://user-images.githubusercontent.com/12543647/135045142-dedfe9bb-01a0-4ed0-8d40-1dc4f2c2b254.png) 22 | 23 | This plugin provides a handy pop-up menu for code actions. Its key competence is 24 | to provide the user with more detailed insights for each available code action. 25 | Such include meta data as well as a preview diff for certain types of actions. 26 | These includes: 27 | - descriptive title (usually shown by all code action "menu" solutions) 28 | - kind of the action (e.g. refactor, command, quickfix, organize imports, ...) 29 | - name of the action 30 | - if the action is the preferred one according to the server (preferred 31 | actions get automatically sorted to the top of the menu) 32 | - if the action is disabled (disabled actions can be used and get 33 | automatically sorted to the bottom of the menu with a special hightlight) 34 | - a preview of the changes this action will do in a diff view for all affected 35 | files (includes a diff count box visualization as GitHub pul requests do) 36 | - ...more will come, especially when servers start to use the `dataSupport` 37 | capability for the code action provider 38 | 39 | The experience for all these features might vary according to the implementation 40 | of the used language server. Especially for the diff view, do some servers still 41 | use the old code action data scheme from older Language Server Protocol versions 42 | which include less information for the clients. However this plugin tries to 43 | inspect those actions more deeply and try to parse as much information as 44 | possible. 45 | 46 | This plugin is just a menu for code actions. Nothing more and nothing less. It 47 | is a minimal plugin that focuses on one single task. It tries to be a contrast 48 | to other plugins which also provide a code action menu but being more advanced 49 | a do not provide any other functionality. 50 | 51 | ## Installation 52 | 53 | Install the plugin with your favorite manager tool. Here is an example using 54 | [packer.nvim](https://github.com/wbthomason/packer.nvim/issues): 55 | 56 | ```lua 57 | require('packer').use({ 58 | 'weilbith/nvim-code-action-menu', 59 | cmd = 'CodeActionMenu', 60 | }) 61 | ``` 62 | 63 | It is recommended to use the 64 | [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) plugin to attach 65 | language clients to your buffers. 66 | 67 | Furthermore is quite handy to also use the 68 | [nvim-lightbulb](https://github.com/kosayoda/nvim-lightbulb) plugin. It will 69 | automatically inform the user visually if any code actions are available for the 70 | current cursor location. 71 | 72 | Please note that LSP and other features for this plugin require you to have at 73 | least `v0.5.0` of NeoVim. 74 | 75 | ## Usage 76 | 77 | The plugin has a single command only: `CodeActionMenu` This command works in 78 | normal mode, as well as visual mode. For the latter mode it will switch to the 79 | code range mode automatically. 80 | The menu can be navigated as usual with `j` and `k`. The docked windows will 81 | always display further information for the currently selected action. Hitting 82 | `` will execute the currently selected code action. Alternatively you can 83 | type the number in front of the list entry to jump directly to this action and 84 | apply it right away. In any case the menu window will close itself. If you want 85 | to manually close the window without selecting an action, just hit `` or 86 | `q`. 87 | 88 | This plugin only supports nvim_lsp, if you are using coc.nvim, you can install 89 | [coc-code-action-menu.nvim](https://github.com/xiyaowong/coc-code-action-menu.nvim) 90 | to add support for coc.nvim. 91 | 92 | ## Customization 93 | 94 | The plugin allows for a bunch of customization. While there is no classic 95 | configuration (yet) it can be adapted by using traditional (Neo)Vim techniques. 96 | Each part of the menu uses its own filetype. This gets used to separate the 97 | logic implementation in Lua from other parts like highlighting, buffer options 98 | and mappings. As beautiful side effect is that the user can do so too. This 99 | means you can just define some 100 | `after/{ftplugin,syntax}/code-action-menu-{menu,details,diff,warning}.{vim,lua}` 101 | files yourself and get the full power of customization. 102 | 103 | At the current state of documentation I must redirect you to the syntax files in 104 | the source code of the plugin to get a list of available highlight groups. The 105 | user can simply overwrite any of the default mappings to his liking. 106 | 107 | ### Window Appearance 108 | 109 | The following global variables can be set to alternate the appearance of the 110 | windows: 111 | 112 | ```lua 113 | vim.g.code_action_menu_window_border = 'single' 114 | ``` 115 | 116 | ```vim 117 | let g:code_action_menu_window_border = 'single' 118 | ``` 119 | ### Disable parts of the UI 120 | 121 | The following global variables can be set to disable parts of the user 122 | interface: 123 | 124 | ```lua 125 | vim.g.code_action_menu_show_details = false 126 | vim.g.code_action_menu_show_diff = false 127 | vim.g.code_action_menu_show_action_kind = false 128 | ``` 129 | 130 | ```vim 131 | let g:code_action_menu_show_details = v:false 132 | let g:code_action_menu_show_diff = v:false 133 | let g:code_action_menu_show_action_kind = v:false 134 | ``` 135 | -------------------------------------------------------------------------------- /ftplugin/code-action-menu-details.vim: -------------------------------------------------------------------------------- 1 | runtime! ftplugin/code-action-menu.vim ftplugin/code-action-menu_*.vim ftplugin/code-action-menu/*.vim 2 | 3 | setlocal iskeyword+=: 4 | let b:undo_ftplugin .= ' iskeyword<' 5 | -------------------------------------------------------------------------------- /ftplugin/code-action-menu-diff.vim: -------------------------------------------------------------------------------- 1 | runtime! ftplugin/code-action-menu.vim ftplugin/code-action-menu_*.vim ftplugin/code-action-menu/*.vim 2 | -------------------------------------------------------------------------------- /ftplugin/code-action-menu-menu.vim: -------------------------------------------------------------------------------- 1 | runtime! ftplugin/code-action-menu.vim ftplugin/code-action-menu_*.vim ftplugin/code-action-menu/*.vim 2 | 3 | nnoremap lua require('code_action_menu').execute_selected_action() 4 | nnoremap 1 lua require('code_action_menu').select_line_and_execute_action(1) 5 | nnoremap 2 lua require('code_action_menu').select_line_and_execute_action(2) 6 | nnoremap 3 lua require('code_action_menu').select_line_and_execute_action(3) 7 | nnoremap 4 lua require('code_action_menu').select_line_and_execute_action(4) 8 | nnoremap 5 lua require('code_action_menu').select_line_and_execute_action(5) 9 | nnoremap 6 lua require('code_action_menu').select_line_and_execute_action(6) 10 | nnoremap 7 lua require('code_action_menu').select_line_and_execute_action(7) 11 | nnoremap 8 lua require('code_action_menu').select_line_and_execute_action(8) 12 | nnoremap 9 lua require('code_action_menu').select_line_and_execute_action(9) 13 | nnoremap lua require('code_action_menu').close_code_action_menu() 14 | nnoremap q lua require('code_action_menu').close_code_action_menu() 15 | 16 | " Fixes awkward visual selection (is it?) for range code actions. 17 | " Do this before the CursorMoved auto-command. 18 | call feedkeys('jk') 19 | 20 | augroup CodeActionMenuMenu 21 | autocmd! 22 | autocmd CursorMoved lua require('code_action_menu').update_selected_action() 23 | autocmd User CodeActionMenuWindowOpened ++once setlocal scrolloff=0 24 | autocmd User CodeActionMenuWindowOpened ++once setlocal cursorline 25 | autocmd User CodeActionMenuWindowOpened ++once setlocal winhighlight=CursorLine:CodeActionMenuMenuSelection 26 | augroup END 27 | -------------------------------------------------------------------------------- /ftplugin/code-action-menu-warning-message.vim: -------------------------------------------------------------------------------- 1 | runtime! ftplugin/code-action-menu.vim ftplugin/code-action-menu_*.vim ftplugin/code-action-menu/*.vim 2 | -------------------------------------------------------------------------------- /ftplugin/code-action-menu.vim: -------------------------------------------------------------------------------- 1 | let b:undo_ftplugin = 'setlocal' 2 | 3 | setlocal readonly 4 | let b:undo_ftplugin .= ' readonly<' 5 | 6 | setlocal nomodifiable 7 | let b:undo_ftplugin .= ' modifiable<' 8 | 9 | setlocal bufhidden=wipe 10 | let b:undo_ftplugin .= ' bufhidden<' 11 | -------------------------------------------------------------------------------- /lua/code_action_menu.lua: -------------------------------------------------------------------------------- 1 | local action_utils = require('code_action_menu.utility_functions.actions') 2 | local AnchorWindow = require('code_action_menu.windows.anchor_window') 3 | local MenuWindow = require('code_action_menu.windows.menu_window') 4 | local DetailsWindow = require('code_action_menu.windows.details_window') 5 | local DiffWindow = require('code_action_menu.windows.diff_window') 6 | local WarningMessageWindow = require( 7 | 'code_action_menu.windows.warning_message_window' 8 | ) 9 | 10 | local anchor_window_instance = nil 11 | local menu_window_instance = nil 12 | local details_window_instance = nil 13 | local diff_window_instance = nil 14 | local warning_message_window_instace = nil 15 | 16 | local function close_code_action_menu() 17 | anchor_window_instance = nil 18 | 19 | if details_window_instance ~= nil then 20 | details_window_instance:close() 21 | details_window_instance = nil 22 | end 23 | 24 | if diff_window_instance ~= nil then 25 | diff_window_instance:close() 26 | diff_window_instance = nil 27 | end 28 | 29 | if menu_window_instance ~= nil then 30 | menu_window_instance:close() 31 | menu_window_instance = nil 32 | end 33 | end 34 | 35 | local function close_warning_message_window() 36 | if warning_message_window_instace ~= nil then 37 | warning_message_window_instace:close() 38 | warning_message_window_instace = nil 39 | end 40 | end 41 | 42 | local function open_code_action_menu(options) 43 | -- Might still be open. 44 | close_code_action_menu() 45 | close_warning_message_window() 46 | 47 | options = options or {} 48 | options.use_range = vim.api.nvim_get_mode().mode ~= 'n' 49 | local all_actions = action_utils.request_actions_from_all_servers(options) 50 | 51 | if #all_actions == 0 then 52 | warning_message_window_instace = WarningMessageWindow:new() 53 | warning_message_window_instace:open() 54 | vim.api.nvim_command( 55 | 'autocmd! CursorMoved ++once lua require("code_action_menu").close_warning_message_window()' 56 | ) 57 | else 58 | anchor_window_instance = AnchorWindow:new() 59 | menu_window_instance = MenuWindow:new(all_actions) 60 | menu_window_instance:open({ 61 | window_stack = { anchor_window_instance }, 62 | }) 63 | end 64 | end 65 | 66 | local function update_details_window(selected_action) 67 | if 68 | vim.g.code_action_menu_show_details == nil 69 | or vim.g.code_action_menu_show_details 70 | then 71 | if details_window_instance == nil then 72 | details_window_instance = DetailsWindow:new(selected_action) 73 | else 74 | details_window_instance:set_action(selected_action) 75 | end 76 | 77 | local window_stack = { anchor_window_instance, menu_window_instance } 78 | 79 | details_window_instance:open({ window_stack = window_stack }) 80 | end 81 | end 82 | 83 | local function update_diff_window(selected_action) 84 | if 85 | vim.g.code_action_menu_show_diff == nil or vim.g.code_action_menu_show_diff 86 | then 87 | if diff_window_instance == nil then 88 | diff_window_instance = DiffWindow:new(selected_action) 89 | else 90 | diff_window_instance:set_action(selected_action) 91 | end 92 | 93 | local window_stack = { anchor_window_instance, menu_window_instance } 94 | 95 | if details_window_instance ~= nil then 96 | table.insert(window_stack, details_window_instance) 97 | end 98 | 99 | diff_window_instance:open({ window_stack = window_stack }) 100 | end 101 | end 102 | 103 | local function update_selected_action() 104 | local selected_action = menu_window_instance:get_selected_action() 105 | update_details_window(selected_action) 106 | update_diff_window(selected_action) 107 | end 108 | 109 | local function execute_selected_action() 110 | local selected_action = menu_window_instance:get_selected_action() 111 | 112 | if selected_action:is_disabled() then 113 | vim.api.nvim_notify( 114 | 'Can not execute disabled action!', 115 | vim.log.levels.ERROR, 116 | {} 117 | ) 118 | else 119 | close_code_action_menu() -- Close first to execute the action in the correct buffer. 120 | selected_action:execute() 121 | end 122 | end 123 | 124 | local function select_line_and_execute_action(line_number) 125 | vim.validate({ ['to select menu line number'] = { line_number, 'number' } }) 126 | 127 | vim.api.nvim_win_set_cursor( 128 | menu_window_instance.window_number, 129 | { line_number, 0 } 130 | ) 131 | execute_selected_action() 132 | end 133 | 134 | return { 135 | open_code_action_menu = open_code_action_menu, 136 | update_selected_action = update_selected_action, 137 | close_code_action_menu = close_code_action_menu, 138 | close_warning_message_window = close_warning_message_window, 139 | execute_selected_action = execute_selected_action, 140 | select_line_and_execute_action = select_line_and_execute_action, 141 | } 142 | -------------------------------------------------------------------------------- /lua/code_action_menu/enumerations/text_document_edit_status_enum.lua: -------------------------------------------------------------------------------- 1 | local function makeEnum() 2 | return { 3 | CREATED = 'created', 4 | CHANGED = 'changed', 5 | RENAMED = 'renamed', 6 | DELETED = 'deleted', 7 | } 8 | end 9 | 10 | return makeEnum() 11 | -------------------------------------------------------------------------------- /lua/code_action_menu/enumerations/window_stack_direction_enum.lua: -------------------------------------------------------------------------------- 1 | local function makeEnum() 2 | return { 3 | UPWARDS = 'upwards', 4 | DOWNWARDS = 'downwards', 5 | } 6 | end 7 | 8 | return makeEnum() 9 | -------------------------------------------------------------------------------- /lua/code_action_menu/lsp_objects/actions/base_action.lua: -------------------------------------------------------------------------------- 1 | local WorkspaceEdit = require( 2 | 'code_action_menu.lsp_objects.edits.workspace_edit' 3 | ) 4 | 5 | local BaseAction = {} 6 | 7 | function BaseAction:new(server_data) 8 | vim.validate({ ['server data'] = { server_data, 'table' } }) 9 | 10 | local instance = { server_data = server_data } 11 | setmetatable(instance, self) 12 | self.__index = self 13 | return instance 14 | end 15 | 16 | function BaseAction:get_title() 17 | return self.server_data.title or 'missing title' 18 | end 19 | 20 | function BaseAction:get_kind() 21 | return 'undefined' 22 | end 23 | 24 | function BaseAction:get_name() 25 | return 'undefined' 26 | end 27 | 28 | function BaseAction:get_disabled_reason() 29 | return 'undefined' 30 | end 31 | 32 | function BaseAction:is_preferred() 33 | return false 34 | end 35 | 36 | function BaseAction:is_disabled() 37 | return false 38 | end 39 | 40 | function BaseAction:get_workspace_edit() 41 | return WorkspaceEdit:new() 42 | end 43 | 44 | function BaseAction:execute() 45 | error( 46 | 'Base actions can not be executed, but derived classes have to implement it!' 47 | ) 48 | end 49 | 50 | return BaseAction 51 | -------------------------------------------------------------------------------- /lua/code_action_menu/lsp_objects/actions/code_action.lua: -------------------------------------------------------------------------------- 1 | local BaseAction = require('code_action_menu.lsp_objects.actions.base_action') 2 | local TextDocumentEdit = require( 3 | 'code_action_menu.lsp_objects.edits.text_document_edit' 4 | ) 5 | local WorkspaceEdit = require( 6 | 'code_action_menu.lsp_objects.edits.workspace_edit' 7 | ) 8 | 9 | local CodeAction = BaseAction:new({}) 10 | 11 | function CodeAction:new(server_data) 12 | local instance = BaseAction:new(server_data) 13 | setmetatable(instance, self) 14 | self.__index = self 15 | return instance 16 | end 17 | 18 | function CodeAction:is_workspace_edit() 19 | local edit = self.server_data.edit 20 | return type(edit) == 'table' 21 | end 22 | 23 | function CodeAction:is_command() 24 | local command = self.server_data.command 25 | return type(command) == 'table' 26 | end 27 | 28 | function CodeAction:get_kind() 29 | if type(self.server_data.kind) == 'string' and #self.server_data.kind > 0 then 30 | return self.server_data.kind 31 | elseif self:is_workspace_edit() then 32 | return 'workspace edit' 33 | elseif self:is_command() then 34 | return 'command' 35 | else 36 | return 'undefined' 37 | end 38 | end 39 | 40 | function CodeAction:get_name() 41 | if self:is_command() then 42 | return self.server_data.command.command 43 | else 44 | return 'undefined' 45 | end 46 | end 47 | 48 | function CodeAction:is_preferred() 49 | return self.server_data.isPreferred or false 50 | end 51 | 52 | function CodeAction:is_disabled() 53 | return self.server_data.disabled ~= nil 54 | end 55 | 56 | function CodeAction:get_disabled_reason() 57 | return self.server_data.disabled.reason 58 | end 59 | 60 | function CodeAction:get_workspace_edit() 61 | local workspace_edit = WorkspaceEdit:new() 62 | 63 | if self:is_workspace_edit() then 64 | for _, data in ipairs(self.server_data.edit.documentChanges or {}) do 65 | local text_document_edit = TextDocumentEdit:new(data) 66 | workspace_edit:add_text_document_edit(text_document_edit) 67 | end 68 | 69 | for uri, edits in pairs(self.server_data.edit.changes or {}) do 70 | local data = { uri = uri, edits = edits } 71 | local text_document_edit = TextDocumentEdit:new(data) 72 | workspace_edit:add_text_document_edit(text_document_edit) 73 | end 74 | end 75 | 76 | return workspace_edit 77 | end 78 | 79 | function CodeAction:execute() 80 | if self:is_workspace_edit() then 81 | vim.lsp.util.apply_workspace_edit(self.server_data.edit, 'utf-8') 82 | elseif self:is_command() then 83 | vim.lsp.buf.execute_command(self.server_data.command) 84 | else 85 | vim.api.nvim_notify( 86 | 'Failed to execute code action of unknown kind!', 87 | vim.log.levels.ERROR, 88 | {} 89 | ) 90 | end 91 | end 92 | 93 | return CodeAction 94 | -------------------------------------------------------------------------------- /lua/code_action_menu/lsp_objects/actions/command.lua: -------------------------------------------------------------------------------- 1 | local BaseAction = require('code_action_menu.lsp_objects.actions.base_action') 2 | local TextDocumentEdit = require( 3 | 'code_action_menu.lsp_objects.edits.text_document_edit' 4 | ) 5 | local WorkspaceEdit = require( 6 | 'code_action_menu.lsp_objects.edits.workspace_edit' 7 | ) 8 | 9 | local Command = BaseAction:new({}) 10 | 11 | function Command:new(server_data) 12 | local instance = BaseAction:new(server_data) 13 | setmetatable(instance, self) 14 | self.__index = self 15 | return instance 16 | end 17 | 18 | function Command:get_kind() 19 | return 'command' 20 | end 21 | 22 | function Command:get_name() 23 | return self.server_data.command or 'undefined' 24 | end 25 | 26 | -- Though commands do nowdays actually not include edits anymore, some LSP 27 | -- servers still use them like that. This can be detected by inspecting the 28 | -- commands arguments. 29 | function Command:get_workspace_edit() 30 | local workspace_edit = WorkspaceEdit:new() 31 | 32 | for _, argument in ipairs(self.server_data.arguments or {}) do 33 | for _, data in ipairs(argument.documentChanges or {}) do 34 | local text_document_edit = TextDocumentEdit:new(data) 35 | workspace_edit:add_text_document_edit(text_document_edit) 36 | end 37 | end 38 | 39 | return workspace_edit 40 | end 41 | 42 | function Command:execute() 43 | vim.lsp.buf.execute_command(self.server_data) 44 | end 45 | 46 | return Command 47 | -------------------------------------------------------------------------------- /lua/code_action_menu/lsp_objects/edits/text_document_edit.lua: -------------------------------------------------------------------------------- 1 | local TextDocumentEditStatusEnum = require( 2 | 'code_action_menu.enumerations.text_document_edit_status_enum' 3 | ) 4 | 5 | local inaccessible_content_placeholder = '' 6 | 7 | local function uri_has_custom_scheme(uri) 8 | return uri:sub(1, 4) ~= 'file' 9 | end 10 | 11 | local function read_line_from_file(file_name, row) 12 | local file_descriptor = vim.loop.fs_open(file_name, 'r', 438) 13 | 14 | if not file_descriptor then 15 | return inaccessible_content_placeholder 16 | end 17 | 18 | local file_statistics = vim.loop.fs_fstat(file_descriptor) 19 | local file_content = vim.loop.fs_read( 20 | file_descriptor, 21 | file_statistics.size, 22 | 0 23 | ) 24 | vim.loop.fs_close(file_descriptor) 25 | 26 | local line_number = 0 27 | 28 | for line in string.gmatch(file_content, '([^\n]*)\n?') do 29 | if line_number == row then 30 | return line 31 | end 32 | line_number = line_number + 1 33 | end 34 | 35 | return inaccessible_content_placeholder 36 | end 37 | 38 | local function get_line(uri, row) 39 | if uri_has_custom_scheme(uri) then 40 | local buffer_number = vim.uri_to_bufnr(uri) 41 | vim.fn.bufload(buffer_number) 42 | end 43 | 44 | local file_name = vim.uri_to_fname(uri) 45 | 46 | if vim.fn.bufloaded(file_name) == 1 then 47 | local buffer_number = vim.fn.bufnr(file_name, false) 48 | local lines = vim.api.nvim_buf_get_lines(buffer_number, row, row + 1, false) 49 | return lines[1] or inaccessible_content_placeholder 50 | end 51 | 52 | return read_line_from_file(file_name, row) 53 | end 54 | 55 | local function get_line_count_of_text(text) 56 | vim.validate({ ['text to get line count for'] = { text, 'string' } }) 57 | 58 | local new_line_count = select(2, text:gsub('\n', '\n')) 59 | local line_count = new_line_count + 1 60 | 61 | if text:sub(-1) == '\n' then 62 | line_count = line_count - 1 63 | end 64 | 65 | return line_count 66 | end 67 | 68 | local function get_added_line_count_of_all_edits(all_edits) 69 | vim.validate({ ['edits to count'] = { all_edits, 'table' } }) 70 | 71 | local added_line_count = 0 72 | 73 | for _, edit in ipairs(all_edits) do 74 | local added_line_count_of_edit = get_line_count_of_text(edit.newText) 75 | added_line_count = added_line_count + added_line_count_of_edit 76 | end 77 | 78 | return added_line_count 79 | end 80 | 81 | local function get_deleted_line_count_of_all_edits(all_edits) 82 | vim.validate({ ['edits to count'] = { all_edits, 'table' } }) 83 | 84 | local deleted_line_count = 0 85 | 86 | for _, edit in ipairs(all_edits) do 87 | local startLine = edit.range.start.line 88 | local startColumn = edit.range.start.character 89 | local endLine = edit.range['end'].line 90 | local endColumn = edit.range['end'].character 91 | local text = edit.newText 92 | 93 | -- This makes sure it isn't a pure new text insertion outside the scope 94 | -- of just one line. 95 | if 96 | not ( 97 | startLine == endLine 98 | and startColumn == endColumn 99 | and text:sub(-1) == '\n' 100 | ) 101 | then 102 | local deleted_line_count_of_edit = endLine - startLine + 1 103 | deleted_line_count = deleted_line_count + deleted_line_count_of_edit 104 | end 105 | end 106 | 107 | return deleted_line_count 108 | end 109 | 110 | local function get_line_count_of_file(uri) 111 | vim.validate({ ['uri to get line count for'] = { uri, 'string' } }) 112 | 113 | local path = vim.uri_to_fname(uri) 114 | local handle = vim.loop.fs_open(path, 'r', 438) 115 | local statistics = vim.loop.fs_fstat(handle) 116 | local text = vim.loop.fs_read(handle, statistics.size) 117 | vim.loop.fs_close(handle) 118 | return get_line_count_of_text(text) 119 | end 120 | 121 | local function get_list_of_added_lines_in_edit(uri, edit) 122 | local added_lines = {} 123 | 124 | for line in vim.gsplit(edit.newText, '\n', true) do 125 | table.insert(added_lines, line) 126 | end 127 | 128 | local first_original_complete_line = get_line(uri, edit.range.start.line) 129 | or '' 130 | 131 | local text_before_changes = first_original_complete_line:sub( 132 | 0, 133 | edit.range.start.character 134 | ) 135 | added_lines[1] = text_before_changes .. added_lines[1] 136 | 137 | local last_original_complete_line = get_line(uri, edit.range['end'].line) 138 | or '' 139 | 140 | if #last_original_complete_line > edit.range['end'].character then 141 | local text_after_changes = last_original_complete_line:sub( 142 | edit.range['end'].character + 1 143 | ) 144 | added_lines[#added_lines] = added_lines[#added_lines] .. text_after_changes 145 | end 146 | 147 | local line_index = edit.range.start.line 148 | 149 | for index = 1, #added_lines, 1 do 150 | added_lines[index] = line_index .. ' ' .. added_lines[index] 151 | line_index = line_index + 1 152 | end 153 | 154 | return added_lines 155 | end 156 | 157 | local function get_list_of_original_lines(uri, start_line, end_line) 158 | local original_lines = {} 159 | 160 | for line_index = start_line, end_line, 1 do 161 | local line = get_line(uri, line_index) 162 | 163 | if line ~= nil then 164 | line = line_index .. ' ' .. line 165 | table.insert(original_lines, line) 166 | end 167 | end 168 | 169 | return original_lines 170 | end 171 | 172 | local TextDocumentEdit = {} 173 | 174 | function TextDocumentEdit:new(server_data) 175 | vim.validate({ ['text document server data'] = { server_data, 'table' } }) 176 | 177 | local uri = server_data.uri 178 | or server_data.newUri 179 | or server_data.textDocument.uri 180 | local edits = server_data.edits or {} 181 | local kind = server_data.kind 182 | local status = ( 183 | kind == 'create' and TextDocumentEditStatusEnum.CREATED 184 | or kind == 'rename' and TextDocumentEditStatusEnum.RENAMED 185 | or kind == 'delete' and TextDocumentEditStatusEnum.DELETED 186 | or TextDocumentEditStatusEnum.CHANGED 187 | ) 188 | 189 | local instance = { 190 | uri = uri, 191 | edits = edits, 192 | status = status, 193 | } 194 | 195 | setmetatable(instance, self) 196 | self.__index = self 197 | return instance 198 | end 199 | 200 | function TextDocumentEdit:merge_text_document_edit_for_same_uri( 201 | text_document_edit 202 | ) 203 | vim.validate({ 204 | ['text document edit to merge'] = { text_document_edit, 'table' }, 205 | }) 206 | 207 | if text_document_edit.uri ~= self.uri then 208 | error('Can not merge to TextDocumentEdits for different URIs!') 209 | end 210 | 211 | vim.list_extend(self.edits, text_document_edit.edits or {}) 212 | 213 | if text_document_edit.status ~= TextDocumentEditStatusEnum.CHANGED then 214 | self.status = text_document_edit.status 215 | end 216 | end 217 | 218 | function TextDocumentEdit:get_document_path() 219 | local absolute_path = vim.uri_to_fname(self.uri) 220 | local current_working_directory = vim.api.nvim_call_function('getcwd', {}) 221 | local home_directory = os.getenv('HOME') 222 | 223 | if absolute_path:find(current_working_directory, 1, true) then 224 | return absolute_path:sub(current_working_directory:len() + 2) 225 | elseif 226 | home_directory ~= nil and absolute_path:find(home_directory, 1, true) 227 | then 228 | return absolute_path:sub(home_directory:len() + 2) 229 | else 230 | return absolute_path 231 | end 232 | end 233 | 234 | -- Takes the `git diff --numstat` as model. This means it only acts on full 235 | -- lines. As result a changed line counts as one added and one deleted at the 236 | -- same time. 237 | -- This "algorithm" is very limited and assumes that the language server its 238 | -- edits are efficient. This means that there are not multiple edits which act 239 | -- on the same or intersecting text ranges. 240 | -- 241 | -- Returns table with the key `added` and `deleted` with numbers as values 242 | function TextDocumentEdit:get_line_number_statistics() 243 | if self.status == TextDocumentEditStatusEnum.CREATED then 244 | local added_line_count = get_added_line_count_of_all_edits(self.edits) 245 | return { added = added_line_count, deleted = 0 } 246 | elseif self.status == TextDocumentEditStatusEnum.DELETED then 247 | local deleted_line_count = get_line_count_of_file(self.uri) 248 | return { added = 0, deleted = deleted_line_count } 249 | else 250 | local added_line_count = get_added_line_count_of_all_edits(self.edits) 251 | local deleted_line_count = get_deleted_line_count_of_all_edits(self.edits) 252 | return { added = added_line_count, deleted = deleted_line_count } 253 | end 254 | end 255 | 256 | function TextDocumentEdit:get_diff_lines() 257 | local all_diffs = {} 258 | local context_line_count = 1 259 | 260 | for _, edit in ipairs(self.edits) do 261 | local start_line = edit.range.start.line 262 | local end_line = edit.range['end'].line 263 | local context_before_lines = get_list_of_original_lines( 264 | self.uri, 265 | start_line - context_line_count, 266 | start_line - 1 267 | ) 268 | local deleted_lines = get_list_of_original_lines( 269 | self.uri, 270 | start_line, 271 | end_line 272 | ) 273 | local added_lines = get_list_of_added_lines_in_edit(self.uri, edit) 274 | local context_after_lines = get_list_of_original_lines( 275 | self.uri, 276 | end_line + 1, 277 | end_line + context_line_count 278 | ) 279 | 280 | table.insert(all_diffs, { 281 | context_before = context_before_lines, 282 | deleted = deleted_lines, 283 | added = added_lines, 284 | context_after = context_after_lines, 285 | }) 286 | end 287 | 288 | return all_diffs 289 | end 290 | 291 | return TextDocumentEdit 292 | -------------------------------------------------------------------------------- /lua/code_action_menu/lsp_objects/edits/workspace_edit.lua: -------------------------------------------------------------------------------- 1 | local WorkspaceEdit = {} 2 | 3 | function WorkspaceEdit:new() 4 | local instance = { all_text_document_edits = {} } 5 | setmetatable(instance, self) 6 | self.__index = self 7 | return instance 8 | end 9 | 10 | function WorkspaceEdit:add_text_document_edit(text_document_edit) 11 | vim.validate({ 12 | ['to add text document edit'] = { text_document_edit, 'table' }, 13 | }) 14 | 15 | for _, existing_text_document_edit in ipairs(self.all_text_document_edits) do 16 | if existing_text_document_edit.uri == text_document_edit.uri then 17 | existing_text_document_edit:merge_text_document_edit_for_same_uri( 18 | text_document_edit 19 | ) 20 | return 21 | end 22 | end 23 | 24 | table.insert(self.all_text_document_edits, text_document_edit) 25 | end 26 | 27 | return WorkspaceEdit 28 | -------------------------------------------------------------------------------- /lua/code_action_menu/utility_functions/actions.lua: -------------------------------------------------------------------------------- 1 | local Command = require('code_action_menu.lsp_objects.actions.command') 2 | local CodeAction = require('code_action_menu.lsp_objects.actions.code_action') 3 | 4 | local function unpack_result_and_error(server_data, client_id) 5 | local client = vim.lsp.get_client_by_id(client_id) 6 | local result = nil 7 | local error = nil 8 | 9 | if server_data == nil then 10 | error = 'Server for client \'' .. client.name .. '\' not yet ready!' 11 | elseif type(server_data) == 'table' and server_data.err ~= nil then 12 | error = 'Server of client \'' .. client.name .. '\' returned error: ' 13 | 14 | if type(server_data.err) == 'string' then 15 | error = error .. server_data.err 16 | elseif 17 | type(server_data.err) == 'table' 18 | and type(server_data.err.message) == 'string' 19 | then 20 | error = error .. server_data.err.message 21 | else 22 | error = error .. 'unknown error - failed to parse response' 23 | end 24 | elseif type(server_data) == 'table' and server_data.result then 25 | result = server_data.result 26 | else 27 | result = server_data 28 | end 29 | 30 | return result, error 31 | end 32 | 33 | local function resolve_code_action(client_id, code_action_object) 34 | local client = vim.lsp.get_client_by_id(client_id) 35 | local response = client.request_sync('codeAction/resolve', code_action_object) 36 | local action_object, error = unpack_result_and_error(response, client_id) 37 | 38 | if error then 39 | vim.api.nvim_notify(error, vim.log.levels.WARN, {}) 40 | end 41 | 42 | return action_object 43 | end 44 | 45 | local function parse_object_as_action(code_action_object) 46 | if 47 | type(code_action_object) == 'table' 48 | and type(code_action_object.command) == 'string' 49 | then 50 | return Command:new(code_action_object) 51 | elseif 52 | type(code_action_object) == 'table' 53 | and ( 54 | type(code_action_object.edit) == 'table' 55 | or type(code_action_object.command) == 'table' 56 | ) 57 | then 58 | return CodeAction:new(code_action_object) 59 | else 60 | local error = 61 | 'Failed to parse unknown code action or command data structure! Skipped.' 62 | vim.api.nvim_notify(error, vim.log.levels.WARN, {}) 63 | return nil 64 | end 65 | end 66 | 67 | local function parse_action_data_objects(client_id, all_code_action_objects) 68 | local all_actions = {} 69 | 70 | for _, code_action_object in ipairs(all_code_action_objects) do 71 | if 72 | type(code_action_object) == 'table' and code_action_object.data ~= nil 73 | then 74 | code_action_object = resolve_code_action(client_id, code_action_object) 75 | end 76 | 77 | local action = parse_object_as_action(code_action_object) 78 | 79 | if action ~= nil then 80 | table.insert(all_actions, action) 81 | end 82 | end 83 | 84 | return all_actions 85 | end 86 | 87 | local function request_actions_from_server(client_id, parameters) 88 | local client = vim.lsp.get_client_by_id(client_id) 89 | 90 | if not client.supports_method('textDocument/codeAction') then 91 | return {} 92 | end 93 | 94 | local response = client.request_sync('textDocument/codeAction', parameters) 95 | local action_objects, error = unpack_result_and_error(response, client_id) 96 | 97 | if error then 98 | vim.api.nvim_notify(error, vim.log.levels.WARN, {}) 99 | end 100 | 101 | return action_objects or {} 102 | end 103 | 104 | local function get_range_request_parameters() 105 | local selection_start = {} 106 | selection_start[1], selection_start[2] = unpack(vim.fn.getpos('v'), 2, 3) 107 | -- NOTE: getpos's column is 1-based, and we need 0-based 108 | selection_start[2] = selection_start[2] - 1 109 | 110 | local selection_end = vim.api.nvim_win_get_cursor(0) 111 | 112 | -- NOTE: handle "reverse" selection (the cursor is at the start of the 113 | -- selection, not the end) 114 | -- Flip based on lines 115 | if selection_start[1] > selection_end[1] then 116 | local temp_selection_end = selection_end 117 | selection_end = selection_start 118 | selection_start = temp_selection_end 119 | end 120 | 121 | -- Flip based on columns 122 | if selection_start[2] > selection_end[2] then 123 | local temp_selection_end = selection_end[2] 124 | selection_end[2] = selection_start[2] 125 | selection_start[2] = temp_selection_end 126 | end 127 | 128 | return vim.lsp.util.make_given_range_params(selection_start, selection_end) 129 | end 130 | 131 | local function request_actions_from_all_servers(options) 132 | vim.validate({ 133 | ['options is table'] = { options, 't', true }, 134 | }) 135 | 136 | options = options or {} 137 | local request_parameters = 138 | options.use_range 139 | and get_range_request_parameters() 140 | or vim.lsp.util.make_range_params() 141 | 142 | local line_diagnostics = vim.lsp.diagnostic.get_line_diagnostics() 143 | request_parameters.context = { 144 | diagnostics = options.diagnostics or line_diagnostics 145 | } 146 | 147 | local all_clients = vim.lsp.buf_get_clients() 148 | local all_actions = {} 149 | 150 | for _, client in pairs(all_clients) do 151 | local action_data_objects = request_actions_from_server( 152 | client.id, 153 | request_parameters 154 | ) 155 | local actions = parse_action_data_objects(client.id, action_data_objects) 156 | vim.list_extend(all_actions, actions) 157 | end 158 | 159 | return all_actions 160 | end 161 | 162 | local function order_actions(action_table, key_a, key_b) 163 | local action_a = action_table[key_a] 164 | local action_b = action_table[key_b] 165 | 166 | if action_b:is_preferred() and not action_a:is_preferred() then 167 | return false 168 | elseif action_a:is_disabled() and not action_b:is_disabled() then 169 | return false 170 | else 171 | -- Ordering function needs to return `true` at some point for every element. 172 | return key_a < key_b 173 | end 174 | end 175 | 176 | local function get_ordered_action_table_keys(action_table) 177 | vim.validate({ ['action table to sort'] = { action_table, 'table' } }) 178 | local keys = {} 179 | 180 | for key in pairs(action_table) do 181 | local index = #keys + 1 182 | keys[index] = key 183 | end 184 | 185 | table.sort(keys, function(key_a, key_b) 186 | return order_actions(action_table, key_a, key_b) 187 | end) 188 | 189 | return keys 190 | end 191 | 192 | -- Put preferred actions at start, disabled at end 193 | local function iterate_actions_ordered(action_table) 194 | vim.validate({ ['actions to sort'] = { action_table, 'table' } }) 195 | 196 | local keys = get_ordered_action_table_keys(action_table) 197 | local index = 0 198 | 199 | return function() 200 | index = index + 1 201 | 202 | if keys[index] then 203 | return index, action_table[keys[index]] 204 | end 205 | end 206 | end 207 | 208 | local function get_action_at_index_ordered(action_table, index) 209 | vim.validate({ ['actions to sort'] = { action_table, 'table' } }) 210 | 211 | local keys = get_ordered_action_table_keys(action_table) 212 | local key_for_index = keys[index] 213 | return action_table[key_for_index] 214 | end 215 | 216 | return { 217 | request_actions_from_all_servers = request_actions_from_all_servers, 218 | iterate_actions_ordered = iterate_actions_ordered, 219 | get_action_at_index_ordered = get_action_at_index_ordered, 220 | } 221 | -------------------------------------------------------------------------------- /lua/code_action_menu/utility_functions/buffers.lua: -------------------------------------------------------------------------------- 1 | local function get_buffer_width(buffer_number) 2 | local buffer_lines = vim.api.nvim_buf_get_lines(buffer_number, 0, -1, false) 3 | local longest_line = '' 4 | 5 | for _, line in ipairs(buffer_lines) do 6 | if #line > #longest_line then 7 | longest_line = line 8 | end 9 | end 10 | 11 | return #longest_line 12 | end 13 | 14 | local function get_buffer_height(buffer_number) 15 | local buffer_lines = vim.api.nvim_buf_get_lines(buffer_number, 0, -1, false) 16 | return #buffer_lines 17 | end 18 | 19 | local function is_buffer_empty(buffer_number) 20 | local buffer_lines = vim.api.nvim_buf_get_lines(buffer_number, 0, -1, false) 21 | return #buffer_lines == 0 or (#buffer_lines == 1 and #buffer_lines[1] == 0) 22 | end 23 | 24 | return { 25 | get_buffer_width = get_buffer_width, 26 | get_buffer_height = get_buffer_height, 27 | is_buffer_empty = is_buffer_empty, 28 | } 29 | -------------------------------------------------------------------------------- /lua/code_action_menu/windows/anchor_window.lua: -------------------------------------------------------------------------------- 1 | local BaseWindow = require('code_action_menu.windows.base_window') 2 | 3 | local AnchorWindow = BaseWindow:new() 4 | 5 | -- The anchor window is just a reference to an already existing window that is 6 | -- used as an anchor for a window stack. Thereby it can't be opened, nor closed. 7 | -- In fact it is just used to read data and still implement the window class. 8 | -- It will reference the window currently active when getting created. 9 | function AnchorWindow:new() 10 | -- These calls only work for the current window, therefore calculate them now 11 | -- and save for later. 12 | local window_number = vim.api.nvim_call_function('win_getid', {}) 13 | local window_position = vim.api.nvim_win_get_position(window_number) 14 | local cursor_row = vim.api.nvim_call_function('winline', {}) 15 | local cursor_column = vim.api.nvim_call_function('wincol', {}) 16 | 17 | local instance = BaseWindow:new({ is_anchor = true }) 18 | setmetatable(instance, self) 19 | self.__index = self 20 | self.is_anchor = true 21 | self.window_number = window_number 22 | self.window_options = { 23 | row = { [false] = window_position[1] + cursor_row }, 24 | col = { [false] = window_position[2] + cursor_column }, 25 | height = 0, 26 | width = 0, 27 | zindex = nil, 28 | } 29 | return instance 30 | end 31 | 32 | -- Prevent it to get opened by code. It is not necesary to "block" the other 33 | -- functions like `create_buffer` as well as they are just called from here 34 | -- usually. 35 | function AnchorWindow:open() 36 | return 37 | end 38 | 39 | -- Prevent it to get opened by code. 40 | function AnchorWindow:close() 41 | return 42 | end 43 | 44 | return AnchorWindow 45 | -------------------------------------------------------------------------------- /lua/code_action_menu/windows/base_window.lua: -------------------------------------------------------------------------------- 1 | local buffer_utils = require('code_action_menu.utility_functions.buffers') 2 | 3 | local BaseWindow = { 4 | window_number = -1, 5 | window_options = nil, 6 | buffer_number = -1, 7 | focusable = false, 8 | filetype = '', 9 | namespace_id = vim.api.nvim_create_namespace('code_action_menu'), 10 | } 11 | 12 | function BaseWindow:new(base_object) 13 | local instance = base_object or {} 14 | setmetatable(instance, self) 15 | self.__index = self 16 | return instance 17 | end 18 | 19 | function BaseWindow:get_content() 20 | return {} 21 | end 22 | 23 | function BaseWindow:update_virtual_text() 24 | return 25 | end 26 | 27 | function BaseWindow:update_buffer_content() 28 | vim.api.nvim_buf_clear_namespace(self.buffer_number, self.namespace_id, 0, -1) 29 | 30 | local content = vim.tbl_map( 31 | function(v) return v:gsub("\n"," ") end, 32 | self:get_content() 33 | ) 34 | 35 | -- Unset and set the filtype option removes temporally th read-only property 36 | vim.api.nvim_buf_set_option(self.buffer_number, 'filetype', '') 37 | vim.api.nvim_buf_set_lines(self.buffer_number, 0, -1, false, content) 38 | vim.api.nvim_buf_set_option(self.buffer_number, 'filetype', self.filetype) 39 | 40 | self:update_virtual_text() 41 | end 42 | 43 | function BaseWindow:create_buffer() 44 | self.buffer_number = vim.api.nvim_create_buf(false, true) 45 | vim.api.nvim_buf_set_option(self.buffer_number, 'filetype', self.filetype) 46 | end 47 | 48 | function BaseWindow:get_window_configuration(_) 49 | local buffer_width = buffer_utils.get_buffer_width(self.buffer_number) + 1 50 | local buffer_height = buffer_utils.get_buffer_height(self.buffer_number) 51 | return vim.lsp.util.make_floating_popup_options(buffer_width, buffer_height, { 52 | border = vim.g.code_action_menu_window_border or 'single', 53 | }) 54 | end 55 | 56 | function BaseWindow:open(window_configuration_options) 57 | vim.validate({ 58 | ['window configuration options'] = { 59 | window_configuration_options, 60 | 'table', 61 | true, 62 | }, 63 | }) 64 | 65 | if self.buffer_number == -1 then 66 | self:create_buffer() 67 | end 68 | 69 | self:update_buffer_content() 70 | 71 | if buffer_utils.is_buffer_empty(self.buffer_number) then 72 | self:delete_buffer() 73 | self:close() 74 | return 75 | end 76 | 77 | local window_configuration = self:get_window_configuration( 78 | window_configuration_options 79 | ) 80 | 81 | if self.window_number == -1 then 82 | self.window_number = vim.api.nvim_open_win( 83 | self.buffer_number, 84 | self.focusable, 85 | window_configuration 86 | ) 87 | self.window_options = vim.api.nvim_win_get_config(self.window_number) 88 | vim.api.nvim_command('doautocmd User CodeActionMenuWindowOpened') 89 | else 90 | vim.api.nvim_win_set_config(self.window_number, window_configuration) 91 | end 92 | 93 | self:after_opened() 94 | end 95 | 96 | function BaseWindow:after_opened() 97 | return 98 | end 99 | 100 | function BaseWindow:set_window_width(width) 101 | pcall(vim.api.nvim_win_set_width, self.window_number, width) 102 | end 103 | 104 | function BaseWindow:get_option(name) 105 | if self.window_options == nil then 106 | return nil 107 | else 108 | local option = self.window_options[name] 109 | 110 | -- Special treatment to get absolute positions. Ugly but... 111 | if name == 'row' or name == 'col' then 112 | return option[false] 113 | else 114 | return option 115 | end 116 | end 117 | end 118 | 119 | function BaseWindow:delete_buffer() 120 | pcall(vim.api.nvim_buf_delete, self.buffer_number, { force = true }) 121 | self.buffer_number = -1 122 | end 123 | 124 | function BaseWindow:close() 125 | self:delete_buffer() 126 | pcall(vim.api.nvim_win_close, self.window_number, true) 127 | self.window_number = -1 128 | end 129 | 130 | return BaseWindow 131 | -------------------------------------------------------------------------------- /lua/code_action_menu/windows/details_window.lua: -------------------------------------------------------------------------------- 1 | local StackingWindow = require('code_action_menu.windows.stacking_window') 2 | 3 | local DetailsWindow = StackingWindow:new() 4 | 5 | function DetailsWindow:new(action) 6 | vim.validate({ ['details window action'] = { action, 'table' } }) 7 | 8 | local instance = StackingWindow:new({ action = action }) 9 | setmetatable(instance, self) 10 | self.__index = self 11 | self.filetype = 'code-action-menu-details' 12 | return instance 13 | end 14 | 15 | function DetailsWindow:get_content() 16 | local title = self.action:get_title() 17 | local kind = self.action:get_kind() 18 | local name = self.action:get_name() 19 | local preferred = self.action:is_preferred() and 'yes' or 'no' 20 | local disabled = self.action:is_disabled() 21 | and ('yes - ' .. self.action:get_disabled_reason()) 22 | or 'no' 23 | 24 | return { 25 | title, 26 | '', 27 | 'Kind: ' .. kind, 28 | 'Name: ' .. name, 29 | 'Preferred: ' .. preferred, 30 | 'Disabled: ' .. disabled, 31 | } 32 | end 33 | 34 | function DetailsWindow:set_action(action) 35 | vim.validate({ ['updated details window action'] = { action, 'table' } }) 36 | 37 | self.action = action 38 | end 39 | 40 | return DetailsWindow 41 | -------------------------------------------------------------------------------- /lua/code_action_menu/windows/diff_window.lua: -------------------------------------------------------------------------------- 1 | local StackingWindow = require('code_action_menu.windows.stacking_window') 2 | local TextDocumentEditStatusEnum = require( 3 | 'code_action_menu.enumerations.text_document_edit_status_enum' 4 | ) 5 | 6 | local function get_text_document_edit_status_icon(status) 7 | return ( 8 | status == TextDocumentEditStatusEnum.CREATED and '*' 9 | or status == TextDocumentEditStatusEnum.CHANGED and '~' 10 | or status == TextDocumentEditStatusEnum.RENAMED and '>' 11 | or status == TextDocumentEditStatusEnum.DELETED and '!' 12 | or error( 13 | 'Can not get icon unknown TextDocumentEdit status: \'' .. status .. '\'' 14 | ) 15 | ) 16 | end 17 | 18 | local function get_summary_line_formatted(text_document_edit) 19 | local status_icon = get_text_document_edit_status_icon( 20 | text_document_edit.status 21 | ) 22 | local file_path = text_document_edit:get_document_path() 23 | local line_number_statistics = text_document_edit:get_line_number_statistics() 24 | local changes = '(+' 25 | .. line_number_statistics.added 26 | .. ' -' 27 | .. line_number_statistics.deleted 28 | .. ')' 29 | return status_icon .. file_path .. ' ' .. changes 30 | end 31 | 32 | local function get_diff_lines_formatted(text_document_edit) 33 | local diff_lines = {} 34 | 35 | for _, changed_lines in ipairs(text_document_edit:get_diff_lines()) do 36 | for _, context_line in ipairs(changed_lines.context_before) do 37 | table.insert(diff_lines, ' ' .. context_line) 38 | end 39 | 40 | for _, deleted_line in ipairs(changed_lines.deleted) do 41 | table.insert(diff_lines, '-' .. deleted_line) 42 | end 43 | 44 | for _, added_line in ipairs(changed_lines.added) do 45 | table.insert(diff_lines, '+' .. added_line) 46 | end 47 | 48 | for _, context_line in ipairs(changed_lines.context_after) do 49 | table.insert(diff_lines, ' ' .. context_line) 50 | end 51 | end 52 | 53 | return diff_lines 54 | end 55 | 56 | local function get_diff_square_counts(text_document_edit) 57 | local line_number_statistics = text_document_edit:get_line_number_statistics() 58 | local total_changed_lines = line_number_statistics.added 59 | + line_number_statistics.deleted 60 | local modulu_five = total_changed_lines % 5 61 | local total_changed_lines_round_to_five = total_changed_lines 62 | + (modulu_five > 0 and 5 - modulu_five or 0) 63 | local lines_per_square = total_changed_lines_round_to_five / 5 64 | local squares_for_added_lines = math.floor( 65 | line_number_statistics.added / lines_per_square 66 | ) 67 | local squares_for_deleted_lines = math.floor( 68 | line_number_statistics.deleted / lines_per_square 69 | ) 70 | 71 | if line_number_statistics.added > 0 and squares_for_added_lines == 0 then 72 | squares_for_added_lines = 1 73 | end 74 | 75 | if line_number_statistics.deleted > 0 and squares_for_deleted_lines == 0 then 76 | squares_for_deleted_lines = 1 77 | end 78 | 79 | local squares_for_neutral_fill = 5 80 | - squares_for_added_lines 81 | - squares_for_deleted_lines 82 | 83 | return { 84 | added = squares_for_added_lines, 85 | deleted = squares_for_deleted_lines, 86 | neutral = squares_for_neutral_fill, 87 | } 88 | end 89 | 90 | local function get_count_of_edits_diff_lines(text_document_edit) 91 | local diff_lines = get_diff_lines_formatted(text_document_edit) 92 | return #diff_lines 93 | end 94 | 95 | local DiffWindow = StackingWindow:new() 96 | 97 | function DiffWindow:new(action) 98 | vim.validate({ ['diff window action'] = { action, 'table' } }) 99 | 100 | local instance = StackingWindow:new({ action = action }) 101 | setmetatable(instance, self) 102 | self.__index = self 103 | self.filetype = 'code-action-menu-diff' 104 | return instance 105 | end 106 | 107 | function DiffWindow:get_content() 108 | local content = {} 109 | local workspace_edit = self.action:get_workspace_edit() 110 | 111 | for _, text_document_edit in ipairs(workspace_edit.all_text_document_edits) do 112 | local summary_line = get_summary_line_formatted(text_document_edit) 113 | table.insert(content, summary_line) 114 | 115 | local diff_lines = get_diff_lines_formatted(text_document_edit) 116 | vim.list_extend(content, diff_lines) 117 | end 118 | 119 | return content 120 | end 121 | 122 | function DiffWindow:update_virtual_text() 123 | local workspace_edit = self.action:get_workspace_edit() 124 | local summary_line_index = 0 125 | 126 | for _, text_document_edit in ipairs(workspace_edit.all_text_document_edits) do 127 | local square_counts = get_diff_square_counts(text_document_edit) 128 | local chunks = {} 129 | 130 | if square_counts.added > 0 then 131 | table.insert(chunks, { 132 | string.rep('■', square_counts.added), 133 | 'CodeActionMenuDetailsAddedSquares', 134 | }) 135 | end 136 | 137 | if square_counts.deleted > 0 then 138 | table.insert(chunks, { 139 | string.rep('■', square_counts.deleted), 140 | 'CodeActionMenuDetailsDeletedSquares', 141 | }) 142 | end 143 | 144 | if square_counts.neutral > 0 then 145 | table.insert(chunks, { 146 | string.rep('■', square_counts.neutral), 147 | 'CodeActionMenuDetailsNeutralSquares', 148 | }) 149 | end 150 | 151 | vim.api.nvim_buf_set_virtual_text( 152 | self.buffer_number, 153 | self.namespace_id, 154 | summary_line_index, 155 | chunks, 156 | {} 157 | ) 158 | summary_line_index = summary_line_index 159 | + get_count_of_edits_diff_lines(text_document_edit) 160 | end 161 | end 162 | 163 | function DiffWindow:set_action(action) 164 | vim.validate({ ['updated diff window action'] = { action, 'table' } }) 165 | 166 | self.action = action 167 | end 168 | 169 | return DiffWindow 170 | -------------------------------------------------------------------------------- /lua/code_action_menu/windows/menu_window.lua: -------------------------------------------------------------------------------- 1 | local action_utils = require('code_action_menu.utility_functions.actions') 2 | local StackingWindow = require('code_action_menu.windows.stacking_window') 3 | 4 | local function format_action_kind(action_kind) 5 | if 6 | vim.g.code_action_menu_show_action_kind ~= nil 7 | and not vim.g.code_action_menu_show_action_kind 8 | then 9 | return '' 10 | end 11 | 12 | return '(' .. action_kind .. ') ' 13 | end 14 | 15 | local function format_summary_for_action(action, index) 16 | vim.validate({ ['action to format summary for'] = { action, 'table' } }) 17 | 18 | local formatted_index = ' [' .. index .. '] ' 19 | local kind = format_action_kind(action:get_kind()) 20 | local title = action:get_title() 21 | local disabled = action:is_disabled() and ' [disabled]' or '' 22 | return formatted_index .. kind .. title .. disabled 23 | end 24 | 25 | local MenuWindow = StackingWindow:new() 26 | 27 | function MenuWindow:new(all_actions) 28 | vim.validate({ ['all code actions'] = { all_actions, 'table' } }) 29 | 30 | local instance = StackingWindow:new({ all_actions = all_actions }) 31 | setmetatable(instance, self) 32 | self.__index = self 33 | self.focusable = true 34 | self.filetype = 'code-action-menu-menu' 35 | return instance 36 | end 37 | 38 | function MenuWindow:get_content() 39 | local content = {} 40 | 41 | for index, action in action_utils.iterate_actions_ordered(self.all_actions) do 42 | local line = format_summary_for_action(action, index) 43 | table.insert(content, line) 44 | end 45 | 46 | return content 47 | end 48 | 49 | function MenuWindow:get_selected_action() 50 | if self.window_number == -1 then 51 | error('Can not retrieve selected action when menu is not open!') 52 | else 53 | local cursor = vim.api.nvim_win_get_cursor(self.window_number) 54 | local line = cursor[1] 55 | return action_utils.get_action_at_index_ordered(self.all_actions, line) 56 | end 57 | end 58 | 59 | return MenuWindow 60 | -------------------------------------------------------------------------------- /lua/code_action_menu/windows/stacking_window.lua: -------------------------------------------------------------------------------- 1 | local buffer_utils = require('code_action_menu.utility_functions.buffers') 2 | local BaseWindow = require('code_action_menu.windows.base_window') 3 | local WindowStackDirectionEnum = require( 4 | 'code_action_menu.enumerations.window_stack_direction_enum' 5 | ) 6 | 7 | local function decide_for_direction(anchor_window) 8 | local editor_height = vim.api.nvim_get_option('lines') 9 | local anchor_window_row = anchor_window:get_option('row') 10 | local free_space_top = anchor_window_row - 1 11 | local free_space_bottom = editor_height - anchor_window_row 12 | 13 | -- We don't know how big the stack will grow. Therefore take the direction 14 | -- with more space left and hope it is enough in most cases. 15 | return free_space_top > free_space_bottom and WindowStackDirectionEnum.UPWARDS 16 | or WindowStackDirectionEnum.DOWNWARDS 17 | end 18 | 19 | local function get_stack_direction(window_stack) 20 | if #window_stack == 1 then 21 | return decide_for_direction(window_stack[1]) 22 | end 23 | 24 | local direction = nil 25 | 26 | for index = 1, #window_stack - 1 do 27 | local current_row = window_stack[index]:get_option('row') 28 | local successor_row = window_stack[index + 1]:get_option('row') 29 | local new_direction = current_row > successor_row 30 | and WindowStackDirectionEnum.UPWARDS 31 | or WindowStackDirectionEnum.DOWNWARDS 32 | 33 | if direction == nil then 34 | direction = new_direction 35 | elseif direction ~= nil and direction ~= new_direction then 36 | error('Window stack is not sorted correctly!') 37 | end 38 | end 39 | 40 | return direction 41 | end 42 | 43 | local StackingWindow = BaseWindow:new() 44 | 45 | function StackingWindow:new(base_object) 46 | local instance = BaseWindow:new(base_object) 47 | setmetatable(instance, self) 48 | self.__index = self 49 | self.window_stack = {} 50 | return instance 51 | end 52 | 53 | -- The window stack is a list of window class instances. Their order is 54 | -- important as they specify in which direction the stack is growing. Each new 55 | -- stacking window will be docked either on top or below the last window in 56 | -- the stack. 57 | function StackingWindow:get_window_configuration(window_configuration_options) 58 | vim.validate({ 59 | ['buffer number to create window for'] = { self.buffer_number, 'number' }, 60 | }) 61 | vim.validate({ 62 | ['window configuration options'] = { 63 | window_configuration_options, 64 | 'table', 65 | }, 66 | }) 67 | vim.validate({ 68 | ['window stack'] = { window_configuration_options.window_stack, 'table' }, 69 | }) 70 | vim.validate({ 71 | ['use buffer width'] = { 72 | window_configuration_options.user_buffer_width, 73 | 'boolean', 74 | true, 75 | }, 76 | }) 77 | 78 | self.window_stack = window_configuration_options.window_stack 79 | local last_window = self.window_stack[#self.window_stack] 80 | local stack_direction = get_stack_direction(self.window_stack) 81 | -- This makes the simplification to assume that all floating windows have a border... 82 | local border_height = last_window:get_option('zindex') and 2 or 0 83 | 84 | local window_height = buffer_utils.get_buffer_height(self.buffer_number) 85 | local window_width = buffer_utils.get_buffer_width(self.buffer_number) + 1 86 | local window_column = last_window:get_option('col') 87 | local window_row = 0 88 | 89 | if stack_direction == WindowStackDirectionEnum.UPWARDS then 90 | window_row = last_window:get_option('row') - window_height - border_height 91 | window_row = window_row - (last_window.is_anchor and 3 or 0) 92 | elseif stack_direction == WindowStackDirectionEnum.DOWNWARDS then 93 | window_row = last_window:get_option('row') 94 | + last_window:get_option('height') 95 | + border_height 96 | end 97 | 98 | return { 99 | relative = 'editor', 100 | row = window_row, 101 | col = window_column, 102 | width = window_width, 103 | height = window_height, 104 | focusable = false, 105 | style = 'minimal', 106 | border = vim.g.code_action_menu_window_border or 'single', 107 | } 108 | end 109 | 110 | -- This function makes sure that all windows in a stack have the same size which 111 | -- relates to the widest buffer. 112 | -- It makes the assumation that stack windows get opened one after each other. 113 | -- This means that we only need to check the last window in the stack because 114 | -- all other windows must have the same width as well. Just because for each of 115 | -- them this function has run. 116 | function StackingWindow:after_opened() 117 | local last_window = self.window_stack[#self.window_stack] 118 | 119 | if not last_window.is_anchor then 120 | local own_width = buffer_utils.get_buffer_width(self.buffer_number) 121 | local last_width = buffer_utils.get_buffer_width(last_window.buffer_number) 122 | 123 | if last_width >= own_width then 124 | self:set_window_width(last_width) 125 | else 126 | for _, window in ipairs(self.window_stack) do 127 | if not window.is_anchor then 128 | window:set_window_width(own_width) 129 | end 130 | end 131 | end 132 | end 133 | end 134 | 135 | return StackingWindow 136 | -------------------------------------------------------------------------------- /lua/code_action_menu/windows/warning_message_window.lua: -------------------------------------------------------------------------------- 1 | local BaseWindow = require('code_action_menu.windows.base_window') 2 | 3 | local WarningMessageWindow = BaseWindow:new() 4 | 5 | function WarningMessageWindow:new() 6 | local instance = BaseWindow:new() 7 | setmetatable(instance, self) 8 | self.__index = self 9 | self.filetype = 'code-action-menu-warning-message' 10 | return instance 11 | end 12 | 13 | function WarningMessageWindow:get_content() 14 | return { 'No code actions available!' } 15 | end 16 | 17 | return WarningMessageWindow 18 | -------------------------------------------------------------------------------- /plugin/code_action_menu.vim: -------------------------------------------------------------------------------- 1 | command! CodeActionMenu lua require('code_action_menu').open_code_action_menu() 2 | -------------------------------------------------------------------------------- /syntax/code-action-menu-details.vim: -------------------------------------------------------------------------------- 1 | syntax match CodeActionMenuDetailsTitle '\%1l.*$' 2 | syntax keyword CodeActionMenuDetailsLabel Kind: 3 | syntax keyword CodeActionMenuDetailsLabel Name: 4 | syntax keyword CodeActionMenuDetailsLabel Preferred: nextgroup=CodeActionMenuDetailsPreferred skipwhite 5 | syntax keyword CodeActionMenuDetailsLabel Disabled: nextgroup=CodeActionMenuDetailsDisabled skipwhite 6 | syntax keyword CodeActionMenuDetailsLabel Changes: 7 | syntax match CodeActionMenuDetailsPreferred 'yes' contained 8 | syntax match CodeActionMenuDetailsDisabled 'yes.*' contained 9 | syntax keyword CodeActionMenuDetailsUndefined undefined 10 | 11 | highlight default link CodeActionMenuDetailsTitle Title 12 | highlight default link CodeActionMenuDetailsLabel Label 13 | highlight default link CodeActionMenuDetailsPreferred DiffAdd 14 | highlight default link CodeActionMenuDetailsDisabled Error 15 | highlight default link CodeActionMenuDetailsUndefined Comment 16 | -------------------------------------------------------------------------------- /syntax/code-action-menu-diff.vim: -------------------------------------------------------------------------------- 1 | syntax match CodeActionMenuDetailsCreatedFile '\*\S\+' 2 | syntax match CodeActionMenuDetailsChangedFile '\~\S\+' 3 | syntax match CodeActionMenuDetailsRenamedFile '\>\S\+' 4 | syntax match CodeActionMenuDetailsDeletedFile '\!\S\+' 5 | syntax match CodeActionMenuDetailsAddedLinesCount '(+\d\+' 6 | syntax match CodeActionMenuDetailsDeletedLinesCount '-\d\+)' 7 | syntax match CodeActionMenuDetailsAddedLine '^+\d\+\s.*$' 8 | syntax match CodeActionMenuDetailsDeletedLine '^-\d\+\s.*$' 9 | 10 | highlight default link CodeActionMenuDetailsCreatedFile DiffAdd 11 | highlight default link CodeActionMenuDetailsChangedFile DiffChange 12 | highlight default link CodeActionMenuDetailsRenamedFile DiffChange 13 | highlight default link CodeActionMenuDetailsDeletedFile DiffDelete 14 | highlight default link CodeActionMenuDetailsAddedLinesCount DiffAdd 15 | highlight default link CodeActionMenuDetailsDeletedLinesCount DiffDelete 16 | highlight default link CodeActionMenuDetailsAddedSquares CodeActionMenuDetailsAddedLinesCount 17 | highlight default link CodeActionMenuDetailsDeletedSquares CodeActionMenuDetailsDeletedLinesCount 18 | highlight default link CodeActionMenuDetailsNeutralSquares Comment 19 | highlight default link CodeActionMenuDetailsAddedLine DiffAdd 20 | highlight default link CodeActionMenuDetailsDeletedLine DiffDelete 21 | -------------------------------------------------------------------------------- /syntax/code-action-menu-menu.vim: -------------------------------------------------------------------------------- 1 | syntax match CodeActionMenuMenuIndex '^\s\[\d\+\]' nextgroup=CodeActionMenuMenuKind skipwhite 2 | syntax match CodeActionMenuMenuKind '(.\+)' contained nextgroup=CodeActionMenuMenuTitle skipwhite 3 | syntax match CodeActionMenuMenuTitle '.*' contained 4 | syntax match CodeActionMenuMenuDisabled '.*\[disabled\]' 5 | 6 | highlight default link CodeActionMenuMenuIndex Special 7 | highlight default link CodeActionMenuMenuKind Type 8 | highlight default link CodeActionMenuMenuTitle Normal 9 | highlight default link CodeActionMenuMenuDisabled Comment 10 | highlight default link CodeActionMenuMenuSelection QuickFixLine 11 | -------------------------------------------------------------------------------- /syntax/code-action-menu-warning-message.vim: -------------------------------------------------------------------------------- 1 | syntax match CodeActionMenuWarningMessageText '.*' 2 | 3 | highlight default link CodeActionMenuWarningMessageText WarningMsg 4 | highlight default link CodeActionMenuWarningMessageBorder WarningMsg 5 | --------------------------------------------------------------------------------