Source file server.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418

open Sigs

(** Current state of a document. *)
type nonrec doc_state = {
  uri: Lsp.Types.DocumentUri.t;
  languageId: string;
  version: int;
  content: string;
}

(** {2 Request ID}

    unique ID of a request, used by JSONRPC to map each request to its reply. *)
module Req_id = struct
  type t = Jsonrpc.Id.t

  (** String representation of the ID *)
  let to_string : t -> string = function
    | `String s -> s
    | `Int i -> string_of_int i
end

(** {2 Server interface for some IO substrate} *)
module Make(IO : IO) = struct
  open Lsp.Types

  module Position = Position
  module Range = Range
  module Diagnostic = Diagnostic
  module DiagnosticSeverity = DiagnosticSeverity
  module Req_id = Req_id

  (** The server baseclass *)
  class virtual base_server = object
    method virtual on_notification :
      notify_back:(Lsp.Server_notification.t -> unit IO.t) ->
      Lsp.Client_notification.t ->
      unit IO.t

    method virtual on_request : 'a.
      notify_back:(Lsp.Server_notification.t -> unit IO.t) ->
      id:Req_id.t ->
      'a Lsp.Client_request.t ->
      'a IO.t
    (** Method called to handle client requests.
        @param notify_back an object used to reply to the client, send progress
        messages, diagnostics, etc.
        @param id the query RPC ID, can be used for tracing, cancellation, etc. *)

    (** Set to true if the client requested to exit *)
    method must_quit = false
  end

  (** A wrapper to more easily reply to notifications *)
  class notify_back ~notify_back ?version ?(uri:DocumentUri.t option) () = object
    val mutable uri = uri
    method set_uri u = uri <- Some u

    (** Send a log message to the editor *)
    method send_log_msg ~type_ msg : unit IO.t =
      let params = ShowMessageParams.create ~type_ ~message:msg in
      notify_back (Lsp.Server_notification.LogMessage params)

    (** Send diagnostics for the current document *)
    method send_diagnostic (l:Diagnostic.t list) : unit IO.t =
      match uri with
      | None -> IO.failwith "notify_back: cannot publish diagnostics, no URI given"
      | Some uri ->
        let params = PublishDiagnosticsParams.create
            ~uri ?version ~diagnostics:l () in
        notify_back (Lsp.Server_notification.PublishDiagnostics params)

    (** Send a notification (general purpose method) *)
    method send_notification (n:Lsp.Server_notification.t) =
      notify_back n
  end

  (** Current state of a document. *)
  type nonrec doc_state = doc_state = {
    uri: DocumentUri.t;
    languageId: string;
    version: int;
    content: string;
  }

  (** An easily overloadable class. Pick the methods you want to support.
      The user must provide at least the callbacks for document lifecycle:
      open, close, update. The most basic LSP server should check documents
      when they're updated and report diagnostics back to the editor. *)
  class virtual server = object(self)
    inherit base_server
    val mutable _quit = false
    val docs : (DocumentUri.t, doc_state) Hashtbl.t = Hashtbl.create 16

    method! must_quit = _quit

    (** Find current state of the given document, if present. *)
    method find_doc (uri:DocumentUri.t) : doc_state option =
      try Some (Hashtbl.find docs uri)
      with Not_found -> None

    (** Override to process other requests *)
    method on_request_unhandled
      : type r. notify_back:notify_back ->
        id:Req_id.t -> r Lsp.Client_request.t -> r IO.t
      = fun ~notify_back:_ ~id:_ _r ->
        Log.debug (fun k->k "req: unhandled request");
        IO.failwith "TODO: handle this request"

    (** Parameter for how to synchronize content with the editor *)
    method config_sync_opts : TextDocumentSyncOptions.t =
      TextDocumentSyncOptions.create
          ~change:TextDocumentSyncKind.Incremental ~willSave:false ()

    method config_completion : CompletionOptions.t option = None
    (** Configuration for the completion API.
        @since 0.4 *)

    method config_code_lens_options : CodeLensOptions.t option = None
    (** @since 0.3 *)

    method config_definition :
      [`Bool of bool | `DefinitionOptions of DefinitionOptions.t ] option = None
    (** @since 0.3 *)

    method config_hover :
      [`Bool of bool | `HoverOptions of HoverOptions.t ] option = None
    (** @since 0.3 *)

    method config_symbol :
      [`Bool of bool | `DocumentSymbolOptions of DocumentSymbolOptions.t ] option = None
    (** @since 0.3 *)

    method config_code_action_provider :
      [`CodeActionOptions of CodeActionOptions.t | `Bool of bool] = `Bool false
    (** @since 0.3 *)

    (** Modify capabilities before sending them back to the client.
        By default we just return them unmodified.
        @since 0.3 *)
    method config_modify_capabilities (c:ServerCapabilities.t) : ServerCapabilities.t = c

    (** List of commands available *)
    method config_list_commands : string list = []

    method on_req_initialize ~notify_back:_
        (_i:InitializeParams.t) : InitializeResult.t IO.t =
      let sync_opts = self#config_sync_opts in
      let capabilities =
        ServerCapabilities.create
          ?codeLensProvider:self#config_code_lens_options
          ~codeActionProvider:self#config_code_action_provider
          ~executeCommandProvider:(ExecuteCommandOptions.create
                                     ~commands:self#config_list_commands ())
          ?completionProvider:self#config_completion
          ?definitionProvider:self#config_definition
          ?hoverProvider:self#config_hover
          ?documentSymbolProvider:self#config_symbol
          ~textDocumentSync:(`TextDocumentSyncOptions sync_opts) ()
        |> self#config_modify_capabilities
      in
      IO.return @@ InitializeResult.create ~capabilities ()

    (** Called when the user hovers on some identifier in the document *)
    method on_req_hover ~notify_back:_ ~id:_ ~uri:_ ~pos:_
        (_ : doc_state) : Hover.t option IO.t =
      IO.return None

    (** Called when the user requests completion in the document *)
    method on_req_completion  ~notify_back:_ ~id:_ ~uri:_ ~pos:_ ~ctx:_
        (_ : doc_state) :
          [ `CompletionList of CompletionList.t
          | `List of CompletionItem.t list ] option IO.t =
      IO.return None

    (** Called when the user wants to jump-to-definition  *)
    method on_req_definition  ~notify_back:_ ~id:_ ~uri:_ ~pos:_
        (_ : doc_state) : Locations.t option IO.t =
      IO.return None

    (** List code lenses for the given document
        @since 0.3 *)
    method on_req_code_lens  ~notify_back:_ ~id:_ ~uri:_
        (_ : doc_state) : CodeLens.t list IO.t =
      IO.return []

    (** Code lens resolution, must return a code lens with non null "command"
        @since 0.3 *)
    method on_req_code_lens_resolve
        ~notify_back:(_:notify_back) ~id:_ (cl:CodeLens.t) : CodeLens.t IO.t =
      IO.return cl

    (** Code action.
        @since 0.3 *)
    method on_req_code_action ~notify_back:(_:notify_back) ~id:_ (_c:CodeActionParams.t)
      : CodeActionResult.t IO.t =
      IO.return None

    (** Execute a command with given arguments.
        @since 0.3 *)
    method on_req_execute_command ~notify_back:_ ~id:_
        (_c:string) (_args:Yojson.Safe.t list option) : Yojson.Safe.t IO.t =
      IO.return `Null

    (** List symbols in this document.
        @since 0.3 *)
    method on_req_symbol ~notify_back:_ ~id:_ ~uri:_
        () : [ `DocumentSymbol of DocumentSymbol.t list
             | `SymbolInformation of SymbolInformation.t list ] option IO.t =
      IO.return None

    method on_request
    : type r. notify_back:_ -> id:Req_id.t -> r Lsp.Client_request.t -> r IO.t
    = fun ~notify_back ~id (r:_ Lsp.Client_request.t) ->
      Log.debug (fun k->k "handle request[id=%s] <opaque>" (Req_id.to_string id));

      begin match r with
        | Lsp.Client_request.Shutdown ->
          Log.info (fun k->k "shutdown");
          _quit <- true; IO.return ()

        | Lsp.Client_request.Initialize i ->
          Log.debug (fun k->k "req: initialize");
          let notify_back = new notify_back ~notify_back () in
          self#on_req_initialize ~notify_back i

        | Lsp.Client_request.TextDocumentHover { textDocument; position } ->
          let uri = textDocument.uri in
          Log.debug (fun k->k "req: hover '%s'" uri);

          begin match Hashtbl.find_opt docs uri with
            | None -> IO.return None
            | Some doc_st ->
              let notify_back = new notify_back ~uri ~notify_back () in
              self#on_req_hover ~notify_back ~id ~uri ~pos:position doc_st
          end

        | Lsp.Client_request.TextDocumentCompletion { textDocument; position; context } ->
          let uri = textDocument.uri in
          Log.debug (fun k->k "req: complete '%s'" uri);
          begin match Hashtbl.find_opt docs uri with
            | None -> IO.return None
            | Some doc_st ->
              let notify_back = new notify_back ~uri ~notify_back () in
              self#on_req_completion ~notify_back ~id ~uri
                ~pos:position ~ctx:context doc_st
          end
        | Lsp.Client_request.TextDocumentDefinition { textDocument; position } ->
          let uri = textDocument.uri in
          Log.debug (fun k->k "req: definition '%s'" uri);
          let notify_back = new notify_back ~uri ~notify_back () in

          begin match Hashtbl.find_opt docs uri with
            | None -> IO.return None
            | Some doc_st ->
              self#on_req_definition ~notify_back ~id
                ~uri ~pos:position doc_st
          end

        | Lsp.Client_request.TextDocumentCodeLens {textDocument} ->
          let uri = textDocument.uri in
          Log.debug (fun k->k "req: codelens '%s'" uri);
          let notify_back = new notify_back ~uri ~notify_back () in

          begin match Hashtbl.find_opt docs uri with
            | None -> IO.return []
            | Some doc_st ->
              self#on_req_code_lens ~notify_back ~id ~uri doc_st
          end

        | Lsp.Client_request.TextDocumentCodeLensResolve cl ->
          Log.debug (fun k->k "req: codelens resolve");
          let notify_back = new notify_back ~notify_back () in
          self#on_req_code_lens_resolve ~notify_back ~id cl

        | Lsp.Client_request.ExecuteCommand { command; arguments } ->
          Log.debug (fun k->k "req: execute command '%s'" command);
          let notify_back = new notify_back ~notify_back () in
          self#on_req_execute_command ~notify_back ~id command arguments

        | Lsp.Client_request.DocumentSymbol { textDocument=d } ->
          let notify_back = new notify_back ~notify_back () in
          self#on_req_symbol ~notify_back ~id ~uri:d.uri ()

        | Lsp.Client_request.CodeAction a ->
          let notify_back = new notify_back ~notify_back () in
          self#on_req_code_action ~notify_back ~id a

        | Lsp.Client_request.TextDocumentDeclaration _
        | Lsp.Client_request.TextDocumentTypeDefinition _
        | Lsp.Client_request.TextDocumentPrepareRename _
        | Lsp.Client_request.TextDocumentRename _
        | Lsp.Client_request.TextDocumentLink _
        | Lsp.Client_request.TextDocumentLinkResolve _
        | Lsp.Client_request.WorkspaceSymbol _
        | Lsp.Client_request.DebugEcho _
        | Lsp.Client_request.DebugTextDocumentGet _
        | Lsp.Client_request.TextDocumentReferences _
        | Lsp.Client_request.TextDocumentHighlight _
        | Lsp.Client_request.TextDocumentFoldingRange _
        | Lsp.Client_request.SignatureHelp _
        | Lsp.Client_request.CompletionItemResolve _
        | Lsp.Client_request.WillSaveWaitUntilTextDocument _
        | Lsp.Client_request.TextDocumentFormatting _
        | Lsp.Client_request.TextDocumentOnTypeFormatting _
        | Lsp.Client_request.TextDocumentColorPresentation _
        | Lsp.Client_request.TextDocumentColor _
        | Lsp.Client_request.SelectionRange _
        | Lsp.Client_request.UnknownRequest _ ->
          let notify_back = new notify_back ~notify_back () in
          self#on_request_unhandled ~notify_back ~id r
      end

    (** Called when a document is opened *)
    method virtual on_notif_doc_did_open :
      notify_back:notify_back ->
      TextDocumentItem.t ->
      content:string ->
      unit IO.t

    method virtual on_notif_doc_did_close :
      notify_back:notify_back ->
      TextDocumentIdentifier.t ->
      unit IO.t

    (** Called when the document changes. *)
    method virtual on_notif_doc_did_change :
      notify_back:notify_back ->
      VersionedTextDocumentIdentifier.t ->
      TextDocumentContentChangeEvent.t list ->
      old_content:string ->
      new_content:string ->
      unit IO.t

    (** Override to handle unprocessed notifications *)
    method on_notification_unhandled
        ~notify_back:_ (_n:Lsp.Client_notification.t) : unit IO.t =
      IO.return ()

    method on_notification
        ~notify_back (n:Lsp.Client_notification.t) : unit IO.t =
      let open Lsp.Types in

      begin match n with
        | Lsp.Client_notification.TextDocumentDidOpen
            {DidOpenTextDocumentParams.textDocument=doc} ->
          Log.debug (fun k->k "notif: did open '%s'" doc.uri);
          let notify_back =
            new notify_back ~uri:doc.uri ~version:doc.version ~notify_back () in
          let st = {
            uri=doc.uri; version=doc.version; content=doc.text;
            languageId=doc.languageId;
          } in
          Hashtbl.replace docs doc.uri st;
          self#on_notif_doc_did_open ~notify_back doc ~content:st.content

        | Lsp.Client_notification.TextDocumentDidClose {textDocument=doc} ->
          Log.debug (fun k->k "notif: did close '%s'" doc.uri);
          let notify_back = new notify_back ~uri:doc.uri ~notify_back () in
          self#on_notif_doc_did_close ~notify_back doc

        | Lsp.Client_notification.TextDocumentDidChange {textDocument=doc; contentChanges=c} ->
          Log.debug (fun k->k "notif: did change '%s'" doc.uri);
          let notify_back = new notify_back ~uri:doc.uri ~notify_back () in

          let old_doc =
            match Hashtbl.find_opt docs doc.uri with
            | None ->
              (* WTF vscode. Well let's try and deal with it. *)
              Log.err (fun k->k "unknown document: '%s'" doc.uri);
              let version = match doc.version with Some x->x | None -> 0 in

              let languageId = "" in (* FIXME*)
              Lsp.Text_document.make
                (DidOpenTextDocumentParams.create
                   ~textDocument:(
                     TextDocumentItem.create ~languageId
                       ~uri:doc.uri ~version ~text:""))
            | Some st ->
                Lsp.Text_document.make
                  (DidOpenTextDocumentParams.create
                     ~textDocument:(
                       TextDocumentItem.create ~languageId:st.languageId
                         ~uri:doc.uri ~version:st.version ~text:st.content))
          in

          let new_doc: Lsp.Text_document.t =
            List.fold_left
              (fun d ev -> Lsp.Text_document.apply_content_change d ev)
              old_doc c
          in

          let new_st : doc_state = {
            uri=doc.uri; languageId=Lsp.Text_document.languageId new_doc;
            content=Lsp.Text_document.text new_doc;
            version=Lsp.Text_document.version new_doc;
          } in

          Hashtbl.replace docs doc.uri new_st;
          self#on_notif_doc_did_change ~notify_back doc c
            ~old_content:(Lsp.Text_document.text old_doc)
            ~new_content:new_st.content

        | Lsp.Client_notification.Exit -> _quit <- true; IO.return ()
        | Lsp.Client_notification.DidSaveTextDocument _
        | Lsp.Client_notification.WillSaveTextDocument _
        | Lsp.Client_notification.ChangeWorkspaceFolders _
        | Lsp.Client_notification.ChangeConfiguration _
        | Lsp.Client_notification.Initialized
        | Lsp.Client_notification.Unknown_notification _
        | Lsp.Client_notification.CancelRequest _
          ->
          let notify_back = new notify_back ~notify_back () in
          self#on_notification_unhandled ~notify_back n
      end
  end
end