mirror of https://github.com/helix-editor/helix
commit
b12a6dc830
@ -0,0 +1,5 @@
|
|||||||
|
[[language]]
|
||||||
|
name = "rust"
|
||||||
|
scope = "source.rust"
|
||||||
|
injection-regex = "rust"
|
||||||
|
file-types = ["rs"]
|
@ -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!");
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue