mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-29 19:14:13 +00:00
Remove Supermaven-related code from Zed (#50537)
Follow-up of https://github.com/zed-industries/zed/pull/49317 See also https://supermaven.com/blog/sunsetting-supermaven - N/A
This commit is contained in:
parent
1f11592d93
commit
7ad524661d
27 changed files with 5 additions and 1390 deletions
2
.github/CODEOWNERS.hold
vendored
2
.github/CODEOWNERS.hold
vendored
|
|
@ -62,8 +62,6 @@
|
|||
/crates/rules_library/ @zed-industries/ai-team
|
||||
# SUGGESTED: Review needed - based on Richard Feldman (2 commits)
|
||||
/crates/shell_command_parser/ @zed-industries/ai-team
|
||||
/crates/supermaven/ @zed-industries/ai-team
|
||||
/crates/supermaven_api/ @zed-industries/ai-team
|
||||
/crates/vercel/ @zed-industries/ai-team
|
||||
/crates/x_ai/ @zed-industries/ai-team
|
||||
/crates/zeta_prompt/ @zed-industries/ai-team
|
||||
|
|
|
|||
45
Cargo.lock
generated
45
Cargo.lock
generated
|
|
@ -5403,7 +5403,6 @@ dependencies = [
|
|||
"semver",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"supermaven",
|
||||
"telemetry",
|
||||
"text",
|
||||
"theme",
|
||||
|
|
@ -16524,49 +16523,6 @@ dependencies = [
|
|||
"ztracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "supermaven"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"edit_prediction_types",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language",
|
||||
"log",
|
||||
"postage",
|
||||
"project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"supermaven_api",
|
||||
"text",
|
||||
"theme",
|
||||
"ui",
|
||||
"unicode-segmentation",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "supermaven_api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures 0.3.31",
|
||||
"http_client",
|
||||
"paths",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval"
|
||||
version = "2.15.0"
|
||||
|
|
@ -21861,7 +21817,6 @@ dependencies = [
|
|||
"smol",
|
||||
"snippet_provider",
|
||||
"snippets_ui",
|
||||
"supermaven",
|
||||
"svg_preview",
|
||||
"sysinfo 0.37.2",
|
||||
"system_specs",
|
||||
|
|
|
|||
|
|
@ -182,8 +182,6 @@ members = [
|
|||
"crates/storybook",
|
||||
"crates/streaming_diff",
|
||||
"crates/sum_tree",
|
||||
"crates/supermaven",
|
||||
"crates/supermaven_api",
|
||||
"crates/svg_preview",
|
||||
"crates/system_specs",
|
||||
"crates/tab_switcher",
|
||||
|
|
@ -427,8 +425,6 @@ sqlez_macros = { path = "crates/sqlez_macros" }
|
|||
story = { path = "crates/story" }
|
||||
streaming_diff = { path = "crates/streaming_diff" }
|
||||
sum_tree = { path = "crates/sum_tree" }
|
||||
supermaven = { path = "crates/supermaven" }
|
||||
supermaven_api = { path = "crates/supermaven_api" }
|
||||
codestral = { path = "crates/codestral" }
|
||||
system_specs = { path = "crates/system_specs" }
|
||||
tab_switcher = { path = "crates/tab_switcher" }
|
||||
|
|
@ -900,7 +896,6 @@ sidebar = { codegen-units = 1 }
|
|||
snippet = { codegen-units = 1 }
|
||||
snippets_ui = { codegen-units = 1 }
|
||||
story = { codegen-units = 1 }
|
||||
supermaven_api = { codegen-units = 1 }
|
||||
telemetry_events = { codegen-units = 1 }
|
||||
theme_selector = { codegen-units = 1 }
|
||||
time_format = { codegen-units = 1 }
|
||||
|
|
|
|||
|
|
@ -371,7 +371,6 @@ fn update_command_palette_filter(cx: &mut App) {
|
|||
filter.hide_namespace("agents");
|
||||
filter.hide_namespace("assistant");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.hide_namespace("supermaven");
|
||||
filter.hide_namespace("zed_predict_onboarding");
|
||||
filter.hide_namespace("edit_prediction");
|
||||
|
||||
|
|
@ -392,19 +391,11 @@ fn update_command_palette_filter(cx: &mut App) {
|
|||
EditPredictionProvider::None => {
|
||||
filter.hide_namespace("edit_prediction");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.hide_namespace("supermaven");
|
||||
filter.hide_action_types(&edit_prediction_actions);
|
||||
}
|
||||
EditPredictionProvider::Copilot => {
|
||||
filter.show_namespace("edit_prediction");
|
||||
filter.show_namespace("copilot");
|
||||
filter.hide_namespace("supermaven");
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
}
|
||||
EditPredictionProvider::Supermaven => {
|
||||
filter.show_namespace("edit_prediction");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.show_namespace("supermaven");
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
}
|
||||
EditPredictionProvider::Zed
|
||||
|
|
@ -416,7 +407,6 @@ fn update_command_palette_filter(cx: &mut App) {
|
|||
| EditPredictionProvider::Experimental(_) => {
|
||||
filter.show_namespace("edit_prediction");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.hide_namespace("supermaven");
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1836,7 +1836,6 @@ fn is_ep_store_provider(provider: EditPredictionProvider) -> bool {
|
|||
| EditPredictionProvider::Experimental(_) => true,
|
||||
EditPredictionProvider::None
|
||||
| EditPredictionProvider::Copilot
|
||||
| EditPredictionProvider::Supermaven
|
||||
| EditPredictionProvider::Codestral => false,
|
||||
}
|
||||
}
|
||||
|
|
@ -1877,7 +1876,6 @@ impl EditPredictionStore {
|
|||
EditPredictionProvider::OpenAiCompatibleApi => (false, 2),
|
||||
EditPredictionProvider::None
|
||||
| EditPredictionProvider::Copilot
|
||||
| EditPredictionProvider::Supermaven
|
||||
| EditPredictionProvider::Codestral => {
|
||||
log::error!("queue_prediction_refresh called with non-store provider");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ paths.workspace = true
|
|||
project.workspace = true
|
||||
regex.workspace = true
|
||||
settings.workspace = true
|
||||
supermaven.workspace = true
|
||||
telemetry.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ use std::{
|
|||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
use supermaven::{AccountStatus, Supermaven};
|
||||
use ui::{
|
||||
Clickable, ContextMenu, ContextMenuEntry, DocumentationSide, IconButton, IconButtonShape,
|
||||
Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,
|
||||
|
|
@ -75,13 +74,6 @@ pub struct EditPredictionButton {
|
|||
project: WeakEntity<Project>,
|
||||
}
|
||||
|
||||
enum SupermavenButtonStatus {
|
||||
Ready,
|
||||
Errored(String),
|
||||
NeedsActivation(String),
|
||||
Initializing,
|
||||
}
|
||||
|
||||
impl Render for EditPredictionButton {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
// Return empty div if AI is disabled
|
||||
|
|
@ -188,101 +180,6 @@ impl Render for EditPredictionButton {
|
|||
.with_handle(self.popover_menu_handle.clone()),
|
||||
)
|
||||
}
|
||||
EditPredictionProvider::Supermaven => {
|
||||
let Some(supermaven) = Supermaven::global(cx) else {
|
||||
return div();
|
||||
};
|
||||
|
||||
let supermaven = supermaven.read(cx);
|
||||
|
||||
let status = match supermaven {
|
||||
Supermaven::Starting => SupermavenButtonStatus::Initializing,
|
||||
Supermaven::FailedDownload { error } => {
|
||||
SupermavenButtonStatus::Errored(error.to_string())
|
||||
}
|
||||
Supermaven::Spawned(agent) => {
|
||||
let account_status = agent.account_status.clone();
|
||||
match account_status {
|
||||
AccountStatus::NeedsActivation { activate_url } => {
|
||||
SupermavenButtonStatus::NeedsActivation(activate_url)
|
||||
}
|
||||
AccountStatus::Unknown => SupermavenButtonStatus::Initializing,
|
||||
AccountStatus::Ready => SupermavenButtonStatus::Ready,
|
||||
}
|
||||
}
|
||||
Supermaven::Error { error } => {
|
||||
SupermavenButtonStatus::Errored(error.to_string())
|
||||
}
|
||||
};
|
||||
|
||||
let icon = status.to_icon();
|
||||
let tooltip_text = status.to_tooltip();
|
||||
let has_menu = status.has_menu();
|
||||
let this = cx.weak_entity();
|
||||
let fs = self.fs.clone();
|
||||
let file = self.file.clone();
|
||||
let language = self.language.clone();
|
||||
let project = self.project.clone();
|
||||
|
||||
div().child(
|
||||
PopoverMenu::new("supermaven")
|
||||
.on_open({
|
||||
let file = file.clone();
|
||||
let language = language;
|
||||
let project = project;
|
||||
Rc::new(move |_window, cx| {
|
||||
emit_edit_prediction_menu_opened(
|
||||
"supermaven",
|
||||
&file,
|
||||
&language,
|
||||
&project,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.menu(move |window, cx| match &status {
|
||||
SupermavenButtonStatus::NeedsActivation(activate_url) => {
|
||||
Some(ContextMenu::build(window, cx, |menu, _, _| {
|
||||
let fs = fs.clone();
|
||||
let activate_url = activate_url.clone();
|
||||
|
||||
menu.entry("Sign In", None, move |_, cx| {
|
||||
cx.open_url(activate_url.as_str())
|
||||
})
|
||||
.entry(
|
||||
"Use Zed AI",
|
||||
None,
|
||||
move |_, cx| {
|
||||
set_completion_provider(
|
||||
fs.clone(),
|
||||
cx,
|
||||
EditPredictionProvider::Zed,
|
||||
)
|
||||
},
|
||||
)
|
||||
}))
|
||||
}
|
||||
SupermavenButtonStatus::Ready => this
|
||||
.update(cx, |this, cx| {
|
||||
this.build_supermaven_context_menu(window, cx)
|
||||
})
|
||||
.ok(),
|
||||
_ => None,
|
||||
})
|
||||
.anchor(Corner::BottomRight)
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("supermaven-icon", icon),
|
||||
move |window, cx| {
|
||||
if has_menu {
|
||||
Tooltip::for_action(tooltip_text.clone(), &ToggleMenu, cx)
|
||||
} else {
|
||||
Tooltip::text(tooltip_text.clone())(window, cx)
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_handle(self.popover_menu_handle.clone()),
|
||||
)
|
||||
}
|
||||
EditPredictionProvider::Codestral => {
|
||||
let enabled = self.editor_enabled.unwrap_or(true);
|
||||
let has_api_key = codestral::codestral_api_key(cx).is_some();
|
||||
|
|
@ -1120,21 +1017,6 @@ impl EditPredictionButton {
|
|||
})
|
||||
}
|
||||
|
||||
fn build_supermaven_context_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |menu, window, cx| {
|
||||
let menu = self.build_language_settings_menu(menu, window, cx);
|
||||
let menu =
|
||||
self.add_provider_switching_section(menu, EditPredictionProvider::Supermaven, cx);
|
||||
|
||||
menu.separator()
|
||||
.action("Sign Out", supermaven::SignOut.boxed_clone())
|
||||
})
|
||||
}
|
||||
|
||||
fn build_codestral_context_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
|
|
@ -1384,33 +1266,6 @@ impl StatusItemView for EditPredictionButton {
|
|||
}
|
||||
}
|
||||
|
||||
impl SupermavenButtonStatus {
|
||||
fn to_icon(&self) -> IconName {
|
||||
match self {
|
||||
SupermavenButtonStatus::Ready => IconName::Supermaven,
|
||||
SupermavenButtonStatus::Errored(_) => IconName::SupermavenError,
|
||||
SupermavenButtonStatus::NeedsActivation(_) => IconName::SupermavenInit,
|
||||
SupermavenButtonStatus::Initializing => IconName::SupermavenInit,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_tooltip(&self) -> String {
|
||||
match self {
|
||||
SupermavenButtonStatus::Ready => "Supermaven is ready".to_string(),
|
||||
SupermavenButtonStatus::Errored(error) => format!("Supermaven error: {}", error),
|
||||
SupermavenButtonStatus::NeedsActivation(_) => "Supermaven needs activation".to_string(),
|
||||
SupermavenButtonStatus::Initializing => "Supermaven initializing".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_menu(&self) -> bool {
|
||||
match self {
|
||||
SupermavenButtonStatus::Ready | SupermavenButtonStatus::NeedsActivation(_) => true,
|
||||
SupermavenButtonStatus::Errored(_) | SupermavenButtonStatus::Initializing => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn open_disabled_globs_setting_in_editor(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
|
|
@ -1507,14 +1362,6 @@ pub fn get_available_providers(cx: &mut App) -> Vec<EditPredictionProvider> {
|
|||
providers.push(EditPredictionProvider::Copilot);
|
||||
};
|
||||
|
||||
if let Some(supermaven) = Supermaven::global(cx) {
|
||||
if let Supermaven::Spawned(agent) = supermaven.read(cx) {
|
||||
if matches!(agent.account_status, AccountStatus::Ready) {
|
||||
providers.push(EditPredictionProvider::Supermaven);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if codestral::codestral_api_key(cx).is_some() {
|
||||
providers.push(EditPredictionProvider::Codestral);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28579,7 +28579,7 @@ fn edit_prediction_edit_text(
|
|||
}
|
||||
|
||||
fn edit_prediction_fallback_text(edits: &[(Range<Anchor>, Arc<str>)], cx: &App) -> HighlightedText {
|
||||
// Fallback for providers that don't provide edit_preview (like Copilot/Supermaven)
|
||||
// Fallback for providers that don't provide edit_preview (like Copilot)
|
||||
// Just show the raw edit text with basic styling
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
|
|
|
|||
|
|
@ -225,10 +225,6 @@ pub enum IconName {
|
|||
Star,
|
||||
StarFilled,
|
||||
Stop,
|
||||
Supermaven,
|
||||
SupermavenDisabled,
|
||||
SupermavenError,
|
||||
SupermavenInit,
|
||||
SwatchBook,
|
||||
SweepAi,
|
||||
SweepAiDisabled,
|
||||
|
|
|
|||
|
|
@ -396,8 +396,7 @@ impl InlayHintSettings {
|
|||
}
|
||||
}
|
||||
|
||||
/// The settings for edit predictions, such as [GitHub Copilot](https://github.com/features/copilot)
|
||||
/// or [Supermaven](https://supermaven.com).
|
||||
/// The settings for edit predictions, such as [GitHub Copilot](https://github.com/features/copilot).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct EditPredictionSettings {
|
||||
/// The provider that supplies edit predictions.
|
||||
|
|
|
|||
|
|
@ -419,12 +419,6 @@ pub fn copilot_dir() -> &'static PathBuf {
|
|||
COPILOT_DIR.get_or_init(|| data_dir().join("copilot"))
|
||||
}
|
||||
|
||||
/// Returns the path to the Supermaven directory.
|
||||
pub fn supermaven_dir() -> &'static PathBuf {
|
||||
static SUPERMAVEN_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
SUPERMAVEN_DIR.get_or_init(|| data_dir().join("supermaven"))
|
||||
}
|
||||
|
||||
/// Returns the path to the default Prettier directory.
|
||||
pub fn default_prettier_dir() -> &'static PathBuf {
|
||||
static DEFAULT_PRETTIER_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ pub enum EditPredictionProvider {
|
|||
None,
|
||||
#[default]
|
||||
Copilot,
|
||||
Supermaven,
|
||||
Zed,
|
||||
Codestral,
|
||||
Ollama,
|
||||
|
|
@ -103,7 +102,6 @@ impl<'de> Deserialize<'de> for EditPredictionProvider {
|
|||
pub enum Content {
|
||||
None,
|
||||
Copilot,
|
||||
Supermaven,
|
||||
Zed,
|
||||
Codestral,
|
||||
Ollama,
|
||||
|
|
@ -116,7 +114,6 @@ impl<'de> Deserialize<'de> for EditPredictionProvider {
|
|||
Ok(match Content::deserialize(deserializer)? {
|
||||
Content::None => EditPredictionProvider::None,
|
||||
Content::Copilot => EditPredictionProvider::Copilot,
|
||||
Content::Supermaven => EditPredictionProvider::Supermaven,
|
||||
Content::Zed => EditPredictionProvider::Zed,
|
||||
Content::Codestral => EditPredictionProvider::Codestral,
|
||||
Content::Ollama => EditPredictionProvider::Ollama,
|
||||
|
|
@ -146,7 +143,6 @@ impl EditPredictionProvider {
|
|||
EditPredictionProvider::Zed => true,
|
||||
EditPredictionProvider::None
|
||||
| EditPredictionProvider::Copilot
|
||||
| EditPredictionProvider::Supermaven
|
||||
| EditPredictionProvider::Codestral
|
||||
| EditPredictionProvider::Ollama
|
||||
| EditPredictionProvider::OpenAiCompatibleApi
|
||||
|
|
@ -160,7 +156,6 @@ impl EditPredictionProvider {
|
|||
match self {
|
||||
EditPredictionProvider::Zed => Some("Zed AI"),
|
||||
EditPredictionProvider::Copilot => Some("GitHub Copilot"),
|
||||
EditPredictionProvider::Supermaven => Some("Supermaven"),
|
||||
EditPredictionProvider::Codestral => Some("Codestral"),
|
||||
EditPredictionProvider::Sweep => Some("Sweep"),
|
||||
EditPredictionProvider::Mercury => Some("Mercury"),
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
[package]
|
||||
name = "supermaven"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/supermaven.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
edit_prediction_types.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
supermaven_api.workspace = true
|
||||
text.workspace = true
|
||||
ui.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
|
|
@ -1 +0,0 @@
|
|||
../../LICENSE-GPL
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Outbound messages
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum OutboundMessage {
|
||||
StateUpdate(StateUpdateMessage),
|
||||
#[allow(dead_code)]
|
||||
UseFreeVersion,
|
||||
Logout,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StateUpdateMessage {
|
||||
pub new_id: String,
|
||||
pub updates: Vec<StateUpdate>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum StateUpdate {
|
||||
FileUpdate(FileUpdateMessage),
|
||||
CursorUpdate(CursorPositionUpdateMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct FileUpdateMessage {
|
||||
pub path: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct CursorPositionUpdateMessage {
|
||||
pub path: String,
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
// Inbound messages coming in on stdout
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ResponseItem {
|
||||
// A completion
|
||||
Text { text: String },
|
||||
// Vestigial message type from old versions -- safe to ignore
|
||||
Del { text: String },
|
||||
// Be able to delete whitespace prior to the cursor, likely for the rest of the completion
|
||||
Dedent { text: String },
|
||||
// When the completion is over
|
||||
End,
|
||||
// Got the closing parentheses and shouldn't show any more after
|
||||
Barrier,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SupermavenResponse {
|
||||
pub state_id: String,
|
||||
pub items: Vec<ResponseItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SupermavenMetadataMessage {
|
||||
pub dust_strings: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SupermavenTaskUpdateMessage {
|
||||
pub task: String,
|
||||
pub status: TaskStatus,
|
||||
pub percent_complete: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TaskStatus {
|
||||
InProgress,
|
||||
Complete,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SupermavenActiveRepoMessage {
|
||||
pub repo_simple_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum SupermavenPopupAction {
|
||||
OpenUrl { label: String, url: String },
|
||||
NoOp { label: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct SupermavenPopupMessage {
|
||||
pub message: String,
|
||||
pub actions: Vec<SupermavenPopupAction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase")]
|
||||
pub struct ActivationRequest {
|
||||
pub activate_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SupermavenSetMessage {
|
||||
pub key: String,
|
||||
pub value: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum ServiceTier {
|
||||
FreeNoLicense,
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum SupermavenMessage {
|
||||
Response(SupermavenResponse),
|
||||
Metadata(SupermavenMetadataMessage),
|
||||
Apology {
|
||||
message: Option<String>,
|
||||
},
|
||||
ActivationRequest(ActivationRequest),
|
||||
ActivationSuccess,
|
||||
Passthrough {
|
||||
passthrough: Box<SupermavenMessage>,
|
||||
},
|
||||
Popup(SupermavenPopupMessage),
|
||||
TaskStatus(SupermavenTaskUpdateMessage),
|
||||
ActiveRepo(SupermavenActiveRepoMessage),
|
||||
ServiceTier {
|
||||
service_tier: ServiceTier,
|
||||
},
|
||||
|
||||
Set(SupermavenSetMessage),
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
|
@ -1,485 +0,0 @@
|
|||
mod messages;
|
||||
mod supermaven_edit_prediction_delegate;
|
||||
|
||||
pub use supermaven_edit_prediction_delegate::*;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
#[allow(unused_imports)]
|
||||
use client::{Client, proto};
|
||||
use collections::BTreeMap;
|
||||
|
||||
use futures::{AsyncBufReadExt, StreamExt, channel::mpsc, io::BufReader};
|
||||
use gpui::{App, AsyncApp, Context, Entity, EntityId, Global, Task, WeakEntity, actions};
|
||||
use language::{
|
||||
Anchor, Buffer, BufferSnapshot, ToOffset, language_settings::all_language_settings,
|
||||
};
|
||||
use messages::*;
|
||||
use postage::watch;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::SettingsStore;
|
||||
use smol::io::AsyncWriteExt;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use util::command::Child;
|
||||
use util::command::Stdio;
|
||||
|
||||
actions!(
|
||||
supermaven,
|
||||
[
|
||||
/// Signs out of Supermaven.
|
||||
SignOut
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut App) {
|
||||
let supermaven = cx.new(|_| Supermaven::Starting);
|
||||
Supermaven::set_global(supermaven.clone(), cx);
|
||||
|
||||
let mut provider = all_language_settings(None, cx).edit_predictions.provider;
|
||||
if provider == language::language_settings::EditPredictionProvider::Supermaven {
|
||||
supermaven.update(cx, |supermaven, cx| supermaven.start(client.clone(), cx));
|
||||
}
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
let new_provider = all_language_settings(None, cx).edit_predictions.provider;
|
||||
if new_provider != provider {
|
||||
provider = new_provider;
|
||||
if provider == language::language_settings::EditPredictionProvider::Supermaven {
|
||||
supermaven.update(cx, |supermaven, cx| supermaven.start(client.clone(), cx));
|
||||
} else {
|
||||
supermaven.update(cx, |supermaven, _cx| supermaven.stop());
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.on_action(|_: &SignOut, cx| {
|
||||
if let Some(supermaven) = Supermaven::global(cx) {
|
||||
supermaven.update(cx, |supermaven, _cx| supermaven.sign_out());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub enum Supermaven {
|
||||
Starting,
|
||||
FailedDownload { error: anyhow::Error },
|
||||
Spawned(SupermavenAgent),
|
||||
Error { error: anyhow::Error },
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AccountStatus {
|
||||
Unknown,
|
||||
NeedsActivation { activate_url: String },
|
||||
Ready,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SupermavenGlobal(Entity<Supermaven>);
|
||||
|
||||
impl Global for SupermavenGlobal {}
|
||||
|
||||
impl Supermaven {
|
||||
pub fn global(cx: &App) -> Option<Entity<Self>> {
|
||||
cx.try_global::<SupermavenGlobal>()
|
||||
.map(|model| model.0.clone())
|
||||
}
|
||||
|
||||
pub fn set_global(supermaven: Entity<Self>, cx: &mut App) {
|
||||
cx.set_global(SupermavenGlobal(supermaven));
|
||||
}
|
||||
|
||||
pub fn start(&mut self, client: Arc<Client>, cx: &mut Context<Self>) {
|
||||
if let Self::Starting = self {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let binary_path =
|
||||
supermaven_api::get_supermaven_agent_path(client.http_client()).await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Self::Starting = this {
|
||||
*this =
|
||||
Self::Spawned(SupermavenAgent::new(binary_path, client.clone(), cx)?);
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
*self = Self::Starting;
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
matches!(self, Self::Spawned { .. })
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
cursor_position: Anchor,
|
||||
cx: &App,
|
||||
) -> Option<SupermavenCompletion> {
|
||||
if let Self::Spawned(agent) = self {
|
||||
let buffer_id = buffer.entity_id();
|
||||
let buffer = buffer.read(cx);
|
||||
let path = buffer
|
||||
.file()
|
||||
.and_then(|file| Some(file.as_local()?.abs_path(cx)))
|
||||
.unwrap_or_else(|| PathBuf::from("untitled"))
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let content = buffer.text();
|
||||
let offset = cursor_position.to_offset(buffer);
|
||||
let state_id = agent.next_state_id;
|
||||
agent.next_state_id.0 += 1;
|
||||
|
||||
let (updates_tx, mut updates_rx) = watch::channel();
|
||||
postage::stream::Stream::try_recv(&mut updates_rx).unwrap();
|
||||
|
||||
agent.states.insert(
|
||||
state_id,
|
||||
SupermavenCompletionState {
|
||||
buffer_id,
|
||||
prefix_anchor: cursor_position,
|
||||
prefix_offset: offset,
|
||||
text: String::new(),
|
||||
dedent: String::new(),
|
||||
updates_tx,
|
||||
},
|
||||
);
|
||||
// ensure the states map is max 1000 elements
|
||||
if agent.states.len() > 1000 {
|
||||
// state id is monotonic so it's sufficient to remove the first element
|
||||
agent
|
||||
.states
|
||||
.remove(&agent.states.keys().next().unwrap().clone());
|
||||
}
|
||||
|
||||
let _ = agent
|
||||
.outgoing_tx
|
||||
.unbounded_send(OutboundMessage::StateUpdate(StateUpdateMessage {
|
||||
new_id: state_id.0.to_string(),
|
||||
updates: vec![
|
||||
StateUpdate::FileUpdate(FileUpdateMessage {
|
||||
path: path.clone(),
|
||||
content,
|
||||
}),
|
||||
StateUpdate::CursorUpdate(CursorPositionUpdateMessage { path, offset }),
|
||||
],
|
||||
}));
|
||||
|
||||
Some(SupermavenCompletion {
|
||||
id: state_id,
|
||||
updates: updates_rx,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn completion(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
cursor_position: Anchor,
|
||||
cx: &App,
|
||||
) -> Option<&str> {
|
||||
if let Self::Spawned(agent) = self {
|
||||
find_relevant_completion(
|
||||
&agent.states,
|
||||
buffer.entity_id(),
|
||||
&buffer.read(cx).snapshot(),
|
||||
cursor_position,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign_out(&mut self) {
|
||||
if let Self::Spawned(agent) = self {
|
||||
agent
|
||||
.outgoing_tx
|
||||
.unbounded_send(OutboundMessage::Logout)
|
||||
.ok();
|
||||
// The account status will get set to RequiresActivation or Ready when the next
|
||||
// message from the agent comes in. Until that happens, set the status to Unknown
|
||||
// to disable the button.
|
||||
agent.account_status = AccountStatus::Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_relevant_completion<'a>(
|
||||
states: &'a BTreeMap<SupermavenCompletionStateId, SupermavenCompletionState>,
|
||||
buffer_id: EntityId,
|
||||
buffer: &BufferSnapshot,
|
||||
cursor_position: Anchor,
|
||||
) -> Option<&'a str> {
|
||||
let mut best_completion: Option<&str> = None;
|
||||
'completions: for state in states.values() {
|
||||
if state.buffer_id != buffer_id {
|
||||
continue;
|
||||
}
|
||||
let Some(state_completion) = state.text.strip_prefix(&state.dedent) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let current_cursor_offset = cursor_position.to_offset(buffer);
|
||||
if current_cursor_offset < state.prefix_offset {
|
||||
continue;
|
||||
}
|
||||
|
||||
let original_cursor_offset = buffer.clip_offset(state.prefix_offset, text::Bias::Left);
|
||||
let text_inserted_since_completion_request: String = buffer
|
||||
.text_for_range(original_cursor_offset..current_cursor_offset)
|
||||
.collect();
|
||||
let trimmed_completion =
|
||||
match state_completion.strip_prefix(&text_inserted_since_completion_request) {
|
||||
Some(suffix) => suffix,
|
||||
None => continue 'completions,
|
||||
};
|
||||
|
||||
if best_completion.is_some_and(|best| best.len() > trimmed_completion.len()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
best_completion = Some(trimmed_completion);
|
||||
}
|
||||
best_completion
|
||||
}
|
||||
|
||||
pub struct SupermavenAgent {
|
||||
_process: Child,
|
||||
next_state_id: SupermavenCompletionStateId,
|
||||
states: BTreeMap<SupermavenCompletionStateId, SupermavenCompletionState>,
|
||||
outgoing_tx: mpsc::UnboundedSender<OutboundMessage>,
|
||||
_handle_outgoing_messages: Task<Result<()>>,
|
||||
_handle_incoming_messages: Task<Result<()>>,
|
||||
pub account_status: AccountStatus,
|
||||
service_tier: Option<ServiceTier>,
|
||||
#[allow(dead_code)]
|
||||
client: Arc<Client>,
|
||||
}
|
||||
|
||||
impl SupermavenAgent {
|
||||
fn new(
|
||||
binary_path: PathBuf,
|
||||
client: Arc<Client>,
|
||||
cx: &mut Context<Supermaven>,
|
||||
) -> Result<Self> {
|
||||
let mut process = util::command::new_command(&binary_path)
|
||||
.arg("stdio")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
.context("failed to start the binary")?;
|
||||
|
||||
let stdin = process
|
||||
.stdin
|
||||
.take()
|
||||
.context("failed to get stdin for process")?;
|
||||
let stdout = process
|
||||
.stdout
|
||||
.take()
|
||||
.context("failed to get stdout for process")?;
|
||||
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::unbounded();
|
||||
|
||||
Ok(Self {
|
||||
_process: process,
|
||||
next_state_id: SupermavenCompletionStateId::default(),
|
||||
states: BTreeMap::default(),
|
||||
outgoing_tx,
|
||||
_handle_outgoing_messages: cx.spawn(async move |_, _cx| {
|
||||
Self::handle_outgoing_messages(outgoing_rx, stdin).await
|
||||
}),
|
||||
_handle_incoming_messages: cx.spawn(async move |this, cx| {
|
||||
Self::handle_incoming_messages(this, stdout, cx).await
|
||||
}),
|
||||
account_status: AccountStatus::Unknown,
|
||||
service_tier: None,
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_outgoing_messages<W: smol::io::AsyncWrite + Unpin>(
|
||||
mut outgoing: mpsc::UnboundedReceiver<OutboundMessage>,
|
||||
mut stdin: W,
|
||||
) -> Result<()> {
|
||||
while let Some(message) = outgoing.next().await {
|
||||
let bytes = serde_json::to_vec(&message)?;
|
||||
stdin.write_all(&bytes).await?;
|
||||
stdin.write_all(&[b'\n']).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_incoming_messages<R: smol::io::AsyncRead + Unpin>(
|
||||
this: WeakEntity<Supermaven>,
|
||||
stdout: R,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
const MESSAGE_PREFIX: &str = "SM-MESSAGE ";
|
||||
|
||||
let stdout = BufReader::new(stdout);
|
||||
let mut lines = stdout.lines();
|
||||
while let Some(line) = lines.next().await {
|
||||
let Some(line) = line.context("failed to read line from stdout").log_err() else {
|
||||
continue;
|
||||
};
|
||||
let Some(line) = line.strip_prefix(MESSAGE_PREFIX) else {
|
||||
continue;
|
||||
};
|
||||
let Some(message) = serde_json::from_str::<SupermavenMessage>(line)
|
||||
.with_context(|| format!("failed to deserialize line from stdout: {:?}", line))
|
||||
.log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
if let Supermaven::Spawned(this) = this {
|
||||
this.handle_message(message);
|
||||
}
|
||||
Task::ready(anyhow::Ok(()))
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_message(&mut self, message: SupermavenMessage) {
|
||||
match message {
|
||||
SupermavenMessage::ActivationRequest(request) => {
|
||||
self.account_status = match request.activate_url {
|
||||
Some(activate_url) => AccountStatus::NeedsActivation { activate_url },
|
||||
None => AccountStatus::Ready,
|
||||
};
|
||||
}
|
||||
SupermavenMessage::ActivationSuccess => {
|
||||
self.account_status = AccountStatus::Ready;
|
||||
}
|
||||
SupermavenMessage::ServiceTier { service_tier } => {
|
||||
self.account_status = AccountStatus::Ready;
|
||||
self.service_tier = Some(service_tier);
|
||||
}
|
||||
SupermavenMessage::Response(response) => {
|
||||
let state_id = SupermavenCompletionStateId(response.state_id.parse().unwrap());
|
||||
if let Some(state) = self.states.get_mut(&state_id) {
|
||||
for item in &response.items {
|
||||
match item {
|
||||
ResponseItem::Text { text } => state.text.push_str(text),
|
||||
ResponseItem::Dedent { text } => state.dedent.push_str(text),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
*state.updates_tx.borrow_mut() = ();
|
||||
}
|
||||
}
|
||||
SupermavenMessage::Passthrough { passthrough } => self.handle_message(*passthrough),
|
||||
_ => {
|
||||
log::warn!("unhandled message: {:?}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
|
||||
pub struct SupermavenCompletionStateId(usize);
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct SupermavenCompletionState {
|
||||
buffer_id: EntityId,
|
||||
prefix_anchor: Anchor,
|
||||
// prefix_offset is tracked independently because the anchor biases left which
|
||||
// doesn't allow us to determine if the prior text has been deleted.
|
||||
prefix_offset: usize,
|
||||
text: String,
|
||||
dedent: String,
|
||||
updates_tx: watch::Sender<()>,
|
||||
}
|
||||
|
||||
pub struct SupermavenCompletion {
|
||||
pub id: SupermavenCompletionStateId,
|
||||
pub updates: watch::Receiver<()>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use collections::BTreeMap;
|
||||
use gpui::TestAppContext;
|
||||
use language::Buffer;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_relevant_completion_no_first_letter_skip(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| Buffer::local("hello world", cx));
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let mut states = BTreeMap::new();
|
||||
let state_id = SupermavenCompletionStateId(1);
|
||||
let (updates_tx, _) = watch::channel();
|
||||
|
||||
states.insert(
|
||||
state_id,
|
||||
SupermavenCompletionState {
|
||||
buffer_id: buffer.entity_id(),
|
||||
prefix_anchor: buffer_snapshot.anchor_before(0), // Start of buffer
|
||||
prefix_offset: 0,
|
||||
text: "hello".to_string(),
|
||||
dedent: String::new(),
|
||||
updates_tx,
|
||||
},
|
||||
);
|
||||
|
||||
let cursor_position = buffer_snapshot.anchor_after(1);
|
||||
|
||||
let result = find_relevant_completion(
|
||||
&states,
|
||||
buffer.entity_id(),
|
||||
&buffer_snapshot,
|
||||
cursor_position,
|
||||
);
|
||||
|
||||
assert_eq!(result, Some("ello"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_relevant_completion_with_multiple_chars(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| Buffer::local("hello world", cx));
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let mut states = BTreeMap::new();
|
||||
let state_id = SupermavenCompletionStateId(1);
|
||||
let (updates_tx, _) = watch::channel();
|
||||
|
||||
states.insert(
|
||||
state_id,
|
||||
SupermavenCompletionState {
|
||||
buffer_id: buffer.entity_id(),
|
||||
prefix_anchor: buffer_snapshot.anchor_before(0), // Start of buffer
|
||||
prefix_offset: 0,
|
||||
text: "hello".to_string(),
|
||||
dedent: String::new(),
|
||||
updates_tx,
|
||||
},
|
||||
);
|
||||
|
||||
let cursor_position = buffer_snapshot.anchor_after(3);
|
||||
|
||||
let result = find_relevant_completion(
|
||||
&states,
|
||||
buffer.entity_id(),
|
||||
&buffer_snapshot,
|
||||
cursor_position,
|
||||
);
|
||||
|
||||
assert_eq!(result, Some("lo"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,303 +0,0 @@
|
|||
use crate::{Supermaven, SupermavenCompletionStateId};
|
||||
use anyhow::Result;
|
||||
use edit_prediction_types::{
|
||||
EditPrediction, EditPredictionDelegate, EditPredictionDiscardReason, EditPredictionIconSet,
|
||||
};
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{App, Context, Entity, EntityId, Task};
|
||||
use language::{Anchor, Buffer, BufferSnapshot};
|
||||
use std::{
|
||||
ops::{AddAssign, Range},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use text::{ToOffset, ToPoint};
|
||||
use ui::prelude::*;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
||||
|
||||
pub struct SupermavenEditPredictionDelegate {
|
||||
supermaven: Entity<Supermaven>,
|
||||
buffer_id: Option<EntityId>,
|
||||
completion_id: Option<SupermavenCompletionStateId>,
|
||||
completion_text: Option<String>,
|
||||
file_extension: Option<String>,
|
||||
pending_refresh: Option<Task<Result<()>>>,
|
||||
completion_position: Option<language::Anchor>,
|
||||
}
|
||||
|
||||
impl SupermavenEditPredictionDelegate {
|
||||
pub fn new(supermaven: Entity<Supermaven>) -> Self {
|
||||
Self {
|
||||
supermaven,
|
||||
buffer_id: None,
|
||||
completion_id: None,
|
||||
completion_text: None,
|
||||
file_extension: None,
|
||||
pending_refresh: None,
|
||||
completion_position: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Computes the edit prediction from the difference between the completion text.
|
||||
// This is defined by greedily matching the buffer text against the completion text.
|
||||
// Inlays are inserted for parts of the completion text that are not present in the buffer text.
|
||||
// For example, given the completion text "axbyc" and the buffer text "xy", the rendered output in the editor would be "[a]x[b]y[c]".
|
||||
// The parts in brackets are the inlays.
|
||||
fn completion_from_diff(
|
||||
snapshot: BufferSnapshot,
|
||||
completion_text: &str,
|
||||
position: Anchor,
|
||||
delete_range: Range<Anchor>,
|
||||
) -> EditPrediction {
|
||||
let buffer_text = snapshot.text_for_range(delete_range).collect::<String>();
|
||||
|
||||
let mut edits: Vec<(Range<language::Anchor>, Arc<str>)> = Vec::new();
|
||||
|
||||
let completion_graphemes: Vec<&str> = completion_text.graphemes(true).collect();
|
||||
let buffer_graphemes: Vec<&str> = buffer_text.graphemes(true).collect();
|
||||
|
||||
let mut offset = position.to_offset(&snapshot);
|
||||
|
||||
let mut i = 0;
|
||||
let mut j = 0;
|
||||
while i < completion_graphemes.len() && j < buffer_graphemes.len() {
|
||||
// find the next instance of the buffer text in the completion text.
|
||||
let k = completion_graphemes[i..]
|
||||
.iter()
|
||||
.position(|c| *c == buffer_graphemes[j]);
|
||||
match k {
|
||||
Some(k) => {
|
||||
if k != 0 {
|
||||
let offset = snapshot.anchor_after(offset);
|
||||
// the range from the current position to item is an inlay.
|
||||
let edit = (
|
||||
offset..offset,
|
||||
completion_graphemes[i..i + k].join("").into(),
|
||||
);
|
||||
edits.push(edit);
|
||||
}
|
||||
i += k + 1;
|
||||
j += 1;
|
||||
offset.add_assign(buffer_graphemes[j - 1].len());
|
||||
}
|
||||
None => {
|
||||
// there are no more matching completions, so drop the remaining
|
||||
// completion text as an inlay.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if j == buffer_graphemes.len() && i < completion_graphemes.len() {
|
||||
let offset = snapshot.anchor_after(offset);
|
||||
// there is leftover completion text, so drop it as an inlay.
|
||||
let edit_range = offset..offset;
|
||||
let edit_text = completion_graphemes[i..].join("");
|
||||
edits.push((edit_range, edit_text.into()));
|
||||
}
|
||||
|
||||
EditPrediction::Local {
|
||||
id: None,
|
||||
edits,
|
||||
cursor_position: None,
|
||||
edit_preview: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl EditPredictionDelegate for SupermavenEditPredictionDelegate {
|
||||
fn name() -> &'static str {
|
||||
"supermaven"
|
||||
}
|
||||
|
||||
fn display_name() -> &'static str {
|
||||
"Supermaven"
|
||||
}
|
||||
|
||||
fn show_predictions_in_menu() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn show_tab_accept_marker() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_jump_to_edit() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn icons(&self, _cx: &App) -> EditPredictionIconSet {
|
||||
EditPredictionIconSet::new(IconName::Supermaven)
|
||||
.with_disabled(IconName::SupermavenDisabled)
|
||||
.with_error(IconName::SupermavenError)
|
||||
}
|
||||
|
||||
fn is_enabled(&self, _buffer: &Entity<Buffer>, _cursor_position: Anchor, cx: &App) -> bool {
|
||||
self.supermaven.read(cx).is_enabled()
|
||||
}
|
||||
|
||||
fn is_refreshing(&self, _cx: &App) -> bool {
|
||||
self.pending_refresh.is_some() && self.completion_id.is_none()
|
||||
}
|
||||
|
||||
fn refresh(
|
||||
&mut self,
|
||||
buffer_handle: Entity<Buffer>,
|
||||
cursor_position: Anchor,
|
||||
debounce: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Only make new completion requests when debounce is true (i.e., when text is typed)
|
||||
// When debounce is false (i.e., cursor movement), we should not make new requests
|
||||
if !debounce {
|
||||
return;
|
||||
}
|
||||
|
||||
reset_completion_cache(self, cx);
|
||||
|
||||
let Some(mut completion) = self.supermaven.update(cx, |supermaven, cx| {
|
||||
supermaven.complete(&buffer_handle, cursor_position, cx)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.pending_refresh = Some(cx.spawn(async move |this, cx| {
|
||||
if debounce {
|
||||
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
|
||||
}
|
||||
|
||||
while let Some(()) = completion.updates.next().await {
|
||||
this.update(cx, |this, cx| {
|
||||
// Get the completion text and cache it
|
||||
if let Some(text) =
|
||||
this.supermaven
|
||||
.read(cx)
|
||||
.completion(&buffer_handle, cursor_position, cx)
|
||||
{
|
||||
this.completion_text = Some(text.to_string());
|
||||
|
||||
this.completion_position = Some(cursor_position);
|
||||
}
|
||||
|
||||
this.completion_id = Some(completion.id);
|
||||
this.buffer_id = Some(buffer_handle.entity_id());
|
||||
this.file_extension = buffer_handle.read(cx).file().and_then(|file| {
|
||||
Some(
|
||||
Path::new(file.file_name(cx))
|
||||
.extension()?
|
||||
.to_str()?
|
||||
.to_string(),
|
||||
)
|
||||
});
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut Context<Self>) {
|
||||
reset_completion_cache(self, _cx);
|
||||
}
|
||||
|
||||
fn discard(&mut self, _reason: EditPredictionDiscardReason, _cx: &mut Context<Self>) {
|
||||
reset_completion_cache(self, _cx);
|
||||
}
|
||||
|
||||
fn suggest(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
cursor_position: Anchor,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<EditPrediction> {
|
||||
if self.buffer_id != Some(buffer.entity_id()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.completion_id.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let completion_text = if let Some(cached_text) = &self.completion_text {
|
||||
cached_text.as_str()
|
||||
} else {
|
||||
let text = self
|
||||
.supermaven
|
||||
.read(cx)
|
||||
.completion(buffer, cursor_position, cx)?;
|
||||
self.completion_text = Some(text.to_string());
|
||||
text
|
||||
};
|
||||
|
||||
// Check if the cursor is still at the same position as the completion request
|
||||
// If we don't have a completion position stored, don't show the completion
|
||||
if let Some(completion_position) = self.completion_position {
|
||||
if cursor_position != completion_position {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
let completion_text = trim_to_end_of_line_unless_leading_newline(completion_text);
|
||||
|
||||
let completion_text = completion_text.trim_end();
|
||||
|
||||
if !completion_text.trim().is_empty() {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
// Calculate the range from cursor to end of line correctly
|
||||
let cursor_point = cursor_position.to_point(&snapshot);
|
||||
let end_of_line = snapshot.anchor_after(language::Point::new(
|
||||
cursor_point.row,
|
||||
snapshot.line_len(cursor_point.row),
|
||||
));
|
||||
let delete_range = cursor_position..end_of_line;
|
||||
|
||||
Some(completion_from_diff(
|
||||
snapshot,
|
||||
completion_text,
|
||||
cursor_position,
|
||||
delete_range,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_completion_cache(
|
||||
provider: &mut SupermavenEditPredictionDelegate,
|
||||
_cx: &mut Context<SupermavenEditPredictionDelegate>,
|
||||
) {
|
||||
provider.pending_refresh = None;
|
||||
provider.completion_id = None;
|
||||
provider.completion_text = None;
|
||||
provider.completion_position = None;
|
||||
provider.buffer_id = None;
|
||||
}
|
||||
|
||||
fn trim_to_end_of_line_unless_leading_newline(text: &str) -> &str {
|
||||
if has_leading_newline(text) {
|
||||
text
|
||||
} else if let Some(i) = text.find('\n') {
|
||||
&text[..i]
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
fn has_leading_newline(text: &str) -> bool {
|
||||
for c in text.chars() {
|
||||
if c == '\n' {
|
||||
return true;
|
||||
}
|
||||
if !c.is_whitespace() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
[package]
|
||||
name = "supermaven_api"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/supermaven_api.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
http_client.workspace = true
|
||||
paths.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
|
|
@ -1 +0,0 @@
|
|||
../../LICENSE-GPL
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
use anyhow::{Context as _, Result};
|
||||
use futures::AsyncReadExt;
|
||||
use futures::io::BufReader;
|
||||
use http_client::{AsyncBody, HttpClient, Request as HttpRequest};
|
||||
use paths::supermaven_dir;
|
||||
use serde::Deserialize;
|
||||
use smol::fs::{self, File};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use util::fs::{make_file_executable, remove_matching};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SupermavenApiError {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SupermavenDownloadResponse {
|
||||
pub download_url: String,
|
||||
pub version: u64,
|
||||
pub sha256_hash: String,
|
||||
}
|
||||
|
||||
pub async fn latest_release(
|
||||
client: Arc<dyn HttpClient>,
|
||||
platform: &str,
|
||||
arch: &str,
|
||||
) -> Result<SupermavenDownloadResponse> {
|
||||
let uri = format!(
|
||||
"https://supermaven.com/api/download-path?platform={}&arch={}",
|
||||
platform, arch
|
||||
);
|
||||
|
||||
// Download is not authenticated
|
||||
let request = HttpRequest::get(&uri);
|
||||
|
||||
let mut response = client
|
||||
.send(request.body(AsyncBody::default())?)
|
||||
.await
|
||||
.with_context(|| "Unable to acquire Supermaven Agent".to_string())?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await?;
|
||||
|
||||
if response.status().is_client_error() || response.status().is_server_error() {
|
||||
let body_str = std::str::from_utf8(&body)?;
|
||||
let error: SupermavenApiError = serde_json::from_str(body_str)?;
|
||||
anyhow::bail!("Supermaven API error: {}", error.message);
|
||||
}
|
||||
|
||||
serde_json::from_slice::<SupermavenDownloadResponse>(&body)
|
||||
.with_context(|| "Unable to parse Supermaven Agent response".to_string())
|
||||
}
|
||||
|
||||
pub fn version_path(version: u64) -> PathBuf {
|
||||
supermaven_dir().join(format!(
|
||||
"sm-agent-{}{}",
|
||||
version,
|
||||
std::env::consts::EXE_SUFFIX
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn has_version(version_path: &Path) -> bool {
|
||||
fs::metadata(version_path).await.is_ok_and(|m| m.is_file())
|
||||
}
|
||||
|
||||
pub async fn get_supermaven_agent_path(client: Arc<dyn HttpClient>) -> Result<PathBuf> {
|
||||
fs::create_dir_all(supermaven_dir())
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Could not create Supermaven Agent Directory at {:?}",
|
||||
supermaven_dir()
|
||||
)
|
||||
})?;
|
||||
|
||||
let platform = match std::env::consts::OS {
|
||||
"macos" => "darwin",
|
||||
"windows" => "windows",
|
||||
"linux" => "linux",
|
||||
unsupported => anyhow::bail!("unsupported platform {unsupported}"),
|
||||
};
|
||||
|
||||
let arch = match std::env::consts::ARCH {
|
||||
"x86_64" => "amd64",
|
||||
"aarch64" => "arm64",
|
||||
unsupported => anyhow::bail!("unsupported architecture {unsupported}"),
|
||||
};
|
||||
|
||||
let download_info = latest_release(client.clone(), platform, arch).await?;
|
||||
|
||||
let binary_path = version_path(download_info.version);
|
||||
|
||||
if has_version(&binary_path).await {
|
||||
// Due to an issue with the Supermaven binary not being made executable on
|
||||
// earlier Zed versions and Supermaven releases not occurring that frequently,
|
||||
// we ensure here that the found binary is actually executable.
|
||||
make_file_executable(&binary_path).await?;
|
||||
|
||||
return Ok(binary_path);
|
||||
}
|
||||
|
||||
let request = HttpRequest::get(&download_info.download_url);
|
||||
|
||||
let mut response = client
|
||||
.send(request.body(AsyncBody::default())?)
|
||||
.await
|
||||
.with_context(|| "Unable to download Supermaven Agent".to_string())?;
|
||||
|
||||
let mut file = File::create(&binary_path)
|
||||
.await
|
||||
.with_context(|| format!("Unable to create file at {:?}", binary_path))?;
|
||||
|
||||
futures::io::copy(BufReader::new(response.body_mut()), &mut file)
|
||||
.await
|
||||
.with_context(|| format!("Unable to write binary to file at {:?}", binary_path))?;
|
||||
|
||||
make_file_executable(&binary_path).await?;
|
||||
|
||||
remove_matching(supermaven_dir(), |file| file != binary_path).await;
|
||||
|
||||
Ok(binary_path)
|
||||
}
|
||||
|
|
@ -189,7 +189,6 @@ sidebar.workspace = true
|
|||
smol.workspace = true
|
||||
snippet_provider.workspace = true
|
||||
snippets_ui.workspace = true
|
||||
supermaven.workspace = true
|
||||
svg_preview.workspace = true
|
||||
sysinfo.workspace = true
|
||||
tab_switcher.workspace = true
|
||||
|
|
|
|||
|
|
@ -638,7 +638,6 @@ fn main() {
|
|||
);
|
||||
|
||||
copilot_ui::init(&app_state, cx);
|
||||
supermaven::init(app_state.client.clone(), cx);
|
||||
language_model::init(app_state.client.clone(), cx);
|
||||
language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
|
||||
acp_tools::init(cx);
|
||||
|
|
|
|||
|
|
@ -4860,7 +4860,6 @@ mod tests {
|
|||
"settings_profile_selector",
|
||||
"snippets",
|
||||
"stash_picker",
|
||||
"supermaven",
|
||||
"svg",
|
||||
"syntax_tree_view",
|
||||
"tab_switcher",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ use settings::{
|
|||
EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, EditPredictionPromptFormat, SettingsStore,
|
||||
};
|
||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||
use supermaven::{Supermaven, SupermavenEditPredictionDelegate};
|
||||
use ui::Window;
|
||||
|
||||
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
|
||||
|
|
@ -132,7 +131,6 @@ fn edit_prediction_provider_config_for_settings(cx: &App) -> Option<EditPredicti
|
|||
match provider {
|
||||
EditPredictionProvider::None => None,
|
||||
EditPredictionProvider::Copilot => Some(EditPredictionProviderConfig::Copilot),
|
||||
EditPredictionProvider::Supermaven => Some(EditPredictionProviderConfig::Supermaven),
|
||||
EditPredictionProvider::Zed => Some(EditPredictionProviderConfig::Zed(
|
||||
EditPredictionModel::Zeta1,
|
||||
)),
|
||||
|
|
@ -204,7 +202,6 @@ fn infer_prompt_format(model: &str) -> Option<EditPredictionPromptFormat> {
|
|||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
enum EditPredictionProviderConfig {
|
||||
Copilot,
|
||||
Supermaven,
|
||||
Codestral,
|
||||
Zed(EditPredictionModel),
|
||||
}
|
||||
|
|
@ -213,7 +210,6 @@ impl EditPredictionProviderConfig {
|
|||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
EditPredictionProviderConfig::Copilot => "Copilot",
|
||||
EditPredictionProviderConfig::Supermaven => "Supermaven",
|
||||
EditPredictionProviderConfig::Codestral => "Codestral",
|
||||
EditPredictionProviderConfig::Zed(model) => match model {
|
||||
EditPredictionModel::Zeta1 => "Zeta1",
|
||||
|
|
@ -306,12 +302,6 @@ fn assign_edit_prediction_provider(
|
|||
editor.set_edit_prediction_provider(Some(provider), window, cx);
|
||||
}
|
||||
}
|
||||
Some(EditPredictionProviderConfig::Supermaven) => {
|
||||
if let Some(supermaven) = Supermaven::global(cx) {
|
||||
let provider = cx.new(|_| SupermavenEditPredictionDelegate::new(supermaven));
|
||||
editor.set_edit_prediction_provider(Some(provider), window, cx);
|
||||
}
|
||||
}
|
||||
Some(EditPredictionProviderConfig::Codestral) => {
|
||||
let http_client = client.http_client();
|
||||
let provider = cx.new(|_| CodestralEditPredictionDelegate::new(http_client));
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ The [Inline Assistant](./inline-assistant.md) works differently: select code or
|
|||
|
||||
[Edit Prediction](./edit-prediction.md) provides AI code completions on every keystroke. Each keypress sends a request to the prediction provider, which returns single or multi-line suggestions you accept with `tab`.
|
||||
|
||||
The default provider is Zeta, Zed's open-source model trained on open data. You can also use GitHub Copilot, Supermaven, or Codestral.
|
||||
The default provider is Zeta, Zed's open-source model trained on open data. You can also use GitHub Copilot, or Codestral.
|
||||
|
||||
## Text threads
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ description: Zed's code completions from language servers and edit predictions.
|
|||
Zed supports two sources for completions:
|
||||
|
||||
1. "Code Completions" provided by Language Servers (LSPs) automatically installed by Zed or via [Zed Language Extensions](languages.md).
|
||||
2. "Edit Predictions" provided by Zed's own Zeta model or by external providers like [GitHub Copilot](#github-copilot) or [Supermaven](#supermaven).
|
||||
2. "Edit Predictions" provided by Zed's own Zeta model or by external providers like [GitHub Copilot](#github-copilot).
|
||||
|
||||
## Language Server Code Completions {#code-completions}
|
||||
|
||||
|
|
|
|||
|
|
@ -1800,17 +1800,7 @@ While other options may be changed at a runtime and should be placed under `sett
|
|||
}
|
||||
```
|
||||
|
||||
3. Use Supermaven as the edit prediction provider:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"edit_predictions": {
|
||||
"provider": "supermaven"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. Turn off edit predictions across all providers
|
||||
3. Turn off edit predictions across all providers
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue