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
- Props (all optional):
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 alanguage ID
, and as shorthand for{language: 'ExampleLanguageId'}
- This is really just a broader type that encompasses
For either of these, when a
string
type is used forlanguage
, it should be alanguage ID
, such asmarkdown
ortypescript
. Here is a list of known IDs.
These filters to match files can come into play in a few different ways:
- When registering things
- Manually, by using the
match
method- This is really handy for checking if an open document matches a certain file type, file path, or even glob pattern
- A return value greater than zero indicates a match (or matches)
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
- This is also done automatically, if your plugin contributes a MarkdownIt plugin, with
- Markdown files :
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:
- 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
- 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
, andonDidCloseTextDocument
- Example:
code-actions-sample
- Main hooks are
- 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)
- 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
- Push to the same named collection created earlier via
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 yourCodeActionProvider
, 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
- A common piece to attach is a specific
- Create a
CodeActionProvider
, by implementingvscode.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, asCodeAction
objects:- Types: These can be fixes (
QuickFix
), empty (Empty
), etc. - seeCodeActionKind
enum / type - The actual action of the code action comes from either an attached
command
oredit
- To tie it back to the diagnostic, attach the
Diagnostic
object viamyAction.diagnostics = [myDiagnosticAlpha, myDiagnosticBravo, ...]
- Any diagnostics included in the array should be ones you would consider resolved if action is taken
- Types: These can be fixes (
- Since
CodeActionProvider
is abstract, you need to useimplements
and notextends
- Example:
code-actions-sample
- Register your
CodeActionProvider
viavscode.languages.registerCodeActionsProvider
- The return of this is a standard
Disposable
, so make sure to push tocontext.subscriptions
in your extension
- The return of this is a standard
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.
- Created with:
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'); });
- NA: This doesn't really exist outside of callback functions, where you are passed this as a builder, within closure:
- Applied through:
textEditor.edit()
- E.g.
vscode.window.activeTextEditor?.edit(myTextEditorEdit)
- E.g.
- Created with:
TextEdit
**- Create with:
new TextEdit()
- Applied through:
myWorkspaceEdit.set()
, and thenworkspace.applyEdit()
- Formatting provider,
provideDocumentFormattingEdits
- And a few other spots
- ** = 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)
- Create with:
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 likeWorkspaceEdit
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 byURI
- You pass a
- 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:
WorkspaceEdit
- Pass to
workspace.applyEdit
- Pass to
TextEditorEdit
- Use builder from
textEditor.edit
- Use builder from
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:
- Users can initiate formatting at-will, through command palette, menu, etc.
- You can push / execute a formatting operation by calling the built-in command of
vscode.executeFormatDocumentProvider
.
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));
- Robust, but verbose:
- How to log from an extension?
- For general logging, you can always use
console.x
methods. These will show up in thedebug console
while debugging your extension - For dedicated output, you can use an output channel
- For general logging, you can always use
- How do I maintain an extension-wide state / global variables?
- See: Docs - Data Storage
- You probably want either
ExtensionContext.globalState
, orExtensionContext.workspaceState
- How do I listen for a change to my settings / preferences, and do something if they change?