I code it

Code and Life

Utouch的snippet功能设计与实现

用过TextMeta或者The E Editor的朋友对snippet应该都不陌生,当然,Emacs/vim的用户需要安装/编写插件也能享受snippet带来的效率。简而言之,snippet就是一些常用的代码片段模板,比如for语句,输入for之后按某个特殊的键(或者组合键,一般是Tab),就自动补全一部分代码,并将光标置于最可能会修改的位置,修改后再按之前的特殊键,光标会跳转到下一个可能修改的位置,以此类推。

TextMetaThe E Editor都提供了各自的实现,并且使用了相似的语法。它们采用一个模板文件,大致的语法如下:

   1: for( ${1:unsigned int} ${2:i} = ${3:0}; ${2:i} < ${4:count}; ${2:i} += ${5:1} )
   2: {
   3:     $0
   4: }

 

$N表示光标跳转时的顺序,$0为最终停止的地方。${N:default}这种记法则表示第N个stub处应该填写的默认值。当然,它们的模板语言可能会更复杂一些,我在utouch中的实现则相对简单一些。如我定义的for模板如下:

   1: for(int ${1:i} = ${2:0}; ${1:} < ${3:count}; ${1:} += ${4:1}){\n\t${0:/*code*/}\n}\n

image

语法大致与The E Editor的类似,下面来分析下实际的需求:

  • 每个snippet具有一个自己的模板
  • 对应的,每个snippet应该具有一个display content,与模板对应
  • 当编辑器使用Tab键时,进入一个snippet模式
  • 当snippet到达$0时,再按Tab键退出snippet模式

事实上,如果不存在模式,及与用户的交互的话,snippet会非常的简单,如:

   1: defaultSnippets = {
   2:     "inc_template" : "#include \"${1:idp_api.h}\"${0:}", 
   3:     "Inc_template" : "#include <${1:stdio.h}>${0:}", 
   4:     "td_template"  : "typedef ${1:int} ${2:my_int};", 
   5:     "struct_template" : "struct ${1:name}{\n\t${0:/*data*/}\n};\n", 
   6:  
   7:     "if_template"  : "if(${1:cond}){\n\t${0:/*code*/}\n}\n", 
   8:     "ifel_template" : "if(${1:cond}){\n\t${2:/*code*/}\n}else{\n\t${3:/*code*/}\n}\n", 
   9:     "while_template" : "while(${1:cond}){\n\t${0:/*code*/}\n}\n", 
  10:     "for_template" : "for(int ${1:i} = ${2:0}; ${1:} < ${3:count}; ${1:} += ${4:1}){\n\t${0:/*code*/}\n}\n", 
  11:     "dowhile_template" : "do{\n\t${0:/*code*/}\n}while(${1:cond});\n", 
  12:  
  13:     "main_template" : "int main(int argc, char *argv[]){\n\t${0:/*code*/}\n\treturn 0;\n}\n",
  14:     "static_tempalte" : "int i = 0;\n"
  15: }
  16:  
  17: if __name__ == "__main__":
  18:     #inc = TouchSnippet(inc_template)
  19:     for key in defaultSnippets:
  20:         """
  21:         if not key == 'for_template':
  22:             continue
  23:         """
  24:         snippet = TouchSnippet(defaultSnippets[key])
  25:         snippet.Build()
  26:         result = snippet.Arrange()
  27:         print result
 
snippet具有Build方法,用以将模板加载并分析成内部的结构,Arrange方法则将模板解释,并将最后需要插入到编辑器的内容返回:
 
   1: typedef int my_int;
   2: #include <stdio.h>
   3: for(int i = 0; i < count; i += 1){
   4:     /*code*/
   5: }
   6:  
   7: do{
   8:     /*code*/
   9: }while(cond);
  10:  
  11: if(cond){
  12:     /*code*/
  13: }
  14:  
  15: int i = 0;
  16:  
  17: #include "idp_api.h"
  18: struct name{
  19:     /*data*/
  20: };
  21:  
  22: int main(int argc, char *argv[]){
  23:     /*code*/
  24:     return 0;
  25: }
  26:  
  27: if(cond){
  28:     /*code*/
  29: }else{
  30:     /*code*/
  31: }
  32:  
  33: while(cond){
  34:     /*code*/
  35: }
  36:  
 
snippet还需要一组方法:
  • 获取下一个编辑点的位置
  • 获取上一个编辑点的位置
  • 当编辑器处于snippet模式,且用户正在输入字符之后,则需要更新snippet的长度,及下一次的编辑点位置
   1: def NextPos(self):
   2:     if self.stop:
   3:         return (-1, -1)
   4:     next = self.it[self.index]
   5:     self.last = self.index
   6:     self.index += 1
   7:     if self.index == self.itlen:
   8:         self.index = 0
   9:     
  10:     if next['index'] == "0":
  11:         self.stop = True
  12:     return next['span']
  13:  
  14: def PrevPos(self):
  15:     if self.stop:
  16:         return (-1, -1)
  17:     prev = self.it[self.index]
  18:     self.last = self.index
  19:     self.index -= 1
  20:     if self.index < 0:
  21:         self.index = self.itlen - 1
  22:  
  23:     #if jumped to the last tag, stop
  24:     if prev['index'] == "0":
  25:         self.stop = True
  26:     return prev['span']
  27:  
  28: def CurrentPos(self):
  29:     return self.index
  30:  
  31: def GetVarCount(self):
  32:     return self.itlen
  33:  
  34: def Update(self, newchar):
  35:     item = self.it[self.last]
  36:     #print item, chr(newchar)
  37:     if self.last == self.prev:
  38:         self.temp += chr(newchar)
  39:     else:
  40:         self.temp = ''
  41:         self.temp = chr(newchar)
  42:     self.prev = self.last
  43:  
  44:     offset = 0
  45:     offset = len(self.temp) - item['length']
  46:     item['length'] = len(self.temp)
  47:     start, end = item['span']
  48:     item['span'] = (start, start+len(self.temp))
  49:  
  50:     for i in range(self.last+1, self.itlen):
  51:         s, e = self.it[i]['span']
  52:         self.it[i]['span'] = (s+offset, e+offset)

然后就是在TouchEditor中,为keypress事件添加对Tab的处理:

   1: #The very first time of BundleMode, init the snippetPos, gotoline, etc
   2: if word in self.snippets.keys() and not self.isBundleMode:
   3:     self.isBundleMode = True
   4:     instance = TouchSnippet.TouchSnippet(self.snippets[word])
   5:     instance.Build()
   6:     snippet = instance.Arrange()
   7:     self.currentSnippet = instance
   8:     self.DelWordLeft()
   9:     self.AddText(snippet)
  10:     self.GotoLine(line)
  11:     self.snippetLine = line
  12:     self.snippetPos = pos - len(word)
  13:  
  14: #move to next position
  15: if self.isBundleMode:
  16:     if event.ShiftDown():
  17:         start, end = self.currentSnippet.PrevPos()
  18:     else:
  19:         start, end = self.currentSnippet.NextPos()
  20:  
  21:     if (start, end) == (-1, -1):
  22:         self.isBundleMode = False
  23:         self.snippetPos = -1
  24:         self.currentSnippet = None
  25:         return 
  26:  
  27:     self.SetSelection(self.snippetPos+start, self.snippetPos+end)
  28: else:
  29:     event.Skip()
当输入的简写与snippet的名称一致时(即,输入了”for”之后,按Tab键),进入snippet模式,实例化snippet,并调用snippet的Build和Arrange来计算最终的字符串。

Comments