use utf-16 for everything because of javascript
This commit is contained in:
parent
7e95a338de
commit
090f3bcb23
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -1084,6 +1084,12 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.3.1"
|
||||
@ -1246,4 +1252,5 @@ dependencies = [
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
@ -14,3 +14,4 @@ tokio = { version = "1.25.0", features = ["full"] }
|
||||
tower-http = { version = "0.3.5", features = ["trace"] }
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||
unicode-segmentation = "1.10.1"
|
||||
|
@ -16,6 +16,9 @@ let ws;
|
||||
const uuid = self.crypto.randomUUID();
|
||||
const wsUrl = "ws://localhost:3000/ws";
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
function setup() {
|
||||
area = document.querySelector(`#${areaId}`);
|
||||
ws = new WebSocket(wsUrl);
|
||||
@ -26,7 +29,7 @@ function setup() {
|
||||
|
||||
function setupUi() {
|
||||
document.addEventListener("selectionchange", onSelectionChange, false);
|
||||
area.addEventListener("input", onInput, false);
|
||||
area.addEventListener("beforeinput", onInput, false);
|
||||
}
|
||||
|
||||
function setupWs() {
|
||||
@ -40,6 +43,7 @@ function setupWs() {
|
||||
ws.onmessage = function (e) {
|
||||
let payload = JSON.parse(e.data);
|
||||
if (payload.client !== uuid) {
|
||||
console.log(payload.doc);
|
||||
area.value = payload.doc;
|
||||
}
|
||||
};
|
||||
@ -49,6 +53,7 @@ function onSelectionChange() {
|
||||
const activeElement = document.activeElement;
|
||||
|
||||
if (activeElement && activeElement.id === areaId) {
|
||||
console.debug("onSelectionChange", area);
|
||||
selectionStart = area.selectionStart;
|
||||
selectionEnd = area.selectionEnd;
|
||||
}
|
||||
@ -56,6 +61,15 @@ function onSelectionChange() {
|
||||
|
||||
function onInput(event) {
|
||||
if (!validEvents.includes(event.inputType)) return;
|
||||
|
||||
// workaround for differences between firefox and chrome
|
||||
// chrome does not fire selectionchange events on backspace/delete events,
|
||||
// while firefox does
|
||||
if (event.inputType === "deleteContentBackward") {
|
||||
selectionStart = area.selectionStart;
|
||||
selectionEnd = area.selectionEnd;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
client: uuid,
|
||||
action: event.inputType,
|
||||
@ -66,10 +80,7 @@ function onInput(event) {
|
||||
|
||||
ws.send(JSON.stringify(payload));
|
||||
|
||||
console.log(event.inputType);
|
||||
console.log(selectionStart);
|
||||
console.log(selectionEnd);
|
||||
console.log(event.data);
|
||||
console.log(selectionStart, selectionEnd);
|
||||
}
|
||||
|
||||
setup();
|
||||
|
@ -3,9 +3,7 @@ use std::sync::Arc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::actions::delete::{DeleteContentBackward, DeleteContentForward};
|
||||
use crate::actions::insert::InsertText;
|
||||
use crate::actions::linebreak::InsertLineBreak;
|
||||
use crate::actions::paste::InsertFromPaste;
|
||||
use crate::actions::insert::{InsertText, InsertFromPaste, InsertLineBreak};
|
||||
use crate::actions::ActionRunner;
|
||||
use crate::AppState;
|
||||
|
||||
@ -40,7 +38,11 @@ impl Action {
|
||||
ActionType::DeleteContentForward => Box::new(DeleteContentForward),
|
||||
};
|
||||
|
||||
*doc = action.run(self.start, self.end, self.data.clone(), doc.clone());
|
||||
let data = match self.data.to_owned() {
|
||||
None => Vec::new(),
|
||||
Some(data) => data.encode_utf16().collect()
|
||||
};
|
||||
*doc = action.run(self.start, self.end, data, doc.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,45 @@
|
||||
use super::ActionRunner;
|
||||
|
||||
pub(crate) struct DeleteContentBackward;
|
||||
|
||||
pub(crate) struct DeleteContentForward;
|
||||
|
||||
impl ActionRunner for DeleteContentBackward {
|
||||
fn run(&self, start: usize, _end: usize, _data: Option<String>, mut doc: String) -> String {
|
||||
if start > 0 {
|
||||
doc.remove(start - 1);
|
||||
fn delete(start: usize, end: usize, mut doc: Vec<u16>) -> Vec<u16> {
|
||||
if start > doc.len() + 1 {
|
||||
return doc;
|
||||
}
|
||||
|
||||
let mut start_idx = start;
|
||||
let mut end_idx = end;
|
||||
unsafe {
|
||||
while start_idx > 0 && matches!(doc.get_unchecked(start_idx), 0xDC00..=0xDFFF) {
|
||||
start_idx -= 1;
|
||||
}
|
||||
|
||||
let end_byte = doc.get_unchecked(end_idx - 1);
|
||||
if end_idx < doc.len() && end_byte > &0xD800 && end_byte < &0xDC00 {
|
||||
end_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
doc.drain(start_idx..end_idx);
|
||||
doc
|
||||
}
|
||||
|
||||
impl ActionRunner for DeleteContentBackward {
|
||||
fn run(&self, start: usize, end: usize, _data: Vec<u16>, doc: Vec<u16>) -> Vec<u16> {
|
||||
if (start == 0 && start == end ) || end > doc.len() { return doc; }
|
||||
let (end, start) = if start == end { (start, start - 1) } else { (end, start) };
|
||||
|
||||
delete(start, end, doc)
|
||||
}
|
||||
}
|
||||
|
||||
impl ActionRunner for DeleteContentForward {
|
||||
fn run(&self, start: usize, _end: usize, _data: Option<String>, mut doc: String) -> String {
|
||||
if doc.len() > start {
|
||||
doc.remove(start);
|
||||
}
|
||||
doc
|
||||
fn run(&self, start: usize, end: usize, _data: Vec<u16>, doc: Vec<u16>) -> Vec<u16> {
|
||||
if start >= doc.len() { return doc; }
|
||||
|
||||
let end = if start == end { start + 1 } else { end };
|
||||
delete(start, end, doc)
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,38 @@
|
||||
use super::ActionRunner;
|
||||
|
||||
pub(crate) struct InsertText;
|
||||
pub(crate) struct InsertFromPaste;
|
||||
pub(crate) struct InsertLineBreak;
|
||||
|
||||
impl ActionRunner for InsertText {
|
||||
fn run(&self, start: usize, end: usize, data: Option<String>, mut doc: String) -> String {
|
||||
if doc.len() < start || data.is_none() {
|
||||
const NL_BYTES: [u16; 1] = [10];
|
||||
|
||||
fn insert(start: usize, end: usize, data: Vec<u16>, mut doc: Vec<u16>) -> Vec<u16> {
|
||||
if doc.len() < start || data.is_empty() {
|
||||
return doc;
|
||||
}
|
||||
|
||||
if start < end {
|
||||
doc.replace_range(start..end, "");
|
||||
doc.drain(start..end);
|
||||
}
|
||||
|
||||
doc.insert_str(start, &data.unwrap());
|
||||
doc.splice(start..start, data);
|
||||
doc
|
||||
}
|
||||
|
||||
impl ActionRunner for InsertText {
|
||||
fn run(&self, start: usize, end: usize, data: Vec<u16>, doc: Vec<u16>) -> Vec<u16> {
|
||||
insert(start, end, data, doc)
|
||||
}
|
||||
}
|
||||
|
||||
impl ActionRunner for InsertFromPaste {
|
||||
fn run(&self, start: usize, end: usize, data: Vec<u16>, doc: Vec<u16>) -> Vec<u16> {
|
||||
insert(start, end, data, doc)
|
||||
}
|
||||
}
|
||||
|
||||
impl ActionRunner for InsertLineBreak {
|
||||
fn run(&self, start: usize, _end: usize, _data: Vec<u16>, doc: Vec<u16>) -> Vec<u16> {
|
||||
insert(start, start, Vec::from(NL_BYTES), doc)
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
use super::ActionRunner;
|
||||
|
||||
pub(crate) struct InsertLineBreak;
|
||||
|
||||
impl ActionRunner for InsertLineBreak {
|
||||
fn run(&self, start: usize, _end: usize, _data: Option<String>, mut doc: String) -> String {
|
||||
if doc.len() >= start {
|
||||
doc.insert(start, '\n');
|
||||
}
|
||||
doc
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
pub(crate) mod delete;
|
||||
pub(crate) mod insert;
|
||||
pub(crate) mod linebreak;
|
||||
pub(crate) mod paste;
|
||||
|
||||
pub(crate) trait ActionRunner {
|
||||
fn run(&self, start: usize, end: usize, data: Option<String>, doc: String) -> String;
|
||||
fn run(&self, start: usize, end: usize, data: Vec<u16>, doc: Vec<u16>) -> Vec<u16>;
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
use super::ActionRunner;
|
||||
|
||||
pub(crate) struct InsertFromPaste;
|
||||
|
||||
impl ActionRunner for InsertFromPaste {
|
||||
fn run(&self, start: usize, end: usize, data: Option<String>, mut doc: String) -> String {
|
||||
if doc.len() < start || data.is_none() {
|
||||
return doc;
|
||||
}
|
||||
|
||||
if start < end {
|
||||
doc.replace_range(start..end, "");
|
||||
}
|
||||
|
||||
doc.insert_str(start, &data.unwrap());
|
||||
doc
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ mod actions;
|
||||
mod ws;
|
||||
|
||||
struct AppState {
|
||||
doc: RwLock<String>,
|
||||
doc: RwLock<Vec<u16>>,
|
||||
tx: broadcast::Sender<String>,
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ async fn main() {
|
||||
|
||||
let (tx, _rx) = broadcast::channel(100);
|
||||
let app_state = Arc::new(AppState {
|
||||
doc: RwLock::new("".to_string()),
|
||||
doc: RwLock::new(Vec::new()),
|
||||
tx,
|
||||
});
|
||||
|
||||
|
@ -31,7 +31,7 @@ async fn handle_socket(stream: WebSocket, state: Arc<AppState>) {
|
||||
|
||||
{
|
||||
let doc = state.doc.read().await;
|
||||
let doc: String = doc.clone();
|
||||
let doc: String = String::from_utf16_lossy(&doc);
|
||||
let new_state = NewState {
|
||||
client: String::from("0000"),
|
||||
doc: doc.to_string(),
|
||||
@ -64,7 +64,7 @@ async fn handle_socket(stream: WebSocket, state: Arc<AppState>) {
|
||||
|
||||
let new_state = NewState {
|
||||
client,
|
||||
doc: doc.to_string(),
|
||||
doc: String::from_utf16_lossy(&doc),
|
||||
};
|
||||
let payload = serde_json::to_string(&new_state).unwrap();
|
||||
let _ = tx.send(payload);
|
||||
|
Loading…
Reference in New Issue
Block a user