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

feat: initial code actions backend for Linter service #139

Merged
merged 21 commits into from
May 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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