diff --git a/code/src/extension.ts b/code/src/extension.ts index 81153b7e62..3b24b73b64 100644 --- a/code/src/extension.ts +++ b/code/src/extension.ts @@ -113,12 +113,26 @@ export function activate(context: vscode.ExtensionContext) { return await vscode.tasks.executeTask(task) } }) - registerCommand('libsyntax-rust.createFile', async (uri_: string) => { - let uri = vscode.Uri.parse(uri_) + registerCommand('libsyntax-rust.fsEdit', async (ops: FsOp[]) => { let edit = new vscode.WorkspaceEdit() - edit.createFile(uri) + let created; + let moved; + for (let op of ops) { + if (op.type == "createFile") { + let uri = vscode.Uri.parse(op.uri!) + edit.createFile(uri) + created = uri + } else if (op.type == "moveFile") { + let src = vscode.Uri.parse(op.src!) + let dst = vscode.Uri.parse(op.dst!) + edit.renameFile(src, dst) + moved = dst + } else { + console.error(`unknown op: ${JSON.stringify(op)}`) + } + } await vscode.workspace.applyEdit(edit) - let doc = await vscode.workspace.openTextDocument(uri) + let doc = await vscode.workspace.openTextDocument((created || moved)!) await vscode.window.showTextDocument(doc) }) @@ -368,3 +382,10 @@ function createTask(spec: Runnable): vscode.Task { let t = new vscode.Task(definition, f, definition.label, TASK_SOURCE, exec, ['$rustc']); return t; } + +interface FsOp { + type: string; + uri?: string; + src?: string; + dst?: string; +} diff --git a/crates/libanalysis/Cargo.toml b/crates/libanalysis/Cargo.toml index a8ef5e5f45..5aca84f0ec 100644 --- a/crates/libanalysis/Cargo.toml +++ b/crates/libanalysis/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Aleksey Kladov "] [dependencies] +relative-path = "0.3.7" log = "0.4.2" failure = "0.1.2" parking_lot = "0.6.3" diff --git a/crates/libanalysis/src/lib.rs b/crates/libanalysis/src/lib.rs index fe2c3c2e6a..96d10a087c 100644 --- a/crates/libanalysis/src/lib.rs +++ b/crates/libanalysis/src/lib.rs @@ -8,13 +8,13 @@ extern crate libsyntax2; extern crate libeditor; extern crate fst; extern crate rayon; +extern crate relative_path; mod symbol_index; mod module_map; use std::{ fmt, - path::{Path, PathBuf}, panic, sync::{ Arc, @@ -24,6 +24,7 @@ use std::{ time::Instant, }; +use relative_path::{RelativePath,RelativePathBuf}; use once_cell::sync::OnceCell; use rayon::prelude::*; @@ -37,13 +38,16 @@ use libeditor::{Diagnostic, LineIndex, FileSymbol, find_node_at_offset}; use self::{ symbol_index::FileSymbols, - module_map::{ModuleMap, ChangeKind}, + module_map::{ModuleMap, ChangeKind, Problem}, }; pub use self::symbol_index::Query; pub type Result = ::std::result::Result; -pub type FileResolver = dyn Fn(FileId, &Path) -> Option + Send + Sync; +pub trait FileResolver: Send + Sync + 'static { + fn file_stem(&self, id: FileId) -> String; + fn resolve(&self, id: FileId, path: &RelativePath) -> Option; +} #[derive(Debug)] pub struct WorldState { @@ -84,7 +88,7 @@ impl WorldState { pub fn snapshot( &self, - file_resolver: impl Fn(FileId, &Path) -> Option + 'static + Send + Sync, + file_resolver: impl FileResolver, ) -> World { World { needs_reindex: AtomicBool::new(false), @@ -132,8 +136,20 @@ impl WorldState { } #[derive(Debug)] -pub enum QuickFix { - CreateFile(PathBuf), +pub struct QuickFix { + pub fs_ops: Vec, +} + +#[derive(Debug)] +pub enum FsOp { + CreateFile { + anchor: FileId, + path: RelativePathBuf, + }, + MoveFile { + file: FileId, + path: RelativePathBuf, + } } impl World { @@ -221,20 +237,49 @@ impl World { .into_iter() .map(|d| (d, None)) .collect::>(); - for module in syntax.ast().modules() { - if module.has_semi() && self.resolve_module(file_id, module).is_empty() { - if let Some(name) = module.name() { - let d = Diagnostic { - range: name.syntax().range(), - msg: "unresolved module".to_string(), - }; - let quick_fix = self.data.module_map.suggested_child_mod_path(module) - .map(QuickFix::CreateFile); - res.push((d, quick_fix)) - } + self.data.module_map.problems( + file_id, + &*self.file_resolver, + &|file_id| self.file_syntax(file_id).unwrap(), + |name_node, problem| { + let (diag, fix) = match problem { + Problem::UnresolvedModule { candidate } => { + let diag = Diagnostic { + range: name_node.syntax().range(), + msg: "unresolved module".to_string(), + }; + let fix = QuickFix { + fs_ops: vec![FsOp::CreateFile { + anchor: file_id, + path: candidate.clone(), + }] + }; + (diag, fix) + } + Problem::NotDirOwner { move_to, candidate } => { + let diag = Diagnostic { + range: name_node.syntax().range(), + msg: "can't declare module at this location".to_string(), + }; + let fix = QuickFix { + fs_ops: vec![ + FsOp::MoveFile { + file: file_id, + path: move_to.clone(), + }, + FsOp::CreateFile { + anchor: file_id, + path: move_to.join(candidate), + } + ], + }; + (diag, fix) + } + }; + res.push((diag, Some(fix))) } - } + ); Ok(res) } diff --git a/crates/libanalysis/src/module_map.rs b/crates/libanalysis/src/module_map.rs index 4f480591ed..b65569c46e 100644 --- a/crates/libanalysis/src/module_map.rs +++ b/crates/libanalysis/src/module_map.rs @@ -1,6 +1,4 @@ -use std::{ - path::{PathBuf}, -}; +use relative_path::RelativePathBuf; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use libsyntax2::{ @@ -43,6 +41,18 @@ struct Link { owner: ModuleId, syntax: SyntaxNode, points_to: Vec, + problem: Option, +} + +#[derive(Clone, Debug)] +pub enum Problem { + UnresolvedModule { + candidate: RelativePathBuf, + }, + NotDirOwner { + move_to: RelativePathBuf, + candidate: RelativePathBuf, + } } impl ModuleMap { @@ -93,9 +103,24 @@ impl ModuleMap { res } - pub fn suggested_child_mod_path(&self, m: ast::Module) -> Option { - let name = m.name()?; - Some(PathBuf::from(format!("../{}.rs", name.text()))) + pub fn problems( + &self, + file: FileId, + file_resolver: &FileResolver, + syntax_provider: &SyntaxProvider, + mut cb: impl FnMut(ast::Name, &Problem), + ) { + let module = self.file2module(file); + let links = self.links(file_resolver, syntax_provider); + links + .links + .iter() + .filter(|link| link.owner == module) + .filter_map(|link| { + let problem = link.problem.as_ref()?; + Some((link, problem)) + }) + .for_each(|(link, problem)| cb(link.name_node(), problem)) } fn links( @@ -176,14 +201,17 @@ impl Link { owner, syntax: module.syntax().owned(), points_to: Vec::new(), + problem: None, }; Some(link) } fn name(&self) -> SmolStr { - self.ast().name() - .unwrap() - .text() + self.name_node().text() + } + + fn name_node(&self) -> ast::Name { + self.ast().name().unwrap() } fn ast(&self) -> ast::Module { @@ -192,14 +220,30 @@ impl Link { } fn resolve(&mut self, file_resolver: &FileResolver) { - let name = self.name(); - let paths = &[ - PathBuf::from(format!("../{}.rs", name)), - PathBuf::from(format!("../{}/mod.rs", name)), - ]; - self.points_to = paths.iter() - .filter_map(|path| file_resolver(self.owner.0, path)) - .map(ModuleId) - .collect(); + let mod_name = file_resolver.file_stem(self.owner.0); + let is_dir_owner = + mod_name == "mod" || mod_name == "lib" || mod_name == "main"; + + let file_mod = RelativePathBuf::from(format!("../{}.rs", self.name())); + let dir_mod = RelativePathBuf::from(format!("../{}/mod.rs", self.name())); + if is_dir_owner { + self.points_to = [&file_mod, &dir_mod].iter() + .filter_map(|path| file_resolver.resolve(self.owner.0, path)) + .map(ModuleId) + .collect(); + self.problem = if self.points_to.is_empty() { + Some(Problem::UnresolvedModule { + candidate: file_mod, + }) + } else { + None + } + } else { + self.points_to = Vec::new(); + self.problem = Some(Problem::NotDirOwner { + move_to: RelativePathBuf::from(format!("../{}/mod.rs", mod_name)), + candidate: file_mod, + }); + } } } diff --git a/crates/libanalysis/tests/tests.rs b/crates/libanalysis/tests/tests.rs index 7c2950cccb..e378ab9868 100644 --- a/crates/libanalysis/tests/tests.rs +++ b/crates/libanalysis/tests/tests.rs @@ -1,11 +1,39 @@ extern crate libanalysis; +extern crate relative_path; extern crate test_utils; -use std::path::PathBuf; +use std::path::{Path}; -use libanalysis::{WorldState, FileId}; +use relative_path::RelativePath; +use libanalysis::{WorldState, FileId, FileResolver}; use test_utils::assert_eq_dbg; +struct FileMap(&'static [(u32, &'static str)]); + +impl FileMap { + fn path(&self, id: FileId) -> &'static Path { + let s = self.0.iter() + .find(|it| it.0 == id.0) + .unwrap() + .1; + Path::new(s) + } +} + +impl FileResolver for FileMap { + fn file_stem(&self, id: FileId) -> String { + self.path(id).file_stem().unwrap().to_str().unwrap().to_string() + } + fn resolve(&self, id: FileId, rel: &RelativePath) -> Option { + let path = rel.to_path(self.path(id)); + let path = path.to_str().unwrap(); + let path = RelativePath::new(&path[1..]).normalize(); + let &(id, _) = self.0.iter() + .find(|it| path == RelativePath::new(&it.1[1..]).normalize())?; + Some(FileId(id)) + } +} + #[test] fn test_resolve_module() { @@ -13,14 +41,10 @@ fn test_resolve_module() { world.change_file(FileId(1), Some("mod foo;".to_string())); world.change_file(FileId(2), Some("".to_string())); - let snap = world.snapshot(|id, path| { - assert_eq!(id, FileId(1)); - if path == PathBuf::from("../foo/mod.rs") { - return None; - } - assert_eq!(path, PathBuf::from("../foo.rs")); - Some(FileId(2)) - }); + let snap = world.snapshot(FileMap(&[ + (1, "/lib.rs"), + (2, "/foo.rs"), + ])); let symbols = snap.approximately_resolve_symbol(FileId(1), 4.into()) .unwrap(); assert_eq_dbg( @@ -28,14 +52,10 @@ fn test_resolve_module() { &symbols, ); - let snap = world.snapshot(|id, path| { - assert_eq!(id, FileId(1)); - if path == PathBuf::from("../foo.rs") { - return None; - } - assert_eq!(path, PathBuf::from("../foo/mod.rs")); - Some(FileId(2)) - }); + let snap = world.snapshot(FileMap(&[ + (1, "/lib.rs"), + (2, "/foo/mod.rs") + ])); let symbols = snap.approximately_resolve_symbol(FileId(1), 4.into()) .unwrap(); assert_eq_dbg( @@ -49,11 +69,11 @@ fn test_unresolved_module_diagnostic() { let mut world = WorldState::new(); world.change_file(FileId(1), Some("mod foo;".to_string())); - let snap = world.snapshot(|_id, _path| None); + let snap = world.snapshot(FileMap(&[(1, "/lib.rs")])); let diagnostics = snap.diagnostics(FileId(1)).unwrap(); assert_eq_dbg( r#"[(Diagnostic { range: [4; 7), msg: "unresolved module" }, - Some(CreateFile("../foo.rs")))]"#, + Some(QuickFix { fs_ops: [CreateFile { anchor: FileId(1), path: "../foo.rs" }] }))]"#, &diagnostics, ); } @@ -64,14 +84,10 @@ fn test_resolve_parent_module() { world.change_file(FileId(1), Some("mod foo;".to_string())); world.change_file(FileId(2), Some("".to_string())); - let snap = world.snapshot(|id, path| { - assert_eq!(id, FileId(1)); - if path == PathBuf::from("../foo/mod.rs") { - return None; - } - assert_eq!(path, PathBuf::from("../foo.rs")); - Some(FileId(2)) - }); + let snap = world.snapshot(FileMap(&[ + (1, "/lib.rs"), + (2, "/foo.rs"), + ])); let symbols = snap.parent_module(FileId(2)); assert_eq_dbg( r#"[(FileId(1), FileSymbol { name: "foo", node_range: [0; 8), kind: MODULE })]"#, diff --git a/crates/libeditor/src/scope.rs b/crates/libeditor/src/scope/fn_scope.rs similarity index 100% rename from crates/libeditor/src/scope.rs rename to crates/libeditor/src/scope/fn_scope.rs index 3d398a74c4..e807c6c71f 100644 --- a/crates/libeditor/src/scope.rs +++ b/crates/libeditor/src/scope/fn_scope.rs @@ -9,6 +9,95 @@ use libsyntax2::{ algo::{ancestors, generate, walk::preorder} }; +type ScopeId = usize; + +#[derive(Debug)] +pub struct FnScopes { + scopes: Vec, + scope_for: HashMap, +} + +impl FnScopes { + pub fn new(fn_def: ast::FnDef) -> FnScopes { + let mut scopes = FnScopes { + scopes: Vec::new(), + scope_for: HashMap::new() + }; + let root = scopes.root_scope(); + fn_def.param_list().into_iter() + .flat_map(|it| it.params()) + .filter_map(|it| it.pat()) + .for_each(|it| scopes.add_bindings(root, it)); + + if let Some(body) = fn_def.body() { + compute_block_scopes(body, &mut scopes, root) + } + scopes + } + pub fn entries(&self, scope: ScopeId) -> &[ScopeEntry] { + &self.scopes[scope].entries + } + pub fn scope_chain<'a>(&'a self, node: SyntaxNodeRef) -> impl Iterator + 'a { + generate(self.scope_for(node), move |&scope| self.scopes[scope].parent) + } + fn root_scope(&mut self) -> ScopeId { + let res = self.scopes.len(); + self.scopes.push(ScopeData { parent: None, entries: vec![] }); + res + } + fn new_scope(&mut self, parent: ScopeId) -> ScopeId { + let res = self.scopes.len(); + self.scopes.push(ScopeData { parent: Some(parent), entries: vec![] }); + res + } + fn add_bindings(&mut self, scope: ScopeId, pat: ast::Pat) { + let entries = preorder(pat.syntax()) + .filter_map(ast::BindPat::cast) + .filter_map(ScopeEntry::new); + self.scopes[scope].entries.extend(entries); + } + fn set_scope(&mut self, node: SyntaxNodeRef, scope: ScopeId) { + self.scope_for.insert(node.owned(), scope); + } + fn scope_for(&self, node: SyntaxNodeRef) -> Option { + ancestors(node) + .filter_map(|it| self.scope_for.get(&it.owned()).map(|&scope| scope)) + .next() + } +} + +pub struct ScopeEntry { + syntax: SyntaxNode +} + +impl ScopeEntry { + fn new(pat: ast::BindPat) -> Option { + if pat.name().is_some() { + Some(ScopeEntry { syntax: pat.syntax().owned() }) + } else { + None + } + } + pub fn name(&self) -> SmolStr { + self.ast().name() + .unwrap() + .text() + } + fn ast(&self) -> ast::BindPat { + ast::BindPat::cast(self.syntax.borrowed()) + .unwrap() + } +} + +impl fmt::Debug for ScopeEntry { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("ScopeEntry") + .field("name", &self.name()) + .field("syntax", &self.syntax) + .finish() + } +} + fn compute_block_scopes(block: ast::Block, scopes: &mut FnScopes, mut scope: ScopeId) { for stmt in block.statements() { match stmt { @@ -95,97 +184,8 @@ fn compute_expr_scopes(expr: ast::Expr, scopes: &mut FnScopes, scope: ScopeId) { } } -type ScopeId = usize; - -#[derive(Debug)] -pub struct FnScopes { - scopes: Vec, - scope_for: HashMap, -} - -impl FnScopes { - pub fn new(fn_def: ast::FnDef) -> FnScopes { - let mut scopes = FnScopes { - scopes: Vec::new(), - scope_for: HashMap::new() - }; - let root = scopes.root_scope(); - fn_def.param_list().into_iter() - .flat_map(|it| it.params()) - .filter_map(|it| it.pat()) - .for_each(|it| scopes.add_bindings(root, it)); - - if let Some(body) = fn_def.body() { - compute_block_scopes(body, &mut scopes, root) - } - scopes - } - pub fn entries(&self, scope: ScopeId) -> &[ScopeEntry] { - &self.scopes[scope].entries - } - pub fn scope_chain<'a>(&'a self, node: SyntaxNodeRef) -> impl Iterator + 'a { - generate(self.scope_for(node), move |&scope| self.scopes[scope].parent) - } - fn root_scope(&mut self) -> ScopeId { - let res = self.scopes.len(); - self.scopes.push(ScopeData { parent: None, entries: vec![] }); - res - } - fn new_scope(&mut self, parent: ScopeId) -> ScopeId { - let res = self.scopes.len(); - self.scopes.push(ScopeData { parent: Some(parent), entries: vec![] }); - res - } - fn add_bindings(&mut self, scope: ScopeId, pat: ast::Pat) { - let entries = preorder(pat.syntax()) - .filter_map(ast::BindPat::cast) - .filter_map(ScopeEntry::new); - self.scopes[scope].entries.extend(entries); - } - fn set_scope(&mut self, node: SyntaxNodeRef, scope: ScopeId) { - self.scope_for.insert(node.owned(), scope); - } - fn scope_for(&self, node: SyntaxNodeRef) -> Option { - ancestors(node) - .filter_map(|it| self.scope_for.get(&it.owned()).map(|&scope| scope)) - .next() - } -} - #[derive(Debug)] struct ScopeData { parent: Option, entries: Vec } - -pub struct ScopeEntry { - syntax: SyntaxNode -} - -impl ScopeEntry { - fn new(pat: ast::BindPat) -> Option { - if pat.name().is_some() { - Some(ScopeEntry { syntax: pat.syntax().owned() }) - } else { - None - } - } - pub fn name(&self) -> SmolStr { - self.ast().name() - .unwrap() - .text() - } - fn ast(&self) -> ast::BindPat { - ast::BindPat::cast(self.syntax.borrowed()) - .unwrap() - } -} - -impl fmt::Debug for ScopeEntry { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("ScopeEntry") - .field("name", &self.name()) - .field("syntax", &self.syntax) - .finish() - } -} diff --git a/crates/libeditor/src/scope/mod.rs b/crates/libeditor/src/scope/mod.rs new file mode 100644 index 0000000000..1a77a8b6e2 --- /dev/null +++ b/crates/libeditor/src/scope/mod.rs @@ -0,0 +1,3 @@ +mod fn_scope; + +pub use self::fn_scope::FnScopes; diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 058bf36d23..35ced91ac7 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Aleksey Kladov "] [dependencies] +relative-path = "0.3.7" failure = "0.1.2" serde_json = "1.0.24" serde = "1.0.71" diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 615dd082c7..1a93af65b7 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -18,6 +18,7 @@ extern crate libeditor; extern crate libanalysis; extern crate libsyntax2; extern crate im; +extern crate relative_path; mod io; mod caps; diff --git a/crates/server/src/main_loop/handlers.rs b/crates/server/src/main_loop/handlers.rs index ca5cd5ab16..92ffb30c30 100644 --- a/crates/server/src/main_loop/handlers.rs +++ b/crates/server/src/main_loop/handlers.rs @@ -7,7 +7,8 @@ use languageserver_types::{ CompletionItem, }; use serde_json::{to_value, from_value}; -use libanalysis::{Query, QuickFix, FileId}; +use url_serde; +use libanalysis::{self, Query, FileId}; use libeditor; use libsyntax2::{ TextUnit, @@ -144,24 +145,49 @@ pub fn handle_code_action( if !contains_offset_nonstrict(diag.range, offset) { continue; } - let cmd = match quick_fix { - QuickFix::CreateFile(path) => { - let path = &path.to_str().unwrap()[3..]; // strip `../` b/c url is weird - let uri = params.text_document.uri.join(path) - .unwrap(); - let uri = ::url_serde::Ser::new(&uri); - Command { - title: "Create file".to_string(), - command: "libsyntax-rust.createFile".to_string(), - arguments: Some(vec![to_value(uri).unwrap()]), - } - } + let mut ops = Vec::new(); + for op in quick_fix.fs_ops { + let op = match op { + libanalysis::FsOp::CreateFile { anchor, path } => { + let uri = world.file_id_to_uri(anchor)?; + let path = &path.as_str()[3..]; // strip `../` b/c url is weird + let uri = uri.join(path)?; + FsOp::CreateFile { uri } + }, + libanalysis::FsOp::MoveFile { file, path } => { + let src = world.file_id_to_uri(file)?; + let path = &path.as_str()[3..]; // strip `../` b/c url is weird + let dst = src.join(path)?; + FsOp::MoveFile { src, dst } + }, + }; + ops.push(op) + } + let cmd = Command { + title: "Create module".to_string(), + command: "libsyntax-rust.fsEdit".to_string(), + arguments: Some(vec![to_value(ops).unwrap()]), }; res.push(cmd) } return Ok(Some(res)); } +#[derive(Serialize)] +#[serde(tag = "type", rename_all = "camelCase")] +enum FsOp { + CreateFile { + #[serde(with = "url_serde")] + uri: Url + }, + MoveFile { + #[serde(with = "url_serde")] + src: Url, + #[serde(with = "url_serde")] + dst: Url, + } +} + pub fn handle_runnables( world: ServerWorld, params: req::RunnablesParams, diff --git a/crates/server/src/path_map.rs b/crates/server/src/path_map.rs index d2b811a3ba..f4ac47e70d 100644 --- a/crates/server/src/path_map.rs +++ b/crates/server/src/path_map.rs @@ -1,6 +1,7 @@ use std::path::{PathBuf, Path, Component}; use im; -use libanalysis::{FileId}; +use relative_path::RelativePath; +use libanalysis::{FileId, FileResolver}; #[derive(Debug, Default, Clone)] pub struct PathMap { @@ -34,12 +35,6 @@ impl PathMap { .as_path() } - pub fn resolve(&self, id: FileId, relpath: &Path) -> Option { - let path = self.get_path(id).join(relpath); - let path = normalize(&path); - self.get_id(&path) - } - fn insert(&mut self, path: PathBuf, id: FileId) { self.path2id.insert(path.clone(), id); self.id2path.insert(id, path.clone()); @@ -52,6 +47,18 @@ impl PathMap { } } +impl FileResolver for PathMap { + fn file_stem(&self, id: FileId) -> String { + self.get_path(id).file_stem().unwrap().to_str().unwrap().to_string() + } + + fn resolve(&self, id: FileId, path: &RelativePath) -> Option { + let path = path.to_path(&self.get_path(id)); + let path = normalize(&path); + self.get_id(&path) + } +} + fn normalize(path: &Path) -> PathBuf { let mut components = path.components().peekable(); let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { @@ -89,7 +96,7 @@ mod test { let id1 = m.get_or_insert(PathBuf::from("/foo")); let id2 = m.get_or_insert(PathBuf::from("/foo/bar.rs")); assert_eq!( - m.resolve(id1, &PathBuf::from("bar.rs")), + m.resolve(id1, &RelativePath::new("bar.rs")), Some(id2), ) } diff --git a/crates/server/src/server_world.rs b/crates/server/src/server_world.rs index c0d2efb863..1593cd59f6 100644 --- a/crates/server/src/server_world.rs +++ b/crates/server/src/server_world.rs @@ -87,11 +87,8 @@ impl ServerWorldState { } pub fn snapshot(&self) -> ServerWorld { - let pm = self.path_map.clone(); ServerWorld { - analysis: self.analysis.snapshot(move |id, path| { - pm.resolve(id, path) - }), + analysis: self.analysis.snapshot(self.path_map.clone()), path_map: self.path_map.clone() } }