diff options
author | Richard Burke <rich.g.burke@gmail.com> | 2019-02-03 16:05:05 +0000 |
---|---|---|
committer | Richard Burke <rich.g.burke@gmail.com> | 2019-02-03 16:05:05 +0000 |
commit | aea21d2559b5f250cd66261292b2f8fc4405edc5 (patch) | |
tree | 2fc2c1151175ac49e28a1fda1a137bc92aa14e36 | |
parent | abfdbd1ff6456e08f371c21842ff59ab1fe92482 (diff) |
Add support for fancy diff format
-rw-r--r-- | cmd/grv/config.go | 61 | ||||
-rw-r--r-- | cmd/grv/diff_view.go | 215 | ||||
-rw-r--r-- | cmd/grv/fancy_diff_view.go | 376 | ||||
-rw-r--r-- | cmd/grv/theme.go | 9 | ||||
-rw-r--r-- | cmd/grv/themes.go | 86 | ||||
-rw-r--r-- | doc/documentation.md | 10 |
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 |