diff --git a/src/Stampeded/Documents/DiffDocumentView.axaml b/src/Stampeded/Documents/DiffDocumentView.axaml index 4b68e8e..52fd00a 100644 --- a/src/Stampeded/Documents/DiffDocumentView.axaml +++ b/src/Stampeded/Documents/DiffDocumentView.axaml @@ -26,6 +26,7 @@ + diff --git a/src/Stampeded/Documents/DiffDocumentView.axaml.cs b/src/Stampeded/Documents/DiffDocumentView.axaml.cs index 4fc6f29..f5636f9 100644 --- a/src/Stampeded/Documents/DiffDocumentView.axaml.cs +++ b/src/Stampeded/Documents/DiffDocumentView.axaml.cs @@ -1,5 +1,6 @@ using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Threading; @@ -11,6 +12,8 @@ using AvaloniaEdit.Rendering; using AvaloniaEdit.Search; +using System.Text; + using Stampeded.Core.Diff; using Stampeded.Core.Roslyn; using Stampeded.Diff; @@ -561,6 +564,9 @@ void OnContextMenuOpening(object? sender, System.ComponentModel.CancelEventArgs CtxFindReferences.IsEnabled = ready; CtxHighlightOccurrences.IsEnabled = ready; CtxCallGraph.IsEnabled = ready; + CtxCopyAsGitDiff.IsEnabled = viewModel is { IsPatch: false, IsSourceView: false } + && model is not null + && !Editor.TextArea.Selection.IsEmpty; } void OnCtxGoToDefinition(object? s, RoutedEventArgs e) => GoToDefinitionCommand(); @@ -599,6 +605,101 @@ void OnCtxPrevCommit(object? s, RoutedEventArgs e) void OnCtxHistoryOfSelection(object? s, RoutedEventArgs e) => HistoryOfSelectionCommand(); void OnCtxCopy(object? s, RoutedEventArgs e) => Editor.Copy(); + void OnCtxCopyAsGitDiff(object? s, RoutedEventArgs e) => CopySelectionAsGitDiff(); + + void CopySelectionAsGitDiff() + { + if (viewModel is null || model is null || Editor.TextArea.Selection.IsEmpty) + return; + var selected = SelectedDocumentLineRange(); + if (selected is not { } range) + return; + string patch = BuildGitDiffForSelection(viewModel.File, model, range.FirstLine, range.LastLine); + if (patch.Length > 0) + TopLevel.GetTopLevel(this)?.Clipboard?.SetTextAsync(patch).HandleExceptions(); + } + + (int FirstLine, int LastLine)? SelectedDocumentLineRange() + { + int start = Editor.SelectionStart; + int end = start + Editor.SelectionLength; + if (end <= start || Editor.Document is null) + return null; + int lastOffset = Math.Min(end, Editor.Document.TextLength) - 1; + if (lastOffset < 0) + return null; + return ( + Editor.Document.GetLineByOffset(start).LineNumber, + Editor.Document.GetLineByOffset(lastOffset).LineNumber); + } + + static string BuildGitDiffForSelection(FileDiff file, DiffDocumentModel m, int firstLine, int lastLine) + { + var sourceLines = m.Text.Split('\n'); + int firstIndex = Math.Max(0, firstLine - 1); + int lastIndex = Math.Min(Math.Min(lastLine, m.Tags.Count), sourceLines.Length) - 1; + if (lastIndex < firstIndex) + return ""; + + var rows = new List<(DiffLineTag Tag, string Text)>(); + for (int i = firstIndex; i <= lastIndex; i++) + { + var tag = m.Tags[i]; + if (tag.Kind is DiffLineKind.Added or DiffLineKind.Removed or DiffLineKind.Context) + rows.Add((tag, sourceLines[i])); + } + if (rows.Count == 0) + return ""; + + int oldLength = rows.Count(r => r.Tag.OldLine > 0); + int newLength = rows.Count(r => r.Tag.NewLine > 0); + int oldStart = oldLength > 0 + ? rows.First(r => r.Tag.OldLine > 0).Tag.OldLine + : PreviousLine(m.Tags, firstIndex, oldSide: true); + int newStart = newLength > 0 + ? rows.First(r => r.Tag.NewLine > 0).Tag.NewLine + : PreviousLine(m.Tags, firstIndex, oldSide: false); + + var text = new StringBuilder(); + text.Append("diff --git ").Append(GitPath('a', file.OldPath)).Append(' ') + .Append(GitPath('b', file.NewPath)).Append('\n'); + if (file.Kind == FileChangeKind.Renamed) + { + text.Append("rename from ").Append(file.OldPath).Append('\n'); + text.Append("rename to ").Append(file.NewPath).Append('\n'); + } + text.Append("--- ").Append(file.Kind == FileChangeKind.Added ? "/dev/null" : GitPath('a', file.OldPath)).Append('\n'); + text.Append("+++ ").Append(file.Kind == FileChangeKind.Deleted ? "/dev/null" : GitPath('b', file.NewPath)).Append('\n'); + text.Append("@@ -").Append(FormatRange(oldStart, oldLength)) + .Append(" +").Append(FormatRange(newStart, newLength)).Append(" @@\n"); + + foreach (var (tag, line) in rows) + { + char prefix = tag.Kind switch { + DiffLineKind.Added => '+', + DiffLineKind.Removed => '-', + _ => ' ', + }; + text.Append(prefix).Append(line).Append('\n'); + } + return text.ToString(); + } + + static int PreviousLine(IReadOnlyList tags, int firstIndex, bool oldSide) + { + for (int i = firstIndex - 1; i >= 0; i--) + { + int line = oldSide ? tags[i].OldLine : tags[i].NewLine; + if (line > 0) + return line; + } + return 0; + } + + static string FormatRange(int start, int length) + => length == 1 ? start.ToString() : $"{start},{length}"; + + static string GitPath(char side, string path) => $"{side}/{path}"; void OnCtxCallGraph(object? s, RoutedEventArgs e) => ShowCallGraphCommand();