tinymist_query/
prepare_rename.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
use tinymist_world::vfs::WorkspaceResolver;

use crate::{
    analysis::Definition,
    prelude::*,
    syntax::{Decl, SyntaxClass},
};

/// The [`textDocument/prepareRename`] request is sent from the client to the
/// server to setup and test the validity of a rename operation at a given
/// location.
///
/// [`textDocument/prepareRename`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareRename
///
/// # Compatibility
///
/// This request was introduced in specification version 3.12.0.
///
/// See <https://github.com/microsoft/vscode-go/issues/2714>.
/// The prepareRename feature is sent before a rename request. If the user
/// is trying to rename a symbol that should not be renamed (inside a
/// string or comment, on a builtin identifier, etc.), VSCode won't even
/// show the rename pop-up.
#[derive(Debug, Clone)]
pub struct PrepareRenameRequest {
    /// The path of the document to request for.
    pub path: PathBuf,
    /// The source code position to request for.
    pub position: LspPosition,
}

// todo: rename alias
// todo: rename import path?
impl StatefulRequest for PrepareRenameRequest {
    type Response = PrepareRenameResponse;

    fn request(self, ctx: &mut LocalContext, graph: LspComputeGraph) -> Option<Self::Response> {
        let doc = graph.snap.success_doc.as_ref();
        let source = ctx.source_by_path(&self.path).ok()?;
        let syntax = ctx.classify_for_decl(&source, self.position)?;
        if matches!(syntax.node().kind(), SyntaxKind::FieldAccess) {
            // todo: rename field access
            log::info!("prepare_rename: field access is not a definition site");
            return None;
        }

        let origin_selection_range = ctx.to_lsp_range(syntax.node().range(), &source);
        let def = ctx.def_of_syntax(&source, doc, syntax.clone())?;

        let (name, range) = prepare_renaming(&syntax, &def)?;

        Some(PrepareRenameResponse::RangeWithPlaceholder {
            range: range.unwrap_or(origin_selection_range),
            placeholder: name,
        })
    }
}

pub(crate) fn prepare_renaming(
    deref_target: &SyntaxClass,
    def: &Definition,
) -> Option<(String, Option<LspRange>)> {
    let name = def.name().clone();
    let def_fid = def.file_id()?;

    if WorkspaceResolver::is_package_file(def_fid) {
        crate::log_debug_ct!(
            "prepare_rename: {name} is in a package {pkg:?}",
            pkg = def_fid.package(),
        );
        return None;
    }

    let var_rename = || Some((name.to_string(), None));

    crate::log_debug_ct!("prepare_rename: {name}");
    use Decl::*;
    match def.decl.as_ref() {
        // Cannot rename headings or blocks
        // LexicalKind::Heading(_) | LexicalKind::Block => None,
        // Cannot rename module star
        // LexicalKind::Mod(Star) => None,
        // Cannot rename expression import
        // LexicalKind::Mod(Module(ModSrc::Expr(..))) => None,
        Var(..) => var_rename(),
        Func(..) | Closure(..) => validate_fn_renaming(def).map(|_| (name.to_string(), None)),
        Module(..) | ModuleAlias(..) | PathStem(..) | ImportPath(..) | IncludePath(..)
        | ModuleImport(..) => {
            let node = deref_target.node().get().clone();
            let path = node.cast::<ast::Str>()?;
            let name = path.get().to_string();
            Some((name, None))
        }
        // todo: label renaming, bibkey renaming
        BibEntry(..) | Label(..) | ContentRef(..) => None,
        ImportAlias(..) | Constant(..) | IdentRef(..) | Import(..) | StrName(..) | Spread(..) => {
            None
        }
        Pattern(..) | Content(..) | Generated(..) | Docs(..) => None,
    }
}

fn validate_fn_renaming(def: &Definition) -> Option<()> {
    use typst::foundations::func::Repr;
    let value = def.value();
    let mut func = match &value {
        None => return Some(()),
        Some(Value::Func(func)) => func,
        Some(..) => {
            log::info!(
                "prepare_rename: not a function on function definition site: {:?}",
                def.term
            );
            return None;
        }
    };
    loop {
        match func.inner() {
            // todo: rename with site
            Repr::With(w) => func = &w.0,
            Repr::Closure(..) | Repr::Plugin(..) => return Some(()),
            // native functions can't be renamed
            Repr::Native(..) | Repr::Element(..) => return None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tests::*;

    #[test]
    fn prepare() {
        snapshot_testing("rename", &|ctx, path| {
            let source = ctx.source_by_path(&path).unwrap();

            let request = PrepareRenameRequest {
                path: path.clone(),
                position: find_test_position(&source),
            };
            let snap = WorldComputeGraph::from_world(ctx.world.clone());

            let result = request.request(ctx, snap);
            assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
        });
    }
}