Skip to content

Commit

Permalink
feat: make built-ins pluggable
Browse files Browse the repository at this point in the history
  * You can now provide built-ins (other than Camunda)
    as you instantiate the editor.

chore: simplify internal structure

  * This is a larger rewrite of the inner structure of the tool,
    fixing some longer standing bugs

chore: treat built-ins like special variables

fix: recognize built-ins in the language grammar

  * We properly configure the language based on built-ins provided
    this allows the grammar to recognize `get or else` or other
    bogus built-ins that would otherwise be recognized as language
    constructs

  Related to camunda/camunda-modeler#3983
  • Loading branch information
nikku committed Jun 15, 2024
1 parent 6e0f4e7 commit ef90925
Show file tree
Hide file tree
Showing 21 changed files with 872 additions and 600 deletions.
2 changes: 1 addition & 1 deletion scripts/compileBuiltins.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const fs = require('node:fs/promises');

// paths relative to CWD
const MARKDOWN_SRC = './camunda-platform-docs/docs/components/modeler/feel/builtin-functions/*.md';
const JSON_DEST = './src/autocompletion/builtins.json';
const JSON_DEST = './src/builtins/camunda.json';

glob(MARKDOWN_SRC).then(files => {

Expand Down
6 changes: 0 additions & 6 deletions src/autocompletion/VariableFacet.js

This file was deleted.

17 changes: 0 additions & 17 deletions src/autocompletion/autocompletionUtil.js

This file was deleted.

60 changes: 0 additions & 60 deletions src/autocompletion/builtins.js

This file was deleted.

37 changes: 22 additions & 15 deletions src/autocompletion/index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { autocompletion, completeFromList } from '@codemirror/autocomplete';
import { snippets, keywordCompletions } from 'lang-feel';
import { completions as builtinCompletions } from './builtins';
import { completions as pathExpressionCompletions } from './pathExpressions';
import { snippets, keywordCompletions, snippetCompletion } from 'lang-feel';

import { completions as variableCompletions } from './variables';
import { pathExpressionCompletion } from './pathExpression';
import { variableCompletion } from './variable';

/**
* @typedef { import('../core').Variable } Variable
* @typedef { import('@codemirror/autocomplete').CompletionSource } CompletionSource
*/

/**
* @param { {
* variables?: Variable[],
* builtins?: Variable[]
* } } options
*
* @return { CompletionSource[] }
*/
export function completions({ variables = [], builtins = [] }) {

export default function() {
return [
autocompletion({
override: [
variableCompletions,
builtinCompletions,
completeFromList(snippets.map(s => ({ ...s, boost: -1 }))),
pathExpressionCompletions,
...keywordCompletions
]
})
pathExpressionCompletion({ variables }),
variableCompletion({ variables, builtins }),
snippetCompletion(snippets.map(snippet => ({ ...snippet, boost: -1 }))),
...keywordCompletions
];
}
120 changes: 120 additions & 0 deletions src/autocompletion/pathExpression.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { syntaxTree } from '@codemirror/language';
import { isPathExpression } from './util';

/**
* @typedef { import('../core').Variable } Variable
* @typedef { import('@codemirror/autocomplete').CompletionSource } CompletionSource
*/

/**
* @param { {
* variables?: Variable[],
* } } options
*
* @return { CompletionSource }
*/
export function pathExpressionCompletion({ variables }) {

return (context) => {

const nodeBefore = syntaxTree(context.state).resolve(context.pos, -1);

if (!isPathExpression(nodeBefore)) {
return;
}

const expression = findPathExpression(nodeBefore);

// if the cursor is directly after the `.`, variable starts at the cursor position
const from = nodeBefore === expression ? context.pos : nodeBefore.from;

const path = getPath(expression, context);

let options = variables;
for (var i = 0; i < path.length - 1; i++) {
var childVar = options.find(val => val.name === path[i].name);

if (!childVar) {
return null;
}

// only suggest if variable type matches
if (
childVar.isList !== 'optional' &&
!!childVar.isList !== path[i].isList
) {
return;
}

options = childVar.entries;
}

if (!options) return;

options = options.map(v => ({
label: v.name,
type: 'variable',
info: v.info,
detail: v.detail
}));

const result = {
from: from,
options: options
};

return result;
};
}


function findPathExpression(node) {
while (node) {
if (node.name === 'PathExpression') {
return node;
}
node = node.parent;
}
}

// parses the path expression into a list of variable names with type information
// e.g. foo[0].bar => [ { name: 'foo', isList: true }, { name: 'bar', isList: false } ]
function getPath(node, context) {
let path = [];

for (let child = node.firstChild; child; child = child.nextSibling) {
if (child.name === 'PathExpression') {
path.push(...getPath(child, context));
} else if (child.name === 'FilterExpression') {
path.push(...getFilter(child, context));
}
else {
path.push({
name: getNodeContent(child, context),
isList: false
});
}
}
return path;
}

function getFilter(node, context) {
const list = node.firstChild;

if (list.name === 'PathExpression') {
const path = getPath(list, context);
const last = path[path.length - 1];
last.isList = true;

return path;
}

return [ {
name: getNodeContent(list, context),
isList: true
} ];
}

function getNodeContent(node, context) {
return context.state.sliceDoc(node.from, node.to);
}
106 changes: 0 additions & 106 deletions src/autocompletion/pathExpressions.js

This file was deleted.

Loading

0 comments on commit ef90925

Please sign in to comment.