Merge branch 'main' into randomized-tests-operation-script
This commit is contained in:
commit
2d63ed3ca4
42 changed files with 1183 additions and 1960 deletions
|
@ -17,6 +17,7 @@ db = { path = "../db" }
|
|||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
rpc = { path = "../rpc" }
|
||||
staff_mode = { path = "../staff_mode" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
anyhow = "1.0.38"
|
||||
async-recursion = "0.3"
|
||||
|
|
|
@ -6,9 +6,10 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
|
|||
use postage::{sink::Sink, watch};
|
||||
use rpc::proto::{RequestMessage, UsersResponse};
|
||||
use settings::Settings;
|
||||
use staff_mode::StaffMode;
|
||||
use std::sync::{Arc, Weak};
|
||||
use util::http::HttpClient;
|
||||
use util::{StaffMode, TryFutureExt as _};
|
||||
use util::TryFutureExt as _;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct User {
|
||||
|
|
|
@ -8,6 +8,16 @@ publish = false
|
|||
path = "src/copilot.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"collections/test-support",
|
||||
"gpui/test-support",
|
||||
"language/test-support",
|
||||
"lsp/test-support",
|
||||
"settings/test-support",
|
||||
"util/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
|
@ -18,7 +28,6 @@ theme = { path = "../theme" }
|
|||
lsp = { path = "../lsp" }
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
client = { path = "../client" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-tar = "0.4.2"
|
||||
anyhow = "1.0"
|
||||
|
@ -29,10 +38,10 @@ smol = "1.2.5"
|
|||
futures = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
mod request;
|
||||
pub mod request;
|
||||
mod sign_in;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use client::Client;
|
||||
use collections::HashMap;
|
||||
use futures::{future::Shared, Future, FutureExt, TryFutureExt};
|
||||
use gpui::{
|
||||
|
@ -25,40 +24,31 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
use util::{
|
||||
fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
|
||||
channel::ReleaseChannel, fs::remove_matching, github::latest_github_release, http::HttpClient,
|
||||
paths, ResultExt,
|
||||
};
|
||||
|
||||
const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
|
||||
actions!(copilot_auth, [SignIn, SignOut]);
|
||||
|
||||
const COPILOT_NAMESPACE: &'static str = "copilot";
|
||||
actions!(
|
||||
copilot,
|
||||
[NextSuggestion, PreviousSuggestion, Toggle, Reinstall]
|
||||
);
|
||||
actions!(copilot, [NextSuggestion, PreviousSuggestion, Reinstall]);
|
||||
|
||||
pub fn init(client: Arc<Client>, node_runtime: Arc<NodeRuntime>, cx: &mut MutableAppContext) {
|
||||
let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), node_runtime, cx));
|
||||
pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut MutableAppContext) {
|
||||
// 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 node_runtime = node_runtime.clone();
|
||||
move |cx| Copilot::start(http, node_runtime, cx)
|
||||
});
|
||||
cx.set_global(copilot.clone());
|
||||
cx.add_global_action(|_: &SignIn, cx| {
|
||||
let copilot = Copilot::global(cx).unwrap();
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
cx.add_global_action(|_: &SignOut, cx| {
|
||||
let copilot = Copilot::global(cx).unwrap();
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
|
||||
cx.add_global_action(|_: &Reinstall, cx| {
|
||||
let copilot = Copilot::global(cx).unwrap();
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||
.detach();
|
||||
});
|
||||
|
||||
cx.observe(&copilot, |handle, cx| {
|
||||
let status = handle.read(cx).status();
|
||||
|
@ -82,6 +72,28 @@ pub fn init(client: Arc<Client>, node_runtime: Arc<NodeRuntime>, cx: &mut Mutabl
|
|||
.detach();
|
||||
|
||||
sign_in::init(cx);
|
||||
cx.add_global_action(|_: &SignIn, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
cx.add_global_action(|_: &SignOut, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
|
||||
cx.add_global_action(|_: &Reinstall, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enum CopilotServer {
|
||||
|
@ -99,12 +111,8 @@ enum CopilotServer {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
enum SignInStatus {
|
||||
Authorized {
|
||||
_user: String,
|
||||
},
|
||||
Unauthorized {
|
||||
_user: String,
|
||||
},
|
||||
Authorized,
|
||||
Unauthorized,
|
||||
SigningIn {
|
||||
prompt: Option<request::PromptUserDeviceFlow>,
|
||||
task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,
|
||||
|
@ -150,13 +158,6 @@ impl Entity for Copilot {
|
|||
}
|
||||
|
||||
impl Copilot {
|
||||
pub fn starting_task(&self) -> Option<Shared<Task<()>>> {
|
||||
match self.server {
|
||||
CopilotServer::Starting { ref task } => Some(task.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn global(cx: &AppContext) -> Option<ModelHandle<Self>> {
|
||||
if cx.has_global::<ModelHandle<Self>>() {
|
||||
Some(cx.global::<ModelHandle<Self>>().clone())
|
||||
|
@ -219,6 +220,23 @@ impl Copilot {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn fake(cx: &mut gpui::TestAppContext) -> (ModelHandle<Self>, lsp::FakeLanguageServer) {
|
||||
let (server, fake_server) =
|
||||
LanguageServer::fake("copilot".into(), Default::default(), cx.to_async());
|
||||
let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
|
||||
let this = cx.add_model(|cx| Self {
|
||||
http: http.clone(),
|
||||
node_runtime: NodeRuntime::new(http, cx.background().clone()),
|
||||
server: CopilotServer::Started {
|
||||
server: Arc::new(server),
|
||||
status: SignInStatus::Authorized,
|
||||
subscriptions_by_buffer_id: Default::default(),
|
||||
},
|
||||
});
|
||||
(this, fake_server)
|
||||
}
|
||||
|
||||
fn start_language_server(
|
||||
http: Arc<dyn HttpClient>,
|
||||
node_runtime: Arc<NodeRuntime>,
|
||||
|
@ -598,14 +616,10 @@ impl Copilot {
|
|||
) {
|
||||
if let CopilotServer::Started { status, .. } = &mut self.server {
|
||||
*status = match lsp_status {
|
||||
request::SignInStatus::Ok { user }
|
||||
| request::SignInStatus::MaybeOk { user }
|
||||
| request::SignInStatus::AlreadySignedIn { user } => {
|
||||
SignInStatus::Authorized { _user: user }
|
||||
}
|
||||
request::SignInStatus::NotAuthorized { user } => {
|
||||
SignInStatus::Unauthorized { _user: user }
|
||||
}
|
||||
request::SignInStatus::Ok { .. }
|
||||
| request::SignInStatus::MaybeOk { .. }
|
||||
| request::SignInStatus::AlreadySignedIn { .. } => SignInStatus::Authorized,
|
||||
request::SignInStatus::NotAuthorized { .. } => SignInStatus::Unauthorized,
|
||||
request::SignInStatus::NotSignedIn => SignInStatus::SignedOut,
|
||||
};
|
||||
cx.notify();
|
||||
|
|
|
@ -117,7 +117,7 @@ pub struct GetCompletionsResult {
|
|||
pub completions: Vec<Completion>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Completion {
|
||||
pub text: String,
|
||||
|
|
|
@ -23,28 +23,17 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
|
||||
match &status {
|
||||
crate::Status::SigningIn { prompt } => {
|
||||
if let Some(code_verification) = code_verification.as_ref() {
|
||||
code_verification.update(cx, |code_verification, cx| {
|
||||
code_verification.set_status(status, cx)
|
||||
});
|
||||
cx.activate_window(code_verification.window_id());
|
||||
if let Some(code_verification_handle) = code_verification.as_mut() {
|
||||
if cx.has_window(code_verification_handle.window_id()) {
|
||||
code_verification_handle.update(cx, |code_verification_view, cx| {
|
||||
code_verification_view.set_status(status, cx)
|
||||
});
|
||||
cx.activate_window(code_verification_handle.window_id());
|
||||
} else {
|
||||
create_copilot_auth_window(cx, &status, &mut code_verification);
|
||||
}
|
||||
} else if let Some(_prompt) = prompt {
|
||||
let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
|
||||
let window_options = WindowOptions {
|
||||
bounds: gpui::WindowBounds::Fixed(RectF::new(
|
||||
Default::default(),
|
||||
window_size,
|
||||
)),
|
||||
titlebar: None,
|
||||
center: true,
|
||||
focus: true,
|
||||
kind: WindowKind::Normal,
|
||||
is_movable: true,
|
||||
screen: None,
|
||||
};
|
||||
let (_, view) =
|
||||
cx.add_window(window_options, |_cx| CopilotCodeVerification::new(status));
|
||||
code_verification = Some(view);
|
||||
create_copilot_auth_window(cx, &status, &mut code_verification);
|
||||
}
|
||||
}
|
||||
Status::Authorized | Status::Unauthorized => {
|
||||
|
@ -67,6 +56,27 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
.detach();
|
||||
}
|
||||
|
||||
fn create_copilot_auth_window(
|
||||
cx: &mut MutableAppContext,
|
||||
status: &Status,
|
||||
code_verification: &mut Option<ViewHandle<CopilotCodeVerification>>,
|
||||
) {
|
||||
let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
|
||||
let window_options = WindowOptions {
|
||||
bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
|
||||
titlebar: None,
|
||||
center: true,
|
||||
focus: true,
|
||||
kind: WindowKind::Normal,
|
||||
is_movable: true,
|
||||
screen: None,
|
||||
};
|
||||
let (_, view) = cx.add_window(window_options, |_cx| {
|
||||
CopilotCodeVerification::new(status.clone())
|
||||
});
|
||||
*code_verification = Some(view);
|
||||
}
|
||||
|
||||
pub struct CopilotCodeVerification {
|
||||
status: Status,
|
||||
}
|
||||
|
|
|
@ -228,13 +228,8 @@ impl CopilotButton {
|
|||
|
||||
Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
|
||||
|
||||
let this_handle = cx.handle().downgrade();
|
||||
cx.observe_global::<Settings, _>(move |cx| {
|
||||
if let Some(handle) = this_handle.upgrade(cx) {
|
||||
handle.update(cx, |_, cx| cx.notify())
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
cx.observe_global::<Settings, _>(move |_, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
popup_menu: menu,
|
||||
|
@ -249,19 +244,6 @@ impl CopilotButton {
|
|||
|
||||
let mut menu_options = Vec::with_capacity(6);
|
||||
|
||||
if let Some((_, view_id)) = self.editor_subscription.as_ref() {
|
||||
let locally_enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
|
||||
menu_options.push(ContextMenuItem::item_for_view(
|
||||
if locally_enabled {
|
||||
"Pause Copilot for this file"
|
||||
} else {
|
||||
"Resume Copilot for this file"
|
||||
},
|
||||
*view_id,
|
||||
copilot::Toggle,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(language) = &self.language {
|
||||
let language_enabled = settings.copilot_on(Some(language.as_ref()));
|
||||
|
||||
|
@ -334,11 +316,7 @@ impl CopilotButton {
|
|||
|
||||
self.language = language_name.clone();
|
||||
|
||||
if let Some(enabled) = editor.copilot_state.user_enabled {
|
||||
self.editor_enabled = Some(enabled);
|
||||
} else {
|
||||
self.editor_enabled = Some(settings.copilot_on(language_name.as_deref()));
|
||||
}
|
||||
self.editor_enabled = Some(settings.copilot_on(language_name.as_deref()));
|
||||
|
||||
cx.notify()
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ doctest = false
|
|||
[features]
|
||||
test-support = [
|
||||
"rand",
|
||||
"copilot/test-support",
|
||||
"text/test-support",
|
||||
"language/test-support",
|
||||
"gpui/test-support",
|
||||
|
@ -65,6 +66,7 @@ tree-sitter-javascript = { version = "*", optional = true }
|
|||
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
copilot = { path = "../copilot", features = ["test-support"] }
|
||||
text = { path = "../text", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
|
|
|
@ -7,7 +7,7 @@ mod wrap_map;
|
|||
use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
pub use block_map::{BlockMap, BlockPoint};
|
||||
use collections::{HashMap, HashSet};
|
||||
use fold_map::FoldMap;
|
||||
use fold_map::{FoldMap, FoldOffset};
|
||||
use gpui::{
|
||||
color::Color,
|
||||
fonts::{FontId, HighlightStyle},
|
||||
|
@ -238,19 +238,22 @@ impl DisplayMap {
|
|||
&self,
|
||||
new_suggestion: Option<Suggestion<T>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) where
|
||||
) -> Option<Suggestion<FoldOffset>>
|
||||
where
|
||||
T: ToPoint,
|
||||
{
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = self.suggestion_map.replace(new_suggestion, snapshot, edits);
|
||||
let (snapshot, edits, old_suggestion) =
|
||||
self.suggestion_map.replace(new_suggestion, snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
self.block_map.read(snapshot, edits);
|
||||
old_suggestion
|
||||
}
|
||||
|
||||
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
|
||||
|
|
|
@ -79,7 +79,11 @@ impl SuggestionMap {
|
|||
new_suggestion: Option<Suggestion<T>>,
|
||||
fold_snapshot: FoldSnapshot,
|
||||
fold_edits: Vec<FoldEdit>,
|
||||
) -> (SuggestionSnapshot, Vec<SuggestionEdit>)
|
||||
) -> (
|
||||
SuggestionSnapshot,
|
||||
Vec<SuggestionEdit>,
|
||||
Option<Suggestion<FoldOffset>>,
|
||||
)
|
||||
where
|
||||
T: ToPoint,
|
||||
{
|
||||
|
@ -99,7 +103,8 @@ impl SuggestionMap {
|
|||
let mut snapshot = self.0.lock();
|
||||
|
||||
let mut patch = Patch::new(edits);
|
||||
if let Some(suggestion) = snapshot.suggestion.take() {
|
||||
let old_suggestion = snapshot.suggestion.take();
|
||||
if let Some(suggestion) = &old_suggestion {
|
||||
patch = patch.compose([SuggestionEdit {
|
||||
old: SuggestionOffset(suggestion.position.0)
|
||||
..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
|
||||
|
@ -119,7 +124,7 @@ impl SuggestionMap {
|
|||
|
||||
snapshot.suggestion = new_suggestion;
|
||||
snapshot.version += 1;
|
||||
(snapshot.clone(), patch.into_inner())
|
||||
(snapshot.clone(), patch.into_inner(), old_suggestion)
|
||||
}
|
||||
|
||||
pub fn sync(
|
||||
|
@ -589,7 +594,7 @@ mod tests {
|
|||
let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
|
||||
assert_eq!(suggestion_snapshot.text(), "abcdefghi");
|
||||
|
||||
let (suggestion_snapshot, _) = suggestion_map.replace(
|
||||
let (suggestion_snapshot, _, _) = suggestion_map.replace(
|
||||
Some(Suggestion {
|
||||
position: 3,
|
||||
text: "123\n456".into(),
|
||||
|
@ -854,7 +859,9 @@ mod tests {
|
|||
};
|
||||
|
||||
log::info!("replacing suggestion with {:?}", new_suggestion);
|
||||
self.replace(new_suggestion, fold_snapshot, Default::default())
|
||||
let (snapshot, edits, _) =
|
||||
self.replace(new_suggestion, fold_snapshot, Default::default());
|
||||
(snapshot, edits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ pub use language::{char_kind, CharKind};
|
|||
use language::{
|
||||
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
|
||||
Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
|
||||
Point, Selection, SelectionGoal, TransactionId,
|
||||
Point, Rope, Selection, SelectionGoal, TransactionId,
|
||||
};
|
||||
use link_go_to_definition::{
|
||||
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
|
||||
|
@ -95,6 +95,7 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
|||
const MAX_LINE_LEN: usize = 1024;
|
||||
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
|
||||
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
|
||||
const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
||||
|
||||
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
|
@ -391,7 +392,6 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
cx.add_async_action(Editor::find_all_references);
|
||||
cx.add_action(Editor::next_copilot_suggestion);
|
||||
cx.add_action(Editor::previous_copilot_suggestion);
|
||||
cx.add_action(Editor::toggle_copilot_suggestions);
|
||||
|
||||
hover_popover::init(cx);
|
||||
link_go_to_definition::init(cx);
|
||||
|
@ -510,7 +510,7 @@ pub struct Editor {
|
|||
hover_state: HoverState,
|
||||
gutter_hovered: bool,
|
||||
link_go_to_definition_state: LinkGoToDefinitionState,
|
||||
pub copilot_state: CopilotState,
|
||||
copilot_state: CopilotState,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
|
@ -1013,7 +1013,6 @@ pub struct CopilotState {
|
|||
pending_refresh: Task<Option<()>>,
|
||||
completions: Vec<copilot::Completion>,
|
||||
active_completion_index: usize,
|
||||
pub user_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for CopilotState {
|
||||
|
@ -1023,7 +1022,6 @@ impl Default for CopilotState {
|
|||
pending_refresh: Task::ready(Some(())),
|
||||
completions: Default::default(),
|
||||
active_completion_index: 0,
|
||||
user_enabled: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1039,6 +1037,11 @@ impl CopilotState {
|
|||
let completion = self.completions.get(self.active_completion_index)?;
|
||||
let excerpt_id = self.excerpt_id?;
|
||||
let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
|
||||
if !completion.range.start.is_valid(completion_buffer)
|
||||
|| !completion.range.end.is_valid(completion_buffer)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut completion_range = completion.range.to_offset(&completion_buffer);
|
||||
let prefix_len = Self::common_prefix(
|
||||
|
@ -1258,6 +1261,7 @@ impl Editor {
|
|||
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||
cx.observe(&display_map, Self::on_display_map_changed),
|
||||
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
|
||||
cx.observe_global::<Settings, _>(Self::on_settings_changed),
|
||||
],
|
||||
};
|
||||
this.end_selection(cx);
|
||||
|
@ -1461,7 +1465,7 @@ impl Editor {
|
|||
self.refresh_code_actions(cx);
|
||||
self.refresh_document_highlights(cx);
|
||||
refresh_matching_bracket_highlights(self, cx);
|
||||
self.refresh_copilot_suggestions(cx);
|
||||
self.hide_copilot_suggestion(cx);
|
||||
}
|
||||
|
||||
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
||||
|
@ -1835,7 +1839,7 @@ impl Editor {
|
|||
return;
|
||||
}
|
||||
|
||||
if self.clear_copilot_suggestions(cx) {
|
||||
if self.hide_copilot_suggestion(cx).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2014,8 +2018,18 @@ impl Editor {
|
|||
}
|
||||
|
||||
drop(snapshot);
|
||||
let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||
this.trigger_completion_on_input(&text, cx);
|
||||
|
||||
if had_active_copilot_suggestion {
|
||||
this.refresh_copilot_suggestions(cx);
|
||||
if !this.has_active_copilot_suggestion(cx) {
|
||||
this.trigger_completion_on_input(&text, cx);
|
||||
}
|
||||
} else {
|
||||
this.trigger_completion_on_input(&text, cx);
|
||||
this.refresh_copilot_suggestions(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2096,6 +2110,7 @@ impl Editor {
|
|||
.collect();
|
||||
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||
this.refresh_copilot_suggestions(cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2188,9 +2203,7 @@ impl Editor {
|
|||
}
|
||||
|
||||
fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
||||
if !cx.global::<Settings>().show_completions_on_input
|
||||
|| self.has_active_copilot_suggestion(cx)
|
||||
{
|
||||
if !cx.global::<Settings>().show_completions_on_input {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2377,8 +2390,8 @@ impl Editor {
|
|||
this.completion_tasks.retain(|(id, _)| *id > menu.id);
|
||||
if this.focused && !menu.matches.is_empty() {
|
||||
this.show_context_menu(ContextMenu::Completions(menu), cx);
|
||||
} else {
|
||||
this.hide_context_menu(cx);
|
||||
} else if this.hide_context_menu(cx).is_none() {
|
||||
this.update_visible_copilot_suggestion(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -2481,6 +2494,8 @@ impl Editor {
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
this.refresh_copilot_suggestions(cx);
|
||||
});
|
||||
|
||||
let project = self.project.clone()?;
|
||||
|
@ -2775,50 +2790,24 @@ impl Editor {
|
|||
|
||||
fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
|
||||
let copilot = Copilot::global(cx)?;
|
||||
|
||||
if self.mode != EditorMode::Full {
|
||||
return None;
|
||||
}
|
||||
|
||||
let settings = cx.global::<Settings>();
|
||||
|
||||
if !self
|
||||
.copilot_state
|
||||
.user_enabled
|
||||
.unwrap_or_else(|| settings.copilot_on(None))
|
||||
{
|
||||
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
|
||||
self.hide_copilot_suggestion(cx);
|
||||
return None;
|
||||
}
|
||||
self.update_visible_copilot_suggestion(cx);
|
||||
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let selection = self.selections.newest_anchor();
|
||||
|
||||
if !self.copilot_state.user_enabled.is_some() {
|
||||
let language_name = snapshot
|
||||
.language_at(selection.start)
|
||||
.map(|language| language.name());
|
||||
|
||||
let copilot_enabled = settings.copilot_on(language_name.as_deref());
|
||||
|
||||
if !copilot_enabled {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let cursor = if selection.start == selection.end {
|
||||
selection.start.bias_left(&snapshot)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
self.refresh_active_copilot_suggestion(cx);
|
||||
|
||||
if !copilot.read(cx).status().is_authorized() {
|
||||
let cursor = self.selections.newest_anchor().head();
|
||||
let language_name = snapshot.language_at(cursor).map(|language| language.name());
|
||||
if !cx.global::<Settings>().copilot_on(language_name.as_deref()) {
|
||||
self.hide_copilot_suggestion(cx);
|
||||
return None;
|
||||
}
|
||||
|
||||
let (buffer, buffer_position) =
|
||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
||||
self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
|
||||
cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
|
||||
let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| {
|
||||
(
|
||||
copilot.completions(&buffer, buffer_position, cx),
|
||||
|
@ -2838,7 +2827,7 @@ impl Editor {
|
|||
for completion in completions {
|
||||
this.copilot_state.push_completion(completion);
|
||||
}
|
||||
this.refresh_active_copilot_suggestion(cx);
|
||||
this.update_visible_copilot_suggestion(cx);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -2849,21 +2838,14 @@ impl Editor {
|
|||
}
|
||||
|
||||
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
|
||||
// Auto re-enable copilot if you're asking for a suggestion
|
||||
if self.copilot_state.user_enabled == Some(false) {
|
||||
cx.notify();
|
||||
self.copilot_state.user_enabled = Some(true);
|
||||
}
|
||||
|
||||
if self.copilot_state.completions.is_empty() {
|
||||
if !self.has_active_copilot_suggestion(cx) {
|
||||
self.refresh_copilot_suggestions(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
self.copilot_state.active_completion_index =
|
||||
(self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len();
|
||||
|
||||
self.refresh_active_copilot_suggestion(cx);
|
||||
self.update_visible_copilot_suggestion(cx);
|
||||
}
|
||||
|
||||
fn previous_copilot_suggestion(
|
||||
|
@ -2871,13 +2853,7 @@ impl Editor {
|
|||
_: &copilot::PreviousSuggestion,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
// Auto re-enable copilot if you're asking for a suggestion
|
||||
if self.copilot_state.user_enabled == Some(false) {
|
||||
cx.notify();
|
||||
self.copilot_state.user_enabled = Some(true);
|
||||
}
|
||||
|
||||
if self.copilot_state.completions.is_empty() {
|
||||
if !self.has_active_copilot_suggestion(cx) {
|
||||
self.refresh_copilot_suggestions(cx);
|
||||
return;
|
||||
}
|
||||
|
@ -2888,45 +2864,44 @@ impl Editor {
|
|||
} else {
|
||||
self.copilot_state.active_completion_index - 1
|
||||
};
|
||||
|
||||
self.refresh_active_copilot_suggestion(cx);
|
||||
self.update_visible_copilot_suggestion(cx);
|
||||
}
|
||||
|
||||
fn toggle_copilot_suggestions(&mut self, _: &copilot::Toggle, cx: &mut ViewContext<Self>) {
|
||||
self.copilot_state.user_enabled = match self.copilot_state.user_enabled {
|
||||
Some(enabled) => Some(!enabled),
|
||||
None => {
|
||||
let selection = self.selections.newest_anchor().start;
|
||||
|
||||
let language_name = self
|
||||
.snapshot(cx)
|
||||
.language_at(selection)
|
||||
.map(|language| language.name());
|
||||
|
||||
let copilot_enabled = cx.global::<Settings>().copilot_on(language_name.as_deref());
|
||||
|
||||
Some(!copilot_enabled)
|
||||
}
|
||||
};
|
||||
|
||||
// We know this can't be None, as we just set it to Some above
|
||||
if self.copilot_state.user_enabled == Some(true) {
|
||||
self.refresh_copilot_suggestions(cx);
|
||||
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if let Some(text) = self.hide_copilot_suggestion(cx) {
|
||||
self.insert_with_autoindent_mode(&text.to_string(), None, cx);
|
||||
true
|
||||
} else {
|
||||
self.clear_copilot_suggestions(cx);
|
||||
false
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn refresh_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let cursor = self.selections.newest_anchor().head();
|
||||
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
|
||||
self.display_map.read(cx).has_suggestion()
|
||||
}
|
||||
|
||||
if self.context_menu.is_some() {
|
||||
self.display_map
|
||||
fn hide_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Rope> {
|
||||
if self.has_active_copilot_suggestion(cx) {
|
||||
let old_suggestion = self
|
||||
.display_map
|
||||
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
|
||||
cx.notify();
|
||||
old_suggestion.map(|suggestion| suggestion.text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let selection = self.selections.newest_anchor();
|
||||
let cursor = selection.head();
|
||||
|
||||
if self.context_menu.is_some()
|
||||
|| !self.completion_tasks.is_empty()
|
||||
|| selection.start != selection.end
|
||||
{
|
||||
self.hide_copilot_suggestion(cx);
|
||||
} else if let Some(text) = self
|
||||
.copilot_state
|
||||
.text_for_active_completion(cursor, &snapshot)
|
||||
|
@ -2941,44 +2916,11 @@ impl Editor {
|
|||
)
|
||||
});
|
||||
cx.notify();
|
||||
} else if self.has_active_copilot_suggestion(cx) {
|
||||
self.display_map
|
||||
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let cursor = self.selections.newest_anchor().head();
|
||||
if let Some(text) = self
|
||||
.copilot_state
|
||||
.text_for_active_completion(cursor, &snapshot)
|
||||
{
|
||||
self.insert_with_autoindent_mode(&text.to_string(), None, cx);
|
||||
self.clear_copilot_suggestions(cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
self.hide_copilot_suggestion(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.display_map
|
||||
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
|
||||
let was_empty = self.copilot_state.completions.is_empty();
|
||||
self.copilot_state.completions.clear();
|
||||
self.copilot_state.active_completion_index = 0;
|
||||
self.copilot_state.pending_refresh = Task::ready(None);
|
||||
self.copilot_state.excerpt_id = None;
|
||||
cx.notify();
|
||||
!was_empty
|
||||
}
|
||||
|
||||
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
|
||||
self.display_map.read(cx).has_suggestion()
|
||||
}
|
||||
|
||||
pub fn render_code_actions_indicator(
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
|
@ -3094,7 +3036,7 @@ impl Editor {
|
|||
self.completion_tasks.clear();
|
||||
}
|
||||
self.context_menu = Some(menu);
|
||||
self.refresh_active_copilot_suggestion(cx);
|
||||
self.hide_copilot_suggestion(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -3102,7 +3044,9 @@ impl Editor {
|
|||
cx.notify();
|
||||
self.completion_tasks.clear();
|
||||
let context_menu = self.context_menu.take();
|
||||
self.refresh_active_copilot_suggestion(cx);
|
||||
if context_menu.is_some() {
|
||||
self.update_visible_copilot_suggestion(cx);
|
||||
}
|
||||
context_menu
|
||||
}
|
||||
|
||||
|
@ -3262,6 +3206,7 @@ impl Editor {
|
|||
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||
this.insert("", cx);
|
||||
this.refresh_copilot_suggestions(cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -3277,6 +3222,7 @@ impl Editor {
|
|||
})
|
||||
});
|
||||
this.insert("", cx);
|
||||
this.refresh_copilot_suggestions(cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -3289,10 +3235,6 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
|
||||
if self.accept_copilot_suggestion(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.move_to_next_snippet_tabstop(cx) {
|
||||
return;
|
||||
}
|
||||
|
@ -3322,8 +3264,8 @@ impl Editor {
|
|||
// If the selection is empty and the cursor is in the leading whitespace before the
|
||||
// suggested indentation, then auto-indent the line.
|
||||
let cursor = selection.head();
|
||||
let current_indent = snapshot.indent_size_for_line(cursor.row);
|
||||
if let Some(suggested_indent) = suggested_indents.get(&cursor.row).copied() {
|
||||
let current_indent = snapshot.indent_size_for_line(cursor.row);
|
||||
if cursor.column < suggested_indent.len
|
||||
&& cursor.column <= current_indent.len
|
||||
&& current_indent.len <= suggested_indent.len
|
||||
|
@ -3342,6 +3284,16 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
// Accept copilot suggestion if there is only one selection and the cursor is not
|
||||
// in the leading whitespace.
|
||||
if self.selections.count() == 1
|
||||
&& cursor.column >= current_indent.len
|
||||
&& self.has_active_copilot_suggestion(cx)
|
||||
{
|
||||
self.accept_copilot_suggestion(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, insert a hard or soft tab.
|
||||
let settings = cx.global::<Settings>();
|
||||
let language_name = buffer.language_at(cursor, cx).map(|l| l.name());
|
||||
|
@ -3365,7 +3317,8 @@ impl Editor {
|
|||
|
||||
self.transact(cx, |this, 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4045,6 +3998,7 @@ impl Editor {
|
|||
}
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
self.unmark_text(cx);
|
||||
self.refresh_copilot_suggestions(cx);
|
||||
cx.emit(Event::Edited);
|
||||
}
|
||||
}
|
||||
|
@ -4059,6 +4013,7 @@ impl Editor {
|
|||
}
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
self.unmark_text(cx);
|
||||
self.refresh_copilot_suggestions(cx);
|
||||
cx.emit(Event::Edited);
|
||||
}
|
||||
}
|
||||
|
@ -6466,7 +6421,9 @@ impl Editor {
|
|||
multi_buffer::Event::Edited => {
|
||||
self.refresh_active_diagnostics(cx);
|
||||
self.refresh_code_actions(cx);
|
||||
self.refresh_copilot_suggestions(cx);
|
||||
if self.has_active_copilot_suggestion(cx) {
|
||||
self.update_visible_copilot_suggestion(cx);
|
||||
}
|
||||
cx.emit(Event::BufferEdited);
|
||||
}
|
||||
multi_buffer::Event::ExcerptsAdded {
|
||||
|
@ -6497,6 +6454,10 @@ impl Editor {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_settings_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.refresh_copilot_suggestions(cx);
|
||||
}
|
||||
|
||||
pub fn set_searchable(&mut self, searchable: bool) {
|
||||
self.searchable = searchable;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ use language::{BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegist
|
|||
use parking_lot::Mutex;
|
||||
use project::FakeFs;
|
||||
use settings::EditorSettings;
|
||||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
assert_set_eq,
|
||||
|
@ -4585,81 +4585,6 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
|||
cx.assert_editor_state("editor.closeˇ");
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
|
||||
// Handle completion request passing a marked string specifying where the completion
|
||||
// should be triggered from using '|' character, what range should be replaced, and what completions
|
||||
// should be returned using '<' and '>' to delimit the range
|
||||
async fn handle_completion_request<'a>(
|
||||
cx: &mut EditorLspTestContext<'a>,
|
||||
marked_string: &str,
|
||||
completions: Vec<&'static str>,
|
||||
) {
|
||||
let complete_from_marker: TextRangeMarker = '|'.into();
|
||||
let replace_range_marker: TextRangeMarker = ('<', '>').into();
|
||||
let (_, mut marked_ranges) = marked_text_ranges_by(
|
||||
marked_string,
|
||||
vec![complete_from_marker.clone(), replace_range_marker.clone()],
|
||||
);
|
||||
|
||||
let complete_from_position =
|
||||
cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
|
||||
let replace_range =
|
||||
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
|
||||
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
|
||||
let completions = completions.clone();
|
||||
async move {
|
||||
assert_eq!(params.text_document_position.text_document.uri, url.clone());
|
||||
assert_eq!(
|
||||
params.text_document_position.position,
|
||||
complete_from_position
|
||||
);
|
||||
Ok(Some(lsp::CompletionResponse::Array(
|
||||
completions
|
||||
.iter()
|
||||
.map(|completion_text| lsp::CompletionItem {
|
||||
label: completion_text.to_string(),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: replace_range,
|
||||
new_text: completion_text.to_string(),
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
)))
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn handle_resolve_completion_request<'a>(
|
||||
cx: &mut EditorLspTestContext<'a>,
|
||||
edits: Option<Vec<(&'static str, &'static str)>>,
|
||||
) {
|
||||
let edits = edits.map(|edits| {
|
||||
edits
|
||||
.iter()
|
||||
.map(|(marked_string, new_text)| {
|
||||
let (_, marked_ranges) = marked_text_ranges(marked_string, false);
|
||||
let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
|
||||
lsp::TextEdit::new(replace_range, new_text.to_string())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
|
||||
let edits = edits.clone();
|
||||
async move {
|
||||
Ok(lsp::CompletionItem {
|
||||
additional_text_edits: edits,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -5956,6 +5881,288 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||||
cx.update(|cx| cx.set_global(copilot));
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
three
|
||||
"});
|
||||
|
||||
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
||||
cx.simulate_keystroke(".");
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
two
|
||||
three
|
||||
"},
|
||||
vec!["completion_a", "completion_b"],
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
text: "copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
|
||||
..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));
|
||||
|
||||
// Confirming a completion inserts it and hides the context menu, without showing
|
||||
// the copilot suggestion afterwards.
|
||||
editor
|
||||
.confirm_completion(&Default::default(), cx)
|
||||
.unwrap()
|
||||
.detach();
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
|
||||
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
three
|
||||
"});
|
||||
|
||||
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
||||
cx.simulate_keystroke(".");
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
two
|
||||
three
|
||||
"},
|
||||
vec!["completion_a", "completion_b"],
|
||||
);
|
||||
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));
|
||||
|
||||
// When hiding the context menu, the Copilot suggestion becomes visible.
|
||||
editor.hide_context_menu(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");
|
||||
});
|
||||
|
||||
// Ensure existing completion is interpolated when inserting again.
|
||||
cx.simulate_keystroke("c");
|
||||
deterministic.run_until_parked();
|
||||
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.c\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// After debouncing, new Copilot completions should be requested.
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
text: "one.copilot2".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
|
||||
..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.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||
|
||||
// Canceling should remove the active Copilot suggestion.
|
||||
editor.cancel(&Default::default(), cx);
|
||||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||
|
||||
// After canceling, tabbing shouldn't insert the previously shown suggestion.
|
||||
editor.tab(&Default::default(), cx);
|
||||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
|
||||
|
||||
// When undoing the previously active suggestion is shown again.
|
||||
editor.undo(&Default::default(), cx);
|
||||
assert!(editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
|
||||
cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
|
||||
|
||||
// Tabbing when there is an active suggestion inserts it.
|
||||
editor.tab(&Default::default(), cx);
|
||||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
|
||||
// When undoing the previously active suggestion is shown again.
|
||||
editor.undo(&Default::default(), cx);
|
||||
assert!(editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
|
||||
|
||||
// Hide suggestion.
|
||||
editor.cancel(&Default::default(), cx);
|
||||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// If an edit occurs outside of this editor but no suggestion is being shown,
|
||||
// we won't make it visible.
|
||||
cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Reset the editor to verify how suggestions behave when tabbing on leading indentation.
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.set_text("fn foo() {\n \n}", cx);
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
|
||||
});
|
||||
});
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
text: " let x = 4;".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
|
||||
deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||||
|
||||
// Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
|
||||
editor.tab(&Default::default(), cx);
|
||||
assert!(editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
|
||||
// Tabbing again accepts the suggestion.
|
||||
editor.tab(&Default::default(), cx);
|
||||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copilot_completion_invalidation(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||||
cx.update(|cx| cx.set_global(copilot));
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
one
|
||||
twˇ
|
||||
three
|
||||
"});
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
|
||||
deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
|
||||
|
||||
editor.backspace(&Default::default(), cx);
|
||||
assert!(editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\nt\nthree\n");
|
||||
|
||||
editor.backspace(&Default::default(), cx);
|
||||
assert!(editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\n\nthree\n");
|
||||
|
||||
// Deleting across the original suggestion range invalidates it.
|
||||
editor.backspace(&Default::default(), cx);
|
||||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\nthree\n");
|
||||
|
||||
// Undoing the deletion restores the suggestion.
|
||||
editor.undo(&Default::default(), cx);
|
||||
assert!(editor.has_active_copilot_suggestion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\n\nthree\n");
|
||||
});
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(row as u32, column as u32);
|
||||
point..point
|
||||
|
@ -5971,3 +6178,106 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo
|
|||
marked_text
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle completion request passing a marked string specifying where the completion
|
||||
/// should be triggered from using '|' character, what range should be replaced, and what completions
|
||||
/// should be returned using '<' and '>' to delimit the range
|
||||
fn handle_completion_request<'a>(
|
||||
cx: &mut EditorLspTestContext<'a>,
|
||||
marked_string: &str,
|
||||
completions: Vec<&'static str>,
|
||||
) -> impl Future<Output = ()> {
|
||||
let complete_from_marker: TextRangeMarker = '|'.into();
|
||||
let replace_range_marker: TextRangeMarker = ('<', '>').into();
|
||||
let (_, mut marked_ranges) = marked_text_ranges_by(
|
||||
marked_string,
|
||||
vec![complete_from_marker.clone(), replace_range_marker.clone()],
|
||||
);
|
||||
|
||||
let complete_from_position =
|
||||
cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
|
||||
let replace_range =
|
||||
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
|
||||
|
||||
let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
|
||||
let completions = completions.clone();
|
||||
async move {
|
||||
assert_eq!(params.text_document_position.text_document.uri, url.clone());
|
||||
assert_eq!(
|
||||
params.text_document_position.position,
|
||||
complete_from_position
|
||||
);
|
||||
Ok(Some(lsp::CompletionResponse::Array(
|
||||
completions
|
||||
.iter()
|
||||
.map(|completion_text| lsp::CompletionItem {
|
||||
label: completion_text.to_string(),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: replace_range,
|
||||
new_text: completion_text.to_string(),
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
)))
|
||||
}
|
||||
});
|
||||
|
||||
async move {
|
||||
request.next().await;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_resolve_completion_request<'a>(
|
||||
cx: &mut EditorLspTestContext<'a>,
|
||||
edits: Option<Vec<(&'static str, &'static str)>>,
|
||||
) -> impl Future<Output = ()> {
|
||||
let edits = edits.map(|edits| {
|
||||
edits
|
||||
.iter()
|
||||
.map(|(marked_string, new_text)| {
|
||||
let (_, marked_ranges) = marked_text_ranges(marked_string, false);
|
||||
let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
|
||||
lsp::TextEdit::new(replace_range, new_text.to_string())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let mut request =
|
||||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
|
||||
let edits = edits.clone();
|
||||
async move {
|
||||
Ok(lsp::CompletionItem {
|
||||
additional_text_edits: edits,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
async move {
|
||||
request.next().await;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_copilot_completion_request(
|
||||
lsp: &lsp::FakeLanguageServer,
|
||||
completions: Vec<copilot::request::Completion>,
|
||||
completions_cycling: Vec<copilot::request::Completion>,
|
||||
) {
|
||||
lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
|
||||
let completions = completions.clone();
|
||||
async move {
|
||||
Ok(copilot::request::GetCompletionsResult {
|
||||
completions: completions.clone(),
|
||||
})
|
||||
}
|
||||
});
|
||||
lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
|
||||
let completions_cycling = completions_cycling.clone();
|
||||
async move {
|
||||
Ok(copilot::request::GetCompletionsResult {
|
||||
completions: completions_cycling.clone(),
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3952,6 +3952,19 @@ impl<'a, T: View> ViewContext<'a, T> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn observe_global<G, F>(&mut self, mut callback: F) -> Subscription
|
||||
where
|
||||
G: Any,
|
||||
F: 'static + FnMut(&mut T, &mut ViewContext<T>),
|
||||
{
|
||||
let observer = self.weak_handle();
|
||||
self.app.observe_global::<G, _>(move |cx| {
|
||||
if let Some(observer) = observer.upgrade(cx) {
|
||||
observer.update(cx, |observer, cx| callback(observer, cx));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
|
||||
where
|
||||
F: 'static + FnMut(&mut T, ViewHandle<V>, bool, &mut ViewContext<T>),
|
||||
|
|
|
@ -227,6 +227,7 @@ pub enum Lifecycle<T: Element> {
|
|||
paint: T::PaintState,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct ElementBox(ElementRc);
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -65,8 +65,15 @@ impl platform::Screen for Screen {
|
|||
// This approach is similar to that which winit takes
|
||||
// https://github.com/rust-windowing/winit/blob/402cbd55f932e95dbfb4e8b5e8551c49e56ff9ac/src/platform_impl/macos/monitor.rs#L99
|
||||
let device_description = self.native_screen.deviceDescription();
|
||||
|
||||
let key = ns_string("NSScreenNumber");
|
||||
let device_id_obj = device_description.objectForKey_(key);
|
||||
if device_id_obj.is_null() {
|
||||
// Under some circumstances, especially display re-arrangements or display locking, we seem to get a null pointer
|
||||
// to the device id. See: https://linear.app/zed-industries/issue/Z-257/lock-screen-crash-with-multiple-monitors
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut device_id: u32 = 0;
|
||||
CFNumberGetValue(
|
||||
device_id_obj as CFNumberRef,
|
||||
|
|
|
@ -335,6 +335,7 @@ impl LanguageServer {
|
|||
did_change_configuration: Some(DynamicRegistrationClientCapabilities {
|
||||
dynamic_registration: Some(true),
|
||||
}),
|
||||
workspace_folders: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
text_document: Some(TextDocumentClientCapabilities {
|
||||
|
|
|
@ -20,6 +20,7 @@ fs = { path = "../fs" }
|
|||
anyhow = "1.0.38"
|
||||
futures = "0.3"
|
||||
theme = { path = "../theme" }
|
||||
staff_mode = { path = "../staff_mode" }
|
||||
util = { path = "../util" }
|
||||
json_comments = "0.2"
|
||||
postage = { workspace = true }
|
||||
|
|
|
@ -177,6 +177,7 @@ pub struct EditorSettings {
|
|||
pub ensure_final_newline_on_save: Option<bool>,
|
||||
pub formatter: Option<Formatter>,
|
||||
pub enable_language_server: Option<bool>,
|
||||
#[schemars(skip)]
|
||||
pub copilot: Option<OnOff>,
|
||||
}
|
||||
|
||||
|
@ -436,6 +437,7 @@ pub struct SettingsFileContent {
|
|||
#[serde(default)]
|
||||
pub base_keymap: Option<BaseKeymap>,
|
||||
#[serde(default)]
|
||||
#[schemars(skip)]
|
||||
pub enable_copilot_integration: Option<bool>,
|
||||
}
|
||||
|
||||
|
@ -779,6 +781,7 @@ pub fn settings_file_json_schema(
|
|||
settings.option_add_null_type = false;
|
||||
});
|
||||
let generator = SchemaGenerator::new(settings);
|
||||
|
||||
let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
|
||||
|
||||
// Create a schema for a theme name.
|
||||
|
@ -791,6 +794,7 @@ pub fn settings_file_json_schema(
|
|||
// Create a schema for a 'languages overrides' object, associating editor
|
||||
// settings with specific langauges.
|
||||
assert!(root_schema.definitions.contains_key("EditorSettings"));
|
||||
|
||||
let languages_object_schema = SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
|
||||
object: Some(Box::new(ObjectValidation {
|
||||
|
|
12
crates/staff_mode/Cargo.toml
Normal file
12
crates/staff_mode/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "staff_mode"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/staff_mode.rs"
|
||||
|
||||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
anyhow = "1.0.38"
|
42
crates/staff_mode/src/staff_mode.rs
Normal file
42
crates/staff_mode/src/staff_mode.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use gpui::MutableAppContext;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StaffMode(pub bool);
|
||||
|
||||
impl std::ops::Deref for StaffMode {
|
||||
type Target = bool;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Despite what the type system requires me to tell you, the init function will only be called a once
|
||||
/// as soon as we know that the staff mode is enabled.
|
||||
pub fn staff_mode<F: FnMut(&mut MutableAppContext) + 'static>(
|
||||
cx: &mut MutableAppContext,
|
||||
mut init: F,
|
||||
) {
|
||||
if **cx.default_global::<StaffMode>() {
|
||||
init(cx)
|
||||
} else {
|
||||
let mut once = Some(());
|
||||
cx.observe_global::<StaffMode, _>(move |cx| {
|
||||
if **cx.global::<StaffMode>() && once.take().is_some() {
|
||||
init(cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
/// Immediately checks and runs the init function if the staff mode is not enabled.
|
||||
/// This is only included for symettry with staff_mode() above
|
||||
pub fn not_staff_mode<F: FnOnce(&mut MutableAppContext) + 'static>(
|
||||
cx: &mut MutableAppContext,
|
||||
init: F,
|
||||
) {
|
||||
if !**cx.default_global::<StaffMode>() {
|
||||
init(cx)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
use crate::{BufferSnapshot, Point, PointUtf16, TextDimension, ToOffset, ToPoint, ToPointUtf16};
|
||||
use crate::{
|
||||
locator::Locator, BufferSnapshot, Point, PointUtf16, TextDimension, ToOffset, ToPoint,
|
||||
ToPointUtf16,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use std::{cmp::Ordering, fmt::Debug, ops::Range};
|
||||
use sum_tree::Bias;
|
||||
|
@ -86,6 +89,20 @@ impl Anchor {
|
|||
{
|
||||
content.summary_for_anchor(self)
|
||||
}
|
||||
|
||||
/// Returns true when the [Anchor] is located inside a visible fragment.
|
||||
pub fn is_valid(&self, buffer: &BufferSnapshot) -> bool {
|
||||
if *self == Anchor::MIN || *self == Anchor::MAX {
|
||||
true
|
||||
} else {
|
||||
let fragment_id = buffer.fragment_id_for_anchor(self);
|
||||
let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>();
|
||||
fragment_cursor.seek(&Some(fragment_id), Bias::Left, &None);
|
||||
fragment_cursor
|
||||
.item()
|
||||
.map_or(false, |fragment| fragment.visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OffsetRangeExt {
|
||||
|
|
|
@ -15,6 +15,7 @@ gpui = { path = "../gpui" }
|
|||
picker = { path = "../picker" }
|
||||
theme = { path = "../theme" }
|
||||
settings = { path = "../settings" }
|
||||
staff_mode = { path = "../staff_mode" }
|
||||
workspace = { path = "../workspace" }
|
||||
util = { path = "../util" }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
|
|
|
@ -5,9 +5,9 @@ use gpui::{
|
|||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::{settings_file::SettingsFile, Settings};
|
||||
use staff_mode::StaffMode;
|
||||
use std::sync::Arc;
|
||||
use theme::{Theme, ThemeMeta, ThemeRegistry};
|
||||
use util::StaffMode;
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
pub struct ThemeSelector {
|
||||
|
|
|
@ -3,8 +3,12 @@ use std::env;
|
|||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref RELEASE_CHANNEL_NAME: String = env::var("ZED_RELEASE_CHANNEL")
|
||||
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").to_string());
|
||||
pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) {
|
||||
env::var("ZED_RELEASE_CHANNEL")
|
||||
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").to_string())
|
||||
} else {
|
||||
include_str!("../../zed/RELEASE_CHANNEL").to_string()
|
||||
};
|
||||
pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str() {
|
||||
"dev" => ReleaseChannel::Dev,
|
||||
"preview" => ReleaseChannel::Preview,
|
||||
|
|
|
@ -17,17 +17,6 @@ pub use backtrace::Backtrace;
|
|||
use futures::Future;
|
||||
use rand::{seq::SliceRandom, Rng};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StaffMode(pub bool);
|
||||
|
||||
impl std::ops::Deref for StaffMode {
|
||||
type Target = bool;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! debug_panic {
|
||||
( $($fmt_arg:tt)* ) => {
|
||||
|
|
|
@ -191,16 +191,8 @@ impl View for WelcomePage {
|
|||
|
||||
impl WelcomePage {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let handle = cx.weak_handle();
|
||||
|
||||
let settings_subscription = cx.observe_global::<Settings, _>(move |cx| {
|
||||
if let Some(handle) = handle.upgrade(cx) {
|
||||
handle.update(cx, |_, cx| cx.notify())
|
||||
}
|
||||
});
|
||||
|
||||
WelcomePage {
|
||||
_settings_subscription: settings_subscription,
|
||||
_settings_subscription: cx.observe_global::<Settings, _>(move |_, cx| cx.notify()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use crate::{ItemHandle, Pane};
|
||||
use gpui::{
|
||||
elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription,
|
||||
View, ViewContext, ViewHandle,
|
||||
elements::*,
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json::{json, ToJson},
|
||||
AnyViewHandle, DebugContext, ElementBox, Entity, LayoutContext, MeasurementContext,
|
||||
MutableAppContext, PaintContext, RenderContext, SizeConstraint, Subscription, View,
|
||||
ViewContext, ViewHandle,
|
||||
};
|
||||
use settings::Settings;
|
||||
|
||||
|
@ -40,27 +49,33 @@ impl View for StatusBar {
|
|||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
|
||||
Flex::row()
|
||||
.with_children(self.left_items.iter().map(|i| {
|
||||
ChildView::new(i.as_any(), cx)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_right(theme.item_spacing)
|
||||
.boxed()
|
||||
}))
|
||||
.with_children(self.right_items.iter().rev().map(|i| {
|
||||
ChildView::new(i.as_any(), cx)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(theme.item_spacing)
|
||||
.flex_float()
|
||||
.boxed()
|
||||
}))
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.constrained()
|
||||
.with_height(theme.height)
|
||||
.boxed()
|
||||
|
||||
StatusBarElement {
|
||||
left: Flex::row()
|
||||
.with_children(self.left_items.iter().map(|i| {
|
||||
ChildView::new(i.as_any(), cx)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_right(theme.item_spacing)
|
||||
.boxed()
|
||||
}))
|
||||
.boxed(),
|
||||
|
||||
right: Flex::row()
|
||||
.with_children(self.right_items.iter().rev().map(|i| {
|
||||
ChildView::new(i.as_any(), cx)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(theme.item_spacing)
|
||||
.boxed()
|
||||
}))
|
||||
.boxed(),
|
||||
}
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.constrained()
|
||||
.with_height(theme.height)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,3 +146,74 @@ impl From<&dyn StatusItemViewHandle> for AnyViewHandle {
|
|||
val.as_any().clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct StatusBarElement {
|
||||
left: ElementBox,
|
||||
right: ElementBox,
|
||||
}
|
||||
|
||||
impl Element for StatusBarElement {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
mut constraint: SizeConstraint,
|
||||
cx: &mut LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let max_width = constraint.max.x();
|
||||
constraint.min = vec2f(0., constraint.min.y());
|
||||
|
||||
let right_size = self.right.layout(constraint, cx);
|
||||
let constraint = SizeConstraint::new(
|
||||
vec2f(0., constraint.min.y()),
|
||||
vec2f(max_width - right_size.x(), constraint.max.y()),
|
||||
);
|
||||
|
||||
self.left.layout(constraint, cx);
|
||||
|
||||
(vec2f(max_width, right_size.y()), ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
let origin_y = bounds.upper_right().y();
|
||||
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||
|
||||
let left_origin = vec2f(bounds.lower_left().x(), origin_y);
|
||||
self.left.paint(left_origin, visible_bounds, cx);
|
||||
|
||||
let right_origin = vec2f(bounds.upper_right().x() - self.right.size().x(), origin_y);
|
||||
self.right.paint(right_origin, visible_bounds, cx);
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
_: Range<usize>,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &MeasurementContext,
|
||||
) -> Option<RectF> {
|
||||
None
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
bounds: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &DebugContext,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "StatusBarElement",
|
||||
"bounds": bounds.to_json()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.81.0"
|
||||
version = "0.82.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
|
@ -55,6 +55,7 @@ project_symbols = { path = "../project_symbols" }
|
|||
recent_projects = { path = "../recent_projects" }
|
||||
rpc = { path = "../rpc" }
|
||||
settings = { path = "../settings" }
|
||||
staff_mode = { path = "../staff_mode" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
text = { path = "../text" }
|
||||
terminal_view = { path = "../terminal_view" }
|
||||
|
|
|
@ -8,6 +8,7 @@ use node_runtime::NodeRuntime;
|
|||
use serde_json::json;
|
||||
use settings::{keymap_file_json_schema, settings_file_json_schema};
|
||||
use smol::fs;
|
||||
use staff_mode::StaffMode;
|
||||
use std::{
|
||||
any::Any,
|
||||
ffi::OsString,
|
||||
|
@ -17,7 +18,7 @@ use std::{
|
|||
};
|
||||
use theme::ThemeRegistry;
|
||||
use util::http::HttpClient;
|
||||
use util::{paths, ResultExt, StaffMode};
|
||||
use util::{paths, ResultExt};
|
||||
|
||||
const SERVER_PATH: &'static str =
|
||||
"node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
|
||||
|
|
|
@ -109,10 +109,6 @@
|
|||
(false)
|
||||
] @constant.builtin
|
||||
|
||||
(interpolation
|
||||
"#{" @punctuation.special
|
||||
"}" @punctuation.special) @embedded
|
||||
|
||||
(comment) @comment
|
||||
|
||||
; Operators
|
||||
|
@ -178,4 +174,8 @@
|
|||
"}"
|
||||
"%w("
|
||||
"%i("
|
||||
] @punctuation.bracket
|
||||
] @punctuation.bracket
|
||||
|
||||
(interpolation
|
||||
"#{" @punctuation.special
|
||||
"}" @punctuation.special) @embedded
|
||||
|
|
|
@ -106,6 +106,9 @@ impl LspAdapter for YamlLspAdapter {
|
|||
let settings = cx.global::<Settings>();
|
||||
Some(
|
||||
future::ready(serde_json::json!({
|
||||
"yaml": {
|
||||
"keyOrdering": false
|
||||
},
|
||||
"[yaml]": {
|
||||
"editor.tabSize": settings.tab_size(Some("YAML"))
|
||||
}
|
||||
|
|
|
@ -38,9 +38,9 @@ use welcome::{show_welcome_experience, FIRST_OPEN};
|
|||
|
||||
use fs::RealFs;
|
||||
use settings::watched_json::WatchedJsonFile;
|
||||
use theme::ThemeRegistry;
|
||||
#[cfg(debug_assertions)]
|
||||
use util::StaffMode;
|
||||
use staff_mode::StaffMode;
|
||||
use theme::ThemeRegistry;
|
||||
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile,
|
||||
|
@ -161,7 +161,7 @@ fn main() {
|
|||
terminal_view::init(cx);
|
||||
theme_testbench::init(cx);
|
||||
recent_projects::init(cx);
|
||||
copilot::init(client.clone(), node_runtime, cx);
|
||||
copilot::init(http.clone(), node_runtime, cx);
|
||||
|
||||
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
|
||||
.detach();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue