Remove 2 suffix for language selector, project panel, recent_projects, copilot_button, breadcrumbs, activity_indicator

Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-01-03 10:47:33 -08:00
parent 252694390a
commit 292b3397ab
37 changed files with 1297 additions and 6979 deletions

134
Cargo.lock generated
View File

@ -5,23 +5,6 @@ version = 3
[[package]]
name = "activity_indicator"
version = "0.1.0"
dependencies = [
"auto_update",
"editor",
"futures 0.3.28",
"gpui",
"language",
"project",
"settings",
"smallvec",
"theme",
"util",
"workspace",
]
[[package]]
name = "activity_indicator2"
version = "0.1.0"
dependencies = [
"anyhow",
"auto_update2",
@ -1128,23 +1111,6 @@ dependencies = [
[[package]]
name = "breadcrumbs"
version = "0.1.0"
dependencies = [
"collections",
"editor",
"gpui",
"itertools 0.10.5",
"language",
"outline",
"project",
"search",
"settings",
"theme",
"workspace",
]
[[package]]
name = "breadcrumbs2"
version = "0.1.0"
dependencies = [
"collections",
"editor2",
@ -1855,7 +1821,7 @@ dependencies = [
"postage",
"pretty_assertions",
"project2",
"recent_projects2",
"recent_projects",
"rich_text2",
"rpc2",
"schemars",
@ -2059,25 +2025,6 @@ dependencies = [
[[package]]
name = "copilot_button"
version = "0.1.0"
dependencies = [
"anyhow",
"context_menu",
"copilot",
"editor",
"fs",
"futures 0.3.28",
"gpui",
"language",
"settings",
"smol",
"theme",
"util",
"workspace",
]
[[package]]
name = "copilot_button2"
version = "0.1.0"
dependencies = [
"anyhow",
"copilot2",
@ -4593,23 +4540,6 @@ dependencies = [
[[package]]
name = "language_selector"
version = "0.1.0"
dependencies = [
"anyhow",
"editor",
"fuzzy",
"gpui",
"language",
"picker",
"project",
"settings",
"theme",
"util",
"workspace",
]
[[package]]
name = "language_selector2"
version = "0.1.0"
dependencies = [
"anyhow",
"editor2",
@ -6618,35 +6548,6 @@ dependencies = [
[[package]]
name = "project_panel"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"context_menu",
"db",
"drag_and_drop",
"editor",
"futures 0.3.28",
"gpui",
"language",
"menu",
"postage",
"pretty_assertions",
"project",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings",
"theme",
"unicase",
"util",
"workspace",
]
[[package]]
name = "project_panel2"
version = "0.1.0"
dependencies = [
"anyhow",
"client2",
@ -7029,27 +6930,6 @@ dependencies = [
[[package]]
name = "recent_projects"
version = "0.1.0"
dependencies = [
"db",
"editor",
"futures 0.3.28",
"fuzzy",
"gpui",
"language",
"ordered-float 2.10.0",
"picker",
"postage",
"settings",
"smol",
"text",
"theme",
"util",
"workspace",
]
[[package]]
name = "recent_projects2"
version = "0.1.0"
dependencies = [
"editor2",
"futures 0.3.28",
@ -11290,7 +11170,7 @@ dependencies = [
name = "zed"
version = "0.119.0"
dependencies = [
"activity_indicator2",
"activity_indicator",
"ai2",
"anyhow",
"assistant2",
@ -11301,7 +11181,7 @@ dependencies = [
"audio2",
"auto_update2",
"backtrace",
"breadcrumbs2",
"breadcrumbs",
"call2",
"channel2",
"chrono",
@ -11311,7 +11191,7 @@ dependencies = [
"collections",
"command_palette",
"copilot2",
"copilot_button2",
"copilot_button",
"ctor",
"db2",
"diagnostics",
@ -11332,7 +11212,7 @@ dependencies = [
"isahc",
"journal2",
"language2",
"language_selector2",
"language_selector",
"language_tools2",
"lazy_static",
"libc",
@ -11346,11 +11226,11 @@ dependencies = [
"parking_lot 0.11.2",
"postage",
"project2",
"project_panel2",
"project_panel",
"project_symbols",
"quick_action_bar",
"rand 0.8.5",
"recent_projects2",
"recent_projects",
"regex",
"rope2",
"rpc2",

View File

@ -1,7 +1,6 @@
[workspace]
members = [
"crates/activity_indicator",
"crates/activity_indicator2",
"crates/ai",
"crates/assistant",
"crates/assistant2",
@ -10,7 +9,6 @@ members = [
"crates/auto_update",
"crates/auto_update2",
"crates/breadcrumbs",
"crates/breadcrumbs2",
"crates/call",
"crates/call2",
"crates/channel",
@ -58,7 +56,6 @@ members = [
"crates/language",
"crates/language2",
"crates/language_selector",
"crates/language_selector2",
"crates/language_tools",
"crates/language_tools2",
"crates/live_kit_client",
@ -84,11 +81,9 @@ members = [
"crates/project",
"crates/project2",
"crates/project_panel",
"crates/project_panel2",
"crates/project_symbols",
"crates/quick_action_bar",
"crates/recent_projects",
"crates/recent_projects2",
"crates/rope",
"crates/rpc",
"crates/rpc2",

View File

@ -9,18 +9,20 @@ path = "src/activity_indicator.rs"
doctest = false
[dependencies]
auto_update = { path = "../auto_update" }
editor = { path = "../editor" }
language = { path = "../language" }
gpui = { path = "../gpui" }
project = { path = "../project" }
settings = { path = "../settings" }
auto_update = { path = "../auto_update2", package = "auto_update2" }
editor = { path = "../editor2", package = "editor2" }
language = { path = "../language2", package = "language2" }
gpui = { path = "../gpui2", package = "gpui2" }
project = { path = "../project2", package = "project2" }
settings = { path = "../settings2", package = "settings2" }
ui = { path = "../ui2", package = "ui2" }
util = { path = "../util" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
theme = { path = "../theme2", package = "theme2" }
workspace = { path = "../workspace2", package = "workspace2" }
anyhow.workspace = true
futures.workspace = true
smallvec.workspace = true
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
editor = { path = "../editor2", package = "editor2", features = ["test-support"] }

View File

@ -2,19 +2,19 @@ use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
use editor::Editor;
use futures::StreamExt;
use gpui::{
actions, anyhow,
elements::*,
platform::{CursorStyle, MouseButton},
AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle,
actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model,
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View,
ViewContext, VisualContext as _,
};
use language::{LanguageRegistry, LanguageServerBinaryStatus};
use project::{LanguageServerProgress, Project};
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc};
use ui::prelude::*;
use util::ResultExt;
use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(lsp_status, [ShowErrorMessage]);
actions!(activity_indicator, [ShowErrorMessage]);
const DOWNLOAD_ICON: &str = "icons/download.svg";
const WARNING_ICON: &str = "icons/warning.svg";
@ -25,8 +25,8 @@ pub enum Event {
pub struct ActivityIndicator {
statuses: Vec<LspStatus>,
project: ModelHandle<Project>,
auto_updater: Option<ModelHandle<AutoUpdater>>,
project: Model<Project>,
auto_updater: Option<Model<AutoUpdater>>,
}
struct LspStatus {
@ -47,20 +47,15 @@ struct Content {
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
}
pub fn init(cx: &mut AppContext) {
cx.add_action(ActivityIndicator::show_error_message);
cx.add_action(ActivityIndicator::dismiss_error_message);
}
impl ActivityIndicator {
pub fn new(
workspace: &mut Workspace,
languages: Arc<LanguageRegistry>,
cx: &mut ViewContext<Workspace>,
) -> ViewHandle<ActivityIndicator> {
) -> View<ActivityIndicator> {
let project = workspace.project().clone();
let auto_updater = AutoUpdater::get(cx);
let this = cx.add_view(|cx: &mut ViewContext<Self>| {
let this = cx.new_view(|cx: &mut ViewContext<Self>| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn(|this, mut cx| async move {
while let Some((language, event)) = status_events.next().await {
@ -77,11 +72,13 @@ impl ActivityIndicator {
})
.detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach();
if let Some(auto_updater) = auto_updater.as_ref() {
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
}
cx.observe_active_labeled_tasks(|_, cx| cx.notify())
.detach();
// cx.observe_active_labeled_tasks(|_, cx| cx.notify())
// .detach();
Self {
statuses: Default::default(),
@ -89,6 +86,7 @@ impl ActivityIndicator {
auto_updater,
}
});
cx.subscribe(&this, move |workspace, _, event, cx| match event {
Event::ShowError { lsp_name, error } => {
if let Some(buffer) = project
@ -104,7 +102,7 @@ impl ActivityIndicator {
});
workspace.add_item(
Box::new(
cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
),
cx,
);
@ -290,71 +288,41 @@ impl ActivityIndicator {
};
}
if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
return Content {
icon: None,
message: most_recent_active_task.to_string(),
on_click: None,
};
}
// todo!(show active tasks)
// if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
// return Content {
// icon: None,
// message: most_recent_active_task.to_string(),
// on_click: None,
// };
// }
Default::default()
}
}
impl Entity for ActivityIndicator {
type Event = Event;
}
impl EventEmitter<Event> for ActivityIndicator {}
impl View for ActivityIndicator {
fn ui_name() -> &'static str {
"ActivityIndicator"
}
impl Render for ActivityIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let content = self.content_to_render(cx);
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let Content {
icon,
message,
on_click,
} = self.content_to_render(cx);
let mut result = h_stack()
.id("activity-indicator")
.on_action(cx.listener(Self::show_error_message))
.on_action(cx.listener(Self::dismiss_error_message));
let mut element = MouseEventHandler::new::<Self, _>(0, cx, |state, cx| {
let theme = &theme::current(cx).workspace.status_bar.lsp_status;
let style = if state.hovered() && on_click.is_some() {
theme.hovered.as_ref().unwrap_or(&theme.default)
} else {
&theme.default
};
Flex::row()
.with_children(icon.map(|path| {
Svg::new(path)
.with_color(style.icon_color)
.constrained()
.with_width(style.icon_width)
.contained()
.with_margin_right(style.icon_spacing)
.aligned()
.into_any_named("activity-icon")
if let Some(on_click) = content.on_click {
result = result
.cursor(CursorStyle::PointingHand)
.on_click(cx.listener(move |this, _, cx| {
on_click(this, cx);
}))
.with_child(
Text::new(message, style.message.clone())
.with_soft_wrap(false)
.aligned(),
)
.constrained()
.with_height(style.height)
.contained()
.with_style(style.container)
.aligned()
});
if let Some(on_click) = on_click.clone() {
element = element
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| on_click(this, cx));
}
element.into_any()
result
.children(content.icon.map(|icon| svg().path(icon)))
.child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
}
}

View File

@ -1,28 +0,0 @@
[package]
name = "activity_indicator2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/activity_indicator.rs"
doctest = false
[dependencies]
auto_update = { path = "../auto_update2", package = "auto_update2" }
editor = { path = "../editor2", package = "editor2" }
language = { path = "../language2", package = "language2" }
gpui = { path = "../gpui2", package = "gpui2" }
project = { path = "../project2", package = "project2" }
settings = { path = "../settings2", package = "settings2" }
ui = { path = "../ui2", package = "ui2" }
util = { path = "../util" }
theme = { path = "../theme2", package = "theme2" }
workspace = { path = "../workspace2", package = "workspace2" }
anyhow.workspace = true
futures.workspace = true
smallvec.workspace = true
[dev-dependencies]
editor = { path = "../editor2", package = "editor2", features = ["test-support"] }

View File

@ -1,331 +0,0 @@
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
use editor::Editor;
use futures::StreamExt;
use gpui::{
actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model,
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View,
ViewContext, VisualContext as _,
};
use language::{LanguageRegistry, LanguageServerBinaryStatus};
use project::{LanguageServerProgress, Project};
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc};
use ui::prelude::*;
use util::ResultExt;
use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(activity_indicator, [ShowErrorMessage]);
const DOWNLOAD_ICON: &str = "icons/download.svg";
const WARNING_ICON: &str = "icons/warning.svg";
pub enum Event {
ShowError { lsp_name: Arc<str>, error: String },
}
pub struct ActivityIndicator {
statuses: Vec<LspStatus>,
project: Model<Project>,
auto_updater: Option<Model<AutoUpdater>>,
}
struct LspStatus {
name: Arc<str>,
status: LanguageServerBinaryStatus,
}
struct PendingWork<'a> {
language_server_name: &'a str,
progress_token: &'a str,
progress: &'a LanguageServerProgress,
}
#[derive(Default)]
struct Content {
icon: Option<&'static str>,
message: String,
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
}
impl ActivityIndicator {
pub fn new(
workspace: &mut Workspace,
languages: Arc<LanguageRegistry>,
cx: &mut ViewContext<Workspace>,
) -> View<ActivityIndicator> {
let project = workspace.project().clone();
let auto_updater = AutoUpdater::get(cx);
let this = cx.new_view(|cx: &mut ViewContext<Self>| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn(|this, mut cx| async move {
while let Some((language, event)) = status_events.next().await {
this.update(&mut cx, |this, cx| {
this.statuses.retain(|s| s.name != language.name());
this.statuses.push(LspStatus {
name: language.name(),
status: event,
});
cx.notify();
})?;
}
anyhow::Ok(())
})
.detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach();
if let Some(auto_updater) = auto_updater.as_ref() {
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
}
// cx.observe_active_labeled_tasks(|_, cx| cx.notify())
// .detach();
Self {
statuses: Default::default(),
project: project.clone(),
auto_updater,
}
});
cx.subscribe(&this, move |workspace, _, event, cx| match event {
Event::ShowError { lsp_name, error } => {
if let Some(buffer) = project
.update(cx, |project, cx| project.create_buffer(error, None, cx))
.log_err()
{
buffer.update(cx, |buffer, cx| {
buffer.edit(
[(0..0, format!("Language server error: {}\n\n", lsp_name))],
None,
cx,
);
});
workspace.add_item(
Box::new(
cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
),
cx,
);
}
}
})
.detach();
this
}
fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
self.statuses.retain(|status| {
if let LanguageServerBinaryStatus::Failed { error } = &status.status {
cx.emit(Event::ShowError {
lsp_name: status.name.clone(),
error: error.clone(),
});
false
} else {
true
}
});
cx.notify();
}
fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
if let Some(updater) = &self.auto_updater {
updater.update(cx, |updater, cx| {
updater.dismiss_error(cx);
});
}
cx.notify();
}
fn pending_language_server_work<'a>(
&self,
cx: &'a AppContext,
) -> impl Iterator<Item = PendingWork<'a>> {
self.project
.read(cx)
.language_server_statuses()
.rev()
.filter_map(|status| {
if status.pending_work.is_empty() {
None
} else {
let mut pending_work = status
.pending_work
.iter()
.map(|(token, progress)| PendingWork {
language_server_name: status.name.as_str(),
progress_token: token.as_str(),
progress,
})
.collect::<SmallVec<[_; 4]>>();
pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
Some(pending_work)
}
})
.flatten()
}
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
// Show any language server has pending activity.
let mut pending_work = self.pending_language_server_work(cx);
if let Some(PendingWork {
language_server_name,
progress_token,
progress,
}) = pending_work.next()
{
let mut message = language_server_name.to_string();
message.push_str(": ");
if let Some(progress_message) = progress.message.as_ref() {
message.push_str(progress_message);
} else {
message.push_str(progress_token);
}
if let Some(percentage) = progress.percentage {
write!(&mut message, " ({}%)", percentage).unwrap();
}
let additional_work_count = pending_work.count();
if additional_work_count > 0 {
write!(&mut message, " + {} more", additional_work_count).unwrap();
}
return Content {
icon: None,
message,
on_click: None,
};
}
// Show any language server installation info.
let mut downloading = SmallVec::<[_; 3]>::new();
let mut checking_for_update = SmallVec::<[_; 3]>::new();
let mut failed = SmallVec::<[_; 3]>::new();
for status in &self.statuses {
let name = status.name.clone();
match status.status {
LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
LanguageServerBinaryStatus::Downloading => downloading.push(name),
LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
}
}
if !downloading.is_empty() {
return Content {
icon: Some(DOWNLOAD_ICON),
message: format!(
"Downloading {} language server{}...",
downloading.join(", "),
if downloading.len() > 1 { "s" } else { "" }
),
on_click: None,
};
} else if !checking_for_update.is_empty() {
return Content {
icon: Some(DOWNLOAD_ICON),
message: format!(
"Checking for updates to {} language server{}...",
checking_for_update.join(", "),
if checking_for_update.len() > 1 {
"s"
} else {
""
}
),
on_click: None,
};
} else if !failed.is_empty() {
return Content {
icon: Some(WARNING_ICON),
message: format!(
"Failed to download {} language server{}. Click to show error.",
failed.join(", "),
if failed.len() > 1 { "s" } else { "" }
),
on_click: Some(Arc::new(|this, cx| {
this.show_error_message(&Default::default(), cx)
})),
};
}
// Show any application auto-update info.
if let Some(updater) = &self.auto_updater {
return match &updater.read(cx).status() {
AutoUpdateStatus::Checking => Content {
icon: Some(DOWNLOAD_ICON),
message: "Checking for Zed updates…".to_string(),
on_click: None,
},
AutoUpdateStatus::Downloading => Content {
icon: Some(DOWNLOAD_ICON),
message: "Downloading Zed update…".to_string(),
on_click: None,
},
AutoUpdateStatus::Installing => Content {
icon: Some(DOWNLOAD_ICON),
message: "Installing Zed update…".to_string(),
on_click: None,
},
AutoUpdateStatus::Updated => Content {
icon: None,
message: "Click to restart and update Zed".to_string(),
on_click: Some(Arc::new(|_, cx| {
workspace::restart(&Default::default(), cx)
})),
},
AutoUpdateStatus::Errored => Content {
icon: Some(WARNING_ICON),
message: "Auto update failed".to_string(),
on_click: Some(Arc::new(|this, cx| {
this.dismiss_error_message(&Default::default(), cx)
})),
},
AutoUpdateStatus::Idle => Default::default(),
};
}
// todo!(show active tasks)
// if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
// return Content {
// icon: None,
// message: most_recent_active_task.to_string(),
// on_click: None,
// };
// }
Default::default()
}
}
impl EventEmitter<Event> for ActivityIndicator {}
impl Render for ActivityIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let content = self.content_to_render(cx);
let mut result = h_stack()
.id("activity-indicator")
.on_action(cx.listener(Self::show_error_message))
.on_action(cx.listener(Self::dismiss_error_message));
if let Some(on_click) = content.on_click {
result = result
.cursor(CursorStyle::PointingHand)
.on_click(cx.listener(move |this, _, cx| {
on_click(this, cx);
}))
}
result
.children(content.icon.map(|icon| svg().path(icon)))
.child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
}
}
impl StatusItemView for ActivityIndicator {
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
}

View File

@ -10,18 +10,19 @@ doctest = false
[dependencies]
collections = { path = "../collections" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
language = { path = "../language" }
project = { path = "../project" }
search = { path = "../search" }
settings = { path = "../settings" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
outline = { path = "../outline" }
editor = { package = "editor2", path = "../editor2" }
gpui = { package = "gpui2", path = "../gpui2" }
ui = { package = "ui2", path = "../ui2" }
language = { package = "language2", path = "../language2" }
project = { package = "project2", path = "../project2" }
search = { package = "search2", path = "../search2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
workspace = { package = "workspace2", path = "../workspace2" }
outline = { package = "outline2", path = "../outline2" }
itertools = "0.10"
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }

View File

@ -1,108 +1,74 @@
use editor::Editor;
use gpui::{
elements::*, platform::MouseButton, AppContext, Entity, Subscription, View, ViewContext,
ViewHandle, WeakViewHandle,
Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
ViewContext,
};
use itertools::Itertools;
use search::ProjectSearchView;
use theme::ActiveTheme;
use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip};
use workspace::{
item::{ItemEvent, ItemHandle},
ToolbarItemLocation, ToolbarItemView, Workspace,
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
};
pub enum Event {
UpdateLocation,
}
pub struct Breadcrumbs {
pane_focused: bool,
active_item: Option<Box<dyn ItemHandle>>,
project_search: Option<ViewHandle<ProjectSearchView>>,
subscription: Option<Subscription>,
workspace: WeakViewHandle<Workspace>,
}
impl Breadcrumbs {
pub fn new(workspace: &Workspace) -> Self {
pub fn new() -> Self {
Self {
pane_focused: false,
active_item: Default::default(),
subscription: Default::default(),
project_search: Default::default(),
workspace: workspace.weak_handle(),
}
}
}
impl Entity for Breadcrumbs {
type Event = Event;
}
impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
impl View for Breadcrumbs {
fn ui_name() -> &'static str {
"Breadcrumbs"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let active_item = match &self.active_item {
Some(active_item) => active_item,
None => return Empty::new().into_any(),
impl Render for Breadcrumbs {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let element = h_stack().text_ui();
let Some(active_item) = self.active_item.as_ref() else {
return element;
};
let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else {
return element;
};
let not_editor = active_item.downcast::<editor::Editor>().is_none();
let theme = theme::current(cx).clone();
let style = &theme.workspace.toolbar.breadcrumbs;
let highlighted_segments = segments.into_iter().map(|segment| {
let mut text_style = cx.text_style();
text_style.color = Color::Muted.color(cx);
let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
Some(breadcrumbs) => breadcrumbs,
None => return Empty::new().into_any(),
}
.into_iter()
.map(|breadcrumb| {
Text::new(
breadcrumb.text,
theme.workspace.toolbar.breadcrumbs.default.text.clone(),
)
.with_highlights(breadcrumb.highlights.unwrap_or_default())
.into_any()
StyledText::new(segment.text)
.with_highlights(&text_style, segment.highlights.unwrap_or_default())
.into_any()
});
let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
Label::new("").color(Color::Muted).into_any_element()
});
let crumbs = Flex::row()
.with_children(Itertools::intersperse_with(breadcrumbs, || {
Label::new(" ", style.default.text.clone()).into_any()
}))
.constrained()
.with_height(theme.workspace.toolbar.breadcrumb_height)
.contained();
if not_editor || !self.pane_focused {
return crumbs
.with_style(style.default.container)
.aligned()
.left()
.into_any();
let breadcrumbs_stack = h_stack().gap_1().children(breadcrumbs);
match active_item
.downcast::<Editor>()
.map(|editor| editor.downgrade())
{
Some(editor) => element.child(
ButtonLike::new("toggle outline view")
.child(breadcrumbs_stack)
.style(ButtonStyle::Subtle)
.on_click(move |_, cx| {
if let Some(editor) = editor.upgrade() {
outline::toggle(editor, &outline::Toggle, cx)
}
})
.tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
),
None => element.child(breadcrumbs_stack),
}
MouseEventHandler::new::<Breadcrumbs, _>(0, cx, |state, _| {
let style = style.style_for(state);
crumbs.with_style(style.container)
})
.on_click(MouseButton::Left, |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
outline::toggle(workspace, &Default::default(), cx)
})
}
})
.with_tooltip::<Breadcrumbs>(
0,
"Show symbol outline".to_owned(),
Some(Box::new(outline::Toggle)),
theme.tooltip.clone(),
cx,
)
.aligned()
.left()
.into_any()
}
}
@ -114,19 +80,21 @@ impl ToolbarItemView for Breadcrumbs {
) -> ToolbarItemLocation {
cx.notify();
self.active_item = None;
self.project_search = None;
if let Some(item) = active_pane_item {
let this = cx.weak_handle();
let this = cx.view().downgrade();
self.subscription = Some(item.subscribe_to_item_events(
cx,
Box::new(move |event, cx| {
if let Some(this) = this.upgrade(cx) {
if let ItemEvent::UpdateBreadcrumbs = event {
this.update(cx, |_, cx| {
cx.emit(Event::UpdateLocation);
cx.notify();
});
}
if let ItemEvent::UpdateBreadcrumbs = event {
this.update(cx, |this, cx| {
cx.notify();
if let Some(active_item) = this.active_item.as_ref() {
cx.emit(ToolbarItemEvent::ChangeLocation(
active_item.breadcrumb_location(cx),
))
}
})
.ok();
}
}),
));
@ -137,19 +105,6 @@ impl ToolbarItemView for Breadcrumbs {
}
}
fn location_for_event(
&self,
_: &Event,
current_location: ToolbarItemLocation,
cx: &AppContext,
) -> ToolbarItemLocation {
if let Some(active_item) = self.active_item.as_ref() {
active_item.breadcrumb_location(cx)
} else {
current_location
}
}
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
self.pane_focused = pane_focused;
}

View File

@ -1,28 +0,0 @@
[package]
name = "breadcrumbs2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/breadcrumbs.rs"
doctest = false
[dependencies]
collections = { path = "../collections" }
editor = { package = "editor2", path = "../editor2" }
gpui = { package = "gpui2", path = "../gpui2" }
ui = { package = "ui2", path = "../ui2" }
language = { package = "language2", path = "../language2" }
project = { package = "project2", path = "../project2" }
search = { package = "search2", path = "../search2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
workspace = { package = "workspace2", path = "../workspace2" }
outline = { package = "outline2", path = "../outline2" }
itertools = "0.10"
[dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }

View File

@ -1,111 +0,0 @@
use editor::Editor;
use gpui::{
Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
ViewContext,
};
use itertools::Itertools;
use theme::ActiveTheme;
use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip};
use workspace::{
item::{ItemEvent, ItemHandle},
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
};
pub struct Breadcrumbs {
pane_focused: bool,
active_item: Option<Box<dyn ItemHandle>>,
subscription: Option<Subscription>,
}
impl Breadcrumbs {
pub fn new() -> Self {
Self {
pane_focused: false,
active_item: Default::default(),
subscription: Default::default(),
}
}
}
impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
impl Render for Breadcrumbs {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let element = h_stack().text_ui();
let Some(active_item) = self.active_item.as_ref() else {
return element;
};
let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else {
return element;
};
let highlighted_segments = segments.into_iter().map(|segment| {
let mut text_style = cx.text_style();
text_style.color = Color::Muted.color(cx);
StyledText::new(segment.text)
.with_highlights(&text_style, segment.highlights.unwrap_or_default())
.into_any()
});
let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
Label::new("").color(Color::Muted).into_any_element()
});
let breadcrumbs_stack = h_stack().gap_1().children(breadcrumbs);
match active_item
.downcast::<Editor>()
.map(|editor| editor.downgrade())
{
Some(editor) => element.child(
ButtonLike::new("toggle outline view")
.child(breadcrumbs_stack)
.style(ButtonStyle::Subtle)
.on_click(move |_, cx| {
if let Some(editor) = editor.upgrade() {
outline::toggle(editor, &outline::Toggle, cx)
}
})
.tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
),
None => element.child(breadcrumbs_stack),
}
}
}
impl ToolbarItemView for Breadcrumbs {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
cx.notify();
self.active_item = None;
if let Some(item) = active_pane_item {
let this = cx.view().downgrade();
self.subscription = Some(item.subscribe_to_item_events(
cx,
Box::new(move |event, cx| {
if let ItemEvent::UpdateBreadcrumbs = event {
this.update(cx, |this, cx| {
cx.notify();
if let Some(active_item) = this.active_item.as_ref() {
cx.emit(ToolbarItemEvent::ChangeLocation(
active_item.breadcrumb_location(cx),
))
}
})
.ok();
}
}),
));
self.active_item = Some(item.boxed_clone());
item.breadcrumb_location(cx)
} else {
ToolbarItemLocation::Hidden
}
}
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
self.pane_focused = pane_focused;
}
}

View File

@ -41,7 +41,7 @@ notifications = { package = "notifications2", path = "../notifications2" }
rich_text = { package = "rich_text2", path = "../rich_text2" }
picker = { package = "picker2", path = "../picker2" }
project = { package = "project2", path = "../project2" }
recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
recent_projects = { path = "../recent_projects" }
rpc = { package ="rpc2", path = "../rpc2" }
settings = { package = "settings2", path = "../settings2" }
feature_flags = { package = "feature_flags2", path = "../feature_flags2"}

View File

@ -9,19 +9,19 @@ path = "src/copilot_button.rs"
doctest = false
[dependencies]
copilot = { path = "../copilot" }
editor = { path = "../editor" }
fs = { path = "../fs" }
context_menu = { path = "../context_menu" }
gpui = { path = "../gpui" }
language = { path = "../language" }
settings = { path = "../settings" }
theme = { path = "../theme" }
copilot = { package = "copilot2", path = "../copilot2" }
editor = { package = "editor2", path = "../editor2" }
fs = { package = "fs2", path = "../fs2" }
zed-actions = { package="zed_actions2", path = "../zed_actions2"}
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
workspace = { path = "../workspace" }
workspace = { package = "workspace2", path = "../workspace2" }
anyhow.workspace = true
smol.workspace = true
futures.workspace = true
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

View File

@ -1,32 +1,31 @@
use anyhow::Result;
use context_menu::{ContextMenu, ContextMenuItem};
use copilot::{Copilot, SignOut, Status};
use editor::{scroll::autoscroll::Autoscroll, Editor};
use fs::Fs;
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement,
Render, Subscription, View, ViewContext, WeakView, WindowContext,
};
use language::{
language_settings::{self, all_language_settings, AllLanguageSettings},
File, Language,
};
use settings::{update_settings_file, SettingsStore};
use settings::{update_settings_file, Settings, SettingsStore};
use std::{path::Path, sync::Arc};
use util::{paths, ResultExt};
use workspace::{
create_and_open_local_file, item::ItemHandle,
notifications::simple_message_notification::OsOpen, StatusItemView, Toast, Workspace,
create_and_open_local_file,
item::ItemHandle,
ui::{popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, IconSize, Tooltip},
StatusItemView, Toast, Workspace,
};
use zed_actions::OpenBrowser;
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
const COPILOT_STARTING_TOAST_ID: usize = 1337;
const COPILOT_ERROR_TOAST_ID: usize = 1338;
pub struct CopilotButton {
popup_menu: ViewHandle<ContextMenu>,
editor_subscription: Option<(Subscription, usize)>,
editor_enabled: Option<bool>,
language: Option<Arc<Language>>,
@ -34,25 +33,15 @@ pub struct CopilotButton {
fs: Arc<dyn Fs>,
}
impl Entity for CopilotButton {
type Event = ();
}
impl View for CopilotButton {
fn ui_name() -> &'static str {
"CopilotButton"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
impl Render for CopilotButton {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let all_language_settings = all_language_settings(None, cx);
if !all_language_settings.copilot.feature_enabled {
return Empty::new().into_any();
return div();
}
let theme = theme::current(cx).clone();
let active = self.popup_menu.read(cx).visible();
let Some(copilot) = Copilot::global(cx) else {
return Empty::new().into_any();
return div();
};
let status = copilot.read(cx).status();
@ -60,59 +49,26 @@ impl View for CopilotButton {
.editor_enabled
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
Stack::new()
.with_child(
MouseEventHandler::new::<Self, _>(0, cx, {
let theme = theme.clone();
let status = status.clone();
move |state, _cx| {
let style = theme
.workspace
.status_bar
.panel_buttons
.button
.in_state(active)
.style_for(state);
let icon = match status {
Status::Error(_) => Icon::CopilotError,
Status::Authorized => {
if enabled {
Icon::Copilot
} else {
Icon::CopilotDisabled
}
}
_ => Icon::CopilotInit,
};
Flex::row()
.with_child(
Svg::new({
match status {
Status::Error(_) => "icons/copilot_error.svg",
Status::Authorized => {
if enabled {
"icons/copilot.svg"
} else {
"icons/copilot_disabled.svg"
}
}
_ => "icons/copilot_init.svg",
}
})
.with_color(style.icon_color)
.constrained()
.with_width(style.icon_size)
.aligned()
.into_any_named("copilot-icon"),
)
.constrained()
.with_height(style.icon_size)
.contained()
.with_style(style.container)
}
})
.with_cursor_style(CursorStyle::PointingHand)
.on_down(MouseButton::Left, |_, this, cx| {
this.popup_menu.update(cx, |menu, _| menu.delay_cancel());
})
.on_click(MouseButton::Left, {
let status = status.clone();
move |_, this, cx| match status {
Status::Authorized => this.deploy_copilot_menu(cx),
Status::Error(ref e) => {
if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>()
{
workspace.update(cx, |workspace, cx| {
if let Status::Error(e) = status {
return div().child(
IconButton::new("copilot-error", icon)
.icon_size(IconSize::Small)
.on_click(cx.listener(move |_, _, cx| {
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
workspace
.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(
COPILOT_ERROR_TOAST_ID,
@ -132,43 +88,40 @@ impl View for CopilotButton {
),
cx,
);
});
}
})
.ok();
}
_ => this.deploy_copilot_start_menu(cx),
}))
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
);
}
let this = cx.view().clone();
div().child(
popover_menu("copilot")
.menu(move |cx| match status {
Status::Authorized => {
Some(this.update(cx, |this, cx| this.build_copilot_menu(cx)))
}
_ => Some(this.update(cx, |this, cx| this.build_copilot_start_menu(cx))),
})
.with_tooltip::<Self>(
0,
"GitHub Copilot",
None,
theme.tooltip.clone(),
cx,
.anchor(AnchorCorner::BottomRight)
.trigger(
IconButton::new("copilot-icon", icon)
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
),
)
.with_child(ChildView::new(&self.popup_menu, cx).aligned().top().right())
.into_any()
)
}
}
impl CopilotButton {
pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
let button_view_id = cx.view_id();
let menu = cx.add_view(|cx| {
let mut menu = ContextMenu::new(button_view_id, cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
});
cx.observe(&menu, |_, _, cx| cx.notify()).detach();
Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify())
cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
.detach();
Self {
popup_menu: menu,
editor_subscription: None,
editor_enabled: None,
language: None,
@ -177,108 +130,91 @@ impl CopilotButton {
}
}
pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) {
let mut menu_options = Vec::with_capacity(2);
pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
let fs = self.fs.clone();
menu_options.push(ContextMenuItem::handler("Sign In", |cx| {
initiate_sign_in(cx)
}));
menu_options.push(ContextMenuItem::handler("Disable Copilot", move |cx| {
hide_copilot(fs.clone(), cx)
}));
self.popup_menu.update(cx, |menu, cx| {
menu.toggle(
Default::default(),
AnchorCorner::BottomRight,
menu_options,
cx,
);
});
ContextMenu::build(cx, |menu, _| {
menu.entry("Sign In", None, initiate_sign_in).entry(
"Disable Copilot",
None,
move |cx| hide_copilot(fs.clone(), cx),
)
})
}
pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
let fs = self.fs.clone();
let mut menu_options = Vec::with_capacity(8);
if let Some(language) = self.language.clone() {
let fs = fs.clone();
let language_enabled = language_settings::language_settings(Some(&language), None, cx)
.show_copilot_suggestions;
menu_options.push(ContextMenuItem::handler(
format!(
"{} Suggestions for {}",
if language_enabled { "Hide" } else { "Show" },
language.name()
),
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
));
}
return ContextMenu::build(cx, move |mut menu, cx| {
if let Some(language) = self.language.clone() {
let fs = fs.clone();
let language_enabled =
language_settings::language_settings(Some(&language), None, cx)
.show_copilot_suggestions;
let settings = settings::get::<AllLanguageSettings>(cx);
menu = menu.entry(
format!(
"{} Suggestions for {}",
if language_enabled { "Hide" } else { "Show" },
language.name()
),
None,
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
);
}
if let Some(file) = &self.file {
let path = file.path().clone();
let path_enabled = settings.copilot_enabled_for_path(&path);
menu_options.push(ContextMenuItem::handler(
format!(
"{} Suggestions for This Path",
if path_enabled { "Hide" } else { "Show" }
),
move |cx| {
if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() {
let workspace = workspace.downgrade();
cx.spawn(|_, cx| {
configure_disabled_globs(
workspace,
path_enabled.then_some(path.clone()),
cx,
)
})
.detach_and_log_err(cx);
}
let settings = AllLanguageSettings::get_global(cx);
if let Some(file) = &self.file {
let path = file.path().clone();
let path_enabled = settings.copilot_enabled_for_path(&path);
menu = menu.entry(
format!(
"{} Suggestions for This Path",
if path_enabled { "Hide" } else { "Show" }
),
None,
move |cx| {
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
if let Ok(workspace) = workspace.root_view(cx) {
let workspace = workspace.downgrade();
cx.spawn(|cx| {
configure_disabled_globs(
workspace,
path_enabled.then_some(path.clone()),
cx,
)
})
.detach_and_log_err(cx);
}
}
},
);
}
let globally_enabled = settings.copilot_enabled(None, None);
menu.entry(
if globally_enabled {
"Hide Suggestions for All Files"
} else {
"Show Suggestions for All Files"
},
));
}
let globally_enabled = settings.copilot_enabled(None, None);
menu_options.push(ContextMenuItem::handler(
if globally_enabled {
"Hide Suggestions for All Files"
} else {
"Show Suggestions for All Files"
},
move |cx| toggle_copilot_globally(fs.clone(), cx),
));
menu_options.push(ContextMenuItem::Separator);
let icon_style = theme::current(cx).copilot.out_link_icon.clone();
menu_options.push(ContextMenuItem::action(
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
Flex::row()
.with_child(Label::new("Copilot Settings", style.label.clone()))
.with_child(theme::ui::icon(icon_style.style_for(state)))
.align_children_center()
.into_any()
},
OsOpen::new(COPILOT_SETTINGS_URL),
));
menu_options.push(ContextMenuItem::action("Sign Out", SignOut));
self.popup_menu.update(cx, |menu, cx| {
menu.toggle(
Default::default(),
AnchorCorner::BottomRight,
menu_options,
cx,
);
None,
move |cx| toggle_copilot_globally(fs.clone(), cx),
)
.separator()
.link(
"Copilot Settings",
OpenBrowser {
url: COPILOT_SETTINGS_URL.to_string(),
}
.boxed_clone(),
)
.action("Sign Out", SignOut.boxed_clone())
});
}
pub fn update_enabled(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let suggestion_anchor = editor.selections.newest_anchor().start;
@ -299,8 +235,10 @@ impl CopilotButton {
impl StatusItemView for CopilotButton {
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
self.editor_subscription =
Some((cx.observe(&editor, Self::update_enabled), editor.id()));
self.editor_subscription = Some((
cx.observe(&editor, Self::update_enabled),
editor.entity_id().as_u64() as usize,
));
self.update_enabled(editor, cx);
} else {
self.language = None;
@ -312,9 +250,9 @@ impl StatusItemView for CopilotButton {
}
async fn configure_disabled_globs(
workspace: WeakViewHandle<Workspace>,
workspace: WeakView<Workspace>,
path_to_disable: Option<Arc<Path>>,
mut cx: AsyncAppContext,
mut cx: AsyncWindowContext,
) -> Result<()> {
let settings_editor = workspace
.update(&mut cx, |_, cx| {
@ -396,20 +334,23 @@ fn initiate_sign_in(cx: &mut WindowContext) {
match status {
Status::Starting { task } => {
let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() else {
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
return;
};
workspace.update(cx, |workspace, cx| {
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
cx,
)
});
let workspace = workspace.downgrade();
);
workspace.weak_handle()
}) else {
return;
};
cx.spawn(|mut cx| async move {
task.await;
if let Some(copilot) = cx.read(Copilot::global) {
if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() {
workspace
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
Status::Authorized => workspace.show_toast(

View File

@ -1,27 +0,0 @@
[package]
name = "copilot_button2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/copilot_button.rs"
doctest = false
[dependencies]
copilot = { package = "copilot2", path = "../copilot2" }
editor = { package = "editor2", path = "../editor2" }
fs = { package = "fs2", path = "../fs2" }
zed-actions = { package="zed_actions2", path = "../zed_actions2"}
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
workspace = { package = "workspace2", path = "../workspace2" }
anyhow.workspace = true
smol.workspace = true
futures.workspace = true
[dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

View File

@ -1,378 +0,0 @@
use anyhow::Result;
use copilot::{Copilot, SignOut, Status};
use editor::{scroll::autoscroll::Autoscroll, Editor};
use fs::Fs;
use gpui::{
div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement,
Render, Subscription, View, ViewContext, WeakView, WindowContext,
};
use language::{
language_settings::{self, all_language_settings, AllLanguageSettings},
File, Language,
};
use settings::{update_settings_file, Settings, SettingsStore};
use std::{path::Path, sync::Arc};
use util::{paths, ResultExt};
use workspace::{
create_and_open_local_file,
item::ItemHandle,
ui::{popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, IconSize, Tooltip},
StatusItemView, Toast, Workspace,
};
use zed_actions::OpenBrowser;
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
const COPILOT_STARTING_TOAST_ID: usize = 1337;
const COPILOT_ERROR_TOAST_ID: usize = 1338;
pub struct CopilotButton {
editor_subscription: Option<(Subscription, usize)>,
editor_enabled: Option<bool>,
language: Option<Arc<Language>>,
file: Option<Arc<dyn File>>,
fs: Arc<dyn Fs>,
}
impl Render for CopilotButton {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let all_language_settings = all_language_settings(None, cx);
if !all_language_settings.copilot.feature_enabled {
return div();
}
let Some(copilot) = Copilot::global(cx) else {
return div();
};
let status = copilot.read(cx).status();
let enabled = self
.editor_enabled
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
let icon = match status {
Status::Error(_) => Icon::CopilotError,
Status::Authorized => {
if enabled {
Icon::Copilot
} else {
Icon::CopilotDisabled
}
}
_ => Icon::CopilotInit,
};
if let Status::Error(e) = status {
return div().child(
IconButton::new("copilot-error", icon)
.icon_size(IconSize::Small)
.on_click(cx.listener(move |_, _, cx| {
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
workspace
.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(
COPILOT_ERROR_TOAST_ID,
format!("Copilot can't be started: {}", e),
)
.on_click(
"Reinstall Copilot",
|cx| {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| {
copilot.reinstall(cx)
})
.detach();
}
},
),
cx,
);
})
.ok();
}
}))
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
);
}
let this = cx.view().clone();
div().child(
popover_menu("copilot")
.menu(move |cx| match status {
Status::Authorized => {
Some(this.update(cx, |this, cx| this.build_copilot_menu(cx)))
}
_ => Some(this.update(cx, |this, cx| this.build_copilot_start_menu(cx))),
})
.anchor(AnchorCorner::BottomRight)
.trigger(
IconButton::new("copilot-icon", icon)
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
),
)
}
}
impl CopilotButton {
pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
.detach();
Self {
editor_subscription: None,
editor_enabled: None,
language: None,
file: None,
fs,
}
}
pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
let fs = self.fs.clone();
ContextMenu::build(cx, |menu, _| {
menu.entry("Sign In", None, initiate_sign_in).entry(
"Disable Copilot",
None,
move |cx| hide_copilot(fs.clone(), cx),
)
})
}
pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
let fs = self.fs.clone();
return ContextMenu::build(cx, move |mut menu, cx| {
if let Some(language) = self.language.clone() {
let fs = fs.clone();
let language_enabled =
language_settings::language_settings(Some(&language), None, cx)
.show_copilot_suggestions;
menu = menu.entry(
format!(
"{} Suggestions for {}",
if language_enabled { "Hide" } else { "Show" },
language.name()
),
None,
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
);
}
let settings = AllLanguageSettings::get_global(cx);
if let Some(file) = &self.file {
let path = file.path().clone();
let path_enabled = settings.copilot_enabled_for_path(&path);
menu = menu.entry(
format!(
"{} Suggestions for This Path",
if path_enabled { "Hide" } else { "Show" }
),
None,
move |cx| {
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
if let Ok(workspace) = workspace.root_view(cx) {
let workspace = workspace.downgrade();
cx.spawn(|cx| {
configure_disabled_globs(
workspace,
path_enabled.then_some(path.clone()),
cx,
)
})
.detach_and_log_err(cx);
}
}
},
);
}
let globally_enabled = settings.copilot_enabled(None, None);
menu.entry(
if globally_enabled {
"Hide Suggestions for All Files"
} else {
"Show Suggestions for All Files"
},
None,
move |cx| toggle_copilot_globally(fs.clone(), cx),
)
.separator()
.link(
"Copilot Settings",
OpenBrowser {
url: COPILOT_SETTINGS_URL.to_string(),
}
.boxed_clone(),
)
.action("Sign Out", SignOut.boxed_clone())
});
}
pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let suggestion_anchor = editor.selections.newest_anchor().start;
let language = snapshot.language_at(suggestion_anchor);
let file = snapshot.file_at(suggestion_anchor).cloned();
self.editor_enabled = Some(
all_language_settings(self.file.as_ref(), cx)
.copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
);
self.language = language.cloned();
self.file = file;
cx.notify()
}
}
impl StatusItemView for CopilotButton {
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
self.editor_subscription = Some((
cx.observe(&editor, Self::update_enabled),
editor.entity_id().as_u64() as usize,
));
self.update_enabled(editor, cx);
} else {
self.language = None;
self.editor_subscription = None;
self.editor_enabled = None;
}
cx.notify();
}
}
async fn configure_disabled_globs(
workspace: WeakView<Workspace>,
path_to_disable: Option<Arc<Path>>,
mut cx: AsyncWindowContext,
) -> Result<()> {
let settings_editor = workspace
.update(&mut cx, |_, cx| {
create_and_open_local_file(&paths::SETTINGS, cx, || {
settings::initial_user_settings_content().as_ref().into()
})
})?
.await?
.downcast::<Editor>()
.unwrap();
settings_editor.downgrade().update(&mut cx, |item, cx| {
let text = item.buffer().read(cx).snapshot(cx).text();
let settings = cx.global::<SettingsStore>();
let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
let copilot = file.copilot.get_or_insert_with(Default::default);
let globs = copilot.disabled_globs.get_or_insert_with(|| {
settings
.get::<AllLanguageSettings>(None)
.copilot
.disabled_globs
.iter()
.map(|glob| glob.glob().to_string())
.collect()
});
if let Some(path_to_disable) = &path_to_disable {
globs.push(path_to_disable.to_string_lossy().into_owned());
} else {
globs.clear();
}
});
if !edits.is_empty() {
item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
selections.select_ranges(edits.iter().map(|e| e.0.clone()));
});
// When *enabling* a path, don't actually perform an edit, just select the range.
if path_to_disable.is_some() {
item.edit(edits.iter().cloned(), cx);
}
}
})?;
anyhow::Ok(())
}
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
});
}
fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_copilot_suggestions =
all_language_settings(None, cx).copilot_enabled(Some(&language), None);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.languages
.entry(language.name())
.or_default()
.show_copilot_suggestions = Some(!show_copilot_suggestions);
});
}
fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.features.get_or_insert(Default::default()).copilot = Some(false);
});
}
fn initiate_sign_in(cx: &mut WindowContext) {
let Some(copilot) = Copilot::global(cx) else {
return;
};
let status = copilot.read(cx).status();
match status {
Status::Starting { task } => {
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
return;
};
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
cx,
);
workspace.weak_handle()
}) else {
return;
};
cx.spawn(|mut cx| async move {
task.await;
if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() {
workspace
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
Status::Authorized => workspace.show_toast(
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
cx,
),
_ => {
workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
}
})
.log_err();
}
})
.detach();
}
_ => {
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
}
}
}

View File

@ -9,17 +9,18 @@ path = "src/language_selector.rs"
doctest = false
[dependencies]
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
language = { path = "../language" }
gpui = { path = "../gpui" }
picker = { path = "../picker" }
project = { path = "../project" }
theme = { path = "../theme" }
settings = { path = "../settings" }
editor = { package = "editor2", path = "../editor2" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
language = { package = "language2", path = "../language2" }
gpui = { package = "gpui2", path = "../gpui2" }
picker = { package = "picker2", path = "../picker2" }
project = { package = "project2", path = "../project2" }
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
settings = { package = "settings2", path = "../settings2" }
util = { path = "../util" }
workspace = { path = "../workspace" }
workspace = { package = "workspace2", path = "../workspace2" }
anyhow.workspace = true
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

View File

@ -1,15 +1,14 @@
use editor::Editor;
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
};
use gpui::{div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView};
use std::sync::Arc;
use ui::{Button, ButtonCommon, Clickable, LabelSize, Tooltip};
use workspace::{item::ItemHandle, StatusItemView, Workspace};
use crate::LanguageSelector;
pub struct ActiveBufferLanguage {
active_language: Option<Option<Arc<str>>>,
workspace: WeakViewHandle<Workspace>,
workspace: WeakView<Workspace>,
_observe_active_editor: Option<Subscription>,
}
@ -22,7 +21,7 @@ impl ActiveBufferLanguage {
}
}
fn update_language(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
fn update_language(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
self.active_language = Some(None);
let editor = editor.read(cx);
@ -36,44 +35,28 @@ impl ActiveBufferLanguage {
}
}
impl Entity for ActiveBufferLanguage {
type Event = ();
}
impl View for ActiveBufferLanguage {
fn ui_name() -> &'static str {
"ActiveBufferLanguage"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(active_language) = self.active_language.as_ref() {
impl Render for ActiveBufferLanguage {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div().when_some(self.active_language.as_ref(), |el, active_language| {
let active_language_text = if let Some(active_language_text) = active_language {
active_language_text.to_string()
} else {
"Unknown".to_string()
};
let theme = theme::current(cx).clone();
MouseEventHandler::new::<Self, _>(0, cx, |state, cx| {
let theme = &theme::current(cx).workspace.status_bar;
let style = theme.active_language.style_for(state);
Label::new(active_language_text, style.text.clone())
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
crate::toggle(workspace, &Default::default(), cx)
});
}
})
.with_tooltip::<Self>(0, "Select Language", None, theme.tooltip.clone(), cx)
.into_any()
} else {
Empty::new().into_any()
}
el.child(
Button::new("change-language", active_language_text)
.label_size(LabelSize::Small)
.on_click(cx.listener(|this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
LanguageSelector::toggle(workspace, cx)
});
}
}))
.tooltip(|cx| Tooltip::text("Select Language", cx)),
)
})
}
}

View File

@ -4,46 +4,87 @@ pub use active_buffer_language::ActiveBufferLanguage;
use anyhow::anyhow;
use editor::Editor;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{actions, elements::*, AppContext, ModelHandle, MouseState, ViewContext};
use gpui::{
actions, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
};
use language::{Buffer, LanguageRegistry};
use picker::{Picker, PickerDelegate, PickerEvent};
use picker::{Picker, PickerDelegate};
use project::Project;
use std::sync::Arc;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::Workspace;
use workspace::{ModalView, Workspace};
actions!(language_selector, [Toggle]);
pub fn init(cx: &mut AppContext) {
Picker::<LanguageSelectorDelegate>::init(cx);
cx.add_action(toggle);
cx.observe_new_views(LanguageSelector::register).detach();
}
pub fn toggle(
workspace: &mut Workspace,
_: &Toggle,
cx: &mut ViewContext<Workspace>,
) -> Option<()> {
let (_, buffer, _) = workspace
.active_item(cx)?
.act_as::<Editor>(cx)?
.read(cx)
.active_excerpt(cx)?;
workspace.toggle_modal(cx, |workspace, cx| {
let registry = workspace.app_state().languages.clone();
cx.add_view(|cx| {
Picker::new(
LanguageSelectorDelegate::new(buffer, workspace.project().clone(), registry),
cx,
)
})
});
Some(())
pub struct LanguageSelector {
picker: View<Picker<LanguageSelectorDelegate>>,
}
impl LanguageSelector {
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(move |workspace, _: &Toggle, cx| {
Self::toggle(workspace, cx);
});
}
fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<()> {
let registry = workspace.app_state().languages.clone();
let (_, buffer, _) = workspace
.active_item(cx)?
.act_as::<Editor>(cx)?
.read(cx)
.active_excerpt(cx)?;
let project = workspace.project().clone();
workspace.toggle_modal(cx, move |cx| {
LanguageSelector::new(buffer, project, registry, cx)
});
Some(())
}
fn new(
buffer: Model<Buffer>,
project: Model<Project>,
language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = LanguageSelectorDelegate::new(
cx.view().downgrade(),
buffer,
project,
language_registry,
);
let picker = cx.new_view(|cx| Picker::new(delegate, cx));
Self { picker }
}
}
impl Render for LanguageSelector {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_stack().w(rems(34.)).child(self.picker.clone())
}
}
impl FocusableView for LanguageSelector {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl EventEmitter<DismissEvent> for LanguageSelector {}
impl ModalView for LanguageSelector {}
pub struct LanguageSelectorDelegate {
buffer: ModelHandle<Buffer>,
project: ModelHandle<Project>,
language_selector: WeakView<LanguageSelector>,
buffer: Model<Buffer>,
project: Model<Project>,
language_registry: Arc<LanguageRegistry>,
candidates: Vec<StringMatchCandidate>,
matches: Vec<StringMatch>,
@ -52,8 +93,9 @@ pub struct LanguageSelectorDelegate {
impl LanguageSelectorDelegate {
fn new(
buffer: ModelHandle<Buffer>,
project: ModelHandle<Project>,
language_selector: WeakView<LanguageSelector>,
buffer: Model<Buffer>,
project: Model<Project>,
language_registry: Arc<LanguageRegistry>,
) -> Self {
let candidates = language_registry
@ -62,29 +104,22 @@ impl LanguageSelectorDelegate {
.enumerate()
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
.collect::<Vec<_>>();
let mut matches = candidates
.iter()
.map(|candidate| StringMatch {
candidate_id: candidate.id,
score: 0.,
positions: Default::default(),
string: candidate.string.clone(),
})
.collect::<Vec<_>>();
matches.sort_unstable_by(|mat1, mat2| mat1.string.cmp(&mat2.string));
Self {
language_selector,
buffer,
project,
language_registry,
candidates,
matches,
matches: vec![],
selected_index: 0,
}
}
}
impl PickerDelegate for LanguageSelectorDelegate {
type ListItem = ListItem;
fn placeholder_text(&self) -> Arc<str> {
"Select a language...".into()
}
@ -102,23 +137,25 @@ impl PickerDelegate for LanguageSelectorDelegate {
cx.spawn(|_, mut cx| async move {
let language = language.await?;
let project = project
.upgrade(&cx)
.upgrade()
.ok_or_else(|| anyhow!("project was dropped"))?;
let buffer = buffer
.upgrade(&cx)
.upgrade()
.ok_or_else(|| anyhow!("buffer was dropped"))?;
project.update(&mut cx, |project, cx| {
project.set_language_for_buffer(&buffer, language, cx);
});
anyhow::Ok(())
})
})
.detach_and_log_err(cx);
}
cx.emit(PickerEvent::Dismiss);
self.dismissed(cx);
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.language_selector
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
}
fn selected_index(&self) -> usize {
self.selected_index
@ -133,7 +170,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
query: String,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
let background = cx.background().clone();
let background = cx.background_executor().clone();
let candidates = self.candidates.clone();
cx.spawn(|this, mut cx| async move {
let matches = if query.is_empty() {
@ -160,7 +197,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
};
this.update(&mut cx, |this, cx| {
let delegate = this.delegate_mut();
let delegate = &mut this.delegate;
delegate.matches = matches;
delegate.selected_index = delegate
.selected_index
@ -174,23 +211,22 @@ impl PickerDelegate for LanguageSelectorDelegate {
fn render_match(
&self,
ix: usize,
mouse_state: &mut MouseState,
selected: bool,
cx: &AppContext,
) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx);
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = &self.matches[ix];
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
let mut label = mat.string.clone();
if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
label.push_str(" (current)");
}
Label::new(label, style.label.clone())
.with_highlights(mat.positions.clone())
.contained()
.with_style(style.container)
.into_any()
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(HighlightedLabel::new(label, mat.positions.clone())),
)
}
}

View File

@ -1,26 +0,0 @@
[package]
name = "language_selector2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/language_selector.rs"
doctest = false
[dependencies]
editor = { package = "editor2", path = "../editor2" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
language = { package = "language2", path = "../language2" }
gpui = { package = "gpui2", path = "../gpui2" }
picker = { package = "picker2", path = "../picker2" }
project = { package = "project2", path = "../project2" }
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
settings = { package = "settings2", path = "../settings2" }
util = { path = "../util" }
workspace = { package = "workspace2", path = "../workspace2" }
anyhow.workspace = true
[dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

View File

@ -1,79 +0,0 @@
use editor::Editor;
use gpui::{div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView};
use std::sync::Arc;
use ui::{Button, ButtonCommon, Clickable, LabelSize, Tooltip};
use workspace::{item::ItemHandle, StatusItemView, Workspace};
use crate::LanguageSelector;
pub struct ActiveBufferLanguage {
active_language: Option<Option<Arc<str>>>,
workspace: WeakView<Workspace>,
_observe_active_editor: Option<Subscription>,
}
impl ActiveBufferLanguage {
pub fn new(workspace: &Workspace) -> Self {
Self {
active_language: None,
workspace: workspace.weak_handle(),
_observe_active_editor: None,
}
}
fn update_language(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
self.active_language = Some(None);
let editor = editor.read(cx);
if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
if let Some(language) = buffer.read(cx).language() {
self.active_language = Some(Some(language.name()));
}
}
cx.notify();
}
}
impl Render for ActiveBufferLanguage {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div().when_some(self.active_language.as_ref(), |el, active_language| {
let active_language_text = if let Some(active_language_text) = active_language {
active_language_text.to_string()
} else {
"Unknown".to_string()
};
el.child(
Button::new("change-language", active_language_text)
.label_size(LabelSize::Small)
.on_click(cx.listener(|this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
LanguageSelector::toggle(workspace, cx)
});
}
}))
.tooltip(|cx| Tooltip::text("Select Language", cx)),
)
})
}
}
impl StatusItemView for ActiveBufferLanguage {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
self._observe_active_editor = Some(cx.observe(&editor, Self::update_language));
self.update_language(editor, cx);
} else {
self.active_language = None;
self._observe_active_editor = None;
}
cx.notify();
}
}

View File

@ -1,232 +0,0 @@
mod active_buffer_language;
pub use active_buffer_language::ActiveBufferLanguage;
use anyhow::anyhow;
use editor::Editor;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
actions, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
};
use language::{Buffer, LanguageRegistry};
use picker::{Picker, PickerDelegate};
use project::Project;
use std::sync::Arc;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::{ModalView, Workspace};
actions!(language_selector, [Toggle]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(LanguageSelector::register).detach();
}
pub struct LanguageSelector {
picker: View<Picker<LanguageSelectorDelegate>>,
}
impl LanguageSelector {
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(move |workspace, _: &Toggle, cx| {
Self::toggle(workspace, cx);
});
}
fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<()> {
let registry = workspace.app_state().languages.clone();
let (_, buffer, _) = workspace
.active_item(cx)?
.act_as::<Editor>(cx)?
.read(cx)
.active_excerpt(cx)?;
let project = workspace.project().clone();
workspace.toggle_modal(cx, move |cx| {
LanguageSelector::new(buffer, project, registry, cx)
});
Some(())
}
fn new(
buffer: Model<Buffer>,
project: Model<Project>,
language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = LanguageSelectorDelegate::new(
cx.view().downgrade(),
buffer,
project,
language_registry,
);
let picker = cx.new_view(|cx| Picker::new(delegate, cx));
Self { picker }
}
}
impl Render for LanguageSelector {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_stack().w(rems(34.)).child(self.picker.clone())
}
}
impl FocusableView for LanguageSelector {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl EventEmitter<DismissEvent> for LanguageSelector {}
impl ModalView for LanguageSelector {}
pub struct LanguageSelectorDelegate {
language_selector: WeakView<LanguageSelector>,
buffer: Model<Buffer>,
project: Model<Project>,
language_registry: Arc<LanguageRegistry>,
candidates: Vec<StringMatchCandidate>,
matches: Vec<StringMatch>,
selected_index: usize,
}
impl LanguageSelectorDelegate {
fn new(
language_selector: WeakView<LanguageSelector>,
buffer: Model<Buffer>,
project: Model<Project>,
language_registry: Arc<LanguageRegistry>,
) -> Self {
let candidates = language_registry
.language_names()
.into_iter()
.enumerate()
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
.collect::<Vec<_>>();
Self {
language_selector,
buffer,
project,
language_registry,
candidates,
matches: vec![],
selected_index: 0,
}
}
}
impl PickerDelegate for LanguageSelectorDelegate {
type ListItem = ListItem;
fn placeholder_text(&self) -> Arc<str> {
"Select a language...".into()
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(mat) = self.matches.get(self.selected_index) {
let language_name = &self.candidates[mat.candidate_id].string;
let language = self.language_registry.language_for_name(language_name);
let project = self.project.downgrade();
let buffer = self.buffer.downgrade();
cx.spawn(|_, mut cx| async move {
let language = language.await?;
let project = project
.upgrade()
.ok_or_else(|| anyhow!("project was dropped"))?;
let buffer = buffer
.upgrade()
.ok_or_else(|| anyhow!("buffer was dropped"))?;
project.update(&mut cx, |project, cx| {
project.set_language_for_buffer(&buffer, language, cx);
})
})
.detach_and_log_err(cx);
}
self.dismissed(cx);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.language_selector
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(
&mut self,
query: String,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
let background = cx.background_executor().clone();
let candidates = self.candidates.clone();
cx.spawn(|this, mut cx| async move {
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
background,
)
.await
};
this.update(&mut cx, |this, cx| {
let delegate = &mut this.delegate;
delegate.matches = matches;
delegate.selected_index = delegate
.selected_index
.min(delegate.matches.len().saturating_sub(1));
cx.notify();
})
.log_err();
})
}
fn render_match(
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = &self.matches[ix];
let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
let mut label = mat.string.clone();
if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
label.push_str(" (current)");
}
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(HighlightedLabel::new(label, mat.positions.clone())),
)
}
}

View File

@ -9,32 +9,33 @@ path = "src/project_panel.rs"
doctest = false
[dependencies]
context_menu = { path = "../context_menu" }
collections = { path = "../collections" }
db = { path = "../db" }
drag_and_drop = { path = "../drag_and_drop" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
db = { path = "../db2", package = "db2" }
editor = { path = "../editor2", package = "editor2" }
gpui = { path = "../gpui2", package = "gpui2" }
menu = { path = "../menu2", package = "menu2" }
project = { path = "../project2", package = "project2" }
search = { package = "search2", path = "../search2" }
settings = { path = "../settings2", package = "settings2" }
theme = { path = "../theme2", package = "theme2" }
ui = { path = "../ui2", package = "ui2" }
util = { path = "../util" }
workspace = { path = "../workspace" }
workspace = { path = "../workspace2", package = "workspace2" }
anyhow.workspace = true
postage.workspace = true
futures.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
anyhow.workspace = true
schemars.workspace = true
smallvec.workspace = true
pretty_assertions.workspace = true
unicase = "2.6"
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
client = { path = "../client2", package = "client2", features = ["test-support"] }
language = { path = "../language2", package = "language2", features = ["test-support"] }
editor = { path = "../editor2", package = "editor2", features = ["test-support"] }
gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }
workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] }
serde_json.workspace = true

View File

@ -41,56 +41,47 @@ impl FileAssociations {
})
}
pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {
pub fn get_icon(path: &Path, cx: &AppContext) -> Option<Arc<str>> {
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
// FIXME: Associate a type with the languages and have the file's langauge
// override these associations
maybe!({
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
let suffix = path.icon_suffix()?;
// FIXME: Associate a type with the languages and have the file's langauge
// override these associations
maybe!({
let suffix = path.icon_suffix()?;
this.suffixes
.get(suffix)
.and_then(|type_str| this.types.get(type_str))
.map(|type_config| type_config.icon.clone())
})
.or_else(|| this.types.get("default").map(|config| config.icon.clone()))
})
.unwrap_or_else(|| Arc::from("".to_string()))
}
pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
maybe!({
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
let key = if expanded {
EXPANDED_DIRECTORY_TYPE
} else {
COLLAPSED_DIRECTORY_TYPE
};
this.types
.get(key)
this.suffixes
.get(suffix)
.and_then(|type_str| this.types.get(type_str))
.map(|type_config| type_config.icon.clone())
})
.unwrap_or_else(|| Arc::from("".to_string()))
.or_else(|| this.types.get("default").map(|config| config.icon.clone()))
}
pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
maybe!({
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
let key = if expanded {
EXPANDED_CHEVRON_TYPE
} else {
COLLAPSED_CHEVRON_TYPE
};
let key = if expanded {
EXPANDED_DIRECTORY_TYPE
} else {
COLLAPSED_DIRECTORY_TYPE
};
this.types
.get(key)
.map(|type_config| type_config.icon.clone())
})
.unwrap_or_else(|| Arc::from("".to_string()))
this.types
.get(key)
.map(|type_config| type_config.icon.clone())
}
pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
let key = if expanded {
EXPANDED_CHEVRON_TYPE
} else {
COLLAPSED_CHEVRON_TYPE
};
this.types
.get(key)
.map(|type_config| type_config.icon.clone())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,8 @@
use anyhow;
use gpui::Pixels;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Setting;
use settings::Settings;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
@ -12,7 +13,7 @@ pub enum ProjectPanelDockPosition {
#[derive(Deserialize, Debug)]
pub struct ProjectPanelSettings {
pub default_width: f32,
pub default_width: Pixels,
pub dock: ProjectPanelDockPosition,
pub file_icons: bool,
pub folder_icons: bool,
@ -32,7 +33,7 @@ pub struct ProjectPanelSettingsContent {
pub auto_reveal_entries: Option<bool>,
}
impl Setting for ProjectPanelSettings {
impl Settings for ProjectPanelSettings {
const KEY: Option<&'static str> = Some("project_panel");
type FileContent = ProjectPanelSettingsContent;
@ -40,7 +41,7 @@ impl Setting for ProjectPanelSettings {
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &gpui::AppContext,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}

View File

@ -1,41 +0,0 @@
[package]
name = "project_panel2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/project_panel.rs"
doctest = false
[dependencies]
collections = { path = "../collections" }
db = { path = "../db2", package = "db2" }
editor = { path = "../editor2", package = "editor2" }
gpui = { path = "../gpui2", package = "gpui2" }
menu = { path = "../menu2", package = "menu2" }
project = { path = "../project2", package = "project2" }
search = { package = "search2", path = "../search2" }
settings = { path = "../settings2", package = "settings2" }
theme = { path = "../theme2", package = "theme2" }
ui = { path = "../ui2", package = "ui2" }
util = { path = "../util" }
workspace = { path = "../workspace2", package = "workspace2" }
anyhow.workspace = true
postage.workspace = true
futures.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
schemars.workspace = true
smallvec.workspace = true
pretty_assertions.workspace = true
unicase = "2.6"
[dev-dependencies]
client = { path = "../client2", package = "client2", features = ["test-support"] }
language = { path = "../language2", package = "language2", features = ["test-support"] }
editor = { path = "../editor2", package = "editor2", features = ["test-support"] }
gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }
workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] }
serde_json.workspace = true

View File

@ -1,87 +0,0 @@
use std::{path::Path, str, sync::Arc};
use collections::HashMap;
use gpui::{AppContext, AssetSource};
use serde_derive::Deserialize;
use util::{maybe, paths::PathExt};
#[derive(Deserialize, Debug)]
struct TypeConfig {
icon: Arc<str>,
}
#[derive(Deserialize, Debug)]
pub struct FileAssociations {
suffixes: HashMap<String, String>,
types: HashMap<String, TypeConfig>,
}
const COLLAPSED_DIRECTORY_TYPE: &'static str = "collapsed_folder";
const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_folder";
const COLLAPSED_CHEVRON_TYPE: &'static str = "collapsed_chevron";
const EXPANDED_CHEVRON_TYPE: &'static str = "expanded_chevron";
pub const FILE_TYPES_ASSET: &'static str = "icons/file_icons/file_types.json";
pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
cx.set_global(FileAssociations::new(assets))
}
impl FileAssociations {
pub fn new(assets: impl AssetSource) -> Self {
assets
.load("icons/file_icons/file_types.json")
.and_then(|file| {
serde_json::from_str::<FileAssociations>(str::from_utf8(&file).unwrap())
.map_err(Into::into)
})
.unwrap_or_else(|_| FileAssociations {
suffixes: HashMap::default(),
types: HashMap::default(),
})
}
pub fn get_icon(path: &Path, cx: &AppContext) -> Option<Arc<str>> {
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
// FIXME: Associate a type with the languages and have the file's langauge
// override these associations
maybe!({
let suffix = path.icon_suffix()?;
this.suffixes
.get(suffix)
.and_then(|type_str| this.types.get(type_str))
.map(|type_config| type_config.icon.clone())
})
.or_else(|| this.types.get("default").map(|config| config.icon.clone()))
}
pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
let key = if expanded {
EXPANDED_DIRECTORY_TYPE
} else {
COLLAPSED_DIRECTORY_TYPE
};
this.types
.get(key)
.map(|type_config| type_config.icon.clone())
}
pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
let key = if expanded {
EXPANDED_CHEVRON_TYPE
} else {
COLLAPSED_CHEVRON_TYPE
};
this.types
.get(key)
.map(|type_config| type_config.icon.clone())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +0,0 @@
use anyhow;
use gpui::Pixels;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Settings;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProjectPanelDockPosition {
Left,
Right,
}
#[derive(Deserialize, Debug)]
pub struct ProjectPanelSettings {
pub default_width: Pixels,
pub dock: ProjectPanelDockPosition,
pub file_icons: bool,
pub folder_icons: bool,
pub git_status: bool,
pub indent_size: f32,
pub auto_reveal_entries: bool,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct ProjectPanelSettingsContent {
pub default_width: Option<f32>,
pub dock: Option<ProjectPanelDockPosition>,
pub file_icons: Option<bool>,
pub folder_icons: Option<bool>,
pub git_status: Option<bool>,
pub indent_size: Option<f32>,
pub auto_reveal_entries: Option<bool>,
}
impl Settings for ProjectPanelSettings {
const KEY: Option<&'static str> = Some("project_panel");
type FileContent = ProjectPanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}

View File

@ -9,17 +9,17 @@ path = "src/recent_projects.rs"
doctest = false
[dependencies]
db = { path = "../db" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
language = { path = "../language" }
picker = { path = "../picker" }
settings = { path = "../settings" }
text = { path = "../text" }
editor = { package = "editor2", path = "../editor2" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
picker = { package = "picker2", path = "../picker2" }
settings = { package = "settings2", path = "../settings2" }
text = { package = "text2", path = "../text2" }
util = { path = "../util"}
theme = { path = "../theme" }
workspace = { path = "../workspace" }
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
workspace = { package = "workspace2", path = "../workspace2" }
futures.workspace = true
ordered-float.workspace = true
@ -27,4 +27,4 @@ postage.workspace = true
smol.workspace = true
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

View File

@ -1,13 +1,11 @@
use std::path::Path;
use fuzzy::StringMatch;
use gpui::{
elements::{Label, LabelStyle},
AnyElement, Element,
};
use ui::{prelude::*, HighlightedLabel};
use util::paths::PathExt;
use workspace::WorkspaceLocation;
#[derive(IntoElement)]
pub struct HighlightedText {
pub text: String,
pub highlight_positions: Vec<usize>,
@ -42,11 +40,11 @@ impl HighlightedText {
char_count,
}
}
}
pub fn render<V: 'static>(self, style: impl Into<LabelStyle>) -> AnyElement<V> {
Label::new(self.text, style)
.with_highlights(self.highlight_positions)
.into_any()
impl RenderOnce for HighlightedText {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
HighlightedLabel::new(self.text, self.highlight_positions)
}
}

View File

@ -1,86 +1,122 @@
mod highlighted_workspace_location;
mod projects;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions,
anyhow::Result,
elements::{Flex, ParentElement},
AnyElement, AppContext, Element, Task, ViewContext, WeakViewHandle,
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, Subscription, Task,
View, ViewContext, WeakView,
};
use highlighted_workspace_location::HighlightedWorkspaceLocation;
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate, PickerEvent};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use ui::{prelude::*, ListItem, ListItemSpacing};
use util::paths::PathExt;
use workspace::{
notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
WORKSPACE_DB,
};
use workspace::{ModalView, Workspace, WorkspaceLocation, WORKSPACE_DB};
actions!(projects, [OpenRecent]);
pub use projects::OpenRecent;
pub fn init(cx: &mut AppContext) {
cx.add_async_action(toggle);
RecentProjects::init(cx);
cx.observe_new_views(RecentProjects::register).detach();
}
fn toggle(
_: &mut Workspace,
_: &OpenRecent,
cx: &mut ViewContext<Workspace>,
) -> Option<Task<Result<()>>> {
Some(cx.spawn(|workspace, mut cx| async move {
let workspace_locations: Vec<_> = cx
.background()
.spawn(async {
WORKSPACE_DB
.recent_workspaces_on_disk()
.await
.unwrap_or_default()
.into_iter()
.map(|(_, location)| location)
.collect()
})
.await;
pub struct RecentProjects {
pub picker: View<Picker<RecentProjectsDelegate>>,
rem_width: f32,
_subscription: Subscription,
}
workspace.update(&mut cx, |workspace, cx| {
if !workspace_locations.is_empty() {
workspace.toggle_modal(cx, |_, cx| {
let workspace = cx.weak_handle();
cx.add_view(|cx| {
RecentProjects::new(
RecentProjectsDelegate::new(workspace, workspace_locations, true),
cx,
)
.with_max_size(800., 1200.)
})
});
} else {
workspace.show_notification(0, cx, |cx| {
cx.add_view(|_| MessageNotification::new("No recent projects to open."))
impl ModalView for RecentProjects {}
impl RecentProjects {
fn new(delegate: RecentProjectsDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> Self {
let picker = cx.new_view(|cx| Picker::new(delegate, cx));
let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
// We do not want to block the UI on a potentially lenghty call to DB, so we're gonna swap
// out workspace locations once the future runs to completion.
cx.spawn(|this, mut cx| async move {
let workspaces = WORKSPACE_DB
.recent_workspaces_on_disk()
.await
.unwrap_or_default()
.into_iter()
.map(|(_, location)| location)
.collect();
this.update(&mut cx, move |this, cx| {
this.picker.update(cx, move |picker, cx| {
picker.delegate.workspace_locations = workspaces;
picker.update_matches(picker.query(cx), cx)
})
}
})?;
Ok(())
}))
})
.ok()
})
.detach();
Self {
picker,
rem_width,
_subscription,
}
}
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|workspace, _: &OpenRecent, cx| {
let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
if let Some(handler) = Self::open(workspace, cx) {
handler.detach_and_log_err(cx);
}
return;
};
recent_projects.update(cx, |recent_projects, cx| {
recent_projects
.picker
.update(cx, |picker, cx| picker.cycle_selection(cx))
});
});
}
fn open(_: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<Task<Result<()>>> {
Some(cx.spawn(|workspace, mut cx| async move {
workspace.update(&mut cx, |workspace, cx| {
let weak_workspace = cx.view().downgrade();
workspace.toggle_modal(cx, |cx| {
let delegate = RecentProjectsDelegate::new(weak_workspace, true);
let modal = RecentProjects::new(delegate, 34., cx);
modal
});
})?;
Ok(())
}))
}
pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx))
}
}
pub fn build_recent_projects(
workspace: WeakViewHandle<Workspace>,
workspaces: Vec<WorkspaceLocation>,
cx: &mut ViewContext<RecentProjects>,
) -> RecentProjects {
Picker::new(
RecentProjectsDelegate::new(workspace, workspaces, false),
cx,
)
.with_theme(|theme| theme.picker.clone())
impl EventEmitter<DismissEvent> for RecentProjects {}
impl FocusableView for RecentProjects {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
pub type RecentProjects = Picker<RecentProjectsDelegate>;
impl Render for RecentProjects {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_stack()
.w(rems(self.rem_width))
.child(self.picker.clone())
.on_mouse_down_out(cx.listener(|this, _, cx| {
this.picker.update(cx, |this, cx| {
this.cancel(&Default::default(), cx);
})
}))
}
}
pub struct RecentProjectsDelegate {
workspace: WeakViewHandle<Workspace>,
workspace: WeakView<Workspace>,
workspace_locations: Vec<WorkspaceLocation>,
selected_match_index: usize,
matches: Vec<StringMatch>,
@ -88,22 +124,20 @@ pub struct RecentProjectsDelegate {
}
impl RecentProjectsDelegate {
fn new(
workspace: WeakViewHandle<Workspace>,
workspace_locations: Vec<WorkspaceLocation>,
render_paths: bool,
) -> Self {
fn new(workspace: WeakView<Workspace>, render_paths: bool) -> Self {
Self {
workspace,
workspace_locations,
workspace_locations: vec![],
selected_match_index: 0,
matches: Default::default(),
render_paths,
}
}
}
impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
impl PickerDelegate for RecentProjectsDelegate {
type ListItem = ListItem;
fn placeholder_text(&self) -> Arc<str> {
"Recent Projects...".into()
}
@ -116,14 +150,14 @@ impl PickerDelegate for RecentProjectsDelegate {
self.selected_match_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<RecentProjects>) {
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_match_index = ix;
}
fn update_matches(
&mut self,
query: String,
cx: &mut ViewContext<RecentProjects>,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
let query = query.trim_start();
let smart_case = query.chars().any(|c| c.is_uppercase());
@ -147,7 +181,7 @@ impl PickerDelegate for RecentProjectsDelegate {
smart_case,
100,
&Default::default(),
cx.background().clone(),
cx.background_executor().clone(),
));
self.matches.sort_unstable_by_key(|m| m.candidate_id);
@ -162,11 +196,11 @@ impl PickerDelegate for RecentProjectsDelegate {
Task::ready(())
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<RecentProjects>) {
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some((selected_match, workspace)) = self
.matches
.get(self.selected_index())
.zip(self.workspace.upgrade(cx))
.zip(self.workspace.upgrade())
{
let workspace_location = &self.workspace_locations[selected_match.candidate_id];
workspace
@ -175,41 +209,39 @@ impl PickerDelegate for RecentProjectsDelegate {
.open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
})
.detach_and_log_err(cx);
cx.emit(PickerEvent::Dismiss);
cx.emit(DismissEvent);
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<RecentProjects>) {}
fn dismissed(&mut self, _: &mut ViewContext<Picker<Self>>) {}
fn render_match(
&self,
ix: usize,
mouse_state: &mut gpui::MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let string_match = &self.matches[ix];
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let Some(r#match) = self.matches.get(ix) else {
return None;
};
let highlighted_location = HighlightedWorkspaceLocation::new(
&string_match,
&self.workspace_locations[string_match.candidate_id],
&r#match,
&self.workspace_locations[r#match.candidate_id],
);
Flex::column()
.with_child(highlighted_location.names.render(style.label.clone()))
.with_children(
highlighted_location
.paths
.into_iter()
.filter(|_| self.render_paths)
.map(|highlighted_path| highlighted_path.render(style.label.clone())),
)
.flex(1., false)
.contained()
.with_style(style.container)
.into_any_named("match")
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(
v_stack()
.child(highlighted_location.names)
.when(self.render_paths, |this| {
this.children(highlighted_location.paths)
}),
),
)
}
}

View File

@ -1,30 +0,0 @@
[package]
name = "recent_projects2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/recent_projects.rs"
doctest = false
[dependencies]
editor = { package = "editor2", path = "../editor2" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
picker = { package = "picker2", path = "../picker2" }
settings = { package = "settings2", path = "../settings2" }
text = { package = "text2", path = "../text2" }
util = { path = "../util"}
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
workspace = { package = "workspace2", path = "../workspace2" }
futures.workspace = true
ordered-float.workspace = true
postage.workspace = true
smol.workspace = true
[dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

View File

@ -1,129 +0,0 @@
use std::path::Path;
use fuzzy::StringMatch;
use ui::{prelude::*, HighlightedLabel};
use util::paths::PathExt;
use workspace::WorkspaceLocation;
#[derive(IntoElement)]
pub struct HighlightedText {
pub text: String,
pub highlight_positions: Vec<usize>,
char_count: usize,
}
impl HighlightedText {
fn join(components: impl Iterator<Item = Self>, separator: &str) -> Self {
let mut char_count = 0;
let separator_char_count = separator.chars().count();
let mut text = String::new();
let mut highlight_positions = Vec::new();
for component in components {
if char_count != 0 {
text.push_str(separator);
char_count += separator_char_count;
}
highlight_positions.extend(
component
.highlight_positions
.iter()
.map(|position| position + char_count),
);
text.push_str(&component.text);
char_count += component.text.chars().count();
}
Self {
text,
highlight_positions,
char_count,
}
}
}
impl RenderOnce for HighlightedText {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
HighlightedLabel::new(self.text, self.highlight_positions)
}
}
pub struct HighlightedWorkspaceLocation {
pub names: HighlightedText,
pub paths: Vec<HighlightedText>,
}
impl HighlightedWorkspaceLocation {
pub fn new(string_match: &StringMatch, location: &WorkspaceLocation) -> Self {
let mut path_start_offset = 0;
let (names, paths): (Vec<_>, Vec<_>) = location
.paths()
.iter()
.map(|path| {
let path = path.compact();
let highlighted_text = Self::highlights_for_path(
path.as_ref(),
&string_match.positions,
path_start_offset,
);
path_start_offset += highlighted_text.1.char_count;
highlighted_text
})
.unzip();
Self {
names: HighlightedText::join(names.into_iter().filter_map(|name| name), ", "),
paths,
}
}
// Compute the highlighted text for the name and path
fn highlights_for_path(
path: &Path,
match_positions: &Vec<usize>,
path_start_offset: usize,
) -> (Option<HighlightedText>, HighlightedText) {
let path_string = path.to_string_lossy();
let path_char_count = path_string.chars().count();
// Get the subset of match highlight positions that line up with the given path.
// Also adjusts them to start at the path start
let path_positions = match_positions
.iter()
.copied()
.skip_while(|position| *position < path_start_offset)
.take_while(|position| *position < path_start_offset + path_char_count)
.map(|position| position - path_start_offset)
.collect::<Vec<_>>();
// Again subset the highlight positions to just those that line up with the file_name
// again adjusted to the start of the file_name
let file_name_text_and_positions = path.file_name().map(|file_name| {
let text = file_name.to_string_lossy();
let char_count = text.chars().count();
let file_name_start = path_char_count - char_count;
let highlight_positions = path_positions
.iter()
.copied()
.skip_while(|position| *position < file_name_start)
.take_while(|position| *position < file_name_start + char_count)
.map(|position| position - file_name_start)
.collect::<Vec<_>>();
HighlightedText {
text: text.to_string(),
highlight_positions,
char_count,
}
});
(
file_name_text_and_positions,
HighlightedText {
text: path_string.to_string(),
highlight_positions: path_positions,
char_count: path_char_count,
},
)
}
}

View File

@ -1,247 +0,0 @@
mod highlighted_workspace_location;
mod projects;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, Subscription, Task,
View, ViewContext, WeakView,
};
use highlighted_workspace_location::HighlightedWorkspaceLocation;
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use ui::{prelude::*, ListItem, ListItemSpacing};
use util::paths::PathExt;
use workspace::{ModalView, Workspace, WorkspaceLocation, WORKSPACE_DB};
pub use projects::OpenRecent;
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(RecentProjects::register).detach();
}
pub struct RecentProjects {
pub picker: View<Picker<RecentProjectsDelegate>>,
rem_width: f32,
_subscription: Subscription,
}
impl ModalView for RecentProjects {}
impl RecentProjects {
fn new(delegate: RecentProjectsDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> Self {
let picker = cx.new_view(|cx| Picker::new(delegate, cx));
let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
// We do not want to block the UI on a potentially lenghty call to DB, so we're gonna swap
// out workspace locations once the future runs to completion.
cx.spawn(|this, mut cx| async move {
let workspaces = WORKSPACE_DB
.recent_workspaces_on_disk()
.await
.unwrap_or_default()
.into_iter()
.map(|(_, location)| location)
.collect();
this.update(&mut cx, move |this, cx| {
this.picker.update(cx, move |picker, cx| {
picker.delegate.workspace_locations = workspaces;
picker.update_matches(picker.query(cx), cx)
})
})
.ok()
})
.detach();
Self {
picker,
rem_width,
_subscription,
}
}
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|workspace, _: &OpenRecent, cx| {
let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
if let Some(handler) = Self::open(workspace, cx) {
handler.detach_and_log_err(cx);
}
return;
};
recent_projects.update(cx, |recent_projects, cx| {
recent_projects
.picker
.update(cx, |picker, cx| picker.cycle_selection(cx))
});
});
}
fn open(_: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<Task<Result<()>>> {
Some(cx.spawn(|workspace, mut cx| async move {
workspace.update(&mut cx, |workspace, cx| {
let weak_workspace = cx.view().downgrade();
workspace.toggle_modal(cx, |cx| {
let delegate = RecentProjectsDelegate::new(weak_workspace, true);
let modal = RecentProjects::new(delegate, 34., cx);
modal
});
})?;
Ok(())
}))
}
pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx))
}
}
impl EventEmitter<DismissEvent> for RecentProjects {}
impl FocusableView for RecentProjects {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for RecentProjects {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_stack()
.w(rems(self.rem_width))
.child(self.picker.clone())
.on_mouse_down_out(cx.listener(|this, _, cx| {
this.picker.update(cx, |this, cx| {
this.cancel(&Default::default(), cx);
})
}))
}
}
pub struct RecentProjectsDelegate {
workspace: WeakView<Workspace>,
workspace_locations: Vec<WorkspaceLocation>,
selected_match_index: usize,
matches: Vec<StringMatch>,
render_paths: bool,
}
impl RecentProjectsDelegate {
fn new(workspace: WeakView<Workspace>, render_paths: bool) -> Self {
Self {
workspace,
workspace_locations: vec![],
selected_match_index: 0,
matches: Default::default(),
render_paths,
}
}
}
impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
impl PickerDelegate for RecentProjectsDelegate {
type ListItem = ListItem;
fn placeholder_text(&self) -> Arc<str> {
"Recent Projects...".into()
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_match_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_match_index = ix;
}
fn update_matches(
&mut self,
query: String,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
let query = query.trim_start();
let smart_case = query.chars().any(|c| c.is_uppercase());
let candidates = self
.workspace_locations
.iter()
.enumerate()
.map(|(id, location)| {
let combined_string = location
.paths()
.iter()
.map(|path| path.compact().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("");
StringMatchCandidate::new(id, combined_string)
})
.collect::<Vec<_>>();
self.matches = smol::block_on(fuzzy::match_strings(
candidates.as_slice(),
query,
smart_case,
100,
&Default::default(),
cx.background_executor().clone(),
));
self.matches.sort_unstable_by_key(|m| m.candidate_id);
self.selected_match_index = self
.matches
.iter()
.enumerate()
.rev()
.max_by_key(|(_, m)| OrderedFloat(m.score))
.map(|(ix, _)| ix)
.unwrap_or(0);
Task::ready(())
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some((selected_match, workspace)) = self
.matches
.get(self.selected_index())
.zip(self.workspace.upgrade())
{
let workspace_location = &self.workspace_locations[selected_match.candidate_id];
workspace
.update(cx, |workspace, cx| {
workspace
.open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
})
.detach_and_log_err(cx);
cx.emit(DismissEvent);
}
}
fn dismissed(&mut self, _: &mut ViewContext<Picker<Self>>) {}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let Some(r#match) = self.matches.get(ix) else {
return None;
};
let highlighted_location = HighlightedWorkspaceLocation::new(
&r#match,
&self.workspace_locations[r#match.candidate_id],
);
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(
v_stack()
.child(highlighted_location.names)
.when(self.render_paths, |this| {
this.children(highlighted_location.paths)
}),
),
)
}
}

View File

@ -17,9 +17,9 @@ path = "src/main.rs"
[dependencies]
ai = { package = "ai2", path = "../ai2"}
audio = { package = "audio2", path = "../audio2" }
activity_indicator = { package = "activity_indicator2", path = "../activity_indicator2"}
activity_indicator = { path = "../activity_indicator"}
auto_update = { package = "auto_update2", path = "../auto_update2" }
breadcrumbs = { package = "breadcrumbs2", path = "../breadcrumbs2" }
breadcrumbs = { path = "../breadcrumbs" }
call = { package = "call2", path = "../call2" }
channel = { package = "channel2", path = "../channel2" }
cli = { path = "../cli" }
@ -30,7 +30,7 @@ command_palette = { path = "../command_palette" }
client = { package = "client2", path = "../client2" }
# clock = { path = "../clock" }
copilot = { package = "copilot2", path = "../copilot2" }
copilot_button = { package = "copilot_button2", path = "../copilot_button2" }
copilot_button = { path = "../copilot_button" }
diagnostics = { path = "../diagnostics" }
db = { package = "db2", path = "../db2" }
editor = { package="editor2", path = "../editor2" }
@ -44,7 +44,7 @@ gpui = { package = "gpui2", path = "../gpui2" }
install_cli = { package = "install_cli2", path = "../install_cli2" }
journal = { package = "journal2", path = "../journal2" }
language = { package = "language2", path = "../language2" }
language_selector = { package = "language_selector2", path = "../language_selector2" }
language_selector = { path = "../language_selector" }
lsp = { package = "lsp2", path = "../lsp2" }
menu = { package = "menu2", path = "../menu2" }
language_tools = { package = "language_tools2", path = "../language_tools2" }
@ -54,10 +54,10 @@ assistant = { package = "assistant2", path = "../assistant2" }
outline = { package = "outline2", path = "../outline2" }
# plugin_runtime = { path = "../plugin_runtime",optional = true }
project = { package = "project2", path = "../project2" }
project_panel = { package = "project_panel2", path = "../project_panel2" }
project_panel = { path = "../project_panel" }
project_symbols = { path = "../project_symbols" }
quick_action_bar = { path = "../quick_action_bar" }
recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
recent_projects = { path = "../recent_projects" }
rope = { package = "rope2", path = "../rope2"}
rpc = { package = "rpc2", path = "../rpc2" }
settings = { package = "settings2", path = "../settings2" }