diff --git a/crates/roughly/src/completion.rs b/crates/roughly/src/completion.rs index 362bee6..39c5aaf 100644 --- a/crates/roughly/src/completion.rs +++ b/crates/roughly/src/completion.rs @@ -57,7 +57,7 @@ pub fn get( completions .into_iter() .map(|item| CompletionItem { - label: item.clone(), + label: item, sort_text: Some("1".into()), ..Default::default() }) @@ -157,7 +157,7 @@ pub fn get( }) .filter(|name| utils::starts_with_lowercase(name, &query)) .map(|label| CompletionItem { - label: label.clone(), + label, label_details: Some(CompletionItemLabelDetails { detail: None, description: Some("Parameter".into()), @@ -175,13 +175,6 @@ pub fn get( .into_iter() .filter(|item| { utils::starts_with_lowercase(&item.label, &query) - }) - .map(|mut item| { - // Update sort_text for local variables to ensure consistent precedence - if let Some(sort_text) = &item.sort_text { - item.sort_text = Some(sort_text.clone()); - } - item }), ); } @@ -259,9 +252,8 @@ pub fn locals_completion(node: Node, rope: &Rope) -> Vec { && maybe_op .is_some_and(|op| [kind::EQUAL, kind::LEFT_ASSIGN].contains(&op.kind_id())) { - let label = rope.byte_slice(lhs.byte_range()).to_string(); symbols.push(CompletionItem { - label: label.clone(), + label: rope.byte_slice(lhs.byte_range()).to_string(), label_details: Some(CompletionItemLabelDetails { detail: None, description: Some("Local".into()), diff --git a/crates/roughly/src/diagnostics/unused.rs b/crates/roughly/src/diagnostics/unused.rs index 150aa9f..f6d1cab 100644 --- a/crates/roughly/src/diagnostics/unused.rs +++ b/crates/roughly/src/diagnostics/unused.rs @@ -194,7 +194,8 @@ fn traverse<'a>( let params = field(node, "parameters")?; - for param in params.children_by_field_name("parameter", &mut cursor.clone()) { + let mut param_cursor = params.walk(); + for param in params.children_by_field_name("parameter", &mut param_cursor) { let name = field(param, "name")?; if name.kind() == "identifier" { let raw = rope.byte_slice(name.byte_range()).to_string(); diff --git a/crates/roughly/src/format.rs b/crates/roughly/src/format.rs index cba015f..6fc4122 100644 --- a/crates/roughly/src/format.rs +++ b/crates/roughly/src/format.rs @@ -91,15 +91,13 @@ pub fn format(node: Node, rope: &Rope, config: Config) -> Result "\r\n", }; + let base_indent = " ".repeat(config.indent_width); let mut buffer = String::with_capacity(rope.len_bytes() * 3 / 2); + let context = Context::new(rope, &base_indent, line_ending); traverse( &mut buffer, &mut node.walk(), - Context { - rope, - indent: &" ".repeat(config.indent_width), - line_ending, - }, + &context, 0, false, )?; @@ -115,11 +113,39 @@ pub fn format(node: Node, rope: &Rope, config: Config) -> Result { rope: &'a Rope, indent: &'a str, line_ending: &'static str, + // Pre-computed indentation strings for common nesting levels (0-10) + indent_cache: Vec, +} + +impl<'a> Context<'a> { + fn new(rope: &'a Rope, indent: &'a str, line_ending: &'static str) -> Self { + // Pre-compute indentation strings for levels 0 through 10 + let mut indent_cache = vec![String::new()]; + for i in 1..=10 { + indent_cache.push(indent.repeat(i)); + } + + Context { + rope, + indent, + line_ending, + indent_cache, + } + } + + #[inline] + fn get_indent(&self, level: usize) -> String { + if level < self.indent_cache.len() { + self.indent_cache[level].clone() + } else { + self.indent.repeat(level) + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -133,7 +159,7 @@ enum Directive { fn traverse( out: &mut String, cursor: &mut TreeCursor, - context: Context, + context: &Context, level: usize, make_multiline: bool, ) -> Result<(), FormatError> { @@ -141,13 +167,13 @@ fn traverse( let indent = |out: &mut String| out.push_str(context.indent); let newline = |out: &mut String| { out.push_str(context.line_ending); - out.push_str(&context.indent.repeat(level)); + out.push_str(&context.get_indent(level)); }; let newlines = |out: &mut String, node: Node, maybe_prev: Option| { out.push_str(&context.line_ending.repeat(maybe_prev.map_or(0, |prev| { usize::clamp(node.start_position().row - prev.end_position().row, 1, 2) }))); - out.push_str(&context.indent.repeat(level)); + out.push_str(&context.get_indent(level)); }; let fmt_with = @@ -239,7 +265,7 @@ fn traverse( match chars.next() { Some(' ') | None => out.push_str(raw), // avoid formatting #'string' - Some(_) if char == '\'' && chars.clone().contains(&'\'') => { + Some(_) if char == '\'' && chars.clone().any(|c| c == '\'') => { out.push_str(raw); } Some(other) => { @@ -256,7 +282,7 @@ fn traverse( Some(other) => { out.push_str("# "); out.push(other); - out.push_str(&chars.collect::()); + out.extend(chars); } } } diff --git a/crates/roughly/src/utils.rs b/crates/roughly/src/utils.rs index a2bc812..f7b1ec5 100644 --- a/crates/roughly/src/utils.rs +++ b/crates/roughly/src/utils.rs @@ -6,7 +6,31 @@ use { }; pub fn starts_with_lowercase(name: &str, query: &str) -> bool { - query.is_empty() || name.to_lowercase().starts_with(&query.to_lowercase()) + if query.is_empty() { + return true; + } + + // Use case-insensitive comparison without allocating strings + // This handles Unicode correctly while being more efficient than allocating + let mut name_chars = name.chars(); + let mut query_chars = query.chars(); + + loop { + match (query_chars.next(), name_chars.next()) { + (None, _) => return true, // Consumed all of query, so name starts with query + (Some(_), None) => return false, // Query is longer than name + (Some(q), Some(n)) => { + // Use eq_ignore_ascii_case for ASCII fast path, fall back to Unicode for others + if q.is_ascii() && n.is_ascii() { + if !q.eq_ignore_ascii_case(&n) { + return false; + } + } else if !q.to_lowercase().eq(n.to_lowercase()) { + return false; + } + } + } + } } pub fn read_to_rope(path: impl AsRef) -> std::io::Result {