Coder Social home page Coder Social logo

chevrotain-nested-scope-lang-content-assist's Introduction

Chevrotain Nested scope language with Content Assist

This example project aims to demonstrate how to build a chevrotain based language (lexer, compiler etc) with nested scopes.

This involves building a scope stack with a symbol stack for each level in the stack. This can then be used for IDE/editor content assist, displaying a list valid variable references at a given point in the document.

WIP

Please note that the error-recovery and syntax folder are currently only for reference until proper error recovery and syntax is developed for this Nested scope language (example).

Tech

Install

$ npm install

Run tests

This project uses Jest with jest-extended for additional convenience expectations

$ npx jest

Design

  • lexer
  • parser
  • actions and AST
  • scope builder
  • indexed assignment nodes
  • content assist using lookup in indexed assignment map (by position)
  • error handling and recovery (TODO)

Lexer

let inputText = "{ b = 2 }";
let lexingResult = lex(inputText);
const { tokens } = lexingResult;

Parser

const inputText = "{ b = 2 }";
const result = parse(inputText);

Invalid input

let inputText = "{ %b = 2 ";
parse(inputText); // throws

Note that the parser must have error recovery enabled in order to function with an invalid document in the editor:

class JsonParser extends CstParser {
    constructor() {
        super(allTokens, {
            // by default the error recovery / fault tolerance capabilities are disabled
            recoveryEnabled: true
        })
    }

Actions

AST

Given an input of: a=1

The AST generated for both embedded and visitor actions looks like this:

{
  type: "ASSIGNMENT",
  variableName: "a",
  valueAssigned: "1",
  position: {
    startOffset: 1,
    endOffset: 1,
    startLine: 1,
    endLine: 1,
    startColumn: 2,
    endColumn: 2
  }
}

Nested Scope example

The nested scope language example can be found in src/scope-lang.

It is intended as an example for how to work with nested scopes and provide content assist over LSP for an editor/IDE such as VS Code.

Content Assist

Alibaba Quick Start guide to VSC plugins

To add the completion provider (aka "content assist) for a VSC extension

connection.onInitialize((params): InitializeResult => {
  return {
    capabilities: {
      // ...
      completionProvider: {
        resolveProvider: true,
        triggerCharacters: ["="]
      },
      hoverProvider: true
    }
  };
});

Note: Much of the following code can be found in scope/lang/lsp/advanced

Sample onCompletion handler:

connection.onCompletion((textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
  let text = documents.get(textDocumentPosition.textDocument.uri).getText();
  let position = textDocumentPosition.position;
  const lines = text.split(/\r?\n/g);
  const currentLine = lines[position.line]

  // use parsed model to lookup via position
  // return a list of auto complete suggestions (for = assignment)
  // must be array of CompletionItem (see below)
  return results;
const assignmentIndex = {
  3: { varsAvailable: ["a"] },
  9: { varsAvailable: ["a, b"] },
  17: { varsAvailable: ["b", "c"] }
};
const toAst = (inputText: string, opts = {}) => {
  const lexResult = lex(inputText);

  const toAstVisitorInstance: any = new AstVisitor(opts);

  // ".input" is a setter which will reset the parser's internal's state.
  parserInstance.input = lexResult.tokens;

  // Automatic CST created when parsing
  const cst = parserInstance.statements();

  if (parserInstance.errors.length > 0) {
    throw Error(
      "Sad sad panda, parsing errors detected!\n" +
        parserInstance.errors[0].message
    );
  }
  const ast = toAstVisitorInstance.visit(cst);
  // console.log("AST - visitor", ast);
  return ast;
}

const onChange = (textDocumentPosition: TextDocumentPositionParams) => {
    let text = documents.get(textDocumentPosition.textDocument.uri).getText();
    const scopeTree = toAstVisitor(text, { positioned: true });

    // run scope builder
    const builder = new ScopeStackBuilder();
    builder.build(scopeTree);
    const { lineMap } = builder;
    // we should
    this.find = {
      assignment: createIndexMatcher(lineMap, "assignment");
    }
  };
};

const onCompletion = (textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
  // position has character and line position
  let text = documents.get(textDocumentPosition.textDocument.uri).getText();
  let position = textDocumentPosition.position;
  const lines = text.split(/\r?\n/g);
  const line = lines[position.line]

  // determine char just before position  
  const lastCharLinePos = Math.min(0, position.character -1)
  const lastTypedChar = line.charAt(lastCharLinePos)

  // map different completion functions for = and _
  completionFn = getCompletionFnFor(lastTypedChar)

  // TODO: execute completion function for last char typed that triggered it

  const pos = {
    line: position.line,
    column: position.character
  };

  let assignmentValue = 'xyz...' // see solution below

  // return a list of auto complete suggestions (for = assignment)
  const const { data, column } = this.find.assignment(pos, assignmentValue);
  const varsWithinScope = data.varsAvailable;
  let completionItems = new Array<CompletionItem>();

  // build completion items list
  varsWithinScope.map(varName => results.push({
    label: varName,
    kind: CompletionItemKind.Reference,
    data: varName
  }))
  return completionItems;
};

See CompletionItemKind enum (and more VS Code API documentation)

Imagine we add a trigger on _ for a language using snake case for variable names.

completionProvider: {
  resolveProvider: true,
  triggerCharacters: ["=", "_"]
},

We could then use an AST? lookup to detect that we are in the middle of an assignment and are typing a name for the RHS (value/reference being assigned).

var c = abc_

We could use logic like the following to propose only var names that start with what we have typed so far.

const const { data, column } = this.find.assignment(pos);

const line = lines[position.line]
const wordBeingTypedAfterAssignToken = line.slice(column+1, position.character).trim()

const filterVars = (varsWithinScope) => varsWithinScope.filter(varName => varName.startsWith(wordBeingTypedAfterAssignToken))

const isTypingVarName = wordBeingTypedAfterAssignToken.length > 0

// if we are typing a (variable) name ref
// - display var names that start with typed name
// - otherwise display all var names in scope
const relevantVarsWithinScope = isTypingVarName ? filterVars(varsWithinScope) : varsWithinScope

Named scopes

We could also use named scopes, similar to namespaces or modules/classes etc.

    $.RULE("scope", () => {
      $.CONSUME(Identifier); // named scopes
      $.CONSUME(BeginScope);
      $.AT_LEAST_ONE({
        DEF: () => {
          $.SUBRULE($.statement);
        }
      });
      $.CONSUME(EndScope);
    });
alpha {
  a = 2
}

Then we could generate the varsAvailable as a map instead, to indicate for which scope a particular variable is made available (reachable):

We could have the parser wrap the source code in a global namespace by convention

global {
  alpha {
    a = 2
  }
}

Then the simple solution would return varsAvailable as follows

varsAvailable = {
  a: {
    scope: 'alpha'
  },
  b: {
    scope: 'global'
  }
}

Scope names are hierarchical, so it would be better to reference the full nested scope name.

varsAvailable = {
  a: {
    scope: 'global.alpha'
  },
  b: {
    scope: 'global'
  }
}

This could be easily achieved by maintaining another scope stack namedScopes in the same fashion that varsAvailable are built, then joining the scope names by . to create the full scope name.

Completion resolve

We can also add an onCompletionResolve handler as follows. This can be used to provide additional context and documentation for the option available to be selected.

connection.onCompletionResolve(
    (item: CompletionItem): CompletionItem => {
        item.detail = item.data;
        item.documentation = `${item.data} reference`;
        return item;
    }
)

VS Code Language extension

See VSC Language extension

Editor/IDE utils

chevrotain-editor-utils

Language Server utils

chevrotain-lsp-utils

chevrotain-nested-scope-lang-content-assist's People

Contributors

bd82 avatar kristianmandrup avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

bd82

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.