diff --git a/plugin.py b/plugin.py index 7778c21..47cd1cd 100644 --- a/plugin.py +++ b/plugin.py @@ -2,33 +2,47 @@ import sublime import sublime_plugin +import logging import random from Codemp.src.task_manager import tm from Codemp.src.client import client, VirtualClient -from Codemp.src.logger import logger -from Codemp.src.utils import status_log +from Codemp.src.logger import inner_logger from Codemp.src.utils import safe_listener_detach from Codemp.src.utils import safe_listener_attach from Codemp.src import globals as g +LOG_LEVEL = logging.DEBUG +handler = logging.StreamHandler() +handler.setFormatter( + logging.Formatter( + fmt="<{thread}/{threadName}>[codemp] [{name} :: {funcName}] {levelname}: {message}", + style="{", + ) +) +package_logger = logging.getLogger(__package__) +package_logger.addHandler(handler) +package_logger.setLevel(LOG_LEVEL) +package_logger.propagate = False + +logger = logging.getLogger(__name__) + TEXT_LISTENER = None + # Initialisation and Deinitialisation ############################################################################## - - def plugin_loaded(): global TEXT_LISTENER # instantiate and start a global asyncio event loop. # pass in the exit_handler coroutine that will be called upon relasing the event loop. tm.acquire(disconnect_client) - tm.dispatch(logger.log(), "codemp-logger") + tm.dispatch(inner_logger.listen(), "codemp-logger") TEXT_LISTENER = CodempClientTextChangeListener() - status_log("plugin loaded") + logger.debug("plugin loaded") async def disconnect_client(): @@ -47,7 +61,8 @@ async def disconnect_client(): def plugin_unloaded(): # releasing the runtime, runs the disconnect callback defined when acquiring the event loop. - status_log("unloading") + logger.debug("unloading") + package_logger.removeHandler(handler) tm.release(False) @@ -71,8 +86,8 @@ class EventListener(sublime_plugin.EventListener): for wsid in s[g.CODEMP_WINDOW_WORKSPACES]: ws = client[wsid] if ws is None: - status_log( - "[WARN] a tag on the window was found but not a matching workspace." + logger.warning( + "a tag on the window was found but not a matching workspace." ) continue @@ -137,7 +152,7 @@ class CodempClientTextChangeListener(sublime_plugin.TextChangeListener): def on_text_changed(self, changes): s = self.buffer.primary_view().settings() if s.get(g.CODEMP_IGNORE_NEXT_TEXT_CHANGE, None): - status_log("Ignoring echoing back the change.") + logger.debug("Ignoring echoing back the change.") s[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = False return diff --git a/src/buffers.py b/src/buffers.py index 17e874b..9825eb3 100644 --- a/src/buffers.py +++ b/src/buffers.py @@ -1,14 +1,13 @@ import sublime import os +import logging from asyncio import CancelledError - from codemp import BufferController -from Codemp.src.workspace import VirtualWorkspace from Codemp.src import globals as g - from Codemp.src.task_manager import tm -from Codemp.src.utils import status_log + +logger = logging.getLogger(__name__) # This class is used as an abstraction between the local buffers (sublime side) and the @@ -18,18 +17,19 @@ from Codemp.src.utils import status_log class VirtualBuffer: def __init__( self, - workspace: VirtualWorkspace, + workspace_id: str, + workspace_rootdir: str, remote_id: str, buffctl: BufferController, ): self.view = sublime.active_window().new_file() self.codemp_id = remote_id self.sublime_id = self.view.buffer_id() - - self.workspace = workspace + self.workspace_id = workspace_id + self.workspace_rootdir = workspace_rootdir self.buffctl = buffctl - self.tmpfile = os.path.join(workspace.rootdir, self.codemp_id) + self.tmpfile = os.path.join(workspace_rootdir, self.codemp_id) self.view.set_name(self.codemp_id) open(self.tmpfile, "a").close() @@ -46,7 +46,7 @@ class VirtualBuffer: self.view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]") s[g.CODEMP_BUFFER_TAG] = True s[g.CODEMP_REMOTE_ID] = self.codemp_id - s[g.CODEMP_WORKSPACE_ID] = self.workspace.id + s[g.CODEMP_WORKSPACE_ID] = self.workspace_id def cleanup(self): os.remove(self.tmpfile) @@ -58,15 +58,15 @@ class VirtualBuffer: self.view.erase_status(g.SUBLIME_STATUS_ID) tm.stop(f"{g.BUFFCTL_TASK_PREFIX}-{self.codemp_id}") - status_log(f"cleaning up virtual buffer '{self.codemp_id}'") + logger.info(f"cleaning up virtual buffer '{self.codemp_id}'") async def apply_bufferchange_task(self): - status_log(f"spinning up '{self.codemp_id}' buffer worker...") + logger.debug(f"spinning up '{self.codemp_id}' buffer worker...") try: while text_change := await self.buffctl.recv(): change_id = self.view.change_id() if text_change.is_empty(): - status_log("change is empty. skipping.") + logger.debug("change is empty. skipping.") continue # In case a change arrives to a background buffer, just apply it. # We are not listening on it. Otherwise, interrupt the listening @@ -88,10 +88,10 @@ class VirtualBuffer: ) except CancelledError: - status_log(f"'{self.codemp_id}' buffer worker stopped...") + logger.debug(f"'{self.codemp_id}' buffer worker stopped...") raise except Exception as e: - status_log(f"buffer worker '{self.codemp_id}' crashed:\n{e}") + logger.error(f"buffer worker '{self.codemp_id}' crashed:\n{e}") raise def send_buffer_change(self, changes): @@ -99,14 +99,14 @@ class VirtualBuffer: # sequential indexing, assuming the changes are applied in the order they are received. for change in changes: region = sublime.Region(change.a.pt, change.b.pt) - status_log( + logger.debug( "sending txt change: Reg({} {}) -> '{}'".format( region.begin(), region.end(), change.str ) ) self.buffctl.send(region.begin(), region.end(), change.str) - def send_cursor(self, vws: VirtualWorkspace): + def send_cursor(self, vws): # pyright: ignore # noqa: F821 # TODO: only the last placed cursor/selection. # status_log(f"sending cursor position in workspace: {vbuff.workspace.id}") region = self.view.sel()[0] diff --git a/src/client.py b/src/client.py index 10fbc51..4528ff7 100644 --- a/src/client.py +++ b/src/client.py @@ -1,11 +1,13 @@ from __future__ import annotations import sublime +import logging from codemp import Client from Codemp.src import globals as g from Codemp.src.workspace import VirtualWorkspace -from Codemp.src.utils import status_log + +logger = logging.getLogger(__name__) class VirtualClient: @@ -18,47 +20,44 @@ class VirtualClient: return self.workspaces.get(key) def connect(self, host: str, user: str, password: str): - status_log(f"Connecting to {host} with user {user}") + logger.info(f"Connecting to {host} with user {user}") try: self.handle = Client(host, user, password) except Exception as e: + logger.error(f"Could not connect: {e}") sublime.error_message( - f"Could not connect:\n Make sure the server is up.\n\ - or your credentials are correct\n\nerror: {e}" + "Could not connect:\n Make sure the server is up.\n\ + or your credentials are correct." ) return id = self.handle.user_id() - status_log(f"Connected to '{host}' with user {user} and id: {id}") + logger.debug(f"Connected to '{host}' with user {user} and id: {id}") async def join_workspace( self, workspace_id: str, ) -> VirtualWorkspace | None: if self.handle is None: - status_log("Connect to a server first!", True) return - status_log(f"Joining workspace: '{workspace_id}'") + logger.info(f"Joining workspace: '{workspace_id}'") try: workspace = await self.handle.join_workspace(workspace_id) except Exception as e: - status_log( - f"Could not join workspace '{workspace_id}'.\n\nerror: {e}", True - ) + logger.error(f"Could not join workspace '{workspace_id}'.\n\nerror: {e}") + sublime.error_message(f"Could not join workspace '{workspace_id}'") return vws = VirtualWorkspace(workspace) self.workspaces[workspace_id] = vws - # self.make_active(vws) return vws def leave_workspace(self, id: str): if self.handle is None: - status_log("Connect to a server first!", True) return False - status_log(f"Leaving workspace: '{id}'") + logger.info(f"Leaving workspace: '{id}'") if self.handle.leave_workspace(id): self.workspaces[id].cleanup() del self.workspaces[id] @@ -70,9 +69,7 @@ class VirtualClient: ws = self.workspaces.get(tag_id) if ws is None: - status_log( - "[WARN] a tag on the view was found but not a matching workspace." - ) + logging.warning("a tag on the view was found but not a matching workspace.") return return ws diff --git a/src/logger.py b/src/logger.py index ebe7686..45a05e5 100644 --- a/src/logger.py +++ b/src/logger.py @@ -1,35 +1,44 @@ +import logging from asyncio import CancelledError - from codemp import PyLogger -from Codemp.src.utils import status_log class CodempLogger: - def __init__(self, debug: bool = False): - self.handle = None + def __init__(self, log_level): + self.logger = logging.getLogger(__name__) + self.level = log_level + self.logger.setLevel(self.level) + self.internal_logger = None self.started = False + try: - self.handle = PyLogger(debug) + # PyLogger spins a tracing_subscriber rust side with a + # .try_init() and errors out if already initialized. + # initialize only once + self.internal_logger = PyLogger(self.level == logging.DEBUG) except Exception: pass - async def log(self): + async def listen(self): if self.started: return - self.started = True - status_log("spinning up the logger...") + self.logger.debug("spinning up internal logger listener...") + + assert self.internal_logger is not None try: - while msg := await self.handle.listen(): - print(msg) + while msg := await self.internal_logger.listen(): + self.logger.log(self.level, msg) except CancelledError: - status_log("stopping logger") + self.logger.debug("inner logger stopped.") self.started = False raise except Exception as e: - status_log(f"logger crashed unexpectedly:\n{e}") - raise + self.logger.error(f"inner logger crashed unexpectedly: \n {e}") + raise e + + def log(self, msg): + self.logger.log(self.level, msg) -DEBUG = False -logger = CodempLogger(DEBUG) +inner_logger = CodempLogger(logging.INFO) diff --git a/src/workspace.py b/src/workspace.py index 8c47553..13a62a6 100644 --- a/src/workspace.py +++ b/src/workspace.py @@ -3,6 +3,7 @@ from __future__ import annotations import sublime import shutil import tempfile +import logging from asyncio import CancelledError from codemp import Workspace @@ -10,9 +11,10 @@ from codemp import Workspace from Codemp.src import globals as g from Codemp.src.buffers import VirtualBuffer from Codemp.src.task_manager import tm -from Codemp.src.utils import status_log from Codemp.src.utils import rowcol_to_region +logger = logging.getLogger(__name__) + # A virtual workspace is a bridge class that aims to translate # events that happen to the codemp workspaces into sublime actions @@ -51,7 +53,7 @@ class VirtualWorkspace: ) d["folders"] = newf self.sublime_window.set_project_data(d) - status_log(f"cleaning up virtual workspace '{self.id}'") + logger.info(f"cleaning up virtual workspace '{self.id}'") shutil.rmtree(self.rootdir, ignore_errors=True) self.curctl.stop() @@ -70,7 +72,7 @@ class VirtualWorkspace: # initialise the virtual filesystem tmpdir = tempfile.mkdtemp(prefix="codemp_") - status_log("setting up virtual fs for workspace in: {} ".format(tmpdir)) + logging.debug("setting up virtual fs for workspace in: {} ".format(tmpdir)) self.rootdir = tmpdir # and add a new "project folder" @@ -119,8 +121,8 @@ class VirtualWorkspace: vbuff = self.active_Buffer.get(local_id) if vbuff is None: - status_log( - "[WARN] a local-remote buffer id pair was found but \ + logging.warning( + "a local-remote buffer id pair was found but \ not the matching virtual buffer." ) return @@ -153,7 +155,7 @@ class VirtualWorkspace: try: await self.handle.create(id) except Exception as e: - status_log(f"could not create buffer:\n\n {e}", True) + logging.error(f"could not create buffer:\n\n {e}", True) return else: return @@ -162,10 +164,10 @@ class VirtualWorkspace: try: buff_ctl = await self.handle.attach(id) except Exception as e: - status_log(f"error when attaching to buffer '{id}':\n\n {e}", True) + logging.error(f"error when attaching to buffer '{id}':\n\n {e}", True) return - vbuff = Buffer.VirtualBuffer(self, id, buff_ctl) + vbuff = VirtualBuffer(self.id, self.rootdir, id, buff_ctl) self.add_buffer(id, vbuff) # TODO! if the view is already active calling focus_view() will not trigger the on_activate @@ -177,7 +179,7 @@ class VirtualWorkspace: attached_Buffer = self.handle.buffer_by_name(id) if attached_Buffer is None: - status_log(f"You are not attached to the buffer '{id}'", True) + logging.warning(f"You are not attached to the buffer '{id}'", True) return self.handle.detach(id) @@ -190,7 +192,7 @@ class VirtualWorkspace: await self.handle.fetch_buffers() existing_Buffer = self.handle.filetree() if id not in existing_Buffer: - status_log(f"The buffer '{id}' does not exists.", True) + logging.info(f"The buffer '{id}' does not exists.", True) return # delete a buffer that exists but you are not attached to attached_Buffer = self.handle.buffer_by_name(id) @@ -204,7 +206,9 @@ class VirtualWorkspace: try: await self.handle.delete(id) except Exception as e: - status_log(f"error when deleting the buffer '{id}':\n\n {e}", True) + logging.error( + f"error when deleting the buffer '{id}':\n\n {e}", True + ) return else: return @@ -221,11 +225,11 @@ class VirtualWorkspace: try: await self.handle.delete(id) except Exception as e: - status_log(f"error when deleting the buffer '{id}':\n\n {e}", True) + logging.error(f"error when deleting the buffer '{id}':\n\n {e}", True) return async def move_cursor_task(self): - status_log(f"spinning up cursor worker for workspace '{self.id}'...") + logger.debug(f"spinning up cursor worker for workspace '{self.id}'...") try: while cursor_event := await self.curctl.recv(): vbuff = self.get_by_remote(cursor_event.buffer) @@ -247,8 +251,8 @@ class VirtualWorkspace: ) except CancelledError: - status_log(f"cursor worker for '{self.id}' stopped...") + logger.debug(f"cursor worker for '{self.id}' stopped...") raise except Exception as e: - status_log(f"cursor worker '{self.id}' crashed:\n{e}") + logger.error(f"cursor worker '{self.id}' crashed:\n{e}") raise