Skip to main content

perl_source_editing/
lib.rs

1//! Source text editing heuristics.
2//!
3//! This crate has a single responsibility: provide reusable source-text
4//! heuristics for insertion points and lightweight display helpers.
5
6#![deny(unsafe_code)]
7#![warn(rust_2018_idioms)]
8#![warn(missing_docs)]
9#![warn(clippy::all)]
10
11/// Find the start byte offset of the current statement at `pos`.
12#[must_use]
13pub fn find_statement_start(source: &str, pos: usize) -> usize {
14    let search_pos = pos.min(source.len());
15    source[..search_pos]
16        .char_indices()
17        .rev()
18        .find_map(|(idx, ch)| ((ch == ';') || (ch == '\n')).then_some(idx + ch.len_utf8()))
19        .unwrap_or(0)
20}
21
22/// Return indentation (leading spaces/tabs) for the line containing `pos`.
23#[must_use]
24pub fn get_indent_at(source: &str, pos: usize) -> String {
25    let clamped_pos = pos.min(source.len());
26    let line_start = source[..clamped_pos].rfind('\n').map_or(0, |idx| idx + 1);
27
28    source[line_start..].chars().take_while(|ch| *ch == ' ' || *ch == '\t').collect()
29}
30
31/// Find pragma insertion position (just after shebang if present).
32#[must_use]
33pub fn find_pragma_insert_position(source: &str) -> usize {
34    if source.starts_with("#!") { source.find('\n').map_or(source.len(), |idx| idx + 1) } else { 0 }
35}
36
37/// Find import insertion position after shebang and existing import/require lines.
38#[must_use]
39pub fn find_import_insert_position(source: &str, lines: &[String]) -> usize {
40    let mut pos = find_pragma_insert_position(source);
41
42    for line in lines {
43        if line.starts_with("use ") || line.starts_with("require ") {
44            if let Some(idx) = source.find(line) {
45                pos = idx + line.len() + 1;
46            }
47        } else if !line.is_empty() && !line.starts_with('#') {
48            break;
49        }
50    }
51
52    pos
53}
54
55/// Truncate an expression for display with `...` suffix when required.
56///
57/// `max_len` is interpreted as a character limit (not bytes) to remain UTF-8 safe.
58#[must_use]
59pub fn truncate_expr(expr: &str, max_len: usize) -> String {
60    let expr_len = expr.chars().count();
61    if expr_len <= max_len {
62        return expr.to_string();
63    }
64
65    if max_len <= 3 {
66        return "...".chars().take(max_len).collect();
67    }
68
69    let prefix: String = expr.chars().take(max_len - 3).collect();
70    format!("{prefix}...")
71}
72
73/// Return true when `source` contains non-ASCII content.
74#[must_use]
75pub fn has_non_ascii_content(source: &str) -> bool {
76    !source.is_ascii()
77}