mirror of
https://github.com/roc-lang/roc.git
synced 2024-09-19 06:40:20 +03:00
progress integrating text rope
This commit is contained in:
parent
2b91262073
commit
bdf48d478f
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -2694,6 +2694,7 @@ dependencies = [
|
||||
"roc_types",
|
||||
"roc_unify",
|
||||
"roc_uniq",
|
||||
"ropey",
|
||||
"snafu",
|
||||
"target-lexicon",
|
||||
"ven_graph",
|
||||
@ -3023,6 +3024,15 @@ dependencies = [
|
||||
"roc_types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ropey"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f3ef16589fdbb3e8fbce3dca944c08e61f39c7f16064b21a257d68ea911a83"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-argon2"
|
||||
version = "0.8.3"
|
||||
|
@ -68,6 +68,7 @@ snafu = { version = "0.6", features = ["backtraces"] }
|
||||
colored = "2"
|
||||
pest = "2.1"
|
||||
pest_derive = "2.1"
|
||||
ropey = "1.2.0"
|
||||
|
||||
|
||||
[dependencies.bytemuck]
|
||||
|
@ -19,13 +19,23 @@ pub enum EdError {
|
||||
vec_len: usize,
|
||||
backtrace: Backtrace,
|
||||
},
|
||||
#[snafu(display("InvalidSelection: {}", err_msg))]
|
||||
#[snafu(display("InvalidSelection: {}.", err_msg))]
|
||||
InvalidSelection {
|
||||
err_msg: String,
|
||||
backtrace: Backtrace,
|
||||
},
|
||||
#[snafu(display("MissingGlyphDims: glyph_dim_rect_opt was None for model. It needs to be set using the example_code_glyph_rect function."))]
|
||||
MissingGlyphDims {},
|
||||
#[snafu(display("FileOpenFailed: failed to open file with path {} with the following error: {}.", path_str, err_msg))]
|
||||
FileOpenFailed {
|
||||
path_str: String,
|
||||
err_msg: String
|
||||
},
|
||||
#[snafu(display("TextBufReadFailed: the file {} could be opened but we encountered the following error while trying to read it: {}.", path_str, err_msg))]
|
||||
TextBufReadFailed {
|
||||
path_str: String,
|
||||
err_msg: String
|
||||
}
|
||||
}
|
||||
|
||||
pub type EdResult<T, E = EdError> = std::result::Result<T, E>;
|
||||
|
@ -1,7 +1,7 @@
|
||||
pub mod ast;
|
||||
mod def;
|
||||
mod expr;
|
||||
pub mod file;
|
||||
pub mod roc_file;
|
||||
mod module;
|
||||
mod pattern;
|
||||
mod pool;
|
||||
|
@ -28,6 +28,8 @@ use crate::graphics::style::CODE_TXT_XY;
|
||||
use crate::selection::create_selection_rects;
|
||||
use crate::tea::ed_model::EdModel;
|
||||
use crate::tea::{ed_model, update};
|
||||
use crate::tea::app_model::AppModel;
|
||||
use crate::vec_result::get_res;
|
||||
use bumpalo::collections::Vec as BumpVec;
|
||||
use bumpalo::Bump;
|
||||
use cgmath::Vector2;
|
||||
@ -51,24 +53,37 @@ mod selection;
|
||||
mod tea;
|
||||
mod util;
|
||||
mod vec_result;
|
||||
mod text_buffer;
|
||||
|
||||
/// The editor is actually launched from the CLI if you pass it zero arguments,
|
||||
/// or if you provide it 1 or more files or directories to open on launch.
|
||||
pub fn launch(_filepaths: &[&Path]) -> io::Result<()> {
|
||||
// TODO do any initialization here
|
||||
pub fn launch(filepaths: &[&Path]) -> io::Result<()> {
|
||||
//TODO support using multiple filepaths
|
||||
let first_path_opt = if filepaths.len() > 0 {
|
||||
match get_res(0, filepaths) {
|
||||
Ok(path_ref_ref) => Some(*path_ref_ref),
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
run_event_loop().expect("Error running event loop");
|
||||
run_event_loop(first_path_opt).expect("Error running event loop");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_event_loop() -> Result<(), Box<dyn Error>> {
|
||||
fn run_event_loop(file_path_opt: Option<&Path>) -> Result<(), Box<dyn Error>> {
|
||||
env_logger::init();
|
||||
|
||||
// Open window and create a surface
|
||||
let event_loop = winit::event_loop::EventLoop::new();
|
||||
|
||||
let window = winit::window::WindowBuilder::new()
|
||||
.with_inner_size(PhysicalSize::new(1000.0, 800.0))
|
||||
.build(&event_loop)
|
||||
.unwrap();
|
||||
|
||||
@ -124,8 +139,31 @@ fn run_event_loop() -> Result<(), Box<dyn Error>> {
|
||||
let mut glyph_brush = build_glyph_brush(&gpu_device, render_format)?;
|
||||
|
||||
let is_animating = true;
|
||||
let mut ed_model = ed_model::init_model();
|
||||
ed_model.glyph_dim_rect_opt = Some(example_code_glyph_rect(&mut glyph_brush));
|
||||
let mut ed_model_opt =
|
||||
if let Some(file_path) = file_path_opt {
|
||||
let ed_model_res =
|
||||
ed_model::init_model(file_path);
|
||||
|
||||
match ed_model_res {
|
||||
Ok(ed_model) => {
|
||||
ed_model.glyph_dim_rect_opt =
|
||||
Some(example_code_glyph_rect(&mut glyph_brush));
|
||||
|
||||
Some(ed_model)
|
||||
},
|
||||
Err(e) => {
|
||||
print_err(&e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut app_model = AppModel {
|
||||
ed_model_opt
|
||||
};
|
||||
|
||||
let mut keyboard_modifiers = ModifiersState::empty();
|
||||
|
||||
let arena = Bump::new();
|
||||
@ -180,7 +218,7 @@ fn run_event_loop() -> Result<(), Box<dyn Error>> {
|
||||
event: event::WindowEvent::ReceivedCharacter(ch),
|
||||
..
|
||||
} => {
|
||||
update::update_text_state(&mut ed_model, &ch);
|
||||
update::handle_new_char(&mut app_model, &ch);
|
||||
}
|
||||
//Keyboard Input
|
||||
Event::WindowEvent {
|
||||
|
1
editor/src/resources/strings.rs
Normal file
1
editor/src/resources/strings.rs
Normal file
@ -0,0 +1 @@
|
||||
pub const NOTHING_OPENED: &str = "Execute `cargo run edit <filename>` to open a file";
|
@ -9,10 +9,10 @@ use snafu::ensure;
|
||||
|
||||
//using the "parse don't validate" pattern
|
||||
struct ValidSelection {
|
||||
selection: RawSelection,
|
||||
pub selection: RawSelection,
|
||||
}
|
||||
|
||||
fn validate_selection(selection: RawSelection) -> EdResult<ValidSelection> {
|
||||
pub fn validate_selection(selection: RawSelection) -> EdResult<ValidSelection> {
|
||||
let RawSelection { start_pos, end_pos } = selection;
|
||||
|
||||
ensure!(
|
||||
|
7
editor/src/tea/app_model.rs
Normal file
7
editor/src/tea/app_model.rs
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
use crate::tea::ed_model::EdModel;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AppModel {
|
||||
pub ed_model_opt: Option<EdModel>
|
||||
}
|
@ -1,21 +1,25 @@
|
||||
use crate::graphics::primitives::rect::Rect;
|
||||
use crate::text_buffer;
|
||||
use crate::text_buffer::TextBuffer;
|
||||
use crate::error::EdResult;
|
||||
use std::path::Path;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EdModel {
|
||||
pub lines: Vec<String>,
|
||||
pub text_buf: TextBuffer,
|
||||
pub caret_pos: Position,
|
||||
pub selection_opt: Option<RawSelection>,
|
||||
pub glyph_dim_rect_opt: Option<Rect>,
|
||||
}
|
||||
|
||||
pub fn init_model() -> EdModel {
|
||||
EdModel {
|
||||
lines: vec![String::new()],
|
||||
pub fn init_model(file_path: &Path) -> EdResult<EdModel> {
|
||||
Ok(EdModel {
|
||||
text_buf: text_buffer::from_path(file_path)?,
|
||||
caret_pos: Position { line: 0, column: 0 },
|
||||
selection_opt: None,
|
||||
glyph_dim_rect_opt: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//Is model.rs the right place for these structs?
|
||||
|
@ -1,2 +1,3 @@
|
||||
pub mod ed_model;
|
||||
pub mod app_model;
|
||||
pub mod update;
|
||||
|
@ -1,6 +1,7 @@
|
||||
use super::ed_model::EdModel;
|
||||
use super::ed_model::{Position, RawSelection};
|
||||
use crate::util::is_newline;
|
||||
use crate::tea::app_model::AppModel;
|
||||
use std::cmp::{max, min};
|
||||
|
||||
pub fn move_caret_left(
|
||||
@ -293,52 +294,54 @@ pub fn move_caret_down(
|
||||
(new_caret_pos, new_selection_opt)
|
||||
}
|
||||
|
||||
pub fn update_text_state(ed_model: &mut EdModel, received_char: &char) {
|
||||
ed_model.selection_opt = None;
|
||||
pub fn handle_new_char(app_model: &mut AppModel, received_char: &char) {
|
||||
if let Some(ed_model) = app_model.ed_model_opt {
|
||||
ed_model.selection_opt = None;
|
||||
|
||||
match received_char {
|
||||
'\u{8}' | '\u{7f}' => {
|
||||
// In Linux, we get a '\u{8}' when you press backspace,
|
||||
// but in macOS we get '\u{7f}'.
|
||||
if let Some(last_line) = ed_model.lines.last_mut() {
|
||||
if !last_line.is_empty() {
|
||||
last_line.pop();
|
||||
} else if ed_model.lines.len() > 1 {
|
||||
ed_model.lines.pop();
|
||||
match received_char {
|
||||
'\u{8}' | '\u{7f}' => {
|
||||
// On Linux, '\u{8}' is backspace,
|
||||
// on macOS '\u{7f}'.
|
||||
if let Some(last_line) = ed_model.lines.last_mut() {
|
||||
if !last_line.is_empty() {
|
||||
last_line.pop();
|
||||
} else if ed_model.lines.len() > 1 {
|
||||
ed_model.lines.pop();
|
||||
}
|
||||
ed_model.caret_pos =
|
||||
move_caret_left(ed_model.caret_pos, None, false, &ed_model.lines).0;
|
||||
}
|
||||
ed_model.caret_pos =
|
||||
move_caret_left(ed_model.caret_pos, None, false, &ed_model.lines).0;
|
||||
}
|
||||
}
|
||||
'\u{e000}'..='\u{f8ff}' | '\u{f0000}'..='\u{ffffd}' | '\u{100000}'..='\u{10fffd}' => {
|
||||
// These are private use characters; ignore them.
|
||||
// See http://www.unicode.org/faq/private_use.html
|
||||
}
|
||||
ch if is_newline(ch) => {
|
||||
if let Some(last_line) = ed_model.lines.last_mut() {
|
||||
last_line.push(*received_char)
|
||||
'\u{e000}'..='\u{f8ff}' | '\u{f0000}'..='\u{ffffd}' | '\u{100000}'..='\u{10fffd}' => {
|
||||
// These are private use characters; ignore them.
|
||||
// See http://www.unicode.org/faq/private_use.html
|
||||
}
|
||||
ed_model.lines.push(String::new());
|
||||
ed_model.caret_pos = Position {
|
||||
line: ed_model.caret_pos.line + 1,
|
||||
column: 0,
|
||||
};
|
||||
|
||||
ed_model.selection_opt = None;
|
||||
}
|
||||
_ => {
|
||||
let nr_lines = ed_model.lines.len();
|
||||
|
||||
if let Some(last_line) = ed_model.lines.last_mut() {
|
||||
last_line.push(*received_char);
|
||||
|
||||
ch if is_newline(ch) => {
|
||||
if let Some(last_line) = ed_model.lines.last_mut() {
|
||||
last_line.push(*received_char)
|
||||
}
|
||||
ed_model.lines.push(String::new());
|
||||
ed_model.caret_pos = Position {
|
||||
line: nr_lines - 1,
|
||||
column: last_line.len(),
|
||||
line: ed_model.caret_pos.line + 1,
|
||||
column: 0,
|
||||
};
|
||||
|
||||
ed_model.selection_opt = None;
|
||||
}
|
||||
_ => {
|
||||
let nr_lines = ed_model.lines.len();
|
||||
|
||||
if let Some(last_line) = ed_model.lines.last_mut() {
|
||||
last_line.push(*received_char);
|
||||
|
||||
ed_model.caret_pos = Position {
|
||||
line: nr_lines - 1,
|
||||
column: last_line.len(),
|
||||
};
|
||||
|
||||
ed_model.selection_opt = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
86
editor/src/text_buffer.rs
Normal file
86
editor/src/text_buffer.rs
Normal file
@ -0,0 +1,86 @@
|
||||
|
||||
// Adapted from https://github.com/cessen/ropey by Nathan Vegdahl, licensed under the MIT license
|
||||
|
||||
use crate::error::EdResult;
|
||||
use crate::error::EdError::{TextBufReadFailed, FileOpenFailed};
|
||||
use crate::tea::ed_model::{Position, RawSelection};
|
||||
use crate::selection::{validate_selection};
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::ops::Range;
|
||||
use ropey::{Rope};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TextBuffer {
|
||||
text_rope: Rope,
|
||||
path_str: String,
|
||||
dirty: bool // true if text has been changed, false if all actions following change have executed
|
||||
}
|
||||
|
||||
impl TextBuffer {
|
||||
pub fn pop_char(&mut self, cursor_pos: Position) {
|
||||
let char_indx = self.pos_to_char_indx(cursor_pos);
|
||||
self.text_rope.remove(char_indx..char_indx);
|
||||
}
|
||||
|
||||
pub fn del_selection(&mut self, raw_sel: RawSelection) -> EdResult<()> {
|
||||
let range = self.sel_to_range(raw_sel)?;
|
||||
self.text_rope.remove(range);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pos_to_char_indx(&self, pos: Position) -> usize {
|
||||
self.text_rope.line_to_char(pos.line) + pos.column
|
||||
}
|
||||
|
||||
fn sel_to_range(&self, raw_sel: RawSelection) -> EdResult<Range<usize>> {
|
||||
let valid_sel = validate_selection(raw_sel)?;
|
||||
let start_char_indx = self.pos_to_char_indx(valid_sel.selection.start_pos);
|
||||
let end_char_indx = self.pos_to_char_indx(valid_sel.selection.start_pos);
|
||||
|
||||
Ok(start_char_indx..end_char_indx)
|
||||
}
|
||||
|
||||
pub fn from_path(&self, path: &Path) -> EdResult<TextBuffer> {
|
||||
// TODO benchmark different file reading methods, see #886
|
||||
let text_rope = rope_from_path(path)?;
|
||||
let path_str = path_to_string(path);
|
||||
|
||||
Ok(TextBuffer {
|
||||
text_rope,
|
||||
path_str,
|
||||
dirty: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_string(path: &Path) -> String {
|
||||
let mut path_str = String::new();
|
||||
path_str.push_str(&path.to_string_lossy());
|
||||
|
||||
path_str
|
||||
}
|
||||
|
||||
fn rope_from_path(path: &Path) -> EdResult<Rope> {
|
||||
match File::open(path) {
|
||||
Ok(file) => {
|
||||
let mut buf_reader = &mut io::BufReader::new(file);
|
||||
match Rope::from_reader(buf_reader) {
|
||||
Ok(rope) =>
|
||||
Ok(rope),
|
||||
Err(e) =>
|
||||
Err(TextBufReadFailed {
|
||||
path_str: path_to_string(path),
|
||||
err_msg: e.to_string()
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
Err(FileOpenFailed {
|
||||
path_str: path_to_string(path),
|
||||
err_msg: e.to_string()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
interface Storage
|
||||
exposes [
|
||||
Storage,
|
||||
decoder,
|
||||
get,
|
||||
listener,
|
||||
set
|
||||
]
|
||||
imports [
|
||||
Map.{ Map },
|
||||
Json.Decode.{ Decoder } as Decode
|
||||
Json.Encode as Encode
|
||||
Ports.FromJs as FromJs
|
||||
Ports.ToJs as ToJs
|
||||
]
|
||||
|
||||
|
||||
################################################################################
|
||||
## TYPES ##
|
||||
################################################################################
|
||||
|
||||
|
||||
Storage : [
|
||||
@Storage (Map Str Decode.Value)
|
||||
]
|
||||
|
||||
|
||||
################################################################################
|
||||
## API ##
|
||||
################################################################################
|
||||
|
||||
|
||||
get : Storage, Str, Decoder a -> [ Ok a, NotInStorage, DecodeError Decode.Error ]*
|
||||
get = \key, decoder, @Storage map ->
|
||||
when Map.get map key is
|
||||
Ok json ->
|
||||
Decode.decodeValue decoder json
|
||||
|
||||
Err NotFound ->
|
||||
NotInStorage
|
||||
|
||||
|
||||
set : Encode.Value, Str -> Effect {}
|
||||
set json str =
|
||||
ToJs.type "setStorage"
|
||||
|> ToJs.setFields [
|
||||
Field "key" (Encode.str str),
|
||||
Field "value" json
|
||||
]
|
||||
|> ToJs.send
|
||||
|
||||
|
||||
decoder : Decoder Storage
|
||||
decoder =
|
||||
Decode.mapType Decode.value
|
||||
|> Decode.map \map -> @Storage map
|
||||
|
||||
|
||||
################################################################################
|
||||
## PORTS INCOMING ##
|
||||
################################################################################
|
||||
|
||||
|
||||
listener : (Storage -> msg) -> FromJs.Listener msg
|
||||
listener toMsg =
|
||||
FromJs.listen "storageUpdated"
|
||||
(Decode.map decoder toMsg)
|
||||
|
@ -6,17 +6,17 @@ extern crate indoc;
|
||||
#[cfg(test)]
|
||||
mod test_file {
|
||||
use bumpalo::Bump;
|
||||
use roc_editor::lang::file::File;
|
||||
use roc_editor::lang::roc_file::File;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn read_and_fmt_simple_roc_module() {
|
||||
let simple_module_path = Path::new("./tests/modules/Simple.roc");
|
||||
let simple_module_path = Path::new("./tests/modules/SimpleUnformatted.roc");
|
||||
|
||||
let arena = Bump::new();
|
||||
|
||||
let file = File::read(simple_module_path, &arena)
|
||||
.expect("Could not read Simple.roc in test_file test");
|
||||
.expect("Could not read SimpleUnformatted.roc in test_file test");
|
||||
|
||||
assert_eq!(
|
||||
file.fmt(),
|
||||
|
Loading…
Reference in New Issue
Block a user