diff options
Diffstat (limited to 'crates/hir_def/src')
-rw-r--r-- | crates/hir_def/src/import_map.rs | 287 | ||||
-rw-r--r-- | crates/hir_def/src/item_scope.rs | 3 |
2 files changed, 177 insertions, 113 deletions
diff --git a/crates/hir_def/src/import_map.rs b/crates/hir_def/src/import_map.rs index 30b22f51d..e5368b293 100644 --- a/crates/hir_def/src/import_map.rs +++ b/crates/hir_def/src/import_map.rs | |||
@@ -7,9 +7,8 @@ use fst::{self, Streamer}; | |||
7 | use hir_expand::name::Name; | 7 | use hir_expand::name::Name; |
8 | use indexmap::{map::Entry, IndexMap}; | 8 | use indexmap::{map::Entry, IndexMap}; |
9 | use itertools::Itertools; | 9 | use itertools::Itertools; |
10 | use rustc_hash::{FxHashMap, FxHashSet, FxHasher}; | 10 | use rustc_hash::{FxHashSet, FxHasher}; |
11 | use smallvec::SmallVec; | 11 | use test_utils::mark; |
12 | use syntax::SmolStr; | ||
13 | 12 | ||
14 | use crate::{ | 13 | use crate::{ |
15 | db::DefDatabase, item_scope::ItemInNs, visibility::Visibility, AssocItemId, ModuleDefId, | 14 | db::DefDatabase, item_scope::ItemInNs, visibility::Visibility, AssocItemId, ModuleDefId, |
@@ -25,6 +24,8 @@ pub struct ImportInfo { | |||
25 | pub path: ImportPath, | 24 | pub path: ImportPath, |
26 | /// The module containing this item. | 25 | /// The module containing this item. |
27 | pub container: ModuleId, | 26 | pub container: ModuleId, |
27 | /// Whether the import is a trait associated item or not. | ||
28 | pub is_trait_assoc_item: bool, | ||
28 | } | 29 | } |
29 | 30 | ||
30 | #[derive(Debug, Clone, Eq, PartialEq)] | 31 | #[derive(Debug, Clone, Eq, PartialEq)] |
@@ -64,10 +65,6 @@ pub struct ImportMap { | |||
64 | /// the index of the first one. | 65 | /// the index of the first one. |
65 | importables: Vec<ItemInNs>, | 66 | importables: Vec<ItemInNs>, |
66 | fst: fst::Map<Vec<u8>>, | 67 | fst: fst::Map<Vec<u8>>, |
67 | |||
68 | /// Maps names of associated items to the item's ID. Only includes items whose defining trait is | ||
69 | /// exported. | ||
70 | assoc_map: FxHashMap<SmolStr, SmallVec<[AssocItemId; 1]>>, | ||
71 | } | 68 | } |
72 | 69 | ||
73 | impl ImportMap { | 70 | impl ImportMap { |
@@ -108,14 +105,27 @@ impl ImportMap { | |||
108 | 105 | ||
109 | for item in per_ns.iter_items() { | 106 | for item in per_ns.iter_items() { |
110 | let path = mk_path(); | 107 | let path = mk_path(); |
108 | let path_len = path.len(); | ||
109 | let import_info = | ||
110 | ImportInfo { path, container: module, is_trait_assoc_item: false }; | ||
111 | |||
112 | if let Some(ModuleDefId::TraitId(tr)) = item.as_module_def_id() { | ||
113 | import_map.collect_trait_assoc_items( | ||
114 | db, | ||
115 | tr, | ||
116 | matches!(item, ItemInNs::Types(_)), | ||
117 | &import_info, | ||
118 | ); | ||
119 | } | ||
120 | |||
111 | match import_map.map.entry(item) { | 121 | match import_map.map.entry(item) { |
112 | Entry::Vacant(entry) => { | 122 | Entry::Vacant(entry) => { |
113 | entry.insert(ImportInfo { path, container: module }); | 123 | entry.insert(import_info); |
114 | } | 124 | } |
115 | Entry::Occupied(mut entry) => { | 125 | Entry::Occupied(mut entry) => { |
116 | // If the new path is shorter, prefer that one. | 126 | // If the new path is shorter, prefer that one. |
117 | if path.len() < entry.get().path.len() { | 127 | if path_len < entry.get().path.len() { |
118 | *entry.get_mut() = ImportInfo { path, container: module }; | 128 | *entry.get_mut() = import_info; |
119 | } else { | 129 | } else { |
120 | continue; | 130 | continue; |
121 | } | 131 | } |
@@ -128,11 +138,6 @@ impl ImportMap { | |||
128 | if let Some(ModuleDefId::ModuleId(mod_id)) = item.as_module_def_id() { | 138 | if let Some(ModuleDefId::ModuleId(mod_id)) = item.as_module_def_id() { |
129 | worklist.push((mod_id, mk_path())); | 139 | worklist.push((mod_id, mk_path())); |
130 | } | 140 | } |
131 | |||
132 | // If we've added a path to a trait, add the trait's methods to the method map. | ||
133 | if let Some(ModuleDefId::TraitId(tr)) = item.as_module_def_id() { | ||
134 | import_map.collect_trait_methods(db, tr); | ||
135 | } | ||
136 | } | 141 | } |
137 | } | 142 | } |
138 | } | 143 | } |
@@ -153,12 +158,10 @@ impl ImportMap { | |||
153 | } | 158 | } |
154 | } | 159 | } |
155 | 160 | ||
156 | let start = last_batch_start; | 161 | let key = fst_path(&importables[last_batch_start].1.path); |
157 | last_batch_start = idx + 1; | 162 | builder.insert(key, last_batch_start as u64).unwrap(); |
158 | |||
159 | let key = fst_path(&importables[start].1.path); | ||
160 | 163 | ||
161 | builder.insert(key, start as u64).unwrap(); | 164 | last_batch_start = idx + 1; |
162 | } | 165 | } |
163 | 166 | ||
164 | import_map.fst = fst::Map::new(builder.into_inner().unwrap()).unwrap(); | 167 | import_map.fst = fst::Map::new(builder.into_inner().unwrap()).unwrap(); |
@@ -176,10 +179,34 @@ impl ImportMap { | |||
176 | self.map.get(&item) | 179 | self.map.get(&item) |
177 | } | 180 | } |
178 | 181 | ||
179 | fn collect_trait_methods(&mut self, db: &dyn DefDatabase, tr: TraitId) { | 182 | fn collect_trait_assoc_items( |
180 | let data = db.trait_data(tr); | 183 | &mut self, |
181 | for (name, item) in data.items.iter() { | 184 | db: &dyn DefDatabase, |
182 | self.assoc_map.entry(name.to_string().into()).or_default().push(*item); | 185 | tr: TraitId, |
186 | is_type_in_ns: bool, | ||
187 | original_import_info: &ImportInfo, | ||
188 | ) { | ||
189 | for (assoc_item_name, item) in &db.trait_data(tr).items { | ||
190 | let module_def_id = match item { | ||
191 | AssocItemId::FunctionId(f) => ModuleDefId::from(*f), | ||
192 | AssocItemId::ConstId(c) => ModuleDefId::from(*c), | ||
193 | // cannot use associated type aliases directly: need a `<Struct as Trait>::TypeAlias` | ||
194 | // qualifier, ergo no need to store it for imports in import_map | ||
195 | AssocItemId::TypeAliasId(_) => { | ||
196 | mark::hit!(type_aliases_ignored); | ||
197 | continue; | ||
198 | } | ||
199 | }; | ||
200 | let assoc_item = if is_type_in_ns { | ||
201 | ItemInNs::Types(module_def_id) | ||
202 | } else { | ||
203 | ItemInNs::Values(module_def_id) | ||
204 | }; | ||
205 | |||
206 | let mut assoc_item_info = original_import_info.clone(); | ||
207 | assoc_item_info.path.segments.push(assoc_item_name.to_owned()); | ||
208 | assoc_item_info.is_trait_assoc_item = true; | ||
209 | self.map.insert(assoc_item, assoc_item_info); | ||
183 | } | 210 | } |
184 | } | 211 | } |
185 | } | 212 | } |
@@ -302,38 +329,38 @@ impl Query { | |||
302 | self.exclude_import_kinds.insert(import_kind); | 329 | self.exclude_import_kinds.insert(import_kind); |
303 | self | 330 | self |
304 | } | 331 | } |
305 | } | ||
306 | 332 | ||
307 | fn contains_query(query: &Query, input_path: &ImportPath, enforce_lowercase: bool) -> bool { | 333 | fn import_matches(&self, import: &ImportInfo, enforce_lowercase: bool) -> bool { |
308 | let mut input = if query.name_only { | 334 | let mut input = if import.is_trait_assoc_item || self.name_only { |
309 | input_path.segments.last().unwrap().to_string() | 335 | import.path.segments.last().unwrap().to_string() |
310 | } else { | 336 | } else { |
311 | input_path.to_string() | 337 | import.path.to_string() |
312 | }; | 338 | }; |
313 | if enforce_lowercase || !query.case_sensitive { | 339 | if enforce_lowercase || !self.case_sensitive { |
314 | input.make_ascii_lowercase(); | 340 | input.make_ascii_lowercase(); |
315 | } | 341 | } |
316 | 342 | ||
317 | let query_string = | 343 | let query_string = |
318 | if !enforce_lowercase && query.case_sensitive { &query.query } else { &query.lowercased }; | 344 | if !enforce_lowercase && self.case_sensitive { &self.query } else { &self.lowercased }; |
319 | 345 | ||
320 | match query.search_mode { | 346 | match self.search_mode { |
321 | SearchMode::Equals => &input == query_string, | 347 | SearchMode::Equals => &input == query_string, |
322 | SearchMode::Contains => input.contains(query_string), | 348 | SearchMode::Contains => input.contains(query_string), |
323 | SearchMode::Fuzzy => { | 349 | SearchMode::Fuzzy => { |
324 | let mut unchecked_query_chars = query_string.chars(); | 350 | let mut unchecked_query_chars = query_string.chars(); |
325 | let mut mismatching_query_char = unchecked_query_chars.next(); | 351 | let mut mismatching_query_char = unchecked_query_chars.next(); |
326 | 352 | ||
327 | for input_char in input.chars() { | 353 | for input_char in input.chars() { |
328 | match mismatching_query_char { | 354 | match mismatching_query_char { |
329 | None => return true, | 355 | None => return true, |
330 | Some(matching_query_char) if matching_query_char == input_char => { | 356 | Some(matching_query_char) if matching_query_char == input_char => { |
331 | mismatching_query_char = unchecked_query_chars.next(); | 357 | mismatching_query_char = unchecked_query_chars.next(); |
358 | } | ||
359 | _ => (), | ||
332 | } | 360 | } |
333 | _ => (), | ||
334 | } | 361 | } |
362 | mismatching_query_char.is_none() | ||
335 | } | 363 | } |
336 | mismatching_query_char.is_none() | ||
337 | } | 364 | } |
338 | } | 365 | } |
339 | } | 366 | } |
@@ -366,13 +393,13 @@ pub fn search_dependencies<'a>( | |||
366 | let import_map = &import_maps[indexed_value.index]; | 393 | let import_map = &import_maps[indexed_value.index]; |
367 | let importables = &import_map.importables[indexed_value.value as usize..]; | 394 | let importables = &import_map.importables[indexed_value.value as usize..]; |
368 | 395 | ||
369 | // Path shared by the importable items in this group. | 396 | let common_importable_data = &import_map.map[&importables[0]]; |
370 | let common_importables_path = &import_map.map[&importables[0]].path; | 397 | if !query.import_matches(common_importable_data, true) { |
371 | if !contains_query(&query, common_importables_path, true) { | ||
372 | continue; | 398 | continue; |
373 | } | 399 | } |
374 | 400 | ||
375 | let common_importables_path_fst = fst_path(common_importables_path); | 401 | // Path shared by the importable items in this group. |
402 | let common_importables_path_fst = fst_path(&common_importable_data.path); | ||
376 | // Add the items from this `ModPath` group. Those are all subsequent items in | 403 | // Add the items from this `ModPath` group. Those are all subsequent items in |
377 | // `importables` whose paths match `path`. | 404 | // `importables` whose paths match `path`. |
378 | let iter = importables | 405 | let iter = importables |
@@ -387,7 +414,7 @@ pub fn search_dependencies<'a>( | |||
387 | }) | 414 | }) |
388 | .filter(|item| { | 415 | .filter(|item| { |
389 | !query.case_sensitive // we've already checked the common importables path case-insensitively | 416 | !query.case_sensitive // we've already checked the common importables path case-insensitively |
390 | || contains_query(&query, &import_map.map[item].path, false) | 417 | || query.import_matches(&import_map.map[item], false) |
391 | }); | 418 | }); |
392 | res.extend(iter); | 419 | res.extend(iter); |
393 | 420 | ||
@@ -398,19 +425,6 @@ pub fn search_dependencies<'a>( | |||
398 | } | 425 | } |
399 | } | 426 | } |
400 | 427 | ||
401 | // Add all exported associated items whose names match the query (exactly). | ||
402 | for map in &import_maps { | ||
403 | if let Some(v) = map.assoc_map.get(&*query.query) { | ||
404 | res.extend(v.iter().map(|&assoc| { | ||
405 | ItemInNs::Types(match assoc { | ||
406 | AssocItemId::FunctionId(it) => it.into(), | ||
407 | AssocItemId::ConstId(it) => it.into(), | ||
408 | AssocItemId::TypeAliasId(it) => it.into(), | ||
409 | }) | ||
410 | })); | ||
411 | } | ||
412 | } | ||
413 | |||
414 | res | 428 | res |
415 | } | 429 | } |
416 | 430 | ||
@@ -432,9 +446,9 @@ fn item_import_kind(item: ItemInNs) -> Option<ImportKind> { | |||
432 | mod tests { | 446 | mod tests { |
433 | use base_db::{fixture::WithFixture, SourceDatabase, Upcast}; | 447 | use base_db::{fixture::WithFixture, SourceDatabase, Upcast}; |
434 | use expect_test::{expect, Expect}; | 448 | use expect_test::{expect, Expect}; |
435 | use stdx::format_to; | 449 | use test_utils::mark; |
436 | 450 | ||
437 | use crate::{data::FunctionData, test_db::TestDB, AssocContainerId, Lookup}; | 451 | use crate::{test_db::TestDB, AssocContainerId, Lookup}; |
438 | 452 | ||
439 | use super::*; | 453 | use super::*; |
440 | 454 | ||
@@ -451,46 +465,66 @@ mod tests { | |||
451 | 465 | ||
452 | let actual = search_dependencies(db.upcast(), krate, query) | 466 | let actual = search_dependencies(db.upcast(), krate, query) |
453 | .into_iter() | 467 | .into_iter() |
454 | .filter_map(|item| { | 468 | .filter_map(|dependency| { |
455 | let mark = match item { | 469 | let dependency_krate = dependency.krate(db.upcast())?; |
456 | ItemInNs::Types(ModuleDefId::FunctionId(_)) | 470 | let dependency_imports = db.import_map(dependency_krate); |
457 | | ItemInNs::Values(ModuleDefId::FunctionId(_)) => "f", | 471 | |
458 | ItemInNs::Types(_) => "t", | 472 | let (path, mark) = match assoc_item_path(&db, &dependency_imports, dependency) { |
459 | ItemInNs::Values(_) => "v", | 473 | Some(assoc_item_path) => (assoc_item_path, "a"), |
460 | ItemInNs::Macros(_) => "m", | 474 | None => ( |
475 | dependency_imports.path_of(dependency)?.to_string(), | ||
476 | match dependency { | ||
477 | ItemInNs::Types(ModuleDefId::FunctionId(_)) | ||
478 | | ItemInNs::Values(ModuleDefId::FunctionId(_)) => "f", | ||
479 | ItemInNs::Types(_) => "t", | ||
480 | ItemInNs::Values(_) => "v", | ||
481 | ItemInNs::Macros(_) => "m", | ||
482 | }, | ||
483 | ), | ||
461 | }; | 484 | }; |
462 | item.krate(db.upcast()).map(|krate| { | 485 | |
463 | let map = db.import_map(krate); | 486 | Some(format!( |
464 | 487 | "{}::{} ({})\n", | |
465 | let path = match assoc_to_trait(&db, item) { | 488 | crate_graph[dependency_krate].display_name.as_ref()?, |
466 | Some(trait_) => { | 489 | path, |
467 | let mut full_path = map.path_of(trait_).unwrap().to_string(); | 490 | mark |
468 | if let ItemInNs::Types(ModuleDefId::FunctionId(function_id)) | 491 | )) |
469 | | ItemInNs::Values(ModuleDefId::FunctionId(function_id)) = item | ||
470 | { | ||
471 | format_to!( | ||
472 | full_path, | ||
473 | "::{}", | ||
474 | FunctionData::fn_data_query(&db, function_id).name, | ||
475 | ); | ||
476 | } | ||
477 | full_path | ||
478 | } | ||
479 | None => map.path_of(item).unwrap().to_string(), | ||
480 | }; | ||
481 | |||
482 | format!( | ||
483 | "{}::{} ({})\n", | ||
484 | crate_graph[krate].display_name.as_ref().unwrap(), | ||
485 | path, | ||
486 | mark | ||
487 | ) | ||
488 | }) | ||
489 | }) | 492 | }) |
490 | .collect::<String>(); | 493 | .collect::<String>(); |
491 | expect.assert_eq(&actual) | 494 | expect.assert_eq(&actual) |
492 | } | 495 | } |
493 | 496 | ||
497 | fn assoc_item_path( | ||
498 | db: &dyn DefDatabase, | ||
499 | dependency_imports: &ImportMap, | ||
500 | dependency: ItemInNs, | ||
501 | ) -> Option<String> { | ||
502 | let dependency_assoc_item_id = match dependency { | ||
503 | ItemInNs::Types(ModuleDefId::FunctionId(id)) | ||
504 | | ItemInNs::Values(ModuleDefId::FunctionId(id)) => AssocItemId::from(id), | ||
505 | ItemInNs::Types(ModuleDefId::ConstId(id)) | ||
506 | | ItemInNs::Values(ModuleDefId::ConstId(id)) => AssocItemId::from(id), | ||
507 | ItemInNs::Types(ModuleDefId::TypeAliasId(id)) | ||
508 | | ItemInNs::Values(ModuleDefId::TypeAliasId(id)) => AssocItemId::from(id), | ||
509 | _ => return None, | ||
510 | }; | ||
511 | |||
512 | let trait_ = assoc_to_trait(db, dependency)?; | ||
513 | if let ModuleDefId::TraitId(tr) = trait_.as_module_def_id()? { | ||
514 | let trait_data = db.trait_data(tr); | ||
515 | let assoc_item_name = | ||
516 | trait_data.items.iter().find_map(|(assoc_item_name, assoc_item_id)| { | ||
517 | if &dependency_assoc_item_id == assoc_item_id { | ||
518 | Some(assoc_item_name) | ||
519 | } else { | ||
520 | None | ||
521 | } | ||
522 | })?; | ||
523 | return Some(format!("{}::{}", dependency_imports.path_of(trait_)?, assoc_item_name)); | ||
524 | } | ||
525 | None | ||
526 | } | ||
527 | |||
494 | fn assoc_to_trait(db: &dyn DefDatabase, item: ItemInNs) -> Option<ItemInNs> { | 528 | fn assoc_to_trait(db: &dyn DefDatabase, item: ItemInNs) -> Option<ItemInNs> { |
495 | let assoc: AssocItemId = match item { | 529 | let assoc: AssocItemId = match item { |
496 | ItemInNs::Types(it) | ItemInNs::Values(it) => match it { | 530 | ItemInNs::Types(it) | ItemInNs::Values(it) => match it { |
@@ -749,6 +783,37 @@ mod tests { | |||
749 | } | 783 | } |
750 | 784 | ||
751 | #[test] | 785 | #[test] |
786 | fn fuzzy_import_trait_and_assoc_items() { | ||
787 | mark::check!(type_aliases_ignored); | ||
788 | let ra_fixture = r#" | ||
789 | //- /main.rs crate:main deps:dep | ||
790 | //- /dep.rs crate:dep | ||
791 | pub mod fmt { | ||
792 | pub trait Display { | ||
793 | type FmtTypeAlias; | ||
794 | const FMT_CONST: bool; | ||
795 | |||
796 | fn format_function(); | ||
797 | fn format_method(&self); | ||
798 | } | ||
799 | } | ||
800 | "#; | ||
801 | |||
802 | check_search( | ||
803 | ra_fixture, | ||
804 | "main", | ||
805 | Query::new("fmt".to_string()).search_mode(SearchMode::Fuzzy), | ||
806 | expect![[r#" | ||
807 | dep::fmt (t) | ||
808 | dep::fmt::Display (t) | ||
809 | dep::fmt::Display::FMT_CONST (a) | ||
810 | dep::fmt::Display::format_function (a) | ||
811 | dep::fmt::Display::format_method (a) | ||
812 | "#]], | ||
813 | ); | ||
814 | } | ||
815 | |||
816 | #[test] | ||
752 | fn search_mode() { | 817 | fn search_mode() { |
753 | let ra_fixture = r#" | 818 | let ra_fixture = r#" |
754 | //- /main.rs crate:main deps:dep | 819 | //- /main.rs crate:main deps:dep |
@@ -784,8 +849,8 @@ mod tests { | |||
784 | dep::Fmt (v) | 849 | dep::Fmt (v) |
785 | dep::Fmt (m) | 850 | dep::Fmt (m) |
786 | dep::fmt::Display (t) | 851 | dep::fmt::Display (t) |
852 | dep::fmt::Display::fmt (a) | ||
787 | dep::format (f) | 853 | dep::format (f) |
788 | dep::fmt::Display::fmt (f) | ||
789 | "#]], | 854 | "#]], |
790 | ); | 855 | ); |
791 | 856 | ||
@@ -798,7 +863,7 @@ mod tests { | |||
798 | dep::Fmt (t) | 863 | dep::Fmt (t) |
799 | dep::Fmt (v) | 864 | dep::Fmt (v) |
800 | dep::Fmt (m) | 865 | dep::Fmt (m) |
801 | dep::fmt::Display::fmt (f) | 866 | dep::fmt::Display::fmt (a) |
802 | "#]], | 867 | "#]], |
803 | ); | 868 | ); |
804 | 869 | ||
@@ -812,7 +877,7 @@ mod tests { | |||
812 | dep::Fmt (v) | 877 | dep::Fmt (v) |
813 | dep::Fmt (m) | 878 | dep::Fmt (m) |
814 | dep::fmt::Display (t) | 879 | dep::fmt::Display (t) |
815 | dep::fmt::Display::fmt (f) | 880 | dep::fmt::Display::fmt (a) |
816 | "#]], | 881 | "#]], |
817 | ); | 882 | ); |
818 | } | 883 | } |
@@ -853,7 +918,7 @@ mod tests { | |||
853 | dep::Fmt (v) | 918 | dep::Fmt (v) |
854 | dep::Fmt (m) | 919 | dep::Fmt (m) |
855 | dep::fmt::Display (t) | 920 | dep::fmt::Display (t) |
856 | dep::fmt::Display::fmt (f) | 921 | dep::fmt::Display::fmt (a) |
857 | "#]], | 922 | "#]], |
858 | ); | 923 | ); |
859 | 924 | ||
@@ -866,7 +931,7 @@ mod tests { | |||
866 | dep::Fmt (t) | 931 | dep::Fmt (t) |
867 | dep::Fmt (v) | 932 | dep::Fmt (v) |
868 | dep::Fmt (m) | 933 | dep::Fmt (m) |
869 | dep::fmt::Display::fmt (f) | 934 | dep::fmt::Display::fmt (a) |
870 | "#]], | 935 | "#]], |
871 | ); | 936 | ); |
872 | } | 937 | } |
diff --git a/crates/hir_def/src/item_scope.rs b/crates/hir_def/src/item_scope.rs index 62ab3b2bd..2750e1c91 100644 --- a/crates/hir_def/src/item_scope.rs +++ b/crates/hir_def/src/item_scope.rs | |||
@@ -10,10 +10,9 @@ use once_cell::sync::Lazy; | |||
10 | use rustc_hash::{FxHashMap, FxHashSet}; | 10 | use rustc_hash::{FxHashMap, FxHashSet}; |
11 | use test_utils::mark; | 11 | use test_utils::mark; |
12 | 12 | ||
13 | use crate::ModuleId; | ||
14 | use crate::{ | 13 | use crate::{ |
15 | db::DefDatabase, per_ns::PerNs, visibility::Visibility, AdtId, BuiltinType, HasModule, ImplId, | 14 | db::DefDatabase, per_ns::PerNs, visibility::Visibility, AdtId, BuiltinType, HasModule, ImplId, |
16 | LocalModuleId, Lookup, MacroDefId, ModuleDefId, TraitId, | 15 | LocalModuleId, Lookup, MacroDefId, ModuleDefId, ModuleId, TraitId, |
17 | }; | 16 | }; |
18 | 17 | ||
19 | #[derive(Copy, Clone)] | 18 | #[derive(Copy, Clone)] |