tangled
alpha
login
or
join now
nonbinary.computer
/
weaver
atproto blogging
24
fork
atom
overview
issues
2
pulls
pipelines
edit tracking more integral
Orual
1 month ago
9d56897a
3d475648
+126
-5
6 changed files
expand all
collapse all
unified
split
Cargo.lock
crates
weaver-editor-core
src
text.rs
undo.rs
writer
embed.rs
weaver-editor-crdt
Cargo.toml
src
buffer.rs
+1
Cargo.lock
···
12262
"weaver-common",
12263
"weaver-editor-core",
12264
"weaver-renderer",
0
12265
]
12266
12267
[[package]]
···
12262
"weaver-common",
12263
"weaver-editor-core",
12264
"weaver-renderer",
12265
+
"web-time",
12266
]
12267
12268
[[package]]
+79
-2
crates/weaver-editor-core/src/text.rs
···
6
7
use smol_str::{SmolStr, ToSmolStr};
8
use std::ops::Range;
0
0
0
9
10
/// A text buffer that supports efficient editing and offset conversion.
11
///
···
51
52
/// Convert byte offset to char offset.
53
fn byte_to_char(&self, byte_offset: usize) -> usize;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
54
}
55
56
/// Ropey-backed text buffer for local editing.
57
///
58
/// Provides O(log n) editing operations and offset conversions.
59
-
#[derive(Clone, Default)]
60
pub struct EditorRope {
61
rope: ropey::Rope,
0
0
0
0
0
0
0
0
0
0
62
}
63
64
impl EditorRope {
···
71
pub fn from_str(s: &str) -> Self {
72
Self {
73
rope: ropey::Rope::from_str(s),
0
74
}
75
}
76
···
101
}
102
103
fn insert(&mut self, char_offset: usize, text: &str) {
0
0
0
104
self.rope.insert(char_offset, text);
0
0
0
0
0
0
0
0
0
0
105
}
106
107
fn delete(&mut self, char_range: Range<usize>) {
108
-
self.rope.remove(char_range);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
109
}
110
111
fn slice(&self, char_range: Range<usize>) -> Option<SmolStr> {
···
132
133
fn byte_to_char(&self, byte_offset: usize) -> usize {
134
self.rope.byte_to_char(byte_offset)
0
0
0
0
0
0
0
0
0
0
0
0
0
135
}
136
}
137
···
6
7
use smol_str::{SmolStr, ToSmolStr};
8
use std::ops::Range;
9
+
use web_time::Instant;
10
+
11
+
use crate::types::{EditInfo, BLOCK_SYNTAX_ZONE};
12
13
/// A text buffer that supports efficient editing and offset conversion.
14
///
···
54
55
/// Convert byte offset to char offset.
56
fn byte_to_char(&self, byte_offset: usize) -> usize;
57
+
58
+
/// Get info about the last edit operation, if any.
59
+
fn last_edit(&self) -> Option<&EditInfo>;
60
+
61
+
/// Check if a char offset is in the block-syntax zone (first few chars of a line).
62
+
fn is_in_block_syntax_zone(&self, offset: usize) -> bool {
63
+
if offset <= BLOCK_SYNTAX_ZONE {
64
+
return true;
65
+
}
66
+
67
+
// Get slice of the search range and look for newline.
68
+
let search_start = offset.saturating_sub(BLOCK_SYNTAX_ZONE + 1);
69
+
match self.slice(search_start..offset) {
70
+
Some(s) => match s.rfind('\n') {
71
+
Some(pos) => (offset - search_start - pos - 1) <= BLOCK_SYNTAX_ZONE,
72
+
None => false, // No newline in range, offset > BLOCK_SYNTAX_ZONE.
73
+
},
74
+
None => false,
75
+
}
76
+
}
77
}
78
79
/// Ropey-backed text buffer for local editing.
80
///
81
/// Provides O(log n) editing operations and offset conversions.
82
+
#[derive(Clone)]
83
pub struct EditorRope {
84
rope: ropey::Rope,
85
+
last_edit: Option<EditInfo>,
86
+
}
87
+
88
+
impl Default for EditorRope {
89
+
fn default() -> Self {
90
+
Self {
91
+
rope: ropey::Rope::default(),
92
+
last_edit: None,
93
+
}
94
+
}
95
}
96
97
impl EditorRope {
···
104
pub fn from_str(s: &str) -> Self {
105
Self {
106
rope: ropey::Rope::from_str(s),
107
+
last_edit: None,
108
}
109
}
110
···
135
}
136
137
fn insert(&mut self, char_offset: usize, text: &str) {
138
+
let in_block_syntax_zone = self.is_in_block_syntax_zone(char_offset);
139
+
let contains_newline = text.contains('\n');
140
+
141
self.rope.insert(char_offset, text);
142
+
143
+
self.last_edit = Some(EditInfo {
144
+
edit_char_pos: char_offset,
145
+
inserted_len: text.chars().count(),
146
+
deleted_len: 0,
147
+
contains_newline,
148
+
in_block_syntax_zone,
149
+
doc_len_after: self.rope.len_chars(),
150
+
timestamp: Instant::now(),
151
+
});
152
}
153
154
fn delete(&mut self, char_range: Range<usize>) {
155
+
let in_block_syntax_zone = self.is_in_block_syntax_zone(char_range.start);
156
+
let contains_newline = self
157
+
.slice(char_range.clone())
158
+
.map(|s| s.contains('\n'))
159
+
.unwrap_or(false);
160
+
let deleted_len = char_range.len();
161
+
162
+
self.rope.remove(char_range.clone());
163
+
164
+
self.last_edit = Some(EditInfo {
165
+
edit_char_pos: char_range.start,
166
+
inserted_len: 0,
167
+
deleted_len,
168
+
contains_newline,
169
+
in_block_syntax_zone,
170
+
doc_len_after: self.rope.len_chars(),
171
+
timestamp: Instant::now(),
172
+
});
173
}
174
175
fn slice(&self, char_range: Range<usize>) -> Option<SmolStr> {
···
196
197
fn byte_to_char(&self, byte_offset: usize) -> usize {
198
self.rope.byte_to_char(byte_offset)
199
+
}
200
+
201
+
fn last_edit(&self) -> Option<&EditInfo> {
202
+
self.last_edit.as_ref()
203
+
}
204
+
205
+
fn is_in_block_syntax_zone(&self, offset: usize) -> bool {
206
+
if offset > self.rope.len_chars() {
207
+
return false;
208
+
}
209
+
let line_num = self.rope.char_to_line(offset);
210
+
let line_start = self.rope.line_to_char(line_num);
211
+
(offset - line_start) <= BLOCK_SYNTAX_ZONE
212
}
213
}
214
+4
crates/weaver-editor-core/src/undo.rs
···
158
fn byte_to_char(&self, byte_offset: usize) -> usize {
159
self.buffer.byte_to_char(byte_offset)
160
}
0
0
0
0
161
}
162
163
impl<T: TextBuffer> UndoManager for UndoableBuffer<T> {
···
158
fn byte_to_char(&self, byte_offset: usize) -> usize {
159
self.buffer.byte_to_char(byte_offset)
160
}
161
+
162
+
fn last_edit(&self) -> Option<&crate::types::EditInfo> {
163
+
self.buffer.last_edit()
164
+
}
165
}
166
167
impl<T: TextBuffer> UndoManager for UndoableBuffer<T> {
+1
-1
crates/weaver-editor-core/src/writer/embed.rs
···
6
7
use jacquard::IntoStatic;
8
use jacquard::types::{ident::AtIdentifier, string::Rkey};
9
-
use markdown_weaver::{CowStr, Event, Tag};
10
use markdown_weaver_escape::{StrWrite, escape_html};
11
use smol_str::SmolStr;
12
···
6
7
use jacquard::IntoStatic;
8
use jacquard::types::{ident::AtIdentifier, string::Rkey};
9
+
use markdown_weaver::{Event, Tag};
10
use markdown_weaver_escape::{StrWrite, escape_html};
11
use smol_str::SmolStr;
12
+1
crates/weaver-editor-crdt/Cargo.toml
···
23
loro = "1.9"
24
serde = { workspace = true }
25
smol_str = "0.3"
0
26
tracing = { workspace = true }
27
thiserror = "2"
28
futures-util = "0.3"
···
23
loro = "1.9"
24
serde = { workspace = true }
25
smol_str = "0.3"
26
+
web-time = "1"
27
tracing = { workspace = true }
28
thiserror = "2"
29
futures-util = "0.3"
+40
-2
crates/weaver-editor-crdt/src/buffer.rs
···
6
7
use loro::{cursor::PosType, LoroDoc, LoroText, UndoManager as LoroUndoManager, VersionVector};
8
use smol_str::{SmolStr, ToSmolStr};
9
-
use weaver_editor_core::{TextBuffer, UndoManager};
0
10
11
use crate::CrdtError;
12
···
19
doc: LoroDoc,
20
content: LoroText,
21
undo_mgr: Rc<RefCell<LoroUndoManager>>,
0
22
}
23
24
impl LoroTextBuffer {
···
32
doc,
33
content,
34
undo_mgr,
0
35
}
36
}
37
···
46
doc,
47
content,
48
undo_mgr,
0
49
})
50
}
51
···
118
}
119
120
fn insert(&mut self, char_offset: usize, text: &str) {
0
0
0
121
self.content.insert(char_offset, text).ok();
0
0
0
0
0
0
0
0
0
0
122
}
123
124
fn delete(&mut self, char_range: Range<usize>) {
125
-
self.content.delete(char_range.start, char_range.len()).ok();
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
126
}
127
128
fn slice(&self, char_range: Range<usize>) -> Option<SmolStr> {
···
153
self.content
154
.convert_pos(byte_offset, PosType::Bytes, PosType::Unicode)
155
.unwrap_or(self.content.len_unicode())
0
0
0
0
156
}
157
}
158
···
6
7
use loro::{cursor::PosType, LoroDoc, LoroText, UndoManager as LoroUndoManager, VersionVector};
8
use smol_str::{SmolStr, ToSmolStr};
9
+
use web_time::Instant;
10
+
use weaver_editor_core::{EditInfo, TextBuffer, UndoManager};
11
12
use crate::CrdtError;
13
···
20
doc: LoroDoc,
21
content: LoroText,
22
undo_mgr: Rc<RefCell<LoroUndoManager>>,
23
+
last_edit: Option<EditInfo>,
24
}
25
26
impl LoroTextBuffer {
···
34
doc,
35
content,
36
undo_mgr,
37
+
last_edit: None,
38
}
39
}
40
···
49
doc,
50
content,
51
undo_mgr,
52
+
last_edit: None,
53
})
54
}
55
···
122
}
123
124
fn insert(&mut self, char_offset: usize, text: &str) {
125
+
let in_block_syntax_zone = self.is_in_block_syntax_zone(char_offset);
126
+
let contains_newline = text.contains('\n');
127
+
128
self.content.insert(char_offset, text).ok();
129
+
130
+
self.last_edit = Some(EditInfo {
131
+
edit_char_pos: char_offset,
132
+
inserted_len: text.chars().count(),
133
+
deleted_len: 0,
134
+
contains_newline,
135
+
in_block_syntax_zone,
136
+
doc_len_after: self.content.len_unicode(),
137
+
timestamp: Instant::now(),
138
+
});
139
}
140
141
fn delete(&mut self, char_range: Range<usize>) {
142
+
let in_block_syntax_zone = self.is_in_block_syntax_zone(char_range.start);
143
+
let contains_newline = self
144
+
.slice(char_range.clone())
145
+
.map(|s| s.contains('\n'))
146
+
.unwrap_or(false);
147
+
let deleted_len = char_range.len();
148
+
149
+
self.content.delete(char_range.start, deleted_len).ok();
150
+
151
+
self.last_edit = Some(EditInfo {
152
+
edit_char_pos: char_range.start,
153
+
inserted_len: 0,
154
+
deleted_len,
155
+
contains_newline,
156
+
in_block_syntax_zone,
157
+
doc_len_after: self.content.len_unicode(),
158
+
timestamp: Instant::now(),
159
+
});
160
}
161
162
fn slice(&self, char_range: Range<usize>) -> Option<SmolStr> {
···
187
self.content
188
.convert_pos(byte_offset, PosType::Bytes, PosType::Unicode)
189
.unwrap_or(self.content.len_unicode())
190
+
}
191
+
192
+
fn last_edit(&self) -> Option<&EditInfo> {
193
+
self.last_edit.as_ref()
194
}
195
}
196