aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2018-11-07 17:34:16 +0000
committerAleksey Kladov <[email protected]>2018-11-07 17:34:16 +0000
commit9b88ec488b3f83ab718c8cb4d7dff95aff0113ed (patch)
treefcdb2d0922b1492df59e779b5cbcb6086c19c402 /crates
parentaf17fc969742a36cee5199860789ed0b14123240 (diff)
split completion mod
Diffstat (limited to 'crates')
-rw-r--r--crates/ra_analysis/src/completion/mod.rs (renamed from crates/ra_analysis/src/completion.rs)280
-rw-r--r--crates/ra_analysis/src/completion/reference_completion.rs331
2 files changed, 339 insertions, 272 deletions
diff --git a/crates/ra_analysis/src/completion.rs b/crates/ra_analysis/src/completion/mod.rs
index 27566a8a1..763533012 100644
--- a/crates/ra_analysis/src/completion.rs
+++ b/crates/ra_analysis/src/completion/mod.rs
@@ -1,20 +1,17 @@
1mod reference_completion;
2
1use ra_editor::find_node_at_offset; 3use ra_editor::find_node_at_offset;
2use ra_syntax::{ 4use ra_syntax::{
3 algo::visit::{visitor, visitor_ctx, Visitor, VisitorCtx}, 5 algo::visit::{visitor_ctx, VisitorCtx},
4 ast::{self, AstChildren, LoopBodyOwner, ModuleItemOwner}, 6 ast,
5 AstNode, AtomEdit, SourceFileNode, 7 AstNode, AtomEdit,
6 SyntaxKind::*,
7 SyntaxNodeRef, 8 SyntaxNodeRef,
8}; 9};
9use rustc_hash::{FxHashMap, FxHashSet}; 10use rustc_hash::{FxHashMap};
10 11
11use crate::{ 12use crate::{
12 db::{self, SyntaxDatabase}, 13 db::{self, SyntaxDatabase},
13 descriptors::function::FnScopes, 14 Cancelable, FilePosition
14 descriptors::module::{ModuleId, ModuleScope, ModuleTree, ModuleSource},
15 descriptors::DescriptorDatabase,
16 input::FilesDatabase,
17 Cancelable, FilePosition, FileId,
18}; 15};
19 16
20#[derive(Debug)] 17#[derive(Debug)]
@@ -43,27 +40,11 @@ pub(crate) fn completions(
43 // First, let's try to complete a reference to some declaration. 40 // First, let's try to complete a reference to some declaration.
44 if let Some(name_ref) = find_node_at_offset::<ast::NameRef>(file.syntax(), position.offset) { 41 if let Some(name_ref) = find_node_at_offset::<ast::NameRef>(file.syntax(), position.offset) {
45 has_completions = true; 42 has_completions = true;
46 // completion from lexical scope 43 reference_completion::completions(&mut res, db, position.file_id, &file, name_ref)?;
47 complete_name_ref(&file, name_ref, &mut res);
48 // special case, `trait T { fn foo(i_am_a_name_ref) {} }` 44 // special case, `trait T { fn foo(i_am_a_name_ref) {} }`
49 if is_node::<ast::Param>(name_ref.syntax()) { 45 if is_node::<ast::Param>(name_ref.syntax()) {
50 param_completions(name_ref.syntax(), &mut res); 46 param_completions(name_ref.syntax(), &mut res);
51 } 47 }
52 // snippet completions
53 {
54 let name_range = name_ref.syntax().range();
55 let top_node = name_ref
56 .syntax()
57 .ancestors()
58 .take_while(|it| it.range() == name_range)
59 .last()
60 .unwrap();
61 match top_node.parent().map(|it| it.kind()) {
62 Some(SOURCE_FILE) | Some(ITEM_LIST) => complete_mod_item_snippets(&mut res),
63 _ => (),
64 }
65 }
66 complete_path(db, position.file_id, name_ref, &mut res)?;
67 } 48 }
68 49
69 // Otherwise, if this is a declaration, use heuristics to suggest a name. 50 // Otherwise, if this is a declaration, use heuristics to suggest a name.
@@ -77,117 +58,6 @@ pub(crate) fn completions(
77 Ok(res) 58 Ok(res)
78} 59}
79 60
80fn complete_path(
81 db: &db::RootDatabase,
82 file_id: FileId,
83 name_ref: ast::NameRef,
84 acc: &mut Vec<CompletionItem>,
85) -> Cancelable<()> {
86 let source_root_id = db.file_source_root(file_id);
87 let module_tree = db.module_tree(source_root_id)?;
88 let module_id = match module_tree.any_module_for_source(ModuleSource::SourceFile(file_id)) {
89 None => return Ok(()),
90 Some(it) => it,
91 };
92 let target_module_id = match find_target_module(&module_tree, module_id, name_ref) {
93 None => return Ok(()),
94 Some(it) => it,
95 };
96 let module_scope = db.module_scope(source_root_id, target_module_id)?;
97 let completions = module_scope.entries().iter().map(|entry| CompletionItem {
98 label: entry.name().to_string(),
99 lookup: None,
100 snippet: None,
101 });
102 acc.extend(completions);
103 Ok(())
104}
105
106fn find_target_module(
107 module_tree: &ModuleTree,
108 module_id: ModuleId,
109 name_ref: ast::NameRef,
110) -> Option<ModuleId> {
111 let mut crate_path = crate_path(name_ref)?;
112
113 crate_path.pop();
114 let mut target_module = module_id.root(&module_tree);
115 for name in crate_path {
116 target_module = target_module.child(module_tree, name.text().as_str())?;
117 }
118 Some(target_module)
119}
120
121fn crate_path(name_ref: ast::NameRef) -> Option<Vec<ast::NameRef>> {
122 let mut path = name_ref
123 .syntax()
124 .parent()
125 .and_then(ast::PathSegment::cast)?
126 .parent_path();
127 let mut res = Vec::new();
128 loop {
129 let segment = path.segment()?;
130 match segment.kind()? {
131 ast::PathSegmentKind::Name(name) => res.push(name),
132 ast::PathSegmentKind::CrateKw => break,
133 ast::PathSegmentKind::SelfKw | ast::PathSegmentKind::SuperKw => return None,
134 }
135 path = path.qualifier()?;
136 }
137 res.reverse();
138 Some(res)
139}
140
141fn complete_module_items(
142 file: &SourceFileNode,
143 items: AstChildren<ast::ModuleItem>,
144 this_item: Option<ast::NameRef>,
145 acc: &mut Vec<CompletionItem>,
146) {
147 let scope = ModuleScope::new(items); // FIXME
148 acc.extend(
149 scope
150 .entries()
151 .iter()
152 .filter(|entry| {
153 let syntax = entry.ptr().resolve(file);
154 Some(syntax.borrowed()) != this_item.map(|it| it.syntax())
155 })
156 .map(|entry| CompletionItem {
157 label: entry.name().to_string(),
158 lookup: None,
159 snippet: None,
160 }),
161 );
162}
163
164fn complete_name_ref(file: &SourceFileNode, name_ref: ast::NameRef, acc: &mut Vec<CompletionItem>) {
165 if !is_node::<ast::Path>(name_ref.syntax()) {
166 return;
167 }
168 let mut visited_fn = false;
169 for node in name_ref.syntax().ancestors() {
170 if let Some(items) = visitor()
171 .visit::<ast::SourceFile, _>(|it| Some(it.items()))
172 .visit::<ast::Module, _>(|it| Some(it.item_list()?.items()))
173 .accept(node)
174 {
175 if let Some(items) = items {
176 complete_module_items(file, items, Some(name_ref), acc);
177 }
178 break;
179 } else if !visited_fn {
180 if let Some(fn_def) = ast::FnDef::cast(node) {
181 visited_fn = true;
182 complete_expr_keywords(&file, fn_def, name_ref, acc);
183 complete_expr_snippets(acc);
184 let scopes = FnScopes::new(fn_def);
185 complete_fn(name_ref, &scopes, acc);
186 }
187 }
188 }
189}
190
191fn param_completions(ctx: SyntaxNodeRef, acc: &mut Vec<CompletionItem>) { 61fn param_completions(ctx: SyntaxNodeRef, acc: &mut Vec<CompletionItem>) {
192 let mut params = FxHashMap::default(); 62 let mut params = FxHashMap::default();
193 for node in ctx.ancestors() { 63 for node in ctx.ancestors() {
@@ -235,140 +105,6 @@ fn is_node<'a, N: AstNode<'a>>(node: SyntaxNodeRef<'a>) -> bool {
235 } 105 }
236} 106}
237 107
238fn complete_expr_keywords(
239 file: &SourceFileNode,
240 fn_def: ast::FnDef,
241 name_ref: ast::NameRef,
242 acc: &mut Vec<CompletionItem>,
243) {
244 acc.push(keyword("if", "if $0 {}"));
245 acc.push(keyword("match", "match $0 {}"));
246 acc.push(keyword("while", "while $0 {}"));
247 acc.push(keyword("loop", "loop {$0}"));
248
249 if let Some(off) = name_ref.syntax().range().start().checked_sub(2.into()) {
250 if let Some(if_expr) = find_node_at_offset::<ast::IfExpr>(file.syntax(), off) {
251 if if_expr.syntax().range().end() < name_ref.syntax().range().start() {
252 acc.push(keyword("else", "else {$0}"));
253 acc.push(keyword("else if", "else if $0 {}"));
254 }
255 }
256 }
257 if is_in_loop_body(name_ref) {
258 acc.push(keyword("continue", "continue"));
259 acc.push(keyword("break", "break"));
260 }
261 acc.extend(complete_return(fn_def, name_ref));
262}
263
264fn is_in_loop_body(name_ref: ast::NameRef) -> bool {
265 for node in name_ref.syntax().ancestors() {
266 if node.kind() == FN_DEF || node.kind() == LAMBDA_EXPR {
267 break;
268 }
269 let loop_body = visitor()
270 .visit::<ast::ForExpr, _>(LoopBodyOwner::loop_body)
271 .visit::<ast::WhileExpr, _>(LoopBodyOwner::loop_body)
272 .visit::<ast::LoopExpr, _>(LoopBodyOwner::loop_body)
273 .accept(node);
274 if let Some(Some(body)) = loop_body {
275 if name_ref
276 .syntax()
277 .range()
278 .is_subrange(&body.syntax().range())
279 {
280 return true;
281 }
282 }
283 }
284 false
285}
286
287fn complete_return(fn_def: ast::FnDef, name_ref: ast::NameRef) -> Option<CompletionItem> {
288 // let is_last_in_block = name_ref.syntax().ancestors().filter_map(ast::Expr::cast)
289 // .next()
290 // .and_then(|it| it.syntax().parent())
291 // .and_then(ast::Block::cast)
292 // .is_some();
293
294 // if is_last_in_block {
295 // return None;
296 // }
297
298 let is_stmt = match name_ref
299 .syntax()
300 .ancestors()
301 .filter_map(ast::ExprStmt::cast)
302 .next()
303 {
304 None => false,
305 Some(expr_stmt) => expr_stmt.syntax().range() == name_ref.syntax().range(),
306 };
307 let snip = match (is_stmt, fn_def.ret_type().is_some()) {
308 (true, true) => "return $0;",
309 (true, false) => "return;",
310 (false, true) => "return $0",
311 (false, false) => "return",
312 };
313 Some(keyword("return", snip))
314}
315
316fn keyword(kw: &str, snip: &str) -> CompletionItem {
317 CompletionItem {
318 label: kw.to_string(),
319 lookup: None,
320 snippet: Some(snip.to_string()),
321 }
322}
323
324fn complete_expr_snippets(acc: &mut Vec<CompletionItem>) {
325 acc.push(CompletionItem {
326 label: "pd".to_string(),
327 lookup: None,
328 snippet: Some("eprintln!(\"$0 = {:?}\", $0);".to_string()),
329 });
330 acc.push(CompletionItem {
331 label: "ppd".to_string(),
332 lookup: None,
333 snippet: Some("eprintln!(\"$0 = {:#?}\", $0);".to_string()),
334 });
335}
336
337fn complete_mod_item_snippets(acc: &mut Vec<CompletionItem>) {
338 acc.push(CompletionItem {
339 label: "tfn".to_string(),
340 lookup: None,
341 snippet: Some("#[test]\nfn $1() {\n $0\n}".to_string()),
342 });
343 acc.push(CompletionItem {
344 label: "pub(crate)".to_string(),
345 lookup: None,
346 snippet: Some("pub(crate) $0".to_string()),
347 })
348}
349
350fn complete_fn(name_ref: ast::NameRef, scopes: &FnScopes, acc: &mut Vec<CompletionItem>) {
351 let mut shadowed = FxHashSet::default();
352 acc.extend(
353 scopes
354 .scope_chain(name_ref.syntax())
355 .flat_map(|scope| scopes.entries(scope).iter())
356 .filter(|entry| shadowed.insert(entry.name()))
357 .map(|entry| CompletionItem {
358 label: entry.name().to_string(),
359 lookup: None,
360 snippet: None,
361 }),
362 );
363 if scopes.self_param.is_some() {
364 acc.push(CompletionItem {
365 label: "self".to_string(),
366 lookup: None,
367 snippet: None,
368 })
369 }
370}
371
372#[cfg(test)] 108#[cfg(test)]
373mod tests { 109mod tests {
374 use test_utils::assert_eq_dbg; 110 use test_utils::assert_eq_dbg;
diff --git a/crates/ra_analysis/src/completion/reference_completion.rs b/crates/ra_analysis/src/completion/reference_completion.rs
new file mode 100644
index 000000000..b08174968
--- /dev/null
+++ b/crates/ra_analysis/src/completion/reference_completion.rs
@@ -0,0 +1,331 @@
1use rustc_hash::{FxHashSet};
2use ra_editor::find_node_at_offset;
3use ra_syntax::{
4 algo::visit::{visitor, Visitor},
5 SourceFileNode, AstNode,
6 ast::{self, AstChildren, ModuleItemOwner, LoopBodyOwner},
7 SyntaxKind::*,
8};
9
10use crate::{
11 db::RootDatabase,
12 input::FilesDatabase,
13 completion::CompletionItem,
14 descriptors::module::{ModuleId, ModuleScope, ModuleTree, ModuleSource},
15 descriptors::function::FnScopes,
16 descriptors::DescriptorDatabase,
17 FileId, Cancelable
18};
19
20pub(super) fn completions(
21 acc: &mut Vec<CompletionItem>,
22 db: &RootDatabase,
23 file_id: FileId,
24 file: &SourceFileNode,
25 name_ref: ast::NameRef,
26) -> Cancelable<()> {
27 let kind = match classify_name_ref(name_ref) {
28 Some(it) => it,
29 None => return Ok(()),
30 };
31 match kind {
32 NameRefKind::LocalRef => {
33 if let Some(fn_def) = complete_local_name(acc, &file, name_ref) {
34 complete_expr_keywords(&file, fn_def, name_ref, acc);
35 complete_expr_snippets(acc);
36 }
37 }
38 NameRefKind::CratePath(path) => complete_path(acc, db, file_id, path)?,
39 NameRefKind::BareIdentInMod => {
40 let name_range = name_ref.syntax().range();
41 let top_node = name_ref
42 .syntax()
43 .ancestors()
44 .take_while(|it| it.range() == name_range)
45 .last()
46 .unwrap();
47 match top_node.parent().map(|it| it.kind()) {
48 Some(SOURCE_FILE) | Some(ITEM_LIST) => complete_mod_item_snippets(acc),
49 _ => (),
50 }
51 }
52 }
53 Ok(())
54}
55
56enum NameRefKind<'a> {
57 /// NameRef is a part of single-segment path, for example, a refernece to a
58 /// local variable.
59 LocalRef,
60 /// NameRef is the last segment in crate:: path
61 CratePath(Vec<ast::NameRef<'a>>),
62 /// NameRef is bare identifier at the module's root.
63 /// Used for keyword completion
64 BareIdentInMod,
65}
66
67fn classify_name_ref(name_ref: ast::NameRef) -> Option<NameRefKind> {
68 let name_range = name_ref.syntax().range();
69 let top_node = name_ref
70 .syntax()
71 .ancestors()
72 .take_while(|it| it.range() == name_range)
73 .last()
74 .unwrap();
75 match top_node.parent().map(|it| it.kind()) {
76 Some(SOURCE_FILE) | Some(ITEM_LIST) => return Some(NameRefKind::BareIdentInMod),
77 _ => (),
78 }
79
80 let parent = name_ref.syntax().parent()?;
81 if let Some(segment) = ast::PathSegment::cast(parent) {
82 let path = segment.parent_path();
83 if path.qualifier().is_none() {
84 return Some(NameRefKind::LocalRef);
85 }
86 if let Some(crate_path) = crate_path(path) {
87 return Some(NameRefKind::CratePath(crate_path));
88 }
89 }
90 None
91}
92
93fn crate_path(mut path: ast::Path) -> Option<Vec<ast::NameRef>> {
94 let mut res = Vec::new();
95 loop {
96 let segment = path.segment()?;
97 match segment.kind()? {
98 ast::PathSegmentKind::Name(name) => res.push(name),
99 ast::PathSegmentKind::CrateKw => break,
100 ast::PathSegmentKind::SelfKw | ast::PathSegmentKind::SuperKw => return None,
101 }
102 path = path.qualifier()?;
103 }
104 res.reverse();
105 Some(res)
106}
107
108fn complete_local_name<'a>(
109 acc: &mut Vec<CompletionItem>,
110 file: &SourceFileNode,
111 name_ref: ast::NameRef<'a>,
112) -> Option<ast::FnDef<'a>> {
113 let mut enclosing_fn = None;
114 for node in name_ref.syntax().ancestors() {
115 if let Some(items) = visitor()
116 .visit::<ast::SourceFile, _>(|it| Some(it.items()))
117 .visit::<ast::Module, _>(|it| Some(it.item_list()?.items()))
118 .accept(node)
119 {
120 if let Some(items) = items {
121 complete_module_items(file, items, Some(name_ref), acc);
122 }
123 break;
124 } else if enclosing_fn.is_none() {
125 if let Some(fn_def) = ast::FnDef::cast(node) {
126 enclosing_fn = Some(fn_def);
127 let scopes = FnScopes::new(fn_def);
128 complete_fn(name_ref, &scopes, acc);
129 }
130 }
131 }
132 enclosing_fn
133}
134
135fn complete_module_items(
136 file: &SourceFileNode,
137 items: AstChildren<ast::ModuleItem>,
138 this_item: Option<ast::NameRef>,
139 acc: &mut Vec<CompletionItem>,
140) {
141 let scope = ModuleScope::new(items); // FIXME
142 acc.extend(
143 scope
144 .entries()
145 .iter()
146 .filter(|entry| {
147 let syntax = entry.ptr().resolve(file);
148 Some(syntax.borrowed()) != this_item.map(|it| it.syntax())
149 })
150 .map(|entry| CompletionItem {
151 label: entry.name().to_string(),
152 lookup: None,
153 snippet: None,
154 }),
155 );
156}
157
158fn complete_fn(name_ref: ast::NameRef, scopes: &FnScopes, acc: &mut Vec<CompletionItem>) {
159 let mut shadowed = FxHashSet::default();
160 acc.extend(
161 scopes
162 .scope_chain(name_ref.syntax())
163 .flat_map(|scope| scopes.entries(scope).iter())
164 .filter(|entry| shadowed.insert(entry.name()))
165 .map(|entry| CompletionItem {
166 label: entry.name().to_string(),
167 lookup: None,
168 snippet: None,
169 }),
170 );
171 if scopes.self_param.is_some() {
172 acc.push(CompletionItem {
173 label: "self".to_string(),
174 lookup: None,
175 snippet: None,
176 })
177 }
178}
179
180fn complete_path(
181 acc: &mut Vec<CompletionItem>,
182 db: &RootDatabase,
183 file_id: FileId,
184 crate_path: Vec<ast::NameRef>,
185) -> Cancelable<()> {
186 let source_root_id = db.file_source_root(file_id);
187 let module_tree = db.module_tree(source_root_id)?;
188 let module_id = match module_tree.any_module_for_source(ModuleSource::SourceFile(file_id)) {
189 None => return Ok(()),
190 Some(it) => it,
191 };
192 let target_module_id = match find_target_module(&module_tree, module_id, crate_path) {
193 None => return Ok(()),
194 Some(it) => it,
195 };
196 let module_scope = db.module_scope(source_root_id, target_module_id)?;
197 let completions = module_scope.entries().iter().map(|entry| CompletionItem {
198 label: entry.name().to_string(),
199 lookup: None,
200 snippet: None,
201 });
202 acc.extend(completions);
203 Ok(())
204}
205
206fn find_target_module(
207 module_tree: &ModuleTree,
208 module_id: ModuleId,
209 mut crate_path: Vec<ast::NameRef>,
210) -> Option<ModuleId> {
211 crate_path.pop();
212 let mut target_module = module_id.root(&module_tree);
213 for name in crate_path {
214 target_module = target_module.child(module_tree, name.text().as_str())?;
215 }
216 Some(target_module)
217}
218
219fn complete_mod_item_snippets(acc: &mut Vec<CompletionItem>) {
220 acc.push(CompletionItem {
221 label: "tfn".to_string(),
222 lookup: None,
223 snippet: Some("#[test]\nfn $1() {\n $0\n}".to_string()),
224 });
225 acc.push(CompletionItem {
226 label: "pub(crate)".to_string(),
227 lookup: None,
228 snippet: Some("pub(crate) $0".to_string()),
229 })
230}
231
232fn complete_expr_keywords(
233 file: &SourceFileNode,
234 fn_def: ast::FnDef,
235 name_ref: ast::NameRef,
236 acc: &mut Vec<CompletionItem>,
237) {
238 acc.push(keyword("if", "if $0 {}"));
239 acc.push(keyword("match", "match $0 {}"));
240 acc.push(keyword("while", "while $0 {}"));
241 acc.push(keyword("loop", "loop {$0}"));
242
243 if let Some(off) = name_ref.syntax().range().start().checked_sub(2.into()) {
244 if let Some(if_expr) = find_node_at_offset::<ast::IfExpr>(file.syntax(), off) {
245 if if_expr.syntax().range().end() < name_ref.syntax().range().start() {
246 acc.push(keyword("else", "else {$0}"));
247 acc.push(keyword("else if", "else if $0 {}"));
248 }
249 }
250 }
251 if is_in_loop_body(name_ref) {
252 acc.push(keyword("continue", "continue"));
253 acc.push(keyword("break", "break"));
254 }
255 acc.extend(complete_return(fn_def, name_ref));
256}
257
258fn is_in_loop_body(name_ref: ast::NameRef) -> bool {
259 for node in name_ref.syntax().ancestors() {
260 if node.kind() == FN_DEF || node.kind() == LAMBDA_EXPR {
261 break;
262 }
263 let loop_body = visitor()
264 .visit::<ast::ForExpr, _>(LoopBodyOwner::loop_body)
265 .visit::<ast::WhileExpr, _>(LoopBodyOwner::loop_body)
266 .visit::<ast::LoopExpr, _>(LoopBodyOwner::loop_body)
267 .accept(node);
268 if let Some(Some(body)) = loop_body {
269 if name_ref
270 .syntax()
271 .range()
272 .is_subrange(&body.syntax().range())
273 {
274 return true;
275 }
276 }
277 }
278 false
279}
280
281fn complete_return(fn_def: ast::FnDef, name_ref: ast::NameRef) -> Option<CompletionItem> {
282 // let is_last_in_block = name_ref.syntax().ancestors().filter_map(ast::Expr::cast)
283 // .next()
284 // .and_then(|it| it.syntax().parent())
285 // .and_then(ast::Block::cast)
286 // .is_some();
287
288 // if is_last_in_block {
289 // return None;
290 // }
291
292 let is_stmt = match name_ref
293 .syntax()
294 .ancestors()
295 .filter_map(ast::ExprStmt::cast)
296 .next()
297 {
298 None => false,
299 Some(expr_stmt) => expr_stmt.syntax().range() == name_ref.syntax().range(),
300 };
301 let snip = match (is_stmt, fn_def.ret_type().is_some()) {
302 (true, true) => "return $0;",
303 (true, false) => "return;",
304 (false, true) => "return $0",
305 (false, false) => "return",
306 };
307 Some(keyword("return", snip))
308}
309
310fn keyword(kw: &str, snip: &str) -> CompletionItem {
311 CompletionItem {
312 label: kw.to_string(),
313 lookup: None,
314 snippet: Some(snip.to_string()),
315 }
316}
317
318fn complete_expr_snippets(acc: &mut Vec<CompletionItem>) {
319 acc.push(CompletionItem {
320 label: "pd".to_string(),
321 lookup: None,
322 snippet: Some("eprintln!(\"$0 = {:?}\", $0);".to_string()),
323 });
324 acc.push(CompletionItem {
325 label: "ppd".to_string(),
326 lookup: None,
327 snippet: Some("eprintln!(\"$0 = {:#?}\", $0);".to_string()),
328 });
329}
330
331