in client/src/extension.ts [802:1493]
function realActivate(context: ExtensionContext): void {
const statusBarItem = Window.createStatusBarItem('generalStatus', StatusBarAlignment.Right, 0);
let serverRunning: boolean | undefined;
const starting = 'ESLint server is starting.';
const running = 'ESLint server is running.';
const stopped = 'ESLint server stopped.';
statusBarItem.name = 'ESLint';
statusBarItem.text = 'ESLint';
statusBarItem.command = 'eslint.showOutputChannel';
const documentStatus: Map<string, Status> = new Map();
function updateDocumentStatus(params: StatusParams): void {
documentStatus.set(params.uri, params.state);
updateStatusBar(params.uri);
}
function updateStatusBar(uri: string | undefined) {
const status = function() {
if (serverRunning === false) {
return Status.error;
}
if (uri === undefined) {
uri = Window.activeTextEditor?.document.uri.toString();
}
return (uri !== undefined ? documentStatus.get(uri) : undefined) ?? Status.ok;
}();
let icon: string| undefined;
let tooltip: string | undefined;
let text: string = 'ESLint';
let backgroundColor: ThemeColor | undefined;
let foregroundColor: ThemeColor | undefined;
switch (status) {
case Status.ok:
icon = undefined;
foregroundColor = new ThemeColor('statusBarItem.foreground');
backgroundColor = new ThemeColor('statusBarItem.background');
break;
case Status.warn:
icon = '$(alert)';
foregroundColor = new ThemeColor('statusBarItem.warningForeground');
backgroundColor = new ThemeColor('statusBarItem.warningBackground');
break;
case Status.error:
icon = '$(issue-opened)';
foregroundColor = new ThemeColor('statusBarItem.errorForeground');
backgroundColor = new ThemeColor('statusBarItem.errorBackground');
break;
}
statusBarItem.text = icon !== undefined ? `${icon} ${text}` : text;
statusBarItem.color = foregroundColor;
statusBarItem.backgroundColor = backgroundColor;
statusBarItem.tooltip = tooltip ? tooltip : serverRunning === undefined ? starting : serverRunning === true ? running : stopped;
const alwaysShow = Workspace.getConfiguration('eslint').get('alwaysShowStatus', false);
if (alwaysShow || status !== Status.ok) {
statusBarItem.show();
} else {
statusBarItem.hide();
}
}
function readCodeActionsOnSaveSetting(document: TextDocument): boolean {
let result: boolean | undefined = undefined;
const languageConfig = Workspace.getConfiguration(undefined, document.uri).get<LanguageSettings>(`[${document.languageId}]`);
function isEnabled(value: CodeActionsOnSave | string[]): boolean | undefined {
if (value === undefined || value === null) {
return undefined;
}
if (Array.isArray(value)) {
const result = value.some((element) => { return element === 'source.fixAll.eslint' || element === 'source.fixAll'; });
return result === true ? true : undefined;
} else {
return value['source.fixAll.eslint'] ?? value['source.fixAll'];
}
}
if (languageConfig !== undefined) {
const codeActionsOnSave = languageConfig?.['editor.codeActionsOnSave'];
if (codeActionsOnSave !== undefined) {
result = isEnabled(codeActionsOnSave);
}
}
if (result === undefined) {
const codeActionsOnSave = Workspace.getConfiguration('editor', document.uri).get<CodeActionsOnSave>('codeActionsOnSave');
if (codeActionsOnSave !== undefined) {
result = isEnabled(codeActionsOnSave);
}
}
return result ?? false;
}
function migrationFailed(error: any): void {
client.error(error.message ?? 'Unknown error', error);
void Window.showErrorMessage('ESLint settings migration failed. Please see the ESLint output channel for further details', 'Open Channel').then((selected) => {
if (selected === undefined) {
return;
}
client.outputChannel.show();
});
}
async function migrateSettings(): Promise<void> {
const folders = Workspace.workspaceFolders;
if (folders === undefined) {
void Window.showErrorMessage('ESLint settings can only be converted if VS Code is opened on a workspace folder.');
return;
}
const folder = await pickFolder(folders, 'Pick a folder to convert its settings');
if (folder === undefined) {
return;
}
const migration = new Migration(folder.uri);
migration.record();
if (migration.needsUpdate()) {
try {
await migration.update();
} catch (error) {
migrationFailed(error);
}
}
}
function sanitize<T, D>(value: T, type: 'bigint' | 'boolean' | 'function' | 'number' | 'object' | 'string' | 'symbol' | 'undefined', def: D): T | D {
if (Array.isArray(value)) {
return value.filter(item => typeof item === type) as unknown as T;
} else if (typeof value !== type) {
return def;
}
return value;
}
const serverModule = Uri.joinPath(context.extensionUri, 'server', 'out', 'eslintServer.js').fsPath;
const eslintConfig = Workspace.getConfiguration('eslint');
const debug = sanitize(eslintConfig.get<boolean>('debug', false) ?? false, 'boolean', false);
const runtime = sanitize(eslintConfig.get<string | null>('runtime', null) ?? undefined, 'string', undefined);
const execArgv = sanitize(eslintConfig.get<string[] | null>('execArgv', null) ?? undefined, 'string', undefined);
const nodeEnv = sanitize(eslintConfig.get('nodeEnv', null) ?? undefined, 'string', undefined);
let env: { [key: string]: string | number | boolean } | undefined;
if (debug) {
env = env || {};
env.DEBUG = 'eslint:*,-eslint:code-path';
}
if (nodeEnv !== undefined) {
env = env || {};
env.NODE_ENV = nodeEnv;
}
const debugArgv = ['--nolazy', '--inspect=6011'];
const serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc, runtime, options: { execArgv, cwd: process.cwd(), env } },
debug: { module: serverModule, transport: TransportKind.ipc, runtime, options: { execArgv: execArgv !== undefined ? execArgv.concat(debugArgv) : debugArgv, cwd: process.cwd(), env } }
};
let defaultErrorHandler: ErrorHandler;
let serverCalledProcessExit: boolean = false;
const packageJsonFilter: DocumentFilter = { scheme: 'file', pattern: '**/package.json' };
const configFileFilter: DocumentFilter = { scheme: 'file', pattern: '**/.eslintr{c.js,c.yaml,c.yml,c,c.json}' };
const syncedDocuments: Map<string, TextDocument> = new Map<string, TextDocument>();
let migration: Migration | undefined;
const migrationSemaphore: Semaphore<void> = new Semaphore<void>(1);
let notNow: boolean = false;
const supportedQuickFixKinds: Set<string> = new Set([CodeActionKind.Source.value, CodeActionKind.SourceFixAll.value, `${CodeActionKind.SourceFixAll.value}.eslint`, CodeActionKind.QuickFix.value]);
const clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: 'file' }, { scheme: 'untitled' }],
diagnosticCollectionName: 'eslint',
revealOutputChannelOn: RevealOutputChannelOn.Never,
initializationOptions: {
},
progressOnInitialization: true,
synchronize: {
// configurationSection: 'eslint',
fileEvents: [
Workspace.createFileSystemWatcher('**/.eslintr{c.js,c.yaml,c.yml,c,c.json}'),
Workspace.createFileSystemWatcher('**/.eslintignore'),
Workspace.createFileSystemWatcher('**/package.json')
]
},
initializationFailedHandler: (error) => {
client.error('Server initialization failed.', error);
client.outputChannel.show(true);
return false;
},
errorHandler: {
error: (error, message, count): ErrorHandlerResult => {
return defaultErrorHandler.error(error, message, count);
},
closed: (): CloseHandlerResult => {
if (serverCalledProcessExit) {
return { action: CloseAction.DoNotRestart };
}
return defaultErrorHandler.closed();
}
},
middleware: {
didOpen: async (document, next) => {
if (Languages.match(packageJsonFilter, document) || Languages.match(configFileFilter, document) || computeValidate(document) !== Validate.off) {
const result = next(document);
syncedDocuments.set(document.uri.toString(), document);
return result;
}
},
didChange: async (event, next) => {
if (syncedDocuments.has(event.document.uri.toString())) {
return next(event);
}
},
willSave: async (event, next) => {
if (syncedDocuments.has(event.document.uri.toString())) {
return next(event);
}
},
willSaveWaitUntil: (event, next) => {
if (syncedDocuments.has(event.document.uri.toString())) {
return next(event);
} else {
return Promise.resolve([]);
}
},
didSave: async (document, next) => {
if (syncedDocuments.has(document.uri.toString())) {
return next(document);
}
},
didClose: async (document, next) => {
const uri = document.uri.toString();
if (syncedDocuments.has(uri)) {
syncedDocuments.delete(uri);
return next(document);
}
},
provideCodeActions: (document, range, context, token, next): ProviderResult<(Command | CodeAction)[]> => {
if (!syncedDocuments.has(document.uri.toString())) {
return [];
}
if (context.only !== undefined && !supportedQuickFixKinds.has(context.only.value)) {
return [];
}
if (context.only === undefined && (!context.diagnostics || context.diagnostics.length === 0)) {
return [];
}
const eslintDiagnostics: Diagnostic[] = [];
for (const diagnostic of context.diagnostics) {
if (diagnostic.source === 'eslint') {
eslintDiagnostics.push(diagnostic);
}
}
if (context.only === undefined && eslintDiagnostics.length === 0) {
return [];
}
const newContext: CodeActionContext = Object.assign({}, context, { diagnostics: eslintDiagnostics });
return next(document, range, newContext, token);
},
workspace: {
didChangeWatchedFile: (event, next) => {
probeFailed.clear();
return next(event);
},
didChangeConfiguration: async (sections, next) => {
if (migration !== undefined && (sections === undefined || sections.length === 0)) {
migration.captureDidChangeSetting(() => {
return next(sections);
});
} else {
return next(sections);
}
},
configuration: async (params, _token, _next): Promise<any[]> => {
if (params.items === undefined) {
return [];
}
const result: (ConfigurationSettings | null)[] = [];
for (const item of params.items) {
if (item.section || !item.scopeUri) {
result.push(null);
continue;
}
const resource = client.protocol2CodeConverter.asUri(item.scopeUri);
const config = Workspace.getConfiguration('eslint', resource);
const workspaceFolder = resource.scheme === 'untitled'
? Workspace.workspaceFolders !== undefined ? Workspace.workspaceFolders[0] : undefined
: Workspace.getWorkspaceFolder(resource);
await migrationSemaphore.lock(async () => {
const globalMigration = Workspace.getConfiguration('eslint').get('migration.2_x', 'on');
if (notNow === false && globalMigration === 'on') {
try {
migration = new Migration(resource);
migration.record();
interface Item extends MessageItem {
id: 'yes' | 'no' | 'readme' | 'global' | 'local';
}
if (migration.needsUpdate()) {
const folder = workspaceFolder?.name;
const file = path.basename(resource.fsPath);
const selected = await Window.showInformationMessage<Item>(
[
`The ESLint 'autoFixOnSave' setting needs to be migrated to the new 'editor.codeActionsOnSave' setting`,
folder !== undefined ? `for the workspace folder: ${folder}.` : `for the file: ${file}.`,
`For compatibility reasons the 'autoFixOnSave' remains and needs to be removed manually.`,
`Do you want to migrate the setting?`
].join(' '),
{ modal: true},
{ id: 'yes', title: 'Yes'},
{ id: 'global', title: 'Never migrate Settings' },
{ id: 'readme', title: 'Open Readme' },
{ id: 'no', title: 'Not now', isCloseAffordance: true }
);
if (selected !== undefined) {
if (selected.id === 'yes') {
try {
await migration.update();
} catch (error) {
migrationFailed(error);
}
} else if (selected.id === 'no') {
notNow = true;
} else if (selected.id === 'global') {
await config.update('migration.2_x', 'off', ConfigurationTarget.Global);
} else if (selected.id === 'readme') {
notNow = true;
void Env.openExternal(Uri.parse('https://github.com/microsoft/vscode-eslint#settings-migration'));
}
}
}
} finally {
migration = undefined;
}
}
});
const settings: ConfigurationSettings = {
validate: Validate.off,
packageManager: config.get('packageManager', 'npm'),
useESLintClass: config.get('useESLintClass', false),
codeActionOnSave: {
enable: false,
mode: CodeActionsOnSaveMode.all
},
format: false,
quiet: config.get('quiet', false),
onIgnoredFiles: ESLintSeverity.from(config.get<string>('onIgnoredFiles', ESLintSeverity.off)),
options: config.get('options', {}),
rulesCustomizations: parseRulesCustomizations(config.get('rules.customizations')),
run: config.get('run', 'onType'),
nodePath: config.get<string | undefined>('nodePath', undefined) ?? null,
workingDirectory: undefined,
workspaceFolder: undefined,
codeAction: {
disableRuleComment: config.get('codeAction.disableRuleComment', { enable: true, location: 'separateLine' as 'separateLine' }),
showDocumentation: config.get('codeAction.showDocumentation', { enable: true })
}
};
const document: TextDocument | undefined = syncedDocuments.get(item.scopeUri);
if (document === undefined) {
result.push(settings);
continue;
}
if (config.get('enabled', true)) {
settings.validate = computeValidate(document);
}
if (settings.validate !== Validate.off) {
settings.format = !!config.get('format.enable', false);
settings.codeActionOnSave.enable = readCodeActionsOnSaveSetting(document);
settings.codeActionOnSave.mode = CodeActionsOnSaveMode.from(config.get('codeActionsOnSave.mode', CodeActionsOnSaveMode.all));
settings.codeActionOnSave.rules = CodeActionsOnSaveRules.from(config.get('codeActionsOnSave.rules', null));
}
if (workspaceFolder !== undefined) {
settings.workspaceFolder = {
name: workspaceFolder.name,
uri: client.code2ProtocolConverter.asUri(workspaceFolder.uri)
};
}
const workingDirectories = config.get<(string | LegacyDirectoryItem | DirectoryItem | PatternItem | ModeItem)[] | undefined>('workingDirectories', undefined);
if (Array.isArray(workingDirectories)) {
let workingDirectory: ModeItem | DirectoryItem | undefined = undefined;
const workspaceFolderPath = workspaceFolder && workspaceFolder.uri.scheme === 'file' ? workspaceFolder.uri.fsPath : undefined;
for (const entry of workingDirectories) {
let directory: string | undefined;
let pattern: string | undefined;
let noCWD = false;
if (Is.string(entry)) {
directory = entry;
} else if (LegacyDirectoryItem.is(entry)) {
directory = entry.directory;
noCWD = !entry.changeProcessCWD;
} else if (DirectoryItem.is(entry)) {
directory = entry.directory;
if (entry['!cwd'] !== undefined) {
noCWD = entry['!cwd'];
}
} else if (PatternItem.is(entry)) {
pattern = entry.pattern;
if (entry['!cwd'] !== undefined) {
noCWD = entry['!cwd'];
}
} else if (ModeItem.is(entry)) {
workingDirectory = entry;
continue;
}
let itemValue: string | undefined;
if (directory !== undefined || pattern !== undefined) {
const filePath = document.uri.scheme === 'file' ? document.uri.fsPath : undefined;
if (filePath !== undefined) {
if (directory !== undefined) {
directory = toOSPath(directory);
if (!path.isAbsolute(directory) && workspaceFolderPath !== undefined) {
directory = path.join(workspaceFolderPath, directory);
}
if (directory.charAt(directory.length - 1) !== path.sep) {
directory = directory + path.sep;
}
if (filePath.startsWith(directory)) {
itemValue = directory;
}
} else if (pattern !== undefined && pattern.length > 0) {
if (!path.posix.isAbsolute(pattern) && workspaceFolderPath !== undefined) {
pattern = path.posix.join(toPosixPath(workspaceFolderPath), pattern);
}
if (pattern.charAt(pattern.length - 1) !== path.posix.sep) {
pattern = pattern + path.posix.sep;
}
const regExp: RegExp | undefined = convert2RegExp(pattern);
if (regExp !== undefined) {
const match = regExp.exec(filePath);
if (match !== null && match.length > 0) {
itemValue = match[0];
}
}
}
}
}
if (itemValue !== undefined) {
if (workingDirectory === undefined || ModeItem.is(workingDirectory)) {
workingDirectory = { directory: itemValue, '!cwd': noCWD };
} else {
if (workingDirectory.directory.length < itemValue.length) {
workingDirectory.directory = itemValue;
workingDirectory['!cwd'] = noCWD;
}
}
}
}
settings.workingDirectory = workingDirectory;
}
result.push(settings);
}
return result;
}
}
}
};
let client: LanguageClient;
try {
client = new LanguageClient('ESLint', serverOptions, clientOptions);
} catch (err) {
void Window.showErrorMessage(`The ESLint extension couldn't be started. See the ESLint output channel for details.`);
return;
}
// Currently we don't need any proposed features.
// client.registerProposedFeatures();
Workspace.onDidChangeConfiguration(() => {
probeFailed.clear();
for (const textDocument of syncedDocuments.values()) {
if (computeValidate(textDocument) === Validate.off) {
try {
const provider = client.getFeature(DidCloseTextDocumentNotification.method).getProvider(textDocument);
provider?.send(textDocument);
} catch (err) {
// A feature currently throws if no provider can be found. So for now we catch the exception.
}
}
}
for (const textDocument of Workspace.textDocuments) {
if (!syncedDocuments.has(textDocument.uri.toString()) && computeValidate(textDocument) !== Validate.off) {
try {
const provider = client.getFeature(DidOpenTextDocumentNotification.method).getProvider(textDocument);
provider?.send(textDocument);
} catch (err) {
// A feature currently throws if no provider can be found. So for now we catch the exception.
}
}
}
});
defaultErrorHandler = client.createDefaultErrorHandler();
client.onDidChangeState((event) => {
if (event.newState === ClientState.Starting) {
client.info('ESLint server is starting');
serverRunning = undefined;
} else if (event.newState === ClientState.Running) {
client.info(running);
serverRunning = true;
} else {
client.info(stopped);
serverRunning = false;
}
updateStatusBar(undefined);
});
const readyHandler = () => {
client.onNotification(ShowOutputChannel.type, () => {
client.outputChannel.show();
});
client.onNotification(StatusNotification.type, (params) => {
updateDocumentStatus(params);
});
client.onNotification(exitCalled, (params) => {
serverCalledProcessExit = true;
client.error(`Server process exited with code ${params[0]}. This usually indicates a misconfigured ESLint setup.`, params[1]);
void Window.showErrorMessage(`ESLint server shut down itself. See 'ESLint' output channel for details.`, { title: 'Open Output', id: 1}).then((value) => {
if (value !== undefined && value.id === 1) {
client.outputChannel.show();
}
});
});
client.onRequest(NoConfigRequest.type, (params) => {
const document = Uri.parse(params.document.uri);
const workspaceFolder = Workspace.getWorkspaceFolder(document);
const fileLocation = document.fsPath;
if (workspaceFolder) {
client.warn([
'',
`No ESLint configuration (e.g .eslintrc) found for file: ${fileLocation}`,
`File will not be validated. Consider running 'eslint --init' in the workspace folder ${workspaceFolder.name}`,
`Alternatively you can disable ESLint by executing the 'Disable ESLint' command.`
].join('\n'));
} else {
client.warn([
'',
`No ESLint configuration (e.g .eslintrc) found for file: ${fileLocation}`,
`File will not be validated. Alternatively you can disable ESLint by executing the 'Disable ESLint' command.`
].join('\n'));
}
updateDocumentStatus({ uri: params.document.uri, state: Status.error });
return {};
});
client.onRequest(NoESLintLibraryRequest.type, (params) => {
const key = 'noESLintMessageShown';
const state = context.globalState.get<NoESLintState>(key, {});
const uri: Uri = Uri.parse(params.source.uri);
const workspaceFolder = Workspace.getWorkspaceFolder(uri);
const packageManager = Workspace.getConfiguration('eslint', uri).get('packageManager', 'npm');
const localInstall = {
npm: 'npm install eslint',
pnpm: 'pnpm install eslint',
yarn: 'yarn add eslint',
};
const globalInstall = {
npm: 'npm install -g eslint',
pnpm: 'pnpm install -g eslint',
yarn: 'yarn global add eslint'
};
const isPackageManagerNpm = packageManager === 'npm';
interface ButtonItem extends MessageItem {
id: number;
}
const outputItem: ButtonItem = {
title: 'Go to output',
id: 1
};
if (workspaceFolder) {
client.info([
'',
`Failed to load the ESLint library for the document ${uri.fsPath}`,
'',
`To use ESLint please install eslint by running ${localInstall[packageManager]} in the workspace folder ${workspaceFolder.name}`,
`or globally using '${globalInstall[packageManager]}'. You need to reopen the workspace after installing eslint.`,
'',
isPackageManagerNpm ? 'If you are using yarn or pnpm instead of npm set the setting `eslint.packageManager` to either `yarn` or `pnpm`' : null,
`Alternatively you can disable ESLint for the workspace folder ${workspaceFolder.name} by executing the 'Disable ESLint' command.`
].filter((str => (str !== null))).join('\n'));
if (state.workspaces === undefined) {
state.workspaces = {};
}
if (!state.workspaces[workspaceFolder.uri.toString()]) {
state.workspaces[workspaceFolder.uri.toString()] = true;
void context.globalState.update(key, state);
void Window.showInformationMessage(`Failed to load the ESLint library for the document ${uri.fsPath}. See the output for more information.`, outputItem).then((item) => {
if (item && item.id === 1) {
client.outputChannel.show(true);
}
});
}
} else {
client.info([
`Failed to load the ESLint library for the document ${uri.fsPath}`,
`To use ESLint for single JavaScript file install eslint globally using '${globalInstall[packageManager]}'.`,
isPackageManagerNpm ? 'If you are using yarn or pnpm instead of npm set the setting `eslint.packageManager` to either `yarn` or `pnpm`' : null,
'You need to reopen VS Code after installing eslint.',
].filter((str => (str !== null))).join('\n'));
if (!state.global) {
state.global = true;
void context.globalState.update(key, state);
void Window.showInformationMessage(`Failed to load the ESLint library for the document ${uri.fsPath}. See the output for more information.`, outputItem).then((item) => {
if (item && item.id === 1) {
client.outputChannel.show(true);
}
});
}
}
return {};
});
client.onRequest(OpenESLintDocRequest.type, async (params) => {
await Commands.executeCommand('vscode.open', Uri.parse(params.url));
return {};
});
client.onRequest(ProbeFailedRequest.type, (params) => {
probeFailed.add(params.textDocument.uri);
const closeFeature = client.getFeature(DidCloseTextDocumentNotification.method);
for (const document of Workspace.textDocuments) {
if (document.uri.toString() === params.textDocument.uri) {
closeFeature.getProvider(document)?.send(document);
}
}
});
};
client.onReady().then(readyHandler).catch((error) => client.error(`On ready failed`, error));
if (onActivateCommands) {
onActivateCommands.forEach(command => command.dispose());
onActivateCommands = undefined;
}
context.subscriptions.push(
client.start(),
Window.onDidChangeActiveTextEditor(() => {
updateStatusBar(undefined);
}),
Workspace.onDidCloseTextDocument((document) => {
const uri = document.uri.toString();
documentStatus.delete(uri);
updateStatusBar(undefined);
}),
Commands.registerCommand('eslint.executeAutofix', async () => {
const textEditor = Window.activeTextEditor;
if (!textEditor) {
return;
}
const textDocument: VersionedTextDocumentIdentifier = {
uri: textEditor.document.uri.toString(),
version: textEditor.document.version
};
const params: ExecuteCommandParams = {
command: 'eslint.applyAllFixes',
arguments: [textDocument]
};
await client.onReady();
client.sendRequest(ExecuteCommandRequest.type, params).then(undefined, () => {
void Window.showErrorMessage('Failed to apply ESLint fixes to the document. Please consider opening an issue with steps to reproduce.');
});
}),
Commands.registerCommand('eslint.showOutputChannel', async () => {
client.outputChannel.show();
}),
Commands.registerCommand('eslint.migrateSettings', () => {
void migrateSettings();
}),
Commands.registerCommand('eslint.restart', async () => {
await client.stop();
// Wait a little to free debugger port. Can not happen in production
// So we should add a dev flag.
const start = () => {
client.start();
client.onReady().then(readyHandler).catch((error) => client.error(`On ready failed`, error));
};
if (isInDebugMode()) {
setTimeout(start, 1000);
} else {
start();
}
})
);
}