create module smartly

This commit is contained in:
Aleksey Kladov 2018-08-28 18:22:52 +03:00
parent 748a4cacd2
commit d34588bf83
12 changed files with 344 additions and 182 deletions

View file

@ -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;
}

View file

@ -4,6 +4,7 @@ version = "0.1.0"
authors = ["Aleksey Kladov <aleksey.kladov@gmail.com>"]
[dependencies]
relative-path = "0.3.7"
log = "0.4.2"
failure = "0.1.2"
parking_lot = "0.6.3"

View file

@ -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<T> = ::std::result::Result<T, ::failure::Error>;
pub type FileResolver = dyn Fn(FileId, &Path) -> Option<FileId> + Send + Sync;
pub trait FileResolver: Send + Sync + 'static {
fn file_stem(&self, id: FileId) -> String;
fn resolve(&self, id: FileId, path: &RelativePath) -> Option<FileId>;
}
#[derive(Debug)]
pub struct WorldState {
@ -84,7 +88,7 @@ impl WorldState {
pub fn snapshot(
&self,
file_resolver: impl Fn(FileId, &Path) -> Option<FileId> + '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<FsOp>,
}
#[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::<Vec<_>>();
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)
}

View file

@ -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<ModuleId>,
problem: Option<Problem>,
}
#[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<PathBuf> {
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,
});
}
}
}

View file

@ -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<FileId> {
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 })]"#,

View file

@ -9,6 +9,95 @@ use libsyntax2::{
algo::{ancestors, generate, walk::preorder}
};
type ScopeId = usize;
#[derive(Debug)]
pub struct FnScopes {
scopes: Vec<ScopeData>,
scope_for: HashMap<SyntaxNode, ScopeId>,
}
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<Item=ScopeId> + '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<ScopeId> {
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<ScopeEntry> {
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<ScopeData>,
scope_for: HashMap<SyntaxNode, ScopeId>,
}
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<Item=ScopeId> + '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<ScopeId> {
ancestors(node)
.filter_map(|it| self.scope_for.get(&it.owned()).map(|&scope| scope))
.next()
}
}
#[derive(Debug)]
struct ScopeData {
parent: Option<ScopeId>,
entries: Vec<ScopeEntry>
}
pub struct ScopeEntry {
syntax: SyntaxNode
}
impl ScopeEntry {
fn new(pat: ast::BindPat) -> Option<ScopeEntry> {
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()
}
}

View file

@ -0,0 +1,3 @@
mod fn_scope;
pub use self::fn_scope::FnScopes;

View file

@ -4,6 +4,7 @@ version = "0.1.0"
authors = ["Aleksey Kladov <aleksey.kladov@gmail.com>"]
[dependencies]
relative-path = "0.3.7"
failure = "0.1.2"
serde_json = "1.0.24"
serde = "1.0.71"

View file

@ -18,6 +18,7 @@ extern crate libeditor;
extern crate libanalysis;
extern crate libsyntax2;
extern crate im;
extern crate relative_path;
mod io;
mod caps;

View file

@ -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,

View file

@ -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<FileId> {
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<FileId> {
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),
)
}

View file

@ -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()
}
}