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

LSP: Using a virtual filesystem #524

Merged
merged 2 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
71 changes: 35 additions & 36 deletions projects/compiler/src/typechecker.abra
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ enum TokenizeAndParseError {
export type ModuleLoader {
stdRoot: String
parsedModules: Map<String, ParsedModule> = {}
documentStore: Map<String, String> = {}
useDocumentStore: Bool = false
_invalidated: Map<String, Bool> = {}
_virtualFileSystem: Map<String, String>? = None

func usingVirtualFileSystem(stdRoot: String, fileMap: Map<String, String>): ModuleLoader =
ModuleLoader(stdRoot: stdRoot, _virtualFileSystem: Some(fileMap))

func resolvePath(self, modulePath: String, relativeTo: String?): String {
if relativeTo |relativeTo| {
Expand All @@ -28,12 +29,12 @@ export type ModuleLoader {
func hasSeenModule(self, modulePathAbs: String): Bool = self.parsedModules.containsKey(modulePathAbs)

func invalidateModule(self, modulePathAbs: String) {
self._invalidated[modulePathAbs] = true
self.parsedModules.remove(modulePathAbs)
}

func _loadFileContents(self, modulePathAbs: String): String? {
if self.useDocumentStore {
val inMemContents = self.documentStore[modulePathAbs]
if self._virtualFileSystem |vfs| {
val inMemContents = vfs[modulePathAbs]
if inMemContents return inMemContents
}

Expand All @@ -44,22 +45,15 @@ export type ModuleLoader {
}

func tokenizeAndParse(self, modulePath: String): Result<ParsedModule, TokenizeAndParseError> {
if self.parsedModules[modulePath] |m| {
val invalidated = self._invalidated[modulePath] ?: false
if !invalidated return Ok(m)
}
if self.parsedModules[modulePath] |m| return Ok(m)

val parsedModule = if self._loadFileContents(modulePath) |contents| {
match Lexer.tokenize(contents) {
Ok(tokens) => {
val module = match Parser.parse(tokens) {
match Parser.parse(tokens) {
Ok(parsedModule) => parsedModule
Err(error) => return Err(TokenizeAndParseError.ParseError(error))
}

self._invalidated[modulePath] = false

module
}
Err(error) => return Err(TokenizeAndParseError.LexerError(error))
}
Expand Down Expand Up @@ -1495,28 +1489,31 @@ export type Typechecker {
func typecheckEntrypoint(self, modulePathAbs: String): Result<TypedModule, TypecheckerError> {
val preludeModulePathSegs = getAbsolutePath(self.moduleLoader.stdRoot + "/prelude.abra")
val preludeModulePathAbs = "/" + preludeModulePathSegs.join("/")
try self._typecheckModule(preludeModulePathAbs)

val preludeStructs = [
self.project.preludeIntStruct,
self.project.preludeFloatStruct,
self.project.preludeBoolStruct,
self.project.preludeCharStruct,
self.project.preludeStringStruct,
self.project.preludeArrayStruct,
self.project.preludeMapStruct,
self.project.preludeSetStruct,
]
for struct in preludeStructs {
if struct.label.position == Position(line: 0, col: 0) unreachable("Improperly initialized prelude struct ${struct.label.name}")
}

val preludeEnums = [
self.project.preludeOptionEnum,
self.project.preludeResultEnum,
]
for enum_ in preludeEnums {
if enum_.label.position == Position(line: 0, col: 0) unreachable("Improperly initialized prelude enum ${enum_.label.name}")
if !self.project.modules[preludeModulePathAbs] {
try self._typecheckModule(preludeModulePathAbs)

val preludeStructs = [
self.project.preludeIntStruct,
self.project.preludeFloatStruct,
self.project.preludeBoolStruct,
self.project.preludeCharStruct,
self.project.preludeStringStruct,
self.project.preludeArrayStruct,
self.project.preludeMapStruct,
self.project.preludeSetStruct,
]
for struct in preludeStructs {
if struct.label.position == Position(line: 0, col: 0) unreachable("Improperly initialized prelude struct ${struct.label.name}")
}

val preludeEnums = [
self.project.preludeOptionEnum,
self.project.preludeResultEnum,
]
for enum_ in preludeEnums {
if enum_.label.position == Position(line: 0, col: 0) unreachable("Improperly initialized prelude enum ${enum_.label.name}")
}
}

self._typecheckModule(modulePathAbs)
Expand Down Expand Up @@ -2070,6 +2067,8 @@ export type Typechecker {
}

func _typecheckModule(self, modulePathAbs: String): Result<TypedModule, TypecheckerError> {
if self.project.modules[modulePathAbs] |mod| return Ok(mod)

self.typecheckingBuiltin = if modulePathAbs == self.moduleLoader.stdRoot + "/prelude.abra" {
self.project.preludeScope = self.currentScope.makeChild("module_prelude", ScopeKind.Module(id: -1))
Some(BuiltinModule.Prelude)
Expand Down
88 changes: 60 additions & 28 deletions projects/lsp/src/language_service.abra
Original file line number Diff line number Diff line change
@@ -1,35 +1,54 @@
import "fs" as fs
import "process" as process
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, TextDocumentSyncKind, ServerInfo, TextDocumentItem, VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent, Diagnostic, DiagnosticSeverity from "./lsp_spec"
import RequestMessage, NotificationMessage, ResponseMessage, ResponseResult, ResponseError, ResponseErrorCode, ServerCapabilities, TextDocumentSyncOptions, TextDocumentSyncKind, SaveOptions, ServerInfo, TextDocumentItem, TextDocumentIdentifier, VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent, Diagnostic, DiagnosticSeverity from "./lsp_spec"

export val contentLengthHeader = "Content-Length: "
export val bogusMessageId = -999

val abraStdRoot = if process.getEnvVar("ABRA_HOME") |v| v else {
println("Could not find ABRA_HOME (make sure \$ABRA_HOME environment variable is set)")
process.exit(1)
}

val moduleLoader = ModuleLoader(stdRoot: abraStdRoot, useDocumentStore: true)
val documentStore = moduleLoader.documentStore
val project = Project()
val typechecker = Typechecker(moduleLoader: moduleLoader, project: project)

export type AbraLanguageService {
initialized: Bool = false
root: String = ""
// _virtualFileSystem: Map<String, String>
_moduleLoader: ModuleLoader
_project: Project
_initialized: Bool = false
_root: String = ""

func new(abraStdRoot: String): AbraLanguageService {
// val virtualFileSystem: Map<String, String> = {}
// val moduleLoader = ModuleLoader.usingVirtualFileSystem(stdRoot: abraStdRoot, fileMap: virtualFileSystem)
val moduleLoader = ModuleLoader(stdRoot: abraStdRoot)
val project = Project()

AbraLanguageService(
// _virtualFileSystem: virtualFileSystem,
_moduleLoader: moduleLoader,
_project: project,
)
}

// Request Message handlers

func _initialize(self, id: Int, processId: Int?, rootPath: String?): ResponseMessage {
self.root = if rootPath |p| p else return internalError(id, "rootPath required")
self.initialized = true

self._root = if rootPath |p| p else return internalError(id, "rootPath required")
self._initialized = true

// Instruct the client to not send textDocument/didOpen or textDocument/didClose events,
// and to send textDocument/didSave events, but do not include file contents when saved.
//
// Additionally, for now, instruct the client to NOT send textDocument/didChange events;
// sending the full contents each time (replacing the file in the ModuleLoader's vfs) is
// pretty wasteful and causes problems with larger files. Eventually, I'll implement a
// more performant model using the Incremental updates, but for now the client will only
// receive diagnostics from the server when the file is saved (which is file, frankly).
val result = ResponseResult.Initialize(
capabilities: ServerCapabilities(textDocumentSync: Some(TextDocumentSyncKind.Full)),
capabilities: ServerCapabilities(
textDocumentSync: Some(TextDocumentSyncOptions(
openClose: Some(false),
change: Some(TextDocumentSyncKind.None_),
save: Some(SaveOptions(includeText: Some(false)))
))
),
serverInfo: ServerInfo(name: "abra-lsp", version: Some("0.0.1"))
)
ResponseMessage.Success(id: id, result: Some(result))
Expand All @@ -38,32 +57,42 @@ export type AbraLanguageService {
// Notification handlers

func _textDocumentDidOpen(self, textDocument: TextDocumentItem) {
// textDocument/didOpen events are currently not sent by the client (see self._initialize)
val diagnostics = self._runTypecheckerStartingAtUri(textDocument.uri)
if !diagnostics.isEmpty() {
val notif = NotificationMessage.TextDocumentPublishDiagnostics(uri: textDocument.uri, diagnostics: diagnostics)
self.sendNotification(notif)
}
val notif = NotificationMessage.TextDocumentPublishDiagnostics(uri: textDocument.uri, diagnostics: diagnostics)
self.sendNotification(notif)
}

func _textDocumentDidChange(self, textDocument: VersionedTextDocumentIdentifier, contentChanges: TextDocumentContentChangeEvent[]) {
// textDocument/didChange events are currently not sent by the client (see self._initialize)
if contentChanges.isEmpty() return

val filePath = textDocument.uri.replaceAll("file://", "")
for changeEvent in contentChanges {
match changeEvent {
TextDocumentContentChangeEvent.Incremental => todo("TextDocumentContentChangeEvent.Incremental")
TextDocumentContentChangeEvent.Full(text) => {
documentStore[filePath] = text
// self._virtualFileSystem[filePath] = text
}
}
}
moduleLoader.invalidateModule(filePath)
self._moduleLoader.invalidateModule(filePath)
self._project.modules.remove(filePath)

val diagnostics = self._runTypecheckerStartingAtUri(textDocument.uri)
if !diagnostics.isEmpty() {
val notif = NotificationMessage.TextDocumentPublishDiagnostics(uri: textDocument.uri, diagnostics: diagnostics)
self.sendNotification(notif)
}
val notif = NotificationMessage.TextDocumentPublishDiagnostics(uri: textDocument.uri, diagnostics: diagnostics)
self.sendNotification(notif)
}

func _textDocumentDidSave(self, textDocument: TextDocumentIdentifier) {
val filePath = textDocument.uri.replaceAll("file://", "")
self._moduleLoader.invalidateModule(filePath)
self._project.modules.remove(filePath)
// self._virtualFileSystem.remove(filePath)

val diagnostics = self._runTypecheckerStartingAtUri(textDocument.uri)
val notif = NotificationMessage.TextDocumentPublishDiagnostics(uri: textDocument.uri, diagnostics: diagnostics)
self.sendNotification(notif)
}

// Compiler bridge
Expand All @@ -72,6 +101,8 @@ 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)

match typechecker.typecheckEntrypoint(filePath) {
Ok => []
Err(e) => {
Expand Down Expand Up @@ -127,6 +158,7 @@ export type AbraLanguageService {
NotificationMessage.Initialized => { /* no-op */ }
NotificationMessage.TextDocumentDidOpen(textDocument) => self._textDocumentDidOpen(textDocument)
NotificationMessage.TextDocumentDidChange(textDocument, contentChanges) => self._textDocumentDidChange(textDocument, contentChanges)
NotificationMessage.TextDocumentDidSave(textDocument) => self._textDocumentDidSave(textDocument)
}
}

Expand Down
Loading
Loading