Skip to content

Commit

Permalink
feat: Rename symbol (LSP) #229
Browse files Browse the repository at this point in the history
LSP Rename symbol
  • Loading branch information
tomlin7 authored Feb 9, 2024
2 parents ad95ff9 + 2d097a5 commit 70a11a5
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 19 deletions.
44 changes: 40 additions & 4 deletions biscuit/core/components/editors/texteditor/text.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import codecs
import os
import queue
import re
Expand All @@ -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
Expand Down Expand Up @@ -114,6 +114,7 @@ def config_bindings(self):

# lspc
self.bind("<Map>", self.event_mapped)
self.bind("<F2>", self.request_rename)
self.bind("<Unmap>", self.event_unmapped)
self.bind("<Destroy>", self.event_destroy)
self.bind("<Motion>", self.request_hover)
Expand Down Expand Up @@ -223,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)
Expand Down Expand Up @@ -330,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()

Expand Down Expand Up @@ -367,6 +382,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)
Expand Down Expand Up @@ -394,15 +412,33 @@ 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)

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.INSERT)
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"))
Expand Down
1 change: 1 addition & 0 deletions biscuit/core/components/floating/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
from .notifications import Notifications
from .palette import ActionSet, Palette
from .pathview import PathView
from .rename import Rename
13 changes: 6 additions & 7 deletions biscuit/core/components/floating/hover/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
2 changes: 1 addition & 1 deletion biscuit/core/components/floating/palette/searchbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
72 changes: 72 additions & 0 deletions biscuit/core/components/floating/rename.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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("<Return>", self.enter)
self.bind("<FocusOut>", self.hide())
self.bind("<Escape>", 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()

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)
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_wordstart_screen_location()))
11 changes: 8 additions & 3 deletions biscuit/core/components/lsp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions biscuit/core/components/lsp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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_cursor_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
Expand Down
23 changes: 23 additions & 0 deletions biscuit/core/components/lsp/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

21 changes: 21 additions & 0 deletions biscuit/core/components/lsp/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions biscuit/core/components/lsp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]:
Expand Down
17 changes: 17 additions & 0 deletions biscuit/core/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -121,6 +123,21 @@ def goto_location(self, path: str, position: int) -> None:
editor = self.open_editor(path, exists=True)
editor.bind("<<FileLoaded>>", 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("<<FileLoaded>>", 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
Expand Down
1 change: 1 addition & 0 deletions biscuit/core/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit 70a11a5

Please sign in to comment.