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