1. 程式人生 > >基於 JFace Text Framework 構建全功能程式碼編輯器

基於 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 編輯器的內容提示
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 的立場。