@@ -1,17 +1,13 @@
11use clap::ArgMatches as Args;
2−use helix_core::{indent::TAB_WIDTH, syntax::HighlightEvent, Position, Range, State};
3−use helix_view::{
4− commands,
5− document::Mode,
6− keymap::{self, Keymaps},
7− prompt::Prompt,
8− Document, Editor, Theme, View,
9−};
2+
3+use helix_view::{document::Mode, Document, Editor, Theme, View};
4+
5+use crate::compositor::Compositor;
6+use crate::ui;
107
118use log::{debug, info};
129
1310use std::{
14− borrow::Cow,
1511 io::{self, stdout, Stdout, Write},
1612 path::PathBuf,
1713 time::Duration,
@@ -22,366 +18,44 @@ use smol::prelude::*;
2218use anyhow::Error;
2319
2420use crossterm::{
25− cursor,
26− event::{read, Event, EventStream, KeyCode, KeyEvent},
27− execute, queue,
28− terminal::{self, disable_raw_mode, enable_raw_mode},
21+ event::{Event, EventStream},
22+ execute, terminal,
2923};
3024
31−use tui::{
32− backend::CrosstermBackend,
33− buffer::Buffer as Surface,
34− layout::Rect,
35− style::{Color, Modifier, Style},
36−};
37−
38−const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
39−
40−type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
25+use tui::{backend::CrosstermBackend, layout::Rect};
4126
42−const BASE_WIDTH: u16 = 30;
27+type Terminal = crate::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
4328
44−pub struct Application<'a> {
29+pub struct Application {
30+ compositor: Compositor,
4531 editor: Editor,
46− prompt: Option<Prompt>,
47− terminal: Renderer,
32+ terminal: Terminal,
4833
49− keymap: Keymaps,
50− executor: &'a smol::Executor<'a>,
34+ executor: &'static smol::Executor<'static>,
5135 language_server: helix_lsp::Client,
5236}
5337
54−struct Renderer {
55− size: (u16, u16),
56− terminal: Terminal,
57− surface: Surface,
58− cache: Surface,
59− text_color: Style,
60−}
61−
62−impl Renderer {
63− pub fn new() -> Result<Self, Error> {
38+impl Application {
39+ pub fn new(mut args: Args, executor: &'static smol::Executor<'static>) -> Result<Self, Error> {
6440 let backend = CrosstermBackend::new(stdout());
6541 let mut terminal = Terminal::new(backend)?;
66− let size = terminal::size().unwrap();
67− let text_color: Style = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
68−
69− let area = Rect::new(0, 0, size.0, size.1);
70−
71− Ok(Self {
72− size,
73− terminal,
74− surface: Surface::empty(area),
75− cache: Surface::empty(area),
76− text_color,
77− })
78− }
79−
80− pub fn resize(&mut self, width: u16, height: u16) {
81− self.size = (width, height);
82− let area = Rect::new(0, 0, width, height);
83− self.surface = Surface::empty(area);
84− self.cache = Surface::empty(area);
85− }
86−
87− pub fn render_view(&mut self, view: &mut View, viewport: Rect, theme: &Theme) {
88− self.render_buffer(view, viewport, theme);
89− self.render_statusline(view, theme);
90− }
91−
92− // TODO: ideally not &mut View but highlights require it because of cursor cache
93− pub fn render_buffer(&mut self, view: &mut View, viewport: Rect, theme: &Theme) {
94− let area = Rect::new(0, 0, self.size.0, self.size.1);
95− self.surface.reset(); // reset is faster than allocating new empty surface
96−
97− // clear with background color
98− self.surface.set_style(area, theme.get("ui.background"));
99−
100− // TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
101− let source_code = view.doc.text().to_string();
102−
103− let last_line = view.last_line();
104−
105− let range = {
106− // calculate viewport byte ranges
107− let start = view.doc.text().line_to_byte(view.first_line);
108− let end = view.doc.text().line_to_byte(last_line)
109− + view.doc.text().line(last_line).len_bytes();
110−
111− start..end
112− };
113−
114− // TODO: range doesn't actually restrict source, just highlight range
115− // TODO: cache highlight results
116− // TODO: only recalculate when state.doc is actually modified
117− let highlights: Vec<_> = match view.doc.syntax.as_mut() {
118− Some(syntax) => {
119− syntax
120− .highlight_iter(source_code.as_bytes(), Some(range), None, |_| None)
121− .unwrap()
122− .collect() // TODO: we collect here to avoid double borrow, fix later
123− }
124− None => vec![Ok(HighlightEvent::Source {
125− start: range.start,
126− end: range.end,
127− })],
128− };
129− let mut spans = Vec::new();
130− let mut visual_x = 0;
131− let mut line = 0u16;
132− let visible_selections: Vec<Range> = view
133− .doc
134− .state
135− .selection()
136− .ranges()
137− .iter()
138− // TODO: limit selection to one in viewport
139− // .filter(|range| !range.is_empty()) // && range.overlaps(&Range::new(start, end + 1))
140− .copied()
141− .collect();
142−
143− 'outer: for event in highlights {
144− match event.unwrap() {
145− HighlightEvent::HighlightStart(span) => {
146− spans.push(span);
147− }
148− HighlightEvent::HighlightEnd => {
149− spans.pop();
150− }
151− HighlightEvent::Source { start, end } => {
152− // TODO: filter out spans out of viewport for now..
153−
154− let start = view.doc.text().byte_to_char(start);
155− let end = view.doc.text().byte_to_char(end); // <-- index 744, len 743
156−
157− let text = view.doc.text().slice(start..end);
158−
159− use helix_core::graphemes::{grapheme_width, RopeGraphemes};
160−
161− let style = match spans.first() {
162− Some(span) => theme.get(theme.scopes()[span.0].as_str()),
163− None => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender
164− };
165−
166− // TODO: we could render the text to a surface, then cache that, that
167− // way if only the selection/cursor changes we can copy from cache
168− // and paint the new cursor.
169−
170− let mut char_index = start;
171−
172− // iterate over range char by char
173− for grapheme in RopeGraphemes::new(&text) {
174− // TODO: track current char_index
175−
176− if grapheme == "\n" {
177− visual_x = 0;
178− line += 1;
179−
180− // TODO: with proper iter this shouldn't be necessary
181− if line >= viewport.height {
182− break 'outer;
183− }
184− } else if grapheme == "\t" {
185− visual_x += (TAB_WIDTH as u16);
186− } else {
187− // Cow will prevent allocations if span contained in a single slice
188− // which should really be the majority case
189− let grapheme = Cow::from(grapheme);
190− let width = grapheme_width(&grapheme) as u16;
191−
192− // TODO: this should really happen as an after pass
193− let style = if visible_selections
194− .iter()
195− .any(|range| range.contains(char_index))
196− {
197− // cedar
198− style.clone().bg(Color::Rgb(128, 47, 0))
199− } else {
200− style
201− };
202−
203− let style = if visible_selections
204− .iter()
205− .any(|range| range.head == char_index)
206− {
207− style.clone().bg(Color::Rgb(255, 255, 255))
208− } else {
209− style
210− };
211−
212− // ugh, improve with a traverse method
213− // or interleave highlight spans with selection and diagnostic spans
214− let style = if view.doc.diagnostics.iter().any(|diagnostic| {
215− diagnostic.range.0 <= char_index && diagnostic.range.1 > char_index
216− }) {
217− style.clone().add_modifier(Modifier::UNDERLINED)
218− } else {
219− style
220− };
221−
222− // TODO: paint cursor heads except primary
223−
224− self.surface
225− .set_string(OFFSET + visual_x, line, grapheme, style);
226−
227− visual_x += width;
228− }
229−
230− char_index += 1;
231− }
232− }
233− }
234− }
235−
236− let style: Style = theme.get("ui.linenr");
237− let warning: Style = theme.get("warning");
238− let last_line = view.last_line();
239− for (i, line) in (view.first_line..last_line).enumerate() {
240− if view.doc.diagnostics.iter().any(|d| d.line == line) {
241− self.surface.set_stringn(0, i as u16, "●", 1, warning);
242− }
243−
244− self.surface
245− .set_stringn(1, i as u16, format!("{:>5}", line + 1), 5, style);
246− }
247− }
248−
249− pub fn render_statusline(&mut self, view: &View, theme: &Theme) {
250− let mode = match view.doc.mode() {
251− Mode::Insert => "INS",
252− Mode::Normal => "NOR",
253− Mode::Goto => "GOTO",
254− };
255− // statusline
256− self.surface.set_style(
257− Rect::new(0, self.size.1 - 2, self.size.0, 1),
258− theme.get("ui.statusline"),
259− );
260− self.surface
261− .set_string(1, self.size.1 - 2, mode, self.text_color);
262−
263− self.surface.set_string(
264− self.size.0 - 10,
265− self.size.1 - 2,
266− format!("{}", view.doc.diagnostics.len()),
267− self.text_color,
268− );
269− }
270−
271− pub fn render_prompt(&mut self, view: &View, prompt: &Prompt, theme: &Theme) {
272− // completion
273− if !prompt.completion.is_empty() {
274− // TODO: find out better way of clearing individual lines of the screen
275− let mut row = 0;
276− let mut col = 0;
277− let max_col = self.size.0 / BASE_WIDTH;
278− let col_height = ((prompt.completion.len() as u16 + max_col - 1) / max_col);
279−
280− for i in (3..col_height + 3) {
281− self.surface.set_string(
282− 0,
283− self.size.1 - i as u16,
284− " ".repeat(self.size.0 as usize),
285− self.text_color,
286− );
287− }
288− self.surface.set_style(
289− Rect::new(0, self.size.1 - col_height - 2, self.size.0, col_height),
290− theme.get("ui.statusline"),
291− );
292− for (i, command) in prompt.completion.iter().enumerate() {
293− let color = if prompt.completion_selection_index.is_some()
294− && i == prompt.completion_selection_index.unwrap()
295− {
296− Style::default().bg(Color::Rgb(104, 060, 232))
297− } else {
298− self.text_color
299− };
300− self.surface.set_stringn(
301− 1 + col * BASE_WIDTH,
302− self.size.1 - col_height - 2 + row,
303− &command,
304− BASE_WIDTH as usize - 1,
305− color,
306− );
307− row += 1;
308− if row > col_height - 1 {
309− row = 0;
310− col += 1;
311− }
312− if col > max_col {
313− break;
314− }
315− }
316− }
317− // render buffer text
318− self.surface
319− .set_string(1, self.size.1 - 1, &prompt.prompt, self.text_color);
320− self.surface
321− .set_string(2, self.size.1 - 1, &prompt.line, self.text_color);
322− }
323−
324− pub fn draw(&mut self) {
325− use tui::backend::Backend;
326− // TODO: theres probably a better place for this
327− self.terminal
328− .backend_mut()
329− .draw(self.cache.diff(&self.surface).into_iter());
330− // swap the buffer
331− std::mem::swap(&mut self.surface, &mut self.cache);
332− }
333−
334− pub fn render_cursor(&mut self, view: &View, prompt: Option<&Prompt>, viewport: Rect) {
335− let mut stdout = stdout();
336− match view.doc.mode() {
337− Mode::Insert => write!(stdout, "\x1B[6 q"),
338− mode => write!(stdout, "\x1B[2 q"),
339− };
340− let pos = if let Some(prompt) = prompt {
341− Position::new(self.size.0 as usize, 2 + prompt.cursor)
342− } else {
343− if let Some(path) = view.doc.path() {
344− self.surface.set_string(
345− 6,
346− self.size.1 - 1,
347− path.to_string_lossy(),
348− self.text_color,
349− );
350− }
351−
352− let cursor = view.doc.state.selection().cursor();
353−
354− let mut pos = view
355− .screen_coords_at_pos(&view.doc.text().slice(..), cursor)
356− .expect("Cursor is out of bounds.");
357− pos.col += viewport.x as usize;
358− pos.row += viewport.y as usize;
359− pos
360− };
361−
362− execute!(stdout, cursor::MoveTo(pos.col as u16, pos.row as u16));
363− }
364−}
365−
366−impl<'a> Application<'a> {
367− pub fn new(mut args: Args, executor: &'a smol::Executor<'a>) -> Result<Self, Error> {
368− let terminal = Renderer::new()?;
36942 let mut editor = Editor::new();
43+ let size = terminal.size()?;
37044
37145 if let Some(file) = args.values_of_t::<PathBuf>("files").unwrap().pop() {
372− editor.open(file, terminal.size)?;
46+ editor.open(file, (size.width, size.height))?;
37347 }
37448
49+ let mut compositor = Compositor::new();
50+ compositor.push(Box::new(ui::EditorView::new()));
51+
37552 let language_server = helix_lsp::Client::start(&executor, "rust-analyzer", &[]);
37653
37754 let mut app = Self {
37855 editor,
37956 terminal,
380− // TODO; move to state
381− prompt: None,
57+ compositor,
38258
383− //
384− keymap: keymap::default(),
38559 executor,
38660 language_server,
38761 };
@@ -390,27 +64,18 @@ impl<'a> Application<'a> {
39064 }
39165
39266 fn render(&mut self) {
393− let viewport = Rect::new(OFFSET, 0, self.terminal.size.0, self.terminal.size.1 - 2); // - 2 for statusline and prompt
394−
395− // SAFETY: we cheat around the view_mut() borrow because it doesn't allow us to also borrow
396− // theme. Theme is immutable mutating view won't disrupt theme_ref.
397− let theme_ref = unsafe { &*(&self.editor.theme as *const Theme) };
398− if let Some(view) = self.editor.view_mut() {
399− self.terminal.render_view(view, viewport, theme_ref);
400− if let Some(prompt) = &self.prompt {
401− if prompt.should_close {
402− self.prompt = None;
403− } else {
404− self.terminal.render_prompt(view, prompt, theme_ref);
405− }
406− }
407− }
67+ let executor = &self.executor;
68+ let editor = &mut self.editor;
69+ let compositor = &self.compositor;
40870
409− self.terminal.draw();
71+ let mut cx = crate::compositor::Context { editor, executor };
72+ let area = self.terminal.size().unwrap();
41073
411− // TODO: drop unwrap
412− self.terminal
413− .render_cursor(self.editor.view().unwrap(), self.prompt.as_ref(), viewport);
74+ compositor.render(area, self.terminal.current_buffer_mut(), &mut cx);
75+ let pos = compositor.cursor_position(area, &mut cx);
76+
77+ self.terminal.draw();
78+ self.terminal.set_cursor(pos.col as u16, pos.row as u16);
41479 }
41580
41681 pub async fn event_loop(&mut self) {
@@ -418,10 +83,11 @@ impl<'a> Application<'a> {
41883
41984 // initialize lsp
42085 self.language_server.initialize().await.unwrap();
421− self.language_server
422− .text_document_did_open(&self.editor.view().unwrap().doc)
423− .await
424− .unwrap();
86+ // TODO: temp
87+ // self.language_server
88+ // .text_document_did_open(&cx.editor.view().unwrap().doc)
89+ // .await
90+ // .unwrap();
42591
42692 self.render();
42793
@@ -433,7 +99,7 @@ impl<'a> Application<'a> {
43399 use futures_util::{select, FutureExt};
434100 select! {
435101 event = reader.next().fuse() => {
436− self.handle_terminal_events(event).await
102+ self.handle_terminal_events(event)
437103 }
438104 call = self.language_server.incoming.next().fuse() => {
439105 self.handle_language_server_message(call).await
@@ -442,151 +108,28 @@ impl<'a> Application<'a> {
442108 }
443109 }
444110
445− pub async fn handle_terminal_events(
446− &mut self,
447− event: Option<Result<Event, crossterm::ErrorKind>>,
448− ) {
111+ pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) {
112+ let mut cx = crate::compositor::Context {
113+ editor: &mut self.editor,
114+ executor: &self.executor,
115+ };
449116 // Handle key events
450− match event {
117+ let should_redraw = match event {
451118 Some(Ok(Event::Resize(width, height))) => {
452− self.terminal.resize(width, height);
453−
454− // TODO: simplistic ensure cursor in view for now
455− // TODO: loop over views
456− if let Some(view) = self.editor.view_mut() {
457− view.size = self.terminal.size;
458− view.ensure_cursor_in_view()
459− };
119+ self.terminal.resize(Rect::new(0, 0, width, height));
460120
461− self.render();
462− }
463− Some(Ok(Event::Key(event))) => {
464− // if there's a prompt, it takes priority
465− if let Some(prompt) = &mut self.prompt {
466− self.prompt
467− .as_mut()
468− .unwrap()
469− .handle_input(event, &mut self.editor);
470−
471− self.render();
472− } else if let Some(view) = self.editor.view_mut() {
473− let keys = vec![event];
474− // TODO: sequences (`gg`)
475− // TODO: handle count other than 1
476− match view.doc.mode() {
477− Mode::Insert => {
478− if let Some(command) = self.keymap[&Mode::Insert].get(&keys) {
479− let mut cx = helix_view::commands::Context {
480− view,
481− executor: self.executor,
482− count: 1,
483− };
484−
485− command(&mut cx);
486− } else if let KeyEvent {
487− code: KeyCode::Char(c),
488− ..
489− } = event
490− {
491− let mut cx = helix_view::commands::Context {
492− view,
493− executor: self.executor,
494− count: 1,
495− };
496− commands::insert::insert_char(&mut cx, c);
497− }
498− view.ensure_cursor_in_view();
499− }
500− Mode::Normal => {
501− if let &[KeyEvent {
502− code: KeyCode::Char(':'),
503− ..
504− }] = keys.as_slice()
505− {
506− let prompt = Prompt::new(
507− ":".to_owned(),
508− |_input: &str| {
509− // TODO: i need this duplicate list right now to avoid borrow checker issues
510− let command_list = vec![
511− String::from("q"),
512− String::from("aaa"),
513− String::from("bbb"),
514− String::from("ccc"),
515− String::from("ddd"),
516− String::from("eee"),
517− String::from("averylongcommandaverylongcommandaverylongcommandaverylongcommandaverylongcommand"),
518− String::from("q"),
519− String::from("aaa"),
520− String::from("bbb"),
521− String::from("ccc"),
522− String::from("ddd"),
523− String::from("eee"),
524− String::from("q"),
525− String::from("aaa"),
526− String::from("bbb"),
527− String::from("ccc"),
528− String::from("ddd"),
529− String::from("eee"),
530− String::from("q"),
531− String::from("aaa"),
532− String::from("bbb"),
533− String::from("ccc"),
534− String::from("ddd"),
535− String::from("eee"),
536− String::from("q"),
537− String::from("aaa"),
538− String::from("bbb"),
539− String::from("ccc"),
540− String::from("ddd"),
541− String::from("eee"),
542− ];
543− command_list
544− .into_iter()
545− .filter(|command| command.contains(_input))
546− .collect()
547− }, // completion
548− |editor: &mut Editor, input: &str| match input {
549− "q" => editor.should_close = true,
550− _ => (),
551− },
552− );
553−
554− self.prompt = Some(prompt);
555−
556− // HAXX: special casing for command mode
557− } else if let Some(command) = self.keymap[&Mode::Normal].get(&keys) {
558− let mut cx = helix_view::commands::Context {
559− view,
560− executor: self.executor,
561− count: 1,
562− };
563− command(&mut cx);
564−
565− // TODO: simplistic ensure cursor in view for now
566− view.ensure_cursor_in_view();
567− }
568− }
569− mode => {
570− if let Some(command) = self.keymap[&mode].get(&keys) {
571− let mut cx = helix_view::commands::Context {
572− view,
573− executor: self.executor,
574− count: 1,
575− };
576− command(&mut cx);
577−
578− // TODO: simplistic ensure cursor in view for now
579− view.ensure_cursor_in_view();
580− }
581− }
582− }
583− self.render();
584− }
121+ self.compositor
122+ .handle_event(Event::Resize(width, height), &mut cx)
585123 }
586− Some(Ok(Event::Mouse(_))) => (), // unhandled
124+ Some(Ok(event)) => self.compositor.handle_event(event, &mut cx),
587125 Some(Err(x)) => panic!(x),
588126 None => panic!(),
589127 };
128+
129+ if should_redraw {
130+ self.render();
131+ // calling render twice here fixes it for some reason
132+ }
590133 }
591134
592135 pub async fn handle_language_server_message(&mut self, call: Option<helix_lsp::Call>) {
@@ -599,11 +142,13 @@ impl<'a> Application<'a> {
599142 match notification {
600143 Notification::PublishDiagnostics(params) => {
601144 let path = Some(params.uri.to_file_path().unwrap());
602− let view = self
603− .editor
604− .views
605− .iter_mut()
606− .find(|view| view.doc.path == path);
145+ let view: Option<&mut helix_view::View> = None;
146+ // TODO:
147+ // let view = self
148+ // .editor
149+ // .views
150+ // .iter_mut()
151+ // .find(|view| view.doc.path == path);
607152
608153 if let Some(view) = view {
609154 let doc = view.doc.text().slice(..);
@@ -653,7 +198,7 @@ impl<'a> Application<'a> {
653198 }
654199
655200 pub async fn run(&mut self) -> Result<(), Error> {
656− enable_raw_mode()?;
201+ terminal::enable_raw_mode()?;
657202
658203 let mut stdout = stdout();
659204
@@ -663,7 +208,7 @@ impl<'a> Application<'a> {
663208 let hook = std::panic::take_hook();
664209 std::panic::set_hook(Box::new(move |info| {
665210 execute!(std::io::stdout(), terminal::LeaveAlternateScreen);
666− disable_raw_mode();
211+ terminal::disable_raw_mode();
667212 hook(info);
668213 }));
669214
@@ -674,7 +219,7 @@ impl<'a> Application<'a> {
674219
675220 execute!(stdout, terminal::LeaveAlternateScreen)?;
676221
677− disable_raw_mode()?;
222+ terminal::disable_raw_mode()?;
678223
679224 Ok(())
680225 }