Compare commits

..

No commits in common. "MyString" and "master" have entirely different histories.

8 changed files with 158 additions and 293 deletions

View File

@ -1,91 +0,0 @@
from abc import ABC, abstractmethod
import curses
class IController(ABC):
@abstractmethod
def GetChar(self) -> int:
pass
class IView(ABC):
@abstractmethod
def Refresh(self) -> None:
pass
@abstractmethod
def Cleanup(self) -> None:
pass
@abstractmethod
def SetCursor(self, x: int, y: int) -> None:
pass
@abstractmethod
def SetChar(self, x: int, y: int, code: int) -> None:
pass
@abstractmethod
def SetColorString(self, x: int, y: int, data: str, attr: int) -> None:
pass
@abstractmethod
def SetString(self, x: int, y: int, data: str) -> None:
pass
class CursesAdapter(IView, IController):
def __init__(self) -> None:
self.KEY_BACKSPACE_2 = 8
self.KEY_ENTER = 10
self.KEY_CTRL_S = 19
self.KEY_ESCAPE = 27
self.KEY_DOLLAR = 36
self.KEY_NULL = 48
self.KEY_TWO_DOTS = 59
self.KEY_G = 71
self.KEY_XOR = 94
self.KEY_b = 98
self.KEY_d = 100
self.KEY_g = 103
self.KEY_p = 112
self.KEY_w = 119
self.KEY_x = 120
self.KEY_y = 121
self.KEY_BACKSPACE_1 = 127
self.KEY_LEFT = curses.KEY_LEFT
self.KEY_RIGHT = curses.KEY_RIGHT
self.KEY_UP = curses.KEY_UP
self.KEY_DOWN = curses.KEY_DOWN
self.KEY_PG_UP = 451
self.KEY_PG_DOWN = 457
self.screen = curses.initscr()
self.screen.keypad(True)
self.cols = curses.COLS
self.lines = curses.LINES
curses.noecho()
curses.curs_set(1) # Make cursor visible
curses.start_color()
curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_BLACK) # Фиолетовый текст на черном фоне
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLUE) # Фиолетовый текст на черном фоне
self.colorPairs = [curses.color_pair(1), curses.color_pair(2)]
def Refresh(self) -> None:
"""Apply changes"""
self.screen.refresh()
def Cleanup(self) -> None:
curses.endwin()
def SetCursor(self, x: int, y: int) -> None:
"""set cursor position (x, y)"""
self.screen.move(x, y)
def SetChar(self, x: int, y: int, code: int) -> None:
"""set char position (x, y)"""
self.screen.addch(x, y, code)
def SetColorString(self, x: int, y: int, data: str, attr: int) -> None:
self.screen.addstr(x, y, data, attr)
def SetString(self, x: int, y: int, data: str) -> None:
"""set string begin position (x, y)"""
self.screen.addstr(x, y, data)
def GetChar(self) -> int:
"""Wait users input"""
return self.screen.getch()

24
main.py
View File

@ -1,9 +1,8 @@
import sys import sys
from mvc.models import VimModel from mvc.models import VimModel
from mvc.views import VimView from mvc.views import VimView, CursesAdapter
from adapter import CursesAdapter from mvc.controllers import Controller, ReturnCode
from mvc.controllers import Controller from mvc.controllers import EditStrategy, CommandStrategy, NormalStrategy
from mvc.controllers import NormalStrategy
import tools import tools
def main(): def main():
@ -20,7 +19,22 @@ def main():
view.SetModel(model) # view subscribe model view.SetModel(model) # view subscribe model
controller.Run() while True:
model.notify()
symbolCode = view.curses_adapter.GetChar()
match controller.HandleInput(symbolCode):
case ReturnCode.SET_BASIC_MODE:
controller.SetStrategy(NormalStrategy(model, view.curses_adapter))
case ReturnCode.SET_COMMAND_MODE:
controller.SetStrategy(CommandStrategy(model, view.curses_adapter))
case ReturnCode.SET_EDIT_MODE:
controller.SetStrategy(EditStrategy(model, view.curses_adapter))
case ReturnCode.EXIT_CODE:
break
view.curses_adapter.Cleanup()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -1,6 +1,6 @@
import tools import tools
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from adapter import CursesAdapter from mvc.views import CursesAdapter
from mvc.models import VimModel, ReturnCode from mvc.models import VimModel, ReturnCode
def CommonInput(HandleInputFunc): def CommonInput(HandleInputFunc):
@ -48,23 +48,6 @@ class Controller:
def HandleInput(self, symbolCode: int): def HandleInput(self, symbolCode: int):
return self.strategy.HandleInput(symbolCode) return self.strategy.HandleInput(symbolCode)
def Run(self) -> None:
while True:
self.strategy.model.notify()
symbolCode = self.strategy.adapter.GetChar()
match self.HandleInput(symbolCode):
case ReturnCode.SET_BASIC_MODE:
self.SetStrategy(NormalStrategy(self.strategy.model, self.strategy.adapter))
case ReturnCode.SET_COMMAND_MODE:
self.SetStrategy(CommandStrategy(self.strategy.model, self.strategy.adapter))
case ReturnCode.SET_EDIT_MODE:
self.SetStrategy(EditStrategy(self.strategy.model, self.strategy.adapter))
case ReturnCode.EXIT_CODE:
break
self.strategy.adapter.Cleanup()
class CommandStrategy(BaseStrategy): class CommandStrategy(BaseStrategy):
"""command input mode""" """command input mode"""
def __init__(self, model: VimModel, adapter: CursesAdapter): def __init__(self, model: VimModel, adapter: CursesAdapter):
@ -128,6 +111,7 @@ class NormalStrategy(BaseStrategy):
case self.adapter.KEY_y: case self.adapter.KEY_y:
if self.model.CombinationCheck("yy", 3): self.model.CopyLine() if self.model.CombinationCheck("yy", 3): self.model.CopyLine()
case self.adapter.KEY_x: self.model.DeleteNext() case self.adapter.KEY_x: self.model.DeleteNext()
case self.adapter.KEY_U: self.model.RecoverLine()
case self.adapter.KEY_G: case self.adapter.KEY_G:
num, ind = tools.ExtracrtNumBeforeLastKey(''.join([chr(item[0]) for item in self.model.keyLog])) num, ind = tools.ExtracrtNumBeforeLastKey(''.join([chr(item[0]) for item in self.model.keyLog]))
if num != None and ind != None: self.model.MoveToLine(num) if num != None and ind != None: self.model.MoveToLine(num)

View File

@ -1,9 +1,5 @@
from abc import ABC, abstractmethod
import tools import tools
from enum import Enum from enum import Enum
from typing import List
from mystring import MyString as MyString
class ReturnCode(Enum): class ReturnCode(Enum):
GOOD = -101 GOOD = -101
@ -13,62 +9,33 @@ class ReturnCode(Enum):
SET_EDIT_MODE = -97 SET_EDIT_MODE = -97
CONTINUE = -96 CONTINUE = -96
class Observable(ABC): class VimModel:
@abstractmethod
def attach(self, observer) -> None:
pass
@abstractmethod
def detach(self, observer) -> None:
pass
@abstractmethod
def notify(self) -> None:
pass
class VimModel(Observable):
observers: List
displayLinesCount: int
displayColsCount: int
showLineNumbers: bool
lastSearch: tuple[str, int] # tuple (search str, direction)
displayBuffer: List[MyString]
dump: List[MyString]
currentLine: int
currentCol: int
scrollY: int
scrollX: int
file_path: str # filepath
mode: str
inputAfterCursor: bool
keyLog: List[tuple[int, int]] # key log for check combinations (tuples (symbol code, tap unix time))
commandBuffer: str # buffer for command
exchangeBuffer: str # buffer for exchange
def __init__(self, displayLinesCount: int, displayColsCount: int): def __init__(self, displayLinesCount: int, displayColsCount: int):
self.observers = [] self.observers = []
self.displayLinesCount = displayLinesCount self.displayLinesCount = displayLinesCount
self.displayColsCount = displayColsCount self.displayColsCount = displayColsCount
self.showLineNumbers = True self.showLineNumbers = True
self.lastSearch = () self.lastSearch = ()
self.displayBuffer = [] self.displayBuffer = [] # буфер для хранения всех строк
self.dump = [] self.dump = []
self.currentLine = 0 self.currentLine = 0 # текущий индекс строки
self.currentCol = 0 self.currentCol = 0 # текущий индекс колонки
self.scrollY = 0 self.scrollY = 0 # вертикальная прокрутка
self.scrollX = 0 self.scrollX = 0 # горизонтальная прокрутка
self.file_path = "" self.file_path = "" # путь к файлу
self.mode = "" self.mode = ""
self.inputAfterCursor = False self.inputAfterCursor = False
self.keyLog = [] self.keyLog = [] # лог нажатий клавиш (кортежи вида (код символа, юникс время нажатия))
self.commandBuffer = "" self.commandBuffer = [] # буффер для команды
self.exchangeBuffer = "" self.exchangeBuffer = [] # буффер обмена
def attach(self, observer) -> None: def attach(self, observer):
self.observers.append(observer) self.observers.append(observer)
def detach(self, observer) -> None: def detach(self, observer):
self.observers.remove(observer) self.observers.remove(observer)
def notify(self) -> None: def notify(self):
for observer in self.observers: for observer in self.observers:
observer.Update() observer.Update()
@ -79,7 +46,7 @@ class VimModel(Observable):
if len(self.keyLog) > 5000: self.keyLog.clear() if len(self.keyLog) > 5000: self.keyLog.clear()
self.keyLog.append((symbolCode, tools.UnixSec())) self.keyLog.append((symbolCode, tools.UnixSec()))
def CombinationCheck(self, comb: str, interval: int) -> bool: def CombinationCheck(self, comb: str, interval: int) -> None:
"""Проверяет была ли нажата комбинация клавиш. """Проверяет была ли нажата комбинация клавиш.
Принимает фактический ввод, команду с которой сравниваем и интервал времени на команду""" Принимает фактический ввод, команду с которой сравниваем и интервал времени на команду"""
if len(self.keyLog) > len(comb) - 1: if len(self.keyLog) > len(comb) - 1:
@ -92,7 +59,7 @@ class VimModel(Observable):
def ModeBar(self) -> str: def ModeBar(self) -> str:
modeBar = f"MODE: {self.mode} | FILE: {self.file_path} | LINE: {self.currentLine+1}/{len(self.displayBuffer)}" modeBar = f"MODE: {self.mode} | FILE: {self.file_path} | LINE: {self.currentLine+1}/{len(self.displayBuffer)}"
if self.mode == "COMMAND": if self.mode == "COMMAND":
modeBar += f" | COMMAND BUFFER: {self.commandBuffer}" modeBar += f" | COMMAND BUFFER: {''.join(self.commandBuffer)}"
return modeBar return modeBar
def Scroll(self) -> None: def Scroll(self) -> None:
@ -114,7 +81,7 @@ class VimModel(Observable):
self.scrollX = self.currentCol - visible_text_width + 1 self.scrollX = self.currentCol - visible_text_width + 1
def InsertCommandSymbol(self, symbolCode: int) -> None: def InsertCommandSymbol(self, symbolCode: int) -> None:
self.commandBuffer += chr(symbolCode) self.commandBuffer.append(chr(symbolCode))
def InsertSymbol(self, symbolCode: int) -> None: def InsertSymbol(self, symbolCode: int) -> None:
if self.currentCol <= len(self.displayBuffer[self.currentLine]): # проверяем, не превышает ли индекс колонки длину строки if self.currentCol <= len(self.displayBuffer[self.currentLine]): # проверяем, не превышает ли индекс колонки длину строки
@ -132,7 +99,7 @@ class VimModel(Observable):
self.currentCol = len(self.displayBuffer[self.currentLine]) self.currentCol = len(self.displayBuffer[self.currentLine])
def ToWordEnd(self) -> None: def ToWordEnd(self) -> None:
line = self.displayBuffer[self.currentLine].data() line = ''.join(self.displayBuffer[self.currentLine])
# Находим ближайший непробельный символ # Находим ближайший непробельный символ
non_space_index = next((i for i, char in enumerate(line[self.currentCol:]) if char != ' '), None) non_space_index = next((i for i, char in enumerate(line[self.currentCol:]) if char != ' '), None)
if non_space_index is not None: if non_space_index is not None:
@ -141,7 +108,7 @@ class VimModel(Observable):
self.currentCol = right_space_index if right_space_index != -1 else len(line) self.currentCol = right_space_index if right_space_index != -1 else len(line)
def ToWordStart(self) -> None: def ToWordStart(self) -> None:
line = self.displayBuffer[self.currentLine].data() line = ''.join(self.displayBuffer[self.currentLine])
non_space_index = next((i for i in range(self.currentCol - 1, -1, -1) if line[i] != ' '), None) 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: if non_space_index is not None:
left_space_index = line.rfind(' ', 0, non_space_index) left_space_index = line.rfind(' ', 0, non_space_index)
@ -157,43 +124,45 @@ class VimModel(Observable):
def DeleteNext(self) -> None: def DeleteNext(self) -> None:
if self.currentCol + 1 < len(self.displayBuffer[self.currentLine]): if self.currentCol + 1 < len(self.displayBuffer[self.currentLine]):
self.displayBuffer[self.currentLine].erase(self.currentCol + 1, 1) del self.displayBuffer[self.currentLine][self.currentCol + 1]
def PageUp(self) -> None: def PageUp(self) -> None:
self.currentCol = 0 self.currentCol = 0
if self.currentLine > self.displayLinesCount: if self.currentLine > self.displayLinesCount:
self.currentLine -= self.displayLinesCount - 2 self.currentLine -= self.displayLinesCount - 2
else: self.currentLine = 0 else:
self.currentLine = 0
def PageDown(self) -> None: def PageDown(self) -> None:
self.currentCol = 0 self.currentCol = 0
if self.currentLine + self.displayLinesCount < len(self.displayBuffer): if self.currentLine + self.displayLinesCount < len(self.displayBuffer):
self.currentLine += self.displayLinesCount - 2 self.currentLine += self.displayLinesCount - 2
else: self.currentLine = len(self.displayBuffer) - 1 else:
self.currentLine = len(self.displayBuffer) - 1
def DeleteWord(self) -> None: def DeleteWord(self) -> None:
start_index, end_index = self.WordUnderCursor() start_index, end_index = self.WordUnderCursor()
line = self.displayBuffer[self.currentLine] line = self.displayBuffer[self.currentLine]
if end_index < len(line) and line[end_index] == ' ': if end_index < len(line) and line[end_index] == ' ':
end_index += 1 end_index += 1
self.displayBuffer[self.currentLine].erase(start_index, end_index-start_index) self.displayBuffer[self.currentLine] = line[:start_index] + line[end_index:]
self.currentCol = start_index self.currentCol = start_index
def CopyWord(self) -> None: def CopyWord(self) -> None:
start_index, end_index = self.WordUnderCursor() start_index, end_index = self.WordUnderCursor()
self.exchangeBuffer = self.displayBuffer[self.currentLine].\ line = self.displayBuffer[self.currentLine]
substr(start_index, end_index-start_index).data() self.exchangeBuffer = line[start_index:end_index+1]
def Paste(self) -> None: def Paste(self) -> None:
self.displayBuffer[self.currentLine].insert(self.currentCol, self.exchangeBuffer) self.displayBuffer[self.currentLine][self.currentCol+1:self.currentCol+1] = self.exchangeBuffer
def CutLine(self) -> None: def CutLine(self) -> None:
self.exchangeBuffer = self.displayBuffer[self.currentLine].data() self.exchangeBuffer = self.displayBuffer[self.currentLine]
self.displayBuffer[self.currentLine] = MyString() self.displayBuffer[self.currentLine] = []
self.currentCol = 0 self.currentCol = 0
def CopyLine(self) -> None: def CopyLine(self) -> None:
self.exchangeBuffer = self.displayBuffer[self.currentLine].data() self.exchangeBuffer = self.displayBuffer[self.currentLine]
def MoveToLine(self, numberLine: int) -> None: def MoveToLine(self, numberLine: int) -> None:
numberLine -= 1 numberLine -= 1
@ -201,10 +170,10 @@ class VimModel(Observable):
self.currentLine = numberLine self.currentLine = numberLine
self.currentCol = 0 self.currentCol = 0
def EnterCommand(self) -> None: def EnterCommand(self):
"""Обработка введенной команды""" """Обработка введенной команды"""
cmd = ''.join(self.commandBuffer) cmd = ''.join(self.commandBuffer)
self.commandBuffer = "" self.commandBuffer.clear()
match cmd: match cmd:
case "q": # Выход из программы case "q": # Выход из программы
if self.displayBuffer == self.dump: if self.displayBuffer == self.dump:
@ -226,7 +195,7 @@ class VimModel(Observable):
return ReturnCode.SET_EDIT_MODE return ReturnCode.SET_EDIT_MODE
case "S": # Удаление строки на которой курсор и вход в режим редактирования case "S": # Удаление строки на которой курсор и вход в режим редактирования
self.currentCol = 0 self.currentCol = 0
self.displayBuffer[self.currentLine] = MyString() self.displayBuffer[self.currentLine] = []
return ReturnCode.SET_EDIT_MODE return ReturnCode.SET_EDIT_MODE
case "o": case "o":
self.InputAfterCursor() self.InputAfterCursor()
@ -236,8 +205,8 @@ class VimModel(Observable):
return ReturnCode.SET_BASIC_MODE return ReturnCode.SET_BASIC_MODE
case "n": case "n":
if self.lastSearch != (): if self.lastSearch != ():
index = tools.findStringInMyStringList(self.displayBuffer, index = tools.findSublistIndex(self.displayBuffer,
self.lastSearch[0], list(self.lastSearch[0]),
self.currentLine, self.currentLine,
direction=self.lastSearch[1]) direction=self.lastSearch[1])
if index != -1: if index != -1:
@ -246,14 +215,16 @@ class VimModel(Observable):
self.lastSearch = (self.lastSearch[0], self.lastSearch[1]) self.lastSearch = (self.lastSearch[0], self.lastSearch[1])
case "N": case "N":
if self.lastSearch != (): if self.lastSearch != ():
index = tools.findStringInMyStringList(self.displayBuffer, index = tools.findSublistIndex(self.displayBuffer,
self.lastSearch[0], list(self.lastSearch[0]),
self.currentLine, self.currentLine,
direction=(self.lastSearch[1]+1)%2) direction=(self.lastSearch[1]+1)%2)
if index != -1: if index != -1:
self.currentLine = index self.currentLine = index
self.currentCol = 0 self.currentCol = 0
self.lastSearch = (self.lastSearch[0], (self.lastSearch[1]+1)%2) self.lastSearch = (self.lastSearch[0], (self.lastSearch[1]+1)%2)
case "e!":
self.displayBuffer = [sublist.copy() for sublist in self.dump]
case "set num": case "set num":
self.showLineNumbers = not self.showLineNumbers self.showLineNumbers = not self.showLineNumbers
case _: case _:
@ -268,14 +239,14 @@ class VimModel(Observable):
self.WriteFile(filename) self.WriteFile(filename)
# Заменяет символ на указанный # Заменяет символ на указанный
elif len(cmd) == 3 and cmd[:2] == 'r ': elif len(cmd) == 3 and cmd[:2] == 'r ':
self.displayBuffer[self.currentLine].replace(self.currentCol, 1, cmd[2]) self.displayBuffer[self.currentLine][self.currentCol] = cmd[2]
# Переход на строку по введенному номеру # Переход на строку по введенному номеру
elif cmd.isdigit(): elif cmd.isdigit():
self.MoveToLine(int(cmd)) self.MoveToLine(int(cmd))
# Поиск строки от курсора до конца файла # Поиск строки от курсора до конца файла
elif len(cmd) > 1 and cmd[0] == '/': elif len(cmd) > 1 and cmd[0] == '/':
index = tools.findStringInMyStringList(self.displayBuffer, index = tools.findSublistIndex(self.displayBuffer,
cmd[1:], list(cmd[1:]),
self.currentLine, self.currentLine,
direction=1) direction=1)
if index != -1: if index != -1:
@ -284,8 +255,8 @@ class VimModel(Observable):
self.lastSearch = (cmd[1:], 1) self.lastSearch = (cmd[1:], 1)
# Поиск строки от курсора до начала файла # Поиск строки от курсора до начала файла
elif len(cmd) > 1 and cmd[0] == '?': elif len(cmd) > 1 and cmd[0] == '?':
index = tools.findStringInMyStringList(self.displayBuffer, index = tools.findSublistIndex(self.displayBuffer,
cmd[1:], list(cmd[1:]),
self.currentLine, self.currentLine,
direction=0) direction=0)
if index != -1: if index != -1:
@ -297,7 +268,7 @@ class VimModel(Observable):
def WordUnderCursor(self)-> tuple[int, int]: def WordUnderCursor(self)-> tuple[int, int]:
"""Возвращает индекс начала и индекс конца слова под курсором""" """Возвращает индекс начала и индекс конца слова под курсором"""
line = self.displayBuffer[self.currentLine].data() line = ''.join(self.displayBuffer[self.currentLine])
start_index = line.rfind(' ', 0, self.currentCol) start_index = line.rfind(' ', 0, self.currentCol)
start_index = 0 if start_index == -1 else start_index + 1 start_index = 0 if start_index == -1 else start_index + 1
end_index = line.find(' ', self.currentCol) end_index = line.find(' ', self.currentCol)
@ -306,24 +277,24 @@ class VimModel(Observable):
def Enter(self) -> None: def Enter(self) -> None:
# Разделяем текущую строку на две части # Разделяем текущую строку на две части
new_line = self.displayBuffer[self.currentLine].substr(self.currentCol) new_line = self.displayBuffer[self.currentLine][self.currentCol:]
self.displayBuffer[self.currentLine] = self.displayBuffer[self.currentLine].substr(0, self.currentCol) self.displayBuffer[self.currentLine] = self.displayBuffer[self.currentLine][:self.currentCol]
self.currentLine += 1 # Переходим на следующую строку self.currentLine += 1 # Переходим на следующую строку
self.displayBuffer.insert(self.currentLine, new_line) # Вставляем новую строку self.displayBuffer.insert(self.currentLine, new_line) # Вставляем новую строку
self.currentCol = 0 # Сбрасываем индекс колонки self.currentCol = 0 # Сбрасываем индекс колонки
def BackspaceCommand(self) -> None: def BackspaceCommand(self) -> None:
if len(self.commandBuffer) > 0: if len(self.commandBuffer) > 0:
self.commandBuffer = self.commandBuffer[:-1] self.commandBuffer.pop()
def Backspace(self) -> None: def Backspace(self) -> None:
if self.currentCol > 0: # Если символ существует в текущей строке if self.currentCol > 0: # Если символ существует в текущей строке
self.currentCol -= 1 self.currentCol -= 1
self.displayBuffer[self.currentLine].erase(self.currentCol, 1) # Удаляем символ del self.displayBuffer[self.currentLine][self.currentCol] # Удаляем символ
elif self.currentLine > 0: # Если текущая строка не первая elif self.currentLine > 0: # Если текущая строка не первая
# Объединяем текущую строку с предыдущей # Объединяем текущую строку с предыдущей
prev_line_length = len(self.displayBuffer[self.currentLine - 1]) prev_line_length = len(self.displayBuffer[self.currentLine - 1])
self.displayBuffer[self.currentLine - 1].append(self.displayBuffer[self.currentLine].data()) self.displayBuffer[self.currentLine - 1].extend(self.displayBuffer[self.currentLine])
del self.displayBuffer[self.currentLine] del self.displayBuffer[self.currentLine]
self.currentLine -= 1 self.currentLine -= 1
self.currentCol = prev_line_length # Переходим в конец предыдущей строки self.currentCol = prev_line_length # Переходим в конец предыдущей строки
@ -354,7 +325,11 @@ class VimModel(Observable):
def Dump(self) -> None: def Dump(self) -> None:
"""Обновляет дамп данных""" """Обновляет дамп данных"""
self.dump = [line.substr(0) for line in self.displayBuffer] 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: def LoadFile(self, file_path) -> None:
"""Загрузка файла для редактирования""" """Загрузка файла для редактирования"""
@ -363,12 +338,12 @@ class VimModel(Observable):
self.mode = "NORMAL" self.mode = "NORMAL"
try: try:
with open(file_path, "r") as file: with open(file_path, "r") as file:
self.displayBuffer = [MyString(line.rstrip()) for line in file.readlines()] self.displayBuffer = [list(line.rstrip('\n')) for line in file.readlines()]
self.Dump() self.Dump()
except FileNotFoundError: except FileNotFoundError:
print(f"File '{file_path}' not found. Starting with empty buffer.") print(f"File '{file_path}' not found. Starting with empty buffer.")
self.displayBuffer = [] self.displayBuffer = []
self.displayBuffer.append(MyString()) self.displayBuffer.append([])
def SaveFile(self) -> None: def SaveFile(self) -> None:
"""Сохранение текущего файла""" """Сохранение текущего файла"""
@ -379,7 +354,7 @@ class VimModel(Observable):
try: try:
with open(file_path, "w") as file: with open(file_path, "w") as file:
for line in self.displayBuffer: for line in self.displayBuffer:
file.write(line.data() + '\n') file.write(''.join(line) + '\n')
self.Dump() self.Dump()
print(f"In file '{file_path}' written successfully.") print(f"In file '{file_path}' written successfully.")
except Exception as e: except Exception as e:
@ -398,5 +373,5 @@ class VimModel(Observable):
self.mode = "" self.mode = ""
self.inputAfterCursor = False self.inputAfterCursor = False
self.keyLog = [] # лог нажатий клавиш (кортежи вида (код символа, юникс время нажатия)) self.keyLog = [] # лог нажатий клавиш (кортежи вида (код символа, юникс время нажатия))
self.commandBuffer = "" # буффер для команды self.commandBuffer = [] # буффер для команды
self.exchangeBuffer = "" # буффер обмена self.exchangeBuffer = [] # буффер обмена

View File

@ -1,11 +1,75 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from adapter import CursesAdapter import curses
import mvc.models import mvc.models
from mystring import MyString as MyString
class Observer(ABC): class CursesAdapter:
def __init__(self) -> None:
self.KEY_BACKSPACE_2 = 8
self.KEY_ENTER = 10
self.KEY_CTRL_S = 19
self.KEY_ESCAPE = 27
self.KEY_DOLLAR = 36
self.KEY_NULL = 48
self.KEY_TWO_DOTS = 59
self.KEY_G = 71
self.KEY_U = 85
self.KEY_XOR = 94
self.KEY_b = 98
self.KEY_d = 100
self.KEY_g = 103
self.KEY_p = 112
self.KEY_w = 119
self.KEY_x = 120
self.KEY_y = 121
self.KEY_BACKSPACE_1 = 127
self.KEY_LEFT = curses.KEY_LEFT
self.KEY_RIGHT = curses.KEY_RIGHT
self.KEY_UP = curses.KEY_UP
self.KEY_DOWN = curses.KEY_DOWN
self.KEY_PG_UP = 450
self.KEY_PG_DOWN = 456
self.screen = curses.initscr()
self.screen.keypad(True)
self.cols = curses.COLS
self.lines = curses.LINES
curses.noecho()
curses.curs_set(1) # Make cursor visible
curses.start_color()
curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_BLACK) # Фиолетовый текст на черном фоне
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLUE) # Фиолетовый текст на черном фоне
self.colorPairs = [curses.color_pair(1), curses.color_pair(2)]
def Refresh(self) -> None:
"""Apply changes"""
self.screen.refresh()
def Cleanup(self) -> None:
curses.endwin()
def SetCursor(self, x: int, y: int):
"""set cursor position (x, y)"""
self.screen.move(x, y)
def SetChar(self, x: int, y: int, code: int):
"""set char position (x, y)"""
self.screen.addch(x, y, code)
def SetColorString(self, x: int, y: int, data: str, attr: int):
self.screen.addstr(x, y, data, attr)
def SetString(self, x: int, y: int, data: str):
"""set string begin position (x, y)"""
self.screen.addstr(x, y, data)
def GetChar(self) -> int:
"""Wait users input"""
return self.screen.getch()
class Observer:
@abstractmethod @abstractmethod
def Update(self) -> None: def Update(self):
pass pass
class VimView(Observer): class VimView(Observer):
@ -13,11 +77,11 @@ class VimView(Observer):
self.curses_adapter = adapter self.curses_adapter = adapter
self.model = None self.model = None
def SetModel(self, model: mvc.models.VimModel) -> None: def SetModel(self, model: mvc.models.VimModel):
self.model = model self.model = model
model.attach(self) model.attach(self)
def Update(self) -> None: def Update(self):
self.Render( self.Render(
self.model.displayBuffer, self.model.displayBuffer,
self.model.currentLine, self.model.currentLine,
@ -29,11 +93,11 @@ class VimView(Observer):
) )
def Render(self, def Render(self,
displayBuffer: list[MyString], displayBuffer: list[list[str]],
currentLine: int, currentCol: int, currentLine: int, currentCol: int,
scrollX: int, scrollY: int, scrollX: int, scrollY: int,
modeBarData: str, modeBarData: str,
show_line_numbers: bool = False) -> None: show_line_numbers: bool = False):
"""Отрисовка текущего состояния""" """Отрисовка текущего состояния"""
self.curses_adapter.screen.clear() self.curses_adapter.screen.clear()
@ -44,7 +108,7 @@ class VimView(Observer):
# Отображение видимой части текста # Отображение видимой части текста
for i in range(self.curses_adapter.lines - 1): for i in range(self.curses_adapter.lines - 1):
if i + scrollY < len(displayBuffer): if i + scrollY < len(displayBuffer):
line = displayBuffer[i + scrollY].data() line = ''.join(displayBuffer[i + scrollY])
# Если нужно отображать номера строк, добавляем их перед текстом # Если нужно отображать номера строк, добавляем их перед текстом
if show_line_numbers: if show_line_numbers:
line_number = f"{i + scrollY + 1:6} " # 6 символов на номер строки line_number = f"{i + scrollY + 1:6} " # 6 символов на номер строки
@ -72,7 +136,7 @@ class VimView(Observer):
self.curses_adapter.Refresh() self.curses_adapter.Refresh()
def SetModeBar(self, modeBarData: str) -> None: def SetModeBar(self, modeBarData: str):
"""Print edit mode information panel""" """Print edit mode information panel"""
if len(modeBarData) > self.curses_adapter.cols - 1: if len(modeBarData) > self.curses_adapter.cols - 1:
scrollX = len(modeBarData) - self.curses_adapter.cols scrollX = len(modeBarData) - self.curses_adapter.cols

79
test.py
View File

@ -1,79 +0,0 @@
# python -m unittest test.py
import unittest
from mvc.models import VimModel
from mystring import MyString as MyString
class TestVimModel(unittest.TestCase):
def setUp(self):
self.model = VimModel(displayLinesCount=10, displayColsCount=80)
self.model.displayBuffer = [MyString("line 1"), MyString("line 2")]
def test_initial_state(self):
self.assertEqual(self.model.currentLine, 0)
self.assertEqual(self.model.currentCol, 0)
self.assertEqual(len(self.model.displayBuffer), 2)
def test_insert_symbol(self):
self.model.InsertSymbol(ord('a'))
self.assertEqual(self.model.displayBuffer[0].data(), "aline 1")
self.assertEqual(self.model.currentCol, 1)
def test_move_right(self):
self.model.currentCol = 5
self.model.MoveRight()
self.assertEqual(self.model.currentCol, 6)
def test_move_left(self):
self.model.currentCol = 5
self.model.MoveLeft()
self.assertEqual(self.model.currentCol, 4)
def test_move_up(self):
self.model.currentLine = 1
self.model.MoveUp()
self.assertEqual(self.model.currentLine, 0)
def test_move_down(self):
self.model.MoveDown()
self.assertEqual(self.model.currentLine, 1)
def test_backspace(self):
self.model.currentCol = 5
self.model.Backspace()
self.assertEqual(self.model.displayBuffer[0].data(), "line1")
self.assertEqual(self.model.currentCol, 4)
def test_enter(self):
self.model.currentCol = 5
self.model.Enter()
self.assertEqual(len(self.model.displayBuffer), 3)
self.assertEqual(self.model.displayBuffer[0].data(), "line ")
self.assertEqual(self.model.displayBuffer[1].data(), "1")
self.assertEqual(self.model.currentLine, 1)
self.assertEqual(self.model.currentCol, 0)
def test_to_line_start(self):
self.model.currentCol = 5
self.model.ToLineStart()
self.assertEqual(self.model.currentCol, 0)
def test_to_line_end(self):
self.model.ToLineEnd()
self.assertEqual(self.model.currentCol, len(self.model.displayBuffer[0]))
def test_save_file(self):
self.model.SaveFile()
with open("testfile.txt", "r") as file:
lines = file.readlines()
self.assertEqual(lines[0].strip(), "line 1")
self.assertEqual(lines[1].strip(), "line 2")
def test_load_file(self):
self.model.LoadFile("testfile.txt")
self.assertEqual(len(self.model.displayBuffer), 2)
self.assertEqual(self.model.displayBuffer[0].data(), "line 1")
self.assertEqual(self.model.displayBuffer[1].data(), "line 2")
if __name__ == '__main__':
unittest.main()

View File

@ -1,2 +0,0 @@
line 1
line 2

View File

@ -1,15 +1,15 @@
import time, re import time, re
def findStringInMyStringList(main_list, sublist, start_index=0, direction=1): def findSublistIndex(main_list, sublist, start_index=0, direction=1):
start_index = max(0, min(start_index, len(main_list) - 1)) start_index = max(0, min(start_index, len(main_list) - 1))
if direction == 1: # forward if direction == 1: # forward
for index in range(start_index+1, len(main_list)): for index in range(start_index+1, len(main_list)):
if main_list[index].data() == sublist: if main_list[index] == sublist:
return index return index
else: # backward else: # backward
for index in range(start_index-1, -1, -1): for index in range(start_index-1, -1, -1):
if main_list[index].data() == sublist: if main_list[index] == sublist:
return index return index
return -1 return -1