前言:术语和参考资料
sublime text 2的扩展模式相当的丰富。有多种方法可以修改语法高亮模式以及所有的菜单等。它还可以创建一个新的build系统,自动补全,语言定义,代码片段,宏定义,快捷键绑定,鼠标事件绑定和插件。所有这些都是通过文件构成的包来实现。
一 个包就是在'Packages'目录下的一个文件夹,可以通过Preferences > Browse Packages…菜单访打开改目录。也可以把一个包大包成一个zip文件,然后把扩展名改成.sublime-package。后面会有更多关于打包的 介绍。
Sublime默认就捆绑了很多包。大部分的包都是跟特定语言相关的。包里面包含了语言定义,自动补全和build系统。另 外还有2个包:Default和User。Default包里包含了所有标准的键盘绑定,菜单定义,文件配置和一大堆用python写的插件。User包 比较特殊,它总是在最后加载。通过在User包里的自定义文件,它允许用户重写所有默认行为。
要写好插件,好手册当然是必须的:Sublime Text 2 API reference
Default包里的东西也是个很好的参考,可以掘墓下前人是如何做的,哪些是可能实现的。
大部分的编辑器都提供命令功能,除了输入字符之外的所有操作都可以通过命令来完成。Preferences > Key Bindings – Default 通过这个菜单可以看到所有内置的命令。
另外,sublime插件需要使用python开发,它内置了python环境,那个控制台其实也是个python控制台。
泪奔啊,貌似除了前端技术外我能懂的就是python了。。。
OK,了解了下插件和包机制,可以开始写个插件玩玩。
Step1-创建一个插件
sublime要写一个简单的插件,首先要创建一个python骨架的代码。
通过Tools > New Plugin…菜单就可以自动创建一个插件的样板。
import sublime, sublime_plugin
class ExampleCommand(sublime_plugin.TextCommand):
def run(self, edit):
self.view.insert(edit, 0, "Hello, World!")
import了2个模块,创建了一个command的类。我们先保存下并运行下试试。
保存的时候要创建一个包。保存弹出框默认是在Packages\User目录下,No,我们要创建一个自己的包保存。在Packages目录下创建一个Prefixr目录:
Packages/
…
- OCaml/
- Perl/
- PHP/
- Prefixr/
- Python/
- R/
- Rails/
…
然 后把文件包存在Prefixr目录下命名为Prefixr.py。(因为原文的这篇教程是基于创建Prefixr这个插件的,其实我们安装的 sublime里已经有了这个插件包,所以自己试验的话可以随便取个别的名字,把它当成另外一个插件就好了。)Prefixr.py这个文件也可以是其它 名字,但必须要.py文件后缀,最好跟插件目录的名称一致。
这样,插件就保存好了。打开sublime的控制台ctrl+`。这其实就是一个Python控制台,可以在里面运行python代码。在控制台输入:
view.run_command('example')
就可以看到"Hello World"被插入在当前编辑器里激活的文件的开头。
记得撤销下,然后继续。。。
Step2-Command类型和名称
sublime给插件提供了3中类型的command.
- Text Commands提供了对当前View对象(就是正在编辑的文件)内容的访问。
- Window Commands提供里对当前编辑器Window对象的引用。
- Application Commands不提供对任何window或者文件的引用,而且也很少用到。
这么看来,我们要对CSS文件进行编辑就得用到sublime_plugin.TextCommand这个类。所以我们这个Prefixr command就继承了sublime_plugin.TextCommand。
class ExampleCommand(sublime_plugin.TextCommand):
然后向运行这个插件的时候就在控制台执行
view.run_command('example')
sublime会把所有继承自sublime_plugin(TextCommand,WindowCommand,ApplicationCommand)的类都去掉Command后缀,然后把驼峰格式转换成下划线格式,当做command的名称
所以,要创建一个prefixr的command,class名称就是PrefixrCommand.
class PrefixrCommand(sublime_plugin.TextCommand):
(依 次类推,类名为MyTestCommand的话,command的名称就是my_test,而用view.run_command('example') 运行这个插件的时候,'example'就是command名称,所以类名为MyTestCommand的话,则用 view.run_command('my_test')运行)。
Step3-选择文本
很好,现在我们的插件终于有个名字了,虽然看起来还是有点屌丝的味道。我们开始从当前的buffer获取css然后传给Prefix API来做些事情了。Sublime一个很强悍的功能就是可以方面的进行多选择。我们现在写的这个插件呢,当然就需要处理所有选中的文本。
text command类下可以通过self.view来访问当前的view,view的sel()方法返回当前所有选择区段的一个iterable。首先,我们 扫描下有没有花括号,如果没有就扩大到外围选区,来确定整个区域的前缀。不管有没有花括号,都可以帮助我们确定是否需要对Prefixr API返回的结果进行空格,格式化处理。
braces = False
sels = self.view.sel()
for sel in sels:
if self.view.substr(sel).find('{') != -1:
braces = True
这段代码替代了run()方法的内容,直接执行。
如果我们没有找到花括号,就在查找每个选择最近的闭合的花括号,然后用内置的expand_selection命令,to参数设置为brackets 每个css规则区域内容就可以选中了。
if not braces:
new_sels = []
for sel in sels:
new_sels.append(self.view.find('\}', sel.end()))
sels.clear()
for sel in new_sels:
sels.add(sel)
self.view.run_command("expand_selection", {"to": "brackets"})
可以参考代码库里的Prefixr-1.py
Step4-线程
现 在,选取已经扩展到了每个css代码块。就要把它发送给Prefixr API了。不用仰望,这只是一个小小的HTTP请求而已,用用urlib,urllib2这等模块就好了。但是我们先想想看,一个缓慢的web请求会对编 辑器的性能造成什么影响。如果Prefixr API的响应太慢,各位大师应该会很焦躁的。。。
所以应该把把这个请求处理放在后台悄悄地进行。这就要用到线程了。
其实呢,线程这玩意是Python本身的能力,跟这个啥sublime是没太大关系的,是吧。
Step5-创建线程
这里就要用到threading模块,创建一个PrefixrApiCall继承自threading.gThread,需要实现run方法,里面包含了需要运行的代码。
class PrefixrApiCall(threading.Thread):
def __init__(self, sel, string, timeout):
self.sel = sel
self.original = string
self.timeout = timeout
self.result = None
threading.Thread.__init__(self)
def run(self):
try:
data = urllib.urlencode({'css': self.original})
request = urllib2.Request('http://prefixr.com/api/index.php', data,
headers={"User-Agent": "Sublime Prefixr"})
http_file = urllib2.urlopen(request, timeout=self.timeout)
self.result = http_file.read()
return
except (urllib2.HTTPError) as (e):
err = '%s: HTTP error %s contacting API' % (__name__, str(e.code))
except (urllib2.URLError) as (e):
err = '%s: URL error %s contacting API' % (__name__, str(e.reason))
sublime.error_message(err)
self.result = False
__init__()方法里设置了做web请求时需要的一些值。run()方法里包含了创建http,请求Prefixr API的代码。因为线程是跟其它代码同时运行的,所以不能直接返回值。所以用self.result来保存调用的结果。
因为我们这里引入了很多其它模块了,所以要在头部加入import申明:
import urllib
import urllib2
import threading
【吐槽一下,这是python本身的东西,python是写插件的基础,这里就不用过多讲了。。】
现在我们有了线程类来做http请求了,我们要给每段选区的css创建一个线程。回到PrefixrCommand类的run()方法,用下面的代码:
threads = []
for sel in sels:
string = self.view.substr(sel)
thread = PrefixrApiCall(sel, string, 5)
threads.append(thread)
thread.start()
记录下每个创建的线程,然后调用线程的start()方法来启动它。
可以参考代码库里的Prefixr-2.py
Step6-为结果做准备
在处理Prefixr API请求的响应结果前我们还需要做点处理。
首先,清除掉所有的选区,因为我们之前做了修改。
self.view.sel().clear()
另外创建一个Edit对象。指定一组prefixr操作,组操作就可以方便重做和撤销。
edit = self.view.begin_edit('prefixr')
最后,调用一个我们后面会写的方法来处理API请求的响应。
self.handle_threads(edit, threads, braces)
Step7-处理线程
现在我们的线程们应该已经在高调的运行了,或者有些才飞了一会就结束了。现在就要实现前面用到的handle_threads()方法。这个方法遍历线程list检测显示是否还在运行。
def handle_threads(self, edit, threads, braces, offset=0, i=0, dir=1):
next_threads = []
for thread in threads:
if thread.is_alive():
next_threads.append(thread)
continue
if thread.result == False:
continue
offset = self.replace(edit, thread, braces, offset)
threads = next_threads
如果线程还在运行,把它添加到一个线程列表中,留校继续查看。如果查看失败就忽略,不过为了有更好的效果,后面会写一个replace()方法。
另外,作为一个前端工程师,当然要懂点用户体验。我们可以在状态栏告诉用户我们的插件是在努力工作的,没有偷懒哦。
if len(threads):
# This animates a little activity indicator in the status area
before = i % 8
after = (7) - before
if not after:
dir = -1
if not before:
dir = 1
i += dir
self.view.set_status('prefixr', 'Prefixr [%s=%s]' % \
(' ' * before, ' ' * after))
sublime.set_timeout(lambda: self.handle_threads(edit, threads,
braces, offset, i, dir), 100)
return
(还是需要不少python的知识。。。)
当所有线程都完成之后,就可以结束撤销的组标记了,然后通知下用户。
self.view.end_edit(edit)
self.view.erase_status('prefixr')
selections = len(self.view.sel())
sublime.status_message('Prefixr successfully run on %s selection%s' %
(selections, '' if selections == 1 else 's'))
可以参考Prefixr-3.py文件代码
Step8-执行替换
正如前面提到的replace()方法,我们需要用Prefixr API返回的结果替换掉原来的css代码。
这个方法接受几个参数,撤销用的Edit对象,Prefixr API返回的结果,选区的偏移量
def replace(self, edit, thread, braces, offset):
sel = thread.sel
original = thread.original
result = thread.result
# Here we adjust each selection for any text we have already inserted
if offset:
sel = sublime.Region(sel.begin() + offset,
sel.end() + offset)
替换前对结果进行格式化一下,处理下空格,结束符等。
result = self.normalize_line_endings(result)
(prefix, main, suffix) = self.fix_whitespace(original, result, sel,
braces)
self.view.replace(edit, sel, prefix + main + suffix)
然后把选区扩展到新插入的CSS代码最后一个行的末尾,并返回便宜位置。
end_point = sel.begin() + len(prefix) + len(main)
self.view.sel().add(sublime.Region(end_point, end_point))
return offset + len(prefix + main + suffix) - len(original)
可以参考代码库里的Prefixr-4.py文件
Step9-处理空白
前面替换的时候用到了一个normalize_line_endings()方法,将换行符转换成当前文档的换行符。
def normalize_line_endings(self, string):
string = string.replace('\r\n', '\n').replace('\r', '\n')
line_endings = self.view.settings().get('default_line_ending')
if line_endings == 'windows':
string = string.replace('\n', '\r\n')
elif line_endings == 'mac':
string = string.replace('\n', '\r')
return string
fix_whitespace()方法处理css块的缩进,空格,只能对单个css块做处理。
def fix_whitespace(self, original, prefixed, sel, braces):
# If braces are present we can do all of the whitespace magic
if braces:
return ('', prefixed, '')
另外,判断下原始css中的缩进。
(row, col) = self.view.rowcol(sel.begin())
indent_region = self.view.find('^\s+', self.view.text_point(row, 0))
if self.view.rowcol(indent_region.begin())[0] == row:
indent = self.view.substr(indent_region)
else:
indent = ''
用当前文件的缩进设置来格式化prefixed的css
prefixed = prefixed.strip()
prefixed = re.sub(re.compile('^\s+', re.M), '', prefixed)
settings = self.view.settings()
use_spaces = settings.get('translate_tabs_to_spaces')
tab_size = int(settings.get('tab_size', 8))
indent_characters = '\t'
if use_spaces:
indent_characters = ' ' * tab_size
prefixed = prefixed.replace('\n', '\n' + indent + indent_characters)
用开头的空白来判断下新插入的CSS代码位置是否正确。
match = re.search('^(\s*)', original)
prefix = match.groups()[0]
match = re.search('(\s*)\Z', original)
suffix = match.groups()[0]
return (prefix, prefixed, suffix)
fix_whitespace()方法中用到了正则,所以要import re模块。
prefixr command就完成了,后面就是要做些快捷键绑定和菜单绑定了。
Step-10 键盘绑定
sublime 大部分的配置都可以通过json文件来完成,键盘绑定也一样。不过它的键盘绑定是区分系统的,所以基本上要建立3个文件,而且命名为Default (Windows).sublime-keymap, Default (Linux).sublime-keymap and Default (OSX).sublime-keymap
Prefixr/
...
- Default (Linux).sublime-keymap
- Default (OSX).sublime-keymap
- Default (Windows).sublime-keymap
- Prefixr.py
这个json文件里包含的是一个对象数组,每个对象需要包含keys,command,如果这个command需要参数的话还会有args。不过windows和linux的配置基本上差不多。
Preferences > Key Bindings – Default 可以通过这个菜单先查看下你想指定的快捷键是否已经被使用了。
[
{
"keys": ["ctrl+alt+x"], "command": "prefixr"
}
]
Step-11 修改菜单
sublime有个很爽的事就是通过创建.sublime-menu文件就可以修改菜单。配置文件需要更具要修改的菜单类型来命名:
Main.sublime-menu 控制了程序的主菜单
Side Bar.sublime-menu 控制侧边栏文件或者目录的右键菜单
Context.sublime-menu 控制处于编辑状态的文件右键菜单
通过这种接口,通过一个菜单配置文件就可能会影响到其它的各个菜单。可以看看Default包下的已有的菜单配置。
我们想给我们的Prefixr插件在Edit菜单下添加一个菜单项,然后在Preferences里添加配置菜单。下面是Edit里的菜单配置,Preferences里的配置有点长就省略了。
[
{
"id": "edit",
"children":
[
{"id": "wrap"},
{ "command": "prefixr" }
]
}
]
注意这里的id的就一个已经存在的菜单结构。
可以参考代码库里的文件 https://github.com/wbond/sublime_prefixr
step-12 发布你的插件包
现在写了一个非常有用的插件了,当然要分享给别人用用。
“Sublime支持zip文件或者一个包目录来分享插件包。把包目录打包成一个zip文件,然后把后缀改成.sublime-package,别人把这个文件放到插件包目录下重启sublime就安装完成了。“
另外一种方式就是通过Package Control的插件,专门来管理插件安装的,相信你已经安装了。可以通过下面的步骤进行:
1).你需要有个github帐号,并fork https://github.com/wbond/package_control_channel
2).通过git clone命令下载你fork完的地址,如: git@github.com:welefen/package_control_channel.git
3).修改repositories.json这个文件,把你的插件名称和对应的github项目地址添加进去
4).ci并push到你的package ctrol里,然后通过pull 5).request推到官方的github里,如果他们审批通过了,那么你的插件就会放到package control里,别人就可以通过install直接安装了
(上面这段引用网络已有文章:
http://www.welefen.com/how-to-develop-sublime-text-plugin.html,简短的插件开发入门也可以参考此文章。)