
# Language features

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.

```ts
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

```ts
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:

```ts
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](/api/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:

```ts
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:

```ts
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:

```ts
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

```ts
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:

```ts
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`:

```ts
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

```ts
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](/api/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](/extensions/lsp) for wiring one up.
