diff --git a/src/base.rs b/src/base.rs index 9f9d717..4b562d9 100644 --- a/src/base.rs +++ b/src/base.rs @@ -16,6 +16,10 @@ pub struct SourceRootId(pub u32); pub struct VfsPath(String); impl VfsPath { + pub fn root() -> Self { + Self(String::new()) + } + pub fn from_path(p: &Path) -> Option { Self::new(p.to_str()?) } @@ -23,7 +27,7 @@ impl VfsPath { pub fn new(s: impl Into) -> Option { let mut s: String = s.into(); if s.is_empty() || s == "/" { - return Some(Self(String::new())); + return Some(Self::root()); } if s.ends_with('/') || s.as_bytes().windows(2).any(|w| w == b"//") { return None; @@ -38,6 +42,16 @@ impl VfsPath { self.0.push_str(&relative.0); } + pub fn push_segment(&mut self, segment: &str) -> Option<()> { + if !segment.contains('/') { + self.0 += "/"; + self.0 += segment; + Some(()) + } else { + None + } + } + pub fn pop(&mut self) -> Option<()> { self.0.truncate(self.0.rsplit_once('/')?.0.len()); Some(()) diff --git a/src/def/lower.rs b/src/def/lower.rs index c37d138..19438a6 100644 --- a/src/def/lower.rs +++ b/src/def/lower.rs @@ -698,37 +698,37 @@ mod tests { check_path( "./.", expect![[r#" - PathData { anchor: Relative(FileId(0)), supers: 0, raw_segments: "" } + PathData { anchor: Relative(FileId(0)), supers: 0, relative: VfsPath("") } "#]], ); check_path( "../.", expect![[r#" - PathData { anchor: Relative(FileId(0)), supers: 1, raw_segments: "" } + PathData { anchor: Relative(FileId(0)), supers: 1, relative: VfsPath("") } "#]], ); check_path( "../a/../../.b/./c", expect![[r#" - PathData { anchor: Relative(FileId(0)), supers: 2, raw_segments: ".b/c" } + PathData { anchor: Relative(FileId(0)), supers: 2, relative: VfsPath("/.b/c") } "#]], ); check_path( "/../a/../../.b/./c", expect![[r#" - PathData { anchor: Absolute, supers: 0, raw_segments: ".b/c" } + PathData { anchor: Absolute, supers: 0, relative: VfsPath("/.b/c") } "#]], ); check_path( "~/../a/../../.b/./c", expect![[r#" - PathData { anchor: Home, supers: 2, raw_segments: ".b/c" } + PathData { anchor: Home, supers: 2, relative: VfsPath("/.b/c") } "#]], ); check_path( "

", expect![[r#" - PathData { anchor: Search("p"), supers: 2, raw_segments: ".b/c" } + PathData { anchor: Search("p"), supers: 2, relative: VfsPath("/.b/c") } "#]], ); } diff --git a/src/def/mod.rs b/src/def/mod.rs index 7bca433..873540f 100644 --- a/src/def/mod.rs +++ b/src/def/mod.rs @@ -37,6 +37,9 @@ pub trait DefDatabase: SourceDatabase { #[salsa::invoke(Module::module_paths_query)] fn module_paths(&self, file_id: FileId) -> Arc<[Path]>; + #[salsa::invoke(Path::resolve_path_query)] + fn resolve_path(&self, path: Path) -> Option; + #[salsa::invoke(ModuleScopes::module_scopes_query)] fn scopes(&self, file_id: FileId) -> Arc; diff --git a/src/def/path.rs b/src/def/path.rs index 3cc4464..8815629 100644 --- a/src/def/path.rs +++ b/src/def/path.rs @@ -1,5 +1,5 @@ use super::DefDatabase; -use crate::FileId; +use crate::{FileId, VfsPath}; use smol_str::SmolStr; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -15,60 +15,64 @@ impl salsa::InternKey for Path { } impl Path { + pub(crate) fn resolve_path_query(db: &dyn DefDatabase, path: Path) -> Option { + let data = path.data(db); + let file = match &data.anchor { + &PathAnchor::Relative(file) => file, + // TODO + PathAnchor::Absolute | PathAnchor::Home | PathAnchor::Search(_) => return None, + }; + let sid = db.file_source_root(file); + let root = db.source_root(sid); + let mut vpath = root.get_path_for_file(file)?.clone(); + for _ in 0..(data.supers.saturating_add(1)) { + vpath.pop()?; + } + vpath.push(&data.relative); + root.get_file_for_path(&vpath).or_else(|| { + vpath.push_segment("default.nix").unwrap(); + root.get_file_for_path(&vpath) + }) + } + pub fn data(self, db: &dyn DefDatabase) -> PathData { db.lookup_intern_path(self) } + + pub fn resolve(self, db: &dyn DefDatabase) -> Option { + db.resolve_path(self) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PathData { anchor: PathAnchor, supers: u8, - // Normalized path separated by `/`, with no `.` or `..` segments. - raw_segments: SmolStr, + // Normalized relative path. + relative: VfsPath, } impl PathData { pub(crate) fn normalize(anchor: PathAnchor, segments: &str) -> Self { - let mut raw_segments = String::new(); + let mut relative = VfsPath::root(); let mut supers = 0u8; for seg in segments .split('/') .filter(|&seg| !seg.is_empty() && seg != ".") { if seg != ".." { - if !raw_segments.is_empty() { - raw_segments.push('/'); - } - raw_segments.push_str(seg); - } else if raw_segments.is_empty() { - // Extra ".." has no effect for absolute path. - if anchor != PathAnchor::Absolute { - supers = supers.saturating_add(1); - } - } else { - let last_slash = raw_segments.bytes().rposition(|c| c == b'/').unwrap_or(0); - raw_segments.truncate(last_slash); + relative.push_segment(seg).expect("Checked by lexer"); + // Extra ".." has no effect for absolute path. + } else if relative.pop().is_none() && anchor != PathAnchor::Absolute { + supers = supers.saturating_add(1); } } Self { anchor, supers, - raw_segments: raw_segments.into(), + relative, } } - - pub fn anchor(&self) -> &PathAnchor { - &self.anchor - } - - pub fn supers(&self) -> u8 { - self.supers - } - - pub fn segments(&self) -> impl Iterator + '_ { - self.raw_segments.split(' ').filter(|s| !s.is_empty()) - } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -82,19 +86,21 @@ pub enum PathAnchor { #[cfg(test)] mod tests { use super::{PathAnchor, PathData}; - use crate::FileId; + use crate::tests::TestDB; + use crate::{DefDatabase, FileId, VfsPath}; #[test] #[rustfmt::skip] fn normalize_relative() { for anchor in [PathAnchor::Relative(FileId(0)), PathAnchor::Home, PathAnchor::Search("foo".into())] { let norm = |s| PathData::normalize(anchor.clone(), s); - assert_eq!(norm(""), PathData { anchor: anchor.clone(), supers: 0, raw_segments: "".into() }); - assert_eq!(norm("./."), PathData { anchor: anchor.clone(), supers: 0, raw_segments: "".into() }); - assert_eq!(norm("./.."), PathData { anchor: anchor.clone(), supers: 1, raw_segments: "".into() }); - assert_eq!(norm("../."), PathData { anchor: anchor.clone(), supers: 1, raw_segments: "".into() }); - assert_eq!(norm("foo/./bar/../.baz"), PathData { anchor: anchor.clone(), supers: 0, raw_segments: "foo/.baz".into() }); - assert_eq!(norm("../../foo"), PathData { anchor: anchor.clone(), supers: 2, raw_segments: "foo".into() }); + let path = |supers, p: &str| PathData { anchor: anchor.clone(), supers, relative: VfsPath::new(p).unwrap() }; + assert_eq!(norm(""), path(0, "")); + assert_eq!(norm("./."), path(0, "")); + assert_eq!(norm("./.."), path(1, "")); + assert_eq!(norm("../."), path(1, "")); + assert_eq!(norm("foo/./bar/../.baz"), path(0, "foo/.baz")); + assert_eq!(norm("../../foo"), path(2, "foo")); } } @@ -103,10 +109,32 @@ mod tests { fn normalize_absolute() { let anchor = PathAnchor::Absolute; let norm = |s| PathData::normalize(anchor.clone(), s); - assert_eq!(norm("/./."), PathData { anchor: anchor.clone(), supers: 0, raw_segments: "".into() }); - assert_eq!(norm("/./.."), PathData { anchor: anchor.clone(), supers: 0, raw_segments: "".into() }); - assert_eq!(norm("/../."), PathData { anchor: anchor.clone(), supers: 0, raw_segments: "".into() }); - assert_eq!(norm("/foo/./bar/../.baz"), PathData { anchor: anchor.clone(), supers: 0, raw_segments: "foo/.baz".into() }); - assert_eq!(norm("/../../foo"), PathData { anchor: anchor.clone(), supers: 0, raw_segments: "foo".into() }); + let path = |p: &str| PathData { anchor: anchor.clone(), supers: 0, relative: VfsPath::new(p).unwrap() }; + assert_eq!(norm("/"), path("")); + assert_eq!(norm("./."), path("")); + assert_eq!(norm("./.."), path("")); + assert_eq!(norm("../."), path("")); + assert_eq!(norm("foo/./bar/../.baz"), path("foo/.baz")); + assert_eq!(norm("../../foo"), path("foo")); + } + + #[test] + fn resolve_path() { + let (db, files) = TestDB::file_set( + " +#- /default.nix +./foo/bar.nix + +#- /foo/bar.nix +baz/../../bar.nix + +#- bar.nix +./. + ", + ) + .unwrap(); + assert_eq!(db.module_paths(files[0])[0].resolve(&db), Some(files[1])); + assert_eq!(db.module_paths(files[1])[0].resolve(&db), Some(files[2])); + assert_eq!(db.module_paths(files[2])[0].resolve(&db), Some(files[0])); } } diff --git a/src/ide/goto_definition.rs b/src/ide/goto_definition.rs index 0543cfd..d39135e 100644 --- a/src/ide/goto_definition.rs +++ b/src/ide/goto_definition.rs @@ -1,8 +1,8 @@ use super::NavigationTarget; -use crate::def::{AstPtr, ResolveResult}; +use crate::def::{AstPtr, Expr, Literal, ResolveResult}; use crate::{DefDatabase, FileId}; use rowan::ast::AstNode; -use rowan::TextSize; +use rowan::{TextRange, TextSize}; use syntax::{ast, match_ast, SyntaxKind, T}; pub(crate) fn goto_definition( @@ -12,7 +12,7 @@ pub(crate) fn goto_definition( ) -> Option> { let parse = db.parse(file_id); let tok = parse.syntax_node().token_at_offset(pos).right_biased()?; - if !matches!(tok.kind(), T![or] | SyntaxKind::IDENT) { + if !matches!(tok.kind(), T![or] | SyntaxKind::IDENT | SyntaxKind::PATH) { return None; } let ptr = tok.parent_ancestors().find_map(|node| { @@ -20,6 +20,7 @@ pub(crate) fn goto_definition( match node { ast::Ref(n) => Some(AstPtr::new(n.syntax())), ast::Name(n) => Some(AstPtr::new(n.syntax())), + ast::Literal(n) => Some(AstPtr::new(n.syntax())), _ => None, } } @@ -28,6 +29,22 @@ pub(crate) fn goto_definition( let source_map = db.source_map(file_id); let expr_id = source_map.node_expr(ptr)?; + if tok.kind() == SyntaxKind::PATH { + let module = db.module(file_id); + let path = match &module[expr_id] { + Expr::Literal(Literal::Path(path)) => path, + _ => return None, + }; + let file_id = path.resolve(db)?; + let full_range = TextRange::up_to(TextSize::of(&*db.file_content(file_id))); + let focus_range = TextRange::default(); + return Some(vec![NavigationTarget { + file_id, + focus_range, + full_range, + }]); + } + match db.resolve_name(file_id, expr_id)? { ResolveResult::NameDef(def) => { let name_node = source_map.name_def_node(def)?.to_node(&parse.syntax_node()); @@ -157,4 +174,10 @@ mod tests { check("let true = 1; in $0true && false", expect![" = 1;"]); check("let true = 1; in true && $0false", expect![""]); } + + // TODO: Multi-files test. + #[test] + fn path() { + check("1 + $0./.", expect!["<>1 + ./."]); + } } diff --git a/src/tests.rs b/src/tests.rs index 557d44d..5dae14b 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,9 +1,10 @@ use crate::base::SourceDatabaseStorage; use crate::def::DefDatabaseStorage; -use crate::{Change, DefDatabase, FileId}; +use crate::{Change, DefDatabase, FileId, FileSet, SourceRoot, VfsPath}; +use indexmap::IndexMap; use rowan::ast::AstNode; use rowan::TextSize; -use std::ops; +use std::{mem, ops}; use syntax::{NixLanguage, SyntaxNode}; pub const MARKER_INDICATOR: char = '$'; @@ -25,11 +26,25 @@ impl TestDB { Ok((db, file, poses)) } + pub fn file_set(fixture: &str) -> Result<(Self, Vec), String> { + let f = Fixture::file_set(fixture)?; + let files = (0..f.files.len()) + .map(|i| FileId(i as u32)) + .collect::>(); + let db = Self::from_fixture(f); + Ok((db, files)) + } + fn from_fixture(fixture: Fixture) -> Self { let mut db = Self::default(); - let root = FileId(0); let mut change = Change::new(); - change.change_file(root, Some(fixture[root].into())); + let mut file_set = FileSet::default(); + for (i, (path, text)) in (0u32..).zip(fixture.files) { + let file = FileId(i); + file_set.insert(file, path); + change.change_file(file, Some(text.into())); + } + change.set_roots(vec![SourceRoot::new_local(file_set)]); change.apply(&mut db); db } @@ -57,19 +72,27 @@ impl TestDB { } } -#[derive(Debug)] +#[derive(Default, Debug)] struct Fixture { - texts: Vec, + files: IndexMap, } impl ops::Index for Fixture { type Output = str; fn index(&self, index: FileId) -> &Self::Output { - &self.texts[index.0 as usize] + &self.files[index.0 as usize] } } impl Fixture { + fn insert_file(&mut self, path: VfsPath, text: String) -> Result<(), String> { + if self.files.insert(path, text).is_none() { + Ok(()) + } else { + Err("Duplicated path".into()) + } + } + fn single_file( fixture: &str, ) -> Result<(Self, FileId, [TextSize; MARKERS]), String> { @@ -101,6 +124,37 @@ impl Fixture { return Err(format!("Marker {} not set", i)); } } - Ok((Self { texts: vec![text] }, FileId(0), markers)) + let mut this = Self::default(); + this.insert_file(VfsPath::new("/default.nix").unwrap(), text)?; + Ok((this, FileId(0), markers)) + } + + fn file_set(fixture: &str) -> Result { + if fixture.len() >= u32::MAX as usize { + return Err("Size too large".into()); + } + + let mut this = Self::default(); + let mut cur_path = None; + let mut cur_text = String::new(); + for line in fixture.lines().skip_while(|line| line.is_empty()) { + if let Some(path) = line.strip_prefix("#- ") { + let path = VfsPath::new(path).ok_or_else(|| "Invalid path".to_owned())?; + if let Some(prev_path) = cur_path.replace(path) { + this.insert_file(prev_path, mem::take(&mut cur_text))?; + } + } else { + if cur_path.is_none() { + return Err("No path specified".into()); + } + cur_text += line; + cur_text += "\n"; + } + } + this.insert_file( + cur_path.ok_or_else(|| "Empty fixture".to_owned())?, + cur_text, + )?; + Ok(this) } }