Skip to content

Commit fb39efe

Browse files
committed
Auto merge of rust-lang#13934 - Veykril:unlinked-file-inline-modules, r=Veykril
feat: Make unlinked_file diagnostic quickfixes work for inline modules Finally got myself to fix this, bothered me quite a bit that this never worked ![Code_Qe3WlMvt5Q](https://user-images.githubusercontent.com/3757771/211927799-023e48ee-7cdd-4dd7-8e25-a23eddc7d897.gif) (Just gotta fix up the indentation still)
2 parents 80e616e + 14777ce commit fb39efe

File tree

4 files changed

+204
-52
lines changed

4 files changed

+204
-52
lines changed

crates/hir-expand/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,14 @@ impl HirFileId {
356356
}
357357
}
358358

359+
#[inline]
360+
pub fn file_id(self) -> Option<FileId> {
361+
match self.0 & Self::MACRO_FILE_TAG_MASK {
362+
0 => Some(FileId(self.0)),
363+
_ => None,
364+
}
365+
}
366+
359367
fn repr(self) -> HirFileIdRepr {
360368
match self.0 & Self::MACRO_FILE_TAG_MASK {
361369
0 => HirFileIdRepr::FileId(FileId(self.0)),

crates/ide-diagnostics/src/handlers/unlinked_file.rs

Lines changed: 179 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
//! Diagnostic emitted for files that aren't part of any crate.
22
3-
use hir::db::DefDatabase;
3+
use std::iter;
4+
5+
use hir::{db::DefDatabase, InFile, ModuleSource};
46
use ide_db::{
57
base_db::{FileId, FileLoader, SourceDatabase, SourceDatabaseExt},
68
source_change::SourceChange,
79
RootDatabase,
810
};
911
use syntax::{
10-
ast::{self, HasModuleItem, HasName},
12+
ast::{self, edit::IndentLevel, HasModuleItem, HasName},
1113
AstNode, TextRange, TextSize,
1214
};
1315
use text_edit::TextEdit;
@@ -42,47 +44,99 @@ fn fixes(ctx: &DiagnosticsContext<'_>, file_id: FileId) -> Option<Vec<Assist>> {
4244

4345
let source_root = ctx.sema.db.source_root(ctx.sema.db.file_source_root(file_id));
4446
let our_path = source_root.path_for_file(&file_id)?;
45-
let (mut module_name, _) = our_path.name_and_extension()?;
46-
47-
// Candidates to look for:
48-
// - `mod.rs`, `main.rs` and `lib.rs` in the same folder
49-
// - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id`
5047
let parent = our_path.parent()?;
51-
let paths = {
52-
let parent = if module_name == "mod" {
53-
// for mod.rs we need to actually look up one higher
54-
// and take the parent as our to be module name
55-
let (name, _) = parent.name_and_extension()?;
56-
module_name = name;
57-
parent.parent()?
58-
} else {
59-
parent
60-
};
61-
let mut paths =
62-
vec![parent.join("mod.rs")?, parent.join("lib.rs")?, parent.join("main.rs")?];
63-
64-
// `submod/bla.rs` -> `submod.rs`
65-
let parent_mod = (|| {
48+
let (module_name, _) = our_path.name_and_extension()?;
49+
let (parent, module_name) = match module_name {
50+
// for mod.rs we need to actually look up one higher
51+
// and take the parent as our to be module name
52+
"mod" => {
6653
let (name, _) = parent.name_and_extension()?;
67-
parent.parent()?.join(&format!("{name}.rs"))
68-
})();
69-
paths.extend(parent_mod);
70-
paths
54+
(parent.parent()?, name.to_owned())
55+
}
56+
_ => (parent, module_name.to_owned()),
7157
};
7258

73-
for &parent_id in paths.iter().filter_map(|path| source_root.file_for_path(path)) {
74-
for &krate in ctx.sema.db.relevant_crates(parent_id).iter() {
75-
let crate_def_map = ctx.sema.db.crate_def_map(krate);
76-
for (_, module) in crate_def_map.modules() {
77-
if module.origin.is_inline() {
78-
// We don't handle inline `mod parent {}`s, they use different paths.
79-
continue;
80-
}
59+
// check crate roots, i.e. main.rs, lib.rs, ...
60+
'crates: for &krate in &*ctx.sema.db.relevant_crates(file_id) {
61+
let crate_def_map = ctx.sema.db.crate_def_map(krate);
62+
63+
let root_module = &crate_def_map[crate_def_map.root()];
64+
let Some(root_file_id) = root_module.origin.file_id() else { continue };
65+
let Some(crate_root_path) = source_root.path_for_file(&root_file_id) else { continue };
66+
let Some(rel) = parent.strip_prefix(&crate_root_path.parent()?) else { continue };
67+
68+
// try resolving the relative difference of the paths as inline modules
69+
let mut current = root_module;
70+
for ele in rel.as_ref().components() {
71+
let seg = match ele {
72+
std::path::Component::Normal(seg) => seg.to_str()?,
73+
std::path::Component::RootDir => continue,
74+
// shouldn't occur
75+
_ => continue 'crates,
76+
};
77+
match current.children.iter().find(|(name, _)| name.to_smol_str() == seg) {
78+
Some((_, &child)) => current = &crate_def_map[child],
79+
None => continue 'crates,
80+
}
81+
if !current.origin.is_inline() {
82+
continue 'crates;
83+
}
84+
}
85+
86+
let InFile { file_id: parent_file_id, value: source } =
87+
current.definition_source(ctx.sema.db);
88+
let parent_file_id = parent_file_id.file_id()?;
89+
return make_fixes(ctx.sema.db, parent_file_id, source, &module_name, file_id);
90+
}
8191

82-
if module.origin.file_id() == Some(parent_id) {
83-
return make_fixes(ctx.sema.db, parent_id, module_name, file_id);
92+
// if we aren't adding to a crate root, walk backwards such that we support `#[path = ...]` overrides if possible
93+
94+
// build all parent paths of the form `../module_name/mod.rs` and `../module_name.rs`
95+
let paths = iter::successors(Some(parent.clone()), |prev| prev.parent()).filter_map(|path| {
96+
let parent = path.parent()?;
97+
let (name, _) = path.name_and_extension()?;
98+
Some(([parent.join(&format!("{name}.rs"))?, path.join("mod.rs")?], name.to_owned()))
99+
});
100+
let mut stack = vec![];
101+
let &parent_id =
102+
paths.inspect(|(_, name)| stack.push(name.clone())).find_map(|(paths, _)| {
103+
paths.into_iter().find_map(|path| source_root.file_for_path(&path))
104+
})?;
105+
stack.pop();
106+
'crates: for &krate in ctx.sema.db.relevant_crates(parent_id).iter() {
107+
let crate_def_map = ctx.sema.db.crate_def_map(krate);
108+
let Some((_, module)) =
109+
crate_def_map.modules()
110+
.find(|(_, module)| module.origin.file_id() == Some(parent_id) && !module.origin.is_inline())
111+
else { continue };
112+
113+
if stack.is_empty() {
114+
return make_fixes(
115+
ctx.sema.db,
116+
parent_id,
117+
module.definition_source(ctx.sema.db).value,
118+
&module_name,
119+
file_id,
120+
);
121+
} else {
122+
// direct parent file is missing,
123+
// try finding a parent that has an inline tree from here on
124+
let mut current = module;
125+
for s in stack.iter().rev() {
126+
match module.children.iter().find(|(name, _)| name.to_smol_str() == s) {
127+
Some((_, child)) => {
128+
current = &crate_def_map[*child];
129+
}
130+
None => continue 'crates,
131+
}
132+
if !current.origin.is_inline() {
133+
continue 'crates;
84134
}
85135
}
136+
let InFile { file_id: parent_file_id, value: source } =
137+
current.definition_source(ctx.sema.db);
138+
let parent_file_id = parent_file_id.file_id()?;
139+
return make_fixes(ctx.sema.db, parent_file_id, source, &module_name, file_id);
86140
}
87141
}
88142

@@ -92,6 +146,7 @@ fn fixes(ctx: &DiagnosticsContext<'_>, file_id: FileId) -> Option<Vec<Assist>> {
92146
fn make_fixes(
93147
db: &RootDatabase,
94148
parent_file_id: FileId,
149+
source: ModuleSource,
95150
new_mod_name: &str,
96151
added_file_id: FileId,
97152
) -> Option<Vec<Assist>> {
@@ -102,14 +157,18 @@ fn make_fixes(
102157
let mod_decl = format!("mod {new_mod_name};");
103158
let pub_mod_decl = format!("pub mod {new_mod_name};");
104159

105-
let ast: ast::SourceFile = db.parse(parent_file_id).tree();
106-
107160
let mut mod_decl_builder = TextEdit::builder();
108161
let mut pub_mod_decl_builder = TextEdit::builder();
109162

163+
let mut items = match &source {
164+
ModuleSource::SourceFile(it) => it.items(),
165+
ModuleSource::Module(it) => it.item_list()?.items(),
166+
ModuleSource::BlockExpr(_) => return None,
167+
};
168+
110169
// If there's an existing `mod m;` statement matching the new one, don't emit a fix (it's
111170
// probably `#[cfg]`d out).
112-
for item in ast.items() {
171+
for item in items.clone() {
113172
if let ast::Item::Module(m) = item {
114173
if let Some(name) = m.name() {
115174
if m.item_list().is_none() && name.to_string() == new_mod_name {
@@ -121,28 +180,40 @@ fn make_fixes(
121180
}
122181

123182
// If there are existing `mod m;` items, append after them (after the first group of them, rather).
124-
match ast.items().skip_while(|item| !is_outline_mod(item)).take_while(is_outline_mod).last() {
183+
match items.clone().skip_while(|item| !is_outline_mod(item)).take_while(is_outline_mod).last() {
125184
Some(last) => {
126185
cov_mark::hit!(unlinked_file_append_to_existing_mods);
127186
let offset = last.syntax().text_range().end();
128-
mod_decl_builder.insert(offset, format!("\n{mod_decl}"));
129-
pub_mod_decl_builder.insert(offset, format!("\n{pub_mod_decl}"));
187+
let indent = IndentLevel::from_node(last.syntax());
188+
mod_decl_builder.insert(offset, format!("\n{indent}{mod_decl}"));
189+
pub_mod_decl_builder.insert(offset, format!("\n{indent}{pub_mod_decl}"));
130190
}
131191
None => {
132192
// Prepend before the first item in the file.
133-
match ast.items().next() {
134-
Some(item) => {
193+
match items.next() {
194+
Some(first) => {
135195
cov_mark::hit!(unlinked_file_prepend_before_first_item);
136-
let offset = item.syntax().text_range().start();
137-
mod_decl_builder.insert(offset, format!("{mod_decl}\n\n"));
138-
pub_mod_decl_builder.insert(offset, format!("{pub_mod_decl}\n\n"));
196+
let offset = first.syntax().text_range().start();
197+
let indent = IndentLevel::from_node(first.syntax());
198+
mod_decl_builder.insert(offset, format!("{mod_decl}\n\n{indent}"));
199+
pub_mod_decl_builder.insert(offset, format!("{pub_mod_decl}\n\n{indent}"));
139200
}
140201
None => {
141202
// No items in the file, so just append at the end.
142203
cov_mark::hit!(unlinked_file_empty_file);
143-
let offset = ast.syntax().text_range().end();
144-
mod_decl_builder.insert(offset, format!("{mod_decl}\n"));
145-
pub_mod_decl_builder.insert(offset, format!("{pub_mod_decl}\n"));
204+
let mut indent = IndentLevel::from(0);
205+
let offset = match &source {
206+
ModuleSource::SourceFile(it) => it.syntax().text_range().end(),
207+
ModuleSource::Module(it) => {
208+
indent = IndentLevel::from_node(it.syntax()) + 1;
209+
it.item_list()?.r_curly_token()?.text_range().start()
210+
}
211+
ModuleSource::BlockExpr(it) => {
212+
it.stmt_list()?.r_curly_token()?.text_range().start()
213+
}
214+
};
215+
mod_decl_builder.insert(offset, format!("{indent}{mod_decl}\n"));
216+
pub_mod_decl_builder.insert(offset, format!("{indent}{pub_mod_decl}\n"));
146217
}
147218
}
148219
}
@@ -167,7 +238,6 @@ fn make_fixes(
167238

168239
#[cfg(test)]
169240
mod tests {
170-
171241
use crate::tests::{check_diagnostics, check_fix, check_fixes, check_no_fix};
172242

173243
#[test]
@@ -330,6 +400,64 @@ $0
330400
mod foo;
331401
332402
//- /foo.rs
403+
"#,
404+
);
405+
}
406+
407+
#[test]
408+
fn unlinked_file_insert_into_inline_simple() {
409+
check_fix(
410+
r#"
411+
//- /main.rs
412+
mod bar;
413+
//- /bar.rs
414+
mod foo {
415+
}
416+
//- /bar/foo/baz.rs
417+
$0
418+
"#,
419+
r#"
420+
mod foo {
421+
mod baz;
422+
}
423+
"#,
424+
);
425+
}
426+
427+
#[test]
428+
fn unlinked_file_insert_into_inline_simple_modrs() {
429+
check_fix(
430+
r#"
431+
//- /main.rs
432+
mod bar;
433+
//- /bar.rs
434+
mod baz {
435+
}
436+
//- /bar/baz/foo/mod.rs
437+
$0
438+
"#,
439+
r#"
440+
mod baz {
441+
mod foo;
442+
}
443+
"#,
444+
);
445+
}
446+
447+
#[test]
448+
fn unlinked_file_insert_into_inline_simple_modrs_main() {
449+
check_fix(
450+
r#"
451+
//- /main.rs
452+
mod bar {
453+
}
454+
//- /bar/foo/mod.rs
455+
$0
456+
"#,
457+
r#"
458+
mod bar {
459+
mod foo;
460+
}
333461
"#,
334462
);
335463
}

crates/ide/src/goto_declaration.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::{
1717
// This is the same as `Go to Definition` with the following exceptions:
1818
// - outline modules will navigate to the `mod name;` item declaration
1919
// - trait assoc items will navigate to the assoc item of the trait declaration opposed to the trait impl
20+
// - fields in patterns will navigate to the field declaration of the struct, union or variant
2021
pub(crate) fn goto_declaration(
2122
db: &RootDatabase,
2223
position: FilePosition,

crates/vfs/src/vfs_path.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Abstract-ish representation of paths for VFS.
22
use std::fmt;
33

4-
use paths::{AbsPath, AbsPathBuf};
4+
use paths::{AbsPath, AbsPathBuf, RelPath};
55

66
/// Path in [`Vfs`].
77
///
@@ -84,6 +84,14 @@ impl VfsPath {
8484
}
8585
}
8686

87+
pub fn strip_prefix(&self, other: &VfsPath) -> Option<&RelPath> {
88+
match (&self.0, &other.0) {
89+
(VfsPathRepr::PathBuf(lhs), VfsPathRepr::PathBuf(rhs)) => lhs.strip_prefix(rhs),
90+
(VfsPathRepr::VirtualPath(lhs), VfsPathRepr::VirtualPath(rhs)) => lhs.strip_prefix(rhs),
91+
(VfsPathRepr::PathBuf(_) | VfsPathRepr::VirtualPath(_), _) => None,
92+
}
93+
}
94+
8795
/// Returns the `VfsPath` without its final component, if there is one.
8896
///
8997
/// Returns [`None`] if the path is a root or prefix.
@@ -320,6 +328,13 @@ impl VirtualPath {
320328
self.0.starts_with(&other.0)
321329
}
322330

331+
fn strip_prefix(&self, base: &VirtualPath) -> Option<&RelPath> {
332+
<_ as AsRef<std::path::Path>>::as_ref(&self.0)
333+
.strip_prefix(&base.0)
334+
.ok()
335+
.map(RelPath::new_unchecked)
336+
}
337+
323338
/// Remove the last component of `self`.
324339
///
325340
/// This will find the last `'/'` in `self`, and remove everything after it,

0 commit comments

Comments
 (0)