Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions emacs.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,16 +476,27 @@ func (rl *Shell) bracketedPasteBegin() {
}

if len(sequence) > 6 {
pasted := string(sequence[:len(sequence)-6])
// Terminals send \r (or \r\n) for line breaks inside a bracketed paste.
// Normalise them to \n, otherwise the stray carriage returns corrupt the
// line buffer and break multiline display and evaluation.
pasted = strings.ReplaceAll(pasted, "\r\n", "\n")
pasted = strings.ReplaceAll(pasted, "\r", "\n")
rl.cursor.InsertAt([]rune(pasted)...)
rl.insertPastedText(string(sequence[:len(sequence)-6]))
}
}

func (rl *Shell) insertPastedText(text string) {
// Terminals send \r (or \r\n) for line breaks inside a bracketed paste.
// Normalise them to \n, otherwise the stray carriage returns corrupt the
// line buffer and break multiline display and evaluation.
text = strings.ReplaceAll(text, "\r\n", "\n")
text = strings.ReplaceAll(text, "\r", "\n")

if rl.PasteTransformer != nil {
text = rl.PasteTransformer(text)
}
if text == "" {
return
}

rl.cursor.InsertAt([]rune(text)...)
}

// skipCsiSequence consumes the remainder of a CSI escape sequence (the GNU
// readline skip-csi-sequence command). Terminals encode many special keys as
// "ESC [" followed by parameter/intermediate bytes (0x20-0x3F) and a single
Expand Down
31 changes: 31 additions & 0 deletions emacs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@ import (
func feedPaste(t *testing.T, payload string) string {
t.Helper()

return feedPasteWithTransformer(t, payload, nil)
}

func feedPasteWithTransformer(t *testing.T, payload string, transformer func(string) string) string {
t.Helper()

line := new(core.Line)
rl := &Shell{
Keys: new(core.Keys),
line: line,
cursor: core.NewCursor(line),
}
rl.SetPasteTransformer(transformer)

// The handler consumes keys until it sees the paste-end sequence, so the
// terminator must be fed too — otherwise it would block waiting for input.
Expand Down Expand Up @@ -52,6 +59,30 @@ func TestBracketedPasteNormalizesCarriageReturns(t *testing.T) {
}
}

func TestBracketedPasteTransformer(t *testing.T) {
got := feedPasteWithTransformer(t, "a\r\nb", func(text string) string {
if text != "a\nb" {
t.Fatalf("transformer saw %q, want normalized text", text)
}

return "rewritten"
})

if got != "rewritten" {
t.Fatalf("transformed paste = %q, want rewritten", got)
}
}

func TestBracketedPasteTransformerCanDropText(t *testing.T) {
got := feedPasteWithTransformer(t, "secret", func(string) string {
return ""
})

if got != "" {
t.Fatalf("dropped paste = %q, want empty line", got)
}
}

// drainKeys pops everything remaining in the key buffer and returns it.
func drainKeys(rl *Shell) string {
var b []byte
Expand Down
14 changes: 14 additions & 0 deletions shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ type Shell struct {
// Once enabled, set to nil to disable again.
SyntaxHighlighter func(line []rune) string

// PasteTransformer, when set, rewrites bracketed paste payloads before
// they are inserted into the input buffer.
PasteTransformer func(text string) string

// Completer is a function that produces completions.
// It takes the readline line ([]rune) and cursor pos as parameters,
// and returns completions with their associated metadata/settings.
Expand Down Expand Up @@ -116,6 +120,16 @@ func NewShell(opts ...inputrc.Option) *Shell {
return shell
}

// SetPasteTransformer sets a function used to rewrite bracketed paste payloads
// before they are inserted into the input buffer. Passing nil disables it.
func (rl *Shell) SetPasteTransformer(fn func(text string) string) {
if rl == nil {
return
}

rl.PasteTransformer = fn
}

// Line is the shell input line buffer.
// Contains methods to search and modify its contents,
// split itself with tokenizers, and displaying itself.
Expand Down
Loading