Skip to content

Commit 4bcf8c8

Browse files
author
Jonas Schievink
committed
Add an FST index to ImportMap
1 parent 54936e8 commit 4bcf8c8

File tree

4 files changed

+261
-3
lines changed

4 files changed

+261
-3
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ra_hir_def/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ rustc-hash = "1.1.0"
1414
either = "1.5.3"
1515
anymap = "0.12.1"
1616
drop_bomb = "0.1.4"
17+
fst = { version = "0.4", default-features = false }
18+
itertools = "0.9.0"
1719

1820
stdx = { path = "../stdx" }
1921

crates/ra_hir_def/src/import_map.rs

Lines changed: 250 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
//! A map of all publicly exported items in a crate.
22
3+
use std::cmp::Ordering;
34
use std::{collections::hash_map::Entry, fmt, sync::Arc};
45

6+
use fst::{self, Streamer};
7+
use itertools::Itertools;
58
use ra_db::CrateId;
69
use rustc_hash::FxHashMap;
710

@@ -21,9 +24,17 @@ use crate::{
2124
///
2225
/// Note that all paths are relative to the containing crate's root, so the crate name still needs
2326
/// to be prepended to the `ModPath` before the path is valid.
24-
#[derive(Eq, PartialEq)]
2527
pub struct ImportMap {
2628
map: FxHashMap<ItemInNs, ModPath>,
29+
30+
/// List of keys stored in `map`, sorted lexicographically by their `ModPath`. Indexed by the
31+
/// values returned by running `fst`.
32+
///
33+
/// Since a path can refer to multiple items due to namespacing, we store all items with the
34+
/// same path right after each other. This allows us to find all items after the FST gives us
35+
/// the index of the first one.
36+
importables: Vec<ItemInNs>,
37+
fst: fst::Map<Vec<u8>>,
2738
}
2839

2940
impl ImportMap {
@@ -88,7 +99,34 @@ impl ImportMap {
8899
}
89100
}
90101

91-
Arc::new(Self { map: import_map })
102+
let mut importables = import_map.iter().collect::<Vec<_>>();
103+
104+
importables.sort_by(cmp);
105+
106+
// Build the FST, taking care not to insert duplicate values.
107+
108+
let mut builder = fst::MapBuilder::memory();
109+
let mut last_batch_start = 0;
110+
111+
for idx in 0..importables.len() {
112+
if let Some(next_item) = importables.get(idx + 1) {
113+
if cmp(&importables[last_batch_start], next_item) == Ordering::Equal {
114+
continue;
115+
}
116+
}
117+
118+
let start = last_batch_start;
119+
last_batch_start = idx + 1;
120+
121+
let key: String = fst_path(&importables[start].1).collect();
122+
123+
builder.insert(key, start as u64).unwrap();
124+
}
125+
126+
let fst = fst::Map::new(builder.into_inner().unwrap()).unwrap();
127+
let importables = importables.iter().map(|(item, _)| **item).collect();
128+
129+
Arc::new(Self { map: import_map, fst, importables })
92130
}
93131

94132
/// Returns the `ModPath` needed to import/mention `item`, relative to this crate's root.
@@ -97,6 +135,14 @@ impl ImportMap {
97135
}
98136
}
99137

138+
impl PartialEq for ImportMap {
139+
fn eq(&self, other: &Self) -> bool {
140+
self.importables == other.importables
141+
}
142+
}
143+
144+
impl Eq for ImportMap {}
145+
100146
impl fmt::Debug for ImportMap {
101147
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102148
let mut importable_paths: Vec<_> = self
@@ -117,13 +163,97 @@ impl fmt::Debug for ImportMap {
117163
}
118164
}
119165

166+
fn fst_path(path: &ModPath) -> impl Iterator<Item = char> + '_ {
167+
path.segments
168+
.iter()
169+
.map(|name| name.as_text().unwrap())
170+
.intersperse("::")
171+
.flat_map(|s| s.chars().map(|c| c.to_ascii_lowercase()))
172+
}
173+
174+
fn cmp((_, lhs): &(&ItemInNs, &ModPath), (_, rhs): &(&ItemInNs, &ModPath)) -> Ordering {
175+
let lhs_chars = fst_path(lhs);
176+
let rhs_chars = fst_path(rhs);
177+
lhs_chars.cmp(rhs_chars)
178+
}
179+
180+
#[derive(Debug)]
181+
pub struct Query {
182+
query: String,
183+
anchor_end: bool,
184+
}
185+
186+
impl Query {
187+
pub fn new(query: impl AsRef<str>) -> Self {
188+
Self { query: query.as_ref().to_lowercase(), anchor_end: false }
189+
}
190+
191+
/// Only returns items whose paths end with the (case-insensitive) query string as their last
192+
/// segment.
193+
pub fn anchor_end(self) -> Self {
194+
Self { anchor_end: true, ..self }
195+
}
196+
}
197+
198+
/// Searches dependencies of `krate` for an importable path matching `query`.
199+
///
200+
/// This returns all items that could be imported from within `krate`, excluding paths inside
201+
/// `krate` itself.
202+
pub fn search_dependencies<'a>(
203+
db: &'a dyn DefDatabase,
204+
krate: CrateId,
205+
query: Query,
206+
) -> Vec<ItemInNs> {
207+
let _p = ra_prof::profile("import_map::global_search").detail(|| format!("{:?}", query));
208+
209+
let graph = db.crate_graph();
210+
let import_maps: Vec<_> =
211+
graph[krate].dependencies.iter().map(|dep| db.import_map(dep.crate_id)).collect();
212+
213+
let automaton = fst::automaton::Subsequence::new(&query.query);
214+
215+
let mut op = fst::map::OpBuilder::new();
216+
for map in &import_maps {
217+
op = op.add(map.fst.search(&automaton));
218+
}
219+
220+
let mut stream = op.union();
221+
let mut res = Vec::new();
222+
while let Some((_, indexed_values)) = stream.next() {
223+
for indexed_value in indexed_values {
224+
let import_map = &import_maps[indexed_value.index];
225+
let importables = &import_map.importables[indexed_value.value as usize..];
226+
227+
// Path shared by the importable items in this group.
228+
let path = &import_map.map[&importables[0]];
229+
230+
if query.anchor_end {
231+
// Last segment must match query.
232+
let last = path.segments.last().unwrap().to_string();
233+
if last.to_lowercase() != query.query {
234+
continue;
235+
}
236+
}
237+
238+
// Add the items from this `ModPath` group. Those are all subsequent items in
239+
// `importables` whose paths match `path`.
240+
res.extend(importables.iter().copied().take_while(|item| {
241+
let item_path = &import_map.map[item];
242+
fst_path(item_path).eq(fst_path(path))
243+
}));
244+
}
245+
}
246+
247+
res
248+
}
249+
120250
#[cfg(test)]
121251
mod tests {
122252
use super::*;
123253
use crate::test_db::TestDB;
124254
use insta::assert_snapshot;
125255
use ra_db::fixture::WithFixture;
126-
use ra_db::SourceDatabase;
256+
use ra_db::{SourceDatabase, Upcast};
127257

128258
fn import_map(ra_fixture: &str) -> String {
129259
let db = TestDB::with_files(ra_fixture);
@@ -144,6 +274,40 @@ mod tests {
144274
import_maps.join("\n")
145275
}
146276

277+
fn search_dependencies_of(ra_fixture: &str, krate_name: &str, query: Query) -> String {
278+
let db = TestDB::with_files(ra_fixture);
279+
let crate_graph = db.crate_graph();
280+
let krate = crate_graph
281+
.iter()
282+
.find(|krate| {
283+
crate_graph[*krate].display_name.as_ref().map(|n| n.to_string())
284+
== Some(krate_name.to_string())
285+
})
286+
.unwrap();
287+
288+
search_dependencies(db.upcast(), krate, query)
289+
.into_iter()
290+
.filter_map(|item| {
291+
let mark = match item {
292+
ItemInNs::Types(_) => "t",
293+
ItemInNs::Values(_) => "v",
294+
ItemInNs::Macros(_) => "m",
295+
};
296+
item.krate(db.upcast()).map(|krate| {
297+
let map = db.import_map(krate);
298+
let path = map.path_of(item).unwrap();
299+
format!(
300+
"{}::{} ({})",
301+
crate_graph[krate].display_name.as_ref().unwrap(),
302+
path,
303+
mark
304+
)
305+
})
306+
})
307+
.collect::<Vec<_>>()
308+
.join("\n")
309+
}
310+
147311
#[test]
148312
fn smoke() {
149313
let map = import_map(
@@ -328,4 +492,87 @@ mod tests {
328492
lib:
329493
"###);
330494
}
495+
496+
#[test]
497+
fn namespacing() {
498+
let map = import_map(
499+
r"
500+
//- /lib.rs crate:lib
501+
pub struct Thing; // t + v
502+
#[macro_export]
503+
macro_rules! Thing { // m
504+
() => {};
505+
}
506+
",
507+
);
508+
509+
assert_snapshot!(map, @r###"
510+
lib:
511+
- Thing (m)
512+
- Thing (t)
513+
- Thing (v)
514+
"###);
515+
516+
let map = import_map(
517+
r"
518+
//- /lib.rs crate:lib
519+
pub mod Thing {} // t
520+
#[macro_export]
521+
macro_rules! Thing { // m
522+
() => {};
523+
}
524+
",
525+
);
526+
527+
assert_snapshot!(map, @r###"
528+
lib:
529+
- Thing (m)
530+
- Thing (t)
531+
"###);
532+
}
533+
534+
#[test]
535+
fn search() {
536+
let ra_fixture = r#"
537+
//- /main.rs crate:main deps:dep
538+
//- /dep.rs crate:dep deps:tdep
539+
use tdep::fmt as fmt_dep;
540+
pub mod fmt {
541+
pub trait Display {
542+
fn fmt();
543+
}
544+
}
545+
#[macro_export]
546+
macro_rules! Fmt {
547+
() => {};
548+
}
549+
pub struct Fmt;
550+
551+
pub fn format() {}
552+
pub fn no() {}
553+
554+
//- /tdep.rs crate:tdep
555+
pub mod fmt {
556+
pub struct NotImportableFromMain;
557+
}
558+
"#;
559+
560+
let res = search_dependencies_of(ra_fixture, "main", Query::new("fmt"));
561+
assert_snapshot!(res, @r###"
562+
dep::Fmt (v)
563+
dep::fmt (t)
564+
dep::Fmt (t)
565+
dep::Fmt (m)
566+
dep::fmt::Display (t)
567+
dep::format (v)
568+
"###);
569+
570+
let res = search_dependencies_of(ra_fixture, "main", Query::new("fmt").anchor_end());
571+
assert_snapshot!(res, @r###"
572+
dep::Fmt (v)
573+
dep::fmt (t)
574+
dep::Fmt (t)
575+
dep::Fmt (m)
576+
"###);
577+
}
331578
}

crates/ra_hir_expand/src/name.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ impl Name {
6767
_ => None,
6868
}
6969
}
70+
71+
pub fn as_text(&self) -> Option<&str> {
72+
match &self.0 {
73+
Repr::Text(s) => Some(s.as_str()),
74+
_ => None,
75+
}
76+
}
7077
}
7178

7279
pub trait AsName {

0 commit comments

Comments
 (0)