Update formatting
This commit is contained in:
parent
dda356ea00
commit
49638e5aa7
27 changed files with 239 additions and 232 deletions
|
|
@ -12,24 +12,27 @@
|
|||
//!
|
||||
//! # Heuristics
|
||||
//!
|
||||
//! At present this implementation of Myers' does not implement any more advanced
|
||||
//! heuristics that would solve some pathological cases. For instance passing two
|
||||
//! large and completely distinct sequences to the algorithm will make it spin
|
||||
//! without making reasonable progress.
|
||||
//! At present this implementation of Myers' does not implement any more
|
||||
//! advanced heuristics that would solve some pathological cases. For instance
|
||||
//! passing two large and completely distinct sequences to the algorithm will
|
||||
//! make it spin without making reasonable progress.
|
||||
//! For potential improvements here see [similar#15](https://github.com/mitsuhiko/similar/issues/15).
|
||||
|
||||
use std::ops::{Index, IndexMut, Range};
|
||||
use std::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 std::{
|
||||
ops::{Index, IndexMut, Range},
|
||||
vec,
|
||||
};
|
||||
|
||||
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.
|
||||
///
|
||||
/// 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
|
||||
/// 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.
|
||||
|
||||
/// `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
|
||||
/// `y` can be computed from `x - k`. In other words, `V` is an array of integers
|
||||
/// where `V[k]` contains the row index of the endpoint of the furthest reaching
|
||||
/// path in diagonal `k`.
|
||||
/// recorded endpoint `(x,y)` in diagonal `k`, we only need to retain `x`
|
||||
/// because `y` can be computed from `x - k`. In other words, `V` is an array of
|
||||
/// integers where `V[k]` contains the row index of the endpoint of the furthest
|
||||
/// reaching path in diagonal `k`.
|
||||
///
|
||||
/// 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
|
||||
/// light-weight wrapper around a Vec plus an `offset` which is the maximum value
|
||||
/// `k` can take on in order to map negative `k`'s back to a value >= 0.
|
||||
/// light-weight wrapper around a Vec plus an `offset` which is the maximum
|
||||
/// value `k` can take on in order to map negative `k`'s back to a value >= 0.
|
||||
#[derive(Debug)]
|
||||
struct V {
|
||||
offset: isize,
|
||||
|
|
@ -81,17 +84,13 @@ impl V {
|
|||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.v.len()
|
||||
}
|
||||
fn len(&self) -> usize { self.v.len() }
|
||||
}
|
||||
|
||||
impl Index<isize> for V {
|
||||
type Output = usize;
|
||||
|
||||
fn index(&self, index: isize) -> &Self::Output {
|
||||
&self.v[(index + self.offset) as usize]
|
||||
}
|
||||
fn index(&self, index: isize) -> &Self::Output { &self.v[(index + self.offset) as usize] }
|
||||
}
|
||||
|
||||
impl IndexMut<isize> for V {
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ where
|
|||
self.tokens().iter().map(|t| t.original()).collect()
|
||||
}
|
||||
|
||||
/// Extends the operation with another operation if returning the new operation.
|
||||
/// Only operations of the same type can be used to extend. If the operations are of different
|
||||
/// types, returns None.
|
||||
/// Extends the operation with another operation if returning the new
|
||||
/// operation. Only operations of the same type can be used to extend.
|
||||
/// If the operations are of different types, returns None.
|
||||
pub fn extend(self, other: RawOperation<T>) -> Option<RawOperation<T>> {
|
||||
match (self, other) {
|
||||
(RawOperation::Insert(tokens1), RawOperation::Insert(tokens2)) => Some(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,5 @@ mod operation_transformation;
|
|||
mod tokenizer;
|
||||
mod utils;
|
||||
|
||||
pub use operation_transformation::reconcile;
|
||||
pub use operation_transformation::reconcile_with_tokenizer;
|
||||
pub use operation_transformation::EditedText;
|
||||
pub use operation_transformation::{reconcile, reconcile_with_tokenizer, EditedText};
|
||||
pub use tokenizer::token::Token;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
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.
|
||||
/// EditedText supports merging two sequences of operations using the
|
||||
/// principle of Operational Transformation.
|
||||
///
|
||||
/// It's mainly created through the from_strings method, then merged with another
|
||||
/// EditedText derived from the same original text and then applied to the original text
|
||||
/// to get the reconciled text of concurrent edits.
|
||||
/// It's mainly created through the from_strings method, then merged with
|
||||
/// another EditedText derived from the same original text and then applied to
|
||||
/// the original text to get the reconciled text of concurrent edits.
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct EditedText<'a, T>
|
||||
|
|
@ -30,10 +32,12 @@ where
|
|||
}
|
||||
|
||||
impl<'a> EditedText<'a, String> {
|
||||
/// Create an EditedText from the given original (old) and updated (new) strings.
|
||||
/// The returned EditedText represents the changes from the original to the updated text.
|
||||
/// When the return value is applied to 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.
|
||||
/// Create an EditedText from the given original (old) and updated (new)
|
||||
/// strings. The returned EditedText represents the changes from the
|
||||
/// original to the updated text. When the return value is applied to
|
||||
/// 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 {
|
||||
Self::from_strings_with_tokenizer(original, updated, &word_tokenizer)
|
||||
}
|
||||
|
|
@ -43,10 +47,11 @@ impl<'a, T> EditedText<'a, T>
|
|||
where
|
||||
T: PartialEq + Clone,
|
||||
{
|
||||
/// Create an EditedText from the given original (old) and updated (new) strings.
|
||||
/// The returned EditedText represents the changes from the original to the updated text.
|
||||
/// When the return value is applied to the original text, it will result in the updated text.
|
||||
/// The tokenizer function is used to tokenize the text.
|
||||
/// Create an EditedText from the given original (old) and updated (new)
|
||||
/// strings. The returned EditedText represents the changes from the
|
||||
/// original to the updated text. When the return value is applied to
|
||||
/// 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(
|
||||
original: &'a 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>>
|
||||
where
|
||||
I: IntoIterator<Item = RawOperation<T>>,
|
||||
|
|
@ -162,8 +168,8 @@ where
|
|||
}
|
||||
|
||||
/// 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 not overlap.
|
||||
/// The operations must be in the order in which they are meant to be
|
||||
/// applied. The operations must not overlap.
|
||||
fn new(text: &'a str, operations: Vec<OrderedOperation<T>>) -> Self {
|
||||
operations
|
||||
.iter()
|
||||
|
|
@ -227,7 +233,8 @@ where
|
|||
///
|
||||
/// # 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 {
|
||||
let mut builder: StringBuilder<'_> = StringBuilder::new(self.text);
|
||||
|
||||
|
|
|
|||
|
|
@ -39,12 +39,10 @@ impl<T> MergeContext<T>
|
|||
where
|
||||
T: PartialEq + Clone,
|
||||
{
|
||||
pub fn last_operation(&self) -> Option<&Operation<T>> {
|
||||
self.last_operation.as_ref()
|
||||
}
|
||||
pub fn last_operation(&self) -> Option<&Operation<T>> { self.last_operation.as_ref() }
|
||||
|
||||
/// Replace the last delete operation (if there was one) with a new one while
|
||||
/// applying it to the shift.
|
||||
/// Replace the last delete operation (if there was one) with a new one
|
||||
/// while applying it to the shift.
|
||||
pub fn consume_and_replace_last_operation(&mut self, operation: Option<Operation<T>>) {
|
||||
if let Some(Operation::Delete {
|
||||
deleted_character_count,
|
||||
|
|
@ -62,8 +60,8 @@ where
|
|||
}
|
||||
|
||||
/// 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
|
||||
/// a delete.
|
||||
/// threshold operation. This changes the shift in case the last operation
|
||||
/// was a delete.
|
||||
pub fn consume_last_operation_if_it_is_too_behind(
|
||||
&mut self,
|
||||
threshold_operation: &Operation<T>,
|
||||
|
|
|
|||
|
|
@ -33,11 +33,13 @@ where
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::{fs, ops::Range, path::Path};
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use test_case::test_matrix;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_merges() {
|
||||
// Both replaced one token but different
|
||||
|
|
@ -64,7 +66,8 @@ mod test {
|
|||
"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(
|
||||
"original_1 original_2 original_3 original_4 original_5",
|
||||
"original_1 original_5",
|
||||
|
|
@ -102,7 +105,8 @@ mod test {
|
|||
"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(
|
||||
"both delete the same word",
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
use crate::utils::find_common_overlap::find_common_overlap;
|
||||
use crate::utils::string_builder::StringBuilder;
|
||||
use crate::Token;
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::Display;
|
||||
use std::ops::Range;
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
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.
|
||||
/// Operation is tied to a ropey::Rope and is mainly expected to be
|
||||
|
|
@ -38,7 +40,8 @@ where
|
|||
T: PartialEq + Clone,
|
||||
{
|
||||
/// 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> {
|
||||
if text.is_empty() {
|
||||
return None;
|
||||
|
|
@ -47,8 +50,9 @@ where
|
|||
Some(Operation::Insert { index, text })
|
||||
}
|
||||
|
||||
/// Creates a delete operation with the given index and number of to-be-deleted characters.
|
||||
/// If the operation would delete 0 (meaning that the operation would be a no-op), returns None.
|
||||
/// Creates a delete operation with the given index and number of
|
||||
/// 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> {
|
||||
if deleted_character_count == 0 {
|
||||
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
|
||||
///
|
||||
/// Returns a SyncLibError::OperationApplicationError if the operation cannot be applied.
|
||||
/// Returns a SyncLibError::OperationApplicationError if the operation
|
||||
/// cannot be applied.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When compiled in debug mode, panics if a delete operation is attempted on a range
|
||||
/// of text that does not match the text to be deleted.
|
||||
/// When compiled in debug mode, panics if a delete operation is attempted
|
||||
/// 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> {
|
||||
match self {
|
||||
Operation::Insert { text, .. } => builder.insert(
|
||||
|
|
@ -135,11 +141,10 @@ where
|
|||
}
|
||||
|
||||
/// Returns the range of indices of characters that the operation affects.
|
||||
pub fn range(&self) -> Range<usize> {
|
||||
self.start_index()..self.end_index() + 1
|
||||
}
|
||||
pub fn range(&self) -> Range<usize> { 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 {
|
||||
match self {
|
||||
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 {
|
||||
match self {
|
||||
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.
|
||||
/// The offset can be negative but the resulting index must be non-negative.
|
||||
/// Creates a new operation with the same type and text but with the index
|
||||
/// shifted by the given offset. The offset can be negative but the
|
||||
/// resulting index must be non-negative.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
|
|
@ -185,8 +192,9 @@ where
|
|||
self.with_index(index as usize)
|
||||
}
|
||||
|
||||
/// Merges the operation with the given context, producing a new operation and updating the context.
|
||||
/// This implements a comples FSM that handles the merging of operations in a way that is consistent with the text.
|
||||
/// Merges the operation with the given context, producing a new operation
|
||||
/// 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.
|
||||
pub fn merge_operations_with_context(
|
||||
self,
|
||||
|
|
@ -242,9 +250,10 @@ where
|
|||
produced_context.shift += operation.len() as i64;
|
||||
|
||||
debug_assert!(
|
||||
last_delete.range().contains(&operation.start_index()),
|
||||
"There is a last delete ({last_delete}) but the operation ({operation}) is not contained in it"
|
||||
);
|
||||
last_delete.range().contains(&operation.start_index()),
|
||||
"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;
|
||||
|
||||
|
|
@ -266,9 +275,10 @@ where
|
|||
Some(last_delete @ Operation::Delete { .. }),
|
||||
) => {
|
||||
debug_assert!(
|
||||
last_delete.range().contains(&operation.start_index()),
|
||||
"There is a last delete ({last_delete}) but the operation ({operation}) is not contained in it"
|
||||
);
|
||||
last_delete.range().contains(&operation.start_index()),
|
||||
"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;
|
||||
|
||||
|
|
@ -341,16 +351,15 @@ impl<T> Debug for Operation<T>
|
|||
where
|
||||
T: PartialEq + Clone,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self)
|
||||
}
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_shifting_error() {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 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))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Token<T>
|
||||
|
|
@ -33,24 +34,16 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub fn original(&self) -> &str {
|
||||
&self.original
|
||||
}
|
||||
pub fn original(&self) -> &str { &self.original }
|
||||
|
||||
pub fn normalised(&self) -> &T {
|
||||
&self.normalised
|
||||
}
|
||||
pub fn normalised(&self) -> &T { &self.normalised }
|
||||
|
||||
pub fn get_original_length(&self) -> usize {
|
||||
self.original.chars().count()
|
||||
}
|
||||
pub fn get_original_length(&self) -> usize { self.original.chars().count() }
|
||||
}
|
||||
|
||||
impl<T> PartialEq for Token<T>
|
||||
where
|
||||
T: PartialEq + Clone,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.normalised == other.normalised
|
||||
}
|
||||
fn eq(&self, other: &Self) -> bool { self.normalised == other.normalised }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,10 @@ where
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_common_prefix_len() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ where
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_common_suffix_len() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
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.
|
||||
/// Thus, the suffix of the old list from the offset to the end is equal to a prefix of the new list.
|
||||
/// 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.
|
||||
/// 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
|
||||
/// ```
|
||||
|
|
@ -27,9 +30,10 @@ where
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_common_overlap() {
|
||||
assert_eq!(find_common_overlap(&["".into()], &["".into()]), 0);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::cmp::Ordering;
|
||||
use std::iter::Peekable;
|
||||
use std::{cmp::Ordering, iter::Peekable};
|
||||
|
||||
pub struct MergeAscending<L, R, F, O>
|
||||
where
|
||||
|
|
@ -70,9 +69,10 @@ impl<T: ?Sized> MergeSorted for T where T: Iterator {}
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_merge_sorted_by_key() {
|
||||
let left = [9, 7, 5, 3, 1];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
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.
|
||||
/// It is safe to use with UTF-8 strings as all operations are based on character indices.
|
||||
/// A helper for building a string in order based on an original string and a
|
||||
/// 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)]
|
||||
pub struct StringBuilder<'a> {
|
||||
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) {
|
||||
self.copy_until(from);
|
||||
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>) {
|
||||
self.copy_until(range.start);
|
||||
self.last_old_char_index += range.len();
|
||||
|
|
@ -50,7 +53,8 @@ impl StringBuilder<'_> {
|
|||
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 {
|
||||
self.buffer.push_str(
|
||||
&self
|
||||
|
|
|
|||
8
backend/rustfmt.toml
Normal file
8
backend/rustfmt.toml
Normal 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
|
||||
|
|
@ -3,13 +3,9 @@ use errors::SyncLibError;
|
|||
|
||||
pub mod errors;
|
||||
|
||||
pub fn bytes_to_base64(input: &[u8]) -> String {
|
||||
STANDARD_NO_PAD.encode(input)
|
||||
}
|
||||
pub fn bytes_to_base64(input: &[u8]) -> String { STANDARD_NO_PAD.encode(input) }
|
||||
|
||||
pub fn string_to_base64(input: &str) -> String {
|
||||
bytes_to_base64(input.as_bytes())
|
||||
}
|
||||
pub fn string_to_base64(input: &str) -> String { bytes_to_base64(input.as_bytes()) }
|
||||
|
||||
pub fn base64_to_bytes(input: &str) -> Result<Vec<u8>, SyncLibError> {
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn is_binary(data: &[u8]) -> bool {
|
||||
data.iter().any(|&b| b == 0)
|
||||
}
|
||||
pub fn is_binary(data: &[u8]) -> bool { data.iter().any(|&b| b == 0) }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{config::Config, consts::CONFIG_PATH, database::Database};
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{config::Config, consts::CONFIG_PATH, database::Database};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppState {
|
||||
pub config: Config,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use rand::distributions::Alphanumeric;
|
||||
use rand::{thread_rng, Rng};
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ use anyhow::{Context, Result};
|
|||
use models::{
|
||||
DocumentId, DocumentVersionId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId,
|
||||
};
|
||||
use sqlx::sqlite::SqliteConnectOptions;
|
||||
use sqlx::types::chrono::Utc;
|
||||
use sqlx::{sqlite::SqliteConnectOptions, types::chrono::Utc};
|
||||
pub mod models;
|
||||
use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite};
|
||||
|
||||
|
|
|
|||
|
|
@ -21,9 +21,7 @@ pub struct StoredDocumentVersion {
|
|||
}
|
||||
|
||||
impl StoredDocumentVersion {
|
||||
pub fn content_as_string(&self) -> String {
|
||||
String::from_utf8_lossy(&self.content).to_string()
|
||||
}
|
||||
pub fn content_as_string(&self) -> String { String::from_utf8_lossy(&self.content).to_string() }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||
|
|
|
|||
|
|
@ -82,9 +82,7 @@ impl OperationOutput for SyncServerError {
|
|||
type Inner = Self;
|
||||
}
|
||||
|
||||
pub fn init_error(error: anyhow::Error) -> SyncServerError {
|
||||
SyncServerError::InitError(error)
|
||||
}
|
||||
pub fn init_error(error: anyhow::Error) -> SyncServerError { SyncServerError::InitError(error) }
|
||||
|
||||
pub fn server_error(error: anyhow::Error) -> SyncServerError {
|
||||
warn!("Server error: {:?}", error);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::app_state::AppState;
|
||||
use aide::{
|
||||
axum::{
|
||||
routing::{delete, get, post, put},
|
||||
|
|
@ -7,12 +6,15 @@ use aide::{
|
|||
openapi::{Info, OpenApi},
|
||||
scalar::Scalar,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::{extract::DefaultBodyLimit, Extension};
|
||||
use axum::{extract::WebSocketUpgrade, Json};
|
||||
use anyhow::{Context, Result};
|
||||
use axum::{
|
||||
extract::{DefaultBodyLimit, WebSocketUpgrade},
|
||||
response::{IntoResponse, Response},
|
||||
Extension, Json,
|
||||
};
|
||||
use log::info;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
mod auth;
|
||||
mod create_document;
|
||||
mod delete_document;
|
||||
|
|
@ -90,6 +92,4 @@ async fn handler(ws: WebSocketUpgrade) -> Response {
|
|||
})
|
||||
}
|
||||
|
||||
async fn serve_api(Extension(api): Extension<OpenApi>) -> impl IntoResponse {
|
||||
Json(api)
|
||||
}
|
||||
async fn serve_api(Extension(api): Extension<OpenApi>) -> impl IntoResponse { Json(api) }
|
||||
|
|
|
|||
|
|
@ -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 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 axum::{
|
||||
extract::{Path, State},
|
||||
Json,
|
||||
};
|
||||
use axum_extra::{
|
||||
headers::{authorization::Bearer, Authorization},
|
||||
TypedHeader,
|
||||
};
|
||||
use sync_lib::base64_to_bytes;
|
||||
|
||||
use super::auth::auth;
|
||||
use super::requests::CreateDocumentVersion;
|
||||
use super::{auth::auth, requests::CreateDocumentVersion};
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
database::models::{DocumentVersionWithoutContent, StoredDocumentVersion, VaultId},
|
||||
errors::{client_error, server_error, SyncServerError},
|
||||
};
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn create_document(
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
use crate::app_state::AppState;
|
||||
use crate::database::models::DocumentId;
|
||||
use crate::database::models::StoredDocumentVersion;
|
||||
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::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 anyhow::{anyhow, Context};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
Json,
|
||||
};
|
||||
use axum_extra::{
|
||||
headers::{authorization::Bearer, Authorization},
|
||||
TypedHeader,
|
||||
};
|
||||
|
||||
use super::auth::auth;
|
||||
use super::requests::DeleteDocumentVersion;
|
||||
use super::{auth::auth, requests::DeleteDocumentVersion};
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
database::models::{DocumentId, StoredDocumentVersion, VaultId},
|
||||
errors::{not_found_error, server_error, SyncServerError},
|
||||
};
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn delete_document(
|
||||
|
|
|
|||
|
|
@ -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 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 axum::{
|
||||
extract::{Path, State},
|
||||
Json,
|
||||
};
|
||||
use axum_extra::{
|
||||
headers::{authorization::Bearer, Authorization},
|
||||
TypedHeader,
|
||||
};
|
||||
|
||||
use super::auth::auth;
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
database::models::{DocumentId, DocumentVersion, VaultId},
|
||||
errors::{not_found_error, server_error, SyncServerError},
|
||||
};
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn fetch_latest_document_version(
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
use crate::app_state::AppState;
|
||||
use crate::database::models::DocumentVersionWithoutContent;
|
||||
use crate::database::models::VaultId;
|
||||
use crate::errors::server_error;
|
||||
use crate::errors::SyncServerError;
|
||||
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 axum::{
|
||||
extract::{Path, State},
|
||||
Json,
|
||||
};
|
||||
use axum_extra::{
|
||||
headers::{authorization::Bearer, Authorization},
|
||||
TypedHeader,
|
||||
};
|
||||
|
||||
use super::auth::auth;
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
database::models::{DocumentVersionWithoutContent, VaultId},
|
||||
errors::{server_error, SyncServerError},
|
||||
};
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn fetch_latest_documents(
|
||||
|
|
|
|||
|
|
@ -1,25 +1,20 @@
|
|||
use crate::app_state::AppState;
|
||||
use crate::database::models::DocumentId;
|
||||
use crate::database::models::DocumentVersionWithoutContent;
|
||||
use crate::database::models::StoredDocumentVersion;
|
||||
use crate::database::models::VaultId;
|
||||
use crate::errors::client_error;
|
||||
use crate::errors::not_found_error;
|
||||
use crate::errors::server_error;
|
||||
use crate::errors::SyncServerError;
|
||||
use anyhow::anyhow;
|
||||
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 anyhow::{anyhow, Context};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
Json,
|
||||
};
|
||||
use axum_extra::{
|
||||
headers::{authorization::Bearer, Authorization},
|
||||
TypedHeader,
|
||||
};
|
||||
use sync_lib::{base64_to_bytes, base64_to_string};
|
||||
|
||||
use super::auth::auth;
|
||||
use super::requests::UpdateDocumentVersion;
|
||||
use super::{auth::auth, 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]
|
||||
pub async fn update_document(
|
||||
|
|
|
|||
|
|
@ -14,6 +14,4 @@ use wasm_bindgen::prelude::*;
|
|||
// }
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn greet() -> String {
|
||||
"hi".to_string()
|
||||
}
|
||||
pub fn greet() -> String { "hi".to_string() }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue