diff --git a/CHANGELOG.md b/CHANGELOG.md index 79bd7d109..6d4a49128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt ### Added +* Added [#502](https://github.com/NeoVintageous/NeoVintageous/issues/502): Visual bell styles * Added [#500](https://github.com/NeoVintageous/NeoVintageous/issues/500): `-` in Visual line mode * Added [#492](https://github.com/NeoVintageous/NeoVintageous/issues/492): `(` and `)` in Visual line mode * Added [#499](https://github.com/NeoVintageous/NeoVintageous/issues/499): `` in Visual line mode diff --git a/Preferences.sublime-settings b/Preferences.sublime-settings index 863a1bd02..a591ee009 100644 --- a/Preferences.sublime-settings +++ b/Preferences.sublime-settings @@ -107,5 +107,10 @@ // Propagate copy actions to the system clipboard. // {not in Vim} - "vintageous_use_sys_clipboard": false + "vintageous_use_sys_clipboard": false, + + // Visual bell type. + // Valid values are: blink, view, or views. + // {not in Vim} + "vintageous_bell": "blink" } diff --git a/nv/commands.py b/nv/commands.py index eb8c6054a..f1f14a0ad 100644 --- a/nv/commands.py +++ b/nv/commands.py @@ -56,7 +56,7 @@ from NeoVintageous.nv.mappings import mappings_resolve from NeoVintageous.nv.state import init_state from NeoVintageous.nv.state import State -from NeoVintageous.nv.ui import ui_blink +from NeoVintageous.nv.ui import ui_bell from NeoVintageous.nv.ui import ui_cmdline_prompt from NeoVintageous.nv.ui import ui_highlight_yank from NeoVintageous.nv.ui import ui_highlight_yank_clear @@ -387,12 +387,12 @@ def _next_history(self, edit, backwards): if count == 0: _nv_cmdline_feed_key.LAST_HISTORY_ITEM_INDEX = None - return ui_blink() + return ui_bell() if abs(_nv_cmdline_feed_key.LAST_HISTORY_ITEM_INDEX) > count: _nv_cmdline_feed_key.LAST_HISTORY_ITEM_INDEX = -count - return ui_blink() + return ui_bell() if _nv_cmdline_feed_key.LAST_HISTORY_ITEM_INDEX >= 0: _nv_cmdline_feed_key.LAST_HISTORY_ITEM_INDEX = 0 @@ -400,7 +400,7 @@ def _next_history(self, edit, backwards): if self.view.size() > 1: return self.view.erase(edit, Region(1, self.view.size())) else: - return ui_blink() + return ui_bell() if self.view.size() > 1: self.view.erase(edit, Region(1, self.view.size())) @@ -627,7 +627,7 @@ def _handle_missing_command(self, state, command): state.mode = NORMAL state.reset_command_data() - ui_blink() + ui_bell() return True @@ -741,7 +741,7 @@ def run(self, keys, repeat_count=None, check_user_mappings=True): if motion_data is None: state.reset_command_data() - ui_blink() + ui_bell() return self.window.run_command(motion_data['motion'], motion_data['motion_args']) @@ -773,7 +773,7 @@ def collect_input(self): except IndexError: _log.debug('could not find a command to collect more user input') - ui_blink() + ui_bell() finally: self.state.non_interactive = False @@ -915,7 +915,7 @@ def f(view, s): if self.has_sel_changed(): regions_transformer(self.view, f) else: - ui_blink() + ui_bell() else: regions_transformer(self.view, f) @@ -940,7 +940,7 @@ def f(view, s): if self.has_sel_changed(): regions_transformer(self.view, f) else: - ui_blink() + ui_bell() else: regions_transformer(self.view, f) @@ -997,7 +997,7 @@ def shrink(view, s): for s in self.old_sel: self.view.sel().add(s.begin()) else: - ui_blink() + ui_bell() enter_normal_mode(self.view, mode) @@ -1029,7 +1029,7 @@ def run(self, count=1, **kwargs): self.view.run_command('redo') if self.view.change_count() == change_count_before: - return ui_blink() + return ui_bell() # Fix EOL issue. # See https://github.com/SublimeTextIssues/Core/issues/2121. @@ -1357,7 +1357,7 @@ def f(view, s): return Region(s.a, s.b) else: if s.empty() and (s.b == self.view.size()): - ui_blink() + ui_bell() return s @@ -1396,7 +1396,7 @@ def run(self, edit, mode=None, force=False): # Abort if we are at EOF -- no newline char to hold on to. if any(s.b == self.view.size() for s in self.view.sel()): - return ui_blink() + return ui_bell() self.view.run_command('_enter_visual_line_mode_impl', {'mode': mode}) state.enter_visual_line_mode() @@ -1450,7 +1450,7 @@ def run(self, mode=None, count=None, repeat_data=None): state.mode = NORMAL if repeat_data is None: - ui_blink() + ui_bell() return # TODO: Find out if the user actually meant '1'. @@ -1464,9 +1464,9 @@ def run(self, mode=None, count=None, repeat_data=None): state.restore_visual_data(visual_data) elif not visual_data and (mode == VISUAL): # Can't repeat normal mode commands in visual mode. - return ui_blink() + return ui_bell() elif mode not in (VISUAL, VISUAL_LINE, NORMAL, INTERNAL_NORMAL, INSERT): - return ui_blink() + return ui_bell() if type_ == 'vi': self.window.run_command('_nv_process_notation', {'keys': seq_or_cmd, 'repeat_count': count}) @@ -1580,7 +1580,7 @@ def restore(): if mode not in (INTERNAL_NORMAL, VISUAL): enter_normal_mode(self.view, mode) - ui_blink() + ui_bell() return self.save_sel() @@ -1637,12 +1637,12 @@ def run(self, edit, count=1, mode=None, motion=None, register=None): if not self.has_sel_changed(): enter_normal_mode(self.view, mode) - ui_blink() + ui_bell() return if all(s.empty() for s in self.view.sel()): enter_normal_mode(self.view, mode) - ui_blink() + ui_bell() return self.state.registers.op_delete(register=register, linewise=(mode == VISUAL_LINE)) @@ -1934,7 +1934,7 @@ def select(view, s): if mode not in (VISUAL, VISUAL_LINE, VISUAL_BLOCK, INTERNAL_NORMAL): enter_normal_mode(self.view, mode) - ui_blink() + ui_bell() return self.save_sel() @@ -1960,7 +1960,7 @@ def select(view, s): if mode not in (VISUAL, VISUAL_LINE, VISUAL_BLOCK, INTERNAL_NORMAL): enter_normal_mode(self.view, mode) - ui_blink() + ui_bell() return if mode == INTERNAL_NORMAL and all(self.view.line(s.b).empty() for s in self.view.sel()): @@ -2111,7 +2111,7 @@ def indent_from_begin(view, s, level=1): if motion: self.view.run_command(motion['motion'], motion['motion_args']) elif mode not in (VISUAL, VISUAL_LINE): - return ui_blink() + return ui_bell() for i in range(count): self.view.run_command('indent') @@ -2134,7 +2134,7 @@ def f(view, s): if motion: self.view.run_command(motion['motion'], motion['motion_args']) elif mode not in (VISUAL, VISUAL_LINE): - return ui_blink() + return ui_bell() for i in range(count): self.view.run_command('unindent') @@ -2152,7 +2152,7 @@ def f(view, s): if motion: self.view.run_command(motion['motion'], motion['motion_args']) elif mode not in (VISUAL, VISUAL_LINE): - return ui_blink() + return ui_bell() self.view.run_command('reindent', {'force_indent': False}) @@ -2589,7 +2589,7 @@ def run(self, edit, count=1, mode=None, subtract=False): pts = self.find_next_num(regs) if not pts: - return ui_blink() + return ui_bell() end_sels = [] count = count if not subtract else -count @@ -2828,14 +2828,14 @@ def run(self, name=None, mode=None, count=1): return if name not in tuple('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"'): - return ui_blink("E354: Invalid register name: '" + name + "'") + return ui_bell("E354: Invalid register name: '" + name + "'") state.start_recording() self.__class__._current = name except (AttributeError, ValueError): state.stop_recording() self.__class__._current = None - ui_blink() + ui_bell() class _vi_at(IrreversibleTextCommand): @@ -2844,20 +2844,20 @@ class _vi_at(IrreversibleTextCommand): def run(self, name, mode=None, count=1): if name not in tuple('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".=*+@'): - return ui_blink("E354: Invalid register name: '" + name + "'") + return ui_bell("E354: Invalid register name: '" + name + "'") if name == '@': name = self._last_used if not name: - return ui_blink('E748: No previously used register') + return ui_bell('E748: No previously used register') try: cmds = State.macro_registers[name] except (KeyError, ValueError): - return ui_blink() + return ui_bell() if not cmds: - ui_blink() + ui_bell() return self.__class__._last_used = name @@ -2899,7 +2899,7 @@ def f(view, s): if self.view.line(first.end() - 1).empty(): enter_normal_mode(self.view, mode) - ui_blink() + ui_bell() return self.view.sel().clear() @@ -3015,7 +3015,7 @@ def f(view, s): self.view.run_command(motion['motion'], motion['motion_args']) if not self.has_sel_changed(): - ui_blink() + ui_bell() enter_normal_mode(self.view, mode) return @@ -3119,7 +3119,7 @@ def run(self, mode=None, count=1): self.state.display_status() return - ui_blink() + ui_bell() status_message('no available search matches') self.state.reset_command_data() @@ -3147,7 +3147,7 @@ def run(self, edit, mode=None, register='"'): raise ValueError('wrong mode') if (len(self.view.sel()) > 1 or not self.view.sel()[0].empty()): - return ui_blink() + return ui_bell() s = self.view.sel()[0] line_begin = self.view.text_point(row_at(self.view, s.b), 0) @@ -3160,7 +3160,7 @@ def run(self, edit, mode=None, register='"'): state.reset_command_data() return - ui_blink() + ui_bell() def show_matches(self, items): self.view.window().show_quick_panel(items, self.replace, MONOSPACE_FONT) @@ -4575,7 +4575,7 @@ def f(view, s): number_of_scroll_lines = count if count >= 1 else get_option_scroll(self.view) scroll_target_pt = get_scroll_up_target_pt(self.view, number_of_scroll_lines) if scroll_target_pt is None: - return ui_blink() + return ui_bell() regions_transformer(self.view, f) if not self.view.visible_region().contains(0): @@ -4600,7 +4600,7 @@ def f(view, s): number_of_scroll_lines = count if count >= 1 else get_option_scroll(self.view) scroll_target_pt = get_scroll_down_target_pt(self.view, number_of_scroll_lines) if scroll_target_pt is None: - return ui_blink() + return ui_bell() regions_transformer(self.view, f) if not self.view.visible_region().contains(self.view.size()): @@ -5102,7 +5102,7 @@ def advance(view, s): return Region(min(row_start + mid_pt, line.b - 1)) if mode != NORMAL: - return ui_blink() + return ui_bell() regions_transformer(self.view, advance) diff --git a/nv/ex_cmds.py b/nv/ex_cmds.py index e18086d94..67db9e917 100644 --- a/nv/ex_cmds.py +++ b/nv/ex_cmds.py @@ -46,7 +46,7 @@ from NeoVintageous.nv.mappings import mappings_add from NeoVintageous.nv.mappings import mappings_remove from NeoVintageous.nv.state import State -from NeoVintageous.nv.ui import ui_blink +from NeoVintageous.nv.ui import ui_bell from NeoVintageous.nv.vi.search import find_all_in_range from NeoVintageous.nv.vi.settings import get_cmdline_cwd from NeoVintageous.nv.vi.settings import set_cmdline_cwd @@ -1232,12 +1232,12 @@ def ex_wq(window, view, forceit=False, **kwargs): def ex_wqall(window, **kwargs): if not all(view.file_name() for view in window.views()): - ui_blink() + ui_bell() return status_message("E32: No file name") if any(view.is_read_only() for view in window.views()): - ui_blink() + ui_bell() return status_message("E45: 'readonly' option is set (add ! to override)") @@ -1334,12 +1334,12 @@ def _do_write(window, view, file_name, forceit, line_range): if not forceit: if os.path.exists(fname): - ui_blink() + ui_bell() return status_message("E13: File exists (add ! to override)") if _check_is_readonly(fname): - ui_blink() + ui_bell() return status_message("E45: 'readonly' option is set (add ! to override)") @@ -1373,7 +1373,7 @@ def _do_write(window, view, file_name, forceit, line_range): read_only = (_check_is_readonly(view.file_name()) or view.is_read_only()) if read_only and not forceit: - ui_blink() + ui_bell() return status_message("E45: 'readonly' option is set (add ! to override)") diff --git a/nv/goto.py b/nv/goto.py index 2e67a7939..54925a7d6 100644 --- a/nv/goto.py +++ b/nv/goto.py @@ -19,7 +19,7 @@ from sublime import version from NeoVintageous.nv.jumplist import jumplist_update -from NeoVintageous.nv.ui import ui_blink +from NeoVintageous.nv.ui import ui_bell from NeoVintageous.nv.utils import resolve_visual_line_target from NeoVintageous.nv.utils import resolve_visual_target from NeoVintageous.nv.vi.text_objects import find_next_lone_bracket @@ -154,7 +154,7 @@ def goto_prev_target(view, mode, count, target): brackets = targets.get(target) if not brackets or mode not in (NORMAL, VISUAL, VISUAL_LINE): - ui_blink() + ui_bell() return def f(view, s): @@ -196,7 +196,7 @@ def goto_next_target(view, mode, count, target): brackets = targets.get(target) if not brackets or mode not in (NORMAL, VISUAL, VISUAL_LINE): - ui_blink() + ui_bell() return def f(view, s): diff --git a/nv/plugin_commentary.py b/nv/plugin_commentary.py index 994dd4886..8caa31aac 100644 --- a/nv/plugin_commentary.py +++ b/nv/plugin_commentary.py @@ -26,7 +26,7 @@ from NeoVintageous.nv.plugin import VISUAL from NeoVintageous.nv.plugin import VISUAL_BLOCK from NeoVintageous.nv.plugin import VISUAL_LINE -from NeoVintageous.nv.ui import ui_blink +from NeoVintageous.nv.ui import ui_bell from NeoVintageous.nv.vi.utils import next_non_white_space_char from NeoVintageous.nv.vi.utils import regions_transformer from NeoVintageous.nv.vi.utils import regions_transformer_reversed @@ -122,7 +122,7 @@ def f(view, s): if motion: view.run_command(motion['motion'], motion['motion_args']) elif mode not in (VISUAL, VISUAL_LINE): - return ui_blink() + return ui_bell() view.run_command('toggle_comment', {'block': False}) @@ -193,7 +193,7 @@ def f(view, s): if motion: view.run_command(motion['motion'], motion['motion_args']) elif mode not in (VISUAL, VISUAL_LINE): - return ui_blink() + return ui_bell() view.run_command('toggle_comment', {'block': True}) regions_transformer(view, f) diff --git a/nv/ui.py b/nv/ui.py index d7387b5b9..c4e91f9a0 100644 --- a/nv/ui.py +++ b/nv/ui.py @@ -27,76 +27,60 @@ from NeoVintageous.nv.vim import status_message -# TODO Implement bell. See :h 'belloff'. 'belloff' defaults to 'all' in Neovim. See https://github.com/neovim/neovim/issues/2676. # noqa: E501 -# TODO How to make the bell theme adaptive i.e. work nice in light AND dark color schemes. -def _ui_bell(): +def ui_bell(msg=None): + if msg: + status_message(msg) + window = active_window() + if not window: + return view = window.active_view() if not view: return settings = view.settings() - if settings.get('vintageous_belloff') == 'all': return + style = settings.get('vintageous_bell') theme = 'Packages/NeoVintageous/res/Bell.tmTheme' - duration = int(0.3 * 1000) + times = 4 + delay = 55 + + if style == 'view': + settings.set('color_scheme', theme) - if settings.get('vintageous_wip_bell_all_active_views'): + def remove_bell(): + settings.erase('color_scheme') + + set_timeout(remove_bell, duration) + elif style == 'views': views = [] for group in range(window.num_groups()): view = window.active_view_in_group(group) if view: - settings = settings - settings.set('color_scheme', theme) + view.settings().set('color_scheme', theme) views.append(view) def remove_bell(): for view in views: - settings.erase('color_scheme') - - set_timeout(remove_bell, duration) - - else: - def remove_bell(): - settings.erase('color_scheme') + view.settings().erase('color_scheme') - settings.set('color_scheme', theme) set_timeout(remove_bell, duration) - - -# TODO [refactor] Rework this to use the _ui_bell(). -# TODO [refactor] Rework this to require a view or settings object. -def ui_blink(msg=None, times=4, delay=55): - if msg: - status_message(msg) - - view = active_window().active_view() - if not view: - return - - settings = view.settings() - - if settings.get('vintageous_belloff') == 'all': - return - - if settings.get('vintageous_wip'): - return _ui_bell() - - # Ensure we leave the setting as we found it. - times = times if (times % 2) == 0 else times + 1 - - def do_blink(): - nonlocal times - if times > 0: - settings.set('highlight_line', not settings.get('highlight_line')) - times -= 1 - set_timeout(do_blink, delay) - - do_blink() + elif style == 'blink': + # Ensure we leave the setting as we found it. + times = times if (times % 2) == 0 else times + 1 + + def do_blink(): + nonlocal times + if times > 0: + settings.set('highlight_line', not settings.get('highlight_line')) + times -= 1 + set_timeout(do_blink, delay) + + do_blink() def ui_cmdline_prompt(window, initial_text, on_done, on_change, on_cancel): diff --git a/res/doc/neovintageous.txt b/res/doc/neovintageous.txt index 7b45a48e5..9d4d89d04 100644 --- a/res/doc/neovintageous.txt +++ b/res/doc/neovintageous.txt @@ -445,6 +445,11 @@ OPTIONS SUMMARY *nv-option-summary* boolean (default off) Propagate copy actions to the system clipboard. + *'vintageous_bell'* +'vintageous_bell' string (default "blink") + Style of visual bell. The available styles are: blink (default), view, + or views. + *'vintageous_belloff'* 'vintageous_belloff' string (default "") Specifies for which events the bell (visual blink) will not be rung. It diff --git a/tests/unittest.py b/tests/unittest.py index 2fdda49a2..7a1c466d3 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -846,7 +846,7 @@ def test_name(self): """ def wrapper(f): - @mock.patch('NeoVintageous.nv.commands.ui_blink') + @mock.patch('NeoVintageous.nv.commands.ui_bell') def wrapped(self, *args, **kwargs): self.bells = [ args[-1]