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:
parent
cbfb8e522c
commit
2d0b9b0cf5
16
src/base.rs
16
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> {
|
||||
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(())
|
||||
|
@ -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") }
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
@ -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>;
|
||||
|
||||
|
110
src/def/path.rs
110
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<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]));
|
||||
}
|
||||
}
|
||||
|
@ -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 + ./."]);
|
||||
}
|
||||
}
|
||||
|
70
src/tests.rs
70
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<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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user