aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/application.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/application.rs')
-rw-r--r--helix-term/src/application.rs579
1 files changed, 62 insertions, 517 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 141779ec..dc37612a 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -1,17 +1,13 @@
use clap::ArgMatches as Args;
-use helix_core::{indent::TAB_WIDTH, syntax::HighlightEvent, Position, Range, State};
-use helix_view::{
- commands,
- document::Mode,
- keymap::{self, Keymaps},
- prompt::Prompt,
- Document, Editor, Theme, View,
-};
+
+use helix_view::{document::Mode, Document, Editor, Theme, View};
+
+use crate::compositor::Compositor;
+use crate::ui;
use log::{debug, info};
use std::{
- borrow::Cow,
io::{self, stdout, Stdout, Write},
path::PathBuf,
time::Duration,
@@ -22,366 +18,44 @@ use smol::prelude::*;
use anyhow::Error;
use crossterm::{
- cursor,
- event::{read, Event, EventStream, KeyCode, KeyEvent},
- execute, queue,
- terminal::{self, disable_raw_mode, enable_raw_mode},
+ event::{Event, EventStream},
+ execute, terminal,
};
-use tui::{
- backend::CrosstermBackend,
- buffer::Buffer as Surface,
- layout::Rect,
- style::{Color, Modifier, Style},
-};
-
-const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
-
-type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
+use tui::{backend::CrosstermBackend, layout::Rect};
-const BASE_WIDTH: u16 = 30;
+type Terminal = crate::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
-pub struct Application<'a> {
+pub struct Application {
+ compositor: Compositor,
editor: Editor,
- prompt: Option<Prompt>,
- terminal: Renderer,
+ terminal: Terminal,
- keymap: Keymaps,
- executor: &'a smol::Executor<'a>,
+ executor: &'static smol::Executor<'static>,
language_server: helix_lsp::Client,
}
-struct Renderer {
- size: (u16, u16),
- terminal: Terminal,
- surface: Surface,
- cache: Surface,
- text_color: Style,
-}
-
-impl Renderer {
- pub fn new() -> Result<Self, Error> {
+impl Application {
+ pub fn new(mut args: Args, executor: &'static smol::Executor<'static>) -> Result<Self, Error> {
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
- let size = terminal::size().unwrap();
- let text_color: Style = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
-
- let area = Rect::new(0, 0, size.0, size.1);
-
- Ok(Self {
- size,
- terminal,
- surface: Surface::empty(area),
- cache: Surface::empty(area),
- text_color,
- })
- }
-
- pub fn resize(&mut self, width: u16, height: u16) {
- self.size = (width, height);
- let area = Rect::new(0, 0, width, height);
- self.surface = Surface::empty(area);
- self.cache = Surface::empty(area);
- }
-
- pub fn render_view(&mut self, view: &mut View, viewport: Rect, theme: &Theme) {
- self.render_buffer(view, viewport, theme);
- self.render_statusline(view, theme);
- }
-
- // 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) {
- 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
- self.surface.set_style(area, theme.get("ui.background"));
-
- // TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
- let source_code = view.doc.text().to_string();
-
- let last_line = view.last_line();
-
- let range = {
- // calculate viewport byte ranges
- let start = view.doc.text().line_to_byte(view.first_line);
- let end = view.doc.text().line_to_byte(last_line)
- + view.doc.text().line(last_line).len_bytes();
-
- start..end
- };
-
- // TODO: range doesn't actually restrict source, just highlight range
- // TODO: cache highlight results
- // TODO: only recalculate when state.doc is actually modified
- let highlights: Vec<_> = match view.doc.syntax.as_mut() {
- Some(syntax) => {
- syntax
- .highlight_iter(source_code.as_bytes(), Some(range), None, |_| None)
- .unwrap()
- .collect() // TODO: we collect here to avoid double borrow, fix later
- }
- None => vec![Ok(HighlightEvent::Source {
- start: range.start,
- end: range.end,
- })],
- };
- let mut spans = Vec::new();
- let mut visual_x = 0;
- let mut line = 0u16;
- let visible_selections: Vec<Range> = view
- .doc
- .state
- .selection()
- .ranges()
- .iter()
- // TODO: limit selection to one in viewport
- // .filter(|range| !range.is_empty()) // && range.overlaps(&Range::new(start, end + 1))
- .copied()
- .collect();
-
- 'outer: for event in highlights {
- match event.unwrap() {
- HighlightEvent::HighlightStart(span) => {
- spans.push(span);
- }
- HighlightEvent::HighlightEnd => {
- spans.pop();
- }
- HighlightEvent::Source { start, end } => {
- // TODO: filter out spans out of viewport for now..
-
- let start = view.doc.text().byte_to_char(start);
- let end = view.doc.text().byte_to_char(end); // <-- index 744, len 743
-
- let text = view.doc.text().slice(start..end);
-
- use helix_core::graphemes::{grapheme_width, RopeGraphemes};
-
- let style = match spans.first() {
- Some(span) => theme.get(theme.scopes()[span.0].as_str()),
- None => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender
- };
-
- // TODO: we could render the text to a surface, then cache that, that
- // way if only the selection/cursor changes we can copy from cache
- // and paint the new cursor.
-
- let mut char_index = start;
-
- // iterate over range char by char
- for grapheme in RopeGraphemes::new(&text) {
- // TODO: track current char_index
-
- if grapheme == "\n" {
- visual_x = 0;
- line += 1;
-
- // TODO: with proper iter this shouldn't be necessary
- if line >= viewport.height {
- break 'outer;
- }
- } else if grapheme == "\t" {
- visual_x += (TAB_WIDTH as u16);
- } else {
- // Cow will prevent allocations if span contained in a single slice
- // which should really be the majority case
- let grapheme = Cow::from(grapheme);
- let width = grapheme_width(&grapheme) as u16;
-
- // TODO: this should really happen as an after pass
- let style = if visible_selections
- .iter()
- .any(|range| range.contains(char_index))
- {
- // cedar
- style.clone().bg(Color::Rgb(128, 47, 0))
- } else {
- style
- };
-
- let style = if visible_selections
- .iter()
- .any(|range| range.head == char_index)
- {
- style.clone().bg(Color::Rgb(255, 255, 255))
- } else {
- style
- };
-
- // ugh, improve with a traverse method
- // or interleave highlight spans with selection and diagnostic spans
- let style = if view.doc.diagnostics.iter().any(|diagnostic| {
- diagnostic.range.0 <= char_index && diagnostic.range.1 > char_index
- }) {
- style.clone().add_modifier(Modifier::UNDERLINED)
- } else {
- style
- };
-
- // TODO: paint cursor heads except primary
-
- self.surface
- .set_string(OFFSET + visual_x, line, grapheme, style);
-
- visual_x += width;
- }
-
- char_index += 1;
- }
- }
- }
- }
-
- let style: Style = theme.get("ui.linenr");
- let warning: Style = theme.get("warning");
- let last_line = view.last_line();
- for (i, line) in (view.first_line..last_line).enumerate() {
- if view.doc.diagnostics.iter().any(|d| d.line == line) {
- self.surface.set_stringn(0, i as u16, "●", 1, warning);
- }
-
- self.surface
- .set_stringn(1, i as u16, format!("{:>5}", line + 1), 5, style);
- }
- }
-
- pub fn render_statusline(&mut self, view: &View, theme: &Theme) {
- let mode = match view.doc.mode() {
- Mode::Insert => "INS",
- Mode::Normal => "NOR",
- Mode::Goto => "GOTO",
- };
- // statusline
- self.surface.set_style(
- Rect::new(0, self.size.1 - 2, self.size.0, 1),
- theme.get("ui.statusline"),
- );
- self.surface
- .set_string(1, self.size.1 - 2, mode, self.text_color);
-
- self.surface.set_string(
- self.size.0 - 10,
- self.size.1 - 2,
- format!("{}", view.doc.diagnostics.len()),
- self.text_color,
- );
- }
-
- pub fn render_prompt(&mut self, view: &View, prompt: &Prompt, theme: &Theme) {
- // completion
- if !prompt.completion.is_empty() {
- // TODO: find out better way of clearing individual lines of the screen
- let mut row = 0;
- let mut col = 0;
- let max_col = self.size.0 / BASE_WIDTH;
- let col_height = ((prompt.completion.len() as u16 + max_col - 1) / max_col);
-
- for i in (3..col_height + 3) {
- self.surface.set_string(
- 0,
- self.size.1 - i as u16,
- " ".repeat(self.size.0 as usize),
- self.text_color,
- );
- }
- self.surface.set_style(
- Rect::new(0, self.size.1 - col_height - 2, self.size.0, col_height),
- theme.get("ui.statusline"),
- );
- for (i, command) in prompt.completion.iter().enumerate() {
- let color = if prompt.completion_selection_index.is_some()
- && i == prompt.completion_selection_index.unwrap()
- {
- Style::default().bg(Color::Rgb(104, 060, 232))
- } else {
- self.text_color
- };
- self.surface.set_stringn(
- 1 + col * BASE_WIDTH,
- self.size.1 - col_height - 2 + row,
- &command,
- BASE_WIDTH as usize - 1,
- color,
- );
- row += 1;
- if row > col_height - 1 {
- row = 0;
- col += 1;
- }
- if col > max_col {
- break;
- }
- }
- }
- // render buffer text
- self.surface
- .set_string(1, self.size.1 - 1, &prompt.prompt, self.text_color);
- self.surface
- .set_string(2, self.size.1 - 1, &prompt.line, self.text_color);
- }
-
- pub fn draw(&mut self) {
- use tui::backend::Backend;
- // TODO: theres probably a better place for this
- self.terminal
- .backend_mut()
- .draw(self.cache.diff(&self.surface).into_iter());
- // swap the buffer
- std::mem::swap(&mut self.surface, &mut self.cache);
- }
-
- pub fn render_cursor(&mut self, view: &View, prompt: Option<&Prompt>, viewport: Rect) {
- let mut stdout = stdout();
- match view.doc.mode() {
- Mode::Insert => write!(stdout, "\x1B[6 q"),
- mode => write!(stdout, "\x1B[2 q"),
- };
- let pos = if let Some(prompt) = prompt {
- Position::new(self.size.0 as usize, 2 + prompt.cursor)
- } else {
- if let Some(path) = view.doc.path() {
- self.surface.set_string(
- 6,
- self.size.1 - 1,
- path.to_string_lossy(),
- self.text_color,
- );
- }
-
- let cursor = view.doc.state.selection().cursor();
-
- let mut pos = view
- .screen_coords_at_pos(&view.doc.text().slice(..), cursor)
- .expect("Cursor is out of bounds.");
- pos.col += viewport.x as usize;
- pos.row += viewport.y as usize;
- pos
- };
-
- execute!(stdout, cursor::MoveTo(pos.col as u16, pos.row as u16));
- }
-}
-
-impl<'a> Application<'a> {
- pub fn new(mut args: Args, executor: &'a smol::Executor<'a>) -> Result<Self, Error> {
- let terminal = Renderer::new()?;
let mut editor = Editor::new();
+ let size = terminal.size()?;
if let Some(file) = args.values_of_t::<PathBuf>("files").unwrap().pop() {
- editor.open(file, terminal.size)?;
+ editor.open(file, (size.width, size.height))?;
}
+ let mut compositor = Compositor::new();
+ compositor.push(Box::new(ui::EditorView::new()));
+
let language_server = helix_lsp::Client::start(&executor, "rust-analyzer", &[]);
let mut app = Self {
editor,
terminal,
- // TODO; move to state
- prompt: None,
+ compositor,
- //
- keymap: keymap::default(),
executor,
language_server,
};
@@ -390,27 +64,18 @@ impl<'a> Application<'a> {
}
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);
- }
- }
- }
+ let executor = &self.executor;
+ let editor = &mut self.editor;
+ let compositor = &self.compositor;
- self.terminal.draw();
+ let mut cx = crate::compositor::Context { editor, executor };
+ let area = self.terminal.size().unwrap();
- // TODO: drop unwrap
- self.terminal
- .render_cursor(self.editor.view().unwrap(), self.prompt.as_ref(), viewport);
+ compositor.render(area, self.terminal.current_buffer_mut(), &mut cx);
+ let pos = compositor.cursor_position(area, &mut cx);
+
+ self.terminal.draw();
+ self.terminal.set_cursor(pos.col as u16, pos.row as u16);
}
pub async fn event_loop(&mut self) {
@@ -418,10 +83,11 @@ impl<'a> Application<'a> {
// initialize lsp
self.language_server.initialize().await.unwrap();
- self.language_server
- .text_document_did_open(&self.editor.view().unwrap().doc)
- .await
- .unwrap();
+ // TODO: temp
+ // self.language_server
+ // .text_document_did_open(&cx.editor.view().unwrap().doc)
+ // .await
+ // .unwrap();
self.render();
@@ -433,7 +99,7 @@ impl<'a> Application<'a> {
use futures_util::{select, FutureExt};
select! {
event = reader.next().fuse() => {
- self.handle_terminal_events(event).await
+ self.handle_terminal_events(event)
}
call = self.language_server.incoming.next().fuse() => {
self.handle_language_server_message(call).await
@@ -442,151 +108,28 @@ impl<'a> Application<'a> {
}
}
- pub async fn handle_terminal_events(
- &mut self,
- event: Option<Result<Event, crossterm::ErrorKind>>,
- ) {
+ pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) {
+ let mut cx = crate::compositor::Context {
+ editor: &mut self.editor,
+ executor: &self.executor,
+ };
// Handle key events
- match event {
+ let should_redraw = match event {
Some(Ok(Event::Resize(width, height))) => {
- self.terminal.resize(width, height);
-
- // TODO: simplistic ensure cursor in view for now
- // TODO: loop over views
- if let Some(view) = self.editor.view_mut() {
- view.size = self.terminal.size;
- view.ensure_cursor_in_view()
- };
+ self.terminal.resize(Rect::new(0, 0, width, height));
- self.render();
- }
- Some(Ok(Event::Key(event))) => {
- // if there's a prompt, it takes priority
- if let Some(prompt) = &mut self.prompt {
- self.prompt
- .as_mut()
- .unwrap()
- .handle_input(event, &mut self.editor);
-
- self.render();
- } else if let Some(view) = self.editor.view_mut() {
- let keys = vec![event];
- // TODO: sequences (`gg`)
- // TODO: handle count other than 1
- match view.doc.mode() {
- Mode::Insert => {
- if let Some(command) = self.keymap[&Mode::Insert].get(&keys) {
- let mut cx = helix_view::commands::Context {
- view,
- executor: self.executor,
- count: 1,
- };
-
- command(&mut cx);
- } else if let KeyEvent {
- code: KeyCode::Char(c),
- ..
- } = event
- {
- let mut cx = helix_view::commands::Context {
- view,
- executor: self.executor,
- count: 1,
- };
- commands::insert::insert_char(&mut cx, c);
- }
- view.ensure_cursor_in_view();
- }
- Mode::Normal => {
- if let &[KeyEvent {
- code: KeyCode::Char(':'),
- ..
- }] = keys.as_slice()
- {
- let prompt = Prompt::new(
- ":".to_owned(),
- |_input: &str| {
- // TODO: i need this duplicate list right now to avoid borrow checker issues
- let command_list = vec![
- String::from("q"),
- String::from("aaa"),
- String::from("bbb"),
- String::from("ccc"),
- String::from("ddd"),
- String::from("eee"),
- String::from("averylongcommandaverylongcommandaverylongcommandaverylongcommandaverylongcommand"),
- String::from("q"),
- String::from("aaa"),
- String::from("bbb"),
- String::from("ccc"),
- String::from("ddd"),
- String::from("eee"),
- String::from("q"),
- String::from("aaa"),
- String::from("bbb"),
- String::from("ccc"),
- String::from("ddd"),
- String::from("eee"),
- String::from("q"),
- String::from("aaa"),
- String::from("bbb"),
- String::from("ccc"),
- String::from("ddd"),
- String::from("eee"),
- String::from("q"),
- String::from("aaa"),
- String::from("bbb"),
- String::from("ccc"),
- String::from("ddd"),
- String::from("eee"),
- ];
- command_list
- .into_iter()
- .filter(|command| command.contains(_input))
- .collect()
- }, // completion
- |editor: &mut Editor, input: &str| match input {
- "q" => editor.should_close = true,
- _ => (),
- },
- );
-
- self.prompt = Some(prompt);
-
- // HAXX: special casing for command mode
- } else if let Some(command) = self.keymap[&Mode::Normal].get(&keys) {
- let mut cx = helix_view::commands::Context {
- view,
- executor: self.executor,
- count: 1,
- };
- command(&mut cx);
-
- // TODO: simplistic ensure cursor in view for now
- view.ensure_cursor_in_view();
- }
- }
- mode => {
- if let Some(command) = self.keymap[&mode].get(&keys) {
- let mut cx = helix_view::commands::Context {
- view,
- executor: self.executor,
- count: 1,
- };
- command(&mut cx);
-
- // TODO: simplistic ensure cursor in view for now
- view.ensure_cursor_in_view();
- }
- }
- }
- self.render();
- }
+ self.compositor
+ .handle_event(Event::Resize(width, height), &mut cx)
}
- Some(Ok(Event::Mouse(_))) => (), // unhandled
+ Some(Ok(event)) => self.compositor.handle_event(event, &mut cx),
Some(Err(x)) => panic!(x),
None => panic!(),
};
+
+ if should_redraw {
+ self.render();
+ // calling render twice here fixes it for some reason
+ }
}
pub async fn handle_language_server_message(&mut self, call: Option<helix_lsp::Call>) {
@@ -599,11 +142,13 @@ impl<'a> Application<'a> {
match notification {
Notification::PublishDiagnostics(params) => {
let path = Some(params.uri.to_file_path().unwrap());
- let view = self
- .editor
- .views
- .iter_mut()
- .find(|view| view.doc.path == path);
+ let view: Option<&mut helix_view::View> = None;
+ // TODO:
+ // let view = self
+ // .editor
+ // .views
+ // .iter_mut()
+ // .find(|view| view.doc.path == path);
if let Some(view) = view {
let doc = view.doc.text().slice(..);
@@ -653,7 +198,7 @@ impl<'a> Application<'a> {
}
pub async fn run(&mut self) -> Result<(), Error> {
- enable_raw_mode()?;
+ terminal::enable_raw_mode()?;
let mut stdout = stdout();
@@ -663,7 +208,7 @@ impl<'a> Application<'a> {
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
execute!(std::io::stdout(), terminal::LeaveAlternateScreen);
- disable_raw_mode();
+ terminal::disable_raw_mode();
hook(info);
}));
@@ -674,7 +219,7 @@ impl<'a> Application<'a> {
execute!(stdout, terminal::LeaveAlternateScreen)?;
- disable_raw_mode()?;
+ terminal::disable_raw_mode()?;
Ok(())
}