From 7d4529885b7f87926b34bd0a936f3e0af797eb00 Mon Sep 17 00:00:00 2001 From: Adam Obuchowicz Date: Wed, 13 May 2020 13:57:36 +0200 Subject: [PATCH] Execution Context (https://github.com/enso-org/ide/pull/419) This PR introduces Executed Graph Controller, which is a Graph Controller with additional info about execution context. Original commit: https://github.com/enso-org/ide/commit/026a2585aec59d606933f00e3ce591b1696f928c --- .../ide/enso-protocol/src/language_server.rs | 12 +- .../src/language_server/response.rs | 1 + .../src/language_server/tests.rs | 7 +- .../src/language_server/types.rs | 19 +- gui/src/rust/ide/json-rpc/src/macros.rs | 68 ++++-- gui/src/rust/ide/src/controller.rs | 18 +- gui/src/rust/ide/src/controller/graph.rs | 12 +- .../rust/ide/src/controller/graph/executed.rs | 30 +++ gui/src/rust/ide/src/controller/module.rs | 102 +++++++- gui/src/rust/ide/src/controller/project.rs | 49 ++-- gui/src/rust/ide/src/controller/text.rs | 14 +- .../ide/src/double_representation/graph.rs | 2 + gui/src/rust/ide/src/model.rs | 3 + .../rust/ide/src/model/execution_context.rs | 93 ++++++++ gui/src/rust/ide/src/model/module/registry.rs | 4 +- gui/src/rust/ide/src/model/synchronized.rs | 6 + .../model/synchronized/execution_context.rs | 222 ++++++++++++++++++ gui/src/rust/ide/src/view/layout.rs | 8 +- gui/src/rust/ide/src/view/node_editor.rs | 19 +- gui/src/rust/ide/src/view/project.rs | 15 +- gui/src/rust/ide/tests/language_server.rs | 4 +- 21 files changed, 595 insertions(+), 113 deletions(-) create mode 100644 gui/src/rust/ide/src/controller/graph/executed.rs create mode 100644 gui/src/rust/ide/src/model/execution_context.rs create mode 100644 gui/src/rust/ide/src/model/synchronized.rs create mode 100644 gui/src/rust/ide/src/model/synchronized/execution_context.rs diff --git a/gui/src/rust/ide/enso-protocol/src/language_server.rs b/gui/src/rust/ide/enso-protocol/src/language_server.rs index c64cb0c041..d2ebe09cc5 100644 --- a/gui/src/rust/ide/enso-protocol/src/language_server.rs +++ b/gui/src/rust/ide/enso-protocol/src/language_server.rs @@ -130,14 +130,14 @@ trait API { fn destroy_execution_context(&self, context_id:ContextId) -> (); /// Move the execution context to a new location deeper down the stack. - #[MethodInput=PushExecutionContextInput,rpc_name="executionContext/push", - result=push_execution_context_result,set_result=set_push_execution_context_result] - fn push_execution_context(&self, context_id:ContextId, stack_item:StackItem) -> (); + #[MethodInput=PushToExecutionContextInput,rpc_name="executionContext/push", + result=push_to_execution_context_result,set_result=set_push_to_execution_context_result] + fn push_to_execution_context(&self, context_id:ContextId, stack_item:StackItem) -> (); /// Move the execution context up the stack. - #[MethodInput=PopExecutionContextInput,rpc_name="executionContext/pop", - result=pop_execution_context_result,set_result=set_pop_execution_context_result] - fn pop_execution_context(&self, context_id:ContextId) -> (); + #[MethodInput=PopFromExecutionContextInput,rpc_name="executionContext/pop", + result=pop_from_execution_context_result,set_result=set_pop_from_execution_context_result] + fn pop_from_execution_context(&self, context_id:ContextId) -> (); /// Attach a visualisation, potentially preprocessed by some arbitrary Enso code, to a given /// node in the program. diff --git a/gui/src/rust/ide/enso-protocol/src/language_server/response.rs b/gui/src/rust/ide/enso-protocol/src/language_server/response.rs index f68a1220aa..df1aefe556 100644 --- a/gui/src/rust/ide/enso-protocol/src/language_server/response.rs +++ b/gui/src/rust/ide/enso-protocol/src/language_server/response.rs @@ -52,6 +52,7 @@ pub struct OpenTextFile { #[serde(rename_all = "camelCase")] #[allow(missing_docs)] pub struct CreateExecutionContext { + pub context_id : ContextId, pub can_modify : CapabilityRegistration, pub receives_updates : CapabilityRegistration } diff --git a/gui/src/rust/ide/enso-protocol/src/language_server/tests.rs b/gui/src/rust/ide/enso-protocol/src/language_server/tests.rs index c1bf41ed69..3e5da64e8b 100644 --- a/gui/src/rust/ide/enso-protocol/src/language_server/tests.rs +++ b/gui/src/rust/ide/enso-protocol/src/language_server/tests.rs @@ -322,12 +322,13 @@ fn test_execution_context() { let method = "executionContext/receivesUpdates".to_string(); let receives_updates = CapabilityRegistration{method,register_options}; let create_execution_context_response = response::CreateExecutionContext - {can_modify,receives_updates}; + {context_id,can_modify,receives_updates}; test_request( |client| client.create_execution_context(), "executionContext/create", json!({}), json!({ + "contextId" : "00000000-0000-0000-0000-000000000000", "canModify" : { "method" : "executionContext/canModify", "registerOptions" : { @@ -354,7 +355,7 @@ fn test_execution_context() { let local_call = LocalCall {expression_id}; let stack_item = StackItem::LocalCall(local_call); test_request( - |client| client.push_execution_context(context_id,stack_item), + |client| client.push_to_execution_context(context_id,stack_item), "executionContext/push", json!({ "contextId" : "00000000-0000-0000-0000-000000000000", @@ -367,7 +368,7 @@ fn test_execution_context() { () ); test_request( - |client| client.pop_execution_context(context_id), + |client| client.pop_from_execution_context(context_id), "executionContext/pop", json!({"contextId":"00000000-0000-0000-0000-000000000000"}), unit_json.clone(), diff --git a/gui/src/rust/ide/enso-protocol/src/language_server/types.rs b/gui/src/rust/ide/enso-protocol/src/language_server/types.rs index deda174d79..1e0cf39fce 100644 --- a/gui/src/rust/ide/enso-protocol/src/language_server/types.rs +++ b/gui/src/rust/ide/enso-protocol/src/language_server/types.rs @@ -24,6 +24,7 @@ pub struct Path { pub root_id:Uuid, /// Path's segments. pub segments:Vec, + } impl Display for Path { @@ -34,12 +35,24 @@ impl Display for Path { } impl Path { + /// Returns the file name, i.e. the last segment if exists. + pub fn file_name(&self) -> Option<&String> { + self.segments.last() + } + /// Returns the file extension, i.e. the part of last path segment after the last dot. /// Returns `None` is there is no segments or no dot in the last segment. pub fn extension(&self) -> Option<&str> { - let segment = self.segments.last()?; - let last_dot_index = segment.rfind('.')?; - Some(&segment[last_dot_index + 1..]) + let name = self.file_name()?; + let last_dot_index = name.rfind('.')?; + Some(&name[last_dot_index + 1..]) + } + + /// Returns the stem of filename, i.e. part of last segment without extension if present. + pub fn file_stem(&self) -> Option<&str> { + let name = self.file_name()?; + let name_length = name.rfind('.').unwrap_or_else(|| name.len()); + Some(&name[..name_length]) } /// Constructs a new path from given root ID and segments. diff --git a/gui/src/rust/ide/json-rpc/src/macros.rs b/gui/src/rust/ide/json-rpc/src/macros.rs index 362c1b20b7..e8a8cc0077 100644 --- a/gui/src/rust/ide/json-rpc/src/macros.rs +++ b/gui/src/rust/ide/json-rpc/src/macros.rs @@ -60,25 +60,25 @@ macro_rules! make_rpc_methods { } impl Client { - /// Create a new client that will use given transport. - pub fn new(transport:impl json_rpc::Transport + 'static) -> Self { - let handler = RefCell::new(Handler::new(transport)); - Self { handler } - } + /// Create a new client that will use given transport. + pub fn new(transport:impl json_rpc::Transport + 'static) -> Self { + let handler = RefCell::new(Handler::new(transport)); + Self { handler } + } - /// Asynchronous event stream with notification and errors. - /// - /// On a repeated call, previous stream is closed. - pub fn events(&self) -> impl Stream { - self.handler.borrow_mut().handler_event_stream() - } + /// Asynchronous event stream with notification and errors. + /// + /// On a repeated call, previous stream is closed. + pub fn events(&self) -> impl Stream { + self.handler.borrow_mut().handler_event_stream() + } - /// Returns a future that performs any background, asynchronous work needed - /// for this Client to correctly work. Should be continually run while the - /// `Client` is used. Will end once `Client` is dropped. - pub fn runner(&self) -> impl Future { - self.handler.borrow_mut().runner() - } + /// Returns a future that performs any background, asynchronous work needed + /// for this Client to correctly work. Should be continually run while the + /// `Client` is used. Will end once `Client` is dropped. + pub fn runner(&self) -> impl Future { + self.handler.borrow_mut().runner() + } } impl API for Client { @@ -112,15 +112,18 @@ macro_rules! make_rpc_methods { /// Mock used for tests. #[derive(Debug,Default)] pub struct MockClient { - $($method_result : RefCell>>,)* + expect_all_calls : Cell, + $($method_result : RefCell>>>,)* } impl API for MockClient { $(fn $method(&self $(,$param_name:$param_ty)*) -> std::pin::Pin>>> { - let mut result = self.$method_result.borrow_mut(); - let result = result.remove(&($($param_name),*)).unwrap(); - Box::pin(async move { result }) + let mut results = self.$method_result.borrow_mut(); + let params = ($($param_name),*); + let result = results.get_mut(¶ms).and_then(|res| res.pop()); + let err = format!("Unrecognized call {} with params {:?}",$rpc_name,params); + Box::pin(futures::future::ready(result.expect(err.as_str()))) })* } @@ -128,9 +131,30 @@ macro_rules! make_rpc_methods { $( /// Sets `$method`'s result to be returned when it is called. pub fn $set_result(&self $(,$param_name:$param_ty)*, result:Result<$result>) { - self.$method_result.borrow_mut().insert(($($param_name),*),result); + let mut results = self.$method_result.borrow_mut(); + let mut entry = results.entry(($($param_name),*)); + entry.or_default().push(result); } )* + + /// Mark all calls defined by `set_$method_result` as required. If client will be + /// dropped without calling the test will fail. + pub fn expect_all_calls(&self) { + self.expect_all_calls.set(true); + } + } + + impl Drop for MockClient { + fn drop(&mut self) { + if self.expect_all_calls.get() { + $( + for (params,results) in self.$method_result.borrow().iter() { + assert!(results.is_empty(), "Didn't make expected call {} with \ + parameters {:?}",$rpc_name,params); + } + )* + } + } } } } diff --git a/gui/src/rust/ide/src/controller.rs b/gui/src/rust/ide/src/controller.rs index 316787b274..00854083de 100644 --- a/gui/src/rust/ide/src/controller.rs +++ b/gui/src/rust/ide/src/controller.rs @@ -20,7 +20,17 @@ pub mod module; pub mod project; pub mod text; -pub use graph::Handle as Graph; -pub use module::Handle as Module; -pub use project::Handle as Project; -pub use text::Handle as Text; +pub use graph::Handle as Graph; +pub use graph::executed::Handle as ExecutedGraph; +pub use module::Handle as Module; +pub use project::Handle as Project; +pub use text::Handle as Text; + + + +// ============ +// === Path === +// ============ + +/// Path to a file on disc, used across all controllers +pub type FilePath = enso_protocol::language_server::Path; diff --git a/gui/src/rust/ide/src/controller/graph.rs b/gui/src/rust/ide/src/controller/graph.rs index add4654d22..bb8e839913 100644 --- a/gui/src/rust/ide/src/controller/graph.rs +++ b/gui/src/rust/ide/src/controller/graph.rs @@ -2,6 +2,7 @@ //! //! This controller provides access to a specific graph. It lives under a module controller, as //! each graph belongs to some module. +pub mod executed; use crate::prelude::*; @@ -23,7 +24,6 @@ use span_tree::SpanTree; use ast::crumbs::InfixCrumb; - // ============== // === Errors === // ============== @@ -395,18 +395,14 @@ pub struct Handle { impl Handle { /// Creates a new controller. Does not check if id is valid. - /// - /// Requires global executor to spawn the events relay task. pub fn new_unchecked(module:Rc, parser:Parser, id:Id) -> Handle { - let id = Rc::new(id); + let id = Rc::new(id); let logger = Logger::new(format!("Graph Controller {}", id)); Handle {module,parser,id,logger} } /// Creates a new graph controller. Given ID should uniquely identify a definition in the /// module. Fails if ID cannot be resolved. - /// - /// Requires global executor to spawn the events relay task. pub fn new(module:Rc, parser:Parser, id:Id) -> FallibleResult { let ret = Self::new_unchecked(module,parser,id); // Get and discard definition info, we are just making sure it can be obtained. @@ -751,7 +747,7 @@ mod tests { Fut : Future { let code = code.as_ref(); let ls = language_server::Connection::new_mock_rc(default()); - let path = controller::module::Path::new(default(),&["Main"]); + let path = controller::module::Path::from_module_name("Main"); let parser = Parser::new_or_panic(); let module = controller::Module::new_mock(path,code,default(),ls,parser).unwrap(); let graph_id = Id::new_single_crumb(DefinitionName::new_plain(function_name.into())); @@ -766,7 +762,7 @@ mod tests { Fut : Future { let code = code.as_ref(); let ls = language_server::Connection::new_mock_rc(default()); - let path = controller::module::Path::new(default(),&["Main"]); + let path = controller::module::Path::from_module_name("Main"); let parser = Parser::new_or_panic(); let module = controller::Module::new_mock(path, code, default(), ls, parser).unwrap(); let graph = module.graph_controller(graph_id).unwrap(); diff --git a/gui/src/rust/ide/src/controller/graph/executed.rs b/gui/src/rust/ide/src/controller/graph/executed.rs new file mode 100644 index 0000000000..0f05cafd16 --- /dev/null +++ b/gui/src/rust/ide/src/controller/graph/executed.rs @@ -0,0 +1,30 @@ +//! A module with Executed Graph Controller. +//! +//! This controller provides operations on a specific graph with some execution context - these +//! operations usually involves retrieving values on nodes: that's are i.e. operations on +//! visualisations, retrieving types on ports, etc. +use crate::prelude::*; + +use crate::model::synchronized::ExecutionContext; + +/// Handle providing executed graph controller interface. +#[derive(Clone,CloneRef,Debug)] +pub struct Handle { + /// A handle to basic graph operations. + pub graph : controller::Graph, + execution_ctx : Rc, +} + +impl Handle { + /// Create handle for given graph and execution context. + /// + /// This takes ownership of execution context which will be shared between all copies of this + /// handle; when all copies will be dropped, the execution context will be dropped as well + /// (and will then removed from LanguageServer). + pub fn new(graph:controller::Graph, execution_ctx:ExecutionContext) -> Self { + let execution_ctx = Rc::new(execution_ctx); + Handle{graph,execution_ctx} + } + + //TODO[ao] Here goes the methods requiring ContextId +} diff --git a/gui/src/rust/ide/src/controller/module.rs b/gui/src/rust/ide/src/controller/module.rs index a5959df5d9..20d5c90201 100644 --- a/gui/src/rust/ide/src/controller/module.rs +++ b/gui/src/rust/ide/src/controller/module.rs @@ -7,7 +7,9 @@ use crate::prelude::*; +use crate::controller::FilePath; use crate::double_representation::text::apply_code_change_to_id_map; +use crate::model::synchronized::ExecutionContext; use ast; use ast::HasIdMap; @@ -15,6 +17,23 @@ use data::text::*; use double_representation as dr; use enso_protocol::language_server; use parser::Parser; +use failure::_core::fmt::Formatter; + + + +// ============== +// === Errors === +// ============== + +/// Error returned when module path is invalid, i.e. cannot obtain module name from it. +#[derive(Clone,Copy,Debug,Fail)] +#[fail(display="Invalid module path.")] +pub struct InvalidModulePath {} + +/// Error returned when graph id invalid. +#[derive(Clone,Debug,Fail)] +#[fail(display="Invalid graph id: {:?}.",_0)] +pub struct InvalidGraphId(controller::graph::Id); @@ -23,7 +42,55 @@ use parser::Parser; // ============ /// Path identifying module's file in the Language Server. -pub type Path = language_server::Path; +#[derive(Clone,Debug,Eq,Hash,PartialEq)] +pub struct Path { + file_path : FilePath, +} + +impl Path { + /// Create a path from the file path. Returns None if given path is not a valid module file. + pub fn from_file_path(file_path:FilePath) -> Option { + let has_proper_ext = file_path.extension() == Some(constants::LANGUAGE_FILE_EXTENSION); + let capitalized_name = file_path.file_name()?.chars().next()?.is_uppercase(); + let is_module = has_proper_ext && capitalized_name; + is_module.and_option_from(|| Some(Path{file_path})) + } + + /// Get the file path. + pub fn file_path(&self) -> &FilePath { + &self.file_path + } + + /// Get the module name from path. + /// + /// The module name is a filename without extension. + pub fn module_name(&self) -> &str { + // The file stem existence should be checked during construction. + self.file_path.file_stem().unwrap() + } + /// Create a module path consisting of a single segment, based on a given module name. + #[cfg(test)] + pub fn from_module_name(name:impl Str) -> Self { + let name:String = name.into(); + let file_name = format!("{}.{}",name,constants::LANGUAGE_FILE_EXTENSION); + let file_path = FilePath::new(default(),&[file_name]); + Self::from_file_path(file_path).unwrap() + } +} + +impl TryFrom for Path { + type Error = InvalidModulePath; + + fn try_from(value:FilePath) -> Result { + Path::from_file_path(value).ok_or(InvalidModulePath{}) + } +} + +impl Display for Path { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.file_path, f) + } +} @@ -59,7 +126,7 @@ impl Handle { /// Load or reload module content from file. pub async fn load_file(&self) -> FallibleResult<()> { self.logger.info(|| "Loading module file"); - let path = self.path.deref().clone(); + let path = self.path.file_path().clone(); let content = self.language_server.client.read_file(path).await?.contents; self.logger.info(|| "Parsing code"); // TODO[ao] We should not fail here when metadata are malformed, but discard them and set @@ -73,7 +140,7 @@ impl Handle { /// Save the module to file. pub fn save_file(&self) -> impl Future> { - let path = self.path.deref().clone(); + let path = self.path.file_path().clone(); let ls = self.language_server.clone(); let content = self.model.source_as_string(); async move { Ok(ls.client.write_file(path,content?).await?) } @@ -122,6 +189,20 @@ impl Handle { controller::Graph::new(self.model.clone_ref(), self.parser.clone_ref(), id) } + /// Returns a executed graph controller for graph in this module's subtree identified by id. + /// The execution context will be rooted at definition of this graph. + /// + /// This function wont check if the definition under id exists. + pub async fn executed_graph_controller_unchecked + (&self, id:dr::graph::Id) -> FallibleResult { + let definition_name = id.crumbs.last().cloned().ok_or_else(|| InvalidGraphId(id.clone()))?; + let graph = self.graph_controller_unchecked(id); + let language_server = self.language_server.clone_ref(); + let path = self.path.clone_ref(); + let execution_ctx = ExecutionContext::create(language_server,path,definition_name).await?; + Ok(controller::ExecutedGraph::new(graph,execution_ctx)) + } + /// Returns a graph controller for graph in this module's subtree identified by `id` without /// checking if the graph exists. pub fn graph_controller_unchecked(&self, id:dr::graph::Id) -> controller::Graph { @@ -167,17 +248,28 @@ mod test { use ast::BlockLine; use ast::Ast; use data::text::Span; - use enso_protocol::language_server; use parser::Parser; use uuid::Uuid; use wasm_bindgen_test::wasm_bindgen_test; + #[test] + fn module_path_conversion() { + let path = FilePath::new(default(), &["src","Main.enso"]); + assert!(Path::from_file_path(path).is_some()); + + let path = FilePath::new(default(), &["src","Main.txt"]); + assert!(Path::from_file_path(path).is_none()); + + let path = FilePath::new(default(), &["src","main.txt"]); + assert!(Path::from_file_path(path).is_none()); + } + #[wasm_bindgen_test] fn update_ast_after_text_change() { TestWithLocalPoolExecutor::set_up().run_task(async { let ls = language_server::Connection::new_mock_rc(default()); let parser = Parser::new().unwrap(); - let location = Path{root_id:default(),segments:vec!["Test".into()]}; + let location = Path::from_module_name("Test"); let uuid1 = Uuid::new_v4(); let uuid2 = Uuid::new_v4(); diff --git a/gui/src/rust/ide/src/controller/project.rs b/gui/src/rust/ide/src/controller/project.rs index fb9a167335..5c561858ad 100644 --- a/gui/src/rust/ide/src/controller/project.rs +++ b/gui/src/rust/ide/src/controller/project.rs @@ -5,6 +5,8 @@ use crate::prelude::*; +use crate::controller::FilePath; + use enso_protocol::language_server; use parser::Parser; @@ -40,9 +42,8 @@ impl Handle { /// Returns a text controller for a given file path. /// /// It supports both modules and plain text files. - pub async fn text_controller - (&self, path:language_server::Path) -> FallibleResult { - if is_path_to_module(&path) { + pub async fn text_controller(&self, path:FilePath) -> FallibleResult { + if let Some(path) = controller::module::Path::from_file_path(path.clone()) { trace!(self.logger,"Obtaining controller for module {path}"); let module = self.module_controller(path).await?; Ok(controller::Text::new_for_module(module)) @@ -77,16 +78,16 @@ impl Handle { } } -/// Checks if the given path looks like it is referring to module file. -fn is_path_to_module(path:&language_server::Path) -> bool { - path.extension() == Some(constants::LANGUAGE_FILE_EXTENSION) -} + + +// ============ +// === Test === +// ============ #[cfg(test)] mod test { use super::*; - use crate::controller::text::FilePath; use crate::executor::test_utils::TestWithLocalPoolExecutor; use language_server::response; @@ -96,28 +97,20 @@ mod test { wasm_bindgen_test_configure!(run_in_browser); - - #[test] - fn is_path_to_module_test() { - let path = language_server::Path::new(default(), &["src","Main.enso"]); - assert!(is_path_to_module(&path)); - - let path = language_server::Path::new(default(), &["src","Main.txt"]); - assert_eq!(is_path_to_module(&path), false); - } - #[wasm_bindgen_test] fn obtain_module_controller() { let mut test = TestWithLocalPoolExecutor::set_up(); test.run_task(async move { - let path = ModulePath{root_id:default(),segments:vec!["TestLocation".into()]}; - let another_path = ModulePath{root_id:default(),segments:vec!["TestLocation2".into()]}; + let path = ModulePath::from_module_name("TestModule"); + let another_path = ModulePath::from_module_name("TestModule2"); - let client = language_server::MockClient::default(); - let contents = "2+2".to_string(); - client.set_file_read_result(path.clone(),Ok(response::Read{contents})); - let contents = "2 + 2".to_string(); - client.set_file_read_result(another_path.clone(),Ok(response::Read{contents})); + let client = language_server::MockClient::default(); + let contents = "2+2".to_string(); + let file_path = path.file_path().clone(); + client.set_file_read_result(file_path,Ok(response::Read{contents})); + let file_path = another_path.file_path().clone(); + let contents = "2 + 2".to_string(); + client.set_file_read_result(file_path,Ok(response::Read{contents})); let connection = language_server::Connection::new_mock(client); let project = controller::Project::new(connection); let module = project.module_controller(path.clone()).await.unwrap(); @@ -136,8 +129,8 @@ mod test { let connection = language_server::Connection::new_mock(default()); let project_ctrl = controller::Project::new(connection); let root_id = default(); - let path = FilePath{root_id,segments:vec!["TestPath".into()]}; - let another_path = FilePath{root_id,segments:vec!["TestPath2".into()]}; + let path = FilePath::new(root_id,&["TestPath"]); + let another_path = FilePath::new(root_id,&["TestPath2"]); let text_ctrl = project_ctrl.text_controller(path.clone()).await.unwrap(); let another_ctrl = project_ctrl.text_controller(another_path.clone()).await.unwrap(); @@ -156,7 +149,7 @@ mod test { let mut test = TestWithLocalPoolExecutor::set_up(); test.run_task(async move { let file_name = format!("test.{}",constants::LANGUAGE_FILE_EXTENSION); - let path = ModulePath{root_id:default(),segments:vec![file_name]}; + let path = FilePath::new(default(),&[file_name]); let contents = "2 + 2".to_string(); let client = language_server::MockClient::default(); diff --git a/gui/src/rust/ide/src/controller/text.rs b/gui/src/rust/ide/src/controller/text.rs index f64ff28ff3..985eabfc27 100644 --- a/gui/src/rust/ide/src/controller/text.rs +++ b/gui/src/rust/ide/src/controller/text.rs @@ -6,6 +6,7 @@ use crate::prelude::*; +use crate::controller::FilePath; use crate::notification; use data::text::TextChange; @@ -15,15 +16,6 @@ use std::pin::Pin; -// ============ -// === Path === -// ============ - -/// Path to a file on disc. -pub type FilePath = language_server::Path; - - - // ======================= // === Text Controller === // ======================= @@ -68,7 +60,7 @@ impl Handle { pub fn file_path(&self) -> &FilePath { match &self.file { FileHandle::PlainText{path,..} => &*path, - FileHandle::Module{controller} => &*controller.path, + FileHandle::Module{controller} => controller.path.file_path() } } @@ -162,7 +154,7 @@ mod test { let mut test = TestWithLocalPoolExecutor::set_up(); test.run_task(async move { let ls = language_server::Connection::new_mock_rc(default()); - let path = FilePath{root_id:default(),segments:vec!["test".into()]}; + let path = controller::module::Path::from_module_name("Test"); let parser = Parser::new().unwrap(); let module_res = controller::Module::new_mock(path,"main = 2+2",default(),ls,parser); let module = module_res.unwrap(); diff --git a/gui/src/rust/ide/src/double_representation/graph.rs b/gui/src/rust/ide/src/double_representation/graph.rs index 85c637a515..3c69cbeee8 100644 --- a/gui/src/rust/ide/src/double_representation/graph.rs +++ b/gui/src/rust/ide/src/double_representation/graph.rs @@ -13,6 +13,8 @@ use ast::known; use utils::fail::FallibleResult; use crate::double_representation::connection::Connection; + + /// Graph uses the same `Id` as the definition which introduces the graph. pub type Id = double_representation::definition::Id; diff --git a/gui/src/rust/ide/src/model.rs b/gui/src/rust/ide/src/model.rs index 826a36f725..92bfb91d37 100644 --- a/gui/src/rust/ide/src/model.rs +++ b/gui/src/rust/ide/src/model.rs @@ -1,5 +1,8 @@ //! The module with structures describing models shared between different controllers. +pub mod execution_context; pub mod module; +pub mod synchronized; +pub use execution_context::ExecutionContext; pub use module::Module; diff --git a/gui/src/rust/ide/src/model/execution_context.rs b/gui/src/rust/ide/src/model/execution_context.rs new file mode 100644 index 0000000000..85de40693d --- /dev/null +++ b/gui/src/rust/ide/src/model/execution_context.rs @@ -0,0 +1,93 @@ +//! This module consists of all structures describing Execution Context. + +use crate::prelude::*; + +use crate::double_representation::definition::DefinitionName; + +use enso_protocol::language_server; + + + +// ============== +// === Errors === +// ============== + +/// Error then trying to pop stack item on ExecutionContext when there only root call remains. +#[derive(Clone,Copy,Debug,Fail)] +#[fail(display="Tried to pop an entry point")] +pub struct PopOnEmptyStack {} + + + +// ================= +// === StackItem === +// ================= + +/// An identifier of called definition in module. +pub type DefinitionId = crate::double_representation::definition::Id; +/// An identifier of expression. +pub type ExpressionId = ast::Id; + +/// A specific function call occurring within another function's definition body. +/// +/// This is a single item in ExecutionContext stack. +#[derive(Clone,Debug,Eq,PartialEq)] +pub struct LocalCall { + /// An expression being a call. + pub call : ExpressionId, + /// A definition of function called in `call` expression. + pub definition : DefinitionId, +} + +/// An identifier of ExecutionContext. +pub type Id = language_server::ContextId; + + + +// ============= +// === Model === +// ============= + +/// Execution Context Model. +/// +/// The execution context consists of the root call (which is a direct call of some function +/// definition) and stack of function calls (see `StackItem` definition and docs). +/// +/// It implements internal mutability pattern, so the state may be shared between different +/// controllers. +#[derive(Debug)] +pub struct ExecutionContext { + /// A name of definition which is a root call of this context. + pub entry_point : DefinitionName, + stack : RefCell>, + //TODO[ao] I think we can put here info about visualisation set as well. +} + +impl ExecutionContext { + /// Create new execution context + pub fn new(entry_point:DefinitionName) -> Self { + let stack = default(); + Self {entry_point,stack} + } + + /// Push a new stack item to execution context. + pub fn push(&self, stack_item:LocalCall) { + self.stack.borrow_mut().push(stack_item); + } + + /// Pop the last stack item from this context. It returns error when only root call + /// remains. + pub fn pop(&self) -> FallibleResult<()> { + self.stack.borrow_mut().pop().ok_or(PopOnEmptyStack{})?; + Ok(()) + } + + /// Get an iterator over stack items. + /// + /// Because this struct implements _internal mutability pattern_, the stack can actually change + /// during iteration. It should not panic, however might give an unpredictable result. + pub fn stack_items<'a>(&'a self) -> impl Iterator + 'a { + let stack_size = self.stack.borrow().len(); + (0..stack_size).filter_map(move |i| self.stack.borrow().get(i).cloned()) + } +} diff --git a/gui/src/rust/ide/src/model/module/registry.rs b/gui/src/rust/ide/src/model/module/registry.rs index de41bcc231..2215c45860 100644 --- a/gui/src/rust/ide/src/model/module/registry.rs +++ b/gui/src/rust/ide/src/model/module/registry.rs @@ -168,7 +168,7 @@ mod test { let state = Rc::new(model::Module::new(ast.try_into().unwrap(),default())); let registry = Rc::new(Registry::default()); let expected = state.clone_ref(); - let path = ModulePath::new(default(),&["test"]); + let path = ModulePath::from_module_name("Test"); let loader = async move { Ok(state) }; let module = registry.get_or_load(path.clone(),loader).await.unwrap(); @@ -188,7 +188,7 @@ mod test { let state2 = state1.clone_ref(); let registry1 = Rc::new(Registry::default()); let registry2 = registry1.clone_ref(); - let path1 = ModulePath::new(default(),&["test"]); + let path1 = ModulePath::from_module_name("Test"); let path2 = path1.clone(); let (loaded_send, loaded_recv) = futures::channel::oneshot::channel::<()>(); diff --git a/gui/src/rust/ide/src/model/synchronized.rs b/gui/src/rust/ide/src/model/synchronized.rs new file mode 100644 index 0000000000..f21320ee6b --- /dev/null +++ b/gui/src/rust/ide/src/model/synchronized.rs @@ -0,0 +1,6 @@ +//! This module contains synchronising wrappers for models whose state are a reflection of +//! Language Server state, e.g. modules, execution contexts etc. These wrappers synchronize both +//! states by notifying Language Server of every change and listening on LanguageServer. +pub mod execution_context; + +pub use execution_context::ExecutionContext; diff --git a/gui/src/rust/ide/src/model/synchronized/execution_context.rs b/gui/src/rust/ide/src/model/synchronized/execution_context.rs new file mode 100644 index 0000000000..f57d708728 --- /dev/null +++ b/gui/src/rust/ide/src/model/synchronized/execution_context.rs @@ -0,0 +1,222 @@ +//! A ExecutionContext model which is synchronized with LanguageServer state. + +use crate::prelude::*; + +use crate::double_representation::definition::DefinitionName; +use crate::model::execution_context::LocalCall; + +use enso_protocol::language_server; +use json_rpc::error::RpcError; + + + +// ========================== +// === Synchronized Model === +// ========================== + +/// An ExecutionContext model synchronized with LanguageServer. It will be automatically removed +/// from LS once dropped. +#[derive(Debug)] +pub struct ExecutionContext { + id : model::execution_context::Id, + model : model::ExecutionContext, + module_path : Rc, + language_server : Rc, + logger : Logger, +} + +impl ExecutionContext { + /// Create new ExecutionContext. It will be created in LanguageServer and the ExplicitCall + /// stack frame will be pushed. + pub async fn create + ( language_server : Rc + , module_path : Rc + , root_definition : DefinitionName + ) -> FallibleResult { + let logger = Logger::new("ExecutionContext"); + let model = model::ExecutionContext::new(root_definition); + trace!(logger,"Creating Execution Context."); + let id = language_server.client.create_execution_context().await?.context_id; + trace!(logger,"Execution Context created. Id:{id}"); + let this = Self {id,module_path,model,language_server,logger}; + this.push_root_frame().await?; + trace!(this.logger,"Pushed root frame"); + Ok(this) + } + + fn push_root_frame(&self) -> impl Future> { + let method_pointer = language_server::MethodPointer { + file : self.module_path.file_path().clone(), + defined_on_type : self.module_path.module_name().to_string(), + name : self.model.entry_point.name.item.clone(), + }; + let this_argument_expression = default(); + let positional_arguments_expressions = default(); + + let call = language_server::ExplicitCall {method_pointer,this_argument_expression, + positional_arguments_expressions}; + let frame = language_server::StackItem::ExplicitCall(call); + let result = self.language_server.push_to_execution_context(self.id,frame); + result.map(|res| res.map_err(|err| err.into())) + } + + /// Push a new stack item to execution context. + pub fn push(&self, stack_item: LocalCall) -> impl Future> { + let expression_id = stack_item.call; + let call = language_server::LocalCall{expression_id}; + let frame = language_server::StackItem::LocalCall(call); + self.model.push(stack_item); + self.language_server.push_to_execution_context(self.id,frame) + } + + /// Pop the last stack item from this context. It returns error when only root call + /// remains. + pub async fn pop(&self) -> FallibleResult<()> { + self.model.pop()?; + self.language_server.pop_from_execution_context(self.id).await?; + Ok(()) + } + + /// Create a mock which does no call on `language_server` during construction. + #[cfg(test)] + pub fn new_mock + ( id : model::execution_context::Id + , path : controller::module::Path + , model : model::ExecutionContext + , language_server : language_server::MockClient + ) -> Self { + let module_path = Rc::new(path); + let language_server = language_server::Connection::new_mock_rc(language_server); + let logger = Logger::new("ExecuctionContext mock"); + ExecutionContext {id,model,module_path,language_server,logger} + } +} + +impl Drop for ExecutionContext { + fn drop(&mut self) { + let id = self.id; + let ls = self.language_server.clone_ref(); + let logger = self.logger.clone_ref(); + executor::global::spawn(async move { + let result = ls.client.destroy_execution_context(id).await; + if result.is_err() { + error!(logger,"Error when destroying Execution Context: {result:?}."); + } + }); + } +} + + + +// ============= +// === Tests === +// ============= + +#[cfg(test)] +mod test { + use super::*; + + use crate::executor::test_utils::TestWithLocalPoolExecutor; + + use language_server::response; + use utils::test::ExpectTuple; + + + + #[test] + fn creating_context() { + let path = Rc::new(controller::module::Path::from_module_name("Test")); + let context_id = model::execution_context::Id::new_v4(); + let root_def = DefinitionName::new_plain("main"); + let ls_client = language_server::MockClient::default(); + ls_client.set_create_execution_context_result(Ok(response::CreateExecutionContext { + context_id, + can_modify : create_capability("executionContext/canModify",context_id), + receives_updates : create_capability("executionContext/receivesUpdates",context_id), + })); + let expected_method = language_server::MethodPointer { + file : path.file_path().clone(), + defined_on_type : "Test".to_string(), + name : "main".to_string(), + }; + let expected_root_frame = language_server::ExplicitCall { + method_pointer : expected_method, + this_argument_expression : None, + positional_arguments_expressions : vec![] + }; + let expected_stack_item = language_server::StackItem::ExplicitCall(expected_root_frame); + ls_client.set_push_to_execution_context_result(context_id,expected_stack_item,Ok(())); + ls_client.set_destroy_execution_context_result(context_id,Ok(())); + ls_client.expect_all_calls(); + let connection = language_server::Connection::new_mock_rc(ls_client); + + let mut test = TestWithLocalPoolExecutor::set_up(); + test.run_task(async move { + let context = ExecutionContext::create(connection,path.clone(),root_def).await.unwrap(); + assert_eq!(context_id , context.id); + assert_eq!(path , context.module_path); + assert_eq!(Vec::::new(), context.model.stack_items().collect_vec()); + }) + } + + fn create_capability + (method:impl Str, context_id:model::execution_context::Id) + -> language_server::CapabilityRegistration { + language_server::CapabilityRegistration { + method : method.into(), + register_options : language_server::RegisterOptions::ExecutionContextId {context_id}, + } + } + + #[test] + fn pushing_stack_item() { + let id = model::execution_context::Id::new_v4(); + let definition = model::execution_context::DefinitionId::new_plain_name("foo"); + let expression_id = model::execution_context::ExpressionId::new_v4(); + let path = controller::module::Path::from_module_name("Test"); + let root_def = DefinitionName::new_plain("main"); + let model = model::ExecutionContext::new(root_def); + let ls = language_server::MockClient::default(); + let expected_call_frame = language_server::LocalCall{expression_id}; + let expected_stack_item = language_server::StackItem::LocalCall(expected_call_frame); + + ls.set_push_to_execution_context_result(id,expected_stack_item,Ok(())); + ls.set_destroy_execution_context_result(id,Ok(())); + let context = ExecutionContext::new_mock(id,path.clone(),model,ls); + + let mut test = TestWithLocalPoolExecutor::set_up(); + test.run_task(async move { + let item = LocalCall { + call : expression_id, + definition : definition.clone() + }; + context.push(item.clone()).await.unwrap(); + assert_eq!((item,), context.model.stack_items().expect_tuple()); + }) + } + + #[test] + fn popping_stack_item() { + let id = model::execution_context::Id::new_v4(); + let item = LocalCall { + call : model::execution_context::ExpressionId::new_v4(), + definition : model::execution_context::DefinitionId::new_plain_name("foo"), + }; + let path = controller::module::Path::from_module_name("Test"); + let root_def = DefinitionName::new_plain("main"); + let ls = language_server::MockClient::default(); + let model = model::ExecutionContext::new(root_def); + ls.set_pop_from_execution_context_result(id,Ok(())); + ls.set_destroy_execution_context_result(id,Ok(())); + model.push(item); + let context = ExecutionContext::new_mock(id,path.clone(),model,ls); + + let mut test = TestWithLocalPoolExecutor::set_up(); + test.run_task(async move { + context.pop().await.unwrap(); + assert_eq!(Vec::::new(), context.model.stack_items().collect_vec()); + // Pop on empty stack. + assert!(context.pop().await.is_err()); + }) + } +} diff --git a/gui/src/rust/ide/src/view/layout.rs b/gui/src/rust/ide/src/view/layout.rs index 4104fca87f..d64b9fb240 100644 --- a/gui/src/rust/ide/src/view/layout.rs +++ b/gui/src/rust/ide/src/view/layout.rs @@ -91,15 +91,15 @@ impl ViewLayout { ( logger : &Logger , kb_actions : &mut keyboard::Actions , application : &Application - , text_controller : controller::text::Handle - , graph_controller : controller::graph::Handle + , text_controller : controller::Text + , graph_controller : controller::ExecutedGraph , fonts : &mut FontRegistry ) -> Self { let logger = logger.sub("ViewLayout"); let world = &application.display; let text_editor = TextEditor::new(&logger,world,text_controller,kb_actions,fonts); - let node_editor = NodeEditor::new(&logger,application,graph_controller.clone()); - let node_searcher = NodeSearcher::new(world,&logger,graph_controller,fonts); + let node_editor = NodeEditor::new(&logger,application,graph_controller.clone_ref()); + let node_searcher = NodeSearcher::new(world,&logger,graph_controller.graph.clone_ref(),fonts); world.add_child(&text_editor.display_object()); world.add_child(&node_editor); world.add_child(&node_searcher); diff --git a/gui/src/rust/ide/src/view/node_editor.rs b/gui/src/rust/ide/src/view/node_editor.rs index 7c16b2168b..51ee06dc8a 100644 --- a/gui/src/rust/ide/src/view/node_editor.rs +++ b/gui/src/rust/ide/src/view/node_editor.rs @@ -27,7 +27,7 @@ use weak_table::weak_value_hash_map::Entry::{Occupied, Vacant}; pub struct GraphEditorIntegration { pub logger : Logger, pub editor : GraphEditor, - pub controller : controller::Graph, + pub controller : controller::ExecutedGraph, id_to_node : RefCell>, node_to_id : RefCell>, @@ -35,7 +35,7 @@ pub struct GraphEditorIntegration { impl GraphEditorIntegration { /// Constructor. It creates GraphEditor panel and connect it with given controller handle. - pub fn new(logger:Logger, app:&Application, controller:controller::Graph) -> Rc { + pub fn new(logger:Logger, app:&Application, controller:controller::ExecutedGraph) -> Rc { let editor = app.views.new::(); let id_to_node = default(); let node_to_id = default(); @@ -43,12 +43,15 @@ impl GraphEditorIntegration { Self::setup_controller_event_handling(&this); Self::setup_keyboard_event_handling(&this); Self::setup_mouse_event_handling(&this); + if let Err(err) = this.invalidate_graph() { + error!(this.logger,"Error while initializing graph display: {err}"); + } this } /// Reloads whole displayed content to be up to date with module state. pub fn invalidate_graph(&self) -> FallibleResult<()> { - let nodes = self.controller.nodes()?; + let nodes = self.controller.graph.nodes()?; let ids = nodes.iter().map(|node| node.info.id() ).collect(); self.retain_ids(&ids); for (i,node_info) in nodes.iter().enumerate() { @@ -71,7 +74,7 @@ impl GraphEditorIntegration { } fn setup_controller_event_handling(this:&Rc) { - let stream = this.controller.subscribe(); + let stream = this.controller.graph.subscribe(); let weak = Rc::downgrade(this); let handler = process_stream_with_handle(stream,weak,move |notification,this| { let result = match notification { @@ -97,7 +100,7 @@ impl GraphEditorIntegration { this.editor.nodes.selected.for_each(|node_id| { let id = this.node_to_id.borrow().get(&node_id.0).cloned(); // FIXME .0 if let Some(id) = id { - if let Err(err) = this.controller.remove_node(id) { + if let Err(err) = this.controller.graph.remove_node(id) { this.logger.error(|| format!("ERR: {:?}", err)); } } @@ -119,7 +122,7 @@ impl GraphEditorIntegration { if let Some((node_pos,this)) = node_pos.and_then(|n| this.map(|t| (n,t))) { let id = this.node_to_id.borrow().get(&node_id.0).cloned(); // FIXME .0 if let Some(id) = id { - this.controller.module.with_node_metadata(id, |md| { + this.controller.graph.module.with_node_metadata(id, |md| { md.position = Some(model::module::Position::new(node_pos.x, node_pos.y)); }) } @@ -153,12 +156,12 @@ impl GraphEditorIntegration { pub struct NodeEditor { display_object : display::object::Instance, graph : Rc, - controller : controller::graph::Handle, + controller : controller::ExecutedGraph, } impl NodeEditor { /// Create Node Editor Panel. - pub fn new(logger:&Logger, app:&Application, controller:controller::graph::Handle) -> Self { + pub fn new(logger:&Logger, app:&Application, controller:controller::ExecutedGraph) -> Self { let logger = logger.sub("NodeEditor"); let graph = GraphEditorIntegration::new(logger,app,controller.clone_ref()); let display_object = display::object::Instance::new(&graph.logger); diff --git a/gui/src/rust/ide/src/view/project.rs b/gui/src/rust/ide/src/view/project.rs index d8feeb7a7a..c0a046eac7 100644 --- a/gui/src/rust/ide/src/view/project.rs +++ b/gui/src/rust/ide/src/view/project.rs @@ -73,12 +73,13 @@ impl ProjectView { pub async fn new(logger:&Logger, controller:controller::Project) -> FallibleResult { let root_id = controller.language_server_rpc.content_root(); - let path = controller::module::Path::new(root_id,&INITIAL_FILE_PATH); + let path = controller::FilePath::new(root_id,&INITIAL_FILE_PATH); let text_controller = controller.text_controller(path.clone()).await?; let main_name = DefinitionName::new_plain(MAIN_DEFINITION_NAME); let graph_id = controller::graph::Id::new_single_crumb(main_name); - let module_controller = controller.module_controller(path).await?; - let graph_controller = module_controller.graph_controller_unchecked(graph_id); + let module_controller = controller.module_controller(path.try_into()?).await?; + let graph_controller = module_controller.executed_graph_controller_unchecked(graph_id); + let graph_controller = graph_controller.await?; let application = Application::new(&web::get_html_element_by_id("root").unwrap()); let _world = &application.display; // graph::register_shapes(&world); @@ -88,10 +89,10 @@ impl ProjectView { let mut keyboard_actions = keyboard::Actions::new(&keyboard); let resize_callback = None; let mut fonts = FontRegistry::new(); - let layout = ViewLayout::new - (&logger,&mut keyboard_actions,&application,text_controller,graph_controller,&mut fonts); - let data = ProjectViewData - {application,layout,resize_callback,controller,keyboard,keyboard_bindings,keyboard_actions}; + let layout = ViewLayout::new(&logger,&mut keyboard_actions,&application, + text_controller,graph_controller,&mut fonts); + let data = ProjectViewData {application,layout,resize_callback,controller,keyboard, + keyboard_bindings,keyboard_actions}; Ok(Self::new_from_data(data).init()) } diff --git a/gui/src/rust/ide/tests/language_server.rs b/gui/src/rust/ide/tests/language_server.rs index b42165b186..666f1b9993 100644 --- a/gui/src/rust/ide/tests/language_server.rs +++ b/gui/src/rust/ide/tests/language_server.rs @@ -101,10 +101,10 @@ async fn file_operations() { let explicit_call = ExplicitCall {method_pointer,positional_arguments_expressions,this_argument_expression}; let stack_item = StackItem::ExplicitCall(explicit_call); - let response = client.push_execution_context(execution_context_id,stack_item).await; + let response = client.push_to_execution_context(execution_context_id,stack_item).await; response.expect("Couldn't push execution context."); - let response = client.pop_execution_context(execution_context_id).await; + let response = client.pop_from_execution_context(execution_context_id).await; response.expect("Couldn't pop execution context."); let visualisation_id = uuid::Uuid::new_v4();