mirror of
https://github.com/roc-lang/roc.git
synced 2024-09-19 23:07:33 +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_types",
|
||||||
"roc_unify",
|
"roc_unify",
|
||||||
"roc_uniq",
|
"roc_uniq",
|
||||||
|
"ropey",
|
||||||
"snafu",
|
"snafu",
|
||||||
"target-lexicon",
|
"target-lexicon",
|
||||||
"ven_graph",
|
"ven_graph",
|
||||||
@ -3023,6 +3024,15 @@ dependencies = [
|
|||||||
"roc_types",
|
"roc_types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ropey"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0f3ef16589fdbb3e8fbce3dca944c08e61f39c7f16064b21a257d68ea911a83"
|
||||||
|
dependencies = [
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-argon2"
|
name = "rust-argon2"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
@ -68,6 +68,7 @@ snafu = { version = "0.6", features = ["backtraces"] }
|
|||||||
colored = "2"
|
colored = "2"
|
||||||
pest = "2.1"
|
pest = "2.1"
|
||||||
pest_derive = "2.1"
|
pest_derive = "2.1"
|
||||||
|
ropey = "1.2.0"
|
||||||
|
|
||||||
|
|
||||||
[dependencies.bytemuck]
|
[dependencies.bytemuck]
|
||||||
|
@ -19,13 +19,23 @@ pub enum EdError {
|
|||||||
vec_len: usize,
|
vec_len: usize,
|
||||||
backtrace: Backtrace,
|
backtrace: Backtrace,
|
||||||
},
|
},
|
||||||
#[snafu(display("InvalidSelection: {}", err_msg))]
|
#[snafu(display("InvalidSelection: {}.", err_msg))]
|
||||||
InvalidSelection {
|
InvalidSelection {
|
||||||
err_msg: String,
|
err_msg: String,
|
||||||
backtrace: Backtrace,
|
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."))]
|
#[snafu(display("MissingGlyphDims: glyph_dim_rect_opt was None for model. It needs to be set using the example_code_glyph_rect function."))]
|
||||||
MissingGlyphDims {},
|
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>;
|
pub type EdResult<T, E = EdError> = std::result::Result<T, E>;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
pub mod ast;
|
pub mod ast;
|
||||||
mod def;
|
mod def;
|
||||||
mod expr;
|
mod expr;
|
||||||
pub mod file;
|
pub mod roc_file;
|
||||||
mod module;
|
mod module;
|
||||||
mod pattern;
|
mod pattern;
|
||||||
mod pool;
|
mod pool;
|
||||||
|
@ -28,6 +28,8 @@ use crate::graphics::style::CODE_TXT_XY;
|
|||||||
use crate::selection::create_selection_rects;
|
use crate::selection::create_selection_rects;
|
||||||
use crate::tea::ed_model::EdModel;
|
use crate::tea::ed_model::EdModel;
|
||||||
use crate::tea::{ed_model, update};
|
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::collections::Vec as BumpVec;
|
||||||
use bumpalo::Bump;
|
use bumpalo::Bump;
|
||||||
use cgmath::Vector2;
|
use cgmath::Vector2;
|
||||||
@ -51,24 +53,37 @@ mod selection;
|
|||||||
mod tea;
|
mod tea;
|
||||||
mod util;
|
mod util;
|
||||||
mod vec_result;
|
mod vec_result;
|
||||||
|
mod text_buffer;
|
||||||
|
|
||||||
/// The editor is actually launched from the CLI if you pass it zero arguments,
|
/// 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.
|
/// or if you provide it 1 or more files or directories to open on launch.
|
||||||
pub fn launch(_filepaths: &[&Path]) -> io::Result<()> {
|
pub fn launch(filepaths: &[&Path]) -> io::Result<()> {
|
||||||
// TODO do any initialization here
|
//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(())
|
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();
|
env_logger::init();
|
||||||
|
|
||||||
// Open window and create a surface
|
// Open window and create a surface
|
||||||
let event_loop = winit::event_loop::EventLoop::new();
|
let event_loop = winit::event_loop::EventLoop::new();
|
||||||
|
|
||||||
let window = winit::window::WindowBuilder::new()
|
let window = winit::window::WindowBuilder::new()
|
||||||
|
.with_inner_size(PhysicalSize::new(1000.0, 800.0))
|
||||||
.build(&event_loop)
|
.build(&event_loop)
|
||||||
.unwrap();
|
.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 mut glyph_brush = build_glyph_brush(&gpu_device, render_format)?;
|
||||||
|
|
||||||
let is_animating = true;
|
let is_animating = true;
|
||||||
let mut ed_model = ed_model::init_model();
|
let mut ed_model_opt =
|
||||||
ed_model.glyph_dim_rect_opt = Some(example_code_glyph_rect(&mut glyph_brush));
|
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 mut keyboard_modifiers = ModifiersState::empty();
|
||||||
|
|
||||||
let arena = Bump::new();
|
let arena = Bump::new();
|
||||||
@ -180,7 +218,7 @@ fn run_event_loop() -> Result<(), Box<dyn Error>> {
|
|||||||
event: event::WindowEvent::ReceivedCharacter(ch),
|
event: event::WindowEvent::ReceivedCharacter(ch),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
update::update_text_state(&mut ed_model, &ch);
|
update::handle_new_char(&mut app_model, &ch);
|
||||||
}
|
}
|
||||||
//Keyboard Input
|
//Keyboard Input
|
||||||
Event::WindowEvent {
|
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
|
//using the "parse don't validate" pattern
|
||||||
struct ValidSelection {
|
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;
|
let RawSelection { start_pos, end_pos } = selection;
|
||||||
|
|
||||||
ensure!(
|
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::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;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EdModel {
|
pub struct EdModel {
|
||||||
pub lines: Vec<String>,
|
pub text_buf: TextBuffer,
|
||||||
pub caret_pos: Position,
|
pub caret_pos: Position,
|
||||||
pub selection_opt: Option<RawSelection>,
|
pub selection_opt: Option<RawSelection>,
|
||||||
pub glyph_dim_rect_opt: Option<Rect>,
|
pub glyph_dim_rect_opt: Option<Rect>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_model() -> EdModel {
|
pub fn init_model(file_path: &Path) -> EdResult<EdModel> {
|
||||||
EdModel {
|
Ok(EdModel {
|
||||||
lines: vec![String::new()],
|
text_buf: text_buffer::from_path(file_path)?,
|
||||||
caret_pos: Position { line: 0, column: 0 },
|
caret_pos: Position { line: 0, column: 0 },
|
||||||
selection_opt: None,
|
selection_opt: None,
|
||||||
glyph_dim_rect_opt: None,
|
glyph_dim_rect_opt: None,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//Is model.rs the right place for these structs?
|
//Is model.rs the right place for these structs?
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
pub mod ed_model;
|
pub mod ed_model;
|
||||||
|
pub mod app_model;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use super::ed_model::EdModel;
|
use super::ed_model::EdModel;
|
||||||
use super::ed_model::{Position, RawSelection};
|
use super::ed_model::{Position, RawSelection};
|
||||||
use crate::util::is_newline;
|
use crate::util::is_newline;
|
||||||
|
use crate::tea::app_model::AppModel;
|
||||||
use std::cmp::{max, min};
|
use std::cmp::{max, min};
|
||||||
|
|
||||||
pub fn move_caret_left(
|
pub fn move_caret_left(
|
||||||
@ -293,13 +294,14 @@ pub fn move_caret_down(
|
|||||||
(new_caret_pos, new_selection_opt)
|
(new_caret_pos, new_selection_opt)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_text_state(ed_model: &mut EdModel, received_char: &char) {
|
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;
|
ed_model.selection_opt = None;
|
||||||
|
|
||||||
match received_char {
|
match received_char {
|
||||||
'\u{8}' | '\u{7f}' => {
|
'\u{8}' | '\u{7f}' => {
|
||||||
// In Linux, we get a '\u{8}' when you press backspace,
|
// On Linux, '\u{8}' is backspace,
|
||||||
// but in macOS we get '\u{7f}'.
|
// on macOS '\u{7f}'.
|
||||||
if let Some(last_line) = ed_model.lines.last_mut() {
|
if let Some(last_line) = ed_model.lines.last_mut() {
|
||||||
if !last_line.is_empty() {
|
if !last_line.is_empty() {
|
||||||
last_line.pop();
|
last_line.pop();
|
||||||
@ -342,3 +344,4 @@ pub fn update_text_state(ed_model: &mut EdModel, received_char: &char) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod test_file {
|
mod test_file {
|
||||||
use bumpalo::Bump;
|
use bumpalo::Bump;
|
||||||
use roc_editor::lang::file::File;
|
use roc_editor::lang::roc_file::File;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn read_and_fmt_simple_roc_module() {
|
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 arena = Bump::new();
|
||||||
|
|
||||||
let file = File::read(simple_module_path, &arena)
|
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!(
|
assert_eq!(
|
||||||
file.fmt(),
|
file.fmt(),
|
||||||
|
Loading…
Reference in New Issue
Block a user