-using Gtk;
+//using Gtk;
// not sure why - but extending Gtk.SourceCompletionProvider seems to give an error..
namespace Palete {
- public class CompletionProvider : Object, SourceCompletionProvider
+ public class CompletionProvider : Object, GtkSource.CompletionProvider
{
- Editor editor;
- WindowState windowstate;
- //public List<Gtk.SourceCompletionItem> filtered_proposals;
+ public JsRender.JsRender file {
+ get { return this.editor.file; }
+ private set {}
+ }
+ public Editor editor;
+ //public WindowState windowstate;
+ public CompletionModel model;
+ global::Gtk.StringFilter? filter = null;
public CompletionProvider(Editor editor)
{
this.editor = editor;
- this.windowstate = null; // not ready until the UI is built.
+
+ // this.windowstate = null; // not ready until the UI is built.
}
return "roojsbuilder";
}
- public int get_priority ()
+ public int get_priority (GtkSource.CompletionContext context)
{
return 200;
}
-
- public bool match (SourceCompletionContext context)
+
+
+ public bool is_trigger(global::Gtk.TextIter iter, unichar ch)
{
- bool has_matches = false;
- this.fetchMatches(context, out has_matches);
- return has_matches;
+ if (this.in_populate || ch == 32) {
+ return false;
+ }
+ if (this.editor.buffer.el.iter_has_context_class(iter, "comment") ||
+ this.editor.buffer.el.iter_has_context_class(iter, "string")
+ ) {
+ return false;
+ }
+
+
+ //GLib.debug("should trigger? %c", (int) ch);
+
+
+ return true;
}
-
- public List<SourceCompletionItem>? fetchMatches(SourceCompletionContext context, out bool has_matches)
+
+ public void activate (GtkSource.CompletionContext context, GtkSource.CompletionProposal proposal)
{
- has_matches = false;
+ GLib.debug("compelte activate");
+
+ var p = (CompletionProposal) proposal;
+ GLib.debug("lsp says use %s", p.ci.insertText);
+
+
+ global::Gtk.TextMark end_mark = null;
+ global::Gtk.TextIter begin, end;
- if (this.windowstate == null) {
- this.windowstate = this.editor.window.windowstate;
- }
-
+ if (!context.get_bounds(out begin, out end)) {
+ return;
+ }
+ var buffer = begin.get_buffer();
- var buffer = context.completion.view.buffer;
- var mark = buffer.get_insert ();
- TextIter end;
+ var word = p.label;
+ if (p.ci.kind == Lsp.CompletionItemKind.Method || p.ci.kind == Lsp.CompletionItemKind.Function) {
+ var bits = p.text.split("(");
+ var abits = bits[1].split(")");
+ var args = abits[0].split(",");
+
+ word += "(";
+ for(var i = 0 ; i < args.length; i++) {
+ word += i > 0 ? ", " : " ";
+ var wbit = args[i].strip().split(" ");
+ if (wbit.length < 2) {
+ word += wbit[0];
+ continue;
+ }
+ var ty = wbit[wbit.length - 2];
+ ty = ty.has_suffix("?") ? "?" : "";
+ word += ty + wbit[wbit.length-1]; // property type..?
+ }
+ word += args.length > 0 ? " )" : ")";
+ }
+
+ var len = -1;
+
- buffer.get_iter_at_mark (out end, mark);
- var endpos = end;
-
- var searchpos = endpos;
-
- searchpos.backward_find_char(is_space, null);
- searchpos.forward_char();
- var search = endpos.get_text(searchpos);
- print("got search %s\n", search);
-
- if (search.length < 2) {
- return null;
- }
-
- // now do our magic..
- var filtered_proposals = this.windowstate.file.palete().suggestComplete(
- this.windowstate.file,
- this.editor.node,
- this.editor.ptype,
- this.editor.key,
- search
- );
-
- print("GOT %d results\n", (int) filtered_proposals.length());
-
- if (filtered_proposals.length() < 2) {
- return null;
- }
+ /* If the insertion cursor is within a word and the trailing characters
+ * of the word match the suffix of the proposal, then limit how much
+ * text we insert so that the word is completed properly.
+ */
+ if (!end.ends_line() &&
+ !end.get_char().isspace() &&
+ !end.ends_word ())
+ {
+ var word_end = end;
+
+ if (word_end.forward_word_end ()) {
+ var text = end.get_slice(word_end);
+ if (text.length > word.length) {
+ return;
+ }
+ if (word.has_suffix (text)) {
+ //g_assert (strlen (word) >= strlen (text));
+ len = word.length - text.length;
+ end_mark = buffer.create_mark (null, word_end, false);
+ }
+ }
+ }
+
+ buffer.begin_user_action();
+ buffer.delete (ref begin, ref end);
+ buffer.insert ( ref begin, word, len);
+ buffer.end_user_action ();
+
+ if (end_mark != null)
+ {
+ buffer.get_iter_at_mark(out end, end_mark);
+ buffer.select_range(end, end);
+ buffer.delete_mark(end_mark);
+ }
- filtered_proposals.sort((a, b) => {
- return ((string)(a.text)).collate((string)(b.text));
- });
- has_matches = true;
- return filtered_proposals;
}
-
- public void populate (SourceCompletionContext context)
+
+
+ public void display (GtkSource.CompletionContext context, GtkSource.CompletionProposal proposal, GtkSource.CompletionCell cell)
+ {
+ //GLib.debug("compelte display");
+ var col = cell.get_column();
+
+ var p = (CompletionProposal) proposal;
+ switch(col) {
+ case GtkSource.CompletionColumn.TYPED_TEXT:
+ cell.set_text(p.label);
+ break;
+ case GtkSource.CompletionColumn.ICON:
+//cell.set_icon_name("lang-define-symbolic");return;
+//cell.set_icon_name("lang-include-symbolic");return;
+//cell.set_icon_name("lang-typedef-symbolic");return;
+//cell.set_icon_name("lang-union-symbolic");return;
+ switch (p.ci.kind) {
+
+ case Lsp.CompletionItemKind.Text: cell.set_icon_name("completion-snippet-symbolic");return;
+ case Lsp.CompletionItemKind.Method: cell.set_icon_name("lang-method-symbolic");return;
+ case Lsp.CompletionItemKind.Function: cell.set_icon_name("lang-function-symbolic");return;
+ case Lsp.CompletionItemKind.Constructor: cell.set_icon_name("lang-method-symbolic");return;
+ case Lsp.CompletionItemKind.Field: cell.set_icon_name("lang-struct-field-symbolic");return;
+ case Lsp.CompletionItemKind.Variable: cell.set_icon_name("lang-variable-symbolic");return;
+ case Lsp.CompletionItemKind.Class: cell.set_icon_name("lang-class-symbolic");return;
+ case Lsp.CompletionItemKind.Interface: cell.set_icon_name("lang-class-symbolic");return;
+ case Lsp.CompletionItemKind.Module: cell.set_icon_name("lang-namespace-symbolic");return;
+ case Lsp.CompletionItemKind.Property:cell.set_icon_name("lang-struct-field-symbolic");return;
+ case Lsp.CompletionItemKind.Unit: cell.set_icon_name("lang-variable-symbolic");return;
+ case Lsp.CompletionItemKind.Value: cell.set_icon_name("lang-variable-symbolic");return;
+ case Lsp.CompletionItemKind.Enum: cell.set_icon_name("lang-enum-symbolic");return;
+ case Lsp.CompletionItemKind.Keyword: cell.set_icon_name("completion-word-symbolic");return;
+ case Lsp.CompletionItemKind.Snippet: cell.set_icon_name("completion-snippet-symbolic");return;
+
+ case Lsp.CompletionItemKind.Color: cell.set_icon_name("lang-typedef-symbolic");return;
+ case Lsp.CompletionItemKind.File:cell.set_icon_name("lang-typedef-symbolic");return;
+ case Lsp.CompletionItemKind.Reference: cell.set_icon_name("lang-typedef-symbolic");return;
+ case Lsp.CompletionItemKind.Folder:cell.set_icon_name("lang-typedef-symbolic");return;
+ case Lsp.CompletionItemKind.EnumMember: cell.set_icon_name("lang-typedef-symbolic");return;
+ case Lsp.CompletionItemKind.Constant:cell.set_icon_name("lang-typedef-symbolic");return;
+ case Lsp.CompletionItemKind.Struct: cell.set_icon_name("lang-struct-symbolic");return;
+ case Lsp.CompletionItemKind.Event:cell.set_icon_name("lang-typedef-symbolic");return;
+ case Lsp.CompletionItemKind.Operator:cell.set_icon_name("lang-typedef-symbolic");return;
+ case Lsp.CompletionItemKind.TypeParameter:cell.set_icon_name("lang-typedef-symbolic");return;
+ default:
+
+
+
+
+ cell.set_icon_name("completion-snippet-symbolic");
+ return;
+ }
+
+
+ case GtkSource.CompletionColumn.COMMENT:
+ cell.set_text(p.text);
+ break;
+ case GtkSource.CompletionColumn.DETAILS:
+ if (p.ci.documentation != null) {
+ cell.set_text(p.ci.documentation.value);
+ return;
+ }
+
+ cell.set_text(p.text);
+ break;
+ default:
+ cell.set_text(null);
+ break;
+ }
+ }
+
+ bool in_populate = false;
+
+ internal async GLib.ListModel populate_async (GtkSource.CompletionContext context, GLib.Cancellable? cancellable)
{
- bool has_matches = false;
- var filtered_proposals = this.fetchMatches(context, out has_matches);
- if (!has_matches) {
- context.add_proposals (this, null, true);
- return;
+ GLib.debug("pupoulate async");
+ var ret = new GLib.ListStore(typeof(CompletionProposal));
+
+ if (this.in_populate) {
+ GLib.debug("pupoulate async - skipped waiting for reply");
+ return ret;
+ }
+ this.in_populate = true;
+
+ global::Gtk.TextIter begin, end;
+ Lsp.CompletionList res;
+ if (context.get_bounds (out begin, out end)) {
+ var line = end.get_line();
+ var offset = end.get_line_offset();
+ if (this.editor.prop != null) {
+ // tried line -1 (does not work)
+ GLib.debug("node pad = '%s' %d", this.editor.node.node_pad, this.editor.node.node_pad.length);
+
+ line += this.editor.prop.start_line ;
+ // this is based on Gtk using tabs (hence 1/2 chars);
+ offset += this.editor.node.node_pad.length;
+ // javascript listeners are indented 2 more spaces.
+ if (this.editor.prop.ptype == JsRender.NodePropType.LISTENER) {
+ offset += 2;
+ }
+ }
+ // this should not really be slow, as it's a quick repsonse
+ yield this.file.getLanguageServer().document_change_force(this.file, this.editor.tempFileContents());
+ try {
+ GLib.debug("sending request to language server %s", this.file.getLanguageServer().get_type().name());
+
+ res = yield this.file.getLanguageServer().completion(this.file, line, offset, 1);
+ } catch (GLib.Error e) {
+ GLib.debug("got error %s", e.message);
+ this.in_populate = false;
+ return ret;
+ }
+
+ } else {
+ this.in_populate = false;
+ return ret;
+ }
+
+ GLib.debug("pupoulate async - got reply");
+ this.model = new CompletionModel(this, context, res, cancellable);
+ var word = context.get_word();
+ GLib.debug("Context word is %s, %d", word, (int)word.length);
+ if (word.length < 1) {
+ word = " "; // this should filter out everything, and prevent it displaying
}
- // add proposals triggers a critical error in Gtk - try running gtksourceview/tests/test-completion.
- // see https://bugzilla.gnome.org/show_bug.cgi?id=758646
- var fe = GLib.Log.set_always_fatal(0);
- context.add_proposals (this, filtered_proposals, true);
- GLib.Log.set_always_fatal(fe);
+
+ var expression = new global::Gtk.PropertyExpression(typeof(CompletionProposal), null, "label");
+ this.filter = new global::Gtk.StringFilter(expression);
+ this.filter.set_search( word);
+ var filter_model = new global::Gtk.FilterListModel(this.model, this.filter);
+ filter.match_mode = global::Gtk.StringFilterMatchMode.PREFIX;
+ filter_model.set_incremental(true);
+ this.in_populate = false;
+ return filter_model;
+
+
+
}
+ internal void refilter (GtkSource.CompletionContext context, GLib.ListModel in_model)
+ {
+
+ //GLib.debug("pupoulate refilter");
+ if (this.filter == null) {
+ return;
+ }
+
+ var word = context.get_word();
+ this.filter.set_search(word);
+
+
+ }
- public bool activate_proposal (SourceCompletionProposal proposal, TextIter iter)
+/*
+ public bool activate_proposal (GtkSource.CompletionProposal proposal, TextIter iter)
{
var istart = iter;
istart.backward_find_char(is_space, null);
return true;
}
+
+
- public SourceCompletionActivation get_activation ()
- {
- //if(SettingsManager.Get_Setting("complete_auto") == "true"){
- return SourceCompletionActivation.INTERACTIVE | SourceCompletionActivation.USER_REQUESTED;
- //} else {
- // return Gtk.SourceCompletionActivation.USER_REQUESTED;
- //}
+ private bool is_space(unichar space){
+ return space.isspace() || space.to_string() == "";
}
-
- public int get_interactive_delay ()
+ */
+
+ }
+ public class CompletionModel : Object, GLib.ListModel
+ {
+ CompletionProvider provider;
+ Gee.ArrayList<CompletionProposal> items;
+ string search;
+ int minimum_word_size = 2;
+
+ public Cancellable? cancellable;
+
+ public CompletionModel(CompletionProvider provider, GtkSource.CompletionContext context, Lsp.CompletionList? res, Cancellable? cancellable)
+ {
+ this.provider = provider;
+ this.cancellable = cancellable;
+ this.items = new Gee.ArrayList<CompletionProposal>();
+
+ var word = context.get_word();
+ GLib.debug("looking for %s", word);
+ this.search = word;
+ if (res != null) {
+ foreach(var comp in res.items) {
+ if (comp.label == "_") { // skip '_'
+ continue;
+ }
+ this.items.add(new CompletionProposal(comp));
+
+ }
+ }
+ print("GOT %d results\n", (int) items.size);
+ // WHY TWICE?
+ if (this.items.size < this.minimum_word_size) {
+ return;
+ }
+
+ items.sort((a, b) => {
+ return ((string)(a.label)).collate((string)(b.label));
+ });
+
+ }
+
+
+
+ public GLib.Object? get_item (uint pos)
+ {
+ return (Object) this.items.get((int) pos);
+ }
+ public GLib.Type get_item_type ()
{
- return -1;
+ return typeof(GtkSource.CompletionProposal);
}
-
- public bool get_start_iter (SourceCompletionContext context, SourceCompletionProposal proposal, out TextIter iter)
+ public uint get_n_items ()
{
- return false;
+ return this.items.size;
}
-
- public void update_info (SourceCompletionProposal proposal, SourceCompletionInfo info)
+ public bool can_filter (string word)
{
+ if (word == null || word[0] == 0) {
+ return false;
+ }
+
+ if (word.length < this.minimum_word_size) {
+ return false;
+ }
+ /* If the new word starts with our initial word, then we can simply
+ * refilter outside this model using a GtkFilterListModel.
+ */
+
+ return word.has_prefix(this.search);
}
-
- private bool is_space(unichar space){
- return space.isspace() || space.to_string() == "";
- }
-
- private bool is_forward_space(unichar space){
- return !(
- space.to_string() == " "
- ||
- space.to_string() == ""
- ||
- space.to_string() == "\n"
- ||
- space.to_string() == ")"
- ||
- space.to_string() == "("
-
- );
+ public void cancel ()
+ {
+ if (this.cancellable != null) {
+ this.cancellable.cancel();
+ }
}
- }
+
+ }
+ public class CompletionProposal : Object, GtkSource.CompletionProposal
+ {
+
+ public string label { get; set; default = ""; }
+
+ public string text { get; set; default = ""; }
+ public string info { get; set; default = ""; }
+
+ public Lsp.CompletionItem ci;
+
+ public CompletionProposal(Lsp.CompletionItem ci) //string label, string text, string info)
+ {
+
+ this.ci = ci;
+ this.text = ci.detail == null ? "" : ci.detail ;
+ this.label = ci.label;
+ this.info = ci.documentation == null ? "": ci.documentation.value;
+ //GLib.debug("SET: detail =%s, label = %s; info =%s", ci.detail, ci.label, "to long..");
+ }
+
+ }
+
}