atom-languageclient
Version:
Integrate Language Servers with Atom
223 lines (205 loc) • 8.82 kB
text/typescript
import type * as atomIde from "atom-ide-base"
import * as ls from "./languageclient"
import { Point, FilesystemChange, Range, TextEditor } from "atom"
// eslint-disable-next-line import/no-deprecated
import { diagnosticTypeToLSSeverity, atomIdeDiagnosticToLSDiagnostic } from "./adapters/diagnostic-adapter"
/**
* Public: Class that contains a number of helper methods for general conversions between the language server protocol
* and Atom/Atom packages.
*/
export default class Convert {
/**
* Public: Convert a path to a Uri.
*
* @param filePath A file path to convert to a Uri.
* @returns The Uri corresponding to the path. e.g. file:///a/b/c.txt
*/
public static pathToUri(filePath: string): string {
if (new URL(filePath, "file://").protocol !== "file:") {
return filePath
}
let newPath = filePath.replace(/\\/g, "/")
if (newPath[0] !== "/") {
newPath = `/${newPath}`
}
return encodeURI(`file://${newPath}`).replace(/[#?]/g, encodeURIComponent)
}
/**
* Public: Convert a Uri to a path.
*
* @param uri A Uri to convert to a file path.
* @returns A file path corresponding to the Uri. e.g. /a/b/c.txt If the Uri does not begin file: then it is returned
* as-is to allow Atom to deal with http/https sources in the future.
*/
public static uriToPath(uri: string): string {
const url = new URL(uri, "file://")
if (url.protocol !== "file:" || url.pathname == null) {
return uri
}
let filePath = decodeURIComponent(url.pathname)
if (process.platform === "win32") {
// Deal with Windows drive names
if (filePath[0] === "/") {
filePath = filePath.substr(1)
}
return filePath.replace(/\//g, "\\")
}
return filePath
}
/**
* Public: Convert an Atom {Point} to a language server {Position}.
*
* @param point An Atom {Point} to convert from.
* @returns The {Position} representation of the Atom {PointObject}.
*/
public static pointToPosition(point: Point): ls.Position {
return { line: point.row, character: point.column }
}
/**
* Public: Convert a language server {Position} into an Atom {PointObject}.
*
* @param position A language server {Position} to convert from.
* @returns The Atom {PointObject} representation of the given {Position}.
*/
public static positionToPoint(position: ls.Position): Point {
return new Point(position.line, position.character)
}
/**
* Public: Convert a language server {Range} into an Atom {Range}.
*
* @param range A language server {Range} to convert from.
* @returns The Atom {Range} representation of the given language server {Range}.
*/
public static lsRangeToAtomRange(range: ls.Range): Range {
return new Range(Convert.positionToPoint(range.start), Convert.positionToPoint(range.end))
}
/**
* Public: Convert an Atom {Range} into an language server {Range}.
*
* @param range An Atom {Range} to convert from.
* @returns The language server {Range} representation of the given Atom {Range}.
*/
public static atomRangeToLSRange(range: Range): ls.Range {
return {
start: Convert.pointToPosition(range.start),
end: Convert.pointToPosition(range.end),
}
}
/**
* Public: Create a {TextDocumentIdentifier} from an Atom {TextEditor}.
*
* @param editor A {TextEditor} that will be used to form the uri property.
* @returns A {TextDocumentIdentifier} that has a `uri` property with the Uri for the given editor's path.
*/
public static editorToTextDocumentIdentifier(editor: TextEditor): ls.TextDocumentIdentifier {
return { uri: Convert.pathToUri(editor.getPath() || "") }
}
/**
* Public: Create a {TextDocumentPositionParams} from a {TextEditor} and optional {Point}.
*
* @param editor A {TextEditor} that will be used to form the uri property.
* @param point An optional {Point} that will supply the position property. If not specified the current cursor
* position will be used.
* @returns A {TextDocumentPositionParams} that has textDocument property with the editors {TextDocumentIdentifier}
* and a position property with the supplied point (or current cursor position when not specified).
*/
public static editorToTextDocumentPositionParams(editor: TextEditor, point?: Point): ls.TextDocumentPositionParams {
return {
textDocument: Convert.editorToTextDocumentIdentifier(editor),
position: Convert.pointToPosition(point != null ? point : editor.getCursorBufferPosition()),
}
}
/**
* Public: Create a string of scopes for the atom text editor using the data-grammar selector from an {Array} of
* grammarScope strings.
*
* @param grammarScopes An {Array} of grammar scope string to convert from.
* @returns A single comma-separated list of CSS selectors targetting the grammars of Atom text editors. e.g. `['c',
* 'cpp']` => `'atom-text-editor[data-grammar='c'], atom-text-editor[data-grammar='cpp']`
*/
public static grammarScopesToTextEditorScopes(grammarScopes: string[]): string {
return grammarScopes
.map((g) => `atom-text-editor[data-grammar="${Convert.encodeHTMLAttribute(g.replace(/\./g, " "))}"]`)
.join(", ")
}
/**
* Public: Encode a string so that it can be safely used within a HTML attribute - i.e. replacing all quoted values
* with their HTML entity encoded versions. e.g. `Hello"` becomes `Hello"`
*
* @param s A string to be encoded.
* @returns A string that is HTML attribute encoded by replacing &, <, >, " and ' with their HTML entity named equivalents.
*/
public static encodeHTMLAttribute(s: string): string {
const attributeMap: { [key: string]: string } = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
}
return s.replace(/["&'<>]/g, (c) => attributeMap[c])
}
/**
* Public: Convert an Atom File Event as received from atom.project.onDidChangeFiles and convert it into an Array of
* Language Server Protocol {FileEvent} objects. Normally this will be a 1-to-1 but renames will be represented by a
* deletion and a subsequent creation as LSP does not know about renames.
*
* @param fileEvent An {atom$ProjectFileEvent} to be converted.
* @returns An array of LSP {ls.FileEvent} objects that equivalent conversions to the fileEvent parameter.
*/
public static atomFileEventToLSFileEvents(fileEvent: FilesystemChange): ls.FileEvent[] {
switch (fileEvent.action) {
case "created":
return [{ uri: Convert.pathToUri(fileEvent.path), type: ls.FileChangeType.Created }]
case "modified":
return [{ uri: Convert.pathToUri(fileEvent.path), type: ls.FileChangeType.Changed }]
case "deleted":
return [{ uri: Convert.pathToUri(fileEvent.path), type: ls.FileChangeType.Deleted }]
case "renamed": {
const results: Array<{ uri: string; type: ls.FileChangeType }> = []
if (fileEvent.oldPath) {
results.push({ uri: Convert.pathToUri(fileEvent.oldPath), type: ls.FileChangeType.Deleted })
}
if (fileEvent.path) {
results.push({ uri: Convert.pathToUri(fileEvent.path), type: ls.FileChangeType.Created })
}
return results
}
default:
return []
}
}
/** @deprecated Use Linter V2 service */
public static atomIdeDiagnosticToLSDiagnostic(diagnostic: atomIde.Diagnostic): ls.Diagnostic {
// eslint-disable-next-line import/no-deprecated
return atomIdeDiagnosticToLSDiagnostic(diagnostic)
}
/** @deprecated Use Linter V2 service */
public static diagnosticTypeToLSSeverity(type: atomIde.DiagnosticType): ls.DiagnosticSeverity {
// eslint-disable-next-line import/no-deprecated
return diagnosticTypeToLSSeverity(type)
}
/**
* Public: Convert an array of language server protocol {atomIde.TextEdit} objects to an equivalent array of Atom
* {atomIde.TextEdit} objects.
*
* @param textEdits The language server protocol {atomIde.TextEdit} objects to convert.
* @returns An {Array} of Atom {atomIde.TextEdit} objects.
*/
public static convertLsTextEdits(textEdits?: ls.TextEdit[] | null): atomIde.TextEdit[] {
return (textEdits || []).map(Convert.convertLsTextEdit)
}
/**
* Public: Convert a language server protocol {atomIde.TextEdit} object to the Atom equivalent {atomIde.TextEdit}.
*
* @param textEdits The language server protocol {atomIde.TextEdit} objects to convert.
* @returns An Atom {atomIde.TextEdit} object.
*/
public static convertLsTextEdit(textEdit: ls.TextEdit): atomIde.TextEdit {
// TODO: support annotations
return {
oldRange: Convert.lsRangeToAtomRange(textEdit.range),
newText: textEdit.newText,
}
}
}