mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-11 21:13:02 +00:00
Merge pull request #1587 from zed-industries/alac-search
Terminal Search
This commit is contained in:
commit
0e0bae8faa
12 changed files with 792 additions and 438 deletions
|
@ -179,7 +179,7 @@ impl View for DiagnosticIndicator {
|
|||
if in_progress {
|
||||
element.add_child(
|
||||
Label::new(
|
||||
"checking…".into(),
|
||||
"Checking…".into(),
|
||||
style.diagnostic_message.default.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
|
|
|
@ -513,17 +513,17 @@ impl SearchableItem for Editor {
|
|||
|
||||
fn to_search_event(event: &Self::Event) -> Option<SearchEvent> {
|
||||
match event {
|
||||
Event::BufferEdited => Some(SearchEvent::ContentsUpdated),
|
||||
Event::SelectionsChanged { .. } => Some(SearchEvent::SelectionsChanged),
|
||||
Event::BufferEdited => Some(SearchEvent::MatchesInvalidated),
|
||||
Event::SelectionsChanged { .. } => Some(SearchEvent::ActiveMatchChanged),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_highlights(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.clear_background_highlights::<BufferSearchHighlights>(cx);
|
||||
}
|
||||
|
||||
fn highlight_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
|
||||
fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
|
||||
self.highlight_background::<BufferSearchHighlights>(
|
||||
matches,
|
||||
|theme| theme.search.match_background,
|
||||
|
@ -553,40 +553,56 @@ impl SearchableItem for Editor {
|
|||
}
|
||||
}
|
||||
|
||||
fn select_next_match_in_direction(
|
||||
&mut self,
|
||||
index: usize,
|
||||
direction: Direction,
|
||||
matches: Vec<Range<Anchor>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let new_index: usize = match_index_for_direction(
|
||||
matches.as_slice(),
|
||||
&self.selections.newest_anchor().head(),
|
||||
index,
|
||||
direction,
|
||||
&self.buffer().read(cx).snapshot(cx),
|
||||
);
|
||||
|
||||
let range_to_select = matches[new_index].clone();
|
||||
self.unfold_ranges([range_to_select.clone()], false, cx);
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_ranges([range_to_select])
|
||||
});
|
||||
}
|
||||
|
||||
fn select_match_by_index(
|
||||
fn activate_match(
|
||||
&mut self,
|
||||
index: usize,
|
||||
matches: Vec<Range<Anchor>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.unfold_ranges([matches[index].clone()], false, cx);
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_ranges([matches[index].clone()])
|
||||
});
|
||||
}
|
||||
|
||||
fn matches(
|
||||
fn match_index_for_direction(
|
||||
&mut self,
|
||||
matches: &Vec<Range<Anchor>>,
|
||||
mut current_index: usize,
|
||||
direction: Direction,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> usize {
|
||||
let buffer = self.buffer().read(cx).snapshot(cx);
|
||||
let cursor = self.selections.newest_anchor().head();
|
||||
if matches[current_index].start.cmp(&cursor, &buffer).is_gt() {
|
||||
if direction == Direction::Prev {
|
||||
if current_index == 0 {
|
||||
current_index = matches.len() - 1;
|
||||
} else {
|
||||
current_index -= 1;
|
||||
}
|
||||
}
|
||||
} else if matches[current_index].end.cmp(&cursor, &buffer).is_lt() {
|
||||
if direction == Direction::Next {
|
||||
current_index = 0;
|
||||
}
|
||||
} else if direction == Direction::Prev {
|
||||
if current_index == 0 {
|
||||
current_index = matches.len() - 1;
|
||||
} else {
|
||||
current_index -= 1;
|
||||
}
|
||||
} else if direction == Direction::Next {
|
||||
if current_index == matches.len() - 1 {
|
||||
current_index = 0
|
||||
} else {
|
||||
current_index += 1;
|
||||
}
|
||||
};
|
||||
current_index
|
||||
}
|
||||
|
||||
fn find_matches(
|
||||
&mut self,
|
||||
query: project::search::SearchQuery,
|
||||
cx: &mut ViewContext<Self>,
|
||||
|
@ -637,41 +653,6 @@ impl SearchableItem for Editor {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn match_index_for_direction(
|
||||
ranges: &[Range<Anchor>],
|
||||
cursor: &Anchor,
|
||||
mut index: usize,
|
||||
direction: Direction,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
) -> usize {
|
||||
if ranges[index].start.cmp(cursor, buffer).is_gt() {
|
||||
if direction == Direction::Prev {
|
||||
if index == 0 {
|
||||
index = ranges.len() - 1;
|
||||
} else {
|
||||
index -= 1;
|
||||
}
|
||||
}
|
||||
} else if ranges[index].end.cmp(cursor, buffer).is_lt() {
|
||||
if direction == Direction::Next {
|
||||
index = 0;
|
||||
}
|
||||
} else if direction == Direction::Prev {
|
||||
if index == 0 {
|
||||
index = ranges.len() - 1;
|
||||
} else {
|
||||
index -= 1;
|
||||
}
|
||||
} else if direction == Direction::Next {
|
||||
if index == ranges.len() - 1 {
|
||||
index = 0
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
};
|
||||
index
|
||||
}
|
||||
|
||||
pub fn active_match_index(
|
||||
ranges: &[Range<Anchor>],
|
||||
cursor: &Anchor,
|
||||
|
|
|
@ -233,7 +233,6 @@ impl SyntaxSnapshot {
|
|||
};
|
||||
let (start_byte, start_point) = layer.range.start.summary::<(usize, Point)>(text);
|
||||
|
||||
|
||||
// Ignore edits that end before the start of this layer, and don't consider them
|
||||
// for any subsequent layers at this same depth.
|
||||
loop {
|
||||
|
|
|
@ -95,6 +95,12 @@ impl View for BufferSearchBar {
|
|||
} else {
|
||||
theme.search.editor.input.container
|
||||
};
|
||||
let supported_options = self
|
||||
.active_searchable_item
|
||||
.as_ref()
|
||||
.map(|active_searchable_item| active_searchable_item.supported_options())
|
||||
.unwrap_or_default();
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
|
@ -143,9 +149,24 @@ impl View for BufferSearchBar {
|
|||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(self.render_search_option("Case", SearchOption::CaseSensitive, cx))
|
||||
.with_child(self.render_search_option("Word", SearchOption::WholeWord, cx))
|
||||
.with_child(self.render_search_option("Regex", SearchOption::Regex, cx))
|
||||
.with_children(self.render_search_option(
|
||||
supported_options.case,
|
||||
"Case",
|
||||
SearchOption::CaseSensitive,
|
||||
cx,
|
||||
))
|
||||
.with_children(self.render_search_option(
|
||||
supported_options.word,
|
||||
"Word",
|
||||
SearchOption::WholeWord,
|
||||
cx,
|
||||
))
|
||||
.with_children(self.render_search_option(
|
||||
supported_options.regex,
|
||||
"Regex",
|
||||
SearchOption::Regex,
|
||||
cx,
|
||||
))
|
||||
.contained()
|
||||
.with_style(theme.search.option_button_group)
|
||||
.aligned()
|
||||
|
@ -174,7 +195,9 @@ impl ToolbarItemView for BufferSearchBar {
|
|||
cx,
|
||||
Box::new(move |search_event, cx| {
|
||||
if let Some(this) = handle.upgrade(cx) {
|
||||
this.update(cx, |this, cx| this.on_active_editor_event(search_event, cx));
|
||||
this.update(cx, |this, cx| {
|
||||
this.on_active_searchable_item_event(search_event, cx)
|
||||
});
|
||||
}
|
||||
}),
|
||||
));
|
||||
|
@ -232,7 +255,7 @@ impl BufferSearchBar {
|
|||
if let Some(searchable_item) =
|
||||
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
|
||||
{
|
||||
searchable_item.clear_highlights(cx);
|
||||
searchable_item.clear_matches(cx);
|
||||
}
|
||||
}
|
||||
if let Some(active_editor) = self.active_searchable_item.as_ref() {
|
||||
|
@ -281,36 +304,43 @@ impl BufferSearchBar {
|
|||
|
||||
fn render_search_option(
|
||||
&self,
|
||||
option_supported: bool,
|
||||
icon: &str,
|
||||
option: SearchOption,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
) -> Option<ElementBox> {
|
||||
if !option_supported {
|
||||
return None;
|
||||
}
|
||||
|
||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
let is_active = self.is_search_option_enabled(option);
|
||||
MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
|
||||
let style = &cx
|
||||
.global::<Settings>()
|
||||
.theme
|
||||
.search
|
||||
.option_button
|
||||
.style_for(state, is_active);
|
||||
Label::new(icon.to_string(), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_any_action(option.to_toggle_action())
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<Self, _>(
|
||||
option as usize,
|
||||
format!("Toggle {}", option.label()),
|
||||
Some(option.to_toggle_action()),
|
||||
tooltip_style,
|
||||
cx,
|
||||
Some(
|
||||
MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
|
||||
let style = &cx
|
||||
.global::<Settings>()
|
||||
.theme
|
||||
.search
|
||||
.option_button
|
||||
.style_for(state, is_active);
|
||||
Label::new(icon.to_string(), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_any_action(option.to_toggle_action())
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<Self, _>(
|
||||
option as usize,
|
||||
format!("Toggle {}", option.label()),
|
||||
Some(option.to_toggle_action()),
|
||||
tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_nav_button(
|
||||
|
@ -420,8 +450,10 @@ impl BufferSearchBar {
|
|||
.seachable_items_with_matches
|
||||
.get(&searchable_item.downgrade())
|
||||
{
|
||||
searchable_item.select_next_match_in_direction(index, direction, matches, cx);
|
||||
searchable_item.highlight_matches(matches, cx);
|
||||
let new_match_index =
|
||||
searchable_item.match_index_for_direction(matches, index, direction, cx);
|
||||
searchable_item.update_matches(matches, cx);
|
||||
searchable_item.activate_match(new_match_index, matches, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -461,10 +493,10 @@ impl BufferSearchBar {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_active_editor_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
|
||||
fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
|
||||
match event {
|
||||
SearchEvent::ContentsUpdated => self.update_matches(false, cx),
|
||||
SearchEvent::SelectionsChanged => self.update_match_index(cx),
|
||||
SearchEvent::MatchesInvalidated => self.update_matches(false, cx),
|
||||
SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -477,7 +509,7 @@ impl BufferSearchBar {
|
|||
if Some(&searchable_item) == self.active_searchable_item.as_ref() {
|
||||
active_item_matches = Some((searchable_item.downgrade(), matches));
|
||||
} else {
|
||||
searchable_item.clear_highlights(cx);
|
||||
searchable_item.clear_matches(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -492,7 +524,7 @@ impl BufferSearchBar {
|
|||
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
|
||||
if query.is_empty() {
|
||||
self.active_match_index.take();
|
||||
active_searchable_item.clear_highlights(cx);
|
||||
active_searchable_item.clear_matches(cx);
|
||||
} else {
|
||||
let query = if self.regex {
|
||||
match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
|
||||
|
@ -507,7 +539,7 @@ impl BufferSearchBar {
|
|||
SearchQuery::text(query, self.whole_word, self.case_sensitive)
|
||||
};
|
||||
|
||||
let matches = active_searchable_item.matches(query, cx);
|
||||
let matches = active_searchable_item.find_matches(query, cx);
|
||||
|
||||
let active_searchable_item = active_searchable_item.downgrade();
|
||||
self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
|
||||
|
@ -527,13 +559,13 @@ impl BufferSearchBar {
|
|||
.seachable_items_with_matches
|
||||
.get(&active_searchable_item.downgrade())
|
||||
.unwrap();
|
||||
active_searchable_item.update_matches(matches, cx);
|
||||
if select_closest_match {
|
||||
if let Some(match_ix) = this.active_match_index {
|
||||
active_searchable_item
|
||||
.select_match_by_index(match_ix, matches, cx);
|
||||
.activate_match(match_ix, matches, cx);
|
||||
}
|
||||
}
|
||||
active_searchable_item.highlight_matches(matches, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ use crate::{
|
|||
};
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
items::{active_match_index, match_index_for_direction},
|
||||
Anchor, Autoscroll, Editor, MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN,
|
||||
items::active_match_index, Anchor, Autoscroll, Editor, MultiBuffer, SelectAll,
|
||||
MAX_TAB_TITLE_LEN,
|
||||
};
|
||||
use gpui::{
|
||||
actions, elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox,
|
||||
|
@ -23,7 +23,7 @@ use std::{
|
|||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
searchable::{Direction, SearchableItemHandle},
|
||||
searchable::{Direction, SearchableItem, SearchableItemHandle},
|
||||
Item, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
|
@ -486,16 +486,12 @@ impl ProjectSearchView {
|
|||
|
||||
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
||||
if let Some(index) = self.active_match_index {
|
||||
let model = self.model.read(cx);
|
||||
let results_editor = self.results_editor.read(cx);
|
||||
let new_index = match_index_for_direction(
|
||||
&model.match_ranges,
|
||||
&results_editor.selections.newest_anchor().head(),
|
||||
index,
|
||||
direction,
|
||||
&results_editor.buffer().read(cx).snapshot(cx),
|
||||
);
|
||||
let range_to_select = model.match_ranges[new_index].clone();
|
||||
let match_ranges = self.model.read(cx).match_ranges.clone();
|
||||
let new_index = self.results_editor.update(cx, |editor, cx| {
|
||||
editor.match_index_for_direction(&match_ranges, index, direction, cx)
|
||||
});
|
||||
|
||||
let range_to_select = match_ranges[new_index].clone();
|
||||
self.results_editor.update(cx, |editor, cx| {
|
||||
editor.unfold_ranges([range_to_select.clone()], false, cx);
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -9,15 +9,21 @@ use alacritty_terminal::{
|
|||
config::{Config, Program, PtyConfig, Scrolling},
|
||||
event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
|
||||
event_loop::{EventLoop, Msg, Notifier},
|
||||
grid::{Dimensions, Scroll},
|
||||
index::{Direction, Point},
|
||||
selection::{Selection, SelectionType},
|
||||
grid::{Dimensions, Scroll as AlacScroll},
|
||||
index::{Column, Direction as AlacDirection, Line, Point},
|
||||
selection::{Selection, SelectionRange, SelectionType},
|
||||
sync::FairMutex,
|
||||
term::{RenderableContent, TermMode},
|
||||
term::{
|
||||
cell::Cell,
|
||||
color::Rgb,
|
||||
search::{Match, RegexIter, RegexSearch},
|
||||
RenderableCursor, TermMode,
|
||||
},
|
||||
tty::{self, setup_env},
|
||||
Term,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use futures::{
|
||||
channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
|
||||
FutureExt,
|
||||
|
@ -27,14 +33,15 @@ use mappings::mouse::{
|
|||
alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
|
||||
};
|
||||
use modal::deploy_modal;
|
||||
|
||||
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
fmt::Display,
|
||||
ops::Sub,
|
||||
ops::{Deref, RangeInclusive, Sub},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
|
@ -43,7 +50,7 @@ use gpui::{
|
|||
keymap::Keystroke,
|
||||
scene::{ClickRegionEvent, DownRegionEvent, DragRegionEvent, UpRegionEvent},
|
||||
ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext,
|
||||
ScrollWheelEvent,
|
||||
ScrollWheelEvent, Task,
|
||||
};
|
||||
|
||||
use crate::mappings::{
|
||||
|
@ -62,8 +69,8 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
|
||||
///Scroll multiplier that is set to 3 by default. This will be removed when I
|
||||
///Implement scroll bars.
|
||||
pub const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
|
||||
|
||||
const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
|
||||
const MAX_SEARCH_LINES: usize = 100;
|
||||
const DEBUG_TERMINAL_WIDTH: f32 = 500.;
|
||||
const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
|
||||
const DEBUG_CELL_WIDTH: f32 = 5.;
|
||||
|
@ -77,15 +84,18 @@ pub enum Event {
|
|||
Bell,
|
||||
Wakeup,
|
||||
BlinkChanged,
|
||||
SelectionsChanged,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
enum InternalEvent {
|
||||
TermEvent(AlacTermEvent),
|
||||
ColorRequest(usize, Arc<dyn Fn(Rgb) -> String + Sync + Send + 'static>),
|
||||
Resize(TerminalSize),
|
||||
Clear,
|
||||
Scroll(Scroll),
|
||||
SetSelection(Option<Selection>),
|
||||
// FocusNextMatch,
|
||||
Scroll(AlacScroll),
|
||||
ScrollToPoint(Point),
|
||||
SetSelection(Option<(Selection, Point)>),
|
||||
UpdateSelection(Vector2F),
|
||||
Copy,
|
||||
}
|
||||
|
@ -164,8 +174,12 @@ impl From<TerminalSize> for WindowSize {
|
|||
}
|
||||
|
||||
impl Dimensions for TerminalSize {
|
||||
/// Note: this is supposed to be for the back buffer's length,
|
||||
/// but we exclusively use it to resize the terminal, which does not
|
||||
/// use this method. We still have to implement it for the trait though,
|
||||
/// hence, this comment.
|
||||
fn total_lines(&self) -> usize {
|
||||
self.screen_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer...
|
||||
self.screen_lines()
|
||||
}
|
||||
|
||||
fn screen_lines(&self) -> usize {
|
||||
|
@ -366,11 +380,13 @@ 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,
|
||||
current_selection: false,
|
||||
matches: Vec::new(),
|
||||
last_synced: Instant::now(),
|
||||
sync_task: None,
|
||||
selection_head: None,
|
||||
};
|
||||
|
||||
Ok(TerminalBuilder {
|
||||
|
@ -432,17 +448,62 @@ 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,
|
||||
last_mouse: Option<(Point, AlacDirection)>,
|
||||
pub matches: Vec<RangeInclusive<Point>>,
|
||||
cur_size: TerminalSize,
|
||||
last_mode: TermMode,
|
||||
last_offset: usize,
|
||||
last_mouse: Option<(Point, Direction)>,
|
||||
current_selection: bool,
|
||||
last_content: TerminalContent,
|
||||
last_synced: Instant,
|
||||
sync_task: Option<Task<()>>,
|
||||
selection_head: Option<Point>,
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
|
@ -482,9 +543,11 @@ impl Terminal {
|
|||
cx.emit(Event::Wakeup);
|
||||
cx.notify();
|
||||
}
|
||||
AlacTermEvent::ColorRequest(_, _) => self
|
||||
.events
|
||||
.push_back(InternalEvent::TermEvent(event.clone())),
|
||||
AlacTermEvent::ColorRequest(idx, fun_ptr) => {
|
||||
self.events
|
||||
.push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
|
||||
cx.notify(); //Immediately schedule a render to respond to the color request
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -496,14 +559,12 @@ impl Terminal {
|
|||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
InternalEvent::TermEvent(term_event) => {
|
||||
if let AlacTermEvent::ColorRequest(index, format) = term_event {
|
||||
let color = term.colors()[*index].unwrap_or_else(|| {
|
||||
let term_style = &cx.global::<Settings>().theme.terminal;
|
||||
to_alac_rgb(get_color_at_index(index, &term_style.colors))
|
||||
});
|
||||
self.write_to_pty(format(color))
|
||||
}
|
||||
InternalEvent::ColorRequest(index, format) => {
|
||||
let color = term.colors()[*index].unwrap_or_else(|| {
|
||||
let term_style = &cx.global::<Settings>().theme.terminal;
|
||||
to_alac_rgb(get_color_at_index(index, &term_style.colors))
|
||||
});
|
||||
self.write_to_pty(format(color))
|
||||
}
|
||||
InternalEvent::Resize(new_size) => {
|
||||
self.cur_size = *new_size;
|
||||
|
@ -519,7 +580,14 @@ impl Terminal {
|
|||
InternalEvent::Scroll(scroll) => {
|
||||
term.scroll_display(*scroll);
|
||||
}
|
||||
InternalEvent::SetSelection(sel) => term.selection = sel.clone(),
|
||||
InternalEvent::SetSelection(selection) => {
|
||||
term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
|
||||
|
||||
if let Some((_, head)) = selection {
|
||||
self.selection_head = Some(*head);
|
||||
}
|
||||
cx.emit(Event::SelectionsChanged)
|
||||
}
|
||||
InternalEvent::UpdateSelection(position) => {
|
||||
if let Some(mut selection) = term.selection.take() {
|
||||
let point = mouse_point(*position, self.cur_size, term.grid().display_offset());
|
||||
|
@ -527,6 +595,9 @@ impl Terminal {
|
|||
|
||||
selection.update(point, side);
|
||||
term.selection = Some(selection);
|
||||
|
||||
self.selection_head = Some(point);
|
||||
cx.emit(Event::SelectionsChanged)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -535,27 +606,30 @@ impl Terminal {
|
|||
cx.write_to_clipboard(ClipboardItem::new(txt))
|
||||
}
|
||||
}
|
||||
InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point),
|
||||
}
|
||||
}
|
||||
|
||||
fn begin_select(&mut self, sel: Selection) {
|
||||
self.current_selection = true;
|
||||
pub fn last_content(&self) -> &TerminalContent {
|
||||
&self.last_content
|
||||
}
|
||||
|
||||
//To test:
|
||||
//- Activate match on terminal (scrolling and selection)
|
||||
//- Editor search snapping behavior
|
||||
|
||||
pub fn activate_match(&mut self, index: usize) {
|
||||
if let Some(search_match) = self.matches.get(index).cloned() {
|
||||
self.set_selection(Some((make_selection(&search_match), *search_match.end())));
|
||||
|
||||
self.events
|
||||
.push_back(InternalEvent::ScrollToPoint(*search_match.start()));
|
||||
}
|
||||
}
|
||||
|
||||
fn set_selection(&mut self, selection: Option<(Selection, Point)>) {
|
||||
self.events
|
||||
.push_back(InternalEvent::SetSelection(Some(sel)));
|
||||
}
|
||||
|
||||
fn continue_selection(&mut self, location: Vector2F) {
|
||||
self.events
|
||||
.push_back(InternalEvent::UpdateSelection(location))
|
||||
}
|
||||
|
||||
fn end_select(&mut self) {
|
||||
self.current_selection = false;
|
||||
self.events.push_back(InternalEvent::SetSelection(None));
|
||||
}
|
||||
|
||||
fn scroll(&mut self, scroll: Scroll) {
|
||||
self.events.push_back(InternalEvent::Scroll(scroll));
|
||||
.push_back(InternalEvent::SetSelection(selection));
|
||||
}
|
||||
|
||||
pub fn copy(&mut self) {
|
||||
|
@ -577,13 +651,15 @@ impl Terminal {
|
|||
}
|
||||
|
||||
pub fn input(&mut self, input: String) {
|
||||
self.scroll(Scroll::Bottom);
|
||||
self.end_select();
|
||||
self.events
|
||||
.push_back(InternalEvent::Scroll(AlacScroll::Bottom));
|
||||
self.events.push_back(InternalEvent::SetSelection(None));
|
||||
|
||||
self.write_to_pty(input);
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -594,7 +670,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")
|
||||
|
@ -602,42 +678,81 @@ 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,
|
||||
{
|
||||
let m = self.term.clone(); //Arc clone
|
||||
let mut term = m.lock();
|
||||
pub fn try_sync(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let term = self.term.clone();
|
||||
|
||||
//Note that this ordering matters for
|
||||
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.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());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mouse_changed(&mut self, point: Point, side: Direction) -> bool {
|
||||
pub fn mouse_changed(&mut self, point: Point, side: AlacDirection) -> bool {
|
||||
match self.last_mouse {
|
||||
Some((old_point, old_side)) => {
|
||||
if old_point == point && old_side == side {
|
||||
|
@ -655,17 +770,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);
|
||||
}
|
||||
}
|
||||
|
@ -677,40 +792,54 @@ impl Terminal {
|
|||
if !self.mouse_mode(e.shift) {
|
||||
// Alacritty has the same ordering, of first updating the selection
|
||||
// then scrolling 15ms later
|
||||
self.continue_selection(position);
|
||||
self.events
|
||||
.push_back(InternalEvent::UpdateSelection(position));
|
||||
|
||||
// Doesn't make sense to scroll the alt screen
|
||||
if !self.last_mode.contains(TermMode::ALT_SCREEN) {
|
||||
//TODO: Why do these need to be doubled?
|
||||
let top = e.region.origin_y() + (self.cur_size.line_height * 2.);
|
||||
let bottom = e.region.lower_left().y() - (self.cur_size.line_height * 2.);
|
||||
|
||||
let scroll_delta = if e.position.y() < top {
|
||||
(top - e.position.y()).powf(1.1)
|
||||
} else if e.position.y() > bottom {
|
||||
-((e.position.y() - bottom).powf(1.1))
|
||||
} else {
|
||||
return; //Nothing to do
|
||||
if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
|
||||
let scroll_delta = match self.drag_line_delta(e) {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let scroll_lines = (scroll_delta / self.cur_size.line_height) as i32;
|
||||
self.scroll(Scroll::Delta(scroll_lines));
|
||||
self.continue_selection(position)
|
||||
|
||||
self.events
|
||||
.push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
|
||||
self.events
|
||||
.push_back(InternalEvent::UpdateSelection(position))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drag_line_delta(&mut self, e: DragRegionEvent) -> Option<f32> {
|
||||
//TODO: Why do these need to be doubled? Probably the same problem that the IME has
|
||||
let top = e.region.origin_y() + (self.cur_size.line_height * 2.);
|
||||
let bottom = e.region.lower_left().y() - (self.cur_size.line_height * 2.);
|
||||
let scroll_delta = if e.position.y() < top {
|
||||
(top - e.position.y()).powf(1.1)
|
||||
} else if e.position.y() > bottom {
|
||||
-((e.position.y() - bottom).powf(1.1))
|
||||
} else {
|
||||
return None; //Nothing to do
|
||||
};
|
||||
Some(scroll_delta)
|
||||
}
|
||||
|
||||
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 {
|
||||
self.begin_select(Selection::new(SelectionType::Simple, point, side));
|
||||
self.events.push_back(InternalEvent::SetSelection(Some((
|
||||
Selection::new(SelectionType::Simple, point, side),
|
||||
point,
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -718,7 +847,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 {
|
||||
|
@ -733,7 +862,8 @@ impl Terminal {
|
|||
selection_type.map(|selection_type| Selection::new(selection_type, point, side));
|
||||
|
||||
if let Some(sel) = selection {
|
||||
self.begin_select(sel);
|
||||
self.events
|
||||
.push_back(InternalEvent::SetSelection(Some((sel, point))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -741,9 +871,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 {
|
||||
|
@ -751,6 +881,7 @@ impl Terminal {
|
|||
// so let's do that here
|
||||
self.copy();
|
||||
}
|
||||
self.last_mouse = None;
|
||||
}
|
||||
|
||||
///Scroll the terminal
|
||||
|
@ -761,15 +892,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
|
||||
{
|
||||
|
@ -782,11 +920,39 @@ impl Terminal {
|
|||
let scroll_lines =
|
||||
((e.delta.y() * ALACRITTY_SCROLL_MULTIPLIER) / self.cur_size.line_height) as i32;
|
||||
if scroll_lines != 0 {
|
||||
let scroll = Scroll::Delta(scroll_lines);
|
||||
self.scroll(scroll);
|
||||
let scroll = AlacScroll::Delta(scroll_lines);
|
||||
|
||||
self.events.push_back(InternalEvent::Scroll(scroll));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_matches(
|
||||
&mut self,
|
||||
query: project::search::SearchQuery,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Vec<RangeInclusive<Point>>> {
|
||||
let term = self.term.clone();
|
||||
cx.background().spawn(async move {
|
||||
let searcher = match query {
|
||||
project::search::SearchQuery::Text { query, .. } => {
|
||||
RegexSearch::new(query.as_ref())
|
||||
}
|
||||
project::search::SearchQuery::Regex { query, .. } => {
|
||||
RegexSearch::new(query.as_ref())
|
||||
}
|
||||
};
|
||||
|
||||
if searcher.is_err() {
|
||||
return Vec::new();
|
||||
}
|
||||
let searcher = searcher.unwrap();
|
||||
|
||||
let term = term.lock();
|
||||
|
||||
make_search_matches(&term, &searcher).collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Terminal {
|
||||
|
@ -799,6 +965,30 @@ impl Entity for Terminal {
|
|||
type Event = Event;
|
||||
}
|
||||
|
||||
fn make_selection(range: &RangeInclusive<Point>) -> Selection {
|
||||
let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
|
||||
selection.update(*range.end(), AlacDirection::Right);
|
||||
selection
|
||||
}
|
||||
|
||||
/// Copied from alacritty/src/display/hint.rs HintMatches::visible_regex_matches()
|
||||
/// Iterate over all visible regex matches.
|
||||
fn make_search_matches<'a, T>(
|
||||
term: &'a Term<T>,
|
||||
regex: &'a RegexSearch,
|
||||
) -> impl Iterator<Item = Match> + 'a {
|
||||
let viewport_start = Line(-(term.grid().display_offset() as i32));
|
||||
let viewport_end = viewport_start + term.bottommost_line();
|
||||
let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
|
||||
let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
|
||||
start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
|
||||
end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
|
||||
|
||||
RegexIter::new(start, end, AlacDirection::Right, term, regex)
|
||||
.skip_while(move |rm| rm.end().line < viewport_start)
|
||||
.take_while(move |rm| rm.start().line <= viewport_end)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub mod terminal_test_context;
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
use crate::terminal_view::TerminalView;
|
||||
use crate::{Event, Terminal, TerminalBuilder, TerminalError};
|
||||
|
||||
use alacritty_terminal::index::Point;
|
||||
use dirs::home_dir;
|
||||
use gpui::{
|
||||
actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, View,
|
||||
ViewContext, ViewHandle,
|
||||
actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, Task,
|
||||
View, ViewContext, ViewHandle,
|
||||
};
|
||||
use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
|
||||
use workspace::{Item, Workspace};
|
||||
|
||||
use crate::TerminalSize;
|
||||
use project::{LocalWorktree, Project, ProjectPath};
|
||||
use settings::{AlternateScroll, Settings, WorkingDirectory};
|
||||
use smallvec::SmallVec;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::terminal_element::TerminalElement;
|
||||
|
@ -26,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(),
|
||||
|
@ -42,7 +45,7 @@ impl TerminalContent {
|
|||
|
||||
pub struct TerminalContainer {
|
||||
modal: bool,
|
||||
pub content: TerminalContent,
|
||||
pub content: TerminalContainerContent,
|
||||
associated_directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
|
@ -116,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());
|
||||
|
@ -142,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,
|
||||
}
|
||||
}
|
||||
|
@ -155,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>();
|
||||
|
@ -235,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()
|
||||
|
@ -306,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
|
||||
|
@ -314,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
|
||||
|
@ -328,6 +331,115 @@ impl Item for TerminalContainer {
|
|||
fn should_close_item_on_event(event: &Self::Event) -> bool {
|
||||
matches!(event, &Event::CloseTerminal)
|
||||
}
|
||||
|
||||
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(handle.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableItem for TerminalContainer {
|
||||
type Match = RangeInclusive<Point>;
|
||||
|
||||
fn supported_options() -> SearchOptions {
|
||||
SearchOptions {
|
||||
case: false,
|
||||
word: false,
|
||||
regex: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert events raised by this item into search-relevant events (if applicable)
|
||||
fn to_search_event(event: &Self::Event) -> Option<SearchEvent> {
|
||||
match event {
|
||||
Event::Wakeup => Some(SearchEvent::MatchesInvalidated),
|
||||
Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear stored matches
|
||||
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let TerminalContainerContent::Connected(connected) = &self.content {
|
||||
let terminal = connected.read(cx).terminal().clone();
|
||||
terminal.update(cx, |term, _| term.matches.clear())
|
||||
}
|
||||
}
|
||||
|
||||
/// Store matches returned from find_matches somewhere for rendering
|
||||
fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
|
||||
if let TerminalContainerContent::Connected(connected) = &self.content {
|
||||
let terminal = connected.read(cx).terminal().clone();
|
||||
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 TerminalContainerContent::Connected(connected) = &self.content {
|
||||
let terminal = connected.read(cx).terminal().clone();
|
||||
terminal
|
||||
.read(cx)
|
||||
.last_content
|
||||
.selection_text
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Focus match at given index into the Vec of matches
|
||||
fn activate_match(&mut self, index: usize, _: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
|
||||
if let TerminalContainerContent::Connected(connected) = &self.content {
|
||||
let terminal = connected.read(cx).terminal().clone();
|
||||
terminal.update(cx, |term, _| term.activate_match(index));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all of the matches for this query, should be done on the background
|
||||
fn find_matches(
|
||||
&mut self,
|
||||
query: project::search::SearchQuery,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Vec<Self::Match>> {
|
||||
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 {
|
||||
Task::ready(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Reports back to the search toolbar what the active match should be (the selection)
|
||||
fn active_match_index(
|
||||
&mut self,
|
||||
matches: Vec<Self::Match>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<usize> {
|
||||
if let TerminalContainerContent::Connected(connected) = &self.content {
|
||||
if let Some(selection_head) = connected.read(cx).terminal().read(cx).selection_head {
|
||||
// If selection head is contained in a match. Return that match
|
||||
for (ix, search_match) in matches.iter().enumerate() {
|
||||
if search_match.contains(&selection_head) {
|
||||
return Some(ix);
|
||||
}
|
||||
|
||||
// If not contained, return the next match after the selection head
|
||||
if search_match.start() > &selection_head {
|
||||
return Some(ix);
|
||||
}
|
||||
}
|
||||
|
||||
// If no selection after selection head, return the last match
|
||||
return Some(matches.len() - 1);
|
||||
} else {
|
||||
Some(0)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///Get's the working directory for the given workspace, respecting the user's settings.
|
||||
|
|
|
@ -2,11 +2,7 @@ use alacritty_terminal::{
|
|||
ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
|
||||
grid::Dimensions,
|
||||
index::Point,
|
||||
selection::SelectionRange,
|
||||
term::{
|
||||
cell::{Cell, Flags},
|
||||
TermMode,
|
||||
},
|
||||
term::{cell::Flags, TermMode},
|
||||
};
|
||||
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
|
||||
use gpui::{
|
||||
|
@ -27,43 +23,25 @@ use settings::Settings;
|
|||
use theme::TerminalStyle;
|
||||
use util::ResultExt;
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::{
|
||||
mem,
|
||||
ops::{Deref, Range},
|
||||
};
|
||||
use std::{fmt::Debug, ops::RangeInclusive};
|
||||
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
|
||||
pub struct LayoutState {
|
||||
cells: Vec<LayoutCell>,
|
||||
rects: Vec<LayoutRect>,
|
||||
highlights: Vec<RelativeHighlightedRange>,
|
||||
relative_highlighted_ranges: Vec<(RangeInclusive<Point>, Color)>,
|
||||
cursor: Option<Cursor>,
|
||||
background_color: Color,
|
||||
selection_color: Color,
|
||||
size: TerminalSize,
|
||||
mode: TermMode,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct IndexedCell {
|
||||
point: Point,
|
||||
cell: Cell,
|
||||
}
|
||||
|
||||
impl Deref for IndexedCell {
|
||||
type Target = Cell;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Cell {
|
||||
&self.cell
|
||||
}
|
||||
display_offset: usize,
|
||||
}
|
||||
|
||||
///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
|
||||
|
@ -166,30 +144,6 @@ impl LayoutRect {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct RelativeHighlightedRange {
|
||||
line_index: usize,
|
||||
range: Range<usize>,
|
||||
}
|
||||
|
||||
impl RelativeHighlightedRange {
|
||||
fn new(line_index: usize, range: Range<usize>) -> Self {
|
||||
RelativeHighlightedRange { line_index, range }
|
||||
}
|
||||
|
||||
fn to_highlighted_range_line(
|
||||
&self,
|
||||
origin: Vector2F,
|
||||
layout: &LayoutState,
|
||||
) -> HighlightedRangeLine {
|
||||
let start_x = origin.x() + self.range.start as f32 * layout.size.cell_width;
|
||||
let end_x =
|
||||
origin.x() + self.range.end as f32 * layout.size.cell_width + layout.size.cell_width;
|
||||
|
||||
HighlightedRangeLine { start_x, end_x }
|
||||
}
|
||||
}
|
||||
|
||||
///The GPUI element that paints the terminal.
|
||||
///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 {
|
||||
|
@ -217,48 +171,31 @@ 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,
|
||||
font_cache: &FontCache,
|
||||
modal: bool,
|
||||
selection_range: Option<SelectionRange>,
|
||||
) -> (
|
||||
Vec<LayoutCell>,
|
||||
Vec<LayoutRect>,
|
||||
Vec<RelativeHighlightedRange>,
|
||||
) {
|
||||
) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
|
||||
let mut cells = vec![];
|
||||
let mut rects = vec![];
|
||||
let mut highlight_ranges = vec![];
|
||||
|
||||
let mut cur_rect: Option<LayoutRect> = None;
|
||||
let mut cur_alac_color = None;
|
||||
let mut highlighted_range = None;
|
||||
|
||||
let linegroups = grid.into_iter().group_by(|i| i.point.line);
|
||||
for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
|
||||
for (x_index, cell) in line.enumerate() {
|
||||
for cell in line {
|
||||
let mut fg = cell.fg;
|
||||
let mut bg = cell.bg;
|
||||
if cell.flags.contains(Flags::INVERSE) {
|
||||
mem::swap(&mut fg, &mut bg);
|
||||
}
|
||||
|
||||
//Increase selection range
|
||||
{
|
||||
if selection_range
|
||||
.map(|range| range.contains(cell.point))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let mut range = highlighted_range.take().unwrap_or(x_index..x_index);
|
||||
range.end = range.end.max(x_index);
|
||||
highlighted_range = Some(range);
|
||||
}
|
||||
}
|
||||
|
||||
//Expand background rect range
|
||||
{
|
||||
if matches!(bg, Named(NamedColor::Background)) {
|
||||
|
@ -324,18 +261,11 @@ impl TerminalElement {
|
|||
};
|
||||
}
|
||||
|
||||
if highlighted_range.is_some() {
|
||||
highlight_ranges.push(RelativeHighlightedRange::new(
|
||||
line_index,
|
||||
highlighted_range.take().unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
if cur_rect.is_some() {
|
||||
rects.push(cur_rect.take().unwrap());
|
||||
}
|
||||
}
|
||||
(cells, rects, highlight_ranges)
|
||||
(cells, rects)
|
||||
}
|
||||
|
||||
// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
|
||||
|
@ -612,60 +542,59 @@ impl Element for TerminalElement {
|
|||
let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
|
||||
let text_style = TerminalElement::make_text_style(font_cache, settings);
|
||||
let selection_color = settings.theme.editor.selection.selection;
|
||||
let match_color = settings.theme.search.match_background;
|
||||
let dimensions = {
|
||||
let line_height = font_cache.line_height(text_style.font_size);
|
||||
let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
|
||||
TerminalSize::new(line_height, cell_width, constraint.max)
|
||||
};
|
||||
|
||||
let search_matches = if let Some(terminal_model) = self.terminal.upgrade(cx) {
|
||||
terminal_model.read(cx).matches.clone()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let background_color = if self.modal {
|
||||
terminal_theme.colors.modal_background
|
||||
} 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, mcx| {
|
||||
terminal.set_size(dimensions);
|
||||
terminal.render_lock(mcx, |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 (cells, rects, highlights) = TerminalElement::layout_grid(
|
||||
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();
|
||||
for search_match in search_matches {
|
||||
relative_highlighted_ranges.push((search_match, match_color))
|
||||
}
|
||||
if let Some(selection) = selection {
|
||||
relative_highlighted_ranges.push((selection.start..=selection.end, selection_color));
|
||||
}
|
||||
|
||||
// then have that representation be converted to the appropriate highlight data structure
|
||||
|
||||
let (cells, rects) = TerminalElement::layout_grid(
|
||||
cells,
|
||||
&text_style,
|
||||
&terminal_theme,
|
||||
cx.text_layout_cache,
|
||||
cx.font_cache(),
|
||||
self.modal,
|
||||
selection,
|
||||
);
|
||||
|
||||
//Layout cursor. Rectangle is used for IME, so we should lay it out even
|
||||
|
@ -673,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
|
||||
|
@ -728,11 +657,11 @@ impl Element for TerminalElement {
|
|||
cells,
|
||||
cursor,
|
||||
background_color,
|
||||
selection_color,
|
||||
size: dimensions,
|
||||
rects,
|
||||
highlights,
|
||||
mode,
|
||||
relative_highlighted_ranges,
|
||||
mode: *mode,
|
||||
display_offset: *display_offset,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -753,6 +682,11 @@ impl Element for TerminalElement {
|
|||
//Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
|
||||
self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.mode, cx);
|
||||
|
||||
cx.scene.push_cursor_region(gpui::CursorRegion {
|
||||
bounds,
|
||||
style: gpui::CursorStyle::IBeam,
|
||||
});
|
||||
|
||||
cx.paint_layer(clip_bounds, |cx| {
|
||||
//Start with a background color
|
||||
cx.scene.push_quad(Quad {
|
||||
|
@ -767,30 +701,23 @@ impl Element for TerminalElement {
|
|||
}
|
||||
});
|
||||
|
||||
//Draw Selection
|
||||
//Draw Highlighted Backgrounds
|
||||
cx.paint_layer(clip_bounds, |cx| {
|
||||
let start_y = layout.highlights.get(0).map(|highlight| {
|
||||
origin.y() + highlight.line_index as f32 * layout.size.line_height
|
||||
});
|
||||
|
||||
if let Some(y) = start_y {
|
||||
let range_lines = layout
|
||||
.highlights
|
||||
.iter()
|
||||
.map(|relative_highlight| {
|
||||
relative_highlight.to_highlighted_range_line(origin, layout)
|
||||
})
|
||||
.collect::<Vec<HighlightedRangeLine>>();
|
||||
|
||||
let hr = HighlightedRange {
|
||||
start_y: y, //Need to change this
|
||||
line_height: layout.size.line_height,
|
||||
lines: range_lines,
|
||||
color: layout.selection_color,
|
||||
//Copied from editor. TODO: move to theme or something
|
||||
corner_radius: 0.15 * layout.size.line_height,
|
||||
};
|
||||
hr.paint(bounds, cx.scene);
|
||||
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.size.line_height,
|
||||
lines: highlighted_range_lines,
|
||||
color: color.clone(),
|
||||
//Copied from editor. TODO: move to theme or something
|
||||
corner_radius: 0.15 * layout.size.line_height,
|
||||
};
|
||||
hr.paint(bounds, cx.scene);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -893,3 +820,65 @@ impl Element for TerminalElement {
|
|||
Some(layout.cursor.as_ref()?.bounding_rect(origin))
|
||||
}
|
||||
}
|
||||
|
||||
fn to_highlighted_range_lines(
|
||||
range: &RangeInclusive<Point>,
|
||||
layout: &LayoutState,
|
||||
origin: Vector2F,
|
||||
) -> Option<(f32, Vec<HighlightedRangeLine>)> {
|
||||
// Step 1. Normalize the points to be viewport relative.
|
||||
//When display_offset = 1, here's how the grid is arranged:
|
||||
//--- Viewport top
|
||||
//-2,0 -2,1...
|
||||
//-1,0 -1,1...
|
||||
//--------- Terminal Top
|
||||
// 0,0 0,1...
|
||||
// 1,0 1,1...
|
||||
//--- Viewport Bottom
|
||||
// 2,0 2,1...
|
||||
//--------- Terminal Bottom
|
||||
|
||||
// Normalize to viewport relative, from terminal relative.
|
||||
// lines are i32s, which are negative above the top left corner of the terminal
|
||||
// If the user has scrolled, we use the display_offset to tell us which offset
|
||||
// of the grid data we should be looking at. But for the rendering step, we don't
|
||||
// want negatives. We want things relative to the 'viewport' (the area of the grid
|
||||
// which is currently shown according to the display offset)
|
||||
let unclamped_start = Point::new(
|
||||
range.start().line + layout.display_offset,
|
||||
range.start().column,
|
||||
);
|
||||
let unclamped_end = Point::new(range.end().line + layout.display_offset, range.end().column);
|
||||
|
||||
// Step 2. Clamp range to viewport, and return None if it doesn't overlap
|
||||
if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.size.num_lines() as i32 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let clamped_start_line = unclamped_start.line.0.max(0) as usize;
|
||||
let clamped_end_line = unclamped_end.line.0.min(layout.size.num_lines() as i32) as usize;
|
||||
//Convert the start of the range to pixels
|
||||
let start_y = origin.y() + clamped_start_line as f32 * layout.size.line_height;
|
||||
|
||||
// Step 3. Expand ranges that cross lines into a collection of single-line ranges.
|
||||
// (also convert to pixels)
|
||||
let mut highlighted_range_lines = Vec::new();
|
||||
for line in clamped_start_line..=clamped_end_line {
|
||||
let mut line_start = 0;
|
||||
let mut line_end = layout.size.columns();
|
||||
|
||||
if line == clamped_start_line {
|
||||
line_start = unclamped_start.column.0 as usize;
|
||||
}
|
||||
if line == clamped_end_line {
|
||||
line_end = unclamped_end.column.0 as usize + 1; //+1 for inclusive
|
||||
}
|
||||
|
||||
highlighted_range_lines.push(HighlightedRangeLine {
|
||||
start_x: origin.x() + line_start as f32 * layout.size.cell_width,
|
||||
end_x: origin.x() + line_end as f32 * layout.size.cell_width,
|
||||
});
|
||||
}
|
||||
|
||||
Some((start_y, highlighted_range_lines))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::time::Duration;
|
||||
use std::{ops::RangeInclusive, time::Duration};
|
||||
|
||||
use alacritty_terminal::term::TermMode;
|
||||
use alacritty_terminal::{index::Point, term::TermMode};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use gpui::{
|
||||
actions,
|
||||
|
@ -8,8 +8,8 @@ use gpui::{
|
|||
geometry::vector::Vector2F,
|
||||
impl_internal_actions,
|
||||
keymap::Keystroke,
|
||||
AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View,
|
||||
ViewContext, ViewHandle,
|
||||
AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task,
|
||||
View, ViewContext, ViewHandle,
|
||||
};
|
||||
use settings::{Settings, TerminalBlink};
|
||||
use smol::Timer;
|
||||
|
@ -40,6 +40,7 @@ actions!(
|
|||
Copy,
|
||||
Paste,
|
||||
ShowCharacterPalette,
|
||||
SearchTest
|
||||
]
|
||||
);
|
||||
impl_internal_actions!(project_panel, [DeployContextMenu]);
|
||||
|
@ -148,7 +149,8 @@ impl TerminalView {
|
|||
if !self
|
||||
.terminal
|
||||
.read(cx)
|
||||
.last_mode
|
||||
.last_content
|
||||
.mode
|
||||
.contains(TermMode::ALT_SCREEN)
|
||||
{
|
||||
cx.show_character_palette();
|
||||
|
@ -176,7 +178,8 @@ impl TerminalView {
|
|||
|| self
|
||||
.terminal
|
||||
.read(cx)
|
||||
.last_mode
|
||||
.last_content
|
||||
.mode
|
||||
.contains(TermMode::ALT_SCREEN)
|
||||
{
|
||||
return true;
|
||||
|
@ -235,6 +238,19 @@ impl TerminalView {
|
|||
.detach();
|
||||
}
|
||||
|
||||
pub fn find_matches(
|
||||
&mut self,
|
||||
query: project::search::SearchQuery,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Vec<RangeInclusive<Point>>> {
|
||||
self.terminal
|
||||
.update(cx, |term, cx| term.find_matches(query, cx))
|
||||
}
|
||||
|
||||
pub fn terminal(&self) -> &ModelHandle<Terminal> {
|
||||
&self.terminal
|
||||
}
|
||||
|
||||
fn next_blink_epoch(&mut self) -> usize {
|
||||
self.blink_epoch += 1;
|
||||
self.blink_epoch
|
||||
|
@ -348,7 +364,8 @@ impl View for TerminalView {
|
|||
if self
|
||||
.terminal
|
||||
.read(cx)
|
||||
.last_mode
|
||||
.last_content
|
||||
.mode
|
||||
.contains(TermMode::ALT_SCREEN)
|
||||
{
|
||||
None
|
||||
|
@ -373,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) {
|
||||
|
|
|
@ -10,38 +10,73 @@ use crate::{Item, ItemHandle, WeakItemHandle};
|
|||
|
||||
#[derive(Debug)]
|
||||
pub enum SearchEvent {
|
||||
ContentsUpdated,
|
||||
SelectionsChanged,
|
||||
MatchesInvalidated,
|
||||
ActiveMatchChanged,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Direction {
|
||||
Prev,
|
||||
Next,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct SearchOptions {
|
||||
pub case: bool,
|
||||
pub word: bool,
|
||||
pub regex: bool,
|
||||
}
|
||||
|
||||
pub trait SearchableItem: Item {
|
||||
type Match: Any + Sync + Send + Clone;
|
||||
|
||||
fn supported_options() -> SearchOptions {
|
||||
SearchOptions {
|
||||
case: true,
|
||||
word: true,
|
||||
regex: true,
|
||||
}
|
||||
}
|
||||
fn to_search_event(event: &Self::Event) -> Option<SearchEvent>;
|
||||
fn clear_highlights(&mut self, cx: &mut ViewContext<Self>);
|
||||
fn highlight_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
|
||||
fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
|
||||
fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
|
||||
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
|
||||
fn select_next_match_in_direction(
|
||||
fn activate_match(
|
||||
&mut self,
|
||||
index: usize,
|
||||
matches: Vec<Self::Match>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
);
|
||||
fn match_index_for_direction(
|
||||
&mut self,
|
||||
matches: &Vec<Self::Match>,
|
||||
mut current_index: usize,
|
||||
direction: Direction,
|
||||
matches: Vec<Self::Match>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
);
|
||||
fn select_match_by_index(
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> usize {
|
||||
match direction {
|
||||
Direction::Prev => {
|
||||
if current_index == 0 {
|
||||
matches.len() - 1
|
||||
} else {
|
||||
current_index - 1
|
||||
}
|
||||
}
|
||||
Direction::Next => {
|
||||
current_index += 1;
|
||||
if current_index == matches.len() {
|
||||
0
|
||||
} else {
|
||||
current_index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn find_matches(
|
||||
&mut self,
|
||||
index: usize,
|
||||
matches: Vec<Self::Match>,
|
||||
query: SearchQuery,
|
||||
cx: &mut ViewContext<Self>,
|
||||
);
|
||||
fn matches(&mut self, query: SearchQuery, cx: &mut ViewContext<Self>)
|
||||
-> Task<Vec<Self::Match>>;
|
||||
) -> Task<Vec<Self::Match>>;
|
||||
fn active_match_index(
|
||||
&mut self,
|
||||
matches: Vec<Self::Match>,
|
||||
|
@ -52,28 +87,29 @@ pub trait SearchableItem: Item {
|
|||
pub trait SearchableItemHandle: ItemHandle {
|
||||
fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle>;
|
||||
fn boxed_clone(&self) -> Box<dyn SearchableItemHandle>;
|
||||
fn supported_options(&self) -> SearchOptions;
|
||||
fn subscribe(
|
||||
&self,
|
||||
cx: &mut MutableAppContext,
|
||||
handler: Box<dyn Fn(SearchEvent, &mut MutableAppContext)>,
|
||||
) -> Subscription;
|
||||
fn clear_highlights(&self, cx: &mut MutableAppContext);
|
||||
fn highlight_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut MutableAppContext);
|
||||
fn clear_matches(&self, cx: &mut MutableAppContext);
|
||||
fn update_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut MutableAppContext);
|
||||
fn query_suggestion(&self, cx: &mut MutableAppContext) -> String;
|
||||
fn select_next_match_in_direction(
|
||||
fn activate_match(
|
||||
&self,
|
||||
index: usize,
|
||||
matches: &Vec<Box<dyn Any + Send>>,
|
||||
cx: &mut MutableAppContext,
|
||||
);
|
||||
fn match_index_for_direction(
|
||||
&self,
|
||||
matches: &Vec<Box<dyn Any + Send>>,
|
||||
current_index: usize,
|
||||
direction: Direction,
|
||||
matches: &Vec<Box<dyn Any + Send>>,
|
||||
cx: &mut MutableAppContext,
|
||||
);
|
||||
fn select_match_by_index(
|
||||
&self,
|
||||
index: usize,
|
||||
matches: &Vec<Box<dyn Any + Send>>,
|
||||
cx: &mut MutableAppContext,
|
||||
);
|
||||
fn matches(
|
||||
) -> usize;
|
||||
fn find_matches(
|
||||
&self,
|
||||
query: SearchQuery,
|
||||
cx: &mut MutableAppContext,
|
||||
|
@ -94,6 +130,10 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
|
|||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn supported_options(&self) -> SearchOptions {
|
||||
T::supported_options()
|
||||
}
|
||||
|
||||
fn subscribe(
|
||||
&self,
|
||||
cx: &mut MutableAppContext,
|
||||
|
@ -106,45 +146,43 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
|
|||
})
|
||||
}
|
||||
|
||||
fn clear_highlights(&self, cx: &mut MutableAppContext) {
|
||||
self.update(cx, |this, cx| this.clear_highlights(cx));
|
||||
fn clear_matches(&self, cx: &mut MutableAppContext) {
|
||||
self.update(cx, |this, cx| this.clear_matches(cx));
|
||||
}
|
||||
fn highlight_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut MutableAppContext) {
|
||||
fn update_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut MutableAppContext) {
|
||||
let matches = downcast_matches(matches);
|
||||
self.update(cx, |this, cx| this.highlight_matches(matches, cx));
|
||||
self.update(cx, |this, cx| this.update_matches(matches, cx));
|
||||
}
|
||||
fn query_suggestion(&self, cx: &mut MutableAppContext) -> String {
|
||||
self.update(cx, |this, cx| this.query_suggestion(cx))
|
||||
}
|
||||
fn select_next_match_in_direction(
|
||||
fn activate_match(
|
||||
&self,
|
||||
index: usize,
|
||||
matches: &Vec<Box<dyn Any + Send>>,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
let matches = downcast_matches(matches);
|
||||
self.update(cx, |this, cx| this.activate_match(index, matches, cx));
|
||||
}
|
||||
fn match_index_for_direction(
|
||||
&self,
|
||||
matches: &Vec<Box<dyn Any + Send>>,
|
||||
current_index: usize,
|
||||
direction: Direction,
|
||||
matches: &Vec<Box<dyn Any + Send>>,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
) -> usize {
|
||||
let matches = downcast_matches(matches);
|
||||
self.update(cx, |this, cx| {
|
||||
this.select_next_match_in_direction(index, direction, matches, cx)
|
||||
});
|
||||
this.match_index_for_direction(&matches, current_index, direction, cx)
|
||||
})
|
||||
}
|
||||
fn select_match_by_index(
|
||||
&self,
|
||||
index: usize,
|
||||
matches: &Vec<Box<dyn Any + Send>>,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
let matches = downcast_matches(matches);
|
||||
self.update(cx, |this, cx| {
|
||||
this.select_match_by_index(index, matches, cx)
|
||||
});
|
||||
}
|
||||
fn matches(
|
||||
fn find_matches(
|
||||
&self,
|
||||
query: SearchQuery,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Vec<Box<dyn Any + Send>>> {
|
||||
let matches = self.update(cx, |this, cx| this.matches(query, cx));
|
||||
let matches = self.update(cx, |this, cx| this.find_matches(query, cx));
|
||||
cx.foreground().spawn(async {
|
||||
let matches = matches.await;
|
||||
matches
|
||||
|
|
1
styles/package-lock.json
generated
1
styles/package-lock.json
generated
|
@ -5,7 +5,6 @@
|
|||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "styles",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
|
Loading…
Reference in a new issue