#[must_use]
pub fn safe_slice(s: &str, start: usize, end: usize) -> &str {
let lo = s.floor_char_boundary(start.min(s.len()));
let hi = s.ceil_char_boundary(end.min(s.len()));
&s[lo..hi]
}
#[must_use]
pub fn safe_prefix(s: &str, end: usize) -> &str {
&s[..s.floor_char_boundary(end.min(s.len()))]
}
#[must_use]
pub fn safe_suffix(s: &str, start: usize) -> &str {
&s[s.ceil_char_boundary(start.min(s.len()))..]
}
#[must_use]
pub fn snap_range(s: &str, start: usize, end: usize) -> (usize, usize) {
(
s.floor_char_boundary(start.min(s.len())),
s.ceil_char_boundary(end.min(s.len())),
)
}
#[must_use]
pub fn min_suggestion_distance(token: &str, suggestions: &[String]) -> Option<usize> {
let lowered = token.to_lowercase();
suggestions
.iter()
.filter(|s| !s.chars().any(char::is_whitespace))
.map(|s| strsim::damerau_levenshtein(&lowered, &s.to_lowercase()))
.min()
}
#[cfg(test)]
mod tests {
use super::{min_suggestion_distance, safe_prefix, safe_slice, safe_suffix, snap_range};
#[test]
fn ascii_slice_is_exact() {
assert_eq!(safe_slice("hello world", 0, 5), "hello");
assert_eq!(safe_slice("hello world", 6, 11), "world");
}
#[test]
fn snaps_offsets_inside_multibyte_chars() {
let s = "Ölförderung";
let slice = safe_slice(s, 1, 4);
assert!(s.starts_with(slice) || s.contains(slice));
assert!(slice.is_char_boundary(0));
}
#[test]
fn clamps_out_of_range_offsets() {
assert_eq!(safe_slice("abc", 0, 999), "abc");
assert_eq!(safe_slice("abc", 999, 999), "");
}
#[test]
fn prefix_and_suffix_snap_away_from_a_split_char() {
let s = "Föö";
assert_eq!(safe_prefix(s, 2), "F"); assert_eq!(safe_suffix(s, 2), "ö"); assert_eq!(safe_prefix(s, 0), "");
assert_eq!(safe_suffix(s, 999), "");
}
#[test]
fn snap_range_reports_whether_it_moved() {
let s = "Föö";
assert_eq!(snap_range(s, 0, 3), (0, 3));
assert_eq!(snap_range(s, 2, 2), (1, 3));
assert_eq!(snap_range(s, 0, 999), (0, s.len()));
}
#[test]
fn prefix_and_suffix_partition_on_a_real_boundary() {
let s = "Föö";
assert_eq!(format!("{}{}", safe_prefix(s, 3), safe_suffix(s, 3)), s);
}
#[test]
fn word_split_suggestions_are_not_evidence_of_a_typo() {
let sugg = vec!["Abram sky".to_string()];
assert_eq!(min_suggestion_distance("Abramsky", &sugg), None);
}
#[test]
fn suggestion_distance_is_case_insensitive() {
let sugg = vec!["Hoar".to_string()];
assert_eq!(min_suggestion_distance("Hoare", &sugg), Some(1));
assert_eq!(min_suggestion_distance("recieve", &[]), None);
}
}
pub fn in_comment<T>(line: &str, parse: impl Fn(&str) -> Option<T>) -> Option<T> {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("<!--")
&& let Some(inner) = rest.strip_suffix("-->")
{
return parse(inner.trim());
}
if let Some(rest) = trimmed.strip_prefix("//") {
return parse(rest.trim());
}
if let Some(rest) = trimmed.strip_prefix("/*")
&& let Some(inner) = rest.strip_suffix("*/")
{
return parse(inner.trim());
}
if let Some(rest) = trimmed.strip_prefix('%') {
return parse(rest.trim());
}
None
}
#[derive(Debug, Default)]
pub struct FenceTracker {
open: Option<(u8, usize)>,
}
impl FenceTracker {
#[must_use]
pub const fn new() -> Self {
Self { open: None }
}
pub fn consume(&mut self, line: &str) -> bool {
let Some((marker, run)) = fence_run(line) else {
return self.open.is_some();
};
if let Some((open_marker, open_run)) = self.open {
if marker == open_marker && run >= open_run && info_string(line, marker).is_empty() {
self.open = None;
}
} else {
self.open = Some((marker, run));
}
true
}
#[must_use]
pub const fn inside(&self) -> bool {
self.open.is_some()
}
}
fn fence_run(line: &str) -> Option<(u8, usize)> {
let indent = line.len() - line.trim_start().len();
if indent > 3 {
return None;
}
let rest = line.trim_start();
let marker = match rest.as_bytes().first() {
Some(&b'`') => b'`',
Some(&b'~') => b'~',
_ => return None,
};
let run = rest.bytes().take_while(|&b| b == marker).count();
(run >= 3).then_some((marker, run))
}
fn info_string(line: &str, marker: u8) -> &str {
line.trim_start().trim_start_matches(marker as char).trim()
}