1 /* --------------------------------------------------------------------------------------------
2 * Copyright (c) Microsoft Corporation. All rights reserved.
3 * Licensed under the MIT License. See License.txt in the project root for license information.
4 * ------------------------------------------------------------------------------------------ */
7 import * as os from 'os'
8 import * as path from 'path'
9 import { Position, CancellationToken, CodeAction, CodeActionKind, CodeActionRequest, Command, createConnection, Diagnostic, DiagnosticSeverity, DidChangeConfigurationNotification, DidChangeWatchedFilesNotification, ErrorCodes, ExecuteCommandRequest, Files, IConnection, NotificationHandler, NotificationType, Range, RequestHandler, RequestType, ResponseError, TextDocumentIdentifier, TextDocuments, TextDocumentSaveReason, TextDocumentSyncKind, TextEdit, VersionedTextDocumentIdentifier, WorkspaceChange } from 'vscode-languageserver'
10 import { URI } from 'vscode-uri'
11 import { CLIOptions, ESLintAutoFixEdit, ESLintError, ESLintModule, ESLintProblem, ESLintReport, Is, TextDocumentSettings } from './types'
12 import { getAllFixEdits, executeInWorkspaceDirectory, getFilePath, isUNC, resolveModule } from './util'
13 import { TextDocument } from 'vscode-languageserver-textdocument'
14 declare var __webpack_require__: any
15 declare var __non_webpack_require__: any
16 const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require
18 namespace CommandIds {
19 export const applySingleFix = 'eslint.applySingleFix'
20 export const applySameFixes = 'eslint.applySameFixes'
21 export const applyAllFixes = 'eslint.applyAllFixes'
22 export const applyAutoFix = 'eslint.applyAutoFix'
23 export const applyDisableLine = 'eslint.applyDisableLine'
24 export const applyDisableFile = 'eslint.applyDisableFile'
25 export const openRuleDoc = 'eslint.openRuleDoc'
28 namespace OpenESLintDocRequest {
29 export const type = new RequestType<OpenESLintDocParams, OpenESLintDocResult, void, void>('eslint/openDoc')
32 interface OpenESLintDocParams {
36 interface OpenESLintDocResult {
45 interface StatusParams {
49 namespace StatusNotification {
50 export const type = new NotificationType<StatusParams, void>('eslint/status')
53 interface NoConfigParams {
55 document: TextDocumentIdentifier
58 interface NoConfigResult { }
60 namespace NoConfigRequest {
61 export const type = new RequestType<
69 interface NoESLintLibraryParams {
70 source: TextDocumentIdentifier
73 interface NoESLintLibraryResult { }
75 namespace NoESLintLibraryRequest {
76 export const type = new RequestType<
77 NoESLintLibraryParams,
78 NoESLintLibraryResult,
84 interface RuleCodeActions {
88 disableFile?: CodeAction
89 showDocumentation?: CodeAction
92 class CodeActionResult {
93 private _actions: Map<string, RuleCodeActions>
94 private _fixAll: CodeAction | undefined
96 public constructor() {
97 this._actions = new Map()
100 public get(ruleId: string): RuleCodeActions {
101 let result: RuleCodeActions = this._actions.get(ruleId)
102 if (result === undefined) {
103 result = { fixes: [] }
104 this._actions.set(ruleId, result)
109 public set fixAll(action: CodeAction) {
110 this._fixAll = action
113 public all(): CodeAction[] {
114 let result: CodeAction[] = []
115 for (let actions of this._actions.values()) {
116 result.push(...actions.fixes)
117 if (actions.disable) {
118 result.push(actions.disable)
120 if (actions.fixAll) {
121 result.push(actions.fixAll)
123 if (actions.disableFile) {
124 result.push(actions.disableFile)
126 if (actions.showDocumentation) {
127 result.push(actions.showDocumentation)
130 if (this._fixAll !== undefined) {
131 result.push(this._fixAll)
136 public get length(): number {
138 for (let actions of this._actions.values()) {
139 result += actions.fixes.length
145 function makeDiagnostic(problem: ESLintProblem): Diagnostic {
147 problem.ruleId != null
148 ? `${problem.message} (${problem.ruleId})`
149 : `${problem.message}`
150 let startLine = Math.max(0, problem.line - 1)
151 let startChar = Math.max(0, problem.column - 1)
153 problem.endLine != null ? Math.max(0, problem.endLine - 1) : startLine
155 problem.endColumn != null ? Math.max(0, problem.endColumn - 1) : startChar
158 severity: convertSeverity(problem.severity),
161 start: { line: startLine, character: startChar },
162 end: { line: endLine, character: endChar }
168 interface FixableProblem {
170 documentVersion: number
173 edit?: ESLintAutoFixEdit
176 function computeKey(diagnostic: Diagnostic): string {
177 let range = diagnostic.range
178 return `[${range.start.line},${range.start.character},${range.end.line},${
180 }]-${diagnostic.code}`
183 let codeActions: Map<string, Map<string, FixableProblem>> = new Map<
185 Map<string, FixableProblem>
187 function recordCodeAction(
188 document: TextDocument,
189 diagnostic: Diagnostic,
190 problem: ESLintProblem
192 if (!problem.ruleId) {
195 let uri = document.uri
196 let edits: Map<string, FixableProblem> = codeActions.get(uri)
198 edits = new Map<string, FixableProblem>()
199 codeActions.set(uri, edits)
201 edits.set(computeKey(diagnostic), { label: `Fix this ${problem.ruleId} problem`, documentVersion: document.version, ruleId: problem.ruleId, edit: problem.fix, line: problem.line })
204 function convertSeverity(severity: number): DiagnosticSeverity {
206 // Eslint 1 is warning
208 return DiagnosticSeverity.Warning
210 return DiagnosticSeverity.Error
212 return DiagnosticSeverity.Error
216 const exitCalled = new NotificationType<[number, string], void>(
220 const nodeExit = process.exit
221 process.exit = ((code?: number): void => {
222 let stack = new Error('stack')
223 connection.sendNotification(exitCalled, [code ? code : 0, stack.stack])
228 process.on('uncaughtException', (error: any) => {
231 if (typeof error.stack === 'string') {
232 message = error.stack
233 } else if (typeof error.message === 'string') {
234 message = error.message
235 } else if (typeof error === 'string') {
240 message = JSON.stringify(error, undefined, 4)
242 // Should not happen.
246 connection.console.error(`Uncaught exception recevied.
250 let connection = createConnection()
251 connection.console.info(`ESLint server running in node ${process.version}`)
252 let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument)
254 let _globalNpmPath: string | null | undefined
255 function globalNpmPath(): string {
256 if (_globalNpmPath === void 0) {
257 _globalNpmPath = Files.resolveGlobalNodePath(trace)
258 if (_globalNpmPath === void 0) {
259 _globalNpmPath = null
262 if (_globalNpmPath === null) {
265 return _globalNpmPath
267 let _globalYarnPath: string | undefined
268 function globalYarnPath(): string {
269 if (_globalYarnPath === void 0) {
270 _globalYarnPath = Files.resolveGlobalYarnPath(trace)
271 if (_globalYarnPath === void 0) {
272 _globalYarnPath = null
275 if (_globalYarnPath === null) {
278 return _globalYarnPath
280 let path2Library: Map<string, ESLintModule> = new Map<string, ESLintModule>()
281 let document2Settings: Map<string, Thenable<TextDocumentSettings>> = new Map<
283 Thenable<TextDocumentSettings>
288 urls: Map<string, string>
290 handled: new Set<string>(),
291 urls: new Map<string, string>()
294 function resolveSettings(
295 document: TextDocument
296 ): Thenable<TextDocumentSettings> {
297 let uri = document.uri
298 let resultPromise = document2Settings.get(uri)
302 resultPromise = connection.workspace
303 .getConfiguration({ scopeUri: uri, section: '' })
304 .then((settings: TextDocumentSettings) => {
306 if (settings.nodePath) {
307 nodePath = settings.nodePath
308 if (nodePath.startsWith('~')) {
309 nodePath = nodePath.replace(/^~/, os.homedir())
311 if (!path.isAbsolute(nodePath)) {
312 nodePath = path.join(URI.parse(settings.workspaceFolder.uri).fsPath, nodePath)
314 } else if (settings.packageManager === 'npm') {
315 nodePath = globalNpmPath()
316 } else if (settings.packageManager === 'yarn') {
317 nodePath = globalYarnPath()
319 let uri = URI.parse(document.uri)
320 let promise: Thenable<string>
321 let directory: string
322 if (uri.scheme === 'file') {
323 directory = path.dirname(uri.fsPath)
325 directory = settings.workspaceFolder ? URI.parse(settings.workspaceFolder.uri).fsPath : undefined
327 promise = resolveModule('./eslint', directory, nodePath).catch(() => {
328 return resolveModule('eslint', directory, nodePath)
330 return promise.then(path => {
331 let library = path2Library.get(path)
333 library = requireFunc(path)
334 if (!library.CLIEngine) {
335 settings.validate = false
336 connection.console.error(
337 `The eslint library loaded from ${path} doesn\'t export a CLIEngine. You need at least eslint@1.0.0`
340 connection.console.info(`ESLint library loaded from: ${path}`)
341 settings.library = library
343 path2Library.set(path, library)
345 settings.library = library
349 settings.validate = false
350 connection.sendRequest(NoESLintLibraryRequest.type, {
351 source: { uri: document.uri }
357 document2Settings.set(uri, resultPromise)
361 interface Request<P, R> {
364 documentVersion: number | undefined
365 resolve: (value: R | Thenable<R>) => void | undefined
366 reject: (error: any) => void | undefined
367 token: CancellationToken | undefined
371 export function is(value: any): value is Request<any, any> {
372 let candidate: Request<any, any> = value
376 !!candidate.resolve &&
382 interface Notifcation<P> {
385 documentVersion: number
388 type Message<P, R> = Notifcation<P> | Request<P, R>
390 type VersionProvider<P> = (params: P) => number
393 export function is<T>(value: any): value is Thenable<T> {
394 let candidate: Thenable<T> = value
395 return candidate && typeof candidate.then === 'function'
399 class BufferedMessageQueue {
400 private queue: Message<any, any>[]
401 private requestHandlers: Map<
404 handler: RequestHandler<any, any, any>
405 versionProvider?: VersionProvider<any>
408 private notificationHandlers: Map<
410 { handler: NotificationHandler<any>; versionProvider?: VersionProvider<any> }
412 private timer: NodeJS.Immediate | undefined
414 constructor(private connection: IConnection) {
416 this.requestHandlers = new Map()
417 this.notificationHandlers = new Map()
420 public registerRequest<P, R, E, RO>(
421 type: RequestType<P, R, E, RO>,
422 handler: RequestHandler<P, R, E>,
423 versionProvider?: VersionProvider<P>
425 this.connection.onRequest(type, (params, token) => {
426 return new Promise<R>((resolve, reject) => {
430 documentVersion: versionProvider
431 ? versionProvider(params)
440 this.requestHandlers.set(type.method, { handler, versionProvider })
443 public registerNotification<P, RO>(
444 type: NotificationType<P, RO>,
445 handler: NotificationHandler<P>,
446 versionProvider?: (params: P) => number
448 connection.onNotification(type, params => {
452 documentVersion: versionProvider ? versionProvider(params) : undefined
456 this.notificationHandlers.set(type.method, { handler, versionProvider })
459 public addNotificationMessage<P, RO>(
460 type: NotificationType<P, RO>,
467 documentVersion: version
472 public onNotification<P, RO>(
473 type: NotificationType<P, RO>,
474 handler: NotificationHandler<P>,
475 versionProvider?: (params: P) => number
477 this.notificationHandlers.set(type.method, { handler, versionProvider })
480 private trigger(): void {
481 if (this.timer || this.queue.length === 0) {
484 this.timer = setImmediate(() => {
485 this.timer = undefined
490 private processQueue(): void {
491 let message = this.queue.shift()
495 if (Request.is(message)) {
496 let requestMessage = message
497 if (requestMessage.token.isCancellationRequested) {
498 requestMessage.reject(
499 // tslint:disable-next-line: no-inferred-empty-object-type
501 ErrorCodes.RequestCancelled,
502 'Request got cancelled'
507 let elem = this.requestHandlers.get(requestMessage.method)
509 elem.versionProvider &&
510 requestMessage.documentVersion !== void 0 &&
511 requestMessage.documentVersion !==
512 elem.versionProvider(requestMessage.params)
514 requestMessage.reject(
515 // tslint:disable-next-line: no-inferred-empty-object-type
517 ErrorCodes.RequestCancelled,
518 'Request got cancelled'
523 let result = elem.handler(requestMessage.params, requestMessage.token)
524 if (Thenable.is(result)) {
527 requestMessage.resolve(value)
530 requestMessage.reject(error)
534 requestMessage.resolve(result)
537 let notificationMessage = message
538 let elem = this.notificationHandlers.get(notificationMessage.method)
540 elem.versionProvider &&
541 notificationMessage.documentVersion !== void 0 &&
542 notificationMessage.documentVersion !==
543 elem.versionProvider(notificationMessage.params)
547 elem.handler(notificationMessage.params)
553 let messageQueue: BufferedMessageQueue = new BufferedMessageQueue(connection)
555 namespace ValidateNotification {
556 export const type: NotificationType<
559 > = new NotificationType<TextDocument, void>('eslint/validate')
562 messageQueue.onNotification(
563 ValidateNotification.type,
565 validateSingle(document, true)
567 (document): number => {
568 return document.version
572 // The documents manager listen for text document create, change
573 // and close on the connection
574 documents.listen(connection)
575 documents.onDidOpen(event => {
576 resolveSettings(event.document).then(settings => {
577 if (!settings.validate) {
580 if (settings.run === 'onSave') {
581 messageQueue.addNotificationMessage(
582 ValidateNotification.type,
584 event.document.version
590 // A text document has changed. Validate the document according the run setting.
591 documents.onDidChangeContent(event => {
592 resolveSettings(event.document).then(settings => {
593 if (!settings.validate || settings.run !== 'onType') {
596 messageQueue.addNotificationMessage(
597 ValidateNotification.type,
599 event.document.version
604 documents.onWillSaveWaitUntil(event => {
605 if (event.reason === TextDocumentSaveReason.AfterDelay) {
609 let document = event.document
610 return resolveSettings(document).then(settings => {
611 if (!settings.autoFixOnSave) {
614 // If we validate on save and want to apply fixes on will save
615 // we need to validate the file.
616 if (settings.run === 'onSave') {
617 // Do not queue this since we want to get the fixes as fast as possible.
618 return validateSingle(document, false).then(() => getAllFixEdits(document, settings))
620 return getAllFixEdits(document, settings)
625 // A text document has been saved. Validate the document according the run setting.
626 documents.onDidSave(event => {
627 resolveSettings(event.document).then(settings => {
628 if (!settings.validate || settings.run !== 'onSave') {
631 messageQueue.addNotificationMessage(
632 ValidateNotification.type,
634 event.document.version
639 documents.onDidClose(event => {
640 resolveSettings(event.document).then(settings => {
641 let uri = event.document.uri
642 document2Settings.delete(uri)
643 codeActions.delete(uri)
644 if (settings.validate) {
645 connection.sendDiagnostics({ uri, diagnostics: [] })
650 function environmentChanged(): void {
651 document2Settings.clear()
652 for (let document of documents.all()) {
653 messageQueue.addNotificationMessage(
654 ValidateNotification.type,
661 function trace(message: string, verbose?: string): void {
662 connection.tracer.log(message, verbose)
665 connection.onInitialize(_params => {
670 change: TextDocumentSyncKind.Full,
671 willSaveWaitUntil: true,
676 codeActionProvider: true,
677 executeCommandProvider: {
679 CommandIds.applySingleFix,
680 CommandIds.applySameFixes,
681 CommandIds.applyAllFixes,
682 CommandIds.applyAutoFix,
683 CommandIds.applyDisableLine,
684 CommandIds.applyDisableFile,
685 CommandIds.openRuleDoc,
692 connection.onInitialized(() => {
693 connection.client.register(DidChangeConfigurationNotification.type, undefined)
696 messageQueue.registerNotification(
697 DidChangeConfigurationNotification.type,
703 // messageQueue.registerNotification(
704 // DidChangeWorkspaceFoldersNotification.type,
706 // environmentChanged()
710 const singleErrorHandlers: ((
712 document: TextDocument,
713 library: ESLintModule
716 tryHandleConfigError,
717 tryHandleMissingModule,
721 function validateSingle(
722 document: TextDocument,
723 publishDiagnostics = true
725 // We validate document in a queue but open / close documents directly. So we need to deal with the
726 // fact that a document might be gone from the server.
727 if (!documents.get(document.uri)) {
728 return Promise.resolve(undefined)
730 return resolveSettings(document).then(settings => {
731 if (!settings.validate) {
735 validate(document, settings, publishDiagnostics)
736 connection.sendNotification(StatusNotification.type, { state: Status.ok })
739 for (let handler of singleErrorHandlers) {
740 status = handler(err, document, settings.library)
745 status = status || Status.error
746 connection.sendNotification(StatusNotification.type, { state: status })
751 function validateMany(documents: TextDocument[]): void {
752 documents.forEach(document => {
753 messageQueue.addNotificationMessage(
754 ValidateNotification.type,
761 function getMessage(err: any, document: TextDocument): string {
762 let result: string = null
763 if (typeof err.message === 'string' || err.message instanceof String) {
764 result = err.message as string
765 result = result.replace(/\r?\n/g, ' ')
766 if (/^CLI: /.test(result)) {
767 result = result.substr(5)
770 result = `An unknown error occured while validating document: ${
777 function validate(document: TextDocument, settings: TextDocumentSettings, publishDiagnostics = true): void {
778 const uri = document.uri
779 const content = document.getText()
780 const newOptions: CLIOptions = Object.assign(Object.create(null), settings.options)
781 executeInWorkspaceDirectory(document, settings, newOptions, (file: string, options: CLIOptions) => {
782 const cli = new settings.library.CLIEngine(options)
783 // Clean previously computed code actions.
784 codeActions.delete(uri)
785 const report: ESLintReport = cli.executeOnText(content, file)
786 const diagnostics: Diagnostic[] = []
787 if (report && report.results && Array.isArray(report.results) && report.results.length > 0) {
788 const docReport = report.results[0]
789 if (docReport.messages && Array.isArray(docReport.messages)) {
790 docReport.messages.forEach(problem => {
792 const isWarning = convertSeverity(problem.severity) === DiagnosticSeverity.Warning
793 if (settings.quiet && isWarning) {
794 // Filter out warnings when quiet mode is enabled
797 const diagnostic = makeDiagnostic(problem)
798 diagnostics.push(diagnostic)
799 if (settings.autoFix) {
800 if (typeof cli.getRules === 'function' && problem.ruleId !== undefined && problem.fix !== undefined) {
801 const rule = cli.getRules().get(problem.ruleId)
802 if (rule !== undefined && rule.meta && typeof rule.meta.fixable == 'string') {
803 recordCodeAction(document, diagnostic, problem)
806 recordCodeAction(document, diagnostic, problem)
813 if (publishDiagnostics) {
814 connection.sendDiagnostics({ uri, diagnostics })
817 // cache documentation urls for all rules
818 if (typeof cli.getRules === 'function' && !ruleDocData.handled.has(uri)) {
819 ruleDocData.handled.add(uri)
820 cli.getRules().forEach((rule, key) => {
821 if (rule.meta && rule.meta.docs && Is.string(rule.meta.docs.url)) {
822 ruleDocData.urls.set(key, rule.meta.docs.url)
829 let noConfigReported: Map<string, ESLintModule> = new Map<
834 function isNoConfigFoundError(error: any): boolean {
835 let candidate = error as ESLintError
837 candidate.messageTemplate === 'no-config-found' ||
838 candidate.message === 'No ESLint configuration found.'
842 function tryHandleNoConfig(
844 document: TextDocument,
845 library: ESLintModule
847 if (!isNoConfigFoundError(error)) {
850 if (!noConfigReported.has(document.uri)) {
852 .sendRequest(NoConfigRequest.type, {
853 message: getMessage(error, document),
858 .then(undefined, () => {
861 noConfigReported.set(document.uri, library)
866 let configErrorReported: Map<string, ESLintModule> = new Map<
871 function tryHandleConfigError(
873 document: TextDocument,
874 library: ESLintModule
876 if (!error.message) {
880 function handleFileName(filename: string): Status {
881 if (!configErrorReported.has(filename)) {
882 connection.console.error(getMessage(error, document))
883 if (!documents.get(URI.file(filename).toString())) {
884 connection.window.showInformationMessage(getMessage(error, document))
886 configErrorReported.set(filename, library)
891 let matches = /Cannot read config file:\s+(.*)\nError:\s+(.*)/.exec(
894 if (matches && matches.length === 3) {
895 return handleFileName(matches[1])
898 matches = /(.*):\n\s*Configuration for rule \"(.*)\" is /.exec(error.message)
899 if (matches && matches.length === 3) {
900 return handleFileName(matches[1])
903 matches = /Cannot find module '([^']*)'\nReferenced from:\s+(.*)/.exec(
906 if (matches && matches.length === 3) {
907 return handleFileName(matches[2])
913 let missingModuleReported: Map<string, ESLintModule> = new Map<
918 function tryHandleMissingModule(
920 document: TextDocument,
921 library: ESLintModule
923 if (!error.message) {
927 function handleMissingModule(
932 if (!missingModuleReported.has(plugin)) {
933 let fsPath = getFilePath(document)
934 missingModuleReported.set(plugin, library)
935 if (error.messageTemplate === 'plugin-missing') {
936 connection.console.error(
939 `${error.message.toString()}`,
940 `Happened while validating ${fsPath ? fsPath : document.uri}`,
941 `This can happen for a couple of reasons:`,
942 `1. The plugin name is spelled incorrectly in an ESLint configuration file (e.g. .eslintrc).`,
943 `2. If ESLint is installed globally, then make sure ${module} is installed globally as well.`,
944 `3. If ESLint is installed locally, then ${module} isn't installed correctly.`,
946 `Consider running eslint --debug ${
947 fsPath ? fsPath : document.uri
948 } from a terminal to obtain a trace about the configuration files used.`
952 connection.console.error(
954 `${error.message.toString()}`,
955 `Happend while validating ${fsPath ? fsPath : document.uri}`
963 let matches = /Failed to load plugin (.*): Cannot find module (.*)/.exec(
966 if (matches && matches.length === 3) {
967 return handleMissingModule(matches[1], matches[2], error)
973 function showErrorMessage(error: any, document: TextDocument): Status {
974 connection.window.showErrorMessage(
975 `ESLint: ${getMessage(
978 )}. Please see the 'ESLint' output channel for details.`
980 if (Is.string(error.stack)) {
981 connection.console.error('ESLint stack trace:')
982 connection.console.error(error.stack)
987 messageQueue.registerNotification(
988 DidChangeWatchedFilesNotification.type,
990 // A .eslintrc has change. No smartness here.
991 // Simply revalidate all file.
992 noConfigReported = new Map<string, ESLintModule>()
993 missingModuleReported = new Map<string, ESLintModule>()
994 params.changes.forEach(change => {
995 let fsPath = getFilePath(change.uri)
996 if (!fsPath || isUNC(fsPath)) {
999 let dirname = path.dirname(fsPath)
1001 let library = configErrorReported.get(fsPath)
1003 let cli = new library.CLIEngine({})
1005 cli.executeOnText('', path.join(dirname, '___test___.js'))
1006 configErrorReported.delete(fsPath)
1013 validateMany(documents.all())
1018 constructor(private edits: Map<string, FixableProblem>) { }
1020 public static overlaps(lastEdit: FixableProblem, newEdit: FixableProblem): boolean {
1021 return !!lastEdit && lastEdit.edit.range[1] > newEdit.edit.range[0]
1024 public isEmpty(): boolean {
1025 return this.edits.size === 0
1028 public getDocumentVersion(): number {
1029 if (this.isEmpty()) {
1030 throw new Error('No edits recorded.')
1032 return this.edits.values().next().value.documentVersion
1035 public getScoped(diagnostics: Diagnostic[]): FixableProblem[] {
1036 let result: FixableProblem[] = []
1037 for (let diagnostic of diagnostics) {
1038 let key = computeKey(diagnostic)
1039 let editInfo = this.edits.get(key)
1041 result.push(editInfo)
1047 public getAllSorted(): FixableProblem[] {
1048 let result: FixableProblem[] = []
1049 this.edits.forEach(value => result.push(value))
1050 return result.sort((a, b) => {
1051 let d = a.edit.range[0] - b.edit.range[0]
1055 if (a.edit.range[1] === 0) {
1058 if (b.edit.range[1] === 0) {
1061 return a.edit.range[1] - b.edit.range[1]
1065 public getOverlapFree(): FixableProblem[] {
1066 let sorted = this.getAllSorted()
1067 if (sorted.length <= 1) {
1070 let result: FixableProblem[] = []
1071 let last: FixableProblem = sorted[0]
1073 for (let i = 1; i < sorted.length; i++) {
1074 let current = sorted[i]
1075 if (!Fixes.overlaps(last, current)) {
1076 result.push(current)
1084 let commands: Map<string, WorkspaceChange>
1085 messageQueue.registerRequest(
1086 CodeActionRequest.type,
1088 commands = new Map<string, WorkspaceChange>()
1089 let result: CodeActionResult = new CodeActionResult()
1090 let uri = params.textDocument.uri
1091 let edits = codeActions.get(uri)
1092 if (!edits) return []
1093 let fixes = new Fixes(edits)
1094 if (fixes.isEmpty()) return []
1096 let textDocument = documents.get(uri)
1097 let documentVersion = -1
1098 let allFixableRuleIds: string[] = []
1100 function createTextEdit(editInfo: FixableProblem): TextEdit {
1101 return TextEdit.replace(Range.create(textDocument.positionAt(editInfo.edit.range[0]), textDocument.positionAt(editInfo.edit.range[1])), editInfo.edit.text || '')
1104 function createDisableLineTextEdit(editInfo: FixableProblem, indentationText: string): TextEdit {
1105 return TextEdit.insert(Position.create(editInfo.line - 1, 0), `${indentationText}// eslint-disable-next-line ${editInfo.ruleId}\n`)
1108 function createDisableSameLineTextEdit(editInfo: FixableProblem): TextEdit {
1109 return TextEdit.insert(Position.create(editInfo.line - 1, Number.MAX_VALUE), ` // eslint-disable-line ${editInfo.ruleId}`)
1112 function createDisableFileTextEdit(editInfo: FixableProblem): TextEdit {
1113 return TextEdit.insert(Position.create(0, 0), `/* eslint-disable ${editInfo.ruleId} */\n`)
1116 function getLastEdit(array: FixableProblem[]): FixableProblem {
1117 let length = array.length
1121 return array[length - 1]
1124 return resolveSettings(textDocument).then(settings => {
1125 for (let editInfo of fixes.getScoped(params.context.diagnostics)) {
1126 documentVersion = editInfo.documentVersion
1127 let ruleId = editInfo.ruleId
1128 allFixableRuleIds.push(ruleId)
1130 if (!!editInfo.edit) {
1131 let workspaceChange = new WorkspaceChange()
1132 workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createTextEdit(editInfo))
1133 commands.set(`${CommandIds.applySingleFix}:${ruleId}`, workspaceChange)
1134 result.get(ruleId).fixes.push(CodeAction.create(
1136 Command.create(editInfo.label, CommandIds.applySingleFix, ruleId),
1137 CodeActionKind.QuickFix
1141 if (settings.codeAction.disableRuleComment.enable) {
1142 let workspaceChange = new WorkspaceChange()
1143 if (settings.codeAction.disableRuleComment.location === 'sameLine') {
1144 workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createDisableSameLineTextEdit(editInfo))
1146 let lineText = textDocument.getText(Range.create(Position.create(editInfo.line - 1, 0), Position.create(editInfo.line - 1, Number.MAX_VALUE)))
1147 let indentationText = /^([ \t]*)/.exec(lineText)[1]
1148 workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createDisableLineTextEdit(editInfo, indentationText))
1150 commands.set(`${CommandIds.applyDisableLine}:${ruleId}`, workspaceChange)
1151 let title = `Disable ${ruleId} for this line`
1152 result.get(ruleId).disable = CodeAction.create(
1154 Command.create(title, CommandIds.applyDisableLine, ruleId),
1155 CodeActionKind.QuickFix
1158 if (result.get(ruleId).disableFile === undefined) {
1159 workspaceChange = new WorkspaceChange()
1160 workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createDisableFileTextEdit(editInfo))
1161 commands.set(`${CommandIds.applyDisableFile}:${ruleId}`, workspaceChange)
1162 title = `Disable ${ruleId} for the entire file`
1163 result.get(ruleId).disableFile = CodeAction.create(
1165 Command.create(title, CommandIds.applyDisableFile, ruleId),
1166 CodeActionKind.QuickFix
1171 if (settings.codeAction.showDocumentation.enable && result.get(ruleId).showDocumentation === undefined) {
1172 if (ruleDocData.urls.has(ruleId)) {
1173 let title = `Show documentation for ${ruleId}`
1174 result.get(ruleId).showDocumentation = CodeAction.create(
1176 Command.create(title, CommandIds.openRuleDoc, ruleId),
1177 CodeActionKind.QuickFix
1183 if (result.length > 0) {
1184 let sameProblems: Map<string, FixableProblem[]> = new Map<string, FixableProblem[]>(allFixableRuleIds.map<[string, FixableProblem[]]>(s => [s, []]))
1185 let all: FixableProblem[] = []
1187 for (let editInfo of fixes.getAllSorted()) {
1188 if (documentVersion === -1) {
1189 documentVersion = editInfo.documentVersion
1191 if (sameProblems.has(editInfo.ruleId)) {
1192 let same = sameProblems.get(editInfo.ruleId)
1193 if (!Fixes.overlaps(getLastEdit(same), editInfo)) {
1197 if (!Fixes.overlaps(getLastEdit(all), editInfo)) {
1201 sameProblems.forEach((same, ruleId) => {
1202 if (same.length > 1) {
1203 let sameFixes: WorkspaceChange = new WorkspaceChange()
1204 let sameTextChange = sameFixes.getTextEditChange({ uri, version: documentVersion })
1205 same.map(createTextEdit).forEach(edit => sameTextChange.add(edit))
1206 commands.set(CommandIds.applySameFixes, sameFixes)
1207 let title = `Fix all ${ruleId} problems`
1208 let command = Command.create(title, CommandIds.applySameFixes)
1209 result.get(ruleId).fixAll = CodeAction.create(
1212 CodeActionKind.QuickFix
1216 if (all.length > 1) {
1217 let allFixes: WorkspaceChange = new WorkspaceChange()
1218 let allTextChange = allFixes.getTextEditChange({ uri, version: documentVersion })
1219 all.map(createTextEdit).forEach(edit => allTextChange.add(edit))
1220 commands.set(CommandIds.applyAllFixes, allFixes)
1221 let title = `Fix all auto-fixable problems`
1222 let command = Command.create(title, CommandIds.applyAllFixes)
1223 result.fixAll = CodeAction.create(
1226 CodeActionKind.SourceFixAll
1233 (params): number => {
1234 let document = documents.get(params.textDocument.uri)
1235 return document ? document.version : undefined
1239 messageQueue.registerRequest(
1240 ExecuteCommandRequest.type,
1242 let workspaceChange: WorkspaceChange
1243 if (params.command === CommandIds.applyAutoFix) {
1244 let identifier: VersionedTextDocumentIdentifier = params.arguments[0]
1245 if (!identifier.uri.startsWith('file:')) {
1248 let textDocument = documents.get(identifier.uri)
1249 let settings = await Promise.resolve(resolveSettings(textDocument))
1250 let edits = getAllFixEdits(textDocument, settings)
1251 if (edits && edits.length) {
1252 workspaceChange = new WorkspaceChange()
1253 let textChange = workspaceChange.getTextEditChange(identifier)
1254 edits.forEach(edit => textChange.add(edit))
1257 if ([CommandIds.applySingleFix, CommandIds.applyDisableLine, CommandIds.applyDisableFile].indexOf(params.command) !== -1) {
1258 let ruleId = params.arguments[0]
1259 workspaceChange = commands.get(`${params.command}:${ruleId}`)
1260 } else if (params.command === CommandIds.openRuleDoc) {
1261 let ruleId = params.arguments[0]
1262 let url = ruleDocData.urls.get(ruleId)
1264 await connection.sendRequest(OpenESLintDocRequest.type, { url })
1267 workspaceChange = commands.get(params.command)
1271 if (!workspaceChange) {
1275 let response = await Promise.resolve(connection.workspace.applyEdit(workspaceChange.edit))
1276 if (!response.applied) {
1277 connection.console.error(`Failed to apply command: ${params.command}`)
1280 connection.console.error(`Failed to apply command: ${params.command}`)
1284 (params): number => {
1285 if (params.command === CommandIds.applyAutoFix) {
1286 let identifier: VersionedTextDocumentIdentifier = params.arguments[0]
1287 return identifier.version
1294 connection.tracer.connection.listen()