Merge pull request #6 from helix-editor/compositor

Compositor v2 rewrite
pull/8/head
Jan Hrastnik 4 years ago committed by GitHub
commit b12a6dc830
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

51
Cargo.lock generated

@ -11,9 +11,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.34" version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7" checksum = "2c0df63cb2955042487fad3aefd2c6e3ae7389ac5dc1beb28921de0b69f779d4"
[[package]] [[package]]
name = "arrayref" name = "arrayref"
@ -195,9 +195,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.65" version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95752358c8f7552394baf48cd82695b345628ad3f170d607de3ca03b8dacca15" checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
dependencies = [ dependencies = [
"jobserver", "jobserver",
] ]
@ -242,15 +242,6 @@ dependencies = [
"vec_map", "vec_map",
] ]
[[package]]
name = "cloudabi"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "1.2.2" version = "1.2.2"
@ -522,6 +513,7 @@ dependencies = [
"helix-view", "helix-view",
"log", "log",
"num_cpus", "num_cpus",
"once_cell",
"smol", "smol",
"tui", "tui",
] ]
@ -614,9 +606,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.80" version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@ -776,12 +768,11 @@ dependencies = [
[[package]] [[package]]
name = "parking_lot_core" name = "parking_lot_core"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" checksum = "d7c6d9b8427445284a09c55be860a15855ab580a417ccad9da88f5a06787ced0"
dependencies = [ dependencies = [
"cfg-if 0.1.10", "cfg-if 1.0.0",
"cloudabi",
"instant", "instant",
"libc", "libc",
"redox_syscall", "redox_syscall",
@ -946,18 +937,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.117" version = "1.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.117" version = "1.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -966,9 +957,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.59" version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -1023,9 +1014,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7acad6f34eb9e8a259d3283d1e8c1d34d7415943d4895f65cc73813c7396fc85" checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75"
[[package]] [[package]]
name = "smol" name = "smol"
@ -1059,9 +1050,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.53" version = "1.0.54"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68" checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1155,7 +1146,7 @@ dependencies = [
[[package]] [[package]]
name = "tui" name = "tui"
version = "0.13.0" version = "0.13.0"
source = "git+https://github.com/fdehau/tui-rs#efdd6bfb193dafcb5e3bdc75e7d2d314065da1d7" source = "git+https://github.com/fdehau/tui-rs#74243394d90ea1316b6bedac6c9e4f26971c76b6"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cassowary", "cassowary",

@ -177,7 +177,6 @@ impl Transport {
.expect("pending_request with id not found!"); .expect("pending_request with id not found!");
tx.send(Err(error.into())).await?; tx.send(Err(error.into())).await?;
} }
msg => unimplemented!("{:?}", msg),
} }
Ok(()) Ok(())
} }

@ -0,0 +1,5 @@
[[language]]
name = "rust"
scope = "source.rust"
injection-regex = "rust"
file-types = ["rs"]

@ -17,6 +17,7 @@ helix-view = { path = "../helix-view", features = ["term"]}
helix-lsp = { path = "../helix-lsp"} helix-lsp = { path = "../helix-lsp"}
anyhow = "1" anyhow = "1"
once_cell = "1.4"
smol = "1" smol = "1"
num_cpus = "1" num_cpus = "1"

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

@ -6,18 +6,24 @@ use helix_core::{
state::{Direction, Granularity, State}, state::{Direction, Granularity, State},
ChangeSet, Range, Selection, Tendril, Transaction, ChangeSet, Range, Selection, Tendril, Transaction,
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use crate::{ use crate::compositor::Compositor;
use crate::ui::Prompt;
use helix_view::{
document::Mode, document::Mode,
prompt::Prompt,
view::{View, PADDING}, view::{View, PADDING},
Editor,
}; };
pub struct Context<'a, 'b> { pub struct Context<'a, 'b> {
pub count: usize, pub count: usize,
pub view: &'a mut View, pub view: &'a mut View,
pub executor: &'a smol::Executor<'b>, pub executor: &'a smol::Executor<'b>,
pub callback: Option<crate::compositor::Callback>,
} }
/// A command is a function that takes the current state and a count, and does a side-effect on the /// 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) {
} }
// TODO: I, A, o and O can share a lot of the primitives. // TODO: I, A, o and O can share a lot of the primitives.
pub fn command_mode(_cx: &mut Context) { pub fn command_mode(cx: &mut Context) {
unimplemented!() cx.callback = Some(Box::new(|compositor: &mut Compositor| {
let prompt = Prompt::new(
":".to_owned(),
|_input: &str| {
// TODO: i need this duplicate list right now to avoid borrow checker issues
let command_list = vec![
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
String::from("averylongcommandaverylongcommandaverylongcommandaverylongcommandaverylongcommand"),
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
];
command_list
.into_iter()
.filter(|command| command.contains(_input))
.collect()
}, // completion
|editor: &mut Editor, input: &str| match input {
"q" => editor.should_close = true,
_ => (),
},
);
compositor.push(Box::new(prompt));
}));
} }
// calculate line numbers for each selection range // calculate line numbers for each selection range

@ -1,20 +0,0 @@
// IDEA: render to a cache buffer, then if not changed, copy the buf into the parent
type Surface = ();
pub trait Component {
/// Process input events, return true if handled.
fn process_event(&mut self, event: crossterm::event::Event, args: ()) -> bool;
/// Should redraw? Useful for saving redraw cycles if we know component didn't change.
fn should_update(&self) -> bool {
true
}
fn render(&mut self, surface: &mut Surface, args: ());
}
// HStack / VStack
// focus by component id: each View/Editor gets it's own incremental id at create
// Component: View(Arc<State>) -> multiple views can point to same state
// id 0 = prompt?
// when entering to prompt, it needs to direct Commands to last focus window
// -> prompt.trigger(focus_id), on_leave -> focus(focus_id)
// popups on another layer

@ -0,0 +1,155 @@
// Features:
// Tracks currently focused component which receives all input
// Event loop is external as opposed to cursive-rs
// Calls render on the component and translates screen coords to local component coords
//
// TODO:
// Q: where is the Application state stored? do we store it into an external static var?
// A: probably makes sense to initialize the editor into a `static Lazy<>` global var.
//
// Q: how do we composit nested structures? There should be sub-components/views
//
// Each component declares it's own size constraints and gets fitted based on it's parent.
// Q: how does this work with popups?
// cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>)
use crossterm::event::Event;
use helix_core::Position;
use smol::Executor;
use tui::buffer::Buffer as Surface;
use tui::layout::Rect;
pub type Callback = Box<dyn Fn(&mut Compositor)>;
// --> EventResult should have a callback that takes a context with methods like .popup(),
// .prompt() etc. That way we can abstract it from the renderer.
// Q: How does this interact with popups where we need to be able to specify the rendering of the
// popup?
// A: It could just take a textarea.
//
// If Compositor was specified in the callback that's then problematic because of
// Cursive-inspired
pub enum EventResult {
Ignored,
Consumed(Option<Callback>),
}
use helix_view::{Editor, View};
// shared with commands.rs
pub struct Context<'a> {
pub editor: &'a mut Editor,
pub executor: &'static smol::Executor<'static>,
}
pub trait Component {
/// Process input events, return true if handled.
fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult;
// , args: ()
/// Should redraw? Useful for saving redraw cycles if we know component didn't change.
fn should_update(&self) -> bool {
true
}
fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context);
fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
None
}
}
// struct Editor { };
// For v1:
// Child views are something each view needs to handle on it's own for now, positioning and sizing
// options, focus tracking. In practice this is simple: we only will need special solving for
// splits etc
// impl Editor {
// fn render(&mut self, surface: &mut Surface, args: ()) {
// // compute x, y, w, h rects for sub-views!
// // get surface area
// // get constraints for textarea, statusbar
// // -> cassowary-rs
// // first render textarea
// // then render statusbar
// }
// }
// usecases to consider:
// - a single view with subviews (textarea + statusbar)
// - a popup panel / dialog with it's own interactions
// - an autocomplete popup that doesn't change focus
//fn main() {
// let root = Editor::new();
// let compositor = Compositor::new();
// compositor.push(root);
// // pos: clip to bottom of screen
// compositor.push_at(pos, Prompt::new(
// ":",
// (),
// |input: &str| match input {}
// )); // TODO: this Prompt needs to somehow call compositor.pop() on close, but it can't refer to parent
// // Cursive solves this by allowing to return a special result on process_event
// // that's either Ignore | Consumed(Opt<C>) where C: fn (Compositor) -> ()
// // TODO: solve popup focus: we want to push autocomplete popups on top of the current layer
// // but retain the focus where it was. The popup will also need to update as we type into the
// // textarea. It should also capture certain input, such as tab presses etc
// //
// // 1) This could be faked by the top layer pushing down edits into the previous layer.
// // 2) Alternatively,
//}
pub struct Compositor {
layers: Vec<Box<dyn Component>>,
}
impl Compositor {
pub fn new() -> Self {
Self { layers: Vec::new() }
}
pub fn push(&mut self, layer: Box<dyn Component>) {
self.layers.push(layer);
}
pub fn pop(&mut self) {
self.layers.pop();
}
pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
// TODO: custom focus
if let Some(layer) = self.layers.last_mut() {
return match layer.handle_event(event, cx) {
EventResult::Consumed(Some(callback)) => {
callback(self);
true
}
EventResult::Consumed(None) => true,
EventResult::Ignored => false,
};
}
false
}
pub fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
for layer in &self.layers {
layer.render(area, surface, cx)
}
}
pub fn cursor_position(&self, area: Rect, cx: &mut Context) -> Position {
for layer in self.layers.iter().rev() {
if let Some(pos) = layer.cursor_position(area, cx) {
return pos;
}
}
panic!("No layer returned a position!");
}
}

@ -1,6 +1,6 @@
use crate::commands::{self, Command}; use crate::commands::{self, Command};
use crate::document::Mode;
use helix_core::hashmap; use helix_core::hashmap;
use helix_view::document::Mode;
use std::collections::HashMap; use std::collections::HashMap;
// Kakoune-inspired: // Kakoune-inspired:
@ -87,7 +87,7 @@ use std::collections::HashMap;
// gr = goto reference // gr = goto reference
// } // }
#[cfg(feature = "term")] // #[cfg(feature = "term")]
pub use crossterm::event::{KeyCode, KeyEvent as Key, KeyModifiers as Modifiers}; pub use crossterm::event::{KeyCode, KeyEvent as Key, KeyModifiers as Modifiers};
// TODO: could be trie based // TODO: could be trie based

@ -1,6 +1,11 @@
#![allow(unused)] #![allow(unused)]
mod application; mod application;
mod commands;
mod compositor;
mod keymap;
mod terminal;
mod ui;
use application::Application; use application::Application;

@ -0,0 +1,221 @@
use std::io;
use tui::{
backend::Backend,
buffer::Buffer,
layout::Rect,
widgets::{StatefulWidget, Widget},
};
#[derive(Debug, Clone, PartialEq)]
/// UNSTABLE
enum ResizeBehavior {
Fixed,
Auto,
}
#[derive(Debug, Clone, PartialEq)]
/// UNSTABLE
pub struct Viewport {
area: Rect,
resize_behavior: ResizeBehavior,
}
impl Viewport {
/// UNSTABLE
pub fn fixed(area: Rect) -> Viewport {
Viewport {
area,
resize_behavior: ResizeBehavior::Fixed,
}
}
}
#[derive(Debug, Clone, PartialEq)]
/// Options to pass to [`Terminal::with_options`]
pub struct TerminalOptions {
/// Viewport used to draw to the terminal
pub viewport: Viewport,
}
/// Interface to the terminal backed by Termion
#[derive(Debug)]
pub struct Terminal<B>
where
B: Backend,
{
backend: B,
/// Holds the results of the current and previous draw calls. The two are compared at the end
/// of each draw pass to output the necessary updates to the terminal
buffers: [Buffer; 2],
/// Index of the current buffer in the previous array
current: usize,
/// Whether the cursor is currently hidden
hidden_cursor: bool,
/// Viewport
viewport: Viewport,
}
impl<B> Drop for Terminal<B>
where
B: Backend,
{
fn drop(&mut self) {
// Attempt to restore the cursor state
if self.hidden_cursor {
if let Err(err) = self.show_cursor() {
eprintln!("Failed to show the cursor: {}", err);
}
}
}
}
impl<B> Terminal<B>
where
B: Backend,
{
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
/// default colors for the foreground and the background
pub fn new(backend: B) -> io::Result<Terminal<B>> {
let size = backend.size()?;
Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport {
area: size,
resize_behavior: ResizeBehavior::Auto,
},
},
)
}
/// UNSTABLE
pub fn with_options(backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
Ok(Terminal {
backend,
buffers: [
Buffer::empty(options.viewport.area),
Buffer::empty(options.viewport.area),
],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
})
}
// /// Get a Frame object which provides a consistent view into the terminal state for rendering.
// pub fn get_frame(&mut self) -> Frame<B> {
// Frame {
// terminal: self,
// cursor_position: None,
// }
// }
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffers[self.current]
}
pub fn backend(&self) -> &B {
&self.backend
}
pub fn backend_mut(&mut self) -> &mut B {
&mut self.backend
}
/// Obtains a difference between the previous and the current buffer and passes it to the
/// current backend for drawing.
pub fn flush(&mut self) -> io::Result<()> {
let previous_buffer = &self.buffers[1 - self.current];
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff(current_buffer);
self.backend.draw(updates.into_iter())
}
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
/// be saved so the size can remain consistent when rendering.
/// This leads to a full clear of the screen.
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].resize(area);
self.viewport.area = area;
self.clear()
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
if self.viewport.resize_behavior == ResizeBehavior::Auto {
let size = self.size()?;
if size != self.viewport.area {
self.resize(size)?;
}
};
Ok(())
}
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
/// and prepares for the next draw call.
pub fn draw(&mut self) -> io::Result<()> {
// // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// // and the terminal (if growing), which may OOB.
// self.autoresize()?;
// let mut frame = self.get_frame();
// f(&mut frame);
// // We can't change the cursor position right away because we have to flush the frame to
// // stdout first. But we also can't keep the frame around, since it holds a &mut to
// // Terminal. Thus, we're taking the important data out of the Frame and dropping it.
// let cursor_position = frame.cursor_position;
// Draw to stdout
self.flush()?;
// match cursor_position {
// None => self.hide_cursor()?,
// Some((x, y)) => {
// self.show_cursor()?;
// self.set_cursor(x, y)?;
// }
// }
// Swap buffers
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
// Flush
self.backend.flush()?;
Ok(())
}
pub fn hide_cursor(&mut self) -> io::Result<()> {
self.backend.hide_cursor()?;
self.hidden_cursor = true;
Ok(())
}
pub fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
}
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
self.backend.get_cursor()
}
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)
}
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
self.backend.clear()?;
// Reset the back buffer to make sure the next update will redraw everything.
self.buffers[1 - self.current].reset();
Ok(())
}
/// Queries the real size of the backend.
pub fn size(&self) -> io::Result<Rect> {
self.backend.size()
}
}

@ -0,0 +1,327 @@
use crate::commands;
use crate::compositor::{Component, Compositor, Context, EventResult};
use crate::keymap::{self, Keymaps};
use crate::ui::text_color;
use helix_core::{indent::TAB_WIDTH, syntax::HighlightEvent, Position, Range, State};
use helix_view::{document::Mode, Document, Editor, Theme, View};
use std::borrow::Cow;
use crossterm::{
cursor,
event::{read, Event, EventStream, KeyCode, KeyEvent},
};
use tui::{
backend::CrosstermBackend,
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Modifier, Style},
};
pub struct EditorView {
keymap: Keymaps,
}
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
impl EditorView {
pub fn new() -> Self {
Self {
keymap: keymap::default(),
}
}
pub fn render_view(
&self,
view: &mut View,
viewport: Rect,
surface: &mut Surface,
theme: &Theme,
) {
let area = Rect::new(OFFSET, 0, viewport.width - OFFSET, viewport.height - 2); // - 2 for statusline and prompt
self.render_buffer(view, area, surface, theme);
let area = Rect::new(0, viewport.height - 2, viewport.width, 1);
self.render_statusline(view, area, surface, theme);
}
// TODO: ideally not &mut View but highlights require it because of cursor cache
pub fn render_buffer(
&self,
view: &mut View,
viewport: Rect,
surface: &mut Surface,
theme: &Theme,
) {
// clear with background color
surface.set_style(viewport, theme.get("ui.background"));
// TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
let source_code = view.doc.text().to_string();
let last_line = view.last_line();
let range = {
// calculate viewport byte ranges
let start = view.doc.text().line_to_byte(view.first_line);
let end = view.doc.text().line_to_byte(last_line)
+ view.doc.text().line(last_line).len_bytes();
start..end
};
// TODO: range doesn't actually restrict source, just highlight range
// TODO: cache highlight results
// TODO: only recalculate when state.doc is actually modified
let highlights: Vec<_> = match view.doc.syntax.as_mut() {
Some(syntax) => {
syntax
.highlight_iter(source_code.as_bytes(), Some(range), None, |_| None)
.unwrap()
.collect() // TODO: we collect here to avoid double borrow, fix later
}
None => vec![Ok(HighlightEvent::Source {
start: range.start,
end: range.end,
})],
};
let mut spans = Vec::new();
let mut visual_x = 0;
let mut line = 0u16;
let visible_selections: Vec<Range> = view
.doc
.state
.selection()
.ranges()
.iter()
// TODO: limit selection to one in viewport
// .filter(|range| !range.is_empty()) // && range.overlaps(&Range::new(start, end + 1))
.copied()
.collect();
'outer: for event in highlights {
match event.unwrap() {
HighlightEvent::HighlightStart(span) => {
spans.push(span);
}
HighlightEvent::HighlightEnd => {
spans.pop();
}
HighlightEvent::Source { start, end } => {
// TODO: filter out spans out of viewport for now..
let start = view.doc.text().byte_to_char(start);
let end = view.doc.text().byte_to_char(end); // <-- index 744, len 743
let text = view.doc.text().slice(start..end);
use helix_core::graphemes::{grapheme_width, RopeGraphemes};
let style = match spans.first() {
Some(span) => theme.get(theme.scopes()[span.0].as_str()),
None => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender
};
// TODO: we could render the text to a surface, then cache that, that
// way if only the selection/cursor changes we can copy from cache
// and paint the new cursor.
let mut char_index = start;
// iterate over range char by char
for grapheme in RopeGraphemes::new(&text) {
// TODO: track current char_index
if grapheme == "\n" {
visual_x = 0;
line += 1;
// TODO: with proper iter this shouldn't be necessary
if line >= viewport.height {
break 'outer;
}
} else if grapheme == "\t" {
visual_x += (TAB_WIDTH as u16);
} else {
// Cow will prevent allocations if span contained in a single slice
// which should really be the majority case
let grapheme = Cow::from(grapheme);
let width = grapheme_width(&grapheme) as u16;
// TODO: this should really happen as an after pass
let style = if visible_selections
.iter()
.any(|range| range.contains(char_index))
{
// cedar
style.clone().bg(Color::Rgb(128, 47, 0))
} else {
style
};
let style = if visible_selections
.iter()
.any(|range| range.head == char_index)
{
style.clone().bg(Color::Rgb(255, 255, 255))
} else {
style
};
// ugh, improve with a traverse method
// or interleave highlight spans with selection and diagnostic spans
let style = if view.doc.diagnostics.iter().any(|diagnostic| {
diagnostic.range.0 <= char_index && diagnostic.range.1 > char_index
}) {
style.clone().add_modifier(Modifier::UNDERLINED)
} else {
style
};
// TODO: paint cursor heads except primary
surface.set_string(
viewport.x + visual_x,
viewport.y + line,
grapheme,
style,
);
visual_x += width;
}
char_index += 1;
}
}
}
}
let style: Style = theme.get("ui.linenr");
let warning: Style = theme.get("warning");
let last_line = view.last_line();
for (i, line) in (view.first_line..last_line).enumerate() {
if view.doc.diagnostics.iter().any(|d| d.line == line) {
surface.set_stringn(0, i as u16, "●", 1, warning);
}
surface.set_stringn(1, i as u16, format!("{:>5}", line + 1), 5, style);
}
}
pub fn render_statusline(
&self,
view: &View,
viewport: Rect,
surface: &mut Surface,
theme: &Theme,
) {
let text_color = text_color();
let mode = match view.doc.mode() {
Mode::Insert => "INS",
Mode::Normal => "NOR",
Mode::Goto => "GOTO",
};
// statusline
surface.set_style(
Rect::new(0, viewport.y, viewport.width, 1),
theme.get("ui.statusline"),
);
surface.set_string(1, viewport.y, mode, text_color);
if let Some(path) = view.doc.path() {
surface.set_string(6, viewport.y, path.to_string_lossy(), text_color);
}
surface.set_string(
viewport.width - 10,
viewport.y,
format!("{}", view.doc.diagnostics.len()),
text_color,
);
}
}
impl Component for EditorView {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
match event {
Event::Resize(width, height) => {
// TODO: simplistic ensure cursor in view for now
// TODO: loop over views
if let Some(view) = cx.editor.view_mut() {
view.size = (width, height);
view.ensure_cursor_in_view()
};
EventResult::Consumed(None)
}
Event::Key(event) => {
if let Some(view) = cx.editor.view_mut() {
let keys = vec![event];
// TODO: sequences (`gg`)
let mode = view.doc.mode();
// TODO: handle count other than 1
let mut cx = commands::Context {
view,
executor: cx.executor,
count: 1,
callback: None,
};
match mode {
Mode::Insert => {
if let Some(command) = self.keymap[&Mode::Insert].get(&keys) {
command(&mut cx);
} else if let KeyEvent {
code: KeyCode::Char(c),
..
} = event
{
commands::insert::insert_char(&mut cx, c);
}
}
mode => {
if let Some(command) = self.keymap[&mode].get(&keys) {
command(&mut cx);
// TODO: simplistic ensure cursor in view for now
}
}
}
// appease borrowck
let callback = cx.callback.take();
view.ensure_cursor_in_view();
EventResult::Consumed(callback)
} else {
EventResult::Ignored
}
}
Event::Mouse(_) => EventResult::Ignored,
}
}
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
// SAFETY: we cheat around the view_mut() borrow because it doesn't allow us to also borrow
// theme. Theme is immutable mutating view won't disrupt theme_ref.
let theme_ref = unsafe { &*(&cx.editor.theme as *const Theme) };
if let Some(view) = cx.editor.view_mut() {
self.render_view(view, area, surface, theme_ref);
}
// TODO: drop unwrap
}
fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
// match view.doc.mode() {
// Mode::Insert => write!(stdout, "\x1B[6 q"),
// mode => write!(stdout, "\x1B[2 q"),
// };
let view = ctx.editor.view().unwrap();
let cursor = view.doc.state.selection().cursor();
let mut pos = view
.screen_coords_at_pos(&view.doc.text().slice(..), cursor)
.expect("Cursor is out of bounds.");
pos.col += area.x as usize + OFFSET as usize;
pos.row += area.y as usize;
Some(pos)
}
}

@ -0,0 +1,14 @@
mod editor;
mod prompt;
pub use editor::EditorView;
pub use prompt::Prompt;
pub use tui::layout::Rect;
pub use tui::style::{Color, Modifier, Style};
// TODO: temp
#[inline(always)]
pub fn text_color() -> Style {
Style::default().fg(Color::Rgb(219, 191, 239)) // lilac
}

@ -1,5 +1,8 @@
use crate::Editor; use crate::compositor::{Component, Compositor, Context, EventResult};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use helix_core::Position;
use helix_view::Editor;
use helix_view::Theme;
use std::string::String; use std::string::String;
pub struct Prompt { pub struct Prompt {
@ -79,16 +82,91 @@ impl Prompt {
pub fn exit_selection(&mut self) { pub fn exit_selection(&mut self) {
self.completion_selection_index = None; self.completion_selection_index = None;
} }
}
use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Modifier, Style},
};
const BASE_WIDTH: u16 = 30;
use crate::ui::text_color;
impl Prompt {
pub fn render_prompt(&self, area: Rect, surface: &mut Surface, theme: &Theme) {
let text_color = text_color();
// completion
if !self.completion.is_empty() {
// TODO: find out better way of clearing individual lines of the screen
let mut row = 0;
let mut col = 0;
let max_col = area.width / BASE_WIDTH;
let col_height = ((self.completion.len() as u16 + max_col - 1) / max_col);
for i in (3..col_height + 3) {
surface.set_string(
0,
area.height - i as u16,
" ".repeat(area.width as usize),
text_color,
);
}
surface.set_style(
Rect::new(0, area.height - col_height - 2, area.width, col_height),
theme.get("ui.statusline"),
);
for (i, command) in self.completion.iter().enumerate() {
let color = if self.completion_selection_index.is_some()
&& i == self.completion_selection_index.unwrap()
{
Style::default().bg(Color::Rgb(104, 060, 232))
} else {
text_color
};
surface.set_stringn(
1 + col * BASE_WIDTH,
area.height - col_height - 2 + row,
&command,
BASE_WIDTH as usize - 1,
color,
);
row += 1;
if row > col_height - 1 {
row = 0;
col += 1;
}
if col > max_col {
break;
}
}
}
// render buffer text
surface.set_string(1, area.height - 1, &self.prompt, text_color);
surface.set_string(2, area.height - 1, &self.line, text_color);
}
}
impl Component for Prompt {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let event = match event {
Event::Key(event) => event,
_ => return EventResult::Ignored,
};
pub fn handle_input(&mut self, key_event: KeyEvent, editor: &mut Editor) { match event {
match key_event {
KeyEvent { KeyEvent {
code: KeyCode::Char(c), code: KeyCode::Char(c),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
} => self.insert_char(c), } => self.insert_char(c),
KeyEvent { KeyEvent {
code: KeyCode::Esc, .. code: KeyCode::Esc, ..
} => self.should_close = true, } => {
return EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
// remove the layer
compositor.pop();
})));
}
KeyEvent { KeyEvent {
code: KeyCode::Right, code: KeyCode::Right,
.. ..
@ -112,7 +190,7 @@ impl Prompt {
KeyEvent { KeyEvent {
code: KeyCode::Enter, code: KeyCode::Enter,
.. ..
} => (self.callback_fn)(editor, &self.line), } => (self.callback_fn)(cx.editor, &self.line),
KeyEvent { KeyEvent {
code: KeyCode::Tab, .. code: KeyCode::Tab, ..
} => self.change_completion_selection(), } => self.change_completion_selection(),
@ -121,6 +199,19 @@ impl Prompt {
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => self.exit_selection(), } => self.exit_selection(),
_ => (), _ => (),
};
EventResult::Consumed(None)
} }
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
self.render_prompt(area, surface, &cx.editor.theme)
}
fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
Some(Position::new(
area.height as usize - 1,
area.x as usize + 2 + self.cursor,
))
} }
} }

@ -1,8 +1,5 @@
pub mod commands;
pub mod document; pub mod document;
pub mod editor; pub mod editor;
pub mod keymap;
pub mod prompt;
pub mod theme; pub mod theme;
pub mod view; pub mod view;

Loading…
Cancel
Save