diff options
Diffstat (limited to 'crates/ra_ide_api/src/typing.rs')
-rw-r--r-- | crates/ra_ide_api/src/typing.rs | 352 |
1 files changed, 193 insertions, 159 deletions
diff --git a/crates/ra_ide_api/src/typing.rs b/crates/ra_ide_api/src/typing.rs index 2f5782012..26a3111fd 100644 --- a/crates/ra_ide_api/src/typing.rs +++ b/crates/ra_ide_api/src/typing.rs | |||
@@ -1,4 +1,17 @@ | |||
1 | //! FIXME: write short doc here | 1 | //! This module handles auto-magic editing actions applied together with users |
2 | //! edits. For example, if the user typed | ||
3 | //! | ||
4 | //! ```text | ||
5 | //! foo | ||
6 | //! .bar() | ||
7 | //! .baz() | ||
8 | //! | // <- cursor is here | ||
9 | //! ``` | ||
10 | //! | ||
11 | //! and types `.` next, we want to indent the dot. | ||
12 | //! | ||
13 | //! Language server executes such typing assists synchronously. That is, they | ||
14 | //! block user's typing and should be pretty fast for this reason! | ||
2 | 15 | ||
3 | use ra_db::{FilePosition, SourceDatabase}; | 16 | use ra_db::{FilePosition, SourceDatabase}; |
4 | use ra_fmt::leading_indent; | 17 | use ra_fmt::leading_indent; |
@@ -11,7 +24,7 @@ use ra_syntax::{ | |||
11 | }; | 24 | }; |
12 | use ra_text_edit::{TextEdit, TextEditBuilder}; | 25 | use ra_text_edit::{TextEdit, TextEditBuilder}; |
13 | 26 | ||
14 | use crate::{db::RootDatabase, SourceChange, SourceFileEdit}; | 27 | use crate::{db::RootDatabase, source_change::SingleFileChange, SourceChange, SourceFileEdit}; |
15 | 28 | ||
16 | pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> { | 29 | pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> { |
17 | let parse = db.parse(position.file_id); | 30 | let parse = db.parse(position.file_id); |
@@ -68,39 +81,67 @@ fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> { | |||
68 | Some(text[pos..].into()) | 81 | Some(text[pos..].into()) |
69 | } | 82 | } |
70 | 83 | ||
71 | pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option<TextEdit> { | 84 | pub(crate) const TRIGGER_CHARS: &str = ".=>"; |
72 | assert_eq!(file.syntax().text().char_at(eq_offset), Some('=')); | 85 | |
73 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), eq_offset)?; | 86 | pub(crate) fn on_char_typed( |
87 | db: &RootDatabase, | ||
88 | position: FilePosition, | ||
89 | char_typed: char, | ||
90 | ) -> Option<SourceChange> { | ||
91 | assert!(TRIGGER_CHARS.contains(char_typed)); | ||
92 | let file = &db.parse(position.file_id).tree(); | ||
93 | assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); | ||
94 | let single_file_change = on_char_typed_inner(file, position.offset, char_typed)?; | ||
95 | Some(single_file_change.into_source_change(position.file_id)) | ||
96 | } | ||
97 | |||
98 | fn on_char_typed_inner( | ||
99 | file: &SourceFile, | ||
100 | offset: TextUnit, | ||
101 | char_typed: char, | ||
102 | ) -> Option<SingleFileChange> { | ||
103 | assert!(TRIGGER_CHARS.contains(char_typed)); | ||
104 | match char_typed { | ||
105 | '.' => on_dot_typed(file, offset), | ||
106 | '=' => on_eq_typed(file, offset), | ||
107 | '>' => on_arrow_typed(file, offset), | ||
108 | _ => unreachable!(), | ||
109 | } | ||
110 | } | ||
111 | |||
112 | /// Returns an edit which should be applied after `=` was typed. Primarily, | ||
113 | /// this works when adding `let =`. | ||
114 | // FIXME: use a snippet completion instead of this hack here. | ||
115 | fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { | ||
116 | assert_eq!(file.syntax().text().char_at(offset), Some('=')); | ||
117 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; | ||
74 | if let_stmt.has_semi() { | 118 | if let_stmt.has_semi() { |
75 | return None; | 119 | return None; |
76 | } | 120 | } |
77 | if let Some(expr) = let_stmt.initializer() { | 121 | if let Some(expr) = let_stmt.initializer() { |
78 | let expr_range = expr.syntax().text_range(); | 122 | let expr_range = expr.syntax().text_range(); |
79 | if expr_range.contains(eq_offset) && eq_offset != expr_range.start() { | 123 | if expr_range.contains(offset) && offset != expr_range.start() { |
80 | return None; | 124 | return None; |
81 | } | 125 | } |
82 | if file.syntax().text().slice(eq_offset..expr_range.start()).contains_char('\n') { | 126 | if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') { |
83 | return None; | 127 | return None; |
84 | } | 128 | } |
85 | } else { | 129 | } else { |
86 | return None; | 130 | return None; |
87 | } | 131 | } |
88 | let offset = let_stmt.syntax().text_range().end(); | 132 | let offset = let_stmt.syntax().text_range().end(); |
89 | let mut edit = TextEditBuilder::default(); | 133 | Some(SingleFileChange { |
90 | edit.insert(offset, ";".to_string()); | 134 | label: "add semicolon".to_string(), |
91 | Some(edit.finish()) | 135 | edit: TextEdit::insert(offset, ";".to_string()), |
136 | cursor_position: None, | ||
137 | }) | ||
92 | } | 138 | } |
93 | 139 | ||
94 | pub(crate) fn on_dot_typed(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> { | 140 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. |
95 | let parse = db.parse(position.file_id); | 141 | fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { |
96 | assert_eq!(parse.tree().syntax().text().char_at(position.offset), Some('.')); | 142 | assert_eq!(file.syntax().text().char_at(offset), Some('.')); |
97 | 143 | let whitespace = | |
98 | let whitespace = parse | 144 | file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; |
99 | .tree() | ||
100 | .syntax() | ||
101 | .token_at_offset(position.offset) | ||
102 | .left_biased() | ||
103 | .and_then(ast::Whitespace::cast)?; | ||
104 | 145 | ||
105 | let current_indent = { | 146 | let current_indent = { |
106 | let text = whitespace.text(); | 147 | let text = whitespace.text(); |
@@ -117,20 +158,36 @@ pub(crate) fn on_dot_typed(db: &RootDatabase, position: FilePosition) -> Option< | |||
117 | if current_indent_len == target_indent_len { | 158 | if current_indent_len == target_indent_len { |
118 | return None; | 159 | return None; |
119 | } | 160 | } |
120 | let mut edit = TextEditBuilder::default(); | 161 | |
121 | edit.replace( | 162 | Some(SingleFileChange { |
122 | TextRange::from_to(position.offset - current_indent_len, position.offset), | 163 | label: "reindent dot".to_string(), |
123 | target_indent, | 164 | edit: TextEdit::replace( |
124 | ); | 165 | TextRange::from_to(offset - current_indent_len, offset), |
125 | 166 | target_indent, | |
126 | let res = SourceChange::source_file_edit_from("reindent dot", position.file_id, edit.finish()) | 167 | ), |
127 | .with_cursor(FilePosition { | 168 | cursor_position: Some( |
128 | offset: position.offset + target_indent_len - current_indent_len | 169 | offset + target_indent_len - current_indent_len + TextUnit::of_char('.'), |
129 | + TextUnit::of_char('.'), | 170 | ), |
130 | file_id: position.file_id, | 171 | }) |
131 | }); | 172 | } |
132 | 173 | ||
133 | Some(res) | 174 | /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` |
175 | fn on_arrow_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { | ||
176 | let file_text = file.syntax().text(); | ||
177 | assert_eq!(file_text.char_at(offset), Some('>')); | ||
178 | let after_arrow = offset + TextUnit::of_char('>'); | ||
179 | if file_text.char_at(after_arrow) != Some('{') { | ||
180 | return None; | ||
181 | } | ||
182 | if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() { | ||
183 | return None; | ||
184 | } | ||
185 | |||
186 | Some(SingleFileChange { | ||
187 | label: "add space after return type".to_string(), | ||
188 | edit: TextEdit::insert(after_arrow, " ".to_string()), | ||
189 | cursor_position: Some(after_arrow), | ||
190 | }) | ||
134 | } | 191 | } |
135 | 192 | ||
136 | #[cfg(test)] | 193 | #[cfg(test)] |
@@ -142,21 +199,87 @@ mod tests { | |||
142 | use super::*; | 199 | use super::*; |
143 | 200 | ||
144 | #[test] | 201 | #[test] |
145 | fn test_on_eq_typed() { | 202 | fn test_on_enter() { |
146 | fn type_eq(before: &str, after: &str) { | 203 | fn apply_on_enter(before: &str) -> Option<String> { |
147 | let (offset, before) = extract_offset(before); | 204 | let (offset, before) = extract_offset(before); |
148 | let mut edit = TextEditBuilder::default(); | 205 | let (analysis, file_id) = single_file(&before); |
149 | edit.insert(offset, "=".to_string()); | 206 | let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?; |
150 | let before = edit.finish().apply(&before); | 207 | |
151 | let parse = SourceFile::parse(&before); | 208 | assert_eq!(result.source_file_edits.len(), 1); |
152 | if let Some(result) = on_eq_typed(&parse.tree(), offset) { | 209 | let actual = result.source_file_edits[0].edit.apply(&before); |
153 | let actual = result.apply(&before); | 210 | let actual = add_cursor(&actual, result.cursor_position.unwrap().offset); |
154 | assert_eq_text!(after, &actual); | 211 | Some(actual) |
155 | } else { | 212 | } |
156 | assert_eq_text!(&before, after) | 213 | |
157 | }; | 214 | fn do_check(before: &str, after: &str) { |
215 | let actual = apply_on_enter(before).unwrap(); | ||
216 | assert_eq_text!(after, &actual); | ||
217 | } | ||
218 | |||
219 | fn do_check_noop(text: &str) { | ||
220 | assert!(apply_on_enter(text).is_none()) | ||
221 | } | ||
222 | |||
223 | do_check( | ||
224 | r" | ||
225 | /// Some docs<|> | ||
226 | fn foo() { | ||
227 | } | ||
228 | ", | ||
229 | r" | ||
230 | /// Some docs | ||
231 | /// <|> | ||
232 | fn foo() { | ||
233 | } | ||
234 | ", | ||
235 | ); | ||
236 | do_check( | ||
237 | r" | ||
238 | impl S { | ||
239 | /// Some<|> docs. | ||
240 | fn foo() {} | ||
241 | } | ||
242 | ", | ||
243 | r" | ||
244 | impl S { | ||
245 | /// Some | ||
246 | /// <|> docs. | ||
247 | fn foo() {} | ||
248 | } | ||
249 | ", | ||
250 | ); | ||
251 | do_check_noop(r"<|>//! docz"); | ||
252 | } | ||
253 | |||
254 | fn do_type_char(char_typed: char, before: &str) -> Option<(String, SingleFileChange)> { | ||
255 | let (offset, before) = extract_offset(before); | ||
256 | let edit = TextEdit::insert(offset, char_typed.to_string()); | ||
257 | let before = edit.apply(&before); | ||
258 | let parse = SourceFile::parse(&before); | ||
259 | on_char_typed_inner(&parse.tree(), offset, char_typed) | ||
260 | .map(|it| (it.edit.apply(&before), it)) | ||
261 | } | ||
262 | |||
263 | fn type_char(char_typed: char, before: &str, after: &str) { | ||
264 | let (actual, file_change) = do_type_char(char_typed, before) | ||
265 | .expect(&format!("typing `{}` did nothing", char_typed)); | ||
266 | |||
267 | if after.contains("<|>") { | ||
268 | let (offset, after) = extract_offset(after); | ||
269 | assert_eq_text!(&after, &actual); | ||
270 | assert_eq!(file_change.cursor_position, Some(offset)) | ||
271 | } else { | ||
272 | assert_eq_text!(after, &actual); | ||
158 | } | 273 | } |
274 | } | ||
159 | 275 | ||
276 | fn type_char_noop(char_typed: char, before: &str) { | ||
277 | let file_change = do_type_char(char_typed, before); | ||
278 | assert!(file_change.is_none()) | ||
279 | } | ||
280 | |||
281 | #[test] | ||
282 | fn test_on_eq_typed() { | ||
160 | // do_check(r" | 283 | // do_check(r" |
161 | // fn foo() { | 284 | // fn foo() { |
162 | // let foo =<|> | 285 | // let foo =<|> |
@@ -166,7 +289,8 @@ mod tests { | |||
166 | // let foo =; | 289 | // let foo =; |
167 | // } | 290 | // } |
168 | // "); | 291 | // "); |
169 | type_eq( | 292 | type_char( |
293 | '=', | ||
170 | r" | 294 | r" |
171 | fn foo() { | 295 | fn foo() { |
172 | let foo <|> 1 + 1 | 296 | let foo <|> 1 + 1 |
@@ -191,24 +315,10 @@ fn foo() { | |||
191 | // "); | 315 | // "); |
192 | } | 316 | } |
193 | 317 | ||
194 | fn type_dot(before: &str, after: &str) { | ||
195 | let (offset, before) = extract_offset(before); | ||
196 | let mut edit = TextEditBuilder::default(); | ||
197 | edit.insert(offset, ".".to_string()); | ||
198 | let before = edit.finish().apply(&before); | ||
199 | let (analysis, file_id) = single_file(&before); | ||
200 | if let Some(result) = analysis.on_dot_typed(FilePosition { offset, file_id }).unwrap() { | ||
201 | assert_eq!(result.source_file_edits.len(), 1); | ||
202 | let actual = result.source_file_edits[0].edit.apply(&before); | ||
203 | assert_eq_text!(after, &actual); | ||
204 | } else { | ||
205 | assert_eq_text!(&before, after) | ||
206 | }; | ||
207 | } | ||
208 | |||
209 | #[test] | 318 | #[test] |
210 | fn indents_new_chain_call() { | 319 | fn indents_new_chain_call() { |
211 | type_dot( | 320 | type_char( |
321 | '.', | ||
212 | r" | 322 | r" |
213 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 323 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
214 | self.child_impl(db, name) | 324 | self.child_impl(db, name) |
@@ -222,25 +332,21 @@ fn foo() { | |||
222 | } | 332 | } |
223 | ", | 333 | ", |
224 | ); | 334 | ); |
225 | type_dot( | 335 | type_char_noop( |
336 | '.', | ||
226 | r" | 337 | r" |
227 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 338 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
228 | self.child_impl(db, name) | 339 | self.child_impl(db, name) |
229 | <|> | 340 | <|> |
230 | } | 341 | } |
231 | ", | 342 | ", |
232 | r" | ||
233 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
234 | self.child_impl(db, name) | ||
235 | . | ||
236 | } | ||
237 | ", | ||
238 | ) | 343 | ) |
239 | } | 344 | } |
240 | 345 | ||
241 | #[test] | 346 | #[test] |
242 | fn indents_new_chain_call_with_semi() { | 347 | fn indents_new_chain_call_with_semi() { |
243 | type_dot( | 348 | type_char( |
349 | '.', | ||
244 | r" | 350 | r" |
245 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 351 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
246 | self.child_impl(db, name) | 352 | self.child_impl(db, name) |
@@ -254,25 +360,21 @@ fn foo() { | |||
254 | } | 360 | } |
255 | ", | 361 | ", |
256 | ); | 362 | ); |
257 | type_dot( | 363 | type_char_noop( |
364 | '.', | ||
258 | r" | 365 | r" |
259 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 366 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
260 | self.child_impl(db, name) | 367 | self.child_impl(db, name) |
261 | <|>; | 368 | <|>; |
262 | } | 369 | } |
263 | ", | 370 | ", |
264 | r" | ||
265 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
266 | self.child_impl(db, name) | ||
267 | .; | ||
268 | } | ||
269 | ", | ||
270 | ) | 371 | ) |
271 | } | 372 | } |
272 | 373 | ||
273 | #[test] | 374 | #[test] |
274 | fn indents_continued_chain_call() { | 375 | fn indents_continued_chain_call() { |
275 | type_dot( | 376 | type_char( |
377 | '.', | ||
276 | r" | 378 | r" |
277 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 379 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
278 | self.child_impl(db, name) | 380 | self.child_impl(db, name) |
@@ -288,7 +390,8 @@ fn foo() { | |||
288 | } | 390 | } |
289 | ", | 391 | ", |
290 | ); | 392 | ); |
291 | type_dot( | 393 | type_char_noop( |
394 | '.', | ||
292 | r" | 395 | r" |
293 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 396 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
294 | self.child_impl(db, name) | 397 | self.child_impl(db, name) |
@@ -296,19 +399,13 @@ fn foo() { | |||
296 | <|> | 399 | <|> |
297 | } | 400 | } |
298 | ", | 401 | ", |
299 | r" | ||
300 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
301 | self.child_impl(db, name) | ||
302 | .first() | ||
303 | . | ||
304 | } | ||
305 | ", | ||
306 | ); | 402 | ); |
307 | } | 403 | } |
308 | 404 | ||
309 | #[test] | 405 | #[test] |
310 | fn indents_middle_of_chain_call() { | 406 | fn indents_middle_of_chain_call() { |
311 | type_dot( | 407 | type_char( |
408 | '.', | ||
312 | r" | 409 | r" |
313 | fn source_impl() { | 410 | fn source_impl() { |
314 | let var = enum_defvariant_list().unwrap() | 411 | let var = enum_defvariant_list().unwrap() |
@@ -326,7 +423,8 @@ fn foo() { | |||
326 | } | 423 | } |
327 | ", | 424 | ", |
328 | ); | 425 | ); |
329 | type_dot( | 426 | type_char_noop( |
427 | '.', | ||
330 | r" | 428 | r" |
331 | fn source_impl() { | 429 | fn source_impl() { |
332 | let var = enum_defvariant_list().unwrap() | 430 | let var = enum_defvariant_list().unwrap() |
@@ -335,95 +433,31 @@ fn foo() { | |||
335 | .unwrap(); | 433 | .unwrap(); |
336 | } | 434 | } |
337 | ", | 435 | ", |
338 | r" | ||
339 | fn source_impl() { | ||
340 | let var = enum_defvariant_list().unwrap() | ||
341 | . | ||
342 | .nth(92) | ||
343 | .unwrap(); | ||
344 | } | ||
345 | ", | ||
346 | ); | 436 | ); |
347 | } | 437 | } |
348 | 438 | ||
349 | #[test] | 439 | #[test] |
350 | fn dont_indent_freestanding_dot() { | 440 | fn dont_indent_freestanding_dot() { |
351 | type_dot( | 441 | type_char_noop( |
442 | '.', | ||
352 | r" | 443 | r" |
353 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 444 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
354 | <|> | 445 | <|> |
355 | } | 446 | } |
356 | ", | 447 | ", |
357 | r" | ||
358 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
359 | . | ||
360 | } | ||
361 | ", | ||
362 | ); | 448 | ); |
363 | type_dot( | 449 | type_char_noop( |
450 | '.', | ||
364 | r" | 451 | r" |
365 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | 452 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { |
366 | <|> | 453 | <|> |
367 | } | 454 | } |
368 | ", | 455 | ", |
369 | r" | ||
370 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
371 | . | ||
372 | } | ||
373 | ", | ||
374 | ); | 456 | ); |
375 | } | 457 | } |
376 | 458 | ||
377 | #[test] | 459 | #[test] |
378 | fn test_on_enter() { | 460 | fn adds_space_after_return_type() { |
379 | fn apply_on_enter(before: &str) -> Option<String> { | 461 | type_char('>', "fn foo() -<|>{ 92 }", "fn foo() -><|> { 92 }") |
380 | let (offset, before) = extract_offset(before); | ||
381 | let (analysis, file_id) = single_file(&before); | ||
382 | let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?; | ||
383 | |||
384 | assert_eq!(result.source_file_edits.len(), 1); | ||
385 | let actual = result.source_file_edits[0].edit.apply(&before); | ||
386 | let actual = add_cursor(&actual, result.cursor_position.unwrap().offset); | ||
387 | Some(actual) | ||
388 | } | ||
389 | |||
390 | fn do_check(before: &str, after: &str) { | ||
391 | let actual = apply_on_enter(before).unwrap(); | ||
392 | assert_eq_text!(after, &actual); | ||
393 | } | ||
394 | |||
395 | fn do_check_noop(text: &str) { | ||
396 | assert!(apply_on_enter(text).is_none()) | ||
397 | } | ||
398 | |||
399 | do_check( | ||
400 | r" | ||
401 | /// Some docs<|> | ||
402 | fn foo() { | ||
403 | } | ||
404 | ", | ||
405 | r" | ||
406 | /// Some docs | ||
407 | /// <|> | ||
408 | fn foo() { | ||
409 | } | ||
410 | ", | ||
411 | ); | ||
412 | do_check( | ||
413 | r" | ||
414 | impl S { | ||
415 | /// Some<|> docs. | ||
416 | fn foo() {} | ||
417 | } | ||
418 | ", | ||
419 | r" | ||
420 | impl S { | ||
421 | /// Some | ||
422 | /// <|> docs. | ||
423 | fn foo() {} | ||
424 | } | ||
425 | ", | ||
426 | ); | ||
427 | do_check_noop(r"<|>//! docz"); | ||
428 | } | 462 | } |
429 | } | 463 | } |