-
Notifications
You must be signed in to change notification settings - Fork 17
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
Basic language server implementation #121
base: master
Are you sure you want to change the base?
Conversation
Provides a dblls binary The only implemented feature is diagnostic information
I've tested it shortly on my setup, seems to be working. Does highlight errors with description. Steps I've taken to setup it:
|
This comment was marked as resolved.
This comment was marked as resolved.
And for now I'll drop the Emacs configuration here:
|
This comment was marked as resolved.
This comment was marked as resolved.
Update with upstream
Everything needs to be perfectly aligned
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checked some files, so far so good (left couple ideas to improve type safety, could be dismissed or done later).
@@ -56,6 +56,12 @@ let report ?pos ~cls msg = | |||
| None, _ -> | |||
Printf.eprintf "%s: %s\n" name msg | |||
|
|||
let report_impl = ref report_to_stderr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a fan of mutable state. Do we need to dynamically change this? Perhaps I'll answer this myself as I read further.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think anyone here is. However, without any mutable state we would have to pass a report
argument to every type-checker function which seems impractical. Regular DBL code has no reason to change this variable and the language server only does it before running the type-checker, so I think there is little risk with using a ref here.
; content_type: string option | ||
} | ||
|
||
let parse_header headers line = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds like something we could use a third party library for unless it's pretty custom. (could refactor later)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a fan of dependencies and the whole file is like 60 lines (and does exactly what's needed), so I'm not sure how much a library would help.
src/Lsp/Uri.ml
Outdated
let to_path uri = | ||
String.sub uri (String.length "file://") | ||
(String.length uri - String.length "file://") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps it should check that the prefix is indeed "file://"
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll provide an update which uses the uri
library to manage URIs (the current version doesn't work with percent-encoded strings), so this file will be gone. That said, I'm not sure what to do if someone provides a URI with a scheme different than file
(e.g http
). I don't think it's worth handling such cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be nice to fail gracefully in that case rather than doing random nonsense but if that sounds like too much hassle then I'm okay with it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that the only place where the path to the file is actually needed is when setting the directory to look for modules, so in the worst case it will just not find other modules. I think I'll leave it like that.
* Add comments * Use `uri` library to handle URIs - +1 dependency :( * Change naming convention in Message.ml to make it more consistent - json_of_type -> from_type * Add Message.mli - +200 lines :) * Redesign JsonRpc main loop - Use result type instead of exceptions to handle errors
I believe I'm finished with the final clean-ups (that seem to have made the PR 500 lines bigger oops) and the code is now ready to be thoroughly reviewed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will probably need one more pass to finish the review. Nothing major in this batch of comments, mostly "I think this could improve readability although I'm not sure and it's not a big deal".
header1: value1\r\n | ||
header2: value2\r\n | ||
\r\n | ||
[JSON-RPC message] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(suggestion might be badly formatted)
[JSON-RPC message] | |
[JSON-RPC message] | |
The same header may occur multiple times, in which case the last value will be taken. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's an implementation detail (not stated in the specification) and the top comment wasn't meant for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough.
`framls` is a language server for Fram. This document is meant to be | ||
an entry point for someone wanting to contribute to `framls`. | ||
If you want to configure `framls` for use with your code editor please visit | ||
[fram-lang.org](https://fram-lang.org). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there some PR for that doc page?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No because we don't have any editor extensions yet.
Base | ||
---- | ||
|
||
Base is a simple HTTP-like protocol. It consists of a header and content part, | ||
separated by "\r\n": | ||
``` | ||
header1: value1\r\n | ||
header2: value2\r\n | ||
\r\n | ||
[JSON-RPC message] | ||
``` | ||
|
||
It is implemented in the `Connection` module. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think we'll manage to keep this description in sync with Connection.ml or would we rather just link the file? (I suppose the same question applies to other modules)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we will because it probably won't change. I think it's useful to have it both in README (to get an overview) and in each file on its own (so you don't have to jump between files just to read the doc comment).
let rec loop state = | ||
let string = receive_string (State.in_channel state) in | ||
match message_of_string string with | ||
(* There was an error parsing the message *) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it definitely parsing error or could it be some connection error too? If it's the former we could incorporate the comment as self-explanatory custom type: type ParsingResult 'a = ParsingError ... | ...
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's definitely a parsing error (the string is already received). I don't think a custom type is a good idea because the error is produced by the callee and we're just passing it over, so we don't care what kind of error it is.
set_module_dirs real_path; | ||
type_check temp_path; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we typecheck the temporary file instead of the real one? Does temporary one include changes from the editor that are not saved yet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly. I will add that to the doc comment of State.get_document_path
. Do you think it should be explained anywhere else? It's important to know the difference between the temp and real files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking it could be sufficient to document that in type document
definition but now I realized this code avoids using document
type. Huh.
I don't think any other place produced the same question though.
Co-authored-by: ilikeheaps <[email protected]>
Co-authored-by: ilikeheaps <[email protected]>
src/Lsp/State.ml
Outdated
|
||
module UriMap = Map.Make(Uri) | ||
|
||
(** We keep the latest version of each document in a temp file *) |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's indeed explained in State.mli
(or at least I tried). If someone wants to edit the .ml
file they should know what latest means. Because of that I also removed this comment.
src/Lsp/State.ml
Outdated
|
||
let close_document state uri = | ||
match UriMap.find_opt uri state.documents with | ||
| None -> state |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This case shouldn't happen. If it does that probably means an error happened on our side and so we should fail, you're right.
src/Lsp/State.mli
Outdated
|
||
(** Server state *) | ||
|
||
(** The LSP client sends a notification when the user opens, updates |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's up to the client but most probably 1 (anything else would likely make the lsp feel laggy).
type range = { | ||
start : position; | ||
end_ : position (* json key: end *); | ||
} |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
type position = { | ||
line : int; | ||
character : int; |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd link the specification for each type but the url is crazy long (e.g. https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#range). If someone wants to implement an LSP feature they should probably have the specification open regardless.
Yojson.Safe.from_string | ||
{| | ||
{ | ||
"capabilities": { | ||
"textDocumentSync": { | ||
"openClose": true, | ||
"change": 1 | ||
} | ||
}, | ||
"serverInfo": { | ||
"name": "framls" | ||
} | ||
} | ||
|} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this use JSON constructors from Message.ml
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No (but it could be added). This message is a bit different because it depends on the capabilities that the server supports. It'd be best to create this message in a way that avoids inconsistencies (e.g. someone extends the server with some capability but forgets to update it here). I added a comment explaining it. It's fine for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I've seen it all. I left some more minor questions/suggestions but it looks good enough to me.
Whoop, seems like I can't mark conversations as resolved, only single comments of my own. |
That's all from me. We can finally merge it. |
Provides a basic language server with a diagnostics reporting feature (shows you errors in the code).
Available with a new
framls
executable.The server intercepts error reports made by the typechecker so the result should be the same as running the file with
dbl
.The code structure makes it easy to add new capabilities (but they will require changes in the dbl code).