~rrc/pbls

94bfd5417f26ee220ae79bb30f32f0f958daa80a — Ryan Roden-Corrent 7 months ago daee276
Handle nested imports.
M Cargo.lock => Cargo.lock +51 -0
@@ 199,6 199,7 @@ dependencies = [
 "toml",
 "tree-sitter",
 "tree-sitter-protobuf",
 "walkdir",
]

[[package]]


@@ 293,6 294,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"

[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
 "winapi-util",
]

[[package]]
name = "serde"
version = "1.0.189"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 469,6 479,47 @@ dependencies = [
]

[[package]]
name = "walkdir"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
 "same-file",
 "winapi-util",
]

[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
 "winapi-i686-pc-windows-gnu",
 "winapi-x86_64-pc-windows-gnu",
]

[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"

[[package]]
name = "winapi-util"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
 "winapi",
]

[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

M Cargo.toml => Cargo.toml +1 -0
@@ 13,6 13,7 @@ tree-sitter-protobuf = { git = "https://github.com/yusdacra/tree-sitter-protobuf
tree-sitter = "~0.20.0"
log = "0.4.20"
env_logger = { version = "0.10.1", default-features = false }
walkdir = "2.4.0"

[dev-dependencies]
pretty_assertions = "1.4.0"

M src/file.rs => src/file.rs +1 -1
@@ 259,7 259,7 @@ impl File {
        );

        log::trace!(
            "Getting completion context for node text:\n{}",
            "Getting completion context for node text: {}",
            self.get_text(node),
        );


M src/lib.rs => src/lib.rs +8 -1
@@ 264,7 264,7 @@ pub fn run(connection: Connection) -> Result<()> {
    } else {
        log::info!("Using default config");
        Config {
            proto_paths: find_import_paths(root)?,
            proto_paths: find_import_paths(root.clone())?,
        }
    };
    log::info!("Using config {:?}", conf);


@@ 272,6 272,13 @@ pub fn run(connection: Connection) -> Result<()> {
    let proto_paths = conf
        .proto_paths
        .iter()
        .map(|path| {
            if path.is_relative() {
                root.join(path)
            } else {
                path.clone()
            }
        })
        .filter_map(|p| match p.canonicalize() {
            Ok(path) => Some(path),
            Err(err) => {

M src/workspace.rs => src/workspace.rs +128 -36
@@ 197,31 197,7 @@ impl Workspace {
        }
    }

    // Return all available imports for a given file.
    // Excludes the file itself and any files already imported.
    pub fn available_imports<'a>(
        &'a self,
        uri: &'a Url,
    ) -> Result<impl Iterator<Item = String> + 'a> {
        let name = std::path::Path::new(uri.path())
            .file_name()
            .ok_or("Invalid path: {uri}")?;
        let file = self.files.get(uri).ok_or("File not loaded: {uri}")?;
        let mut qc = tree_sitter::QueryCursor::new();
        let imports = file.imports(&mut qc).collect::<Vec<_>>();
        Ok(self
            .proto_paths
            .iter()
            .filter_map(|dir| std::fs::read_dir(dir).ok())
            .flatten()
            .filter_map(|entry| entry.ok())
            .filter(|entry| entry.metadata().is_ok_and(|m| m.is_file()))
            .map(|entry| entry.file_name())
            .filter(move |fname| fname != name)
            .filter_map(|fname| fname.into_string().ok())
            .filter(|fname| fname.ends_with(".proto"))
            .filter(move |fname| !imports.contains(&fname.as_str())))
    }
    // Return the relative paths of proto files under the given dir.

    pub fn goto(&self, uri: Url, pos: lsp_types::Position) -> Result<Option<lsp_types::Location>> {
        let file = self.get(&uri)?;


@@ 413,17 389,102 @@ impl Workspace {
        &self,
        url: &lsp_types::Url,
    ) -> Result<Option<lsp_types::CompletionResponse>> {
        log::debug!("Completing imports for {url:?}");

        let current = std::path::Path::new(url.path())
            .file_name()
            .ok_or("Invalid path: {uri}")?
            .to_str()
            .ok_or("Invalid path: {uri}")?;

        let file = self.files.get(url).ok_or("File not loaded: {uri}")?;
        let mut qc = tree_sitter::QueryCursor::new();
        let existing = file
            .imports(&mut qc)
            .chain(std::iter::once(current))
            .collect::<Vec<_>>();

        log::trace!("Excluding existing imports: {existing:?}");

        let items = self
            .available_imports(&url)?
            .map(|s| lsp_types::CompletionItem {
                label: s.clone(),
                label_details: None,
                kind: Some(lsp_types::CompletionItemKind::FILE),
                insert_text: Some(format!("{}\";", s)),
                ..Default::default()
            });
        Ok(Some(lsp_types::CompletionResponse::Array(items.collect())))
            .proto_paths
            .iter()
            .map(|p| find_protos(p.as_path(), &existing))
            .flat_map(|p| {
                p.iter()
                    .map(|s| lsp_types::CompletionItem {
                        insert_text: Some(format!("{}\";", s)),
                        label: s.to_owned(),
                        label_details: None,
                        kind: Some(lsp_types::CompletionItemKind::FILE),
                        ..Default::default()
                    })
                    .collect::<Vec<_>>()
            })
            .collect();
        Ok(Some(lsp_types::CompletionResponse::Array(items)))
    }
}

fn find_protos(dir: &std::path::Path, excludes: &Vec<&str>) -> Vec<String> {
    let mut res = vec![];
    let entries = match std::fs::read_dir(dir) {
        Ok(ok) => ok,
        Err(err) => {
            log::warn!("Failed to read dir {dir:?}: {err:?}");
            return res;
        }
    };
    log::trace!("Finding imports under {dir:?}");
    for path in entries {
        let path = match path {
            Ok(ok) => ok,
            Err(err) => {
                log::warn!("Failed to read dir {dir:?}: {err:?}");
                continue;
            }
        };

        let meta = match path.metadata() {
            Ok(ok) => ok,
            Err(err) => {
                log::warn!("Failed to read dir {dir:?}: {err:?}");
                continue;
            }
        };

        if meta.is_dir() {
            let dir = dir.join(path.path());
            let protos = find_protos(dir.as_path(), excludes);
            let root = &path.file_name();
            let root = std::path::PathBuf::from(root);
            res.extend(
                protos
                    .iter()
                    .filter_map(|p| root.join(p).to_str().map(str::to_string)),
            );
            continue;
        }

        if !meta.is_file() {
            continue;
        }

        let name = &path.file_name();
        let Some(name) = name.to_str() else {
            continue;
        };

        if !name.ends_with(".proto") {
            continue;
        }

        if !excludes.contains(&name) {
            log::trace!("Found import {name:?}");
            res.push(name.to_string())
        }
    }
    res
}

fn complete_keywords() -> Option<lsp_types::CompletionResponse> {


@@ 495,8 556,8 @@ mod tests {
        (Workspace::new(vec![tmp.path().into()]), tmp)
    }

    fn proto(tmp: &tempfile::TempDir, path: &str, lines: &[&str]) -> (Url, String) {
        let path = tmp.path().join(path);
    fn proto(dir: impl AsRef<std::path::Path>, path: &str, lines: &[&str]) -> (Url, String) {
        let path = dir.as_ref().join(path);
        let text = lines.join("\n") + "\n";
        std::fs::write(&path, &text).unwrap();
        (Url::from_file_path(path).unwrap(), text)


@@ 568,6 629,37 @@ mod tests {
    }

    #[test]
    fn test_complete_nested_import() {
        let (mut ws, tmp) = setup();
        let (uri, text) = proto(&tmp, "foo.proto", &["syntax = \"proto3\";", "import \""]);
        proto(&tmp, "bar.proto", &["syntax = \"proto3\";"]);

        let subdir = tmp.path().join("subdir");
        let subdir = subdir.as_path();
        std::fs::create_dir(subdir).unwrap();
        proto(subdir, "baz.proto", &["syntax = \"proto3\";"]);

        ws.open(uri.clone(), text).unwrap();
        assert_eq!(
            ws.complete(&uri, 1, "import \"".len()).unwrap().unwrap(),
            lsp_types::CompletionResponse::Array(vec![
                lsp_types::CompletionItem {
                    label: "bar.proto".into(),
                    kind: Some(lsp_types::CompletionItemKind::FILE),
                    insert_text: Some("bar.proto\";".into()),
                    ..Default::default()
                },
                lsp_types::CompletionItem {
                    label: "subdir/baz.proto".into(),
                    kind: Some(lsp_types::CompletionItemKind::FILE),
                    insert_text: Some("subdir/baz.proto\";".into()),
                    ..Default::default()
                },
            ])
        );
    }

    #[test]
    fn test_complete_options() {
        let (mut ws, tmp) = setup();
        let (uri, text) = proto(

A testdata/folder/stuff.proto => testdata/folder/stuff.proto +5 -0
@@ 0,0 1,5 @@
syntax = "proto3";

package folder.stuff;

message Stuff{}

M testdata/simple.proto => testdata/simple.proto +2 -0
@@ 4,6 4,7 @@ package main;

import "dep.proto";
import "other.proto";
import "folder/stuff.proto";

enum Thing {
  THING_FOO = 0;


@@ 25,6 26,7 @@ message Bar {
  other.Other other = 2;
  Foo.Buz buz = 3;
  other.Other.Nested other_nested = 4;
  folder.stuff.Stuff stuff = 5;
}

message Empty {}

M tests/integration_test.rs => tests/integration_test.rs +23 -7
@@ 32,6 32,10 @@ fn dep_uri() -> Url {
    Url::from_file_path(std::fs::canonicalize("./testdata/dep.proto").unwrap()).unwrap()
}

fn stuff_uri() -> Url {
    Url::from_file_path(std::fs::canonicalize("./testdata/folder/stuff.proto").unwrap()).unwrap()
}

fn error_uri() -> Url {
    Url::from_file_path(std::fs::canonicalize("./testdata/error.proto").unwrap()).unwrap()
}


@@ 210,6 214,7 @@ struct TestClient {

impl TestClient {
    fn new() -> Result<TestClient> {
        let _ = env_logger::builder().is_test(true).try_init();
        Self::new_with_root("testdata")
    }



@@ 507,6 512,7 @@ fn test_workspace_symbols() -> pbls::Result<()> {
        sym(error_uri(), "Nope", "enum Nope"),
        sym(error_uri(), "Nah", "message Nah"),
        sym(error_uri(), "Noo", "message Noo"),
        // sym(stuff_uri(), "Stuff", "message Stuff"), BUG: should find nested symbols
    ];
    assert_elements_equal(actual, expected, |s| s.name.clone());
    Ok(())


@@ 643,7 649,7 @@ fn test_complete_import() -> pbls::Result<()> {
    let mut client = TestClient::new()?;
    client.open(base_uri())?;

    let loc = locate_sym(base_uri(), "import \"other.proto\"");
    let loc = locate_sym(base_uri(), "import \"folder");
    let pos = lsp_types::Position {
        line: loc.range.start.line + 1,
        character: 0,


@@ 679,12 685,21 @@ fn test_complete_import() -> pbls::Result<()> {
    // excludes dep.proto (already imported)
    assert_elements_equal(
        actual,
        vec![CompletionItem {
            label: "error.proto".into(),
            kind: Some(CompletionItemKind::FILE),
            insert_text: Some("error.proto\";".into()),
            ..Default::default()
        }],
        vec![
            CompletionItem {
                label: "error.proto".into(),
                kind: Some(CompletionItemKind::FILE),
                insert_text: Some("error.proto\";".into()),
                ..Default::default()
            },
            // BUG: Should be excluded
            CompletionItem {
                label: "folder/stuff.proto".into(),
                kind: Some(CompletionItemKind::FILE),
                insert_text: Some("folder/stuff.proto\";".into()),
                ..Default::default()
            },
        ],
        |s| s.label.clone(),
    );



@@ 821,6 836,7 @@ fn test_complete_type() -> pbls::Result<()> {
            _enum("Dep2"),
            _struct("other.Other"),
            _struct("other.Other.Nested"),
            _struct("folder.Stuff"), // BUG: should be folder.stuff.Stuff
        ],
        |s| s.label.clone(),
    );