基於 JFace Text Framework 構建全功能程式碼編輯器
轉載自https://www.ibm.com/developerworks/cn/opensource/os-cn-ecljtf4/
Content Assistant
Content Assistant(內容提示)可以幫助程式設計師快速的完成程式碼,並且還有程式碼自動補全的附加功能。這對於一個程式碼編輯器來說是至關重要的,也是不少人喜歡用 IDE 編寫程式碼的原因之一。但是這個功能背後卻不是那麼簡單的,我們先來了解一下 JTF 中和 Content Assistant 相關的概念,下面是 Eclipse 中 Java 編輯器的內容提示的樣子:
圖1. Java 編輯器的內容提示
先來介紹一下圖 1 中出現的三個概念:
- Proposal(提議):Proposal 代表了一個可能的自動完成選項,程式設計師選擇之後,程式碼會自動填入到編輯器裡。
- Proposal Popup(提議彈出列表):Proposal Popup 是用來顯示自動完成列表的視窗
- Additional Info(附加資訊):每個提議都可以附帶一些幫助資訊,叫做 Additional Info,它會顯示在彈出列表的旁邊,並且當你選擇某個 Proposal 的時候自動重新整理。
提示:在彈出列表出現後,你可能會發現有些鍵盤事件被彈出列表處理了,比如你按上下箭頭,它會改變當前被選擇的 Proposal。這是因為在列表彈出之前,內容提示管理器向文字框添加了一個按鍵校驗事件處理器,截獲了這些按鍵。具體的程式碼可以參考 ContentAssistant 的內部類 InternalListener。
這三個部分都是可以定製的,只不過有的簡單有點麻煩一點。比如我們看到彈出列表的下面有一行提示“Press ‘Alt+/’ to show Template Proposals”,這在標準的彈出列表裡面是沒有的,JDT 定製了這一部分。
為示例程式碼新增內容提示支援
我打算為本文的示例程式碼新增以下的內容提示支援:自動提示已經宣告的變數名。比如下面的語句:
清單 1. 示例語句
1 2 |
a = 3; b = 4;
|
那麼當用戶在啟用內容提示時,我們將顯示出 a 和 b 供它選擇,也就是顯示之前宣告過的變數。所有的宣告過的變數可以通過遍歷語法樹來得到,我們在 TreeHelper 裡面有一個 getVariables,它會完成這樣的功能,如果你生成的語法樹不一樣,調整這個方法就可以了。注意輸入的時候語法必須是正確的,不然語法解析器識別不出這是一個宣告語句,也就得不到變量了。
IContentAssistProcessor
第一步,我們要實現 IContentAssistProcessor 介面,它就是所有 Proposal 的來源。不過這個介面的方法比我們想象的要多一些:
清單 2. IContentAssistProcessor 介面
1 2 3 4 5 6 7 8 |
public interface IContentAssistProcessor {
ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset);
IContextInformation[] computeContextInformation(ITextViewer viewer, int offset);
char[] getCompletionProposalAutoActivationCharacters();
char[] getContextInformationAutoActivationCharacters();
String getErrorMessage();
IContextInformationValidator getContextInformationValidator();
}
|
這些方法牽涉到了一些概念,我們來一一的解釋它們:
- computeCompletionProposals:這個就是所有 Proposal 的來源了,返回的型別是 ICompletionProposal 陣列,ICompletionProposal 代表的就是單個的自動完成選項。
- computeContextInformation;Context Information(上下文資訊)是個新概念,它在這裡表示你選擇了某個 Proposal 之後,會有一個提示資訊彈出來,那個就叫上下文資訊。要注意它和上面提到過的 Additional Info 是不同的東西。
- getCompletionProposalAutoActivationCharacters:這個方法引入了一個 Auto Activation(自動啟用)的概念,所謂自動啟用就是在某種條件下 Proposal Popup 自動彈出。這個“某種條件”指的是一些字元,比如最常用的應該是“.”號。
- getContextInformationAutoActivationCharacters:上下文資訊也有自動啟用的功能
- getErrorMessage:如果內容提示無法找到任何 Proposal,它可以返回一個錯誤資訊給使用者
- getContextInformationValidator:上下文資訊是可以進行校驗的,如果失敗,上下文資訊不會被顯示
computeCompletionProposals 方法顯然是必須實現的,我添加了一個 ExprContentAssistProcessor 類,下面是它的實現方式:
清單 3. ExprContentAssistProcessor 實現了 IContentAssistProcessor 介面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
// get document
IDocument doc = viewer.getDocument();
// get tree
Tree tree = TreeManager.getTree(doc);
if(tree == null)
return null;
// get current selected range
Point range = viewer.getSelectedRange();
// get all declared variables
List<
String
> variables = TreeHelper.getVariables(tree);
// create proposals
List<
ICompletionProposal
> proposals = new ArrayList<
ICompletionProposal
>();
for(String var : variables) {
proposals.add(new CompletionProposal(
var, range.x, range.y, var.length(), null, var, null, "Add your info here"));
}
return proposals.toArray(new ICompletionProposal[proposals.size()]);
}
|
我們遍歷語法樹得到了所有的變數,你可以看到整個實現程式碼在 ANTLR 以及一些工具類的幫助下顯得非常簡潔。注意我們為每個變數建立了一個 CompletionProposal,它的建構函式引數非常多,最後一個就是 Additional Info,我這裡只是填了一些無用的資訊作為演示之用。其它的引數涉及自動完成需要的所有資訊,比如插入的字串,在哪裡插入,圖示等等。
配置
又到了將我們的實現和 JTF 連線起來的時間,還是修改 ExprConfiguration, 要覆蓋的方法變成了 getContentAssistant:
清單 4. 讓 JTF 知道我們的內容提示實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public IContentAssistant getContentAssistant(ISourceViewer sourceViewer)
{
ContentAssistant assistant = new ContentAssistant();
assistant.setInformationControlCreator(new IInformationControlCreator() {
public IInformationControl createInformationControl(Shell parent)
{
DefaultInformationControl control = new DefaultInformationControl(parent);
return control;
}
});
// add assist processor
IContentAssistProcessor processor = new ExprContentAssistProcessor();
assistant.setContentAssistProcessor(processor, IDocument.DEFAULT_CONTENT_TYPE);
return assistant;
}
|
注意我們第一步實現的只是一個 Processor,還不是真正的內容提示管理器,幸運的是 JTF 為我們提供了 ContentAssistant,我們只要新建一個就可以了。第二行看上去有些不解,稍後我會解釋。請注意最後一段,大家可以發現內容提示也是和文字型別繫結到一起的。
快捷鍵
用過 Java 編輯器的應該知道,內容提示可以用熱鍵進行撥出,這個熱鍵可以在 Eclipse 的設定裡找到,以 Eclipse 3.3 為例,我們在設定中找到 General->Keys,然後在 filter 中輸入 Content Assist 即可找到。為了能夠讓快捷鍵對我們的編輯器也有效果,需要安裝一個 Handle 來處理它。這部分內容超出了本文的範圍,所以我就不詳細解釋了。大家可以發現 ExprViewer 中多了一些成員和方法,比如 createHandlers 方法,它們都是為了處理快捷鍵而準備的。
效果
到這裡為止,一個很基本的內容提示就完成了,下圖是它的效果:
圖 2. 內容提示效果圖
Information Control
回過頭來看看上一節中我賣的關子:ContentAssistant 設定了一個 IInformationControlCreator。從字面上很好理解,Information Control(資訊控制元件)就是用來顯示資訊的一個控制元件,而 IInformationControlCreator 就是建立控制元件的工廠了。資訊控制元件可以用來顯示任何資訊,在內容提示的情況下,顯示的就是 Additional Info。這個控制元件可以使用任何形式,那麼裡面的內容也就根據控制元件的能力可以有不同的變化。比如,你可以用一個瀏覽器控制元件來顯示資訊,這樣的話,你的資訊可以用HTML來寫。在例子中,我們用的是 JTF 的預設實現:DefaultInformationControl,它內部使用的是 StyledText 控制元件。它雖然用的不是瀏覽器,但是它內部提供了一個資訊渲染介面:IInformationPresenter。如果你使用 HTMLTextPresenter,它可以支援你在資訊中嵌入 HTML 標籤。
由於資訊控制元件是一個通用的部件,它被廣泛的用在其它需要顯示資訊的地方,比如我們以後會提到的Text Hover(文字懸浮幫助)。同時由於JTF使用了一系列的介面來抽象資訊控制元件的功能,因此可以很方便的實現自己的資訊控制元件。
結束語
正如我所說,本文的例子是很基本的,有很多可以提高的地方,這些高階的功能留給有興趣的讀者完成。這裡給出一些我能想到的問題以供參考:
- 內容提示只是顯示所有的變數,它不會根據使用者已經輸入的內容來提示。比如有兩個變數 test 和 haha,如果使用者輸入了“te”再啟用內容提示,那麼我們應該只提示 test。這個並非難事,我們有 TokenList 來幫助我們得到符號資訊。
- 列出的 Proposal 沒有圖示,只有文字,這是一個小問題。學習了本文之後,你能立刻想起來要加個圖示應該修改哪裡嗎?
- 對於 Proposal Popup:我們沒有定製,可以嘗試像 Java 編輯器那樣給它底部加上些提示
- 對於資訊控制元件,用的是預設實現。可以嘗試使用瀏覽器,然後使用 HTML 顯示幫助資訊,看上去效果會更好。
- 對於 IContentAssistProcessor,我們沒有實現其它方法,比如上下文資訊,自動啟用。
要使內容提示功能達到和 Java 編輯器一樣的高度,還是要花一些精力的。我一向提倡先了解基本概念,再深入具體細節。希望本文可以作為大家的起點,最終構造出一個專業的內容提示模組。
宣告
本文僅代表作者的個人觀點,不代表 IBM 的立場。