diff --git a/.gitignore b/.gitignore index 902d344..5928005 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,6 @@ build/ develop-eggs/ dist/ eggs/ -lib/ lib64/ parts/ sdist/ diff --git a/.no-sublime-package b/.no-sublime-package new file mode 100644 index 0000000..e69de29 diff --git a/Codemp.sublime-commands b/Codemp.sublime-commands index 218e76f..11461ae 100644 --- a/Codemp.sublime-commands +++ b/Codemp.sublime-commands @@ -32,7 +32,7 @@ "caption": "Codemp: Connect", "command": "codemp_connect", "args": { - // "server_host": "http://[::1]:50051" + "server_host": "http://codemp.dev:50053", } }, { diff --git a/bindings/codemp-0.7.0-cp38-cp38-macosx_11_0_arm64.whl.REMOVED.git-id b/bindings/codemp-0.7.0-cp38-cp38-macosx_11_0_arm64.whl.REMOVED.git-id deleted file mode 100644 index 0e6a02e..0000000 --- a/bindings/codemp-0.7.0-cp38-cp38-macosx_11_0_arm64.whl.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -f0cef32c2765ba5d1bcaca6e11544caacf717006 \ No newline at end of file diff --git a/bindings/codemp.cpython-38-darwin.so.REMOVED.git-id b/bindings/codemp.cpython-38-darwin.so.REMOVED.git-id deleted file mode 100644 index bcd524e..0000000 --- a/bindings/codemp.cpython-38-darwin.so.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -8ee9409baac472917d5a57fff2f6e584d90588a7 \ No newline at end of file diff --git a/lib/codemp-0.0.5.dist-info/METADATA b/lib/codemp-0.0.5.dist-info/METADATA new file mode 100644 index 0000000..81befda --- /dev/null +++ b/lib/codemp-0.0.5.dist-info/METADATA @@ -0,0 +1,105 @@ +Metadata-Version: 2.3 +Name: codemp +Version: 0.0.5 +Classifier: Programming Language :: Python +Summary: code multiplexer +Keywords: codemp,cooperative,rust,python +Home-Page: https://codemp.dev +Author: alemi , zaaarf , frelodev , cschen +Author-email: cschen , alemi , zaaarf , frelodev +Maintainer-email: cschen +License: GPL-3.0-only +Requires-Python: >=3.8 +Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM +Project-URL: repository, https://github.com/hexedtech/codemp.git + +[![codemp](https://codemp.dev/static/banner.png)](https://codemp.dev) + +[![Actions Status](https://github.com/hexedtech/codemp/actions/workflows/ci.yml/badge.svg)](https://github.com/hexedtech/codemp/actions) +[![docs.rs](https://img.shields.io/docsrs/codemp)](https://docs.rs/codemp/0.7.0-beta.2/codemp/) +[![Crates.io Version](https://img.shields.io/crates/v/codemp)](https://crates.io/crates/codemp) +[![NPM Version](https://img.shields.io/npm/v/codemp)](https://npmjs.org/package/codemp) +[![PyPI - Version](https://img.shields.io/pypi/v/codemp)](https://pypi.org/project/codemp) +[![Crates.io License](https://img.shields.io/crates/l/codemp)](https://github.com/hexedtech/codemp/blob/dev/LICENSE) +[![Gitter](https://img.shields.io/gitter/room/hexedtech/codemp)](https://gitter.im/hexedtech/codemp) + +> `codemp` is a **collaborative** text editing solution to work remotely. + +It seamlessly integrates in your editor providing remote cursors and instant text synchronization, +as well as a remote virtual workspace for you and your team. + +> `codemp` is build with state-of-the-art CRDT technology, guaranteeing eventual consistency. + +This means everyone in a workspace will always be working on the exact same file _eventually_: +even under unreliable networks or constrained resources, the underlying CRDT will always reach a +convergent state across all users. Even with this baseline, `codemp`'s protocol is optimized for speed +and low network footprint, meaning even slow connections can provide stable real-time editing. + +The full documentation is available on [docs.rs](https://docs.rs/codemp/0.7.0-beta.2/codemp/). + +# Usage +`codemp` is primarily used as a plugin in your editor of choice. + +## Installation +> [!IMPORTANT] +> The editor plugins are in active development. Expect frequent changes. + +`codemp` is available as a plugin for a growing number of text editors. Currently we support: + - [NeoVim](https://github.com/hexedtech/codemp-nvim) + - [VSCode](https://github.com/hexedtech/codemp-vscode) + - [Sublime Text](https://github.com/hexedtech/codemp-sublime) + + +## Registration +The `codemp` protocol is [openly available](https://github.com/hexedtech/codemp-proto/) and servers may be freely developed with it. + +A reference instance is provided by hexed.technology at [codemp.dev](https://codemp.dev). You may create an account for it [here](https://codemp.dev/register). +During the initial closed beta, registrations will require an invite code. Get in contact if interested. + +An open beta is going to follow with free access to a single workspace per user. +After such period, [codemp.dev](https://codemp.dev) will switch to a subscription-based model. + +# Development +This is the main client library for `codemp`. It provides a batteries-included fully-featured `Client`, managed by the library itself, and exposes a number of functions to interact with it. The host program can obtain a `Client` handle by connecting, and from that reference can retrieve every other necessary component. + +`codemp` is primarily a rlib and can be used as such, but is also available in other languages via FFI. + +Adding a dependency on `codemp` is **easy**: + +### From Rust +Just `cargo add codemp` and check the docs for some examples. + +### From supported languages +We provide first-class bindings for: + - [JavaScript](./dist/js/README.md): available from `npm` as [`codemp`](https://npmjs.org/package/codemp) + - [Python](./dist/lua/README.md): available from `PyPI` as [`codemp`](https://pypi.org/project/codemp) + - [Lua](./dist/lua/README.md): run `cargo build --features=lua` + - [Java](./dist/java/README.md): run `gradle build` in `dist/java/` (requires Gradle) + +As a design philosophy, our binding APIs attempt to perfectly mimic their Rust counterparts, so the main documentation can still be referenced as source of truth. +Refer to specific language documentation for specifics, differences and quirks. + +### From other languages +> [!IMPORTANT] +> The common C bindings are not available yet! + +Any other language with C FFI capabilities will be able to use `codemp` via its bare C bindings. +This may be more complex and may require wrapping the native calls underneath. + +# Get in Touch +We love to hear back from users! Be it to give feedback, propose new features or highlight bugs, don't hesitate to reach out! + +## Contacts +We have a public [Gitter](https://gitter.im) room available on [gitter.im/hexedtech/codemp](https://gitter.im/hexedtech/codemp). +It's possible to freely browse the room history, but to send new messages it will be necessary to sign in with your GitHub account. + +If you have a [Matrix](https://matrix.org) account, you can join the gitter room directly at [#hexedtech_codemp:gitter.im](https://matrix.to/#/#hexedtech_codemp:gitter.im) + +## Contributing +If you find bugs or would like to see new features implemented, be sure to open an issue on this repository. + +> [!WARNING] +> The CLA necessary for code contributions is not yet available! + +In case you wish to contribute code, that's great! We love external contributions, but we require you to sign our CLA first (available soon). + diff --git a/lib/codemp-0.0.5.dist-info/RECORD b/lib/codemp-0.0.5.dist-info/RECORD new file mode 100644 index 0000000..11b75bd --- /dev/null +++ b/lib/codemp-0.0.5.dist-info/RECORD @@ -0,0 +1,7 @@ +codemp-0.0.5.dist-info/METADATA,sha256=9Gy6K7EREfjx1ozQv3OQPBhPEqsDphl2uOUzYXO5HXY,5819 +codemp-0.0.5.dist-info/WHEEL,sha256=Z7z_TcN-SqAJ4k-DAgSbQsbDhIg--F0f-wRIhoPNirs,102 +codemp/__init__.py,sha256=mu2xQJfcdjBHPYAGe4y3B-O1zKM0O6jqyutzs-XfgsE,107 +codemp/__init__.pyi,sha256=u_Yx1O12UzHwKtz97pfkT0mYbECDzzPEi8ewSb6yb9g,4155 +codemp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +codemp/codemp.abi3.so,sha256=OrEWjqxkDYxY0wN9O7T3Osg8JMvRu3Fj6-vVQFqPgv4,4193664 +codemp-0.0.5.dist-info/RECORD,, diff --git a/lib/codemp-0.0.5.dist-info/WHEEL b/lib/codemp-0.0.5.dist-info/WHEEL new file mode 100644 index 0000000..90bf034 --- /dev/null +++ b/lib/codemp-0.0.5.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: maturin (1.7.0) +Root-Is-Purelib: false +Tag: cp38-abi3-macosx_11_0_arm64 diff --git a/lib/codemp/__init__.py b/lib/codemp/__init__.py new file mode 100644 index 0000000..6b96f01 --- /dev/null +++ b/lib/codemp/__init__.py @@ -0,0 +1,5 @@ +from .codemp import * + +__doc__ = codemp.__doc__ +if hasattr(codemp, "__all__"): + __all__ = codemp.__all__ \ No newline at end of file diff --git a/lib/codemp/__init__.pyi b/lib/codemp/__init__.pyi new file mode 100644 index 0000000..dc9a974 --- /dev/null +++ b/lib/codemp/__init__.pyi @@ -0,0 +1,124 @@ +from typing import Tuple, Optional, Callable + +class Driver: + """ + this is akin to a big red button with a white "STOP" on top of it. + it is used to stop the runtime. + """ + def stop(self) -> None: ... + + +def init() -> Driver: ... +def set_logger(logger_cb: Callable[[str], None], debug: bool) -> bool: ... +def connect(host: str, username: str, password: str) -> Promise[Client]: ... + +class Promise[T]: + """ + This is a class akin to a future, which wraps a join handle from a spawned + task on the rust side. you may call .pyawait() on this promise to block + until we have a result, or return immediately if we already have one. + This only goes one way rust -> python. + + It can either be used directly or you can wrap it inside a future python side. + """ + def wait(self) -> T: ... + def is_done(self) -> bool: ... + +class Client: + """ + Handle to the actual client that manages the session. It manages the connection + to a server and joining/creating new workspaces + """ + def join_workspace(self, workspace: str) -> Promise[Workspace]: ... + def create_workspace(self, workspace: str) -> Promise[None]: ... + def delete_workspace(self, workspace: str) -> Promise[None]: ... + def invite_to_workspace(self, workspace: str, username: str) -> Promise[None]: ... + def list_workspaces(self, owned: bool, invited: bool) -> Promise[list[str]]: ... + def leave_workspace(self, workspace: str) -> bool: ... + def get_workspace(self, id: str) -> Workspace: ... + def active_workspaces(self) -> list[str]: ... + def user_id(self) -> str: ... + def user_name(self) -> str: ... + def refresh(self) -> Promise[None]: ... + +class Workspace: + """ + Handle to a workspace inside codemp. It manages buffers. + A cursor is tied to the single workspace. + """ + def create(self, path: str) -> Promise[None]: ... + def attach(self, path: str) -> Promise[BufferController]: ... + def detach(self, path: str) -> bool: ... + def fetch_buffers(self) -> Promise[None]: ... + def fetch_users(self) -> Promise[None]: ... + def list_buffer_users(self, path: str) -> Promise[list[str]]: ... + def delete(self, path: str) -> Promise[None]: ... + def id(self) -> str: ... + def cursor(self) -> CursorController: ... + def buffer_by_name(self, path: str) -> Optional[BufferController]: ... + def buffer_list(self) -> list[str]: ... + def filetree(self, filter: Optional[str]) -> list[str]: ... + +class TextChange: + """ + Editor agnostic representation of a text change, it translate between internal + codemp text operations and editor operations + """ + start: int + end: int + content: str + + def is_delete(self) -> bool: ... + def is_insert(self) -> bool: ... + def is_empty(self) -> bool: ... + def apply(self, txt: str) -> str: ... + + +class BufferController: + """ + Handle to the controller for a specific buffer, which manages the back and forth + of operations to and from other peers. + """ + def path(self) -> str: ... + def content(self) -> Promise[str]: ... + def send(self, + start: int, + end: int, + txt: str) -> Promise[None]: ... + def try_recv(self) -> Promise[Optional[TextChange]]: ... + def recv(self) -> Promise[TextChange]: ... + def poll(self) -> Promise[None]: ... + def callback(self, + cb: Callable[[BufferController], None]) -> None: ... + def clear_callback(self) -> None: ... + def stop(self) -> bool: ... + + + +class Cursor: + """ + An Editor agnostic cursor position representation + """ + start: Tuple[int, int] + end: Tuple[int, int] + buffer: str + user: Optional[str] # can be an empty string + + +class CursorController: + """ + Handle to the controller for a workspace, which manages the back and forth of + cursor movements to and from other peers + """ + def send(self, + path: str, + start: Tuple[int, int], + end: Tuple[int, int]) -> Promise[None]: ... + def try_recv(self) -> Promise[Optional[Cursor]]: ... + def recv(self) -> Promise[Cursor]: ... + def poll(self) -> Promise[None]: ... + def callback(self, + cb: Callable[[CursorController], None]) -> None: ... + def clear_callback(self) -> None: ... + def stop(self) -> bool: ... + diff --git a/lib/codemp/codemp.abi3.so.REMOVED.git-id b/lib/codemp/codemp.abi3.so.REMOVED.git-id new file mode 100644 index 0000000..97be16c --- /dev/null +++ b/lib/codemp/codemp.abi3.so.REMOVED.git-id @@ -0,0 +1 @@ +7145849af30523094153d307d57c79bb7a90680f \ No newline at end of file diff --git a/lib/codemp/py.typed b/lib/codemp/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/plugin.py b/plugin.py index b030718..79e9d73 100644 --- a/plugin.py +++ b/plugin.py @@ -5,10 +5,10 @@ import logging import random from typing import Tuple, Union -from Codemp.src.client import client -from Codemp.src.utils import safe_listener_detach -from Codemp.src.utils import safe_listener_attach -from Codemp.src import globals as g +from .src.client import client +from .src.utils import safe_listener_detach +from .src.utils import safe_listener_attach +from .src import globals as g LOG_LEVEL = logging.DEBUG handler = logging.StreamHandler() diff --git a/src/buffers.py b/src/buffers.py index 85b78f1..1a74f9b 100644 --- a/src/buffers.py +++ b/src/buffers.py @@ -4,9 +4,9 @@ import sublime import os import logging -import codemp -from Codemp.src import globals as g -from Codemp.src.utils import populate_view, safe_listener_attach, safe_listener_detach +from . import globals as g +from .utils import populate_view, safe_listener_attach, safe_listener_detach +from ..lib import codemp logger = logging.getLogger(__name__) diff --git a/src/client.py b/src/client.py index 3a2bbe6..b6cc047 100644 --- a/src/client.py +++ b/src/client.py @@ -5,11 +5,10 @@ from typing import Optional import sublime import logging -import codemp -from Codemp.src import globals as g -from Codemp.src.workspace import VirtualWorkspace -from Codemp.src.buffers import VirtualBuffer -from Codemp.src.utils import bidict +from ..lib import codemp +from .workspace import VirtualWorkspace +from .buffers import VirtualBuffer +from .utils import bidict logger = logging.getLogger(__name__) diff --git a/src/task_manager.py b/src/task_manager.py deleted file mode 100644 index 8b770d9..0000000 --- a/src/task_manager.py +++ /dev/null @@ -1,147 +0,0 @@ -from typing import Optional, Callable, Any - -import sublime -import logging -import asyncio -import threading -import concurrent.futures - -logger = logging.getLogger(__name__) - - -class sublimeWorkerThreadExecutor(concurrent.futures.Executor): - def __init__(self): - self._futures_pending = 0 - self._shutting_down = False - - # reentrant lock: we either increment from the main thread (submit calls) - # or we decrement from the worker thread (futures) - self._condvar = threading.Condition() - - def submit( - self, fn: Callable[..., Any], *args: Any, **kwargs: Any - ) -> concurrent.futures.Future: - if self._shutting_down: - raise RuntimeError("Executor is shutting down") - - with self._condvar: - self._futures_pending += 1 - - logger.debug("Spawning a future in the main thread") - future = concurrent.futures.Future() - - def coro() -> None: - logger.debug("Running a future from the worker thread") - try: - future.set_result(fn(*args, **kwargs)) - except BaseException as e: - future.set_exception(e) - with self._condvar: - self._futures_pending -= 1 - - sublime.set_timeout_async(coro) - return future - - def shutdown(self, wait: bool = True) -> None: - self._shutting_down = True - if not wait: - return - - with self._condvar: - self._condvar.wait_for(lambda: self._futures_pending == 0) - - -class Runtime: - def __init__(self): - self.tasks = [] - self.loop = asyncio.new_event_loop() - self.loop.set_default_executor(sublimeWorkerThreadExecutor()) - self.loop.set_debug(True) - self.thread = threading.Thread( - target=self.loop.run_forever, name="codemp-asyncio-loop" - ) - logger.debug("spinning up even loop in its own thread.") - self.thread.start() - - def __del__(self): - logger.debug("closing down the event loop.") - for task in asyncio.all_tasks(self.loop): - task.cancel() - - self.stop_loop() - - try: - self.loop.run_until_complete(self.loop.shutdown_asyncgens()) - except Exception as e: - logger.error(f"Unexpected crash while shutting down event loop: {e}") - - self.thread.join() - - def stop_loop(self): - logger.debug("stopping event loop.") - self.loop.call_soon_threadsafe(lambda: asyncio.get_running_loop().stop()) - - def run_blocking(self, fut, *args, **kwargs): - return self.loop.run_in_executor(None, fut, *args, **kwargs) - - def dispatch(self, coro, name=None): - """ - Dispatch a task on the event loop and returns the task itself. - Similar to `run_coroutine_threadsafe` but returns the - actual task running and not the result of the coroutine. - - `run_coroutine_threadsafe` returns a concurrent.futures.Future - which has a blocking .result so not really suited for long running - coroutines - """ - logger.debug("dispatching coroutine...") - - def make_task(fut): - logger.debug("creating task on the loop.") - try: - fut.set_result(self.loop.create_task(coro)) - except Exception as e: - fut.set_exception(e) - - # create the future to populate with the task - # we use the concurrent.futures.Future since it is thread safe - # and the .result() call is blocking. - fut = concurrent.futures.Future() - self.loop.call_soon_threadsafe(make_task, fut) - task = fut.result(None) # wait for the task to be created - task.set_name(name) - self.tasks.append(task) # save the reference - return task - - def block_on(self, coro, timeout=None): - fut = asyncio.run_coroutine_threadsafe(coro, self.loop) - try: - return fut.result(timeout) - except asyncio.CancelledError: - logger.debug("future got cancelled.") - raise - except TimeoutError: - logger.debug("future took too long to finish.") - raise - except Exception as e: - raise e - - def get_task(self, name) -> Optional[asyncio.Task]: - return next((t for t in self.tasks if t.get_name() == name), None) - - def stop_task(self, name): - task = self.get_task(name) - if task is not None: - self.dispatch(self.wait_for_cancel(task)) - - async def wait_for_cancel(self, task): - task.cancel() # cancelling a task, merely requests a cancellation. - try: - await task - except asyncio.CancelledError: - return - - -# store a global in the module so it acts as a singleton -# (modules are loaded only once) -# rt = Runtime() diff --git a/src/utils.py b/src/utils.py index 9915ea0..6f9b7c4 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,7 +1,7 @@ import sublime import sublime_plugin from typing import Dict, Generic, TypeVar -from Codemp.src import globals as g +from . import globals as g # bidirectional dictionary so that we can have bidirectional # lookup! diff --git a/src/workspace.py b/src/workspace.py index e16c19d..5fcdbfd 100644 --- a/src/workspace.py +++ b/src/workspace.py @@ -6,12 +6,10 @@ import shutil import tempfile import logging -import codemp -from Codemp.src import globals as g -from Codemp.src.buffers import VirtualBuffer -from Codemp.src.utils import draw_cursor_region, safe_listener_attach, sublime_plugin -from Codemp.src.utils import bidict - +from ..lib import codemp +from . import globals as g +from .buffers import VirtualBuffer +from .utils import draw_cursor_region, sublime_plugin logger = logging.getLogger(__name__)