摘要: 序言 这篇教程将会教你怎么制做你的第一个 Atom 文本编辑器的插件。咱们将会制做一个山寨版的 Sourcerer,这是一个从 StackOverflow 查询并使用代码片断的插件。到教程结束时,你将会制做好一个将编程问题(用英语描述的)转换成获取自 StackOverflow 的代码片断的插件,像这样: 教程须知 Atom 文本编辑器是用 web 技术创造出来的。javascript
这篇教程将会教你怎么制做你的第一个 Atom 文本编辑器的插件。咱们将会制做一个山寨版的 Sourcerer,这是一个从 StackOverflow 查询并使用代码片断的插件。到教程结束时,你将会制做好一个将编程问题(用英语描述的)转换成获取自 StackOverflow 的代码片断的插件,像这样:html
Atom 文本编辑器是用 web 技术创造出来的。咱们将彻底使用 JavaScript 的 EcmaScript 6 规范来制做插件。你须要熟悉如下内容:java
你能够跟着教程一步一步走,或者看看 放在 GitHub 上的仓库,这里有插件的源代码。这个仓库的历史提交记录包含了这里每个标题。node
根据 Atom 官网 的说明来下载 Atom。咱们同时还要安装上 apm
(Atom 包管理器的命令行工具)。你能够打开 Atom 并在应用菜单中导航到 Atom > Install Shell Commands
来安装。打开你的命令行终端,运行apm -v
来检查 apm
是否已经正确安装好,安装成功的话打印出来的工具版本和相关环境信息应该是像这样的:python
apm -v
> apm 1.9.2
> npm 2.13.3
> node 0.10.40
> python 2.7.10
> git 2.7.4
让咱们使用 Atom 提供的一个实用工具建立一个新的 package(软件包)来开始这篇教程。git
Cmd+Shift+P
(MacOS)或者 Ctrl+Shift+P
(Windows/Linux)来打开命令面板Command Palette。若是你在侧边栏没有看到软件包的文件,依次按下 Cmd+K
Cmd+B
(MacOS)或者 Ctrl+K
Ctrl+B
(Windows/Linux)。github
命令面板Command Palette可让你经过模糊搜索来找到并运行软件包。这是一个执行命令比较方便的途径,你不用去找导航菜单,也不用刻意去记快捷键。咱们将会在整篇教程中使用这个方法。web
在开始编程前让咱们来试用一下这个骨架代码包。咱们首先须要重启 Atom,这样它才能够识别咱们新增的软件包。再次打开命令面板,执行 Window: Reload
命令。chrome
从新加载当前窗口以确保 Atom 执行的是咱们最新的源代码。每当须要测试咱们对软件包的改动的时候,就须要运行这条命令。npm
经过导航到编辑器菜单的 Packages > sourcefetch > Toggle
或者在命令面板执行 sourcefetch:toggle
来运行软件包的 toggle
命令。你应该会看到屏幕的顶部出现了一个小黑窗。再次运行这条命令就能够隐藏它。
打开 lib/sourcefetch.js
,这个文件包含有软件包的逻辑和 toggle
命令的定义。
toggle() {
console.log('Sourcefetch was toggled!');
return (
this.modalPanel.isVisible() ?
this.modalPanel.hide() :
this.modalPanel.show()
);
}
toggle
是这个模块导出的一个函数。根据模态面板的可见性,它经过一个三目运算符 来调用 show
和hide
方法。modalPanel
是 Panel(一个由 Atom API 提供的 UI 元素) 的一个实例。咱们须要在export default
内部声明 modalPanel
才可让咱们经过一个实例变量 this
来访问它。
this.subscriptions.add(atom.commands.add('atom-workspace', {
'sourcefetch:toggle': () => this.toggle()
}));
上面的语句让 Atom 在用户运行 sourcefetch:toggle
的时候执行 toggle
方法。咱们指定了一个 匿名函数 () => this.toggle()
,每次执行这条命令的时候都会执行这个函数。这是事件驱动编程(一种经常使用的 JavaScript 模式)的一个范例。
命令只是用户触发事件时使用的一些字符串标识符,它定义在软件包的命名空间内。咱们已经用过的命令有:
package-generator:generate-package
Window:reload
sourcefetch:toggle
软件包对应到命令,以执行代码来响应事件。
让咱们来进行第一次代码更改——咱们将经过改变 toggle
函数来实现逆转用户选中文本的功能。
以下更改 toggle
函数。
toggle() {
let editor
if (editor = atom.workspace.getActiveTextEditor()) {
let selection = editor.getSelectedText()
let reversed = selection.split('').reverse().join('')
editor.insertText(reversed)
}
}
Window: Reload
来从新加载 Atom。File > New
来建立一个新文件,随便写点什么并经过光标选中它。Toggle sourcefetch
来运行sourcefetch:toggle
命令。更新后的命令将会改变选中文本的顺序:
在 sourcefetch 教程仓库 查看这一步的所有代码更改。
咱们添加的代码经过用 TextEditor API 来访问编辑器内的文本并进行操做。让咱们来仔细看看。
let editor
if (editor = atom.workspace.getActiveTextEditor()) { /* ... */ }
头两行代码获取了 TextEditor 实例的一个引用。变量的赋值和后面的代码被包在一个条件结构里,这是为了处理没有可用的编辑器实例的状况,例如,当用户在设置菜单中运行该命令时。
let selection = editor.getSelectedText()
调用 getSelectedText
方法可让咱们访问到用户选中的文本。若是当前没有文本被选中,函数将返回一个空字符串。
let reversed = selection.split('').reverse().join('')
editor.insertText(reversed)
咱们选中的文本经过一个 JavaScript 字符串方法 来逆转。最后,咱们调用 insertText
方法来将选中的文本替换为逆转后的文本副本。经过阅读 Atom API 文档,你能够学到更多关于 TextEditor 的不一样的方法。
如今咱们已经完成第一次代码更改了,让咱们浏览骨架代码包的代码来深刻了解一下 Atom 的软件包是怎样构成的。
主文件是 Atom 软件包的入口文件。Atom 经过 package.json
里的条目设置来找到主文件的位置:
"main": "./lib/sourcefetch",
这个文件导出一个带有生命周期函数(Atom 在特定的事件发生时调用的处理函数)的对象。
activate
。咱们将会重命名咱们的软件包命令为 fetch
,并移除一些咱们再也不须要的用户界面元素。按照以下更改主文件:
'use babel';
import { CompositeDisposable } from 'atom'
export default {
subscriptions: null,
activate() {
this.subscriptions = new CompositeDisposable()
this.subscriptions.add(atom.commands.add('atom-workspace', {
'sourcefetch:fetch': () => this.fetch()
}))
},
deactivate() {
this.subscriptions.dispose()
},
fetch() {
let editor
if (editor = atom.workspace.getActiveTextEditor()) {
let selection = editor.getSelectedText()
selection = selection.split('').reverse().join('')
editor.insertText(selection)
}
}
};
为了提高性能,Atom 软件包能够用时加载。咱们可让 Atom 在用户执行特定的命令的时候才加载咱们的软件包。这些命令被称为 启用命令,它们在 package.json
中定义:
"activationCommands": {
"atom-workspace": "sourcefetch:toggle"
},
更新一下这个条目设置,让 fetch
成为一个启用命令。
"activationCommands": {
"atom-workspace": "sourcefetch:fetch"
},
有一些软件包须要在 Atom 启动的时候被加载,例如那些改变 Atom 外观的软件包。在那样的状况下,activationCommands
会被彻底忽略。
menus
目录下的 JSON 文件指定了哪些菜单项是为咱们的软件包而建的。让咱们看看menus/sourcefetch.json
:
"context-menu": {
"atom-text-editor": [
{
"label": "Toggle sourcefetch",
"command": "sourcefetch:toggle"
}
]
},
这个 context-menu
对象可让咱们定义右击菜单的一些新条目。每个条目都是经过一个显示在菜单的label
属性和一个点击后执行的命令的 command
属性来定义的。
"context-menu": {
"atom-text-editor": [
{
"label": "Fetch code",
"command": "sourcefetch:fetch"
}
]
},
同一个文件中的这个 menu
对象用来定义插件的自定义应用菜单。咱们以下重命名它的条目:
"menu": [
{
"label": "Packages",
"submenu": [
{
"label": "sourcefetch",
"submenu": [
{
"label": "Fetch code",
"command": "sourcefetch:fetch"
}
]
}
]
}
]
命令还能够经过键盘快捷键来触发。快捷键经过 keymaps
目录的 JSON 文件来定义:
{
"atom-workspace": {
"ctrl-alt-o": "sourcefetch:toggle"
}
}
以上代码可让用户经过 Ctrl+Alt+O
(Windows/Linux) 或 Cmd+Alt+O
(MacOS) 来触发 toggle
命令。
重命名引用的命令为 fetch
:
"ctrl-alt-o": "sourcefetch:fetch"
经过执行 Window: Reload
命令来重启 Atom。你应该会看到 Atom 的右击菜单更新了,而且逆转文本的功能应该还能够像以前同样使用。
在 sourcefetch 教程仓库 查看这一步全部的代码更改。
如今咱们已经完成了第一次代码更改而且了解了 Atom 软件包的结构,让咱们介绍一下 Node 包管理器(npm) 中的第一个依赖项模块。咱们将使用 request 模块发起 HTTP 请求来下载网站的 HTML 文件。稍后将会用到这个功能来扒 StackOverflow 的页面。
打开你的命令行工具,切换到你的软件包的根目录并运行:
npm install --save request@2.73.0
apm install
这两条命令将 request
模块添加到咱们软件包的依赖列表并将模块安装到 node_modules
目录。你应该会在 package.json
看到一个新条目。@
符号的做用是让 npm 安装咱们这篇教程须要用到的特定版本的模块。运行 apm install
是为了让 Atom 知道使用咱们新安装的模块。
"dependencies": {
"request": "^2.73.0"
}
经过在 lib/sourcefetch.js
的顶部添加一条引用语句引入 request
模块到咱们的主文件:
import { CompositeDisposable } from 'atom'
import request from 'request'
如今,在 fetch
函数下面添加一个新函数 download
做为模块的导出项:
export default {
/* subscriptions, activate(), deactivate() */
fetch() {
...
},
download(url) {
request(url, (error, response, body) => {
if (!error && response.statusCode == 200) {
console.log(body)
}
})
}
}
这个函数用 request
模块来下载一个页面的内容并将记录输出到控制台。当 HTTP 请求完成以后,咱们的回调函数会将响应体做为参数来被调用。
最后一步是更新 fetch
函数以调用 download
函数:
fetch() {
let editor
if (editor = atom.workspace.getActiveTextEditor()) {
let selection = editor.getSelectedText()
this.download(selection)
}
},
fetch
函数如今的功能是将 selection 看成一个 URL 传递给 download
函数,而再也不是逆转选中的文本了。让咱们来看看此次的更改:
Window: Reload
命令来从新加载 Atom。View > Developer > Toggle Developer Tools
。File > New
。http://www.atom.io
。开发者工具让 Atom 软件包的调试更轻松。每一个
console.log
语句均可以将信息打印到交互控制台,你还可使用Elements
选项卡来浏览整个应用的可视化结构——即 HTML 的文本对象模型(DOM)。
在 sourcefetch 教程仓库 查看这一步全部的代码更改。
理想状况下,咱们但愿 download
函数能够将 HTML 做为一个字符串来返回,而不只仅是将页面的内容打印到控制台。然而,返回文本内容是没法实现的,由于咱们要在回调函数里面访问内容而不是在 download
函数那里。
咱们会经过返回一个 Promise 来解决这个问题,而再也不是返回一个值。让咱们改动 download
函数来返回一个 Promise:
download(url) {
return new Promise((resolve, reject) => {
request(url, (error, response, body) => {
if (!error && response.statusCode == 200) {
resolve(body)
} else {
reject({
reason: 'Unable to download page'
})
}
})
})
}
Promises 容许咱们经过将异步逻辑封装在一个提供两个回调方法的函数里来返回得到的值(resolve
用来处理请求成功的返回值,reject
用来向使用者报错)。若是请求返回了错误咱们就调用 reject
,不然就用resolve
来处理 HTML。
让咱们更改 fetch
函数来使用 download
返回的 Promise:
fetch() {
let editor
if (editor = atom.workspace.getActiveTextEditor()) {
let selection = editor.getSelectedText()
this.download(selection).then((html) => {
editor.insertText(html)
}).catch((error) => {
atom.notifications.addWarning(error.reason)
})
}
},
在咱们新版的 fetch
函数里,咱们经过在 download
返回的 Promise 调用 then
方法来对 HTML 进行操做。这会将 HTML 插入到编辑器中。咱们一样会经过调用 catch
方法来接收并处理全部的错误。咱们经过用Atom Notification API 来显示警告的形式来处理错误。
看看发生了什么变化。从新加载 Atom 并在一个选中的 URL 上执行软件包命令:
若是这个 URL 是无效的,一个警告通知将会弹出来:
在 sourcefetch 教程仓库 查看这一步全部的代码更改。
下一步涉及用咱们前面扒到的 StackOverflow 的页面的 HTML 来提取代码片断。咱们尤为关注那些来自采纳答案(提问者选择的一个正确答案)的代码。咱们能够在假设这类答案都是相关且正确的前提下大大简化咱们这个软件包的实现。
这一部分假设你使用的是 Chrome 浏览器。你接下来可使用其它浏览器,可是提示可能会不同。
让咱们先看看一张典型的包含采纳答案和代码片断的 StackOverflow 页面。咱们将会使用 Chrome 开发者工具来浏览 HTML:
C
来读取文本内容的问题。检查
。注意文本结构应该是这样的:
<div class="accepted-answer">
...
...
<pre>
<code>
...snippet elements...
</code>
</pre>
...
...
</div>
accepted-answer
的 div
来表示pre
元素的内部code
标签如今让咱们写一些 jQuery
代码来提取代码片断:
$('div.accepted-answer pre code').text()
并按下回车键。你应该会看到控制台中打印出采纳答案的代码片断。咱们刚刚运行的代码使用了一个 jQuery 提供的特别的 $
函数。$
接收要选择的查询字符串并返回网站中的某些 HTML 元素。让咱们经过思考几个查询案例看看这段代码的工做原理:
$('div.accepted-answer')
> [<div id="answer-1077349" class="answer accepted-answer" ... ></div>]
上面的查询会匹配全部 class 为 accepted-answer
的 <div>
元素,在咱们的案例中只有一个 div。
$('div.accepted-answer pre code')
> [<code>...</code>]
在前面的基础上改造了一下,这个查询会匹配全部在以前匹配的 <div>
内部的 <pre>
元素内部的 <code>
元素。
$('div.accepted-answer pre code').text()
> "print("Hello World!")"
text
函数提取并链接本来将由上一个查询返回的元素列表中的全部文本。这也从代码中去除了用来使语法高亮的元素。
咱们的下一步涉及使用咱们建立好的查询结合 Cheerio(一个服务器端实现的 jQuery)来实现扒页面的功能。
打开你的命令行工具,切换到你的软件包的根目录并执行:
npm install --save cheerio@0.20.0
apm install
在 lib/sourcefetch.js
为 cheerio
添加一条引用语句:
import { CompositeDisposable } from 'atom'
import request from 'request'
import cheerio from 'cheerio'
如今建立一个新函数 scrape
,它用来提取 StackOverflow HTML 里面的代码片断:
fetch() {
...
},
scrape(html) {
$ = cheerio.load(html)
return $('div.accepted-answer pre code').text()
},
download(url) {
...
}
最后,让咱们更改 fetch
函数以传递下载好的 HTML 给 scrape
而不是将其插入到编辑器:
fetch() {
let editor
let self = this
if (editor = atom.workspace.getActiveTextEditor()) {
let selection = editor.getSelectedText()
this.download(selection).then((html) => {
let answer = self.scrape(html)
if (answer === '') {
atom.notifications.addWarning('No answer found :(')
} else {
editor.insertText(answer)
}
}).catch((error) => {
console.log(error)
atom.notifications.addWarning(error.reason)
})
}
},
咱们扒取页面的功能仅仅用两行代码就实现了,由于 cheerio 已经替咱们作好了全部的工做!咱们经过调用load
方法加载 HTML 字符串来建立一个 $
函数,而后用这个函数来执行 jQuery 语句并返回结果。你能够在官方 开发者文档 查看完整的 Cheerio API
。
从新加载 Atom 并在一个选中的 StackOverflow URL 上运行 soucefetch:fetch
以查看到目前为止的进度。
若是咱们在一个有采纳答案的页面上运行这条命令,代码片断将会被插入到编辑器中:
若是咱们在一个没有采纳答案的页面上运行这条命令,将会弹出一个警告通知:
咱们最新的 fetch
函数给咱们提供了一个 StackOverflow 页面的代码片断而再也不是整个 HTML 内容。要注意咱们更新的 fetch
函数会检查有没有答案并显示通知以提醒用户。
在 sourcefetch 教程仓库 查看这一步全部的代码更改。
如今咱们已经将 StackOverflow 的 URL 转化为代码片断了,让咱们来实现最后一个函数——search
,它应该要返回一个相关的 URL 并附加一些像“hello world”或者“快速排序”这样的描述。咱们会经过一个非官方的 google
npm 模块来使用谷歌搜索功能,这样可让咱们以编程的方式来搜索。
经过在软件包的根目录打开命令行工具并执行命令来安装 google
模块:
npm install --save google@2.0.0
apm install
在 lib/sourcefetch.js
的顶部为 google
模块添加一条引用语句:
import google from "google"
咱们将配置一下 google
以限制搜索期间返回的结果数。将下面这行代码添加到引用语句下面以限制搜索返回最热门的那个结果。
google.resultsPerPage = 1
接下来让咱们来实现咱们的 search
函数:
fetch() {
...
},
search(query, language) {
return new Promise((resolve, reject) => {
let searchString = `${query} in ${language} site:stackoverflow.com`
google(searchString, (err, res) => {
if (err) {
reject({
reason: 'A search error has occured :('
})
} else if (res.links.length === 0) {
reject({
reason: 'No results found :('
})
} else {
resolve(res.links[0].href)
}
})
})
},
scrape() {
...
}
以上代码经过谷歌来搜索一个和指定的关键词以及编程语言相关的 StackOverflow 页面,并返回一个最热门的 URL。让咱们看看这是怎样来实现的:
let searchString = `${query} in ${language} site:stackoverflow.com`
咱们使用用户输入的查询和当前所选的语言来构造搜索字符串。比方说,当用户在写 Python 的时候输入“hello world”,查询语句就会变成 hello world in python site:stackoverflow.com
。字符串的最后一部分是谷歌搜索提供的一个过滤器,它让咱们能够将搜索结果的来源限制为 StackOverflow。
google(searchString, (err, res) => {
if (err) {
reject({
reason: 'A search error has occured :('
})
} else if (res.links.length === 0) {
reject({
reason: 'No results found :('
})
} else {
resolve(res.links[0].href)
}
})
咱们将 google
方法放在一个 Promise
里面,这样咱们能够异步地返回咱们的 URL。咱们会传递由google
返回的全部错误而且会在没有可用的搜索结果的时候返回一个错误。不然咱们将经过 resolve
来解析最热门结果的 URL。
咱们的最后一步是更新 fetch
函数来使用 search
函数:
fetch() {
let editor
let self = this
if (editor = atom.workspace.getActiveTextEditor()) {
let query = editor.getSelectedText()
let language = editor.getGrammar().name
self.search(query, language).then((url) => {
atom.notifications.addSuccess('Found google results!')
return self.download(url)
}).then((html) => {
let answer = self.scrape(html)
if (answer === '') {
atom.notifications.addWarning('No answer found :(')
} else {
atom.notifications.addSuccess('Found snippet!')
editor.insertText(answer)
}
}).catch((error) => {
atom.notifications.addWarning(error.reason)
})
}
}
让咱们看看发生了什么变化:
query
language
search
方法来获取一个 URL,而后经过在获得的 Promise 上调用 then
方法来访问这个 URL咱们不在 download
返回的 Promise 上调用 then
方法,而是在前面 search
方法自己链式调用的另外一个then
方法返回的 Promise 上面接着调用 then
方法。这样能够帮助咱们避免回调地狱
在 sourcefetch 教程仓库 查看这一步全部的代码更改。
大功告成了!从新加载 Atom,对一个“问题描述”运行软件包的命令来看看咱们最终的插件是否工做,不要忘了在编辑器右下角选择一种语言。
如今你知道怎么去 “hack” Atom 的基本原理了,经过 分叉 sourcefetch 这个仓库并添加你的特性 来为所欲为地实践你所学到的知识。