on master help sign in
Merge pull request #6 from helix-editor/compositor

Compositor v2 rewrite

commitb12a6dc8303bbc1b4b08a9abb4668741d154adbd authorJan Hrastnik <jan.hrastnik2@gmail.com> dateSun 13 Dec 2020, 12:30 (6 years ago) parentb7a3e5207801b6
16 files changed+970−584
Cargo.lock+21−30 helix-lsp/src/transport.rs−1 helix-syntax/languages.toml+5 helix-term/Cargo.toml+1 helix-term/src/application.rs+62−517 helix-view/src/commands.rs → helix-term/src/commands.rs+59−4 helix-term/src/component.rs−20 helix-term/src/compositor.rs+155 helix-term/src/helix.log helix-view/src/keymap.rs → helix-term/src/keymap.rs+2−2 helix-term/src/main.rs+5 helix-term/src/terminal.rs+221 helix-term/src/ui/editor.rs+327 helix-term/src/ui/mod.rs+14 helix-view/src/prompt.rs → helix-term/src/ui/prompt.rs+98−7 helix-view/src/lib.rs−3
Cargo.lock+21−30
@@ -11,9 +11,9 @@ dependencies = [
1111
1212[[package]]
1313name = "anyhow"
14−version = "1.0.34"
14+version = "1.0.35"
1515source = "registry+https://github.com/rust-lang/crates.io-index"
16−checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7"
16+checksum = "2c0df63cb2955042487fad3aefd2c6e3ae7389ac5dc1beb28921de0b69f779d4"
1717
1818[[package]]
1919name = "arrayref"
@@ -195,9 +195,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
195195
196196[[package]]
197197name = "cc"
198−version = "1.0.65"
198+version = "1.0.66"
199199source = "registry+https://github.com/rust-lang/crates.io-index"
200−checksum = "95752358c8f7552394baf48cd82695b345628ad3f170d607de3ca03b8dacca15"
200+checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
201201dependencies = [
202202 "jobserver",
203203]
@@ -242,15 +242,6 @@ dependencies = [
242242 "vec_map",
243243]
244244
245−[[package]]
246−name = "cloudabi"
247−version = "0.1.0"
248−source = "registry+https://github.com/rust-lang/crates.io-index"
249−checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467"
250−dependencies = [
251− "bitflags",
252−]
253−
254245[[package]]
255246name = "concurrent-queue"
256247version = "1.2.2"
@@ -522,6 +513,7 @@ dependencies = [
522513 "helix-view",
523514 "log",
524515 "num_cpus",
516+ "once_cell",
525517 "smol",
526518 "tui",
527519]
@@ -614,9 +606,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
614606
615607[[package]]
616608name = "libc"
617−version = "0.2.80"
609+version = "0.2.81"
618610source = "registry+https://github.com/rust-lang/crates.io-index"
619−checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
611+checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
620612
621613[[package]]
622614name = "lock_api"
@@ -776,12 +768,11 @@ dependencies = [
776768
777769[[package]]
778770name = "parking_lot_core"
779−version = "0.8.0"
771+version = "0.8.1"
780772source = "registry+https://github.com/rust-lang/crates.io-index"
781−checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b"
773+checksum = "d7c6d9b8427445284a09c55be860a15855ab580a417ccad9da88f5a06787ced0"
782774dependencies = [
783− "cfg-if 0.1.10",
784− "cloudabi",
775+ "cfg-if 1.0.0",
785776 "instant",
786777 "libc",
787778 "redox_syscall",
@@ -946,18 +937,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
946937
947938[[package]]
948939name = "serde"
949−version = "1.0.117"
940+version = "1.0.118"
950941source = "registry+https://github.com/rust-lang/crates.io-index"
951−checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
942+checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800"
952943dependencies = [
953944 "serde_derive",
954945]
955946
956947[[package]]
957948name = "serde_derive"
958−version = "1.0.117"
949+version = "1.0.118"
959950source = "registry+https://github.com/rust-lang/crates.io-index"
960−checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
951+checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
961952dependencies = [
962953 "proc-macro2",
963954 "quote",
@@ -966,9 +957,9 @@ dependencies = [
966957
967958[[package]]
968959name = "serde_json"
969−version = "1.0.59"
960+version = "1.0.60"
970961source = "registry+https://github.com/rust-lang/crates.io-index"
971−checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95"
962+checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779"
972963dependencies = [
973964 "itoa",
974965 "ryu",
@@ -1023,9 +1014,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
10231014
10241015[[package]]
10251016name = "smallvec"
1026−version = "1.5.0"
1017+version = "1.5.1"
10271018source = "registry+https://github.com/rust-lang/crates.io-index"
1028−checksum = "7acad6f34eb9e8a259d3283d1e8c1d34d7415943d4895f65cc73813c7396fc85"
1019+checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75"
10291020
10301021[[package]]
10311022name = "smol"
@@ -1059,9 +1050,9 @@ dependencies = [
10591050
10601051[[package]]
10611052name = "syn"
1062−version = "1.0.53"
1053+version = "1.0.54"
10631054source = "registry+https://github.com/rust-lang/crates.io-index"
1064−checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68"
1055+checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44"
10651056dependencies = [
10661057 "proc-macro2",
10671058 "quote",
@@ -1155,7 +1146,7 @@ dependencies = [
11551146[[package]]
11561147name = "tui"
11571148version = "0.13.0"
1158−source = "git+https://github.com/fdehau/tui-rs#efdd6bfb193dafcb5e3bdc75e7d2d314065da1d7"
1149+source = "git+https://github.com/fdehau/tui-rs#74243394d90ea1316b6bedac6c9e4f26971c76b6"
11591150dependencies = [
11601151 "bitflags",
11611152 "cassowary",
helix-lsp/src/transport.rs−1
@@ -177,7 +177,6 @@ impl Transport {
177177 .expect("pending_request with id not found!");
178178 tx.send(Err(error.into())).await?;
179179 }
180− msg => unimplemented!("{:?}", msg),
181180 }
182181 Ok(())
183182 }
helix-syntax/languages.tomlnew file+5
@@ -0,0 +1,5 @@
1+[[language]]
2+name = "rust"
3+scope = "source.rust"
4+injection-regex = "rust"
5+file-types = ["rs"]
helix-term/Cargo.toml+1
@@ -17,6 +17,7 @@ helix-view = { path = "../helix-view", features = ["term"]}
1717helix-lsp = { path = "../helix-lsp"}
1818
1919anyhow = "1"
20+once_cell = "1.4"
2021
2122smol = "1"
2223num_cpus = "1"
helix-term/src/application.rs+62−517
@@ -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 }
helix-view/src/commands.rs → helix-term/src/commands.rsrenamed+59−4
@@ -6,18 +6,24 @@ use helix_core::{
66 state::{Direction, Granularity, State},
77 ChangeSet, Range, Selection, Tendril, Transaction,
88};
9+
910use once_cell::sync::Lazy;
1011
11−use crate::{
12+use crate::compositor::Compositor;
13+use crate::ui::Prompt;
14+
15+use helix_view::{
1216 document::Mode,
13− prompt::Prompt,
1417 view::{View, PADDING},
18+ Editor,
1519};
1620
1721pub struct Context<'a, 'b> {
1822 pub count: usize,
1923 pub view: &'a mut View,
2024 pub executor: &'a smol::Executor<'b>,
25+
26+ pub callback: Option<crate::compositor::Callback>,
2127}
2228
2329/// A command is a function that takes the current state and a count, and does a side-effect on the
@@ -331,8 +337,57 @@ pub fn append_mode(cx: &mut Context) {
331337}
332338
333339// TODO: I, A, o and O can share a lot of the primitives.
334−pub fn command_mode(_cx: &mut Context) {
335− unimplemented!()
340+pub fn command_mode(cx: &mut Context) {
341+ cx.callback = Some(Box::new(|compositor: &mut Compositor| {
342+ let prompt = Prompt::new(
343+ ":".to_owned(),
344+ |_input: &str| {
345+ // TODO: i need this duplicate list right now to avoid borrow checker issues
346+ let command_list = vec![
347+ String::from("q"),
348+ String::from("aaa"),
349+ String::from("bbb"),
350+ String::from("ccc"),
351+ String::from("ddd"),
352+ String::from("eee"),
353+ String::from("averylongcommandaverylongcommandaverylongcommandaverylongcommandaverylongcommand"),
354+ String::from("q"),
355+ String::from("aaa"),
356+ String::from("bbb"),
357+ String::from("ccc"),
358+ String::from("ddd"),
359+ String::from("eee"),
360+ String::from("q"),
361+ String::from("aaa"),
362+ String::from("bbb"),
363+ String::from("ccc"),
364+ String::from("ddd"),
365+ String::from("eee"),
366+ String::from("q"),
367+ String::from("aaa"),
368+ String::from("bbb"),
369+ String::from("ccc"),
370+ String::from("ddd"),
371+ String::from("eee"),
372+ String::from("q"),
373+ String::from("aaa"),
374+ String::from("bbb"),
375+ String::from("ccc"),
376+ String::from("ddd"),
377+ String::from("eee"),
378+ ];
379+ command_list
380+ .into_iter()
381+ .filter(|command| command.contains(_input))
382+ .collect()
383+ }, // completion
384+ |editor: &mut Editor, input: &str| match input {
385+ "q" => editor.should_close = true,
386+ _ => (),
387+ },
388+ );
389+ compositor.push(Box::new(prompt));
390+ }));
336391}
337392
338393// calculate line numbers for each selection range
helix-term/src/component.rsdeleted−20
@@ -1,20 +0,0 @@
1−// IDEA: render to a cache buffer, then if not changed, copy the buf into the parent
2−type Surface = ();
3−pub trait Component {
4− /// Process input events, return true if handled.
5− fn process_event(&mut self, event: crossterm::event::Event, args: ()) -> bool;
6− /// Should redraw? Useful for saving redraw cycles if we know component didn't change.
7− fn should_update(&self) -> bool {
8− true
9− }
10−
11− fn render(&mut self, surface: &mut Surface, args: ());
12−}
13−
14−// HStack / VStack
15−// focus by component id: each View/Editor gets it's own incremental id at create
16−// Component: View(Arc<State>) -> multiple views can point to same state
17−// id 0 = prompt?
18−// when entering to prompt, it needs to direct Commands to last focus window
19−// -> prompt.trigger(focus_id), on_leave -> focus(focus_id)
20−// popups on another layer
helix-term/src/compositor.rsnew file+155
@@ -0,0 +1,155 @@
1+// Features:
2+// Tracks currently focused component which receives all input
3+// Event loop is external as opposed to cursive-rs
4+// Calls render on the component and translates screen coords to local component coords
5+//
6+// TODO:
7+// Q: where is the Application state stored? do we store it into an external static var?
8+// A: probably makes sense to initialize the editor into a `static Lazy<>` global var.
9+//
10+// Q: how do we composit nested structures? There should be sub-components/views
11+//
12+// Each component declares it's own size constraints and gets fitted based on it's parent.
13+// Q: how does this work with popups?
14+// cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>)
15+
16+use crossterm::event::Event;
17+use helix_core::Position;
18+use smol::Executor;
19+use tui::buffer::Buffer as Surface;
20+use tui::layout::Rect;
21+
22+pub type Callback = Box<dyn Fn(&mut Compositor)>;
23+
24+// --> EventResult should have a callback that takes a context with methods like .popup(),
25+// .prompt() etc. That way we can abstract it from the renderer.
26+// Q: How does this interact with popups where we need to be able to specify the rendering of the
27+// popup?
28+// A: It could just take a textarea.
29+//
30+// If Compositor was specified in the callback that's then problematic because of
31+
32+// Cursive-inspired
33+pub enum EventResult {
34+ Ignored,
35+ Consumed(Option<Callback>),
36+}
37+
38+use helix_view::{Editor, View};
39+// shared with commands.rs
40+pub struct Context<'a> {
41+ pub editor: &'a mut Editor,
42+ pub executor: &'static smol::Executor<'static>,
43+}
44+
45+pub trait Component {
46+ /// Process input events, return true if handled.
47+ fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult;
48+ // , args: ()
49+
50+ /// Should redraw? Useful for saving redraw cycles if we know component didn't change.
51+ fn should_update(&self) -> bool {
52+ true
53+ }
54+
55+ fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context);
56+
57+ fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
58+ None
59+ }
60+}
61+
62+// struct Editor { };
63+
64+// For v1:
65+// Child views are something each view needs to handle on it's own for now, positioning and sizing
66+// options, focus tracking. In practice this is simple: we only will need special solving for
67+// splits etc
68+
69+// impl Editor {
70+// fn render(&mut self, surface: &mut Surface, args: ()) {
71+// // compute x, y, w, h rects for sub-views!
72+// // get surface area
73+// // get constraints for textarea, statusbar
74+// // -> cassowary-rs
75+
76+// // first render textarea
77+// // then render statusbar
78+// }
79+// }
80+
81+// usecases to consider:
82+// - a single view with subviews (textarea + statusbar)
83+// - a popup panel / dialog with it's own interactions
84+// - an autocomplete popup that doesn't change focus
85+
86+//fn main() {
87+// let root = Editor::new();
88+// let compositor = Compositor::new();
89+
90+// compositor.push(root);
91+
92+// // pos: clip to bottom of screen
93+// compositor.push_at(pos, Prompt::new(
94+// ":",
95+// (),
96+// |input: &str| match input {}
97+// )); // TODO: this Prompt needs to somehow call compositor.pop() on close, but it can't refer to parent
98+// // Cursive solves this by allowing to return a special result on process_event
99+// // that's either Ignore | Consumed(Opt<C>) where C: fn (Compositor) -> ()
100+
101+// // TODO: solve popup focus: we want to push autocomplete popups on top of the current layer
102+// // but retain the focus where it was. The popup will also need to update as we type into the
103+// // textarea. It should also capture certain input, such as tab presses etc
104+// //
105+// // 1) This could be faked by the top layer pushing down edits into the previous layer.
106+// // 2) Alternatively,
107+//}
108+
109+pub struct Compositor {
110+ layers: Vec<Box<dyn Component>>,
111+}
112+
113+impl Compositor {
114+ pub fn new() -> Self {
115+ Self { layers: Vec::new() }
116+ }
117+
118+ pub fn push(&mut self, layer: Box<dyn Component>) {
119+ self.layers.push(layer);
120+ }
121+
122+ pub fn pop(&mut self) {
123+ self.layers.pop();
124+ }
125+
126+ pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
127+ // TODO: custom focus
128+ if let Some(layer) = self.layers.last_mut() {
129+ return match layer.handle_event(event, cx) {
130+ EventResult::Consumed(Some(callback)) => {
131+ callback(self);
132+ true
133+ }
134+ EventResult::Consumed(None) => true,
135+ EventResult::Ignored => false,
136+ };
137+ }
138+ false
139+ }
140+
141+ pub fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
142+ for layer in &self.layers {
143+ layer.render(area, surface, cx)
144+ }
145+ }
146+
147+ pub fn cursor_position(&self, area: Rect, cx: &mut Context) -> Position {
148+ for layer in self.layers.iter().rev() {
149+ if let Some(pos) = layer.cursor_position(area, cx) {
150+ return pos;
151+ }
152+ }
153+ panic!("No layer returned a position!");
154+ }
155+}
helix-term/src/helix.lognew file

No changes to the content.

helix-view/src/keymap.rs → helix-term/src/keymap.rsrenamed+2−2
@@ -1,6 +1,6 @@
11use crate::commands::{self, Command};
2−use crate::document::Mode;
32use helix_core::hashmap;
3+use helix_view::document::Mode;
44use std::collections::HashMap;
55
66// Kakoune-inspired:
@@ -87,7 +87,7 @@ use std::collections::HashMap;
8787// gr = goto reference
8888// }
8989
90−#[cfg(feature = "term")]
90+// #[cfg(feature = "term")]
9191pub use crossterm::event::{KeyCode, KeyEvent as Key, KeyModifiers as Modifiers};
9292
9393// TODO: could be trie based
helix-term/src/main.rs+5
@@ -1,6 +1,11 @@
11#![allow(unused)]
22
33mod application;
4+mod commands;
5+mod compositor;
6+mod keymap;
7+mod terminal;
8+mod ui;
49
510use application::Application;
611
helix-term/src/terminal.rsnew file+221
@@ -0,0 +1,221 @@
1+use std::io;
2+use tui::{
3+ backend::Backend,
4+ buffer::Buffer,
5+ layout::Rect,
6+ widgets::{StatefulWidget, Widget},
7+};
8+
9+#[derive(Debug, Clone, PartialEq)]
10+/// UNSTABLE
11+enum ResizeBehavior {
12+ Fixed,
13+ Auto,
14+}
15+
16+#[derive(Debug, Clone, PartialEq)]
17+/// UNSTABLE
18+pub struct Viewport {
19+ area: Rect,
20+ resize_behavior: ResizeBehavior,
21+}
22+
23+impl Viewport {
24+ /// UNSTABLE
25+ pub fn fixed(area: Rect) -> Viewport {
26+ Viewport {
27+ area,
28+ resize_behavior: ResizeBehavior::Fixed,
29+ }
30+ }
31+}
32+
33+#[derive(Debug, Clone, PartialEq)]
34+/// Options to pass to [`Terminal::with_options`]
35+pub struct TerminalOptions {
36+ /// Viewport used to draw to the terminal
37+ pub viewport: Viewport,
38+}
39+
40+/// Interface to the terminal backed by Termion
41+#[derive(Debug)]
42+pub struct Terminal<B>
43+where
44+ B: Backend,
45+{
46+ backend: B,
47+ /// Holds the results of the current and previous draw calls. The two are compared at the end
48+ /// of each draw pass to output the necessary updates to the terminal
49+ buffers: [Buffer; 2],
50+ /// Index of the current buffer in the previous array
51+ current: usize,
52+ /// Whether the cursor is currently hidden
53+ hidden_cursor: bool,
54+ /// Viewport
55+ viewport: Viewport,
56+}
57+
58+impl<B> Drop for Terminal<B>
59+where
60+ B: Backend,
61+{
62+ fn drop(&mut self) {
63+ // Attempt to restore the cursor state
64+ if self.hidden_cursor {
65+ if let Err(err) = self.show_cursor() {
66+ eprintln!("Failed to show the cursor: {}", err);
67+ }
68+ }
69+ }
70+}
71+
72+impl<B> Terminal<B>
73+where
74+ B: Backend,
75+{
76+ /// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
77+ /// default colors for the foreground and the background
78+ pub fn new(backend: B) -> io::Result<Terminal<B>> {
79+ let size = backend.size()?;
80+ Terminal::with_options(
81+ backend,
82+ TerminalOptions {
83+ viewport: Viewport {
84+ area: size,
85+ resize_behavior: ResizeBehavior::Auto,
86+ },
87+ },
88+ )
89+ }
90+
91+ /// UNSTABLE
92+ pub fn with_options(backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
93+ Ok(Terminal {
94+ backend,
95+ buffers: [
96+ Buffer::empty(options.viewport.area),
97+ Buffer::empty(options.viewport.area),
98+ ],
99+ current: 0,
100+ hidden_cursor: false,
101+ viewport: options.viewport,
102+ })
103+ }
104+
105+ // /// Get a Frame object which provides a consistent view into the terminal state for rendering.
106+ // pub fn get_frame(&mut self) -> Frame<B> {
107+ // Frame {
108+ // terminal: self,
109+ // cursor_position: None,
110+ // }
111+ // }
112+
113+ pub fn current_buffer_mut(&mut self) -> &mut Buffer {
114+ &mut self.buffers[self.current]
115+ }
116+
117+ pub fn backend(&self) -> &B {
118+ &self.backend
119+ }
120+
121+ pub fn backend_mut(&mut self) -> &mut B {
122+ &mut self.backend
123+ }
124+
125+ /// Obtains a difference between the previous and the current buffer and passes it to the
126+ /// current backend for drawing.
127+ pub fn flush(&mut self) -> io::Result<()> {
128+ let previous_buffer = &self.buffers[1 - self.current];
129+ let current_buffer = &self.buffers[self.current];
130+ let updates = previous_buffer.diff(current_buffer);
131+ self.backend.draw(updates.into_iter())
132+ }
133+
134+ /// Updates the Terminal so that internal buffers match the requested size. Requested size will
135+ /// be saved so the size can remain consistent when rendering.
136+ /// This leads to a full clear of the screen.
137+ pub fn resize(&mut self, area: Rect) -> io::Result<()> {
138+ self.buffers[self.current].resize(area);
139+ self.buffers[1 - self.current].resize(area);
140+ self.viewport.area = area;
141+ self.clear()
142+ }
143+
144+ /// Queries the backend for size and resizes if it doesn't match the previous size.
145+ pub fn autoresize(&mut self) -> io::Result<()> {
146+ if self.viewport.resize_behavior == ResizeBehavior::Auto {
147+ let size = self.size()?;
148+ if size != self.viewport.area {
149+ self.resize(size)?;
150+ }
151+ };
152+ Ok(())
153+ }
154+
155+ /// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
156+ /// and prepares for the next draw call.
157+ pub fn draw(&mut self) -> io::Result<()> {
158+ // // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
159+ // // and the terminal (if growing), which may OOB.
160+ // self.autoresize()?;
161+
162+ // let mut frame = self.get_frame();
163+ // f(&mut frame);
164+ // // We can't change the cursor position right away because we have to flush the frame to
165+ // // stdout first. But we also can't keep the frame around, since it holds a &mut to
166+ // // Terminal. Thus, we're taking the important data out of the Frame and dropping it.
167+ // let cursor_position = frame.cursor_position;
168+
169+ // Draw to stdout
170+ self.flush()?;
171+
172+ // match cursor_position {
173+ // None => self.hide_cursor()?,
174+ // Some((x, y)) => {
175+ // self.show_cursor()?;
176+ // self.set_cursor(x, y)?;
177+ // }
178+ // }
179+
180+ // Swap buffers
181+ self.buffers[1 - self.current].reset();
182+ self.current = 1 - self.current;
183+
184+ // Flush
185+ self.backend.flush()?;
186+ Ok(())
187+ }
188+
189+ pub fn hide_cursor(&mut self) -> io::Result<()> {
190+ self.backend.hide_cursor()?;
191+ self.hidden_cursor = true;
192+ Ok(())
193+ }
194+
195+ pub fn show_cursor(&mut self) -> io::Result<()> {
196+ self.backend.show_cursor()?;
197+ self.hidden_cursor = false;
198+ Ok(())
199+ }
200+
201+ pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
202+ self.backend.get_cursor()
203+ }
204+
205+ pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
206+ self.backend.set_cursor(x, y)
207+ }
208+
209+ /// Clear the terminal and force a full redraw on the next draw call.
210+ pub fn clear(&mut self) -> io::Result<()> {
211+ self.backend.clear()?;
212+ // Reset the back buffer to make sure the next update will redraw everything.
213+ self.buffers[1 - self.current].reset();
214+ Ok(())
215+ }
216+
217+ /// Queries the real size of the backend.
218+ pub fn size(&self) -> io::Result<Rect> {
219+ self.backend.size()
220+ }
221+}
helix-term/src/ui/editor.rsnew file+327
@@ -0,0 +1,327 @@
1+use crate::commands;
2+use crate::compositor::{Component, Compositor, Context, EventResult};
3+use crate::keymap::{self, Keymaps};
4+use crate::ui::text_color;
5+
6+use helix_core::{indent::TAB_WIDTH, syntax::HighlightEvent, Position, Range, State};
7+use helix_view::{document::Mode, Document, Editor, Theme, View};
8+use std::borrow::Cow;
9+
10+use crossterm::{
11+ cursor,
12+ event::{read, Event, EventStream, KeyCode, KeyEvent},
13+};
14+use tui::{
15+ backend::CrosstermBackend,
16+ buffer::Buffer as Surface,
17+ layout::Rect,
18+ style::{Color, Modifier, Style},
19+};
20+
21+pub struct EditorView {
22+ keymap: Keymaps,
23+}
24+
25+const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
26+
27+impl EditorView {
28+ pub fn new() -> Self {
29+ Self {
30+ keymap: keymap::default(),
31+ }
32+ }
33+ pub fn render_view(
34+ &self,
35+ view: &mut View,
36+ viewport: Rect,
37+ surface: &mut Surface,
38+ theme: &Theme,
39+ ) {
40+ let area = Rect::new(OFFSET, 0, viewport.width - OFFSET, viewport.height - 2); // - 2 for statusline and prompt
41+ self.render_buffer(view, area, surface, theme);
42+ let area = Rect::new(0, viewport.height - 2, viewport.width, 1);
43+ self.render_statusline(view, area, surface, theme);
44+ }
45+
46+ // TODO: ideally not &mut View but highlights require it because of cursor cache
47+ pub fn render_buffer(
48+ &self,
49+ view: &mut View,
50+ viewport: Rect,
51+ surface: &mut Surface,
52+ theme: &Theme,
53+ ) {
54+ // clear with background color
55+ surface.set_style(viewport, theme.get("ui.background"));
56+
57+ // TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
58+ let source_code = view.doc.text().to_string();
59+
60+ let last_line = view.last_line();
61+
62+ let range = {
63+ // calculate viewport byte ranges
64+ let start = view.doc.text().line_to_byte(view.first_line);
65+ let end = view.doc.text().line_to_byte(last_line)
66+ + view.doc.text().line(last_line).len_bytes();
67+
68+ start..end
69+ };
70+
71+ // TODO: range doesn't actually restrict source, just highlight range
72+ // TODO: cache highlight results
73+ // TODO: only recalculate when state.doc is actually modified
74+ let highlights: Vec<_> = match view.doc.syntax.as_mut() {
75+ Some(syntax) => {
76+ syntax
77+ .highlight_iter(source_code.as_bytes(), Some(range), None, |_| None)
78+ .unwrap()
79+ .collect() // TODO: we collect here to avoid double borrow, fix later
80+ }
81+ None => vec![Ok(HighlightEvent::Source {
82+ start: range.start,
83+ end: range.end,
84+ })],
85+ };
86+ let mut spans = Vec::new();
87+ let mut visual_x = 0;
88+ let mut line = 0u16;
89+ let visible_selections: Vec<Range> = view
90+ .doc
91+ .state
92+ .selection()
93+ .ranges()
94+ .iter()
95+ // TODO: limit selection to one in viewport
96+ // .filter(|range| !range.is_empty()) // && range.overlaps(&Range::new(start, end + 1))
97+ .copied()
98+ .collect();
99+
100+ 'outer: for event in highlights {
101+ match event.unwrap() {
102+ HighlightEvent::HighlightStart(span) => {
103+ spans.push(span);
104+ }
105+ HighlightEvent::HighlightEnd => {
106+ spans.pop();
107+ }
108+ HighlightEvent::Source { start, end } => {
109+ // TODO: filter out spans out of viewport for now..
110+
111+ let start = view.doc.text().byte_to_char(start);
112+ let end = view.doc.text().byte_to_char(end); // <-- index 744, len 743
113+
114+ let text = view.doc.text().slice(start..end);
115+
116+ use helix_core::graphemes::{grapheme_width, RopeGraphemes};
117+
118+ let style = match spans.first() {
119+ Some(span) => theme.get(theme.scopes()[span.0].as_str()),
120+ None => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender
121+ };
122+
123+ // TODO: we could render the text to a surface, then cache that, that
124+ // way if only the selection/cursor changes we can copy from cache
125+ // and paint the new cursor.
126+
127+ let mut char_index = start;
128+
129+ // iterate over range char by char
130+ for grapheme in RopeGraphemes::new(&text) {
131+ // TODO: track current char_index
132+
133+ if grapheme == "\n" {
134+ visual_x = 0;
135+ line += 1;
136+
137+ // TODO: with proper iter this shouldn't be necessary
138+ if line >= viewport.height {
139+ break 'outer;
140+ }
141+ } else if grapheme == "\t" {
142+ visual_x += (TAB_WIDTH as u16);
143+ } else {
144+ // Cow will prevent allocations if span contained in a single slice
145+ // which should really be the majority case
146+ let grapheme = Cow::from(grapheme);
147+ let width = grapheme_width(&grapheme) as u16;
148+
149+ // TODO: this should really happen as an after pass
150+ let style = if visible_selections
151+ .iter()
152+ .any(|range| range.contains(char_index))
153+ {
154+ // cedar
155+ style.clone().bg(Color::Rgb(128, 47, 0))
156+ } else {
157+ style
158+ };
159+
160+ let style = if visible_selections
161+ .iter()
162+ .any(|range| range.head == char_index)
163+ {
164+ style.clone().bg(Color::Rgb(255, 255, 255))
165+ } else {
166+ style
167+ };
168+
169+ // ugh, improve with a traverse method
170+ // or interleave highlight spans with selection and diagnostic spans
171+ let style = if view.doc.diagnostics.iter().any(|diagnostic| {
172+ diagnostic.range.0 <= char_index && diagnostic.range.1 > char_index
173+ }) {
174+ style.clone().add_modifier(Modifier::UNDERLINED)
175+ } else {
176+ style
177+ };
178+
179+ // TODO: paint cursor heads except primary
180+
181+ surface.set_string(
182+ viewport.x + visual_x,
183+ viewport.y + line,
184+ grapheme,
185+ style,
186+ );
187+
188+ visual_x += width;
189+ }
190+
191+ char_index += 1;
192+ }
193+ }
194+ }
195+ }
196+
197+ let style: Style = theme.get("ui.linenr");
198+ let warning: Style = theme.get("warning");
199+ let last_line = view.last_line();
200+ for (i, line) in (view.first_line..last_line).enumerate() {
201+ if view.doc.diagnostics.iter().any(|d| d.line == line) {
202+ surface.set_stringn(0, i as u16, "●", 1, warning);
203+ }
204+
205+ surface.set_stringn(1, i as u16, format!("{:>5}", line + 1), 5, style);
206+ }
207+ }
208+
209+ pub fn render_statusline(
210+ &self,
211+ view: &View,
212+ viewport: Rect,
213+ surface: &mut Surface,
214+ theme: &Theme,
215+ ) {
216+ let text_color = text_color();
217+ let mode = match view.doc.mode() {
218+ Mode::Insert => "INS",
219+ Mode::Normal => "NOR",
220+ Mode::Goto => "GOTO",
221+ };
222+ // statusline
223+ surface.set_style(
224+ Rect::new(0, viewport.y, viewport.width, 1),
225+ theme.get("ui.statusline"),
226+ );
227+ surface.set_string(1, viewport.y, mode, text_color);
228+
229+ if let Some(path) = view.doc.path() {
230+ surface.set_string(6, viewport.y, path.to_string_lossy(), text_color);
231+ }
232+
233+ surface.set_string(
234+ viewport.width - 10,
235+ viewport.y,
236+ format!("{}", view.doc.diagnostics.len()),
237+ text_color,
238+ );
239+ }
240+}
241+
242+impl Component for EditorView {
243+ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
244+ match event {
245+ Event::Resize(width, height) => {
246+ // TODO: simplistic ensure cursor in view for now
247+ // TODO: loop over views
248+ if let Some(view) = cx.editor.view_mut() {
249+ view.size = (width, height);
250+ view.ensure_cursor_in_view()
251+ };
252+ EventResult::Consumed(None)
253+ }
254+ Event::Key(event) => {
255+ if let Some(view) = cx.editor.view_mut() {
256+ let keys = vec![event];
257+ // TODO: sequences (`gg`)
258+ let mode = view.doc.mode();
259+ // TODO: handle count other than 1
260+ let mut cx = commands::Context {
261+ view,
262+ executor: cx.executor,
263+ count: 1,
264+ callback: None,
265+ };
266+
267+ match mode {
268+ Mode::Insert => {
269+ if let Some(command) = self.keymap[&Mode::Insert].get(&keys) {
270+ command(&mut cx);
271+ } else if let KeyEvent {
272+ code: KeyCode::Char(c),
273+ ..
274+ } = event
275+ {
276+ commands::insert::insert_char(&mut cx, c);
277+ }
278+ }
279+ mode => {
280+ if let Some(command) = self.keymap[&mode].get(&keys) {
281+ command(&mut cx);
282+
283+ // TODO: simplistic ensure cursor in view for now
284+ }
285+ }
286+ }
287+ // appease borrowck
288+ let callback = cx.callback.take();
289+
290+ view.ensure_cursor_in_view();
291+
292+ EventResult::Consumed(callback)
293+ } else {
294+ EventResult::Ignored
295+ }
296+ }
297+ Event::Mouse(_) => EventResult::Ignored,
298+ }
299+ }
300+
301+ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
302+ // SAFETY: we cheat around the view_mut() borrow because it doesn't allow us to also borrow
303+ // theme. Theme is immutable mutating view won't disrupt theme_ref.
304+ let theme_ref = unsafe { &*(&cx.editor.theme as *const Theme) };
305+ if let Some(view) = cx.editor.view_mut() {
306+ self.render_view(view, area, surface, theme_ref);
307+ }
308+
309+ // TODO: drop unwrap
310+ }
311+
312+ fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
313+ // match view.doc.mode() {
314+ // Mode::Insert => write!(stdout, "\x1B[6 q"),
315+ // mode => write!(stdout, "\x1B[2 q"),
316+ // };
317+ let view = ctx.editor.view().unwrap();
318+ let cursor = view.doc.state.selection().cursor();
319+
320+ let mut pos = view
321+ .screen_coords_at_pos(&view.doc.text().slice(..), cursor)
322+ .expect("Cursor is out of bounds.");
323+ pos.col += area.x as usize + OFFSET as usize;
324+ pos.row += area.y as usize;
325+ Some(pos)
326+ }
327+}
helix-term/src/ui/mod.rsnew file+14
@@ -0,0 +1,14 @@
1+mod editor;
2+mod prompt;
3+
4+pub use editor::EditorView;
5+pub use prompt::Prompt;
6+
7+pub use tui::layout::Rect;
8+pub use tui::style::{Color, Modifier, Style};
9+
10+// TODO: temp
11+#[inline(always)]
12+pub fn text_color() -> Style {
13+ Style::default().fg(Color::Rgb(219, 191, 239)) // lilac
14+}
helix-view/src/prompt.rs → helix-term/src/ui/prompt.rsrenamed+98−7
@@ -1,5 +1,8 @@
1−use crate::Editor;
2−use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
1+use crate::compositor::{Component, Compositor, Context, EventResult};
2+use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
3+use helix_core::Position;
4+use helix_view::Editor;
5+use helix_view::Theme;
36use std::string::String;
47
58pub struct Prompt {
@@ -79,16 +82,91 @@ impl Prompt {
7982 pub fn exit_selection(&mut self) {
8083 self.completion_selection_index = None;
8184 }
85+}
86+
87+use tui::{
88+ buffer::Buffer as Surface,
89+ layout::Rect,
90+ style::{Color, Modifier, Style},
91+};
92+
93+const BASE_WIDTH: u16 = 30;
94+use crate::ui::text_color;
95+
96+impl Prompt {
97+ pub fn render_prompt(&self, area: Rect, surface: &mut Surface, theme: &Theme) {
98+ let text_color = text_color();
99+ // completion
100+ if !self.completion.is_empty() {
101+ // TODO: find out better way of clearing individual lines of the screen
102+ let mut row = 0;
103+ let mut col = 0;
104+ let max_col = area.width / BASE_WIDTH;
105+ let col_height = ((self.completion.len() as u16 + max_col - 1) / max_col);
106+
107+ for i in (3..col_height + 3) {
108+ surface.set_string(
109+ 0,
110+ area.height - i as u16,
111+ " ".repeat(area.width as usize),
112+ text_color,
113+ );
114+ }
115+ surface.set_style(
116+ Rect::new(0, area.height - col_height - 2, area.width, col_height),
117+ theme.get("ui.statusline"),
118+ );
119+ for (i, command) in self.completion.iter().enumerate() {
120+ let color = if self.completion_selection_index.is_some()
121+ && i == self.completion_selection_index.unwrap()
122+ {
123+ Style::default().bg(Color::Rgb(104, 060, 232))
124+ } else {
125+ text_color
126+ };
127+ surface.set_stringn(
128+ 1 + col * BASE_WIDTH,
129+ area.height - col_height - 2 + row,
130+ &command,
131+ BASE_WIDTH as usize - 1,
132+ color,
133+ );
134+ row += 1;
135+ if row > col_height - 1 {
136+ row = 0;
137+ col += 1;
138+ }
139+ if col > max_col {
140+ break;
141+ }
142+ }
143+ }
144+ // render buffer text
145+ surface.set_string(1, area.height - 1, &self.prompt, text_color);
146+ surface.set_string(2, area.height - 1, &self.line, text_color);
147+ }
148+}
149+
150+impl Component for Prompt {
151+ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
152+ let event = match event {
153+ Event::Key(event) => event,
154+ _ => return EventResult::Ignored,
155+ };
82156
83− pub fn handle_input(&mut self, key_event: KeyEvent, editor: &mut Editor) {
84− match key_event {
157+ match event {
85158 KeyEvent {
86159 code: KeyCode::Char(c),
87160 modifiers: KeyModifiers::NONE,
88161 } => self.insert_char(c),
89162 KeyEvent {
90163 code: KeyCode::Esc, ..
91− } => self.should_close = true,
164+ } => {
165+ return EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
166+ // remove the layer
167+ compositor.pop();
168+ })));
169+ }
92170 KeyEvent {
93171 code: KeyCode::Right,
94172 ..
@@ -112,7 +190,7 @@ impl Prompt {
112190 KeyEvent {
113191 code: KeyCode::Enter,
114192 ..
115− } => (self.callback_fn)(editor, &self.line),
193+ } => (self.callback_fn)(cx.editor, &self.line),
116194 KeyEvent {
117195 code: KeyCode::Tab, ..
118196 } => self.change_completion_selection(),
@@ -121,6 +199,19 @@ impl Prompt {
121199 modifiers: KeyModifiers::CONTROL,
122200 } => self.exit_selection(),
123201 _ => (),
124− }
202+ };
203+
204+ EventResult::Consumed(None)
205+ }
206+
207+ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
208+ self.render_prompt(area, surface, &cx.editor.theme)
209+ }
210+
211+ fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
212+ Some(Position::new(
213+ area.height as usize - 1,
214+ area.x as usize + 2 + self.cursor,
215+ ))
125216 }
126217}
helix-view/src/lib.rs−3
@@ -1,8 +1,5 @@
1−pub mod commands;
21pub mod document;
32pub mod editor;
4−pub mod keymap;
5−pub mod prompt;
63pub mod theme;
74pub mod view;
85