Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/soft-cups-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"env-spec-language": minor
---

Add IntelliSense, inline diagnostics, and docs demos for the VS Code extension.
9 changes: 5 additions & 4 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
"configurations": [
{
// this launches the vscode plugin debug environment
// TODO: ideally this would live within the vscode extension folder?
"name": "Launch Extension: @env-spec language",
"type": "extensionHost",
"request": "launch",
"preLaunchTask": "watch:vscode-plugin",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/packages/vscode-plugin",
"--disable-extensions",
"${workspaceFolder}/packages/vscode-plugin", // using our example repo as the folder to open
],
"outFiles": ["${workspaceFolder}/packages/vscode-plugin/**/*.js"]
},
],
"outFiles": ["${workspaceFolder}/packages/vscode-plugin/dist/**/*.js"],
"sourceMaps": true
}
]
}
31 changes: 31 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "watch:vscode-plugin",
"type": "shell",
"command": "bun run --filter env-spec-language dev",
"isBackground": true,
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": {
"owner": "typescript",
"fileLocation": "absolute",
"pattern": {
"regexp": "^$"
},
"background": {
"activeOnStart": true,
"beginsPattern": ".*",
"endsPattern": "Watching for changes"
}
},
"presentation": {
"reveal": "always",
"panel": "dedicated",
"clear": false
}
}
]
}
1,585 changes: 1,060 additions & 525 deletions bun.lock

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,62 @@ The extension is available on the [VS Code Marketplace](https://marketplace.visu
## Features

- Syntax highlighting
- IntelliSense for decorators, `@type` values, type options, resolver functions, and `$KEY` references
- Enum value completion for item values below `@type=enum(...)`
- Inline validation for invalid enum values, incompatible decorators, and obvious static `@type` mismatches
- Hover info for common @decorators
- Comment continuation - automatically continue comment blocks when you hit enter within one

## How to use this extension
## IntelliSense and diagnostics

### Decorators and built-in types

The extension suggests decorators and built-in `@type=` values directly inside comment blocks.

![Decorator and type completion](/env-spec-vscode/autocomplete.gif)

### Type option completions

Built-in types surface context-aware option completions like `email(normalize=...)`, `ip(version=..., normalize=...)`, and `url(prependHttps=...)`.

![Type option completion](/env-spec-vscode/types.gif)

### Email-specific option completions

Type-specific completions also work for focused cases like `email(normalize=...)`, with boolean choice values suggested inline.

![Email option completion](/env-spec-vscode/email.gif)

### Enum value completions

When an item is declared as `@type=enum(...)`, the allowed values are suggested directly on the item value line below.

![Enum value completion](/env-spec-vscode/enum.gif)

### Variable references

Typing `$` inside values and decorator expressions suggests config keys from the current file.

![Key reference completion](/env-spec-vscode/key.gif)

### Prefix-aware completions

Decorator and validation workflows also support prefix-related configuration scenarios while editing schema comments.

![Prefix-related completion or validation](/env-spec-vscode/prefix.gif)

### Invalid decorator combinations

Autocomplete filters out incompatible decorators like `@required` and `@optional`, and inline diagnostics catch invalid combinations if they still appear in the file.

![Incompatible decorator diagnostics](/env-spec-vscode/exclusive_options.gif)

### Inline validation

The extension also highlights obvious static validation issues, such as invalid enum values or incorrect `prependHttps` URL usage.

![Inline validation](/env-spec-vscode/prepend_https.gif)

## How to use this extension

The new @env-spec language mode should be enabled automatically for any .env and .env.* files, but you can always set it via the Language Mode selector in the bottom right of your editor.
53 changes: 53 additions & 0 deletions packages/vscode-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,63 @@ This new DSL builds upon the common `.env` format, adding support for JSDoc styl
## Features

- Syntax highlighting
- IntelliSense for decorators, `@type` values, type options, resolver functions, and `$KEY` references
- Enum value completion for item values below `@type=enum(...)`
- Inline validation for invalid enum values, incompatible decorators, and obvious static `@type` mismatches
- Hover info for common `@decorators`
- Better toggle-comment behavior (CMD+/), to enable/disable decorators within comment blocks
- Comment continuation (automatically continue comment blocks when you hit enter within one)

## IntelliSense and diagnostics

### Decorators and built-in types

Get completions for common item and root decorators, plus built-in `@type=` values while editing comment blocks.

![@env-spec decorator and type completion](./images/autocomplete.gif "Decorator and @type completion")

### Type option completions

Built-in types surface context-aware option completions like `email(normalize=...)`, `ip(version=..., normalize=...)`, and `url(prependHttps=...)`.

![@env-spec type option completion](./images/types.gif "Type option completion")

### Email-specific option completions

Type-specific completions also work for focused cases like `email(normalize=...)`, with boolean choice values suggested inline.

![@env-spec email option completion](./images/email.gif "Email option completion")

### Enum value completions

When an item is declared as `@type=enum(...)`, the allowed values are suggested directly on the item value line below.

![@env-spec enum value completion](./images/enum.gif "Enum value completion")

### Variable references

Typing `$` inside values and decorator expressions suggests config keys from the current file.

![@env-spec key reference completion](./images/key.gif "Key reference completion")

### Prefix-aware completions

Decorator and validation workflows also support prefix-related configuration scenarios while editing schema comments.

![@env-spec prefix behavior](./images/prefix.gif "Prefix-related completion or validation")

### Invalid decorator combinations

Autocomplete filters out incompatible decorators like `@required` and `@optional`, and inline diagnostics catch invalid combinations if they still appear in the file.

![@env-spec incompatible decorator diagnostics](./images/exclusive_options.gif "Incompatible decorator diagnostics")

### Inline validation

The extension also highlights obvious static validation issues, such as invalid enum values or incorrect `prependHttps` URL usage.

![@env-spec inline validation](./images/prepend_https.gif "Inline validation")

## How to use this extension

The new `@env-spec` language mode should be enabled automatically for any `.env` and `.env.*` files, but you can always set it via the Language Mode selector in the bottom right of your editor.
Expand Down
Binary file added packages/vscode-plugin/images/autocomplete.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/vscode-plugin/images/email.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/vscode-plugin/images/enum.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/vscode-plugin/images/key.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/vscode-plugin/images/prefix.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/vscode-plugin/images/prepend_https.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/vscode-plugin/images/types.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion packages/vscode-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@
"scripts": {
"dev": "tsup --watch",
"build": "tsup",
"test": "vscode-tmgrammar-test -g ./language/env-spec.tmLanguage.json ./test/grammar.test.txt",
"test": "vitest",
"test:grammar": "vscode-tmgrammar-test -g ./language/env-spec.tmLanguage.json ./test/grammar.test.txt",
"test:ci": "vitest --run && bun run test:grammar",
"prepack": "cp README.md README.marketplace.md && cp README.npm.md README.md",
"postpack": "mv README.marketplace.md README.md",
"package": "bun run build && vsce package -o env-spec-language.vsix --no-dependencies",
Expand All @@ -96,6 +98,7 @@
"@vscode/vsce": "^3.6.2",
"ovsx": "^0.10.6",
"tsup": "catalog:",
"vitest": "catalog:",
"vscode-tmgrammar-test": "^0.1.3"
},
"vsce": {
Expand Down
126 changes: 126 additions & 0 deletions packages/vscode-plugin/src/completion-core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type { DataTypeInfo, DecoratorInfo } from './intellisense-catalog';

type LineDocument = {
lineCount: number;
lineAt(line: number): { text: string };
};

const HEADER_SEPARATOR_PATTERN = /^\s*#\s*---+\s*$/;
const DECORATOR_PATTERN = /@([A-Za-z][\w-]*)/g;
const INCOMPATIBLE_DECORATORS = new Map<string, Set<string>>([
['required', new Set(['optional'])],
['optional', new Set(['required'])],
['sensitive', new Set(['public'])],
['public', new Set(['sensitive'])],
]);

function splitArgs(input: string) {
const parts: Array<string> = [];
let current = '';
let quote: '"' | '\'' | '' = '';
let depth = 0;

for (const char of input) {
if (quote) {
current += char;
if (char === quote) quote = '';
continue;
}

if (char === '"' || char === '\'') {
quote = char;
current += char;
continue;
}

if (char === '(') {
depth += 1;
current += char;
continue;
}

if (char === ')') {
depth = Math.max(depth - 1, 0);
current += char;
continue;
}

if (char === ',' && depth === 0) {
const value = current.trim();
if (value) parts.push(value);
current = '';
continue;
}

current += char;
}

const value = current.trim();
if (value) parts.push(value);
return parts;
}

export function isInHeader(document: LineDocument, lineNumber: number) {
for (let line = lineNumber; line >= 0; line -= 1) {
if (HEADER_SEPARATOR_PATTERN.test(document.lineAt(line).text)) return false;
}
return true;
}

export function getExistingDecoratorNames(
document: LineDocument,
lineNumber: number,
commentPrefix: string,
) {
const names = new Set<string>();

for (let line = lineNumber - 1; line >= 0; line -= 1) {
const text = document.lineAt(line).text.trim();
if (!text.startsWith('#')) break;
for (const match of text.matchAll(DECORATOR_PATTERN)) {
names.add(match[1]);
}
}

for (const match of commentPrefix.matchAll(DECORATOR_PATTERN)) {
names.add(match[1]);
}

return names;
}

export function filterAvailableDecorators(
decorators: Array<DecoratorInfo>,
existingDecoratorNames: Set<string>,
) {
return decorators.filter((decorator) => {
if (!decorator.isFunction && existingDecoratorNames.has(decorator.name)) return false;

const incompatible = INCOMPATIBLE_DECORATORS.get(decorator.name);
if (!incompatible) return true;

return ![...incompatible].some((name) => existingDecoratorNames.has(name));
});
}

export function splitEnumArgs(input: string) {
return splitArgs(input).map((value) => value.replace(/^['"]|['"]$/g, '').trim()).filter(Boolean);
}

export function getEnumValuesFromPrecedingComments(document: LineDocument, lineNumber: number) {
for (let line = lineNumber - 1; line >= 0; line -= 1) {
const text = document.lineAt(line).text.trim();
if (!text.startsWith('#')) break;

const match = text.match(/@type=enum\((.*)\)/);
if (match) return splitEnumArgs(match[1]);
}

return undefined;
}

export function getTypeOptionDataType(dataTypes: Array<DataTypeInfo>, commentPrefix: string) {
const match = commentPrefix.match(/(^|\s)@type=([A-Za-z][\w-]*)\([^#)]*$/);
if (!match) return undefined;
return dataTypes.find((dataType) => dataType.name === match[2]);
}
Loading