如何开发一个 Atom 插件
quzx
7年前
<p><img src="https://simg.open-open.com/show/6cfba6f848cb8eb9be6af5abc3970a56.png"></p> <h2><strong>准备</strong></h2> <ul> <li>工具: <a href="/misc/goto?guid=4959751017650928913" rel="nofollow,noindex"> Atom </a></li> <li>语法: <a href="/misc/goto?guid=4959751017738763826" rel="nofollow,noindex"> ES6 </a></li> </ul> <h2><strong>基础知识</strong></h2> <p>在开始编写插件之前,了解一些基本的 Atom 知识是必要的。以下是我根据官方文档加自己在开发过程中的一些理解的归纳,没有包含所有的细节,但在稍微复杂点的插件中基本都会用到。</p> <h2><strong>1、生成一个插件</strong></h2> <p>Atom 生成插件很简单,打开命令面板( cmd+shift+p )输入“Generate Package”,将出现一个对话框,输入你将要建立的包名字,回车即可。Atom 会自动创建一个已刚输入的包名字命名的文件夹,里面包含了默认生成的文件,默认的位置在 ~/atom/package 中,其目录结构如下:</p> <pre> <code class="language-javascript">my-package ├─ keymaps/ ├─ lib/ ├─ menus/ ├─ spec/ ├─ styles/ └─ package.json</code></pre> <p>其实,基本的路径结构还包括 snippets 和 grammars 目录,下面实战中将会用到 snippets,用来存放自定义的代码段文件。</p> <p><strong>keymaps</strong></p> <p>我们可以为插件自定义快捷键,形如上面创建包的 kemaps/my-package.json 文件中定义:</p> <pre> <code class="language-javascript">{ "atom-workspace": { "ctrl-alt-o": "my-package:toggle" } }</code></pre> <p>这样,我们在键盘上按组合键 ctrl+alt+o 就会执行命令 my-package:toggle, 其中键 atom-workspace 是设置快捷键起作用范围,这里 atom-workspace 元素是 Atom UI 的父级,所以只要在工作区域按快捷键就可触发命令。如果定义 atom-text-editor,那么就只能在编辑区域触发命令。 <a href="/misc/goto?guid=4959751017821270907" rel="nofollow,noindex"> 了解更多 </a></p> <p><strong>lib</strong></p> <p>该目录下是主要是实现插件功能的代码,并且必须包含插件的主入口文件(其将在 package.json 的 main 字段指定。如不指定,默认 lib 下的 index.js 或 <a href="/misc/goto?guid=4959751017906809903" rel="nofollow,noindex"> index.coffee </a> 文件), 主入口文件可以实现以下基本方法:</p> <ul> <li>config: 该对象中可以为包自定义配置。</li> <li>activate(state):该方法是在包激活的时候调用。如果你的包实现了 serialize() 方法,那么将会传递上次窗口序列化的 state 数据给该方法</li> <li>initialize(state):( Atom1.14 以上版本可用 ) 类似于 activate(), 但在它之前调用。intialize() 是在你的发序列化器或视图构建之前调用,activate() 是在工作区环境都已经准备后调用。</li> <li>serialize():窗口关闭后调用,允许返回一个组件状态的 JSON 对象。当你下次窗口启动时传递给 activate() 方法。</li> <li>deactivate():当窗口关闭时调用。如果包正在使用某些文件或其他外部资源,将在这里释放它们。</li> </ul> <p><strong>styles</strong></p> <p>styles 目录下存放的是包的样式文件。可以使用 CSS 或 LESS 编写。</p> <p><strong>snippets</strong></p> <p>snippets 目录下存放的是包含常用的代码段文件,这样就可以通过输入缩写前缀快速生成常用的代码语法,实战中将会详细讲解。</p> <p><strong>menus</strong></p> <p>该目录下存放的是创建应用菜单和编辑区域菜单文件。形如:</p> <pre> <code class="language-javascript">{ "context-menu": { "atom-text-editor": [{ "label": "Toggle my-package", "command": "my-package:toggle" }] }, "menu": [{ "label": "Packages", "submenu": [{ "label": "my-package", "submenu": [{ "label": "Toggle", "command": "my-package:toggle" }] }] }] }</code></pre> <p>context-menu 字段定义的上下文菜单,通过在你定义的元素(示例中是 atom-text-editor 字段)范围内点击右键呼出菜单栏,点击 Toggle my-package 会执行 my-package:toggle 命令。</p> <p>menu 字段定义应用的菜单,其出现在 Atom 主菜单栏中,定义类似 context-menu。</p> <h2><strong>2、配置</strong></h2> <p>为了让开发的包可配置化,Atom 为我们提供了 Configuration API。</p> <p>atom.config.set 方法为包写入配置。</p> <p>atom.config.setSchema 方法为包写入配置 schema, 下面示例 demo 中定了一个 type 为 string 的枚举类型 schema。 <a href="/misc/goto?guid=4959751017993225199" rel="nofollow,noindex"> 了解更多 </a></p> <p>atom.config.get 方法读取包配置。</p> <p>下面是一些 demo:</p> <pre> <code class="language-javascript">const versionSchema = { title: 'Element Version', description: 'Document version of Element UI.', type: 'string', default: '1.3', enum: ['1.1', '1.2', '1.3'], order: 1 }; // 设置配置schema atom.config.setSchema('element-helper.element_version', versionSchema); // 修改配置的element-helper.element_version值为1.2 atom.config.set('element-helper.element_version', '1.2'); // 获取配置element-helper.element_version atom.config.get('element-helper.element_version'); // 1.2</code></pre> <p>为了监听 config 的变化 Atom 提供了 observe 和 onDidChange 两个方法。前者会在指定keypath 的值时立即调用它的回调函数,以后改变也会调用。后者则是在 keypath 下次改变后调用它的回调函数。使用 demo 如下:</p> <pre> <code class="language-javascript">atom.config.observe('element-helper.element_version', (newValue) => { console.log(newValue); }); atom.config.onDidChange('element-helper.element_version', (event) => { console.log(event.newValue, event.oldValue); });</code></pre> <h2><strong>3、作用域</strong></h2> <p>Atom 中的作用域是一个很重要的概念,类似于 CSS 的 class,通过作用域的名称来选择操作作用的范围。打开一个 .vue 文件,按 alt+cmd+i 打开开发者工具,切换到 Elements 选项。会看到类似如下的 HTML 结构:</p> <p><img src="https://simg.open-open.com/show/3d1af9b7bfdcfc1b8f602ef41a7a15e0.png"></p> <p>图上 span 元素的 class 的值就是作用域名称。那怎么来利用作用域名称来选择作用范围呢?比如要选择 .vue 文件中所有元素标签名称作用范围。其实就像 CSS 选择器一样,如 “text.html.vue .entity.name.tag.other.html” 是选择 .vue 文件中所有标签名称节点,注意这里是不要加 syntax-- 前缀的,Atom 内部会处理。其可用于 snippets、config 等需要限定作用范围的功能中,后面实战中很多地方都有用到。 <a href="/misc/goto?guid=4959751018088159030" rel="nofollow,noindex"> 了解更多 </a></p> <h2><strong>4、package.json</strong></h2> <p>类似于 Node modules,Atom 包也包含一个 package.json 文件,但是 Atom 拥有自己定义的字段,如 main 指定包的主入口文件;activationCommands 指定那些命令可以激活入口文件中 activate 方法;menus, styles, keymaps, snippets 分别指定目录,样式,快键键映射,代码段的文件加载位置和顺序。 <a href="/misc/goto?guid=4959751018177107965" rel="nofollow,noindex"> 了解更多 </a></p> <h2><strong>5、与其他包交互</strong></h2> <p>可以在 package.json 中指定一个或多个版本号的外包来为自己提供服务。如:</p> <pre> <code class="language-javascript">"providedServices": { "autocomplete.provider": { "versions": { "2.0.0": "provide" } } }</code></pre> <p>这里使用 autocomplete+ 包提供的 provide API,版本是 2.0.0。这样只要在 package.json 中main 字段指定的主文件中实现 provide 方法,就可以在包激活的任何时候调用。如:</p> <pre> <code class="language-javascript">export default { activate(state) {} provide() { return yourProviderHere; } }</code></pre> <h2><strong>实战</strong></h2> <p>经过简单的基础知识介绍,是不是有点跃跃欲试了?好,满足你。接下来我们将通过实战来运用这些知识,深入了解其原理和工作机制。下面示例都是以 <a href="/misc/goto?guid=4959751018255910579" rel="nofollow,noindex"> Element-Helper </a> 插件为样本,你先不必着急去看源码。</p> <h2><strong>1、自动补全</strong></h2> <p>这里通过使用 <a href="/misc/goto?guid=4959751018343798863" rel="nofollow,noindex"> autocomplete+ </a> 提供的服务( <a href="/misc/goto?guid=4959751018431031031" rel="nofollow,noindex"> Provide API </a> )来实现自动补全功能。关于怎么建立交互请看基础知识中的第 5 小节,那么我们要实现的就是定义自己的 yourProiverHere, 它是一个对象包含如下示例中的方法:</p> <pre> <code class="language-javascript">const provider = { // 选择器,指定工作的作用域范围,这里是只有在选择器 '.text.html' 作用域下才能工作, 如 html 文件有作用域 '.text.html.basic', vue 文件有作用域 '.text.html.vue' 都是包含于 '.text.html' 的 selector: '.text.html', // 排除工作作用域中子作用域,如 html, vue 文件中的注释。可选 disableForSelector: '.text.html .comment', // 表示提示建议显示优先级, 数字越高优先级越高,默认优先级是0。可选 inclusionPriority: 1, // 如果为 true,那么根据 inclusionPriority 大小,高优先级就会阻止其他低优先级的建议服务提供者。可选 excludeLowerPriority: true, // 在提示下拉选项中的排序, 默认为 1,数字越高越靠前。可选 suggestionPriority: 2 // 返回一个 promise 对象、包含提示的数组或者 null。这里返回提示列表给 autocomplete+ 提供的服务展示,我们不用关心如何展示 getSuggestions(request) { // todo return []; }, // provide 提供的建议(即 getSuggetion 方法返回的提示)插入到缓冲区时被调用。可选 onDidInsertSuggestion({editor, triggerPosition, suggestion}) { // todo }, // provider 销毁后的善后工作,可选 dispose() { // todo } }</code></pre> <p>重点介绍下 getSuggestion 的参数 request 对象,它包含下面属性:</p> <ul> <li>editor: 当前的 <a href="/misc/goto?guid=4959751018510144288" rel="nofollow,noindex"> 文本编辑上下文 </a></li> <li>bufferPosition:当前光标的 <a href="/misc/goto?guid=4959751018594098398" rel="nofollow,noindex"> 位置 </a> ,包含属性 row 和 column。</li> <li>scopeDescriptor: 当前光标位置所在的作用域描述符,可通过其 .getScopesArray 方法获取到包含所有自己和祖先作用域选择器的数组。你可以通过按 cmd+shift+p 打开命令面板输入 Log Cursor scope 来查看作用描述符。</li> <li>prefix:当前光标输入位置所在单词的前缀,注意 autocomplete+ 不会捕获 ‘<’, ‘@’ 和 ‘:’ 字符,所以后面我们得自己做处理。原来没有仔细阅读文档(衰),我发现我原来实现的方法比较局限,其实这里教你怎么定义 <a href="/misc/goto?guid=4959751018679744732" rel="nofollow,noindex"> 自己的 prefix </a> 了</li> <li>activateManually:这个提示是否是用户 <a href="/misc/goto?guid=4959751018760989988" rel="nofollow,noindex"> 手动触发 </a></li> </ul> <p>介绍完 API 了,是时候来一起小试牛刀了。这里就以 <a href="/misc/goto?guid=4959751018847150233" rel="nofollow,noindex"> Element UI </a> 标签的属性值自动提示为例:</p> <p><img src="https://simg.open-open.com/show/f5b2db9cc41d351bb1e97c8463691fb6.gif"></p> <p>autocomplete+ 提供的 provider 会在用户包激活后任何时候调用(比如输入字符),我们只需在 getSuggestion 方法中返回提示信息(建议)数组就好了。那么问题重点来了,怎么获取这个提示信息数组?观察示例,想一想,可以分两大部分:判断提示出现的时机和过滤出提示信息数组。</p> <p><strong>1、判断提示出现时机</strong></p> <p>示例中的时机是是否是标签属性值开始(isAttrValueStart),我们先实现三个方法:是否在字符串作用域范围内(hasStringScope)、是否在标签作用域范围内(hasTagScope)和输入字符位置前是否具有属性名称(getPreAttr):</p> <pre> <code class="language-javascript">// scopes 是否包含单引号和双引号作用域选择器来决定是否在字符串中 function hasStringScope(scopes) { return (scopes.includes('string.quoted.double.html') || scopes.includes('string.quoted.single.html')); } // scopes 是否存在标签(tag)的作用域选择器来决定是否在标签作用域内,这里也是存在多种 tag 作用域选择器 function hasTagScope(scopes) { return (scopes.includes('meta.tag.any.html') || scopes.includes('meta.tag.other.html') || scopes.includes('meta.tag.block.any.html') || scopes.includes('meta.tag.inline.any.html') || scopes.includes('meta.tag.structure.any.html')); } // 获取当前输入位置存在的属性名 function getPreAttr(editor, bufferPosition) { // 初始引号的位置 let quoteIndex = bufferPosition.column - 1; // 引号的作用域描述符 let preScopeDescriptor = null; // 引号的作用域描述符字符串数组 let scopes = null; // 在当前行循环知道找到引号或索引为 0 while (quoteIndex) { // 获取位置的作用描述符 preScopeDescriptor = editor.scopeDescriptorForBufferPosition([bufferPosition.row, quoteIndex]); scopes = preScopeDescriptor.getScopesArray(); // 当前位置不在字符串作用域内或为引号起始位置, 则跳出循环 if (!this.hasStringScope(scopes) || scopes.includes('punctuation.definition.string.begin.html')) { break; } quoteIndex--; } // 属性名匹配正则表达 let attrReg = /\s+[:@]*([a-zA-Z][-a-zA-Z]*)\s*=\s*$/; // 正则匹配当前行引号之前的文本 let attr = attrReg.exec(editor.getTextInBufferRange([[bufferPosition.row, 0], [bufferPosition.row, quoteIndex]])); return attr && attr[1]; }</code></pre> <p>说明:</p> <ol> <li>参数 scopes 是前面讲的作用域描述符。如果不是很清楚,可以打开命令面板输入 Log Cursor scope 来查看。</li> <li>scopeDescriptorForBufferPosition 方法是获取给定位置的作用域描述符,具体请查看 <a href="/misc/goto?guid=4959751018088159030" rel="nofollow,noindex"> 这里 </a> 。</li> <li>getTextInBufferRange 方法是根据位置范围( <a href="/misc/goto?guid=4959751018927377489" rel="nofollow,noindex"> Range </a> )获取文本字符串,具体请查看 <a href="/misc/goto?guid=4959751019024360699" rel="nofollow,noindex"> 这里 </a> ,他有个别称 getTextInRange(官方文档里是没有的,可以查看源代码 <a href="/misc/goto?guid=4959751019098774529" rel="nofollow,noindex"> L1024 </a> 和 <a href="/misc/goto?guid=4959751019190151560" rel="nofollow,noindex"> L931 </a> ,实现一毛一样)。</li> </ol> <p>那么接下来结合三个方法来实现 isAttrValueStart 方法:</p> <pre> <code class="language-javascript">// 参数解释请看 ‘自动补全’ 小节 function isAttrValueStart({scopeDescriptor, bufferPosition, editor}) { // 获取作用域描述符字符串数组, 形如 ['text.html.vue', 'meta.tag.other.html', 'string.quoted.double.html', 'punctuation.definition.string.end.html'] const scopes = scopeDescriptor.getScopesArray(); // 获取当前位置的前一个字符位置 const preBufferPosition = [bufferPosition.row, Math.max(0, bufferPosition.column - 1)]; // 获取前一个字符位置的作用域描述符 const preScopeDescriptor = editor.scopeDescriptorForBufferPosition(preBufferPosition); // 获取作用域描述符字符串数组 const preScopes = preScopeDescriptor.getScopesArray(); // 当前鼠标位置 and 前一个位置(这个里主要是判断 attr= 再输入 ' 或 " 这种情况)是包含在字符串作用域中 and 前一个字符不能是字符串定义结束字符(' or ")为真,就说明是开始输入属性值 return (this.hasStringScope(scopes) && this.hasStringScope(preScopes) && !preScopes.includes('punctuation.definition.string.end.html') && this.hasTagScope(scopes) && this.getPreAttr(editor, bufferPosition)); }</code></pre> <p><strong>2、过滤出提示信息数组</strong></p> <p>前面已经判断提示信息出现的时机,剩下就是如何展示相应标签属性的值了, 这真是个精细化工作。惯例,先做些准备工作:1.获取输入位置所在的标签名(getPreTag); 2.获取输入位置所在的属性名(getPreAttr) - 这个上小节已实现;3.既然知道标签名和属性名,那么就可以从事先纯手工打造的 attributes.json 文件(具体请看 <a href="/misc/goto?guid=4959751019267115936"> element-helper-json </a> )中找到对应的属性值了(getAttrValues)- 这个就是遍历 json 对象属性,不具体解释。</p> <pre> <code class="language-javascript">// 标签名匹配正则表达式 - 标签匹配有很多情况,这里并不完善...,仅供参考。 let tagReg = /<([-\w]*)(?:\s|$)/; // 参数请查看上面 getSuggestion 参数对象属性解析 function getPreTag(editor, bufferPosition) { // 当前行 let row = bufferPosition.row; // 标签名 let tag = null; // 文件逐行向上遍历知道找到正则匹配的字符串,或 row = 0; while (row) { // lineTextForBufferRow 获取当前行文本字符串 tag = tagReg.exec(editor.lineTextForBufferRow(row)); if (tag && tag[1]) { return tag[1]; } row--; } return; }</code></pre> <p>OK,准备工作好了,我们来对获取到的属性值数组进行格式化处理,获得 getSuggestions 能识别的数据结构数组:</p> <pre> <code class="language-javascript">function getAttrValueSuggestion({editor, bufferPosition, prefix}) { // 存放提示信息对象数据 const suggestions = []; // 获取当前所在标签名 const tag = this.getPreTag(editor, bufferPosition); // 获取当前所在属性名称 const attr = this.getPreAttr(editor, bufferPosition); // 获取当前所在标签属性名下的属性值 const values = this.getAttrValues(tag, attr); // 属性值数组进行格式化处理 values.forEach(value => { if (this.firstCharsEqual(value, prefix) || !prefix) { suggestions.push(buildAttrValueSuggestion(tag, attr, value)); } }); // 返回符合 autocompete+ 服务解析的数据结构数组 return suggestions; } // 对原始数据加工处理 function buildAttrValueSuggestion(tag, attr, value) { // ATTRS 是 attributes.json 文件解析出的 json 对象 const attrItem = ATTRS[`${tag}/${attr}`] || ATTRS[attr]; // 返回 suggestion 对象 具体格式说明请看:https://github.com/atom/autocomplete-plus/wiki/Provider-API#suggestions return { text: value, // 插入文本编辑器,替换 prefix type: 'value', // 提示类型,用于列表提示左边的 icon 展示,有变量(varabale), 方法(method)和函数(function)等可选 description: attrItem.description, // 用于选中提示条目后,提示框下面展示的信息 rightLabel: attrItem.global ? 'element-ui' : `<${tag}>` // 右边展示的文本信息 }; }</code></pre> <p>经过以上两步,只需在 getSuggestions 方法中返回数组给 autocomplete+ 服务即可:</p> <pre> <code class="language-javascript">// ... getSuggestions(request) { if (this.isAttrValueStart(request)) { return this.getAttrValueSuggestion(request); } } // ...</code></pre> <p>到这里大家应该明白自动补全工作原理了吧,其他的可以依葫芦画瓢啦,be happy。</p> <h2><strong>2、代码段</strong></h2> <p>定义代码段的方式有三种方式:</p> <ul> <li>全局定义。在 Atom -> Snippets 菜单中定义,定义方式同第二种</li> <li>包内定义。在基础知识部分,我们介绍了生成包后的文件目录结构和作用,其中 snippets 文件夹里放的就是我们自定义的常用代码块 json 文件,这里我使用为 coffeescript 对象提供的 <a href="/misc/goto?guid=4959751019346803006" rel="nofollow,noindex"> cson </a> 文件,类似 json,但语法没有那么严格且支持多行字符串,如官方介绍:</li> </ul> <p>Which is far more lenient than JSON, way nicer to write and read, no need to quote and escape everything, has comments and readable multi-line strings, and won't fail if you forget a comma.</p> <p>现在来看下如何编写一个代码段,基本格式如下:</p> <pre> <code class="language-javascript">".source.js": "notification": "prefix": "notify", "body": """ this.$notify({ title: '${1:title}', message: '${2:string|VNode}' }); """</code></pre> <p>最顶层的键字符串(.source.js)是作用域选择器,指定在文本编辑器中那个范围内可触发匹配(示例中是在 js 文件或 script 标签域中触发代码段匹配)。下一层键字符串(notification)是表示代码段的描述,将展示在下拉条目的右边文本;prefix 字段是匹配输入字符的前缀;body 字段是插入的文本,可以通过 """ 来使用多行语法,body 中 $ 符表示占位符,每按一次 tab 键,都会按 ${num} 中的 num 顺序移动位置。如果要在占位符位置填充字符的话,可以这样 ${num: yourString}。示例效果如下: <a href="/misc/goto?guid=4959751019432301556"> 了解更多 </a></p> <p><img src="https://simg.open-open.com/show/5c021d43893a912f0a222ee14979ac29.gif"></p> <ul> <li>在‘自动补全’小节中可以定义返回 snippet 的提示,只需在 suggestion 对象中定义 snippet 和 displayText 属性即可,不要定义 text 属性。snippet 语法同第二种方式中基本格式中的body字段, displayText 用于提示展示文本,snippet 是插入文本编辑器的代码段。</li> </ul> <h2><strong>3、创建一个 modal 下拉列表和文档视图</strong></h2> <p>ATOM 为实现这两个功能提供了 npm 包 <a href="/misc/goto?guid=4959751019519995685" rel="nofollow,noindex"> atom-space-pen-views </a> ,它包括三个视图类:文本编辑器视图类( <a href="/misc/goto?guid=4959751019604205285" rel="nofollow,noindex"> TextEditorView </a> )、滚动文档视图类( <a href="/misc/goto?guid=4959751019684914067" rel="nofollow,noindex"> ScrollView </a> )和下拉选项视图类( <a href="/misc/goto?guid=4959751019769802583" rel="nofollow,noindex"> SelectListView </a> ),我们只需继承视图类,实现类方法即可。下面重点讲下拉列表和滚动视图类:</p> <p><strong>模态下拉列表</strong></p> <p>我们只要提供用于展示的条目给 SelectListView,实现两个必选方法,它会帮我们的条目渲染成一个下拉列表形式,如下:</p> <pre> <code class="language-javascript">// file: search-veiw.js import { SelectListView } from 'atom-space-pen-views'; class SearchView extends SelectListView { // keyword:用于初始化列表搜索框值,items:用于展示列表的条目数组,eg: [{name: 'el-button'}, {name: 'el-alert'}] constructor(keyword, items) { super(); // 执行 SelectListView 的构造函数 // ATOM API:addModalPanel(options), 添加一个模态框, item 是用于模块框展示的 DOM 元素,JQuery元素或实现veiw model this.panel = atom.workspace.addModalPanel({item: this}); // 给下拉列表搜索框写入文本 this.filterEditorView.setText(keyword); // 下拉列表可展示条目的最大数目 this.setMaxItems(50); // 设置用于展示的下拉条目数组 this.setItems(items); // 鼠标焦距到列表搜索框 this.focusFilterEditor(); } // 必须实现。自定义列表条目展示视图,该方法会在 setItems(items) 中单条 item 插入到下拉列表视图时调用 veiwForItem(item) { return `<li>${item.name}</li>`; } // 必须实现。 当下拉列表条目被选中后触发,参数 item 为选中条目对象 confirmed(item) { // todo this.cancel(); // 选中后关闭视图 } // 当列表视图关闭后调用 cancelled () { // todo } // 搜索框输入值,按 item 对象哪个键值模糊匹配,eg: item.name getFilterKey() { return 'name'; } } export default SearchView;</code></pre> <p>视图效果和 DOM 树结构如下图,我们可以看到通过 addModalPanel 把 selectListView 的 HTML 元素添加到模态框元素中了</p> <p><img src="https://simg.open-open.com/show/12824a06ea9f18e30d64e91c224b1b3c.png"></p> <p>定义好了视图类,那怎么渲染展示呢?少年莫慌,我们可以在主文件 activate 方法中注册命令来触发视图展示(当然你可以用其他方式,只要你确定想要触发时机执行方法就行了):</p> <pre> <code class="language-javascript">import SearchView from './search-view.js'; export default { activate() { // 实例化一个销毁容器,便于清除订阅到 Atom 系统的事件 this.subscriptions = new CompositeDisposable(); // 这里需在 keymaps 目录下的文件中配置 keymap. this.subscriptions.add(atom.commands.add('atom-workspace', { 'element-helper:search': () => { // 获取当前正在编辑(活跃)的文本编辑器 if (editor = atom.workspace.getActiveTextEditor()) { // 获取你光标选中的文本 const selectedText = editor.getSelectedText(); // 获取光标下的单词字符串 const wordUnderCursor = editor.getWordUnderCursor({ includeNonWordCharacters: false }); // 用于下拉列表展示的数据, 这里只是个 demo const items = [{ "type": "basic", "name": "Button 按钮", "path": "button", "tag": "el-button", "description": "el-button,Button 按钮" }]; // 没有范围选中文本,就用当前光标下的单词 const queryText = selectedText ? selectedText : wordUnderCursor; // 实例化搜索下拉列表视图 new SearchView(queryText, items); } } })); } }</code></pre> <p><strong>文档视图</strong></p> <p>Atom 提供了打开一个空白文本编辑器的 API ( <a href="/misc/goto?guid=4959751019852114848" rel="nofollow,noindex"> atom.workspace.open </a> ) 和注册 URI 打开钩子(opener)函数的 API ( <a href="/misc/goto?guid=4959751019925098260" rel="nofollow,noindex">atom.workspace.addOpener(opener)</a> ),那么再结合 ScrollView 可以打开一个可滚动的文档窗口。No BB, show my code:</p> <pre> <code class="language-javascript">/** * file: doc-view.js * 继承 ScrollView 类, 实现自己的文档视图类 */ import { Emitter, disposable } from 'atom'; import { ScrollView } from 'atom-space-pen-views'; class DocView extends ScrollView { // 视图html static content(){ // this.div 方法将会创建一个包含参数指定属性的 div 元素,可以换成其他 html 标签,eg: this.section()将创建 section 标签元素 return this.div({class: 'yourClassName', otherAttributeName: 'value'}); } constructor(uri) { super(); // 实例化事件监听器,用于监听和触发事件 this.emitter_ = new Emitter(); // 文档标题,tab名称 this.title_ = 'Loading...'; this.pane_ = null; this.uri_ = uri; } // 自定义方法,用户可自定义视图中展示的内容, 具体可查看 Element-Helper 源码 setView(args) { // todo, demo this.element.innerHTML = '<h1>Welcome to use Atom</h1>'; this.title_ = "Welcome!"; this.emitter_.emit('did-change-title'); } // 当视图 html 插入文本编辑器后触发,注意 view 被激活后才会触发视图的插入 attached() { // 这里可以在视图插入 DOM 后做些操作,比如监听事件,操作 DOM 等等 // 通过 atom.workspace.open 打开文本编辑器的URI获取视图所在的窗口容器,看下图比较容易理解什么是窗口容器 this.pane_ = atom.workspace.paneForURI(this.uri_); this.pane_.activateItem(this); } // 文档标题被激活执行 onDidChangeTitle(callback) { // 监听自定义事件 return this.emitter_.on('did-change-title', callback); } // 文档视图关闭后销毁函数 detory() { // 销毁文档视图 this.pane_.destroyItem(this); // 如果当前窗口容器中只有文档视图,那么把容器都销毁掉 if (this.pane_.getItems().length === 0) { this.pane_.destroy(); } } // 标题改变后触发事件 did-change-title, callback 内部将调用改方法 getTitle { return this.title_; } }</code></pre> <p><img src="https://simg.open-open.com/show/d8d77ee9e81c25949bfcd337414f739e.png"></p> <pre> <code class="language-javascript">/** * 主文件, 这里只写 activate 函数里的关键代码 */ import Url from 'url'; import DocView from './doc-view.js'; // .... // 初始化文档视图对象 this.docView_ = null; // 便于测试沿用上面搜索命令,触发打开视图 this.subscriptions.add(atom.commands.add('atom-workspace', { 'element-helper:search': () => { // 异步打开一个给定 URI 的资源或文本编辑器,URI 定义请看:https://en.wikipedia.org/wiki/Uniform_Resource_Identifier, 参数 split 确定打开视图的位置,activatePane 是否激活窗口容器 atom.workspace.open('element-docs://document-view', { split: 'right', activatePane: false}) .then(docView => { // docView 是经过 addOpener 添加钩子处理后的视图对象,如果没有相应的 opener 返回数据,默认为文本编辑器对象(TextEditor) this.docView_ = docView; // 为docView填充内容,具体展示的内容请在DocView中定义的setView方法中操作 this.docView_.setView(yourArgments); }); } }); // 为 URI 注册一个打开钩子,一旦 WorkSpace::open 打开 URI 资源,addOpener 里面的方法就会将会执行 this.subscriptions.add(atom.workspace.addOpener((url) => { if (Url.parse(url).protocol == 'element-docs:') { return new DocView(url); } })); // ...</code></pre> <p>如果还对这两个API有所疑虑,请查看上面提供的链接。那么最终效果如下:</p> <p><img src="https://simg.open-open.com/show/82cc8769c776be4fc6010629430cd7d6.png"></p> <h2>写在最后</h2> <p>这算是我在编写 <a href="/misc/goto?guid=4959751020013097437"> Element-Helper </a> 插件时的一些总结和过程吧,实现方式不一定完善或优雅,表述不清楚或错误的地方请留言指正。题外话,如果你想开发 <a href="/misc/goto?guid=4958872398868295437" rel="nofollow,noindex"> VSCode </a> 插件,以 Atom 插件开发为入门也是不错的选择,它们都是基于 <a href="/misc/goto?guid=4959751020133036073"> Electron </a> 框架,很多概念都互通,但 Atom 更易入手和灵活(个人见解)。最后,希望本文能为大家提供些许帮助。Enjoy it!</p> <p> </p> <p>来自:https://zhuanlan.zhihu.com/p/27913291</p> <p> </p>