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

Basic language server implementation #121

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open

Conversation

pa000
Copy link

@pa000 pa000 commented Jun 7, 2024

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).

@pa000 pa000 marked this pull request as draft October 29, 2024 14:18
@ilikeheaps
Copy link

I've tested it shortly on my setup, seems to be working. Does highlight errors with description.

Steps I've taken to setup it:

  1. I have updated the branch with master locally, will put up a branch in a moment so we're sure we work on the same version
  2. Added yojson library and findlib to my development environment (Nix)
  3. Made lsp client configuration for Emacs with lsp-mode, will post it somewhere

@ilikeheaps

This comment was marked as resolved.

@ilikeheaps
Copy link

And for now I'll drop the Emacs configuration here:
lsp.el:

;; eval this file to setup framls for lsp-mode
(add-to-list 'lsp-language-id-configuration '(".*\\.fram$" . "fram"))

;; Emacs must be ran within environment with 'framls' available; either:
;; - install it (TODO 'dune install ???' -- I don't use it)
;; - or start Emacs with 'dune exec emacs'
(lsp-register-client (make-lsp-client
                      :new-connection (lsp-stdio-connection "framls")
                      :activation-fn (lsp-activate-on "fram")
                      :server-id 'framls))

@ilikeheaps

This comment was marked as resolved.

Everything needs     to
be         perfectly aligned
Copy link

@ilikeheaps ilikeheaps left a 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

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.

Copy link
Author

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 =

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)

Copy link
Author

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
Comment on lines 7 to 9
let to_path uri =
String.sub uri (String.length "file://")
(String.length uri - String.length "file://")

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://"?

Copy link
Author

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.

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.

Copy link
Author

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
@pa000 pa000 marked this pull request as ready for review December 1, 2024 21:27
@pa000
Copy link
Author

pa000 commented Dec 1, 2024

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.

Copy link

@ilikeheaps ilikeheaps left a 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]

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)

Suggested change
[JSON-RPC message]
[JSON-RPC message]
The same header may occur multiple times, in which case the last value will be taken.

Copy link
Author

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough.

src/Lsp/Connection.ml Outdated Show resolved Hide resolved
`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).

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?

Copy link
Author

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.

Comment on lines +45 to +57
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.

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)

Copy link
Author

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).

src/Lsp/JsonRpc.ml Outdated Show resolved Hide resolved
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 *)

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 ... | ....

Copy link
Author

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.

src/Lsp/State.ml Outdated Show resolved Hide resolved
Comment on lines +69 to +70
set_module_dirs real_path;
type_check temp_path;

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?

Copy link
Author

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.

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.

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.

Copy link
Author

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 Show resolved Hide resolved
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.

Copy link
Author

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.


(** Server state *)

(** The LSP client sends a notification when the user opens, updates

This comment was marked as resolved.

Copy link
Author

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).

Comment on lines +20 to +23
type range = {
start : position;
end_ : position (* json key: end *);
}

This comment was marked as resolved.

Copy link
Author

@pa000 pa000 Jan 16, 2025

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

This comment was marked as resolved.

Copy link
Author

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.

Comment on lines +14 to +27
Yojson.Safe.from_string
{|
{
"capabilities": {
"textDocumentSync": {
"openClose": true,
"change": 1
}
},
"serverInfo": {
"name": "framls"
}
}
|}

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?

Copy link
Author

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.

Copy link

@ilikeheaps ilikeheaps left a 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.

@ilikeheaps
Copy link

Whoop, seems like I can't mark conversations as resolved, only single comments of my own.

@pa000
Copy link
Author

pa000 commented Jan 23, 2025

That's all from me. We can finally merge it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants