Improved performance of terminal rendering further

This commit is contained in:
Mikayla Maki 2022-09-01 11:43:27 -07:00
parent a8b8003980
commit faad24542f
6 changed files with 193 additions and 131 deletions

View File

@ -4,7 +4,7 @@ use workspace::Workspace;
use crate::{
terminal_container_view::{
get_working_directory, DeployModal, TerminalContainer, TerminalContent,
get_working_directory, DeployModal, TerminalContainer, TerminalContainerContent,
},
Event, Terminal,
};
@ -42,7 +42,7 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon
let this = cx.add_view(|cx| TerminalContainer::new(working_directory, true, cx));
if let TerminalContent::Connected(connected) = &this.read(cx).content {
if let TerminalContainerContent::Connected(connected) = &this.read(cx).content {
let terminal_handle = connected.read(cx).handle();
cx.subscribe(&terminal_handle, on_event).detach();
// Set the global immediately if terminal construction was successful,
@ -55,7 +55,8 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon
this
}) {
// Terminal modal was dismissed. Store terminal if the terminal view is connected
if let TerminalContent::Connected(connected) = &closed_terminal_handle.read(cx).content
if let TerminalContainerContent::Connected(connected) =
&closed_terminal_handle.read(cx).content
{
let terminal_handle = connected.read(cx).handle();
// Set the global immediately if terminal construction was successful,

View File

@ -11,17 +11,19 @@ use alacritty_terminal::{
event_loop::{EventLoop, Msg, Notifier},
grid::{Dimensions, Scroll as AlacScroll},
index::{Column, Direction, Line, Point},
selection::{Selection, SelectionType},
selection::{Selection, SelectionRange, SelectionType},
sync::FairMutex,
term::{
cell::Cell,
color::Rgb,
search::{Match, RegexIter, RegexSearch},
RenderableContent, TermMode,
RenderableCursor, TermMode,
},
tty::{self, setup_env},
Term,
};
use anyhow::{bail, Result};
use futures::{
channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
FutureExt,
@ -36,10 +38,10 @@ use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
use std::{
collections::{HashMap, VecDeque},
fmt::Display,
ops::{RangeInclusive, Sub},
ops::{Deref, RangeInclusive, Sub},
path::PathBuf,
sync::Arc,
time::Duration,
time::{Duration, Instant},
};
use thiserror::Error;
@ -376,12 +378,12 @@ impl TerminalBuilder {
events: VecDeque::with_capacity(10), //Should never get this high.
title: shell_txt.clone(),
default_title: shell_txt,
last_mode: TermMode::NONE,
last_content: Default::default(),
cur_size: initial_size,
last_mouse: None,
last_offset: 0,
matches: Vec::new(),
selection_text: None,
last_synced: Instant::now(),
sync_task: None,
};
Ok(TerminalBuilder {
@ -443,18 +445,61 @@ impl TerminalBuilder {
}
}
#[derive(Debug, Clone)]
struct IndexedCell {
point: Point,
cell: Cell,
}
impl Deref for IndexedCell {
type Target = Cell;
#[inline]
fn deref(&self) -> &Cell {
&self.cell
}
}
#[derive(Clone)]
pub struct TerminalContent {
cells: Vec<IndexedCell>,
mode: TermMode,
display_offset: usize,
selection_text: Option<String>,
selection: Option<SelectionRange>,
cursor: RenderableCursor,
cursor_char: char,
}
impl Default for TerminalContent {
fn default() -> Self {
TerminalContent {
cells: Default::default(),
mode: Default::default(),
display_offset: Default::default(),
selection_text: Default::default(),
selection: Default::default(),
cursor: RenderableCursor {
shape: alacritty_terminal::ansi::CursorShape::Block,
point: Point::new(Line(0), Column(0)),
},
cursor_char: Default::default(),
}
}
}
pub struct Terminal {
pty_tx: Notifier,
term: Arc<FairMutex<Term<ZedListener>>>,
events: VecDeque<InternalEvent>,
default_title: String,
title: String,
cur_size: TerminalSize,
last_mode: TermMode,
last_offset: usize,
last_mouse: Option<(Point, Direction)>,
pub matches: Vec<RangeInclusive<Point>>,
pub selection_text: Option<String>,
cur_size: TerminalSize,
last_content: TerminalContent,
last_synced: Instant,
sync_task: Option<Task<()>>,
}
impl Terminal {
@ -576,6 +621,10 @@ impl Terminal {
}
}
pub fn last_content(&self) -> &TerminalContent {
&self.last_content
}
fn begin_select(&mut self, sel: Selection) {
self.events
.push_back(InternalEvent::SetSelection(Some(sel)));
@ -648,7 +697,7 @@ impl Terminal {
}
pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
let esc = to_esc_str(keystroke, &self.last_mode);
let esc = to_esc_str(keystroke, &self.last_content.mode);
if let Some(esc) = esc {
self.input(esc);
true
@ -659,7 +708,7 @@ impl Terminal {
///Paste text into the terminal
pub fn paste(&mut self, text: &str) {
let paste_text = if self.last_mode.contains(TermMode::BRACKETED_PASTE) {
let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
} else {
text.replace("\r\n", "\r").replace('\n', "\r")
@ -667,38 +716,76 @@ impl Terminal {
self.input(paste_text)
}
pub fn render_lock<F, T>(&mut self, cx: &mut ModelContext<Self>, f: F) -> T
where
F: FnOnce(RenderableContent, char) -> T,
{
pub fn try_sync(&mut self, cx: &mut ModelContext<Self>) {
let term = self.term.clone();
let mut term = term.lock();
let mut terminal = if let Some(term) = term.try_lock_unfair() {
term
} else if self.last_synced.elapsed().as_secs_f32() > 0.25 {
term.lock_unfair()
} else if let None = self.sync_task {
//Skip this frame
let delay = cx.background().timer(Duration::from_millis(16));
self.sync_task = Some(cx.spawn_weak(|weak_handle, mut cx| async move {
delay.await;
cx.update(|cx| {
if let Some(handle) = weak_handle.upgrade(cx) {
handle.update(cx, |terminal, cx| {
terminal.sync_task.take();
cx.notify();
});
}
});
}));
return;
} else {
//No lock and delayed rendering already scheduled, nothing to do
return;
};
//Note that this ordering matters for event processing
while let Some(e) = self.events.pop_front() {
self.process_terminal_event(&e, &mut term, cx)
self.process_terminal_event(&e, &mut terminal, cx)
}
self.last_mode = *term.mode();
self.last_content = Self::make_content(&terminal);
self.last_synced = Instant::now();
}
fn make_content(term: &Term<ZedListener>) -> TerminalContent {
let content = term.renderable_content();
self.selection_text = term.selection_to_string();
self.last_offset = content.display_offset;
let cursor_text = term.grid()[content.cursor.point].c;
f(content, cursor_text)
TerminalContent {
cells: content
.display_iter
//TODO: Add this once there's a way to retain empty lines
// .filter(|ic| {
// !ic.flags.contains(Flags::HIDDEN)
// && !(ic.bg == Named(NamedColor::Background)
// && ic.c == ' '
// && !ic.flags.contains(Flags::INVERSE))
// })
.map(|ic| IndexedCell {
point: ic.point,
cell: ic.cell.clone(),
})
.collect::<Vec<IndexedCell>>(),
mode: content.mode,
display_offset: content.display_offset,
selection_text: term.selection_to_string(),
selection: content.selection,
cursor: content.cursor,
cursor_char: term.grid()[content.cursor.point].c,
}
}
pub fn focus_in(&self) {
if self.last_mode.contains(TermMode::FOCUS_IN_OUT) {
if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
self.write_to_pty("\x1b[I".to_string());
}
}
pub fn focus_out(&self) {
if self.last_mode.contains(TermMode::FOCUS_IN_OUT) {
if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
self.write_to_pty("\x1b[O".to_string());
}
}
@ -721,17 +808,17 @@ impl Terminal {
}
pub fn mouse_mode(&self, shift: bool) -> bool {
self.last_mode.intersects(TermMode::MOUSE_MODE) && !shift
self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift
}
pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
let position = e.position.sub(origin);
let point = mouse_point(position, self.cur_size, self.last_offset);
let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
let side = mouse_side(position, self.cur_size);
if self.mouse_changed(point, side) && self.mouse_mode(e.shift) {
if let Some(bytes) = mouse_moved_report(point, e, self.last_mode) {
if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
self.pty_tx.notify(bytes);
}
}
@ -746,7 +833,7 @@ impl Terminal {
self.continue_selection(position);
// Doesn't make sense to scroll the alt screen
if !self.last_mode.contains(TermMode::ALT_SCREEN) {
if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
let scroll_delta = match self.drag_line_delta(e) {
Some(value) => value,
None => return,
@ -775,11 +862,11 @@ impl Terminal {
pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin);
let point = mouse_point(position, self.cur_size, self.last_offset);
let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
let side = mouse_side(position, self.cur_size);
if self.mouse_mode(e.shift) {
if let Some(bytes) = mouse_button_report(point, e, true, self.last_mode) {
if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
self.pty_tx.notify(bytes);
}
} else if e.button == MouseButton::Left {
@ -791,7 +878,7 @@ impl Terminal {
let position = e.position.sub(origin);
if !self.mouse_mode(e.shift) {
let point = mouse_point(position, self.cur_size, self.last_offset);
let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
let side = mouse_side(position, self.cur_size);
let selection_type = match e.click_count {
@ -814,9 +901,9 @@ impl Terminal {
pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin);
if self.mouse_mode(e.shift) {
let point = mouse_point(position, self.cur_size, self.last_offset);
let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
if let Some(bytes) = mouse_button_report(point, e, false, self.last_mode) {
if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
self.pty_tx.notify(bytes);
}
} else if e.button == MouseButton::Left {
@ -835,15 +922,22 @@ impl Terminal {
//The scroll enters 'TouchPhase::Started'. Do I need to replicate this?
//This would be consistent with a scroll model based on 'distance from origin'...
let scroll_lines = (e.delta.y() / self.cur_size.line_height) as i32;
let point = mouse_point(e.position.sub(origin), self.cur_size, self.last_offset);
let point = mouse_point(
e.position.sub(origin),
self.cur_size,
self.last_content.display_offset,
);
if let Some(scrolls) = scroll_report(point, scroll_lines as i32, e, self.last_mode) {
if let Some(scrolls) =
scroll_report(point, scroll_lines as i32, e, self.last_content.mode)
{
for scroll in scrolls {
self.pty_tx.notify(scroll);
}
};
} else if self
.last_mode
.last_content
.mode
.contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
&& !e.shift
{
@ -868,7 +962,6 @@ impl Terminal {
cx: &mut ModelContext<Self>,
) -> Task<Vec<RangeInclusive<Point>>> {
let term = self.term.clone();
dbg!("Spawning find_matches");
cx.background().spawn(async move {
let searcher = match query {
project::search::SearchQuery::Text { query, .. } => {
@ -885,7 +978,8 @@ impl Terminal {
let searcher = searcher.unwrap();
let term = term.lock();
dbg!(make_search_matches(&term, &searcher).collect())
make_search_matches(&term, &searcher).collect()
})
}
}

View File

@ -29,12 +29,12 @@ pub fn init(cx: &mut MutableAppContext) {
//Take away all the result unwrapping in the current TerminalView by making it 'infallible'
//Bubble up to deploy(_modal)() calls
pub enum TerminalContent {
pub enum TerminalContainerContent {
Connected(ViewHandle<TerminalView>),
Error(ViewHandle<ErrorView>),
}
impl TerminalContent {
impl TerminalContainerContent {
fn handle(&self) -> AnyViewHandle {
match self {
Self::Connected(handle) => handle.into(),
@ -45,7 +45,7 @@ impl TerminalContent {
pub struct TerminalContainer {
modal: bool,
pub content: TerminalContent,
pub content: TerminalContainerContent,
associated_directory: Option<PathBuf>,
}
@ -119,13 +119,13 @@ impl TerminalContainer {
let view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
cx.subscribe(&view, |_this, _content, event, cx| cx.emit(*event))
.detach();
TerminalContent::Connected(view)
TerminalContainerContent::Connected(view)
}
Err(error) => {
let view = cx.add_view(|_| ErrorView {
error: error.downcast::<TerminalError>().unwrap(),
});
TerminalContent::Error(view)
TerminalContainerContent::Error(view)
}
};
cx.focus(content.handle());
@ -145,7 +145,7 @@ impl TerminalContainer {
let connected_view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
TerminalContainer {
modal,
content: TerminalContent::Connected(connected_view),
content: TerminalContainerContent::Connected(connected_view),
associated_directory: None,
}
}
@ -158,8 +158,8 @@ impl View for TerminalContainer {
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
let child_view = match &self.content {
TerminalContent::Connected(connected) => ChildView::new(connected),
TerminalContent::Error(error) => ChildView::new(error),
TerminalContainerContent::Connected(connected) => ChildView::new(connected),
TerminalContainerContent::Error(error) => ChildView::new(error),
};
if self.modal {
let settings = cx.global::<Settings>();
@ -238,10 +238,10 @@ impl Item for TerminalContainer {
cx: &gpui::AppContext,
) -> ElementBox {
let title = match &self.content {
TerminalContent::Connected(connected) => {
TerminalContainerContent::Connected(connected) => {
connected.read(cx).handle().read(cx).title.to_string()
}
TerminalContent::Error(_) => "Terminal".to_string(),
TerminalContainerContent::Error(_) => "Terminal".to_string(),
};
Flex::row()
@ -309,7 +309,7 @@ impl Item for TerminalContainer {
}
fn is_dirty(&self, cx: &gpui::AppContext) -> bool {
if let TerminalContent::Connected(connected) = &self.content {
if let TerminalContainerContent::Connected(connected) = &self.content {
connected.read(cx).has_new_content()
} else {
false
@ -317,7 +317,7 @@ impl Item for TerminalContainer {
}
fn has_conflict(&self, cx: &AppContext) -> bool {
if let TerminalContent::Connected(connected) = &self.content {
if let TerminalContainerContent::Connected(connected) = &self.content {
connected.read(cx).has_bell()
} else {
false
@ -351,7 +351,7 @@ impl SearchableItem for TerminalContainer {
/// Clear stored matches
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
if let TerminalContent::Connected(connected) = &self.content {
if let TerminalContainerContent::Connected(connected) = &self.content {
let terminal = connected.read(cx).terminal().clone();
terminal.update(cx, |term, _| term.matches.clear())
}
@ -359,18 +359,22 @@ impl SearchableItem for TerminalContainer {
/// Store matches returned from find_matches somewhere for rendering
fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
if let TerminalContent::Connected(connected) = &self.content {
if let TerminalContainerContent::Connected(connected) = &self.content {
let terminal = connected.read(cx).terminal().clone();
dbg!(&matches);
terminal.update(cx, |term, _| term.matches = matches)
}
}
/// Return the selection content to pre-load into this search
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
if let TerminalContent::Connected(connected) = &self.content {
if let TerminalContainerContent::Connected(connected) = &self.content {
let terminal = connected.read(cx).terminal().clone();
terminal.read(cx).selection_text.clone().unwrap_or_default()
terminal
.read(cx)
.last_content
.selection_text
.clone()
.unwrap_or_default()
} else {
Default::default()
}
@ -403,7 +407,7 @@ impl SearchableItem for TerminalContainer {
query: project::search::SearchQuery,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Self::Match>> {
if let TerminalContent::Connected(connected) = &self.content {
if let TerminalContainerContent::Connected(connected) = &self.content {
let terminal = connected.read(cx).terminal().clone();
terminal.update(cx, |term, cx| term.find_matches(query, cx))
} else {

View File

@ -2,10 +2,7 @@ use alacritty_terminal::{
ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
grid::Dimensions,
index::Point,
term::{
cell::{Cell, Flags},
TermMode,
},
term::{cell::Flags, TermMode},
};
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
use gpui::{
@ -27,15 +24,12 @@ use theme::TerminalStyle;
use util::ResultExt;
use std::{fmt::Debug, ops::RangeInclusive};
use std::{
mem,
ops::{Deref, Range},
};
use std::{mem, ops::Range};
use crate::{
mappings::colors::convert_color,
terminal_view::{DeployContextMenu, TerminalView},
Terminal, TerminalSize,
IndexedCell, Terminal, TerminalContent, TerminalSize,
};
///The information generated during layout that is nescessary for painting
@ -50,21 +44,6 @@ pub struct LayoutState {
display_offset: usize,
}
#[derive(Debug)]
struct IndexedCell {
point: Point,
cell: Cell,
}
impl Deref for IndexedCell {
type Target = Cell;
#[inline]
fn deref(&self) -> &Cell {
&self.cell
}
}
///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
struct DisplayCursor {
line: i32,
@ -195,7 +174,7 @@ impl TerminalElement {
//Vec<Range<Point>> -> Clip out the parts of the ranges
fn layout_grid(
grid: Vec<IndexedCell>,
grid: &Vec<IndexedCell>,
text_style: &TextStyle,
terminal_theme: &TerminalStyle,
text_layout_cache: &TextLayoutCache,
@ -581,40 +560,22 @@ impl Element for TerminalElement {
} else {
terminal_theme.colors.background
};
let terminal_handle = self.terminal.upgrade(cx).unwrap();
let (cells, selection, cursor, display_offset, cursor_text, mode) = self
.terminal
.upgrade(cx)
.unwrap()
.update(cx.app, |terminal, cx| {
terminal.set_size(dimensions);
terminal.render_lock(cx, |content, cursor_text| {
let mut cells = vec![];
cells.extend(
content
.display_iter
//TODO: Add this once there's a way to retain empty lines
// .filter(|ic| {
// !ic.flags.contains(Flags::HIDDEN)
// && !(ic.bg == Named(NamedColor::Background)
// && ic.c == ' '
// && !ic.flags.contains(Flags::INVERSE))
// })
.map(|ic| IndexedCell {
point: ic.point,
cell: ic.cell.clone(),
}),
);
(
cells,
content.selection,
content.cursor,
content.display_offset,
cursor_text,
content.mode,
)
})
});
terminal_handle.update(cx.app, |terminal, cx| {
terminal.set_size(dimensions);
terminal.try_sync(cx)
});
let TerminalContent {
cells,
mode,
display_offset,
cursor_char,
selection,
cursor,
..
} = &terminal_handle.read(cx).last_content;
// searches, highlights to a single range representations
let mut relative_highlighted_ranges = Vec::new();
@ -641,9 +602,9 @@ impl Element for TerminalElement {
let cursor = if let AlacCursorShape::Hidden = cursor.shape {
None
} else {
let cursor_point = DisplayCursor::from(cursor.point, display_offset);
let cursor_point = DisplayCursor::from(cursor.point, *display_offset);
let cursor_text = {
let str_trxt = cursor_text.to_string();
let str_trxt = cursor_char.to_string();
let color = if self.focused {
terminal_theme.colors.background
@ -699,8 +660,8 @@ impl Element for TerminalElement {
size: dimensions,
rects,
relative_highlighted_ranges,
mode,
display_offset,
mode: *mode,
display_offset: *display_offset,
},
)
}

View File

@ -149,7 +149,8 @@ impl TerminalView {
if !self
.terminal
.read(cx)
.last_mode
.last_content
.mode
.contains(TermMode::ALT_SCREEN)
{
cx.show_character_palette();
@ -177,7 +178,8 @@ impl TerminalView {
|| self
.terminal
.read(cx)
.last_mode
.last_content
.mode
.contains(TermMode::ALT_SCREEN)
{
return true;
@ -362,7 +364,8 @@ impl View for TerminalView {
if self
.terminal
.read(cx)
.last_mode
.last_content
.mode
.contains(TermMode::ALT_SCREEN)
{
None
@ -387,7 +390,7 @@ impl View for TerminalView {
if self.modal {
context.set.insert("ModalTerminal".into());
}
let mode = self.terminal.read(cx).last_mode;
let mode = self.terminal.read(cx).last_content.mode;
context.map.insert(
"screen".to_string(),
(if mode.contains(TermMode::ALT_SCREEN) {

View File

@ -5,7 +5,6 @@
"requires": true,
"packages": {
"": {
"name": "styles",
"version": "1.0.0",
"license": "ISC",
"dependencies": {