feat: added whole content diff to opseq

This commit is contained in:
ftbsc 2023-04-12 16:58:28 +02:00
parent 0f5e4c7f8d
commit 7ae5329ce5
5 changed files with 85 additions and 17 deletions

View file

@ -41,6 +41,7 @@ tracing-subscriber = { version = "0.3", optional = true }
rmpv = { version = "1", optional = true } rmpv = { version = "1", optional = true }
clap = { version = "4.2.1", features = ["derive"], optional = true } clap = { version = "4.2.1", features = ["derive"], optional = true }
nvim-rs = { version = "0.5", features = ["use_tokio"], optional = true } nvim-rs = { version = "0.5", features = ["use_tokio"], optional = true }
similar = { version = "2.2", features = ["inline"] }
[build-dependencies] [build-dependencies]
tonic-build = "0.9" tonic-build = "0.9"

View file

@ -1,14 +1,16 @@
local BINARY = vim.g.codemp_binary or "./codemp-client-nvim" local BINARY = vim.g.codemp_binary or "./codemp-client-nvim"
local M = {} local M = {}
M.jobid = nil
M.create = function(path, content) return vim.rpcrequest(M.jobid, "create", path, content) end M.jobid = nil
M.insert = function(path, txt, pos) return vim.rpcrequest(M.jobid, "insert", path, txt, pos) end M.create = function(path, content) return vim.rpcrequest(M.jobid, "create", path, content) end
M.cursor = function(path, row, col) return vim.rpcrequest(M.jobid, "cursor", path, row, col) end M.insert = function(path, txt, pos) return vim.rpcrequest(M.jobid, "insert", path, txt, pos) end
M.delete = function(path, pos, count) return vim.rpcrequest(M.jobid, "delete", path, pos, count) end M.delete = function(path, pos, count) return vim.rpcrequest(M.jobid, "delete", path, pos, count) end
M.attach = function(path) return vim.rpcrequest(M.jobid, "attach", path) end M.replace = function(path, txt) return vim.rpcrequest(M.jobid, "replace", path, txt) end
M.listen = function(path) return vim.rpcrequest(M.jobid, "listen", path) end M.cursor = function(path, row, col) return vim.rpcrequest(M.jobid, "cursor", path, row, col) end
M.detach = function(path) return vim.rpcrequest(M.jobid, "detach", path) end M.attach = function(path) return vim.rpcrequest(M.jobid, "attach", path) end
M.listen = function(path) return vim.rpcrequest(M.jobid, "listen", path) end
M.detach = function(path) return vim.rpcrequest(M.jobid, "detach", path) end
local function cursor_offset() local function cursor_offset()
local cursor = vim.api.nvim_win_get_cursor(0) local cursor = vim.api.nvim_win_get_cursor(0)
@ -18,10 +20,14 @@ end
local codemp_autocmds = vim.api.nvim_create_augroup("CodempAuGroup", { clear = true }) local codemp_autocmds = vim.api.nvim_create_augroup("CodempAuGroup", { clear = true })
local function hook_callbacks(path, buffer) local function hook_callbacks(path, buffer)
local prev_changedtick = vim.b.changedtick
vim.api.nvim_create_autocmd( vim.api.nvim_create_autocmd(
{ "InsertCharPre" }, { "InsertCharPre" },
{ {
callback = function(_) M.insert(path, vim.v.char, cursor_offset()) end, callback = function(_)
M.insert(path, vim.v.char, cursor_offset())
prev_changedtick = vim.b.changedtick + 1
end,
buffer = buffer, buffer = buffer,
group = codemp_autocmds, group = codemp_autocmds,
} }
@ -29,7 +35,12 @@ local function hook_callbacks(path, buffer)
vim.api.nvim_create_autocmd( vim.api.nvim_create_autocmd(
{ "CursorMoved", "CursorMovedI" }, { "CursorMoved", "CursorMovedI" },
{ {
callback = function(_) callback = function(args)
if vim.b.changedtick ~= prev_changedtick then
prev_changedtick = vim.b.changedtick
local lines = vim.api.nvim_buf_get_lines(args.buf, 0, -1, false)
M.replace(path, vim.fn.join(lines, "\n"))
end
local cursor = vim.api.nvim_win_get_cursor(0) local cursor = vim.api.nvim_win_get_cursor(0)
M.cursor(path, cursor[1], cursor[2]) M.cursor(path, cursor[1], cursor[2])
end, end,
@ -56,9 +67,9 @@ vim.api.nvim_create_user_command('Connect',
return return
end end
local bin_args = { BINARY } local bin_args = { BINARY }
if #args.args > 0 then if #args.fargs > 0 then
table.insert(bin_args, "--host") table.insert(bin_args, "--host")
table.insert(bin_args, args.args[1]) table.insert(bin_args, args.fargs[1])
end end
if args.bang then if args.bang then
table.insert(bin_args, "--debug") table.insert(bin_args, "--debug")

View file

@ -100,6 +100,23 @@ impl Handler for NeovimHandler {
} }
}, },
"replace" => {
if args.len() < 2 {
return Err(Value::from("not enough arguments"));
}
let path = default_empty_str(&args, 0);
let txt = default_empty_str(&args, 1);
let mut c = self.client.clone();
match c.replace(path, txt).await {
Ok(res) => match res {
true => Ok(Value::Nil),
false => Err(Value::from("rejected")),
},
Err(e) => Err(Value::from(format!("could not send replace: {}", e))),
}
},
"attach" => { "attach" => {
if args.len() < 1 { if args.len() < 1 {
return Err(Value::from("no path given")); return Err(Value::from("no path given"));

View file

@ -89,6 +89,24 @@ impl CodempClient {
} }
} }
pub async fn replace(&mut self, path: String, txt: String) -> Result<bool, Status> {
let factory = self.get_factory(&path)?;
match factory.replace(txt).await {
Err(e) => Err(Status::internal(format!("invalid operation: {}", e))),
Ok(op) => {
let req = OperationRequest {
path,
hash: "".into(),
user: self.id.to_string(),
opseq: serde_json::to_string(&op)
.map_err(|_| Status::invalid_argument("could not serialize opseq"))?,
};
let res = self.client.edit(req).await?.into_inner();
Ok(res.accepted)
},
}
}
pub async fn cursor(&mut self, path: String, row: i64, col: i64) -> Result<(), Status> { pub async fn cursor(&mut self, path: String, row: i64, col: i64) -> Result<(), Status> {
let req = CursorMov { let req = CursorMov {
path, user: self.id.to_string(), path, user: self.id.to_string(),

View file

@ -1,4 +1,5 @@
use operational_transform::{OperationSeq, OTError}; use operational_transform::{OperationSeq, OTError};
use similar::TextDiff;
use tokio::sync::{mpsc, watch, oneshot}; use tokio::sync::{mpsc, watch, oneshot};
use tracing::error; use tracing::error;
@ -21,13 +22,25 @@ impl OperationFactory {
self.content == txt self.content == txt
} }
pub fn replace(&mut self, txt: &str) -> OperationSeq { pub fn replace(&mut self, txt: &str) -> Result<OperationSeq, OTError> {
let out = OperationSeq::default(); let mut out = OperationSeq::default();
if self.content == txt { if self.content == txt { // TODO throw and error rather than wasting everyone's resources
return out; // nothing to do out.retain(txt.len() as u64);
return Ok(out); // nothing to do
} }
todo!() let diff = TextDiff::from_chars(self.content.as_str(), txt);
for change in diff.iter_all_changes() {
match change.tag() {
similar::ChangeTag::Equal => out.retain(1),
similar::ChangeTag::Delete => out.delete(1),
similar::ChangeTag::Insert => out.insert(change.value()),
}
}
self.content = out.apply(&self.content)?;
Ok(out)
} }
pub fn insert(&mut self, txt: &str, pos: u64) -> Result<OperationSeq, OTError> { pub fn insert(&mut self, txt: &str, pos: u64) -> Result<OperationSeq, OTError> {
@ -116,6 +129,12 @@ impl AsyncFactory {
rx.await.map_err(|_| OTError)? rx.await.map_err(|_| OTError)?
} }
pub async fn replace(&self, txt: String) -> Result<OperationSeq, OTError> {
let (tx, rx) = oneshot::channel();
self.ops.send(OpMsg::Exec(OpWrapper::Replace(txt), tx)).await.map_err(|_| OTError)?;
rx.await.map_err(|_| OTError)?
}
pub async fn process(&self, opseq: OperationSeq) -> Result<String, OTError> { pub async fn process(&self, opseq: OperationSeq) -> Result<String, OTError> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.ops.send(OpMsg::Process(opseq, tx)).await.map_err(|_| OTError)?; self.ops.send(OpMsg::Process(opseq, tx)).await.map_err(|_| OTError)?;
@ -135,6 +154,7 @@ enum OpWrapper {
Insert(String, u64), Insert(String, u64),
Delete(u64, u64), Delete(u64, u64),
Cancel(u64, u64), Cancel(u64, u64),
Replace(String),
} }
struct AsyncFactoryWorker { struct AsyncFactoryWorker {
@ -176,6 +196,7 @@ impl AsyncFactoryWorker {
OpWrapper::Insert(txt, pos) => Ok(self.factory.insert(&txt, pos)?), OpWrapper::Insert(txt, pos) => Ok(self.factory.insert(&txt, pos)?),
OpWrapper::Delete(pos, count) => Ok(self.factory.delete(pos, count)?), OpWrapper::Delete(pos, count) => Ok(self.factory.delete(pos, count)?),
OpWrapper::Cancel(pos, count) => Ok(self.factory.cancel(pos, count)?), OpWrapper::Cancel(pos, count) => Ok(self.factory.cancel(pos, count)?),
OpWrapper::Replace(txt) => Ok(self.factory.replace(&txt)?),
} }
} }
} }