Apply more lints

This commit is contained in:
Andras Schmelczer 2024-12-18 22:14:09 +00:00
commit 1e1dd7b877
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
24 changed files with 61 additions and 63 deletions

View file

@ -0,0 +1,2 @@
pub mod myers;
pub mod raw_operation;

View file

@ -1,4 +1,4 @@
//! Taken from https://github.com/mitsuhiko/similar/blob/7e15c44de11a1cd61e1149189929e189ef977fd8/src/algorithms/myers.rs //! Taken from <https://github.com/mitsuhiko/similar/blob/7e15c44de11a1cd61e1149189929e189ef977fd8/src/algorithms/myers.rs>
//! Myers' diff algorithm. //! Myers' diff algorithm.
//! //!
//! * time: `O((N+M)D)` //! * time: `O((N+M)D)`

View file

@ -23,12 +23,10 @@ where
} }
pub fn original_text_length(&self) -> usize { pub fn original_text_length(&self) -> usize {
self.tokens().iter().map(|t| t.get_original_length()).sum() self.tokens().iter().map(Token::get_original_length).sum()
} }
pub fn get_original_text(self) -> String { pub fn get_original_text(self) -> String { self.tokens().iter().map(Token::original).collect() }
self.tokens().iter().map(|t| t.original()).collect()
}
/// Extends the operation with another operation if returning the new /// Extends the operation with another operation if returning the new
/// operation. Only operations of the same type can be used to extend. /// operation. Only operations of the same type can be used to extend.

View file

@ -0,0 +1,198 @@
mod edited_text;
mod merge_context;
mod operation;
pub use edited_text::EditedText;
pub use operation::Operation;
use crate::tokenizer::Tokenizer;
#[must_use] pub fn reconcile(original: &str, left: &str, right: &str) -> String {
if left == right {
return left.to_owned();
}
if original == left {
return right.to_owned();
}
if original == right {
return left.to_owned();
}
let left_operations = EditedText::from_strings(original, left);
let right_operations = EditedText::from_strings(original, right);
let merged_operations = left_operations.merge(right_operations);
merged_operations.apply()
}
pub fn reconcile_with_tokenizer<F, T>(
original: &str,
left: &str,
right: &str,
tokenizer: &Tokenizer<T>,
) -> String
where
T: PartialEq + Clone,
{
let left_operations = EditedText::from_strings_with_tokenizer(original, left, tokenizer);
let right_operations = EditedText::from_strings_with_tokenizer(original, right, tokenizer);
let merged_operations = left_operations.merge(right_operations);
merged_operations.apply()
}
#[cfg(test)]
mod test {
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
test_merge_both_ways(
"original_1 original_2 original_3",
"original_1 edit_1 original_3",
"original_1 original_2 edit_2",
"original_1 edit_1 edit_2",
);
// Both replaced the same one token
test_merge_both_ways(
"original_1 original_2 original_3",
"original_1 edit_1 original_3",
"original_1 edit_1 original_3",
"original_1 edit_1 original_3",
);
// One deleted a large range, the other deleted subranges and inserted as well
test_merge_both_ways(
"original_1 original_2 original_3 original_4 original_5",
"original_1 original_5",
"original_1 edit_1 original_3 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
test_merge_both_ways(
"original_1 original_2 original_3 original_4 original_5",
"original_1 original_5",
"original_1 edit_1 original_3 edit_2",
"original_1 edit_1 edit_2",
);
// Merge a replace and an append
test_merge_both_ways("a b ", "c d ", "a b c d ", "c d c d ");
test_merge_both_ways("a b c d e", "a e", "a c e", "a e");
test_merge_both_ways("a 0 1 2 b", "a b", "a E 1 F b", "a E F b");
test_merge_both_ways(
"a this one delete b",
"a b",
"a my one change b",
"a my change b",
);
test_merge_both_ways(
"this stays, this is one big delete, don't touch this",
"this stays, don't touch this",
"this stays, my one change, don't touch this",
"this stays, my change, don't touch this",
);
test_merge_both_ways("1 2 3 4 5 6", "1 6", "1 2 4 ", "1 ");
test_merge_both_ways(
"hello world",
"hi, world",
"hello my friend!",
"hi, my friend!",
);
// test_merge_both_ways("hello world", "world !", "hi hello world", "hi world
// !");
test_merge_both_ways(
"both delete the same word",
"both the same word",
"both the same word",
"both the same word",
);
test_merge_both_ways(" ", "it’s utf-8!", " ", "it’s utf-8!");
test_merge_both_ways(
"both delete the same word but one a bit more",
"both the same word",
"both same word",
"both same word",
);
test_merge_both_ways(
"long text with one big delete and many small",
"long small",
"long with big and small",
"long small",
);
}
#[test_matrix( [
"pride_and_prejudice.txt",
"romeo_and_juliet.txt",
"room_with_a_view.txt",
"kun_lu.txt",
], [
"pride_and_prejudice.txt",
"romeo_and_juliet.txt",
"room_with_a_view.txt",
"kun_lu.txt"
], [
"pride_and_prejudice.txt",
"romeo_and_juliet.txt",
"room_with_a_view.txt",
"kun_lu.txt"
], [0..10000, 10000..20000], [0..10000, 10000..20000], [0..10000, 10000..20000])]
fn test_merge_files_without_panic(
file_name_1: &str,
file_name_2: &str,
file_name_3: &str,
range_1: Range<usize>,
range_2: Range<usize>,
range_3: Range<usize>,
) {
let files = [file_name_1, file_name_2, file_name_3];
let permutations = [range_1, range_2, range_3];
let root = Path::new("test/resources/");
let contents = files
.iter()
.zip(permutations.iter())
.map(|(file, range)| {
let path = root.join(file);
fs::read_to_string(&path)
.unwrap()
.chars()
.skip(range.start)
.take(range.end)
.collect::<String>()
})
.collect::<Vec<_>>();
reconcile(&contents[0], &contents[1], &contents[2]);
}
fn test_merge_both_ways(original: &str, edit_1: &str, edit_2: &str, expected: &str) {
assert_eq!(reconcile(original, edit_1, edit_2), expected);
assert_eq!(reconcile(original, edit_2, edit_1), expected);
}
}

View file

@ -1,4 +1,4 @@
use std::iter; use core::iter;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -9,17 +9,17 @@ use crate::{
operation_transformation::merge_context::MergeContext, operation_transformation::merge_context::MergeContext,
tokenizer::{word_tokenizer::word_tokenizer, Tokenizer}, tokenizer::{word_tokenizer::word_tokenizer, Tokenizer},
utils::{ utils::{
merge_iters::MergeSorted, ordered_operation::OrderedOperation, side::Side, merge_iters::MergeSorted as _, ordered_operation::OrderedOperation, side::Side,
string_builder::StringBuilder, 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 /// It's mainly created through the `from_strings` method, then merged with
/// another EditedText derived from the same original text and then applied to /// another `EditedText` derived from the same original text and then applied to
/// the original text 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)]
@ -32,13 +32,13 @@ where
} }
impl<'a> EditedText<'a, String> { impl<'a> EditedText<'a, String> {
/// Create an EditedText from the given original (old) and updated (new) /// Create an `EditedText` from the given original (old) and updated (new)
/// strings. The returned EditedText represents the changes from the /// strings. The returned `EditedText` represents the changes from the
/// original to the updated text. When the return value is applied to /// original to the updated text. When the return value is applied to
/// the original text, it will result in the updated text. The default /// 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 /// word tokenizer is used to tokenize the text which splits the text on
/// whitespaces. /// whitespaces.
pub fn from_strings(original: &'a str, updated: &str) -> Self { #[must_use] 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)
} }
} }
@ -47,8 +47,8 @@ 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) /// Create an `EditedText` from the given original (old) and updated (new)
/// strings. The returned EditedText represents the changes from the /// strings. The returned `EditedText` represents the changes from the
/// original to the updated text. When the return value is applied to /// original to the updated text. When the return value is applied to
/// the original text, it will result in the updated text. The tokenizer /// the original text, it will result in the updated text. The tokenizer
/// function is used to tokenize the text. /// function is used to tokenize the text.
@ -167,7 +167,7 @@ where
result result
} }
/// 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 /// The operations must be in the order in which they are meant to be
/// applied. 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 {
@ -186,7 +186,7 @@ where
Self { text, operations } Self { text, operations }
} }
pub fn merge(self, other: Self) -> Self { #[must_use] pub fn merge(self, other: Self) -> Self {
debug_assert_eq!( debug_assert_eq!(
self.text, other.text, self.text, other.text,
"EditedTexts must be derived from the same text to be mergable" "EditedTexts must be derived from the same text to be mergable"
@ -207,7 +207,7 @@ where
operation.order, operation.order,
// Operations on left and right must come in the same order so that // Operations on left and right must come in the same order so that
// inserts can be merged with other inserts and deletes with deletes. // inserts can be merged with other inserts and deletes with deletes.
matches!(operation.operation, Operation::Delete { .. }) as usize, usize::from(matches!(operation.operation, Operation::Delete { .. })),
) )
}, },
) )
@ -233,9 +233,9 @@ where
/// ///
/// # Errors /// # Errors
/// ///
/// Returns an SyncLibError::OperationError if the operations cannot be /// Returns an `SyncLibError::OperationError` if the operations cannot be
/// applied to the text. /// applied to the text.
pub fn apply(&self) -> String { #[must_use] pub fn apply(&self) -> String {
let mut builder: StringBuilder<'_> = StringBuilder::new(self.text); let mut builder: StringBuilder<'_> = StringBuilder::new(self.text);
for OrderedOperation { operation, .. } in &self.operations { for OrderedOperation { operation, .. } in &self.operations {

View file

@ -1,4 +1,4 @@
use std::fmt::Debug; use core::fmt::Debug;
use crate::operation_transformation::Operation; use crate::operation_transformation::Operation;
@ -27,7 +27,7 @@ impl<T> Debug for MergeContext<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 core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("MergeContext") f.debug_struct("MergeContext")
.field("last_operation", &self.last_operation) .field("last_operation", &self.last_operation)
.field("shift", &self.shift) .field("shift", &self.shift)

View file

@ -1,4 +1,4 @@
use std::{ use core::{
fmt::{Debug, Display}, fmt::{Debug, Display},
ops::Range, ops::Range,
}; };
@ -13,8 +13,8 @@ use crate::{
}; };
/// 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
/// created by EditedText. /// created by `EditedText`.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub enum Operation<T> pub enum Operation<T>
@ -81,12 +81,12 @@ where
}) })
} }
/// Tries to apply the operation to the given ropey::Rope text, returning /// Tries to apply the operation to the given `ropey::Rope` text, returning
/// the modified text. /// the modified text.
/// ///
/// # Errors /// # Errors
/// ///
/// Returns a SyncLibError::OperationApplicationError if the operation /// Returns a `SyncLibError::OperationApplicationError` if the operation
/// cannot be applied. /// cannot be applied.
/// ///
/// # Panics /// # Panics
@ -99,7 +99,7 @@ where
self.start_index(), self.start_index(),
&text &text
.iter() .iter()
.map(|token| token.original()) .map(super::super::tokenizer::token::Token::original)
.collect::<String>(), .collect::<String>(),
), ),
Operation::Delete { Operation::Delete {
@ -111,8 +111,7 @@ where
debug_assert!( debug_assert!(
deleted_text deleted_text
.as_ref() .as_ref()
.map(|text| builder.get_slice(self.range()) == *text) .map_or(true, |text| builder.get_slice(self.range()) == *text),
.unwrap_or(true),
"Text to delete does not match the text in the rope" "Text to delete does not match the text in the rope"
); );
@ -141,15 +140,16 @@ 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> { 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 /// Returns the number of affected characters. It is always greater than 0
/// because empty operations cannot be created. /// 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, .. } => text
text.iter().map(|token| token.get_original_length()).sum() .iter()
} .map(super::super::tokenizer::token::Token::get_original_length)
.sum(),
Operation::Delete { Operation::Delete {
deleted_character_count, deleted_character_count,
.. ..
@ -223,7 +223,7 @@ where
let trimmed_length = previous_inserted_text let trimmed_length = previous_inserted_text
.iter() .iter()
.skip(offset_in_tokens) .skip(offset_in_tokens)
.map(|token| token.get_original_length()) .map(super::super::tokenizer::token::Token::get_original_length)
.sum::<usize>(); .sum::<usize>();
let trimmed_operation = let trimmed_operation =
Operation::create_insert(index, text[trimmed_length_in_tokens..].to_vec()); Operation::create_insert(index, text[trimmed_length_in_tokens..].to_vec());
@ -231,7 +231,7 @@ where
affecting_context.shift -= trimmed_length as i64; affecting_context.shift -= trimmed_length as i64;
produced_context.shift += trimmed_operation produced_context.shift += trimmed_operation
.as_ref() .as_ref()
.map(|op| op.len()) .map(Operation::len)
.unwrap_or_default() as i64; .unwrap_or_default() as i64;
produced_context.consume_and_replace_last_operation(trimmed_operation.clone()); produced_context.consume_and_replace_last_operation(trimmed_operation.clone());
@ -305,14 +305,14 @@ impl<T> Display 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 core::fmt::Formatter<'_>) -> core::fmt::Result {
match self { match self {
Operation::Insert { index, text } => { Operation::Insert { index, text } => {
write!( write!(
f, f,
"<insert '{}' from index {}>", "<insert '{}' from index {}>",
text.iter() text.iter()
.map(|token| token.original()) .map(super::super::tokenizer::token::Token::original)
.collect::<String>(), .collect::<String>(),
index index
) )
@ -351,7 +351,7 @@ 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 { write!(f, "{}", self) } fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{self}") }
} }
#[cfg(test)] #[cfg(test)]

View file

@ -0,0 +1,6 @@
use token::Token;
pub mod token;
pub mod word_tokenizer;
pub type Tokenizer<T> = dyn Fn(&str) -> Vec<Token<T>>;

View file

@ -17,8 +17,8 @@ where
impl From<&str> for Token<String> { impl From<&str> for Token<String> {
fn from(s: &str) -> Self { fn from(s: &str) -> Self {
Token { Token {
normalised: s.to_string(), normalised: s.to_owned(),
original: s.to_string(), original: s.to_owned(),
} }
} }
} }

View file

@ -2,6 +2,6 @@ use super::token::Token;
pub fn word_tokenizer(text: &str) -> Vec<Token<String>> { pub fn word_tokenizer(text: &str) -> Vec<Token<String>> {
text.split_inclusive(char::is_whitespace) text.split_inclusive(char::is_whitespace)
.map(|s| Token::new(s.to_string(), s.to_string())) .map(|s| Token::new(s.to_owned(), s.to_owned()))
.collect() .collect()
} }

View file

@ -0,0 +1,7 @@
pub mod common_prefix_len;
pub mod common_suffix_len;
pub mod find_common_overlap;
pub mod merge_iters;
pub mod ordered_operation;
pub mod side;
pub mod string_builder;

View file

@ -1,7 +1,7 @@
use std::ops::{Index, Range}; use core::ops::{Index, Range};
/// Given two lookups and ranges calculates the length of the common prefix. /// Given two lookups and ranges calculates the length of the common prefix.
/// Copied from https://github.com/mitsuhiko/similar/blob/7e15c44de11a1cd61e1149189929e189ef977fd8/src/algorithms/utils.rs /// Copied from <https://github.com/mitsuhiko/similar/blob/7e15c44de11a1cd61e1149189929e189ef977fd8/src/algorithms/utils.rs>
pub fn common_prefix_len<Old, New>( pub fn common_prefix_len<Old, New>(
old: &Old, old: &Old,
old_range: Range<usize>, old_range: Range<usize>,

View file

@ -1,7 +1,7 @@
use std::ops::{Index, Range}; use core::ops::{Index, Range};
/// Given two lookups and ranges calculates the length of common suffix. /// Given two lookups and ranges calculates the length of common suffix.
/// Copied from https://github.com/mitsuhiko/similar/blob/7e15c44de11a1cd61e1149189929e189ef977fd8/src/algorithms/utils.rs /// Copied from <https://github.com/mitsuhiko/similar/blob/7e15c44de11a1cd61e1149189929e189ef977fd8/src/algorithms/utils.rs>
pub fn common_suffix_len<Old, New>( pub fn common_suffix_len<Old, New>(
old: &Old, old: &Old,
old_range: Range<usize>, old_range: Range<usize>,

View file

@ -1,4 +1,4 @@
use std::{cmp::Ordering, iter::Peekable}; use core::{cmp::Ordering, iter::Peekable};
pub struct MergeAscending<L, R, F, O> pub struct MergeAscending<L, R, F, O>
where where

View file

@ -1,4 +1,4 @@
use std::ops::Range; use core::ops::Range;
/// A helper for building a string in order based on an original string and a /// 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 /// series of insertions and deletions applied to it. It is safe to use with
@ -28,7 +28,7 @@ impl StringBuilder<'_> {
/// Delete a string at the given index after copying the original string up /// Delete a string at the given index after copying the original string up
/// to that index from the last insertion or deletion. /// 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: core::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();
} }

View file

@ -1,6 +1,6 @@
use std::path::Path; use std::path::Path;
use anyhow::{Context, Result}; use anyhow::{Context as _, Result};
use database_config::DatabaseConfig; use database_config::DatabaseConfig;
use log::{info, warn}; use log::{info, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View file

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

View file

@ -1,6 +1,6 @@
use core::{str::FromStr, time::Duration}; use core::{str::FromStr as _, time::Duration};
use anyhow::{Context, Result}; use anyhow::{Context as _, Result};
use models::{ use models::{
DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId, VaultUpdateId, DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId, VaultUpdateId,
}; };

View file

@ -26,7 +26,7 @@ pub enum SyncServerError {
#[error("Unauthorized: {0}")] #[error("Unauthorized: {0}")]
Unauthorized(#[source] anyhow::Error), Unauthorized(#[source] anyhow::Error),
#[allow(dead_code)] #[expect(dead_code)]
#[error("Permission denied error: {0}")] #[error("Permission denied error: {0}")]
PermissionDeniedError(#[source] anyhow::Error), PermissionDeniedError(#[source] anyhow::Error),
} }

View file

@ -5,7 +5,7 @@ mod database;
mod errors; mod errors;
mod server; mod server;
use anyhow::{Context, Result}; use anyhow::{Context as _, Result};
use app_state::AppState; use app_state::AppState;
use errors::{init_error, SyncServerError}; use errors::{init_error, SyncServerError};
use server::create_server; use server::create_server;

Some files were not shown because too many files have changed in this diff Show more