mirror of
https://github.com/hexedtech/codemp.git
synced 2024-11-21 23:04:49 +01:00
feat: initial work on jni-rs java glue
This commit is contained in:
parent
df95b20728
commit
e2ae53b35f
18 changed files with 437 additions and 458 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -12,3 +12,9 @@ node_modules/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
index.d.ts
|
index.d.ts
|
||||||
index.node
|
index.node
|
||||||
|
|
||||||
|
# java
|
||||||
|
java/*.iml
|
||||||
|
java/.idea/
|
||||||
|
java/*.h
|
||||||
|
java/**/*.class
|
||||||
|
|
95
build.rs
95
build.rs
|
@ -6,93 +6,6 @@ extern crate pyo3_build_config;
|
||||||
|
|
||||||
/// The main method of the buildscript, required by some glue modules.
|
/// The main method of the buildscript, required by some glue modules.
|
||||||
fn main() {
|
fn main() {
|
||||||
#[cfg(feature = "java")]
|
|
||||||
{
|
|
||||||
let pkg = "com.codemp.jni".to_string();
|
|
||||||
let pkg_folder = pkg.replace('.', "/"); // java moment
|
|
||||||
|
|
||||||
let out_dir = std::env::var("OUT_DIR").expect("cargo did not provide OUT_DIR");
|
|
||||||
let out_dir = std::path::Path::new(&out_dir);
|
|
||||||
let generated_glue_file = out_dir.join("generated_glue.in");
|
|
||||||
let src_dir = std::path::Path::new("src").join("ffi").join("java");
|
|
||||||
let typemap_file = src_dir.join("typemap.in");
|
|
||||||
rifgen::Generator::new(
|
|
||||||
rifgen::TypeCases::CamelCase,
|
|
||||||
rifgen::Language::Java,
|
|
||||||
vec![src_dir],
|
|
||||||
)
|
|
||||||
.generate_interface(&generated_glue_file);
|
|
||||||
|
|
||||||
// build java source path
|
|
||||||
let target = out_dir
|
|
||||||
.parent()
|
|
||||||
.unwrap()
|
|
||||||
.parent()
|
|
||||||
.unwrap()
|
|
||||||
.parent()
|
|
||||||
.unwrap()
|
|
||||||
.to_path_buf(); // target/debug
|
|
||||||
|
|
||||||
let mut java_target = target.clone(); // target/debug/java
|
|
||||||
java_target.push("java");
|
|
||||||
|
|
||||||
let mut pkg_path = java_target.clone(); // target/debug/java/src/com/codemp/intellij
|
|
||||||
pkg_path.push("src");
|
|
||||||
pkg_path.push(pkg_folder);
|
|
||||||
|
|
||||||
// delete folder if it exists, then create it
|
|
||||||
recreate_path(&pkg_path);
|
|
||||||
|
|
||||||
// generate java code
|
|
||||||
let java_cfg = flapigen::JavaConfig::new(pkg_path.clone(), pkg);
|
|
||||||
let java_gen = flapigen::Generator::new(flapigen::LanguageConfig::JavaConfig(java_cfg))
|
|
||||||
.rustfmt_bindings(true);
|
|
||||||
java_gen.expand_many(
|
|
||||||
"codemp-intellij",
|
|
||||||
&[&generated_glue_file, &typemap_file],
|
|
||||||
out_dir.join("glue.rs"),
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(feature = "java-artifact")]
|
|
||||||
{
|
|
||||||
// panic if no jdk
|
|
||||||
std::process::Command::new("javac")
|
|
||||||
.arg("--version")
|
|
||||||
.status()
|
|
||||||
.expect("java not found");
|
|
||||||
|
|
||||||
// compile java code
|
|
||||||
let mut java_compiled = java_target.clone(); // target/debug/java/classes
|
|
||||||
java_compiled.push("classes");
|
|
||||||
recreate_path(&java_compiled);
|
|
||||||
|
|
||||||
let mut javac_cmd = std::process::Command::new("javac");
|
|
||||||
javac_cmd.arg("-d").arg(java_compiled.as_os_str());
|
|
||||||
for java_file in pkg_path.read_dir().unwrap().filter_map(|e| e.ok()) {
|
|
||||||
javac_cmd.arg(java_file.path().as_os_str());
|
|
||||||
}
|
|
||||||
javac_cmd.status().expect("failed to run javac");
|
|
||||||
|
|
||||||
// jar it!
|
|
||||||
let mut jar_file = target.clone(); // target/debug/codemp-java.jar
|
|
||||||
jar_file.push("codemp-java.jar");
|
|
||||||
|
|
||||||
let mut jar_cmd = std::process::Command::new("jar");
|
|
||||||
jar_cmd
|
|
||||||
.current_dir(&java_compiled)
|
|
||||||
.arg("cf")
|
|
||||||
.arg(jar_file.as_os_str());
|
|
||||||
for java_file in java_compiled.read_dir().unwrap().filter_map(|e| e.ok()) {
|
|
||||||
let relative_path = java_file.path().clone();
|
|
||||||
let relative_path = relative_path.strip_prefix(&java_compiled).unwrap();
|
|
||||||
jar_cmd.arg(relative_path.as_os_str());
|
|
||||||
}
|
|
||||||
jar_cmd.status().expect("failed to run jar!");
|
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed={}", generated_glue_file.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "js")]
|
#[cfg(feature = "js")]
|
||||||
{
|
{
|
||||||
napi_build::setup();
|
napi_build::setup();
|
||||||
|
@ -103,11 +16,3 @@ fn main() {
|
||||||
pyo3_build_config::add_extension_module_link_args();
|
pyo3_build_config::add_extension_module_link_args();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "java")]
|
|
||||||
fn recreate_path(path: &std::path::PathBuf) {
|
|
||||||
if path.exists() {
|
|
||||||
std::fs::remove_dir_all(path).expect("failed to delete old dir!");
|
|
||||||
}
|
|
||||||
std::fs::create_dir_all(path).expect("error while creating folder!");
|
|
||||||
}
|
|
||||||
|
|
39
java/src/mp/code/BufferController.java
Normal file
39
java/src/mp/code/BufferController.java
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package mp.code;
|
||||||
|
|
||||||
|
import mp.code.data.TextChange;
|
||||||
|
import mp.code.exceptions.CodeMPLibException;
|
||||||
|
|
||||||
|
public class BufferController {
|
||||||
|
private final long ptr;
|
||||||
|
|
||||||
|
BufferController(long ptr) {
|
||||||
|
this.ptr = ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static native String get_name(long self);
|
||||||
|
public String getName() {
|
||||||
|
return get_name(this.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static native String get_content(long self);
|
||||||
|
public String getContent() {
|
||||||
|
return get_content(this.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native TextChange try_recv(long self) throws CodeMPLibException;
|
||||||
|
public TextChange tryRecv() throws CodeMPLibException {
|
||||||
|
return try_recv(this.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native void send(long self, TextChange change) throws CodeMPLibException;
|
||||||
|
public void send(TextChange change) throws CodeMPLibException {
|
||||||
|
send(this.ptr, change);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native void free(long self);
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("removal")
|
||||||
|
protected void finalize() throws Throwable {
|
||||||
|
free(this.ptr);
|
||||||
|
}
|
||||||
|
}
|
64
java/src/mp/code/Client.java
Normal file
64
java/src/mp/code/Client.java
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package mp.code;
|
||||||
|
|
||||||
|
import mp.code.exceptions.CodeMPLibException;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class Client {
|
||||||
|
private final long ptr;
|
||||||
|
private final String url;
|
||||||
|
|
||||||
|
private static native long setup_tracing(String path);
|
||||||
|
|
||||||
|
private static native long connect(String url) throws CodeMPLibException;
|
||||||
|
public Client(String url) throws CodeMPLibException {
|
||||||
|
this.ptr = connect(url);
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return this.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native void login(long self, String username, String password, String workspace) throws CodeMPLibException;
|
||||||
|
public void login(String username, String password, String workspace) throws CodeMPLibException {
|
||||||
|
login(this.ptr, username, password, workspace);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native long join_workspace(long self, String id) throws CodeMPLibException;
|
||||||
|
public Workspace joinWorkspace(String id) throws CodeMPLibException {
|
||||||
|
return new Workspace(join_workspace(this.ptr, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native long get_workspace(long self);
|
||||||
|
public Optional<Workspace> getWorkspace() {
|
||||||
|
long ptr = get_workspace(this.ptr);
|
||||||
|
if(ptr == 0) { // TODO it would be better to init in rust directly
|
||||||
|
return Optional.empty();
|
||||||
|
} else {
|
||||||
|
return Optional.of(new Workspace(ptr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native void free(long self);
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("removal") // muh java 8
|
||||||
|
protected void finalize() {
|
||||||
|
free(this.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - remove everything past this line
|
||||||
|
public static void main(String[] args) throws CodeMPLibException {
|
||||||
|
Client c = new Client("http://alemi.dev:50053");
|
||||||
|
c.login(UUID.randomUUID().toString(), "lmaodefaultpassword", "glue");
|
||||||
|
c.joinWorkspace("glue");
|
||||||
|
System.out.println("Done!");
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
System.loadLibrary("codemp");
|
||||||
|
setup_tracing(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
29
java/src/mp/code/CursorController.java
Normal file
29
java/src/mp/code/CursorController.java
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package mp.code;
|
||||||
|
|
||||||
|
import mp.code.data.Cursor;
|
||||||
|
import mp.code.data.TextChange;
|
||||||
|
|
||||||
|
public class CursorController {
|
||||||
|
private final long ptr;
|
||||||
|
|
||||||
|
CursorController(long ptr) {
|
||||||
|
this.ptr = ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native Cursor recv(long self);
|
||||||
|
public Cursor recv() {
|
||||||
|
return recv(this.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native void send(long self, Cursor cursor);
|
||||||
|
public void send(TextChange change, Cursor cursor) {
|
||||||
|
send(this.ptr, cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native void free(long self);
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("removal")
|
||||||
|
protected void finalize() throws Throwable {
|
||||||
|
free(this.ptr);
|
||||||
|
}
|
||||||
|
}
|
70
java/src/mp/code/Workspace.java
Normal file
70
java/src/mp/code/Workspace.java
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package mp.code;
|
||||||
|
|
||||||
|
import mp.code.exceptions.CodeMPLibException;
|
||||||
|
|
||||||
|
public class Workspace {
|
||||||
|
private final long ptr;
|
||||||
|
|
||||||
|
Workspace(long ptr) {
|
||||||
|
this.ptr = ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native String get_workspace_id(long self);
|
||||||
|
public String getWorkspaceId() {
|
||||||
|
return get_workspace_id(this.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native long get_cursor(long self);
|
||||||
|
public CursorController getCursor() {
|
||||||
|
return new CursorController(get_cursor(this.ptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native long get_buffer(long self, String path);
|
||||||
|
public BufferController getBuffer(String path) {
|
||||||
|
return new BufferController(get_buffer(this.ptr, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native void get_file_tree(long self);
|
||||||
|
public void getFileTree() {
|
||||||
|
// TODO vector?
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native long create_buffer(String path) throws CodeMPLibException;
|
||||||
|
public BufferController createBuffer(String path) throws CodeMPLibException {
|
||||||
|
return new BufferController(create_buffer(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native long attach_to_buffer(long self) throws CodeMPLibException;
|
||||||
|
public BufferController attachToBuffer() throws CodeMPLibException {
|
||||||
|
return new BufferController(attach_to_buffer(ptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native void fetch_buffers(long self) throws CodeMPLibException;
|
||||||
|
public void fetchBuffers() throws CodeMPLibException {
|
||||||
|
fetch_buffers(this.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native void fetch_users(long self) throws CodeMPLibException;
|
||||||
|
public void fetchUsers() throws CodeMPLibException {
|
||||||
|
fetch_buffers(this.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native void list_buffer_users(long self, String path) throws CodeMPLibException;
|
||||||
|
public void listBufferUsers(String path) throws CodeMPLibException {
|
||||||
|
// TODO pass vector
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native void delete_buffer(long self, String path) throws CodeMPLibException;
|
||||||
|
public void deleteBuffer(String path) throws CodeMPLibException {
|
||||||
|
delete_buffer(this.ptr, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO select_buffer
|
||||||
|
|
||||||
|
private static native void free(long self);
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("removal")
|
||||||
|
protected void finalize() throws Throwable {
|
||||||
|
free(this.ptr);
|
||||||
|
}
|
||||||
|
}
|
16
java/src/mp/code/data/Cursor.java
Normal file
16
java/src/mp/code/data/Cursor.java
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package mp.code.data;
|
||||||
|
|
||||||
|
public class Cursor {
|
||||||
|
public final int startRow, startCol, endRow, endCol;
|
||||||
|
public final String buffer;
|
||||||
|
public final String user;
|
||||||
|
|
||||||
|
public Cursor(int startRow, int startCol, int endRow, int endCol, String buffer, String user) {
|
||||||
|
this.startRow = startRow;
|
||||||
|
this.startCol = startCol;
|
||||||
|
this.endRow = endRow;
|
||||||
|
this.endCol = endCol;
|
||||||
|
this.buffer = buffer;
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
}
|
13
java/src/mp/code/data/TextChange.java
Normal file
13
java/src/mp/code/data/TextChange.java
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package mp.code.data;
|
||||||
|
|
||||||
|
public class TextChange {
|
||||||
|
public final long start;
|
||||||
|
public final long end;
|
||||||
|
public final String content;
|
||||||
|
|
||||||
|
public TextChange(long start, long end, String content) {
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
}
|
7
java/src/mp/code/exceptions/ChannelException.java
Normal file
7
java/src/mp/code/exceptions/ChannelException.java
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package mp.code.exceptions;
|
||||||
|
|
||||||
|
public class ChannelException extends CodeMPLibException {
|
||||||
|
public ChannelException(String input) {
|
||||||
|
super(input);
|
||||||
|
}
|
||||||
|
}
|
10
java/src/mp/code/exceptions/CodeMPLibException.java
Normal file
10
java/src/mp/code/exceptions/CodeMPLibException.java
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package mp.code.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic class for all our exceptions coming through the JNI from the library.
|
||||||
|
*/
|
||||||
|
public abstract class CodeMPLibException extends Exception {
|
||||||
|
protected CodeMPLibException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
7
java/src/mp/code/exceptions/DeadlockedException.java
Normal file
7
java/src/mp/code/exceptions/DeadlockedException.java
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package mp.code.exceptions;
|
||||||
|
|
||||||
|
public class DeadlockedException extends CodeMPLibException {
|
||||||
|
public DeadlockedException(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
|
}
|
7
java/src/mp/code/exceptions/InvalidStateException.java
Normal file
7
java/src/mp/code/exceptions/InvalidStateException.java
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package mp.code.exceptions;
|
||||||
|
|
||||||
|
public class InvalidStateException extends CodeMPLibException {
|
||||||
|
public InvalidStateException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
7
java/src/mp/code/exceptions/TransportException.java
Normal file
7
java/src/mp/code/exceptions/TransportException.java
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package mp.code.exceptions;
|
||||||
|
|
||||||
|
public class TransportException extends CodeMPLibException {
|
||||||
|
public TransportException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
86
src/ffi/java/client.rs
Normal file
86
src/ffi/java/client.rs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
use jni::{objects::{JClass, JString}, sys::jlong, JNIEnv};
|
||||||
|
use crate::{client::Client, Workspace};
|
||||||
|
|
||||||
|
use super::{util::JExceptable, RT};
|
||||||
|
|
||||||
|
/// Called by the Java GC to drop a [Client].
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_mp_code_Client_free(_env: JNIEnv, _class: JClass, input: jlong) {
|
||||||
|
super::util::dereference_and_drop::<Client>(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets up tracing subscriber
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_mp_code_Client_setup_1tracing<'local>(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass<'local>,
|
||||||
|
path: JString<'local>
|
||||||
|
) {
|
||||||
|
let path: Option<String> = if path.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(env.get_string(&path).expect("Couldn't get java string!").into())
|
||||||
|
};
|
||||||
|
|
||||||
|
super::setup_logger(true, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects to a given URL and returns a [Client] to interact with that server.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_mp_code_Client_connect<'local>(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass<'local>,
|
||||||
|
input: JString<'local>
|
||||||
|
) -> jlong {
|
||||||
|
let url: String = env.get_string(&input).expect("Couldn't get java string!").into();
|
||||||
|
RT.block_on(crate::Client::new(&url))
|
||||||
|
.map(|client| Box::into_raw(Box::new(client)) as jlong)
|
||||||
|
.jexcept(&mut env)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Logs in to a specific [Workspace].
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_mp_code_Client_login<'local>(
|
||||||
|
mut env: JNIEnv<'local>,
|
||||||
|
_class: JClass<'local>,
|
||||||
|
self_ptr: jlong,
|
||||||
|
user: JString<'local>,
|
||||||
|
pwd: JString<'local>,
|
||||||
|
workspace: JString<'local>
|
||||||
|
) {
|
||||||
|
let client = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Client)) };
|
||||||
|
let user: String = env.get_string(&user).expect("Couldn't get java string!").into();
|
||||||
|
let pwd: String = env.get_string(&pwd).expect("Couldn't get java string!").into();
|
||||||
|
let workspace: String = env.get_string(&workspace).expect("Couldn't get java string!").into();
|
||||||
|
RT.block_on(client.login(user, pwd, Some(workspace)))
|
||||||
|
.jexcept(&mut env)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Joins a [Workspace] and returns a pointer to it.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_mp_code_Client_join_1workspace<'local>(
|
||||||
|
mut env: JNIEnv<'local>,
|
||||||
|
_class: JClass<'local>,
|
||||||
|
self_ptr: jlong,
|
||||||
|
input: JString<'local>
|
||||||
|
) -> jlong {
|
||||||
|
let client = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Client)) };
|
||||||
|
let workspace_id = unsafe { env.get_string_unchecked(&input).expect("Couldn't get java string!") };
|
||||||
|
RT.block_on(client.join_workspace(workspace_id.to_str().expect("Not UTF-8")))
|
||||||
|
.map(|workspace| spawn_updater(workspace.clone()))
|
||||||
|
.map(|workspace| Box::into_raw(Box::new(workspace)) as jlong)
|
||||||
|
.jexcept(&mut env)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this stays until we get rid of the arc then i'll have to find a better way
|
||||||
|
fn spawn_updater(workspace: Workspace) -> Workspace {
|
||||||
|
let w = workspace.clone();
|
||||||
|
RT.spawn(async move {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
|
||||||
|
w.fetch_buffers().await.unwrap();
|
||||||
|
w.fetch_users().await.unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
workspace
|
||||||
|
}
|
|
@ -1,333 +1,33 @@
|
||||||
use api::Controller;
|
pub mod client;
|
||||||
use codemp_proto::{cursor::{RowCol, CursorPosition}, files::BufferNode};
|
pub mod workspace;
|
||||||
use rifgen::rifgen_attr::{generate_access_methods, generate_interface, generate_interface_doc};
|
pub mod util;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Client,
|
|
||||||
Error,
|
|
||||||
Result,
|
|
||||||
api::TextChange,
|
|
||||||
buffer::{self, tools},
|
|
||||||
cursor,
|
|
||||||
workspace::Workspace
|
|
||||||
};
|
|
||||||
|
|
||||||
//rifgen generated code
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/glue.rs"));
|
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
/// the tokio runtime, since we can't easily have Java and Rust async work together
|
pub(crate) static ref RT: tokio::runtime::Runtime = tokio::runtime::Runtime::new().expect("could not create tokio runtime");
|
||||||
static ref RT: tokio::runtime::Runtime = tokio::runtime::Runtime::new()
|
|
||||||
.expect("could not start tokio runtime");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[generate_interface_doc]
|
pub(crate) fn setup_logger(debug: bool, path: Option<String>) {
|
||||||
/// the handler class that represent an instance of a CodeMP client
|
let format = tracing_subscriber::fmt::format()
|
||||||
struct ClientHandler {
|
.with_level(true)
|
||||||
client: Client,
|
.with_target(true)
|
||||||
url: String,
|
.with_thread_ids(false)
|
||||||
}
|
.with_thread_names(false)
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_file(false)
|
||||||
|
.with_line_number(false)
|
||||||
|
.with_source_location(false)
|
||||||
|
.compact();
|
||||||
|
|
||||||
impl ClientHandler {
|
let level = if debug { tracing::Level::DEBUG } else {tracing::Level::INFO };
|
||||||
#[generate_interface(constructor)]
|
|
||||||
/// construct a new [ClientHandler]
|
|
||||||
fn new(address: &str) -> ClientHandler {
|
|
||||||
ClientHandler {
|
|
||||||
client: RT.block_on(Client::new(address)).unwrap(),
|
|
||||||
url: address.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
let builder = tracing_subscriber::fmt()
|
||||||
/// join a workspace by name
|
.event_format(format)
|
||||||
fn join_workspace(&mut self, workspace_id: &str) -> Result<WorkspaceHandler> {
|
.with_max_level(level);
|
||||||
RT.block_on(self.client.join_workspace(workspace_id))
|
|
||||||
.map(|workspace| {
|
|
||||||
Self::spawn_updater(workspace.clone());
|
|
||||||
WorkspaceHandler { workspace }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn_updater(workspace: Arc<Workspace>) {
|
if let Some(path) = path {
|
||||||
tokio::spawn(async move {
|
let logfile = std::fs::File::create(path).expect("failed creating logfile");
|
||||||
loop {
|
builder.with_writer(std::sync::Mutex::new(logfile)).init();
|
||||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
} else {
|
||||||
workspace.fetch_buffers().await.unwrap();
|
builder.with_writer(std::sync::Mutex::new(std::io::stdout())).init();
|
||||||
workspace.fetch_users().await.unwrap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// get the url you are currently connected to
|
|
||||||
fn get_url(&self) -> String {
|
|
||||||
self.url.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface_doc]
|
|
||||||
/// wraps a [codemp::workspace::Workspace] to be handled by Java
|
|
||||||
struct WorkspaceHandler {
|
|
||||||
workspace: Arc<Workspace>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WorkspaceHandler { // TODO: workspace leave / buffer detach ?
|
|
||||||
#[generate_interface(constructor)]
|
|
||||||
/// constructor required by flapigen, DO NOT CALL THIS
|
|
||||||
fn new() -> WorkspaceHandler {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// create a new buffer in current workspace
|
|
||||||
fn create_buffer(&mut self, path: &str) -> Result<BufferHandler> {
|
|
||||||
RT.block_on(self.workspace.create(path))?;
|
|
||||||
Ok(self.get_buffer(path).unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// attach to a buffer and get a [crate::BufferHandler] for it
|
|
||||||
fn attach_to_buffer(&mut self, path: &str) -> Result<BufferHandler> {
|
|
||||||
RT.block_on(self.workspace.attach(path))
|
|
||||||
.map(|buffer| BufferHandler { buffer })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// updates the local list of the workspace's buffers
|
|
||||||
fn fetch_buffers(&mut self) -> Result<()> {
|
|
||||||
RT.block_on(self.workspace.fetch_buffers())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// updates the local list of the workspace's users
|
|
||||||
fn fetch_users(&mut self) -> Result<()> {
|
|
||||||
RT.block_on(self.workspace.fetch_users())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// gets a list of all users in a buffer
|
|
||||||
fn list_buffer_users(&mut self, path: &str) -> Result<StringVec> {
|
|
||||||
let mut res = StringVec::new();
|
|
||||||
RT.block_on(self.workspace.list_buffer_users(path))?
|
|
||||||
.iter()
|
|
||||||
.for_each(|u| res.push(Uuid::from(u.clone()).to_string()));
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// delete a buffer
|
|
||||||
fn delete_buffer(&mut self, path: &str) -> Result<()> {
|
|
||||||
RT.block_on(self.workspace.delete(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// get the workspace id
|
|
||||||
fn get_workspace_id(&self) -> String {
|
|
||||||
self.workspace.id().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// get a [crate::CursorHandler] for the workspace's cursor
|
|
||||||
fn get_cursor(&self) -> CursorHandler {
|
|
||||||
CursorHandler {
|
|
||||||
cursor: self.workspace.cursor().clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// get a [crate::BufferHandler] for one of the workspace's buffers
|
|
||||||
fn get_buffer(&self, path: &str) -> Option<BufferHandler> {
|
|
||||||
self.workspace
|
|
||||||
.buffer_by_name(path)
|
|
||||||
.map(|buffer| BufferHandler { buffer })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// get the names of all buffers available in the workspace
|
|
||||||
fn get_filetree(&self) -> StringVec {
|
|
||||||
StringVec {
|
|
||||||
v: self.workspace.filetree()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// polls a list of buffers, returning the first ready one
|
|
||||||
fn select_buffer(
|
|
||||||
&mut self,
|
|
||||||
mut buffer_ids: StringVec,
|
|
||||||
timeout: i64,
|
|
||||||
) -> Result<Option<BufferHandler>> {
|
|
||||||
let mut buffers = Vec::new();
|
|
||||||
for id in buffer_ids.v.iter_mut() {
|
|
||||||
match self.get_buffer(id.as_str()) {
|
|
||||||
Some(buf) => buffers.push(buf.buffer),
|
|
||||||
None => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = RT.block_on(tools::select_buffer(
|
|
||||||
buffers.as_slice(),
|
|
||||||
Some(Duration::from_millis(timeout as u64)),
|
|
||||||
));
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Err(e) => Err(e),
|
|
||||||
Ok(buffer) => Ok(buffer.map(|buffer| BufferHandler { buffer })),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface_doc]
|
|
||||||
#[generate_access_methods]
|
|
||||||
/// wraps a [codemp::proto::cursor::CursorEvent] to be handled by Java
|
|
||||||
struct CursorEventWrapper {
|
|
||||||
user: String,
|
|
||||||
buffer: String,
|
|
||||||
start_row: i32,
|
|
||||||
start_col: i32,
|
|
||||||
end_row: i32,
|
|
||||||
end_col: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface_doc]
|
|
||||||
/// a handler providing Java access to [codemp::cursor::Controller] methods
|
|
||||||
struct CursorHandler {
|
|
||||||
pub cursor: Arc<cursor::Controller>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CursorHandler {
|
|
||||||
#[generate_interface(constructor)]
|
|
||||||
/// constructor required by flapigen, DO NOT CALL THIS
|
|
||||||
fn new() -> CursorHandler {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// get next cursor event from current workspace, or block until one is available
|
|
||||||
fn recv(&self) -> Result<CursorEventWrapper> {
|
|
||||||
match RT.block_on(self.cursor.recv()) {
|
|
||||||
Err(err) => Err(err),
|
|
||||||
Ok(event) => Ok(CursorEventWrapper {
|
|
||||||
user: Uuid::from(event.user).to_string(),
|
|
||||||
buffer: event.position.buffer.path.clone(),
|
|
||||||
start_row: event.position.start.row,
|
|
||||||
start_col: event.position.start.col,
|
|
||||||
end_row: event.position.end.row,
|
|
||||||
end_col: event.position.end.col,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// broadcast a cursor event
|
|
||||||
/// will automatically fix start and end if they are accidentally inverted
|
|
||||||
fn send(
|
|
||||||
&self,
|
|
||||||
buffer: String,
|
|
||||||
start_row: i32,
|
|
||||||
start_col: i32,
|
|
||||||
end_row: i32,
|
|
||||||
end_col: i32,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.cursor.send(CursorPosition {
|
|
||||||
buffer: BufferNode { path: buffer },
|
|
||||||
start: RowCol::from((start_row, start_col)),
|
|
||||||
end: RowCol::from((end_row, end_col)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface_doc]
|
|
||||||
#[generate_access_methods]
|
|
||||||
/// wraps a [codemp::api::change::TextChange] to make it accessible from Java
|
|
||||||
struct TextChangeWrapper {
|
|
||||||
start: usize,
|
|
||||||
end: usize, //not inclusive
|
|
||||||
content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface_doc]
|
|
||||||
/// a handler providing Java access to [codemp::buffer::Controller] methods
|
|
||||||
struct BufferHandler {
|
|
||||||
pub buffer: Arc<buffer::Controller>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BufferHandler {
|
|
||||||
#[generate_interface(constructor)]
|
|
||||||
/// constructor required by flapigen, DO NOT CALL THIS
|
|
||||||
fn new() -> BufferHandler {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// get the name of the buffer
|
|
||||||
fn get_name(&self) -> String {
|
|
||||||
self.buffer.name().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// get the contents of the buffer
|
|
||||||
fn get_content(&self) -> String {
|
|
||||||
self.buffer.content()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// if a text change is available on the buffer, return it immediately
|
|
||||||
fn try_recv(&self) -> Result<Option<TextChangeWrapper>> {
|
|
||||||
match self.buffer.try_recv() {
|
|
||||||
Err(err) => Err(err),
|
|
||||||
Ok(None) => Ok(None),
|
|
||||||
Ok(Some(change)) => Ok(Some(TextChangeWrapper {
|
|
||||||
start: change.span.start,
|
|
||||||
end: change.span.end,
|
|
||||||
content: change.content.clone(),
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// broadcast a text change on the buffer
|
|
||||||
fn send(&self, start_offset: usize, end_offset: usize, content: String) -> Result<()> {
|
|
||||||
self.buffer.send(TextChange {
|
|
||||||
span: start_offset..end_offset,
|
|
||||||
content,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface_doc]
|
|
||||||
/// a convenience struct allowing Java access to a Rust vector
|
|
||||||
struct StringVec {
|
|
||||||
//jni moment
|
|
||||||
v: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StringVec {
|
|
||||||
#[generate_interface(constructor)]
|
|
||||||
/// initialize an empty vector
|
|
||||||
fn new() -> StringVec {
|
|
||||||
Self { v: Vec::new() }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// push a new value onto the vector
|
|
||||||
fn push(&mut self, s: String) {
|
|
||||||
self.v.push(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// get the length of the underlying vector
|
|
||||||
fn length(&self) -> i64 {
|
|
||||||
self.v.len() as i64
|
|
||||||
}
|
|
||||||
|
|
||||||
#[generate_interface]
|
|
||||||
/// access the element at a given index
|
|
||||||
fn get(&self, idx: i64) -> Option<String> {
|
|
||||||
let elem: Option<&String> = self.v.get(idx as usize);
|
|
||||||
elem.map(|s| s.clone())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
foreign_typemap!( //thanks @tasn on GitHub for the idea
|
|
||||||
($p:r_type) <T> Result<T> => swig_i_type!(T) {
|
|
||||||
$out = match $p {
|
|
||||||
Ok(x) => {
|
|
||||||
swig_from_rust_to_i_type!(T, x, ret)
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let (msg, exception_class) = match err {
|
|
||||||
Error::Filler { message } => (
|
|
||||||
message,
|
|
||||||
swig_jni_find_class!(CODEMP_EXCEPTION, "com/codemp/intellij/exceptions/CodeMPException")
|
|
||||||
),
|
|
||||||
Error::Transport { status, message } => (
|
|
||||||
format!("Status {}: {}", status, message),
|
|
||||||
swig_jni_find_class!(TRANSPORT_EXCEPTION, "com/codemp/intellij/exceptions/lib/TransportException")
|
|
||||||
),
|
|
||||||
Error::InvalidState { msg } => (
|
|
||||||
msg, swig_jni_find_class!(INVALID_STATE_EXCEPTION, "com/codemp/intellij/exceptions/lib/InvalidStateException")
|
|
||||||
),
|
|
||||||
Error::Deadlocked => (
|
|
||||||
"WOOT deadlocked (safe to retry)!".to_string(),
|
|
||||||
swig_jni_find_class!(DEADLOCKED_EXCEPTION, "com/codemp/intellij/exceptions/lib/DeadlockedException")
|
|
||||||
),
|
|
||||||
Error::Channel { send } => {
|
|
||||||
let verb = if send { "sending" } else { "reading" };
|
|
||||||
(
|
|
||||||
format!("Error while {} message on channel: the channel was closed!", verb),
|
|
||||||
swig_jni_find_class!(CHANNEL_EXCEPTION, "com/codemp/intellij/exceptions/lib/ChannelException")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
jni_throw(env, exception_class, &msg);
|
|
||||||
return <swig_i_type!(T)>::jni_invalid_value();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
($p:f_type, unique_prefix="/*err*/") => "/*err*/swig_f_type!(T)" "swig_foreign_from_i_type!(T, $p)";
|
|
||||||
);
|
|
29
src/ffi/java/util.rs
Normal file
29
src/ffi/java/util.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use jni::{JNIEnv, sys::jlong};
|
||||||
|
|
||||||
|
/// A simple utility method that converts a pointer back into a [Box] and then drops it.
|
||||||
|
pub(crate) fn dereference_and_drop<T>(ptr: jlong) {
|
||||||
|
let client : Box<T> = unsafe { Box::from_raw(ptr as *mut T) };
|
||||||
|
std::mem::drop(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait meant for our [crate::Result] type to make converting it to Java easier.
|
||||||
|
pub(crate) trait JExceptable<T> {
|
||||||
|
/// Unwraps it and throws an appropriate Java exception if it's an error.
|
||||||
|
/// Theoretically it returns the type's default value, but the exception makes the value ignored.
|
||||||
|
fn jexcept(self, env: &mut JNIEnv) -> T;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> JExceptable<T> for crate::Result<T> where T: Default {
|
||||||
|
fn jexcept(self, env: &mut JNIEnv) -> T {
|
||||||
|
if let Err(err) = &self {
|
||||||
|
let msg = format!("{err}");
|
||||||
|
match err {
|
||||||
|
crate::Error::InvalidState { .. } => env.throw_new("mp/code/exceptions/InvalidStateException", msg),
|
||||||
|
crate::Error::Deadlocked => env.throw_new("mp/code/exceptions/DeadlockedException", msg),
|
||||||
|
crate::Error::Transport { .. } => env.throw_new("mp/code/exceptions/TransportException", msg),
|
||||||
|
crate::Error::Channel { .. } => env.throw_new("mp/code/exceptions/ChannelException", msg)
|
||||||
|
}.expect("Failed to throw exception!");
|
||||||
|
}
|
||||||
|
self.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
23
src/ffi/java/workspace.rs
Normal file
23
src/ffi/java/workspace.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use jni::{objects::{JClass, JString}, sys::jlong, JNIEnv};
|
||||||
|
use crate::{Client, Workspace};
|
||||||
|
|
||||||
|
use super::{util::JExceptable, RT};
|
||||||
|
|
||||||
|
/// Called by the Java GC to drop a [Workspace].
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_mp_code_Workspace_free(_env: JNIEnv, _class: JClass, input: jlong) {
|
||||||
|
super::util::dereference_and_drop::<Client>(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [Buffer]
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_mp_code_Workspace_create_1buffer<'local>(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass<'local>,
|
||||||
|
self_ptr: jlong,
|
||||||
|
input: JString<'local>
|
||||||
|
) {
|
||||||
|
let ws: Box<Workspace> = unsafe { Box::from_raw(self_ptr as *mut Workspace) };
|
||||||
|
let path: String = env.get_string(&input).expect("Couldn't get java string!").into();
|
||||||
|
RT.block_on(ws.create(&path)).jexcept(&mut env);
|
||||||
|
}
|
Loading…
Reference in a new issue