mirror of
https://github.com/hexedtech/codemp-sublime.git
synced 2024-12-04 03:44:52 +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