1
1
mirror of https://github.com/oxalica/nil.git synced 2024-10-27 12:30:52 +03:00

Use VfsPath in Path and impl path resolution

This commit is contained in:
oxalica 2022-08-10 01:49:21 +08:00
parent cbfb8e522c
commit 2d0b9b0cf5
6 changed files with 181 additions and 59 deletions

View File

@ -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> {
Self::new(p.to_str()?)
}
@ -23,7 +27,7 @@ impl VfsPath {
pub fn new(s: impl Into<String>) -> Option<Self> {
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(())

View File

@ -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(
"<p/../a/../../.b/./c>",
expect![[r#"
PathData { anchor: Search("p"), supers: 2, raw_segments: ".b/c" }
PathData { anchor: Search("p"), supers: 2, relative: VfsPath("/.b/c") }
"#]],
);
}

View File

@ -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<FileId>;
#[salsa::invoke(ModuleScopes::module_scopes_query)]
fn scopes(&self, file_id: FileId) -> Arc<ModuleScopes>;

View File

@ -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<FileId> {
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<FileId> {
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<Item = &str> + '_ {
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]));
}
}

View File

@ -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<Vec<NavigationTarget>> {
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!["<true> = 1;"]);
check("let true = 1; in true && $0false", expect![""]);
}
// TODO: Multi-files test.
#[test]
fn path() {
check("1 + $0./.", expect!["<>1 + ./."]);
}
}

View File

@ -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<FileId>), String> {
let f = Fixture::file_set(fixture)?;
let files = (0..f.files.len())
.map(|i| FileId(i as u32))
.collect::<Vec<_>>();
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<String>,
files: IndexMap<VfsPath, String>,
}
impl ops::Index<FileId> 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<const MARKERS: usize>(
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<Self, String> {
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)
}
}