Compare commits
24 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
949525e77d | ||
![]() |
7ee665bbc0 | ||
![]() |
397c3efbde | ||
![]() |
389ce38e5d | ||
![]() |
5799f0f10f | ||
![]() |
89496c0285 | ||
![]() |
f5407fa19a | ||
![]() |
aba8893cef | ||
![]() |
cdef1c1db7 | ||
![]() |
2a899aa702 | ||
![]() |
d21238f5a0 | ||
![]() |
1722fc9fe2 | ||
![]() |
6ae29d3bb0 | ||
![]() |
3afbe51e30 | ||
![]() |
79df2a4f3c | ||
![]() |
a4c45236e3 | ||
![]() |
6dde8a9b59 | ||
![]() |
ccde289ed7 | ||
![]() |
9a026f3c4b | ||
![]() |
301609d595 | ||
![]() |
ea8dba625d | ||
![]() |
b089be40ba | ||
![]() |
b83451ccf6 | ||
![]() |
50658077a0 |
27 changed files with 1675 additions and 968 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -1354,7 +1354,6 @@ dependencies = [
|
||||||
"smol",
|
"smol",
|
||||||
"theme",
|
"theme",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -8516,7 +8515,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.82.0"
|
version = "0.82.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
|
@ -177,7 +177,7 @@
|
||||||
"focus": false
|
"focus": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"alt-\\": "copilot::NextSuggestion",
|
"alt-\\": "copilot::Suggest",
|
||||||
"alt-]": "copilot::NextSuggestion",
|
"alt-]": "copilot::NextSuggestion",
|
||||||
"alt-[": "copilot::PreviousSuggestion"
|
"alt-[": "copilot::PreviousSuggestion"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
{
|
{
|
||||||
// The name of the Zed theme to use for the UI
|
// The name of the Zed theme to use for the UI
|
||||||
"theme": "One Dark",
|
"theme": "One Dark",
|
||||||
|
// Features that can be globally enabled or disabled
|
||||||
|
"features": {
|
||||||
|
// Show Copilot icon in status bar
|
||||||
|
"copilot": true
|
||||||
|
},
|
||||||
// The name of a font to use for rendering text in the editor
|
// The name of a font to use for rendering text in the editor
|
||||||
"buffer_font_family": "Zed Mono",
|
"buffer_font_family": "Zed Mono",
|
||||||
// The OpenType features to enable for text in the editor.
|
// The OpenType features to enable for text in the editor.
|
||||||
|
@ -13,11 +18,6 @@
|
||||||
// The factor to grow the active pane by. Defaults to 1.0
|
// The factor to grow the active pane by. Defaults to 1.0
|
||||||
// which gives the same size as all other panes.
|
// which gives the same size as all other panes.
|
||||||
"active_pane_magnification": 1.0,
|
"active_pane_magnification": 1.0,
|
||||||
// Enable / disable copilot integration.
|
|
||||||
"enable_copilot_integration": true,
|
|
||||||
// Controls whether copilot provides suggestion immediately
|
|
||||||
// or waits for a `copilot::Toggle`
|
|
||||||
"copilot": "on",
|
|
||||||
// Whether to enable vim modes and key bindings
|
// Whether to enable vim modes and key bindings
|
||||||
"vim_mode": false,
|
"vim_mode": false,
|
||||||
// Whether to show the informational hover box when moving the mouse
|
// Whether to show the informational hover box when moving the mouse
|
||||||
|
@ -30,6 +30,9 @@
|
||||||
// Whether to pop the completions menu while typing in an editor without
|
// Whether to pop the completions menu while typing in an editor without
|
||||||
// explicitly requesting it.
|
// explicitly requesting it.
|
||||||
"show_completions_on_input": true,
|
"show_completions_on_input": true,
|
||||||
|
// Controls whether copilot provides suggestion immediately
|
||||||
|
// or waits for a `copilot::Toggle`
|
||||||
|
"show_copilot_suggestions": true,
|
||||||
// Whether the screen sharing icon is shown in the os status bar.
|
// Whether the screen sharing icon is shown in the os status bar.
|
||||||
"show_call_status_icon": true,
|
"show_call_status_icon": true,
|
||||||
// Whether to use language servers to provide code intelligence.
|
// Whether to use language servers to provide code intelligence.
|
||||||
|
|
|
@ -29,7 +29,10 @@ use std::{
|
||||||
env, future, mem,
|
env, future, mem,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::Arc,
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering::SeqCst},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use unindent::Unindent as _;
|
use unindent::Unindent as _;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
|
@ -3535,6 +3538,141 @@ async fn test_collaborating_with_diagnostics(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||||
|
deterministic: Arc<Deterministic>,
|
||||||
|
cx_a: &mut TestAppContext,
|
||||||
|
cx_b: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
deterministic.forbid_parking();
|
||||||
|
let mut server = TestServer::start(&deterministic).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;
|
||||||
|
|
||||||
|
// Set up a fake language server.
|
||||||
|
let mut language = Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "Rust".into(),
|
||||||
|
path_suffixes: vec!["rs".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_rust::language()),
|
||||||
|
);
|
||||||
|
let mut fake_language_servers = language
|
||||||
|
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||||
|
disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
|
||||||
|
disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
client_a.language_registry.add(Arc::new(language));
|
||||||
|
|
||||||
|
let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
|
||||||
|
client_a
|
||||||
|
.fs
|
||||||
|
.insert_tree(
|
||||||
|
"/test",
|
||||||
|
json!({
|
||||||
|
"one.rs": "const ONE: usize = 1;",
|
||||||
|
"two.rs": "const TWO: usize = 2;",
|
||||||
|
"three.rs": "const THREE: usize = 3;",
|
||||||
|
"four.rs": "const FOUR: usize = 3;",
|
||||||
|
"five.rs": "const FIVE: usize = 3;",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
|
||||||
|
|
||||||
|
// Share a project as client A
|
||||||
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
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 and open all three files.
|
||||||
|
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||||
|
let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
|
||||||
|
project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Simulate a language server reporting errors for a file.
|
||||||
|
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||||
|
fake_language_server
|
||||||
|
.request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
|
||||||
|
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
|
||||||
|
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
|
||||||
|
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
|
||||||
|
lsp::WorkDoneProgressBegin {
|
||||||
|
title: "Progress Began".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
});
|
||||||
|
for file_name in file_names {
|
||||||
|
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||||
|
lsp::PublishDiagnosticsParams {
|
||||||
|
uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
|
||||||
|
version: None,
|
||||||
|
diagnostics: vec![lsp::Diagnostic {
|
||||||
|
severity: Some(lsp::DiagnosticSeverity::WARNING),
|
||||||
|
source: Some("the-disk-based-diagnostics-source".into()),
|
||||||
|
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||||
|
message: "message one".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
|
||||||
|
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
|
||||||
|
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
|
||||||
|
lsp::WorkDoneProgressEnd { message: None },
|
||||||
|
)),
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the "disk base diagnostics finished" message is received, the buffers'
|
||||||
|
// diagnostics are expected to be present.
|
||||||
|
let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
|
||||||
|
project_b.update(cx_b, {
|
||||||
|
let project_b = project_b.clone();
|
||||||
|
let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
|
||||||
|
move |_, cx| {
|
||||||
|
cx.subscribe(&project_b, move |_, _, event, cx| {
|
||||||
|
if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
|
||||||
|
disk_based_diagnostics_finished.store(true, SeqCst);
|
||||||
|
for buffer in &guest_buffers {
|
||||||
|
assert_eq!(
|
||||||
|
buffer
|
||||||
|
.read(cx)
|
||||||
|
.snapshot()
|
||||||
|
.diagnostics_in_range::<_, usize>(0..5, false)
|
||||||
|
.count(),
|
||||||
|
1,
|
||||||
|
"expected a diagnostic for buffer {:?}",
|
||||||
|
buffer.read(cx).file().unwrap().path(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
assert!(disk_based_diagnostics_finished.load(SeqCst));
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_collaborating_with_completion(
|
async fn test_collaborating_with_completion(
|
||||||
deterministic: Arc<Deterministic>,
|
deterministic: Arc<Deterministic>,
|
||||||
|
@ -5862,10 +6000,17 @@ async fn test_basic_following(
|
||||||
|
|
||||||
// Client A updates their selections in those editors
|
// Client A updates their selections in those editors
|
||||||
editor_a1.update(cx_a, |editor, cx| {
|
editor_a1.update(cx_a, |editor, cx| {
|
||||||
editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
|
editor.handle_input("a", cx);
|
||||||
|
editor.handle_input("b", cx);
|
||||||
|
editor.handle_input("c", cx);
|
||||||
|
editor.select_left(&Default::default(), cx);
|
||||||
|
assert_eq!(editor.selections.ranges(cx), vec![3..2]);
|
||||||
});
|
});
|
||||||
editor_a2.update(cx_a, |editor, cx| {
|
editor_a2.update(cx_a, |editor, cx| {
|
||||||
editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
|
editor.handle_input("d", cx);
|
||||||
|
editor.handle_input("e", cx);
|
||||||
|
editor.select_left(&Default::default(), cx);
|
||||||
|
assert_eq!(editor.selections.ranges(cx), vec![2..1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// When client B starts following client A, all visible view states are replicated to client B.
|
// When client B starts following client A, all visible view states are replicated to client B.
|
||||||
|
@ -5878,6 +6023,27 @@ async fn test_basic_following(
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
cx_c.foreground().run_until_parked();
|
||||||
|
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
cx_b.read(|cx| editor_b2.project_path(cx)),
|
||||||
|
Some((worktree_id, "2.txt").into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||||
|
vec![2..1]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||||
|
vec![3..2]
|
||||||
|
);
|
||||||
|
|
||||||
cx_c.foreground().run_until_parked();
|
cx_c.foreground().run_until_parked();
|
||||||
let active_call_c = cx_c.read(ActiveCall::global);
|
let active_call_c = cx_c.read(ActiveCall::global);
|
||||||
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||||
|
@ -6033,26 +6199,6 @@ async fn test_basic_following(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
|
||||||
workspace
|
|
||||||
.active_item(cx)
|
|
||||||
.unwrap()
|
|
||||||
.downcast::<Editor>()
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
assert_eq!(
|
|
||||||
cx_b.read(|cx| editor_b2.project_path(cx)),
|
|
||||||
Some((worktree_id, "2.txt").into())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
|
||||||
vec![2..3]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
|
||||||
vec![0..1]
|
|
||||||
);
|
|
||||||
|
|
||||||
// When client A activates a different editor, client B does so as well.
|
// When client A activates a different editor, client B does so as well.
|
||||||
workspace_a.update(cx_a, |workspace, cx| {
|
workspace_a.update(cx_a, |workspace, cx| {
|
||||||
workspace.activate_item(&editor_a1, cx)
|
workspace.activate_item(&editor_a1, cx)
|
||||||
|
|
|
@ -44,4 +44,3 @@ language = { path = "../language", features = ["test-support"] }
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
util = { path = "../util", features = ["test-support"] }
|
util = { path = "../util", features = ["test-support"] }
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
|
||||||
|
|
|
@ -21,26 +21,19 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use util::{
|
use util::{
|
||||||
channel::ReleaseChannel, fs::remove_matching, github::latest_github_release, http::HttpClient,
|
fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
|
||||||
paths, ResultExt,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
|
const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
|
||||||
actions!(copilot_auth, [SignIn, SignOut]);
|
actions!(copilot_auth, [SignIn, SignOut]);
|
||||||
|
|
||||||
const COPILOT_NAMESPACE: &'static str = "copilot";
|
const COPILOT_NAMESPACE: &'static str = "copilot";
|
||||||
actions!(copilot, [NextSuggestion, PreviousSuggestion, Reinstall]);
|
actions!(
|
||||||
|
copilot,
|
||||||
|
[Suggest, NextSuggestion, PreviousSuggestion, Reinstall]
|
||||||
|
);
|
||||||
|
|
||||||
pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) {
|
pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) {
|
||||||
// Disable Copilot for stable releases.
|
|
||||||
if *cx.global::<ReleaseChannel>() == ReleaseChannel::Stable {
|
|
||||||
cx.update_global::<collections::CommandPaletteFilter, _, _>(|filter, _cx| {
|
|
||||||
filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
|
||||||
filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let copilot = cx.add_model({
|
let copilot = cx.add_model({
|
||||||
let node_runtime = node_runtime.clone();
|
let node_runtime = node_runtime.clone();
|
||||||
move |cx| Copilot::start(http, node_runtime, cx)
|
move |cx| Copilot::start(http, node_runtime, cx)
|
||||||
|
@ -172,7 +165,7 @@ impl Copilot {
|
||||||
let http = http.clone();
|
let http = http.clone();
|
||||||
let node_runtime = node_runtime.clone();
|
let node_runtime = node_runtime.clone();
|
||||||
move |this, cx| {
|
move |this, cx| {
|
||||||
if cx.global::<Settings>().enable_copilot_integration {
|
if cx.global::<Settings>().features.copilot {
|
||||||
if matches!(this.server, CopilotServer::Disabled) {
|
if matches!(this.server, CopilotServer::Disabled) {
|
||||||
let start_task = cx
|
let start_task = cx
|
||||||
.spawn({
|
.spawn({
|
||||||
|
@ -194,12 +187,14 @@ impl Copilot {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
if cx.global::<Settings>().enable_copilot_integration {
|
if cx.global::<Settings>().features.copilot {
|
||||||
let start_task = cx
|
let start_task = cx
|
||||||
.spawn({
|
.spawn({
|
||||||
let http = http.clone();
|
let http = http.clone();
|
||||||
let node_runtime = node_runtime.clone();
|
let node_runtime = node_runtime.clone();
|
||||||
move |this, cx| Self::start_language_server(http, node_runtime, this, cx)
|
move |this, cx| async {
|
||||||
|
Self::start_language_server(http, node_runtime, this, cx).await
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.shared();
|
.shared();
|
||||||
|
|
||||||
|
@ -254,13 +249,6 @@ impl Copilot {
|
||||||
cx.clone(),
|
cx.clone(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let server = server.initialize(Default::default()).await?;
|
|
||||||
let status = server
|
|
||||||
.request::<request::CheckStatus>(request::CheckStatusParams {
|
|
||||||
local_checks_only: false,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
server
|
server
|
||||||
.on_notification::<LogMessage, _>(|params, _cx| {
|
.on_notification::<LogMessage, _>(|params, _cx| {
|
||||||
match params.level {
|
match params.level {
|
||||||
|
@ -281,6 +269,13 @@ impl Copilot {
|
||||||
)
|
)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
let server = server.initialize(Default::default()).await?;
|
||||||
|
let status = server
|
||||||
|
.request::<request::CheckStatus>(request::CheckStatusParams {
|
||||||
|
local_checks_only: false,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
anyhow::Ok((server, status))
|
anyhow::Ok((server, status))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -146,8 +146,8 @@ pub enum LogMessage {}
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct LogMessageParams {
|
pub struct LogMessageParams {
|
||||||
pub message: String,
|
|
||||||
pub level: u8,
|
pub level: u8,
|
||||||
|
pub message: String,
|
||||||
pub metadata_str: String,
|
pub metadata_str: String,
|
||||||
pub extra: Vec<String>,
|
pub extra: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,18 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*,
|
elements::*,
|
||||||
geometry::rect::RectF,
|
geometry::rect::RectF,
|
||||||
|
impl_internal_actions,
|
||||||
platform::{WindowBounds, WindowKind, WindowOptions},
|
platform::{WindowBounds, WindowKind, WindowOptions},
|
||||||
AppContext, ClipboardItem, Element, Entity, View, ViewContext, ViewHandle,
|
AppContext, ClipboardItem, Element, Entity, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use theme::ui::modal;
|
use theme::ui::modal;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
|
struct ClickedConnect;
|
||||||
|
|
||||||
|
impl_internal_actions!(copilot_verification, [ClickedConnect]);
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
struct CopyUserCode;
|
struct CopyUserCode;
|
||||||
|
|
||||||
|
@ -17,45 +23,51 @@ struct OpenGithub;
|
||||||
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
let copilot = Copilot::global(cx).unwrap();
|
if let Some(copilot) = Copilot::global(cx) {
|
||||||
|
let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
|
||||||
|
cx.observe(&copilot, move |copilot, cx| {
|
||||||
|
let status = copilot.read(cx).status();
|
||||||
|
|
||||||
let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
|
match &status {
|
||||||
cx.observe(&copilot, move |copilot, cx| {
|
crate::Status::SigningIn { prompt } => {
|
||||||
let status = copilot.read(cx).status();
|
if let Some(code_verification_handle) = code_verification.as_mut() {
|
||||||
|
if cx.has_window(code_verification_handle.window_id()) {
|
||||||
match &status {
|
code_verification_handle.update(cx, |code_verification_view, cx| {
|
||||||
crate::Status::SigningIn { prompt } => {
|
code_verification_view.set_status(status, cx)
|
||||||
if let Some(code_verification_handle) = code_verification.as_mut() {
|
});
|
||||||
if cx.has_window(code_verification_handle.window_id()) {
|
cx.activate_window(code_verification_handle.window_id());
|
||||||
code_verification_handle.update(cx, |code_verification_view, cx| {
|
} else {
|
||||||
code_verification_view.set_status(status, cx)
|
create_copilot_auth_window(cx, &status, &mut code_verification);
|
||||||
});
|
}
|
||||||
cx.activate_window(code_verification_handle.window_id());
|
} else if let Some(_prompt) = prompt {
|
||||||
} else {
|
|
||||||
create_copilot_auth_window(cx, &status, &mut code_verification);
|
create_copilot_auth_window(cx, &status, &mut code_verification);
|
||||||
}
|
}
|
||||||
} else if let Some(_prompt) = prompt {
|
|
||||||
create_copilot_auth_window(cx, &status, &mut code_verification);
|
|
||||||
}
|
}
|
||||||
}
|
Status::Authorized | Status::Unauthorized => {
|
||||||
Status::Authorized | Status::Unauthorized => {
|
if let Some(code_verification) = code_verification.as_ref() {
|
||||||
if let Some(code_verification) = code_verification.as_ref() {
|
code_verification.update(cx, |code_verification, cx| {
|
||||||
code_verification.update(cx, |code_verification, cx| {
|
code_verification.set_status(status, cx)
|
||||||
code_verification.set_status(status, cx)
|
});
|
||||||
});
|
|
||||||
|
|
||||||
cx.platform().activate(true);
|
cx.platform().activate(true);
|
||||||
cx.activate_window(code_verification.window_id());
|
cx.activate_window(code_verification.window_id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(code_verification) = code_verification.take() {
|
||||||
|
cx.remove_window(code_verification.window_id());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
})
|
||||||
if let Some(code_verification) = code_verification.take() {
|
.detach();
|
||||||
cx.remove_window(code_verification.window_id());
|
|
||||||
}
|
cx.add_action(
|
||||||
}
|
|code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| {
|
||||||
}
|
code_verification.connect_clicked = true;
|
||||||
})
|
},
|
||||||
.detach();
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_copilot_auth_window(
|
fn create_copilot_auth_window(
|
||||||
|
@ -81,11 +93,15 @@ fn create_copilot_auth_window(
|
||||||
|
|
||||||
pub struct CopilotCodeVerification {
|
pub struct CopilotCodeVerification {
|
||||||
status: Status,
|
status: Status,
|
||||||
|
connect_clicked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CopilotCodeVerification {
|
impl CopilotCodeVerification {
|
||||||
pub fn new(status: Status) -> Self {
|
pub fn new(status: Status) -> Self {
|
||||||
Self { status }
|
Self {
|
||||||
|
status,
|
||||||
|
connect_clicked: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
|
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -143,6 +159,7 @@ impl CopilotCodeVerification {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_prompting_modal(
|
fn render_prompting_modal(
|
||||||
|
connect_clicked: bool,
|
||||||
data: &PromptUserDeviceFlow,
|
data: &PromptUserDeviceFlow,
|
||||||
style: &theme::Copilot,
|
style: &theme::Copilot,
|
||||||
cx: &mut gpui::RenderContext<Self>,
|
cx: &mut gpui::RenderContext<Self>,
|
||||||
|
@ -189,13 +206,20 @@ impl CopilotCodeVerification {
|
||||||
.with_style(style.auth.prompting.hint.container.clone())
|
.with_style(style.auth.prompting.hint.container.clone())
|
||||||
.boxed(),
|
.boxed(),
|
||||||
theme::ui::cta_button_with_click(
|
theme::ui::cta_button_with_click(
|
||||||
"Connect to GitHub",
|
if connect_clicked {
|
||||||
|
"Waiting for connection..."
|
||||||
|
} else {
|
||||||
|
"Connect to GitHub"
|
||||||
|
},
|
||||||
style.auth.content_width,
|
style.auth.content_width,
|
||||||
&style.auth.cta_button,
|
&style.auth.cta_button,
|
||||||
cx,
|
cx,
|
||||||
{
|
{
|
||||||
let verification_uri = data.verification_uri.clone();
|
let verification_uri = data.verification_uri.clone();
|
||||||
move |_, cx| cx.platform().open_url(&verification_uri)
|
move |_, cx| {
|
||||||
|
cx.platform().open_url(&verification_uri);
|
||||||
|
cx.dispatch_action(ClickedConnect)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
|
@ -343,9 +367,20 @@ impl View for CopilotCodeVerification {
|
||||||
match &self.status {
|
match &self.status {
|
||||||
Status::SigningIn {
|
Status::SigningIn {
|
||||||
prompt: Some(prompt),
|
prompt: Some(prompt),
|
||||||
} => Self::render_prompting_modal(&prompt, &style.copilot, cx),
|
} => Self::render_prompting_modal(
|
||||||
Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx),
|
self.connect_clicked,
|
||||||
Status::Authorized => Self::render_enabled_modal(&style.copilot, cx),
|
&prompt,
|
||||||
|
&style.copilot,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
Status::Unauthorized => {
|
||||||
|
self.connect_clicked = false;
|
||||||
|
Self::render_unauthorized_modal(&style.copilot, cx)
|
||||||
|
}
|
||||||
|
Status::Authorized => {
|
||||||
|
self.connect_clicked = false;
|
||||||
|
Self::render_enabled_modal(&style.copilot, cx)
|
||||||
|
}
|
||||||
_ => Empty::new().boxed(),
|
_ => Empty::new().boxed(),
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
|
@ -24,6 +24,15 @@ const COPILOT_ERROR_TOAST_ID: usize = 1338;
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct DeployCopilotMenu;
|
pub struct DeployCopilotMenu;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct DeployCopilotStartMenu;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct HideCopilot;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct InitiateSignIn;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct ToggleCopilotForLanguage {
|
pub struct ToggleCopilotForLanguage {
|
||||||
language: Arc<str>,
|
language: Arc<str>,
|
||||||
|
@ -40,6 +49,9 @@ impl_internal_actions!(
|
||||||
copilot,
|
copilot,
|
||||||
[
|
[
|
||||||
DeployCopilotMenu,
|
DeployCopilotMenu,
|
||||||
|
DeployCopilotStartMenu,
|
||||||
|
HideCopilot,
|
||||||
|
InitiateSignIn,
|
||||||
DeployCopilotModal,
|
DeployCopilotModal,
|
||||||
ToggleCopilotForLanguage,
|
ToggleCopilotForLanguage,
|
||||||
ToggleCopilotGlobally,
|
ToggleCopilotGlobally,
|
||||||
|
@ -48,17 +60,19 @@ impl_internal_actions!(
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(CopilotButton::deploy_copilot_menu);
|
cx.add_action(CopilotButton::deploy_copilot_menu);
|
||||||
|
cx.add_action(CopilotButton::deploy_copilot_start_menu);
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
|
|_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
|
||||||
let language = action.language.to_owned();
|
let language = action.language.clone();
|
||||||
|
let show_copilot_suggestions = cx
|
||||||
let current_langauge = cx.global::<Settings>().copilot_on(Some(&language));
|
.global::<Settings>()
|
||||||
|
.show_copilot_suggestions(Some(&language));
|
||||||
|
|
||||||
SettingsFile::update(cx, move |file_contents| {
|
SettingsFile::update(cx, move |file_contents| {
|
||||||
file_contents.languages.insert(
|
file_contents.languages.insert(
|
||||||
language.to_owned(),
|
language,
|
||||||
settings::EditorSettings {
|
settings::EditorSettings {
|
||||||
copilot: Some((!current_langauge).into()),
|
show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -67,12 +81,63 @@ pub fn init(cx: &mut AppContext) {
|
||||||
);
|
);
|
||||||
|
|
||||||
cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
|
cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
|
||||||
let copilot_on = cx.global::<Settings>().copilot_on(None);
|
let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None);
|
||||||
|
|
||||||
SettingsFile::update(cx, move |file_contents| {
|
SettingsFile::update(cx, move |file_contents| {
|
||||||
file_contents.editor.copilot = Some((!copilot_on).into())
|
file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cx.add_action(|_: &mut CopilotButton, _: &HideCopilot, cx| {
|
||||||
|
SettingsFile::update(cx, move |file_contents| {
|
||||||
|
file_contents.features.copilot = Some(false)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.add_action(|_: &mut CopilotButton, _: &InitiateSignIn, cx| {
|
||||||
|
let Some(copilot) = Copilot::global(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let status = copilot.read(cx).status();
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Starting { task } => {
|
||||||
|
cx.dispatch_action(workspace::Toast::new(
|
||||||
|
COPILOT_STARTING_TOAST_ID,
|
||||||
|
"Copilot is starting...",
|
||||||
|
));
|
||||||
|
let window_id = cx.window_id();
|
||||||
|
let task = task.to_owned();
|
||||||
|
cx.spawn(|handle, mut cx| async move {
|
||||||
|
task.await;
|
||||||
|
cx.update(|cx| {
|
||||||
|
if let Some(copilot) = Copilot::global(cx) {
|
||||||
|
let status = copilot.read(cx).status();
|
||||||
|
match status {
|
||||||
|
Status::Authorized => cx.dispatch_action_at(
|
||||||
|
window_id,
|
||||||
|
handle.id(),
|
||||||
|
workspace::Toast::new(
|
||||||
|
COPILOT_STARTING_TOAST_ID,
|
||||||
|
"Copilot has started!",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
cx.dispatch_action_at(
|
||||||
|
window_id,
|
||||||
|
handle.id(),
|
||||||
|
DismissToast::new(COPILOT_STARTING_TOAST_ID),
|
||||||
|
);
|
||||||
|
cx.dispatch_action_at(window_id, handle.id(), SignIn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
_ => cx.dispatch_action(SignIn),
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CopilotButton {
|
pub struct CopilotButton {
|
||||||
|
@ -94,7 +159,7 @@ impl View for CopilotButton {
|
||||||
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
|
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
|
|
||||||
if !settings.enable_copilot_integration {
|
if !settings.features.copilot {
|
||||||
return Empty::new().boxed();
|
return Empty::new().boxed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,9 +170,9 @@ impl View for CopilotButton {
|
||||||
};
|
};
|
||||||
let status = copilot.read(cx).status();
|
let status = copilot.read(cx).status();
|
||||||
|
|
||||||
let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
|
let enabled = self
|
||||||
|
.editor_enabled
|
||||||
let view_id = cx.view_id();
|
.unwrap_or(settings.show_copilot_suggestions(None));
|
||||||
|
|
||||||
Stack::new()
|
Stack::new()
|
||||||
.with_child(
|
.with_child(
|
||||||
|
@ -155,48 +220,13 @@ impl View for CopilotButton {
|
||||||
let status = status.clone();
|
let status = status.clone();
|
||||||
move |_, cx| match status {
|
move |_, cx| match status {
|
||||||
Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
|
Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
|
||||||
Status::Starting { ref task } => {
|
|
||||||
cx.dispatch_action(workspace::Toast::new(
|
|
||||||
COPILOT_STARTING_TOAST_ID,
|
|
||||||
"Copilot is starting...",
|
|
||||||
));
|
|
||||||
let window_id = cx.window_id();
|
|
||||||
let task = task.to_owned();
|
|
||||||
cx.spawn(|mut cx| async move {
|
|
||||||
task.await;
|
|
||||||
cx.update(|cx| {
|
|
||||||
if let Some(copilot) = Copilot::global(cx) {
|
|
||||||
let status = copilot.read(cx).status();
|
|
||||||
match status {
|
|
||||||
Status::Authorized => cx.dispatch_action_at(
|
|
||||||
window_id,
|
|
||||||
view_id,
|
|
||||||
workspace::Toast::new(
|
|
||||||
COPILOT_STARTING_TOAST_ID,
|
|
||||||
"Copilot has started!",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_ => {
|
|
||||||
cx.dispatch_action_at(
|
|
||||||
window_id,
|
|
||||||
view_id,
|
|
||||||
DismissToast::new(COPILOT_STARTING_TOAST_ID),
|
|
||||||
);
|
|
||||||
cx.dispatch_global_action(SignIn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
|
Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
|
||||||
COPILOT_ERROR_TOAST_ID,
|
COPILOT_ERROR_TOAST_ID,
|
||||||
format!("Copilot can't be started: {}", e),
|
format!("Copilot can't be started: {}", e),
|
||||||
"Reinstall Copilot",
|
"Reinstall Copilot",
|
||||||
Reinstall,
|
Reinstall,
|
||||||
)),
|
)),
|
||||||
_ => cx.dispatch_action(SignIn),
|
_ => cx.dispatch_action(DeployCopilotStartMenu),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.with_tooltip::<Self, _>(
|
.with_tooltip::<Self, _>(
|
||||||
|
@ -242,22 +272,38 @@ impl CopilotButton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn deploy_copilot_start_menu(
|
||||||
|
&mut self,
|
||||||
|
_: &DeployCopilotStartMenu,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let mut menu_options = Vec::with_capacity(2);
|
||||||
|
|
||||||
|
menu_options.push(ContextMenuItem::item("Sign In", InitiateSignIn));
|
||||||
|
menu_options.push(ContextMenuItem::item("Hide Copilot", HideCopilot));
|
||||||
|
|
||||||
|
self.popup_menu.update(cx, |menu, cx| {
|
||||||
|
menu.show(
|
||||||
|
Default::default(),
|
||||||
|
AnchorCorner::BottomRight,
|
||||||
|
menu_options,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
|
pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
|
|
||||||
let mut menu_options = Vec::with_capacity(6);
|
let mut menu_options = Vec::with_capacity(6);
|
||||||
|
|
||||||
if let Some(language) = &self.language {
|
if let Some(language) = &self.language {
|
||||||
let language_enabled = settings.copilot_on(Some(language.as_ref()));
|
let language_enabled = settings.show_copilot_suggestions(Some(language.as_ref()));
|
||||||
|
|
||||||
menu_options.push(ContextMenuItem::item(
|
menu_options.push(ContextMenuItem::item(
|
||||||
format!(
|
format!(
|
||||||
"{} Copilot for {}",
|
"{} Suggestions for {}",
|
||||||
if language_enabled {
|
if language_enabled { "Hide" } else { "Show" },
|
||||||
"Disable"
|
|
||||||
} else {
|
|
||||||
"Enable"
|
|
||||||
},
|
|
||||||
language
|
language
|
||||||
),
|
),
|
||||||
ToggleCopilotForLanguage {
|
ToggleCopilotForLanguage {
|
||||||
|
@ -266,12 +312,12 @@ impl CopilotButton {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let globally_enabled = cx.global::<Settings>().copilot_on(None);
|
let globally_enabled = cx.global::<Settings>().show_copilot_suggestions(None);
|
||||||
menu_options.push(ContextMenuItem::item(
|
menu_options.push(ContextMenuItem::item(
|
||||||
if globally_enabled {
|
if globally_enabled {
|
||||||
"Disable Copilot Globally"
|
"Hide Suggestions for All Files"
|
||||||
} else {
|
} else {
|
||||||
"Enable Copilot Globally"
|
"Show Suggestions for All Files"
|
||||||
},
|
},
|
||||||
ToggleCopilotGlobally,
|
ToggleCopilotGlobally,
|
||||||
));
|
));
|
||||||
|
@ -319,7 +365,7 @@ impl CopilotButton {
|
||||||
|
|
||||||
self.language = language_name.clone();
|
self.language = language_name.clone();
|
||||||
|
|
||||||
self.editor_enabled = Some(settings.copilot_on(language_name.as_deref()));
|
self.editor_enabled = Some(settings.show_copilot_suggestions(language_name.as_deref()));
|
||||||
|
|
||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ mod editor_tests;
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
|
||||||
use aho_corasick::AhoCorasick;
|
use aho_corasick::AhoCorasick;
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
use blink_manager::BlinkManager;
|
use blink_manager::BlinkManager;
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||||
|
@ -395,6 +395,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_async_action(Editor::find_all_references);
|
cx.add_async_action(Editor::find_all_references);
|
||||||
cx.add_action(Editor::next_copilot_suggestion);
|
cx.add_action(Editor::next_copilot_suggestion);
|
||||||
cx.add_action(Editor::previous_copilot_suggestion);
|
cx.add_action(Editor::previous_copilot_suggestion);
|
||||||
|
cx.add_action(Editor::copilot_suggest);
|
||||||
|
|
||||||
hover_popover::init(cx);
|
hover_popover::init(cx);
|
||||||
link_go_to_definition::init(cx);
|
link_go_to_definition::init(cx);
|
||||||
|
@ -1014,6 +1015,8 @@ impl CodeActionsMenu {
|
||||||
pub struct CopilotState {
|
pub struct CopilotState {
|
||||||
excerpt_id: Option<ExcerptId>,
|
excerpt_id: Option<ExcerptId>,
|
||||||
pending_refresh: Task<Option<()>>,
|
pending_refresh: Task<Option<()>>,
|
||||||
|
pending_cycling_refresh: Task<Option<()>>,
|
||||||
|
cycled: bool,
|
||||||
completions: Vec<copilot::Completion>,
|
completions: Vec<copilot::Completion>,
|
||||||
active_completion_index: usize,
|
active_completion_index: usize,
|
||||||
}
|
}
|
||||||
|
@ -1022,9 +1025,11 @@ impl Default for CopilotState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
excerpt_id: None,
|
excerpt_id: None,
|
||||||
|
pending_cycling_refresh: Task::ready(Some(())),
|
||||||
pending_refresh: Task::ready(Some(())),
|
pending_refresh: Task::ready(Some(())),
|
||||||
completions: Default::default(),
|
completions: Default::default(),
|
||||||
active_completion_index: 0,
|
active_completion_index: 0,
|
||||||
|
cycled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1040,7 +1045,8 @@ impl CopilotState {
|
||||||
let completion = self.completions.get(self.active_completion_index)?;
|
let completion = self.completions.get(self.active_completion_index)?;
|
||||||
let excerpt_id = self.excerpt_id?;
|
let excerpt_id = self.excerpt_id?;
|
||||||
let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
|
let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
|
||||||
if !completion.range.start.is_valid(completion_buffer)
|
if excerpt_id != cursor.excerpt_id
|
||||||
|
|| !completion.range.start.is_valid(completion_buffer)
|
||||||
|| !completion.range.end.is_valid(completion_buffer)
|
|| !completion.range.end.is_valid(completion_buffer)
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
|
@ -1067,6 +1073,26 @@ impl CopilotState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cycle_completions(&mut self, direction: Direction) {
|
||||||
|
match direction {
|
||||||
|
Direction::Prev => {
|
||||||
|
self.active_completion_index = if self.active_completion_index == 0 {
|
||||||
|
self.completions.len().saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
self.active_completion_index - 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Direction::Next => {
|
||||||
|
if self.completions.len() == 0 {
|
||||||
|
self.active_completion_index = 0
|
||||||
|
} else {
|
||||||
|
self.active_completion_index =
|
||||||
|
(self.active_completion_index + 1) % self.completions.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn push_completion(&mut self, new_completion: copilot::Completion) {
|
fn push_completion(&mut self, new_completion: copilot::Completion) {
|
||||||
for completion in &self.completions {
|
for completion in &self.completions {
|
||||||
if *completion == new_completion {
|
if *completion == new_completion {
|
||||||
|
@ -1264,7 +1290,7 @@ impl Editor {
|
||||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||||
cx.observe(&display_map, Self::on_display_map_changed),
|
cx.observe(&display_map, Self::on_display_map_changed),
|
||||||
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
|
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
|
||||||
cx.observe_global::<Settings, _>(Self::on_settings_changed),
|
cx.observe_global::<Settings, _>(Self::settings_changed),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
this.end_selection(cx);
|
this.end_selection(cx);
|
||||||
|
@ -2025,13 +2051,13 @@ impl Editor {
|
||||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||||
|
|
||||||
if had_active_copilot_suggestion {
|
if had_active_copilot_suggestion {
|
||||||
this.refresh_copilot_suggestions(cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
if !this.has_active_copilot_suggestion(cx) {
|
if !this.has_active_copilot_suggestion(cx) {
|
||||||
this.trigger_completion_on_input(&text, cx);
|
this.trigger_completion_on_input(&text, cx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.trigger_completion_on_input(&text, cx);
|
this.trigger_completion_on_input(&text, cx);
|
||||||
this.refresh_copilot_suggestions(cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2113,7 +2139,7 @@ impl Editor {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||||
this.refresh_copilot_suggestions(cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2351,53 +2377,66 @@ impl Editor {
|
||||||
let id = post_inc(&mut self.next_completion_id);
|
let id = post_inc(&mut self.next_completion_id);
|
||||||
let task = cx.spawn_weak(|this, mut cx| {
|
let task = cx.spawn_weak(|this, mut cx| {
|
||||||
async move {
|
async move {
|
||||||
let completions = completions.await?;
|
let menu = if let Some(completions) = completions.await.log_err() {
|
||||||
if completions.is_empty() {
|
let mut menu = CompletionsMenu {
|
||||||
return Ok(());
|
id,
|
||||||
}
|
initial_position: position,
|
||||||
|
match_candidates: completions
|
||||||
let mut menu = CompletionsMenu {
|
.iter()
|
||||||
id,
|
.enumerate()
|
||||||
initial_position: position,
|
.map(|(id, completion)| {
|
||||||
match_candidates: completions
|
StringMatchCandidate::new(
|
||||||
.iter()
|
id,
|
||||||
.enumerate()
|
completion.label.text[completion.label.filter_range.clone()]
|
||||||
.map(|(id, completion)| {
|
.into(),
|
||||||
StringMatchCandidate::new(
|
)
|
||||||
id,
|
})
|
||||||
completion.label.text[completion.label.filter_range.clone()].into(),
|
.collect(),
|
||||||
)
|
buffer,
|
||||||
})
|
completions: completions.into(),
|
||||||
.collect(),
|
matches: Vec::new().into(),
|
||||||
buffer,
|
selected_item: 0,
|
||||||
completions: completions.into(),
|
list: Default::default(),
|
||||||
matches: Vec::new().into(),
|
};
|
||||||
selected_item: 0,
|
menu.filter(query.as_deref(), cx.background()).await;
|
||||||
list: Default::default(),
|
if menu.matches.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(menu)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
menu.filter(query.as_deref(), cx.background()).await;
|
let this = this
|
||||||
|
.upgrade(&cx)
|
||||||
|
.ok_or_else(|| anyhow!("editor was dropped"))?;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.completion_tasks.retain(|(task_id, _)| *task_id > id);
|
||||||
|
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
match this.context_menu.as_ref() {
|
||||||
this.update(&mut cx, |this, cx| {
|
None => {}
|
||||||
match this.context_menu.as_ref() {
|
Some(ContextMenu::Completions(prev_menu)) => {
|
||||||
None => {}
|
if prev_menu.id > id {
|
||||||
Some(ContextMenu::Completions(prev_menu)) => {
|
return;
|
||||||
if prev_menu.id > menu.id {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => return,
|
|
||||||
}
|
}
|
||||||
|
_ => return,
|
||||||
|
}
|
||||||
|
|
||||||
this.completion_tasks.retain(|(id, _)| *id > menu.id);
|
if this.focused && menu.is_some() {
|
||||||
if this.focused && !menu.matches.is_empty() {
|
let menu = menu.unwrap();
|
||||||
this.show_context_menu(ContextMenu::Completions(menu), cx);
|
this.show_context_menu(ContextMenu::Completions(menu), cx);
|
||||||
} else if this.hide_context_menu(cx).is_none() {
|
} else if this.completion_tasks.is_empty() {
|
||||||
|
// If there are no more completion tasks and the last menu was
|
||||||
|
// empty, we should hide it. If it was already hidden, we should
|
||||||
|
// also show the copilot suggestion when available.
|
||||||
|
if this.hide_context_menu(cx).is_none() {
|
||||||
this.update_visible_copilot_suggestion(cx);
|
this.update_visible_copilot_suggestion(cx);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
Ok::<_, anyhow::Error>(())
|
Ok::<_, anyhow::Error>(())
|
||||||
}
|
}
|
||||||
.log_err()
|
.log_err()
|
||||||
|
@ -2498,7 +2537,7 @@ impl Editor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refresh_copilot_suggestions(cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let project = self.project.clone()?;
|
let project = self.project.clone()?;
|
||||||
|
@ -2791,10 +2830,14 @@ impl Editor {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
|
fn refresh_copilot_suggestions(
|
||||||
|
&mut self,
|
||||||
|
debounce: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<()> {
|
||||||
let copilot = Copilot::global(cx)?;
|
let copilot = Copilot::global(cx)?;
|
||||||
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
|
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
|
||||||
self.hide_copilot_suggestion(cx);
|
self.clear_copilot_suggestions(cx);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
self.update_visible_copilot_suggestion(cx);
|
self.update_visible_copilot_suggestion(cx);
|
||||||
|
@ -2802,28 +2845,35 @@ impl Editor {
|
||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||||
let cursor = self.selections.newest_anchor().head();
|
let cursor = self.selections.newest_anchor().head();
|
||||||
let language_name = snapshot.language_at(cursor).map(|language| language.name());
|
let language_name = snapshot.language_at(cursor).map(|language| language.name());
|
||||||
if !cx.global::<Settings>().copilot_on(language_name.as_deref()) {
|
if !cx
|
||||||
self.hide_copilot_suggestion(cx);
|
.global::<Settings>()
|
||||||
|
.show_copilot_suggestions(language_name.as_deref())
|
||||||
|
{
|
||||||
|
self.clear_copilot_suggestions(cx);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (buffer, buffer_position) =
|
let (buffer, buffer_position) =
|
||||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
||||||
self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
|
self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
|
||||||
cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
|
if debounce {
|
||||||
let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| {
|
cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
|
||||||
(
|
}
|
||||||
copilot.completions(&buffer, buffer_position, cx),
|
|
||||||
copilot.completions_cycling(&buffer, buffer_position, cx),
|
let completions = copilot
|
||||||
)
|
.update(&mut cx, |copilot, cx| {
|
||||||
});
|
copilot.completions(&buffer, buffer_position, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.log_err()
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
let (completion, completions_cycling) = futures::join!(completion, completions_cycling);
|
|
||||||
let mut completions = Vec::new();
|
|
||||||
completions.extend(completion.log_err().into_iter().flatten());
|
|
||||||
completions.extend(completions_cycling.log_err().into_iter().flatten());
|
|
||||||
this.upgrade(&cx)?.update(&mut cx, |this, cx| {
|
this.upgrade(&cx)?.update(&mut cx, |this, cx| {
|
||||||
if !completions.is_empty() {
|
if !completions.is_empty() {
|
||||||
|
this.copilot_state.cycled = false;
|
||||||
|
this.copilot_state.pending_cycling_refresh = Task::ready(None);
|
||||||
this.copilot_state.completions.clear();
|
this.copilot_state.completions.clear();
|
||||||
this.copilot_state.active_completion_index = 0;
|
this.copilot_state.active_completion_index = 0;
|
||||||
this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
|
this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
|
||||||
|
@ -2840,34 +2890,73 @@ impl Editor {
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
|
fn cycle_suggestions(
|
||||||
|
&mut self,
|
||||||
|
direction: Direction,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<()> {
|
||||||
|
let copilot = Copilot::global(cx)?;
|
||||||
|
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.copilot_state.cycled {
|
||||||
|
self.copilot_state.cycle_completions(direction);
|
||||||
|
self.update_visible_copilot_suggestion(cx);
|
||||||
|
} else {
|
||||||
|
let cursor = self.selections.newest_anchor().head();
|
||||||
|
let (buffer, buffer_position) =
|
||||||
|
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
||||||
|
self.copilot_state.pending_cycling_refresh = cx.spawn_weak(|this, mut cx| async move {
|
||||||
|
let completions = copilot
|
||||||
|
.update(&mut cx, |copilot, cx| {
|
||||||
|
copilot.completions_cycling(&buffer, buffer_position, cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
this.upgrade(&cx)?.update(&mut cx, |this, cx| {
|
||||||
|
this.copilot_state.cycled = true;
|
||||||
|
for completion in completions.log_err().into_iter().flatten() {
|
||||||
|
this.copilot_state.push_completion(completion);
|
||||||
|
}
|
||||||
|
this.copilot_state.cycle_completions(direction);
|
||||||
|
this.update_visible_copilot_suggestion(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copilot_suggest(&mut self, _: &copilot::Suggest, cx: &mut ViewContext<Self>) {
|
||||||
if !self.has_active_copilot_suggestion(cx) {
|
if !self.has_active_copilot_suggestion(cx) {
|
||||||
self.refresh_copilot_suggestions(cx);
|
self.refresh_copilot_suggestions(false, cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.copilot_state.active_completion_index =
|
|
||||||
(self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len();
|
|
||||||
self.update_visible_copilot_suggestion(cx);
|
self.update_visible_copilot_suggestion(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
|
||||||
|
if self.has_active_copilot_suggestion(cx) {
|
||||||
|
self.cycle_suggestions(Direction::Next, cx);
|
||||||
|
} else {
|
||||||
|
self.refresh_copilot_suggestions(false, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn previous_copilot_suggestion(
|
fn previous_copilot_suggestion(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &copilot::PreviousSuggestion,
|
_: &copilot::PreviousSuggestion,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
if !self.has_active_copilot_suggestion(cx) {
|
if self.has_active_copilot_suggestion(cx) {
|
||||||
self.refresh_copilot_suggestions(cx);
|
self.cycle_suggestions(Direction::Prev, cx);
|
||||||
return;
|
} else {
|
||||||
|
self.refresh_copilot_suggestions(false, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.copilot_state.active_completion_index =
|
|
||||||
if self.copilot_state.active_completion_index == 0 {
|
|
||||||
self.copilot_state.completions.len() - 1
|
|
||||||
} else {
|
|
||||||
self.copilot_state.active_completion_index - 1
|
|
||||||
};
|
|
||||||
self.update_visible_copilot_suggestion(cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||||
|
@ -2909,11 +2998,11 @@ impl Editor {
|
||||||
.copilot_state
|
.copilot_state
|
||||||
.text_for_active_completion(cursor, &snapshot)
|
.text_for_active_completion(cursor, &snapshot)
|
||||||
{
|
{
|
||||||
self.display_map.update(cx, |map, cx| {
|
self.display_map.update(cx, move |map, cx| {
|
||||||
map.replace_suggestion(
|
map.replace_suggestion(
|
||||||
Some(Suggestion {
|
Some(Suggestion {
|
||||||
position: cursor,
|
position: cursor,
|
||||||
text: text.into(),
|
text: text.trim_end().into(),
|
||||||
}),
|
}),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -2924,6 +3013,11 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
self.copilot_state = Default::default();
|
||||||
|
self.hide_copilot_suggestion(cx);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_code_actions_indicator(
|
pub fn render_code_actions_indicator(
|
||||||
&self,
|
&self,
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
|
@ -3209,7 +3303,7 @@ impl Editor {
|
||||||
|
|
||||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||||
this.insert("", cx);
|
this.insert("", cx);
|
||||||
this.refresh_copilot_suggestions(cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3225,7 +3319,7 @@ impl Editor {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
this.insert("", cx);
|
this.insert("", cx);
|
||||||
this.refresh_copilot_suggestions(cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3321,7 +3415,7 @@ impl Editor {
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
|
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
|
||||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||||
this.refresh_copilot_suggestions(cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4001,7 +4095,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||||
self.unmark_text(cx);
|
self.unmark_text(cx);
|
||||||
self.refresh_copilot_suggestions(cx);
|
self.refresh_copilot_suggestions(true, cx);
|
||||||
cx.emit(Event::Edited);
|
cx.emit(Event::Edited);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4016,7 +4110,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||||
self.unmark_text(cx);
|
self.unmark_text(cx);
|
||||||
self.refresh_copilot_suggestions(cx);
|
self.refresh_copilot_suggestions(true, cx);
|
||||||
cx.emit(Event::Edited);
|
cx.emit(Event::Edited);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6477,8 +6571,8 @@ impl Editor {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_settings_changed(&mut self, cx: &mut ViewContext<Self>) {
|
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
self.refresh_copilot_suggestions(cx);
|
self.refresh_copilot_suggestions(true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_searchable(&mut self, searchable: bool) {
|
pub fn set_searchable(&mut self, searchable: bool) {
|
||||||
|
@ -6619,13 +6713,15 @@ impl Editor {
|
||||||
.as_singleton()
|
.as_singleton()
|
||||||
.and_then(|b| b.read(cx).file()),
|
.and_then(|b| b.read(cx).file()),
|
||||||
) {
|
) {
|
||||||
|
let settings = cx.global::<Settings>();
|
||||||
|
|
||||||
let extension = Path::new(file.file_name(cx))
|
let extension = Path::new(file.file_name(cx))
|
||||||
.extension()
|
.extension()
|
||||||
.and_then(|e| e.to_str());
|
.and_then(|e| e.to_str());
|
||||||
project.read(cx).client().report_event(
|
project.read(cx).client().report_event(
|
||||||
name,
|
name,
|
||||||
json!({ "File Extension": extension }),
|
json!({ "File Extension": extension, "Vim Mode": settings.vim_mode }),
|
||||||
cx.global::<Settings>().telemetry(),
|
settings.telemetry(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4322,7 +4322,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
|
||||||
cx.set_state(
|
cx.set_state(
|
||||||
&[
|
&[
|
||||||
"one ", //
|
"one ", //
|
||||||
"twoˇ", //
|
"twoˇ", //
|
||||||
"three ", //
|
"three ", //
|
||||||
"four", //
|
"four", //
|
||||||
]
|
]
|
||||||
|
@ -4397,7 +4397,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
|
||||||
&[
|
&[
|
||||||
"one", //
|
"one", //
|
||||||
"", //
|
"", //
|
||||||
"twoˇ", //
|
"twoˇ", //
|
||||||
"", //
|
"", //
|
||||||
"three", //
|
"three", //
|
||||||
"four", //
|
"four", //
|
||||||
|
@ -4412,7 +4412,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
|
||||||
cx.assert_editor_state(
|
cx.assert_editor_state(
|
||||||
&[
|
&[
|
||||||
"one ", //
|
"one ", //
|
||||||
"twoˇ", //
|
"twoˇ", //
|
||||||
"three ", //
|
"three ", //
|
||||||
"four", //
|
"four", //
|
||||||
]
|
]
|
||||||
|
@ -5897,13 +5897,12 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
||||||
cx.set_state(indoc! {"
|
cx.set_state(indoc! {"
|
||||||
oneˇ
|
oneˇ
|
||||||
two
|
two
|
||||||
three
|
three
|
||||||
"});
|
"});
|
||||||
|
|
||||||
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
|
||||||
cx.simulate_keystroke(".");
|
cx.simulate_keystroke(".");
|
||||||
let _ = handle_completion_request(
|
let _ = handle_completion_request(
|
||||||
&mut cx,
|
&mut cx,
|
||||||
|
@ -5917,8 +5916,8 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
|
||||||
handle_copilot_completion_request(
|
handle_copilot_completion_request(
|
||||||
&copilot_lsp,
|
&copilot_lsp,
|
||||||
vec![copilot::request::Completion {
|
vec![copilot::request::Completion {
|
||||||
text: "copilot1".into(),
|
text: "one.copilot1".into(),
|
||||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
|
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
vec![],
|
vec![],
|
||||||
|
@ -5940,13 +5939,45 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
|
||||||
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
|
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ensure Copilot suggestions are shown right away if no autocompletion is available.
|
||||||
cx.set_state(indoc! {"
|
cx.set_state(indoc! {"
|
||||||
oneˇ
|
oneˇ
|
||||||
two
|
two
|
||||||
three
|
three
|
||||||
"});
|
"});
|
||||||
|
cx.simulate_keystroke(".");
|
||||||
|
let _ = handle_completion_request(
|
||||||
|
&mut cx,
|
||||||
|
indoc! {"
|
||||||
|
one.|<>
|
||||||
|
two
|
||||||
|
three
|
||||||
|
"},
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
handle_copilot_completion_request(
|
||||||
|
&copilot_lsp,
|
||||||
|
vec![copilot::request::Completion {
|
||||||
|
text: "one.copilot1".into(),
|
||||||
|
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||||
|
cx.update_editor(|editor, cx| {
|
||||||
|
assert!(!editor.context_menu_visible());
|
||||||
|
assert!(editor.has_active_copilot_suggestion(cx));
|
||||||
|
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||||
|
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||||
|
});
|
||||||
|
|
||||||
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
// Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
oneˇ
|
||||||
|
two
|
||||||
|
three
|
||||||
|
"});
|
||||||
cx.simulate_keystroke(".");
|
cx.simulate_keystroke(".");
|
||||||
let _ = handle_completion_request(
|
let _ = handle_completion_request(
|
||||||
&mut cx,
|
&mut cx,
|
||||||
|
@ -6163,6 +6194,110 @@ async fn test_copilot_completion_invalidation(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_copilot_multibuffer(
|
||||||
|
deterministic: Arc<Deterministic>,
|
||||||
|
cx: &mut gpui::TestAppContext,
|
||||||
|
) {
|
||||||
|
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.set_global(Settings::test(cx));
|
||||||
|
cx.set_global(copilot)
|
||||||
|
});
|
||||||
|
|
||||||
|
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx));
|
||||||
|
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx));
|
||||||
|
let multibuffer = cx.add_model(|cx| {
|
||||||
|
let mut multibuffer = MultiBuffer::new(0);
|
||||||
|
multibuffer.push_excerpts(
|
||||||
|
buffer_1.clone(),
|
||||||
|
[ExcerptRange {
|
||||||
|
context: Point::new(0, 0)..Point::new(2, 0),
|
||||||
|
primary: None,
|
||||||
|
}],
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
multibuffer.push_excerpts(
|
||||||
|
buffer_2.clone(),
|
||||||
|
[ExcerptRange {
|
||||||
|
context: Point::new(0, 0)..Point::new(2, 0),
|
||||||
|
primary: None,
|
||||||
|
}],
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
multibuffer
|
||||||
|
});
|
||||||
|
let (_, editor) = cx.add_window(|cx| build_editor(multibuffer, cx));
|
||||||
|
|
||||||
|
handle_copilot_completion_request(
|
||||||
|
&copilot_lsp,
|
||||||
|
vec![copilot::request::Completion {
|
||||||
|
text: "b = 2 + a".into(),
|
||||||
|
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
// Ensure copilot suggestions are shown for the first excerpt.
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
|
||||||
|
});
|
||||||
|
editor.next_copilot_suggestion(&Default::default(), cx);
|
||||||
|
});
|
||||||
|
deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
assert!(editor.has_active_copilot_suggestion(cx));
|
||||||
|
assert_eq!(
|
||||||
|
editor.display_text(cx),
|
||||||
|
"\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
|
||||||
|
);
|
||||||
|
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
handle_copilot_completion_request(
|
||||||
|
&copilot_lsp,
|
||||||
|
vec![copilot::request::Completion {
|
||||||
|
text: "d = 4 + c".into(),
|
||||||
|
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
// Move to another excerpt, ensuring the suggestion gets cleared.
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
|
||||||
|
});
|
||||||
|
assert!(!editor.has_active_copilot_suggestion(cx));
|
||||||
|
assert_eq!(
|
||||||
|
editor.display_text(cx),
|
||||||
|
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
|
||||||
|
);
|
||||||
|
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
|
||||||
|
|
||||||
|
// Type a character, ensuring we don't even try to interpolate the previous suggestion.
|
||||||
|
editor.handle_input(" ", cx);
|
||||||
|
assert!(!editor.has_active_copilot_suggestion(cx));
|
||||||
|
assert_eq!(
|
||||||
|
editor.display_text(cx),
|
||||||
|
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
|
||||||
|
);
|
||||||
|
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure the new suggestion is displayed when the debounce timeout expires.
|
||||||
|
deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
assert!(editor.has_active_copilot_suggestion(cx));
|
||||||
|
assert_eq!(
|
||||||
|
editor.display_text(cx),
|
||||||
|
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
|
||||||
|
);
|
||||||
|
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||||
let point = DisplayPoint::new(row as u32, column as u32);
|
let point = DisplayPoint::new(row as u32, column as u32);
|
||||||
point..point
|
point..point
|
||||||
|
|
|
@ -3,12 +3,12 @@ use crate::{
|
||||||
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
|
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
|
||||||
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
|
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use futures::future::try_join_all;
|
use futures::future::try_join_all;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, RenderContext,
|
elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle,
|
||||||
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use language::{
|
use language::{
|
||||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
|
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
|
||||||
|
@ -72,11 +72,11 @@ impl FollowableItem for Editor {
|
||||||
let editor = pane.read_with(&cx, |pane, cx| {
|
let editor = pane.read_with(&cx, |pane, cx| {
|
||||||
let mut editors = pane.items_of_type::<Self>();
|
let mut editors = pane.items_of_type::<Self>();
|
||||||
editors.find(|editor| {
|
editors.find(|editor| {
|
||||||
editor.remote_id(&client, cx) == Some(remote_id)
|
let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
|
||||||
|| state.singleton
|
let singleton_buffer_matches = state.singleton
|
||||||
&& buffers.len() == 1
|
&& buffers.first()
|
||||||
&& editor.read(cx).buffer.read(cx).as_singleton().as_ref()
|
== editor.read(cx).buffer.read(cx).as_singleton().as_ref();
|
||||||
== Some(&buffers[0])
|
ids_match || singleton_buffer_matches
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -115,46 +115,29 @@ impl FollowableItem for Editor {
|
||||||
multibuffer
|
multibuffer
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))
|
cx.add_view(|cx| {
|
||||||
|
let mut editor =
|
||||||
|
Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
|
||||||
|
editor.remote_id = Some(remote_id);
|
||||||
|
editor
|
||||||
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.update(&mut cx, |editor, cx| {
|
update_editor_from_message(
|
||||||
editor.remote_id = Some(remote_id);
|
editor.clone(),
|
||||||
let buffer = editor.buffer.read(cx).read(cx);
|
project,
|
||||||
let selections = state
|
proto::update_view::Editor {
|
||||||
.selections
|
selections: state.selections,
|
||||||
.into_iter()
|
pending_selection: state.pending_selection,
|
||||||
.map(|selection| {
|
scroll_top_anchor: state.scroll_top_anchor,
|
||||||
deserialize_selection(&buffer, selection)
|
scroll_x: state.scroll_x,
|
||||||
.ok_or_else(|| anyhow!("invalid selection"))
|
scroll_y: state.scroll_y,
|
||||||
})
|
..Default::default()
|
||||||
.collect::<Result<Vec<_>>>()?;
|
},
|
||||||
let pending_selection = state
|
&mut cx,
|
||||||
.pending_selection
|
)
|
||||||
.map(|selection| deserialize_selection(&buffer, selection))
|
.await?;
|
||||||
.flatten();
|
|
||||||
let scroll_top_anchor = state
|
|
||||||
.scroll_top_anchor
|
|
||||||
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
|
|
||||||
drop(buffer);
|
|
||||||
|
|
||||||
if !selections.is_empty() || pending_selection.is_some() {
|
|
||||||
editor.set_selections_from_remote(selections, pending_selection, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(scroll_top_anchor) = scroll_top_anchor {
|
|
||||||
editor.set_scroll_anchor_remote(
|
|
||||||
ScrollAnchor {
|
|
||||||
top_anchor: scroll_top_anchor,
|
|
||||||
offset: vec2f(state.scroll_x, state.scroll_y),
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(editor)
|
Ok(editor)
|
||||||
}))
|
}))
|
||||||
|
@ -299,96 +282,9 @@ impl FollowableItem for Editor {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let update_view::Variant::Editor(message) = message;
|
let update_view::Variant::Editor(message) = message;
|
||||||
let multibuffer = self.buffer.read(cx);
|
|
||||||
let multibuffer = multibuffer.read(cx);
|
|
||||||
|
|
||||||
let buffer_ids = message
|
|
||||||
.inserted_excerpts
|
|
||||||
.iter()
|
|
||||||
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
|
|
||||||
.collect::<HashSet<_>>();
|
|
||||||
|
|
||||||
let mut removals = message
|
|
||||||
.deleted_excerpts
|
|
||||||
.into_iter()
|
|
||||||
.map(ExcerptId::from_proto)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
|
|
||||||
|
|
||||||
let selections = message
|
|
||||||
.selections
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|selection| deserialize_selection(&multibuffer, selection))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let pending_selection = message
|
|
||||||
.pending_selection
|
|
||||||
.and_then(|selection| deserialize_selection(&multibuffer, selection));
|
|
||||||
|
|
||||||
let scroll_top_anchor = message
|
|
||||||
.scroll_top_anchor
|
|
||||||
.and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
|
|
||||||
drop(multibuffer);
|
|
||||||
|
|
||||||
let buffers = project.update(cx, |project, cx| {
|
|
||||||
buffer_ids
|
|
||||||
.into_iter()
|
|
||||||
.map(|id| project.open_buffer_by_id(id, cx))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
});
|
|
||||||
|
|
||||||
let project = project.clone();
|
let project = project.clone();
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let _buffers = try_join_all(buffers).await?;
|
update_editor_from_message(this, project, message, &mut cx).await
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.buffer.update(cx, |multibuffer, cx| {
|
|
||||||
let mut insertions = message.inserted_excerpts.into_iter().peekable();
|
|
||||||
while let Some(insertion) = insertions.next() {
|
|
||||||
let Some(excerpt) = insertion.excerpt else { continue };
|
|
||||||
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
|
|
||||||
let buffer_id = excerpt.buffer_id;
|
|
||||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
|
|
||||||
|
|
||||||
let adjacent_excerpts = iter::from_fn(|| {
|
|
||||||
let insertion = insertions.peek()?;
|
|
||||||
if insertion.previous_excerpt_id.is_none()
|
|
||||||
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
|
|
||||||
{
|
|
||||||
insertions.next()?.excerpt
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
multibuffer.insert_excerpts_with_ids_after(
|
|
||||||
ExcerptId::from_proto(previous_excerpt_id),
|
|
||||||
buffer,
|
|
||||||
[excerpt]
|
|
||||||
.into_iter()
|
|
||||||
.chain(adjacent_excerpts)
|
|
||||||
.filter_map(|excerpt| {
|
|
||||||
Some((
|
|
||||||
ExcerptId::from_proto(excerpt.id),
|
|
||||||
deserialize_excerpt_range(excerpt)?,
|
|
||||||
))
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
multibuffer.remove_excerpts(removals, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
if !selections.is_empty() || pending_selection.is_some() {
|
|
||||||
this.set_selections_from_remote(selections, pending_selection, cx);
|
|
||||||
this.request_autoscroll_remotely(Autoscroll::newest(), cx);
|
|
||||||
} else if let Some(anchor) = scroll_top_anchor {
|
|
||||||
this.set_scroll_anchor_remote(ScrollAnchor {
|
|
||||||
top_anchor: anchor,
|
|
||||||
offset: vec2f(message.scroll_x, message.scroll_y)
|
|
||||||
}, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -402,6 +298,128 @@ impl FollowableItem for Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_editor_from_message(
|
||||||
|
this: ViewHandle<Editor>,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
message: proto::update_view::Editor,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Open all of the buffers of which excerpts were added to the editor.
|
||||||
|
let inserted_excerpt_buffer_ids = message
|
||||||
|
.inserted_excerpts
|
||||||
|
.iter()
|
||||||
|
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
let inserted_excerpt_buffers = project.update(cx, |project, cx| {
|
||||||
|
inserted_excerpt_buffer_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|id| project.open_buffer_by_id(id, cx))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
|
||||||
|
|
||||||
|
// Update the editor's excerpts.
|
||||||
|
this.update(cx, |editor, cx| {
|
||||||
|
editor.buffer.update(cx, |multibuffer, cx| {
|
||||||
|
let mut removed_excerpt_ids = message
|
||||||
|
.deleted_excerpts
|
||||||
|
.into_iter()
|
||||||
|
.map(ExcerptId::from_proto)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
removed_excerpt_ids.sort_by({
|
||||||
|
let multibuffer = multibuffer.read(cx);
|
||||||
|
move |a, b| a.cmp(&b, &multibuffer)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut insertions = message.inserted_excerpts.into_iter().peekable();
|
||||||
|
while let Some(insertion) = insertions.next() {
|
||||||
|
let Some(excerpt) = insertion.excerpt else { continue };
|
||||||
|
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
|
||||||
|
let buffer_id = excerpt.buffer_id;
|
||||||
|
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
|
||||||
|
|
||||||
|
let adjacent_excerpts = iter::from_fn(|| {
|
||||||
|
let insertion = insertions.peek()?;
|
||||||
|
if insertion.previous_excerpt_id.is_none()
|
||||||
|
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
|
||||||
|
{
|
||||||
|
insertions.next()?.excerpt
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
multibuffer.insert_excerpts_with_ids_after(
|
||||||
|
ExcerptId::from_proto(previous_excerpt_id),
|
||||||
|
buffer,
|
||||||
|
[excerpt]
|
||||||
|
.into_iter()
|
||||||
|
.chain(adjacent_excerpts)
|
||||||
|
.filter_map(|excerpt| {
|
||||||
|
Some((
|
||||||
|
ExcerptId::from_proto(excerpt.id),
|
||||||
|
deserialize_excerpt_range(excerpt)?,
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
multibuffer.remove_excerpts(removed_excerpt_ids, cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deserialize the editor state.
|
||||||
|
let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
|
||||||
|
let buffer = editor.buffer.read(cx).read(cx);
|
||||||
|
let selections = message
|
||||||
|
.selections
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|selection| deserialize_selection(&buffer, selection))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let pending_selection = message
|
||||||
|
.pending_selection
|
||||||
|
.and_then(|selection| deserialize_selection(&buffer, selection));
|
||||||
|
let scroll_top_anchor = message
|
||||||
|
.scroll_top_anchor
|
||||||
|
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
|
||||||
|
anyhow::Ok((selections, pending_selection, scroll_top_anchor))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Wait until the buffer has received all of the operations referenced by
|
||||||
|
// the editor's new state.
|
||||||
|
this.update(cx, |editor, cx| {
|
||||||
|
editor.buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.wait_for_anchors(
|
||||||
|
selections
|
||||||
|
.iter()
|
||||||
|
.chain(pending_selection.as_ref())
|
||||||
|
.flat_map(|selection| [selection.start, selection.end])
|
||||||
|
.chain(scroll_top_anchor),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Update the editor's state.
|
||||||
|
this.update(cx, |editor, cx| {
|
||||||
|
if !selections.is_empty() || pending_selection.is_some() {
|
||||||
|
editor.set_selections_from_remote(selections, pending_selection, cx);
|
||||||
|
editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
|
||||||
|
} else if let Some(scroll_top_anchor) = scroll_top_anchor {
|
||||||
|
editor.set_scroll_anchor_remote(
|
||||||
|
ScrollAnchor {
|
||||||
|
top_anchor: scroll_top_anchor,
|
||||||
|
offset: vec2f(message.scroll_x, message.scroll_y),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn serialize_excerpt(
|
fn serialize_excerpt(
|
||||||
buffer_id: u64,
|
buffer_id: u64,
|
||||||
id: &ExcerptId,
|
id: &ExcerptId,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
mod anchor;
|
mod anchor;
|
||||||
|
|
||||||
pub use anchor::{Anchor, AnchorRangeExt};
|
pub use anchor::{Anchor, AnchorRangeExt};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
||||||
use futures::{channel::mpsc, SinkExt};
|
use futures::{channel::mpsc, SinkExt};
|
||||||
|
@ -16,7 +17,9 @@ use language::{
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
cell::{Ref, RefCell},
|
cell::{Ref, RefCell},
|
||||||
cmp, fmt, io,
|
cmp, fmt,
|
||||||
|
future::Future,
|
||||||
|
io,
|
||||||
iter::{self, FromIterator},
|
iter::{self, FromIterator},
|
||||||
mem,
|
mem,
|
||||||
ops::{Range, RangeBounds, Sub},
|
ops::{Range, RangeBounds, Sub},
|
||||||
|
@ -1238,6 +1241,39 @@ impl MultiBuffer {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn wait_for_anchors<'a>(
|
||||||
|
&self,
|
||||||
|
anchors: impl 'a + Iterator<Item = Anchor>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> impl 'static + Future<Output = Result<()>> {
|
||||||
|
let borrow = self.buffers.borrow();
|
||||||
|
let mut error = None;
|
||||||
|
let mut futures = Vec::new();
|
||||||
|
for anchor in anchors {
|
||||||
|
if let Some(buffer_id) = anchor.buffer_id {
|
||||||
|
if let Some(buffer) = borrow.get(&buffer_id) {
|
||||||
|
buffer.buffer.update(cx, |buffer, _| {
|
||||||
|
futures.push(buffer.wait_for_anchors([anchor.text_anchor]))
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
error = Some(anyhow!(
|
||||||
|
"buffer {buffer_id} is not part of this multi-buffer"
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async move {
|
||||||
|
if let Some(error) = error {
|
||||||
|
Err(error)?;
|
||||||
|
}
|
||||||
|
for future in futures {
|
||||||
|
future.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn text_anchor_for_position<T: ToOffset>(
|
pub fn text_anchor_for_position<T: ToOffset>(
|
||||||
&self,
|
&self,
|
||||||
position: T,
|
position: T,
|
||||||
|
|
|
@ -523,31 +523,7 @@ impl FakeFs {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
|
pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
|
||||||
let mut state = self.state.lock();
|
self.write_file_internal(path, content).unwrap()
|
||||||
let path = path.as_ref();
|
|
||||||
let inode = state.next_inode;
|
|
||||||
let mtime = state.next_mtime;
|
|
||||||
state.next_inode += 1;
|
|
||||||
state.next_mtime += Duration::from_nanos(1);
|
|
||||||
let file = Arc::new(Mutex::new(FakeFsEntry::File {
|
|
||||||
inode,
|
|
||||||
mtime,
|
|
||||||
content,
|
|
||||||
}));
|
|
||||||
state
|
|
||||||
.write_path(path, move |entry| {
|
|
||||||
match entry {
|
|
||||||
btree_map::Entry::Vacant(e) => {
|
|
||||||
e.insert(file);
|
|
||||||
}
|
|
||||||
btree_map::Entry::Occupied(mut e) => {
|
|
||||||
*e.get_mut() = file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
state.emit_event(&[path]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
|
pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
|
||||||
|
@ -569,6 +545,33 @@ impl FakeFs {
|
||||||
state.emit_event(&[path]);
|
state.emit_event(&[path]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
|
||||||
|
let mut state = self.state.lock();
|
||||||
|
let path = path.as_ref();
|
||||||
|
let inode = state.next_inode;
|
||||||
|
let mtime = state.next_mtime;
|
||||||
|
state.next_inode += 1;
|
||||||
|
state.next_mtime += Duration::from_nanos(1);
|
||||||
|
let file = Arc::new(Mutex::new(FakeFsEntry::File {
|
||||||
|
inode,
|
||||||
|
mtime,
|
||||||
|
content,
|
||||||
|
}));
|
||||||
|
state.write_path(path, move |entry| {
|
||||||
|
match entry {
|
||||||
|
btree_map::Entry::Vacant(e) => {
|
||||||
|
e.insert(file);
|
||||||
|
}
|
||||||
|
btree_map::Entry::Occupied(mut e) => {
|
||||||
|
*e.get_mut() = file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
state.emit_event(&[path]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn pause_events(&self) {
|
pub async fn pause_events(&self) {
|
||||||
self.state.lock().events_paused = true;
|
self.state.lock().events_paused = true;
|
||||||
}
|
}
|
||||||
|
@ -952,7 +955,7 @@ impl Fs for FakeFs {
|
||||||
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
|
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
|
||||||
self.simulate_random_delay().await;
|
self.simulate_random_delay().await;
|
||||||
let path = normalize_path(path.as_path());
|
let path = normalize_path(path.as_path());
|
||||||
self.insert_file(path, data.to_string()).await;
|
self.write_file_internal(path, data.to_string())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -961,7 +964,7 @@ impl Fs for FakeFs {
|
||||||
self.simulate_random_delay().await;
|
self.simulate_random_delay().await;
|
||||||
let path = normalize_path(path);
|
let path = normalize_path(path);
|
||||||
let content = chunks(text, line_ending).collect();
|
let content = chunks(text, line_ending).collect();
|
||||||
self.insert_file(path, content).await;
|
self.write_file_internal(path, content)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1313,10 +1313,10 @@ impl Buffer {
|
||||||
self.text.wait_for_edits(edit_ids)
|
self.text.wait_for_edits(edit_ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wait_for_anchors<'a>(
|
pub fn wait_for_anchors(
|
||||||
&mut self,
|
&mut self,
|
||||||
anchors: impl IntoIterator<Item = &'a Anchor>,
|
anchors: impl IntoIterator<Item = Anchor>,
|
||||||
) -> impl Future<Output = Result<()>> {
|
) -> impl 'static + Future<Output = Result<()>> {
|
||||||
self.text.wait_for_anchors(anchors)
|
self.text.wait_for_anchors(anchors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -572,7 +572,7 @@ async fn location_links_from_proto(
|
||||||
.and_then(deserialize_anchor)
|
.and_then(deserialize_anchor)
|
||||||
.ok_or_else(|| anyhow!("missing origin end"))?;
|
.ok_or_else(|| anyhow!("missing origin end"))?;
|
||||||
buffer
|
buffer
|
||||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
|
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
|
||||||
.await?;
|
.await?;
|
||||||
Some(Location {
|
Some(Location {
|
||||||
buffer,
|
buffer,
|
||||||
|
@ -597,7 +597,7 @@ async fn location_links_from_proto(
|
||||||
.and_then(deserialize_anchor)
|
.and_then(deserialize_anchor)
|
||||||
.ok_or_else(|| anyhow!("missing target end"))?;
|
.ok_or_else(|| anyhow!("missing target end"))?;
|
||||||
buffer
|
buffer
|
||||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
|
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
|
||||||
.await?;
|
.await?;
|
||||||
let target = Location {
|
let target = Location {
|
||||||
buffer,
|
buffer,
|
||||||
|
@ -868,7 +868,7 @@ impl LspCommand for GetReferences {
|
||||||
.and_then(deserialize_anchor)
|
.and_then(deserialize_anchor)
|
||||||
.ok_or_else(|| anyhow!("missing target end"))?;
|
.ok_or_else(|| anyhow!("missing target end"))?;
|
||||||
target_buffer
|
target_buffer
|
||||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
|
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
|
||||||
.await?;
|
.await?;
|
||||||
locations.push(Location {
|
locations.push(Location {
|
||||||
buffer: target_buffer,
|
buffer: target_buffer,
|
||||||
|
@ -1012,7 +1012,7 @@ impl LspCommand for GetDocumentHighlights {
|
||||||
.and_then(deserialize_anchor)
|
.and_then(deserialize_anchor)
|
||||||
.ok_or_else(|| anyhow!("missing target end"))?;
|
.ok_or_else(|| anyhow!("missing target end"))?;
|
||||||
buffer
|
buffer
|
||||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
|
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
|
||||||
.await?;
|
.await?;
|
||||||
let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) {
|
let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) {
|
||||||
Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT,
|
Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT,
|
||||||
|
|
|
@ -92,7 +92,7 @@ pub trait Item {
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
worktrees: Vec<WorktreeHandle>,
|
worktrees: Vec<WorktreeHandle>,
|
||||||
active_entry: Option<ProjectEntryId>,
|
active_entry: Option<ProjectEntryId>,
|
||||||
buffer_changes_tx: mpsc::UnboundedSender<BufferMessage>,
|
buffer_ordered_messages_tx: mpsc::UnboundedSender<BufferOrderedMessage>,
|
||||||
languages: Arc<LanguageRegistry>,
|
languages: Arc<LanguageRegistry>,
|
||||||
language_servers: HashMap<usize, LanguageServerState>,
|
language_servers: HashMap<usize, LanguageServerState>,
|
||||||
language_server_ids: HashMap<(WorktreeId, LanguageServerName), usize>,
|
language_server_ids: HashMap<(WorktreeId, LanguageServerName), usize>,
|
||||||
|
@ -131,11 +131,16 @@ pub struct Project {
|
||||||
terminals: Terminals,
|
terminals: Terminals,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BufferMessage {
|
/// Message ordered with respect to buffer operations
|
||||||
|
enum BufferOrderedMessage {
|
||||||
Operation {
|
Operation {
|
||||||
buffer_id: u64,
|
buffer_id: u64,
|
||||||
operation: proto::Operation,
|
operation: proto::Operation,
|
||||||
},
|
},
|
||||||
|
LanguageServerUpdate {
|
||||||
|
language_server_id: usize,
|
||||||
|
message: proto::update_language_server::Variant,
|
||||||
|
},
|
||||||
Resync,
|
Resync,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -436,11 +441,11 @@ impl Project {
|
||||||
) -> ModelHandle<Self> {
|
) -> ModelHandle<Self> {
|
||||||
cx.add_model(|cx: &mut ModelContext<Self>| {
|
cx.add_model(|cx: &mut ModelContext<Self>| {
|
||||||
let (tx, rx) = mpsc::unbounded();
|
let (tx, rx) = mpsc::unbounded();
|
||||||
cx.spawn_weak(|this, cx| Self::send_buffer_messages(this, rx, cx))
|
cx.spawn_weak(|this, cx| Self::send_buffer_ordered_messages(this, rx, cx))
|
||||||
.detach();
|
.detach();
|
||||||
Self {
|
Self {
|
||||||
worktrees: Default::default(),
|
worktrees: Default::default(),
|
||||||
buffer_changes_tx: tx,
|
buffer_ordered_messages_tx: tx,
|
||||||
collaborators: Default::default(),
|
collaborators: Default::default(),
|
||||||
opened_buffers: Default::default(),
|
opened_buffers: Default::default(),
|
||||||
shared_buffers: Default::default(),
|
shared_buffers: Default::default(),
|
||||||
|
@ -504,11 +509,11 @@ impl Project {
|
||||||
}
|
}
|
||||||
|
|
||||||
let (tx, rx) = mpsc::unbounded();
|
let (tx, rx) = mpsc::unbounded();
|
||||||
cx.spawn_weak(|this, cx| Self::send_buffer_messages(this, rx, cx))
|
cx.spawn_weak(|this, cx| Self::send_buffer_ordered_messages(this, rx, cx))
|
||||||
.detach();
|
.detach();
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
worktrees: Vec::new(),
|
worktrees: Vec::new(),
|
||||||
buffer_changes_tx: tx,
|
buffer_ordered_messages_tx: tx,
|
||||||
loading_buffers_by_path: Default::default(),
|
loading_buffers_by_path: Default::default(),
|
||||||
opened_buffer: watch::channel(),
|
opened_buffer: watch::channel(),
|
||||||
shared_buffers: Default::default(),
|
shared_buffers: Default::default(),
|
||||||
|
@ -1152,8 +1157,8 @@ impl Project {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
self.buffer_changes_tx
|
self.buffer_ordered_messages_tx
|
||||||
.unbounded_send(BufferMessage::Resync)
|
.unbounded_send(BufferOrderedMessage::Resync)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1731,38 +1736,64 @@ impl Project {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_buffer_messages(
|
async fn send_buffer_ordered_messages(
|
||||||
this: WeakModelHandle<Self>,
|
this: WeakModelHandle<Self>,
|
||||||
mut rx: UnboundedReceiver<BufferMessage>,
|
rx: UnboundedReceiver<BufferOrderedMessage>,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) {
|
) -> Option<()> {
|
||||||
|
const MAX_BATCH_SIZE: usize = 128;
|
||||||
|
|
||||||
|
let mut operations_by_buffer_id = HashMap::default();
|
||||||
|
async fn flush_operations(
|
||||||
|
this: &ModelHandle<Project>,
|
||||||
|
operations_by_buffer_id: &mut HashMap<u64, Vec<proto::Operation>>,
|
||||||
|
needs_resync_with_host: &mut bool,
|
||||||
|
is_local: bool,
|
||||||
|
cx: &AsyncAppContext,
|
||||||
|
) {
|
||||||
|
for (buffer_id, operations) in operations_by_buffer_id.drain() {
|
||||||
|
let request = this.read_with(cx, |this, _| {
|
||||||
|
let project_id = this.remote_id()?;
|
||||||
|
Some(this.client.request(proto::UpdateBuffer {
|
||||||
|
buffer_id,
|
||||||
|
project_id,
|
||||||
|
operations,
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
if let Some(request) = request {
|
||||||
|
if request.await.is_err() && !is_local {
|
||||||
|
*needs_resync_with_host = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut needs_resync_with_host = false;
|
let mut needs_resync_with_host = false;
|
||||||
while let Some(change) = rx.next().await {
|
let mut changes = rx.ready_chunks(MAX_BATCH_SIZE);
|
||||||
if let Some(this) = this.upgrade(&mut cx) {
|
|
||||||
let is_local = this.read_with(&cx, |this, _| this.is_local());
|
while let Some(changes) = changes.next().await {
|
||||||
|
let this = this.upgrade(&mut cx)?;
|
||||||
|
let is_local = this.read_with(&cx, |this, _| this.is_local());
|
||||||
|
|
||||||
|
for change in changes {
|
||||||
match change {
|
match change {
|
||||||
BufferMessage::Operation {
|
BufferOrderedMessage::Operation {
|
||||||
buffer_id,
|
buffer_id,
|
||||||
operation,
|
operation,
|
||||||
} => {
|
} => {
|
||||||
if needs_resync_with_host {
|
if needs_resync_with_host {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let request = this.read_with(&cx, |this, _| {
|
|
||||||
let project_id = this.remote_id()?;
|
operations_by_buffer_id
|
||||||
Some(this.client.request(proto::UpdateBuffer {
|
.entry(buffer_id)
|
||||||
buffer_id,
|
.or_insert(Vec::new())
|
||||||
project_id,
|
.push(operation);
|
||||||
operations: vec![operation],
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
if let Some(request) = request {
|
|
||||||
if request.await.is_err() && !is_local {
|
|
||||||
needs_resync_with_host = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
BufferMessage::Resync => {
|
|
||||||
|
BufferOrderedMessage::Resync => {
|
||||||
|
operations_by_buffer_id.clear();
|
||||||
if this
|
if this
|
||||||
.update(&mut cx, |this, cx| this.synchronize_remote_buffers(cx))
|
.update(&mut cx, |this, cx| this.synchronize_remote_buffers(cx))
|
||||||
.await
|
.await
|
||||||
|
@ -1771,11 +1802,46 @@ impl Project {
|
||||||
needs_resync_with_host = false;
|
needs_resync_with_host = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BufferOrderedMessage::LanguageServerUpdate {
|
||||||
|
language_server_id,
|
||||||
|
message,
|
||||||
|
} => {
|
||||||
|
flush_operations(
|
||||||
|
&this,
|
||||||
|
&mut operations_by_buffer_id,
|
||||||
|
&mut needs_resync_with_host,
|
||||||
|
is_local,
|
||||||
|
&cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
this.read_with(&cx, |this, _| {
|
||||||
|
if let Some(project_id) = this.remote_id() {
|
||||||
|
this.client
|
||||||
|
.send(proto::UpdateLanguageServer {
|
||||||
|
project_id,
|
||||||
|
language_server_id: language_server_id as u64,
|
||||||
|
variant: Some(message),
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flush_operations(
|
||||||
|
&this,
|
||||||
|
&mut operations_by_buffer_id,
|
||||||
|
&mut needs_resync_with_host,
|
||||||
|
is_local,
|
||||||
|
&cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_buffer_event(
|
fn on_buffer_event(
|
||||||
|
@ -1786,8 +1852,8 @@ impl Project {
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
match event {
|
match event {
|
||||||
BufferEvent::Operation(operation) => {
|
BufferEvent::Operation(operation) => {
|
||||||
self.buffer_changes_tx
|
self.buffer_ordered_messages_tx
|
||||||
.unbounded_send(BufferMessage::Operation {
|
.unbounded_send(BufferOrderedMessage::Operation {
|
||||||
buffer_id: buffer.read(cx).remote_id(),
|
buffer_id: buffer.read(cx).remote_id(),
|
||||||
operation: language::proto::serialize_operation(operation),
|
operation: language::proto::serialize_operation(operation),
|
||||||
})
|
})
|
||||||
|
@ -1878,14 +1944,19 @@ impl Project {
|
||||||
let task = cx.spawn_weak(|this, mut cx| async move {
|
let task = cx.spawn_weak(|this, mut cx| async move {
|
||||||
cx.background().timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE).await;
|
cx.background().timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE).await;
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
this.update(&mut cx, |this, cx | {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.disk_based_diagnostics_finished(language_server_id, cx);
|
this.disk_based_diagnostics_finished(
|
||||||
this.broadcast_language_server_update(
|
|
||||||
language_server_id,
|
language_server_id,
|
||||||
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
|
cx,
|
||||||
proto::LspDiskBasedDiagnosticsUpdated {},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
this.buffer_ordered_messages_tx
|
||||||
|
.unbounded_send(
|
||||||
|
BufferOrderedMessage::LanguageServerUpdate {
|
||||||
|
language_server_id,
|
||||||
|
message:proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(Default::default())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -2524,12 +2595,12 @@ impl Project {
|
||||||
if is_disk_based_diagnostics_progress {
|
if is_disk_based_diagnostics_progress {
|
||||||
language_server_status.has_pending_diagnostic_updates = true;
|
language_server_status.has_pending_diagnostic_updates = true;
|
||||||
self.disk_based_diagnostics_started(server_id, cx);
|
self.disk_based_diagnostics_started(server_id, cx);
|
||||||
self.broadcast_language_server_update(
|
self.buffer_ordered_messages_tx
|
||||||
server_id,
|
.unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
|
||||||
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
|
language_server_id: server_id,
|
||||||
proto::LspDiskBasedDiagnosticsUpdating {},
|
message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(Default::default())
|
||||||
),
|
})
|
||||||
);
|
.ok();
|
||||||
} else {
|
} else {
|
||||||
self.on_lsp_work_start(
|
self.on_lsp_work_start(
|
||||||
server_id,
|
server_id,
|
||||||
|
@ -2541,14 +2612,18 @@ impl Project {
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
self.broadcast_language_server_update(
|
self.buffer_ordered_messages_tx
|
||||||
server_id,
|
.unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
|
||||||
proto::update_language_server::Variant::WorkStart(proto::LspWorkStart {
|
language_server_id: server_id,
|
||||||
token,
|
message: proto::update_language_server::Variant::WorkStart(
|
||||||
message: report.message,
|
proto::LspWorkStart {
|
||||||
percentage: report.percentage.map(|p| p as u32),
|
token,
|
||||||
}),
|
message: report.message,
|
||||||
);
|
percentage: report.percentage.map(|p| p as u32),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lsp::WorkDoneProgress::Report(report) => {
|
lsp::WorkDoneProgress::Report(report) => {
|
||||||
|
@ -2563,16 +2638,18 @@ impl Project {
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
self.broadcast_language_server_update(
|
self.buffer_ordered_messages_tx
|
||||||
server_id,
|
.unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
|
||||||
proto::update_language_server::Variant::WorkProgress(
|
language_server_id: server_id,
|
||||||
proto::LspWorkProgress {
|
message: proto::update_language_server::Variant::WorkProgress(
|
||||||
token,
|
proto::LspWorkProgress {
|
||||||
message: report.message,
|
token,
|
||||||
percentage: report.percentage.map(|p| p as u32),
|
message: report.message,
|
||||||
},
|
percentage: report.percentage.map(|p| p as u32),
|
||||||
),
|
},
|
||||||
);
|
),
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lsp::WorkDoneProgress::End(_) => {
|
lsp::WorkDoneProgress::End(_) => {
|
||||||
|
@ -2581,20 +2658,25 @@ impl Project {
|
||||||
if is_disk_based_diagnostics_progress {
|
if is_disk_based_diagnostics_progress {
|
||||||
language_server_status.has_pending_diagnostic_updates = false;
|
language_server_status.has_pending_diagnostic_updates = false;
|
||||||
self.disk_based_diagnostics_finished(server_id, cx);
|
self.disk_based_diagnostics_finished(server_id, cx);
|
||||||
self.broadcast_language_server_update(
|
self.buffer_ordered_messages_tx
|
||||||
server_id,
|
.unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
|
||||||
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
|
language_server_id: server_id,
|
||||||
proto::LspDiskBasedDiagnosticsUpdated {},
|
message:
|
||||||
),
|
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
|
||||||
);
|
Default::default(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
} else {
|
} else {
|
||||||
self.on_lsp_work_end(server_id, token.clone(), cx);
|
self.on_lsp_work_end(server_id, token.clone(), cx);
|
||||||
self.broadcast_language_server_update(
|
self.buffer_ordered_messages_tx
|
||||||
server_id,
|
.unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
|
||||||
proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd {
|
language_server_id: server_id,
|
||||||
token,
|
message: proto::update_language_server::Variant::WorkEnd(
|
||||||
}),
|
proto::LspWorkEnd { token },
|
||||||
);
|
),
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2703,22 +2785,6 @@ impl Project {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn broadcast_language_server_update(
|
|
||||||
&self,
|
|
||||||
language_server_id: usize,
|
|
||||||
event: proto::update_language_server::Variant,
|
|
||||||
) {
|
|
||||||
if let Some(project_id) = self.remote_id() {
|
|
||||||
self.client
|
|
||||||
.send(proto::UpdateLanguageServer {
|
|
||||||
project_id,
|
|
||||||
language_server_id: language_server_id as u64,
|
|
||||||
variant: Some(event),
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn language_server_statuses(
|
pub fn language_server_statuses(
|
||||||
&self,
|
&self,
|
||||||
) -> impl DoubleEndedIterator<Item = &LanguageServerStatus> {
|
) -> impl DoubleEndedIterator<Item = &LanguageServerStatus> {
|
||||||
|
@ -4727,8 +4793,8 @@ impl Project {
|
||||||
if is_host {
|
if is_host {
|
||||||
this.opened_buffers
|
this.opened_buffers
|
||||||
.retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_)));
|
.retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_)));
|
||||||
this.buffer_changes_tx
|
this.buffer_ordered_messages_tx
|
||||||
.unbounded_send(BufferMessage::Resync)
|
.unbounded_send(BufferOrderedMessage::Resync)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2183,7 +2183,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_save_file(cx: &mut gpui::TestAppContext) {
|
async fn test_save_file(cx: &mut gpui::TestAppContext) {
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -28,11 +28,11 @@ pub use watched_json::watch_files;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
|
pub features: Features,
|
||||||
pub buffer_font_family_name: String,
|
pub buffer_font_family_name: String,
|
||||||
pub buffer_font_features: fonts::Features,
|
pub buffer_font_features: fonts::Features,
|
||||||
pub buffer_font_family: FamilyId,
|
pub buffer_font_family: FamilyId,
|
||||||
pub default_buffer_font_size: f32,
|
pub default_buffer_font_size: f32,
|
||||||
pub enable_copilot_integration: bool,
|
|
||||||
pub buffer_font_size: f32,
|
pub buffer_font_size: f32,
|
||||||
pub active_pane_magnification: f32,
|
pub active_pane_magnification: f32,
|
||||||
pub cursor_blink: bool,
|
pub cursor_blink: bool,
|
||||||
|
@ -177,43 +177,7 @@ pub struct EditorSettings {
|
||||||
pub ensure_final_newline_on_save: Option<bool>,
|
pub ensure_final_newline_on_save: Option<bool>,
|
||||||
pub formatter: Option<Formatter>,
|
pub formatter: Option<Formatter>,
|
||||||
pub enable_language_server: Option<bool>,
|
pub enable_language_server: Option<bool>,
|
||||||
#[schemars(skip)]
|
pub show_copilot_suggestions: Option<bool>,
|
||||||
pub copilot: Option<OnOff>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum OnOff {
|
|
||||||
On,
|
|
||||||
Off,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OnOff {
|
|
||||||
pub fn as_bool(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
OnOff::On => true,
|
|
||||||
OnOff::Off => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_bool(value: bool) -> OnOff {
|
|
||||||
match value {
|
|
||||||
true => OnOff::On,
|
|
||||||
false => OnOff::Off,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<OnOff> for bool {
|
|
||||||
fn from(value: OnOff) -> bool {
|
|
||||||
value.as_bool()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<bool> for OnOff {
|
|
||||||
fn from(value: bool) -> OnOff {
|
|
||||||
OnOff::from_bool(value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
@ -437,8 +401,7 @@ pub struct SettingsFileContent {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub base_keymap: Option<BaseKeymap>,
|
pub base_keymap: Option<BaseKeymap>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[schemars(skip)]
|
pub features: FeaturesContent,
|
||||||
pub enable_copilot_integration: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
@ -447,6 +410,18 @@ pub struct LspSettings {
|
||||||
pub initialization_options: Option<Value>,
|
pub initialization_options: Option<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub struct Features {
|
||||||
|
pub copilot: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub struct FeaturesContent {
|
||||||
|
pub copilot: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
/// Fill out the settings corresponding to the default.json file, overrides will be set later
|
/// Fill out the settings corresponding to the default.json file, overrides will be set later
|
||||||
pub fn defaults(
|
pub fn defaults(
|
||||||
|
@ -500,7 +475,7 @@ impl Settings {
|
||||||
format_on_save: required(defaults.editor.format_on_save),
|
format_on_save: required(defaults.editor.format_on_save),
|
||||||
formatter: required(defaults.editor.formatter),
|
formatter: required(defaults.editor.formatter),
|
||||||
enable_language_server: required(defaults.editor.enable_language_server),
|
enable_language_server: required(defaults.editor.enable_language_server),
|
||||||
copilot: required(defaults.editor.copilot),
|
show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions),
|
||||||
},
|
},
|
||||||
editor_overrides: Default::default(),
|
editor_overrides: Default::default(),
|
||||||
git: defaults.git.unwrap(),
|
git: defaults.git.unwrap(),
|
||||||
|
@ -517,7 +492,9 @@ impl Settings {
|
||||||
telemetry_overrides: Default::default(),
|
telemetry_overrides: Default::default(),
|
||||||
auto_update: defaults.auto_update.unwrap(),
|
auto_update: defaults.auto_update.unwrap(),
|
||||||
base_keymap: Default::default(),
|
base_keymap: Default::default(),
|
||||||
enable_copilot_integration: defaults.enable_copilot_integration.unwrap(),
|
features: Features {
|
||||||
|
copilot: defaults.features.copilot.unwrap(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -569,10 +546,7 @@ impl Settings {
|
||||||
merge(&mut self.autosave, data.autosave);
|
merge(&mut self.autosave, data.autosave);
|
||||||
merge(&mut self.default_dock_anchor, data.default_dock_anchor);
|
merge(&mut self.default_dock_anchor, data.default_dock_anchor);
|
||||||
merge(&mut self.base_keymap, data.base_keymap);
|
merge(&mut self.base_keymap, data.base_keymap);
|
||||||
merge(
|
merge(&mut self.features.copilot, data.features.copilot);
|
||||||
&mut self.enable_copilot_integration,
|
|
||||||
data.enable_copilot_integration,
|
|
||||||
);
|
|
||||||
|
|
||||||
self.editor_overrides = data.editor;
|
self.editor_overrides = data.editor;
|
||||||
self.git_overrides = data.git.unwrap_or_default();
|
self.git_overrides = data.git.unwrap_or_default();
|
||||||
|
@ -596,12 +570,15 @@ impl Settings {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn copilot_on(&self, language: Option<&str>) -> bool {
|
pub fn features(&self) -> &Features {
|
||||||
if self.enable_copilot_integration {
|
&self.features
|
||||||
self.language_setting(language, |settings| settings.copilot.map(Into::into))
|
}
|
||||||
} else {
|
|
||||||
false
|
pub fn show_copilot_suggestions(&self, language: Option<&str>) -> bool {
|
||||||
}
|
self.features.copilot
|
||||||
|
&& self.language_setting(language, |settings| {
|
||||||
|
settings.show_copilot_suggestions.map(Into::into)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
|
pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
|
||||||
|
@ -740,7 +717,7 @@ impl Settings {
|
||||||
format_on_save: Some(FormatOnSave::On),
|
format_on_save: Some(FormatOnSave::On),
|
||||||
formatter: Some(Formatter::LanguageServer),
|
formatter: Some(Formatter::LanguageServer),
|
||||||
enable_language_server: Some(true),
|
enable_language_server: Some(true),
|
||||||
copilot: Some(OnOff::On),
|
show_copilot_suggestions: Some(true),
|
||||||
},
|
},
|
||||||
editor_overrides: Default::default(),
|
editor_overrides: Default::default(),
|
||||||
journal_defaults: Default::default(),
|
journal_defaults: Default::default(),
|
||||||
|
@ -760,7 +737,7 @@ impl Settings {
|
||||||
telemetry_overrides: Default::default(),
|
telemetry_overrides: Default::default(),
|
||||||
auto_update: true,
|
auto_update: true,
|
||||||
base_keymap: Default::default(),
|
base_keymap: Default::default(),
|
||||||
enable_copilot_integration: true,
|
features: Features { copilot: true },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1125,7 +1102,7 @@ mod tests {
|
||||||
{
|
{
|
||||||
"language_overrides": {
|
"language_overrides": {
|
||||||
"JSON": {
|
"JSON": {
|
||||||
"copilot": "off"
|
"show_copilot_suggestions": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1135,7 +1112,7 @@ mod tests {
|
||||||
settings.languages.insert(
|
settings.languages.insert(
|
||||||
"Rust".into(),
|
"Rust".into(),
|
||||||
EditorSettings {
|
EditorSettings {
|
||||||
copilot: Some(OnOff::On),
|
show_copilot_suggestions: Some(true),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1144,10 +1121,10 @@ mod tests {
|
||||||
{
|
{
|
||||||
"language_overrides": {
|
"language_overrides": {
|
||||||
"Rust": {
|
"Rust": {
|
||||||
"copilot": "on"
|
"show_copilot_suggestions": true
|
||||||
},
|
},
|
||||||
"JSON": {
|
"JSON": {
|
||||||
"copilot": "off"
|
"show_copilot_suggestions": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1163,21 +1140,21 @@ mod tests {
|
||||||
{
|
{
|
||||||
"languages": {
|
"languages": {
|
||||||
"JSON": {
|
"JSON": {
|
||||||
"copilot": "off"
|
"show_copilot_suggestions": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"#
|
"#
|
||||||
.unindent(),
|
.unindent(),
|
||||||
|settings| {
|
|settings| {
|
||||||
settings.editor.copilot = Some(OnOff::On);
|
settings.editor.show_copilot_suggestions = Some(true);
|
||||||
},
|
},
|
||||||
r#"
|
r#"
|
||||||
{
|
{
|
||||||
"copilot": "on",
|
"show_copilot_suggestions": true,
|
||||||
"languages": {
|
"languages": {
|
||||||
"JSON": {
|
"JSON": {
|
||||||
"copilot": "off"
|
"show_copilot_suggestions": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1187,13 +1164,13 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_update_langauge_copilot() {
|
fn test_update_language_copilot() {
|
||||||
assert_new_settings(
|
assert_new_settings(
|
||||||
r#"
|
r#"
|
||||||
{
|
{
|
||||||
"languages": {
|
"languages": {
|
||||||
"JSON": {
|
"JSON": {
|
||||||
"copilot": "off"
|
"show_copilot_suggestions": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1203,7 +1180,7 @@ mod tests {
|
||||||
settings.languages.insert(
|
settings.languages.insert(
|
||||||
"Rust".into(),
|
"Rust".into(),
|
||||||
EditorSettings {
|
EditorSettings {
|
||||||
copilot: Some(OnOff::On),
|
show_copilot_suggestions: Some(true),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1212,10 +1189,10 @@ mod tests {
|
||||||
{
|
{
|
||||||
"languages": {
|
"languages": {
|
||||||
"Rust": {
|
"Rust": {
|
||||||
"copilot": "on"
|
"show_copilot_suggestions": true
|
||||||
},
|
},
|
||||||
"JSON": {
|
"JSON": {
|
||||||
"copilot": "off"
|
"show_copilot_suggestions": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,6 +154,12 @@ impl<K> TreeSet<K>
|
||||||
where
|
where
|
||||||
K: Clone + Debug + Default + Ord,
|
K: Clone + Debug + Default + Ord,
|
||||||
{
|
{
|
||||||
|
pub fn from_ordered_entries(entries: impl IntoIterator<Item = K>) -> Self {
|
||||||
|
Self(TreeMap::from_ordered_entries(
|
||||||
|
entries.into_iter().map(|key| (key, ())),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, key: K) {
|
pub fn insert(&mut self, key: K) {
|
||||||
self.0.insert(key, ());
|
self.0.insert(key, ());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1331,15 +1331,15 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wait_for_anchors<'a>(
|
pub fn wait_for_anchors(
|
||||||
&mut self,
|
&mut self,
|
||||||
anchors: impl IntoIterator<Item = &'a Anchor>,
|
anchors: impl IntoIterator<Item = Anchor>,
|
||||||
) -> impl 'static + Future<Output = Result<()>> {
|
) -> impl 'static + Future<Output = Result<()>> {
|
||||||
let mut futures = Vec::new();
|
let mut futures = Vec::new();
|
||||||
for anchor in anchors {
|
for anchor in anchors {
|
||||||
if !self.version.observed(anchor.timestamp)
|
if !self.version.observed(anchor.timestamp)
|
||||||
&& *anchor != Anchor::MAX
|
&& anchor != Anchor::MAX
|
||||||
&& *anchor != Anchor::MIN
|
&& anchor != Anchor::MIN
|
||||||
{
|
{
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
self.edit_id_resolvers
|
self.edit_id_resolvers
|
||||||
|
|
|
@ -785,6 +785,10 @@ impl Pane {
|
||||||
) -> Option<Task<Result<()>>> {
|
) -> Option<Task<Result<()>>> {
|
||||||
let pane_handle = workspace.active_pane().clone();
|
let pane_handle = workspace.active_pane().clone();
|
||||||
let pane = pane_handle.read(cx);
|
let pane = pane_handle.read(cx);
|
||||||
|
|
||||||
|
if pane.items.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let active_item_id = pane.items[pane.active_item_index].id();
|
let active_item_id = pane.items[pane.active_item_index].id();
|
||||||
|
|
||||||
let task = Self::close_item_by_id(workspace, pane_handle, active_item_id, cx);
|
let task = Self::close_item_by_id(workspace, pane_handle, active_item_id, cx);
|
||||||
|
@ -2078,6 +2082,19 @@ mod tests {
|
||||||
use gpui::{executor::Deterministic, TestAppContext};
|
use gpui::{executor::Deterministic, TestAppContext};
|
||||||
use project::FakeFs;
|
use project::FakeFs;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_remove_active_empty(cx: &mut TestAppContext) {
|
||||||
|
Settings::test_async(cx);
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
|
let project = Project::test(fs, None, cx).await;
|
||||||
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
|
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
assert!(Pane::close_active_item(workspace, &CloseActiveItem, cx).is_none())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
|
async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||||
description = "The fast, collaborative code editor."
|
description = "The fast, collaborative code editor."
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.82.0"
|
version = "0.82.9"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
dev
|
stable
|
|
@ -44,9 +44,7 @@ export default function editor(colorScheme: ColorScheme) {
|
||||||
activeLineBackground: withOpacity(background(layer, "on"), 0.75),
|
activeLineBackground: withOpacity(background(layer, "on"), 0.75),
|
||||||
highlightedLineBackground: background(layer, "on"),
|
highlightedLineBackground: background(layer, "on"),
|
||||||
// Inline autocomplete suggestions, Co-pilot suggestions, etc.
|
// Inline autocomplete suggestions, Co-pilot suggestions, etc.
|
||||||
suggestion: {
|
suggestion: syntax.predictive,
|
||||||
color: syntax.predictive.color,
|
|
||||||
},
|
|
||||||
codeActions: {
|
codeActions: {
|
||||||
indicator: {
|
indicator: {
|
||||||
color: foreground(layer, "variant"),
|
color: foreground(layer, "variant"),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import deepmerge from "deepmerge"
|
import deepmerge from "deepmerge"
|
||||||
import { FontWeight, fontWeights } from "../../common"
|
import { FontWeight, fontWeights } from "../../common"
|
||||||
import { ColorScheme } from "./colorScheme"
|
import { ColorScheme } from "./colorScheme"
|
||||||
|
import chroma from "chroma-js"
|
||||||
|
|
||||||
export interface SyntaxHighlightStyle {
|
export interface SyntaxHighlightStyle {
|
||||||
color: string
|
color: string
|
||||||
|
@ -128,6 +129,8 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
|
||||||
[key: string]: Omit<SyntaxHighlightStyle, "color">
|
[key: string]: Omit<SyntaxHighlightStyle, "color">
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
|
const light = colorScheme.isLight
|
||||||
|
|
||||||
// then spread the default to each style
|
// then spread the default to each style
|
||||||
for (const key of Object.keys({} as Syntax)) {
|
for (const key of Object.keys({} as Syntax)) {
|
||||||
syntax[key as keyof Syntax] = {
|
syntax[key as keyof Syntax] = {
|
||||||
|
@ -135,11 +138,20 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mix the neutral and blue colors to get a
|
||||||
|
// predictive color distinct from any other color in the theme
|
||||||
|
const predictive = chroma.mix(
|
||||||
|
colorScheme.ramps.neutral(0.4).hex(),
|
||||||
|
colorScheme.ramps.blue(0.4).hex(),
|
||||||
|
0.45,
|
||||||
|
"lch"
|
||||||
|
).hex()
|
||||||
|
|
||||||
const color = {
|
const color = {
|
||||||
primary: colorScheme.ramps.neutral(1).hex(),
|
primary: colorScheme.ramps.neutral(1).hex(),
|
||||||
comment: colorScheme.ramps.neutral(0.71).hex(),
|
comment: colorScheme.ramps.neutral(0.71).hex(),
|
||||||
punctuation: colorScheme.ramps.neutral(0.86).hex(),
|
punctuation: colorScheme.ramps.neutral(0.86).hex(),
|
||||||
predictive: colorScheme.ramps.neutral(0.57).hex(),
|
predictive: predictive,
|
||||||
emphasis: colorScheme.ramps.blue(0.5).hex(),
|
emphasis: colorScheme.ramps.blue(0.5).hex(),
|
||||||
string: colorScheme.ramps.orange(0.5).hex(),
|
string: colorScheme.ramps.orange(0.5).hex(),
|
||||||
function: colorScheme.ramps.yellow(0.5).hex(),
|
function: colorScheme.ramps.yellow(0.5).hex(),
|
||||||
|
@ -169,6 +181,7 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
|
||||||
},
|
},
|
||||||
predictive: {
|
predictive: {
|
||||||
color: color.predictive,
|
color: color.predictive,
|
||||||
|
italic: true,
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
color: color.emphasis,
|
color: color.emphasis,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue