Skip to content

Instantly share code, notes, and snippets.

@mwrouse
Last active March 5, 2024 05:05
Show Gist options
  • Star 47 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save mwrouse/05d8c11cd3872c19c684bd1904a2202e to your computer and use it in GitHub Desktop.
Save mwrouse/05d8c11cd3872c19c684bd1904a2202e to your computer and use it in GitHub Desktop.
Autocompletion for an object in the monaco editor
function ShowAutocompletion(obj) {
// Disable default autocompletion for javascript
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ noLib: true });
// Helper function to return the monaco completion item type of a thing
function getType(thing, isMember) {
isMember = (isMember == undefined) ? (typeof isMember == "boolean") ? isMember : false : false; // Give isMember a default value of false
switch ((typeof thing).toLowerCase()) {
case "object":
return monaco.languages.CompletionItemKind.Class;
case "function":
return (isMember) ? monaco.languages.CompletionItemKind.Method : monaco.languages.CompletionItemKind.Function;
default:
return (isMember) ? monaco.languages.CompletionItemKind.Property : monaco.languages.CompletionItemKind.Variable;
}
}
// Register object that will return autocomplete items
monaco.languages.registerCompletionItemProvider('javascript', {
// Run this function when the period or open parenthesis is typed (and anything after a space)
triggerCharacters: ['.', '('],
// Function to generate autocompletion results
provideCompletionItems: function(model, position, token) {
// Split everything the user has typed on the current line up at each space, and only look at the last word
var last_chars = model.getValueInRange({startLineNumber: position.lineNumber, startColumn: 0, endLineNumber: position.lineNumber, endColumn: position.column});
var words = last_chars.replace("\t", "").split(" ");
var active_typing = words[words.length - 1]; // What the user is currently typing (everything after the last space)
// This if statement adds support for autocomplete inside if statements and stuff
if (active_typing.includes('(')) {
active_typing = active_typing.split('(');
active_typing = active_typing[active_typing.length - 1];
}
// If the last character typed is a period then we need to look at member objects of the obj object
var is_member = active_typing.charAt(active_typing.length - 1) == ".";
// Array of autocompletion results
var result = [];
// Used for generic handling between member and non-member objects
var last_token = obj;
var prefix = '';
if (is_member) {
// Is a member, get a list of all members, and the prefix
var parents = active_typing.substring(0, active_typing.length - 1).split(".");
last_token = obj[parents[0]];
prefix = parents[0];
// Loop through all the parents the current one will have (to generate prefix)
for (var i = 1; i < parents.length; i++) {
var propToLookFor = parents[i];
// Support for arrays
var isPropAnArray = propToLookFor.charAt(propToLookFor.length - 1) == ']';
if (isPropAnArray)
propToLookFor = propToLookFor.split('[')[0];
if (last_token.hasOwnProperty(propToLookFor)) {
prefix += '.' + propToLookFor;
last_token = last_token[propToLookFor];
if (isPropAnArray && Array.isArray(last_token))
{
last_token = last_token[0];
}
} else {
// Not valid
return result;
}
}
prefix += '.';
}
// Array properties
if (Array.isArray(last_token))
last_token = { length: 0 };
// Get all the child properties of the last token
for (var prop in last_token) {
// Do not show properites that begin with "__"
if (last_token.hasOwnProperty(prop) && !prop.startsWith("__")) {
// Get the detail type (try-catch) incase object does not have prototype
var details = '';
try {
details = last_token[prop].__proto__.constructor.name;
} catch (e) {
details = typeof last_token[prop];
}
// Create completion object
var to_push = {
label: prefix + prop,
kind: getType(last_token[prop], is_member),
detail: details,
insertText: prop
};
// Change insertText and documentation for functions
if (to_push.detail.toLowerCase() == 'function') {
to_push.insertText += "(";
to_push.documentation = (last_token[prop].toString()).split("{")[0]; // Show function prototype in the documentation popup
}
// Add to final results
result.push(to_push);
}
}
return {
suggestions: result
};
}
});
}
// This simple example will show autocompletion for "Person" with the given properties of name, age
ShowAutocompletion({
Person: {
name: "",
age: 0
}
});
@ycoliveira
Copy link

@mwrouse I tried to copy and paste it on monaco playground (and creating a editor stance) but it's not working there. What am I doing wrong?

@mwrouse
Copy link
Author

mwrouse commented Jun 22, 2020

@ycoliveira It appears Monaco slightly changed something, or I forgot to update this early on in testing. I have updated the gist to reflect code that currently works in the Monaco playground.

@ycoliveira
Copy link

It's kinda working now. When I start typing "Person" it autocompletes and suggests "Person" but after "." it's not suggesting it properties "age" nor "name". Is it right? (sorry for being annoying, I'm new do monaco and this is the only info I got searching for autocompleting objects.)

@mwrouse
Copy link
Author

mwrouse commented Jun 22, 2020

@ycoliveira
Using the code below (whatever was in the monaco playground + this stuff here):

// The Monaco Editor can be easily created, given an
// empty container and an options literal.
// Two members of the literal are "value" and "language".
// The editor takes the full size of its container.

monaco.editor.create(document.getElementById("container"), {
	value: "function hello() {\n\talert('Hello world!');\n}",
	language: "javascript"
});

function ShowAutocompletion(obj) { 
    // Disable default autocompletion for javascript
    monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ noLib: true  });
  
    // Helper function to return the monaco completion item type of a thing
    function getType(thing, isMember) {
      isMember =  (isMember == undefined) ? (typeof isMember == "boolean") ? isMember : false : false; // Give isMember a default value of false
    
      switch ((typeof thing).toLowerCase()) { 
        case "object": 
          return monaco.languages.CompletionItemKind.Class;

        case "function": 
          return (isMember) ? monaco.languages.CompletionItemKind.Method : monaco.languages.CompletionItemKind.Function;

        default: 
          return (isMember) ? monaco.languages.CompletionItemKind.Property : monaco.languages.CompletionItemKind.Variable;
      }
    }
  
    // Register object that will return autocomplete items 
    monaco.languages.registerCompletionItemProvider('javascript', {
      // Run this function when the period or open parenthesis is typed (and anything after a space)
      triggerCharacters: ['.', '('],

      // Function to generate autocompletion results
      provideCompletionItems: function(model, position, token) {
        // Split everything the user has typed on the current line up at each space, and only look at the last word
        var last_chars = model.getValueInRange({startLineNumber: position.lineNumber, startColumn: 0, endLineNumber: position.lineNumber, endColumn: position.column});
        var words = last_chars.replace("\t", "").split(" "); 
        var active_typing = words[words.length - 1]; // What the user is currently typing (everything after the last space)
  
        // If the last character typed is a period then we need to look at member objects of the obj object 
        var is_member = active_typing.charAt(active_typing.length - 1) == ".";

        // Array of autocompletion results
        var result = [];
            
        // Used for generic handling between member and non-member objects
        var last_token = obj; 
        var prefix = ''; 
      
        if (is_member) { 
          // Is a member, get a list of all members, and the prefix
          var parents = active_typing.substring(0, active_typing.length - 1).split("."); 
          last_token = obj[parents[0]]; 
          prefix = parents[0]; 

          // Loop through all the parents the current one will have (to generate prefix)
          for (var i = 1; i < parents.length; i++) { 
            if (last_token.hasOwnProperty(parents[i])) { 
              prefix += '.' + parents[i]; 
              last_token = last_token[parents[i]];
            } else { 
              // Not valid
              return result;
            }
          }
      
          prefix += '.';
        }
      
        // Get all the child properties of the last token
        for (var prop in last_token) { 
          // Do not show properites that begin with "__"
          if (last_token.hasOwnProperty(prop) && !prop.startsWith("__")) { 
            // Get the detail type (try-catch) incase object does not have prototype 
            var details = ''; 
            try { 
              details = last_token[prop].__proto__.constructor.name; 
            } catch (e) { 
              details = typeof last_token[prop]; 
            }
            
            // Create completion object
            var to_push = {
              label: prefix + prop,
              kind: getType(last_token[prop], is_member), 
              detail: details,     
              insertText: prop
            };

            // Change insertText and documentation for functions
            if (to_push.detail.toLowerCase() == 'function') { 
              to_push.insertText += "(";
              to_push.documentation = (last_token[prop].toString()).split("{")[0]; // Show function prototype in the documentation popup
            }

            // Add to final results
            result.push(to_push);
          }
        }

        return {
            suggestions: result
        };
      }
  });
}

ShowAutocompletion({ 
  Person: { 
    name: "",
    age: 0
  }
});

When I run that and type "Person." in the playground I see this:
image

@ycoliveira
Copy link

Heey! Thanks! It worked and it's exactly what I was looking for. Just one doubt, inside if blocks the autocomplete works only for Person, it doesn't shows it children like "if(Person.age)" it completes Person and after dot the suggestions doesn't appear. Any idea what could it be?

@mwrouse
Copy link
Author

mwrouse commented Jun 22, 2020

@ycoliveira Yes, that appears to be a problem with my parsing. If you add a space between ( and Person all will be fine.

Typing comes through to these extensions as words, so if there is no space my code sees it as one word, which is not in the object provided to ShowAutocompletion.

@suren-atoyan
Copy link

@ycoliveira @mwrouse as an easy fix for that, you can write the following after line 31

if (activeTyping.includes('(')) {
  activeTyping = last(activeTyping.split('('));
}

@red-meadow
Copy link

Great example, thank you very much!

@jack-gaojz
Copy link

I try to use your codes, but always throw the exception as below:
image
If I removed this codes as below:

monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ noLib: true });

The exception will be disappear.
But if that, it will inherit the javascript variable or other features there. That is not my expected. I just want to display the speicified keyword for the suggestions and auto complete.
How to fix this issue? Anyone help me? Thanks so much. :)

@DaniShMyGit
Copy link

@mwrouse If Inside an object I have a property of type Array, How can I make it to work with [index] ?
Because currently the result with your code is array.0.property instead of array[0].property

@mwrouse
Copy link
Author

mwrouse commented Feb 16, 2022

@jack-gaojz I can't speak for your exact setup, you appear to be using tsWorker, and I'm not familiar with that. But using the code in the Monaco Editor Playground works just fine.

Actually, I am seeing that same error in the monaco playground, not sure what it is. But it still works fine there even with the error. Might be something with Monaco.

@mwrouse
Copy link
Author

mwrouse commented Feb 16, 2022

@DaniShMyGit can you please share what you are passing in to the ShowAutocompletion function?

@DaniShMyGit
Copy link

@mwrouse Thanks for your quick response.
This is what I'm passing to the function:
ShowAutocompletion({ input: { accountId: 0, array: [{ id: 0 }] } });

@DaniShMyGit
Copy link

And I'm getting: input.array.0

@DaniShMyGit
Copy link

Also, there are no suggestsion for my auto-complete results because they appeared as "any" . For example, if I type input.array, I expect to see in the suggestions 'length' function, but it isn't shown
(I removed monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ noLib: true }));

@mwrouse
Copy link
Author

mwrouse commented Feb 16, 2022

@DaniShMyGit I have updated the gist to allow for arrays, good find. Basically in the for-loop on line 56 I checked to see if it contained ']' to check if it was an array.

Also, if the input object is an array but you type "array." it won't show anything.

I also added some code by @suren-atoyan to allow for things to be in an if statement.

@DaniShMyGit
Copy link

@mwrouse you are a legend, it works!
Thanks!

@DaniShMyGit
Copy link

@mwrouse so basically we don't have any option how to tell the compiler that I have a property of type Array, right?
I mean for array functions completions like 'length' and such

@mwrouse
Copy link
Author

mwrouse commented Feb 16, 2022

@DaniShMyGit you can make the following changes:
Change the if statement at line 68 to this:

if (isPropAnArray && Array.isArray(last_token))
{
  last_token = last_token[0];
}

Then before the loop on line 84 add this:

if (Array.isArray(last_token))
{
   last_token = { length: 0};
}

That will show input.array.length

I'm not sure if I'm going to add that to the file. That adds a slipper slope on what this should cover.

But you can add as many properties on there as you want.

@DaniShMyGit
Copy link

@mwrouse Thank you very much!
You helped me a-lot!

@mwrouse
Copy link
Author

mwrouse commented Feb 16, 2022

@DaniShMyGit I changed my mind, I updated the gist to have those changes. But it still stands, if you want more properties on array, add it to last_token before that final for-loop.

You can also inject onto any properties there.

@DaniShMyGit
Copy link

@mwrouse I have another question, maybe you can advise me.
I'm sending this object to your function:
ShowAutocompletion( { output: { "getAccountByIdOut":{ "id": 1000, "name": "MyName" } } } )

So I'll be able in my monaco editor access my properties not using '.' but as json... for example:

`(inputStr) => {
const input = JSON.parse(inputStr);

output = {
 getAccount..	
}

return output;

}`

typing 'getAccount..' will auto-complete based on the schema I provided...

@mwrouse
Copy link
Author

mwrouse commented Feb 17, 2022

@DaniShMyGit I'm not sure I understand what you are trying to say, or what the question is.

@DaniShMyGit
Copy link

DaniShMyGit commented Feb 17, 2022

@mwrouse what I'm trying to do is to apply auto-completion inside {} block.
Instead of typing output.getAccountByIdOut.id I want to apply auto-completion like this:

output = { getAccountByIdOut: { id: } }

@mwrouse
Copy link
Author

mwrouse commented Feb 17, 2022

I see, that is not was this was designed for. From the link you added looks like it would be fundamentally different.

This was made when a friend of mine was working on a little game editor, he wanted to expose the editor to only the game objects available to the user. This allowed him to write the code that works, and pass it directly to the monaco editor and it would add auto-completion for it to the user.

The things this is auto completing should already exist in the context of whatever the editor code is going to be run in once written.

@DaniShMyGit
Copy link

DaniShMyGit commented Feb 17, 2022

I understand, thanks for your response!

@velikayikci
Copy link

var code = 'const person = ' + JSON.stringify(yourObject);
var libUri = 'ts:filename/facts.d.ts';
monaco.languages.typescript.typescriptDefaults.addExtraLib(code, libUri);
monaco.editor.createModel(code, 'typescript', monaco.Uri.parse(libUri));

:)

@Haigos
Copy link

Haigos commented Nov 8, 2022

Just came across this, exactly what I needed. Thx for sharing!

@kankk
Copy link

kankk commented Nov 14, 2023

var code = 'const person = ' + JSON.stringify(yourObject); var libUri = 'ts:filename/facts.d.ts'; monaco.languages.typescript.typescriptDefaults.addExtraLib(code, libUri); monaco.editor.createModel(code, 'typescript', monaco.Uri.parse(libUri));

:)

This is the recommended usage of monaco-editor, but it cannot solve the variable autocompletion on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment