Skip to main content

php_lsp/editing/
use_import.rs

1use std::collections::HashMap;
2
3use tower_lsp_server::ls_types::{Position, Range, TextEdit, Uri, WorkspaceEdit};
4
5use crate::index::file_index::FileIndex;
6
7/// Find a class FQN matching `name` in the workspace indexes. Unlike
8/// `find_fqn_for_function`, a match is returned even when the class lives in
9/// the global namespace (`fqn` has no `\`): referencing a global-namespace
10/// class from inside a namespaced file still requires an explicit `use`,
11/// since (unlike functions/constants) unqualified class names never fall
12/// back to the global namespace.
13pub(crate) fn find_fqn_for_class(
14    name: &str,
15    indexes: &[(Uri, std::sync::Arc<FileIndex>)],
16) -> Option<String> {
17    for (_uri, idx) in indexes {
18        for class in &idx.classes {
19            if class.name.as_ref() == name {
20                return Some(class.fqn.to_string());
21            }
22        }
23    }
24    None
25}
26
27/// Build a `WorkspaceEdit` that inserts `use FQN;` near the top of the file.
28pub(crate) fn build_use_import_edit(source: &str, uri: &Uri, fqn: &str) -> WorkspaceEdit {
29    // Insert after the `<?php` line and any existing `use` / `namespace` lines
30    let insert_line = find_use_insert_line(source);
31    let insert_text = format!("use {fqn};\n");
32    let pos = Position {
33        line: insert_line,
34        character: 0,
35    };
36    let edit = TextEdit {
37        range: Range {
38            start: pos,
39            end: pos,
40        },
41        new_text: insert_text,
42    };
43    let mut changes = HashMap::new();
44    changes.insert(uri.clone(), vec![edit]);
45    WorkspaceEdit {
46        changes: Some(changes),
47        ..Default::default()
48    }
49}
50
51/// Find a namespaced function FQN matching `name` in the workspace indexes.
52/// Returns `Some(fqn)` only when the FQN is namespaced (contains `\`).
53pub(crate) fn find_fqn_for_function(
54    name: &str,
55    indexes: &[(Uri, std::sync::Arc<FileIndex>)],
56) -> Option<String> {
57    for (_uri, idx) in indexes {
58        for func in &idx.functions {
59            if func.name.as_ref() == name && func.fqn.contains('\\') {
60                return Some(func.fqn.trim_start_matches('\\').to_string());
61            }
62        }
63    }
64    None
65}
66
67/// Build a `WorkspaceEdit` that inserts `use function FQN;` near the top of the file.
68pub(crate) fn build_use_function_import_edit(source: &str, uri: &Uri, fqn: &str) -> WorkspaceEdit {
69    let insert_line = find_use_insert_line(source);
70    let insert_text = format!("use function {fqn};\n");
71    let pos = Position {
72        line: insert_line,
73        character: 0,
74    };
75    let edit = TextEdit {
76        range: Range {
77            start: pos,
78            end: pos,
79        },
80        new_text: insert_text,
81    };
82    let mut changes = HashMap::new();
83    changes.insert(uri.clone(), vec![edit]);
84    WorkspaceEdit {
85        changes: Some(changes),
86        ..Default::default()
87    }
88}
89
90pub(crate) fn find_use_insert_line(source: &str) -> u32 {
91    let mut last_use_or_ns: u32 = 0;
92    for (i, line) in source.lines().enumerate() {
93        let trimmed = line.trim();
94        if trimmed.starts_with("<?php")
95            || trimmed.starts_with("namespace ")
96            || trimmed.starts_with("use ")
97        {
98            last_use_or_ns = i as u32 + 1;
99        }
100    }
101    last_use_or_ns
102}