@@ -4,7 +4,7 @@ use helix_view::{commands, keymap, View};
6 borrow::Cow,
6 borrow::Cow,
7− io::{self, stdout, Write},
7+ io::{self, stdout, Stdout, Write},
8 path::PathBuf,
8 path::PathBuf,
9 time::Duration,
9 time::Duration,
@@ -24,6 +24,8 @@ use crossterm::{
25use tui::{backend::CrosstermBackend, buffer::Buffer as Surface, layout::Rect, style::Style};
25use tui::{backend::CrosstermBackend, buffer::Buffer as Surface, layout::Rect, style::Style};
27+const OFFSET: u16 = 6; // 5 linenr + 1 gutter
27type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
29type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
29static EX: smol::Executor = smol::Executor::new();
31static EX: smol::Executor = smol::Executor::new();
@@ -68,232 +70,235 @@ impl Editor {
68 use tui::backend::Backend;
70 use tui::backend::Backend;
69 use tui::style::Color;
71 use tui::style::Color;
70 // TODO: ideally not mut but highlights require it because of cursor cache
72 // TODO: ideally not mut but highlights require it because of cursor cache
71− match &mut self.view {
73− let area = Rect::new(0, 0, self.size.0, self.size.1);
73+ let viewport = Rect::new(OFFSET, 0, self.size.0, self.size.1 - 1); // - 1 for statusline
74− let mut stdout = stdout();
74+ let text_color: Style = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
75− self.surface.reset(); // reset is faster than allocating new empty surface
77− // clear with background color
76+ self.render_view(viewport);
79− .set_style(area, view.theme.get("ui.background"));
81− let offset = 5 + 1; // 5 linenr + 1 gutter
78+ self.render_prompt(text_color);
82− let viewport = Rect::new(offset, 0, self.size.0, self.size.1 - 1); // - 1 for statusline
84− // TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
80+ self.render_cursor(viewport, text_color);
85− let source_code = view.state.doc().to_string();
81+ }
87− let last_line = view.last_line();
83+ pub fn render_view(&mut self, viewport: Rect) {
84+ use tui::style::Color;
85+ let area = Rect::new(0, 0, self.size.0, self.size.1);
86+ let mut view: &mut View = self.view.as_mut().unwrap();
87+ self.surface.reset(); // reset is faster than allocating new empty surface
89− let range = {
89+ // clear with background color
90− // calculate viewport byte ranges
90+ self.surface
91− let start = view.state.doc().line_to_byte(view.first_line);
91+ .set_style(area, view.theme.get("ui.background"));
92− let end = view.state.doc().line_to_byte(last_line)
93− + view.state.doc().line(last_line).len_bytes();
95− start..end
93+ // TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
96− };
94+ let source_code = view.state.doc().to_string();
98− // TODO: range doesn't actually restrict source, just highlight range
96+ let last_line = view.last_line();
100− // TODO: cache highlight results
98+ let range = {
101− // TODO: only recalculate when state.doc is actually modified
99+ // calculate viewport byte ranges
102− let highlights: Vec<_> = match view.state.syntax.as_mut() {
100+ let start = view.state.doc().line_to_byte(view.first_line);
103− Some(syntax) => {
101+ let end = view.state.doc().line_to_byte(last_line)
104− syntax
102+ + view.state.doc().line(last_line).len_bytes();
105− .highlight_iter(source_code.as_bytes(), Some(range), None, |_| None)
107− .collect() // TODO: we collect here to avoid double borrow, fix later
109− None => vec![Ok(HighlightEvent::Source {
115− let mut spans = Vec::new();
117− let mut visual_x = 0;
118− let mut line = 0u16;
120− let visible_selections: Vec<Range> = view
125− // TODO: limit selection to one in viewport
126− // .filter(|range| !range.is_empty()) // && range.overlaps(&Range::new(start, end + 1))
130− 'outer: for event in highlights {
131− match event.unwrap() {
132− HighlightEvent::HighlightStart(span) => {
135− HighlightEvent::HighlightEnd => {
138− HighlightEvent::Source { start, end } => {
139− // TODO: filter out spans out of viewport for now..
141− let start = view.state.doc().byte_to_char(start);
104+ start..end
142− let end = view.state.doc().byte_to_char(end); // <-- index 744, len 743
105+ };
144− let text = view.state.doc().slice(start..end);
107+ // TODO: range doesn't actually restrict source, just highlight range
108+ // TODO: cache highlight results
109+ // TODO: only recalculate when state.doc is actually modified
110+ let highlights: Vec<_> = match view.state.syntax.as_mut() {
113+ .highlight_iter(source_code.as_bytes(), Some(range), None, |_| None)
115+ .collect() // TODO: we collect here to avoid double borrow, fix later
117+ None => vec![Ok(HighlightEvent::Source {
122+ let mut spans = Vec::new();
123+ let mut visual_x = 0;
124+ let mut line = 0u16;
125+ let visible_selections: Vec<Range> = view
130+ // TODO: limit selection to one in viewport
131+ // .filter(|range| !range.is_empty()) // && range.overlaps(&Range::new(start, end + 1))
135+ 'outer: for event in highlights {
136+ match event.unwrap() {
137+ HighlightEvent::HighlightStart(span) => {
140+ HighlightEvent::HighlightEnd => {
143+ HighlightEvent::Source { start, end } => {
144+ // TODO: filter out spans out of viewport for now..
146− use helix_core::graphemes::{grapheme_width, RopeGraphemes};
146+ let start = view.state.doc().byte_to_char(start);
147+ let end = view.state.doc().byte_to_char(end); // <-- index 744, len 743
148− let style = match spans.first() {
149+ let text = view.state.doc().slice(start..end);
149− Some(span) => view.theme.get(view.theme.scopes()[span.0].as_str()),
150− None => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender
153− // TODO: we could render the text to a surface, then cache that, that
151+ use helix_core::graphemes::{grapheme_width, RopeGraphemes};
154− // way if only the selection/cursor changes we can copy from cache
155− // and paint the new cursor.
157− let mut char_index = start;
159− // iterate over range char by char
160− for grapheme in RopeGraphemes::new(&text) {
161− // TODO: track current char_index
163− if grapheme == "\n" {
167− // TODO: with proper iter this shouldn't be necessary
168− if line >= viewport.height {
171− } else if grapheme == "\t" {
172− visual_x += (TAB_WIDTH as u16);
174− // Cow will prevent allocations if span contained in a single slice
175− // which should really be the majority case
176− let grapheme = Cow::from(grapheme);
177− let width = grapheme_width(&grapheme) as u16;
179− // TODO: this should really happen as an after pass
180− let style = if visible_selections
182− .any(|range| range.contains(char_index))
185− style.clone().bg(Color::Rgb(128, 47, 0))
190− let style = if visible_selections
192− .any(|range| range.head == char_index)
194− style.clone().bg(Color::Rgb(255, 255, 255))
199− // TODO: paint cursor heads except primary
201− self.surface.set_string(
210− // if grapheme == "\t"
153+ let style = match spans.first() {
154+ Some(span) => view.theme.get(view.theme.scopes()[span.0].as_str()),
155+ None => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender
158+ // TODO: we could render the text to a surface, then cache that, that
159+ // way if only the selection/cursor changes we can copy from cache
160+ // and paint the new cursor.
212− char_index += 1;
162+ let mut char_index = start;
164+ // iterate over range char by char
165+ for grapheme in RopeGraphemes::new(&text) {
166+ // TODO: track current char_index
168+ if grapheme == "\n" {
172+ // TODO: with proper iter this shouldn't be necessary
173+ if line >= viewport.height {
214− }
176+ } else if grapheme == "\t" {
215− }
177+ visual_x += (TAB_WIDTH as u16);
179+ // Cow will prevent allocations if span contained in a single slice
180+ // which should really be the majority case
181+ let grapheme = Cow::from(grapheme);
182+ let width = grapheme_width(&grapheme) as u16;
184+ // TODO: this should really happen as an after pass
185+ let style = if visible_selections
187+ .any(|range| range.contains(char_index))
190+ style.clone().bg(Color::Rgb(128, 47, 0))
218− let style: Style = view.theme.get("ui.linenr");
195+ let style = if visible_selections
219− for (i, line) in (view.first_line..last_line).enumerate() {
196+ .iter()
220− self.surface
197+ .any(|range| range.head == char_index)
221− .set_stringn(0, i as u16, format!("{:>5}", line + 1), 5, style);
198+ {
222− // lavender
199+ style.clone().bg(Color::Rgb(255, 255, 255))
225− // // iterate over selections and render them
204+ // TODO: paint cursor heads except primary
226− // for range in state.selection.ranges() {
205+
227− // // get terminal coords for x,y for each range pos
228− // // TODO: this won't work with multiline
229− // let (y1, x1) = coords_at_pos(&text, range.from());
230− // let (y2, x2) = coords_at_pos(&text, range.to());
231− // let area = Rect::new(
234− // (x2 - x1 + 1) as u16,
235− // (y2 - y1 + 1) as u16,
237− // self.surface.set_style(area, select);
206+ self.surface
238− // }
207+ .set_string(OFFSET + visual_x, line, grapheme, style);
241− let mode = match view.state.mode() {
242− Mode::Insert => "INS",
243− Mode::Normal => "NOR",
244− Mode::Goto => "GOTO",
245− Mode::Command => ":",
247− self.surface.set_style(
248− Rect::new(0, self.size.1 - 1, self.size.0, 1),
249− view.theme.get("ui.statusline"),
251− // TODO: unfocused one with different color
252− let text_color = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
254− .set_string(1, self.size.1 - 1, mode, text_color);
257− match view.state.mode() {
258− Mode::Insert => write!(stdout, "\x1B[6 q"),
259− Mode::Normal => write!(stdout, "\x1B[2 q"),
260− Mode::Goto => write!(stdout, "\x1B[2 q"),
261− Mode::Command => write!(stdout, "\x1B[2 q"),
264− // render the cursor
265− let mut pos: Position;
266− if view.state.mode() == Mode::Command {
267− pos = Position::new(self.size.0 as usize, 2);
269− if let Some(path) = view.state.path() {
270− self.surface.set_string(
273− path.to_string_lossy(),
278− let cursor = view.state.selection().cursor();
209+ visual_x += width;
211+ // if grapheme == "\t"
280− pos = view
213+ char_index += 1;
281− .screen_coords_at_pos(&view.state.doc().slice(..), cursor)
214+ }
282− .expect("Cursor is out of bounds.");
283− pos.col += viewport.x as usize;
284− pos.row += viewport.y as usize;
287− self.terminal
220+ pub fn render_prompt(&mut self, text_color: Style) {
288− .backend_mut()
221+ use tui::backend::Backend;
222+ let view = self.view.as_ref().unwrap();
223+ let style: Style = view.theme.get("ui.linenr");
224+ let last_line = view.last_line();
225+ for (i, line) in (view.first_line..last_line).enumerate() {
289− .draw(self.cache.diff(&self.surface).into_iter());
226+ self.surface
290− // swap the buffer
227+ .set_stringn(0, i as u16, format!("{:>5}", line + 1), 5, style);
291− std::mem::swap(&mut self.surface, &mut self.cache);
228+ // lavender
293− execute!(stdout, cursor::MoveTo(pos.col as u16, pos.row as u16));
231+ // let lines = state
233+ // .lines_at(self.first_line as usize)
234+ // .take(self.size.1 as usize)
235+ // .map(|x| x.as_str().unwrap());
237+ // // iterate over selections and render them
238+ // let select = Style::default().bg(tui::style::Color::LightBlue);
239+ // let text = state.doc.slice(..);
240+ // for range in state.selection.ranges() {
241+ // // get terminal coords for x,y for each range pos
242+ // // TODO: this won't work with multiline
243+ // let (y1, x1) = coords_at_pos(&text, range.from());
244+ // let (y2, x2) = coords_at_pos(&text, range.to());
245+ // let area = Rect::new(
248+ // (x2 - x1 + 1) as u16,
249+ // (y2 - y1 + 1) as u16,
251+ // self.surface.set_style(area, select);
253+ // // TODO: don't highlight next char in append mode
257+ let mode = match view.state.mode() {
258+ Mode::Insert => "INS",
259+ Mode::Normal => "NOR",
260+ Mode::Goto => "GOTO",
261+ Mode::Command => ":",
263+ self.surface.set_style(
264+ Rect::new(0, self.size.1 - 1, self.size.0, 1),
265+ view.theme.get("ui.statusline"),
268+ .set_string(1, self.size.1 - 1, mode, text_color);
271+ .draw(self.cache.diff(&self.surface).into_iter());
273+ std::mem::swap(&mut self.surface, &mut self.cache);
276+ pub fn render_cursor(&mut self, viewport: Rect, text_color: Style) {
277+ let mut pos: Position;
278+ let view = self.view.as_ref().unwrap();
279+ let mut stdout = stdout();
280+ match view.state.mode() {
281+ Mode::Insert => write!(stdout, "\x1B[6 q"),
282+ mode => write!(stdout, "\x1B[2 q"),
284+ if view.state.mode() == Mode::Command {
285+ pos = Position::new(self.size.0 as usize, 2);
287+ if let Some(path) = view.state.path() {
289+ .set_string(6, self.size.1 - 1, path.to_string_lossy(), text_color);
292+ let cursor = view.state.selection().cursor();
295+ .screen_coords_at_pos(&view.state.doc().slice(..), cursor)
296+ .expect("Cursor is out of bounds.");
297+ pos.col += viewport.x as usize;
298+ pos.row += viewport.y as usize;
301+ execute!(stdout, cursor::MoveTo(pos.col as u16, pos.row as u16));
299 pub async fn event_loop(&mut self) {
304 pub async fn event_loop(&mut self) {
@@ -331,9 +336,9 @@ impl Editor {
331 // TODO: sequences (`gg`)
336 // TODO: sequences (`gg`)
332 // TODO: handle count other than 1
337 // TODO: handle count other than 1
333 if let Some(view) = &mut self.view {
338 if let Some(view) = &mut self.view {
339+ let keys = vec![event];
334 match view.state.mode() {
340 match view.state.mode() {
335 Mode::Insert => {
341 Mode::Insert => {
336− let keys = vec![event];
337 if let Some(command) = keymap[&Mode::Insert].get(&keys) {
342 if let Some(command) = keymap[&Mode::Insert].get(&keys) {
338 command(view, 1);
343 command(view, 1);
339 } else if let KeyEvent {
344 } else if let KeyEvent {
@@ -344,44 +349,25 @@ impl Editor {
344 commands::insert::insert_char(view, c);
349 commands::insert::insert_char(view, c);
346 view.ensure_cursor_in_view();
351 view.ensure_cursor_in_view();
348 self.render();
352 self.render();
350− Mode::Normal => {
354+ Mode::Command => {
351− let keys = vec![event];
352− if let Some(command) = keymap[&Mode::Normal].get(&keys) {
355+ if let Some(command) = keymap[&Mode::Command].get(&keys) {
353 command(view, 1);
356 command(view, 1);
355− // TODO: simplistic ensure cursor in view for now
356− view.ensure_cursor_in_view();
358 self.render();
357 self.render();
361− Mode::Goto => {
360+ mode => {
362− // TODO: handle modes and sequences (`gg`)
363− let keys = vec![event];
364− if let Some(command) = keymap[&Mode::Goto].get(&keys) {
361+ if let Some(command) = keymap[&mode].get(&keys) {
365− // TODO: handle count other than 1
366 command(view, 1);
362 command(view, 1);
368 // TODO: simplistic ensure cursor in view for now
364 // TODO: simplistic ensure cursor in view for now
369 view.ensure_cursor_in_view();
365 view.ensure_cursor_in_view();
375− // TODO: handle modes and sequences (`gg`)
376− let keys = vec![event];
377− if let Some(command) = keymap[&Mode::Goto].get(&keys) {
378− // TODO: handle count other than 1
381 self.render();
366 self.render();
387 Some(Ok(_)) => {
373 Some(Ok(_)) => {