Skip to content

Commit

Permalink
feat: doctor panel
Browse files Browse the repository at this point in the history
close #1254
  • Loading branch information
johnsoncodehk committed Oct 3, 2022
1 parent f1367d2 commit 5b32d4e
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 100 deletions.
7 changes: 6 additions & 1 deletion extensions/vscode-vue-language-features/package.json
Expand Up @@ -334,6 +334,11 @@
"default": true,
"description": "Show Vite / Nuxt App preview icon."
},
"volar.doctor.statusBarItem": {
"type": "boolean",
"default": true,
"description": "Show known problems in status bar."
},
"volar.codeLens.scriptSetupTools": {
"type": "boolean",
"default": false,
Expand Down Expand Up @@ -614,7 +619,7 @@
},
{
"command": "volar.action.doctor",
"title": "Show doctor panel (WIP)",
"title": "Doctor",
"category": "Volar"
},
{
Expand Down
2 changes: 1 addition & 1 deletion extensions/vscode-vue-language-features/src/common.ts
Expand Up @@ -98,7 +98,7 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang

splitEditors.register(context, syntacticClient);
preview.register(context, syntacticClient);
doctor.register(context);
doctor.register(context, semanticClient);
tsVersion.register('volar.selectTypeScriptVersion', context, semanticClient);
reloadProject.register('volar.action.reloadProject', context, semanticClient);

Expand Down
278 changes: 184 additions & 94 deletions extensions/vscode-vue-language-features/src/features/doctor.ts
@@ -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 { }
}
Expand Up @@ -36,9 +36,9 @@ export async function register(cmd: string, context: vscode.ExtensionContext, la
languageClient.code2ProtocolConverter.asTextDocumentIdentifier(vscode.window.activeTextEditor.document),
);
if (tsconfig) {
statusBar.text = path.relative(vscode.workspace.rootPath! as path.OsPath, tsconfig as path.PosixPath);
statusBar.text = path.relative(vscode.workspace.rootPath! as path.OsPath, tsconfig.fileName as path.PosixPath);
statusBar.command = cmd;
currentTsconfig = tsconfig;
currentTsconfig = tsconfig.fileName;
}
else {
statusBar.text = 'No tsconfig';
Expand Down
8 changes: 7 additions & 1 deletion packages/language-server/src/features/customFeatures.ts
Expand Up @@ -9,7 +9,13 @@ export function register(
projects: Workspaces,
) {
connection.onRequest(GetMatchTsConfigRequest.type, async params => {
return (await projects.getProject(params.uri))?.tsconfig;
const project = (await projects.getProject(params.uri));
if (project) {
return {
fileName: project.tsconfig,
raw: project.project?.getParsedCommandLine().raw,
};
}
});
connection.onNotification(ReloadProjectNotification.type, async params => {
projects.reloadProject();
Expand Down
2 changes: 1 addition & 1 deletion packages/language-server/src/protocol.ts
Expand Up @@ -32,7 +32,7 @@ export namespace FindFileReferenceRequest {

export namespace GetMatchTsConfigRequest {
export type ParamsType = vscode.TextDocumentIdentifier;
export type ResponseType = string | null | undefined;
export type ResponseType = { fileName: string, raw: any } | null | undefined;
export type ErrorType = never;
export const type = new vscode.RequestType<ParamsType, ResponseType, ErrorType>('volar/tsconfig');
}
Expand Down

0 comments on commit 5b32d4e

Please sign in to comment.