Add terminal inline assistant (#13638)
Release Notes: - N/A --------- Co-authored-by: Antonio <antonio@zed.dev>
This commit is contained in:
parent
c516b8f038
commit
e243856559
11 changed files with 1587 additions and 141 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -417,6 +417,7 @@ dependencies = [
|
||||||
"strsim 0.11.1",
|
"strsim 0.11.1",
|
||||||
"strum",
|
"strum",
|
||||||
"telemetry_events",
|
"telemetry_events",
|
||||||
|
"terminal",
|
||||||
"terminal_view",
|
"terminal_view",
|
||||||
"theme",
|
"theme",
|
||||||
"tiktoken-rs",
|
"tiktoken-rs",
|
||||||
|
|
|
@ -653,6 +653,7 @@
|
||||||
"ctrl-insert": "terminal::Copy",
|
"ctrl-insert": "terminal::Copy",
|
||||||
"shift-ctrl-v": "terminal::Paste",
|
"shift-ctrl-v": "terminal::Paste",
|
||||||
"shift-insert": "terminal::Paste",
|
"shift-insert": "terminal::Paste",
|
||||||
|
"ctrl-enter": "assistant::InlineAssist",
|
||||||
"up": ["terminal::SendKeystroke", "up"],
|
"up": ["terminal::SendKeystroke", "up"],
|
||||||
"pageup": ["terminal::SendKeystroke", "pageup"],
|
"pageup": ["terminal::SendKeystroke", "pageup"],
|
||||||
"down": ["terminal::SendKeystroke", "down"],
|
"down": ["terminal::SendKeystroke", "down"],
|
||||||
|
|
|
@ -688,6 +688,7 @@
|
||||||
"cmd-c": "terminal::Copy",
|
"cmd-c": "terminal::Copy",
|
||||||
"cmd-v": "terminal::Paste",
|
"cmd-v": "terminal::Paste",
|
||||||
"cmd-k": "terminal::Clear",
|
"cmd-k": "terminal::Clear",
|
||||||
|
"ctrl-enter": "assistant::InlineAssist",
|
||||||
// Some nice conveniences
|
// Some nice conveniences
|
||||||
"cmd-backspace": ["terminal::SendText", "\u0015"],
|
"cmd-backspace": ["terminal::SendText", "\u0015"],
|
||||||
"cmd-right": ["terminal::SendText", "\u0005"],
|
"cmd-right": ["terminal::SendText", "\u0005"],
|
||||||
|
|
|
@ -56,6 +56,7 @@ smol.workspace = true
|
||||||
strsim = "0.11"
|
strsim = "0.11"
|
||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
telemetry_events.workspace = true
|
telemetry_events.workspace = true
|
||||||
|
terminal.workspace = true
|
||||||
terminal_view.workspace = true
|
terminal_view.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
tiktoken-rs.workspace = true
|
tiktoken-rs.workspace = true
|
||||||
|
|
|
@ -9,6 +9,7 @@ mod prompts;
|
||||||
mod search;
|
mod search;
|
||||||
mod slash_command;
|
mod slash_command;
|
||||||
mod streaming_diff;
|
mod streaming_diff;
|
||||||
|
mod terminal_inline_assistant;
|
||||||
|
|
||||||
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
|
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
|
||||||
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaModel, OpenAiModel};
|
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaModel, OpenAiModel};
|
||||||
|
@ -289,6 +290,7 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
|
||||||
register_slash_commands(cx);
|
register_slash_commands(cx);
|
||||||
assistant_panel::init(cx);
|
assistant_panel::init(cx);
|
||||||
inline_assistant::init(fs.clone(), client.telemetry().clone(), cx);
|
inline_assistant::init(fs.clone(), client.telemetry().clone(), cx);
|
||||||
|
terminal_inline_assistant::init(fs.clone(), client.telemetry().clone(), cx);
|
||||||
RustdocStore::init_global(cx);
|
RustdocStore::init_global(cx);
|
||||||
|
|
||||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||||
|
|
|
@ -7,6 +7,7 @@ use crate::{
|
||||||
default_command::DefaultSlashCommand, SlashCommandCompletionProvider, SlashCommandLine,
|
default_command::DefaultSlashCommand, SlashCommandCompletionProvider, SlashCommandLine,
|
||||||
SlashCommandRegistry,
|
SlashCommandRegistry,
|
||||||
},
|
},
|
||||||
|
terminal_inline_assistant::TerminalInlineAssistant,
|
||||||
ApplyEdit, Assist, CompletionProvider, ConfirmCommand, ContextStore, CycleMessageRole,
|
ApplyEdit, Assist, CompletionProvider, ConfirmCommand, ContextStore, CycleMessageRole,
|
||||||
InlineAssist, InlineAssistant, LanguageModelRequest, LanguageModelRequestMessage, MessageId,
|
InlineAssist, InlineAssistant, LanguageModelRequest, LanguageModelRequestMessage, MessageId,
|
||||||
MessageMetadata, MessageStatus, ModelSelector, QuoteSelection, ResetKey, Role, SavedContext,
|
MessageMetadata, MessageStatus, ModelSelector, QuoteSelection, ResetKey, Role, SavedContext,
|
||||||
|
@ -58,6 +59,7 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use telemetry_events::AssistantKind;
|
use telemetry_events::AssistantKind;
|
||||||
|
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||||
use ui::{
|
use ui::{
|
||||||
prelude::*, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
|
prelude::*, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
|
||||||
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tab, TabBar, Tooltip,
|
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tab, TabBar, Tooltip,
|
||||||
|
@ -124,6 +126,11 @@ enum SavedContextPickerEvent {
|
||||||
Confirmed { path: PathBuf },
|
Confirmed { path: PathBuf },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum InlineAssistTarget {
|
||||||
|
Editor(View<Editor>, bool),
|
||||||
|
Terminal(View<TerminalView>),
|
||||||
|
}
|
||||||
|
|
||||||
impl EventEmitter<SavedContextPickerEvent> for Picker<SavedContextPickerDelegate> {}
|
impl EventEmitter<SavedContextPickerEvent> for Picker<SavedContextPickerDelegate> {}
|
||||||
|
|
||||||
impl SavedContextPickerDelegate {
|
impl SavedContextPickerDelegate {
|
||||||
|
@ -369,6 +376,103 @@ impl AssistantPanel {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let Some(inline_assist_target) =
|
||||||
|
Self::resolve_inline_assist_target(workspace, &assistant_panel, cx)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if assistant_panel.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
|
||||||
|
match inline_assist_target {
|
||||||
|
InlineAssistTarget::Editor(active_editor, include_context) => {
|
||||||
|
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||||
|
assistant.assist(
|
||||||
|
&active_editor,
|
||||||
|
Some(cx.view().downgrade()),
|
||||||
|
include_context.then_some(&assistant_panel),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
InlineAssistTarget::Terminal(active_terminal) => {
|
||||||
|
TerminalInlineAssistant::update_global(cx, |assistant, cx| {
|
||||||
|
assistant.assist(
|
||||||
|
&active_terminal,
|
||||||
|
Some(cx.view().downgrade()),
|
||||||
|
Some(&assistant_panel),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let assistant_panel = assistant_panel.downgrade();
|
||||||
|
cx.spawn(|workspace, mut cx| async move {
|
||||||
|
assistant_panel
|
||||||
|
.update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
|
||||||
|
.await?;
|
||||||
|
if assistant_panel.update(&mut cx, |panel, cx| panel.is_authenticated(cx))? {
|
||||||
|
cx.update(|cx| match inline_assist_target {
|
||||||
|
InlineAssistTarget::Editor(active_editor, include_context) => {
|
||||||
|
let assistant_panel = if include_context {
|
||||||
|
assistant_panel.upgrade()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||||
|
assistant.assist(
|
||||||
|
&active_editor,
|
||||||
|
Some(workspace),
|
||||||
|
assistant_panel.as_ref(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
InlineAssistTarget::Terminal(active_terminal) => {
|
||||||
|
TerminalInlineAssistant::update_global(cx, |assistant, cx| {
|
||||||
|
assistant.assist(
|
||||||
|
&active_terminal,
|
||||||
|
Some(workspace),
|
||||||
|
assistant_panel.upgrade().as_ref(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
workspace.focus_panel::<AssistantPanel>(cx)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_inline_assist_target(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
assistant_panel: &View<AssistantPanel>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Option<InlineAssistTarget> {
|
||||||
|
if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
|
||||||
|
if terminal_panel
|
||||||
|
.read(cx)
|
||||||
|
.focus_handle(cx)
|
||||||
|
.contains_focused(cx)
|
||||||
|
{
|
||||||
|
if let Some(terminal_view) = terminal_panel
|
||||||
|
.read(cx)
|
||||||
|
.pane()
|
||||||
|
.read(cx)
|
||||||
|
.active_item()
|
||||||
|
.and_then(|t| t.downcast::<TerminalView>())
|
||||||
|
{
|
||||||
|
return Some(InlineAssistTarget::Terminal(terminal_view));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let context_editor = assistant_panel
|
let context_editor = assistant_panel
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.active_context_editor()
|
.active_context_editor()
|
||||||
|
@ -381,63 +485,15 @@ impl AssistantPanel {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let include_context;
|
|
||||||
let active_editor;
|
|
||||||
if let Some(context_editor) = context_editor {
|
if let Some(context_editor) = context_editor {
|
||||||
active_editor = context_editor;
|
Some(InlineAssistTarget::Editor(context_editor, false))
|
||||||
include_context = false;
|
|
||||||
} else if let Some(workspace_editor) = workspace
|
} else if let Some(workspace_editor) = workspace
|
||||||
.active_item(cx)
|
.active_item(cx)
|
||||||
.and_then(|item| item.act_as::<Editor>(cx))
|
.and_then(|item| item.act_as::<Editor>(cx))
|
||||||
{
|
{
|
||||||
active_editor = workspace_editor;
|
Some(InlineAssistTarget::Editor(workspace_editor, true))
|
||||||
include_context = true;
|
|
||||||
} else {
|
} else {
|
||||||
return;
|
None
|
||||||
};
|
|
||||||
|
|
||||||
if assistant_panel.update(cx, |panel, cx| panel.is_authenticated(cx)) {
|
|
||||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
|
||||||
assistant.assist(
|
|
||||||
&active_editor,
|
|
||||||
Some(cx.view().downgrade()),
|
|
||||||
include_context.then_some(&assistant_panel),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
let assistant_panel = assistant_panel.downgrade();
|
|
||||||
cx.spawn(|workspace, mut cx| async move {
|
|
||||||
assistant_panel
|
|
||||||
.update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
|
|
||||||
.await?;
|
|
||||||
if assistant_panel
|
|
||||||
.update(&mut cx, |assistant, cx| assistant.is_authenticated(cx))?
|
|
||||||
{
|
|
||||||
cx.update(|cx| {
|
|
||||||
let assistant_panel = if include_context {
|
|
||||||
assistant_panel.upgrade()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
|
||||||
assistant.assist(
|
|
||||||
&active_editor,
|
|
||||||
Some(workspace),
|
|
||||||
assistant_panel.as_ref(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})?
|
|
||||||
} else {
|
|
||||||
workspace.update(&mut cx, |workspace, cx| {
|
|
||||||
workspace.focus_panel::<AssistantPanel>(cx)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
|
||||||
.detach_and_log_err(cx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -109,3 +109,27 @@ pub fn generate_content_prompt(
|
||||||
|
|
||||||
Ok(prompt)
|
Ok(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_terminal_assistant_prompt(
|
||||||
|
user_prompt: &str,
|
||||||
|
shell: Option<&str>,
|
||||||
|
working_directory: Option<&str>,
|
||||||
|
) -> String {
|
||||||
|
let mut prompt = String::new();
|
||||||
|
writeln!(&mut prompt, "You are an expert terminal user.").unwrap();
|
||||||
|
writeln!(&mut prompt, "You will be given a description of a command and you need to respond with a command that matches the description.").unwrap();
|
||||||
|
writeln!(&mut prompt, "Do not include markdown blocks or any other text formatting in your response, always respond with a single command that can be executed in the given shell.").unwrap();
|
||||||
|
if let Some(shell) = shell {
|
||||||
|
writeln!(&mut prompt, "Current shell is '{shell}'.").unwrap();
|
||||||
|
}
|
||||||
|
if let Some(working_directory) = working_directory {
|
||||||
|
writeln!(
|
||||||
|
&mut prompt,
|
||||||
|
"Current working directory is '{working_directory}'."
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
writeln!(&mut prompt, "Here is the description of the command:").unwrap();
|
||||||
|
prompt.push_str(user_prompt);
|
||||||
|
prompt
|
||||||
|
}
|
||||||
|
|
1122
crates/assistant/src/terminal_inline_assistant.rs
Normal file
1122
crates/assistant/src/terminal_inline_assistant.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -945,6 +945,18 @@ impl Terminal {
|
||||||
&self.last_content
|
&self.last_content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn total_lines(&self) -> usize {
|
||||||
|
let term = self.term.clone();
|
||||||
|
let terminal = term.lock_unfair();
|
||||||
|
terminal.total_lines()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn viewport_lines(&self) -> usize {
|
||||||
|
let term = self.term.clone();
|
||||||
|
let terminal = term.lock_unfair();
|
||||||
|
terminal.screen_lines()
|
||||||
|
}
|
||||||
|
|
||||||
//To test:
|
//To test:
|
||||||
//- Activate match on terminal (scrolling and selection)
|
//- Activate match on terminal (scrolling and selection)
|
||||||
//- Editor search snapping behavior
|
//- Editor search snapping behavior
|
||||||
|
@ -999,11 +1011,21 @@ impl Terminal {
|
||||||
.push_back(InternalEvent::Scroll(AlacScroll::Delta(1)));
|
.push_back(InternalEvent::Scroll(AlacScroll::Delta(1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scroll_up_by(&mut self, lines: usize) {
|
||||||
|
self.events
|
||||||
|
.push_back(InternalEvent::Scroll(AlacScroll::Delta(lines as i32)));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scroll_line_down(&mut self) {
|
pub fn scroll_line_down(&mut self) {
|
||||||
self.events
|
self.events
|
||||||
.push_back(InternalEvent::Scroll(AlacScroll::Delta(-1)));
|
.push_back(InternalEvent::Scroll(AlacScroll::Delta(-1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scroll_down_by(&mut self, lines: usize) {
|
||||||
|
self.events
|
||||||
|
.push_back(InternalEvent::Scroll(AlacScroll::Delta(-(lines as i32))));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scroll_page_up(&mut self) {
|
pub fn scroll_page_up(&mut self) {
|
||||||
self.events
|
self.events
|
||||||
.push_back(InternalEvent::Scroll(AlacScroll::PageUp));
|
.push_back(InternalEvent::Scroll(AlacScroll::PageUp));
|
||||||
|
@ -1436,6 +1458,13 @@ impl Terminal {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn working_directory(&self) -> Option<PathBuf> {
|
||||||
|
self.pty_info
|
||||||
|
.current
|
||||||
|
.as_ref()
|
||||||
|
.map(|process| process.cwd.clone())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn title(&self, truncate: bool) -> String {
|
pub fn title(&self, truncate: bool) -> String {
|
||||||
const MAX_CHARS: usize = 25;
|
const MAX_CHARS: usize = 25;
|
||||||
match &self.task {
|
match &self.task {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine};
|
use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, fill, point, px, relative, AnyElement, Bounds, DispatchPhase, Element, ElementId,
|
div, fill, point, px, relative, size, AnyElement, AvailableSpace, Bounds, ContentMask,
|
||||||
FocusHandle, Font, FontStyle, FontWeight, GlobalElementId, HighlightStyle, Hitbox, Hsla,
|
DispatchPhase, Element, ElementId, FocusHandle, Font, FontStyle, FontWeight, GlobalElementId,
|
||||||
InputHandler, InteractiveElement, Interactivity, IntoElement, LayoutId, Model, ModelContext,
|
HighlightStyle, Hitbox, Hsla, InputHandler, InteractiveElement, Interactivity, IntoElement,
|
||||||
ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, Point, ShapedLine,
|
LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels,
|
||||||
StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, UnderlineStyle,
|
Point, ShapedLine, StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle,
|
||||||
WeakView, WhiteSpace, WindowContext, WindowTextSystem,
|
UnderlineStyle, View, WeakView, WhiteSpace, WindowContext, WindowTextSystem,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::CursorShape;
|
use language::CursorShape;
|
||||||
|
@ -24,11 +24,13 @@ use terminal::{
|
||||||
HoveredWord, IndexedCell, Terminal, TerminalContent, TerminalSize,
|
HoveredWord, IndexedCell, Terminal, TerminalContent, TerminalSize,
|
||||||
};
|
};
|
||||||
use theme::{ActiveTheme, Theme, ThemeSettings};
|
use theme::{ActiveTheme, Theme, ThemeSettings};
|
||||||
use ui::Tooltip;
|
use ui::{ParentElement, Tooltip};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use std::mem;
|
|
||||||
use std::{fmt::Debug, ops::RangeInclusive};
|
use std::{fmt::Debug, ops::RangeInclusive};
|
||||||
|
use std::{mem, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{BlockContext, BlockProperties, TerminalView};
|
||||||
|
|
||||||
/// The information generated during layout that is necessary for painting.
|
/// The information generated during layout that is necessary for painting.
|
||||||
pub struct LayoutState {
|
pub struct LayoutState {
|
||||||
|
@ -44,6 +46,7 @@ pub struct LayoutState {
|
||||||
hyperlink_tooltip: Option<AnyElement>,
|
hyperlink_tooltip: Option<AnyElement>,
|
||||||
gutter: Pixels,
|
gutter: Pixels,
|
||||||
last_hovered_word: Option<HoveredWord>,
|
last_hovered_word: Option<HoveredWord>,
|
||||||
|
block_below_cursor_element: Option<AnyElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points.
|
/// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points.
|
||||||
|
@ -146,12 +149,14 @@ impl LayoutRect {
|
||||||
/// We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
|
/// We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
|
||||||
pub struct TerminalElement {
|
pub struct TerminalElement {
|
||||||
terminal: Model<Terminal>,
|
terminal: Model<Terminal>,
|
||||||
|
terminal_view: View<TerminalView>,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
focus: FocusHandle,
|
focus: FocusHandle,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
cursor_visible: bool,
|
cursor_visible: bool,
|
||||||
can_navigate_to_selected_word: bool,
|
can_navigate_to_selected_word: bool,
|
||||||
interactivity: Interactivity,
|
interactivity: Interactivity,
|
||||||
|
block_below_cursor: Option<Arc<BlockProperties>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InteractiveElement for TerminalElement {
|
impl InteractiveElement for TerminalElement {
|
||||||
|
@ -163,21 +168,26 @@ impl InteractiveElement for TerminalElement {
|
||||||
impl StatefulInteractiveElement for TerminalElement {}
|
impl StatefulInteractiveElement for TerminalElement {}
|
||||||
|
|
||||||
impl TerminalElement {
|
impl TerminalElement {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
terminal: Model<Terminal>,
|
terminal: Model<Terminal>,
|
||||||
|
terminal_view: View<TerminalView>,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
focus: FocusHandle,
|
focus: FocusHandle,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
cursor_visible: bool,
|
cursor_visible: bool,
|
||||||
can_navigate_to_selected_word: bool,
|
can_navigate_to_selected_word: bool,
|
||||||
|
block_below_cursor: Option<Arc<BlockProperties>>,
|
||||||
) -> TerminalElement {
|
) -> TerminalElement {
|
||||||
TerminalElement {
|
TerminalElement {
|
||||||
terminal,
|
terminal,
|
||||||
|
terminal_view,
|
||||||
workspace,
|
workspace,
|
||||||
focused,
|
focused,
|
||||||
focus: focus.clone(),
|
focus: focus.clone(),
|
||||||
cursor_visible,
|
cursor_visible,
|
||||||
can_navigate_to_selected_word,
|
can_navigate_to_selected_word,
|
||||||
|
block_below_cursor,
|
||||||
interactivity: Default::default(),
|
interactivity: Default::default(),
|
||||||
}
|
}
|
||||||
.track_focus(&focus)
|
.track_focus(&focus)
|
||||||
|
@ -192,7 +202,7 @@ impl TerminalElement {
|
||||||
// terminal_theme: &TerminalStyle,
|
// terminal_theme: &TerminalStyle,
|
||||||
text_system: &WindowTextSystem,
|
text_system: &WindowTextSystem,
|
||||||
hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
|
hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
|
||||||
cx: &WindowContext<'_>,
|
cx: &WindowContext,
|
||||||
) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
|
) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let mut cells = vec![];
|
let mut cells = vec![];
|
||||||
|
@ -491,12 +501,14 @@ impl TerminalElement {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
self.interactivity.on_scroll_wheel({
|
self.interactivity.on_scroll_wheel({
|
||||||
let terminal = terminal.clone();
|
let terminal_view = self.terminal_view.downgrade();
|
||||||
move |e, cx| {
|
move |e, cx| {
|
||||||
terminal.update(cx, |terminal, cx| {
|
terminal_view
|
||||||
terminal.scroll_wheel(e, origin);
|
.update(cx, |terminal_view, cx| {
|
||||||
cx.notify();
|
terminal_view.scroll_wheel(e, origin, cx);
|
||||||
})
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -538,6 +550,26 @@ impl TerminalElement {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rem_size(&self, cx: &WindowContext) -> Option<Pixels> {
|
||||||
|
let settings = ThemeSettings::get_global(cx).clone();
|
||||||
|
let buffer_font_size = settings.buffer_font_size(cx);
|
||||||
|
let rem_size_scale = {
|
||||||
|
// Our default UI font size is 14px on a 16px base scale.
|
||||||
|
// This means the default UI font size is 0.875rems.
|
||||||
|
let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX;
|
||||||
|
|
||||||
|
// We then determine the delta between a single rem and the default font
|
||||||
|
// size scale.
|
||||||
|
let default_font_size_delta = 1. - default_font_size_scale;
|
||||||
|
|
||||||
|
// Finally, we add this delta to 1rem to get the scale factor that
|
||||||
|
// should be used to scale up the UI.
|
||||||
|
1. + default_font_size_delta
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(buffer_font_size * rem_size_scale)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element for TerminalElement {
|
impl Element for TerminalElement {
|
||||||
|
@ -558,6 +590,7 @@ impl Element for TerminalElement {
|
||||||
.request_layout(global_id, cx, |mut style, cx| {
|
.request_layout(global_id, cx, |mut style, cx| {
|
||||||
style.size.width = relative(1.).into();
|
style.size.width = relative(1.).into();
|
||||||
style.size.height = relative(1.).into();
|
style.size.height = relative(1.).into();
|
||||||
|
// style.overflow = point(Overflow::Hidden, Overflow::Hidden);
|
||||||
let layout_id = cx.request_layout(style, None);
|
let layout_id = cx.request_layout(style, None);
|
||||||
|
|
||||||
layout_id
|
layout_id
|
||||||
|
@ -572,6 +605,7 @@ impl Element for TerminalElement {
|
||||||
_: &mut Self::RequestLayoutState,
|
_: &mut Self::RequestLayoutState,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Self::PrepaintState {
|
) -> Self::PrepaintState {
|
||||||
|
let rem_size = self.rem_size(cx);
|
||||||
self.interactivity
|
self.interactivity
|
||||||
.prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, cx| {
|
.prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, cx| {
|
||||||
let hitbox = hitbox.unwrap();
|
let hitbox = hitbox.unwrap();
|
||||||
|
@ -675,8 +709,9 @@ impl Element for TerminalElement {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let scroll_top = self.terminal_view.read(cx).scroll_top;
|
||||||
let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| {
|
let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| {
|
||||||
let offset = bounds.origin + Point::new(gutter, px(0.));
|
let offset = bounds.origin + point(gutter, px(0.)) - point(px(0.), scroll_top);
|
||||||
let mut element = div()
|
let mut element = div()
|
||||||
.size_full()
|
.size_full()
|
||||||
.id("terminal-element")
|
.id("terminal-element")
|
||||||
|
@ -695,6 +730,8 @@ impl Element for TerminalElement {
|
||||||
cursor,
|
cursor,
|
||||||
..
|
..
|
||||||
} = &self.terminal.read(cx).last_content;
|
} = &self.terminal.read(cx).last_content;
|
||||||
|
let mode = *mode;
|
||||||
|
let display_offset = *display_offset;
|
||||||
|
|
||||||
// searches, highlights to a single range representations
|
// searches, highlights to a single range representations
|
||||||
let mut relative_highlighted_ranges = Vec::new();
|
let mut relative_highlighted_ranges = Vec::new();
|
||||||
|
@ -723,7 +760,7 @@ impl Element for TerminalElement {
|
||||||
let cursor = if let AlacCursorShape::Hidden = cursor.shape {
|
let cursor = if let AlacCursorShape::Hidden = cursor.shape {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let cursor_point = DisplayCursor::from(cursor.point, *display_offset);
|
let cursor_point = DisplayCursor::from(cursor.point, display_offset);
|
||||||
let cursor_text = {
|
let cursor_text = {
|
||||||
let str_trxt = cursor_char.to_string();
|
let str_trxt = cursor_char.to_string();
|
||||||
let len = str_trxt.len();
|
let len = str_trxt.len();
|
||||||
|
@ -768,6 +805,37 @@ impl Element for TerminalElement {
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let block_below_cursor_element = if let Some(block) = &self.block_below_cursor {
|
||||||
|
let terminal = self.terminal.read(cx);
|
||||||
|
if terminal.last_content.display_offset == 0 {
|
||||||
|
let target_line = terminal.last_content.cursor.point.line.0 + 1;
|
||||||
|
let render = &block.render;
|
||||||
|
let mut block_cx = BlockContext {
|
||||||
|
context: cx,
|
||||||
|
dimensions,
|
||||||
|
};
|
||||||
|
let element = render(&mut block_cx);
|
||||||
|
let mut element = div().occlude().child(element).into_any_element();
|
||||||
|
let available_space = size(
|
||||||
|
AvailableSpace::Definite(dimensions.width() + gutter),
|
||||||
|
AvailableSpace::Definite(
|
||||||
|
block.height as f32 * dimensions.line_height(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let origin = bounds.origin
|
||||||
|
+ point(px(0.), target_line as f32 * dimensions.line_height())
|
||||||
|
- point(px(0.), scroll_top);
|
||||||
|
cx.with_rem_size(rem_size, |cx| {
|
||||||
|
element.prepaint_as_root(origin, available_space, cx);
|
||||||
|
});
|
||||||
|
Some(element)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
LayoutState {
|
LayoutState {
|
||||||
hitbox,
|
hitbox,
|
||||||
cells,
|
cells,
|
||||||
|
@ -776,11 +844,12 @@ impl Element for TerminalElement {
|
||||||
dimensions,
|
dimensions,
|
||||||
rects,
|
rects,
|
||||||
relative_highlighted_ranges,
|
relative_highlighted_ranges,
|
||||||
mode: *mode,
|
mode,
|
||||||
display_offset: *display_offset,
|
display_offset,
|
||||||
hyperlink_tooltip,
|
hyperlink_tooltip,
|
||||||
gutter,
|
gutter,
|
||||||
last_hovered_word,
|
last_hovered_word,
|
||||||
|
block_below_cursor_element,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -793,82 +862,92 @@ impl Element for TerminalElement {
|
||||||
layout: &mut Self::PrepaintState,
|
layout: &mut Self::PrepaintState,
|
||||||
cx: &mut WindowContext<'_>,
|
cx: &mut WindowContext<'_>,
|
||||||
) {
|
) {
|
||||||
cx.paint_quad(fill(bounds, layout.background_color));
|
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||||
let origin = bounds.origin + Point::new(layout.gutter, px(0.));
|
let scroll_top = self.terminal_view.read(cx).scroll_top;
|
||||||
|
|
||||||
let terminal_input_handler = TerminalInputHandler {
|
cx.paint_quad(fill(bounds, layout.background_color));
|
||||||
terminal: self.terminal.clone(),
|
let origin =
|
||||||
cursor_bounds: layout
|
bounds.origin + Point::new(layout.gutter, px(0.)) - Point::new(px(0.), scroll_top);
|
||||||
.cursor
|
|
||||||
.as_ref()
|
|
||||||
.map(|cursor| cursor.bounding_rect(origin)),
|
|
||||||
workspace: self.workspace.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.register_mouse_listeners(origin, layout.mode, &layout.hitbox, cx);
|
let terminal_input_handler = TerminalInputHandler {
|
||||||
if self.can_navigate_to_selected_word && layout.last_hovered_word.is_some() {
|
terminal: self.terminal.clone(),
|
||||||
cx.set_cursor_style(gpui::CursorStyle::PointingHand, &layout.hitbox);
|
cursor_bounds: layout
|
||||||
} else {
|
.cursor
|
||||||
cx.set_cursor_style(gpui::CursorStyle::IBeam, &layout.hitbox);
|
.as_ref()
|
||||||
}
|
.map(|cursor| cursor.bounding_rect(origin)),
|
||||||
|
workspace: self.workspace.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let cursor = layout.cursor.take();
|
self.register_mouse_listeners(origin, layout.mode, &layout.hitbox, cx);
|
||||||
let hyperlink_tooltip = layout.hyperlink_tooltip.take();
|
if self.can_navigate_to_selected_word && layout.last_hovered_word.is_some() {
|
||||||
self.interactivity
|
cx.set_cursor_style(gpui::CursorStyle::PointingHand, &layout.hitbox);
|
||||||
.paint(global_id, bounds, Some(&layout.hitbox), cx, |_, cx| {
|
} else {
|
||||||
cx.handle_input(&self.focus, terminal_input_handler);
|
cx.set_cursor_style(gpui::CursorStyle::IBeam, &layout.hitbox);
|
||||||
|
}
|
||||||
|
|
||||||
cx.on_key_event({
|
let cursor = layout.cursor.take();
|
||||||
let this = self.terminal.clone();
|
let hyperlink_tooltip = layout.hyperlink_tooltip.take();
|
||||||
move |event: &ModifiersChangedEvent, phase, cx| {
|
let block_below_cursor_element = layout.block_below_cursor_element.take();
|
||||||
if phase != DispatchPhase::Bubble {
|
self.interactivity
|
||||||
return;
|
.paint(global_id, bounds, Some(&layout.hitbox), cx, |_, cx| {
|
||||||
|
cx.handle_input(&self.focus, terminal_input_handler);
|
||||||
|
|
||||||
|
cx.on_key_event({
|
||||||
|
let this = self.terminal.clone();
|
||||||
|
move |event: &ModifiersChangedEvent, phase, cx| {
|
||||||
|
if phase != DispatchPhase::Bubble {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let handled = this
|
||||||
|
.update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
|
||||||
|
|
||||||
|
if handled {
|
||||||
|
cx.refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let handled =
|
for rect in &layout.rects {
|
||||||
this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
|
rect.paint(origin, &layout, cx);
|
||||||
|
}
|
||||||
|
|
||||||
if handled {
|
for (relative_highlighted_range, color) in
|
||||||
cx.refresh();
|
layout.relative_highlighted_ranges.iter()
|
||||||
|
{
|
||||||
|
if let Some((start_y, highlighted_range_lines)) =
|
||||||
|
to_highlighted_range_lines(relative_highlighted_range, &layout, origin)
|
||||||
|
{
|
||||||
|
let hr = HighlightedRange {
|
||||||
|
start_y,
|
||||||
|
line_height: layout.dimensions.line_height,
|
||||||
|
lines: highlighted_range_lines,
|
||||||
|
color: *color,
|
||||||
|
corner_radius: 0.15 * layout.dimensions.line_height,
|
||||||
|
};
|
||||||
|
hr.paint(bounds, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for cell in &layout.cells {
|
||||||
|
cell.paint(origin, &layout, bounds, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.cursor_visible {
|
||||||
|
if let Some(mut cursor) = cursor {
|
||||||
|
cursor.paint(origin, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mut element) = block_below_cursor_element {
|
||||||
|
element.paint(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mut element) = hyperlink_tooltip {
|
||||||
|
element.paint(cx);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
for rect in &layout.rects {
|
|
||||||
rect.paint(origin, &layout, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter()
|
|
||||||
{
|
|
||||||
if let Some((start_y, highlighted_range_lines)) =
|
|
||||||
to_highlighted_range_lines(relative_highlighted_range, &layout, origin)
|
|
||||||
{
|
|
||||||
let hr = HighlightedRange {
|
|
||||||
start_y, //Need to change this
|
|
||||||
line_height: layout.dimensions.line_height,
|
|
||||||
lines: highlighted_range_lines,
|
|
||||||
color: *color,
|
|
||||||
//Copied from editor. TODO: move to theme or something
|
|
||||||
corner_radius: 0.15 * layout.dimensions.line_height,
|
|
||||||
};
|
|
||||||
hr.paint(bounds, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for cell in &layout.cells {
|
|
||||||
cell.paint(origin, &layout, bounds, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.cursor_visible {
|
|
||||||
if let Some(mut cursor) = cursor {
|
|
||||||
cursor.paint(origin, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(mut element) = hyperlink_tooltip {
|
|
||||||
element.paint(cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -951,7 +1030,7 @@ impl InputHandler for TerminalInputHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_blank(cell: &IndexedCell) -> bool {
|
pub fn is_blank(cell: &IndexedCell) -> bool {
|
||||||
if cell.c != ' ' {
|
if cell.c != ' ' {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,12 @@ use futures::{stream::FuturesUnordered, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
anchored, deferred, div, impl_actions, AnyElement, AppContext, DismissEvent, EventEmitter,
|
anchored, deferred, div, impl_actions, AnyElement, AppContext, DismissEvent, EventEmitter,
|
||||||
FocusHandle, FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton,
|
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 language::Bias;
|
||||||
use persistence::TERMINAL_DB;
|
use persistence::TERMINAL_DB;
|
||||||
use project::{search::SearchQuery, Fs, LocalWorktree, Metadata, Project};
|
use project::{search::SearchQuery, Fs, LocalWorktree, Metadata, Project};
|
||||||
use settings::SettingsStore;
|
|
||||||
use task::TerminalWorkDir;
|
use task::TerminalWorkDir;
|
||||||
use terminal::{
|
use terminal::{
|
||||||
alacritty_terminal::{
|
alacritty_terminal::{
|
||||||
|
@ -23,8 +23,9 @@ use terminal::{
|
||||||
terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory},
|
terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory},
|
||||||
Clear, Copy, Event, MaybeNavigationTarget, Paste, ScrollLineDown, ScrollLineUp, ScrollPageDown,
|
Clear, Copy, Event, MaybeNavigationTarget, Paste, ScrollLineDown, ScrollLineUp, ScrollPageDown,
|
||||||
ScrollPageUp, ScrollToBottom, ScrollToTop, ShowCharacterPalette, TaskStatus, Terminal,
|
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 ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Tooltip};
|
||||||
use util::{paths::PathLikeWithPosition, ResultExt};
|
use util::{paths::PathLikeWithPosition, ResultExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
|
@ -39,10 +40,11 @@ use workspace::{
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use dirs::home_dir;
|
use dirs::home_dir;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::{Settings, SettingsStore};
|
||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
cmp,
|
||||||
ops::RangeInclusive,
|
ops::RangeInclusive,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
@ -79,6 +81,16 @@ pub fn init(cx: &mut AppContext) {
|
||||||
.detach();
|
.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
|
///A terminal view, maintains the PTY's file handles and communicates with the terminal
|
||||||
pub struct TerminalView {
|
pub struct TerminalView {
|
||||||
terminal: Model<Terminal>,
|
terminal: Model<Terminal>,
|
||||||
|
@ -94,6 +106,8 @@ pub struct TerminalView {
|
||||||
can_navigate_to_selected_word: bool,
|
can_navigate_to_selected_word: bool,
|
||||||
workspace_id: Option<WorkspaceId>,
|
workspace_id: Option<WorkspaceId>,
|
||||||
show_title: bool,
|
show_title: bool,
|
||||||
|
block_below_cursor: Option<Arc<BlockProperties>>,
|
||||||
|
scroll_top: Pixels,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
_terminal_subscriptions: Vec<Subscription>,
|
_terminal_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
@ -170,6 +184,8 @@ impl TerminalView {
|
||||||
can_navigate_to_selected_word: false,
|
can_navigate_to_selected_word: false,
|
||||||
workspace_id,
|
workspace_id,
|
||||||
show_title: TerminalSettings::get_global(cx).toolbar.title,
|
show_title: TerminalSettings::get_global(cx).toolbar.title,
|
||||||
|
block_below_cursor: None,
|
||||||
|
scroll_top: Pixels::ZERO,
|
||||||
_subscriptions: vec![
|
_subscriptions: vec![
|
||||||
focus_in,
|
focus_in,
|
||||||
focus_out,
|
focus_out,
|
||||||
|
@ -248,27 +264,123 @@ impl TerminalView {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
|
fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
|
||||||
|
self.scroll_top = px(0.);
|
||||||
self.terminal.update(cx, |term, _| term.clear());
|
self.terminal.update(cx, |term, _| term.clear());
|
||||||
cx.notify();
|
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>) {
|
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());
|
self.terminal.update(cx, |term, _| term.scroll_line_up());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_line_down(&mut self, _: &ScrollLineDown, cx: &mut ViewContext<Self>) {
|
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());
|
self.terminal.update(cx, |term, _| term.scroll_line_down());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_page_up(&mut self, _: &ScrollPageUp, cx: &mut ViewContext<Self>) {
|
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();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_page_down(&mut self, _: &ScrollPageDown, cx: &mut ViewContext<Self>) {
|
fn scroll_page_down(&mut self, _: &ScrollPageDown, cx: &mut ViewContext<Self>) {
|
||||||
self.terminal.update(cx, |term, _| term.scroll_page_down());
|
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();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,6 +391,9 @@ impl TerminalView {
|
||||||
|
|
||||||
fn scroll_to_bottom(&mut self, _: &ScrollToBottom, cx: &mut ViewContext<Self>) {
|
fn scroll_to_bottom(&mut self, _: &ScrollToBottom, cx: &mut ViewContext<Self>) {
|
||||||
self.terminal.update(cx, |term, _| term.scroll_to_bottom());
|
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();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,6 +452,18 @@ impl TerminalView {
|
||||||
&self.terminal
|
&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 {
|
fn next_blink_epoch(&mut self) -> usize {
|
||||||
self.blink_epoch += 1;
|
self.blink_epoch += 1;
|
||||||
self.blink_epoch
|
self.blink_epoch
|
||||||
|
@ -761,6 +888,7 @@ impl TerminalView {
|
||||||
impl Render for TerminalView {
|
impl Render for TerminalView {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let terminal_handle = self.terminal.clone();
|
let terminal_handle = self.terminal.clone();
|
||||||
|
let terminal_view_handle = cx.view().clone();
|
||||||
|
|
||||||
let focused = self.focus_handle.is_focused(cx);
|
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
|
// TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu
|
||||||
div().size_full().child(TerminalElement::new(
|
div().size_full().child(TerminalElement::new(
|
||||||
terminal_handle,
|
terminal_handle,
|
||||||
|
terminal_view_handle,
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
self.focus_handle.clone(),
|
self.focus_handle.clone(),
|
||||||
focused,
|
focused,
|
||||||
self.should_show_cursor(focused, cx),
|
self.should_show_cursor(focused, cx),
|
||||||
self.can_navigate_to_selected_word,
|
self.can_navigate_to_selected_word,
|
||||||
|
self.block_below_cursor.clone(),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
|
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue