Skip to content

Commit

Permalink
LSP: Go to definition
Browse files Browse the repository at this point in the history
Implement goto definition in the language service, for variables and
standalone identifiers (much like with hover, there's still a lot of
places that need to have identifier metadata emitted during
typechecking). As those places are updated, they will work for hover as
well as goto definition, which is a big upside!
This also refactors the `Some(...)` AST transformation to use a bogus
Position, so hover and goto definition work for `Some(...)` identifiers.
  • Loading branch information
kengorab committed Dec 24, 2024
1 parent 7565eaa commit 37b1a31
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 36 deletions.
3 changes: 2 additions & 1 deletion projects/compiler/src/parser.abra
Original file line number Diff line number Diff line change
Expand Up @@ -1700,10 +1700,11 @@ export type Parser {
_ => false
}
val invokee = if isSome {
val bogusPosition = Position(line: 0, col: 0)
AstNode(
token: token,
kind: AstNodeKind.Accessor(AccessorAstNode(
root: AstNode(token: Token(position: token.position, kind: TokenKind.Ident("Option")), kind: AstNodeKind.Identifier(IdentifierKind.Named("Option"))),
root: AstNode(token: Token(position: bogusPosition, kind: TokenKind.Ident("Option")), kind: AstNodeKind.Identifier(IdentifierKind.Named("Option"))),
path: [(Token(position: token.position, kind: TokenKind.Dot), Label(name: "Some", position: token.position))]
))
)
Expand Down
23 changes: 20 additions & 3 deletions projects/compiler/src/typechecker.abra
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,16 @@ export enum IdentifierKindMeta {
Type(isEnum: Bool, typeParams: String[])
}

export enum IdentifierMetaImport {
Prelude
Module(filePath: String)
}

export type IdentifierMeta {
name: String
kind: IdentifierKindMeta
importedFrom: String? = None
importedFrom: IdentifierMetaImport? = None
definitionPosition: Position? = None
}

export type TypedModule {
Expand Down Expand Up @@ -3058,6 +3064,7 @@ export type Typechecker {
val ident = IdentifierMeta(
name: label.name,
kind: IdentifierKindMeta.Variable(mutable: mutable, typeRepr: ty.repr()),
definitionPosition: Some(label.position),
)

val v = (label.position.col - 1, label.position.col + label.name.length - 1, ident)
Expand Down Expand Up @@ -4023,10 +4030,11 @@ export type Typechecker {
}
IdentifierKind.Discard => return Err(TypeError(position: token.position, kind: TypeErrorKind.UnknownName("_", "variable")))
IdentifierKind.None_ => {
val bogusPosition = Position(line: 0, col: 0)
val replacement = AstNode(
token: token,
kind: AstNodeKind.Accessor(AccessorAstNode(
root: AstNode(token: Token(position: token.position, kind: TokenKind.Ident("Option")), kind: AstNodeKind.Identifier(IdentifierKind.Named("Option"))),
root: AstNode(token: Token(position: bogusPosition, kind: TokenKind.Ident("Option")), kind: AstNodeKind.Identifier(IdentifierKind.Named("Option"))),
path: [(Token(position: token.position, kind: TokenKind.Dot), Label(name: "None", position: token.position))]
))
)
Expand Down Expand Up @@ -4067,10 +4075,19 @@ export type Typechecker {
VariableAlias.Enum(_enum) => IdentifierKindMeta.Type(isEnum: true, typeParams: _enum.typeParams)
}

val importedFrom = if varImportMod |mod| {
Some(IdentifierMetaImport.Module(mod.name))
} else if variable.scope == self.project.preludeScope {
Some(IdentifierMetaImport.Prelude)
} else {
None
}

val ident = IdentifierMeta(
name: name,
kind: kind,
importedFrom: varImportMod?.name,
importedFrom: importedFrom,
definitionPosition: Some(variable.label.position),
)

val v = (token.position.col - 1, token.position.col + name.length - 1, ident)
Expand Down
97 changes: 66 additions & 31 deletions projects/lsp/src/language_service.abra
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import "fs" as fs
import JsonValue from "json"
import log from "./log"
import ModuleLoader, Project, Typechecker, TypecheckerErrorKind, IdentifierKindMeta from "../../compiler/src/typechecker"
import ModuleLoader, Project, Typechecker, TypecheckerErrorKind, IdentifierMeta, IdentifierKindMeta, IdentifierMetaImport from "../../compiler/src/typechecker"
import RequestMessage, NotificationMessage, ResponseMessage, ResponseResult, ResponseError, ResponseErrorCode, ServerCapabilities, TextDocumentSyncOptions, TextDocumentSyncKind, SaveOptions, ServerInfo, TextDocumentItem, TextDocumentIdentifier, VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent, Diagnostic, DiagnosticSeverity, Position, Range, MarkupContent, MarkupKind from "./lsp_spec"

export val contentLengthHeader = "Content-Length: "
Expand Down Expand Up @@ -49,6 +49,7 @@ export type AbraLanguageService {
save: Some(SaveOptions(includeText: Some(false)))
)),
hoverProvider: Some(true),
definitionProvider: Some(true),
),
serverInfo: ServerInfo(name: "abra-lsp", version: Some("0.0.1"))
)
Expand All @@ -58,43 +59,62 @@ export type AbraLanguageService {
func _hover(self, id: Int, textDocument: TextDocumentIdentifier, position: Position): ResponseMessage {
// todo: what happens if it's not a `file://` uri?
val filePath = textDocument.uri.replaceAll("file://", "")
val module = if self._project.modules[filePath] |mod| mod else return ResponseMessage.Success(id: id, result: None)
val line = position.line
val identsByLine = if module.identsByLine[line] |idents| idents else return ResponseMessage.Success(id: id, result: None)

for (colStart, colEnd, ident) in identsByLine {
if colStart <= position.character && position.character <= colEnd {
val message = match ident.kind {
IdentifierKindMeta.Variable(mutable, typeRepr) => {
val prefix = if mutable "var" else "val"
"$prefix ${ident.name}: $typeRepr"
}
IdentifierKindMeta.Function(typeParams, params, returnTypeRepr) => {
val generics = if typeParams.isEmpty() "" else "<${typeParams.join(", ")}>"
"func ${ident.name}$generics(${params.join(", ")}): $returnTypeRepr"
}
IdentifierKindMeta.Type(isEnum, typeParams) => {
val prefix = if isEnum "enum" else "type"
val generics = if typeParams.isEmpty() "" else "<${typeParams.join(", ")}>"
"$prefix ${ident.name}$generics"
}
}
val lines = ["```abra", message, "```"]
val (line, identColStart, identColEnd, ident) = if self._findIdentAtPosition(filePath, position) |ident| ident else {
return ResponseMessage.Success(id: id, result: None)
}

if ident.importedFrom |modName| {
lines.push("Imported from `$modName`")
}
val message = match ident.kind {
IdentifierKindMeta.Variable(mutable, typeRepr) => {
val prefix = if mutable "var" else "val"
"$prefix ${ident.name}: $typeRepr"
}
IdentifierKindMeta.Function(typeParams, params, returnTypeRepr) => {
val generics = if typeParams.isEmpty() "" else "<${typeParams.join(", ")}>"
"func ${ident.name}$generics(${params.join(", ")}): $returnTypeRepr"
}
IdentifierKindMeta.Type(isEnum, typeParams) => {
val prefix = if isEnum "enum" else "type"
val generics = if typeParams.isEmpty() "" else "<${typeParams.join(", ")}>"
"$prefix ${ident.name}$generics"
}
}
val lines = ["```abra", message, "```"]

if ident.importedFrom |modName| {
lines.push("Imported from `$modName`")
}

val value = lines.join("\\n")
val value = lines.join("\\n")

val range = Range(start: Position(line: line, character: colStart), end: Position(line: line, character: colEnd))
val contents = MarkupContent(kind: MarkupKind.Markdown, value: value)
val result = ResponseResult.Hover(contents: contents, range: Some(range))
return ResponseMessage.Success(id: id, result: Some(result))
val range = Range(start: Position(line: line, character: identColStart), end: Position(line: line, character: identColEnd))
val contents = MarkupContent(kind: MarkupKind.Markdown, value: value)
val result = ResponseResult.Hover(contents: contents, range: Some(range))
ResponseMessage.Success(id: id, result: Some(result))
}

func _goToDefinition(self, id: Int, textDocument: TextDocumentIdentifier, position: Position): ResponseMessage {
// todo: what happens if it's not a `file://` uri?
val filePath = textDocument.uri.replaceAll("file://", "")

val ident = if self._findIdentAtPosition(filePath, position) |(_, _, _, ident)| ident else return ResponseMessage.Success(id: id, result: None)
val result = if ident.definitionPosition |pos| {
val line = pos.line - 1
val character = pos.col - 1
val range = Range(start: Position(line: line, character: character), end: Position(line: line, character: character))
// val uri = if ident.importedFrom |mod| "file://$mod" else textDocument.uri
val definitionFilePath = match ident.importedFrom {
None => filePath
IdentifierMetaImport.Prelude => self._moduleLoader.stdRoot + "/prelude.abra"
IdentifierMetaImport.Module(mod) => mod
}
val uri = "file://$definitionFilePath"
Some(ResponseResult.Definition(uri: uri, range: range))
} else {
None
}

ResponseMessage.Success(id: id, result: None)
ResponseMessage.Success(id: id, result: result)
}

// Notification handlers
Expand Down Expand Up @@ -192,12 +212,27 @@ export type AbraLanguageService {
}
}

func _findIdentAtPosition(self, filePath: String, position: Position): (Int, Int, Int, IdentifierMeta)? {
val module = if self._project.modules[filePath] |mod| mod else return None
val line = position.line
val identsByLine = if module.identsByLine[line] |idents| idents else return None

for (colStart, colEnd, ident) in identsByLine {
if colStart <= position.character && position.character <= colEnd {
return Some((line, colStart, colEnd, ident))
}
}

None
}

// Dispatch

func handleRequest(self, req: RequestMessage): ResponseMessage {
match req {
RequestMessage.Initialize(id, processId, rootPath) => self._initialize(id, processId, rootPath)
RequestMessage.Hover(id, textDocument, position) => self._hover(id, textDocument, position)
RequestMessage.Definition(id, textDocument, position) => self._goToDefinition(id, textDocument, position)
}
}

Expand Down
27 changes: 26 additions & 1 deletion projects/lsp/src/lsp_spec.abra
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import JsonValue, JsonError, JsonObject from "json"
export enum RequestMessage {
Initialize(id: Int, processId: Int?, rootPath: String?)
Hover(id: Int, textDocument: TextDocumentIdentifier, position: Position)
Definition(id: Int, textDocument: TextDocumentIdentifier, position: Position)

func fromJson(json: JsonValue): Result<RequestMessage?, JsonError> {
val obj = try json.asObject()
Expand Down Expand Up @@ -36,6 +37,16 @@ export enum RequestMessage {

Ok(Some(RequestMessage.Hover(id: id, textDocument: textDocument, position: position)))
}
"textDocument/definition" => {
val params = try obj.getObjectRequired("params")
val textDocumentObj = try params.getValueRequired("textDocument")
val textDocument = try TextDocumentIdentifier.fromJson(textDocumentObj)

val positionObj = try params.getValueRequired("position")
val position = try Position.fromJson(positionObj)

Ok(Some(RequestMessage.Definition(id: id, textDocument: textDocument, position: position)))
}
else => {
log.writeln("Error: Unimplemented RequestMessage method '$method'")

Expand Down Expand Up @@ -140,13 +151,16 @@ export enum ResponseMessage {
export enum ResponseResult {
Initialize(capabilities: ServerCapabilities, serverInfo: ServerInfo)
Hover(contents: MarkupContent, range: Range?)
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition
// Note: the actual response body for textDocument/definition requests is just a `Location` type, which is flattened here
Definition(uri: String, range: Range)

func toJson(self): JsonValue {
val obj = match self {
ResponseResult.Initialize(capabilities, serverInfo) => {
JsonObject(_map: {
"capabilities": capabilities.toJson(),
"serverInfo": serverInfo.toJson()
"serverInfo": serverInfo.toJson(),
})
}
ResponseResult.Hover(contents, range) => {
Expand All @@ -160,6 +174,12 @@ export enum ResponseResult {

obj
}
ResponseResult.Definition(uri, range) => {
JsonObject(_map: {
"uri": JsonValue.String(uri),
"range": range.toJson(),
})
}
}

JsonValue.Object(obj)
Expand Down Expand Up @@ -212,6 +232,7 @@ export type ServerCapabilities {
textDocumentSync: TextDocumentSyncOptions? = None
diagnosticProvider: DiagnosticOptions? = None
hoverProvider: Bool? = None
definitionProvider: Bool? = None

func toJson(self): JsonValue {
val obj = JsonObject()
Expand All @@ -228,6 +249,10 @@ export type ServerCapabilities {
obj.set("hoverProvider", JsonValue.Boolean(hp))
}

if self.definitionProvider |dp| {
obj.set("definitionProvider", JsonValue.Boolean(dp))
}

JsonValue.Object(obj)
}
}
Expand Down

0 comments on commit 37b1a31

Please sign in to comment.