diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index ab2895990ab997..0cd073cc17d9c2 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -23,7 +23,7 @@ use language::{ use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName}; use node_runtime::NodeRuntime; use parking_lot::Mutex; -use request::StatusNotification; +use request::DidChangeStatus; use settings::SettingsStore; use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace}; use std::{ @@ -471,6 +471,7 @@ impl Copilot { awaiting_sign_in_after_start: bool, cx: &mut AsyncApp, ) { + let _ = cx; let start_language_server = async { let server_path = get_copilot_lsp(fs, node_runtime.clone()).await?; let node_path = node_runtime.binary_path().await?; @@ -500,14 +501,93 @@ impl Copilot { )?; server - .on_notification::(|_, _| { /* Silence the notification */ }) + .on_notification::({ + let this = this.clone(); + move |params, cx| { + // Convert didChangeStatus to appropriate SignInStatus + let sign_in_status = match params.kind.as_str() { + "Error" => Some(request::SignInStatus::NotAuthorized { + user: String::new(), + }), + "Normal" => Some(request::SignInStatus::AlreadySignedIn { + user: String::new(), + }), + "Inactive" | "Warning" => None, // Don't change auth status for these + _ => None, + }; + + if let Some(status) = sign_in_status { + this.update(cx, |this, cx| { + // Check current status before deciding to update + if let CopilotServer::Running(server) = &this.server { + let should_update = match (&server.sign_in_status, &status) { + // If currently signing in, only update if receiving Authorized status + ( + SignInStatus::SigningIn { .. }, + request::SignInStatus::AlreadySignedIn { .. }, + ) + | ( + SignInStatus::SigningIn { .. }, + request::SignInStatus::Ok { user: Some(_) }, + ) + | ( + SignInStatus::SigningIn { .. }, + request::SignInStatus::MaybeOk { .. }, + ) => true, + + // Don't interrupt sign-in flow with an error + (SignInStatus::SigningIn { .. }, _) => false, + + // Avoid redundant transitions between signed-out states + ( + SignInStatus::SignedOut { .. }, + request::SignInStatus::NotSignedIn, + ) + | ( + SignInStatus::SignedOut { .. }, + request::SignInStatus::Ok { user: None }, + ) => false, + + // Avoid redundant transitions between authorized states + ( + SignInStatus::Authorized, + request::SignInStatus::AlreadySignedIn { .. }, + ) + | ( + SignInStatus::Authorized, + request::SignInStatus::MaybeOk { .. }, + ) + | ( + SignInStatus::Authorized, + request::SignInStatus::Ok { user: Some(_) }, + ) => false, + + // Avoid redundant transitions between unauthorized states + ( + SignInStatus::Unauthorized, + request::SignInStatus::NotAuthorized { .. }, + ) => false, + + // Allow all other transitions + _ => true, + }; + + if should_update { + this.update_sign_in_status(status, cx); + } + } + }) + .ok(); + } + } + }) .detach(); let configuration = lsp::DidChangeConfigurationParams { settings: Default::default(), }; - let editor_info = request::SetEditorInfoParams { + let initialization_options = request::InitializationOptions { editor_info: request::EditorInfo { name: "zed".into(), version: env!("CARGO_PKG_VERSION").into(), @@ -517,12 +597,12 @@ impl Copilot { version: "0.0.1".into(), }, }; - let editor_info_json = serde_json::to_value(&editor_info)?; + let init_options_json = serde_json::to_value(&initialization_options)?; let server = cx .update(|cx| { let mut params = server.default_initialize_params(cx); - params.initialization_options = Some(editor_info_json); + params.initialization_options = Some(init_options_json); server.initialize(params, configuration.into(), cx) })? .await?; @@ -533,10 +613,6 @@ impl Copilot { }) .await?; - server - .request::(editor_info) - .await?; - anyhow::Ok((server, status)) }; @@ -578,15 +654,13 @@ impl Copilot { .spawn(async move |this, cx| { let sign_in = async { let sign_in = lsp - .request::( - request::SignInInitiateParams {}, - ) + .request::(request::SignInParams {}) .await?; match sign_in { - request::SignInInitiateResult::AlreadySignedIn { user } => { - Ok(request::SignInStatus::Ok { user: Some(user) }) + request::SignInResult::AlreadySignedIn {} => { + Ok(request::SignInStatus::Ok { user: None }) } - request::SignInInitiateResult::PromptUserDeviceFlow(flow) => { + request::SignInResult::PromptUserDeviceFlow(flow) => { this.update(cx, |this, cx| { if let CopilotServer::Running(RunningCopilotServer { sign_in_status: status, @@ -842,7 +916,7 @@ impl Copilot { where T: ToPointUtf16, { - self.request_completions::(buffer, position, cx) + self.request_completions::(buffer, position, cx) } pub fn completions_cycling( @@ -854,7 +928,7 @@ impl Copilot { where T: ToPointUtf16, { - self.request_completions::(buffer, position, cx) + self.request_completions::(buffer, position, cx) } pub fn accept_completion( @@ -866,12 +940,15 @@ impl Copilot { Ok(server) => server, Err(error) => return Task::ready(Err(error)), }; - let request = - server - .lsp - .request::(request::NotifyAcceptedParams { - uuid: completion.uuid.clone(), - }); + let request = server + .lsp + .request::(lsp::ExecuteCommandParams { + command: "github.copilot.didAcceptCompletionItem".into(), + arguments: vec![serde_json::Value::String(completion.uuid.clone())], + work_done_progress_params: lsp::WorkDoneProgressParams { + work_done_token: None, + }, + }); cx.background_spawn(async move { request.await?; Ok(()) @@ -911,8 +988,8 @@ impl Copilot { where R: 'static + lsp::request::Request< - Params = request::GetCompletionsParams, - Result = request::GetCompletionsResult, + Params = request::TextDocumentInlineCompletionParams, + Result = request::TextDocumentInlineCompletionResult, >, T: ToPointUtf16, { @@ -938,28 +1015,29 @@ impl Copilot { ); let tab_size = settings.tab_size; let hard_tabs = settings.hard_tabs; - let relative_path = buffer - .file() - .map(|file| file.path().to_path_buf()) - .unwrap_or_default(); + // let relative_path = buffer + // .file() + // .map(|file| file.path().to_path_buf()) + // .unwrap_or_default(); cx.background_spawn(async move { let (version, snapshot) = snapshot.await?; let result = lsp - .request::(request::GetCompletionsParams { - doc: request::GetCompletionsDocument { - uri, + .request::(request::TextDocumentInlineCompletionParams { + text_document: request::TextDocumentIdentifier { + uri: uri.clone(), + version: version.try_into().unwrap(), + }, + formatting_options: request::FormattingOptions { tab_size: tab_size.into(), - indent_size: 1, insert_spaces: !hard_tabs, - relative_path: relative_path.to_string_lossy().into(), - position: point_to_lsp(position), - version: version.try_into().unwrap(), }, + position: point_to_lsp(position), + context: request::InlineCompletionContext { trigger_kind: 2 }, }) .await?; let completions = result - .completions + .items .into_iter() .map(|completion| { let start = snapshot @@ -967,9 +1045,16 @@ impl Copilot { let end = snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left); Completion { - uuid: completion.uuid, + uuid: completion + .command + .arguments + .as_ref() + .and_then(|args| args.get(0)) + .and_then(|val| val.as_str()) + .map(String::from) + .unwrap_or_default(), range: snapshot.anchor_before(start)..snapshot.anchor_after(end), - text: completion.text, + text: completion.insert_text, } }) .collect(); @@ -1208,10 +1293,8 @@ mod tests { ); // Ensure all previously-registered buffers are re-opened when signing in. - lsp.set_request_handler::(|_, _| async { - Ok(request::SignInInitiateResult::AlreadySignedIn { - user: "user-1".into(), - }) + lsp.set_request_handler::(|_, _| async { + Ok(request::SignInResult::AlreadySignedIn {}) }); copilot .update(cx, |copilot, cx| copilot.sign_in(cx)) diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index ff636178753b11..d5cd6e1b547123 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -1003,16 +1003,20 @@ mod tests { .unwrap(); let mut copilot_requests = copilot_lsp - .set_request_handler::( + .set_request_handler::( move |_params, _cx| async move { - Ok(crate::request::GetCompletionsResult { - completions: vec![crate::request::Completion { - text: "next line".into(), + Ok(crate::request::TextDocumentInlineCompletionResult { + items: vec![crate::request::InlineCompletionItem { + insert_text: "next line".into(), range: lsp::Range::new( lsp::Position::new(1, 0), lsp::Position::new(1, 0), ), - ..Default::default() + command: lsp::Command { + command: "github.copilot.didAcceptCompletionItem".into(), + arguments: Some(vec!["uuid".into()]), + title: "Copilot".to_string(), + }, }], }) }, @@ -1044,20 +1048,44 @@ mod tests { completions: Vec, completions_cycling: Vec, ) { - lsp.set_request_handler::(move |_params, _cx| { - let completions = completions.clone(); - async move { - Ok(crate::request::GetCompletionsResult { - completions: completions.clone(), - }) - } - }); - lsp.set_request_handler::( + lsp.set_request_handler::( + move |_params, _cx| { + let completions = completions + .clone() + .into_iter() + .map(|completion| crate::request::InlineCompletionItem { + insert_text: completion.text.clone(), + range: completion.range, + command: lsp::Command { + command: "github.copilot.didAcceptCompletionItem".into(), + arguments: Some(vec![json!(completion.uuid.to_string())]), + title: "Copilot".to_string(), + }, + }) + .collect::>(); + async move { + Ok(crate::request::TextDocumentInlineCompletionResult { items: completions }) + } + }, + ); + lsp.set_request_handler::( move |_params, _cx| { - let completions_cycling = completions_cycling.clone(); + let completions_cycling = completions_cycling + .clone() + .into_iter() + .map(|completion| crate::request::InlineCompletionItem { + insert_text: completion.text.clone(), + range: completion.range, + command: lsp::Command { + command: "github.copilot.didAcceptCompletionItem".into(), + arguments: Some(vec![json!(completion.uuid.to_string())]), + title: "Copilot".to_string(), + }, + }) + .collect::>(); async move { - Ok(crate::request::GetCompletionsResult { - completions: completions_cycling.clone(), + Ok(crate::request::TextDocumentInlineCompletionResult { + items: completions_cycling, }) } }, diff --git a/crates/copilot/src/request.rs b/crates/copilot/src/request.rs index 0deabe16d15c4a..b4c4daadcec0fc 100644 --- a/crates/copilot/src/request.rs +++ b/crates/copilot/src/request.rs @@ -1,5 +1,46 @@ use serde::{Deserialize, Serialize}; +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Completion { + pub text: String, + pub position: lsp::Position, + pub uuid: String, + pub range: lsp::Range, + pub display_text: String, +} +pub enum NotifyAccepted {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NotifyAcceptedParams { + pub uuid: String, +} + +impl lsp::request::Request for NotifyAccepted { + type Params = NotifyAcceptedParams; + type Result = String; + const METHOD: &'static str = "notifyAccepted"; +} + +pub enum NotifyRejected {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NotifyRejectedParams { + pub uuids: Vec, +} + +impl lsp::request::Request for NotifyRejected { + type Params = NotifyRejectedParams; + type Result = String; + const METHOD: &'static str = "notifyRejected"; +} + +// LSP 3.18 and custom Copilot requests/notifications + +// Authentication + pub enum CheckStatus {} #[derive(Debug, Serialize, Deserialize)] @@ -14,15 +55,15 @@ impl lsp::request::Request for CheckStatus { const METHOD: &'static str = "checkStatus"; } -pub enum SignInInitiate {} +pub enum SignIn {} #[derive(Debug, Serialize, Deserialize)] -pub struct SignInInitiateParams {} +pub struct SignInParams {} #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "status")] -pub enum SignInInitiateResult { - AlreadySignedIn { user: String }, +pub enum SignInResult { + AlreadySignedIn {}, PromptUserDeviceFlow(PromptUserDeviceFlow), } @@ -31,12 +72,16 @@ pub enum SignInInitiateResult { pub struct PromptUserDeviceFlow { pub user_code: String, pub verification_uri: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_in: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub interval: Option, } -impl lsp::request::Request for SignInInitiate { - type Params = SignInInitiateParams; - type Result = SignInInitiateResult; - const METHOD: &'static str = "signInInitiate"; +impl lsp::request::Request for SignIn { + type Params = SignInParams; + type Result = SignInResult; + const METHOD: &'static str = "signIn"; } pub enum SignInConfirm {} @@ -88,138 +133,136 @@ impl lsp::request::Request for SignOut { const METHOD: &'static str = "signOut"; } -pub enum GetCompletions {} +// Initialization #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct GetCompletionsParams { - pub doc: GetCompletionsDocument, +pub struct EditorInfo { + pub name: String, + pub version: String, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct GetCompletionsDocument { - pub tab_size: u32, - pub indent_size: u32, - pub insert_spaces: bool, - pub uri: lsp::Url, - pub relative_path: String, - pub position: lsp::Position, - pub version: usize, +pub struct InitializationOptions { + pub editor_info: EditorInfo, + pub editor_plugin_info: EditorPluginInfo, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct GetCompletionsResult { - pub completions: Vec, +pub struct EditorPluginInfo { + pub name: String, + pub version: String, } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +// Status Notification +pub enum DidChangeStatus {} + +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct Completion { - pub text: String, - pub position: lsp::Position, - pub uuid: String, - pub range: lsp::Range, - pub display_text: String, +pub struct DidChangeStatusParams { + pub message: String, + pub kind: String, // 'Normal', 'Error', 'Warning', 'Inactive' } -impl lsp::request::Request for GetCompletions { - type Params = GetCompletionsParams; - type Result = GetCompletionsResult; - const METHOD: &'static str = "getCompletions"; +impl lsp::notification::Notification for DidChangeStatus { + type Params = DidChangeStatusParams; + const METHOD: &'static str = "didChangeStatus"; } -pub enum GetCompletionsCycling {} +// Inline Completions +pub enum TextDocumentInlineCompletion {} -impl lsp::request::Request for GetCompletionsCycling { - type Params = GetCompletionsParams; - type Result = GetCompletionsResult; - const METHOD: &'static str = "getCompletionsCycling"; +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TextDocumentInlineCompletionParams { + pub text_document: TextDocumentIdentifier, + pub position: lsp::Position, + pub context: InlineCompletionContext, + pub formatting_options: FormattingOptions, } -pub enum LogMessage {} - #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct LogMessageParams { - pub level: u8, - pub message: String, - pub metadata_str: String, - pub extra: Vec, +pub struct TextDocumentIdentifier { + pub uri: lsp::Url, + pub version: usize, } -impl lsp::notification::Notification for LogMessage { - type Params = LogMessageParams; - const METHOD: &'static str = "LogMessage"; +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InlineCompletionContext { + pub trigger_kind: u32, } -pub enum StatusNotification {} - #[derive(Debug, Serialize, Deserialize)] -pub struct StatusNotificationParams { - pub message: String, - pub status: String, // One of Normal/InProgress +#[serde(rename_all = "camelCase")] +pub struct FormattingOptions { + pub tab_size: u32, + pub insert_spaces: bool, } -impl lsp::notification::Notification for StatusNotification { - type Params = StatusNotificationParams; - const METHOD: &'static str = "statusNotification"; +#[derive(Debug, Serialize, Deserialize)] +pub struct TextDocumentInlineCompletionResult { + pub items: Vec, } -pub enum SetEditorInfo {} - -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SetEditorInfoParams { - pub editor_info: EditorInfo, - pub editor_plugin_info: EditorPluginInfo, +pub struct InlineCompletionItem { + pub insert_text: String, + pub range: lsp::Range, + pub command: lsp::Command, } -impl lsp::request::Request for SetEditorInfo { - type Params = SetEditorInfoParams; - type Result = String; - const METHOD: &'static str = "setEditorInfo"; +impl lsp::request::Request for TextDocumentInlineCompletion { + type Params = TextDocumentInlineCompletionParams; + type Result = TextDocumentInlineCompletionResult; + const METHOD: &'static str = "textDocument/inlineCompletion"; } +// Show Completion Notification +pub enum TextDocumentDidShowCompletion {} + #[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct EditorInfo { - pub name: String, - pub version: String, +pub struct TextDocumentDidShowCompletionParams { + pub item: InlineCompletionItem, } -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct EditorPluginInfo { - pub name: String, - pub version: String, +impl lsp::notification::Notification for TextDocumentDidShowCompletion { + type Params = TextDocumentDidShowCompletionParams; + const METHOD: &'static str = "textDocument/didShowCompletion"; } -pub enum NotifyAccepted {} +// Partially Accept Completion Notification +pub enum TextDocumentDidPartiallyAcceptCompletion {} #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct NotifyAcceptedParams { - pub uuid: String, +pub struct TextDocumentDidPartiallyAcceptCompletionParams { + pub item: InlineCompletionItem, + pub accepted_length: u32, } -impl lsp::request::Request for NotifyAccepted { - type Params = NotifyAcceptedParams; - type Result = String; - const METHOD: &'static str = "notifyAccepted"; +impl lsp::notification::Notification for TextDocumentDidPartiallyAcceptCompletion { + type Params = TextDocumentDidPartiallyAcceptCompletionParams; + const METHOD: &'static str = "textDocument/didPartiallyAcceptCompletion"; } -pub enum NotifyRejected {} +// Panel Completions +pub enum TextDocumentCopilotPanelCompletion {} #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct NotifyRejectedParams { - pub uuids: Vec, +pub struct TextDocumentCopilotPanelCompletionParams { + pub text_document: TextDocumentIdentifier, + pub position: lsp::Position, + pub partial_result_token: Option, } -impl lsp::request::Request for NotifyRejected { - type Params = NotifyRejectedParams; - type Result = String; - const METHOD: &'static str = "notifyRejected"; +impl lsp::request::Request for TextDocumentCopilotPanelCompletion { + type Params = TextDocumentCopilotPanelCompletionParams; + type Result = TextDocumentInlineCompletionResult; + const METHOD: &'static str = "textDocument/copilotPanelCompletion"; } diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 8b29ab6298ef58..6166fd02669795 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -27,7 +27,6 @@ const MAX_STORED_LOG_ENTRIES: usize = 2000; pub struct LogStore { projects: HashMap, ProjectState>, language_servers: HashMap, - copilot_log_subscription: Option, _copilot_subscription: Option, io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>, } @@ -241,23 +240,6 @@ impl LogStore { cx.subscribe(copilot, |this, copilot, inline_completion_event, cx| { if let copilot::Event::CopilotLanguageServerStarted = inline_completion_event { if let Some(server) = copilot.read(cx).language_server() { - let server_id = server.server_id(); - let weak_this = cx.weak_entity(); - this.copilot_log_subscription = - Some(server.on_notification::( - move |params, cx| { - weak_this - .update(cx, |this, cx| { - this.add_language_server_log( - server_id, - MessageType::LOG, - ¶ms.message, - cx, - ); - }) - .ok(); - }, - )); let name = LanguageServerName::new_static("copilot"); this.add_language_server( LanguageServerKind::Global, @@ -273,7 +255,6 @@ impl LogStore { }); let this = Self { - copilot_log_subscription: None, _copilot_subscription: copilot_subscription, projects: HashMap::default(), language_servers: HashMap::default(),