the lsp kinda works but is very buggy and crashes often
This commit is contained in:
89
completions_menu.py
Normal file
89
completions_menu.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
from textual.screen import ModalScreen
|
||||||
|
from textual.containers import Container
|
||||||
|
|
||||||
|
from context_menu import ButtonStatic
|
||||||
|
from textual.widgets import TextArea, OptionList
|
||||||
|
from textual.widgets.option_list import Option
|
||||||
|
from textual.widget import Widget
|
||||||
|
|
||||||
|
from textual.geometry import Region, Spacing, Offset
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionsMenu(Widget):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
CompletionsMenu {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
overlay: screen;
|
||||||
|
position: absolute;
|
||||||
|
background: $surface;
|
||||||
|
min-width: 30;
|
||||||
|
|
||||||
|
OptionList {
|
||||||
|
padding: 0 1;
|
||||||
|
background: $surface;
|
||||||
|
border: $panel tall;
|
||||||
|
width: 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, options: list[dict], text_area: TextArea):
|
||||||
|
super().__init__()
|
||||||
|
self.options = options
|
||||||
|
self.text_area = text_area
|
||||||
|
self.options_list = None
|
||||||
|
|
||||||
|
def compose(self):
|
||||||
|
self.options_list = OptionList(*[self.render_option(option) for option in self.options])
|
||||||
|
yield self.options_list
|
||||||
|
|
||||||
|
def update(self, new_choices: list[dict]):
|
||||||
|
self.options = new_choices
|
||||||
|
self.options_list.set_options([self.render_option(option) for option in self.options])
|
||||||
|
|
||||||
|
def hide(self):
|
||||||
|
self.display = "none"
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
self.display = "block"
|
||||||
|
|
||||||
|
def render_option(self, option_data: dict):
|
||||||
|
icon = ""
|
||||||
|
|
||||||
|
|
||||||
|
match option_data["type"]:
|
||||||
|
case 1: # text
|
||||||
|
icon = ""
|
||||||
|
case 2 | 3: # method / function
|
||||||
|
icon = "[blueviolet][/]"
|
||||||
|
case 14: # keyword
|
||||||
|
icon = ""
|
||||||
|
case 7: # class
|
||||||
|
icon = "[orange][/]"
|
||||||
|
case 9: # module
|
||||||
|
icon = "[cornflowerblue][/]"
|
||||||
|
|
||||||
|
case _:
|
||||||
|
icon = str(option_data["type"])
|
||||||
|
|
||||||
|
return icon + " " + option_data["text"]
|
||||||
|
|
||||||
|
def align_to_cursor(self):
|
||||||
|
x, y = self.text_area.cursor_screen_offset
|
||||||
|
dropdown = self.options_list
|
||||||
|
width, height = dropdown.outer_size
|
||||||
|
|
||||||
|
# Constrain the dropdown within the screen.
|
||||||
|
x, y, _width, _height = Region(x - 1, y + 1, width, height).constrain(
|
||||||
|
"inside",
|
||||||
|
"none",
|
||||||
|
Spacing.all(0),
|
||||||
|
self.screen.scrollable_content_region,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.absolute_offset = Offset(x, y)
|
||||||
|
|
||||||
|
def on_mount(self):
|
||||||
|
#self.options_list.styles.height = len(self.options_list.children) + 2
|
||||||
|
self.align_to_cursor()
|
||||||
226
lsp_client.py
Normal file
226
lsp_client.py
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import lsprotocol
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class NoFileOpen(Exception):
|
||||||
|
def __init__(self, *args):
|
||||||
|
super().__init__(self, *args)
|
||||||
|
|
||||||
|
class LSPClient:
|
||||||
|
def __init__(self, proc_name: str, proc_flags: list[str], langauge_id: str):
|
||||||
|
self.proc = None
|
||||||
|
self.proc_flags = proc_flags
|
||||||
|
self.proc_name = proc_name
|
||||||
|
self.request_id = 0
|
||||||
|
|
||||||
|
self.langauge_id = langauge_id
|
||||||
|
self.file_path = None
|
||||||
|
self.folder = None
|
||||||
|
|
||||||
|
self.has_file_open = False
|
||||||
|
|
||||||
|
self.pending = {}
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
async def message_loop(self):
|
||||||
|
while True:
|
||||||
|
msg = await self.read_message()
|
||||||
|
|
||||||
|
if "id" in msg:
|
||||||
|
future = self.pending.pop(msg["id"], None)
|
||||||
|
|
||||||
|
if future:
|
||||||
|
future.set_result(msg)
|
||||||
|
elif "method" in msg:
|
||||||
|
print("notification:", msg)
|
||||||
|
|
||||||
|
async def open_folder(self, folder_path: str):
|
||||||
|
self.folder = Path(folder_path).resolve().as_uri()
|
||||||
|
|
||||||
|
async def open_file(self, file_path: str):
|
||||||
|
|
||||||
|
|
||||||
|
with open(file_path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
resolved_path = Path(file_path).resolve()
|
||||||
|
self.file_path = resolved_path.as_uri()
|
||||||
|
await self.open_folder(resolved_path.parent)
|
||||||
|
|
||||||
|
await self.send({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "textDocument/didOpen",
|
||||||
|
"params": {
|
||||||
|
"textDocument": {
|
||||||
|
"uri": self.file_path,
|
||||||
|
"languageId": self.langauge_id,
|
||||||
|
"version": 1,
|
||||||
|
"text": content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.has_file_open = True
|
||||||
|
|
||||||
|
async def start(self, starting_file_path: str):
|
||||||
|
if self.running:
|
||||||
|
raise Exception("LSP is already running!")
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
resolved_path = Path(starting_file_path).resolve()
|
||||||
|
|
||||||
|
self.file_path = resolved_path.as_uri()
|
||||||
|
self.folder = resolved_path.parent.as_uri()
|
||||||
|
|
||||||
|
# create a subprocess which we talk to over stdio
|
||||||
|
self.proc = await asyncio.create_subprocess_exec(
|
||||||
|
self.proc_name,
|
||||||
|
*self.proc_flags,
|
||||||
|
stdin=asyncio.subprocess.PIPE,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.create_task(self.message_loop())
|
||||||
|
|
||||||
|
await self.initialize()
|
||||||
|
await self.open_file(starting_file_path)
|
||||||
|
|
||||||
|
async def send(self, data: dict):
|
||||||
|
body = json.dumps(data).encode("utf-8")
|
||||||
|
|
||||||
|
header = (
|
||||||
|
f"Content-Length: {len(body)}\r\n\r\n"
|
||||||
|
).encode("ascii")
|
||||||
|
|
||||||
|
print("writing")
|
||||||
|
self.proc.stdin.write(header + body)
|
||||||
|
|
||||||
|
print("draining")
|
||||||
|
await self.proc.stdin.drain()
|
||||||
|
print("done draining")
|
||||||
|
|
||||||
|
async def read_message(self):
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line = await self.proc.stdout.readline()
|
||||||
|
|
||||||
|
if line == b"\r\n": break
|
||||||
|
|
||||||
|
decoded = line.decode("ascii").strip()
|
||||||
|
|
||||||
|
key, value = decoded.split(":", 1)
|
||||||
|
headers[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
content_length = int(headers["Content-Length"])
|
||||||
|
|
||||||
|
body = await self.proc.stdout.readexactly(content_length)
|
||||||
|
|
||||||
|
return json.loads(body.decode("utf-8"))
|
||||||
|
|
||||||
|
async def apply_change(self, text: str):
|
||||||
|
await self.send({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "textDocument/didChange",
|
||||||
|
"params": {
|
||||||
|
"textDocument": {
|
||||||
|
"uri": self.file_path,
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
"contentChanges": [
|
||||||
|
{
|
||||||
|
"text": text
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async def request(self, method, params):
|
||||||
|
self.request_id += 1
|
||||||
|
reg_id = self.request_id
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
future = loop.create_future()
|
||||||
|
|
||||||
|
print(self.request_id)
|
||||||
|
|
||||||
|
self.pending[reg_id] = future
|
||||||
|
print(self.pending)
|
||||||
|
|
||||||
|
await self.send({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": self.request_id,
|
||||||
|
"method": method,
|
||||||
|
"params": params
|
||||||
|
})
|
||||||
|
|
||||||
|
return await future
|
||||||
|
|
||||||
|
async def get_completions(self, cursor_pos: tuple[int, int]):
|
||||||
|
if not self.has_file_open:
|
||||||
|
raise NoFileOpen("Can't get completions when you haven't openned a file yet.")
|
||||||
|
|
||||||
|
return await self.request(
|
||||||
|
"textDocument/completion",
|
||||||
|
{
|
||||||
|
"textDocument": {
|
||||||
|
"uri": self.file_path,
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"line": cursor_pos[0],
|
||||||
|
"character": cursor_pos[1]
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"triggerKind": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
print(f"rootUri: {self.folder}")
|
||||||
|
|
||||||
|
response = await self.request(
|
||||||
|
"initialize",
|
||||||
|
{
|
||||||
|
"processId": None,
|
||||||
|
"rootUri": self.folder,
|
||||||
|
"capabilities": {},
|
||||||
|
"initializationOptions": {
|
||||||
|
"clangdFileStatus": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
print(response)
|
||||||
|
|
||||||
|
await self.send({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "initialized",
|
||||||
|
"params": {}
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_from_file_path(file_path: str, ):
|
||||||
|
file_path = Path(file_path)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
async def main():
|
||||||
|
client = LSPClient("jedi-language-server", [], "python")
|
||||||
|
await client.start("test.py")
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
print(await client.get_completions((0, 9)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print(f"took {round(time.time() - start, 1)} seconds")
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
89
main.py
89
main.py
@@ -15,10 +15,13 @@ from plugin_loader import PluginLoader
|
|||||||
from settings import SettingsScreen
|
from settings import SettingsScreen
|
||||||
from settings_store import ConfigHandler
|
from settings_store import ConfigHandler
|
||||||
from directory_tree_custom import CustomDirectoryTree
|
from directory_tree_custom import CustomDirectoryTree
|
||||||
|
from completions_menu import CompletionsMenu
|
||||||
|
|
||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
from watchdog.events import FileSystemEventHandler
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
|
||||||
|
from lsp_client import LSPClient
|
||||||
|
|
||||||
import os, sys
|
import os, sys
|
||||||
|
|
||||||
|
|
||||||
@@ -62,6 +65,13 @@ class Berry(App):
|
|||||||
self.current_editor = None
|
self.current_editor = None
|
||||||
self.file_tabs = {}
|
self.file_tabs = {}
|
||||||
|
|
||||||
|
self.completions_menu = None
|
||||||
|
self.current_lsp = None
|
||||||
|
self.running_lsps = {
|
||||||
|
"py": LSPClient("jedi-language-server", [], "python")
|
||||||
|
}
|
||||||
|
self.last_change_pos = None
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Header()
|
yield Header()
|
||||||
with Vertical(id="sidebar"):
|
with Vertical(id="sidebar"):
|
||||||
@@ -244,11 +254,25 @@ class Berry(App):
|
|||||||
else:
|
else:
|
||||||
tabs.active = self.file_tabs[str(event.path)].id
|
tabs.active = self.file_tabs[str(event.path)].id
|
||||||
|
|
||||||
def open_text_editor(self, file_content: bytes, file_extension: str):
|
async def open_text_editor(self, file_content: bytes, file_extension: str):
|
||||||
code_editor = TextArea.code_editor(placeholder="This file is empty.", theme="css", disabled=True, soft_wrap=bool(int(self.config_handler.get("editor", "word_wrap"))), show_line_numbers=bool(int(self.config_handler.get("editor", "line_numbers"))))
|
code_editor = TextArea.code_editor(placeholder="This file is empty.", classes="code-editor", theme="css", disabled=True, soft_wrap=bool(int(self.config_handler.get("editor", "word_wrap"))), show_line_numbers=bool(int(self.config_handler.get("editor", "line_numbers"))))
|
||||||
|
|
||||||
self.current_editor.mount(code_editor)
|
await self.current_editor.mount(code_editor)
|
||||||
|
|
||||||
|
# start lsp
|
||||||
|
if file_extension in self.running_lsps:
|
||||||
|
lsp = self.running_lsps[file_extension]
|
||||||
|
|
||||||
|
self.current_lsp = file_extension
|
||||||
|
|
||||||
|
if not lsp.running:
|
||||||
|
await lsp.start(self.open_file)
|
||||||
|
else:
|
||||||
|
await lsp.open_file(self.open_file)
|
||||||
|
else:
|
||||||
|
self.current_lsp = None
|
||||||
|
|
||||||
|
# setup text editor
|
||||||
code_editor.language = theme_mappings.get(file_extension, None)
|
code_editor.language = theme_mappings.get(file_extension, None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -261,17 +285,17 @@ class Berry(App):
|
|||||||
|
|
||||||
code_editor.focus()
|
code_editor.focus()
|
||||||
|
|
||||||
def open_editor(self, file_extension: str, file_content: bytes):
|
async def open_editor(self, file_extension: str, file_content: bytes):
|
||||||
default_editors: dict = eval(self.config_handler.get("editor", "default_editors"))
|
default_editors: dict = eval(self.config_handler.get("editor", "default_editors"))
|
||||||
editor_for_file = default_editors.get(file_extension, None)
|
editor_for_file = default_editors.get(file_extension, None)
|
||||||
|
|
||||||
self.current_editor.remove_children()
|
self.current_editor.remove_children()
|
||||||
|
|
||||||
if not editor_for_file:
|
if not editor_for_file:
|
||||||
self.open_text_editor(file_content, file_extension)
|
await self.open_text_editor(file_content, file_extension)
|
||||||
|
|
||||||
@on(Tabs.TabActivated)
|
@on(Tabs.TabActivated)
|
||||||
def on_tab_shown(self, event: Tabs.TabActivated):
|
async def on_tab_shown(self, event: Tabs.TabActivated):
|
||||||
|
|
||||||
if self.file_clicked:
|
if self.file_clicked:
|
||||||
self.file_clicked = False
|
self.file_clicked = False
|
||||||
@@ -307,7 +331,7 @@ class Berry(App):
|
|||||||
# close the file
|
# close the file
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
self.open_editor(file_extension, content)
|
await self.open_editor(file_extension, content)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -316,7 +340,10 @@ class Berry(App):
|
|||||||
def window_minimized(self, event: Window.Minimized):
|
def window_minimized(self, event: Window.Minimized):
|
||||||
event.window.remove_window()
|
event.window.remove_window()
|
||||||
|
|
||||||
def on_text_area_changed(self, event: TextArea.Changed):
|
async def on_text_area_changed(self, event: TextArea.Changed):
|
||||||
|
if not event.text_area.has_class("code-editor"):
|
||||||
|
return
|
||||||
|
|
||||||
if self.switching:
|
if self.switching:
|
||||||
self.switching = False
|
self.switching = False
|
||||||
return
|
return
|
||||||
@@ -329,7 +356,7 @@ class Berry(App):
|
|||||||
with open(self.open_file, "r", encoding="utf-8") as f:
|
with open(self.open_file, "r", encoding="utf-8") as f:
|
||||||
if f.read() == event.text_area.text: # TODO: figure out why im guetting what seems like a race conidition which is making this if statement needed
|
if f.read() == event.text_area.text: # TODO: figure out why im guetting what seems like a race conidition which is making this if statement needed
|
||||||
return
|
return
|
||||||
self.unsaved_files[self.open_file] = {"current": event.text_area.text, "original": f.read()}
|
self.unsaved_files[self.open_file] = {"current": event.text_area.text.encode(), "original": f.read()}
|
||||||
|
|
||||||
|
|
||||||
tabs.active_tab.tooltip = f"Unsaved changes in {tabs.active_tab.label}"
|
tabs.active_tab.tooltip = f"Unsaved changes in {tabs.active_tab.label}"
|
||||||
@@ -342,6 +369,48 @@ class Berry(App):
|
|||||||
tabs.active_tab.tooltip = str(tabs.active_tab.label)
|
tabs.active_tab.tooltip = str(tabs.active_tab.label)
|
||||||
self.unsaved_files.pop(self.open_file)
|
self.unsaved_files.pop(self.open_file)
|
||||||
|
|
||||||
|
await self.run_completions()
|
||||||
|
|
||||||
|
async def run_completions(self):
|
||||||
|
code_editor: TextArea = self.current_editor.query_one(TextArea)
|
||||||
|
if len(code_editor.text.strip()) == 0: return
|
||||||
|
|
||||||
|
lsp: LSPClient = self.running_lsps[self.current_lsp]
|
||||||
|
|
||||||
|
line, col = code_editor.cursor_location
|
||||||
|
|
||||||
|
# ensure we're on the latest version of the file when we apply completions or else we will error
|
||||||
|
if self.last_change_pos:
|
||||||
|
await lsp.apply_change(code_editor.text)
|
||||||
|
self.last_change_pos = (line, col)
|
||||||
|
|
||||||
|
self.log("waiting for completions")
|
||||||
|
completions = await lsp.get_completions((line,col-1))
|
||||||
|
self.log("god completions")
|
||||||
|
|
||||||
|
self.notify(str(completions.get("error", None)))
|
||||||
|
|
||||||
|
if not completions["result"]:
|
||||||
|
if self.completions_menu:
|
||||||
|
self.completions_menu.hide()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if self.completions_menu:
|
||||||
|
self.completions_menu.show()
|
||||||
|
|
||||||
|
|
||||||
|
completion_choices = [{"text": result["label"], "type": result["kind"]} for result in completions["result"]["items"]]
|
||||||
|
|
||||||
|
|
||||||
|
if not self.completions_menu:
|
||||||
|
|
||||||
|
self.completions_menu = CompletionsMenu(completion_choices, code_editor)
|
||||||
|
await self.current_editor.mount(self.completions_menu)
|
||||||
|
else:
|
||||||
|
self.completions_menu.align_to_cursor()
|
||||||
|
self.completions_menu.update(completion_choices)
|
||||||
|
|
||||||
|
|
||||||
def action_new(self):
|
def action_new(self):
|
||||||
tabs: Tabs = self.query_one("#file-tabs")
|
tabs: Tabs = self.query_one("#file-tabs")
|
||||||
new_tab = Tab("New File")
|
new_tab = Tab("New File")
|
||||||
@@ -390,7 +459,7 @@ class Berry(App):
|
|||||||
self.observer.join()
|
self.observer.join()
|
||||||
return super().action_quit()
|
return super().action_quit()
|
||||||
|
|
||||||
def on_ready(self):
|
async def on_ready(self):
|
||||||
self.open_file = None
|
self.open_file = None
|
||||||
self.unsaved_files = {} # list of paths
|
self.unsaved_files = {} # list of paths
|
||||||
self.switching = False
|
self.switching = False
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
local plugin = {}
|
local plugin = {}
|
||||||
|
|
||||||
function plugin.run()
|
function plugin.run()
|
||||||
berry.ui.notify("hi lmao")
|
--berry.ui.notify("hi lmao")
|
||||||
end
|
end
|
||||||
|
|
||||||
return plugin
|
return plugin
|
||||||
Reference in New Issue
Block a user