
Closes https://github.com/zed-industries/zed/issues/33980 Closes https://github.com/zed-industries/zed/discussions/33979 - Switches to the debounce task pattern for diagnostic summary computations, which most importantly lets us do them only once when a large number of DiagnosticUpdated events are received at once. - Makes workspace diagnostic requests not time out if a partial result is received. - Makes diagnostics from workspace diagnostic partial results get merged. There might be some related areas where we're not fully complying with the LSP spec but they may be outside the scope of what this PR should include. Release Notes: - Added support for streaming LSP workspace diagnostics. - Fixed editor freeze from large LSP workspace diagnostic responses.
3923 lines
140 KiB
Rust
3923 lines
140 KiB
Rust
use crate::{
|
|
rpc::RECONNECT_TIMEOUT,
|
|
tests::{TestServer, rust_lang},
|
|
};
|
|
use call::ActiveCall;
|
|
use editor::{
|
|
DocumentColorsRenderMode, Editor, EditorSettings, RowInfo, SelectionEffects,
|
|
actions::{
|
|
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
|
|
ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
|
|
},
|
|
test::{
|
|
editor_test_context::{AssertionContextManager, EditorTestContext},
|
|
expand_macro_recursively,
|
|
},
|
|
};
|
|
use fs::Fs;
|
|
use futures::{StreamExt, lock::Mutex};
|
|
use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
|
|
use indoc::indoc;
|
|
use language::{
|
|
FakeLspAdapter,
|
|
language_settings::{AllLanguageSettings, InlayHintSettings},
|
|
};
|
|
use project::{
|
|
ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
|
|
lsp_store::{
|
|
lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
|
|
rust_analyzer_ext::RUST_ANALYZER_NAME,
|
|
},
|
|
project_settings::{InlineBlameSettings, ProjectSettings},
|
|
};
|
|
use recent_projects::disconnected_overlay::DisconnectedOverlay;
|
|
use rpc::RECEIVE_TIMEOUT;
|
|
use serde_json::json;
|
|
use settings::SettingsStore;
|
|
use std::{
|
|
collections::BTreeSet,
|
|
ops::{Deref as _, Range},
|
|
path::{Path, PathBuf},
|
|
sync::{
|
|
Arc,
|
|
atomic::{self, AtomicBool, AtomicUsize},
|
|
},
|
|
};
|
|
use text::Point;
|
|
use util::{path, uri};
|
|
use workspace::{CloseIntent, Workspace};
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
async fn test_host_disconnect(
|
|
cx_a: &mut TestAppContext,
|
|
cx_b: &mut TestAppContext,
|
|
cx_c: &mut TestAppContext,
|
|
) {
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
let client_c = server.create_client(cx_c, "user_c").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
|
|
.await;
|
|
|
|
cx_b.update(editor::init);
|
|
cx_b.update(recent_projects::init);
|
|
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
"/a",
|
|
json!({
|
|
"a.txt": "a-contents",
|
|
"b.txt": "b-contents",
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
|
|
|
let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
cx_a.background_executor.run_until_parked();
|
|
|
|
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
|
|
|
|
let workspace_b = cx_b.add_window(|window, cx| {
|
|
Workspace::new(
|
|
None,
|
|
project_b.clone(),
|
|
client_b.app_state.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
|
|
let workspace_b_view = workspace_b.root(cx_b).unwrap();
|
|
|
|
let editor_b = workspace_b
|
|
.update(cx_b, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "b.txt"), None, true, window, cx)
|
|
})
|
|
.unwrap()
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
//TODO: focus
|
|
assert!(cx_b.update_window_entity(&editor_b, |editor, window, _| editor.is_focused(window)));
|
|
editor_b.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));
|
|
|
|
cx_b.update(|_, cx| {
|
|
assert!(workspace_b_view.read(cx).is_edited());
|
|
});
|
|
|
|
// Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
|
|
server.forbid_connections();
|
|
server.disconnect_client(client_a.peer_id().unwrap());
|
|
cx_a.background_executor
|
|
.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
|
|
|
|
project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
|
|
|
|
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
|
|
|
|
project_b.read_with(cx_b, |project, cx| project.is_read_only(cx));
|
|
|
|
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
|
|
|
|
// Ensure client B's edited state is reset and that the whole window is blurred.
|
|
workspace_b
|
|
.update(cx_b, |workspace, _, cx| {
|
|
assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
|
|
assert!(!workspace.is_edited());
|
|
})
|
|
.unwrap();
|
|
|
|
// Ensure client B is not prompted to save edits when closing window after disconnecting.
|
|
let can_close = workspace_b
|
|
.update(cx_b, |workspace, window, cx| {
|
|
workspace.prepare_to_close(CloseIntent::Quit, window, cx)
|
|
})
|
|
.unwrap()
|
|
.await
|
|
.unwrap();
|
|
assert!(can_close);
|
|
|
|
// Allow client A to reconnect to the server.
|
|
server.allow_connections();
|
|
cx_a.background_executor.advance_clock(RECEIVE_TIMEOUT);
|
|
|
|
// Client B calls client A again after they reconnected.
|
|
let active_call_b = cx_b.read(ActiveCall::global);
|
|
active_call_b
|
|
.update(cx_b, |call, cx| {
|
|
call.invite(client_a.user_id().unwrap(), None, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
cx_a.background_executor.run_until_parked();
|
|
active_call_a
|
|
.update(cx_a, |call, cx| call.accept_incoming(cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Drop client A's connection again. We should still unshare it successfully.
|
|
server.forbid_connections();
|
|
server.disconnect_client(client_a.peer_id().unwrap());
|
|
cx_a.background_executor
|
|
.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
|
|
|
|
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
|
cx_a: &mut TestAppContext,
|
|
cx_b: &mut TestAppContext,
|
|
) {
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
let executor = cx_a.executor();
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
|
|
client_a
|
|
.fs()
|
|
.insert_tree(path!("/dir"), json!({ "a.txt": "Some text\n" }))
|
|
.await;
|
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
|
|
// Open a buffer as client A
|
|
let buffer_a = project_a
|
|
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
|
.await
|
|
.unwrap();
|
|
let cx_a = cx_a.add_empty_window();
|
|
let editor_a = cx_a
|
|
.new_window_entity(|window, cx| Editor::for_buffer(buffer_a, Some(project_a), window, cx));
|
|
|
|
let mut editor_cx_a = EditorTestContext {
|
|
cx: cx_a.clone(),
|
|
window: cx_a.window_handle(),
|
|
editor: editor_a,
|
|
assertion_cx: AssertionContextManager::new(),
|
|
};
|
|
|
|
let cx_b = cx_b.add_empty_window();
|
|
// Open a buffer as client B
|
|
let buffer_b = project_b
|
|
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
|
.await
|
|
.unwrap();
|
|
let editor_b = cx_b
|
|
.new_window_entity(|window, cx| Editor::for_buffer(buffer_b, Some(project_b), window, cx));
|
|
|
|
let mut editor_cx_b = EditorTestContext {
|
|
cx: cx_b.clone(),
|
|
window: cx_b.window_handle(),
|
|
editor: editor_b,
|
|
assertion_cx: AssertionContextManager::new(),
|
|
};
|
|
|
|
// Test newline above
|
|
editor_cx_a.set_selections_state(indoc! {"
|
|
Some textˇ
|
|
"});
|
|
editor_cx_b.set_selections_state(indoc! {"
|
|
Some textˇ
|
|
"});
|
|
editor_cx_a.update_editor(|editor, window, cx| {
|
|
editor.newline_above(&editor::actions::NewlineAbove, window, cx)
|
|
});
|
|
executor.run_until_parked();
|
|
editor_cx_a.assert_editor_state(indoc! {"
|
|
ˇ
|
|
Some text
|
|
"});
|
|
editor_cx_b.assert_editor_state(indoc! {"
|
|
|
|
Some textˇ
|
|
"});
|
|
|
|
// Test newline below
|
|
editor_cx_a.set_selections_state(indoc! {"
|
|
|
|
Some textˇ
|
|
"});
|
|
editor_cx_b.set_selections_state(indoc! {"
|
|
|
|
Some textˇ
|
|
"});
|
|
editor_cx_a.update_editor(|editor, window, cx| {
|
|
editor.newline_below(&editor::actions::NewlineBelow, window, cx)
|
|
});
|
|
executor.run_until_parked();
|
|
editor_cx_a.assert_editor_state(indoc! {"
|
|
|
|
Some text
|
|
ˇ
|
|
"});
|
|
editor_cx_b.assert_editor_state(indoc! {"
|
|
|
|
Some textˇ
|
|
|
|
"});
|
|
}
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
|
|
client_a.language_registry().add(rust_lang());
|
|
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
|
"Rust",
|
|
FakeLspAdapter {
|
|
capabilities: lsp::ServerCapabilities {
|
|
completion_provider: Some(lsp::CompletionOptions {
|
|
trigger_characters: Some(vec![".".to_string()]),
|
|
resolve_provider: Some(true),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
},
|
|
);
|
|
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
path!("/a"),
|
|
json!({
|
|
"main.rs": "fn main() { a }",
|
|
"other.rs": "",
|
|
}),
|
|
)
|
|
.await;
|
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
|
|
// Open a file in an editor as the guest.
|
|
let buffer_b = project_b
|
|
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
|
.await
|
|
.unwrap();
|
|
let cx_b = cx_b.add_empty_window();
|
|
let editor_b = cx_b.new_window_entity(|window, cx| {
|
|
Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx)
|
|
});
|
|
|
|
let fake_language_server = fake_language_servers.next().await.unwrap();
|
|
cx_a.background_executor.run_until_parked();
|
|
|
|
buffer_b.read_with(cx_b, |buffer, _| {
|
|
assert!(!buffer.completion_triggers().is_empty())
|
|
});
|
|
|
|
// Type a completion trigger character as the guest.
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges([13..13])
|
|
});
|
|
editor.handle_input(".", window, cx);
|
|
});
|
|
cx_b.focus(&editor_b);
|
|
|
|
// Receive a completion request as the host's language server.
|
|
// Return some completions from the host's language server.
|
|
cx_a.executor().start_waiting();
|
|
fake_language_server
|
|
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
|
|
assert_eq!(
|
|
params.text_document_position.text_document.uri,
|
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
);
|
|
assert_eq!(
|
|
params.text_document_position.position,
|
|
lsp::Position::new(0, 14),
|
|
);
|
|
|
|
Ok(Some(lsp::CompletionResponse::Array(vec![
|
|
lsp::CompletionItem {
|
|
label: "first_method(…)".into(),
|
|
detail: Some("fn(&mut self, B) -> C".into()),
|
|
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
|
new_text: "first_method($1)".to_string(),
|
|
range: lsp::Range::new(
|
|
lsp::Position::new(0, 14),
|
|
lsp::Position::new(0, 14),
|
|
),
|
|
})),
|
|
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
|
..Default::default()
|
|
},
|
|
lsp::CompletionItem {
|
|
label: "second_method(…)".into(),
|
|
detail: Some("fn(&mut self, C) -> D<E>".into()),
|
|
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
|
new_text: "second_method()".to_string(),
|
|
range: lsp::Range::new(
|
|
lsp::Position::new(0, 14),
|
|
lsp::Position::new(0, 14),
|
|
),
|
|
})),
|
|
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
|
..Default::default()
|
|
},
|
|
])))
|
|
})
|
|
.next()
|
|
.await
|
|
.unwrap();
|
|
cx_a.executor().finish_waiting();
|
|
|
|
// Open the buffer on the host.
|
|
let buffer_a = project_a
|
|
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
|
.await
|
|
.unwrap();
|
|
cx_a.executor().run_until_parked();
|
|
|
|
buffer_a.read_with(cx_a, |buffer, _| {
|
|
assert_eq!(buffer.text(), "fn main() { a. }")
|
|
});
|
|
|
|
// Confirm a completion on the guest.
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
assert!(editor.context_menu_visible());
|
|
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
|
|
assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
|
|
});
|
|
|
|
// Return a resolved completion from the host's language server.
|
|
// The resolved completion has an additional text edit.
|
|
fake_language_server.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(
|
|
|params, _| async move {
|
|
assert_eq!(params.label, "first_method(…)");
|
|
Ok(lsp::CompletionItem {
|
|
label: "first_method(…)".into(),
|
|
detail: Some("fn(&mut self, B) -> C".into()),
|
|
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
|
new_text: "first_method($1)".to_string(),
|
|
range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
|
|
})),
|
|
additional_text_edits: Some(vec![lsp::TextEdit {
|
|
new_text: "use d::SomeTrait;\n".to_string(),
|
|
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
|
}]),
|
|
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
|
..Default::default()
|
|
})
|
|
},
|
|
);
|
|
|
|
// The additional edit is applied.
|
|
cx_a.executor().run_until_parked();
|
|
|
|
buffer_a.read_with(cx_a, |buffer, _| {
|
|
assert_eq!(
|
|
buffer.text(),
|
|
"use d::SomeTrait;\nfn main() { a.first_method() }"
|
|
);
|
|
});
|
|
|
|
buffer_b.read_with(cx_b, |buffer, _| {
|
|
assert_eq!(
|
|
buffer.text(),
|
|
"use d::SomeTrait;\nfn main() { a.first_method() }"
|
|
);
|
|
});
|
|
|
|
// Now we do a second completion, this time to ensure that documentation/snippets are
|
|
// resolved
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges([46..46])
|
|
});
|
|
editor.handle_input("; a", window, cx);
|
|
editor.handle_input(".", window, cx);
|
|
});
|
|
|
|
buffer_b.read_with(cx_b, |buffer, _| {
|
|
assert_eq!(
|
|
buffer.text(),
|
|
"use d::SomeTrait;\nfn main() { a.first_method(); a. }"
|
|
);
|
|
});
|
|
|
|
let mut completion_response = fake_language_server
|
|
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
|
|
assert_eq!(
|
|
params.text_document_position.text_document.uri,
|
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
);
|
|
assert_eq!(
|
|
params.text_document_position.position,
|
|
lsp::Position::new(1, 32),
|
|
);
|
|
|
|
Ok(Some(lsp::CompletionResponse::Array(vec![
|
|
lsp::CompletionItem {
|
|
label: "third_method(…)".into(),
|
|
detail: Some("fn(&mut self, B, C, D) -> E".into()),
|
|
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
|
// no snippet placehodlers
|
|
new_text: "third_method".to_string(),
|
|
range: lsp::Range::new(
|
|
lsp::Position::new(1, 32),
|
|
lsp::Position::new(1, 32),
|
|
),
|
|
})),
|
|
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
|
documentation: None,
|
|
..Default::default()
|
|
},
|
|
])))
|
|
});
|
|
|
|
// The completion now gets a new `text_edit.new_text` when resolving the completion item
|
|
let mut resolve_completion_response = fake_language_server
|
|
.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(|params, _| async move {
|
|
assert_eq!(params.label, "third_method(…)");
|
|
Ok(lsp::CompletionItem {
|
|
label: "third_method(…)".into(),
|
|
detail: Some("fn(&mut self, B, C, D) -> E".into()),
|
|
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
|
// Now it's a snippet
|
|
new_text: "third_method($1, $2, $3)".to_string(),
|
|
range: lsp::Range::new(lsp::Position::new(1, 32), lsp::Position::new(1, 32)),
|
|
})),
|
|
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
|
documentation: Some(lsp::Documentation::String(
|
|
"this is the documentation".into(),
|
|
)),
|
|
..Default::default()
|
|
})
|
|
});
|
|
|
|
cx_b.executor().run_until_parked();
|
|
|
|
completion_response.next().await.unwrap();
|
|
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
assert!(editor.context_menu_visible());
|
|
editor.context_menu_first(&ContextMenuFirst {}, window, cx);
|
|
});
|
|
|
|
resolve_completion_response.next().await.unwrap();
|
|
cx_b.executor().run_until_parked();
|
|
|
|
// When accepting the completion, the snippet is insert.
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
assert!(editor.context_menu_visible());
|
|
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
|
|
assert_eq!(
|
|
editor.text(cx),
|
|
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
async fn test_collaborating_with_code_actions(
|
|
cx_a: &mut TestAppContext,
|
|
cx_b: &mut TestAppContext,
|
|
) {
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
//
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
|
|
cx_b.update(editor::init);
|
|
|
|
// Set up a fake language server.
|
|
client_a.language_registry().add(rust_lang());
|
|
let mut fake_language_servers = client_a
|
|
.language_registry()
|
|
.register_fake_lsp("Rust", FakeLspAdapter::default());
|
|
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
path!("/a"),
|
|
json!({
|
|
"main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
|
|
"other.rs": "pub fn foo() -> usize { 4 }",
|
|
}),
|
|
)
|
|
.await;
|
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Join the project as client B.
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
|
let editor_b = workspace_b
|
|
.update_in(cx_b, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
let mut fake_language_server = fake_language_servers.next().await.unwrap();
|
|
let mut requests = fake_language_server
|
|
.set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
|
|
assert_eq!(
|
|
params.text_document.uri,
|
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
);
|
|
assert_eq!(params.range.start, lsp::Position::new(0, 0));
|
|
assert_eq!(params.range.end, lsp::Position::new(0, 0));
|
|
Ok(None)
|
|
});
|
|
cx_a.background_executor
|
|
.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
|
|
requests.next().await;
|
|
|
|
// Move cursor to a location that contains code actions.
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
|
|
});
|
|
});
|
|
cx_b.focus(&editor_b);
|
|
|
|
let mut requests = fake_language_server
|
|
.set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
|
|
assert_eq!(
|
|
params.text_document.uri,
|
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
);
|
|
assert_eq!(params.range.start, lsp::Position::new(1, 31));
|
|
assert_eq!(params.range.end, lsp::Position::new(1, 31));
|
|
|
|
Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
|
|
lsp::CodeAction {
|
|
title: "Inline into all callers".to_string(),
|
|
edit: Some(lsp::WorkspaceEdit {
|
|
changes: Some(
|
|
[
|
|
(
|
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
vec![lsp::TextEdit::new(
|
|
lsp::Range::new(
|
|
lsp::Position::new(1, 22),
|
|
lsp::Position::new(1, 34),
|
|
),
|
|
"4".to_string(),
|
|
)],
|
|
),
|
|
(
|
|
lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(),
|
|
vec![lsp::TextEdit::new(
|
|
lsp::Range::new(
|
|
lsp::Position::new(0, 0),
|
|
lsp::Position::new(0, 27),
|
|
),
|
|
"".to_string(),
|
|
)],
|
|
),
|
|
]
|
|
.into_iter()
|
|
.collect(),
|
|
),
|
|
..Default::default()
|
|
}),
|
|
data: Some(json!({
|
|
"codeActionParams": {
|
|
"range": {
|
|
"start": {"line": 1, "column": 31},
|
|
"end": {"line": 1, "column": 31},
|
|
}
|
|
}
|
|
})),
|
|
..Default::default()
|
|
},
|
|
)]))
|
|
});
|
|
cx_a.background_executor
|
|
.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
|
|
requests.next().await;
|
|
|
|
// Toggle code actions and wait for them to display.
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
editor.toggle_code_actions(
|
|
&ToggleCodeActions {
|
|
deployed_from: None,
|
|
quick_launch: false,
|
|
},
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
cx_a.background_executor.run_until_parked();
|
|
|
|
editor_b.update(cx_b, |editor, _| assert!(editor.context_menu_visible()));
|
|
|
|
fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
|
|
|
|
// Confirming the code action will trigger a resolve request.
|
|
let confirm_action = editor_b
|
|
.update_in(cx_b, |editor, window, cx| {
|
|
Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, window, cx)
|
|
})
|
|
.unwrap();
|
|
fake_language_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>(
|
|
|_, _| async move {
|
|
Ok(lsp::CodeAction {
|
|
title: "Inline into all callers".to_string(),
|
|
edit: Some(lsp::WorkspaceEdit {
|
|
changes: Some(
|
|
[
|
|
(
|
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
vec![lsp::TextEdit::new(
|
|
lsp::Range::new(
|
|
lsp::Position::new(1, 22),
|
|
lsp::Position::new(1, 34),
|
|
),
|
|
"4".to_string(),
|
|
)],
|
|
),
|
|
(
|
|
lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(),
|
|
vec![lsp::TextEdit::new(
|
|
lsp::Range::new(
|
|
lsp::Position::new(0, 0),
|
|
lsp::Position::new(0, 27),
|
|
),
|
|
"".to_string(),
|
|
)],
|
|
),
|
|
]
|
|
.into_iter()
|
|
.collect(),
|
|
),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
})
|
|
},
|
|
);
|
|
|
|
// After the action is confirmed, an editor containing both modified files is opened.
|
|
confirm_action.await.unwrap();
|
|
|
|
let code_action_editor = workspace_b.update(cx_b, |workspace, cx| {
|
|
workspace
|
|
.active_item(cx)
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap()
|
|
});
|
|
code_action_editor.update_in(cx_b, |editor, window, cx| {
|
|
assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
|
|
editor.undo(&Undo, window, cx);
|
|
assert_eq!(
|
|
editor.text(cx),
|
|
"mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
|
|
);
|
|
editor.redo(&Redo, window, cx);
|
|
assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
|
|
});
|
|
}
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
|
|
cx_b.update(editor::init);
|
|
|
|
// Set up a fake language server.
|
|
client_a.language_registry().add(rust_lang());
|
|
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
|
"Rust",
|
|
FakeLspAdapter {
|
|
capabilities: lsp::ServerCapabilities {
|
|
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
|
|
prepare_provider: Some(true),
|
|
work_done_progress_options: Default::default(),
|
|
})),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
},
|
|
);
|
|
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
path!("/dir"),
|
|
json!({
|
|
"one.rs": "const ONE: usize = 1;",
|
|
"two.rs": "const TWO: usize = one::ONE + one::ONE;"
|
|
}),
|
|
)
|
|
.await;
|
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
|
|
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
|
let editor_b = workspace_b
|
|
.update_in(cx_b, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
let fake_language_server = fake_language_servers.next().await.unwrap();
|
|
|
|
// Move cursor to a location that can be renamed.
|
|
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges([7..7])
|
|
});
|
|
editor.rename(&Rename, window, cx).unwrap()
|
|
});
|
|
|
|
fake_language_server
|
|
.set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
|
|
assert_eq!(
|
|
params.text_document.uri.as_str(),
|
|
uri!("file:///dir/one.rs")
|
|
);
|
|
assert_eq!(params.position, lsp::Position::new(0, 7));
|
|
Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
|
|
lsp::Position::new(0, 6),
|
|
lsp::Position::new(0, 9),
|
|
))))
|
|
})
|
|
.next()
|
|
.await
|
|
.unwrap();
|
|
prepare_rename.await.unwrap();
|
|
editor_b.update(cx_b, |editor, cx| {
|
|
use editor::ToOffset;
|
|
let rename = editor.pending_rename().unwrap();
|
|
let buffer = editor.buffer().read(cx).snapshot(cx);
|
|
assert_eq!(
|
|
rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
|
|
6..9
|
|
);
|
|
rename.editor.update(cx, |rename_editor, cx| {
|
|
let rename_selection = rename_editor.selections.newest::<usize>(cx);
|
|
assert_eq!(
|
|
rename_selection.range(),
|
|
0..3,
|
|
"Rename that was triggered from zero selection caret, should propose the whole word."
|
|
);
|
|
rename_editor.buffer().update(cx, |rename_buffer, cx| {
|
|
rename_buffer.edit([(0..3, "THREE")], None, cx);
|
|
});
|
|
});
|
|
});
|
|
|
|
// Cancel the rename, and repeat the same, but use selections instead of cursor movement
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
editor.cancel(&editor::actions::Cancel, window, cx);
|
|
});
|
|
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges([7..8])
|
|
});
|
|
editor.rename(&Rename, window, cx).unwrap()
|
|
});
|
|
|
|
fake_language_server
|
|
.set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
|
|
assert_eq!(
|
|
params.text_document.uri.as_str(),
|
|
uri!("file:///dir/one.rs")
|
|
);
|
|
assert_eq!(params.position, lsp::Position::new(0, 8));
|
|
Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
|
|
lsp::Position::new(0, 6),
|
|
lsp::Position::new(0, 9),
|
|
))))
|
|
})
|
|
.next()
|
|
.await
|
|
.unwrap();
|
|
prepare_rename.await.unwrap();
|
|
editor_b.update(cx_b, |editor, cx| {
|
|
use editor::ToOffset;
|
|
let rename = editor.pending_rename().unwrap();
|
|
let buffer = editor.buffer().read(cx).snapshot(cx);
|
|
let lsp_rename_start = rename.range.start.to_offset(&buffer);
|
|
let lsp_rename_end = rename.range.end.to_offset(&buffer);
|
|
assert_eq!(lsp_rename_start..lsp_rename_end, 6..9);
|
|
rename.editor.update(cx, |rename_editor, cx| {
|
|
let rename_selection = rename_editor.selections.newest::<usize>(cx);
|
|
assert_eq!(
|
|
rename_selection.range(),
|
|
1..2,
|
|
"Rename that was triggered from a selection, should have the same selection range in the rename proposal"
|
|
);
|
|
rename_editor.buffer().update(cx, |rename_buffer, cx| {
|
|
rename_buffer.edit([(0..lsp_rename_end - lsp_rename_start, "THREE")], None, cx);
|
|
});
|
|
});
|
|
});
|
|
|
|
let confirm_rename = editor_b.update_in(cx_b, |editor, window, cx| {
|
|
Editor::confirm_rename(editor, &ConfirmRename, window, cx).unwrap()
|
|
});
|
|
fake_language_server
|
|
.set_request_handler::<lsp::request::Rename, _, _>(|params, _| async move {
|
|
assert_eq!(
|
|
params.text_document_position.text_document.uri.as_str(),
|
|
uri!("file:///dir/one.rs")
|
|
);
|
|
assert_eq!(
|
|
params.text_document_position.position,
|
|
lsp::Position::new(0, 6)
|
|
);
|
|
assert_eq!(params.new_name, "THREE");
|
|
Ok(Some(lsp::WorkspaceEdit {
|
|
changes: Some(
|
|
[
|
|
(
|
|
lsp::Url::from_file_path(path!("/dir/one.rs")).unwrap(),
|
|
vec![lsp::TextEdit::new(
|
|
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
|
|
"THREE".to_string(),
|
|
)],
|
|
),
|
|
(
|
|
lsp::Url::from_file_path(path!("/dir/two.rs")).unwrap(),
|
|
vec![
|
|
lsp::TextEdit::new(
|
|
lsp::Range::new(
|
|
lsp::Position::new(0, 24),
|
|
lsp::Position::new(0, 27),
|
|
),
|
|
"THREE".to_string(),
|
|
),
|
|
lsp::TextEdit::new(
|
|
lsp::Range::new(
|
|
lsp::Position::new(0, 35),
|
|
lsp::Position::new(0, 38),
|
|
),
|
|
"THREE".to_string(),
|
|
),
|
|
],
|
|
),
|
|
]
|
|
.into_iter()
|
|
.collect(),
|
|
),
|
|
..Default::default()
|
|
}))
|
|
})
|
|
.next()
|
|
.await
|
|
.unwrap();
|
|
confirm_rename.await.unwrap();
|
|
|
|
let rename_editor = workspace_b.update(cx_b, |workspace, cx| {
|
|
workspace.active_item_as::<Editor>(cx).unwrap()
|
|
});
|
|
|
|
rename_editor.update_in(cx_b, |editor, window, cx| {
|
|
assert_eq!(
|
|
editor.text(cx),
|
|
"const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
|
|
);
|
|
editor.undo(&Undo, window, cx);
|
|
assert_eq!(
|
|
editor.text(cx),
|
|
"const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
|
|
);
|
|
editor.redo(&Redo, window, cx);
|
|
assert_eq!(
|
|
editor.text(cx),
|
|
"const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
|
|
);
|
|
});
|
|
|
|
// Ensure temporary rename edits cannot be undone/redone.
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
editor.undo(&Undo, window, cx);
|
|
assert_eq!(editor.text(cx), "const ONE: usize = 1;");
|
|
editor.undo(&Undo, window, cx);
|
|
assert_eq!(editor.text(cx), "const ONE: usize = 1;");
|
|
editor.redo(&Redo, window, cx);
|
|
assert_eq!(editor.text(cx), "const THREE: usize = 1;");
|
|
})
|
|
}
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let executor = cx_a.executor();
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
|
|
cx_b.update(editor::init);
|
|
|
|
client_a.language_registry().add(rust_lang());
|
|
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
|
"Rust",
|
|
FakeLspAdapter {
|
|
name: "the-language-server",
|
|
..Default::default()
|
|
},
|
|
);
|
|
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
path!("/dir"),
|
|
json!({
|
|
"main.rs": "const ONE: usize = 1;",
|
|
}),
|
|
)
|
|
.await;
|
|
let (project_a, _) = client_a.build_local_project(path!("/dir"), cx_a).await;
|
|
|
|
let _buffer_a = project_a
|
|
.update(cx_a, |p, cx| {
|
|
p.open_local_buffer_with_lsp(path!("/dir/main.rs"), cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let fake_language_server = fake_language_servers.next().await.unwrap();
|
|
fake_language_server.start_progress("the-token").await;
|
|
|
|
executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
|
|
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
|
|
token: lsp::NumberOrString::String("the-token".to_string()),
|
|
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
|
|
lsp::WorkDoneProgressReport {
|
|
message: Some("the-message".to_string()),
|
|
..Default::default()
|
|
},
|
|
)),
|
|
});
|
|
executor.run_until_parked();
|
|
|
|
project_a.read_with(cx_a, |project, cx| {
|
|
let status = project.language_server_statuses(cx).next().unwrap().1;
|
|
assert_eq!(status.name, "the-language-server");
|
|
assert_eq!(status.pending_work.len(), 1);
|
|
assert_eq!(
|
|
status.pending_work["the-token"].message.as_ref().unwrap(),
|
|
"the-message"
|
|
);
|
|
});
|
|
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
executor.run_until_parked();
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
|
|
project_b.read_with(cx_b, |project, cx| {
|
|
let status = project.language_server_statuses(cx).next().unwrap().1;
|
|
assert_eq!(status.name, "the-language-server");
|
|
});
|
|
|
|
executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
|
|
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
|
|
token: lsp::NumberOrString::String("the-token".to_string()),
|
|
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
|
|
lsp::WorkDoneProgressReport {
|
|
message: Some("the-message-2".to_string()),
|
|
..Default::default()
|
|
},
|
|
)),
|
|
});
|
|
executor.run_until_parked();
|
|
|
|
project_a.read_with(cx_a, |project, cx| {
|
|
let status = project.language_server_statuses(cx).next().unwrap().1;
|
|
assert_eq!(status.name, "the-language-server");
|
|
assert_eq!(status.pending_work.len(), 1);
|
|
assert_eq!(
|
|
status.pending_work["the-token"].message.as_ref().unwrap(),
|
|
"the-message-2"
|
|
);
|
|
});
|
|
|
|
project_b.read_with(cx_b, |project, cx| {
|
|
let status = project.language_server_statuses(cx).next().unwrap().1;
|
|
assert_eq!(status.name, "the-language-server");
|
|
assert_eq!(status.pending_work.len(), 1);
|
|
assert_eq!(
|
|
status.pending_work["the-token"].message.as_ref().unwrap(),
|
|
"the-message-2"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
async fn test_share_project(
|
|
cx_a: &mut TestAppContext,
|
|
cx_b: &mut TestAppContext,
|
|
cx_c: &mut TestAppContext,
|
|
) {
|
|
let executor = cx_a.executor();
|
|
let cx_b = cx_b.add_empty_window();
|
|
let mut server = TestServer::start(executor.clone()).await;
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
let client_c = server.create_client(cx_c, "user_c").await;
|
|
server
|
|
.make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
let active_call_b = cx_b.read(ActiveCall::global);
|
|
let active_call_c = cx_c.read(ActiveCall::global);
|
|
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
path!("/a"),
|
|
json!({
|
|
".gitignore": "ignored-dir",
|
|
"a.txt": "a-contents",
|
|
"b.txt": "b-contents",
|
|
"ignored-dir": {
|
|
"c.txt": "",
|
|
"d.txt": "",
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
// Invite client B to collaborate on a project
|
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
|
|
active_call_a
|
|
.update(cx_a, |call, cx| {
|
|
call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
// Join that project as client B
|
|
|
|
let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
|
|
executor.run_until_parked();
|
|
let call = incoming_call_b.borrow().clone().unwrap();
|
|
assert_eq!(call.calling_user.github_login, "user_a");
|
|
let initial_project = call.initial_project.unwrap();
|
|
active_call_b
|
|
.update(cx_b, |call, cx| call.accept_incoming(cx))
|
|
.await
|
|
.unwrap();
|
|
let client_b_peer_id = client_b.peer_id().unwrap();
|
|
let project_b = client_b.join_remote_project(initial_project.id, cx_b).await;
|
|
|
|
let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
|
|
|
|
executor.run_until_parked();
|
|
|
|
project_a.read_with(cx_a, |project, _| {
|
|
let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
|
|
assert_eq!(client_b_collaborator.replica_id, replica_id_b);
|
|
});
|
|
|
|
project_b.read_with(cx_b, |project, cx| {
|
|
let worktree = project.worktrees(cx).next().unwrap().read(cx);
|
|
assert_eq!(
|
|
worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
|
|
[
|
|
Path::new(".gitignore"),
|
|
Path::new("a.txt"),
|
|
Path::new("b.txt"),
|
|
Path::new("ignored-dir"),
|
|
]
|
|
);
|
|
});
|
|
|
|
project_b
|
|
.update(cx_b, |project, cx| {
|
|
let worktree = project.worktrees(cx).next().unwrap();
|
|
let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
|
|
project.expand_entry(worktree_id, entry.id, cx).unwrap()
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
project_b.read_with(cx_b, |project, cx| {
|
|
let worktree = project.worktrees(cx).next().unwrap().read(cx);
|
|
assert_eq!(
|
|
worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
|
|
[
|
|
Path::new(".gitignore"),
|
|
Path::new("a.txt"),
|
|
Path::new("b.txt"),
|
|
Path::new("ignored-dir"),
|
|
Path::new("ignored-dir/c.txt"),
|
|
Path::new("ignored-dir/d.txt"),
|
|
]
|
|
);
|
|
});
|
|
|
|
// Open the same file as client B and client A.
|
|
let buffer_b = project_b
|
|
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
|
|
|
|
project_a.read_with(cx_a, |project, cx| {
|
|
assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
|
|
});
|
|
let buffer_a = project_a
|
|
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
let editor_b =
|
|
cx_b.new_window_entity(|window, cx| Editor::for_buffer(buffer_b, None, window, cx));
|
|
|
|
// Client A sees client B's selection
|
|
executor.run_until_parked();
|
|
|
|
buffer_a.read_with(cx_a, |buffer, _| {
|
|
buffer
|
|
.snapshot()
|
|
.selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
|
|
.count()
|
|
== 1
|
|
});
|
|
|
|
// Edit the buffer as client B and see that edit as client A.
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
editor.handle_input("ok, ", window, cx)
|
|
});
|
|
executor.run_until_parked();
|
|
|
|
buffer_a.read_with(cx_a, |buffer, _| {
|
|
assert_eq!(buffer.text(), "ok, b-contents")
|
|
});
|
|
|
|
// Client B can invite client C on a project shared by client A.
|
|
active_call_b
|
|
.update(cx_b, |call, cx| {
|
|
call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
|
|
executor.run_until_parked();
|
|
let call = incoming_call_c.borrow().clone().unwrap();
|
|
assert_eq!(call.calling_user.github_login, "user_b");
|
|
let initial_project = call.initial_project.unwrap();
|
|
active_call_c
|
|
.update(cx_c, |call, cx| call.accept_incoming(cx))
|
|
.await
|
|
.unwrap();
|
|
let _project_c = client_c.join_remote_project(initial_project.id, cx_c).await;
|
|
|
|
// Client B closes the editor, and client A sees client B's selections removed.
|
|
cx_b.update(move |_, _| drop(editor_b));
|
|
executor.run_until_parked();
|
|
|
|
buffer_a.read_with(cx_a, |buffer, _| {
|
|
buffer
|
|
.snapshot()
|
|
.selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
|
|
.count()
|
|
== 0
|
|
});
|
|
}
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
async fn test_on_input_format_from_host_to_guest(
|
|
cx_a: &mut TestAppContext,
|
|
cx_b: &mut TestAppContext,
|
|
) {
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let executor = cx_a.executor();
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
|
|
client_a.language_registry().add(rust_lang());
|
|
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
|
"Rust",
|
|
FakeLspAdapter {
|
|
capabilities: lsp::ServerCapabilities {
|
|
document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
|
|
first_trigger_character: ":".to_string(),
|
|
more_trigger_character: Some(vec![">".to_string()]),
|
|
}),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
},
|
|
);
|
|
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
path!("/a"),
|
|
json!({
|
|
"main.rs": "fn main() { a }",
|
|
"other.rs": "// Test file",
|
|
}),
|
|
)
|
|
.await;
|
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
|
|
// Open a file in an editor as the host.
|
|
let buffer_a = project_a
|
|
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
|
.await
|
|
.unwrap();
|
|
let cx_a = cx_a.add_empty_window();
|
|
let editor_a = cx_a.new_window_entity(|window, cx| {
|
|
Editor::for_buffer(buffer_a, Some(project_a.clone()), window, cx)
|
|
});
|
|
|
|
let fake_language_server = fake_language_servers.next().await.unwrap();
|
|
executor.run_until_parked();
|
|
|
|
// Receive an OnTypeFormatting request as the host's language server.
|
|
// Return some formatting from the host's language server.
|
|
fake_language_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
|
|
|params, _| async move {
|
|
assert_eq!(
|
|
params.text_document_position.text_document.uri,
|
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
);
|
|
assert_eq!(
|
|
params.text_document_position.position,
|
|
lsp::Position::new(0, 14),
|
|
);
|
|
|
|
Ok(Some(vec![lsp::TextEdit {
|
|
new_text: "~<".to_string(),
|
|
range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
|
|
}]))
|
|
},
|
|
);
|
|
|
|
// Open the buffer on the guest and see that the formatting worked
|
|
let buffer_b = project_b
|
|
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Type a on type formatting trigger character as the guest.
|
|
cx_a.focus(&editor_a);
|
|
editor_a.update_in(cx_a, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges([13..13])
|
|
});
|
|
editor.handle_input(">", window, cx);
|
|
});
|
|
|
|
executor.run_until_parked();
|
|
|
|
buffer_b.read_with(cx_b, |buffer, _| {
|
|
assert_eq!(buffer.text(), "fn main() { a>~< }")
|
|
});
|
|
|
|
// Undo should remove LSP edits first
|
|
editor_a.update_in(cx_a, |editor, window, cx| {
|
|
assert_eq!(editor.text(cx), "fn main() { a>~< }");
|
|
editor.undo(&Undo, window, cx);
|
|
assert_eq!(editor.text(cx), "fn main() { a> }");
|
|
});
|
|
executor.run_until_parked();
|
|
|
|
buffer_b.read_with(cx_b, |buffer, _| {
|
|
assert_eq!(buffer.text(), "fn main() { a> }")
|
|
});
|
|
|
|
editor_a.update_in(cx_a, |editor, window, cx| {
|
|
assert_eq!(editor.text(cx), "fn main() { a> }");
|
|
editor.undo(&Undo, window, cx);
|
|
assert_eq!(editor.text(cx), "fn main() { a }");
|
|
});
|
|
executor.run_until_parked();
|
|
|
|
buffer_b.read_with(cx_b, |buffer, _| {
|
|
assert_eq!(buffer.text(), "fn main() { a }")
|
|
});
|
|
}
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
async fn test_on_input_format_from_guest_to_host(
|
|
cx_a: &mut TestAppContext,
|
|
cx_b: &mut TestAppContext,
|
|
) {
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let executor = cx_a.executor();
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
|
|
client_a.language_registry().add(rust_lang());
|
|
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
|
"Rust",
|
|
FakeLspAdapter {
|
|
capabilities: lsp::ServerCapabilities {
|
|
document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
|
|
first_trigger_character: ":".to_string(),
|
|
more_trigger_character: Some(vec![">".to_string()]),
|
|
}),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
},
|
|
);
|
|
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
path!("/a"),
|
|
json!({
|
|
"main.rs": "fn main() { a }",
|
|
"other.rs": "// Test file",
|
|
}),
|
|
)
|
|
.await;
|
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
|
|
// Open a file in an editor as the guest.
|
|
let buffer_b = project_b
|
|
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
|
.await
|
|
.unwrap();
|
|
let cx_b = cx_b.add_empty_window();
|
|
let editor_b = cx_b.new_window_entity(|window, cx| {
|
|
Editor::for_buffer(buffer_b, Some(project_b.clone()), window, cx)
|
|
});
|
|
|
|
let fake_language_server = fake_language_servers.next().await.unwrap();
|
|
executor.run_until_parked();
|
|
|
|
// Type a on type formatting trigger character as the guest.
|
|
cx_b.focus(&editor_b);
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges([13..13])
|
|
});
|
|
editor.handle_input(":", window, cx);
|
|
});
|
|
|
|
// Receive an OnTypeFormatting request as the host's language server.
|
|
// Return some formatting from the host's language server.
|
|
executor.start_waiting();
|
|
fake_language_server
|
|
.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
|
|
assert_eq!(
|
|
params.text_document_position.text_document.uri,
|
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
);
|
|
assert_eq!(
|
|
params.text_document_position.position,
|
|
lsp::Position::new(0, 14),
|
|
);
|
|
|
|
Ok(Some(vec![lsp::TextEdit {
|
|
new_text: "~:".to_string(),
|
|
range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
|
|
}]))
|
|
})
|
|
.next()
|
|
.await
|
|
.unwrap();
|
|
executor.finish_waiting();
|
|
|
|
// Open the buffer on the host and see that the formatting worked
|
|
let buffer_a = project_a
|
|
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
|
.await
|
|
.unwrap();
|
|
executor.run_until_parked();
|
|
|
|
buffer_a.read_with(cx_a, |buffer, _| {
|
|
assert_eq!(buffer.text(), "fn main() { a:~: }")
|
|
});
|
|
|
|
// Undo should remove LSP edits first
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
assert_eq!(editor.text(cx), "fn main() { a:~: }");
|
|
editor.undo(&Undo, window, cx);
|
|
assert_eq!(editor.text(cx), "fn main() { a: }");
|
|
});
|
|
executor.run_until_parked();
|
|
|
|
buffer_a.read_with(cx_a, |buffer, _| {
|
|
assert_eq!(buffer.text(), "fn main() { a: }")
|
|
});
|
|
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
assert_eq!(editor.text(cx), "fn main() { a: }");
|
|
editor.undo(&Undo, window, cx);
|
|
assert_eq!(editor.text(cx), "fn main() { a }");
|
|
});
|
|
executor.run_until_parked();
|
|
|
|
buffer_a.read_with(cx_a, |buffer, _| {
|
|
assert_eq!(buffer.text(), "fn main() { a }")
|
|
});
|
|
}
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
async fn test_mutual_editor_inlay_hint_cache_update(
|
|
cx_a: &mut TestAppContext,
|
|
cx_b: &mut TestAppContext,
|
|
) {
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let executor = cx_a.executor();
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
let active_call_b = cx_b.read(ActiveCall::global);
|
|
|
|
cx_a.update(editor::init);
|
|
cx_b.update(editor::init);
|
|
|
|
cx_a.update(|cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
|
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
|
enabled: true,
|
|
show_value_hints: true,
|
|
edit_debounce_ms: 0,
|
|
scroll_debounce_ms: 0,
|
|
show_type_hints: true,
|
|
show_parameter_hints: false,
|
|
show_other_hints: true,
|
|
show_background: false,
|
|
toggle_on_modifiers_press: None,
|
|
})
|
|
});
|
|
});
|
|
});
|
|
cx_b.update(|cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
|
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
|
show_value_hints: true,
|
|
enabled: true,
|
|
edit_debounce_ms: 0,
|
|
scroll_debounce_ms: 0,
|
|
show_type_hints: true,
|
|
show_parameter_hints: false,
|
|
show_other_hints: true,
|
|
show_background: false,
|
|
toggle_on_modifiers_press: None,
|
|
})
|
|
});
|
|
});
|
|
});
|
|
|
|
client_a.language_registry().add(rust_lang());
|
|
client_b.language_registry().add(rust_lang());
|
|
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
|
"Rust",
|
|
FakeLspAdapter {
|
|
capabilities: lsp::ServerCapabilities {
|
|
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
},
|
|
);
|
|
|
|
// Client A opens a project.
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
path!("/a"),
|
|
json!({
|
|
"main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
|
|
"other.rs": "// Test file",
|
|
}),
|
|
)
|
|
.await;
|
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
|
|
active_call_a
|
|
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
|
.await
|
|
.unwrap();
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Client B joins the project
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
active_call_b
|
|
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
|
executor.start_waiting();
|
|
|
|
// The host opens a rust file.
|
|
let _buffer_a = project_a
|
|
.update(cx_a, |project, cx| {
|
|
project.open_local_buffer(path!("/a/main.rs"), cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let editor_a = workspace_a
|
|
.update_in(cx_a, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
let fake_language_server = fake_language_servers.next().await.unwrap();
|
|
|
|
// Set up the language server to return an additional inlay hint on each request.
|
|
let edits_made = Arc::new(AtomicUsize::new(0));
|
|
let closure_edits_made = Arc::clone(&edits_made);
|
|
fake_language_server
|
|
.set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
|
|
let task_edits_made = Arc::clone(&closure_edits_made);
|
|
async move {
|
|
assert_eq!(
|
|
params.text_document.uri,
|
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
);
|
|
let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
|
|
Ok(Some(vec![lsp::InlayHint {
|
|
position: lsp::Position::new(0, edits_made as u32),
|
|
label: lsp::InlayHintLabel::String(edits_made.to_string()),
|
|
kind: None,
|
|
text_edits: None,
|
|
tooltip: None,
|
|
padding_left: None,
|
|
padding_right: None,
|
|
data: None,
|
|
}]))
|
|
}
|
|
})
|
|
.next()
|
|
.await
|
|
.unwrap();
|
|
|
|
executor.run_until_parked();
|
|
|
|
let initial_edit = edits_made.load(atomic::Ordering::Acquire);
|
|
editor_a.update(cx_a, |editor, _| {
|
|
assert_eq!(
|
|
vec![initial_edit.to_string()],
|
|
extract_hint_labels(editor),
|
|
"Host should get its first hints when opens an editor"
|
|
);
|
|
});
|
|
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
|
let editor_b = workspace_b
|
|
.update_in(cx_b, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
executor.run_until_parked();
|
|
editor_b.update(cx_b, |editor, _| {
|
|
assert_eq!(
|
|
vec![initial_edit.to_string()],
|
|
extract_hint_labels(editor),
|
|
"Client should get its first hints when opens an editor"
|
|
);
|
|
});
|
|
|
|
let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges([13..13].clone())
|
|
});
|
|
editor.handle_input(":", window, cx);
|
|
});
|
|
cx_b.focus(&editor_b);
|
|
|
|
executor.run_until_parked();
|
|
editor_a.update(cx_a, |editor, _| {
|
|
assert_eq!(
|
|
vec![after_client_edit.to_string()],
|
|
extract_hint_labels(editor),
|
|
);
|
|
});
|
|
editor_b.update(cx_b, |editor, _| {
|
|
assert_eq!(
|
|
vec![after_client_edit.to_string()],
|
|
extract_hint_labels(editor),
|
|
);
|
|
});
|
|
|
|
let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
|
|
editor_a.update_in(cx_a, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges([13..13])
|
|
});
|
|
editor.handle_input("a change to increment both buffers' versions", window, cx);
|
|
});
|
|
cx_a.focus(&editor_a);
|
|
|
|
executor.run_until_parked();
|
|
editor_a.update(cx_a, |editor, _| {
|
|
assert_eq!(
|
|
vec![after_host_edit.to_string()],
|
|
extract_hint_labels(editor),
|
|
);
|
|
});
|
|
editor_b.update(cx_b, |editor, _| {
|
|
assert_eq!(
|
|
vec![after_host_edit.to_string()],
|
|
extract_hint_labels(editor),
|
|
);
|
|
});
|
|
|
|
let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
|
|
fake_language_server
|
|
.request::<lsp::request::InlayHintRefreshRequest>(())
|
|
.await
|
|
.into_response()
|
|
.expect("inlay refresh request failed");
|
|
|
|
executor.run_until_parked();
|
|
editor_a.update(cx_a, |editor, _| {
|
|
assert_eq!(
|
|
vec![after_special_edit_for_refresh.to_string()],
|
|
extract_hint_labels(editor),
|
|
"Host should react to /refresh LSP request"
|
|
);
|
|
});
|
|
editor_b.update(cx_b, |editor, _| {
|
|
assert_eq!(
|
|
vec![after_special_edit_for_refresh.to_string()],
|
|
extract_hint_labels(editor),
|
|
"Guest should get a /refresh LSP request propagated by host"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
async fn test_inlay_hint_refresh_is_forwarded(
|
|
cx_a: &mut TestAppContext,
|
|
cx_b: &mut TestAppContext,
|
|
) {
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let executor = cx_a.executor();
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
let active_call_b = cx_b.read(ActiveCall::global);
|
|
|
|
cx_a.update(editor::init);
|
|
cx_b.update(editor::init);
|
|
|
|
cx_a.update(|cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
|
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
|
show_value_hints: true,
|
|
enabled: false,
|
|
edit_debounce_ms: 0,
|
|
scroll_debounce_ms: 0,
|
|
show_type_hints: false,
|
|
show_parameter_hints: false,
|
|
show_other_hints: false,
|
|
show_background: false,
|
|
toggle_on_modifiers_press: None,
|
|
})
|
|
});
|
|
});
|
|
});
|
|
cx_b.update(|cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
|
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
|
show_value_hints: true,
|
|
enabled: true,
|
|
edit_debounce_ms: 0,
|
|
scroll_debounce_ms: 0,
|
|
show_type_hints: true,
|
|
show_parameter_hints: true,
|
|
show_other_hints: true,
|
|
show_background: false,
|
|
toggle_on_modifiers_press: None,
|
|
})
|
|
});
|
|
});
|
|
});
|
|
|
|
client_a.language_registry().add(rust_lang());
|
|
client_b.language_registry().add(rust_lang());
|
|
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
|
"Rust",
|
|
FakeLspAdapter {
|
|
capabilities: lsp::ServerCapabilities {
|
|
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
},
|
|
);
|
|
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
path!("/a"),
|
|
json!({
|
|
"main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
|
|
"other.rs": "// Test file",
|
|
}),
|
|
)
|
|
.await;
|
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
|
|
active_call_a
|
|
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
|
.await
|
|
.unwrap();
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
active_call_b
|
|
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
|
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
|
|
|
cx_a.background_executor.start_waiting();
|
|
|
|
let editor_a = workspace_a
|
|
.update_in(cx_a, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
let editor_b = workspace_b
|
|
.update_in(cx_b, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
let other_hints = Arc::new(AtomicBool::new(false));
|
|
let fake_language_server = fake_language_servers.next().await.unwrap();
|
|
let closure_other_hints = Arc::clone(&other_hints);
|
|
fake_language_server
|
|
.set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
|
|
let task_other_hints = Arc::clone(&closure_other_hints);
|
|
async move {
|
|
assert_eq!(
|
|
params.text_document.uri,
|
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
);
|
|
let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
|
|
let character = if other_hints { 0 } else { 2 };
|
|
let label = if other_hints {
|
|
"other hint"
|
|
} else {
|
|
"initial hint"
|
|
};
|
|
Ok(Some(vec![lsp::InlayHint {
|
|
position: lsp::Position::new(0, character),
|
|
label: lsp::InlayHintLabel::String(label.to_string()),
|
|
kind: None,
|
|
text_edits: None,
|
|
tooltip: None,
|
|
padding_left: None,
|
|
padding_right: None,
|
|
data: None,
|
|
}]))
|
|
}
|
|
})
|
|
.next()
|
|
.await
|
|
.unwrap();
|
|
executor.finish_waiting();
|
|
|
|
executor.run_until_parked();
|
|
editor_a.update(cx_a, |editor, _| {
|
|
assert!(
|
|
extract_hint_labels(editor).is_empty(),
|
|
"Host should get no hints due to them turned off"
|
|
);
|
|
});
|
|
|
|
executor.run_until_parked();
|
|
editor_b.update(cx_b, |editor, _| {
|
|
assert_eq!(
|
|
vec!["initial hint".to_string()],
|
|
extract_hint_labels(editor),
|
|
"Client should get its first hints when opens an editor"
|
|
);
|
|
});
|
|
|
|
other_hints.fetch_or(true, atomic::Ordering::Release);
|
|
fake_language_server
|
|
.request::<lsp::request::InlayHintRefreshRequest>(())
|
|
.await
|
|
.into_response()
|
|
.expect("inlay refresh request failed");
|
|
executor.run_until_parked();
|
|
editor_a.update(cx_a, |editor, _| {
|
|
assert!(
|
|
extract_hint_labels(editor).is_empty(),
|
|
"Host should get no hints due to them turned off, even after the /refresh"
|
|
);
|
|
});
|
|
|
|
executor.run_until_parked();
|
|
editor_b.update(cx_b, |editor, _| {
|
|
assert_eq!(
|
|
vec!["other hint".to_string()],
|
|
extract_hint_labels(editor),
|
|
"Guest should get a /refresh LSP request propagated by host despite host hints are off"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
|
let expected_color = Rgba {
|
|
r: 0.33,
|
|
g: 0.33,
|
|
b: 0.33,
|
|
a: 0.33,
|
|
};
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let executor = cx_a.executor();
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
let active_call_b = cx_b.read(ActiveCall::global);
|
|
|
|
cx_a.update(editor::init);
|
|
cx_b.update(editor::init);
|
|
|
|
cx_a.update(|cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings::<EditorSettings>(cx, |settings| {
|
|
settings.lsp_document_colors = Some(DocumentColorsRenderMode::None);
|
|
});
|
|
});
|
|
});
|
|
cx_b.update(|cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings::<EditorSettings>(cx, |settings| {
|
|
settings.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
|
|
});
|
|
});
|
|
});
|
|
|
|
client_a.language_registry().add(rust_lang());
|
|
client_b.language_registry().add(rust_lang());
|
|
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
|
"Rust",
|
|
FakeLspAdapter {
|
|
capabilities: lsp::ServerCapabilities {
|
|
color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
|
|
..lsp::ServerCapabilities::default()
|
|
},
|
|
..FakeLspAdapter::default()
|
|
},
|
|
);
|
|
|
|
// Client A opens a project.
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
path!("/a"),
|
|
json!({
|
|
"main.rs": "fn main() { a }",
|
|
}),
|
|
)
|
|
.await;
|
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
|
|
active_call_a
|
|
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
|
.await
|
|
.unwrap();
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Client B joins the project
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
active_call_b
|
|
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
|
|
|
// The host opens a rust file.
|
|
let _buffer_a = project_a
|
|
.update(cx_a, |project, cx| {
|
|
project.open_local_buffer(path!("/a/main.rs"), cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let editor_a = workspace_a
|
|
.update_in(cx_a, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
let fake_language_server = fake_language_servers.next().await.unwrap();
|
|
|
|
let requests_made = Arc::new(AtomicUsize::new(0));
|
|
let closure_requests_made = Arc::clone(&requests_made);
|
|
let mut color_request_handle = fake_language_server
|
|
.set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
|
|
let requests_made = Arc::clone(&closure_requests_made);
|
|
async move {
|
|
assert_eq!(
|
|
params.text_document.uri,
|
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
);
|
|
requests_made.fetch_add(1, atomic::Ordering::Release);
|
|
Ok(vec![lsp::ColorInformation {
|
|
range: lsp::Range {
|
|
start: lsp::Position {
|
|
line: 0,
|
|
character: 0,
|
|
},
|
|
end: lsp::Position {
|
|
line: 0,
|
|
character: 1,
|
|
},
|
|
},
|
|
color: lsp::Color {
|
|
red: 0.33,
|
|
green: 0.33,
|
|
blue: 0.33,
|
|
alpha: 0.33,
|
|
},
|
|
}])
|
|
}
|
|
});
|
|
executor.run_until_parked();
|
|
|
|
assert_eq!(
|
|
0,
|
|
requests_made.load(atomic::Ordering::Acquire),
|
|
"Host did not enable document colors, hence should query for none"
|
|
);
|
|
editor_a.update(cx_a, |editor, cx| {
|
|
assert_eq!(
|
|
Vec::<Rgba>::new(),
|
|
extract_color_inlays(editor, cx),
|
|
"No query colors should result in no hints"
|
|
);
|
|
});
|
|
|
|
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
|
let editor_b = workspace_b
|
|
.update_in(cx_b, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
color_request_handle.next().await.unwrap();
|
|
executor.run_until_parked();
|
|
|
|
assert_eq!(
|
|
1,
|
|
requests_made.load(atomic::Ordering::Acquire),
|
|
"The client opened the file and got its first colors back"
|
|
);
|
|
editor_b.update(cx_b, |editor, cx| {
|
|
assert_eq!(
|
|
vec![expected_color],
|
|
extract_color_inlays(editor, cx),
|
|
"With document colors as inlays, color inlays should be pushed"
|
|
);
|
|
});
|
|
|
|
editor_a.update_in(cx_a, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges([13..13].clone())
|
|
});
|
|
editor.handle_input(":", window, cx);
|
|
});
|
|
color_request_handle.next().await.unwrap();
|
|
executor.run_until_parked();
|
|
assert_eq!(
|
|
2,
|
|
requests_made.load(atomic::Ordering::Acquire),
|
|
"After the host edits his file, the client should request the colors again"
|
|
);
|
|
editor_a.update(cx_a, |editor, cx| {
|
|
assert_eq!(
|
|
Vec::<Rgba>::new(),
|
|
extract_color_inlays(editor, cx),
|
|
"Host has no colors still"
|
|
);
|
|
});
|
|
editor_b.update(cx_b, |editor, cx| {
|
|
assert_eq!(vec![expected_color], extract_color_inlays(editor, cx),);
|
|
});
|
|
|
|
cx_b.update(|_, cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings::<EditorSettings>(cx, |settings| {
|
|
settings.lsp_document_colors = Some(DocumentColorsRenderMode::Background);
|
|
});
|
|
});
|
|
});
|
|
executor.run_until_parked();
|
|
assert_eq!(
|
|
2,
|
|
requests_made.load(atomic::Ordering::Acquire),
|
|
"After the client have changed the colors settings, no extra queries should happen"
|
|
);
|
|
editor_a.update(cx_a, |editor, cx| {
|
|
assert_eq!(
|
|
Vec::<Rgba>::new(),
|
|
extract_color_inlays(editor, cx),
|
|
"Host is unaffected by the client's settings changes"
|
|
);
|
|
});
|
|
editor_b.update(cx_b, |editor, cx| {
|
|
assert_eq!(
|
|
Vec::<Rgba>::new(),
|
|
extract_color_inlays(editor, cx),
|
|
"Client should have no colors hints, as in the settings"
|
|
);
|
|
});
|
|
|
|
cx_b.update(|_, cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings::<EditorSettings>(cx, |settings| {
|
|
settings.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
|
|
});
|
|
});
|
|
});
|
|
executor.run_until_parked();
|
|
assert_eq!(
|
|
2,
|
|
requests_made.load(atomic::Ordering::Acquire),
|
|
"After falling back to colors as inlays, no extra LSP queries are made"
|
|
);
|
|
editor_a.update(cx_a, |editor, cx| {
|
|
assert_eq!(
|
|
Vec::<Rgba>::new(),
|
|
extract_color_inlays(editor, cx),
|
|
"Host is unaffected by the client's settings changes, again"
|
|
);
|
|
});
|
|
editor_b.update(cx_b, |editor, cx| {
|
|
assert_eq!(
|
|
vec![expected_color],
|
|
extract_color_inlays(editor, cx),
|
|
"Client should have its color hints back"
|
|
);
|
|
});
|
|
|
|
cx_a.update(|_, cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings::<EditorSettings>(cx, |settings| {
|
|
settings.lsp_document_colors = Some(DocumentColorsRenderMode::Border);
|
|
});
|
|
});
|
|
});
|
|
color_request_handle.next().await.unwrap();
|
|
executor.run_until_parked();
|
|
assert_eq!(
|
|
3,
|
|
requests_made.load(atomic::Ordering::Acquire),
|
|
"After the host enables document colors, another LSP query should be made"
|
|
);
|
|
editor_a.update(cx_a, |editor, cx| {
|
|
assert_eq!(
|
|
Vec::<Rgba>::new(),
|
|
extract_color_inlays(editor, cx),
|
|
"Host did not configure document colors as hints hence gets nothing"
|
|
);
|
|
});
|
|
editor_b.update(cx_b, |editor, cx| {
|
|
assert_eq!(
|
|
vec![expected_color],
|
|
extract_color_inlays(editor, cx),
|
|
"Client should be unaffected by the host's settings changes"
|
|
);
|
|
});
|
|
}
|
|
|
|
async fn test_lsp_pull_diagnostics(
|
|
should_stream_workspace_diagnostic: bool,
|
|
cx_a: &mut TestAppContext,
|
|
cx_b: &mut TestAppContext,
|
|
) {
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let executor = cx_a.executor();
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
let active_call_b = cx_b.read(ActiveCall::global);
|
|
|
|
cx_a.update(editor::init);
|
|
cx_b.update(editor::init);
|
|
|
|
client_a.language_registry().add(rust_lang());
|
|
client_b.language_registry().add(rust_lang());
|
|
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
|
"Rust",
|
|
FakeLspAdapter {
|
|
capabilities: lsp::ServerCapabilities {
|
|
diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
|
|
lsp::DiagnosticOptions {
|
|
identifier: Some("test-pulls".to_string()),
|
|
inter_file_dependencies: true,
|
|
workspace_diagnostics: true,
|
|
work_done_progress_options: lsp::WorkDoneProgressOptions {
|
|
work_done_progress: None,
|
|
},
|
|
},
|
|
)),
|
|
..lsp::ServerCapabilities::default()
|
|
},
|
|
..FakeLspAdapter::default()
|
|
},
|
|
);
|
|
|
|
// Client A opens a project.
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
path!("/a"),
|
|
json!({
|
|
"main.rs": "fn main() { a }",
|
|
"lib.rs": "fn other() {}",
|
|
}),
|
|
)
|
|
.await;
|
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
|
|
active_call_a
|
|
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
|
.await
|
|
.unwrap();
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Client B joins the project
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
active_call_b
|
|
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
|
executor.start_waiting();
|
|
|
|
// The host opens a rust file.
|
|
let _buffer_a = project_a
|
|
.update(cx_a, |project, cx| {
|
|
project.open_local_buffer(path!("/a/main.rs"), cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let editor_a_main = workspace_a
|
|
.update_in(cx_a, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
let fake_language_server = fake_language_servers.next().await.unwrap();
|
|
let expected_push_diagnostic_main_message = "pushed main diagnostic";
|
|
let expected_push_diagnostic_lib_message = "pushed lib diagnostic";
|
|
let expected_pull_diagnostic_main_message = "pulled main diagnostic";
|
|
let expected_pull_diagnostic_lib_message = "pulled lib diagnostic";
|
|
let expected_workspace_pull_diagnostics_main_message = "pulled workspace main diagnostic";
|
|
let expected_workspace_pull_diagnostics_lib_message = "pulled workspace lib diagnostic";
|
|
|
|
let diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::<Option<String>>::new()));
|
|
let workspace_diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::<String>::new()));
|
|
let diagnostics_pulls_made = Arc::new(AtomicUsize::new(0));
|
|
let closure_diagnostics_pulls_made = diagnostics_pulls_made.clone();
|
|
let closure_diagnostics_pulls_result_ids = diagnostics_pulls_result_ids.clone();
|
|
let mut pull_diagnostics_handle = fake_language_server
|
|
.set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
|
|
let requests_made = closure_diagnostics_pulls_made.clone();
|
|
let diagnostics_pulls_result_ids = closure_diagnostics_pulls_result_ids.clone();
|
|
async move {
|
|
let message = if lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
|
|
== params.text_document.uri
|
|
{
|
|
expected_pull_diagnostic_main_message.to_string()
|
|
} else if lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap()
|
|
== params.text_document.uri
|
|
{
|
|
expected_pull_diagnostic_lib_message.to_string()
|
|
} else {
|
|
panic!("Unexpected document: {}", params.text_document.uri)
|
|
};
|
|
{
|
|
diagnostics_pulls_result_ids
|
|
.lock()
|
|
.await
|
|
.insert(params.previous_result_id);
|
|
}
|
|
let new_requests_count = requests_made.fetch_add(1, atomic::Ordering::Release) + 1;
|
|
Ok(lsp::DocumentDiagnosticReportResult::Report(
|
|
lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
|
|
related_documents: None,
|
|
full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
|
|
result_id: Some(format!("pull-{new_requests_count}")),
|
|
items: vec![lsp::Diagnostic {
|
|
range: lsp::Range {
|
|
start: lsp::Position {
|
|
line: 0,
|
|
character: 0,
|
|
},
|
|
end: lsp::Position {
|
|
line: 0,
|
|
character: 2,
|
|
},
|
|
},
|
|
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
|
message,
|
|
..lsp::Diagnostic::default()
|
|
}],
|
|
},
|
|
}),
|
|
))
|
|
}
|
|
});
|
|
|
|
let workspace_diagnostics_pulls_made = Arc::new(AtomicUsize::new(0));
|
|
let closure_workspace_diagnostics_pulls_made = workspace_diagnostics_pulls_made.clone();
|
|
let closure_workspace_diagnostics_pulls_result_ids =
|
|
workspace_diagnostics_pulls_result_ids.clone();
|
|
let (workspace_diagnostic_cancel_tx, closure_workspace_diagnostic_cancel_rx) =
|
|
smol::channel::bounded::<()>(1);
|
|
let (closure_workspace_diagnostic_received_tx, workspace_diagnostic_received_rx) =
|
|
smol::channel::bounded::<()>(1);
|
|
let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
|
|
"workspace/diagnostic-{}-1",
|
|
fake_language_server.server.server_id()
|
|
));
|
|
let closure_expected_workspace_diagnostic_token = expected_workspace_diagnostic_token.clone();
|
|
let mut workspace_diagnostics_pulls_handle = fake_language_server
|
|
.set_request_handler::<lsp::request::WorkspaceDiagnosticRequest, _, _>(
|
|
move |params, _| {
|
|
let workspace_requests_made = closure_workspace_diagnostics_pulls_made.clone();
|
|
let workspace_diagnostics_pulls_result_ids =
|
|
closure_workspace_diagnostics_pulls_result_ids.clone();
|
|
let workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone();
|
|
let workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone();
|
|
let expected_workspace_diagnostic_token =
|
|
closure_expected_workspace_diagnostic_token.clone();
|
|
async move {
|
|
let workspace_request_count =
|
|
workspace_requests_made.fetch_add(1, atomic::Ordering::Release) + 1;
|
|
{
|
|
workspace_diagnostics_pulls_result_ids
|
|
.lock()
|
|
.await
|
|
.extend(params.previous_result_ids.into_iter().map(|id| id.value));
|
|
}
|
|
if should_stream_workspace_diagnostic && !workspace_diagnostic_cancel_rx.is_closed()
|
|
{
|
|
assert_eq!(
|
|
params.partial_result_params.partial_result_token,
|
|
Some(expected_workspace_diagnostic_token)
|
|
);
|
|
workspace_diagnostic_received_tx.send(()).await.unwrap();
|
|
workspace_diagnostic_cancel_rx.recv().await.unwrap();
|
|
workspace_diagnostic_cancel_rx.close();
|
|
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#partialResults
|
|
// > The final response has to be empty in terms of result values.
|
|
return Ok(lsp::WorkspaceDiagnosticReportResult::Report(
|
|
lsp::WorkspaceDiagnosticReport { items: Vec::new() },
|
|
));
|
|
}
|
|
Ok(lsp::WorkspaceDiagnosticReportResult::Report(
|
|
lsp::WorkspaceDiagnosticReport {
|
|
items: vec![
|
|
lsp::WorkspaceDocumentDiagnosticReport::Full(
|
|
lsp::WorkspaceFullDocumentDiagnosticReport {
|
|
uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
version: None,
|
|
full_document_diagnostic_report:
|
|
lsp::FullDocumentDiagnosticReport {
|
|
result_id: Some(format!(
|
|
"workspace_{workspace_request_count}"
|
|
)),
|
|
items: vec![lsp::Diagnostic {
|
|
range: lsp::Range {
|
|
start: lsp::Position {
|
|
line: 0,
|
|
character: 1,
|
|
},
|
|
end: lsp::Position {
|
|
line: 0,
|
|
character: 3,
|
|
},
|
|
},
|
|
severity: Some(lsp::DiagnosticSeverity::WARNING),
|
|
message:
|
|
expected_workspace_pull_diagnostics_main_message
|
|
.to_string(),
|
|
..lsp::Diagnostic::default()
|
|
}],
|
|
},
|
|
},
|
|
),
|
|
lsp::WorkspaceDocumentDiagnosticReport::Full(
|
|
lsp::WorkspaceFullDocumentDiagnosticReport {
|
|
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
|
|
version: None,
|
|
full_document_diagnostic_report:
|
|
lsp::FullDocumentDiagnosticReport {
|
|
result_id: Some(format!(
|
|
"workspace_{workspace_request_count}"
|
|
)),
|
|
items: vec![lsp::Diagnostic {
|
|
range: lsp::Range {
|
|
start: lsp::Position {
|
|
line: 0,
|
|
character: 1,
|
|
},
|
|
end: lsp::Position {
|
|
line: 0,
|
|
character: 3,
|
|
},
|
|
},
|
|
severity: Some(lsp::DiagnosticSeverity::WARNING),
|
|
message:
|
|
expected_workspace_pull_diagnostics_lib_message
|
|
.to_string(),
|
|
..lsp::Diagnostic::default()
|
|
}],
|
|
},
|
|
},
|
|
),
|
|
],
|
|
},
|
|
))
|
|
}
|
|
},
|
|
);
|
|
|
|
if should_stream_workspace_diagnostic {
|
|
workspace_diagnostic_received_rx.recv().await.unwrap();
|
|
} else {
|
|
workspace_diagnostics_pulls_handle.next().await.unwrap();
|
|
}
|
|
assert_eq!(
|
|
1,
|
|
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
|
|
"Workspace diagnostics should be pulled initially on a server startup"
|
|
);
|
|
pull_diagnostics_handle.next().await.unwrap();
|
|
assert_eq!(
|
|
1,
|
|
diagnostics_pulls_made.load(atomic::Ordering::Acquire),
|
|
"Host should query pull diagnostics when the editor is opened"
|
|
);
|
|
executor.run_until_parked();
|
|
editor_a_main.update(cx_a, |editor, cx| {
|
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
let all_diagnostics = snapshot
|
|
.diagnostics_in_range(0..snapshot.len())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
all_diagnostics.len(),
|
|
1,
|
|
"Expected single diagnostic, but got: {all_diagnostics:?}"
|
|
);
|
|
let diagnostic = &all_diagnostics[0];
|
|
let mut expected_messages = vec![expected_pull_diagnostic_main_message];
|
|
if !should_stream_workspace_diagnostic {
|
|
expected_messages.push(expected_workspace_pull_diagnostics_main_message);
|
|
}
|
|
assert!(
|
|
expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
|
|
"Expected {expected_messages:?} on the host, but got: {}",
|
|
diagnostic.diagnostic.message
|
|
);
|
|
});
|
|
|
|
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
|
&lsp::PublishDiagnosticsParams {
|
|
uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
diagnostics: vec![lsp::Diagnostic {
|
|
range: lsp::Range {
|
|
start: lsp::Position {
|
|
line: 0,
|
|
character: 3,
|
|
},
|
|
end: lsp::Position {
|
|
line: 0,
|
|
character: 4,
|
|
},
|
|
},
|
|
severity: Some(lsp::DiagnosticSeverity::INFORMATION),
|
|
message: expected_push_diagnostic_main_message.to_string(),
|
|
..lsp::Diagnostic::default()
|
|
}],
|
|
version: None,
|
|
},
|
|
);
|
|
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
|
&lsp::PublishDiagnosticsParams {
|
|
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
|
|
diagnostics: vec![lsp::Diagnostic {
|
|
range: lsp::Range {
|
|
start: lsp::Position {
|
|
line: 0,
|
|
character: 3,
|
|
},
|
|
end: lsp::Position {
|
|
line: 0,
|
|
character: 4,
|
|
},
|
|
},
|
|
severity: Some(lsp::DiagnosticSeverity::INFORMATION),
|
|
message: expected_push_diagnostic_lib_message.to_string(),
|
|
..lsp::Diagnostic::default()
|
|
}],
|
|
version: None,
|
|
},
|
|
);
|
|
|
|
if should_stream_workspace_diagnostic {
|
|
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
|
|
token: expected_workspace_diagnostic_token.clone(),
|
|
value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
|
|
lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
|
|
items: vec![
|
|
lsp::WorkspaceDocumentDiagnosticReport::Full(
|
|
lsp::WorkspaceFullDocumentDiagnosticReport {
|
|
uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
version: None,
|
|
full_document_diagnostic_report:
|
|
lsp::FullDocumentDiagnosticReport {
|
|
result_id: Some(format!(
|
|
"workspace_{}",
|
|
workspace_diagnostics_pulls_made
|
|
.fetch_add(1, atomic::Ordering::Release)
|
|
+ 1
|
|
)),
|
|
items: vec![lsp::Diagnostic {
|
|
range: lsp::Range {
|
|
start: lsp::Position {
|
|
line: 0,
|
|
character: 1,
|
|
},
|
|
end: lsp::Position {
|
|
line: 0,
|
|
character: 2,
|
|
},
|
|
},
|
|
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
|
message:
|
|
expected_workspace_pull_diagnostics_main_message
|
|
.to_string(),
|
|
..lsp::Diagnostic::default()
|
|
}],
|
|
},
|
|
},
|
|
),
|
|
lsp::WorkspaceDocumentDiagnosticReport::Full(
|
|
lsp::WorkspaceFullDocumentDiagnosticReport {
|
|
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
|
|
version: None,
|
|
full_document_diagnostic_report:
|
|
lsp::FullDocumentDiagnosticReport {
|
|
result_id: Some(format!(
|
|
"workspace_{}",
|
|
workspace_diagnostics_pulls_made
|
|
.fetch_add(1, atomic::Ordering::Release)
|
|
+ 1
|
|
)),
|
|
items: Vec::new(),
|
|
},
|
|
},
|
|
),
|
|
],
|
|
}),
|
|
),
|
|
});
|
|
};
|
|
|
|
let mut workspace_diagnostic_start_count =
|
|
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
|
|
|
|
executor.run_until_parked();
|
|
editor_a_main.update(cx_a, |editor, cx| {
|
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
let all_diagnostics = snapshot
|
|
.diagnostics_in_range(0..snapshot.len())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
all_diagnostics.len(),
|
|
2,
|
|
"Expected pull and push diagnostics, but got: {all_diagnostics:?}"
|
|
);
|
|
let expected_messages = [
|
|
expected_workspace_pull_diagnostics_main_message,
|
|
expected_pull_diagnostic_main_message,
|
|
expected_push_diagnostic_main_message,
|
|
];
|
|
for diagnostic in all_diagnostics {
|
|
assert!(
|
|
expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
|
|
"Expected push and pull messages on the host: {expected_messages:?}, but got: {}",
|
|
diagnostic.diagnostic.message
|
|
);
|
|
}
|
|
});
|
|
|
|
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
|
let editor_b_main = workspace_b
|
|
.update_in(cx_b, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
pull_diagnostics_handle.next().await.unwrap();
|
|
assert_eq!(
|
|
2,
|
|
diagnostics_pulls_made.load(atomic::Ordering::Acquire),
|
|
"Client should query pull diagnostics when its editor is opened"
|
|
);
|
|
executor.run_until_parked();
|
|
assert_eq!(
|
|
workspace_diagnostic_start_count,
|
|
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
|
|
"Workspace diagnostics should not be changed as the remote client does not initialize the workspace diagnostics pull"
|
|
);
|
|
editor_b_main.update(cx_b, |editor, cx| {
|
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
let all_diagnostics = snapshot
|
|
.diagnostics_in_range(0..snapshot.len())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
all_diagnostics.len(),
|
|
2,
|
|
"Expected pull and push diagnostics, but got: {all_diagnostics:?}"
|
|
);
|
|
|
|
// Despite the workspace diagnostics not re-initialized for the remote client, we can still expect its message synced from the host.
|
|
let expected_messages = [
|
|
expected_workspace_pull_diagnostics_main_message,
|
|
expected_pull_diagnostic_main_message,
|
|
expected_push_diagnostic_main_message,
|
|
];
|
|
for diagnostic in all_diagnostics {
|
|
assert!(
|
|
expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
|
|
"The client should get both push and pull messages: {expected_messages:?}, but got: {}",
|
|
diagnostic.diagnostic.message
|
|
);
|
|
}
|
|
});
|
|
|
|
let editor_b_lib = workspace_b
|
|
.update_in(cx_b, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "lib.rs"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
pull_diagnostics_handle.next().await.unwrap();
|
|
assert_eq!(
|
|
3,
|
|
diagnostics_pulls_made.load(atomic::Ordering::Acquire),
|
|
"Client should query pull diagnostics when its another editor is opened"
|
|
);
|
|
executor.run_until_parked();
|
|
assert_eq!(
|
|
workspace_diagnostic_start_count,
|
|
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
|
|
"The remote client still did not anything to trigger the workspace diagnostics pull"
|
|
);
|
|
editor_b_lib.update(cx_b, |editor, cx| {
|
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
let all_diagnostics = snapshot
|
|
.diagnostics_in_range(0..snapshot.len())
|
|
.collect::<Vec<_>>();
|
|
let expected_messages = [
|
|
expected_pull_diagnostic_lib_message,
|
|
// TODO bug: the pushed diagnostics are not being sent to the client when they open the corresponding buffer.
|
|
// expected_push_diagnostic_lib_message,
|
|
];
|
|
assert_eq!(
|
|
all_diagnostics.len(),
|
|
1,
|
|
"Expected pull diagnostics, but got: {all_diagnostics:?}"
|
|
);
|
|
for diagnostic in all_diagnostics {
|
|
assert!(
|
|
expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
|
|
"The client should get both push and pull messages: {expected_messages:?}, but got: {}",
|
|
diagnostic.diagnostic.message
|
|
);
|
|
}
|
|
});
|
|
|
|
if should_stream_workspace_diagnostic {
|
|
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
|
|
token: expected_workspace_diagnostic_token.clone(),
|
|
value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
|
|
lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
|
|
items: vec![lsp::WorkspaceDocumentDiagnosticReport::Full(
|
|
lsp::WorkspaceFullDocumentDiagnosticReport {
|
|
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
|
|
version: None,
|
|
full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
|
|
result_id: Some(format!(
|
|
"workspace_{}",
|
|
workspace_diagnostics_pulls_made
|
|
.fetch_add(1, atomic::Ordering::Release)
|
|
+ 1
|
|
)),
|
|
items: vec![lsp::Diagnostic {
|
|
range: lsp::Range {
|
|
start: lsp::Position {
|
|
line: 0,
|
|
character: 1,
|
|
},
|
|
end: lsp::Position {
|
|
line: 0,
|
|
character: 2,
|
|
},
|
|
},
|
|
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
|
message: expected_workspace_pull_diagnostics_lib_message
|
|
.to_string(),
|
|
..lsp::Diagnostic::default()
|
|
}],
|
|
},
|
|
},
|
|
)],
|
|
}),
|
|
),
|
|
});
|
|
workspace_diagnostic_start_count =
|
|
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
|
|
workspace_diagnostic_cancel_tx.send(()).await.unwrap();
|
|
workspace_diagnostics_pulls_handle.next().await.unwrap();
|
|
executor.run_until_parked();
|
|
editor_b_lib.update(cx_b, |editor, cx| {
|
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
let all_diagnostics = snapshot
|
|
.diagnostics_in_range(0..snapshot.len())
|
|
.collect::<Vec<_>>();
|
|
let expected_messages = [
|
|
expected_workspace_pull_diagnostics_lib_message,
|
|
// TODO bug: the pushed diagnostics are not being sent to the client when they open the corresponding buffer.
|
|
// expected_push_diagnostic_lib_message,
|
|
];
|
|
assert_eq!(
|
|
all_diagnostics.len(),
|
|
1,
|
|
"Expected pull diagnostics, but got: {all_diagnostics:?}"
|
|
);
|
|
for diagnostic in all_diagnostics {
|
|
assert!(
|
|
expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
|
|
"The client should get both push and pull messages: {expected_messages:?}, but got: {}",
|
|
diagnostic.diagnostic.message
|
|
);
|
|
}
|
|
});
|
|
};
|
|
|
|
{
|
|
assert!(
|
|
diagnostics_pulls_result_ids.lock().await.len() > 0,
|
|
"Initial diagnostics pulls should report None at least"
|
|
);
|
|
assert_eq!(
|
|
0,
|
|
workspace_diagnostics_pulls_result_ids
|
|
.lock()
|
|
.await
|
|
.deref()
|
|
.len(),
|
|
"After the initial workspace request, opening files should not reuse any result ids"
|
|
);
|
|
}
|
|
|
|
editor_b_lib.update_in(cx_b, |editor, window, cx| {
|
|
editor.move_to_end(&MoveToEnd, window, cx);
|
|
editor.handle_input(":", window, cx);
|
|
});
|
|
pull_diagnostics_handle.next().await.unwrap();
|
|
assert_eq!(
|
|
4,
|
|
diagnostics_pulls_made.load(atomic::Ordering::Acquire),
|
|
"Client lib.rs edits should trigger another diagnostics pull for a buffer"
|
|
);
|
|
workspace_diagnostics_pulls_handle.next().await.unwrap();
|
|
assert_eq!(
|
|
workspace_diagnostic_start_count + 1,
|
|
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
|
|
"After client lib.rs edits, the workspace diagnostics request should follow"
|
|
);
|
|
executor.run_until_parked();
|
|
|
|
editor_b_main.update_in(cx_b, |editor, window, cx| {
|
|
editor.move_to_end(&MoveToEnd, window, cx);
|
|
editor.handle_input(":", window, cx);
|
|
});
|
|
pull_diagnostics_handle.next().await.unwrap();
|
|
pull_diagnostics_handle.next().await.unwrap();
|
|
assert_eq!(
|
|
6,
|
|
diagnostics_pulls_made.load(atomic::Ordering::Acquire),
|
|
"Client main.rs edits should trigger another diagnostics pull by both client and host as they share the buffer"
|
|
);
|
|
workspace_diagnostics_pulls_handle.next().await.unwrap();
|
|
assert_eq!(
|
|
workspace_diagnostic_start_count + 2,
|
|
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
|
|
"After client main.rs edits, the workspace diagnostics pull should follow"
|
|
);
|
|
executor.run_until_parked();
|
|
|
|
editor_a_main.update_in(cx_a, |editor, window, cx| {
|
|
editor.move_to_end(&MoveToEnd, window, cx);
|
|
editor.handle_input(":", window, cx);
|
|
});
|
|
pull_diagnostics_handle.next().await.unwrap();
|
|
pull_diagnostics_handle.next().await.unwrap();
|
|
assert_eq!(
|
|
8,
|
|
diagnostics_pulls_made.load(atomic::Ordering::Acquire),
|
|
"Host main.rs edits should trigger another diagnostics pull by both client and host as they share the buffer"
|
|
);
|
|
workspace_diagnostics_pulls_handle.next().await.unwrap();
|
|
assert_eq!(
|
|
workspace_diagnostic_start_count + 3,
|
|
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
|
|
"After host main.rs edits, the workspace diagnostics pull should follow"
|
|
);
|
|
executor.run_until_parked();
|
|
let diagnostic_pulls_result_ids = diagnostics_pulls_result_ids.lock().await.len();
|
|
let workspace_pulls_result_ids = workspace_diagnostics_pulls_result_ids.lock().await.len();
|
|
{
|
|
assert!(
|
|
diagnostic_pulls_result_ids > 1,
|
|
"Should have sent result ids when pulling diagnostics"
|
|
);
|
|
assert!(
|
|
workspace_pulls_result_ids > 1,
|
|
"Should have sent result ids when pulling workspace diagnostics"
|
|
);
|
|
}
|
|
|
|
fake_language_server
|
|
.request::<lsp::request::WorkspaceDiagnosticRefresh>(())
|
|
.await
|
|
.into_response()
|
|
.expect("workspace diagnostics refresh request failed");
|
|
assert_eq!(
|
|
8,
|
|
diagnostics_pulls_made.load(atomic::Ordering::Acquire),
|
|
"No single file pulls should happen after the diagnostics refresh server request"
|
|
);
|
|
workspace_diagnostics_pulls_handle.next().await.unwrap();
|
|
assert_eq!(
|
|
workspace_diagnostic_start_count + 4,
|
|
workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
|
|
"Another workspace diagnostics pull should happen after the diagnostics refresh server request"
|
|
);
|
|
{
|
|
assert!(
|
|
diagnostics_pulls_result_ids.lock().await.len() == diagnostic_pulls_result_ids,
|
|
"Pulls should not happen hence no extra ids should appear"
|
|
);
|
|
assert!(
|
|
workspace_diagnostics_pulls_result_ids.lock().await.len() > workspace_pulls_result_ids,
|
|
"More workspace diagnostics should be pulled"
|
|
);
|
|
}
|
|
editor_b_lib.update(cx_b, |editor, cx| {
|
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
let all_diagnostics = snapshot
|
|
.diagnostics_in_range(0..snapshot.len())
|
|
.collect::<Vec<_>>();
|
|
let expected_messages = [
|
|
expected_workspace_pull_diagnostics_lib_message,
|
|
expected_pull_diagnostic_lib_message,
|
|
expected_push_diagnostic_lib_message,
|
|
];
|
|
assert_eq!(all_diagnostics.len(), 1);
|
|
for diagnostic in &all_diagnostics {
|
|
assert!(
|
|
expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
|
|
"Unexpected diagnostics: {all_diagnostics:?}"
|
|
);
|
|
}
|
|
});
|
|
editor_b_main.update(cx_b, |editor, cx| {
|
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
let all_diagnostics = snapshot
|
|
.diagnostics_in_range(0..snapshot.len())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(all_diagnostics.len(), 2);
|
|
|
|
let expected_messages = [
|
|
expected_workspace_pull_diagnostics_main_message,
|
|
expected_pull_diagnostic_main_message,
|
|
expected_push_diagnostic_main_message,
|
|
];
|
|
for diagnostic in &all_diagnostics {
|
|
assert!(
|
|
expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
|
|
"Unexpected diagnostics: {all_diagnostics:?}"
|
|
);
|
|
}
|
|
});
|
|
editor_a_main.update(cx_a, |editor, cx| {
|
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
let all_diagnostics = snapshot
|
|
.diagnostics_in_range(0..snapshot.len())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(all_diagnostics.len(), 2);
|
|
let expected_messages = [
|
|
expected_workspace_pull_diagnostics_main_message,
|
|
expected_pull_diagnostic_main_message,
|
|
expected_push_diagnostic_main_message,
|
|
];
|
|
for diagnostic in &all_diagnostics {
|
|
assert!(
|
|
expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
|
|
"Unexpected diagnostics: {all_diagnostics:?}"
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
async fn test_non_streamed_lsp_pull_diagnostics(
|
|
cx_a: &mut TestAppContext,
|
|
cx_b: &mut TestAppContext,
|
|
) {
|
|
test_lsp_pull_diagnostics(false, cx_a, cx_b).await;
|
|
}
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
async fn test_streamed_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
|
test_lsp_pull_diagnostics(true, cx_a, cx_b).await;
|
|
}
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
|
|
cx_a.update(editor::init);
|
|
cx_b.update(editor::init);
|
|
// Turn inline-blame-off by default so no state is transferred without us explicitly doing so
|
|
let inline_blame_off_settings = Some(InlineBlameSettings {
|
|
enabled: false,
|
|
delay_ms: None,
|
|
min_column: None,
|
|
show_commit_summary: false,
|
|
});
|
|
cx_a.update(|cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings::<ProjectSettings>(cx, |settings| {
|
|
settings.git.inline_blame = inline_blame_off_settings;
|
|
});
|
|
});
|
|
});
|
|
cx_b.update(|cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings::<ProjectSettings>(cx, |settings| {
|
|
settings.git.inline_blame = inline_blame_off_settings;
|
|
});
|
|
});
|
|
});
|
|
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
path!("/my-repo"),
|
|
json!({
|
|
".git": {},
|
|
"file.txt": "line1\nline2\nline3\nline\n",
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
let blame = git::blame::Blame {
|
|
entries: vec![
|
|
blame_entry("1b1b1b", 0..1),
|
|
blame_entry("0d0d0d", 1..2),
|
|
blame_entry("3a3a3a", 2..3),
|
|
blame_entry("4c4c4c", 3..4),
|
|
],
|
|
messages: [
|
|
("1b1b1b", "message for idx-0"),
|
|
("0d0d0d", "message for idx-1"),
|
|
("3a3a3a", "message for idx-2"),
|
|
("4c4c4c", "message for idx-3"),
|
|
]
|
|
.into_iter()
|
|
.map(|(sha, message)| (sha.parse().unwrap(), message.into()))
|
|
.collect(),
|
|
remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
|
|
};
|
|
client_a.fs().set_blame_for_repo(
|
|
Path::new(path!("/my-repo/.git")),
|
|
vec![("file.txt".into(), blame)],
|
|
);
|
|
|
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/my-repo"), cx_a).await;
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Create editor_a
|
|
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
|
let editor_a = workspace_a
|
|
.update_in(cx_a, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
// Join the project as client B.
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
|
let editor_b = workspace_b
|
|
.update_in(cx_b, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
let buffer_id_b = editor_b.update(cx_b, |editor_b, cx| {
|
|
editor_b
|
|
.buffer()
|
|
.read(cx)
|
|
.as_singleton()
|
|
.unwrap()
|
|
.read(cx)
|
|
.remote_id()
|
|
});
|
|
|
|
// client_b now requests git blame for the open buffer
|
|
editor_b.update_in(cx_b, |editor_b, window, cx| {
|
|
assert!(editor_b.blame().is_none());
|
|
editor_b.toggle_git_blame(&git::Blame {}, window, cx);
|
|
});
|
|
|
|
cx_a.executor().run_until_parked();
|
|
cx_b.executor().run_until_parked();
|
|
|
|
editor_b.update(cx_b, |editor_b, cx| {
|
|
let blame = editor_b.blame().expect("editor_b should have blame now");
|
|
let entries = blame.update(cx, |blame, cx| {
|
|
blame
|
|
.blame_for_rows(
|
|
&(0..4)
|
|
.map(|row| RowInfo {
|
|
buffer_row: Some(row),
|
|
buffer_id: Some(buffer_id_b),
|
|
..Default::default()
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
cx,
|
|
)
|
|
.collect::<Vec<_>>()
|
|
});
|
|
|
|
assert_eq!(
|
|
entries,
|
|
vec![
|
|
Some(blame_entry("1b1b1b", 0..1)),
|
|
Some(blame_entry("0d0d0d", 1..2)),
|
|
Some(blame_entry("3a3a3a", 2..3)),
|
|
Some(blame_entry("4c4c4c", 3..4)),
|
|
]
|
|
);
|
|
|
|
blame.update(cx, |blame, _| {
|
|
for (idx, entry) in entries.iter().flatten().enumerate() {
|
|
let details = blame.details_for_entry(entry).unwrap();
|
|
assert_eq!(details.message, format!("message for idx-{}", idx));
|
|
assert_eq!(
|
|
details.permalink.unwrap().to_string(),
|
|
format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
// editor_b updates the file, which gets sent to client_a, which updates git blame,
|
|
// which gets back to client_b.
|
|
editor_b.update_in(cx_b, |editor_b, _, cx| {
|
|
editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
|
|
});
|
|
|
|
cx_a.executor().run_until_parked();
|
|
cx_b.executor().run_until_parked();
|
|
|
|
editor_b.update(cx_b, |editor_b, cx| {
|
|
let blame = editor_b.blame().expect("editor_b should have blame now");
|
|
let entries = blame.update(cx, |blame, cx| {
|
|
blame
|
|
.blame_for_rows(
|
|
&(0..4)
|
|
.map(|row| RowInfo {
|
|
buffer_row: Some(row),
|
|
buffer_id: Some(buffer_id_b),
|
|
..Default::default()
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
cx,
|
|
)
|
|
.collect::<Vec<_>>()
|
|
});
|
|
|
|
assert_eq!(
|
|
entries,
|
|
vec![
|
|
None,
|
|
Some(blame_entry("0d0d0d", 1..2)),
|
|
Some(blame_entry("3a3a3a", 2..3)),
|
|
Some(blame_entry("4c4c4c", 3..4)),
|
|
]
|
|
);
|
|
});
|
|
|
|
// Now editor_a also updates the file
|
|
editor_a.update_in(cx_a, |editor_a, _, cx| {
|
|
editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
|
|
});
|
|
|
|
cx_a.executor().run_until_parked();
|
|
cx_b.executor().run_until_parked();
|
|
|
|
editor_b.update(cx_b, |editor_b, cx| {
|
|
let blame = editor_b.blame().expect("editor_b should have blame now");
|
|
let entries = blame.update(cx, |blame, cx| {
|
|
blame
|
|
.blame_for_rows(
|
|
&(0..4)
|
|
.map(|row| RowInfo {
|
|
buffer_row: Some(row),
|
|
buffer_id: Some(buffer_id_b),
|
|
..Default::default()
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
cx,
|
|
)
|
|
.collect::<Vec<_>>()
|
|
});
|
|
|
|
assert_eq!(
|
|
entries,
|
|
vec![
|
|
None,
|
|
None,
|
|
Some(blame_entry("3a3a3a", 2..3)),
|
|
Some(blame_entry("4c4c4c", 3..4)),
|
|
]
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test(iterations = 30)]
|
|
async fn test_collaborating_with_editorconfig(
|
|
cx_a: &mut TestAppContext,
|
|
cx_b: &mut TestAppContext,
|
|
) {
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
|
|
cx_b.update(editor::init);
|
|
|
|
// Set up a fake language server.
|
|
client_a.language_registry().add(rust_lang());
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
path!("/a"),
|
|
json!({
|
|
"src": {
|
|
"main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
|
|
"other_mod": {
|
|
"other.rs": "pub fn foo() -> usize {\n 4\n}",
|
|
".editorconfig": "",
|
|
},
|
|
},
|
|
".editorconfig": "[*]\ntab_width = 2\n",
|
|
}),
|
|
)
|
|
.await;
|
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
let main_buffer_a = project_a
|
|
.update(cx_a, |p, cx| {
|
|
p.open_buffer((worktree_id, "src/main.rs"), cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let other_buffer_a = project_a
|
|
.update(cx_a, |p, cx| {
|
|
p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let cx_a = cx_a.add_empty_window();
|
|
let main_editor_a = cx_a.new_window_entity(|window, cx| {
|
|
Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
|
|
});
|
|
let other_editor_a = cx_a.new_window_entity(|window, cx| {
|
|
Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
|
|
});
|
|
let mut main_editor_cx_a = EditorTestContext {
|
|
cx: cx_a.clone(),
|
|
window: cx_a.window_handle(),
|
|
editor: main_editor_a,
|
|
assertion_cx: AssertionContextManager::new(),
|
|
};
|
|
let mut other_editor_cx_a = EditorTestContext {
|
|
cx: cx_a.clone(),
|
|
window: cx_a.window_handle(),
|
|
editor: other_editor_a,
|
|
assertion_cx: AssertionContextManager::new(),
|
|
};
|
|
|
|
// Join the project as client B.
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
let main_buffer_b = project_b
|
|
.update(cx_b, |p, cx| {
|
|
p.open_buffer((worktree_id, "src/main.rs"), cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let other_buffer_b = project_b
|
|
.update(cx_b, |p, cx| {
|
|
p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let cx_b = cx_b.add_empty_window();
|
|
let main_editor_b = cx_b.new_window_entity(|window, cx| {
|
|
Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
|
|
});
|
|
let other_editor_b = cx_b.new_window_entity(|window, cx| {
|
|
Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
|
|
});
|
|
let mut main_editor_cx_b = EditorTestContext {
|
|
cx: cx_b.clone(),
|
|
window: cx_b.window_handle(),
|
|
editor: main_editor_b,
|
|
assertion_cx: AssertionContextManager::new(),
|
|
};
|
|
let mut other_editor_cx_b = EditorTestContext {
|
|
cx: cx_b.clone(),
|
|
window: cx_b.window_handle(),
|
|
editor: other_editor_b,
|
|
assertion_cx: AssertionContextManager::new(),
|
|
};
|
|
|
|
let initial_main = indoc! {"
|
|
ˇmod other;
|
|
fn main() { let foo = other::foo(); }"};
|
|
let initial_other = indoc! {"
|
|
ˇpub fn foo() -> usize {
|
|
4
|
|
}"};
|
|
|
|
let first_tabbed_main = indoc! {"
|
|
ˇmod other;
|
|
fn main() { let foo = other::foo(); }"};
|
|
tab_undo_assert(
|
|
&mut main_editor_cx_a,
|
|
&mut main_editor_cx_b,
|
|
initial_main,
|
|
first_tabbed_main,
|
|
true,
|
|
);
|
|
tab_undo_assert(
|
|
&mut main_editor_cx_a,
|
|
&mut main_editor_cx_b,
|
|
initial_main,
|
|
first_tabbed_main,
|
|
false,
|
|
);
|
|
|
|
let first_tabbed_other = indoc! {"
|
|
ˇpub fn foo() -> usize {
|
|
4
|
|
}"};
|
|
tab_undo_assert(
|
|
&mut other_editor_cx_a,
|
|
&mut other_editor_cx_b,
|
|
initial_other,
|
|
first_tabbed_other,
|
|
true,
|
|
);
|
|
tab_undo_assert(
|
|
&mut other_editor_cx_a,
|
|
&mut other_editor_cx_b,
|
|
initial_other,
|
|
first_tabbed_other,
|
|
false,
|
|
);
|
|
|
|
client_a
|
|
.fs()
|
|
.atomic_write(
|
|
PathBuf::from(path!("/a/src/.editorconfig")),
|
|
"[*]\ntab_width = 3\n".to_owned(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
cx_a.run_until_parked();
|
|
cx_b.run_until_parked();
|
|
|
|
let second_tabbed_main = indoc! {"
|
|
ˇmod other;
|
|
fn main() { let foo = other::foo(); }"};
|
|
tab_undo_assert(
|
|
&mut main_editor_cx_a,
|
|
&mut main_editor_cx_b,
|
|
initial_main,
|
|
second_tabbed_main,
|
|
true,
|
|
);
|
|
tab_undo_assert(
|
|
&mut main_editor_cx_a,
|
|
&mut main_editor_cx_b,
|
|
initial_main,
|
|
second_tabbed_main,
|
|
false,
|
|
);
|
|
|
|
let second_tabbed_other = indoc! {"
|
|
ˇpub fn foo() -> usize {
|
|
4
|
|
}"};
|
|
tab_undo_assert(
|
|
&mut other_editor_cx_a,
|
|
&mut other_editor_cx_b,
|
|
initial_other,
|
|
second_tabbed_other,
|
|
true,
|
|
);
|
|
tab_undo_assert(
|
|
&mut other_editor_cx_a,
|
|
&mut other_editor_cx_b,
|
|
initial_other,
|
|
second_tabbed_other,
|
|
false,
|
|
);
|
|
|
|
let editorconfig_buffer_b = project_b
|
|
.update(cx_b, |p, cx| {
|
|
p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
editorconfig_buffer_b.update(cx_b, |buffer, cx| {
|
|
buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
|
|
});
|
|
project_b
|
|
.update(cx_b, |project, cx| {
|
|
project.save_buffer(editorconfig_buffer_b.clone(), cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
cx_a.run_until_parked();
|
|
cx_b.run_until_parked();
|
|
|
|
tab_undo_assert(
|
|
&mut main_editor_cx_a,
|
|
&mut main_editor_cx_b,
|
|
initial_main,
|
|
second_tabbed_main,
|
|
true,
|
|
);
|
|
tab_undo_assert(
|
|
&mut main_editor_cx_a,
|
|
&mut main_editor_cx_b,
|
|
initial_main,
|
|
second_tabbed_main,
|
|
false,
|
|
);
|
|
|
|
let third_tabbed_other = indoc! {"
|
|
ˇpub fn foo() -> usize {
|
|
4
|
|
}"};
|
|
tab_undo_assert(
|
|
&mut other_editor_cx_a,
|
|
&mut other_editor_cx_b,
|
|
initial_other,
|
|
third_tabbed_other,
|
|
true,
|
|
);
|
|
|
|
tab_undo_assert(
|
|
&mut other_editor_cx_a,
|
|
&mut other_editor_cx_b,
|
|
initial_other,
|
|
third_tabbed_other,
|
|
false,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
|
let executor = cx_a.executor();
|
|
let mut server = TestServer::start(executor.clone()).await;
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
let active_call_b = cx_b.read(ActiveCall::global);
|
|
cx_a.update(editor::init);
|
|
cx_b.update(editor::init);
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
"/a",
|
|
json!({
|
|
"test.txt": "one\ntwo\nthree\nfour\nfive",
|
|
}),
|
|
)
|
|
.await;
|
|
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
|
let project_path = ProjectPath {
|
|
worktree_id,
|
|
path: Arc::from(Path::new(&"test.txt")),
|
|
};
|
|
let abs_path = project_a.read_with(cx_a, |project, cx| {
|
|
project
|
|
.absolute_path(&project_path, cx)
|
|
.map(|path_buf| Arc::from(path_buf.to_owned()))
|
|
.unwrap()
|
|
});
|
|
|
|
active_call_a
|
|
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
|
.await
|
|
.unwrap();
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
active_call_b
|
|
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
|
.await
|
|
.unwrap();
|
|
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
|
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
|
|
|
// Client A opens an editor.
|
|
let editor_a = workspace_a
|
|
.update_in(cx_a, |workspace, window, cx| {
|
|
workspace.open_path(project_path.clone(), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
// Client B opens same editor as A.
|
|
let editor_b = workspace_b
|
|
.update_in(cx_b, |workspace, window, cx| {
|
|
workspace.open_path(project_path.clone(), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
cx_a.run_until_parked();
|
|
cx_b.run_until_parked();
|
|
|
|
// Client A adds breakpoint on line (1)
|
|
editor_a.update_in(cx_a, |editor, window, cx| {
|
|
editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
|
|
});
|
|
|
|
cx_a.run_until_parked();
|
|
cx_b.run_until_parked();
|
|
|
|
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
|
|
editor
|
|
.breakpoint_store()
|
|
.clone()
|
|
.unwrap()
|
|
.read(cx)
|
|
.all_source_breakpoints(cx)
|
|
.clone()
|
|
});
|
|
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
|
|
editor
|
|
.breakpoint_store()
|
|
.clone()
|
|
.unwrap()
|
|
.read(cx)
|
|
.all_source_breakpoints(cx)
|
|
.clone()
|
|
});
|
|
|
|
assert_eq!(1, breakpoints_a.len());
|
|
assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
|
|
assert_eq!(breakpoints_a, breakpoints_b);
|
|
|
|
// Client B adds breakpoint on line(2)
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
editor.move_down(&editor::actions::MoveDown, window, cx);
|
|
editor.move_down(&editor::actions::MoveDown, window, cx);
|
|
editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
|
|
});
|
|
|
|
cx_a.run_until_parked();
|
|
cx_b.run_until_parked();
|
|
|
|
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
|
|
editor
|
|
.breakpoint_store()
|
|
.clone()
|
|
.unwrap()
|
|
.read(cx)
|
|
.all_source_breakpoints(cx)
|
|
.clone()
|
|
});
|
|
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
|
|
editor
|
|
.breakpoint_store()
|
|
.clone()
|
|
.unwrap()
|
|
.read(cx)
|
|
.all_source_breakpoints(cx)
|
|
.clone()
|
|
});
|
|
|
|
assert_eq!(1, breakpoints_a.len());
|
|
assert_eq!(breakpoints_a, breakpoints_b);
|
|
assert_eq!(2, breakpoints_a.get(&abs_path).unwrap().len());
|
|
|
|
// Client A removes last added breakpoint from client B
|
|
editor_a.update_in(cx_a, |editor, window, cx| {
|
|
editor.move_down(&editor::actions::MoveDown, window, cx);
|
|
editor.move_down(&editor::actions::MoveDown, window, cx);
|
|
editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
|
|
});
|
|
|
|
cx_a.run_until_parked();
|
|
cx_b.run_until_parked();
|
|
|
|
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
|
|
editor
|
|
.breakpoint_store()
|
|
.clone()
|
|
.unwrap()
|
|
.read(cx)
|
|
.all_source_breakpoints(cx)
|
|
.clone()
|
|
});
|
|
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
|
|
editor
|
|
.breakpoint_store()
|
|
.clone()
|
|
.unwrap()
|
|
.read(cx)
|
|
.all_source_breakpoints(cx)
|
|
.clone()
|
|
});
|
|
|
|
assert_eq!(1, breakpoints_a.len());
|
|
assert_eq!(breakpoints_a, breakpoints_b);
|
|
assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
|
|
|
|
// Client B removes first added breakpoint by client A
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
editor.move_up(&editor::actions::MoveUp, window, cx);
|
|
editor.move_up(&editor::actions::MoveUp, window, cx);
|
|
editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
|
|
});
|
|
|
|
cx_a.run_until_parked();
|
|
cx_b.run_until_parked();
|
|
|
|
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
|
|
editor
|
|
.breakpoint_store()
|
|
.clone()
|
|
.unwrap()
|
|
.read(cx)
|
|
.all_source_breakpoints(cx)
|
|
.clone()
|
|
});
|
|
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
|
|
editor
|
|
.breakpoint_store()
|
|
.clone()
|
|
.unwrap()
|
|
.read(cx)
|
|
.all_source_breakpoints(cx)
|
|
.clone()
|
|
});
|
|
|
|
assert_eq!(0, breakpoints_a.len());
|
|
assert_eq!(breakpoints_a, breakpoints_b);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
|
let mut server = TestServer::start(cx_a.executor()).await;
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
let client_b = server.create_client(cx_b, "user_b").await;
|
|
server
|
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
|
.await;
|
|
let active_call_a = cx_a.read(ActiveCall::global);
|
|
let active_call_b = cx_b.read(ActiveCall::global);
|
|
|
|
cx_a.update(editor::init);
|
|
cx_b.update(editor::init);
|
|
|
|
client_a.language_registry().add(rust_lang());
|
|
client_b.language_registry().add(rust_lang());
|
|
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
|
"Rust",
|
|
FakeLspAdapter {
|
|
name: RUST_ANALYZER_NAME,
|
|
..FakeLspAdapter::default()
|
|
},
|
|
);
|
|
|
|
client_a
|
|
.fs()
|
|
.insert_tree(
|
|
path!("/a"),
|
|
json!({
|
|
"main.rs": "fn main() {}",
|
|
}),
|
|
)
|
|
.await;
|
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
|
|
active_call_a
|
|
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
|
.await
|
|
.unwrap();
|
|
let project_id = active_call_a
|
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
|
active_call_b
|
|
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
|
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
|
|
|
let editor_a = workspace_a
|
|
.update_in(cx_a, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
let editor_b = workspace_b
|
|
.update_in(cx_b, |workspace, window, cx| {
|
|
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.downcast::<Editor>()
|
|
.unwrap();
|
|
|
|
let fake_language_server = fake_language_servers.next().await.unwrap();
|
|
|
|
// host
|
|
let mut expand_request_a = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
|
|
|params, _| async move {
|
|
assert_eq!(
|
|
params.text_document.uri,
|
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
);
|
|
assert_eq!(params.position, lsp::Position::new(0, 0));
|
|
Ok(Some(ExpandedMacro {
|
|
name: "test_macro_name".to_string(),
|
|
expansion: "test_macro_expansion on the host".to_string(),
|
|
}))
|
|
},
|
|
);
|
|
|
|
editor_a.update_in(cx_a, |editor, window, cx| {
|
|
expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
|
|
});
|
|
expand_request_a.next().await.unwrap();
|
|
cx_a.run_until_parked();
|
|
|
|
workspace_a.update(cx_a, |workspace, cx| {
|
|
workspace.active_pane().update(cx, |pane, cx| {
|
|
assert_eq!(
|
|
pane.items_len(),
|
|
2,
|
|
"Should have added a macro expansion to the host's pane"
|
|
);
|
|
let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
|
|
new_editor.update(cx, |editor, cx| {
|
|
assert_eq!(editor.text(cx), "test_macro_expansion on the host");
|
|
});
|
|
})
|
|
});
|
|
|
|
// client
|
|
let mut expand_request_b = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
|
|
|params, _| async move {
|
|
assert_eq!(
|
|
params.text_document.uri,
|
|
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
|
);
|
|
assert_eq!(
|
|
params.position,
|
|
lsp::Position::new(0, 12),
|
|
"editor_b has selected the entire text and should query for a different position"
|
|
);
|
|
Ok(Some(ExpandedMacro {
|
|
name: "test_macro_name".to_string(),
|
|
expansion: "test_macro_expansion on the client".to_string(),
|
|
}))
|
|
},
|
|
);
|
|
|
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
|
editor.select_all(&SelectAll, window, cx);
|
|
expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
|
|
});
|
|
expand_request_b.next().await.unwrap();
|
|
cx_b.run_until_parked();
|
|
|
|
workspace_b.update(cx_b, |workspace, cx| {
|
|
workspace.active_pane().update(cx, |pane, cx| {
|
|
assert_eq!(
|
|
pane.items_len(),
|
|
2,
|
|
"Should have added a macro expansion to the client's pane"
|
|
);
|
|
let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
|
|
new_editor.update(cx, |editor, cx| {
|
|
assert_eq!(editor.text(cx), "test_macro_expansion on the client");
|
|
});
|
|
})
|
|
});
|
|
}
|
|
|
|
#[track_caller]
|
|
fn tab_undo_assert(
|
|
cx_a: &mut EditorTestContext,
|
|
cx_b: &mut EditorTestContext,
|
|
expected_initial: &str,
|
|
expected_tabbed: &str,
|
|
a_tabs: bool,
|
|
) {
|
|
cx_a.assert_editor_state(expected_initial);
|
|
cx_b.assert_editor_state(expected_initial);
|
|
|
|
if a_tabs {
|
|
cx_a.update_editor(|editor, window, cx| {
|
|
editor.tab(&editor::actions::Tab, window, cx);
|
|
});
|
|
} else {
|
|
cx_b.update_editor(|editor, window, cx| {
|
|
editor.tab(&editor::actions::Tab, window, cx);
|
|
});
|
|
}
|
|
|
|
cx_a.run_until_parked();
|
|
cx_b.run_until_parked();
|
|
|
|
cx_a.assert_editor_state(expected_tabbed);
|
|
cx_b.assert_editor_state(expected_tabbed);
|
|
|
|
if a_tabs {
|
|
cx_a.update_editor(|editor, window, cx| {
|
|
editor.undo(&editor::actions::Undo, window, cx);
|
|
});
|
|
} else {
|
|
cx_b.update_editor(|editor, window, cx| {
|
|
editor.undo(&editor::actions::Undo, window, cx);
|
|
});
|
|
}
|
|
cx_a.run_until_parked();
|
|
cx_b.run_until_parked();
|
|
cx_a.assert_editor_state(expected_initial);
|
|
cx_b.assert_editor_state(expected_initial);
|
|
}
|
|
|
|
fn extract_hint_labels(editor: &Editor) -> Vec<String> {
|
|
let mut labels = Vec::new();
|
|
for hint in editor.inlay_hint_cache().hints() {
|
|
match hint.label {
|
|
project::InlayHintLabel::String(s) => labels.push(s),
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
labels
|
|
}
|
|
|
|
#[track_caller]
|
|
fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
|
|
editor
|
|
.all_inlays(cx)
|
|
.into_iter()
|
|
.filter_map(|inlay| inlay.get_color())
|
|
.map(Rgba::from)
|
|
.collect()
|
|
}
|
|
|
|
fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
|
|
git::blame::BlameEntry {
|
|
sha: sha.parse().unwrap(),
|
|
range,
|
|
..Default::default()
|
|
}
|
|
}
|