Add read-only git management
This commit is contained in:
parent
b9b46e015c
commit
e2e92d5dd5
9 changed files with 782 additions and 79 deletions
81
Cargo.lock
generated
81
Cargo.lock
generated
|
|
@ -190,6 +190,12 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
|
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android-tzdata"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
|
@ -647,6 +653,12 @@ version = "3.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytecount"
|
||||||
|
version = "0.6.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.14.0"
|
version = "1.14.0"
|
||||||
|
|
@ -751,6 +763,20 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
|
||||||
|
dependencies = [
|
||||||
|
"android-tzdata",
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-targets 0.48.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clipboard-win"
|
name = "clipboard-win"
|
||||||
version = "4.5.0"
|
version = "4.5.0"
|
||||||
|
|
@ -1001,6 +1027,7 @@ dependencies = [
|
||||||
"libcosmic",
|
"libcosmic",
|
||||||
"log",
|
"log",
|
||||||
"notify",
|
"notify",
|
||||||
|
"patch",
|
||||||
"rfd",
|
"rfd",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -2539,6 +2566,29 @@ dependencies = [
|
||||||
"syn 2.0.39",
|
"syn 2.0.39",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.58"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys 0.8.6",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced"
|
name = "iced"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
|
@ -3621,6 +3671,17 @@ dependencies = [
|
||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom_locate"
|
||||||
|
version = "4.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3"
|
||||||
|
dependencies = [
|
||||||
|
"bytecount",
|
||||||
|
"memchr",
|
||||||
|
"nom 7.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notify"
|
name = "notify"
|
||||||
version = "6.1.1"
|
version = "6.1.1"
|
||||||
|
|
@ -4064,6 +4125,17 @@ version = "1.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "patch"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15c07fdcdd8b05bdcf2a25bc195b6c34cbd52762ada9dba88bf81e7686d14e7a"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"nom 7.1.3",
|
||||||
|
"nom_locate",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
|
@ -6255,6 +6327,15 @@ dependencies = [
|
||||||
"windows-targets 0.42.2",
|
"windows-targets 0.42.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.51.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.48.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-implement"
|
name = "windows-implement"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,12 @@ grep = "0.3.1"
|
||||||
ignore = "0.4.21"
|
ignore = "0.4.21"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
|
patch = "0.7.0"
|
||||||
notify = "6.1.1"
|
notify = "6.1.1"
|
||||||
#TODO: this is using gtk for file dialogues
|
#TODO: this is using gtk for file dialogues
|
||||||
rfd = { version = "0.12.0", optional = true }
|
rfd = { version = "0.12.0", optional = true }
|
||||||
serde = { version = "1", features = ["serde_derive"] }
|
serde = { version = "1", features = ["serde_derive"] }
|
||||||
tokio = { version = "1", features = ["time"] }
|
tokio = { version = "1", features = ["process", "time"] }
|
||||||
# Extra syntax highlighting
|
# Extra syntax highlighting
|
||||||
syntect = "5.1.0"
|
syntect = "5.1.0"
|
||||||
two-face = "0.3.0"
|
two-face = "0.3.0"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@ character-count = Characters
|
||||||
character-count-no-spaces = Characters (without spaces)
|
character-count-no-spaces = Characters (without spaces)
|
||||||
line-count = Lines
|
line-count = Lines
|
||||||
|
|
||||||
|
## Git management
|
||||||
|
git-management = Git management
|
||||||
|
unstaged-changes = Unstaged changes
|
||||||
|
staged-changes = Staged changes
|
||||||
|
|
||||||
## Project search
|
## Project search
|
||||||
project-search = Project search
|
project-search = Project search
|
||||||
|
|
||||||
|
|
@ -50,6 +55,7 @@ revert-all-changes = Revert all changes
|
||||||
menu-document-statistics = Document statistics...
|
menu-document-statistics = Document statistics...
|
||||||
document-type = Document type...
|
document-type = Document type...
|
||||||
encoding = Encoding...
|
encoding = Encoding...
|
||||||
|
menu-git-management = Git management...
|
||||||
print = Print
|
print = Print
|
||||||
quit = Quit
|
quit = Quit
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ pub enum Action {
|
||||||
Redo,
|
Redo,
|
||||||
Save,
|
Save,
|
||||||
SelectAll,
|
SelectAll,
|
||||||
|
ToggleGitManagement,
|
||||||
ToggleProjectSearch,
|
ToggleProjectSearch,
|
||||||
ToggleSettingsPage,
|
ToggleSettingsPage,
|
||||||
ToggleWordWrap,
|
ToggleWordWrap,
|
||||||
|
|
@ -50,6 +51,7 @@ impl Action {
|
||||||
Self::Redo => Message::Redo,
|
Self::Redo => Message::Redo,
|
||||||
Self::Save => Message::Save,
|
Self::Save => Message::Save,
|
||||||
Self::SelectAll => Message::SelectAll,
|
Self::SelectAll => Message::SelectAll,
|
||||||
|
Self::ToggleGitManagement => Message::ToggleContextPage(ContextPage::GitManagement),
|
||||||
Self::ToggleProjectSearch => Message::ToggleContextPage(ContextPage::ProjectSearch),
|
Self::ToggleProjectSearch => Message::ToggleContextPage(ContextPage::ProjectSearch),
|
||||||
Self::ToggleSettingsPage => Message::ToggleContextPage(ContextPage::Settings),
|
Self::ToggleSettingsPage => Message::ToggleContextPage(ContextPage::Settings),
|
||||||
Self::ToggleWordWrap => Message::ToggleWordWrap,
|
Self::ToggleWordWrap => Message::ToggleWordWrap,
|
||||||
|
|
@ -118,6 +120,7 @@ impl KeyBind {
|
||||||
bind!([Ctrl, Shift], Z, Redo);
|
bind!([Ctrl, Shift], Z, Redo);
|
||||||
bind!([Ctrl], S, Save);
|
bind!([Ctrl], S, Save);
|
||||||
bind!([Ctrl], A, SelectAll);
|
bind!([Ctrl], A, SelectAll);
|
||||||
|
bind!([Ctrl, Shift], G, ToggleGitManagement);
|
||||||
bind!([Ctrl, Shift], F, ToggleProjectSearch);
|
bind!([Ctrl, Shift], F, ToggleProjectSearch);
|
||||||
bind!([Ctrl], Comma, ToggleSettingsPage);
|
bind!([Ctrl], Comma, ToggleSettingsPage);
|
||||||
bind!([Alt], Z, ToggleWordWrap);
|
bind!([Alt], Z, ToggleWordWrap);
|
||||||
|
|
|
||||||
265
src/git.rs
Normal file
265
src/git.rs
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
//TODO: try to use gitoxide
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fs, io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct GitDiff {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub staged: bool,
|
||||||
|
pub hunks: Vec<GitDiffHunk>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct GitDiffHunk {
|
||||||
|
pub old_range: patch::Range,
|
||||||
|
pub new_range: patch::Range,
|
||||||
|
pub lines: Vec<GitDiffLine>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum GitDiffLine {
|
||||||
|
Context {
|
||||||
|
old_line: u64,
|
||||||
|
new_line: u64,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
Added {
|
||||||
|
new_line: u64,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
Deleted {
|
||||||
|
old_line: u64,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct GitStatus {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub old_path: Option<PathBuf>,
|
||||||
|
pub staged: GitStatusKind,
|
||||||
|
pub unstaged: GitStatusKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum GitStatusKind {
|
||||||
|
Unmodified,
|
||||||
|
Modified,
|
||||||
|
FileTypeChanged,
|
||||||
|
Added,
|
||||||
|
Deleted,
|
||||||
|
Renamed,
|
||||||
|
Copied,
|
||||||
|
Updated,
|
||||||
|
Untracked,
|
||||||
|
SubmoduleModified,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<char> for GitStatusKind {
|
||||||
|
type Error = char;
|
||||||
|
|
||||||
|
fn try_from(c: char) -> Result<Self, Self::Error> {
|
||||||
|
// https://git-scm.com/docs/git-status#_short_format
|
||||||
|
match c {
|
||||||
|
' ' => Ok(Self::Unmodified),
|
||||||
|
'M' => Ok(Self::Modified),
|
||||||
|
'T' => Ok(Self::FileTypeChanged),
|
||||||
|
'A' => Ok(Self::Added),
|
||||||
|
'D' => Ok(Self::Deleted),
|
||||||
|
'R' => Ok(Self::Renamed),
|
||||||
|
'C' => Ok(Self::Copied),
|
||||||
|
'U' => Ok(Self::Updated),
|
||||||
|
'?' => Ok(Self::Untracked),
|
||||||
|
'm' => Ok(Self::SubmoduleModified),
|
||||||
|
_ => Err(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GitRepository {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitRepository {
|
||||||
|
pub fn new<P: AsRef<Path>>(path: P) -> io::Result<Self> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
if path.join(".git").exists() {
|
||||||
|
let path = fs::canonicalize(path)?;
|
||||||
|
Ok(Self { path })
|
||||||
|
} else {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
format!("{:?} is not a git repository", path),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command(&self) -> Command {
|
||||||
|
let mut command = Command::new("git");
|
||||||
|
command.arg("-C").arg(&self.path);
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn command_stdout(mut command: Command) -> io::Result<String> {
|
||||||
|
log::info!("{:?}", command);
|
||||||
|
let output = command.output().await?;
|
||||||
|
if output.status.success() {
|
||||||
|
String::from_utf8(output.stdout).map_err(|err| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("failed to parse git stdout: {}", err),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let mut msg = format!("git exited with {}", output.status);
|
||||||
|
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||||
|
msg.push_str("\nstdout> ");
|
||||||
|
msg.push_str(line);
|
||||||
|
}
|
||||||
|
for line in String::from_utf8_lossy(&output.stderr).lines() {
|
||||||
|
msg.push_str("\nstderr> ");
|
||||||
|
msg.push_str(line);
|
||||||
|
}
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn diff<P: AsRef<Path>>(&self, path: P, staged: bool) -> io::Result<GitDiff> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let mut command = self.command();
|
||||||
|
command.arg("diff");
|
||||||
|
if staged {
|
||||||
|
command.arg("--staged");
|
||||||
|
}
|
||||||
|
command.arg("--").arg(path);
|
||||||
|
let diff = Self::command_stdout(command).await?;
|
||||||
|
let patch = patch::Patch::from_single(&diff).map_err(|err| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("failed to parse diff: {}", err),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut hunks = Vec::with_capacity(patch.hunks.len());
|
||||||
|
for hunk in patch.hunks.iter() {
|
||||||
|
//TODO: validate range counts
|
||||||
|
let mut old_line = hunk.old_range.start;
|
||||||
|
let mut new_line = hunk.new_range.start;
|
||||||
|
|
||||||
|
let mut lines = Vec::with_capacity(hunk.lines.len());
|
||||||
|
for line in hunk.lines.iter() {
|
||||||
|
match line {
|
||||||
|
patch::Line::Context(text) => {
|
||||||
|
lines.push(GitDiffLine::Context {
|
||||||
|
old_line,
|
||||||
|
new_line,
|
||||||
|
text: text.to_string(),
|
||||||
|
});
|
||||||
|
old_line += 1;
|
||||||
|
new_line += 1;
|
||||||
|
}
|
||||||
|
patch::Line::Add(text) => {
|
||||||
|
lines.push(GitDiffLine::Added {
|
||||||
|
new_line,
|
||||||
|
text: text.to_string(),
|
||||||
|
});
|
||||||
|
new_line += 1;
|
||||||
|
}
|
||||||
|
patch::Line::Remove(text) => {
|
||||||
|
lines.push(GitDiffLine::Deleted {
|
||||||
|
old_line,
|
||||||
|
text: text.to_string(),
|
||||||
|
});
|
||||||
|
old_line += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hunks.push(GitDiffHunk {
|
||||||
|
old_range: hunk.old_range.clone(),
|
||||||
|
new_range: hunk.new_range.clone(),
|
||||||
|
lines,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(GitDiff {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
staged,
|
||||||
|
hunks,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn status(&self) -> io::Result<Vec<GitStatus>> {
|
||||||
|
let mut command = self.command();
|
||||||
|
command.arg("status").arg("-z");
|
||||||
|
let stdout = Self::command_stdout(command).await?;
|
||||||
|
|
||||||
|
let mut status = Vec::new();
|
||||||
|
let mut lines = stdout.split('\0');
|
||||||
|
while let Some(line) = lines.next() {
|
||||||
|
macro_rules! invalid_line {
|
||||||
|
() => {{
|
||||||
|
log::warn!("invalid git status line {:?}", line);
|
||||||
|
continue;
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.is_empty() {
|
||||||
|
// Ignore empty lines
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut chars = line.chars();
|
||||||
|
|
||||||
|
// Get staged status
|
||||||
|
let staged = match chars.next() {
|
||||||
|
Some(some) => match GitStatusKind::try_from(some) {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(_) => invalid_line!(),
|
||||||
|
},
|
||||||
|
None => invalid_line!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get unstaged status
|
||||||
|
let unstaged = match chars.next() {
|
||||||
|
Some(some) => match GitStatusKind::try_from(some) {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(_) => invalid_line!(),
|
||||||
|
},
|
||||||
|
None => invalid_line!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skip space
|
||||||
|
match chars.next() {
|
||||||
|
Some(' ') => {}
|
||||||
|
_ => invalid_line!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rest of the chars are in the path
|
||||||
|
let relative_path: String = chars.collect();
|
||||||
|
|
||||||
|
let old_path = if staged == GitStatusKind::Renamed || unstaged == GitStatusKind::Renamed
|
||||||
|
{
|
||||||
|
match lines.next() {
|
||||||
|
Some(old_relative_path) => Some(self.path.join(old_relative_path)),
|
||||||
|
None => invalid_line!(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
status.push(GitStatus {
|
||||||
|
path: self.path.join(relative_path),
|
||||||
|
old_path,
|
||||||
|
staged,
|
||||||
|
unstaged,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
468
src/main.rs
468
src/main.rs
|
|
@ -10,7 +10,7 @@ use cosmic::{
|
||||||
futures::{self, SinkExt},
|
futures::{self, SinkExt},
|
||||||
keyboard, subscription,
|
keyboard, subscription,
|
||||||
widget::{row, text},
|
widget::{row, text},
|
||||||
window, Alignment, Length, Point,
|
window, Alignment, Background, Color, Length, Point,
|
||||||
},
|
},
|
||||||
style, theme,
|
style, theme,
|
||||||
widget::{self, button, icon, nav_bar, segmented_button, view_switcher},
|
widget::{self, button, icon, nav_bar, segmented_button, view_switcher},
|
||||||
|
|
@ -29,6 +29,9 @@ use tokio::time;
|
||||||
use config::{Action, AppTheme, Config, CONFIG_VERSION};
|
use config::{Action, AppTheme, Config, CONFIG_VERSION};
|
||||||
mod config;
|
mod config;
|
||||||
|
|
||||||
|
use git::{GitDiff, GitDiffLine, GitRepository, GitStatus, GitStatusKind};
|
||||||
|
mod git;
|
||||||
|
|
||||||
use icon_cache::IconCache;
|
use icon_cache::IconCache;
|
||||||
mod icon_cache;
|
mod icon_cache;
|
||||||
|
|
||||||
|
|
@ -49,7 +52,7 @@ mod project;
|
||||||
use self::search::ProjectSearchResult;
|
use self::search::ProjectSearchResult;
|
||||||
mod search;
|
mod search;
|
||||||
|
|
||||||
use self::tab::Tab;
|
use self::tab::{EditorTab, GitDiffTab, Tab};
|
||||||
mod tab;
|
mod tab;
|
||||||
|
|
||||||
use self::text_box::text_box;
|
use self::text_box::text_box;
|
||||||
|
|
@ -163,6 +166,7 @@ pub enum Message {
|
||||||
Cut,
|
Cut,
|
||||||
DefaultFont(usize),
|
DefaultFont(usize),
|
||||||
DefaultFontSize(usize),
|
DefaultFontSize(usize),
|
||||||
|
GitProjectStatus(Vec<(String, PathBuf, Vec<GitStatus>)>),
|
||||||
Key(keyboard::Modifiers, keyboard::KeyCode),
|
Key(keyboard::Modifiers, keyboard::KeyCode),
|
||||||
NewFile,
|
NewFile,
|
||||||
NewWindow,
|
NewWindow,
|
||||||
|
|
@ -170,11 +174,13 @@ pub enum Message {
|
||||||
NotifyWatcher(WatcherWrapper),
|
NotifyWatcher(WatcherWrapper),
|
||||||
OpenFileDialog,
|
OpenFileDialog,
|
||||||
OpenFile(PathBuf),
|
OpenFile(PathBuf),
|
||||||
|
OpenGitDiff(PathBuf, GitDiff),
|
||||||
OpenProjectDialog,
|
OpenProjectDialog,
|
||||||
OpenProject(PathBuf),
|
OpenProject(PathBuf),
|
||||||
OpenSearchResult(usize, usize),
|
OpenSearchResult(usize, usize),
|
||||||
Paste,
|
Paste,
|
||||||
PasteValue(String),
|
PasteValue(String),
|
||||||
|
PrepareGitDiff(PathBuf, PathBuf, bool),
|
||||||
ProjectSearchResult(ProjectSearchResult),
|
ProjectSearchResult(ProjectSearchResult),
|
||||||
ProjectSearchSubmit,
|
ProjectSearchSubmit,
|
||||||
ProjectSearchValue(String),
|
ProjectSearchValue(String),
|
||||||
|
|
@ -203,6 +209,7 @@ pub enum Message {
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum ContextPage {
|
pub enum ContextPage {
|
||||||
DocumentStatistics,
|
DocumentStatistics,
|
||||||
|
GitManagement,
|
||||||
//TODO: Move search to pop-up
|
//TODO: Move search to pop-up
|
||||||
ProjectSearch,
|
ProjectSearch,
|
||||||
Settings,
|
Settings,
|
||||||
|
|
@ -212,6 +219,7 @@ impl ContextPage {
|
||||||
fn title(&self) -> String {
|
fn title(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::DocumentStatistics => fl!("document-statistics"),
|
Self::DocumentStatistics => fl!("document-statistics"),
|
||||||
|
Self::GitManagement => fl!("git-management"),
|
||||||
Self::ProjectSearch => fl!("project-search"),
|
Self::ProjectSearch => fl!("project-search"),
|
||||||
Self::Settings => fl!("settings"),
|
Self::Settings => fl!("settings"),
|
||||||
}
|
}
|
||||||
|
|
@ -230,6 +238,8 @@ pub struct App {
|
||||||
font_sizes: Vec<u16>,
|
font_sizes: Vec<u16>,
|
||||||
theme_names: Vec<String>,
|
theme_names: Vec<String>,
|
||||||
context_page: ContextPage,
|
context_page: ContextPage,
|
||||||
|
git_project_status: Option<Vec<(String, PathBuf, Vec<GitStatus>)>>,
|
||||||
|
projects: Vec<(String, PathBuf)>,
|
||||||
project_search_id: widget::Id,
|
project_search_id: widget::Id,
|
||||||
project_search_value: String,
|
project_search_value: String,
|
||||||
project_search_result: Option<ProjectSearchResult>,
|
project_search_result: Option<ProjectSearchResult>,
|
||||||
|
|
@ -300,25 +310,31 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_project<P: AsRef<Path>>(&mut self, path: P) {
|
pub fn open_project<P: AsRef<Path>>(&mut self, path: P) {
|
||||||
let node = match ProjectNode::new(&path) {
|
let path = path.as_ref();
|
||||||
|
let node = match ProjectNode::new(path) {
|
||||||
Ok(mut node) => {
|
Ok(mut node) => {
|
||||||
match &mut node {
|
match &mut node {
|
||||||
ProjectNode::Folder { open, root, .. } => {
|
ProjectNode::Folder {
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
open,
|
||||||
|
root,
|
||||||
|
} => {
|
||||||
*open = true;
|
*open = true;
|
||||||
*root = true;
|
*root = true;
|
||||||
|
|
||||||
|
// Save the absolute path
|
||||||
|
self.projects.push((name.to_string(), path.to_path_buf()));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::error!(
|
log::error!("failed to open project {:?}: not a directory", path);
|
||||||
"failed to open project {:?}: not a directory",
|
|
||||||
path.as_ref()
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
node
|
node
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("failed to open project {:?}: {}", path.as_ref(), err);
|
log::error!("failed to open project {:?}: {}", path, err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -351,13 +367,13 @@ impl App {
|
||||||
let mut activate_opt = None;
|
let mut activate_opt = None;
|
||||||
for entity in self.tab_model.iter() {
|
for entity in self.tab_model.iter() {
|
||||||
match self.tab_model.data::<Tab>(entity) {
|
match self.tab_model.data::<Tab>(entity) {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
if tab.path_opt.as_ref() == Some(&canonical) {
|
if tab.path_opt.as_ref() == Some(&canonical) {
|
||||||
activate_opt = Some(entity);
|
activate_opt = Some(entity);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(entity) = activate_opt {
|
if let Some(entity) = activate_opt {
|
||||||
|
|
@ -365,12 +381,12 @@ impl App {
|
||||||
return Some(entity);
|
return Some(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut tab = Tab::new(&self.config);
|
let mut tab = EditorTab::new(&self.config);
|
||||||
tab.open(canonical);
|
tab.open(canonical);
|
||||||
tab.watch(&mut self.watcher_opt);
|
tab.watch(&mut self.watcher_opt);
|
||||||
tab
|
tab
|
||||||
}
|
}
|
||||||
None => Tab::new(&self.config),
|
None => EditorTab::new(&self.config),
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
|
|
@ -378,7 +394,7 @@ impl App {
|
||||||
.insert()
|
.insert()
|
||||||
.text(tab.title())
|
.text(tab.title())
|
||||||
.icon(tab.icon(16))
|
.icon(tab.icon(16))
|
||||||
.data::<Tab>(tab)
|
.data::<Tab>(Tab::Editor(tab))
|
||||||
.closable()
|
.closable()
|
||||||
.activate()
|
.activate()
|
||||||
.id(),
|
.id(),
|
||||||
|
|
@ -389,7 +405,7 @@ impl App {
|
||||||
//TODO: provide iterator over data
|
//TODO: provide iterator over data
|
||||||
let entities: Vec<_> = self.tab_model.iter().collect();
|
let entities: Vec<_> = self.tab_model.iter().collect();
|
||||||
for entity in entities {
|
for entity in entities {
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
if let Some(Tab::Editor(tab)) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
tab.set_config(&self.config);
|
tab.set_config(&self.config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -411,7 +427,8 @@ impl App {
|
||||||
|
|
||||||
fn update_nav_bar_active(&mut self) {
|
fn update_nav_bar_active(&mut self) {
|
||||||
let tab_path_opt = match self.active_tab() {
|
let tab_path_opt = match self.active_tab() {
|
||||||
Some(tab) => tab.path_opt.clone(),
|
Some(Tab::Editor(tab)) => tab.path_opt.clone(),
|
||||||
|
Some(Tab::GitDiff(tab)) => Some(tab.diff.path.clone()),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -464,8 +481,10 @@ impl App {
|
||||||
|
|
||||||
let title = match self.active_tab() {
|
let title = match self.active_tab() {
|
||||||
Some(tab) => {
|
Some(tab) => {
|
||||||
// Force redraw on tab switches
|
if let Tab::Editor(inner) = tab {
|
||||||
tab.editor.lock().unwrap().buffer_mut().set_redraw(true);
|
// Force redraw on tab switches
|
||||||
|
inner.editor.lock().unwrap().buffer_mut().set_redraw(true);
|
||||||
|
}
|
||||||
tab.title()
|
tab.title()
|
||||||
}
|
}
|
||||||
None => format!("No Open File"),
|
None => format!("No Open File"),
|
||||||
|
|
@ -554,6 +573,8 @@ impl Application for App {
|
||||||
font_sizes,
|
font_sizes,
|
||||||
theme_names,
|
theme_names,
|
||||||
context_page: ContextPage::Settings,
|
context_page: ContextPage::Settings,
|
||||||
|
git_project_status: None,
|
||||||
|
projects: Vec::new(),
|
||||||
project_search_id: widget::Id::unique(),
|
project_search_id: widget::Id::unique(),
|
||||||
project_search_value: String::new(),
|
project_search_value: String::new(),
|
||||||
project_search_result: None,
|
project_search_result: None,
|
||||||
|
|
@ -709,17 +730,17 @@ impl Application for App {
|
||||||
log::info!("TODO");
|
log::info!("TODO");
|
||||||
}
|
}
|
||||||
Message::Copy => match self.active_tab() {
|
Message::Copy => match self.active_tab() {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
let editor = tab.editor.lock().unwrap();
|
let editor = tab.editor.lock().unwrap();
|
||||||
let selection_opt = editor.copy_selection();
|
let selection_opt = editor.copy_selection();
|
||||||
if let Some(selection) = selection_opt {
|
if let Some(selection) = selection_opt {
|
||||||
return clipboard::write(selection);
|
return clipboard::write(selection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Message::Cut => match self.active_tab() {
|
Message::Cut => match self.active_tab() {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
let mut editor = tab.editor.lock().unwrap();
|
let mut editor = tab.editor.lock().unwrap();
|
||||||
let selection_opt = editor.copy_selection();
|
let selection_opt = editor.copy_selection();
|
||||||
editor.delete_selection();
|
editor.delete_selection();
|
||||||
|
|
@ -727,7 +748,7 @@ impl Application for App {
|
||||||
return clipboard::write(selection);
|
return clipboard::write(selection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Message::DefaultFont(index) => {
|
Message::DefaultFont(index) => {
|
||||||
match self.font_names.get(index) {
|
match self.font_names.get(index) {
|
||||||
|
|
@ -748,7 +769,9 @@ impl Application for App {
|
||||||
// This does a complete reset of shaping data!
|
// This does a complete reset of shaping data!
|
||||||
let entities: Vec<_> = self.tab_model.iter().collect();
|
let entities: Vec<_> = self.tab_model.iter().collect();
|
||||||
for entity in entities {
|
for entity in entities {
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
if let Some(Tab::Editor(tab)) =
|
||||||
|
self.tab_model.data_mut::<Tab>(entity)
|
||||||
|
{
|
||||||
let mut editor = tab.editor.lock().unwrap();
|
let mut editor = tab.editor.lock().unwrap();
|
||||||
for line in editor.buffer_mut().lines.iter_mut() {
|
for line in editor.buffer_mut().lines.iter_mut() {
|
||||||
line.reset();
|
line.reset();
|
||||||
|
|
@ -774,6 +797,9 @@ impl Application for App {
|
||||||
log::warn!("failed to find font with index {}", index);
|
log::warn!("failed to find font with index {}", index);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Message::GitProjectStatus(project_status) => {
|
||||||
|
self.git_project_status = Some(project_status);
|
||||||
|
}
|
||||||
Message::Key(modifiers, key_code) => {
|
Message::Key(modifiers, key_code) => {
|
||||||
for (key_bind, action) in self.config.keybinds.iter() {
|
for (key_bind, action) in self.config.keybinds.iter() {
|
||||||
if key_bind.matches(modifiers, key_code) {
|
if key_bind.matches(modifiers, key_code) {
|
||||||
|
|
@ -803,7 +829,7 @@ impl Application for App {
|
||||||
let mut needs_reload = Vec::new();
|
let mut needs_reload = Vec::new();
|
||||||
for entity in self.tab_model.iter() {
|
for entity in self.tab_model.iter() {
|
||||||
match self.tab_model.data::<Tab>(entity) {
|
match self.tab_model.data::<Tab>(entity) {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
if let Some(path) = &tab.path_opt {
|
if let Some(path) = &tab.path_opt {
|
||||||
if event.paths.contains(&path) {
|
if event.paths.contains(&path) {
|
||||||
if tab.changed() {
|
if tab.changed() {
|
||||||
|
|
@ -817,16 +843,16 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for entity in needs_reload {
|
for entity in needs_reload {
|
||||||
match self.tab_model.data_mut::<Tab>(entity) {
|
match self.tab_model.data_mut::<Tab>(entity) {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
tab.reload();
|
tab.reload();
|
||||||
}
|
}
|
||||||
None => {
|
_ => {
|
||||||
log::warn!("failed to find tab {:?} that needs reload", entity);
|
log::warn!("failed to find tab {:?} that needs reload", entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -839,10 +865,10 @@ impl Application for App {
|
||||||
|
|
||||||
for entity in self.tab_model.iter() {
|
for entity in self.tab_model.iter() {
|
||||||
match self.tab_model.data::<Tab>(entity) {
|
match self.tab_model.data::<Tab>(entity) {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
tab.watch(&mut self.watcher_opt);
|
tab.watch(&mut self.watcher_opt);
|
||||||
}
|
}
|
||||||
None => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -867,6 +893,39 @@ impl Application for App {
|
||||||
self.open_tab(Some(path));
|
self.open_tab(Some(path));
|
||||||
return self.update_tab();
|
return self.update_tab();
|
||||||
}
|
}
|
||||||
|
Message::OpenGitDiff(project_path, diff) => {
|
||||||
|
let relative_path = match diff.path.strip_prefix(project_path.clone()) {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!(
|
||||||
|
"failed to find relative path of {:?} in project {:?}: {}",
|
||||||
|
diff.path,
|
||||||
|
project_path,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
&diff.path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let title = format!(
|
||||||
|
"{}: {}",
|
||||||
|
if diff.staged {
|
||||||
|
fl!("staged-changes")
|
||||||
|
} else {
|
||||||
|
fl!("unstaged-changes")
|
||||||
|
},
|
||||||
|
relative_path.display()
|
||||||
|
);
|
||||||
|
let icon = mime_icon(&diff.path, 16);
|
||||||
|
let tab = Tab::GitDiff(GitDiffTab { title, diff });
|
||||||
|
self.tab_model
|
||||||
|
.insert()
|
||||||
|
.text(tab.title())
|
||||||
|
.icon(icon)
|
||||||
|
.data::<Tab>(tab)
|
||||||
|
.closable()
|
||||||
|
.activate();
|
||||||
|
return self.update_tab();
|
||||||
|
}
|
||||||
Message::OpenProjectDialog => {
|
Message::OpenProjectDialog => {
|
||||||
#[cfg(feature = "rfd")]
|
#[cfg(feature = "rfd")]
|
||||||
return Command::perform(
|
return Command::perform(
|
||||||
|
|
@ -927,12 +986,43 @@ impl Application for App {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Message::PasteValue(value) => match self.active_tab() {
|
Message::PasteValue(value) => match self.active_tab() {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
let mut editor = tab.editor.lock().unwrap();
|
let mut editor = tab.editor.lock().unwrap();
|
||||||
editor.insert_string(&value, None);
|
editor.insert_string(&value, None);
|
||||||
}
|
}
|
||||||
None => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
Message::PrepareGitDiff(project_path, path, staged) => {
|
||||||
|
return Command::perform(
|
||||||
|
async move {
|
||||||
|
//TODO: send errors to UI
|
||||||
|
match GitRepository::new(&project_path) {
|
||||||
|
Ok(repo) => match repo.diff(&path, staged).await {
|
||||||
|
Ok(diff) => {
|
||||||
|
return message::app(Message::OpenGitDiff(project_path, diff));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(
|
||||||
|
"failed to get diff of {:?} in {:?}: {}",
|
||||||
|
path,
|
||||||
|
project_path,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(
|
||||||
|
"failed to open repository {:?}: {}",
|
||||||
|
project_path,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message::none()
|
||||||
|
},
|
||||||
|
|x| x,
|
||||||
|
);
|
||||||
|
}
|
||||||
Message::ProjectSearchResult(project_search_result) => {
|
Message::ProjectSearchResult(project_search_result) => {
|
||||||
self.project_search_result = Some(project_search_result);
|
self.project_search_result = Some(project_search_result);
|
||||||
|
|
||||||
|
|
@ -942,19 +1032,7 @@ impl Application for App {
|
||||||
Message::ProjectSearchSubmit => {
|
Message::ProjectSearchSubmit => {
|
||||||
//TODO: Figure out length requirements?
|
//TODO: Figure out length requirements?
|
||||||
if !self.project_search_value.is_empty() {
|
if !self.project_search_value.is_empty() {
|
||||||
//TODO: cache projects outside of nav model?
|
let projects = self.projects.clone();
|
||||||
let mut project_paths = Vec::new();
|
|
||||||
for id in self.nav_model.iter() {
|
|
||||||
match self.nav_model.data(id) {
|
|
||||||
Some(ProjectNode::Folder { path, root, .. }) => {
|
|
||||||
if *root {
|
|
||||||
project_paths.push(path.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let project_search_value = self.project_search_value.clone();
|
let project_search_value = self.project_search_value.clone();
|
||||||
let mut project_search_result = ProjectSearchResult {
|
let mut project_search_result = ProjectSearchResult {
|
||||||
value: project_search_value.clone(),
|
value: project_search_value.clone(),
|
||||||
|
|
@ -965,7 +1043,7 @@ impl Application for App {
|
||||||
return Command::perform(
|
return Command::perform(
|
||||||
async move {
|
async move {
|
||||||
let task_res = tokio::task::spawn_blocking(move || {
|
let task_res = tokio::task::spawn_blocking(move || {
|
||||||
project_search_result.search_projects(project_paths);
|
project_search_result.search_projects(projects);
|
||||||
message::app(Message::ProjectSearchResult(project_search_result))
|
message::app(Message::ProjectSearchResult(project_search_result))
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
@ -989,17 +1067,17 @@ impl Application for App {
|
||||||
return window::close();
|
return window::close();
|
||||||
}
|
}
|
||||||
Message::Redo => match self.active_tab() {
|
Message::Redo => match self.active_tab() {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
let mut editor = tab.editor.lock().unwrap();
|
let mut editor = tab.editor.lock().unwrap();
|
||||||
editor.redo();
|
editor.redo();
|
||||||
}
|
}
|
||||||
None => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Message::Save => {
|
Message::Save => {
|
||||||
let mut title_opt = None;
|
let mut title_opt = None;
|
||||||
|
|
||||||
match self.active_tab_mut() {
|
match self.active_tab_mut() {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
#[cfg(feature = "rfd")]
|
#[cfg(feature = "rfd")]
|
||||||
if tab.path_opt.is_none() {
|
if tab.path_opt.is_none() {
|
||||||
//TODO: use async file dialog
|
//TODO: use async file dialog
|
||||||
|
|
@ -1008,10 +1086,7 @@ impl Application for App {
|
||||||
title_opt = Some(tab.title());
|
title_opt = Some(tab.title());
|
||||||
tab.save();
|
tab.save();
|
||||||
}
|
}
|
||||||
None => {
|
_ => {}
|
||||||
//TODO: disable save button?
|
|
||||||
log::warn!("TODO: NO TAB OPEN");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(title) = title_opt {
|
if let Some(title) = title_opt {
|
||||||
|
|
@ -1020,7 +1095,7 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
Message::SelectAll => {
|
Message::SelectAll => {
|
||||||
match self.active_tab_mut() {
|
match self.active_tab_mut() {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
let mut editor = tab.editor.lock().unwrap();
|
let mut editor = tab.editor.lock().unwrap();
|
||||||
|
|
||||||
// Set cursor to lowest possible value
|
// Set cursor to lowest possible value
|
||||||
|
|
@ -1032,7 +1107,7 @@ impl Application for App {
|
||||||
let last_index = buffer.lines[last_line].text().len();
|
let last_index = buffer.lines[last_line].text().len();
|
||||||
editor.set_selection(Selection::Normal(Cursor::new(last_line, last_index)));
|
editor.set_selection(Selection::Normal(Cursor::new(last_line, last_index)));
|
||||||
}
|
}
|
||||||
None => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::SystemThemeModeChange(_theme_mode) => {
|
Message::SystemThemeModeChange(_theme_mode) => {
|
||||||
|
|
@ -1056,13 +1131,13 @@ impl Application for App {
|
||||||
return self.update_tab();
|
return self.update_tab();
|
||||||
}
|
}
|
||||||
Message::TabChanged(entity) => match self.tab_model.data::<Tab>(entity) {
|
Message::TabChanged(entity) => match self.tab_model.data::<Tab>(entity) {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
let mut title = tab.title();
|
let mut title = tab.title();
|
||||||
//TODO: better way of adding change indicator
|
//TODO: better way of adding change indicator
|
||||||
title.push_str(" \u{2022}");
|
title.push_str(" \u{2022}");
|
||||||
self.tab_model.text_set(entity, title);
|
self.tab_model.text_set(entity, title);
|
||||||
}
|
}
|
||||||
None => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Message::TabClose(entity) => {
|
Message::TabClose(entity) => {
|
||||||
// Activate closest item
|
// Activate closest item
|
||||||
|
|
@ -1086,30 +1161,30 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
Message::TabContextAction(entity, action) => {
|
Message::TabContextAction(entity, action) => {
|
||||||
match self.tab_model.data_mut::<Tab>(entity) {
|
match self.tab_model.data_mut::<Tab>(entity) {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
// Close context menu
|
// Close context menu
|
||||||
tab.context_menu = None;
|
tab.context_menu = None;
|
||||||
// Run action's message
|
// Run action's message
|
||||||
return self.update(action.message());
|
return self.update(action.message());
|
||||||
}
|
}
|
||||||
None => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::TabContextMenu(entity, position_opt) => {
|
Message::TabContextMenu(entity, position_opt) => {
|
||||||
match self.tab_model.data_mut::<Tab>(entity) {
|
match self.tab_model.data_mut::<Tab>(entity) {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
// Update context menu
|
// Update context menu
|
||||||
tab.context_menu = position_opt;
|
tab.context_menu = position_opt;
|
||||||
}
|
}
|
||||||
None => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::TabSetCursor(entity, cursor) => match self.tab_model.data::<Tab>(entity) {
|
Message::TabSetCursor(entity, cursor) => match self.tab_model.data::<Tab>(entity) {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
let mut editor = tab.editor.lock().unwrap();
|
let mut editor = tab.editor.lock().unwrap();
|
||||||
editor.set_cursor(cursor);
|
editor.set_cursor(cursor);
|
||||||
}
|
}
|
||||||
None => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Message::TabWidth(tab_width) => {
|
Message::TabWidth(tab_width) => {
|
||||||
self.config.tab_width = tab_width;
|
self.config.tab_width = tab_width;
|
||||||
|
|
@ -1131,10 +1206,52 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
self.set_context_title(context_page.title());
|
self.set_context_title(context_page.title());
|
||||||
|
|
||||||
// Ensure focus of correct input
|
// Execute commands for specific pages
|
||||||
if self.core.window.show_context {
|
if self.core.window.show_context {
|
||||||
match self.context_page {
|
match self.context_page {
|
||||||
|
ContextPage::GitManagement => {
|
||||||
|
self.git_project_status = None;
|
||||||
|
let projects = self.projects.clone();
|
||||||
|
return Command::perform(
|
||||||
|
async move {
|
||||||
|
let mut project_status = Vec::new();
|
||||||
|
for (project_name, project_path) in projects.iter() {
|
||||||
|
//TODO: send errors to UI
|
||||||
|
match GitRepository::new(&project_path) {
|
||||||
|
Ok(repo) => match repo.status().await {
|
||||||
|
Ok(status) => {
|
||||||
|
if !status.is_empty() {
|
||||||
|
project_status.push((
|
||||||
|
project_name.clone(),
|
||||||
|
project_path.clone(),
|
||||||
|
status,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(
|
||||||
|
"failed to get status of {:?}: {}",
|
||||||
|
project_path,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(
|
||||||
|
"failed to open repository {:?}: {}",
|
||||||
|
project_path,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message::app(Message::GitProjectStatus(project_status))
|
||||||
|
},
|
||||||
|
|x| x,
|
||||||
|
);
|
||||||
|
}
|
||||||
ContextPage::ProjectSearch => {
|
ContextPage::ProjectSearch => {
|
||||||
|
// Ensure focus of correct input
|
||||||
return widget::text_input::focus(self.project_search_id.clone());
|
return widget::text_input::focus(self.project_search_id.clone());
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -1147,7 +1264,7 @@ impl Application for App {
|
||||||
// This forces a redraw of all buffers
|
// This forces a redraw of all buffers
|
||||||
let entities: Vec<_> = self.tab_model.iter().collect();
|
let entities: Vec<_> = self.tab_model.iter().collect();
|
||||||
for entity in entities {
|
for entity in entities {
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
if let Some(Tab::Editor(tab)) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
let mut editor = tab.editor.lock().unwrap();
|
let mut editor = tab.editor.lock().unwrap();
|
||||||
editor.buffer_mut().set_redraw(true);
|
editor.buffer_mut().set_redraw(true);
|
||||||
}
|
}
|
||||||
|
|
@ -1160,11 +1277,11 @@ impl Application for App {
|
||||||
return self.save_config();
|
return self.save_config();
|
||||||
}
|
}
|
||||||
Message::Undo => match self.active_tab() {
|
Message::Undo => match self.active_tab() {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
let mut editor = tab.editor.lock().unwrap();
|
let mut editor = tab.editor.lock().unwrap();
|
||||||
editor.undo();
|
editor.undo();
|
||||||
}
|
}
|
||||||
None => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Message::VimBindings(vim_bindings) => {
|
Message::VimBindings(vim_bindings) => {
|
||||||
self.config.vim_bindings = vim_bindings;
|
self.config.vim_bindings = vim_bindings;
|
||||||
|
|
@ -1195,7 +1312,7 @@ impl Application for App {
|
||||||
let mut character_count_no_spaces = 0;
|
let mut character_count_no_spaces = 0;
|
||||||
let line_count;
|
let line_count;
|
||||||
match self.active_tab() {
|
match self.active_tab() {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
let editor = tab.editor.lock().unwrap();
|
let editor = tab.editor.lock().unwrap();
|
||||||
let buffer = editor.buffer();
|
let buffer = editor.buffer();
|
||||||
|
|
||||||
|
|
@ -1210,7 +1327,7 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
_ => {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1232,6 +1349,160 @@ impl Application for App {
|
||||||
.into()])
|
.into()])
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
ContextPage::GitManagement => {
|
||||||
|
if let Some(project_status) = &self.git_project_status {
|
||||||
|
let (success_color, destructive_color, warning_color) = {
|
||||||
|
let cosmic_theme = self.core().system_theme().cosmic();
|
||||||
|
(
|
||||||
|
cosmic_theme.success_color(),
|
||||||
|
cosmic_theme.destructive_color(),
|
||||||
|
cosmic_theme.warning_color(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let added =
|
||||||
|
|| widget::text("[+]").style(theme::Text::Color(success_color.into()));
|
||||||
|
let deleted =
|
||||||
|
|| widget::text("[-]").style(theme::Text::Color(destructive_color.into()));
|
||||||
|
let modified =
|
||||||
|
|| widget::text("[*]").style(theme::Text::Color(warning_color.into()));
|
||||||
|
|
||||||
|
let mut items = Vec::with_capacity(project_status.len().saturating_mul(3));
|
||||||
|
for (project_name, project_path, status) in project_status.iter() {
|
||||||
|
let mut unstaged_items = Vec::with_capacity(status.len());
|
||||||
|
let mut staged_items = Vec::with_capacity(status.len());
|
||||||
|
for item in status.iter() {
|
||||||
|
let relative_path = match item.path.strip_prefix(project_path) {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!(
|
||||||
|
"failed to find relative path of {:?} in project {:?}: {}",
|
||||||
|
item.path,
|
||||||
|
project_path,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
&item.path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = match &item.old_path {
|
||||||
|
Some(old_path) => {
|
||||||
|
let old_relative_path = match old_path
|
||||||
|
.strip_prefix(project_path)
|
||||||
|
{
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!(
|
||||||
|
"failed to find relative path of {:?} in project {:?}: {}",
|
||||||
|
old_path,
|
||||||
|
project_path,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
&old_path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"{} -> {}",
|
||||||
|
old_relative_path.display(),
|
||||||
|
relative_path.display()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => format!("{}", relative_path.display()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let unstaged_opt = match item.unstaged {
|
||||||
|
GitStatusKind::Unmodified => None,
|
||||||
|
GitStatusKind::Modified => Some(modified()),
|
||||||
|
GitStatusKind::FileTypeChanged => Some(modified()),
|
||||||
|
GitStatusKind::Added => Some(added()),
|
||||||
|
GitStatusKind::Deleted => Some(deleted()),
|
||||||
|
GitStatusKind::Renamed => Some(modified()), //TODO
|
||||||
|
GitStatusKind::Copied => Some(modified()), // TODO
|
||||||
|
GitStatusKind::Updated => Some(modified()),
|
||||||
|
GitStatusKind::Untracked => Some(added()),
|
||||||
|
GitStatusKind::SubmoduleModified => Some(modified()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(icon) = unstaged_opt {
|
||||||
|
unstaged_items.push(
|
||||||
|
widget::button(
|
||||||
|
widget::row::with_children(vec![
|
||||||
|
icon.into(),
|
||||||
|
widget::text(text.clone()).into(),
|
||||||
|
])
|
||||||
|
.spacing(space_xs),
|
||||||
|
)
|
||||||
|
.on_press(Message::PrepareGitDiff(
|
||||||
|
project_path.clone(),
|
||||||
|
item.path.clone(),
|
||||||
|
false,
|
||||||
|
))
|
||||||
|
.style(theme::Button::AppletMenu)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let staged_opt = match item.staged {
|
||||||
|
GitStatusKind::Unmodified => None,
|
||||||
|
GitStatusKind::Modified => Some(modified()),
|
||||||
|
GitStatusKind::FileTypeChanged => Some(modified()),
|
||||||
|
GitStatusKind::Added => Some(added()),
|
||||||
|
GitStatusKind::Deleted => Some(deleted()),
|
||||||
|
GitStatusKind::Renamed => Some(modified()), //TODO
|
||||||
|
GitStatusKind::Copied => Some(modified()), // TODO
|
||||||
|
GitStatusKind::Updated => Some(modified()),
|
||||||
|
GitStatusKind::Untracked => None,
|
||||||
|
GitStatusKind::SubmoduleModified => Some(modified()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(icon) = staged_opt {
|
||||||
|
staged_items.push(
|
||||||
|
widget::button(
|
||||||
|
widget::row::with_children(vec![
|
||||||
|
icon.into(),
|
||||||
|
widget::text(text.clone()).into(),
|
||||||
|
])
|
||||||
|
.spacing(space_xs),
|
||||||
|
)
|
||||||
|
.on_press(Message::PrepareGitDiff(
|
||||||
|
project_path.clone(),
|
||||||
|
item.path.clone(),
|
||||||
|
true,
|
||||||
|
))
|
||||||
|
.style(theme::Button::AppletMenu)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(widget::text::heading(project_name.clone()).into());
|
||||||
|
|
||||||
|
if !unstaged_items.is_empty() {
|
||||||
|
items.push(
|
||||||
|
widget::settings::view_section(fl!("unstaged-changes"))
|
||||||
|
.add(widget::column::with_children(unstaged_items))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !staged_items.is_empty() {
|
||||||
|
items.push(
|
||||||
|
widget::settings::view_section(fl!("staged-changes"))
|
||||||
|
.add(widget::column::with_children(staged_items))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
widget::column::with_children(items)
|
||||||
|
.spacing(space_s)
|
||||||
|
.padding([space_xxs, space_none])
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
widget::text("TODO (TRANSLATE): Loading git status...").into()
|
||||||
|
}
|
||||||
|
}
|
||||||
ContextPage::ProjectSearch => {
|
ContextPage::ProjectSearch => {
|
||||||
let search_input = widget::text_input::search_input(
|
let search_input = widget::text_input::search_input(
|
||||||
&fl!("project-search"),
|
&fl!("project-search"),
|
||||||
|
|
@ -1431,7 +1702,7 @@ impl Application for App {
|
||||||
|
|
||||||
let tab_id = self.tab_model.active();
|
let tab_id = self.tab_model.active();
|
||||||
match self.tab_model.data::<Tab>(tab_id) {
|
match self.tab_model.data::<Tab>(tab_id) {
|
||||||
Some(tab) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
let status = {
|
let status = {
|
||||||
let editor = tab.editor.lock().unwrap();
|
let editor = tab.editor.lock().unwrap();
|
||||||
let parser = editor.parser();
|
let parser = editor.parser();
|
||||||
|
|
@ -1486,10 +1757,62 @@ impl Application for App {
|
||||||
tab_column = tab_column.push(tab_element);
|
tab_column = tab_column.push(tab_element);
|
||||||
tab_column = tab_column.push(text(status).font(Font::MONOSPACE));
|
tab_column = tab_column.push(text(status).font(Font::MONOSPACE));
|
||||||
}
|
}
|
||||||
None => {
|
Some(Tab::GitDiff(tab)) => {
|
||||||
log::warn!("TODO: No tab open");
|
let mut diff_widget = widget::column::with_capacity(tab.diff.hunks.len());
|
||||||
|
for hunk in tab.diff.hunks.iter() {
|
||||||
|
let mut hunk_widget = widget::column::with_capacity(hunk.lines.len());
|
||||||
|
for line in hunk.lines.iter() {
|
||||||
|
let line_widget = match line {
|
||||||
|
GitDiffLine::Context {
|
||||||
|
old_line,
|
||||||
|
new_line,
|
||||||
|
text,
|
||||||
|
} => widget::container(widget::text::monotext(format!(
|
||||||
|
"{:4} {:4} {}",
|
||||||
|
old_line, new_line, text
|
||||||
|
))),
|
||||||
|
GitDiffLine::Added { new_line, text } => widget::container(
|
||||||
|
widget::text::monotext(format!(
|
||||||
|
"{:4} {:4} + {}",
|
||||||
|
"", new_line, text
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.style(theme::Container::Custom(Box::new(|_theme| {
|
||||||
|
//TODO: theme this color
|
||||||
|
widget::container::Appearance {
|
||||||
|
background: Some(Background::Color(Color::from_rgb8(
|
||||||
|
0x00, 0x40, 0x00,
|
||||||
|
))),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}))),
|
||||||
|
GitDiffLine::Deleted { old_line, text } => widget::container(
|
||||||
|
widget::text::monotext(format!(
|
||||||
|
"{:4} {:4} - {}",
|
||||||
|
old_line, "", text
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.style(theme::Container::Custom(Box::new(|_theme| {
|
||||||
|
//TODO: theme this color
|
||||||
|
widget::container::Appearance {
|
||||||
|
background: Some(Background::Color(Color::from_rgb8(
|
||||||
|
0x40, 0x00, 0x00,
|
||||||
|
))),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}))),
|
||||||
|
};
|
||||||
|
hunk_widget = hunk_widget.push(line_widget.width(Length::Fill));
|
||||||
|
}
|
||||||
|
diff_widget = diff_widget.push(hunk_widget);
|
||||||
|
}
|
||||||
|
tab_column = tab_column.push(widget::scrollable(
|
||||||
|
widget::cosmic_container::container(diff_widget)
|
||||||
|
.layer(cosmic_theme::Layer::Primary),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
let content: Element<_> = tab_column.into();
|
let content: Element<_> = tab_column.into();
|
||||||
|
|
||||||
|
|
@ -1517,6 +1840,7 @@ impl Application for App {
|
||||||
|mut output| async move {
|
|mut output| async move {
|
||||||
let watcher_res = {
|
let watcher_res = {
|
||||||
let mut output = output.clone();
|
let mut output = output.clone();
|
||||||
|
//TODO: debounce
|
||||||
notify::recommended_watcher(
|
notify::recommended_watcher(
|
||||||
move |event_res: Result<notify::Event, notify::Error>| match event_res {
|
move |event_res: Result<notify::Event, notify::Error>| match event_res {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,10 @@ pub fn menu_bar<'a>(config: &Config) -> Element<'a, Message> {
|
||||||
),
|
),
|
||||||
menu_item(fl!("document-type"), Message::Todo),
|
menu_item(fl!("document-type"), Message::Todo),
|
||||||
menu_item(fl!("encoding"), Message::Todo),
|
menu_item(fl!("encoding"), Message::Todo),
|
||||||
|
menu_item(
|
||||||
|
fl!("menu-git-management"),
|
||||||
|
Message::ToggleContextPage(ContextPage::GitManagement),
|
||||||
|
),
|
||||||
menu_item(fl!("print"), Message::Todo),
|
menu_item(fl!("print"), Message::Todo),
|
||||||
MenuTree::new(horizontal_rule(1)),
|
MenuTree::new(horizontal_rule(1)),
|
||||||
menu_item(fl!("quit"), Message::Quit),
|
menu_item(fl!("quit"), Message::Quit),
|
||||||
|
|
|
||||||
|
|
@ -27,14 +27,14 @@ pub struct ProjectSearchResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectSearchResult {
|
impl ProjectSearchResult {
|
||||||
pub fn search_projects(&mut self, project_paths: Vec<PathBuf>) {
|
pub fn search_projects(&mut self, projects: Vec<(String, PathBuf)>) {
|
||||||
//TODO: support literal search
|
//TODO: support literal search
|
||||||
//TODO: use ignore::WalkParallel?
|
//TODO: use ignore::WalkParallel?
|
||||||
match RegexMatcher::new(&self.value) {
|
match RegexMatcher::new(&self.value) {
|
||||||
Ok(matcher) => {
|
Ok(matcher) => {
|
||||||
let mut searcher = Searcher::new();
|
let mut searcher = Searcher::new();
|
||||||
let mut walk_builder_opt: Option<ignore::WalkBuilder> = None;
|
let mut walk_builder_opt: Option<ignore::WalkBuilder> = None;
|
||||||
for project_path in project_paths.iter() {
|
for (_, project_path) in projects.iter() {
|
||||||
walk_builder_opt = match walk_builder_opt.take() {
|
walk_builder_opt = match walk_builder_opt.take() {
|
||||||
Some(mut walk_builder) => {
|
Some(mut walk_builder) => {
|
||||||
walk_builder.add(project_path);
|
walk_builder.add(project_path);
|
||||||
|
|
@ -49,7 +49,7 @@ impl ProjectSearchResult {
|
||||||
let entry = match entry_res {
|
let entry = match entry_res {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("failed to walk projects {:?}: {}", project_paths, err);
|
log::error!("failed to walk projects {:?}: {}", projects, err);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
25
src/tab.rs
25
src/tab.rs
|
|
@ -8,16 +8,35 @@ use cosmic_text::{Attrs, Buffer, Edit, Shaping, SyntaxEditor, ViEditor, Wrap};
|
||||||
use notify::Watcher;
|
use notify::Watcher;
|
||||||
use std::{fs, path::PathBuf, sync::Mutex};
|
use std::{fs, path::PathBuf, sync::Mutex};
|
||||||
|
|
||||||
use crate::{fl, mime_icon, Config, FALLBACK_MIME_ICON, FONT_SYSTEM, SYNTAX_SYSTEM};
|
use crate::{fl, git::GitDiff, mime_icon, Config, FALLBACK_MIME_ICON, FONT_SYSTEM, SYNTAX_SYSTEM};
|
||||||
|
|
||||||
pub struct Tab {
|
pub enum Tab {
|
||||||
|
Editor(EditorTab),
|
||||||
|
GitDiff(GitDiffTab),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tab {
|
||||||
|
pub fn title(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Editor(tab) => tab.title(),
|
||||||
|
Self::GitDiff(tab) => tab.title.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GitDiffTab {
|
||||||
|
pub title: String,
|
||||||
|
pub diff: GitDiff,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EditorTab {
|
||||||
pub path_opt: Option<PathBuf>,
|
pub path_opt: Option<PathBuf>,
|
||||||
attrs: Attrs<'static>,
|
attrs: Attrs<'static>,
|
||||||
pub editor: Mutex<ViEditor<'static>>,
|
pub editor: Mutex<ViEditor<'static>>,
|
||||||
pub context_menu: Option<Point>,
|
pub context_menu: Option<Point>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tab {
|
impl EditorTab {
|
||||||
pub fn new(config: &Config) -> Self {
|
pub fn new(config: &Config) -> Self {
|
||||||
//TODO: do not repeat, used in App::init
|
//TODO: do not repeat, used in App::init
|
||||||
let attrs = cosmic_text::Attrs::new().family(cosmic_text::Family::Monospace);
|
let attrs = cosmic_text::Attrs::new().family(cosmic_text::Family::Monospace);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue