From 6dc831b4425f36da4d52c1bdf5232ab799db55aa Mon Sep 17 00:00:00 2001 From: Billy Date: Wed, 7 Feb 2024 03:13:34 +0530 Subject: [PATCH 1/2] feat: LSP Rename symbol --- .../components/editors/texteditor/text.py | 28 +++++++- biscuit/core/components/floating/__init__.py | 1 + .../components/floating/palette/searchbar.py | 2 +- biscuit/core/components/floating/rename.py | 68 +++++++++++++++++++ biscuit/core/components/lsp/__init__.py | 11 ++- biscuit/core/components/lsp/client.py | 14 ++++ biscuit/core/components/lsp/data.py | 23 +++++++ biscuit/core/components/lsp/handler.py | 21 ++++++ biscuit/core/components/lsp/utils.py | 8 +-- biscuit/core/events.py | 17 +++++ biscuit/core/gui.py | 1 + 11 files changed, 183 insertions(+), 11 deletions(-) create mode 100644 biscuit/core/components/floating/rename.py diff --git a/biscuit/core/components/editors/texteditor/text.py b/biscuit/core/components/editors/texteditor/text.py index 4f0b4820..87fc65e2 100644 --- a/biscuit/core/components/editors/texteditor/text.py +++ b/biscuit/core/components/editors/texteditor/text.py @@ -1,6 +1,5 @@ from __future__ import annotations -import codecs import os import queue import re @@ -10,9 +9,10 @@ from collections import deque import chardet +import tarts as lsp if typing.TYPE_CHECKING: - from biscuit.core.components.lsp.data import HoverResponse, Jump, Underlines, Completions + from biscuit.core.components.lsp.data import WorkspaceEdits, HoverResponse, Jump, Underlines, Completions from . import TextEditor from ...utils import Text as BaseText @@ -114,6 +114,7 @@ def config_bindings(self): # lspc self.bind("", self.event_mapped) + self.bind("", self.request_rename) self.bind("", self.event_unmapped) self.bind("", self.event_destroy) self.bind("", self.request_hover) @@ -367,6 +368,9 @@ def request_autocomplete(self, _): return self.base.language_server_manager.request_completions(self) self.hide_autocomplete() + + def request_rename(self, *_): + self.base.rename.show(self) def lsp_show_autocomplete(self, response: Completions) -> None: self.autocomplete.lsp_update_completions(self, response.completions) @@ -394,6 +398,14 @@ def lsp_hover(self, response: HoverResponse) -> None: self.hover.show(self, response) except Exception as e: print(e) + + def lsp_rename(self, changes: WorkspaceEdits): + for i in changes.edits: + try: + self.base.open_workspace_edit(i.file_path, i.edits) + except: + # this indeed is a darn task to do so not surprised + pass def get_cursor_pos(self): return self.index(tk.INSERT) @@ -401,8 +413,18 @@ def get_cursor_pos(self): def get_mouse_pos(self): return self.index(tk.CURRENT) - def get_current_word(self): + def get_current_word(self)-> str: + """Returns current word cut till the cursor""" + return self.current_word.strip() + + def get_current_fullword(self) -> str | None: + """Returns current word uncut and fully""" + + index = self.index(tk.CURRENT) + start, end = index + " wordstart", index + " wordend" + word = self.get(start, end).strip() + return word if self.is_identifier(word) else None def move_to_next_word(self): self.mark_set(tk.INSERT, self.index("insert+1c wordend")) diff --git a/biscuit/core/components/floating/__init__.py b/biscuit/core/components/floating/__init__.py index 9a8dcce8..66b06863 100644 --- a/biscuit/core/components/floating/__init__.py +++ b/biscuit/core/components/floating/__init__.py @@ -6,3 +6,4 @@ from .notifications import Notifications from .palette import ActionSet, Palette from .pathview import PathView +from .rename import Rename diff --git a/biscuit/core/components/floating/palette/searchbar.py b/biscuit/core/components/floating/palette/searchbar.py index b124c06f..c0e6703b 100644 --- a/biscuit/core/components/floating/palette/searchbar.py +++ b/biscuit/core/components/floating/palette/searchbar.py @@ -15,7 +15,7 @@ def __init__(self, master: Palette, *args, **kwargs) -> None: self.config(bg=self.base.theme.biscuit) self.text_variable = tk.StringVar() - self.text_variable.trace("w", self.filter) + self.text_variable.trace_add("write", self.filter) frame = Frame(self, **self.base.theme.palette) frame.pack(fill=tk.BOTH, padx=1, pady=1) diff --git a/biscuit/core/components/floating/rename.py b/biscuit/core/components/floating/rename.py new file mode 100644 index 00000000..b58d8933 --- /dev/null +++ b/biscuit/core/components/floating/rename.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import tkinter as tk +import typing + +from ..utils import Frame, Toplevel + +if typing.TYPE_CHECKING: + from biscuit.core import App + from biscuit.core.components.editors.texteditor.text import Text + + +class Rename(Toplevel): + def __init__(self, master: App, *args, **kwargs) -> None: + super().__init__(master, *args, **kwargs) + self.config(pady=1, padx=1, bg=self.base.theme.border) + + self.tab = None + self.active = False + self.withdraw() + self.overrideredirect(True) + + frame = Frame(self, **self.base.theme.palette) + frame.pack(fill=tk.BOTH, padx=2, pady=2) + + self.text_variable = tk.StringVar() + self.entry = tk.Entry( + frame, font=self.base.settings.font, relief=tk.FLAT, highlightcolor=self.base.theme.biscuit, + textvariable=self.text_variable, **self.base.theme.palette.searchbar) + self.entry.grid(sticky=tk.EW, padx=5, pady=3) + + self.entry.bind("", self.enter) + self.bind("", self.hide()) + self.bind("", self.hide) + + def clear(self) -> None: + self.text_variable.set("") + + def focus(self) -> None: + self.entry.focus() + + def get(self): + return self.text_variable.get() + + def enter(self, *_): + if self.tab: + self.base.language_server_manager.request_rename(self.tab, self.get()) + + self.hide() + + def show(self, tab: Text): + self.tab = tab + self.focus() + + self.text_variable.set(tab.get_current_fullword()) + self.entry.selection_range(0, tk.END) + + self.refresh_geometry(tab) + self.deiconify() + self.active = True + + def hide(self, *_): + self.withdraw() + self.active = False + + def refresh_geometry(self, tab: Text): + self.update_idletasks() + self.geometry("+{}+{}".format(*tab.cursor_screen_location())) diff --git a/biscuit/core/components/lsp/__init__.py b/biscuit/core/components/lsp/__init__.py index 6433142d..e015d4bb 100644 --- a/biscuit/core/components/lsp/__init__.py +++ b/biscuit/core/components/lsp/__init__.py @@ -39,17 +39,22 @@ def tab_closed(self, tab: Text) -> None: if inst := self.request_client_instance(tab): inst.close_tab(tab) - def request_completions(self, tab: Text) -> str | None: + def request_completions(self, tab: Text) -> None: for instance in list(self.existing.values()): if tab in instance.tabs_opened: instance.request_completions(tab) - def request_goto_definition(self, tab: Text) -> str: + def request_goto_definition(self, tab: Text) -> None: for instance in list(self.existing.values()): if tab in instance.tabs_opened: instance.request_go_to_definition(tab) + + def request_rename(self, tab: Text, new_name: str) -> None: + for instance in list(self.existing.values()): + if tab in instance.tabs_opened: + instance.request_rename(tab, new_name) - def request_hover(self, tab: Text) -> str | None: + def request_hover(self, tab: Text) -> None: for instance in list(self.existing.values()): if tab in instance.tabs_opened: instance.request_hover(tab) diff --git a/biscuit/core/components/lsp/client.py b/biscuit/core/components/lsp/client.py index e9ea4dc6..422ecc10 100644 --- a/biscuit/core/components/lsp/client.py +++ b/biscuit/core/components/lsp/client.py @@ -48,6 +48,7 @@ def __init__(self, master: LanguageServerManager, tab: Text, root_dir: str) -> N self._autocomplete_req: dict[int, tuple[Text, CompletionRequest]] = {} self._hover_requests: dict[int, tuple[Text, str]] = {} self._gotodef_requests: dict[int, tuple[Text, str]] = {} + self._rename_requests: dict[int, Text] = {} self._outline_requests: dict[int, Text] = {} def run_loop(self) -> None: @@ -145,6 +146,19 @@ def request_go_to_definition(self, tab: Text) -> None: ) self._gotodef_requests[request_id] = (tab, tab.get_mouse_pos()) + def request_rename(self, tab: Text, new_name: str) -> None: + if tab.path is None or self.client.state != lsp.ClientState.NORMAL: + return + + request_id = self.client.rename( + lsp.TextDocumentPosition( + textDocument=lsp.TextDocumentIdentifier(uri=Path(tab.path).as_uri()), + position=encode_position(tab.get_mouse_pos()), + ), + new_name=new_name, + ) + self._rename_requests[request_id] = tab + def request_outline(self, tab: Text) -> None: if tab.path is None or self.client.state != lsp.ClientState.NORMAL: return diff --git a/biscuit/core/components/lsp/data.py b/biscuit/core/components/lsp/data.py index d4248cde..ad264928 100644 --- a/biscuit/core/components/lsp/data.py +++ b/biscuit/core/components/lsp/data.py @@ -75,3 +75,26 @@ def __repr__(self) -> str: class Jump: pos: str locations: List[JumpLocationRange] + + +@dataclasses.dataclass +class TextEdit: + start: str + end: str + new_text: str + + def __repr__(self) -> str: + return self.file_path + +@dataclasses.dataclass +class WorkspaceEdit: + file_path: str + edits: list[TextEdit] + + def __repr__(self) -> str: + return self.file_path + +@dataclasses.dataclass +class WorkspaceEdits: + edits: List[WorkspaceEdit] + diff --git a/biscuit/core/components/lsp/handler.py b/biscuit/core/components/lsp/handler.py index 33d9c2a2..83fa0f75 100644 --- a/biscuit/core/components/lsp/handler.py +++ b/biscuit/core/components/lsp/handler.py @@ -121,6 +121,27 @@ def process(self, e: lsp.Event) -> None: ), ) return + + if isinstance(e, lsp.WorkspaceEdit): + tab = self.master._rename_requests.pop(e.message_id) + if not e.documentChanges: + return + + tab.lsp_rename( + WorkspaceEdits([ + WorkspaceEdit( + file_path=decode_path_uri(i.textDocument.uri), + edits=[ + TextEdit( + start=decode_position(j.range.start), + end=decode_position(j.range.end), + new_text=j.newText + ) + for j in i.edits + ] + ) + for i in e.documentChanges + ])) if isinstance(e, lsp.Hover): requesting_tab, location = self.master._hover_requests.pop(e.message_id) diff --git a/biscuit/core/components/lsp/utils.py b/biscuit/core/components/lsp/utils.py index edd24f14..84b35143 100644 --- a/biscuit/core/components/lsp/utils.py +++ b/biscuit/core/components/lsp/utils.py @@ -39,14 +39,14 @@ def get_completion_item_doc(item: lsp.CompletionItem) -> str: return item.label + "\n" + item.documentation.value return item.label + "\n" + item.documentation -def decode_path_uri(file_url: str) -> Path: +def decode_path_uri(file_url: str) -> str: if sys.platform == "win32": if file_url.startswith("file:///"): - return Path(url2pathname(file_url[8:])) + return url2pathname(file_url[8:]) else: - return Path(url2pathname(file_url[5:])) + return url2pathname(file_url[5:]) else: - return Path(url2pathname(file_url[7:])) + return url2pathname(file_url[7:]) def jump_paths_and_ranges(locations: list[lsp.Location] | lsp.Location) -> Iterator[tuple[Path, lsp.Range]]: diff --git a/biscuit/core/events.py b/biscuit/core/events.py index 0c39eb7a..2129dd28 100644 --- a/biscuit/core/events.py +++ b/biscuit/core/events.py @@ -8,6 +8,8 @@ if typing.TYPE_CHECKING: from .layout import * + from .components.lsp.data import TextEdit + from .components.editors.texteditor import * from .components import * from .config import ConfigManager @@ -121,6 +123,21 @@ def goto_location(self, path: str, position: int) -> None: editor = self.open_editor(path, exists=True) editor.bind("<>", lambda e: editor.content.goto(position)) + def open_workspace_edit(self, path: str, edits: list[TextEdit]): + if self.editorsmanager.is_open(path): + e = self.editorsmanager.set_active_editor_by_path(path).content.text + self.do_workspace_edits(e, edits) + return + + editor = self.open_editor(path, exists=True) + editor.bind("<>", lambda _, editor=editor.content.text,edits=edits:threading.Thread(target=self.do_workspace_edits, args=(editor, edits), daemon=True).start()) + + def do_workspace_edits(self, tab: Text, edits: list[TextEdit]): + for i in edits: + tab.replace(i.start, i.end, i.new_text) + tab.update() + tab.update_idletasks() + def open_editor(self, path: str, exists: bool = True) -> Editor | BaseEditor: if exists and not os.path.isfile(path): return diff --git a/biscuit/core/gui.py b/biscuit/core/gui.py index 10808ccc..d0fdd2e9 100644 --- a/biscuit/core/gui.py +++ b/biscuit/core/gui.py @@ -93,6 +93,7 @@ def setup_floating_widgets(self) -> None: self.autocomplete = AutoComplete(self) self.definitions = Definitions(self) + self.rename = Rename(self) self.pathview = PathView(self) self.hover = Hover(self) From 2d097a59c375c4c406ffd787b7266538503756e6 Mon Sep 17 00:00:00 2001 From: Billy Date: Sat, 10 Feb 2024 02:39:22 +0530 Subject: [PATCH 2/2] fix: Rename window positioning and default text --- .../core/components/editors/texteditor/text.py | 18 ++++++++++++++++-- .../core/components/floating/hover/__init__.py | 13 ++++++------- biscuit/core/components/floating/rename.py | 8 ++++++-- biscuit/core/components/lsp/client.py | 2 +- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/biscuit/core/components/editors/texteditor/text.py b/biscuit/core/components/editors/texteditor/text.py index 87fc65e2..e9602860 100644 --- a/biscuit/core/components/editors/texteditor/text.py +++ b/biscuit/core/components/editors/texteditor/text.py @@ -224,6 +224,17 @@ def cursor_screen_location(self): bbx_x, bbx_y, _, bbx_h = bbox return (pos_x + bbx_x - 1, pos_y + bbx_y + bbx_h) + def cursor_wordstart_screen_location(self): + pos_x, pos_y = self.winfo_rootx(), self.winfo_rooty() + + cursor = tk.INSERT + " wordstart" + bbox = self.bbox(cursor) + if not bbox: + return (0, 0) + + bbx_x, bbx_y, _, bbx_h = bbox + return (pos_x + bbx_x - 1, pos_y + bbx_y + bbx_h) + def enter_key_events(self, *_): if not self.minimalist and self.autocomplete.active: self.autocomplete.choose(self) @@ -331,7 +342,10 @@ def request_hover(self, _): index = self.index(tk.CURRENT) start, end = index + " wordstart", index + " wordend" - word = self.get(start, end).strip() + word = self.get(start, end) + if word.startswith("\n"): + start = index + " linestart" + word = word.strip() self.clear_goto_marks() @@ -421,7 +435,7 @@ def get_current_word(self)-> str: def get_current_fullword(self) -> str | None: """Returns current word uncut and fully""" - index = self.index(tk.CURRENT) + index = self.index(tk.INSERT) start, end = index + " wordstart", index + " wordend" word = self.get(start, end).strip() return word if self.is_identifier(word) else None diff --git a/biscuit/core/components/floating/hover/__init__.py b/biscuit/core/components/floating/hover/__init__.py index 952b68f6..7060f7f9 100644 --- a/biscuit/core/components/floating/hover/__init__.py +++ b/biscuit/core/components/floating/hover/__init__.py @@ -65,17 +65,16 @@ def show(self, tab: Text, response: HoverResponse) -> None: else: self.renderer.pack_forget() - self.update_idletasks() + self.update() self.deiconify() - self.update_idletasks() - self.update_position(response.location, tab) - - def hide(self, *_) -> None: try: - self.withdraw() - except Exception: + self.update_position(response.location, tab) + except: pass + def hide(self, *_) -> None: + self.withdraw() + def hide_if_not_hovered(self) -> None: if not self.hovered: self.hide() diff --git a/biscuit/core/components/floating/rename.py b/biscuit/core/components/floating/rename.py index b58d8933..742e1ec6 100644 --- a/biscuit/core/components/floating/rename.py +++ b/biscuit/core/components/floating/rename.py @@ -52,7 +52,11 @@ def show(self, tab: Text): self.tab = tab self.focus() - self.text_variable.set(tab.get_current_fullword()) + current = tab.get_current_fullword() + if current is None: + return + + self.text_variable.set() self.entry.selection_range(0, tk.END) self.refresh_geometry(tab) @@ -65,4 +69,4 @@ def hide(self, *_): def refresh_geometry(self, tab: Text): self.update_idletasks() - self.geometry("+{}+{}".format(*tab.cursor_screen_location())) + self.geometry("+{}+{}".format(*tab.cursor_wordstart_screen_location())) diff --git a/biscuit/core/components/lsp/client.py b/biscuit/core/components/lsp/client.py index 422ecc10..76d3dfdf 100644 --- a/biscuit/core/components/lsp/client.py +++ b/biscuit/core/components/lsp/client.py @@ -153,7 +153,7 @@ def request_rename(self, tab: Text, new_name: str) -> None: request_id = self.client.rename( lsp.TextDocumentPosition( textDocument=lsp.TextDocumentIdentifier(uri=Path(tab.path).as_uri()), - position=encode_position(tab.get_mouse_pos()), + position=encode_position(tab.get_cursor_pos()), ), new_name=new_name, )