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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to the Official Dotenv VS Code extension will be documented

## [Unreleased](https://github.com/dotenvx/dotenv-vscode/compare/v1.5.2...master)

### Fixed

* Honor cloak color and icon settings in the Dotenv Editor, and folder-specific cloaking and secret-peeking settings in multi-root workspaces.

* Honor the auto-cloaking setting in the Dotenv Editor, including on load, settings changes, and tab switches (#139).

## [1.5.2](https://github.com/dotenvx/dotenv-vscode/compare/v1.5.1...v1.5.2) (2026-09-20)

### Changed
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@

<details><summary>Does it automatically cloak my secrets? <strong>Yes.</strong></summary><br>

Open a `.env` file and its values start masked.
By default, open a `.env` file and its values start masked.

Click **Toggle auto-cloaking** at the top of the file or run **Dotenv: Toggle auto-cloaking**
from the Command Palette to reveal or hide them.

Switching tabs hides them again in the Dotenv Editor.
Switching tabs hides them again in the Dotenv Editor when auto-cloaking is enabled.
Set `dotenv.enableAutocloaking` to `false` to keep values visible, including after
reopening files or switching tabs. Changes to this setting apply to open editors immediately.

&nbsp;

Expand Down Expand Up @@ -313,7 +315,7 @@ of overwriting the other edit.

<details><summary>Does the Dotenv Editor prevent secrets from flashing when I switch tabs? <strong>Yes.</strong></summary><br>

Values are masked before the editor becomes visible, and masking updates
With auto-cloaking enabled, values are masked before the editor becomes visible, and masking updates
locally as you type. Each view starts masked and hides values again on tab switches.

The minimap and general hover previews are disabled; encrypted values have their
Expand All @@ -328,6 +330,10 @@ own explicit **Decrypt value** action.
Fonts and basic editor settings follow VS Code. It uses Monaco, the editor
component behind VS Code.

`dotenv.cloakColor` and `dotenv.cloakIcon` customize the mask in both the Dotenv
Editor and the native text editor. Changes apply immediately. Folder settings
are respected in multi-root workspaces.

Custom theme token rules, arbitrary user keybindings, and other extensions don't
automatically carry into the embedded editor.

Expand Down
7 changes: 4 additions & 3 deletions lib/autocloaking.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,11 @@ function buildToggle (context) {
}

async function dotenvToggleAutocloaking () {
if (settings.autocloakingEnabled()) {
await settings.autocloakingOff()
const uri = vscode.window.activeTextEditor?.document.uri
if (settings.autocloakingEnabled(uri)) {
await settings.autocloakingOff(uri)
} else {
await settings.autocloakingOn()
await settings.autocloakingOn(uri)
}

return true
Expand Down
10 changes: 5 additions & 5 deletions lib/decorations.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const maskDecoration = vscode.window.createTextEditorDecorationType({
color: 'transparent'
})

const parse = function (sourceCode) {
const parse = function (sourceCode, uri) {
const arr = []
// const stillDisplayCommentsRegex = /((=|:)\s?".+"|(=|:)\s?'.+'|(=|:)\s?`.+`|(=|:).*\s?(\s#)?)/
const regex = /=.+/
Expand All @@ -30,7 +30,7 @@ const parse = function (sourceCode) {

// build line and column start and ends
arr.push({
maskedText: settings.cloakIcon().repeat(firstMatch.length),
maskedText: settings.cloakIcon(uri).repeat(firstMatch.length),
start: { line: lineIndex, column: startIndex },
end: { line: lineIndex, column: endIndex }
})
Expand All @@ -52,7 +52,7 @@ const applyDecorations = function (_ctx, editor, patches) {
range,
renderOptions: {
after: {
color: settings.cloakColor(),
color: settings.cloakColor(editor.document.uri),
contentText: `=${patch.maskedText}`
}
}
Expand All @@ -69,11 +69,11 @@ const decorate = (context, editor) => {

try {
yaml.decorate(editor)
if (editor.document.languageId !== 'dotenv' || !settings.autocloakingEnabled()) {
if (editor.document.languageId !== 'dotenv' || !settings.autocloakingEnabled(editor.document.uri)) {
applyDecorations(context, editor, [])
} else {
const sourceCode = editor.document.getText()
const patches = parse(sourceCode)
const patches = parse(sourceCode, editor.document.uri)
applyDecorations(context, editor, patches)
}
} catch (e) {
Expand Down
22 changes: 11 additions & 11 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ function envEntries (document) {
return Object.entries(envParsed(document))
}

function displayValue (value, revealed = settings.secretpeekingEnabled()) {
return (revealed ? value : _mask(value)) || '(empty)'
function displayValue (value, revealed, uri) {
return (revealed ? value : _mask(value, uri)) || '(empty)'
}

function valueDocumentation (values, batch, key, hoverRevealed) {
function valueDocumentation (values, batch, key, hoverRevealed, uri) {
const doc = new vscode.MarkdownString()
for (const { source, value } of values) {
doc.appendText(source)
doc.appendMarkdown('\n\n')
const revealed = batch?.request && batch.request.key === key && batch.request.source === source && batch.request.revealed
doc.appendText(batch ? ((revealed ? value : _mask(value)) || '(empty)') : displayValue(value, hoverRevealed))
doc.appendText(batch ? ((revealed ? value : _mask(value, uri)) || '(empty)') : displayValue(value, hoverRevealed, uri))
if (batch && value) completionReveal.append(doc, batch, key, source, revealed)
doc.appendMarkdown('\n\n---\n\n')
}
Expand All @@ -43,8 +43,8 @@ function valueDocumentation (values, batch, key, hoverRevealed) {
function valueHover (key, document, range) {
const values = module.exports.envValues(document).get(key)
if (!values) return new vscode.Hover(settings.missingText(), range)
const revealed = hoverReveal.begin(document, key, range, settings.secretpeekingEnabled())
const contents = [valueDocumentation(values, undefined, undefined, revealed)]
const revealed = hoverReveal.begin(document, key, range, settings.secretpeekingEnabled(document.uri))
const contents = [valueDocumentation(values, undefined, undefined, revealed, document.uri)]
const control = values.some(entry => entry.value) && hoverReveal.control(document, key, range, revealed)
if (control) contents.push(control)
return new vscode.Hover(contents, range)
Expand Down Expand Up @@ -82,8 +82,8 @@ function hover (language, document, position) {
}
}

function _mask (str) {
return settings.cloakIcon().repeat(str.length)
function _mask (str, uri) {
return settings.cloakIcon(uri).repeat(str.length)
}

function autocomplete (triggerCharacter, document, position) {
Expand All @@ -95,7 +95,7 @@ function autocomplete (triggerCharacter, document, position) {
const values = env[1]
const completionItemLabel = {
label: key,
detail: values.every(entry => entry.value === values[0].value) ? ` ${_mask(values[0].value) || '(empty)'}` : ` (${values.length} files)`,
detail: values.every(entry => entry.value === values[0].value) ? ` ${_mask(values[0].value, document.uri) || '(empty)'}` : ` (${values.length} files)`,
description: values.map(entry => entry.source).join(', ')
}
const item = new vscode.CompletionItem(completionItemLabel, vscode.CompletionItemKind.Variable)
Expand All @@ -104,12 +104,12 @@ function autocomplete (triggerCharacter, document, position) {
item.range = new vscode.Range(new vscode.Position(position.line, position.character - 1), position) // Picks up trigger character as prefix to fix the scoring it does when sorting
item.sortText = '0' // Make this the sortText so that any ENV variables will go to the top of the list above anything else

item.documentation = valueDocumentation(values, batch, key)
item.documentation = valueDocumentation(values, batch, key, undefined, document.uri)

return item
})
const valuesByKey = new Map(entries)
return completionReveal.track(batch, items, (key, refreshed) => valueDocumentation(valuesByKey.get(key), refreshed, key))
return completionReveal.track(batch, items, (key, refreshed) => valueDocumentation(valuesByKey.get(key), refreshed, key, undefined, document.uri))
}

module.exports.envEntries = envEntries
Expand Down
13 changes: 11 additions & 2 deletions lib/secure-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,25 @@ function resolveCustomTextEditor (document, panel, context) {
const config = vscode.workspace.getConfiguration('editor', document.uri)
return Object.fromEntries(['fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'tabSize', 'insertSpaces', 'wordWrap', 'renderWhitespace', 'cursorStyle', 'cursorBlinking', 'mouseWheelZoom'].map(key => [key, config.get(key)]))
}
const configuration = () => {
const config = vscode.workspace.getConfiguration('dotenv', document.uri)
return { autocloaking: config.get('enableAutocloaking', true), cloakColor: config.get('cloakColor', '#000000'), cloakIcon: config.get('cloakIcon', '█') }
}
const send = (extra = {}) => {
if (client && !disposed) webview.postMessage({ type: 'document', client, text: text(), version: document.version, filename: path.basename(document.uri.fsPath || ''), options: options(), ...extra })
if (client && !disposed) webview.postMessage({ type: 'document', client, text: text(), version: document.version, filename: path.basename(document.uri.fsPath || ''), options: options(), ...configuration(), ...extra })
}
const view = { panel, toggle: () => webview.postMessage({ type: 'toggle' }) }
views.add(view)
const subscriptions = [
vscode.workspace.onDidChangeTextDocument(event => {
if (event.document.uri.toString() === document.uri.toString() && !busy) send()
}),
vscode.workspace.onDidChangeConfiguration(event => { if (event.affectsConfiguration('editor', document.uri)) send() }),
vscode.workspace.onDidChangeConfiguration(event => {
if (['enableAutocloaking', 'cloakColor', 'cloakIcon'].some(key => event.affectsConfiguration(`dotenv.${key}`, document.uri)) && client && !disposed) {
webview.postMessage({ type: 'configuration', client, ...configuration() })
}
if (event.affectsConfiguration('editor', document.uri)) send()
}),
webview.onDidReceiveMessage(message => {
return (queue = queue.then(async () => {
if (!message || typeof message.client !== 'string') return
Expand Down
47 changes: 27 additions & 20 deletions lib/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,30 +21,32 @@ const editorTokenColorCustomizationsKey = 'editor.tokenColorCustomizations'

const toggleStateKey = 'autocloakingOverride'
let extensionState
let override
let overrides = []

function initialize (context) {
extensionState = context.globalState
override = extensionState.get(toggleStateKey)
const stored = extensionState.get(toggleStateKey)
overrides = Array.isArray(stored) ? stored : stored ? [stored] : []
}

async function setAutocloaking (enabled) {
override = { enabled, configured: !!userConfig().get(enableAutocloakingKey) }
await extensionState.update(toggleStateKey, override)
async function setAutocloaking (enabled, uri) {
const override = { enabled, configured: !!userConfig(uri).get(enableAutocloakingKey), scope: scope(uri) }
overrides = overrides.filter(item => item.scope !== override.scope).concat(override)
await extensionState.update(toggleStateKey, overrides)
return enabled
}

async function resetAutocloaking () {
override = undefined
overrides = []
await extensionState.update(toggleStateKey, undefined)
}

async function autocloakingOff () {
return setAutocloaking(false)
async function autocloakingOff (uri) {
return setAutocloaking(false, uri)
}

async function autocloakingOn () {
return setAutocloaking(true)
async function autocloakingOn (uri) {
return setAutocloaking(true, uri)
}

// Older releases persisted invisible syntax colors. Remove only their exact
Expand All @@ -69,26 +71,31 @@ async function removeLegacyMask () {
Object.keys(next).length ? next : undefined, vscode.ConfigurationTarget.Global)
}

function userConfig () {
return vscode.workspace.getConfiguration()
function scope (uri) {
return uri && (vscode.workspace.getWorkspaceFolder?.(uri)?.uri || uri).toString()
}

function userConfig (uri) {
return vscode.workspace.getConfiguration(undefined, uri)
}

// settings
function autocloakingEnabled () {
const configured = !!userConfig().get(enableAutocloakingKey)
function autocloakingEnabled (uri) {
const configured = !!userConfig(uri).get(enableAutocloakingKey)
const override = overrides.find(item => item.scope === scope(uri))
return override && override.configured === configured ? override.enabled : configured
}

function secretpeekingEnabled () {
return !!userConfig().get(enableSecretpeekingKey)
function secretpeekingEnabled (uri) {
return !!userConfig(uri).get(enableSecretpeekingKey)
}

function cloakColor () {
return userConfig().get(cloakColorKey)
function cloakColor (uri) {
return userConfig(uri).get(cloakColorKey)
}

function cloakIcon () {
return userConfig().get(cloakIconKey)
function cloakIcon (uri) {
return userConfig(uri).get(cloakIconKey)
}

function missingText () {
Expand Down
4 changes: 2 additions & 2 deletions lib/yaml-cloaking.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ function ranges (document) {
return values.ranges(document.getText(), config.get('yamlSections', values.defaultSections))
}
function decorate (editor) {
const patches = supported(editor.document) && settings.autocloakingEnabled() ? ranges(editor.document) : []
const patches = supported(editor.document) && settings.autocloakingEnabled(editor.document.uri) ? ranges(editor.document) : []
editor.setDecorations(mask, patches.map(({ start, end }) => ({
range: new vscode.Range(editor.document.positionAt(start), editor.document.positionAt(end)),
renderOptions: { after: { color: settings.cloakColor(), contentText: settings.cloakIcon().repeat(end - start) } }
renderOptions: { after: { color: settings.cloakColor(editor.document.uri), contentText: settings.cloakIcon(editor.document.uri).repeat(end - start) } }
})))
}
module.exports = { decorate, ranges, supported, dispose: mask }
41 changes: 40 additions & 1 deletion media/editor/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ self.MonacoEnvironment = { getWorker: () => new Worker(workerUrl) }
let editor
let encryptedHover
let filename
let cloakColor = '#000000'
let cloakIcon = '█'
let maskAppearance
let autocloaking = true
let masked = true
let applying = false
let previous = ''
Expand Down Expand Up @@ -76,8 +80,31 @@ function theme () {
})
monaco.editor.setTheme('dotenv')
}
// Paint a repeating glyph tile behind transparent text so the original model,
// cursor positions and copy/paste remain unchanged. Canvas treats settings as
// text/color values, never markup or CSS source.
function applyMaskAppearance () {
const font = editor.getOption(monaco.editor.EditorOption.fontInfo)
const key = JSON.stringify([cloakColor, cloakIcon, font.fontFamily, font.fontSize, font.fontWeight, font.spaceWidth, font.lineHeight, window.devicePixelRatio])
if (key === maskAppearance) return
maskAppearance = key
const scale = window.devicePixelRatio || 1
const canvas = document.createElement('canvas')
canvas.width = Math.max(1, Math.ceil(font.spaceWidth * scale))
canvas.height = Math.max(1, Math.ceil(font.lineHeight * scale))
const context = canvas.getContext('2d')
context.scale(scale, scale)
context.fillStyle = '#000000'
context.fillStyle = cloakColor
context.font = `${font.fontWeight} ${font.fontSize}px ${font.fontFamily}`
context.textBaseline = 'middle'
context.fillText(cloakIcon, 0, font.lineHeight / 2, font.spaceWidth)
container.style.setProperty('--dotenv-mask-image', `url("${canvas.toDataURL()}")`)
container.style.setProperty('--dotenv-mask-size', `${font.spaceWidth}px ${font.lineHeight}px`)
}
function applyMask () {
if (!editor) return
applyMaskAppearance()
const model = editor.getModel()
decorations.set(masked
? entries(model.getValue()).filter(entry => entry.maskEnd > entry.maskStart).map(entry => {
Expand All @@ -92,7 +119,7 @@ function applyMask () {
function conceal () {
encryptedHover?.hide()
container.classList.add('preparing')
masked = true
masked = autocloaking
applyMask()
if (editor && !document.hidden) {
editor.render(true)
Expand All @@ -109,7 +136,17 @@ function toggleMask () {
if (!document.hidden) container.classList.remove('preparing')
editor.focus()
}
function configure (message) {
cloakColor = typeof message.cloakColor === 'string' ? message.cloakColor : cloakColor
cloakIcon = typeof message.cloakIcon === 'string' ? message.cloakIcon : cloakIcon
if (editor) applyMaskAppearance()
const enabled = message.autocloaking !== false
if (enabled === autocloaking) return
autocloaking = enabled
conceal()
}
function updateDocument (message) {
configure(message)
filename = message.filename
encryptedHover?.hide()
if (pending.size) {
Expand Down Expand Up @@ -141,6 +178,7 @@ function updateDocument (message) {
ariaLabel: 'Dotenv source editor. Values are visually masked until revealed.'
})
decorations = editor.createDecorationsCollection()
editor.onDidChangeConfiguration(() => applyMaskAppearance())
encryptedHover = decryptHover(editor, request, () => ({ filename, version }))
editor.addAction({ id: 'dotenv.save', label: 'Save', keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS], run: () => request('save') })
for (const [id, keys] of [['undo', [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyZ]], ['redo', [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyZ, monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyY]]]) {
Expand Down Expand Up @@ -180,6 +218,7 @@ window.addEventListener('message', event => {
if (!message) return
if (message.type === 'toggle') { toggleMask(); return }
if (message.client !== client) return
if (message.type === 'configuration') configure(message)
if (message.type === 'document') updateDocument(message)
if (message.type === 'decrypted') encryptedHover?.receive(message)
if (message.type === 'ack') { pending.delete(message.id); version = message.version }
Expand Down
2 changes: 1 addition & 1 deletion media/editor/style.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading