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 {
Preserve []string `toml:"preserve"`
Preserve []string `toml:"preserve"`
ContextLines int `toml:"context_lines"`
}
func Default() *Config {
@@ -27,6 +28,7 @@ func Default() *Config {
"mypy:",
"type: ignore",
},
ContextLines: 3,
}
}
@@ -110,8 +112,12 @@ func loadFromFile(path string) (*Config, error) {
return nil, err
}
defaults := Default()
if len(config.Preserve) == 0 {
config.Preserve = Default().Preserve
config.Preserve = defaults.Preserve
}
if config.ContextLines == 0 {
config.ContextLines = defaults.ContextLines
}
return config, nil
@@ -200,6 +206,9 @@ preserve = [
"*IMPORTANT*", # Example wildcard: preserves any comment containing IMPORTANT
"*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)
+113 -15
View File
@@ -297,6 +297,11 @@ func (p *Processor) showGitPreviewWithTotals(filename string, lineRanges []git.L
cfg = config.Default()
}
contextLines := cfg.ContextLines
if p.cli.ContextLines >= 0 {
contextLines = p.cli.ContextLines
}
file, err := os.Open(filename)
if err != nil {
return err
@@ -308,6 +313,7 @@ func (p *Processor) showGitPreviewWithTotals(filename string, lineRanges []git.L
blue := color.New(color.FgBlue)
gray := color.New(color.FgHiBlack)
yellow := color.New(color.FgYellow)
dimGray := color.New(color.FgWhite, color.Faint)
fmt.Printf("\n%s %s\n", yellow.Sprint("Git Preview:"), filename)
if len(lineRanges) == 0 {
@@ -318,37 +324,40 @@ func (p *Processor) showGitPreviewWithTotals(filename string, lineRanges []git.L
fmt.Println()
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
changedCount := 0
preservedCount := 0
processEntireFile := len(lineRanges) == 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
shouldProcess := processEntireFile || git.IsInLineRanges(lineNum, lineRanges)
var changes []changeInfo
lineNumStr := gray.Sprintf("%4d", lineNum)
for i, line := range lines {
lineNum := i + 1
shouldProcess := processEntireFile || git.IsInLineRanges(lineNum, lineRanges)
if shouldProcess {
newLine := p.removeCommentsFromLine(line, language, cfg)
if newLine != line {
changedCount++
if newLine == "" {
fmt.Printf("%s %s %s\n", lineNumStr, red.Sprint("-"), red.Sprint(line))
changes = append(changes, changeInfo{lineNum, line, newLine, "removed"})
} else {
fmt.Printf("%s %s %s\n", lineNumStr, red.Sprint("~"), red.Sprint(line))
fmt.Printf("%s %s %s\n", lineNumStr, green.Sprint("+"), green.Sprint(newLine))
changes = append(changes, changeInfo{lineNum, line, newLine, "modified"})
}
} else {
// Check if this line has comments that were preserved
hasComment := p.lineHasComment(line, language)
if hasComment {
preservedCount++
cyan := color.New(color.FgCyan)
fmt.Printf("%s %s %s\n", lineNumStr, cyan.Sprint("P"), line)
changes = append(changes, changeInfo{lineNum, line, line, "preserved"})
} else {
keptCount++
}
@@ -358,8 +367,28 @@ func (p *Processor) showGitPreviewWithTotals(filename string, lineRanges []git.L
}
}
if err := scanner.Err(); err != nil {
return err
if len(changes) == 0 {
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))
@@ -428,6 +457,75 @@ func (p *Processor) lineHasComment(line string, language types.Language) bool {
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) {
yellow := color.New(color.FgYellow)
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()
}
contextLines := cfg.ContextLines
if p.cli.ContextLines >= 0 {
contextLines = p.cli.ContextLines
}
file, err := os.Open(filename)
if err != nil {
return err
@@ -231,43 +236,69 @@ func (p *Processor) showPreview(filename string, language types.Language) error
gray := color.New(color.FgHiBlack)
yellow := color.New(color.FgYellow)
cyan := color.New(color.FgCyan)
dimGray := color.New(color.FgWhite, color.Faint)
fmt.Printf("\n%s %s\n\n", yellow.Sprint("Preview:"), filename)
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
keptCount := 0
preservedCount := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
newLine := p.removeCommentsFromLine(line, language, cfg)
var changes []changeInfo
lineNumStr := gray.Sprintf("%4d", lineNum)
for i, line := range lines {
lineNum := i + 1
newLine := p.removeCommentsFromLine(line, language, cfg)
if newLine != line {
changedCount++
if newLine == "" {
fmt.Printf("%s %s %s\n", lineNumStr, red.Sprint("-"), red.Sprint(line))
changes = append(changes, changeInfo{lineNum, line, newLine, "removed"})
} else {
fmt.Printf("%s %s %s\n", lineNumStr, red.Sprint("~"), red.Sprint(line))
fmt.Printf("%s %s %s\n", lineNumStr, green.Sprint("+"), green.Sprint(newLine))
changes = append(changes, changeInfo{lineNum, line, newLine, "modified"})
}
} else {
hasComment := p.lineHasComment(line, language)
if hasComment {
preservedCount++
fmt.Printf("%s %s %s\n", lineNumStr, cyan.Sprint("P"), line)
changes = append(changes, changeInfo{lineNum, line, line, "preserved"})
} else {
keptCount++
}
}
}
if err := scanner.Err(); err != nil {
return err
if len(changes) == 0 {
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))
+1
View File
@@ -11,6 +11,7 @@ type CLI struct {
Backup bool `help:"Create backup files before modification"`
Verbose bool `help:"Show detailed output"`
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"`
ChangesOnly bool `help:"Remove comments only from git changes (staged + unstaged + untracked)"`
Staged bool `help:"Remove comments only from staged git changes"`