diff --git a/Cargo.lock b/Cargo.lock index 4040cd3..64071e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,6 +137,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + [[package]] name = "byteorder" version = "1.4.3" @@ -438,6 +447,19 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "globset" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + [[package]] name = "h2" version = "0.3.13" @@ -790,6 +812,7 @@ dependencies = [ "cfgrammar", "console-subscriber", "glob", + "globset", "log", "lrlex", "lrpar", @@ -805,6 +828,7 @@ dependencies = [ "tokio", "tokio-stream", "toml", + "toml-spanned-value", "tower-lsp", ] @@ -1404,6 +1428,16 @@ dependencies = [ "serde", ] +[[package]] +name = "toml-spanned-value" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a1991312b7a0e66feb44bc16d97861c20ca476a3c6960bf888d25030dcf4d9f" +dependencies = [ + "serde", + "toml", +] + [[package]] name = "tonic" version = "0.7.2" diff --git a/server/Cargo.toml b/server/Cargo.toml index f9e3216..7653251 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -24,6 +24,8 @@ tokio-stream = "0.1.8" ropey = "1.4.1" railroad = "0.1.1" glob = "0.3.0" +toml-spanned-value = "0.1.0" +globset = "0.4.8" [dependencies.console-subscriber] version = "0.1.4" diff --git a/server/src/main.rs b/server/src/main.rs index 211c805..5555237 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -3,7 +3,6 @@ mod peek_channel; use cfgrammar::yacc; use parse_thread::{ParseThread, ParserMsg}; -use serde; use tower_lsp::jsonrpc; use tower_lsp::lsp_types as lsp; @@ -152,6 +151,7 @@ async fn process_parser_messages( }) .await; } + // This should probably be named something like ProgressInterrupt ParserMsg::ProgressCancel(token) => { let token = lsp::NumberOrString::Number(token); @@ -186,32 +186,53 @@ struct State { warned_needs_restart: bool, } +#[derive(serde::Deserialize, serde::Serialize, Debug)] +struct TomlTest(Vec>); + impl State { fn affected_parsers(&self, path: &std::path::Path, ids: &mut Vec) { if let Some(extension) = path.extension() { - let id = self.extensions.get(extension).map(ParserInfo::id); - // A couple of corner cases here: - // - // * The kind of case where you have foo.l and bar.y/baz.y using the same lexer. - // -- We should probably allow this case where editing a single file updates multiple parsers. - // * The kind of case where you have a yacc.y for the extension .y, so both the extension - // and the parse_info have the same id. - // -- We don't want to run the same parser multiple times: remove duplicates. - // In the general case, where you either change a .l, .y, or a file of the parsers extension - // this will be a vec of one element. - if let Some(id) = id { - ids.push(id); - } + // FIXME should be a globset of toml tests? + if extension == "toml" { + let stem = path.file_stem(); + if let Some(stem) = stem { + let stem = std::path::PathBuf::from(stem); + let extension = stem.extension(); + if let Some(extension) = extension { + let id = self.extensions.get(extension).map(ParserInfo::id); + if let Some(id) = id { + ids.push(id); + } + } + } + } else { + let id = self.extensions.get(extension).map(ParserInfo::id); + + // A couple of corner cases here: + // + // * The kind of case where you have foo.l and bar.y/baz.y using the same lexer. + // -- We should probably allow this case where editing a single file updates multiple parsers. + // * The kind of case where you have a yacc.y for the extension .y, so both the extension + // and the parse_info have the same id. + // -- We don't want to run the same parser multiple times: remove duplicates. + // In the general case, where you either change a .l, .y, or a file of the parsers extension + // this will be a vec of one element. + if let Some(id) = id { + ids.push(id); + } - ids.extend( - self.extensions - .values() - .filter(|parser_info| path == parser_info.l_path || path == parser_info.y_path) - .map(ParserInfo::id), - ); + ids.extend( + self.extensions + .values() + .filter(|parser_info| { + path == parser_info.l_path || path == parser_info.y_path + }) + .map(ParserInfo::id), + ); - ids.sort_unstable(); - ids.dedup(); + ids.sort_unstable(); + ids.dedup(); + } } } @@ -223,8 +244,7 @@ impl State { } fn parser_for(&self, path: &std::path::Path) -> Option<&ParserInfo> { - path.extension() - .map_or(None, |ext| self.extensions.get(ext)) + path.extension().and_then(|ext| self.extensions.get(ext)) } } @@ -397,8 +417,42 @@ impl tower_lsp::LanguageServer for Backend { let mut state = self.state.lock().await; let state = state.deref_mut(); let mut globs: Vec = Vec::new(); + if state.client_monitor { for WorkspaceCfg { workspace, .. } in state.toml.values() { + for test in &workspace.tests { + if let nimbleparse_toml::TestKind::Toml { + parser_extension: _parser_extension, + toml_test_extension, + } = test.kind.clone().into_inner() + { + self.client + .log_message( + lsp::MessageType::LOG, + format!("registering toml test: {:?}", &test), + ) + .await; + + let mut reg = serde_json::Map::new(); + reg.insert( + "globPattern".to_string(), + serde_json::value::Value::String(format!("*/{}", toml_test_extension)), + ); + let mut watchers = serde_json::Map::new(); + watchers.insert( + "watchers".to_string(), + serde_json::value::Value::Array(vec![ + serde_json::value::Value::Object(reg), + ]), + ); + globs.push(lsp::Registration { + id: "1".to_string(), + method: "workspace/didChangeWatchedFiles".to_string(), + register_options: Some(serde_json::value::Value::Object(watchers)), + }); + } + } + for parser in workspace.parsers.get_ref() { let glob = format!("**/*{}", parser.extension.get_ref()); let mut reg = serde_json::Map::new(); @@ -532,6 +586,7 @@ impl tower_lsp::LanguageServer for Backend { let url = params.text_document.uri.clone(); let path = url.to_file_path(); let nimbleparse_toml = std::ffi::OsStr::new("nimbleparse.toml"); + match path { Ok(path) if Some(nimbleparse_toml) == path.file_name() => { if !state.warned_needs_restart { @@ -594,6 +649,7 @@ impl tower_lsp::LanguageServer for Backend { let mut state = self.state.lock().await; let url = params.text_document.uri.clone(); let path = url.to_file_path(); + match path { Ok(path) => { let mut ids = vec![]; diff --git a/server/src/parse_thread.rs b/server/src/parse_thread.rs index b2e2a62..f6285dd 100644 --- a/server/src/parse_thread.rs +++ b/server/src/parse_thread.rs @@ -236,26 +236,23 @@ impl ParseThread { change_set.clear(); - for test_dir in self.test_dirs() { - let nimbleparse_toml::TestKind::Dir(glob) = test_dir.kind.clone().into_inner(); - { + for test in self.tests().iter().cloned() { + if let nimbleparse_toml::TestKind::Dir(glob) = test.kind.into_inner() { let abs_glob_as_path = self.workspace_path.join(glob); let glob_str = abs_glob_as_path.to_str().unwrap(); let paths = glob::glob(glob_str); if let Ok(paths) = paths { - for test_dir_path in paths { - if let Ok(test_dir_path) = test_dir_path { - let files_of_extension = files.iter().filter(|(test_path, _file)| { - test_path.extension() == Some(&self.parser_info.extension) - && test_path.parent() - == Some(&self.subdir_path(test_dir_path.as_path())) + for test_dir_path in paths.flatten() { + let files_of_extension = files.iter().filter(|(test_path, _file)| { + test_path.extension() == Some(&self.parser_info.extension) + && test_path.parent() + == Some(&self.subdir_path(test_dir_path.as_path())) + }); + for (path, _file) in files_of_extension { + change_set.insert(TestReparse { + path: path.clone(), + pass: test.pass, }); - for (path, _file) in files_of_extension { - change_set.insert(TestReparse { - path: path.clone(), - pass: test_dir.pass, - }); - } } } } @@ -359,7 +356,7 @@ impl ParseThread { self.output .send(ParserMsg::Info(format!( "diagnostics {} {} {:?}", - url.clone(), + url, diags.len(), file.version ))) @@ -375,7 +372,7 @@ impl ParseThread { } } - pub fn test_dirs(&self) -> &[nimbleparse_toml::TestDir] { + pub fn tests(&self) -> &[nimbleparse_toml::Test] { self.workspace_cfg.workspace.tests.as_slice() } @@ -401,20 +398,48 @@ impl ParseThread { std::collections::HashMap::new(); let mut change_set: std::collections::HashSet = std::collections::HashSet::new(); + let mut gb = globset::GlobSetBuilder::new(); + if let Some(extension) = self.parser_info.extension.to_str() { + let glob = globset::Glob::new(&format!("*.{}", extension)); + if let Ok(glob) = glob { + gb.add(glob); + } + } - // Read in all the test dir files... - for nimbleparse_toml::TestDir { kind, .. } in self.test_dirs() { - let nimbleparse_toml::TestKind::Dir(glob) = kind.clone().into_inner(); + for nimbleparse_toml::Test { kind, .. } in self.tests() { + if let nimbleparse_toml::TestKind::Toml { + parser_extension, + toml_test_extension, + } = kind.clone().into_inner() { + let parser_extension = parser_extension + .strip_prefix('.') + .unwrap_or(&parser_extension); + if parser_extension == self.parser_info.extension { + let x = toml_test_extension + .strip_prefix('.') + .unwrap_or(&toml_test_extension); + let glob = globset::Glob::new(&format!("*.{}", x)); + if let Ok(glob) = glob { + gb.add(glob); + } + } + } + } + + let extensions = gb.build().unwrap(); + // Read in all the test dir files... + for nimbleparse_toml::Test { kind, .. } in self.tests() { + if let nimbleparse_toml::TestKind::Dir(glob) = kind.clone().into_inner() { let glob_path = self.workspace_path.join(glob); let glob_str = glob_path.to_str().unwrap(); - for dirs in glob::glob(glob_str) { - for dir in dirs { - if let Ok(dir) = dir { - let dir_read = std::fs::read_dir(dir); - if let Ok(dir_read) = dir_read { - dir_read.for_each(|file| { - if let Ok(file) = file { + if let Ok(dirs) = glob::glob(glob_str) { + for dir in dirs.flatten() { + let dir_read = std::fs::read_dir(dir); + if let Ok(dir_read) = dir_read { + dir_read.for_each(|file| { + if let Ok(file) = file { + if extensions.is_match(file.path()) { let contents = std::fs::read_to_string(file.path()); if let Ok(contents) = contents { let contents = rope::Rope::from(contents); @@ -427,8 +452,8 @@ impl ParseThread { ); } } - }); - } + } + }); } } } @@ -515,17 +540,21 @@ impl ParseThread { } } else { let parent = path.parent(); - let test_dir = self.test_dirs().iter().find(|test_dir| { - let nimbleparse_toml::TestKind::Dir(glob) = - test_dir.kind.clone().into_inner(); + let test_dir = self.tests().iter().find(|test_dir| { + if let nimbleparse_toml::TestKind::Dir(glob) = + test_dir.kind.clone().into_inner() { let glob_path = self.workspace_path.join(glob); let pattern = glob::Pattern::new(glob_path.to_str().unwrap()) .unwrap(); pattern.matches_path(parent.unwrap()) + && extensions.is_match(path.clone()) + } else { + false } }); + if let Some(test_dir) = test_dir { let change = TestReparse { path: path.to_path_buf(), @@ -591,18 +620,21 @@ impl ParseThread { } } else { let parent = path.parent(); - let test_dir = self.test_dirs().iter().find(|test_dir| { - let nimbleparse_toml::TestKind::Dir(glob) = - test_dir.kind.clone().into_inner(); - { - let glob_path = self.workspace_path.join(glob); - let pattern = - glob::Pattern::new(glob_path.to_str().unwrap()) - .unwrap(); - pattern.matches_path(parent.unwrap()) + let test = self.tests().iter().find(|test| { + match test.kind.clone().into_inner() { + nimbleparse_toml::TestKind::Dir(glob) => { + let glob_path = self.workspace_path.join(glob); + let pattern = glob::Pattern::new( + glob_path.to_str().unwrap(), + ) + .unwrap(); + pattern.matches_path(parent.unwrap()) + && extensions.is_match(&path) + } + nimbleparse_toml::TestKind::Toml { .. } => false, } }); - if let Some(test_dir) = test_dir { + if let Some(test_dir) = test { let change = TestReparse { path: path.to_path_buf(), pass: test_dir.pass, @@ -666,7 +698,7 @@ impl ParseThread { let mut choice = railroad::Choice::new(vec![]); for symbols in prod_syms { let mut a_seq = railroad::Sequence::new(vec![]); - if symbols.len() > 0 { + if symbols.is_empty() { for symbol in symbols { match symbol { cfgrammar::Symbol::Rule(prod_ridx) @@ -751,7 +783,7 @@ impl ParseThread { let lexer = lexerdef.lexer(&input); let generic_tree_text = if let Some(pb) = &pb { let (tree, _) = pb.parse_generictree(&lexer); - tree.map_or(None, |tree| Some(tree.pp(&grm, &input))) + tree.map(|tree| tree.pp(&grm, &input)) } else { None }; @@ -789,21 +821,25 @@ impl ParseThread { let TestReparse { path, pass } = &reparse; let file = files.get(path); if let Some(file) = file { - let message: String = format!( - "{i}/{n} {}", - path.strip_prefix(&self.workspace_path).unwrap().display() - ); - self.parse_file(file, path, lexerdef, pb, *pass); - let pcnt = ((i as f32 / n as f32) * 100.0).ceil(); - self.output - .send(ParserMsg::Info(format!( - "parsed {} {} {} {}", - token, message, pcnt as u32, pass - ))) - .unwrap(); - self.output - .send(ParserMsg::ProgressStep(token, message, pcnt as u32)) - .unwrap(); + if path.extension() == Some(&self.parser_info.extension) { + let message: String = format!( + "{i}/{n} {}", + path.strip_prefix(&self.workspace_path).unwrap().display() + ); + self.parse_file(file, path, lexerdef, pb, *pass); + let pcnt = ((i as f32 / n as f32) * 100.0).ceil(); + self.output + .send(ParserMsg::Info(format!( + "parsed {} {} {} {}", + token, message, pcnt as u32, pass + ))) + .unwrap(); + self.output + .send(ParserMsg::ProgressStep(token, message, pcnt as u32)) + .unwrap(); + } else { + // TODO Must be a toml test... + } change_set.remove(&reparse); //std::thread::sleep(std::time::Duration::from_millis(500)); } else { diff --git a/toml/src/lib.rs b/toml/src/lib.rs index ed565ee..134d279 100644 --- a/toml/src/lib.rs +++ b/toml/src/lib.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Workspace { pub parsers: toml::Spanned>, - pub tests: Vec, + pub tests: Vec, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -35,13 +35,20 @@ fn default_recovery_kind() -> RecoveryKind { // are source generators, and files which contain a list of strings each to be parsed as // it's own file... So the reason TestKind is like this is for future expansion. // We should consider having a trait for it... -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] pub enum TestKind { + // path of glob e.g. "tests/pass/**" Dir(String), + Toml { + /// Should match a Parser.extension + parser_extension: String, + /// An extension to be interpreted as toml tests + toml_test_extension: String, + }, } #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct TestDir { +pub struct Test { pub kind: toml::Spanned, pub pass: bool, } diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 7d29ad9..121d96f 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -20,9 +20,13 @@ import { execFileSync } from 'child_process'; let lspClient: LanguageClient; interface Parser { l_file: string, y_file: string, extension: string } + +interface TomlTest {Toml: {parser_extension: string, toml_test_extension: string}}; +interface DirTest {Dir: string}; +interface ParseTest {kind: TomlTest | DirTest, pass: boolean}; interface TomlWorkspace { parsers: Parser[], - tests: {dir: string, pass: boolean} + tests: ParseTest[], }; @@ -46,6 +50,7 @@ export function activate(context: vscode.ExtensionContext) { const lsp_path_relative = path.resolve(path.join(context.extensionPath, "bin"), "nimbleparse_lsp"); const lsp_path_relative_exe = path.resolve(path.join(context.extensionPath, "bin"), "nimbleparse_lsp.exe"); // Try and find it relative to the extension, or fall back to the PATH. + const outputChannel = vscode.window.createOutputChannel("nimbleparse_lsp"); const lsp_path = fs.existsSync(lsp_path_relative) ? lsp_path_relative : (fs.existsSync(lsp_path_relative_exe) ? lsp_path_relative_exe : "nimbleparse_lsp"); // This doesn't quite work if we have a project `foo/` @@ -71,6 +76,20 @@ export function activate(context: vscode.ExtensionContext) { return; } + var testDirs: string[] = []; + tomls?.forEach( + (toml) => { + toml.workspace.tests.forEach( + (test) => { + let foo = test.kind as DirTest; + if (foo.Dir != undefined) { + testDirs.push(toml.folder + '/' + foo.Dir) + } + } + ) + } + ); + var dynSelector = tomls?.flatMap( (toml) => toml.workspace.parsers.flatMap( (parser: Parser) => ([{ @@ -84,7 +103,21 @@ export function activate(context: vscode.ExtensionContext) { ) ); - const outputChannel = vscode.window.createOutputChannel("nimbleparse_lsp"); + testDirs.forEach( + (test_dir) => { + tomls?.forEach( + (toml) => toml.workspace.tests.forEach( + (test) => { + let foo = test.kind as TomlTest; + if (foo.Toml != undefined) { + dynSelector.push({pattern: test_dir + '/*' + foo.Toml.toml_test_extension }) + } + } + ) + ) + } + ); + const lsp_exec: Executable = { command: lsp_path, args: ["--server"],