diff --git a/__init__.py b/__init__.py index b52d6d6..367ce15 100644 --- a/__init__.py +++ b/__init__.py @@ -8,12 +8,16 @@ start_refact = False refact_session_manager = None +open_folders = {} class RefactAutocomplete(sublime_plugin.EventListener): def on_query_completions(self, view, prefix, locations): if start_refact: refact_session_manager.get_session(view).show_completions(prefix, locations) + def on_new_window(self, window): + sublime.set_timeout(lambda: restart_on_new_folder(window), 100) + def on_modified(self, view): if not start_refact: return @@ -21,7 +25,7 @@ def on_modified(self, view): session = refact_session_manager.get_session(view) session.notify_document_update() session.update_completion() - + def on_close(self, view): if not start_refact: return @@ -40,7 +44,7 @@ def on_query_context(self, view, key, operator, operand, match_all): if start_refact: if key == "refact.show_completion": return refact_session_manager.get_session(view).completion_visible() - + def on_post_text_command(self, view, command_name, args): if start_refact: session = refact_session_manager.get_session(view) @@ -72,23 +76,50 @@ def on_text_command(self, view, command_name, args): elif command_name == "drag_select": session.clear_completion() +def restart_server(): + global refact_session_manager + + if refact_session_manager: + s = sublime.load_settings("refact.sublime-settings") + pause_completion = s.get("pause_completion", False) + + if not pause_completion: + refact_session_manager.restart_server() + def plugin_loaded(): global refact_session_manager global start_refact + global open_folders + windows = sublime.windows() + open_folders = {folder for w in windows for folder in w.folders()} s = sublime.load_settings("refact.sublime-settings") pause_completion = s.get("pause_completion", False) + s.add_on_change("restart_server", restart_server) if pause_completion: sublime.status_message("⏸️ refact.ai") else: refact_start() +def restart_on_new_folder(window): + global open_folders + + window_folders = set(window.folders()) + new_folders = window_folders - open_folders + if len(new_folders) > 0: + open_folders |= new_folders + restart_server() + +def get_start_refact(): + print("get_start_refact", start_refact) + return start_refact + def refact_start(): global refact_session_manager global start_refact if refact_session_manager: refact_session_manager.start() else: - refact_session_manager = RefactSessionManager() + refact_session_manager = RefactSessionManager(get_start_refact) start_refact= True class RefactStartCommand(sublime_plugin.TextCommand): @@ -142,14 +173,14 @@ def run(self, edit): class RefactPause(sublime_plugin.TextCommand): def run(self, edit): global start_refact - start_refact= False s = sublime.load_settings("refact.sublime-settings") pause_status = s.get("pause_completion", False) pause_status = not pause_status + start_refact = not pause_status s.set("pause_completion", pause_status) sublime.save_settings("refact.sublime-settings") - if not pause_status: + if not pause_status and refact_session_manager is None: refact_start() else: if refact_session_manager: @@ -158,4 +189,3 @@ def run(self, edit): class RefactClearCompletion(sublime_plugin.TextCommand): def run(self, edit): refact_session_manager.get_session(self.view).clear_completion() - diff --git a/refact.sublime-settings b/refact.sublime-settings index 1bd8669..9a6063e 100644 --- a/refact.sublime-settings +++ b/refact.sublime-settings @@ -9,7 +9,7 @@ "code_completion_model": "", "code_completion_scratchpad": "", "pause_completion": false, - "telemetry_code_snippets": false + "telemetry_basic": true // Use something like // tail -f -n 1000 ~/.cache/refact/logs/rustbinary.2024-02-07 // to see what's going on with the server diff --git a/src/completion_text.py b/src/completion_text.py index 5297b12..f1409fc 100644 --- a/src/completion_text.py +++ b/src/completion_text.py @@ -45,12 +45,13 @@ def collect_space(s, index): def get_completion_text(point, text, line, end = None): if not line or line.isspace(): - s = replace_tab(text) + tab_size = get_tab_size() + + s = replace_tab(text, tab_size) res_space = get_nonwhitespace(s) - l = replace_tab(line) - diff = res_space - len(l) - if diff > 0: - return s[(res_space - diff):] + line_len = len(replace_tab(line, tab_size)) + if res_space > line_len: + return s[line_len:] else: return s[res_space:] diff --git a/src/phantom_state.py b/src/phantom_state.py index a8754c6..7b6a90a 100644 --- a/src/phantom_state.py +++ b/src/phantom_state.py @@ -4,7 +4,7 @@ import sublime_plugin import html from .utils import * -from .completion_text import get_completion_text +from .completion_text import get_completion_text, get_nonwhitespace from dataclasses import dataclass from typing import NamedTuple @@ -107,7 +107,7 @@ def __init__(self, view): self.update_step = False self.phantoms_visible = False s = sublime.load_settings("Preferences.sublime-settings") - self.tab_size = s.get("tab_size", 4) + self.tab_size = get_tab_size() invisibleDiv = """ @@ -176,10 +176,17 @@ def show_start_line_completion(self, position, text): return previous_line = get_previous_line(self.view, position) + previous_line_text = self.view.substr(previous_line) + popup_point = previous_line.a - if len(previous_line) > 0: + if previous_line_text[0] == '\t': + popup_point = previous_line.a + text = " " + text + elif len(previous_line) > 0: popup_point = popup_point + 1 - self.view.show_popup(self.create_annotation_template(text), location = popup_point, flags = sublime.PopupFlags.HIDE_ON_CHARACTER_EVENT, max_width = 999) + + popup_text = self.create_annotation_template(text) + self.view.show_popup(popup_text, location = popup_point, flags = sublime.PopupFlags.HIDE_ON_CHARACTER_EVENT, max_width = 999) def add_phantoms(self, new_phantoms): view = self.view diff --git a/src/refact_lsp.py b/src/refact_lsp.py index 90ba084..9686df8 100644 --- a/src/refact_lsp.py +++ b/src/refact_lsp.py @@ -1,6 +1,8 @@ +import sublime import os import socket import pathlib +import traceback from typing import Optional, Dict, Tuple from .pylspclient.lsp_structs import * from .pylspclient.lsp_endpoint import LspEndpoint @@ -12,7 +14,7 @@ def __init__(self, process, statusbar): self.statusbar = statusbar self.connect(process) - def load_document(self, file_name: str, text: str, version: int = 1, languageId = LANGUAGE_IDENTIFIER.PYTHON): + def load_document(self, file_name: str, version: int, text: str, languageId = LANGUAGE_IDENTIFIER.PYTHON): print("load_document", file_name) if languageId is None: @@ -104,26 +106,27 @@ def get_completions(self, file_name, pos: Tuple[int, int], multiline: bool = Fal return res except Exception as err: self.statusbar.handle_err(err) + return + def shutdown(self): try: self.lsp_client.shutdown() except Exception as err: - self.statusbar.handle_err(err) print("lsp error shutdown") - - def logMessage(self, args): - print("logMessage", args) def connect(self, process): capabilities = {} json_rpc_endpoint = JsonRpcEndpoint(process.stdin, process.stdout) self.lsp_endpoint = LspEndpoint(json_rpc_endpoint, notify_callbacks = {"window/logMessage":print}) self.lsp_client = LspClient(self.lsp_endpoint) - + windows = sublime.windows() + workspaces = [{'name': folder, 'uri': pathlib.Path(folder).as_uri()} for w in windows for folder in w.folders()] + + print("workspaces: ", workspaces) try: - self.lsp_client.initialize(process.pid, None, None, None, capabilities, "off", None) + self.lsp_client.initialize(process.pid, None, None, None, capabilities, "off", workspaces) except Exception as err: self.statusbar.handle_err(err) print("lsp initialize error", err) diff --git a/src/refact_process.py b/src/refact_process.py index 90d9be4..3bef189 100644 --- a/src/refact_process.py +++ b/src/refact_process.py @@ -10,11 +10,12 @@ class RefactProcessWrapper(): def __init__(self): self.connection = None self.active = False + self.process = None self.statusbar = StatusBar() def get_server_path(self): return os.path.join(sublime.packages_path(), "refact", "server", "refact-lsp") - + def get_server_commands(self): s = sublime.load_settings("refact.sublime-settings") @@ -41,11 +42,16 @@ def get_server_commands(self): def start_server(self): self.active = True server_cmds = self.get_server_commands() - startupinfo = None - if os.name == 'nt': + + if not self.process is None: + self.process.kill() + + if hasattr(subprocess, 'STARTUPINFO'): startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - self.process = subprocess.Popen(server_cmds, startupinfo=startupinfo, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr=subprocess.PIPE, shell=False) + self.process = subprocess.Popen(server_cmds, startupinfo=startupinfo, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr=subprocess.PIPE, shell=False) + else: + self.process = subprocess.Popen(server_cmds, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr=subprocess.PIPE, shell=False) self.statusbar.update_statusbar("ok") if not self.connection is None: @@ -54,7 +60,9 @@ def start_server(self): self.connection = LSP(self.process, self.statusbar) def stop_server(self): - self.connection.shutdown() + if not self.connection is None: + self.connection.shutdown() self.process.terminate() + self.active = False self.statusbar.update_statusbar("pause") - + diff --git a/src/refact_sessions.py b/src/refact_sessions.py index 88c36e8..d3a1438 100644 --- a/src/refact_sessions.py +++ b/src/refact_sessions.py @@ -11,17 +11,26 @@ from .phantom_state import PhantomState, PhantomInsertion from .completion_text import get_nonwhitespace +dummyConnection = {"load_document":identity, "did_change" : identity, "did_close" : identity, "did_save" : identity, "get_completions":noop} + class RefactSessionManager: - def __init__(self): + def __init__(self, is_active): + self.is_active = is_active self.connection = None self.process = RefactProcessWrapper() self.connection = self.process.start_server() self.views = {} + def restart_server(self): + self.shutdown() + self.start() + def start(self): self.connection = self.process.start_server() - + for key, session in self.views.items(): + session.notify_document_update() + def shutdown(self): if self.process and self.process.active: self.process.stop_server() @@ -35,7 +44,9 @@ def get_view_id(self, view): return "UI" def get_connection(self): - if not self.process.active: + if not self.is_active(): + return dummyConnection + if not self.process.active: self.process.start_server() return self.process.connection @@ -47,7 +58,7 @@ def get_session(self, view): class RefactSession: def __init__(self, view, connection, is_ui = False): - self.completion_in_process = False + self.update_step = False self.session_state = 0 self.version = 0 self.view = view @@ -65,11 +76,13 @@ def __init__(self, view, connection, is_ui = False): syntax = view.scope_name(get_cursor_point(view)) file_type = syntax[(syntax.rindex(".") + 1):].strip() self.languageId = get_language_id(file_type) - self.connection().load_document(self.file_name, get_text(self.view), self.languageId) + self.connection().load_document(self.file_name, version = self.version, text = get_text(self.view), languageId = self.languageId) def notify_document_update(self): if self.is_ui or self.phantom_state.update_step: return + self.session_state = self.session_state + 1 + self.version = self.version + 1 self.connection().did_change(self.file_name, self.version, get_text(self.view), self.languageId) def notify_close(self): @@ -91,12 +104,13 @@ def update_completion(self): self.clear_completion() return - if self.phantom_state.update_step: + if self.update_step or self.phantom_state.update_step: return if self.has_completion(): self.phantom_state.update() - if not self.completion_visible() and not self.completion_in_process: + + if not self.completion_visible(): text = get_cursor_line(self.view) self.show_completions(text, [get_cursor_point(self.view)], len(text) == 0 or text.isspace()) @@ -110,25 +124,22 @@ def get_completion(self): return self.current_completion def clear_completion(self): + self.update_step = True self.session_state = self.session_state + 1 - self.current_completion = None self.phantom_state.clear_phantoms() + self.current_completion = None + self.update_step = False def is_paused(self): s = sublime.load_settings("refact.sublime-settings") return s.get("pause_completion") - def clear_completion_process(self): - self.completion_in_process = False - def show_completions(self, prefix, locations, multiline = False): - if not self.phantom_state.update_step and not self.completion_in_process: - self.completion_in_process = True + if not self.phantom_state.update_step: sublime.set_timeout_async(lambda:self.show_completions_inner(self.session_state, prefix, locations, multiline)) def set_phantoms(self, version, location, completion): if self.session_state != version or not self.is_position_valid(location): - self.clear_completion_process() return text = get_line(self.view, location) @@ -142,8 +153,8 @@ def set_phantoms(self, version, location, completion): self.phantom_state.set_new_phantoms([[PhantomInsertion(location, insertions[0]), PhantomInsertion(len(remainder), next_text)]]) else: self.current_completion = completion + self.phantom_state.set_new_phantoms([[PhantomInsertion(location, completion)]]) - self.clear_completion_process() def is_position_valid(self, location): rc = self.view.rowcol(location) @@ -156,21 +167,18 @@ def is_position_valid(self, location): return True def show_completions_inner(self, version, prefix, locations, multiline = False): + if version != self.session_state: - self.clear_completion_process() return if self.is_ui or self.phantom_state.update_step: - self.clear_completion_process() return if self.is_paused(): - self.clear_completion_process() return location = locations[0] if not self.is_position_valid(location): - self.clear_completion_process() return rc = self.view.rowcol(location) @@ -179,10 +187,10 @@ def show_completions_inner(self, version, prefix, locations, multiline = False): pos_arg = (rc[0], 0) else: pos_arg = rc + res = self.connection().get_completions(self.file_name, pos_arg, multiline) if res is None: - self.clear_completion_process() return completions = res["choices"] @@ -190,11 +198,11 @@ def show_completions_inner(self, version, prefix, locations, multiline = False): if len(completions) > 0: completion = completions[0]['code_completion'] if not completion or len(completion) == 0 or completion.isspace(): - self.clear_completion_process() - return + return text = get_line(self.view, location) - suggestions = [text[:rc[1]] + s['code_completion'] for s in completions] + suggestion_prefix = "" if not prefix or prefix.isspace() else text[:rc[1]] + suggestions = [suggestion_prefix + s['code_completion'] for s in completions] sublime.set_timeout(lambda: self.set_phantoms(version, location, suggestions[0])) def accept_completion(self): diff --git a/src/statusbar.py b/src/statusbar.py index e911d87..24c959b 100644 --- a/src/statusbar.py +++ b/src/statusbar.py @@ -1,4 +1,3 @@ - import sublime class StatusBar: @@ -34,6 +33,9 @@ def update_statusbar(self, status, msg = ""): self.status_loop() def handle_err(self, err): + if self.status == "pause": + return + if not isinstance(err, str): if hasattr(err, 'message'): err = err.message diff --git a/src/utils.py b/src/utils.py index 3d10aeb..b7bcd1c 100644 --- a/src/utils.py +++ b/src/utils.py @@ -37,9 +37,16 @@ def filter_none(l): def identity(x): return x -def replace_tab(text): +def noop(): + return None + +def get_tab_size(): s = sublime.load_settings("Preferences.sublime-settings") - tab_size = s.get("tab_size", 4) + return s.get("tab_size", 4) + +def replace_tab(text, tab_size = None): + if tab_size is None: + tab_size = get_tab_size() return text.replace('\t', ' ' * tab_size)