Compare commits
22 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
eae06aaca9 | ||
![]() |
8ebee1f31d | ||
![]() |
bc4230a6b1 | ||
![]() |
430d197e91 | ||
![]() |
a9696c59cc | ||
![]() |
3afc9d8607 | ||
![]() |
1c9ba85146 | ||
![]() |
80ebf5aff9 | ||
![]() |
acd21f5c15 | ||
![]() |
608ad1636d | ||
![]() |
c05f8154be | ||
![]() |
de71d7c0a0 | ||
![]() |
da671d9771 | ||
![]() |
993c001fc4 | ||
![]() |
7e501f6c1b | ||
![]() |
3a0f842f3d | ||
![]() |
38564c5ab2 | ||
![]() |
023498ef53 | ||
![]() |
1f61804ae0 | ||
![]() |
d229b39621 | ||
![]() |
ae2326c781 | ||
![]() |
c4d1855824 |
44 changed files with 586 additions and 210 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>> {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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()
|
||||
}),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ path = "src/picker.rs"
|
|||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
menu.workspace = true
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"))]
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
dev
|
||||
stable
|
|
@ -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 />
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue