Merge pull request #1205 from rtfeldman/selection

shortcut to select surrounding expression, backspace to replace expression with blank
This commit is contained in:
Richard Feldman 2021-04-17 22:10:06 -04:00 committed by GitHub
commit e62e56ca86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 606 additions and 178 deletions

View file

@ -8,7 +8,7 @@ sudo apt install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev
- Run the following from the roc folder: - Run the following from the roc folder:
``` ```
cargo run edit examples/hello-world/Hello.roc cargo run edit
``` ```
## Troubleshooting ## Troubleshooting

View file

@ -63,6 +63,28 @@ e.g. you have a test `calculate_sum_test` that only uses the function `add`, whe
* [Sourcetrail](https://www.sourcetrail.com/) nice tree-like source explorer. * [Sourcetrail](https://www.sourcetrail.com/) nice tree-like source explorer.
* [Unisonweb](https://www.unisonweb.org), definition based [editor](https://twitter.com/shojberg/status/1364666092598288385) as opposed to file based. * [Unisonweb](https://www.unisonweb.org), definition based [editor](https://twitter.com/shojberg/status/1364666092598288385) as opposed to file based.
### Voice Interaction Related
* We should label as many things as possible and expose jumps to those labels as shortkeys.
* Update without user interaction. e.g. autosave.
* Could be efficient way to communicate with smart assistant.
* You don't have to remember complex keyboard shortcuts if you can describe actions to execute them. Examples:
* Add latest datetime package to dependencies.
* Generate unit test for this function.
* Show edit history for this function.
* Adjusting settings: switch to light theme, increase font size...
* Use (context specific) voice command state machine to assist Machine Learning voice recognition model.
* Nice special use case: using voice to code while on treadmill desk.
#### Inspiration
* Voice control and eye tracking with [Talon](https://github.com/Gauteab/talon-tree-sitter-service)
* [Seminar about programming by voice](https://www.youtube.com/watch?v=G8B71MbA9u4)
* [Talon voice commands in elm](https://github.com/Gauteab/talon-tree-sitter-service)
* Mozilla DeepSpeech model runs fast, works pretty well for actions but would need additional training for code input.
Possible to reuse [Mozilla common voice](https://github.com/common-voice/common-voice) for creating more "spoken code" data.
### Productivity features ### Productivity features
* When refactoring; * When refactoring;
@ -81,7 +103,7 @@ e.g. you have a test `calculate_sum_test` that only uses the function `add`, whe
* File history timeline view. Show timeline with commits that changed this file, the number of lines added and deleted as well as which user made the changes. Arrow navigation should allow you to quickly view different versions of the file. * File history timeline view. Show timeline with commits that changed this file, the number of lines added and deleted as well as which user made the changes. Arrow navigation should allow you to quickly view different versions of the file.
* Suggested quick fixes should be directly visible and clickable. Not like in vs code where you put the caret on an error until a lightbulb appears in the margin which you have to click for the fixes to apppear, after which you click to apply the fix you want :( . * Suggested quick fixes should be directly visible and clickable. Not like in vs code where you put the caret on an error until a lightbulb appears in the margin which you have to click for the fixes to apppear, after which you click to apply the fix you want :( .
* Regex-like find and substitution based on plain english description and example (replacement). i.e. replace all `[` between double quotes with `{`. [Inspiration](https://alexmoltzau.medium.com/english-to-regex-thanks-to-gpt-3-13f03b68236e). * Regex-like find and substitution based on plain english description and example (replacement). i.e. replace all `[` between double quotes with `{`. [Inspiration](https://alexmoltzau.medium.com/english-to-regex-thanks-to-gpt-3-13f03b68236e).
* Show productivity tips based on behavior. i.e. if the user is scrolling through the error bar and clicking on the next error several times, show a tip with "go to next error" shortcut.
#### Autocomplete #### Autocomplete
- Use more space for autocomplete options: - Use more space for autocomplete options:
@ -117,16 +139,6 @@ e.g. you have a test `calculate_sum_test` that only uses the function `add`, whe
* A simpler start for this idea without user data gathering: how the user a code snippet that is most similar to what they are currently writing. Snippets can be aggregated from examples, tests, docstrings at zero cost to the package/platform authors. * A simpler start for this idea without user data gathering: how the user a code snippet that is most similar to what they are currently writing. Snippets can be aggregated from examples, tests, docstrings at zero cost to the package/platform authors.
* See [codata](https://www.codota.com/code/java/classes/okhttp3.OkHttpClient) for inspiration on a snippet/example finder. * See [codata](https://www.codota.com/code/java/classes/okhttp3.OkHttpClient) for inspiration on a snippet/example finder.
* Fuzzy natural language based setting adjustment in search bar or with voice input: increase font size, enable autosave, switch to light theme... * Fuzzy natural language based setting adjustment in search bar or with voice input: increase font size, enable autosave, switch to light theme...
* Voice input:
* Good for accessibility.
* https://www.youtube.com/watch?v=Ffa3cXM7bjc is interesting for inspiration.
* Could be efficient way to communicate with smart assistant.
* Describe actions to execute them, examples:
* Add latest datetime package to dependencies.
* Generate unit test for this function.
* Show edit history for this function.
* Mozilla DeepSpeech model runs fast, works pretty well for actions but would need additional training for code input.
Possible to reuse [Mozilla common voice](https://github.com/common-voice/common-voice) for creating more "spoken code" data.
* Detect deviation of best practices, example case: alert developer when they are defining a color inline (rgb(30,30,30)) while all colors have been previously imported from a single file. See also [Codota](https://www.codota.com). * Detect deviation of best practices, example case: alert developer when they are defining a color inline (rgb(30,30,30)) while all colors have been previously imported from a single file. See also [Codota](https://www.codota.com).

View file

@ -1,4 +1,5 @@
use crate::ui::text::lines::Lines; use crate::ui::text::lines::Lines;
use crate::ui::text::selection::Selection;
use crate::ui::ui_error::UIResult; use crate::ui::ui_error::UIResult;
use crate::ui::util::slice_get; use crate::ui::util::slice_get;
use crate::ui::util::slice_get_mut; use crate::ui::util::slice_get_mut;
@ -47,6 +48,18 @@ impl CodeLines {
Ok(()) Ok(())
} }
pub fn del_selection(&mut self, selection: Selection) -> UIResult<()> {
if selection.is_on_same_line() {
let line_ref = slice_get_mut(selection.start_pos.line, &mut self.lines)?;
line_ref.drain(selection.start_pos.column..selection.end_pos.column);
} else {
// TODO support multiline selections
}
Ok(())
}
} }
impl Lines for CodeLines { impl Lines for CodeLines {

View file

@ -1,4 +1,5 @@
use crate::editor::slow_pool::MarkNodeId; use crate::editor::slow_pool::MarkNodeId;
use crate::ui::ui_error::UIResult;
use colored::*; use colored::*;
use snafu::{Backtrace, ErrorCompat, NoneError, ResultExt, Snafu}; use snafu::{Backtrace, ErrorCompat, NoneError, ResultExt, Snafu};
@ -71,6 +72,11 @@ pub enum EdError {
backtrace: Backtrace, backtrace: Backtrace,
}, },
#[snafu(display(
"MissingSelection: ed_model.selected_expr2_id was Some(NodeId<Expr2>) but ed_model.caret_w_sel_vec did not contain any Some(Selection)."
))]
MissingSelection { backtrace: Backtrace },
#[snafu(display("NestedNodeMissingChild: expected to find child with id {} in Nested MarkupNode, but it was missing. Id's of the children are {:?}.", node_id, children_ids))] #[snafu(display("NestedNodeMissingChild: expected to find child with id {} in Nested MarkupNode, but it was missing. Id's of the children are {:?}.", node_id, children_ids))]
NestedNodeMissingChild { NestedNodeMissingChild {
node_id: MarkNodeId, node_id: MarkNodeId,
@ -96,6 +102,15 @@ pub enum EdError {
#[snafu(display("NodeWithoutAttributes: expected to have a node with attributes. This is a Nested MarkupNode, only Text and Blank nodes have attributes."))] #[snafu(display("NodeWithoutAttributes: expected to have a node with attributes. This is a Nested MarkupNode, only Text and Blank nodes have attributes."))]
NodeWithoutAttributes { backtrace: Backtrace }, NodeWithoutAttributes { backtrace: Backtrace },
#[snafu(display(
"NodeIdNotInGridNodeMap: MarkNodeId {} was not found in ed_model.grid_node_map.",
node_id
))]
NodeIdNotInGridNodeMap {
node_id: MarkNodeId,
backtrace: Backtrace,
},
#[snafu(display( #[snafu(display(
"OutOfBounds: index {} was out of bounds for {} with length {}.", "OutOfBounds: index {} was out of bounds for {} with length {}.",
index, index,
@ -195,3 +210,10 @@ impl From<UIError> for EdError {
dummy_res.context(UIErrorBacktrace { msg }).unwrap_err() dummy_res.context(UIErrorBacktrace { msg }).unwrap_err()
} }
} }
pub fn from_ui_res<T>(ui_res: UIResult<T>) -> EdResult<T> {
match ui_res {
Ok(t) => Ok(t),
Err(ui_err) => Err(EdError::from(ui_err)),
}
}

View file

@ -1,9 +1,17 @@
use crate::editor::ed_error::EdResult; use crate::editor::ed_error::EdResult;
use crate::editor::ed_error::NestedNodeWithoutChildren;
use crate::editor::ed_error::NodeIdNotInGridNodeMap;
use crate::editor::mvc::ed_model::EdModel;
use crate::editor::slow_pool::MarkNodeId; use crate::editor::slow_pool::MarkNodeId;
use crate::editor::util::first_last_index_of;
use crate::editor::util::index_of; use crate::editor::util::index_of;
use crate::lang::ast::Expr2;
use crate::lang::pool::NodeId;
use crate::ui::text::selection::Selection;
use crate::ui::text::text_pos::TextPos; use crate::ui::text::text_pos::TextPos;
use crate::ui::ui_error::UIResult; use crate::ui::ui_error::UIResult;
use crate::ui::util::{slice_get, slice_get_mut}; use crate::ui::util::{slice_get, slice_get_mut};
use snafu::OptionExt;
use std::fmt; use std::fmt;
#[derive(Debug)] #[derive(Debug)]
@ -50,6 +58,18 @@ impl GridNodeMap {
Ok(()) Ok(())
} }
pub fn del_selection(&mut self, selection: Selection) -> UIResult<()> {
if selection.is_on_same_line() {
let line_ref = slice_get_mut(selection.start_pos.line, &mut self.lines)?;
line_ref.drain(selection.start_pos.column..selection.end_pos.column);
} else {
// TODO support multiline
}
Ok(())
}
/*pub fn new_line(&mut self) { /*pub fn new_line(&mut self) {
self.lines.push(vec![]) self.lines.push(vec![])
}*/ }*/
@ -72,6 +92,161 @@ impl GridNodeMap {
Ok(caret_pos.column - first_node_index) Ok(caret_pos.column - first_node_index)
} }
pub fn node_exists_at_pos(&self, pos: TextPos) -> bool {
if pos.line < self.lines.len() {
// safe unwrap because we checked the length
let line = self.lines.get(pos.line).unwrap();
pos.column < line.len()
} else {
false
}
}
// get position of first occurence of node_id if get_first_pos, else get the last occurence
pub fn get_node_position(&self, node_id: MarkNodeId, get_first_pos: bool) -> EdResult<TextPos> {
let mut last_pos_opt = None;
for (line_index, line) in self.lines.iter().enumerate() {
for (col_index, iter_node_id) in line.iter().enumerate() {
if node_id == *iter_node_id && get_first_pos {
return Ok(TextPos {
line: line_index,
column: col_index,
});
} else if node_id == *iter_node_id {
last_pos_opt = Some(TextPos {
line: line_index,
column: col_index,
})
} else if let Some(last_pos) = last_pos_opt {
return Ok(last_pos);
}
}
}
if let Some(last_pos) = last_pos_opt {
Ok(last_pos)
} else {
NodeIdNotInGridNodeMap { node_id }.fail()
}
}
// retruns start and end pos of Expr2, relevant AST node and MarkNodeId of the corresponding MarkupNode
pub fn get_expr_start_end_pos(
&self,
caret_pos: TextPos,
ed_model: &EdModel,
) -> EdResult<(TextPos, TextPos, NodeId<Expr2>, MarkNodeId)> {
let line = slice_get(caret_pos.line, &self.lines)?;
let node_id = slice_get(caret_pos.column, line)?;
let node = ed_model.markup_node_pool.get(*node_id);
if node.is_nested() {
let (start_pos, end_pos) = self.get_nested_start_end_pos(*node_id, ed_model)?;
Ok((start_pos, end_pos, node.get_ast_node_id(), *node_id))
} else {
let (first_node_index, last_node_index) = first_last_index_of(*node_id, line)?;
let curr_node_id = slice_get(first_node_index, line)?;
let curr_ast_node_id = ed_model
.markup_node_pool
.get(*curr_node_id)
.get_ast_node_id();
let mut expr_start_index = first_node_index;
let mut expr_end_index = last_node_index;
// we may encounter ast id's of children of the current node
let mut pos_extra_subtract = 0;
for i in (0..first_node_index).rev() {
let prev_pos_node_id = slice_get(i, line)?;
let prev_ast_node_id = ed_model
.markup_node_pool
.get(*prev_pos_node_id)
.get_ast_node_id();
if prev_ast_node_id == curr_ast_node_id {
if pos_extra_subtract > 0 {
expr_start_index -= pos_extra_subtract + 1;
pos_extra_subtract = 0;
} else {
expr_start_index -= 1;
}
} else {
pos_extra_subtract += 1;
}
}
// we may encounter ast id's of children of the current node
let mut pos_extra_add = 0;
for i in last_node_index..line.len() {
let next_pos_node_id = slice_get(i, line)?;
let next_ast_node_id = ed_model
.markup_node_pool
.get(*next_pos_node_id)
.get_ast_node_id();
if next_ast_node_id == curr_ast_node_id {
if pos_extra_add > 0 {
expr_end_index += pos_extra_add + 1;
pos_extra_add = 0;
} else {
expr_end_index += 1;
}
} else {
pos_extra_add += 1;
}
}
Ok((
TextPos {
line: caret_pos.line,
column: expr_start_index,
},
TextPos {
line: caret_pos.line,
column: expr_end_index,
},
curr_ast_node_id,
*curr_node_id,
))
}
}
pub fn get_nested_start_end_pos(
&self,
nested_node_id: MarkNodeId,
ed_model: &EdModel,
) -> EdResult<(TextPos, TextPos)> {
let parent_mark_node = ed_model.markup_node_pool.get(nested_node_id);
let all_child_ids = parent_mark_node.get_children_ids();
let first_child_id = all_child_ids
.first()
.with_context(|| NestedNodeWithoutChildren {
node_id: nested_node_id,
})?;
let last_child_id = all_child_ids
.last()
.with_context(|| NestedNodeWithoutChildren {
node_id: nested_node_id,
})?;
let expr_start_pos = ed_model
.grid_node_map
.get_node_position(*first_child_id, true)?;
let expr_end_pos = ed_model
.grid_node_map
.get_node_position(*last_child_id, false)?
.increment_col();
Ok((expr_start_pos, expr_end_pos))
}
} }
impl fmt::Display for GridNodeMap { impl fmt::Display for GridNodeMap {

View file

@ -118,7 +118,7 @@ fn run_event_loop(file_path_opt: Option<&Path>) -> Result<(), Box<dyn Error>> {
format: render_format, format: render_format,
width: size.width, width: size.width,
height: size.height, height: size.height,
present_mode: wgpu::PresentMode::Immediate, present_mode: wgpu::PresentMode::Mailbox,
}; };
let mut swap_chain = gpu_device.create_swap_chain(&surface, &swap_chain_descr); let mut swap_chain = gpu_device.create_swap_chain(&surface, &swap_chain_descr);
@ -228,7 +228,7 @@ fn run_event_loop(file_path_opt: Option<&Path>) -> Result<(), Box<dyn Error>> {
format: render_format, format: render_format,
width: size.width, width: size.width,
height: size.height, height: size.height,
present_mode: wgpu::PresentMode::Immediate, present_mode: wgpu::PresentMode::Mailbox,
}, },
); );

View file

@ -1,7 +1,6 @@
use super::app_model::AppModel; use super::app_model::AppModel;
use super::ed_update; use super::ed_update;
use crate::editor::ed_error::EdResult; use crate::editor::ed_error::EdResult;
use crate::ui::text::lines::SelectableLines;
use crate::window::keyboard_input::from_winit; use crate::window::keyboard_input::from_winit;
use winit::event::{ModifiersState, VirtualKeyCode}; use winit::event::{ModifiersState, VirtualKeyCode};
@ -44,7 +43,7 @@ pub fn pass_keydown_to_focused(
if let Some(ref mut ed_model) = app_model.ed_model_opt { if let Some(ref mut ed_model) = app_model.ed_model_opt {
if ed_model.has_focus { if ed_model.has_focus {
ed_model.handle_key_down(&modifiers, virtual_keycode)?; ed_model.ed_handle_key_down(&modifiers, virtual_keycode)?;
} }
} }

View file

@ -5,7 +5,7 @@ use crate::editor::syntax_highlight::HighlightStyle;
use crate::editor::{ use crate::editor::{
ed_error::EdError::ParseError, ed_error::EdError::ParseError,
ed_error::EdResult, ed_error::EdResult,
markup::attribute::{Attributes, Caret}, markup::attribute::Attributes,
markup::nodes::{expr2_to_markup, set_parent_for_all, MarkupNode}, markup::nodes::{expr2_to_markup, set_parent_for_all, MarkupNode},
}; };
use crate::graphics::primitives::rect::Rect; use crate::graphics::primitives::rect::Rect;
@ -37,6 +37,7 @@ pub struct EdModel<'a> {
pub has_focus: bool, pub has_focus: bool,
// Option<MarkNodeId>: MarkupNode that corresponds to caret position, Option because this MarkNodeId is only calculated when it needs to be used. // Option<MarkNodeId>: MarkupNode that corresponds to caret position, Option because this MarkNodeId is only calculated when it needs to be used.
pub caret_w_select_vec: NonEmpty<(CaretWSelect, Option<MarkNodeId>)>, pub caret_w_select_vec: NonEmpty<(CaretWSelect, Option<MarkNodeId>)>,
pub selected_expr2_tup: Option<(NodeId<Expr2>, MarkNodeId)>,
pub show_debug_view: bool, pub show_debug_view: bool,
// EdModel is dirty if it has changed since the previous render. // EdModel is dirty if it has changed since the previous render.
pub dirty: bool, pub dirty: bool,
@ -56,9 +57,7 @@ pub fn init_model<'a>(
let markup_root_id = if code_str.is_empty() { let markup_root_id = if code_str.is_empty() {
let blank_root = MarkupNode::Blank { let blank_root = MarkupNode::Blank {
ast_node_id: ast_root_id, ast_node_id: ast_root_id,
attributes: Attributes { attributes: Attributes::new(),
all: vec![Caret::new_attr(0)],
},
syn_high_style: HighlightStyle::Blank, syn_high_style: HighlightStyle::Blank,
parent_id_opt: None, parent_id_opt: None,
}; };
@ -92,6 +91,7 @@ pub fn init_model<'a>(
glyph_dim_rect_opt: None, glyph_dim_rect_opt: None,
has_focus: true, has_focus: true,
caret_w_select_vec: NonEmpty::new((CaretWSelect::default(), None)), caret_w_select_vec: NonEmpty::new((CaretWSelect::default(), None)),
selected_expr2_tup: None,
show_debug_view: false, show_debug_view: false,
dirty: true, dirty: true,
}) })

View file

@ -1,6 +1,10 @@
use crate::editor::code_lines::CodeLines; use crate::editor::code_lines::CodeLines;
use crate::editor::ed_error::from_ui_res;
use crate::editor::ed_error::print_ui_err;
use crate::editor::ed_error::EdResult; use crate::editor::ed_error::EdResult;
use crate::editor::ed_error::MissingSelection;
use crate::editor::grid_node_map::GridNodeMap; use crate::editor::grid_node_map::GridNodeMap;
use crate::editor::markup::attribute::Attributes;
use crate::editor::markup::nodes; use crate::editor::markup::nodes;
use crate::editor::markup::nodes::MarkupNode; use crate::editor::markup::nodes::MarkupNode;
use crate::editor::mvc::app_update::InputOutcome; use crate::editor::mvc::app_update::InputOutcome;
@ -15,6 +19,7 @@ use crate::editor::mvc::string_update::update_small_string;
use crate::editor::mvc::string_update::update_string; use crate::editor::mvc::string_update::update_string;
use crate::editor::slow_pool::MarkNodeId; use crate::editor::slow_pool::MarkNodeId;
use crate::editor::slow_pool::SlowPool; use crate::editor::slow_pool::SlowPool;
use crate::editor::syntax_highlight::HighlightStyle;
use crate::lang::ast::Expr2; use crate::lang::ast::Expr2;
use crate::lang::pool::NodeId; use crate::lang::pool::NodeId;
use crate::ui::text::caret_w_select::CaretWSelect; use crate::ui::text::caret_w_select::CaretWSelect;
@ -26,6 +31,7 @@ use crate::ui::text::text_pos::TextPos;
use crate::ui::text::{lines, lines::Lines, lines::SelectableLines}; use crate::ui::text::{lines, lines::Lines, lines::SelectableLines};
use crate::ui::ui_error::UIResult; use crate::ui::ui_error::UIResult;
use crate::window::keyboard_input::Modifiers; use crate::window::keyboard_input::Modifiers;
use snafu::OptionExt;
use winit::event::VirtualKeyCode; use winit::event::VirtualKeyCode;
use VirtualKeyCode::*; use VirtualKeyCode::*;
@ -41,6 +47,7 @@ impl<'a> EdModel<'a> {
caret_tup.0 = move_fun(&self.code_lines, caret_tup.0, modifiers)?; caret_tup.0 = move_fun(&self.code_lines, caret_tup.0, modifiers)?;
caret_tup.1 = None; caret_tup.1 = None;
} }
self.selected_expr2_tup = None;
Ok(()) Ok(())
} }
@ -136,6 +143,121 @@ impl<'a> EdModel<'a> {
self.grid_node_map.del_at_line(line_nr, index)?; self.grid_node_map.del_at_line(line_nr, index)?;
self.code_lines.del_at_line(line_nr, index) self.code_lines.del_at_line(line_nr, index)
} }
// select all MarkupNodes that refer to specific ast node and its children.
pub fn select_expr(&mut self) -> EdResult<()> {
// include parent in selection if an `Expr2` was already selected
if let Some((_sel_expr2_id, mark_node_id)) = self.selected_expr2_tup {
let expr2_level_mark_node = self.markup_node_pool.get(mark_node_id);
if let Some(parent_id) = expr2_level_mark_node.get_parent_id_opt() {
let parent_mark_node = self.markup_node_pool.get(parent_id);
let ast_node_id = parent_mark_node.get_ast_node_id();
let (expr_start_pos, expr_end_pos) = self
.grid_node_map
.get_nested_start_end_pos(parent_id, self)?;
self.set_raw_sel(RawSelection {
start_pos: expr_start_pos,
end_pos: expr_end_pos,
})?;
self.set_caret(expr_start_pos);
self.selected_expr2_tup = Some((ast_node_id, parent_id));
self.dirty = true;
}
} else {
// select `Expr2` in which caret is currently positioned
let caret_pos = self.get_caret();
if self.grid_node_map.node_exists_at_pos(caret_pos) {
let (expr_start_pos, expr_end_pos, ast_node_id, mark_node_id) = self
.grid_node_map
.get_expr_start_end_pos(self.get_caret(), &self)?;
self.set_raw_sel(RawSelection {
start_pos: expr_start_pos,
end_pos: expr_end_pos,
})?;
self.set_caret(expr_start_pos);
self.selected_expr2_tup = Some((ast_node_id, mark_node_id));
self.dirty = true;
}
}
Ok(())
}
pub fn ed_handle_key_down(
&mut self,
modifiers: &Modifiers,
virtual_keycode: VirtualKeyCode,
) -> EdResult<()> {
match virtual_keycode {
Left => from_ui_res(self.move_caret_left(modifiers)),
Up => {
if modifiers.ctrl && modifiers.shift {
self.select_expr()
} else {
from_ui_res(self.move_caret_up(modifiers))
}
}
Right => from_ui_res(self.move_caret_right(modifiers)),
Down => from_ui_res(self.move_caret_down(modifiers)),
A => {
if modifiers.ctrl {
from_ui_res(self.select_all())
} else {
Ok(())
}
}
Home => from_ui_res(self.move_caret_home(modifiers)),
End => from_ui_res(self.move_caret_end(modifiers)),
F11 => {
self.show_debug_view = !self.show_debug_view;
self.dirty = true;
Ok(())
}
_ => Ok(()),
}
}
fn replace_slected_expr_with_blank(&mut self) -> EdResult<()> {
if let Some((sel_expr2_id, mark_node_id)) = self.selected_expr2_tup {
let expr2_level_mark_node = self.markup_node_pool.get(mark_node_id);
let blank_replacement = MarkupNode::Blank {
ast_node_id: sel_expr2_id,
attributes: Attributes::new(),
syn_high_style: HighlightStyle::Blank,
parent_id_opt: expr2_level_mark_node.get_parent_id_opt(),
};
self.markup_node_pool
.replace_node(mark_node_id, blank_replacement);
let active_selection = self.get_selection().context(MissingSelection {})?;
self.code_lines.del_selection(active_selection)?;
self.grid_node_map.del_selection(active_selection)?;
let caret_pos = self.get_caret();
self.insert_between_line(
caret_pos.line,
caret_pos.column,
nodes::BLANK_PLACEHOLDER,
mark_node_id,
)?;
self.module.env.pool.set(sel_expr2_id, Expr2::Blank)
}
self.set_sel_none();
Ok(())
}
} }
impl<'a> SelectableLines for EdModel<'a> { impl<'a> SelectableLines for EdModel<'a> {
@ -230,6 +352,7 @@ impl<'a> SelectableLines for EdModel<'a> {
fn set_sel_none(&mut self) { fn set_sel_none(&mut self) {
self.caret_w_select_vec.first_mut().0.selection_opt = None; self.caret_w_select_vec.first_mut().0.selection_opt = None;
self.selected_expr2_tup = None;
} }
fn set_caret_w_sel(&mut self, caret_w_sel: CaretWSelect) { fn set_caret_w_sel(&mut self, caret_w_sel: CaretWSelect) {
@ -264,31 +387,10 @@ impl<'a> SelectableLines for EdModel<'a> {
fn handle_key_down( fn handle_key_down(
&mut self, &mut self,
modifiers: &Modifiers, _modifiers: &Modifiers,
virtual_keycode: VirtualKeyCode, _virtual_keycode: VirtualKeyCode,
) -> UIResult<()> { ) -> UIResult<()> {
match virtual_keycode { unreachable!("Use EdModel::ed_handle_key_down instead.")
Left => self.move_caret_left(modifiers),
Up => self.move_caret_up(modifiers),
Right => self.move_caret_right(modifiers),
Down => self.move_caret_down(modifiers),
A => {
if modifiers.ctrl {
self.select_all()
} else {
Ok(())
}
}
Home => self.move_caret_home(modifiers),
End => self.move_caret_end(modifiers),
F11 => {
self.show_debug_view = !self.show_debug_view;
self.dirty = true;
Ok(())
}
_ => Ok(()),
}
} }
} }
@ -319,9 +421,6 @@ pub fn get_node_context<'a>(ed_model: &'a EdModel) -> EdResult<NodeContext<'a>>
} }
pub fn handle_new_char(received_char: &char, ed_model: &mut EdModel) -> EdResult<InputOutcome> { pub fn handle_new_char(received_char: &char, ed_model: &mut EdModel) -> EdResult<InputOutcome> {
// TODO set all selections to none
// TODO nested records
let input_outcome = match received_char { let input_outcome = match received_char {
'\u{1}' // Ctrl + A '\u{1}' // Ctrl + A
| '\u{3}' // Ctrl + C | '\u{3}' // Ctrl + C
@ -334,121 +433,70 @@ pub fn handle_new_char(received_char: &char, ed_model: &mut EdModel) -> EdResult
// chars that can be ignored // chars that can be ignored
InputOutcome::Ignored InputOutcome::Ignored
} }
'\u{8}' | '\u{7f}' => {
// On Linux, '\u{8}' is backspace,
// on macOS '\u{7f}'.
ed_model.replace_slected_expr_with_blank()?;
InputOutcome::Accepted
}
ch => { ch => {
let curr_mark_node_id_res = ed_model.get_curr_mark_node_id(); let curr_mark_node_id_res = ed_model.get_curr_mark_node_id();
if let Ok(curr_mark_node_id) = curr_mark_node_id_res { let outcome =
let curr_mark_node = ed_model.markup_node_pool.get(curr_mark_node_id); match curr_mark_node_id_res {
let prev_mark_node_id_opt = ed_model.get_prev_mark_node_id()?; Ok(curr_mark_node_id) => {
let curr_mark_node = ed_model.markup_node_pool.get(curr_mark_node_id);
let prev_mark_node_id_opt = ed_model.get_prev_mark_node_id()?;
let ast_node_id = curr_mark_node.get_ast_node_id(); let ast_node_id = curr_mark_node.get_ast_node_id();
let ast_node_ref = ed_model.module.env.pool.get(ast_node_id); let ast_node_ref = ed_model.module.env.pool.get(ast_node_id);
if let Expr2::Blank {..} = ast_node_ref { if let Expr2::Blank {..} = ast_node_ref {
match ch { match ch {
'"' => { '"' => {
start_new_string(ed_model)? start_new_string(ed_model)?
}, },
'{' => { '{' => {
start_new_record(ed_model)? start_new_record(ed_model)?
}
_ => InputOutcome::Ignored
}
} else if let Some(prev_mark_node_id) = prev_mark_node_id_opt{
if prev_mark_node_id == curr_mark_node_id {
match ast_node_ref {
Expr2::SmallStr(old_arr_str) => {
update_small_string(
&ch, old_arr_str, ed_model
)?
}
Expr2::Str(old_pool_str) => {
update_string(
&ch.to_string(), old_pool_str, ed_model
)?
}
Expr2::InvalidLookup(old_pool_str) => {
update_invalid_lookup(
&ch.to_string(),
old_pool_str,
curr_mark_node_id,
ast_node_id,
ed_model
)?
}
Expr2::EmptyRecord => {
// prev_mark_node_id and curr_mark_node_id should be different to allow creating field at current caret position
InputOutcome::Ignored
}
Expr2::Record{ record_var:_, fields } => {
if curr_mark_node.get_content()?.chars().all(|chr| chr.is_ascii_alphanumeric()){
update_record_field(
&ch.to_string(),
ed_model.get_caret(),
curr_mark_node_id,
fields,
ed_model,
)?
} else {
InputOutcome::Ignored
} }
_ => InputOutcome::Ignored
} }
_ => InputOutcome::Ignored } else if let Some(prev_mark_node_id) = prev_mark_node_id_opt{
} if prev_mark_node_id == curr_mark_node_id {
} else if ch.is_ascii_alphanumeric() { // prev_mark_node_id != curr_mark_node_id
let prev_ast_node_id =
ed_model
.markup_node_pool
.get(prev_mark_node_id)
.get_ast_node_id();
let prev_node_ref = ed_model.module.env.pool.get(prev_ast_node_id);
match prev_node_ref {
Expr2::InvalidLookup(old_pool_str) => {
update_invalid_lookup(
&ch.to_string(),
old_pool_str,
prev_mark_node_id,
prev_ast_node_id,
ed_model
)?
}
Expr2::Record{ record_var:_, fields } => {
let prev_mark_node = ed_model.markup_node_pool.get(prev_mark_node_id);
if (curr_mark_node.get_content()? == nodes::RIGHT_ACCOLADE || curr_mark_node.get_content()? == nodes::COLON) &&
prev_mark_node.is_all_alphanumeric()? {
update_record_field(
&ch.to_string(),
ed_model.get_caret(),
prev_mark_node_id,
fields,
ed_model,
)?
} else if prev_mark_node.get_content()? == nodes::LEFT_ACCOLADE && curr_mark_node.is_all_alphanumeric()? {
update_record_field(
&ch.to_string(),
ed_model.get_caret(),
curr_mark_node_id,
fields,
ed_model,
)?
} else {
InputOutcome::Ignored
}
}
_ => {
match ast_node_ref { match ast_node_ref {
Expr2::SmallStr(old_arr_str) => {
update_small_string(
&ch, old_arr_str, ed_model
)?
}
Expr2::Str(old_pool_str) => {
update_string(
&ch.to_string(), old_pool_str, ed_model
)?
}
Expr2::InvalidLookup(old_pool_str) => {
update_invalid_lookup(
&ch.to_string(),
old_pool_str,
curr_mark_node_id,
ast_node_id,
ed_model
)?
}
Expr2::EmptyRecord => { Expr2::EmptyRecord => {
let sibling_ids = curr_mark_node.get_sibling_ids(&ed_model.markup_node_pool); // prev_mark_node_id and curr_mark_node_id should be different to allow creating field at current caret position
InputOutcome::Ignored
if ch.is_ascii_alphabetic() && ch.is_ascii_lowercase() { }
update_empty_record( Expr2::Record{ record_var:_, fields } => {
if curr_mark_node.get_content()?.chars().all(|chr| chr.is_ascii_alphanumeric()){
update_record_field(
&ch.to_string(), &ch.to_string(),
prev_mark_node_id, ed_model.get_caret(),
sibling_ids, curr_mark_node_id,
ed_model fields,
ed_model,
)? )?
} else { } else {
InputOutcome::Ignored InputOutcome::Ignored
@ -456,30 +504,99 @@ pub fn handle_new_char(received_char: &char, ed_model: &mut EdModel) -> EdResult
} }
_ => InputOutcome::Ignored _ => InputOutcome::Ignored
} }
} else if ch.is_ascii_alphanumeric() { // prev_mark_node_id != curr_mark_node_id
let prev_ast_node_id =
ed_model
.markup_node_pool
.get(prev_mark_node_id)
.get_ast_node_id();
let prev_node_ref = ed_model.module.env.pool.get(prev_ast_node_id);
match prev_node_ref {
Expr2::InvalidLookup(old_pool_str) => {
update_invalid_lookup(
&ch.to_string(),
old_pool_str,
prev_mark_node_id,
prev_ast_node_id,
ed_model
)?
}
Expr2::Record{ record_var:_, fields } => {
let prev_mark_node = ed_model.markup_node_pool.get(prev_mark_node_id);
if (curr_mark_node.get_content()? == nodes::RIGHT_ACCOLADE || curr_mark_node.get_content()? == nodes::COLON) &&
prev_mark_node.is_all_alphanumeric()? {
update_record_field(
&ch.to_string(),
ed_model.get_caret(),
prev_mark_node_id,
fields,
ed_model,
)?
} else if prev_mark_node.get_content()? == nodes::LEFT_ACCOLADE && curr_mark_node.is_all_alphanumeric()? {
update_record_field(
&ch.to_string(),
ed_model.get_caret(),
curr_mark_node_id,
fields,
ed_model,
)?
} else {
InputOutcome::Ignored
}
}
_ => {
match ast_node_ref {
Expr2::EmptyRecord => {
let sibling_ids = curr_mark_node.get_sibling_ids(&ed_model.markup_node_pool);
if ch.is_ascii_alphabetic() && ch.is_ascii_lowercase() {
update_empty_record(
&ch.to_string(),
prev_mark_node_id,
sibling_ids,
ed_model
)?
} else {
InputOutcome::Ignored
}
}
_ => InputOutcome::Ignored
}
}
}
} else if *ch == ':' {
let mark_parent_id_opt = curr_mark_node.get_parent_id_opt();
if let Some(mark_parent_id) = mark_parent_id_opt {
let parent_ast_id = ed_model.markup_node_pool.get(mark_parent_id).get_ast_node_id();
update_record_colon(ed_model, parent_ast_id)?
} else {
InputOutcome::Ignored
}
} else {
InputOutcome::Ignored
} }
}
} else if *ch == ':' {
let mark_parent_id_opt = curr_mark_node.get_parent_id_opt();
if let Some(mark_parent_id) = mark_parent_id_opt {
let parent_ast_id = ed_model.markup_node_pool.get(mark_parent_id).get_ast_node_id();
update_record_colon(ed_model, parent_ast_id)?
} else { } else {
// Not supporting any Expr2 right now that would allow prepending at the start of a line
InputOutcome::Ignored InputOutcome::Ignored
} }
} else { },
Err(e) => {
print_ui_err(&e);
InputOutcome::Ignored InputOutcome::Ignored
} }
};
} else { if let InputOutcome::Accepted = outcome {
// Not supporting any Expr2 right now that would allow prepending at the start of a line ed_model.set_sel_none();
InputOutcome::Ignored
} }
} else { outcome
InputOutcome::Ignored
}
} }
}; };

View file

@ -5,7 +5,9 @@ use crate::editor::render_ast::build_code_graphics;
use crate::editor::render_debug::build_debug_graphics; use crate::editor::render_debug::build_debug_graphics;
use crate::graphics::primitives::rect::Rect; use crate::graphics::primitives::rect::Rect;
use crate::ui::text::caret_w_select::make_caret_rect; use crate::ui::text::caret_w_select::make_caret_rect;
use crate::ui::text::caret_w_select::make_selection_rect;
use crate::ui::text::caret_w_select::CaretWSelect; use crate::ui::text::caret_w_select::CaretWSelect;
use crate::ui::text::selection::Selection;
use crate::ui::ui_error::MissingGlyphDims; use crate::ui::ui_error::MissingGlyphDims;
use cgmath::Vector2; use cgmath::Vector2;
use snafu::OptionExt; use snafu::OptionExt;
@ -70,20 +72,39 @@ pub fn build_selection_graphics(
let char_width = glyph_dim_rect.width; let char_width = glyph_dim_rect.width;
let char_height = glyph_dim_rect.height; let char_height = glyph_dim_rect.height;
let y_offset = 0.1 * char_height;
for caret_w_sel in caret_w_select_vec { for caret_w_sel in caret_w_select_vec {
let caret_row = caret_w_sel.caret_pos.line as f32; let caret_row = caret_w_sel.caret_pos.line as f32;
let caret_col = caret_w_sel.caret_pos.column as f32; let caret_col = caret_w_sel.caret_pos.column as f32;
let top_left_x = txt_coords.x + caret_col * char_width; let top_left_x = txt_coords.x + caret_col * char_width;
let top_left_y = txt_coords.y + caret_row * char_height + y_offset;
let top_left_y = txt_coords.y + caret_row * char_height + 0.1 * char_height; if let Some(selection) = caret_w_sel.selection_opt {
let Selection { start_pos, end_pos } = selection;
let sel_rect_x = txt_coords.x + ((start_pos.column as f32) * char_width);
let sel_rect_y = txt_coords.y + char_height * (start_pos.line as f32) + y_offset;
let width =
((end_pos.column as f32) * char_width) - ((start_pos.column as f32) * char_width);
rects.push(make_selection_rect(
sel_rect_x,
sel_rect_y,
width,
&glyph_dim_rect,
&config.ed_theme.ui_theme,
));
}
rects.push(make_caret_rect( rects.push(make_caret_rect(
top_left_x, top_left_x,
top_left_y, top_left_y,
&glyph_dim_rect, &glyph_dim_rect,
&config.ed_theme.ui_theme, &config.ed_theme.ui_theme,
)) ));
} }
Ok(rects) Ok(rects)

View file

@ -40,7 +40,7 @@ pub fn start_new_record(ed_model: &mut EdModel) -> EdResult<InputOutcome> {
ast_node_id, ast_node_id,
syn_high_style: HighlightStyle::Bracket, syn_high_style: HighlightStyle::Bracket,
attributes: Attributes::new(), attributes: Attributes::new(),
parent_id_opt: Some(curr_mark_node_id), // current node will be replace with nested one parent_id_opt: Some(curr_mark_node_id), // current node will be replaced with nested one
}; };
let left_bracket_node_id = mark_node_pool.add(left_bracket_node); let left_bracket_node_id = mark_node_pool.add(left_bracket_node);
@ -50,7 +50,7 @@ pub fn start_new_record(ed_model: &mut EdModel) -> EdResult<InputOutcome> {
ast_node_id, ast_node_id,
syn_high_style: HighlightStyle::Bracket, syn_high_style: HighlightStyle::Bracket,
attributes: Attributes::new(), attributes: Attributes::new(),
parent_id_opt: Some(curr_mark_node_id), // current node will be replace with nested one parent_id_opt: Some(curr_mark_node_id), // current node will be replaced with nested one
}; };
let right_bracket_node_id = mark_node_pool.add(right_bracket_node); let right_bracket_node_id = mark_node_pool.add(right_bracket_node);

View file

@ -33,6 +33,9 @@ impl SlowPool {
pub fn replace_node(&mut self, node_id: MarkNodeId, new_node: MarkupNode) { pub fn replace_node(&mut self, node_id: MarkNodeId, new_node: MarkupNode) {
self.nodes[node_id] = new_node; self.nodes[node_id] = new_node;
// TODO delete children of old node, this requires SlowPool to be changed to
// make sure the indexes still make sense after removal/compaction
} }
} }

View file

@ -31,3 +31,38 @@ pub fn index_of<T: ::std::fmt::Debug + std::cmp::Eq>(elt: T, slice: &[T]) -> EdR
Ok(index) Ok(index)
} }
// returns the index of the first occurence of element and index of the last occurence
pub fn first_last_index_of<T: ::std::fmt::Debug + std::cmp::Eq>(
elt: T,
slice: &[T],
) -> EdResult<(usize, usize)> {
let mut first_index_opt = None;
let mut last_index_opt = None;
for (index, list_elt) in slice.iter().enumerate() {
if *list_elt == elt {
if first_index_opt.is_none() {
first_index_opt = Some(index);
last_index_opt = Some(index);
} else {
last_index_opt = Some(index)
}
} else if last_index_opt.is_some() {
break;
}
}
if let (Some(first_index), Some(last_index)) = (first_index_opt, last_index_opt) {
Ok((first_index, last_index))
} else {
let elt_str = format!("{:?}", elt);
let collection_str = format!("{:?}", slice);
IndexOfFailed {
elt_str,
collection_str,
}
.fail()
}
}

View file

@ -53,17 +53,17 @@ pub fn create_render_pipeline(
entry_point: "fs_main", entry_point: "fs_main",
targets: &[wgpu::ColorTargetState { targets: &[wgpu::ColorTargetState {
format: color_format, format: color_format,
color_blend: wgpu::BlendState::REPLACE, color_blend: wgpu::BlendState {
operation: wgpu::BlendOperation::Add,
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
},
alpha_blend: wgpu::BlendState::REPLACE, alpha_blend: wgpu::BlendState::REPLACE,
write_mask: wgpu::ColorWrite::ALL, write_mask: wgpu::ColorWrite::ALL,
}], }],
}), }),
primitive: wgpu::PrimitiveState::default(), primitive: wgpu::PrimitiveState::default(),
depth_stencil: None, depth_stencil: None,
multisample: wgpu::MultisampleState { multisample: wgpu::MultisampleState::default(),
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
}) })
} }

View file

@ -112,6 +112,21 @@ pub fn make_caret_rect(
} }
} }
pub fn make_selection_rect(
sel_rect_x: f32,
sel_rect_y: f32,
width: f32,
glyph_dim_rect: &Rect,
ui_theme: &UITheme,
) -> Rect {
Rect {
top_left_coords: (sel_rect_x, sel_rect_y).into(),
height: glyph_dim_rect.height,
width,
color: ui_theme.select_highlight,
}
}
#[cfg(test)] #[cfg(test)]
pub mod test_caret_w_select { pub mod test_caret_w_select {
use crate::ui::text::caret_w_select::CaretWSelect; use crate::ui::text::caret_w_select::CaretWSelect;

View file

@ -30,6 +30,12 @@ pub struct Selection {
pub end_pos: TextPos, pub end_pos: TextPos,
} }
impl Selection {
pub fn is_on_same_line(&self) -> bool {
self.start_pos.line == self.end_pos.line
}
}
pub fn validate_raw_sel(raw_sel: RawSelection) -> UIResult<Selection> { pub fn validate_raw_sel(raw_sel: RawSelection) -> UIResult<Selection> {
validate_selection(raw_sel.start_pos, raw_sel.end_pos) validate_selection(raw_sel.start_pos, raw_sel.end_pos)
} }

View file

@ -6,6 +6,15 @@ pub struct TextPos {
pub column: usize, pub column: usize,
} }
impl TextPos {
pub fn increment_col(&self) -> TextPos {
TextPos {
line: self.line,
column: self.column + 1,
}
}
}
impl Ord for TextPos { impl Ord for TextPos {
fn cmp(&self, other: &Self) -> Ordering { fn cmp(&self, other: &Self) -> Ordering {
(self.line, self.column).cmp(&(other.line, other.column)) (self.line, self.column).cmp(&(other.line, other.column))

View file

@ -1,4 +1,4 @@
use gr_colors::{from_hsb, RgbaTup}; use gr_colors::{from_hsba, RgbaTup};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::graphics::colors as gr_colors; use crate::graphics::colors as gr_colors;
@ -22,7 +22,7 @@ impl Default for UITheme {
dark_brand: DARK_BRAND_COL, dark_brand: DARK_BRAND_COL,
text: gr_colors::WHITE, text: gr_colors::WHITE,
caret: gr_colors::WHITE, caret: gr_colors::WHITE,
select_highlight: from_hsb(240, 55, 100), select_highlight: from_hsba(240, 55, 100, 0.3),
} }
} }
} }

View file

@ -1,3 +1,4 @@
#[derive(Debug)]
pub struct Modifiers { pub struct Modifiers {
pub shift: bool, pub shift: bool,
pub ctrl: bool, pub ctrl: bool,