mirror of
https://github.com/hexedtech/codemp.git
synced 2024-11-22 07:14:50 +01:00
Merge pull request #39 from hexedtech/feat/buffer-ack
Feat: Buffer ack-ing
This commit is contained in:
commit
fb35ddb3cd
36 changed files with 574 additions and 425 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -231,7 +231,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codemp"
|
name = "codemp"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"codemp-proto",
|
"codemp-proto",
|
||||||
|
|
|
@ -41,7 +41,7 @@ tracing-subscriber = { version = "0.3", optional = true }
|
||||||
# glue (java)
|
# glue (java)
|
||||||
lazy_static = { version = "1.5", optional = true }
|
lazy_static = { version = "1.5", optional = true }
|
||||||
jni = { version = "0.21", features = ["invocation"], optional = true }
|
jni = { version = "0.21", features = ["invocation"], optional = true }
|
||||||
jni-toolbox = { version = "0.2.0", optional = true, features = ["uuid"] }
|
jni-toolbox = { version = "0.2", optional = true, features = ["uuid"] }
|
||||||
|
|
||||||
# glue (lua)
|
# glue (lua)
|
||||||
mlua-codemp-patch = { version = "0.10.0-beta.2", features = ["module", "send", "serialize"], optional = true }
|
mlua-codemp-patch = { version = "0.10.0-beta.2", features = ["module", "send", "serialize"], optional = true }
|
||||||
|
|
26
dist/java/src/mp/code/BufferController.java
vendored
26
dist/java/src/mp/code/BufferController.java
vendored
|
@ -1,5 +1,6 @@
|
||||||
package mp.code;
|
package mp.code;
|
||||||
|
|
||||||
|
import mp.code.data.BufferUpdate;
|
||||||
import mp.code.data.TextChange;
|
import mp.code.data.TextChange;
|
||||||
import mp.code.exceptions.ControllerException;
|
import mp.code.exceptions.ControllerException;
|
||||||
|
|
||||||
|
@ -42,26 +43,26 @@ public final class BufferController {
|
||||||
return get_content(this.ptr);
|
return get_content(this.ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static native TextChange try_recv(long self) throws ControllerException;
|
private static native BufferUpdate try_recv(long self) throws ControllerException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to get a {@link TextChange} from the queue if any were present, and returns
|
* Tries to get a {@link BufferUpdate} from the queue if any were present, and returns
|
||||||
* an empty optional otherwise.
|
* an empty optional otherwise.
|
||||||
* @return the first text change in queue, if any are present
|
* @return the first text change in queue, if any are present
|
||||||
* @throws ControllerException if the controller was stopped
|
* @throws ControllerException if the controller was stopped
|
||||||
*/
|
*/
|
||||||
public Optional<TextChange> tryRecv() throws ControllerException {
|
public Optional<BufferUpdate> tryRecv() throws ControllerException {
|
||||||
return Optional.ofNullable(try_recv(this.ptr));
|
return Optional.ofNullable(try_recv(this.ptr));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static native TextChange recv(long self) throws ControllerException;
|
private static native BufferUpdate recv(long self) throws ControllerException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blocks until a {@link TextChange} is available and returns it.
|
* Blocks until a {@link BufferUpdate} is available and returns it.
|
||||||
* @return the text change update that occurred
|
* @return the text change update that occurred
|
||||||
* @throws ControllerException if the controller was stopped
|
* @throws ControllerException if the controller was stopped
|
||||||
*/
|
*/
|
||||||
public TextChange recv() throws ControllerException {
|
public BufferUpdate recv() throws ControllerException {
|
||||||
return recv(this.ptr);
|
return recv(this.ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +79,7 @@ public final class BufferController {
|
||||||
private static native void callback(long self, Consumer<BufferController> cb);
|
private static native void callback(long self, Consumer<BufferController> cb);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a callback to be invoked whenever a {@link TextChange} occurs.
|
* Registers a callback to be invoked whenever a {@link BufferUpdate} occurs.
|
||||||
* This will not work unless a Java thread has been dedicated to the event loop.
|
* This will not work unless a Java thread has been dedicated to the event loop.
|
||||||
* @see Extensions#drive(boolean)
|
* @see Extensions#drive(boolean)
|
||||||
*/
|
*/
|
||||||
|
@ -106,6 +107,17 @@ public final class BufferController {
|
||||||
poll(this.ptr);
|
poll(this.ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static native void ack(long self, long[] version);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acknowledges that a certain CRDT version has been correctly applied.
|
||||||
|
* @param version the version to acknowledge
|
||||||
|
* @see BufferUpdate#version
|
||||||
|
*/
|
||||||
|
public void ack(long[] version) {
|
||||||
|
ack(this.ptr, version);
|
||||||
|
}
|
||||||
|
|
||||||
private static native void free(long self);
|
private static native void free(long self);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
|
|
7
dist/java/src/mp/code/CursorController.java
vendored
7
dist/java/src/mp/code/CursorController.java
vendored
|
@ -1,6 +1,7 @@
|
||||||
package mp.code;
|
package mp.code;
|
||||||
|
|
||||||
import mp.code.data.Cursor;
|
import mp.code.data.Cursor;
|
||||||
|
import mp.code.data.Selection;
|
||||||
import mp.code.exceptions.ControllerException;
|
import mp.code.exceptions.ControllerException;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -42,13 +43,13 @@ public final class CursorController {
|
||||||
return recv(this.ptr);
|
return recv(this.ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static native void send(long self, Cursor cursor) throws ControllerException;
|
private static native void send(long self, Selection cursor) throws ControllerException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to send a {@link Cursor} update.
|
* Tries to send a {@link Selection} update.
|
||||||
* @throws ControllerException if the controller was stopped
|
* @throws ControllerException if the controller was stopped
|
||||||
*/
|
*/
|
||||||
public void send(Cursor cursor) throws ControllerException {
|
public void send(Selection cursor) throws ControllerException {
|
||||||
send(this.ptr, cursor);
|
send(this.ptr, cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
35
dist/java/src/mp/code/data/BufferUpdate.java
vendored
Normal file
35
dist/java/src/mp/code/data/BufferUpdate.java
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package mp.code.data;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
import mp.code.Extensions;
|
||||||
|
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data class holding information about a buffer update.
|
||||||
|
*/
|
||||||
|
@ToString
|
||||||
|
@EqualsAndHashCode
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||||
|
public class BufferUpdate {
|
||||||
|
/**
|
||||||
|
* The hash of the content after applying it (calculated with {@link Extensions#hash(String)}).
|
||||||
|
* It is generally meaningless to send, but when received it is an invitation to check the hash
|
||||||
|
* and forcefully re-sync if necessary.
|
||||||
|
*/
|
||||||
|
public final OptionalLong hash; // xxh3 hash
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The CRDT version after the associated change has been applied.
|
||||||
|
* You MUST acknowledge that it was applied with {@link mp.code.BufferController#ack(long[])}.
|
||||||
|
*/
|
||||||
|
public final long[] version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link TextChange} contained in this buffer update.
|
||||||
|
*/
|
||||||
|
public final TextChange change;
|
||||||
|
}
|
34
dist/java/src/mp/code/data/Cursor.java
vendored
34
dist/java/src/mp/code/data/Cursor.java
vendored
|
@ -11,37 +11,13 @@ import lombok.ToString;
|
||||||
@EqualsAndHashCode
|
@EqualsAndHashCode
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class Cursor {
|
public class Cursor {
|
||||||
/**
|
|
||||||
* The starting row of the cursor position.
|
|
||||||
* If negative, it is clamped to 0.
|
|
||||||
*/
|
|
||||||
public final int startRow;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The starting column of the cursor position.
|
|
||||||
* If negative, it is clamped to 0.
|
|
||||||
*/
|
|
||||||
public final int startCol;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ending row of the cursor position.
|
|
||||||
* If negative, it is clamped to 0.
|
|
||||||
*/
|
|
||||||
public final int endRow;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ending column of the cursor position.
|
|
||||||
* If negative, it is clamped to 0.
|
|
||||||
*/
|
|
||||||
public final int endCol;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The buffer the cursor is located on.
|
|
||||||
*/
|
|
||||||
public final String buffer;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user who controls the cursor.
|
* The user who controls the cursor.
|
||||||
*/
|
*/
|
||||||
public final String user;
|
public final String user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The associated selection update.
|
||||||
|
*/
|
||||||
|
public final Selection selection;
|
||||||
}
|
}
|
||||||
|
|
42
dist/java/src/mp/code/data/Selection.java
vendored
Normal file
42
dist/java/src/mp/code/data/Selection.java
vendored
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package mp.code.data;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data class holding information about a cursor selection.
|
||||||
|
*/
|
||||||
|
@ToString
|
||||||
|
@EqualsAndHashCode
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class Selection {
|
||||||
|
/**
|
||||||
|
* The starting row of the cursor position.
|
||||||
|
* If negative, it is clamped to 0.
|
||||||
|
*/
|
||||||
|
public final int startRow;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The starting column of the cursor position.
|
||||||
|
* If negative, it is clamped to 0.
|
||||||
|
*/
|
||||||
|
public final int startCol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ending row of the cursor position.
|
||||||
|
* If negative, it is clamped to 0.
|
||||||
|
*/
|
||||||
|
public final int endRow;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ending column of the cursor position.
|
||||||
|
* If negative, it is clamped to 0.
|
||||||
|
*/
|
||||||
|
public final int endCol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The buffer the cursor is located on.
|
||||||
|
*/
|
||||||
|
public final String buffer;
|
||||||
|
}
|
9
dist/java/src/mp/code/data/TextChange.java
vendored
9
dist/java/src/mp/code/data/TextChange.java
vendored
|
@ -22,7 +22,7 @@ public class TextChange {
|
||||||
public final long start;
|
public final long start;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The endomg position of the change.
|
* The ending position of the change.
|
||||||
* If negative, it is clamped to 0.
|
* If negative, it is clamped to 0.
|
||||||
*/
|
*/
|
||||||
public final long end;
|
public final long end;
|
||||||
|
@ -33,13 +33,6 @@ public class TextChange {
|
||||||
*/
|
*/
|
||||||
public final String content;
|
public final String content;
|
||||||
|
|
||||||
/**
|
|
||||||
* The hash of the content after applying it (calculated with {@link Extensions#hash(String)}).
|
|
||||||
* It is generally meaningless to send, but when received it is an invitation to check the hash
|
|
||||||
* and forcefully re-sync if necessary.
|
|
||||||
*/
|
|
||||||
public final OptionalLong hash; // xxh3 hash
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the change represents a deletion.
|
* Checks if the change represents a deletion.
|
||||||
* It does if the starting index is lower than the ending index.
|
* It does if the starting index is lower than the ending index.
|
||||||
|
|
60
dist/lua/annotations.lua
vendored
60
dist/lua/annotations.lua
vendored
|
@ -135,28 +135,28 @@ function MaybeCursorPromise:cancel() end
|
||||||
function MaybeCursorPromise:and_then(cb) end
|
function MaybeCursorPromise:and_then(cb) end
|
||||||
|
|
||||||
|
|
||||||
---@class (exact) TextChangePromise : Promise
|
---@class (exact) BufferUpdatePromise : Promise
|
||||||
local TextChangePromise = {}
|
local BufferUpdatePromise = {}
|
||||||
--- block until promise is ready and return value
|
--- block until promise is ready and return value
|
||||||
--- @return TextChange
|
--- @return BufferUpdate
|
||||||
function TextChangePromise:await() end
|
function BufferUpdatePromise:await() end
|
||||||
--- cancel promise execution
|
--- cancel promise execution
|
||||||
function TextChangePromise:cancel() end
|
function BufferUpdatePromise:cancel() end
|
||||||
---@param cb fun(x: TextChange) callback to invoke
|
---@param cb fun(x: BufferUpdate) callback to invoke
|
||||||
---invoke callback asynchronously as soon as promise is ready
|
---invoke callback asynchronously as soon as promise is ready
|
||||||
function TextChangePromise:and_then(cb) end
|
function BufferUpdatePromise:and_then(cb) end
|
||||||
|
|
||||||
|
|
||||||
---@class (exact) MaybeTextChangePromise : Promise
|
---@class (exact) MaybeBufferUpdatePromise : Promise
|
||||||
local MaybeTextChangePromise = {}
|
local MaybeBufferUpdatePromise = {}
|
||||||
--- block until promise is ready and return value
|
--- block until promise is ready and return value
|
||||||
--- @return TextChange | nil
|
--- @return BufferUpdate | nil
|
||||||
function MaybeTextChangePromise:await() end
|
function MaybeBufferUpdatePromise:await() end
|
||||||
--- cancel promise execution
|
--- cancel promise execution
|
||||||
function MaybeTextChangePromise:cancel() end
|
function MaybeBufferUpdatePromise:cancel() end
|
||||||
---@param cb fun(x: TextChange | nil) callback to invoke
|
---@param cb fun(x: BufferUpdate | nil) callback to invoke
|
||||||
---invoke callback asynchronously as soon as promise is ready
|
---invoke callback asynchronously as soon as promise is ready
|
||||||
function MaybeTextChangePromise:and_then(cb) end
|
function MaybeBufferUpdatePromise:and_then(cb) end
|
||||||
|
|
||||||
-- [[ END ASYNC STUFF ]]
|
-- [[ END ASYNC STUFF ]]
|
||||||
|
|
||||||
|
@ -322,9 +322,14 @@ local BufferController = {}
|
||||||
---@field content string text content of change
|
---@field content string text content of change
|
||||||
---@field start integer start index of change
|
---@field start integer start index of change
|
||||||
---@field finish integer end index of change
|
---@field finish integer end index of change
|
||||||
---@field hash integer? optional hash of text buffer after this change, for sync checks
|
|
||||||
local TextChange = {}
|
local TextChange = {}
|
||||||
|
|
||||||
|
---@class (exact) BufferUpdate
|
||||||
|
---@field change TextChange text change for this delta
|
||||||
|
---@field version table<integer> CRDT version after this change
|
||||||
|
---@field hash integer? optional hash of text buffer after this change, for sync checks
|
||||||
|
local BufferUpdate = {}
|
||||||
|
|
||||||
---@param other string text to apply change to
|
---@param other string text to apply change to
|
||||||
---apply this text change to a string, returning the result
|
---apply this text change to a string, returning the result
|
||||||
function TextChange:apply(other) end
|
function TextChange:apply(other) end
|
||||||
|
@ -336,13 +341,13 @@ function TextChange:apply(other) end
|
||||||
---update buffer with a text change; note that to delete content should be empty but not span, while to insert span should be empty but not content (can insert and delete at the same time)
|
---update buffer with a text change; note that to delete content should be empty but not span, while to insert span should be empty but not content (can insert and delete at the same time)
|
||||||
function BufferController:send(change) end
|
function BufferController:send(change) end
|
||||||
|
|
||||||
---@return MaybeTextChangePromise
|
---@return MaybeBufferUpdatePromise
|
||||||
---@async
|
---@async
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---try to receive text changes, returning nil if none is available
|
---try to receive text changes, returning nil if none is available
|
||||||
function BufferController:try_recv() end
|
function BufferController:try_recv() end
|
||||||
|
|
||||||
---@return TextChangePromise
|
---@return BufferUpdatePromise
|
||||||
---@async
|
---@async
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---block until next text change and return it
|
---block until next text change and return it
|
||||||
|
@ -367,6 +372,10 @@ function BufferController:callback(cb) end
|
||||||
---get current content of buffer controller, marking all pending changes as seen
|
---get current content of buffer controller, marking all pending changes as seen
|
||||||
function BufferController:content() end
|
function BufferController:content() end
|
||||||
|
|
||||||
|
---@param version [integer] version to ack
|
||||||
|
---notify controller that this version's change has been correctly applied
|
||||||
|
function BufferController:ack(version) end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -374,14 +383,19 @@ function BufferController:content() end
|
||||||
---handle to a workspace's cursor channel, allowing send/recv operations
|
---handle to a workspace's cursor channel, allowing send/recv operations
|
||||||
local CursorController = {}
|
local CursorController = {}
|
||||||
|
|
||||||
---@class Cursor
|
---@class Selection
|
||||||
---@field user string? id of user owning this cursor
|
|
||||||
---@field buffer string relative path ("name") of buffer on which this cursor is
|
---@field buffer string relative path ("name") of buffer on which this cursor is
|
||||||
---@field start [integer, integer] cursor start position
|
---@field start_row integer
|
||||||
---@field finish [integer, integer] cursor end position
|
---@field start_col integer
|
||||||
---a cursor position
|
---@field end_row integer
|
||||||
|
---@field end_col integer
|
||||||
|
---a cursor selected region, as row-col indices
|
||||||
|
|
||||||
---@param cursor Cursor cursor event to broadcast
|
---@class Cursor
|
||||||
|
---@field user string id of user owning this cursor
|
||||||
|
---@field sel Selection selected region for this user
|
||||||
|
|
||||||
|
---@param cursor Selection cursor position to broadcast
|
||||||
---@return NilPromise
|
---@return NilPromise
|
||||||
---@async
|
---@async
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
|
|
19
dist/py/src/codemp/codemp.pyi
vendored
19
dist/py/src/codemp/codemp.pyi
vendored
|
@ -92,6 +92,14 @@ class TextChange:
|
||||||
def is_empty(self) -> bool: ...
|
def is_empty(self) -> bool: ...
|
||||||
def apply(self, txt: str) -> str: ...
|
def apply(self, txt: str) -> str: ...
|
||||||
|
|
||||||
|
class BufferUpdate:
|
||||||
|
"""
|
||||||
|
A single editor delta event, wrapping a TextChange and the new version
|
||||||
|
"""
|
||||||
|
change: TextChange
|
||||||
|
hash: Optional[int]
|
||||||
|
version: list[int]
|
||||||
|
|
||||||
|
|
||||||
class BufferController:
|
class BufferController:
|
||||||
"""
|
"""
|
||||||
|
@ -100,6 +108,7 @@ class BufferController:
|
||||||
"""
|
"""
|
||||||
def path(self) -> str: ...
|
def path(self) -> str: ...
|
||||||
def content(self) -> Promise[str]: ...
|
def content(self) -> Promise[str]: ...
|
||||||
|
def ack(self, v: list[int]) -> None: ...
|
||||||
def send(self,
|
def send(self,
|
||||||
start: int,
|
start: int,
|
||||||
end: int,
|
end: int,
|
||||||
|
@ -113,14 +122,20 @@ class BufferController:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Cursor:
|
class Selection:
|
||||||
"""
|
"""
|
||||||
An Editor agnostic cursor position representation
|
An Editor agnostic cursor position representation
|
||||||
"""
|
"""
|
||||||
start: Tuple[int, int]
|
start: Tuple[int, int]
|
||||||
end: Tuple[int, int]
|
end: Tuple[int, int]
|
||||||
buffer: str
|
buffer: str
|
||||||
user: Optional[str] # can be an empty string
|
|
||||||
|
class Cursor:
|
||||||
|
"""
|
||||||
|
A remote cursor event
|
||||||
|
"""
|
||||||
|
user: str
|
||||||
|
sel: Selection
|
||||||
|
|
||||||
|
|
||||||
class CursorController:
|
class CursorController:
|
||||||
|
|
|
@ -1,6 +1,25 @@
|
||||||
//! # TextChange
|
//! # TextChange
|
||||||
//! A high-level representation of a change within a given buffer.
|
//! A high-level representation of a change within a given buffer.
|
||||||
|
|
||||||
|
/// A [`TextChange`] event happening on a buffer.
|
||||||
|
///
|
||||||
|
/// Contains the change itself, the new version after this change and an optional `hash` field.
|
||||||
|
/// This is used for error correction: if provided, it should match the hash of the buffer
|
||||||
|
/// content **after** applying this change. Note that the `hash` field will not necessarily
|
||||||
|
/// be provided every time.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
#[cfg_attr(feature = "js", napi_derive::napi(object))]
|
||||||
|
#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyo3::pyclass(get_all))]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct BufferUpdate {
|
||||||
|
/// Optional content hash after applying this change.
|
||||||
|
pub hash: Option<i64>,
|
||||||
|
/// CRDT version after this change has been applied.
|
||||||
|
pub version: Vec<i64>,
|
||||||
|
/// The change that has occurred.
|
||||||
|
pub change: TextChange,
|
||||||
|
}
|
||||||
|
|
||||||
/// An editor-friendly representation of a text change in a given buffer.
|
/// An editor-friendly representation of a text change in a given buffer.
|
||||||
///
|
///
|
||||||
/// It's expressed with a range of characters and a string of content that should replace them,
|
/// It's expressed with a range of characters and a string of content that should replace them,
|
||||||
|
@ -9,21 +28,22 @@
|
||||||
/// Bulky and large operations will result in a single [`TextChange`] effectively sending the whole
|
/// Bulky and large operations will result in a single [`TextChange`] effectively sending the whole
|
||||||
/// new buffer, but smaller changes are efficient and easy to create or apply.
|
/// new buffer, but smaller changes are efficient and easy to create or apply.
|
||||||
///
|
///
|
||||||
/// [`TextChange`] contains an optional `hash` field. This is used for error correction: if
|
|
||||||
/// provided, it should match the hash of the buffer content **after** applying this change.
|
|
||||||
/// Note that the `hash` field will not necessarily be provided every time.
|
|
||||||
///
|
|
||||||
/// ### Examples
|
/// ### Examples
|
||||||
/// To insert 'a' after 4th character we should send a.
|
/// To insert 'a' after 4th character we should send:
|
||||||
/// `TextChange { start: 4, end: 4, content: "a".into(), hash: None }`
|
/// ```
|
||||||
|
/// codemp::api::TextChange { start: 4, end: 4, content: "a".into() };
|
||||||
|
/// ```
|
||||||
///
|
///
|
||||||
/// To delete a the fourth character we should send a.
|
/// To delete the fourth character we should send:
|
||||||
/// `TextChange { start: 3, end: 4, content: "".into(), hash: None }`
|
/// ```
|
||||||
|
/// codemp::api::TextChange { start: 3, end: 4, content: "".into() };
|
||||||
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```
|
||||||
/// let change = codemp::api::TextChange {
|
/// let change = codemp::api::TextChange {
|
||||||
/// start: 6, end: 11,
|
/// start: 6,
|
||||||
/// content: "mom".to_string(), hash: None
|
/// end: 11,
|
||||||
|
/// content: "mom".to_string()
|
||||||
/// };
|
/// };
|
||||||
/// let before = "hello world!";
|
/// let before = "hello world!";
|
||||||
/// let after = change.apply(before);
|
/// let after = change.apply(before);
|
||||||
|
@ -41,8 +61,6 @@ pub struct TextChange {
|
||||||
pub end: u32,
|
pub end: u32,
|
||||||
/// New content of text inside span.
|
/// New content of text inside span.
|
||||||
pub content: String,
|
pub content: String,
|
||||||
/// Optional content hash after applying this change.
|
|
||||||
pub hash: Option<i64>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextChange {
|
impl TextChange {
|
||||||
|
@ -90,7 +108,6 @@ mod tests {
|
||||||
start: 5,
|
start: 5,
|
||||||
end: 5,
|
end: 5,
|
||||||
content: " cruel".to_string(),
|
content: " cruel".to_string(),
|
||||||
hash: None,
|
|
||||||
};
|
};
|
||||||
let result = change.apply("hello world!");
|
let result = change.apply("hello world!");
|
||||||
assert_eq!(result, "hello cruel world!");
|
assert_eq!(result, "hello cruel world!");
|
||||||
|
@ -102,7 +119,6 @@ mod tests {
|
||||||
start: 5,
|
start: 5,
|
||||||
end: 11,
|
end: 11,
|
||||||
content: "".to_string(),
|
content: "".to_string(),
|
||||||
hash: None,
|
|
||||||
};
|
};
|
||||||
let result = change.apply("hello cruel world!");
|
let result = change.apply("hello cruel world!");
|
||||||
assert_eq!(result, "hello world!");
|
assert_eq!(result, "hello world!");
|
||||||
|
@ -114,7 +130,6 @@ mod tests {
|
||||||
start: 5,
|
start: 5,
|
||||||
end: 11,
|
end: 11,
|
||||||
content: " not very pleasant".to_string(),
|
content: " not very pleasant".to_string(),
|
||||||
hash: None,
|
|
||||||
};
|
};
|
||||||
let result = change.apply("hello cruel world!");
|
let result = change.apply("hello cruel world!");
|
||||||
assert_eq!(result, "hello not very pleasant world!");
|
assert_eq!(result, "hello not very pleasant world!");
|
||||||
|
@ -126,7 +141,6 @@ mod tests {
|
||||||
start: 100,
|
start: 100,
|
||||||
end: 110,
|
end: 110,
|
||||||
content: "a very long string \n which totally matters".to_string(),
|
content: "a very long string \n which totally matters".to_string(),
|
||||||
hash: None,
|
|
||||||
};
|
};
|
||||||
let result = change.apply("a short text");
|
let result = change.apply("a short text");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -141,7 +155,6 @@ mod tests {
|
||||||
start: 42,
|
start: 42,
|
||||||
end: 42,
|
end: 42,
|
||||||
content: "".to_string(),
|
content: "".to_string(),
|
||||||
hash: None,
|
|
||||||
};
|
};
|
||||||
let result = change.apply("some important text");
|
let result = change.apply("some important text");
|
||||||
assert_eq!(result, "some important text");
|
assert_eq!(result, "some important text");
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
//! # Config
|
//! # Config
|
||||||
//! Data structure defining clients configuration
|
//! Data structure defining clients configuration
|
||||||
|
|
||||||
/// Configuration struct for `codemp` client
|
/// Configuration struct for the `codemp` client.
|
||||||
///
|
///
|
||||||
/// username and password are required fields, while everything else is optional
|
/// `username` and `password` are required fields, everything else is optional.
|
||||||
///
|
///
|
||||||
/// host, port and tls affect all connections to all grpc services
|
/// `host`, `port` and `tls` affect all connections to all gRPC services; the
|
||||||
/// resulting endpoint is composed like this:
|
/// resulting endpoint is composed like this:
|
||||||
/// http{tls?'s':''}://{host}:{port}
|
/// http{tls?'s':''}://{host}:{port}
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -16,20 +16,20 @@
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// user identifier used to register, possibly your email
|
/// User identifier used to register, possibly your email.
|
||||||
pub username: String,
|
pub username: String,
|
||||||
/// user password chosen upon registration
|
/// User password chosen upon registration.
|
||||||
pub password: String,
|
pub password: String,
|
||||||
/// address of server to connect to, default api.code.mp
|
/// Address of server to connect to, default api.code.mp.
|
||||||
pub host: Option<String>,
|
pub host: Option<String>,
|
||||||
/// port to connect to, default 50053
|
/// Port to connect to, default 50053.
|
||||||
pub port: Option<u16>,
|
pub port: Option<u16>,
|
||||||
/// enable or disable tls, default true
|
/// Enable or disable tls, default true.
|
||||||
pub tls: Option<bool>,
|
pub tls: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
/// construct a new Config object, with given username and password
|
/// Construct a new Config object, with given username and password.
|
||||||
pub fn new(username: impl ToString, password: impl ToString) -> Self {
|
pub fn new(username: impl ToString, password: impl ToString) -> Self {
|
||||||
Self {
|
Self {
|
||||||
username: username.to_string(),
|
username: username.to_string(),
|
||||||
|
|
|
@ -37,11 +37,9 @@ where
|
||||||
/// See [`Controller`]'s documentation for details.
|
/// See [`Controller`]'s documentation for details.
|
||||||
///
|
///
|
||||||
/// Details about the receiving end are left to the implementor.
|
/// Details about the receiving end are left to the implementor.
|
||||||
#[allow(async_fn_in_trait)]
|
|
||||||
#[cfg_attr(feature = "async-trait", async_trait::async_trait)]
|
|
||||||
pub trait AsyncSender<T: Sized + Send + Sync>: Sized + Send + Sync {
|
pub trait AsyncSender<T: Sized + Send + Sync>: Sized + Send + Sync {
|
||||||
/// Enqueue a new value to be sent to all other users.
|
/// Enqueue a new value to be sent to all other users without blocking
|
||||||
async fn send(&self, x: T) -> ControllerResult<()>;
|
fn send(&self, x: T) -> ControllerResult<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asynchronous and thread-safe handle to receive data from a stream.
|
/// Asynchronous and thread-safe handle to receive data from a stream.
|
||||||
|
|
|
@ -4,19 +4,34 @@
|
||||||
#[cfg(any(feature = "py", feature = "py-noabi"))]
|
#[cfg(any(feature = "py", feature = "py-noabi"))]
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
/// User cursor position in a buffer
|
/// An event that occurred about a user's cursor.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
|
#[cfg_attr(feature = "js", napi_derive::napi(object))]
|
||||||
#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyclass)]
|
#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyclass)]
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
// #[cfg_attr(feature = "py", pyo3(crate = "reexported::pyo3"))]
|
// #[cfg_attr(feature = "py", pyo3(crate = "reexported::pyo3"))]
|
||||||
pub struct Cursor {
|
pub struct Cursor {
|
||||||
/// Cursor start position in buffer, as 0-indexed row-column tuple.
|
/// User who sent the cursor.
|
||||||
pub start: (i32, i32),
|
pub user: String,
|
||||||
/// Cursor end position in buffer, as 0-indexed row-column tuple.
|
/// The updated cursor selection.
|
||||||
#[cfg_attr(feature = "serialize", serde(alias = "finish"))] // Lua uses `end` as keyword
|
pub sel: Selection,
|
||||||
pub end: (i32, i32),
|
}
|
||||||
|
|
||||||
|
/// A cursor selection span.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
#[cfg_attr(feature = "js", napi_derive::napi(object))]
|
||||||
|
#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyclass)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
// #[cfg_attr(feature = "py", pyo3(crate = "reexported::pyo3"))]
|
||||||
|
pub struct Selection {
|
||||||
|
/// Cursor position starting row in buffer.
|
||||||
|
pub start_row: i32,
|
||||||
|
/// Cursor position starting column in buffer.
|
||||||
|
pub start_col: i32,
|
||||||
|
/// Cursor position final row in buffer.
|
||||||
|
pub end_row: i32,
|
||||||
|
/// Cursor position final column in buffer.
|
||||||
|
pub end_col: i32,
|
||||||
/// Path of buffer this cursor is on.
|
/// Path of buffer this cursor is on.
|
||||||
pub buffer: String,
|
pub buffer: String,
|
||||||
/// User display name, if provided.
|
|
||||||
pub user: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,11 @@ pub mod event;
|
||||||
/// data structure for remote users
|
/// data structure for remote users
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
|
pub use change::BufferUpdate;
|
||||||
pub use change::TextChange;
|
pub use change::TextChange;
|
||||||
pub use config::Config;
|
pub use config::Config;
|
||||||
pub use controller::Controller;
|
pub use controller::Controller;
|
||||||
pub use cursor::Cursor;
|
pub use cursor::Cursor;
|
||||||
|
pub use cursor::Selection;
|
||||||
pub use event::Event;
|
pub use event::Event;
|
||||||
pub use user::User;
|
pub use user::User;
|
||||||
|
|
|
@ -7,11 +7,10 @@ use diamond_types::LocalVersion;
|
||||||
use tokio::sync::{mpsc, oneshot, watch};
|
use tokio::sync::{mpsc, oneshot, watch};
|
||||||
|
|
||||||
use crate::api::controller::{AsyncReceiver, AsyncSender, Controller, ControllerCallback};
|
use crate::api::controller::{AsyncReceiver, AsyncSender, Controller, ControllerCallback};
|
||||||
|
use crate::api::BufferUpdate;
|
||||||
use crate::api::TextChange;
|
use crate::api::TextChange;
|
||||||
use crate::errors::ControllerResult;
|
use crate::errors::ControllerResult;
|
||||||
use crate::ext::InternallyMutable;
|
use crate::ext::IgnorableError;
|
||||||
|
|
||||||
use super::worker::DeltaRequest;
|
|
||||||
|
|
||||||
/// A [Controller] to asynchronously interact with remote buffers.
|
/// A [Controller] to asynchronously interact with remote buffers.
|
||||||
///
|
///
|
||||||
|
@ -23,53 +22,59 @@ use super::worker::DeltaRequest;
|
||||||
pub struct BufferController(pub(crate) Arc<BufferControllerInner>);
|
pub struct BufferController(pub(crate) Arc<BufferControllerInner>);
|
||||||
|
|
||||||
impl BufferController {
|
impl BufferController {
|
||||||
/// Get the buffer path
|
/// Get the buffer path.
|
||||||
pub fn path(&self) -> &str {
|
pub fn path(&self) -> &str {
|
||||||
&self.0.name
|
&self.0.name
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return buffer whole content, updating internal acknowledgement tracker
|
/// Return buffer whole content, updating internal acknowledgement tracker.
|
||||||
pub async fn content(&self) -> ControllerResult<String> {
|
pub async fn content(&self) -> ControllerResult<String> {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
self.0.content_request.send(tx).await?;
|
self.0.content_request.send(tx).await?;
|
||||||
let content = rx.await?;
|
let content = rx.await?;
|
||||||
self.0
|
|
||||||
.last_update
|
|
||||||
.set(self.0.latest_version.borrow().clone());
|
|
||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Notify CRDT that changes up to the given version have been merged succesfully.
|
||||||
|
pub fn ack(&self, version: Vec<i64>) {
|
||||||
|
let version = version
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| usize::from_ne_bytes(x.to_ne_bytes()))
|
||||||
|
.collect();
|
||||||
|
self.0
|
||||||
|
.ack_tx
|
||||||
|
.send(version)
|
||||||
|
.unwrap_or_warn("no worker to receive sent ack");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct BufferControllerInner {
|
pub(crate) struct BufferControllerInner {
|
||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
pub(crate) latest_version: watch::Receiver<diamond_types::LocalVersion>,
|
pub(crate) latest_version: watch::Receiver<diamond_types::LocalVersion>,
|
||||||
pub(crate) last_update: InternallyMutable<diamond_types::LocalVersion>,
|
pub(crate) local_version: watch::Receiver<diamond_types::LocalVersion>,
|
||||||
pub(crate) ops_in: mpsc::UnboundedSender<(TextChange, oneshot::Sender<LocalVersion>)>,
|
pub(crate) ops_in: mpsc::UnboundedSender<TextChange>,
|
||||||
pub(crate) poller: mpsc::UnboundedSender<oneshot::Sender<()>>,
|
pub(crate) poller: mpsc::UnboundedSender<oneshot::Sender<()>>,
|
||||||
pub(crate) content_request: mpsc::Sender<oneshot::Sender<String>>,
|
pub(crate) content_request: mpsc::Sender<oneshot::Sender<String>>,
|
||||||
pub(crate) delta_request: mpsc::Sender<DeltaRequest>,
|
pub(crate) delta_request: mpsc::Sender<(LocalVersion, oneshot::Sender<Option<BufferUpdate>>)>,
|
||||||
pub(crate) callback: watch::Sender<Option<ControllerCallback<BufferController>>>,
|
pub(crate) callback: watch::Sender<Option<ControllerCallback<BufferController>>>,
|
||||||
|
pub(crate) ack_tx: mpsc::UnboundedSender<LocalVersion>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "async-trait", async_trait::async_trait)]
|
#[cfg_attr(feature = "async-trait", async_trait::async_trait)]
|
||||||
impl Controller<TextChange> for BufferController {}
|
impl Controller<TextChange, BufferUpdate> for BufferController {}
|
||||||
|
|
||||||
#[cfg_attr(feature = "async-trait", async_trait::async_trait)]
|
|
||||||
impl AsyncSender<TextChange> for BufferController {
|
impl AsyncSender<TextChange> for BufferController {
|
||||||
async fn send(&self, op: TextChange) -> ControllerResult<()> {
|
fn send(&self, op: TextChange) -> ControllerResult<()> {
|
||||||
// we let the worker do the updating to the last version and send it back.
|
self.0.ops_in.send(op)?;
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
self.0.ops_in.send((op, tx))?;
|
|
||||||
self.0.last_update.set(rx.await?);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "async-trait", async_trait::async_trait)]
|
#[cfg_attr(feature = "async-trait", async_trait::async_trait)]
|
||||||
impl AsyncReceiver<TextChange> for BufferController {
|
impl AsyncReceiver<BufferUpdate> for BufferController {
|
||||||
async fn poll(&self) -> ControllerResult<()> {
|
async fn poll(&self) -> ControllerResult<()> {
|
||||||
if self.0.last_update.get() != *self.0.latest_version.borrow() {
|
if *self.0.local_version.borrow() != *self.0.latest_version.borrow() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,8 +84,8 @@ impl AsyncReceiver<TextChange> for BufferController {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn try_recv(&self) -> ControllerResult<Option<TextChange>> {
|
async fn try_recv(&self) -> ControllerResult<Option<BufferUpdate>> {
|
||||||
let last_update = self.0.last_update.get();
|
let last_update = self.0.local_version.borrow().clone();
|
||||||
let latest_version = self.0.latest_version.borrow().clone();
|
let latest_version = self.0.latest_version.borrow().clone();
|
||||||
|
|
||||||
if last_update == latest_version {
|
if last_update == latest_version {
|
||||||
|
@ -89,16 +94,11 @@ impl AsyncReceiver<TextChange> for BufferController {
|
||||||
|
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
self.0.delta_request.send((last_update, tx)).await?;
|
self.0.delta_request.send((last_update, tx)).await?;
|
||||||
let (v, change) = rx.await?;
|
Ok(rx.await?)
|
||||||
self.0.last_update.set(v);
|
|
||||||
Ok(change)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn callback(&self, cb: impl Into<ControllerCallback<BufferController>>) {
|
fn callback(&self, cb: impl Into<ControllerCallback<BufferController>>) {
|
||||||
if self.0.callback.send(Some(cb.into())).is_err() {
|
self.0.callback.send_replace(Some(cb.into()));
|
||||||
// TODO should we panic? we failed what we were supposed to do
|
|
||||||
tracing::error!("no active buffer worker to run registered callback!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_callback(&self) {
|
fn clear_callback(&self) {
|
||||||
|
|
|
@ -7,25 +7,25 @@ use tonic::Streaming;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::api::controller::ControllerCallback;
|
use crate::api::controller::ControllerCallback;
|
||||||
|
use crate::api::BufferUpdate;
|
||||||
use crate::api::TextChange;
|
use crate::api::TextChange;
|
||||||
use crate::ext::{IgnorableError, InternallyMutable};
|
use crate::ext::IgnorableError;
|
||||||
|
|
||||||
use codemp_proto::buffer::{BufferEvent, Operation};
|
use codemp_proto::buffer::{BufferEvent, Operation};
|
||||||
|
|
||||||
use super::controller::{BufferController, BufferControllerInner};
|
use super::controller::{BufferController, BufferControllerInner};
|
||||||
|
|
||||||
pub(crate) type DeltaOp = (LocalVersion, Option<TextChange>);
|
|
||||||
pub(crate) type DeltaRequest = (LocalVersion, oneshot::Sender<DeltaOp>);
|
|
||||||
|
|
||||||
struct BufferWorker {
|
struct BufferWorker {
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
path: String,
|
path: String,
|
||||||
latest_version: watch::Sender<diamond_types::LocalVersion>,
|
latest_version: watch::Sender<diamond_types::LocalVersion>,
|
||||||
ops_in: mpsc::UnboundedReceiver<(TextChange, oneshot::Sender<LocalVersion>)>,
|
local_version: watch::Sender<diamond_types::LocalVersion>,
|
||||||
|
ack_rx: mpsc::UnboundedReceiver<LocalVersion>,
|
||||||
|
ops_in: mpsc::UnboundedReceiver<TextChange>,
|
||||||
poller: mpsc::UnboundedReceiver<oneshot::Sender<()>>,
|
poller: mpsc::UnboundedReceiver<oneshot::Sender<()>>,
|
||||||
pollers: Vec<oneshot::Sender<()>>,
|
pollers: Vec<oneshot::Sender<()>>,
|
||||||
content_checkout: mpsc::Receiver<oneshot::Sender<String>>,
|
content_checkout: mpsc::Receiver<oneshot::Sender<String>>,
|
||||||
delta_req: mpsc::Receiver<DeltaRequest>,
|
delta_req: mpsc::Receiver<(LocalVersion, oneshot::Sender<Option<BufferUpdate>>)>,
|
||||||
controller: std::sync::Weak<BufferControllerInner>,
|
controller: std::sync::Weak<BufferControllerInner>,
|
||||||
callback: watch::Receiver<Option<ControllerCallback<BufferController>>>,
|
callback: watch::Receiver<Option<ControllerCallback<BufferController>>>,
|
||||||
oplog: OpLog,
|
oplog: OpLog,
|
||||||
|
@ -43,7 +43,9 @@ impl BufferController {
|
||||||
let init = diamond_types::LocalVersion::default();
|
let init = diamond_types::LocalVersion::default();
|
||||||
|
|
||||||
let (latest_version_tx, latest_version_rx) = watch::channel(init.clone());
|
let (latest_version_tx, latest_version_rx) = watch::channel(init.clone());
|
||||||
|
let (my_version_tx, my_version_rx) = watch::channel(init.clone());
|
||||||
let (opin_tx, opin_rx) = mpsc::unbounded_channel();
|
let (opin_tx, opin_rx) = mpsc::unbounded_channel();
|
||||||
|
let (ack_tx, ack_rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
let (req_tx, req_rx) = mpsc::channel(1);
|
let (req_tx, req_rx) = mpsc::channel(1);
|
||||||
let (recv_tx, recv_rx) = mpsc::channel(1);
|
let (recv_tx, recv_rx) = mpsc::channel(1);
|
||||||
|
@ -54,12 +56,13 @@ impl BufferController {
|
||||||
let controller = Arc::new(BufferControllerInner {
|
let controller = Arc::new(BufferControllerInner {
|
||||||
name: path.to_string(),
|
name: path.to_string(),
|
||||||
latest_version: latest_version_rx,
|
latest_version: latest_version_rx,
|
||||||
last_update: InternallyMutable::new(diamond_types::LocalVersion::default()),
|
local_version: my_version_rx,
|
||||||
ops_in: opin_tx,
|
ops_in: opin_tx,
|
||||||
poller: poller_tx,
|
poller: poller_tx,
|
||||||
content_request: req_tx,
|
content_request: req_tx,
|
||||||
delta_request: recv_tx,
|
delta_request: recv_tx,
|
||||||
callback: cb_tx,
|
callback: cb_tx,
|
||||||
|
ack_tx,
|
||||||
});
|
});
|
||||||
|
|
||||||
let weak = Arc::downgrade(&controller);
|
let weak = Arc::downgrade(&controller);
|
||||||
|
@ -68,6 +71,8 @@ impl BufferController {
|
||||||
user_id,
|
user_id,
|
||||||
path: path.to_string(),
|
path: path.to_string(),
|
||||||
latest_version: latest_version_tx,
|
latest_version: latest_version_tx,
|
||||||
|
local_version: my_version_tx,
|
||||||
|
ack_rx,
|
||||||
ops_in: opin_rx,
|
ops_in: opin_rx,
|
||||||
poller: poller_rx,
|
poller: poller_rx,
|
||||||
pollers: Vec::new(),
|
pollers: Vec::new(),
|
||||||
|
@ -106,10 +111,18 @@ impl BufferController {
|
||||||
Some(tx) => worker.pollers.push(tx),
|
Some(tx) => worker.pollers.push(tx),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// received new change ack, merge editor branch up to that version
|
||||||
|
res = worker.ack_rx.recv() => match res {
|
||||||
|
None => break tracing::error!("ack channel closed"),
|
||||||
|
Some(v) => {
|
||||||
|
worker.branch.merge(&worker.oplog, &v)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// received a text change from editor
|
// received a text change from editor
|
||||||
res = worker.ops_in.recv() => match res {
|
res = worker.ops_in.recv() => match res {
|
||||||
None => break tracing::debug!("stopping: editor closed channel"),
|
None => break tracing::debug!("stopping: editor closed channel"),
|
||||||
Some((change, ack)) => worker.handle_editor_change(change, ack, &tx).await,
|
Some(change) => worker.handle_editor_change(change, &tx).await,
|
||||||
},
|
},
|
||||||
|
|
||||||
// received a message from server: add to oplog and update latest version (+unlock pollers)
|
// received a message from server: add to oplog and update latest version (+unlock pollers)
|
||||||
|
@ -142,12 +155,7 @@ impl BufferController {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BufferWorker {
|
impl BufferWorker {
|
||||||
async fn handle_editor_change(
|
async fn handle_editor_change(&mut self, change: TextChange, tx: &mpsc::Sender<Operation>) {
|
||||||
&mut self,
|
|
||||||
change: TextChange,
|
|
||||||
ack: oneshot::Sender<LocalVersion>,
|
|
||||||
tx: &mpsc::Sender<Operation>,
|
|
||||||
) {
|
|
||||||
let agent_id = self.oplog.get_or_create_agent_id(&self.user_id.to_string());
|
let agent_id = self.oplog.get_or_create_agent_id(&self.user_id.to_string());
|
||||||
let last_ver = self.oplog.local_version();
|
let last_ver = self.oplog.local_version();
|
||||||
// clip to buffer extents
|
// clip to buffer extents
|
||||||
|
@ -174,9 +182,10 @@ impl BufferWorker {
|
||||||
self.latest_version
|
self.latest_version
|
||||||
.send(self.oplog.local_version())
|
.send(self.oplog.local_version())
|
||||||
.unwrap_or_warn("failed to update latest version!");
|
.unwrap_or_warn("failed to update latest version!");
|
||||||
|
self.local_version
|
||||||
|
.send(self.branch.local_version())
|
||||||
|
.unwrap_or_warn("failed to update local version!");
|
||||||
}
|
}
|
||||||
ack.send(self.branch.local_version())
|
|
||||||
.unwrap_or_warn("controller didn't wait for ack");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_server_change(&mut self, change: BufferEvent) -> bool {
|
async fn handle_server_change(&mut self, change: BufferEvent) -> bool {
|
||||||
|
@ -203,7 +212,11 @@ impl BufferWorker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_delta_request(&mut self, last_ver: LocalVersion, tx: oneshot::Sender<DeltaOp>) {
|
async fn handle_delta_request(
|
||||||
|
&mut self,
|
||||||
|
last_ver: LocalVersion,
|
||||||
|
tx: oneshot::Sender<Option<BufferUpdate>>,
|
||||||
|
) {
|
||||||
if let Some((lv, Some(dtop))) = self
|
if let Some((lv, Some(dtop))) = self
|
||||||
.oplog
|
.oplog
|
||||||
.iter_xf_operations_from(&last_ver, self.oplog.local_version_ref())
|
.iter_xf_operations_from(&last_ver, self.oplog.local_version_ref())
|
||||||
|
@ -228,25 +241,40 @@ impl BufferWorker {
|
||||||
{
|
{
|
||||||
tracing::error!("[?!?!] Insert span differs from effective content len (TODO remove this error after a bit)");
|
tracing::error!("[?!?!] Insert span differs from effective content len (TODO remove this error after a bit)");
|
||||||
}
|
}
|
||||||
crate::api::change::TextChange {
|
crate::api::BufferUpdate {
|
||||||
|
hash,
|
||||||
|
version: step_ver
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| i64::from_ne_bytes(x.to_ne_bytes()))
|
||||||
|
.collect(), // TODO this is wasteful
|
||||||
|
change: crate::api::TextChange {
|
||||||
start: dtop.start() as u32,
|
start: dtop.start() as u32,
|
||||||
end: dtop.start() as u32,
|
end: dtop.start() as u32,
|
||||||
content: dtop.content_as_str().unwrap_or_default().to_string(),
|
content: dtop.content_as_str().unwrap_or_default().to_string(),
|
||||||
hash,
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diamond_types::list::operation::OpKind::Del => crate::api::change::TextChange {
|
diamond_types::list::operation::OpKind::Del => crate::api::BufferUpdate {
|
||||||
|
hash,
|
||||||
|
version: step_ver
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| i64::from_ne_bytes(x.to_ne_bytes()))
|
||||||
|
.collect(), // TODO this is wasteful
|
||||||
|
change: crate::api::TextChange {
|
||||||
start: dtop.start() as u32,
|
start: dtop.start() as u32,
|
||||||
end: dtop.end() as u32,
|
end: dtop.end() as u32,
|
||||||
content: dtop.content_as_str().unwrap_or_default().to_string(),
|
content: dtop.content_as_str().unwrap_or_default().to_string(),
|
||||||
hash,
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
tx.send((new_local_v, Some(tc)))
|
self.local_version
|
||||||
|
.send(new_local_v)
|
||||||
|
.unwrap_or_warn("could not update local version");
|
||||||
|
tx.send(Some(tc))
|
||||||
.unwrap_or_warn("could not update ops channel -- is controller dead?");
|
.unwrap_or_warn("could not update ops channel -- is controller dead?");
|
||||||
} else {
|
} else {
|
||||||
tx.send((last_ver, None))
|
tx.send(None)
|
||||||
.unwrap_or_warn("could not update ops channel -- is controller dead?");
|
.unwrap_or_warn("could not update ops channel -- is controller dead?");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use tokio::sync::{mpsc, oneshot, watch};
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
controller::{AsyncReceiver, AsyncSender, ControllerCallback},
|
controller::{AsyncReceiver, AsyncSender, ControllerCallback},
|
||||||
Controller, Cursor,
|
Controller, Cursor, Selection,
|
||||||
},
|
},
|
||||||
errors::ControllerResult,
|
errors::ControllerResult,
|
||||||
};
|
};
|
||||||
|
@ -27,38 +27,38 @@ pub struct CursorController(pub(crate) Arc<CursorControllerInner>);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct CursorControllerInner {
|
pub(crate) struct CursorControllerInner {
|
||||||
pub(crate) op: mpsc::Sender<CursorPosition>,
|
pub(crate) op: mpsc::UnboundedSender<CursorPosition>,
|
||||||
pub(crate) stream: mpsc::Sender<oneshot::Sender<Option<Cursor>>>,
|
pub(crate) stream: mpsc::Sender<oneshot::Sender<Option<Cursor>>>,
|
||||||
pub(crate) poll: mpsc::UnboundedSender<oneshot::Sender<()>>,
|
pub(crate) poll: mpsc::UnboundedSender<oneshot::Sender<()>>,
|
||||||
pub(crate) callback: watch::Sender<Option<ControllerCallback<CursorController>>>,
|
pub(crate) callback: watch::Sender<Option<ControllerCallback<CursorController>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "async-trait", async_trait::async_trait)]
|
#[cfg_attr(feature = "async-trait", async_trait::async_trait)]
|
||||||
impl Controller<Cursor> for CursorController {}
|
impl Controller<Selection, Cursor> for CursorController {}
|
||||||
|
|
||||||
#[cfg_attr(feature = "async-trait", async_trait::async_trait)]
|
#[cfg_attr(feature = "async-trait", async_trait::async_trait)]
|
||||||
impl AsyncSender<Cursor> for CursorController {
|
impl AsyncSender<Selection> for CursorController {
|
||||||
async fn send(&self, mut cursor: Cursor) -> ControllerResult<()> {
|
fn send(&self, mut cursor: Selection) -> ControllerResult<()> {
|
||||||
if cursor.start > cursor.end {
|
if cursor.start_row > cursor.end_row
|
||||||
std::mem::swap(&mut cursor.start, &mut cursor.end);
|
|| (cursor.start_row == cursor.end_row && cursor.start_col > cursor.end_col)
|
||||||
|
{
|
||||||
|
std::mem::swap(&mut cursor.start_row, &mut cursor.end_row);
|
||||||
|
std::mem::swap(&mut cursor.start_col, &mut cursor.end_col);
|
||||||
}
|
}
|
||||||
Ok(self
|
|
||||||
.0
|
Ok(self.0.op.send(CursorPosition {
|
||||||
.op
|
|
||||||
.send(CursorPosition {
|
|
||||||
buffer: BufferNode {
|
buffer: BufferNode {
|
||||||
path: cursor.buffer,
|
path: cursor.buffer,
|
||||||
},
|
},
|
||||||
start: RowCol {
|
start: RowCol {
|
||||||
row: cursor.start.0,
|
row: cursor.start_row,
|
||||||
col: cursor.start.1,
|
col: cursor.start_col,
|
||||||
},
|
},
|
||||||
end: RowCol {
|
end: RowCol {
|
||||||
row: cursor.end.0,
|
row: cursor.end_row,
|
||||||
col: cursor.end.1,
|
col: cursor.end_col,
|
||||||
},
|
},
|
||||||
})
|
})?)
|
||||||
.await?)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ use tonic::Streaming;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{controller::ControllerCallback, Cursor, User},
|
api::{controller::ControllerCallback, Cursor, Selection, User},
|
||||||
ext::IgnorableError,
|
ext::IgnorableError,
|
||||||
};
|
};
|
||||||
use codemp_proto::cursor::{CursorEvent, CursorPosition};
|
use codemp_proto::cursor::{CursorEvent, CursorPosition};
|
||||||
|
@ -13,7 +13,7 @@ use codemp_proto::cursor::{CursorEvent, CursorPosition};
|
||||||
use super::controller::{CursorController, CursorControllerInner};
|
use super::controller::{CursorController, CursorControllerInner};
|
||||||
|
|
||||||
struct CursorWorker {
|
struct CursorWorker {
|
||||||
op: mpsc::Receiver<CursorPosition>,
|
op: mpsc::UnboundedReceiver<CursorPosition>,
|
||||||
map: Arc<dashmap::DashMap<Uuid, User>>,
|
map: Arc<dashmap::DashMap<Uuid, User>>,
|
||||||
stream: mpsc::Receiver<oneshot::Sender<Option<Cursor>>>,
|
stream: mpsc::Receiver<oneshot::Sender<Option<Cursor>>>,
|
||||||
poll: mpsc::UnboundedReceiver<oneshot::Sender<()>>,
|
poll: mpsc::UnboundedReceiver<oneshot::Sender<()>>,
|
||||||
|
@ -30,7 +30,7 @@ impl CursorController {
|
||||||
rx: Streaming<CursorEvent>,
|
rx: Streaming<CursorEvent>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// TODO we should tweak the channel buffer size to better propagate backpressure
|
// TODO we should tweak the channel buffer size to better propagate backpressure
|
||||||
let (op_tx, op_rx) = mpsc::channel(64);
|
let (op_tx, op_rx) = mpsc::unbounded_channel();
|
||||||
let (stream_tx, stream_rx) = mpsc::channel(1);
|
let (stream_tx, stream_rx) = mpsc::channel(1);
|
||||||
let (cb_tx, cb_rx) = watch::channel(None);
|
let (cb_tx, cb_rx) = watch::channel(None);
|
||||||
let (poll_tx, poll_rx) = mpsc::unbounded_channel();
|
let (poll_tx, poll_rx) = mpsc::unbounded_channel();
|
||||||
|
@ -86,16 +86,18 @@ impl CursorController {
|
||||||
None => break, // clean exit, just weird that we got it here
|
None => break, // clean exit, just weird that we got it here
|
||||||
Some(controller) => {
|
Some(controller) => {
|
||||||
tracing::debug!("received cursor from server");
|
tracing::debug!("received cursor from server");
|
||||||
let mut cursor = Cursor {
|
|
||||||
buffer: cur.position.buffer.path,
|
|
||||||
start: (cur.position.start.row, cur.position.start.col),
|
|
||||||
end: (cur.position.end.row, cur.position.end.col),
|
|
||||||
user: None,
|
|
||||||
};
|
|
||||||
let user_id = Uuid::from(cur.user);
|
let user_id = Uuid::from(cur.user);
|
||||||
if let Some(user) = worker.map.get(&user_id) {
|
let cursor = Cursor {
|
||||||
cursor.user = Some(user.name.clone());
|
user: worker.map.get(&user_id).map(|u| u.name.clone()).unwrap_or_default(),
|
||||||
|
sel: Selection {
|
||||||
|
buffer: cur.position.buffer.path,
|
||||||
|
start_row: cur.position.start.row,
|
||||||
|
start_col: cur.position.start.col,
|
||||||
|
end_row: cur.position.end.row,
|
||||||
|
end_col: cur.position.end.col
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
worker.store.push_back(cursor);
|
worker.store.push_back(cursor);
|
||||||
for tx in worker.pollers.drain(..) {
|
for tx in worker.pollers.drain(..) {
|
||||||
tx.send(()).unwrap_or_warn("poller dropped before unblocking");
|
tx.send(()).unwrap_or_warn("poller dropped before unblocking");
|
||||||
|
|
|
@ -4,7 +4,7 @@ use jni_toolbox::jni;
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
controller::{AsyncReceiver, AsyncSender},
|
controller::{AsyncReceiver, AsyncSender},
|
||||||
TextChange,
|
BufferUpdate, TextChange,
|
||||||
},
|
},
|
||||||
errors::ControllerError,
|
errors::ControllerError,
|
||||||
};
|
};
|
||||||
|
@ -27,13 +27,13 @@ fn get_content(controller: &mut crate::buffer::Controller) -> Result<String, Con
|
||||||
#[jni(package = "mp.code", class = "BufferController")]
|
#[jni(package = "mp.code", class = "BufferController")]
|
||||||
fn try_recv(
|
fn try_recv(
|
||||||
controller: &mut crate::buffer::Controller,
|
controller: &mut crate::buffer::Controller,
|
||||||
) -> Result<Option<TextChange>, ControllerError> {
|
) -> Result<Option<BufferUpdate>, ControllerError> {
|
||||||
super::tokio().block_on(controller.try_recv())
|
super::tokio().block_on(controller.try_recv())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block until it receives a [TextChange].
|
/// Block until it receives a [TextChange].
|
||||||
#[jni(package = "mp.code", class = "BufferController")]
|
#[jni(package = "mp.code", class = "BufferController")]
|
||||||
fn recv(controller: &mut crate::buffer::Controller) -> Result<TextChange, ControllerError> {
|
fn recv(controller: &mut crate::buffer::Controller) -> Result<BufferUpdate, ControllerError> {
|
||||||
super::tokio().block_on(controller.recv())
|
super::tokio().block_on(controller.recv())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ fn send(
|
||||||
controller: &mut crate::buffer::Controller,
|
controller: &mut crate::buffer::Controller,
|
||||||
change: TextChange,
|
change: TextChange,
|
||||||
) -> Result<(), ControllerError> {
|
) -> Result<(), ControllerError> {
|
||||||
super::tokio().block_on(controller.send(change))
|
controller.send(change)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a callback for buffer changes.
|
/// Register a callback for buffer changes.
|
||||||
|
@ -99,6 +99,12 @@ fn poll(controller: &mut crate::buffer::Controller) -> Result<(), ControllerErro
|
||||||
super::tokio().block_on(controller.poll())
|
super::tokio().block_on(controller.poll())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Acknowledge that a change has been correctly applied.
|
||||||
|
#[jni(package = "mp.code", class = "BufferController")]
|
||||||
|
fn ack(controller: &mut crate::buffer::Controller, version: Vec<i64>) {
|
||||||
|
controller.ack(version)
|
||||||
|
}
|
||||||
|
|
||||||
/// Called by the Java GC to drop a [crate::buffer::Controller].
|
/// Called by the Java GC to drop a [crate::buffer::Controller].
|
||||||
#[jni(package = "mp.code", class = "BufferController")]
|
#[jni(package = "mp.code", class = "BufferController")]
|
||||||
fn free(input: jni::sys::jlong) {
|
fn free(input: jni::sys::jlong) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
controller::{AsyncReceiver, AsyncSender},
|
controller::{AsyncReceiver, AsyncSender},
|
||||||
Cursor,
|
Cursor, Selection,
|
||||||
},
|
},
|
||||||
errors::ControllerError,
|
errors::ControllerError,
|
||||||
};
|
};
|
||||||
|
@ -24,8 +24,8 @@ fn recv(controller: &mut crate::cursor::Controller) -> Result<Cursor, Controller
|
||||||
|
|
||||||
/// Receive from Java, converts and sends a [Cursor].
|
/// Receive from Java, converts and sends a [Cursor].
|
||||||
#[jni(package = "mp.code", class = "CursorController")]
|
#[jni(package = "mp.code", class = "CursorController")]
|
||||||
fn send(controller: &mut crate::cursor::Controller, cursor: Cursor) -> Result<(), ControllerError> {
|
fn send(controller: &mut crate::cursor::Controller, sel: Selection) -> Result<(), ControllerError> {
|
||||||
super::tokio().block_on(controller.send(cursor))
|
controller.send(sel)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a callback for cursor changes.
|
/// Register a callback for cursor changes.
|
||||||
|
|
|
@ -79,6 +79,7 @@ macro_rules! null_check {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) use null_check;
|
pub(crate) use null_check;
|
||||||
|
|
||||||
impl jni_toolbox::JniToolboxError for crate::errors::ConnectionError {
|
impl jni_toolbox::JniToolboxError for crate::errors::ConnectionError {
|
||||||
|
@ -193,13 +194,13 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Event {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::TextChange {
|
impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::BufferUpdate {
|
||||||
const CLASS: &'static str = "mp/code/data/TextChange";
|
const CLASS: &'static str = "mp/code/data/BufferUpdate";
|
||||||
fn into_java_object(
|
fn into_java_object(
|
||||||
self,
|
self,
|
||||||
env: &mut jni::JNIEnv<'j>,
|
env: &mut jni::JNIEnv<'j>,
|
||||||
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
|
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
|
||||||
let content = env.new_string(self.content)?;
|
let class = env.find_class(Self::CLASS)?;
|
||||||
|
|
||||||
let hash_class = env.find_class("java/util/OptionalLong")?;
|
let hash_class = env.find_class("java/util/OptionalLong")?;
|
||||||
let hash = if let Some(h) = self.hash {
|
let hash = if let Some(h) = self.hash {
|
||||||
|
@ -214,15 +215,36 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::TextChange {
|
||||||
}?
|
}?
|
||||||
.l()?;
|
.l()?;
|
||||||
|
|
||||||
|
let version = self.version.into_java_object(env)?;
|
||||||
|
let change = self.change.into_java_object(env)?;
|
||||||
|
|
||||||
|
env.new_object(
|
||||||
|
class,
|
||||||
|
"(Ljava/util/OptionalLong;[JLmp/code/data/TextChange;)V",
|
||||||
|
&[
|
||||||
|
jni::objects::JValueGen::Object(&hash),
|
||||||
|
jni::objects::JValueGen::Object(&version),
|
||||||
|
jni::objects::JValueGen::Object(&change),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::TextChange {
|
||||||
|
const CLASS: &'static str = "mp/code/data/TextChange";
|
||||||
|
fn into_java_object(
|
||||||
|
self,
|
||||||
|
env: &mut jni::JNIEnv<'j>,
|
||||||
|
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
|
||||||
|
let content = env.new_string(self.content)?;
|
||||||
let class = env.find_class(Self::CLASS)?;
|
let class = env.find_class(Self::CLASS)?;
|
||||||
env.new_object(
|
env.new_object(
|
||||||
class,
|
class,
|
||||||
"(JJLjava/lang/String;Ljava/util/OptionalLong;)V",
|
"(JJLjava/lang/String;)V",
|
||||||
&[
|
&[
|
||||||
jni::objects::JValueGen::Long(self.start.into()),
|
jni::objects::JValueGen::Long(self.start.into()),
|
||||||
jni::objects::JValueGen::Long(self.end.into()),
|
jni::objects::JValueGen::Long(self.end.into()),
|
||||||
jni::objects::JValueGen::Object(&content),
|
jni::objects::JValueGen::Object(&content),
|
||||||
jni::objects::JValueGen::Object(&hash),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -234,24 +256,39 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Cursor {
|
||||||
self,
|
self,
|
||||||
env: &mut jni::JNIEnv<'j>,
|
env: &mut jni::JNIEnv<'j>,
|
||||||
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
|
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
|
||||||
let class = env.find_class("mp/code/data/Cursor")?;
|
let class = env.find_class(Self::CLASS)?;
|
||||||
let buffer = env.new_string(&self.buffer)?;
|
let user = env.new_string(&self.user)?;
|
||||||
let user = if let Some(user) = self.user {
|
let sel = self.sel.into_java_object(env)?;
|
||||||
env.new_string(user)?.into()
|
|
||||||
} else {
|
|
||||||
jni::objects::JObject::null()
|
|
||||||
};
|
|
||||||
|
|
||||||
env.new_object(
|
env.new_object(
|
||||||
class,
|
class,
|
||||||
"(IIIILjava/lang/String;Ljava/lang/String;)V",
|
"(Ljava/lang/String;Lmp/code/data/Selection;)V",
|
||||||
&[
|
&[
|
||||||
jni::objects::JValueGen::Int(self.start.0),
|
|
||||||
jni::objects::JValueGen::Int(self.start.1),
|
|
||||||
jni::objects::JValueGen::Int(self.end.0),
|
|
||||||
jni::objects::JValueGen::Int(self.end.1),
|
|
||||||
jni::objects::JValueGen::Object(&buffer),
|
|
||||||
jni::objects::JValueGen::Object(&user),
|
jni::objects::JValueGen::Object(&user),
|
||||||
|
jni::objects::JValueGen::Object(&sel),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Selection {
|
||||||
|
const CLASS: &'static str = "mp/code/data/Selection";
|
||||||
|
fn into_java_object(
|
||||||
|
self,
|
||||||
|
env: &mut jni::JNIEnv<'j>,
|
||||||
|
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
|
||||||
|
let class = env.find_class(Self::CLASS)?;
|
||||||
|
let buffer = env.new_string(&self.buffer)?;
|
||||||
|
|
||||||
|
env.new_object(
|
||||||
|
class,
|
||||||
|
"(IIIILjava/lang/String;)V",
|
||||||
|
&[
|
||||||
|
jni::objects::JValueGen::Int(self.start_row),
|
||||||
|
jni::objects::JValueGen::Int(self.start_col),
|
||||||
|
jni::objects::JValueGen::Int(self.end_row),
|
||||||
|
jni::objects::JValueGen::Int(self.end_col),
|
||||||
|
jni::objects::JValueGen::Object(&buffer),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -363,7 +400,7 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'j> jni_toolbox::FromJava<'j> for crate::api::Cursor {
|
impl<'j> jni_toolbox::FromJava<'j> for crate::api::Selection {
|
||||||
type From = jni::objects::JObject<'j>;
|
type From = jni::objects::JObject<'j>;
|
||||||
fn from_java(
|
fn from_java(
|
||||||
env: &mut jni::JNIEnv<'j>,
|
env: &mut jni::JNIEnv<'j>,
|
||||||
|
@ -384,20 +421,12 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Cursor {
|
||||||
unsafe { env.get_string_unchecked(&jfield.into()) }?.into()
|
unsafe { env.get_string_unchecked(&jfield.into()) }?.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
let user = {
|
|
||||||
let jfield = env.get_field(&cursor, "user", "Ljava/lang/String;")?.l()?;
|
|
||||||
if jfield.is_null() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(unsafe { env.get_string_unchecked(&jfield.into()) }?.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
start: (start_row, start_col),
|
start_row,
|
||||||
end: (end_row, end_col),
|
start_col,
|
||||||
|
end_row,
|
||||||
|
end_col,
|
||||||
buffer,
|
buffer,
|
||||||
user,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -427,21 +456,10 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::TextChange {
|
||||||
unsafe { env.get_string_unchecked(&jfield.into()) }?.into()
|
unsafe { env.get_string_unchecked(&jfield.into()) }?.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
let hash = {
|
|
||||||
let jfield = env
|
|
||||||
.get_field(&change, "hash", "Ljava/util/OptionalLong;")?
|
|
||||||
.l()?;
|
|
||||||
if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? {
|
|
||||||
Some(env.call_method(&jfield, "getAsLong", "()J", &[])?.j()?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
content,
|
content,
|
||||||
hash,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,7 @@ fn delete_buffer(workspace: &mut Workspace, path: String) -> Result<(), RemoteEr
|
||||||
super::tokio().block_on(workspace.delete(&path))
|
super::tokio().block_on(workspace.delete(&path))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block and receive a workspace event
|
/// Block and receive a workspace event.
|
||||||
#[jni(package = "mp.code", class = "Workspace")]
|
#[jni(package = "mp.code", class = "Workspace")]
|
||||||
fn recv(workspace: &mut Workspace) -> Result<crate::api::Event, ControllerError> {
|
fn recv(workspace: &mut Workspace) -> Result<crate::api::Event, ControllerError> {
|
||||||
super::tokio().block_on(workspace.recv())
|
super::tokio().block_on(workspace.recv())
|
||||||
|
@ -103,13 +103,13 @@ fn try_recv(workspace: &mut Workspace) -> Result<Option<crate::api::Event>, Cont
|
||||||
super::tokio().block_on(workspace.try_recv())
|
super::tokio().block_on(workspace.try_recv())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block until a workspace event is available
|
/// Block until a workspace event is available.
|
||||||
#[jni(package = "mp.code", class = "Workspace")]
|
#[jni(package = "mp.code", class = "Workspace")]
|
||||||
fn poll(workspace: &mut Workspace) -> Result<(), ControllerError> {
|
fn poll(workspace: &mut Workspace) -> Result<(), ControllerError> {
|
||||||
super::tokio().block_on(workspace.poll())
|
super::tokio().block_on(workspace.poll())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear previously registered callback
|
/// Clear previously registered callback.
|
||||||
#[jni(package = "mp.code", class = "Workspace")]
|
#[jni(package = "mp.code", class = "Workspace")]
|
||||||
fn clear_callback(workspace: &mut Workspace) {
|
fn clear_callback(workspace: &mut Workspace) {
|
||||||
workspace.clear_callback();
|
workspace.clear_callback();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::api::controller::{AsyncReceiver, AsyncSender};
|
use crate::api::controller::{AsyncReceiver, AsyncSender};
|
||||||
use crate::api::TextChange;
|
use crate::api::{BufferUpdate, TextChange};
|
||||||
use crate::buffer::controller::BufferController;
|
use crate::buffer::controller::BufferController;
|
||||||
use napi::threadsafe_function::{
|
use napi::threadsafe_function::{
|
||||||
ErrorStrategy::Fatal, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
ErrorStrategy::Fatal, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||||
|
@ -51,20 +51,20 @@ impl BufferController {
|
||||||
|
|
||||||
/// Return next buffer event if present
|
/// Return next buffer event if present
|
||||||
#[napi(js_name = "try_recv")]
|
#[napi(js_name = "try_recv")]
|
||||||
pub async fn js_try_recv(&self) -> napi::Result<Option<TextChange>> {
|
pub async fn js_try_recv(&self) -> napi::Result<Option<BufferUpdate>> {
|
||||||
Ok(self.try_recv().await?)
|
Ok(self.try_recv().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wait for next buffer event and return it
|
/// Wait for next buffer event and return it
|
||||||
#[napi(js_name = "recv")]
|
#[napi(js_name = "recv")]
|
||||||
pub async fn js_recv(&self) -> napi::Result<TextChange> {
|
pub async fn js_recv(&self) -> napi::Result<BufferUpdate> {
|
||||||
Ok(self.recv().await?)
|
Ok(self.recv().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a buffer update to workspace
|
/// Send a buffer update to workspace
|
||||||
#[napi(js_name = "send")]
|
#[napi(js_name = "send")]
|
||||||
pub async fn js_send(&self, op: TextChange) -> napi::Result<()> {
|
pub fn js_send(&self, op: TextChange) -> napi::Result<()> {
|
||||||
Ok(self.send(op).await?)
|
Ok(self.send(op)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return buffer whole content
|
/// Return buffer whole content
|
||||||
|
|
|
@ -6,41 +6,6 @@ use napi::threadsafe_function::{
|
||||||
};
|
};
|
||||||
use napi_derive::napi;
|
use napi_derive::napi;
|
||||||
|
|
||||||
#[napi(object, js_name = "Cursor")]
|
|
||||||
pub struct JsCursor {
|
|
||||||
/// range of text change, as char indexes in buffer previous state
|
|
||||||
pub start_row: i32,
|
|
||||||
pub start_col: i32,
|
|
||||||
pub end_row: i32,
|
|
||||||
pub end_col: i32,
|
|
||||||
pub buffer: String,
|
|
||||||
pub user: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<JsCursor> for crate::api::Cursor {
|
|
||||||
fn from(value: JsCursor) -> Self {
|
|
||||||
crate::api::Cursor {
|
|
||||||
start: (value.start_row, value.start_col),
|
|
||||||
end: (value.end_row, value.end_col),
|
|
||||||
buffer: value.buffer,
|
|
||||||
user: value.user,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crate::api::Cursor> for JsCursor {
|
|
||||||
fn from(value: crate::api::Cursor) -> Self {
|
|
||||||
JsCursor {
|
|
||||||
start_row: value.start.0,
|
|
||||||
start_col: value.start.1,
|
|
||||||
end_row: value.end.0,
|
|
||||||
end_col: value.end.1,
|
|
||||||
buffer: value.buffer,
|
|
||||||
user: value.user.map(|x| x.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
impl CursorController {
|
impl CursorController {
|
||||||
/// Register a callback to be called on receive.
|
/// Register a callback to be called on receive.
|
||||||
|
@ -74,19 +39,19 @@ impl CursorController {
|
||||||
|
|
||||||
/// Send a new cursor event to remote
|
/// Send a new cursor event to remote
|
||||||
#[napi(js_name = "send")]
|
#[napi(js_name = "send")]
|
||||||
pub async fn js_send(&self, pos: JsCursor) -> napi::Result<()> {
|
pub fn js_send(&self, sel: crate::api::Selection) -> napi::Result<()> {
|
||||||
Ok(self.send(crate::api::Cursor::from(pos)).await?)
|
Ok(self.send(sel)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get next cursor event if available without blocking
|
/// Get next cursor event if available without blocking
|
||||||
#[napi(js_name = "try_recv")]
|
#[napi(js_name = "try_recv")]
|
||||||
pub async fn js_try_recv(&self) -> napi::Result<Option<JsCursor>> {
|
pub async fn js_try_recv(&self) -> napi::Result<Option<crate::api::Cursor>> {
|
||||||
Ok(self.try_recv().await?.map(JsCursor::from))
|
Ok(self.try_recv().await?.map(crate::api::Cursor::from))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block until next
|
/// Block until next
|
||||||
#[napi(js_name = "recv")]
|
#[napi(js_name = "recv")]
|
||||||
pub async fn js_recv(&self) -> napi::Result<JsCursor> {
|
pub async fn js_recv(&self) -> napi::Result<crate::api::Cursor> {
|
||||||
Ok(self.recv().await?.into())
|
Ok(self.recv().await?.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,6 @@ use napi::threadsafe_function::{
|
||||||
};
|
};
|
||||||
use napi_derive::napi;
|
use napi_derive::napi;
|
||||||
|
|
||||||
use super::client::JsUser;
|
|
||||||
|
|
||||||
#[napi(object, js_name = "Event")]
|
#[napi(object, js_name = "Event")]
|
||||||
pub struct JsEvent {
|
pub struct JsEvent {
|
||||||
pub r#type: String,
|
pub r#type: String,
|
||||||
|
@ -148,12 +146,15 @@ impl Workspace {
|
||||||
|
|
||||||
/// List users attached to a specific buffer
|
/// List users attached to a specific buffer
|
||||||
#[napi(js_name = "list_buffer_users")]
|
#[napi(js_name = "list_buffer_users")]
|
||||||
pub async fn js_list_buffer_users(&self, path: String) -> napi::Result<Vec<JsUser>> {
|
pub async fn js_list_buffer_users(
|
||||||
|
&self,
|
||||||
|
path: String,
|
||||||
|
) -> napi::Result<Vec<crate::ffi::js::client::JsUser>> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.list_buffer_users(&path)
|
.list_buffer_users(&path)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(JsUser::from)
|
.map(super::client::JsUser::from)
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,9 @@ impl LuaUserData for CodempBufferController {
|
||||||
Ok(format!("{:?}", this))
|
Ok(format!("{:?}", this))
|
||||||
});
|
});
|
||||||
|
|
||||||
methods.add_method(
|
methods.add_method("send", |_, this, (change,): (CodempTextChange,)| {
|
||||||
"send",
|
Ok(this.send(change)?)
|
||||||
|_, this, (change,): (CodempTextChange,)| a_sync! { this => this.send(change).await? },
|
});
|
||||||
);
|
|
||||||
|
|
||||||
methods.add_method(
|
methods.add_method(
|
||||||
"try_recv",
|
"try_recv",
|
||||||
|
@ -22,21 +21,20 @@ impl LuaUserData for CodempBufferController {
|
||||||
);
|
);
|
||||||
methods.add_method("recv", |_, this, ()| a_sync! { this => this.recv().await? });
|
methods.add_method("recv", |_, this, ()| a_sync! { this => this.recv().await? });
|
||||||
methods.add_method("poll", |_, this, ()| a_sync! { this => this.poll().await? });
|
methods.add_method("poll", |_, this, ()| a_sync! { this => this.poll().await? });
|
||||||
|
methods.add_method_mut("ack", |_, this, (version,): (Vec<i64>,)| {
|
||||||
|
Ok(this.ack(version))
|
||||||
|
});
|
||||||
|
|
||||||
methods.add_method(
|
methods.add_method(
|
||||||
"content",
|
"content",
|
||||||
|_, this, ()| a_sync! { this => this.content().await? },
|
|_, this, ()| a_sync! { this => this.content().await? },
|
||||||
);
|
);
|
||||||
|
|
||||||
methods.add_method("clear_callback", |_, this, ()| {
|
methods.add_method("clear_callback", |_, this, ()| Ok(this.clear_callback()));
|
||||||
this.clear_callback();
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
methods.add_method("callback", |_, this, (cb,): (LuaFunction,)| {
|
methods.add_method("callback", |_, this, (cb,): (LuaFunction,)| {
|
||||||
this.callback(move |controller: CodempBufferController| {
|
Ok(this.callback(move |controller: CodempBufferController| {
|
||||||
super::ext::callback().invoke(cb.clone(), controller)
|
super::ext::callback().invoke(cb.clone(), controller)
|
||||||
});
|
}))
|
||||||
Ok(())
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,7 +45,6 @@ impl LuaUserData for CodempTextChange {
|
||||||
fields.add_field_method_get("content", |_, this| Ok(this.content.clone()));
|
fields.add_field_method_get("content", |_, this| Ok(this.content.clone()));
|
||||||
fields.add_field_method_get("start", |_, this| Ok(this.start));
|
fields.add_field_method_get("start", |_, this| Ok(this.start));
|
||||||
fields.add_field_method_get("end", |_, this| Ok(this.end));
|
fields.add_field_method_get("end", |_, this| Ok(this.end));
|
||||||
fields.add_field_method_get("hash", |_, this| Ok(this.hash));
|
|
||||||
// add a 'finish' accessor too because in Lua 'end' is reserved
|
// add a 'finish' accessor too because in Lua 'end' is reserved
|
||||||
fields.add_field_method_get("finish", |_, this| Ok(this.end));
|
fields.add_field_method_get("finish", |_, this| Ok(this.end));
|
||||||
}
|
}
|
||||||
|
@ -59,3 +56,18 @@ impl LuaUserData for CodempTextChange {
|
||||||
methods.add_method("apply", |_, this, (txt,): (String,)| Ok(this.apply(&txt)));
|
methods.add_method("apply", |_, this, (txt,): (String,)| Ok(this.apply(&txt)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
from_lua_serde! { CodempBufferUpdate }
|
||||||
|
impl LuaUserData for CodempBufferUpdate {
|
||||||
|
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||||
|
fields.add_field_method_get("hash", |_, this| Ok(this.hash));
|
||||||
|
fields.add_field_method_get("version", |_, this| Ok(this.version.clone()));
|
||||||
|
fields.add_field_method_get("change", |_, this| Ok(this.change.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||||
|
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
|
||||||
|
Ok(format!("{:?}", this))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ use mlua_codemp_patch as mlua;
|
||||||
|
|
||||||
use super::ext::a_sync::a_sync;
|
use super::ext::a_sync::a_sync;
|
||||||
use super::ext::from_lua_serde;
|
use super::ext::from_lua_serde;
|
||||||
use super::ext::lua_tuple;
|
|
||||||
|
|
||||||
impl LuaUserData for CodempCursorController {
|
impl LuaUserData for CodempCursorController {
|
||||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||||
|
@ -12,10 +11,9 @@ impl LuaUserData for CodempCursorController {
|
||||||
Ok(format!("{:?}", this))
|
Ok(format!("{:?}", this))
|
||||||
});
|
});
|
||||||
|
|
||||||
methods.add_method(
|
methods.add_method("send", |_, this, (cursor,): (CodempSelection,)| {
|
||||||
"send",
|
Ok(this.send(cursor)?)
|
||||||
|_, this, (cursor,): (CodempCursor,)| a_sync! { this => this.send(cursor).await? },
|
});
|
||||||
);
|
|
||||||
methods.add_method(
|
methods.add_method(
|
||||||
"try_recv",
|
"try_recv",
|
||||||
|_, this, ()| a_sync! { this => this.try_recv().await? },
|
|_, this, ()| a_sync! { this => this.try_recv().await? },
|
||||||
|
@ -23,33 +21,42 @@ impl LuaUserData for CodempCursorController {
|
||||||
methods.add_method("recv", |_, this, ()| a_sync! { this => this.recv().await? });
|
methods.add_method("recv", |_, this, ()| a_sync! { this => this.recv().await? });
|
||||||
methods.add_method("poll", |_, this, ()| a_sync! { this => this.poll().await? });
|
methods.add_method("poll", |_, this, ()| a_sync! { this => this.poll().await? });
|
||||||
|
|
||||||
methods.add_method("clear_callback", |_, this, ()| {
|
methods.add_method("clear_callback", |_, this, ()| Ok(this.clear_callback()));
|
||||||
this.clear_callback();
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
methods.add_method("callback", |_, this, (cb,): (LuaFunction,)| {
|
methods.add_method("callback", |_, this, (cb,): (LuaFunction,)| {
|
||||||
this.callback(move |controller: CodempCursorController| {
|
Ok(this.callback(move |controller: CodempCursorController| {
|
||||||
super::ext::callback().invoke(cb.clone(), controller)
|
super::ext::callback().invoke(cb.clone(), controller)
|
||||||
});
|
}))
|
||||||
Ok(())
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
from_lua_serde! { CodempCursor }
|
from_lua_serde! { CodempCursor }
|
||||||
impl LuaUserData for CodempCursor {
|
impl LuaUserData for CodempCursor {
|
||||||
|
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||||
|
fields.add_field_method_get("user", |_, this| Ok(this.user.clone()));
|
||||||
|
fields.add_field_method_get("sel", |_, this| Ok(this.sel.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||||
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
|
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
|
||||||
Ok(format!("{:?}", this))
|
Ok(format!("{:?}", this))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
from_lua_serde! { CodempSelection }
|
||||||
|
impl LuaUserData for CodempSelection {
|
||||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||||
fields.add_field_method_get("user", |_, this| Ok(this.user.clone()));
|
|
||||||
fields.add_field_method_get("buffer", |_, this| Ok(this.buffer.clone()));
|
fields.add_field_method_get("buffer", |_, this| Ok(this.buffer.clone()));
|
||||||
fields.add_field_method_get("start", |lua, this| lua_tuple(lua, this.start));
|
fields.add_field_method_get("start_row", |_, this| Ok(this.start_row));
|
||||||
fields.add_field_method_get("end", |lua, this| lua_tuple(lua, this.end));
|
fields.add_field_method_get("start_col", |_, this| Ok(this.start_col));
|
||||||
// add a 'finish' accessor too because in Lua 'end' is reserved
|
fields.add_field_method_get("end_row", |_, this| Ok(this.end_row));
|
||||||
fields.add_field_method_get("finish", |lua, this| lua_tuple(lua, this.end));
|
fields.add_field_method_get("end_col", |_, this| Ok(this.end_col));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||||
|
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
|
||||||
|
Ok(format!("{:?}", this))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,10 +51,7 @@ impl LuaUserData for Promise {
|
||||||
});
|
});
|
||||||
methods.add_method_mut("cancel", |_, this, ()| match this.0.take() {
|
methods.add_method_mut("cancel", |_, this, ()| match this.0.take() {
|
||||||
None => Err(LuaError::runtime("Promise already awaited")),
|
None => Err(LuaError::runtime("Promise already awaited")),
|
||||||
Some(x) => {
|
Some(x) => Ok(x.abort()),
|
||||||
x.abort();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
methods.add_method_mut("and_then", |_, this, (cb,): (LuaFunction,)| {
|
methods.add_method_mut("and_then", |_, this, (cb,): (LuaFunction,)| {
|
||||||
match this.0.take() {
|
match this.0.take() {
|
||||||
|
|
|
@ -62,55 +62,57 @@ pub(crate) enum LuaCallback {
|
||||||
Invoke(LuaFunction, CallbackArg),
|
Invoke(LuaFunction, CallbackArg),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! callback_args {
|
||||||
|
($($name:ident : $t:ty ,)*) => {
|
||||||
pub(crate) enum CallbackArg {
|
pub(crate) enum CallbackArg {
|
||||||
Nil,
|
Nil,
|
||||||
Str(String),
|
$(
|
||||||
VecStr(Vec<String>),
|
$name($t),
|
||||||
Client(CodempClient),
|
)*
|
||||||
CursorController(CodempCursorController),
|
|
||||||
BufferController(CodempBufferController),
|
|
||||||
Workspace(CodempWorkspace),
|
|
||||||
Event(CodempEvent),
|
|
||||||
MaybeEvent(Option<CodempEvent>),
|
|
||||||
Cursor(CodempCursor),
|
|
||||||
MaybeCursor(Option<CodempCursor>),
|
|
||||||
TextChange(CodempTextChange),
|
|
||||||
MaybeTextChange(Option<CodempTextChange>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoLua for CallbackArg {
|
impl IntoLua for CallbackArg {
|
||||||
// TODO this basically calls .into_lua() on all enum variants
|
|
||||||
// i wish i could do this with a Box<dyn IntoLua> or an impl IntoLua
|
|
||||||
// but IntoLua requires Sized so it can't be made into an object
|
|
||||||
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
|
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
|
||||||
match self {
|
match self {
|
||||||
CallbackArg::Nil => Ok(LuaValue::Nil),
|
Self::Nil => Ok(LuaValue::Nil),
|
||||||
CallbackArg::Str(x) => x.into_lua(lua),
|
$(
|
||||||
CallbackArg::Client(x) => x.into_lua(lua),
|
Self::$name(x) => x.into_lua(lua),
|
||||||
CallbackArg::CursorController(x) => x.into_lua(lua),
|
)*
|
||||||
CallbackArg::BufferController(x) => x.into_lua(lua),
|
|
||||||
CallbackArg::Workspace(x) => x.into_lua(lua),
|
|
||||||
CallbackArg::VecStr(x) => x.into_lua(lua),
|
|
||||||
CallbackArg::Event(x) => x.into_lua(lua),
|
|
||||||
CallbackArg::MaybeEvent(x) => x.into_lua(lua),
|
|
||||||
CallbackArg::Cursor(x) => x.into_lua(lua),
|
|
||||||
CallbackArg::MaybeCursor(x) => x.into_lua(lua),
|
|
||||||
CallbackArg::TextChange(x) => x.into_lua(lua),
|
|
||||||
CallbackArg::MaybeTextChange(x) => x.into_lua(lua),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<()> for CallbackArg { fn from(_: ()) -> Self { CallbackArg::Nil } }
|
impl From<()> for CallbackArg {
|
||||||
impl From<String> for CallbackArg { fn from(value: String) -> Self { CallbackArg::Str(value) } }
|
fn from(_value: ()) -> Self {
|
||||||
impl From<CodempClient> for CallbackArg { fn from(value: CodempClient) -> Self { CallbackArg::Client(value) } }
|
Self::Nil
|
||||||
impl From<CodempCursorController> for CallbackArg { fn from(value: CodempCursorController) -> Self { CallbackArg::CursorController(value) } }
|
}
|
||||||
impl From<CodempBufferController> for CallbackArg { fn from(value: CodempBufferController) -> Self { CallbackArg::BufferController(value) } }
|
}
|
||||||
impl From<CodempWorkspace> for CallbackArg { fn from(value: CodempWorkspace) -> Self { CallbackArg::Workspace(value) } }
|
|
||||||
impl From<Vec<String>> for CallbackArg { fn from(value: Vec<String>) -> Self { CallbackArg::VecStr(value) } }
|
$(
|
||||||
impl From<CodempEvent> for CallbackArg { fn from(value: CodempEvent) -> Self { CallbackArg::Event(value) } }
|
impl From<$t> for CallbackArg {
|
||||||
impl From<CodempCursor> for CallbackArg { fn from(value: CodempCursor) -> Self { CallbackArg::Cursor(value) } }
|
fn from(value: $t) -> Self {
|
||||||
impl From<Option<CodempCursor>> for CallbackArg { fn from(value: Option<CodempCursor>) -> Self { CallbackArg::MaybeCursor(value) } }
|
Self::$name(value)
|
||||||
impl From<CodempTextChange> for CallbackArg { fn from(value: CodempTextChange) -> Self { CallbackArg::TextChange(value) } }
|
}
|
||||||
impl From<Option<CodempTextChange>> for CallbackArg { fn from(value: Option<CodempTextChange>) -> Self { CallbackArg::MaybeTextChange(value) } }
|
}
|
||||||
impl From<Option<CodempEvent>> for CallbackArg { fn from(value: Option<CodempEvent>) -> Self { CallbackArg::MaybeEvent(value) } }
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
callback_args! {
|
||||||
|
Str: String,
|
||||||
|
VecStr: Vec<String>,
|
||||||
|
Client: CodempClient,
|
||||||
|
CursorController: CodempCursorController,
|
||||||
|
BufferController: CodempBufferController,
|
||||||
|
Workspace: CodempWorkspace,
|
||||||
|
Event: CodempEvent,
|
||||||
|
MaybeEvent: Option<CodempEvent>,
|
||||||
|
Cursor: CodempCursor,
|
||||||
|
MaybeCursor: Option<CodempCursor>,
|
||||||
|
Selection: CodempSelection,
|
||||||
|
MaybeSelection: Option<CodempSelection>,
|
||||||
|
TextChange: CodempTextChange,
|
||||||
|
MaybeTextChange: Option<CodempTextChange>,
|
||||||
|
BufferUpdate: CodempBufferUpdate,
|
||||||
|
MaybeBufferUpdate: Option<CodempBufferUpdate>,
|
||||||
|
}
|
||||||
|
|
|
@ -2,19 +2,9 @@ pub mod a_sync;
|
||||||
pub mod callback;
|
pub mod callback;
|
||||||
pub mod log;
|
pub mod log;
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use mlua_codemp_patch as mlua;
|
|
||||||
|
|
||||||
pub(crate) use a_sync::tokio;
|
pub(crate) use a_sync::tokio;
|
||||||
pub(crate) use callback::callback;
|
pub(crate) use callback::callback;
|
||||||
|
|
||||||
pub(crate) fn lua_tuple<T: IntoLua>(lua: &Lua, (a, b): (T, T)) -> LuaResult<LuaTable> {
|
|
||||||
let table = lua.create_table()?;
|
|
||||||
table.set(1, a)?;
|
|
||||||
table.set(2, b)?;
|
|
||||||
Ok(table)
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! from_lua_serde {
|
macro_rules! from_lua_serde {
|
||||||
($($t:ty)*) => {
|
($($t:ty)*) => {
|
||||||
$(
|
$(
|
||||||
|
|
|
@ -61,16 +61,12 @@ impl LuaUserData for CodempWorkspace {
|
||||||
methods.add_method("poll", |_, this, ()| a_sync! { this => this.poll().await? });
|
methods.add_method("poll", |_, this, ()| a_sync! { this => this.poll().await? });
|
||||||
|
|
||||||
methods.add_method("callback", |_, this, (cb,): (LuaFunction,)| {
|
methods.add_method("callback", |_, this, (cb,): (LuaFunction,)| {
|
||||||
this.callback(move |controller: CodempWorkspace| {
|
Ok(this.callback(move |controller: CodempWorkspace| {
|
||||||
super::ext::callback().invoke(cb.clone(), controller)
|
super::ext::callback().invoke(cb.clone(), controller)
|
||||||
});
|
}))
|
||||||
Ok(())
|
|
||||||
});
|
});
|
||||||
|
|
||||||
methods.add_method("clear_callbacl", |_, this, ()| {
|
methods.add_method("clear_callbacl", |_, this, ()| Ok(this.clear_callback()));
|
||||||
this.clear_callback();
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||||
|
|
|
@ -43,6 +43,8 @@
|
||||||
//! `JNIException`s are however unchecked: there is nothing you can do to recover from them, as they usually represent a severe error in the glue code. If they arise, it's probably a bug.
|
//! `JNIException`s are however unchecked: there is nothing you can do to recover from them, as they usually represent a severe error in the glue code. If they arise, it's probably a bug.
|
||||||
//!
|
//!
|
||||||
|
|
||||||
|
#![allow(clippy::unit_arg)]
|
||||||
|
|
||||||
/// java bindings, built with [jni]
|
/// java bindings, built with [jni]
|
||||||
#[cfg(feature = "java")]
|
#[cfg(feature = "java")]
|
||||||
pub mod java;
|
pub mod java;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::api::controller::{AsyncReceiver, AsyncSender};
|
use crate::api::controller::{AsyncReceiver, AsyncSender};
|
||||||
use crate::api::Cursor;
|
|
||||||
use crate::api::TextChange;
|
use crate::api::TextChange;
|
||||||
|
use crate::api::{Cursor, Selection};
|
||||||
use crate::buffer::Controller as BufferController;
|
use crate::buffer::Controller as BufferController;
|
||||||
use crate::cursor::Controller as CursorController;
|
use crate::cursor::Controller as CursorController;
|
||||||
use pyo3::exceptions::PyValueError;
|
use pyo3::exceptions::PyValueError;
|
||||||
|
@ -15,19 +15,21 @@ impl CursorController {
|
||||||
#[pyo3(name = "send")]
|
#[pyo3(name = "send")]
|
||||||
fn pysend(
|
fn pysend(
|
||||||
&self,
|
&self,
|
||||||
py: Python,
|
_py: Python,
|
||||||
path: String,
|
path: String,
|
||||||
start: (i32, i32),
|
start: (i32, i32),
|
||||||
end: (i32, i32),
|
end: (i32, i32),
|
||||||
) -> PyResult<Promise> {
|
) -> PyResult<()> {
|
||||||
let pos = Cursor {
|
let pos = Selection {
|
||||||
start,
|
start_row: start.0,
|
||||||
end,
|
start_col: start.1,
|
||||||
|
end_row: end.0,
|
||||||
|
end_col: end.1,
|
||||||
buffer: path,
|
buffer: path,
|
||||||
user: None,
|
|
||||||
};
|
};
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
a_sync_allow_threads!(py, this.send(pos).await)
|
this.send(pos)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyo3(name = "try_recv")]
|
#[pyo3(name = "try_recv")]
|
||||||
|
@ -84,15 +86,15 @@ impl BufferController {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyo3(name = "send")]
|
#[pyo3(name = "send")]
|
||||||
fn pysend(&self, py: Python, start: u32, end: u32, txt: String) -> PyResult<Promise> {
|
fn pysend(&self, _py: Python, start: u32, end: u32, txt: String) -> PyResult<()> {
|
||||||
let op = TextChange {
|
let op = TextChange {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
content: txt,
|
content: txt,
|
||||||
hash: None,
|
|
||||||
};
|
};
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
a_sync_allow_threads!(py, this.send(op).await)
|
this.send(op)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyo3(name = "try_recv")]
|
#[pyo3(name = "try_recv")]
|
||||||
|
@ -141,21 +143,21 @@ impl BufferController {
|
||||||
impl Cursor {
|
impl Cursor {
|
||||||
#[getter(start)]
|
#[getter(start)]
|
||||||
fn pystart(&self) -> (i32, i32) {
|
fn pystart(&self) -> (i32, i32) {
|
||||||
self.start
|
(self.sel.start_row, self.sel.start_col)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[getter(end)]
|
#[getter(end)]
|
||||||
fn pyend(&self) -> (i32, i32) {
|
fn pyend(&self) -> (i32, i32) {
|
||||||
self.end
|
(self.sel.end_row, self.sel.end_col)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[getter(buffer)]
|
#[getter(buffer)]
|
||||||
fn pybuffer(&self) -> String {
|
fn pybuffer(&self) -> String {
|
||||||
self.buffer.clone()
|
self.sel.buffer.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[getter(user)]
|
#[getter(user)]
|
||||||
fn pyuser(&self) -> Option<String> {
|
fn pyuser(&self) -> Option<String> {
|
||||||
self.user.clone()
|
Some(self.user.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
src/lib.rs
10
src/lib.rs
|
@ -53,7 +53,7 @@
|
||||||
//! use codemp::api::controller::{AsyncSender, AsyncReceiver}; // needed to access trait methods
|
//! use codemp::api::controller::{AsyncSender, AsyncReceiver}; // needed to access trait methods
|
||||||
//! let cursor = workspace.cursor();
|
//! let cursor = workspace.cursor();
|
||||||
//! let event = cursor.recv().await.expect("disconnected while waiting for event!");
|
//! let event = cursor.recv().await.expect("disconnected while waiting for event!");
|
||||||
//! println!("user {} moved on buffer {}", event.user.unwrap_or_default(), event.buffer);
|
//! println!("user {} moved on buffer {}", event.user, event.sel.buffer);
|
||||||
//! # };
|
//! # };
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
@ -69,8 +69,12 @@
|
||||||
//! # use codemp::api::controller::{AsyncSender, AsyncReceiver};
|
//! # use codemp::api::controller::{AsyncSender, AsyncReceiver};
|
||||||
//! let buffer = workspace.attach("/some/file.txt").await.expect("failed to attach");
|
//! let buffer = workspace.attach("/some/file.txt").await.expect("failed to attach");
|
||||||
//! buffer.content(); // force-sync
|
//! buffer.content(); // force-sync
|
||||||
//! if let Some(change) = buffer.try_recv().await.unwrap() {
|
//! if let Some(mut update) = buffer.try_recv().await.unwrap() {
|
||||||
//! println!("content: {}, span: {}-{}", change.content, change.start, change.end);
|
//! println!(
|
||||||
|
//! "content: {}, span: {}-{}",
|
||||||
|
//! update.change.content, update.change.start, update.change.end
|
||||||
|
//! );
|
||||||
|
//! buffer.ack(update.version);
|
||||||
//! } // if None, no changes are currently available
|
//! } // if None, no changes are currently available
|
||||||
//! # };
|
//! # };
|
||||||
//! ```
|
//! ```
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
|
|
||||||
pub use crate::api::{
|
pub use crate::api::{
|
||||||
controller::AsyncReceiver as CodempAsyncReceiver, controller::AsyncSender as CodempAsyncSender,
|
controller::AsyncReceiver as CodempAsyncReceiver, controller::AsyncSender as CodempAsyncSender,
|
||||||
Config as CodempConfig, Controller as CodempController, Cursor as CodempCursor,
|
BufferUpdate as CodempBufferUpdate, Config as CodempConfig, Controller as CodempController,
|
||||||
Event as CodempEvent, TextChange as CodempTextChange, User as CodempUser,
|
Cursor as CodempCursor, Event as CodempEvent, Selection as CodempSelection,
|
||||||
|
TextChange as CodempTextChange, User as CodempUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
|
|
Loading…
Reference in a new issue