Compare commits

...

5 Commits

Author SHA1 Message Date
Conrad Irwin
6d9e40c395 zed 0.126.1 2024-03-07 16:14:10 -07:00
gcp-cherry-pick-bot[bot]
8845e68233 Fix panic in open urls (cherry-pick #9032) (#9034)
Cherry-picked Fix panic in open urls (#9032)

Co-Authored-By: Nathan <nathan@zed.dev>

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Nathan <nathan@zed.dev>
2024-03-07 14:29:41 -07:00
gcp-cherry-pick-bot[bot]
e0034ec633 Make anchor_in_excerpt Optional (cherry-pick #8975) (#8979)
Cherry-picked Make anchor_in_excerpt Optional (#8975)

We were seeing panics due to callers assuming they had valid
excerpt_ids, but that cannot easily be guaranteed across await points as
anyone may remove an excerpt.

Release Notes:

- Fixed a panic when hovering in a multibuffer

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-03-06 20:42:38 -07:00
Max Brunsfeld
5a82dc2233 Emit the WorktreeUpdatedEntries event for all projects, not just local (#8963)
Fixes a regression introduced in
https://github.com/zed-industries/zed/pull/8846 (which hasn't yet been
released), in which the project panel didn't update correctly for remote
projects when collaborating.

Release Notes:

- N/A
2024-03-06 15:17:11 -05:00
Joseph T. Lyons
afed6f4980 v0.126.x preview 2024-03-06 12:29:47 -05:00
21 changed files with 296 additions and 311 deletions

2
Cargo.lock generated
View File

@@ -12824,7 +12824,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.126.0"
version = "0.126.1"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -2413,7 +2413,9 @@ impl ConversationEditor {
.read(cx)
.messages(cx)
.map(|message| BlockProperties {
position: buffer.anchor_in_excerpt(excerpt_id, message.anchor),
position: buffer
.anchor_in_excerpt(excerpt_id, message.anchor)
.unwrap(),
height: 2,
style: BlockStyle::Sticky,
render: Arc::new({

View File

@@ -517,15 +517,15 @@ impl ProjectDiagnosticsEditor {
self.editor.update(cx, |editor, cx| {
editor.remove_blocks(blocks_to_remove, None, cx);
let block_ids = editor.insert_blocks(
blocks_to_add.into_iter().map(|block| {
blocks_to_add.into_iter().flat_map(|block| {
let (excerpt_id, text_anchor) = block.position;
BlockProperties {
position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
Some(BlockProperties {
position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
height: block.height,
style: block.style,
render: block.render,
disposition: block.disposition,
}
})
}),
Some(Autoscroll::fit()),
cx,
@@ -589,14 +589,16 @@ impl ProjectDiagnosticsEditor {
Ok(ix) | Err(ix) => ix,
};
if let Some(group) = groups.get(group_ix) {
let offset = excerpts_snapshot
if let Some(offset) = excerpts_snapshot
.anchor_in_excerpt(
group.excerpts[group.primary_excerpt_ix],
group.primary_diagnostic.range.start,
)
.to_offset(&excerpts_snapshot);
selection.start = offset;
selection.end = offset;
.map(|anchor| anchor.to_offset(&excerpts_snapshot))
{
selection.start = offset;
selection.end = offset;
}
}
}
}

View File

@@ -249,7 +249,7 @@ pub fn init(cx: &mut AppContext) {
cx.on_action(move |_: &workspace::NewFile, cx| {
let app_state = workspace::AppState::global(cx);
if let Some(app_state) = app_state.upgrade() {
workspace::open_new(&app_state, cx, |workspace, cx| {
workspace::open_new(app_state, cx, |workspace, cx| {
Editor::new_file(workspace, &Default::default(), cx)
})
.detach();
@@ -258,7 +258,7 @@ pub fn init(cx: &mut AppContext) {
cx.on_action(move |_: &workspace::NewWindow, cx| {
let app_state = workspace::AppState::global(cx);
if let Some(app_state) = app_state.upgrade() {
workspace::open_new(&app_state, cx, |workspace, cx| {
workspace::open_new(app_state, cx, |workspace, cx| {
Editor::new_file(workspace, &Default::default(), cx)
})
.detach();

View File

@@ -13,7 +13,7 @@ use project::{
};
use std::ops::Range;
use theme::ActiveTheme as _;
use util::TryFutureExt;
use util::{maybe, TryFutureExt};
#[derive(Debug)]
pub struct HoveredLinkState {
@@ -424,12 +424,13 @@ pub fn show_link_definition(
TriggerPoint::Text(_) => {
if let Some((url_range, url)) = find_url(&buffer, buffer_position, cx.clone()) {
this.update(&mut cx, |_, _| {
let start = snapshot.anchor_in_excerpt(excerpt_id, url_range.start);
let end = snapshot.anchor_in_excerpt(excerpt_id, url_range.end);
(
Some(RangeInEditor::Text(start..end)),
vec![HoverLink::Url(url)],
)
let range = maybe!({
let start =
snapshot.anchor_in_excerpt(excerpt_id, url_range.start)?;
let end = snapshot.anchor_in_excerpt(excerpt_id, url_range.end)?;
Some(RangeInEditor::Text(start..end))
});
(range, vec![HoverLink::Url(url)])
})
.ok()
} else if let Some(project) = project {
@@ -449,12 +450,14 @@ pub fn show_link_definition(
.map(|definition_result| {
(
definition_result.iter().find_map(|link| {
link.origin.as_ref().map(|origin| {
let start = snapshot
.anchor_in_excerpt(excerpt_id, origin.range.start);
link.origin.as_ref().and_then(|origin| {
let start = snapshot.anchor_in_excerpt(
excerpt_id,
origin.range.start,
)?;
let end = snapshot
.anchor_in_excerpt(excerpt_id, origin.range.end);
RangeInEditor::Text(start..end)
.anchor_in_excerpt(excerpt_id, origin.range.end)?;
Some(RangeInEditor::Text(start..end))
})
}),
definition_result.into_iter().map(HoverLink::Text).collect(),

View File

@@ -294,18 +294,19 @@ fn show_hover(
let hover_popover = match hover_result {
Some(hover_result) if !hover_result.is_empty() => {
// Create symbol range of anchors for highlighting and filtering of future requests.
let range = if let Some(range) = hover_result.range {
let start = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.start);
let end = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.end);
let range = hover_result
.range
.and_then(|range| {
let start = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.start)?;
let end = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.end)?;
start..end
} else {
anchor..anchor
};
Some(start..end)
})
.unwrap_or_else(|| anchor..anchor);
let language_registry =
project.update(&mut cx, |p, _| p.languages().clone())?;

View File

@@ -460,14 +460,15 @@ impl InlayHintCache {
if !old_kinds.contains(&cached_hint.kind)
&& new_kinds.contains(&cached_hint.kind)
{
to_insert.push(Inlay::hint(
cached_hint_id.id(),
multi_buffer_snapshot.anchor_in_excerpt(
*excerpt_id,
cached_hint.position,
),
&cached_hint,
));
if let Some(anchor) = multi_buffer_snapshot
.anchor_in_excerpt(*excerpt_id, cached_hint.position)
{
to_insert.push(Inlay::hint(
cached_hint_id.id(),
anchor,
&cached_hint,
));
}
}
excerpt_cache.next();
}
@@ -483,12 +484,15 @@ impl InlayHintCache {
let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
let cached_hint_kind = maybe_missed_cached_hint.kind;
if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
to_insert.push(Inlay::hint(
cached_hint_id.id(),
multi_buffer_snapshot
.anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
&maybe_missed_cached_hint,
));
if let Some(anchor) = multi_buffer_snapshot
.anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position)
{
to_insert.push(Inlay::hint(
cached_hint_id.id(),
anchor,
&maybe_missed_cached_hint,
));
}
}
}
}
@@ -1200,11 +1204,13 @@ fn apply_hint_update(
.allowed_hint_kinds
.contains(&new_hint.kind)
{
let new_hint_position =
multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position);
splice
.to_insert
.push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
if let Some(new_hint_position) =
multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position)
{
splice
.to_insert
.push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
}
}
let new_id = InlayId::Hint(new_inlay_id);
cached_excerpt_hints.hints_by_id.insert(new_id, new_hint);

View File

@@ -1154,8 +1154,8 @@ impl SearchableItem for Editor {
let end = excerpt
.buffer
.anchor_before(excerpt_range.start + range.end);
buffer.anchor_in_excerpt(excerpt.id, start)
..buffer.anchor_in_excerpt(excerpt.id, end)
buffer.anchor_in_excerpt(excerpt.id, start).unwrap()
..buffer.anchor_in_excerpt(excerpt.id, end).unwrap()
}),
);
}

View File

@@ -150,14 +150,9 @@ impl App {
/// to open one or more URLs.
pub fn on_open_urls<F>(&self, mut callback: F) -> &Self
where
F: 'static + FnMut(Vec<String>, &mut AppContext),
F: 'static + FnMut(Vec<String>),
{
let this = Rc::downgrade(&self.0);
self.0.borrow().platform.on_open_urls(Box::new(move |urls| {
if let Some(app) = this.upgrade() {
callback(urls, &mut app.borrow_mut());
}
}));
self.0.borrow().platform.on_open_urls(Box::new(callback));
self
}

View File

@@ -102,7 +102,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut WindowContext) {
cx.spawn(|mut cx| async move {
let (journal_dir, entry_path) = create_entry.await?;
let (workspace, _) = cx
.update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))?
.update(|cx| workspace::open_paths(&[journal_dir], app_state, None, cx))?
.await?;
let opened = workspace

View File

@@ -227,8 +227,12 @@ impl SyntaxTreeView {
let multibuffer = editor_state.editor.read(cx).buffer();
let multibuffer = multibuffer.read(cx).snapshot(cx);
let excerpt_id = buffer_state.excerpt_id;
let range = multibuffer.anchor_in_excerpt(excerpt_id, range.start)
..multibuffer.anchor_in_excerpt(excerpt_id, range.end);
let range = multibuffer
.anchor_in_excerpt(excerpt_id, range.start)
.unwrap()
..multibuffer
.anchor_in_excerpt(excerpt_id, range.end)
.unwrap();
// Update the editor with the anchor range.
editor_state.editor.update(cx, |editor, cx| {

View File

@@ -2788,7 +2788,13 @@ impl MultiBufferSnapshot {
}
}
pub fn anchor_in_excerpt(&self, excerpt_id: ExcerptId, text_anchor: text::Anchor) -> Anchor {
/// Returns an anchor for the given excerpt and text anchor,
/// returns None if the excerpt_id is no longer valid.
pub fn anchor_in_excerpt(
&self,
excerpt_id: ExcerptId,
text_anchor: text::Anchor,
) -> Option<Anchor> {
let locator = self.excerpt_locator_for_id(excerpt_id);
let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
cursor.seek(locator, Bias::Left, &());
@@ -2796,14 +2802,14 @@ impl MultiBufferSnapshot {
if excerpt.id == excerpt_id {
let text_anchor = excerpt.clip_anchor(text_anchor);
drop(cursor);
return Anchor {
return Some(Anchor {
buffer_id: Some(excerpt.buffer_id),
excerpt_id,
text_anchor,
};
});
}
}
panic!("excerpt not found");
None
}
pub fn can_resolve(&self, anchor: &Anchor) -> bool {
@@ -3283,13 +3289,15 @@ impl MultiBufferSnapshot {
outline
.items
.into_iter()
.map(|item| OutlineItem {
depth: item.depth,
range: self.anchor_in_excerpt(*excerpt_id, item.range.start)
..self.anchor_in_excerpt(*excerpt_id, item.range.end),
text: item.text,
highlight_ranges: item.highlight_ranges,
name_ranges: item.name_ranges,
.flat_map(|item| {
Some(OutlineItem {
depth: item.depth,
range: self.anchor_in_excerpt(*excerpt_id, item.range.start)?
..self.anchor_in_excerpt(*excerpt_id, item.range.end)?,
text: item.text,
highlight_ranges: item.highlight_ranges,
name_ranges: item.name_ranges,
})
})
.collect(),
))
@@ -3310,13 +3318,15 @@ impl MultiBufferSnapshot {
.symbols_containing(anchor.text_anchor, theme)
.into_iter()
.flatten()
.map(|item| OutlineItem {
depth: item.depth,
range: self.anchor_in_excerpt(excerpt_id, item.range.start)
..self.anchor_in_excerpt(excerpt_id, item.range.end),
text: item.text,
highlight_ranges: item.highlight_ranges,
name_ranges: item.name_ranges,
.flat_map(|item| {
Some(OutlineItem {
depth: item.depth,
range: self.anchor_in_excerpt(excerpt_id, item.range.start)?
..self.anchor_in_excerpt(excerpt_id, item.range.end)?,
text: item.text,
highlight_ranges: item.highlight_ranges,
name_ranges: item.name_ranges,
})
})
.collect(),
))

View File

@@ -6745,24 +6745,30 @@ impl Project {
fn add_worktree(&mut self, worktree: &Model<Worktree>, cx: &mut ModelContext<Self>) {
cx.observe(worktree, |_, _, cx| cx.notify()).detach();
if worktree.read(cx).is_local() {
cx.subscribe(worktree, |this, worktree, event, cx| match event {
cx.subscribe(worktree, |this, worktree, event, cx| {
let is_local = worktree.read(cx).is_local();
match event {
worktree::Event::UpdatedEntries(changes) => {
this.update_local_worktree_buffers(&worktree, changes, cx);
this.update_local_worktree_language_servers(&worktree, changes, cx);
this.update_local_worktree_settings(&worktree, changes, cx);
this.update_prettier_settings(&worktree, changes, cx);
if is_local {
this.update_local_worktree_buffers(&worktree, changes, cx);
this.update_local_worktree_language_servers(&worktree, changes, cx);
this.update_local_worktree_settings(&worktree, changes, cx);
this.update_prettier_settings(&worktree, changes, cx);
}
cx.emit(Event::WorktreeUpdatedEntries(
worktree.read(cx).id(),
changes.clone(),
));
}
worktree::Event::UpdatedGitRepositories(updated_repos) => {
this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
if is_local {
this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
}
}
})
.detach();
}
}
})
.detach();
let push_strong_handle = {
let worktree = worktree.read(cx);

View File

@@ -486,7 +486,7 @@ mod tests {
}),
)
.await;
cx.update(|cx| open_paths(&[PathBuf::from("/dir/main.ts")], &app_state, None, cx))
cx.update(|cx| open_paths(&[PathBuf::from("/dir/main.ts")], app_state, None, cx))
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);

View File

@@ -37,8 +37,8 @@ pub fn init(cx: &mut AppContext) {
base_keymap_picker::init(cx);
}
pub fn show_welcome_view(app_state: &Arc<AppState>, cx: &mut AppContext) {
open_new(&app_state, cx, |workspace, cx| {
pub fn show_welcome_view(app_state: Arc<AppState>, cx: &mut AppContext) {
open_new(app_state, cx, |workspace, cx| {
workspace.toggle_dock(DockPosition::Left, cx);
let welcome_page = WelcomePage::new(workspace, cx);
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);

View File

@@ -262,7 +262,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.spawn(move |cx| async move {
if let Some(paths) = paths.await.log_err().flatten() {
cx.update(|cx| {
open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
open_paths(&paths, app_state, None, cx).detach_and_log_err(cx)
})
.ok();
}
@@ -1414,7 +1414,7 @@ impl Workspace {
let app_state = self.app_state.clone();
cx.spawn(|_, mut cx| async move {
cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx))?
cx.update(|cx| open_paths(&paths, app_state, window_to_replace, cx))?
.await?;
Ok(())
})
@@ -4381,7 +4381,7 @@ fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandl
#[allow(clippy::type_complexity)]
pub fn open_paths(
abs_paths: &[PathBuf],
app_state: &Arc<AppState>,
app_state: Arc<AppState>,
requesting_window: Option<WindowHandle<Workspace>>,
cx: &mut AppContext,
) -> Task<
@@ -4390,7 +4390,6 @@ pub fn open_paths(
Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
)>,
> {
let app_state = app_state.clone();
let abs_paths = abs_paths.to_vec();
// Open paths in existing workspace if possible
let existing = activate_workspace_for_project(cx, {
@@ -4417,11 +4416,11 @@ pub fn open_paths(
}
pub fn open_new(
app_state: &Arc<AppState>,
app_state: Arc<AppState>,
cx: &mut AppContext,
init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
) -> Task<()> {
let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
let task = Workspace::new_local(Vec::new(), app_state, None, cx);
cx.spawn(|mut cx| async move {
if let Some((workspace, opened_paths)) = task.await.log_err() {
workspace

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.126.0"
version = "0.126.1"
publish = false
license = "GPL-3.0-or-later"

View File

@@ -1 +1 @@
dev
preview

View File

@@ -106,11 +106,11 @@ fn main() {
let (listener, mut open_rx) = OpenListener::new();
let listener = Arc::new(listener);
let open_listener = listener.clone();
app.on_open_urls(move |urls, cx| open_listener.open_urls(&urls, cx));
app.on_open_urls(move |urls| open_listener.open_urls(urls));
app.on_reopen(move |cx| {
if let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade())
{
workspace::open_new(&app_state, cx, |workspace, cx| {
workspace::open_new(app_state, cx, |workspace, cx| {
Editor::new_file(workspace, &Default::default(), cx)
})
.detach();
@@ -273,7 +273,7 @@ fn main() {
cx.activate(true);
let urls = collect_url_args(cx);
if !urls.is_empty() {
listener.open_urls(&urls, cx)
listener.open_urls(urls)
}
} else {
upload_panics_and_crashes(http.clone(), cx);
@@ -282,141 +282,38 @@ fn main() {
if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some()
&& !listener.triggered.load(Ordering::Acquire)
{
listener.open_urls(&collect_url_args(cx), cx)
listener.open_urls(collect_url_args(cx))
}
}
let mut triggered_authentication = false;
fn open_paths_and_log_errs(
paths: &[PathBuf],
app_state: &Arc<AppState>,
cx: &mut AppContext,
) {
let task = workspace::open_paths(&paths, &app_state, None, cx);
cx.spawn(|_| async move {
if let Some((_window, results)) = task.await.log_err() {
for result in results.into_iter().flatten() {
if let Err(err) = result {
log::error!("Error opening path: {err}",);
}
}
}
})
.detach();
}
match open_rx.try_next() {
Ok(Some(OpenRequest::Paths { paths })) => {
open_paths_and_log_errs(&paths, &app_state, cx)
match open_rx
.try_next()
.ok()
.flatten()
.and_then(|urls| OpenRequest::parse(urls, cx).log_err())
{
Some(request) => {
triggered_authentication = handle_open_request(request, app_state.clone(), cx)
}
Ok(Some(OpenRequest::CliConnection { connection })) => {
let app_state = app_state.clone();
cx.spawn(move |cx| handle_cli_connection(connection, app_state, cx))
.detach();
}
Ok(Some(OpenRequest::JoinChannel { channel_id })) => {
triggered_authentication = true;
let app_state = app_state.clone();
let client = client.clone();
cx.spawn(|cx| async move {
// ignore errors here, we'll show a generic "not signed in"
let _ = authenticate(client, &cx).await;
cx.update(|cx| {
workspace::join_channel(client::ChannelId(channel_id), app_state, None, cx)
})?
.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
Ok(Some(OpenRequest::OpenChannelNotes {
channel_id,
heading,
})) => {
triggered_authentication = true;
let app_state = app_state.clone();
let client = client.clone();
cx.spawn(|mut cx| async move {
// ignore errors here, we'll show a generic "not signed in"
let _ = authenticate(client, &cx).await;
let workspace_window =
workspace::get_any_active_workspace(app_state, cx.clone()).await?;
let workspace = workspace_window.root_view(&cx)?;
cx.update_window(workspace_window.into(), |_, cx| {
ChannelView::open(client::ChannelId(channel_id), heading, workspace, cx)
})?
.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
Ok(None) | Err(_) => cx
None => cx
.spawn({
let app_state = app_state.clone();
|cx| async move { restore_or_create_workspace(&app_state, cx).await }
|cx| async move { restore_or_create_workspace(app_state, cx).await }
})
.detach(),
}
let app_state = app_state.clone();
cx.spawn(move |cx| async move {
while let Some(request) = open_rx.next().await {
match request {
OpenRequest::Paths { paths } => {
cx.update(|cx| open_paths_and_log_errs(&paths, &app_state, cx))
.ok();
while let Some(urls) = open_rx.next().await {
cx.update(|cx| {
if let Some(request) = OpenRequest::parse(urls, cx).log_err() {
handle_open_request(request, app_state.clone(), cx);
}
OpenRequest::CliConnection { connection } => {
let app_state = app_state.clone();
cx.spawn(move |cx| {
handle_cli_connection(connection, app_state.clone(), cx)
})
.detach();
}
OpenRequest::JoinChannel { channel_id } => {
let app_state = app_state.clone();
cx.update(|mut cx| {
cx.spawn(|cx| async move {
cx.update(|cx| {
workspace::join_channel(
client::ChannelId(channel_id),
app_state,
None,
cx,
)
})?
.await?;
anyhow::Ok(())
})
.detach_and_log_err(&mut cx);
})
.log_err();
}
OpenRequest::OpenChannelNotes {
channel_id,
heading,
} => {
let app_state = app_state.clone();
let open_notes_task = cx.spawn(|mut cx| async move {
let workspace_window =
workspace::get_any_active_workspace(app_state, cx.clone()).await?;
let workspace = workspace_window.root_view(&cx)?;
cx.update_window(workspace_window.into(), |_, cx| {
ChannelView::open(
client::ChannelId(channel_id),
heading,
workspace,
cx,
)
})?
.await?;
anyhow::Ok(())
});
cx.update(|cx| open_notes_task.detach_and_log_err(cx))
.log_err();
}
}
})
.ok();
}
})
.detach();
@@ -428,6 +325,70 @@ fn main() {
});
}
fn open_paths_and_log_errs(paths: &[PathBuf], app_state: Arc<AppState>, cx: &mut AppContext) {
let task = workspace::open_paths(&paths, app_state, None, cx);
cx.spawn(|_| async move {
if let Some((_window, results)) = task.await.log_err() {
for result in results.into_iter().flatten() {
if let Err(err) = result {
log::error!("Error opening path: {err}",);
}
}
}
})
.detach();
}
fn handle_open_request(
request: OpenRequest,
app_state: Arc<AppState>,
cx: &mut AppContext,
) -> bool {
let mut triggered_authentication = false;
match request {
OpenRequest::Paths { paths } => open_paths_and_log_errs(&paths, app_state, cx),
OpenRequest::CliConnection { connection } => {
let app_state = app_state.clone();
cx.spawn(move |cx| handle_cli_connection(connection, app_state, cx))
.detach();
}
OpenRequest::JoinChannel { channel_id } => {
triggered_authentication = true;
cx.spawn(|cx| async move {
// ignore errors here, we'll show a generic "not signed in"
let _ = authenticate(app_state.client.clone(), &cx).await;
cx.update(|cx| {
workspace::join_channel(client::ChannelId(channel_id), app_state, None, cx)
})?
.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
OpenRequest::OpenChannelNotes {
channel_id,
heading,
} => {
triggered_authentication = true;
let client = app_state.client.clone();
cx.spawn(|mut cx| async move {
// ignore errors here, we'll show a generic "not signed in"
let _ = authenticate(client, &cx).await;
let workspace_window =
workspace::get_any_active_workspace(app_state, cx.clone()).await?;
let workspace = workspace_window.root_view(&cx)?;
cx.update_window(workspace_window.into(), |_, cx| {
ChannelView::open(client::ChannelId(channel_id), heading, workspace, cx)
})?
.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
triggered_authentication
}
async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
if stdout_is_a_pty() {
if client::IMPERSONATE_LOGIN.is_some() {
@@ -465,7 +426,7 @@ async fn installation_id() -> Result<(String, bool)> {
Ok((installation_id, false))
}
async fn restore_or_create_workspace(app_state: &Arc<AppState>, cx: AsyncAppContext) {
async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: AsyncAppContext) {
async_maybe!({
if let Some(location) = workspace::last_opened_workspace_paths().await {
cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))?

View File

@@ -37,8 +37,66 @@ pub enum OpenRequest {
},
}
impl OpenRequest {
pub fn parse(urls: Vec<String>, cx: &AppContext) -> Result<Self> {
if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) {
Self::parse_cli_connection(server_name)
} else if let Some(request_path) = urls.first().and_then(|url| parse_zed_link(url, cx)) {
Self::parse_zed_url(request_path)
} else {
Ok(Self::parse_file_urls(urls))
}
}
fn parse_cli_connection(server_name: &str) -> Result<OpenRequest> {
let connection = connect_to_cli(server_name)?;
Ok(OpenRequest::CliConnection { connection })
}
fn parse_zed_url(request_path: &str) -> Result<OpenRequest> {
let mut parts = request_path.split('/');
if parts.next() == Some("channel") {
if let Some(slug) = parts.next() {
if let Some(id_str) = slug.split('-').last() {
if let Ok(channel_id) = id_str.parse::<u64>() {
let Some(next) = parts.next() else {
return Ok(OpenRequest::JoinChannel { channel_id });
};
if let Some(heading) = next.strip_prefix("notes#") {
return Ok(OpenRequest::OpenChannelNotes {
channel_id,
heading: Some([heading].into_iter().chain(parts).join("/")),
});
} else if next == "notes" {
return Ok(OpenRequest::OpenChannelNotes {
channel_id,
heading: None,
});
}
}
}
}
}
Err(anyhow!("invalid zed url: {}", request_path))
}
fn parse_file_urls(urls: Vec<String>) -> OpenRequest {
let paths: Vec<_> = urls
.iter()
.flat_map(|url| url.strip_prefix("file://"))
.flat_map(|url| {
let decoded = urlencoding::decode_binary(url.as_bytes());
PathBuf::try_from_bytes(decoded.as_ref()).log_err()
})
.collect();
OpenRequest::Paths { paths }
}
}
pub struct OpenListener {
tx: UnboundedSender<OpenRequest>,
tx: UnboundedSender<Vec<String>>,
pub triggered: AtomicBool,
}
@@ -55,7 +113,7 @@ impl OpenListener {
cx.set_global(GlobalOpenListener(listener))
}
pub fn new() -> (Self, UnboundedReceiver<OpenRequest>) {
pub fn new() -> (Self, UnboundedReceiver<Vec<String>>) {
let (tx, rx) = mpsc::unbounded();
(
OpenListener {
@@ -66,74 +124,12 @@ impl OpenListener {
)
}
pub fn open_urls(&self, urls: &[String], cx: &AppContext) {
pub fn open_urls(&self, urls: Vec<String>) {
self.triggered.store(true, Ordering::Release);
let request = if let Some(server_name) =
urls.first().and_then(|url| url.strip_prefix("zed-cli://"))
{
self.handle_cli_connection(server_name)
} else if let Some(request_path) = urls.first().and_then(|url| parse_zed_link(url, cx)) {
self.handle_zed_url_scheme(request_path)
} else {
self.handle_file_urls(urls)
};
if let Some(request) = request {
self.tx
.unbounded_send(request)
.map_err(|_| anyhow!("no listener for open requests"))
.log_err();
}
}
fn handle_cli_connection(&self, server_name: &str) -> Option<OpenRequest> {
if let Some(connection) = connect_to_cli(server_name).log_err() {
return Some(OpenRequest::CliConnection { connection });
}
None
}
fn handle_zed_url_scheme(&self, request_path: &str) -> Option<OpenRequest> {
let mut parts = request_path.split('/');
if parts.next() == Some("channel") {
if let Some(slug) = parts.next() {
if let Some(id_str) = slug.split('-').last() {
if let Ok(channel_id) = id_str.parse::<u64>() {
let Some(next) = parts.next() else {
return Some(OpenRequest::JoinChannel { channel_id });
};
if let Some(heading) = next.strip_prefix("notes#") {
return Some(OpenRequest::OpenChannelNotes {
channel_id,
heading: Some([heading].into_iter().chain(parts).join("/")),
});
} else if next == "notes" {
return Some(OpenRequest::OpenChannelNotes {
channel_id,
heading: None,
});
}
}
}
}
}
log::error!("invalid zed url: {}", request_path);
None
}
fn handle_file_urls(&self, urls: &[String]) -> Option<OpenRequest> {
let paths: Vec<_> = urls
.iter()
.flat_map(|url| url.strip_prefix("file://"))
.flat_map(|url| {
let decoded = urlencoding::decode_binary(url.as_bytes());
PathBuf::try_from_bytes(decoded.as_ref()).log_err()
})
.collect();
Some(OpenRequest::Paths { paths })
self.tx
.unbounded_send(urls)
.map_err(|_| anyhow!("no listener for open requests"))
.log_err();
}
}
@@ -210,7 +206,7 @@ pub async fn handle_cli_connection(
let mut errored = false;
match cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) {
match cx.update(|cx| workspace::open_paths(&paths, app_state, None, cx)) {
Ok(task) => match task.await {
Ok((workspace, items)) => {
let mut item_release_futures = Vec::new();

View File

@@ -233,7 +233,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.toggle_full_screen();
})
.register_action(|_, action: &OpenZedUrl, cx| {
OpenListener::global(cx).open_urls(&[action.url.clone()], cx)
OpenListener::global(cx).open_urls(vec![action.url.clone()])
})
.register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url))
.register_action(move |_, _: &IncreaseBufferFontSize, cx| {
@@ -399,7 +399,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(&app_state);
move |_, _: &NewWindow, cx| {
if let Some(app_state) = app_state.upgrade() {
open_new(&app_state, cx, |workspace, cx| {
open_new(app_state, cx, |workspace, cx| {
Editor::new_file(workspace, &Default::default(), cx)
})
.detach();
@@ -410,7 +410,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(&app_state);
move |_, _: &NewFile, cx| {
if let Some(app_state) = app_state.upgrade() {
open_new(&app_state, cx, |workspace, cx| {
open_new(app_state, cx, |workspace, cx| {
Editor::new_file(workspace, &Default::default(), cx)
})
.detach();
@@ -911,7 +911,7 @@ mod tests {
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
&app_state,
app_state.clone(),
None,
cx,
)
@@ -920,7 +920,7 @@ mod tests {
.unwrap();
assert_eq!(cx.read(|cx| cx.windows().len()), 1);
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], app_state.clone(), None, cx))
.await
.unwrap();
assert_eq!(cx.read(|cx| cx.windows().len()), 1);
@@ -942,7 +942,7 @@ mod tests {
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
&app_state,
app_state.clone(),
None,
cx,
)
@@ -958,7 +958,7 @@ mod tests {
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
&app_state,
app_state,
Some(window),
cx,
)
@@ -995,7 +995,7 @@ mod tests {
.insert_tree("/root", json!({"a": "hey"}))
.await;
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], app_state.clone(), None, cx))
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
@@ -1062,7 +1062,7 @@ mod tests {
assert!(!window_is_edited(window, cx));
// Opening the buffer again doesn't impact the window's edited state.
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], app_state, None, cx))
.await
.unwrap();
let editor = window
@@ -1100,7 +1100,7 @@ mod tests {
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
let app_state = init_test(cx);
cx.update(|cx| {
open_new(&app_state, cx, |workspace, cx| {
open_new(app_state.clone(), cx, |workspace, cx| {
Editor::new_file(workspace, &Default::default(), cx)
})
})
@@ -1291,7 +1291,7 @@ mod tests {
)
.await;
cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx))
cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], app_state, None, cx))
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
@@ -1525,7 +1525,7 @@ mod tests {
Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
];
let (opened_workspace, new_items) = cx
.update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx))
.update(|cx| workspace::open_paths(&paths_to_open, app_state, None, cx))
.await
.unwrap();