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

View file

@ -341,6 +341,13 @@
{
"context": "Workspace",
"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-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",

View file

@ -383,6 +383,13 @@
{
"context": "Workspace",
"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-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",

View file

@ -20,7 +20,7 @@ use smol::io::AsyncReadExt;
use settings::{Settings, SettingsStore};
use smol::{fs::File, process::Command};
use release_channel::{AppCommitSha, ReleaseChannel};
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use std::{
env::consts::{ARCH, OS},
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>) {
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 url = client.build_url(&format!(

View file

@ -373,8 +373,10 @@ async fn test_basic_following(
editor_a1.update(cx_a, |editor, cx| {
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();
cx_b.background_executor.run_until_parked();
editor_b1.update(cx_b, |editor, cx| {
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.set_scroll_position(point(0., 100.), cx);
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked();
editor_b1.update(cx_b, |editor, cx| {
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.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();
editor_b.update(cx_b, |editor, cx| {
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.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();
editor_b.update(cx_b, |editor, cx| {
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
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();
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.select_right(&Default::default(), cx);
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked();
// 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);
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width;
panel.width = serialized_panel.width.map(|r| r.round());
cx.notify();
});
}

View file

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

View file

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

View file

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

View file

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

View file

@ -513,8 +513,4 @@ impl PlatformWindow for X11Window {
let inner = self.0.inner.lock();
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> {
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 {

View file

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

View file

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

View file

@ -280,7 +280,6 @@ pub struct Window {
pub(crate) focus: Option<FocusId>,
focus_enabled: bool,
pending_input: Option<PendingInput>,
graphics_profiler_enabled: bool,
}
#[derive(Default, Debug)]
@ -474,7 +473,6 @@ impl Window {
focus: None,
focus_enabled: true,
pending_input: None,
graphics_profiler_enabled: false,
}
}
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
/// is updated.
pub fn observe_global<G: Global>(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -541,7 +541,14 @@ impl LanguageServer {
}),
data_support: Some(true),
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()
}),

View file

@ -195,7 +195,7 @@ struct Excerpt {
///
/// Contains methods for getting the [`Buffer`] of the excerpt,
/// as well as mapping offsets to/from buffer and multibuffer coordinates.
#[derive(Copy, Clone)]
#[derive(Clone)]
pub struct MultiBufferExcerpt<'a> {
excerpt: &'a Excerpt,
excerpt_offset: usize,
@ -2967,7 +2967,16 @@ impl MultiBufferSnapshot {
excerpt
.buffer()
.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
[dependencies]
anyhow.workspace = true
editor.workspace = true
gpui.workspace = true
menu.workspace = true

View file

@ -1,3 +1,4 @@
use anyhow::Result;
use editor::Editor;
use gpui::{
div, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent, DismissEvent,
@ -13,11 +14,16 @@ enum ElementContainer {
UniformList(UniformListScrollHandle),
}
struct PendingUpdateMatches {
delegate_update_matches: Option<Task<()>>,
_task: Task<Result<()>>,
}
pub struct Picker<D: PickerDelegate> {
pub delegate: D,
element_container: ElementContainer,
editor: View<Editor>,
pending_update_matches: Option<Task<()>>,
pending_update_matches: Option<PendingUpdateMatches>,
confirm_on_update: Option<bool>,
width: 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>) {
let update = self.delegate.update_matches(query, cx);
let delegate_pending_update_matches = self.delegate.update_matches(query, cx);
self.matches_updated(cx);
self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
update.await;
this.update(&mut cx, |this, cx| {
this.matches_updated(cx);
})
.ok();
}));
// This struct ensures that we can synchronously drop the task returned by the
// delegate's `update_matches` method and the task that the picker is spawning.
// If we simply capture the delegate's task into the picker's task, when the picker's
// 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
// 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>) {

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)]
impl LspCommand for OnTypeFormatting {
type Response = Option<Transaction>;

View file

@ -35,11 +35,11 @@ use language::{
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
serialize_anchor, serialize_version, split_operations,
},
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability,
CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
Documentation, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName,
LocalFile, LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer,
PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, CodeAction,
CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation,
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
LspAdapterDelegate, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
ToOffset, ToPointUtf16, Transaction, Unclipped,
};
use log::error;
use lsp::{
@ -4235,7 +4235,10 @@ impl Project {
})?
.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 edit.changes.is_none() && edit.document_changes.is_none() {
continue;
@ -4255,6 +4258,7 @@ impl Project {
project_transaction.0.extend(new.0);
}
// TODO kb here too:
if let Some(command) = action.lsp_action.command {
project.update(&mut cx, |this, _| {
this.last_workspace_edits_by_language_server
@ -5296,33 +5300,10 @@ impl Project {
} else {
return Task::ready(Ok(Default::default()));
};
let range = action.range.to_point_utf16(buffer);
cx.spawn(move |this, mut cx| async move {
if let Some(lsp_range) = action
.lsp_action
.data
.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;
}
Self::try_resolve_code_action(&lang_server, &mut action)
.await
.context("resolving a code action")?;
if let Some(edit) = action.lsp_action.edit {
if edit.changes.is_some() || edit.document_changes.is_some() {
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(
this: Model<Self>,
_: TypedEnvelope<proto::RefreshInlayHints>,

View file

@ -2564,7 +2564,20 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
},
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());
fs.insert_tree(
@ -2591,16 +2604,14 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
Ok(Some(vec![
lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
title: "The code action".into(),
command: Some(lsp::Command {
title: "The command".into(),
command: "_the/command".into(),
arguments: Some(vec![json!("the-argument")]),
}),
..Default::default()
data: Some(serde_json::json!({
"command": "_the/command",
})),
..lsp::CodeAction::default()
}),
lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
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
// edits, we must execute the given command.
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

View file

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

View file

@ -15,7 +15,15 @@ gpui.workspace = true
menu.workspace = true
ordered-float.workspace = true
picker.workspace = true
serde.workspace = true
smol.workspace = true
ui.workspace = true
util.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 ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use serde::Deserialize;
use std::sync::Arc;
use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpacing, Tooltip};
use util::paths::PathExt;
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) {
cx.observe_new_views(RecentProjects::register).detach();
@ -63,9 +74,9 @@ impl RecentProjects {
}
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 {
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);
}
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 {
workspace.update(&mut cx, |workspace, cx| {
let weak_workspace = cx.view().downgrade();
workspace.toggle_modal(cx, |cx| {
let delegate = RecentProjectsDelegate::new(weak_workspace, true);
let delegate =
RecentProjectsDelegate::new(weak_workspace, create_new_window, true);
let modal = Self::new(delegate, 34., cx);
modal
@ -95,7 +111,13 @@ impl RecentProjects {
}
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,
matches: Vec<StringMatch>,
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
reset_selected_match_index: bool,
}
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 {
workspace,
workspaces: vec![],
selected_match_index: 0,
matches: Default::default(),
create_new_window,
render_paths,
reset_selected_match_index: true,
}
@ -147,10 +171,19 @@ impl PickerDelegate for RecentProjectsDelegate {
type ListItem = ListItem;
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!(
"{} reuses the window, {} opens a new one",
cx.keystroke_text_for(&menu::Confirm),
cx.keystroke_text_for(&menu::SecondaryConfirm),
"{reuse_window} reuses the window, {create_window} opens a new one",
))
}
@ -219,15 +252,39 @@ impl PickerDelegate for RecentProjectsDelegate {
{
let (candidate_workspace_id, candidate_workspace_location) =
&self.workspaces[selected_match.candidate_id];
let replace_current_window = !secondary;
let replace_current_window = if self.create_new_window {
secondary
} else {
!secondary
};
workspace
.update(cx, |workspace, cx| {
if workspace.database_id() != *candidate_workspace_id {
workspace.open_workspace_for_paths(
replace_current_window,
candidate_workspace_location.paths().as_ref().clone(),
cx,
)
let candidate_paths = candidate_workspace_location.paths().as_ref().clone();
if replace_current_window {
cx.spawn(move |workspace, mut cx| async move {
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 {
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"
);
} else {
let message_type = proto::build_typed_envelope(connection_id, incoming)
.map(|p| p.payload_type_name());
tracing::warn!(
%connection_id,
message_id,
responding_to,
message_type,
"incoming response: unknown request"
);
}

View file

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

View file

@ -3,6 +3,7 @@ use anyhow::{anyhow, bail, Context, Result};
use futures::AsyncReadExt;
use serde::Deserialize;
use std::sync::Arc;
use url::Url;
pub struct GitHubLspBinaryVersion {
pub name: String,
@ -74,3 +75,70 @@ pub async fn latest_github_release(
.find(|release| release.pre_release == pre_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>) {
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);
cx.notify();
}

View file

@ -11,6 +11,7 @@ use client::{
proto::{self, PeerId},
Client,
};
use futures::{channel::mpsc, StreamExt};
use gpui::{
AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
HighlightStyle, Model, Pixels, Point, SharedString, Task, View, ViewContext, WeakView,
@ -27,14 +28,13 @@ use std::{
ops::Range,
path::PathBuf,
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
sync::Arc,
time::Duration,
};
use theme::Theme;
pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200);
#[derive(Deserialize)]
pub struct ItemSettings {
pub git_status: bool,
@ -405,7 +405,7 @@ impl<T: Item> ItemHandle for View<T> {
followed_item.is_project_item(cx),
proto::update_followers::Variant::CreateView(proto::View {
id: followed_item
.remote_id(&workspace.app_state.client, cx)
.remote_id(&workspace.client(), cx)
.map(|id| id.to_proto()),
variant: Some(message),
leader_id: workspace.leader_for_pane(&pane),
@ -421,8 +421,46 @@ impl<T: Item> ItemHandle for View<T> {
.is_none()
{
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_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 =
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) {
let is_project_item = item.is_project_item(cx);
let leader_id = workspace.leader_for_pane(&pane);
let follow_event = item.to_follow_event(event);
if leader_id.is_some()
&& matches!(follow_event, Some(FollowEvent::Unfollow))
@ -448,35 +484,13 @@ impl<T: Item> ItemHandle for View<T> {
workspace.unfollow(&pane, cx);
}
if item.focus_handle(cx).contains_focused(cx)
&& item.add_event_to_update_proto(
if item.focus_handle(cx).contains_focused(cx) {
item.add_event_to_update_proto(
event,
&mut *pending_update.borrow_mut(),
cx,
)
&& !pending_update_scheduled.load(Ordering::SeqCst)
{
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,
);
}
});
);
pending_update_tx.unbounded_send(leader_id).ok();
}
}
@ -525,6 +539,7 @@ impl<T: Item> ItemHandle for View<T> {
cx.observe_release(self, move |workspace, _, _| {
workspace.panes_by_item.remove(&item_id);
event_subscription.take();
send_follower_updates.take();
})
.detach();
}
@ -700,6 +715,7 @@ pub trait FollowableItem: Item {
pub trait FollowableItemHandle: ItemHandle {
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 to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
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) {
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"))]
pub mod test {
use super::{Item, ItemEvent};

View file

@ -883,7 +883,8 @@ mod element {
let child_size = bounds
.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 {
origin,

View file

@ -22,6 +22,16 @@ impl WorkspaceLocation {
pub fn paths(&self) -> Arc<Vec<PathBuf>> {
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 {

View file

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

View file

@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.125.0"
version = "0.125.4"
publish = false
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>
<key>NSRemindersUsageDescription</key>
<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::separator(),
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::action("Add Folder to Project…", workspace::AddFolderToProject),
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 Dependency Licenses", crate::OpenLicenses),
MenuItem::action("Show Welcome", workspace::Welcome),
MenuItem::action(
"Toggle Graphics Profiler",
workspace::ToggleGraphicsProfiler,
),
MenuItem::separator(),
MenuItem::action(
"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.
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] {
let mut children = smol::fs::read_dir(&dir).await?;

View file

@ -2751,18 +2751,20 @@ mod tests {
}
#[gpui::test]
fn test_bundled_languages(cx: &mut AppContext) {
let settings = SettingsStore::test(cx);
async fn test_bundled_languages(cx: &mut TestAppContext) {
let settings = cx.update(|cx| SettingsStore::test(cx));
cx.set_global(settings);
let mut languages = LanguageRegistry::test();
languages.set_executor(cx.background_executor().clone());
languages.set_executor(cx.executor().clone());
let languages = Arc::new(languages);
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() {
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> {