Compare commits
17 Commits
fix-git-ht
...
v0.157.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6192aa1469 | ||
|
|
2b902c185e | ||
|
|
e2e95f2c49 | ||
|
|
5e3a02b3f3 | ||
|
|
bc768d8586 | ||
|
|
84caa0cf4c | ||
|
|
8445b4adfb | ||
|
|
86e2510414 | ||
|
|
5222a1162c | ||
|
|
be25c51c5b | ||
|
|
ef0eeb4853 | ||
|
|
4e0db8ba32 | ||
|
|
ed379fe233 | ||
|
|
eb933ce203 | ||
|
|
515f9a6c7d | ||
|
|
9c33d723f8 | ||
|
|
5b303e892a |
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -6302,6 +6302,7 @@ dependencies = [
|
||||
"strum 0.25.0",
|
||||
"text",
|
||||
"theme",
|
||||
"thiserror",
|
||||
"tiktoken-rs",
|
||||
"ui",
|
||||
"unindent",
|
||||
@@ -14396,7 +14397,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.157.0"
|
||||
version = "0.157.5"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -817,6 +817,7 @@
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Astro": {
|
||||
"language_servers": ["astro-language-server", "..."],
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-astro"]
|
||||
|
||||
@@ -1496,6 +1496,13 @@ struct WorkflowAssist {
|
||||
|
||||
type MessageHeader = MessageMetadata;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum AssistError {
|
||||
PaymentRequired,
|
||||
MaxMonthlySpendReached,
|
||||
Message(SharedString),
|
||||
}
|
||||
|
||||
pub struct ContextEditor {
|
||||
context: Model<Context>,
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -1514,7 +1521,7 @@ pub struct ContextEditor {
|
||||
workflow_steps: HashMap<Range<language::Anchor>, WorkflowStepViewState>,
|
||||
active_workflow_step: Option<ActiveWorkflowStep>,
|
||||
assistant_panel: WeakView<AssistantPanel>,
|
||||
error_message: Option<SharedString>,
|
||||
last_error: Option<AssistError>,
|
||||
show_accept_terms: bool,
|
||||
pub(crate) slash_menu_handle:
|
||||
PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
|
||||
@@ -1585,7 +1592,7 @@ impl ContextEditor {
|
||||
workflow_steps: HashMap::default(),
|
||||
active_workflow_step: None,
|
||||
assistant_panel,
|
||||
error_message: None,
|
||||
last_error: None,
|
||||
show_accept_terms: false,
|
||||
slash_menu_handle: Default::default(),
|
||||
dragged_file_worktrees: Vec::new(),
|
||||
@@ -1629,7 +1636,7 @@ impl ContextEditor {
|
||||
}
|
||||
|
||||
if !self.apply_active_workflow_step(cx) {
|
||||
self.error_message = None;
|
||||
self.last_error = None;
|
||||
self.send_to_model(cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1779,7 +1786,7 @@ impl ContextEditor {
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
|
||||
self.error_message = None;
|
||||
self.last_error = None;
|
||||
|
||||
if self
|
||||
.context
|
||||
@@ -2284,7 +2291,13 @@ impl ContextEditor {
|
||||
}
|
||||
ContextEvent::Operation(_) => {}
|
||||
ContextEvent::ShowAssistError(error_message) => {
|
||||
self.error_message = Some(error_message.clone());
|
||||
self.last_error = Some(AssistError::Message(error_message.clone()));
|
||||
}
|
||||
ContextEvent::ShowPaymentRequiredError => {
|
||||
self.last_error = Some(AssistError::PaymentRequired);
|
||||
}
|
||||
ContextEvent::ShowMaxMonthlySpendReachedError => {
|
||||
self.last_error = Some(AssistError::MaxMonthlySpendReached);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4298,6 +4311,154 @@ impl ContextEditor {
|
||||
focus_handle.dispatch_action(&Assist, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
let last_error = self.last_error.as_ref()?;
|
||||
|
||||
Some(
|
||||
div()
|
||||
.absolute()
|
||||
.right_3()
|
||||
.bottom_12()
|
||||
.max_w_96()
|
||||
.py_2()
|
||||
.px_3()
|
||||
.elevation_2(cx)
|
||||
.occlude()
|
||||
.child(match last_error {
|
||||
AssistError::PaymentRequired => self.render_payment_required_error(cx),
|
||||
AssistError::MaxMonthlySpendReached => {
|
||||
self.render_max_monthly_spend_reached_error(cx)
|
||||
}
|
||||
AssistError::Message(error_message) => {
|
||||
self.render_assist_error(error_message, cx)
|
||||
}
|
||||
})
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
|
||||
const ACCOUNT_URL: &str = "https://zed.dev/account";
|
||||
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(ERROR_MESSAGE)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.open_url(ACCOUNT_URL);
|
||||
cx.notify();
|
||||
},
|
||||
)))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
|
||||
const ACCOUNT_URL: &str = "https://zed.dev/account";
|
||||
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(ERROR_MESSAGE)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(
|
||||
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
|
||||
cx.listener(|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.open_url(ACCOUNT_URL);
|
||||
cx.notify();
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_assist_error(
|
||||
&self,
|
||||
error_message: &SharedString,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(
|
||||
Label::new("Error interacting with language model")
|
||||
.weight(FontWeight::MEDIUM),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(error_message.clone())),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the contents of the *outermost* fenced code block that contains the given offset.
|
||||
@@ -4434,48 +4595,7 @@ impl Render for ContextEditor {
|
||||
.child(element),
|
||||
)
|
||||
})
|
||||
.when_some(self.error_message.clone(), |this, error_message| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_3()
|
||||
.bottom_12()
|
||||
.max_w_96()
|
||||
.py_2()
|
||||
.px_3()
|
||||
.elevation_2(cx)
|
||||
.occlude()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(
|
||||
Label::new("Error interacting with language model")
|
||||
.weight(FontWeight::MEDIUM),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(error_message)),
|
||||
)
|
||||
.child(h_flex().justify_end().mt_1().child(
|
||||
Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.error_message = None;
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.children(self.render_last_error(cx))
|
||||
.child(
|
||||
h_flex().w_full().relative().child(
|
||||
h_flex()
|
||||
|
||||
@@ -26,6 +26,7 @@ use gpui::{
|
||||
|
||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||
use language_model::{
|
||||
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
|
||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role,
|
||||
@@ -294,6 +295,8 @@ impl ContextOperation {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ContextEvent {
|
||||
ShowAssistError(SharedString),
|
||||
ShowPaymentRequiredError,
|
||||
ShowMaxMonthlySpendReachedError,
|
||||
MessagesEdited,
|
||||
SummaryChanged,
|
||||
StreamedCompletion,
|
||||
@@ -2112,25 +2115,36 @@ impl Context {
|
||||
let result = stream_completion.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let error_message = result
|
||||
.as_ref()
|
||||
.err()
|
||||
.map(|error| error.to_string().trim().to_string());
|
||||
|
||||
if let Some(error_message) = error_message.as_ref() {
|
||||
cx.emit(ContextEvent::ShowAssistError(SharedString::from(
|
||||
error_message.clone(),
|
||||
)));
|
||||
}
|
||||
|
||||
this.update_metadata(assistant_message_id, cx, |metadata| {
|
||||
if let Some(error_message) = error_message.as_ref() {
|
||||
metadata.status =
|
||||
MessageStatus::Error(SharedString::from(error_message.clone()));
|
||||
let error_message = if let Some(error) = result.as_ref().err() {
|
||||
if error.is::<PaymentRequiredError>() {
|
||||
cx.emit(ContextEvent::ShowPaymentRequiredError);
|
||||
this.update_metadata(assistant_message_id, cx, |metadata| {
|
||||
metadata.status = MessageStatus::Canceled;
|
||||
});
|
||||
Some(error.to_string())
|
||||
} else if error.is::<MaxMonthlySpendReachedError>() {
|
||||
cx.emit(ContextEvent::ShowMaxMonthlySpendReachedError);
|
||||
this.update_metadata(assistant_message_id, cx, |metadata| {
|
||||
metadata.status = MessageStatus::Canceled;
|
||||
});
|
||||
Some(error.to_string())
|
||||
} else {
|
||||
metadata.status = MessageStatus::Done;
|
||||
let error_message = error.to_string().trim().to_string();
|
||||
cx.emit(ContextEvent::ShowAssistError(SharedString::from(
|
||||
error_message.clone(),
|
||||
)));
|
||||
this.update_metadata(assistant_message_id, cx, |metadata| {
|
||||
metadata.status =
|
||||
MessageStatus::Error(SharedString::from(error_message.clone()));
|
||||
});
|
||||
Some(error_message)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.update_metadata(assistant_message_id, cx, |metadata| {
|
||||
metadata.status = MessageStatus::Done;
|
||||
});
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(telemetry) = this.telemetry.as_ref() {
|
||||
let language_name = this
|
||||
|
||||
@@ -58,27 +58,32 @@ struct Args {
|
||||
dev_server_token: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_path_with_position(argument_str: &str) -> Result<String, std::io::Error> {
|
||||
let path = PathWithPosition::parse_str(argument_str);
|
||||
let curdir = env::current_dir()?;
|
||||
|
||||
let canonicalized = path.map_path(|path| match fs::canonicalize(&path) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(e) => {
|
||||
if let Some(mut parent) = path.parent() {
|
||||
if parent == Path::new("") {
|
||||
parent = &curdir
|
||||
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
|
||||
let canonicalized = match Path::new(argument_str).canonicalize() {
|
||||
Ok(existing_path) => PathWithPosition::from_path(existing_path),
|
||||
Err(_) => {
|
||||
let path = PathWithPosition::parse_str(argument_str);
|
||||
let curdir = env::current_dir().context("reteiving current directory")?;
|
||||
path.map_path(|path| match fs::canonicalize(&path) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(e) => {
|
||||
if let Some(mut parent) = path.parent() {
|
||||
if parent == Path::new("") {
|
||||
parent = &curdir
|
||||
}
|
||||
match fs::canonicalize(parent) {
|
||||
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
|
||||
Err(_) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
match fs::canonicalize(parent) {
|
||||
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
|
||||
Err(_) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
})?;
|
||||
Ok(canonicalized.to_string(|path| path.display().to_string()))
|
||||
.with_context(|| format!("parsing as path with position {argument_str}"))?,
|
||||
};
|
||||
Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
|
||||
@@ -379,75 +379,51 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
.next()
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.executor().finish_waiting();
|
||||
|
||||
// Open the buffer on the host.
|
||||
let buffer_a = project_a
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.executor().run_until_parked();
|
||||
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.text(), "fn main() { a. }")
|
||||
});
|
||||
|
||||
// Confirm a completion on the guest.
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
|
||||
});
|
||||
|
||||
// Return a resolved completion from the host's language server.
|
||||
// The resolved completion has an additional text edit.
|
||||
fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
|
||||
|params, _| async move {
|
||||
Ok(match params.label.as_str() {
|
||||
"first_method(…)" => lsp::CompletionItem {
|
||||
label: "first_method(…)".into(),
|
||||
detail: Some("fn(&mut self, B) -> C".into()),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
new_text: "first_method($1)".to_string(),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(0, 14),
|
||||
lsp::Position::new(0, 14),
|
||||
),
|
||||
})),
|
||||
additional_text_edits: Some(vec![lsp::TextEdit {
|
||||
new_text: "use d::SomeTrait;\n".to_string(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||
}]),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||
..Default::default()
|
||||
},
|
||||
"second_method(…)" => lsp::CompletionItem {
|
||||
label: "second_method(…)".into(),
|
||||
detail: Some("fn(&mut self, C) -> D<E>".into()),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
new_text: "second_method()".to_string(),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(0, 14),
|
||||
lsp::Position::new(0, 14),
|
||||
),
|
||||
})),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||
additional_text_edits: Some(vec![lsp::TextEdit {
|
||||
new_text: "use d::SomeTrait;\n".to_string(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||
}]),
|
||||
..Default::default()
|
||||
},
|
||||
_ => panic!("unexpected completion label: {:?}", params.label),
|
||||
assert_eq!(params.label, "first_method(…)");
|
||||
Ok(lsp::CompletionItem {
|
||||
label: "first_method(…)".into(),
|
||||
detail: Some("fn(&mut self, B) -> C".into()),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
new_text: "first_method($1)".to_string(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
|
||||
})),
|
||||
additional_text_edits: Some(vec![lsp::TextEdit {
|
||||
new_text: "use d::SomeTrait;\n".to_string(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||
}]),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||
..Default::default()
|
||||
})
|
||||
},
|
||||
);
|
||||
cx_a.executor().finish_waiting();
|
||||
cx_a.executor().run_until_parked();
|
||||
|
||||
// Confirm a completion on the guest.
|
||||
editor_b
|
||||
.update(cx_b, |editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.executor().run_until_parked();
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// The additional edit is applied.
|
||||
cx_a.executor().run_until_parked();
|
||||
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
@@ -540,15 +516,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// When accepting the completion, the snippet is insert.
|
||||
editor_b
|
||||
.update(cx_b, |editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"
|
||||
|
||||
@@ -363,12 +363,10 @@ mod tests {
|
||||
|
||||
// Confirming a completion inserts it and hides the context menu, without showing
|
||||
// the copilot suggestion afterwards.
|
||||
editor.confirm_completion(&Default::default(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor
|
||||
.confirm_completion(&Default::default(), cx)
|
||||
.unwrap()
|
||||
.detach();
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{ops::ControlFlow, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::{channel::oneshot, FutureExt};
|
||||
use gpui::{Task, ViewContext};
|
||||
@@ -7,7 +7,7 @@ use crate::Editor;
|
||||
|
||||
pub struct DebouncedDelay {
|
||||
task: Option<Task<()>>,
|
||||
cancel_channel: Option<oneshot::Sender<ControlFlow<()>>>,
|
||||
cancel_channel: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl DebouncedDelay {
|
||||
@@ -23,22 +23,17 @@ impl DebouncedDelay {
|
||||
F: 'static + Send + FnOnce(&mut Editor, &mut ViewContext<Editor>) -> Task<()>,
|
||||
{
|
||||
if let Some(channel) = self.cancel_channel.take() {
|
||||
channel.send(ControlFlow::Break(())).ok();
|
||||
_ = channel.send(());
|
||||
}
|
||||
|
||||
let (sender, mut receiver) = oneshot::channel::<ControlFlow<()>>();
|
||||
let (sender, mut receiver) = oneshot::channel::<()>();
|
||||
self.cancel_channel = Some(sender);
|
||||
|
||||
drop(self.task.take());
|
||||
self.task = Some(cx.spawn(move |model, mut cx| async move {
|
||||
let mut timer = cx.background_executor().timer(delay).fuse();
|
||||
futures::select_biased! {
|
||||
interrupt = receiver => {
|
||||
match interrupt {
|
||||
Ok(ControlFlow::Break(())) | Err(_) => return,
|
||||
Ok(ControlFlow::Continue(())) => {},
|
||||
}
|
||||
}
|
||||
_ = receiver => return,
|
||||
_ = timer => {}
|
||||
}
|
||||
|
||||
@@ -47,11 +42,4 @@ impl DebouncedDelay {
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn start_now(&mut self) -> Option<Task<()>> {
|
||||
if let Some(channel) = self.cancel_channel.take() {
|
||||
channel.send(ControlFlow::Continue(())).ok();
|
||||
}
|
||||
self.task.take()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4427,49 +4427,16 @@ impl Editor {
|
||||
&mut self,
|
||||
item_ix: Option<usize>,
|
||||
intent: CompletionIntent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Task<anyhow::Result<()>>> {
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
|
||||
use language::ToOffset as _;
|
||||
|
||||
let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
|
||||
menu
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut resolve_task_store = completions_menu
|
||||
.selected_completion_documentation_resolve_debounce
|
||||
.lock();
|
||||
let selected_completion_resolve = resolve_task_store.start_now();
|
||||
let menu_pre_resolve = self
|
||||
.completion_documentation_pre_resolve_debounce
|
||||
.start_now();
|
||||
drop(resolve_task_store);
|
||||
|
||||
Some(cx.spawn(|editor, mut cx| async move {
|
||||
match (selected_completion_resolve, menu_pre_resolve) {
|
||||
(None, None) => {}
|
||||
(Some(resolve), None) | (None, Some(resolve)) => resolve.await,
|
||||
(Some(resolve_1), Some(resolve_2)) => {
|
||||
futures::join!(resolve_1, resolve_2);
|
||||
}
|
||||
}
|
||||
if let Some(apply_edits_task) = editor.update(&mut cx, |editor, cx| {
|
||||
editor.apply_resolved_completion(completions_menu, item_ix, intent, cx)
|
||||
})? {
|
||||
apply_edits_task.await?;
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn apply_resolved_completion(
|
||||
&mut self,
|
||||
completions_menu: CompletionsMenu,
|
||||
item_ix: Option<usize>,
|
||||
intent: CompletionIntent,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> Option<Task<anyhow::Result<Option<language::Transaction>>>> {
|
||||
use language::ToOffset as _;
|
||||
|
||||
let mat = completions_menu
|
||||
.matches
|
||||
.get(item_ix.unwrap_or(completions_menu.selected_item))?;
|
||||
@@ -4628,7 +4595,11 @@ impl Editor {
|
||||
// so we should automatically call signature_help
|
||||
self.show_signature_help(&ShowSignatureHelp, cx);
|
||||
}
|
||||
Some(apply_edits)
|
||||
|
||||
Some(cx.foreground_executor().spawn(async move {
|
||||
apply_edits.await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
|
||||
|
||||
@@ -7996,7 +7996,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.ˇ
|
||||
one.second_completionˇ
|
||||
two
|
||||
three
|
||||
"});
|
||||
@@ -8029,9 +8029,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completionˇ
|
||||
two
|
||||
thoverlapping additional editree
|
||||
|
||||
additional edit"});
|
||||
three
|
||||
additional edit
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
one.second_completion
|
||||
@@ -8091,8 +8091,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completion
|
||||
two siˇ
|
||||
three siˇ
|
||||
two sixth_completionˇ
|
||||
three sixth_completionˇ
|
||||
additional edit
|
||||
"});
|
||||
|
||||
@@ -8133,11 +8133,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state("editor.cloˇ");
|
||||
cx.assert_editor_state("editor.closeˇ");
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
editor.closeˇ"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -10142,7 +10140,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
|
||||
cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
|
||||
|
||||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
|
||||
let task_completion_item = completion_item.clone();
|
||||
|
||||
@@ -821,7 +821,7 @@ mod tests {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
resolve_provider: Some(false),
|
||||
resolve_provider: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
@@ -913,15 +913,12 @@ mod tests {
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
|
||||
//apply a completion and check it was successfully applied
|
||||
let () = cx
|
||||
.update_editor(|editor, cx| {
|
||||
editor.context_menu_next(&Default::default(), cx);
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let _apply_additional_edits = cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_next(&Default::default(), cx);
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completionˇ
|
||||
two
|
||||
|
||||
@@ -13,7 +13,6 @@ use itertools::Itertools;
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry};
|
||||
use multi_buffer::{ExcerptRange, ToPoint};
|
||||
use parking_lot::RwLock;
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::{FakeFs, Project};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
|
||||
@@ -47,6 +47,7 @@ settings.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
theme.workspace = true
|
||||
thiserror.workspace = true
|
||||
tiktoken-rs.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -7,7 +7,10 @@ use crate::{
|
||||
};
|
||||
use anthropic::AnthropicError;
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{Client, PerformCompletionParams, UserStore, EXPIRED_LLM_TOKEN_HEADER_NAME};
|
||||
use client::{
|
||||
Client, PerformCompletionParams, UserStore, EXPIRED_LLM_TOKEN_HEADER_NAME,
|
||||
MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME,
|
||||
};
|
||||
use collections::BTreeMap;
|
||||
use feature_flags::{FeatureFlagAppExt, LlmClosedBeta, ZedPro};
|
||||
use futures::{
|
||||
@@ -15,10 +18,11 @@ use futures::{
|
||||
TryStreamExt as _,
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, AnyView, AppContext, AsyncAppContext, FontWeight, Model, ModelContext,
|
||||
Subscription, Task,
|
||||
AnyElement, AnyView, AppContext, AsyncAppContext, EventEmitter, FontWeight, Global, Model,
|
||||
ModelContext, ReadGlobal, Subscription, Task,
|
||||
};
|
||||
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Response};
|
||||
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Response, StatusCode};
|
||||
use proto::TypedEnvelope;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
@@ -27,12 +31,14 @@ use smol::{
|
||||
io::{AsyncReadExt, BufReader},
|
||||
lock::{RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard},
|
||||
};
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
future,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
use strum::IntoEnumIterator;
|
||||
use thiserror::Error;
|
||||
use ui::{prelude::*, TintColor};
|
||||
|
||||
use crate::{LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider};
|
||||
@@ -90,22 +96,93 @@ pub struct AvailableModel {
|
||||
pub default_temperature: Option<f32>,
|
||||
}
|
||||
|
||||
struct GlobalRefreshLlmTokenListener(Model<RefreshLlmTokenListener>);
|
||||
|
||||
impl Global for GlobalRefreshLlmTokenListener {}
|
||||
|
||||
pub struct RefreshLlmTokenEvent;
|
||||
|
||||
pub struct RefreshLlmTokenListener {
|
||||
_llm_token_subscription: client::Subscription,
|
||||
}
|
||||
|
||||
impl EventEmitter<RefreshLlmTokenEvent> for RefreshLlmTokenListener {}
|
||||
|
||||
impl RefreshLlmTokenListener {
|
||||
pub fn register(client: Arc<Client>, cx: &mut AppContext) {
|
||||
let listener = cx.new_model(|cx| RefreshLlmTokenListener::new(client, cx));
|
||||
cx.set_global(GlobalRefreshLlmTokenListener(listener));
|
||||
}
|
||||
|
||||
pub fn global(cx: &AppContext) -> Model<Self> {
|
||||
GlobalRefreshLlmTokenListener::global(cx).0.clone()
|
||||
}
|
||||
|
||||
fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
|
||||
Self {
|
||||
_llm_token_subscription: client
|
||||
.add_message_handler(cx.weak_model(), Self::handle_refresh_llm_token),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_refresh_llm_token(
|
||||
this: Model<Self>,
|
||||
_: TypedEnvelope<proto::RefreshLlmToken>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |_this, cx| cx.emit(RefreshLlmTokenEvent))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CloudLanguageModelProvider {
|
||||
client: Arc<Client>,
|
||||
llm_api_token: LlmApiToken,
|
||||
state: gpui::Model<State>,
|
||||
_maintain_client_status: Task<()>,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
client: Arc<Client>,
|
||||
llm_api_token: LlmApiToken,
|
||||
user_store: Model<UserStore>,
|
||||
status: client::Status,
|
||||
accept_terms: Option<Task<Result<()>>>,
|
||||
_subscription: Subscription,
|
||||
_settings_subscription: Subscription,
|
||||
_llm_token_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn new(
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
status: client::Status,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx);
|
||||
|
||||
Self {
|
||||
client: client.clone(),
|
||||
llm_api_token: LlmApiToken::default(),
|
||||
user_store,
|
||||
status,
|
||||
accept_terms: None,
|
||||
_settings_subscription: cx.observe_global::<SettingsStore>(|_, cx| {
|
||||
cx.notify();
|
||||
}),
|
||||
_llm_token_subscription: cx.subscribe(
|
||||
&refresh_llm_token_listener,
|
||||
|this, _listener, _event, cx| {
|
||||
let client = this.client.clone();
|
||||
let llm_api_token = this.llm_api_token.clone();
|
||||
cx.spawn(|_this, _cx| async move {
|
||||
llm_api_token.refresh(&client).await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_signed_out(&self) -> bool {
|
||||
self.status.is_signed_out()
|
||||
}
|
||||
@@ -144,15 +221,7 @@ impl CloudLanguageModelProvider {
|
||||
let mut status_rx = client.status();
|
||||
let status = *status_rx.borrow();
|
||||
|
||||
let state = cx.new_model(|cx| State {
|
||||
client: client.clone(),
|
||||
user_store,
|
||||
status,
|
||||
accept_terms: None,
|
||||
_subscription: cx.observe_global::<SettingsStore>(|_, cx| {
|
||||
cx.notify();
|
||||
}),
|
||||
});
|
||||
let state = cx.new_model(|cx| State::new(client.clone(), user_store.clone(), status, cx));
|
||||
|
||||
let state_ref = state.downgrade();
|
||||
let maintain_client_status = cx.spawn(|mut cx| async move {
|
||||
@@ -172,8 +241,7 @@ impl CloudLanguageModelProvider {
|
||||
|
||||
Self {
|
||||
client,
|
||||
state,
|
||||
llm_api_token: LlmApiToken::default(),
|
||||
state: state.clone(),
|
||||
_maintain_client_status: maintain_client_status,
|
||||
}
|
||||
}
|
||||
@@ -272,13 +340,14 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||
models.insert(model.id().to_string(), model.clone());
|
||||
}
|
||||
|
||||
let llm_api_token = self.state.read(cx).llm_api_token.clone();
|
||||
models
|
||||
.into_values()
|
||||
.map(|model| {
|
||||
Arc::new(CloudLanguageModel {
|
||||
id: LanguageModelId::from(model.id().to_string()),
|
||||
model,
|
||||
llm_api_token: self.llm_api_token.clone(),
|
||||
llm_api_token: llm_api_token.clone(),
|
||||
client: self.client.clone(),
|
||||
request_limiter: RateLimiter::new(4),
|
||||
}) as Arc<dyn LanguageModel>
|
||||
@@ -377,6 +446,30 @@ pub struct CloudLanguageModel {
|
||||
#[derive(Clone, Default)]
|
||||
struct LlmApiToken(Arc<RwLock<Option<String>>>);
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub struct PaymentRequiredError;
|
||||
|
||||
impl fmt::Display for PaymentRequiredError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Payment required to use this language model. Please upgrade your account."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub struct MaxMonthlySpendReachedError;
|
||||
|
||||
impl fmt::Display for MaxMonthlySpendReachedError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Maximum spending limit reached for this month. For more usage, increase your spending limit."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CloudLanguageModel {
|
||||
async fn perform_llm_completion(
|
||||
client: Arc<Client>,
|
||||
@@ -411,6 +504,15 @@ impl CloudLanguageModel {
|
||||
{
|
||||
did_retry = true;
|
||||
token = llm_api_token.refresh(&client).await?;
|
||||
} else if response.status() == StatusCode::FORBIDDEN
|
||||
&& response
|
||||
.headers()
|
||||
.get(MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME)
|
||||
.is_some()
|
||||
{
|
||||
break Err(anyhow!(MaxMonthlySpendReachedError))?;
|
||||
} else if response.status() == StatusCode::PAYMENT_REQUIRED {
|
||||
break Err(anyhow!(PaymentRequiredError))?;
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::provider::cloud::RefreshLlmTokenListener;
|
||||
use crate::{
|
||||
provider::{
|
||||
anthropic::AnthropicLanguageModelProvider, cloud::CloudLanguageModelProvider,
|
||||
@@ -30,6 +31,8 @@ fn register_language_model_providers(
|
||||
) {
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
|
||||
RefreshLlmTokenListener::register(client.clone(), cx);
|
||||
|
||||
registry.register_provider(
|
||||
AnthropicLanguageModelProvider::new(client.http_client(), cx),
|
||||
cx,
|
||||
|
||||
@@ -626,12 +626,9 @@ impl LanguageServer {
|
||||
properties: vec![
|
||||
"additionalTextEdits".to_string(),
|
||||
"command".to_string(),
|
||||
"detail".to_string(),
|
||||
"documentation".to_string(),
|
||||
"filterText".to_string(),
|
||||
"labelDetails".to_string(),
|
||||
"tags".to_string(),
|
||||
"textEdit".to_string(),
|
||||
// NB: Do not have this resolved, otherwise Zed becomes slow to complete things
|
||||
// "textEdit".to_string(),
|
||||
],
|
||||
}),
|
||||
insert_replace_support: Some(true),
|
||||
|
||||
@@ -7656,10 +7656,16 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
|
||||
}
|
||||
|
||||
async fn read_text_file(&self, path: PathBuf) -> Result<String> {
|
||||
if self.worktree.entry_for_path(&path).is_none() {
|
||||
return Err(anyhow!("no such path {path:?}"));
|
||||
};
|
||||
self.fs.load(&path).await
|
||||
let entry = self
|
||||
.worktree
|
||||
.entry_for_path(&path)
|
||||
.with_context(|| format!("no worktree entry for path {path:?}"))?;
|
||||
let abs_path = self
|
||||
.worktree
|
||||
.absolutize(&entry.path)
|
||||
.with_context(|| format!("cannot absolutize path {path:?}"))?;
|
||||
|
||||
self.fs.load(&abs_path).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1906,11 +1906,9 @@ impl ProjectPanel {
|
||||
Some(Arc::<Path>::from(full_path.join(suffix)))
|
||||
})
|
||||
})
|
||||
.or_else(|| entry.path.file_name().map(Path::new).map(Arc::from))
|
||||
.unwrap_or_else(|| entry.path.clone());
|
||||
let depth = path
|
||||
.strip_prefix(worktree_abs_path)
|
||||
.map(|suffix| suffix.components().count())
|
||||
.unwrap_or_default();
|
||||
let depth = path.components().count();
|
||||
(depth, path)
|
||||
};
|
||||
let width_estimate = item_width_estimate(
|
||||
|
||||
@@ -271,6 +271,7 @@ message Envelope {
|
||||
|
||||
GetLlmToken get_llm_token = 235;
|
||||
GetLlmTokenResponse get_llm_token_response = 236;
|
||||
RefreshLlmToken refresh_llm_token = 259; // current max
|
||||
|
||||
LspExtSwitchSourceHeader lsp_ext_switch_source_header = 241;
|
||||
LspExtSwitchSourceHeaderResponse lsp_ext_switch_source_header_response = 242;
|
||||
@@ -284,7 +285,7 @@ message Envelope {
|
||||
CheckFileExists check_file_exists = 255;
|
||||
CheckFileExistsResponse check_file_exists_response = 256;
|
||||
|
||||
ShutdownRemoteServer shutdown_remote_server = 257; // current max
|
||||
ShutdownRemoteServer shutdown_remote_server = 257;
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
@@ -2482,6 +2483,8 @@ message GetLlmTokenResponse {
|
||||
string token = 1;
|
||||
}
|
||||
|
||||
message RefreshLlmToken {}
|
||||
|
||||
// Remote FS
|
||||
|
||||
message AddWorktree {
|
||||
|
||||
@@ -253,6 +253,7 @@ messages!(
|
||||
(ProjectEntryResponse, Foreground),
|
||||
(CountLanguageModelTokens, Background),
|
||||
(CountLanguageModelTokensResponse, Background),
|
||||
(RefreshLlmToken, Background),
|
||||
(RefreshInlayHints, Foreground),
|
||||
(RejoinChannelBuffers, Foreground),
|
||||
(RejoinChannelBuffersResponse, Foreground),
|
||||
|
||||
@@ -563,6 +563,7 @@ impl SshRemoteClient {
|
||||
return Ok(());
|
||||
}
|
||||
drop(lock);
|
||||
|
||||
self.set_state(State::Reconnecting, cx);
|
||||
|
||||
log::info!("Trying to reconnect to ssh server... Attempt {}", attempts);
|
||||
@@ -734,6 +735,7 @@ impl SshRemoteClient {
|
||||
} else {
|
||||
state.heartbeat_recovered()
|
||||
};
|
||||
|
||||
self.set_state(next_state, cx);
|
||||
|
||||
if missed_heartbeats >= MAX_MISSED_HEARTBEATS {
|
||||
@@ -877,8 +879,12 @@ impl SshRemoteClient {
|
||||
cx: &mut ModelContext<Self>,
|
||||
map: impl FnOnce(&State) -> Option<State>,
|
||||
) {
|
||||
if let Some(new_state) = self.state.lock().as_ref().and_then(map) {
|
||||
self.set_state(new_state, cx);
|
||||
let mut lock = self.state.lock();
|
||||
let new_state = lock.as_ref().and_then(map);
|
||||
|
||||
if let Some(new_state) = new_state {
|
||||
lock.replace(new_state);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ use strum::{Display, EnumIter, EnumString};
|
||||
|
||||
pub const EXPIRED_LLM_TOKEN_HEADER_NAME: &str = "x-zed-expired-token";
|
||||
|
||||
pub const MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME: &str = "x-zed-llm-max-monthly-spend-reached";
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, EnumString, EnumIter, Display,
|
||||
)]
|
||||
|
||||
@@ -221,11 +221,7 @@ impl PathWithPosition {
|
||||
pub fn parse_str(s: &str) -> Self {
|
||||
let trimmed = s.trim();
|
||||
let path = Path::new(trimmed);
|
||||
let maybe_file_name_with_row_col = path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default();
|
||||
let maybe_file_name_with_row_col = path.file_name().unwrap_or_default().to_string_lossy();
|
||||
if maybe_file_name_with_row_col.is_empty() {
|
||||
return Self {
|
||||
path: Path::new(s).to_path_buf(),
|
||||
@@ -240,7 +236,7 @@ impl PathWithPosition {
|
||||
static SUFFIX_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(ROW_COL_CAPTURE_REGEX).unwrap());
|
||||
match SUFFIX_RE
|
||||
.captures(maybe_file_name_with_row_col)
|
||||
.captures(&maybe_file_name_with_row_col)
|
||||
.map(|caps| caps.extract())
|
||||
{
|
||||
Some((_, [file_name, maybe_row, maybe_column])) => {
|
||||
@@ -361,26 +357,26 @@ pub fn compare_paths(
|
||||
let b_is_file = components_b.peek().is_none() && b_is_file;
|
||||
let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
|
||||
let path_a = Path::new(component_a.as_os_str());
|
||||
let num_and_remainder_a = NumericPrefixWithSuffix::from_numeric_prefixed_str(
|
||||
if a_is_file {
|
||||
path_a.file_stem()
|
||||
} else {
|
||||
path_a.file_name()
|
||||
}
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
let path_string_a = if a_is_file {
|
||||
path_a.file_stem()
|
||||
} else {
|
||||
path_a.file_name()
|
||||
}
|
||||
.map(|s| s.to_string_lossy());
|
||||
let num_and_remainder_a = path_string_a
|
||||
.as_deref()
|
||||
.map(NumericPrefixWithSuffix::from_numeric_prefixed_str);
|
||||
|
||||
let path_b = Path::new(component_b.as_os_str());
|
||||
let num_and_remainder_b = NumericPrefixWithSuffix::from_numeric_prefixed_str(
|
||||
if b_is_file {
|
||||
path_b.file_stem()
|
||||
} else {
|
||||
path_b.file_name()
|
||||
}
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
let path_string_b = if b_is_file {
|
||||
path_b.file_stem()
|
||||
} else {
|
||||
path_b.file_name()
|
||||
}
|
||||
.map(|s| s.to_string_lossy());
|
||||
let num_and_remainder_b = path_string_b
|
||||
.as_deref()
|
||||
.map(NumericPrefixWithSuffix::from_numeric_prefixed_str);
|
||||
|
||||
num_and_remainder_a.cmp(&num_and_remainder_b)
|
||||
});
|
||||
|
||||
@@ -387,7 +387,7 @@ mod test {
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
resolve_provider: Some(false),
|
||||
resolve_provider: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
@@ -432,9 +432,7 @@ mod test {
|
||||
request.next().await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.simulate_keystrokes("down enter");
|
||||
cx.run_until_parked();
|
||||
cx.simulate_keystrokes("! escape");
|
||||
cx.simulate_keystrokes("down enter ! escape");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.157.0"
|
||||
version = "0.157.5"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
stable
|
||||
@@ -58,8 +58,9 @@ use workspace::{
|
||||
AppState, WorkspaceSettings, WorkspaceStore,
|
||||
};
|
||||
use zed::{
|
||||
app_menus, build_window_options, handle_cli_connection, handle_keymap_file_changes,
|
||||
initialize_workspace, open_paths_with_positions, OpenListener, OpenRequest,
|
||||
app_menus, build_window_options, derive_paths_with_position, handle_cli_connection,
|
||||
handle_keymap_file_changes, initialize_workspace, open_paths_with_positions, OpenListener,
|
||||
OpenRequest,
|
||||
};
|
||||
|
||||
use crate::zed::inline_completion_registry;
|
||||
@@ -712,13 +713,11 @@ fn handle_open_request(
|
||||
|
||||
if let Some(connection_info) = request.ssh_connection {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let paths_with_position =
|
||||
derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await;
|
||||
open_ssh_project(
|
||||
connection_info,
|
||||
request
|
||||
.open_paths
|
||||
.into_iter()
|
||||
.map(|path| path.path)
|
||||
.collect::<Vec<_>>(),
|
||||
paths_with_position.into_iter().map(|p| p.path).collect(),
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
&mut cx,
|
||||
@@ -733,8 +732,10 @@ fn handle_open_request(
|
||||
if !request.open_paths.is_empty() {
|
||||
let app_state = app_state.clone();
|
||||
task = Some(cx.spawn(|mut cx| async move {
|
||||
let paths_with_position =
|
||||
derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await;
|
||||
let (_window, results) = open_paths_with_positions(
|
||||
&request.open_paths,
|
||||
&paths_with_position,
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
&mut cx,
|
||||
|
||||
@@ -9,12 +9,15 @@ use collections::HashMap;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::scroll::Autoscroll;
|
||||
use editor::Editor;
|
||||
use fs::Fs;
|
||||
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::future::join_all;
|
||||
use futures::{FutureExt, SinkExt, StreamExt};
|
||||
use gpui::{AppContext, AsyncAppContext, Global, WindowHandle};
|
||||
use language::{Bias, Point};
|
||||
use remote::SshConnectionOptions;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{process, thread};
|
||||
@@ -27,7 +30,7 @@ use workspace::{AppState, OpenOptions, Workspace};
|
||||
#[derive(Default, Debug)]
|
||||
pub struct OpenRequest {
|
||||
pub cli_connection: Option<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)>,
|
||||
pub open_paths: Vec<PathWithPosition>,
|
||||
pub open_paths: Vec<String>,
|
||||
pub open_channel_notes: Vec<(u64, Option<String>)>,
|
||||
pub join_channel: Option<u64>,
|
||||
pub ssh_connection: Option<SshConnectionOptions>,
|
||||
@@ -57,8 +60,7 @@ impl OpenRequest {
|
||||
|
||||
fn parse_file_path(&mut self, file: &str) {
|
||||
if let Some(decoded) = urlencoding::decode(file).log_err() {
|
||||
let path_buf = PathWithPosition::parse_str(&decoded);
|
||||
self.open_paths.push(path_buf)
|
||||
self.open_paths.push(decoded.into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,26 +371,15 @@ async fn open_workspaces(
|
||||
location
|
||||
.paths()
|
||||
.iter()
|
||||
.map(|path| PathWithPosition {
|
||||
path: path.clone(),
|
||||
row: None,
|
||||
column: None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.collect()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
} else {
|
||||
// If paths are provided, parse them (they include positions)
|
||||
let paths_with_position = paths
|
||||
.into_iter()
|
||||
.map(|path_with_position_string| {
|
||||
PathWithPosition::parse_str(&path_with_position_string)
|
||||
})
|
||||
.collect();
|
||||
vec![paths_with_position]
|
||||
vec![paths]
|
||||
};
|
||||
|
||||
if grouped_paths.is_empty() {
|
||||
@@ -441,7 +432,7 @@ async fn open_workspaces(
|
||||
}
|
||||
|
||||
async fn open_workspace(
|
||||
workspace_paths: Vec<PathWithPosition>,
|
||||
workspace_paths: Vec<String>,
|
||||
open_new_workspace: Option<bool>,
|
||||
wait: bool,
|
||||
responses: &IpcSender<CliResponse>,
|
||||
@@ -451,8 +442,10 @@ async fn open_workspace(
|
||||
) -> bool {
|
||||
let mut errored = false;
|
||||
|
||||
let paths_with_position =
|
||||
derive_paths_with_position(app_state.fs.as_ref(), workspace_paths).await;
|
||||
match open_paths_with_positions(
|
||||
&workspace_paths,
|
||||
&paths_with_position,
|
||||
app_state.clone(),
|
||||
workspace::OpenOptions {
|
||||
open_new_workspace,
|
||||
@@ -466,7 +459,7 @@ async fn open_workspace(
|
||||
Ok((workspace, items)) => {
|
||||
let mut item_release_futures = Vec::new();
|
||||
|
||||
for (item, path) in items.into_iter().zip(&workspace_paths) {
|
||||
for (item, path) in items.into_iter().zip(&paths_with_position) {
|
||||
match item {
|
||||
Some(Ok(item)) => {
|
||||
cx.update(|cx| {
|
||||
@@ -497,7 +490,7 @@ async fn open_workspace(
|
||||
if wait {
|
||||
let background = cx.background_executor().clone();
|
||||
let wait = async move {
|
||||
if workspace_paths.is_empty() {
|
||||
if paths_with_position.is_empty() {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
let _subscription = workspace.update(cx, |_, cx| {
|
||||
cx.on_release(move |_, _, _| {
|
||||
@@ -532,7 +525,7 @@ async fn open_workspace(
|
||||
errored = true;
|
||||
responses
|
||||
.send(CliResponse::Stderr {
|
||||
message: format!("error opening {workspace_paths:?}: {error}"),
|
||||
message: format!("error opening {paths_with_position:?}: {error}"),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
@@ -540,9 +533,26 @@ async fn open_workspace(
|
||||
errored
|
||||
}
|
||||
|
||||
pub async fn derive_paths_with_position(
|
||||
fs: &dyn Fs,
|
||||
path_strings: impl IntoIterator<Item = impl AsRef<str>>,
|
||||
) -> Vec<PathWithPosition> {
|
||||
join_all(path_strings.into_iter().map(|path_str| async move {
|
||||
let canonicalized = fs.canonicalize(Path::new(path_str.as_ref())).await;
|
||||
(path_str, canonicalized)
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|(original, canonicalized)| match canonicalized {
|
||||
Ok(canonicalized) => PathWithPosition::from_path(canonicalized),
|
||||
Err(_) => PathWithPosition::parse_str(original.as_ref()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use cli::{
|
||||
ipc::{self},
|
||||
@@ -551,7 +561,6 @@ mod tests {
|
||||
use editor::Editor;
|
||||
use gpui::TestAppContext;
|
||||
use serde_json::json;
|
||||
use util::paths::PathWithPosition;
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
use crate::zed::{open_listener::open_workspace, tests::init_test};
|
||||
@@ -665,12 +674,7 @@ mod tests {
|
||||
) {
|
||||
let (response_tx, _) = ipc::channel::<CliResponse>().unwrap();
|
||||
|
||||
let path = PathBuf::from(path);
|
||||
let workspace_paths = vec![PathWithPosition {
|
||||
path,
|
||||
row: None,
|
||||
column: None,
|
||||
}];
|
||||
let workspace_paths = vec![path.to_owned()];
|
||||
|
||||
let errored = cx
|
||||
.spawn(|mut cx| async move {
|
||||
|
||||
Reference in New Issue
Block a user