ACE.js自定義提示實現方法
僅僅把代碼高亮了還不夠,在正常的編輯器中當輸入少量的幾個字符串就可以根據它來提示可能的輸入:
這樣用起來能極大地提高輸入的效率,而實現起來非常簡單:
ace.require("ace/ext/language_tools"); var editor = ace.edit("editor"); editor.session.setMode("ace/mode/groovy"); editor.setTheme("ace/theme/tomorrow"); editor.setOptions({ enableBasicAutocompletion: true, enableSnippets: true, enableLiveAutocompletion: true });
另外注意需要引入 ext-language_tools.js 文件!感覺看英文的文檔有些地方不是很清楚(可能是英語水平的問題☺),於是我們繼續開始讀源碼。
源碼分析
我們設置了 enableLiveAutocompletion 后輸入內容時會執行doLiveAutocomplete 方法:
var doLiveAutocomplete = function(e) { var editor = e.editor; var hasCompleter = editor.completer && editor.completer.activated; if (e.command.name === "backspace") {// 刪除動作 if (hasCompleter && !getCompletionPrefix(editor)) editor.completer.detach(); } else if (e.command.name === "insertstring") {// 輸入動作 var prefix = getCompletionPrefix(editor); if (prefix && !hasCompleter) { if (!editor.completer) { editor.completer = new Autocomplete(); } editor.completer.autoInsert = false; editor.completer.showPopup(editor);// 入口方法 } } };
對操作的類型及內容做一些簡單的過濾之后就交由 Autocomplete 來完成實質性的工作:
this.showPopup = function(editor) { // 初始化 if (this.editor) this.detach(); this.activated = true; this.editor = editor; if (editor.completer != this) { if (editor.completer) editor.completer.detach(); editor.completer = this; } // 綁定方法 editor.on("changeSelection", this.changeListener); editor.on("blur", this.blurListener); editor.on("mousedown", this.mousedownListener); editor.on("mousewheel", this.mousewheelListener); // 更新補全信息列表 this.updateCompletions(); };
方法 showPopup 中先進行初始化:
- 使用detach進行清理;
- 綁定事件;
接下來就使用 updateCompletions 來獲取補全列表信息並進行展示:
this.updateCompletions = function(keepPopupPosition) { if (keepPopupPosition && this.base && this.completions) { var pos = this.editor.getCursorPosition(); var prefix = this.editor.session.getTextRange({start: this.base, end: pos}); // 內容沒有發生變化 if (prefix == this.completions.filterText) return; this.completions.setFilter(prefix); if (!this.completions.filtered.length) return this.detach(); if (this.completions.filtered.length == 1 && this.completions.filtered[0].value == prefix && !this.completions.filtered[0].snippet) return this.detach(); this.openPopup(this.editor, prefix, keepPopupPosition); return; } var _id = this.gatherCompletionsId; // 收集所有的補全信息並執行(全部用回調函數來搞看着好累- -!) this.gatherCompletions(this.editor, function(err, results) { var detachIfFinished = function() { if (!results.finished) return; return this.detach(); }.bind(this); // 獲取前綴 var prefix = results.prefix; var matches = results && results.matches; // 沒有匹配到的時候就可以清理一下然后返回了 if (!matches || !matches.length) return detachIfFinished(); if (prefix.indexOf(results.prefix) !== 0 || _id != this.gatherCompletionsId) return; this.completions = new FilteredList(matches); // 是否精確匹配 if (this.exactMatch) this.completions.exactMatch = true; // 過濾,過濾完的結果保存在filtered中 this.completions.setFilter(prefix); var filtered = this.completions.filtered; // 檢查過濾完的結果,沒有匹配到的就清理並返回 if (!filtered.length) return detachIfFinished(); if (filtered.length == 1 && filtered[0].value == prefix && !filtered[0].snippet) return detachIfFinished(); if (this.autoInsert && filtered.length == 1 && results.finished) return this.insertMatch(filtered[0]); // 展示內容 this.openPopup(this.editor, prefix, keepPopupPosition); }.bind(this)); };
其中參數 keepPopupPosition 表示是否保持彈出框的位置保持不變:
補全框中的內容會隨着你的輸入變化而變化,但是位置卻保持不變就是這個參數在起作用!
其中比較關鍵的用 gatherCompletions 來收集所有補全器提供的數據(感覺是用這個方法把language_tools.js和autocomplete.js打通):
this.gatherCompletions = function(editor, callback) { var session = editor.getSession(); var pos = editor.getCursorPosition(); var line = session.getLine(pos.row); var prefix = util.retrievePrecedingIdentifier(line, pos.column); this.base = session.doc.createAnchor(pos.row, pos.column - prefix.length); this.base.$insertRight = true; var matches = []; var total = editor.completers.length; // 遍歷執行每個補全器 editor.completers.forEach(function(completer, i) { // 獲取補全列表 completer.getCompletions(editor, session, pos, prefix, function(err, results) { // 在沒有發生錯誤的時候,將結果合並到matchs中 if (!err) matches = matches.concat(results); var pos = editor.getCursorPosition(); var line = session.getLine(pos.row); // 調用回調函數 callback(null, { prefix: util.retrievePrecedingIdentifier(line, pos.column, results[0] && results[0].identifierRegex), matches: matches, finished: (--total === 0) }); }); }); return true; };
在每個補全器的 getCompletions 方法中都會調用callback方法:將自己的結果合並到全局的數據中。獲取補全器的數據之后就會調用 openPopup 方法來更新展示:
this.openPopup = function(editor, prefix, keepPopupPosition) { if (!this.popup) this.$init(); this.popup.setData(this.completions.filtered); editor.keyBinding.addKeyboardHandler(this.keyboardHandler); var renderer = editor.renderer; this.popup.setRow(this.autoSelect ? 0 : -1); if (!keepPopupPosition) { // 設置展示 this.popup.setTheme(editor.getTheme()); this.popup.setFontSize(editor.getFontSize()); var lineHeight = renderer.layerConfig.lineHeight; // 設置位置 var pos = renderer.$cursorLayer.getPixelPosition(this.base, true); pos.left -= this.popup.getTextLeftOffset(); var rect = editor.container.getBoundingClientRect(); pos.top += rect.top - renderer.layerConfig.offset; pos.left += rect.left - editor.renderer.scrollLeft; pos.left += renderer.gutterWidth; // 展示內容 this.popup.show(pos, lineHeight); } else if (keepPopupPosition && !prefix) { this.detach(); } };
回過頭再來看language_tools.js中的補全器:
- getCompletions :獲取補全列表;
- getDocTooltip :返回HTML格式的提示內容;
每個補全列表中的元素包含如下信息:
- caption :字幕,也就是展示在列表中的內容
- meta :展示類型
- name :名稱
- value :值
- score :分數,越大的排在越上面
而getDocTooltip感覺又進一步地提升了寫代碼時候的體驗(在寫代碼的時候就知道輸入的是什么):
具體是怎么實現的呢?接着來看代碼, Mode 中的 getCompletions 如下:
this.getCompletions = function(state, session, pos, prefix) { // 獲取當前Mode的關鍵字 var keywords = this.$keywordList || this.$createKeywordList(); // 根據關鍵字組裝補全列表 return keywords.map(function(word) { return { name: word, value: word, score: 0, meta: "keyword" }; }); };
在當前文件中寫過的單詞被自動提示補全的邏輯在 text_completer.js 中實現(邏輯很簡單),比較麻煩的是 enableSnippets ,這個后面有時間再看。
自定義補全
知道了ACE的補全運行的原理,那么現在擴展起來就比較簡單了:
var languageTools = ace.require("ace/ext/language_tools"); languageTools.addCompleter({ getCompletions: function(editor, session, pos, prefix, callback) { callback(null, [ { name : "test", value : "test", caption: "test", meta: "test", type: "local", score : 1000 // 讓test排在最上面 } ]); } });
雖然看到的例子都是同步執行callback方法,但用異步來做也是完全沒有問題的:
在上面看源碼的時候還不明白為啥每次回調的時候都要更新顯示而不是等全部執行完后更新一次~
在事件驅動的系統中接口的設計還是需要多思考、多推敲的啊!
總結
有了自動補全之后與IDE的距離又近了一步,不僅僅能加快腳步編寫的速度,更重要的是代碼的准確性也會有所提高,當然這還是不夠的!