From b6def2542503d9429c3c7a9e67585aafe764af45 Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 1 Mar 2025 15:33:56 -0500 Subject: [PATCH 1/6] Use vim.on_key for mouse functionality instead of mappings Fixes #140 --- README.md | 3 +- autoload/scrollview.vim | 36 ----------------------- doc/scrollview.txt | 15 ++++------ lua/scrollview.lua | 63 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 68 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 93ef890..bf9b346 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ scrollview-configuration`). ## Requirements * `nvim>=0.6` -* Mouse functionality requires mouse support (see `:help 'mouse'`) +* Mouse functionality requires mouse support (see `:help 'mouse'`) and + `nvim>=0.11` * Signs require `nvim>=0.9` ## Installation diff --git a/autoload/scrollview.vim b/autoload/scrollview.vim index 240273d..03ef6bc 100644 --- a/autoload/scrollview.vim +++ b/autoload/scrollview.vim @@ -432,42 +432,6 @@ endif " * Mappings " ************************************************* -function! s:SetUpMouseMappings(button, primary) abort - if a:button isnot# v:null - " Create a mouse mapping only if mappings don't already exist and "!" is - " not used at the end of the button. For example, a mapping may already - " exist if the user uses swapped buttons from $VIMRUNTIME/pack/dist/opt - " /swapmouse/plugin/swapmouse.vim. Handling for that scenario would - " require modifications (e.g., possibly by updating the non-initial - " feedkeys calls in handle_mouse() to remap keys). - let l:force = v:false - let l:button = a:button - if strcharpart(l:button, strchars(l:button, 1) - 1, 1) ==# '!' - let l:force = v:true - let l:button = - \ strcharpart(l:button, 0, strchars(l:button, 1) - 1) - endif - " scrollview mouse handling is not supported in select-mode. #140 - for l:mapmode in ['n', 'x', 'i'] - execute printf( - \ 'silent! %snoremap %s <%smouse>' - \ .. ' lua require("scrollview").handle_mouse("%s", %s)', - \ l:mapmode, - \ l:force ? '' : '', - \ l:button, - \ l:button, - \ a:primary ? 'true' : 'false', - \ ) - endfor - endif -endfunction - -call s:SetUpMouseMappings(g:scrollview_mouse_primary, v:true) -" :popup doesn't work for nvim<0.8. -if has('nvim-0.8') - call s:SetUpMouseMappings(g:scrollview_mouse_secondary, v:false) -endif - " Additional mappings are defined for convenience of creating " user-defined mappings that call nvim-scrollview functionality. However, " since the usage of mappings requires recursive map commands, this diff --git a/doc/scrollview.txt b/doc/scrollview.txt index 381b36d..53bcaac 100644 --- a/doc/scrollview.txt +++ b/doc/scrollview.txt @@ -20,7 +20,8 @@ signs. The plugin is customizable (see |scrollview-configuration|). 1. Requirements *scrollview-requirements* * `nvim>=0.6` -* Scrollbar mouse dragging requires mouse support (see |'mouse'|) +* Scrollbar mouse functionality requires mouse support (see |'mouse'|) and + `nvim>=0.11` * Signs require `nvim>=0.9` ============================================================================ @@ -301,19 +302,15 @@ scrollview_mouse_primary *scrollview_mouse_primary* Possible values include `'left'`, `'middle'`, `'right'`, `'x1'`, and `'x2'`. These can be prepended with `'c-'` or `'m-'` for the control-key and alt-key variants (e.g., - `'c-left'` for control-left). An existing mapping will - not be clobbered, unless `'!'` is added at the end (e.g., - `'left!'`). Set to `v:null` to disable the functionality. - Defaults to `'left'`. Considered only when the plugin is - loaded. + `'c-left'` for control-left). Set to `v:null` to disable + the functionality. Defaults to `'left'`. scrollview_mouse_secondary *scrollview_mouse_secondary* |String| specifying the button for secondary mouse operations (clicking signs for additional information). See |scrollview_mouse_primary| for the possible values, - including how `'c-'`, `'m-'`, `'!'`, and `v:null` can be - utilized. Defaults to `'right'`. Considered only when the - plugin is loaded. + including how `'c-'`, `'m-'`, and `v:null` can be utilized. + Defaults to `'right'`. *scrollview_on_startup* scrollview_on_startup |Boolean| specifying whether scrollbars are enabled on diff --git a/lua/scrollview.lua b/lua/scrollview.lua index 9ab8482..c91a28a 100644 --- a/lua/scrollview.lua +++ b/lua/scrollview.lua @@ -138,6 +138,20 @@ local BORDER_RIGHT = 4 local BORDER_BOTTOM = 6 local BORDER_LEFT = 8 +-- Maps mouse buttons (e.g., 'left') to the Neovim key representation. +local MOUSE_LOOKUP = (function() + local valid_buttons = { + 'left', 'middle', 'right', 'x1', 'x2', + 'c-left', 'c-middle', 'c-right', 'c-x1', 'c-x2', + 'm-left', 'm-middle', 'm-right', 'm-x1', 'm-x2', + } + local result = {} + for _, button in ipairs(valid_buttons) do + result[button] = t('<' .. button .. 'mouse>') + end + return result +end)() + -- ************************************************* -- * Memoization -- ************************************************* @@ -2962,9 +2976,14 @@ local handle_mouse = function(button, primary) local mousedown = t('<' .. button .. 'mouse>') local mouseup = t('<' .. button .. 'release>') if not vim.g.scrollview_enabled then - -- nvim-scrollview is disabled. Process the click as it would ordinarily be - -- processed, by re-sending the click and returning. + -- We have to temporarily set handling_mouse to true so that the on_key + -- handler doesn't call handle_mouse. Setting it back to false is deferred + -- (a later comment explains why, where the same type of handling is used). + handling_mouse = true fn.feedkeys(mousedown, 'ni') + vim.defer_fn(function() + handling_mouse = false + end, 0) return end local state = init() @@ -3312,7 +3331,45 @@ local handle_mouse = function(button, primary) reset_memoize() end restore(state) - handling_mouse = false + -- The processing of feedkeys calls initiated above (that aren't consumed by + -- read_input_stream) does not happen immediately. Defer the setting of + -- handling_mouse to false, to prevent an infinite loop of calls to + -- handle_mouse (e.g., for an ordinary click not on a sign or scrollbar). + vim.defer_fn(function() + handling_mouse = false + end, 0) +end + +-- pcall is not necessary here to avoid an error in some cases (Neovim +-- #17273), since that would be necessary for nvim<0.8, where this code would +-- not execute (the on_key handling for the mouse requires nvim==0.11, for the +-- ability to ignore the key by returning the empty string). +if to_bool(fn.has('nvim-0.11')) then -- Neovim 0.11 for ignoring keys + vim.on_key(function(str) + local normalize = function(button) + if button == vim.NIL then + button = nil + elseif button:sub(-1) == '!' then + -- Remove a trailing "!", which was supported in older versions of the + -- plugin for clobbering mappings. + button = button:sub(1, -2) + end + return button + end + local primary = normalize(vim.g.scrollview_mouse_primary) + local secondary = normalize(vim.g.scrollview_mouse_secondary) + if primary ~= nil + and not handling_mouse + and str == MOUSE_LOOKUP[primary] then + handle_mouse(primary, true) + return '' -- ignore the mousedown + elseif secondary ~= nil + and not handling_mouse + and str == MOUSE_LOOKUP[secondary] then + handle_mouse(secondary, false) + return '' -- ignore the mousedown + end + end) end -- A convenience function for setting global options with From 37b5a695edd95446c1676c4511c77d050d0beafa Mon Sep 17 00:00:00 2001 From: Dan Date: Sun, 2 Mar 2025 14:22:20 -0500 Subject: [PATCH 2/6] Move handling to the on_key handler --- lua/scrollview.lua | 295 ++++++++++++++++++++++----------------------- 1 file changed, 143 insertions(+), 152 deletions(-) diff --git a/lua/scrollview.lua b/lua/scrollview.lua index c91a28a..863696d 100644 --- a/lua/scrollview.lua +++ b/lua/scrollview.lua @@ -2961,7 +2961,7 @@ end -- prepended for the control-key and alt-key variants. If primary is true, the -- handling is for navigation (dragging scrollbars and navigating to signs). -- If primary is false, the handling is for context (showing popups with info). -local handle_mouse = function(button, primary) +local handle_mouse = function(button, is_primary, init_props, init_mousepos) local valid_buttons = { 'left', 'middle', 'right', 'x1', 'x2', 'c-left', 'c-middle', 'c-right', 'c-x1', 'c-x2', @@ -2970,30 +2970,21 @@ local handle_mouse = function(button, primary) if not vim.tbl_contains(valid_buttons, button) then error('Unsupported button: ' .. button) end - if primary == nil then - primary = true + if is_primary == nil then + is_primary = true end local mousedown = t('<' .. button .. 'mouse>') local mouseup = t('<' .. button .. 'release>') - if not vim.g.scrollview_enabled then - -- We have to temporarily set handling_mouse to true so that the on_key - -- handler doesn't call handle_mouse. Setting it back to false is deferred - -- (a later comment explains why, where the same type of handling is used). - handling_mouse = true - fn.feedkeys(mousedown, 'ni') - vim.defer_fn(function() - handling_mouse = false - end, 0) - return + -- We don't support mouse functionality in visual nor select mode. + if is_visual_mode(fn.mode()) or is_select_mode(fn.mode()) then + vim.cmd('normal! ' .. t'') + vim.cmd('redraw') end local state = init() local resume_memoize = memoize start_memoize() pcall(function() handling_mouse = true - -- Re-send the click, so its position can be obtained through - -- read_input_stream(). - fn.feedkeys(mousedown, 'ni') -- Mouse handling is not relevant in the command line window since -- scrollbars are not shown. Additionally, the overlay cannot be closed -- from that mode. @@ -3017,10 +3008,22 @@ local handle_mouse = function(button, primary) local init_winline while true do while true do - idx = idx + 1 - if idx > #chars_props then - idx = 1 - str, chars_props = read_input_stream() + if count == 0 then + str = mousedown + chars_props = {{ + char = mousedown, + str_idx = 1, + charmod = 0, + mouse_winid = init_mousepos.winid, + mouse_row = init_mousepos.winrow, + mouse_col = init_mousepos.wincol, + }} + else + idx = idx + 1 + if idx > #chars_props then + idx = 1 + str, chars_props = read_input_stream() + end end local char_props = chars_props[idx] str_idx = char_props.str_idx @@ -3042,9 +3045,9 @@ local handle_mouse = function(button, primary) end end if char == t'' then - fn.feedkeys(string.sub(str, str_idx + #char), 'ni') return end + -- TODO: Can you remove this check? -- In select-mode, mouse usage results in the mode intermediately -- switching to visual mode, accompanied by a call to this function. -- After the initial mouse event, the next getchar() character is @@ -3058,23 +3061,14 @@ local handle_mouse = function(button, primary) if char ~= '\x80\xf5X' or count == 0 then if mouse_winid == 0 then -- There was no mouse event. - fn.feedkeys(string.sub(str, str_idx), 'ni') return end if char == mouseup then - if count == 0 then - -- No initial mousedown was captured. - fn.feedkeys(string.sub(str, str_idx), 'ni') - elseif count == 1 then + if count == 1 then -- A scrollbar was clicked, but there was no corresponding drag. - -- Allow the interaction to be processed as it would be with no - -- scrollbar. - fn.feedkeys(mousedown .. string.sub(str, str_idx), 'ni') else -- A scrollbar was clicked and there was a corresponding drag. - -- 'feedkeys' is not called, since the full mouse interaction has - -- already been processed. The current window (from prior to - -- scrolling) is not changed. + -- The current window (from prior to scrolling) is not changed. -- Refresh scrollbars to handle the scenario where -- scrollview_hide_on_float_intersect is enabled and dragging -- resulted in a scrollbar overlapping a floating window. @@ -3087,51 +3081,21 @@ local handle_mouse = function(button, primary) return end if count == 0 then - if mouse_winid < 0 then - -- The mouse event was on the tabline or command line. - fn.feedkeys(string.sub(str, str_idx), 'ni') - return - end - props = get_scrollview_bar_props(mouse_winid) - local clicked_bar = false - local clicked_sign = false - local sign_props = nil -- set when clicked_sign is true - if not vim.tbl_isempty(props) then - clicked_bar = mouse_row >= props.row - and mouse_row < props.row + props.height - and mouse_col >= props.col - and mouse_col <= props.col - end - -- First check for a click on a sign and handle accordingly. - for _, sign_props2 in ipairs(get_scrollview_sign_props(mouse_winid)) do - if mouse_row == sign_props2.row - and mouse_col >= sign_props2.col - and mouse_col <= sign_props2.col + sign_props2.width - 1 - and (not clicked_bar or sign_props2.zindex > props.zindex) then - clicked_sign = true - clicked_bar = false - sign_props = sign_props2 - break - end - end - if not clicked_bar and not clicked_sign then - -- There was either no scrollbar or signs in the window where a - -- click occurred or the click was not on a scrollbar or sign. - fn.feedkeys(string.sub(str, str_idx), 'ni') - return - end - if clicked_sign and primary then + props = init_props + local clicked_bar = props.type == BAR_TYPE + local clicked_sign = props.type == SIGN_TYPE + if clicked_sign and is_primary then -- There was a primary click on a sign. Navigate to the next -- sign_props line after the cursor. api.nvim_win_call(mouse_winid, function() local current = fn.line('.') - local target = subsequent(sign_props.lines, current, 1, true) + local target = subsequent(props.lines, current, 1, true) vim.cmd('normal!' .. target .. 'G') end) refresh_bars() return end - if not primary then + if not is_primary then -- There was a secondary click on either a scrollbar or sign. Show -- a popup accordingly. -- Menus starting with ']' are excluded from the main menu bar @@ -3140,11 +3104,11 @@ local handle_mouse = function(button, primary) local lhs, rhs local mousepos = fn.getmousepos() if clicked_sign then - local group = sign_specs[sign_props.sign_spec_id].group + local group = sign_specs[props.sign_spec_id].group lhs = menu_name .. '.' .. group rhs = '' vim.cmd('anoremenu ' .. lhs .. ' ' .. rhs) - local variant = sign_specs[sign_props.sign_spec_id].variant + local variant = sign_specs[props.sign_spec_id].variant if variant ~= nil then lhs = menu_name .. '.' .. variant rhs = '' @@ -3169,7 +3133,7 @@ local handle_mouse = function(button, primary) menu_slots_available = menu_slots_available - #fn.menu_info(menu_name).submenus end - for line_idx, line in ipairs(sign_props.lines) do + for line_idx, line in ipairs(props.lines) do if menu_slots_available ~= nil and line_idx > menu_slots_available then break @@ -3177,7 +3141,7 @@ local handle_mouse = function(button, primary) lhs = menu_name .. '.' .. line rhs = string.format( 'call win_execute(%d, "normal! %dG")', - sign_props.parent_winid, + props.parent_winid, line ) vim.cmd('anoremenu ' .. lhs .. ' ' .. rhs) @@ -3244,10 +3208,6 @@ local handle_mouse = function(button, primary) return end -- By this point, the click on a scrollbar was successful. - if is_visual_mode(fn.mode()) then - -- Exit visual mode. - vim.cmd('normal! ' .. t'') - end winid = mouse_winid api.nvim_win_call(winid, function() init_wincol = fn.wincol() @@ -3256,70 +3216,66 @@ local handle_mouse = function(button, primary) scrollbar_offset = props.row - mouse_row previous_row = props.row end - -- Only consider a scrollbar update for mouse events on windows (i.e., - -- not on the tabline or command line). - if mouse_winid > 0 then - local winheight = get_window_height(winid) - local mouse_winrow = fn.getwininfo(mouse_winid)[1].winrow - local winrow = fn.getwininfo(winid)[1].winrow - local window_offset = mouse_winrow - winrow - local row = mouse_row + window_offset + scrollbar_offset - row = math.min(row, winheight) - row = math.max(1, row) - if vim.g.scrollview_include_end_region then - -- Don't allow scrollbar to overflow. - row = math.min(row, winheight - props.height + 1) + local winheight = get_window_height(winid) + local mouse_winrow = fn.getwininfo(mouse_winid)[1].winrow + local winrow = fn.getwininfo(winid)[1].winrow + local window_offset = mouse_winrow - winrow + local row = mouse_row + window_offset + scrollbar_offset + row = math.min(row, winheight) + row = math.max(1, row) + if vim.g.scrollview_include_end_region then + -- Don't allow scrollbar to overflow. + row = math.min(row, winheight - props.height + 1) + end + -- Only update scrollbar if the row changed. + if previous_row ~= row then + if topline_lookup == nil then + topline_lookup = get_topline_lookup(winid) end - -- Only update scrollbar if the row changed. - if previous_row ~= row then - if topline_lookup == nil then - topline_lookup = get_topline_lookup(winid) - end - local topline = topline_lookup[row] - topline = math.max(1, topline) - if row == 1 then - -- If the scrollbar was dragged to the top of the window, always - -- show the first line. - topline = 1 - end - set_topline(winid, topline) - if api.nvim_win_get_option(winid, 'scrollbind') - or api.nvim_win_get_option(winid, 'cursorbind') then - refresh_bars() - props = get_scrollview_bar_props(winid) - end - props = move_scrollbar(props, row) -- luacheck: ignore - -- Refresh since sign backgrounds might be stale, for signs that - -- switched intersection state with scrollbar. This is fast, from - -- caching. + local topline = topline_lookup[row] + topline = math.max(1, topline) + if row == 1 then + -- If the scrollbar was dragged to the top of the window, always + -- show the first line. + topline = 1 + end + set_topline(winid, topline) + if api.nvim_win_get_option(winid, 'scrollbind') + or api.nvim_win_get_option(winid, 'cursorbind') then refresh_bars() props = get_scrollview_bar_props(winid) - -- Apply appropriate highlighting where relevant. - if mousemove_received - and to_bool(fn.exists('&mousemoveevent')) - and vim.o.mousemoveevent then - -- But be sure to keep the scrollbar highlighted. - if not vim.tbl_isempty(props) and props.highlight_fn ~= nil then - props.highlight_fn(true) - end - -- Be sure that signs are not highlighted. Without this handling, - -- signs could be higlighted if a sign is moved to the same - -- position as the cursor while dragging a scrollbar. - for _, winid2 in ipairs(get_scrollview_windows()) do - local props2 = api.nvim_win_get_var(winid2, PROPS_VAR) - if not vim.tbl_isempty(props2) - and props2.highlight_fn ~= nil - and props2.type == SIGN_TYPE then - props2.highlight_fn(false) - end + end + props = move_scrollbar(props, row) -- luacheck: ignore + -- Refresh since sign backgrounds might be stale, for signs that + -- switched intersection state with scrollbar. This is fast, from + -- caching. + refresh_bars() + props = get_scrollview_bar_props(winid) + -- Apply appropriate highlighting where relevant. + if mousemove_received + and to_bool(fn.exists('&mousemoveevent')) + and vim.o.mousemoveevent then + -- But be sure to keep the scrollbar highlighted. + if not vim.tbl_isempty(props) and props.highlight_fn ~= nil then + props.highlight_fn(true) + end + -- Be sure that signs are not highlighted. Without this handling, + -- signs could be higlighted if a sign is moved to the same + -- position as the cursor while dragging a scrollbar. + for _, winid2 in ipairs(get_scrollview_windows()) do + local props2 = api.nvim_win_get_var(winid2, PROPS_VAR) + if not vim.tbl_isempty(props2) + and props2.highlight_fn ~= nil + and props2.type == SIGN_TYPE then + props2.highlight_fn(false) end end - -- Window workspaces may still be present as a result of the - -- earlier commands. Remove prior to redrawing. - reset_win_workspaces() - vim.cmd('redraw') - previous_row = row end + -- Window workspaces may still be present as a result of the + -- earlier commands. Remove prior to redrawing. + reset_win_workspaces() + vim.cmd('redraw') + previous_row = row end count = count + 1 end -- end if @@ -3331,13 +3287,7 @@ local handle_mouse = function(button, primary) reset_memoize() end restore(state) - -- The processing of feedkeys calls initiated above (that aren't consumed by - -- read_input_stream) does not happen immediately. Defer the setting of - -- handling_mouse to false, to prevent an infinite loop of calls to - -- handle_mouse (e.g., for an ordinary click not on a sign or scrollbar). - vim.defer_fn(function() - handling_mouse = false - end, 0) + handling_mouse = false end -- pcall is not necessary here to avoid an error in some cases (Neovim @@ -3346,6 +3296,12 @@ end -- ability to ignore the key by returning the empty string). if to_bool(fn.has('nvim-0.11')) then -- Neovim 0.11 for ignoring keys vim.on_key(function(str) + if not vim.g.scrollview_enabled then + return + end + if handling_mouse then + return + end local normalize = function(button) if button == vim.NIL then button = nil @@ -3358,17 +3314,53 @@ if to_bool(fn.has('nvim-0.11')) then -- Neovim 0.11 for ignoring keys end local primary = normalize(vim.g.scrollview_mouse_primary) local secondary = normalize(vim.g.scrollview_mouse_secondary) - if primary ~= nil - and not handling_mouse - and str == MOUSE_LOOKUP[primary] then - handle_mouse(primary, true) - return '' -- ignore the mousedown - elseif secondary ~= nil - and not handling_mouse - and str == MOUSE_LOOKUP[secondary] then - handle_mouse(secondary, false) - return '' -- ignore the mousedown + if primary == nil and secondary == nil then + return + end + if str ~= MOUSE_LOOKUP[primary] and str ~= MOUSE_LOOKUP[secondary] then + return + end + local mousepos = fn.getmousepos() + local mouse_winid = mousepos.winid + -- TODO: return if mouse over tabline, winbar, command line, etc. (see read_input_stream) + local mouse_row = mousepos.winrow + local mouse_col = mousepos.wincol + local props = get_scrollview_bar_props(mouse_winid) + local clicked_bar = false + local clicked_sign = false + if not vim.tbl_isempty(props) then + clicked_bar = mouse_row >= props.row + and mouse_row < props.row + props.height + and mouse_col >= props.col + and mouse_col <= props.col + end + -- First check for a click on a sign and handle accordingly. + for _, sign_props in ipairs(get_scrollview_sign_props(mouse_winid)) do + if mouse_row == sign_props.row + and mouse_col >= sign_props.col + and mouse_col <= sign_props.col + sign_props.width - 1 + and (not clicked_bar or sign_props.zindex > props.zindex) then + clicked_sign = true + clicked_bar = false + props = sign_props + break + end + end + if not clicked_bar and not clicked_sign then + return + end + local button, is_primary + if str == MOUSE_LOOKUP[primary] then + button, is_primary = primary, true + elseif str == MOUSE_LOOKUP[secondary] then + button, is_primary = secondary, false + else + -- This should not be reached, since there's a return earlier for this + -- scenario. + return end + handle_mouse(button, is_primary, props, mousepos) + return '' -- ignore the mouse event end) end @@ -3672,7 +3664,6 @@ return { first = first, fold_count_exceeds = fold_count_exceeds, get_sign_eligible_windows = get_sign_eligible_windows, - handle_mouse = handle_mouse, last = last, legend = legend, next = next, From 312bb3dabaa23c6de3dec3e5adec1621733d368c Mon Sep 17 00:00:00 2001 From: Dan Date: Sun, 2 Mar 2025 14:58:57 -0500 Subject: [PATCH 3/6] Move on_key handler to its own function --- lua/scrollview.lua | 143 +++++++++++++++++++++++++-------------------- 1 file changed, 79 insertions(+), 64 deletions(-) diff --git a/lua/scrollview.lua b/lua/scrollview.lua index 863696d..8455c15 100644 --- a/lua/scrollview.lua +++ b/lua/scrollview.lua @@ -3290,77 +3290,92 @@ local handle_mouse = function(button, is_primary, init_props, init_mousepos) handling_mouse = false end +-- Checks if an input event is over a scrollview window and should be handled. +-- TODO: Finish documentation (including inputs/outputs). +local should_handle_mouse = function(str) + if not vim.g.scrollview_enabled then + return false + end + if handling_mouse then + return false + end + local normalize = function(button) + if button == vim.NIL then + button = nil + elseif button:sub(-1) == '!' then + -- Remove a trailing "!", which was supported in older versions of the + -- plugin for clobbering mappings. + button = button:sub(1, -2) + end + return button + end + local primary = normalize(vim.g.scrollview_mouse_primary) + local secondary = normalize(vim.g.scrollview_mouse_secondary) + if primary == nil and secondary == nil then + return false + end + if str ~= MOUSE_LOOKUP[primary] and str ~= MOUSE_LOOKUP[secondary] then + return false + end + local mousepos = fn.getmousepos() + local mouse_winid = mousepos.winid + -- TODO: return if mouse over tabline, winbar, command line, etc. (see read_input_stream) + local mouse_row = mousepos.winrow + local mouse_col = mousepos.wincol + local props = get_scrollview_bar_props(mouse_winid) + local clicked_bar = false + local clicked_sign = false + if not vim.tbl_isempty(props) then + clicked_bar = mouse_row >= props.row + and mouse_row < props.row + props.height + and mouse_col >= props.col + and mouse_col <= props.col + end + -- First check for a click on a sign and handle accordingly. + for _, sign_props in ipairs(get_scrollview_sign_props(mouse_winid)) do + if mouse_row == sign_props.row + and mouse_col >= sign_props.col + and mouse_col <= sign_props.col + sign_props.width - 1 + and (not clicked_bar or sign_props.zindex > props.zindex) then + clicked_sign = true + clicked_bar = false + props = sign_props + break + end + end + if not clicked_bar and not clicked_sign then + return false + end + local button, is_primary + if str == MOUSE_LOOKUP[primary] then + button, is_primary = primary, true + elseif str == MOUSE_LOOKUP[secondary] then + button, is_primary = secondary, false + else + -- This should not be reached, since there's a return earlier for this + -- scenario. + return false + end + local data = { + button = button, + is_primary = is_primary, + props = props, + mousepos = mousepos, + } + return true, data +end + -- pcall is not necessary here to avoid an error in some cases (Neovim -- #17273), since that would be necessary for nvim<0.8, where this code would -- not execute (the on_key handling for the mouse requires nvim==0.11, for the -- ability to ignore the key by returning the empty string). if to_bool(fn.has('nvim-0.11')) then -- Neovim 0.11 for ignoring keys vim.on_key(function(str) - if not vim.g.scrollview_enabled then - return - end - if handling_mouse then - return - end - local normalize = function(button) - if button == vim.NIL then - button = nil - elseif button:sub(-1) == '!' then - -- Remove a trailing "!", which was supported in older versions of the - -- plugin for clobbering mappings. - button = button:sub(1, -2) - end - return button - end - local primary = normalize(vim.g.scrollview_mouse_primary) - local secondary = normalize(vim.g.scrollview_mouse_secondary) - if primary == nil and secondary == nil then - return - end - if str ~= MOUSE_LOOKUP[primary] and str ~= MOUSE_LOOKUP[secondary] then - return - end - local mousepos = fn.getmousepos() - local mouse_winid = mousepos.winid - -- TODO: return if mouse over tabline, winbar, command line, etc. (see read_input_stream) - local mouse_row = mousepos.winrow - local mouse_col = mousepos.wincol - local props = get_scrollview_bar_props(mouse_winid) - local clicked_bar = false - local clicked_sign = false - if not vim.tbl_isempty(props) then - clicked_bar = mouse_row >= props.row - and mouse_row < props.row + props.height - and mouse_col >= props.col - and mouse_col <= props.col - end - -- First check for a click on a sign and handle accordingly. - for _, sign_props in ipairs(get_scrollview_sign_props(mouse_winid)) do - if mouse_row == sign_props.row - and mouse_col >= sign_props.col - and mouse_col <= sign_props.col + sign_props.width - 1 - and (not clicked_bar or sign_props.zindex > props.zindex) then - clicked_sign = true - clicked_bar = false - props = sign_props - break - end - end - if not clicked_bar and not clicked_sign then - return - end - local button, is_primary - if str == MOUSE_LOOKUP[primary] then - button, is_primary = primary, true - elseif str == MOUSE_LOOKUP[secondary] then - button, is_primary = secondary, false - else - -- This should not be reached, since there's a return earlier for this - -- scenario. - return + local should_handle, data = should_handle_mouse(str) + if should_handle then + handle_mouse(data.button, data.is_primary, data.props, data.mousepos) + return '' -- ignore the mouse event end - handle_mouse(button, is_primary, props, mousepos) - return '' -- ignore the mouse event end) end From a7b8308819588e465544d5e494b6bab5d46d89c6 Mon Sep 17 00:00:00 2001 From: Dan Date: Sun, 2 Mar 2025 15:32:57 -0500 Subject: [PATCH 4/6] Address luacheck warnings --- lua/scrollview.lua | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lua/scrollview.lua b/lua/scrollview.lua index 8455c15..b46c2e9 100644 --- a/lua/scrollview.lua +++ b/lua/scrollview.lua @@ -2996,8 +2996,8 @@ local handle_mouse = function(button, is_primary, init_props, init_mousepos) local scrollbar_offset local previous_row local idx = 1 - local str, chars_props = '', {} - local str_idx, char, mouse_winid, mouse_row, mouse_col + local chars_props = {} + local char, mouse_winid, mouse_row local props -- Computing this prior to the first mouse event could distort the location -- since this could be an expensive operation (and the mouse could move). @@ -3009,7 +3009,6 @@ local handle_mouse = function(button, is_primary, init_props, init_mousepos) while true do while true do if count == 0 then - str = mousedown chars_props = {{ char = mousedown, str_idx = 1, @@ -3022,15 +3021,13 @@ local handle_mouse = function(button, is_primary, init_props, init_mousepos) idx = idx + 1 if idx > #chars_props then idx = 1 - str, chars_props = read_input_stream() + chars_props = select(2, read_input_stream()) end end local char_props = chars_props[idx] - str_idx = char_props.str_idx char = char_props.char mouse_winid = char_props.mouse_winid mouse_row = char_props.mouse_row - mouse_col = char_props.mouse_col -- Break unless it's a mouse drag followed by another mouse drag, so -- that the first drag is skipped. if mouse_winid == 0 @@ -3064,7 +3061,7 @@ local handle_mouse = function(button, is_primary, init_props, init_mousepos) return end if char == mouseup then - if count == 1 then + if count == 1 then -- luacheck: ignore 542 (an empty if branch) -- A scrollbar was clicked, but there was no corresponding drag. else -- A scrollbar was clicked and there was a corresponding drag. From 508c7d42908b1f855365a5d9f4390231960858af Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 3 Mar 2025 11:15:18 -0500 Subject: [PATCH 5/6] Add back support for nvim<0.11. Where mouse functionality is handled with mappings. --- autoload/scrollview.vim | 60 +++++++++++++++++++++++++++++++++ doc/scrollview.txt | 10 ++++-- lua/scrollview.lua | 73 ++++++++++++++++++++++++++++++++++------- 3 files changed, 129 insertions(+), 14 deletions(-) diff --git a/autoload/scrollview.vim b/autoload/scrollview.vim index 03ef6bc..df29698 100644 --- a/autoload/scrollview.vim +++ b/autoload/scrollview.vim @@ -322,6 +322,10 @@ let g:scrollview_ins_mode_buf_lines = 0 " escaping. let g:scrollview_echo_string = v:null +" Keep track of the initial mouse settings. These are only used for nvim<0.11. +let g:scrollview_init_mouse_primary = g:scrollview_mouse_primary +let g:scrollview_init_mouse_secondary = g:scrollview_mouse_secondary + " ************************************************* " * Versioning " ************************************************* @@ -432,6 +436,62 @@ endif " * Mappings " ************************************************* +function! scrollview#HandleMouseFromMapping(button, is_primary) abort + let l:button_repr = nvim_replace_termcodes( + \ printf('<%smouse>', a:button), v:true, v:true, v:true) + let l:packed = luaeval( + \ '{require("scrollview").should_handle_mouse(_A)}', l:button_repr) + let l:should_handle = l:packed[0] + if l:should_handle + let l:data = l:packed[1] + call luaeval( + \ 'require("scrollview").handle_mouse(' + \ .. '_A.button, _A.is_primary, _A.props, _A.mousepos)', l:data) + else + " Process the click as it would ordinarily be processed. + call feedkeys(l:button_repr, 'ni') + endif +endfunction + +function! s:SetUpMouseMappings(button, primary) abort + if a:button isnot# v:null + " Create a mouse mapping only if mappings don't already exist and "!" is + " not used at the end of the button. For example, a mapping may already + " exist if the user uses swapped buttons from $VIMRUNTIME/pack/dist/opt + " /swapmouse/plugin/swapmouse.vim. Handling for that scenario would + " require modifications (e.g., possibly by updating the non-initial + " feedkeys calls in handle_mouse() to remap keys). + let l:force = v:false + let l:button = a:button + if strcharpart(l:button, strchars(l:button, 1) - 1, 1) ==# '!' + let l:force = v:true + let l:button = + \ strcharpart(l:button, 0, strchars(l:button, 1) - 1) + endif + for l:mapmode in ['n', 'v', 'i'] + execute printf( + \ 'silent! %snoremap %s <%smouse>' + \ .. ' call scrollview#HandleMouseFromMapping("%s", %s)', + \ l:mapmode, + \ l:force ? '' : '', + \ l:button, + \ l:button, + \ a:primary, + \ ) + endfor + endif +endfunction + +" With Neovim 0.11, mouse functionality is handled with vim.on_key, not +" mappings. +if !has('nvim-0.11') + call s:SetUpMouseMappings(g:scrollview_mouse_primary, v:true) + " :popup doesn't work for nvim<0.8. + if has('nvim-0.8') + call s:SetUpMouseMappings(g:scrollview_mouse_secondary, v:false) + endif +endif + " Additional mappings are defined for convenience of creating " user-defined mappings that call nvim-scrollview functionality. However, " since the usage of mappings requires recursive map commands, this diff --git a/doc/scrollview.txt b/doc/scrollview.txt index 53bcaac..223b555 100644 --- a/doc/scrollview.txt +++ b/doc/scrollview.txt @@ -303,14 +303,18 @@ scrollview_mouse_primary *scrollview_mouse_primary* `'x1'`, and `'x2'`. These can be prepended with `'c-'` or `'m-'` for the control-key and alt-key variants (e.g., `'c-left'` for control-left). Set to `v:null` to disable - the functionality. Defaults to `'left'`. + the functionality. Defaults to `'left'`. For `nvim<0.11`, + an existing mapping will not be clobbered, unless `'!'` + is added at the end (e.g., `'left!'`). For `nvim<0.11`, + the option is considered only when the plugin is loaded. scrollview_mouse_secondary *scrollview_mouse_secondary* |String| specifying the button for secondary mouse operations (clicking signs for additional information). See |scrollview_mouse_primary| for the possible values, - including how `'c-'`, `'m-'`, and `v:null` can be utilized. - Defaults to `'right'`. + including how `'c-'`, `'m-'`, and `v:null` can be utilized, + as well as the behavior specific to `nvim<0.11`. Defaults + to `'right'`. *scrollview_on_startup* scrollview_on_startup |Boolean| specifying whether scrollbars are enabled on diff --git a/lua/scrollview.lua b/lua/scrollview.lua index b46c2e9..c63efb1 100644 --- a/lua/scrollview.lua +++ b/lua/scrollview.lua @@ -3044,7 +3044,6 @@ local handle_mouse = function(button, is_primary, init_props, init_mousepos) if char == t'' then return end - -- TODO: Can you remove this check? -- In select-mode, mouse usage results in the mode intermediately -- switching to visual mode, accompanied by a call to this function. -- After the initial mouse event, the next getchar() character is @@ -3055,13 +3054,18 @@ local handle_mouse = function(button, is_primary, init_props, init_mousepos) -- c54f347d63bcca97ead673d01ac6b59914bb04e5/src/getchar.c#L2660-L2672) -- Ignore this character after scrolling has started. -- NOTE: "\x80\xf5X" (hex) ==# "\200\365X" (octal) + -- WARN: This handling may no longer be necessary after addressing Issue + -- #140, but was kept as a precaution. if char ~= '\x80\xf5X' or count == 0 then if mouse_winid == 0 then -- There was no mouse event. return end if char == mouseup then - if count == 1 then -- luacheck: ignore 542 (an empty if branch) + if count == 0 then -- luacheck: ignore 542 (an empty if branch) + -- No initial mousedown was captured. This can't happen with the + -- approach used to resolve Issue #140. + elseif count == 1 then -- luacheck: ignore 542 (an empty if branch) -- A scrollbar was clicked, but there was no corresponding drag. else -- A scrollbar was clicked and there was a corresponding drag. @@ -3288,7 +3292,11 @@ local handle_mouse = function(button, is_primary, init_props, init_mousepos) end -- Checks if an input event is over a scrollview window and should be handled. --- TODO: Finish documentation (including inputs/outputs). +-- 'str' is the representation of a key press (as represented by the argument +-- to on_key, which e.g., would be "\" or +-- nvim_replace_termcodes('', 1, 1, 1)). Returns either a single +-- value, false, or multiple values, true along with a table containing +-- 'button', 'is_primary', 'props', and 'mousepos'. local should_handle_mouse = function(str) if not vim.g.scrollview_enabled then return false @@ -3306,17 +3314,56 @@ local should_handle_mouse = function(str) end return button end - local primary = normalize(vim.g.scrollview_mouse_primary) - local secondary = normalize(vim.g.scrollview_mouse_secondary) + local primary = vim.g.scrollview_mouse_primary + local secondary = vim.g.scrollview_mouse_secondary + if not to_bool(fn.has('nvim-0.11')) then + -- On nvim<0.11, mouse mappings are created when the plugin starts, so we + -- don't support changes to the settings. + primary = vim.g.scrollview_init_mouse_primary + secondary = vim.g.scrollview_init_mouse_secondary + end + primary = normalize(primary) + secondary = normalize(secondary) if primary == nil and secondary == nil then return false end if str ~= MOUSE_LOOKUP[primary] and str ~= MOUSE_LOOKUP[secondary] then return false end - local mousepos = fn.getmousepos() + local mousepos = vim.deepcopy(fn.getmousepos()) + -- Ignore clicks on the command line. + if mousepos.screenrow > vim.go.lines - vim.go.cmdheight then + return false + end + -- Ignore clicks on the tabline. When the click is on a floating window + -- covering the tabline, mousepos.winid will be set to that floating window's + -- winid. Otherwise, mousepos.winid would correspond to an ordinary window ID + -- (seemingly for the window below the tabline). + if fn.win_screenpos(1) == {2, 1} -- Checks for presence of a tabline. + and mousepos.screenrow == 1 + and is_ordinary_window(mousepos.winid) then + return false + end + -- Adjust for a winbar. + if mousepos.winid > 0 + and to_bool(tbl_get(fn.getwininfo(mousepos.winid)[1], 'winbar', 0)) then + mousepos.winrow = mousepos.winrow - 1 + end + -- Adjust for floating window borders. local mouse_winid = mousepos.winid - -- TODO: return if mouse over tabline, winbar, command line, etc. (see read_input_stream) + local config = api.nvim_win_get_config(mouse_winid) + local is_float = tbl_get(config, 'relative', '') ~= '' + if is_float then + local border = config.border + if border ~= nil and islist(border) and #border == 8 then + if border[BORDER_TOP] ~= '' then + mousepos.winrow = mousepos.winrow - 1 + end + if border[BORDER_LEFT] ~= '' then + mousepos.wincol = mousepos.wincol - 1 + end + end + end local mouse_row = mousepos.winrow local mouse_col = mousepos.wincol local props = get_scrollview_bar_props(mouse_winid) @@ -3362,11 +3409,13 @@ local should_handle_mouse = function(str) return true, data end --- pcall is not necessary here to avoid an error in some cases (Neovim --- #17273), since that would be necessary for nvim<0.8, where this code would --- not execute (the on_key handling for the mouse requires nvim==0.11, for the --- ability to ignore the key by returning the empty string). +-- With nvim<0.11, mouse functionality is handled with mappings, not +-- vim.on_key, since the on_key handling for the mouse requires nvim==0.11, +-- for the ability to ignore the key by returning the empty string. if to_bool(fn.has('nvim-0.11')) then -- Neovim 0.11 for ignoring keys + -- pcall is not necessary here to avoid an error in some cases (Neovim + -- #17273), since that would be necessary for nvim<0.8, where this code + -- would not execute (this only runs on nvim>=0.11). vim.on_key(function(str) local should_handle, data = should_handle_mouse(str) if should_handle then @@ -3676,12 +3725,14 @@ return { first = first, fold_count_exceeds = fold_count_exceeds, get_sign_eligible_windows = get_sign_eligible_windows, + handle_mouse = handle_mouse, last = last, legend = legend, next = next, prev = prev, refresh = refresh, set_state = set_state, + should_handle_mouse = should_handle_mouse, with_win_workspace = with_win_workspace, -- Sign registration/configuration From 5ab73f9e372b67353c3879718aa899c62cfd15d5 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 3 Mar 2025 16:39:32 -0500 Subject: [PATCH 6/6] Remove v0.11 requirement for mouse --- README.md | 3 +-- doc/scrollview.txt | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bf9b346..93ef890 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,7 @@ scrollview-configuration`). ## Requirements * `nvim>=0.6` -* Mouse functionality requires mouse support (see `:help 'mouse'`) and - `nvim>=0.11` +* Mouse functionality requires mouse support (see `:help 'mouse'`) * Signs require `nvim>=0.9` ## Installation diff --git a/doc/scrollview.txt b/doc/scrollview.txt index 223b555..e5a60b3 100644 --- a/doc/scrollview.txt +++ b/doc/scrollview.txt @@ -20,8 +20,7 @@ signs. The plugin is customizable (see |scrollview-configuration|). 1. Requirements *scrollview-requirements* * `nvim>=0.6` -* Scrollbar mouse functionality requires mouse support (see |'mouse'|) and - `nvim>=0.11` +* Scrollbar mouse functionality requires mouse support (see |'mouse'|) * Signs require `nvim>=0.9` ============================================================================