Skip to content

Adding custom mappers

Oleg Shilo edited this page Jul 18, 2024 · 19 revisions

The most intriguing plugin's feature is the possibility to extend it to support new even the most exotic syntaxes.

There are two ways of achieving this: with a dedicated mapper script or by using a generic mapper with the syntax-specific Regex definitions in the user settings.

When creating a new or editing an existing mapper, the easiest way to set up the environment is to execute VSCode either CodeMap: Edit ... or CodeMap: Create ... command.

Mapping basics

Any mapper produces a map definition - a collection of strings/lines where every line describes a code map item via a very simple format:

[indent]<name>|<line>|<icon>

indent - the whitespace indent is optional and is used to express the code nesting (if applicable). Indentation works pretty much like in Python syntax.

name - display the name of the navigatable code member

line - line number to navigate to

icon - the name of the predefined icon:

  • class
  • interface
  • function
  • property
  • document
  • level1
  • level2
  • level3
  • <leave icon property unassigned if you do not want any icon>

Note, you can also use custom icons specified by absolute path: path:<absolute path to svg icon file> The path string can contain a special token {theme} which is replaced at runtime with the word "dark" or "light". Depending on the VS theme. Sample (generic mapper):

    "codemap.md": [
      {
          "pattern": "^(\\s*)### (.*)",
          "clear": "###",
          "prefix": " -",
          "icon": "path:E:\\icons\\VSCode\\codemap\\{theme}\\custom_level_a.svg"
      },

You can also use an emoji character for icon. In this case, the tree node icon will be empty and the emoji character will be placed in the very first position of the node title. This is a pure syntactic sugar feature and the same can be achieved by returning the emoji character as a prefix in the name.

Below is a sample map definition produced by the built-in Python mapper:

def settings()|1|function
class NavigateCodeMap|4|class
    def highlight_line()|6|function
    def keep_going_down()|9|function
        def indented()|11|function
    def keep_going_up()|16|function
        def indented()|18|function
        def up()|23|function
def down()|26|function

And this is how this map is transformed into the code tree:

Mapping configuration

The information about the mapping rules is stored in the extension configuration (settings.json file). It is stored in this format:

"codemap.<file extension>": "<mapper information>",

None, Complex file extensions containing dots should be dot-escaped in the config file with the / character due to the VSCode limitations of not handling dots in the config names correctly.

IE the mapper configuration for *.git.md files should be encoded in the setting.json as codemap.git/md:

"codemap.git/md": "config:codemap.md",

IMPORTANT:

If you want to replace the default mapper that comes with the extension out of the box (e.g. for JSON, MD), you will need to specify a special suffix overloaded in the config key name so the extension knows that you are replacing the existing mapper:

"codemap.overloaded.md": "D:\\Dropbox_local\\software\\V\\Visual Studio Code\\extensions\\mapper_md.js",

Otherwise, you may get the runtime error if the type of the default mapper (regex vs dedicated) does not match a new mapper overload:

Regex-based definition of the mapping rules for Markup syntax.
Incorrect type. Expected "array".

Mapping with a dedicated mapper

A dedicated mapper is nothing else but a JS file implementing a very simple routine (class mapper method generate) that produces the map definition by analyzing the VS document passed as a method parameter.

The sample below is a simple mapper that produces the map of the MD (Markdown syntax) file. The mapping handles entries for lines starting with '###', '##', # and containing images.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");

class mapper {

    static read_all_lines(file) {
        let text = fs.readFileSync(file, 'utf8');
        return text.split(/\r?\n/g);
    }

    static generate(file) {
        let members = [];
        try {
            let line_num = 0; 
            let image_index = 1;
            mapper
                .read_all_lines(file)
                .forEach(line => {
                    line_num++;
                    line = line.trimStart();
                    if (line.startsWith("### "))
                        members.push(`${line.substr(4)}|${line_num}|level3`);
                    else if (line.startsWith("## "))
                        members.push(`${line.substr(3)}|${line_num}|level2`);
                    else if (line.startsWith("# "))
                        members.push(`${line.substr(2)}|${line_num}|level1`);
                    else if (line.startsWith("![image]"))
                        members.push(`<image ${image_index++}>|${line_num}|none`);
            });
        }
        catch (error) {
        }
        return members;
    }
}
exports.mapper = mapper;

And the produced map:

You can automate the creation of the dedicated mapper skeleton code by executing VSCode command "CodeMap: Create ...". It will create a dedicated mapper for the currently opened file type. And you can also use "CodeMap: Edit ..." command to open an already existing mapper for editing.

To enable a dedicated mapper you will need to save its content in the JavaScript file (e.g. mapper_md.js). Add the corresponding entry to your user settings so the plugin can load the mapper if the file with a specific file extension is loaded:

// "codemap.<file extension>": "<mapper file path>"

"codemap.md": "E:\\User\\Projects\\VSCode\\mapper_md.js",

Mapping with the generic mapper

The plugin comes with a built-in generic mapper, which is a specially designed mapper that uses a set of Regex values from the settings file to produce the map. While it's not as generic and powerful as a dedicated mapper it gives the user the ability to quickly add support for mapping new syntaxes by simply listing the most important tokens of the syntax (e.g. keywords) in the settings and avoid dealing with the scripts.

Below is the sample of the MD syntax mapper, which deliverers practically the same functionality as the mapper_md.js sample:

"codemap.md": [
    {
      "pattern": "^(\\s*)### (.*)",
      "clear": "##",
      "prefix": " -",
      "icon": "level3"
    },
    {
      "pattern": "^(\\s*)## (.*)",
      "clear": "##",
      "prefix": " ",
      "icon": "level2"
    },
    {
      "pattern": "^(\\s*)# (.*)",
      "clear": "#",
      "prefix": "",
      "icon": "level1"
    },
    {
      "pattern": "!\\[image\\]",
      "clear": "![image]",
      "prefix": "<image>",
      "icon": "none"
    }
  ],

And the produced map:

Reusing mappers

When defining a generic mapper in the settings you can link multiple syntaxes (file extensions) to the same mapper. This is how Codemap links XML, SVG and XAML to the same mapper:

"codemap.svg": "config:codemap.xml",
"codemap.xaml": "config:codemap.xml",
"codemap.xml": [
  {
    "pattern": "\\s<[^\\/|^\\!][\\w:]*",
    "clear": "<",
    "prefix": "",
    "icon": "level3"
  }
],

Generic mapper rules

pattern: the pattern value for the RegExp to match the string that needs to be presented in the map.
clear: A |-delimited string values (RegExp is allowed) to be removed form the match returned by pattern.
prefix: Optional prefix to be added to the code map entry displayed text (e.g. "section ").
suffix: Optional suffix to be added to the code map entry displayed text (e.g. ":").
icon: The name of the predefined icon, icon path value (path:...) or an emoji character
levelIndent: Optional setting to allow defining logical indent for otherwise non-indented items matching generic mapper definition (triggered by #32). This value defines not physical but logical indent that is assigned to the matching item that is non-indented at otherwise at runtime. For the cases of mixed indented/non-indented syntax like yours, this value needs to match the indent of the main (indented) syntax. Thus if your Python used 4 spaces to define first level nesting should be 4. And the second level - 8.

How To

Hint
When creating a custom generic mapper it is difficult to get it right from the first hit because your mapper will depend on the correctness of your regular expressions. Which in turn usually starts working only after a few attempts. Thus it will be much easier if you implement a very generic mapper first and only then start refining your regex patterns.

Creating a simple generic mapper

Assume we need to create a mapper for the files with the extension '.x'. Follow these very simple steps.

  • Open settings page
  • Open settings.json file
  • Insert generic pattern
      . . .
      "codemap.x": [
        {
          "pattern": "function \\w*",
          "icon": "function"
        }
      ],
      . . .

You can automate the procedure above by executing VSCode command "CodeMap: Create ...". It will create either a generic mapper for the currently opened file type. And you can also use "CodeMap: Edit ..." command to open an already existing mapper for editing.

The configuration above will trigger building the code tree with nodes corresponding to every line that starts from "function " word.

  • Trigger code map generation by pressing the refresh button:
    image

  • Use one of the online regex testing tools to refine your regex expression to serve your specific syntax.

The regex from the sample above was taken from the PowerShell syntax mapper:

"codemap.ps1": [
  {
    "pattern": "function \\w*",
    "clear": "function ",
    "suffix": "(...)",
    "icon": "function"
  }
],