Compare commits

..

2 Commits

Author SHA1 Message Date
Nathan Sobo
c7e68a043e Update test assertions to pass with new cursor bias
This is a behavior change, but I honestly think it's an improvement.
2024-05-22 20:46:37 -06:00
Nathan Sobo
79ee03f1a1 Preserve a trailing user message in the context editor
To make this work, I had to switch the bias of selections to always
be left on the start and the end. Worried about what this could break,
but we've wanted to make this change for a long time anyway.
2024-05-22 20:30:03 -06:00
20 changed files with 244 additions and 670 deletions

3
Cargo.lock generated
View File

@@ -8067,7 +8067,6 @@ name = "recent_projects"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"dev_server_projects",
"editor",
"feature_flags",
@@ -10153,7 +10152,6 @@ dependencies = [
"gpui",
"language",
"menu",
"parking_lot",
"picker",
"project",
"schemars",
@@ -10161,7 +10159,6 @@ dependencies = [
"serde_json",
"settings",
"task",
"text",
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",

View File

@@ -1625,6 +1625,7 @@ impl Conversation {
slash_command_registry,
};
this.set_language(cx);
this.ensure_trailing_user_message(cx);
this.reparse_edit_suggestions(cx);
this.count_remaining_tokens(cx);
this
@@ -1704,13 +1705,33 @@ impl Conversation {
cx: &mut ModelContext<Self>,
) {
if *event == language::Event::Edited {
self.count_remaining_tokens(cx);
self.ensure_trailing_user_message(cx);
self.reparse_edit_suggestions(cx);
self.reparse_slash_command_calls(cx);
self.count_remaining_tokens(cx);
cx.emit(ConversationEvent::MessagesEdited);
}
}
/// If we find a valid trailing message that isn't a user message, then add one
fn ensure_trailing_user_message(&mut self, cx: &mut ModelContext<Self>) {
let mut trailing_id = None;
for MessageAnchor { id, start } in self.message_anchors.iter().rev() {
if start.is_valid(self.buffer.read(cx)) {
if self.messages_metadata[id].role == Role::User {
return;
} else {
trailing_id = Some(*id);
break;
}
}
}
if let Some(id) = trailing_id {
self.insert_message_after(id, Role::User, MessageStatus::Done, cx);
}
}
pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
let request = self.to_completion_request(cx);
self.pending_token_count = cx.spawn(|this, mut cx| {
@@ -2207,6 +2228,7 @@ impl Conversation {
for id in ids {
if let Some(metadata) = self.messages_metadata.get_mut(&id) {
metadata.role.cycle();
self.ensure_trailing_user_message(cx);
cx.emit(ConversationEvent::MessagesEdited);
cx.notify();
}

View File

@@ -6958,7 +6958,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
cx.assert_editor_state(indoc!(
"fn a() {
// dog();
catˇ();
ˇcat();
}"
));
@@ -6974,7 +6974,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
});
cx.assert_editor_state(indoc!(
"fn a() {
// «dog()ˇ»;
«// dog()ˇ»;
cat();
}"
));
@@ -6992,7 +6992,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
cx.assert_editor_state(indoc!(
"fn a() {
// dog();
catˇ(ˇ);
ˇcat(ˇ);
}"
));
@@ -7008,7 +7008,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
});
cx.assert_editor_state(indoc!(
"fn a() {
// ˇdˇog«()ˇ»;
ˇ// dˇog«()ˇ»;
cat();
}"
));

View File

@@ -534,20 +534,14 @@ impl<'a> MutableSelectionsCollection<'a> {
}
}
self.collection.disjoint = Arc::from_iter(selections.into_iter().map(|selection| {
let end_bias = if selection.end > selection.start {
Bias::Left
} else {
Bias::Right
};
Selection {
self.collection.disjoint =
Arc::from_iter(selections.into_iter().map(|selection| Selection {
id: selection.id,
start: buffer.anchor_after(selection.start),
end: buffer.anchor_at(selection.end, end_bias),
start: buffer.anchor_before(selection.start),
end: buffer.anchor_before(selection.end),
reversed: selection.reversed,
goal: selection.goal,
}
}));
}));
self.collection.pending = None;
self.selections_changed = true;

View File

@@ -49,11 +49,6 @@ pub trait UpdateGlobal {
where
C: BorrowAppContext,
F: FnOnce(&mut Self, &mut C) -> R;
/// Set the global instance of the implementing type.
fn set_global<C>(cx: &mut C, global: Self)
where
C: BorrowAppContext;
}
impl<T: Global> UpdateGlobal for T {
@@ -64,11 +59,4 @@ impl<T: Global> UpdateGlobal for T {
{
cx.update_global(update)
}
fn set_global<C>(cx: &mut C, global: Self)
where
C: BorrowAppContext,
{
cx.set_global(global)
}
}

View File

@@ -1,5 +1,5 @@
use anyhow::Result;
use editor::{scroll::Autoscroll, CompletionProvider, Editor};
use editor::{scroll::Autoscroll, Editor};
use gpui::{
actions, div, impl_actions, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent,
DismissEvent, EventEmitter, FocusHandle, FocusableView, Length, ListState, MouseButton,
@@ -166,17 +166,6 @@ impl<D: PickerDelegate> Picker<D> {
Self::new(delegate, ContainerKind::List, head, cx)
}
/// Adds a completion provider for this pickers query editor, if it has one.
pub fn with_completions_provider(
self,
provider: Box<dyn CompletionProvider>,
cx: &mut WindowContext<'_>,
) -> Self {
if let Head::Editor(editor) = &self.head {
editor.update(cx, |this, _| this.set_completion_provider(provider))
}
self
}
fn new(delegate: D, container: ContainerKind, head: Head, cx: &mut ViewContext<Self>) -> Self {
let mut this = Self {
delegate,

View File

@@ -14,7 +14,6 @@ doctest = false
[dependencies]
anyhow.workspace = true
client.workspace = true
editor.workspace = true
feature_flags.workspace = true
fuzzy.workspace = true

View File

@@ -1,12 +1,10 @@
use std::time::Duration;
use anyhow::anyhow;
use anyhow::Context;
use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
use editor::Editor;
use feature_flags::FeatureFlagAppExt;
use feature_flags::FeatureFlagViewExt;
use gpui::AsyncWindowContext;
use gpui::Subscription;
use gpui::Task;
use gpui::WeakView;
@@ -49,9 +47,9 @@ pub struct DevServerProjects {
_dev_server_subscription: Subscription,
}
#[derive(Default)]
#[derive(Default, Clone)]
struct CreateDevServer {
creating: Option<Task<()>>,
creating: bool,
dev_server_id: Option<DevServerId>,
access_token: Option<String>,
manual_setup: bool,
@@ -314,77 +312,95 @@ impl DevServerProjects {
});
let workspace = self.workspace.clone();
let store = dev_server_projects::Store::global(cx);
let task = cx
.spawn({
|this, mut cx| async move {
let result = dev_server.await;
cx.spawn({
let access_token = access_token.clone();
|this, mut cx| async move {
let result = dev_server.await;
match result {
Ok(dev_server) => {
if let Some(ssh_connection_string) = ssh_connection_string {
this.update(&mut cx, |this, cx| {
if let Mode::CreateDevServer(CreateDevServer {
access_token,
dev_server_id,
..
}) = &mut this.mode
{
access_token.replace(dev_server.access_token.clone());
dev_server_id
.replace(DevServerId(dev_server.dev_server_id));
}
cx.notify();
})?;
match result {
Ok(dev_server) => {
if let Some(ssh_connection_string) = ssh_connection_string {
spawn_ssh_task(
workspace
.upgrade()
.ok_or_else(|| anyhow!("workspace dropped"))?,
store,
DevServerId(dev_server.dev_server_id),
ssh_connection_string,
dev_server.access_token.clone(),
&mut cx,
)
.await
.log_err();
}
this.update(&mut cx, |this, cx| {
let access_token = access_token.clone();
this.update(&mut cx, |this, cx| {
this.focus_handle.focus(cx);
this.mode = Mode::CreateDevServer(CreateDevServer {
creating: None,
creating: true,
dev_server_id: Some(DevServerId(dev_server.dev_server_id)),
access_token: Some(dev_server.access_token),
manual_setup,
});
access_token: Some(access_token.unwrap_or(dev_server.access_token.clone())),
manual_setup: false,
});
cx.notify();
})?;
Ok(())
}
Err(e) => {
this.update(&mut cx, |this, cx| {
this.mode = Mode::CreateDevServer(CreateDevServer {
creating: None,
dev_server_id: existing_id,
access_token: None,
manual_setup,
});
cx.notify()
})
.log_err();
})?;
let terminal_panel = workspace
.update(&mut cx, |workspace, cx| workspace.panel::<TerminalPanel>(cx))
.ok()
.flatten()
.with_context(|| anyhow::anyhow!("No terminal panel"))?;
return Err(e);
let command = "sh".to_string();
let args = vec!["-x".to_string(),"-c".to_string(),
format!(r#"~/.local/bin/zed -v >/dev/stderr || (curl -sSL https://zed.dev/install.sh || wget -qO- https://zed.dev/install.sh) | bash && ~/.local/bin/zed --dev-server-token {}"#, dev_server.access_token)];
let terminal = terminal_panel.update(&mut cx, |terminal_panel, cx| {
terminal_panel.spawn_in_new_terminal(
SpawnInTerminal {
id: task::TaskId("ssh-remote".into()),
full_label: "Install zed over ssh".into(),
label: "Install zed over ssh".into(),
command,
args,
command_label: ssh_connection_string.clone(),
cwd: Some(TerminalWorkDir::Ssh { ssh_command: ssh_connection_string, path: None }),
env: Default::default(),
use_new_terminal: true,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
},
cx,
)
})?.await?;
terminal.update(&mut cx, |terminal, cx| {
terminal.wait_for_completed_task(cx)
})?.await;
// There's a race-condition between the task completing successfully, and the server sending us the online status. Make it less likely we'll show the error state.
if this.update(&mut cx, |this, cx| {
this.dev_server_store.read(cx).dev_server_status(DevServerId(dev_server.dev_server_id))
})? == DevServerStatus::Offline {
cx.background_executor().timer(Duration::from_millis(200)).await
}
}
}
})
.prompt_err("Failed to create server", cx, |_, _| None);
this.update(&mut cx, |this, cx| {
this.focus_handle.focus(cx);
this.mode = Mode::CreateDevServer(CreateDevServer {
creating: false,
dev_server_id: Some(DevServerId(dev_server.dev_server_id)),
access_token: Some(dev_server.access_token),
manual_setup,
});
cx.notify();
})?;
Ok(())
}
Err(e) => {
this.update(&mut cx, |this, cx| {
this.mode = Mode::CreateDevServer(CreateDevServer { creating:false, dev_server_id: existing_id, access_token: None, manual_setup });
cx.notify()
})
.log_err();
return Err(e)
}
}
}})
.detach_and_prompt_err("Failed to create server", cx, |_, _| None);
self.mode = Mode::CreateDevServer(CreateDevServer {
creating: Some(task),
creating: true,
dev_server_id: existing_id,
access_token,
manual_setup,
@@ -486,7 +502,7 @@ impl DevServerProjects {
self.create_dev_server_project(create_project.dev_server_id, cx);
}
Mode::CreateDevServer(state) => {
if state.creating.is_none() || state.dev_server_id.is_some() {
if !state.creating {
self.create_or_update_dev_server(
state.manual_setup,
state.dev_server_id,
@@ -563,7 +579,7 @@ impl DevServerProjects {
.on_click(cx.listener(move |this, _, cx| {
this.mode = Mode::CreateDevServer(CreateDevServer {
dev_server_id: Some(dev_server_id),
creating: None,
creating: false,
access_token: None,
manual_setup,
});
@@ -698,14 +714,16 @@ impl DevServerProjects {
}
fn render_create_dev_server(
&self,
state: &CreateDevServer,
&mut self,
state: CreateDevServer,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let creating = state.creating.is_some();
let dev_server_id = state.dev_server_id;
let access_token = state.access_token.clone();
let manual_setup = state.manual_setup;
let CreateDevServer {
creating,
dev_server_id,
access_token,
manual_setup,
} = state.clone();
let status = dev_server_id
.map(|id| self.dev_server_store.read(cx).dev_server_status(id))
@@ -751,11 +769,13 @@ impl DevServerProjects {
Label::new("Connect via SSH (default)"),
!manual_setup,
cx.listener({
let state = state.clone();
move |this, _, cx| {
if let Mode::CreateDevServer(CreateDevServer{ manual_setup, .. }) = &mut this.mode {
*manual_setup = false;
}
cx.notify()
this.mode = Mode::CreateDevServer(CreateDevServer {
manual_setup: false,
..state.clone()
});
cx.notify()
}
}),
))
@@ -764,11 +784,13 @@ impl DevServerProjects {
Label::new("Manual Setup"),
manual_setup,
cx.listener({
let state = state.clone();
move |this, _, cx| {
if let Mode::CreateDevServer(CreateDevServer{ manual_setup, .. }) = &mut this.mode {
*manual_setup = true;
}
cx.notify()
this.mode = Mode::CreateDevServer(CreateDevServer {
manual_setup: true,
..state.clone()
});
cx.notify()
}}),
)))
.when(dev_server_id.is_none(), |el| {
@@ -816,10 +838,10 @@ impl DevServerProjects {
cx.notify();
}))
} else {
Button::new("create-dev-server", if manual_setup { if dev_server_id.is_some() { "Update" } else { "Create"} } else { if dev_server_id.is_some() { "Reconnect" } else { "Connect"} })
Button::new("create-dev-server", if manual_setup { "Create"} else { "Connect"})
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.disabled(creating && dev_server_id.is_none())
.disabled(creating)
.on_click(cx.listener({
let access_token = access_token.clone();
move |this, _, cx| {
@@ -984,115 +1006,18 @@ impl Render for DevServerProjects {
.on_mouse_down_out(cx.listener(|this, _, cx| {
if matches!(this.mode, Mode::Default(None)) {
cx.emit(DismissEvent)
} else {
this.focus_handle(cx).focus(cx);
cx.stop_propagation()
}
}))
.w(rems(34.))
.max_h(rems(40.))
.child(match &self.mode {
Mode::Default(_) => self.render_default(cx).into_any_element(),
Mode::CreateDevServer(state) => {
self.render_create_dev_server(state, cx).into_any_element()
}
Mode::CreateDevServer(state) => self
.render_create_dev_server(state.clone(), cx)
.into_any_element(),
})
}
}
pub fn reconnect_to_dev_server(
workspace: View<Workspace>,
dev_server: DevServer,
cx: &mut WindowContext,
) -> Task<anyhow::Result<()>> {
let Some(ssh_connection_string) = dev_server.ssh_connection_string else {
return Task::ready(Err(anyhow!("can't reconnect, no ssh_connection_string")));
};
let dev_server_store = dev_server_projects::Store::global(cx);
let get_access_token = dev_server_store.update(cx, |store, cx| {
store.regenerate_dev_server_token(dev_server.id, cx)
});
cx.spawn(|mut cx| async move {
let access_token = get_access_token.await?.access_token;
spawn_ssh_task(
workspace,
dev_server_store,
dev_server.id,
ssh_connection_string.to_string(),
access_token,
&mut cx,
)
.await
})
}
pub async fn spawn_ssh_task(
workspace: View<Workspace>,
dev_server_store: Model<dev_server_projects::Store>,
dev_server_id: DevServerId,
ssh_connection_string: String,
access_token: String,
cx: &mut AsyncWindowContext,
) -> anyhow::Result<()> {
let terminal_panel = workspace
.update(cx, |workspace, cx| workspace.panel::<TerminalPanel>(cx))
.ok()
.flatten()
.with_context(|| anyhow!("No terminal panel"))?;
let command = "sh".to_string();
let args = vec![
"-x".to_string(),
"-c".to_string(),
format!(
r#"~/.local/bin/zed -v >/dev/stderr || (curl -sSL https://zed.dev/install.sh || wget -qO- https://zed.dev/install.sh) | bash && ~/.local/bin/zed --dev-server-token {}"#,
access_token
),
];
let ssh_connection_string = ssh_connection_string.to_string();
let terminal = terminal_panel
.update(cx, |terminal_panel, cx| {
terminal_panel.spawn_in_new_terminal(
SpawnInTerminal {
id: task::TaskId("ssh-remote".into()),
full_label: "Install zed over ssh".into(),
label: "Install zed over ssh".into(),
command,
args,
command_label: ssh_connection_string.clone(),
cwd: Some(TerminalWorkDir::Ssh {
ssh_command: ssh_connection_string,
path: None,
}),
env: Default::default(),
use_new_terminal: true,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
},
cx,
)
})?
.await?;
terminal
.update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.await;
// There's a race-condition between the task completing successfully, and the server sending us the online status. Make it less likely we'll show the error state.
if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
== DevServerStatus::Offline
{
cx.background_executor()
.timer(Duration::from_millis(200))
.await
}
if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
== DevServerStatus::Offline
{
return Err(anyhow!("couldn't reconnect"))?;
}
Ok(())
}

View File

@@ -1,7 +1,5 @@
mod dev_servers;
use client::ProjectId;
use dev_servers::reconnect_to_dev_server;
pub use dev_servers::DevServerProjects;
use feature_flags::FeatureFlagAppExt;
use fuzzy::{StringMatch, StringMatchCandidate};
@@ -19,7 +17,6 @@ use serde::Deserialize;
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use ui::{
prelude::*, tooltip_container, ButtonLike, IconWithIndicator, Indicator, KeyBinding, ListItem,
@@ -316,59 +313,73 @@ impl PickerDelegate for RecentProjectsDelegate {
}
}
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
let store = dev_server_projects::Store::global(cx);
let Some(project_id) = store.read(cx)
let store = dev_server_projects::Store::global(cx).read(cx);
let Some(project_id) = store
.dev_server_project(dev_server_project.id)
.and_then(|p| p.project_id)
else {
let server = store.read(cx).dev_server_for_project(dev_server_project.id);
if server.is_some_and(|server| server.ssh_connection_string.is_some()) {
let reconnect = reconnect_to_dev_server(cx.view().clone(), server.unwrap().clone(), cx);
let id = dev_server_project.id;
return cx.spawn(|workspace, mut cx| async move {
reconnect.await?;
cx.background_executor().timer(Duration::from_millis(1000)).await;
if let Some(project_id) = store.update(&mut cx, |store, _| {
store.dev_server_project(id)
.and_then(|p| p.project_id)
})? {
workspace.update(&mut cx, move |_, cx| {
open_dev_server_project(replace_current_window, project_id, cx)
})?.await?;
}
Ok(())
})
let dev_server_name = dev_server_project.dev_server_name.clone();
return cx.spawn(|workspace, mut cx| async move {
let response =
cx.prompt(gpui::PromptLevel::Warning,
"Dev Server is offline",
Some(format!("Cannot connect to {}. To debug open the remote project settings.", dev_server_name).as_str()),
&["Ok", "Open Settings"]
).await?;
if response == 1 {
workspace.update(&mut cx, |workspace, cx| {
let handle = cx.view().downgrade();
workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, handle))
})?;
} else {
workspace.update(&mut cx, |workspace, cx| {
RecentProjects::open(workspace, true, cx);
})?;
}
Ok(())
})
};
if let Some(app_state) = AppState::global(cx).upgrade() {
let handle = if replace_current_window {
cx.window_handle().downcast::<Workspace>()
} else {
let dev_server_name = dev_server_project.dev_server_name.clone();
return cx.spawn(|workspace, mut cx| async move {
let response =
cx.prompt(gpui::PromptLevel::Warning,
"Dev Server is offline",
Some(format!("Cannot connect to {}. To debug open the remote project settings.", dev_server_name).as_str()),
&["Ok", "Open Settings"]
).await?;
if response == 1 {
workspace.update(&mut cx, |workspace, cx| {
let handle = cx.view().downgrade();
workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, handle))
})?;
} else {
workspace.update(&mut cx, |workspace, cx| {
RecentProjects::open(workspace, true, cx);
})?;
}
None
};
if let Some(handle) = handle {
cx.spawn(move |workspace, mut cx| async move {
let continue_replacing = workspace
.update(&mut cx, |workspace, cx| {
workspace.
prepare_to_close(true, cx)
})?
.await?;
if continue_replacing {
workspace
.update(&mut cx, |_workspace, cx| {
workspace::join_dev_server_project(project_id, app_state, Some(handle), cx)
})?
.await?;
}
Ok(())
})
}
else {
let task =
workspace::join_dev_server_project(project_id, app_state, None, cx);
cx.spawn(|_, _| async move {
task.await?;
Ok(())
})
}
};
open_dev_server_project(replace_current_window, project_id, cx)
} else {
Task::ready(Err(anyhow::anyhow!("App state not found")))
}
}
}
}
}
})
.detach_and_log_err(cx);
.detach_and_log_err(cx);
cx.emit(DismissEvent);
}
}
@@ -535,7 +546,7 @@ impl PickerDelegate for RecentProjectsDelegate {
.when_some(KeyBinding::for_action(&OpenRemote, cx), |button, key| {
button.child(key)
})
.child(Label::new("New remote project…").color(Color::Muted))
.child(Label::new("Connect…").color(Color::Muted))
.on_click(|_, cx| cx.dispatch_action(OpenRemote.boxed_clone())),
)
.child(
@@ -544,7 +555,7 @@ impl PickerDelegate for RecentProjectsDelegate {
KeyBinding::for_action(&workspace::Open, cx),
|button, key| button.child(key),
)
.child(Label::new("Open local folder…").color(Color::Muted))
.child(Label::new("Open folder…").color(Color::Muted))
.on_click(|_, cx| cx.dispatch_action(workspace::Open.boxed_clone())),
)
.into_any(),
@@ -552,51 +563,6 @@ impl PickerDelegate for RecentProjectsDelegate {
}
}
fn open_dev_server_project(
replace_current_window: bool,
project_id: ProjectId,
cx: &mut ViewContext<Workspace>,
) -> Task<anyhow::Result<()>> {
if let Some(app_state) = AppState::global(cx).upgrade() {
let handle = if replace_current_window {
cx.window_handle().downcast::<Workspace>()
} else {
None
};
if let Some(handle) = handle {
cx.spawn(move |workspace, mut cx| async move {
let continue_replacing = workspace
.update(&mut cx, |workspace, cx| {
workspace.prepare_to_close(true, cx)
})?
.await?;
if continue_replacing {
workspace
.update(&mut cx, |_workspace, cx| {
workspace::join_dev_server_project(
project_id,
app_state,
Some(handle),
cx,
)
})?
.await?;
}
Ok(())
})
} else {
let task = workspace::join_dev_server_project(project_id, app_state, None, cx);
cx.spawn(|_, _| async move {
task.await?;
Ok(())
})
}
} else {
Task::ready(Err(anyhow::anyhow!("App state not found")))
}
}
// Compute the highlighted text for the name and path
fn highlights_for_path(
path: &Path,

View File

@@ -5,11 +5,10 @@ pub mod static_source;
mod task_template;
mod vscode_format;
use collections::{BTreeMap, HashMap, HashSet};
use collections::{HashMap, HashSet};
use gpui::SharedString;
use serde::Serialize;
use std::path::PathBuf;
use std::str::FromStr;
use std::{borrow::Cow, path::Path};
pub use task_template::{RevealStrategy, TaskTemplate, TaskTemplates};
@@ -121,7 +120,7 @@ impl ResolvedTask {
}
/// Variables, available for use in [`TaskContext`] when a Zed's [`TaskTemplate`] gets resolved into a [`ResolvedTask`].
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub enum VariableName {
/// An absolute path of the currently opened file.
File,
@@ -135,6 +134,8 @@ pub enum VariableName {
Column,
/// Text from the latest selection.
SelectedText,
/// The symbol selected by the symbol tagging system, specifically the @run capture in a runnables.scm
RunnableSymbol,
/// Custom variable, provided by the plugin or other external source.
/// Will be printed with `ZED_` prefix to avoid potential conflicts with other variables.
Custom(Cow<'static, str>),
@@ -164,6 +165,7 @@ impl std::fmt::Display for VariableName {
Self::Row => write!(f, "{ZED_VARIABLE_NAME_PREFIX}ROW"),
Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"),
Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"),
Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"),
Self::Custom(s) => write!(f, "{ZED_VARIABLE_NAME_PREFIX}CUSTOM_{s}"),
}
}
@@ -171,7 +173,7 @@ impl std::fmt::Display for VariableName {
/// Container for predefined environment variables that describe state of Zed at the time the task was spawned.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct TaskVariables(BTreeMap<VariableName, String>);
pub struct TaskVariables(HashMap<VariableName, String>);
impl TaskVariables {
/// Inserts another variable into the container, overwriting the existing one if it already exists — in this case, the old value is returned.
@@ -197,42 +199,14 @@ impl TaskVariables {
}
})
}
/// Returns iterator over names of all set task variables.
pub fn keys(&self) -> impl Iterator<Item = &VariableName> {
self.0.keys()
}
}
impl FromIterator<(VariableName, String)> for TaskVariables {
fn from_iter<T: IntoIterator<Item = (VariableName, String)>>(iter: T) -> Self {
Self(BTreeMap::from_iter(iter))
Self(HashMap::from_iter(iter))
}
}
impl FromStr for VariableName {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let without_prefix = s.strip_prefix(ZED_VARIABLE_NAME_PREFIX).ok_or(())?;
let value = match without_prefix {
"FILE" => Self::File,
"WORKTREE_ROOT" => Self::WorktreeRoot,
"SYMBOL" => Self::Symbol,
"SELECTED_TEXT" => Self::SelectedText,
"ROW" => Self::Row,
"COLUMN" => Self::Column,
_ => {
if let Some(custom_name) = without_prefix.strip_prefix("CUSTOM_") {
Self::Custom(Cow::Owned(custom_name.to_owned()))
} else {
return Err(());
}
}
};
Ok(value)
}
}
/// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function).
/// Keeps all Zed-related state inside, used to produce a resolved task out of its template.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]

View File

@@ -14,11 +14,9 @@ file_icons.workspace = true
fuzzy.workspace = true
gpui.workspace = true
menu.workspace = true
parking_lot.workspace = true
picker.workspace = true
project.workspace = true
task.workspace = true
text.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true

View File

@@ -10,7 +10,6 @@ use workspace::tasks::schedule_task;
use workspace::{tasks::schedule_resolved_task, Workspace};
mod modal;
mod modal_completions;
mod settings;
pub use modal::Spawn;

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::{active_item_selection_properties, modal_completions::TaskVariablesCompletionProvider};
use crate::active_item_selection_properties;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
impl_actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusableView,
@@ -139,14 +139,11 @@ impl TasksModal {
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let provider = TaskVariablesCompletionProvider::new(task_context.task_variables.clone());
let picker = cx.new_view(|cx| {
Picker::uniform_list(
TasksModalDelegate::new(inventory, task_context, workspace),
cx,
)
.with_completions_provider(Box::new(provider), cx)
});
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
cx.emit(DismissEvent);

View File

@@ -1,138 +0,0 @@
use std::{str::FromStr, sync::Arc};
use editor::CompletionProvider;
use fuzzy::{CharBag, StringMatchCandidate};
use gpui::{AppContext, Model, Task};
use language::{CodeLabel, Documentation, LanguageServerId};
use parking_lot::RwLock;
use task::{TaskVariables, VariableName};
use text::{Anchor, ToOffset};
use ui::ViewContext;
pub(crate) struct TaskVariablesCompletionProvider {
task_variables: Arc<TaskVariables>,
pub(crate) names: Arc<[StringMatchCandidate]>,
}
impl TaskVariablesCompletionProvider {
pub(crate) fn new(variables: TaskVariables) -> Self {
let names = variables
.keys()
.enumerate()
.map(|(index, name)| {
let name = name.to_string();
StringMatchCandidate {
id: index,
char_bag: CharBag::from(name.as_str()),
string: name,
}
})
.collect::<Arc<[_]>>();
Self {
names,
task_variables: Arc::new(variables),
}
}
fn current_query(
buffer: &Model<language::Buffer>,
position: language::Anchor,
cx: &AppContext,
) -> Option<String> {
let mut has_trigger_character = false;
let reversed_query = buffer
.read(cx)
.reversed_chars_for_range(Anchor::MIN..position)
.take_while(|c| {
let is_trigger = *c == '$';
if is_trigger {
has_trigger_character = true;
}
!is_trigger && (*c == '_' || c.is_ascii_alphanumeric())
})
.collect::<String>();
has_trigger_character.then(|| reversed_query.chars().rev().collect())
}
}
impl CompletionProvider for TaskVariablesCompletionProvider {
fn completions(
&self,
buffer: &Model<language::Buffer>,
buffer_position: text::Anchor,
cx: &mut ViewContext<editor::Editor>,
) -> gpui::Task<gpui::Result<Vec<project::Completion>>> {
let Some(current_query) = Self::current_query(buffer, buffer_position, cx) else {
return Task::ready(Ok(vec![]));
};
let buffer = buffer.read(cx);
let buffer_snapshot = buffer.snapshot();
let offset = buffer_position.to_offset(&buffer_snapshot);
let starting_offset = offset - current_query.len();
let starting_anchor = buffer.anchor_before(starting_offset);
let executor = cx.background_executor().clone();
let names = self.names.clone();
let variables = self.task_variables.clone();
cx.background_executor().spawn(async move {
let matches = fuzzy::match_strings(
&names,
&current_query,
true,
100,
&Default::default(),
executor,
)
.await;
// Find all variables starting with this
Ok(matches
.into_iter()
.filter_map(|hit| {
let variable_key = VariableName::from_str(&hit.string).ok()?;
let value_of_var = variables.get(&variable_key)?.to_owned();
Some(project::Completion {
old_range: starting_anchor..buffer_position,
new_text: hit.string.clone(),
label: CodeLabel::plain(hit.string, None),
documentation: Some(Documentation::SingleLine(value_of_var)),
server_id: LanguageServerId(0), // TODO: Make this optional or something?
lsp_completion: Default::default(), // TODO: Make this optional or something?
})
})
.collect())
})
}
fn resolve_completions(
&self,
_buffer: Model<language::Buffer>,
_completion_indices: Vec<usize>,
_completions: Arc<RwLock<Box<[project::Completion]>>>,
_cx: &mut ViewContext<editor::Editor>,
) -> gpui::Task<gpui::Result<bool>> {
Task::ready(Ok(true))
}
fn apply_additional_edits_for_completion(
&self,
_buffer: Model<language::Buffer>,
_completion: project::Completion,
_push_to_history: bool,
_cx: &mut ViewContext<editor::Editor>,
) -> gpui::Task<gpui::Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
buffer: &Model<language::Buffer>,
position: language::Anchor,
text: &str,
_trigger_in_words: bool,
cx: &mut ViewContext<editor::Editor>,
) -> bool {
if text == "$" {
return true;
}
Self::current_query(buffer, position, cx).is_some()
}
}

View File

@@ -283,7 +283,7 @@ impl TerminalView {
cx.spawn(|this, mut cx| async move {
Timer::after(CURSOR_BLINK_INTERVAL).await;
this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx))
.ok();
.log_err();
})
.detach();
}

View File

@@ -34,142 +34,6 @@ macro_rules! debug_panic {
};
}
#[macro_export]
macro_rules! with_clone {
($i:ident, move ||$l:expr) => {{
let $i = $i.clone();
move || {
$l
}
}};
($i:ident, move |$($k:pat_param),*|$l:expr) => {{
let $i = $i.clone();
move |$( $k ),*| {
$l
}
}};
(($($i:ident),+), move ||$l:expr) => {{
let ($($i),+) = ($($i.clone()),+);
move || {
$l
}
}};
(($($i:ident),+), move |$($k:pat_param),*|$l:expr) => {{
let ($($i),+) = ($($i.clone()),+);
move |$( $k ),*| {
$l
}
}};
}
mod test_with_clone {
// If this test compiles, it works
#[test]
fn test() {
let x = "String".to_string();
let y = std::sync::Arc::new(5);
fn no_arg(f: impl FnOnce()) {
f()
}
no_arg(with_clone!(x, move || {
drop(x);
}));
no_arg(with_clone!((x, y), move || {
drop(x);
drop(y);
}));
fn one_arg(f: impl FnOnce(usize)) {
f(1)
}
one_arg(with_clone!(x, move |_| {
drop(x);
}));
one_arg(with_clone!((x, y), move |b| {
drop(x);
drop(y);
println!("{}", b);
}));
fn two_arg(f: impl FnOnce(usize, bool)) {
f(5, true)
}
two_arg(with_clone!((x, y), move |a, b| {
drop(x);
drop(y);
println!("{}{}", a, b)
}));
two_arg(with_clone!((x, y), move |a, _| {
drop(x);
drop(y);
println!("{}", a)
}));
two_arg(with_clone!((x, y), move |_, b| {
drop(x);
drop(y);
println!("{}", b)
}));
struct Example {
z: usize,
}
fn destructuring_example(f: impl FnOnce(Example)) {
f(Example { z: 10 })
}
destructuring_example(with_clone!(x, move |Example { z }| {
drop(x);
println!("{}", z);
}));
let a_long_variable_1 = "".to_string();
let a_long_variable_2 = "".to_string();
let a_long_variable_3 = "".to_string();
let a_long_variable_4 = "".to_string();
two_arg(with_clone!(
(
x,
y,
a_long_variable_1,
a_long_variable_2,
a_long_variable_3,
a_long_variable_4
),
move |a, b| {
drop(x);
drop(y);
drop(a_long_variable_1);
drop(a_long_variable_2);
drop(a_long_variable_3);
drop(a_long_variable_4);
println!("{}{}", a, b)
}
));
fn single_expression_body(f: impl FnOnce(usize) -> usize) -> usize {
f(20)
}
let _result = single_expression_body(with_clone!(y, move |z| *y + z));
// Explicitly move all variables
drop(x);
drop(y);
drop(a_long_variable_1);
drop(a_long_variable_2);
drop(a_long_variable_3);
drop(a_long_variable_4);
}
}
pub fn truncate(s: &str, max_chars: usize) -> &str {
match s.char_indices().nth(max_chars) {
None => s,

View File

@@ -512,13 +512,6 @@ where
}
pub trait DetachAndPromptErr {
fn prompt_err(
self,
msg: &str,
cx: &mut WindowContext,
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
) -> Task<()>;
fn detach_and_prompt_err(
self,
msg: &str,
@@ -531,12 +524,12 @@ impl<R> DetachAndPromptErr for Task<anyhow::Result<R>>
where
R: 'static,
{
fn prompt_err(
fn detach_and_prompt_err(
self,
msg: &str,
cx: &mut WindowContext,
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
) -> Task<()> {
) {
let msg = msg.to_owned();
cx.spawn(|mut cx| async move {
if let Err(err) = self.await {
@@ -550,14 +543,6 @@ where
}
}
})
}
fn detach_and_prompt_err(
self,
msg: &str,
cx: &mut WindowContext,
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
) {
self.prompt_err(msg, cx, f).detach();
.detach();
}
}

View File

@@ -17,9 +17,7 @@ use env_logger::Builder;
use fs::RealFs;
use futures::{future, StreamExt};
use git::GitHostingProviderRegistry;
use gpui::{
App, AppContext, AsyncAppContext, Context, Global, Task, UpdateGlobal as _, VisualContext,
};
use gpui::{App, AppContext, AsyncAppContext, Context, Global, Task, VisualContext};
use image_viewer;
use language::LanguageRegistry;
use log::LevelFilter;
@@ -40,7 +38,11 @@ use std::{
sync::Arc,
};
use theme::{ActiveTheme, SystemAppearance, ThemeRegistry, ThemeSettings};
use util::{maybe, parse_env_output, paths, with_clone, ResultExt, TryFutureExt};
use util::{
maybe, parse_env_output,
paths::{self},
ResultExt, TryFutureExt,
};
use uuid::Uuid;
use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN};
use workspace::{AppState, WorkspaceSettings, WorkspaceStore};
@@ -258,11 +260,13 @@ fn main() {
let session_id = Uuid::new_v4().to_string();
reliability::init_panic_hook(&app, installation_id.clone(), session_id.clone());
let (open_listener, mut open_rx) = OpenListener::new();
let (listener, mut open_rx) = OpenListener::new();
let listener = Arc::new(listener);
let open_listener = listener.clone();
#[cfg(target_os = "linux")]
{
if crate::zed::listen_for_cli_connections(open_listener.clone()).is_err() {
if crate::zed::listen_for_cli_connections(listener.clone()).is_err() {
println!("zed is already running");
return;
}
@@ -313,7 +317,7 @@ fn main() {
})
};
app.on_open_urls(with_clone!(open_listener, move |urls| open_listener.open_urls(urls)));
app.on_open_urls(move |urls| open_listener.open_urls(urls));
app.on_reopen(move |cx| {
if let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade())
{
@@ -334,7 +338,7 @@ fn main() {
GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx);
git_hosting_providers::init(cx);
OpenListener::set_global(cx, open_listener.clone());
OpenListener::set_global(listener.clone(), cx);
settings::init(cx);
handle_settings_file_changes(user_settings_file_rx, cx);
@@ -392,7 +396,7 @@ fn main() {
.collect();
if !urls.is_empty() {
open_listener.open_urls(urls)
listener.open_urls(urls)
}
match open_rx

View File

@@ -11,7 +11,7 @@ use collections::VecDeque;
use editor::{scroll::Autoscroll, Editor, MultiBuffer};
use gpui::{
actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, MenuItem, PromptLevel,
ReadGlobal, TitlebarOptions, View, ViewContext, VisualContext, WindowKind, WindowOptions,
TitlebarOptions, View, ViewContext, VisualContext, WindowKind, WindowOptions,
};
pub use open_listener::*;

View File

@@ -90,19 +90,30 @@ impl OpenRequest {
}
}
#[derive(Clone)]
pub struct OpenListener(UnboundedSender<Vec<String>>);
pub struct OpenListener {
tx: UnboundedSender<Vec<String>>,
}
impl Global for OpenListener {}
struct GlobalOpenListener(Arc<OpenListener>);
impl Global for GlobalOpenListener {}
impl OpenListener {
pub fn global(cx: &AppContext) -> Arc<Self> {
cx.global::<GlobalOpenListener>().0.clone()
}
pub fn set_global(listener: Arc<OpenListener>, cx: &mut AppContext) {
cx.set_global(GlobalOpenListener(listener))
}
pub fn new() -> (Self, UnboundedReceiver<Vec<String>>) {
let (tx, rx) = mpsc::unbounded();
(OpenListener(tx), rx)
(OpenListener { tx }, rx)
}
pub fn open_urls(&self, urls: Vec<String>) {
self.0
self.tx
.unbounded_send(urls)
.map_err(|_| anyhow!("no listener for open requests"))
.log_err();
@@ -110,7 +121,7 @@ impl OpenListener {
}
#[cfg(target_os = "linux")]
pub fn listen_for_cli_connections(opener: OpenListener) -> Result<()> {
pub fn listen_for_cli_connections(opener: Arc<OpenListener>) -> Result<()> {
use release_channel::RELEASE_CHANNEL_NAME;
use std::os::{linux::net::SocketAddrExt, unix::net::SocketAddr, unix::net::UnixDatagram};