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