Added erorr states and first-pass error handling to the copilot status bar item.
Added correct icons Added a new 'Toast' action which allows other crates to easily pop toasts with an optional click action
This commit is contained in:
parent
f235d9f411
commit
f5d4bcd934
9 changed files with 279 additions and 73 deletions
Before Width: | Height: | Size: 605 B After Width: | Height: | Size: 605 B |
|
@ -28,7 +28,10 @@ const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
|
||||||
actions!(copilot_auth, [SignIn, SignOut]);
|
actions!(copilot_auth, [SignIn, SignOut]);
|
||||||
|
|
||||||
const COPILOT_NAMESPACE: &'static str = "copilot";
|
const COPILOT_NAMESPACE: &'static str = "copilot";
|
||||||
actions!(copilot, [NextSuggestion, PreviousSuggestion, Toggle]);
|
actions!(
|
||||||
|
copilot,
|
||||||
|
[NextSuggestion, PreviousSuggestion, Toggle, Reinstall]
|
||||||
|
);
|
||||||
|
|
||||||
pub fn init(client: Arc<Client>, node_runtime: Arc<NodeRuntime>, cx: &mut MutableAppContext) {
|
pub fn init(client: Arc<Client>, node_runtime: Arc<NodeRuntime>, cx: &mut MutableAppContext) {
|
||||||
let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), node_runtime, cx));
|
let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), node_runtime, cx));
|
||||||
|
@ -46,6 +49,13 @@ pub fn init(client: Arc<Client>, node_runtime: Arc<NodeRuntime>, cx: &mut Mutabl
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cx.add_global_action(|_: &Reinstall, cx| {
|
||||||
|
let copilot = Copilot::global(cx).unwrap();
|
||||||
|
copilot
|
||||||
|
.update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||||
|
.detach();
|
||||||
|
});
|
||||||
|
|
||||||
cx.observe(&copilot, |handle, cx| {
|
cx.observe(&copilot, |handle, cx| {
|
||||||
let status = handle.read(cx).status();
|
let status = handle.read(cx).status();
|
||||||
cx.update_global::<collections::CommandPaletteFilter, _, _>(
|
cx.update_global::<collections::CommandPaletteFilter, _, _>(
|
||||||
|
@ -73,7 +83,7 @@ pub fn init(client: Arc<Client>, node_runtime: Arc<NodeRuntime>, cx: &mut Mutabl
|
||||||
enum CopilotServer {
|
enum CopilotServer {
|
||||||
Disabled,
|
Disabled,
|
||||||
Starting {
|
Starting {
|
||||||
_task: Shared<Task<()>>,
|
task: Shared<Task<()>>,
|
||||||
},
|
},
|
||||||
Error(Arc<str>),
|
Error(Arc<str>),
|
||||||
Started {
|
Started {
|
||||||
|
@ -97,9 +107,11 @@ enum SignInStatus {
|
||||||
SignedOut,
|
SignedOut,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
Starting,
|
Starting {
|
||||||
|
task: Shared<Task<()>>,
|
||||||
|
},
|
||||||
Error(Arc<str>),
|
Error(Arc<str>),
|
||||||
Disabled,
|
Disabled,
|
||||||
SignedOut,
|
SignedOut,
|
||||||
|
@ -123,6 +135,8 @@ pub struct Completion {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Copilot {
|
pub struct Copilot {
|
||||||
|
http: Arc<dyn HttpClient>,
|
||||||
|
node_runtime: Arc<NodeRuntime>,
|
||||||
server: CopilotServer,
|
server: CopilotServer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,6 +145,13 @@ impl Entity for Copilot {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Copilot {
|
impl Copilot {
|
||||||
|
pub fn starting_task(&self) -> Option<Shared<Task<()>>> {
|
||||||
|
match self.server {
|
||||||
|
CopilotServer::Starting { ref task } => Some(task.clone()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn global(cx: &AppContext) -> Option<ModelHandle<Self>> {
|
pub fn global(cx: &AppContext) -> Option<ModelHandle<Self>> {
|
||||||
if cx.has_global::<ModelHandle<Self>>() {
|
if cx.has_global::<ModelHandle<Self>>() {
|
||||||
Some(cx.global::<ModelHandle<Self>>().clone())
|
Some(cx.global::<ModelHandle<Self>>().clone())
|
||||||
|
@ -159,10 +180,12 @@ impl Copilot {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.shared();
|
.shared();
|
||||||
this.server = CopilotServer::Starting { _task: start_task }
|
this.server = CopilotServer::Starting { task: start_task };
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.server = CopilotServer::Disabled
|
this.server = CopilotServer::Disabled;
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -178,10 +201,14 @@ impl Copilot {
|
||||||
.shared();
|
.shared();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
server: CopilotServer::Starting { _task: start_task },
|
http,
|
||||||
|
node_runtime,
|
||||||
|
server: CopilotServer::Starting { task: start_task },
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Self {
|
Self {
|
||||||
|
http,
|
||||||
|
node_runtime,
|
||||||
server: CopilotServer::Disabled,
|
server: CopilotServer::Disabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -332,6 +359,27 @@ impl Copilot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reinstall(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
|
||||||
|
let start_task = cx
|
||||||
|
.spawn({
|
||||||
|
let http = self.http.clone();
|
||||||
|
let node_runtime = self.node_runtime.clone();
|
||||||
|
move |this, cx| async move {
|
||||||
|
clear_copilot_dir().await;
|
||||||
|
Self::start_language_server(http, node_runtime, this, cx).await
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.shared();
|
||||||
|
|
||||||
|
self.server = CopilotServer::Starting {
|
||||||
|
task: start_task.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
cx.foreground().spawn(start_task)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn completion<T>(
|
pub fn completion<T>(
|
||||||
&self,
|
&self,
|
||||||
buffer: &ModelHandle<Buffer>,
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
@ -391,7 +439,7 @@ impl Copilot {
|
||||||
|
|
||||||
pub fn status(&self) -> Status {
|
pub fn status(&self) -> Status {
|
||||||
match &self.server {
|
match &self.server {
|
||||||
CopilotServer::Starting { .. } => Status::Starting,
|
CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
|
||||||
CopilotServer::Disabled => Status::Disabled,
|
CopilotServer::Disabled => Status::Disabled,
|
||||||
CopilotServer::Error(error) => Status::Error(error.clone()),
|
CopilotServer::Error(error) => Status::Error(error.clone()),
|
||||||
CopilotServer::Started { status, .. } => match status {
|
CopilotServer::Started { status, .. } => match status {
|
||||||
|
@ -501,8 +549,12 @@ fn completion_from_lsp(completion: request::Completion, buffer: &BufferSnapshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn clear_copilot_dir() {
|
||||||
|
remove_matching(&paths::COPILOT_DIR, |_| true).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||||
const SERVER_PATH: &'static str = "agent.js";
|
const SERVER_PATH: &'static str = "dist/agent.js";
|
||||||
|
|
||||||
///Check for the latest copilot language server and download it if we haven't already
|
///Check for the latest copilot language server and download it if we haven't already
|
||||||
async fn fetch_latest(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
async fn fetch_latest(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||||
|
@ -514,6 +566,10 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||||
let server_path = version_dir.join(SERVER_PATH);
|
let server_path = version_dir.join(SERVER_PATH);
|
||||||
|
|
||||||
if fs::metadata(&server_path).await.is_err() {
|
if fs::metadata(&server_path).await.is_err() {
|
||||||
|
// Copilot LSP looks for this dist dir specifcially, so lets add it in.
|
||||||
|
let dist_dir = version_dir.join("dist");
|
||||||
|
fs::create_dir_all(dist_dir.as_path()).await?;
|
||||||
|
|
||||||
let url = &release
|
let url = &release
|
||||||
.assets
|
.assets
|
||||||
.get(0)
|
.get(0)
|
||||||
|
@ -526,7 +582,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||||
.map_err(|err| anyhow!("error downloading copilot release: {}", err))?;
|
.map_err(|err| anyhow!("error downloading copilot release: {}", err))?;
|
||||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||||||
let archive = Archive::new(decompressed_bytes);
|
let archive = Archive::new(decompressed_bytes);
|
||||||
archive.unpack(version_dir).await?;
|
archive.unpack(dist_dir).await?;
|
||||||
|
|
||||||
remove_matching(&paths::COPILOT_DIR, |entry| entry != version_dir).await;
|
remove_matching(&paths::COPILOT_DIR, |entry| entry != version_dir).await;
|
||||||
}
|
}
|
||||||
|
|
|
@ -298,9 +298,7 @@ impl CopilotCodeVerification {
|
||||||
.with_children([
|
.with_children([
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children([
|
.with_children([
|
||||||
theme::ui::svg(&style.auth.copilot_icon).boxed(),
|
theme::ui::svg(&style.auth.copilot_plus_zed_icon).boxed()
|
||||||
theme::ui::icon(&style.auth.plus_icon).boxed(),
|
|
||||||
theme::ui::svg(&style.auth.zed_icon).boxed(),
|
|
||||||
])
|
])
|
||||||
.boxed(),
|
.boxed(),
|
||||||
Flex::column()
|
Flex::column()
|
||||||
|
@ -362,9 +360,7 @@ impl CopilotCodeVerification {
|
||||||
.with_children([
|
.with_children([
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children([
|
.with_children([
|
||||||
theme::ui::svg(&style.auth.copilot_icon).boxed(),
|
theme::ui::svg(&style.auth.copilot_plus_zed_icon).boxed()
|
||||||
theme::ui::icon(&style.auth.plus_icon).boxed(),
|
|
||||||
theme::ui::svg(&style.auth.zed_icon).boxed(),
|
|
||||||
])
|
])
|
||||||
.boxed(),
|
.boxed(),
|
||||||
Label::new("Copilot Enabled!", style.auth.enable_text.clone()).boxed(),
|
Label::new("Copilot Enabled!", style.auth.enable_text.clone()).boxed(),
|
||||||
|
@ -410,9 +406,7 @@ impl CopilotCodeVerification {
|
||||||
.with_children([
|
.with_children([
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children([
|
.with_children([
|
||||||
theme::ui::svg(&style.auth.copilot_icon).boxed(),
|
theme::ui::svg(&style.auth.copilot_plus_zed_icon).boxed()
|
||||||
theme::ui::icon(&style.auth.plus_icon).boxed(),
|
|
||||||
theme::ui::svg(&style.auth.zed_icon).boxed(),
|
|
||||||
])
|
])
|
||||||
.boxed(),
|
.boxed(),
|
||||||
Flex::column()
|
Flex::column()
|
||||||
|
@ -483,13 +477,13 @@ impl View for CopilotCodeVerification {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
||||||
let style = cx.global::<Settings>().theme.copilot.clone();
|
let style = cx.global::<Settings>().theme.clone();
|
||||||
match &self.status {
|
match &self.status {
|
||||||
Status::SigningIn {
|
Status::SigningIn {
|
||||||
prompt: Some(prompt),
|
prompt: Some(prompt),
|
||||||
} => Self::render_prompting_modal(&prompt, &style, cx),
|
} => Self::render_prompting_modal(&prompt, &style.copilot, cx),
|
||||||
Status::Unauthorized => Self::render_unauthorized_modal(&style, cx),
|
Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx),
|
||||||
Status::Authorized => Self::render_enabled_modal(&style, cx),
|
Status::Authorized => Self::render_enabled_modal(&style.copilot, cx),
|
||||||
_ => Empty::new().boxed(),
|
_ => Empty::new().boxed(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,15 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use settings::{settings_file::SettingsFile, Settings};
|
use settings::{settings_file::SettingsFile, Settings};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::ItemHandle, notifications::simple_message_notification::OsOpen, StatusItemView,
|
item::ItemHandle, notifications::simple_message_notification::OsOpen, DismissToast,
|
||||||
|
StatusItemView,
|
||||||
};
|
};
|
||||||
|
|
||||||
use copilot::{Copilot, SignIn, SignOut, Status};
|
use copilot::{Copilot, Reinstall, SignIn, SignOut, Status};
|
||||||
|
|
||||||
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_ERROR_TOAST_ID: usize = 1338;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct DeployCopilotMenu;
|
pub struct DeployCopilotMenu;
|
||||||
|
@ -36,7 +39,7 @@ impl_internal_actions!(
|
||||||
DeployCopilotMenu,
|
DeployCopilotMenu,
|
||||||
DeployCopilotModal,
|
DeployCopilotModal,
|
||||||
ToggleCopilotForLanguage,
|
ToggleCopilotForLanguage,
|
||||||
ToggleCopilotGlobally
|
ToggleCopilotGlobally,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -93,14 +96,18 @@ impl View for CopilotButton {
|
||||||
}
|
}
|
||||||
|
|
||||||
let theme = settings.theme.clone();
|
let theme = settings.theme.clone();
|
||||||
let active = self.popup_menu.read(cx).visible() /* || modal.is_shown */;
|
let active = self.popup_menu.read(cx).visible();
|
||||||
let authorized = Copilot::global(cx).unwrap().read(cx).status() == Status::Authorized;
|
let status = Copilot::global(cx).unwrap().read(cx).status();
|
||||||
|
|
||||||
let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
|
let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
|
||||||
|
|
||||||
|
let view_id = cx.view_id();
|
||||||
|
|
||||||
Stack::new()
|
Stack::new()
|
||||||
.with_child(
|
.with_child(
|
||||||
MouseEventHandler::<Self>::new(0, cx, {
|
MouseEventHandler::<Self>::new(0, cx, {
|
||||||
let theme = theme.clone();
|
let theme = theme.clone();
|
||||||
|
let status = status.clone();
|
||||||
move |state, _cx| {
|
move |state, _cx| {
|
||||||
let style = theme
|
let style = theme
|
||||||
.workspace
|
.workspace
|
||||||
|
@ -112,14 +119,16 @@ impl View for CopilotButton {
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
Svg::new({
|
Svg::new({
|
||||||
if authorized {
|
match status {
|
||||||
if enabled {
|
Status::Error(_) => "icons/copilot_error_16.svg",
|
||||||
"icons/copilot_16.svg"
|
Status::Authorized => {
|
||||||
} else {
|
if enabled {
|
||||||
"icons/copilot_disabled_16.svg"
|
"icons/copilot_16.svg"
|
||||||
|
} else {
|
||||||
|
"icons/copilot_disabled_16.svg"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
_ => "icons/copilot_init_16.svg",
|
||||||
"icons/copilot_init_16.svg"
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.with_color(style.icon_color)
|
.with_color(style.icon_color)
|
||||||
|
@ -136,11 +145,50 @@ impl View for CopilotButton {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(MouseButton::Left, move |_, cx| {
|
.on_click(MouseButton::Left, {
|
||||||
if authorized {
|
let status = status.clone();
|
||||||
cx.dispatch_action(DeployCopilotMenu);
|
move |_, cx| match status {
|
||||||
} else {
|
Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
|
||||||
cx.dispatch_action(SignIn);
|
Status::Starting { ref task } => {
|
||||||
|
cx.dispatch_action(workspace::Toast::new(
|
||||||
|
COPILOT_STARTING_TOAST_ID,
|
||||||
|
"Copilot is starting...",
|
||||||
|
));
|
||||||
|
let window_id = cx.window_id();
|
||||||
|
let task = task.to_owned();
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
task.await;
|
||||||
|
cx.update(|cx| {
|
||||||
|
let status = Copilot::global(cx).unwrap().read(cx).status();
|
||||||
|
match status {
|
||||||
|
Status::Authorized => cx.dispatch_action_at(
|
||||||
|
window_id,
|
||||||
|
view_id,
|
||||||
|
workspace::Toast::new(
|
||||||
|
COPILOT_STARTING_TOAST_ID,
|
||||||
|
"Copilot has started!",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
cx.dispatch_action_at(
|
||||||
|
window_id,
|
||||||
|
view_id,
|
||||||
|
DismissToast::new(COPILOT_STARTING_TOAST_ID),
|
||||||
|
);
|
||||||
|
cx.dispatch_global_action(SignIn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
|
||||||
|
COPILOT_ERROR_TOAST_ID,
|
||||||
|
format!("Copilot can't be started: {}", e),
|
||||||
|
"Reinstall Copilot",
|
||||||
|
Reinstall,
|
||||||
|
)),
|
||||||
|
_ => cx.dispatch_action(SignIn),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.with_tooltip::<Self, _>(
|
.with_tooltip::<Self, _>(
|
||||||
|
@ -195,9 +243,9 @@ impl CopilotButton {
|
||||||
let locally_enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
|
let locally_enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
|
||||||
menu_options.push(ContextMenuItem::item_for_view(
|
menu_options.push(ContextMenuItem::item_for_view(
|
||||||
if locally_enabled {
|
if locally_enabled {
|
||||||
"Pause Copilot for file"
|
"Pause Copilot for this file"
|
||||||
} else {
|
} else {
|
||||||
"Resume Copilot for file"
|
"Resume Copilot for this file"
|
||||||
},
|
},
|
||||||
*view_id,
|
*view_id,
|
||||||
copilot::Toggle,
|
copilot::Toggle,
|
||||||
|
@ -244,6 +292,7 @@ impl CopilotButton {
|
||||||
Label::new("Copilot Settings", style.label.clone()).boxed(),
|
Label::new("Copilot Settings", style.label.clone()).boxed(),
|
||||||
theme::ui::icon(icon_style.style_for(state, false)).boxed(),
|
theme::ui::icon(icon_style.style_for(state, false)).boxed(),
|
||||||
])
|
])
|
||||||
|
.align_children_center()
|
||||||
.boxed()
|
.boxed()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -131,9 +131,7 @@ pub struct CopilotAuth {
|
||||||
pub instruction_text: TextStyle,
|
pub instruction_text: TextStyle,
|
||||||
pub cta_button: ButtonStyle,
|
pub cta_button: ButtonStyle,
|
||||||
pub content_width: f32,
|
pub content_width: f32,
|
||||||
pub copilot_icon: SvgStyle,
|
pub copilot_plus_zed_icon: SvgStyle,
|
||||||
pub plus_icon: IconStyle,
|
|
||||||
pub zed_icon: SvgStyle,
|
|
||||||
pub device_code_group: ContainerStyle,
|
pub device_code_group: ContainerStyle,
|
||||||
pub github_group: ContainerStyle,
|
pub github_group: ContainerStyle,
|
||||||
pub header_group: ContainerStyle,
|
pub header_group: ContainerStyle,
|
||||||
|
|
|
@ -97,7 +97,7 @@ impl Workspace {
|
||||||
let notification = build_notification(cx);
|
let notification = build_notification(cx);
|
||||||
cx.subscribe(¬ification, move |this, handle, event, cx| {
|
cx.subscribe(¬ification, move |this, handle, event, cx| {
|
||||||
if handle.read(cx).should_dismiss_notification_on_event(event) {
|
if handle.read(cx).should_dismiss_notification_on_event(event) {
|
||||||
this.dismiss_notification(type_id, id, cx);
|
this.dismiss_notification_internal(type_id, id, cx);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -107,7 +107,18 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext<Self>) {
|
pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
|
||||||
|
let type_id = TypeId::of::<V>();
|
||||||
|
|
||||||
|
self.dismiss_notification_internal(type_id, id, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss_notification_internal(
|
||||||
|
&mut self,
|
||||||
|
type_id: TypeId,
|
||||||
|
id: usize,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
self.notifications
|
self.notifications
|
||||||
.retain(|(existing_type_id, existing_id, _)| {
|
.retain(|(existing_type_id, existing_id, _)| {
|
||||||
if (*existing_type_id, *existing_id) == (type_id, id) {
|
if (*existing_type_id, *existing_id) == (type_id, id) {
|
||||||
|
@ -183,6 +194,18 @@ pub mod simple_message_notification {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_boxed_action<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>(
|
||||||
|
message: S1,
|
||||||
|
click_action: Box<dyn Action>,
|
||||||
|
click_message: S2,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
message: message.into(),
|
||||||
|
click_action: Some(click_action),
|
||||||
|
click_message: Some(click_message.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new<S1: Into<Cow<'static, str>>, A: Action, S2: Into<Cow<'static, str>>>(
|
pub fn new<S1: Into<Cow<'static, str>>, A: Action, S2: Into<Cow<'static, str>>>(
|
||||||
message: S1,
|
message: S1,
|
||||||
click_action: A,
|
click_action: A,
|
||||||
|
@ -270,9 +293,13 @@ pub mod simple_message_notification {
|
||||||
let style = theme.action_message.style_for(state, false);
|
let style = theme.action_message.style_for(state, false);
|
||||||
if let Some(click_message) = click_message {
|
if let Some(click_message) = click_message {
|
||||||
Some(
|
Some(
|
||||||
Text::new(click_message, style.text.clone())
|
Flex::row()
|
||||||
.contained()
|
.with_child(
|
||||||
.with_style(style.container)
|
Text::new(click_message, style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -288,7 +315,8 @@ pub mod simple_message_notification {
|
||||||
.on_up(MouseButton::Left, |_, _| {})
|
.on_up(MouseButton::Left, |_, _| {})
|
||||||
.on_click(MouseButton::Left, move |_, cx| {
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
if let Some(click_action) = click_action.as_ref() {
|
if let Some(click_action) = click_action.as_ref() {
|
||||||
cx.dispatch_any_action(click_action.boxed_clone())
|
cx.dispatch_any_action(click_action.boxed_clone());
|
||||||
|
cx.dispatch_action(CancelMessageNotification)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.with_cursor_style(if has_click_action {
|
.with_cursor_style(if has_click_action {
|
||||||
|
|
|
@ -41,10 +41,10 @@ use gpui::{
|
||||||
impl_actions, impl_internal_actions,
|
impl_actions, impl_internal_actions,
|
||||||
keymap_matcher::KeymapContext,
|
keymap_matcher::KeymapContext,
|
||||||
platform::{CursorStyle, WindowOptions},
|
platform::{CursorStyle, WindowOptions},
|
||||||
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
Action, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext,
|
||||||
MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, RenderContext,
|
ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel,
|
||||||
SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
RenderContext, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
|
||||||
WindowBounds,
|
WeakViewHandle, WindowBounds,
|
||||||
};
|
};
|
||||||
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
|
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
|
@ -165,6 +165,67 @@ pub struct OpenProjectEntryInPane {
|
||||||
project_entry: ProjectEntryId,
|
project_entry: ProjectEntryId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Toast {
|
||||||
|
id: usize,
|
||||||
|
msg: Cow<'static, str>,
|
||||||
|
click: Option<(Cow<'static, str>, Box<dyn Action>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Toast {
|
||||||
|
pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
|
||||||
|
Toast {
|
||||||
|
id,
|
||||||
|
msg: msg.into(),
|
||||||
|
click: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_action<I1: Into<Cow<'static, str>>, I2: Into<Cow<'static, str>>>(
|
||||||
|
id: usize,
|
||||||
|
msg: I1,
|
||||||
|
click_msg: I2,
|
||||||
|
action: impl Action,
|
||||||
|
) -> Self {
|
||||||
|
Toast {
|
||||||
|
id,
|
||||||
|
msg: msg.into(),
|
||||||
|
click: Some((click_msg.into(), Box::new(action))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Toast {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id
|
||||||
|
&& self.msg == other.msg
|
||||||
|
&& self.click.is_some() == other.click.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for Toast {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Toast {
|
||||||
|
id: self.id,
|
||||||
|
msg: self.msg.to_owned(),
|
||||||
|
click: self
|
||||||
|
.click
|
||||||
|
.as_ref()
|
||||||
|
.map(|(msg, click)| (msg.to_owned(), click.boxed_clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct DismissToast {
|
||||||
|
id: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DismissToast {
|
||||||
|
pub fn new(id: usize) -> Self {
|
||||||
|
DismissToast { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type WorkspaceId = i64;
|
pub type WorkspaceId = i64;
|
||||||
|
|
||||||
impl_internal_actions!(
|
impl_internal_actions!(
|
||||||
|
@ -178,6 +239,8 @@ impl_internal_actions!(
|
||||||
SplitWithItem,
|
SplitWithItem,
|
||||||
SplitWithProjectEntry,
|
SplitWithProjectEntry,
|
||||||
OpenProjectEntryInPane,
|
OpenProjectEntryInPane,
|
||||||
|
Toast,
|
||||||
|
DismissToast
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
impl_actions!(workspace, [ActivatePane]);
|
impl_actions!(workspace, [ActivatePane]);
|
||||||
|
@ -353,6 +416,24 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
.detach();
|
.detach();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cx.add_action(|workspace: &mut Workspace, alert: &Toast, cx| {
|
||||||
|
workspace.dismiss_notification::<MessageNotification>(alert.id, cx);
|
||||||
|
workspace.show_notification(alert.id, cx, |cx| {
|
||||||
|
cx.add_view(|_cx| match &alert.click {
|
||||||
|
Some((click_msg, action)) => MessageNotification::new_boxed_action(
|
||||||
|
alert.msg.clone(),
|
||||||
|
action.boxed_clone(),
|
||||||
|
click_msg.clone(),
|
||||||
|
),
|
||||||
|
None => MessageNotification::new_message(alert.msg.clone()),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.add_action(|workspace: &mut Workspace, alert: &DismissToast, cx| {
|
||||||
|
workspace.dismiss_notification::<MessageNotification>(alert.id, cx);
|
||||||
|
});
|
||||||
|
|
||||||
let client = &app_state.client;
|
let client = &app_state.client;
|
||||||
client.add_view_request_handler(Workspace::handle_follow);
|
client.add_view_request_handler(Workspace::handle_follow);
|
||||||
client.add_view_message_handler(Workspace::handle_unfollow);
|
client.add_view_message_handler(Workspace::handle_unfollow);
|
||||||
|
|
|
@ -31,13 +31,13 @@ export default function copilot(colorScheme: ColorScheme) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
outLinkIcon: {
|
outLinkIcon: {
|
||||||
icon: svg(foreground(layer, "variant"), "icons/maybe_link_out.svg", 12, 12),
|
icon: svg(foreground(layer, "variant"), "icons/link_out_12.svg", 12, 12),
|
||||||
container: {
|
container: {
|
||||||
cornerRadius: 6,
|
cornerRadius: 6,
|
||||||
padding: { top: 6, bottom: 6, left: 6, right: 6 },
|
padding: { left: 6 },
|
||||||
},
|
},
|
||||||
hover: {
|
hover: {
|
||||||
icon: svg(foreground(layer, "hovered"), "icons/maybe_link_out.svg", 12, 12)
|
icon: svg(foreground(layer, "hovered"), "icons/link_out_12.svg", 12, 12)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
|
@ -103,19 +103,7 @@ export default function copilot(colorScheme: ColorScheme) {
|
||||||
right: 0
|
right: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
copilotIcon: svg(foreground(layer, "default"), "icons/github-copilot-dummy.svg", 32, 32),
|
copilotPlusZedIcon: svg(foreground(layer, "default"), "icons/zed_plus_copilot_32.svg", 32, 92),
|
||||||
plusIcon: {
|
|
||||||
icon: svg(foreground(layer, "default"), "icons/plus_12.svg", 12, 12),
|
|
||||||
container: {
|
|
||||||
padding: {
|
|
||||||
top: 12,
|
|
||||||
bottom: 12,
|
|
||||||
left: 12,
|
|
||||||
right: 12,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
zedIcon: svg(foreground(layer, "default"), "icons/logo_96.svg", 32, 32),
|
|
||||||
enableText: text(layer, "sans", { size: "md" }),
|
enableText: text(layer, "sans", { size: "md" }),
|
||||||
enableGroup: {
|
enableGroup: {
|
||||||
margin: {
|
margin: {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ColorScheme } from "../themes/common/colorScheme"
|
import { ColorScheme } from "../themes/common/colorScheme"
|
||||||
import { foreground, text } from "./components"
|
import { background, border, foreground, text } from "./components"
|
||||||
|
|
||||||
const headerPadding = 8
|
const headerPadding = 8
|
||||||
|
|
||||||
|
@ -14,9 +14,21 @@ export default function simpleMessageNotification(
|
||||||
},
|
},
|
||||||
actionMessage: {
|
actionMessage: {
|
||||||
...text(layer, "sans", { size: "xs" }),
|
...text(layer, "sans", { size: "xs" }),
|
||||||
|
border: border(layer, "active"),
|
||||||
|
cornerRadius: 4,
|
||||||
|
padding: {
|
||||||
|
top: 3,
|
||||||
|
bottom: 3,
|
||||||
|
left: 7,
|
||||||
|
right: 7,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
margin: { left: headerPadding, top: 6, bottom: 6 },
|
margin: { left: headerPadding, top: 6, bottom: 6 },
|
||||||
hover: {
|
hover: {
|
||||||
color: foreground(layer, "hovered"),
|
...text(layer, "sans", "default", { size: "xs" }),
|
||||||
|
background: background(layer, "hovered"),
|
||||||
|
border: border(layer, "active"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dismissButton: {
|
dismissButton: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue