mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
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:
parent
252694390a
commit
292b3397ab
134
Cargo.lock
generated
134
Cargo.lock
generated
@ -5,23 +5,6 @@ version = 3
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "activity_indicator"
|
name = "activity_indicator"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"auto_update2",
|
"auto_update2",
|
||||||
@ -1128,23 +1111,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "breadcrumbs"
|
name = "breadcrumbs"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"editor2",
|
"editor2",
|
||||||
@ -1855,7 +1821,7 @@ dependencies = [
|
|||||||
"postage",
|
"postage",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"project2",
|
"project2",
|
||||||
"recent_projects2",
|
"recent_projects",
|
||||||
"rich_text2",
|
"rich_text2",
|
||||||
"rpc2",
|
"rpc2",
|
||||||
"schemars",
|
"schemars",
|
||||||
@ -2059,25 +2025,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "copilot_button"
|
name = "copilot_button"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"copilot2",
|
"copilot2",
|
||||||
@ -4593,23 +4540,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "language_selector"
|
name = "language_selector"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"editor2",
|
"editor2",
|
||||||
@ -6618,35 +6548,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "project_panel"
|
name = "project_panel"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"client2",
|
"client2",
|
||||||
@ -7029,27 +6930,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "recent_projects"
|
name = "recent_projects"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"editor2",
|
"editor2",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
@ -11290,7 +11170,7 @@ dependencies = [
|
|||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.119.0"
|
version = "0.119.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator2",
|
"activity_indicator",
|
||||||
"ai2",
|
"ai2",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assistant2",
|
"assistant2",
|
||||||
@ -11301,7 +11181,7 @@ dependencies = [
|
|||||||
"audio2",
|
"audio2",
|
||||||
"auto_update2",
|
"auto_update2",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"breadcrumbs2",
|
"breadcrumbs",
|
||||||
"call2",
|
"call2",
|
||||||
"channel2",
|
"channel2",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -11311,7 +11191,7 @@ dependencies = [
|
|||||||
"collections",
|
"collections",
|
||||||
"command_palette",
|
"command_palette",
|
||||||
"copilot2",
|
"copilot2",
|
||||||
"copilot_button2",
|
"copilot_button",
|
||||||
"ctor",
|
"ctor",
|
||||||
"db2",
|
"db2",
|
||||||
"diagnostics",
|
"diagnostics",
|
||||||
@ -11332,7 +11212,7 @@ dependencies = [
|
|||||||
"isahc",
|
"isahc",
|
||||||
"journal2",
|
"journal2",
|
||||||
"language2",
|
"language2",
|
||||||
"language_selector2",
|
"language_selector",
|
||||||
"language_tools2",
|
"language_tools2",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
@ -11346,11 +11226,11 @@ dependencies = [
|
|||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"postage",
|
"postage",
|
||||||
"project2",
|
"project2",
|
||||||
"project_panel2",
|
"project_panel",
|
||||||
"project_symbols",
|
"project_symbols",
|
||||||
"quick_action_bar",
|
"quick_action_bar",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"recent_projects2",
|
"recent_projects",
|
||||||
"regex",
|
"regex",
|
||||||
"rope2",
|
"rope2",
|
||||||
"rpc2",
|
"rpc2",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/activity_indicator",
|
"crates/activity_indicator",
|
||||||
"crates/activity_indicator2",
|
|
||||||
"crates/ai",
|
"crates/ai",
|
||||||
"crates/assistant",
|
"crates/assistant",
|
||||||
"crates/assistant2",
|
"crates/assistant2",
|
||||||
@ -10,7 +9,6 @@ members = [
|
|||||||
"crates/auto_update",
|
"crates/auto_update",
|
||||||
"crates/auto_update2",
|
"crates/auto_update2",
|
||||||
"crates/breadcrumbs",
|
"crates/breadcrumbs",
|
||||||
"crates/breadcrumbs2",
|
|
||||||
"crates/call",
|
"crates/call",
|
||||||
"crates/call2",
|
"crates/call2",
|
||||||
"crates/channel",
|
"crates/channel",
|
||||||
@ -58,7 +56,6 @@ members = [
|
|||||||
"crates/language",
|
"crates/language",
|
||||||
"crates/language2",
|
"crates/language2",
|
||||||
"crates/language_selector",
|
"crates/language_selector",
|
||||||
"crates/language_selector2",
|
|
||||||
"crates/language_tools",
|
"crates/language_tools",
|
||||||
"crates/language_tools2",
|
"crates/language_tools2",
|
||||||
"crates/live_kit_client",
|
"crates/live_kit_client",
|
||||||
@ -84,11 +81,9 @@ members = [
|
|||||||
"crates/project",
|
"crates/project",
|
||||||
"crates/project2",
|
"crates/project2",
|
||||||
"crates/project_panel",
|
"crates/project_panel",
|
||||||
"crates/project_panel2",
|
|
||||||
"crates/project_symbols",
|
"crates/project_symbols",
|
||||||
"crates/quick_action_bar",
|
"crates/quick_action_bar",
|
||||||
"crates/recent_projects",
|
"crates/recent_projects",
|
||||||
"crates/recent_projects2",
|
|
||||||
"crates/rope",
|
"crates/rope",
|
||||||
"crates/rpc",
|
"crates/rpc",
|
||||||
"crates/rpc2",
|
"crates/rpc2",
|
||||||
|
@ -9,18 +9,20 @@ path = "src/activity_indicator.rs"
|
|||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
auto_update = { path = "../auto_update" }
|
auto_update = { path = "../auto_update2", package = "auto_update2" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor2", package = "editor2" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language2", package = "language2" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui2", package = "gpui2" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project2", package = "project2" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings2", package = "settings2" }
|
||||||
|
ui = { path = "../ui2", package = "ui2" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme2", package = "theme2" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace2", package = "workspace2" }
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor2", package = "editor2", features = ["test-support"] }
|
||||||
|
@ -2,19 +2,19 @@ use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
|
|||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, anyhow,
|
actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model,
|
||||||
elements::*,
|
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View,
|
||||||
platform::{CursorStyle, MouseButton},
|
ViewContext, VisualContext as _,
|
||||||
AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle,
|
|
||||||
};
|
};
|
||||||
use language::{LanguageRegistry, LanguageServerBinaryStatus};
|
use language::{LanguageRegistry, LanguageServerBinaryStatus};
|
||||||
use project::{LanguageServerProgress, Project};
|
use project::{LanguageServerProgress, Project};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{cmp::Reverse, fmt::Write, sync::Arc};
|
use std::{cmp::Reverse, fmt::Write, sync::Arc};
|
||||||
|
use ui::prelude::*;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||||
|
|
||||||
actions!(lsp_status, [ShowErrorMessage]);
|
actions!(activity_indicator, [ShowErrorMessage]);
|
||||||
|
|
||||||
const DOWNLOAD_ICON: &str = "icons/download.svg";
|
const DOWNLOAD_ICON: &str = "icons/download.svg";
|
||||||
const WARNING_ICON: &str = "icons/warning.svg";
|
const WARNING_ICON: &str = "icons/warning.svg";
|
||||||
@ -25,8 +25,8 @@ pub enum Event {
|
|||||||
|
|
||||||
pub struct ActivityIndicator {
|
pub struct ActivityIndicator {
|
||||||
statuses: Vec<LspStatus>,
|
statuses: Vec<LspStatus>,
|
||||||
project: ModelHandle<Project>,
|
project: Model<Project>,
|
||||||
auto_updater: Option<ModelHandle<AutoUpdater>>,
|
auto_updater: Option<Model<AutoUpdater>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LspStatus {
|
struct LspStatus {
|
||||||
@ -47,20 +47,15 @@ struct Content {
|
|||||||
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
|
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 {
|
impl ActivityIndicator {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
languages: Arc<LanguageRegistry>,
|
languages: Arc<LanguageRegistry>,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) -> ViewHandle<ActivityIndicator> {
|
) -> View<ActivityIndicator> {
|
||||||
let project = workspace.project().clone();
|
let project = workspace.project().clone();
|
||||||
let auto_updater = AutoUpdater::get(cx);
|
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();
|
let mut status_events = languages.language_server_binary_statuses();
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
while let Some((language, event)) = status_events.next().await {
|
while let Some((language, event)) = status_events.next().await {
|
||||||
@ -77,11 +72,13 @@ impl ActivityIndicator {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||||
|
|
||||||
if let Some(auto_updater) = auto_updater.as_ref() {
|
if let Some(auto_updater) = auto_updater.as_ref() {
|
||||||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
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 {
|
Self {
|
||||||
statuses: Default::default(),
|
statuses: Default::default(),
|
||||||
@ -89,6 +86,7 @@ impl ActivityIndicator {
|
|||||||
auto_updater,
|
auto_updater,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.subscribe(&this, move |workspace, _, event, cx| match event {
|
cx.subscribe(&this, move |workspace, _, event, cx| match event {
|
||||||
Event::ShowError { lsp_name, error } => {
|
Event::ShowError { lsp_name, error } => {
|
||||||
if let Some(buffer) = project
|
if let Some(buffer) = project
|
||||||
@ -104,7 +102,7 @@ impl ActivityIndicator {
|
|||||||
});
|
});
|
||||||
workspace.add_item(
|
workspace.add_item(
|
||||||
Box::new(
|
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,
|
cx,
|
||||||
);
|
);
|
||||||
@ -290,71 +288,41 @@ impl ActivityIndicator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
|
// todo!(show active tasks)
|
||||||
return Content {
|
// if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
|
||||||
icon: None,
|
// return Content {
|
||||||
message: most_recent_active_task.to_string(),
|
// icon: None,
|
||||||
on_click: None,
|
// message: most_recent_active_task.to_string(),
|
||||||
};
|
// on_click: None,
|
||||||
}
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for ActivityIndicator {
|
impl EventEmitter<Event> for ActivityIndicator {}
|
||||||
type Event = Event;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for ActivityIndicator {
|
impl Render for ActivityIndicator {
|
||||||
fn ui_name() -> &'static str {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
"ActivityIndicator"
|
let content = self.content_to_render(cx);
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
let mut result = h_stack()
|
||||||
let Content {
|
.id("activity-indicator")
|
||||||
icon,
|
.on_action(cx.listener(Self::show_error_message))
|
||||||
message,
|
.on_action(cx.listener(Self::dismiss_error_message));
|
||||||
on_click,
|
|
||||||
} = self.content_to_render(cx);
|
|
||||||
|
|
||||||
let mut element = MouseEventHandler::new::<Self, _>(0, cx, |state, cx| {
|
if let Some(on_click) = content.on_click {
|
||||||
let theme = &theme::current(cx).workspace.status_bar.lsp_status;
|
result = result
|
||||||
let style = if state.hovered() && on_click.is_some() {
|
.cursor(CursorStyle::PointingHand)
|
||||||
theme.hovered.as_ref().unwrap_or(&theme.default)
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
} else {
|
on_click(this, cx);
|
||||||
&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")
|
|
||||||
}))
|
}))
|
||||||
.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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"] }
|
|
@ -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>) {}
|
|
||||||
}
|
|
@ -10,18 +10,19 @@ doctest = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
editor = { path = "../editor" }
|
editor = { package = "editor2", path = "../editor2" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { package = "gpui2", path = "../gpui2" }
|
||||||
language = { path = "../language" }
|
ui = { package = "ui2", path = "../ui2" }
|
||||||
project = { path = "../project" }
|
language = { package = "language2", path = "../language2" }
|
||||||
search = { path = "../search" }
|
project = { package = "project2", path = "../project2" }
|
||||||
settings = { path = "../settings" }
|
search = { package = "search2", path = "../search2" }
|
||||||
theme = { path = "../theme" }
|
settings = { package = "settings2", path = "../settings2" }
|
||||||
workspace = { path = "../workspace" }
|
theme = { package = "theme2", path = "../theme2" }
|
||||||
outline = { path = "../outline" }
|
workspace = { package = "workspace2", path = "../workspace2" }
|
||||||
|
outline = { package = "outline2", path = "../outline2" }
|
||||||
itertools = "0.10"
|
itertools = "0.10"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||||
|
@ -1,108 +1,74 @@
|
|||||||
|
use editor::Editor;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*, platform::MouseButton, AppContext, Entity, Subscription, View, ViewContext,
|
Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
|
||||||
ViewHandle, WeakViewHandle,
|
ViewContext,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use search::ProjectSearchView;
|
use theme::ActiveTheme;
|
||||||
|
use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{ItemEvent, ItemHandle},
|
item::{ItemEvent, ItemHandle},
|
||||||
ToolbarItemLocation, ToolbarItemView, Workspace,
|
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum Event {
|
|
||||||
UpdateLocation,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Breadcrumbs {
|
pub struct Breadcrumbs {
|
||||||
pane_focused: bool,
|
pane_focused: bool,
|
||||||
active_item: Option<Box<dyn ItemHandle>>,
|
active_item: Option<Box<dyn ItemHandle>>,
|
||||||
project_search: Option<ViewHandle<ProjectSearchView>>,
|
|
||||||
subscription: Option<Subscription>,
|
subscription: Option<Subscription>,
|
||||||
workspace: WeakViewHandle<Workspace>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Breadcrumbs {
|
impl Breadcrumbs {
|
||||||
pub fn new(workspace: &Workspace) -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
pane_focused: false,
|
pane_focused: false,
|
||||||
active_item: Default::default(),
|
active_item: Default::default(),
|
||||||
subscription: Default::default(),
|
subscription: Default::default(),
|
||||||
project_search: Default::default(),
|
|
||||||
workspace: workspace.weak_handle(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for Breadcrumbs {
|
impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
|
||||||
type Event = Event;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for Breadcrumbs {
|
impl Render for Breadcrumbs {
|
||||||
fn ui_name() -> &'static str {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
"Breadcrumbs"
|
let element = h_stack().text_ui();
|
||||||
}
|
let Some(active_item) = self.active_item.as_ref() else {
|
||||||
|
return element;
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
};
|
||||||
let active_item = match &self.active_item {
|
let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else {
|
||||||
Some(active_item) => active_item,
|
return element;
|
||||||
None => return Empty::new().into_any(),
|
|
||||||
};
|
};
|
||||||
let not_editor = active_item.downcast::<editor::Editor>().is_none();
|
|
||||||
|
|
||||||
let theme = theme::current(cx).clone();
|
let highlighted_segments = segments.into_iter().map(|segment| {
|
||||||
let style = &theme.workspace.toolbar.breadcrumbs;
|
let mut text_style = cx.text_style();
|
||||||
|
text_style.color = Color::Muted.color(cx);
|
||||||
|
|
||||||
let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
|
StyledText::new(segment.text)
|
||||||
Some(breadcrumbs) => breadcrumbs,
|
.with_highlights(&text_style, segment.highlights.unwrap_or_default())
|
||||||
None => return Empty::new().into_any(),
|
.into_any()
|
||||||
}
|
});
|
||||||
.into_iter()
|
let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
|
||||||
.map(|breadcrumb| {
|
Label::new("›").color(Color::Muted).into_any_element()
|
||||||
Text::new(
|
|
||||||
breadcrumb.text,
|
|
||||||
theme.workspace.toolbar.breadcrumbs.default.text.clone(),
|
|
||||||
)
|
|
||||||
.with_highlights(breadcrumb.highlights.unwrap_or_default())
|
|
||||||
.into_any()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let crumbs = Flex::row()
|
let breadcrumbs_stack = h_stack().gap_1().children(breadcrumbs);
|
||||||
.with_children(Itertools::intersperse_with(breadcrumbs, || {
|
match active_item
|
||||||
Label::new(" › ", style.default.text.clone()).into_any()
|
.downcast::<Editor>()
|
||||||
}))
|
.map(|editor| editor.downgrade())
|
||||||
.constrained()
|
{
|
||||||
.with_height(theme.workspace.toolbar.breadcrumb_height)
|
Some(editor) => element.child(
|
||||||
.contained();
|
ButtonLike::new("toggle outline view")
|
||||||
|
.child(breadcrumbs_stack)
|
||||||
if not_editor || !self.pane_focused {
|
.style(ButtonStyle::Subtle)
|
||||||
return crumbs
|
.on_click(move |_, cx| {
|
||||||
.with_style(style.default.container)
|
if let Some(editor) = editor.upgrade() {
|
||||||
.aligned()
|
outline::toggle(editor, &outline::Toggle, cx)
|
||||||
.left()
|
}
|
||||||
.into_any();
|
})
|
||||||
|
.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 {
|
) -> ToolbarItemLocation {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
self.active_item = None;
|
self.active_item = None;
|
||||||
self.project_search = None;
|
|
||||||
if let Some(item) = active_pane_item {
|
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(
|
self.subscription = Some(item.subscribe_to_item_events(
|
||||||
cx,
|
cx,
|
||||||
Box::new(move |event, cx| {
|
Box::new(move |event, cx| {
|
||||||
if let Some(this) = this.upgrade(cx) {
|
if let ItemEvent::UpdateBreadcrumbs = event {
|
||||||
if let ItemEvent::UpdateBreadcrumbs = event {
|
this.update(cx, |this, cx| {
|
||||||
this.update(cx, |_, cx| {
|
cx.notify();
|
||||||
cx.emit(Event::UpdateLocation);
|
if let Some(active_item) = this.active_item.as_ref() {
|
||||||
cx.notify();
|
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>) {
|
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
|
||||||
self.pane_focused = pane_focused;
|
self.pane_focused = pane_focused;
|
||||||
}
|
}
|
||||||
|
@ -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"] }
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -41,7 +41,7 @@ notifications = { package = "notifications2", path = "../notifications2" }
|
|||||||
rich_text = { package = "rich_text2", path = "../rich_text2" }
|
rich_text = { package = "rich_text2", path = "../rich_text2" }
|
||||||
picker = { package = "picker2", path = "../picker2" }
|
picker = { package = "picker2", path = "../picker2" }
|
||||||
project = { package = "project2", path = "../project2" }
|
project = { package = "project2", path = "../project2" }
|
||||||
recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
|
recent_projects = { path = "../recent_projects" }
|
||||||
rpc = { package ="rpc2", path = "../rpc2" }
|
rpc = { package ="rpc2", path = "../rpc2" }
|
||||||
settings = { package = "settings2", path = "../settings2" }
|
settings = { package = "settings2", path = "../settings2" }
|
||||||
feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
|
feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
|
||||||
|
@ -9,19 +9,19 @@ path = "src/copilot_button.rs"
|
|||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
copilot = { path = "../copilot" }
|
copilot = { package = "copilot2", path = "../copilot2" }
|
||||||
editor = { path = "../editor" }
|
editor = { package = "editor2", path = "../editor2" }
|
||||||
fs = { path = "../fs" }
|
fs = { package = "fs2", path = "../fs2" }
|
||||||
context_menu = { path = "../context_menu" }
|
zed-actions = { package="zed_actions2", path = "../zed_actions2"}
|
||||||
gpui = { path = "../gpui" }
|
gpui = { package = "gpui2", path = "../gpui2" }
|
||||||
language = { path = "../language" }
|
language = { package = "language2", path = "../language2" }
|
||||||
settings = { path = "../settings" }
|
settings = { package = "settings2", path = "../settings2" }
|
||||||
theme = { path = "../theme" }
|
theme = { package = "theme2", path = "../theme2" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { package = "workspace2", path = "../workspace2" }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||||
|
@ -1,32 +1,31 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use context_menu::{ContextMenu, ContextMenuItem};
|
|
||||||
use copilot::{Copilot, SignOut, Status};
|
use copilot::{Copilot, SignOut, Status};
|
||||||
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*,
|
div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement,
|
||||||
platform::{CursorStyle, MouseButton},
|
Render, Subscription, View, ViewContext, WeakView, WindowContext,
|
||||||
AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
|
|
||||||
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
|
||||||
};
|
};
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{self, all_language_settings, AllLanguageSettings},
|
language_settings::{self, all_language_settings, AllLanguageSettings},
|
||||||
File, Language,
|
File, Language,
|
||||||
};
|
};
|
||||||
use settings::{update_settings_file, SettingsStore};
|
use settings::{update_settings_file, Settings, SettingsStore};
|
||||||
use std::{path::Path, sync::Arc};
|
use std::{path::Path, sync::Arc};
|
||||||
use util::{paths, ResultExt};
|
use util::{paths, ResultExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
create_and_open_local_file, item::ItemHandle,
|
create_and_open_local_file,
|
||||||
notifications::simple_message_notification::OsOpen, StatusItemView, Toast, Workspace,
|
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_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||||
const COPILOT_STARTING_TOAST_ID: usize = 1337;
|
const COPILOT_STARTING_TOAST_ID: usize = 1337;
|
||||||
const COPILOT_ERROR_TOAST_ID: usize = 1338;
|
const COPILOT_ERROR_TOAST_ID: usize = 1338;
|
||||||
|
|
||||||
pub struct CopilotButton {
|
pub struct CopilotButton {
|
||||||
popup_menu: ViewHandle<ContextMenu>,
|
|
||||||
editor_subscription: Option<(Subscription, usize)>,
|
editor_subscription: Option<(Subscription, usize)>,
|
||||||
editor_enabled: Option<bool>,
|
editor_enabled: Option<bool>,
|
||||||
language: Option<Arc<Language>>,
|
language: Option<Arc<Language>>,
|
||||||
@ -34,25 +33,15 @@ pub struct CopilotButton {
|
|||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for CopilotButton {
|
impl Render for CopilotButton {
|
||||||
type Event = ();
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
}
|
|
||||||
|
|
||||||
impl View for CopilotButton {
|
|
||||||
fn ui_name() -> &'static str {
|
|
||||||
"CopilotButton"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
|
||||||
let all_language_settings = all_language_settings(None, cx);
|
let all_language_settings = all_language_settings(None, cx);
|
||||||
if !all_language_settings.copilot.feature_enabled {
|
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 {
|
let Some(copilot) = Copilot::global(cx) else {
|
||||||
return Empty::new().into_any();
|
return div();
|
||||||
};
|
};
|
||||||
let status = copilot.read(cx).status();
|
let status = copilot.read(cx).status();
|
||||||
|
|
||||||
@ -60,59 +49,26 @@ impl View for CopilotButton {
|
|||||||
.editor_enabled
|
.editor_enabled
|
||||||
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
|
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
|
||||||
|
|
||||||
Stack::new()
|
let icon = match status {
|
||||||
.with_child(
|
Status::Error(_) => Icon::CopilotError,
|
||||||
MouseEventHandler::new::<Self, _>(0, cx, {
|
Status::Authorized => {
|
||||||
let theme = theme.clone();
|
if enabled {
|
||||||
let status = status.clone();
|
Icon::Copilot
|
||||||
move |state, _cx| {
|
} else {
|
||||||
let style = theme
|
Icon::CopilotDisabled
|
||||||
.workspace
|
}
|
||||||
.status_bar
|
}
|
||||||
.panel_buttons
|
_ => Icon::CopilotInit,
|
||||||
.button
|
};
|
||||||
.in_state(active)
|
|
||||||
.style_for(state);
|
|
||||||
|
|
||||||
Flex::row()
|
if let Status::Error(e) = status {
|
||||||
.with_child(
|
return div().child(
|
||||||
Svg::new({
|
IconButton::new("copilot-error", icon)
|
||||||
match status {
|
.icon_size(IconSize::Small)
|
||||||
Status::Error(_) => "icons/copilot_error.svg",
|
.on_click(cx.listener(move |_, _, cx| {
|
||||||
Status::Authorized => {
|
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||||
if enabled {
|
workspace
|
||||||
"icons/copilot.svg"
|
.update(cx, |workspace, cx| {
|
||||||
} 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| {
|
|
||||||
workspace.show_toast(
|
workspace.show_toast(
|
||||||
Toast::new(
|
Toast::new(
|
||||||
COPILOT_ERROR_TOAST_ID,
|
COPILOT_ERROR_TOAST_ID,
|
||||||
@ -132,43 +88,40 @@ impl View for CopilotButton {
|
|||||||
),
|
),
|
||||||
cx,
|
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>(
|
.anchor(AnchorCorner::BottomRight)
|
||||||
0,
|
.trigger(
|
||||||
"GitHub Copilot",
|
IconButton::new("copilot-icon", icon)
|
||||||
None,
|
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
|
||||||
theme.tooltip.clone(),
|
|
||||||
cx,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.with_child(ChildView::new(&self.popup_menu, cx).aligned().top().right())
|
|
||||||
.into_any()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CopilotButton {
|
impl CopilotButton {
|
||||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
|
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());
|
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();
|
.detach();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
popup_menu: menu,
|
|
||||||
editor_subscription: None,
|
editor_subscription: None,
|
||||||
editor_enabled: None,
|
editor_enabled: None,
|
||||||
language: None,
|
language: None,
|
||||||
@ -177,108 +130,91 @@ impl CopilotButton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||||
let mut menu_options = Vec::with_capacity(2);
|
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
|
ContextMenu::build(cx, |menu, _| {
|
||||||
menu_options.push(ContextMenuItem::handler("Sign In", |cx| {
|
menu.entry("Sign In", None, initiate_sign_in).entry(
|
||||||
initiate_sign_in(cx)
|
"Disable Copilot",
|
||||||
}));
|
None,
|
||||||
menu_options.push(ContextMenuItem::handler("Disable Copilot", move |cx| {
|
move |cx| hide_copilot(fs.clone(), cx),
|
||||||
hide_copilot(fs.clone(), cx)
|
)
|
||||||
}));
|
})
|
||||||
|
|
||||||
self.popup_menu.update(cx, |menu, cx| {
|
|
||||||
menu.toggle(
|
|
||||||
Default::default(),
|
|
||||||
AnchorCorner::BottomRight,
|
|
||||||
menu_options,
|
|
||||||
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 fs = self.fs.clone();
|
||||||
let mut menu_options = Vec::with_capacity(8);
|
|
||||||
|
|
||||||
if let Some(language) = self.language.clone() {
|
return ContextMenu::build(cx, move |mut menu, cx| {
|
||||||
let fs = fs.clone();
|
if let Some(language) = self.language.clone() {
|
||||||
let language_enabled = language_settings::language_settings(Some(&language), None, cx)
|
let fs = fs.clone();
|
||||||
.show_copilot_suggestions;
|
let language_enabled =
|
||||||
menu_options.push(ContextMenuItem::handler(
|
language_settings::language_settings(Some(&language), None, cx)
|
||||||
format!(
|
.show_copilot_suggestions;
|
||||||
"{} Suggestions for {}",
|
|
||||||
if language_enabled { "Hide" } else { "Show" },
|
|
||||||
language.name()
|
|
||||||
),
|
|
||||||
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 settings = AllLanguageSettings::get_global(cx);
|
||||||
let path = file.path().clone();
|
|
||||||
let path_enabled = settings.copilot_enabled_for_path(&path);
|
if let Some(file) = &self.file {
|
||||||
menu_options.push(ContextMenuItem::handler(
|
let path = file.path().clone();
|
||||||
format!(
|
let path_enabled = settings.copilot_enabled_for_path(&path);
|
||||||
"{} Suggestions for This Path",
|
|
||||||
if path_enabled { "Hide" } else { "Show" }
|
menu = menu.entry(
|
||||||
),
|
format!(
|
||||||
move |cx| {
|
"{} Suggestions for This Path",
|
||||||
if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() {
|
if path_enabled { "Hide" } else { "Show" }
|
||||||
let workspace = workspace.downgrade();
|
),
|
||||||
cx.spawn(|_, cx| {
|
None,
|
||||||
configure_disabled_globs(
|
move |cx| {
|
||||||
workspace,
|
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||||
path_enabled.then_some(path.clone()),
|
if let Ok(workspace) = workspace.root_view(cx) {
|
||||||
cx,
|
let workspace = workspace.downgrade();
|
||||||
)
|
cx.spawn(|cx| {
|
||||||
})
|
configure_disabled_globs(
|
||||||
.detach_and_log_err(cx);
|
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),
|
||||||
|
)
|
||||||
let globally_enabled = settings.copilot_enabled(None, None);
|
.separator()
|
||||||
menu_options.push(ContextMenuItem::handler(
|
.link(
|
||||||
if globally_enabled {
|
"Copilot Settings",
|
||||||
"Hide Suggestions for All Files"
|
OpenBrowser {
|
||||||
} else {
|
url: COPILOT_SETTINGS_URL.to_string(),
|
||||||
"Show Suggestions for All Files"
|
}
|
||||||
},
|
.boxed_clone(),
|
||||||
move |cx| toggle_copilot_globally(fs.clone(), cx),
|
)
|
||||||
));
|
.action("Sign Out", SignOut.boxed_clone())
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 editor = editor.read(cx);
|
||||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
let suggestion_anchor = editor.selections.newest_anchor().start;
|
let suggestion_anchor = editor.selections.newest_anchor().start;
|
||||||
@ -299,8 +235,10 @@ impl CopilotButton {
|
|||||||
impl StatusItemView for CopilotButton {
|
impl StatusItemView for CopilotButton {
|
||||||
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
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() {
|
if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
|
||||||
self.editor_subscription =
|
self.editor_subscription = Some((
|
||||||
Some((cx.observe(&editor, Self::update_enabled), editor.id()));
|
cx.observe(&editor, Self::update_enabled),
|
||||||
|
editor.entity_id().as_u64() as usize,
|
||||||
|
));
|
||||||
self.update_enabled(editor, cx);
|
self.update_enabled(editor, cx);
|
||||||
} else {
|
} else {
|
||||||
self.language = None;
|
self.language = None;
|
||||||
@ -312,9 +250,9 @@ impl StatusItemView for CopilotButton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn configure_disabled_globs(
|
async fn configure_disabled_globs(
|
||||||
workspace: WeakViewHandle<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
path_to_disable: Option<Arc<Path>>,
|
path_to_disable: Option<Arc<Path>>,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncWindowContext,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let settings_editor = workspace
|
let settings_editor = workspace
|
||||||
.update(&mut cx, |_, cx| {
|
.update(&mut cx, |_, cx| {
|
||||||
@ -396,20 +334,23 @@ fn initiate_sign_in(cx: &mut WindowContext) {
|
|||||||
|
|
||||||
match status {
|
match status {
|
||||||
Status::Starting { task } => {
|
Status::Starting { task } => {
|
||||||
let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() else {
|
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
workspace.update(cx, |workspace, cx| {
|
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
|
||||||
workspace.show_toast(
|
workspace.show_toast(
|
||||||
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
|
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
|
||||||
cx,
|
cx,
|
||||||
)
|
);
|
||||||
});
|
workspace.weak_handle()
|
||||||
let workspace = workspace.downgrade();
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
task.await;
|
task.await;
|
||||||
if let Some(copilot) = cx.read(Copilot::global) {
|
if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() {
|
||||||
workspace
|
workspace
|
||||||
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
|
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
|
||||||
Status::Authorized => workspace.show_toast(
|
Status::Authorized => workspace.show_toast(
|
||||||
|
@ -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"] }
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,17 +9,18 @@ path = "src/language_selector.rs"
|
|||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
editor = { path = "../editor" }
|
editor = { package = "editor2", path = "../editor2" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||||
language = { path = "../language" }
|
language = { package = "language2", path = "../language2" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { package = "gpui2", path = "../gpui2" }
|
||||||
picker = { path = "../picker" }
|
picker = { package = "picker2", path = "../picker2" }
|
||||||
project = { path = "../project" }
|
project = { package = "project2", path = "../project2" }
|
||||||
theme = { path = "../theme" }
|
theme = { package = "theme2", path = "../theme2" }
|
||||||
settings = { path = "../settings" }
|
ui = { package = "ui2", path = "../ui2" }
|
||||||
|
settings = { package = "settings2", path = "../settings2" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { package = "workspace2", path = "../workspace2" }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::{
|
use gpui::{div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView};
|
||||||
elements::*,
|
|
||||||
platform::{CursorStyle, MouseButton},
|
|
||||||
Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
|
||||||
};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use ui::{Button, ButtonCommon, Clickable, LabelSize, Tooltip};
|
||||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||||
|
|
||||||
|
use crate::LanguageSelector;
|
||||||
|
|
||||||
pub struct ActiveBufferLanguage {
|
pub struct ActiveBufferLanguage {
|
||||||
active_language: Option<Option<Arc<str>>>,
|
active_language: Option<Option<Arc<str>>>,
|
||||||
workspace: WeakViewHandle<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_observe_active_editor: Option<Subscription>,
|
_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);
|
self.active_language = Some(None);
|
||||||
|
|
||||||
let editor = editor.read(cx);
|
let editor = editor.read(cx);
|
||||||
@ -36,44 +35,28 @@ impl ActiveBufferLanguage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for ActiveBufferLanguage {
|
impl Render for ActiveBufferLanguage {
|
||||||
type Event = ();
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
}
|
div().when_some(self.active_language.as_ref(), |el, active_language| {
|
||||||
|
|
||||||
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() {
|
|
||||||
let active_language_text = if let Some(active_language_text) = active_language {
|
let active_language_text = if let Some(active_language_text) = active_language {
|
||||||
active_language_text.to_string()
|
active_language_text.to_string()
|
||||||
} else {
|
} else {
|
||||||
"Unknown".to_string()
|
"Unknown".to_string()
|
||||||
};
|
};
|
||||||
let theme = theme::current(cx).clone();
|
|
||||||
|
|
||||||
MouseEventHandler::new::<Self, _>(0, cx, |state, cx| {
|
el.child(
|
||||||
let theme = &theme::current(cx).workspace.status_bar;
|
Button::new("change-language", active_language_text)
|
||||||
let style = theme.active_language.style_for(state);
|
.label_size(LabelSize::Small)
|
||||||
Label::new(active_language_text, style.text.clone())
|
.on_click(cx.listener(|this, _, cx| {
|
||||||
.contained()
|
if let Some(workspace) = this.workspace.upgrade() {
|
||||||
.with_style(style.container)
|
workspace.update(cx, |workspace, cx| {
|
||||||
})
|
LanguageSelector::toggle(workspace, cx)
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
});
|
||||||
.on_click(MouseButton::Left, |_, this, cx| {
|
}
|
||||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
}))
|
||||||
workspace.update(cx, |workspace, cx| {
|
.tooltip(|cx| Tooltip::text("Select Language", 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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,46 +4,87 @@ pub use active_buffer_language::ActiveBufferLanguage;
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
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 language::{Buffer, LanguageRegistry};
|
||||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::Workspace;
|
use workspace::{ModalView, Workspace};
|
||||||
|
|
||||||
actions!(language_selector, [Toggle]);
|
actions!(language_selector, [Toggle]);
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
Picker::<LanguageSelectorDelegate>::init(cx);
|
cx.observe_new_views(LanguageSelector::register).detach();
|
||||||
cx.add_action(toggle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle(
|
pub struct LanguageSelector {
|
||||||
workspace: &mut Workspace,
|
picker: View<Picker<LanguageSelectorDelegate>>,
|
||||||
_: &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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
pub struct LanguageSelectorDelegate {
|
||||||
buffer: ModelHandle<Buffer>,
|
language_selector: WeakView<LanguageSelector>,
|
||||||
project: ModelHandle<Project>,
|
buffer: Model<Buffer>,
|
||||||
|
project: Model<Project>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
candidates: Vec<StringMatchCandidate>,
|
candidates: Vec<StringMatchCandidate>,
|
||||||
matches: Vec<StringMatch>,
|
matches: Vec<StringMatch>,
|
||||||
@ -52,8 +93,9 @@ pub struct LanguageSelectorDelegate {
|
|||||||
|
|
||||||
impl LanguageSelectorDelegate {
|
impl LanguageSelectorDelegate {
|
||||||
fn new(
|
fn new(
|
||||||
buffer: ModelHandle<Buffer>,
|
language_selector: WeakView<LanguageSelector>,
|
||||||
project: ModelHandle<Project>,
|
buffer: Model<Buffer>,
|
||||||
|
project: Model<Project>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let candidates = language_registry
|
let candidates = language_registry
|
||||||
@ -62,29 +104,22 @@ impl LanguageSelectorDelegate {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
|
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
|
||||||
.collect::<Vec<_>>();
|
.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 {
|
Self {
|
||||||
|
language_selector,
|
||||||
buffer,
|
buffer,
|
||||||
project,
|
project,
|
||||||
language_registry,
|
language_registry,
|
||||||
candidates,
|
candidates,
|
||||||
matches,
|
matches: vec![],
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PickerDelegate for LanguageSelectorDelegate {
|
impl PickerDelegate for LanguageSelectorDelegate {
|
||||||
|
type ListItem = ListItem;
|
||||||
|
|
||||||
fn placeholder_text(&self) -> Arc<str> {
|
fn placeholder_text(&self) -> Arc<str> {
|
||||||
"Select a language...".into()
|
"Select a language...".into()
|
||||||
}
|
}
|
||||||
@ -102,23 +137,25 @@ impl PickerDelegate for LanguageSelectorDelegate {
|
|||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
let language = language.await?;
|
let language = language.await?;
|
||||||
let project = project
|
let project = project
|
||||||
.upgrade(&cx)
|
.upgrade()
|
||||||
.ok_or_else(|| anyhow!("project was dropped"))?;
|
.ok_or_else(|| anyhow!("project was dropped"))?;
|
||||||
let buffer = buffer
|
let buffer = buffer
|
||||||
.upgrade(&cx)
|
.upgrade()
|
||||||
.ok_or_else(|| anyhow!("buffer was dropped"))?;
|
.ok_or_else(|| anyhow!("buffer was dropped"))?;
|
||||||
project.update(&mut cx, |project, cx| {
|
project.update(&mut cx, |project, cx| {
|
||||||
project.set_language_for_buffer(&buffer, language, cx);
|
project.set_language_for_buffer(&buffer, language, cx);
|
||||||
});
|
})
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
self.dismissed(cx);
|
||||||
cx.emit(PickerEvent::Dismiss);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
fn selected_index(&self) -> usize {
|
||||||
self.selected_index
|
self.selected_index
|
||||||
@ -133,7 +170,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
|
|||||||
query: String,
|
query: String,
|
||||||
cx: &mut ViewContext<Picker<Self>>,
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> gpui::Task<()> {
|
) -> gpui::Task<()> {
|
||||||
let background = cx.background().clone();
|
let background = cx.background_executor().clone();
|
||||||
let candidates = self.candidates.clone();
|
let candidates = self.candidates.clone();
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let matches = if query.is_empty() {
|
let matches = if query.is_empty() {
|
||||||
@ -160,7 +197,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
let delegate = this.delegate_mut();
|
let delegate = &mut this.delegate;
|
||||||
delegate.matches = matches;
|
delegate.matches = matches;
|
||||||
delegate.selected_index = delegate
|
delegate.selected_index = delegate
|
||||||
.selected_index
|
.selected_index
|
||||||
@ -174,23 +211,22 @@ impl PickerDelegate for LanguageSelectorDelegate {
|
|||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
mouse_state: &mut MouseState,
|
|
||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &AppContext,
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> AnyElement<Picker<Self>> {
|
) -> Option<Self::ListItem> {
|
||||||
let theme = theme::current(cx);
|
|
||||||
let mat = &self.matches[ix];
|
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 buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
|
||||||
let mut label = mat.string.clone();
|
let mut label = mat.string.clone();
|
||||||
if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
|
if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
|
||||||
label.push_str(" (current)");
|
label.push_str(" (current)");
|
||||||
}
|
}
|
||||||
|
|
||||||
Label::new(label, style.label.clone())
|
Some(
|
||||||
.with_highlights(mat.positions.clone())
|
ListItem::new(ix)
|
||||||
.contained()
|
.inset(true)
|
||||||
.with_style(style.container)
|
.spacing(ListItemSpacing::Sparse)
|
||||||
.into_any()
|
.selected(selected)
|
||||||
|
.child(HighlightedLabel::new(label, mat.positions.clone())),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"] }
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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())),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,32 +9,33 @@ path = "src/project_panel.rs"
|
|||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
context_menu = { path = "../context_menu" }
|
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
db = { path = "../db" }
|
db = { path = "../db2", package = "db2" }
|
||||||
drag_and_drop = { path = "../drag_and_drop" }
|
editor = { path = "../editor2", package = "editor2" }
|
||||||
editor = { path = "../editor" }
|
gpui = { path = "../gpui2", package = "gpui2" }
|
||||||
gpui = { path = "../gpui" }
|
menu = { path = "../menu2", package = "menu2" }
|
||||||
menu = { path = "../menu" }
|
project = { path = "../project2", package = "project2" }
|
||||||
project = { path = "../project" }
|
search = { package = "search2", path = "../search2" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings2", package = "settings2" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme2", package = "theme2" }
|
||||||
|
ui = { path = "../ui2", package = "ui2" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace2", package = "workspace2" }
|
||||||
|
anyhow.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_derive.workspace = true
|
serde_derive.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
anyhow.workspace = true
|
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
unicase = "2.6"
|
unicase = "2.6"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
client = { path = "../client", features = ["test-support"] }
|
client = { path = "../client2", package = "client2", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language2", package = "language2", features = ["test-support"] }
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor2", package = "editor2", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
@ -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!({
|
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
|
this.suffixes
|
||||||
// override these associations
|
.get(suffix)
|
||||||
maybe!({
|
.and_then(|type_str| this.types.get(type_str))
|
||||||
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)
|
|
||||||
.map(|type_config| type_config.icon.clone())
|
.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> {
|
pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
|
||||||
maybe!({
|
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
|
||||||
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
|
|
||||||
|
|
||||||
let key = if expanded {
|
let key = if expanded {
|
||||||
EXPANDED_CHEVRON_TYPE
|
EXPANDED_DIRECTORY_TYPE
|
||||||
} else {
|
} else {
|
||||||
COLLAPSED_CHEVRON_TYPE
|
COLLAPSED_DIRECTORY_TYPE
|
||||||
};
|
};
|
||||||
|
|
||||||
this.types
|
this.types
|
||||||
.get(key)
|
.get(key)
|
||||||
.map(|type_config| type_config.icon.clone())
|
.map(|type_config| type_config.icon.clone())
|
||||||
})
|
}
|
||||||
.unwrap_or_else(|| Arc::from("".to_string()))
|
|
||||||
|
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
@ -1,7 +1,8 @@
|
|||||||
use anyhow;
|
use anyhow;
|
||||||
|
use gpui::Pixels;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use settings::Setting;
|
use settings::Settings;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
@ -12,7 +13,7 @@ pub enum ProjectPanelDockPosition {
|
|||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct ProjectPanelSettings {
|
pub struct ProjectPanelSettings {
|
||||||
pub default_width: f32,
|
pub default_width: Pixels,
|
||||||
pub dock: ProjectPanelDockPosition,
|
pub dock: ProjectPanelDockPosition,
|
||||||
pub file_icons: bool,
|
pub file_icons: bool,
|
||||||
pub folder_icons: bool,
|
pub folder_icons: bool,
|
||||||
@ -32,7 +33,7 @@ pub struct ProjectPanelSettingsContent {
|
|||||||
pub auto_reveal_entries: Option<bool>,
|
pub auto_reveal_entries: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Setting for ProjectPanelSettings {
|
impl Settings for ProjectPanelSettings {
|
||||||
const KEY: Option<&'static str> = Some("project_panel");
|
const KEY: Option<&'static str> = Some("project_panel");
|
||||||
|
|
||||||
type FileContent = ProjectPanelSettingsContent;
|
type FileContent = ProjectPanelSettingsContent;
|
||||||
@ -40,7 +41,7 @@ impl Setting for ProjectPanelSettings {
|
|||||||
fn load(
|
fn load(
|
||||||
default_value: &Self::FileContent,
|
default_value: &Self::FileContent,
|
||||||
user_values: &[&Self::FileContent],
|
user_values: &[&Self::FileContent],
|
||||||
_: &gpui::AppContext,
|
_: &mut gpui::AppContext,
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
Self::load_via_json_merge(default_value, user_values)
|
Self::load_via_json_merge(default_value, user_values)
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
@ -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
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,17 +9,17 @@ path = "src/recent_projects.rs"
|
|||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
db = { path = "../db" }
|
editor = { package = "editor2", path = "../editor2" }
|
||||||
editor = { path = "../editor" }
|
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
gpui = { package = "gpui2", path = "../gpui2" }
|
||||||
gpui = { path = "../gpui" }
|
language = { package = "language2", path = "../language2" }
|
||||||
language = { path = "../language" }
|
picker = { package = "picker2", path = "../picker2" }
|
||||||
picker = { path = "../picker" }
|
settings = { package = "settings2", path = "../settings2" }
|
||||||
settings = { path = "../settings" }
|
text = { package = "text2", path = "../text2" }
|
||||||
text = { path = "../text" }
|
|
||||||
util = { path = "../util"}
|
util = { path = "../util"}
|
||||||
theme = { path = "../theme" }
|
theme = { package = "theme2", path = "../theme2" }
|
||||||
workspace = { path = "../workspace" }
|
ui = { package = "ui2", path = "../ui2" }
|
||||||
|
workspace = { package = "workspace2", path = "../workspace2" }
|
||||||
|
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
ordered-float.workspace = true
|
ordered-float.workspace = true
|
||||||
@ -27,4 +27,4 @@ postage.workspace = true
|
|||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use fuzzy::StringMatch;
|
use fuzzy::StringMatch;
|
||||||
use gpui::{
|
use ui::{prelude::*, HighlightedLabel};
|
||||||
elements::{Label, LabelStyle},
|
|
||||||
AnyElement, Element,
|
|
||||||
};
|
|
||||||
use util::paths::PathExt;
|
use util::paths::PathExt;
|
||||||
use workspace::WorkspaceLocation;
|
use workspace::WorkspaceLocation;
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
pub struct HighlightedText {
|
pub struct HighlightedText {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub highlight_positions: Vec<usize>,
|
pub highlight_positions: Vec<usize>,
|
||||||
@ -42,11 +40,11 @@ impl HighlightedText {
|
|||||||
char_count,
|
char_count,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render<V: 'static>(self, style: impl Into<LabelStyle>) -> AnyElement<V> {
|
impl RenderOnce for HighlightedText {
|
||||||
Label::new(self.text, style)
|
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||||
.with_highlights(self.highlight_positions)
|
HighlightedLabel::new(self.text, self.highlight_positions)
|
||||||
.into_any()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,86 +1,122 @@
|
|||||||
mod highlighted_workspace_location;
|
mod highlighted_workspace_location;
|
||||||
|
mod projects;
|
||||||
|
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, Subscription, Task,
|
||||||
anyhow::Result,
|
View, ViewContext, WeakView,
|
||||||
elements::{Flex, ParentElement},
|
|
||||||
AnyElement, AppContext, Element, Task, ViewContext, WeakViewHandle,
|
|
||||||
};
|
};
|
||||||
use highlighted_workspace_location::HighlightedWorkspaceLocation;
|
use highlighted_workspace_location::HighlightedWorkspaceLocation;
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
use picker::{Picker, PickerDelegate};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||||
use util::paths::PathExt;
|
use util::paths::PathExt;
|
||||||
use workspace::{
|
use workspace::{ModalView, Workspace, WorkspaceLocation, WORKSPACE_DB};
|
||||||
notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
|
|
||||||
WORKSPACE_DB,
|
|
||||||
};
|
|
||||||
|
|
||||||
actions!(projects, [OpenRecent]);
|
pub use projects::OpenRecent;
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_async_action(toggle);
|
cx.observe_new_views(RecentProjects::register).detach();
|
||||||
RecentProjects::init(cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle(
|
pub struct RecentProjects {
|
||||||
_: &mut Workspace,
|
pub picker: View<Picker<RecentProjectsDelegate>>,
|
||||||
_: &OpenRecent,
|
rem_width: f32,
|
||||||
cx: &mut ViewContext<Workspace>,
|
_subscription: Subscription,
|
||||||
) -> 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;
|
|
||||||
|
|
||||||
workspace.update(&mut cx, |workspace, cx| {
|
impl ModalView for RecentProjects {}
|
||||||
if !workspace_locations.is_empty() {
|
|
||||||
workspace.toggle_modal(cx, |_, cx| {
|
impl RecentProjects {
|
||||||
let workspace = cx.weak_handle();
|
fn new(delegate: RecentProjectsDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> Self {
|
||||||
cx.add_view(|cx| {
|
let picker = cx.new_view(|cx| Picker::new(delegate, cx));
|
||||||
RecentProjects::new(
|
let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
|
||||||
RecentProjectsDelegate::new(workspace, workspace_locations, true),
|
// We do not want to block the UI on a potentially lenghty call to DB, so we're gonna swap
|
||||||
cx,
|
// out workspace locations once the future runs to completion.
|
||||||
)
|
cx.spawn(|this, mut cx| async move {
|
||||||
.with_max_size(800., 1200.)
|
let workspaces = WORKSPACE_DB
|
||||||
})
|
.recent_workspaces_on_disk()
|
||||||
});
|
.await
|
||||||
} else {
|
.unwrap_or_default()
|
||||||
workspace.show_notification(0, cx, |cx| {
|
.into_iter()
|
||||||
cx.add_view(|_| MessageNotification::new("No recent projects to open."))
|
.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(
|
impl EventEmitter<DismissEvent> for RecentProjects {}
|
||||||
workspace: WeakViewHandle<Workspace>,
|
|
||||||
workspaces: Vec<WorkspaceLocation>,
|
impl FocusableView for RecentProjects {
|
||||||
cx: &mut ViewContext<RecentProjects>,
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||||
) -> RecentProjects {
|
self.picker.focus_handle(cx)
|
||||||
Picker::new(
|
}
|
||||||
RecentProjectsDelegate::new(workspace, workspaces, false),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.with_theme(|theme| theme.picker.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
pub struct RecentProjectsDelegate {
|
||||||
workspace: WeakViewHandle<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
workspace_locations: Vec<WorkspaceLocation>,
|
workspace_locations: Vec<WorkspaceLocation>,
|
||||||
selected_match_index: usize,
|
selected_match_index: usize,
|
||||||
matches: Vec<StringMatch>,
|
matches: Vec<StringMatch>,
|
||||||
@ -88,22 +124,20 @@ pub struct RecentProjectsDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RecentProjectsDelegate {
|
impl RecentProjectsDelegate {
|
||||||
fn new(
|
fn new(workspace: WeakView<Workspace>, render_paths: bool) -> Self {
|
||||||
workspace: WeakViewHandle<Workspace>,
|
|
||||||
workspace_locations: Vec<WorkspaceLocation>,
|
|
||||||
render_paths: bool,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
workspace,
|
workspace,
|
||||||
workspace_locations,
|
workspace_locations: vec![],
|
||||||
selected_match_index: 0,
|
selected_match_index: 0,
|
||||||
matches: Default::default(),
|
matches: Default::default(),
|
||||||
render_paths,
|
render_paths,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
|
||||||
impl PickerDelegate for RecentProjectsDelegate {
|
impl PickerDelegate for RecentProjectsDelegate {
|
||||||
|
type ListItem = ListItem;
|
||||||
|
|
||||||
fn placeholder_text(&self) -> Arc<str> {
|
fn placeholder_text(&self) -> Arc<str> {
|
||||||
"Recent Projects...".into()
|
"Recent Projects...".into()
|
||||||
}
|
}
|
||||||
@ -116,14 +150,14 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
self.selected_match_index
|
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;
|
self.selected_match_index = ix;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_matches(
|
fn update_matches(
|
||||||
&mut self,
|
&mut self,
|
||||||
query: String,
|
query: String,
|
||||||
cx: &mut ViewContext<RecentProjects>,
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> gpui::Task<()> {
|
) -> gpui::Task<()> {
|
||||||
let query = query.trim_start();
|
let query = query.trim_start();
|
||||||
let smart_case = query.chars().any(|c| c.is_uppercase());
|
let smart_case = query.chars().any(|c| c.is_uppercase());
|
||||||
@ -147,7 +181,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
smart_case,
|
smart_case,
|
||||||
100,
|
100,
|
||||||
&Default::default(),
|
&Default::default(),
|
||||||
cx.background().clone(),
|
cx.background_executor().clone(),
|
||||||
));
|
));
|
||||||
self.matches.sort_unstable_by_key(|m| m.candidate_id);
|
self.matches.sort_unstable_by_key(|m| m.candidate_id);
|
||||||
|
|
||||||
@ -162,11 +196,11 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
Task::ready(())
|
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
|
if let Some((selected_match, workspace)) = self
|
||||||
.matches
|
.matches
|
||||||
.get(self.selected_index())
|
.get(self.selected_index())
|
||||||
.zip(self.workspace.upgrade(cx))
|
.zip(self.workspace.upgrade())
|
||||||
{
|
{
|
||||||
let workspace_location = &self.workspace_locations[selected_match.candidate_id];
|
let workspace_location = &self.workspace_locations[selected_match.candidate_id];
|
||||||
workspace
|
workspace
|
||||||
@ -175,41 +209,39 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
.open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
|
.open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
|
||||||
})
|
})
|
||||||
.detach_and_log_err(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(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
mouse_state: &mut gpui::MouseState,
|
|
||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &gpui::AppContext,
|
_cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> AnyElement<Picker<Self>> {
|
) -> Option<Self::ListItem> {
|
||||||
let theme = theme::current(cx);
|
let Some(r#match) = self.matches.get(ix) else {
|
||||||
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
|
return None;
|
||||||
|
};
|
||||||
let string_match = &self.matches[ix];
|
|
||||||
|
|
||||||
let highlighted_location = HighlightedWorkspaceLocation::new(
|
let highlighted_location = HighlightedWorkspaceLocation::new(
|
||||||
&string_match,
|
&r#match,
|
||||||
&self.workspace_locations[string_match.candidate_id],
|
&self.workspace_locations[r#match.candidate_id],
|
||||||
);
|
);
|
||||||
|
|
||||||
Flex::column()
|
Some(
|
||||||
.with_child(highlighted_location.names.render(style.label.clone()))
|
ListItem::new(ix)
|
||||||
.with_children(
|
.inset(true)
|
||||||
highlighted_location
|
.spacing(ListItemSpacing::Sparse)
|
||||||
.paths
|
.selected(selected)
|
||||||
.into_iter()
|
.child(
|
||||||
.filter(|_| self.render_paths)
|
v_stack()
|
||||||
.map(|highlighted_path| highlighted_path.render(style.label.clone())),
|
.child(highlighted_location.names)
|
||||||
)
|
.when(self.render_paths, |this| {
|
||||||
.flex(1., false)
|
this.children(highlighted_location.paths)
|
||||||
.contained()
|
}),
|
||||||
.with_style(style.container)
|
),
|
||||||
.into_any_named("match")
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"] }
|
|
@ -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,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -17,9 +17,9 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
ai = { package = "ai2", path = "../ai2"}
|
ai = { package = "ai2", path = "../ai2"}
|
||||||
audio = { package = "audio2", path = "../audio2" }
|
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" }
|
auto_update = { package = "auto_update2", path = "../auto_update2" }
|
||||||
breadcrumbs = { package = "breadcrumbs2", path = "../breadcrumbs2" }
|
breadcrumbs = { path = "../breadcrumbs" }
|
||||||
call = { package = "call2", path = "../call2" }
|
call = { package = "call2", path = "../call2" }
|
||||||
channel = { package = "channel2", path = "../channel2" }
|
channel = { package = "channel2", path = "../channel2" }
|
||||||
cli = { path = "../cli" }
|
cli = { path = "../cli" }
|
||||||
@ -30,7 +30,7 @@ command_palette = { path = "../command_palette" }
|
|||||||
client = { package = "client2", path = "../client2" }
|
client = { package = "client2", path = "../client2" }
|
||||||
# clock = { path = "../clock" }
|
# clock = { path = "../clock" }
|
||||||
copilot = { package = "copilot2", path = "../copilot2" }
|
copilot = { package = "copilot2", path = "../copilot2" }
|
||||||
copilot_button = { package = "copilot_button2", path = "../copilot_button2" }
|
copilot_button = { path = "../copilot_button" }
|
||||||
diagnostics = { path = "../diagnostics" }
|
diagnostics = { path = "../diagnostics" }
|
||||||
db = { package = "db2", path = "../db2" }
|
db = { package = "db2", path = "../db2" }
|
||||||
editor = { package="editor2", path = "../editor2" }
|
editor = { package="editor2", path = "../editor2" }
|
||||||
@ -44,7 +44,7 @@ gpui = { package = "gpui2", path = "../gpui2" }
|
|||||||
install_cli = { package = "install_cli2", path = "../install_cli2" }
|
install_cli = { package = "install_cli2", path = "../install_cli2" }
|
||||||
journal = { package = "journal2", path = "../journal2" }
|
journal = { package = "journal2", path = "../journal2" }
|
||||||
language = { package = "language2", path = "../language2" }
|
language = { package = "language2", path = "../language2" }
|
||||||
language_selector = { package = "language_selector2", path = "../language_selector2" }
|
language_selector = { path = "../language_selector" }
|
||||||
lsp = { package = "lsp2", path = "../lsp2" }
|
lsp = { package = "lsp2", path = "../lsp2" }
|
||||||
menu = { package = "menu2", path = "../menu2" }
|
menu = { package = "menu2", path = "../menu2" }
|
||||||
language_tools = { package = "language_tools2", path = "../language_tools2" }
|
language_tools = { package = "language_tools2", path = "../language_tools2" }
|
||||||
@ -54,10 +54,10 @@ assistant = { package = "assistant2", path = "../assistant2" }
|
|||||||
outline = { package = "outline2", path = "../outline2" }
|
outline = { package = "outline2", path = "../outline2" }
|
||||||
# plugin_runtime = { path = "../plugin_runtime",optional = true }
|
# plugin_runtime = { path = "../plugin_runtime",optional = true }
|
||||||
project = { package = "project2", path = "../project2" }
|
project = { package = "project2", path = "../project2" }
|
||||||
project_panel = { package = "project_panel2", path = "../project_panel2" }
|
project_panel = { path = "../project_panel" }
|
||||||
project_symbols = { path = "../project_symbols" }
|
project_symbols = { path = "../project_symbols" }
|
||||||
quick_action_bar = { path = "../quick_action_bar" }
|
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"}
|
rope = { package = "rope2", path = "../rope2"}
|
||||||
rpc = { package = "rpc2", path = "../rpc2" }
|
rpc = { package = "rpc2", path = "../rpc2" }
|
||||||
settings = { package = "settings2", path = "../settings2" }
|
settings = { package = "settings2", path = "../settings2" }
|
||||||
|
Loading…
Reference in New Issue
Block a user