[WIP] 200 行で vi っぽいエディタを作る (python)
120 行で vi っぽいエディタを作るを見て面白そうだったので、Python で書いてみた。とりあえず動くレベル。個人的メモ。
手元の環境が古かったのか、変なところでこけたりする(addch()
で '\n'
を描画すると ERR が返ってきた)ので、こまごま修正しました。
エラーハンドリングはほとんどしていないので(例:ファイル出力でエラーになるとこける、など)まったく実用にはなりません。
扱う文字範囲は、ASCII の範囲内で。
終了は Ctrl+Q。それ以外の操作は、ソース参照。
ae2019.py
#!/usr/bin/env python3importsysimportosimportcursesimportcurses.asciiasascdefctrl(ch):returnord(ch)&0x1fclassEditor:file_name=''buff=[]done=Falseindex,page_start,page_end=0,0,0col,row=0,0actions={}stdscr=Nonedef__init__(self):self.actions={ord('h'):self.left,ord('l'):self.right,ord('k'):self.up,ord('j'):self.down,ord('b'):self.word_left,ord('w'):self.word_right,ctrl('D'):self.page_down,ctrl('U'):self.page_up,ord('0'):self.line_begin,ord('$'):self.line_end,ord('t'):self.top,ord('G'):self.bottom,ord('i'):self.insert,ord('x'):self.delete,ctrl('Q'):self.quit,ctrl('R'):self.redraw,ctrl('S'):self.save,}defline_top(self,in_offset):foroffsetinreversed(range(0,in_offset)):ifself.buff[offset]=='\n':returnoffset+1return0defnext_line_top(self,in_offset):foroffsetinrange(in_offset,len(self.buff)):ifself.buff[offset]=='\n':returnmin(offset+1,len(self.buff)-1)returnlen(self.buff)-1defadjust(self,top_offset,in_col):x=0foroffsetinrange(top_offset,len(self.buff)):ifin_col<=xorself.buff[offset]=='\n':returnoffsetx+=(8-(x&7))ifself.buff[offset]=='\t'else1returnlen(self.buff)-1defdisplay(self):ifself.index<self.page_start:self.page_start=self.line_top(self.index)ifself.page_end<=self.index:self.page_start=self.next_line_top(self.index)n=(curses.LINES-2)ifself.page_start==len(self.buff)-1 \
elsecurses.LINESforiinrange(0,n):self.page_start=self.line_top(self.page_start-1)self.stdscr.erase()(y,x)=(0,0)forself.page_endinrange(self.page_start,len(self.buff)):ifcurses.LINES<=y:breakch=self.buff[self.page_end]ifself.index==self.page_end:(self.row,self.col)=(y,x)ifch!='\r'andch!='\n':self.stdscr.addch(y,x,ch)x+=(8-(x&7))ifch=='\t'else1ifch=='\n'orcurses.COLS<=x:(y,x)=(y+1,0)ify+1<curses.LINES:self.stdscr.addstr(y+1,0,'<< EOF >>')self.stdscr.move(self.row,self.col)self.stdscr.refresh()defleft(self):self.index=max(self.index-1,0)defright(self):self.index=min(self.index+1,len(self.buff)-1)defup(self):current_top=self.line_top(self.index)prev_top=self.line_top(current_top-1)self.index=self.adjust(prev_top,self.col)defdown(self):next_top=self.next_line_top(self.index)self.index=self.adjust(next_top,self.col)defline_begin(self):self.index=self.line_top(self.index)defline_end(self):self.index=max(self.next_line_top(self.index)-1,0)deftop(self):self.index=0defbottom(self):self.index=len(self.buff)-1defdelete(self):ifself.index<len(self.buff)-1:del(self.buff[self.index])defquit(self):self.done=Truedefredraw(self):curses.clear()self.display()definsert(self):whileTrue:code=self.stdscr.getch()ifcode==asc.ESC:breakelif0<self.indexandcodein[asc.BS,asc.DEL]:self.index-=1del(self.buff[self.index])else:iflen(self.buff)==0:self.buff=['\n']ch='\n'ifcode==ord('\r')elsechr(code)self.buff.insert(self.index,ch)self.index+=1self.display()defword_left(self):while0<self.indexandnotself.buff[self.index].isspace():self.index-=1while0<self.indexandself.buff[self.index].isspace():self.index-=1defword_right(self):whileself.index<len(self.buff)-1 \
andnotself.buff[self.index].isspace():self.index+=1whileself.index<len(self.buff)-1 \
andself.buff[self.index].isspace():self.index+=1defpage_down(self):self.page_start=self.index=self.line_top(self.page_end-1)foriinrange(0,self.row):self.down()self.page_end=len(self.buff)-1defpage_up(self):foriinrange(0,curses.LINES):self.page_start=self.line_top(self.page_start-1)self.up()defsave(self):withopen(self.file_name,mode='w')asf:f.write(''.join(self.buff))defmain(self,stdscr,file_name):self.stdscr=stdscrcurses.raw()self.stdscr.idlok(True)self.file_name=file_nameifos.path.isfile(self.file_name):withopen(self.file_name)asf:self.buff=list(f.read())else:self.buff=[]whilenotself.done:self.display()code=self.stdscr.getch()ifcodeinself.actions:self.actions[code]()if__name__=='__main__':iflen(sys.argv)<2:raiseException("usage: ae2019.py FILE_NAME")curses.wrapper(Editor().main,sys.argv[1])