diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a7f10..826f1b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,15 @@ All notable changes to the Official Dotenv VS Code extension will be documented in this file. -## [Unreleased](https://github.com/dotenvx/dotenv-vscode/compare/v1.5.6...master) +## [Unreleased](https://github.com/dotenvx/dotenv-vscode/compare/v1.5.7...master) -## [1.5.6](https://github.com/dotenvx/dotenv-vscode/compare/v1.5.4...v1.5.6) (2026-09-23) +## [1.5.7](https://github.com/dotenvx/dotenv-vscode/compare/v1.5.6...v1.5.7) (2026-09-23) + +### Changed + +* Respect secret-peeking global setting when off. + +## [1.5.6](https://github.com/dotenvx/dotenv-vscode/compare/v1.5.5...v1.5.6) (2026-09-23) ### Changed @@ -177,6 +183,8 @@ All notable changes to the Official Dotenv VS Code extension will be documented ### Fixed +* Disable dotenv hovers and expanded autocomplete value details when secret peeking is turned off, while keeping autocomplete suggestions available. + * Reverted code causing autocloaking to fail [#93](https://github.com/dotenvx/dotenv-vscode/pull/93) ## 0.24.0 diff --git a/README.md b/README.md index 248fca6..20a1541 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,9 @@ secret-peeking setting stays unchanged. Hover over a reference such as `process.env.SECRET_KEY` or `ENV["SECRET_KEY"]`. The popup shows the source file and lets you **Reveal value** or **Hide value**. -New hovers follow your `dotenv.enableSecretpeeking` setting. +Uncheck **Dotenv: Enable Secretpeeking** to disable dotenv hovers and in-code +Reveal actions, including the expanded value details in autocomplete. Completion +suggestions remain available with masked values.   diff --git a/lib/completion-reveal.js b/lib/completion-reveal.js index ae4c97e..3a39b31 100644 --- a/lib/completion-reveal.js +++ b/lib/completion-reveal.js @@ -1,5 +1,6 @@ const vscode = require('vscode') const crypto = require('crypto') +const settings = require('./settings') const command = 'dotenv.toggleCompletionValue' const links = new Map() const batches = new WeakMap() @@ -69,11 +70,13 @@ function run (context) { const editor = vscode.window.activeTextEditor if (running || !request || request.expires < Date.now() || !editor || !matches(request.batch, editor.document, editor.selection.active)) return links.delete(id) + if (!settings.secretpeekingEnabled(editor.document.uri)) return running = true try { await vscode.commands.executeCommand('hideSuggestWidget') const active = vscode.window.activeTextEditor if (!active || !matches(request.batch, active.document, active.selection.active)) return + if (!settings.secretpeekingEnabled(active.document.uri)) return pending = request await vscode.commands.executeCommand('editor.action.triggerSuggest') } finally { diff --git a/lib/helpers.js b/lib/helpers.js index fb576c2..e168a5d 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -28,19 +28,22 @@ function displayValue (value, revealed, uri) { } function valueDocumentation (values, batch, key, hoverRevealed, uri) { + const peeking = settings.secretpeekingEnabled(uri) + if (!peeking) return undefined 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 + const revealed = peeking && batch?.request && batch.request.key === key && batch.request.source === source && batch.request.revealed doc.appendText(batch ? ((revealed ? value : _mask(value, uri)) || '(empty)') : displayValue(value, hoverRevealed, uri)) - if (batch && value) completionReveal.append(doc, batch, key, source, revealed) + if (peeking && batch && value) completionReveal.append(doc, batch, key, source, revealed) doc.appendMarkdown('\n\n---\n\n') } return doc } function valueHover (key, document, range) { + if (!settings.secretpeekingEnabled(document.uri)) return undefined 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(document.uri)) @@ -51,6 +54,7 @@ function valueHover (key, document, range) { } function hover (language, document, position) { + if (!settings.secretpeekingEnabled(document.uri)) return undefined const regexDict = { javascript: /(?:process|import\.meta)\.env\.([A-Z]{1}[A-Z_0123456789]+)/, ruby: /ENV\[['"]([A-Z]{1}[A-Z_0123456789]+)['"]\]/, diff --git a/lib/hover-reveal.js b/lib/hover-reveal.js index 97ec2ce..57d6e0f 100644 --- a/lib/hover-reveal.js +++ b/lib/hover-reveal.js @@ -1,5 +1,6 @@ const vscode = require('vscode') const crypto = require('crypto') +const settings = require('./settings') const command = 'dotenv.toggleHoverValue' const links = new Map() let pending @@ -34,12 +35,14 @@ function run (context) { const editor = vscode.window.activeTextEditor if (running || !request || request.expires < Date.now() || !editor || !matches(request, editor.document, request.key, request.range)) return links.delete(id) + if (!settings.secretpeekingEnabled(editor.document.uri)) return running = true try { await vscode.commands.executeCommand('editor.action.hideHover') if (vscode.window.activeTextEditor !== editor || !matches(request, editor.document, request.key, request.range)) return // VS Code reopens hovers at the cursor, which may differ from the mouse position. editor.selection = new vscode.Selection(request.range.start, request.range.start) + if (!settings.secretpeekingEnabled(editor.document.uri)) return pending = request await vscode.commands.executeCommand('editor.action.showHover', { focus: 'autoFocusImmediately' }) } finally { diff --git a/test/suite/lib/completion-reveal.test.js b/test/suite/lib/completion-reveal.test.js index 3e03a32..d478b37 100644 --- a/test/suite/lib/completion-reveal.test.js +++ b/test/suite/lib/completion-reveal.test.js @@ -33,8 +33,8 @@ function fixture () { }) return module.exports } - const reveal = load('completion-reveal.js', { vscode: fakeVscode }) const settings = { cloakIcon: () => '█', secretpeekingEnabled: () => true } + const reveal = load('completion-reveal.js', { vscode: fakeVscode, './settings': settings }) const helpers = load('helpers.js', { vscode: fakeVscode, './completion-reveal': reveal, @@ -69,6 +69,18 @@ function fixture () { } describe('completion popup reveal', () => { + it('keeps autocomplete suggestions but removes expanded details when peeking is disabled', async () => { + const f = fixture() + const token = f.tokens(f.original[0])[0] + f.settings.secretpeekingEnabled = () => false + const items = f.helpers.autocomplete('.', f.editor.document, f.editor.selection.active) + assert(items.length > 0) + assert.strictEqual(items[0].label.label, 'HELLO') + assert.strictEqual(items[0].insertText, '.HELLO') + assert.strictEqual(items[0].documentation, undefined) + assert.strictEqual(await f.click(token), undefined) + assert.strictEqual(f.triggers, 0) + }) it('reveals one source only, preserves insertion, and masks again on hide and fresh requests', async () => { const f = fixture() const original = f.original[0] diff --git a/test/suite/lib/dotnet.test.js b/test/suite/lib/dotnet.test.js index fad4703..53df60c 100644 --- a/test/suite/lib/dotnet.test.js +++ b/test/suite/lib/dotnet.test.js @@ -68,19 +68,22 @@ describe('.NET hover', () => { assert.strictEqual(dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf('UNKNOWN'))).contents[0], settings.missingText()) }) - it('handles lowercase keys, empty values, and short masked values', () => { + it('handles lowercase keys, empty values, and disabled peeking', () => { const originalParse = helpers.envValues const originalPeeking = settings.secretpeekingEnabled try { helpers.envValues = () => new Map(Object.entries({ lower_key: 'x', EMPTY: '' }).map(([key, value]) => [key, [{ value, source: '.env' }]])) settings.secretpeekingEnabled = () => false - for (const [key, expected] of [['lower_key', '█'], ['EMPTY', '(empty)']]) { + for (const [key, expected] of [['lower_key', 'x'], ['EMPTY', '(empty)']]) { const text = `Environment.GetEnvironmentVariable("${key}")` + settings.secretpeekingEnabled = () => false + assert.strictEqual(dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf(key))), undefined) + settings.secretpeekingEnabled = () => true assert(dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf(key))).contents[0].value.includes(new vscode.MarkdownString().appendText(expected).value)) } const text = 'Environment.GetEnvironmentVariable("HELLO")' helpers.envValues = () => new Map(Object.entries({ HELLO: 'World' }).map(([key, value]) => [key, [{ value, source: '.env' }]])) - assert(dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf('HELLO'))).contents[0].value.includes('█████')) + assert(dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf('HELLO'))).contents[0].value.includes('World')) helpers.envValues = () => new Map() assert.strictEqual(dotnet.hover.provideHover(document(text), new vscode.Position(0, text.indexOf('HELLO'))).contents[0], settings.missingText()) } finally { diff --git a/test/suite/lib/env-discovery-providers.test.js b/test/suite/lib/env-discovery-providers.test.js index 4199389..b0d898b 100644 --- a/test/suite/lib/env-discovery-providers.test.js +++ b/test/suite/lib/env-discovery-providers.test.js @@ -69,27 +69,23 @@ describe('dotenv discovery through language providers', () => { }) } - it('masks every conflicting value when secret peeking is disabled', async () => { + it('keeps suggestions without value details when secret peeking is disabled', async () => { const original = settings.secretpeekingEnabled try { settings.secretpeekingEnabled = () => false const uri = await write('app/src/masked.js', 'process.env.') const document = await vscode.workspace.openTextDocument(uri) const item = helpers.autocomplete('.', document, new vscode.Position(0, 12)).find(item => item.label.label === 'DISCOVERY_KEY') - const hover = helpers.valueHover('DISCOVERY_KEY', document).contents[0] - for (const content of [item.documentation.value, hover.value]) { - assert(!content.includes('localvalue')) - assert(!content.includes('productionvalue')) - assert(!content.replace(/\[.*?\]\(command:[^)]*\)/g, '').includes('ue'), 'Must not reveal the last two characters') - assert(content.includes('█')) - assert(content.includes('.env.local')) - } + assert.strictEqual(helpers.valueHover('DISCOVERY_KEY', document), undefined) + assert.strictEqual(item.documentation, undefined) + assert(item.label.description.includes('.env.local')) + assert.strictEqual(item.insertText, '.DISCOVERY_KEY') } finally { settings.secretpeekingEnabled = original } }) - it('fully masks short and long values in completion labels, documentation and hover', async () => { + it('keeps short and long values masked in suggestions without peeking details', async () => { const originalPeeking = settings.secretpeekingEnabled const originalIcon = settings.cloakIcon try { @@ -102,9 +98,8 @@ describe('dotenv discovery through language providers', () => { const item = helpers.autocomplete('.', document, new vscode.Position(0, 12)).find(item => item.label.label === 'MASK_TEST') const mask = '█'.repeat(value.length) assert.strictEqual(item.label.detail, ` ${mask}`) - assert(item.documentation.value.includes(mask)) - assert(!item.documentation.value.replace(/\[.*?\]\(command:[^)]*\)/g, '').includes(value.slice(-2))) - assert(helpers.valueHover('MASK_TEST', document).contents[0].value.includes(mask)) + assert.strictEqual(item.documentation, undefined) + assert.strictEqual(helpers.valueHover('MASK_TEST', document), undefined) } settings.secretpeekingEnabled = () => true assert(helpers.valueHover('MASK_TEST', document).contents[0].value.includes('🌴secret')) diff --git a/test/suite/lib/env-literals.test.js b/test/suite/lib/env-literals.test.js index 4e999bd..e1c6350 100644 --- a/test/suite/lib/env-literals.test.js +++ b/test/suite/lib/env-literals.test.js @@ -41,14 +41,17 @@ for (const [language, provider, calls, unrelated] of [ assert.strictEqual(provider.completion.provideCompletionItems(document(text), new vscode.Position(0, text.length)), undefined) }) } - it('handles masking, empty values, missing keys and missing files', () => { + it('handles disabled peeking, empty values, missing keys and missing files', () => { const originalParse = helpers.envValues const originalPeeking = settings.secretpeekingEnabled try { settings.secretpeekingEnabled = () => false helpers.envValues = () => new Map(Object.entries({ lower_key: 'World', EMPTY: '' }).map(([key, value]) => [key, [{ value, source: '.env' }]])) - for (const [key, value] of [['lower_key', '█████'], ['EMPTY', '(empty)'], ['UNKNOWN', settings.missingText()]]) { + for (const [key, value] of [['lower_key', 'World'], ['EMPTY', '(empty)'], ['UNKNOWN', settings.missingText()]]) { const text = expression(calls[0], `"${key}"`) + settings.secretpeekingEnabled = () => false + assert.strictEqual(provider.hover.provideHover(document(text), new vscode.Position(0, text.indexOf(key))), undefined) + settings.secretpeekingEnabled = () => true const content = provider.hover.provideHover(document(text), new vscode.Position(0, text.indexOf(key))).contents[0] assert((typeof content === 'string' ? content : content.value).includes(typeof content === 'string' ? value : new vscode.MarkdownString().appendText(value).value)) } diff --git a/test/suite/lib/hover-reveal.test.js b/test/suite/lib/hover-reveal.test.js index 83e3730..409725a 100644 --- a/test/suite/lib/hover-reveal.test.js +++ b/test/suite/lib/hover-reveal.test.js @@ -5,7 +5,7 @@ const path = require('path') const vm = require('vm') const vscode = require('vscode') -function fixture (defaultRevealed = false) { +function fixture (defaultRevealed = true) { const document = { uri: vscode.Uri.file('/test/hover.js'), version: 1 } const range = new vscode.Range(0, 12, 0, 17) const editor = { document, selection: new vscode.Selection(3, 0, 3, 0) } @@ -32,8 +32,8 @@ function fixture (defaultRevealed = false) { }) return module.exports } - const reveal = load('hover-reveal.js', { vscode: fakeVscode }) const settings = { cloakIcon: () => '█', secretpeekingEnabled: () => defaultRevealed, missingText: () => 'MISSING' } + const reveal = load('hover-reveal.js', { vscode: fakeVscode, './settings': settings }) const helpers = load('helpers.js', { vscode: fakeVscode, './hover-reveal': reveal, @@ -56,9 +56,19 @@ function fixture (defaultRevealed = false) { } describe('hover popup reveal', () => { + it('returns no hover when disabled and rejects previously issued reveal links', async () => { + const f = fixture() + const masked = await f.click(f.token(f.hover())) + const token = f.token(masked) + f.settings.secretpeekingEnabled = () => false + assert.strictEqual(f.hover(), undefined) + const before = f.triggers + assert.strictEqual(await f.click(token), undefined) + assert.strictEqual(f.triggers, before) + }) it('shows and hides a masked value without changing settings or fresh hovers', async () => { const f = fixture() - const masked = f.hover() + const masked = await f.click(f.token(f.hover())) assert(masked.contents[0].value.startsWith('.env\n\n')) assert(!masked.contents[0].value.includes('SECRET')) assert(masked.contents[0].value.includes('██████')) @@ -73,8 +83,8 @@ describe('hover popup reveal', () => { assert(hidden.contents[0].value.startsWith('.env\n\n')) assert(!hidden.contents[0].value.includes('SECRET')) assert(hidden.contents[0].value.includes('██████')) - assert.strictEqual(f.settings.secretpeekingEnabled(), false) - assert(f.hover().contents[0].value.includes('██████')) + assert.strictEqual(f.settings.secretpeekingEnabled(), true) + assert(f.hover().contents[0].value.includes('SECRET')) assert.strictEqual(hidden.contents[1].isTrusted.enabledCommands[0], 'dotenv.toggleHoverValue') assert.strictEqual(hidden.contents[1].isTrusted.enabledCommands.length, 1) }) @@ -96,7 +106,7 @@ describe('hover popup reveal', () => { const content = f.hover().contents[0].value assert(content.includes('.env\n\n')) assert(content.includes('.env.local\n\n')) - assert(!content.includes('SECRET')) + assert(content.includes('SECRET')) }) it('ignores unknown, reused, edited-document, and other-file links', async () => { @@ -135,7 +145,7 @@ it('refreshes the real VS Code hover after clicking Reveal value and Hide value' let provider try { await vscode.extensions.getExtension('dotenv.dotenv-vscode').activate() - settings.secretpeekingEnabled = () => false + settings.secretpeekingEnabled = () => true await vscode.workspace.fs.writeFile(uri, Buffer.from('HELLO')) const document = await vscode.workspace.openTextDocument(uri) await vscode.window.showTextDocument(document) @@ -147,7 +157,7 @@ it('refreshes the real VS Code hover after clicking Reveal value and Hide value' }) const token = hover => JSON.parse(decodeURIComponent(hover.contents[1].value.match(/\?([^)]*)/)[1]))[0] let current = helpers.valueHover('HELLO', document, range) - for (const expected of ['World', '█████']) { + for (const expected of ['█████', 'World']) { latest = undefined await vscode.commands.executeCommand('dotenv.toggleHoverValue', token(current)) const deadline = Date.now() + 3000 @@ -166,3 +176,34 @@ it('refreshes the real VS Code hover after clicking Reveal value and Hide value' await vscode.workspace.fs.delete(uri) } }) + +it('disables in-code hovers when the actual secret-peeking setting is unchecked', async function () { + // Settings writes and cold language-provider startup can exceed Mocha's 2s default on CI. + this.timeout(15000) + const uri = vscode.Uri.joinPath(vscode.workspace.workspaceFolders[0].uri, 'peeking-setting.js') + const config = vscode.workspace.getConfiguration('dotenv', uri) + const original = config.inspect('enableSecretpeeking').workspaceValue + try { + await vscode.extensions.getExtension('dotenv.dotenv-vscode').activate() + await vscode.workspace.fs.writeFile(uri, Buffer.from('process.env.HELLO\n')) + await vscode.workspace.openTextDocument(uri) + for (const enabled of [true, false, true]) { + await config.update('enableSecretpeeking', enabled, vscode.ConfigurationTarget.Workspace) + const hovers = await vscode.commands.executeCommand('vscode.executeHoverProvider', uri, new vscode.Position(0, 14)) + const content = hovers.flatMap(hover => hover.contents).map(item => item.value || '').join('\n') + assert.strictEqual(content.includes('.env'), enabled) + assert.strictEqual(content.includes('World'), enabled) + const completions = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', uri, new vscode.Position(0, 12)) + const item = completions.items.find(item => item.label.label === 'HELLO') + assert(item, 'Autocomplete must remain available') + assert.strictEqual(!!item.documentation, enabled) + if (!enabled) { + assert(!content.includes('█████')) + assert(!content.includes('Reveal value')) + } + } + } finally { + await config.update('enableSecretpeeking', original, vscode.ConfigurationTarget.Workspace) + await vscode.workspace.fs.delete(uri) + } +}) diff --git a/test/suite/lib/javascript-env.test.js b/test/suite/lib/javascript-env.test.js index 8c371d0..bd91d23 100644 --- a/test/suite/lib/javascript-env.test.js +++ b/test/suite/lib/javascript-env.test.js @@ -73,7 +73,7 @@ describe('named process env imports', () => { settings.secretpeekingEnabled = () => false const document = documentFor("import { env } from 'node:process'\nenv.HELLO") const hover = providers.javascriptHover.provideHover(document, new vscode.Position(1, 6)) - assert(hover.contents[0].value.includes('█████')) + assert.strictEqual(hover, undefined) } finally { settings.secretpeekingEnabled = original }