Browse Source

add flowy-editor crate

appflowy 3 years ago
parent
commit
fa061ea832

+ 1 - 0
rust-lib/Cargo.toml

@@ -13,6 +13,7 @@ members = [
   "flowy-infra",
   "flowy-workspace",
   "flowy-observable",
+  "flowy-editor",
 ]
 
 [profile.dev]

+ 15 - 0
rust-lib/flowy-editor/Cargo.toml

@@ -0,0 +1,15 @@
+[package]
+name = "flowy-editor"
+version = "0.1.0"
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+derive_more = {version = "0.99", features = ["display"]}
+flowy-dispatch = { path = "../flowy-dispatch" }
+flowy-log = { path = "../flowy-log" }
+flowy-derive = { path = "../flowy-derive" }
+flowy-database = { path = "../flowy-database" }
+diesel = {version = "1.4.7", features = ["sqlite"]}
+diesel_derives = {version = "1.4.1", features = ["sqlite"]}

+ 3 - 0
rust-lib/flowy-editor/Flowy.toml

@@ -0,0 +1,3 @@
+
+proto_crates = ["src/entities", "src/event.rs", "src/errors.rs"]
+event_files = ["src/event.rs"]

+ 44 - 0
rust-lib/flowy-editor/src/entities/doc/doc_create.rs

@@ -0,0 +1,44 @@
+use crate::{
+    entities::doc::parser::*,
+    errors::{ErrorBuilder, *},
+};
+use flowy_derive::ProtoBuf;
+use std::convert::TryInto;
+
+#[derive(ProtoBuf, Default)]
+pub struct CreateDocRequest {
+    #[pb(index = 1)]
+    view_id: String,
+
+    #[pb(index = 2)]
+    pub name: String,
+}
+
+pub struct CreateDocParams {
+    pub view_id: String,
+    pub name: String,
+}
+
+impl TryInto<CreateDocParams> for CreateDocRequest {
+    type Error = WorkspaceError;
+
+    fn try_into(self) -> Result<CreateDocParams, Self::Error> {
+        let name = DocName::parse(self.name)
+            .map_err(|e| {
+                ErrorBuilder::new(EditorErrorCode::DocNameInvalid)
+                    .msg(e)
+                    .build()
+            })?
+            .0;
+
+        let view_id = DocViewId::parse(self.view_id)
+            .map_err(|e| {
+                ErrorBuilder::new(EditorErrorCode::DocViewIdInvalid)
+                    .msg(e)
+                    .build()
+            })?
+            .0;
+
+        Ok(CreateDocParams { view_id, name })
+    }
+}

+ 0 - 0
rust-lib/flowy-editor/src/entities/doc/doc_modify.rs


+ 5 - 0
rust-lib/flowy-editor/src/entities/doc/mod.rs

@@ -0,0 +1,5 @@
+mod doc_create;
+mod doc_modify;
+mod parser;
+
+pub use doc_create::*;

+ 12 - 0
rust-lib/flowy-editor/src/entities/doc/parser/doc_name.rs

@@ -0,0 +1,12 @@
+#[derive(Debug)]
+pub struct DocName(pub String);
+
+impl DocName {
+    pub fn parse(s: String) -> Result<DocName, String> {
+        if s.trim().is_empty() {
+            return Err(format!("Doc name can not be empty or whitespace"));
+        }
+
+        Ok(Self(s))
+    }
+}

+ 12 - 0
rust-lib/flowy-editor/src/entities/doc/parser/doc_view_id.rs

@@ -0,0 +1,12 @@
+#[derive(Debug)]
+pub struct DocViewId(pub String);
+
+impl DocViewId {
+    pub fn parse(s: String) -> Result<DocViewId, String> {
+        if s.trim().is_empty() {
+            return Err(format!("Doc view id can not be empty or whitespace"));
+        }
+
+        Ok(Self(s))
+    }
+}

+ 5 - 0
rust-lib/flowy-editor/src/entities/doc/parser/mod.rs

@@ -0,0 +1,5 @@
+mod doc_name;
+mod doc_view_id;
+
+pub use doc_name::*;
+pub use doc_view_id::*;

+ 1 - 0
rust-lib/flowy-editor/src/entities/mod.rs

@@ -0,0 +1 @@
+pub mod doc;

+ 85 - 0
rust-lib/flowy-editor/src/errors.rs

@@ -0,0 +1,85 @@
+use derive_more::Display;
+use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
+use flowy_dispatch::prelude::{EventResponse, ResponseBuilder};
+use std::convert::TryInto;
+
+#[derive(Debug, Default, Clone, ProtoBuf)]
+pub struct EditorError {
+    #[pb(index = 1)]
+    pub code: UserErrorCode,
+
+    #[pb(index = 2)]
+    pub msg: String,
+}
+
+impl EditorError {
+    fn new(code: EditorErrorCode, msg: &str) -> Self {
+        Self {
+            code,
+            msg: msg.to_owned(),
+        }
+    }
+}
+
+#[derive(Debug, Clone, ProtoBuf_Enum, Display, PartialEq, Eq)]
+pub enum EditorErrorCode {
+    #[display(fmt = "Unknown")]
+    Unknown          = 0,
+
+    #[display(fmt = "EditorDBInternalError")]
+    EditorDBInternalError = 1,
+
+    #[display(fmt = "DocNameInvalid")]
+    DocNameInvalid   = 10,
+
+    #[display(fmt = "DocViewIdInvalid")]
+    DocViewIdInvalid = 11,
+}
+
+impl std::default::Default for UserErrorCode {
+    fn default() -> Self { UserErrorCode::Unknown }
+}
+
+impl std::convert::From<flowy_database::result::Error> for EditorError {
+    fn from(error: flowy_database::result::Error) -> Self {
+        ErrorBuilder::new(EditorErrorCode::EditorDBInternalError)
+            .error(error)
+            .build()
+    }
+}
+
+impl flowy_dispatch::Error for EditorError {
+    fn as_response(&self) -> EventResponse {
+        let bytes: Vec<u8> = self.clone().try_into().unwrap();
+        ResponseBuilder::Err().data(bytes).build()
+    }
+}
+
+pub struct ErrorBuilder {
+    pub code: UserErrorCode,
+    pub msg: Option<String>,
+}
+
+impl ErrorBuilder {
+    pub fn new(code: EditorErrorCode) -> Self { ErrorBuilder { code, msg: None } }
+
+    pub fn msg<T>(mut self, msg: T) -> Self
+    where
+        T: Into<String>,
+    {
+        self.msg = Some(msg.into());
+        self
+    }
+
+    pub fn error<T>(mut self, msg: T) -> Self
+    where
+        T: std::fmt::Debug,
+    {
+        self.msg = Some(format!("{:?}", msg));
+        self
+    }
+
+    pub fn build(mut self) -> EditorError {
+        EditorError::new(self.code, &self.msg.take().unwrap_or("".to_owned()))
+    }
+}

+ 0 - 0
rust-lib/flowy-editor/src/event.rs


+ 117 - 0
rust-lib/flowy-editor/src/file_manager/file.rs

@@ -0,0 +1,117 @@
+use std::{
+    ffi::OsString,
+    fs,
+    fs::File,
+    io,
+    io::{Read, Write},
+    path::{Path, PathBuf},
+    str,
+    time::SystemTime,
+};
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct FileId(pub(crate) usize);
+
+#[derive(Debug, Clone, Copy)]
+pub enum CharacterEncoding {
+    Utf8,
+    Utf8WithBom,
+}
+
+const UTF8_BOM: &str = "\u{feff}";
+impl CharacterEncoding {
+    pub(crate) fn guess(s: &[u8]) -> Self {
+        if s.starts_with(UTF8_BOM.as_bytes()) {
+            CharacterEncoding::Utf8WithBom
+        } else {
+            CharacterEncoding::Utf8
+        }
+    }
+}
+
+pub enum FileError {
+    Io(io::Error, PathBuf),
+    UnknownEncoding(PathBuf),
+    HasChanged(PathBuf),
+}
+
+pub struct FileInfo {
+    pub path: PathBuf,
+    pub modify_time: Option<SystemTime>,
+    pub has_changed: bool,
+    pub encoding: CharacterEncoding,
+}
+
+pub(crate) fn try_load_file<P>(path: P) -> Result<(String, FileInfo), FileError>
+where
+    P: AsRef<Path>,
+{
+    let mut f =
+        File::open(path.as_ref()).map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?;
+    let mut bytes = Vec::new();
+    f.read_to_end(&mut bytes)
+        .map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?;
+
+    let encoding = CharacterEncoding::guess(&bytes);
+    let s = try_decode(bytes, encoding, path.as_ref())?;
+    let info = FileInfo {
+        encoding,
+        path: path.as_ref().to_owned(),
+        modify_time: get_mod_time(&path),
+        has_changed: false,
+    };
+    Ok((s, info))
+}
+
+fn get_mod_time<P: AsRef<Path>>(path: P) -> Option<SystemTime> {
+    File::open(path)
+        .and_then(|f| f.metadata())
+        .and_then(|meta| meta.modified())
+        .ok()
+}
+
+fn try_save(
+    path: &Path,
+    text: &str,
+    encoding: CharacterEncoding,
+    _file_info: Option<&FileInfo>,
+) -> io::Result<()> {
+    let tmp_extension = path.extension().map_or_else(
+        || OsString::from("swp"),
+        |ext| {
+            let mut ext = ext.to_os_string();
+            ext.push(".swp");
+            ext
+        },
+    );
+    let tmp_path = &path.with_extension(tmp_extension);
+
+    let mut f = File::create(tmp_path)?;
+    match encoding {
+        CharacterEncoding::Utf8WithBom => f.write_all(UTF8_BOM.as_bytes())?,
+        CharacterEncoding::Utf8 => (),
+    }
+
+    f.write_all(text.as_bytes())?;
+    fs::rename(tmp_path, path)?;
+
+    Ok(())
+}
+
+fn try_decode(
+    bytes: Vec<u8>,
+    encoding: CharacterEncoding,
+    path: &Path,
+) -> Result<String, FileError> {
+    match encoding {
+        CharacterEncoding::Utf8 => {
+            Ok(String::from(str::from_utf8(&bytes).map_err(|_e| {
+                FileError::UnknownEncoding(path.to_owned())
+            })?))
+        },
+        CharacterEncoding::Utf8WithBom => {
+            let s = String::from_utf8(bytes)
+                .map_err(|_e| FileError::UnknownEncoding(path.to_owned()))?;
+            Ok(String::from(&s[UTF8_BOM.len()..]))
+        },
+    }
+}

+ 39 - 0
rust-lib/flowy-editor/src/file_manager/manager.rs

@@ -0,0 +1,39 @@
+use crate::file_manager::file::*;
+use std::{
+    collections::HashMap,
+    path::{Path, PathBuf},
+};
+
+pub struct FileManager {
+    open_files: HashMap<PathBuf, FileId>,
+    file_info: HashMap<FileId, FileInfo>,
+}
+
+impl FileManager {
+    pub fn new() -> Self {
+        Self {
+            open_files: HashMap::new(),
+            file_info: HashMap::new(),
+        }
+    }
+
+    pub fn get_info(&self, id: FileId) -> Option<&FileInfo> { self.file_info.get(&id) }
+
+    pub fn get_editor(&self, path: &Path) -> Option<FileId> { self.open_files.get(path).cloned() }
+
+    pub fn open(&mut self, path: &Path, id: FileId) -> Result<String, FileError> {
+        if !path.exists() {
+            return Ok("".to_string());
+        }
+
+        let (s, info) = try_load_file(path)?;
+        self.open_files.insert(path.to_owned(), id);
+        Ok(s)
+    }
+
+    pub fn close(&mut self, id: FileId) {
+        if let Some(info) = self.file_info.remove(&id) {
+            self.open_files.remove(&info.path);
+        }
+    }
+}

+ 2 - 0
rust-lib/flowy-editor/src/file_manager/mod.rs

@@ -0,0 +1,2 @@
+mod file;
+mod manager;

+ 0 - 0
rust-lib/flowy-editor/src/handlers/mod.rs


+ 6 - 0
rust-lib/flowy-editor/src/lib.rs

@@ -0,0 +1,6 @@
+mod entities;
+mod errors;
+mod event;
+mod file_manager;
+mod handlers;
+mod module;

+ 0 - 0
rust-lib/flowy-editor/src/module.rs