This PR introduces Executed Graph Controller, which is a Graph Controller with additional info about execution context.

Original commit: 026a2585ae
This commit is contained in:
Adam Obuchowicz 2020-05-13 13:57:36 +02:00 committed by GitHub
parent f8bd0d56e2
commit 7d4529885b
21 changed files with 595 additions and 113 deletions

View File

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

View File

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

View File

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

View File

@ -24,6 +24,7 @@ pub struct Path {
pub root_id:Uuid,
/// Path's segments.
pub segments:Vec<String>,
}
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.

View File

@ -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<Item = Event> {
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<Item = Event> {
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<Output = ()> {
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<Output = ()> {
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<HashMap<($($param_ty),*),Result<$result>>>,)*
expect_all_calls : Cell<bool>,
$($method_result : RefCell<HashMap<($($param_ty),*),Vec<Result<$result>>>>,)*
}
impl API for MockClient {
$(fn $method(&self $(,$param_name:$param_ty)*)
-> std::pin::Pin<Box<dyn Future<Output=Result<$result>>>> {
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(&params).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);
}
)*
}
}
}
}
}

View File

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

View File

@ -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<model::Module>, 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<model::Module>, parser:Parser, id:Id) -> FallibleResult<Handle> {
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<Output=()> {
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<Output=()> {
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();

View File

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

View File

@ -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<Self> {
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<FilePath> for Path {
type Error = InvalidModulePath;
fn try_from(value:FilePath) -> Result<Self, Self::Error> {
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<Output=FallibleResult<()>> {
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<controller::ExecutedGraph> {
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();

View File

@ -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<controller::Text> {
if is_path_to_module(&path) {
pub async fn text_controller(&self, path:FilePath) -> FallibleResult<controller::Text> {
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();

View File

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

View File

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

View File

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

View File

@ -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<Vec<LocalCall>>,
//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<Item=LocalCall> + 'a {
let stack_size = self.stack.borrow().len();
(0..stack_size).filter_map(move |i| self.stack.borrow().get(i).cloned())
}
}

View File

@ -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::<()>();

View File

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

View File

@ -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<controller::module::Path>,
language_server : Rc<language_server::Connection>,
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<language_server::Connection>
, module_path : Rc<controller::module::Path>
, root_definition : DefinitionName
) -> FallibleResult<Self> {
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<Output=FallibleResult<()>> {
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<Output=Result<(),RpcError>> {
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::<LocalCall>::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::<LocalCall>::new(), context.model.stack_items().collect_vec());
// Pop on empty stack.
assert!(context.pop().await.is_err());
})
}
}

View File

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

View File

@ -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<WeakValueHashMap<ast::Id,WeakNode>>,
node_to_id : RefCell<WeakKeyHashMap<WeakNode,ast::Id>>,
@ -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<Self> {
pub fn new(logger:Logger, app:&Application, controller:controller::ExecutedGraph) -> Rc<Self> {
let editor = app.views.new::<GraphEditor>();
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<Self>) {
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<GraphEditorIntegration>,
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);

View File

@ -73,12 +73,13 @@ impl ProjectView {
pub async fn new(logger:&Logger, controller:controller::Project)
-> FallibleResult<Self> {
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())
}

View File

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