Search code examples
visual-studio-codevscode-extensionsvscode-snippets

How to get all active snippets for a given language programmatically in extension?


I'm developing an extension for VS Code that let's you create CodeLenses by writing a comment like that:

// Code-Lens-Action insert-snippet <SNIPPET_NAME>

...that inserts code snippet with a given name above that line.

I got stuck at the point of validating the snippet name because I could not find a way to list all active snippets (so snippets defined by the user but also built-in snippets and those provided by other extensions. Simply put: all snippets that may show up in IntelliSense while coding in the same file as the CodeLens) for the language of current active file. I want to notify the user that they might have mistyped the snippet name.

I know about this SO answer and I'll probably be using the vscode.commands.executeCommand function but I need to know if the name is correct.

I tried this:

vscode.commands.executeCommand("editor.action.insertSnippet", { "name": "non-existent-name" })
.then(
    val => {
        vscode.window.showInformationMessage(val);
    },
    reason => {
        vscode.window.showErrorMessage(reason);
    }
);

But it does not call the onrejected callback. Besides that, this command does not take in any Position or Range objects (I think; I haven't found any documentation on it besides that SO answer), it just inserts the snippet at the current cursor position. So I need to move the cursor before insertion, so I need to know if the name is correct before insertion to not move the cursor if name is not correct.


Clarification update:

So I don't know where you plan on the cursor being on that line.

I don't want to use current cursor position at all. I assume you used it in your example because you had to pass something to executeCommand. But some positions in the document won't get all suggestions. After a bit of testing (done by pressing Ctrl + Space (shortcut for editor.action.triggerSuggest) a bunch of times while changing the cursor position) I came to a conclusion that for a position to return all snippets it needs to be at least 1 white space away from any text.

Does the Position at the beginning of the snippet name do what you want?

No, because the only snippet name present in the document will be inside a comment that is supposed to trigger the CodeLens and suggestions don't show up there. I need a position in empty space where all suggestions show up.

So the supplementary question is: how to reliably get a position that is in a "free" space of the current document in a language agnostic way?


Solution

  • Here is something you might be able to adapt to your needs. You can use the command vscode.executeCompletionItemProvider to return all "completions" relevent at a position.

    Those completions can then be filtered to only include snippets.

    Consider this code:

    // Code-Lens-Action insert-snippet wsNameFolder _cc
    

    where the snippet prefix is _cc and the snippet name is wsNameFolder.

    const doc = vscode.window.activeTextEditor.document;
    const thisUri = doc.uri;
    const pos = vscode.window.activeTextEditor.selection.active;
            
    /** @type { import("vscode").CompletionList } */  // if using javascript
    const completionList = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', thisUri, pos);
            
    let filteredList = [];
            
    const word = doc.getText(doc.getWordRangeAtPosition(pos));
    let good = undefined;
            
    if (completionList && completionList.items) {
      filteredList = completionList?.items.filter(item => item.kind === vscode.CompletionItemKind.Snippet);
            
        // does the filteredList (of snippets) contain one with a label that matches the "word" 
        good = filteredList.find(snippet => {
            if (typeof snippet.label !== 'string')
                return snippet.label.label === word;
        });
    }
    

    Triggering the executeCompletionItemProvider with the cursor immediately after or within _cc will get those snippets that match that prefix.

    But, triggering the executeCompletionItemProvider with the cursor immediately after or within wsNameFolder will still treat that name as the prefix to find matching snippets. Which likely won't work for you.

    Also. triggering the executeCompletionItemProvider in an empty area - not immediately after or in a word will work. You will get all suggestions that can then be filtered. If the cursor is before the name that works too.

    So I don't know where you plan on the cursor being on that line. If it will be after or in the snippet name you should be able to calculate the position immediately before the name and use that position in the executeCompletionItemProvider call.

    The following code gets the Position just before the snippet name. Using that you get all suggestions. Filter that for snippets and then find the one where the snippet.label.description matches the snippet name. It is a little confusing but snippet.label.description is really the name and not the snippet description.

    "wsNameFolder": {             // I assume you are using this
      "prefix": "_cc",
      "body": [
        "${WORKSPACE_NAME}"
      ],
      "description": "Workspace Name and Folder"   // not this
    },
    

    If you are really using the description above, you can find that too - that info is in the snippets returned.

    const doc = vscode.window.activeTextEditor.document;
    const thisUri = doc.uri;
    const pos = vscode.window.activeTextEditor.selection.active;
    
    const nameStart = doc.getWordRangeAtPosition(pos).start;
    
    // const completionList = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', thisUri, pos);
    const completionList = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', thisUri, nameStart);
    let filteredList = [];
            
    const word = doc.getText(doc.getWordRangeAtPosition(pos));
    let good = undefined;
    
    if (completionList && completionList.items) {
        filteredList = completionList?.items.filter(item => item.kind === vscode.CompletionItemKind.Snippet);
    
        // does the filteredList (of snippets) contain one with a label that matches the "word" 
        good = filteredList.find(snippet => {
            // return snippet.label.label === word;
            if (typeof snippet.label !== 'string')
                return snippet.label.description === word;
        });
        
    console.log(good);
    }
    

    Assuming there are any empty lines, this seems to return all the suggestions that can then be filtered to Snippets:

    const text = doc.getText();
    const emptyLines = [...text.matchAll(/^$/gm)];
    
    const firstEmptyLinePos = doc.positionAt(emptyLines[0].index);
    
    /** @type { import("vscode").CompletionList } */ // if using javascript
    const completionList = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', thisUri, firstEmptyLinePos);