标题: sublime插件开发手记

时间: 2014-01-05 14:58:02

正文:

文中把 sublime text 简称 sublime 了。我是安装 portable 版本,非 portalble 版本可能有些差异,需要酌情变通。

sublime 用 python 作为插件扩展语言是十分讨人喜的, 学习门槛低, 不会 Python 也能依样画葫芦折腾些功能。 如果喜欢 sublime , 不妨多花点时间折腾学习下, 以下是 hick (http://blog.hickwu.com)同学学习过程中遇到的一些实用的方法和渠道, 整理的有些乱,是陆续收集网文和记录自己实践的汇总。主要针对 sublime 3 , sublime 2 的稍有差异,仅供参考,欢迎补充, 尝试建了个 sublime 插件开发交流 QQ 群 285447128, 有兴趣的可以加下, 请注明“sublime” .

特别说明,汉化版基本上是针对安装目录 packages 下的 Default.sublime-package 包,还原该包可以还原大多数被汉化的菜单等。

        转载请注明出处 http://blog.hickwu.com/posts/315 by Hick

插件基本结构

sublime 的安装后的目录分为安装目录和数据目录。windows 下建议使用 portable 版, 安装目录下的 Data 为数据目录;非 portable 版则是把数据目录放到了 %APPDATA% 下,比如我的为 C:\Documents and Settings\Administrator\Application Data , 通常这里的文件容易丢失,不建议放这里。

Packages(包组) 目录, 编辑器所支持的编程或标记语言的所有资源都被存放在这里。Preferences | Browse Packages, 也可以通过调用sublime.packages_path()这个api来访问。

User(用户) 包 Packages/User 这个目录是一个存放所有用户自定义的插件、代码片段、宏等等的大杂烩。请把这个 目录看成是你在包组目录中的私人领地。Sublime Text 2在升级的过程中永远不会覆盖*Packages/User* 这个目录中的内容。

发现一个有趣的现象: 插件中读取配置会从 Data\Packages 下的插件包读取,但是保存的时候,会保存的 Data\Packages\User 下。我理解这是有意思的设计:原包配置等一律不动,以防升级丢失,而读取的时候,会先读原包配置,再从 Data\Packages\User 读取,这是一种类似类继承的很合理的操作方式,实际上可以看到多个菜单中 sublime text 都有 Default 和 User 的区别,该区别正是在于此。实际上 User 目录下保存着用户相关的很多配置,包括按键、snippet 等等。

Data/Installed Packages 目录和 Data/Packages 下都可以放插件包,根据实测,以后者优先。甚至默认安装的包在 Packages 目录下。

Packages/Markdown.sublime-package 中包括 Markdown.tmLanguage 这样的语法定义文件; Symbol List - Heading.tmPreferences 这样的 symbol (Ctrl + r) 定义 。 我 hack 了下让 ctrl + r 出来的缩进清晰一边定位: 第 16 行去掉 # 的地方修改成 s/(?<=#)#/ - /g;

官方对几个概念没有特别做说明,实际上理解了编辑器这些说法才好进行插件开发:

View:

Edit:

继承自 sublime_plugin.TextCommand 的命令, Edit 为第一个参数。 打开一个 view 以后必须要指定 Edit 才能真正的 insert 字符。需要 view.begin_edit 开始使用编辑区,使用完要 end_edit 。根据 sublime 3 API 的说法,只能继承,不能被创建。

Selection(RegionSet):

Window:

line:

这里看到 line 返回的是鼠标所在行的起始字符数,从文件头开始,算 UTF8 字符,一个中文也是一个字符,无论光标在何处,都是这样的结构(行开始字符数, 行结束字符)

        mark = self.view.sel()[0]
        line = self.view.line(mark.a)
        sublime.status_message("hickdebug "  + time.strftime("%Y-%m-%d %X - ") + str(line))

Point:

Region:

基本插件实现

再次啰嗦,开发插件的话务必打开 console 随时观察异常情况。

Tools(工具)菜单里有一项“new Plugin 新插件"菜单可以;也可以找一个 别人的小插件修改。

Main.sublime-menu 菜单文件, 类似下面这样的定义:

[{ 
"id": "edit","children":[ 
        {"caption": "清除剪贴板中空行并粘贴","command": "paste_without_blank_lines"} 
    ] 
}] 

向菜单id为edit的菜单(即“edit/编辑”菜单)中添加项目。要搞清楚id和caption不同,id用于标识真正的身份,caption只作为在菜单中的显示内容。

*.sublime-keymap 热键设置, 可以为不同平台做不同的定义。也可以定义在 一起。 { "keys": ["ctrl+alt+shift+l"], "command": "paste_without_blank_lines" }

Default.sublime-commands 在“Ctrl+Shift+P”的命令面板中增加插件所 含的指令供调用。

当一个插件只有一个PY文件时,该文件名和插件的名称可以不同,ST会自动调用这个唯一的PY程序。下面是一个简单的例子:

import sublime, sublime_plugin  # 必须要引用这两个基础类库
import re   # 本插件需要用到的正则表达式类库。
class PasteWithoutBlankLinesCommand(sublime_plugin.TextCommand): 
    """
       进行多行注释:每个菜单命令都对应于一个类。注意类名的写法,
       是把菜单命令的下划线去掉,改成驼峰式写法,并且在末尾加上Command。
        括号中 sublime_plugin.TextCommand 是此类的父类,表示此类 是一个
        命令菜单的实际行为类。如果不是命令菜单引起的而是由于窗口命令引
        起的实际行为类,父类就要指定为 sublime_plugin.WindowCommand 。
    """
    # def表示定义一个方法。ST插件机制会自动调用指令类的run方法,
    # 所以必须重载实现此方法以供执行。
    def run(self, edit):  
        s = sublime.get_clipboard()     # 获取剪切板内容
        """
       从ST文件视图配置中读取默认行结束符的类别(用操作系统环境表示)。
       因为不同的操作系统对硬回车的表示和存储方式不同,而这个插件正是
       需要对这些进行处理。如果你的插件也涉及操作系统的分别或者是配置的分别,
       都需要考虑按此方法先读取相应的配置,再根据配置进行不同的处理。
        """
        line_ending = self.view.settings().get('default_line_ending') 
        # 根据不同的操作系统环境进行不同的替换处理。
        if line_ending == 'windows': 
            s = re.compile('\n\r').sub('',s) 
            s = re.compile('\r\n\s*\r\n').sub('\r\n',s) 
        elif line_ending == 'mac': 
            s = re.compile('\r\r').sub('\r',s) 
            s = re.compile('\r\s*\r').sub('\r',s) 
        else: # unix / system
            s = re.compile('\n\n').sub('\n',s) 
            s = re.compile('\n\s*\n').sub('\n',s) 
        # 修改剪贴板内容,此方法可使减肥过的剪贴板内容在别处也能使用
        sublime.set_clipboard(s)   
        self.view.run_command('paste')    # 调用粘贴命令

保存好以后,在 sublime console 里输入view.run_command(PasteWithoutBlankLines) 就能看到效果。进一步说明下,上面继承的是 TextCommand 类,类似的一共三种,至少需要继承上面的三种命令之一:

与Installed Packages文件夹同级的Packages文件夹,可以说是专为调试插件准备的,在其中无论是对菜单还是对命令实现的程序进行更改,都会即时反应到 sublime 中。

很多插件都会自带一个配置文件(以 .sublime-settings 后缀结尾的文件),用以配置用户的参数,我们也可以将一些有可能修改的字段定义在插件配置文件中,日后便不用修改代码,直接修改配置文件即可。

下面是配置文件示例(JSON格式):

/* ScriptOgr default setting */
{
    "proxy_server" : "",
    "user_id" : "",
    "base_url": "http://scriptogr.am/api/article/"
}

读取文件配置的代码如下,调用一下 Sublime Text 2 提供的 API 即可

settings = sublime.load_settings('ScriptOgrSender.sublime-settings')
base_url = settings.get('base_url')
self.user_id = settings.get('user_id')
self.proxy_server = settings.get('proxy_server')

text command类下 self.view.window() 可以获得当前窗口 window 对象,可以通过self.view来访问当前的view ,view的sel()方法返回当前所有选择区段的一个iterable。

线程处理

当需要处理一些网络请求时,主线程创建诸如 http 请求,则整个 sublime 可能会被挂住而失去响应,直到请求完成才会醒过来。因此我们需要将请求代码放到另外一个单独的线程中进行。

thread = ScriptOgrApiCall(filename, content, 'post', self.user_id, \
    self.proxy_server, 500)
threads.append(thread)
thread.start()

参考的原文原作者是把内容发到博客上发表,为了将我们需要提交的内容放置到单独的线程中,我创建了一个单独的 ApiCall 类,负责接收请求的内容。线程初始化时,定义了好几个参数以传入线程中进行处理,参数基本对应 ScriptOgr.am 中的 API 请求而制定的:

class ScriptOgrApiCall(threading.Thread):
    """docstring for ScriptOgrApiCall"""
    def __init__(self, filename, filedata, operation, user_id, proxy_server, timeout):
        threading.Thread.__init__(self)
        self.filename  = filename
        self.filedata  = filedata
        self.operation = operation
        self.timeout   = timeout
        self.user_id   = user_id
        self.proxy     = proxy_server
        self.response  = None
        self.result    = None

线程执行完毕后,将服务器返回的 JSON 解析后输出赋予线程中的 response 属性。解析 JSON 时使用的是 Python 自带的 JSON 模块。

def parse_response(self):
    response = json.loads(self.response)
    if response['status'] == 'success':
        if self.operation == 'post':
            self.response = 'Successfully post your article'
        elif self.operation == 'delete':
            self.response = 'Successfully delete your article'
    elif response['status'] == 'failed':
        self.response = response['reason']

在创建自己的命令时,先行创建一个命令基类,定义一些需要重复使用的函数:

class ScriptOgrCommandBase(sublime_plugin.TextCommand):
    """docstring for CommandBase"""
    def __init__(self, view):
        # Inherit from class TextCommand
        sublime_plugin.TextCommand.__init__(self, view)

创建好基类后,定义一个线程管理类,负责监控线程的运行情况,如线程完成后,则打印出服务器返回的信息:

def handle_threads(self, threads, i=0, dir=0):
    next_threads = []
    for thread in threads:
        if thread.is_alive():
            next_threads.append(thread)
        else:
            print '\nScriptOgr.am api response: ' + thread.get_response() + '\n'
            sublime.status_message('ScriptOgr.am api response: ' + thread.get_response())
        if thread.result == False:
            continue
    threads = next_threads

    if len(threads):
        before = i % 8
        after = (7) - before
        if not after:
            dir = -1
        if not before:
            dir = 1
        i += dir
        self.view.set_status('operating', 'ScriptOgrSender is opearting [%s=%s]' % \
            (' ' * before, ' ' * after))

        sublime.set_timeout(lambda: self.handle_threads(threads, i, dir), 100)
        return
    self.view.erase_status('operating')
包发布管理

如果需要将你亲自编写的插件发布到 Package Control 上,可以参照官网上的说明(点击查看)。 似乎上边的链接被重定向,换 这个 了。

据说要发布插件的话不能有中文,否则通不过审核。

sublime 的语法解析

获得和使用当前 view 使用的语法解析文件参考下面的 view.set_syntax_file 。 需要定制,搜索 "sublime custom syntax highlighting"、"tmlanguage sublime"。

根据使用的经验,光有语法定义,解析了语法元素还不够,还需要 color theme 支持,比如我的 markdown 默认的情况下标题就是没有颜色的,选择 Twilight 以后就有了。默认的主题 theme 相关信息应该是定义在 Packages/Theme - Default.sublime-package , 更具体的配色方案 定义在 Color Scheme - Default.sublime-package 中(用类似 winrar 打开直接编辑保存即可)。

一直想实现 markdown 不同的级的标题跟 emacs 一样不同颜色显示,找到语法定义文件, 找到 "markup.heading" , 不过没看出来什么。以后再说。

现在用的是 twilight 注意,压缩软件打开 Color Scheme - Default.sublime-package 中的 Twilight.tmTheme 以后, 搜索 heading 找到 Markup: Heading , 修改其中的颜色成功。 需要因为压缩文件已经被 sublime 打开,需要关闭 sublime 以后才可以保存刚才的修改。注意 该修改改变的是各级标题前导的 # 号的颜色, 另外我还增加了背景色:

<dict>
    <key>name</key>
    <string>Markup: Heading</string>
    <key>scope</key>
    <string>markup.heading</string>
    <key>settings</key>
    <dict>
        <key>background</key>
        <string>#562D56</string>
        <key>foreground</key>
        <string>#ff0000</string>
    </dict>
</dict>

不过貌似背景色定义不是常规的颜色编码,搜索其他地方也有奇怪的定义,效果也还可以,暂不细究。

markdown 的语法定义打开是在对应压缩包的 Markdown.tmLanguage 中搜索 <key>heading</key>

尝试拷贝一份,新定义一个以 @ 替代 # 号的, 外层 key 可能是唯一的, name.string 暂时不动,后边试改:

    <key>heading-at</key>
    <dict>
        <key>begin</key>
        <string>\G(@{1,6})(?!#)\s*(?=\S)</string>
        <key>captures</key>
        <dict>
            <key>1</key>
            <dict>
                <key>name</key>
                <string>punctuation.definition.heading.markdown</string>
            </dict>
        </dict>
        <key>contentName</key>
        <string>entity.name.section.markdown</string>
        <key>end</key>
        <string>\s*(#*)$\n?</string>
        <key>name</key>
        <string>markup.heading.markdown</string>
        <key>patterns</key>
        <array>
            <dict>
                <key>include</key>
                <string>#inline</string>
            </dict>
        </array>
    </dict>

终于搞定了! markdown.tmLanguage 三处, TwilightTheme 一处。当匹配规则有重复定义时,发现以第一次定义的为准。

发现链接内文字用的是 Twilight 的这个 String

    <dict>
        <key>name</key>
        <string>String</string>
        <key>scope</key>
        <string>string</string>
        <key>settings</key>
        <dict>
            <key>fontStyle</key>
            <string></string>
            <key>foreground</key>
            <string>#8F9D6A</string>
        </dict>
    </dict>
API 使用参考

几个有用的信息:

分栏(columns)在全局键能看到是 set_layout 命令,参数 {"cols": [0.0, 0.85, 1.0], "rows": [0.0, 1.0], "cells": [[0, 0, 1, 1], [1, 0, 2, 1]]} 。 该命令在命令行可以用 window.set_layout 调用,但是不能在 sublime 以及 view 对象下调用。比如这样

    window.set_layout({"cols": [0.0, 0.85, 1.0], "rows": [0.0, 1.0], 
        "cells": [[0, 0, 1, 1], [1, 0, 2, 1]]})

view.insert(edit, point, string)

第一个参数出乎一般思路之外, edit 作为 sublime 的编辑单元,需要单独创建, sublime 2 中可以这样:

edit = view.begin_edit() 
view.insert(edit, 0, 'Hello')
view.end_edit(edit)

搜索参考 markdown-preview 的作法,sublime 3 上没有使用 begin_edit 来插入字符, 而是用 view.run_command 调用 append 来插入字符,示例:

view.run_command('append', {'characters': 'I am Hick', 'force': True, 'scroll_to_end': True})

view.sel() -> Selection

获得当前选择点,注意可能是多个选择点,比如居中的时候,一般除了 view.show_at_center 以外,一般会清除当前所有选择点,并在当前 Region 或者 Point 添加选择光标:

regions = tocedview.find_all(line_txt)
tocedview.show_at_center(regions[0])
tocedview.sel().clear()
tocedview.sel().add(regions[0].a)

view.set_syntax_file(syntax_file) -> None

设定 view 的语法, 参数为语法文件路径,比如 Packages/Python/Python.tmLanguage , 命令窗口执行 view.settings().get('syntax') 可以获得当前的语法文件。

参考资料:

查看更多文章
分享到:


分享到: