Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
6 changed files
with
201 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
278 changes: 184 additions & 94 deletions
278
extensions/vscode-vue-language-features/src/features/doctor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,111 +1,201 @@ | ||
import { getCurrentTsdk, getTsVersion } from './tsVersion'; | ||
import * as vscode from 'vscode'; | ||
import { takeOverModeEnabled } from '../common'; | ||
import * as fs from '../utils/fs'; | ||
import * as semver from 'semver' | ||
import * as semver from 'semver'; | ||
import { BaseLanguageClient } from 'vscode-languageclient'; | ||
import { GetMatchTsConfigRequest, ParseSFCRequest } from '@volar/vue-language-server'; | ||
|
||
export async function register(context: vscode.ExtensionContext) { | ||
context.subscriptions.push(vscode.commands.registerCommand('volar.action.doctor', async () => { | ||
const scheme = 'vue-doctor'; | ||
const knownValidSyntanxHighlightExtensions = { | ||
postcss: ['cpylua.language-postcss'], | ||
stylus: ['sysoev.language-stylus'], | ||
sass: ['Syler.sass-indented'], | ||
}; | ||
|
||
// TODO: tsconfig infos | ||
// TODO: warnings | ||
const vetur = vscode.extensions.getExtension('octref.vetur'); | ||
if (vetur && vetur.isActive) { | ||
vscode.window.showWarningMessage( | ||
'Vetur is active. Disable it for Volar to work properly.' | ||
); | ||
export async function register(context: vscode.ExtensionContext, client: BaseLanguageClient) { | ||
|
||
const item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); | ||
item.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); | ||
item.command = 'volar.action.doctor'; | ||
|
||
const docChangeEvent = new vscode.EventEmitter<vscode.Uri>(); | ||
|
||
updateStatusBar(vscode.window.activeTextEditor); | ||
|
||
context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(updateStatusBar)); | ||
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider( | ||
scheme, | ||
{ | ||
onDidChange: docChangeEvent.event, | ||
async provideTextDocumentContent(doctorUri: vscode.Uri): Promise<string | undefined> { | ||
|
||
const fileUri = doctorUri.with({ | ||
scheme: 'file', | ||
path: doctorUri.path.substring(0, doctorUri.path.length - '/Doctor.md'.length), | ||
}); | ||
const problems = await getProblems(fileUri); | ||
|
||
let content = `# ${fileUri.path.split('/').pop()} Doctor\n\n`; | ||
|
||
for (const problem of problems) { | ||
content += '## ❗ ' + problem.title + '\n\n'; | ||
content += problem.message + '\n\n'; | ||
} | ||
|
||
content += '---\n\n'; | ||
content += `> Have question about the report message? You can see how it judge by inspecting the [source code](https://github.com/johnsoncodehk/volar/blob/master/extensions/vscode-vue-language-features/src/features/doctor.ts).\n\n`; | ||
|
||
return content.trim(); | ||
} | ||
}, | ||
)); | ||
context.subscriptions.push(vscode.commands.registerCommand('volar.action.doctor', () => { | ||
const doc = vscode.window.activeTextEditor?.document; | ||
if (doc?.languageId === 'vue' && doc.uri.scheme === 'file') { | ||
vscode.commands.executeCommand('markdown.showPreviewToSide', getDoctorUri(doc.uri)); | ||
} | ||
})); | ||
|
||
const tsConfigPaths = [ | ||
...await vscode.workspace.findFiles('tsconfig.json'), | ||
...await vscode.workspace.findFiles('jsconfig.json'), | ||
]; | ||
const tsConfigs = await Promise.all(tsConfigPaths.map(async tsConfigPath => ({ | ||
path: tsConfigPath, | ||
content: await fs.readFile(tsConfigPath), | ||
}))); | ||
|
||
const vueVersion = getWorkspacePackageJson('vue')?.version; | ||
const runtimeDomVersion = getWorkspacePackageJson('@vue/runtime-dom')?.version; | ||
|
||
let parsedTsConfig: undefined | Record<string, any> | ||
try { | ||
parsedTsConfig = tsConfigs[0] ? JSON.parse(tsConfigs[0].content) : undefined | ||
} catch (error) { | ||
console.error(error) | ||
parsedTsConfig = undefined | ||
function getDoctorUri(fileUri: vscode.Uri) { | ||
return fileUri.with({ scheme, path: fileUri.path + '/Doctor.md' }); | ||
} | ||
|
||
async function updateStatusBar(editor: vscode.TextEditor | undefined) { | ||
if ( | ||
vscode.workspace.getConfiguration('volar').get<boolean>('doctor.statusBarItem') | ||
&& editor | ||
&& editor.document.languageId === 'vue' | ||
&& editor.document.uri.scheme === 'file' | ||
) { | ||
const problems = await getProblems(editor.document.uri); | ||
if (problems.length && vscode.window.activeTextEditor?.document === editor.document) { | ||
item.show(); | ||
item.text = problems.length + (problems.length === 1 ? ' problem found' : ' problems found'); | ||
docChangeEvent.fire(getDoctorUri(editor.document.uri)); | ||
} | ||
} | ||
else { | ||
item.hide(); | ||
} | ||
} | ||
|
||
async function getProblems(fileUri: vscode.Uri) { | ||
|
||
const workspaceFolder = vscode.workspace.workspaceFolders?.find(f => fileUri.path.startsWith(f.uri.path))?.uri.fsPath ?? vscode.workspace.rootPath!; | ||
const tsconfig = await client.sendRequest(GetMatchTsConfigRequest.type, { uri: fileUri.toString() }); | ||
const vueDoc = vscode.workspace.textDocuments.find(doc => doc.fileName === fileUri.fsPath); | ||
const sfc = vueDoc ? await client.sendRequest(ParseSFCRequest.type, vueDoc.getText()) : undefined; | ||
const vueVersion = getWorkspacePackageJson(workspaceFolder, 'vue')?.version; | ||
const problems: { | ||
title: string; | ||
message: string; | ||
}[] = []; | ||
|
||
// check vue module exist | ||
if (!vueVersion) { | ||
problems.push({ | ||
title: '`vue` module not found', | ||
message: 'Vue module not found from workspace, you may have not install `node_modules` yet.', | ||
}); | ||
} | ||
const vueTarget = parsedTsConfig?.vueCompilerOptions?.target | ||
|
||
// check vue version < 3 but missing vueCompilerOptions.target | ||
if (vueVersion) { | ||
if (semver.lte(vueVersion, '2.6.14')) { | ||
if (!runtimeDomVersion) { | ||
vscode.window.showWarningMessage( | ||
'Found Vue with version <2.7 but no "@vue/runtime-dom". Consider adding "@vue/runtime-dom" to your dev dependencies.' | ||
); | ||
} | ||
if (vueTarget !== 2) { | ||
vscode.window.showWarningMessage( | ||
'Found Vue with version <2.7 but incorrect "target" option in your "tsconfig.json". Consider adding "target": 2.' | ||
); | ||
} | ||
const vueVersionNumber = semver.gte(vueVersion, '3.0.0') ? 3 : semver.gte(vueVersion, '2.7.0') ? 2.7 : 2; | ||
const targetVersionNumber = tsconfig?.raw?.vueCompilerOptions?.target ?? 3; | ||
const lines = [ | ||
`Target version not match, you can specify the target version in \`vueCompilerOptions.target\` in tsconfig.json / jsconfig.json. (expected \`"target": ${vueVersionNumber}\`)`, | ||
'', | ||
'- Vue version: ' + vueVersion, | ||
'- tsconfig: ' + (tsconfig?.fileName ?? 'Not found'), | ||
'- tsconfig target: ' + targetVersionNumber + (tsconfig?.raw?.vueCompilerOptions?.target !== undefined ? '' : ' (default)'), | ||
]; | ||
if (vueVersionNumber !== targetVersionNumber) { | ||
problems.push({ | ||
title: 'Incorrect Target', | ||
message: lines.join('\n'), | ||
}); | ||
} | ||
|
||
if (semver.gt(vueVersion, '2.6.14') && semver.lt(vueVersion, '3.0.0') && vueTarget !== 2.7) { | ||
vscode.window.showWarningMessage( | ||
'Found Vue with version <2.7 but incorrect "target" option in your "tsconfig.json". Consider adding "target": 2.7' | ||
); | ||
} | ||
|
||
// check vue version < 2.7 but @vue/compiler-dom missing | ||
if (vueVersion && semver.lt(vueVersion, '2.7.0') && !getWorkspacePackageJson(workspaceFolder, '@vue/compiler-dom')) { | ||
problems.push({ | ||
title: 'TsConfig missing for Vue 2', | ||
message: 'In ', | ||
}); | ||
} | ||
|
||
// check vue-tsc version same with extension version | ||
const vueTscVersoin = getWorkspacePackageJson(workspaceFolder, 'vue-tsc')?.version; | ||
if (vueTscVersoin && vueTscVersoin !== context.extension.packageJSON.version) { | ||
problems.push({ | ||
title: '`vue-tsc` version', | ||
message: `The \`${context.extension.packageJSON.displayName}\` version is \`${context.extension.packageJSON.version}\`, but workspace \`vue-tsc\` version is \`${vueTscVersoin}\`, there may have different type checking behavior.`, | ||
}); | ||
} | ||
|
||
// check should use @volar-plugins/vetur instead of vetur | ||
const vetur = vscode.extensions.getExtension('octref.vetur'); | ||
if (vetur?.isActive) { | ||
problems.push({ | ||
title: 'Use @volar-plugins/vetur instead of Vetur', | ||
message: 'Detected Vetur enabled, you might consider disabling it and use [@volar-plugins/vetur](https://github.com/johnsoncodehk/volar-plugins/tree/master/packages/vetur) instead of.', | ||
}); | ||
} | ||
|
||
// check using png bug don't install @volar/vue-language-plugin-pug | ||
if ( | ||
sfc?.descriptor.template?.lang === 'pug' | ||
&& !tsconfig?.raw?.vueCompilerOptions?.plugins?.includes('@volar/vue-language-plugin-pug') | ||
) { | ||
problems.push({ | ||
title: '`@volar/vue-language-plugin-pug` missing', | ||
message: [ | ||
'For `<template lang="pug">`, you need add plugin via `$ npm install -D @volar/vue-language-plugin-pug` and add it to `vueCompilerOptions.plugins` to support TypeScript intellisense in Pug template.', | ||
'', | ||
'- tsconfig.json / jsconfig.json', | ||
'```jsonc', | ||
JSON.stringify({ vueCompilerOptions: { plugins: ["@volar/vue-language-plugin-pug"] } }, undefined, 2), | ||
'```', | ||
].join('\n'), | ||
}); | ||
} | ||
|
||
// check syntax highlight extension installed | ||
if (sfc) { | ||
const blocks = [ | ||
sfc.descriptor.template, | ||
sfc.descriptor.script, | ||
sfc.descriptor.scriptSetup, | ||
...sfc.descriptor.styles, | ||
...sfc.descriptor.customBlocks, | ||
]; | ||
for (const block of blocks) { | ||
if (!block) continue; | ||
if (block.lang && block.lang in knownValidSyntanxHighlightExtensions) { | ||
const validExts = knownValidSyntanxHighlightExtensions[block.lang as keyof typeof knownValidSyntanxHighlightExtensions]; | ||
const someInstalled = validExts.some(ext => !!vscode.extensions.getExtension(ext)); | ||
if (!someInstalled) { | ||
problems.push({ | ||
title: 'Syntax Highlight for ' + block.lang, | ||
message: `Not found valid syntax highlight extension for ${block.lang} langauge block, you can choose to install one of the following:\n\n` | ||
+ validExts.map(ext => `- [${ext}](https://marketplace.visualstudio.com/items?itemName=${ext})\n`), | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
|
||
const tsdk = getCurrentTsdk(context); | ||
const tsVersion = getTsVersion(tsdk.tsdk); | ||
const content = ` | ||
## Infos | ||
- vscode.version: ${vscode.version} | ||
- vscode.typescript.version: ${tsVersion} | ||
- vscode.typescript-extension.actived: ${!!vscode.extensions.getExtension('vscode.typescript-language-features')} | ||
- vue-language-features.version: ${context.extension.packageJSON.version} | ||
- typescript-vue-plugin.version: ${vscode.extensions.getExtension('Vue.vscode-typescript-vue-plugin')?.packageJSON.version} | ||
- vetur.actived: ${!!vetur} | ||
- workspace.vue-tsc.version: ${getWorkspacePackageJson('vue-tsc')?.version} | ||
- workspace.typescript.version: ${getWorkspacePackageJson('typescript')?.version} | ||
- workspace.vue.version: ${vueVersion} | ||
- workspace.@vue/runtime-dom.version: ${runtimeDomVersion} | ||
- workspace.tsconfig.vueCompilerOptions.target: ${vueTarget} | ||
- takeover-mode.enabled: ${takeOverModeEnabled()} | ||
## tsconfigs | ||
${tsConfigs.map(tsconfig => ` | ||
\`${tsconfig.path}\` | ||
\`\`\`jsonc | ||
${tsconfig.content} | ||
\`\`\` | ||
`)} | ||
### Configuration | ||
\`\`\`json | ||
${JSON.stringify({ | ||
volar: vscode.workspace.getConfiguration('').get('volar'), | ||
typescript: vscode.workspace.getConfiguration('').get('typescript'), | ||
javascript: vscode.workspace.getConfiguration('').get('javascript'), | ||
}, null, 2)} | ||
\`\`\` | ||
`; | ||
|
||
const document = await vscode.workspace.openTextDocument({ content: content.trim(), language: 'markdown' }); | ||
|
||
await vscode.window.showTextDocument(document); | ||
})); | ||
// check outdated language services plugins | ||
// check outdated vue language plugins | ||
// check node_modules has more than one vue versions | ||
// check ESLint, Prettier... | ||
|
||
return problems; | ||
} | ||
} | ||
|
||
function getWorkspacePackageJson(pkg: string): { version: string; } | undefined { | ||
function getWorkspacePackageJson(folder: string, pkg: string): { version: string; } | undefined { | ||
try { | ||
return require(require.resolve(pkg + '/package.json', { paths: [vscode.workspace.rootPath!] })); | ||
return require(require.resolve(pkg + '/package.json', { paths: [folder] })); | ||
} catch { } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters