mirror of
https://github.com/hexedtech/codemp-sublime.git
synced 2024-12-03 11:34:51 +01:00
initial commit
Former-commit-id: fbb58155042cd05b869941cdbdd83819b7c1907e
This commit is contained in:
commit
1ab1ac36be
14 changed files with 847 additions and 0 deletions
120
.github/workflows/CI.yml
vendored
Normal file
120
.github/workflows/CI.yml
vendored
Normal file
|
@ -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 *
|
77
.gitignore
vendored
Normal file
77
.gitignore
vendored
Normal file
|
@ -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
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "ext/sublime_asyncio"]
|
||||
path = ext/sublime_asyncio
|
||||
url = git@github.com:sublimelsp/sublime_asyncio.git
|
1
.python-version
Normal file
1
.python-version
Normal file
|
@ -0,0 +1 @@
|
|||
3.8
|
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
@ -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"
|
|
@ -0,0 +1 @@
|
|||
8d18661818436ebf65d16f9447601b123aef410e
|
5
build.rs
Normal file
5
build.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
use pyo3_build_config;
|
||||
|
||||
fn main() {
|
||||
pyo3_build_config::add_extension_module_link_args();
|
||||
}
|
20
build.sh
Executable file
20
build.sh
Executable file
|
@ -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"
|
1
ext/sublime_asyncio
Submodule
1
ext/sublime_asyncio
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 6a8eb2dbd36c72b34eb0078bc1b3cb96f1692acb
|
254
plugin.py
Normal file
254
plugin.py
Normal file
|
@ -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"
|
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
|
@ -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"]
|
71
src/codemp_client.py
Normal file
71
src/codemp_client.py
Normal file
|
@ -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)
|
||||
|
234
src/lib.rs
Normal file
234
src/lib.rs
Normal file
|
@ -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<Mutex<CodempClient>>);
|
||||
|
||||
impl From::<CodempClient> 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> = PyString::new(py, binding.id()).into();
|
||||
Ok(id)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn create<'a>(&self, py: Python<'a>, path: String, content: Option<String>) -> 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> = 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::<CursorControllerHandle> for PyControllerHandle {
|
||||
fn from(value: CursorControllerHandle) -> Self {
|
||||
PyControllerHandle(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From::<OperationControllerHandle> 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<PyAny>, caller_id: Py<PyString>) -> 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<PyAny>, caller_id: Py<PyString>) -> 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<Py<PyString>> {
|
||||
let cont: Py<PyString> = 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> = PyBool::new(py, true).into();
|
||||
// Ok(accepted)
|
||||
// })
|
||||
// } else {
|
||||
// Python::with_gil(|py| {
|
||||
// let accepted: Py<PyBool> = 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::<PyClientHandle>()?;
|
||||
m.add_class::<PyControllerHandle>()?;
|
||||
m.add_class::<PyOperationsHandle>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
26
test.py
Normal file
26
test.py
Normal file
|
@ -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())
|
Loading…
Reference in a new issue