-
Notifications
You must be signed in to change notification settings - Fork 15
/
copypaste.go
262 lines (222 loc) · 7.9 KB
/
copypaste.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
package main
import (
"fmt"
"os"
"strings"
"time"
"github.com/xyproto/clip"
"github.com/xyproto/env/v2"
"github.com/xyproto/files"
"github.com/xyproto/mode"
"github.com/xyproto/vt100"
)
// When pasting text, portals older than this duration will be disregarded
const maxPortalAge = 25 * time.Minute
// SetClipboardFromFile can copy the given file to the clipboard.
// The returned int is the number of bytes written.
// The returned string is the last 7 characters written to the file.
func SetClipboardFromFile(filename string, primaryClipboard bool) (int, string, error) {
// Read the file
data, err := os.ReadFile(filename)
if err != nil {
return 0, "", err
}
// Write to the clipboard
if err := clip.WriteAllBytes(data, primaryClipboard); err != nil {
return 0, "", err
}
contents := string(data)
tailString := ""
if l := len(contents); l > 7 {
tailString = string(contents[l-8:])
}
return len(data), tailString, nil
}
// WriteClipboardToFile can write the contents of the clipboard to a file.
// If overwrite is true, the original file will be removed first, if it exists.
// The returned int is the number of bytes written.
// The fist returned string is the first 7 characters written to the file.
// The second returned string is the last 7 characters written to the file.
func WriteClipboardToFile(filename string, overwrite, primaryClipboard bool) (int, string, string, error) {
// Check if the file exists first
if files.Exists(filename) {
if overwrite {
if err := os.Remove(filename); err != nil {
return 0, "", "", err
}
} else {
return 0, "", "", fmt.Errorf("%s already exists", filename)
}
}
// Read the clipboard
contents, err := clip.ReadAllBytes(primaryClipboard)
if err != nil {
return 0, "", "", err
}
// Write to file
f, err := os.Create(filename)
if err != nil {
return 0, "", "", err
}
defer f.Close()
lenContents := len(contents)
headString := ""
if lenContents > 7 {
headString = string(contents[:8])
}
tailString := ""
if lenContents > 7 {
tailString = string(contents[lenContents-8:])
}
n, err := f.Write(contents)
if err != nil {
return 0, "", "", err
}
return n, headString, tailString, nil
}
// Paste is called when the user presses ctrl-v, and handles portals, clipboards and also non-clipboard-based copy and paste
func (e *Editor) Paste(c *vt100.Canvas, status *StatusBar, copyLines, previousCopyLines *[]string, firstPasteAction *bool, lastCopyY, lastPasteY, lastCutY *LineIndex, prevKeyWasReturn bool) {
if portal, err := LoadPortal(maxPortalAge); err == nil { // no error
line, err := portal.PopLine(e, false) // pop the line, but don't remove it from the source file
if err == nil { // success
status.ClearAll(c, false)
status.SetMessageAfterRedraw("Pasting through the portal")
undo.Snapshot(e)
if e.EmptyRightTrimmedLine() {
// If the line is empty, replace with the string from the portal
e.SetCurrentLine(line)
} else {
// If the line is not empty, insert the trimmed string
e.InsertStringAndMove(c, strings.TrimSpace(line))
}
e.InsertLineBelow()
e.Down(c, nil) // no status message if the end of document is reached, there should always be a new line
e.redraw.Store(true)
return
}
e.ClosePortal()
status.Clear(c, false)
status.SetError(err)
status.Show(c, e)
}
// This may only work for the same user, and not with sudo/su
// Try fetching the lines from the clipboard first
var s string
var err error
if isDarwin {
s, err = pbpaste()
} else {
// Read the clipboard, for other platforms
s, err = clip.ReadAll(false) // non-primary clipboard
if err == nil && strings.TrimSpace(s) == "" {
s, err = clip.ReadAll(true) // try the primary clipboard
}
}
if err == nil { // no error
// Make the replacements, then split the text into lines and store it in "copyLines"
*copyLines = strings.Split(opinionatedStringReplacer.Replace(s), "\n")
// Note that control characters are not replaced, they are just not printed.
} else if *firstPasteAction {
missingUtility := false
status.Clear(c, false)
if env.Has("WAYLAND_DISPLAY") && files.WhichCached("wl-paste") == "" { // Wayland + wl-paste not found
status.SetErrorMessage("The wl-paste utility (from wl-clipboard) is missing!")
missingUtility = true
} else if env.Has("DISPLAY") && files.WhichCached("xclip") == "" { // X + xclip not found
status.SetErrorMessage("The xclip utility is missing!")
missingUtility = true
} else if isDarwin && files.WhichCached("pbpaste") == "" { // pbcopy is missing, on macOS
status.SetErrorMessage("The pbpaste utility is missing!")
missingUtility = true
}
if missingUtility && *firstPasteAction {
*firstPasteAction = false
status.Show(c, e)
return // Break instead of pasting from the internal buffer, but only the first time
}
} else {
status.Clear(c, true)
e.redrawCursor.Store(true)
}
// Now check if there is anything to paste
if len(*copyLines) == 0 {
return
}
// Now save the contents to "previousCopyLines" and check if they are the same first
if !equalStringSlices(*copyLines, *previousCopyLines) {
// Start with single-line paste if the contents are new
*lastPasteY = -1
}
*previousCopyLines = *copyLines
// Prepare to paste
undo.Snapshot(e)
y := e.DataY()
// Forget the cut and copy line state
*lastCutY = -1
*lastCopyY = -1
// Redraw after pasting
e.redraw.Store(true)
if *lastPasteY != y { // Single line paste
*lastPasteY = y
// Pressed for the first time for this line number, paste only one line
// (*copyLines)[0] is the line to be pasted, and it exists
if e.EmptyRightTrimmedLine() {
// If the line is empty, use the existing indentation before pasting
e.SetLine(y, e.LeadingWhitespace()+strings.TrimSpace((*copyLines)[0]))
} else {
// If the line is not empty, insert the trimmed string
e.InsertStringAndMove(c, strings.TrimSpace((*copyLines)[0]))
}
} else { // Multi line paste (the rest of the lines)
// Pressed the second time for this line number, paste multiple lines without trimming
var (
firstLine = (*copyLines)[0]
tailLines = (*copyLines)[1:]
tailLineCount = len(tailLines)
// tailLines contains the lines to be pasted, and they are > 1
// the first line is skipped since that was already pasted when ctrl-v was pressed the first time
lastIndex = tailLineCount - 1
// If the first line has been pasted, and return has been pressed, paste the rest of the lines differently
skipFirstLineInsert bool
)
// Consider smart indentation for programming languages
if e.ProgrammingLanguage() || e.mode == mode.Config { // not mode.Ini, since it seldom has indentations
// Indent the block that is about to be pasted to the smart indentation level, if the block had no indentation
if getLeadingWhitespace(firstLine) == "" {
leadingWhitespace := e.LeadingWhitespace()
// add indentation to each line
firstLine = leadingWhitespace + firstLine
for i := 0; i < tailLineCount; i++ {
(*copyLines)[1+i] = leadingWhitespace + (*copyLines)[1+i]
}
}
}
if !prevKeyWasReturn {
// Start by pasting (and overwriting) an untrimmed version of this line,
// if the previous key was not return.
e.SetLine(y, firstLine)
} else if e.EmptyRightTrimmedLine() {
skipFirstLineInsert = true
}
// Then paste the rest of the lines, also untrimmed
for i, line := range tailLines {
if i == lastIndex && len(strings.TrimSpace(line)) == 0 {
// If the last line is blank, skip it
break
}
if skipFirstLineInsert {
skipFirstLineInsert = false
} else {
e.InsertLineBelow()
e.Down(c, nil) // no status message if the end of document is reached, there should always be a new line
}
e.InsertStringAndMove(c, line)
}
if numLines := 1 + tailLineCount; numLines > 1 {
status.SetMessageAfterRedraw(fmt.Sprintf("Pasted %d lines", numLines))
}
}
// Prepare to redraw the text
e.redraw.Store(true)
e.redrawCursor.Store(true)
}