diff --git a/core/src/buffer.rs b/core/src/buffer.rs index 45b96ed194..586aabf16f 100644 --- a/core/src/buffer.rs +++ b/core/src/buffer.rs @@ -391,7 +391,7 @@ impl BufferNew { let event_sink = self.event_sink.clone(); let tab_id = self.tab_id; let version = version.to_string(); - rayon::spawn_fifo(move || { + rayon::spawn(move || { let mut highlight_config = highlight_config.lock(); let mut highlighter = highlighter.lock(); let highlights = rope_styles( diff --git a/core/src/command.rs b/core/src/command.rs index 997bdf2a02..dc58923039 100644 --- a/core/src/command.rs +++ b/core/src/command.rs @@ -5,7 +5,9 @@ use anyhow::Result; use druid::{Point, Rect, Selector, Size, WidgetId}; use indexmap::IndexMap; use lapce_proxy::{ - dispatch::FileNodeItem, plugin::PluginDescription, terminal::TermId, + dispatch::{FileDiff, FileNodeItem}, + plugin::PluginDescription, + terminal::TermId, }; use lsp_types::{ CodeActionResponse, CompletionItem, CompletionResponse, Location, Position, @@ -181,6 +183,9 @@ pub enum LapceWorkbenchCommand { #[strum(serialize = "focus_terminal")] FocusTerminal, + + #[strum(serialize = "source_control_commit")] + SourceControlCommit, } #[derive(Display, EnumString, EnumIter, Clone, PartialEq, Debug, EnumMessage)] @@ -501,7 +506,7 @@ pub enum LapceUICommand { UpdateLineChanges(BufferId), PublishDiagnostics(PublishDiagnosticsParams), WorkDoneProgress(ProgressParams), - UpdateDiffFiles(Vec), + UpdateFileDiffs(Vec), ReloadBuffer(BufferId, u64, String), EnsureVisible((Rect, (f64, f64), Option)), EnsureRectVisible(Rect), diff --git a/core/src/data.rs b/core/src/data.rs index fe33c82827..97f5e9869e 100644 --- a/core/src/data.rs +++ b/core/src/data.rs @@ -26,7 +26,7 @@ use druid::{ }; use im::{self, hashmap}; use itertools::Itertools; -use lapce_proxy::{plugin::PluginDescription, terminal::TermId}; +use lapce_proxy::{dispatch::FileDiff, plugin::PluginDescription, terminal::TermId}; use lsp_types::{ CodeActionOrCommand, CodeActionResponse, CompletionItem, CompletionResponse, CompletionTextEdit, Diagnostic, DiagnosticSeverity, GotoDefinitionResponse, @@ -1007,6 +1007,47 @@ impl LapceTabData { } } } + LapceWorkbenchCommand::SourceControlCommit => { + let diffs: Vec = self + .source_control + .file_diffs + .iter() + .filter_map( + |(diff, checked)| { + if *checked { + Some(diff.clone()) + } else { + None + } + }, + ) + .collect(); + if diffs.len() == 0 { + return; + } + let buffer = self + .main_split + .local_buffers + .get_mut(&LocalBufferKind::SourceControl) + .unwrap(); + let message = buffer.rope.to_string(); + let message = message.trim(); + if message == "" { + return; + } + self.proxy.git_commit(message, diffs); + Arc::make_mut(buffer).load_content(""); + let editor = self + .main_split + .editors + .get_mut(&self.source_control.editor_view_id) + .unwrap(); + Arc::make_mut(editor).cursor = if self.config.lapce.modal { + Cursor::new(CursorMode::Normal(0), None) + } else { + Cursor::new(CursorMode::Insert(Selection::caret(0)), None) + }; + } } } diff --git a/core/src/editor.rs b/core/src/editor.rs index 53068d001b..05ac3d76ca 100644 --- a/core/src/editor.rs +++ b/core/src/editor.rs @@ -984,14 +984,15 @@ impl LapceEditorBufferData { fn next_diff(&mut self, ctx: &mut EventCtx, env: &Env) { if let BufferContent::File(buffer_path) = &self.buffer.content { - if self.source_control.diff_files.len() == 0 { + if self.source_control.file_diffs.len() == 0 { return; } let mut diff_files: Vec<(PathBuf, Vec)> = self .source_control - .diff_files + .file_diffs .iter() - .map(|(path, _)| { + .map(|(diff, _)| { + let path = diff.path(); let mut positions = Vec::new(); if let Some(buffer) = self.main_split.open_files.get(path) { if let Some(changes) = buffer.history_changes.get("head") { diff --git a/core/src/proxy.rs b/core/src/proxy.rs index a537b11f0d..3280111a60 100644 --- a/core/src/proxy.rs +++ b/core/src/proxy.rs @@ -12,6 +12,7 @@ use crossbeam_utils::sync::WaitGroup; use druid::{ExtEventSink, WidgetId}; use druid::{Target, WindowId}; use flate2::read::GzDecoder; +use lapce_proxy::dispatch::FileDiff; use lapce_proxy::dispatch::{FileNodeItem, NewBufferResponse}; use lapce_proxy::plugin::PluginDescription; use lapce_proxy::terminal::TermId; @@ -269,6 +270,16 @@ impl LapceProxy { ) } + pub fn git_commit(&self, message: &str, diffs: Vec) { + self.peer.lock().as_ref().unwrap().send_rpc_notification( + "git_commit", + &json!({ + "message": message, + "diffs": diffs, + }), + ) + } + pub fn install_plugin(&self, plugin: &PluginDescription) { self.peer .lock() @@ -532,6 +543,9 @@ pub enum Notification { DiffFiles { files: Vec, }, + FileDiffs { + diffs: Vec, + }, UpdateTerminal { term_id: TermId, content: String, @@ -607,10 +621,11 @@ impl Handler for ProxyHandlerNew { ); } Notification::ListDir { items } => {} - Notification::DiffFiles { files } => { + Notification::DiffFiles { files } => {} + Notification::FileDiffs { diffs } => { self.event_sink.submit_command( LAPCE_UI_COMMAND, - LapceUICommand::UpdateDiffFiles(files), + LapceUICommand::UpdateFileDiffs(diffs), Target::Widget(self.tab_id), ); } diff --git a/core/src/source_control.rs b/core/src/source_control.rs index 82ae1ae346..9793901cfa 100644 --- a/core/src/source_control.rs +++ b/core/src/source_control.rs @@ -13,6 +13,7 @@ use druid::{ Rect, RenderContext, Size, Target, TextLayout, UpdateCtx, Widget, WidgetExt, WidgetId, WidgetPod, WindowId, }; +use lapce_proxy::dispatch::FileDiff; use crate::{ command::{ @@ -29,7 +30,7 @@ use crate::{ scroll::LapceScrollNew, split::{LapceSplitNew, SplitDirection, SplitMoveDirection}, state::Mode, - svg::file_svg_new, + svg::{file_svg_new, get_svg}, theme::OldLapceTheme, }; @@ -45,7 +46,7 @@ pub struct SourceControlData { pub file_list_id: WidgetId, pub file_list_index: usize, pub editor_view_id: WidgetId, - pub diff_files: Vec<(PathBuf, bool)>, + pub file_diffs: Vec<(FileDiff, bool)>, } impl SourceControlData { @@ -60,7 +61,7 @@ impl SourceControlData { file_list_index: 0, split_id: WidgetId::next(), split_direction: SplitDirection::Horizontal, - diff_files: Vec::new(), + file_diffs: Vec::new(), } } @@ -141,7 +142,7 @@ impl KeyPressFocus for SourceControlData { LapceCommand::Up | LapceCommand::ListPrevious => { self.file_list_index = Movement::Up.update_index( self.file_list_index, - self.diff_files.len(), + self.file_diffs.len(), 1, true, ); @@ -149,23 +150,23 @@ impl KeyPressFocus for SourceControlData { LapceCommand::Down | LapceCommand::ListNext => { self.file_list_index = Movement::Down.update_index( self.file_list_index, - self.diff_files.len(), + self.file_diffs.len(), 1, true, ); } LapceCommand::ListExpand => { - if self.diff_files.len() > 0 { - self.diff_files[self.file_list_index].1 = - !self.diff_files[self.file_list_index].1; + if self.file_diffs.len() > 0 { + self.file_diffs[self.file_list_index].1 = + !self.file_diffs[self.file_list_index].1; } } LapceCommand::ListSelect => { - if self.diff_files.len() > 0 { + if self.file_diffs.len() > 0 { ctx.submit_command(Command::new( LAPCE_UI_COMMAND, LapceUICommand::OpenFileDiff( - self.diff_files[self.file_list_index].0.clone(), + self.file_diffs[self.file_list_index].0.path().clone(), "head".to_string(), ), Target::Auto, @@ -224,15 +225,15 @@ impl Widget for SourceControlFileList { let y = mouse_event.pos.y; if y > 0.0 { let line = (y / line_height).floor() as usize; - if line < data.source_control.diff_files.len() + if line < data.source_control.file_diffs.len() && mouse_event.pos.x < line_height { if let Some(mouse_down) = self.mouse_down { if mouse_down == line { let source_control = Arc::make_mut(&mut data.source_control); - source_control.diff_files[line].1 = - !source_control.diff_files[line].1; + source_control.file_diffs[line].1 = + !source_control.file_diffs[line].1; } } } @@ -247,7 +248,7 @@ impl Widget for SourceControlFileList { let y = mouse_event.pos.y; if y > 0.0 { let line = (y / line_height).floor() as usize; - if line < source_control.diff_files.len() { + if line < source_control.file_diffs.len() { source_control.file_list_index = line; if mouse_event.pos.x < line_height { self.mouse_down = Some(line); @@ -255,7 +256,7 @@ impl Widget for SourceControlFileList { ctx.submit_command(Command::new( LAPCE_UI_COMMAND, LapceUICommand::OpenFileDiff( - source_control.diff_files[line].0.clone(), + source_control.file_diffs[line].0.path().clone(), "head".to_string(), ), Target::Widget(data.id), @@ -316,6 +317,11 @@ impl Widget for SourceControlFileList { data: &LapceTabData, env: &Env, ) { + if data.source_control.file_diffs.len() + != old_data.source_control.file_diffs.len() + { + ctx.request_layout(); + } } fn layout( @@ -326,16 +332,18 @@ impl Widget for SourceControlFileList { env: &Env, ) -> Size { let line_height = data.config.editor.line_height as f64; - let height = line_height * data.source_control.diff_files.len() as f64; + let height = line_height * data.source_control.file_diffs.len() as f64; Size::new(bc.max().width, height) } fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) { + let self_size = ctx.size(); + let line_height = data.config.editor.line_height as f64; - let files = &data.source_control.diff_files; + let diffs = &data.source_control.file_diffs; - if ctx.is_focused() && files.len() > 0 { + if ctx.is_focused() && diffs.len() > 0 { let rect = Size::new(ctx.size().width, line_height) .to_rect() .with_origin(Point::new( @@ -352,11 +360,12 @@ impl Widget for SourceControlFileList { let start_line = (rect.y0 / line_height).floor() as usize; let end_line = (rect.y1 / line_height).ceil() as usize; for line in start_line..end_line { - if line >= files.len() { + if line >= diffs.len() { break; } let y = line_height * line as f64; - let (mut path, checked) = files[line].clone(); + let (diff, checked) = diffs[line].clone(); + let mut path = diff.path().clone(); if let Some(workspace) = data.workspace.as_ref() { path = path .strip_prefix(&workspace.path) @@ -407,7 +416,13 @@ impl Widget for SourceControlFileList { ) .build() .unwrap(); - ctx.draw_text(&text_layout, Point::new(line_height * 2.0, y + 4.0)); + ctx.draw_text( + &text_layout, + Point::new( + line_height * 2.0, + y + (line_height - text_layout.size().height) / 2.0, + ), + ); let folder = path .parent() .and_then(|s| s.to_str()) @@ -429,9 +444,46 @@ impl Widget for SourceControlFileList { .unwrap(); ctx.draw_text( &text_layout, - Point::new(line_height * 2.0 + x + 5.0, y + 4.0), + Point::new( + line_height * 2.0 + x + 5.0, + y + (line_height - text_layout.size().height) / 2.0, + ), ); } + + let (svg, color) = match diff { + FileDiff::Modified(_) => ( + "diff-modified.svg", + data.config + .get_color_unchecked(LapceTheme::SOURCE_CONTROL_MODIFIED), + ), + FileDiff::Added(_) => ( + "diff-added.svg", + data.config + .get_color_unchecked(LapceTheme::SOURCE_CONTROL_ADDED), + ), + FileDiff::Deleted(_) => ( + "diff-removed.svg", + data.config + .get_color_unchecked(LapceTheme::SOURCE_CONTROL_REMOVED), + ), + FileDiff::Renamed(_, _) => ( + "diff-renamed.svg", + data.config + .get_color_unchecked(LapceTheme::SOURCE_CONTROL_MODIFIED), + ), + }; + let svg = get_svg(svg).unwrap(); + + let svg_size = 15.0; + let rect = + Size::new(svg_size, svg_size) + .to_rect() + .with_origin(Point::new( + self_size.width - svg_size - 10.0, + line as f64 * line_height + (line_height - svg_size) / 2.0, + )); + ctx.draw_svg(&svg, rect, Some(&color.clone().with_alpha(0.9))); } } } diff --git a/core/src/tab.rs b/core/src/tab.rs index 2086d12016..3ab1dcbf6a 100644 --- a/core/src/tab.rs +++ b/core/src/tab.rs @@ -313,21 +313,29 @@ impl Widget for LapceTabNew { LapceUICommand::UpdateInstalledPlugins(plugins) => { data.installed_plugins = Arc::new(plugins.to_owned()); } - LapceUICommand::UpdateDiffFiles(files) => { + LapceUICommand::UpdateFileDiffs(diffs) => { let source_control = Arc::make_mut(&mut data.source_control); - source_control.diff_files = files + source_control.file_diffs = diffs .iter() - .map(|path| { + .map(|diff| { let mut checked = true; - for (p, c) in source_control.diff_files.iter() { - if p == path { + for (p, c) in source_control.file_diffs.iter() { + if p == diff { checked = *c; break; } } - (path.clone(), checked) + (diff.clone(), checked) }) .collect(); + + for (path, buffer) in data.main_split.open_files.iter() { + buffer.retrieve_file_head( + data.id, + data.proxy.clone(), + ctx.get_external_handle(), + ); + } ctx.set_handled(); } LapceUICommand::WorkDoneProgress(params) => { diff --git a/icons/diff-added.svg b/icons/diff-added.svg new file mode 100644 index 0000000000..0f9cd120e9 --- /dev/null +++ b/icons/diff-added.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/diff-modified.svg b/icons/diff-modified.svg new file mode 100644 index 0000000000..e7a3694998 --- /dev/null +++ b/icons/diff-modified.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/diff-removed.svg b/icons/diff-removed.svg new file mode 100644 index 0000000000..8a9891b2a7 --- /dev/null +++ b/icons/diff-removed.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/diff-renamed.svg b/icons/diff-renamed.svg new file mode 100644 index 0000000000..1b1624c376 --- /dev/null +++ b/icons/diff-renamed.svg @@ -0,0 +1,3 @@ + + + diff --git a/proxy/src/dispatch.rs b/proxy/src/dispatch.rs index 3838dcd415..06e0c11bcd 100644 --- a/proxy/src/dispatch.rs +++ b/proxy/src/dispatch.rs @@ -123,6 +123,10 @@ pub enum Notification { InstallPlugin { plugin: PluginDescription, }, + GitCommit { + message: String, + diffs: Vec, + }, TerminalWrite { term_id: TermId, content: String, @@ -204,6 +208,25 @@ pub struct BufferHeadResponse { pub content: String, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum FileDiff { + Modified(PathBuf), + Added(PathBuf), + Deleted(PathBuf), + Renamed(PathBuf, PathBuf), +} + +impl FileDiff { + pub fn path(&self) -> &PathBuf { + match &self { + FileDiff::Modified(p) + | FileDiff::Added(p) + | FileDiff::Deleted(p) + | FileDiff::Renamed(_, p) => p, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct FileNodeItem { pub path_buf: PathBuf, @@ -309,11 +332,11 @@ impl Dispatcher { if self.workspace_updated.load(atomic::Ordering::Relaxed) { self.workspace_updated .store(false, atomic::Ordering::Relaxed); - if let Some(diff_files) = git_diff(&self.workspace.lock()) { + if let Some(file_diffs) = git_diff_new(&self.workspace.lock()) { self.send_notification( - "diff_files", + "file_diffs", json!({ - "files": diff_files, + "diffs": file_diffs, }), ); } @@ -418,11 +441,11 @@ impl Dispatcher { true, GIT_EVENT_TOKEN, ); - if let Some(diff_files) = git_diff(&workspace) { + if let Some(file_diffs) = git_diff_new(&workspace) { self.send_notification( - "diff_files", + "file_diffs", json!({ - "files": diff_files, + "diffs": file_diffs, }), ); } @@ -494,6 +517,13 @@ impl Dispatcher { ); tx.send(Msg::Resize(size)); } + Notification::GitCommit { message, diffs } => { + eprintln!("received git commit"); + let workspace = self.workspace.lock().clone(); + if let Err(e) = git_commit(&workspace, &message, diffs) { + eprintln!("git commit error {e}"); + } + } } } @@ -685,15 +715,81 @@ pub struct DiffHunk { pub header: String, } -fn git_diff(workspace_path: &PathBuf) -> Option> { +fn git_commit( + workspace_path: &PathBuf, + message: &str, + diffs: Vec, +) -> Result<()> { + let repo = Repository::open( + workspace_path + .to_str() + .ok_or(anyhow!("workspace path can't changed to str"))?, + )?; + let mut index = repo.index()?; + for diff in diffs { + match diff { + FileDiff::Modified(p) | FileDiff::Added(p) => { + index.add_path(p.strip_prefix(workspace_path)?)?; + } + FileDiff::Renamed(a, d) => { + index.add_path(a.strip_prefix(workspace_path)?)?; + index.remove_path(d.strip_prefix(workspace_path)?)?; + } + FileDiff::Deleted(p) => { + index.remove_path(p.strip_prefix(workspace_path)?)?; + } + } + } + index.write()?; + let tree = index.write_tree()?; + let tree = repo.find_tree(tree)?; + let signature = repo.signature()?; + let parent = repo.head()?.peel_to_commit()?; + repo.commit( + Some("HEAD"), + &signature, + &signature, + message, + &tree, + &[&parent], + )?; + Ok(()) +} + +fn git_delta_format( + workspace_path: &PathBuf, + delta: &git2::DiffDelta, +) -> Option<(git2::Delta, git2::Oid, PathBuf)> { + match delta.status() { + git2::Delta::Added | git2::Delta::Untracked => Some(( + git2::Delta::Added, + delta.new_file().id(), + delta.new_file().path().map(|p| workspace_path.join(p))?, + )), + git2::Delta::Deleted => Some(( + git2::Delta::Deleted, + delta.old_file().id(), + delta.old_file().path().map(|p| workspace_path.join(p))?, + )), + git2::Delta::Modified => Some(( + git2::Delta::Modified, + delta.new_file().id(), + delta.new_file().path().map(|p| workspace_path.join(p))?, + )), + _ => None, + } +} + +fn git_diff_new(workspace_path: &PathBuf) -> Option> { let repo = Repository::open(workspace_path.to_str()?).ok()?; - let mut diff_files = HashSet::new(); - let diff = repo.diff_index_to_workdir(None, None).ok()?; + let mut deltas = Vec::new(); + let mut diff_options = DiffOptions::new(); + let diff = repo + .diff_index_to_workdir(None, Some(diff_options.include_untracked(true))) + .ok()?; for delta in diff.deltas() { - if let Some(path) = delta.new_file().path() { - if let Some(s) = path.to_str() { - diff_files.insert(workspace_path.join(s).to_str()?.to_string()); - } + if let Some(delta) = git_delta_format(workspace_path, &delta) { + deltas.push(delta); } } let cached_diff = repo @@ -706,17 +802,91 @@ fn git_diff(workspace_path: &PathBuf) -> Option> { ) .ok()?; for delta in cached_diff.deltas() { - if let Some(path) = delta.new_file().path() { - if let Some(s) = path.to_str() { - diff_files.insert(workspace_path.join(s).to_str()?.to_string()); + if let Some(delta) = git_delta_format(workspace_path, &delta) { + deltas.push(delta); + } + } + let mut renames = Vec::new(); + let mut renamed_deltas = HashSet::new(); + + for (i, delta) in deltas.iter().enumerate() { + if delta.0 == git2::Delta::Added { + for (j, d) in deltas.iter().enumerate() { + if d.0 == git2::Delta::Deleted && d.1 == delta.1 { + renames.push((i, j)); + renamed_deltas.insert(i); + renamed_deltas.insert(j); + break; + } } } } - let mut diff_files: Vec = diff_files.into_iter().collect(); - diff_files.sort(); - Some(diff_files) + + let mut file_diffs = Vec::new(); + for (i, j) in renames.iter() { + file_diffs.push(FileDiff::Renamed( + deltas[*i].2.clone(), + deltas[*j].2.clone(), + )); + } + for (i, delta) in deltas.iter().enumerate() { + if renamed_deltas.contains(&i) { + continue; + } + let diff = match delta.0 { + git2::Delta::Added => FileDiff::Added(delta.2.clone()), + git2::Delta::Deleted => FileDiff::Deleted(delta.2.clone()), + git2::Delta::Modified => FileDiff::Modified(delta.2.clone()), + _ => continue, + }; + file_diffs.push(diff); + } + file_diffs.sort_by_key(|d| match d { + FileDiff::Modified(p) + | FileDiff::Added(p) + | FileDiff::Renamed(p, _) + | FileDiff::Deleted(p) => p.clone(), + }); + Some(file_diffs) } +// fn git_diff(workspace_path: &PathBuf) -> Option> { +// let repo = Repository::open(workspace_path.to_str()?).ok()?; +// let mut diff_files = HashSet::new(); +// let mut diff_options = DiffOptions::new(); +// let diff = repo +// .diff_index_to_workdir(None, Some(diff_options.include_untracked(true))) +// .ok()?; +// for delta in diff.deltas() { +// eprintln!("delta {:?}", delta); +// if let Some(path) = delta.new_file().path() { +// if let Some(s) = path.to_str() { +// diff_files.insert(workspace_path.join(s).to_str()?.to_string()); +// } +// } +// } +// let cached_diff = repo +// .diff_tree_to_index( +// repo.find_tree(repo.revparse_single("HEAD^{tree}").ok()?.id()) +// .ok() +// .as_ref(), +// None, +// None, +// ) +// .ok()?; +// for delta in cached_diff.deltas() { +// eprintln!("delta {:?}", delta); +// if let Some(path) = delta.new_file().path() { +// if let Some(s) = path.to_str() { +// diff_files.insert(workspace_path.join(s).to_str()?.to_string()); +// } +// } +// } +// let mut diff_files: Vec = diff_files.into_iter().collect(); +// diff_files.sort(); +// Some(diff_files) +// } + fn file_get_head( workspace_path: &PathBuf, path: &PathBuf,