summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRichard Burke <rich.g.burke@gmail.com>2019-02-03 16:05:05 +0000
committerRichard Burke <rich.g.burke@gmail.com>2019-02-03 16:05:05 +0000
commitaea21d2559b5f250cd66261292b2f8fc4405edc5 (patch)
tree2fc2c1151175ac49e28a1fda1a137bc92aa14e36
parentabfdbd1ff6456e08f371c21842ff59ab1fe92482 (diff)
Add support for fancy diff format
-rw-r--r--cmd/grv/config.go61
-rw-r--r--cmd/grv/diff_view.go215
-rw-r--r--cmd/grv/fancy_diff_view.go376
-rw-r--r--cmd/grv/theme.go9
-rw-r--r--cmd/grv/themes.go86
-rw-r--r--doc/documentation.md10
6 files changed, 704 insertions, 53 deletions
diff --git a/cmd/grv/config.go b/cmd/grv/config.go
index b615333..4198d86 100644
--- a/cmd/grv/config.go
+++ b/cmd/grv/config.go
@@ -32,6 +32,7 @@ const (
cfGitBinaryFilePathDefaultValue = ""
cfCommitLimitDefaultValue = ""
cfDefaultViewDefaultValue = ""
+ cfDiffDisplayDefaultValue = "fancy"
cfAllView = "All"
cfMainView = "MainView"
@@ -81,6 +82,8 @@ const (
CfCommitLimit ConfigVariable = "commit-limit"
// CfDefaultView stores the command to generate the default view
CfDefaultView ConfigVariable = "default-view"
+ // CfDiffDisplay stores the way diffs are displayed
+ CfDiffDisplay ConfigVariable = "diff-display"
)
var systemColorValues = map[string]SystemColorValue{
@@ -158,22 +161,31 @@ var themeComponents = map[string]ThemeComponentID{
cfCommitView + ".CommitGraphBranch6": CmpCommitviewGraphBranch6,
cfCommitView + ".CommitGraphBranch7": CmpCommitviewGraphBranch7,
- cfDiffView + ".Title": CmpDiffviewTitle,
- cfDiffView + ".Footer": CmpDiffviewFooter,
- cfDiffView + ".Normal": CmpDiffviewDifflineNormal,
- cfDiffView + ".CommitAuthor": CmpDiffviewDifflineDiffCommitAuthor,
- cfDiffView + ".CommitAuthorDate": CmpDiffviewDifflineDiffCommitAuthorDate,
- cfDiffView + ".CommitCommitter": CmpDiffviewDifflineDiffCommitCommitter,
- cfDiffView + ".CommitCommitterDate": CmpDiffviewDifflineDiffCommitCommitterDate,
- cfDiffView + ".CommitMessage": CmpDiffviewDifflineDiffCommitMessage,
- cfDiffView + ".StatsFile": CmpDiffviewDifflineDiffStatsFile,
- cfDiffView + ".GitDiffHeader": CmpDiffviewDifflineGitDiffHeader,
- cfDiffView + ".GitDiffExtendedHeader": CmpDiffviewDifflineGitDiffExtendedHeader,
- cfDiffView + ".UnifiedDiffHeader": CmpDiffviewDifflineUnifiedDiffHeader,
- cfDiffView + ".HunkStart": CmpDiffviewDifflineHunkStart,
- cfDiffView + ".HunkHeader": CmpDiffviewDifflineHunkHeader,
- cfDiffView + ".AddedLine": CmpDiffviewDifflineLineAdded,
- cfDiffView + ".RemovedLine": CmpDiffviewDifflineLineRemoved,
+ cfDiffView + ".Title": CmpDiffviewTitle,
+ cfDiffView + ".Footer": CmpDiffviewFooter,
+ cfDiffView + ".Normal": CmpDiffviewDifflineNormal,
+ cfDiffView + ".CommitAuthor": CmpDiffviewDifflineDiffCommitAuthor,
+ cfDiffView + ".CommitAuthorDate": CmpDiffviewDifflineDiffCommitAuthorDate,
+ cfDiffView + ".CommitCommitter": CmpDiffviewDifflineDiffCommitCommitter,
+ cfDiffView + ".CommitCommitterDate": CmpDiffviewDifflineDiffCommitCommitterDate,
+ cfDiffView + ".CommitMessage": CmpDiffviewDifflineDiffCommitMessage,
+ cfDiffView + ".StatsFile": CmpDiffviewDifflineDiffStatsFile,
+ cfDiffView + ".GitDiffHeader": CmpDiffviewDifflineGitDiffHeader,
+ cfDiffView + ".GitDiffExtendedHeader": CmpDiffviewDifflineGitDiffExtendedHeader,
+ cfDiffView + ".UnifiedDiffHeader": CmpDiffviewDifflineUnifiedDiffHeader,
+ cfDiffView + ".HunkStart": CmpDiffviewDifflineHunkStart,
+ cfDiffView + ".HunkHeader": CmpDiffviewDifflineHunkHeader,
+ cfDiffView + ".AddedLine": CmpDiffviewDifflineLineAdded,
+ cfDiffView + ".RemovedLine": CmpDiffviewDifflineLineRemoved,
+ cfDiffView + ".FancySeparator": CmpDiffviewFancyDiffLineSeparator,
+ cfDiffView + ".FancyFile": CmpDiffviewFancyDiffLineFile,
+ cfDiffView + ".FancyLineAdded": CmpDiffviewFancyDifflineLineAdded,
+ cfDiffView + ".FancyLineRemoved": CmpDiffviewFancyDifflineLineRemoved,
+ cfDiffView + ".FancyLineAddedChange": CmpDiffviewFancyDifflineLineAddedChange,
+ cfDiffView + ".FancyLineRemovedChange": CmpDiffviewFancyDifflineLineRemovedChange,
+ cfDiffView + ".FancyEmptyLineAdded": CmpDiffviewFancyDifflineEmptyLineAdded,
+ cfDiffView + ".FancyEmptyLineRemoved": CmpDiffviewFancyDifflineEmptyLineRemoved,
+ cfDiffView + ".FancyTrailingWhitespace": CmpDiffviewFancyDifflineTrailingWhitespace,
cfGitStatusView + ".Message": CmpGitStatusMessage,
cfGitStatusView + ".StagedTitle": CmpGitStatusStagedTitle,
@@ -395,6 +407,11 @@ func NewConfiguration(keyBindings KeyBindings, channels Channels, variables GRVV
validator: &defaultViewValidator{config: config},
description: "Command to generate a custom default view on start up",
},
+ CfDiffDisplay: {
+ defaultValue: cfDiffDisplayDefaultValue,
+ validator: &diffDisplayValidator{},
+ description: "Diff display format",
+ },
}
for _, configVariable := range config.configVariables {
@@ -1216,3 +1233,15 @@ func (defaultViewValidator *defaultViewValidator) validate(value string) (proces
return
}
+
+type diffDisplayValidator struct{}
+
+func (diffDisplayValidator *diffDisplayValidator) validate(value string) (processedValue interface{}, err error) {
+ if IsValidDiffProcessorName(value) {
+ processedValue = value
+ } else {
+ err = fmt.Errorf("Invalid %v value %v", CfDiffDisplay, value)
+ }
+
+ return
+}
diff --git a/cmd/grv/diff_view.go b/cmd/grv/diff_view.go
index b15243b..3590a54 100644
--- a/cmd/grv/diff_view.go
+++ b/cmd/grv/diff_view.go
@@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"fmt"
+ "regexp"
"strings"
"sync"
@@ -23,9 +24,19 @@ const (
dltDiffCommitCommitterDate
dltDiffCommitMessage
dltDiffStatsFile
- dltGitDiffHeader
- dltGitDiffExtendedHeader
- dltUnifiedDiffHeader
+ dltDiffStatsSummary
+ dltGitDiffHeaderDiff
+ dltGitDiffHeaderIndex
+ dltGitDiffHeaderNewFile
+ dltGitDiffHeaderOldFile
+ dltGitDiffHeaderNewMode
+ dltGitDiffHeaderOldMode
+ dltGitDiffHeaderNewFileMode
+ dltGitDiffHeaderDeletedFileMode
+ dltGitDiffHeaderSimilarityIndex
+ dltGitDiffHeaderRenameFrom
+ dltGitDiffHeaderRenameTo
+ dltGitDiffHeaderBinaryFile
dltHunkStart
dltLineAdded
dltLineRemoved
@@ -36,8 +47,11 @@ const (
dvDiffLoadRequestChannelSize = 100
)
+var diffStatsSummaryRegex = regexp.MustCompile(`^\d+\sfiles?\schanged,`)
+
type diffLineSection struct {
text string
+ char AcsChar
themeComponentID ThemeComponentID
}
@@ -82,8 +96,10 @@ func newSectionedDiffLineData(sections []*diffLineSection, lineType diffLineType
}
type diffLines struct {
- lines []*diffLineData
- viewPos ViewPos
+ rawLines []*diffLineData
+ lines []*diffLineData
+ diffType diffProcessorType
+ viewPos ViewPos
}
type diffLoadRequest interface {
@@ -117,6 +133,40 @@ func (stageDiffLoadRequest *stageDiffLoadRequest) diffID() diffID {
type diffID string
+type diffProcessor interface {
+ processDiff([]*diffLineData) ([]*diffLineData, error)
+}
+
+type gitDiffProcessor struct{}
+
+func (gitDiffProcessor *gitDiffProcessor) processDiff(lines []*diffLineData) ([]*diffLineData, error) {
+ return lines, nil
+}
+
+type diffProcessorType int
+
+const (
+ dptGit diffProcessorType = iota
+ dptFancy
+)
+
+var diffProcessorNames = map[string]diffProcessorType{
+ "git": dptGit,
+ "fancy": dptFancy,
+}
+
+var diffProcessors = map[diffProcessorType]diffProcessor{
+ dptGit: &gitDiffProcessor{},
+ dptFancy: &fancyDiffProcessor{},
+}
+
+// IsValidDiffProcessorName returns true if there exists a diff processor with the
+// specified name
+func IsValidDiffProcessorName(diffProcessorName string) (isValid bool) {
+ _, isValid = diffProcessorNames[diffProcessorName]
+ return
+}
+
// DiffView contains all state for the diff view
type DiffView struct {
*AbstractWindowView
@@ -165,6 +215,8 @@ func (diffView *DiffView) Initialise() (err error) {
diffView.waitGroup.Add(1)
go diffView.processDiffLoadRequests()
+ diffView.config.AddOnChangeListener(CfDiffDisplay, diffView)
+
return
}
@@ -217,7 +269,11 @@ func (diffView *DiffView) Render(win RenderWindow) (err error) {
lineBuilder.Append(" ")
for _, section := range diffLine.sections {
- lineBuilder.AppendWithStyle(section.themeComponentID, section.text)
+ if section.char != 0 {
+ lineBuilder.AppendACSChar(section.char, section.themeComponentID)
+ } else {
+ lineBuilder.AppendWithStyle(section.themeComponentID, section.text)
+ }
}
lineIndex++
@@ -289,11 +345,7 @@ func (diffView *DiffView) OnCommitSelected(commit *Commit) (err error) {
diffView.lock.Lock()
diffView.lastRequestedDiff = diffID
- if diffLines, ok := diffView.diffs[diffID]; ok {
- diffView.activeDiff = diffID
- diffView.activeViewPos = diffLines.viewPos
- diffView.setVariables()
- diffView.channels.UpdateDisplay()
+ if diffView.switchToDiffIfExists(diffID) {
diffView.lock.Unlock()
return
}
@@ -307,6 +359,32 @@ func (diffView *DiffView) OnCommitSelected(commit *Commit) (err error) {
return
}
+func (diffView *DiffView) switchToDiffIfExists(diffID diffID) (exists bool) {
+ diffLines, exists := diffView.diffs[diffID]
+ if exists {
+ if diffLines.diffType != diffView.currentDiffProcessorType() {
+ diffProcessor, diffType := diffView.currentDiffProcessor()
+ lines, err := diffProcessor.processDiff(diffLines.rawLines)
+
+ if err != nil {
+ log.Errorf("Failed to convert diff to format %v: %v", diffType, err)
+ diffLines.diffType = dptGit
+ diffLines.lines = diffLines.rawLines
+ } else {
+ diffLines.diffType = diffType
+ diffLines.lines = lines
+ }
+ }
+
+ diffView.activeDiff = diffID
+ diffView.activeViewPos = diffLines.viewPos
+ diffView.setVariables()
+ diffView.channels.UpdateDisplay()
+ }
+
+ return
+}
+
// OnFileSelected loads/fetches the diff for the selected file and refreshes the display
func (diffView *DiffView) OnFileSelected(statusType StatusType, filePath string) {
log.Debugf("DiffView loading diff for file %v", filePath)
@@ -454,9 +532,19 @@ func (diffView *DiffView) storeDiff(diffID diffID, lines []*diffLineData) {
diffView.lock.Lock()
defer diffView.lock.Unlock()
+ rawLines := lines
+ diffProcessor, diffType := diffView.currentDiffProcessor()
+ lines, err := diffProcessor.processDiff(lines)
+ if err != nil {
+ log.Errorf("Failed to convert diff to format %v: %v", diffType, err)
+ lines = rawLines
+ }
+
diffLines := &diffLines{
- lines: lines,
- viewPos: NewViewPosition(),
+ rawLines: rawLines,
+ lines: lines,
+ diffType: diffType,
+ viewPos: NewViewPosition(),
}
diffView.diffs[diffID] = diffLines
@@ -472,6 +560,20 @@ func (diffView *DiffView) storeDiff(diffID diffID, lines []*diffLineData) {
return
}
+func (diffView *DiffView) currentDiffProcessorType() diffProcessorType {
+ diffDisplay := diffView.config.GetString(CfDiffDisplay)
+ if diffType, exists := diffProcessorNames[diffDisplay]; exists {
+ return diffType
+ }
+
+ return dptGit
+}
+
+func (diffView *DiffView) currentDiffProcessor() (diffProcessor, diffProcessorType) {
+ diffType := diffView.currentDiffProcessorType()
+ return diffProcessors[diffType], diffType
+}
+
func (diffView *DiffView) generateDiffLinesForCommit(commit *Commit) (lines []*diffLineData, err error) {
author := commit.commit.Author()
committer := commit.commit.Committer()
@@ -533,9 +635,14 @@ func (diffView *DiffView) generateDiffLinesForDiff(diff *Diff) (lines []*diffLin
for scanner.Scan() {
line := scanner.Text()
+ if diffStatsSummaryRegex.MatchString(line) {
+ lines = append(lines, newDiffLineData(line, dltDiffStatsSummary, CmpDiffviewDifflineNormal))
+ continue
+ }
+
filePart, changePart, err := diffView.splitDiffStatsFileLine(line)
if err != nil {
- lines = append(lines, newNormalDiffLineData(line))
+ log.Warnf("Diff stats line in unexpected format: %v - %v", line, err)
continue
}
@@ -546,23 +653,30 @@ func (diffView *DiffView) generateDiffLinesForDiff(diff *Diff) (lines []*diffLin
},
}
- for _, char := range changePart {
- switch char {
- case '+':
- sections = append(sections, &diffLineSection{
- text: "+",
- themeComponentID: CmpDiffviewDifflineLineAdded,
- })
- case '-':
- sections = append(sections, &diffLineSection{
- text: "-",
- themeComponentID: CmpDiffviewDifflineLineRemoved,
- })
- default:
- sections = append(sections, &diffLineSection{
- text: fmt.Sprintf("%c", char),
- themeComponentID: CmpDiffviewDifflineNormal,
- })
+ if strings.Contains(changePart, " -> ") {
+ sections = append(sections, &diffLineSection{
+ text: changePart,
+ themeComponentID: CmpDiffviewDifflineNormal,
+ })
+ } else {
+ for _, char := range changePart {
+ switch char {
+ case '+':
+ sections = append(sections, &diffLineSection{
+ text: "+",
+ themeComponentID: CmpDiffviewDifflineLineAdded,
+ })
+ case '-':
+ sections = append(sections, &diffLineSection{
+ text: "-",
+ themeComponentID: CmpDiffviewDifflineLineRemoved,
+ })
+ default:
+ sections = append(sections, &diffLineSection{
+ text: fmt.Sprintf("%c", char),
+ themeComponentID: CmpDiffviewDifflineNormal,
+ })
+ }
}
}
@@ -581,11 +695,29 @@ func (diffView *DiffView) generateDiffLinesForDiff(diff *Diff) (lines []*diffLin
switch {
case strings.HasPrefix(line, "diff --git"):
- diffLine = newDiffLineData(line, dltGitDiffHeader, CmpDiffviewDifflineGitDiffHeader)
+ diffLine = newDiffLineData(line, dltGitDiffHeaderDiff, CmpDiffviewDifflineGitDiffHeader)
+ case strings.HasPrefix(line, "new mode"):
+ diffLine = newDiffLineData(line, dltGitDiffHeaderNewMode, CmpDiffviewDifflineNormal)
+ case strings.HasPrefix(line, "old mode"):
+ diffLine = newDiffLineData(line, dltGitDiffHeaderOldMode, CmpDiffviewDifflineNormal)
+ case strings.HasPrefix(line, "new file mode"):
+ diffLine = newDiffLineData(line, dltGitDiffHeaderNewFileMode, CmpDiffviewDifflineNormal)
+ case strings.HasPrefix(line, "deleted file mode"):
+ diffLine = newDiffLineData(line, dltGitDiffHeaderDeletedFileMode, CmpDiffviewDifflineNormal)
+ case strings.HasPrefix(line, "similarity index"):
+ diffLine = newDiffLineData(line, dltGitDiffHeaderSimilarityIndex, CmpDiffviewDifflineNormal)
+ case strings.HasPrefix(line, "rename from"):
+ diffLine = newDiffLineData(line, dltGitDiffHeaderRenameFrom, CmpDiffviewDifflineNormal)
+ case strings.HasPrefix(line, "rename to"):
+ diffLine = newDiffLineData(line, dltGitDiffHeaderRenameTo, CmpDiffviewDifflineNormal)
+ case strings.HasPrefix(line, "Binary files"):
+ diffLine = newDiffLineData(line, dltGitDiffHeaderBinaryFile, CmpDiffviewDifflineNormal)
case strings.HasPrefix(line, "index"):
- diffLine = newDiffLineData(line, dltGitDiffExtendedHeader, CmpDiffviewDifflineGitDiffExtendedHeader)
- case strings.HasPrefix(line, "--- ") || strings.HasPrefix(line, "+++ "):
- diffLine = newDiffLineData(line, dltUnifiedDiffHeader, CmpDiffviewDifflineUnifiedDiffHeader)
+ diffLine = newDiffLineData(line, dltGitDiffHeaderIndex, CmpDiffviewDifflineGitDiffExtendedHeader)
+ case strings.HasPrefix(line, "+++"):
+ diffLine = newDiffLineData(line, dltGitDiffHeaderNewFile, CmpDiffviewDifflineUnifiedDiffHeader)
+ case strings.HasPrefix(line, "---"):
+ diffLine = newDiffLineData(line, dltGitDiffHeaderOldFile, CmpDiffviewDifflineUnifiedDiffHeader)
case strings.HasPrefix(line, "@@"):
if lineParts := strings.SplitAfter(line, "@@"); len(lineParts) != 3 {
log.Warnf("Unable to handle hunk header line: %v", line)
@@ -627,6 +759,16 @@ func (diffView *DiffView) viewPos() ViewPos {
return diffView.activeViewPos
}
+func (diffView *DiffView) onConfigVariableChange(configVariable ConfigVariable) {
+ diffView.lock.Lock()
+ defer diffView.lock.Unlock()
+
+ switch configVariable {
+ case CfDiffDisplay:
+ diffView.switchToDiffIfExists(diffView.activeDiff)
+ }
+}
+
// HandleAction checks if the diff view supports the provided action and executes it if so
func (diffView *DiffView) HandleAction(action Action) (err error) {
log.Debugf("DiffView handling action %v", action)
@@ -749,12 +891,11 @@ func selectDiffLine(diffView *DiffView, action Action) (err error) {
}
filePart = strings.TrimRight(filePart, " ")
- pattern := fmt.Sprintf("diff --git a/%v b/%v", filePart, filePart)
for lineIndex++; lineIndex < uint(len(diffLines.lines)); lineIndex++ {
diffLine = diffLines.lines[lineIndex]
- if strings.HasPrefix(diffLine.line, pattern) {
+ if diffLine.lineType == dltGitDiffHeaderDiff && strings.Contains(diffLine.line, filePart) {
break
}
}
diff --git a/cmd/grv/fancy_diff_view.go b/cmd/grv/fancy_diff_view.go
new file mode 100644
index 0000000..77ca83f
--- /dev/null
+++ b/cmd/grv/fancy_diff_view.go
@@ -0,0 +1,376 @@
+package main
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+ "unicode"
+)
+
+const (
+ fileAdded = "added"
+ fileModified = "modified"
+ fileDeleted = "deleted"
+ fileRenamed = "renamed"
+)
+
+var diffHeaderRegex = regexp.MustCompile(`^diff --git \w\/(.+?)(\s|\x00|$)`)
+var hunkStartRegex = regexp.MustCompile(`@@ (-\d+(,\d+)?)? \+(\d+)(,\d+)? @@`)
+var oldNewFileRegex = regexp.MustCompile(`^(\+\+\+|---)\s(\w\/)?`)
+var oldNewModeRegex = regexp.MustCompile(`^(old|new)\smode\s`)
+var renameFromToRegex = regexp.MustCompile(`^rename\s(from|to)\s`)
+var trailingSpaceRegex = regexp.MustCompile(`\s+$`)
+
+var separatorDiffLine *diffLineData
+
+var emptyLineAdded = newDiffLineData(" ", dltLineAdded, CmpDiffviewFancyDifflineEmptyLineAdded)
+var emptyLineRemoved = newDiffLineData(" ", dltLineRemoved, CmpDiffviewFancyDifflineEmptyLineRemoved)
+
+var fileStatusStyling = map[string]ThemeComponentID{
+ fileAdded: CmpDiffviewFancyDifflineLineAddedChange,
+ fileModified: CmpDiffviewFancyDiffLineFile,
+ fileDeleted: CmpDiffviewFancyDifflineLineRemovedChange,
+}
+
+func init() {
+ var sections []*diffLineSection
+ for i := 0; i < 1000; i++ {
+ sections = append(sections, &diffLineSection{
+ char: AcsHline,
+ themeComponentID: CmpDiffviewFancyDiffLineSeparator,
+ })
+ }
+
+ separatorDiffLine = newSectionedDiffLineData(sections, dltNormal)
+}
+
+type fancyDiffProcessor struct{}
+
+func (fancyDiffProcessor *fancyDiffProcessor) processDiff(lines []*diffLineData) (processedLines []*diffLineData, err error) {
+ var generatedLines []*diffLineData
+ var currentFile string
+
+ for lineIndex, line := range lines {
+ switch line.lineType {
+ case dltGitDiffHeaderDiff:
+ if generatedLines, currentFile, err = fancyDiffProcessor.processDiffHeader(lines, lineIndex); err != nil {
+ return
+ }
+
+ processedLines = append(processedLines, generatedLines...)
+ case dltGitDiffHeaderIndex,
+ dltGitDiffHeaderNewFile,
+ dltGitDiffHeaderOldFile,
+ dltGitDiffHeaderNewMode,
+ dltGitDiffHeaderOldMode,
+ dltGitDiffHeaderNewFileMode,
+ dltGitDiffHeaderDeletedFileMode,
+ dltGitDiffHeaderSimilarityIndex,
+ dltGitDiffHeaderRenameFrom,
+ dltGitDiffHeaderRenameTo,
+ dltGitDiffHeaderBinaryFile:
+ case dltHunkStart:
+ if generatedLines, err = fancyDiffProcessor.processHunkStart(lines, lineIndex, currentFile); err != nil {
+ return
+ }
+
+ processedLines = append(processedLines, generatedLines...)
+ case dltLineAdded:
+ processedLines = append(processedLines, newDiffLineData(trimFirstCharacter(line.line), line.lineType, CmpDiffviewFancyDifflineLineAdded))
+ case dltLineRemoved:
+ processedLines = append(processedLines, newDiffLineData(trimFirstCharacter(line.line), line.lineType, CmpDiffviewFancyDifflineLineRemoved))
+ case dltNormal:
+ processedLines = append(processedLines, newDiffLineData(trimFirstCharacter(line.line), line.lineType, line.sections[0].themeComponentID))
+ default:
+ processedLines = append(processedLines, line)
+ }
+ }
+
+ fancyDiffProcessor.highlightChanges(processedLines)
+
+ return
+}
+
+func (fancyDiffProcessor *fancyDiffProcessor) processDiffHeader(lines []*diffLineData, diffHeaderIndex int) (generatedLines []*diffLineData, currentFile string, err error) {
+ var oldFile, newFile, oldFileMode, newFileMode string
+ var isBinary bool
+ status := fileModified
+
+OuterLoop:
+ for lineIndex := diffHeaderIndex; lineIndex < len(lines); lineIndex++ {
+ line := lines[lineIndex]
+
+ switch line.lineType {
+ case dltGitDiffHeaderDiff:
+ matches := diffHeaderRegex.FindStringSubmatch(line.line)
+ if len(matches) != 3 {
+ err = fmt.Errorf("line: \"%v\" doesn't have expected diff header format: %v", line.line, matches)
+ return
+ }
+
+ newFile = matches[1]
+ case dltGitDiffHeaderNewFile:
+ newFile = oldNewFileRegex.ReplaceAllString(line.line, "")
+ case dltGitDiffHeaderOldFile:
+ oldFile = oldNewFileRegex.ReplaceAllString(line.line, "")
+ case dltGitDiffHeaderNewMode:
+ newFileMode = oldNewModeRegex.ReplaceAllString(line.line, "")
+ case dltGitDiffHeaderOldMode:
+ oldFileMode = oldNewModeRegex.ReplaceAllString(line.line, "")
+ case dltGitDiffHeaderNewFileMode:
+ status = fileAdded
+ case dltGitDiffHeaderDeletedFileMode:
+ status = fileDeleted
+ case dltGitDiffHeaderSimilarityIndex:
+ status = fileRenamed
+ case dltGitDiffHeaderRenameFrom:
+ oldFile = renameFromToRegex.ReplaceAllString(line.line, "")
+ case dltGitDiffHeaderRenameTo:
+ newFile = renameFromToRegex.ReplaceAllString(line.line, "")
+ case dltGitDiffHeaderBinaryFile:
+ isBinary = true
+ case dltGitDiffHeaderIndex:
+ default:
+ break OuterLoop
+ }
+ }
+
+ if newFile == "" {
+ err = fmt.Errorf("Unable to determine new file from diff headers")
+ return
+ }
+
+ currentFile = newFile
+ if status == fileDeleted && oldFile != "" {
+ currentFile = oldFile
+ }
+
+ sections := []*diffLineSection{}
+
+ if status == fileRenamed {
+ commonPrefix, commonSuffix := determineCommonFixes(oldFile, newFile)
+ oldFileLine := newDiffLineData(oldFile, dltNormal, CmpDiffviewFancyDiffLineFile)
+ newFileLine := newDiffLineData(newFile, dltNormal, CmpDiffviewFancyDiffLineFile)
+ highlightLine(oldFileLine, commonPrefix, commonSuffix, CmpDiffviewFancyDifflineLineRemovedChange)
+ highlightLine(newFileLine, commonPrefix, commonSuffix, CmpDiffviewFancyDifflineLineAddedChange)
+
+ sections = append(sections, &diffLineSection{
+ text: fmt.Sprintf("%v: ", status),
+ themeComponentID: CmpDiffviewFancyDiffLineFile,
+ })
+ sections = append(sections, oldFileLine.sections...)
+ sections = append(sections, &diffLineSection{
+ text: " to ",
+ themeComponentID: CmpDiffviewFancyDiffLineFile,
+ })
+ sections = append(sections, newFileLine.sections...)
+ } else {
+ sections = append(sections,
+ &diffLineSection{
+ text: fmt.Sprintf("%v: ", status),
+ themeComponentID: CmpDiffviewFancyDiffLineFile,
+ },
+ &diffLineSection{
+ text: currentFile,
+ themeComponentID: fileStatusStyling[status],
+ })
+ }
+
+ if isBinary {
+ sections = append(sections, &diffLineSection{
+ text: " (binary)",
+ themeComponentID: CmpDiffviewFancyDiffLineFile,
+ })
+ }
+
+ if oldFileMode != "" && newFileMode != "" {
+ modeChange := fmt.Sprintf("%v changed file mode from %v to %v", currentFile, oldFileMode, newFileMode)
+ generatedLines = append(generatedLines, newDiffLineData(modeChange, dltNormal, CmpDiffviewDifflineNormal))
+ }
+
+ generatedLines = append(generatedLines,
+ separatorDiffLine,
+ newSectionedDiffLineData(sections, dltGitDiffHeaderDiff),
+ separatorDiffLine,
+ )
+
+ return
+}
+
+func (fancyDiffProcessor *fancyDiffProcessor) processHunkStart(lines []*diffLineData, hunkStartIndex int, currentFile string) (generatedLines []*diffLineData, err error) {
+ hunkLine := lines[hunkStartIndex]
+ matches := hunkStartRegex.FindStringSubmatch(hunkLine.line)
+ if len(matches) != 5 {
+ err = fmt.Errorf("Hunk start line didn't match expected format, matches: %v", matches)
+ return
+ }
+
+ hunkStartLineNumber, err := strconv.Atoi(matches[3])
+ if err != nil {
+ err = fmt.Errorf("Failed to parse hunk start line number %v: %v", matches[3], err)
+ return
+ }
+
+ var index int
+ for index = hunkStartIndex + 1; index < len(lines); index++ {
+ if lines[index].lineType == dltLineAdded || lines[index].lineType == dltLineRemoved {
+ break
+ }
+
+ hunkStartLineNumber++
+ }
+
+ if index >= len(lines) {
+ err = fmt.Errorf("Failed to find changes in hunk")
+ return
+ }
+
+ hunkStartLineNumber = MaxInt(hunkStartLineNumber, 1)
+
+ hunkParts := strings.Split(hunkLine.line, " @@")
+ if len(hunkParts) != 2 {
+ err = fmt.Errorf("Expected 2 hunk parts but got: %v", hunkParts)
+ return
+ }
+
+ sections := []*diffLineSection{
+ &diffLineSection{
+ text: fmt.Sprintf("@ %v:%v @", currentFile, hunkStartLineNumber),
+ themeComponentID: CmpDiffviewDifflineHunkStart,
+ },
+ &diffLineSection{
+ text: hunkParts[1],
+ themeComponentID: CmpDiffviewDifflineHunkHeader,
+ },
+ }
+
+ generatedLines = append(generatedLines, newSectionedDiffLineData(sections, dltHunkStart))
+
+ return
+}
+
+func (fancyDiffProcessor *fancyDiffProcessor) highlightChanges(lines []*diffLineData) {
+ for lineIndex := 0; lineIndex < len(lines); lineIndex++ {
+ if lines[lineIndex].lineType == dltLineRemoved {
+ linesRemoved := []*diffLineData{lines[lineIndex]}
+ for lineIndex++; lineIndex < len(lines) && lines[lineIndex].lineType == dltLineRemoved; lineIndex++ {
+ linesRemoved = append(linesRemoved, lines[lineIndex])
+ }
+
+ var linesAdded []*diffLineData
+ for ; lineIndex < len(lines) && lines[lineIndex].lineType == dltLineAdded; lineIndex++ {
+ linesAdded = append(linesAdded, lines[lineIndex])
+ }
+ lineIndex--
+
+ if len(linesRemoved) == len(linesAdded) {
+ for i := 0; i < len(linesRemoved); i++ {
+ commonPrefix, commonSuffix := determineCommonFixes(linesRemoved[i].line, linesAdded[i].line)
+ if commonPrefix > 0 || commonSuffix > 0 {
+ highlightLine(linesRemoved[i], commonPrefix, commonSuffix, CmpDiffviewFancyDifflineLineRemovedChange)
+ highlightLine(linesAdded[i], commonPrefix, commonSuffix, CmpDiffviewFancyDifflineLineAddedChange)
+ }
+ }
+ }
+ }
+ }
+
+ for lineIndex := 0; lineIndex < len(lines); lineIndex++ {
+ line := lines[lineIndex]
+ if line.lineType == dltLineAdded {
+ lines[lineIndex] = processWhitespaceLine(line, emptyLineAdded)
+ } else if line.lineType == dltLineRemoved {
+ lines[lineIndex] = processWhitespaceLine(line, emptyLineRemoved)
+ }
+ }
+}
+
+func processWhitespaceLine(line *diffLineData, emptyLine *diffLineData) *diffLineData {
+ if line.line == "" {
+ return emptyLine
+ } else if matchIndexes := trailingSpaceRegex.FindStringIndex(line.line); len(matchIndexes) == 2 {
+ section := line.sections[len(line.sections)-1]
+ matchLength := MinInt(len(section.text), matchIndexes[1]-matchIndexes[0])
+
+ if matchLength >= len(section.text) {
+ section.themeComponentID = CmpDiffviewFancyDifflineTrailingWhitespace
+ } else {
+ section.text = section.text[:len(section.text)-matchLength]
+ line.sections = append(line.sections, &diffLineSection{
+ text: line.line[len(line.line)-matchLength:],
+ themeComponentID: CmpDiffviewFancyDifflineTrailingWhitespace,
+ })
+ }
+ }
+
+ return line
+}
+
+func determineCommonFixes(lineRemoved, lineAdded string) (commonPrefix, commonSuffix int) {
+ removedChars := []rune(lineRemoved)
+ addedChars := []rune(lineAdded)
+ charLength := MinInt(len(removedChars), len(addedChars))
+
+ for ; commonPrefix < charLength && removedChars[commonPrefix] == addedChars[commonPrefix]; commonPrefix++ {
+ }
+
+ for i, j := len(addedChars)-1, len(removedChars)-1; i > -1 && j > -1 && addedChars[i] == removedChars[j]; i, j = i-1, j-1 {
+ commonSuffix++
+ }
+
+ return
+}
+
+func highlightLine(line *diffLineData, commonPrefix, commonSuffix int, highlightThemeComponentID ThemeComponentID) {
+ lineString := []rune(line.line)
+ lineLength := len(lineString)
+ if commonPrefix+commonSuffix > lineLength {
+ return
+ }
+
+ var commonPrefixString, commonSuffixString string
+ if commonPrefix > 0 {
+ commonPrefixString = string(lineString[:commonPrefix])
+ }
+ if commonSuffix > 0 {
+ commonSuffixString = string(lineString[lineLength-commonSuffix:])
+ }
+
+ if !(commonPrefix == 0 || strings.TrimLeftFunc(commonPrefixString, unicode.IsSpace) != "" &&
+ commonSuffix == 0 || strings.TrimRightFunc(commonSuffixString, unicode.IsSpace) != "") {
+ return
+ }
+
+ sections := []*diffLineSection{}
+ themeComponentID := line.sections[0].themeComponentID
+
+ if commonPrefixString != "" {
+ sections = append(sections, &diffLineSection{
+ text: commonPrefixString,
+ themeComponentID: themeComponentID,
+ })
+ }
+
+ sections = append(sections, &diffLineSection{
+ text: string(lineString[commonPrefix : lineLength-commonSuffix]),
+ themeComponentID: highlightThemeComponentID,
+ })
+
+ if commonSuffixString != "" {
+ sections = append(sections, &diffLineSection{
+ text: commonSuffixString,
+ themeComponentID: themeComponentID,
+ })
+ }
+
+ line.sections = sections
+}
+
+func trimFirstCharacter(line string) string {
+ if line != "" {
+ return line[1:]
+ }
+
+ return line
+}
diff --git a/cmd/grv/theme.go b/cmd/grv/theme.go
index 8716f67..ce0d149 100644
--- a/cmd/grv/theme.go
+++ b/cmd/grv/theme.go
@@ -61,6 +61,15 @@ const (
CmpDiffviewDifflineHunkHeader
CmpDiffviewDifflineLineAdded
CmpDiffviewDifflineLineRemoved
+ CmpDiffviewFancyDiffLineSeparator
+ CmpDiffviewFancyDiffLineFile
+ CmpDiffviewFancyDifflineLineAdded
+ CmpDiffviewFancyDifflineLineRemoved
+ CmpDiffviewFancyDifflineLineAddedChange
+ CmpDiffviewFancyDifflineLineRemovedChange
+ CmpDiffviewFancyDifflineEmptyLineAdded
+ CmpDiffviewFancyDifflineEmptyLineRemoved
+ CmpDiffviewFancyDifflineTrailingWhitespace
CmpGitStatusMessage
CmpGitStatusStagedTitle
diff --git a/cmd/grv/themes.go b/cmd/grv/themes.go
index 5899795..48db135 100644
--- a/cmd/grv/themes.go
+++ b/cmd/grv/themes.go
@@ -164,6 +164,49 @@ func NewClassicTheme() MutableTheme {
bgcolor: NewSystemColor(ColorNone),
fgcolor: NewSystemColor(ColorRed),
},
+ CmpDiffviewFancyDiffLineSeparator: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewSystemColor(ColorYellow),
+ },
+ CmpDiffviewFancyDiffLineFile: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewSystemColor(ColorYellow),
+ },
+ CmpDiffviewFancyDifflineLineAdded: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewSystemColor(ColorGreen),
+ style: ThemeStyle{styleTypes: TstBold},
+ },
+ CmpDiffviewFancyDifflineLineRemoved: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewSystemColor(ColorRed),
+ style: ThemeStyle{styleTypes: TstBold},
+ },
+ CmpDiffviewFancyDifflineLineAddedChange: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewSystemColor(ColorGreen),
+ style: ThemeStyle{styleTypes: TstBold | TstReverse},
+ },
+ CmpDiffviewFancyDifflineLineRemovedChange: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewSystemColor(ColorRed),
+ style: ThemeStyle{styleTypes: TstBold | TstReverse},
+ },
+ CmpDiffviewFancyDifflineEmptyLineAdded: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewSystemColor(ColorGreen),
+ style: ThemeStyle{styleTypes: TstReverse},
+ },
+ CmpDiffviewFancyDifflineEmptyLineRemoved: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewSystemColor(ColorRed),
+ style: ThemeStyle{styleTypes: TstReverse},
+ },
+ CmpDiffviewFancyDifflineTrailingWhitespace: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewSystemColor(ColorRed),
+ style: ThemeStyle{styleTypes: TstReverse},
+ },
CmpRefviewTitle: {
bgcolor: NewSystemColor(ColorNone),
fgcolor: NewSystemColor(ColorCyan),
@@ -605,6 +648,49 @@ func NewSolarizedTheme() MutableTheme {
bgcolor: NewSystemColor(ColorNone),
fgcolor: NewColorNumber(solarizedRed),
},
+ CmpDiffviewFancyDiffLineSeparator: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewColorNumber(solarizedYellow),
+ },
+ CmpDiffviewFancyDiffLineFile: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewColorNumber(solarizedYellow),
+ },
+ CmpDiffviewFancyDifflineLineAdded: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewColorNumber(solarizedGreen),
+ style: ThemeStyle{styleTypes: TstBold},
+ },
+ CmpDiffviewFancyDifflineLineRemoved: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewColorNumber(solarizedRed),
+ style: ThemeStyle{styleTypes: TstBold},
+ },
+ CmpDiffviewFancyDifflineLineAddedChange: {
+ bgcolor: NewColorNumber(22),
+ fgcolor: NewColorNumber(10),
+ style: ThemeStyle{styleTypes: TstBold},
+ },
+ CmpDiffviewFancyDifflineLineRemovedChange: {
+ bgcolor: NewColorNumber(52),
+ fgcolor: NewColorNumber(9),
+ style: ThemeStyle{styleTypes: TstBold},
+ },
+ CmpDiffviewFancyDifflineEmptyLineAdded: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewColorNumber(solarizedGreen),
+ style: ThemeStyle{styleTypes: TstReverse},
+ },
+ CmpDiffviewFancyDifflineEmptyLineRemoved: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewColorNumber(solarizedRed),
+ style: ThemeStyle{styleTypes: TstReverse},
+ },
+ CmpDiffviewFancyDifflineTrailingWhitespace: {
+ bgcolor: NewSystemColor(ColorNone),
+ fgcolor: NewColorNumber(solarizedRed),
+ style: ThemeStyle{styleTypes: TstReverse},
+ },
CmpRefviewTitle: {
bgcolor: NewSystemColor(ColorNone),
fgcolor: NewColorNumber(solarizedCyan),
diff --git a/doc/documentation.md b/doc/documentation.md
index e4fb6e4..6ac1507 100644
--- a/doc/documentation.md
+++ b/doc/documentation.md
@@ -209,6 +209,7 @@ They are specified using the set command in the grvrc file or at the command pro
commit-limit | string | | Limit the number of commits loaded. Allowed values: number, date, oid or tag
confirm-checkout | bool | true | Confirm before performing git checkout
default-view | string | | Command to generate a custom default view on start up
+ diff-display | string | fancy | Diff display format
git-binary-file-path | string | | File path to git binary. Required only when git binary is not in $PATH
mouse | bool | false | Mouse support enabled
mouse-scroll-rows | int | 3 | Number of rows scrolled for each mouse event
@@ -596,6 +597,15 @@ DiffView.CommitAuthorDate
DiffView.CommitCommitter
DiffView.CommitCommitterDate
DiffView.CommitMessage
+DiffView.FancyEmptyLineAdded
+DiffView.FancyEmptyLineRemoved
+DiffView.FancyFile
+DiffView.FancyLineAdded
+DiffView.FancyLineAddedChange
+DiffView.FancyLineRemoved
+DiffView.FancyLineRemovedChange
+DiffView.FancySeparator
+DiffView.FancyTrailingWhitespace
DiffView.Footer
DiffView.GitDiffExtendedHeader
DiffView.GitDiffHeader