1121 lines · 42.4 KB · Rust
RawDownload
use crate::{    CompletionSource, Copilot, CopilotEditPrediction,    request::{        DidShowCompletion, DidShowCompletionParams, DidShowInlineEdit, DidShowInlineEditParams,        InlineCompletionItem,    },};use anyhow::Result;use edit_prediction_types::{    EditPrediction, EditPredictionDelegate, EditPredictionDiscardReason, EditPredictionIconSet,    interpolate_edits,};use gpui::{App, Context, Entity, Task};use icons::IconName;use language::{Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToPointUtf16};use std::{ops::Range, sync::Arc, time::Duration}; pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub struct CopilotEditPredictionDelegate {    completion: Option<(CopilotEditPrediction, EditPreview)>,    pending_refresh: Option<Task<Result<()>>>,    copilot: Entity<Copilot>,} impl CopilotEditPredictionDelegate {    pub fn new(copilot: Entity<Copilot>) -> Self {        Self {            completion: None,            pending_refresh: None,            copilot,        }    }     fn active_completion(&self) -> Option<&(CopilotEditPrediction, EditPreview)> {        self.completion.as_ref()    }} impl EditPredictionDelegate for CopilotEditPredictionDelegate {    fn name() -> &'static str {        "copilot"    }     fn display_name() -> &'static str {        "Copilot"    }     fn show_predictions_in_menu() -> bool {        true    }     fn show_tab_accept_marker() -> bool {        true    }     fn icons(&self, _cx: &App) -> EditPredictionIconSet {        EditPredictionIconSet::new(IconName::Copilot)            .with_disabled(IconName::CopilotDisabled)            .with_error(IconName::CopilotError)    }     fn is_refreshing(&self, _cx: &App) -> bool {        self.pending_refresh.is_some() && self.completion.is_none()    }     fn is_enabled(        &self,        _buffer: &Entity<Buffer>,        _cursor_position: language::Anchor,        cx: &App,    ) -> bool {        self.copilot.read(cx).status().is_authorized()    }     fn refresh(        &mut self,        buffer: Entity<Buffer>,        cursor_position: language::Anchor,        debounce: bool,        cx: &mut Context<Self>,    ) {        let copilot = self.copilot.clone();        self.pending_refresh = Some(cx.spawn(async move |this, cx| {            if debounce {                cx.background_executor()                    .timer(COPILOT_DEBOUNCE_TIMEOUT)                    .await;            }             let completions = copilot                .update(cx, |copilot, cx| {                    copilot.completions(&buffer, cursor_position, cx)                })                .await?;             if let Some(mut completion) = completions.into_iter().next()                && let Some((trimmed_range, trimmed_text, snapshot)) =                    cx.update(|cx| trim_completion(&completion, cx))            {                let preview = buffer                    .update(cx, |this, cx| {                        this.preview_edits(                            Arc::from([(trimmed_range.clone(), trimmed_text.clone())].as_slice()),                            cx,                        )                    })                    .await;                this.update(cx, |this, cx| {                    this.pending_refresh = None;                    completion.range = trimmed_range;                    completion.text = trimmed_text.to_string();                    completion.snapshot = snapshot;                    this.completion = Some((completion, preview));                     cx.notify();                })?;            }             Ok(())        }));    }     fn accept(&mut self, cx: &mut Context<Self>) {        if let Some((completion, _)) = self.active_completion() {            self.copilot                .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))                .detach_and_log_err(cx);        }    }     fn discard(&mut self, _reason: EditPredictionDiscardReason, _: &mut Context<Self>) {}     fn suggest(        &mut self,        buffer: &Entity<Buffer>,        _: language::Anchor,        cx: &mut Context<Self>,    ) -> Option<EditPrediction> {        let buffer_id = buffer.entity_id();        let buffer = buffer.read(cx);        let (completion, edit_preview) = self.active_completion()?;         if Some(buffer_id) != Some(completion.buffer.entity_id())            || !completion.range.start.is_valid(buffer)            || !completion.range.end.is_valid(buffer)        {            return None;        }        let edits = vec![(            completion.range.clone(),            Arc::from(completion.text.as_ref()),        )];        let edits = interpolate_edits(&completion.snapshot, &buffer.snapshot(), &edits)            .filter(|edits| !edits.is_empty())?;        self.copilot.update(cx, |this, _| {            if let Ok(server) = this.server.as_authenticated() {                match completion.source {                    CompletionSource::NextEditSuggestion => {                        if let Some(cmd) = completion.command.as_ref() {                            _ = server                                .lsp                                .notify::<DidShowInlineEdit>(DidShowInlineEditParams {                                    item: serde_json::json!({"command": {"arguments": cmd.arguments}}),                                });                        }                    }                    CompletionSource::InlineCompletion => {                        _ = server.lsp.notify::<DidShowCompletion>(DidShowCompletionParams {                            item: InlineCompletionItem {                                insert_text: completion.text.clone(),                                range: lsp::Range::new(                                    language::point_to_lsp(                                        completion.range.start.to_point_utf16(&completion.snapshot),                                    ),                                    language::point_to_lsp(                                        completion.range.end.to_point_utf16(&completion.snapshot),                                    ),                                ),                                command: completion.command.clone(),                            },                        });                    }                }            }        });        Some(EditPrediction::Local {            id: None,            edits,            cursor_position: None,            edit_preview: Some(edit_preview.clone()),        })    }} fn trim_completion(    completion: &CopilotEditPrediction,    cx: &mut App,) -> Option<(Range<Anchor>, Arc<str>, BufferSnapshot)> {    let buffer = completion.buffer.read(cx);    let mut completion_range = completion.range.to_offset(buffer);    let prefix_len = common_prefix(        buffer.chars_for_range(completion_range.clone()),        completion.text.chars(),    );    completion_range.start += prefix_len;    let suffix_len = common_prefix(        buffer.reversed_chars_for_range(completion_range.clone()),        completion.text[prefix_len..].chars().rev(),    );    completion_range.end = completion_range.end.saturating_sub(suffix_len);    let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];    if completion_text.trim().is_empty() {        None    } else {        let snapshot = buffer.snapshot();        let completion_range = snapshot.anchor_after(completion_range.start)            ..snapshot.anchor_after(completion_range.end);         Some((completion_range, Arc::from(completion_text), snapshot))    }} fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {    a.zip(b)        .take_while(|(a, b)| a == b)        .map(|(a, _)| a.len_utf8())        .sum()} #[cfg(test)]mod tests {    use super::*;    use edit_prediction_types::EditPredictionGranularity;    use editor::{        Editor, MultiBuffer, MultiBufferOffset, PathKey, SelectionEffects,        test::{editor_content_with_blocks, editor_lsp_test_context::EditorLspTestContext},    };    use fs::FakeFs;    use futures::StreamExt;    use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal};    use indoc::indoc;    use language::{        Point,        language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode},    };    use lsp::Uri;    use project::Project;    use serde_json::json;    use settings::{AllLanguageSettingsContent, SettingsStore};    use std::future::Future;    use util::{        path,        test::{TextRangeMarker, marked_text_ranges_by},    };     #[gpui::test(iterations = 10)]    async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {        // flaky        init_test(cx, |settings| {            settings.defaults.completions = Some(CompletionSettingsContent {                words: Some(WordsCompletionMode::Disabled),                words_min_length: Some(0),                lsp_insert_mode: Some(LspInsertMode::Insert),                ..Default::default()            });        });         let (copilot, copilot_lsp) = Copilot::fake(cx);        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;        let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));        cx.update_editor(|editor, window, cx| {            editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)        });         cx.set_state(indoc! {"            oneˇ            two            three        "});        cx.simulate_keystroke(".");        drop(handle_completion_request(            &mut cx,            indoc! {"                one.|<>                two                three            "},            vec!["completion_a", "completion_b"],        ));        handle_copilot_completion_request(            &copilot_lsp,            vec![crate::request::NextEditSuggestion {                text: "one.copilot1".into(),                range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),                command: None,                text_document: lsp::VersionedTextDocumentIdentifier {                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),                    version: 0,                },            }],        );        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);        cx.update_editor(|editor, window, cx| {            assert!(editor.context_menu_visible());            assert!(editor.has_active_edit_prediction());            // Since we have both, the copilot suggestion is existing but does not show up as ghost text            assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");            assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n");             // Confirming a non-copilot completion inserts it and hides the context menu, without showing            // the copilot suggestion afterwards.            editor                .confirm_completion(&Default::default(), window, cx)                .unwrap()                .detach();            assert!(!editor.context_menu_visible());            assert!(!editor.has_active_edit_prediction());            assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");            assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");        });         // Reset editor and only return copilot suggestions        cx.set_state(indoc! {"            oneˇ            two            three        "});        cx.simulate_keystroke(".");         drop(handle_completion_request(            &mut cx,            indoc! {"                one.|<>                two                three            "},            vec![],        ));        handle_copilot_completion_request(            &copilot_lsp,            vec![crate::request::NextEditSuggestion {                text: "one.copilot1".into(),                range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),                command: None,                text_document: lsp::VersionedTextDocumentIdentifier {                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),                    version: 0,                },            }],        );        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);        cx.update_editor(|editor, _, cx| {            assert!(!editor.context_menu_visible());            assert!(editor.has_active_edit_prediction());            // Since only the copilot is available, it's shown inline            assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");            assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");        });         // Ensure existing edit prediction is interpolated when inserting again.        cx.simulate_keystroke("c");        executor.run_until_parked();        cx.update_editor(|editor, _, cx| {            assert!(!editor.context_menu_visible());            assert!(editor.has_active_edit_prediction());            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![crate::request::NextEditSuggestion {                text: "one.copilot2".into(),                range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),                command: None,                text_document: lsp::VersionedTextDocumentIdentifier {                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),                    version: 0,                },            }],        );        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);        cx.update_editor(|editor, window, cx| {            assert!(!editor.context_menu_visible());            assert!(editor.has_active_edit_prediction());            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(), window, cx);            assert!(!editor.has_active_edit_prediction());            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(), window, cx);            assert!(!editor.has_active_edit_prediction());            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(), window, cx);            assert!(editor.has_active_edit_prediction());            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, window, cx| {            assert!(editor.has_active_edit_prediction());            assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");            assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");             // AcceptEditPrediction when there is an active suggestion inserts it.            editor.accept_edit_prediction(&Default::default(), window, cx);            assert!(!editor.has_active_edit_prediction());            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(), window, cx);            assert!(editor.has_active_edit_prediction());            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(), window, cx);            assert!(!editor.has_active_edit_prediction());            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_edit_prediction());            assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");            assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");        });    }     #[gpui::test(iterations = 10)]    async fn test_accept_partial_copilot_suggestion(        executor: BackgroundExecutor,        cx: &mut TestAppContext,    ) {        // flaky        init_test(cx, |settings| {            settings.defaults.completions = Some(CompletionSettingsContent {                words: Some(WordsCompletionMode::Disabled),                words_min_length: Some(0),                lsp_insert_mode: Some(LspInsertMode::Insert),                ..Default::default()            });        });         let (copilot, copilot_lsp) = Copilot::fake(cx);        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;        let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));        cx.update_editor(|editor, window, cx| {            editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)        });         // Setup the editor with a completion request.        cx.set_state(indoc! {"            oneˇ            two            three        "});        cx.simulate_keystroke(".");        drop(handle_completion_request(            &mut cx,            indoc! {"                one.|<>                two                three

Lines 1–500 of 1121.