-
-
Notifications
You must be signed in to change notification settings - Fork 6
feat: implement version lens #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ea8f628
0f21aa0
1bbf963
a8f657e
a859f83
a918671
63508f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import type { Range as LspRange } from '@volar/vscode' | ||
| import { Position, Range, Uri, workspace, WorkspaceEdit } from 'vscode' | ||
|
|
||
| export async function replaceText(uri: string, range: LspRange, newText: string) { | ||
| const edit = new WorkspaceEdit() | ||
| edit.replace( | ||
| Uri.parse(uri), | ||
| new Range( | ||
| new Position(range.start.line, range.start.character), | ||
| new Position(range.end.line, range.end.character), | ||
| ), | ||
| newText, | ||
| ) | ||
| await workspace.applyEdit(edit) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| import type { CodeLens, LanguageServicePlugin, LanguageServicePluginInstance } from '@volar/language-service' | ||
| import type { OffsetRange } from 'npmx-language-core/types' | ||
| import type { IWorkspaceState } from '../types' | ||
| import type { UpgradeTier } from '../utils/version' | ||
| import { isDependencyFile } from 'npmx-language-core/utils' | ||
| import { REPLACE_TEXT_COMMAND } from 'npmx-shared/commands' | ||
| import { URI } from 'vscode-uri' | ||
| import { getConfig } from '../config' | ||
| import { formatUpgradeVersion, resolveUpgradeTiers } from '../utils/version' | ||
| import { resolveUpgrade } from './diagnostics/rules/upgrade' | ||
|
|
||
| interface LenData { | ||
| uri: string | ||
| specRange: OffsetRange | ||
| tier?: UpgradeTier | ||
| } | ||
|
|
||
| export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { | ||
| const UNKNOWN_COMMAND: CodeLens['command'] = { title: '$(question) unknown', command: '' } | ||
|
|
||
| return { | ||
| name: 'npmx-version-lens', | ||
| capabilities: { | ||
| codeLensProvider: { | ||
| resolveProvider: true, | ||
| }, | ||
| }, | ||
| create(context): LanguageServicePluginInstance { | ||
| async function resolveVersionLensCommand({ uri, specRange, tier }: LenData, range: CodeLens['range']): Promise<CodeLens['command']> { | ||
| const dependencies = await workspaceState.getResolvedDependencies(uri) | ||
| const dep = dependencies?.find( | ||
| (d) => d.specRange[0] === specRange[0] && d.specRange[1] === specRange[1], | ||
| ) | ||
| if (!dep) | ||
| return UNKNOWN_COMMAND | ||
|
|
||
| const pkg = await dep.packageInfo() | ||
| if (!pkg) | ||
| return UNKNOWN_COMMAND | ||
|
|
||
| const resolvedVersion = await dep.resolvedVersion() | ||
| if (!resolvedVersion) | ||
| return UNKNOWN_COMMAND | ||
|
|
||
| if (tier) { | ||
| const formatted = formatUpgradeVersion(dep, tier.version) | ||
| return { | ||
| title: `$(arrow-up) ${formatted} (${tier.type})`, | ||
| command: REPLACE_TEXT_COMMAND, | ||
| arguments: [uri, range, formatted], | ||
| } | ||
| } | ||
|
|
||
| const ignoreList = await getConfig(context, 'npmx.ignore.upgrade') | ||
| const targetVersion = resolveUpgrade(dep, pkg, resolvedVersion, ignoreList) | ||
| if (!targetVersion) | ||
| return { title: '$(check) latest', command: '' } | ||
|
|
||
| return { | ||
| title: `$(arrow-up) ${targetVersion}`, | ||
| command: REPLACE_TEXT_COMMAND, | ||
| arguments: [uri, range, targetVersion], | ||
| } | ||
|
Comment on lines
+54
to
+63
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't short-circuit the fallback path with Line 107 hides every dependency that reaches the non-tiered branch, so the fallback logic on Lines 54-63 never runs for those cases. That suppresses the fallback lens entirely and also hides the Suggested fix const dependencies = await workspaceState.getResolvedDependencies(document.uri)
if (!dependencies)
return []
const lenses: CodeLens[] = []
+ const hideWhenLatest = await getConfig(context, 'npmx.versionLens.hideWhenLatest')
+ const ignoreList = await getConfig(context, 'npmx.ignore.upgrade')
for (const dep of dependencies) {
if (dep.resolvedProtocol !== 'npm' || dep.category === 'peerDependencies')
continue
@@
- const hideWhenLatest = await getConfig(context, 'npmx.versionLens.hideWhenLatest')
- if (hideWhenLatest)
+ if (pkg && resolvedVersion && hideWhenLatest && !resolveUpgrade(dep, pkg, resolvedVersion, ignoreList))
continue
lenses.push({ range, data: baseData })
}Also applies to: 79-109 |
||
| } | ||
|
|
||
| return { | ||
| async provideCodeLenses(document): Promise<CodeLens[]> { | ||
| if (!await getConfig(context, 'npmx.versionLens.enabled')) | ||
| return [] | ||
|
|
||
| const uri = URI.parse(document.uri) | ||
| if (uri.scheme !== 'file' || !isDependencyFile(uri.path)) | ||
| return [] | ||
|
|
||
| const dependencies = await workspaceState.getResolvedDependencies(document.uri) | ||
| if (!dependencies) | ||
| return [] | ||
|
|
||
| const lenses: CodeLens[] = [] | ||
| const hideWhenLatest = await getConfig(context, 'npmx.versionLens.hideWhenLatest') | ||
|
|
||
| for (const dep of dependencies) { | ||
| if (dep.resolvedProtocol !== 'npm' || dep.category === 'peerDependencies') | ||
| continue | ||
|
|
||
| const range = { | ||
| start: document.positionAt(dep.specRange[0]), | ||
| end: document.positionAt(dep.specRange[1]), | ||
| } | ||
| const baseData: LenData = { uri: document.uri, specRange: dep.specRange } | ||
|
|
||
| const pkg = await dep.packageInfo() | ||
| const resolvedVersion = await dep.resolvedVersion() | ||
|
|
||
| if (pkg && resolvedVersion) { | ||
| const tiers = resolveUpgradeTiers(pkg, resolvedVersion) | ||
| if (tiers.length > 0) { | ||
| for (const tier of tiers) { | ||
| lenses.push({ | ||
| range, | ||
| data: { ...baseData, tier } satisfies LenData, | ||
| }) | ||
| } | ||
| continue | ||
| } | ||
| } | ||
|
|
||
| if (hideWhenLatest) | ||
| continue | ||
|
|
||
| lenses.push({ range, data: baseData }) | ||
| } | ||
|
|
||
| return lenses | ||
| }, | ||
|
|
||
| async resolveCodeLens(lens): Promise<CodeLens> { | ||
| const command = await resolveVersionLensCommand(lens.data as LenData, lens.range) | ||
| return { ...lens, command } | ||
| }, | ||
| } | ||
| }, | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import { displayName } from './meta' | ||
|
|
||
| export const ADD_TO_IGNORE_COMMAND = `${displayName}.addToIgnore` | ||
| export const REPLACE_TEXT_COMMAND = `${displayName}.replaceText` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n packages/language-service/src/plugins/version-lens.ts | head -150Repository: npmx-dev/vscode-npmx
Length of output: 5201
Remove the
as LenDatacast and validatelens.databefore destructuring.Line 118 uses an unchecked type cast that bypasses validation. If
lens.datais missing or malformed, the destructuring at line 29 will throw instead of degrading gracefully. Implement a validation function and guard the call:Suggested implementation
async resolveCodeLens(lens): Promise<CodeLens> { - const command = await resolveVersionLensCommand(lens.data as LenData, lens.range) + if (!isLenData(lens.data)) + return { ...lens, command: UNKNOWN_COMMAND } + const command = await resolveVersionLensCommand(lens.data, lens.range) return { ...lens, command } },This aligns with the coding guideline: "Avoid
astype casts—validate instead in TypeScript".