Joshua's Docs - VSCode Extension Development - Tips, Tricks, and Notes

Resources

What & Link Type
Official Docs - Extension API Official Docs (Homepage)
Docs: Extension Guidelines
- Also a good overview of different parts of VSCode
Cheatsheet / Guide
Docs: Extension Samples and Guides
- Samples Repo
- Guides and Samples Listing
Annotated Samples and Links to Guides
Docs: Built-in Commands Cheatsheet
Docs: Extensions Capabilities Overview
- Covers different types of extensions and capability sets
Cheatsheet
"Emoji Badges, anyone?" Guide: Takes you through the procedural part of developing and publishing a snippet extension

🤔 There is a readthedocs VSCode site that I've stumbled across, but at the moment, it doesn't seem accurate, and I'm not sure of its relation to the official docs. It could be a mirror of an older version...

Snippets

The official docs already have a wonderful guide on adding Snippets to extensions, as well as for writing the snippets themselves.

The only thing I'll add is a note on how to get better Intellisense / autocomplete / validation while working with your snippet files, with Snippet Schema Intellisense (see below):

Snippet Schema Intellisense

There are multiple ways that VSCode can resolve the schema associated with a JSON file in order to provide better Intellisense / validation, and a common trick is to add a $schema prop pointing to the schema to validate against. Unfortunately, since the schema doesn't allow for free-form additional properties, you can't just use "$schema": "vscode://schemas/snippets" in the JSON file.

However, you can get VSCode to validate your snippets JSON against the correct schema by carefully naming it; using the .code-snippets extension will trigger validation against vscode://schemas/global-snippets as defined here.

PS: This is even how VSCode approaches snippets internally.

Exporting Releases and Sideloading

To generate a shareable extension release (i.e. a .vsix file), you'll need to use the vsce (Visual Studio Code Extensions) CLI. If you just want a local packaged release, you can use vsce package to build it.

There is a docs page on packaging and sideloading.

Inspecting a Packaged VSIX Release

Since .vsix files are basically a modified version of the .zip archive format (hint is also given in 50 4B 03 04 magic bytes), you can inspect the contents of them with whatever your go-to archive tool for ZIPs is; for example, with PeaZip.

Publishing

Follow the official guide.

Matching Files

There are multiple ways in which you can have your VSCode extension only affect certain files. I'm new to VSCode extension development at the time of writing this, so I'm still figuring this out, but here is what I have so far:

There appear to be two main types of matchers:

  • DocumentFilter
    • Props (all optional): language, pattern, scheme
  • DocumentSelector
    • This is really just a broader type that encompasses DocumentFilter
    • Type: DocumentFilter | string | ReadonlyArray<DocumentFilter | string>
    • It allows passing just a string, when the string is a language ID, and as shorthand for {language: 'ExampleLanguageId'}

For either of these, when a string type is used for language, it should be a language ID, such as markdown or typescript. Here is a list of known IDs.

These filters to match files can come into play in a few different ways:


Markdown Extensions

📄 Main Doc: Markdown Extension Guide

Alternative approach - using custom WebView

Markdown Extensions - Hooks

Here are some tips and tricks for hooking into the Markdown Preview WebView, and Markdown files.

  • For activation events
    • Markdown files : onLanguage:markdown
    • Preview: onWebviewPanel:markdown.preview (example)
      • This is also done automatically, if your plugin contributes a MarkdownIt plugin, with "markdown.markdownItPlugins": true. If so, this applies:

        Extensions that contribute markdown-it plugins are activated lazily, when a Markdown preview is shown for the first time.

            Source

Using Markdown-it

If you are building a Markdown extension that modifies the webview, you might be looking at doing so by using the Markdown-it plugin hook (starting with enabling markdown.markdownItPlugins). The VSCode docs cover the basics of how to pass your plugin function to VSCode, but not how to write a Markdown-it plugin.

For writing a Markdown-it plugin from scratch and custom rules, I wrote a docs page on ways to accomplish this and different tips for Markdown-it usage.


Programmatic Languages Features

If you want to dynamically hook into the code that a user is writing, and provide different linting features, IntelliSense, or code formatting, you will need to use Programmatic Language Features.

Language features in an extension can be integrated via a direct VS Code API (such as createDiagnosticCollection) or by writing your own Language Server, using the MS Language Server Protocol (LSP).

Language Features - Diagnostics and Code Actions

There are multiple parts to working with diagnostics in VSCode.

Assuming that this is a non-LSP approach (using VSCode APIs instead), here are the general pieces you need to have implemented:

  1. Create a diagnostics collection, via createDiagnosticCollection, and push it to the context's subscriptions
    • This should be done on extension activation, and once you have done so, you can continue to pass the collection object around
    • Example: code-actions-sample
  2. Add document event listeners that track when editor text has possibly changed, and you need to re-check for issues
    • Main hooks are onDidChangeActiveTextEditor, onDidChangeTextDocument, and onDidCloseTextDocument
    • Example: code-actions-sample
  3. Implement the actual code that runs through the text of the active document(s) and checks for issues
    • Should be triggered by above document listeners
    • You could check line-by-line, with a for-loop and doc.lineAt(index) (like so), or retrieve full text and do your own parsing / iteration
    • When you find an issue, you need to track where in the document it occurred, and use that to create a vscode.Range instance, to use as part of the diagnostic creation process (see next step)
  4. When you find an issue you want to highlight and bring to user's attention, do so by pushing a new Diagnostic object, to the collection
    • Push to the same named collection created earlier via createDiagnosticCollection

This is all it takes to get basic error reporting working, but if you want to display actionable error messages (e.g. with quick fix options), you need to go a few steps further:

  • When pushing Diagnostic objects into the collection, make sure to attached metadata that you can then filter on inside your CodeActionProvider, to provide intelligent choices / UI to the user
    • A common piece to attach is a specific .code, which could correspond to a private enum you maintain
  • Create a CodeActionProvider, by implementing vscode.CodeActionProvider
    • This is the main place where VSCode is going to call your code to query for code actions it can display to the user
    • You need to return the possible actions from your provideCodeActions() method, as CodeAction objects:
      • Types: These can be fixes (QuickFix), empty (Empty), etc. - see CodeActionKind enum / type
      • The actual action of the code action comes from either an attached command or edit
      • To tie it back to the diagnostic, attach the Diagnostic object via myAction.diagnostics = [myDiagnosticAlpha, myDiagnosticBravo, ...]
        • Any diagnostics included in the array should be ones you would consider resolved if action is taken
    • Since CodeActionProvider is abstract, you need to use implements and not extends
    • Example: code-actions-sample
  • Register your CodeActionProvider via vscode.languages.registerCodeActionsProvider
    • The return of this is a standard Disposable, so make sure to push to context.subscriptions in your extension

Programmatic Text Editing

I'm not sure if this is the best headline, but I'm not 100% sure what to call this section. I'm hoping to have it cover the different approaches to having your extension initiating or preparing text editing / mutating of document(s) within VSCode.

There are a lot of different ways an extension can trigger a text update / deletion / insertion in VSCode, and there are different use-cases for each.

Building Edit Actions

  • WorkspaceEdit
    • Created with:
      • new WorkspaceEdit()
    • Applied through:
      • codeAction.edit()
        • Note: This is important, because this means you can attach these to codeAction objects and let the user initiate edit ops (a pull approach) instead of initiating from the extension itself (push approach)
      • workspace.applyEdit()
    • When applied, the changes grouped within the WorkspaceEdit instance are applied in the same order in which they were added.
  • TextEditorEdit
    • Created with:
      • NA: This doesn't really exist outside of callback functions, where you are passed this as a builder, within closure:
        const editor = vscode.window.activeTextEditor;
        editor?.edit(editBuilder => {
        	editBuilder.insert(editor.document.lineAt(0).range.start, 'Hello');
        });
    • Applied through:
  • TextEdit**
    • Create with:
      • new TextEdit()
    • Applied through:
    • ** = This is special: kind of a detached meta object that, by itself, contains no linkages to a document or editor, and really just contains the minimal instructions to perform a text edit (range, text, and operation type)

WorkspaceEdit vs TextEditorEdit

At first glance, it might not be clear what the difference is between WorkspaceEdit and TextEditorEdit; they both are used to group edit actions and cannot be used directly (both require passing to another method to execute).

What about TextEdit? Well, TextEdit is a whole other special thing, but can largely be ignored for this discussion since it is not really used directly, and instead would be passed with something like WorkspaceEdit anyways.

The main difference is that WorkspaceEdit is for, as the name would imply, the workspace level of organization, as opposed to a single document or editor (like TextEditorEdit is bound to). This makes it very powerful:

  • A single WorkspaceEdit can contain edits for multiple files
    • You pass a URI when adding edits to a WorkspaceEdit grouping, which ties it to the specific doc
    • However, with TextEditorEdit, your changes can only be applied to one doc / text editor at a time, and the link is established by which editor you call .edit() on, as opposed to by URI
  • The WorkspaceEdit object can contain edits for a workspace beyond just text edits - you can also group together file creation, file deletion, renames, and more.

Another key difference is in how they are instantiated and executed. The TextEditorEdit interface exists in a a very limited capacity; you cannot really directly instantiate an instance, and instead you are given an instance of it, as a builder within callback functions:

const editor = vscode.window.activeTextEditor;
editor?.edit((editBuilder: vscode.TextEditorEdit) => {
	// We only have access to editBuilder for duration of callback
	editBuilder.insert(editor.document.lineAt(0).range.start, 'Hello');
});

In opposition, WorkspaceEdit is a full-fledged class; you can instantiate a new instance with new vscode.WorkspaceEdit() whenever you want, even outside of when you need it, and they can be passed around as you see fit.

Finally, WorkspaceEdit objects can be attached to CodeAction objects, which means that they can be triggered by the user in certain situations, like applying QuickFix actions. The same is not true for TextEditorEdit.

Triggering Edit Actions

As there are multiple ways to actually have VSCode run your edit actions, you should think about which use-case best fits your needs.

First, in a imperative approach, you can directly call methods to execute an edit operation:

Another approach is more of a declarative, or pull approach. Attach WorkspaceEdit objects to CodeActions, via codeAction.edit, and then:

  • Users can run the edits at-will, if the action type supports it and they click the button

Finally, if your extension provides document formatting or that is a feature you are willing to implement, you can run bulk edits through DocumentFormattingEditProvider, and its provideDocumentFormattingEdits method. Once this is implemented, this exposes both pull and push interfaces:


Positions and Ranges

Converting Between Index / Offset Systems

VSCode uses its special Position object, to hold reference to a specific line and character position, as opposed to using zero-based index offsets relative to the entire document.

This can require translating between systems, as something like myRegExPattern.exec(textDocContents) is going to return matches that are offset-based.

There is a built-in utility fn to convert a zero-based index offset into a VSCode position: textDocument.positionAt(index) method. And once you have a start position, you can get the end by similarly using positionAt a second time, with positionAt(index + length), or by using startPos.translate(numRows, numChars).

🔀 For going the reverse route, and converting a VSCode based position into a zero-based index offset, there are similar utility functions, such as textDocument.offsetAt(position)

📄 For some insightful discussion into how VSCode handles positions and how it differs from other systems, see Issue #96 on the LSP repo.


Preferences / Settings

To listen for when a user changes settings that are specific to your extension, you can use a combination of a onDidChangeConfiguration event listener, with the exposed event.affectsConfiguration() method.

For example:

vscode.workspace.onDidChangeConfiguration((evt) => {
	if (evt.affectsConfiguration(PLUGIN_CONFIG_KEY)) {
		// Do something
	}
});

Misc. / FAQ

  • How to get the full Range of a document?
    • Robust, but verbose:
      const entireRange = new vscode.Range(doc.lineAt(0).range.start, doc.lineAt(doc.lineCount - 1).range.end);
    • Use validate trick (might have perf penalty):
      const entireRange = doc.validateRange(new vscode.Range(0,0,doc.lineCount,0));
  • How to log from an extension?
    • For general logging, you can always use console.x methods. These will show up in the debug console while debugging your extension
    • For dedicated output, you can use an output channel
  • How do I maintain an extension-wide state / global variables?
  • How do I listen for a change to my settings / preferences, and do something if they change?
Markdown Source Last Updated:
Sun Sep 05 2021 21:45:19 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Mon Jun 14 2021 10:21:18 GMT+0000 (Coordinated Universal Time)
© 2024 Joshua Tzucker, Built with Gatsby
Feedback