Compare commits
6 Commits
nathan
...
devcontain
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0ebb401c4 | ||
|
|
ca47822667 | ||
|
|
a34fe06bb1 | ||
|
|
0ce484e66c | ||
|
|
251033f88f | ||
|
|
9f90c1a1b7 |
@@ -278,6 +278,7 @@ pub struct AcpThreadView {
|
||||
thread_retry_status: Option<RetryStatus>,
|
||||
thread_error: Option<ThreadError>,
|
||||
thread_error_markdown: Option<Entity<Markdown>>,
|
||||
token_limit_callout_dismissed: bool,
|
||||
thread_feedback: ThreadFeedbackState,
|
||||
list_state: ListState,
|
||||
auth_task: Option<Task<()>>,
|
||||
@@ -430,13 +431,13 @@ impl AcpThreadView {
|
||||
message_editor,
|
||||
model_selector: None,
|
||||
profile_selector: None,
|
||||
|
||||
notifications: Vec::new(),
|
||||
notification_subscriptions: HashMap::default(),
|
||||
list_state: list_state,
|
||||
thread_retry_status: None,
|
||||
thread_error: None,
|
||||
thread_error_markdown: None,
|
||||
token_limit_callout_dismissed: false,
|
||||
thread_feedback: Default::default(),
|
||||
auth_task: None,
|
||||
expanded_tool_calls: HashSet::default(),
|
||||
@@ -1394,6 +1395,7 @@ impl AcpThreadView {
|
||||
fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
|
||||
self.thread_error = None;
|
||||
self.thread_error_markdown = None;
|
||||
self.token_limit_callout_dismissed = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -5391,22 +5393,26 @@ impl AcpThreadView {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_token_limit_callout(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Callout> {
|
||||
fn render_token_limit_callout(&self, cx: &mut Context<Self>) -> Option<Callout> {
|
||||
if self.token_limit_callout_dismissed {
|
||||
return None;
|
||||
}
|
||||
|
||||
let token_usage = self.thread()?.read(cx).token_usage()?;
|
||||
let ratio = token_usage.ratio();
|
||||
|
||||
let (severity, title) = match ratio {
|
||||
let (severity, icon, title) = match ratio {
|
||||
acp_thread::TokenUsageRatio::Normal => return None,
|
||||
acp_thread::TokenUsageRatio::Warning => {
|
||||
(Severity::Warning, "Thread reaching the token limit soon")
|
||||
}
|
||||
acp_thread::TokenUsageRatio::Exceeded => {
|
||||
(Severity::Error, "Thread reached the token limit")
|
||||
}
|
||||
acp_thread::TokenUsageRatio::Warning => (
|
||||
Severity::Warning,
|
||||
IconName::Warning,
|
||||
"Thread reaching the token limit soon",
|
||||
),
|
||||
acp_thread::TokenUsageRatio::Exceeded => (
|
||||
Severity::Error,
|
||||
IconName::XCircle,
|
||||
"Thread reached the token limit",
|
||||
),
|
||||
};
|
||||
|
||||
let burn_mode_available = self.as_native_thread(cx).is_some_and(|thread| {
|
||||
@@ -5426,7 +5432,7 @@ impl AcpThreadView {
|
||||
Some(
|
||||
Callout::new()
|
||||
.severity(severity)
|
||||
.line_height(line_height)
|
||||
.icon(icon)
|
||||
.title(title)
|
||||
.description(description)
|
||||
.actions_slot(
|
||||
@@ -5458,7 +5464,8 @@ impl AcpThreadView {
|
||||
})),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.dismiss_action(self.dismiss_error_button(cx)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5892,7 +5899,7 @@ impl AcpThreadView {
|
||||
fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Dismiss Error"))
|
||||
.tooltip(Tooltip::text("Dismiss"))
|
||||
.on_click(cx.listener({
|
||||
move |this, _, _, cx| {
|
||||
this.clear_thread_error(cx);
|
||||
@@ -6152,7 +6159,7 @@ impl Render for AcpThreadView {
|
||||
if let Some(usage_callout) = self.render_usage_callout(line_height, cx) {
|
||||
Some(usage_callout.into_any_element())
|
||||
} else {
|
||||
self.render_token_limit_callout(line_height, cx)
|
||||
self.render_token_limit_callout(cx)
|
||||
.map(|token_limit_callout| token_limit_callout.into_any_element())
|
||||
},
|
||||
)
|
||||
|
||||
@@ -8,9 +8,9 @@ use git::{
|
||||
parse_git_remote_url,
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, AsyncApp, AsyncWindowContext, Context, Element, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
|
||||
PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,
|
||||
AnyElement, App, AppContext as _, AsyncApp, AsyncWindowContext, ClipboardItem, Context,
|
||||
Element, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement,
|
||||
ParentElement, PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,
|
||||
};
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
|
||||
@@ -24,7 +24,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{DiffStat, Tooltip, prelude::*};
|
||||
use ui::{ButtonLike, DiffStat, Tooltip, prelude::*};
|
||||
use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
|
||||
use workspace::item::TabTooltipContent;
|
||||
use workspace::{
|
||||
@@ -383,6 +383,7 @@ impl CommitView {
|
||||
fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let commit = &self.commit;
|
||||
let author_name = commit.author_name.clone();
|
||||
let commit_sha = commit.sha.clone();
|
||||
let commit_date = time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)
|
||||
.unwrap_or_else(|_| time::OffsetDateTime::now_utc());
|
||||
let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
|
||||
@@ -429,6 +430,19 @@ impl CommitView {
|
||||
.full_width()
|
||||
});
|
||||
|
||||
let clipboard_has_link = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|entry| entry.text())
|
||||
.map_or(false, |clipboard_text| {
|
||||
clipboard_text.trim() == commit_sha.as_ref()
|
||||
});
|
||||
|
||||
let (copy_icon, copy_icon_color) = if clipboard_has_link {
|
||||
(IconName::Check, Color::Success)
|
||||
} else {
|
||||
(IconName::Copy, Color::Muted)
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
@@ -454,13 +468,47 @@ impl CommitView {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(author_name).color(Color::Default))
|
||||
.child(
|
||||
Label::new(format!("Commit:{}", commit.sha))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.truncate()
|
||||
.buffer_font(cx),
|
||||
),
|
||||
.child({
|
||||
ButtonLike::new("sha")
|
||||
.child(
|
||||
h_flex()
|
||||
.group("sha_btn")
|
||||
.size_full()
|
||||
.max_w_32()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(commit_sha.clone())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.truncate()
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
div().visible_on_hover("sha_btn").child(
|
||||
Icon::new(copy_icon)
|
||||
.color(copy_icon_color)
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
),
|
||||
)
|
||||
.tooltip({
|
||||
let commit_sha = commit_sha.clone();
|
||||
move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Copy Commit SHA",
|
||||
None,
|
||||
commit_sha.clone(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
commit_sha.to_string(),
|
||||
));
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name = "JSONC"
|
||||
grammar = "jsonc"
|
||||
path_suffixes = ["jsonc", "bun.lock", "tsconfig.json", "pyrightconfig.json"]
|
||||
path_suffixes = ["jsonc", "bun.lock", "devcontainer.json", "pyrightconfig.json", "tsconfig.json"]
|
||||
line_comments = ["// "]
|
||||
autoclose_before = ",]}"
|
||||
brackets = [
|
||||
|
||||
@@ -1293,34 +1293,13 @@ impl Project {
|
||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.detach();
|
||||
if init_worktree_trust {
|
||||
let trust_remote_project = match &connection_options {
|
||||
RemoteConnectionOptions::Ssh(..) | RemoteConnectionOptions::Wsl(..) => false,
|
||||
RemoteConnectionOptions::Docker(..) => true,
|
||||
};
|
||||
let remote_host = RemoteHostLocation::from(connection_options);
|
||||
trusted_worktrees::track_worktree_trust(
|
||||
worktree_store.clone(),
|
||||
Some(remote_host.clone()),
|
||||
Some(RemoteHostLocation::from(connection_options)),
|
||||
None,
|
||||
Some((remote_proto.clone(), REMOTE_SERVER_PROJECT_ID)),
|
||||
cx,
|
||||
);
|
||||
if trust_remote_project {
|
||||
if let Some(trusted_worktres) = TrustedWorktrees::try_get_global(cx) {
|
||||
trusted_worktres.update(cx, |trusted_worktres, cx| {
|
||||
trusted_worktres.trust(
|
||||
worktree_store
|
||||
.read(cx)
|
||||
.worktrees()
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
.map(PathTrust::Worktree)
|
||||
.collect(),
|
||||
Some(remote_host),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let weak_self = cx.weak_entity();
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::AsyncWindowContext;
|
||||
use gpui::{
|
||||
Action, AsyncWindowContext, DismissEvent, EventEmitter, FocusHandle, Focusable, RenderOnce,
|
||||
};
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde::Deserialize;
|
||||
use settings::DevContainerConnection;
|
||||
use smol::fs;
|
||||
use workspace::Workspace;
|
||||
use ui::{
|
||||
App, Color, Context, Headline, HeadlineSize, Icon, IconName, InteractiveElement, IntoElement,
|
||||
Label, ListItem, ListSeparator, ModalHeader, Navigable, NavigableEntry, ParentElement, Render,
|
||||
Styled, StyledExt, Toggleable, Window, div, rems,
|
||||
};
|
||||
use workspace::{ModalView, Workspace, with_active_or_new_workspace};
|
||||
|
||||
use crate::remote_connections::Connection;
|
||||
|
||||
@@ -275,6 +282,122 @@ pub(crate) enum DevContainerError {
|
||||
DevContainerParseFailed,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
#[action(namespace = containers)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct InitDevContainer;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.on_action(|_: &InitDevContainer, cx| {
|
||||
with_active_or_new_workspace(cx, move |workspace, window, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| DevContainerModal::new(window, cx));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
struct DevContainerModal {
|
||||
focus_handle: FocusHandle,
|
||||
search_navigable_entry: NavigableEntry,
|
||||
other_navigable_entry: NavigableEntry,
|
||||
}
|
||||
|
||||
impl DevContainerModal {
|
||||
fn new(window: &mut Window, cx: &mut App) -> Self {
|
||||
let search_navigable_entry = NavigableEntry::focusable(cx);
|
||||
let other_navigable_entry = NavigableEntry::focusable(cx);
|
||||
let focus_handle = cx.focus_handle();
|
||||
DevContainerModal {
|
||||
focus_handle,
|
||||
search_navigable_entry,
|
||||
other_navigable_entry,
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for DevContainerModal {}
|
||||
impl EventEmitter<DismissEvent> for DevContainerModal {}
|
||||
impl Focusable for DevContainerModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DevContainerModal {
|
||||
fn render(
|
||||
&mut self,
|
||||
window: &mut ui::Window,
|
||||
cx: &mut ui::Context<Self>,
|
||||
) -> impl ui::IntoElement {
|
||||
let mut view =
|
||||
Navigable::new(
|
||||
div()
|
||||
.child(div().track_focus(&self.focus_handle).child(
|
||||
ModalHeader::new().child(
|
||||
Headline::new("Create Dev Container").size(HeadlineSize::XSmall),
|
||||
),
|
||||
))
|
||||
.child(ListSeparator)
|
||||
.child(
|
||||
div()
|
||||
.track_focus(&self.search_navigable_entry.focus_handle)
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
|
||||
println!("action on search containers");
|
||||
}))
|
||||
.child(
|
||||
ListItem::new("li-search-containers")
|
||||
.inset(true)
|
||||
.spacing(ui::ListItemSpacing::Sparse)
|
||||
.start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
|
||||
.toggle_state(
|
||||
self.search_navigable_entry
|
||||
.focus_handle
|
||||
.contains_focused(window, cx),
|
||||
)
|
||||
.child(Label::new("Search for dev containers in registry")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.track_focus(&self.other_navigable_entry.focus_handle)
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
|
||||
println!("action on other containers");
|
||||
}))
|
||||
.child(
|
||||
ListItem::new("li-search-containers")
|
||||
.inset(true)
|
||||
.spacing(ui::ListItemSpacing::Sparse)
|
||||
.start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
|
||||
.toggle_state(
|
||||
self.other_navigable_entry
|
||||
.focus_handle
|
||||
.contains_focused(window, cx),
|
||||
)
|
||||
.child(Label::new("Do another thing")),
|
||||
),
|
||||
)
|
||||
.into_any_element(),
|
||||
);
|
||||
view = view.entry(self.search_navigable_entry.clone());
|
||||
view = view.entry(self.other_navigable_entry.clone());
|
||||
|
||||
// // This is an interesting edge. Can't focus in render, or you'll just override whatever was focused before.
|
||||
// // self.search_navigable_entry.focus_handle.focus(window, cx);
|
||||
|
||||
// view.render(window, cx).into_any_element()
|
||||
div()
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
// WHY IS THIS NEEDED FOR ACTION DISPATCH OMG
|
||||
.key_context("ContainerModal")
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.child(view.render(window, cx).into_any_element())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
mod dev_container;
|
||||
pub mod dev_container;
|
||||
mod dev_container_suggest;
|
||||
pub mod disconnected_overlay;
|
||||
mod remote_connections;
|
||||
|
||||
@@ -158,6 +158,9 @@ fn handle_rpc_messages_over_child_process_stdio(
|
||||
}
|
||||
};
|
||||
let status = remote_proxy_process.status().await?.code().unwrap_or(1);
|
||||
if status != 0 {
|
||||
anyhow::bail!("Remote server exited with status {status}");
|
||||
}
|
||||
match result {
|
||||
Ok(_) => Ok(status),
|
||||
Err(error) => Err(error),
|
||||
|
||||
@@ -582,19 +582,21 @@ impl RemoteConnection for DockerExecConnection {
|
||||
return Task::ready(Err(anyhow!("Remote binary path not set")));
|
||||
};
|
||||
|
||||
let mut docker_args = vec![
|
||||
"exec".to_string(),
|
||||
"-w".to_string(),
|
||||
self.remote_dir_for_server.clone(),
|
||||
"-i".to_string(),
|
||||
self.connection_options.container_id.to_string(),
|
||||
];
|
||||
let mut docker_args = vec!["exec".to_string()];
|
||||
for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] {
|
||||
if let Some(value) = std::env::var(env_var).ok() {
|
||||
docker_args.push("-e".to_string());
|
||||
docker_args.push(format!("{}='{}'", env_var, value));
|
||||
}
|
||||
}
|
||||
|
||||
docker_args.extend([
|
||||
"-w".to_string(),
|
||||
self.remote_dir_for_server.clone(),
|
||||
"-i".to_string(),
|
||||
self.connection_options.container_id.to_string(),
|
||||
]);
|
||||
|
||||
let val = remote_binary_relpath
|
||||
.display(self.path_style())
|
||||
.into_owned();
|
||||
|
||||
@@ -1065,73 +1065,43 @@ impl Element for TerminalElement {
|
||||
// then have that representation be converted to the appropriate highlight data structure
|
||||
|
||||
let content_mode = self.terminal_view.read(cx).content_mode(window, cx);
|
||||
let (rects, batched_text_runs) = match content_mode {
|
||||
ContentMode::Scrollable => {
|
||||
// In scrollable mode, the terminal already provides cells
|
||||
// that are correctly positioned for the current viewport
|
||||
// based on its display_offset. We don't need additional filtering.
|
||||
TerminalElement::layout_grid(
|
||||
cells.iter().cloned(),
|
||||
0,
|
||||
&text_style,
|
||||
last_hovered_word.as_ref().map(|last_hovered_word| {
|
||||
(link_style, &last_hovered_word.word_match)
|
||||
}),
|
||||
minimum_contrast,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
ContentMode::Inline { .. } => {
|
||||
let intersection = window.content_mask().bounds.intersect(&bounds);
|
||||
let start_row = (intersection.top() - bounds.top()) / line_height_px;
|
||||
let end_row = start_row + intersection.size.height / line_height_px;
|
||||
let line_range = (start_row as i32)..=(end_row as i32);
|
||||
|
||||
// Calculate the intersection of the terminal's bounds with the current
|
||||
// content mask (the visible viewport after all parent clipping).
|
||||
// This allows us to only render cells that are actually visible, which is
|
||||
// critical for performance when terminals are inside scrollable containers
|
||||
// like the Agent Panel thread view.
|
||||
//
|
||||
// This optimization is analogous to the editor optimization in PR #45077
|
||||
// which fixed performance issues with large AutoHeight editors inside Lists.
|
||||
let visible_bounds = window.content_mask().bounds;
|
||||
let intersection = visible_bounds.intersect(&bounds);
|
||||
|
||||
// If the terminal is entirely outside the viewport, skip all cell processing.
|
||||
// This handles the case where the terminal has been scrolled past (above or
|
||||
// below the viewport), similar to the editor fix in PR #45077 where start_row
|
||||
// could exceed max_row when the editor was positioned above the viewport.
|
||||
let (rects, batched_text_runs) = if intersection.size.height <= px(0.)
|
||||
|| intersection.size.width <= px(0.)
|
||||
{
|
||||
(Vec::new(), Vec::new())
|
||||
} else if intersection == bounds {
|
||||
// Fast path: terminal fully visible, no clipping needed.
|
||||
// Avoid grouping/allocation overhead by streaming cells directly.
|
||||
TerminalElement::layout_grid(
|
||||
cells.iter().cloned(),
|
||||
0,
|
||||
&text_style,
|
||||
last_hovered_word
|
||||
.as_ref()
|
||||
.map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
|
||||
minimum_contrast,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
// Calculate which screen rows are visible based on pixel positions.
|
||||
// This works for both Scrollable and Inline modes because we filter
|
||||
// by screen position (enumerated line group index), not by the cell's
|
||||
// internal line number (which can be negative in Scrollable mode for
|
||||
// scrollback history).
|
||||
let rows_above_viewport =
|
||||
((intersection.top() - bounds.top()).max(px(0.)) / line_height_px) as usize;
|
||||
let visible_row_count =
|
||||
(intersection.size.height / line_height_px).ceil() as usize + 1;
|
||||
|
||||
// Group cells by line and filter to only the visible screen rows.
|
||||
// skip() and take() work on enumerated line groups (screen position),
|
||||
// making this work regardless of the actual cell.point.line values.
|
||||
let visible_cells: Vec<_> = cells
|
||||
.iter()
|
||||
.chunk_by(|c| c.point.line)
|
||||
.into_iter()
|
||||
.skip(rows_above_viewport)
|
||||
.take(visible_row_count)
|
||||
.flat_map(|(_, line_cells)| line_cells)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
TerminalElement::layout_grid(
|
||||
visible_cells.into_iter(),
|
||||
rows_above_viewport as i32,
|
||||
&text_style,
|
||||
last_hovered_word
|
||||
.as_ref()
|
||||
.map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
|
||||
minimum_contrast,
|
||||
cx,
|
||||
)
|
||||
TerminalElement::layout_grid(
|
||||
cells
|
||||
.iter()
|
||||
.skip_while(|i| &i.point.line < line_range.start())
|
||||
.take_while(|i| &i.point.line <= line_range.end())
|
||||
.cloned(),
|
||||
*line_range.start(),
|
||||
&text_style,
|
||||
last_hovered_word.as_ref().map(|last_hovered_word| {
|
||||
(link_style, &last_hovered_word.word_match)
|
||||
}),
|
||||
minimum_contrast,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// Layout cursor. Rectangle is used for IME, so we should lay it out even
|
||||
@@ -2089,248 +2059,4 @@ mod tests {
|
||||
let merged2 = merge_background_regions(regions2);
|
||||
assert_eq!(merged2.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_screen_position_filtering_with_positive_lines() {
|
||||
// Test the unified screen-position-based filtering approach.
|
||||
// This works for both Scrollable and Inline modes because we filter
|
||||
// by enumerated line group index, not by cell.point.line values.
|
||||
use itertools::Itertools;
|
||||
use terminal::IndexedCell;
|
||||
use terminal::alacritty_terminal::index::{Column, Line, Point as AlacPoint};
|
||||
use terminal::alacritty_terminal::term::cell::Cell;
|
||||
|
||||
// Create mock cells for lines 0-23 (typical terminal with 24 visible lines)
|
||||
let mut cells = Vec::new();
|
||||
for line in 0..24i32 {
|
||||
for col in 0..3i32 {
|
||||
cells.push(IndexedCell {
|
||||
point: AlacPoint::new(Line(line), Column(col as usize)),
|
||||
cell: Cell::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Scenario: Terminal partially scrolled above viewport
|
||||
// First 5 lines (0-4) are clipped, lines 5-15 should be visible
|
||||
let rows_above_viewport = 5usize;
|
||||
let visible_row_count = 11usize;
|
||||
|
||||
// Apply the same filtering logic as in the render code
|
||||
let filtered: Vec<_> = cells
|
||||
.iter()
|
||||
.chunk_by(|c| c.point.line)
|
||||
.into_iter()
|
||||
.skip(rows_above_viewport)
|
||||
.take(visible_row_count)
|
||||
.flat_map(|(_, line_cells)| line_cells)
|
||||
.collect();
|
||||
|
||||
// Should have lines 5-15 (11 lines * 3 cells each = 33 cells)
|
||||
assert_eq!(filtered.len(), 11 * 3, "Should have 33 cells for 11 lines");
|
||||
|
||||
// First filtered cell should be line 5
|
||||
assert_eq!(
|
||||
filtered.first().unwrap().point.line,
|
||||
Line(5),
|
||||
"First cell should be on line 5"
|
||||
);
|
||||
|
||||
// Last filtered cell should be line 15
|
||||
assert_eq!(
|
||||
filtered.last().unwrap().point.line,
|
||||
Line(15),
|
||||
"Last cell should be on line 15"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_screen_position_filtering_with_negative_lines() {
|
||||
// This is the key test! In Scrollable mode, cells have NEGATIVE line numbers
|
||||
// for scrollback history. The screen-position filtering approach works because
|
||||
// we filter by enumerated line group index, not by cell.point.line values.
|
||||
use itertools::Itertools;
|
||||
use terminal::IndexedCell;
|
||||
use terminal::alacritty_terminal::index::{Column, Line, Point as AlacPoint};
|
||||
use terminal::alacritty_terminal::term::cell::Cell;
|
||||
|
||||
// Simulate cells from a scrolled terminal with scrollback
|
||||
// These have negative line numbers representing scrollback history
|
||||
let mut scrollback_cells = Vec::new();
|
||||
for line in -588i32..=-578i32 {
|
||||
for col in 0..80i32 {
|
||||
scrollback_cells.push(IndexedCell {
|
||||
point: AlacPoint::new(Line(line), Column(col as usize)),
|
||||
cell: Cell::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Scenario: First 3 screen rows clipped, show next 5 rows
|
||||
let rows_above_viewport = 3usize;
|
||||
let visible_row_count = 5usize;
|
||||
|
||||
// Apply the same filtering logic as in the render code
|
||||
let filtered: Vec<_> = scrollback_cells
|
||||
.iter()
|
||||
.chunk_by(|c| c.point.line)
|
||||
.into_iter()
|
||||
.skip(rows_above_viewport)
|
||||
.take(visible_row_count)
|
||||
.flat_map(|(_, line_cells)| line_cells)
|
||||
.collect();
|
||||
|
||||
// Should have 5 lines * 80 cells = 400 cells
|
||||
assert_eq!(filtered.len(), 5 * 80, "Should have 400 cells for 5 lines");
|
||||
|
||||
// First filtered cell should be line -585 (skipped 3 lines from -588)
|
||||
assert_eq!(
|
||||
filtered.first().unwrap().point.line,
|
||||
Line(-585),
|
||||
"First cell should be on line -585"
|
||||
);
|
||||
|
||||
// Last filtered cell should be line -581 (5 lines: -585, -584, -583, -582, -581)
|
||||
assert_eq!(
|
||||
filtered.last().unwrap().point.line,
|
||||
Line(-581),
|
||||
"Last cell should be on line -581"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_screen_position_filtering_skip_all() {
|
||||
// Test what happens when we skip more rows than exist
|
||||
use itertools::Itertools;
|
||||
use terminal::IndexedCell;
|
||||
use terminal::alacritty_terminal::index::{Column, Line, Point as AlacPoint};
|
||||
use terminal::alacritty_terminal::term::cell::Cell;
|
||||
|
||||
let mut cells = Vec::new();
|
||||
for line in 0..10i32 {
|
||||
cells.push(IndexedCell {
|
||||
point: AlacPoint::new(Line(line), Column(0)),
|
||||
cell: Cell::default(),
|
||||
});
|
||||
}
|
||||
|
||||
// Skip more rows than exist
|
||||
let rows_above_viewport = 100usize;
|
||||
let visible_row_count = 5usize;
|
||||
|
||||
let filtered: Vec<_> = cells
|
||||
.iter()
|
||||
.chunk_by(|c| c.point.line)
|
||||
.into_iter()
|
||||
.skip(rows_above_viewport)
|
||||
.take(visible_row_count)
|
||||
.flat_map(|(_, line_cells)| line_cells)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
filtered.len(),
|
||||
0,
|
||||
"Should have no cells when all are skipped"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_grid_positioning_math() {
|
||||
// Test the math that layout_grid uses for positioning.
|
||||
// When we skip N rows, we pass N as start_line_offset to layout_grid,
|
||||
// which positions the first visible line at screen row N.
|
||||
|
||||
// Scenario: Terminal at y=-100px, line_height=20px
|
||||
// First 5 screen rows are above viewport (clipped)
|
||||
// So we skip 5 rows and pass offset=5 to layout_grid
|
||||
|
||||
let terminal_origin_y = -100.0f32;
|
||||
let line_height = 20.0f32;
|
||||
let rows_skipped = 5;
|
||||
|
||||
// The first visible line (at offset 5) renders at:
|
||||
// y = terminal_origin + offset * line_height = -100 + 5*20 = 0
|
||||
let first_visible_y = terminal_origin_y + rows_skipped as f32 * line_height;
|
||||
assert_eq!(
|
||||
first_visible_y, 0.0,
|
||||
"First visible line should be at viewport top (y=0)"
|
||||
);
|
||||
|
||||
// The 6th visible line (at offset 10) renders at:
|
||||
let sixth_visible_y = terminal_origin_y + (rows_skipped + 5) as f32 * line_height;
|
||||
assert_eq!(
|
||||
sixth_visible_y, 100.0,
|
||||
"6th visible line should be at y=100"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unified_filtering_works_for_both_modes() {
|
||||
// This test proves that the unified screen-position filtering approach
|
||||
// works for BOTH positive line numbers (Inline mode) and negative line
|
||||
// numbers (Scrollable mode with scrollback).
|
||||
//
|
||||
// The key insight: we filter by enumerated line group index (screen position),
|
||||
// not by cell.point.line values. This makes the filtering agnostic to the
|
||||
// actual line numbers in the cells.
|
||||
use itertools::Itertools;
|
||||
use terminal::IndexedCell;
|
||||
use terminal::alacritty_terminal::index::{Column, Line, Point as AlacPoint};
|
||||
use terminal::alacritty_terminal::term::cell::Cell;
|
||||
|
||||
// Test with positive line numbers (Inline mode style)
|
||||
let positive_cells: Vec<_> = (0..10i32)
|
||||
.flat_map(|line| {
|
||||
(0..3i32).map(move |col| IndexedCell {
|
||||
point: AlacPoint::new(Line(line), Column(col as usize)),
|
||||
cell: Cell::default(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Test with negative line numbers (Scrollable mode with scrollback)
|
||||
let negative_cells: Vec<_> = (-10i32..0i32)
|
||||
.flat_map(|line| {
|
||||
(0..3i32).map(move |col| IndexedCell {
|
||||
point: AlacPoint::new(Line(line), Column(col as usize)),
|
||||
cell: Cell::default(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let rows_to_skip = 3usize;
|
||||
let rows_to_take = 4usize;
|
||||
|
||||
// Filter positive cells
|
||||
let positive_filtered: Vec<_> = positive_cells
|
||||
.iter()
|
||||
.chunk_by(|c| c.point.line)
|
||||
.into_iter()
|
||||
.skip(rows_to_skip)
|
||||
.take(rows_to_take)
|
||||
.flat_map(|(_, cells)| cells)
|
||||
.collect();
|
||||
|
||||
// Filter negative cells
|
||||
let negative_filtered: Vec<_> = negative_cells
|
||||
.iter()
|
||||
.chunk_by(|c| c.point.line)
|
||||
.into_iter()
|
||||
.skip(rows_to_skip)
|
||||
.take(rows_to_take)
|
||||
.flat_map(|(_, cells)| cells)
|
||||
.collect();
|
||||
|
||||
// Both should have same count: 4 lines * 3 cells = 12
|
||||
assert_eq!(positive_filtered.len(), 12);
|
||||
assert_eq!(negative_filtered.len(), 12);
|
||||
|
||||
// Positive: lines 3, 4, 5, 6
|
||||
assert_eq!(positive_filtered.first().unwrap().point.line, Line(3));
|
||||
assert_eq!(positive_filtered.last().unwrap().point.line, Line(6));
|
||||
|
||||
// Negative: lines -7, -6, -5, -4
|
||||
assert_eq!(negative_filtered.first().unwrap().point.line, Line(-7));
|
||||
assert_eq!(negative_filtered.last().unwrap().point.line, Line(-4));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ use assets::Assets;
|
||||
use node_runtime::{NodeBinaryOptions, NodeRuntime};
|
||||
use parking_lot::Mutex;
|
||||
use project::{project_settings::ProjectSettings, trusted_worktrees};
|
||||
use recent_projects::{SshSettings, open_remote_project};
|
||||
use recent_projects::{SshSettings, dev_container, open_remote_project};
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use session::{AppSession, Session};
|
||||
use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
|
||||
@@ -616,6 +616,7 @@ fn main() {
|
||||
agent_ui_v2::agents_panel::init(cx);
|
||||
repl::init(app_state.fs.clone(), cx);
|
||||
recent_projects::init(cx);
|
||||
dev_container::init(cx);
|
||||
|
||||
load_embedded_fonts(cx);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user