Add support for setting language
This commit is contained in:
parent
06445802da
commit
ff9069eda5
4 changed files with 125 additions and 7 deletions
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
38
src/App.tsx
38
src/App.tsx
|
@ -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}>
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue