Compare commits

..

No commits in common. "main" and "collab-staging" have entirely different histories.

34 changed files with 185 additions and 805 deletions

2
.gitignore vendored
View file

@ -58,5 +58,3 @@ crates/docs_preprocessor/actions.json
# NixOS integration test state
.nixos-test-history
.local*

5
Cargo.lock generated
View file

@ -12560,7 +12560,6 @@ dependencies = [
"lsp",
"menu",
"picker",
"picker_preview",
"project",
"rope",
"serde_json",
@ -18275,9 +18274,9 @@ dependencies = [
[[package]]
name = "taffy"
version = "0.12.2"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "340a09581f29809fc0df82a3955501dc7f2a21f887e5d1c13dbe288fe1c0bef4"
checksum = "73afc801dd6bd47529eaa7c7e90557f107527d1b7c9c7ed7d7803c7b8d0c357f"
dependencies = [
"arrayvec",
"grid",

View file

@ -387,11 +387,6 @@
"font_style": null,
"font_weight": null
},
"variable.parameter": {
"color": "#d2a6ffff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#5ac1feff",
"font_style": null,
@ -794,11 +789,6 @@
"font_style": null,
"font_weight": null
},
"variable.parameter": {
"color": "#a37accff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#3b9ee5ff",
"font_style": null,
@ -1201,11 +1191,6 @@
"font_style": null,
"font_weight": null
},
"variable.parameter": {
"color": "#dfbfffff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#72cffeff",
"font_style": null,

View file

@ -397,11 +397,6 @@
"font_style": null,
"font_weight": null
},
"variable.parameter": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#83a598ff",
"font_style": null,
@ -819,11 +814,6 @@
"font_style": null,
"font_weight": null
},
"variable.parameter": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#83a598ff",
"font_style": null,
@ -1241,11 +1231,6 @@
"font_style": null,
"font_weight": null
},
"variable.parameter": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#83a598ff",
"font_style": null,
@ -1663,11 +1648,6 @@
"font_style": null,
"font_weight": null
},
"variable.parameter": {
"color": "#076678ff",
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#066578ff",
"font_style": null,
@ -2085,11 +2065,6 @@
"font_style": null,
"font_weight": null
},
"variable.parameter": {
"color": "#076678ff",
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#066578ff",
"font_style": null,
@ -2507,11 +2482,6 @@
"font_style": null,
"font_weight": null
},
"variable.parameter": {
"color": "#076678ff",
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#066578ff",
"font_style": null,

View file

@ -394,11 +394,6 @@
"font_style": null,
"font_weight": null
},
"variable.parameter": {
"color": "#d07277ff",
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#bf956aff",
"font_style": null,
@ -811,11 +806,6 @@
"font_style": null,
"font_weight": null
},
"variable.parameter": {
"color": "#d3604fff",
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#ad6e25ff",
"font_style": null,

View file

@ -2223,7 +2223,7 @@ fn diagnostics_crease_label(
diagnostics_label(summary, include_errors, include_warnings).into()
}
pub(crate) fn pluralize(noun: &str, count: usize) -> String {
fn pluralize(noun: &str, count: usize) -> String {
if count == 1 {
noun.to_string()
} else {

View file

@ -22,7 +22,7 @@ use cloud_api_types::{SubmitAgentThreadFeedbackBody, SubmitAgentThreadFeedbackCo
use editor::actions::OpenExcerpts;
use sandbox::{SandboxFsPolicy, SandboxNetPolicy, SandboxPolicy};
use crate::completion_provider::{AvailableSkill, PromptLocalCommand, pluralize};
use crate::completion_provider::{AvailableSkill, PromptLocalCommand};
use crate::message_editor::SharedSessionCapabilities;
use crate::ui::{
SandboxGroup, SandboxRow, SandboxSection, SandboxStatusTooltip, TerminalSandboxWarning,
@ -5780,12 +5780,8 @@ impl Render for TokenUsageTooltip {
Button::new(
"open-project-rules",
format!(
"{} {}",
project_rules_count,
pluralize(
"project rule",
project_rules_count
)
"{} project rules",
project_rules_count
),
)
.end_icon(

View file

@ -49,10 +49,6 @@ pub struct CsvPreviewView {
/// Performance metrics for debugging and monitoring CSV operations.
pub(crate) performance_metrics: PerformanceMetrics,
pub(crate) list_state: gpui::ListState,
/// Cached row height, refreshed from the actual text line height on every render.
/// Used to size not-yet-rendered rows for the scrollbar without a full `.measure_all()`
/// pass, so it tracks the real row height instead of a hardcoded guess.
pub(crate) row_height: Pixels,
/// Time when the last parsing operation ended, used for smart debouncing
pub(crate) last_parse_end_time: Option<std::time::Instant>,
}
@ -98,7 +94,7 @@ impl CsvPreviewView {
.and_then(|item| item.act_as::<Editor>(cx))
.filter(|editor| Self::is_csv_file(editor, cx))
{
let csv_preview = Self::new(&editor, window, cx);
let csv_preview = Self::new(&editor, cx);
workspace.active_pane().update(cx, |pane, cx| {
let existing = pane
.items_of_type::<CsvPreviewView>()
@ -119,7 +115,7 @@ impl CsvPreviewView {
.and_then(|item| item.act_as::<Editor>(cx))
.filter(|editor| Self::is_csv_file(editor, cx))
{
let csv_preview = Self::new(&editor, window, cx);
let csv_preview = Self::new(&editor, cx);
let pane = workspace
.find_pane_in_direction(SplitDirection::Right, cx)
.unwrap_or_else(|| {
@ -156,7 +152,7 @@ impl CsvPreviewView {
});
}
fn new(editor: &Entity<Editor>, window: &Window, cx: &mut Context<Workspace>) -> Entity<Self> {
fn new(editor: &Entity<Editor>, cx: &mut Context<Workspace>) -> Entity<Self> {
let contents = TableLikeContent::default();
let table_interaction_state = cx.new(|cx| {
TableInteractionState::new(cx).with_custom_scrollbar(ui::Scrollbars::for_settings::<
@ -177,7 +173,6 @@ impl CsvPreviewView {
},
);
let row_height = window.pixel_snap(window.line_height());
let mut view = CsvPreviewView {
focus_handle: cx.focus_handle(),
active_editor_state: EditorState {
@ -191,8 +186,7 @@ impl CsvPreviewView {
filter_sort_task: None,
performance_metrics: PerformanceMetrics::default(),
list_state: gpui::ListState::new(contents.rows.len(), ListAlignment::Top, px(1.))
.with_uniform_item_height(row_height),
row_height,
.with_uniform_item_height(px(24.)),
settings: CsvPreviewSettings::default(),
last_parse_end_time: None,
engine: TableDataEngine::default(),
@ -245,10 +239,11 @@ impl CsvPreviewView {
this.update(cx, |view, cx| {
view.engine.set_d2d_mapping(mapping);
let visible_rows = view.engine.d2d_mapping().visible_row_count();
// Uses the row height measured on the last render. Cheaper than a full
// `.measure_all()` pass; exact row heights are re-measured on scrolling.
// Approximation of single csv table row height. Will be re-measured on scrolling.
// This cheap solution allow to render scrollbar with fraction of a cost compared to `.measure_all()` call
let approximate_height = px(24.);
view.list_state
.reset_with_uniform_height(visible_rows, view.row_height);
.reset_with_uniform_height(visible_rows, approximate_height);
cx.notify();
})
.ok();

View file

@ -7,15 +7,7 @@ use crate::CsvPreviewView;
impl Render for CsvPreviewView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme();
let row_height = window.pixel_snap(window.line_height());
if row_height != self.row_height {
self.row_height = row_height;
// Font size (rem size, buffer font override, ...) changed since the list was last
// measured: existing rows and unmeasured-item height hints are now the wrong size.
// Unlike `reset_with_uniform_height`, this preserves scroll position and keeps each
// item's prior size as a hint rather than dropping straight to a fresh guess.
self.list_state.remeasure();
}
let render_prep_start = Instant::now();
let table_with_settings = v_flex()
.size_full()

View file

@ -69,7 +69,6 @@ impl CsvPreviewView {
cols,
display_row,
row_identifier_text_color,
this.row_height,
cx,
)
.unwrap_or_else(|| panic!("Expected to render a table row"))
@ -84,7 +83,6 @@ impl CsvPreviewView {
.rendered_indices
.extend(range.clone());
let row_height = this.row_height;
range
.filter_map(|display_index| {
Self::render_single_table_row(
@ -92,7 +90,6 @@ impl CsvPreviewView {
cols,
DisplayRow(display_index),
row_identifier_text_color,
row_height,
cx,
)
})
@ -113,7 +110,6 @@ impl CsvPreviewView {
cols: usize,
display_row: DisplayRow,
row_identifier_text_color: gpui::Hsla,
row_height: Pixels,
cx: &Context<CsvPreviewView>,
) -> Option<UncheckedTableRow<AnyElement>> {
// Get the actual row index from our sorted indices
@ -132,23 +128,14 @@ impl CsvPreviewView {
let display_cell_id = DisplayCellId::new(display_row, col);
let cell = div()
.size_full()
.when(
!this.settings.multiline_cells_effectively_enabled(),
|div| {
div.whitespace_nowrap()
.text_ellipsis()
.h(row_height)
.overflow_hidden()
},
)
.child(CsvPreviewView::create_selectable_cell(
let cell = div().size_full().whitespace_nowrap().text_ellipsis().child(
CsvPreviewView::create_selectable_cell(
display_cell_id,
cell_content,
this.settings.vertical_alignment,
cx,
));
),
);
elements.push(
div()
@ -167,6 +154,7 @@ impl CsvPreviewView {
},
))
})
.text_ui(cx)
.child(cell)
.into_any_element(),
);

View file

@ -1,12 +1,12 @@
use ui::{
ActiveTheme as _, AnyElement, Button, ButtonCommon as _, ButtonSize, ButtonStyle,
Clickable as _, Context, ElementId, IntoElement as _, ParentElement as _, SharedString,
Styled as _, StyledTypography as _, Tooltip, div,
Clickable as _, Context, ElementId, FluentBuilder as _, IntoElement as _, ParentElement as _,
SharedString, Styled as _, StyledTypography as _, Tooltip, div,
};
use crate::{
CsvPreviewView,
settings::RowIdentifiers,
settings::{RowIdentifiers, VerticalAlignment},
types::{DataRow, DisplayRow, LineNumber},
};
@ -34,7 +34,7 @@ impl LineNumber {
if start + 1 == end {
format!("{start}\n{end}")
} else {
format!("{start}\n-\n{end}")
format!("{start}\n...\n{end}")
}
}
RowIdentDisplayMode::Horizontal => {
@ -78,6 +78,11 @@ impl CsvPreviewView {
(max_line_number as f32).log10().floor() as usize + 1
};
// if !self.settings.multiline_cells_enabled {
// // Uses horizontal line numbers layout like `123-456`. Needs twice the size
// digit_count *= 2;
// }
let char_width_px = 9.0; // TODO: get real width of the characters
let base_width = (digit_count as f32) * char_width_px;
let padding = 20.0;
@ -152,7 +157,7 @@ impl CsvPreviewView {
.contents
.line_numbers
.get(*data_row)?
.display_string(if self.settings.multiline_cells_effectively_enabled() {
.display_string(if self.settings.multiline_cells_enabled {
RowIdentDisplayMode::Vertical
} else {
RowIdentDisplayMode::Horizontal
@ -167,11 +172,14 @@ impl CsvPreviewView {
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().panel_background)
.h_full()
.text_ui(cx)
.text_color(cx.theme().colors().text_muted)
.justify_center()
.items_center()
.map(|div| match self.settings.vertical_alignment {
VerticalAlignment::Top => div.items_start(),
VerticalAlignment::Center => div.items_center(),
})
.font_buffer(cx)
.text_ui(cx)
.child(row_identifier)
.into_any_element();
Some(value)

View file

@ -1,7 +1,6 @@
use ui::{
ActiveTheme as _, AnyElement, ButtonSize, Checkbox, Context, ContextMenu, DropdownMenu,
ElementId, IntoElement as _, ParentElement as _, Styled as _, ToggleState, Tooltip, Window,
div, h_flex,
ActiveTheme as _, AnyElement, ButtonSize, Context, ContextMenu, DropdownMenu, ElementId,
IntoElement as _, ParentElement as _, Styled as _, Tooltip, Window, div, h_flex,
};
use crate::{
@ -122,30 +121,6 @@ impl CsvPreviewView {
),
);
let multiline_enabled = self.settings.multiline_cells_enabled;
let panel = panel.child({
let view = view.clone();
Checkbox::new(
ElementId::Name("multiline-rows-checkbox".into()),
if multiline_enabled {
ToggleState::Selected
} else {
ToggleState::Unselected
},
)
.label("Display multiline rows")
.tooltip(Tooltip::text(
"When enabled, row height grows to show all content. \
When disabled, only the first line is visible hover a cell to see the rest.",
))
.on_click(move |_state, _window, cx| {
view.update(cx, |this, cx| {
this.settings.multiline_cells_enabled = !this.settings.multiline_cells_enabled;
cx.notify();
});
})
});
#[cfg(feature = "dev-tools")]
let panel = panel.child(
h_flex()
@ -196,6 +171,7 @@ fn create_dev_only_popover_menu(
view_entity.update(cx, |view, cx| {
view.settings.rendering_with =
RowRenderMechanism::VariableList;
view.settings.multiline_cells_enabled = true;
cx.notify();
})
}
@ -212,6 +188,7 @@ fn create_dev_only_popover_menu(
view_entity.update(cx, |view, cx| {
view.settings.rendering_with =
RowRenderMechanism::UniformList;
view.settings.multiline_cells_enabled = false;
cx.notify();
})
}

View file

@ -44,7 +44,6 @@ impl CsvPreviewView {
.w_full()
.items_center()
.font_buffer(cx)
.text_buffer(cx)
.child(
div()
.flex_1()

View file

@ -46,12 +46,3 @@ pub(crate) struct CsvPreviewSettings {
pub(crate) show_perf_metrics_overlay: bool,
pub(crate) multiline_cells_enabled: bool,
}
impl CsvPreviewSettings {
/// `multiline_cells_enabled` only makes sense with `VariableList`, which
/// supports per-row heights; `UniformList` requires every row to share one
/// height, so multiline is never honored there regardless of the setting.
pub(crate) fn multiline_cells_effectively_enabled(&self) -> bool {
self.multiline_cells_enabled && self.rendering_with == RowRenderMechanism::VariableList
}
}

View file

@ -1,7 +1,7 @@
pub use crate::commit_context_menu::{CopyCommitSha, CopyCommitTag, OpenCommitView};
use crate::{
commit_context_menu::{CommitContextMenuData, CommitContextMenuSource, commit_context_menu},
commit_tooltip::CommitAvatar,
commit_tooltip::{CommitAvatar, CommitDetails, CommitTooltip},
commit_view::CommitView,
git_status_icon,
};
@ -10,6 +10,7 @@ use editor::Editor;
use file_icons::FileIcons;
use git::{
BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, ParsedGitRemote,
commit::ParsedCommitMessage,
parse_git_remote_url,
repository::{
CommitDiff, CommitFile, InitialGraphCommitData, LogOrder, LogSource, RepoPath,
@ -1693,6 +1694,10 @@ impl GitGraph {
git_store.repositories().get(&self.repo_id).cloned()
}
fn has_context_menu(&self) -> bool {
self.context_menu.is_some()
}
/// Checks whether a ref name from git's `%D` decoration
/// format refers to the currently checked-out branch.
fn is_head_ref(ref_name: &str, head_branch_name: &Option<SharedString>) -> bool {
@ -1786,6 +1791,7 @@ impl GitGraph {
});
let row_height = Self::row_height(window, cx);
let has_context_menu = self.has_context_menu();
// We fetch data outside the visible viewport to avoid loading entries when
// users scroll through the git graph
@ -1893,6 +1899,55 @@ impl GitGraph {
div()
.id(ElementId::NamedInteger("commit-subject".into(), idx as u64))
.overflow_hidden()
.when(!has_context_menu, |this| {
if let CommitDataState::Loaded(commit_data) = &data {
let sha = commit.data.sha.to_string();
let author_name = commit_data.author_name.clone();
let author_email = commit_data.author_email.clone();
let message = commit_data.message.clone();
let commit_timestamp = commit_data.commit_timestamp;
let tag_names = commit
.data
.tag_names()
.into_iter()
.map(SharedString::from)
.collect::<Vec<_>>();
let workspace = self.workspace.clone();
let repository = repository.clone();
this.hoverable_tooltip(move |_window, cx| {
let remote_url = repository.read(cx).default_remote_url();
let provider_registry =
GitHostingProviderRegistry::default_global(cx);
let commit_details = CommitDetails {
sha: sha.clone().into(),
author_name: author_name.clone(),
author_email: author_email.clone(),
commit_time: OffsetDateTime::from_unix_timestamp(
commit_timestamp,
)
.unwrap_or_else(|_| OffsetDateTime::now_utc()),
message: Some(ParsedCommitMessage::parse(
sha.clone(),
message.to_string(),
remote_url.as_deref(),
Some(provider_registry),
)),
tag_names: tag_names.clone(),
};
cx.new(|cx| {
CommitTooltip::new(
commit_details,
repository.clone(),
workspace.clone(),
cx,
)
})
.into()
})
} else {
this
}
})
.child(
h_flex()
.gap_2()
@ -3670,6 +3725,7 @@ impl GitGraph {
// grid, on the other hand, doesn't appear to give taffy the same
// problems.
.w_full()
.max_h(line_height * 12.)
.py_2()
.pl_2()
.grid()
@ -3678,13 +3734,12 @@ impl GitGraph {
.child(
div()
.relative()
.w_full()
.size_full()
.child(
div()
.id("commit-message")
.text_sm()
.w_full()
.max_h(line_height * 12.)
.size_full()
.overflow_y_scroll()
.track_scroll(scroll_handle)
.child(MarkdownElement::new(message.clone(), message_style)),
@ -7326,136 +7381,6 @@ mod tests {
});
}
#[gpui::test]
async fn test_long_commit_message_is_constrained_to_scroll_viewport(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
Path::new("/project"),
json!({ ".git": {}, "file.txt": "content" }),
)
.await;
let commit_sha = Oid::from_bytes(&[1; 20]).expect("commit SHA should be valid");
let commits = vec![Arc::new(InitialGraphCommitData {
sha: commit_sha,
parents: smallvec![],
ref_names: vec!["HEAD -> main".into()],
})];
fs.set_graph_commits(Path::new("/project/.git"), commits);
let message = (0..40)
.map(|line_number| {
format!(
"Line {line_number}: This commit message is long enough to require scrolling."
)
})
.collect::<Vec<_>>()
.join("\n\n");
fs.set_commit_data(
Path::new("/project/.git"),
[(
CommitData {
sha: commit_sha,
parents: smallvec![],
author_name: "Author".into(),
author_email: "author@example.com".into(),
commit_timestamp: 1_700_000_000,
subject: "Long commit message".into(),
message: message.into(),
},
false,
)],
);
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
cx.run_until_parked();
let repository = project.read_with(cx, |project, cx| {
project
.active_repository(cx)
.expect("should have a repository")
});
let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
workspace::MultiWorkspace::test_new(project.clone(), window, cx)
});
let workspace_weak =
multi_workspace.read_with(&*cx, |multi, _| multi.workspace().downgrade());
let git_graph = cx.new_window_entity(|window, cx| {
GitGraph::new(
repository.read(cx).id,
project.read(cx).git_store().clone(),
workspace_weak,
None,
window,
cx,
)
});
cx.run_until_parked();
git_graph.update_in(cx, |graph, window, cx| {
graph.select_first(&menu::SelectFirst, window, cx);
});
cx.run_until_parked();
git_graph.update(cx, |graph, cx| {
graph.selected_commit_diff = Some(CommitDiff {
files: vec![CommitFile {
path: RepoPath::new("file.txt").expect("repository path should be valid"),
old_text: Some("content".into()),
new_text: Some("updated content".into()),
is_binary: false,
}],
});
graph.selected_commit_diff_stats = Some((1, 1));
cx.notify();
});
cx.draw(
point(px(0.), px(0.)),
gpui::size(px(1200.), px(800.)),
|_, _| git_graph.clone().into_any_element(),
);
cx.run_until_parked();
let (message_scroll_handle, changed_files_scroll_handle) =
git_graph.read_with(&*cx, |graph, _| {
(
graph
.selected_commit_message
.as_ref()
.expect("selected commit message should be loaded")
.scroll_handle
.clone(),
graph.changed_files_scroll_handle.clone(),
)
});
let maximum_message_height = git_graph.update_in(cx, |_, window, cx| {
editor::hover_markdown_style(window, cx)
.base_text_style
.line_height_in_pixels(window.rem_size())
* 12.
});
let message_bounds = message_scroll_handle.bounds();
let changed_files_bounds = changed_files_scroll_handle.0.borrow().base_handle.bounds();
assert!(
message_bounds.size.height <= maximum_message_height,
"commit message viewport height ({}) should not exceed its maximum ({})",
message_bounds.size.height,
maximum_message_height,
);
assert!(
message_scroll_handle.max_offset().y > px(0.),
"long commit message should be scrollable"
);
assert!(
message_bounds.bottom() <= changed_files_bounds.top(),
"commit message viewport {message_bounds:?} should not overlap changed files {changed_files_bounds:?}"
);
}
#[gpui::test]
async fn test_commit_message_not_reloaded_for_same_sha(cx: &mut TestAppContext) {
init_test(cx);

View file

@ -91,7 +91,7 @@ async-channel.workspace = true
stacksafe.workspace = true
strum.workspace = true
sum_tree.workspace = true
taffy = "=0.12.2"
taffy = "=0.12.1"
thiserror.workspace = true
gpui_util.workspace = true
hdrhistogram = { workspace = true, optional = true }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 B

View file

@ -100,7 +100,7 @@ impl Render for ImageShowcase {
.items_center()
.gap_8()
.child(ImageContainer::new(
"Image loaded from a local file with EXIF orientation",
"Image loaded from a local file",
self.local_resource.clone(),
))
.child(ImageContainer::new(
@ -202,9 +202,7 @@ fn run_example() {
cx.open_window(window_options, |_, cx| {
cx.new(|_| ImageShowcase {
// Relative path to your root project path
local_resource: manifest_dir
.join("examples/image/exif-orientation-rotate-180.jpg")
.into(),
local_resource: manifest_dir.join("examples/image/app-icon.png").into(),
remote_resource: "https://picsum.photos/800/400".into(),
asset_resource: "image/color.svg".into(),
})

View file

@ -262,10 +262,7 @@ impl Interactivity {
) {
self.mouse_down_listeners
.push(Box::new(move |event, phase, hitbox, window, cx| {
if phase == DispatchPhase::Capture
&& !window.has_active_prompt()
&& !hitbox.contains(&window.mouse_position())
{
if phase == DispatchPhase::Capture && !hitbox.contains(&window.mouse_position()) {
(listener)(event, window, cx)
}
}));
@ -2672,8 +2669,8 @@ impl Interactivity {
if phase == DispatchPhase::Capture && group_hovered != was_group_hovered {
if let Some(hover_state) = &hover_state {
hover_state.borrow_mut().group = group_hovered;
cx.notify(current_view);
}
cx.notify(current_view);
}
});
}
@ -4117,108 +4114,9 @@ mod tests {
use super::*;
use crate::{
AnyWindowHandle, AppContext as _, Context, InputEvent, Keystroke, MouseMoveEvent,
TestAppContext, canvas, util::FluentBuilder as _,
TestAppContext, util::FluentBuilder as _,
};
use std::{cell::Cell, rc::Weak};
struct GroupHoverTestView {
render_count: Rc<Cell<usize>>,
anonymous_paint_count: Rc<Cell<usize>>,
stateful_width: Rc<Cell<Pixels>>,
}
impl Render for GroupHoverTestView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.render_count.set(self.render_count.get() + 1);
let anonymous_paint_count = self.anonymous_paint_count.clone();
let stateful_width = self.stateful_width.clone();
div().size_full().child(
div()
.ml(px(20.))
.mt(px(20.))
.size(px(50.))
.relative()
.group("hover-group")
.child(
div()
.absolute()
.size_full()
.invisible()
.group_hover("hover-group", |style| style.visible())
.child(canvas(
|_, _, _| {},
move |_, _, _, _| {
anonymous_paint_count.set(anonymous_paint_count.get() + 1)
},
)),
)
.child(
div()
.id("stateful-group-hover-target")
.absolute()
.top_0()
.left_0()
.size(px(10.))
.group_hover("hover-group", |style| style.size(px(20.)))
.child(canvas(
move |bounds, _, _| stateful_width.set(bounds.size.width),
|_, _, _, _| {},
)),
),
)
}
}
#[gpui::test]
fn group_hover_styles_update_only_on_transitions(cx: &mut TestAppContext) {
let render_count = Rc::new(Cell::new(0));
let anonymous_paint_count = Rc::new(Cell::new(0));
let stateful_width = Rc::new(Cell::new(px(0.)));
let window = cx.add_window({
let render_count = render_count.clone();
let anonymous_paint_count = anonymous_paint_count.clone();
let stateful_width = stateful_width.clone();
move |_, _| GroupHoverTestView {
render_count,
anonymous_paint_count,
stateful_width,
}
});
let window = AnyWindowHandle::from(window);
cx.update_window(window, |_, window, cx| window.draw(cx).clear())
.unwrap();
assert_eq!(anonymous_paint_count.get(), 0);
assert_eq!(stateful_width.get(), px(10.));
let move_mouse = |cx: &mut TestAppContext, position| {
cx.update_window(window, |_, window, cx| {
window.simulate_mouse_move(position, cx)
})
.unwrap();
};
let initial_render_count = render_count.get();
move_mouse(cx, point(px(25.), px(25.)));
assert_eq!(render_count.get(), initial_render_count + 1);
assert_eq!(anonymous_paint_count.get(), 1);
assert_eq!(stateful_width.get(), px(20.));
move_mouse(cx, point(px(30.), px(30.)));
assert_eq!(render_count.get(), initial_render_count + 1);
assert_eq!(anonymous_paint_count.get(), 1);
assert_eq!(stateful_width.get(), px(20.));
move_mouse(cx, point(px(5.), px(5.)));
assert_eq!(render_count.get(), initial_render_count + 2);
assert_eq!(anonymous_paint_count.get(), 1);
assert_eq!(stateful_width.get(), px(10.));
move_mouse(cx, point(px(10.), px(10.)));
assert_eq!(render_count.get(), initial_render_count + 2);
assert_eq!(anonymous_paint_count.get(), 1);
assert_eq!(stateful_width.get(), px(10.));
}
use std::rc::Weak;
struct TestTooltipView;
@ -4526,87 +4424,6 @@ mod tests {
assert!(active_tooltip.borrow().is_none());
}
struct MouseDownOutOwner {
mouse_down_out_count: Rc<RefCell<usize>>,
}
impl Render for MouseDownOutOwner {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let mouse_down_out_count = self.mouse_down_out_count.clone();
div()
.size_full()
.child(div().id("target").w(px(50.)).h(px(50.)).on_mouse_down_out(
move |_, _, _| {
*mouse_down_out_count.borrow_mut() += 1;
},
))
}
}
#[test]
fn mouse_down_out_is_suppressed_while_window_prompt_is_active() {
let mut test_app = TestAppContext::single();
let mouse_down_out_count = Rc::new(RefCell::new(0));
let window = test_app.add_window({
let mouse_down_out_count = mouse_down_out_count.clone();
move |_, _| MouseDownOutOwner {
mouse_down_out_count,
}
});
let any_window: AnyWindowHandle = window.into();
fn dispatch_mouse_down_outside_target(
test_app: &mut TestAppContext,
any_window: AnyWindowHandle,
) {
test_app
.update_window(any_window, |_, window, cx| {
window.dispatch_event(
MouseDownEvent {
position: point(px(75.), px(75.)),
button: MouseButton::Left,
modifiers: Default::default(),
click_count: 1,
first_mouse: false,
}
.to_platform_input(),
cx,
);
})
.unwrap();
}
test_app
.update_window(any_window, |_, window, cx| {
window.draw(cx).clear();
})
.unwrap();
dispatch_mouse_down_outside_target(&mut test_app, any_window);
assert_eq!(
*mouse_down_out_count.borrow(),
1,
"mouse down outside the element should fire mouse-down-out listeners"
);
test_app
.update_window(any_window, |_, window, cx| {
cx.set_prompt_builder(crate::fallback_prompt_renderer);
let _receiver =
window.prompt(crate::PromptLevel::Warning, "message", None, &["Ok"], cx);
assert!(window.has_active_prompt());
window.draw(cx).clear();
})
.unwrap();
dispatch_mouse_down_outside_target(&mut test_app, any_window);
assert_eq!(
*mouse_down_out_count.borrow(),
1,
"mouse down over an active prompt should not fire mouse-down-out listeners"
);
}
#[test]
fn test_write_a11y_info_string_and_numeric_properties() {
let mut interactivity = Interactivity::default();

View file

@ -2,15 +2,14 @@ use crate::{
AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength, Element, ElementId,
Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId, InteractiveElement,
Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
SharedString, SharedUri, StyleRefinement, Styled, Task, Window, decode_static_image,
decode_static_image_from_decoder, px,
SharedString, SharedUri, StyleRefinement, Styled, Task, Window, px,
};
use anyhow::Result;
use futures::Future;
use gpui_util::ResultExt;
use image::{
AnimationDecoder, ImageError, ImageFormat, Rgba,
AnimationDecoder, DynamicImage, Frame, ImageError, ImageFormat, Rgba,
codecs::{gif::GifDecoder, webp::WebPDecoder},
};
use scheduler::Instant;
@ -717,10 +716,27 @@ impl Asset for ImageAssetLoader {
frames
} else {
decode_static_image_from_decoder(decoder)?
let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8();
// Convert from RGBA to BGRA.
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
SmallVec::from_elem(Frame::new(data), 1)
}
}
_ => decode_static_image(&bytes, format)?,
_ => {
let mut data =
image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
// Convert from RGBA to BGRA.
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
SmallVec::from_elem(Frame::new(data), 1)
}
};
Ok(Arc::new(RenderImage::new(data)))

View file

@ -1038,7 +1038,7 @@ impl StateInner {
let mut rendered_focused_item = false;
let available_item_space = size(
available_width.map_or(AvailableSpace::MaxContent, |width| {
available_width.map_or(AvailableSpace::MinContent, |width| {
AvailableSpace::Definite(width)
}),
AvailableSpace::MinContent,

View file

@ -618,7 +618,6 @@ struct TextLayoutInner {
lines: SmallVec<[WrappedLine; 1]>,
line_height: Pixels,
wrap_width: Option<Pixels>,
truncate_width: Option<Pixels>,
size: Option<Size<Pixels>>,
bounds: Option<Bounds<Pixels>>,
}
@ -681,20 +680,16 @@ impl TextLayout {
// 2. wrap_width matches (or both are None)
// 3. truncate_width is None (if truncate_width is Some, we need to re-layout
// because the previous layout may have been computed without truncation)
// 4. the cached layout was not truncated (a truncated layout answers an
// unconstrained probe with the truncated size, which poisons intrinsic
// sizing with whatever width some earlier measure pass happened to use)
if let Some(text_layout) = element_state.0.borrow().as_ref()
&& let Some(size) = text_layout.size
&& (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
&& truncate_width.is_none()
&& text_layout.truncate_width.is_none()
{
return size;
}
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
let (text, runs) = if let Some(truncate_width) = truncate_width {
let (text, runs) = if truncate_width.is_some() {
if let Some(max_lines) = text_style.line_clamp
&& let Some(wrap_width) = wrap_width
{
@ -706,25 +701,10 @@ impl TextLayout {
&runs,
truncate_from,
)
} else if let Some(unclipped) = window
.text_system()
.shape_text(text.clone(), font_size, &runs, None, None)
.log_err()
&& unclipped
.iter()
.all(|line| line.size(line_height).width <= truncate_width)
{
// The truncation decision below sums per-character advances,
// which overestimates the shaped width (no kerning), truncating
// text that fits exactly in its measured width. Skip truncation
// whenever the honestly-shaped text fits; the shaping result
// comes from the line layout cache when the same text was
// already measured untruncated this frame.
(text.clone(), Cow::Borrowed(&*runs))
} else {
line_wrapper.truncate_line(
text.clone(),
truncate_width,
truncate_width.unwrap_or(Pixels::MAX),
&truncation_affix,
&runs,
truncate_from,
@ -751,7 +731,6 @@ impl TextLayout {
len: 0,
line_height,
wrap_width,
truncate_width,
size: Some(Size::default()),
bounds: None,
});
@ -770,7 +749,6 @@ impl TextLayout {
len,
line_height,
wrap_width,
truncate_width,
size: Some(size),
bounds: None,
});

View file

@ -671,7 +671,7 @@ impl UniformList {
return Size::default();
};
let available_space = size(
list_width.map_or(AvailableSpace::MaxContent, |width| {
list_width.map_or(AvailableSpace::MinContent, |width| {
AvailableSpace::Definite(width)
}),
AvailableSpace::MinContent,

View file

@ -42,15 +42,15 @@ use crate::{
RenderImageParams, RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString, Size,
SvgRenderer, SystemWindowTab, Task, Window, WindowControlArea, hash, point, px, size,
};
use anyhow::Result;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use anyhow::bail;
use anyhow::{Context as _, Result};
use async_task::Runnable;
use futures::channel::oneshot;
#[cfg(any(test, feature = "test-support"))]
use image::RgbaImage;
use image::codecs::gif::GifDecoder;
use image::{AnimationDecoder as _, DynamicImage, Frame};
use image::{AnimationDecoder as _, Frame};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use scheduler::Instant;
pub use scheduler::RunnableMeta;
@ -2368,33 +2368,6 @@ pub struct Image {
pub id: u64,
}
pub(crate) fn decode_static_image(
bytes: &[u8],
format: image::ImageFormat,
) -> Result<SmallVec<[Frame; 1]>> {
let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
.into_decoder()
.context("creating image decoder")?;
decode_static_image_from_decoder(decoder)
}
pub(crate) fn decode_static_image_from_decoder(
mut decoder: impl image::ImageDecoder,
) -> Result<SmallVec<[Frame; 1]>> {
let orientation = decoder
.orientation()
.context("reading decoder's orientation")?;
let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
image.apply_orientation(orientation);
let mut data = image.into_rgba8();
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
Ok(SmallVec::from_elem(Frame::new(data), 1))
}
impl Hash for Image {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u64(self.id);
@ -2450,6 +2423,20 @@ impl Image {
/// Convert the clipboard image to an `ImageData` object.
pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
fn frames_for_image(
bytes: &[u8],
format: image::ImageFormat,
) -> Result<SmallVec<[Frame; 1]>> {
let mut data = image::load_from_memory_with_format(bytes, format)?.into_rgba8();
// Convert from RGBA to BGRA.
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
Ok(SmallVec::from_elem(Frame::new(data), 1))
}
let frames = match self.format {
ImageFormat::Gif => {
let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
@ -2476,18 +2463,18 @@ impl Image {
frames
}
ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
ImageFormat::Png => frames_for_image(&self.bytes, image::ImageFormat::Png)?,
ImageFormat::Jpeg => frames_for_image(&self.bytes, image::ImageFormat::Jpeg)?,
ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?,
ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?,
ImageFormat::Svg => {
return svg_renderer
.render_single_frame(&self.bytes, 1.0)
.map_err(Into::into);
}
ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
ImageFormat::Pnm => frames_for_image(&self.bytes, image::ImageFormat::Pnm)?,
};
Ok(Arc::new(RenderImage::new(frames)))
@ -2572,22 +2559,6 @@ mod image_tests {
use super::*;
use std::sync::Arc;
#[test]
fn test_image_to_image_data_applies_exif_orientation() {
let image = Image::from_bytes(
ImageFormat::Jpeg,
include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
);
let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
assert_eq!(render_image.size(0), size(16.into(), 32.into()));
let bytes = render_image.as_bytes(0).unwrap();
assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
}
#[test]
fn test_svg_image_to_image_data_converts_to_bgra() {
let image = Image::from_bytes(

View file

@ -5343,14 +5343,6 @@ impl Window {
receiver
}
/// Returns whether a prompt rendered by GPUI is currently active in this window.
///
/// This is only true for prompts rendered in the window (see
/// [`App::set_prompt_builder`]), not for platform-native prompt dialogs.
pub fn has_active_prompt(&self) -> bool {
self.prompt.is_some()
}
/// Returns the current context stack.
pub fn context_stack(&self) -> Vec<KeyContext> {
let node_id = self.focus_node_id_in_rendered_frame(self.focus);

View file

@ -489,56 +489,27 @@ impl LspAdapter for RustLspAdapter {
.collect::<SmallVec<[_; 8]>>();
all_stop_ranges.sort_unstable_by_key(|a| (a.start, Reverse(a.end)));
// Placeholders may nest, e.g. `$2` inside `${1:"$2"}`
struct OpenPlaceholder {
snippet_text_end: usize,
label_run_start: usize,
}
let mut open_placeholders = SmallVec::<[OpenPlaceholder; 4]>::new();
for range in &all_stop_ranges {
let start_pos = range.start as usize;
let end_pos = range.end as usize;
while let Some(placeholder) = open_placeholders.last() {
if placeholder.snippet_text_end > start_pos {
break;
}
label.push_str(&snippet.text[text_pos..placeholder.snippet_text_end]);
text_pos = placeholder.snippet_text_end;
runs.push((
placeholder.label_run_start..label.len(),
HighlightId::TABSTOP_REPLACE_ID,
));
open_placeholders.pop();
}
label.push_str(&snippet.text[text_pos..start_pos]);
text_pos = start_pos;
if start_pos == end_pos {
let caret_start = label.len();
label.push('…');
runs.push((caret_start..label.len(), HighlightId::TABSTOP_INSERT_ID));
} else {
open_placeholders.push(OpenPlaceholder {
snippet_text_end: end_pos,
label_run_start: label.len(),
});
let label_start = label.len();
label.push_str(&snippet.text[start_pos..end_pos]);
let label_end = label.len();
runs.push((label_start..label_end, HighlightId::TABSTOP_REPLACE_ID));
}
}
while let Some(placeholder) = open_placeholders.pop() {
label.push_str(&snippet.text[text_pos..placeholder.snippet_text_end]);
text_pos = placeholder.snippet_text_end;
runs.push((
placeholder.label_run_start..label.len(),
HighlightId::TABSTOP_REPLACE_ID,
));
text_pos = end_pos;
}
label.push_str(&snippet.text[text_pos..]);
runs.sort_unstable_by_key(|(range, _)| (range.start, Reverse(range.end)));
if detail_left.is_some_and(|detail_left| detail_left == new_text) {
// We only include the left detail if it isn't the snippet again
@ -1482,7 +1453,6 @@ mod tests {
use crate::language;
use gpui::{BorrowAppContext, Hsla, TestAppContext};
use lsp::CompletionItemLabelDetails;
use pretty_assertions::assert_eq;
use settings::SettingsStore;
use theme::SyntaxTheme;
use util::path;
@ -1906,34 +1876,6 @@ mod tests {
))
);
assert_eq!(
adapter
.label_for_completion(
&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::SNIPPET),
label: "unimplemented".to_string(),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::default(),
new_text: "unimplemented!(${1:\"$2\"})".to_string(),
})),
..lsp::CompletionItem::default()
},
&language,
)
.await,
Some(CodeLabel::new(
"unimplemented!(\"\")".to_string(),
0..13,
vec![
(15..20, HighlightId::TABSTOP_REPLACE_ID),
(16..19, HighlightId::TABSTOP_INSERT_ID),
(0..13, HighlightId::new(2)),
(13..14, HighlightId::new(2)),
],
))
);
// Postfix completion without actual tabstops (only implicit final $0)
// The label should use completion.label so it can be filtered by "ref"
let ref_completion = adapter

View file

@ -637,8 +637,6 @@ impl TypeScriptLspAdapter {
"node_modules/typescript/lib"
};
// typescript-language-server doesn't support TypeScript 7+, which no longer
// ships `tsserver.js`.
if self
.fs
.is_file(

View file

@ -57,15 +57,9 @@ impl VtslsLspAdapter {
Self::TYPESCRIPT_TSDK_PATH
};
// vtsls doesn't support TypeScript 7+, which no longer ships `tsserver.js`.
if self
.fs
.is_file(
&adapter
.worktree_root_path()
.join(tsdk_path)
.join("tsserver.js"),
)
.is_dir(&adapter.worktree_root_path().join(tsdk_path))
.await
{
Some(tsdk_path)

View file

@ -18,7 +18,6 @@ fuzzy_nucleo.workspace = true
gpui.workspace = true
language.workspace = true
picker.workspace = true
picker_preview.workspace = true
settings.workspace = true
theme.workspace = true
theme_settings.workspace = true

View file

@ -10,8 +10,8 @@ use gpui::{
ParentElement, Point, Rems, Render, Styled, StyledText, Task, TextStyle, WeakEntity, Window,
div, rems,
};
use language::{OffsetRangeExt, Outline, OutlineItem, OutlineSearchEntry};
use picker::{MatchLocation, Picker, PickerDelegate, PreviewUpdate};
use language::{Outline, OutlineItem, OutlineSearchEntry};
use picker::{Picker, PickerDelegate};
use settings::Settings;
use theme::ActiveTheme;
use theme_settings::ThemeSettings;
@ -176,16 +176,9 @@ impl OutlineView {
window: &mut Window,
cx: &mut Context<Self>,
) -> OutlineView {
let project = editor.read(cx).project().cloned();
let delegate = OutlineViewDelegate::new(cx.entity().downgrade(), outline, editor, cx);
let picker = cx.new(|cx| {
let picker = if let Some(project) = project {
let preview = picker_preview::editor_preview(project, window, cx);
Picker::uniform_list_with_preview(delegate, preview, window, cx)
} else {
Picker::uniform_list(delegate, window, cx)
};
picker
Picker::uniform_list(delegate, window, cx)
.max_height(Rems::from_pixels(
window.viewport_size().height * 0.75,
window,
@ -300,30 +293,6 @@ impl PickerDelegate for OutlineViewDelegate {
self.set_selected_index(ix, true, cx);
}
fn try_get_preview_data_for_match(&self, cx: &App) -> Option<PreviewUpdate> {
let selected_match = self.matches.get(self.selected_match_index)?;
let outline_item = self.outline.items.get(selected_match.candidate_id())?;
let multi_buffer = self.active_editor.read(cx).buffer().clone();
let (buffer, start) = multi_buffer
.read(cx)
.text_anchor_for_position(outline_item.selection_range.start, cx)?;
let (end_buffer, end) = multi_buffer
.read(cx)
.text_anchor_for_position(outline_item.selection_range.end, cx)?;
if buffer != end_buffer {
return None;
}
let range = (start..end).to_offset(&buffer.read(cx).text_snapshot());
Some(PreviewUpdate::from_buffer(
buffer,
MatchLocation {
anchor_range: start..end,
range,
},
))
}
fn update_matches(
&mut self,
query: String,

View file

@ -61,7 +61,7 @@ use collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map};
use futures::{
AsyncWriteExt, Future, FutureExt, StreamExt,
channel::oneshot,
future::{Either, Shared, join_all, select},
future::{Either, Shared, join_all, pending, select},
select, select_biased,
stream::FuturesUnordered,
};
@ -123,6 +123,7 @@ use std::{
collections::{VecDeque, hash_map},
convert::TryInto,
ffi::OsStr,
future::ready,
iter, mem,
ops::{ControlFlow, Range},
path::{self, Path, PathBuf},
@ -14151,10 +14152,9 @@ fn lsp_workspace_diagnostics_refresh(
let mut requests = 0;
loop {
let Some(completion_tx) = refresh_rx.recv().await else {
let Some(mut completion_tx) = refresh_rx.recv().await else {
return;
};
let mut completion_txs = completion_tx.into_iter().collect::<Vec<_>>();
'request: loop {
requests += 1;
@ -14170,12 +14170,6 @@ fn lsp_workspace_diagnostics_refresh(
.await;
attempts += 1;
// Absorb refresh requests that queued up in the meantime, so their
// waiters are resolved by this attempt instead of waiting behind it.
while let Ok(queued_refresh) = refresh_rx.try_recv() {
completion_txs.extend(queued_refresh);
}
let Ok(previous_result_ids) = lsp_store.update(cx, |lsp_store, _| {
lsp_store
.result_ids_for_workspace_refresh(server.server_id(), &registration_id_shared)
@ -14202,21 +14196,8 @@ fn lsp_workspace_diagnostics_refresh(
};
progress_rx.try_recv().ok();
// Restart the timeout whenever a partial result arrives: streaming
// servers may legitimately take longer than a single timeout period,
// but a server that stops streaming without completing the request
// should still time out instead of hanging forever.
let timer = async {
loop {
let timer = pin!(server.request_timer(timeout).fuse());
let progress = pin!(progress_rx.recv().fuse());
match select(timer, progress).await {
Either::Left((message, ..)) => break message,
Either::Right((Some(()), ..)) => {}
Either::Right((None, timer)) => break timer.await,
}
}
};
let timer = server.request_timer(timeout).fuse();
let progress = pin!(progress_rx.recv().fuse());
let response_result = server
.request_with_timer::<lsp::WorkspaceDiagnosticRequest, _>(
lsp::WorkspaceDiagnosticParams {
@ -14227,7 +14208,10 @@ fn lsp_workspace_diagnostics_refresh(
partial_result_token: Some(lsp::ProgressToken::String(token)),
},
},
timer,
select(timer, progress).then(|either| match either {
Either::Left((message, ..)) => ready(message).left_future(),
Either::Right(..) => pending::<String>().right_future(),
}),
)
.await;
@ -14236,16 +14220,6 @@ fn lsp_workspace_diagnostics_refresh(
match response_result {
ConnectionResult::Timeout => {
log::error!("Timeout during workspace diagnostics pull");
// Release everyone waiting on this refresh (e.g. the agent's
// diagnostics tool), including waiters that queued up while the
// request was in flight, so they can fall back to cached
// diagnostics instead of waiting through every retry.
while let Ok(queued_refresh) = refresh_rx.try_recv() {
completion_txs.extend(queued_refresh);
}
for tx in completion_txs.drain(..) {
tx.send(false).ok();
}
continue 'request;
}
ConnectionResult::ConnectionReset => {
@ -14254,7 +14228,7 @@ fn lsp_workspace_diagnostics_refresh(
}
ConnectionResult::Result(Err(e)) => {
log::error!("Error during workspace diagnostics pull: {e:#}");
for tx in completion_txs.drain(..) {
if let Some(tx) = completion_tx.take() {
tx.send(false).ok();
}
break 'request;
@ -14274,7 +14248,7 @@ fn lsp_workspace_diagnostics_refresh(
{
return;
}
for tx in completion_txs.drain(..) {
if let Some(tx) = completion_tx.take() {
tx.send(true).ok();
}
break 'request;

View file

@ -4644,71 +4644,6 @@ async fn test_diagnostic_summaries_retained_on_buffer_close_with_workspace_diagn
});
}
#[gpui::test]
async fn test_workspace_diagnostics_pull_timeout_releases_waiters(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/dir"), json!({ "a.rs": "one two three" }))
.await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
lsp::DiagnosticOptions {
identifier: Some("test-ws-timeout".to_string()),
inter_file_dependencies: true,
workspace_diagnostics: true,
work_done_progress_options: Default::default(),
},
)),
..lsp::ServerCapabilities::default()
},
initializer: Some(Box::new(move |fake_server| {
// Simulate a server that holds workspace diagnostic pull requests
// open forever without responding or streaming partial results.
fake_server.set_request_handler::<lsp::request::WorkspaceDiagnosticRequest, _, _>(
move |_, _| async move {
future::pending::<()>().await;
Err(anyhow::anyhow!("should never respond"))
},
);
})),
..FakeLspAdapter::default()
},
);
let (_buffer, _handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
})
.await
.unwrap();
let _fake_server = fake_servers.next().await.unwrap();
cx.executor().run_until_parked();
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let pull_task = lsp_store.update(cx, |lsp_store, cx| {
lsp_store.pull_workspace_diagnostics_once(cx)
});
// The waiter must be released after the request times out, instead of
// being held through every retry of the background refresh loop.
cx.executor().advance_clock(DEFAULT_LSP_REQUEST_TIMEOUT * 2);
let refreshed = pull_task.await;
assert!(
!refreshed,
"pull should report a failed refresh when the server never responds"
);
}
#[gpui::test]
async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
init_test(cx);

View file

@ -31,7 +31,7 @@ WizardStyle=modern
CloseApplications=force
#if GetEnv("ZED_SIGN_BUNDLE") != ""
#if GetEnv("CI") != ""
SignTool=Defaultsign
#endif

View file

@ -13,7 +13,6 @@ $ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
$buildSuccess = $false
$canCodeSign = $false
$OSArchitecture = switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) {
"X64" { "x86_64" }
@ -67,31 +66,18 @@ function CheckEnvironmentVariables {
return
}
$requiredVars = @('ZED_WORKSPACE', 'RELEASE_VERSION', 'ZED_RELEASE_CHANNEL')
foreach ($var in $requiredVars) {
if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($var))) {
Write-Error "$var is not set"
exit 1
}
}
# On PRs from forks the signing secrets are not populated,
# so skip code signing instead of failing, like bundle-mac does.
$signingVars = @(
$requiredVars = @(
'ZED_WORKSPACE', 'RELEASE_VERSION', 'ZED_RELEASE_CHANNEL',
'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', 'AZURE_CLIENT_SECRET',
'ACCOUNT_NAME', 'CERT_PROFILE_NAME', 'ENDPOINT',
'FILE_DIGEST', 'TIMESTAMP_DIGEST', 'TIMESTAMP_SERVER'
)
$missingVars = @($signingVars | Where-Object { [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_)) })
if ($missingVars.Count -eq 0) {
$script:canCodeSign = $true
} else {
Write-Host "====== WARNING ======"
Write-Host "One or more of the following variables are missing: $($missingVars -join ', ')"
Write-Host "This bundle will not be code signed"
Write-Host "====== WARNING ======"
foreach ($var in $requiredVars) {
if (-not (Test-Path "env:$var")) {
Write-Error "$var is not set"
exit 1
}
}
}
@ -142,7 +128,7 @@ function BuildRemoteServer {
# Create zipped remote server binary
$remoteServerSrc = (Resolve-Path ".\$CargoOutDir\remote_server.exe").Path
if ($canCodeSign) {
if ($env:CI) {
Write-Output "Code signing remote_server.exe"
& "$innoDir\sign.ps1" $remoteServerSrc
}
@ -173,7 +159,7 @@ function UploadToSentry {
Write-Output "install with: 'winget install -e --id=Sentry.sentry-cli'"
return
}
if ([string]::IsNullOrWhiteSpace($env:SENTRY_AUTH_TOKEN)) {
if (-not (Test-Path "env:SENTRY_AUTH_TOKEN")) {
Write-Output "missing SENTRY_AUTH_TOKEN. skipping sentry upload."
return
}
@ -214,7 +200,7 @@ function MakeAppx {
}
function SignZedAndItsFriends {
if (-not $canCodeSign) {
if (-not $env:CI) {
return
}
@ -354,9 +340,7 @@ function BuildInstaller {
}
$innoArgs = @($issFilePath) + $defs
if($canCodeSign) {
# Checked by zed.iss to decide whether to sign the installer.
$env:ZED_SIGN_BUNDLE = "1"
if($env:CI) {
$signTool = "powershell.exe -ExecutionPolicy Bypass -File $innoDir\sign.ps1 `$f"
$innoArgs += "/sDefaultsign=`"$signTool`""
}