use std::collections::HashMap;
use tower_lsp::lsp_types::*;
use crate::util::short_name;
#[derive(Debug, Clone)]
pub(crate) struct UseBlockInfo {
existing: Vec<(u32, String)>,
fallback_line: u32,
}
impl UseBlockInfo {
pub(crate) fn insert_position_for(&self, fqn: &str) -> Position {
self.insert_position_for_key(&fqn.to_lowercase())
}
pub(crate) fn insert_position_for_key(&self, key: &str) -> Position {
if self.existing.is_empty() {
return Position {
line: self.fallback_line,
character: 0,
};
}
let new_group = Self::key_group(key);
let same_group: Vec<&(u32, String)> = self
.existing
.iter()
.filter(|(_, k)| Self::key_group(k) == new_group)
.collect();
if !same_group.is_empty() {
for (line, existing_key) in &same_group {
if existing_key.as_str() > key {
return Position {
line: *line,
character: 0,
};
}
}
let last_line = same_group.last().expect("non-empty").0;
return Position {
line: last_line + 1,
character: 0,
};
}
let lower: Vec<&(u32, String)> = self
.existing
.iter()
.filter(|(_, k)| Self::key_group(k) < new_group)
.collect();
if let Some(&&(last_line, _)) = lower.last() {
return Position {
line: last_line + 1,
character: 0,
};
}
let first_line = self.existing.first().expect("non-empty checked above").0;
Position {
line: first_line,
character: 0,
}
}
fn key_group(key: &str) -> u8 {
if key.starts_with("function ") {
2
} else if key.starts_with("const ") {
1
} else {
0
}
}
pub(crate) fn has_function_imports(&self) -> bool {
self.existing.iter().any(|(_, k)| Self::key_group(k) == 2)
}
pub(crate) fn has_class_imports(&self) -> bool {
self.existing.iter().any(|(_, k)| Self::key_group(k) == 0)
}
}
fn extract_use_sort_key(line: &str) -> Option<String> {
let trimmed = line.trim();
let rest = trimmed
.strip_prefix("use ")
.or_else(|| trimmed.strip_prefix("use\t"))?;
if rest.starts_with('(') {
return None;
}
let (prefix, fqn_part) = if let Some(r) = rest.strip_prefix("function ") {
("function ", r)
} else if let Some(r) = rest.strip_prefix("const ") {
("const ", r)
} else {
("", rest)
};
let fqn = fqn_part
.split(';')
.next()
.unwrap_or(fqn_part)
.split(" as ")
.next()
.unwrap_or(fqn_part)
.split('{')
.next()
.unwrap_or(fqn_part)
.trim()
.trim_start_matches('\\');
Some(format!("{}{}", prefix, fqn).to_lowercase())
}
pub(crate) fn analyze_use_block(content: &str) -> UseBlockInfo {
let mut existing: Vec<(u32, String)> = Vec::new();
let mut namespace_line: Option<u32> = None;
let mut php_open_line: Option<u32> = None;
let mut brace_depth: u32 = 0;
let mut uses_brace_namespace = false;
for (i, line) in content.lines().enumerate() {
let trimmed = line.trim();
let depth_at_start = brace_depth;
for ch in trimmed.chars() {
match ch {
'{' => brace_depth += 1,
'}' => brace_depth = brace_depth.saturating_sub(1),
_ => {}
}
}
if trimmed.starts_with("<?php") && php_open_line.is_none() {
php_open_line = Some(i as u32);
}
if trimmed.starts_with("namespace ") || trimmed.starts_with("namespace\t") {
namespace_line = Some(i as u32);
if trimmed.contains('{') {
uses_brace_namespace = true;
}
}
let max_import_depth = if uses_brace_namespace { 1 } else { 0 };
if depth_at_start <= max_import_depth
&& (trimmed.starts_with("use ") || trimmed.starts_with("use\t"))
&& !trimmed.starts_with("use (")
&& !trimmed.starts_with("use(")
&& let Some(sort_key) = extract_use_sort_key(trimmed)
{
existing.push((i as u32, sort_key));
}
}
let fallback_line = namespace_line.or(php_open_line).map(|l| l + 1).unwrap_or(0);
UseBlockInfo {
existing,
fallback_line,
}
}
pub(crate) fn use_import_conflicts(fqn: &str, file_use_map: &HashMap<String, String>) -> bool {
let sn = short_name(fqn);
let first_segment = fqn.split('\\').next().unwrap_or(fqn);
let has_namespace = fqn.contains('\\');
for (alias, existing_fqn) in file_use_map {
if alias.eq_ignore_ascii_case(sn) && !existing_fqn.eq_ignore_ascii_case(fqn) {
return true;
}
if has_namespace && alias.eq_ignore_ascii_case(first_segment) {
return true;
}
}
false
}
pub(crate) fn build_use_edit(
fqn: &str,
use_block: &UseBlockInfo,
file_namespace: &Option<String>,
) -> Option<Vec<TextEdit>> {
if !fqn.contains('\\') && file_namespace.is_none() {
return None;
}
let insert_pos = use_block.insert_position_for(fqn);
Some(vec![TextEdit {
range: Range {
start: insert_pos,
end: insert_pos,
},
new_text: format!("use {};\n", fqn),
}])
}
pub(crate) fn build_use_function_edit(
fqn: &str,
use_block: &UseBlockInfo,
) -> Option<Vec<TextEdit>> {
if !fqn.contains('\\') {
return None;
}
let sort_key = format!("function {}", fqn.to_lowercase());
let insert_pos = use_block.insert_position_for_key(&sort_key);
let separator = if !use_block.has_function_imports() && use_block.has_class_imports() {
"\n"
} else {
""
};
Some(vec![TextEdit {
range: Range {
start: insert_pos,
end: insert_pos,
},
new_text: format!("{}use function {};\n", separator, fqn),
}])
}
#[cfg(test)]
#[path = "use_edit_tests.rs"]
mod tests;