製作你的第一個 Atom 文本編輯器插件
序言
這篇教程將會教你怎麼製作你的第一個 Atom 文本編輯器的插件。我們將會製作一個山寨版的 Sourcerer,這是一個從 StackOverflow 查詢並使用代碼片段的插件。到教程結束時,你將會製作好一個將編程問題(用英語描述的)轉換成獲取自 StackOverflow 的代碼片段的插件,像這樣:
教程須知
Atom 文本編輯器是用 web 技術創造出來的。我們將完全使用 JavaScript 的 EcmaScript 6 規範來製作插件。你需要熟悉以下內容:
教程的倉庫
你可以跟著教程一步一步走,或者看看 放在 GitHub 上的倉庫,這裡有插件的源代碼。這個倉庫的歷史提交記錄包含了這裡每一個標題。
開始
安裝 Atom
根據 Atom 官網 的說明來下載 Atom。我們同時還要安裝上 apm
(Atom 包管理器的命令行工具)。你可以打開 Atom 並在應用菜單中導航到 Atom > Install Shell Commands
來安裝。打開你的命令行終端,運行 apm -v
來檢查 apm
是否已經正確安裝好,安裝成功的話列印出來的工具版本和相關環境信息應該是像這樣的:
apm -v
> apm 1.9.2
> npm 2.13.3
> node 0.10.40
> python 2.7.10
> git 2.7.4
生成骨架代碼
讓我們使用 Atom 提供的一個實用工具創建一個新的 package(軟體包)來開始這篇教程。
- 啟動編輯器,按下
Cmd+Shift+P
(MacOS)或者Ctrl+Shift+P
(Windows/Linux)來打開 命令面板 。 - 搜索「Package Generator: Generate Package」並點擊列表中正確的條目,你會看到一個輸入提示,輸入軟體包的名稱:「sourcefetch」。
- 按下回車鍵來生成這個骨架代碼包,它會自動在 Atom 中打開。
如果你在側邊欄沒有看到軟體包的文件,依次按下 Cmd+K
Cmd+B
(MacOS)或者 Ctrl+K
Ctrl+B
(Windows/Linux)。
命令面板 可以讓你通過模糊搜索來找到並運行軟體包。這是一個執行命令比較方便的途徑,你不用去找導航菜單,也不用刻意去記快捷鍵。我們將會在整篇教程中使用這個方法。
運行骨架代碼包
在開始編程前讓我們來試用一下這個骨架代碼包。我們首先需要重啟 Atom,這樣它才可以識別我們新增的軟體包。再次打開命令面板,執行 Window: Reload
命令。
重新載入當前窗口以確保 Atom 執行的是我們最新的源代碼。每當需要測試我們對軟體包的改動的時候,就需要運行這條命令。
通過導航到編輯器菜單的 Packages > sourcefetch > Toggle
或者在命令面板執行 sourcefetch:toggle
來運行軟體包的 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 模式)的一個範例。
Atom 命令
命令只是用戶觸發事件時使用的一些字元串標識符,它定義在軟體包的命名空間內。我們已經用過的命令有:
package-generator:generate-package
Window:reload
sourcefetch:toggle
軟體包對應到命令,以執行代碼來響應事件。
進行你的第一次代碼更改
讓我們來進行第一次代碼更改——我們將通過改變 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
來創建一個新文件,隨便寫點什麼並通過游標選中它。 - 通過命令面板、Atom 菜單或者右擊文本然後選中
Toggle sourcefetch
來運行sourcefetch:toggle
命令。
更新後的命令將會改變選中文本的順序:
在 sourcefetch 教程倉庫 查看這一步的全部代碼更改。
Atom 編輯器 API
我們添加的代碼通過用 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 會在 Atom 初次載入軟體包的時候調用。這個函數用來初始化一些諸如軟體包所需的用戶界面元素的對象,以及訂閱軟體包命令的處理函數。
- deactivate 會在軟體包停用的時候調用,例如,當用戶關閉或者刷新編輯器的時候。
- serialize Atom 調用它在使用軟體包的過程中保存軟體包的當前狀態。它的返回值會在 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 教程倉庫 查看這一步所有的代碼更改。
使用 NodeJS 模塊
現在我們已經完成了第一次代碼更改並且了解了 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"
}
下載 HTML 並將記錄列印在開發者控制台
通過在 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
。 - 輸入一個 URL 並選中它,例如:
http://www.atom.io
。 - 用上述的任意一種方法執行我們軟體包的命令:
開發者工具讓 Atom 軟體包的調試更輕鬆。每個
console.log
語句都可以將信息列印到交互控制台,你還可以使用Elements
選項卡來瀏覽整個應用的可視化結構——即 HTML 的文本對象模型(DOM)。
在 sourcefetch 教程倉庫 查看這一步所有的代碼更改。
用 Promises 來將下載好的 HTML 插入到編輯器中
理想情況下,我們希望 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 頁面的代碼片段
下一步涉及用我們前面扒到的 StackOverflow 的頁面的 HTML 來提取代碼片段。我們尤其關注那些來自採納答案(提問者選擇的一個正確答案)的代碼。我們可以在假設這類答案都是相關且正確的前提下大大簡化我們這個軟體包的實現。
使用 jQuery 和 Chrome 開發者工具來構建查詢
這一部分假設你使用的是 Chrome 瀏覽器。你接下來可以使用其它瀏覽器,但是提示可能會不一樣。
讓我們先看看一張典型的包含採納答案和代碼片段的 StackOverflow 頁面。我們將會使用 Chrome 開發者工具來瀏覽 HTML:
- 打開 Chrome 並跳到任意一個帶有採納答案和代碼的 StackOverflow 頁面,比如像這個用 Python 寫的 hello world 的例子或者這個關於 用
C
來讀取文本內容的問題。 - 滾動窗口到採納答案的位置並選中一部分代碼。
- 右擊選中文本並選擇
檢查
。 - 使用元素偵察器來檢查代碼片段在 HTML 中的位置。
注意文本結構應該是這樣的:
<div class="accepted-answer">
...
...
<pre>
<code>
...snippet elements...
</code>
</pre>
...
...
</div>
- 採納的答案通過一個 class 為
accepted-answer
的div
來表示 - 代碼塊位於
pre
元素的內部 - 呈現代碼片段的元素就是裡面那一對
code
標籤
現在讓我們寫一些 jQuery
代碼來提取代碼片段:
- 在開發者工具那裡點擊 Console 選項卡來訪問 Javascript 控制台。
- 在控制台中輸入
$('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
我們的下一步涉及使用我們創建好的查詢結合 Cheerio(一個伺服器端實現的 jQuery)來實現扒頁面的功能。
安裝 Cheerio
打開你的命令行工具,切換到你的軟體包的根目錄並執行:
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 的谷歌搜索功能
現在我們已經將 StackOverflow 的 URL 轉化為代碼片段了,讓我們來實現最後一個函數——search
,它應該要返回一個相關的 URL 並附加一些像「hello world」或者「快速排序」這樣的描述。我們會通過一個非官方的 google
npm 模塊來使用谷歌搜索功能,這樣可以讓我們以編程的方式來搜索。
安裝這個 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 函數
接下來讓我們來實現我們的 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
函數來使用 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
- 我們使用 TextEditor API 來獲取當前編輯器選項卡使用的
language
- 我們調用
search
方法來獲取一個 URL,然後通過在得到的 Promise 上調用then
方法來訪問這個 URL
我們不在 download
返回的 Promise 上調用 then
方法,而是在前面 search
方法本身鏈式調用的另一個 then
方法返回的 Promise 上面接著調用 then
方法。這樣可以幫助我們避免回調地獄
在 sourcefetch 教程倉庫 查看這一步所有的代碼更改。
測試最終的插件
大功告成了!重新載入 Atom,對一個「問題描述」運行軟體包的命令來看看我們最終的插件是否工作,不要忘了在編輯器右下角選擇一種語言。
下一步
現在你知道怎麼去 「hack」 Atom 的基本原理了,通過 分叉 sourcefetch 這個倉庫並添加你的特性 來隨心所欲地實踐你所學到的知識。
via: https://github.com/blog/2231-building-your-first-atom-plugin
作者:NickTikhonov 譯者:OneNewLife 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive