@@ -1,13 +1,9 @@
1−use crate::{
2− commands,
3− keymap::{self, Keymaps},
4−};
51use clap::ArgMatches as Args;
6−use helix_core::{indent::TAB_WIDTH, syntax::HighlightEvent, Position, Range, State};
72
83use helix_view::{document::Mode, Document, Editor, Theme, View};
94
105use crate::compositor::{Component, Compositor, EventResult};
6+use crate::editor_view::EditorView;
117use crate::prompt::Prompt;
128
139use log::{debug, info};
@@ -37,426 +33,54 @@ use tui::{
3733 style::{Color, Modifier, Style},
3834};
3935
40−type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
41−
42−const BASE_WIDTH: u16 = 30;
36+type Terminal = crate::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
4337
44−pub struct Application<'a> {
38+pub struct Application {
4539 compositor: Compositor,
4640 editor: Editor,
47− renderer: Renderer,
41+ terminal: Terminal,
4842
49− executor: &'a smol::Executor<'a>,
43+ executor: &'static smol::Executor<'static>,
5044 language_server: helix_lsp::Client,
5145}
5246
53−pub struct Renderer {
54− size: (u16, u16),
55− terminal: Terminal,
56− surface: Surface,
57− cache: Surface,
58− text_color: Style,
47+// TODO: temp
48+#[inline(always)]
49+pub fn text_color() -> Style {
50+ return Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
5951}
6052
61−impl Renderer {
62− pub fn new() -> Result<Self, Error> {
53+// pub fn render_cursor(&mut self, view: &View, prompt: Option<&Prompt>, viewport: Rect) {
54+// let mut stdout = stdout();
55+// match view.doc.mode() {
56+// Mode::Insert => write!(stdout, "\x1B[6 q"),
57+// mode => write!(stdout, "\x1B[2 q"),
58+// };
59+// let pos = if let Some(prompt) = prompt {
60+// Position::new(self.size.0 as usize, 2 + prompt.cursor)
61+// } else {
62+// let cursor = view.doc.state.selection().cursor();
63+
64+// let mut pos = view
65+// .screen_coords_at_pos(&view.doc.text().slice(..), cursor)
66+// .expect("Cursor is out of bounds.");
67+// pos.col += viewport.x as usize;
68+// pos.row += viewport.y as usize;
69+// pos
70+// };
71+
72+// execute!(stdout, cursor::MoveTo(pos.col as u16, pos.row as u16));
73+// }
74+
75+impl Application {
76+ pub fn new(mut args: Args, executor: &'static smol::Executor<'static>) -> Result<Self, Error> {
6377 let backend = CrosstermBackend::new(stdout());
6478 let mut terminal = Terminal::new(backend)?;
65− let size = terminal::size().unwrap();
66− let text_color: Style = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
67−
68− let area = Rect::new(0, 0, size.0, size.1);
69−
70− Ok(Self {
71− size,
72− terminal,
73− surface: Surface::empty(area),
74− cache: Surface::empty(area),
75− text_color,
76− })
77− }
78−
79− pub fn resize(&mut self, width: u16, height: u16) {
80− self.size = (width, height);
81− let area = Rect::new(0, 0, width, height);
82− self.surface = Surface::empty(area);
83− self.cache = Surface::empty(area);
84− }
85−
86− pub fn render_view(&mut self, view: &mut View, viewport: Rect, theme: &Theme) {
87− self.render_buffer(view, viewport, theme);
88− self.render_statusline(view, theme);
89− }
90−
91− // TODO: ideally not &mut View but highlights require it because of cursor cache
92− pub fn render_buffer(&mut self, view: &mut View, viewport: Rect, theme: &Theme) {
93− let area = Rect::new(0, 0, self.size.0, self.size.1);
94−
95− // clear with background color
96− self.surface.set_style(area, theme.get("ui.background"));
97−
98− // TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
99− let source_code = view.doc.text().to_string();
100−
101− let last_line = view.last_line();
102−
103− let range = {
104− // calculate viewport byte ranges
105− let start = view.doc.text().line_to_byte(view.first_line);
106− let end = view.doc.text().line_to_byte(last_line)
107− + view.doc.text().line(last_line).len_bytes();
108−
109− start..end
110− };
111−
112− // TODO: range doesn't actually restrict source, just highlight range
113− // TODO: cache highlight results
114− // TODO: only recalculate when state.doc is actually modified
115− let highlights: Vec<_> = match view.doc.syntax.as_mut() {
116− Some(syntax) => {
117− syntax
118− .highlight_iter(source_code.as_bytes(), Some(range), None, |_| None)
119− .unwrap()
120− .collect() // TODO: we collect here to avoid double borrow, fix later
121− }
122− None => vec![Ok(HighlightEvent::Source {
123− start: range.start,
124− end: range.end,
125− })],
126− };
127− let mut spans = Vec::new();
128− let mut visual_x = 0;
129− let mut line = 0u16;
130− let visible_selections: Vec<Range> = view
131− .doc
132− .state
133− .selection()
134− .ranges()
135− .iter()
136− // TODO: limit selection to one in viewport
137− // .filter(|range| !range.is_empty()) // && range.overlaps(&Range::new(start, end + 1))
138− .copied()
139− .collect();
140−
141− 'outer: for event in highlights {
142− match event.unwrap() {
143− HighlightEvent::HighlightStart(span) => {
144− spans.push(span);
145− }
146− HighlightEvent::HighlightEnd => {
147− spans.pop();
148− }
149− HighlightEvent::Source { start, end } => {
150− // TODO: filter out spans out of viewport for now..
151−
152− let start = view.doc.text().byte_to_char(start);
153− let end = view.doc.text().byte_to_char(end); // <-- index 744, len 743
154−
155− let text = view.doc.text().slice(start..end);
156−
157− use helix_core::graphemes::{grapheme_width, RopeGraphemes};
158−
159− let style = match spans.first() {
160− Some(span) => theme.get(theme.scopes()[span.0].as_str()),
161− None => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender
162− };
163−
164− // TODO: we could render the text to a surface, then cache that, that
165− // way if only the selection/cursor changes we can copy from cache
166− // and paint the new cursor.
167−
168− let mut char_index = start;
169−
170− // iterate over range char by char
171− for grapheme in RopeGraphemes::new(&text) {
172− // TODO: track current char_index
173−
174− if grapheme == "\n" {
175− visual_x = 0;
176− line += 1;
177−
178− // TODO: with proper iter this shouldn't be necessary
179− if line >= viewport.height {
180− break 'outer;
181− }
182− } else if grapheme == "\t" {
183− visual_x += (TAB_WIDTH as u16);
184− } else {
185− // Cow will prevent allocations if span contained in a single slice
186− // which should really be the majority case
187− let grapheme = Cow::from(grapheme);
188− let width = grapheme_width(&grapheme) as u16;
189−
190− // TODO: this should really happen as an after pass
191− let style = if visible_selections
192− .iter()
193− .any(|range| range.contains(char_index))
194− {
195− // cedar
196− style.clone().bg(Color::Rgb(128, 47, 0))
197− } else {
198− style
199− };
200−
201− let style = if visible_selections
202− .iter()
203− .any(|range| range.head == char_index)
204− {
205− style.clone().bg(Color::Rgb(255, 255, 255))
206− } else {
207− style
208− };
209−
210− // ugh, improve with a traverse method
211− // or interleave highlight spans with selection and diagnostic spans
212− let style = if view.doc.diagnostics.iter().any(|diagnostic| {
213− diagnostic.range.0 <= char_index && diagnostic.range.1 > char_index
214− }) {
215− style.clone().add_modifier(Modifier::UNDERLINED)
216− } else {
217− style
218− };
219−
220− // TODO: paint cursor heads except primary
221−
222− self.surface.set_string(
223− viewport.x + visual_x,
224− viewport.y + line,
225− grapheme,
226− style,
227− );
228−
229− visual_x += width;
230− }
231−
232− char_index += 1;
233− }
234− }
235− }
236− }
237−
238− let style: Style = theme.get("ui.linenr");
239− let warning: Style = theme.get("warning");
240− let last_line = view.last_line();
241− for (i, line) in (view.first_line..last_line).enumerate() {
242− if view.doc.diagnostics.iter().any(|d| d.line == line) {
243− self.surface.set_stringn(0, i as u16, "●", 1, warning);
244− }
245−
246− self.surface
247− .set_stringn(1, i as u16, format!("{:>5}", line + 1), 5, style);
248− }
249− }
250−
251− pub fn render_statusline(&mut self, view: &View, theme: &Theme) {
252− let mode = match view.doc.mode() {
253− Mode::Insert => "INS",
254− Mode::Normal => "NOR",
255− Mode::Goto => "GOTO",
256− };
257− // statusline
258− self.surface.set_style(
259− Rect::new(0, self.size.1 - 2, self.size.0, 1),
260− theme.get("ui.statusline"),
261− );
262− self.surface
263− .set_string(1, self.size.1 - 2, mode, self.text_color);
264−
265− if let Some(path) = view.doc.path() {
266− self.surface
267− .set_string(6, self.size.1 - 2, path.to_string_lossy(), self.text_color);
268− }
269−
270− self.surface.set_string(
271− self.size.0 - 10,
272− self.size.1 - 2,
273− format!("{}", view.doc.diagnostics.len()),
274− self.text_color,
275− );
276− }
277−
278− pub fn render_prompt(&mut self, prompt: &Prompt, theme: &Theme) {
279− // completion
280− if !prompt.completion.is_empty() {
281− // TODO: find out better way of clearing individual lines of the screen
282− let mut row = 0;
283− let mut col = 0;
284− let max_col = self.size.0 / BASE_WIDTH;
285− let col_height = ((prompt.completion.len() as u16 + max_col - 1) / max_col);
286−
287− for i in (3..col_height + 3) {
288− self.surface.set_string(
289− 0,
290− self.size.1 - i as u16,
291− " ".repeat(self.size.0 as usize),
292− self.text_color,
293− );
294− }
295− self.surface.set_style(
296− Rect::new(0, self.size.1 - col_height - 2, self.size.0, col_height),
297− theme.get("ui.statusline"),
298− );
299− for (i, command) in prompt.completion.iter().enumerate() {
300− let color = if prompt.completion_selection_index.is_some()
301− && i == prompt.completion_selection_index.unwrap()
302− {
303− Style::default().bg(Color::Rgb(104, 060, 232))
304− } else {
305− self.text_color
306− };
307− self.surface.set_stringn(
308− 1 + col * BASE_WIDTH,
309− self.size.1 - col_height - 2 + row,
310− &command,
311− BASE_WIDTH as usize - 1,
312− color,
313− );
314− row += 1;
315− if row > col_height - 1 {
316− row = 0;
317− col += 1;
318− }
319− if col > max_col {
320− break;
321− }
322− }
323− }
324− // render buffer text
325− self.surface
326− .set_string(1, self.size.1 - 1, &prompt.prompt, self.text_color);
327− self.surface
328− .set_string(2, self.size.1 - 1, &prompt.line, self.text_color);
329− }
330−
331− pub fn draw_and_swap(&mut self) {
332− use tui::backend::Backend;
333− // TODO: theres probably a better place for this
334− self.terminal
335− .backend_mut()
336− .draw(self.cache.diff(&self.surface).into_iter());
337− // swap the buffer
338− std::mem::swap(&mut self.surface, &mut self.cache);
339− self.surface.reset(); // reset is faster than allocating new empty surface
340− }
341−
342− pub fn render_cursor(&mut self, view: &View, prompt: Option<&Prompt>, viewport: Rect) {
343− let mut stdout = stdout();
344− match view.doc.mode() {
345− Mode::Insert => write!(stdout, "\x1B[6 q"),
346− mode => write!(stdout, "\x1B[2 q"),
347− };
348− let pos = if let Some(prompt) = prompt {
349− Position::new(self.size.0 as usize, 2 + prompt.cursor)
350− } else {
351− let cursor = view.doc.state.selection().cursor();
352−
353− let mut pos = view
354− .screen_coords_at_pos(&view.doc.text().slice(..), cursor)
355− .expect("Cursor is out of bounds.");
356− pos.col += viewport.x as usize;
357− pos.row += viewport.y as usize;
358− pos
359− };
360−
361− execute!(stdout, cursor::MoveTo(pos.col as u16, pos.row as u16));
362− }
363−}
364−
365−struct EditorView {
366− keymap: Keymaps,
367−}
368−
369−impl EditorView {
370− fn new() -> Self {
371− Self {
372− keymap: keymap::default(),
373− }
374− }
375−}
376−
377−use crate::compositor::Context;
378−
379−impl Component for EditorView {
380− fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
381− match event {
382− Event::Resize(width, height) => {
383− // TODO: simplistic ensure cursor in view for now
384− // TODO: loop over views
385− if let Some(view) = cx.editor.view_mut() {
386− view.size = (width, height);
387− view.ensure_cursor_in_view()
388− };
389− EventResult::Consumed(None)
390− }
391− Event::Key(event) => {
392− if let Some(view) = cx.editor.view_mut() {
393− let keys = vec![event];
394− // TODO: sequences (`gg`)
395− let mode = view.doc.mode();
396− // TODO: handle count other than 1
397− let mut cx = commands::Context {
398− view,
399− executor: cx.executor,
400− count: 1,
401− callback: None,
402− };
403−
404− match mode {
405− Mode::Insert => {
406− if let Some(command) = self.keymap[&Mode::Insert].get(&keys) {
407− command(&mut cx);
408− } else if let KeyEvent {
409− code: KeyCode::Char(c),
410− ..
411− } = event
412− {
413− commands::insert::insert_char(&mut cx, c);
414− }
415− }
416− mode => {
417− if let Some(command) = self.keymap[&mode].get(&keys) {
418− command(&mut cx);
419−
420− // TODO: simplistic ensure cursor in view for now
421− }
422− }
423− }
424− // appease borrowck
425− let callback = cx.callback.take();
426−
427− view.ensure_cursor_in_view();
428−
429− EventResult::Consumed(callback)
430− } else {
431− EventResult::Ignored
432− }
433− }
434− Event::Mouse(_) => EventResult::Ignored,
435− }
436− }
437− fn render(&mut self, renderer: &mut Renderer, cx: &mut Context) {
438− const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
439− let viewport = Rect::new(OFFSET, 0, renderer.size.0, renderer.size.1 - 2); // - 2 for statusline and prompt
440−
441− // SAFETY: we cheat around the view_mut() borrow because it doesn't allow us to also borrow
442− // theme. Theme is immutable mutating view won't disrupt theme_ref.
443− let theme_ref = unsafe { &*(&cx.editor.theme as *const Theme) };
444− if let Some(view) = cx.editor.view_mut() {
445− renderer.render_view(view, viewport, theme_ref);
446− }
447−
448− // TODO: drop unwrap
449− renderer.render_cursor(cx.editor.view().unwrap(), None, viewport);
450− }
451−}
452−
453−impl<'a> Application<'a> {
454− pub fn new(mut args: Args, executor: &'a smol::Executor<'a>) -> Result<Self, Error> {
455− let renderer = Renderer::new()?;
45679 let mut editor = Editor::new();
80+ let size = terminal.size()?;
45781
45882 if let Some(file) = args.values_of_t::<PathBuf>("files").unwrap().pop() {
459− editor.open(file, renderer.size)?;
83+ editor.open(file, (size.width, size.height))?;
46084 }
46185
46286 let mut compositor = Compositor::new();
@@ -466,7 +90,7 @@ impl<'a> Application<'a> {
46690
46791 let mut app = Self {
46892 editor,
469− renderer,
93+ terminal,
47094 // TODO; move to state
47195 compositor,
47296
@@ -478,12 +102,17 @@ impl<'a> Application<'a> {
478102 }
479103
480104 fn render(&mut self) {
481− let mut cx = crate::compositor::Context {
482− editor: &mut self.editor,
483− executor: &self.executor,
484− };
485− self.compositor.render(&mut self.renderer, &mut cx); // viewport,
486− self.renderer.draw_and_swap();
105+ let executor = &self.executor;
106+ let editor = &mut self.editor;
107+ let compositor = &self.compositor;
108+
109+ // TODO: should be unnecessary
110+ // self.terminal.autoresize();
111+ let mut cx = crate::compositor::Context { editor, executor };
112+ let area = self.terminal.size().unwrap();
113+ compositor.render(area, self.terminal.current_buffer_mut(), &mut cx);
114+
115+ self.terminal.draw();
487116 }
488117
489118 pub async fn event_loop(&mut self) {
@@ -524,7 +153,7 @@ impl<'a> Application<'a> {
524153 // Handle key events
525154 let should_redraw = match event {
526155 Some(Ok(Event::Resize(width, height))) => {
527− self.renderer.resize(width, height);
156+ self.terminal.resize(Rect::new(0, 0, width, height));
528157
529158 self.compositor
530159 .handle_event(Event::Resize(width, height), &mut cx)