Read-only mode for controllers (#6259)

Closes #6181

- Added a read-only flag for the project model (now controlled by a temporary shortcut).
- Renaming the project, editing the code, connecting and disconnecting nodes, and collapsing, and navigating through collapsed nodes are forbidden and will result in the error message in the console.
- Some of the above actions produce undesired effects on the IDE, which will be fixed later. This PR is focused on restricting actual AST modifications.
- Moving nodes (and updating metadata in general) no longer causes reevaluation


https://user-images.githubusercontent.com/6566674/231616408-4f334bb7-1985-43ba-9953-4c0998338a9b.mp4
This commit is contained in:
Ilya Bogdanov 2023-04-20 17:17:18 +03:00 committed by GitHub
parent 68119ad6bb
commit 6aba602a34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 305 additions and 71 deletions

View File

@ -113,7 +113,7 @@ trait API {
/// have permission to edit the resources for which edits are sent. This failure may be partial,
/// in that some edits are applied and others are not.
#[MethodInput=ApplyTextFileEditInput, rpc_name="text/applyEdit"]
fn apply_text_file_edit(&self, edit: FileEdit) -> ();
fn apply_text_file_edit(&self, edit: FileEdit, execute: bool) -> ();
/// Create a new execution context. Return capabilities executionContext/canModify and
/// executionContext/receivesUpdates containing freshly created ContextId

View File

@ -547,7 +547,7 @@ fn test_execution_context() {
let path = main.clone();
let edit = FileEdit { path, edits, old_version, new_version };
test_request(
|client| client.apply_text_file_edit(&edit),
|client| client.apply_text_file_edit(&edit, &true),
"text/applyEdit",
json!({
"edit" : {
@ -572,7 +572,8 @@ fn test_execution_context() {
],
"oldVersion" : "d3ee9b1ba1990fecfd794d2f30e0207aaa7be5d37d463073096d86f8",
"newVersion" : "6a33e22f20f16642697e8bd549ff7b759252ad56c05a1b0acc31dc69"
}
},
"execute": true
}),
unit_json.clone(),
(),

View File

@ -44,6 +44,10 @@ pub struct NotEvaluatedYet(double_representation::node::Id);
#[fail(display = "The node {} does not resolve to a method call.", _0)]
pub struct NoResolvedMethod(double_representation::node::Id);
#[allow(missing_docs)]
#[derive(Debug, Fail, Clone, Copy)]
#[fail(display = "Operation is not permitted in read only mode")]
pub struct ReadOnly;
// ====================
@ -216,8 +220,13 @@ impl Handle {
/// This will cause pushing a new stack frame to the execution context and changing the graph
/// controller to point to a new definition.
///
/// Fails if method graph cannot be created (see `graph_for_method` documentation).
/// ### Errors
/// - Fails if method graph cannot be created (see `graph_for_method` documentation).
/// - Fails if the project is in read-only mode.
pub async fn enter_method_pointer(&self, local_call: &LocalCall) -> FallibleResult {
if self.project.read_only() {
Err(ReadOnly.into())
} else {
debug!("Entering node {}.", local_call.call);
let method_ptr = &local_call.definition;
let graph = controller::Graph::new_method(&self.project, method_ptr);
@ -230,6 +239,7 @@ impl Handle {
Ok(())
}
}
/// Attempts to get the computed value of the specified node.
///
@ -246,9 +256,14 @@ impl Handle {
/// Leave the current node. Reverse of `enter_node`.
///
/// Fails if this execution context is already at the stack's root or if the parent graph
/// ### Errors
/// - Fails if this execution context is already at the stack's root or if the parent graph
/// cannot be retrieved.
/// - Fails if the project is in read-only mode.
pub async fn exit_node(&self) -> FallibleResult {
if self.project.read_only() {
Err(ReadOnly.into())
} else {
let frame = self.execution_ctx.pop().await?;
let method = self.execution_ctx.current_method();
let graph = controller::Graph::new_method(&self.project, &method).await?;
@ -256,6 +271,7 @@ impl Handle {
self.notifier.publish(Notification::SteppedOutOfNode(frame.call)).await;
Ok(())
}
}
/// Interrupt the program execution.
pub async fn interrupt(&self) -> FallibleResult {
@ -264,10 +280,17 @@ impl Handle {
}
/// Restart the program execution.
///
/// ### Errors
/// - Fails if the project is in read-only mode.
pub async fn restart(&self) -> FallibleResult {
if self.project.read_only() {
Err(ReadOnly.into())
} else {
self.execution_ctx.restart().await?;
Ok(())
}
}
/// Get the current call stack frames.
pub fn call_stack(&self) -> Vec<LocalCall> {
@ -308,17 +331,31 @@ impl Handle {
}
/// Create connection in graph.
///
/// ### Errors
/// - Fails if the project is in read-only mode.
pub fn connect(&self, connection: &Connection) -> FallibleResult {
if self.project.read_only() {
Err(ReadOnly.into())
} else {
self.graph.borrow().connect(connection, self)
}
}
/// Remove the connections from the graph. Returns an updated edge destination endpoint for
/// disconnected edge, in case it is still used as destination-only edge. When `None` is
/// returned, no update is necessary.
///
/// ### Errors
/// - Fails if the project is in read-only mode.
pub fn disconnect(&self, connection: &Connection) -> FallibleResult<Option<span_tree::Crumbs>> {
if self.project.read_only() {
Err(ReadOnly.into())
} else {
self.graph.borrow().disconnect(connection, self)
}
}
}
// === Span Tree Context ===
@ -368,6 +405,8 @@ pub mod tests {
use crate::model::execution_context::ExpressionId;
use crate::test;
use crate::test::mock::Fixture;
use controller::graph::SpanTree;
use engine_protocol::language_server::types::test::value_update_with_type;
use wasm_bindgen_test::wasm_bindgen_test;
use wasm_bindgen_test::wasm_bindgen_test_configure;
@ -454,4 +493,95 @@ pub mod tests {
notifications.expect_pending();
}
// Test that moving nodes is possible in read-only mode.
#[wasm_bindgen_test]
fn read_only_mode_does_not_restrict_moving_nodes() {
use model::module::Position;
let fixture = crate::test::mock::Unified::new().fixture();
let Fixture { executed_graph, graph, .. } = fixture;
let nodes = executed_graph.graph().nodes().unwrap();
let node = &nodes[0];
let pos1 = Position::new(500.0, 250.0);
let pos2 = Position::new(300.0, 150.0);
graph.set_node_position(node.id(), pos1).unwrap();
assert_eq!(graph.node(node.id()).unwrap().position(), Some(pos1));
graph.set_node_position(node.id(), pos2).unwrap();
assert_eq!(graph.node(node.id()).unwrap().position(), Some(pos2));
}
// Test that certain actions are forbidden in read-only mode.
#[wasm_bindgen_test]
fn read_only_mode() {
fn run(code: &str, f: impl FnOnce(&Handle)) {
let mut data = crate::test::mock::Unified::new();
data.set_code(code);
let fixture = data.fixture();
fixture.read_only.set(true);
let Fixture { executed_graph, .. } = fixture;
f(&executed_graph);
}
// === Editing the node. ===
let default_code = r#"
main =
foo = 2 * 2
"#;
run(default_code, |executed| {
let nodes = executed.graph().nodes().unwrap();
let node = &nodes[0];
assert!(executed.graph().set_expression(node.info.id(), "5 * 20").is_err());
});
// === Collapsing nodes. ===
let code = r#"
main =
foo = 2
bar = foo + 6
baz = 2 + foo + bar
caz = baz / 2 * baz
"#;
run(code, |executed| {
let nodes = executed.graph().nodes().unwrap();
// Collapse two middle nodes.
let nodes_range = vec![nodes[1].id(), nodes[2].id()];
assert!(executed.graph().collapse(nodes_range, "extracted").is_err());
});
// === Connecting nodes. ===
let code = r#"
main =
2 + 2
5 * 5
"#;
run(code, |executed| {
let nodes = executed.graph().nodes().unwrap();
let sum_node = &nodes[0];
let product_node = &nodes[1];
assert_eq!(sum_node.expression().to_string(), "2 + 2");
assert_eq!(product_node.expression().to_string(), "5 * 5");
let context = &span_tree::generate::context::Empty;
let sum_tree = SpanTree::<()>::new(&sum_node.expression(), context).unwrap();
let sum_input =
sum_tree.root_ref().leaf_iter().find(|n| n.is_argument()).unwrap().crumbs;
let connection = Connection {
source: controller::graph::Endpoint::new(product_node.id(), []),
destination: controller::graph::Endpoint::new(sum_node.id(), sum_input),
};
assert!(executed.connect(&connection).is_err());
});
}
}

View File

@ -173,7 +173,7 @@ impl Handle {
) -> FallibleResult<Self> {
let ast = parser.parse(code.to_string(), id_map).try_into()?;
let metadata = default();
let model = Rc::new(model::module::Plain::new(path, ast, metadata, repository));
let model = Rc::new(model::module::Plain::new(path, ast, metadata, repository, default()));
Ok(Handle { model, language_server, parser })
}

View File

@ -97,6 +97,7 @@ pub struct CannotCommitExpression {
}
// =====================
// === Notifications ===
// =====================

View File

@ -744,7 +744,9 @@ pub mod test {
repository: Rc<model::undo_redo::Repository>,
) -> Module {
let ast = parser.parse_module(&self.code, self.id_map.clone()).unwrap();
let module = Plain::new(self.path.clone(), ast, self.metadata.clone(), repository);
let path = self.path.clone();
let metadata = self.metadata.clone();
let module = Plain::new(path, ast, metadata, repository, default());
Rc::new(module)
}
}

View File

@ -26,6 +26,17 @@ use std::collections::hash_map::Entry;
// ==============
// === Errors ===
// ==============
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, Fail)]
#[fail(display = "Attempt to edit a read-only module")]
pub struct EditInReadOnly;
// ==============
// === Module ===
// ==============
@ -41,6 +52,7 @@ pub struct Module {
content: RefCell<Content>,
notifications: notification::Publisher<Notification>,
repository: Rc<model::undo_redo::Repository>,
read_only: Rc<Cell<bool>>,
}
impl Module {
@ -50,26 +62,33 @@ impl Module {
ast: ast::known::Module,
metadata: Metadata,
repository: Rc<model::undo_redo::Repository>,
read_only: Rc<Cell<bool>>,
) -> Self {
Module {
content: RefCell::new(ParsedSourceFile { ast, metadata }),
notifications: default(),
path,
repository,
read_only,
}
}
/// Replace the module's content with the new value and emit notification of given kind.
///
/// Fails if the `new_content` is so broken that it cannot be serialized to text. In such case
/// ### Errors
/// - Fails if the `new_content` is so broken that it cannot be serialized to text. In such case
/// the module's state is guaranteed to remain unmodified and the notification will not be
/// emitted.
/// - Fails if the module is read-only. Metadata-only changes are allowed in read-only mode.
#[profile(Debug)]
fn set_content(&self, new_content: Content, kind: NotificationKind) -> FallibleResult {
if new_content == *self.content.borrow() {
debug!("Ignoring spurious update.");
return Ok(());
}
if self.read_only.get() && kind != NotificationKind::MetadataChanged {
Err(EditInReadOnly.into())
} else {
trace!("Updating module's content: {kind:?}. New content:\n{new_content}");
let transaction = self.repository.transaction("Setting module's content");
transaction.fill_content(self.id(), self.content.borrow().clone());
@ -81,6 +100,7 @@ impl Module {
self.notifications.notify(notification);
Ok(())
}
}
/// Use `f` to update the module's content.
///

View File

@ -165,6 +165,7 @@ impl Module {
language_server: Rc<language_server::Connection>,
parser: Parser,
repository: Rc<model::undo_redo::Repository>,
read_only: Rc<Cell<bool>>,
) -> FallibleResult<Rc<Self>> {
let file_path = path.file_path().clone();
info!("Opening module {file_path}");
@ -176,7 +177,9 @@ impl Module {
let source = parser.parse_with_metadata(opened.content);
let digest = opened.current_version;
let summary = ContentSummary { digest, end_of_file };
let model = model::module::Plain::new(path, source.ast, source.metadata, repository);
let metadata = source.metadata;
let ast = source.ast;
let model = model::module::Plain::new(path, ast, metadata, repository, read_only);
let this = Rc::new(Module { model, language_server });
let content = this.model.serialized_content()?;
let first_invalidation = this.full_invalidation(&summary, content);
@ -238,7 +241,7 @@ impl Module {
self.content().replace(parsed_source);
let summary = ContentSummary::new(&content);
let change = TextEdit::from_prefix_postfix_differences(&content, &source.content);
self.notify_language_server(&summary, &source, vec![change]).await?;
self.notify_language_server(&summary, &source, vec![change], true).await?;
let notification = Notification::new(source, NotificationKind::Reloaded);
self.notify(notification);
Ok(())
@ -423,7 +426,8 @@ impl Module {
};
//id_map goes first, because code change may alter its position.
let edits = vec![id_map_change, code_change];
let notify_ls = self.notify_language_server(&summary.summary, &new_file, edits);
let summary = &summary.summary;
let notify_ls = self.notify_language_server(summary, &new_file, edits, true);
profiler::await_!(notify_ls, _profiler)
}
NotificationKind::MetadataChanged => {
@ -431,7 +435,8 @@ impl Module {
range: summary.metadata_engine_range().into(),
text: new_file.metadata_slice().to_string(),
}];
let notify_ls = self.notify_language_server(&summary.summary, &new_file, edits);
let summary = &summary.summary;
let notify_ls = self.notify_language_server(summary, &new_file, edits, false);
profiler::await_!(notify_ls, _profiler)
}
NotificationKind::Reloaded => Ok(ParsedContentSummary::from_source(&new_file)),
@ -450,7 +455,7 @@ impl Module {
debug!("Handling full invalidation: {ls_content:?}.");
let range = Range::new(Location::default(), ls_content.end_of_file);
let edits = vec![TextEdit { range: range.into(), text: new_file.content.clone() }];
self.notify_language_server(ls_content, &new_file, edits)
self.notify_language_server(ls_content, &new_file, edits, true)
}
fn edit_for_snipped(
@ -518,7 +523,7 @@ impl Module {
.into_iter()
.flatten()
.collect_vec();
self.notify_language_server(&ls_content.summary, &new_file, edits)
self.notify_language_server(&ls_content.summary, &new_file, edits, true)
}
/// This is a helper function with all common logic regarding sending the update to
@ -529,6 +534,7 @@ impl Module {
ls_content: &ContentSummary,
new_file: &SourceFile,
edits: Vec<TextEdit>,
execute: bool,
) -> impl Future<Output = FallibleResult<ParsedContentSummary>> + 'static {
let summary = ParsedContentSummary::from_source(new_file);
let edit = FileEdit {
@ -538,7 +544,7 @@ impl Module {
new_version: Sha3_224::new(new_file.content.as_bytes()),
};
debug!("Notifying LS with edit: {edit:#?}.");
let ls_future_reply = self.language_server.client.apply_text_file_edit(&edit);
let ls_future_reply = self.language_server.client.apply_text_file_edit(&edit, &execute);
async move {
ls_future_reply.await?;
Ok(summary)
@ -634,7 +640,7 @@ pub mod test {
f: impl FnOnce(&FileEdit) -> json_rpc::Result<()> + 'static,
) {
let this = self.clone();
client.expect.apply_text_file_edit(move |edits| {
client.expect.apply_text_file_edit(move |edits, _execute| {
let content_so_far = this.current_ls_content.get();
let result = f(edits);
let new_content = apply_edits(content_so_far, edits);
@ -755,9 +761,11 @@ pub mod test {
// * there is an initial invalidation after opening the module
// * replacing AST causes invalidation
// * localized text edit emits similarly localized synchronization updates.
// * modifying the code fails if the read-only mode is enabled.
let initial_code = "main =\n println \"Hello World!\"";
let mut data = crate::test::mock::Unified::new();
data.set_code(initial_code);
let read_only: Rc<Cell<bool>> = default();
// We do actually care about sharing `data` between `test` invocations, as it stores the
// Parser which is time-consuming to construct.
let test = |runner: &mut Runner| {
@ -784,19 +792,33 @@ pub mod test {
Ok(())
});
});
fixture.read_only.set(read_only.get());
let parser = data.parser.clone();
let module = fixture.synchronized_module();
let new_content = "main =\n println \"Test\"";
let new_ast = parser.parse_module(new_content, default()).unwrap();
module.update_ast(new_ast).unwrap();
let res = module.update_ast(new_ast);
if read_only.get() {
assert!(res.is_err());
} else {
assert!(res.is_ok());
}
runner.perhaps_run_until_stalled(&mut fixture);
let change = TextChange { range: (20..24).into(), text: "Test 2".to_string() };
module.apply_code_change(change, &Parser::new(), default()).unwrap();
let res = module.apply_code_change(change, &Parser::new(), default());
if read_only.get() {
assert!(res.is_err());
} else {
assert!(res.is_ok());
}
runner.perhaps_run_until_stalled(&mut fixture);
};
read_only.set(false);
Runner::run(test);
read_only.set(true);
Runner::run(test);
}

View File

@ -39,6 +39,15 @@ pub trait API: Debug {
/// Project's qualified name
fn qualified_name(&self) -> project::QualifiedName;
/// Whether the read-only mode is enabled for the project.
///
/// Read-only mode forbids certain operations, like renaming the project or editing the code
/// through the IDE.
fn read_only(&self) -> bool;
/// Set the read-only mode for the project.
fn set_read_only(&self, read_only: bool);
/// Get Language Server JSON-RPC Connection for this project.
fn json_rpc(&self) -> Rc<language_server::Connection>;
@ -267,4 +276,8 @@ pub mod test {
path.qualified_module_name(name.clone())
});
}
pub fn expect_read_only(project: &mut MockAPI, read_only: Rc<Cell<bool>>) {
project.expect_read_only().returning_st(move || read_only.get());
}
}

View File

@ -224,6 +224,11 @@ async fn update_modules_on_file_change(
#[fail(display = "Project Manager is unavailable.")]
pub struct ProjectManagerUnavailable;
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug, Fail)]
#[fail(display = "Project renaming is not available in read-only mode.")]
pub struct RenameInReadOnly;
/// A wrapper for an error with information that user tried to open project with unsupported
/// engine's version (which is likely the cause of the problems).
#[derive(Debug, Fail)]
@ -295,6 +300,7 @@ pub struct Project {
pub parser: Parser,
pub notifications: notification::Publisher<model::project::Notification>,
pub urm: Rc<model::undo_redo::Manager>,
pub read_only: Rc<Cell<bool>>,
}
impl Project {
@ -339,6 +345,7 @@ impl Project {
parser,
notifications,
urm,
read_only: default(),
};
let binary_handler = ret.binary_event_handler();
@ -620,13 +627,13 @@ impl Project {
&self,
path: module::Path,
) -> impl Future<Output = FallibleResult<Rc<module::Synchronized>>> {
let language_server = self.language_server_rpc.clone_ref();
let ls = self.language_server_rpc.clone_ref();
let parser = self.parser.clone_ref();
let urm = self.urm();
let repository = urm.repository.clone_ref();
let repo = urm.repository.clone_ref();
let read_only = self.read_only.clone_ref();
async move {
let module =
module::Synchronized::open(path, language_server, parser, repository).await?;
let module = module::Synchronized::open(path, ls, parser, repo, read_only).await?;
urm.module_opened(module.clone());
Ok(module)
}
@ -702,10 +709,14 @@ impl model::project::API for Project {
}
fn rename_project(&self, name: String) -> BoxFuture<FallibleResult> {
if self.read_only() {
std::future::ready(Err(RenameInReadOnly.into())).boxed_local()
} else {
async move {
let old_name = self.properties.borrow_mut().name.project.clone_ref();
let referent_name = name.to_im_string();
let project_manager = self.project_manager.as_ref().ok_or(ProjectManagerUnavailable)?;
let project_manager =
self.project_manager.as_ref().ok_or(ProjectManagerUnavailable)?;
let project_id = self.properties.borrow().id;
let project_name = ProjectName::new_unchecked(name);
project_manager.rename_project(&project_id, &project_name).await?;
@ -715,6 +726,7 @@ impl model::project::API for Project {
}
.boxed_local()
}
}
fn project_content_root_id(&self) -> Uuid {
self.language_server_rpc.project_root().id()
@ -727,6 +739,14 @@ impl model::project::API for Project {
fn urm(&self) -> Rc<model::undo_redo::Manager> {
self.urm.clone_ref()
}
fn read_only(&self) -> bool {
self.read_only.get()
}
fn set_read_only(&self, read_only: bool) {
self.read_only.set(read_only);
}
}
@ -864,7 +884,7 @@ mod test {
let write_capability = Some(write_capability);
let open_response = response::OpenTextFile { content, current_version, write_capability };
expect_call!(client.open_text_file(path=path.clone()) => Ok(open_response));
client.expect.apply_text_file_edit(|_| Ok(()));
client.expect.apply_text_file_edit(|_, _| Ok(()));
expect_call!(client.close_text_file(path) => Ok(()));
}

View File

@ -165,9 +165,10 @@ mod test {
use super::*;
use crate::executor::test_utils::TestWithLocalPoolExecutor;
use model::module::Plain;
type ModulePath = model::module::Path;
type Registry = super::Registry<ModulePath, model::module::Plain>;
type Registry = super::Registry<ModulePath, Plain>;
#[test]
fn getting_module() {
@ -177,7 +178,7 @@ mod test {
let ast = ast::Ast::one_line_module(line).try_into().unwrap();
let path = ModulePath::from_mock_module_name("Test");
let urm = default();
let state = Rc::new(model::module::Plain::new(path.clone(), ast, default(), urm));
let state = Rc::new(Plain::new(path.clone(), ast, default(), urm, default()));
let registry = Registry::default();
let expected = state.clone_ref();
@ -198,7 +199,7 @@ mod test {
let path1 = ModulePath::from_mock_module_name("Test");
let path2 = path1.clone();
let urm = default();
let state1 = Rc::new(model::module::Plain::new(path1.clone_ref(), ast, default(), urm));
let state1 = Rc::new(Plain::new(path1.clone_ref(), ast, default(), urm, default()));
let state2 = state1.clone_ref();
let registry1 = Rc::new(Registry::default());
let registry2 = registry1.clone_ref();

View File

@ -199,6 +199,12 @@ impl Model {
self.ide_controller.set_component_browser_private_entries_visibility(!visibility);
}
fn toggle_read_only(&self) {
let read_only = self.controller.model.read_only();
self.controller.model.set_read_only(!read_only);
info!("New read only state: {}.", self.controller.model.read_only());
}
fn restore_project_snapshot(&self) {
let controller = self.controller.clone_ref();
let breadcrumbs = self.view.graph().model.breadcrumbs.clone_ref();
@ -374,6 +380,8 @@ impl Project {
eval_ view.execution_context_interrupt(model.execution_context_interrupt());
eval_ view.execution_context_restart(model.execution_context_restart());
eval_ view.toggle_read_only(model.toggle_read_only());
}
let graph_controller = self.model.graph_controller.clone_ref();

View File

@ -134,7 +134,7 @@ impl Model {
match self.suggestion_for_entry_id(entry_id) {
Ok(suggestion) =>
if let Err(error) = self.controller.preview_suggestion(suggestion) {
warn!("Failed to preview suggestion {entry_id:?} because of error: {error:?}.");
warn!("Failed to preview suggestion {entry_id:?} because of error: {error}.");
},
Err(err) => warn!("Error while previewing suggestion: {err}."),
}

View File

@ -135,6 +135,7 @@ pub mod mock {
pub suggestions: HashMap<suggestion_database::entry::Id, suggestion_database::Entry>,
pub context_id: model::execution_context::Id,
pub parser: parser::Parser,
pub read_only: Rc<Cell<bool>>,
code: String,
id_map: ast::IdMap,
metadata: crate::model::module::Metadata,
@ -172,6 +173,7 @@ pub mod mock {
context_id: CONTEXT_ID,
root_definition: definition_name(),
parser: parser::Parser::new(),
read_only: default(),
}
}
@ -184,8 +186,14 @@ pub mod mock {
let path = self.module_path.clone();
let metadata = self.metadata.clone();
let repository = urm.repository.clone_ref();
let module = Rc::new(model::module::Plain::new(path, ast, metadata, repository));
urm.module_opened(module.clone());
let module = Rc::new(model::module::Plain::new(
path,
ast,
metadata,
repository,
self.read_only.clone_ref(),
));
urm.module_opened(module.clone_ref());
module
}
@ -242,6 +250,7 @@ pub mod mock {
// Root ID is needed to generate module path used to get the module.
model::project::test::expect_root_id(&mut project, crate::test::mock::data::ROOT_ID);
model::project::test::expect_suggestion_db(&mut project, suggestion_database);
model::project::test::expect_read_only(&mut project, self.read_only.clone_ref());
let json_rpc = language_server::Connection::new_mock_rc(json_client);
model::project::test::expect_json_rpc(&mut project, json_rpc);
let binary_rpc = binary::Connection::new_mock_rc(binary_client);
@ -301,6 +310,7 @@ pub mod mock {
position_in_code,
)
.unwrap();
let read_only = self.read_only.clone_ref();
executor.run_until_stalled();
Fixture {
executor,
@ -313,6 +323,7 @@ pub mod mock {
project,
searcher,
ide,
read_only,
}
}
@ -360,6 +371,7 @@ pub mod mock {
pub executed_graph: controller::ExecutedGraph,
pub suggestion_db: Rc<model::SuggestionDatabase>,
pub project: model::Project,
pub read_only: Rc<Cell<bool>>,
pub ide: controller::Ide,
pub searcher: controller::Searcher,
#[deref]
@ -384,7 +396,8 @@ pub mod mock {
let path = self.data.module_path.clone();
let ls = self.project.json_rpc();
let repository = self.project.urm().repository.clone_ref();
let module_future = model::module::Synchronized::open(path, ls, parser, repository);
let ro = self.read_only.clone_ref();
let module_future = model::module::Synchronized::open(path, ls, parser, repository, ro);
// We can `expect_ready`, because in fact this is synchronous in test conditions.
// (there's no real asynchronous connection beneath, just the `MockClient`)
let module = module_future.boxed_local().expect_ready().unwrap();

View File

@ -240,7 +240,7 @@ async fn ls_text_protocol_test() {
let new_version = Sha3_224::new(b"Hello, world!");
let path = move_path.clone();
let edit = FileEdit { path, edits, old_version, new_version: new_version.clone() };
client.apply_text_file_edit(&edit).await.expect("Couldn't apply edit.");
client.apply_text_file_edit(&edit, &true).await.expect("Couldn't apply edit.");
let saving_result = client.save_text_file(&move_path, &new_version).await;
saving_result.expect("Couldn't save file.");

View File

@ -103,6 +103,7 @@ ensogl::define_endpoints! {
execution_context_interrupt(),
/// Restart the program execution.
execution_context_restart(),
toggle_read_only(),
}
Output {
@ -689,6 +690,8 @@ impl application::View for View {
(Press, "debug_mode", DEBUG_MODE_SHORTCUT, "disable_debug_mode"),
(Press, "", "cmd shift t", "execution_context_interrupt"),
(Press, "", "cmd shift r", "execution_context_restart"),
// TODO(#6179): Remove this temporary shortcut when Play button is ready.
(Press, "", "ctrl shift b", "toggle_read_only"),
]
.iter()
.map(|(a, b, c, d)| Self::self_shortcut_when(*a, *c, *d, *b))