sublime插件开发手记
标题: 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” .
- 官方的文档: api 说明文档 还是要有个概要的了解,尽量去理解 API 都可以干什么。
- 安装目录下的 sublime.py 中有 api 定义, 包括一些新增没在官网文档的 api ; sublime_plugin.py 中有定义加载的事件等插件相关处理。
- Sublime 3 默认情况下安装的包是 zip 打包的,可以解压缩以后放到 data/package 下调试,会自动覆盖压缩文件的情况:多安装一些插件 没事看看某些插件的功能是怎么实现的,改完了保存即可生效。
- 务必打开 console , 随时观察其中是输出情况,print() 的调试信息也在这里看
- console 中 sublime, window, view 等都已经是代表程序、窗口和视图对象,结合 python 的自省函数,比如
dir(view)[20:30]
可以分段查看 view 提供的方法, 并在 console 进行简单调试,类似
open("t.txt", "a").write(str(dir(view)))` 可以拿到 view 所有方法 - 不想打开 console 的调试输出
sublime.status_message("hickdebug " + time.strftime("%Y-%m-%d %X - ") + markId)
, 或者弹出式的窗口 sublime.error_message('1111') - 调用外部程序可以用 python 那一套, import os 以后通过 os.system 和 os.popen 都可以。所以 windows 下打开浏览器、资源管理器等都很方便了: 控制台输入
os.system("explorer /e,f:\\Python")
。system 方法会闪一个命令行窗口,popen 不会。 - 安装目录下的 Packages 子目录有不少可以解压缩参考学习,尤其是 Default 包是默认的组件,很多小功能值得参考。
- Packages/Default/delete_word.py 删除光标左侧或右侧的一个单词
- Packages/Default/duplicate_line.py 复制当前行
- Packages/Default/goto_line.py 提示用户输入,然后更新选区
- Packages/Default/font.py 显示如何使用配置
- Packages/Default/mark.py 使用add_regions()向行号前的地方加一个图标
- Packages/Default/trim_trailing_whitespace.py 修改缓冲区刚好在它保存前
特别说明,汉化版基本上是针对安装目录 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:
- 一个 view 可以认为是打开的一个 tab
Edit:
继承自 sublime_plugin.TextCommand 的命令, Edit 为第一个参数。 打开一个 view 以后必须要指定 Edit 才能真正的 insert 字符。需要 view.begin_edit 开始使用编辑区,使用完要 end_edit 。根据 sublime 3 API 的说法,只能继承,不能被创建。
Selection(RegionSet):
- 一个选区是一个范围
Window:
- 打开的 sublime text 窗口
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:
- Point 是跟文本开头位置的偏移量
- 获得当前光标位置的 point: view.sel()[0].a
- 一个 region 的属性 a b 为起始的俩个 point
Region:
- view.sel() 可以获得当前的所以选择区,因为 sublime 支持同时选择多区域,所以单选区 时要获得当前选区也必须是
view.sel()[0]
dd - view 的 find_all 等可以匹配一系列的 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 类,类似的一共三种,至少需要继承上面的三种命令之一:
- Text Command 文字处理型命令 提供对文件和 buffer, 简单的理解就是打开的 tab 的操作, 继承自该对象的类下可以用 self.view.window() 获得下面的 Window Command 对象, fun 方法接受两个参数 self, edit
- Window Command 对应当前窗口的命令, 继承自该对象的类中可以用 self.window.active_view() 获得当前活动 view , run 方法一个参数 self
- Application Command 调用 sublime 本身的程序命令
与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 使用参考
几个有用的信息:
- Region 虽然 print 出来是 tuple, 但是不能直接定义为 tuple, 需要类似这样: full_edit = sublime.Region(0, toc_view.size()) , 其他的举一反三
- 默认键设置中把一些基本操作映射为 sublime 命令了,值得好好学学,比如回车 enter 是这样定义的
{ "keys": ["enter"], "command": "insert", "args": {"characters": "\n"} },
- 默认带的 python 模块可能不够用,可以通过追加 sys.path 增加使用外部模块寻找路径,不过需要特别注意 python 版本的问题:
sys.path.insert(0, 'E:\\MyDocument\\KuaiPan\\source\\utls\\spide')
分栏(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')
可以获得当前的语法文件。
参考资料:
- sublime插件开发
- Sublime Text 2 文档
- sublime text 手册台湾繁体版
- Sublime Text 3 插件的汉化、开发、发布方法教程
- 如何编写一个Sublime Text 2插件
- Sublime Text - 插件开发API参考
- Extending Sublime Text
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)