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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user