Original commit: 329534b75c
This commit is contained in:
Michał Wawrzyniec Urbańczyk 2020-07-13 13:58:22 +02:00 committed by GitHub
parent 87c5f28bbb
commit 48b8fea226
14 changed files with 396 additions and 74 deletions

View File

@ -1108,6 +1108,7 @@ dependencies = [
"flo_stream 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"ide-view 0.1.0",
"itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
"js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)",
"json-rpc 0.1.0",
"logger 0.1.0",

View File

@ -31,6 +31,7 @@ console_error_panic_hook = { version = "0.1.6"
failure = { version = "0.1.6" }
flo_stream = { version = "0.4.0" }
futures = { version = "0.3.1" }
itertools = { version = "0.8.1" }
nalgebra = { version = "0.21.1" , features = ["serde-serialize"] }
js-sys = { version = "0.3.35" }
serde = { version = "1.0" , features = ["derive"] }

View File

@ -69,12 +69,13 @@ where for<'t> &'t Shape<Ast> : TryInto<&'t T,Error=E> {
}
/// Updated self in place by applying given function on the stored Shape.
pub fn update_shape(&mut self, f:impl FnOnce(&mut T))
pub fn update_shape<R>(&mut self, f:impl FnOnce(&mut T) -> R) -> R
where T : Clone + Into<Shape<Ast>>,
E : Debug {
let mut shape = self.shape().clone();
f(&mut shape);
self.ast = self.ast.with_shape(shape)
let ret = f(&mut shape);
self.ast = self.ast.with_shape(shape);
ret
}
/// Create new instance of KnownAst with mapped shape.

View File

@ -399,9 +399,10 @@ impl<'de> Deserialize<'de> for Ast {
#[ast(flat)]
#[derive(HasTokens)]
pub enum Shape<T> {
Unrecognized { str : String },
InvalidQuote { quote : Builder },
InlineBlock { quote : Builder },
Unrecognized { str : String },
Unexpected { msg : String, stream:Vec<Shifted<T>> },
InvalidQuote { quote : Builder },
InlineBlock { quote : Builder },
// === Identifiers ===
Blank { },
@ -475,7 +476,7 @@ pub enum Shape<T> {
#[macro_export]
macro_rules! with_shape_variants {
($f:ident) => {
$f! { [Unrecognized] [InvalidQuote] [InlineBlock]
$f! { [Unrecognized] [Unexpected Ast] [InvalidQuote] [InlineBlock]
[Blank] [Var] [Cons] [Opr] [Mod] [InvalidSuffix Ast]
[Number] [DanglingBase]
[TextLineRaw] [TextLineFmt Ast] [TextBlockRaw] [TextBlockFmt Ast] [TextUnclosed Ast]

View File

@ -11,6 +11,35 @@ use crate::known;
// ===============
// === Imports ===
// ===============
/// The keyword introducing an import declaration. See:
/// https://dev.enso.org/docs/enso/syntax/imports.html#import-syntax
pub const IMPORT_KEYWORD:&str = "import";
/// If the given AST node is an import declaration, returns it as a Match (which is the only shape
/// capable of storing import declarations). Returns `None` otherwise.
pub fn ast_as_import_match(ast:&Ast) -> Option<known::Match> {
let macro_match = known::Match::try_from(ast).ok()?;
is_match_import(&macro_match).then(macro_match)
}
/// Check if the given macro match node is an import declaration.
pub fn is_match_import(ast:&known::Match) -> bool {
let segment = &ast.segs.head;
let keyword = crate::identifier::name(&segment.head);
keyword.contains_if(|str| *str == IMPORT_KEYWORD)
}
/// Check if the given ast node is an import declaration.
pub fn is_ast_import(ast:&Ast) -> bool {
ast_as_import_match(ast).is_some()
}
// ===============
// === Lambdas ===
// ===============

View File

@ -174,9 +174,10 @@ has_tokens!(ShiftedVec1<T>, self.head, self.tail);
// === Invalid ===
// ===============
has_tokens!(Unrecognized, self.str );
has_tokens!(InvalidQuote, self.quote);
has_tokens!(InlineBlock , self.quote);
has_tokens!(Unrecognized , self.str);
has_tokens!(Unexpected<T> , self.stream);
has_tokens!(InvalidQuote , self.quote);
has_tokens!(InlineBlock , self.quote);
// ===================

View File

@ -6,6 +6,38 @@ use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn import_utilities() {
use ast::macros::ast_as_import_match;
use ast::macros::is_ast_import;
use ast::macros::is_match_import;
let parser = Parser::new_or_panic();
let expect_import = |code:&str| {
let ast = parser.parse_line(code).unwrap();
assert!(is_ast_import(&ast));
let ast_match = ast_as_import_match(&ast).unwrap();
assert_eq!(&ast,ast_match.ast());
assert!(is_match_import(&ast_match));
};
let expect_not_import = |code:&str| {
let ast = parser.parse_line(code).unwrap();
assert!(!is_ast_import(&ast));
assert!(ast_as_import_match(&ast).is_none());
};
expect_import("import");
expect_import("import Foo");
expect_import("import Foo.Bar");
expect_import("import Foo.Bar.Baz");
expect_not_import("type Foo");
expect_not_import("type Foo as Bar");
expect_not_import("if Foo then Bar else Baz");
expect_not_import("Foo.Bar.Baz");
}
#[wasm_bindgen_test]
fn recognizing_lambdas() {
let parser = Parser::new_or_panic();

View File

@ -1,4 +1,5 @@
#![feature(generators, generator_trait)]
#![feature(matches_macro)]
use parser::prelude::*;
@ -85,6 +86,18 @@ impl Fixture {
});
}
fn deserialize_unexpected(&mut self) {
let unexpected = "import";
let ast = self.parser.parse_line(unexpected).unwrap();
// This does not deserialize to "Unexpected" but to a very complex macro match tree that has
// Unexpected somewhere within. We just make sure that it is somewhere, and that confirms
// that we are able to deserialize such node.
let has_unexpected = ast.iter_recursive().find(|ast| {
matches!(ast.shape(), Shape::Unexpected(_))
});
assert!(has_unexpected.is_some());
}
fn deserialize_invalid_quote(&mut self) {
let unfinished = "'a''";
self.test_shape(unfinished,|shape:&Prefix<Ast>| {
@ -383,6 +396,7 @@ impl Fixture {
self.blank_line_round_trip();
self.deserialize_metadata();
self.deserialize_unrecognized();
self.deserialize_unexpected();
self.deserialize_invalid_quote();
self.deserialize_inline_block();
self.deserialize_blank();

View File

@ -3,6 +3,7 @@
use crate::prelude::*;
use crate::double_representation::text::apply_code_change_to_id_map;
use crate::double_representation::module;
use crate::model::module::Path;
use ast;
@ -13,6 +14,7 @@ use enso_protocol::types::Sha3_224;
use parser::Parser;
// ==============
// === Errors ===
// ==============
@ -129,6 +131,43 @@ impl Handle {
})
}
/// Modify module by modifying its `Info` description (which is a wrapper directly over module's
/// AST).
pub fn modify<R>(&self, f:impl FnOnce(&mut module::Info) -> R) -> R {
let mut module = self.module_info();
let ret = f(&mut module);
self.model.update_ast(module.ast);
ret
}
/// Obtains the `Info` value describing this module's AST.
pub fn module_info(&self) -> module::Info {
let ast = self.model.ast();
double_representation::module::Info {ast}
}
/// Adds a new import to the module.
///
/// May create duplicate entries if such import was already present.
pub fn add_import(&self, target:&module::QualifiedName) {
let import = module::ImportInfo::from_qualified_name(target);
self.modify(|info| info.add_import(&self.parser, import));
}
/// Removes an import declaration that brings given target.
///
/// Fails, if there was no such declaration found.
pub fn remove_import(&self, target:&module::QualifiedName) -> FallibleResult<()> {
let import = module::ImportInfo::from_qualified_name(target);
self.modify(|info| info.remove_import(&import))
}
/// Retrieve a vector describing all import declarations currently present in the module.
pub fn imports(&self) -> Vec<module::ImportInfo> {
let module = self.module_info();
module.iter_imports().collect()
}
/// Creates a mocked module controller.
pub fn new_mock
( path : Path

View File

@ -6,11 +6,204 @@ use crate::double_representation::definition;
use crate::double_representation::definition::DefinitionProvider;
use ast::crumbs::ChildAst;
use ast::crumbs::ModuleCrumb;
use ast::known;
use ast::BlockLine;
use enso_protocol::language_server;
// =====================
// === QualifiedName ===
// =====================
/// Module's qualified name is used in some of the Language Server's APIs, like
/// `VisualisationConfiguration`.
///
/// Qualified name is constructed as follows:
/// `ProjectName.<directories_between_src_and_enso_file>.<file_without_ext>`
///
/// See https://dev.enso.org/docs/distribution/packaging.html for more information about the
/// package structure.
#[derive(Clone,Debug,Display,Shrinkwrap)]
pub struct QualifiedName(String);
impl QualifiedName {
/// Build a module's full qualified name from its name segments and the project name.
///
/// ```
/// use ide::model::module::QualifiedName;
///
/// let name = QualifiedName::from_segments("Project",&["Main"]);
/// assert_eq!(name.to_string(), "Project.Main");
/// ```
pub fn from_segments
(project_name:impl Str, module_segments:impl IntoIterator<Item:AsRef<str>>)
-> QualifiedName {
let project_name = std::iter::once(project_name.into());
let module_segments = module_segments.into_iter();
let module_segments = module_segments.map(|segment| segment.as_ref().to_string());
let mut all_segments = project_name.chain(module_segments);
let name = all_segments.join(".");
QualifiedName(name)
}
}
// ==================
// === ImportInfo ===
// ==================
/// Representation of a single import declaration.
// TODO [mwu]
// Currently only supports the unqualified imports like `import Foo.Bar`. Qualified, restricted and
// and hiding imports are not supported by the parser yet. In future when parser and engine
// supports them, this structure should be adjusted as well.
#[derive(Clone,Debug,PartialEq)]
pub struct ImportInfo {
/// The segments of the qualified name of the imported target.
pub target:Vec<String>
}
impl ImportInfo {
/// Construct from a string describing an import target, like `"Foo.Bar"`.
pub fn from_target_str(name:impl AsRef<str>) -> Self {
let name = name.as_ref().trim();
let target = if name.is_empty() {
Vec::new()
} else {
name.split(ast::opr::predefined::ACCESS).map(Into::into).collect()
};
ImportInfo {target}
}
/// Construct from a module qualified name like `"Foo.Bar"` that describes imported target.
pub fn from_qualified_name(name:&QualifiedName) -> Self {
Self::from_target_str(name.as_str())
}
/// Obtain the qualified name of the imported module.
pub fn qualified_name(&self) -> QualifiedName {
QualifiedName(self.target.join(ast::opr::predefined::ACCESS))
}
/// Construct from an AST. Fails if the Ast is not an import declaration.
pub fn from_ast(ast:&Ast) -> Option<Self> {
let macro_match = known::Match::try_from(ast).ok()?;
Self::from_match(macro_match)
}
/// Construct from a macro match AST. Fails if the Ast is not an import declaration.
pub fn from_match(ast:known::Match) -> Option<Self> {
ast::macros::is_match_import(&ast).then_with(|| {
ImportInfo::from_target_str(ast.segs.head.body.repr().trim())
})
}
}
impl Display for ImportInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}",ast::macros::IMPORT_KEYWORD,self.qualified_name())
}
}
// ==============
// === Errors ===
// ==============
#[derive(Clone,Debug,Fail)]
#[fail(display="Import `{}` was not found in the module.",_0)]
#[allow(missing_docs)]
pub struct ImportNotFound(pub ImportInfo);
#[derive(Clone,Copy,Debug,Fail)]
#[fail(display="Line index is out of bounds.")]
#[allow(missing_docs)]
pub struct LineIndexOutOfBounds;
// ============
// === Info ===
// ============
/// Wrapper allowing getting information about the module and updating it.
#[derive(Clone,Debug)]
pub struct Info {
#[allow(missing_docs)]
pub ast:known::Module,
}
impl Info {
/// Iterate over all lines in module that contain an import declaration.
pub fn enumerate_imports<'a>(&'a self) -> impl Iterator<Item=(ModuleCrumb, ImportInfo)> + 'a {
let children = self.ast.shape().enumerate();
children.filter_map(|(crumb,ast)| Some((crumb,ImportInfo::from_ast(ast)?)))
}
/// Iterate over all import declarations in the module.
///
/// If the caller wants to know *where* the declarations are, use `enumerate_imports`.
pub fn iter_imports<'a>(&'a self) -> impl Iterator<Item=ImportInfo> + 'a {
self.enumerate_imports().map(|(_,import)| import)
}
/// Add a new line to the module's block.
///
/// Note that indices are the "module line" indices, which usually are quite different from text
/// API line indices (because nested blocks doesn't count as separate "module lines").
pub fn add_line(&mut self, index:usize, ast:Option<Ast>) {
let line = BlockLine::new(ast);
self.ast.update_shape(|shape| shape.lines.insert(index,line))
}
/// Remove line with given index.
///
/// Returns removed line. Fails if the index is out of bounds.
pub fn remove_line(&mut self, index:usize) -> FallibleResult<BlockLine<Option<Ast>>> {
self.ast.update_shape(|shape| {
shape.lines.try_remove(index).ok_or_else(|| LineIndexOutOfBounds.into())
})
}
/// Remove a line that matches given import description.
///
/// If there is more than one line matching, only the first one will be removed.
/// Fails if there is no import matching given argument.
pub fn remove_import(&mut self, to_remove:&ImportInfo) -> FallibleResult<()> {
let lookup_result = self.enumerate_imports().find(|(_,import)| import == to_remove);
let (crumb,_) = lookup_result.ok_or_else(|| ImportNotFound(to_remove.clone()))?;
self.remove_line(crumb.line_index)?;
Ok(())
}
/// Add a new import declaration to a module.
// TODO [mwu]
// Ideally we should not require parser but should use some sane way of generating AST from
// the `ImportInfo` value.
pub fn add_import(&mut self, parser:&parser::Parser, to_add:ImportInfo) -> usize {
// Find last import that is not "after" the added one lexicographically.
let previous_import = self.enumerate_imports().take_while(|(_,import)| {
to_add.target > import.target
}).last();
let index_to_place_at = previous_import.map_or(0,|(crumb,_)| crumb.line_index + 1);
let import_ast = parser.parse_line(to_add.to_string()).unwrap();
self.add_line(index_to_place_at,Some(import_ast));
index_to_place_at
}
#[cfg(test)]
pub fn expect_code(&self,expected_code:impl AsRef<str>) {
assert_eq!(self.ast.repr(),expected_code.as_ref());
}
}
// ==============
// === Errors ===
// ==============
@ -104,6 +297,55 @@ mod tests {
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn import_listing() {
let parser = parser::Parser::new_or_panic();
let expect_imports = |code:&str, expected:&[&[&str]]| {
let ast = parser.parse_module(code,default()).unwrap();
let info = Info {ast};
let imports = info.iter_imports().collect_vec();
assert_eq!(imports.len(), expected.len());
for (import,expected_segments) in imports.iter().zip(expected) {
itertools::assert_equal(import.target.iter(),expected_segments.iter());
}
};
expect_imports("import", &[&[]]);
expect_imports("import Foo", &[&["Foo"]]);
expect_imports("import Foo.Bar", &[&["Foo","Bar"]]);
expect_imports("foo = bar\nimport Foo.Bar", &[&["Foo","Bar"]]);
expect_imports("import Foo.Bar\nfoo=bar\nimport Foo.Bar", &[&["Foo","Bar"],&["Foo","Bar"]]);
}
#[wasm_bindgen_test]
fn import_adding_and_removing() {
let parser = parser::Parser::new_or_panic();
let code = "import Foo.Bar.Baz";
let ast = parser.parse_module(code,default()).unwrap();
let mut info = Info { ast };
let import = |code| {
let ast = parser.parse_line(code).unwrap();
ImportInfo::from_ast(&ast).unwrap()
};
info.add_import(&parser,import("import Bar.Gar"));
info.expect_code("import Bar.Gar\nimport Foo.Bar.Baz");
info.add_import(&parser,import("import Gar.Bar"));
info.expect_code("import Bar.Gar\nimport Foo.Bar.Baz\nimport Gar.Bar");
info.remove_import(&ImportInfo::from_target_str("Foo.Bar.Baz")).unwrap();
info.expect_code("import Bar.Gar\nimport Gar.Bar");
info.remove_import(&ImportInfo::from_target_str("Foo.Bar.Baz")).unwrap_err();
info.expect_code("import Bar.Gar\nimport Gar.Bar");
info.remove_import(&ImportInfo::from_target_str("Gar.Bar")).unwrap();
info.expect_code("import Bar.Gar");
info.remove_import(&ImportInfo::from_target_str("Bar.Gar")).unwrap();
info.expect_code("");
info.add_import(&parser,import("import Bar.Gar"));
info.expect_code("import Bar.Gar");
}
#[wasm_bindgen_test]
fn implicit_method_resolution() {
let parser = parser::Parser::new_or_panic();

View File

@ -17,7 +17,7 @@ use parser::Parser;
use serde::Serialize;
use serde::Deserialize;
pub use double_representation::module::QualifiedName;
// ============
// == Errors ==
@ -148,6 +148,26 @@ impl Path {
file : self.file_path.deref().clone(),
}
}
/// Obtain a module's full qualified name from the path and the project name.
///
/// ```
/// use ide::prelude::*;
/// use ide::model::module::QualifiedName;
/// use ide::model::module::Path;
///
/// let path = Path::from_name_segments(default(),&["Main"]).unwrap();
/// assert_eq!(path.to_string(),"//00000000-0000-0000-0000-000000000000/src/Main.enso");
/// let name = path.qualified_module_name("Project");
/// assert_eq!(name.to_string(),"Project.Main");
/// ```
pub fn qualified_module_name(&self, project_name:impl Str) -> QualifiedName {
let non_src_directories = &self.file_path.segments[1..self.file_path.segments.len()-1];
let non_src_directories = non_src_directories.iter().map(|dirname| dirname.as_str());
let module_name = std::iter::once(self.module_name());
let module_segments = non_src_directories.chain(module_name);
QualifiedName::from_segments(project_name,module_segments)
}
}
impl PartialEq<FilePath> for Path {
@ -179,65 +199,6 @@ impl TryFrom<MethodPointer> for Path {
}
// ===========================
// === ModuleQualifiedName ===
// ===========================
/// Module's qualified name is used in some of the Language Server's APIs, like
/// `VisualisationConfiguration`.
///
/// Qualified name is constructed as follows:
/// `ProjectName.<directories_between_src_and_enso_file>.<file_without_ext>`
///
/// See https://dev.enso.org/docs/distribution/packaging.html for more information about the
/// package structure.
#[derive(Clone,Debug,Shrinkwrap)]
pub struct QualifiedName(String);
impl QualifiedName {
/// Obtain a module's full qualified name from its path and the project name.
///
/// ```
/// use ide::prelude::*;
/// use ide::model::module::QualifiedName;
/// use ide::model::module::Path;
///
/// let path = Path::from_name_segments(default(),&["Main"]).unwrap();
/// assert_eq!(path.to_string(),"//00000000-0000-0000-0000-000000000000/src/Main.enso");
/// let name = QualifiedName::from_path(&path,"Project");
/// assert_eq!(name.to_string(),"Project.Main");
/// ```
pub fn from_path(path:&Path, project_name:impl Str) -> QualifiedName {
let non_src_directories = &path.file_path.segments[1..path.file_path.segments.len()-1];
let non_src_directories = non_src_directories.iter().map(|dirname| dirname.as_str());
let module_name = std::iter::once(path.module_name());
let module_segments = non_src_directories.chain(module_name);
Self::from_module_segments(module_segments,project_name)
}
/// Obtain a module's full qualified name from its path and the project name.
///
/// ```
/// use ide::model::module::QualifiedName;
///
/// let name = QualifiedName::from_module_segments(&["Main"],"Project");
/// assert_eq!(name.to_string(), "Project.Main");
/// ```
pub fn from_module_segments
(module_segments:impl IntoIterator<Item:AsRef<str>>, project_name:impl Str)
-> QualifiedName {
let project_name = std::iter::once(project_name.into());
let module_segments = module_segments.into_iter();
let module_segments = module_segments.map(|segment| segment.as_ref().to_string());
let mut all_segments = project_name.chain(module_segments);
let name = all_segments.join(".");
QualifiedName(name)
}
}
// ====================
// === Notification ===
// ====================
@ -589,7 +550,7 @@ mod test {
let root_id = default();
let file_path = FilePath::new(root_id, &["src", "Foo", "Bar.enso"]);
let module_path = Path::from_file_path(file_path).unwrap();
let qualified = QualifiedName::from_path(&module_path,project_name);
let qualified = module_path.qualified_module_name(project_name);
assert_eq!(*qualified, "P.Foo.Bar");
}
}

View File

@ -279,7 +279,7 @@ impl Project {
/// Generates full module's qualified name that includes the leading project name segment.
pub fn qualified_module_name(&self, path:&model::module::Path) -> ModuleQualifiedName {
ModuleQualifiedName::from_path(path,self.name.deref())
path.qualified_module_name(self.name.deref())
}
fn load_module(&self, path:ModulePath)

View File

@ -335,7 +335,7 @@ pub mod tests {
}
pub fn module_qualified_name(&self) -> ModuleQualifiedName {
ModuleQualifiedName::from_path(&self.module_path,&self.project_name)
self.module_path.qualified_module_name(&self.project_name)
}
pub fn definition_id(&self) -> model::execution_context::DefinitionId {

View File

@ -588,7 +588,7 @@ impl GraphEditorIntegratedWithControllerModel {
// to the customised values.
let project_name = self.project.name.as_ref();
let module_name = crate::view::project::INITIAL_MODULE_NAME;
let visualisation_module = QualifiedName::from_module_segments(&[module_name],project_name);
let visualisation_module = QualifiedName::from_segments(project_name,&[module_name]);
let id = VisualizationId::new_v4();
let expression = crate::constants::SERIALIZE_TO_JSON_EXPRESSION.into();
let ast_id = self.get_controller_node_id(*node_id)?;