Add terminal inline assistant (#13638)

Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
This commit is contained in:
Bennet Bo Fenner 2024-07-01 20:53:56 +02:00 committed by GitHub
parent c516b8f038
commit e243856559
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1587 additions and 141 deletions

View file

@ -8,12 +8,12 @@ use futures::{stream::FuturesUnordered, StreamExt};
use gpui::{
anchored, deferred, div, impl_actions, AnyElement, AppContext, DismissEvent, EventEmitter,
FocusHandle, FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton,
MouseDownEvent, Pixels, Render, Styled, Subscription, Task, View, VisualContext, WeakView,
MouseDownEvent, Pixels, Render, ScrollWheelEvent, Styled, Subscription, Task, View,
VisualContext, WeakView,
};
use language::Bias;
use persistence::TERMINAL_DB;
use project::{search::SearchQuery, Fs, LocalWorktree, Metadata, Project};
use settings::SettingsStore;
use task::TerminalWorkDir;
use terminal::{
alacritty_terminal::{
@ -23,8 +23,9 @@ use terminal::{
terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory},
Clear, Copy, Event, MaybeNavigationTarget, Paste, ScrollLineDown, ScrollLineUp, ScrollPageDown,
ScrollPageUp, ScrollToBottom, ScrollToTop, ShowCharacterPalette, TaskStatus, Terminal,
TerminalSize,
};
use terminal_element::TerminalElement;
use terminal_element::{is_blank, TerminalElement};
use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Tooltip};
use util::{paths::PathLikeWithPosition, ResultExt};
use workspace::{
@ -39,10 +40,11 @@ use workspace::{
use anyhow::Context;
use dirs::home_dir;
use serde::Deserialize;
use settings::Settings;
use settings::{Settings, SettingsStore};
use smol::Timer;
use std::{
cmp,
ops::RangeInclusive,
path::{Path, PathBuf},
sync::Arc,
@ -79,6 +81,16 @@ pub fn init(cx: &mut AppContext) {
.detach();
}
pub struct BlockProperties {
pub height: u8,
pub render: Box<dyn Send + Fn(&mut BlockContext) -> AnyElement>,
}
pub struct BlockContext<'a, 'b> {
pub context: &'b mut WindowContext<'a>,
pub dimensions: TerminalSize,
}
///A terminal view, maintains the PTY's file handles and communicates with the terminal
pub struct TerminalView {
terminal: Model<Terminal>,
@ -94,6 +106,8 @@ pub struct TerminalView {
can_navigate_to_selected_word: bool,
workspace_id: Option<WorkspaceId>,
show_title: bool,
block_below_cursor: Option<Arc<BlockProperties>>,
scroll_top: Pixels,
_subscriptions: Vec<Subscription>,
_terminal_subscriptions: Vec<Subscription>,
}
@ -170,6 +184,8 @@ impl TerminalView {
can_navigate_to_selected_word: false,
workspace_id,
show_title: TerminalSettings::get_global(cx).toolbar.title,
block_below_cursor: None,
scroll_top: Pixels::ZERO,
_subscriptions: vec![
focus_in,
focus_out,
@ -248,27 +264,123 @@ impl TerminalView {
}
fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
self.scroll_top = px(0.);
self.terminal.update(cx, |term, _| term.clear());
cx.notify();
}
fn max_scroll_top(&self, cx: &AppContext) -> Pixels {
let terminal = self.terminal.read(cx);
let Some(block) = self.block_below_cursor.as_ref() else {
return Pixels::ZERO;
};
let line_height = terminal.last_content().size.line_height;
let mut terminal_lines = terminal.total_lines();
let viewport_lines = terminal.viewport_lines();
if terminal.total_lines() == terminal.viewport_lines() {
let mut last_line = None;
for cell in terminal.last_content.cells.iter().rev() {
if !is_blank(cell) {
break;
}
let last_line = last_line.get_or_insert(cell.point.line);
if *last_line != cell.point.line {
terminal_lines -= 1;
}
*last_line = cell.point.line;
}
}
let max_scroll_top_in_lines =
(block.height as usize).saturating_sub(viewport_lines.saturating_sub(terminal_lines));
max_scroll_top_in_lines as f32 * line_height
}
fn scroll_wheel(
&mut self,
event: &ScrollWheelEvent,
origin: gpui::Point<Pixels>,
cx: &mut ViewContext<Self>,
) {
let terminal_content = self.terminal.read(cx).last_content();
if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 {
let line_height = terminal_content.size.line_height;
let y_delta = event.delta.pixel_delta(line_height).y;
if y_delta < Pixels::ZERO || self.scroll_top > Pixels::ZERO {
self.scroll_top = cmp::max(
Pixels::ZERO,
cmp::min(self.scroll_top - y_delta, self.max_scroll_top(cx)),
);
cx.notify();
return;
}
}
self.terminal
.update(cx, |term, _| term.scroll_wheel(event, origin));
}
fn scroll_line_up(&mut self, _: &ScrollLineUp, cx: &mut ViewContext<Self>) {
let terminal_content = self.terminal.read(cx).last_content();
if self.block_below_cursor.is_some()
&& terminal_content.display_offset == 0
&& self.scroll_top > Pixels::ZERO
{
let line_height = terminal_content.size.line_height;
self.scroll_top = cmp::max(self.scroll_top - line_height, Pixels::ZERO);
return;
}
self.terminal.update(cx, |term, _| term.scroll_line_up());
cx.notify();
}
fn scroll_line_down(&mut self, _: &ScrollLineDown, cx: &mut ViewContext<Self>) {
let terminal_content = self.terminal.read(cx).last_content();
if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 {
let max_scroll_top = self.max_scroll_top(cx);
if self.scroll_top < max_scroll_top {
let line_height = terminal_content.size.line_height;
self.scroll_top = cmp::min(self.scroll_top + line_height, max_scroll_top);
}
return;
}
self.terminal.update(cx, |term, _| term.scroll_line_down());
cx.notify();
}
fn scroll_page_up(&mut self, _: &ScrollPageUp, cx: &mut ViewContext<Self>) {
self.terminal.update(cx, |term, _| term.scroll_page_up());
if self.scroll_top == Pixels::ZERO {
self.terminal.update(cx, |term, _| term.scroll_page_up());
} else {
let line_height = self.terminal.read(cx).last_content.size.line_height();
let visible_block_lines = (self.scroll_top / line_height) as usize;
let viewport_lines = self.terminal.read(cx).viewport_lines();
let visible_content_lines = viewport_lines - visible_block_lines;
if visible_block_lines >= viewport_lines {
self.scroll_top = ((visible_block_lines - viewport_lines) as f32) * line_height;
} else {
self.scroll_top = px(0.);
self.terminal
.update(cx, |term, _| term.scroll_up_by(visible_content_lines));
}
}
cx.notify();
}
fn scroll_page_down(&mut self, _: &ScrollPageDown, cx: &mut ViewContext<Self>) {
self.terminal.update(cx, |term, _| term.scroll_page_down());
let terminal = self.terminal.read(cx);
if terminal.last_content().display_offset < terminal.viewport_lines() {
self.scroll_top = self.max_scroll_top(cx);
}
cx.notify();
}
@ -279,6 +391,9 @@ impl TerminalView {
fn scroll_to_bottom(&mut self, _: &ScrollToBottom, cx: &mut ViewContext<Self>) {
self.terminal.update(cx, |term, _| term.scroll_to_bottom());
if self.block_below_cursor.is_some() {
self.scroll_top = self.max_scroll_top(cx);
}
cx.notify();
}
@ -337,6 +452,18 @@ impl TerminalView {
&self.terminal
}
pub fn set_block_below_cursor(&mut self, block: BlockProperties, cx: &mut ViewContext<Self>) {
self.block_below_cursor = Some(Arc::new(block));
self.scroll_to_bottom(&ScrollToBottom, cx);
cx.notify();
}
pub fn clear_block_below_cursor(&mut self, cx: &mut ViewContext<Self>) {
self.block_below_cursor = None;
self.scroll_top = Pixels::ZERO;
cx.notify();
}
fn next_blink_epoch(&mut self) -> usize {
self.blink_epoch += 1;
self.blink_epoch
@ -761,6 +888,7 @@ impl TerminalView {
impl Render for TerminalView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let terminal_handle = self.terminal.clone();
let terminal_view_handle = cx.view().clone();
let focused = self.focus_handle.is_focused(cx);
@ -796,11 +924,13 @@ impl Render for TerminalView {
// TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu
div().size_full().child(TerminalElement::new(
terminal_handle,
terminal_view_handle,
self.workspace.clone(),
self.focus_handle.clone(),
focused,
self.should_show_cursor(focused, cx),
self.can_navigate_to_selected_word,
self.block_below_cursor.clone(),
)),
)
.children(self.context_menu.as_ref().map(|(menu, position, _)| {