github.com/hoop33/elvish@v0.0.0-20160801152013-6d25485beab4/edit/insert.go (about) 1 package edit 2 3 import ( 4 "io" 5 "strings" 6 "unicode" 7 "unicode/utf8" 8 9 "github.com/elves/elvish/util" 10 ) 11 12 // Builtins related to insert and command mode. 13 14 type insert struct { 15 quotePaste bool 16 // The number of consecutive key inserts. Used for abbreviation expansion. 17 literalInserts int 18 // Indicates whether a key was inserted (via insert-default). A hack for 19 // maintaining the inserts field. 20 insertedLiteral bool 21 } 22 23 func (*insert) Mode() ModeType { 24 return modeInsert 25 } 26 27 // Insert mode is the default mode and has an empty mode. 28 func (ins *insert) ModeLine(width int) *buffer { 29 if ins.quotePaste { 30 return makeModeLine(" INSERT (quote paste) ", width) 31 } 32 return nil 33 } 34 35 type command struct{} 36 37 func (*command) Mode() ModeType { 38 return modeCommand 39 } 40 41 func (*command) ModeLine(width int) *buffer { 42 return makeModeLine(" COMMAND ", width) 43 } 44 45 func startInsert(ed *Editor) { 46 ed.mode = &ed.insert 47 } 48 49 func startCommand(ed *Editor) { 50 ed.mode = &ed.command 51 } 52 53 func killLineLeft(ed *Editor) { 54 sol := util.FindLastSOL(ed.line[:ed.dot]) 55 ed.line = ed.line[:sol] + ed.line[ed.dot:] 56 ed.dot = sol 57 } 58 59 func killLineRight(ed *Editor) { 60 eol := util.FindFirstEOL(ed.line[ed.dot:]) + ed.dot 61 ed.line = ed.line[:ed.dot] + ed.line[eol:] 62 } 63 64 // NOTE(xiaq): A word is a run of non-space runes. When killing a word, 65 // trimming spaces are removed as well. Examples: 66 // "abc xyz" -> "abc ", "abc xyz " -> "abc ". 67 68 func killWordLeft(ed *Editor) { 69 if ed.dot == 0 { 70 return 71 } 72 space := strings.LastIndexFunc( 73 strings.TrimRightFunc(ed.line[:ed.dot], unicode.IsSpace), 74 unicode.IsSpace) + 1 75 ed.line = ed.line[:space] + ed.line[ed.dot:] 76 ed.dot = space 77 } 78 79 // NOTE(xiaq): A small word is either a run of alphanumeric (Unicode category L 80 // or N) runes or a run of non-alphanumeric runes. This is consistent with vi's 81 // definition of word, except that "_" is not considered alphanumeric. When 82 // killing a small word, trimming spaces are removed as well. Examples: 83 // "abc/~" -> "abc", "~/abc" -> "~/", "abc* " -> "abc" 84 85 func killSmallWordLeft(ed *Editor) { 86 left := strings.TrimRightFunc(ed.line[:ed.dot], unicode.IsSpace) 87 // The case of left == "" is handled as well. 88 r, _ := utf8.DecodeLastRuneInString(left) 89 if isAlnum(r) { 90 left = strings.TrimRightFunc(left, isAlnum) 91 } else { 92 left = strings.TrimRightFunc( 93 left, func(r rune) bool { return !isAlnum(r) }) 94 } 95 ed.line = left + ed.line[ed.dot:] 96 ed.dot = len(left) 97 } 98 99 func isAlnum(r rune) bool { 100 return unicode.IsLetter(r) || unicode.IsNumber(r) 101 } 102 103 func killRuneLeft(ed *Editor) { 104 if ed.dot > 0 { 105 _, w := utf8.DecodeLastRuneInString(ed.line[:ed.dot]) 106 ed.line = ed.line[:ed.dot-w] + ed.line[ed.dot:] 107 ed.dot -= w 108 } else { 109 ed.flash() 110 } 111 } 112 113 func killRuneRight(ed *Editor) { 114 if ed.dot < len(ed.line) { 115 _, w := utf8.DecodeRuneInString(ed.line[ed.dot:]) 116 ed.line = ed.line[:ed.dot] + ed.line[ed.dot+w:] 117 } else { 118 ed.flash() 119 } 120 } 121 122 func moveDotLeft(ed *Editor) { 123 _, w := utf8.DecodeLastRuneInString(ed.line[:ed.dot]) 124 ed.dot -= w 125 } 126 127 func moveDotRight(ed *Editor) { 128 _, w := utf8.DecodeRuneInString(ed.line[ed.dot:]) 129 ed.dot += w 130 } 131 132 func moveDotLeftWord(ed *Editor) { 133 if ed.dot == 0 { 134 return 135 } 136 space := strings.LastIndexFunc( 137 strings.TrimRightFunc(ed.line[:ed.dot], unicode.IsSpace), 138 unicode.IsSpace) + 1 139 ed.dot = space 140 } 141 142 func moveDotRightWord(ed *Editor) { 143 // Move to first space 144 p := strings.IndexFunc(ed.line[ed.dot:], unicode.IsSpace) 145 if p == -1 { 146 ed.dot = len(ed.line) 147 return 148 } 149 ed.dot += p 150 // Move to first nonspace 151 p = strings.IndexFunc(ed.line[ed.dot:], notSpace) 152 if p == -1 { 153 ed.dot = len(ed.line) 154 return 155 } 156 ed.dot += p 157 } 158 159 func notSpace(r rune) bool { 160 return !unicode.IsSpace(r) 161 } 162 163 func moveDotSOL(ed *Editor) { 164 sol := util.FindLastSOL(ed.line[:ed.dot]) 165 ed.dot = sol 166 } 167 168 func moveDotEOL(ed *Editor) { 169 eol := util.FindFirstEOL(ed.line[ed.dot:]) + ed.dot 170 ed.dot = eol 171 } 172 173 func moveDotUp(ed *Editor) { 174 sol := util.FindLastSOL(ed.line[:ed.dot]) 175 if sol == 0 { 176 ed.flash() 177 return 178 } 179 prevEOL := sol - 1 180 prevSOL := util.FindLastSOL(ed.line[:prevEOL]) 181 width := WcWidths(ed.line[sol:ed.dot]) 182 ed.dot = prevSOL + len(TrimWcWidth(ed.line[prevSOL:prevEOL], width)) 183 } 184 185 func moveDotDown(ed *Editor) { 186 eol := util.FindFirstEOL(ed.line[ed.dot:]) + ed.dot 187 if eol == len(ed.line) { 188 ed.flash() 189 return 190 } 191 nextSOL := eol + 1 192 nextEOL := util.FindFirstEOL(ed.line[nextSOL:]) + nextSOL 193 sol := util.FindLastSOL(ed.line[:ed.dot]) 194 width := WcWidths(ed.line[sol:ed.dot]) 195 ed.dot = nextSOL + len(TrimWcWidth(ed.line[nextSOL:nextEOL], width)) 196 } 197 198 func insertLastWord(ed *Editor) { 199 if ed.store == nil { 200 ed.addTip("store offline") 201 return 202 } 203 _, lastLine, err := ed.store.LastCmd(-1, "", true) 204 if err == nil { 205 ed.insertAtDot(lastWord(lastLine)) 206 } else { 207 ed.addTip("db error: %s", err.Error()) 208 } 209 } 210 211 func lastWord(s string) string { 212 s = strings.TrimRightFunc(s, unicode.IsSpace) 213 i := strings.LastIndexFunc(s, unicode.IsSpace) + 1 214 return s[i:] 215 } 216 217 func insertKey(ed *Editor) { 218 k := ed.lastKey 219 ed.insertAtDot(string(k.Rune)) 220 } 221 222 func returnLine(ed *Editor) { 223 ed.nextAction = action{typ: exitReadLine, returnLine: ed.line} 224 } 225 226 func smartEnter(ed *Editor) { 227 if ed.parseErrorAtEnd { 228 // There is a parsing error at the end. Insert a newline and copy 229 // indents from previous line. 230 indent := findLastIndent(ed.line[:ed.dot]) 231 ed.insertAtDot("\n" + indent) 232 } else { 233 returnLine(ed) 234 } 235 } 236 237 func findLastIndent(s string) string { 238 line := s[util.FindLastSOL(s):] 239 trimmed := strings.TrimLeft(line, " \t") 240 return line[:len(line)-len(trimmed)] 241 } 242 243 func returnEOF(ed *Editor) { 244 if len(ed.line) == 0 { 245 ed.nextAction = action{typ: exitReadLine, returnErr: io.EOF} 246 } 247 } 248 249 func toggleQuotePaste(ed *Editor) { 250 ed.insert.quotePaste = !ed.insert.quotePaste 251 } 252 253 func defaultInsert(ed *Editor) { 254 k := ed.lastKey 255 if likeChar(k) { 256 insertKey(ed) 257 // Match abbreviations. 258 literals := ed.line[ed.dot-ed.insert.literalInserts-1 : ed.dot] 259 for abbr, full := range ed.abbreviations { 260 if strings.HasSuffix(literals, abbr) { 261 ed.line = ed.line[:ed.dot-len(abbr)] + full + ed.line[ed.dot:] 262 ed.dot += len(full) - len(abbr) 263 return 264 } 265 } 266 // No match. 267 ed.insert.insertedLiteral = true 268 } else { 269 ed.notify("Unbound: %s", k) 270 } 271 } 272 273 func defaultCommand(ed *Editor) { 274 k := ed.lastKey 275 ed.notify("Unbound: %s", k) 276 } 277 278 // likeChar returns if a key looks like a character meant to be input (as 279 // opposed to a function key). 280 func likeChar(k Key) bool { 281 return k.Mod == 0 && k.Rune > 0 && unicode.IsGraphic(k.Rune) 282 }