diff --git a/projects/compiler/src/typechecker.abra b/projects/compiler/src/typechecker.abra index 00ff4c4c..e66f14b8 100644 --- a/projects/compiler/src/typechecker.abra +++ b/projects/compiler/src/typechecker.abra @@ -1,4 +1,5 @@ import "fs" as fs +import log from "../../lsp/src/log" import getAbsolutePath, resolveRelativePath from "./utils" import Lexer, LexerError, Token, TokenKind, Position from "./lexer" import Parser, ParsedModule, ParseError, AstNode, AstNodeKind, LiteralAstNode, UnaryAstNode, UnaryOp, BinaryAstNode, BinaryOp, BindingDeclarationNode, BindingPattern, TypeIdentifier, Label, IdentifierKind, FunctionDeclarationNode, FunctionParam, InvocationAstNode, InvocationArgument, TypeDeclarationNode, EnumDeclarationNode, EnumVariant, AccessorAstNode, IndexingMode, AssignOp, AssignmentMode, ImportNode, ImportKind, DecoratorNode, LambdaNode, MatchCase, MatchCaseKind from "./parser" @@ -88,6 +89,18 @@ type ImportedModule { imports: Map = {} } +export enum IdentifierKindMeta { + Variable(mutable: Bool, typeRepr: String) + Function(typeParams: String[], params: String[], returnTypeRepr: String) + Type(isEnum: Bool, typeParams: String[]) +} + +export type IdentifierMeta { + name: String + kind: IdentifierKindMeta + importedFrom: String? = None +} + export type TypedModule { id: Int name: String @@ -96,6 +109,7 @@ export type TypedModule { complete: Bool = false imports: Map = {} exports: Map = {} + identsByLine: Map = {} } export enum VariableAlias { @@ -1485,13 +1499,18 @@ export type Typechecker { isEnumContainerValueAllowed: Bool = false numLambdas: Int = 0 typecheckingBuiltin: BuiltinModule? = None + trackIdentsForLsp: Bool = false + identsByLine: Map = {} func typecheckEntrypoint(self, modulePathAbs: String): Result { val preludeModulePathSegs = getAbsolutePath(self.moduleLoader.stdRoot + "/prelude.abra") val preludeModulePathAbs = "/" + preludeModulePathSegs.join("/") if !self.project.modules[preludeModulePathAbs] { + val trackIdentsForLsp = self.trackIdentsForLsp + self.trackIdentsForLsp = false try self._typecheckModule(preludeModulePathAbs) + self.trackIdentsForLsp = trackIdentsForLsp val preludeStructs = [ self.project.preludeIntStruct, @@ -2169,6 +2188,8 @@ export type Typechecker { mod.complete = true + mod.identsByLine = self.identsByLine + if self.typecheckingBuiltin { self.typecheckingBuiltin = None } @@ -3033,6 +3054,20 @@ export type Typechecker { val variable = Variable(label: label, scope: self.currentScope, mutable: mutable, ty: ty) try self._addVariableToScope(variable) + if self.trackIdentsForLsp { + val ident = IdentifierMeta( + name: label.name, + kind: IdentifierKindMeta.Variable(mutable: mutable, typeRepr: ty.repr()), + ) + + val v = (label.position.col - 1, label.position.col + label.name.length - 1, ident) + if self.identsByLine[label.position.line - 1] |idents| { + idents.push(v) + } else { + self.identsByLine[label.position.line - 1] = [v] + } + } + Ok([variable]) } BindingPattern.Tuple(lParenTok, patterns) => { @@ -4005,10 +4040,47 @@ export type Typechecker { } return Err(TypeError(position: token.position, kind: TypeErrorKind.UnknownName("self", "variable"))) } + (resolvedIdentifier, "self", None) } } + if self.trackIdentsForLsp { + val kind = match variable.alias { + None => IdentifierKindMeta.Variable(variable.mutable, variable.ty.repr()) + VariableAlias.Function(fn) => { + val typeParams: String[] = [] + for (_, label) in fn.typeParams { + typeParams.push(label.name) + } + + val params: String[] = [] + for param in fn.params { + params.push("${if param.isVariadic "..." else ""}${param.label.name}${if param.defaultValue "?" else ""}: ${param.ty.repr()}") + } + + val returnTypeRepr = fn.returnType.repr() + + IdentifierKindMeta.Function(typeParams, params, returnTypeRepr) + } + VariableAlias.Struct(struct) => IdentifierKindMeta.Type(isEnum: false, typeParams: struct.typeParams) + VariableAlias.Enum(_enum) => IdentifierKindMeta.Type(isEnum: true, typeParams: _enum.typeParams) + } + + val ident = IdentifierMeta( + name: name, + kind: kind, + importedFrom: varImportMod?.name, + ) + + val v = (token.position.col - 1, token.position.col + name.length - 1, ident) + if self.identsByLine[token.position.line - 1] |idents| { + idents.push(v) + } else { + self.identsByLine[token.position.line - 1] = [v] + } + } + Ok(TypedAstNode(token: token, ty: variable.ty, kind: TypedAstNodeKind.Identifier(name, variable, fnTypeHint, varImportMod))) } diff --git a/projects/lsp/src/language_service.abra b/projects/lsp/src/language_service.abra index e337957e..9b5f5a2b 100644 --- a/projects/lsp/src/language_service.abra +++ b/projects/lsp/src/language_service.abra @@ -1,8 +1,8 @@ import "fs" as fs import JsonValue from "json" import log from "./log" -import ModuleLoader, Project, Typechecker, TypecheckerErrorKind from "../../compiler/src/typechecker" -import RequestMessage, NotificationMessage, ResponseMessage, ResponseResult, ResponseError, ResponseErrorCode, ServerCapabilities, TextDocumentSyncOptions, TextDocumentSyncKind, SaveOptions, ServerInfo, TextDocumentItem, TextDocumentIdentifier, VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent, Diagnostic, DiagnosticSeverity from "./lsp_spec" +import ModuleLoader, Project, Typechecker, TypecheckerErrorKind, IdentifierKindMeta 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: " export val bogusMessageId = -999 @@ -47,13 +47,56 @@ export type AbraLanguageService { openClose: Some(false), change: Some(TextDocumentSyncKind.None_), save: Some(SaveOptions(includeText: Some(false))) - )) + )), + hoverProvider: Some(true), ), serverInfo: ServerInfo(name: "abra-lsp", version: Some("0.0.1")) ) ResponseMessage.Success(id: id, result: Some(result)) } + 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, "```"] + + if ident.importedFrom |modName| { + lines.push("Imported from `$modName`") + } + + 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)) + } + } + + ResponseMessage.Success(id: id, result: None) + } + // Notification handlers func _textDocumentDidOpen(self, textDocument: TextDocumentItem) { @@ -101,7 +144,11 @@ export type AbraLanguageService { // todo: what happens if it's not a `file://` uri? val filePath = uri.replaceAll("file://", "") - val typechecker = Typechecker(moduleLoader: self._moduleLoader, project: self._project) + val typechecker = Typechecker( + moduleLoader: self._moduleLoader, + project: self._project, + trackIdentsForLsp: true, + ) match typechecker.typecheckEntrypoint(filePath) { Ok => [] @@ -150,6 +197,7 @@ export type AbraLanguageService { 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) } } diff --git a/projects/lsp/src/lsp_spec.abra b/projects/lsp/src/lsp_spec.abra index 94988e82..062f85a1 100644 --- a/projects/lsp/src/lsp_spec.abra +++ b/projects/lsp/src/lsp_spec.abra @@ -3,6 +3,7 @@ import JsonValue, JsonError, JsonObject from "json" export enum RequestMessage { Initialize(id: Int, processId: Int?, rootPath: String?) + Hover(id: Int, textDocument: TextDocumentIdentifier, position: Position) func fromJson(json: JsonValue): Result { val obj = try json.asObject() @@ -25,6 +26,16 @@ export enum RequestMessage { Ok(Some(RequestMessage.Initialize(id: id, processId: processId, rootPath: rootPath))) } + "textDocument/hover" => { + 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.Hover(id: id, textDocument: textDocument, position: position))) + } else => { log.writeln("Error: Unimplemented RequestMessage method '$method'") @@ -128,14 +139,26 @@ export enum ResponseMessage { export enum ResponseResult { Initialize(capabilities: ServerCapabilities, serverInfo: ServerInfo) + Hover(contents: MarkupContent, range: Range?) func toJson(self): JsonValue { - val obj = JsonObject() - - match self { + val obj = match self { ResponseResult.Initialize(capabilities, serverInfo) => { - obj.set("capabilities", capabilities.toJson()) - obj.set("serverInfo", serverInfo.toJson()) + JsonObject(_map: { + "capabilities": capabilities.toJson(), + "serverInfo": serverInfo.toJson() + }) + } + ResponseResult.Hover(contents, range) => { + val obj = JsonObject(_map: { + contents: contents.toJson() + }) + + if range |range| { + obj.set("range", range.toJson()) + } + + obj } } @@ -188,6 +211,7 @@ export enum ResponseErrorCode { export type ServerCapabilities { textDocumentSync: TextDocumentSyncOptions? = None diagnosticProvider: DiagnosticOptions? = None + hoverProvider: Bool? = None func toJson(self): JsonValue { val obj = JsonObject() @@ -200,6 +224,10 @@ export type ServerCapabilities { obj.set("diagnosticProvider", dp.toJson()) } + if self.hoverProvider |hp| { + obj.set("hoverProvider", JsonValue.Boolean(hp)) + } + JsonValue.Object(obj) } } @@ -327,6 +355,55 @@ export type VersionedTextDocumentIdentifier { } } +export type Position { + // line and character are both zero-based + line: Int + character: Int + + func fromJson(json: JsonValue): Result { + val obj = try json.asObject() + val line = match try obj.getNumberRequired("line") { + Either.Left(int) => int + Either.Right(float) => float.asInt() + } + val character = match try obj.getNumberRequired("character") { + Either.Left(int) => int + Either.Right(float) => float.asInt() + } + + Ok(Position(line: line, character: character)) + } + + func toJson(self): JsonValue { + JsonValue.Object(JsonObject(_map: { + line: JsonValue.Number(Either.Left(self.line)), + character: JsonValue.Number(Either.Left(self.character)), + })) + } +} + +export type Range { + start: Position + end: Position // exclusive + + func fromJson(json: JsonValue): Result { + val obj = try json.asObject() + val startObj = try obj.getValueRequired("start") + val start = try Position.fromJson(startObj) + val endObj = try obj.getValueRequired("end") + val end = try Position.fromJson(endObj) + + Ok(Range(start: start, end: end)) + } + + func toJson(self): JsonValue { + JsonValue.Object(JsonObject(_map: { + start: self.start.toJson(), + end: self.end.toJson(), + })) + } +} + export enum TextDocumentContentChangeEvent { Incremental( range: ((Int, Int), (Int, Int)), // (line: Int, character: Int), zero-based @@ -388,3 +465,23 @@ export enum DiagnosticSeverity { DiagnosticSeverity.Hint => 4 } } + +export type MarkupContent { + kind: MarkupKind + value: String + + func toJson(self): JsonValue { + JsonValue.Object(JsonObject(_map: { + kind: JsonValue.String(match self.kind { + MarkupKind.PlainText => "plaintext" + MarkupKind.Markdown => "markdown" + }), + value: JsonValue.String(self.value) + })) + } +} + +export enum MarkupKind { + PlainText + Markdown +}