aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api_light/src/typing.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide_api_light/src/typing.rs')
-rw-r--r--crates/ra_ide_api_light/src/typing.rs284
1 files changed, 141 insertions, 143 deletions
diff --git a/crates/ra_ide_api_light/src/typing.rs b/crates/ra_ide_api_light/src/typing.rs
index 5726209cc..c8f3dfe44 100644
--- a/crates/ra_ide_api_light/src/typing.rs
+++ b/crates/ra_ide_api_light/src/typing.rs
@@ -1,11 +1,11 @@
1use ra_syntax::{ 1use ra_syntax::{
2 AstNode, SourceFile, SyntaxKind::*,
3 SyntaxNode, TextUnit, TextRange,
2 algo::{find_node_at_offset, find_leaf_at_offset, LeafAtOffset}, 4 algo::{find_node_at_offset, find_leaf_at_offset, LeafAtOffset},
3 ast, 5 ast::{self, AstToken},
4 AstNode, Direction, SourceFile, SyntaxKind::*,
5 SyntaxNode, TextUnit,
6}; 6};
7 7
8use crate::{LocalEdit, TextEditBuilder}; 8use crate::{LocalEdit, TextEditBuilder, formatting::leading_indent};
9 9
10pub fn on_enter(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> { 10pub fn on_enter(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> {
11 let comment = find_leaf_at_offset(file.syntax(), offset) 11 let comment = find_leaf_at_offset(file.syntax(), offset)
@@ -53,20 +53,21 @@ fn node_indent<'a>(file: &'a SourceFile, node: &SyntaxNode) -> Option<&'a str> {
53 Some(&text[pos..]) 53 Some(&text[pos..])
54} 54}
55 55
56pub fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> { 56pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option<LocalEdit> {
57 let let_stmt: &ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; 57 assert_eq!(file.syntax().text().char_at(eq_offset), Some('='));
58 let let_stmt: &ast::LetStmt = find_node_at_offset(file.syntax(), eq_offset)?;
58 if let_stmt.has_semi() { 59 if let_stmt.has_semi() {
59 return None; 60 return None;
60 } 61 }
61 if let Some(expr) = let_stmt.initializer() { 62 if let Some(expr) = let_stmt.initializer() {
62 let expr_range = expr.syntax().range(); 63 let expr_range = expr.syntax().range();
63 if expr_range.contains(offset) && offset != expr_range.start() { 64 if expr_range.contains(eq_offset) && eq_offset != expr_range.start() {
64 return None; 65 return None;
65 } 66 }
66 if file 67 if file
67 .syntax() 68 .syntax()
68 .text() 69 .text()
69 .slice(offset..expr_range.start()) 70 .slice(eq_offset..expr_range.start())
70 .contains('\n') 71 .contains('\n')
71 { 72 {
72 return None; 73 return None;
@@ -84,54 +85,44 @@ pub fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> {
84 }) 85 })
85} 86}
86 87
87pub fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> { 88pub fn on_dot_typed(file: &SourceFile, dot_offset: TextUnit) -> Option<LocalEdit> {
88 let before_dot_offset = offset - TextUnit::of_char('.'); 89 assert_eq!(file.syntax().text().char_at(dot_offset), Some('.'));
89 90
90 let whitespace = find_leaf_at_offset(file.syntax(), before_dot_offset).left_biased()?; 91 let whitespace = find_leaf_at_offset(file.syntax(), dot_offset)
91 92 .left_biased()
92 // find whitespace just left of the dot 93 .and_then(ast::Whitespace::cast)?;
93 ast::Whitespace::cast(whitespace)?;
94
95 // make sure there is a method call
96 let method_call = whitespace
97 .siblings(Direction::Prev)
98 // first is whitespace
99 .skip(1)
100 .next()?;
101
102 ast::MethodCallExpr::cast(method_call)?;
103
104 // find how much the _method call is indented
105 let method_chain_indent = method_call
106 .parent()?
107 .siblings(Direction::Prev)
108 .skip(1)
109 .next()?
110 .leaf_text()
111 .map(|x| last_line_indent_in_whitespace(x))?;
112
113 let current_indent = TextUnit::of_str(last_line_indent_in_whitespace(whitespace.leaf_text()?));
114 // TODO: indent is always 4 spaces now. A better heuristic could look on the previous line(s)
115
116 let target_indent = TextUnit::of_str(method_chain_indent) + TextUnit::from_usize(4);
117
118 let diff = target_indent - current_indent;
119
120 let indent = "".repeat(diff.to_usize());
121 94
122 let cursor_position = offset + diff; 95 let current_indent = {
96 let text = whitespace.text();
97 let newline = text.rfind('\n')?;
98 &text[newline + 1..]
99 };
100 let current_indent_len = TextUnit::of_str(current_indent);
101
102 // Make sure dot is a part of call chain
103 let field_expr = whitespace
104 .syntax()
105 .parent()
106 .and_then(ast::FieldExpr::cast)?;
107 let prev_indent = leading_indent(field_expr.syntax())?;
108 let target_indent = format!(" {}", prev_indent);
109 let target_indent_len = TextUnit::of_str(&target_indent);
110 if current_indent_len == target_indent_len {
111 return None;
112 }
123 let mut edit = TextEditBuilder::default(); 113 let mut edit = TextEditBuilder::default();
124 edit.insert(before_dot_offset, indent); 114 edit.replace(
125 Some(LocalEdit { 115 TextRange::from_to(dot_offset - current_indent_len, dot_offset),
126 label: "indent dot".to_string(), 116 target_indent.into(),
117 );
118 let res = LocalEdit {
119 label: "reindent dot".to_string(),
127 edit: edit.finish(), 120 edit: edit.finish(),
128 cursor_position: Some(cursor_position), 121 cursor_position: Some(
129 }) 122 dot_offset + target_indent_len - current_indent_len + TextUnit::of_char('.'),
130} 123 ),
131 124 };
132/// Finds the last line in the whitespace 125 Some(res)
133fn last_line_indent_in_whitespace(ws: &str) -> &str {
134 ws.split('\n').last().unwrap_or("")
135} 126}
136 127
137#[cfg(test)] 128#[cfg(test)]
@@ -142,12 +133,18 @@ mod tests {
142 133
143 #[test] 134 #[test]
144 fn test_on_eq_typed() { 135 fn test_on_eq_typed() {
145 fn do_check(before: &str, after: &str) { 136 fn type_eq(before: &str, after: &str) {
146 let (offset, before) = extract_offset(before); 137 let (offset, before) = extract_offset(before);
138 let mut edit = TextEditBuilder::default();
139 edit.insert(offset, "=".to_string());
140 let before = edit.finish().apply(&before);
147 let file = SourceFile::parse(&before); 141 let file = SourceFile::parse(&before);
148 let result = on_eq_typed(&file, offset).unwrap(); 142 if let Some(result) = on_eq_typed(&file, offset) {
149 let actual = result.edit.apply(&before); 143 let actual = result.edit.apply(&before);
150 assert_eq_text!(after, &actual); 144 assert_eq_text!(after, &actual);
145 } else {
146 assert_eq_text!(&before, after)
147 };
151 } 148 }
152 149
153 // do_check(r" 150 // do_check(r"
@@ -159,10 +156,10 @@ mod tests {
159 // let foo =; 156 // let foo =;
160 // } 157 // }
161 // "); 158 // ");
162 do_check( 159 type_eq(
163 r" 160 r"
164fn foo() { 161fn foo() {
165 let foo =<|> 1 + 1 162 let foo <|> 1 + 1
166} 163}
167", 164",
168 r" 165 r"
@@ -184,112 +181,113 @@ fn foo() {
184 // "); 181 // ");
185 } 182 }
186 183
184 fn type_dot(before: &str, after: &str) {
185 let (offset, before) = extract_offset(before);
186 let mut edit = TextEditBuilder::default();
187 edit.insert(offset, ".".to_string());
188 let before = edit.finish().apply(&before);
189 let file = SourceFile::parse(&before);
190 if let Some(result) = on_dot_typed(&file, offset) {
191 let actual = result.edit.apply(&before);
192 assert_eq_text!(after, &actual);
193 } else {
194 assert_eq_text!(&before, after)
195 };
196 }
197
187 #[test] 198 #[test]
188 fn test_on_dot_typed() { 199 fn indents_new_chain_call() {
189 fn do_check(before: &str, after: &str) { 200 type_dot(
190 let (offset, before) = extract_offset(before);
191 let file = SourceFile::parse(&before);
192 if let Some(result) = on_eq_typed(&file, offset) {
193 let actual = result.edit.apply(&before);
194 assert_eq_text!(after, &actual);
195 };
196 }
197 // indent if continuing chain call
198 do_check(
199 r" 201 r"
200 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { 202 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
201 self.child_impl(db, name) 203 self.child_impl(db, name)
202 .<|> 204 <|>
203 } 205 }
204", 206 ",
205 r" 207 r"
206 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { 208 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
207 self.child_impl(db, name) 209 self.child_impl(db, name)
208 . 210 .
209 } 211 }
210", 212 ",
211 ); 213 );
212 214 type_dot(
213 // do not indent if already indented
214 do_check(
215 r" 215 r"
216 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { 216 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
217 self.child_impl(db, name) 217 self.child_impl(db, name)
218 .<|> 218 <|>
219 } 219 }
220", 220 ",
221 r" 221 r"
222 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { 222 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
223 self.child_impl(db, name) 223 self.child_impl(db, name)
224 . 224 .
225 }
226 ",
227 )
225 } 228 }
226",
227 );
228 229
229 // indent if the previous line is already indented 230 #[test]
230 do_check( 231 fn indents_continued_chain_call() {
232 type_dot(
231 r" 233 r"
232 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { 234 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
233 self.child_impl(db, name) 235 self.child_impl(db, name)
234 .first() 236 .first()
235 .<|> 237 <|>
236 } 238 }
237", 239 ",
238 r" 240 r"
239 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { 241 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
240 self.child_impl(db, name) 242 self.child_impl(db, name)
241 .first() 243 .first()
242 . 244 .
243 } 245 }
244", 246 ",
245 ); 247 );
246 248 type_dot(
247 // don't indent if indent matches previous line
248 do_check(
249 r" 249 r"
250 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { 250 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
251 self.child_impl(db, name) 251 self.child_impl(db, name)
252 .first() 252 .first()
253 .<|> 253 <|>
254 } 254 }
255", 255 ",
256 r" 256 r"
257 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { 257 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
258 self.child_impl(db, name) 258 self.child_impl(db, name)
259 .first() 259 .first()
260 . 260 .
261 } 261 }
262", 262 ",
263 ); 263 );
264 }
264 265
265 // don't indent if there is no method call on previous line 266 #[test]
266 do_check( 267 fn dont_indent_freestanding_dot() {
268 type_dot(
267 r" 269 r"
268 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { 270 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
269 .<|> 271 <|>
270 } 272 }
271", 273 ",
272 r" 274 r"
273 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { 275 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
274 . 276 .
275 } 277 }
276", 278 ",
277 ); 279 );
278 280 type_dot(
279 // indent to match previous expr
280 do_check(
281 r" 281 r"
282 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { 282 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
283 self.child_impl(db, name) 283 <|>
284.<|> 284 }
285 } 285 ",
286",
287 r" 286 r"
288 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { 287 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
289 self.child_impl(db, name)
290 . 288 .
291 } 289 }
292", 290 ",
293 ); 291 );
294 } 292 }
295 293