Skip to content

Commit

Permalink
File picker prompt rework and custom input component
Browse files Browse the repository at this point in the history
  • Loading branch information
mcobzarenco committed Jul 31, 2022
1 parent a3c4cd8 commit 65b3307
Show file tree
Hide file tree
Showing 12 changed files with 576 additions and 153 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
[#49](https://github.com/zee-editor/zee/pull/49)
- Add [Haskell](https://github.com/tree-sitter/tree-sitter-haskell) syntax
highlighting [#62](https://github.com/zee-editor/zee/pull/62)
- Add a custom input component, rather than relying on zi's input. This change
enables reusing zee's text editing functions and makes the editing prompt
input behave similar to text editing in a buffer.
[#76](https://github.com/zee-editor/zee/pull/76)
- Refactor file pickers to use a newly added custom input component. New text
editing bindings were introduced to match buffers. A number of edge cases and
bugs were fixed, e.g. when editing an empty path. The picker now shows file
size and the humanized last modified timestamp.
[#76](https://github.com/zee-editor/zee/pull/76)

### Fixed

Expand All @@ -30,6 +39,8 @@ format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
[#31](https://github.com/zee-editor/zee/pull/31)
- Fix tree sitter spans not being aligned with text after saving
[#65](https://github.com/zee-editor/zee/pull/65)
- Fix erroneous insert of `/` when path is `/` in the file picker.
[#76](https://github.com/zee-editor/zee/pull/76)

## 0.3.2 - 2022-04-23

Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions zee-edit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ impl Cursor {
}
}

pub fn is_selecting(&self) -> bool {
self.selection.is_some()
}

pub fn column_offset(&self, tab_width: usize, text: &Rope) -> usize {
let char_line_start = text.line_to_char(text.cursor_to_line(self));
graphemes::width(tab_width, &text.slice(char_line_start..self.range.start))
Expand Down
1 change: 1 addition & 0 deletions zee/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ size_format = "1.0.2"
smallstr = "0.3.0"
smallvec = "1.9.0"
thiserror = "1.0.31"
time-humanize = "0.1.3"
tree-sitter = "0.20.8"
zi = "0.3.2"
zi-term = "0.3.2"
Expand Down
297 changes: 297 additions & 0 deletions zee/src/components/input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
use ropey::Rope;

use zi::{
unicode_width::UnicodeWidthStr, AnyCharacter, Bindings, Callback, Canvas, Component,
ComponentLink, Key, Layout, Rect, ShouldRender, Style,
};

use crate::editor::ContextHandle;
use zee_edit::{graphemes::RopeGraphemes, movement, Cursor, Direction};

#[derive(Clone, PartialEq)]
pub struct InputProperties {
pub context: ContextHandle,
pub style: InputStyle,
pub content: Rope,
pub cursor: Cursor,
pub on_change: Option<Callback<InputChange>>,
pub focused: bool,
}

#[derive(Clone, Debug, PartialEq)]
pub struct InputStyle {
pub content: Style,
pub cursor: Style,
}

#[derive(Clone, Debug)]
pub struct InputChange {
pub content: Option<Rope>,
pub cursor: Cursor,
}

pub struct Input {
properties: InputProperties,
frame: Rect,
}

impl Component for Input {
type Message = Message;
type Properties = InputProperties;

fn create(properties: Self::Properties, frame: Rect, _link: ComponentLink<Self>) -> Self {
Self { properties, frame }
}

fn change(&mut self, properties: Self::Properties) -> ShouldRender {
let should_render = (self.properties != properties).into();
self.properties = properties;
should_render
}

fn resize(&mut self, frame: Rect) -> ShouldRender {
self.frame = frame;
ShouldRender::Yes
}

fn update(&mut self, message: Self::Message) -> ShouldRender {
let mut cursor = self.properties.cursor.clone();
let mut content_change = None;
let content = &self.properties.content;
match message {
// Movement
Message::Move(direction, count) => {
movement::move_horizontally(content, &mut cursor, direction, count);
}
Message::MoveWord(direction, count) => {
movement::move_word(content, &mut cursor, direction, count)
}
Message::StartOfLine => {
movement::move_to_start_of_line(content, &mut cursor);
}
Message::EndOfLine => {
movement::move_to_end_of_line(content, &mut cursor);
}

// Insertion
Message::InsertChar { character } => {
let mut new_content = content.clone();
cursor.insert_char(&mut new_content, character);
movement::move_horizontally(&new_content, &mut cursor, Direction::Forward, 1);
content_change = Some(new_content);
}

// Deletion
Message::DeleteBackward => {
let mut new_content = content.clone();
cursor.delete_backward(&mut new_content);
content_change = Some(new_content);
}
Message::DeleteForward => {
let mut new_content = content.clone();
cursor.delete_forward(&mut new_content);
content_change = Some(new_content);
}
Message::DeleteLine => {
let mut new_content = content.clone();
cursor.delete_line(&mut new_content);
content_change = Some(new_content);
}

// Selection
Message::BeginSelection => {
if cursor.is_selecting() {
cursor.clear_selection();
} else {
cursor.begin_selection();
}
}
Message::SelectAll => {
cursor.select_all(content);
}

Message::Yank => {
let clipboard_str = self.properties.context.clipboard.get_contents().unwrap();
if !clipboard_str.is_empty() {
let mut new_content = content.clone();
cursor.insert_chars(&mut new_content, clipboard_str.chars());
movement::move_horizontally(
&new_content,
&mut cursor,
Direction::Forward,
clipboard_str.chars().count(),
);
content_change = Some(new_content);
}
}
Message::CopySelection => {
let selection = cursor.selection();
self.properties
.context
.clipboard
.set_contents(content.slice(selection.start..selection.end).into())
.unwrap();
cursor.clear_selection();
}
Message::CutSelection => {
let mut new_content = content.clone();
let operation = cursor.delete_selection(&mut new_content);
self.properties
.context
.clipboard
.set_contents(operation.deleted.into())
.unwrap();
content_change = Some(new_content);
}
}

if let Some(on_change) = self.properties.on_change.as_ref() {
on_change.emit(InputChange {
cursor,
content: content_change,
});
}

ShouldRender::Yes
}

fn view(&self) -> Layout {
let Self {
properties:
InputProperties {
ref content,
ref cursor,
ref style,
..
},
..
} = *self;

let mut canvas = Canvas::new(self.frame.size);
canvas.clear(style.content);

let mut char_offset = 0;
let mut visual_offset = 0;
for grapheme in RopeGraphemes::new(&content.slice(..)) {
let len_chars = grapheme.len_chars();
// TODO: don't unwrap (need to be able to create a smallstring from a rope slice)
let grapheme = grapheme.as_str().unwrap();
let grapheme_width = UnicodeWidthStr::width(grapheme);

canvas.draw_str(
visual_offset,
0,
if cursor.selection().contains(&char_offset) {
style.cursor
} else {
style.content
},
if grapheme_width > 0 { grapheme } else { " " },
);
visual_offset += grapheme_width;
char_offset += len_chars;
}

if cursor.range().start == char_offset {
canvas.draw_str(visual_offset, 0, style.cursor, " ");
}

canvas.into()
}

fn bindings(&self, bindings: &mut Bindings<Self>) {
use Key::*;

bindings.set_focus(self.properties.focused);
if !bindings.is_empty() {
return;
}

// Movement
bindings
.command("move-backward", || Message::Move(Direction::Backward, 1))
.with([Ctrl('b')])
.with([Left]);
bindings
.command("move-forward", || Message::Move(Direction::Forward, 1))
.with([Ctrl('f')])
.with([Right]);
bindings
.command("move-backward-word", || {
Message::MoveWord(Direction::Backward, 1)
})
.with([Alt('b')]);
bindings
.command("move-forward-word", || {
Message::MoveWord(Direction::Forward, 1)
})
.with([Alt('f')]);
bindings
.command("start-of-line", || Message::StartOfLine)
.with([Ctrl('a')])
.with([Home]);
bindings
.command("end-of-line", || Message::EndOfLine)
.with([Ctrl('e')])
.with([End]);

// Selection
//
// Begin selection
bindings
.command("begin-selection", || Message::BeginSelection)
.with([Null])
.with([Ctrl(' ')]);

// Select all
bindings.add("select-all", [Ctrl('x'), Char('h')], || Message::SelectAll);
// Copy selection to clipboard
bindings.add("copy-selection", [Alt('w')], || Message::CopySelection);
// Cut selection to clipboard
bindings.add("cut-selection", [Ctrl('w')], || Message::CutSelection);
// Paste from clipboard
bindings.add("paste-clipboard", [Ctrl('y')], || Message::Yank);

// Editing
bindings
.command("delete-forward", || Message::DeleteForward)
.with([Ctrl('d')])
.with([Delete]);
bindings.add("delete-backward", [Backspace], || Message::DeleteBackward);
bindings.add("delete-line", [Ctrl('k')], || Message::DeleteLine);

bindings.add(
"insert-character",
AnyCharacter,
|keys: &[Key]| match keys {
&[Char(character)]
if character != '\n' && character != '\r' && character != '\t' =>
{
Some(Message::InsertChar { character })
}
_ => None,
},
);
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Message {
// Movement
Move(Direction, usize),
MoveWord(Direction, usize),
StartOfLine,
EndOfLine,

// Editing
BeginSelection,
SelectAll,
Yank,
CopySelection,
CutSelection,

DeleteForward,
DeleteBackward,
DeleteLine,
InsertChar { character: char },
}
1 change: 1 addition & 0 deletions zee/src/components/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod buffer;
pub mod edit_tree_viewer;
pub mod input;
pub mod prompt;
pub mod splash;
pub mod theme;
4 changes: 2 additions & 2 deletions zee/src/components/prompt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ impl Component for Prompt {

BufferPicker::with(BufferPickerProperties {
message: message.clone(),
context: self.properties.context.clone(),
context: self.properties.context,
theme: self.properties.theme.clone(),
entries: entries.clone(),
on_select: on_select.clone(),
Expand All @@ -142,7 +142,7 @@ impl Component for Prompt {
on_change_height,
on_open,
} => FilePicker::with(FilePickerProperties {
context: self.properties.context.clone(),
context: self.properties.context,
theme: self.properties.theme.clone(),
source: *source,
on_open: on_open.clone(),
Expand Down
Loading

0 comments on commit 65b3307

Please sign in to comment.