доделал режим навигации и редактирования
parent
b420a4e667
commit
6ed9b61e5f
5
main.py
5
main.py
|
@ -1,8 +1,9 @@
|
|||
import sys, time
|
||||
import sys
|
||||
from mvc.models import VimModel
|
||||
from mvc.views import VimView, CursesAdapter
|
||||
from mvc.controllers import Controller, ReturnCode
|
||||
from mvc.controllers import EditStrategy, CommandStrategy, NormalStrategy
|
||||
import tools
|
||||
|
||||
def main():
|
||||
adapter = CursesAdapter()
|
||||
|
@ -15,7 +16,7 @@ def main():
|
|||
|
||||
# Первинчая загрузка файла для редактирования
|
||||
if len(sys.argv) > 1: model.LoadFile(sys.argv[1])
|
||||
else: model.LoadFile("File not found but i create it"+str(int(time.time()))+".txt")
|
||||
else: model.LoadFile("File not found but i create it"+str(tools.UnixSec())+".txt")
|
||||
|
||||
while True:
|
||||
view.Render(model.displayBuffer,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import tools
|
||||
from abc import ABC, abstractmethod
|
||||
from mvc.views import CursesAdapter
|
||||
from mvc.models import VimModel, ReturnCode
|
||||
|
@ -90,11 +91,36 @@ class NormalStrategy(BaseStrategy):
|
|||
|
||||
def HandleInput(self, symbolCode) -> int:
|
||||
"""Обработка ввода пользователя"""
|
||||
result = self.handle_common_input(symbolCode)
|
||||
if result != ReturnCode.CONTINUE: return result
|
||||
|
||||
self.model.UpdateKeyLog(symbolCode)
|
||||
match symbolCode:
|
||||
case self.adapter.KEY_TWO_DOTS: return ReturnCode.SET_COMMAND_MODE
|
||||
|
||||
case self.adapter.KEY_LEFT: self.model.MoveLeft()
|
||||
case self.adapter.KEY_RIGHT: self.model.MoveRight()
|
||||
case self.adapter.KEY_UP: self.model.MoveUp()
|
||||
case self.adapter.KEY_DOWN: self.model.MoveDown()
|
||||
case self.adapter.KEY_CTRL_S: self.model.SaveFile()
|
||||
case self.adapter.KEY_NULL | self.adapter.KEY_XOR: self.model.ToLineStart()
|
||||
case self.adapter.KEY_DOLLAR: self.model.ToLineEnd()
|
||||
case self.adapter.KEY_p: self.model.Paste()
|
||||
case self.adapter.KEY_w:
|
||||
if self.model.CombinationCheck("diw", 3): self.model.DeleteWord()
|
||||
elif self.model.CombinationCheck("yw", 3): self.model.CopyWord()
|
||||
else: self.model.ToWordEnd()
|
||||
case self.adapter.KEY_b: self.model.ToWordStart()
|
||||
case self.adapter.KEY_d:
|
||||
if self.model.CombinationCheck("dd", 3): self.model.CutLine()
|
||||
case self.adapter.KEY_y:
|
||||
if self.model.CombinationCheck("yy", 3): self.model.CopyLine()
|
||||
case self.adapter.KEY_x: self.model.DeleteNext()
|
||||
case self.adapter.KEY_G:
|
||||
num, ind = tools.ExtracrtNumBeforeLastKey(''.join([chr(item[0]) for item in self.model.keyLog]))
|
||||
if num != None and ind != None:
|
||||
self.model.MoveToLine(num)
|
||||
else:
|
||||
self.model.ToFileEnd()
|
||||
case self.adapter.KEY_g:
|
||||
if self.model.CombinationCheck("gg", 3): self.model.ToFileStart()
|
||||
case self.adapter.KEY_PG_UP: self.model.PageUp()
|
||||
case self.adapter.KEY_PG_DOWN: self.model.PageDown()
|
||||
self.model.Scroll()
|
||||
return ReturnCode.GOOD
|
168
mvc/models.py
168
mvc/models.py
|
@ -1,3 +1,5 @@
|
|||
import re
|
||||
import tools
|
||||
from enum import Enum
|
||||
|
||||
class ReturnCode(Enum):
|
||||
|
@ -19,9 +21,24 @@ class VimModel:
|
|||
self.scrollX = 0 # горизонтальная прокрутка
|
||||
self.file_path = "" # путь к файлу
|
||||
self.mode = ""
|
||||
self.keyLog = [] # лог нажатий клавиш (кортежи вида (код символа, юникс время нажатия))
|
||||
self.commandBuffer = [] # буффер для команды
|
||||
self.exchangeBuffer = [] # буффер обмена
|
||||
|
||||
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":
|
||||
|
@ -47,15 +64,89 @@ class VimModel:
|
|||
self.displayBuffer[self.currentLine].insert(self.currentCol, chr(symbolCode))
|
||||
self.currentCol += 1
|
||||
|
||||
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
|
||||
else:
|
||||
self.currentLine = 0
|
||||
|
||||
def PageDown(self) -> None:
|
||||
self.currentCol = 0
|
||||
if self.currentLine + self.displayLinesCount < len(self.displayBuffer):
|
||||
self.currentLine += self.displayLinesCount
|
||||
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 "$": # Перемещение в конец строки
|
||||
self.currentCol = len(self.displayBuffer[self.currentLine])
|
||||
case "^" | "0": # Перемещение в начало строки
|
||||
self.currentCol = 0
|
||||
case "q": # Выход из программы
|
||||
return ReturnCode.EXIT_CODE
|
||||
case "q!": # Выход без сохранения
|
||||
|
@ -75,75 +166,6 @@ class VimModel:
|
|||
self.currentCol = 0
|
||||
self.displayBuffer[self.currentLine] = []
|
||||
return ReturnCode.SET_EDIT_MODE
|
||||
case "RIGHT": # Перемещение курсора на 1 позицию вправо
|
||||
self.currentCol += 1
|
||||
case "LEFT": # Перемещение курсора на 1 позицию влево
|
||||
if self.currentCol > 0:
|
||||
self.currentCol -= 1
|
||||
case "UP": # Перемещение курсора на 1 позицию вверх
|
||||
if self.currentLine > 0:
|
||||
self.currentLine -= 1
|
||||
if self.currentCol > len(self.displayBuffer[self.currentLine]):
|
||||
self.currentCol = len(self.displayBuffer[self.currentLine])
|
||||
case "DOWN": # Перемещение курсора на 1 позицию вниз
|
||||
if self.currentLine < len(self.displayBuffer) - 1:
|
||||
self.currentLine += 1
|
||||
if self.currentCol > len(self.displayBuffer[self.currentLine]):
|
||||
self.currentCol = len(self.displayBuffer[self.currentLine])
|
||||
case "w": # Перемещение курсора в конец слова справа от курсора
|
||||
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)
|
||||
case "b": # Перемещает курсор в начало слова слева от курсора
|
||||
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
|
||||
case "gg": # Переход в начало файла
|
||||
self.currentLine = 0
|
||||
self.currentCol = 0
|
||||
case "G": # Переход в конец файла
|
||||
self.currentLine = len(self.displayBuffer) - 1
|
||||
self.currentCol = len(self.displayBuffer[self.currentLine])
|
||||
case "x": # Удаляет символ после курсора
|
||||
if self.currentCol + 1 < len(self.displayBuffer[self.currentLine]):
|
||||
del self.displayBuffer[self.currentLine][self.currentCol + 1]
|
||||
case "yy": # копирует текущую строку
|
||||
self.exchangeBuffer = self.displayBuffer[self.currentLine]
|
||||
case "yw": # Копирует слово под курсором
|
||||
start_index, end_index = self.WordUnderCursor()
|
||||
line = self.displayBuffer[self.currentLine]
|
||||
self.exchangeBuffer = line[start_index:end_index+1]
|
||||
case "diw": # Удаляет слово под курсором
|
||||
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
|
||||
case "dd": # Вырезает текущую строку
|
||||
self.exchangeBuffer = self.displayBuffer[self.currentLine]
|
||||
self.displayBuffer[self.currentLine] = []
|
||||
self.currentCol = 0
|
||||
case "p": # Вставить после курсора
|
||||
self.displayBuffer[self.currentLine][self.currentCol+1:self.currentCol+1] = self.exchangeBuffer
|
||||
case "PG_UP": # Перейти на экран вверх
|
||||
self.currentCol = 0
|
||||
if self.currentLine > self.displayLinesCount:
|
||||
self.currentLine -= self.displayLinesCount
|
||||
else:
|
||||
self.currentLine = 0
|
||||
case "PG_DOWN": # Перейти на экран вниз
|
||||
self.currentCol = 0
|
||||
if self.currentLine + self.displayLinesCount < len(self.displayBuffer):
|
||||
self.currentLine += self.displayLinesCount
|
||||
else:
|
||||
self.currentLine = len(self.displayBuffer) - 1
|
||||
case "h":
|
||||
self.LoadFile("config/usage.txt")
|
||||
return ReturnCode.SET_BASIC_MODE
|
||||
|
|
24
mvc/views.py
24
mvc/views.py
|
@ -2,16 +2,30 @@ import curses
|
|||
|
||||
class CursesAdapter:
|
||||
def __init__(self) -> None:
|
||||
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_BACKSPACE_1 = 127
|
||||
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 = 450
|
||||
self.KEY_PG_DOWN = 456
|
||||
|
||||
|
||||
self.screen = curses.initscr()
|
||||
self.screen.keypad(True)
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import time, re
|
||||
|
||||
def ExtracrtNumBeforeLastKey(s):
|
||||
numbers_list = list(re.finditer(r'\d+', s))
|
||||
if numbers_list:
|
||||
last_number_match = numbers_list[-1]
|
||||
number = last_number_match.group()
|
||||
i = last_number_match.start()
|
||||
if i + len(number) == len(s) - 1:
|
||||
return int(number), i
|
||||
return None, None
|
||||
|
||||
def UnixSec() -> int:
|
||||
return int(time.time())
|
Loading…
Reference in New Issue