diff --git a/bindings/codemp_client.cpython-38-darwin.so.REMOVED.git-id b/bindings/codemp_client.cpython-38-darwin.so.REMOVED.git-id index 0127aff..3df941f 100644 --- a/bindings/codemp_client.cpython-38-darwin.so.REMOVED.git-id +++ b/bindings/codemp_client.cpython-38-darwin.so.REMOVED.git-id @@ -1 +1 @@ -8d18661818436ebf65d16f9447601b123aef410e \ No newline at end of file +80ba7b16f0b0647e2f1d8520f3a90c51000bbd2c \ No newline at end of file diff --git a/build.sh b/build.sh index 2395b00..e75ee18 100755 --- a/build.sh +++ b/build.sh @@ -14,7 +14,9 @@ PYO3_PYTHON="$(pyenv which python)" PYTHON_SYS_EXECUTABLE="$PYO3_PYTHON" CARGO_FEATURES="pyo3/extension-module" +echo "Building with python: $PYO3_PYTHON" env PYO3_PYTHON="${PYO3_PYTHON}" PYTHON_SYS_EXECUTABLE="$PYO3_PYTHON" cargo build --features "$CARGO_FEATURES" +echo "Copying into: $TARGET_DIR/$FULL_TARGET" [[ -f "$TARGET_DIR/$FUll_TARGET" ]] && echo "$FILE exists." ditto "$BUILD_DIR/${FILENAME}.dylib" "$TARGET_DIR/$FULL_TARGET" diff --git a/src/lib.rs b/src/lib.rs index 240fa27..07d19c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,9 +38,7 @@ impl From for PyErr { #[pyfunction] fn codemp_init<'a>(py: Python<'a>) -> PyResult> { let py_instance: PyClientHandle = CodempInstance::default().into(); - Python::with_gil(|py| { - Ok(Py::new(py, py_instance)?) - }) + Ok(Py::new(py, py_instance)?) } #[pyclass] @@ -79,12 +77,13 @@ impl PyClientHandle { let rc = self.0.clone(); pyo3_asyncio::tokio::future_into_py(py, async move { - let buffctrl = rc.attach(path.as_str()) + let buffctrl: PyBufferController = rc.attach(path.as_str()) .await - .map_err(PyCodempError::from)?; + .map_err(PyCodempError::from)? + .into(); Python::with_gil(|py| { - Ok(Py::new(py, PyBufferController(buffctrl))?) + Ok(Py::new(py, buffctrl)?) }) }) } @@ -93,12 +92,13 @@ impl PyClientHandle { let rc = self.0.clone(); pyo3_asyncio::tokio::future_into_py(py, async move { - let curctrl = rc.join(session.as_str()) + let curctrl: PyCursorController = rc.join(session.as_str()) .await - .map_err(PyCodempError::from)?; + .map_err(PyCodempError::from)? + .into(); Python::with_gil(|py| { - Ok(Py::new(py, PyCursorController(curctrl))?) + Ok(Py::new(py, curctrl)?) }) }) } @@ -107,12 +107,13 @@ impl PyClientHandle { let rc = self.0.clone(); pyo3_asyncio::tokio::future_into_py(py, async move { - let curctrl = rc.get_cursor() + let curctrl: PyCursorController = rc.get_cursor() .await - .map_err(PyCodempError::from)?; + .map_err(PyCodempError::from)? + .into(); Python::with_gil(|py| { - Ok(Py::new(py, PyCursorController(curctrl))?) + Ok(Py::new(py, curctrl)?) }) }) } @@ -121,12 +122,13 @@ impl PyClientHandle { let rc = self.0.clone(); pyo3_asyncio::tokio::future_into_py(py, async move { - let buffctrl = rc.get_buffer(path.as_str()) + let buffctrl: PyBufferController = rc.get_buffer(path.as_str()) .await - .map_err(PyCodempError::from)?; + .map_err(PyCodempError::from)? + .into(); Python::with_gil(|py| { - Ok(Py::new(py, PyBufferController(buffctrl))?) + Ok(Py::new(py, buffctrl)?) }) }) } @@ -154,20 +156,30 @@ impl PyClientHandle { } } -impl From:: for PyCursorController { - fn from(value: CodempCursorController) -> Self { - PyCursorController(Arc::new(value)) - } -} - -impl From:: for PyBufferController { - fn from(value: CodempBufferController) -> Self { - PyBufferController(Arc::new(value)) - } -} - #[pyclass] -struct PyCursorController(Arc); +struct PyCursorController { + handle: Arc, + cb_trigger: Option> +} + +impl From::> for PyCursorController { + fn from(value: Arc) -> Self { + PyCursorController { + handle: value, + cb_trigger: None + } + } +} + +fn py_cursor_callback_wrapper(cb: PyObject) + -> Box () + Send + Sync + 'static> +{ + let closure = move |data: CodempCursorEvent| { + let args: PyCursorEvent = data.into(); + Python::with_gil(|py| { let _ = cb.call1(py, (args,)); }); + }; + Box::new(closure) +} #[pymethods] impl PyCursorController { @@ -193,13 +205,39 @@ impl PyCursorController { // 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) -> 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, py: Python<'a>, path: String, start: (i32, i32), end: (i32, i32)) -> PyResult<&'a PyAny> { - let rc = self.0.clone(); + let rc = self.handle.clone(); let pos = CodempCursorPosition { buffer: path, - start: Some(CodempRowCol { row: start.0, col: start.1 }), - end: Some(CodempRowCol { row: end.0, col: end.1 }) + start: Some(start.into()), + end: Some(end.into()) }; pyo3_asyncio::tokio::future_into_py(py, async move { @@ -210,19 +248,63 @@ impl PyCursorController { } + fn try_recv(&self, py: Python<'_>) -> PyResult { + match self.handle.try_recv().map_err(PyCodempError::from)? { + Some(cur_event) => { + let evt = PyCursorEvent::from(cur_event); + Ok(evt.into_py(py)) + }, + None => Ok(py.None()) + } + } + fn recv<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { - let rc = self.0.clone(); + let rc = self.handle.clone(); pyo3_asyncio::tokio::future_into_py(py, async move { - let cur_event = rc.recv() + let cur_event: PyCursorEvent = rc.recv() .await - .map_err(PyCodempError::from)?; - Ok(()) + .map_err(PyCodempError::from)? + .into(); + Python::with_gil(|py| { + Ok(Py::new(py, cur_event)?) + }) + }) + } + + fn poll<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { + let rc = self.handle.clone(); + + pyo3_asyncio::tokio::future_into_py(py, async move { + Ok(rc.poll().await.map_err(PyCodempError::from)?) }) } } + #[pyclass] -struct PyBufferController(Arc); +struct PyBufferController { + handle: Arc, + cb_trigger: Option> +} + +impl From::> for PyBufferController { + fn from(value: Arc) -> Self { + PyBufferController{ + handle: value, + cb_trigger: None + } + } +} + +fn py_buffer_callback_wrapper(cb: PyObject) + -> Box () + 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] impl PyBufferController { @@ -250,40 +332,150 @@ impl PyBufferController { // }) // } + 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) -> 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> { - let cont: Py = PyString::new(py, self.0.content().as_str()).into(); + let cont: Py = PyString::new(py, self.handle.content().as_str()).into(); Ok(cont) } - fn send<'a>(&self, py: Python<'a>, skip: usize, text: String, tail: usize) -> PyResult<&'a PyAny>{ - let rc = self.0.clone(); + // What to do with this send? does it make sense to implement it at all? + // fn send<'a>(&self, py: Python<'a>, skip: usize, text: String, tail: usize) -> PyResult<&'a PyAny>{ + // let rc = self.handle.clone(); + // pyo3_asyncio::tokio::future_into_py(py, async move { + // Ok(()) + // }) + // } + + fn try_recv(&self, py: Python<'_>) -> PyResult { + match self.handle.try_recv().map_err(PyCodempError::from)? { + Some(txt_change) => { + let evt = PyTextChange::from(txt_change); + Ok(evt.into_py(py)) + }, + None => Ok(py.None()) + } + } + + fn recv<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { + let rc = self.handle.clone(); + pyo3_asyncio::tokio::future_into_py(py, async move { - Ok(()) + let txt_change: PyTextChange = rc.recv() + .await + .map_err(PyCodempError::from)? + .into(); + Python::with_gil(|py| { + Ok(Py::new(py, txt_change)?) + }) }) - } + } + + fn poll<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { + let rc = self.handle.clone(); + + pyo3_asyncio::tokio::future_into_py(py, async move { + Ok(rc.poll().await.map_err(PyCodempError::from)?) + }) + } } -// #[pyclass] -// struct PyCursorEvent { -// user: String, -// buffer: Some(String), -// start: (i32, i32), -// end: (i32, i32) -// } +/* ---------- Type Wrappers ----------*/ +// All these objects are not meant to be handled rust side. +// Just to be sent to the python heap. -// impl From for PyCursorEvent { -// fn from(value: CodempCursorEvent) -> Self { -// PyCursorEvent { -// user: value.user, -// buffer: value.position.buffer, -// start: (value.position.start.row, value.position.start.col), -// end: (value.position.end.row, value.position.end.col) -// } -// } -// } +#[pyclass] +struct PyCursorEvent { + user: String, + buffer: String, + start: (i32, i32), + end: (i32, i32) +} +impl From for PyCursorEvent { + fn from(value: CodempCursorEvent) -> Self { + // todo, handle this optional better? + let pos = value.position.unwrap_or_default(); + PyCursorEvent { + user: value.user, + buffer: pos.buffer, + start: pos.start.unwrap_or_default().into(), + end: pos.end.unwrap_or_default().into() + } + } +} -// Python module +#[pyclass] +struct PyTextChange { + start_incl: usize, + end_excl: usize, + content: String +} + +impl From for PyTextChange { + fn from(value: CodempTextChange) -> Self { + PyTextChange { start_incl: value.span.start, end_excl: value.span.end, content: value.content } + } +} + +/* ------ Python module --------*/ #[pymodule] fn codemp_client(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(codemp_init, m)?)?; @@ -291,5 +483,10 @@ fn codemp_client(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) -} \ No newline at end of file +} + +