Skip to content

Commit

Permalink
chore: wip [skip ci]
Browse files Browse the repository at this point in the history
  • Loading branch information
aminya committed Mar 19, 2021
1 parent 797a173 commit e43063c
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 30 deletions.
37 changes: 15 additions & 22 deletions lib/adapters/code-action-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type * as atomIde from "atom-ide-base"
import * as linter from "atom/linter"
import LinterPushV2Adapter from "./linter-push-v2-adapter"
import assert = require("assert")
import Convert from "../convert"
Expand All @@ -7,12 +8,12 @@ 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 {
/**
Expand All @@ -31,8 +32,8 @@ 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(
Expand All @@ -41,14 +42,14 @@ export default class CodeActionAdapter {
linterAdapter: LinterPushV2Adapter | undefined,
editor: TextEditor,
range: Range,
diagnostics: atomIde.Diagnostic[]
linterMessages: linter.Message[]
): 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 = CodeActionAdapter.createCodeActionParams(linterAdapter, editor, range, linterMessages)
const actions = await connection.codeAction(params)
return actions.map((action) => CodeActionAdapter.createCodeAction(action, connection))
}
Expand Down Expand Up @@ -81,34 +82,26 @@ 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[]
linterMessages: linter.Message[]
): CodeActionParams {
const diagnostics = linterMessages
.map(linterAdapter.getLSDiagnostic)
.filter((diagnostic): diagnostic is Diagnostic => diagnostic != null)
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
}),
},
context: { diagnostics },
}
}
}
25 changes: 19 additions & 6 deletions lib/adapters/linter-push-v2-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import {

/**
* Public: Listen to diagnostics messages from the language server and publish them
* to the user by way of the Linter Push (Indie) v2 API supported by Atom IDE UI.
* to the user by way of the Linter Push (Indie) v2 API provided by the Base Linter package.
*/
export default class LinterPushV2Adapter {
private _diagnosticMap: Map<string, linter.Message[]> = new Map()
private _diagnosticCodes: Map<string, Map<string, DiagnosticCode | null>> = new Map()
private _diagnosticCodes: Map<string, Map<string, DiagnosticCode | null>> = new Map() // REMOVE
private _lsDiagnosticMap: Map<string, Map<string, Diagnostic>> = new Map()
private _indies: Set<linter.IndieDelegate> = new Set()

/**
Expand Down Expand Up @@ -61,14 +62,14 @@ export default class LinterPushV2Adapter {
*/
public captureDiagnostics(params: PublishDiagnosticsParams): void {
const path = Convert.uriToPath(params.uri)
const codeMap = new Map()
const codeMap = new Map<string, Diagnostic>()
const messages = params.diagnostics.map((d) => {
const linterMessage = this.diagnosticToV2Message(path, d)
codeMap.set(getCodeKey(linterMessage.location.position, d.message), d.code)
const linterMessage = Convert.lsDiagnosticToV2Message(path, d)
codeMap.set(Convert.getV2MessageIdentifier(linterMessage), d)
return linterMessage
})
this._diagnosticMap.set(path, messages)
this._diagnosticCodes.set(path, codeMap)
this._lsDiagnosticMap.set(path, codeMap)
this._indies.forEach((i) => i.setMessages(path, messages))
}

Expand All @@ -92,6 +93,18 @@ export default class LinterPushV2Adapter {
}
}

/**
* Public: Get the {Diagnostic} that is associated with the given Base Linter v2 {Message}.
*
* It has to be stored separately because a {Message} object cannot hold all of the information
* that a {Diagnostic} provides, thus we store the original {Diagnostic} object.
* @param message The {Message} object to fetch the {Diagnostic} for.
* @returns The associated {Diagnostic}.
*/
public getLSDiagnostic(message: linter.Message): Diagnostic | undefined {
return this._lsDiagnosticMap.get(message.location.file)?.get(Convert.getV2MessageIdentifier(message))
}

/**
* Public: Convert a diagnostic severity number obtained from the language server into
* the textual equivalent for a Linter {V2Message}.
Expand Down
101 changes: 101 additions & 0 deletions lib/convert.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type * as atomIde from "atom-ide-base"
import * as linter from "atom/linter"
import * as ls from "./languageclient"
import { Point, FilesystemChange, Range, TextEditor } from "atom"

Expand Down Expand Up @@ -193,6 +194,31 @@ export default class Convert {
}
}

/**
* Public: Convert a single {Diagnostic} received from a language server into a single
* {Message} expected by the Linter V2 API.
*
* @param path A string representing the path of the file the diagnostic belongs to.
* @param diagnostics A {Diagnostic} object received from the language server.
* @returns A {Message} equivalent to the {Diagnostic} object supplied by the language server.
*/
public static lsDiagnosticToV2Message(path: string, diagnostic: ls.Diagnostic): linter.Message {
return {
location: {
file: path,
position: Convert.lsRangeToAtomRange(diagnostic.range),
},
reference: Convert.relatedInformationToReference(diagnostic.relatedInformation),
url: diagnostic.codeDescription?.href,
icon: Convert.iconForLSSeverity(diagnostic.severity ?? ls.DiagnosticSeverity.Error),
excerpt: diagnostic.message,
linterName: diagnostic.source,
severity: Convert.lsSeverityToV2MessageSeverity(diagnostic.severity ?? ls.DiagnosticSeverity.Error),
// BLOCKED: on steelbrain/linter#1722
solutions: undefined,
}
}

public static diagnosticTypeToLSSeverity(type: atomIde.DiagnosticType): ls.DiagnosticSeverity {
switch (type) {
case "Error":
Expand All @@ -206,6 +232,18 @@ export default class Convert {
}
}

/**
* Public: Construct an identifier for a Base Linter v2 Message.
*
* The identifier has the form: ${startRow},${startColumn},${endRow},${endColumn},${message}.
*
* @param message A {Message} object to serialize.
* @returns A string identifier.
*/
public static getV2MessageIdentifier(message: linter.Message): string {
return ([] as any[]).concat(...message.location.position.serialize(), message.excerpt).join(",")
}

/**
* Public: Convert an array of language server protocol {atomIde.TextEdit} objects to an
* equivalent array of Atom {atomIde.TextEdit} objects.
Expand All @@ -231,4 +269,67 @@ export default class Convert {
newText: textEdit.newText,
}
}

/**
* Convert a severity level of an LSP {Diagnostic} to that of a Base Linter v2 {Message}.
* Note: this conversion is lossy due to the v2 Message not being able to represent hints.
*
* @param severity A severity level of of an LSP {Diagnostic} to be converted.
* @returns A severity level a Base Linter v2 {Message}.
*/
private static lsSeverityToV2MessageSeverity(severity: ls.DiagnosticSeverity): linter.Message["severity"] {
switch (severity) {
case ls.DiagnosticSeverity.Error:
return "error"
case ls.DiagnosticSeverity.Warning:
return "warning"
case ls.DiagnosticSeverity.Information:
case ls.DiagnosticSeverity.Hint:
return "info"
default:
throw Error(`Unexpected diagnostic severity '${severity}'`)
}
}

/**
* Convert a diagnostic severity number obtained from the language server into an Octicon.
*
* @param severity A number representing the severity of the diagnostic.
* @returns An Octicon name.
*/
private static iconForLSSeverity(severity: ls.DiagnosticSeverity): string | undefined {
switch (severity) {
case ls.DiagnosticSeverity.Error:
return "stop"
case ls.DiagnosticSeverity.Warning:
return "warning"
case ls.DiagnosticSeverity.Information:
return "info"
case ls.DiagnosticSeverity.Hint:
return "light-bulb"
default:
return undefined
}
}

/**
* Convert the related information from a diagnostic into
* a reference point for a Linter {V2Message}.
*
* @param relatedInfo Several related information objects (only the first is used).
* @returns A value that is suitable for using as {V2Message}.reference.
*/
private static relatedInformationToReference(
relatedInfo: ls.DiagnosticRelatedInformation[] | undefined
): linter.Message["reference"] {
if (relatedInfo == null || relatedInfo.length === 0) {
return undefined
}

const location = relatedInfo[0].location
return {
file: Convert.uriToPath(location.uri),
position: Convert.lsRangeToAtomRange(location.range).start,
}
}
}
14 changes: 12 additions & 2 deletions lib/languageclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,13 +398,23 @@ export class LanguageClientConnection extends EventEmitter {
* Public: Send a `textDocument/codeAction` request.
*
* @param params The {CodeActionParams} identifying the document, range and context for the code action.
* @returns A {Promise} containing an {Array} of {Commands}s that can be performed against the given
* documents range.
* @returns A {Promise} containing an {Array} of {Command}s or {CodeAction}s that can be performed
* against the given documents range.
*/
public codeAction(params: lsp.CodeActionParams): Promise<Array<lsp.Command | lsp.CodeAction>> {
return this._sendRequest("textDocument/codeAction", params)
}

/**
* Public: Send a `codeAction/resolve` request.
*
* @param params The {CodeAction} whose properties (e.g. `edit`) are to be resolved.
* @returns A resolved {CodeAction} that can be applied immediately.
*/
public codeActionResolve(params: lsp.CodeAction): Promise<lsp.CodeAction> {
return this._sendRequest("codeAction/resolve", params)
}

/**
* Public: Send a `textDocument/codeLens` request.
*
Expand Down

0 comments on commit e43063c

Please sign in to comment.