Skip to content
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

Add plugin support #757

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -51,6 +51,12 @@ You can turn on format-on-save on a per-language basis by scoping the setting:

`eslint`, `tslint`, and all peer dependencies required by your specific configuration must be installed locally. Global installations will not be recognized.

### Prettier Plugins

Prettier plugins are supported when using a custom Prettier installation local or global. The plugins must be installed to the corresponding installation.

Eg. if you have installed Prettier locally to your project with `npm install --save-dev prettier` you must install the plugins like it too `npm install --save-dev @prettier/plugin-php`.

## Settings

### Prettier's Settings
Expand Down
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -240,6 +240,7 @@
"prettier-stylelint": "^0.4.2",
"prettier-tslint": "^0.4.2",
"read-pkg-up": "^4.0.0",
"requireg": "^0.2.1",
"resolve": "^1.10.0",
"typescript": "^2.9.2"
}
Expand Down
46 changes: 40 additions & 6 deletions src/PrettierEditProvider.ts
Expand Up @@ -6,10 +6,16 @@ import {
FormattingOptions,
CancellationToken,
TextEdit,
window,
} from 'vscode';

import { safeExecution, addToOutput, setUsedModule } from './errorHandler';
import { getParsersFromLanguageId, getConfig } from './utils';
import {
getParsersFromLanguageId,
getConfig,
supportsLanguage,
requireGlobalPrettier,
} from './utils';
import { requireLocalPkg } from './requirePkg';

import {
Expand Down Expand Up @@ -82,6 +88,27 @@ function mergeConfig(
)
: Object.assign(vscodeConfig, prettierConfig, additionalConfig);
}

function getPrettierForFile(fileName: string): Prettier {
// First try to use the local prettier for this file
const local = requireLocalPkg<Prettier>(fileName, 'prettier');
if (local) {
console.log('Formatting with local prettier');
return local;
}

// Fallback to global if no local is found
const global = requireGlobalPrettier();
if (global) {
console.log('Formatting with global prettier');
return global;
}

// Finally use the bundled one if all else fail
console.log('Formatting with bundled prettier');
return bundledPrettier;
}

/**
* Format the given text with user's configuration.
* @param text Text to format
Expand All @@ -94,7 +121,7 @@ async function format(
customOptions: Partial<PrettierConfig>
): Promise<string> {
const vscodeConfig: PrettierVSCodeConfig = getConfig(uri);
const localPrettier = requireLocalPkg(fileName, 'prettier') as Prettier;
const prettierInstance = getPrettierForFile(fileName);

// This has to stay, as it allows to skip in sub workspaceFolders. Sadly noop.
// wf1 (with "lang") -> glob: "wf1/**"
Expand All @@ -103,9 +130,16 @@ async function format(
return text;
}

if (!supportsLanguage(languageId, prettierInstance)) {
window.showErrorMessage(
`Prettier does not support "${languageId}". Maybe a plugin is missing from the workspace?`
);
return text;
}

const dynamicParsers = getParsersFromLanguageId(
languageId,
localPrettier,
prettierInstance,
isUntitled ? undefined : fileName
);
let useBundled = false;
Expand Down Expand Up @@ -224,7 +258,7 @@ async function format(
() => {
const warningMessage =
`prettier@${
localPrettier.version
prettierInstance.version
} doesn't support ${languageId}. ` +
`Falling back to bundled prettier@${
bundledPrettier.version
Expand All @@ -241,10 +275,10 @@ async function format(
);
}

setUsedModule('prettier', localPrettier.version, false);
setUsedModule('prettier', prettierInstance.version, false);

return safeExecution(
() => localPrettier.format(text, prettierOptions),
() => prettierInstance.format(text, prettierOptions),
text,
fileName
);
Expand Down
7 changes: 3 additions & 4 deletions src/requirePkg.ts
Expand Up @@ -36,18 +36,17 @@ function findPkg(fspath: string, pkgName: string): string | undefined {
* @param {string} pkgName package's name to require
* @returns module
*/
function requireLocalPkg(fspath: string, pkgName: string): any {
function requireLocalPkg<T>(fspath: string, pkgName: string): T | undefined {
const modulePath = findPkg(fspath, pkgName);
console.log('Found module path', modulePath);
if (modulePath !== void 0) {
try {
return require(modulePath);
} catch (e) {
addToOutput(
`Failed to load ${pkgName} from ${modulePath}. Using bundled`
`Failed to load ${pkgName} from ${modulePath}. Using bundled or global`
);
}
}

return require(pkgName);
}
export { requireLocalPkg };
127 changes: 119 additions & 8 deletions src/utils.ts
@@ -1,14 +1,50 @@
import { workspace, Uri } from 'vscode';
import { basename } from 'path';
import { basename, dirname, join } from 'path';
import {
PrettierVSCodeConfig,
Prettier,
PrettierSupportInfo,
ParserOption,
} from './types.d';
import { requireLocalPkg } from './requirePkg';
import { execSync } from 'child_process';

const requireGlobal = require('requireg');
const bundledPrettier = require('prettier') as Prettier;

let globalNodeExecPath: string | undefined;

try {
globalNodeExecPath = execSync(
'node --eval "process.stdout.write(process.execPath)"'
).toString();
} catch (error) {
// Not installed
}

/**
* Require global prettier or undefined if none is installed
*/
export function requireGlobalPrettier(): Prettier | undefined {
if (!globalNodeExecPath) {
return;
}

// The global node is different from the one vscode extensions executes in.
// So workaround it by setting NODE_PATH using the process.execPath from the
// global node installation
const origNodePath = process.env.NODE_PATH;
process.env.NODE_PATH = join(dirname(globalNodeExecPath), 'node_modules');

try {
return requireGlobal('prettier', true);
} catch (error) {
// No global installed
} finally {
process.env.NODE_PATH = origNodePath;
}
}

export function getConfig(uri?: Uri): PrettierVSCodeConfig {
return workspace.getConfiguration('prettier', uri) as any;
}
Expand All @@ -34,11 +70,66 @@ export function getParsersFromLanguageId(
return language.parsers;
}

/**
* Type Guard function for filtering empty values out of arrays.
*
* Usage: arr.filter(notEmpty)
*/
export function notEmpty<TValue>(
value: TValue | null | undefined
): value is TValue {
return value !== null && value !== undefined;
}

/**
* Find all enabled languages from all available prettiers: local, global and
* bundled.
*/
export function allEnabledLanguages(): string[] {
return getSupportLanguages().reduce(
(ids, language) => [...ids, ...(language.vscodeLanguageIds || [])],
[] as string[]
);
let prettierInstances: Prettier[] = [bundledPrettier];

const globalPrettier = requireGlobalPrettier();
if (globalPrettier) {
console.log('Has global Prettier');
prettierInstances = prettierInstances.concat(globalPrettier);
} else {
console.log('No global Prettier');
}

if (workspace.workspaceFolders) {
// Each workspace can have own local prettier
const localPrettiers = workspace.workspaceFolders
.map(workspaceFolder =>
requireLocalPkg<Prettier>(
workspaceFolder.uri.fsPath,
'prettier'
)
)
.filter(notEmpty);

prettierInstances = prettierInstances.concat(localPrettiers);
}

return getUniqueSupportedLanguages(prettierInstances);
}

function getUniqueSupportedLanguages(prettierInstances: Prettier[]): string[] {
const languages = new Set<string>();

for (const prettier of prettierInstances) {
for (const language of getSupportLanguages(prettier)) {
if (!language.vscodeLanguageIds) {
continue;
}

for (const id of language.vscodeLanguageIds) {
languages.add(id);
}
}
}

console.log('LANGS', prettierInstances.length, Array.from(languages));
return Array.from(languages);
}

export function rangeSupportedLanguages(): string[] {
Expand All @@ -52,10 +143,30 @@ export function rangeSupportedLanguages(): string[] {
];
}

export function getGroup(group: string): PrettierSupportInfo['languages'] {
return getSupportLanguages().filter(language => language.group === group);
export function getGroup(
prettier: Prettier,
group: string
): PrettierSupportInfo['languages'] {
return getSupportLanguages(prettier).filter(
language => language.group === group
);
}

function getSupportLanguages(prettierInstance: Prettier = bundledPrettier) {
function getSupportLanguages(prettierInstance: Prettier) {
return prettierInstance.getSupportInfo(prettierInstance.version).languages;
}

export function supportsLanguage(
vscodeLanguageId: string,
prettierInstance: Prettier
) {
return prettierInstance
.getSupportInfo(prettierInstance.version)
.languages.some(language => {
if (!language.vscodeLanguageIds) {
return false;
}

return language.vscodeLanguageIds.includes(vscodeLanguageId);
});
}