mirror of https://github.com/helix-editor/helix
wip: Compositor
parent
b7a3e525ed
commit
83f2c24115
|
@ -8,6 +8,8 @@ use helix_view::{
|
||||||
Document, Editor, Theme, View,
|
Document, Editor, Theme, View,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::compositor::{Component, Compositor};
|
||||||
|
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -35,23 +37,21 @@ use tui::{
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
};
|
};
|
||||||
|
|
||||||
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
|
|
||||||
|
|
||||||
type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
|
type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
|
||||||
|
|
||||||
const BASE_WIDTH: u16 = 30;
|
const BASE_WIDTH: u16 = 30;
|
||||||
|
|
||||||
pub struct Application<'a> {
|
pub struct Application<'a> {
|
||||||
editor: Editor,
|
|
||||||
prompt: Option<Prompt>,
|
prompt: Option<Prompt>,
|
||||||
terminal: Renderer,
|
|
||||||
|
|
||||||
keymap: Keymaps,
|
compositor: Compositor,
|
||||||
|
renderer: Renderer,
|
||||||
|
|
||||||
executor: &'a smol::Executor<'a>,
|
executor: &'a smol::Executor<'a>,
|
||||||
language_server: helix_lsp::Client,
|
language_server: helix_lsp::Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Renderer {
|
pub(crate) struct Renderer {
|
||||||
size: (u16, u16),
|
size: (u16, u16),
|
||||||
terminal: Terminal,
|
terminal: Terminal,
|
||||||
surface: Surface,
|
surface: Surface,
|
||||||
|
@ -92,7 +92,6 @@ impl Renderer {
|
||||||
// TODO: ideally not &mut View but highlights require it because of cursor cache
|
// TODO: ideally not &mut View but highlights require it because of cursor cache
|
||||||
pub fn render_buffer(&mut self, view: &mut View, viewport: Rect, theme: &Theme) {
|
pub fn render_buffer(&mut self, view: &mut View, viewport: Rect, theme: &Theme) {
|
||||||
let area = Rect::new(0, 0, self.size.0, self.size.1);
|
let area = Rect::new(0, 0, self.size.0, self.size.1);
|
||||||
self.surface.reset(); // reset is faster than allocating new empty surface
|
|
||||||
|
|
||||||
// clear with background color
|
// clear with background color
|
||||||
self.surface.set_style(area, theme.get("ui.background"));
|
self.surface.set_style(area, theme.get("ui.background"));
|
||||||
|
@ -221,8 +220,12 @@ impl Renderer {
|
||||||
|
|
||||||
// TODO: paint cursor heads except primary
|
// TODO: paint cursor heads except primary
|
||||||
|
|
||||||
self.surface
|
self.surface.set_string(
|
||||||
.set_string(OFFSET + visual_x, line, grapheme, style);
|
viewport.x + visual_x,
|
||||||
|
viewport.y + line,
|
||||||
|
grapheme,
|
||||||
|
style,
|
||||||
|
);
|
||||||
|
|
||||||
visual_x += width;
|
visual_x += width;
|
||||||
}
|
}
|
||||||
|
@ -321,7 +324,7 @@ impl Renderer {
|
||||||
.set_string(2, self.size.1 - 1, &prompt.line, self.text_color);
|
.set_string(2, self.size.1 - 1, &prompt.line, self.text_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(&mut self) {
|
pub fn draw_and_swap(&mut self) {
|
||||||
use tui::backend::Backend;
|
use tui::backend::Backend;
|
||||||
// TODO: theres probably a better place for this
|
// TODO: theres probably a better place for this
|
||||||
self.terminal
|
self.terminal
|
||||||
|
@ -363,112 +366,40 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Application<'a> {
|
struct EditorView {
|
||||||
pub fn new(mut args: Args, executor: &'a smol::Executor<'a>) -> Result<Self, Error> {
|
editor: Editor,
|
||||||
let terminal = Renderer::new()?;
|
prompt: Option<Prompt>, // TODO: this is None for now, make a layer
|
||||||
let mut editor = Editor::new();
|
keymap: Keymaps,
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(file) = args.values_of_t::<PathBuf>("files").unwrap().pop() {
|
impl EditorView {
|
||||||
editor.open(file, terminal.size)?;
|
fn new(editor: Editor) -> Self {
|
||||||
}
|
Self {
|
||||||
|
|
||||||
let language_server = helix_lsp::Client::start(&executor, "rust-analyzer", &[]);
|
|
||||||
|
|
||||||
let mut app = Self {
|
|
||||||
editor,
|
editor,
|
||||||
terminal,
|
|
||||||
// TODO; move to state
|
|
||||||
prompt: None,
|
prompt: None,
|
||||||
|
|
||||||
//
|
|
||||||
keymap: keymap::default(),
|
keymap: keymap::default(),
|
||||||
executor,
|
|
||||||
language_server,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(app)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self) {
|
|
||||||
let viewport = Rect::new(OFFSET, 0, self.terminal.size.0, self.terminal.size.1 - 2); // - 2 for statusline and prompt
|
|
||||||
|
|
||||||
// SAFETY: we cheat around the view_mut() borrow because it doesn't allow us to also borrow
|
|
||||||
// theme. Theme is immutable mutating view won't disrupt theme_ref.
|
|
||||||
let theme_ref = unsafe { &*(&self.editor.theme as *const Theme) };
|
|
||||||
if let Some(view) = self.editor.view_mut() {
|
|
||||||
self.terminal.render_view(view, viewport, theme_ref);
|
|
||||||
if let Some(prompt) = &self.prompt {
|
|
||||||
if prompt.should_close {
|
|
||||||
self.prompt = None;
|
|
||||||
} else {
|
|
||||||
self.terminal.render_prompt(view, prompt, theme_ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.terminal.draw();
|
|
||||||
|
|
||||||
// TODO: drop unwrap
|
|
||||||
self.terminal
|
|
||||||
.render_cursor(self.editor.view().unwrap(), self.prompt.as_ref(), viewport);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn event_loop(&mut self) {
|
|
||||||
let mut reader = EventStream::new();
|
|
||||||
|
|
||||||
// initialize lsp
|
|
||||||
self.language_server.initialize().await.unwrap();
|
|
||||||
self.language_server
|
|
||||||
.text_document_did_open(&self.editor.view().unwrap().doc)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
self.render();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if self.editor.should_close {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
use futures_util::{select, FutureExt};
|
|
||||||
select! {
|
|
||||||
event = reader.next().fuse() => {
|
|
||||||
self.handle_terminal_events(event).await
|
|
||||||
}
|
|
||||||
call = self.language_server.incoming.next().fuse() => {
|
|
||||||
self.handle_language_server_message(call).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_terminal_events(
|
impl Component for EditorView {
|
||||||
&mut self,
|
fn handle_event(&mut self, event: Event, executor: &smol::Executor) -> bool {
|
||||||
event: Option<Result<Event, crossterm::ErrorKind>>,
|
|
||||||
) {
|
|
||||||
// Handle key events
|
|
||||||
match event {
|
match event {
|
||||||
Some(Ok(Event::Resize(width, height))) => {
|
Event::Resize(width, height) => {
|
||||||
self.terminal.resize(width, height);
|
|
||||||
|
|
||||||
// TODO: simplistic ensure cursor in view for now
|
// TODO: simplistic ensure cursor in view for now
|
||||||
// TODO: loop over views
|
// TODO: loop over views
|
||||||
if let Some(view) = self.editor.view_mut() {
|
if let Some(view) = self.editor.view_mut() {
|
||||||
view.size = self.terminal.size;
|
view.size = (width, height);
|
||||||
view.ensure_cursor_in_view()
|
view.ensure_cursor_in_view()
|
||||||
};
|
};
|
||||||
|
|
||||||
self.render();
|
|
||||||
}
|
}
|
||||||
Some(Ok(Event::Key(event))) => {
|
Event::Key(event) => {
|
||||||
// if there's a prompt, it takes priority
|
// if there's a prompt, it takes priority
|
||||||
if let Some(prompt) = &mut self.prompt {
|
if let Some(prompt) = &mut self.prompt {
|
||||||
self.prompt
|
self.prompt
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.handle_input(event, &mut self.editor);
|
.handle_input(event, &mut self.editor);
|
||||||
|
|
||||||
self.render();
|
|
||||||
} else if let Some(view) = self.editor.view_mut() {
|
} else if let Some(view) = self.editor.view_mut() {
|
||||||
let keys = vec![event];
|
let keys = vec![event];
|
||||||
// TODO: sequences (`gg`)
|
// TODO: sequences (`gg`)
|
||||||
|
@ -478,7 +409,7 @@ impl<'a> Application<'a> {
|
||||||
if let Some(command) = self.keymap[&Mode::Insert].get(&keys) {
|
if let Some(command) = self.keymap[&Mode::Insert].get(&keys) {
|
||||||
let mut cx = helix_view::commands::Context {
|
let mut cx = helix_view::commands::Context {
|
||||||
view,
|
view,
|
||||||
executor: self.executor,
|
executor: executor,
|
||||||
count: 1,
|
count: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -490,7 +421,7 @@ impl<'a> Application<'a> {
|
||||||
{
|
{
|
||||||
let mut cx = helix_view::commands::Context {
|
let mut cx = helix_view::commands::Context {
|
||||||
view,
|
view,
|
||||||
executor: self.executor,
|
executor: executor,
|
||||||
count: 1,
|
count: 1,
|
||||||
};
|
};
|
||||||
commands::insert::insert_char(&mut cx, c);
|
commands::insert::insert_char(&mut cx, c);
|
||||||
|
@ -557,7 +488,7 @@ impl<'a> Application<'a> {
|
||||||
} else if let Some(command) = self.keymap[&Mode::Normal].get(&keys) {
|
} else if let Some(command) = self.keymap[&Mode::Normal].get(&keys) {
|
||||||
let mut cx = helix_view::commands::Context {
|
let mut cx = helix_view::commands::Context {
|
||||||
view,
|
view,
|
||||||
executor: self.executor,
|
executor: executor,
|
||||||
count: 1,
|
count: 1,
|
||||||
};
|
};
|
||||||
command(&mut cx);
|
command(&mut cx);
|
||||||
|
@ -570,7 +501,7 @@ impl<'a> Application<'a> {
|
||||||
if let Some(command) = self.keymap[&mode].get(&keys) {
|
if let Some(command) = self.keymap[&mode].get(&keys) {
|
||||||
let mut cx = helix_view::commands::Context {
|
let mut cx = helix_view::commands::Context {
|
||||||
view,
|
view,
|
||||||
executor: self.executor,
|
executor: executor,
|
||||||
count: 1,
|
count: 1,
|
||||||
};
|
};
|
||||||
command(&mut cx);
|
command(&mut cx);
|
||||||
|
@ -580,10 +511,119 @@ impl<'a> Application<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.render();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Ok(Event::Mouse(_))) => (), // unhandled
|
Event::Mouse(_) => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn render(&mut self, renderer: &mut Renderer) {
|
||||||
|
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
|
||||||
|
let viewport = Rect::new(OFFSET, 0, renderer.size.0, renderer.size.1 - 2); // - 2 for statusline and prompt
|
||||||
|
|
||||||
|
// SAFETY: we cheat around the view_mut() borrow because it doesn't allow us to also borrow
|
||||||
|
// theme. Theme is immutable mutating view won't disrupt theme_ref.
|
||||||
|
let theme_ref = unsafe { &*(&self.editor.theme as *const Theme) };
|
||||||
|
if let Some(view) = self.editor.view_mut() {
|
||||||
|
renderer.render_view(view, viewport, theme_ref);
|
||||||
|
if let Some(prompt) = &self.prompt {
|
||||||
|
if prompt.should_close {
|
||||||
|
self.prompt = None;
|
||||||
|
} else {
|
||||||
|
renderer.render_prompt(view, prompt, theme_ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: drop unwrap
|
||||||
|
renderer.render_cursor(self.editor.view().unwrap(), self.prompt.as_ref(), viewport);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Application<'a> {
|
||||||
|
pub fn new(mut args: Args, executor: &'a smol::Executor<'a>) -> Result<Self, Error> {
|
||||||
|
let renderer = Renderer::new()?;
|
||||||
|
let mut editor = Editor::new();
|
||||||
|
|
||||||
|
if let Some(file) = args.values_of_t::<PathBuf>("files").unwrap().pop() {
|
||||||
|
editor.open(file, renderer.size)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut compositor = Compositor::new();
|
||||||
|
compositor.push(Box::new(EditorView::new(editor)));
|
||||||
|
|
||||||
|
let language_server = helix_lsp::Client::start(&executor, "rust-analyzer", &[]);
|
||||||
|
|
||||||
|
let mut app = Self {
|
||||||
|
renderer,
|
||||||
|
// TODO; move to state
|
||||||
|
compositor,
|
||||||
|
prompt: None,
|
||||||
|
|
||||||
|
executor,
|
||||||
|
language_server,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self) {
|
||||||
|
// v2:
|
||||||
|
self.renderer.surface.reset(); // reset is faster than allocating new empty surface
|
||||||
|
self.compositor.render(&mut self.renderer); // viewport,
|
||||||
|
self.renderer.draw_and_swap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn event_loop(&mut self) {
|
||||||
|
let mut reader = EventStream::new();
|
||||||
|
|
||||||
|
// initialize lsp
|
||||||
|
self.language_server.initialize().await.unwrap();
|
||||||
|
// TODO: temp
|
||||||
|
// self.language_server
|
||||||
|
// .text_document_did_open(&self.editor.view().unwrap().doc)
|
||||||
|
// .await
|
||||||
|
// .unwrap();
|
||||||
|
|
||||||
|
self.render();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// TODO:
|
||||||
|
// if self.editor.should_close {
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
|
||||||
|
use futures_util::{select, FutureExt};
|
||||||
|
select! {
|
||||||
|
event = reader.next().fuse() => {
|
||||||
|
self.handle_terminal_events(event)
|
||||||
|
}
|
||||||
|
call = self.language_server.incoming.next().fuse() => {
|
||||||
|
self.handle_language_server_message(call).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) {
|
||||||
|
// Handle key events
|
||||||
|
match event {
|
||||||
|
Some(Ok(Event::Resize(width, height))) => {
|
||||||
|
self.renderer.resize(width, height);
|
||||||
|
|
||||||
|
// TODO: use the response
|
||||||
|
self.compositor
|
||||||
|
.handle_event(Event::Resize(width, height), self.executor);
|
||||||
|
|
||||||
|
self.render();
|
||||||
|
}
|
||||||
|
Some(Ok(event)) => {
|
||||||
|
// TODO: use the response
|
||||||
|
self.compositor.handle_event(event, self.executor);
|
||||||
|
|
||||||
|
self.render();
|
||||||
|
}
|
||||||
Some(Err(x)) => panic!(x),
|
Some(Err(x)) => panic!(x),
|
||||||
None => panic!(),
|
None => panic!(),
|
||||||
};
|
};
|
||||||
|
@ -599,11 +639,13 @@ impl<'a> Application<'a> {
|
||||||
match notification {
|
match notification {
|
||||||
Notification::PublishDiagnostics(params) => {
|
Notification::PublishDiagnostics(params) => {
|
||||||
let path = Some(params.uri.to_file_path().unwrap());
|
let path = Some(params.uri.to_file_path().unwrap());
|
||||||
let view = self
|
let view: Option<&mut helix_view::View> = None;
|
||||||
.editor
|
// TODO:
|
||||||
.views
|
// let view = self
|
||||||
.iter_mut()
|
// .editor
|
||||||
.find(|view| view.doc.path == path);
|
// .views
|
||||||
|
// .iter_mut()
|
||||||
|
// .find(|view| view.doc.path == path);
|
||||||
|
|
||||||
if let Some(view) = view {
|
if let Some(view) = view {
|
||||||
let doc = view.doc.text().slice(..);
|
let doc = view.doc.text().slice(..);
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
// Features:
|
||||||
|
// Tracks currently focused component which receives all input
|
||||||
|
// Event loop is external as opposed to cursive-rs
|
||||||
|
// Calls render on the component and translates screen coords to local component coords
|
||||||
|
//
|
||||||
|
// TODO:
|
||||||
|
// Q: where is the Application state stored? do we store it into an external static var?
|
||||||
|
// A: probably makes sense to initialize the editor into a `static Lazy<>` global var.
|
||||||
|
//
|
||||||
|
// Q: how do we composit nested structures? There should be sub-components/views
|
||||||
|
//
|
||||||
|
// Each component declares it's own size constraints and gets fitted based on it's parent.
|
||||||
|
// Q: how does this work with popups?
|
||||||
|
// cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>)
|
||||||
|
|
||||||
|
use crate::application::Renderer;
|
||||||
|
use crossterm::event::Event;
|
||||||
|
use smol::Executor;
|
||||||
|
use tui::buffer::Buffer as Surface;
|
||||||
|
|
||||||
|
pub(crate) trait Component {
|
||||||
|
/// Process input events, return true if handled.
|
||||||
|
fn handle_event(&mut self, event: Event, executor: &Executor) -> bool;
|
||||||
|
// , args: ()
|
||||||
|
|
||||||
|
/// Should redraw? Useful for saving redraw cycles if we know component didn't change.
|
||||||
|
fn should_update(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, renderer: &mut Renderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// struct Editor { };
|
||||||
|
|
||||||
|
// For v1:
|
||||||
|
// Child views are something each view needs to handle on it's own for now, positioning and sizing
|
||||||
|
// options, focus tracking. In practice this is simple: we only will need special solving for
|
||||||
|
// splits etc
|
||||||
|
|
||||||
|
// impl Editor {
|
||||||
|
// fn render(&mut self, surface: &mut Surface, args: ()) {
|
||||||
|
// // compute x, y, w, h rects for sub-views!
|
||||||
|
// // get surface area
|
||||||
|
// // get constraints for textarea, statusbar
|
||||||
|
// // -> cassowary-rs
|
||||||
|
|
||||||
|
// // first render textarea
|
||||||
|
// // then render statusbar
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// usecases to consider:
|
||||||
|
// - a single view with subviews (textarea + statusbar)
|
||||||
|
// - a popup panel / dialog with it's own interactions
|
||||||
|
// - an autocomplete popup that doesn't change focus
|
||||||
|
|
||||||
|
//fn main() {
|
||||||
|
// let root = Editor::new();
|
||||||
|
// let compositor = Compositor::new();
|
||||||
|
|
||||||
|
// compositor.push(root);
|
||||||
|
|
||||||
|
// // pos: clip to bottom of screen
|
||||||
|
// compositor.push_at(pos, Prompt::new(
|
||||||
|
// ":",
|
||||||
|
// (),
|
||||||
|
// |input: &str| match input {}
|
||||||
|
// )); // TODO: this Prompt needs to somehow call compositor.pop() on close, but it can't refer to parent
|
||||||
|
// // Cursive solves this by allowing to return a special result on process_event
|
||||||
|
// // that's either Ignore | Consumed(Opt<C>) where C: fn (Compositor) -> ()
|
||||||
|
|
||||||
|
// // TODO: solve popup focus: we want to push autocomplete popups on top of the current layer
|
||||||
|
// // but retain the focus where it was. The popup will also need to update as we type into the
|
||||||
|
// // textarea. It should also capture certain input, such as tab presses etc
|
||||||
|
// //
|
||||||
|
// // 1) This could be faked by the top layer pushing down edits into the previous layer.
|
||||||
|
// // 2) Alternatively,
|
||||||
|
//}
|
||||||
|
|
||||||
|
pub(crate) struct Compositor {
|
||||||
|
layers: Vec<Box<dyn Component>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Compositor {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { layers: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, layer: Box<dyn Component>) {
|
||||||
|
self.layers.push(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop(&mut self) {
|
||||||
|
self.layers.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_event(&mut self, event: Event, executor: &Executor) -> () {
|
||||||
|
// TODO: custom focus
|
||||||
|
if let Some(layer) = self.layers.last_mut() {
|
||||||
|
layer.handle_event(event, executor);
|
||||||
|
// return should_update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self, renderer: &mut Renderer) {
|
||||||
|
for layer in &mut self.layers {
|
||||||
|
layer.render(renderer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
|
|
||||||
mod application;
|
mod application;
|
||||||
|
mod compositor;
|
||||||
|
|
||||||
use application::Application;
|
use application::Application;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue