sublime text 插件开发

前言:术语和参考资料

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,简短的插件开发入门也可以参考此文章。)
 
 
posted @ 2015-06-11 14:52  *骑着蜗牛游世界*  阅读(446)  评论(0编辑  收藏  举报