Update formatting

This commit is contained in:
Andras Schmelczer 2024-12-08 18:22:17 +00:00
parent dda356ea00
commit 49638e5aa7
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
27 changed files with 239 additions and 232 deletions

View file

@ -12,24 +12,27 @@
//! //!
//! # Heuristics //! # Heuristics
//! //!
//! At present this implementation of Myers' does not implement any more advanced //! At present this implementation of Myers' does not implement any more
//! heuristics that would solve some pathological cases. For instance passing two //! advanced heuristics that would solve some pathological cases. For instance
//! large and completely distinct sequences to the algorithm will make it spin //! passing two large and completely distinct sequences to the algorithm will
//! without making reasonable progress. //! make it spin without making reasonable progress.
//! For potential improvements here see [similar#15](https://github.com/mitsuhiko/similar/issues/15). //! For potential improvements here see [similar#15](https://github.com/mitsuhiko/similar/issues/15).
use std::ops::{Index, IndexMut, Range}; use std::{
use std::vec; ops::{Index, IndexMut, Range},
vec,
use crate::tokenizer::token::Token; };
use crate::utils::common_prefix_len::common_prefix_len;
use crate::utils::common_suffix_len::common_suffix_len;
use super::raw_operation::RawOperation; use super::raw_operation::RawOperation;
use crate::{
tokenizer::token::Token,
utils::{common_prefix_len::common_prefix_len, common_suffix_len::common_suffix_len},
};
/// Myers' diff algorithm with deadline. /// Myers' diff algorithm with deadline.
/// ///
/// Diff `old`, between indices `old_range` and `new` between indices `new_range`. /// Diff `old`, between indices `old_range` and `new` between indices
/// `new_range`.
/// ///
/// This diff is done with an optional deadline that defines the maximal /// This diff is done with an optional deadline that defines the maximal
/// execution time permitted before it bails and falls back to an approximation. /// execution time permitted before it bails and falls back to an approximation.
@ -58,15 +61,15 @@ where
// and then a possibly empty sequence of diagonal edges called a snake. // and then a possibly empty sequence of diagonal edges called a snake.
/// `V` contains the endpoints of the furthest reaching `D-paths`. For each /// `V` contains the endpoints of the furthest reaching `D-paths`. For each
/// recorded endpoint `(x,y)` in diagonal `k`, we only need to retain `x` because /// recorded endpoint `(x,y)` in diagonal `k`, we only need to retain `x`
/// `y` can be computed from `x - k`. In other words, `V` is an array of integers /// because `y` can be computed from `x - k`. In other words, `V` is an array of
/// where `V[k]` contains the row index of the endpoint of the furthest reaching /// integers where `V[k]` contains the row index of the endpoint of the furthest
/// path in diagonal `k`. /// reaching path in diagonal `k`.
/// ///
/// We can't use a traditional Vec to represent `V` since we use `k` as an index /// We can't use a traditional Vec to represent `V` since we use `k` as an index
/// and it can take on negative values. So instead `V` is represented as a /// and it can take on negative values. So instead `V` is represented as a
/// light-weight wrapper around a Vec plus an `offset` which is the maximum value /// light-weight wrapper around a Vec plus an `offset` which is the maximum
/// `k` can take on in order to map negative `k`'s back to a value >= 0. /// value `k` can take on in order to map negative `k`'s back to a value >= 0.
#[derive(Debug)] #[derive(Debug)]
struct V { struct V {
offset: isize, offset: isize,
@ -81,17 +84,13 @@ impl V {
} }
} }
fn len(&self) -> usize { fn len(&self) -> usize { self.v.len() }
self.v.len()
}
} }
impl Index<isize> for V { impl Index<isize> for V {
type Output = usize; type Output = usize;
fn index(&self, index: isize) -> &Self::Output { fn index(&self, index: isize) -> &Self::Output { &self.v[(index + self.offset) as usize] }
&self.v[(index + self.offset) as usize]
}
} }
impl IndexMut<isize> for V { impl IndexMut<isize> for V {

View file

@ -30,9 +30,9 @@ where
self.tokens().iter().map(|t| t.original()).collect() self.tokens().iter().map(|t| t.original()).collect()
} }
/// Extends the operation with another operation if returning the new operation. /// Extends the operation with another operation if returning the new
/// Only operations of the same type can be used to extend. If the operations are of different /// operation. Only operations of the same type can be used to extend.
/// types, returns None. /// If the operations are of different types, returns None.
pub fn extend(self, other: RawOperation<T>) -> Option<RawOperation<T>> { pub fn extend(self, other: RawOperation<T>) -> Option<RawOperation<T>> {
match (self, other) { match (self, other) {
(RawOperation::Insert(tokens1), RawOperation::Insert(tokens2)) => Some( (RawOperation::Insert(tokens1), RawOperation::Insert(tokens2)) => Some(

View file

@ -3,7 +3,5 @@ mod operation_transformation;
mod tokenizer; mod tokenizer;
mod utils; mod utils;
pub use operation_transformation::reconcile; pub use operation_transformation::{reconcile, reconcile_with_tokenizer, EditedText};
pub use operation_transformation::reconcile_with_tokenizer;
pub use operation_transformation::EditedText;
pub use tokenizer::token::Token; pub use tokenizer::token::Token;

View file

@ -1,24 +1,26 @@
use super::Operation;
use crate::diffs::raw_operation::RawOperation;
use crate::operation_transformation::merge_context::MergeContext;
use crate::tokenizer::word_tokenizer::word_tokenizer;
use crate::tokenizer::Tokenizer;
use crate::utils::ordered_operation::OrderedOperation;
use crate::utils::side::Side;
use crate::utils::string_builder::StringBuilder;
use crate::{diffs::myers::diff, utils::merge_iters::MergeSorted};
use std::iter; use std::iter;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::Operation;
use crate::{
diffs::{myers::diff, raw_operation::RawOperation},
operation_transformation::merge_context::MergeContext,
tokenizer::{word_tokenizer::word_tokenizer, Tokenizer},
utils::{
merge_iters::MergeSorted, ordered_operation::OrderedOperation, side::Side,
string_builder::StringBuilder,
},
};
/// A sequence of operations that can be applied to a text document. /// A sequence of operations that can be applied to a text document.
/// EditedText supports merging two sequences of operations using the /// EditedText supports merging two sequences of operations using the
/// principle of Operational Transformation. /// principle of Operational Transformation.
/// ///
/// It's mainly created through the from_strings method, then merged with another /// It's mainly created through the from_strings method, then merged with
/// EditedText derived from the same original text and then applied to the original text /// another EditedText derived from the same original text and then applied to
/// to get the reconciled text of concurrent edits. /// the original text to get the reconciled text of concurrent edits.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct EditedText<'a, T> pub struct EditedText<'a, T>
@ -30,10 +32,12 @@ where
} }
impl<'a> EditedText<'a, String> { impl<'a> EditedText<'a, String> {
/// Create an EditedText from the given original (old) and updated (new) strings. /// Create an EditedText from the given original (old) and updated (new)
/// The returned EditedText represents the changes from the original to the updated text. /// strings. The returned EditedText represents the changes from the
/// When the return value is applied to the original text, it will result in the updated text. /// original to the updated text. When the return value is applied to
/// The default word tokenizer is used to tokenize the text which splits the text on whitespaces. /// the original text, it will result in the updated text. The default
/// word tokenizer is used to tokenize the text which splits the text on
/// whitespaces.
pub fn from_strings(original: &'a str, updated: &str) -> Self { pub fn from_strings(original: &'a str, updated: &str) -> Self {
Self::from_strings_with_tokenizer(original, updated, &word_tokenizer) Self::from_strings_with_tokenizer(original, updated, &word_tokenizer)
} }
@ -43,10 +47,11 @@ impl<'a, T> EditedText<'a, T>
where where
T: PartialEq + Clone, T: PartialEq + Clone,
{ {
/// Create an EditedText from the given original (old) and updated (new) strings. /// Create an EditedText from the given original (old) and updated (new)
/// The returned EditedText represents the changes from the original to the updated text. /// strings. The returned EditedText represents the changes from the
/// When the return value is applied to the original text, it will result in the updated text. /// original to the updated text. When the return value is applied to
/// The tokenizer function is used to tokenize the text. /// the original text, it will result in the updated text. The tokenizer
/// function is used to tokenize the text.
pub fn from_strings_with_tokenizer( pub fn from_strings_with_tokenizer(
original: &'a str, original: &'a str,
updated: &str, updated: &str,
@ -64,7 +69,8 @@ where
) )
} }
// Turn raw operations into ordered operations while keeping track of old & new indexes. // Turn raw operations into ordered operations while keeping track of old & new
// indexes.
fn cook_operations<I>(raw_operations: I) -> impl Iterator<Item = OrderedOperation<T>> fn cook_operations<I>(raw_operations: I) -> impl Iterator<Item = OrderedOperation<T>>
where where
I: IntoIterator<Item = RawOperation<T>>, I: IntoIterator<Item = RawOperation<T>>,
@ -162,8 +168,8 @@ where
} }
/// Create a new EditedText with the given operations. /// Create a new EditedText with the given operations.
/// The operations must be in the order in which they are meant to be applied. /// The operations must be in the order in which they are meant to be
/// The operations must not overlap. /// applied. The operations must not overlap.
fn new(text: &'a str, operations: Vec<OrderedOperation<T>>) -> Self { fn new(text: &'a str, operations: Vec<OrderedOperation<T>>) -> Self {
operations operations
.iter() .iter()
@ -227,7 +233,8 @@ where
/// ///
/// # Errors /// # Errors
/// ///
/// Returns an SyncLibError::OperationError if the operations cannot be applied to the text. /// Returns an SyncLibError::OperationError if the operations cannot be
/// applied to the text.
pub fn apply(&self) -> String { pub fn apply(&self) -> String {
let mut builder: StringBuilder<'_> = StringBuilder::new(self.text); let mut builder: StringBuilder<'_> = StringBuilder::new(self.text);

View file

@ -39,12 +39,10 @@ impl<T> MergeContext<T>
where where
T: PartialEq + Clone, T: PartialEq + Clone,
{ {
pub fn last_operation(&self) -> Option<&Operation<T>> { pub fn last_operation(&self) -> Option<&Operation<T>> { self.last_operation.as_ref() }
self.last_operation.as_ref()
}
/// Replace the last delete operation (if there was one) with a new one while /// Replace the last delete operation (if there was one) with a new one
/// applying it to the shift. /// while applying it to the shift.
pub fn consume_and_replace_last_operation(&mut self, operation: Option<Operation<T>>) { pub fn consume_and_replace_last_operation(&mut self, operation: Option<Operation<T>>) {
if let Some(Operation::Delete { if let Some(Operation::Delete {
deleted_character_count, deleted_character_count,
@ -62,8 +60,8 @@ where
} }
/// Remove the last operation (if there was one) in case it is behind the /// Remove the last operation (if there was one) in case it is behind the
/// threshold operation. This changes the shift in case the last operation was /// threshold operation. This changes the shift in case the last operation
/// a delete. /// was a delete.
pub fn consume_last_operation_if_it_is_too_behind( pub fn consume_last_operation_if_it_is_too_behind(
&mut self, &mut self,
threshold_operation: &Operation<T>, threshold_operation: &Operation<T>,

View file

@ -33,11 +33,13 @@ where
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*;
use pretty_assertions::assert_eq;
use std::{fs, ops::Range, path::Path}; use std::{fs, ops::Range, path::Path};
use pretty_assertions::assert_eq;
use test_case::test_matrix; use test_case::test_matrix;
use super::*;
#[test] #[test]
fn test_merges() { fn test_merges() {
// Both replaced one token but different // Both replaced one token but different
@ -64,7 +66,8 @@ mod test {
"original_1 edit_1 edit_2 original_5", "original_1 edit_1 edit_2 original_5",
); );
// One deleted a large range, the other inserted and deleted a partially overlapping range // One deleted a large range, the other inserted and deleted a partially
// overlapping range
test_merge_both_ways( test_merge_both_ways(
"original_1 original_2 original_3 original_4 original_5", "original_1 original_2 original_3 original_4 original_5",
"original_1 original_5", "original_1 original_5",
@ -102,7 +105,8 @@ mod test {
"hi, my friend!", "hi, my friend!",
); );
// test_merge_both_ways("hello world", "world !", "hi hello world", "hi world !"); // test_merge_both_ways("hello world", "world !", "hi hello world", "hi world
// !");
test_merge_both_ways( test_merge_both_ways(
"both delete the same word", "both delete the same word",

View file

@ -1,14 +1,16 @@
use crate::utils::find_common_overlap::find_common_overlap; use std::{
use crate::utils::string_builder::StringBuilder; fmt::{Debug, Display},
use crate::Token; ops::Range,
use std::fmt::Debug; };
use std::fmt::Display;
use std::ops::Range;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::merge_context::MergeContext; use super::merge_context::MergeContext;
use crate::{
utils::{find_common_overlap::find_common_overlap, string_builder::StringBuilder},
Token,
};
/// Represents a change that can be applied to a text document. /// Represents a change that can be applied to a text document.
/// Operation is tied to a ropey::Rope and is mainly expected to be /// Operation is tied to a ropey::Rope and is mainly expected to be
@ -38,7 +40,8 @@ where
T: PartialEq + Clone, T: PartialEq + Clone,
{ {
/// Creates an insert operation with the given index and text. /// Creates an insert operation with the given index and text.
/// If the text is empty (meaning that the operation would be a no-op), returns None. /// If the text is empty (meaning that the operation would be a no-op),
/// returns None.
pub fn create_insert(index: usize, text: Vec<Token<T>>) -> Option<Self> { pub fn create_insert(index: usize, text: Vec<Token<T>>) -> Option<Self> {
if text.is_empty() { if text.is_empty() {
return None; return None;
@ -47,8 +50,9 @@ where
Some(Operation::Insert { index, text }) Some(Operation::Insert { index, text })
} }
/// Creates a delete operation with the given index and number of to-be-deleted characters. /// Creates a delete operation with the given index and number of
/// If the operation would delete 0 (meaning that the operation would be a no-op), returns None. /// to-be-deleted characters. If the operation would delete 0 (meaning
/// that the operation would be a no-op), returns None.
pub fn create_delete(index: usize, deleted_character_count: usize) -> Option<Self> { pub fn create_delete(index: usize, deleted_character_count: usize) -> Option<Self> {
if deleted_character_count == 0 { if deleted_character_count == 0 {
return None; return None;
@ -77,16 +81,18 @@ where
}) })
} }
/// Tries to apply the operation to the given ropey::Rope text, returning the modified text. /// Tries to apply the operation to the given ropey::Rope text, returning
/// the modified text.
/// ///
/// # Errors /// # Errors
/// ///
/// Returns a SyncLibError::OperationApplicationError if the operation cannot be applied. /// Returns a SyncLibError::OperationApplicationError if the operation
/// cannot be applied.
/// ///
/// # Panics /// # Panics
/// ///
/// When compiled in debug mode, panics if a delete operation is attempted on a range /// When compiled in debug mode, panics if a delete operation is attempted
/// of text that does not match the text to be deleted. /// on a range of text that does not match the text to be deleted.
pub fn apply<'a>(&self, mut builder: StringBuilder<'a>) -> StringBuilder<'a> { pub fn apply<'a>(&self, mut builder: StringBuilder<'a>) -> StringBuilder<'a> {
match self { match self {
Operation::Insert { text, .. } => builder.insert( Operation::Insert { text, .. } => builder.insert(
@ -135,11 +141,10 @@ where
} }
/// Returns the range of indices of characters that the operation affects. /// Returns the range of indices of characters that the operation affects.
pub fn range(&self) -> Range<usize> { pub fn range(&self) -> Range<usize> { self.start_index()..self.end_index() + 1 }
self.start_index()..self.end_index() + 1
}
/// Returns the number of affected characters. It is always greater than 0 because empty operations cannot be created. /// Returns the number of affected characters. It is always greater than 0
/// because empty operations cannot be created.
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
match self { match self {
Operation::Insert { text, .. } => { Operation::Insert { text, .. } => {
@ -152,7 +157,8 @@ where
} }
} }
/// Creates a new operation with the same type and text but with the given index. /// Creates a new operation with the same type and text but with the given
/// index.
pub fn with_index(self, index: usize) -> Self { pub fn with_index(self, index: usize) -> Self {
match self { match self {
Operation::Insert { text, .. } => Operation::Insert { index, text }, Operation::Insert { text, .. } => Operation::Insert { index, text },
@ -172,8 +178,9 @@ where
} }
} }
/// Creates a new operation with the same type and text but with the index shifted by the given offset. /// Creates a new operation with the same type and text but with the index
/// The offset can be negative but the resulting index must be non-negative. /// shifted by the given offset. The offset can be negative but the
/// resulting index must be non-negative.
/// ///
/// # Panics /// # Panics
/// ///
@ -185,8 +192,9 @@ where
self.with_index(index as usize) self.with_index(index as usize)
} }
/// Merges the operation with the given context, producing a new operation and updating the context. /// Merges the operation with the given context, producing a new operation
/// This implements a comples FSM that handles the merging of operations in a way that is consistent with the text. /// and updating the context. This implements a comples FSM that handles
/// the merging of operations in a way that is consistent with the text.
/// The contexts are updated in-place. /// The contexts are updated in-place.
pub fn merge_operations_with_context( pub fn merge_operations_with_context(
self, self,
@ -242,9 +250,10 @@ where
produced_context.shift += operation.len() as i64; produced_context.shift += operation.len() as i64;
debug_assert!( debug_assert!(
last_delete.range().contains(&operation.start_index()), last_delete.range().contains(&operation.start_index()),
"There is a last delete ({last_delete}) but the operation ({operation}) is not contained in it" "There is a last delete ({last_delete}) but the operation ({operation}) is \
); not contained in it"
);
let difference = operation.start_index() as i64 - last_delete.start_index() as i64; let difference = operation.start_index() as i64 - last_delete.start_index() as i64;
@ -266,9 +275,10 @@ where
Some(last_delete @ Operation::Delete { .. }), Some(last_delete @ Operation::Delete { .. }),
) => { ) => {
debug_assert!( debug_assert!(
last_delete.range().contains(&operation.start_index()), last_delete.range().contains(&operation.start_index()),
"There is a last delete ({last_delete}) but the operation ({operation}) is not contained in it" "There is a last delete ({last_delete}) but the operation ({operation}) is \
); not contained in it"
);
let difference = operation.start_index() as i64 - last_delete.start_index() as i64; let difference = operation.start_index() as i64 - last_delete.start_index() as i64;
@ -341,16 +351,15 @@ impl<T> Debug for Operation<T>
where where
T: PartialEq + Clone, T: PartialEq + Clone,
{ {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self) }
write!(f, "{}", self)
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*;
#[test] #[test]
#[should_panic] #[should_panic]
fn test_shifting_error() { fn test_shifting_error() {

View file

@ -2,7 +2,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// A token is a string that has been normalised in some way. /// A token is a string that has been normalised in some way.
/// The normalised form is used for comparison, while the original form is used for applying Operations. /// The normalised form is used for comparison, while the original form is used
/// for applying Operations.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Token<T> pub struct Token<T>
@ -33,24 +34,16 @@ where
} }
} }
pub fn original(&self) -> &str { pub fn original(&self) -> &str { &self.original }
&self.original
}
pub fn normalised(&self) -> &T { pub fn normalised(&self) -> &T { &self.normalised }
&self.normalised
}
pub fn get_original_length(&self) -> usize { pub fn get_original_length(&self) -> usize { self.original.chars().count() }
self.original.chars().count()
}
} }
impl<T> PartialEq for Token<T> impl<T> PartialEq for Token<T>
where where
T: PartialEq + Clone, T: PartialEq + Clone,
{ {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool { self.normalised == other.normalised }
self.normalised == other.normalised
}
} }

View file

@ -21,9 +21,10 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*;
#[test] #[test]
fn test_common_prefix_len() { fn test_common_prefix_len() {
assert_eq!( assert_eq!(

View file

@ -22,9 +22,10 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*;
#[test] #[test]
fn test_common_suffix_len() { fn test_common_suffix_len() {
assert_eq!( assert_eq!(

View file

@ -1,9 +1,12 @@
use crate::Token; use crate::Token;
/// Given two lists of tokens, returns the offset in the first (old) list from which the two lists have the same tokens until the end of the first list. /// Given two lists of tokens, returns the offset in the first (old) list from
/// Thus, the suffix of the old list from the offset to the end is equal to a prefix of the new list. /// which the two lists have the same tokens until the end of the first list.
/// Thus, the suffix of the old list from the offset to the end is equal to a
/// prefix of the new list.
/// ///
/// If there is no overlap, the function returns the maxmium offset, the length of the old list. /// If there is no overlap, the function returns the maxmium offset, the length
/// of the old list.
/// ///
/// ## Example /// ## Example
/// ``` /// ```
@ -27,9 +30,10 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*;
#[test] #[test]
fn test_common_overlap() { fn test_common_overlap() {
assert_eq!(find_common_overlap(&["".into()], &["".into()]), 0); assert_eq!(find_common_overlap(&["".into()], &["".into()]), 0);

View file

@ -1,5 +1,4 @@
use std::cmp::Ordering; use std::{cmp::Ordering, iter::Peekable};
use std::iter::Peekable;
pub struct MergeAscending<L, R, F, O> pub struct MergeAscending<L, R, F, O>
where where
@ -70,9 +69,10 @@ impl<T: ?Sized> MergeSorted for T where T: Iterator {}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*;
#[test] #[test]
fn test_merge_sorted_by_key() { fn test_merge_sorted_by_key() {
let left = [9, 7, 5, 3, 1]; let left = [9, 7, 5, 3, 1];

View file

@ -1,7 +1,8 @@
use std::ops::Range; use std::ops::Range;
/// A helper for building a string in order based on an original string and a series of insertions and deletions applied to it. /// A helper for building a string in order based on an original string and a
/// It is safe to use with UTF-8 strings as all operations are based on character indices. /// series of insertions and deletions applied to it. It is safe to use with
/// UTF-8 strings as all operations are based on character indices.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StringBuilder<'a> { pub struct StringBuilder<'a> {
original: &'a str, original: &'a str,
@ -18,13 +19,15 @@ impl StringBuilder<'_> {
} }
} }
/// Insert a string at the given index after copying the original string up to that index from the last insertion or deletion. /// Insert a string at the given index after copying the original string up
/// to that index from the last insertion or deletion.
pub fn insert(&mut self, from: usize, text: &str) { pub fn insert(&mut self, from: usize, text: &str) {
self.copy_until(from); self.copy_until(from);
self.buffer.push_str(text); self.buffer.push_str(text);
} }
/// Delete a string at the given index after copying the original string up to that index from the last insertion or deletion. /// Delete a string at the given index after copying the original string up
/// to that index from the last insertion or deletion.
pub fn delete(&mut self, range: std::ops::Range<usize>) { pub fn delete(&mut self, range: std::ops::Range<usize>) {
self.copy_until(range.start); self.copy_until(range.start);
self.last_old_char_index += range.len(); self.last_old_char_index += range.len();
@ -50,7 +53,8 @@ impl StringBuilder<'_> {
self.last_old_char_index += jump; self.last_old_char_index += jump;
} }
/// Finish building the string after copying the remaining original string since the last insertion or deletion. /// Finish building the string after copying the remaining original string
/// since the last insertion or deletion.
pub fn build(mut self) -> String { pub fn build(mut self) -> String {
self.buffer.push_str( self.buffer.push_str(
&self &self

8
backend/rustfmt.toml Normal file
View file

@ -0,0 +1,8 @@
imports_granularity = "crate"
condense_wildcard_suffixes = true
fn_single_line = true
format_strings = true
reorder_impl_items = true
group_imports = "StdExternalCrate"
use_field_init_shorthand = true
wrap_comments=true

View file

@ -3,13 +3,9 @@ use errors::SyncLibError;
pub mod errors; pub mod errors;
pub fn bytes_to_base64(input: &[u8]) -> String { pub fn bytes_to_base64(input: &[u8]) -> String { STANDARD_NO_PAD.encode(input) }
STANDARD_NO_PAD.encode(input)
}
pub fn string_to_base64(input: &str) -> String { pub fn string_to_base64(input: &str) -> String { bytes_to_base64(input.as_bytes()) }
bytes_to_base64(input.as_bytes())
}
pub fn base64_to_bytes(input: &str) -> Result<Vec<u8>, SyncLibError> { pub fn base64_to_bytes(input: &str) -> Result<Vec<u8>, SyncLibError> {
STANDARD_NO_PAD.decode(input).map_err(SyncLibError::from) STANDARD_NO_PAD.decode(input).map_err(SyncLibError::from)
@ -20,6 +16,4 @@ pub fn base64_to_string(input: &str) -> Result<String, SyncLibError> {
String::from_utf8(bytes).map_err(SyncLibError::from) String::from_utf8(bytes).map_err(SyncLibError::from)
} }
pub fn is_binary(data: &[u8]) -> bool { pub fn is_binary(data: &[u8]) -> bool { data.iter().any(|&b| b == 0) }
data.iter().any(|&b| b == 0)
}

View file

@ -1,6 +1,7 @@
use crate::{config::Config, consts::CONFIG_PATH, database::Database};
use anyhow::Result; use anyhow::Result;
use crate::{config::Config, consts::CONFIG_PATH, database::Database};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AppState { pub struct AppState {
pub config: Config, pub config: Config,

View file

@ -1,5 +1,4 @@
use rand::distributions::Alphanumeric; use rand::{distributions::Alphanumeric, thread_rng, Rng};
use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]

View file

@ -4,8 +4,7 @@ use anyhow::{Context, Result};
use models::{ use models::{
DocumentId, DocumentVersionId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId, DocumentId, DocumentVersionId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId,
}; };
use sqlx::sqlite::SqliteConnectOptions; use sqlx::{sqlite::SqliteConnectOptions, types::chrono::Utc};
use sqlx::types::chrono::Utc;
pub mod models; pub mod models;
use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite}; use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite};

View file

@ -21,9 +21,7 @@ pub struct StoredDocumentVersion {
} }
impl StoredDocumentVersion { impl StoredDocumentVersion {
pub fn content_as_string(&self) -> String { pub fn content_as_string(&self) -> String { String::from_utf8_lossy(&self.content).to_string() }
String::from_utf8_lossy(&self.content).to_string()
}
} }
#[derive(Debug, Clone, Serialize, JsonSchema)] #[derive(Debug, Clone, Serialize, JsonSchema)]

View file

@ -82,9 +82,7 @@ impl OperationOutput for SyncServerError {
type Inner = Self; type Inner = Self;
} }
pub fn init_error(error: anyhow::Error) -> SyncServerError { pub fn init_error(error: anyhow::Error) -> SyncServerError { SyncServerError::InitError(error) }
SyncServerError::InitError(error)
}
pub fn server_error(error: anyhow::Error) -> SyncServerError { pub fn server_error(error: anyhow::Error) -> SyncServerError {
warn!("Server error: {:?}", error); warn!("Server error: {:?}", error);

View file

@ -1,4 +1,3 @@
use crate::app_state::AppState;
use aide::{ use aide::{
axum::{ axum::{
routing::{delete, get, post, put}, routing::{delete, get, post, put},
@ -7,12 +6,15 @@ use aide::{
openapi::{Info, OpenApi}, openapi::{Info, OpenApi},
scalar::Scalar, scalar::Scalar,
}; };
use anyhow::Context; use anyhow::{Context, Result};
use anyhow::Result; use axum::{
use axum::response::{IntoResponse, Response}; extract::{DefaultBodyLimit, WebSocketUpgrade},
use axum::{extract::DefaultBodyLimit, Extension}; response::{IntoResponse, Response},
use axum::{extract::WebSocketUpgrade, Json}; Extension, Json,
};
use log::info; use log::info;
use crate::app_state::AppState;
mod auth; mod auth;
mod create_document; mod create_document;
mod delete_document; mod delete_document;
@ -90,6 +92,4 @@ async fn handler(ws: WebSocketUpgrade) -> Response {
}) })
} }
async fn serve_api(Extension(api): Extension<OpenApi>) -> impl IntoResponse { async fn serve_api(Extension(api): Extension<OpenApi>) -> impl IntoResponse { Json(api) }
Json(api)
}

View file

@ -1,21 +1,20 @@
use crate::app_state::AppState;
use crate::database::models::DocumentVersionWithoutContent;
use crate::database::models::StoredDocumentVersion;
use crate::database::models::VaultId;
use crate::errors::client_error;
use crate::errors::server_error;
use crate::errors::SyncServerError;
use anyhow::Context; use anyhow::Context;
use axum::extract::Path; use axum::{
use axum::extract::State; extract::{Path, State},
use axum::Json; Json,
use axum_extra::headers::authorization::Bearer; };
use axum_extra::headers::Authorization; use axum_extra::{
use axum_extra::TypedHeader; headers::{authorization::Bearer, Authorization},
TypedHeader,
};
use sync_lib::base64_to_bytes; use sync_lib::base64_to_bytes;
use super::auth::auth; use super::{auth::auth, requests::CreateDocumentVersion};
use super::requests::CreateDocumentVersion; use crate::{
app_state::AppState,
database::models::{DocumentVersionWithoutContent, StoredDocumentVersion, VaultId},
errors::{client_error, server_error, SyncServerError},
};
#[axum::debug_handler] #[axum::debug_handler]
pub async fn create_document( pub async fn create_document(

View file

@ -1,21 +1,19 @@
use crate::app_state::AppState; use anyhow::{anyhow, Context};
use crate::database::models::DocumentId; use axum::{
use crate::database::models::StoredDocumentVersion; extract::{Path, State},
use crate::database::models::VaultId; Json,
use crate::errors::not_found_error; };
use crate::errors::server_error; use axum_extra::{
use crate::errors::SyncServerError; headers::{authorization::Bearer, Authorization},
use anyhow::anyhow; TypedHeader,
use anyhow::Context; };
use axum::extract::Path;
use axum::extract::State;
use axum::Json;
use axum_extra::headers::authorization::Bearer;
use axum_extra::headers::Authorization;
use axum_extra::TypedHeader;
use super::auth::auth; use super::{auth::auth, requests::DeleteDocumentVersion};
use super::requests::DeleteDocumentVersion; use crate::{
app_state::AppState,
database::models::{DocumentId, StoredDocumentVersion, VaultId},
errors::{not_found_error, server_error, SyncServerError},
};
#[axum::debug_handler] #[axum::debug_handler]
pub async fn delete_document( pub async fn delete_document(

View file

@ -1,19 +1,19 @@
use crate::app_state::AppState;
use crate::database::models::DocumentId;
use crate::database::models::DocumentVersion;
use crate::database::models::VaultId;
use crate::errors::not_found_error;
use crate::errors::server_error;
use crate::errors::SyncServerError;
use anyhow::anyhow; use anyhow::anyhow;
use axum::extract::Path; use axum::{
use axum::extract::State; extract::{Path, State},
use axum::Json; Json,
use axum_extra::headers::authorization::Bearer; };
use axum_extra::headers::Authorization; use axum_extra::{
use axum_extra::TypedHeader; headers::{authorization::Bearer, Authorization},
TypedHeader,
};
use super::auth::auth; use super::auth::auth;
use crate::{
app_state::AppState,
database::models::{DocumentId, DocumentVersion, VaultId},
errors::{not_found_error, server_error, SyncServerError},
};
#[axum::debug_handler] #[axum::debug_handler]
pub async fn fetch_latest_document_version( pub async fn fetch_latest_document_version(

View file

@ -1,16 +1,18 @@
use crate::app_state::AppState; use axum::{
use crate::database::models::DocumentVersionWithoutContent; extract::{Path, State},
use crate::database::models::VaultId; Json,
use crate::errors::server_error; };
use crate::errors::SyncServerError; use axum_extra::{
use axum::extract::Path; headers::{authorization::Bearer, Authorization},
use axum::extract::State; TypedHeader,
use axum::Json; };
use axum_extra::headers::authorization::Bearer;
use axum_extra::headers::Authorization;
use axum_extra::TypedHeader;
use super::auth::auth; use super::auth::auth;
use crate::{
app_state::AppState,
database::models::{DocumentVersionWithoutContent, VaultId},
errors::{server_error, SyncServerError},
};
#[axum::debug_handler] #[axum::debug_handler]
pub async fn fetch_latest_documents( pub async fn fetch_latest_documents(

View file

@ -1,25 +1,20 @@
use crate::app_state::AppState; use anyhow::{anyhow, Context};
use crate::database::models::DocumentId; use axum::{
use crate::database::models::DocumentVersionWithoutContent; extract::{Path, State},
use crate::database::models::StoredDocumentVersion; Json,
use crate::database::models::VaultId; };
use crate::errors::client_error; use axum_extra::{
use crate::errors::not_found_error; headers::{authorization::Bearer, Authorization},
use crate::errors::server_error; TypedHeader,
use crate::errors::SyncServerError; };
use anyhow::anyhow; use sync_lib::{base64_to_bytes, base64_to_string};
use anyhow::Context;
use axum::extract::Path;
use axum::extract::State;
use axum::Json;
use axum_extra::headers::authorization::Bearer;
use axum_extra::headers::Authorization;
use axum_extra::TypedHeader;
use sync_lib::base64_to_bytes;
use sync_lib::base64_to_string;
use super::auth::auth; use super::{auth::auth, requests::UpdateDocumentVersion};
use super::requests::UpdateDocumentVersion; use crate::{
app_state::AppState,
database::models::{DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId},
errors::{client_error, not_found_error, server_error, SyncServerError},
};
#[axum::debug_handler] #[axum::debug_handler]
pub async fn update_document( pub async fn update_document(

View file

@ -14,6 +14,4 @@ use wasm_bindgen::prelude::*;
// } // }
#[wasm_bindgen] #[wasm_bindgen]
pub fn greet() -> String { pub fn greet() -> String { "hi".to_string() }
"hi".to_string()
}