commit 1ab1ac36be9de66032684868248cf52724f6caa3 Author: Camillo Schenone Date: Thu Aug 17 18:39:47 2023 +0200 initial commit Former-commit-id: fbb58155042cd05b869941cdbdd83819b7c1907e diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..8268eaf --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,120 @@ +# This file is autogenerated by maturin v1.1.0 +# To update, run +# +# maturin generate-ci github +# +name: CI + +on: + push: + branches: + - main + - master + tags: + - '*' + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64, x86, aarch64, armv7, s390x, ppc64le] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + manylinux: auto + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + windows: + runs-on: windows-latest + strategy: + matrix: + target: [x64, x86] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + architecture: ${{ matrix.target }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + macos: + runs-on: macos-latest + strategy: + matrix: + target: [x86_64, aarch64] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + release: + name: Release + runs-on: ubuntu-latest + if: "startsWith(github.ref, 'refs/tags/')" + needs: [linux, windows, macos, sdist] + steps: + - uses: actions/download-artifact@v3 + with: + name: wheels + - name: Publish to PyPI + uses: PyO3/maturin-action@v1 + env: + MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + with: + command: upload + args: --skip-existing * diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89fa328 --- /dev/null +++ b/.gitignore @@ -0,0 +1,77 @@ +/target +/ext/codemp_client/* + +# Byte-compiled / optimized / DLL files +__pycache__/ +.pytest_cache/ +*.py[cod] + +# cargo +*.lock + +#sublime +*.sublime-commands +*.sublime-project + +# Distribution / packaging +.Python +.venv/ +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +include/ +man/ +venv/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +pip-selfcheck.json + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +.DS_Store + +# Sphinx documentation +docs/_build/ + +# PyCharm +.idea/ + +# VSCode +.vscode/ + +# Pyenv +#.python-version diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a8100f9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ext/sublime_asyncio"] + path = ext/sublime_asyncio + url = git@github.com:sublimelsp/sublime_asyncio.git diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..cc1923a --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.8 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bd4fa9f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "CodempClient-Sublime" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "codemp_client" +crate-type = ["cdylib"] + +[dependencies] +codemp = { path = "./ext/codemp" } +pyo3 = { version = "0.19", features = ["extension-module"] } +pyo3-asyncio = { version = "0.19", features = ["tokio-runtime"] } +tokio = "1.29.1" + +[build-dependencies] +pyo3-build-config = "0.19.2" \ No newline at end of file diff --git a/bindings/codemp_client.cpython-38-darwin.so.REMOVED.git-id b/bindings/codemp_client.cpython-38-darwin.so.REMOVED.git-id new file mode 100644 index 0000000..0127aff --- /dev/null +++ b/bindings/codemp_client.cpython-38-darwin.so.REMOVED.git-id @@ -0,0 +1 @@ +8d18661818436ebf65d16f9447601b123aef410e \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..642e934 --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +use pyo3_build_config; + +fn main() { + pyo3_build_config::add_extension_module_link_args(); +} \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..2395b00 --- /dev/null +++ b/build.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +ROOT_DIR="$(pwd)" +BUILD_DIR="$ROOT_DIR/target/debug/deps" +FILENAME="libcodemp_client" + +TARGET_DIR="$ROOT_DIR/bindings" +TARGET_NAME="codemp_client" +TARGET_EXT="$(python -c 'import sysconfig; print(sysconfig.get_config_var("EXT_SUFFIX"))')" + +FULL_TARGET="${TARGET_NAME}${TARGET_EXT}" + +PYO3_PYTHON="$(pyenv which python)" +PYTHON_SYS_EXECUTABLE="$PYO3_PYTHON" +CARGO_FEATURES="pyo3/extension-module" + +env PYO3_PYTHON="${PYO3_PYTHON}" PYTHON_SYS_EXECUTABLE="$PYO3_PYTHON" cargo build --features "$CARGO_FEATURES" + +[[ -f "$TARGET_DIR/$FUll_TARGET" ]] && echo "$FILE exists." +ditto "$BUILD_DIR/${FILENAME}.dylib" "$TARGET_DIR/$FULL_TARGET" diff --git a/ext/sublime_asyncio b/ext/sublime_asyncio new file mode 160000 index 0000000..6a8eb2d --- /dev/null +++ b/ext/sublime_asyncio @@ -0,0 +1 @@ +Subproject commit 6a8eb2dbd36c72b34eb0078bc1b3cb96f1692acb diff --git a/plugin.py b/plugin.py new file mode 100644 index 0000000..9a8d2bd --- /dev/null +++ b/plugin.py @@ -0,0 +1,254 @@ +import sublime +import sublime_plugin + +# import Codemp.codemp_client as codemp +from Codemp.src.codemp_client import * +import Codemp.ext.sublime_asyncio as sublime_asyncio +import asyncio +import time + +# UGLYYYY, find a way to not have global variables laying around. +_tasks = [] +_client = CodempClient() +_cursor_controller = None +_op_controller = None +_setting_key = "codemp_buffer" + +def store_task(name = None): + def store_named_task(task): + global _tasks + task.set_name(name) + _tasks.append(task) + + return store_named_task + +def plugin_loaded(): + sublime_asyncio.acquire() # instantiate and start a global event loop. + +class CodempClientViewEventListener(sublime_plugin.ViewEventListener): + @classmethod + def is_applicable(cls, settings): + return "codemp_buffer" in settings + + def on_selection_modified_async(self): + global _cursor_controller + if _cursor_controller: + sublime_asyncio.dispatch(send_selection(self.view)) + + def on_close(self): + self.view.settings()["codemp_buffer"] = False + +class CodempClientTextChangeListener(sublime_plugin.TextChangeListener): + @classmethod + def is_applicable(cls, buffer): + for view in buffer.views(): + if "codemp_buffer" in view.settings(): + return True + return False + + def on_text_changed(self, changes): + global _op_controller + if _op_controller: + for change in changes: + sublime_asyncio.dispatch(apply_changes(change)) + +async def apply_changes(change): + global _op_controller + + text = change.str + skip = change.a.pt + if change.len_utf8 == 0: # we are inserting new text. + tail = len(_op_controller.get_content()) - skip + else: # we are changing an existing region of text of length len_utf8 + tail = len(_op_controller.get_content()) - skip - change.len_utf8 + + tail_skip = len(_op_controller.get_content()) - tail + print("[buff change]", skip, text, tail_skip) + await _op_controller.apply(skip, text, tail) + +async def make_connection(server_host): + global _client + + if _client.ready: + sublime.message_dialog("A connection already exists.") + return + + sublime.status_message("[codemp] Connecting to {}".format(server_host)) + print("[codemp] Connecting to {}".format(server_host)) + await _client.connect(server_host) + + id = await _client.get_id() + sublime.status_message("[codemp] Connected with client ID: {}".format(id)) + print("[codemp] Connected with client ID: ", id) + +async def move_cursor(usr, caller, path, start, end): + print(usr, caller, start, end) + +async def sync_buffer(caller, start, end, txt): + print("[buffer]", caller, start, end, txt) + +async def share_buffer(buffer): + global _client + global _cursor_controller + global _op_controller + + if not _client.ready: + sublime.error_message("No connected client.") + return + + sublime.status_message("[codemp] Sharing buffer {}".format(buffer)) + print("[codemp] Sharing buffer {}".format(buffer)) + + view = get_matching_view(buffer) + contents = get_contents(view) + created = await _client.create(view.file_name(), contents) + if not created: + sublime.error_message("Could not share buffer.") + return + + _op_controller = await _client.attach(buffer) + _op_controller.callback(sync_buffer, _client.id) + + _cursor_controller = await _client.listen() + _cursor_controller.callback(move_cursor, _client.id) + + if not _cursor_controller: + sublime.error_message("Could not subsribe a listener.") + return + if not _op_controller: + sublime.error_message("Could not attach to the buffer.") + return + + sublime.status_message("[codemp] Listening") + print("[codemp] Listening") + + view.settings()["codemp_buffer"] = True + +async def join_buffer(window, buffer): + global _client + global _cursor_controller + global _op_controller + + if not _client.ready: + sublime.error_message("No connected client.") + return + + view = get_matching_view(buffer) + + sublime.status_message("[codemp] Joining buffer {}".format(buffer)) + print("[codemp] Joining buffer {}".format(buffer)) + + _op_controller = await _client.attach(buffer) + content = _op_controller.get_content() + view.run_command("codemp_replace_view", {"content": content}) + + _cursor_controller = await _client.listen() + _cursor_controller.callback(move_cursor) + + + if not _cursor_controller: + sublime.error_message("Could not subsribe a listener.") + return + if not _op_controller: + sublime.error_message("Could not attach to the buffer.") + return + + sublime.status_message("[codemp] Listening") + print("[codemp] Listening") + + view.settings()["codemp_buffer"] = True + +async def send_selection(view): + global _cursor_controller + + path = view.file_name() + region = view.sel()[0] # TODO: only the last placed cursor/selection. + start = view.rowcol(region.begin()) #only counts UTF8 chars + end = view.rowcol(region.end()) + + await _cursor_controller.send(path, start, end) + +def get_contents(view): + r = sublime.Region(0, view.size()) + return view.substr(r) + +def get_matching_view(path): + for window in sublime.windows(): + for view in window.views(): + if view.file_name() == path: + return view + + +# See the proxy command class at the bottom +class CodempConnectCommand(sublime_plugin.WindowCommand): + def run(self, server_host): + sublime_asyncio.dispatch(make_connection(server_host)) + +# see proxy command at the bottom +class CodempShareCommand(sublime_plugin.WindowCommand): + def run(self, buffer): + sublime_asyncio.dispatch(share_buffer(buffer)) + +# see proxy command at the bottom +class CodempJoinCommand(sublime_plugin.WindowCommand): + def run(self, buffer): + sublime_asyncio.dispatch(join_buffer(self.window, buffer)) + +class CodempPopulateView(sublime_plugin.TextCommand): + def run(self, edit, content): + self.view.replace(edit, sublime.Region(0, self.view.size()), content) + +class ProxyCodempConnectCommand(sublime_plugin.WindowCommand): + # on_window_command, does not trigger when called from the command palette + # See: https://github.com/sublimehq/sublime_text/issues/2234 + def run(self, **kwargs): + self.window.run_command("codemp_connect", kwargs) + + def input(self, args): + if 'server_host' not in args: + return ServerHostInputHandler() + + def input_description(self): + return 'Server host:' + +class ProxyCodempShareCommand(sublime_plugin.WindowCommand): + # on_window_command, does not trigger when called from the command palette + # See: https://github.com/sublimehq/sublime_text/issues/2234 + def run(self, **kwargs): + self.window.run_command("codemp_share", kwargs) + + def input(self, args): + if 'buffer' not in args: + return BufferInputHandler() + + def input_description(self): + return 'Share Buffer:' + + +class ProxyCodempJoinCommand(sublime_plugin.WindowCommand): + # on_window_command, does not trigger when called from the command palette + # See: https://github.com/sublimehq/sublime_text/issues/2234 + def run(self, **kwargs): + self.window.run_command("codemp_join", kwargs) + + def input(self, args): + if 'buffer' not in args: + return BufferInputHandler() + + def input_description(self): + return 'Join Buffer:' + +class BufferInputHandler(sublime_plugin.ListInputHandler): + def list_items(self): + ret_list = [] + + for window in sublime.windows(): + for view in window.views(): + if view.file_name(): + ret_list.append(view.file_name()) + + return ret_list + +class ServerHostInputHandler(sublime_plugin.TextInputHandler): + def initial_text(self): + return "http://[::1]:50051" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6ffb3c6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["maturin>=1.1,<2.0"] +build-backend = "maturin" + +[project] +name = "Codemp Client" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] + + +[tool.maturin] +features = ["pyo3/extension-module"] diff --git a/src/codemp_client.py b/src/codemp_client.py new file mode 100644 index 0000000..b6749a3 --- /dev/null +++ b/src/codemp_client.py @@ -0,0 +1,71 @@ +import asyncio +import Codemp.bindings.codemp_client as libcodemp + +class CodempClient(): + + def __init__(self): + self.handle = None + self.id = None + self.ready = False + + async def connect(self, server_host): + self.handle = await libcodemp.connect(server_host) + self.id = await self.handle.get_id() + self.ready = True + + def disconnect(self): + self.handle = None + self.id = None + self.ready = False + # some code that tells the server to unsubscribe stuff as well. + + async def get_id(self): + if self.ready and not self.id: + self.id = await self.handle.get_id() + return self.id + elif self.ready: + return self.id + else: + raise RuntimeError("Attemp to get id without an established connection.") + + async def create(self, path, content=None): + if self.ready: + return await self.handle.create(path, content) + else: + raise RuntimeError("Attemp to create a buffer without a connection.") + + async def listen(self): + if self.ready: + return CursorController(await self.handle.listen()) + else: + raise RuntimeError("Attempt to listen without a connection.") + + async def attach(self, path): + if self.ready: + return ContentController(await self.handle.attach(path)) + else: + raise RuntimeError("Attempt to attach without a connection.") + +class CursorController(): + def __init__(self, handle): + self.handle = handle + + async def send(self, path, start, end): + await self.handle.send(path, start, end) + + def callback(self, coro, id): + self.handle.callback(coro, id) + +class ContentController(): + def __init__(self, handle): + self.handle = handle + + def get_content(self): + return self.handle.content() + + async def apply(self, skip, text, tail): + return await self.handle.apply(skip, text, tail) + + def callback(self, coro, id): + self.handle.callback(coro, id) + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a675f47 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,234 @@ +use std::{sync::Arc, error::Error, borrow::BorrowMut}; + +use codemp::{ + client::CodempClient, + controller::{cursor::{CursorSubscriber, CursorControllerHandle}, + buffer::{OperationControllerHandle, OperationControllerSubscriber}}, + proto::Position, factory::OperationFactory, tokio::sync::Mutex +}; + +use pyo3::{ + prelude::*, + exceptions::PyConnectionError, + types::{PyBool, PyString} +}; + +#[pyfunction] +fn connect<'a>(py: Python<'a>, dest: String) -> PyResult<&'a PyAny> { + // construct a python coroutine + pyo3_asyncio::tokio::future_into_py(py, async move { + match CodempClient::new(dest.as_str()).await { + Ok(c) => { + Python::with_gil(|py|{ + let cc: PyClientHandle = c.into(); + let handle = Py::new(py, cc)?; + Ok(handle) + }) + }, + Err(e) => { Err(PyConnectionError::new_err(e.source().unwrap().to_string())) } + } + }) +} + +#[pyclass] +#[derive(Clone)] +struct PyClientHandle(Arc>); + +impl From:: for PyClientHandle { + fn from(value: CodempClient) -> Self { + PyClientHandle(Arc::new(Mutex::new(value))) + } +} + +#[pymethods] +impl PyClientHandle { + fn get_id<'a>(&self, py: Python<'a>) -> PyResult<&'a PyAny> { + let rc = self.0.clone(); + pyo3_asyncio::tokio::future_into_py(py, async move { + let binding = rc.lock().await; + + Python::with_gil(|py| { + let id: Py = PyString::new(py, binding.id()).into(); + Ok(id) + }) + }) + } + + fn create<'a>(&self, py: Python<'a>, path: String, content: Option) -> PyResult<&'a PyAny> { + let rc = self.0.clone(); + + pyo3_asyncio::tokio::future_into_py(py, async move { + match rc.lock().await.create(path, content).await { + Ok(accepted) => { + Python::with_gil(|py| { + let accepted: Py = PyBool::new(py, accepted).into(); + Ok(accepted) + }) + }, + Err(e) => { Err(PyConnectionError::new_err(e.source().unwrap().to_string())) } + } + + }) + } + + fn listen<'a>(&self, py: Python<'a>) -> PyResult<&'a PyAny> { + let rc = self.0.clone(); + + pyo3_asyncio::tokio::future_into_py(py, async move { + match rc.lock().await.listen().await { + Ok(controller) => { + Python::with_gil(|py| { + let cc: PyControllerHandle = controller.into(); + let contr = Py::new(py, cc)?; + Ok(contr) + }) + }, + Err(e) => {Err(PyConnectionError::new_err(e.source().unwrap().to_string()))} + } + }) + } + + fn attach<'a>(&self, py: Python<'a>, path: String) -> PyResult<&'a PyAny> { + let rc = self.0.clone(); + let uri = path.clone(); + + pyo3_asyncio::tokio::future_into_py(py, async move { + match rc.lock().await.attach(uri).await { + Ok(factory) => { + Python::with_gil(|py| { + let ff: PyOperationsHandle = factory.into(); + let fact = Py::new(py, ff)?; + Ok(fact) + }) + }, + Err(e) => {Err(PyConnectionError::new_err(e.source().unwrap().to_string()))} + } + }) + } +} + + +impl From:: for PyControllerHandle { + fn from(value: CursorControllerHandle) -> Self { + PyControllerHandle(value) + } +} + +impl From:: for PyOperationsHandle { + fn from(value: OperationControllerHandle) -> Self { + PyOperationsHandle(value) + } +} + +#[pyclass] +struct PyControllerHandle(CursorControllerHandle); + +#[pymethods] +impl PyControllerHandle { + fn callback<'a>(&'a self, py: Python<'a>, coro_py: Py, caller_id: Py) -> PyResult<&'a PyAny> { + let mut rc = self.0.clone(); + let cb = coro_py.clone(); + // We want to start polling the ControlHandle and call the callback every time + // we have something. + + pyo3_asyncio::tokio::future_into_py(py, async move { + while let Some(op) = rc.poll().await { + let start = op.start.unwrap_or(Position { row: 0, col: 0}); + let end = op.end.unwrap_or(Position { row: 0, col: 0}); + + let cb_fut = Python::with_gil(|py| -> PyResult<_> { + let args = (op.user, caller_id.clone(), op.buffer, (start.row, start.col), (end.row, end.col)); + let coro = cb.call1(py, args)?; + pyo3_asyncio::tokio::into_future(coro.into_ref(py)) + })?; + + cb_fut.await?; + } + Ok(()) + }) + } // to call after polling cursor movements. + + fn send<'a>(&self, py: Python<'a>, path: String, start: (i32, i32), end: (i32, i32)) -> PyResult<&'a PyAny> { + let rc = self.0.clone(); + + pyo3_asyncio::tokio::future_into_py(py, async move { + let startpos = Position { row: start.0, col: start.1 }; + let endpos = Position { row: end.0, col: end.1 }; + + rc.send(path.as_str(), startpos, endpos).await; + Ok(Python::with_gil(|py| py.None())) + }) + + } // when we change the cursor ourselves. +} + +#[pyclass] +struct PyOperationsHandle(OperationControllerHandle); + +#[pymethods] +impl PyOperationsHandle { + fn callback<'a>(&'a self, py: Python<'a>, coro_py: Py, caller_id: Py) -> PyResult<&'a PyAny> { + let mut rc = self.0.clone(); + let cb = coro_py.clone(); + // We want to start polling the ControlHandle and call the callback every time + // we have something. + + pyo3_asyncio::tokio::future_into_py(py, async move { + while let Some(edit) = rc.poll().await { + let start = edit.span.start; + let end = edit.span.end; + let text = edit.content; + + let cb_fut = Python::with_gil(|py| -> PyResult<_> { + let args = (caller_id.clone(), start, end, text); + let coro = cb.call1(py, args)?; + pyo3_asyncio::tokio::into_future(coro.into_ref(py)) + })?; + + cb_fut.await?; + } + Ok(()) + }) + } //to call after polling text changes + + fn content(&self, py: Python<'_>) -> PyResult> { + let cont: Py = PyString::new(py, self.0.content().as_str()).into(); + Ok(cont) + } + + fn apply<'a>(&self, py: Python<'a>, skip: usize, text: String, tail: usize) -> PyResult<&'a PyAny>{ + let rc = self.0.clone(); + + pyo3_asyncio::tokio::future_into_py(py, async move { + + match rc.delta(skip, text.as_str(), tail) { + Some(op) => { rc.apply(op).await; Ok(()) }, + None => Err(PyConnectionError::new_err("delta failed")) + } + // if let Some(op) = rc.delta(skip, text.as_str(), tail) { + // rc.apply(op).await; + // Python::with_gil(|py| { + // let accepted: Py = PyBool::new(py, true).into(); + // Ok(accepted) + // }) + // } else { + // Python::with_gil(|py| { + // let accepted: Py = PyBool::new(py, false).into(); + // Ok(accepted) + // }) + // } + }) + } //after making text mofications. +} + + +// Python module +#[pymodule] +fn codemp_client(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(connect, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + + Ok(()) +} \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..113a965 --- /dev/null +++ b/test.py @@ -0,0 +1,26 @@ +import bindings.codemp_client +import asyncio + +HOST = "http://alemi.dev:50051" +LOCAL_HOST = "http://[::1]:50051" + +async def get_handle(host): + return await bindings.codemp_client.connect(host) + +async def get_id(handle): + return await handle.get_id() + +async def create_buffer(handle): + return await handle.create("test.py") + +async def main(): + handle = await bindings.codemp_client.connect(HOST) + print("Client Handle: ", handle) + id = await handle.get_id() + print("Client ID: ", id) + buffer_created = await create_buffer(handle) + print("buffer_created: ", buffer_created) + + + +asyncio.run(main())