302 lines
No EOL
6 KiB
Go
Executable file
302 lines
No EOL
6 KiB
Go
Executable file
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// === ПОИСК В СТРОКЕ (f/t команды) ===
|
||
|
||
func (m *EditorModel) findCharInLine(char rune, forward bool) {
|
||
line := []rune(m.lines[m.cursorY])
|
||
|
||
if forward {
|
||
for i := m.cursorX + 1; i < len(line); i++ {
|
||
if line[i] == char {
|
||
m.cursorX = i
|
||
m.lastFindChar = char
|
||
m.lastFindIsForward = true
|
||
return
|
||
}
|
||
}
|
||
} else {
|
||
for i := m.cursorX - 1; i >= 0; i-- {
|
||
if line[i] == char {
|
||
m.cursorX = i
|
||
m.lastFindChar = char
|
||
m.lastFindIsForward = false
|
||
return
|
||
}
|
||
}
|
||
}
|
||
m.SetStatusMsg("Character not found")
|
||
}
|
||
|
||
func (m *EditorModel) findCharToLine(char rune, forward bool) {
|
||
line := []rune(m.lines[m.cursorY])
|
||
|
||
if forward {
|
||
for i := m.cursorX + 1; i < len(line); i++ {
|
||
if line[i] == char {
|
||
if i > 0 {
|
||
m.cursorX = i - 1
|
||
}
|
||
m.lastFindChar = char
|
||
m.lastFindIsForward = true
|
||
return
|
||
}
|
||
}
|
||
} else {
|
||
for i := m.cursorX - 1; i >= 0; i-- {
|
||
if line[i] == char {
|
||
if i < len(line)-1 {
|
||
m.cursorX = i + 1
|
||
}
|
||
m.lastFindChar = char
|
||
m.lastFindIsForward = false
|
||
return
|
||
}
|
||
}
|
||
}
|
||
m.SetStatusMsg("Character not found")
|
||
}
|
||
|
||
func (m *EditorModel) repeatLastFind(reverse bool) {
|
||
if m.lastFindChar == 0 {
|
||
m.SetStatusMsg("No previous search")
|
||
return
|
||
}
|
||
forward := m.lastFindIsForward
|
||
if reverse {
|
||
forward = !forward
|
||
}
|
||
m.findCharInLine(m.lastFindChar, forward)
|
||
}
|
||
|
||
// === ГЛОБАЛЬНЫЙ ПОИСК (/pattern) ===
|
||
|
||
func (m *EditorModel) startSearch() {
|
||
m.mode = SEARCH
|
||
m.searchBuffer = ""
|
||
m.searchMatches = nil
|
||
m.searchIdx = 0
|
||
m.SetStatusMsg("/")
|
||
}
|
||
|
||
// maxSearchMatches — максимум совпадений при поиске
|
||
// Предотвращает зависание на файлах с сотнями тысяч строк
|
||
const maxSearchMatches = 1000
|
||
|
||
func (m *EditorModel) performSearch(pattern string) {
|
||
m.lastSearchTerm = pattern
|
||
m.searchMatches = nil
|
||
m.searchIdx = 0
|
||
|
||
if pattern == "" {
|
||
return
|
||
}
|
||
|
||
patRunes := []rune(pattern)
|
||
truncated := false
|
||
|
||
for lineNum, line := range m.lines {
|
||
lineRunes := []rune(line)
|
||
for i := 0; i <= len(lineRunes)-len(patRunes); i++ {
|
||
match := true
|
||
for j, r := range patRunes {
|
||
if lineRunes[i+j] != r {
|
||
match = false
|
||
break
|
||
}
|
||
}
|
||
if match {
|
||
m.searchMatches = append(m.searchMatches, struct{ line, col int }{lineNum, i})
|
||
if len(m.searchMatches) >= maxSearchMatches {
|
||
truncated = true
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if truncated {
|
||
break
|
||
}
|
||
}
|
||
|
||
if len(m.searchMatches) > 0 {
|
||
match := m.searchMatches[0]
|
||
m.cursorY = match.line
|
||
m.cursorX = match.col
|
||
m.searchIdx = 0
|
||
if truncated {
|
||
m.SetStatusMsg(fmt.Sprintf("Found %d+ matches (showing first %d): %s",
|
||
maxSearchMatches, maxSearchMatches, pattern))
|
||
} else {
|
||
m.SetStatusMsg(fmt.Sprintf("Found %d match(es): %s", len(m.searchMatches), pattern))
|
||
}
|
||
} else {
|
||
m.SetStatusMsg("Pattern not found: " + pattern)
|
||
}
|
||
}
|
||
|
||
func (m *EditorModel) nextMatch() {
|
||
if len(m.searchMatches) == 0 {
|
||
return
|
||
}
|
||
m.searchIdx = (m.searchIdx + 1) % len(m.searchMatches)
|
||
match := m.searchMatches[m.searchIdx]
|
||
m.cursorY = match.line
|
||
m.cursorX = match.col
|
||
}
|
||
|
||
func (m *EditorModel) prevMatch() {
|
||
if len(m.searchMatches) == 0 {
|
||
return
|
||
}
|
||
m.searchIdx--
|
||
if m.searchIdx < 0 {
|
||
m.searchIdx = len(m.searchMatches) - 1
|
||
}
|
||
match := m.searchMatches[m.searchIdx]
|
||
m.cursorY = match.line
|
||
m.cursorX = match.col
|
||
}
|
||
|
||
// === ЗАМЕНА ===
|
||
|
||
func (m *EditorModel) replaceAll(old, new string) int {
|
||
if old == "" {
|
||
return 0
|
||
}
|
||
count := 0
|
||
for i, line := range m.lines {
|
||
newLine := strings.ReplaceAll(line, old, new)
|
||
if newLine != line {
|
||
count += strings.Count(line, old)
|
||
m.lines[i] = newLine
|
||
m.MarkDirty()
|
||
}
|
||
}
|
||
return count
|
||
}
|
||
|
||
func (m *EditorModel) replaceInLine(old, new string) int {
|
||
if old == "" || m.cursorY >= len(m.lines) {
|
||
return 0
|
||
}
|
||
line := m.lines[m.cursorY]
|
||
newLine := strings.Replace(line, old, new, 1)
|
||
if newLine != line {
|
||
m.lines[m.cursorY] = newLine
|
||
m.MarkDirty()
|
||
return 1
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// === ЧИСЛОВЫЕ ПРЕФИКСЫ ===
|
||
|
||
func (m *EditorModel) parseNumberPrefix() int {
|
||
if m.numPrefix == "" {
|
||
return 1
|
||
}
|
||
result := 0
|
||
for _, ch := range m.numPrefix {
|
||
result = result*10 + int(ch-'0')
|
||
}
|
||
m.numPrefix = ""
|
||
return result
|
||
}
|
||
|
||
// === НАВИГАЦИЯ ===
|
||
|
||
func (m *EditorModel) moveByWords(count int, forward bool) {
|
||
for i := 0; i < count; i++ {
|
||
if forward {
|
||
m.moveNextWord()
|
||
} else {
|
||
m.movePrevWord()
|
||
}
|
||
}
|
||
}
|
||
|
||
func (m *EditorModel) moveByLines(count int, direction int) {
|
||
m.cursorY += count * direction
|
||
m.CursorWithinBounds()
|
||
}
|
||
|
||
// findMatchingBracket — поиск парной скобки (%)
|
||
func (m *EditorModel) findMatchingBracket() {
|
||
if m.cursorY >= len(m.lines) {
|
||
return
|
||
}
|
||
line := []rune(m.lines[m.cursorY])
|
||
if m.cursorX >= len(line) {
|
||
return
|
||
}
|
||
|
||
char := line[m.cursorX]
|
||
var opening, closing rune
|
||
var isOpening bool
|
||
|
||
switch char {
|
||
case '(':
|
||
opening, closing, isOpening = '(', ')', true
|
||
case ')':
|
||
opening, closing, isOpening = '(', ')', false
|
||
case '[':
|
||
opening, closing, isOpening = '[', ']', true
|
||
case ']':
|
||
opening, closing, isOpening = '[', ']', false
|
||
case '{':
|
||
opening, closing, isOpening = '{', '}', true
|
||
case '}':
|
||
opening, closing, isOpening = '{', '}', false
|
||
default:
|
||
return
|
||
}
|
||
|
||
if isOpening {
|
||
count := 1
|
||
for y := m.cursorY; y < len(m.lines); y++ {
|
||
lr := []rune(m.lines[y])
|
||
startX := 0
|
||
if y == m.cursorY {
|
||
startX = m.cursorX + 1
|
||
}
|
||
for x := startX; x < len(lr); x++ {
|
||
if lr[x] == opening {
|
||
count++
|
||
} else if lr[x] == closing {
|
||
count--
|
||
if count == 0 {
|
||
m.cursorY = y
|
||
m.cursorX = x
|
||
return
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
count := 1
|
||
for y := m.cursorY; y >= 0; y-- {
|
||
lr := []rune(m.lines[y])
|
||
endX := len(lr)
|
||
if y == m.cursorY {
|
||
endX = m.cursorX
|
||
}
|
||
for x := endX - 1; x >= 0; x-- {
|
||
if lr[x] == closing {
|
||
count++
|
||
} else if lr[x] == opening {
|
||
count--
|
||
if count == 0 {
|
||
m.cursorY = y
|
||
m.cursorX = x
|
||
return
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
m.SetStatusMsg("No matching bracket")
|
||
} |