diff options
Diffstat (limited to 'crates/ra_analysis/src')
7 files changed, 181 insertions, 167 deletions
diff --git a/crates/ra_analysis/src/completion.rs b/crates/ra_analysis/src/completion.rs index 93edcc4c2..2d61a3aef 100644 --- a/crates/ra_analysis/src/completion.rs +++ b/crates/ra_analysis/src/completion.rs | |||
@@ -1,4 +1,5 @@ | |||
1 | mod completion_item; | 1 | mod completion_item; |
2 | mod completion_context; | ||
2 | 3 | ||
3 | mod complete_fn_param; | 4 | mod complete_fn_param; |
4 | mod complete_keyword; | 5 | mod complete_keyword; |
@@ -6,34 +7,33 @@ mod complete_snippet; | |||
6 | mod complete_path; | 7 | mod complete_path; |
7 | mod complete_scope; | 8 | mod complete_scope; |
8 | 9 | ||
9 | use ra_editor::find_node_at_offset; | ||
10 | use ra_text_edit::AtomTextEdit; | ||
11 | use ra_syntax::{ | ||
12 | algo::find_leaf_at_offset, | ||
13 | ast, | ||
14 | AstNode, | ||
15 | SyntaxNodeRef, | ||
16 | SourceFileNode, | ||
17 | TextUnit, | ||
18 | SyntaxKind::*, | ||
19 | }; | ||
20 | use ra_db::SyntaxDatabase; | 10 | use ra_db::SyntaxDatabase; |
21 | use hir::source_binder; | ||
22 | 11 | ||
23 | use crate::{ | 12 | use crate::{ |
24 | db, | 13 | db, |
25 | Cancelable, FilePosition, | 14 | Cancelable, FilePosition, |
26 | completion::completion_item::{Completions, CompletionKind}, | 15 | completion::{ |
16 | completion_item::{Completions, CompletionKind}, | ||
17 | completion_context::CompletionContext, | ||
18 | }, | ||
27 | }; | 19 | }; |
28 | 20 | ||
29 | pub use crate::completion::completion_item::{CompletionItem, InsertText}; | 21 | pub use crate::completion::completion_item::{CompletionItem, InsertText}; |
30 | 22 | ||
23 | /// Main entry point for copmletion. We run comletion as a two-phase process. | ||
24 | /// | ||
25 | /// First, we look at the position and collect a so-called `CompletionContext. | ||
26 | /// This is a somewhat messy process, because, during completion, syntax tree is | ||
27 | /// incomplete and can look readlly weired. | ||
28 | /// | ||
29 | /// Once the context is collected, we run a series of completion routines whihc | ||
30 | /// look at the context and produce completion items. | ||
31 | pub(crate) fn completions( | 31 | pub(crate) fn completions( |
32 | db: &db::RootDatabase, | 32 | db: &db::RootDatabase, |
33 | position: FilePosition, | 33 | position: FilePosition, |
34 | ) -> Cancelable<Option<Completions>> { | 34 | ) -> Cancelable<Option<Completions>> { |
35 | let original_file = db.source_file(position.file_id); | 35 | let original_file = db.source_file(position.file_id); |
36 | let ctx = ctry!(SyntaxContext::new(db, &original_file, position)?); | 36 | let ctx = ctry!(CompletionContext::new(db, &original_file, position)?); |
37 | 37 | ||
38 | let mut acc = Completions::default(); | 38 | let mut acc = Completions::default(); |
39 | 39 | ||
@@ -47,148 +47,6 @@ pub(crate) fn completions( | |||
47 | Ok(Some(acc)) | 47 | Ok(Some(acc)) |
48 | } | 48 | } |
49 | 49 | ||
50 | /// `SyntaxContext` is created early during completion to figure out, where | ||
51 | /// exactly is the cursor, syntax-wise. | ||
52 | #[derive(Debug)] | ||
53 | pub(super) struct SyntaxContext<'a> { | ||
54 | db: &'a db::RootDatabase, | ||
55 | offset: TextUnit, | ||
56 | leaf: SyntaxNodeRef<'a>, | ||
57 | module: Option<hir::Module>, | ||
58 | enclosing_fn: Option<ast::FnDef<'a>>, | ||
59 | is_param: bool, | ||
60 | /// A single-indent path, like `foo`. | ||
61 | is_trivial_path: bool, | ||
62 | /// If not a trivial, path, the prefix (qualifier). | ||
63 | path_prefix: Option<hir::Path>, | ||
64 | after_if: bool, | ||
65 | is_stmt: bool, | ||
66 | /// Something is typed at the "top" level, in module or impl/trait. | ||
67 | is_new_item: bool, | ||
68 | } | ||
69 | |||
70 | impl<'a> SyntaxContext<'a> { | ||
71 | pub(super) fn new( | ||
72 | db: &'a db::RootDatabase, | ||
73 | original_file: &'a SourceFileNode, | ||
74 | position: FilePosition, | ||
75 | ) -> Cancelable<Option<SyntaxContext<'a>>> { | ||
76 | let module = source_binder::module_from_position(db, position)?; | ||
77 | let leaf = | ||
78 | ctry!(find_leaf_at_offset(original_file.syntax(), position.offset).left_biased()); | ||
79 | let mut ctx = SyntaxContext { | ||
80 | db, | ||
81 | leaf, | ||
82 | offset: position.offset, | ||
83 | module, | ||
84 | enclosing_fn: None, | ||
85 | is_param: false, | ||
86 | is_trivial_path: false, | ||
87 | path_prefix: None, | ||
88 | after_if: false, | ||
89 | is_stmt: false, | ||
90 | is_new_item: false, | ||
91 | }; | ||
92 | ctx.fill(original_file, position.offset); | ||
93 | Ok(Some(ctx)) | ||
94 | } | ||
95 | |||
96 | fn fill(&mut self, original_file: &SourceFileNode, offset: TextUnit) { | ||
97 | // Insert a fake ident to get a valid parse tree. We will use this file | ||
98 | // to determine context, though the original_file will be used for | ||
99 | // actual completion. | ||
100 | let file = { | ||
101 | let edit = AtomTextEdit::insert(offset, "intellijRulezz".to_string()); | ||
102 | original_file.reparse(&edit) | ||
103 | }; | ||
104 | |||
105 | // First, let's try to complete a reference to some declaration. | ||
106 | if let Some(name_ref) = find_node_at_offset::<ast::NameRef>(file.syntax(), offset) { | ||
107 | // Special case, `trait T { fn foo(i_am_a_name_ref) {} }`. | ||
108 | // See RFC#1685. | ||
109 | if is_node::<ast::Param>(name_ref.syntax()) { | ||
110 | self.is_param = true; | ||
111 | return; | ||
112 | } | ||
113 | self.classify_name_ref(&file, name_ref); | ||
114 | } | ||
115 | |||
116 | // Otherwise, see if this is a declaration. We can use heuristics to | ||
117 | // suggest declaration names, see `CompletionKind::Magic`. | ||
118 | if let Some(name) = find_node_at_offset::<ast::Name>(file.syntax(), offset) { | ||
119 | if is_node::<ast::Param>(name.syntax()) { | ||
120 | self.is_param = true; | ||
121 | return; | ||
122 | } | ||
123 | } | ||
124 | } | ||
125 | fn classify_name_ref(&mut self, file: &SourceFileNode, name_ref: ast::NameRef) { | ||
126 | let name_range = name_ref.syntax().range(); | ||
127 | let top_node = name_ref | ||
128 | .syntax() | ||
129 | .ancestors() | ||
130 | .take_while(|it| it.range() == name_range) | ||
131 | .last() | ||
132 | .unwrap(); | ||
133 | |||
134 | match top_node.parent().map(|it| it.kind()) { | ||
135 | Some(SOURCE_FILE) | Some(ITEM_LIST) => { | ||
136 | self.is_new_item = true; | ||
137 | return; | ||
138 | } | ||
139 | _ => (), | ||
140 | } | ||
141 | |||
142 | let parent = match name_ref.syntax().parent() { | ||
143 | Some(it) => it, | ||
144 | None => return, | ||
145 | }; | ||
146 | if let Some(segment) = ast::PathSegment::cast(parent) { | ||
147 | let path = segment.parent_path(); | ||
148 | if let Some(mut path) = hir::Path::from_ast(path) { | ||
149 | if !path.is_ident() { | ||
150 | path.segments.pop().unwrap(); | ||
151 | self.path_prefix = Some(path); | ||
152 | return; | ||
153 | } | ||
154 | } | ||
155 | if path.qualifier().is_none() { | ||
156 | self.is_trivial_path = true; | ||
157 | self.enclosing_fn = self | ||
158 | .leaf | ||
159 | .ancestors() | ||
160 | .take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE) | ||
161 | .find_map(ast::FnDef::cast); | ||
162 | |||
163 | self.is_stmt = match name_ref | ||
164 | .syntax() | ||
165 | .ancestors() | ||
166 | .filter_map(ast::ExprStmt::cast) | ||
167 | .next() | ||
168 | { | ||
169 | None => false, | ||
170 | Some(expr_stmt) => expr_stmt.syntax().range() == name_ref.syntax().range(), | ||
171 | }; | ||
172 | |||
173 | if let Some(off) = name_ref.syntax().range().start().checked_sub(2.into()) { | ||
174 | if let Some(if_expr) = find_node_at_offset::<ast::IfExpr>(file.syntax(), off) { | ||
175 | if if_expr.syntax().range().end() < name_ref.syntax().range().start() { | ||
176 | self.after_if = true; | ||
177 | } | ||
178 | } | ||
179 | } | ||
180 | } | ||
181 | } | ||
182 | } | ||
183 | } | ||
184 | |||
185 | fn is_node<'a, N: AstNode<'a>>(node: SyntaxNodeRef<'a>) -> bool { | ||
186 | match node.ancestors().filter_map(N::cast).next() { | ||
187 | None => false, | ||
188 | Some(n) => n.syntax().range() == node.range(), | ||
189 | } | ||
190 | } | ||
191 | |||
192 | #[cfg(test)] | 50 | #[cfg(test)] |
193 | fn check_completion(code: &str, expected_completions: &str, kind: CompletionKind) { | 51 | fn check_completion(code: &str, expected_completions: &str, kind: CompletionKind) { |
194 | use crate::mock_analysis::{single_file_with_position, analysis_and_position}; | 52 | use crate::mock_analysis::{single_file_with_position, analysis_and_position}; |
diff --git a/crates/ra_analysis/src/completion/complete_fn_param.rs b/crates/ra_analysis/src/completion/complete_fn_param.rs index d05a5e3cf..3ec507fdf 100644 --- a/crates/ra_analysis/src/completion/complete_fn_param.rs +++ b/crates/ra_analysis/src/completion/complete_fn_param.rs | |||
@@ -8,14 +8,14 @@ use ra_syntax::{ | |||
8 | use rustc_hash::{FxHashMap}; | 8 | use rustc_hash::{FxHashMap}; |
9 | 9 | ||
10 | use crate::{ | 10 | use crate::{ |
11 | completion::{SyntaxContext, Completions, CompletionKind, CompletionItem}, | 11 | completion::{CompletionContext, Completions, CompletionKind, CompletionItem}, |
12 | }; | 12 | }; |
13 | 13 | ||
14 | /// Complete repeated parametes, both name and type. For example, if all | 14 | /// Complete repeated parametes, both name and type. For example, if all |
15 | /// functions in a file have a `spam: &mut Spam` parameter, a completion with | 15 | /// functions in a file have a `spam: &mut Spam` parameter, a completion with |
16 | /// `spam: &mut Spam` insert text/label and `spam` lookup string will be | 16 | /// `spam: &mut Spam` insert text/label and `spam` lookup string will be |
17 | /// suggested. | 17 | /// suggested. |
18 | pub(super) fn complete_fn_param(acc: &mut Completions, ctx: &SyntaxContext) { | 18 | pub(super) fn complete_fn_param(acc: &mut Completions, ctx: &CompletionContext) { |
19 | if !ctx.is_param { | 19 | if !ctx.is_param { |
20 | return; | 20 | return; |
21 | } | 21 | } |
diff --git a/crates/ra_analysis/src/completion/complete_keyword.rs b/crates/ra_analysis/src/completion/complete_keyword.rs index d0a6ec19e..2ee36430e 100644 --- a/crates/ra_analysis/src/completion/complete_keyword.rs +++ b/crates/ra_analysis/src/completion/complete_keyword.rs | |||
@@ -6,10 +6,10 @@ use ra_syntax::{ | |||
6 | }; | 6 | }; |
7 | 7 | ||
8 | use crate::{ | 8 | use crate::{ |
9 | completion::{SyntaxContext, CompletionItem, Completions, CompletionKind::*}, | 9 | completion::{CompletionContext, CompletionItem, Completions, CompletionKind::*}, |
10 | }; | 10 | }; |
11 | 11 | ||
12 | pub(super) fn complete_expr_keyword(acc: &mut Completions, ctx: &SyntaxContext) { | 12 | pub(super) fn complete_expr_keyword(acc: &mut Completions, ctx: &CompletionContext) { |
13 | if !ctx.is_trivial_path { | 13 | if !ctx.is_trivial_path { |
14 | return; | 14 | return; |
15 | } | 15 | } |
diff --git a/crates/ra_analysis/src/completion/complete_path.rs b/crates/ra_analysis/src/completion/complete_path.rs index 8374ec346..41e439b1b 100644 --- a/crates/ra_analysis/src/completion/complete_path.rs +++ b/crates/ra_analysis/src/completion/complete_path.rs | |||
@@ -1,9 +1,9 @@ | |||
1 | use crate::{ | 1 | use crate::{ |
2 | completion::{CompletionItem, Completions, CompletionKind::*, SyntaxContext}, | 2 | completion::{CompletionItem, Completions, CompletionKind::*, CompletionContext}, |
3 | Cancelable, | 3 | Cancelable, |
4 | }; | 4 | }; |
5 | 5 | ||
6 | pub(super) fn complete_path(acc: &mut Completions, ctx: &SyntaxContext) -> Cancelable<()> { | 6 | pub(super) fn complete_path(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { |
7 | let (path, module) = match (&ctx.path_prefix, &ctx.module) { | 7 | let (path, module) = match (&ctx.path_prefix, &ctx.module) { |
8 | (Some(path), Some(module)) => (path.clone(), module), | 8 | (Some(path), Some(module)) => (path.clone(), module), |
9 | _ => return Ok(()), | 9 | _ => return Ok(()), |
diff --git a/crates/ra_analysis/src/completion/complete_scope.rs b/crates/ra_analysis/src/completion/complete_scope.rs index ddaf13b88..c1ab19d5b 100644 --- a/crates/ra_analysis/src/completion/complete_scope.rs +++ b/crates/ra_analysis/src/completion/complete_scope.rs | |||
@@ -2,11 +2,11 @@ use rustc_hash::FxHashSet; | |||
2 | use ra_syntax::TextUnit; | 2 | use ra_syntax::TextUnit; |
3 | 3 | ||
4 | use crate::{ | 4 | use crate::{ |
5 | completion::{CompletionItem, Completions, CompletionKind::*, SyntaxContext}, | 5 | completion::{CompletionItem, Completions, CompletionKind::*, CompletionContext}, |
6 | Cancelable | 6 | Cancelable |
7 | }; | 7 | }; |
8 | 8 | ||
9 | pub(super) fn complete_scope(acc: &mut Completions, ctx: &SyntaxContext) -> Cancelable<()> { | 9 | pub(super) fn complete_scope(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { |
10 | if !ctx.is_trivial_path { | 10 | if !ctx.is_trivial_path { |
11 | return Ok(()); | 11 | return Ok(()); |
12 | } | 12 | } |
diff --git a/crates/ra_analysis/src/completion/complete_snippet.rs b/crates/ra_analysis/src/completion/complete_snippet.rs index 5d6cc5dc9..6816ae695 100644 --- a/crates/ra_analysis/src/completion/complete_snippet.rs +++ b/crates/ra_analysis/src/completion/complete_snippet.rs | |||
@@ -1,8 +1,8 @@ | |||
1 | use crate::{ | 1 | use crate::{ |
2 | completion::{CompletionItem, Completions, CompletionKind::*, SyntaxContext}, | 2 | completion::{CompletionItem, Completions, CompletionKind::*, CompletionContext}, |
3 | }; | 3 | }; |
4 | 4 | ||
5 | pub(super) fn complete_expr_snippet(acc: &mut Completions, ctx: &SyntaxContext) { | 5 | pub(super) fn complete_expr_snippet(acc: &mut Completions, ctx: &CompletionContext) { |
6 | if !(ctx.is_trivial_path && ctx.enclosing_fn.is_some()) { | 6 | if !(ctx.is_trivial_path && ctx.enclosing_fn.is_some()) { |
7 | return; | 7 | return; |
8 | } | 8 | } |
@@ -16,7 +16,7 @@ pub(super) fn complete_expr_snippet(acc: &mut Completions, ctx: &SyntaxContext) | |||
16 | .add_to(acc); | 16 | .add_to(acc); |
17 | } | 17 | } |
18 | 18 | ||
19 | pub(super) fn complete_item_snippet(acc: &mut Completions, ctx: &SyntaxContext) { | 19 | pub(super) fn complete_item_snippet(acc: &mut Completions, ctx: &CompletionContext) { |
20 | if !ctx.is_new_item { | 20 | if !ctx.is_new_item { |
21 | return; | 21 | return; |
22 | } | 22 | } |
diff --git a/crates/ra_analysis/src/completion/completion_context.rs b/crates/ra_analysis/src/completion/completion_context.rs new file mode 100644 index 000000000..064fbc6f7 --- /dev/null +++ b/crates/ra_analysis/src/completion/completion_context.rs | |||
@@ -0,0 +1,156 @@ | |||
1 | use ra_editor::find_node_at_offset; | ||
2 | use ra_text_edit::AtomTextEdit; | ||
3 | use ra_syntax::{ | ||
4 | algo::find_leaf_at_offset, | ||
5 | ast, | ||
6 | AstNode, | ||
7 | SyntaxNodeRef, | ||
8 | SourceFileNode, | ||
9 | TextUnit, | ||
10 | SyntaxKind::*, | ||
11 | }; | ||
12 | use hir::source_binder; | ||
13 | |||
14 | use crate::{db, FilePosition, Cancelable}; | ||
15 | |||
16 | /// `CompletionContext` is created early during completion to figure out, where | ||
17 | /// exactly is the cursor, syntax-wise. | ||
18 | #[derive(Debug)] | ||
19 | pub(super) struct CompletionContext<'a> { | ||
20 | pub(super) db: &'a db::RootDatabase, | ||
21 | pub(super) offset: TextUnit, | ||
22 | pub(super) leaf: SyntaxNodeRef<'a>, | ||
23 | pub(super) module: Option<hir::Module>, | ||
24 | pub(super) enclosing_fn: Option<ast::FnDef<'a>>, | ||
25 | pub(super) is_param: bool, | ||
26 | /// A single-indent path, like `foo`. | ||
27 | pub(super) is_trivial_path: bool, | ||
28 | /// If not a trivial, path, the prefix (qualifier). | ||
29 | pub(super) path_prefix: Option<hir::Path>, | ||
30 | pub(super) after_if: bool, | ||
31 | pub(super) is_stmt: bool, | ||
32 | /// Something is typed at the "top" level, in module or impl/trait. | ||
33 | pub(super) is_new_item: bool, | ||
34 | } | ||
35 | |||
36 | impl<'a> CompletionContext<'a> { | ||
37 | pub(super) fn new( | ||
38 | db: &'a db::RootDatabase, | ||
39 | original_file: &'a SourceFileNode, | ||
40 | position: FilePosition, | ||
41 | ) -> Cancelable<Option<CompletionContext<'a>>> { | ||
42 | let module = source_binder::module_from_position(db, position)?; | ||
43 | let leaf = | ||
44 | ctry!(find_leaf_at_offset(original_file.syntax(), position.offset).left_biased()); | ||
45 | let mut ctx = CompletionContext { | ||
46 | db, | ||
47 | leaf, | ||
48 | offset: position.offset, | ||
49 | module, | ||
50 | enclosing_fn: None, | ||
51 | is_param: false, | ||
52 | is_trivial_path: false, | ||
53 | path_prefix: None, | ||
54 | after_if: false, | ||
55 | is_stmt: false, | ||
56 | is_new_item: false, | ||
57 | }; | ||
58 | ctx.fill(original_file, position.offset); | ||
59 | Ok(Some(ctx)) | ||
60 | } | ||
61 | |||
62 | fn fill(&mut self, original_file: &SourceFileNode, offset: TextUnit) { | ||
63 | // Insert a fake ident to get a valid parse tree. We will use this file | ||
64 | // to determine context, though the original_file will be used for | ||
65 | // actual completion. | ||
66 | let file = { | ||
67 | let edit = AtomTextEdit::insert(offset, "intellijRulezz".to_string()); | ||
68 | original_file.reparse(&edit) | ||
69 | }; | ||
70 | |||
71 | // First, let's try to complete a reference to some declaration. | ||
72 | if let Some(name_ref) = find_node_at_offset::<ast::NameRef>(file.syntax(), offset) { | ||
73 | // Special case, `trait T { fn foo(i_am_a_name_ref) {} }`. | ||
74 | // See RFC#1685. | ||
75 | if is_node::<ast::Param>(name_ref.syntax()) { | ||
76 | self.is_param = true; | ||
77 | return; | ||
78 | } | ||
79 | self.classify_name_ref(&file, name_ref); | ||
80 | } | ||
81 | |||
82 | // Otherwise, see if this is a declaration. We can use heuristics to | ||
83 | // suggest declaration names, see `CompletionKind::Magic`. | ||
84 | if let Some(name) = find_node_at_offset::<ast::Name>(file.syntax(), offset) { | ||
85 | if is_node::<ast::Param>(name.syntax()) { | ||
86 | self.is_param = true; | ||
87 | return; | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | fn classify_name_ref(&mut self, file: &SourceFileNode, name_ref: ast::NameRef) { | ||
92 | let name_range = name_ref.syntax().range(); | ||
93 | let top_node = name_ref | ||
94 | .syntax() | ||
95 | .ancestors() | ||
96 | .take_while(|it| it.range() == name_range) | ||
97 | .last() | ||
98 | .unwrap(); | ||
99 | |||
100 | match top_node.parent().map(|it| it.kind()) { | ||
101 | Some(SOURCE_FILE) | Some(ITEM_LIST) => { | ||
102 | self.is_new_item = true; | ||
103 | return; | ||
104 | } | ||
105 | _ => (), | ||
106 | } | ||
107 | |||
108 | let parent = match name_ref.syntax().parent() { | ||
109 | Some(it) => it, | ||
110 | None => return, | ||
111 | }; | ||
112 | if let Some(segment) = ast::PathSegment::cast(parent) { | ||
113 | let path = segment.parent_path(); | ||
114 | if let Some(mut path) = hir::Path::from_ast(path) { | ||
115 | if !path.is_ident() { | ||
116 | path.segments.pop().unwrap(); | ||
117 | self.path_prefix = Some(path); | ||
118 | return; | ||
119 | } | ||
120 | } | ||
121 | if path.qualifier().is_none() { | ||
122 | self.is_trivial_path = true; | ||
123 | self.enclosing_fn = self | ||
124 | .leaf | ||
125 | .ancestors() | ||
126 | .take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE) | ||
127 | .find_map(ast::FnDef::cast); | ||
128 | |||
129 | self.is_stmt = match name_ref | ||
130 | .syntax() | ||
131 | .ancestors() | ||
132 | .filter_map(ast::ExprStmt::cast) | ||
133 | .next() | ||
134 | { | ||
135 | None => false, | ||
136 | Some(expr_stmt) => expr_stmt.syntax().range() == name_ref.syntax().range(), | ||
137 | }; | ||
138 | |||
139 | if let Some(off) = name_ref.syntax().range().start().checked_sub(2.into()) { | ||
140 | if let Some(if_expr) = find_node_at_offset::<ast::IfExpr>(file.syntax(), off) { | ||
141 | if if_expr.syntax().range().end() < name_ref.syntax().range().start() { | ||
142 | self.after_if = true; | ||
143 | } | ||
144 | } | ||
145 | } | ||
146 | } | ||
147 | } | ||
148 | } | ||
149 | } | ||
150 | |||
151 | fn is_node<'a, N: AstNode<'a>>(node: SyntaxNodeRef<'a>) -> bool { | ||
152 | match node.ancestors().filter_map(N::cast).next() { | ||
153 | None => false, | ||
154 | Some(n) => n.syntax().range() == node.range(), | ||
155 | } | ||
156 | } | ||