logicaffeine_lsp/
rename.rs1use lsp_types::{Position, Range, TextEdit, Url, WorkspaceEdit};
2use std::collections::HashMap;
3
4use crate::document::DocumentState;
5
6pub fn validate_new_name(new_name: &str) -> Option<String> {
9 if new_name.is_empty() {
10 return Some("Name cannot be empty".to_string());
11 }
12 if new_name.chars().any(|c| c.is_whitespace()) {
13 return Some("Name cannot contain whitespace".to_string());
14 }
15 let first = new_name.chars().next().unwrap();
16 if !first.is_alphabetic() && first != '_' {
17 return Some("Name must start with a letter or underscore".to_string());
18 }
19 let reserved = [
20 "Let",
21 "Set",
22 "If",
23 "While",
24 "Repeat",
25 "Return",
26 "Show",
27 "Give",
28 "Push",
29 "Pop",
30 "Call",
31 "Inspect",
32 "Check",
33 "Assert",
34 "Trust",
35 "Escape",
36 "Be",
37 "New",
38 "Otherwise",
39 "Else",
40 ];
41 if reserved.contains(&new_name) {
42 return Some(format!("'{}' is a reserved keyword", new_name));
43 }
44 None
45}
46
47pub fn rename(
52 doc: &DocumentState,
53 position: Position,
54 new_name: String,
55 uri: &Url,
56) -> Option<WorkspaceEdit> {
57 if validate_new_name(&new_name).is_some() {
58 return None;
59 }
60
61 let offset = doc.line_index.offset(position);
62
63 let (old_name, _) = crate::document::name_at(doc, offset)?;
66 let old_name = old_name.as_str();
67
68 let mut edits = Vec::new();
69
70 for def in doc.symbol_index.definitions_of(old_name) {
72 if def.span != logicaffeine_language::token::Span::default() {
73 edits.push(TextEdit {
74 range: Range {
75 start: doc.line_index.position(def.span.start),
76 end: doc.line_index.position(def.span.end),
77 },
78 new_text: new_name.clone(),
79 });
80 }
81 }
82
83 for reference in doc.symbol_index.references_to(old_name) {
85 edits.push(TextEdit {
86 range: Range {
87 start: doc.line_index.position(reference.span.start),
88 end: doc.line_index.position(reference.span.end),
89 },
90 new_text: new_name.clone(),
91 });
92 }
93
94 if edits.is_empty() {
95 return None;
96 }
97
98 let mut changes = HashMap::new();
99 changes.insert(uri.clone(), edits);
100
101 Some(WorkspaceEdit {
102 changes: Some(changes),
103 ..Default::default()
104 })
105}
106
107pub fn prepare_rename(doc: &DocumentState, position: Position) -> Option<(Range, String)> {
111 let offset = doc.line_index.offset(position);
112
113 if let Some(reference) = doc
118 .symbol_index
119 .references
120 .iter()
121 .find(|r| offset >= r.span.start && offset < r.span.end)
122 {
123 return Some((
124 Range {
125 start: doc.line_index.position(reference.span.start),
126 end: doc.line_index.position(reference.span.end),
127 },
128 reference.name.clone(),
129 ));
130 }
131
132 let token = doc
133 .tokens
134 .iter()
135 .find(|t| offset >= t.span.start && offset < t.span.end)?;
136
137 let text = doc.source.get(token.span.start..token.span.end)?;
138
139 match &token.kind {
141 logicaffeine_language::token::TokenType::Identifier
142 | logicaffeine_language::token::TokenType::ProperName(_)
143 | logicaffeine_language::token::TokenType::Noun(_)
144 | logicaffeine_language::token::TokenType::Adjective(_)
145 | logicaffeine_language::token::TokenType::Verb { .. } => {}
146 _ => return None,
147 }
148
149 Some((
150 Range {
151 start: doc.line_index.position(token.span.start),
152 end: doc.line_index.position(token.span.end),
153 },
154 text.to_string(),
155 ))
156}
157
158#[cfg(test)]
159#[path = "../../logicaffeine_tests/unit/logicaffeine_lsp/rename/tests.rs"]
160mod tests;