Dynamic autocompletion

Hey everyone,
I’ve created a code-editor using codemirror in react js. i want to provide dynamic autocompletion in the editor using tern js.
here is the code would look in vanilla js.

import { EditorView, basicSetup } from "codemirror";
import { EditorState } from "@codemirror/state";
import { javascript } from "@codemirror/lang-javascript";
import { autocompletion } from "@codemirror/autocomplete";

const state = EditorState.create({
  doc: "function main(){\n  return true;\n}\n",
  extensions: [basicSetup, javascript(), autocompletion()],
});
const editorView = new EditorView({
  state,
  parent: document.querySelector("#editor"),
});

here is the code of how I’ve created the tern server. you can try it to find how it is providing completions.

import * as tern from "tern";
import "tern/plugin/doc_comment";
import "tern/plugin/complete_strings";
import ecma from "tern/defs/ecmascript.json";

export default function getAutocompletion(code) {
  const query = {
    type: "completions",
    file: "temp.js",
    end: code.length,
    lineCharPositions: true,
    types: true,
  };

  const defs = [ecma];

  const ternServer = new tern.Server({
    getFile(filename, callback) {
      callback(null, code);
    },
    async: false,
    defs,
  });

  let ast = code;
  const completionList = [];

  ternServer.request(
    {
      query,
      files: [
        {
          type: "full",
          name: "temp.js",
          text: code,
          scope: "document",
        },
      ],
      doc: ast,
    },
    (error, res) => {
      if (error) {
        console.error("Tern.js error:", error);
        return;
      }

      if (res?.completions) {
        completionList.push(
          ...res?.completions.map((item) => ({
            label: item.name,
            detail: item.type,
            apply: () => item.name,
          }))
        );
      }
    }
  );

  return completionList;
}

console.log(getAutocompletion("let obj = {\n  foo: true\n}\nobj."));

can somebody help me use this getAutocompletion function to get completions in the editor ? I’ve tried various ways like override in autocompletion extension, completionList, using EditorView.updateListener.of() and few more… but none of them showed me completions in the editor.

Does the autocompletion example not give you enough information to do this?

I’ve tried it but it isn’t working as expected. I want to update the list of autocompletion every time there is a change the EditorState.doc. the getAutocompletions function returns completions dynamically using the tern server, the completions are not stored statically in a array. Isn’t there any proper way to solve this issue ? Or would it be a better approach to use codemirror v5 coz it has addon of tern server ?

That’s how autocompletion works in general, in CodeMirror. Completion can happen from a static list, but usually a completion source function returns a specific list for a completion at a given point in the current document.

1 Like

Hey, Thank you for your help. After spending hours going through the documentation and my code, I was finally able to solve the problem - actually, I solved it two days ago.

I used a function in the documentation that looked like this:

function myCompletions(context) {
  let before = context.matchBefore(/\w+/);
  // getting my completions amd storing it in completions variable
  let completions = getAutocompletion(context.state.doc.toString());

  // If completion wasn't explicitly started and there is no word before the cursor, don't open completions.
  if (!context.explicit && !before) {
    return null;
  }

  return {
    from: before ? before.from : context.pos,
    options: completions,
    validFor: /^\w*$/,
  };
}

I then used this function to override the completions in the autocompletion extension like so:

autocompletion({ override: [customProvider] })

Once again, thank you for your support.

1 Like

Hi Glitch, can you share your ternserver+codemirror6 whole codes? I need it also.

Hey there, I’m really sorry I came here after a long time :sweat:
My bad, i should have posted the whole code when I solved my issue.

I was using vite (vanilla javascript template) to build the project.
Here is what my tern + cm6 code looked like:

Html:

<div id="editor" class="editor"></div>

Javascript for using tern autocompletion:

import { EditorState } from "@codemirror/state";
import { basicSetup } from "codemirror";
import { EditorView } from "@codemirror/view";
import { javascript, javascriptLanguage } from "@codemirror/lang-javascript";
import {
  autocompletion,
  startCompletion,
} from "@codemirror/autocomplete";

import * as tern from "tern";
import "tern/plugin/doc_comment";
import "tern/plugin/complete_strings";
import ecma from "tern/defs/ecmascript.json";

// a basic completion list
let completions = [
  { label: "panic", detail: "panicking", type: "keyword" },
  { label: "park", type: "constant", info: "Test completion" },
  { label: "password", type: "variable" },
];

// Custom auto completion provider, docs at https://codemirror.net/examples/autocompletion/
function myCompletions(context) {
  // i don't remember why i used 2 different regex patterns
  // but the autocompletion doesn't work as expected if you remove one of them
  let before = context.matchBefore(/\w+/);
  let match = context.matchBefore(/\w+.?/);
  //console.log({ before, match });
  if (match) {
    completions = getAutocompletion(context.state.doc.toString(), context.pos);
  }
  // If completion wasn't explicitly started and there
  // is no word before the cursor, don't open completions.
  if (!context.explicit && !before) return null;
  //console.log({ context });
  return {
    from: before ? before.from : context.pos,
    options: completions,
    validFor: /^\w*$/,
  };
}

// adding our custom completion function into javascript language document completion
const jsDocCompletions = javascriptLanguage.data.of({
  autocomplete: myCompletions,
});

// extensions list
let extensions = [
  basicSetup,
  javascript(),
  autocompletion(),
  jsDocCompletions,
  EditorView.updateListener.of(updateList), // using update listener to check if user has pressed a dot (to explicitly provide completions)
];

let state = EditorState.create({
  doc: "console.log('Hello, world!');\n\nvar day = new Date()\n",
  extensions,
});

const editor = new EditorView({
  state,
  parent: document.getElementById("editor"),
});

// update listener to check if user has typed a dot
function updateList(args) {
  let change = args?.changes?.inserted[1]?.text[0];
  let changes = args?.changes?.sections[0];
  // console.log({ change, changes })
  if (change == ".") {
    startCompletion(editor);
  }
}

// our function to get all the completions from tern.js
// I don't remember how i got this code... (maybe i used ChatGPT, or maybe i got to this code after alot of trials and errors...)
function getAutocompletion(code, position) {
  const defs = [ecma];
  // creating our tern server first
  const ternServer = new tern.Server({
    getFile(filename, callback) {
      return code;
      // callback function was working for me, idk what happened now, i just need to return the code (string)
      // callback(null, code);
    },
    async: false,
    defs,
  });
  const query = {
    type: "completions",
    file: "index.js",
    end: position,
    lineCharPositions: true,
    types: true,
  };

  const completionList = [];
  // requesting completions from our tern server
  ternServer.request(
    {
      query,
      files: [
        {
          type: "full",
          name: "temp.js",
          text: code,
          scope: "document",
        },
      ],
      doc: code,
    },
    (error, res) => {
      if (error) {
        console.error("Tern.js error:", error);
        return;
      }

      if (res?.completions) {
        completionList.push(
          ...res?.completions?.map((item) => ({
            label: item.name,
            apply: item.name,
            detail: item.type,
          }))
        );
      }
    }
  );
  //console.log({ completionList });
  return completionList;
}

Here’s the package versions i was using:

  "@codemirror/autocomplete": "^6.7.1",
  "@codemirror/lang-javascript": "^6.1.8",
  "@codemirror/state": "^6.2.1",
  "@codemirror/view": "^6.12.0",
  "codemirror": "^6.0.1",
  "tern": "^0.24.3"

Again sorry for replying to you soo lately, I hope this will helpful to somebody.

Just implement the CompletionSource interface, you could add as many sources as you want.
In the code bellow I have a data-finalc point to a remote server to get the autocompletion candicates.

<div class="cm-editor-wrap"
 id="html-cm-wrap"
 x-show="activetab === 'html'">
  <input
    type="hidden"
    name="html"
    id="playground-html"
    x-on:demo-change.window="
      $el.value=$store.demos.currentItem.html;
      $dispatch('writeback', {value: $store.demos.currentItem.html})"
    x-on:cmwritein.debounce.2000ms="
      if($event.detail.cmid === 'playground-html')
      { 
        $store.demos.currentItem.html = $event.detail.value;
        $dispatch('html-change', {});
      }"
    data-final-try="/devtools/finaltry"
    data-finalc="https://lets-script.com/devtools/ph-playground-completion"
    data-lang="html"
    x-bind:data-height="cmheight"
    x-bind:data-max-height="cmSizes['html-cm-wrap']"
    data-firewritein
    data-resizable
    data-mode="normal"
  />
</div>

and here’s the demo. https://pagehelper.lets-script.com/playground/