├── .busted ├── .editorconfig ├── .gitignore ├── .luacheckrc ├── .pre-commit-config.yaml ├── Makefile ├── README.md ├── doc └── nvim-sluice.txt ├── lua ├── sluice.lua └── sluice │ ├── commands.lua │ ├── config.lua │ ├── convert.lua │ ├── debounce.lua │ ├── gutter.lua │ ├── highlight.lua │ ├── integrations │ ├── counters.lua │ ├── extmark_signs.lua │ ├── matchlist.lua │ ├── search.lua │ ├── signs.lua │ └── viewport.lua │ ├── luaxxhash.lua │ └── window.lua ├── plugin └── sluice.vim ├── requirements-dev.txt ├── static └── screenshot.png └── tests └── plenary └── sluice ├── config_spec.lua ├── convert_spec.lua ├── highlight_spec.lua ├── integrations ├── counters_spec.lua ├── extmark_signs_spec.lua └── signs_spec.lua └── window_spec.lua /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | _all = { 3 | lpath = "lua/?.lua" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | charset = utf-8 10 | 11 | [{Makefile,**/Makefile,runtime/doc/*.txt}] 12 | indent_style = tab 13 | indent_size = 8 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .aider* 2 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | -- vim: ft=lua tw=80 2 | 3 | -- Global objects defined by the C code 4 | read_globals = { 5 | "vim", "it", "describe" 6 | } 7 | 8 | std = "lua51" 9 | files["tests/**/*_spec.lua"].std = "+busted" 10 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: make-test 5 | name: Make Test 6 | entry: make test 7 | language: system 8 | files: '.*' 9 | stages: [commit] 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | nvim --headless -c "PlenaryBustedDirectory tests/plenary/" 3 | 4 | lint: 5 | luacheck lua/* 6 | 7 | .PHONY: test lint 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-sluice 2 | 3 | Sluice: 4 | 5 | > A trough to channel water from a distance. 6 | 7 | A neovim plugin that provides vertical channels along the sides of your window: 8 | - minimap of the +signs and extmarks for the side of the window. 9 | - replacement for the signcolumn: customize the width and contents of the column to your liking! 10 | 11 | ## Install 12 | 13 | You can install this plugin using a variety of plugin managers. 14 | 15 | Plug: 16 | 17 | ``` 18 | Plug 'dsummersl/nvim-sluice' 19 | ``` 20 | 21 | Lazy: 22 | 23 | ``` 24 | { 25 | "dsummersl/nvim-sluice", 26 | config = function() 27 | require("sluice").setup({ 28 | ... override any defaults ... 29 | }) 30 | end 31 | }, 32 | 33 | ``` 34 | 35 | 36 | Default configuration: 37 | 38 | ```vim 39 | { 40 | enable = true, 41 | gutters = { { 42 | plugins = { "viewport", "search" }, 43 | window = { 44 | default_gutter_hl = "SluiceColumn", 45 | enabled_fn = 'sluice.config.default_enabled_fn', 46 | width = 1 47 | } 48 | }, { 49 | plugins = { "viewport", "signs" }, 50 | window = { 51 | count_method = "", 52 | default_gutter_hl = "SluiceColumn", 53 | enabled_fn = 'sluice.config.default_enabled_fn', 54 | width = 1 55 | } 56 | } }, 57 | throttle_ms = 150 58 | } 59 | ``` 60 | 61 | ## Configuration 62 | 63 | You can configure nvim-sluice to create custom gutters on either the left or the right side of the screen. Each gutter can be configured to display specific symbols and have a custom width. You can also specify which groups to include or exclude from the gutter. 64 | 65 | Example configuration with left and right gutters: 66 | 67 | ```vim 68 | { 69 | enable = true, 70 | gutters = { 71 | left = { -- Define a gutter on the left side 72 | plugins = { "gitsigns", "lsp" }, 73 | window = { 74 | default_gutter_hl = "SluiceGutter", 75 | enabled_fn = , 76 | width = 2, 77 | whitelist = { "GitSignsAdd", "GitSignsChange", "LspDiagnosticsSignError" }, 78 | blacklist = { "GitSignsDelete" } 79 | } 80 | }, 81 | right = { -- Define a gutter on the right side (existing functionality) 82 | plugins = { "viewport", "search" }, 83 | window = { 84 | default_gutter_hl = "SluiceColumn", 85 | enabled_fn = , 86 | width = 1 87 | } 88 | } 89 | }, 90 | throttle_ms = 150 91 | } 92 | ``` 93 | 94 | In the above example, the left gutter is configured to show signs from gitsigns and LSP messages, with a width of 2 cells. It includes only the specified whitelist groups and excludes the blacklist groups. The right gutter remains as previously configured. 95 | 96 | ## Screenshot 97 | 98 | [![asciicast](./static/screenshot.png)](https://asciinema.org/a/QXQfhGBm5Zlx1R2oYQkgQfYVu?t=10) 99 | 100 | See this [asciinema screencast](https://asciinema.org/a/QXQfhGBm5Zlx1R2oYQkgQfYVu?t=10) for a demonstration. 101 | 102 | ## Commands 103 | 104 | `SluiceEnable`/`SluiceDisable`/`SluiceToggle`. 105 | 106 | ## Development 107 | 108 | Run tests: 109 | 110 | make test 111 | 112 | Pre commit hooks: 113 | 114 | pip install requirements-dev.txt 115 | pre-commit install 116 | 117 | ## Notes 118 | 119 | Thanks to [nvim-treesitter-context](https://github.com/romgrk/nvim-treesitter-context) which I based the lua windowing that this plugin uses. 120 | 121 | The idea behind this project is based on [vim-sluice](https://github.com/dsummersl/vim-sluice) -- a buggier and more feature-ful version of this plugin for vim/gvim. 122 | 123 | ## Features 124 | 125 | With the new configuration options, you can: 126 | 127 | - Define gutters on both the left and right sides of the screen. 128 | - Configure the symbols and width of each gutter. 129 | - Whitelist or blacklist specific highlight groups to fine-tune what is displayed in the gutters. 130 | - Create dedicated gutters for specific plugins like gitsigns or LSP messages. 131 | 132 | These features provide greater flexibility in how you view and interact with different signs and messages within Neovim. 133 | 134 | - https://github.com/lewis6991/satellite.nvim -- a good inspiration for a reboot from 135 | -------------------------------------------------------------------------------- /doc/nvim-sluice.txt: -------------------------------------------------------------------------------- 1 | *nvim-sluice.txt* View +signs for the entire buffer on the right side of the window. 2 | 3 | 4 | View +signs for the entire buffer on the right side of the window. 5 | 6 | ============================================================================== 7 | CONTENTS *NvimSluiceContents* 8 | 9 | 1. Intro ...................... |NvimSluiceIntro| 10 | 2.1 Commands .................. |NvimSluiceCommands| 11 | 2.1 :SluiceEnable ............... |:SluiceEnable| 12 | 2.2 :SluiceDisable .............. |:SluiceDisable| 13 | 2.3 :SluiceToggle ............... |:SluiceToggle| 14 | 3. Configuration .............. |NvimSluiceConfig| 15 | 3.1 SluiceViewportVisibleArea ........... |hl-SluiceViewportVisibleArea| 16 | 3.2 SluiceViewportCursor ................ |hl-SluiceViewportCursor| 17 | 3.3 SluiceColumn ................ |hl-SluiceColumn| 18 | 4. License .................... |NvimSluiceLicense| 19 | 5. Credits .................... |NvimSluiceCredits| 20 | 21 | ============================================================================== 22 | 1. Intro *NvimSluiceIntro* 23 | 24 | Sluice provides a minimap of the vim :signs column, on the right side of your 25 | window. 26 | 27 | ============================================================================== 28 | 2. Commands *NvimSluiceCommands* 29 | 30 | ------------------------------------------------------------------------------ 31 | 2.1 :SluiceEnable *:SluiceEnable* 32 | 33 | Open the Slice signs window. 34 | 35 | Usage: 36 | > 37 | :SluiceEnable 38 | < 39 | ------------------------------------------------------------------------------ 40 | 2.2 :SluiceDisable *:SluiceDisable* 41 | 42 | Close the Sluice signs window. 43 | 44 | Usage: 45 | > 46 | :SluiceDisable 47 | 48 | ------------------------------------------------------------------------------ 49 | 2.3 :SluiceToggle *:SluiceToggle* 50 | 51 | Toggle the Slice window off and on. 52 | 53 | Usage: 54 | > 55 | :SluiceToggle 56 | 57 | ============================================================================== 58 | 3. Configuration *NvimSluiceConfig* 59 | 60 | Sluice can be configured by calling the setup function with a table of options. 61 | Here's an example of how to configure Sluice with its default settings: 62 | 63 | > 64 | require('sluice').setup({ 65 | enable = true, 66 | throttle_ms = 150, 67 | gutters = { 68 | { 69 | plugins = { 'viewport', 'search' }, 70 | window = { 71 | width = 1, 72 | default_gutter_hl = 'SluiceColumn', 73 | enabled_fn = function that checks if search has results, 74 | count_method = 'horizontal_block', 75 | }, 76 | }, 77 | { 78 | plugins = { 'viewport', 'signs', 'extmark_signs' }, 79 | window = { 80 | width = 1, 81 | default_gutter_hl = 'SluiceColumn', 82 | enabled_fn = default_enabled_fn, 83 | count_method = '', 84 | }, 85 | }, 86 | }, 87 | }) 88 | < 89 | 90 | ------------------------------------------------------------------------------ 91 | 3.1 Global Options *NvimSluiceGlobalOptions* 92 | 93 | `enable` *NvimSluiceEnable* 94 | Type: boolean 95 | Default: `true` 96 | Enable or disable Sluice globally. 97 | 98 | `throttle_ms` *NvimSluiceThrottleMs* 99 | Type: number 100 | Default: `150` 101 | The number of milliseconds to wait before updating the Sluice window. 102 | 103 | ------------------------------------------------------------------------------ 104 | 3.2 Gutter Options *NvimSluiceGutterOptions* 105 | 106 | Sluice supports multiple gutters, each with its own configuration. 107 | 108 | `plugins` *NvimSluiceGutterPlugins* 109 | Type: table of strings 110 | Default: `{ 'viewport', 'search' }` for the first gutter, 111 | `{ 'viewport', 'signs', 'extmark_signs' }` for the second gutter 112 | The plugins to use for this gutter. 113 | 114 | `window` *NvimSluiceGutterWindow* 115 | Type: table 116 | Configuration for the gutter window. 117 | 118 | `width` *NvimSluiceGutterWindowWidth* 119 | Type: number 120 | Default: `1` 121 | The width of the gutter window. 122 | 123 | `default_gutter_hl` *NvimSluiceGutterWindowDefaultGutterHl* 124 | Type: string 125 | Default: `'SluiceColumn'` 126 | The default highlight group for the gutter. 127 | 128 | `enabled_fn` *NvimSluiceGutterWindowEnabledFn* 129 | Type: function 130 | Default: `default_enabled_fn` or a function that checks if search has results 131 | A function that determines whether the gutter should be displayed. 132 | 133 | `count_method` *NvimSluiceGutterWindowCountMethod* 134 | Type: string or function 135 | Default: `'horizontal_block'` for the first gutter, `''` for the second 136 | The method to use for displaying counts in the gutter. 137 | 138 | ------------------------------------------------------------------------------ 139 | 3.3 Highlight Groups *NvimSluiceHighlightGroups* 140 | 141 | The Sluice plugin uses the following highlight groups. If you define these 142 | highlights they will override the default values. 143 | 144 | ------------------------------------------------------------------------------ 145 | 3.3.1 hl-SluiceViewportVisibleArea *hl-SluiceViewportVisibleArea* 146 | 147 | The highlight style of the Sluice window corresponding to the visible area of 148 | the screen. Default: 149 | 150 | > 151 | hi link SluiceViewportVisibleArea Normal 152 | < 153 | 154 | ------------------------------------------------------------------------------ 155 | 3.3.2 hl-SluiceViewportCursor *hl-SluiceViewportCursor* 156 | 157 | The highlight of the location of the cursor within the file as seen in the 158 | Sluice window. Default: 159 | 160 | > 161 | hi link SluiceViewportCursor Normal 162 | < 163 | 164 | For instance, if you want the position of the cursor to be visible in the 165 | Signs window you could set a custom cursor highlight: 166 | 167 | > 168 | hi link SluiceViewportCursor CursorLine 169 | < 170 | 171 | ------------------------------------------------------------------------------ 172 | 3.3.3 hl-SluiceColumn *hl-SluiceColumn* 173 | 174 | The highlight of the entire Sluice window (height of the window). Default: 175 | 176 | > 177 | hi link SluiceColumn SignColumn 178 | < 179 | 180 | ============================================================================== 181 | 4. License *NvimSluiceLicense* 182 | 183 | Released under the MIT License. 184 | 185 | ============================================================================== 186 | 5. Credits *NvimSluiceCredits* 187 | 188 | Thanks to 189 | [nvim-treesitter-context](https://github.com/romgrk/nvim-treesitter-context) 190 | which I based the lua windowing that this plugin uses. 191 | 192 | The idea behind this project is based on an older plugin I wrote called 193 | [vim-sluice](https://github.com/dsummersl/vim-sluice) -- it was a buggier and 194 | more feature-ful version of this plugin for vim/gvim. 195 | 196 | " vim: ft=help 197 | -------------------------------------------------------------------------------- /lua/sluice.lua: -------------------------------------------------------------------------------- 1 | local M = { 2 | vim = vim 3 | } 4 | 5 | M.setup = function(settings) 6 | local config = require('sluice.config') 7 | config.apply_user_settings(settings) 8 | 9 | M.vim.api.nvim_command('command! SluiceEnable lua require("sluice.commands").enable()') 10 | M.vim.api.nvim_command('command! SluiceDisable lua require("sluice.commands").disable()') 11 | M.vim.api.nvim_command('command! SluiceToggle lua require("sluice.commands").toggle()') 12 | 13 | if config.settings.enable then 14 | require("sluice.commands").enable() 15 | else 16 | require("sluice.commands").disable() 17 | end 18 | end 19 | 20 | M.setup() 21 | 22 | return M 23 | -------------------------------------------------------------------------------- /lua/sluice/commands.lua: -------------------------------------------------------------------------------- 1 | local M = { 2 | vim = vim, 3 | enabled = false 4 | } 5 | 6 | local gutter = require('sluice.gutter') 7 | local debounce = require('sluice.debounce') 8 | 9 | --- Assign autocmds for a group. 10 | local nvim_augroup = function(group_name, definitions) 11 | M.vim.api.nvim_command('augroup ' .. group_name) 12 | M.vim.api.nvim_command('autocmd!') 13 | for _, def in ipairs(definitions) do 14 | local command = table.concat({'autocmd', unpack(def)}, ' ') 15 | if M.vim.api.nvim_call_function('exists', {'##' .. def[1]}) ~= 0 then 16 | M.vim.api.nvim_command(command) 17 | end 18 | end 19 | M.vim.api.nvim_command('augroup END') 20 | end 21 | 22 | local function update_context() 23 | if not M.enabled then return end 24 | 25 | gutter.open() 26 | end 27 | 28 | M.update_context = debounce(update_context, 100) 29 | 30 | function M.enable() 31 | if M.enabled then return end 32 | 33 | M.enabled = true 34 | 35 | nvim_augroup('sluice', { 36 | {'DiagnosticChanged', '*', 'lua require("sluice.commands").update_context()'}, 37 | {'WinScrolled', '*', 'lua require("sluice.commands").update_context()'}, 38 | {'CursorMoved', '*', 'lua require("sluice.commands").update_context()'}, 39 | {'CursorHold', '*', 'lua require("sluice.commands").update_context()'}, 40 | {'CursorHoldI', '*', 'lua require("sluice.commands").update_context()'}, 41 | {'BufEnter', '*', 'lua require("sluice.commands").update_context()'}, 42 | {'WinEnter', '*', 'lua require("sluice.commands").update_context()'}, 43 | {'VimResized', '*', 'lua require("sluice.commands").update_context()'}, 44 | }) 45 | 46 | M.update_context() 47 | end 48 | 49 | function M.disable() 50 | if not M.enabled then return end 51 | 52 | M.enabled = false 53 | 54 | nvim_augroup('sluice', {}) 55 | 56 | gutter.close() 57 | end 58 | 59 | function M.toggle() 60 | if M.enabled then 61 | M.disable() 62 | else 63 | M.enable() 64 | end 65 | end 66 | 67 | return M 68 | -------------------------------------------------------------------------------- /lua/sluice/config.lua: -------------------------------------------------------------------------------- 1 | local counters = require('sluice.integrations.counters') 2 | 3 | local M = { 4 | vim = vim 5 | } 6 | 7 | --- Utility function to check if a value matches a string, is in a table, or passes a function test 8 | -- @param obj string|table|function The object to check against 9 | -- @param value any The value to check 10 | -- @return boolean True if the value matches, is in the table, or passes the function test 11 | function M.str_table_fn(obj, value) 12 | if type(obj) == "string" then 13 | return string.match(value, obj) ~= nil 14 | elseif type(obj) == "table" then 15 | for _, v in ipairs(obj) do 16 | if type(v) == "string" and string.match(value, v) then 17 | return true 18 | elseif v == value then 19 | return true 20 | end 21 | end 22 | return false 23 | elseif type(obj) == "function" then 24 | return obj(value) 25 | end 26 | return false 27 | end 28 | 29 | --- Whether to display the gutter or not. 30 | -- 31 | -- Returns boolean indicating whether the gutter is shown on screen or not. 32 | -- 33 | -- Show the gutter if: 34 | -- - the buffer is not smaller than the window 35 | -- - the buffer is not a special &buftype 36 | -- - the buffer is not a &previewwindow 37 | -- - the buffer is not a &diff 38 | function M.default_enabled_fn(_gutter) 39 | local win_height = M.vim.api.nvim_win_get_height(0) 40 | local buf_lines = M.vim.api.nvim_buf_line_count(0) 41 | if win_height >= buf_lines then 42 | return false 43 | end 44 | if M.vim.fn.getwinvar(0, '&buftype') ~= '' then 45 | return false 46 | end 47 | if M.vim.fn.getwinvar(0, '&previewwindow') ~= 0 then 48 | return false 49 | end 50 | if M.vim.fn.getwinvar(0, '&diff') ~= 0 then 51 | return false 52 | end 53 | 54 | return true 55 | end 56 | 57 | --- Create an enable_fn function that returns true if a specific plugin has contributed lines to the gutter. 58 | function M.make_has_results_fn(plugin) 59 | local function has_results_fn(gutter) 60 | if not M.default_enabled_fn() then 61 | return false 62 | end 63 | 64 | for _, line in pairs(gutter.lines) do 65 | if line.plugin == plugin then 66 | return true 67 | end 68 | end 69 | 70 | return false 71 | end 72 | 73 | return has_results_fn 74 | end 75 | 76 | local default_gutter_settings = { 77 | plugins = { 'viewport' }, 78 | window = { 79 | --- Width of the gutter. 80 | width = 1, 81 | 82 | --- Default highlight to use in the gutter. 83 | -- This serves as the base linehl highlight for a column in each gutter. Plugins can 84 | -- override parts of this highlight (typically this is the background color of 85 | -- areas represented in the gutter of offscreen content) 86 | default_gutter_hl = 'SluiceColumn', 87 | 88 | --- Whether to display the gutter or not. 89 | enabled_fn = M.default_enabled_fn, 90 | 91 | --- When there are many matches in an area, how to show the number. Set to 'nil' to disable. 92 | count_method = nil, 93 | 94 | --- Layout of the gutter. Can be 'left' or 'right'. 95 | layout = 'right', 96 | 97 | --- Render method for the gutter. Can be 'macro' or 'line'. 98 | render_method = 'macro', 99 | }, 100 | } 101 | 102 | local apply_gutter_settings = function(gutters) 103 | local result = {} 104 | for _, gutter in ipairs(gutters) do 105 | table.insert(result, M.vim.tbl_deep_extend('keep', gutter or {}, default_gutter_settings)) 106 | end 107 | return result 108 | end 109 | 110 | local default_settings = { 111 | enable = true, 112 | throttle_ms = 150, 113 | gutters = apply_gutter_settings{ 114 | { 115 | plugins = { 'viewport', 'search' }, 116 | window = { 117 | enabled_fn = M.make_has_results_fn('search'), 118 | count_method = counters.methods.horizontal_block, 119 | }, 120 | }, 121 | { 122 | plugins = { 'viewport', 'signs', 'extmark_signs' }, 123 | window = { 124 | count_method = '', 125 | }, 126 | extmarks = { 127 | hl_groups = '.*' 128 | }, 129 | signs = { 130 | -- TODO rename to groups? 131 | group = '.*' 132 | } 133 | }, 134 | } 135 | } 136 | 137 | function M.apply_user_settings(user_settings) 138 | if user_settings ~= nil then 139 | M.vim.validate({ user_settings = { user_settings, 'table', true} }) 140 | 141 | -- Validate global options 142 | if user_settings.enable ~= nil then 143 | M.vim.validate({ enable = { user_settings.enable, 'boolean' } }) 144 | end 145 | if user_settings.throttle_ms ~= nil then 146 | M.vim.validate({ throttle_ms = { user_settings.throttle_ms, 'number' } }) 147 | end 148 | 149 | -- Validate gutters 150 | if user_settings.gutters ~= nil then 151 | M.vim.validate({ gutters = { user_settings.gutters, 'table' } }) 152 | for i, gutter in ipairs(user_settings.gutters) do 153 | M.vim.validate({ 154 | ['gutters[' .. i .. ']'] = { gutter, 'table' }, 155 | ['gutters[' .. i .. '].plugins'] = { gutter.plugins, 'table', true }, 156 | }) 157 | if gutter.window ~= nil then 158 | M.vim.validate({ 159 | ['gutters[' .. i .. '].window'] = { gutter.window, 'table' }, 160 | ['gutters[' .. i .. '].window.width'] = { gutter.window.width, 'number', true }, 161 | ['gutters[' .. i .. '].window.default_gutter_hl'] = { gutter.window.default_gutter_hl, 'string', true }, 162 | ['gutters[' .. i .. '].window.enabled_fn'] = { gutter.window.enabled_fn, 'function', true }, 163 | ['gutters[' .. i .. '].window.count_method'] = { gutter.window.count_method, {'table'}, true }, 164 | ['gutters[' .. i .. '].window.layout'] = { gutter.window.layout, 'string', true }, 165 | ['gutters[' .. i .. '].window.render_method'] = { gutter.window.render_method, 'string', true }, 166 | }) 167 | if gutter.window.layout ~= nil and gutter.window.layout ~= 'left' and gutter.window.layout ~= 'right' then 168 | error("gutters[" .. i .. "].window.layout must be 'left' or 'right'") 169 | end 170 | if gutter.window.render_method ~= nil and gutter.window.render_method ~= 'macro' and gutter.window.render_method ~= 'line' then 171 | error("gutters[" .. i .. "].window.render_method must be 'macro' or 'line'") 172 | end 173 | end 174 | end 175 | end 176 | end 177 | 178 | M.settings = M.vim.tbl_deep_extend('force', M.vim.deepcopy(default_settings), user_settings or {}) 179 | if user_settings ~= nil and user_settings.gutters ~= nil then 180 | M.settings.gutters = apply_gutter_settings(user_settings.gutters) 181 | end 182 | end 183 | 184 | M.settings = default_settings 185 | 186 | return M 187 | -------------------------------------------------------------------------------- /lua/sluice/convert.lua: -------------------------------------------------------------------------------- 1 | local M = { 2 | vim = vim 3 | } 4 | 5 | --- Convert a line in the file, to the corresponding line in the gutter's 6 | --- window. 7 | function M.line_to_gutter_line(line, buffer_lines, height, top_line_number) 8 | if line < top_line_number or line > top_line_number + height then 9 | return 0 10 | end 11 | 12 | return line - top_line_number + 1 13 | end 14 | 15 | --- Convert a line in the file, to the corresponding line in the gutter's 16 | --- window. This only converts the number relative to the total number of lines 17 | --- in the file (macro mode) 18 | function M.line_to_gutter_line_macro(line, buffer_lines, height, cursor_position) 19 | local gutter_line = math.floor(line / buffer_lines * height) 20 | if gutter_line == 0 then 21 | return 1 22 | end 23 | 24 | return gutter_line 25 | end 26 | 27 | --- Look for a highlight definition. 28 | function M.find_definition(definitions, name) 29 | for _, v in ipairs(definitions) do 30 | if v["name"] == name then 31 | return v 32 | end 33 | end 34 | 35 | return nil 36 | end 37 | 38 | --- Convert a list of lines/styles to a list of gutter lines. 39 | -- @param lines A list of dicts with any keys from :highlight, plus text/line. 40 | -- @returns A list of dicts (of all plugin entries) for each gutter line. 41 | function M.lines_to_gutters(settings, lines, buffer_lines, height, top_line_number) 42 | -- ensure that each line of the gutter has a definition. 43 | local gutter_lines = {} 44 | for line = 1, height do 45 | gutter_lines[line] = {{ texthl = "", linehl = settings.window.default_gutter_hl, text = " " }} 46 | end 47 | 48 | -- drop in all the lines provided by an integration. 49 | for _, line in ipairs(lines) do 50 | local gutter_line_number = 0 51 | if settings.window.render_method == "macro" then 52 | gutter_line_number = M.line_to_gutter_line_macro(line['lnum'], buffer_lines, height, top_line_number) 53 | else 54 | gutter_line_number = M.line_to_gutter_line(line['lnum'], buffer_lines, height, top_line_number) 55 | end 56 | if not (gutter_line_number < 1 or gutter_line_number > height) then 57 | table.insert(gutter_lines[gutter_line_number], line) 58 | end 59 | end 60 | 61 | return gutter_lines 62 | end 63 | 64 | --- 65 | function M.lines_to_gutter_lines(settings, lines) 66 | local win_height = M.vim.api.nvim_win_get_height(0) 67 | local buf_lines = M.vim.api.nvim_buf_line_count(0) 68 | local top_line_number = M.vim.fn.line('w0') 69 | 70 | if win_height >= buf_lines then 71 | return {} 72 | end 73 | 74 | return M.lines_to_gutters(settings, lines, buf_lines, win_height, top_line_number) 75 | end 76 | 77 | return M 78 | -------------------------------------------------------------------------------- /lua/sluice/debounce.lua: -------------------------------------------------------------------------------- 1 | local function debounce(func, delay) 2 | local timer_id = nil 3 | return function(...) 4 | local args = { ... } 5 | if not timer_id then 6 | func(unpack(args)) 7 | else 8 | vim.fn.timer_stop(timer_id) 9 | timer_id = vim.fn.timer_start(delay, function() 10 | func(unpack(args)) 11 | timer_id = nil 12 | end) 13 | end 14 | end 15 | end 16 | 17 | return debounce 18 | -------------------------------------------------------------------------------- /lua/sluice/gutter.lua: -------------------------------------------------------------------------------- 1 | local M = { 2 | vim = vim, 3 | gutters = nil, 4 | gutter_lines = {}, 5 | } 6 | 7 | local config = require('sluice.config') 8 | local window = require('sluice.window') 9 | local convert = require('sluice.convert') 10 | 11 | --- Update the gutter with new lines. 12 | function M.update(gutter, lines) 13 | local gutter_settings = config.settings.gutters[gutter.index] 14 | local gutter_lines = convert.lines_to_gutter_lines(gutter_settings, lines) 15 | M.vim.schedule(function() 16 | window.set_gutter_lines(gutter.bufnr, gutter_lines, gutter_settings.window.count_method, gutter_settings.window.width) 17 | window.refresh_highlights(gutter.bufnr, gutter.ns, gutter_lines) 18 | end) 19 | M.gutter_lines[gutter.bufnr] = gutter_lines 20 | end 21 | 22 | --- Get all integration lines for a gutter 23 | function M.get_lines(gutter) 24 | local bufnr = M.vim.fn.bufnr() 25 | local lines = {} 26 | local gutter_settings = config.settings.gutters[gutter.index] 27 | for _, plugin in ipairs(gutter_settings.plugins) do 28 | local enable_fn = nil 29 | local update_fn = nil 30 | 31 | if type(plugin) == "string" then 32 | -- when there is an integration, load it, and enable it. 33 | plugin = require('sluice.integrations.' .. plugin) 34 | end 35 | 36 | if plugin.enable ~= nil then 37 | enable_fn = plugin.enable 38 | end 39 | if plugin.update ~= nil then 40 | update_fn = plugin.update 41 | end 42 | 43 | if enable_fn ~= nil then 44 | enable_fn(gutter_settings, bufnr) 45 | end 46 | 47 | if update_fn == nil then 48 | print("No update function for gutter") 49 | -- TODO close the gutter 50 | return 51 | end 52 | 53 | -- TODO the update_fn of an integration (ie, viewport) is called multiple times when there are multiple 54 | -- gutters using it - this doesn't need to happen. We should be able to: 55 | -- - obtain a list of all integrations configured for all the gutters 56 | -- - call each integration once 57 | -- - pass these results to this function and use the values rather than recompute them 58 | -- 59 | -- honestly though, rather than computing the lines for each gutter and 60 | -- storing that value, it would be wiser to store them by integration. And 61 | -- then coalesce them into the gutter lines at the time of rendering. 62 | local integration_lines = update_fn(gutter_settings, bufnr) 63 | for _, il in ipairs(integration_lines) do 64 | table.insert(lines, il) 65 | end 66 | end 67 | 68 | return lines 69 | end 70 | 71 | --- Create initial gutter settings 72 | function M.init_gutters(config) 73 | local gutters = {} 74 | for i, v in ipairs(config.settings.gutters) do 75 | gutters[i] = { 76 | index = i, 77 | enabled = v.enabled 78 | } 79 | end 80 | 81 | return gutters 82 | end 83 | 84 | --- Open all gutters configured for this plugin. 85 | function M.open() 86 | -- if M.should_throttle() then 87 | -- return 88 | -- end 89 | 90 | -- TODO we need some better way to init the gutters but only minimally? 91 | if M.gutters == nil or #M.gutters ~= #config.settings.gutters then 92 | M.gutters = M.init_gutters(config) 93 | end 94 | 95 | for i, gutter_settings in ipairs(config.settings.gutters) do 96 | local gutter = M.gutters[i] 97 | gutter.lines = M.get_lines(gutter) 98 | gutter.enabled = gutter_settings.window.enabled_fn(gutter) 99 | end 100 | 101 | M.vim.schedule(function() 102 | for i, _ in ipairs(config.settings.gutters) do 103 | if M.gutters[i].enabled then 104 | M.open_gutter(i) 105 | else 106 | M.close_gutter(M.gutters[i]) 107 | end 108 | end 109 | end) 110 | end 111 | 112 | --- Open one gutter 113 | function M.open_gutter(gutter_index) 114 | local gutter = M.gutters[gutter_index] 115 | 116 | window.create_window(M.gutters, gutter_index) 117 | 118 | M.update(gutter, gutter.lines) 119 | end 120 | 121 | --- Close one gutter 122 | function M.close_gutter(gutter) 123 | -- Can't close other windows when the command-line window is open 124 | if M.vim.api.nvim_call_function('getcmdwintype', {}) ~= '' then 125 | return 126 | end 127 | 128 | local gutter_settings = config.settings.gutters[gutter.index] 129 | for _, plugin in ipairs(gutter_settings.plugins) do 130 | local disable_fn = nil 131 | if type(plugin) == "string" then 132 | -- when there is an integration, load it, and enable it. 133 | local integration = require('sluice.integrations.' .. plugin) 134 | disable_fn = integration.disable 135 | end 136 | if plugin.disable ~= nil then 137 | disable_fn = plugin.disable 138 | end 139 | 140 | if M.vim.fn.bufexists(gutter.bufnr) ~= 0 then 141 | disable_fn(gutter_settings, gutter.bufnr) 142 | end 143 | end 144 | 145 | if vim.fn.win_id2win(gutter.winid) ~= 0 then 146 | M.vim.api.nvim_win_close(gutter.winid, true) 147 | gutter.winid = nil 148 | end 149 | end 150 | 151 | function M.close() 152 | for _, gutter in ipairs(M.gutters) do 153 | M.close_gutter(gutter) 154 | end 155 | end 156 | 157 | return M 158 | -------------------------------------------------------------------------------- /lua/sluice/highlight.lua: -------------------------------------------------------------------------------- 1 | local xxhash32 = require('sluice.luaxxhash') 2 | 3 | local M = { 4 | vim = vim, 5 | } 6 | 7 | --- Create a new highlight from another. 8 | -- mode == gui or cterm boolean 9 | local function copy_highlight(highlight, is_gui_mode, override_bg) 10 | local mode = "cterm" 11 | if is_gui_mode then 12 | mode = "gui" 13 | end 14 | local properties = {} 15 | 16 | local attribs = { "bg", "fg", "sp" } 17 | for _, v in ipairs(attribs) do 18 | local attrib = M.vim.fn.synIDattr(M.vim.fn.synIDtrans(M.vim.fn.hlID(highlight)), v, mode) 19 | if attrib ~= "" then 20 | properties[mode .. v] = attrib 21 | end 22 | end 23 | 24 | local cterms = { "bold", "italic", "reverse", "inverse", "standout", "underline", "undercurl", 25 | "strikethrough" } 26 | local cterm_attribs = {} 27 | for _, v in ipairs(cterms) do 28 | local attrib = M.vim.fn.synIDattr(M.vim.fn.synIDtrans(M.vim.fn.hlID(highlight)), v, mode) 29 | if attrib ~= "" then 30 | table.insert(cterm_attribs, v) 31 | end 32 | end 33 | 34 | if override_bg ~= "" then 35 | properties[mode .. 'bg'] = override_bg 36 | end 37 | 38 | local cterm_vals = mode .. "=NONE" 39 | if #cterm_attribs > 0 then 40 | cterm_vals = mode .. "=" .. table.concat(cterm_attribs, ",") 41 | end 42 | 43 | local property_vals = "" 44 | for k, v in pairs(properties) do 45 | property_vals = property_vals .. " " .. k .. "=" .. v 46 | end 47 | 48 | local new_name = "Sluice" .. xxhash32(property_vals .. cterm_vals) 49 | local highlight_definition = "hi " .. new_name .. property_vals .. " " .. cterm_vals 50 | 51 | -- print("|highlight_definition = " .. M.vim.inspect(highlight_definition)) 52 | M.vim.api.nvim_exec(highlight_definition, false) 53 | 54 | return new_name 55 | end 56 | 57 | M.copy_highlight = copy_highlight 58 | 59 | return M 60 | -------------------------------------------------------------------------------- /lua/sluice/integrations/counters.lua: -------------------------------------------------------------------------------- 1 | local M = { } 2 | 3 | M.methods = { 4 | roman_lower = { 'ⅰ', 'ⅱ', 'ⅲ', 'ⅳ', 'ⅴ', 'ⅵ', 'ⅶ', 'ⅷ', 'ⅸ', 'ⅹ', 'ⅺ', 'ⅻ', '∞' }, 5 | roman_upper = { 'Ⅰ', 'Ⅱ', 'Ⅲ', 'Ⅳ', 'Ⅴ', 'Ⅵ', 'Ⅶ', 'Ⅷ', 'Ⅸ', 'Ⅹ', 'Ⅺ', 'Ⅻ', '∞' }, 6 | circle = { '①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⑪', '⑫', '⑬', '⑭', '⑮', '⑯', '⑰', '⑱', '⑲', '⑳', '∞' }, 7 | circle_2 = { '⑴', '⑵', '⑶', '⑷', '⑸', '⑹', '⑺', '⑻', '⑼', '⑽', '⑾', '⑿', '⒀', '⒁', '⒂', '⒃', '⒄', '⒅', '⒆', '⒇', '∞' }, 8 | braille = { '⠂', '⠃', '⠇', '⠧', '⠏', '⠟', '⠿', '⡿', '⣿', '*' }, 9 | -- TODO it'd be ideal if these actually represented the % of the screen that the lines represent, rather than just their count 10 | horizontal_block = { '▁', '▁', '▂', '▂', '▃', '▃', '▄', '▄', '▅', '▅', '▆', '▆', '▇', '▇', '█' }, 11 | vertical_block = { '▏', '▏', '▎', '▎', '▍', '▍', '▌', '▌', '▋', '▋', '▊', '▊', '▉', '▉', '█' }, 12 | } 13 | 14 | --- Functions that allow showing a value for a count of items in different ways 15 | function M.count(number, method) 16 | if number <= 0 then 17 | return ' ' 18 | end 19 | 20 | local max = #method 21 | if number >= max then 22 | return method[max] 23 | end 24 | 25 | return method[number] 26 | end 27 | 28 | return M 29 | -------------------------------------------------------------------------------- /lua/sluice/integrations/extmark_signs.lua: -------------------------------------------------------------------------------- 1 | local config = require('sluice.config') 2 | 3 | local M = { 4 | vim = vim 5 | } 6 | 7 | --- Returns all 'signs' in the extmark buffer 8 | function M.update(settings, bufnr) 9 | local hl_groups = settings.extmarks and settings.extmarks.hl_groups 10 | if not hl_groups then 11 | return {} 12 | end 13 | 14 | local extmarks = M.vim.api.nvim_buf_get_extmarks(bufnr, -1, 0, -1, {details = true}) 15 | 16 | local result = {} 17 | 18 | for _, mark in ipairs(extmarks) do 19 | local row = mark[2] 20 | local details = mark[4] 21 | if details['sign_hl_group'] ~= nil and config.str_table_fn(hl_groups, details['sign_hl_group']) then 22 | table.insert(result, { 23 | lnum = row + 1, 24 | text = details["sign_text"], 25 | texthl = details["sign_hl_group"], 26 | priority = details["priority"], 27 | plugin = 'extmark_signs', 28 | }) 29 | end 30 | end 31 | 32 | return result 33 | end 34 | 35 | function M.enable(_settings, _bufnr) 36 | -- TODO setup the listeners for this. 37 | -- Specific events to update on - DiagnosticChanged would be one 38 | end 39 | 40 | function M.disable(settings, _bufnr) 41 | end 42 | 43 | return M 44 | -------------------------------------------------------------------------------- /lua/sluice/integrations/matchlist.lua: -------------------------------------------------------------------------------- 1 | local M = { 2 | vim = vim 3 | } 4 | 5 | function M.update(settings, winid) 6 | local matchlist = matchlist() 7 | local bufnr = M.vim.fn.getwininfo(winid)[1] 8 | 9 | local lines_with_matches = {} 10 | 11 | local lines = M.vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) 12 | 13 | for lnum, line in ipairs(lines) do 14 | if M.vim.fn.match(line, pattern) ~= -1 then 15 | -- TODO settings - read them in. 16 | table.insert(lines_with_matches, { 17 | lnum = lnum, 18 | text = "/ ", 19 | texthl = "Comment", 20 | priority = 10, 21 | plugin = 'matchlist', 22 | }) 23 | end 24 | end 25 | 26 | -- return lines_with_matches 27 | local pattern = M.vim.fn.getreg('/') 28 | if pattern == '' or M.vim.v.hlsearch == 0 then 29 | return {} 30 | end 31 | 32 | if M.vim.o.ignorecase and not M.vim.o.ignorecase then 33 | pattern = '\\C' .. pattern 34 | end 35 | 36 | local lines_with_matches = {} 37 | 38 | local lines = M.vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) 39 | 40 | for lnum, line in ipairs(lines) do 41 | if M.vim.fn.match(line, pattern) ~= -1 then 42 | -- TODO settings - read them in. 43 | table.insert(lines_with_matches, { 44 | lnum = lnum, 45 | text = "/ ", 46 | texthl = "Comment", 47 | priority = 10, 48 | }) 49 | end 50 | end 51 | 52 | return lines_with_matches 53 | end 54 | 55 | 56 | function M.enable(settings, winid) 57 | end 58 | 59 | 60 | function M.disable(settings, winid) 61 | end 62 | 63 | return M 64 | -------------------------------------------------------------------------------- /lua/sluice/integrations/search.lua: -------------------------------------------------------------------------------- 1 | local M = { 2 | vim = vim 3 | } 4 | 5 | local default_settings = { 6 | match_hl = "SluiceSearchMatch", 7 | match_line_hl = "SluiceSearchMatchLine", 8 | } 9 | 10 | function M.update(settings, bufnr) 11 | local pattern = M.vim.fn.getreg('/') 12 | local current_line = M.vim.fn.getpos('.')[2] 13 | local update_settings = M.vim.tbl_deep_extend('keep', settings.search or {}, default_settings) 14 | 15 | if pattern == '' or M.vim.v.hlsearch == 0 then 16 | return {} 17 | end 18 | 19 | if M.vim.o.ignorecase and not M.vim.o.ignorecase then 20 | pattern = '\\C' .. pattern 21 | end 22 | 23 | local lines_with_matches = {} 24 | 25 | local lines = M.vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) 26 | 27 | for lnum, line in ipairs(lines) do 28 | if M.vim.fn.match(line, pattern) ~= -1 then 29 | local texthl = update_settings.match_hl 30 | if lnum == current_line then 31 | texthl = update_settings.match_line_hl 32 | end 33 | -- TODO settings - read them in. 34 | table.insert(lines_with_matches, { 35 | lnum = lnum, 36 | text = "—", 37 | texthl = texthl, 38 | priority = 10, 39 | plugin = 'search', 40 | }) 41 | end 42 | end 43 | 44 | return lines_with_matches 45 | end 46 | 47 | 48 | function M.enable(_settings, _bufnr) 49 | -- TODO shouldn't there be a way to make these go away on cursor move 50 | if M.vim.fn.hlexists('SluiceSearchMatch') == 0 then 51 | M.vim.cmd('hi link SluiceSearchMatch Comment') 52 | end 53 | if M.vim.fn.hlexists('SluiceSearchMatchLine') == 0 then 54 | M.vim.cmd('hi link SluiceSearchMatchLine Error') 55 | end 56 | end 57 | 58 | 59 | function M.disable(_settings, _bufnr) 60 | end 61 | 62 | 63 | return M 64 | -------------------------------------------------------------------------------- /lua/sluice/integrations/signs.lua: -------------------------------------------------------------------------------- 1 | local config = require('sluice.config') 2 | 3 | local M = { 4 | vim = vim 5 | } 6 | 7 | --- Get a table with keys set to the `name` of each sign that is defined. 8 | local function sign_getdefined() 9 | local get_defined = M.vim.fn.sign_getdefined() 10 | local signs_defined = {} 11 | for _, v in ipairs(get_defined) do 12 | signs_defined[v["name"]] = v 13 | end 14 | 15 | return signs_defined 16 | end 17 | 18 | --- Returns a table of signs, and whether they have changed since the last call to this method. 19 | function M.update(settings, bufnr) 20 | local get_defined = sign_getdefined() 21 | local group = (settings.signs and settings.signs.group) or '.*' 22 | local get_placed = M.vim.fn.sign_getplaced(bufnr, { group = '*' }) 23 | 24 | -- local new_hash = xxh32(M.vim.inspect(get_placed)) 25 | -- local _, old_hash = pcall(M.vim.api.nvim_buf_get_var, bufnr, 'sluice_last_defined') 26 | -- 27 | -- if new_hash == old_hash then 28 | -- return get_defined 29 | -- end 30 | -- M.vim.api.nvim_buf_set_var(bufnr, 'sluice_last_defined', new_hash) 31 | 32 | local result = {} 33 | for _, v in ipairs(get_placed[1]["signs"]) do 34 | if config.str_table_fn(group, v["name"]) and v["name"] ~= "" then 35 | local line = M.vim.tbl_extend('force', get_defined[v["name"]], v) 36 | line.plugin = 'signs' 37 | table.insert(result, line) 38 | end 39 | end 40 | 41 | return result 42 | end 43 | 44 | function M.enable(_settings, _bufnr) 45 | -- TODO setup the listeners for this. 46 | -- Specific events to update on - DiagnosticChanged would be one 47 | end 48 | 49 | function M.disable(settings, bufnr) 50 | -- TODO this cleanup should happen elsewhere. 51 | local lines = M.update(settings, bufnr) 52 | if not lines then 53 | for _, v in ipairs(lines) do 54 | if v["texthl"] == "" then 55 | local line_text_hl = v["linehl"] .. v["texthl"] 56 | M.vim.api.nvim_exec("hi clear " .. line_text_hl, false) 57 | end 58 | end 59 | end 60 | end 61 | 62 | return M 63 | -------------------------------------------------------------------------------- /lua/sluice/integrations/viewport.lua: -------------------------------------------------------------------------------- 1 | local M = { 2 | vim = vim, 3 | } 4 | 5 | local default_settings = { 6 | visible_area_hl = "SluiceViewportVisibleArea", 7 | cursor_hl = "SluiceViewportCursor", 8 | } 9 | 10 | function M.update(settings, _bufnr) 11 | local cursor_position = M.vim.api.nvim_win_get_cursor(0)[1] 12 | local update_settings = M.vim.tbl_deep_extend('keep', settings.viewport or {}, default_settings) 13 | 14 | local lines = {} 15 | for lnum = M.vim.fn.line('w0'), M.vim.fn.line('w$') do 16 | local linehl = update_settings.visible_area_hl 17 | local text = " " 18 | local priority = 0 19 | if lnum == cursor_position then 20 | linehl = update_settings.cursor_hl 21 | text = " " 22 | priority = 1 23 | end 24 | table.insert(lines, { 25 | text = text, 26 | linehl = linehl, 27 | lnum = lnum, 28 | priority = priority, 29 | plugin = 'viewport', 30 | }) 31 | end 32 | 33 | return lines 34 | end 35 | 36 | function M.enable(_settings, _bufnr) 37 | if M.vim.fn.hlexists('SluiceViewportVisibleArea') == 0 then 38 | M.vim.cmd('hi link SluiceViewportVisibleArea Normal') 39 | end 40 | if M.vim.fn.hlexists('SluiceViewportCursor') == 0 then 41 | M.vim.cmd('hi link SluiceViewportCursor Normal') 42 | end 43 | end 44 | 45 | function M.disable(_settings, _bufnr) 46 | end 47 | 48 | return M 49 | -------------------------------------------------------------------------------- /lua/sluice/luaxxhash.lua: -------------------------------------------------------------------------------- 1 | local ffi = require('ffi') 2 | local bit = require('bit') 3 | 4 | local rotl, xor, shr = bit.rol, bit.bxor, bit.rshift 5 | local uint32_t = ffi.typeof("uint32_t") 6 | 7 | -- Prime constants 8 | local P1 = uint32_t(0x9E3779B1) 9 | local P2 = uint32_t(0x85EBCA77) 10 | local P3 = (0xC2B2AE3D) 11 | local P4 = (0x27D4EB2F) 12 | local P5 = (0x165667B1) 13 | 14 | -- multiplication with modulo2 semantics 15 | -- see https://github.com/luapower/murmurhash3 16 | local function mmul(a, b) 17 | local type = 'uint32_t' 18 | return tonumber(ffi.cast(type, ffi.cast(type, a) * ffi.cast(type, b))) 19 | end 20 | 21 | local function xxhash32(data, len, seed) 22 | seed, len = seed or 0, len or #data 23 | local i,n = 0, 0 -- byte and word index 24 | local bytes = ffi.cast( 'const uint8_t*', data) 25 | local words = ffi.cast('const uint32_t*', data) 26 | 27 | local h32 28 | if len >= 16 then 29 | local limit = len - 16 30 | local v = ffi.new("uint32_t[4]") 31 | v[0], v[1] = seed + P1 + P2, seed + P2 32 | v[2], v[3] = seed, seed - P1 33 | while i <= limit do 34 | for j=0, 3 do 35 | v[j] = v[j] + words[n] * P2 36 | v[j] = rotl(v[j], 13); v[j] = v[j] * P1 37 | i = i + 4; n = n + 1 38 | end 39 | end 40 | h32 = rotl(v[0], 1) + rotl(v[1], 7) + rotl(v[2], 12) + rotl(v[3], 18) 41 | else 42 | h32 = seed + P5 43 | end 44 | h32 = h32 + len 45 | 46 | local limit = len - 4 47 | while i <= limit do 48 | h32 = (h32 + mmul(words[n], P3)) 49 | h32 = mmul(rotl(h32, 17), P4) 50 | i = i + 4; n = n + 1 51 | end 52 | 53 | while i < len do 54 | h32 = h32 + mmul(bytes[i], P5) 55 | h32 = mmul(rotl(h32, 11), P1) 56 | i = i + 1 57 | end 58 | 59 | h32 = xor(h32, shr(h32, 15)) 60 | h32 = mmul(h32, P2) 61 | h32 = xor(h32, shr(h32, 13)) 62 | h32 = mmul(h32, P3) 63 | return tonumber(ffi.cast("uint32_t", xor(h32, shr(h32, 16)))) 64 | end 65 | 66 | return xxhash32 67 | -------------------------------------------------------------------------------- /lua/sluice/window.lua: -------------------------------------------------------------------------------- 1 | local highlight = require('sluice.highlight') 2 | local counters = require('sluice.integrations.counters') 3 | 4 | local M = { 5 | vim = vim, 6 | } 7 | 8 | --- Find the best match, ordered by priority. 9 | -- @param matches List of matches from plugins. 10 | -- @param optional key to prioritize by (beyond priority). 11 | function M.find_best_match(matches, key) 12 | local best_match = nil 13 | for _, match in ipairs(matches) do 14 | if best_match == nil then 15 | best_match = match 16 | else 17 | if not (key ~= nil and match[key] == nil) then 18 | if best_match == nil then 19 | best_match = match 20 | elseif best_match.priority == nil then 21 | best_match = match 22 | elseif best_match.priority ~= nil and match.priority ~= nil and match.priority > best_match.priority then 23 | best_match = match 24 | end 25 | end 26 | end 27 | end 28 | 29 | return best_match 30 | end 31 | 32 | --- Add styling to the gutter. 33 | function M.refresh_highlights(bufnr, ns, lines) 34 | M.vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) 35 | for i, matches in ipairs(lines) do 36 | local best_texthl_match = M.find_best_match(matches, "texthl") 37 | local best_linehl_match = M.find_best_match(matches, "linehl") 38 | local best_linehl = nil 39 | local best_texthl = nil 40 | if best_linehl_match ~= nil then 41 | best_linehl = best_linehl_match.linehl 42 | end 43 | if best_texthl_match ~= nil then 44 | best_texthl = best_texthl_match.texthl 45 | end 46 | 47 | if best_texthl ~= nil then 48 | local mode = "cterm" 49 | if vim.o.termguicolors then 50 | mode = "gui" 51 | end 52 | if best_linehl ~= nil then 53 | local line_bg = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(best_linehl)), "bg", mode) 54 | local highlight_name = highlight.copy_highlight(best_texthl, mode == "gui", line_bg) 55 | M.vim.api.nvim_buf_add_highlight(bufnr, ns, highlight_name, i - 1, 0, -1) 56 | else 57 | M.vim.api.nvim_buf_add_highlight(bufnr, ns, best_texthl, i - 1, 0, -1) 58 | end 59 | else 60 | M.vim.api.nvim_buf_add_highlight(bufnr, ns, best_linehl, i - 1, 0, -1) 61 | end 62 | end 63 | end 64 | 65 | --- Refresh the content of the gutter. 66 | function M.set_gutter_lines(bufnr, lines, count_method, width) 67 | local win_height = M.vim.api.nvim_win_get_height(0) 68 | 69 | local strings = {} 70 | for _, matches in ipairs(lines) do 71 | local text = ' ' 72 | local non_empty_matches = 0 73 | for _, match in ipairs(matches) do 74 | if match.text ~= " " then 75 | non_empty_matches = non_empty_matches + 1 76 | end 77 | end 78 | if count_method ~= nil and non_empty_matches > 1 then 79 | text = counters.count(non_empty_matches, count_method) 80 | else 81 | text = M.find_best_match(matches, "text")['text'] 82 | end 83 | 84 | -- pad text to width 85 | text = string.rep(' ', width - #text) .. text 86 | table.insert(strings, text) 87 | end 88 | 89 | M.vim.api.nvim_buf_set_lines(bufnr, 0, win_height - 1, false, strings) 90 | end 91 | 92 | function M.get_gutter_column(gutters, gutter_index, layout) 93 | local window_width = M.vim.api.nvim_win_get_width and M.vim.api.nvim_win_get_width(0) or 94 | 80 -- Default to 80 if function not available 95 | local column = 0 96 | local gutter_count = #gutters 97 | local config = require('sluice.config') 98 | 99 | if layout == 'right' then 100 | for i = gutter_count, gutter_index, -1 do 101 | local gutter_settings = config.settings.gutters[i] 102 | if gutter_settings and gutters[i] and gutters[i].enabled ~= false and gutter_settings.window.layout == 'right' then 103 | column = column + gutter_settings.window.width 104 | end 105 | end 106 | return window_width - column 107 | else -- 'left' layout 108 | for i = 1, gutter_index - 1 do 109 | local gutter_settings = config.settings.gutters[i] 110 | if gutter_settings and gutters[i] and gutters[i].enabled ~= false and gutter_settings.window.layout == 'left' then 111 | column = column + gutter_settings.window.width 112 | end 113 | end 114 | return column 115 | end 116 | end 117 | 118 | --- Create a gutter. 119 | -- side effect: creates bufnr and ns 120 | function M.create_window(gutters, gutter_index) 121 | local gutter = gutters[gutter_index] 122 | local gutter_settings = require('sluice.config').settings.gutters[gutter_index] 123 | local gutter_width = gutter_settings.window.width 124 | local layout = gutter_settings.window.layout 125 | 126 | local col = M.get_gutter_column(gutters, gutter_index, layout) 127 | local height = M.vim.api.nvim_win_get_height(0) 128 | 129 | if gutter.bufnr == nil then 130 | gutter.bufnr = M.vim.api.nvim_create_buf(false, true) 131 | gutter.ns = M.vim.api.nvim_create_namespace('sluice' .. gutter.bufnr) 132 | end 133 | if gutter.winid == nil or M.vim.fn.win_id2win(gutter.winid) == 0 then 134 | gutter.winid = M.vim.api.nvim_open_win(gutter.bufnr, false, { 135 | relative = 'win', 136 | width = gutter_width, 137 | height = height, 138 | row = 0, 139 | col = col, 140 | focusable = false, 141 | style = 'minimal', 142 | }) 143 | else 144 | M.vim.api.nvim_win_set_config(gutter.winid, { 145 | win = M.vim.api.nvim_get_current_win(), 146 | relative = 'win', 147 | width = gutter_width, 148 | height = height, 149 | row = 0, 150 | col = col, 151 | }) 152 | end 153 | end 154 | 155 | return M 156 | -------------------------------------------------------------------------------- /plugin/sluice.vim: -------------------------------------------------------------------------------- 1 | if !hlexists('SluiceColumn') 2 | hi link SluiceColumn SignColumn 3 | endif 4 | 5 | lua require'sluice' 6 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pre-commit-3.7.0 2 | -------------------------------------------------------------------------------- /static/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsummersl/nvim-sluice/0f3a3014fe7f102abce442d391b6ada4f0c4a1d4/static/screenshot.png -------------------------------------------------------------------------------- /tests/plenary/sluice/config_spec.lua: -------------------------------------------------------------------------------- 1 | local config = require('sluice.config') 2 | 3 | describe('sluice.config', function() 4 | describe('default_enabled_fn', function() 5 | before_each(function() 6 | config.vim = { 7 | validate = vim.validate, 8 | deepcopy = vim.deepcopy, 9 | tbl_deep_extend = vim.tbl_deep_extend 10 | } 11 | config.vim.api = { 12 | nvim_win_get_height = function() return 50 end, 13 | nvim_buf_line_count = function() return 100 end 14 | } 15 | config.vim.fn = { 16 | getwinvar = function(_, var) 17 | if var == '&buftype' then return '' end 18 | if var == '&previewwindow' then return 0 end 19 | if var == '&diff' then return 0 end 20 | end 21 | } 22 | end) 23 | 24 | it('should return true when conditions are met', function() 25 | assert.is_true(config.default_enabled_fn({})) 26 | end) 27 | 28 | it('should return false if window height is larger than buffer lines', function() 29 | config.vim.api.nvim_win_get_height = function() return 200 end 30 | assert.is_false(config.default_enabled_fn({})) 31 | end) 32 | 33 | it('should return false if buftype is not empty', function() 34 | config.vim.fn.getwinvar = function(_, var) 35 | if var == '&buftype' then return 'help' end 36 | if var == '&previewwindow' then return 0 end 37 | if var == '&diff' then return 0 end 38 | end 39 | assert.is_false(config.default_enabled_fn({})) 40 | end) 41 | 42 | it('should return false if previewwindow', function() 43 | config.vim.fn.getwinvar = function(_, var) 44 | if var == '&buftype' then return '' end 45 | if var == '&previewwindow' then return 1 end 46 | if var == '&diff' then return 0 end 47 | end 48 | assert.is_false(config.default_enabled_fn({})) 49 | end) 50 | 51 | it('should return false if diff', function() 52 | config.vim.fn.getwinvar = function(_, var) 53 | if var == '&buftype' then return '' end 54 | if var == '&previewwindow' then return 0 end 55 | if var == '&diff' then return 1 end 56 | end 57 | assert.is_false(config.default_enabled_fn({})) 58 | end) 59 | end) 60 | 61 | -- Additional tests for other functions and configurations can be added here 62 | 63 | describe('apply_user_settings', function() 64 | it('should override default settings with user settings', function() 65 | local user_settings = { 66 | enable = false, 67 | throttle_ms = 200, 68 | gutters = { 69 | { 70 | plugins = { 'viewport', 'counters' }, 71 | window = { 72 | width = 2, 73 | enabled_fn = function() return true end, 74 | }, 75 | }, 76 | }, 77 | } 78 | config.apply_user_settings(user_settings) 79 | assert.is_false(config.settings.enable) 80 | assert.are.equal(200, config.settings.throttle_ms) 81 | assert.are.equal(2, config.settings.gutters[1].window.width) 82 | assert.is_true(config.settings.gutters[1].window.enabled_fn()) 83 | end) 84 | 85 | it('should not override unspecified settings', function() 86 | config.apply_user_settings({}) 87 | assert.is_true(config.settings.enable) 88 | -- Check that other settings are still at their default values 89 | assert.are.equal(150, config.settings.throttle_ms) 90 | assert.are.equal('viewport', config.settings.gutters[1].plugins[1]) 91 | end) 92 | 93 | it('should handle nil user settings', function() 94 | config.apply_user_settings(nil) 95 | -- Check that settings are still at their default values 96 | assert.is_true(config.settings.enable) 97 | assert.are.equal(150, config.settings.throttle_ms) 98 | assert.are.equal('viewport', config.settings.gutters[1].plugins[1]) 99 | end) 100 | 101 | it('should throw an error for invalid types', function() 102 | assert.has_error(function() 103 | config.apply_user_settings({ enable = 'true' }) 104 | end, "enable: expected boolean, got string") 105 | 106 | assert.has_error(function() 107 | config.apply_user_settings({ throttle_ms = '200' }) 108 | end, "throttle_ms: expected number, got string") 109 | 110 | assert.has_error(function() 111 | config.apply_user_settings({ gutters = 'not a table' }) 112 | end, "gutters: expected table, got string") 113 | 114 | assert.has_error(function() 115 | config.apply_user_settings({ gutters = { { window = { width = 'two' } } } }) 116 | end, "gutters[1].window.width: expected number, got string") 117 | end) 118 | 119 | it('should handle signs group setting', function() 120 | local user_settings = { 121 | gutters = { 122 | { 123 | plugins = { 'signs' }, 124 | signs = { 125 | group = 'custom_group' 126 | } 127 | } 128 | } 129 | } 130 | config.apply_user_settings(user_settings) 131 | assert.are.equal('custom_group', config.settings.gutters[1].signs.group) 132 | end) 133 | 134 | it('should handle window layout setting', function() 135 | local user_settings = { 136 | gutters = { 137 | { 138 | plugins = { 'viewport' }, 139 | window = { 140 | layout = 'left' 141 | } 142 | } 143 | } 144 | } 145 | config.apply_user_settings(user_settings) 146 | assert.are.equal('left', config.settings.gutters[1].window.layout) 147 | end) 148 | 149 | it('should default to right layout when not specified', function() 150 | local user_settings = { 151 | gutters = { 152 | { 153 | plugins = { 'viewport' }, 154 | window = {} 155 | } 156 | } 157 | } 158 | config.apply_user_settings(user_settings) 159 | assert.are.equal('right', config.settings.gutters[1].window.layout) 160 | end) 161 | 162 | it('should throw an error for invalid layout', function() 163 | local user_settings = { 164 | gutters = { 165 | { 166 | plugins = { 'viewport' }, 167 | window = { 168 | layout = 'invalid' 169 | } 170 | } 171 | } 172 | } 173 | assert.has_error(function() 174 | config.apply_user_settings(user_settings) 175 | end, "gutters[1].window.layout must be 'left' or 'right'") 176 | end) 177 | 178 | it('should throw an error for invalid render_method', function() 179 | local user_settings = { 180 | gutters = { 181 | { 182 | plugins = { 'viewport' }, 183 | window = { 184 | render_method = 'invalid' 185 | } 186 | } 187 | } 188 | } 189 | assert.has_error(function() 190 | config.apply_user_settings(user_settings) 191 | end, "gutters[1].window.render_method must be 'macro' or 'line'") 192 | end) 193 | 194 | it('should accept valid render_method', function() 195 | local user_settings = { 196 | gutters = { 197 | { 198 | plugins = { 'viewport' }, 199 | window = { 200 | render_method = 'line' 201 | } 202 | } 203 | } 204 | } 205 | config.apply_user_settings(user_settings) 206 | assert.are.equal('line', config.settings.gutters[1].window.render_method) 207 | end) 208 | 209 | it('should throw an error for invalid types', function() 210 | assert.has_error(function() 211 | config.apply_user_settings({ enable = 'true' }) 212 | end, "enable: expected boolean, got string") 213 | 214 | assert.has_error(function() 215 | config.apply_user_settings({ throttle_ms = '200' }) 216 | end, "throttle_ms: expected number, got string") 217 | 218 | assert.has_error(function() 219 | config.apply_user_settings({ gutters = 'not a table' }) 220 | end, "gutters: expected table, got string") 221 | 222 | assert.has_error(function() 223 | config.apply_user_settings({ gutters = { { window = { width = 'two' } } } }) 224 | end, "gutters[1].window.width: expected number, got string") 225 | end) 226 | 227 | it('should handle signs group setting', function() 228 | local user_settings = { 229 | gutters = { 230 | { 231 | plugins = { 'signs' }, 232 | signs = { 233 | group = 'custom_group' 234 | } 235 | } 236 | } 237 | } 238 | config.apply_user_settings(user_settings) 239 | assert.are.equal('custom_group', config.settings.gutters[1].signs.group) 240 | end) 241 | end) 242 | 243 | describe('str_table_fn', function() 244 | it('should return true for matching strings', function() 245 | assert.is_true(config.str_table_fn("test", "test")) 246 | end) 247 | 248 | it('should return false for non-matching strings', function() 249 | assert.is_false(config.str_table_fn("test", "other")) 250 | end) 251 | 252 | it('should return true for value in table', function() 253 | assert.is_true(config.str_table_fn({"a", "b", "c"}, "b")) 254 | end) 255 | 256 | it('should return false for value not in table', function() 257 | assert.is_false(config.str_table_fn({"a", "b", "c"}, "d")) 258 | end) 259 | 260 | it('should return true when function returns true', function() 261 | assert.is_true(config.str_table_fn(function(x) return x > 5 end, 10)) 262 | end) 263 | 264 | it('should return false when function returns false', function() 265 | assert.is_false(config.str_table_fn(function(x) return x > 5 end, 3)) 266 | end) 267 | 268 | it('should return false for unsupported types', function() 269 | assert.is_false(config.str_table_fn(123, "test")) 270 | assert.is_false(config.str_table_fn(true, "test")) 271 | assert.is_false(config.str_table_fn(nil, "test")) 272 | end) 273 | end) 274 | end) 275 | -------------------------------------------------------------------------------- /tests/plenary/sluice/convert_spec.lua: -------------------------------------------------------------------------------- 1 | local convert = require('sluice.convert') 2 | 3 | describe('line_to_gutter_line()', function() 4 | it('returns the correct line number when scrolling', function() 5 | assert.are.equal(2, convert.line_to_gutter_line(2, -1, 50, 1)) 6 | assert.are.equal(1, convert.line_to_gutter_line(2, -1, 50, 2)) 7 | assert.are.equal(0, convert.line_to_gutter_line(2, -1, 50, 3)) 8 | end) 9 | 10 | it('returns 0 for lines outside the visible range', function() 11 | assert.are.equal(0, convert.line_to_gutter_line(1, -1, 50, 50)) 12 | assert.are.equal(0, convert.line_to_gutter_line(101, -1, 50, 50)) 13 | end) 14 | end) 15 | 16 | describe('line_to_gutter_line_macro()', function() 17 | it('converts file lines to gutter lines correctly', function() 18 | assert.are.equal(convert.line_to_gutter_line_macro(1, 100, 50, 1), 1) 19 | assert.are.equal(convert.line_to_gutter_line_macro(50, 100, 50, 50), 25) 20 | assert.are.equal(convert.line_to_gutter_line_macro(100, 100, 50, 100), 50) 21 | end) 22 | 23 | it('handles edge cases', function() 24 | assert.are.equal(convert.line_to_gutter_line_macro(1, 1000, 10, 1), 1) 25 | assert.are.equal(convert.line_to_gutter_line_macro(1000, 1000, 10, 1000), 10) 26 | end) 27 | end) 28 | 29 | local gutter_settings = { 30 | plugins = { 'viewport' }, 31 | viewport = { 32 | cursor_hl = 'IncSearch', 33 | }, 34 | window = { 35 | width = 1, 36 | default_gutter_hl = 'SluiceColumn', 37 | } 38 | } 39 | 40 | describe('lines_to_gutters()', function() 41 | it('returns filler gutter values if there are no lines', function() 42 | local expected = {} 43 | for i = 1, 10, 1 do 44 | table.insert(expected, {{ 45 | linehl = 'SluiceColumn', 46 | text = ' ', 47 | texthl = '', 48 | }}) 49 | end 50 | assert.are.same(convert.lines_to_gutters(gutter_settings, {}, 100, 10), expected) 51 | end) 52 | 53 | it('shows a match on the first line', function() 54 | local expected = { 55 | { 56 | linehl = 'SluiceColumn', 57 | text = ' ', 58 | texthl = '', 59 | }, 60 | { 61 | linehl = 'SluiceViewportCursor', 62 | text = ' ', 63 | lnum = 1, 64 | priority = 1, 65 | } 66 | } 67 | assert.are.same( 68 | convert.lines_to_gutters(gutter_settings, { 69 | { 70 | linehl = "SluiceViewportCursor", 71 | lnum = 1, 72 | priority = 1, 73 | text = " " 74 | } 75 | }, 60, 42, 1)[1], 76 | expected) 77 | end) 78 | end) 79 | -------------------------------------------------------------------------------- /tests/plenary/sluice/highlight_spec.lua: -------------------------------------------------------------------------------- 1 | local highlight = require('sluice.highlight') 2 | 3 | describe('copy_highlight()', function() 4 | end) 5 | -------------------------------------------------------------------------------- /tests/plenary/sluice/integrations/counters_spec.lua: -------------------------------------------------------------------------------- 1 | local counters = require('sluice.integrations.counters') 2 | 3 | describe('count()', function() 4 | it('returns an empty string for <= 0', function() 5 | for _, values in pairs(counters.methods) do 6 | assert.are.equal(' ', counters.count(0, values)) 7 | assert.are.equal(' ', counters.count(-1, values)) 8 | end 9 | end) 10 | 11 | it('returns a value for 1', function () 12 | for _, values in pairs(counters.methods) do 13 | assert.are.equal(values[1], counters.count(1, values)) 14 | end 15 | end) 16 | 17 | it('returns a max value for big numbers', function () 18 | for _, values in pairs(counters.methods) do 19 | assert.are.equal(values[#values], counters.count(100, values)) 20 | end 21 | end) 22 | end) 23 | -------------------------------------------------------------------------------- /tests/plenary/sluice/integrations/extmark_signs_spec.lua: -------------------------------------------------------------------------------- 1 | local extmark = require("sluice.integrations.extmark_signs") 2 | local config = require("sluice.config") 3 | 4 | extmark.vim = { 5 | api = { 6 | nvim_buf_get_extmarks = function() 7 | return { 8 | { 1, 4, 0, { 9 | line_hl_group = "DiagnosticSignWarn", 10 | ns_id = 57, 11 | priority = 12, 12 | right_gravity = true, 13 | sign_hl_group = "DiagnosticSignWarn", 14 | sign_text = "A " 15 | } }, 16 | { 12, 13, 0, { 17 | invalidate = true, 18 | ns_id = 23, 19 | priority = 199, 20 | right_gravity = true, 21 | sign_hl_group = "MiniDiffSignAdd", 22 | sign_text = "B " 23 | } } 24 | } 25 | end 26 | } 27 | } 28 | 29 | describe("update", function() 30 | it("should return extmarks as signs when hl_groups match", function() 31 | config.str_table_fn = function(t, s) return t[s] end 32 | local result = extmark.update({ extmarks = { hl_groups = { DiagnosticSignWarn = true, MiniDiffSignAdd = true } } }, 0) 33 | assert.is_table(result) 34 | assert.equals(2, #result) 35 | assert.same({ 36 | lnum = 5, 37 | text = "A ", 38 | texthl = "DiagnosticSignWarn", 39 | priority = 12, 40 | plugin = 'extmarks', 41 | }, result[1]) 42 | assert.same({ 43 | lnum = 14, 44 | text = "B ", 45 | texthl = "MiniDiffSignAdd", 46 | priority = 199, 47 | plugin = 'extmarks', 48 | }, result[2]) 49 | end) 50 | 51 | it("should filter extmarks based on hl_groups", function() 52 | config.str_table_fn = function(t, s) return t[s] end 53 | local result = extmark.update({ extmarks = { hl_groups = { DiagnosticSignWarn = true } } }, 0) 54 | assert.is_table(result) 55 | assert.equals(1, #result) 56 | assert.same({ 57 | lnum = 5, 58 | text = "A ", 59 | texthl = "DiagnosticSignWarn", 60 | priority = 12, 61 | plugin = 'extmarks', 62 | }, result[1]) 63 | end) 64 | 65 | it("should return empty table when no hl_groups match", function() 66 | config.str_table_fn = function(t, s) return t[s] end 67 | local result = extmark.update({ extmarks = { hl_groups = { SomeOtherGroup = true } } }, 0) 68 | assert.is_table(result) 69 | assert.equals(0, #result) 70 | end) 71 | end) 72 | -------------------------------------------------------------------------------- /tests/plenary/sluice/integrations/signs_spec.lua: -------------------------------------------------------------------------------- 1 | local signs = require('sluice.integrations.signs') 2 | local config = require('sluice.config') 3 | 4 | -- Mock vim functions 5 | signs.vim = { 6 | fn = { 7 | sign_getdefined = function() 8 | return { 9 | { name = "sign1", texthl = "HL1" }, 10 | { name = "sign2", texthl = "HL2" } 11 | } 12 | end, 13 | sign_getplaced = function() 14 | return { { 15 | signs = { 16 | { name = "sign1", lnum = 1 }, 17 | { name = "sign2", lnum = 2 } 18 | } 19 | } } 20 | end 21 | }, 22 | tbl_extend = vim.tbl_extend 23 | } 24 | 25 | describe("signs update() integration", function() 26 | it("should return correct sign data without config data", function() 27 | local result = signs.update({}, 0) 28 | assert.equal(2, #result) 29 | assert.same({ name = "sign1", texthl = "HL1", lnum = 1, plugin = "signs" }, result[1]) 30 | assert.same({ name = "sign2", texthl = "HL2", lnum = 2, plugin = "signs" }, result[2]) 31 | end) 32 | 33 | it("should return correct sign data if filtered by settings", function() 34 | local result = signs.update({ signs = { group = "sign1" } }, 0) 35 | assert.equal(1, #result) 36 | assert.same({ name = "sign1", texthl = "HL1", lnum = 1, plugin = "signs" }, result[1]) 37 | end) 38 | end) 39 | -------------------------------------------------------------------------------- /tests/plenary/sluice/window_spec.lua: -------------------------------------------------------------------------------- 1 | local gutter = require('sluice.gutter') 2 | local window = require('sluice.window') 3 | local config = require('sluice.config') 4 | 5 | local lines = { { 6 | linehl = "SluiceViewportVisibleArea", 7 | lnum = 1, 8 | priority = 5, 9 | text = " " 10 | }, { 11 | linehl = "SluiceViewportCursor", 12 | lnum = 2, 13 | priority = 10, 14 | text = "-" 15 | }, { 16 | linehl = "SluiceViewportVisibleArea", 17 | lnum = 3, 18 | priority = 5, 19 | text = " " 20 | }, { 21 | linehl = "SluiceViewportVisibleArea", 22 | lnum = 4, 23 | priority = 5, 24 | text = " " 25 | }, { 26 | linehl = "SluiceViewportVisibleArea", 27 | lnum = 5, 28 | priority = 5, 29 | text = " " 30 | }, { 31 | linehl = "SluiceViewportVisibleArea", 32 | priority = 5, 33 | text = " " 34 | }, { 35 | linehl = "SluiceColumn", 36 | lnum = 7, 37 | priority = 0, 38 | text = " " 39 | }, { 40 | linehl = "SluiceColumn", 41 | lnum = 8, 42 | priority = 0, 43 | text = " " 44 | }, { 45 | linehl = "SluiceColumn", 46 | -- no lnum 47 | priority = 0, 48 | text = " " 49 | }, { 50 | linehl = "SluiceColumn", 51 | lnum = 10, 52 | priority = 0, 53 | text = " " 54 | } } 55 | 56 | describe('find_best_match()', function() 57 | it('picks the highest priority', function() 58 | assert.are.same(window.find_best_match(lines), 59 | { 60 | linehl = "SluiceViewportCursor", 61 | lnum = 2, 62 | priority = 10, 63 | text = "-" 64 | } 65 | ) 66 | end) 67 | 68 | it('prioritizes the last highest priority', function() 69 | assert.are.same(window.find_best_match({unpack(lines, 3, 5)}), 70 | { 71 | linehl = "SluiceViewportVisibleArea", 72 | lnum = 3, 73 | priority = 5, 74 | text = " " 75 | } 76 | ) 77 | end) 78 | 79 | describe('with a key', function() 80 | it('still prioritizes by priority', function() 81 | assert.are.same(window.find_best_match(lines, 'lnum'), 82 | { 83 | linehl = "SluiceViewportCursor", 84 | lnum = 2, 85 | priority = 10, 86 | text = "-" 87 | } 88 | ) 89 | end) 90 | it('excludes entries without the key', function() 91 | assert.are.same(window.find_best_match({unpack(lines, 7, 9)}, 'lnum'), 92 | { 93 | linehl = "SluiceColumn", 94 | lnum = 7, 95 | priority = 0, 96 | text = " " 97 | } 98 | ) 99 | end) 100 | 101 | it('does not compare non-int key values', function() 102 | local lines = { 103 | { linehl = "SluiceColumn" , text = " ", texthl = "" }, 104 | { linehl = "IncSearch" , lnum = 7 , priority = 1 , text = " " }, 105 | { linehl = "SluiceViewportVisibleArea", lnum = 8 , priority = 0 , text = " " }, 106 | { linehl = "SluiceViewportVisibleArea", lnum = 9 , priority = 0 , text = " " }, 107 | } 108 | assert.are.same({ 109 | linehl = "IncSearch", 110 | lnum = 7, 111 | priority = 1, 112 | text = " " 113 | }, 114 | window.find_best_match(lines, 'linehl') 115 | ) 116 | end) 117 | end) 118 | end) 119 | 120 | describe('create_window()', function() 121 | local mock_vim = { 122 | api = { 123 | nvim_create_buf = function() return 1 end, 124 | nvim_create_namespace = function() return 1 end, 125 | nvim_open_win = function() return 1 end, 126 | nvim_win_set_config = function() end, 127 | nvim_win_get_height = function() return 10 end, 128 | nvim_win_get_width = function() return 80 end, 129 | nvim_get_current_win = function() return 0 end, 130 | }, 131 | fn = { 132 | win_id2win = function() return 0 end, 133 | }, 134 | -- Remove spy object 135 | } 136 | 137 | before_each(function() 138 | window.vim = mock_vim 139 | package.loaded['sluice.config'] = nil 140 | config = require('sluice.config') 141 | config.settings = { 142 | gutters = { 143 | { 144 | window = { 145 | width = 2, 146 | layout = 'right', 147 | }, 148 | }, 149 | }, 150 | } 151 | end) 152 | 153 | it('creates a window with right layout', function() 154 | local gutters = {{}} 155 | local called_with = nil 156 | mock_vim.api.nvim_open_win = function(...) 157 | called_with = {...} 158 | return 1 159 | end 160 | window.create_window(gutters, 1) 161 | assert.are.same({1, false, { 162 | relative = 'win', 163 | width = 2, 164 | height = 10, 165 | row = 0, 166 | col = mock_vim.api.nvim_win_get_width(0) - 2, 167 | focusable = false, 168 | style = 'minimal', 169 | }}, called_with) 170 | end) 171 | 172 | it('creates a window with left layout', function() 173 | config.settings.gutters[1].window.layout = 'left' 174 | local gutters = {{}} 175 | local called_with = nil 176 | mock_vim.api.nvim_open_win = function(...) 177 | called_with = {...} 178 | return 1 179 | end 180 | window.create_window(gutters, 1) 181 | assert.are.same({1, false, { 182 | relative = 'win', 183 | width = 2, 184 | height = 10, 185 | row = 0, 186 | col = 0, 187 | focusable = false, 188 | style = 'minimal', 189 | }}, called_with) 190 | end) 191 | 192 | it('updates an existing window', function() 193 | local gutters = {{winid = 1}} 194 | mock_vim.fn.win_id2win = function() return 1 end 195 | local called_with = nil 196 | mock_vim.api.nvim_win_set_config = function(...) 197 | called_with = {...} 198 | end 199 | window.create_window(gutters, 1) 200 | assert.are.same({1, { 201 | win = 0, 202 | relative = 'win', 203 | width = 2, 204 | height = 10, 205 | row = 0, 206 | col = mock_vim.api.nvim_win_get_width(0) - 2, 207 | }}, called_with) 208 | end) 209 | end) 210 | 211 | describe('get_gutter_column()', function() 212 | local vim_width = vim.api.nvim_win_get_width(0) 213 | local one_gutter = { 214 | gutters = {{ 215 | window = { 216 | width = 3, 217 | layout = 'right' 218 | } 219 | }}, 220 | } 221 | local two_gutters = { 222 | gutters = {{ 223 | window = { 224 | width = 3, 225 | layout = 'right' 226 | } 227 | }, { 228 | window = { 229 | width = 2, 230 | layout = 'right' 231 | } 232 | }}, 233 | } 234 | local mixed_gutters = { 235 | gutters = {{ 236 | window = { 237 | width = 3, 238 | layout = 'right' 239 | } 240 | }, { 241 | window = { 242 | width = 2, 243 | layout = 'left' 244 | } 245 | }, { 246 | window = { 247 | width = 1, 248 | layout = 'right' 249 | } 250 | }}, 251 | } 252 | 253 | it('would account for a plugin with a custom width', function() 254 | config.apply_user_settings(one_gutter) 255 | local gutters = gutter.init_gutters(config) 256 | assert.are.same(vim_width - 3, window.get_gutter_column(gutters, 1, 'right')) 257 | end) 258 | 259 | it('would count multiple gutters with the same layout', function() 260 | config.apply_user_settings(two_gutters) 261 | local gutters = gutter.init_gutters(config) 262 | assert.are.same(vim_width - 5, window.get_gutter_column(gutters, 1, 'right')) 263 | assert.are.same(vim_width - 2, window.get_gutter_column(gutters, 2, 'right')) 264 | end) 265 | 266 | it('ignores gutters that are not enabled', function() 267 | two_gutters.gutters[2].enabled = false 268 | config.apply_user_settings(two_gutters) 269 | local gutters = gutter.init_gutters(config) 270 | assert.are.same(vim_width - 3, window.get_gutter_column(gutters, 1, 'right')) 271 | end) 272 | 273 | it('handles mixed layouts correctly', function() 274 | config.apply_user_settings(mixed_gutters) 275 | local gutters = gutter.init_gutters(config) 276 | assert.are.same(vim_width - 4, window.get_gutter_column(gutters, 1, 'right')) 277 | assert.are.same(0, window.get_gutter_column(gutters, 2, 'left')) 278 | assert.are.same(vim_width - 1, window.get_gutter_column(gutters, 3, 'right')) 279 | end) 280 | end) 281 | --------------------------------------------------------------------------------