Compare commits
No commits in common. "MyString" and "master" have entirely different histories.
91
adapter.py
91
adapter.py
|
@ -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
24
main.py
|
@ -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()
|
|
@ -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)
|
||||||
|
|
145
mvc/models.py
145
mvc/models.py
|
@ -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 = [] # буффер обмена
|
84
mvc/views.py
84
mvc/views.py
|
@ -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
79
test.py
|
@ -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()
|
|
|
@ -1,2 +0,0 @@
|
||||||
line 1
|
|
||||||
line 2
|
|
6
tools.py
6
tools.py
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue