package buffer import "unicode" // Cursor returns where the insertion point currently is. func (b *Buffer) Cursor() Position { return b.cursor } // SetCursor moves the insertion point, clamping it to a position that exists, // and drops the selection — that is what an unmodified arrow key does. Use // SetCursorKeepingSelection for Shift-arrow and for a mouse drag. // // Any pending run of typed characters stops merging into one undo step here, // because a deliberate move ends the "word" being typed. // // b := buffer.NewFromString("hello") // b.SetCursor(buffer.Position{Line: 0, Col: 99}) // fmt.Println(b.Cursor().Col) // 5 func (b *Buffer) SetCursor(p Position) { b.cursor = b.clamp(p) b.anchor = nil b.coalesce = false } // MoveLeft steps one character back, wrapping onto the end of the previous // line. It is a no-op at the very start of the buffer. func (b *Buffer) MoveLeft() { switch { case b.cursor.Col > 0: b.SetCursor(Position{Line: b.cursor.Line, Col: b.cursor.Col - 1}) case b.cursor.Line > 0: prev := b.cursor.Line - 1 b.SetCursor(Position{Line: prev, Col: len(b.lines[prev])}) } } // MoveRight steps one character forward, wrapping onto the start of the next // line. It is a no-op at the very end of the buffer. func (b *Buffer) MoveRight() { switch { case b.cursor.Col < len(b.lines[b.cursor.Line]): b.SetCursor(Position{Line: b.cursor.Line, Col: b.cursor.Col + 1}) case b.cursor.Line < len(b.lines)-1: b.SetCursor(Position{Line: b.cursor.Line + 1, Col: 0}) } } // MoveUp steps to the previous line, keeping the column where it fits. func (b *Buffer) MoveUp() { b.SetCursor(Position{Line: b.cursor.Line - 1, Col: b.cursor.Col}) } // MoveDown steps to the next line, keeping the column where it fits. func (b *Buffer) MoveDown() { b.SetCursor(Position{Line: b.cursor.Line + 1, Col: b.cursor.Col}) } // MoveLineStart goes to column zero of the current line. func (b *Buffer) MoveLineStart() { b.SetCursor(Position{Line: b.cursor.Line, Col: 0}) } // MoveLineEnd goes just past the last character of the current line. func (b *Buffer) MoveLineEnd() { b.SetCursor(Position{Line: b.cursor.Line, Col: len(b.lines[b.cursor.Line])}) } // MoveBufferStart goes to the very first position of the text. func (b *Buffer) MoveBufferStart() { b.SetCursor(Position{}) } // MoveBufferEnd goes just past the very last character of the text. func (b *Buffer) MoveBufferEnd() { b.SetCursor(b.wholeRange().End) } // MoveWordLeft goes to the start of the word before the cursor, skipping over // any separators in between, as Ctrl-Left does in Turbo C. func (b *Buffer) MoveWordLeft() { if b.cursor.Col == 0 { b.MoveLeft() // wrap onto the end of the previous line, or stay put return } line := b.lines[b.cursor.Line] col := b.cursor.Col for col > 0 && !IsWordRune(line[col-1]) { col-- } for col > 0 && IsWordRune(line[col-1]) { col-- } b.SetCursor(Position{Line: b.cursor.Line, Col: col}) } // MoveWordRight goes to the start of the word after the cursor, as Ctrl-Right // does in Turbo C. func (b *Buffer) MoveWordRight() { line := b.lines[b.cursor.Line] if b.cursor.Col >= len(line) { b.MoveRight() return } col := b.cursor.Col for col < len(line) && IsWordRune(line[col]) { col++ } for col < len(line) && !IsWordRune(line[col]) { col++ } b.SetCursor(Position{Line: b.cursor.Line, Col: col}) } // IsWordRune reports whether r belongs to an identifier. // // Word movement treats a run of them as one unit, and so does the completion // popup when it works out which word is being typed — which is why this is // exported rather than private to the cursor. // // buffer.IsWordRune('_') // true // buffer.IsWordRune('.') // false func IsWordRune(r rune) bool { return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r) } // DisplayColumn converts a rune column on line into the screen column it // occupies once tabs are expanded. // // b := buffer.NewFromString("\tx") // fmt.Println(b.DisplayColumn(0, 1)) // 8, with the default tab width func (b *Buffer) DisplayColumn(line, col int) int { runes := b.lineAt(line) if col < 0 { col = 0 } if col > len(runes) { col = len(runes) } screen := 0 for _, r := range runes[:col] { if r == '\t' { screen += b.tabWidth - screen%b.tabWidth } else { screen++ } } return screen } // RuneColumn is the inverse of DisplayColumn: it maps a screen column back to // the rune column the cursor should sit at, which is what a mouse click needs. func (b *Buffer) RuneColumn(line, screenCol int) int { runes := b.lineAt(line) screen := 0 for i, r := range runes { width := 1 if r == '\t' { width = b.tabWidth - screen%b.tabWidth } // A click in the second half of a wide tab lands on the next rune. if screenCol < screen+width { return i } screen += width } return len(runes) }