Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Vim mode #452

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"tasks": {
"build": "poetry install && biscuit"
}
}
3 changes: 3 additions & 0 deletions config/settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ cursor_style="line"
word_wrap=false
exclude_dirs=[".git"]
exclude_types=["__pycache__"]

[vim]
enabled=false
3 changes: 3 additions & 0 deletions src/biscuit/api/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ def __init__(self, *a) -> None:

self.register_command = self.settings.register_command

# Register the toggle Vim mode command in the command palette
self.register_command("toggle_vim_mode", self.base.commands.toggle_vim_mode)

@property
def commands(self) -> None:
"""Return all registered commands"""
Expand Down
3 changes: 3 additions & 0 deletions src/biscuit/binder.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def bind_all(self) -> None:
self.bind(self.bindings.change_tab_back, self.events.change_tab_back)
self.bind(self.bindings.split_tab, self.events.split_editor)

# Vim mode bindings
self.bind("<Key>", self.events.vim_handle_key)

def late_bind_all(self) -> None:
"""Bindings that require full initialization"""

Expand Down
142 changes: 142 additions & 0 deletions src/biscuit/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ def __init__(self, base: App) -> None:
self.minimized = False
self.previous_pos = None

self.vim_mode = "NORMAL"
self.vim_visual_mode = False
self.vim_insert_mode = False

def new_file(self, *_) -> None:
self.base.open_editor(f"Untitled-{self.count}", exists=False)
self.count += 1
Expand Down Expand Up @@ -450,3 +454,141 @@ def view_biscuit_licenses(self, *_) -> None:
def show_about(self, *_) -> None:
messagebox.showinfo("Biscuit", str(self.base.system))
self.base.logger.info(str(self.base.system))

def toggle_vim_mode(self, *_) -> None:
self.base.settings.vim.enabled = not self.base.settings.vim.enabled
self.base.update_statusbar()

def vim_normal_mode(self, *_) -> None:
self.vim_mode = "NORMAL"
self.vim_visual_mode = False
self.vim_insert_mode = False
self.base.statusbar.update_vim_mode_indicator(self.vim_mode)

def vim_insert_mode(self, *_) -> None:
self.vim_mode = "INSERT"
self.vim_visual_mode = False
self.vim_insert_mode = True
self.base.statusbar.update_vim_mode_indicator(self.vim_mode)

def vim_visual_mode(self, *_) -> None:
self.vim_mode = "VISUAL"
self.vim_visual_mode = True
self.vim_insert_mode = False
self.base.statusbar.update_vim_mode_indicator(self.vim_mode)

def vim_command_mode(self, *_) -> None:
self.vim_mode = "COMMAND"
self.vim_visual_mode = False
self.vim_insert_mode = False
self.base.statusbar.update_vim_mode_indicator(self.vim_mode)

def vim_handle_key(self, event: tk.Event) -> str:
key = event.keysym

if self.vim_mode == "NORMAL":
if key in ["h", "j", "k", "l"]:
self.vim_handle_navigation(key)
elif key == "i":
self.vim_insert_mode()
elif key == "v":
self.vim_visual_mode()
elif key == ":":
self.vim_command_mode()
elif key == "u":
self.undo()
elif key == "r" and event.state & 0x4:
self.redo()
elif key == "x":
self.vim_delete_char()
elif key == "d":
self.vim_delete_line()
elif key == "y":
self.vim_yank_line()
elif key == "p":
self.vim_paste()
elif key == "w":
self.vim_delete_word()
elif key == "c":
self.vim_cut_word()
elif key == "g":
self.vim_goto_line()
elif key == "G":
self.vim_goto_end_of_file()
elif key == "esc":
self.vim_normal_mode()

elif self.vim_mode == "INSERT":
if key == "esc":
self.vim_normal_mode()

elif self.vim_mode == "VISUAL":
if key == "esc":
self.vim_normal_mode()

elif self.vim_mode == "COMMAND":
if key == "esc":
self.vim_normal_mode()

return "break"

def vim_handle_navigation(self, key: str) -> None:
if key == "h":
self.base.editorsmanager.active_editor.content.text.mark_set(
"insert", "insert-1c"
)
elif key == "j":
self.base.editorsmanager.active_editor.content.text.mark_set(
"insert", "insert+1l"
)
elif key == "k":
self.base.editorsmanager.active_editor.content.text.mark_set(
"insert", "insert-1l"
)
elif key == "l":
self.base.editorsmanager.active_editor.content.text.mark_set(
"insert", "insert+1c"
)

def vim_delete_char(self) -> None:
self.base.editorsmanager.active_editor.content.text.delete("insert")

def vim_delete_line(self) -> None:
self.base.editorsmanager.active_editor.content.text.delete(
"insert linestart", "insert lineend"
)

def vim_yank_line(self) -> None:
self.base.editorsmanager.active_editor.content.text.clipboard_clear()
self.base.editorsmanager.active_editor.content.text.clipboard_append(
self.base.editorsmanager.active_editor.content.text.get(
"insert linestart", "insert lineend"
)
)

def vim_paste(self) -> None:
self.base.editorsmanager.active_editor.content.text.insert(
"insert", self.base.editorsmanager.active_editor.content.text.clipboard_get()
)

def vim_delete_word(self) -> None:
self.base.editorsmanager.active_editor.content.text.delete(
"insert", "insert wordend"
)

def vim_cut_word(self) -> None:
self.base.editorsmanager.active_editor.content.text.clipboard_clear()
self.base.editorsmanager.active_editor.content.text.clipboard_append(
self.base.editorsmanager.active_editor.content.text.get(
"insert", "insert wordend"
)
)
self.base.editorsmanager.active_editor.content.text.delete(
"insert", "insert wordend"
)

def vim_goto_line(self) -> None:
self.base.editorsmanager.active_editor.content.text.mark_set("insert", "1.0")

def vim_goto_end_of_file(self) -> None:
self.base.editorsmanager.active_editor.content.text.mark_set("insert", "end")
8 changes: 8 additions & 0 deletions src/biscuit/editor/misc/welcome.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ def create_quick_group(self):
["Ctrl", "Shift", "X"],
).pack(fill=tk.X, expand=True)

QuickItem(
quick,
"Toggle Vim Mode",
Icons.KEYBOARD,
self.base.commands.toggle_vim_mode,
["Ctrl", "Shift", "V"],
).pack(fill=tk.X, expand=True)

def create_recent_group(self):
Label(
self.container,
Expand Down
74 changes: 74 additions & 0 deletions src/biscuit/editor/text/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -1497,3 +1497,77 @@ def _proxy(self, *args):
self.event_generate("<<Scroll>>", when="tail")

return result

def handle_vim_visual_mode(self, event: tk.Event) -> str:
key = event.keysym

if key in ["h", "j", "k", "l"]:
self.vim_handle_navigation(key)
elif key == "v":
self.vim_visual_mode = False
self.tag_remove(tk.SEL, "1.0", tk.END)
elif key == "V":
self.vim_visual_mode = False
self.tag_remove(tk.SEL, "1.0", tk.END)
elif key == "esc":
self.vim_visual_mode = False
self.tag_remove(tk.SEL, "1.0", tk.END)

return "break"

def handle_vim_visual_line_mode(self, event: tk.Event) -> str:
key = event.keysym

if key in ["h", "j", "k", "l"]:
self.vim_handle_navigation(key)
elif key == "v":
self.vim_visual_line_mode = False
self.tag_remove(tk.SEL, "1.0", tk.END)
elif key == "V":
self.vim_visual_line_mode = False
self.tag_remove(tk.SEL, "1.0", tk.END)
elif key == "esc":
self.vim_visual_line_mode = False
self.tag_remove(tk.SEL, "1.0", tk.END)

return "break"

def vim_handle_navigation(self, key: str) -> None:
if key == "h":
self.mark_set(tk.INSERT, "insert-1c")
elif key == "j":
self.mark_set(tk.INSERT, "insert+1l")
elif key == "k":
self.mark_set(tk.INSERT, "insert-1l")
elif key == "l":
self.mark_set(tk.INSERT, "insert+1c")

if self.vim_visual_mode:
self.tag_add(tk.SEL, "insert", "insert+1c")
elif self.vim_visual_line_mode:
self.tag_add(tk.SEL, "insert linestart", "insert lineend")

def vim_visual_mode(self, *_) -> None:
self.vim_visual_mode = True
self.vim_visual_line_mode = False
self.tag_add(tk.SEL, "insert", "insert+1c")

def vim_visual_line_mode(self, *_) -> None:
self.vim_visual_mode = False
self.vim_visual_line_mode = True
self.tag_add(tk.SEL, "insert linestart", "insert lineend")

def vim_handle_key(self, event: tk.Event) -> str:
key = event.keysym

if self.vim_visual_mode:
return self.handle_vim_visual_mode(event)
elif self.vim_visual_line_mode:
return self.handle_vim_visual_line_mode(event)

if key == "v":
self.vim_visual_mode()
elif key == "V":
self.vim_visual_line_mode()

return "break"
18 changes: 18 additions & 0 deletions src/biscuit/layout/statusbar/statusbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,16 @@ def __init__(self, master: Frame, *args, **kwargs) -> None:

self.panel_toggle.show()

# Add Vim mode indicators
self.vim_mode_indicator = self.add_button(
text="NORMAL",
callback=None,
description="Vim mode indicator",
side=tk.LEFT,
padx=(2, 0),
)
self.vim_mode_indicator.show()

def add_button(
self,
text="",
Expand Down Expand Up @@ -342,3 +352,11 @@ def change_language(self, language: str) -> typing.Callable:
return lambda _: self.base.editorsmanager.active_editor.content.text.highlighter.change_language(
language
)

def update_vim_mode_indicator(self, mode: str) -> None:
"""Updates the Vim mode indicator on the status bar.

Args:
mode (str): The current Vim mode.
"""
self.vim_mode_indicator.change_text(mode)
Loading