mirror of
https://github.com/hexedtech/codemp-vscode.git
synced 2024-11-24 00:14:48 +01:00
merged in a single folder, rust bindings napi + vscode extension
This commit is contained in:
parent
b842d5b64c
commit
9b281b1b4c
5 changed files with 424 additions and 0 deletions
104
src/extension.ts
Normal file
104
src/extension.ts
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
|
||||||
|
|
||||||
|
vscode
|
||||||
|
+ src
|
||||||
|
+ glue.rs
|
||||||
|
+ extension.ts
|
||||||
|
+ Cargo.toml
|
||||||
|
+ package.json
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// The module 'vscode' contains the VS Code extensibility API
|
||||||
|
// Import the module and reference it with the alias vscode in your code below
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
//import * as codempp from '/home/***REMOVED***/projects/codemp/mine/codempvscode/codemp.node';
|
||||||
|
const codemp = require("/home/***REMOVED***/projects/codemp/mine/vscode/target/debug/libcodemp_vscode.node");
|
||||||
|
// import * as codemp from "/home/***REMOVED***/projects/codemp/mine/vscode/target/debug/libcodemp_vscode.node";
|
||||||
|
|
||||||
|
// This method is called when your extension is activated
|
||||||
|
// Your extension is activated the very first time the command is executed
|
||||||
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
|
|
||||||
|
// Use the console to output diagnostic information (console.log) and errors (console.error)
|
||||||
|
// This line of code will only be executed once when your extension is activated
|
||||||
|
console.log('Congratulations, your extension "codempvscode" is now active!');
|
||||||
|
|
||||||
|
// The command has been defined in the package.json file
|
||||||
|
// Now provide the implementation of the command with registerCommand
|
||||||
|
// The commandId parameter must match the command field in package.json
|
||||||
|
let disposable = vscode.commands.registerCommand('codempvscode.helloWorld', () => {
|
||||||
|
// The code you place here will be executed every time your command is executed
|
||||||
|
// Display a message box to the user
|
||||||
|
vscode.window.showInformationMessage(process.cwd());
|
||||||
|
});
|
||||||
|
let connectCommand = vscode.commands.registerCommand('codempvscode.connect', connect);
|
||||||
|
let joinCommand = vscode.commands.registerCommand('codempvscode.join', join);
|
||||||
|
context.subscriptions.push(connectCommand);
|
||||||
|
context.subscriptions.push(joinCommand);
|
||||||
|
context.subscriptions.push(disposable);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
let host = await vscode.window.showInputBox({prompt: "server host (default to http://alemi.dev:50051)"})
|
||||||
|
if (host === undefined) return // user cancelled with ESC
|
||||||
|
if (host.length == 0) host = "http://alemi.dev:50051"
|
||||||
|
await codemp.connect(host);
|
||||||
|
vscode.window.showInformationMessage(`Connected to codemp ***REMOVED*** @[${host}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function join() {
|
||||||
|
let workspace = await vscode.window.showInputBox({prompt: "workspace to attach (default to default)"})
|
||||||
|
let buffer = await vscode.window.showInputBox({prompt: "buffer name for the file needed to update other clients cursors"})
|
||||||
|
if (workspace === undefined) return // user cancelled with ESC
|
||||||
|
if (workspace.length == 0) workspace = "default"
|
||||||
|
|
||||||
|
if (buffer === undefined) return // user cancelled with ESC
|
||||||
|
if (buffer.length == 0) workspace = "test"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let controller = await codemp.join(workspace)
|
||||||
|
controller.callback((event:any) => {
|
||||||
|
console.log(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
vscode.window.onDidChangeTextEditorSelection((event: vscode.TextEditorSelectionChangeEvent)=>{
|
||||||
|
if(event.kind==1 || event.kind ==2){
|
||||||
|
let buf = event.textEditor.document.uri.toString()
|
||||||
|
let selection = event.selections[0] // TODO there may be more than one cursor!!
|
||||||
|
//let anchor = [selection.anchor.line+1, selection.anchor.character]
|
||||||
|
//let position = [selection.active.line+1, selection.active.character+1]
|
||||||
|
let anchor = [selection.anchor.line, selection.anchor.character]
|
||||||
|
let position = [selection.active.line, selection.active.character+1]
|
||||||
|
console.log("Buffer from selection" + buffer+"\n");
|
||||||
|
console.log("selection " + selection+"\n");
|
||||||
|
console.log("Anchor selection" + anchor+"\n");
|
||||||
|
console.log("position selection" + position+"\n");
|
||||||
|
controller.send(buffer, anchor, position);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
vscode.window.showInformationMessage(`Connected to workspace @[${workspace}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*async function attach() {
|
||||||
|
let workspace = await vscode.window.showInputBox({prompt: "workspace to attach (default to default)"})
|
||||||
|
if (workspace === undefined) return // user cancelled with ESC
|
||||||
|
if (workspace.length == 0) workspace = "default"
|
||||||
|
await codemp.attach(workspace);
|
||||||
|
vscode.window.showInformationMessage(`Connected to codemp ***REMOVED*** @[${workspace}]`);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
// This method is called when your extension is deactivated
|
||||||
|
export function deactivate() {}
|
242
src/rust/lib.rs
Normal file
242
src/rust/lib.rs
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
#![deny(clippy::all)]
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use futures::prelude::*;
|
||||||
|
use codemp::{
|
||||||
|
prelude::*,
|
||||||
|
proto::{RowCol, CursorEvent},
|
||||||
|
buffer::factory::OperationFactory, ot::OperationSeq
|
||||||
|
};
|
||||||
|
use napi_derive::napi;
|
||||||
|
use napi::{CallContext, Status, threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunctionCallMode, ErrorStrategy::{CalleeHandled, Fatal}, ThreadsafeFunction}};
|
||||||
|
use napi::tokio::{self, fs};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct JsCodempError(CodempError);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
impl From::<JsCodempError> for napi::Error {
|
||||||
|
fn from(value: JsCodempError) -> Self {
|
||||||
|
napi::Error::new(Status::GenericFailure, &format!("CodempError: {:?}", value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub async fn connect(addr: String) -> napi::Result<()> {
|
||||||
|
let f = std::fs::File::create("/home/***REMOVED***/projects/codemp/mine/vscode/log.txt").unwrap();
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_max_level(tracing::Level::INFO)
|
||||||
|
.with_writer(std::sync::Mutex::new(f))
|
||||||
|
.init();
|
||||||
|
CODEMP_INSTANCE.connect(&addr).await
|
||||||
|
.map_err(|e| JsCodempError(e).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// CURSOR
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub async fn join(session: String) -> napi::Result<JsCursorController> {
|
||||||
|
let controller = CODEMP_INSTANCE.join(&session).await
|
||||||
|
.map_err(|e| napi::Error::from(JsCodempError(e)))?;
|
||||||
|
Ok(controller.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub struct JsCursorController(Arc<CodempCursorController>);
|
||||||
|
|
||||||
|
impl From::<Arc<CodempCursorController>> for JsCursorController {
|
||||||
|
fn from(value: Arc<CodempCursorController>) -> Self {
|
||||||
|
JsCursorController(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl JsCursorController {
|
||||||
|
|
||||||
|
|
||||||
|
/*#[napi]
|
||||||
|
pub fn call_threadsafe_recv(callback: JsFunction) -> Result<()>{
|
||||||
|
let tsfn: ThreadsafeFunction<u32, ErrorStrategy::CalleeHandled> =
|
||||||
|
callback.create_threadsafe_function(0, |ctx| Ok(vec![ctx.value + 1]))?;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
#[napi(ts_args_type = "fun: (event: JsCursorEvent) => void")]
|
||||||
|
pub fn callback(&self, fun: napi::JsFunction) -> napi::Result<()>{ //TODO it sucks but v0.5 will improve it!!!
|
||||||
|
let tsfn : ThreadsafeFunction<CodempCursorEvent, Fatal> =
|
||||||
|
fun.create_threadsafe_function(0,
|
||||||
|
|ctx : ThreadSafeCallContext<CodempCursorEvent>| {
|
||||||
|
Ok(vec![JsCursorEvent::from(ctx.value)])
|
||||||
|
}
|
||||||
|
)?;
|
||||||
|
let _controller = self.0.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let event = _controller.recv().await.expect("could not receive cursor event!");
|
||||||
|
tracing::info!("printing '{:?}' event", event); // works?
|
||||||
|
tsfn.call(event.clone(), ThreadsafeFunctionCallMode::NonBlocking); //check this shit with tracing also we could use Ok(event) to get the error
|
||||||
|
tracing::info!("printing '{:?}' event after tsfn", event); // works?
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// let controller = codemp.join('default').await
|
||||||
|
// // TODO register cursor callback, when cursormoved call { controller.send(event) }
|
||||||
|
// controller.callback( (ev) => {
|
||||||
|
// editor.change(event.tex)
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// #[napi]
|
||||||
|
// pub async fn recv(&self) -> napi::Result<JsCursorEvent> {
|
||||||
|
// Ok(
|
||||||
|
// self.0.recv().await
|
||||||
|
// .map_err(|e| napi::Error::from(JsCodempError(e)))?
|
||||||
|
// .into()
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn send(&self, buffer: String, start: (i32, i32), end: (i32, i32)) -> napi::Result<()> {
|
||||||
|
let pos = CodempCursorPosition { buffer, start: Some(RowCol::from(start)), end: Some(RowCol::from(end)) };
|
||||||
|
self.0.send(pos)
|
||||||
|
.map_err(|e| napi::Error::from(JsCodempError(e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct JsCursorEvent {
|
||||||
|
pub user: String,
|
||||||
|
pub buffer: String,
|
||||||
|
pub start: JsRowCol,
|
||||||
|
pub end: JsRowCol,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From::<CursorEvent> for JsCursorEvent {
|
||||||
|
fn from(value: CursorEvent) -> Self {
|
||||||
|
let pos = value.position.unwrap_or_default();
|
||||||
|
let start = pos.start.unwrap_or_default();
|
||||||
|
let end = pos.end.unwrap_or_default();
|
||||||
|
JsCursorEvent {
|
||||||
|
user: value.user,
|
||||||
|
buffer: pos.buffer,
|
||||||
|
start: JsRowCol { row: start.row, col: start.col },
|
||||||
|
end: JsRowCol { row: end.row, col: end.col },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct JsRowCol {
|
||||||
|
pub row: i32,
|
||||||
|
pub col: i32
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From::<RowCol> for JsRowCol {
|
||||||
|
fn from(value: RowCol) -> Self {
|
||||||
|
JsRowCol { row: value.row, col: value.col }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// BUFFER
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct JsTextChange {
|
||||||
|
pub span: JSRange,
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct JSRange{
|
||||||
|
pub start: i32,
|
||||||
|
pub end: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From::<CodempTextChange> for JsTextChange {
|
||||||
|
fn from(value: CodempTextChange) -> Self {
|
||||||
|
JsTextChange {
|
||||||
|
// TODO how is x.. represented ? span.end can never be None
|
||||||
|
span: JSRange { start: value.span.start as i32, end: Some(value.span.end as i32) },
|
||||||
|
content: value.content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From::<OperationSeq> for JsCodempOperationSeq{
|
||||||
|
fn from(value: OperationSeq) -> Self {
|
||||||
|
JsCodempOperationSeq(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl From::<Arc<CodempBufferController>> for JsBufferController {
|
||||||
|
fn from(value: Arc<CodempBufferController>) -> Self {
|
||||||
|
JsBufferController(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub struct JsBufferController(Arc<CodempBufferController>);
|
||||||
|
|
||||||
|
#[napi(js_name = "CodempOperationSeq")]
|
||||||
|
pub struct JsCodempOperationSeq(CodempOperationSeq);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl JsBufferController {
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn delta(&self, start: i64, txt: String, end: i64) -> Option<JsCodempOperationSeq> {
|
||||||
|
self.0.delta(start as usize, &txt, end as usize).map(|x| x.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub async fn recv(&self) -> napi::Result<JsTextChange> {
|
||||||
|
Ok(
|
||||||
|
self.0.recv().await
|
||||||
|
.map_err(|e| napi::Error::from(JsCodempError(e)))?
|
||||||
|
.into()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn send(&self, op: &JsCodempOperationSeq) -> napi::Result<()> {
|
||||||
|
// TODO might be nice to take ownership of the opseq
|
||||||
|
self.0.send(op.0.clone())
|
||||||
|
.map_err(|e| napi::Error::from(JsCodempError(e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub async fn create(path: String, content: Option<String>) -> napi::Result<()> {
|
||||||
|
CODEMP_INSTANCE.create(&path, content.as_deref()).await
|
||||||
|
.map_err(|e| napi::Error::from(JsCodempError(e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub async fn attach(path: String) -> napi::Result<JsBufferController> {
|
||||||
|
Ok(
|
||||||
|
CODEMP_INSTANCE.attach(&path).await
|
||||||
|
.map_err(|e| napi::Error::from(JsCodempError(e)))?
|
||||||
|
.into()
|
||||||
|
)
|
||||||
|
}
|
23
src/test/runTest.ts
Normal file
23
src/test/runTest.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import { runTests } from '@vscode/test-electron';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
// The folder containing the Extension Manifest package.json
|
||||||
|
// Passed to `--extensionDevelopmentPath`
|
||||||
|
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
|
||||||
|
|
||||||
|
// The path to test runner
|
||||||
|
// Passed to --extensionTestsPath
|
||||||
|
const extensionTestsPath = path.resolve(__dirname, './suite/index');
|
||||||
|
|
||||||
|
// Download VS Code, unzip it and run the integration test
|
||||||
|
await runTests({ extensionDevelopmentPath, extensionTestsPath });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to run tests', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
15
src/test/suite/extension.test.ts
Normal file
15
src/test/suite/extension.test.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import * as assert from 'assert';
|
||||||
|
|
||||||
|
// You can import and use all API from the 'vscode' module
|
||||||
|
// as well as import your extension to test it
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
// import * as myExtension from '../../extension';
|
||||||
|
|
||||||
|
suite('Extension Test Suite', () => {
|
||||||
|
vscode.window.showInformationMessage('Start all tests.');
|
||||||
|
|
||||||
|
test('Sample test', () => {
|
||||||
|
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
|
||||||
|
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
|
||||||
|
});
|
||||||
|
});
|
40
src/test/suite/index.ts
Normal file
40
src/test/suite/index.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as Mocha from 'mocha';
|
||||||
|
import * as glob from 'glob';
|
||||||
|
|
||||||
|
export function run(): Promise<void> {
|
||||||
|
// Create the mocha test
|
||||||
|
const mocha = new Mocha({
|
||||||
|
ui: 'tdd',
|
||||||
|
color: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const testsRoot = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
return new Promise((c, e) => {
|
||||||
|
const testFiles = new glob.Glob("**/**.test.js", { cwd: testsRoot });
|
||||||
|
const testFileStream = testFiles.stream();
|
||||||
|
|
||||||
|
testFileStream.on("data", (file) => {
|
||||||
|
mocha.addFile(path.resolve(testsRoot, file));
|
||||||
|
});
|
||||||
|
testFileStream.on("error", (err) => {
|
||||||
|
e(err);
|
||||||
|
});
|
||||||
|
testFileStream.on("end", () => {
|
||||||
|
try {
|
||||||
|
// Run the mocha test
|
||||||
|
mocha.run(failures => {
|
||||||
|
if (failures > 0) {
|
||||||
|
e(new Error(`${failures} tests failed.`));
|
||||||
|
} else {
|
||||||
|
c();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
e(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue