Skip to content

Commit

Permalink
Merge pull request #139 from atom-community/code-actions
Browse files Browse the repository at this point in the history
  • Loading branch information
aminya authored May 14, 2021
2 parents 21aff92 + a548bdf commit 33887a6
Show file tree
Hide file tree
Showing 9 changed files with 400 additions and 113 deletions.
77 changes: 45 additions & 32 deletions lib/adapters/code-action-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import type * as atomIde from "atom-ide-base"
import * as linter from "atom/linter"
import LinterPushV2Adapter from "./linter-push-v2-adapter"
/* eslint-disable import/no-deprecated */
import IdeDiagnosticAdapter from "./diagnostic-adapter"
import assert = require("assert")
import Convert from "../convert"
import ApplyEditAdapter from "./apply-edit-adapter"
import {
CodeAction,
CodeActionParams,
Command,
Diagnostic,
LanguageClientConnection,
ServerCapabilities,
WorkspaceEdit,
} from "../languageclient"
import { Range, TextEditor } from "atom"
import CommandExecutionAdapter from "./command-execution-adapter"

export default class CodeActionAdapter {
/** @returns A {Boolean} indicating this adapter can adapt the server based on the given serverCapabilities. */
Expand All @@ -28,24 +31,24 @@ export default class CodeActionAdapter {
* @param serverCapabilities The {ServerCapabilities} of the language server that will be used.
* @param editor The Atom {TextEditor} containing the diagnostics.
* @param range The Atom {Range} to fetch code actions for.
* @param diagnostics An {Array<atomIde$Diagnostic>} to fetch code actions for. This is typically a list of
* diagnostics intersecting `range`.
* @param linterMessages An {Array<linter$Message>} to fetch code actions for. This is typically a list of messages
* intersecting `range`.
* @returns A {Promise} of an {Array} of {atomIde$CodeAction}s to display.
*/
public static async getCodeActions(
connection: LanguageClientConnection,
serverCapabilities: ServerCapabilities,
linterAdapter: LinterPushV2Adapter | undefined,
linterAdapter: LinterPushV2Adapter | IdeDiagnosticAdapter | undefined,
editor: TextEditor,
range: Range,
diagnostics: atomIde.Diagnostic[]
linterMessages: linter.Message[] | atomIde.Diagnostic[]
): Promise<atomIde.CodeAction[]> {
if (linterAdapter == null) {
return []
}
assert(serverCapabilities.codeActionProvider, "Must have the textDocument/codeAction capability")

const params = CodeActionAdapter.createCodeActionParams(linterAdapter, editor, range, diagnostics)
const params = createCodeActionParams(linterAdapter, editor, range, linterMessages)
const actions = await connection.codeAction(params)
if (actions === null) {
return []
Expand Down Expand Up @@ -81,34 +84,44 @@ export default class CodeActionAdapter {

private static async executeCommand(command: any, connection: LanguageClientConnection): Promise<void> {
if (Command.is(command)) {
await CommandExecutionAdapter.executeCommand(connection, command.command, command.arguments)
await connection.executeCommand({
command: command.command,
arguments: command.arguments,
})
}
}
}

private static createCodeActionParams(
linterAdapter: LinterPushV2Adapter,
editor: TextEditor,
range: Range,
diagnostics: atomIde.Diagnostic[]
): CodeActionParams {
return {
textDocument: Convert.editorToTextDocumentIdentifier(editor),
range: Convert.atomRangeToLSRange(range),
context: {
diagnostics: diagnostics.map((diagnostic) => {
// Retrieve the stored diagnostic code if it exists.
// Until the Linter API provides a place to store the code,
// there's no real way for the code actions API to give it back to us.
const converted = Convert.atomIdeDiagnosticToLSDiagnostic(diagnostic)
if (diagnostic.range != null && diagnostic.text != null) {
const code = linterAdapter.getDiagnosticCode(editor, diagnostic.range, diagnostic.text)
if (code != null) {
converted.code = code
}
}
return converted
}),
},
}
function createCodeActionParams(
linterAdapter: LinterPushV2Adapter | IdeDiagnosticAdapter,
editor: TextEditor,
range: Range,
linterMessages: linter.Message[] | atomIde.Diagnostic[]
): CodeActionParams {
let diagnostics: Diagnostic[]
if (linterMessages.length === 0) {
diagnostics = []
} else {
// TODO compile time dispatch using function names
diagnostics = areLinterMessages(linterMessages)
? linterAdapter.getLSDiagnosticsForMessages(linterMessages as linter.Message[])
: (linterAdapter as IdeDiagnosticAdapter).getLSDiagnosticsForIdeDiagnostics(
linterMessages as atomIde.Diagnostic[],
editor
)
}
return {
textDocument: Convert.editorToTextDocumentIdentifier(editor),
range: Convert.atomRangeToLSRange(range),
context: {
diagnostics,
},
}
}

function areLinterMessages(linterMessages: linter.Message[] | atomIde.Diagnostic[]): boolean {
if ("excerpt" in linterMessages[0]) {
return true
}
return false
}
117 changes: 117 additions & 0 deletions lib/adapters/diagnostic-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import * as atomIde from "atom-ide-base"
import * as atom from "atom"
import * as ls from "../languageclient"
import Convert from "../convert"
import LinterPushV2Adapter from "./linter-push-v2-adapter"

/** @deprecated Use Linter V2 service */
export type DiagnosticCode = number | string

/** @deprecated Use Linter V2 service */
export default class IdeDiagnosticAdapter extends LinterPushV2Adapter {
private _diagnosticCodes: Map<string, Map<string, DiagnosticCode | null>> = new Map()

/**
* Public: Capture the diagnostics sent from a langguage server, convert them to the Linter V2 format and forward them
* on to any attached {V2IndieDelegate}s.
*
* @deprecated Use Linter V2 service
* @param params The {PublishDiagnosticsParams} received from the language server that should be captured and
* forwarded on to any attached {V2IndieDelegate}s.
*/
public captureDiagnostics(params: ls.PublishDiagnosticsParams): void {
const path = Convert.uriToPath(params.uri)
const codeMap = new Map()
const messages = params.diagnostics.map((d) => {
const linterMessage = this.diagnosticToV2Message(path, d)
codeMap.set(getCodeKey(linterMessage.location.position, d.message), d.code)
return linterMessage
})
this._diagnosticMap.set(path, messages)
this._diagnosticCodes.set(path, codeMap)
this._indies.forEach((i) => i.setMessages(path, messages))
}

/**
* Public: get diagnostics for the given linter messages
*
* @deprecated Use Linter V2 service
* @param linterMessages An array of linter {V2Message}
* @param editor
* @returns An array of LS {Diagnostic[]}
*/
public getLSDiagnosticsForIdeDiagnostics(
diagnostics: atomIde.Diagnostic[],
editor: atom.TextEditor
): ls.Diagnostic[] {
return diagnostics.map((diagnostic) => this.getLSDiagnosticForIdeDiagnostic(diagnostic, editor))
}

/**
* Public: Get the {Diagnostic} that is associated with the given {atomIde.Diagnostic}.
*
* @deprecated Use Linter V2 service
* @param diagnostic The {atomIde.Diagnostic} object to fetch the {Diagnostic} for.
* @param editor
* @returns The associated {Diagnostic}.
*/
public getLSDiagnosticForIdeDiagnostic(diagnostic: atomIde.Diagnostic, editor: atom.TextEditor): ls.Diagnostic {
// Retrieve the stored diagnostic code if it exists.
// Until the Linter API provides a place to store the code,
// there's no real way for the code actions API to give it back to us.
const converted = atomIdeDiagnosticToLSDiagnostic(diagnostic)
if (diagnostic.range != null && diagnostic.text != null) {
const code = this.getDiagnosticCode(editor, diagnostic.range, diagnostic.text)
if (code != null) {
converted.code = code
}
}
return converted
}

/**
* Private: Get the recorded diagnostic code for a range/message. Diagnostic codes are tricky because there's no
* suitable place in the Linter API for them. For now, we'll record the original code for each range/message
* combination and retrieve it when needed (e.g. for passing back into code actions)
*/
private getDiagnosticCode(editor: atom.TextEditor, range: atom.Range, text: string): DiagnosticCode | null {
const path = editor.getPath()
if (path != null) {
const diagnosticCodes = this._diagnosticCodes.get(path)
if (diagnosticCodes != null) {
return diagnosticCodes.get(getCodeKey(range, text)) || null
}
}
return null
}
}

/** @deprecated Use Linter V2 service */
export function atomIdeDiagnosticToLSDiagnostic(diagnostic: atomIde.Diagnostic): ls.Diagnostic {
// TODO: support diagnostic codes and codeDescriptions
// TODO!: support data
return {
range: Convert.atomRangeToLSRange(diagnostic.range),
severity: diagnosticTypeToLSSeverity(diagnostic.type),
source: diagnostic.providerName,
message: diagnostic.text || "",
}
}

/** @deprecated Use Linter V2 service */
export function diagnosticTypeToLSSeverity(type: atomIde.DiagnosticType): ls.DiagnosticSeverity {
switch (type) {
case "Error":
return ls.DiagnosticSeverity.Error
case "Warning":
return ls.DiagnosticSeverity.Warning
case "Info":
return ls.DiagnosticSeverity.Information
default:
throw Error(`Unexpected diagnostic type ${type}`)
}
}

function getCodeKey(range: atom.Range, text: string): string {
return ([] as any[]).concat(...range.serialize(), text).join(",")
}
Loading

0 comments on commit 33887a6

Please sign in to comment.