Compare commits

...

4 Commits

Author SHA1 Message Date
Piotr Osiewicz
d895f53337 tasks: Provide environment variable autocomplete for tasks modal
Fixes #12099
2024-05-23 16:00:33 +02:00
Mikayla Maki
3eb0418bda Make a macro for less boilerplate when moving variables (#12182)
Also: 
- Simplify open listener implementation
- Add set_global API to global traits

Release Notes:

- N/A
2024-05-22 22:07:29 -07:00
Conrad Irwin
8b57d6d4c6 remote config fixes (#12178)
Release Notes:

- N/A
2024-05-22 22:28:00 -06:00
Conrad Irwin
af8641ce5b reconnect ssh (#12147)
Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-05-22 21:25:38 -06:00
17 changed files with 653 additions and 211 deletions

3
Cargo.lock generated
View File

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

View File

@@ -49,6 +49,11 @@ 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 {
@@ -59,4 +64,11 @@ 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, Editor};
use editor::{scroll::Autoscroll, CompletionProvider, Editor};
use gpui::{
actions, div, impl_actions, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent,
DismissEvent, EventEmitter, FocusHandle, FocusableView, Length, ListState, MouseButton,
@@ -166,6 +166,17 @@ 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,6 +14,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
client.workspace = true
editor.workspace = true
feature_flags.workspace = true
fuzzy.workspace = true

View File

@@ -1,10 +1,12 @@
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;
@@ -47,9 +49,9 @@ pub struct DevServerProjects {
_dev_server_subscription: Subscription,
}
#[derive(Default, Clone)]
#[derive(Default)]
struct CreateDevServer {
creating: bool,
creating: Option<Task<()>>,
dev_server_id: Option<DevServerId>,
access_token: Option<String>,
manual_setup: bool,
@@ -312,95 +314,77 @@ impl DevServerProjects {
});
let workspace = self.workspace.clone();
let store = dev_server_projects::Store::global(cx);
cx.spawn({
let access_token = access_token.clone();
|this, mut cx| async move {
let result = dev_server.await;
let task = cx
.spawn({
|this, mut cx| async move {
let result = dev_server.await;
match result {
Ok(dev_server) => {
if let Some(ssh_connection_string) = ssh_connection_string {
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();
})?;
let access_token = access_token.clone();
this.update(&mut cx, |this, cx| {
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| {
this.focus_handle.focus(cx);
this.mode = Mode::CreateDevServer(CreateDevServer {
creating: true,
creating: None,
dev_server_id: Some(DevServerId(dev_server.dev_server_id)),
access_token: Some(access_token.unwrap_or(dev_server.access_token.clone())),
manual_setup: false,
});
access_token: Some(dev_server.access_token),
manual_setup,
});
cx.notify();
})?;
let terminal_panel = workspace
.update(&mut cx, |workspace, cx| workspace.panel::<TerminalPanel>(cx))
.ok()
.flatten()
.with_context(|| anyhow::anyhow!("No terminal panel"))?;
})?;
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 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
return Err(e);
}
}
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);
}
})
.prompt_err("Failed to create server", cx, |_, _| None);
self.mode = Mode::CreateDevServer(CreateDevServer {
creating: true,
creating: Some(task),
dev_server_id: existing_id,
access_token,
manual_setup,
@@ -502,7 +486,7 @@ impl DevServerProjects {
self.create_dev_server_project(create_project.dev_server_id, cx);
}
Mode::CreateDevServer(state) => {
if !state.creating {
if state.creating.is_none() || state.dev_server_id.is_some() {
self.create_or_update_dev_server(
state.manual_setup,
state.dev_server_id,
@@ -579,7 +563,7 @@ impl DevServerProjects {
.on_click(cx.listener(move |this, _, cx| {
this.mode = Mode::CreateDevServer(CreateDevServer {
dev_server_id: Some(dev_server_id),
creating: false,
creating: None,
access_token: None,
manual_setup,
});
@@ -714,16 +698,14 @@ impl DevServerProjects {
}
fn render_create_dev_server(
&mut self,
state: CreateDevServer,
&self,
state: &CreateDevServer,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let CreateDevServer {
creating,
dev_server_id,
access_token,
manual_setup,
} = state.clone();
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 status = dev_server_id
.map(|id| self.dev_server_store.read(cx).dev_server_status(id))
@@ -769,13 +751,11 @@ impl DevServerProjects {
Label::new("Connect via SSH (default)"),
!manual_setup,
cx.listener({
let state = state.clone();
move |this, _, cx| {
this.mode = Mode::CreateDevServer(CreateDevServer {
manual_setup: false,
..state.clone()
});
cx.notify()
if let Mode::CreateDevServer(CreateDevServer{ manual_setup, .. }) = &mut this.mode {
*manual_setup = false;
}
cx.notify()
}
}),
))
@@ -784,13 +764,11 @@ impl DevServerProjects {
Label::new("Manual Setup"),
manual_setup,
cx.listener({
let state = state.clone();
move |this, _, cx| {
this.mode = Mode::CreateDevServer(CreateDevServer {
manual_setup: true,
..state.clone()
});
cx.notify()
if let Mode::CreateDevServer(CreateDevServer{ manual_setup, .. }) = &mut this.mode {
*manual_setup = true;
}
cx.notify()
}}),
)))
.when(dev_server_id.is_none(), |el| {
@@ -838,10 +816,10 @@ impl DevServerProjects {
cx.notify();
}))
} else {
Button::new("create-dev-server", if manual_setup { "Create"} else { "Connect"})
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"} })
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.disabled(creating)
.disabled(creating && dev_server_id.is_none())
.on_click(cx.listener({
let access_token = access_token.clone();
move |this, _, cx| {
@@ -1006,18 +984,115 @@ 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.clone(), cx)
.into_any_element(),
Mode::CreateDevServer(state) => {
self.render_create_dev_server(state, 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,5 +1,7 @@
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};
@@ -17,6 +19,7 @@ use serde::Deserialize;
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use ui::{
prelude::*, tooltip_container, ButtonLike, IconWithIndicator, Indicator, KeyBinding, ListItem,
@@ -313,73 +316,59 @@ impl PickerDelegate for RecentProjectsDelegate {
}
}
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
let store = dev_server_projects::Store::global(cx).read(cx);
let Some(project_id) = store
let store = dev_server_projects::Store::global(cx);
let Some(project_id) = store.read(cx)
.dev_server_project(dev_server_project.id)
.and_then(|p| p.project_id)
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);
})?;
}
Ok(())
})
};
if let Some(app_state) = AppState::global(cx).upgrade() {
let handle = if replace_current_window {
cx.window_handle().downcast::<Workspace>()
} else {
None
};
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?;
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?;
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(())
})
}
else {
let task =
workspace::join_dev_server_project(project_id, app_state, None, cx);
cx.spawn(|_, _| async move {
task.await?;
Ok(())
})
} 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);
})?;
}
Ok(())
})
}
} else {
Task::ready(Err(anyhow::anyhow!("App state not found")))
}
}
};
open_dev_server_project(replace_current_window, project_id, cx)
}
}
}
})
.detach_and_log_err(cx);
.detach_and_log_err(cx);
cx.emit(DismissEvent);
}
}
@@ -546,7 +535,7 @@ impl PickerDelegate for RecentProjectsDelegate {
.when_some(KeyBinding::for_action(&OpenRemote, cx), |button, key| {
button.child(key)
})
.child(Label::new("Connect…").color(Color::Muted))
.child(Label::new("New remote project…").color(Color::Muted))
.on_click(|_, cx| cx.dispatch_action(OpenRemote.boxed_clone())),
)
.child(
@@ -555,7 +544,7 @@ impl PickerDelegate for RecentProjectsDelegate {
KeyBinding::for_action(&workspace::Open, cx),
|button, key| button.child(key),
)
.child(Label::new("Open folder…").color(Color::Muted))
.child(Label::new("Open local folder…").color(Color::Muted))
.on_click(|_, cx| cx.dispatch_action(workspace::Open.boxed_clone())),
)
.into_any(),
@@ -563,6 +552,51 @@ 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,10 +5,11 @@ pub mod static_source;
mod task_template;
mod vscode_format;
use collections::{HashMap, HashSet};
use collections::{BTreeMap, 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};
@@ -120,7 +121,7 @@ impl ResolvedTask {
}
/// Variables, available for use in [`TaskContext`] when a Zed's [`TaskTemplate`] gets resolved into a [`ResolvedTask`].
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub enum VariableName {
/// An absolute path of the currently opened file.
File,
@@ -134,8 +135,6 @@ 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>),
@@ -165,7 +164,6 @@ 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}"),
}
}
@@ -173,7 +171,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(HashMap<VariableName, String>);
pub struct TaskVariables(BTreeMap<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.
@@ -199,14 +197,42 @@ 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(HashMap::from_iter(iter))
Self(BTreeMap::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,9 +14,11 @@ 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,6 +10,7 @@ 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;
use crate::{active_item_selection_properties, modal_completions::TaskVariablesCompletionProvider};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
impl_actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusableView,
@@ -139,11 +139,14 @@ 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

@@ -0,0 +1,138 @@
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))
.log_err();
.ok();
})
.detach();
}

View File

@@ -34,6 +34,142 @@ 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,6 +512,13 @@ 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,
@@ -524,12 +531,12 @@ impl<R> DetachAndPromptErr for Task<anyhow::Result<R>>
where
R: 'static,
{
fn detach_and_prompt_err(
fn 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 {
@@ -543,6 +550,14 @@ where
}
}
})
.detach();
}
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();
}
}

View File

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

View File

@@ -90,30 +90,19 @@ impl OpenRequest {
}
}
pub struct OpenListener {
tx: UnboundedSender<Vec<String>>,
}
#[derive(Clone)]
pub struct OpenListener(UnboundedSender<Vec<String>>);
struct GlobalOpenListener(Arc<OpenListener>);
impl Global for GlobalOpenListener {}
impl Global for OpenListener {}
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.tx
self.0
.unbounded_send(urls)
.map_err(|_| anyhow!("no listener for open requests"))
.log_err();
@@ -121,7 +110,7 @@ impl OpenListener {
}
#[cfg(target_os = "linux")]
pub fn listen_for_cli_connections(opener: Arc<OpenListener>) -> Result<()> {
pub fn listen_for_cli_connections(opener: OpenListener) -> Result<()> {
use release_channel::RELEASE_CHANNEL_NAME;
use std::os::{linux::net::SocketAddrExt, unix::net::SocketAddr, unix::net::UnixDatagram};