diff --git a/crates/lsp/src/indexing/content_indexing.rs b/crates/lsp/src/indexing/content_indexing.rs index a77e6ec4..4a56064c 100644 --- a/crates/lsp/src/indexing/content_indexing.rs +++ b/crates/lsp/src/indexing/content_indexing.rs @@ -1,57 +1,59 @@ use std::collections::HashMap; use tokio::time::Instant; use abs_path::AbsPath; -use witcherscript_project::content::{ContentScanError, ProjectDirectory, find_content_in_directory}; +use witcherscript_project::content::ContentScanError; use witcherscript_project::source_tree::SourceTreeDifference; -use witcherscript_project::{Content, ContentRepositories, FileError}; +use witcherscript_project::{ContentScanner, FileError}; use witcherscript_project::content_graph::{ContentGraphDifference, ContentGraphError}; use crate::{reporting::IntoLspDiagnostic, Backend}; impl Backend { - pub async fn scan_workspace_projects(&self) { - self.log_info("Scanning workspace projects...").await; - - let mut projects = Vec::new(); - + pub async fn setup_workspace_content_scanners(&self) { + let mut graph = self.content_graph.write().await; let workspace_roots = self.workspace_roots.read().await; + + graph.clear_workspace_scanners(); + for root in workspace_roots.iter() { - let (contents, errors) = find_content_in_directory(root, true); - - for content in contents { - if let Ok(proj) = content.into_any().downcast::() { - projects.push(proj); - } - } - - for err in errors { - self.report_content_scan_error(err).await; - } - } + let scanner = + ContentScanner::new(root.clone()).unwrap() + .recursive(true) + .only_projects(true); - if projects.is_empty() { - self.log_info("Found no projects in the workspace.").await; - } else { - for proj in &projects { - self.log_info(format!("Found project {}", proj.content_name())).await; - } + graph.add_workspace_scanner(scanner); } - - let mut graph = self.content_graph.write().await; - graph.set_workspace_projects(projects); - } - - pub async fn scan_content_repositories(&self) { - self.log_info("Scanning content repositories...").await; + } - let mut repos = ContentRepositories::new(); - + pub async fn setup_repository_content_scanners(&self) { + let mut graph = self.content_graph.write().await; let config = self.config.read().await; + + let mut repo_paths = Vec::new(); + for repo in &config.project_repositories { + repo_paths.push(repo.clone()); + } + + repo_paths.push(config.game_directory.join("content")); + repo_paths.push(config.game_directory.join("Mods")); + + + graph.clear_repository_scanners(); + + for repo in repo_paths { if !repo.as_os_str().is_empty() { - match AbsPath::resolve(repo, None) { + match AbsPath::resolve(&repo, None) { Ok(abs_repo) => { - repos.add_repository(abs_repo); + match ContentScanner::new(abs_repo) { + Ok(scanner) => { + let scanner = scanner.recursive(false).only_projects(false); + graph.add_repository_scanner(scanner); + }, + Err(err) => { + self.report_content_scan_error(err).await; + }, + } } Err(_) => { self.log_error(format!("Invalid project repository path: {}", repo.display())).await; @@ -59,39 +61,13 @@ impl Backend { } } } - if !config.game_directory.as_os_str().is_empty() { - match AbsPath::resolve(&config.game_directory, None) { - Ok(abs_game_directory) => { - repos.add_repository(abs_game_directory.join("content").unwrap()); - repos.add_repository(abs_game_directory.join("Mods").unwrap()); - } - Err(_) => { - self.log_error(format!("Invalid game directory path: {}", config.game_directory.display())).await; - } - } - } - - repos.scan(); - - for err in &repos.errors { - self.report_content_scan_error(err.clone()).await; - } - - if repos.found_content().is_empty() { - self.log_info("Found no script contents in repositories.").await; - } else { - for content in repos.found_content() { - self.log_info(format!("Found script content {}", content.content_name())).await; - } - } - - let mut graph = self.content_graph.write().await; - graph.set_repositories(repos); } pub async fn build_content_graph(&self) { self.log_info("Building content graph...").await; + self.clear_all_diagnostics().await; + let mut graph = self.content_graph.write().await; let diff = graph.build(); diff --git a/crates/lsp/src/providers/configuration.rs b/crates/lsp/src/providers/configuration.rs index 6b814d59..e9b0ba77 100644 --- a/crates/lsp/src/providers/configuration.rs +++ b/crates/lsp/src/providers/configuration.rs @@ -4,9 +4,7 @@ use crate::Backend; pub async fn did_change_configuration(backend: &Backend, _: lsp::DidChangeConfigurationParams) { if backend.fetch_config().await { - backend.clear_all_diagnostics().await; - - backend.scan_content_repositories().await; + backend.setup_repository_content_scanners().await; backend.build_content_graph().await; } } \ No newline at end of file diff --git a/crates/lsp/src/providers/document_ops.rs b/crates/lsp/src/providers/document_ops.rs index 31def2a8..36fa87da 100644 --- a/crates/lsp/src/providers/document_ops.rs +++ b/crates/lsp/src/providers/document_ops.rs @@ -4,7 +4,7 @@ use tower_lsp::jsonrpc::Result; use abs_path::AbsPath; use witcherscript::{script_document::ScriptDocument, Script}; use witcherscript_analysis::{diagnostics::Diagnostic, jobs::syntax_analysis}; -use witcherscript_project::Manifest; +use witcherscript_project::{content::ProjectDirectory, Manifest}; use crate::{reporting::IntoLspDiagnostic, Backend}; @@ -49,13 +49,12 @@ pub async fn did_open(backend: &Backend, params: lsp::DidOpenTextDocumentParams) let project_is_known = backend .content_graph .read().await - .get_workspace_projects() - .iter() + .nodes() + .filter_map(|n| n.content.as_any().downcast_ref::()) .any(|p| p.manifest_path() == &doc_path); if !project_is_known { backend.log_info("Opened unknown manifest file").await; - backend.scan_workspace_projects().await; backend.build_content_graph().await; } } diff --git a/crates/lsp/src/providers/initialization.rs b/crates/lsp/src/providers/initialization.rs index 5a8482c5..aa4be66d 100644 --- a/crates/lsp/src/providers/initialization.rs +++ b/crates/lsp/src/providers/initialization.rs @@ -72,7 +72,7 @@ pub async fn initialized(backend: &Backend, _: lsp::InitializedParams) { } ]).await.unwrap(); - backend.scan_content_repositories().await; - backend.scan_workspace_projects().await; + backend.setup_workspace_content_scanners().await; + backend.setup_repository_content_scanners().await; backend.build_content_graph().await; } \ No newline at end of file diff --git a/crates/lsp/src/providers/workspace.rs b/crates/lsp/src/providers/workspace.rs index 218dace4..7261a171 100644 --- a/crates/lsp/src/providers/workspace.rs +++ b/crates/lsp/src/providers/workspace.rs @@ -18,8 +18,6 @@ pub async fn did_change_workspace_folders(backend: &Backend, params: lsp::DidCha workspace_roots.extend(added); } - backend.clear_all_diagnostics().await; - - backend.scan_workspace_projects().await; + backend.setup_workspace_content_scanners().await; backend.build_content_graph().await; } \ No newline at end of file diff --git a/crates/project/src/content.rs b/crates/project/src/content.rs index 2af6caa3..a4ed2dae 100644 --- a/crates/project/src/content.rs +++ b/crates/project/src/content.rs @@ -176,53 +176,6 @@ pub enum ContentScanError { NotContent, } -pub fn find_content_in_directory(path: &AbsPath, scan_recursively: bool) -> (Vec>, Vec) { - let mut contents = Vec::new(); - let mut errors = Vec::new(); - - if path.is_dir() { - if let Ok(content) = try_make_content(path) { - contents.push(content); - } else { - _find_content_in_directory(path, scan_recursively, &mut contents, &mut errors); - } - } - - (contents, errors) -} - -fn _find_content_in_directory(path: &AbsPath, scan_recursively: bool, contents: &mut Vec>, errors: &mut Vec) { - match std::fs::read_dir(path) { - Ok(iter) => { - for entry in iter { - match entry { - Ok(entry) => { - let candidate = AbsPath::resolve(entry.path(), None).unwrap(); - if candidate.is_dir() { - match try_make_content(&candidate) { - Ok(content) => contents.push(content), - Err(err) => { - if let (&ContentScanError::NotContent, true) = (&err, scan_recursively) { - _find_content_in_directory(&candidate, scan_recursively, contents, errors) - } else { - errors.push(err); - } - } - } - } - }, - Err(err) => { - errors.push(FileError::new(path.clone(), err).into()); - } - } - } - }, - Err(err) => { - errors.push(FileError::new(path.clone(), err).into()); - } - } -} - pub fn try_make_content(path: &AbsPath) -> Result, ContentScanError> { let manifest_path = path.join(Manifest::FILE_NAME).unwrap(); if manifest_path.exists() { diff --git a/crates/project/src/content_graph.rs b/crates/project/src/content_graph.rs index 05de9165..f5acda41 100644 --- a/crates/project/src/content_graph.rs +++ b/crates/project/src/content_graph.rs @@ -4,7 +4,7 @@ use abs_path::AbsPath; use thiserror::Error; use lsp_types as lsp; use crate::content::{try_make_content, ContentScanError, ProjectDirectory}; -use crate::{Content, ContentRepositories, FileError}; +use crate::{Content, FileError, ContentScanner}; use crate::manifest::{DependencyValue, ManifestParseError}; @@ -44,8 +44,8 @@ pub enum ContentGraphError { /// Stores contents needed in the current workspace and tracks relationships between them. #[derive(Debug)] pub struct ContentGraph { - repos: ContentRepositories, - workspace_projects: Vec>, + repo_scanners: Vec, + workspace_scanners: Vec, nodes: Vec, edges: Vec, @@ -78,8 +78,8 @@ enum GraphEdgeDirection { impl ContentGraph { pub fn new() -> Self { Self { - repos: ContentRepositories::new(), - workspace_projects: Vec::new(), + repo_scanners: Vec::new(), + workspace_scanners: Vec::new(), nodes: Vec::new(), edges: Vec::new(), @@ -88,22 +88,20 @@ impl ContentGraph { } } - /// Set repositories which the graph can access for any dependencies - pub fn set_repositories(&mut self, repos: ContentRepositories) { - self.repos = repos; + pub fn clear_repository_scanners(&mut self) { + self.repo_scanners.clear(); } - pub fn get_reposity_contents(&self) -> &[Box] { - self.repos.found_content() + pub fn add_repository_scanner(&mut self, scanner: ContentScanner) { + self.repo_scanners.push(scanner); } - /// Set paths to contents from the workspace that should be actively monitored - pub fn set_workspace_projects(&mut self, contents: Vec>) { - self.workspace_projects = contents; + pub fn clear_workspace_scanners(&mut self) { + self.workspace_scanners.clear(); } - pub fn get_workspace_projects(&self) -> &[Box] { - &self.workspace_projects + pub fn add_workspace_scanner(&mut self, scanner: ContentScanner) { + self.workspace_scanners.push(scanner); } @@ -116,24 +114,12 @@ impl ContentGraph { self.edges.clear(); self.errors.clear(); - if !self.workspace_projects.is_empty() { - for i in 0..self.workspace_projects.len() { - let content = &self.workspace_projects[i]; - self.create_node_for_content(content.clone(), false, true); - } - - for i in 0..self.repos.found_content().len() { - let content = &self.repos.found_content()[i]; - self.create_node_for_content(dyn_clone::clone_box(&**content), true, false); - } - - // Correct nodes if repository and workspace paths overlap - for n in &mut self.nodes { - if self.repos.found_content().iter().any(|repo_content| repo_content.path() == n.content.path()) { - n.in_repository = true; - } - } - + self.create_workspace_content_nodes(); + + // do not try finding dependencies etc. if workspace scanners returned no contents + if !self.nodes.is_empty() { + self.create_repository_content_nodes(); + // Now visit each of workspace content nodes to check for their dependencies. let mut visited = HashSet::new(); for i in 0..self.nodes.len() { @@ -141,18 +127,18 @@ impl ContentGraph { self.link_dependencies(i, &mut visited); } } - + // At the start all contents found in repos were given a node. // Now we're going to remove nodes that are not needed anymore (the ones not used by workspace's projects). // Since we've built dependencies only for workspace contents, the contents that do not have any dependants are technically unnecessary. - let unneeded_content_paths: Vec<_> = self.nodes.iter() + let unneeded_content_indices: Vec<_> = self.nodes.iter() .enumerate() .filter(|(i, n)| !n.in_workspace && !self.edges.iter().any(|e| e.dependency_idx == *i)) - .map(|(_, n)| n.content.path().clone()) + .map(|(i, _)| i) .collect(); - for p in unneeded_content_paths { - self.remove_node_by_path(&p); + for i in unneeded_content_indices { + self.remove_node_by_index(i); } } @@ -222,18 +208,66 @@ impl ContentGraph { - /// Returns index of the node if it was inserted successfully - fn create_node_for_content(&mut self, content: Box, in_repository: bool, in_workspace: bool) { - if self.get_node_index_by_path(content.path()).is_some() { - // node has already been made for this content - return; + fn create_workspace_content_nodes(&mut self) { + for scanner in &self.workspace_scanners { + let (contents, errors) = scanner.scan(); + + for content in contents { + if let Some(i) = self.get_node_index_by_path(content.path()) { + self.nodes[i].in_workspace = true; + } else { + self.nodes.push(GraphNode { + content, + in_workspace: true, + in_repository: false, + }); + } + } + + for err in errors { + match err { + ContentScanError::Io(err) => { + self.errors.push(ContentGraphError::Io(err)); + }, + ContentScanError::ManifestParse(err) => { + self.errors.push(ContentGraphError::ManifestParse(err)) + }, + // NotContent only occurs when trying to make content manually and not when scanning + ContentScanError::NotContent => {}, + } + } } + } + + fn create_repository_content_nodes(&mut self) { + for scanner in &self.repo_scanners { + let (contents, errors) = scanner.scan(); + + for content in contents { + if let Some(i) = self.get_node_index_by_path(content.path()) { + self.nodes[i].in_repository = true; + } else { + self.nodes.push(GraphNode { + content, + in_workspace: false, + in_repository: true, + }); + } + } - self.insert_node(GraphNode { - content, - in_workspace, - in_repository, - }); + for err in errors { + match err { + ContentScanError::Io(err) => { + self.errors.push(ContentGraphError::Io(err)); + }, + ContentScanError::ManifestParse(err) => { + self.errors.push(ContentGraphError::ManifestParse(err)) + }, + // NotContent only occurs when trying to make content manually and not when scanning + ContentScanError::NotContent => {}, + } + } + } } fn link_dependencies(&mut self, node_idx: usize, visited: &mut HashSet) { @@ -256,18 +290,6 @@ impl ContentGraph { }, DependencyValue::FromPath { path } => { self.link_dependencies_value_from_path(node_idx, &manifest_path, visited, path, entry.value.range()); - // if `path` is absolute it will be returned as-is without joining it onto content path - // match self.nodes[node_idx].content.path().join(path) { - // Ok(final_path) => { - // }, - // Err(_) => { - // self.errors.push(ContentGraphError::DependencyPathNotFound { - // content_path: path.to_path_buf(), - // manifest_path: manifest_path.clone(), - // manifest_range: entry.value.range().clone() - // }) - // }, - // } }, } } @@ -381,34 +403,32 @@ impl ContentGraph { /// Changes node indices. Be aware! - fn remove_node_by_path(&mut self, content_path: &AbsPath) { - if let Some(target_idx) = self.get_node_index_by_path(content_path) { - // first remove all edges that mention this node - self.edges.retain(|edge| edge.dependant_idx != target_idx && edge.dependency_idx != target_idx); - - let last_idx = self.nodes.len() - 1; - if self.nodes.len() > 1 && target_idx != last_idx { - // swap this and the last node to retain the same indices for all but these swapped nodes - self.nodes.swap(target_idx, last_idx); - - // fix references to the swapped edge - self.edges.iter_mut() - .for_each(|edge| { - if edge.dependant_idx == last_idx { - edge.dependant_idx = target_idx; - } - if edge.dependency_idx == last_idx { - edge.dependency_idx = target_idx; - } - }); - - self.edges.sort(); - } + fn remove_node_by_index(&mut self, target_idx: usize) { + // first remove all edges that mention this node + self.edges.retain(|edge| edge.dependant_idx != target_idx && edge.dependency_idx != target_idx); + + let last_idx = self.nodes.len() - 1; + if self.nodes.len() > 1 && target_idx != last_idx { + // swap this and the last node to retain the same indices for all but these swapped nodes + self.nodes.swap(target_idx, last_idx); + + // fix references to the swapped edge + self.edges.iter_mut() + .for_each(|edge| { + if edge.dependant_idx == last_idx { + edge.dependant_idx = target_idx; + } + if edge.dependency_idx == last_idx { + edge.dependency_idx = target_idx; + } + }); - // remove the last element - // if we did a swap it is the node we've been intending to remove - self.nodes.pop(); + self.edges.sort(); } + + // remove the last element + // if we did a swap it is the node we've been intending to remove + self.nodes.pop(); } fn get_node_index_by_path(&self, path: &AbsPath) -> Option { diff --git a/crates/project/src/content_repository.rs b/crates/project/src/content_repository.rs deleted file mode 100644 index 17b0c070..00000000 --- a/crates/project/src/content_repository.rs +++ /dev/null @@ -1,45 +0,0 @@ -use abs_path::AbsPath; -use crate::{content::ContentScanError, find_content_in_directory, Content}; - - -/// A collection of directories in which content directories can be found. -/// Only direct directory descendants are checked for being content directories. -/// Mainly used repositories are `Witcher 3/content` and `Witcher 3/Mods`. -#[derive(Debug, Default)] -pub struct ContentRepositories { - repository_paths: Vec, - found_content: Vec>, - /// Errors encountered during scanning - pub errors: Vec -} - -impl ContentRepositories { - pub fn new() -> Self { - Self { - repository_paths: Vec::new(), - found_content: Vec::new(), - errors: Vec::new() - } - } - - pub fn add_repository(&mut self, path: AbsPath) { - if !self.repository_paths.contains(&path) { - self.repository_paths.push(path); - } - } - - pub fn found_content(&self) -> &[Box] { - &self.found_content - } - - pub fn scan(&mut self) { - self.found_content.clear(); - self.errors.clear(); - - for repo in &self.repository_paths { - let (contents, errors) = find_content_in_directory(repo, false); - self.found_content.extend(contents); - self.errors.extend(errors); - } - } -} \ No newline at end of file diff --git a/crates/project/src/content_scanner.rs b/crates/project/src/content_scanner.rs new file mode 100644 index 00000000..7ed70a29 --- /dev/null +++ b/crates/project/src/content_scanner.rs @@ -0,0 +1,93 @@ +use abs_path::AbsPath; +use crate::{content::{try_make_content, ContentScanError, ProjectDirectory}, Content, FileError}; + + +#[derive(Debug, Clone)] +pub struct ContentScanner { + scan_root: AbsPath, + recursive: bool, + only_projects: bool +} + +impl ContentScanner { + pub fn new(scan_root: AbsPath) -> Result { + if !scan_root.is_dir() { + return Err(ContentScanError::Io(FileError::new( + scan_root, + std::io::Error::new(std::io::ErrorKind::NotFound, "Path is not an existing directory") + ))); + } + + Ok(Self { + scan_root, + recursive: false, + only_projects: false + }) + } + + pub fn recursive(self, val: bool) -> Self { + Self { + recursive: val, + ..self + } + } + + pub fn only_projects(self, val: bool) -> Self { + Self { + only_projects: val, + ..self + } + } + + + pub fn scan(&self) -> (Vec>, Vec) { + let mut contents = Vec::new(); + let mut errors = Vec::new(); + + if let Ok(content) = try_make_content(&self.scan_root) { + contents.push(content); + } else { + self.find_content_in_directory(&self.scan_root, &mut contents, &mut errors); + } + + if self.only_projects { + contents.retain(|c| c.as_any().is::()); + } + + (contents, errors) + } + + fn find_content_in_directory(&self, path: &AbsPath, contents: &mut Vec>, errors: &mut Vec) { + match std::fs::read_dir(path) { + Ok(iter) => { + for entry in iter { + match entry { + Ok(entry) => { + let candidate = AbsPath::resolve(entry.path(), None).unwrap(); + if candidate.is_dir() { + match try_make_content(&candidate) { + Ok(content) => { + contents.push(content) + }, + Err(err) => { + if let (&ContentScanError::NotContent, true) = (&err, self.recursive) { + self.find_content_in_directory(&candidate, contents, errors) + } else { + errors.push(err); + } + } + } + } + }, + Err(err) => { + errors.push(FileError::new(path.clone(), err).into()); + } + } + } + }, + Err(err) => { + errors.push(FileError::new(path.clone(), err).into()); + } + } + } +} diff --git a/crates/project/src/lib.rs b/crates/project/src/lib.rs index e1b3d701..10f0660d 100644 --- a/crates/project/src/lib.rs +++ b/crates/project/src/lib.rs @@ -5,10 +5,10 @@ pub mod source_tree; pub use source_tree::SourceTree; pub mod content; -pub use content::{Content, find_content_in_directory}; +pub use content::{Content, try_make_content}; -mod content_repository; -pub use content_repository::ContentRepositories; +mod content_scanner; +pub use content_scanner::ContentScanner; pub mod content_graph; pub use content_graph::ContentGraph;