Skip to content

Commit 55154a0

Browse files
committed
feat(service): add mpx-css and mpx-document-link plugins
1 parent 3ed617f commit 55154a0

File tree

11 files changed

+277
-8
lines changed

11 files changed

+277
-8
lines changed

packages/language-core/src/virtualFile/computedEmbeddedCodes.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type * as ts from 'typescript'
22
import type { Mapping, VirtualCode } from '@volar/language-core'
33
import type { Code, MpxLanguagePluginReturn, Sfc, SfcBlock } from '../types'
4-
54
import { computed } from 'alien-signals'
65
import { toString } from 'muggle-string'
76
import { buildMappings } from '../utils/buildMappings'

packages/language-core/src/virtualFile/computedMpxSfc.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type * as ts from 'typescript'
22
import type { SFCParseResult } from '@vue/compiler-sfc'
33
import type { MpxLanguagePluginReturn } from '../types'
4-
54
import { computed } from 'alien-signals'
65

76
export function computedMpxSfc(

packages/language-core/src/virtualFile/computedSfc.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import type {
77
SfcBlock,
88
SfcBlockAttr,
99
} from '../types'
10-
1110
import { computed, pauseTracking, resumeTracking } from 'alien-signals'
1211
import { parseCssClassNames } from '../utils/parseCssClassNames'
1312
import { parseCssVars } from '../utils/parseCssVars'

packages/language-core/src/virtualFile/mpxFile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type * as ts from 'typescript'
22
import type { VirtualCode } from '@volar/language-core'
33
import type { MpxCompilerOptions, MpxLanguagePluginReturn } from '../types'
4-
54
import { computed, signal } from 'alien-signals'
65
import { allCodeFeatures } from '../plugins'
76
import { computedSfc } from './computedSfc'
87
import { computedMpxSfc } from './computedMpxSfc'
98
import { computedEmbeddedCodes } from './computedEmbeddedCodes'
9+
1010
export class MpxVirtualCode implements VirtualCode {
1111
id = 'main'
1212

packages/language-server/src/node.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import {
1818
import {
1919
LanguageService,
2020
createLanguageService,
21+
createMpxLanguageServicePlugins,
2122
createUriMap,
22-
getHybridModeLanguageServicePlugins,
2323
} from '@mpxjs/language-service'
2424

2525
const connection = createConnection()
@@ -109,7 +109,7 @@ connection.onInitialize(params => {
109109
simpleLs = undefined
110110
},
111111
},
112-
getHybridModeLanguageServicePlugins(
112+
createMpxLanguageServicePlugins(
113113
ts,
114114
options.typescript.tsserverRequestCommand
115115
? {

packages/language-service/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
"@volar/language-service": "catalog:",
2626
"@volar/typescript": "catalog:",
2727
"@vue/compiler-dom": "catalog:",
28+
"volar-service-css": "0.0.64",
2829
"volar-service-emmet": "0.0.63",
2930
"volar-service-html": "0.0.64",
3031
"volar-service-json": "0.0.64",
3132
"volar-service-typescript": "0.0.64",
33+
"vscode-css-languageservice": "^6.3.6",
3234
"vscode-html-languageservice": "^5.5.0",
3335
"vscode-uri": "catalog:"
3436
},

packages/language-service/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ import { create as createTypeScriptDocCommentTemplatePlugin } from 'volar-servic
1111
import { create as creatempxDocumentHighlightsPlugin } from './plugins/mpx-document-highlights'
1212
import { create as createMpxSfcPlugin } from './plugins/mpx-sfc'
1313
import { create as createMpxTemplatePlugin } from './plugins/mpx-template'
14+
import { create as createMpxDocumentLinksPlugin } from './plugins/mpx-document-links'
15+
import { create as createCSSPlugin } from './plugins/css'
1416
import { Commands } from './types'
1517

1618
export * from '@volar/language-service'
1719
export * from '@mpxjs/language-core'
1820
export * from './types'
1921

20-
export function getHybridModeLanguageServicePlugins(
22+
export function createMpxLanguageServicePlugins(
2123
ts: typeof import('typescript'),
2224
tsPluginClient:
2325
| (IRequests & {
@@ -51,10 +53,13 @@ function getCommonLanguageServicePlugins(
5153
return [
5254
createMpxSfcPlugin(),
5355
createMpxTemplatePlugin(),
56+
createMpxDocumentLinksPlugin(),
57+
createCSSPlugin(),
5458
createJsonPlugin(),
5559
createEmmetPlugin({
5660
mappedLanguages: {
5761
'mpx-root-tags': 'html',
62+
postcss: 'scss',
5863
},
5964
}),
6065
{
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import type * as CSS from 'vscode-css-languageservice'
2+
import type { TextDocument } from 'vscode-languageserver-textdocument'
3+
import type {
4+
LanguageServicePlugin,
5+
VirtualCode,
6+
} from '@volar/language-service'
7+
import { type Provide, create as baseCreate } from 'volar-service-css'
8+
import { URI } from 'vscode-uri'
9+
import { MpxVirtualCode } from '@mpxjs/language-core'
10+
11+
export function create(): LanguageServicePlugin {
12+
const base = baseCreate({
13+
scssDocumentSelector: ['scss', 'postcss'],
14+
})
15+
16+
return {
17+
...base,
18+
name: 'mpx-css',
19+
create(context) {
20+
const baseInstance = base.create(context)
21+
const {
22+
'css/languageService': getCssLs,
23+
'css/stylesheet': getStylesheet,
24+
} = baseInstance.provide as Provide
25+
26+
return {
27+
...baseInstance,
28+
async provideDiagnostics(document, token) {
29+
let diagnostics =
30+
(await baseInstance.provideDiagnostics?.(document, token)) ?? []
31+
if (document.languageId === 'postcss') {
32+
diagnostics = diagnostics.filter(
33+
diag => diag.code !== 'css-semicolonexpected',
34+
)
35+
diagnostics = diagnostics.filter(
36+
diag => diag.code !== 'css-ruleorselectorexpected',
37+
)
38+
diagnostics = diagnostics.filter(
39+
diag => diag.code !== 'unknownAtRules',
40+
)
41+
}
42+
return diagnostics
43+
},
44+
/**
45+
* If the editing position is within the virtual code and navigation is enabled,
46+
* skip the CSS renaming feature.
47+
*/
48+
provideRenameRange(document, position) {
49+
do {
50+
const uri = URI.parse(document.uri)
51+
const decoded = context.decodeEmbeddedDocumentUri(uri)
52+
const sourceScript =
53+
decoded && context.language.scripts.get(decoded[0])
54+
const virtualCode =
55+
decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1])
56+
if (
57+
!sourceScript?.generated ||
58+
!virtualCode?.id.startsWith('style_')
59+
) {
60+
break
61+
}
62+
63+
const root = sourceScript.generated.root
64+
if (!(root instanceof MpxVirtualCode)) {
65+
break
66+
}
67+
68+
const block = root.sfc.styles.find(
69+
style => style.name === decoded![1],
70+
)
71+
if (!block) {
72+
break
73+
}
74+
75+
let script: VirtualCode | undefined
76+
for (const [key, value] of sourceScript.generated.embeddedCodes) {
77+
if (key.startsWith('script_')) {
78+
script = value
79+
break
80+
}
81+
}
82+
if (!script) {
83+
break
84+
}
85+
86+
const offset = document.offsetAt(position) + block.startTagEnd
87+
for (const { sourceOffsets, lengths, data } of script.mappings) {
88+
if (
89+
!sourceOffsets.length ||
90+
!data.navigation ||
91+
(typeof data.navigation === 'object' &&
92+
!data.navigation.shouldRename)
93+
) {
94+
continue
95+
}
96+
97+
const start = sourceOffsets[0]
98+
const end = sourceOffsets.at(-1)! + lengths.at(-1)!
99+
100+
if (offset >= start && offset <= end) {
101+
return
102+
}
103+
}
104+
// eslint-disable-next-line no-constant-condition
105+
} while (0)
106+
107+
return worker(document, (stylesheet, cssLs) => {
108+
return cssLs.prepareRename(document, position, stylesheet)
109+
})
110+
},
111+
}
112+
113+
function worker<T>(
114+
document: TextDocument,
115+
callback: (stylesheet: CSS.Stylesheet, cssLs: CSS.LanguageService) => T,
116+
) {
117+
const cssLs = getCssLs(document)
118+
if (!cssLs) {
119+
return
120+
}
121+
return callback(getStylesheet(document, cssLs), cssLs)
122+
}
123+
},
124+
}
125+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type * as vscode from 'vscode-languageserver-protocol'
2+
import type { LanguageServicePlugin } from '@volar/language-service'
3+
import { URI } from 'vscode-uri'
4+
import { MpxVirtualCode, Sfc, tsCodegen } from '@mpxjs/language-core'
5+
6+
export function create(): LanguageServicePlugin {
7+
return {
8+
name: 'mpx-document-links',
9+
capabilities: {
10+
documentLinkProvider: {},
11+
},
12+
create(context) {
13+
return {
14+
provideDocumentLinks(document) {
15+
const uri = URI.parse(document.uri)
16+
const decoded = context.decodeEmbeddedDocumentUri(uri)
17+
const sourceScript =
18+
decoded && context.language.scripts.get(decoded[0])
19+
const virtualCode =
20+
decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1])
21+
if (!sourceScript?.generated || virtualCode?.id !== 'template') {
22+
return
23+
}
24+
25+
const root = sourceScript.generated.root
26+
if (!(root instanceof MpxVirtualCode)) {
27+
return
28+
}
29+
30+
const result: vscode.DocumentLink[] = []
31+
32+
const { sfc } = root
33+
const codegen = tsCodegen.get(sfc)
34+
const scopedClasses =
35+
codegen?.getGeneratedTemplate()?.scopedClasses ?? []
36+
const styleClasses = new Map<
37+
string,
38+
{
39+
index: number
40+
style: Sfc['styles'][number]
41+
classOffset: number
42+
}[]
43+
>()
44+
const option =
45+
root.mpxCompilerOptions.experimentalResolveStyleCssClasses
46+
47+
for (let i = 0; i < sfc.styles.length; i++) {
48+
const style = sfc.styles[i]
49+
if (option === 'always' || (option === 'scoped' && style.scoped)) {
50+
for (const className of style.classNames) {
51+
if (!styleClasses.has(className.text.slice(1))) {
52+
styleClasses.set(className.text.slice(1), [])
53+
}
54+
styleClasses.get(className.text.slice(1))!.push({
55+
index: i,
56+
style,
57+
classOffset: className.offset,
58+
})
59+
}
60+
}
61+
}
62+
63+
for (const { className, offset } of scopedClasses) {
64+
const styles = styleClasses.get(className)
65+
if (styles) {
66+
for (const style of styles) {
67+
const styleDocumentUri = context.encodeEmbeddedDocumentUri(
68+
decoded![0],
69+
'style_' + style.index,
70+
)
71+
const styleVirtualCode =
72+
sourceScript.generated.embeddedCodes.get(
73+
'style_' + style.index,
74+
)
75+
if (!styleVirtualCode) {
76+
continue
77+
}
78+
const styleDocument = context.documents.get(
79+
styleDocumentUri,
80+
styleVirtualCode.languageId,
81+
styleVirtualCode.snapshot,
82+
)
83+
const start = styleDocument.positionAt(style.classOffset)
84+
const end = styleDocument.positionAt(
85+
style.classOffset + className.length + 1,
86+
)
87+
result.push({
88+
range: {
89+
start: document.positionAt(offset),
90+
end: document.positionAt(offset + className.length),
91+
},
92+
target:
93+
context.encodeEmbeddedDocumentUri(
94+
decoded![0],
95+
'style_' + style.index,
96+
) +
97+
`#L${start.line + 1},${start.character + 1}-L${end.line + 1},${end.character + 1}`,
98+
})
99+
}
100+
}
101+
}
102+
103+
return result
104+
},
105+
}
106+
},
107+
}
108+
}

packages/language-service/src/plugins/mpx-template.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function create(): LanguageServicePlugin {
3131
})
3232

3333
return {
34-
name: `mpx-template`,
34+
name: 'mpx-template',
3535
capabilities: {
3636
...baseService.capabilities,
3737
completionProvider: {

0 commit comments

Comments
 (0)