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 matches!(syntax.node().kind(), SyntaxKind::FieldAccess) {
41            // todo: rename field access
42            log::info!("prepare_rename: field access is not a definition site");
43            return None;
44        }
45
46        let origin_selection_range = ctx.to_lsp_range(syntax.node().range(), &source);
47        let def = ctx.def_of_syntax(&source, syntax.clone())?;
48
49        let (name, range) = prepare_renaming(&syntax, &def)?;
50
51        Some(PrepareRenameResponse::RangeWithPlaceholder {
52            range: range.unwrap_or(origin_selection_range),
53            placeholder: name,
54        })
55    }
56}
57
58pub(crate) fn prepare_renaming(
59    deref_target: &SyntaxClass,
60    def: &Definition,
61) -> Option<(String, Option<LspRange>)> {
62    let name = def.name().clone();
63    let def_fid = def.file_id()?;
64
65    if WorkspaceResolver::is_package_file(def_fid) {
66        crate::log_debug_ct!(
67            "prepare_rename: {name} is in a package {pkg:?}",
68            pkg = def_fid.package(),
69        );
70        return None;
71    }
72
73    let var_rename = || Some((name.to_string(), None));
74
75    crate::log_debug_ct!("prepare_rename: {name}");
76    use Decl::*;
77    match def.decl.as_ref() {
78        // Cannot rename headings or blocks
79        // LexicalKind::Heading(_) | LexicalKind::Block => None,
80        // Cannot rename module star
81        // LexicalKind::Mod(Star) => None,
82        // Cannot rename expression import
83        // LexicalKind::Mod(Module(ModSrc::Expr(..))) => None,
84        Var(..) => var_rename(),
85        Func(..) | Closure(..) => validate_fn_renaming(def).map(|_| (name.to_string(), None)),
86        Module(..) | ModuleAlias(..) | PathStem(..) | ImportPath(..) | IncludePath(..)
87        | ModuleImport(..) => {
88            let node = deref_target.node().get().clone();
89            let path = node.cast::<ast::Str>()?;
90            let name = path.get().to_string();
91            Some((name, None))
92        }
93        // todo: label renaming, bibkey renaming
94        BibEntry(..) | Label(..) | ContentRef(..) => None,
95        ImportAlias(..) | Constant(..) | IdentRef(..) | Import(..) | StrName(..) | Spread(..) => {
96            None
97        }
98        Pattern(..) | Content(..) | Generated(..) | Docs(..) => None,
99    }
100}
101
102fn validate_fn_renaming(def: &Definition) -> Option<()> {
103    use typst::foundations::func::Repr;
104    let value = def.value();
105    let mut func = match &value {
106        None => return Some(()),
107        Some(Value::Func(func)) => func,
108        Some(..) => {
109            log::info!(
110                "prepare_rename: not a function on function definition site: {:?}",
111                def.term
112            );
113            return None;
114        }
115    };
116    loop {
117        match func.inner() {
118            // todo: rename with site
119            Repr::With(w) => func = &w.0,
120            Repr::Closure(..) | Repr::Plugin(..) => return Some(()),
121            // native functions can't be renamed
122            Repr::Native(..) | Repr::Element(..) => return None,
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::tests::*;
131
132    #[test]
133    fn prepare() {
134        snapshot_testing("rename", &|ctx, path| {
135            let source = ctx.source_by_path(&path).unwrap();
136
137            let request = PrepareRenameRequest {
138                path: path.clone(),
139                position: find_test_position(&source),
140            };
141
142            let result = request.request(ctx);
143            assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
144        });
145    }
146}