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 中先进行初始化:

  1. 使用detach进行清理;
  2. 绑定事件;

接下来就使用 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中的补全器:

  1. getCompletions :获取补全列表;
  2. 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的距离又近了一步,不仅仅能加快脚步编写的速度,更重要的是代码的准确性也会有所提高,当然这还是不够的!

ACE.js自定义提示实现方法的更多相关文章

  1. 超酷HTML5 Canvas图表应用Chart.js自定义提示折线图

    超酷HTML5 Canvas图表应用Chart.js自定义提示折线图 效果预览 实例代码 <div class="htmleaf-container"> <div ...

  2. javaScript prototype实例(正则) 自定义日期格式化方法

    一个JS自定义日期格式化方法,包括了不少知识点,以下方法来自jQuery DataTable中文的官方参考 //return (new Date(data)).Format("yyyy-MM ...

  3. vue2.0 自定义 提示框(Toast)组件

    1.自定义 提示框 组件 src / components / Toast / index.js /** * 自定义 提示框( Toast )组件 */ var Toast = {}; var sho ...

  4. vue 自定义 提示框(Toast)组件

    1.自定义 提示框 组件 src / components / Toast / index.js /** * 自定义 提示框( Toast )组件 */ var Toast = {}; var sho ...

  5. js关闭当前页面不弹出提示的方法

    js关闭当前页面不弹出提示的方法 js关闭当前页面不弹出提示的方法 "window.opener=null;window.open('','_self','');window.close() ...

  6. 转载 jQuery和js自定义函数和文件的方法(全网最全)

    jQuery和js自定义函数和文件的方法(全网最全)    版权声明:本文为像雾像雨又像风_http://blog.csdn.net/topdandan的原创文章,未经允许不得转载. https:// ...

  7. js自定义弹出框

    js自定义弹出框: 代码如下 <html> <head><title>自定义弹出对话框</title> <style type ="te ...

  8. jQuery Validate 表单验证插件----自定义一个验证方法

    一.下载依赖包 网盘下载:https://yunpan.cn/cryvgGGAQ3DSW  访问密码 f224 二.引入依赖包 <script src="../../scripts/j ...

  9. jquery.validate.js默认配置,jquery.validate.js自定义提示信息

    jquery.validate.js默认配置,jquery.validate.js自定义提示信息 配置jQuery.validator默认的处理方法 >>>>>>& ...

随机推荐

  1. 移动端H5适配方法(盒子+图片+文字)

    一.怎么让H5页面适应手机 a.利用meta标签 <meta name="viewport" content="width=device-width,initial ...

  2. Directory文件类

    创建一个新文件 Directory.CreateDirectory(@"C: \Users\enle\Desktop\new");//路径 Console.WriteLine(&q ...

  3. SQL Server Profiler小技巧——筛选请求

    如果需要转载,请附上本文作者和原文链接:http://www.cnblogs.com/zeusro/p/4016228.html Microsoft SQL Server Profiler 是 SQL ...

  4. Linux线程同步——条件变量

    互斥锁是用来给资源上锁的,而条件变量是用来等待而不是用来上锁的. 条件变量用来自动阻塞一个线程,直到某特殊情况发生为止. 通常条件变量和互斥锁同时使用. 和条件变量使用有关的几个重要函数: int p ...

  5. spring-boot集成PageHelper和通用Mapper

    前提条件:已经集成mybatis 代码生成步骤: 添加依赖 <dependency> <groupId>tk.mybatis</groupId> <artif ...

  6. css 画图形大全

    Square   #square { width: 100px; height: 100px; background: red; } Rectangle   #rectangle { width: 2 ...

  7. css网页布局血泪经验

    刚开始学css,没想到写页面刚写个banner就出了不少问题,写了几个下午都没有搞定对齐问题,现在从分析源码开始,尽量理解,总结,记住一般页面是从哪里开始布局的... 有些页面文字居中,其实不是用di ...

  8. python之迭代

    1.何为迭代: 对于给定list或者tuple,通过for循环来遍历这个list或tuple,这种遍历我们称之为迭代(Iteration). 2.python中利用for...in语句来完成迭代语句: ...

  9. null的专栏:https://blog.csdn.net/google19890102

    null的专栏:https://blog.csdn.net/google19890102 csdn博客专栏:https://blog.csdn.net/column.html

  10. Excel连接字符串在.NET中的应用

    转:https://www.cnblogs.com/jaxu/archive/2011/07/29/2121022.html 介绍几种在.NET中直接连接Excel作为数据源的几种方法以及连接字符串的 ...