Motoko VS Code Extension Improvements — Stage 2

Improvements and New Features for the Motoko VSCode Extension

Hello, everyone! Serokell is pleased to announce that we’ve completed a new grant for the Visual Studio Code extension for Motoko, which brings new features and improvements starting with version 0.21.0. Below are the key contributions that we are proud to share with you.

You can also check our pitch video on YouTube: https://youtu.be/3CSH2bRh-ZI

New features

Signature help

Users should now see hints for function parameters as they are being typed.

signature-help

Types, documentation, and better kinds for completion items

Completion items will now show the type and documentation, when available. In addition, the kind (the icon to the left of the completion item name) should be more accurate.

completion-improvements

Scoped completion items

Completion items now consider the scopes in which variables are defined. In the video below, the in-scope definedInForBlock is displayed, while the off-scope definedInIfBlock is omitted, uncluttering the completion list.

completion-scope

References of record fields

The extension was previously able to find fields defined in actors, classes, and modules, but not objects defined in types. We’ve extended the definitions and references searches so that record types are also considered.

references-small

Renames

The extension is now able to rename symbols across a project.

rename

Support for multiple compiler versions

Previously, the version of moc.js (the Motoko compiler compiled to JavaScript) used by the extension was hardcoded, varying based on the extension’s version. Now, the correct version of the compiler will be downloaded and cached based on the mops toolchain, or failing that, from the dfx cache.

In logs, you might see messages such as this:

download-moc

The compiler should be downloaded to <extension_installation_path>/out/compiler/moc-<version>.js

Miscellaneous improvements and fixes

Structured types for every node

Users of node-motoko may have noticed that, when parsing the Abstract Syntax Tree (AST) with types, only DotE expressions contained the typeRep field, with the structured type AST. After some optimizations in the Motoko compiler, we now serialize the typeRep field for all nodes that can be assigned a type.

  • Here are the results of a benchmark before our changes:

    ┌─────────────┬──────────┐
    │ (index)     │ Values   │
    ├─────────────┼──────────┤
    │ Mean (ms)   │ 1684.89  │
    │ Median (ms) │ 1587.55  │
    │ Min (ms)    │ 1441.61  │
    │ Max (ms)    │ 2887.5   │
    │ Total (ms)  │ 16848.86 │
    └─────────────┴──────────┘
    
  • And after our changes:

    ┌─────────────┬──────────┐
    │ (index)     │ Values   │
    ├─────────────┼──────────┤
    │ Mean (ms)   │ 1615.52  │
    │ Median (ms) │ 1535.63  │
    │ Min (ms)    │ 1378.18  │
    │ Max (ms)    │ 2736.59  │
    │ Total (ms)  │ 16155.19 │
    └─────────────┴──────────┘
    

Which show that even after serializing the structured types for all nodes, there is still a small improvement.

As an example, take the following snippet:

func identity<T>(x : T) : T {
  x
};
  • Which produces the following AST:

    {
      name: 'Prog',
      args: [
        {
          name: 'LetD',
          args: [
            {
              name: 'VarP',
              args: [
                {
                  name: 'ID',
                  args: [ 'identity' ],
                  type: [Getter/Setter: '<T>(x : T) -> T'],
                  typeRep: [Getter/Setter] {
                    name: 'Func',
                    args: [
                      'Local',
                      'Returns',
                      { name: 'T', args: [ 'Any' ] },
                      {
                        name: '',
                        args: [
                          {
                            name: 'Name',
                            args: [ 'x', { name: 'Var', args: [ 'T', '0' ] } ]
                          }
                        ]
                      },
                      {
                        name: '',
                        args: [ { name: 'Var', args: [ 'T', '0' ] } ]
                      }
                    ]
                  },
                  doc: [Getter/Setter: undefined],
                  start: [ 1, 5 ],
                  end: [ 1, 13 ]
                }
              ],
              type: '<T>(x : T) -> T',
              typeRep: {
                name: 'Func',
                args: [
                  'Local',
                  'Returns',
                  { name: 'T', args: [ 'Any' ] },
                  {
                    name: '',
                    args: [
                      {
                        name: 'Name',
                        args: [ 'x', { name: 'Var', args: [ 'T', '0' ] } ]
                      }
                    ]
                  },
                  {
                    name: '',
                    args: [ { name: 'Var', args: [ 'T', '0' ] } ]
                  }
                ]
              },
              start: [ 1, 5 ],
              end: [ 1, 13 ]
            },
            {
              name: 'FuncE',
              args: [
                '<T>(x : T) -> T',
                'Local',
                'identity',
                {
                  name: 'T',
                  args: [
                    {
                      name: 'PrimT',
                      args: [ 'Any' ],
                      type: 'Any',
                      typeRep: 'Any',
                      start: [ 1, 14 ],
                      end: [ 1, 15 ]
                    }
                  ],
                  start: [ 1, 14 ],
                  end: [ 1, 15 ]
                },
                {
                  name: 'ParP',
                  args: [
                    {
                      name: 'AnnotP',
                      args: [
                        {
                          name: 'VarP',
                          args: [
                            {
                              name: 'ID',
                              args: [ 'x' ],
                              type: [Getter/Setter: 'T'],
                              typeRep: [Getter/Setter] { name: 'Con', args: [ 'T' ] },
                              doc: [Getter/Setter: undefined],
                              start: [ 1, 17 ],
                              end: [ 1, 18 ]
                            }
                          ],
                          type: 'T',
                          typeRep: { name: 'Con', args: [ 'T' ] },
                          start: [ 1, 17 ],
                          end: [ 1, 18 ]
                        },
                        {
                          name: 'PathT',
                          args: [
                            {
                              name: 'IdH',
                              args: [
                                {
                                  name: 'ID',
                                  args: [ 'T' ],
                                  type: [Getter/Setter: undefined],
                                  typeRep: [Getter/Setter: undefined],
                                  doc: [Getter/Setter: undefined],
                                  start: [ 1, 21 ],
                                  end: [ 1, 22 ]
                                }
                              ]
                            }
                          ],
                          type: 'T',
                          typeRep: { name: 'Con', args: [ 'T' ] },
                          start: [ 1, 21 ],
                          end: [ 1, 22 ]
                        }
                      ],
                      type: 'T',
                      typeRep: { name: 'Con', args: [ 'T' ] },
                      start: [ 1, 17 ],
                      end: [ 1, 22 ]
                    }
                  ],
                  type: 'T',
                  typeRep: { name: 'Con', args: [ 'T' ] },
                  start: [ 1, 16 ],
                  end: [ 1, 23 ]
                },
                {
                  name: 'PathT',
                  args: [
                    {
                      name: 'IdH',
                      args: [
                        {
                          name: 'ID',
                          args: [ 'T' ],
                          type: [Getter/Setter: undefined],
                          typeRep: [Getter/Setter: undefined],
                          doc: [Getter/Setter: undefined],
                          start: [ 1, 26 ],
                          end: [ 1, 27 ]
                        }
                      ]
                    }
                  ],
                  type: 'T',
                  typeRep: { name: 'Con', args: [ 'T' ] },
                  start: [ 1, 26 ],
                  end: [ 1, 27 ]
                },
                '',
                {
                  name: 'BlockE',
                  args: [
                    {
                      name: 'ExpD',
                      args: [
                        {
                          name: 'VarE',
                          args: [
                            {
                              name: 'ID',
                              args: [ 'x' ],
                              type: [Getter/Setter: 'T'],
                              typeRep: [Getter/Setter] { name: 'Con', args: [ 'T' ] },
                              doc: [Getter/Setter: undefined],
                              start: [ 2, 2 ],
                              end: [ 2, 3 ]
                            }
                          ],
                          type: 'T',
                          typeRep: { name: 'Con', args: [ 'T' ] },
                          start: [ 2, 2 ],
                          end: [ 2, 3 ]
                        }
                      ],
                      start: [ 2, 2 ],
                      end: [ 2, 3 ]
                    }
                  ],
                  type: 'T',
                  typeRep: { name: 'Con', args: [ 'T' ] },
                  start: [ 1, 28 ],
                  end: [ 3, 1 ]
                }
              ],
              type: '<T>(x : T) -> T',
              typeRep: {
                name: 'Func',
                args: [
                  'Local',
                  'Returns',
                  { name: 'T', args: [ 'Any' ] },
                  {
                    name: '',
                    args: [
                      {
                        name: 'Name',
                        args: [ 'x', { name: 'Var', args: [ 'T', '0' ] } ]
                      }
                    ]
                  },
                  {
                    name: '',
                    args: [ { name: 'Var', args: [ 'T', '0' ] } ]
                  }
                ]
              },
              start: [ 1, 0 ],
              end: [ 3, 1 ]
            }
          ],
          start: [ 1, 0 ],
          end: [ 3, 1 ]
        }
      ]
    }
    

The typeRep fields now appear consistently, allowing developers to inspect types.

Tests for all capabilities

One of the aspects that made the extension’s codebase difficult to work with was the lack of tests. Most LSP capabilities had no tests, and during our first grant, Serokell laid the groundwork for writing proper tests, adding them for completions, definitions, and references. Now, for our second grant, we’ve added tests for other capabilities that were untested: code actions (organize imports, quick fix imports), workspace symbols, and document symbols. We are positive that this will help contributors make changes more fearlessly, with better guarantees that their contributions work as intended.

Typer error recovery

The extension should now be more lenient when type-checking Motoko source code, showing more diagnostics related to types and allowing more capabilities to work despite the presence of type errors.

typer-error-recovery

The video above shows that we can see multiple typer errors. We can also hover over a to see that its type is Text.

Increased support for error recovery also improves the UX around two newly implemented features: signature help and support for completions for local and nested modules.

Local and nested modules completions

Previously, the extension would not display completions for modules that were defined locally or within other modules. Take the following snippet, as an example:

module Local {
  public let foo : Int = 5;
  public let bar : Text = "test";
  public type Foo = Nat;

  public module Nested {
    public let baz : Text = "nested";
  };
};

let a = Local.;
let b = Local.Nested.;

Trying to complete from Local. or Local.Nested. will now display the correct completion items.

It is awesome to see this progress! Great additions.