1092 lines · 40.8 KB · Rust
RawDownload
use crate::{Completion, Copilot};use anyhow::Result;use gpui::{AppContext, EntityId, Model, ModelContext, Task};use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider};use language::{    language_settings::{all_language_settings, AllLanguageSettings},    Buffer, OffsetRangeExt, ToOffset,};use settings::Settings;use std::{path::Path, time::Duration}; pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub struct CopilotCompletionProvider {    cycled: bool,    buffer_id: Option<EntityId>,    completions: Vec<Completion>,    active_completion_index: usize,    file_extension: Option<String>,    pending_refresh: Task<Result<()>>,    pending_cycling_refresh: Task<Result<()>>,    copilot: Model<Copilot>,} impl CopilotCompletionProvider {    pub fn new(copilot: Model<Copilot>) -> Self {        Self {            cycled: false,            buffer_id: None,            completions: Vec::new(),            active_completion_index: 0,            file_extension: None,            pending_refresh: Task::ready(Ok(())),            pending_cycling_refresh: Task::ready(Ok(())),            copilot,        }    }     fn active_completion(&self) -> Option<&Completion> {        self.completions.get(self.active_completion_index)    }     fn push_completion(&mut self, new_completion: Completion) {        for completion in &self.completions {            if completion.text == new_completion.text && completion.range == new_completion.range {                return;            }        }        self.completions.push(new_completion);    }} impl InlineCompletionProvider for CopilotCompletionProvider {    fn name() -> &'static str {        "copilot"    }     fn is_enabled(        &self,        buffer: &Model<Buffer>,        cursor_position: language::Anchor,        cx: &AppContext,    ) -> bool {        if !self.copilot.read(cx).status().is_authorized() {            return false;        }         let buffer = buffer.read(cx);        let file = buffer.file();        let language = buffer.language_at(cursor_position);        let settings = all_language_settings(file, cx);        settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx)    }     fn refresh(        &mut self,        buffer: Model<Buffer>,        cursor_position: language::Anchor,        debounce: bool,        cx: &mut ModelContext<Self>,    ) {        let copilot = self.copilot.clone();        self.pending_refresh = cx.spawn(|this, mut cx| async move {            if debounce {                cx.background_executor()                    .timer(COPILOT_DEBOUNCE_TIMEOUT)                    .await;            }             let completions = copilot                .update(&mut cx, |copilot, cx| {                    copilot.completions(&buffer, cursor_position, cx)                })?                .await?;             this.update(&mut cx, |this, cx| {                if !completions.is_empty() {                    this.cycled = false;                    this.pending_cycling_refresh = Task::ready(Ok(()));                    this.completions.clear();                    this.active_completion_index = 0;                    this.buffer_id = Some(buffer.entity_id());                    this.file_extension = buffer.read(cx).file().and_then(|file| {                        Some(                            Path::new(file.file_name(cx))                                .extension()?                                .to_str()?                                .to_string(),                        )                    });                     for completion in completions {                        this.push_completion(completion);                    }                    cx.notify();                }            })?;             Ok(())        });    }     fn cycle(        &mut self,        buffer: Model<Buffer>,        cursor_position: language::Anchor,        direction: Direction,        cx: &mut ModelContext<Self>,    ) {        if self.cycled {            match direction {                Direction::Prev => {                    self.active_completion_index = if self.active_completion_index == 0 {                        self.completions.len().saturating_sub(1)                    } else {                        self.active_completion_index - 1                    };                }                Direction::Next => {                    if self.completions.is_empty() {                        self.active_completion_index = 0                    } else {                        self.active_completion_index =                            (self.active_completion_index + 1) % self.completions.len();                    }                }            }             cx.notify();        } else {            let copilot = self.copilot.clone();            self.pending_cycling_refresh = cx.spawn(|this, mut cx| async move {                let completions = copilot                    .update(&mut cx, |copilot, cx| {                        copilot.completions_cycling(&buffer, cursor_position, cx)                    })?                    .await?;                 this.update(&mut cx, |this, cx| {                    this.cycled = true;                    this.file_extension = buffer.read(cx).file().and_then(|file| {                        Some(                            Path::new(file.file_name(cx))                                .extension()?                                .to_str()?                                .to_string(),                        )                    });                    for completion in completions {                        this.push_completion(completion);                    }                    this.cycle(buffer, cursor_position, direction, cx);                })?;                 Ok(())            });        }    }     fn accept(&mut self, cx: &mut ModelContext<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, cx: &mut ModelContext<Self>) {        let settings = AllLanguageSettings::get_global(cx);         let copilot_enabled = settings.inline_completions_enabled(None, None, cx);         if !copilot_enabled {            return;        }         self.copilot            .update(cx, |copilot, cx| {                copilot.discard_completions(&self.completions, cx)            })            .detach_and_log_err(cx);    }     fn suggest(        &mut self,        buffer: &Model<Buffer>,        cursor_position: language::Anchor,        cx: &mut ModelContext<Self>,    ) -> Option<InlineCompletion> {        let buffer_id = buffer.entity_id();        let buffer = buffer.read(cx);        let completion = self.active_completion()?;        if Some(buffer_id) != self.buffer_id            || !completion.range.start.is_valid(buffer)            || !completion.range.end.is_valid(buffer)        {            return None;        }         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);         if completion_range.is_empty()            && completion_range.start == cursor_position.to_offset(buffer)        {            let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];            if completion_text.trim().is_empty() {                None            } else {                let position = cursor_position.bias_right(buffer);                Some(InlineCompletion {                    edits: vec![(position..position, completion_text.into())],                })            }        } else {            None        }    }} 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 editor::{        test::editor_lsp_test_context::EditorLspTestContext, Editor, ExcerptRange, MultiBuffer,    };    use fs::FakeFs;    use futures::StreamExt;    use gpui::{BackgroundExecutor, Context, TestAppContext, UpdateGlobal};    use indoc::indoc;    use language::{        language_settings::{AllLanguageSettings, AllLanguageSettingsContent},        Point,    };    use project::Project;    use serde_json::json;    use settings::SettingsStore;    use std::future::Future;    use util::test::{marked_text_ranges_by, TextRangeMarker};     #[gpui::test(iterations = 10)]    async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {        // flaky        init_test(cx, |_| {});         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_model(|_| CopilotCompletionProvider::new(copilot));        cx.update_editor(|editor, cx| {            editor.set_inline_completion_provider(Some(copilot_provider), 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::Completion {                text: "one.copilot1".into(),                range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),                ..Default::default()            }],            vec![],        );        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);        cx.update_editor(|editor, cx| {            // We want to show both: the inline completion and the completion menu            assert!(editor.context_menu_visible());            assert!(editor.has_active_inline_completion());             // 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_inline_completion());            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 test that accepting completions works        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::Completion {                text: "one.copilot1".into(),                range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),                ..Default::default()            }],            vec![],        );        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);        cx.update_editor(|editor, cx| {            assert!(editor.context_menu_visible());            assert!(editor.has_active_inline_completion());            assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");            assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");        });         // Ensure existing inline completion 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_inline_completion());            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::Completion {                text: "one.copilot2".into(),                range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),                ..Default::default()            }],            vec![],        );        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);        cx.update_editor(|editor, cx| {            assert!(!editor.context_menu_visible());            assert!(editor.has_active_inline_completion());            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_inline_completion());            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_inline_completion());            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_inline_completion());            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_inline_completion());            assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");            assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");             // AcceptInlineCompletion when there is an active suggestion inserts it.            editor.accept_inline_completion(&Default::default(), cx);            assert!(!editor.has_active_inline_completion());            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_inline_completion());            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_inline_completion());            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_inline_completion());            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![crate::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_inline_completion(&Default::default(), cx));        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);        cx.update_editor(|editor, cx| {            assert!(editor.has_active_inline_completion());            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_inline_completion());            assert_eq!(editor.text(cx), "fn foo() {\n    \n}");            assert_eq!(editor.display_text(cx), "fn foo() {\n    let x = 4;\n}");             // Using AcceptInlineCompletion again accepts the suggestion.            editor.accept_inline_completion(&Default::default(), cx);            assert!(!editor.has_active_inline_completion());            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(iterations = 10)]    async fn test_accept_partial_copilot_suggestion(        executor: BackgroundExecutor,        cx: &mut TestAppContext,    ) {        // flaky        init_test(cx, |_| {}); 

Lines 1–500 of 1092.