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