Skip to content
Open
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
3 changes: 3 additions & 0 deletions extensions/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@

| Key | Description | Type | Default |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------------- |
| `npmx.catalog.display` | Display style for resolved catalog versions | `string` | `"decoration"` |
| `npmx.hover.enabled` | Enable hover information for packages | `boolean` | `true` |
| `npmx.completion.version` | Version completion behavior | `string` | `"provenance-only"` |
| `npmx.completion.excludePrerelease` | Exclude prerelease versions (alpha, beta, rc, canary, etc.) from completion suggestions | `boolean` | `true` |
Expand All @@ -76,6 +77,8 @@

<!-- configs -->

Set `npmx.catalog.display` to `"decoration"` (default) for resolved catalog versions at the end of the line, or `"inlay-hint"` to show them next to the catalog reference. Changes take effect without reloading the window. Inlay hints also require `editor.inlayHints.enabled` to be enabled.

## Ignore Diagnostics

`npmx` supports ignore lists for selected diagnostics.
Expand Down
14 changes: 14 additions & 0 deletions extensions/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@
"configuration": {
"title": "npmx",
"properties": {
"npmx.catalog.display": {
"type": "string",
"enum": [
"decoration",
"inlay-hint"
],
"enumDescriptions": [
"Show resolved catalog versions at the end of the line",
"Show resolved catalog versions next to the catalog reference using inlay hints"
],
"default": "decoration",
"description": "Display style for resolved catalog versions",
"markdownDescription": "Display style for resolved catalog versions. Inlay hints also require `#editor.inlayHints.enabled#` to be enabled."
},
"npmx.hover.enabled": {
"type": "boolean",
"default": true,
Expand Down
4 changes: 3 additions & 1 deletion extensions/vscode/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { LanguageClient, TransportKind } from '@volar/vscode/node'
import { DEPENDENCY_FILE_GLOB } from 'npmx-language-core/constants'
import { displayName, extensionId } from 'npmx-shared/meta'
import { Hover, MarkdownString } from 'vscode'
import { provideCatalogInlayHints } from './providers/catalog-display'

const SUPPORTED_LANGUAGES = [
'javascript',
Expand Down Expand Up @@ -39,6 +40,7 @@ export function launch(serverPath: string) {
{
middleware: {
...middleware,
provideInlayHints: provideCatalogInlayHints,
provideHover: async (document, position, token, next) => {
const hover = await next(document, position, token)
if (!hover)
Expand Down Expand Up @@ -70,7 +72,7 @@ export function launch(serverPath: string) {
initializationOptions: {
npmx: {
clientFeatures: {
catalogInlayHints: false,
catalogInlayHints: true,
iconStyle: 'codicon',
},
},
Expand Down
45 changes: 45 additions & 0 deletions extensions/vscode/src/providers/catalog-display.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { CancellationToken, InlayHint } from 'vscode'
import { createTextDocument } from 'jest-mock-vscode'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Position, Range, Uri, workspace } from 'vscode'
import { provideCatalogInlayHints } from './catalog-display'

describe('catalog display', () => {
const document = createTextDocument(Uri.file('/workspace/package.json'), '{"dependencies":{"vue":"catalog:"}}', 'json')
const range = new Range(0, 0, 0, document.getText().length)
const token: CancellationToken = {
isCancellationRequested: false,
onCancellationRequested: vi.fn(() => ({ dispose: vi.fn() })),
}
const hints: InlayHint[] = [{ position: new Position(0, 32), label: '^3.5.0' }]

afterEach(async () => {
await workspace.getConfiguration().update('npmx.catalog.display', undefined)
})

it('keeps hints hidden by default and after switching back to decorations', async () => {
const next = vi.fn(() => hints)
expect(await provideCatalogInlayHints(document, range, token, next)).toEqual([])
expect(next).not.toHaveBeenCalled()

await workspace.getConfiguration().update('npmx.catalog.display', 'inlay-hint')
expect(await provideCatalogInlayHints(document, range, token, next)).toEqual(hints)
expect(next).toHaveBeenCalledWith(document, range, token)

next.mockClear()
await workspace.getConfiguration().update('npmx.catalog.display', 'decoration')
expect(await provideCatalogInlayHints(document, range, token, next)).toEqual([])
expect(next).not.toHaveBeenCalled()
})

it('discards hints if the display mode changes while a request is pending', async () => {
await workspace.getConfiguration().update('npmx.catalog.display', 'inlay-hint')
const pending = Promise.withResolvers<InlayHint[]>()
const result = provideCatalogInlayHints(document, range, token, () => pending.promise)

await workspace.getConfiguration().update('npmx.catalog.display', 'decoration')
pending.resolve(hints)

expect(await result).toEqual([])
})
})
15 changes: 15 additions & 0 deletions extensions/vscode/src/providers/catalog-display.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Middleware } from '@volar/vscode'
import { configs } from 'npmx-shared/meta'
import { workspace } from 'vscode'

export function usesCatalogInlayHints(): boolean {
return workspace.getConfiguration().get(configs.catalogDisplay.key, configs.catalogDisplay.default) === 'inlay-hint'
}

export const provideCatalogInlayHints: NonNullable<Middleware['provideInlayHints']> = async (document, range, token, next) => {
if (!usesCatalogInlayHints())
return []

const hints = await next(document, range, token)
return usesCatalogInlayHints() ? hints : []
}
Comment on lines +9 to +15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

git diff dee1eb4b1585699d0c7ff4babd8cdcf5c13aa803 2217edb492a9c924b37a39ce7de54575517aebd1 -- extensions/vscode/src/providers/catalog-display.ts extensions/vscode/src/client.ts extensions/vscode/src/providers/decorators.ts
rg -n 'inlayHint|inlayHints|refresh|didChangeConfiguration' packages/language-service/src extensions/vscode/src

Repository: npmx-dev/vscode-npmx

Length of output: 5953


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- client.ts ---'
cat -n extensions/vscode/src/client.ts
printf '%s\n' '--- catalog language-service plugin ---'
cat -n packages/language-service/src/plugins/catalog.ts
printf '%s\n' '--- configuration and display references ---'
rg -n -C 4 'catalogDisplay|catalogInlayHints|inlayHintProvider|sendNotification|onDidChangeConfiguration|InlayHint' extensions packages --glob '*.{ts,tsx,json,md}'
printf '%s\n' '--- package manifests and lock versions ---'
rg -n -C 3 '"@volar/vscode"|"vscode-languageclient"|volar' extensions packages package.json pnpm-lock.yaml

Repository: npmx-dev/vscode-npmx

Length of output: 41635


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- all inlay-hint refresh/provider references ---'
rg -n -C 5 'inlayHint/refresh|InlayHintsProvider|onDidChangeInlayHints|provideInlayHints|workspace/inlayHint' . --glob '!pnpm-lock.yaml' --glob '!*.map'
printf '%s\n' '--- display configuration declarations and documentation ---'
rg -n -C 6 'catalog\.display|catalogDisplay|inlay-hint|decoration' extensions packages README.md docs .github --glob '*.{ts,tsx,json,md,yaml,yml}' 2>/dev/null
printf '%s\n' '--- display provider tests ---'
cat -n extensions/vscode/src/providers/catalog-display.test.ts
printf '%s\n' '--- decorator configuration refresh ---'
cat -n extensions/vscode/src/providers/decorators.ts
printf '%s\n' '--- language-server configuration handling ---'
cat -n packages/language-server/src/server.ts
cat -n packages/language-service/src/config.ts

Repository: npmx-dev/vscode-npmx

Length of output: 20197


🌐 Web query:

vscode-languageclient 9.0.1 inlay hints middleware onDidChangeInlayHints refresh implementation

💡 Result:

In `vscode-languageclient` **9.0.1**, `onDidChangeInlayHints` is the VS Code provider’s **change event**, not a middleware callback you implement. The inlay-hints middleware hook is for providing hints; the provider exposes `onDidChangeInlayHints` so VS Code knows when to request them again. The VS Code API describes it as an optional event signaling that the provider’s hints changed. ([code.visualstudio.com](https://code.visualstudio.com/api/references/vscode-api%5C?utm_source=openai))

So, to refresh hints, fire the provider’s event when your data changes—for example, with an `EventEmitter<void>`—and return its `.event` as `onDidChangeInlayHints`. The language client’s registered provider connects that event to VS Code’s refresh behavior.

I couldn’t verify the exact 9.0.1 implementation from an accessible version-pinned source; the repository search result points to the current branch, not necessarily 9.0.1. The API contract above is established, but details such as the exact provider wiring in 9.0.1 remain unconfirmed. ([github.com](https://github.com/microsoft/vscode-languageserver-node/blob/main/client/src/common/client.ts?utm_source=openai))

Citations:

- 1: https://code.visualstudio.com/api/references/vscode-api%5C?utm_source=openai
- 2: https://github.com/microsoft/vscode-languageserver-node/blob/main/client/src/common/client.ts?utm_source=openai

🌐 Web query:

site:github.com/microsoft/vscode-languageserver-node v9.0.1 client provideInlayHints onDidChangeInlayHints

💡 Result:

`provideInlayHints` and `onDidChangeInlayHints` are **VS Code extension API** provider members, not methods of `vscode-languageserver-node`’s language client. The repository’s current protocol source defines LSP inlay-hint support (`inlayHintProvider`), but that’s a separate server/protocol mechanism. ([github.com](https://github.com/microsoft/vscode-languageserver-node/blob/main/protocol/src/common/protocol.ts?utm_source=openai))

I couldn’t verify those exact names in the repository’s **v9.0.1** source: the available repository results point to current sources, not that version. The VS Code source shows an example provider using both names, but it is not evidence about `vscode-languageserver-node` v9.0.1. ([github.com](https://github.com/microsoft/vscode/blob/main/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts?utm_source=openai))

Citations:

- 1: https://github.com/microsoft/vscode-languageserver-node/blob/main/protocol/src/common/protocol.ts?utm_source=openai
- 2: https://github.com/microsoft/vscode/blob/main/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts?utm_source=openai

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- repository refresh symbols ---'
rg -n 'inlayHint/refresh|onDidChangeInlayHints|InlayHintsProvider|provideInlayHints|sendNotification|sendRequest' extensions packages --glob '*.{ts,tsx}'
printf '%s\n' '--- extension activation and provider setup ---'
cat -n extensions/vscode/src/index.ts
printf '%s\n' '--- exact current change ---'
git diff --unified=20 dee1eb4b1585699d0c7ff4babd8cdcf5c13aa803 2217edb492a9c924b37a39ce7de54575517aebd1 -- extensions/vscode/src/providers/catalog-display.ts extensions/vscode/src/providers/decorators.ts extensions/vscode/src/client.ts extensions/vscode/src/providers/catalog-display.test.ts
printf '%s\n' '--- installed/cache candidates for pinned client source ---'
find . -path '*vscode-languageclient*' -o -path '*@volar/vscode*' | head -80

Repository: npmx-dev/vscode-npmx

Length of output: 11714


🏁 Script executed:

#!/bin/bash
set -e
for url in \
  'https://raw.githubusercontent.com/microsoft/vscode-languageserver-node/9.0.1/client/src/common/inlayHint.ts' \
  'https://raw.githubusercontent.com/microsoft/vscode-languageserver-node/release/9.0.1/client/src/common/inlayHint.ts' \
  'https://raw.githubusercontent.com/microsoft/vscode-languageserver-node/9.0.1/client/src/common/client.ts' \
  'https://raw.githubusercontent.com/microsoft/vscode-languageserver-node/release/9.0.1/client/src/common/client.ts'
do
  echo "--- $url ---"
  curl -L --fail --silent --show-error "$url" | rg -n -C 8 'InlayHint|inlayHint|onDidChange|refresh' || true
done

Repository: npmx-dev/vscode-npmx

Length of output: 828


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
import io
import json
import tarfile
import urllib.request

meta = json.load(urllib.request.urlopen('https://registry.npmjs.org/vscode-languageclient/9.0.1'))
data = urllib.request.urlopen(meta['dist']['tarball']).read()
with tarfile.open(fileobj=io.BytesIO(data), mode='r:gz') as archive:
    names = [n for n in archive.getnames() if any(term in n.lower() for term in ('inlayhint', 'client.d.ts', 'inlay'))]
    for name in names:
        print(f'--- {name} ---')
        member = archive.getmember(name)
        text = archive.extractfile(member).read().decode('utf-8', errors='replace')
        for number, line in enumerate(text.splitlines(), 1):
            if any(term in line.lower() for term in ('inlayhint', 'onDidChange', 'refresh', 'provideInlay')):
                start = max(1, number - 8)
                end = min(len(text.splitlines()), number + 8)
                lines = text.splitlines()
                for i in range(start, end + 1):
                    print(f'{i}: {lines[i - 1]}')
                print()
PY

Repository: npmx-dev/vscode-npmx

Length of output: 2444


🏁 Script executed:

#!/bin/bash
set -e
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -k --fail --silent --show-error 'https://registry.npmjs.org/vscode-languageclient/9.0.1' > "$tmpdir/meta.json"
python3 - "$tmpdir/meta.json" "$tmpdir/package.tgz" <<'PY'
import json
import sys
import urllib.request

with open(sys.argv[1]) as f:
    meta = json.load(f)
url = meta['dist']['tarball']
request = urllib.request.Request(url)
with urllib.request.urlopen(request, context=__import__('ssl')._create_unverified_context()) as source, open(sys.argv[2], 'wb') as target:
    target.write(source.read())
PY
tar -xOf "$tmpdir/package.tgz" package/client.js 2>/dev/null | rg -n -C 12 'InlayHint|inlayHint|onDidChange|provideInlay|refresh' || true
tar -tzf "$tmpdir/package.tgz" | rg -i 'inlay|client\.(js|d\.ts)$'

Repository: npmx-dev/vscode-npmx

Length of output: 366


🏁 Script executed:

#!/bin/bash
set -e
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -k --fail --silent --show-error 'https://registry.npmjs.org/vscode-languageclient/9.0.1' > "$tmpdir/meta.json"
python3 - "$tmpdir/meta.json" "$tmpdir/package.tgz" <<'PY'
import json
import ssl
import sys
import urllib.request

with open(sys.argv[1]) as f:
    meta = json.load(f)
with urllib.request.urlopen(meta['dist']['tarball'], context=ssl._create_unverified_context()) as source, open(sys.argv[2], 'wb') as target:
    target.write(source.read())
PY
for member in package/lib/common/inlayHint.js package/lib/common/inlayHint.d.ts package/lib/common/client.js package/lib/common/client.d.ts; do
  echo "--- $member ---"
  tar -xOf "$tmpdir/package.tgz" "$member" | grep -n -C 12 -E 'InlayHint|inlayHint|onDidChange|provideInlay|refresh' || true
done

Repository: npmx-dev/vscode-npmx

Length of output: 29514


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- language server setup ---'
cat -n packages/language-server/src/server.ts
printf '%s\n' '--- language service configuration hook ---'
cat -n packages/language-service/src/config.ts
printf '%s\n' '--- workspace state and server event references ---'
rg -n -C 8 'onDidChangeConfiguration|didChangeConfiguration|connection\.|workspace/inlayHint|InlayHintRefresh|InlayHintRequest' packages/language-server packages/language-service --glob '*.ts'

Repository: npmx-dev/vscode-npmx

Length of output: 14340


Refresh inlay hints when npmx.catalog.display changes.

provideCatalogInlayHints filters only future requests. It does not invalidate the result already held by VS Code. When the setting changes, VS Code can keep empty hints after switching to "inlay-hint", or keep old hints after switching to "decoration". This violates the documented no-reload behaviour.

Send the standard InlayHintRefreshRequest when configuration changes. The pinned language client handles this request and fires its inlay-hint provider change event.

Suggested fix
 import type { ClientFeatures } from 'npmx-language-service/types'
+import { InlayHintRefreshRequest } from '@volar/language-server'
 import { createConnection, createServer, createSimpleProject } from '@volar/language-server/node'
 import { createNpmxLanguageServicePlugins } from 'npmx-language-service'
 import { DEFAULT_CLIENT_FEATURES } from 'npmx-language-service/types'
@@
   connection.onInitialized(() => {
     connection.console.info('npmx language server initialized')

     server.initialized()
   })
+  connection.onDidChangeConfiguration(() => {
+    void connection.sendRequest(InlayHintRefreshRequest.type)
+  })
   connection.onShutdown(server.shutdown)

88 changes: 50 additions & 38 deletions extensions/vscode/src/providers/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,64 @@
import type { BaseLanguageClient } from '@volar/vscode'
import type { DecorationOptions } from 'vscode'
import type { DecorationOptions, TextEditor } from 'vscode'
import { isPackageManifest } from 'npmx-language-core/utils'
import { useActiveTextEditor, useEditorDecorations, watch } from 'reactive-vscode'
import { Range } from 'vscode'
import { configs } from 'npmx-shared/meta'
import { useActiveTextEditor, useDisposable, useEditorDecorations, useEvent, watch } from 'reactive-vscode'
import { Range, window, workspace } from 'vscode'
import { logger } from '#state'
import { offsetRangeToRange } from '#utils/ast'
import { getResolvedDependencies } from '#utils/request'
import { usesCatalogInlayHints } from './catalog-display'

export function useDecorators(client: BaseLanguageClient) {
const activeEditor = useActiveTextEditor()

const { update } = useEditorDecorations(
activeEditor,
{
after: { color: 'rgba(136, 136, 136, 0.63)' },
},
async (editor) => {
const document = editor.document
if (!isPackageManifest(document.uri.path))
return []
logger.info(`[decorators] updating ${document.uri.path}`)

const dependencies = await getResolvedDependencies(client, document.uri)
if (!dependencies)
return []

const result: DecorationOptions[] = []

for (const dep of dependencies) {
if (dep.protocol !== 'catalog')
continue

const range = offsetRangeToRange(document, dep.specRange)
const line = range.end.line
const len = document.lineAt(line).text.length
result.push({
range: new Range(line, 0, line, len),
renderOptions: {
after: {
contentText: `\t\t ${dep.resolvedSpec}`,
},
const decorationType = useDisposable(window.createTextEditorDecorationType({
after: { color: 'rgba(136, 136, 136, 0.63)' },
}))
const { update } = useEditorDecorations(activeEditor, decorationType, getDecorations)

async function getDecorations(editor: TextEditor): Promise<DecorationOptions[]> {
if (usesCatalogInlayHints())
return []

const document = editor.document
if (!isPackageManifest(document.uri.path))
return []
logger.info(`[decorators] updating ${document.uri.path}`)

const dependencies = await getResolvedDependencies(client, document.uri)
if (!dependencies || usesCatalogInlayHints())
return []

const result: DecorationOptions[] = []

for (const dep of dependencies) {
if (dep.protocol !== 'catalog')
continue

const range = offsetRangeToRange(document, dep.specRange)
const line = range.end.line
const len = document.lineAt(line).text.length
result.push({
range: new Range(line, 0, line, len),
renderOptions: {
after: {
contentText: `\t\t ${dep.resolvedSpec}`,
},
})
}
},
})
}

return result
},
)
return result
}

watch(activeEditor, update)
useEvent(workspace.onDidChangeConfiguration, [async (event) => {
if (!event.affectsConfiguration(configs.catalogDisplay.key))
return

await Promise.all(window.visibleTextEditors.map(async (editor) => {
editor.setDecorations(decorationType, await getDecorations(editor))
}))
}])
}
4 changes: 4 additions & 0 deletions packages/language-service/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ function stringArrayConfig(value: unknown): string[] | undefined {
}

const configSpecs = {
'npmx.catalog.display': {
scopedKey: 'catalog.display',
validate: (value) => value === 'decoration' || value === 'inlay-hint' ? value : undefined,
},
'npmx.hover.enabled': {
scopedKey: 'hover.enabled',
validate: booleanConfig,
Expand Down
Loading