Compare commits

...
Sign in to create a new pull request.

22 commits

Author SHA1 Message Date
Thorsten Ball
eae06aaca9 zed 0.125.4 2024-03-12 12:32:41 +01:00
Thorsten Ball
8ebee1f31d Fix clippy warning after cherry-picking fix into 0.125.x 2024-03-12 12:32:13 +01:00
Thorsten Ball
bc4230a6b1 Fix broken ESLint by pinning to 2.2.20-Insiders release (#9215)
This fixes #9213 by pinning ESLint to `2.2.20-Insiders` which is the
last known version to work well with Zed.

Once this fix is out, we can take a closer look at upgrading to 2.4.x or
even 3.x once that's out of prerelease.

Release Notes:

- Fixed ESLint integration being broken after Mar 7 2024 due to ESLint
3.0.1 alpha release being pushed.
([#9213](https://github.com/zed-industries/zed/issues/9213)).
2024-03-12 12:18:55 +01:00
Joseph T. Lyons
430d197e91 v0.125.x stable 2024-03-06 12:29:47 -05:00
Joseph T. Lyons
a9696c59cc zed 0.125.3 2024-03-06 11:39:44 -05:00
gcp-cherry-pick-bot[bot]
3afc9d8607
Throttle the sending of UpdateFollowers messages (cherry-pick #8918) (#8946)
Cherry-picked Throttle the sending of UpdateFollowers messages (#8918)

## Problem

We're trying to figure out why we sometimes see high latency when
collaborating, even though the collab server logs indicate that messages
are not taking long to process.

We think that high volumes of certain types of messages, including
`UpdateFollowers` may cause a lot of messages to queue up, causing
delays before collab sees certain messages.

## Fix

This PR reduces the number of `UpdateFollowers` messages that clients
send to collab when scrolling around or moving the cursor, using a
time-based throttle.

The downside of this change is that scrolling will not be as smooth when
following someone. The advantage is that it will be much easier to keep
up with the stream of updates, since they will be sent much less
frequently.

## Release Notes:

- Fixed slowness that could occur when collaborating due to excessive
messages being sent to support following.

---------

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Thorsten Ball <mrnugget@gmail.com>

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-03-06 15:41:19 +01:00
Kirill Bulatov
1c9ba85146 Always resolve code action if needed (#8904)
Follow-up of https://github.com/zed-industries/zed/pull/8874 and
https://github.com/zed-industries/zed/pull/7635
Closes https://github.com/zed-industries/zed/issues/7609

* mentions all `lsp::CodeActions` properties in the Zed client resolve
capabilities to remove more json out of general actions request
potentially
* removes odd `CodeActions.data` field checks, as that field is opaque
and is intended to store data, needed by the langserver to resolve this
code action
* if any `CodeAction` lacks either `command` or `edits` fields, tries to
resolve the action

This all effectively causes Zed to always fire an action resolve
request, since we update actions list (replacing the resolved actions
with the new, unresolved ones) via `refresh_code_actions`

9e66d48ccd/crates/editor/src/editor.rs (L3650)
that is being called on selections change and the actions menu open.

Yet, we do not query the resolve until the action is either applied
(selected in the list), or called for formatting, so it seems to be fine
to resolve them always, as it's not a frequent operation such as
reacting to every keystroke.

Release Notes:

- Fixed certain code actions not being resolved properly ([7609](https://github.com/zed-industries/zed/issues/7609))

---------

Co-authored-by: Derrick Laird <swampdonk@gmail.com>
2024-03-06 09:02:58 +02:00
Conrad Irwin
80ebf5aff9 Fix panic in enclosing bracket ranges (#8870)
This function was operating in the wrong co-ordinate space (c.f. #8081)

Release Notes:

- Fixed a panic on `ctrl-m` in a multibuffer
2024-03-05 15:23:50 -07:00
Conrad Irwin
acd21f5c15 Upload crashes to collab directly (#8649)
This lets us run rustc_demangle on the backtrace, which helps the Slack
view significantly.

We're also now uploading files to digital ocean's S3 equivalent (with a
1 month expiry) instead of to Slack.

This PR paves the way for (but does not yet implement) sending this data
to clickhouse too.

Release Notes:

- N/A
2024-03-05 15:23:33 -07:00
Conrad Irwin
608ad1636d Revert "Upload crashes to collab directly (#8649)"
This reverts commit c05f8154be.
2024-03-05 15:22:50 -07:00
Conrad Irwin
c05f8154be Upload crashes to collab directly (#8649)
This lets us run rustc_demangle on the backtrace, which helps the Slack
view significantly.

We're also now uploading files to digital ocean's S3 equivalent (with a
1 month expiry) instead of to Slack.

This PR paves the way for (but does not yet implement) sending this data
to clickhouse too.

Release Notes:

- N/A
2024-03-04 17:07:58 -07:00
Kirill Bulatov
de71d7c0a0 zed 0.125.2 2024-03-04 20:08:08 +02:00
Jason Lee
da671d9771 Return "open in new window" as default in recent projects (#8798)
https://github.com/zed-industries/zed/assets/5518/8bbd13a7-9144-48b0-9bc8-6651725476f8

Closes https://github.com/zed-industries/zed/issues/8651

Reworks `recent_projects::OpenRecent` action with collab projects in mind:
* keep the "open in new window" behavior for corresponding menu and command entries
* use new, "reuse current window" behavior in the recent projects picker up in the toolbar

This way, old Zed behavior is not customizable, kept as original in all main use cases — so that projects shared via remote entities: a channel and a call, are never accidentally closed, breaking the sharing. 

Release Notes:

- Return "open in new window" as default in recent projects
2024-03-04 11:43:11 +02:00
Kirill Bulatov
993c001fc4 When clicking the checkbox, toggle open the LSP trace logs (#8689)
Before this change, enabling LSP trace checkbox closed the panel and
toggled the server logs on.
Now, the newly enabled trace logs are shown instead.

Release Notes:

- Improved LSP logs checkbox behavior
2024-03-02 02:04:03 +02:00
Kirill Bulatov
7e501f6c1b Add a way to change what menu::Confirm does in the recent projects modal (#8688)
Follow-up of
https://github.com/zed-industries/zed/issues/8651#issuecomment-1973411072

Zed current default is still to reuse the current window, but now it's
possible to do
```json
"alt-cmd-o": [
  "projects::OpenRecent",
  {
    "create_new_window": true
  }
]
```
and change this.

menu::Secondary confirm does the action with opposite window creation
strategy.

Release Notes:

- Improved open recent projects flexibility: settings can change whether
`menu::Confirm` opens a new window or reuses the old one
2024-03-02 00:32:59 +02:00
Kirill Bulatov
3a0f842f3d Prompt to save files on recent project selection (#8673) 2024-03-01 22:01:02 +02:00
Conrad Irwin
38564c5ab2 zed 0.125.1 2024-02-29 10:35:20 -07:00
gcp-cherry-pick-bot[bot]
023498ef53
Ensure panel and pane sizes are integral (cherry-pick #8619) (#8620)
Cherry-picked Ensure panel and pane sizes are integral (#8619)

Fixes: #8050

For some reason that we didn't investigate, if you have view caching
enabled,
and you have non-integer sized bounds, and you are right aligning
things, the
co-ordinates can differ by +/- 1px when using the cached view.

The easiest fix for now is to just not do that.

Co-Authored-By: Antonio <as-cii@zed.dev>

Release Notes:

- Fixed the pane icons flickering
([#8050](https://github.com/zed-industries/zed/issues/8050)).

Co-authored-by: Antonio <as-cii@zed.dev>

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Antonio <as-cii@zed.dev>
2024-02-29 10:03:11 -07:00
gcp-cherry-pick-bot[bot]
1f61804ae0
Revert "Introduce a new ToggleGraphicsProfiler command (#7607)" (cherry-pick #8567) (#8569)
Cherry-picked Revert "Introduce a new `ToggleGraphicsProfiler` command
(#7607)" (#8567)

This reverts commit 0cebf68306.

Although this thing is very cool, it is a top source of crashes.

Example crash:
```
Segmentation fault: 11 on thread 26
  objc_retain +16
  invocation function for block in Overlay::onCommandBufferCommit(id<MTLCommandBuffer>) +60
  MTLDispatchListApply +52
```

Release Notes:

- Removed "Toggle Graphics Profiler" as it crashes too much.

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-02-28 19:06:09 -07:00
gcp-cherry-pick-bot[bot]
d229b39621
Avoid an unwrap when loading languages (cherry-pick #8562) (#8566)
Cherry-picked Avoid an unwrap when loading languages (#8562)

We couldn't reproduce the panic, but I believe it was possible when
uninstalling an extension while one if its grammars was still loading.

Release Notes:
- Fixed a crash that could happen when uninstalling a language extension
while its grammar was loading.

---------

Co-authored-by: Conrad <conrad@zed.dev>

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Conrad <conrad@zed.dev>
2024-02-28 15:51:52 -07:00
Joseph T. Lyons
ae2326c781 Fix view release notes locally (#8553)
🤦‍♂️

Release Notes:

- N/A
2024-02-28 15:50:41 -05:00
Joseph T. Lyons
c4d1855824 v0.125.x preview 2024-02-28 13:13:28 -05:00
44 changed files with 586 additions and 210 deletions

8
Cargo.lock generated
View file

@ -6374,6 +6374,7 @@ dependencies = [
name = "picker" name = "picker"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"ctor", "ctor",
"editor", "editor",
"env_logger", "env_logger",
@ -7143,11 +7144,16 @@ dependencies = [
name = "recent_projects" name = "recent_projects"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"editor",
"fuzzy", "fuzzy",
"gpui", "gpui",
"language",
"menu", "menu",
"ordered-float 2.10.0", "ordered-float 2.10.0",
"picker", "picker",
"project",
"serde",
"serde_json",
"smol", "smol",
"ui", "ui",
"util", "util",
@ -11603,7 +11609,7 @@ dependencies = [
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.125.0" version = "0.125.4"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"anyhow", "anyhow",

View file

@ -341,6 +341,13 @@
{ {
"context": "Workspace", "context": "Workspace",
"bindings": { "bindings": {
// Change the default action on `menu::Confirm` by setting the parameter
// "alt-cmd-o": [
// "projects::OpenRecent",
// {
// "create_new_window": true
// }
// ]
"ctrl-alt-o": "projects::OpenRecent", "ctrl-alt-o": "projects::OpenRecent",
"ctrl-alt-b": "branches::OpenRecent", "ctrl-alt-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal", "ctrl-~": "workspace::NewTerminal",

View file

@ -383,6 +383,13 @@
{ {
"context": "Workspace", "context": "Workspace",
"bindings": { "bindings": {
// Change the default action on `menu::Confirm` by setting the parameter
// "alt-cmd-o": [
// "projects::OpenRecent",
// {
// "create_new_window": true
// }
// ]
"alt-cmd-o": "projects::OpenRecent", "alt-cmd-o": "projects::OpenRecent",
"alt-cmd-b": "branches::OpenRecent", "alt-cmd-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal", "ctrl-~": "workspace::NewTerminal",

View file

@ -20,7 +20,7 @@ use smol::io::AsyncReadExt;
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use smol::{fs::File, process::Command}; use smol::{fs::File, process::Command};
use release_channel::{AppCommitSha, ReleaseChannel}; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use std::{ use std::{
env::consts::{ARCH, OS}, env::consts::{ARCH, OS},
ffi::OsString, ffi::OsString,
@ -190,7 +190,7 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<(
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) { fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
let release_channel = ReleaseChannel::global(cx); let release_channel = ReleaseChannel::global(cx);
let version = env!("CARGO_PKG_VERSION"); let version = AppVersion::global(cx).to_string();
let client = client::Client::global(cx).http_client(); let client = client::Client::global(cx).http_client();
let url = client.build_url(&format!( let url = client.build_url(&format!(

View file

@ -373,8 +373,10 @@ async fn test_basic_following(
editor_a1.update(cx_a, |editor, cx| { editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
}); });
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked(); executor.run_until_parked();
cx_b.background_executor.run_until_parked(); cx_b.background_executor.run_until_parked();
editor_b1.update(cx_b, |editor, cx| { editor_b1.update(cx_b, |editor, cx| {
assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
}); });
@ -387,6 +389,7 @@ async fn test_basic_following(
editor.change_selections(None, cx, |s| s.select_ranges([3..3])); editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
editor.set_scroll_position(point(0., 100.), cx); editor.set_scroll_position(point(0., 100.), cx);
}); });
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked(); executor.run_until_parked();
editor_b1.update(cx_b, |editor, cx| { editor_b1.update(cx_b, |editor, cx| {
assert_eq!(editor.selections.ranges(cx), &[3..3]); assert_eq!(editor.selections.ranges(cx), &[3..3]);
@ -1600,6 +1603,8 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
editor_a.update(cx_a, |editor, cx| { editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([1..1])) editor.change_selections(None, cx, |s| s.select_ranges([1..1]))
}); });
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
cx_a.run_until_parked(); cx_a.run_until_parked();
editor_b.update(cx_b, |editor, cx| { editor_b.update(cx_b, |editor, cx| {
assert_eq!(editor.selections.ranges(cx), vec![1..1]) assert_eq!(editor.selections.ranges(cx), vec![1..1])
@ -1618,6 +1623,8 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
editor_a.update(cx_a, |editor, cx| { editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([2..2])) editor.change_selections(None, cx, |s| s.select_ranges([2..2]))
}); });
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
cx_a.run_until_parked(); cx_a.run_until_parked();
editor_b.update(cx_b, |editor, cx| { editor_b.update(cx_b, |editor, cx| {
assert_eq!(editor.selections.ranges(cx), vec![1..1]) assert_eq!(editor.selections.ranges(cx), vec![1..1])
@ -1722,6 +1729,7 @@ async fn test_following_into_excluded_file(
// When client B starts following client A, currently visible file is replicated // When client B starts following client A, currently visible file is replicated
workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx)); workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked(); executor.run_until_parked();
let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| { let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
@ -1743,6 +1751,7 @@ async fn test_following_into_excluded_file(
editor_for_excluded_a.update(cx_a, |editor, cx| { editor_for_excluded_a.update(cx_a, |editor, cx| {
editor.select_right(&Default::default(), cx); editor.select_right(&Default::default(), cx);
}); });
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked(); executor.run_until_parked();
// Changes from B to the excluded file are replicated in A's editor // Changes from B to the excluded file are replicated in A's editor

View file

@ -204,7 +204,7 @@ impl ChatPanel {
let panel = Self::new(workspace, cx); let panel = Self::new(workspace, cx);
if let Some(serialized_panel) = serialized_panel { if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width; panel.width = serialized_panel.width.map(|r| r.round());
cx.notify(); cx.notify();
}); });
} }

View file

@ -327,7 +327,7 @@ impl CollabPanel {
let panel = CollabPanel::new(workspace, cx); let panel = CollabPanel::new(workspace, cx);
if let Some(serialized_panel) = serialized_panel { if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width; panel.width = serialized_panel.width.map(|w| w.round());
panel.collapsed_channels = serialized_panel panel.collapsed_channels = serialized_panel
.collapsed_channels .collapsed_channels
.unwrap_or_else(|| Vec::new()) .unwrap_or_else(|| Vec::new())

View file

@ -183,7 +183,7 @@ impl NotificationPanel {
let panel = Self::new(workspace, cx); let panel = Self::new(workspace, cx);
if let Some(serialized_panel) = serialized_panel { if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width; panel.width = serialized_panel.width.map(|w| w.round());
cx.notify(); cx.notify();
}); });
} }

View file

@ -195,8 +195,8 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>); fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool; fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
fn draw(&self, scene: &Scene); fn draw(&self, scene: &Scene);
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>; fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
fn set_graphics_profiler_enabled(&self, enabled: bool);
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
fn as_test(&mut self) -> Option<&mut TestWindow> { fn as_test(&mut self) -> Option<&mut TestWindow> {

View file

@ -396,10 +396,6 @@ impl PlatformWindow for WaylandWindow {
let inner = self.0.inner.lock(); let inner = self.0.inner.lock();
inner.renderer.sprite_atlas().clone() inner.renderer.sprite_atlas().clone()
} }
fn set_graphics_profiler_enabled(&self, enabled: bool) {
//todo!(linux)
}
} }
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]

View file

@ -513,8 +513,4 @@ impl PlatformWindow for X11Window {
let inner = self.0.inner.lock(); let inner = self.0.inner.lock();
inner.renderer.sprite_atlas().clone() inner.renderer.sprite_atlas().clone()
} }
fn set_graphics_profiler_enabled(&self, enabled: bool) {
unimplemented!("linux")
}
} }

View file

@ -1060,27 +1060,6 @@ impl PlatformWindow for MacWindow {
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> { fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
self.0.lock().renderer.sprite_atlas().clone() self.0.lock().renderer.sprite_atlas().clone()
} }
/// Enables or disables the Metal HUD for debugging purposes. Note that this only works
/// when the app is bundled and it has the `MetalHudEnabled` key set to true in Info.plist.
fn set_graphics_profiler_enabled(&self, enabled: bool) {
let this_lock = self.0.lock();
let layer = this_lock.renderer.layer();
unsafe {
if enabled {
let hud_properties = NSDictionary::dictionaryWithObject_forKey_(
nil,
ns_string("default"),
ns_string("mode"),
);
let _: () = msg_send![layer, setDeveloperHUDProperties: hud_properties];
} else {
let _: () =
msg_send![layer, setDeveloperHUDProperties: NSDictionary::dictionary(nil)];
}
}
}
} }
impl HasWindowHandle for MacWindow { impl HasWindowHandle for MacWindow {

View file

@ -251,8 +251,6 @@ impl PlatformWindow for TestWindow {
self.0.lock().sprite_atlas.clone() self.0.lock().sprite_atlas.clone()
} }
fn set_graphics_profiler_enabled(&self, _enabled: bool) {}
fn as_test(&mut self) -> Option<&mut TestWindow> { fn as_test(&mut self) -> Option<&mut TestWindow> {
Some(self) Some(self)
} }

View file

@ -46,10 +46,10 @@ pub fn run_test(
let starting_seed = env::var("SEED") let starting_seed = env::var("SEED")
.map(|seed| seed.parse().expect("invalid SEED variable")) .map(|seed| seed.parse().expect("invalid SEED variable"))
.unwrap_or(0); .unwrap_or(0);
let is_randomized = num_iterations > 1;
if let Ok(iterations) = env::var("ITERATIONS") { if let Ok(iterations) = env::var("ITERATIONS") {
num_iterations = iterations.parse().expect("invalid ITERATIONS variable"); num_iterations = iterations.parse().expect("invalid ITERATIONS variable");
} }
let is_randomized = num_iterations > 1;
for seed in starting_seed..starting_seed + num_iterations { for seed in starting_seed..starting_seed + num_iterations {
let mut retry = 0; let mut retry = 0;

View file

@ -280,7 +280,6 @@ pub struct Window {
pub(crate) focus: Option<FocusId>, pub(crate) focus: Option<FocusId>,
focus_enabled: bool, focus_enabled: bool,
pending_input: Option<PendingInput>, pending_input: Option<PendingInput>,
graphics_profiler_enabled: bool,
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
@ -474,7 +473,6 @@ impl Window {
focus: None, focus: None,
focus_enabled: true, focus_enabled: true,
pending_input: None, pending_input: None,
graphics_profiler_enabled: false,
} }
} }
fn new_focus_listener( fn new_focus_listener(
@ -1519,14 +1517,6 @@ impl<'a> WindowContext<'a> {
} }
} }
/// Toggle the graphics profiler to debug your application's rendering performance.
pub fn toggle_graphics_profiler(&mut self) {
self.window.graphics_profiler_enabled = !self.window.graphics_profiler_enabled;
self.window
.platform_window
.set_graphics_profiler_enabled(self.window.graphics_profiler_enabled);
}
/// Register the given handler to be invoked whenever the global of the given type /// Register the given handler to be invoked whenever the global of the given type
/// is updated. /// is updated.
pub fn observe_global<G: Global>( pub fn observe_global<G: Global>(

View file

@ -97,14 +97,14 @@ fn test_select_language() {
// matching file extension // matching file extension
assert_eq!( assert_eq!(
registry registry
.language_for_file("zed/lib.rs", None) .language_for_file("zed/lib.rs".as_ref(), None)
.now_or_never() .now_or_never()
.and_then(|l| Some(l.ok()?.name())), .and_then(|l| Some(l.ok()?.name())),
Some("Rust".into()) Some("Rust".into())
); );
assert_eq!( assert_eq!(
registry registry
.language_for_file("zed/lib.mk", None) .language_for_file("zed/lib.mk".as_ref(), None)
.now_or_never() .now_or_never()
.and_then(|l| Some(l.ok()?.name())), .and_then(|l| Some(l.ok()?.name())),
Some("Make".into()) Some("Make".into())
@ -113,7 +113,7 @@ fn test_select_language() {
// matching filename // matching filename
assert_eq!( assert_eq!(
registry registry
.language_for_file("zed/Makefile", None) .language_for_file("zed/Makefile".as_ref(), None)
.now_or_never() .now_or_never()
.and_then(|l| Some(l.ok()?.name())), .and_then(|l| Some(l.ok()?.name())),
Some("Make".into()) Some("Make".into())
@ -122,21 +122,21 @@ fn test_select_language() {
// matching suffix that is not the full file extension or filename // matching suffix that is not the full file extension or filename
assert_eq!( assert_eq!(
registry registry
.language_for_file("zed/cars", None) .language_for_file("zed/cars".as_ref(), None)
.now_or_never() .now_or_never()
.and_then(|l| Some(l.ok()?.name())), .and_then(|l| Some(l.ok()?.name())),
None None
); );
assert_eq!( assert_eq!(
registry registry
.language_for_file("zed/a.cars", None) .language_for_file("zed/a.cars".as_ref(), None)
.now_or_never() .now_or_never()
.and_then(|l| Some(l.ok()?.name())), .and_then(|l| Some(l.ok()?.name())),
None None
); );
assert_eq!( assert_eq!(
registry registry
.language_for_file("zed/sumk", None) .language_for_file("zed/sumk".as_ref(), None)
.now_or_never() .now_or_never()
.and_then(|l| Some(l.ok()?.name())), .and_then(|l| Some(l.ok()?.name())),
None None

View file

@ -1522,16 +1522,16 @@ mod tests {
}); });
languages languages
.language_for_file("the/script", None) .language_for_file("the/script".as_ref(), None)
.await .await
.unwrap_err(); .unwrap_err();
languages languages
.language_for_file("the/script", Some(&"nothing".into())) .language_for_file("the/script".as_ref(), Some(&"nothing".into()))
.await .await
.unwrap_err(); .unwrap_err();
assert_eq!( assert_eq!(
languages languages
.language_for_file("the/script", Some(&"#!/bin/env node".into())) .language_for_file("the/script".as_ref(), Some(&"#!/bin/env node".into()))
.await .await
.unwrap() .unwrap()
.name() .name()

View file

@ -7,7 +7,7 @@ use collections::{hash_map, HashMap};
use futures::{ use futures::{
channel::{mpsc, oneshot}, channel::{mpsc, oneshot},
future::Shared, future::Shared,
FutureExt as _, TryFutureExt as _, Future, FutureExt as _, TryFutureExt as _,
}; };
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task}; use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
use lsp::{LanguageServerBinary, LanguageServerId}; use lsp::{LanguageServerBinary, LanguageServerId};
@ -24,7 +24,7 @@ use sum_tree::Bias;
use text::{Point, Rope}; use text::{Point, Rope};
use theme::Theme; use theme::Theme;
use unicase::UniCase; use unicase::UniCase;
use util::{paths::PathExt, post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; use util::{paths::PathExt, post_inc, ResultExt};
pub struct LanguageRegistry { pub struct LanguageRegistry {
state: RwLock<LanguageRegistryState>, state: RwLock<LanguageRegistryState>,
@ -291,35 +291,36 @@ impl LanguageRegistry {
pub fn language_for_name( pub fn language_for_name(
self: &Arc<Self>, self: &Arc<Self>,
name: &str, name: &str,
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> { ) -> impl Future<Output = Result<Arc<Language>>> {
let name = UniCase::new(name); let name = UniCase::new(name);
self.get_or_load_language(|language_name, _| UniCase::new(language_name) == name) let rx = self.get_or_load_language(|language_name, _| UniCase::new(language_name) == name);
async move { rx.await? }
} }
pub fn language_for_name_or_extension( pub fn language_for_name_or_extension(
self: &Arc<Self>, self: &Arc<Self>,
string: &str, string: &str,
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> { ) -> impl Future<Output = Result<Arc<Language>>> {
let string = UniCase::new(string); let string = UniCase::new(string);
self.get_or_load_language(|name, config| { let rx = self.get_or_load_language(|name, config| {
UniCase::new(name) == string UniCase::new(name) == string
|| config || config
.path_suffixes .path_suffixes
.iter() .iter()
.any(|suffix| UniCase::new(suffix) == string) .any(|suffix| UniCase::new(suffix) == string)
}) });
async move { rx.await? }
} }
pub fn language_for_file( pub fn language_for_file(
self: &Arc<Self>, self: &Arc<Self>,
path: impl AsRef<Path>, path: &Path,
content: Option<&Rope>, content: Option<&Rope>,
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> { ) -> impl Future<Output = Result<Arc<Language>>> {
let path = path.as_ref();
let filename = path.file_name().and_then(|name| name.to_str()); let filename = path.file_name().and_then(|name| name.to_str());
let extension = path.extension_or_hidden_file_name(); let extension = path.extension_or_hidden_file_name();
let path_suffixes = [extension, filename]; let path_suffixes = [extension, filename];
self.get_or_load_language(|_, config| { let rx = self.get_or_load_language(move |_, config| {
let path_matches = config let path_matches = config
.path_suffixes .path_suffixes
.iter() .iter()
@ -334,13 +335,14 @@ impl LanguageRegistry {
}, },
); );
path_matches || content_matches path_matches || content_matches
}) });
async move { rx.await? }
} }
fn get_or_load_language( fn get_or_load_language(
self: &Arc<Self>, self: &Arc<Self>,
callback: impl Fn(&str, &LanguageMatcher) -> bool, callback: impl Fn(&str, &LanguageMatcher) -> bool,
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> { ) -> oneshot::Receiver<Result<Arc<Language>>> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
let mut state = self.state.write(); let mut state = self.state.write();
@ -421,13 +423,13 @@ impl LanguageRegistry {
let _ = tx.send(Err(anyhow!("executor does not exist"))); let _ = tx.send(Err(anyhow!("executor does not exist")));
} }
rx.unwrap() rx
} }
fn get_or_load_grammar( fn get_or_load_grammar(
self: &Arc<Self>, self: &Arc<Self>,
name: Arc<str>, name: Arc<str>,
) -> UnwrapFuture<oneshot::Receiver<Result<tree_sitter::Language>>> { ) -> impl Future<Output = Result<tree_sitter::Language>> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
let mut state = self.state.write(); let mut state = self.state.write();
@ -483,7 +485,7 @@ impl LanguageRegistry {
tx.send(Err(anyhow!("no such grammar {}", name))).ok(); tx.send(Err(anyhow!("no such grammar {}", name))).ok();
} }
rx.unwrap() async move { rx.await? }
} }
pub fn to_vec(&self) -> Vec<Arc<Language>> { pub fn to_vec(&self) -> Vec<Arc<Language>> {

View file

@ -823,7 +823,7 @@ impl Render for LspLogToolbarItemView {
selection, selection,
Selection::Selected Selection::Selected
); );
view.toggle_logging_for_server( view.toggle_rpc_logging_for_server(
row.server_id, row.server_id,
enabled, enabled,
cx, cx,
@ -887,7 +887,7 @@ impl LspLogToolbarItemView {
} }
} }
fn toggle_logging_for_server( fn toggle_rpc_logging_for_server(
&mut self, &mut self,
id: LanguageServerId, id: LanguageServerId,
enabled: bool, enabled: bool,
@ -899,6 +899,9 @@ impl LspLogToolbarItemView {
if !enabled && Some(id) == log_view.current_server_id { if !enabled && Some(id) == log_view.current_server_id {
log_view.show_logs_for_server(id, cx); log_view.show_logs_for_server(id, cx);
cx.notify(); cx.notify();
} else if enabled {
log_view.show_rpc_trace_for_server(id, cx);
cx.notify();
} }
cx.focus(&log_view.focus_handle); cx.focus(&log_view.focus_handle);
}); });

View file

@ -20,7 +20,7 @@ use std::{
use util::{ use util::{
async_maybe, async_maybe,
fs::remove_matching, fs::remove_matching,
github::{latest_github_release, GitHubLspBinaryVersion}, github::{github_release_with_tag, GitHubLspBinaryVersion},
ResultExt, ResultExt,
}; };
@ -291,13 +291,11 @@ impl LspAdapter for EsLintLspAdapter {
&self, &self,
delegate: &dyn LspAdapterDelegate, delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> { ) -> Result<Box<dyn 'static + Send + Any>> {
// At the time of writing the latest vscode-eslint release was released in 2020 and requires // We're using this hardcoded release tag, because ESLint's API changed with
// special custom LSP protocol extensions be handled to fully initialize. Download the latest // >= 2.3 and we haven't upgraded yet.
// prerelease instead to sidestep this issue let release = github_release_with_tag(
let release = latest_github_release(
"microsoft/vscode-eslint", "microsoft/vscode-eslint",
false, "release/2.2.20-Insider",
true,
delegate.http_client(), delegate.http_client(),
) )
.await?; .await?;

View file

@ -541,7 +541,14 @@ impl LanguageServer {
}), }),
data_support: Some(true), data_support: Some(true),
resolve_support: Some(CodeActionCapabilityResolveSupport { resolve_support: Some(CodeActionCapabilityResolveSupport {
properties: vec!["edit".to_string(), "command".to_string()], properties: vec![
"kind".to_string(),
"diagnostics".to_string(),
"isPreferred".to_string(),
"disabled".to_string(),
"edit".to_string(),
"command".to_string(),
],
}), }),
..Default::default() ..Default::default()
}), }),

View file

@ -195,7 +195,7 @@ struct Excerpt {
/// ///
/// Contains methods for getting the [`Buffer`] of the excerpt, /// Contains methods for getting the [`Buffer`] of the excerpt,
/// as well as mapping offsets to/from buffer and multibuffer coordinates. /// as well as mapping offsets to/from buffer and multibuffer coordinates.
#[derive(Copy, Clone)] #[derive(Clone)]
pub struct MultiBufferExcerpt<'a> { pub struct MultiBufferExcerpt<'a> {
excerpt: &'a Excerpt, excerpt: &'a Excerpt,
excerpt_offset: usize, excerpt_offset: usize,
@ -2967,7 +2967,16 @@ impl MultiBufferSnapshot {
excerpt excerpt
.buffer() .buffer()
.enclosing_bracket_ranges(excerpt.map_range_to_buffer(range)) .enclosing_bracket_ranges(excerpt.map_range_to_buffer(range))
.filter(move |(open, close)| excerpt.contains_buffer_range(open.start..close.end)), .filter_map(move |(open, close)| {
if excerpt.contains_buffer_range(open.start..close.end) {
Some((
excerpt.map_range_from_buffer(open),
excerpt.map_range_from_buffer(close),
))
} else {
None
}
}),
) )
} }

View file

@ -10,6 +10,7 @@ path = "src/picker.rs"
doctest = false doctest = false
[dependencies] [dependencies]
anyhow.workspace = true
editor.workspace = true editor.workspace = true
gpui.workspace = true gpui.workspace = true
menu.workspace = true menu.workspace = true

View file

@ -1,3 +1,4 @@
use anyhow::Result;
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
div, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent, DismissEvent, div, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent, DismissEvent,
@ -13,11 +14,16 @@ enum ElementContainer {
UniformList(UniformListScrollHandle), UniformList(UniformListScrollHandle),
} }
struct PendingUpdateMatches {
delegate_update_matches: Option<Task<()>>,
_task: Task<Result<()>>,
}
pub struct Picker<D: PickerDelegate> { pub struct Picker<D: PickerDelegate> {
pub delegate: D, pub delegate: D,
element_container: ElementContainer, element_container: ElementContainer,
editor: View<Editor>, editor: View<Editor>,
pending_update_matches: Option<Task<()>>, pending_update_matches: Option<PendingUpdateMatches>,
confirm_on_update: Option<bool>, confirm_on_update: Option<bool>,
width: Option<Length>, width: Option<Length>,
max_height: Option<Length>, max_height: Option<Length>,
@ -268,15 +274,32 @@ impl<D: PickerDelegate> Picker<D> {
} }
pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) { pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
let update = self.delegate.update_matches(query, cx); let delegate_pending_update_matches = self.delegate.update_matches(query, cx);
self.matches_updated(cx); self.matches_updated(cx);
self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move { // This struct ensures that we can synchronously drop the task returned by the
update.await; // delegate's `update_matches` method and the task that the picker is spawning.
this.update(&mut cx, |this, cx| { // If we simply capture the delegate's task into the picker's task, when the picker's
this.matches_updated(cx); // task gets synchronously dropped, the delegate's task would keep running until
}) // the picker's task has a chance of being scheduled, because dropping a task happens
.ok(); // asynchronously.
})); self.pending_update_matches = Some(PendingUpdateMatches {
delegate_update_matches: Some(delegate_pending_update_matches),
_task: cx.spawn(|this, mut cx| async move {
let delegate_pending_update_matches = this.update(&mut cx, |this, _| {
this.pending_update_matches
.as_mut()
.unwrap()
.delegate_update_matches
.take()
.unwrap()
})?;
delegate_pending_update_matches.await;
this.update(&mut cx, |this, cx| {
this.matches_updated(cx);
})
}),
});
} }
fn matches_updated(&mut self, cx: &mut ViewContext<Self>) { fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {

View file

@ -1818,6 +1818,19 @@ impl LspCommand for GetCodeActions {
} }
} }
impl GetCodeActions {
pub fn can_resolve_actions(capabilities: &ServerCapabilities) -> bool {
capabilities
.code_action_provider
.as_ref()
.and_then(|options| match options {
lsp::CodeActionProviderCapability::Simple(_is_supported) => None,
lsp::CodeActionProviderCapability::Options(options) => options.resolve_provider,
})
.unwrap_or(false)
}
}
#[async_trait(?Send)] #[async_trait(?Send)]
impl LspCommand for OnTypeFormatting { impl LspCommand for OnTypeFormatting {
type Response = Option<Transaction>; type Response = Option<Transaction>;

View file

@ -35,11 +35,11 @@ use language::{
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
serialize_anchor, serialize_version, split_operations, serialize_anchor, serialize_version, split_operations,
}, },
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, CodeAction,
CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation,
Documentation, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
LocalFile, LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, LspAdapterDelegate, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, ToOffset, ToPointUtf16, Transaction, Unclipped,
}; };
use log::error; use log::error;
use lsp::{ use lsp::{
@ -4235,7 +4235,10 @@ impl Project {
})? })?
.await?; .await?;
for action in actions { for mut action in actions {
Self::try_resolve_code_action(&language_server, &mut action)
.await
.context("resolving a formatting code action")?;
if let Some(edit) = action.lsp_action.edit { if let Some(edit) = action.lsp_action.edit {
if edit.changes.is_none() && edit.document_changes.is_none() { if edit.changes.is_none() && edit.document_changes.is_none() {
continue; continue;
@ -4255,6 +4258,7 @@ impl Project {
project_transaction.0.extend(new.0); project_transaction.0.extend(new.0);
} }
// TODO kb here too:
if let Some(command) = action.lsp_action.command { if let Some(command) = action.lsp_action.command {
project.update(&mut cx, |this, _| { project.update(&mut cx, |this, _| {
this.last_workspace_edits_by_language_server this.last_workspace_edits_by_language_server
@ -5296,33 +5300,10 @@ impl Project {
} else { } else {
return Task::ready(Ok(Default::default())); return Task::ready(Ok(Default::default()));
}; };
let range = action.range.to_point_utf16(buffer);
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
if let Some(lsp_range) = action Self::try_resolve_code_action(&lang_server, &mut action)
.lsp_action .await
.data .context("resolving a code action")?;
.as_mut()
.and_then(|d| d.get_mut("codeActionParams"))
.and_then(|d| d.get_mut("range"))
{
*lsp_range = serde_json::to_value(&range_to_lsp(range)).unwrap();
action.lsp_action = lang_server
.request::<lsp::request::CodeActionResolveRequest>(action.lsp_action)
.await?;
} else {
let actions = this
.update(&mut cx, |this, cx| {
this.code_actions(&buffer_handle, action.range, cx)
})?
.await?;
action.lsp_action = actions
.into_iter()
.find(|a| a.lsp_action.title == action.lsp_action.title)
.ok_or_else(|| anyhow!("code action is outdated"))?
.lsp_action;
}
if let Some(edit) = action.lsp_action.edit { if let Some(edit) = action.lsp_action.edit {
if edit.changes.is_some() || edit.document_changes.is_some() { if edit.changes.is_some() || edit.document_changes.is_some() {
return Self::deserialize_workspace_edit( return Self::deserialize_workspace_edit(
@ -8143,6 +8124,23 @@ impl Project {
}) })
} }
async fn try_resolve_code_action(
lang_server: &LanguageServer,
action: &mut CodeAction,
) -> anyhow::Result<()> {
if GetCodeActions::can_resolve_actions(&lang_server.capabilities()) {
if action.lsp_action.data.is_some()
&& (action.lsp_action.command.is_none() || action.lsp_action.edit.is_none())
{
action.lsp_action = lang_server
.request::<lsp::request::CodeActionResolveRequest>(action.lsp_action.clone())
.await?;
}
}
anyhow::Ok(())
}
async fn handle_refresh_inlay_hints( async fn handle_refresh_inlay_hints(
this: Model<Self>, this: Model<Self>,
_: TypedEnvelope<proto::RefreshInlayHints>, _: TypedEnvelope<proto::RefreshInlayHints>,

View file

@ -2564,7 +2564,20 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
}, },
None, None,
); );
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; let mut fake_language_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
code_action_provider: Some(lsp::CodeActionProviderCapability::Options(
lsp::CodeActionOptions {
resolve_provider: Some(true),
..lsp::CodeActionOptions::default()
},
)),
..lsp::ServerCapabilities::default()
},
..FakeLspAdapter::default()
}))
.await;
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
@ -2591,16 +2604,14 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
Ok(Some(vec![ Ok(Some(vec![
lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
title: "The code action".into(), title: "The code action".into(),
command: Some(lsp::Command { data: Some(serde_json::json!({
title: "The command".into(), "command": "_the/command",
command: "_the/command".into(), })),
arguments: Some(vec![json!("the-argument")]), ..lsp::CodeAction::default()
}),
..Default::default()
}), }),
lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
title: "two".into(), title: "two".into(),
..Default::default() ..lsp::CodeAction::default()
}), }),
])) ]))
}) })
@ -2615,7 +2626,16 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
// Resolving the code action does not populate its edits. In absence of // Resolving the code action does not populate its edits. In absence of
// edits, we must execute the given command. // edits, we must execute the given command.
fake_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>( fake_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
|action, _| async move { Ok(action) }, |mut action, _| async move {
if action.data.is_some() {
action.command = Some(lsp::Command {
title: "The command".into(),
command: "_the/command".into(),
arguments: Some(vec![json!("the-argument")]),
});
}
Ok(action)
},
); );
// While executing the command, the language server sends the editor // While executing the command, the language server sends the editor

View file

@ -338,7 +338,7 @@ impl ProjectPanel {
let panel = ProjectPanel::new(workspace, cx); let panel = ProjectPanel::new(workspace, cx);
if let Some(serialized_panel) = serialized_panel { if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width; panel.width = serialized_panel.width.map(|px| px.round());
cx.notify(); cx.notify();
}); });
} }

View file

@ -15,7 +15,15 @@ gpui.workspace = true
menu.workspace = true menu.workspace = true
ordered-float.workspace = true ordered-float.workspace = true
picker.workspace = true picker.workspace = true
serde.workspace = true
smol.workspace = true smol.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
workspace.workspace = true workspace.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
serde_json.workspace = true
workspace = { workspace = true, features = ["test-support"] }

View file

@ -8,12 +8,23 @@ use gpui::{
use highlighted_workspace_location::HighlightedWorkspaceLocation; use highlighted_workspace_location::HighlightedWorkspaceLocation;
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use serde::Deserialize;
use std::sync::Arc; use std::sync::Arc;
use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpacing, Tooltip}; use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpacing, Tooltip};
use util::paths::PathExt; use util::paths::PathExt;
use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB}; use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB};
gpui::actions!(projects, [OpenRecent]); #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct OpenRecent {
#[serde(default = "default_create_new_window")]
pub create_new_window: bool,
}
fn default_create_new_window() -> bool {
true
}
gpui::impl_actions!(projects, [OpenRecent]);
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.observe_new_views(RecentProjects::register).detach(); cx.observe_new_views(RecentProjects::register).detach();
@ -63,9 +74,9 @@ impl RecentProjects {
} }
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) { fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|workspace, _: &OpenRecent, cx| { workspace.register_action(|workspace, open_recent: &OpenRecent, cx| {
let Some(recent_projects) = workspace.active_modal::<Self>(cx) else { let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
if let Some(handler) = Self::open(workspace, cx) { if let Some(handler) = Self::open(workspace, open_recent.create_new_window, cx) {
handler.detach_and_log_err(cx); handler.detach_and_log_err(cx);
} }
return; return;
@ -79,12 +90,17 @@ impl RecentProjects {
}); });
} }
fn open(_: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<Task<Result<()>>> { fn open(
_: &mut Workspace,
create_new_window: bool,
cx: &mut ViewContext<Workspace>,
) -> Option<Task<Result<()>>> {
Some(cx.spawn(|workspace, mut cx| async move { Some(cx.spawn(|workspace, mut cx| async move {
workspace.update(&mut cx, |workspace, cx| { workspace.update(&mut cx, |workspace, cx| {
let weak_workspace = cx.view().downgrade(); let weak_workspace = cx.view().downgrade();
workspace.toggle_modal(cx, |cx| { workspace.toggle_modal(cx, |cx| {
let delegate = RecentProjectsDelegate::new(weak_workspace, true); let delegate =
RecentProjectsDelegate::new(weak_workspace, create_new_window, true);
let modal = Self::new(delegate, 34., cx); let modal = Self::new(delegate, 34., cx);
modal modal
@ -95,7 +111,13 @@ impl RecentProjects {
} }
pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> { pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx)) cx.new_view(|cx| {
Self::new(
RecentProjectsDelegate::new(workspace, false, false),
20.,
cx,
)
})
} }
} }
@ -126,17 +148,19 @@ pub struct RecentProjectsDelegate {
selected_match_index: usize, selected_match_index: usize,
matches: Vec<StringMatch>, matches: Vec<StringMatch>,
render_paths: bool, render_paths: bool,
create_new_window: bool,
// Flag to reset index when there is a new query vs not reset index when user delete an item // Flag to reset index when there is a new query vs not reset index when user delete an item
reset_selected_match_index: bool, reset_selected_match_index: bool,
} }
impl RecentProjectsDelegate { impl RecentProjectsDelegate {
fn new(workspace: WeakView<Workspace>, render_paths: bool) -> Self { fn new(workspace: WeakView<Workspace>, create_new_window: bool, render_paths: bool) -> Self {
Self { Self {
workspace, workspace,
workspaces: vec![], workspaces: vec![],
selected_match_index: 0, selected_match_index: 0,
matches: Default::default(), matches: Default::default(),
create_new_window,
render_paths, render_paths,
reset_selected_match_index: true, reset_selected_match_index: true,
} }
@ -147,10 +171,19 @@ impl PickerDelegate for RecentProjectsDelegate {
type ListItem = ListItem; type ListItem = ListItem;
fn placeholder_text(&self, cx: &mut WindowContext) -> Arc<str> { fn placeholder_text(&self, cx: &mut WindowContext) -> Arc<str> {
let (create_window, reuse_window) = if self.create_new_window {
(
cx.keystroke_text_for(&menu::Confirm),
cx.keystroke_text_for(&menu::SecondaryConfirm),
)
} else {
(
cx.keystroke_text_for(&menu::SecondaryConfirm),
cx.keystroke_text_for(&menu::Confirm),
)
};
Arc::from(format!( Arc::from(format!(
"{} reuses the window, {} opens a new one", "{reuse_window} reuses the window, {create_window} opens a new one",
cx.keystroke_text_for(&menu::Confirm),
cx.keystroke_text_for(&menu::SecondaryConfirm),
)) ))
} }
@ -219,15 +252,39 @@ impl PickerDelegate for RecentProjectsDelegate {
{ {
let (candidate_workspace_id, candidate_workspace_location) = let (candidate_workspace_id, candidate_workspace_location) =
&self.workspaces[selected_match.candidate_id]; &self.workspaces[selected_match.candidate_id];
let replace_current_window = !secondary; let replace_current_window = if self.create_new_window {
secondary
} else {
!secondary
};
workspace workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
if workspace.database_id() != *candidate_workspace_id { if workspace.database_id() != *candidate_workspace_id {
workspace.open_workspace_for_paths( let candidate_paths = candidate_workspace_location.paths().as_ref().clone();
replace_current_window, if replace_current_window {
candidate_workspace_location.paths().as_ref().clone(), cx.spawn(move |workspace, mut cx| async move {
cx, let continue_replacing = workspace
) .update(&mut cx, |workspace, cx| {
workspace.prepare_to_close(true, cx)
})?
.await?;
if continue_replacing {
workspace
.update(&mut cx, |workspace, cx| {
workspace.open_workspace_for_paths(
true,
candidate_paths,
cx,
)
})?
.await
} else {
Ok(())
}
})
} else {
workspace.open_workspace_for_paths(false, candidate_paths, cx)
}
} else { } else {
Task::ready(Ok(())) Task::ready(Ok(()))
} }
@ -360,3 +417,141 @@ impl Render for MatchTooltip {
}) })
} }
} }
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use editor::Editor;
use gpui::{TestAppContext, WindowHandle};
use project::Project;
use serde_json::json;
use workspace::{open_paths, AppState};
use super::*;
#[gpui::test]
async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/dir",
json!({
"main.ts": "a"
}),
)
.await;
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);
let workspace = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
workspace
.update(cx, |workspace, _| assert!(!workspace.is_edited()))
.unwrap();
let editor = workspace
.read_with(cx, |workspace, cx| {
workspace
.active_item(cx)
.unwrap()
.downcast::<Editor>()
.unwrap()
})
.unwrap();
workspace
.update(cx, |_, cx| {
editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
})
.unwrap();
workspace
.update(cx, |workspace, _| assert!(workspace.is_edited(), "After inserting more text into the editor without saving, we should have a dirty project"))
.unwrap();
let recent_projects_picker = open_recent_projects(&workspace, cx);
workspace
.update(cx, |_, cx| {
recent_projects_picker.update(cx, |picker, cx| {
assert_eq!(picker.query(cx), "");
let delegate = &mut picker.delegate;
delegate.matches = vec![StringMatch {
candidate_id: 0,
score: 1.0,
positions: Vec::new(),
string: "fake candidate".to_string(),
}];
delegate.workspaces = vec![(0, WorkspaceLocation::new(vec!["/test/path/"]))];
});
})
.unwrap();
assert!(
!cx.has_pending_prompt(),
"Should have no pending prompt on dirty project before opening the new recent project"
);
cx.dispatch_action((*workspace).into(), menu::Confirm);
workspace
.update(cx, |workspace, cx| {
assert!(
workspace.active_modal::<RecentProjects>(cx).is_none(),
"Should remove the modal after selecting new recent project"
)
})
.unwrap();
assert!(
cx.has_pending_prompt(),
"Dirty workspace should prompt before opening the new recent project"
);
// Cancel
cx.simulate_prompt_answer(0);
assert!(
!cx.has_pending_prompt(),
"Should have no pending prompt after cancelling"
);
workspace
.update(cx, |workspace, _| {
assert!(
workspace.is_edited(),
"Should be in the same dirty project after cancelling"
)
})
.unwrap();
}
fn open_recent_projects(
workspace: &WindowHandle<Workspace>,
cx: &mut TestAppContext,
) -> View<Picker<RecentProjectsDelegate>> {
cx.dispatch_action(
(*workspace).into(),
OpenRecent {
create_new_window: false,
},
);
workspace
.update(cx, |workspace, cx| {
workspace
.active_modal::<RecentProjects>(cx)
.unwrap()
.read(cx)
.picker
.clone()
})
.unwrap()
}
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.update(|cx| {
let state = AppState::test(cx);
language::init(cx);
crate::init(cx);
editor::init(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
state
})
}
}

View file

@ -315,10 +315,13 @@ impl Peer {
"incoming response: requester resumed" "incoming response: requester resumed"
); );
} else { } else {
let message_type = proto::build_typed_envelope(connection_id, incoming)
.map(|p| p.payload_type_name());
tracing::warn!( tracing::warn!(
%connection_id, %connection_id,
message_id, message_id,
responding_to, responding_to,
message_type,
"incoming response: unknown request" "incoming response: unknown request"
); );
} }

View file

@ -192,8 +192,8 @@ impl TerminalPanel {
let items = if let Some(serialized_panel) = serialized_panel.as_ref() { let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
cx.notify(); cx.notify();
panel.height = serialized_panel.height; panel.height = serialized_panel.height.map(|h| h.round());
panel.width = serialized_panel.width; panel.width = serialized_panel.width.map(|w| w.round());
panel.pane.update(cx, |_, cx| { panel.pane.update(cx, |_, cx| {
serialized_panel serialized_panel
.items .items

View file

@ -3,6 +3,7 @@ use anyhow::{anyhow, bail, Context, Result};
use futures::AsyncReadExt; use futures::AsyncReadExt;
use serde::Deserialize; use serde::Deserialize;
use std::sync::Arc; use std::sync::Arc;
use url::Url;
pub struct GitHubLspBinaryVersion { pub struct GitHubLspBinaryVersion {
pub name: String, pub name: String,
@ -74,3 +75,70 @@ pub async fn latest_github_release(
.find(|release| release.pre_release == pre_release) .find(|release| release.pre_release == pre_release)
.ok_or(anyhow!("Failed to find a release")) .ok_or(anyhow!("Failed to find a release"))
} }
pub async fn github_release_with_tag(
repo_name_with_owner: &str,
tag: &str,
http: Arc<dyn HttpClient>,
) -> Result<GithubRelease, anyhow::Error> {
let url = build_tagged_release_url(repo_name_with_owner, tag)?;
let mut response = http
.get(&url, Default::default(), true)
.await
.context("error fetching latest release")?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading latest release")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
match serde_json::from_slice::<GithubRelease>(body.as_slice()) {
Ok(release) => Ok(release),
Err(err) => {
log::error!("Error deserializing: {:?}", err);
log::error!(
"GitHub API response text: {:?}",
String::from_utf8_lossy(body.as_slice())
);
Err(anyhow!("error deserializing latest release"))
}
}
}
fn build_tagged_release_url(repo_name_with_owner: &str, tag: &str) -> Result<String> {
let mut url = Url::parse(&format!(
"https://api.github.com/repos/{repo_name_with_owner}/releases/tags"
))?;
// We're pushing this here, because tags may contain `/` and other characters
// that need to be escaped.
url.path_segments_mut()
.map_err(|_| anyhow!("cannot modify url path segments"))?
.push(tag);
Ok(url.to_string())
}
#[cfg(test)]
mod tests {
use super::build_tagged_release_url;
#[test]
fn test_build_tagged_release_url() {
let tag = "release/2.2.20-Insider";
let repo_name_with_owner = "microsoft/vscode-eslint";
let have = build_tagged_release_url(repo_name_with_owner, tag).unwrap();
assert_eq!(have, "https://api.github.com/repos/microsoft/vscode-eslint/releases/tags/release%2F2.2.20-Insider");
}
}

View file

@ -522,7 +522,7 @@ impl Dock {
pub fn resize_active_panel(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) { pub fn resize_active_panel(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) { if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE)); let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
entry.panel.set_size(size, cx); entry.panel.set_size(size, cx);
cx.notify(); cx.notify();
} }

View file

@ -11,6 +11,7 @@ use client::{
proto::{self, PeerId}, proto::{self, PeerId},
Client, Client,
}; };
use futures::{channel::mpsc, StreamExt};
use gpui::{ use gpui::{
AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
HighlightStyle, Model, Pixels, Point, SharedString, Task, View, ViewContext, WeakView, HighlightStyle, Model, Pixels, Point, SharedString, Task, View, ViewContext, WeakView,
@ -27,14 +28,13 @@ use std::{
ops::Range, ops::Range,
path::PathBuf, path::PathBuf,
rc::Rc, rc::Rc,
sync::{ sync::Arc,
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration, time::Duration,
}; };
use theme::Theme; use theme::Theme;
pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200);
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ItemSettings { pub struct ItemSettings {
pub git_status: bool, pub git_status: bool,
@ -405,7 +405,7 @@ impl<T: Item> ItemHandle for View<T> {
followed_item.is_project_item(cx), followed_item.is_project_item(cx),
proto::update_followers::Variant::CreateView(proto::View { proto::update_followers::Variant::CreateView(proto::View {
id: followed_item id: followed_item
.remote_id(&workspace.app_state.client, cx) .remote_id(&workspace.client(), cx)
.map(|id| id.to_proto()), .map(|id| id.to_proto()),
variant: Some(message), variant: Some(message),
leader_id: workspace.leader_for_pane(&pane), leader_id: workspace.leader_for_pane(&pane),
@ -421,8 +421,46 @@ impl<T: Item> ItemHandle for View<T> {
.is_none() .is_none()
{ {
let mut pending_autosave = DelayedDebouncedEditAction::new(); let mut pending_autosave = DelayedDebouncedEditAction::new();
let (pending_update_tx, mut pending_update_rx) = mpsc::unbounded();
let pending_update = Rc::new(RefCell::new(None)); let pending_update = Rc::new(RefCell::new(None));
let pending_update_scheduled = Arc::new(AtomicBool::new(false));
let mut send_follower_updates = None;
if let Some(item) = self.to_followable_item_handle(cx) {
let is_project_item = item.is_project_item(cx);
let item = item.downgrade();
send_follower_updates = Some(cx.spawn({
let pending_update = pending_update.clone();
|workspace, mut cx| async move {
while let Some(mut leader_id) = pending_update_rx.next().await {
while let Ok(Some(id)) = pending_update_rx.try_next() {
leader_id = id;
}
workspace.update(&mut cx, |workspace, cx| {
let item = item.upgrade().expect(
"item to be alive, otherwise task would have been dropped",
);
workspace.update_followers(
is_project_item,
proto::update_followers::Variant::UpdateView(
proto::UpdateView {
id: item
.remote_id(workspace.client(), cx)
.map(|id| id.to_proto()),
variant: pending_update.borrow_mut().take(),
leader_id,
},
),
cx,
);
})?;
cx.background_executor().timer(LEADER_UPDATE_THROTTLE).await;
}
anyhow::Ok(())
}
}));
}
let mut event_subscription = let mut event_subscription =
Some(cx.subscribe(self, move |workspace, item, event, cx| { Some(cx.subscribe(self, move |workspace, item, event, cx| {
@ -438,9 +476,7 @@ impl<T: Item> ItemHandle for View<T> {
}; };
if let Some(item) = item.to_followable_item_handle(cx) { if let Some(item) = item.to_followable_item_handle(cx) {
let is_project_item = item.is_project_item(cx);
let leader_id = workspace.leader_for_pane(&pane); let leader_id = workspace.leader_for_pane(&pane);
let follow_event = item.to_follow_event(event); let follow_event = item.to_follow_event(event);
if leader_id.is_some() if leader_id.is_some()
&& matches!(follow_event, Some(FollowEvent::Unfollow)) && matches!(follow_event, Some(FollowEvent::Unfollow))
@ -448,35 +484,13 @@ impl<T: Item> ItemHandle for View<T> {
workspace.unfollow(&pane, cx); workspace.unfollow(&pane, cx);
} }
if item.focus_handle(cx).contains_focused(cx) if item.focus_handle(cx).contains_focused(cx) {
&& item.add_event_to_update_proto( item.add_event_to_update_proto(
event, event,
&mut *pending_update.borrow_mut(), &mut *pending_update.borrow_mut(),
cx, cx,
) );
&& !pending_update_scheduled.load(Ordering::SeqCst) pending_update_tx.unbounded_send(leader_id).ok();
{
pending_update_scheduled.store(true, Ordering::SeqCst);
cx.defer({
let pending_update = pending_update.clone();
let pending_update_scheduled = pending_update_scheduled.clone();
move |this, cx| {
pending_update_scheduled.store(false, Ordering::SeqCst);
this.update_followers(
is_project_item,
proto::update_followers::Variant::UpdateView(
proto::UpdateView {
id: item
.remote_id(&this.app_state.client, cx)
.map(|id| id.to_proto()),
variant: pending_update.borrow_mut().take(),
leader_id,
},
),
cx,
);
}
});
} }
} }
@ -525,6 +539,7 @@ impl<T: Item> ItemHandle for View<T> {
cx.observe_release(self, move |workspace, _, _| { cx.observe_release(self, move |workspace, _, _| {
workspace.panes_by_item.remove(&item_id); workspace.panes_by_item.remove(&item_id);
event_subscription.take(); event_subscription.take();
send_follower_updates.take();
}) })
.detach(); .detach();
} }
@ -700,6 +715,7 @@ pub trait FollowableItem: Item {
pub trait FollowableItemHandle: ItemHandle { pub trait FollowableItemHandle: ItemHandle {
fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId>; fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId>;
fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle>;
fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext); fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>; fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
fn add_event_to_update_proto( fn add_event_to_update_proto(
@ -728,6 +744,10 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
}) })
} }
fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle> {
Box::new(self.downgrade())
}
fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext) { fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext) {
self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx)) self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx))
} }
@ -767,6 +787,16 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
} }
} }
pub trait WeakFollowableItemHandle: Send + Sync {
fn upgrade(&self) -> Option<Box<dyn FollowableItemHandle>>;
}
impl<T: FollowableItem> WeakFollowableItemHandle for WeakView<T> {
fn upgrade(&self) -> Option<Box<dyn FollowableItemHandle>> {
Some(Box::new(self.upgrade()?))
}
}
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub mod test { pub mod test {
use super::{Item, ItemEvent}; use super::{Item, ItemEvent};

View file

@ -883,7 +883,8 @@ mod element {
let child_size = bounds let child_size = bounds
.size .size
.apply_along(self.axis, |_| space_per_flex * child_flex); .apply_along(self.axis, |_| space_per_flex * child_flex)
.map(|d| d.round());
let child_bounds = Bounds { let child_bounds = Bounds {
origin, origin,

View file

@ -22,6 +22,16 @@ impl WorkspaceLocation {
pub fn paths(&self) -> Arc<Vec<PathBuf>> { pub fn paths(&self) -> Arc<Vec<PathBuf>> {
self.0.clone() self.0.clone()
} }
#[cfg(any(test, feature = "test-support"))]
pub fn new<P: AsRef<Path>>(paths: Vec<P>) -> Self {
Self(Arc::new(
paths
.into_iter()
.map(|p| p.as_ref().to_path_buf())
.collect(),
))
}
} }
impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation { impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {

View file

@ -121,7 +121,6 @@ actions!(
ToggleRightDock, ToggleRightDock,
ToggleBottomDock, ToggleBottomDock,
CloseAllDocks, CloseAllDocks,
ToggleGraphicsProfiler,
] ]
); );
@ -1110,7 +1109,7 @@ impl Workspace {
) )
} }
pub fn client(&self) -> &Client { pub fn client(&self) -> &Arc<Client> {
&self.app_state.client &self.app_state.client
} }
@ -3572,7 +3571,6 @@ impl Workspace {
workspace.reopen_closed_item(cx).detach(); workspace.reopen_closed_item(cx).detach();
}), }),
) )
.on_action(|_: &ToggleGraphicsProfiler, cx| cx.toggle_graphics_profiler())
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]

View file

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

View file

@ -1 +1 @@
dev stable

View file

@ -22,5 +22,3 @@
<string>An application in Zed wants to use speech recognition.</string> <string>An application in Zed wants to use speech recognition.</string>
<key>NSRemindersUsageDescription</key> <key>NSRemindersUsageDescription</key>
<string>An application in Zed wants to use your reminders.</string> <string>An application in Zed wants to use your reminders.</string>
<key>MetalHudEnabled</key>
<true />

View file

@ -37,7 +37,12 @@ pub fn app_menus() -> Vec<Menu<'static>> {
MenuItem::action("New Window", workspace::NewWindow), MenuItem::action("New Window", workspace::NewWindow),
MenuItem::separator(), MenuItem::separator(),
MenuItem::action("Open…", workspace::Open), MenuItem::action("Open…", workspace::Open),
MenuItem::action("Open Recent...", recent_projects::OpenRecent), MenuItem::action(
"Open Recent...",
recent_projects::OpenRecent {
create_new_window: true,
},
),
MenuItem::separator(), MenuItem::separator(),
MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject), MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject),
MenuItem::action("Save", workspace::Save { save_intent: None }), MenuItem::action("Save", workspace::Save { save_intent: None }),
@ -156,10 +161,6 @@ pub fn app_menus() -> Vec<Menu<'static>> {
MenuItem::action("View Telemetry", crate::OpenTelemetryLog), MenuItem::action("View Telemetry", crate::OpenTelemetryLog),
MenuItem::action("View Dependency Licenses", crate::OpenLicenses), MenuItem::action("View Dependency Licenses", crate::OpenLicenses),
MenuItem::action("Show Welcome", workspace::Welcome), MenuItem::action("Show Welcome", workspace::Welcome),
MenuItem::action(
"Toggle Graphics Profiler",
workspace::ToggleGraphicsProfiler,
),
MenuItem::separator(), MenuItem::separator(),
MenuItem::action( MenuItem::action(
"Documentation", "Documentation",

View file

@ -807,7 +807,7 @@ async fn upload_previous_crashes(
.unwrap_or("zed-2024-01-17-221900.ips".to_string()); // don't upload old crash reports from before we had this. .unwrap_or("zed-2024-01-17-221900.ips".to_string()); // don't upload old crash reports from before we had this.
let mut uploaded = last_uploaded.clone(); let mut uploaded = last_uploaded.clone();
let crash_report_url = http.build_url("/api/crash"); let crash_report_url = http.build_zed_api_url("/telemetry/crashes");
for dir in [&*CRASHES_DIR, &*CRASHES_RETIRED_DIR] { for dir in [&*CRASHES_DIR, &*CRASHES_RETIRED_DIR] {
let mut children = smol::fs::read_dir(&dir).await?; let mut children = smol::fs::read_dir(&dir).await?;

View file

@ -2751,18 +2751,20 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
fn test_bundled_languages(cx: &mut AppContext) { async fn test_bundled_languages(cx: &mut TestAppContext) {
let settings = SettingsStore::test(cx); let settings = cx.update(|cx| SettingsStore::test(cx));
cx.set_global(settings); cx.set_global(settings);
let mut languages = LanguageRegistry::test(); let mut languages = LanguageRegistry::test();
languages.set_executor(cx.background_executor().clone()); languages.set_executor(cx.executor().clone());
let languages = Arc::new(languages); let languages = Arc::new(languages);
let node_runtime = node_runtime::FakeNodeRuntime::new(); let node_runtime = node_runtime::FakeNodeRuntime::new();
languages::init(languages.clone(), node_runtime, cx); cx.update(|cx| {
languages::init(languages.clone(), node_runtime, cx);
});
for name in languages.language_names() { for name in languages.language_names() {
languages.language_for_name(&name); languages.language_for_name(&name).await.unwrap();
} }
cx.background_executor().run_until_parked(); cx.run_until_parked();
} }
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> { fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {