Add support for setting language

This commit is contained in:
Eric Zhang 2021-06-03 17:24:48 -05:00
parent 06445802da
commit ff9069eda5
4 changed files with 125 additions and 7 deletions

View file

@ -9,15 +9,20 @@ use log::{info, warn};
use operational_transform::OperationSeq;
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use serde::{Deserialize, Serialize};
use tokio::{sync::Notify, time};
use tokio::sync::{broadcast, Notify};
use tokio::time;
use warp::ws::{Message, WebSocket};
/// The main object for a collaborative session.
#[derive(Default)]
pub struct Rustpad {
/// State modified by critical sections of the code.
state: RwLock<State>,
/// Incremented to obtain unique user IDs.
count: AtomicU64,
/// Used to notify clients of new text operations.
notify: Notify,
/// Used to inform all clients of metadata updates.
update: broadcast::Sender<ServerMsg>,
}
/// Shared state involving multiple users, protected by a lock.
@ -25,6 +30,7 @@ pub struct Rustpad {
struct State {
operations: Vec<UserOperation>,
text: String,
language: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -41,6 +47,8 @@ enum ClientMsg {
revision: usize,
operation: OperationSeq,
},
/// Set the language of the editor.
SetLanguage(String),
}
/// A message sent to the client over WebSocket.
@ -53,6 +61,8 @@ enum ServerMsg {
start: usize,
operations: Vec<UserOperation>,
},
/// Broadcasts the current language, last writer wins.
Language(String),
}
impl From<ServerMsg> for Message {
@ -62,6 +72,18 @@ impl From<ServerMsg> for Message {
}
}
impl Default for Rustpad {
fn default() -> Self {
let (tx, _) = broadcast::channel(1);
Self {
state: Default::default(),
count: Default::default(),
notify: Default::default(),
update: tx,
}
}
}
impl Rustpad {
/// Handle a connection from a WebSocket.
pub async fn on_connection(&self, socket: WebSocket) {
@ -86,8 +108,9 @@ impl Rustpad {
}
async fn handle_connection(&self, id: u64, mut socket: WebSocket) -> Result<()> {
socket.send(ServerMsg::Identity(id).into()).await?;
let mut update_rx = self.update.subscribe();
self.send_initial(id, &mut socket).await?;
let mut revision: usize = 0;
loop {
@ -100,6 +123,9 @@ impl Rustpad {
tokio::select! {
_ = &mut sleep => {}
_ = self.notify.notified() => {}
update = update_rx.recv() => {
socket.send(update?.into()).await?;
}
result = socket.next() => {
match result {
None => break,
@ -114,6 +140,21 @@ impl Rustpad {
Ok(())
}
async fn send_initial(&self, id: u64, socket: &mut WebSocket) -> Result<()> {
socket.send(ServerMsg::Identity(id).into()).await?;
let mut messages = Vec::new();
{
let state = self.state.read();
if let Some(language) = &state.language {
messages.push(ServerMsg::Language(language.clone()));
}
};
for msg in messages {
socket.send(msg.into()).await?;
}
Ok(())
}
async fn send_history(&self, start: usize, socket: &mut WebSocket) -> Result<usize> {
let operations = {
let state = self.state.read();
@ -146,6 +187,10 @@ impl Rustpad {
.context("invalid edit operation")?;
self.notify.notify_waiters();
}
ClientMsg::SetLanguage(language) => {
self.state.write().language = Some(language.clone());
self.update.send(ServerMsg::Language(language)).ok();
}
}
Ok(())
}

View file

@ -195,3 +195,36 @@ async fn test_concurrent_transform() -> Result<()> {
expect_text(&filter, "foobar", "~rust~henlo").await;
Ok(())
}
#[tokio::test]
async fn test_set_language() -> Result<()> {
pretty_env_logger::try_init().ok();
let filter = server();
let mut client = connect(&filter, "foobar").await?;
let msg = client.recv().await?;
assert_eq!(msg, json!({ "Identity": 0 }));
let msg = json!({ "SetLanguage": "javascript" });
client.send(&msg).await;
let msg = client.recv().await?;
assert_eq!(msg, json!({ "Language": "javascript" }));
let mut client2 = connect(&filter, "foobar").await?;
let msg = client2.recv().await?;
assert_eq!(msg, json!({ "Identity": 1 }));
let msg = client2.recv().await?;
assert_eq!(msg, json!({ "Language": "javascript" }));
let msg = json!({ "SetLanguage": "python" });
client2.send(&msg).await;
let msg = client.recv().await?;
assert_eq!(msg, json!({ "Language": "python" }));
let msg = client2.recv().await?;
assert_eq!(msg, json!({ "Language": "python" }));
expect_text(&filter, "foobar", "").await;
Ok(())
}

View file

@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { set_panic_hook } from "rustpad-wasm";
import {
Box,
@ -48,13 +48,14 @@ function App() {
const [connection, setConnection] =
useState<"connected" | "disconnected" | "desynchronized">("disconnected");
const [editor, setEditor] = useState<editor.IStandaloneCodeEditor>();
const rustpad = useRef<Rustpad>();
useEffect(() => {
if (editor) {
const model = editor.getModel()!;
model.setValue("");
model.setEOL(0); // LF
const rustpad = new Rustpad({
rustpad.current = new Rustpad({
uri: wsUri,
editor,
onConnected: () => setConnection("connected"),
@ -68,11 +69,40 @@ function App() {
duration: null,
});
},
onChangeLanguage: (language) => {
if (languages.includes(language)) {
setLanguage(language);
}
},
});
return () => rustpad.dispose();
return () => {
rustpad.current?.dispose();
rustpad.current = undefined;
};
}
}, [editor, toast]);
function handleChangeLanguage(language: string) {
setLanguage(language);
if (rustpad.current?.setLanguage(language)) {
toast({
title: "Language updated",
description: (
<>
All users are now editing in{" "}
<Text as="span" fontWeight="semibold">
{language}
</Text>
.
</>
),
status: "info",
duration: 2000,
isClosable: true,
});
}
}
async function handleCopy() {
await navigator.clipboard.writeText(`${window.location.origin}/#${id}`);
toast({
@ -128,7 +158,7 @@ function App() {
size="sm"
bgColor="white"
value={language}
onChange={(event) => setLanguage(event.target.value)}
onChange={(event) => handleChangeLanguage(event.target.value)}
>
{languages.map((lang) => (
<option key={lang} value={lang}>

View file

@ -8,6 +8,7 @@ export type RustpadOptions = {
readonly onConnected?: () => unknown;
readonly onDisconnected?: () => unknown;
readonly onDesynchronized?: () => unknown;
readonly onChangeLanguage?: (language: string) => unknown;
readonly reconnectInterval?: number;
};
@ -65,6 +66,12 @@ class Rustpad {
this.ws?.close();
}
/** Try to set the language of the editor, if connected. */
setLanguage(language: string): boolean {
this.ws?.send(`{"SetLanguage":${JSON.stringify(language)}}`);
return this.ws !== undefined;
}
/**
* Attempts a WebSocket connection.
*
@ -129,6 +136,8 @@ class Rustpad {
this.applyServer(operation);
}
}
} else if (msg.Language !== undefined) {
this.options.onChangeLanguage?.(msg.Language);
}
}
@ -271,6 +280,7 @@ type ServerMsg = {
start: number;
operations: UserOperation[];
};
Language?: string;
};
export default Rustpad;