From 89d5fa64a64e49aa549032fe76c36a2a79bcbf21 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Wed, 29 May 2019 23:51:09 +1000 Subject: [PATCH 01/16] Fix comment --- pyvim/window_arrangement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyvim/window_arrangement.py b/pyvim/window_arrangement.py index 3984168..c8220d9 100644 --- a/pyvim/window_arrangement.py +++ b/pyvim/window_arrangement.py @@ -22,7 +22,7 @@ class HSplit(list): class VSplit(list): - """ Horizontal split. """ + """ Vertical split. """ class Window(object): From 127baefb42bbaca5bf1ce31e2af6d30bade1e24f Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Thu, 30 May 2019 00:34:55 +1000 Subject: [PATCH 02/16] Fix C-t (indent_line) --- pyvim/key_bindings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyvim/key_bindings.py b/pyvim/key_bindings.py index e3209ee..477100e 100644 --- a/pyvim/key_bindings.py +++ b/pyvim/key_bindings.py @@ -52,7 +52,7 @@ def indent_line(event): """ Indent current line. """ - b = event.application.current_buffer + b = event.app.current_buffer # Move to start of line. pos = b.document.get_start_of_line_position(after_whitespace=True) From ce151fcc304395ba1444a3cca8724653d8dd6726 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Thu, 30 May 2019 00:49:34 +1000 Subject: [PATCH 03/16] Add :ls and :files aliases for :buffers --- pyvim/commands/commands.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyvim/commands/commands.py b/pyvim/commands/commands.py index def2a59..1200938 100644 --- a/pyvim/commands/commands.py +++ b/pyvim/commands/commands.py @@ -208,6 +208,8 @@ def buffer_add(editor, location): editor.window_arrangement.open_buffer(location) +@cmd('files') +@cmd('ls') @cmd('buffers') def buffer_list(editor): """ From 6aaea692016992106025c43bcc2956b60e4ef0f0 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Thu, 30 May 2019 02:49:10 +1000 Subject: [PATCH 04/16] Fix quit_all (:qa) --- pyvim/commands/commands.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyvim/commands/commands.py b/pyvim/commands/commands.py index 1200938..c6511f9 100644 --- a/pyvim/commands/commands.py +++ b/pyvim/commands/commands.py @@ -305,7 +305,11 @@ def quit_all(editor, force=False): """ Quit all. """ - quit(editor, all_=True, force=force) + ebs = editor.window_arrangement.editor_buffers + if not force and any(eb.has_unsaved_changes for eb in ebs): + editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT) + else: + editor.application.exit() @location_cmd('w', accepts_force=True) From fac9f7119cd3ececcdbd428d0452291a95279b23 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Thu, 30 May 2019 02:52:24 +1000 Subject: [PATCH 05/16] Change write_and_quit_all (:wqa) to use quit_all() --- pyvim/commands/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyvim/commands/commands.py b/pyvim/commands/commands.py index c6511f9..8971554 100644 --- a/pyvim/commands/commands.py +++ b/pyvim/commands/commands.py @@ -358,7 +358,7 @@ def write_and_quit_all(editor): editor.show_message(_NO_FILE_NAME) else: eb.write() - quit(editor, all_=True, force=False) + quit_all(editor, force=False) @cmd('h') From ae6dbc640cf4a62b7daf2198c8ff9973cdffbf52 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Thu, 30 May 2019 03:38:02 +1000 Subject: [PATCH 06/16] Implement {WindowArrangement,TabPage}.get_windows_for_buffer() --- pyvim/window_arrangement.py | 8 +++++++ tests/conftest.py | 8 +++++-- tests/test_window_arrangements.py | 36 +++++++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/pyvim/window_arrangement.py b/pyvim/window_arrangement.py index c8220d9..babd0c6 100644 --- a/pyvim/window_arrangement.py +++ b/pyvim/window_arrangement.py @@ -103,6 +103,10 @@ def _get_split_parent(self, split): if child == split: return parent + def get_windows_for_buffer(self, editor_buffer): + """ Return a list of all windows in this tab page. """ + return (window for _, window in self._walk_through_windows() if window.editor_buffer == editor_buffer) + def _split(self, split_cls, editor_buffer=None): """ Split horizontal or vertical. @@ -260,6 +264,10 @@ def get_editor_buffer_for_buffer_name(self, buffer_name): if eb.buffer_name == buffer_name: return eb + def get_windows_for_buffer(self, editor_buffer): + """ Return a list of all windows in this tab page. """ + return (b for t in self.tab_pages for b in t.get_windows_for_buffer(editor_buffer)) + def close_window(self): """ Close active window of active tab. diff --git a/tests/conftest.py b/tests/conftest.py index 4fda75b..df96dd6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,11 +2,10 @@ import pytest -from prompt_toolkit.buffer import Buffer from prompt_toolkit.output import DummyOutput from prompt_toolkit.input import DummyInput from pyvim.editor import Editor -from pyvim.window_arrangement import TabPage, EditorBuffer, Window +from pyvim.window_arrangement import TabPage, EditorBuffer, Window, WindowArrangement @pytest.fixture @@ -27,3 +26,8 @@ def window(editor_buffer): @pytest.fixture def tab_page(window): return TabPage(window) + + +@pytest.fixture +def window_arrangement(editor): + return WindowArrangement(editor) diff --git a/tests/test_window_arrangements.py b/tests/test_window_arrangements.py index b3d02bd..8a25287 100644 --- a/tests/test_window_arrangements.py +++ b/tests/test_window_arrangements.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals -from prompt_toolkit.buffer import Buffer -from pyvim.window_arrangement import EditorBuffer, VSplit +from pyvim.window_arrangement import EditorBuffer, VSplit, TabPage, Window def test_initial(window, tab_page): @@ -18,3 +17,36 @@ def test_vsplit(editor, tab_page): assert isinstance(tab_page.root, VSplit) assert len(tab_page.root) == 2 + + +def test_tab_page_get_windows_for_buffer(editor): + # Create new buffer. + eb1 = EditorBuffer(editor) + eb2 = EditorBuffer(editor) + + # Insert in tab, by splitting. + tab_page1 = TabPage(Window(eb1)) + tab_page1.vsplit(eb1) + tab_page1.vsplit(eb2) + tab_page1.hsplit(eb1) + + windows = list(tab_page1.get_windows_for_buffer(eb1)) + assert all(w.editor_buffer == eb1 for w in windows) + assert len(windows) == 3 + +def test_window_arrangement_get_windows_for_buffer(editor, window_arrangement): + # Create new buffer. + eb1 = EditorBuffer(editor) + eb2 = EditorBuffer(editor) + + # Insert in tab, by splitting. + tab_page1 = TabPage(Window(eb1)) + tab_page1.vsplit(eb1) + tab_page1.vsplit(eb2) + tab_page1.hsplit(eb1) + tab_page2 = TabPage(Window(eb1)) + + window_arrangement.tab_pages[:] = [tab_page1, tab_page2] + windows = list(window_arrangement.get_windows_for_buffer(eb1)) + assert all(w.editor_buffer == eb1 for w in windows) + assert len(windows) == 4 From b81a5e1e1fbca6f3d2f667adb122de68d35fe457 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Thu, 30 May 2019 03:56:29 +1000 Subject: [PATCH 07/16] Fix quit (:q) to close split/tab/pyvim to match vim --- pyvim/commands/commands.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/pyvim/commands/commands.py b/pyvim/commands/commands.py index 8971554..38e8964 100644 --- a/pyvim/commands/commands.py +++ b/pyvim/commands/commands.py @@ -281,20 +281,18 @@ def buffer_edit(editor, location, force=False): @cmd('q', accepts_force=True) @cmd('quit', accepts_force=True) -def quit(editor, all_=False, force=False): +def quit(editor, force=False): """ Quit. """ - ebs = editor.window_arrangement.editor_buffers - - # When there are buffers that have unsaved changes, show balloon. - if not force and any(eb.has_unsaved_changes for eb in ebs): + eb = editor.window_arrangement.active_editor_buffer + eb_is_open_in_another_window = len(list(editor.window_arrangement.get_windows_for_buffer(eb))) > 1 + if not force and eb.has_unsaved_changes and not eb_is_open_in_another_window: editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT) - - # When there is more than one buffer open. - elif not all_ and len(ebs) > 1: - editor.show_message('%i more files to edit' % (len(ebs) - 1)) - + elif editor.window_arrangement.active_tab.window_count() > 1: + editor.window_arrangement.close_window() + elif len(editor.window_arrangement.tab_pages) > 1: + editor.window_arrangement.close_tab() else: editor.application.exit() From ac8a5f8a811953afa8a5f4b42fde06cb9c6c37c6 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Fri, 31 May 2019 02:40:06 +1000 Subject: [PATCH 08/16] Refactor tests --- tests/conftest.py | 11 ++++++++++ tests/test_window_arrangements.py | 34 ++++++++----------------------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index df96dd6..60bf78b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,6 +28,17 @@ def tab_page(window): return TabPage(window) +@pytest.fixture +def tab_page_with_splits(editor_buffer, window): + editor_buffer2 = EditorBuffer(editor) + + tab_page = TabPage(Window(editor_buffer)) + tab_page.vsplit(editor_buffer) + tab_page.vsplit(editor_buffer2) + tab_page.hsplit(editor_buffer) + return tab_page + + @pytest.fixture def window_arrangement(editor): return WindowArrangement(editor) diff --git a/tests/test_window_arrangements.py b/tests/test_window_arrangements.py index 8a25287..d1b8b35 100644 --- a/tests/test_window_arrangements.py +++ b/tests/test_window_arrangements.py @@ -19,34 +19,18 @@ def test_vsplit(editor, tab_page): assert len(tab_page.root) == 2 -def test_tab_page_get_windows_for_buffer(editor): - # Create new buffer. - eb1 = EditorBuffer(editor) - eb2 = EditorBuffer(editor) - - # Insert in tab, by splitting. - tab_page1 = TabPage(Window(eb1)) - tab_page1.vsplit(eb1) - tab_page1.vsplit(eb2) - tab_page1.hsplit(eb1) +def test_tab_page_get_windows_for_buffer(editor, editor_buffer, tab_page_with_splits): + tab_page1 = tab_page_with_splits - windows = list(tab_page1.get_windows_for_buffer(eb1)) - assert all(w.editor_buffer == eb1 for w in windows) + windows = list(tab_page1.get_windows_for_buffer(editor_buffer)) + assert all(w.editor_buffer == editor_buffer for w in windows) assert len(windows) == 3 -def test_window_arrangement_get_windows_for_buffer(editor, window_arrangement): - # Create new buffer. - eb1 = EditorBuffer(editor) - eb2 = EditorBuffer(editor) - - # Insert in tab, by splitting. - tab_page1 = TabPage(Window(eb1)) - tab_page1.vsplit(eb1) - tab_page1.vsplit(eb2) - tab_page1.hsplit(eb1) - tab_page2 = TabPage(Window(eb1)) +def test_window_arrangement_get_windows_for_buffer(editor, editor_buffer, tab_page_with_splits, window_arrangement): + tab_page1 = tab_page_with_splits + tab_page2 = TabPage(Window(editor_buffer)) window_arrangement.tab_pages[:] = [tab_page1, tab_page2] - windows = list(window_arrangement.get_windows_for_buffer(eb1)) - assert all(w.editor_buffer == eb1 for w in windows) + windows = list(window_arrangement.get_windows_for_buffer(editor_buffer)) + assert all(w.editor_buffer == editor_buffer for w in windows) assert len(windows) == 4 From 0b1b694015b752427c5ddb0b4ca4c747b8e8131f Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Fri, 31 May 2019 22:41:13 +1000 Subject: [PATCH 09/16] Move handling of closing tab to close_window() and write test --- pyvim/commands/commands.py | 8 +++----- pyvim/window_arrangement.py | 5 ++++- tests/test_window_arrangements.py | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/pyvim/commands/commands.py b/pyvim/commands/commands.py index 38e8964..9a1c7f0 100644 --- a/pyvim/commands/commands.py +++ b/pyvim/commands/commands.py @@ -289,12 +289,10 @@ def quit(editor, force=False): eb_is_open_in_another_window = len(list(editor.window_arrangement.get_windows_for_buffer(eb))) > 1 if not force and eb.has_unsaved_changes and not eb_is_open_in_another_window: editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT) - elif editor.window_arrangement.active_tab.window_count() > 1: - editor.window_arrangement.close_window() - elif len(editor.window_arrangement.tab_pages) > 1: - editor.window_arrangement.close_tab() - else: + elif editor.window_arrangement.active_tab.window_count() == 1 and len(editor.window_arrangement.tab_pages) == 1: editor.application.exit() + else: + editor.window_arrangement.close_window() @cmd('qa', accepts_force=True) diff --git a/pyvim/window_arrangement.py b/pyvim/window_arrangement.py index babd0c6..ef3a37c 100644 --- a/pyvim/window_arrangement.py +++ b/pyvim/window_arrangement.py @@ -272,7 +272,10 @@ def close_window(self): """ Close active window of active tab. """ - self.active_tab.close_active_window() + if self.active_tab.window_count() > 1: + self.active_tab.close_active_window() + else: + self.close_tab() # Clean up buffers. self._auto_close_new_empty_buffers() diff --git a/tests/test_window_arrangements.py b/tests/test_window_arrangements.py index d1b8b35..0a52a4e 100644 --- a/tests/test_window_arrangements.py +++ b/tests/test_window_arrangements.py @@ -26,6 +26,7 @@ def test_tab_page_get_windows_for_buffer(editor, editor_buffer, tab_page_with_sp assert all(w.editor_buffer == editor_buffer for w in windows) assert len(windows) == 3 + def test_window_arrangement_get_windows_for_buffer(editor, editor_buffer, tab_page_with_splits, window_arrangement): tab_page1 = tab_page_with_splits tab_page2 = TabPage(Window(editor_buffer)) @@ -34,3 +35,21 @@ def test_window_arrangement_get_windows_for_buffer(editor, editor_buffer, tab_pa windows = list(window_arrangement.get_windows_for_buffer(editor_buffer)) assert all(w.editor_buffer == editor_buffer for w in windows) assert len(windows) == 4 + + +def test_close_window_closes_split(editor): + editor.window_arrangement.create_tab() + editor.window_arrangement.hsplit() + assert len(editor.window_arrangement.tab_pages) == 2 + + assert editor.window_arrangement.active_tab.window_count() == 2 + editor.window_arrangement.close_window() + assert editor.window_arrangement.active_tab.window_count() == 1 + + +def test_close_window_also_closes_empty_tab(editor): + editor.window_arrangement.create_tab() + + assert len(editor.window_arrangement.tab_pages) == 2 + editor.window_arrangement.close_window() + assert len(editor.window_arrangement.tab_pages) == 1 From 8e928404956c06f3276330c19b645b87bd159462 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Sat, 1 Jun 2019 02:16:25 +1000 Subject: [PATCH 10/16] Implement :wq to use quit() --- pyvim/commands/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyvim/commands/commands.py b/pyvim/commands/commands.py index 9a1c7f0..d9e17fd 100644 --- a/pyvim/commands/commands.py +++ b/pyvim/commands/commands.py @@ -330,7 +330,7 @@ def write_and_quit(editor, location, force=False): Write file and quit. """ write(editor, location, force=force) - editor.application.exit() + quit(editor) @cmd('cq') From 43c2825e1b791ca903cf95cd338ecf0c668ba34c Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Sat, 1 Jun 2019 02:17:02 +1000 Subject: [PATCH 11/16] Implement ZZ (normal mode map for :wq) --- pyvim/key_bindings.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyvim/key_bindings.py b/pyvim/key_bindings.py index 477100e..5fe101e 100644 --- a/pyvim/key_bindings.py +++ b/pyvim/key_bindings.py @@ -4,6 +4,8 @@ from prompt_toolkit.filters import Condition, has_focus, vi_insert_mode, vi_navigation_mode from prompt_toolkit.key_binding import KeyBindings +from .commands.commands import write_and_quit + import os __all__ = ( @@ -38,6 +40,14 @@ def vi_buffer_focussed(): in_insert_mode = vi_insert_mode & vi_buffer_focussed in_navigation_mode = vi_navigation_mode & vi_buffer_focussed + @kb.add('Z', 'Z', filter=in_navigation_mode) + def _(event): + """ + Write and quit. + """ + write_and_quit(editor, None) + editor.sync_with_prompt_toolkit() + @kb.add('c-t') def _(event): """ From 9aea51f768730654d3a2e502e2ea79e0cfd858c3 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Sat, 1 Jun 2019 02:47:26 +1000 Subject: [PATCH 12/16] Implement (suspend_to_background) --- pyvim/key_bindings.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyvim/key_bindings.py b/pyvim/key_bindings.py index 5fe101e..7ca70a1 100644 --- a/pyvim/key_bindings.py +++ b/pyvim/key_bindings.py @@ -48,6 +48,13 @@ def _(event): write_and_quit(editor, None) editor.sync_with_prompt_toolkit() + @kb.add('c-z', filter=in_navigation_mode) + def _(event): + """ + Suspend process to background. + """ + event.app.suspend_to_background() + @kb.add('c-t') def _(event): """ From ffcec1b3bc490a5bb21d46ced04375e1e6ecb326 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Sat, 1 Jun 2019 03:14:37 +1000 Subject: [PATCH 13/16] Fix typo pwd() was incorrectly named, should've been cd --- pyvim/commands/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyvim/commands/commands.py b/pyvim/commands/commands.py index d9e17fd..629cd75 100644 --- a/pyvim/commands/commands.py +++ b/pyvim/commands/commands.py @@ -411,7 +411,7 @@ def pwd(editor): @location_cmd('cd', accepts_force=False) -def pwd(editor, location): +def cd(editor, location): " Change working directory. " try: os.chdir(location) From 8f3d933382fac1fc84b534be86ec9b60fa49d0ac Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Sat, 1 Jun 2019 03:18:54 +1000 Subject: [PATCH 14/16] Add expanduser to cd() This allows changing directory with user directory like `:cd ~` --- pyvim/commands/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyvim/commands/commands.py b/pyvim/commands/commands.py index 629cd75..9b0b6fe 100644 --- a/pyvim/commands/commands.py +++ b/pyvim/commands/commands.py @@ -414,7 +414,7 @@ def pwd(editor): def cd(editor, location): " Change working directory. " try: - os.chdir(location) + os.chdir(os.path.expanduser(location)) except OSError as e: editor.show_message('{}'.format(e)) From 6ea3acf04678063034246b2028e9841f3440123d Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Sat, 1 Jun 2019 04:03:55 +1000 Subject: [PATCH 15/16] Implement :wa and :wqa to match vim --- pyvim/commands/commands.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pyvim/commands/commands.py b/pyvim/commands/commands.py index 9b0b6fe..cbedff0 100644 --- a/pyvim/commands/commands.py +++ b/pyvim/commands/commands.py @@ -344,17 +344,26 @@ def quit_nonzero(editor): editor.application.exit() +@cmd('wa') +def write_all(editor): + """ + Write all changed buffers + """ + for eb in editor.window_arrangement.editor_buffers: + if eb.location is None: + editor.show_message(_NO_FILE_NAME) + break + else: + eb.write() + + @cmd('wqa') def write_and_quit_all(editor): """ - Write current buffer and quit all. + Write all changed buffers and quit all. """ - eb = editor.window_arrangement.active_editor_buffer - if eb.location is None: - editor.show_message(_NO_FILE_NAME) - else: - eb.write() - quit_all(editor, force=False) + write_all(editor) + quit_all(editor) @cmd('h') From 268eb1c9753a1510ac9a0d5f41c0b6594a5ea4a4 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Sun, 1 Mar 2020 18:05:54 +1100 Subject: [PATCH 16/16] Implement ZQ (normal mode map for :q!) --- pyvim/key_bindings.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyvim/key_bindings.py b/pyvim/key_bindings.py index 7ca70a1..73788b8 100644 --- a/pyvim/key_bindings.py +++ b/pyvim/key_bindings.py @@ -4,7 +4,7 @@ from prompt_toolkit.filters import Condition, has_focus, vi_insert_mode, vi_navigation_mode from prompt_toolkit.key_binding import KeyBindings -from .commands.commands import write_and_quit +from .commands.commands import write_and_quit, quit import os @@ -48,6 +48,14 @@ def _(event): write_and_quit(editor, None) editor.sync_with_prompt_toolkit() + @kb.add('Z', 'Q', filter=in_navigation_mode) + def _(event): + """ + Quit and discard changes. + """ + quit(editor, force=True) + editor.sync_with_prompt_toolkit() + @kb.add('c-z', filter=in_navigation_mode) def _(event): """