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"
|
||||
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
|
|
@ -647,6 +653,12 @@ version = "3.14.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.14.0"
|
||||
|
|
@ -751,6 +763,20 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "clipboard-win"
|
||||
version = "4.5.0"
|
||||
|
|
@ -1001,6 +1027,7 @@ dependencies = [
|
|||
"libcosmic",
|
||||
"log",
|
||||
"notify",
|
||||
"patch",
|
||||
"rfd",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
|
|
@ -2539,6 +2566,29 @@ dependencies = [
|
|||
"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]]
|
||||
name = "iced"
|
||||
version = "0.10.0"
|
||||
|
|
@ -3621,6 +3671,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "notify"
|
||||
version = "6.1.1"
|
||||
|
|
@ -4064,6 +4125,17 @@ version = "1.0.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
|
|
@ -6255,6 +6327,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "windows-implement"
|
||||
version = "0.44.0"
|
||||
|
|
|
|||
|
|
@ -11,11 +11,12 @@ grep = "0.3.1"
|
|||
ignore = "0.4.21"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.20"
|
||||
patch = "0.7.0"
|
||||
notify = "6.1.1"
|
||||
#TODO: this is using gtk for file dialogues
|
||||
rfd = { version = "0.12.0", optional = true }
|
||||
serde = { version = "1", features = ["serde_derive"] }
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
tokio = { version = "1", features = ["process", "time"] }
|
||||
# Extra syntax highlighting
|
||||
syntect = "5.1.0"
|
||||
two-face = "0.3.0"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ character-count = Characters
|
|||
character-count-no-spaces = Characters (without spaces)
|
||||
line-count = Lines
|
||||
|
||||
## Git management
|
||||
git-management = Git management
|
||||
unstaged-changes = Unstaged changes
|
||||
staged-changes = Staged changes
|
||||
|
||||
## Project search
|
||||
project-search = Project search
|
||||
|
||||
|
|
@ -50,6 +55,7 @@ revert-all-changes = Revert all changes
|
|||
menu-document-statistics = Document statistics...
|
||||
document-type = Document type...
|
||||
encoding = Encoding...
|
||||
menu-git-management = Git management...
|
||||
print = Print
|
||||
quit = Quit
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ pub enum Action {
|
|||
Redo,
|
||||
Save,
|
||||
SelectAll,
|
||||
ToggleGitManagement,
|
||||
ToggleProjectSearch,
|
||||
ToggleSettingsPage,
|
||||
ToggleWordWrap,
|
||||
|
|
@ -50,6 +51,7 @@ impl Action {
|
|||
Self::Redo => Message::Redo,
|
||||
Self::Save => Message::Save,
|
||||
Self::SelectAll => Message::SelectAll,
|
||||
Self::ToggleGitManagement => Message::ToggleContextPage(ContextPage::GitManagement),
|
||||
Self::ToggleProjectSearch => Message::ToggleContextPage(ContextPage::ProjectSearch),
|
||||
Self::ToggleSettingsPage => Message::ToggleContextPage(ContextPage::Settings),
|
||||
Self::ToggleWordWrap => Message::ToggleWordWrap,
|
||||
|
|
@ -118,6 +120,7 @@ impl KeyBind {
|
|||
bind!([Ctrl, Shift], Z, Redo);
|
||||
bind!([Ctrl], S, Save);
|
||||
bind!([Ctrl], A, SelectAll);
|
||||
bind!([Ctrl, Shift], G, ToggleGitManagement);
|
||||
bind!([Ctrl, Shift], F, ToggleProjectSearch);
|
||||
bind!([Ctrl], Comma, ToggleSettingsPage);
|
||||
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},
|
||||
keyboard, subscription,
|
||||
widget::{row, text},
|
||||
window, Alignment, Length, Point,
|
||||
window, Alignment, Background, Color, Length, Point,
|
||||
},
|
||||
style, theme,
|
||||
widget::{self, button, icon, nav_bar, segmented_button, view_switcher},
|
||||
|
|
@ -29,6 +29,9 @@ use tokio::time;
|
|||
use config::{Action, AppTheme, Config, CONFIG_VERSION};
|
||||
mod config;
|
||||
|
||||
use git::{GitDiff, GitDiffLine, GitRepository, GitStatus, GitStatusKind};
|
||||
mod git;
|
||||
|
||||
use icon_cache::IconCache;
|
||||
mod icon_cache;
|
||||
|
||||
|
|
@ -49,7 +52,7 @@ mod project;
|
|||
use self::search::ProjectSearchResult;
|
||||
mod search;
|
||||
|
||||
use self::tab::Tab;
|
||||
use self::tab::{EditorTab, GitDiffTab, Tab};
|
||||
mod tab;
|
||||
|
||||
use self::text_box::text_box;
|
||||
|
|
@ -163,6 +166,7 @@ pub enum Message {
|
|||
Cut,
|
||||
DefaultFont(usize),
|
||||
DefaultFontSize(usize),
|
||||
GitProjectStatus(Vec<(String, PathBuf, Vec<GitStatus>)>),
|
||||
Key(keyboard::Modifiers, keyboard::KeyCode),
|
||||
NewFile,
|
||||
NewWindow,
|
||||
|
|
@ -170,11 +174,13 @@ pub enum Message {
|
|||
NotifyWatcher(WatcherWrapper),
|
||||
OpenFileDialog,
|
||||
OpenFile(PathBuf),
|
||||
OpenGitDiff(PathBuf, GitDiff),
|
||||
OpenProjectDialog,
|
||||
OpenProject(PathBuf),
|
||||
OpenSearchResult(usize, usize),
|
||||
Paste,
|
||||
PasteValue(String),
|
||||
PrepareGitDiff(PathBuf, PathBuf, bool),
|
||||
ProjectSearchResult(ProjectSearchResult),
|
||||
ProjectSearchSubmit,
|
||||
ProjectSearchValue(String),
|
||||
|
|
@ -203,6 +209,7 @@ pub enum Message {
|
|||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ContextPage {
|
||||
DocumentStatistics,
|
||||
GitManagement,
|
||||
//TODO: Move search to pop-up
|
||||
ProjectSearch,
|
||||
Settings,
|
||||
|
|
@ -212,6 +219,7 @@ impl ContextPage {
|
|||
fn title(&self) -> String {
|
||||
match self {
|
||||
Self::DocumentStatistics => fl!("document-statistics"),
|
||||
Self::GitManagement => fl!("git-management"),
|
||||
Self::ProjectSearch => fl!("project-search"),
|
||||
Self::Settings => fl!("settings"),
|
||||
}
|
||||
|
|
@ -230,6 +238,8 @@ pub struct App {
|
|||
font_sizes: Vec<u16>,
|
||||
theme_names: Vec<String>,
|
||||
context_page: ContextPage,
|
||||
git_project_status: Option<Vec<(String, PathBuf, Vec<GitStatus>)>>,
|
||||
projects: Vec<(String, PathBuf)>,
|
||||
project_search_id: widget::Id,
|
||||
project_search_value: String,
|
||||
project_search_result: Option<ProjectSearchResult>,
|
||||
|
|
@ -300,25 +310,31 @@ impl App {
|
|||
}
|
||||
|
||||
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) => {
|
||||
match &mut node {
|
||||
ProjectNode::Folder { open, root, .. } => {
|
||||
ProjectNode::Folder {
|
||||
name,
|
||||
path,
|
||||
open,
|
||||
root,
|
||||
} => {
|
||||
*open = true;
|
||||
*root = true;
|
||||
|
||||
// Save the absolute path
|
||||
self.projects.push((name.to_string(), path.to_path_buf()));
|
||||
}
|
||||
_ => {
|
||||
log::error!(
|
||||
"failed to open project {:?}: not a directory",
|
||||
path.as_ref()
|
||||
);
|
||||
log::error!("failed to open project {:?}: not a directory", path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
node
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to open project {:?}: {}", path.as_ref(), err);
|
||||
log::error!("failed to open project {:?}: {}", path, err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
|
@ -351,13 +367,13 @@ impl App {
|
|||
let mut activate_opt = None;
|
||||
for entity in self.tab_model.iter() {
|
||||
match self.tab_model.data::<Tab>(entity) {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
if tab.path_opt.as_ref() == Some(&canonical) {
|
||||
activate_opt = Some(entity);
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(entity) = activate_opt {
|
||||
|
|
@ -365,12 +381,12 @@ impl App {
|
|||
return Some(entity);
|
||||
}
|
||||
|
||||
let mut tab = Tab::new(&self.config);
|
||||
let mut tab = EditorTab::new(&self.config);
|
||||
tab.open(canonical);
|
||||
tab.watch(&mut self.watcher_opt);
|
||||
tab
|
||||
}
|
||||
None => Tab::new(&self.config),
|
||||
None => EditorTab::new(&self.config),
|
||||
};
|
||||
|
||||
Some(
|
||||
|
|
@ -378,7 +394,7 @@ impl App {
|
|||
.insert()
|
||||
.text(tab.title())
|
||||
.icon(tab.icon(16))
|
||||
.data::<Tab>(tab)
|
||||
.data::<Tab>(Tab::Editor(tab))
|
||||
.closable()
|
||||
.activate()
|
||||
.id(),
|
||||
|
|
@ -389,7 +405,7 @@ impl App {
|
|||
//TODO: provide iterator over data
|
||||
let entities: Vec<_> = self.tab_model.iter().collect();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -411,7 +427,8 @@ impl App {
|
|||
|
||||
fn update_nav_bar_active(&mut self) {
|
||||
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,
|
||||
};
|
||||
|
||||
|
|
@ -464,8 +481,10 @@ impl App {
|
|||
|
||||
let title = match self.active_tab() {
|
||||
Some(tab) => {
|
||||
// Force redraw on tab switches
|
||||
tab.editor.lock().unwrap().buffer_mut().set_redraw(true);
|
||||
if let Tab::Editor(inner) = tab {
|
||||
// Force redraw on tab switches
|
||||
inner.editor.lock().unwrap().buffer_mut().set_redraw(true);
|
||||
}
|
||||
tab.title()
|
||||
}
|
||||
None => format!("No Open File"),
|
||||
|
|
@ -554,6 +573,8 @@ impl Application for App {
|
|||
font_sizes,
|
||||
theme_names,
|
||||
context_page: ContextPage::Settings,
|
||||
git_project_status: None,
|
||||
projects: Vec::new(),
|
||||
project_search_id: widget::Id::unique(),
|
||||
project_search_value: String::new(),
|
||||
project_search_result: None,
|
||||
|
|
@ -709,17 +730,17 @@ impl Application for App {
|
|||
log::info!("TODO");
|
||||
}
|
||||
Message::Copy => match self.active_tab() {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
let editor = tab.editor.lock().unwrap();
|
||||
let selection_opt = editor.copy_selection();
|
||||
if let Some(selection) = selection_opt {
|
||||
return clipboard::write(selection);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
_ => {}
|
||||
},
|
||||
Message::Cut => match self.active_tab() {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
let mut editor = tab.editor.lock().unwrap();
|
||||
let selection_opt = editor.copy_selection();
|
||||
editor.delete_selection();
|
||||
|
|
@ -727,7 +748,7 @@ impl Application for App {
|
|||
return clipboard::write(selection);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
_ => {}
|
||||
},
|
||||
Message::DefaultFont(index) => {
|
||||
match self.font_names.get(index) {
|
||||
|
|
@ -748,7 +769,9 @@ impl Application for App {
|
|||
// This does a complete reset of shaping data!
|
||||
let entities: Vec<_> = self.tab_model.iter().collect();
|
||||
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();
|
||||
for line in editor.buffer_mut().lines.iter_mut() {
|
||||
line.reset();
|
||||
|
|
@ -774,6 +797,9 @@ impl Application for App {
|
|||
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) => {
|
||||
for (key_bind, action) in self.config.keybinds.iter() {
|
||||
if key_bind.matches(modifiers, key_code) {
|
||||
|
|
@ -803,7 +829,7 @@ impl Application for App {
|
|||
let mut needs_reload = Vec::new();
|
||||
for entity in self.tab_model.iter() {
|
||||
match self.tab_model.data::<Tab>(entity) {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
if let Some(path) = &tab.path_opt {
|
||||
if event.paths.contains(&path) {
|
||||
if tab.changed() {
|
||||
|
|
@ -817,16 +843,16 @@ impl Application for App {
|
|||
}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
for entity in needs_reload {
|
||||
match self.tab_model.data_mut::<Tab>(entity) {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
tab.reload();
|
||||
}
|
||||
None => {
|
||||
_ => {
|
||||
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() {
|
||||
match self.tab_model.data::<Tab>(entity) {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
tab.watch(&mut self.watcher_opt);
|
||||
}
|
||||
None => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -867,6 +893,39 @@ impl Application for App {
|
|||
self.open_tab(Some(path));
|
||||
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 => {
|
||||
#[cfg(feature = "rfd")]
|
||||
return Command::perform(
|
||||
|
|
@ -927,12 +986,43 @@ impl Application for App {
|
|||
});
|
||||
}
|
||||
Message::PasteValue(value) => match self.active_tab() {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
let mut editor = tab.editor.lock().unwrap();
|
||||
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) => {
|
||||
self.project_search_result = Some(project_search_result);
|
||||
|
||||
|
|
@ -942,19 +1032,7 @@ impl Application for App {
|
|||
Message::ProjectSearchSubmit => {
|
||||
//TODO: Figure out length requirements?
|
||||
if !self.project_search_value.is_empty() {
|
||||
//TODO: cache projects outside of nav model?
|
||||
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 projects = self.projects.clone();
|
||||
let project_search_value = self.project_search_value.clone();
|
||||
let mut project_search_result = ProjectSearchResult {
|
||||
value: project_search_value.clone(),
|
||||
|
|
@ -965,7 +1043,7 @@ impl Application for App {
|
|||
return Command::perform(
|
||||
async 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))
|
||||
})
|
||||
.await;
|
||||
|
|
@ -989,17 +1067,17 @@ impl Application for App {
|
|||
return window::close();
|
||||
}
|
||||
Message::Redo => match self.active_tab() {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
let mut editor = tab.editor.lock().unwrap();
|
||||
editor.redo();
|
||||
}
|
||||
None => {}
|
||||
_ => {}
|
||||
},
|
||||
Message::Save => {
|
||||
let mut title_opt = None;
|
||||
|
||||
match self.active_tab_mut() {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
#[cfg(feature = "rfd")]
|
||||
if tab.path_opt.is_none() {
|
||||
//TODO: use async file dialog
|
||||
|
|
@ -1008,10 +1086,7 @@ impl Application for App {
|
|||
title_opt = Some(tab.title());
|
||||
tab.save();
|
||||
}
|
||||
None => {
|
||||
//TODO: disable save button?
|
||||
log::warn!("TODO: NO TAB OPEN");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(title) = title_opt {
|
||||
|
|
@ -1020,7 +1095,7 @@ impl Application for App {
|
|||
}
|
||||
Message::SelectAll => {
|
||||
match self.active_tab_mut() {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
let mut editor = tab.editor.lock().unwrap();
|
||||
|
||||
// Set cursor to lowest possible value
|
||||
|
|
@ -1032,7 +1107,7 @@ impl Application for App {
|
|||
let last_index = buffer.lines[last_line].text().len();
|
||||
editor.set_selection(Selection::Normal(Cursor::new(last_line, last_index)));
|
||||
}
|
||||
None => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Message::SystemThemeModeChange(_theme_mode) => {
|
||||
|
|
@ -1056,13 +1131,13 @@ impl Application for App {
|
|||
return self.update_tab();
|
||||
}
|
||||
Message::TabChanged(entity) => match self.tab_model.data::<Tab>(entity) {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
let mut title = tab.title();
|
||||
//TODO: better way of adding change indicator
|
||||
title.push_str(" \u{2022}");
|
||||
self.tab_model.text_set(entity, title);
|
||||
}
|
||||
None => {}
|
||||
_ => {}
|
||||
},
|
||||
Message::TabClose(entity) => {
|
||||
// Activate closest item
|
||||
|
|
@ -1086,30 +1161,30 @@ impl Application for App {
|
|||
}
|
||||
Message::TabContextAction(entity, action) => {
|
||||
match self.tab_model.data_mut::<Tab>(entity) {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
// Close context menu
|
||||
tab.context_menu = None;
|
||||
// Run action's message
|
||||
return self.update(action.message());
|
||||
}
|
||||
None => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Message::TabContextMenu(entity, position_opt) => {
|
||||
match self.tab_model.data_mut::<Tab>(entity) {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
// Update context menu
|
||||
tab.context_menu = position_opt;
|
||||
}
|
||||
None => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Message::TabSetCursor(entity, cursor) => match self.tab_model.data::<Tab>(entity) {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
let mut editor = tab.editor.lock().unwrap();
|
||||
editor.set_cursor(cursor);
|
||||
}
|
||||
None => {}
|
||||
_ => {}
|
||||
},
|
||||
Message::TabWidth(tab_width) => {
|
||||
self.config.tab_width = tab_width;
|
||||
|
|
@ -1131,10 +1206,52 @@ impl Application for App {
|
|||
}
|
||||
self.set_context_title(context_page.title());
|
||||
|
||||
// Ensure focus of correct input
|
||||
// Execute commands for specific pages
|
||||
if self.core.window.show_context {
|
||||
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 => {
|
||||
// Ensure focus of correct input
|
||||
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
|
||||
let entities: Vec<_> = self.tab_model.iter().collect();
|
||||
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();
|
||||
editor.buffer_mut().set_redraw(true);
|
||||
}
|
||||
|
|
@ -1160,11 +1277,11 @@ impl Application for App {
|
|||
return self.save_config();
|
||||
}
|
||||
Message::Undo => match self.active_tab() {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
let mut editor = tab.editor.lock().unwrap();
|
||||
editor.undo();
|
||||
}
|
||||
None => {}
|
||||
_ => {}
|
||||
},
|
||||
Message::VimBindings(vim_bindings) => {
|
||||
self.config.vim_bindings = vim_bindings;
|
||||
|
|
@ -1195,7 +1312,7 @@ impl Application for App {
|
|||
let mut character_count_no_spaces = 0;
|
||||
let line_count;
|
||||
match self.active_tab() {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
let editor = tab.editor.lock().unwrap();
|
||||
let buffer = editor.buffer();
|
||||
|
||||
|
|
@ -1210,7 +1327,7 @@ impl Application for App {
|
|||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
|
@ -1232,6 +1349,160 @@ impl Application for App {
|
|||
.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 => {
|
||||
let search_input = widget::text_input::search_input(
|
||||
&fl!("project-search"),
|
||||
|
|
@ -1431,7 +1702,7 @@ impl Application for App {
|
|||
|
||||
let tab_id = self.tab_model.active();
|
||||
match self.tab_model.data::<Tab>(tab_id) {
|
||||
Some(tab) => {
|
||||
Some(Tab::Editor(tab)) => {
|
||||
let status = {
|
||||
let editor = tab.editor.lock().unwrap();
|
||||
let parser = editor.parser();
|
||||
|
|
@ -1486,10 +1757,62 @@ impl Application for App {
|
|||
tab_column = tab_column.push(tab_element);
|
||||
tab_column = tab_column.push(text(status).font(Font::MONOSPACE));
|
||||
}
|
||||
None => {
|
||||
log::warn!("TODO: No tab open");
|
||||
Some(Tab::GitDiff(tab)) => {
|
||||
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();
|
||||
|
||||
|
|
@ -1517,6 +1840,7 @@ impl Application for App {
|
|||
|mut output| async move {
|
||||
let watcher_res = {
|
||||
let mut output = output.clone();
|
||||
//TODO: debounce
|
||||
notify::recommended_watcher(
|
||||
move |event_res: Result<notify::Event, notify::Error>| match event_res {
|
||||
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!("encoding"), Message::Todo),
|
||||
menu_item(
|
||||
fl!("menu-git-management"),
|
||||
Message::ToggleContextPage(ContextPage::GitManagement),
|
||||
),
|
||||
menu_item(fl!("print"), Message::Todo),
|
||||
MenuTree::new(horizontal_rule(1)),
|
||||
menu_item(fl!("quit"), Message::Quit),
|
||||
|
|
|
|||
|
|
@ -27,14 +27,14 @@ pub struct 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: use ignore::WalkParallel?
|
||||
match RegexMatcher::new(&self.value) {
|
||||
Ok(matcher) => {
|
||||
let mut searcher = Searcher::new();
|
||||
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() {
|
||||
Some(mut walk_builder) => {
|
||||
walk_builder.add(project_path);
|
||||
|
|
@ -49,7 +49,7 @@ impl ProjectSearchResult {
|
|||
let entry = match entry_res {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
log::error!("failed to walk projects {:?}: {}", project_paths, err);
|
||||
log::error!("failed to walk projects {:?}: {}", projects, err);
|
||||
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 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>,
|
||||
attrs: Attrs<'static>,
|
||||
pub editor: Mutex<ViEditor<'static>>,
|
||||
pub context_menu: Option<Point>,
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
impl EditorTab {
|
||||
pub fn new(config: &Config) -> Self {
|
||||
//TODO: do not repeat, used in App::init
|
||||
let attrs = cosmic_text::Attrs::new().family(cosmic_text::Family::Monospace);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue