First steps towards the migration, updated the bindings, and python side wrapper

Former-commit-id: fe60dec9d36c28b9f86048a0791349a804c66c8f
This commit is contained in:
Camillo Schenone 2023-11-22 12:25:08 +01:00
parent 9a964a099b
commit 8808405a3a
4 changed files with 81 additions and 256 deletions

View file

@ -9,7 +9,7 @@ name = "codemp_client"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", tag = "v0.4.4"} codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", branch = "woot"}
pyo3 = { version = "0.19", features = ["extension-module"] } pyo3 = { version = "0.19", features = ["extension-module"] }
pyo3-asyncio = { version = "0.19", features = ["tokio-runtime"] } pyo3-asyncio = { version = "0.19", features = ["tokio-runtime"] }
tokio = "1.29.1" tokio = "1.29.1"

View file

@ -1 +1 @@
a4493ce329ed791cd4bd17b102efdde3ca4acd8f 293406e9737fd8937bf0b8c3e835778531c58115

View file

@ -5,34 +5,40 @@ class CodempClient():
def __init__(self): def __init__(self):
self.handle = libcodemp.codemp_init() self.handle = libcodemp.codemp_init()
## Bindings
async def connect(self, server_host): # -> None async def connect(self, server_host): # -> None
await self.handle.connect(server_host) await self.handle.connect(server_host)
def disconnect(self): # -> None async def join(self, session): # -> CursorController
self.handle = None return CursorController(await self.handle.join(session))
async def create(self, path, content=None): # -> None async def create(self, path, content=None): # -> None
await self.handle.create(path, content) await self.handle.create(path, content)
# join a workspace
async def join(self, session): # -> CursorController
return CursorController(await self.handle.join(session))
async def attach(self, path): # -> BufferController async def attach(self, path): # -> BufferController
return BufferController(await self.handle.attach(path)) return BufferController(await self.handle.attach(path))
async def get_cursor(self): # -> CursorController async def get_cursor(self): # -> CursorController
return CursorController(await self.handle.get_cursor()) return CursorController(await self.handle.get_cursor())
async def get_buffer(self, path): # -> BufferController async def get_buffer(self, path): # -> BufferController
return BufferController(await self.handle.get_buffer()) return BufferController(await self.handle.get_buffer())
async def disconnect_buffer(self, path): # -> None
await self.handle.disconnect_buffer(path)
async def leave_workspace(self): # -> None async def leave_workspace(self): # -> None
pass # todo await self.handle.leave_workspace()
async def disconnect_buffer(self, path): # -> None
await self.handle.disconnect_buffer(path)
async def select_buffer(): # -> String
await self.handle.select_buffer()
## Custom
async def disconnect(self): # -> None
# disconnect all buffers and workspaces first, maybe?
await self.leave_workspace()
# drop the handle, it will require a new instantiation
self.handle = None
class CursorController(): class CursorController():
def __init__(self, handle): def __init__(self, handle):
@ -51,38 +57,12 @@ class CursorController():
# await until new cursor event, then returns # await until new cursor event, then returns
return await self.handle.poll() return await self.handle.poll()
def drop_callback(self): # -> None
self.handle.drop_callback()
def callback(self, coro): # -> None
self.handle.callback(coro)
class BufferController(): class BufferController():
def __init__(self, handle): def __init__(self, handle):
self.handle = handle self.handle = handle
def get_content(self): # -> String def send(self, start, end, txt): # -> None
return self.handle.content() self.handle.send(start, end, txt)
def replace(self, txt): # -> None
# replace the whole buffer.
self.handle.replace(txt)
def insert(self, txt, pos): # -> None
# insert text at buffer position pos
self.handle.insert(txt, pos)
def delta(self, start, txt, end): # -> None
# delta in the region start..end with txt new content
self.handle.delta(start, txt, end)
def delete(self, pos, count): # -> None
# delete starting from pos, count chars.
self.handle.delete(pos, count)
def cancel(self, pos, count): # -> None
# cancel backward `count` elements from pos.
self.handle.cancle(pos, count)
def try_recv(self): # -> Optional[TextChange] def try_recv(self): # -> Optional[TextChange]
return self.handle.try_recv() return self.handle.try_recv()
@ -93,12 +73,6 @@ class BufferController():
async def poll(self): # -> ?? async def poll(self): # -> ??
return await self.handle.poll() return await self.handle.poll()
def drop_callback(self): # -> None
self.handle.drop_callback()
def callback(self, coro): # -> None
self.handle.callback(coro)

View file

@ -5,8 +5,7 @@ use codemp::errors::Error as CodempError;
use pyo3::{ use pyo3::{
prelude::*, prelude::*,
exceptions::{PyConnectionError, PyRuntimeError, PyBaseException}, exceptions::{PyConnectionError, PyRuntimeError, PyBaseException}
types::PyString
}; };
struct PyCodempError(CodempError); struct PyCodempError(CodempError);
@ -28,6 +27,9 @@ impl From<PyCodempError> for PyErr {
CodempError::InvalidState { msg } => { CodempError::InvalidState { msg } => {
PyRuntimeError::new_err(format!("Invalid state: {}", msg)) PyRuntimeError::new_err(format!("Invalid state: {}", msg))
}, },
CodempError::Deadlocked => {
PyRuntimeError::new_err(format!("Deadlock, retry."))
},
CodempError::Filler { message } => { CodempError::Filler { message } => {
PyBaseException::new_err(format!("Generic error: {}", message)) PyBaseException::new_err(format!("Generic error: {}", message))
} }
@ -42,7 +44,6 @@ fn codemp_init<'a>(py: Python<'a>) -> PyResult<Py<PyClientHandle>> {
} }
#[pyclass] #[pyclass]
#[derive(Clone)]
struct PyClientHandle(Arc<CodempInstance>); struct PyClientHandle(Arc<CodempInstance>);
impl From::<CodempInstance> for PyClientHandle { impl From::<CodempInstance> for PyClientHandle {
@ -65,6 +66,22 @@ impl PyClientHandle {
}) })
} }
// join a workspace
fn join<'a>(&'a self, py: Python<'a>, session: String) -> PyResult<&'a PyAny> {
let rc = self.0.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
let curctrl: PyCursorController = rc.join(session.as_str())
.await
.map_err(PyCodempError::from)?
.into();
Python::with_gil(|py| {
Ok(Py::new(py, curctrl)?)
})
})
}
fn create<'a>(&'a self, py: Python<'a>, path: String, content: Option<String>) -> PyResult<&'a PyAny> { fn create<'a>(&'a self, py: Python<'a>, path: String, content: Option<String>) -> PyResult<&'a PyAny> {
let rc = self.0.clone(); let rc = self.0.clone();
@ -91,22 +108,6 @@ impl PyClientHandle {
}) })
} }
// join a workspace
fn join<'a>(&'a self, py: Python<'a>, session: String) -> PyResult<&'a PyAny> {
let rc = self.0.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
let curctrl: PyCursorController = rc.join(session.as_str())
.await
.map_err(PyCodempError::from)?
.into();
Python::with_gil(|py| {
Ok(Py::new(py, curctrl)?)
})
})
}
fn get_cursor<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { fn get_cursor<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> {
let rc = self.0.clone(); let rc = self.0.clone();
@ -158,98 +159,47 @@ impl PyClientHandle {
Ok(()) Ok(())
}) })
} }
}
#[pyclass] fn select_buffer<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> {
struct PyCursorController { let rc = self.0.clone();
handle: Arc<CodempCursorController>,
cb_trigger: Option<tokio::sync::mpsc::UnboundedSender<()>>
}
impl From::<Arc<CodempCursorController>> for PyCursorController { pyo3_asyncio::tokio::future_into_py(py, async move {
fn from(value: Arc<CodempCursorController>) -> Self { let cont = rc.select_buffer()
PyCursorController { .await
handle: value, .map_err(PyCodempError::from)?;
cb_trigger: None
} Ok(cont)
})
} }
} }
fn py_cursor_callback_wrapper(cb: PyObject)
-> Box<dyn FnMut(CodempCursorEvent) -> () + Send + Sync + 'static> /* ########################################################################### */
{
let closure = move |data: CodempCursorEvent| { #[pyclass]
let args: PyCursorEvent = data.into(); struct PyCursorController(Arc<CodempCursorController>);
Python::with_gil(|py| { let _ = cb.call1(py, (args,)); });
}; impl From::<Arc<CodempCursorController>> for PyCursorController {
Box::new(closure) fn from(value: Arc<CodempCursorController>) -> Self {
PyCursorController(value)
}
} }
#[pymethods] #[pymethods]
impl PyCursorController { impl PyCursorController {
// 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(())
// })
// }
fn drop_callback(&mut self) -> PyResult<()> {
if let Some(channel) = &self.cb_trigger {
channel.send(())
.map_err(CodempError::from)
.map_err(PyCodempError::from)?;
self.cb_trigger = None;
}
Ok(())
}
fn callback<'a>(&'a mut self, py_cb: Py<PyAny>) -> PyResult<()> {
if let Some(_channel) = &self.cb_trigger {
Err(PyCodempError::from(CodempError::InvalidState { msg: "A callback is already running.".into() }).into())
} else {
let rt = pyo3_asyncio::tokio::get_runtime();
// create a channel to stop the callback task running on the tokio runtime.
// and save the sendent inside the python object, so that we can later call it.
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
self.cb_trigger = Some(tx);
self.handle.callback(rt, rx, py_cursor_callback_wrapper(py_cb));
Ok(())
}
}
fn send<'a>(&'a self, path: String, start: (i32, i32), end: (i32, i32)) -> PyResult<()> { fn send<'a>(&'a self, path: String, start: (i32, i32), end: (i32, i32)) -> PyResult<()> {
let rc = self.handle.clone();
let pos = CodempCursorPosition { let pos = CodempCursorPosition {
buffer: path, buffer: path,
start: Some(start.into()), start: Some(start.into()),
end: Some(end.into()) end: Some(end.into())
}; };
rc.send(pos).map_err(PyCodempError::from)?; Ok(self.0.send(pos).map_err(PyCodempError::from)?)
Ok(())
} }
fn try_recv(&self, py: Python<'_>) -> PyResult<PyObject> { fn try_recv(&self, py: Python<'_>) -> PyResult<PyObject> {
match self.handle.try_recv().map_err(PyCodempError::from)? { match self.0.try_recv().map_err(PyCodempError::from)? {
Some(cur_event) => { Some(cur_event) => {
let evt = PyCursorEvent::from(cur_event); let evt = PyCursorEvent::from(cur_event);
Ok(evt.into_py(py)) Ok(evt.into_py(py))
@ -259,7 +209,7 @@ impl PyCursorController {
} }
fn recv<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { fn recv<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> {
let rc = self.handle.clone(); let rc = self.0.clone();
pyo3_asyncio::tokio::future_into_py(py, async move { pyo3_asyncio::tokio::future_into_py(py, async move {
let cur_event: PyCursorEvent = rc.recv() let cur_event: PyCursorEvent = rc.recv()
@ -273,7 +223,7 @@ impl PyCursorController {
} }
fn poll<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { fn poll<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> {
let rc = self.handle.clone(); let rc = self.0.clone();
pyo3_asyncio::tokio::future_into_py(py, async move { pyo3_asyncio::tokio::future_into_py(py, async move {
Ok(rc.poll().await.map_err(PyCodempError::from)?) Ok(rc.poll().await.map_err(PyCodempError::from)?)
@ -282,129 +232,30 @@ impl PyCursorController {
} }
#[pyclass] #[pyclass]
struct PyBufferController { struct PyBufferController(Arc<CodempBufferController>);
handle: Arc<CodempBufferController>,
cb_trigger: Option<tokio::sync::mpsc::UnboundedSender<()>>
}
impl From::<Arc<CodempBufferController>> for PyBufferController { impl From::<Arc<CodempBufferController>> for PyBufferController {
fn from(value: Arc<CodempBufferController>) -> Self { fn from(value: Arc<CodempBufferController>) -> Self {
PyBufferController{ PyBufferController(value)
handle: value,
cb_trigger: None
}
} }
} }
fn py_buffer_callback_wrapper(cb: PyObject)
-> Box<dyn FnMut(CodempTextChange) -> () + Send + Sync + 'static>
{
let closure = move |data: CodempTextChange| {
let args: PyTextChange = data.into();
Python::with_gil(|py| { let _ = cb.call1(py, (args,)); });
};
Box::new(closure)
}
#[pymethods] #[pymethods]
impl PyBufferController { impl PyBufferController {
// 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(())
// })
// }
fn drop_callback(&mut self) -> PyResult<()> {
if let Some(channel) = &self.cb_trigger {
channel.send(())
.map_err(CodempError::from)
.map_err(PyCodempError::from)?;
self.cb_trigger = None;
}
Ok(())
}
fn callback<'a>(&'a mut self, py_cb: Py<PyAny>) -> PyResult<()> {
if let Some(_channel) = &self.cb_trigger {
Err(PyCodempError::from(CodempError::InvalidState { msg: "A callback is already running.".into() }).into())
} else {
let rt = pyo3_asyncio::tokio::get_runtime();
// could this be a oneshot channel?
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
self.cb_trigger = Some(tx);
self.handle.callback(rt, rx, py_buffer_callback_wrapper(py_cb));
Ok(())
}
}
fn replace(&self, txt: &str) -> PyResult<()> {
if let Some(op) = self.handle.replace(txt) {
self.handle.send(op).map_err(PyCodempError::from)?;
}
Ok(())
}
fn delta(&self, start: usize, txt: &str, end: usize) -> PyResult<()> {
if let Some(op) = self.handle.delta(start, txt, end){
self.handle.send(op).map_err(PyCodempError::from)?;
}
Ok(())
}
fn insert(&self, txt: &str, pos: u64) -> PyResult<()> {
let op = self.handle.insert(txt, pos);
self.handle.send(op).map_err(PyCodempError::from)?;
Ok(())
}
fn delete(&self, pos: u64, count: u64) -> PyResult<()> {
let op = self.handle.delete(pos, count);
self.handle.send(op).map_err(PyCodempError::from)?;
Ok(())
}
fn cancel(&self, pos: u64, count: u64) -> PyResult<()> {
let op = self.handle.cancel(pos, count);
self.handle.send(op).map_err(PyCodempError::from)?;
Ok(())
}
fn content(&self, py: Python<'_>) -> PyResult<Py<PyString>> {
let cont: Py<PyString> = PyString::new(py, self.handle.content().as_str()).into();
Ok(cont)
}
// TODO: What to do with this send? // TODO: What to do with this send?
// does it make sense to implement it at all for the python side?? // does it make sense to implement it at all for the python side??
// fn send<'a>(&self, py: Python<'a>, skip: usize, text: String, tail: usize) -> PyResult<&'a PyAny>{ fn send(&self, start: usize, end: usize, txt: String) -> PyResult<()>{
// todo!() let op = CodempTextChange {
// } span: start..end,
content: txt.into()
};
Ok(self.0.send(op).map_err(PyCodempError::from)?)
}
fn try_recv(&self, py: Python<'_>) -> PyResult<PyObject> { fn try_recv(&self, py: Python<'_>) -> PyResult<PyObject> {
match self.handle.try_recv().map_err(PyCodempError::from)? { match self.0.try_recv().map_err(PyCodempError::from)? {
Some(txt_change) => { Some(txt_change) => {
let evt = PyTextChange::from(txt_change); let evt = PyTextChange::from(txt_change);
Ok(evt.into_py(py)) Ok(evt.into_py(py))
@ -414,7 +265,7 @@ impl PyBufferController {
} }
fn recv<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { fn recv<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> {
let rc = self.handle.clone(); let rc = self.0.clone();
pyo3_asyncio::tokio::future_into_py(py, async move { pyo3_asyncio::tokio::future_into_py(py, async move {
let txt_change: PyTextChange = rc.recv() let txt_change: PyTextChange = rc.recv()
@ -428,7 +279,7 @@ impl PyBufferController {
} }
fn poll<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { fn poll<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> {
let rc = self.handle.clone(); let rc = self.0.clone();
pyo3_asyncio::tokio::future_into_py(py, async move { pyo3_asyncio::tokio::future_into_py(py, async move {
Ok(rc.poll().await.map_err(PyCodempError::from)?) Ok(rc.poll().await.map_err(PyCodempError::from)?)