tinymist_query/
prepare_rename.rs

1use tinymist_world::vfs::WorkspaceResolver;
2
3use crate::{
4    analysis::Definition,
5    prelude::*,
6    syntax::{Decl, SyntaxClass},
7};
8
9/// The [`textDocument/prepareRename`] request is sent from the client to the
10/// server to setup and test the validity of a rename operation at a given
11/// location.
12///
13/// [`textDocument/prepareRename`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareRename
14///
15/// # Compatibility
16///
17/// This request was introduced in specification version 3.12.0.
18///
19/// See <https://github.com/microsoft/vscode-go/issues/2714>.
20/// The prepareRename feature is sent before a rename request. If the user
21/// is trying to rename a symbol that should not be renamed (inside a
22/// string or comment, on a builtin identifier, etc.), VSCode won't even
23/// show the rename pop-up.
24#[derive(Debug, Clone)]
25pub struct PrepareRenameRequest {
26    /// The path of the document to request for.
27    pub path: PathBuf,
28    /// The source code position to request for.
29    pub position: LspPosition,
30}
31
32// todo: rename alias
33// todo: rename import path?
34impl SemanticRequest for PrepareRenameRequest {
35    type Response = PrepareRenameResponse;
36
37    fn request(self, ctx: &mut LocalContext) -> Option<Self::Response> {
38        let source = ctx.source_by_path(&self.path).ok()?;
39        let syntax = ctx.classify_for_decl(&source, self.position)?;
40        if bad_syntax(&syntax) {
41            return None;
42        }
43
44        //  todo: process RefMarker consistently?
45        let mut node = syntax.node().clone();
46        if matches!(node.kind(), SyntaxKind::Ref) {
47            let marker = node
48                .children()
49                .find(|child| child.kind() == SyntaxKind::RefMarker)?;
50            node = marker;
51        }
52        let mut range = node.range();
53        if matches!(node.kind(), SyntaxKind::RefMarker) {
54            range.start += 1;
55        }
56
57        let origin_selection_range = ctx.to_lsp_range(range, &source);
58        let def = ctx.def_of_syntax(&source, syntax.clone())?;
59
60        let name = prepare_renaming(&syntax, &def)?;
61
62        Some(PrepareRenameResponse::RangeWithPlaceholder {
63            range: origin_selection_range,
64            placeholder: name,
65        })
66    }
67}
68
69fn bad_syntax(syntax: &SyntaxClass) -> bool {
70    if matches!(syntax.node().kind(), SyntaxKind::FieldAccess) {
71        // todo: rename field access
72        log::info!("prepare_rename: field access is not a definition site");
73        return true;
74    }
75
76    if syntax.erroneous() {
77        return true;
78    }
79
80    false
81}
82
83pub(crate) fn prepare_renaming(syntax: &SyntaxClass, def: &Definition) -> Option<String> {
84    if bad_syntax(syntax) {
85        return None;
86    }
87
88    let def_fid = def.file_id()?;
89
90    if WorkspaceResolver::is_package_file(def_fid) {
91        crate::log_debug_ct!(
92            "prepare_rename: is in a package {pkg:?}, def: {def:?}",
93            pkg = def_fid.package(),
94        );
95        return None;
96    }
97
98    let decl_name = || def.name().clone().to_string();
99
100    use Decl::*;
101    match def.decl.as_ref() {
102        // Cannot rename headings or blocks
103        // LexicalKind::Heading(_) | LexicalKind::Block => None,
104        // Cannot rename module star
105        // LexicalKind::Mod(Star) => None,
106        // Cannot rename expression import
107        // LexicalKind::Mod(Module(ModSrc::Expr(..))) => None,
108        Var(..) | Label(..) | ContentRef(..) => Some(decl_name()),
109        Func(..) | Closure(..) => validate_fn_renaming(def).map(|_| decl_name()),
110        Module(..) | ModuleAlias(..) | PathStem(..) | ImportPath(..) | IncludePath(..)
111        | ModuleImport(..) => {
112            let node = syntax.node().get().clone();
113            let path = node.cast::<ast::Str>()?;
114            let name = path.get().to_string();
115            Some(name)
116        }
117        // todo: bibkey renaming
118        BibEntry(..) => None,
119        ImportAlias(..) | Constant(..) | IdentRef(..) | Import(..) | StrName(..) | Spread(..) => {
120            None
121        }
122        Pattern(..) | Content(..) | Generated(..) | Docs(..) => None,
123    }
124}
125
126fn validate_fn_renaming(def: &Definition) -> Option<()> {
127    use typst::foundations::func::Repr;
128    let value = def.value();
129    let mut func = match &value {
130        None => return Some(()),
131        Some(Value::Func(func)) => func,
132        Some(..) => {
133            log::info!(
134                "prepare_rename: not a function on function definition site: {:?}",
135                def.term
136            );
137            return None;
138        }
139    };
140    loop {
141        match func.inner() {
142            // todo: rename with site
143            Repr::With(w) => func = &w.0,
144            Repr::Closure(..) | Repr::Plugin(..) => return Some(()),
145            // native functions can't be renamed
146            Repr::Native(..) | Repr::Element(..) => return None,
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::tests::*;
155
156    #[test]
157    fn prepare() {
158        snapshot_testing("rename", &|ctx, path| {
159            let source = ctx.source_by_path(&path).unwrap();
160
161            let request = PrepareRenameRequest {
162                path: path.clone(),
163                position: find_test_position(&source),
164            };
165
166            let result = request.request(ctx);
167            assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
168        });
169    }
170}