Merge branch 'main' into theme-improvements
This commit is contained in:
commit
bcc554a3db
76 changed files with 4053 additions and 1557 deletions
|
@ -1,25 +1,29 @@
|
|||
mod keymappings;
|
||||
|
||||
use alacritty_terminal::{
|
||||
ansi::{ClearMode, Handler},
|
||||
config::{Config, PtyConfig},
|
||||
config::{Config, Program, PtyConfig},
|
||||
event::{Event as AlacTermEvent, Notify},
|
||||
event_loop::{EventLoop, Msg, Notifier},
|
||||
grid::Scroll,
|
||||
sync::FairMutex,
|
||||
term::SizeInfo,
|
||||
term::{SizeInfo, TermMode},
|
||||
tty::{self, setup_env},
|
||||
Term,
|
||||
};
|
||||
use futures::{channel::mpsc::unbounded, StreamExt};
|
||||
use settings::Settings;
|
||||
use settings::{Settings, Shell};
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||
|
||||
use gpui::{ClipboardItem, CursorStyle, Entity, ModelContext};
|
||||
use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext};
|
||||
|
||||
use crate::{
|
||||
color_translation::{get_color_at_index, to_alac_rgb},
|
||||
ZedListener,
|
||||
};
|
||||
|
||||
use self::keymappings::to_esc_str;
|
||||
|
||||
const DEFAULT_TITLE: &str = "Terminal";
|
||||
|
||||
///Upward flowing events, for changing the title and such
|
||||
|
@ -42,16 +46,32 @@ pub struct TerminalConnection {
|
|||
impl TerminalConnection {
|
||||
pub fn new(
|
||||
working_directory: Option<PathBuf>,
|
||||
shell: Option<Shell>,
|
||||
env_vars: Option<Vec<(String, String)>>,
|
||||
initial_size: SizeInfo,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> TerminalConnection {
|
||||
let pty_config = PtyConfig {
|
||||
shell: None, //Use the users default shell
|
||||
working_directory: working_directory.clone(),
|
||||
hold: false,
|
||||
let pty_config = {
|
||||
let shell = shell.and_then(|shell| match shell {
|
||||
Shell::System => None,
|
||||
Shell::Program(program) => Some(Program::Just(program)),
|
||||
Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
|
||||
});
|
||||
|
||||
PtyConfig {
|
||||
shell,
|
||||
working_directory: working_directory.clone(),
|
||||
hold: false,
|
||||
}
|
||||
};
|
||||
|
||||
let mut env: HashMap<String, String> = HashMap::new();
|
||||
if let Some(envs) = env_vars {
|
||||
for (var, val) in envs {
|
||||
env.insert(var, val);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Properly set the current locale,
|
||||
env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
|
||||
|
||||
|
@ -71,7 +91,20 @@ impl TerminalConnection {
|
|||
let term = Arc::new(FairMutex::new(term));
|
||||
|
||||
//Setup the pty...
|
||||
let pty = tty::new(&pty_config, &initial_size, None).expect("Could not create tty");
|
||||
let pty = {
|
||||
if let Some(pty) = tty::new(&pty_config, &initial_size, None).ok() {
|
||||
pty
|
||||
} else {
|
||||
let pty_config = PtyConfig {
|
||||
shell: None,
|
||||
working_directory: working_directory.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
tty::new(&pty_config, &initial_size, None)
|
||||
.expect("Failed with default shell too :(")
|
||||
}
|
||||
};
|
||||
|
||||
//And connect them together
|
||||
let event_loop = EventLoop::new(
|
||||
|
@ -182,6 +215,30 @@ impl TerminalConnection {
|
|||
self.write_to_pty("\x0c".into());
|
||||
self.term.lock().clear_screen(ClearMode::Saved);
|
||||
}
|
||||
|
||||
pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
|
||||
let guard = self.term.lock();
|
||||
let mode = guard.mode();
|
||||
let esc = to_esc_str(keystroke, mode);
|
||||
drop(guard);
|
||||
if esc.is_some() {
|
||||
self.write_to_pty(esc.unwrap());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
///Paste text into the terminal
|
||||
pub fn paste(&mut self, text: &str) {
|
||||
if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) {
|
||||
self.write_to_pty("\x1b[200~".to_string());
|
||||
self.write_to_pty(text.replace('\x1b', "").to_string());
|
||||
self.write_to_pty("\x1b[201~".to_string());
|
||||
} else {
|
||||
self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TerminalConnection {
|
||||
|
|
444
crates/terminal/src/connection/keymappings.rs
Normal file
444
crates/terminal/src/connection/keymappings.rs
Normal file
|
@ -0,0 +1,444 @@
|
|||
use alacritty_terminal::term::TermMode;
|
||||
use gpui::keymap::Keystroke;
|
||||
|
||||
/*
|
||||
Connection events still to do:
|
||||
- Reporting mouse events correctly.
|
||||
- Reporting scrolls
|
||||
- Correctly bracketing a paste
|
||||
- Storing changed colors
|
||||
- Focus change sequence
|
||||
*/
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Modifiers {
|
||||
None,
|
||||
Alt,
|
||||
Ctrl,
|
||||
Shift,
|
||||
CtrlShift,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Modifiers {
|
||||
fn new(ks: &Keystroke) -> Self {
|
||||
match (ks.alt, ks.ctrl, ks.shift, ks.cmd) {
|
||||
(false, false, false, false) => Modifiers::None,
|
||||
(true, false, false, false) => Modifiers::Alt,
|
||||
(false, true, false, false) => Modifiers::Ctrl,
|
||||
(false, false, true, false) => Modifiers::Shift,
|
||||
(false, true, true, false) => Modifiers::CtrlShift,
|
||||
_ => Modifiers::Other,
|
||||
}
|
||||
}
|
||||
|
||||
fn any(&self) -> bool {
|
||||
match &self {
|
||||
Modifiers::None => false,
|
||||
Modifiers::Alt => true,
|
||||
Modifiers::Ctrl => true,
|
||||
Modifiers::Shift => true,
|
||||
Modifiers::CtrlShift => true,
|
||||
Modifiers::Other => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode) -> Option<String> {
|
||||
let modifiers = Modifiers::new(&keystroke);
|
||||
|
||||
// Manual Bindings including modifiers
|
||||
let manual_esc_str = match (keystroke.key.as_ref(), &modifiers) {
|
||||
//Basic special keys
|
||||
("space", Modifiers::None) => Some(" ".to_string()),
|
||||
("tab", Modifiers::None) => Some("\x09".to_string()),
|
||||
("escape", Modifiers::None) => Some("\x1b".to_string()),
|
||||
("enter", Modifiers::None) => Some("\x0d".to_string()),
|
||||
("backspace", Modifiers::None) => Some("\x7f".to_string()),
|
||||
//Interesting escape codes
|
||||
("tab", Modifiers::Shift) => Some("\x1b[Z".to_string()),
|
||||
("backspace", Modifiers::Alt) => Some("\x1b\x7f".to_string()),
|
||||
("backspace", Modifiers::Shift) => Some("\x7f".to_string()),
|
||||
("home", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
|
||||
Some("\x1b[1;2H".to_string())
|
||||
}
|
||||
("end", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
|
||||
Some("\x1b[1;2F".to_string())
|
||||
}
|
||||
("pageup", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
|
||||
Some("\x1b[5;2~".to_string())
|
||||
}
|
||||
("pagedown", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
|
||||
Some("\x1b[6;2~".to_string())
|
||||
}
|
||||
("home", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1bOH".to_string())
|
||||
}
|
||||
("home", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1b[H".to_string())
|
||||
}
|
||||
("end", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1bOF".to_string())
|
||||
}
|
||||
("end", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1b[F".to_string())
|
||||
}
|
||||
("up", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1bOA".to_string())
|
||||
}
|
||||
("up", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1b[A".to_string())
|
||||
}
|
||||
("down", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1bOB".to_string())
|
||||
}
|
||||
("down", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1b[B".to_string())
|
||||
}
|
||||
("right", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1bOC".to_string())
|
||||
}
|
||||
("right", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1b[C".to_string())
|
||||
}
|
||||
("left", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1bOD".to_string())
|
||||
}
|
||||
("left", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1b[D".to_string())
|
||||
}
|
||||
("back", Modifiers::None) => Some("\x7f".to_string()),
|
||||
("insert", Modifiers::None) => Some("\x1b[2~".to_string()),
|
||||
("delete", Modifiers::None) => Some("\x1b[3~".to_string()),
|
||||
("pageup", Modifiers::None) => Some("\x1b[5~".to_string()),
|
||||
("pagedown", Modifiers::None) => Some("\x1b[6~".to_string()),
|
||||
("f1", Modifiers::None) => Some("\x1bOP".to_string()),
|
||||
("f2", Modifiers::None) => Some("\x1bOQ".to_string()),
|
||||
("f3", Modifiers::None) => Some("\x1bOR".to_string()),
|
||||
("f4", Modifiers::None) => Some("\x1bOS".to_string()),
|
||||
("f5", Modifiers::None) => Some("\x1b[15~".to_string()),
|
||||
("f6", Modifiers::None) => Some("\x1b[17~".to_string()),
|
||||
("f7", Modifiers::None) => Some("\x1b[18~".to_string()),
|
||||
("f8", Modifiers::None) => Some("\x1b[19~".to_string()),
|
||||
("f9", Modifiers::None) => Some("\x1b[20~".to_string()),
|
||||
("f10", Modifiers::None) => Some("\x1b[21~".to_string()),
|
||||
("f11", Modifiers::None) => Some("\x1b[23~".to_string()),
|
||||
("f12", Modifiers::None) => Some("\x1b[24~".to_string()),
|
||||
("f13", Modifiers::None) => Some("\x1b[25~".to_string()),
|
||||
("f14", Modifiers::None) => Some("\x1b[26~".to_string()),
|
||||
("f15", Modifiers::None) => Some("\x1b[28~".to_string()),
|
||||
("f16", Modifiers::None) => Some("\x1b[29~".to_string()),
|
||||
("f17", Modifiers::None) => Some("\x1b[31~".to_string()),
|
||||
("f18", Modifiers::None) => Some("\x1b[32~".to_string()),
|
||||
("f19", Modifiers::None) => Some("\x1b[33~".to_string()),
|
||||
("f20", Modifiers::None) => Some("\x1b[34~".to_string()),
|
||||
// NumpadEnter, Action::Esc("\n".into());
|
||||
//Mappings for caret notation keys
|
||||
("a", Modifiers::Ctrl) => Some("\x01".to_string()), //1
|
||||
("A", Modifiers::CtrlShift) => Some("\x01".to_string()), //1
|
||||
("b", Modifiers::Ctrl) => Some("\x02".to_string()), //2
|
||||
("B", Modifiers::CtrlShift) => Some("\x02".to_string()), //2
|
||||
("c", Modifiers::Ctrl) => Some("\x03".to_string()), //3
|
||||
("C", Modifiers::CtrlShift) => Some("\x03".to_string()), //3
|
||||
("d", Modifiers::Ctrl) => Some("\x04".to_string()), //4
|
||||
("D", Modifiers::CtrlShift) => Some("\x04".to_string()), //4
|
||||
("e", Modifiers::Ctrl) => Some("\x05".to_string()), //5
|
||||
("E", Modifiers::CtrlShift) => Some("\x05".to_string()), //5
|
||||
("f", Modifiers::Ctrl) => Some("\x06".to_string()), //6
|
||||
("F", Modifiers::CtrlShift) => Some("\x06".to_string()), //6
|
||||
("g", Modifiers::Ctrl) => Some("\x07".to_string()), //7
|
||||
("G", Modifiers::CtrlShift) => Some("\x07".to_string()), //7
|
||||
("h", Modifiers::Ctrl) => Some("\x08".to_string()), //8
|
||||
("H", Modifiers::CtrlShift) => Some("\x08".to_string()), //8
|
||||
("i", Modifiers::Ctrl) => Some("\x09".to_string()), //9
|
||||
("I", Modifiers::CtrlShift) => Some("\x09".to_string()), //9
|
||||
("j", Modifiers::Ctrl) => Some("\x0a".to_string()), //10
|
||||
("J", Modifiers::CtrlShift) => Some("\x0a".to_string()), //10
|
||||
("k", Modifiers::Ctrl) => Some("\x0b".to_string()), //11
|
||||
("K", Modifiers::CtrlShift) => Some("\x0b".to_string()), //11
|
||||
("l", Modifiers::Ctrl) => Some("\x0c".to_string()), //12
|
||||
("L", Modifiers::CtrlShift) => Some("\x0c".to_string()), //12
|
||||
("m", Modifiers::Ctrl) => Some("\x0d".to_string()), //13
|
||||
("M", Modifiers::CtrlShift) => Some("\x0d".to_string()), //13
|
||||
("n", Modifiers::Ctrl) => Some("\x0e".to_string()), //14
|
||||
("N", Modifiers::CtrlShift) => Some("\x0e".to_string()), //14
|
||||
("o", Modifiers::Ctrl) => Some("\x0f".to_string()), //15
|
||||
("O", Modifiers::CtrlShift) => Some("\x0f".to_string()), //15
|
||||
("p", Modifiers::Ctrl) => Some("\x10".to_string()), //16
|
||||
("P", Modifiers::CtrlShift) => Some("\x10".to_string()), //16
|
||||
("q", Modifiers::Ctrl) => Some("\x11".to_string()), //17
|
||||
("Q", Modifiers::CtrlShift) => Some("\x11".to_string()), //17
|
||||
("r", Modifiers::Ctrl) => Some("\x12".to_string()), //18
|
||||
("R", Modifiers::CtrlShift) => Some("\x12".to_string()), //18
|
||||
("s", Modifiers::Ctrl) => Some("\x13".to_string()), //19
|
||||
("S", Modifiers::CtrlShift) => Some("\x13".to_string()), //19
|
||||
("t", Modifiers::Ctrl) => Some("\x14".to_string()), //20
|
||||
("T", Modifiers::CtrlShift) => Some("\x14".to_string()), //20
|
||||
("u", Modifiers::Ctrl) => Some("\x15".to_string()), //21
|
||||
("U", Modifiers::CtrlShift) => Some("\x15".to_string()), //21
|
||||
("v", Modifiers::Ctrl) => Some("\x16".to_string()), //22
|
||||
("V", Modifiers::CtrlShift) => Some("\x16".to_string()), //22
|
||||
("w", Modifiers::Ctrl) => Some("\x17".to_string()), //23
|
||||
("W", Modifiers::CtrlShift) => Some("\x17".to_string()), //23
|
||||
("x", Modifiers::Ctrl) => Some("\x18".to_string()), //24
|
||||
("X", Modifiers::CtrlShift) => Some("\x18".to_string()), //24
|
||||
("y", Modifiers::Ctrl) => Some("\x19".to_string()), //25
|
||||
("Y", Modifiers::CtrlShift) => Some("\x19".to_string()), //25
|
||||
("z", Modifiers::Ctrl) => Some("\x1a".to_string()), //26
|
||||
("Z", Modifiers::CtrlShift) => Some("\x1a".to_string()), //26
|
||||
("@", Modifiers::Ctrl) => Some("\x00".to_string()), //0
|
||||
("[", Modifiers::Ctrl) => Some("\x1b".to_string()), //27
|
||||
("\\", Modifiers::Ctrl) => Some("\x1c".to_string()), //28
|
||||
("]", Modifiers::Ctrl) => Some("\x1d".to_string()), //29
|
||||
("^", Modifiers::Ctrl) => Some("\x1e".to_string()), //30
|
||||
("_", Modifiers::Ctrl) => Some("\x1f".to_string()), //31
|
||||
("?", Modifiers::Ctrl) => Some("\x7f".to_string()), //127
|
||||
_ => None,
|
||||
};
|
||||
if manual_esc_str.is_some() {
|
||||
return manual_esc_str;
|
||||
}
|
||||
|
||||
// Automated bindings applying modifiers
|
||||
if modifiers.any() {
|
||||
let modifier_code = modifier_code(&keystroke);
|
||||
let modified_esc_str = match keystroke.key.as_ref() {
|
||||
"up" => Some(format!("\x1b[1;{}A", modifier_code)),
|
||||
"down" => Some(format!("\x1b[1;{}B", modifier_code)),
|
||||
"right" => Some(format!("\x1b[1;{}C", modifier_code)),
|
||||
"left" => Some(format!("\x1b[1;{}D", modifier_code)),
|
||||
"f1" => Some(format!("\x1b[1;{}P", modifier_code)),
|
||||
"f2" => Some(format!("\x1b[1;{}Q", modifier_code)),
|
||||
"f3" => Some(format!("\x1b[1;{}R", modifier_code)),
|
||||
"f4" => Some(format!("\x1b[1;{}S", modifier_code)),
|
||||
"F5" => Some(format!("\x1b[15;{}~", modifier_code)),
|
||||
"f6" => Some(format!("\x1b[17;{}~", modifier_code)),
|
||||
"f7" => Some(format!("\x1b[18;{}~", modifier_code)),
|
||||
"f8" => Some(format!("\x1b[19;{}~", modifier_code)),
|
||||
"f9" => Some(format!("\x1b[20;{}~", modifier_code)),
|
||||
"f10" => Some(format!("\x1b[21;{}~", modifier_code)),
|
||||
"f11" => Some(format!("\x1b[23;{}~", modifier_code)),
|
||||
"f12" => Some(format!("\x1b[24;{}~", modifier_code)),
|
||||
"f13" => Some(format!("\x1b[25;{}~", modifier_code)),
|
||||
"f14" => Some(format!("\x1b[26;{}~", modifier_code)),
|
||||
"f15" => Some(format!("\x1b[28;{}~", modifier_code)),
|
||||
"f16" => Some(format!("\x1b[29;{}~", modifier_code)),
|
||||
"f17" => Some(format!("\x1b[31;{}~", modifier_code)),
|
||||
"f18" => Some(format!("\x1b[32;{}~", modifier_code)),
|
||||
"f19" => Some(format!("\x1b[33;{}~", modifier_code)),
|
||||
"f20" => Some(format!("\x1b[34;{}~", modifier_code)),
|
||||
_ if modifier_code == 2 => None,
|
||||
"insert" => Some(format!("\x1b[2;{}~", modifier_code)),
|
||||
"pageup" => Some(format!("\x1b[5;{}~", modifier_code)),
|
||||
"pagedown" => Some(format!("\x1b[6;{}~", modifier_code)),
|
||||
"end" => Some(format!("\x1b[1;{}F", modifier_code)),
|
||||
"home" => Some(format!("\x1b[1;{}H", modifier_code)),
|
||||
_ => None,
|
||||
};
|
||||
if modified_esc_str.is_some() {
|
||||
return modified_esc_str;
|
||||
}
|
||||
}
|
||||
|
||||
//Fallback to sending the keystroke input directly
|
||||
//Skin colors in utf8 are implemented as a seperate, invisible character
|
||||
//that modifies the associated emoji. Some languages may have similarly
|
||||
//implemented modifiers, e.g. certain diacritics that can be typed as a single character.
|
||||
//This means that we need to assume some user input can result in multi-byte,
|
||||
//multi-char strings. This is somewhat difficult, as GPUI normalizes all
|
||||
//keys into a string representation. Hence, the check here to filter out GPUI
|
||||
//keys that weren't captured above.
|
||||
if !matches_gpui_key_str(&keystroke.key) {
|
||||
return Some(keystroke.key.clone());
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
///Checks if the given string matches a GPUI key string.
|
||||
///Table made from reading the source at gpui/src/platform/mac/event.rs
|
||||
fn matches_gpui_key_str(str: &str) -> bool {
|
||||
match str {
|
||||
"backspace" => true,
|
||||
"up" => true,
|
||||
"down" => true,
|
||||
"left" => true,
|
||||
"right" => true,
|
||||
"pageup" => true,
|
||||
"pagedown" => true,
|
||||
"home" => true,
|
||||
"end" => true,
|
||||
"delete" => true,
|
||||
"enter" => true,
|
||||
"escape" => true,
|
||||
"tab" => true,
|
||||
"f1" => true,
|
||||
"f2" => true,
|
||||
"f3" => true,
|
||||
"f4" => true,
|
||||
"f5" => true,
|
||||
"f6" => true,
|
||||
"f7" => true,
|
||||
"f8" => true,
|
||||
"f9" => true,
|
||||
"f10" => true,
|
||||
"f11" => true,
|
||||
"f12" => true,
|
||||
"space" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Code Modifiers
|
||||
/// ---------+---------------------------
|
||||
/// 2 | Shift
|
||||
/// 3 | Alt
|
||||
/// 4 | Shift + Alt
|
||||
/// 5 | Control
|
||||
/// 6 | Shift + Control
|
||||
/// 7 | Alt + Control
|
||||
/// 8 | Shift + Alt + Control
|
||||
/// ---------+---------------------------
|
||||
/// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
|
||||
fn modifier_code(keystroke: &Keystroke) -> u32 {
|
||||
let mut modifier_code = 0;
|
||||
if keystroke.shift {
|
||||
modifier_code |= 1;
|
||||
}
|
||||
if keystroke.alt {
|
||||
modifier_code |= 1 << 1;
|
||||
}
|
||||
if keystroke.ctrl {
|
||||
modifier_code |= 1 << 2;
|
||||
}
|
||||
modifier_code + 1
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_scroll_keys() {
|
||||
//These keys should be handled by the scrolling element directly
|
||||
//Need to signify this by returning 'None'
|
||||
let shift_pageup = Keystroke::parse("shift-pageup").unwrap();
|
||||
let shift_pagedown = Keystroke::parse("shift-pagedown").unwrap();
|
||||
let shift_home = Keystroke::parse("shift-home").unwrap();
|
||||
let shift_end = Keystroke::parse("shift-end").unwrap();
|
||||
|
||||
let none = TermMode::NONE;
|
||||
assert_eq!(to_esc_str(&shift_pageup, &none), None);
|
||||
assert_eq!(to_esc_str(&shift_pagedown, &none), None);
|
||||
assert_eq!(to_esc_str(&shift_home, &none), None);
|
||||
assert_eq!(to_esc_str(&shift_end, &none), None);
|
||||
|
||||
let alt_screen = TermMode::ALT_SCREEN;
|
||||
assert_eq!(
|
||||
to_esc_str(&shift_pageup, &alt_screen),
|
||||
Some("\x1b[5;2~".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
to_esc_str(&shift_pagedown, &alt_screen),
|
||||
Some("\x1b[6;2~".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
to_esc_str(&shift_home, &alt_screen),
|
||||
Some("\x1b[1;2H".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
to_esc_str(&shift_end, &alt_screen),
|
||||
Some("\x1b[1;2F".to_string())
|
||||
);
|
||||
|
||||
let pageup = Keystroke::parse("pageup").unwrap();
|
||||
let pagedown = Keystroke::parse("pagedown").unwrap();
|
||||
let any = TermMode::ANY;
|
||||
|
||||
assert_eq!(to_esc_str(&pageup, &any), Some("\x1b[5~".to_string()));
|
||||
assert_eq!(to_esc_str(&pagedown, &any), Some("\x1b[6~".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_char_fallthrough() {
|
||||
let ks = Keystroke {
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
cmd: false,
|
||||
|
||||
key: "🖖🏻".to_string(), //2 char string
|
||||
};
|
||||
|
||||
assert_eq!(to_esc_str(&ks, &TermMode::NONE), Some("🖖🏻".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_application_mode() {
|
||||
let app_cursor = TermMode::APP_CURSOR;
|
||||
let none = TermMode::NONE;
|
||||
|
||||
let up = Keystroke::parse("up").unwrap();
|
||||
let down = Keystroke::parse("down").unwrap();
|
||||
let left = Keystroke::parse("left").unwrap();
|
||||
let right = Keystroke::parse("right").unwrap();
|
||||
|
||||
assert_eq!(to_esc_str(&up, &none), Some("\x1b[A".to_string()));
|
||||
assert_eq!(to_esc_str(&down, &none), Some("\x1b[B".to_string()));
|
||||
assert_eq!(to_esc_str(&right, &none), Some("\x1b[C".to_string()));
|
||||
assert_eq!(to_esc_str(&left, &none), Some("\x1b[D".to_string()));
|
||||
|
||||
assert_eq!(to_esc_str(&up, &app_cursor), Some("\x1bOA".to_string()));
|
||||
assert_eq!(to_esc_str(&down, &app_cursor), Some("\x1bOB".to_string()));
|
||||
assert_eq!(to_esc_str(&right, &app_cursor), Some("\x1bOC".to_string()));
|
||||
assert_eq!(to_esc_str(&left, &app_cursor), Some("\x1bOD".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ctrl_codes() {
|
||||
let letters_lower = 'a'..='z';
|
||||
let letters_upper = 'A'..='Z';
|
||||
let mode = TermMode::ANY;
|
||||
|
||||
for (lower, upper) in letters_lower.zip(letters_upper) {
|
||||
assert_eq!(
|
||||
to_esc_str(
|
||||
&Keystroke::parse(&format!("ctrl-{}", lower)).unwrap(),
|
||||
&mode
|
||||
),
|
||||
to_esc_str(
|
||||
&Keystroke::parse(&format!("ctrl-shift-{}", upper)).unwrap(),
|
||||
&mode
|
||||
),
|
||||
"On letter: {}/{}",
|
||||
lower,
|
||||
upper
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modifier_code_calc() {
|
||||
// Code Modifiers
|
||||
// ---------+---------------------------
|
||||
// 2 | Shift
|
||||
// 3 | Alt
|
||||
// 4 | Shift + Alt
|
||||
// 5 | Control
|
||||
// 6 | Shift + Control
|
||||
// 7 | Alt + Control
|
||||
// 8 | Shift + Alt + Control
|
||||
// ---------+---------------------------
|
||||
// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
|
||||
assert_eq!(2, modifier_code(&Keystroke::parse("shift-A").unwrap()));
|
||||
assert_eq!(3, modifier_code(&Keystroke::parse("alt-A").unwrap()));
|
||||
assert_eq!(4, modifier_code(&Keystroke::parse("shift-alt-A").unwrap()));
|
||||
assert_eq!(5, modifier_code(&Keystroke::parse("ctrl-A").unwrap()));
|
||||
assert_eq!(6, modifier_code(&Keystroke::parse("shift-ctrl-A").unwrap()));
|
||||
assert_eq!(7, modifier_code(&Keystroke::parse("alt-ctrl-A").unwrap()));
|
||||
assert_eq!(
|
||||
8,
|
||||
modifier_code(&Keystroke::parse("shift-ctrl-alt-A").unwrap())
|
||||
);
|
||||
}
|
||||
}
|
|
@ -16,8 +16,11 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon
|
|||
if let Some(StoredConnection(stored_connection)) = possible_connection {
|
||||
// Create a view from the stored connection
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
cx.add_view(|cx| Terminal::from_connection(stored_connection, true, cx))
|
||||
cx.add_view(|cx| Terminal::from_connection(stored_connection.clone(), true, cx))
|
||||
});
|
||||
cx.set_global::<Option<StoredConnection>>(Some(StoredConnection(
|
||||
stored_connection.clone(),
|
||||
)));
|
||||
} else {
|
||||
// No connection was stored, create a new terminal
|
||||
if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| {
|
||||
|
|
|
@ -5,7 +5,6 @@ pub mod terminal_element;
|
|||
|
||||
use alacritty_terminal::{
|
||||
event::{Event as AlacTermEvent, EventListener},
|
||||
grid::Scroll,
|
||||
term::SizeInfo,
|
||||
};
|
||||
|
||||
|
@ -14,29 +13,19 @@ use dirs::home_dir;
|
|||
use editor::Input;
|
||||
use futures::channel::mpsc::UnboundedSender;
|
||||
use gpui::{
|
||||
actions, elements::*, impl_internal_actions, AppContext, ClipboardItem, Entity, ModelHandle,
|
||||
actions, elements::*, keymap::Keystroke, AppContext, ClipboardItem, Entity, ModelHandle,
|
||||
MutableAppContext, View, ViewContext,
|
||||
};
|
||||
use modal::deploy_modal;
|
||||
|
||||
use project::{Project, ProjectPath};
|
||||
use settings::Settings;
|
||||
use project::{LocalWorktree, Project, ProjectPath};
|
||||
use settings::{Settings, WorkingDirectory};
|
||||
use smallvec::SmallVec;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use workspace::{Item, Workspace};
|
||||
|
||||
use crate::terminal_element::TerminalEl;
|
||||
|
||||
//ASCII Control characters on a keyboard
|
||||
const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c'
|
||||
const TAB_CHAR: char = 9_u8 as char;
|
||||
const CARRIAGE_RETURN_CHAR: char = 13_u8 as char;
|
||||
const ESC_CHAR: char = 27_u8 as char; // == \x1b
|
||||
const DEL_CHAR: char = 127_u8 as char;
|
||||
const LEFT_SEQ: &str = "\x1b[D";
|
||||
const RIGHT_SEQ: &str = "\x1b[C";
|
||||
const UP_SEQ: &str = "\x1b[A";
|
||||
const DOWN_SEQ: &str = "\x1b[B";
|
||||
const DEBUG_TERMINAL_WIDTH: f32 = 1000.; //This needs to be wide enough that the prompt can fill the whole space.
|
||||
const DEBUG_TERMINAL_HEIGHT: f32 = 200.;
|
||||
const DEBUG_CELL_WIDTH: f32 = 5.;
|
||||
|
@ -52,44 +41,34 @@ pub struct ScrollTerminal(pub i32);
|
|||
actions!(
|
||||
terminal,
|
||||
[
|
||||
Sigint,
|
||||
Escape,
|
||||
Del,
|
||||
Return,
|
||||
Left,
|
||||
Right,
|
||||
Deploy,
|
||||
Up,
|
||||
Down,
|
||||
Tab,
|
||||
CtrlC,
|
||||
Escape,
|
||||
Enter,
|
||||
Clear,
|
||||
Copy,
|
||||
Paste,
|
||||
Deploy,
|
||||
Quit,
|
||||
DeployModal,
|
||||
DeployModal
|
||||
]
|
||||
);
|
||||
impl_internal_actions!(terminal, [ScrollTerminal]);
|
||||
|
||||
///Initialize and register all of our action handlers
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Terminal::deploy);
|
||||
cx.add_action(Terminal::send_sigint);
|
||||
cx.add_action(Terminal::escape);
|
||||
cx.add_action(Terminal::quit);
|
||||
cx.add_action(Terminal::del);
|
||||
cx.add_action(Terminal::carriage_return);
|
||||
cx.add_action(Terminal::left);
|
||||
cx.add_action(Terminal::right);
|
||||
//Global binding overrrides
|
||||
cx.add_action(Terminal::ctrl_c);
|
||||
cx.add_action(Terminal::up);
|
||||
cx.add_action(Terminal::down);
|
||||
cx.add_action(Terminal::tab);
|
||||
cx.add_action(Terminal::escape);
|
||||
cx.add_action(Terminal::enter);
|
||||
//Useful terminal actions
|
||||
cx.add_action(Terminal::deploy);
|
||||
cx.add_action(deploy_modal);
|
||||
cx.add_action(Terminal::copy);
|
||||
cx.add_action(Terminal::paste);
|
||||
cx.add_action(Terminal::scroll_terminal);
|
||||
cx.add_action(Terminal::input);
|
||||
cx.add_action(Terminal::clear);
|
||||
cx.add_action(deploy_modal);
|
||||
}
|
||||
|
||||
///A translation struct for Alacritty to communicate with us from their event loop
|
||||
|
@ -131,8 +110,15 @@ impl Terminal {
|
|||
false,
|
||||
);
|
||||
|
||||
let connection =
|
||||
cx.add_model(|cx| TerminalConnection::new(working_directory, size_info, cx));
|
||||
let (shell, envs) = {
|
||||
let settings = cx.global::<Settings>();
|
||||
let shell = settings.terminal_overrides.shell.clone();
|
||||
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
|
||||
(shell, envs)
|
||||
};
|
||||
|
||||
let connection = cx
|
||||
.add_model(|cx| TerminalConnection::new(working_directory, shell, envs, size_info, cx));
|
||||
|
||||
Terminal::from_connection(connection, modal, cx)
|
||||
}
|
||||
|
@ -168,15 +154,6 @@ impl Terminal {
|
|||
}
|
||||
}
|
||||
|
||||
///Scroll the terminal. This locks the terminal
|
||||
fn scroll_terminal(&mut self, scroll: &ScrollTerminal, cx: &mut ViewContext<Self>) {
|
||||
self.connection
|
||||
.read(cx)
|
||||
.term
|
||||
.lock()
|
||||
.scroll_display(Scroll::Delta(scroll.0));
|
||||
}
|
||||
|
||||
fn input(&mut self, Input(text): &Input, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
//TODO: This is probably not encoding UTF8 correctly (see alacritty/src/input.rs:L825-837)
|
||||
|
@ -200,11 +177,6 @@ impl Terminal {
|
|||
workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(wd, false, cx))), cx);
|
||||
}
|
||||
|
||||
///Tell Zed to close us
|
||||
fn quit(&mut self, _: &Quit, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::CloseTerminal);
|
||||
}
|
||||
|
||||
///Attempt to paste the clipboard into the terminal
|
||||
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
|
||||
let term = self.connection.read(cx).term.lock();
|
||||
|
@ -219,71 +191,43 @@ impl Terminal {
|
|||
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
|
||||
if let Some(item) = cx.read_from_clipboard() {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(item.text().to_owned());
|
||||
connection.paste(item.text());
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
///Send the `up` key
|
||||
///Synthesize the keyboard event corresponding to 'up'
|
||||
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(UP_SEQ.to_string());
|
||||
connection.try_keystroke(&Keystroke::parse("up").unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
///Send the `down` key
|
||||
///Synthesize the keyboard event corresponding to 'down'
|
||||
fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(DOWN_SEQ.to_string());
|
||||
connection.try_keystroke(&Keystroke::parse("down").unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
///Send the `tab` key
|
||||
fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
|
||||
///Synthesize the keyboard event corresponding to 'ctrl-c'
|
||||
fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(TAB_CHAR.to_string());
|
||||
connection.try_keystroke(&Keystroke::parse("ctrl-c").unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
///Send `SIGINT` (`ctrl-c`)
|
||||
fn send_sigint(&mut self, _: &Sigint, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(ETX_CHAR.to_string());
|
||||
});
|
||||
}
|
||||
|
||||
///Send the `escape` key
|
||||
///Synthesize the keyboard event corresponding to 'escape'
|
||||
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(ESC_CHAR.to_string());
|
||||
connection.try_keystroke(&Keystroke::parse("escape").unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
///Send the `delete` key. TODO: Difference between this and backspace?
|
||||
fn del(&mut self, _: &Del, cx: &mut ViewContext<Self>) {
|
||||
///Synthesize the keyboard event corresponding to 'enter'
|
||||
fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(DEL_CHAR.to_string());
|
||||
});
|
||||
}
|
||||
|
||||
///Send a carriage return. TODO: May need to check the terminal mode.
|
||||
fn carriage_return(&mut self, _: &Return, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(CARRIAGE_RETURN_CHAR.to_string());
|
||||
});
|
||||
}
|
||||
|
||||
//Send the `left` key
|
||||
fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(LEFT_SEQ.to_string());
|
||||
});
|
||||
}
|
||||
|
||||
//Send the `right` key
|
||||
fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(RIGHT_SEQ.to_string());
|
||||
connection.try_keystroke(&Keystroke::parse("enter").unwrap());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -324,7 +268,12 @@ impl View for Terminal {
|
|||
}
|
||||
|
||||
impl Item for Terminal {
|
||||
fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox {
|
||||
fn tab_content(
|
||||
&self,
|
||||
_detail: Option<usize>,
|
||||
tab_theme: &theme::Tab,
|
||||
cx: &gpui::AppContext,
|
||||
) -> ElementBox {
|
||||
let settings = cx.global::<Settings>();
|
||||
let search_theme = &settings.theme.search; //TODO properly integrate themes
|
||||
|
||||
|
@ -429,12 +378,41 @@ impl Item for Terminal {
|
|||
}
|
||||
}
|
||||
|
||||
///Get's the working directory for the given workspace, respecting the user's settings.
|
||||
fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
|
||||
let wd_setting = cx
|
||||
.global::<Settings>()
|
||||
.terminal_overrides
|
||||
.working_directory
|
||||
.clone()
|
||||
.unwrap_or(WorkingDirectory::CurrentProjectDirectory);
|
||||
let res = match wd_setting {
|
||||
WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx),
|
||||
WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
|
||||
WorkingDirectory::AlwaysHome => None,
|
||||
WorkingDirectory::Always { directory } => shellexpand::full(&directory)
|
||||
.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 get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
|
||||
fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
|
||||
let project = workspace.project().read(cx);
|
||||
|
||||
project
|
||||
|
@ -442,96 +420,36 @@ fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option<PathBu
|
|||
.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(|wt| {
|
||||
wt.root_entry()
|
||||
.filter(|re| re.is_dir())
|
||||
.map(|_| wt.abs_path().to_path_buf())
|
||||
})
|
||||
.or_else(|| home_dir())
|
||||
.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 alacritty_terminal::{
|
||||
grid::GridIterator,
|
||||
index::{Column, Line, Point, Side},
|
||||
selection::{Selection, SelectionType},
|
||||
term::cell::Cell,
|
||||
};
|
||||
use gpui::TestAppContext;
|
||||
use itertools::Itertools;
|
||||
use crate::tests::terminal_test_context::TerminalTestContext;
|
||||
|
||||
use std::{path::Path, time::Duration};
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use std::path::Path;
|
||||
use workspace::AppState;
|
||||
|
||||
mod terminal_test_context;
|
||||
|
||||
///Basic integration test, can we get the terminal to show up, execute a command,
|
||||
//and produce noticable output?
|
||||
#[gpui::test]
|
||||
#[gpui::test(retries = 5)]
|
||||
async fn test_terminal(cx: &mut TestAppContext) {
|
||||
let terminal = cx.add_view(Default::default(), |cx| Terminal::new(None, false, cx));
|
||||
let mut cx = TerminalTestContext::new(cx);
|
||||
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty("expr 3 + 4".to_string());
|
||||
});
|
||||
terminal.carriage_return(&Return, cx);
|
||||
});
|
||||
|
||||
cx.set_condition_duration(Some(Duration::from_secs(2)));
|
||||
terminal
|
||||
.condition(cx, |terminal, cx| {
|
||||
let term = terminal.connection.read(cx).term.clone();
|
||||
let content = grid_as_str(term.lock().renderable_content().display_iter);
|
||||
content.contains("7")
|
||||
})
|
||||
cx.execute_and_wait("expr 3 + 4", |content, _cx| content.contains("7"))
|
||||
.await;
|
||||
cx.set_condition_duration(None);
|
||||
}
|
||||
|
||||
/// Integration test for selections, clipboard, and terminal execution
|
||||
#[gpui::test]
|
||||
async fn test_copy(cx: &mut TestAppContext) {
|
||||
let mut result_line: i32 = 0;
|
||||
let terminal = cx.add_view(Default::default(), |cx| Terminal::new(None, false, cx));
|
||||
cx.set_condition_duration(Some(Duration::from_secs(2)));
|
||||
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty("expr 3 + 4".to_string());
|
||||
});
|
||||
terminal.carriage_return(&Return, cx);
|
||||
});
|
||||
|
||||
terminal
|
||||
.condition(cx, |terminal, cx| {
|
||||
let term = terminal.connection.read(cx).term.clone();
|
||||
let content = grid_as_str(term.lock().renderable_content().display_iter);
|
||||
|
||||
if content.contains("7") {
|
||||
let idx = content.chars().position(|c| c == '7').unwrap();
|
||||
result_line = content.chars().take(idx).filter(|c| *c == '\n').count() as i32;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
let mut term = terminal.connection.read(cx).term.lock();
|
||||
term.selection = Some(Selection::new(
|
||||
SelectionType::Semantic,
|
||||
Point::new(Line(2), Column(0)),
|
||||
Side::Right,
|
||||
));
|
||||
drop(term);
|
||||
terminal.copy(&Copy, cx)
|
||||
});
|
||||
|
||||
cx.assert_clipboard_content(Some(&"7"));
|
||||
cx.set_condition_duration(None);
|
||||
}
|
||||
|
||||
///Working directory calculation tests
|
||||
|
@ -553,8 +471,10 @@ mod tests {
|
|||
assert!(active_entry.is_none());
|
||||
assert!(workspace.worktrees(cx).next().is_none());
|
||||
|
||||
let res = get_wd_for_workspace(workspace, cx);
|
||||
assert_eq!(res, home_dir())
|
||||
let res = current_project_directory(workspace, cx);
|
||||
assert_eq!(res, None);
|
||||
let res = first_project_directory(workspace, cx);
|
||||
assert_eq!(res, None);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -591,8 +511,10 @@ mod tests {
|
|||
assert!(active_entry.is_none());
|
||||
assert!(workspace.worktrees(cx).next().is_some());
|
||||
|
||||
let res = get_wd_for_workspace(workspace, cx);
|
||||
assert_eq!(res, home_dir())
|
||||
let res = current_project_directory(workspace, cx);
|
||||
assert_eq!(res, None);
|
||||
let res = first_project_directory(workspace, cx);
|
||||
assert_eq!(res, None);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -627,7 +549,9 @@ mod tests {
|
|||
assert!(active_entry.is_none());
|
||||
assert!(workspace.worktrees(cx).next().is_some());
|
||||
|
||||
let res = get_wd_for_workspace(workspace, cx);
|
||||
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()));
|
||||
});
|
||||
}
|
||||
|
@ -639,17 +563,32 @@ mod tests {
|
|||
let params = cx.update(AppState::test);
|
||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (wt, _) = project
|
||||
let (wt1, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root.txt", true, cx)
|
||||
project.find_or_create_local_worktree("/root1/", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (wt2, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root2.txt", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//Setup root
|
||||
let entry = cx
|
||||
let _ = cx
|
||||
.update(|cx| {
|
||||
wt.update(cx, |wt, cx| {
|
||||
wt1.update(cx, |wt, cx| {
|
||||
wt.as_local().unwrap().create_entry(Path::new(""), true, cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let entry2 = cx
|
||||
.update(|cx| {
|
||||
wt2.update(cx, |wt, cx| {
|
||||
wt.as_local()
|
||||
.unwrap()
|
||||
.create_entry(Path::new(""), false, cx)
|
||||
|
@ -660,8 +599,8 @@ mod tests {
|
|||
|
||||
cx.update(|cx| {
|
||||
let p = ProjectPath {
|
||||
worktree_id: wt.read(cx).id(),
|
||||
path: entry.path,
|
||||
worktree_id: wt2.read(cx).id(),
|
||||
path: entry2.path,
|
||||
};
|
||||
project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
|
||||
});
|
||||
|
@ -673,8 +612,10 @@ mod tests {
|
|||
|
||||
assert!(active_entry.is_some());
|
||||
|
||||
let res = get_wd_for_workspace(workspace, cx);
|
||||
assert_eq!(res, home_dir());
|
||||
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()));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -685,17 +626,32 @@ mod tests {
|
|||
let params = cx.update(AppState::test);
|
||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (wt, _) = project
|
||||
let (wt1, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root/", true, cx)
|
||||
project.find_or_create_local_worktree("/root1/", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (wt2, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root2/", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//Setup root
|
||||
let entry = cx
|
||||
let _ = cx
|
||||
.update(|cx| {
|
||||
wt.update(cx, |wt, cx| {
|
||||
wt1.update(cx, |wt, cx| {
|
||||
wt.as_local().unwrap().create_entry(Path::new(""), true, cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let entry2 = cx
|
||||
.update(|cx| {
|
||||
wt2.update(cx, |wt, cx| {
|
||||
wt.as_local().unwrap().create_entry(Path::new(""), true, cx)
|
||||
})
|
||||
})
|
||||
|
@ -704,8 +660,8 @@ mod tests {
|
|||
|
||||
cx.update(|cx| {
|
||||
let p = ProjectPath {
|
||||
worktree_id: wt.read(cx).id(),
|
||||
path: entry.path,
|
||||
worktree_id: wt2.read(cx).id(),
|
||||
path: entry2.path,
|
||||
};
|
||||
project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
|
||||
});
|
||||
|
@ -717,17 +673,10 @@ mod tests {
|
|||
|
||||
assert!(active_entry.is_some());
|
||||
|
||||
let res = get_wd_for_workspace(workspace, cx);
|
||||
assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
|
||||
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()));
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn grid_as_str(grid_iterator: GridIterator<Cell>) -> String {
|
||||
let lines = grid_iterator.group_by(|i| i.point.line.0);
|
||||
lines
|
||||
.into_iter()
|
||||
.map(|(_, line)| line.map(|i| i.c).collect::<String>())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use alacritty_terminal::{
|
||||
grid::{Dimensions, GridIterator, Indexed},
|
||||
grid::{Dimensions, GridIterator, Indexed, Scroll},
|
||||
index::{Column as GridCol, Line as GridLine, Point, Side},
|
||||
selection::{Selection, SelectionRange, SelectionType},
|
||||
sync::FairMutex,
|
||||
|
@ -9,7 +9,7 @@ use alacritty_terminal::{
|
|||
},
|
||||
Term,
|
||||
};
|
||||
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine, Input};
|
||||
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::*,
|
||||
|
@ -20,20 +20,19 @@ use gpui::{
|
|||
},
|
||||
json::json,
|
||||
text_layout::{Line, RunStyle},
|
||||
Event, FontCache, KeyDownEvent, MouseRegion, PaintContext, Quad, ScrollWheelEvent,
|
||||
SizeConstraint, TextLayoutCache, WeakModelHandle,
|
||||
Event, FontCache, KeyDownEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion,
|
||||
PaintContext, Quad, ScrollWheelEvent, SizeConstraint, TextLayoutCache, WeakModelHandle,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ordered_float::OrderedFloat;
|
||||
use settings::Settings;
|
||||
use theme::TerminalStyle;
|
||||
use util::ResultExt;
|
||||
|
||||
use std::{cmp::min, ops::Range, rc::Rc, sync::Arc};
|
||||
use std::{cmp::min, ops::Range, sync::Arc};
|
||||
use std::{fmt::Debug, ops::Sub};
|
||||
|
||||
use crate::{
|
||||
color_translation::convert_color, connection::TerminalConnection, ScrollTerminal, ZedListener,
|
||||
};
|
||||
use crate::{color_translation::convert_color, connection::TerminalConnection, ZedListener};
|
||||
|
||||
///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
|
||||
|
@ -258,10 +257,11 @@ impl Element for TerminalEl {
|
|||
for layout_line in &layout.layout_lines {
|
||||
for layout_cell in &layout_line.cells {
|
||||
let position = vec2f(
|
||||
origin.x() + layout_cell.point.column as f32 * layout.em_width.0,
|
||||
(origin.x() + layout_cell.point.column as f32 * layout.em_width.0)
|
||||
.floor(),
|
||||
origin.y() + layout_cell.point.line as f32 * layout.line_height.0,
|
||||
);
|
||||
let size = vec2f(layout.em_width.0, layout.line_height.0);
|
||||
let size = vec2f(layout.em_width.0.ceil(), layout.line_height.0);
|
||||
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: RectF::new(position, size),
|
||||
|
@ -320,7 +320,7 @@ impl Element for TerminalEl {
|
|||
|
||||
//Don't actually know the start_x for a line, until here:
|
||||
let cell_origin = vec2f(
|
||||
origin.x() + point.column as f32 * layout.em_width.0,
|
||||
(origin.x() + point.column as f32 * layout.em_width.0).floor(),
|
||||
origin.y() + point.line as f32 * layout.line_height.0,
|
||||
);
|
||||
|
||||
|
@ -359,25 +359,6 @@ impl Element for TerminalEl {
|
|||
_paint: &mut Self::PaintState,
|
||||
cx: &mut gpui::EventContext,
|
||||
) -> bool {
|
||||
//The problem:
|
||||
//Depending on the terminal mode, we either send an escape sequence
|
||||
//OR update our own data structures.
|
||||
//e.g. scrolling. If we do smooth scrolling, then we need to check if
|
||||
//we own scrolling and then if so, do our scrolling thing.
|
||||
//Ok, so the terminal connection should have APIs for querying it semantically
|
||||
//something like `should_handle_scroll()`. This means we need a handle to the connection.
|
||||
//Actually, this is the only time that this app needs to talk to the outer world.
|
||||
//TODO for scrolling rework: need a way of intercepting Home/End/PageUp etc.
|
||||
//Sometimes going to scroll our own internal buffer, sometimes going to send ESC
|
||||
//
|
||||
//Same goes for key events
|
||||
//Actually, we don't use the terminal at all in dispatch_event code, the view
|
||||
//Handles it all. Check how the editor implements scrolling, is it view-level
|
||||
//or element level?
|
||||
|
||||
//Question: Can we continue dispatching to the view, so it can talk to the connection
|
||||
//Or should we instead add a connection into here?
|
||||
|
||||
match event {
|
||||
Event::ScrollWheel(ScrollWheelEvent {
|
||||
delta, position, ..
|
||||
|
@ -386,17 +367,30 @@ impl Element for TerminalEl {
|
|||
.then(|| {
|
||||
let vertical_scroll =
|
||||
(delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER;
|
||||
cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
|
||||
})
|
||||
.is_some(),
|
||||
Event::KeyDown(KeyDownEvent {
|
||||
input: Some(input), ..
|
||||
}) => cx
|
||||
.is_parent_view_focused()
|
||||
.then(|| {
|
||||
cx.dispatch_action(Input(input.to_string()));
|
||||
|
||||
if let Some(connection) = self.connection.upgrade(cx.app) {
|
||||
connection.update(cx.app, |connection, _| {
|
||||
connection
|
||||
.term
|
||||
.lock()
|
||||
.scroll_display(Scroll::Delta(vertical_scroll.round() as i32));
|
||||
})
|
||||
}
|
||||
})
|
||||
.is_some(),
|
||||
Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
|
||||
if !cx.is_parent_view_focused() {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.connection
|
||||
.upgrade(cx.app)
|
||||
.map(|connection| {
|
||||
connection
|
||||
.update(cx.app, |connection, _| connection.try_keystroke(keystroke))
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -428,14 +422,33 @@ pub fn mouse_to_cell_data(
|
|||
|
||||
///Configures a text style from the current settings.
|
||||
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()
|
||||
.and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
|
||||
.or_else(|| {
|
||||
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);
|
||||
|
||||
TextStyle {
|
||||
color: settings.theme.editor.text_color,
|
||||
font_family_id: settings.buffer_font_family,
|
||||
font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
|
||||
font_family_id: family_id,
|
||||
font_family_name: font_cache.family_name(family_id).unwrap(),
|
||||
font_id: font_cache
|
||||
.select_font(settings.buffer_font_family, &Default::default())
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap(),
|
||||
font_size: settings.buffer_font_size,
|
||||
font_size: settings
|
||||
.terminal_overrides
|
||||
.font_size
|
||||
.or(settings.terminal_defaults.font_size)
|
||||
.unwrap_or(settings.buffer_font_size),
|
||||
font_properties: Default::default(),
|
||||
underline: Default::default(),
|
||||
}
|
||||
|
@ -581,63 +594,76 @@ fn attach_mouse_handlers(
|
|||
let drag_mutex = terminal_mutex.clone();
|
||||
let mouse_down_mutex = terminal_mutex.clone();
|
||||
|
||||
cx.scene.push_mouse_region(MouseRegion {
|
||||
view_id,
|
||||
mouse_down: Some(Rc::new(move |pos, _| {
|
||||
let mut term = mouse_down_mutex.lock();
|
||||
let (point, side) = mouse_to_cell_data(
|
||||
pos,
|
||||
origin,
|
||||
cur_size,
|
||||
term.renderable_content().display_offset,
|
||||
);
|
||||
term.selection = Some(Selection::new(SelectionType::Simple, point, side))
|
||||
})),
|
||||
click: Some(Rc::new(move |pos, click_count, cx| {
|
||||
let mut term = click_mutex.lock();
|
||||
cx.scene.push_mouse_region(
|
||||
MouseRegion::new(view_id, None, visible_bounds)
|
||||
.on_down(
|
||||
MouseButton::Left,
|
||||
move |MouseButtonEvent { position, .. }, _| {
|
||||
let mut term = mouse_down_mutex.lock();
|
||||
|
||||
let (point, side) = mouse_to_cell_data(
|
||||
pos,
|
||||
origin,
|
||||
cur_size,
|
||||
term.renderable_content().display_offset,
|
||||
);
|
||||
let (point, side) = mouse_to_cell_data(
|
||||
position,
|
||||
origin,
|
||||
cur_size,
|
||||
term.renderable_content().display_offset,
|
||||
);
|
||||
term.selection = Some(Selection::new(SelectionType::Simple, point, side))
|
||||
},
|
||||
)
|
||||
.on_click(
|
||||
MouseButton::Left,
|
||||
move |MouseButtonEvent {
|
||||
position,
|
||||
click_count,
|
||||
..
|
||||
},
|
||||
cx| {
|
||||
let mut term = click_mutex.lock();
|
||||
|
||||
let selection_type = match click_count {
|
||||
0 => return, //This is a release
|
||||
1 => Some(SelectionType::Simple),
|
||||
2 => Some(SelectionType::Semantic),
|
||||
3 => Some(SelectionType::Lines),
|
||||
_ => None,
|
||||
};
|
||||
let (point, side) = mouse_to_cell_data(
|
||||
position,
|
||||
origin,
|
||||
cur_size,
|
||||
term.renderable_content().display_offset,
|
||||
);
|
||||
|
||||
let selection =
|
||||
selection_type.map(|selection_type| Selection::new(selection_type, point, side));
|
||||
let selection_type = match click_count {
|
||||
0 => return, //This is a release
|
||||
1 => Some(SelectionType::Simple),
|
||||
2 => Some(SelectionType::Semantic),
|
||||
3 => Some(SelectionType::Lines),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
term.selection = selection;
|
||||
cx.focus_parent_view();
|
||||
cx.notify();
|
||||
})),
|
||||
bounds: visible_bounds,
|
||||
drag: Some(Rc::new(move |_delta, pos, cx| {
|
||||
let mut term = drag_mutex.lock();
|
||||
let selection = selection_type
|
||||
.map(|selection_type| Selection::new(selection_type, point, side));
|
||||
|
||||
let (point, side) = mouse_to_cell_data(
|
||||
pos,
|
||||
origin,
|
||||
cur_size,
|
||||
term.renderable_content().display_offset,
|
||||
);
|
||||
term.selection = selection;
|
||||
cx.focus_parent_view();
|
||||
cx.notify();
|
||||
},
|
||||
)
|
||||
.on_drag(
|
||||
MouseButton::Left,
|
||||
move |_, MouseMovedEvent { position, .. }, cx| {
|
||||
let mut term = drag_mutex.lock();
|
||||
|
||||
if let Some(mut selection) = term.selection.take() {
|
||||
selection.update(point, side);
|
||||
term.selection = Some(selection);
|
||||
}
|
||||
let (point, side) = mouse_to_cell_data(
|
||||
position,
|
||||
origin,
|
||||
cur_size,
|
||||
term.renderable_content().display_offset,
|
||||
);
|
||||
|
||||
cx.notify();
|
||||
})),
|
||||
..Default::default()
|
||||
});
|
||||
if let Some(mut selection) = term.selection.take() {
|
||||
selection.update(point, side);
|
||||
term.selection = Some(selection);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
|
||||
|
|
76
crates/terminal/src/tests/terminal_test_context.rs
Normal file
76
crates/terminal/src/tests/terminal_test_context.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use alacritty_terminal::term::SizeInfo;
|
||||
use gpui::{AppContext, ModelHandle, ReadModelWith, TestAppContext};
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::{
|
||||
connection::TerminalConnection, DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT,
|
||||
DEBUG_TERMINAL_WIDTH,
|
||||
};
|
||||
|
||||
pub struct TerminalTestContext<'a> {
|
||||
pub cx: &'a mut TestAppContext,
|
||||
pub connection: ModelHandle<TerminalConnection>,
|
||||
}
|
||||
|
||||
impl<'a> TerminalTestContext<'a> {
|
||||
pub fn new(cx: &'a mut TestAppContext) -> Self {
|
||||
cx.set_condition_duration(Some(Duration::from_secs(5)));
|
||||
|
||||
let size_info = SizeInfo::new(
|
||||
DEBUG_TERMINAL_WIDTH,
|
||||
DEBUG_TERMINAL_HEIGHT,
|
||||
DEBUG_CELL_WIDTH,
|
||||
DEBUG_LINE_HEIGHT,
|
||||
0.,
|
||||
0.,
|
||||
false,
|
||||
);
|
||||
|
||||
let connection =
|
||||
cx.add_model(|cx| TerminalConnection::new(None, None, None, size_info, cx));
|
||||
|
||||
TerminalTestContext { cx, connection }
|
||||
}
|
||||
|
||||
pub async fn execute_and_wait<F>(&mut self, command: &str, f: F) -> String
|
||||
where
|
||||
F: Fn(String, &AppContext) -> bool,
|
||||
{
|
||||
let command = command.to_string();
|
||||
self.connection.update(self.cx, |connection, _| {
|
||||
connection.write_to_pty(command);
|
||||
connection.write_to_pty("\r".to_string());
|
||||
});
|
||||
|
||||
self.connection
|
||||
.condition(self.cx, |conn, cx| {
|
||||
let content = Self::grid_as_str(conn);
|
||||
f(content, cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
self.cx
|
||||
.read_model_with(&self.connection, &mut |conn, _: &AppContext| {
|
||||
Self::grid_as_str(conn)
|
||||
})
|
||||
}
|
||||
|
||||
fn grid_as_str(connection: &TerminalConnection) -> String {
|
||||
let term = connection.term.lock();
|
||||
let grid_iterator = term.renderable_content().display_iter;
|
||||
let lines = grid_iterator.group_by(|i| i.point.line.0);
|
||||
lines
|
||||
.into_iter()
|
||||
.map(|(_, line)| line.map(|i| i.c).collect::<String>())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for TerminalTestContext<'a> {
|
||||
fn drop(&mut self) {
|
||||
self.cx.set_condition_duration(None);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue