package readline import ( "fmt" "strconv" ) // InputMode - The shell input mode type InputMode int const ( // Vim - Vim editing mode Vim = iota // Emacs - Emacs (classic) editing mode Emacs ) type ViMode int const ( VimInsert ViMode = iota VimReplaceOnce VimReplaceMany VimDelete VimKeys ) var ( VimInsertStr = "[I]" VimReplaceOnceStr = "[V]" VimReplaceManyStr = "[R]" VimDeleteStr = "[D]" VimKeysStr = "[N]" ) var ( // registerFreeKeys - Some Vim keys don't act on/ aren't affected by registers, // and using these keys will automatically cancel any active register. // NOTE: Don't forget to update if you add Vim bindings !! registerFreeKeys = []rune{'a', 'A', 'h', 'i', 'I', 'j', 'k', 'l', 'r', 'R', 'u', 'v', '$', '%', '[', ']'} // validRegisterKeys - All valid register IDs (keys) for read/write Vim registers validRegisterKeys = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/-\"" ) // vi - Apply a key to a Vi action. Note that as in the rest of the code, all cursor movements // have been moved away, and only the rl.pos is adjusted: when echoing the input line, the shell // will compute the new cursor pos accordingly. func (rl *Instance) vi(r rune) { // Check if we are in register mode. If yes, and for some characters, // we select the register and exit this func immediately. if rl.registers.registerSelectWait { for _, char := range validRegisterKeys { if r == char { rl.registers.setActiveRegister(r) return } } } // If we are on register mode and one is already selected, // check if the key stroke to be evaluated is acting on it // or not: if not, we cancel the active register now. if rl.registers.onRegister { for _, char := range registerFreeKeys { if char == r { rl.registers.resetRegister() } } } // Then evaluate the key. switch r { case 'a': if len(rl.line) > 0 { rl.pos++ } rl.modeViMode = VimInsert rl.viIteration = "" rl.viUndoSkipAppend = true case 'A': if len(rl.line) > 0 { rl.pos = len(rl.line) } rl.modeViMode = VimInsert rl.viIteration = "" rl.viUndoSkipAppend = true case 'b': if rl.viIsYanking { vii := rl.getViIterations() rl.saveToRegisterTokenize(tokeniseLine, rl.viJumpB, vii) rl.viIsYanking = false return } rl.viUndoSkipAppend = true vii := rl.getViIterations() for i := 1; i <= vii; i++ { rl.moveCursorByAdjust(rl.viJumpB(tokeniseLine)) } case 'B': if rl.viIsYanking { vii := rl.getViIterations() rl.saveToRegisterTokenize(tokeniseSplitSpaces, rl.viJumpB, vii) rl.viIsYanking = false return } rl.viUndoSkipAppend = true vii := rl.getViIterations() for i := 1; i <= vii; i++ { rl.moveCursorByAdjust(rl.viJumpB(tokeniseSplitSpaces)) } case 'd': rl.modeViMode = VimDelete rl.viUndoSkipAppend = true case 'D': rl.saveBufToRegister(rl.line[rl.pos-1:]) rl.line = rl.line[:rl.pos] // Only go back if there is an input if len(rl.line) > 0 { rl.pos-- } rl.resetHelpers() rl.updateHelpers() rl.viIteration = "" case 'e': if rl.viIsYanking { vii := rl.getViIterations() rl.saveToRegisterTokenize(tokeniseLine, rl.viJumpE, vii) rl.viIsYanking = false return } rl.viUndoSkipAppend = true vii := rl.getViIterations() for i := 1; i <= vii; i++ { rl.moveCursorByAdjust(rl.viJumpE(tokeniseLine)) } case 'E': if rl.viIsYanking { vii := rl.getViIterations() rl.saveToRegisterTokenize(tokeniseSplitSpaces, rl.viJumpE, vii) rl.viIsYanking = false return } rl.viUndoSkipAppend = true vii := rl.getViIterations() for i := 1; i <= vii; i++ { rl.moveCursorByAdjust(rl.viJumpE(tokeniseSplitSpaces)) } case 'h': if rl.pos > 0 { rl.pos-- } rl.viUndoSkipAppend = true case 'i': rl.modeViMode = VimInsert rl.viIteration = "" rl.viUndoSkipAppend = true rl.registers.resetRegister() case 'I': rl.modeViMode = VimInsert rl.viIteration = "" rl.viUndoSkipAppend = true rl.pos = 0 case 'j': // Set the main history as the one we navigate, by default rl.mainHist = true rl.walkHistory(-1) case 'k': // Set the main history as the one we navigate, by default rl.mainHist = true rl.walkHistory(1) case 'l': if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) || (rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) { rl.pos++ } rl.viUndoSkipAppend = true case 'p': // paste after the cursor position rl.viUndoSkipAppend = true rl.pos++ buffer := rl.pasteFromRegister() vii := rl.getViIterations() for i := 1; i <= vii; i++ { rl.insert(buffer) } rl.pos-- case 'P': // paste before rl.viUndoSkipAppend = true buffer := rl.pasteFromRegister() vii := rl.getViIterations() for i := 1; i <= vii; i++ { rl.insert(buffer) } case 'r': rl.modeViMode = VimReplaceOnce rl.viIteration = "" rl.viUndoSkipAppend = true case 'R': rl.modeViMode = VimReplaceMany rl.viIteration = "" rl.viUndoSkipAppend = true case 'u': rl.undoLast() rl.viUndoSkipAppend = true case 'v': rl.clearHelpers() var multiline []rune if rl.GetMultiLine == nil { multiline = rl.line } else { multiline = rl.GetMultiLine(rl.line) } // Keep the previous cursor position prev := rl.pos new, err := rl.StartEditorWithBuffer(multiline, "") if err != nil || len(new) == 0 || string(new) == string(multiline) { fmt.Println(err) rl.viUndoSkipAppend = true return } // Clean the shell and put the new buffer, with adjusted pos if needed. rl.clearLine() rl.line = new if prev > len(rl.line) { rl.pos = len(rl.line) - 1 } else { rl.pos = prev } case 'w': // If we were not yanking rl.viUndoSkipAppend = true // If the input line is empty, we don't do anything if rl.pos == 0 && len(rl.line) == 0 { return } // If we were yanking, we forge the new yank buffer // and return without moving the cursor. if rl.viIsYanking { vii := rl.getViIterations() rl.saveToRegisterTokenize(tokeniseLine, rl.viJumpW, vii) rl.viIsYanking = false return } // Else get iterations and move vii := rl.getViIterations() for i := 1; i <= vii; i++ { rl.moveCursorByAdjust(rl.viJumpW(tokeniseLine)) } case 'W': // If the input line is empty, we don't do anything if rl.pos == 0 && len(rl.line) == 0 { return } rl.viUndoSkipAppend = true if rl.viIsYanking { vii := rl.getViIterations() rl.saveToRegisterTokenize(tokeniseSplitSpaces, rl.viJumpW, vii) rl.viIsYanking = false return } vii := rl.getViIterations() for i := 1; i <= vii; i++ { rl.moveCursorByAdjust(rl.viJumpW(tokeniseSplitSpaces)) } case 'x': vii := rl.getViIterations() // We might be on an active register, but not yanking... rl.saveToRegister(vii) // Delete the chars in the line anyway for i := 1; i <= vii; i++ { rl.deleteX() } if rl.pos == len(rl.line) && len(rl.line) > 0 { rl.pos-- } case 'y': if rl.viIsYanking { rl.saveBufToRegister(rl.line) rl.viIsYanking = false } rl.viIsYanking = true rl.viUndoSkipAppend = true case 'Y': rl.saveBufToRegister(rl.line) rl.viUndoSkipAppend = true case '[': if rl.viIsYanking { rl.saveToRegister(rl.viJumpPreviousBrace()) rl.viIsYanking = false return } rl.viUndoSkipAppend = true rl.moveCursorByAdjust(rl.viJumpPreviousBrace()) case ']': if rl.viIsYanking { rl.saveToRegister(rl.viJumpNextBrace()) rl.viIsYanking = false return } rl.viUndoSkipAppend = true rl.moveCursorByAdjust(rl.viJumpNextBrace()) case '$': if rl.viIsYanking { rl.saveBufToRegister(rl.line[rl.pos:]) rl.viIsYanking = false return } rl.pos = len(rl.line) rl.viUndoSkipAppend = true case '%': if rl.viIsYanking { rl.saveToRegister(rl.viJumpBracket()) rl.viIsYanking = false return } rl.viUndoSkipAppend = true rl.moveCursorByAdjust(rl.viJumpBracket()) case '"': // We might be on a register already, so reset it, // and then wait again for a new register ID. if rl.registers.onRegister { rl.registers.resetRegister() } rl.registers.registerSelectWait = true default: if r <= '9' && '0' <= r { rl.viIteration += string(r) } rl.viUndoSkipAppend = true } } func (rl *Instance) getViIterations() int { i, _ := strconv.Atoi(rl.viIteration) if i < 1 { i = 1 } rl.viIteration = "" return i } func (rl *Instance) refreshVimStatus() { rl.ViModeCallback(rl.modeViMode) rl.computePrompt() rl.updateHelpers() } // viHintMessage - lmorg's way of showing Vim status is to overwrite the hint. // Currently not used, as there is a possibility to show the current Vim mode in the prompt. func (rl *Instance) viHintMessage() { switch rl.modeViMode { case VimKeys: rl.hintText = []rune("-- VIM KEYS -- (press `i` to return to normal editing mode)") case VimInsert: rl.hintText = []rune("-- INSERT --") case VimReplaceOnce: rl.hintText = []rune("-- REPLACE CHARACTER --") case VimReplaceMany: rl.hintText = []rune("-- REPLACE --") case VimDelete: rl.hintText = []rune("-- DELETE --") default: rl.getHintText() } rl.clearHelpers() rl.renderHelpers() } func (rl *Instance) viJumpB(tokeniser tokeniser) (adjust int) { split, index, pos := tokeniser(rl.line, rl.pos) switch { case len(split) == 0: return case index == 0 && pos == 0: return case pos == 0: adjust = len(split[index-1]) default: adjust = pos } return adjust * -1 } func (rl *Instance) viJumpE(tokeniser tokeniser) (adjust int) { split, index, pos := tokeniser(rl.line, rl.pos) if len(split) == 0 { return } word := rTrimWhiteSpace(split[index]) switch { case len(split) == 0: return case index == len(split)-1 && pos >= len(word)-1: return case pos >= len(word)-1: word = rTrimWhiteSpace(split[index+1]) adjust = len(split[index]) - pos adjust += len(word) - 1 default: adjust = len(word) - pos - 1 } return } func (rl *Instance) viJumpW(tokeniser tokeniser) (adjust int) { split, index, pos := tokeniser(rl.line, rl.pos) switch { case len(split) == 0: return case index+1 == len(split): adjust = len(rl.line) - rl.pos default: adjust = len(split[index]) - pos } return } func (rl *Instance) viJumpPreviousBrace() (adjust int) { if rl.pos == 0 { return 0 } for i := rl.pos - 1; i != 0; i-- { if rl.line[i] == '{' { return i - rl.pos } } return 0 } func (rl *Instance) viJumpNextBrace() (adjust int) { if rl.pos >= len(rl.line)-1 { return 0 } for i := rl.pos + 1; i < len(rl.line); i++ { if rl.line[i] == '{' { return i - rl.pos } } return 0 } func (rl *Instance) viJumpBracket() (adjust int) { split, index, pos := tokeniseBrackets(rl.line, rl.pos) switch { case len(split) == 0: return case pos == 0: adjust = len(split[index]) default: adjust = pos * -1 } return }