VIM-like-text-editor/mvc/models.py

377 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import tools
from enum import Enum
class ReturnCode(Enum):
GOOD = -101
EXIT_CODE = -100
SET_BASIC_MODE = -99
SET_COMMAND_MODE = -98
SET_EDIT_MODE = -97
CONTINUE = -96
class VimModel:
def __init__(self, displayLinesCount: int, displayColsCount: int):
self.observers = []
self.displayLinesCount = displayLinesCount
self.displayColsCount = displayColsCount
self.showLineNumbers = True
self.lastSearch = ()
self.displayBuffer = [] # буфер для хранения всех строк
self.dump = []
self.currentLine = 0 # текущий индекс строки
self.currentCol = 0 # текущий индекс колонки
self.scrollY = 0 # вертикальная прокрутка
self.scrollX = 0 # горизонтальная прокрутка
self.file_path = "" # путь к файлу
self.mode = ""
self.inputAfterCursor = False
self.keyLog = [] # лог нажатий клавиш (кортежи вида (код символа, юникс время нажатия))
self.commandBuffer = [] # буффер для команды
self.exchangeBuffer = [] # буффер обмена
def attach(self, observer):
self.observers.append(observer)
def detach(self, observer):
self.observers.remove(observer)
def notify(self):
for observer in self.observers:
observer.Update()
def InputAfterCursor(self) -> None:
self.inputAfterCursor = not self.inputAfterCursor
def UpdateKeyLog(self, symbolCode: int) -> None:
if len(self.keyLog) > 5000: self.keyLog.clear()
self.keyLog.append((symbolCode, tools.UnixSec()))
def CombinationCheck(self, comb: str, interval: int) -> None:
"""Проверяет была ли нажата комбинация клавиш.
Принимает фактический ввод, команду с которой сравниваем и интервал времени на команду"""
if len(self.keyLog) > len(comb) - 1:
mbComb = self.keyLog[-len(comb):]
if all(chr(mbComb[i][0]) == char for i, char in enumerate(comb)) \
and mbComb[len(mbComb)-1][1] - mbComb[0][1] < interval:
return True
return False
def ModeBar(self) -> str:
modeBar = f"MODE: {self.mode} | FILE: {self.file_path} | LINE: {self.currentLine+1}/{len(self.displayBuffer)}"
if self.mode == "COMMAND":
modeBar += f" | COMMAND BUFFER: {''.join(self.commandBuffer)}"
return modeBar
def Scroll(self) -> None:
# Учитываем ширину нумерации строк (6 символов + 1 пробел)
line_number_width = 7 if self.showLineNumbers else 0
# Вертикальная прокрутка
if self.currentLine < self.scrollY:
self.scrollY = self.currentLine
elif self.currentLine >= self.scrollY + self.displayLinesCount - 1:
self.scrollY = self.currentLine - self.displayLinesCount + 2
# Горизонтальная прокрутка с учетом нумерации строк
# Видимая область для текста уменьшается на ширину нумерации строк
visible_text_width = self.displayColsCount - line_number_width
if self.currentCol < self.scrollX:
self.scrollX = self.currentCol
elif self.currentCol >= self.scrollX + visible_text_width:
self.scrollX = self.currentCol - visible_text_width + 1
def InsertCommandSymbol(self, symbolCode: int) -> None:
self.commandBuffer.append(chr(symbolCode))
def InsertSymbol(self, symbolCode: int) -> None:
if self.currentCol <= len(self.displayBuffer[self.currentLine]): # проверяем, не превышает ли индекс колонки длину строки
self.displayBuffer[self.currentLine].insert(self.currentCol, chr(symbolCode))
self.currentCol += 1
def InsertSymbolAfterCursor(self, symbolCode: int) -> None:
if self.currentCol <= len(self.displayBuffer[self.currentLine]): # проверяем, не превышает ли индекс колонки длину строки
self.displayBuffer[self.currentLine].insert(self.currentCol, chr(symbolCode))
def ToLineStart(self) -> None:
self.currentCol = 0
def ToLineEnd(self) -> None:
self.currentCol = len(self.displayBuffer[self.currentLine])
def ToWordEnd(self) -> None:
line = ''.join(self.displayBuffer[self.currentLine])
# Находим ближайший непробельный символ
non_space_index = next((i for i, char in enumerate(line[self.currentCol:]) if char != ' '), None)
if non_space_index is not None:
non_space_index += self.currentCol
right_space_index = line.find(' ', non_space_index)
self.currentCol = right_space_index if right_space_index != -1 else len(line)
def ToWordStart(self) -> None:
line = ''.join(self.displayBuffer[self.currentLine])
non_space_index = next((i for i in range(self.currentCol - 1, -1, -1) if line[i] != ' '), None)
if non_space_index is not None:
left_space_index = line.rfind(' ', 0, non_space_index)
self.currentCol = left_space_index + 1 if left_space_index != -1 else 0
def ToFileEnd(self) -> None:
self.currentLine = len(self.displayBuffer) - 1
self.currentCol = len(self.displayBuffer[self.currentLine])
def ToFileStart(self) -> None:
self.currentLine = 0
self.currentCol = 0
def DeleteNext(self) -> None:
if self.currentCol + 1 < len(self.displayBuffer[self.currentLine]):
del self.displayBuffer[self.currentLine][self.currentCol + 1]
def PageUp(self) -> None:
self.currentCol = 0
if self.currentLine > self.displayLinesCount:
self.currentLine -= self.displayLinesCount - 2
else:
self.currentLine = 0
def PageDown(self) -> None:
self.currentCol = 0
if self.currentLine + self.displayLinesCount < len(self.displayBuffer):
self.currentLine += self.displayLinesCount - 2
else:
self.currentLine = len(self.displayBuffer) - 1
def DeleteWord(self) -> None:
start_index, end_index = self.WordUnderCursor()
line = self.displayBuffer[self.currentLine]
if end_index < len(line) and line[end_index] == ' ':
end_index += 1
self.displayBuffer[self.currentLine] = line[:start_index] + line[end_index:]
self.currentCol = start_index
def CopyWord(self) -> None:
start_index, end_index = self.WordUnderCursor()
line = self.displayBuffer[self.currentLine]
self.exchangeBuffer = line[start_index:end_index+1]
def Paste(self) -> None:
self.displayBuffer[self.currentLine][self.currentCol+1:self.currentCol+1] = self.exchangeBuffer
def CutLine(self) -> None:
self.exchangeBuffer = self.displayBuffer[self.currentLine]
self.displayBuffer[self.currentLine] = []
self.currentCol = 0
def CopyLine(self) -> None:
self.exchangeBuffer = self.displayBuffer[self.currentLine]
def MoveToLine(self, numberLine: int) -> None:
numberLine -= 1
if numberLine >= 0 and numberLine < len(self.displayBuffer):
self.currentLine = numberLine
self.currentCol = 0
def EnterCommand(self):
"""Обработка введенной команды"""
cmd = ''.join(self.commandBuffer)
self.commandBuffer.clear()
match cmd:
case "q": # Выход из программы
if self.displayBuffer == self.dump:
return ReturnCode.EXIT_CODE
case "q!": # Выход без сохранения
return ReturnCode.EXIT_CODE
case "wq!" | "x": # Записать в текущий файл + выйти
self.SaveFile()
return ReturnCode.EXIT_CODE
case "w": # Сохраняет файл
self.SaveFile()
case "i": # Вход в режим редактирования
return ReturnCode.SET_EDIT_MODE
case "I": # Переходит в начало строки и начинает ввод текста
self.currentCol = 0
return ReturnCode.SET_EDIT_MODE
case "A": # Переходит в конец строки и начинает ввод текста
self.currentCol = len(self.displayBuffer[self.currentLine])
return ReturnCode.SET_EDIT_MODE
case "S": # Удаление строки на которой курсор и вход в режим редактирования
self.currentCol = 0
self.displayBuffer[self.currentLine] = []
return ReturnCode.SET_EDIT_MODE
case "o":
self.InputAfterCursor()
return ReturnCode.SET_EDIT_MODE
case "h":
self.LoadFile("config/usage.txt")
return ReturnCode.SET_BASIC_MODE
case "n":
if self.lastSearch != ():
index = tools.findSublistIndex(self.displayBuffer,
list(self.lastSearch[0]),
self.currentLine,
direction=self.lastSearch[1])
if index != -1:
self.currentLine = index
self.currentCol = 0
self.lastSearch = (self.lastSearch[0], self.lastSearch[1])
case "N":
if self.lastSearch != ():
index = tools.findSublistIndex(self.displayBuffer,
list(self.lastSearch[0]),
self.currentLine,
direction=(self.lastSearch[1]+1)%2)
if index != -1:
self.currentLine = index
self.currentCol = 0
self.lastSearch = (self.lastSearch[0], (self.lastSearch[1]+1)%2)
case "e!":
self.displayBuffer = [sublist.copy() for sublist in self.dump]
case "set num":
self.showLineNumbers = not self.showLineNumbers
case _:
# Открывает файл filename
if len(cmd) > 2 and cmd[:2] == 'o ':
filename = cmd[2:]
self.LoadFile(filename)
return ReturnCode.SET_BASIC_MODE
# Запись в файл filename
elif len(cmd) > 2 and cmd[:2] == 'w ':
filename = cmd[2:]
self.WriteFile(filename)
# Заменяет символ на указанный
elif len(cmd) == 3 and cmd[:2] == 'r ':
self.displayBuffer[self.currentLine][self.currentCol] = cmd[2]
# Переход на строку по введенному номеру
elif cmd.isdigit():
self.MoveToLine(int(cmd))
# Поиск строки от курсора до конца файла
elif len(cmd) > 1 and cmd[0] == '/':
index = tools.findSublistIndex(self.displayBuffer,
list(cmd[1:]),
self.currentLine,
direction=1)
if index != -1:
self.currentLine = index
self.currentCol = 0
self.lastSearch = (cmd[1:], 1)
# Поиск строки от курсора до начала файла
elif len(cmd) > 1 and cmd[0] == '?':
index = tools.findSublistIndex(self.displayBuffer,
list(cmd[1:]),
self.currentLine,
direction=0)
if index != -1:
self.currentLine = index
self.currentCol = 0
self.lastSearch = (cmd[1:], 0)
return ReturnCode.GOOD
def WordUnderCursor(self)-> tuple[int, int]:
"""Возвращает индекс начала и индекс конца слова под курсором"""
line = ''.join(self.displayBuffer[self.currentLine])
start_index = line.rfind(' ', 0, self.currentCol)
start_index = 0 if start_index == -1 else start_index + 1
end_index = line.find(' ', self.currentCol)
end_index = len(line) if end_index == -1 else end_index
return start_index, end_index
def Enter(self) -> None:
# Разделяем текущую строку на две части
new_line = self.displayBuffer[self.currentLine][self.currentCol:]
self.displayBuffer[self.currentLine] = self.displayBuffer[self.currentLine][:self.currentCol]
self.currentLine += 1 # Переходим на следующую строку
self.displayBuffer.insert(self.currentLine, new_line) # Вставляем новую строку
self.currentCol = 0 # Сбрасываем индекс колонки
def BackspaceCommand(self) -> None:
if len(self.commandBuffer) > 0:
self.commandBuffer.pop()
def Backspace(self) -> None:
if self.currentCol > 0: # Если символ существует в текущей строке
self.currentCol -= 1
del self.displayBuffer[self.currentLine][self.currentCol] # Удаляем символ
elif self.currentLine > 0: # Если текущая строка не первая
# Объединяем текущую строку с предыдущей
prev_line_length = len(self.displayBuffer[self.currentLine - 1])
self.displayBuffer[self.currentLine - 1].extend(self.displayBuffer[self.currentLine])
del self.displayBuffer[self.currentLine]
self.currentLine -= 1
self.currentCol = prev_line_length # Переходим в конец предыдущей строки
def MoveLeft(self) -> None:
if self.currentCol > 0:
self.currentCol -= 1
elif self.currentLine > 0:
self.currentLine -= 1
self.currentCol = len(self.displayBuffer[self.currentLine])
def MoveRight(self) -> None:
if self.currentCol < len(self.displayBuffer[self.currentLine]):
self.currentCol += 1
elif self.currentLine < len(self.displayBuffer) - 1:
self.currentLine += 1
self.currentCol = 0
def MoveUp(self) -> None:
if self.currentLine > 0:
self.currentLine -= 1
self.currentCol = min(self.currentCol, len(self.displayBuffer[self.currentLine]))
def MoveDown(self) -> None:
if self.currentLine < len(self.displayBuffer) - 1:
self.currentLine += 1
self.currentCol = min(self.currentCol, len(self.displayBuffer[self.currentLine]))
def Dump(self) -> None:
"""Обновляет дамп данных"""
self.dump = [sublist.copy() for sublist in self.displayBuffer]
def RecoverLine(self) -> None:
self.displayBuffer[self.currentLine] = self.dump[self.currentLine].copy()
self.currentCol = 0
def LoadFile(self, file_path) -> None:
"""Загрузка файла для редактирования"""
self.Reset()
self.file_path = file_path
self.mode = "NORMAL"
try:
with open(file_path, "r") as file:
self.displayBuffer = [list(line.rstrip('\n')) for line in file.readlines()]
self.Dump()
except FileNotFoundError:
print(f"File '{file_path}' not found. Starting with empty buffer.")
self.displayBuffer = []
self.displayBuffer.append([])
def SaveFile(self) -> None:
"""Сохранение текущего файла"""
self.WriteFile(self.file_path)
def WriteFile(self, file_path) -> None:
"""Запись в файл по указанному пути"""
try:
with open(file_path, "w") as file:
for line in self.displayBuffer:
file.write(''.join(line) + '\n')
self.Dump()
print(f"In file '{file_path}' written successfully.")
except Exception as e:
print(f"Error writing file: {str(e)}")
def Reset(self) -> None:
self.showLineNumbers = True
self.lastSearch = ()
self.displayBuffer = [] # буфер для хранения всех строк
self.dump = []
self.currentLine = 0 # текущий индекс строки
self.currentCol = 0 # текущий индекс колонки
self.scrollY = 0 # вертикальная прокрутка
self.scrollX = 0 # горизонтальная прокрутка
self.file_path = "" # путь к файлу
self.mode = ""
self.inputAfterCursor = False
self.keyLog = [] # лог нажатий клавиш (кортежи вида (код символа, юникс время нажатия))
self.commandBuffer = [] # буффер для команды
self.exchangeBuffer = [] # буффер обмена