Language features
Diagnostics and every provider in the languages namespace, from completion to code actions
Everything on this page is optional. The workbench draws whatever a registered provider answers and nothing more — a workbench with no language extensions still opens and edits every file, just without squiggles, hovers or a completion list.
Every registration takes a DocumentSelector saying which files it's for: a bare string is a
language id, an object narrows further, and an array of either registers for the union of them.
let selector: vscode.DocumentSelector;
selector = "typescript"; // every TypeScript file
selector = { language: "typescript", scheme: "file" }; // only ones the tree holds
selector = { pattern: "**/*.test.ts" }; // a glob against the path, regardless of language
selector = ["typescript", "typescriptreact"]; // the union of either shape#Diagnostics
const collection = vscode.languages.createDiagnosticCollection("no-console");
function lint(document: vscode.TextDocument) {
const found: vscode.Diagnostic[] = [];
for (let line = 0; line < document.lineCount; line++) {
const text = document.lineAt(line).text;
const at = text.indexOf("console.log");
if (at < 0) continue;
const range = new vscode.Range(line, at, line, at + "console.log".length);
const diagnostic = new vscode.Diagnostic(
range,
"Remove console.log before committing",
vscode.DiagnosticSeverity.Warning,
);
diagnostic.source = "no-console";
diagnostic.tags = [vscode.DiagnosticTag.Unnecessary];
found.push(diagnostic);
}
collection.set(document.uri, found);
}
context.subscriptions.push(
collection,
vscode.workspace.onDidOpenTextDocument(lint),
vscode.workspace.onDidChangeTextDocument((event) => lint(event.document)),
);A DiagnosticCollection is your extension's own publisher — set(uri, diagnostics) replaces
everything it said about that file, delete/clear take it back. Diagnostic carries a
range, a message and a severity (Error by default); tags fades a span as
Unnecessary or strikes it as Deprecated, and relatedInformation points at other places
that are part of the same problem — "first declared here" on a duplicate symbol. Diagnostics
surface as squiggles under the range, rows in the Problems panel, and a count in the status bar.
languages.getDiagnostics(uri) reads every publisher's answer for a file, including your own.
#Navigation
| Registration | What it answers |
|---|---|
registerDefinitionProvider | Where a name is declared — go to definition. |
registerTypeDefinitionProvider | Where a name's type is declared. |
registerImplementationProvider | What implements it. |
registerDeclarationProvider | Its declaration, for a language that separates the two. |
registerReferenceProvider | Every place a name is used. |
The first four are the same shape — provide…(document, position, token) answering a
Location or Location[] — differing only in which command reaches them.
Here's one for a tiny wiki convention — [[Term]] jumps to the # Term heading in the same
file:
context.subscriptions.push(
vscode.languages.registerDefinitionProvider("markdown", {
provideDefinition(document, position) {
const range = document.getWordRangeAtPosition(position, /\[\[[^\]]+\]\]/);
if (!range) return;
const term = document.getText(range).slice(2, -2);
for (let line = 0; line < document.lineCount; line++) {
const text = document.lineAt(line).text;
if (text.match(/^#+\s/) && text.includes(term)) {
return new vscode.Location(document.uri, new vscode.Position(line, 0));
}
}
},
}),
);registerReferenceProvider answers everywhere a name is used, all providers' answers at once —
it's the one gesture here whose result doesn't go into the editor. Register
codelet.window.registerLocationsView to draw the list yourself; codelet/extensions/search
already does, so a reference search lands in the Search view's rows without any extra wiring —
see Views.
#Information
| Registration | What it draws | Gesture |
|---|---|---|
registerHoverProvider | A tooltip over a span. | Pointer rests. |
registerDocumentSymbolProvider | The names in this file. | Palette @. |
registerWorkspaceSymbolProvider | The same across every file (no selector). | Palette #. |
registerInlayHintsProvider | Text written into the line — a type, a parameter name. | Drawn in the viewport. |
registerCodeLensProvider | A command drawn above a line — "3 references". | Click. |
registerDocumentLinkProvider | An underlined span that leads somewhere. | Modified click. |
registerDocumentHighlightProvider | Every other occurrence of the name under the caret. | Caret rests. |
registerColorProvider | A swatch beside a colour literal. | Click opens the picker. |
Continuing the wiki example, a hover that previews what a [[Term]] link points at:
context.subscriptions.push(
vscode.languages.registerHoverProvider("markdown", {
provideHover(document, position) {
const range = document.getWordRangeAtPosition(position, /\[\[[^\]]+\]\]/);
if (!range) return;
const term = document.getText(range).slice(2, -2);
return new vscode.Hover(new vscode.MarkdownString(`Jump to **${term}**`), range);
},
}),
);Hover.contents takes a MarkdownString — .appendText(), .appendMarkdown(),
.appendCodeblock() build one up — or a plain string.
#Authoring
| Registration | What it does |
|---|---|
registerCompletionItemProvider | Suggestions as the reader types, or on trigger characters. |
registerSignatureHelpProvider | Parameter hints while a call is being typed. |
registerCodeActionsProvider | Quick fixes and refactors offered at the caret. |
registerRenameProvider | F2 — a WorkspaceEdit across every file that uses the name. |
registerDocumentFormattingEditProvider | Reformat the whole file. |
registerDocumentRangeFormattingEditProvider | Reformat a selection only. |
registerFoldingRangeProvider | Narrows the editor's own indentation-based folding. |
registerDocumentDropEditProvider | Rewrite what a drop inserts — an image dropped as an embed. |
registerDocumentPasteEditProvider | Rewrite what a paste inserts — a URL pasted as a link. |
Finishing the wiki extension, completion after [[ suggests the file's own headings:
context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
"markdown",
{
provideCompletionItems(document) {
const items: vscode.CompletionItem[] = [];
for (let line = 0; line < document.lineCount; line++) {
const match = document.lineAt(line).text.match(/^#+\s+(.+)/);
if (match)
items.push(new vscode.CompletionItem(match[1], vscode.CompletionItemKind.Reference));
}
return items;
},
},
"[",
),
);The last argument to registerCompletionItemProvider (and registerSignatureHelpProvider) is
one or more trigger characters that open the list unprompted — typing a word opens one either
way. CompletionItem takes a label and an optional kind; set insertText where it differs
from the label, range for what it replaces, and additionalTextEdits for edits that should
land alongside it, like adding an import.
Back to the no-console linter for a quick fix that removes the offending line:
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider("typescript", {
provideCodeActions(document, range, actionContext) {
return actionContext.diagnostics
.filter((diagnostic) => diagnostic.source === "no-console")
.map((diagnostic) => {
const fix = new vscode.CodeAction("Remove console.log", vscode.CodeActionKind.QuickFix);
fix.edit = new vscode.WorkspaceEdit();
fix.edit.delete(
document.uri,
document.lineAt(diagnostic.range.start.line).rangeIncludingLineBreak,
);
fix.diagnostics = [diagnostic];
fix.isPreferred = true;
return fix;
});
},
}),
);CodeActionContext.diagnostics is already filtered to the caret's range, so there's no need to
re-scan the file. A CodeAction needs an edit, a command, or both — a bare Command is also
accepted where an action is only something to run. CodeActionKind (QuickFix, Refactor,
RefactorExtract, Source, SourceOrganizeImports, …) decides row order, preferred and
quick-fix rows first.
#Language ids
console.log(await vscode.languages.getLanguages()); // every id codelet knows
const document = await vscode.workspace.openTextDocument(vscode.Uri.file("/.eslintrc.json"));
await vscode.languages.setTextDocumentLanguage(document, "jsonc");getLanguages() lists every language id; setTextDocumentLanguage overrides what a file is
treated as regardless of its name, and re-reads the document under the new id. A manifest can add
languages codelet doesn't already know:
contributes: {
languages: [{ id: "gleam", extensions: [".gleam"], aliases: ["Gleam"] }],
},This buys a languageId for selectors, a label in pickers and the footer — not highlighting,
which is a separate grammar codelet either has or doesn't. A handful of free functions answer the
same table without a document in hand, exported directly from codelet/extensions:
import { languageIdOf, hasLanguage, labelOf, languageIds } from "codelet/extensions";
languageIdOf("tsconfig.json"); // "jsonc" — whole names beat extensions
hasLanguage("gleam"); // false unless a manifest contributed it
labelOf("typescript"); // "TypeScript"
languageIds(); // every built-in id#Language status items
const status = vscode.languages.createLanguageStatusItem("myLang.status", "typescript");
status.text = "$(check) MyLang 2.0";
status.detail = "Project loaded";A LanguageStatusItem shows in the status bar only while the active file matches its selector
— nearer a StatusBarItem than a provider, since there's nothing to ask, only something to say.
It shows from the moment it's created; take it away with dispose() or by narrowing the
selector to nothing.
For the full member list and everything narrowed or left out, see Full reference.
#Have a language server already?
Every provider above can also be answered by a real language server over LSP instead of
hand-written functions. codelet/extensions/lsp is the client; see
Lsp for wiring one up.