typst_analyzer/code_actions/
handle.rs

1use std::collections::HashMap;
2
3use regex::Regex;
4use tower_lsp::lsp_types::*;
5
6use crate::backend::Backend;
7
8pub trait TypstCodeActions {
9    fn get_table_parameters(&self) -> HashMap<String, String>;
10    fn parse_table_params(&self, content: &str) -> Vec<String>;
11    fn calculate_code_actions(
12        &self,
13        content: &str,
14        range: Range,
15        uri: Url,
16    ) -> Vec<CodeActionOrCommand>;
17}
18
19/*    // code action for each params for table function
20#table(
21    columns: auto,
22    rows: auto,
23    gutter: auto,
24    column-gutter: auto,
25    row-gutter: auto,
26    fill: none,
27    align: auto,
28    stroke: none,
29    inset: relative,
30)
31    */
32impl TypstCodeActions for Backend {
33    fn get_table_parameters(&self) -> HashMap<String, String> {
34        let mut params = HashMap::new();
35        params.insert("columns".to_owned(), "auto".to_owned());
36        params.insert("rows".to_owned(), "auto".to_owned());
37        params.insert("gutter".to_owned(), "auto".to_owned());
38        params.insert("column-gutter".to_owned(), "auto".to_owned());
39        params.insert("row-gutter".to_owned(), "auto".to_owned());
40        params.insert("fill".to_owned(), "none".to_owned());
41        params.insert("align".to_owned(), "auto".to_owned());
42        params.insert("stroke".to_owned(), "none".to_owned());
43        params.insert("inset".to_owned(), "relative".to_owned());
44        params
45    }
46
47    /// Parses the content inside `#table(...)` and extracts the parameters already defined.
48    fn parse_table_params(&self, content: &str) -> Vec<String> {
49        // Regular expression to find parameters (e.g., `param:`).
50        let re = Regex::new(r"(\w+(-\w+)?)\s*:").unwrap();
51        let mut existing_params = Vec::new();
52
53        for cap in re.captures_iter(content) {
54            if let Some(param) = cap.get(1) {
55                existing_params.push(param.as_str().to_owned());
56            }
57        }
58        existing_params
59    }
60
61    fn calculate_code_actions(
62        &self,
63        content: &str,
64        range: Range,
65        uri: Url,
66    ) -> Vec<CodeActionOrCommand> {
67        let mut actions = Vec::new();
68
69        // Check if the text "VS Code" is within the range
70        let vs_code_re = Regex::new(r"VS Code").unwrap();
71
72        for (line_idx, line) in content.lines().enumerate() {
73            if let Some(vs_code_match) = vs_code_re.find(line) {
74                let start = vs_code_match.start();
75                let end = vs_code_match.end();
76
77                // Ensure the match is within the specified range
78                if line_idx == range.start.line as usize && line_idx == range.end.line as usize {
79                    let edit = TextEdit {
80                        range: Range {
81                            start: Position {
82                                line: line_idx as u32,
83                                character: start as u32,
84                            },
85                            end: Position {
86                                line: line_idx as u32,
87                                character: end as u32,
88                            },
89                        },
90                        new_text: "Neovim".to_owned(),
91                    };
92
93                    let workspace_edit = WorkspaceEdit {
94                        changes: Some(HashMap::from([(uri.clone(), vec![edit])])),
95                        document_changes: None,
96                        change_annotations: None,
97                    };
98
99                    let code_action = CodeAction {
100                        title: "Replace 'VS Code' with 'Neovim'".to_owned(),
101                        kind: Some(CodeActionKind::QUICKFIX),
102                        diagnostics: None,
103                        edit: Some(workspace_edit),
104                        command: None,
105                        is_preferred: Some(true),
106                        disabled: None,
107                        data: None,
108                    };
109
110                    // Wrap CodeAction in CodeActionOrCommand
111                    actions.push(CodeActionOrCommand::CodeAction(code_action));
112                }
113            }
114        }
115
116        let mut multiline_table = String::new();
117        let mut in_table_block = false;
118        let mut table_start_line = 0;
119
120        for (line_idx, line) in content.lines().enumerate() {
121            // Handle multi-line `#table(...)` blocks.
122            if line.contains("#table(") {
123                in_table_block = true;
124                table_start_line = line_idx;
125            }
126
127            if in_table_block {
128                multiline_table.push_str(line);
129                multiline_table.push('\n');
130
131                if line.contains(")") {
132                    in_table_block = false;
133
134                    // Extract existing parameters inside `#table(...)`.
135                    let existing_params: Vec<String> = self.parse_table_params(&multiline_table);
136
137                    // Get all default parameters.
138                    let all_params: HashMap<String, String> = self.get_table_parameters();
139
140                    // Generate a separate code action for each missing parameter.
141                    for (param, default_value) in all_params {
142                        if !existing_params.contains(&param) {
143                            let title = format!("Add missing parameter: {}", param);
144
145                            // Create a new parameter string.
146                            let new_param = format!("{}: {},\n  ", param, default_value);
147                            // Prepare the text edit to add the missing parameter.
148                            let edit = TextEdit {
149                                range: Range {
150                                    start: Position {
151                                        line: table_start_line as u32 + 1,
152                                        character: 2,
153                                        // line: table_start_line as u32,
154                                        // character: line.find("#table(").unwrap_or(5) as u32 + 7, // Position after `#table(`.
155                                    },
156                                    end: Position {
157                                        line: table_start_line as u32 + 1,
158                                        character: 2,
159                                        // line: table_start_line as u32,
160                                        // character: line.find("#table(").unwrap_or(0) as u32 + 7,
161                                    },
162                                },
163                                new_text: new_param,
164                            };
165
166                            // Define the workspace edit for the code action.
167                            let workspace_edit = WorkspaceEdit {
168                                changes: Some(HashMap::from([(uri.clone(), vec![edit])])),
169                                document_changes: None,
170                                change_annotations: None,
171                            };
172
173                            // Create the code action for adding the missing parameter.
174                            let code_action = CodeAction {
175                                title,
176                                kind: Some(CodeActionKind::QUICKFIX),
177                                diagnostics: None,
178                                edit: Some(workspace_edit),
179                                command: None,
180                                is_preferred: Some(true),
181                                disabled: None,
182                                data: None,
183                            };
184
185                            // Add the code action to the list.
186                            actions.push(CodeActionOrCommand::CodeAction(code_action));
187                        }
188                    }
189
190                    // Reset the multiline table content for the next block.
191                    multiline_table.clear();
192                }
193            }
194        }
195
196        actions
197    }
198}