feat: add context lines for preview mode with intelligent truncation

- Add --context-lines (-c) flag to control preview context display
- Add context_lines option to .shush.toml configuration
- Implement lazy-git style preview with context lines around changes
- Add intelligent truncation with "..." markers for distant changes
- Improve context line colors from dark gray to light for better visibility on dark terminals
- Support both regular and git-aware preview modes
- Default to 3 context lines, configurable via flag or config file
- Use -c 0 to show only changed lines (original behavior)
This commit is contained in:
carraes
2025-07-07 14:36:58 -03:00
parent 9109964235
commit d2dc3d8c36
4 changed files with 168 additions and 29 deletions
+11 -2
View File
@@ -9,7 +9,8 @@ import (
) )
type Config struct { type Config struct {
Preserve []string `toml:"preserve"` Preserve []string `toml:"preserve"`
ContextLines int `toml:"context_lines"`
} }
func Default() *Config { func Default() *Config {
@@ -27,6 +28,7 @@ func Default() *Config {
"mypy:", "mypy:",
"type: ignore", "type: ignore",
}, },
ContextLines: 3,
} }
} }
@@ -110,8 +112,12 @@ func loadFromFile(path string) (*Config, error) {
return nil, err return nil, err
} }
defaults := Default()
if len(config.Preserve) == 0 { if len(config.Preserve) == 0 {
config.Preserve = Default().Preserve config.Preserve = defaults.Preserve
}
if config.ContextLines == 0 {
config.ContextLines = defaults.ContextLines
} }
return config, nil return config, nil
@@ -200,6 +206,9 @@ preserve = [
"*IMPORTANT*", # Example wildcard: preserves any comment containing IMPORTANT "*IMPORTANT*", # Example wildcard: preserves any comment containing IMPORTANT
"*DEBUG*", # Example wildcard: preserves any comment containing DEBUG "*DEBUG*", # Example wildcard: preserves any comment containing DEBUG
] ]
# Number of context lines to show around changes in preview mode (default: 3)
context_lines = 3
` `
return os.WriteFile(".shush.toml", []byte(content), 0644) return os.WriteFile(".shush.toml", []byte(content), 0644)
+113 -15
View File
@@ -297,6 +297,11 @@ func (p *Processor) showGitPreviewWithTotals(filename string, lineRanges []git.L
cfg = config.Default() cfg = config.Default()
} }
contextLines := cfg.ContextLines
if p.cli.ContextLines >= 0 {
contextLines = p.cli.ContextLines
}
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
return err return err
@@ -308,6 +313,7 @@ func (p *Processor) showGitPreviewWithTotals(filename string, lineRanges []git.L
blue := color.New(color.FgBlue) blue := color.New(color.FgBlue)
gray := color.New(color.FgHiBlack) gray := color.New(color.FgHiBlack)
yellow := color.New(color.FgYellow) yellow := color.New(color.FgYellow)
dimGray := color.New(color.FgWhite, color.Faint)
fmt.Printf("\n%s %s\n", yellow.Sprint("Git Preview:"), filename) fmt.Printf("\n%s %s\n", yellow.Sprint("Git Preview:"), filename)
if len(lineRanges) == 0 { if len(lineRanges) == 0 {
@@ -318,37 +324,40 @@ func (p *Processor) showGitPreviewWithTotals(filename string, lineRanges []git.L
fmt.Println() fmt.Println()
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
lineNum := 0 var lines []string
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return err
}
keptCount := 0 keptCount := 0
changedCount := 0 changedCount := 0
preservedCount := 0 preservedCount := 0
processEntireFile := len(lineRanges) == 0 processEntireFile := len(lineRanges) == 0
for scanner.Scan() { var changes []changeInfo
lineNum++
line := scanner.Text()
shouldProcess := processEntireFile || git.IsInLineRanges(lineNum, lineRanges)
lineNumStr := gray.Sprintf("%4d", lineNum) for i, line := range lines {
lineNum := i + 1
shouldProcess := processEntireFile || git.IsInLineRanges(lineNum, lineRanges)
if shouldProcess { if shouldProcess {
newLine := p.removeCommentsFromLine(line, language, cfg) newLine := p.removeCommentsFromLine(line, language, cfg)
if newLine != line { if newLine != line {
changedCount++ changedCount++
if newLine == "" { if newLine == "" {
fmt.Printf("%s %s %s\n", lineNumStr, red.Sprint("-"), red.Sprint(line)) changes = append(changes, changeInfo{lineNum, line, newLine, "removed"})
} else { } else {
fmt.Printf("%s %s %s\n", lineNumStr, red.Sprint("~"), red.Sprint(line)) changes = append(changes, changeInfo{lineNum, line, newLine, "modified"})
fmt.Printf("%s %s %s\n", lineNumStr, green.Sprint("+"), green.Sprint(newLine))
} }
} else { } else {
// Check if this line has comments that were preserved
hasComment := p.lineHasComment(line, language) hasComment := p.lineHasComment(line, language)
if hasComment { if hasComment {
preservedCount++ preservedCount++
cyan := color.New(color.FgCyan) changes = append(changes, changeInfo{lineNum, line, line, "preserved"})
fmt.Printf("%s %s %s\n", lineNumStr, cyan.Sprint("P"), line)
} else { } else {
keptCount++ keptCount++
} }
@@ -358,8 +367,28 @@ func (p *Processor) showGitPreviewWithTotals(filename string, lineRanges []git.L
} }
} }
if err := scanner.Err(); err != nil { if len(changes) == 0 {
return err fmt.Printf("%s No comments found to remove\n", gray.Sprint("→"))
fmt.Println()
return nil
}
if contextLines > 0 {
p.displayChangesWithContext(lines, changes, contextLines, red, green, blue, gray, dimGray)
} else {
for _, change := range changes {
lineNumStr := gray.Sprintf("%4d", change.lineNum)
switch change.changeType {
case "removed":
fmt.Printf("%s %s %s\n", lineNumStr, red.Sprint("-"), red.Sprint(change.oldLine))
case "modified":
fmt.Printf("%s %s %s\n", lineNumStr, red.Sprint("~"), red.Sprint(change.oldLine))
fmt.Printf("%s %s %s\n", lineNumStr, green.Sprint("+"), green.Sprint(change.newLine))
case "preserved":
cyan := color.New(color.FgCyan)
fmt.Printf("%s %s %s\n", lineNumStr, cyan.Sprint("P"), change.oldLine)
}
}
} }
fmt.Printf("\n%s\n", strings.Repeat("-", 50)) fmt.Printf("\n%s\n", strings.Repeat("-", 50))
@@ -428,6 +457,75 @@ func (p *Processor) lineHasComment(line string, language types.Language) bool {
return false return false
} }
type changeInfo struct {
lineNum int
oldLine string
newLine string
changeType string
}
func (p *Processor) displayChangesWithContext(lines []string, changes []changeInfo, contextLines int, red, green, blue, gray, dimGray *color.Color) {
if len(changes) == 0 {
return
}
cyan := color.New(color.FgCyan)
totalLines := len(lines)
displayedLines := make(map[int]bool)
for i, change := range changes {
startLine := max(1, change.lineNum-contextLines)
endLine := min(totalLines, change.lineNum+contextLines)
if i > 0 {
prevEndLine := min(totalLines, changes[i-1].lineNum+contextLines)
if startLine > prevEndLine+3 {
fmt.Printf("%s\n", dimGray.Sprint(" ..."))
} else {
startLine = prevEndLine + 1
}
}
for lineNum := startLine; lineNum <= endLine; lineNum++ {
if displayedLines[lineNum] {
continue
}
displayedLines[lineNum] = true
lineNumStr := gray.Sprintf("%4d", lineNum)
line := lines[lineNum-1]
if lineNum == change.lineNum {
switch change.changeType {
case "removed":
fmt.Printf("%s %s %s\n", lineNumStr, red.Sprint("-"), red.Sprint(line))
case "modified":
fmt.Printf("%s %s %s\n", lineNumStr, red.Sprint("~"), red.Sprint(line))
fmt.Printf("%s %s %s\n", lineNumStr, green.Sprint("+"), green.Sprint(change.newLine))
case "preserved":
fmt.Printf("%s %s %s\n", lineNumStr, cyan.Sprint("P"), line)
}
} else {
fmt.Printf("%s %s %s\n", lineNumStr, dimGray.Sprint(" "), dimGray.Sprint(line))
}
}
}
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func (p *Processor) showGitTotals(totals *GitTotals) { func (p *Processor) showGitTotals(totals *GitTotals) {
yellow := color.New(color.FgYellow) yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen) green := color.New(color.FgGreen)
+43 -12
View File
@@ -220,6 +220,11 @@ func (p *Processor) showPreview(filename string, language types.Language) error
cfg = config.Default() cfg = config.Default()
} }
contextLines := cfg.ContextLines
if p.cli.ContextLines >= 0 {
contextLines = p.cli.ContextLines
}
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
return err return err
@@ -231,43 +236,69 @@ func (p *Processor) showPreview(filename string, language types.Language) error
gray := color.New(color.FgHiBlack) gray := color.New(color.FgHiBlack)
yellow := color.New(color.FgYellow) yellow := color.New(color.FgYellow)
cyan := color.New(color.FgCyan) cyan := color.New(color.FgCyan)
dimGray := color.New(color.FgWhite, color.Faint)
fmt.Printf("\n%s %s\n\n", yellow.Sprint("Preview:"), filename) fmt.Printf("\n%s %s\n\n", yellow.Sprint("Preview:"), filename)
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
lineNum := 0 var lines []string
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return err
}
changedCount := 0 changedCount := 0
keptCount := 0 keptCount := 0
preservedCount := 0 preservedCount := 0
for scanner.Scan() { var changes []changeInfo
lineNum++
line := scanner.Text()
newLine := p.removeCommentsFromLine(line, language, cfg)
lineNumStr := gray.Sprintf("%4d", lineNum) for i, line := range lines {
lineNum := i + 1
newLine := p.removeCommentsFromLine(line, language, cfg)
if newLine != line { if newLine != line {
changedCount++ changedCount++
if newLine == "" { if newLine == "" {
fmt.Printf("%s %s %s\n", lineNumStr, red.Sprint("-"), red.Sprint(line)) changes = append(changes, changeInfo{lineNum, line, newLine, "removed"})
} else { } else {
fmt.Printf("%s %s %s\n", lineNumStr, red.Sprint("~"), red.Sprint(line)) changes = append(changes, changeInfo{lineNum, line, newLine, "modified"})
fmt.Printf("%s %s %s\n", lineNumStr, green.Sprint("+"), green.Sprint(newLine))
} }
} else { } else {
hasComment := p.lineHasComment(line, language) hasComment := p.lineHasComment(line, language)
if hasComment { if hasComment {
preservedCount++ preservedCount++
fmt.Printf("%s %s %s\n", lineNumStr, cyan.Sprint("P"), line) changes = append(changes, changeInfo{lineNum, line, line, "preserved"})
} else { } else {
keptCount++ keptCount++
} }
} }
} }
if err := scanner.Err(); err != nil { if len(changes) == 0 {
return err fmt.Printf("%s No comments found to remove\n", gray.Sprint("→"))
fmt.Println()
return nil
}
if contextLines > 0 {
p.displayChangesWithContext(lines, changes, contextLines, red, green, nil, gray, dimGray)
} else {
for _, change := range changes {
lineNumStr := gray.Sprintf("%4d", change.lineNum)
switch change.changeType {
case "removed":
fmt.Printf("%s %s %s\n", lineNumStr, red.Sprint("-"), red.Sprint(change.oldLine))
case "modified":
fmt.Printf("%s %s %s\n", lineNumStr, red.Sprint("~"), red.Sprint(change.oldLine))
fmt.Printf("%s %s %s\n", lineNumStr, green.Sprint("+"), green.Sprint(change.newLine))
case "preserved":
fmt.Printf("%s %s %s\n", lineNumStr, cyan.Sprint("P"), change.oldLine)
}
}
} }
fmt.Printf("\n%s\n", strings.Repeat("-", 50)) fmt.Printf("\n%s\n", strings.Repeat("-", 50))
+1
View File
@@ -11,6 +11,7 @@ type CLI struct {
Backup bool `help:"Create backup files before modification"` Backup bool `help:"Create backup files before modification"`
Verbose bool `help:"Show detailed output"` Verbose bool `help:"Show detailed output"`
PreserveLines bool `help:"Remove comments but preserve empty lines"` PreserveLines bool `help:"Remove comments but preserve empty lines"`
ContextLines int `short:"c" help:"Number of context lines to show in preview mode (default: from config)" default:"-1"`
LLM bool `help:"Show LLM-friendly usage guide"` LLM bool `help:"Show LLM-friendly usage guide"`
ChangesOnly bool `help:"Remove comments only from git changes (staged + unstaged + untracked)"` ChangesOnly bool `help:"Remove comments only from git changes (staged + unstaged + untracked)"`
Staged bool `help:"Remove comments only from staged git changes"` Staged bool `help:"Remove comments only from staged git changes"`