diff --git a/Cargo.lock b/Cargo.lock index 4ed2e205..7de0f130 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1990,6 +1990,7 @@ dependencies = [ name = "lapce-proxy" version = "0.0.1" dependencies = [ + "alacritty_terminal", "anyhow", "crossbeam-channel 0.5.1", "git2", diff --git a/core/src/command.rs b/core/src/command.rs index 7354bbb7..5d6a829e 100644 --- a/core/src/command.rs +++ b/core/src/command.rs @@ -3,6 +3,7 @@ use anyhow::Result; use druid::{Point, Rect, Selector, Size, WidgetId}; use indexmap::IndexMap; +use lapce_proxy::terminal::TermId; use lsp_types::{ CodeActionResponse, CompletionItem, CompletionResponse, Location, Position, PublishDiagnosticsParams, Range, TextEdit, @@ -20,6 +21,7 @@ data::EditorKind, editor::{EditorLocation, EditorLocationNew, HighlightTextLayout}, palette::{NewPaletteItem, PaletteType}, + proxy::TerminalContent, split::SplitMoveDirection, state::LapceWorkspace, }; @@ -374,6 +376,7 @@ pub enum LapceUICommand { UpdateLineChanges(BufferId), PublishDiagnostics(PublishDiagnosticsParams), UpdateDiffFiles(Vec), + TerminalUpdateContent(TermId, TerminalContent), ReloadBuffer(BufferId, u64, String), EnsureVisible((Rect, (f64, f64), Option)), EnsureRectVisible(Rect), diff --git a/core/src/data.rs b/core/src/data.rs index 053dcc5a..ff4d8f1c 100644 --- a/core/src/data.rs +++ b/core/src/data.rs @@ -70,6 +70,7 @@ source_control::{SourceControlData, SOURCE_CONTROL_BUFFER}, split::SplitMoveDirection, state::{LapceWorkspace, LapceWorkspaceType, Mode, VisualMode}, + terminal::TerminalSplitData, theme::OldLapceTheme, }; @@ -214,6 +215,7 @@ pub struct LapceTabData { pub workspace: Option>, pub main_split: LapceMainSplitData, pub completion: Arc, + pub terminal: Arc, pub palette: Arc, pub source_control: Arc, pub proxy: Arc, @@ -270,6 +272,8 @@ pub fn new( &config, ); + let terminal = Arc::new(TerminalSplitData::new()); + let mut panels = im::HashMap::new(); panels.insert( PanelPosition::LeftTop, @@ -279,11 +283,20 @@ pub fn new( shown: true, }), ); + panels.insert( + PanelPosition::BottomLeft, + Arc::new(PanelData { + active: terminal.widget_id, + widgets: vec![terminal.widget_id], + shown: true, + }), + ); let mut tab = Self { id: tab_id, workspace: None, main_split, completion, + terminal, source_control, palette, proxy, diff --git a/core/src/proxy.rs b/core/src/proxy.rs index 0f7b39af..9f3863c1 100644 --- a/core/src/proxy.rs +++ b/core/src/proxy.rs @@ -5,11 +5,13 @@ use std::thread; use std::{path::PathBuf, process::Child, sync::Arc}; +use alacritty_terminal::term::cell::Cell; use anyhow::{anyhow, Result}; use crossbeam_utils::sync::WaitGroup; use druid::{ExtEventSink, WidgetId}; use druid::{Target, WindowId}; use lapce_proxy::dispatch::{FileNodeItem, NewBufferResponse}; +use lapce_proxy::terminal::TermId; use lsp_types::CompletionItem; use lsp_types::Position; use lsp_types::PublishDiagnosticsParams; @@ -28,6 +30,8 @@ use crate::state::LapceWorkspaceType; use crate::{buffer::BufferId, command::LAPCE_UI_COMMAND}; +pub type TerminalContent = Vec<(alacritty_terminal::index::Point, Cell)>; + #[derive(Clone)] pub struct LapceProxy { peer: Arc>>, @@ -139,6 +143,15 @@ pub fn new_buffer(&self, buffer_id: BufferId, path: PathBuf) -> Result { return Ok(resp.content); } + pub fn new_terminal(&self, term_id: TermId) { + self.peer.lock().as_ref().unwrap().send_rpc_notification( + "new_terminal", + &json!({ + "term_id": term_id, + }), + ) + } + pub fn update(&self, buffer_id: BufferId, delta: &RopeDelta, rev: u64) { self.peer.lock().as_ref().unwrap().send_rpc_notification( "update", @@ -343,6 +356,10 @@ pub enum Notification { ListDir { items: Vec, }, + TerminalUpdateContent { + id: TermId, + content: TerminalContent, + }, DiffFiles { files: Vec, }, @@ -385,6 +402,7 @@ fn handle_notification( Notification::ListDir { mut items } => {} Notification::DiffFiles { files } => {} Notification::PublishDiagnostics { diagnostics } => {} + Notification::TerminalUpdateContent { id, content } => {} } } @@ -519,6 +537,13 @@ fn handle_notification( Target::Widget(self.tab_id), ); } + Notification::TerminalUpdateContent { id, content } => { + self.event_sink.submit_command( + LAPCE_UI_COMMAND, + LapceUICommand::TerminalUpdateContent(id, content), + Target::Widget(self.tab_id), + ); + } } } diff --git a/core/src/tab.rs b/core/src/tab.rs index 27308e50..eb366f32 100644 --- a/core/src/tab.rs +++ b/core/src/tab.rs @@ -33,6 +33,7 @@ split::LapceSplitNew, state::{LapceWorkspace, LapceWorkspaceType}, status::LapceStatusNew, + terminal::TerminalPanel, }; pub struct LapceTabNew { @@ -73,6 +74,8 @@ pub fn new(data: &LapceTabData) -> Self { data.source_control.widget_id, WidgetPod::new(source_control.boxed()), ); + let terminal = TerminalPanel::new(&data); + panels.insert(data.terminal.widget_id, WidgetPod::new(terminal.boxed())); Self { id: data.id, @@ -186,6 +189,7 @@ fn event( Arc::make_mut(buffer).load_content(content); ctx.set_handled(); } + LapceUICommand::TerminalUpdateContent(id, content) => {} LapceUICommand::UpdateDiffFiles(files) => { let source_control = Arc::make_mut(&mut data.source_control); source_control.diff_files = files diff --git a/core/src/terminal.rs b/core/src/terminal.rs index 08cfab0d..f551dbeb 100644 --- a/core/src/terminal.rs +++ b/core/src/terminal.rs @@ -1,95 +1,107 @@ -use std::{borrow::Cow, collections::HashMap, sync::Arc}; +use std::sync::Arc; -use alacritty_terminal::{ - config::Config, - event::{Event, EventListener, Notify}, - event_loop::{EventLoop, Notifier}, - grid::GridCell, - sync::FairMutex, - term::SizeInfo, - tty, Term, +use druid::{ + BoxConstraints, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, + PaintCtx, Point, Size, UpdateCtx, Widget, WidgetId, WidgetPod, }; -use anyhow::Result; -use crossbeam_channel::{Receiver, Sender}; -use lsp_types::notification; +use lapce_proxy::terminal::TermId; -#[derive(Clone)] -pub struct Terminal { - pub term: Arc>>, - sender: Sender, +use crate::{data::LapceTabData, proxy::LapceProxy, split::LapceSplitNew}; + +pub struct TerminalSplitData { + pub widget_id: WidgetId, + pub split_id: WidgetId, + pub terminals: im::HashMap, } -pub type TermConfig = Config>; - -#[derive(Clone)] -pub struct EventProxy { - sender: Sender, -} - -impl EventProxy {} - -impl EventListener for EventProxy { - fn send_event(&self, event: alacritty_terminal::event::Event) { - self.sender.send(event); - } -} - -impl Terminal { +impl TerminalSplitData { pub fn new() -> Self { - let config = TermConfig::default(); - let (sender, receiver) = crossbeam_channel::unbounded(); - let event_proxy = EventProxy { - sender: sender.clone(), - }; - let size = SizeInfo::new(10.0, 10.0, 1.0, 1.0, 0.0, 0.0, true); - let pty = tty::new(&config, &size, None); - let terminal = Term::new(&config, size, event_proxy.clone()); - let terminal = Arc::new(FairMutex::new(terminal)); - let event_loop = - EventLoop::new(terminal.clone(), event_proxy, pty, false, false); - let loop_tx = event_loop.channel(); - let notifier = Notifier(loop_tx.clone()); - event_loop.spawn(); + let split_id = WidgetId::next(); + let mut terminals = im::HashMap::new(); - let terminal = Terminal { - term: terminal, - sender, - }; - terminal.run(receiver, notifier); - terminal - } - - fn run(&self, receiver: Receiver, notifier: Notifier) { - let term = self.term.clone(); - std::thread::spawn(move || -> Result<()> { - loop { - let event = receiver.recv()?; - match event { - Event::MouseCursorDirty => {} - Event::Title(_) => {} - Event::ResetTitle => {} - Event::ClipboardStore(_, _) => {} - Event::ClipboardLoad(_, _) => {} - Event::ColorRequest(_, _) => {} - Event::PtyWrite(s) => { - notifier.notify(s.into_bytes()); - } - Event::CursorBlinkingChange(_) => {} - Event::Wakeup => { - for cell in term.lock().renderable_content().display_iter { - if !cell.is_empty() { - println!("{:?}", cell); - } - } - } - Event::Bell => {} - Event::Exit => {} - } - } - }); - } - - pub fn insert>(&self, data: B) { - self.sender.send(Event::PtyWrite(data.into())); + Self { + widget_id: WidgetId::next(), + split_id, + terminals, + } + } +} + +pub struct LapceTerminalData { + id: TermId, +} + +impl LapceTerminalData { + pub fn new(proxy: Arc) -> Self { + let id = TermId::next(); + proxy.new_terminal(id); + Self { id } + } +} + +pub struct TerminalPanel { + widget_id: WidgetId, + split: WidgetPod, +} + +impl TerminalPanel { + pub fn new(data: &LapceTabData) -> Self { + let split = LapceSplitNew::new(data.terminal.split_id); + Self { + widget_id: data.terminal.widget_id, + split: WidgetPod::new(split), + } + } +} + +impl Widget for TerminalPanel { + fn id(&self) -> Option { + Some(self.widget_id) + } + + fn event( + &mut self, + ctx: &mut EventCtx, + event: &Event, + data: &mut LapceTabData, + env: &Env, + ) { + self.split.event(ctx, event, data, env); + } + + fn lifecycle( + &mut self, + ctx: &mut LifeCycleCtx, + event: &LifeCycle, + data: &LapceTabData, + env: &Env, + ) { + self.split.lifecycle(ctx, event, data, env); + } + + fn update( + &mut self, + ctx: &mut UpdateCtx, + old_data: &LapceTabData, + data: &LapceTabData, + env: &Env, + ) { + self.split.update(ctx, data, env); + } + + fn layout( + &mut self, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &LapceTabData, + env: &Env, + ) -> Size { + self.split.layout(ctx, bc, data, env); + self.split.set_origin(ctx, data, env, Point::ZERO); + bc.max() + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) { + self.split.paint(ctx, data, env); } } diff --git a/proxy/Cargo.toml b/proxy/Cargo.toml index 74a79015..ae7ede8f 100644 --- a/proxy/Cargo.toml +++ b/proxy/Cargo.toml @@ -5,6 +5,7 @@ authors = ["Dongdong Zhou "] edition = "2018" [dependencies] +alacritty_terminal = "0.15.0" notify = "4.0.16" lapce-rpc = { path = "../rpc" } xi-core-lib = "0.3.0" diff --git a/proxy/src/dispatch.rs b/proxy/src/dispatch.rs index 8d550de7..5c856898 100644 --- a/proxy/src/dispatch.rs +++ b/proxy/src/dispatch.rs @@ -2,6 +2,7 @@ use crate::core_proxy::CoreProxy; use crate::lsp::LspCatalog; use crate::plugin::PluginCatalog; +use crate::terminal::{TermId, Terminal, TerminalEvent}; use anyhow::{anyhow, Result}; use crossbeam_channel::{unbounded, Receiver, Sender}; use git2::{DiffOptions, Oid, Repository}; @@ -31,6 +32,7 @@ pub struct Dispatcher { pub git_sender: Sender<(BufferId, u64)>, pub workspace: Arc>, pub buffers: Arc>>, + pub terminals: Arc>>, open_files: Arc>>, plugins: Arc>, pub lsp: Arc>, @@ -112,6 +114,9 @@ pub enum Notification { delta: RopeDelta, rev: u64, }, + NewTerminal { + term_id: TermId, + }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -212,6 +217,7 @@ pub fn new(sender: Sender) -> Dispatcher { git_sender, workspace: Arc::new(Mutex::new(PathBuf::new())), buffers: Arc::new(Mutex::new(HashMap::new())), + terminals: Arc::new(Mutex::new(HashMap::new())), open_files: Arc::new(Mutex::new(HashMap::new())), plugins: Arc::new(Mutex::new(plugins)), lsp: Arc::new(Mutex::new(LspCatalog::new())), @@ -400,6 +406,27 @@ fn handle_notification(&self, rpc: Notification) { self.lsp.lock().update(buffer, &content_change, buffer.rev); } } + Notification::NewTerminal { term_id } => { + let (terminal, receiver) = Terminal::new(); + self.terminals.lock().insert(term_id, terminal); + + let local_proxy = self.clone(); + thread::spawn(move || -> Result<()> { + loop { + let event = receiver.recv()?; + match event { + TerminalEvent::UpdateContent(content) => local_proxy + .send_notification( + "terminal_update_content", + json!({ + "id": term_id, + "content": content, + }), + ), + } + } + }); + } } } diff --git a/proxy/src/lib.rs b/proxy/src/lib.rs index 38fb5789..097397d3 100644 --- a/proxy/src/lib.rs +++ b/proxy/src/lib.rs @@ -3,6 +3,7 @@ pub mod dispatch; pub mod lsp; pub mod plugin; +pub mod terminal; use dispatch::Dispatcher; diff --git a/proxy/src/terminal.rs b/proxy/src/terminal.rs new file mode 100644 index 00000000..092423f2 --- /dev/null +++ b/proxy/src/terminal.rs @@ -0,0 +1,143 @@ +use std::{ + borrow::Cow, + collections::HashMap, + sync::{ + atomic::{self, AtomicU64}, + Arc, + }, +}; + +use alacritty_terminal::{ + config::Config, + event::{Event, EventListener, Notify}, + event_loop::{EventLoop, Notifier}, + grid::GridCell, + index::Point, + sync::FairMutex, + term::{cell::Cell, SizeInfo}, + tty, Term, +}; +use anyhow::Result; +use crossbeam_channel::{Receiver, Sender}; +use lsp_types::notification; +use serde::{Deserialize, Deserializer, Serialize}; + +pub struct Counter(AtomicU64); + +impl Counter { + pub const fn new() -> Counter { + Counter(AtomicU64::new(1)) + } + + pub fn next(&self) -> u64 { + self.0.fetch_add(1, atomic::Ordering::Relaxed) + } +} + +#[derive(Eq, PartialEq, Hash, Copy, Clone, Debug, Serialize, Deserialize)] +pub struct TermId(pub u64); + +impl TermId { + pub fn next() -> Self { + static TERM_ID_COUNTER: Counter = Counter::new(); + Self(TERM_ID_COUNTER.next()) + } +} + +pub enum TerminalEvent { + UpdateContent(Vec<(Point, Cell)>), +} + +#[derive(Clone)] +pub struct Terminal { + pub term: Arc>>, + sender: Sender, + host_sender: Sender, +} + +pub type TermConfig = Config>; + +#[derive(Clone)] +pub struct EventProxy { + sender: Sender, +} + +impl EventProxy {} + +impl EventListener for EventProxy { + fn send_event(&self, event: alacritty_terminal::event::Event) { + self.sender.send(event); + } +} + +impl Terminal { + pub fn new() -> (Self, Receiver) { + let config = TermConfig::default(); + let (sender, receiver) = crossbeam_channel::unbounded(); + let event_proxy = EventProxy { + sender: sender.clone(), + }; + let size = SizeInfo::new(10.0, 10.0, 1.0, 1.0, 0.0, 0.0, true); + let pty = tty::new(&config, &size, None); + let terminal = Term::new(&config, size, event_proxy.clone()); + let terminal = Arc::new(FairMutex::new(terminal)); + let event_loop = + EventLoop::new(terminal.clone(), event_proxy, pty, false, false); + let loop_tx = event_loop.channel(); + let notifier = Notifier(loop_tx.clone()); + event_loop.spawn(); + + let (host_sender, host_receiver) = crossbeam_channel::unbounded(); + let terminal = Terminal { + term: terminal, + sender, + host_sender, + }; + terminal.run(receiver, notifier); + (terminal, host_receiver) + } + + fn run(&self, receiver: Receiver, notifier: Notifier) { + let term = self.term.clone(); + let host_sender = self.host_sender.clone(); + std::thread::spawn(move || -> Result<()> { + loop { + let event = receiver.recv()?; + match event { + Event::MouseCursorDirty => {} + Event::Title(_) => {} + Event::ResetTitle => {} + Event::ClipboardStore(_, _) => {} + Event::ClipboardLoad(_, _) => {} + Event::ColorRequest(_, _) => {} + Event::PtyWrite(s) => { + notifier.notify(s.into_bytes()); + } + Event::CursorBlinkingChange(_) => {} + Event::Wakeup => { + let content = term + .lock() + .renderable_content() + .display_iter + .filter_map(|c| { + if c.is_empty() { + None + } else { + Some((c.point, c.cell.clone())) + } + }) + .collect::>(); + let event = TerminalEvent::UpdateContent(content); + host_sender.send(event); + } + Event::Bell => {} + Event::Exit => {} + } + } + }); + } + + pub fn insert>(&self, data: B) { + self.sender.send(Event::PtyWrite(data.into())); + } +}