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") }