Skip to content

Commit fa51a69

Browse files
committed
refactor: extract JSON parsing logic into reusable extractor classes
1 parent 9ed4331 commit fa51a69

12 files changed

Lines changed: 192 additions & 175 deletions

File tree

src/constants.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
11
export const PACKAGE_JSON_PATTERN = '**/package.json'
22

33
export const NPM_REGISTRY = 'https://registry.npmjs.org'
4-
5-
export const DEP_SECTIONS = [
6-
'dependencies',
7-
'devDependencies',
8-
'peerDependencies',
9-
'optionalDependencies',
10-
]

src/extractors/json.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { DependencyInfo, Extractor } from '#types/extractor'
2+
import type { Node } from 'jsonc-parser'
3+
import type { TextDocument } from 'vscode'
4+
import { createCachedParse } from '#utils/data'
5+
import { findNodeAtLocation, findNodeAtOffset, parseTree } from 'jsonc-parser'
6+
import { Range } from 'vscode'
7+
8+
const DEP_SECTIONS = [
9+
'dependencies',
10+
'devDependencies',
11+
'peerDependencies',
12+
'optionalDependencies',
13+
]
14+
15+
export class JsonExtractor implements Extractor<Node> {
16+
parse = createCachedParse(parseTree)
17+
18+
getNodeRange(doc: TextDocument, node: Node) {
19+
const start = doc.positionAt(node.offset + 1)
20+
const end = doc.positionAt(
21+
node.offset + node.length - 1,
22+
)
23+
24+
return new Range(start, end)
25+
}
26+
27+
inDependencySection(root: Node, node: Node) {
28+
return DEP_SECTIONS.some((section) => {
29+
const dep = findNodeAtLocation(root, [section])
30+
if (!dep || !dep.parent)
31+
return false
32+
33+
const { offset, length } = dep.parent.children![1]
34+
35+
return node.offset > offset && node.offset < offset + length
36+
})
37+
}
38+
39+
getDependenciesInfo(root: Node) {
40+
const info: DependencyInfo<Node>[] = []
41+
42+
DEP_SECTIONS.forEach((section) => {
43+
const node = findNodeAtLocation(root, [section])
44+
if (!node || !node.children)
45+
return
46+
47+
for (const dep of node.children) {
48+
const keyNode = dep.children?.[0]
49+
if (!keyNode || typeof keyNode.value !== 'string')
50+
continue
51+
52+
info.push({
53+
node: keyNode,
54+
name: keyNode.value,
55+
version: '',
56+
})
57+
}
58+
})
59+
60+
return info
61+
}
62+
63+
getDependencyInfoByOffset(root: Node, offset: number) {
64+
const node = findNodeAtOffset(root, offset)
65+
if (!node || node.type !== 'string' || !this.inDependencySection(root, node))
66+
return
67+
68+
return {
69+
node,
70+
name: node.parent!.children![0].value as string,
71+
version: node.value as string,
72+
}
73+
}
74+
}

src/index.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
1+
import { PACKAGE_JSON_PATTERN } from '#constants'
12
import { defineExtension } from 'reactive-vscode'
3+
import { languages } from 'vscode'
4+
import { JsonExtractor } from './extractors/json'
25
import { displayName, version } from './generated-meta'
3-
import { registerJsonCompletionItemProvider } from './providers/completion-item/json'
4-
import { registerJsonDocumentLinkProvider } from './providers/document-link/json'
6+
import { triggerChars, VersionCompletionItemProvider } from './providers/completion-item/version'
7+
import { NpmxDocumentLinkProvider } from './providers/document-link/npmx'
58
import { config, logger } from './state'
69

710
export const { activate, deactivate } = defineExtension((ctx) => {
811
logger.info(`${displayName} Activated, v${version}`)
912

13+
const jsonExtractor = new JsonExtractor()
14+
1015
ctx.subscriptions.push(
11-
registerJsonDocumentLinkProvider(),
16+
languages.registerDocumentLinkProvider(
17+
{ pattern: PACKAGE_JSON_PATTERN },
18+
new NpmxDocumentLinkProvider(jsonExtractor),
19+
),
1220
)
1321

1422
if (config.versionCompletion !== 'off') {
1523
ctx.subscriptions.push(
16-
registerJsonCompletionItemProvider(),
24+
languages.registerCompletionItemProvider(
25+
{ pattern: PACKAGE_JSON_PATTERN },
26+
new VersionCompletionItemProvider(jsonExtractor),
27+
...triggerChars,
28+
),
1729
)
1830
}
1931
})

src/providers/completion-item/json.ts

Lines changed: 0 additions & 39 deletions
This file was deleted.
Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { CompletionItemProvider, Position, Range, TextDocument } from 'vscode'
1+
import type { Extractor } from '#types/extractor'
2+
import type { CompletionItemProvider, Position, TextDocument } from 'vscode'
23
import { config } from '#state'
34
import { getPackageInfo } from '#utils/npm'
45
import { CompletionItem, CompletionItemKind } from 'vscode'
@@ -16,12 +17,20 @@ function getPrefix(v: string) {
1617

1718
export const triggerChars = ['.', '^', '~', ...Array.from({ length: 10 }).map((_, i) => `${i}`)]
1819

19-
export abstract class BaseCompletionItemProvider<T> implements CompletionItemProvider {
20-
abstract getDepInfo(document: TextDocument, position: Position): { node: T, name: string, version: string } | undefined
21-
abstract getReplacingRange(document: TextDocument, node: T): Range
20+
export class VersionCompletionItemProvider<T extends Extractor<any>> implements CompletionItemProvider {
21+
extractor: T
22+
23+
constructor(extractor: T) {
24+
this.extractor = extractor
25+
}
2226

2327
async provideCompletionItems(document: TextDocument, position: Position) {
24-
const info = this.getDepInfo(document, position)
28+
const root = this.extractor.parse(document)
29+
if (!root)
30+
return
31+
32+
const offset = document.offsetAt(position)
33+
const info = this.extractor.getDependencyInfoByOffset(root, offset)
2534
if (!info)
2635
return
2736

@@ -44,7 +53,7 @@ export abstract class BaseCompletionItemProvider<T> implements CompletionItemPro
4453
const text = `${prefix}${version}`
4554
const item = new CompletionItem(text, CompletionItemKind.Value)
4655

47-
item.range = this.getReplacingRange(document, node)
56+
item.range = this.extractor.getNodeRange(document, node)
4857
item.insertText = text
4958
if (tag)
5059
item.detail = tag

src/providers/document-link/base.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/providers/document-link/json.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { Extractor } from '#types/extractor'
2+
import type { DocumentLinkProvider, TextDocument } from 'vscode'
3+
import { DocumentLink, Uri } from 'vscode'
4+
5+
export class NpmxDocumentLinkProvider<T extends Extractor<any>> implements DocumentLinkProvider {
6+
extractor: T
7+
8+
constructor(extractor: T) {
9+
this.extractor = extractor
10+
}
11+
12+
provideDocumentLinks(document: TextDocument) {
13+
const root = this.extractor.parse(document)
14+
if (!root)
15+
return
16+
17+
return this.extractor.getDependenciesInfo(root).map(({ node, name }) => {
18+
const range = this.extractor.getNodeRange(document, node)
19+
20+
const uri = Uri.parse(`https://npmx.dev/package/${name}`)
21+
22+
return new DocumentLink(range, uri)
23+
})
24+
}
25+
}

src/types/extractor.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Range, TextDocument } from 'vscode'
2+
3+
export interface DependencyInfo<T> {
4+
node: T
5+
name: string
6+
version: string
7+
}
8+
9+
export interface Extractor<T> {
10+
parse: (document: TextDocument) => T | undefined
11+
12+
getNodeRange: (document: TextDocument, node: T) => Range
13+
14+
getDependenciesInfo: (root: T) => DependencyInfo<T>[]
15+
16+
getDependencyInfoByOffset: (root: T, offset: number) => DependencyInfo<T> | undefined
17+
}

src/utils/ast/json.ts

Lines changed: 0 additions & 67 deletions
This file was deleted.

0 commit comments

Comments
 (0)