WIP - move terminal to project as pre-prep for collaboration
This commit is contained in:
parent
7dde54b052
commit
1b8763d0cf
14 changed files with 95 additions and 30 deletions
|
@ -7,17 +7,12 @@ edition = "2021"
|
|||
path = "src/terminal.rs"
|
||||
doctest = false
|
||||
|
||||
|
||||
[dependencies]
|
||||
context_menu = { path = "../context_menu" }
|
||||
editor = { path = "../editor" }
|
||||
language = { path = "../language" }
|
||||
gpui = { path = "../gpui" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
db = { path = "../db" }
|
||||
theme = { path = "../theme" }
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "a51dbe25d67e84d6ed4261e640d3954fbdd9be45" }
|
||||
procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
|
@ -32,13 +27,4 @@ libc = "0.2"
|
|||
anyhow = "1"
|
||||
thiserror = "1.0"
|
||||
lazy_static = "1.4.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
client = { path = "../client", features = ["test-support"]}
|
||||
project = { path = "../project", features = ["test-support"]}
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
rand = "0.8.5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
|
@ -1,23 +0,0 @@
|
|||
Design notes:
|
||||
|
||||
This crate is split into two conceptual halves:
|
||||
- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here.
|
||||
- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.
|
||||
|
||||
ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context.
|
||||
|
||||
The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors.
|
||||
|
||||
#Input
|
||||
|
||||
There are currently many distinct paths for getting keystrokes to the terminal:
|
||||
|
||||
1. Terminal specific characters and bindings. Things like ctrl-a mapping to ASCII control character 1, ANSI escape codes associated with the function keys, etc. These are caught with a raw key-down handler in the element and are processed immediately. This is done with the `try_keystroke()` method on Terminal
|
||||
|
||||
2. GPU Action handlers. GPUI clobbers a few vital keys by adding bindings to them in the global context. These keys are synthesized and then dispatched through the same `try_keystroke()` API as the above mappings
|
||||
|
||||
3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`.
|
||||
|
||||
4. Pasted text has a seperate pathway.
|
||||
|
||||
Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal
|
|
@ -1,96 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Tom Hale, 2016. MIT Licence.
|
||||
# Print out 256 colours, with each number printed in its corresponding colour
|
||||
# See http://askubuntu.com/questions/821157/print-a-256-color-test-pattern-in-the-terminal/821163#821163
|
||||
|
||||
set -eu # Fail on errors or undeclared variables
|
||||
|
||||
printable_colours=256
|
||||
|
||||
# Return a colour that contrasts with the given colour
|
||||
# Bash only does integer division, so keep it integral
|
||||
function contrast_colour {
|
||||
local r g b luminance
|
||||
colour="$1"
|
||||
|
||||
if (( colour < 16 )); then # Initial 16 ANSI colours
|
||||
(( colour == 0 )) && printf "15" || printf "0"
|
||||
return
|
||||
fi
|
||||
|
||||
# Greyscale # rgb_R = rgb_G = rgb_B = (number - 232) * 10 + 8
|
||||
if (( colour > 231 )); then # Greyscale ramp
|
||||
(( colour < 244 )) && printf "15" || printf "0"
|
||||
return
|
||||
fi
|
||||
|
||||
# All other colours:
|
||||
# 6x6x6 colour cube = 16 + 36*R + 6*G + B # Where RGB are [0..5]
|
||||
# See http://stackoverflow.com/a/27165165/5353461
|
||||
|
||||
# r=$(( (colour-16) / 36 ))
|
||||
g=$(( ((colour-16) % 36) / 6 ))
|
||||
# b=$(( (colour-16) % 6 ))
|
||||
|
||||
# If luminance is bright, print number in black, white otherwise.
|
||||
# Green contributes 587/1000 to human perceived luminance - ITU R-REC-BT.601
|
||||
(( g > 2)) && printf "0" || printf "15"
|
||||
return
|
||||
|
||||
# Uncomment the below for more precise luminance calculations
|
||||
|
||||
# # Calculate percieved brightness
|
||||
# # See https://www.w3.org/TR/AERT#color-contrast
|
||||
# # and http://www.itu.int/rec/R-REC-BT.601
|
||||
# # Luminance is in range 0..5000 as each value is 0..5
|
||||
# luminance=$(( (r * 299) + (g * 587) + (b * 114) ))
|
||||
# (( $luminance > 2500 )) && printf "0" || printf "15"
|
||||
}
|
||||
|
||||
# Print a coloured block with the number of that colour
|
||||
function print_colour {
|
||||
local colour="$1" contrast
|
||||
contrast=$(contrast_colour "$1")
|
||||
printf "\e[48;5;%sm" "$colour" # Start block of colour
|
||||
printf "\e[38;5;%sm%3d" "$contrast" "$colour" # In contrast, print number
|
||||
printf "\e[0m " # Reset colour
|
||||
}
|
||||
|
||||
# Starting at $1, print a run of $2 colours
|
||||
function print_run {
|
||||
local i
|
||||
for (( i = "$1"; i < "$1" + "$2" && i < printable_colours; i++ )) do
|
||||
print_colour "$i"
|
||||
done
|
||||
printf " "
|
||||
}
|
||||
|
||||
# Print blocks of colours
|
||||
function print_blocks {
|
||||
local start="$1" i
|
||||
local end="$2" # inclusive
|
||||
local block_cols="$3"
|
||||
local block_rows="$4"
|
||||
local blocks_per_line="$5"
|
||||
local block_length=$((block_cols * block_rows))
|
||||
|
||||
# Print sets of blocks
|
||||
for (( i = start; i <= end; i += (blocks_per_line-1) * block_length )) do
|
||||
printf "\n" # Space before each set of blocks
|
||||
# For each block row
|
||||
for (( row = 0; row < block_rows; row++ )) do
|
||||
# Print block columns for all blocks on the line
|
||||
for (( block = 0; block < blocks_per_line; block++ )) do
|
||||
print_run $(( i + (block * block_length) )) "$block_cols"
|
||||
done
|
||||
(( i += block_cols )) # Prepare to print the next row
|
||||
printf "\n"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
print_run 0 16 # The first 16 colours are spread over the whole spectrum
|
||||
printf "\n"
|
||||
print_blocks 16 231 6 6 3 # 6x6x6 colour cube between 16 and 231 inclusive
|
||||
print_blocks 232 255 12 2 1 # Not 50, but 24 Shades of Grey
|
|
@ -1,19 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Copied from: https://unix.stackexchange.com/a/696756
|
||||
# Based on: https://gist.github.com/XVilka/8346728 and https://unix.stackexchange.com/a/404415/395213
|
||||
|
||||
awk -v term_cols="${width:-$(tput cols || echo 80)}" -v term_lines="${height:-1}" 'BEGIN{
|
||||
s="/\\";
|
||||
total_cols=term_cols*term_lines;
|
||||
for (colnum = 0; colnum<total_cols; colnum++) {
|
||||
r = 255-(colnum*255/total_cols);
|
||||
g = (colnum*510/total_cols);
|
||||
b = (colnum*255/total_cols);
|
||||
if (g>255) g = 510-g;
|
||||
printf "\033[48;2;%d;%d;%dm", r,g,b;
|
||||
printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b;
|
||||
printf "%s\033[0m", substr(s,colnum%2+1,1);
|
||||
if (colnum%term_cols==term_cols) printf "\n";
|
||||
}
|
||||
printf "\n";
|
||||
}'
|
|
@ -2,16 +2,16 @@ use std::path::PathBuf;
|
|||
|
||||
use db::{define_connection, query, sqlez_macros::sql};
|
||||
|
||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||
type ModelId = usize;
|
||||
|
||||
define_connection! {
|
||||
pub static ref TERMINAL_CONNECTION: TerminalDb<WorkspaceDb> =
|
||||
pub static ref TERMINAL_CONNECTION: TerminalDb<()> =
|
||||
&[sql!(
|
||||
CREATE TABLE terminals (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
model_id INTEGER UNIQUE,
|
||||
working_directory BLOB,
|
||||
PRIMARY KEY(workspace_id, item_id),
|
||||
PRIMARY KEY(workspace_id, model_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
@ -23,7 +23,7 @@ impl TerminalDb {
|
|||
pub async fn update_workspace_id(
|
||||
new_id: WorkspaceId,
|
||||
old_id: WorkspaceId,
|
||||
item_id: ItemId
|
||||
item_id: ModelId
|
||||
) -> Result<()> {
|
||||
UPDATE terminals
|
||||
SET workspace_id = ?
|
||||
|
@ -33,7 +33,7 @@ impl TerminalDb {
|
|||
|
||||
query! {
|
||||
pub async fn save_working_directory(
|
||||
item_id: ItemId,
|
||||
item_id: ModelId,
|
||||
workspace_id: WorkspaceId,
|
||||
working_directory: PathBuf
|
||||
) -> Result<()> {
|
||||
|
@ -43,7 +43,7 @@ impl TerminalDb {
|
|||
}
|
||||
|
||||
query! {
|
||||
pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
|
||||
pub fn get_working_directory(item_id: ModelId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
|
||||
SELECT working_directory
|
||||
FROM terminals
|
||||
WHERE item_id = ? AND workspace_id = ?
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
pub mod mappings;
|
||||
mod persistence;
|
||||
pub mod terminal_container_view;
|
||||
pub mod terminal_element;
|
||||
pub mod terminal_view;
|
||||
|
||||
use alacritty_terminal::{
|
||||
ansi::{ClearMode, Handler},
|
||||
|
@ -37,7 +34,6 @@ use persistence::TERMINAL_CONNECTION;
|
|||
use procinfo::LocalProcessInfo;
|
||||
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
|
||||
use util::ResultExt;
|
||||
use workspace::{ItemId, WorkspaceId};
|
||||
|
||||
use std::{
|
||||
cmp::min,
|
||||
|
|
|
@ -1,711 +0,0 @@
|
|||
use crate::persistence::TERMINAL_CONNECTION;
|
||||
use crate::terminal_view::TerminalView;
|
||||
use crate::{Event, TerminalBuilder, TerminalError};
|
||||
|
||||
use alacritty_terminal::index::Point;
|
||||
use dirs::home_dir;
|
||||
use gpui::{
|
||||
actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, Task,
|
||||
View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use util::{truncate_and_trailoff, ResultExt};
|
||||
use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
|
||||
use workspace::{
|
||||
item::{Item, ItemEvent},
|
||||
ToolbarItemLocation, Workspace,
|
||||
};
|
||||
use workspace::{register_deserializable_item, Pane, WorkspaceId};
|
||||
|
||||
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;
|
||||
|
||||
actions!(terminal, [DeployModal]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(TerminalContainer::deploy);
|
||||
|
||||
register_deserializable_item::<TerminalContainer>(cx);
|
||||
}
|
||||
|
||||
//Make terminal view an enum, that can give you views for the error and non-error states
|
||||
//Take away all the result unwrapping in the current TerminalView by making it 'infallible'
|
||||
//Bubble up to deploy(_modal)() calls
|
||||
|
||||
pub enum TerminalContainerContent {
|
||||
Connected(ViewHandle<TerminalView>),
|
||||
Error(ViewHandle<ErrorView>),
|
||||
}
|
||||
|
||||
impl TerminalContainerContent {
|
||||
fn handle(&self) -> AnyViewHandle {
|
||||
match self {
|
||||
Self::Connected(handle) => handle.into(),
|
||||
Self::Error(handle) => handle.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TerminalContainer {
|
||||
pub content: TerminalContainerContent,
|
||||
associated_directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub struct ErrorView {
|
||||
error: TerminalError,
|
||||
}
|
||||
|
||||
impl Entity for TerminalContainer {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl Entity for ErrorView {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl TerminalContainer {
|
||||
///Create a new Terminal in the current working directory or the user's home directory
|
||||
pub fn deploy(
|
||||
workspace: &mut Workspace,
|
||||
_: &workspace::NewTerminal,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let strategy = cx
|
||||
.global::<Settings>()
|
||||
.terminal_overrides
|
||||
.working_directory
|
||||
.clone()
|
||||
.unwrap_or(WorkingDirectory::CurrentProjectDirectory);
|
||||
|
||||
let working_directory = get_working_directory(workspace, cx, strategy);
|
||||
let view = cx.add_view(|cx| {
|
||||
TerminalContainer::new(working_directory, false, workspace.database_id(), cx)
|
||||
});
|
||||
workspace.add_item(Box::new(view), cx);
|
||||
}
|
||||
|
||||
///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices
|
||||
pub fn new(
|
||||
working_directory: Option<PathBuf>,
|
||||
modal: bool,
|
||||
workspace_id: WorkspaceId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let settings = cx.global::<Settings>();
|
||||
let shell = settings.terminal_overrides.shell.clone();
|
||||
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
|
||||
|
||||
//TODO: move this pattern to settings
|
||||
let scroll = settings
|
||||
.terminal_overrides
|
||||
.alternate_scroll
|
||||
.as_ref()
|
||||
.unwrap_or(
|
||||
settings
|
||||
.terminal_defaults
|
||||
.alternate_scroll
|
||||
.as_ref()
|
||||
.unwrap_or_else(|| &AlternateScroll::On),
|
||||
);
|
||||
|
||||
let content = match TerminalBuilder::new(
|
||||
working_directory.clone(),
|
||||
shell,
|
||||
envs,
|
||||
settings.terminal_overrides.blinking.clone(),
|
||||
scroll,
|
||||
cx.window_id(),
|
||||
cx.view_id(),
|
||||
workspace_id,
|
||||
) {
|
||||
Ok(terminal) => {
|
||||
let terminal = cx.add_model(|cx| terminal.subscribe(cx));
|
||||
let view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
|
||||
|
||||
cx.subscribe(&view, |_this, _content, event, cx| cx.emit(*event))
|
||||
.detach();
|
||||
TerminalContainerContent::Connected(view)
|
||||
}
|
||||
Err(error) => {
|
||||
let view = cx.add_view(|_| ErrorView {
|
||||
error: error.downcast::<TerminalError>().unwrap(),
|
||||
});
|
||||
TerminalContainerContent::Error(view)
|
||||
}
|
||||
};
|
||||
|
||||
TerminalContainer {
|
||||
content,
|
||||
associated_directory: working_directory,
|
||||
}
|
||||
}
|
||||
|
||||
fn connected(&self) -> Option<ViewHandle<TerminalView>> {
|
||||
match &self.content {
|
||||
TerminalContainerContent::Connected(vh) => Some(vh.clone()),
|
||||
TerminalContainerContent::Error(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl View for TerminalContainer {
|
||||
fn ui_name() -> &'static str {
|
||||
"Terminal"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
|
||||
match &self.content {
|
||||
TerminalContainerContent::Connected(connected) => ChildView::new(connected, cx),
|
||||
TerminalContainerContent::Error(error) => ChildView::new(error, cx),
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(self.content.handle());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl View for ErrorView {
|
||||
fn ui_name() -> &'static str {
|
||||
"Terminal Error"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
|
||||
let settings = cx.global::<Settings>();
|
||||
let style = TerminalElement::make_text_style(cx.font_cache(), settings);
|
||||
|
||||
//TODO:
|
||||
//We want markdown style highlighting so we can format the program and working directory with ``
|
||||
//We want a max-width of 75% with word-wrap
|
||||
//We want to be able to select the text
|
||||
//Want to be able to scroll if the error message is massive somehow (resiliency)
|
||||
|
||||
let program_text = {
|
||||
match self.error.shell_to_string() {
|
||||
Some(shell_txt) => format!("Shell Program: `{}`", shell_txt),
|
||||
None => "No program specified".to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
let directory_text = {
|
||||
match self.error.directory.as_ref() {
|
||||
Some(path) => format!("Working directory: `{}`", path.to_string_lossy()),
|
||||
None => "No working directory specified".to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
let error_text = self.error.source.to_string();
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Text::new("Failed to open the terminal.".to_string(), style.clone())
|
||||
.contained()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Text::new(program_text, style.clone()).contained().boxed())
|
||||
.with_child(Text::new(directory_text, style.clone()).contained().boxed())
|
||||
.with_child(Text::new(error_text, style).contained().boxed())
|
||||
.aligned()
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for TerminalContainer {
|
||||
fn tab_content(
|
||||
&self,
|
||||
_detail: Option<usize>,
|
||||
tab_theme: &theme::Tab,
|
||||
cx: &gpui::AppContext,
|
||||
) -> ElementBox {
|
||||
let title = match &self.content {
|
||||
TerminalContainerContent::Connected(connected) => connected
|
||||
.read(cx)
|
||||
.handle()
|
||||
.read(cx)
|
||||
.foreground_process_info
|
||||
.as_ref()
|
||||
.map(|fpi| {
|
||||
format!(
|
||||
"{} — {}",
|
||||
truncate_and_trailoff(
|
||||
&fpi.cwd
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
.unwrap_or_default(),
|
||||
25
|
||||
),
|
||||
truncate_and_trailoff(
|
||||
&{
|
||||
format!(
|
||||
"{}{}",
|
||||
fpi.name,
|
||||
if fpi.argv.len() >= 1 {
|
||||
format!(" {}", (&fpi.argv[1..]).join(" "))
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
)
|
||||
},
|
||||
25
|
||||
)
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| "Terminal".to_string()),
|
||||
TerminalContainerContent::Error(_) => "Terminal".to_string(),
|
||||
};
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(title, tab_theme.label.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
workspace_id: WorkspaceId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Self> {
|
||||
//From what I can tell, there's no way to tell the current working
|
||||
//Directory of the terminal from outside the shell. There might be
|
||||
//solutions to this, but they are non-trivial and require more IPC
|
||||
Some(TerminalContainer::new(
|
||||
self.associated_directory.clone(),
|
||||
false,
|
||||
workspace_id,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
fn project_path(&self, _cx: &gpui::AppContext) -> Option<ProjectPath> {
|
||||
None
|
||||
}
|
||||
|
||||
fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
|
||||
SmallVec::new()
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _cx: &gpui::AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn can_save(&self, _cx: &gpui::AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
_project: gpui::ModelHandle<Project>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<gpui::anyhow::Result<()>> {
|
||||
unreachable!("save should not have been called");
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_project: gpui::ModelHandle<Project>,
|
||||
_abs_path: std::path::PathBuf,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<gpui::anyhow::Result<()>> {
|
||||
unreachable!("save_as should not have been called");
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
_project: gpui::ModelHandle<Project>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<gpui::anyhow::Result<()>> {
|
||||
gpui::Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &gpui::AppContext) -> bool {
|
||||
if let TerminalContainerContent::Connected(connected) = &self.content {
|
||||
connected.read(cx).has_bell()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn has_conflict(&self, _cx: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(handle.clone()))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
|
||||
match event {
|
||||
Event::BreadcrumbsChanged => vec![ItemEvent::UpdateBreadcrumbs],
|
||||
Event::TitleChanged | Event::Wakeup => vec![ItemEvent::UpdateTab],
|
||||
Event::CloseTerminal => vec![ItemEvent::CloseItem],
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
if self.connected().is_some() {
|
||||
ToolbarItemLocation::PrimaryLeft { flex: None }
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
|
||||
let connected = self.connected()?;
|
||||
|
||||
Some(vec![Text::new(
|
||||
connected
|
||||
.read(cx)
|
||||
.terminal()
|
||||
.read(cx)
|
||||
.breadcrumb_text
|
||||
.to_string(),
|
||||
theme.breadcrumbs.text.clone(),
|
||||
)
|
||||
.boxed()])
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
Some("Terminal")
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
_project: ModelHandle<Project>,
|
||||
_workspace: WeakViewHandle<Workspace>,
|
||||
workspace_id: workspace::WorkspaceId,
|
||||
item_id: workspace::ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<anyhow::Result<ViewHandle<Self>>> {
|
||||
let working_directory = TERMINAL_CONNECTION.get_working_directory(item_id, workspace_id);
|
||||
Task::ready(Ok(cx.add_view(|cx| {
|
||||
TerminalContainer::new(
|
||||
working_directory.log_err().flatten(),
|
||||
false,
|
||||
workspace_id,
|
||||
cx,
|
||||
)
|
||||
})))
|
||||
}
|
||||
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||
if let Some(connected) = self.connected() {
|
||||
let id = workspace.database_id();
|
||||
let terminal_handle = connected.read(cx).terminal().clone();
|
||||
terminal_handle.update(cx, |terminal, cx| terminal.set_workspace_id(id, cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
let connected = self.connected();
|
||||
// Selection head might have a value if there's a selection that isn't
|
||||
// associated with a match. Therefore, if there are no matches, we should
|
||||
// report None, no matter the state of the terminal
|
||||
let res = if matches.len() > 0 && connected.is_some() {
|
||||
if let Some(selection_head) = connected
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.terminal()
|
||||
.read(cx)
|
||||
.selection_head
|
||||
{
|
||||
// If selection head is contained in a match. Return that match
|
||||
if let Some(ix) = matches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, search_match)| {
|
||||
search_match.contains(&selection_head)
|
||||
|| search_match.start() > &selection_head
|
||||
})
|
||||
.map(|(ix, _)| ix)
|
||||
{
|
||||
Some(ix)
|
||||
} else {
|
||||
// If no selection after selection head, return the last match
|
||||
Some(matches.len().saturating_sub(1))
|
||||
}
|
||||
} else {
|
||||
// Matches found but no active selection, return the first last one (closest to cursor)
|
||||
Some(matches.len().saturating_sub(1))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
///Get's the working directory for the given workspace, respecting the user's settings.
|
||||
pub fn get_working_directory(
|
||||
workspace: &Workspace,
|
||||
cx: &AppContext,
|
||||
strategy: WorkingDirectory,
|
||||
) -> Option<PathBuf> {
|
||||
let res = match strategy {
|
||||
WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx)
|
||||
.or_else(|| first_project_directory(workspace, cx)),
|
||||
WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
|
||||
WorkingDirectory::AlwaysHome => None,
|
||||
WorkingDirectory::Always { directory } => {
|
||||
shellexpand::full(&directory) //TODO handle this better
|
||||
.ok()
|
||||
.map(|dir| Path::new(&dir.to_string()).to_path_buf())
|
||||
.filter(|dir| dir.is_dir())
|
||||
}
|
||||
};
|
||||
res.or_else(home_dir)
|
||||
}
|
||||
|
||||
///Get's the first project's home directory, or the home directory
|
||||
fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
|
||||
workspace
|
||||
.worktrees(cx)
|
||||
.next()
|
||||
.and_then(|worktree_handle| worktree_handle.read(cx).as_local())
|
||||
.and_then(get_path_from_wt)
|
||||
}
|
||||
|
||||
///Gets the intuitively correct working directory from the given workspace
|
||||
///If there is an active entry for this project, returns that entry's worktree root.
|
||||
///If there's no active entry but there is a worktree, returns that worktrees root.
|
||||
///If either of these roots are files, or if there are any other query failures,
|
||||
/// returns the user's home directory
|
||||
fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
|
||||
let project = workspace.project().read(cx);
|
||||
|
||||
project
|
||||
.active_entry()
|
||||
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
|
||||
.or_else(|| workspace.worktrees(cx).next())
|
||||
.and_then(|worktree_handle| worktree_handle.read(cx).as_local())
|
||||
.and_then(get_path_from_wt)
|
||||
}
|
||||
|
||||
fn get_path_from_wt(wt: &LocalWorktree) -> Option<PathBuf> {
|
||||
wt.root_entry()
|
||||
.filter(|re| re.is_dir())
|
||||
.map(|_| wt.abs_path().to_path_buf())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::tests::terminal_test_context::TerminalTestContext;
|
||||
|
||||
///Working directory calculation tests
|
||||
|
||||
///No Worktrees in project -> home_dir()
|
||||
#[gpui::test]
|
||||
async fn no_worktree(cx: &mut TestAppContext) {
|
||||
//Setup variables
|
||||
let mut cx = TerminalTestContext::new(cx);
|
||||
let (project, workspace) = cx.blank_workspace().await;
|
||||
//Test
|
||||
cx.cx.read(|cx| {
|
||||
let workspace = workspace.read(cx);
|
||||
let active_entry = project.read(cx).active_entry();
|
||||
|
||||
//Make sure enviroment is as expeted
|
||||
assert!(active_entry.is_none());
|
||||
assert!(workspace.worktrees(cx).next().is_none());
|
||||
|
||||
let res = current_project_directory(workspace, cx);
|
||||
assert_eq!(res, None);
|
||||
let res = first_project_directory(workspace, cx);
|
||||
assert_eq!(res, None);
|
||||
});
|
||||
}
|
||||
|
||||
///No active entry, but a worktree, worktree is a file -> home_dir()
|
||||
#[gpui::test]
|
||||
async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
|
||||
//Setup variables
|
||||
|
||||
let mut cx = TerminalTestContext::new(cx);
|
||||
let (project, workspace) = cx.blank_workspace().await;
|
||||
cx.create_file_wt(project.clone(), "/root.txt").await;
|
||||
|
||||
cx.cx.read(|cx| {
|
||||
let workspace = workspace.read(cx);
|
||||
let active_entry = project.read(cx).active_entry();
|
||||
|
||||
//Make sure enviroment is as expeted
|
||||
assert!(active_entry.is_none());
|
||||
assert!(workspace.worktrees(cx).next().is_some());
|
||||
|
||||
let res = current_project_directory(workspace, cx);
|
||||
assert_eq!(res, None);
|
||||
let res = first_project_directory(workspace, cx);
|
||||
assert_eq!(res, None);
|
||||
});
|
||||
}
|
||||
|
||||
//No active entry, but a worktree, worktree is a folder -> worktree_folder
|
||||
#[gpui::test]
|
||||
async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
|
||||
//Setup variables
|
||||
let mut cx = TerminalTestContext::new(cx);
|
||||
let (project, workspace) = cx.blank_workspace().await;
|
||||
let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root/").await;
|
||||
|
||||
//Test
|
||||
cx.cx.update(|cx| {
|
||||
let workspace = workspace.read(cx);
|
||||
let active_entry = project.read(cx).active_entry();
|
||||
|
||||
assert!(active_entry.is_none());
|
||||
assert!(workspace.worktrees(cx).next().is_some());
|
||||
|
||||
let res = current_project_directory(workspace, cx);
|
||||
assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
|
||||
let res = first_project_directory(workspace, cx);
|
||||
assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
|
||||
});
|
||||
}
|
||||
|
||||
//Active entry with a work tree, worktree is a file -> home_dir()
|
||||
#[gpui::test]
|
||||
async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
|
||||
//Setup variables
|
||||
let mut cx = TerminalTestContext::new(cx);
|
||||
let (project, workspace) = cx.blank_workspace().await;
|
||||
let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await;
|
||||
let (wt2, entry2) = cx.create_file_wt(project.clone(), "/root2.txt").await;
|
||||
cx.insert_active_entry_for(wt2, entry2, project.clone());
|
||||
|
||||
//Test
|
||||
cx.cx.update(|cx| {
|
||||
let workspace = workspace.read(cx);
|
||||
let active_entry = project.read(cx).active_entry();
|
||||
|
||||
assert!(active_entry.is_some());
|
||||
|
||||
let res = current_project_directory(workspace, cx);
|
||||
assert_eq!(res, None);
|
||||
let res = first_project_directory(workspace, cx);
|
||||
assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
|
||||
});
|
||||
}
|
||||
|
||||
//Active entry, with a worktree, worktree is a folder -> worktree_folder
|
||||
#[gpui::test]
|
||||
async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
|
||||
//Setup variables
|
||||
let mut cx = TerminalTestContext::new(cx);
|
||||
let (project, workspace) = cx.blank_workspace().await;
|
||||
let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await;
|
||||
let (wt2, entry2) = cx.create_folder_wt(project.clone(), "/root2/").await;
|
||||
cx.insert_active_entry_for(wt2, entry2, project.clone());
|
||||
|
||||
//Test
|
||||
cx.cx.update(|cx| {
|
||||
let workspace = workspace.read(cx);
|
||||
let active_entry = project.read(cx).active_entry();
|
||||
|
||||
assert!(active_entry.is_some());
|
||||
|
||||
let res = current_project_directory(workspace, cx);
|
||||
assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
|
||||
let res = first_project_directory(workspace, cx);
|
||||
assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,912 +0,0 @@
|
|||
use alacritty_terminal::{
|
||||
ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
|
||||
grid::Dimensions,
|
||||
index::Point,
|
||||
term::{cell::Flags, TermMode},
|
||||
};
|
||||
use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::{Empty, Overlay},
|
||||
fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight},
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
serde_json::json,
|
||||
text_layout::{Line, RunStyle},
|
||||
Element, ElementBox, EventContext, FontCache, ModelContext, MouseButton, MouseRegion,
|
||||
PaintContext, Quad, SizeConstraint, TextLayoutCache, WeakModelHandle, WeakViewHandle,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::CursorShape;
|
||||
use ordered_float::OrderedFloat;
|
||||
use settings::Settings;
|
||||
use theme::TerminalStyle;
|
||||
use util::ResultExt;
|
||||
|
||||
use std::{fmt::Debug, ops::RangeInclusive};
|
||||
use std::{mem, ops::Range};
|
||||
|
||||
use crate::{
|
||||
mappings::colors::convert_color,
|
||||
terminal_view::{DeployContextMenu, TerminalView},
|
||||
IndexedCell, Terminal, TerminalContent, TerminalSize,
|
||||
};
|
||||
|
||||
///The information generated during layout that is nescessary for painting
|
||||
pub struct LayoutState {
|
||||
cells: Vec<LayoutCell>,
|
||||
rects: Vec<LayoutRect>,
|
||||
relative_highlighted_ranges: Vec<(RangeInclusive<Point>, Color)>,
|
||||
cursor: Option<Cursor>,
|
||||
background_color: Color,
|
||||
size: TerminalSize,
|
||||
mode: TermMode,
|
||||
display_offset: usize,
|
||||
hyperlink_tooltip: Option<ElementBox>,
|
||||
}
|
||||
|
||||
///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
|
||||
struct DisplayCursor {
|
||||
line: i32,
|
||||
col: usize,
|
||||
}
|
||||
|
||||
impl DisplayCursor {
|
||||
fn from(cursor_point: Point, display_offset: usize) -> Self {
|
||||
Self {
|
||||
line: cursor_point.line.0 + display_offset as i32,
|
||||
col: cursor_point.column.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn line(&self) -> i32 {
|
||||
self.line
|
||||
}
|
||||
|
||||
pub fn col(&self) -> usize {
|
||||
self.col
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct LayoutCell {
|
||||
point: Point<i32, i32>,
|
||||
text: Line,
|
||||
}
|
||||
|
||||
impl LayoutCell {
|
||||
fn new(point: Point<i32, i32>, text: Line) -> LayoutCell {
|
||||
LayoutCell { point, text }
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&self,
|
||||
origin: Vector2F,
|
||||
layout: &LayoutState,
|
||||
visible_bounds: RectF,
|
||||
cx: &mut PaintContext,
|
||||
) {
|
||||
let pos = {
|
||||
let point = self.point;
|
||||
vec2f(
|
||||
(origin.x() + point.column as f32 * layout.size.cell_width).floor(),
|
||||
origin.y() + point.line as f32 * layout.size.line_height,
|
||||
)
|
||||
};
|
||||
|
||||
self.text
|
||||
.paint(pos, visible_bounds, layout.size.line_height, cx);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct LayoutRect {
|
||||
point: Point<i32, i32>,
|
||||
num_of_cells: usize,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl LayoutRect {
|
||||
fn new(point: Point<i32, i32>, num_of_cells: usize, color: Color) -> LayoutRect {
|
||||
LayoutRect {
|
||||
point,
|
||||
num_of_cells,
|
||||
color,
|
||||
}
|
||||
}
|
||||
|
||||
fn extend(&self) -> Self {
|
||||
LayoutRect {
|
||||
point: self.point,
|
||||
num_of_cells: self.num_of_cells + 1,
|
||||
color: self.color,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&self, origin: Vector2F, layout: &LayoutState, cx: &mut PaintContext) {
|
||||
let position = {
|
||||
let point = self.point;
|
||||
vec2f(
|
||||
(origin.x() + point.column as f32 * layout.size.cell_width).floor(),
|
||||
origin.y() + point.line as f32 * layout.size.line_height,
|
||||
)
|
||||
};
|
||||
let size = vec2f(
|
||||
(layout.size.cell_width * self.num_of_cells as f32).ceil(),
|
||||
layout.size.line_height,
|
||||
);
|
||||
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: RectF::new(position, size),
|
||||
background: Some(self.color),
|
||||
border: Default::default(),
|
||||
corner_radius: 0.,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
///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 {
|
||||
terminal: WeakModelHandle<Terminal>,
|
||||
view: WeakViewHandle<TerminalView>,
|
||||
focused: bool,
|
||||
cursor_visible: bool,
|
||||
}
|
||||
|
||||
impl TerminalElement {
|
||||
pub fn new(
|
||||
view: WeakViewHandle<TerminalView>,
|
||||
terminal: WeakModelHandle<Terminal>,
|
||||
focused: bool,
|
||||
cursor_visible: bool,
|
||||
) -> TerminalElement {
|
||||
TerminalElement {
|
||||
view,
|
||||
terminal,
|
||||
focused,
|
||||
cursor_visible,
|
||||
}
|
||||
}
|
||||
|
||||
//Vec<Range<Point>> -> Clip out the parts of the ranges
|
||||
|
||||
fn layout_grid(
|
||||
grid: &Vec<IndexedCell>,
|
||||
text_style: &TextStyle,
|
||||
terminal_theme: &TerminalStyle,
|
||||
text_layout_cache: &TextLayoutCache,
|
||||
font_cache: &FontCache,
|
||||
hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
|
||||
) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
|
||||
let mut cells = vec![];
|
||||
let mut rects = vec![];
|
||||
|
||||
let mut cur_rect: Option<LayoutRect> = None;
|
||||
let mut cur_alac_color = None;
|
||||
|
||||
let linegroups = grid.into_iter().group_by(|i| i.point.line);
|
||||
for (line_index, (_, line)) in linegroups.into_iter().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);
|
||||
}
|
||||
|
||||
//Expand background rect range
|
||||
{
|
||||
if matches!(bg, Named(NamedColor::Background)) {
|
||||
//Continue to next cell, resetting variables if nescessary
|
||||
cur_alac_color = None;
|
||||
if let Some(rect) = cur_rect {
|
||||
rects.push(rect);
|
||||
cur_rect = None
|
||||
}
|
||||
} else {
|
||||
match cur_alac_color {
|
||||
Some(cur_color) => {
|
||||
if bg == cur_color {
|
||||
cur_rect = cur_rect.take().map(|rect| rect.extend());
|
||||
} else {
|
||||
cur_alac_color = Some(bg);
|
||||
if cur_rect.is_some() {
|
||||
rects.push(cur_rect.take().unwrap());
|
||||
}
|
||||
cur_rect = Some(LayoutRect::new(
|
||||
Point::new(line_index as i32, cell.point.column.0 as i32),
|
||||
1,
|
||||
convert_color(&bg, &terminal_theme),
|
||||
));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
cur_alac_color = Some(bg);
|
||||
cur_rect = Some(LayoutRect::new(
|
||||
Point::new(line_index as i32, cell.point.column.0 as i32),
|
||||
1,
|
||||
convert_color(&bg, &terminal_theme),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Layout current cell text
|
||||
{
|
||||
let cell_text = &cell.c.to_string();
|
||||
if !is_blank(&cell) {
|
||||
let cell_style = TerminalElement::cell_style(
|
||||
&cell,
|
||||
fg,
|
||||
terminal_theme,
|
||||
text_style,
|
||||
font_cache,
|
||||
hyperlink,
|
||||
);
|
||||
|
||||
let layout_cell = text_layout_cache.layout_str(
|
||||
cell_text,
|
||||
text_style.font_size,
|
||||
&[(cell_text.len(), cell_style)],
|
||||
);
|
||||
|
||||
cells.push(LayoutCell::new(
|
||||
Point::new(line_index as i32, cell.point.column.0 as i32),
|
||||
layout_cell,
|
||||
))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if cur_rect.is_some() {
|
||||
rects.push(cur_rect.take().unwrap());
|
||||
}
|
||||
}
|
||||
(cells, rects)
|
||||
}
|
||||
|
||||
// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
|
||||
// the same position for sequential indexes. Use em_width instead
|
||||
fn shape_cursor(
|
||||
cursor_point: DisplayCursor,
|
||||
size: TerminalSize,
|
||||
text_fragment: &Line,
|
||||
) -> Option<(Vector2F, f32)> {
|
||||
if cursor_point.line() < size.total_lines() as i32 {
|
||||
let cursor_width = if text_fragment.width() == 0. {
|
||||
size.cell_width()
|
||||
} else {
|
||||
text_fragment.width()
|
||||
};
|
||||
|
||||
//Cursor should always surround as much of the text as possible,
|
||||
//hence when on pixel boundaries round the origin down and the width up
|
||||
Some((
|
||||
vec2f(
|
||||
(cursor_point.col() as f32 * size.cell_width()).floor(),
|
||||
(cursor_point.line() as f32 * size.line_height()).floor(),
|
||||
),
|
||||
cursor_width.ceil(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
///Convert the Alacritty cell styles to GPUI text styles and background color
|
||||
fn cell_style(
|
||||
indexed: &IndexedCell,
|
||||
fg: AnsiColor,
|
||||
style: &TerminalStyle,
|
||||
text_style: &TextStyle,
|
||||
font_cache: &FontCache,
|
||||
hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
|
||||
) -> RunStyle {
|
||||
let flags = indexed.cell.flags;
|
||||
let fg = convert_color(&fg, &style);
|
||||
|
||||
let mut underline = flags
|
||||
.intersects(Flags::ALL_UNDERLINES)
|
||||
.then(|| Underline {
|
||||
color: Some(fg),
|
||||
squiggly: flags.contains(Flags::UNDERCURL),
|
||||
thickness: OrderedFloat(1.),
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if indexed.cell.hyperlink().is_some() {
|
||||
if underline.thickness == OrderedFloat(0.) {
|
||||
underline.thickness = OrderedFloat(1.);
|
||||
}
|
||||
}
|
||||
|
||||
let mut properties = Properties::new();
|
||||
if indexed.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) {
|
||||
properties = *properties.weight(Weight::BOLD);
|
||||
}
|
||||
if indexed.flags.intersects(Flags::ITALIC) {
|
||||
properties = *properties.style(Italic);
|
||||
}
|
||||
|
||||
let font_id = font_cache
|
||||
.select_font(text_style.font_family_id, &properties)
|
||||
.unwrap_or(text_style.font_id);
|
||||
|
||||
let mut result = RunStyle {
|
||||
color: fg,
|
||||
font_id,
|
||||
underline,
|
||||
};
|
||||
|
||||
if let Some((style, range)) = hyperlink {
|
||||
if range.contains(&indexed.point) {
|
||||
if let Some(underline) = style.underline {
|
||||
result.underline = underline;
|
||||
}
|
||||
|
||||
if let Some(color) = style.color {
|
||||
result.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn generic_button_handler<E>(
|
||||
connection: WeakModelHandle<Terminal>,
|
||||
origin: Vector2F,
|
||||
f: impl Fn(&mut Terminal, Vector2F, E, &mut ModelContext<Terminal>),
|
||||
) -> impl Fn(E, &mut EventContext) {
|
||||
move |event, cx| {
|
||||
cx.focus_parent_view();
|
||||
if let Some(conn_handle) = connection.upgrade(cx.app) {
|
||||
conn_handle.update(cx.app, |terminal, cx| {
|
||||
f(terminal, origin, event, cx);
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn attach_mouse_handlers(
|
||||
&self,
|
||||
origin: Vector2F,
|
||||
view_id: usize,
|
||||
visible_bounds: RectF,
|
||||
mode: TermMode,
|
||||
cx: &mut PaintContext,
|
||||
) {
|
||||
let connection = self.terminal;
|
||||
|
||||
let mut region = MouseRegion::new::<Self>(view_id, 0, visible_bounds);
|
||||
|
||||
// Terminal Emulator controlled behavior:
|
||||
region = region
|
||||
// Start selections
|
||||
.on_down(
|
||||
MouseButton::Left,
|
||||
TerminalElement::generic_button_handler(
|
||||
connection,
|
||||
origin,
|
||||
move |terminal, origin, e, _cx| {
|
||||
terminal.mouse_down(&e, origin);
|
||||
},
|
||||
),
|
||||
)
|
||||
// Update drag selections
|
||||
.on_drag(MouseButton::Left, move |event, cx| {
|
||||
if cx.is_parent_view_focused() {
|
||||
if let Some(conn_handle) = connection.upgrade(cx.app) {
|
||||
conn_handle.update(cx.app, |terminal, cx| {
|
||||
terminal.mouse_drag(event, origin);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
// Copy on up behavior
|
||||
.on_up(
|
||||
MouseButton::Left,
|
||||
TerminalElement::generic_button_handler(
|
||||
connection,
|
||||
origin,
|
||||
move |terminal, origin, e, cx| {
|
||||
terminal.mouse_up(&e, origin, cx);
|
||||
},
|
||||
),
|
||||
)
|
||||
// Context menu
|
||||
.on_click(MouseButton::Right, move |e, cx| {
|
||||
let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx.app) {
|
||||
conn_handle.update(cx.app, |terminal, _cx| terminal.mouse_mode(e.shift))
|
||||
} else {
|
||||
// If we can't get the model handle, probably can't deploy the context menu
|
||||
true
|
||||
};
|
||||
if !mouse_mode {
|
||||
cx.dispatch_action(DeployContextMenu {
|
||||
position: e.position,
|
||||
});
|
||||
}
|
||||
})
|
||||
.on_move(move |event, cx| {
|
||||
if cx.is_parent_view_focused() {
|
||||
if let Some(conn_handle) = connection.upgrade(cx.app) {
|
||||
conn_handle.update(cx.app, |terminal, cx| {
|
||||
terminal.mouse_move(&event, origin);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_scroll(move |event, cx| {
|
||||
// cx.focus_parent_view();
|
||||
if let Some(conn_handle) = connection.upgrade(cx.app) {
|
||||
conn_handle.update(cx.app, |terminal, cx| {
|
||||
terminal.scroll_wheel(event, origin);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse mode handlers:
|
||||
// All mouse modes need the extra click handlers
|
||||
if mode.intersects(TermMode::MOUSE_MODE) {
|
||||
region = region
|
||||
.on_down(
|
||||
MouseButton::Right,
|
||||
TerminalElement::generic_button_handler(
|
||||
connection,
|
||||
origin,
|
||||
move |terminal, origin, e, _cx| {
|
||||
terminal.mouse_down(&e, origin);
|
||||
},
|
||||
),
|
||||
)
|
||||
.on_down(
|
||||
MouseButton::Middle,
|
||||
TerminalElement::generic_button_handler(
|
||||
connection,
|
||||
origin,
|
||||
move |terminal, origin, e, _cx| {
|
||||
terminal.mouse_down(&e, origin);
|
||||
},
|
||||
),
|
||||
)
|
||||
.on_up(
|
||||
MouseButton::Right,
|
||||
TerminalElement::generic_button_handler(
|
||||
connection,
|
||||
origin,
|
||||
move |terminal, origin, e, cx| {
|
||||
terminal.mouse_up(&e, origin, cx);
|
||||
},
|
||||
),
|
||||
)
|
||||
.on_up(
|
||||
MouseButton::Middle,
|
||||
TerminalElement::generic_button_handler(
|
||||
connection,
|
||||
origin,
|
||||
move |terminal, origin, e, cx| {
|
||||
terminal.mouse_up(&e, origin, cx);
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
cx.scene.push_mouse_region(region);
|
||||
}
|
||||
|
||||
///Configures a text style from the current settings.
|
||||
pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
|
||||
// Pull the font family from settings properly overriding
|
||||
let family_id = settings
|
||||
.terminal_overrides
|
||||
.font_family
|
||||
.as_ref()
|
||||
.or(settings.terminal_defaults.font_family.as_ref())
|
||||
.and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
|
||||
.unwrap_or(settings.buffer_font_family);
|
||||
|
||||
let font_size = settings
|
||||
.terminal_overrides
|
||||
.font_size
|
||||
.or(settings.terminal_defaults.font_size)
|
||||
.unwrap_or(settings.buffer_font_size);
|
||||
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
|
||||
TextStyle {
|
||||
color: settings.theme.editor.text_color,
|
||||
font_family_id: family_id,
|
||||
font_family_name: font_cache.family_name(family_id).unwrap(),
|
||||
font_id,
|
||||
font_size,
|
||||
font_properties: Default::default(),
|
||||
underline: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for TerminalElement {
|
||||
type LayoutState = LayoutState;
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: gpui::SizeConstraint,
|
||||
cx: &mut gpui::LayoutContext,
|
||||
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
|
||||
let settings = cx.global::<Settings>();
|
||||
let font_cache = cx.font_cache();
|
||||
|
||||
//Setup layout information
|
||||
let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
|
||||
let link_style = settings.theme.editor.link_definition;
|
||||
let tooltip_style = settings.theme.tooltip.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 = terminal_theme.background;
|
||||
let terminal_handle = self.terminal.upgrade(cx).unwrap();
|
||||
|
||||
let last_hovered_hyperlink = terminal_handle.update(cx.app, |terminal, cx| {
|
||||
terminal.set_size(dimensions);
|
||||
terminal.try_sync(cx);
|
||||
terminal.last_content.last_hovered_hyperlink.clone()
|
||||
});
|
||||
|
||||
let view_handle = self.view.clone();
|
||||
let hyperlink_tooltip = last_hovered_hyperlink.and_then(|(uri, _, id)| {
|
||||
// last_mouse.and_then(|_last_mouse| {
|
||||
view_handle.upgrade(cx).map(|handle| {
|
||||
let mut tooltip = cx.render(&handle, |_, cx| {
|
||||
Overlay::new(
|
||||
Empty::new()
|
||||
.contained()
|
||||
.constrained()
|
||||
.with_width(dimensions.width())
|
||||
.with_height(dimensions.height())
|
||||
.with_tooltip::<TerminalElement, _>(id, uri, None, tooltip_style, cx)
|
||||
.boxed(),
|
||||
)
|
||||
.with_position_mode(gpui::elements::OverlayPositionMode::Local)
|
||||
.boxed()
|
||||
});
|
||||
|
||||
tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
|
||||
tooltip
|
||||
})
|
||||
// })
|
||||
});
|
||||
|
||||
let TerminalContent {
|
||||
cells,
|
||||
mode,
|
||||
display_offset,
|
||||
cursor_char,
|
||||
selection,
|
||||
cursor,
|
||||
last_hovered_hyperlink,
|
||||
..
|
||||
} = { &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(),
|
||||
last_hovered_hyperlink
|
||||
.as_ref()
|
||||
.map(|(_, range, _)| (link_style, range)),
|
||||
);
|
||||
|
||||
//Layout cursor. Rectangle is used for IME, so we should lay it out even
|
||||
//if we don't end up showing it.
|
||||
let cursor = if let AlacCursorShape::Hidden = cursor.shape {
|
||||
None
|
||||
} else {
|
||||
let cursor_point = DisplayCursor::from(cursor.point, *display_offset);
|
||||
let cursor_text = {
|
||||
let str_trxt = cursor_char.to_string();
|
||||
|
||||
let color = if self.focused {
|
||||
terminal_theme.background
|
||||
} else {
|
||||
terminal_theme.foreground
|
||||
};
|
||||
|
||||
cx.text_layout_cache.layout_str(
|
||||
&str_trxt,
|
||||
text_style.font_size,
|
||||
&[(
|
||||
str_trxt.len(),
|
||||
RunStyle {
|
||||
font_id: text_style.font_id,
|
||||
color,
|
||||
underline: Default::default(),
|
||||
},
|
||||
)],
|
||||
)
|
||||
};
|
||||
|
||||
let focused = self.focused;
|
||||
TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
|
||||
move |(cursor_position, block_width)| {
|
||||
let (shape, text) = match cursor.shape {
|
||||
AlacCursorShape::Block if !focused => (CursorShape::Hollow, None),
|
||||
AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)),
|
||||
AlacCursorShape::Underline => (CursorShape::Underscore, None),
|
||||
AlacCursorShape::Beam => (CursorShape::Bar, None),
|
||||
AlacCursorShape::HollowBlock => (CursorShape::Hollow, None),
|
||||
//This case is handled in the if wrapping the whole cursor layout
|
||||
AlacCursorShape::Hidden => unreachable!(),
|
||||
};
|
||||
|
||||
Cursor::new(
|
||||
cursor_position,
|
||||
block_width,
|
||||
dimensions.line_height,
|
||||
terminal_theme.cursor,
|
||||
shape,
|
||||
text,
|
||||
)
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
//Done!
|
||||
(
|
||||
constraint.max,
|
||||
LayoutState {
|
||||
cells,
|
||||
cursor,
|
||||
background_color,
|
||||
size: dimensions,
|
||||
rects,
|
||||
relative_highlighted_ranges,
|
||||
mode: *mode,
|
||||
display_offset: *display_offset,
|
||||
hyperlink_tooltip,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: gpui::geometry::rect::RectF,
|
||||
visible_bounds: gpui::geometry::rect::RectF,
|
||||
layout: &mut Self::LayoutState,
|
||||
cx: &mut gpui::PaintContext,
|
||||
) -> Self::PaintState {
|
||||
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||
|
||||
//Setup element stuff
|
||||
let clip_bounds = Some(visible_bounds);
|
||||
|
||||
cx.paint_layer(clip_bounds, |cx| {
|
||||
let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
|
||||
|
||||
//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: if layout.hyperlink_tooltip.is_some() {
|
||||
gpui::CursorStyle::PointingHand
|
||||
} else {
|
||||
gpui::CursorStyle::IBeam
|
||||
},
|
||||
});
|
||||
|
||||
cx.paint_layer(clip_bounds, |cx| {
|
||||
//Start with a background color
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: RectF::new(bounds.origin(), bounds.size()),
|
||||
background: Some(layout.background_color),
|
||||
border: Default::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
|
||||
for rect in &layout.rects {
|
||||
rect.paint(origin, layout, cx)
|
||||
}
|
||||
});
|
||||
|
||||
//Draw Highlighted Backgrounds
|
||||
cx.paint_layer(clip_bounds, |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.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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//Draw the text cells
|
||||
cx.paint_layer(clip_bounds, |cx| {
|
||||
for cell in &layout.cells {
|
||||
cell.paint(origin, layout, visible_bounds, cx);
|
||||
}
|
||||
});
|
||||
|
||||
//Draw cursor
|
||||
if self.cursor_visible {
|
||||
if let Some(cursor) = &layout.cursor {
|
||||
cx.paint_layer(clip_bounds, |cx| {
|
||||
cursor.paint(origin, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(element) = &mut layout.hyperlink_tooltip {
|
||||
element.paint(origin, visible_bounds, cx)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Option<&dyn std::any::Any> {
|
||||
None
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
_bounds: gpui::geometry::rect::RectF,
|
||||
_layout: &Self::LayoutState,
|
||||
_paint: &Self::PaintState,
|
||||
_cx: &gpui::DebugContext,
|
||||
) -> gpui::serde_json::Value {
|
||||
json!({
|
||||
"type": "TerminalElement",
|
||||
})
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
_: Range<usize>,
|
||||
bounds: RectF,
|
||||
_: RectF,
|
||||
layout: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &gpui::MeasurementContext,
|
||||
) -> Option<RectF> {
|
||||
// Use the same origin that's passed to `Cursor::paint` in the paint
|
||||
// method bove.
|
||||
let mut origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
|
||||
|
||||
// TODO - Why is it necessary to move downward one line to get correct
|
||||
// positioning? I would think that we'd want the same rect that is
|
||||
// painted for the cursor.
|
||||
origin += vec2f(0., layout.size.line_height);
|
||||
|
||||
Some(layout.cursor.as_ref()?.bounding_rect(origin))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_blank(cell: &IndexedCell) -> bool {
|
||||
if cell.c != ' ' {
|
||||
return false;
|
||||
}
|
||||
|
||||
if cell.bg != AnsiColor::Named(NamedColor::Background) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if cell.hyperlink().is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if cell
|
||||
.flags
|
||||
.intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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:
|
||||
//-2,0 -2,1...
|
||||
//--- Viewport top
|
||||
//-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,471 +0,0 @@
|
|||
use std::{ops::RangeInclusive, time::Duration};
|
||||
|
||||
use alacritty_terminal::{index::Point, term::TermMode};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{AnchorCorner, ChildView, ParentElement, Stack},
|
||||
geometry::vector::Vector2F,
|
||||
impl_actions, impl_internal_actions,
|
||||
keymap::Keystroke,
|
||||
AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task,
|
||||
View, ViewContext, ViewHandle,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use settings::{Settings, TerminalBlink};
|
||||
use smol::Timer;
|
||||
use util::ResultExt;
|
||||
use workspace::pane;
|
||||
|
||||
use crate::{terminal_element::TerminalElement, Event, Terminal};
|
||||
|
||||
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
|
||||
///Event to transmit the scroll from the element to the view
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ScrollTerminal(pub i32);
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct DeployContextMenu {
|
||||
pub position: Vector2F,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct SendText(String);
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct SendKeystroke(String);
|
||||
|
||||
actions!(
|
||||
terminal,
|
||||
[Clear, Copy, Paste, ShowCharacterPalette, SearchTest]
|
||||
);
|
||||
|
||||
impl_actions!(terminal, [SendText, SendKeystroke]);
|
||||
|
||||
impl_internal_actions!(project_panel, [DeployContextMenu]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
//Useful terminal views
|
||||
cx.add_action(TerminalView::send_text);
|
||||
cx.add_action(TerminalView::send_keystroke);
|
||||
cx.add_action(TerminalView::deploy_context_menu);
|
||||
cx.add_action(TerminalView::copy);
|
||||
cx.add_action(TerminalView::paste);
|
||||
cx.add_action(TerminalView::clear);
|
||||
cx.add_action(TerminalView::show_character_palette);
|
||||
}
|
||||
|
||||
///A terminal view, maintains the PTY's file handles and communicates with the terminal
|
||||
pub struct TerminalView {
|
||||
terminal: ModelHandle<Terminal>,
|
||||
has_new_content: bool,
|
||||
//Currently using iTerm bell, show bell emoji in tab until input is received
|
||||
has_bell: bool,
|
||||
// Only for styling purposes. Doesn't effect behavior
|
||||
modal: bool,
|
||||
context_menu: ViewHandle<ContextMenu>,
|
||||
blink_state: bool,
|
||||
blinking_on: bool,
|
||||
blinking_paused: bool,
|
||||
blink_epoch: usize,
|
||||
}
|
||||
|
||||
impl Entity for TerminalView {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl TerminalView {
|
||||
pub fn from_terminal(
|
||||
terminal: ModelHandle<Terminal>,
|
||||
modal: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
|
||||
cx.subscribe(&terminal, |this, _, event, cx| match event {
|
||||
Event::Wakeup => {
|
||||
if !cx.is_self_focused() {
|
||||
this.has_new_content = true;
|
||||
cx.notify();
|
||||
}
|
||||
cx.emit(Event::Wakeup);
|
||||
}
|
||||
Event::Bell => {
|
||||
this.has_bell = true;
|
||||
cx.emit(Event::Wakeup);
|
||||
}
|
||||
Event::BlinkChanged => this.blinking_on = !this.blinking_on,
|
||||
_ => cx.emit(*event),
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
terminal,
|
||||
has_new_content: true,
|
||||
has_bell: false,
|
||||
modal,
|
||||
context_menu: cx.add_view(ContextMenu::new),
|
||||
blink_state: true,
|
||||
blinking_on: false,
|
||||
blinking_paused: false,
|
||||
blink_epoch: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(&self) -> ModelHandle<Terminal> {
|
||||
self.terminal.clone()
|
||||
}
|
||||
|
||||
pub fn has_new_content(&self) -> bool {
|
||||
self.has_new_content
|
||||
}
|
||||
|
||||
pub fn has_bell(&self) -> bool {
|
||||
self.has_bell
|
||||
}
|
||||
|
||||
pub fn clear_bel(&mut self, cx: &mut ViewContext<TerminalView>) {
|
||||
self.has_bell = false;
|
||||
cx.emit(Event::Wakeup);
|
||||
}
|
||||
|
||||
pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
|
||||
let menu_entries = vec![
|
||||
ContextMenuItem::item("Clear", Clear),
|
||||
ContextMenuItem::item("Close", pane::CloseActiveItem),
|
||||
];
|
||||
|
||||
self.context_menu.update(cx, |menu, cx| {
|
||||
menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx)
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
|
||||
if !self
|
||||
.terminal
|
||||
.read(cx)
|
||||
.last_content
|
||||
.mode
|
||||
.contains(TermMode::ALT_SCREEN)
|
||||
{
|
||||
cx.show_character_palette();
|
||||
} else {
|
||||
self.terminal.update(cx, |term, cx| {
|
||||
term.try_keystroke(
|
||||
&Keystroke::parse("ctrl-cmd-space").unwrap(),
|
||||
cx.global::<Settings>()
|
||||
.terminal_overrides
|
||||
.option_as_meta
|
||||
.unwrap_or(false),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
|
||||
self.terminal.update(cx, |term, _| term.clear());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn should_show_cursor(
|
||||
&self,
|
||||
focused: bool,
|
||||
cx: &mut gpui::RenderContext<'_, Self>,
|
||||
) -> bool {
|
||||
//Don't blink the cursor when not focused, blinking is disabled, or paused
|
||||
if !focused
|
||||
|| !self.blinking_on
|
||||
|| self.blinking_paused
|
||||
|| self
|
||||
.terminal
|
||||
.read(cx)
|
||||
.last_content
|
||||
.mode
|
||||
.contains(TermMode::ALT_SCREEN)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
let setting = {
|
||||
let settings = cx.global::<Settings>();
|
||||
settings
|
||||
.terminal_overrides
|
||||
.blinking
|
||||
.clone()
|
||||
.unwrap_or(TerminalBlink::TerminalControlled)
|
||||
};
|
||||
|
||||
match setting {
|
||||
//If the user requested to never blink, don't blink it.
|
||||
TerminalBlink::Off => true,
|
||||
//If the terminal is controlling it, check terminal mode
|
||||
TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state,
|
||||
}
|
||||
}
|
||||
|
||||
fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
|
||||
if epoch == self.blink_epoch && !self.blinking_paused {
|
||||
self.blink_state = !self.blink_state;
|
||||
cx.notify();
|
||||
|
||||
let epoch = self.next_blink_epoch();
|
||||
cx.spawn(|this, mut cx| {
|
||||
let this = this.downgrade();
|
||||
async move {
|
||||
Timer::after(CURSOR_BLINK_INTERVAL).await;
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.blink_state = true;
|
||||
cx.notify();
|
||||
|
||||
let epoch = self.next_blink_epoch();
|
||||
cx.spawn(|this, mut cx| {
|
||||
let this = this.downgrade();
|
||||
async move {
|
||||
Timer::after(CURSOR_BLINK_INTERVAL).await;
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.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
|
||||
}
|
||||
|
||||
fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
|
||||
if epoch == self.blink_epoch {
|
||||
self.blinking_paused = false;
|
||||
self.blink_cursors(epoch, cx);
|
||||
}
|
||||
}
|
||||
|
||||
///Attempt to paste the clipboard into the terminal
|
||||
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
|
||||
self.terminal.update(cx, |term, _| term.copy())
|
||||
}
|
||||
|
||||
///Attempt to paste the clipboard into the terminal
|
||||
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
|
||||
if let Some(item) = cx.read_from_clipboard() {
|
||||
self.terminal
|
||||
.update(cx, |terminal, _cx| terminal.paste(item.text()));
|
||||
}
|
||||
}
|
||||
|
||||
fn send_text(&mut self, text: &SendText, cx: &mut ViewContext<Self>) {
|
||||
self.clear_bel(cx);
|
||||
self.terminal.update(cx, |term, _| {
|
||||
term.input(text.0.to_string());
|
||||
});
|
||||
}
|
||||
|
||||
fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext<Self>) {
|
||||
if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
|
||||
self.clear_bel(cx);
|
||||
self.terminal.update(cx, |term, cx| {
|
||||
term.try_keystroke(
|
||||
&keystroke,
|
||||
cx.global::<Settings>()
|
||||
.terminal_overrides
|
||||
.option_as_meta
|
||||
.unwrap_or(false),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl View for TerminalView {
|
||||
fn ui_name() -> &'static str {
|
||||
"Terminal"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
|
||||
let terminal_handle = self.terminal.clone().downgrade();
|
||||
|
||||
let self_id = cx.view_id();
|
||||
let focused = cx
|
||||
.focused_view_id(cx.window_id())
|
||||
.filter(|view_id| *view_id == self_id)
|
||||
.is_some();
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
TerminalElement::new(
|
||||
cx.handle(),
|
||||
terminal_handle,
|
||||
focused,
|
||||
self.should_show_cursor(focused, cx),
|
||||
)
|
||||
.contained()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(ChildView::new(&self.context_menu, cx).boxed())
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.has_new_content = false;
|
||||
self.terminal.read(cx).focus_in();
|
||||
self.blink_cursors(self.blink_epoch, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.terminal.update(cx, |terminal, _| {
|
||||
terminal.focus_out();
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn key_down(&mut self, event: &gpui::KeyDownEvent, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.clear_bel(cx);
|
||||
self.pause_cursor_blinking(cx);
|
||||
|
||||
self.terminal.update(cx, |term, cx| {
|
||||
term.try_keystroke(
|
||||
&event.keystroke,
|
||||
cx.global::<Settings>()
|
||||
.terminal_overrides
|
||||
.option_as_meta
|
||||
.unwrap_or(false),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
//IME stuff
|
||||
fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
|
||||
if self
|
||||
.terminal
|
||||
.read(cx)
|
||||
.last_content
|
||||
.mode
|
||||
.contains(TermMode::ALT_SCREEN)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(0..0)
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
_: Option<std::ops::Range<usize>>,
|
||||
text: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.terminal.update(cx, |terminal, _| {
|
||||
terminal.input(text.into());
|
||||
});
|
||||
}
|
||||
|
||||
fn keymap_context(&self, cx: &gpui::AppContext) -> gpui::keymap::Context {
|
||||
let mut context = Self::default_keymap_context();
|
||||
if self.modal {
|
||||
context.set.insert("ModalTerminal".into());
|
||||
}
|
||||
let mode = self.terminal.read(cx).last_content.mode;
|
||||
context.map.insert(
|
||||
"screen".to_string(),
|
||||
(if mode.contains(TermMode::ALT_SCREEN) {
|
||||
"alt"
|
||||
} else {
|
||||
"normal"
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
if mode.contains(TermMode::APP_CURSOR) {
|
||||
context.set.insert("DECCKM".to_string());
|
||||
}
|
||||
if mode.contains(TermMode::APP_KEYPAD) {
|
||||
context.set.insert("DECPAM".to_string());
|
||||
}
|
||||
//Note the ! here
|
||||
if !mode.contains(TermMode::APP_KEYPAD) {
|
||||
context.set.insert("DECPNM".to_string());
|
||||
}
|
||||
if mode.contains(TermMode::SHOW_CURSOR) {
|
||||
context.set.insert("DECTCEM".to_string());
|
||||
}
|
||||
if mode.contains(TermMode::LINE_WRAP) {
|
||||
context.set.insert("DECAWM".to_string());
|
||||
}
|
||||
if mode.contains(TermMode::ORIGIN) {
|
||||
context.set.insert("DECOM".to_string());
|
||||
}
|
||||
if mode.contains(TermMode::INSERT) {
|
||||
context.set.insert("IRM".to_string());
|
||||
}
|
||||
//LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
|
||||
if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
|
||||
context.set.insert("LNM".to_string());
|
||||
}
|
||||
if mode.contains(TermMode::FOCUS_IN_OUT) {
|
||||
context.set.insert("report_focus".to_string());
|
||||
}
|
||||
if mode.contains(TermMode::ALTERNATE_SCROLL) {
|
||||
context.set.insert("alternate_scroll".to_string());
|
||||
}
|
||||
if mode.contains(TermMode::BRACKETED_PASTE) {
|
||||
context.set.insert("bracketed_paste".to_string());
|
||||
}
|
||||
if mode.intersects(TermMode::MOUSE_MODE) {
|
||||
context.set.insert("any_mouse_reporting".to_string());
|
||||
}
|
||||
{
|
||||
let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
|
||||
"click"
|
||||
} else if mode.contains(TermMode::MOUSE_DRAG) {
|
||||
"drag"
|
||||
} else if mode.contains(TermMode::MOUSE_MOTION) {
|
||||
"motion"
|
||||
} else {
|
||||
"off"
|
||||
};
|
||||
context
|
||||
.map
|
||||
.insert("mouse_reporting".to_string(), mouse_reporting.to_string());
|
||||
}
|
||||
{
|
||||
let format = if mode.contains(TermMode::SGR_MOUSE) {
|
||||
"sgr"
|
||||
} else if mode.contains(TermMode::UTF8_MOUSE) {
|
||||
"utf8"
|
||||
} else {
|
||||
"normal"
|
||||
};
|
||||
context
|
||||
.map
|
||||
.insert("mouse_format".to_string(), format.to_string());
|
||||
}
|
||||
context
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue