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