src/Tools/VSCode/src/language_server.scala
changeset 72761 4519eeefe3b5
parent 72627 8d83acc5062e
child 72763 3cc73d00553c
equal deleted inserted replaced
72760:042180540068 72761:4519eeefe3b5
       
     1 /*  Title:      Tools/VSCode/src/language_server.scala
       
     2     Author:     Makarius
       
     3 
       
     4 Server for VS Code Language Server Protocol 2.0/3.0, see also
       
     5 https://github.com/Microsoft/language-server-protocol
       
     6 https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md
       
     7 
       
     8 PIDE protocol extensions depend on system option "vscode_pide_extensions".
       
     9 */
       
    10 
       
    11 package isabelle.vscode
       
    12 
       
    13 
       
    14 import isabelle._
       
    15 
       
    16 import java.io.{PrintStream, OutputStream, File => JFile}
       
    17 
       
    18 import scala.annotation.tailrec
       
    19 import scala.collection.mutable
       
    20 
       
    21 
       
    22 object Language_Server
       
    23 {
       
    24   type Editor = isabelle.Editor[Unit]
       
    25 
       
    26 
       
    27   /* Isabelle tool wrapper */
       
    28 
       
    29   private lazy val default_logic = Isabelle_System.getenv("ISABELLE_LOGIC")
       
    30 
       
    31   val isabelle_tool = Isabelle_Tool("vscode_server", "VSCode Language Server for PIDE", args =>
       
    32   {
       
    33     try {
       
    34       var logic_ancestor: Option[String] = None
       
    35       var log_file: Option[Path] = None
       
    36       var logic_requirements = false
       
    37       var dirs: List[Path] = Nil
       
    38       var include_sessions: List[String] = Nil
       
    39       var logic = default_logic
       
    40       var modes: List[String] = Nil
       
    41       var options = Options.init()
       
    42       var verbose = false
       
    43 
       
    44       val getopts = Getopts("""
       
    45 Usage: isabelle vscode_server [OPTIONS]
       
    46 
       
    47   Options are:
       
    48     -A NAME      ancestor session for option -R (default: parent)
       
    49     -L FILE      logging on FILE
       
    50     -R NAME      build image with requirements from other sessions
       
    51     -d DIR       include session directory
       
    52     -i NAME      include session in name-space of theories
       
    53     -l NAME      logic session name (default ISABELLE_LOGIC=""" + quote(default_logic) + """)
       
    54     -m MODE      add print mode for output
       
    55     -o OPTION    override Isabelle system OPTION (via NAME=VAL or NAME)
       
    56     -v           verbose logging
       
    57 
       
    58   Run the VSCode Language Server protocol (JSON RPC) over stdin/stdout.
       
    59 """,
       
    60         "A:" -> (arg => logic_ancestor = Some(arg)),
       
    61         "L:" -> (arg => log_file = Some(Path.explode(File.standard_path(arg)))),
       
    62         "R:" -> (arg => { logic = arg; logic_requirements = true }),
       
    63         "d:" -> (arg => dirs = dirs ::: List(Path.explode(File.standard_path(arg)))),
       
    64         "i:" -> (arg => include_sessions = include_sessions ::: List(arg)),
       
    65         "l:" -> (arg => logic = arg),
       
    66         "m:" -> (arg => modes = arg :: modes),
       
    67         "o:" -> (arg => options = options + arg),
       
    68         "v" -> (_ => verbose = true))
       
    69 
       
    70       val more_args = getopts(args)
       
    71       if (more_args.nonEmpty) getopts.usage()
       
    72 
       
    73       val log = Logger.make(log_file)
       
    74       val channel = new Channel(System.in, System.out, log, verbose)
       
    75       val server =
       
    76         new Language_Server(channel, options, session_name = logic, session_dirs = dirs,
       
    77           include_sessions = include_sessions, session_ancestor = logic_ancestor,
       
    78           session_requirements = logic_requirements, modes = modes, log = log)
       
    79 
       
    80       // prevent spurious garbage on the main protocol channel
       
    81       val orig_out = System.out
       
    82       try {
       
    83         System.setOut(new PrintStream(new OutputStream { def write(n: Int) {} }))
       
    84         server.start()
       
    85       }
       
    86       finally { System.setOut(orig_out) }
       
    87     }
       
    88     catch {
       
    89       case exn: Throwable =>
       
    90         val channel = new Channel(System.in, System.out, No_Logger)
       
    91         channel.error_message(Exn.message(exn))
       
    92         throw(exn)
       
    93     }
       
    94   })
       
    95 }
       
    96 
       
    97 class Language_Server(
       
    98   val channel: Channel,
       
    99   options: Options,
       
   100   session_name: String = Language_Server.default_logic,
       
   101   session_dirs: List[Path] = Nil,
       
   102   include_sessions: List[String] = Nil,
       
   103   session_ancestor: Option[String] = None,
       
   104   session_requirements: Boolean = false,
       
   105   modes: List[String] = Nil,
       
   106   log: Logger = No_Logger)
       
   107 {
       
   108   server =>
       
   109 
       
   110 
       
   111   /* prover session */
       
   112 
       
   113   private val session_ = Synchronized(None: Option[Session])
       
   114   def session: Session = session_.value getOrElse error("Server inactive")
       
   115   def resources: VSCode_Resources = session.resources.asInstanceOf[VSCode_Resources]
       
   116 
       
   117   def rendering_offset(node_pos: Line.Node_Position): Option[(VSCode_Rendering, Text.Offset)] =
       
   118     for {
       
   119       model <- resources.get_model(new JFile(node_pos.name))
       
   120       rendering = model.rendering()
       
   121       offset <- model.content.doc.offset(node_pos.pos)
       
   122     } yield (rendering, offset)
       
   123 
       
   124   private val dynamic_output = Dynamic_Output(server)
       
   125 
       
   126 
       
   127   /* input from client or file-system */
       
   128 
       
   129   private val delay_input: Delay =
       
   130     Delay.last(options.seconds("vscode_input_delay"), channel.Error_Logger)
       
   131     { resources.flush_input(session, channel) }
       
   132 
       
   133   private val delay_load: Delay =
       
   134     Delay.last(options.seconds("vscode_load_delay"), channel.Error_Logger) {
       
   135       val (invoke_input, invoke_load) =
       
   136         resources.resolve_dependencies(session, editor, file_watcher)
       
   137       if (invoke_input) delay_input.invoke()
       
   138       if (invoke_load) delay_load.invoke
       
   139     }
       
   140 
       
   141   private val file_watcher =
       
   142     File_Watcher(sync_documents, options.seconds("vscode_load_delay"))
       
   143 
       
   144   private def close_document(file: JFile)
       
   145   {
       
   146     if (resources.close_model(file)) {
       
   147       file_watcher.register_parent(file)
       
   148       sync_documents(Set(file))
       
   149       delay_input.invoke()
       
   150       delay_output.invoke()
       
   151     }
       
   152   }
       
   153 
       
   154   private def sync_documents(changed: Set[JFile])
       
   155   {
       
   156     resources.sync_models(changed)
       
   157     delay_input.invoke()
       
   158     delay_output.invoke()
       
   159   }
       
   160 
       
   161   private def change_document(file: JFile, version: Long, changes: List[LSP.TextDocumentChange])
       
   162   {
       
   163     val norm_changes = new mutable.ListBuffer[LSP.TextDocumentChange]
       
   164     @tailrec def norm(chs: List[LSP.TextDocumentChange])
       
   165     {
       
   166       if (chs.nonEmpty) {
       
   167         val (full_texts, rest1) = chs.span(_.range.isEmpty)
       
   168         val (edits, rest2) = rest1.span(_.range.nonEmpty)
       
   169         norm_changes ++= full_texts
       
   170         norm_changes ++= edits.sortBy(_.range.get.start)(Line.Position.Ordering).reverse
       
   171         norm(rest2)
       
   172       }
       
   173     }
       
   174     norm(changes)
       
   175     norm_changes.foreach(change =>
       
   176       resources.change_model(session, editor, file, version, change.text, change.range))
       
   177 
       
   178     delay_input.invoke()
       
   179     delay_output.invoke()
       
   180   }
       
   181 
       
   182 
       
   183   /* caret handling */
       
   184 
       
   185   private val delay_caret_update: Delay =
       
   186     Delay.last(options.seconds("vscode_input_delay"), channel.Error_Logger)
       
   187     { session.caret_focus.post(Session.Caret_Focus) }
       
   188 
       
   189   private def update_caret(caret: Option[(JFile, Line.Position)])
       
   190   {
       
   191     resources.update_caret(caret)
       
   192     delay_caret_update.invoke()
       
   193     delay_input.invoke()
       
   194   }
       
   195 
       
   196 
       
   197   /* preview */
       
   198 
       
   199   private lazy val preview_panel = new Preview_Panel(resources)
       
   200 
       
   201   private lazy val delay_preview: Delay =
       
   202     Delay.last(options.seconds("vscode_output_delay"), channel.Error_Logger)
       
   203     {
       
   204       if (preview_panel.flush(channel)) delay_preview.invoke()
       
   205     }
       
   206 
       
   207   private def request_preview(file: JFile, column: Int)
       
   208   {
       
   209     preview_panel.request(file, column)
       
   210     delay_preview.invoke()
       
   211   }
       
   212 
       
   213 
       
   214   /* output to client */
       
   215 
       
   216   private val delay_output: Delay =
       
   217     Delay.last(options.seconds("vscode_output_delay"), channel.Error_Logger)
       
   218     {
       
   219       if (resources.flush_output(channel)) delay_output.invoke()
       
   220     }
       
   221 
       
   222   def update_output(changed_nodes: Traversable[JFile])
       
   223   {
       
   224     resources.update_output(changed_nodes)
       
   225     delay_output.invoke()
       
   226   }
       
   227 
       
   228   def update_output_visible()
       
   229   {
       
   230     resources.update_output_visible()
       
   231     delay_output.invoke()
       
   232   }
       
   233 
       
   234   private val prover_output =
       
   235     Session.Consumer[Session.Commands_Changed](getClass.getName) {
       
   236       case changed =>
       
   237         update_output(changed.nodes.toList.map(resources.node_file(_)))
       
   238     }
       
   239 
       
   240   private val syslog_messages =
       
   241     Session.Consumer[Prover.Output](getClass.getName) {
       
   242       case output => channel.log_writeln(resources.output_xml(output.message))
       
   243     }
       
   244 
       
   245 
       
   246   /* init and exit */
       
   247 
       
   248   def init(id: LSP.Id)
       
   249   {
       
   250     def reply_ok(msg: String)
       
   251     {
       
   252       channel.write(LSP.Initialize.reply(id, ""))
       
   253       channel.writeln(msg)
       
   254     }
       
   255 
       
   256     def reply_error(msg: String)
       
   257     {
       
   258       channel.write(LSP.Initialize.reply(id, msg))
       
   259       channel.error_message(msg)
       
   260     }
       
   261 
       
   262     val try_session =
       
   263       try {
       
   264         val base_info =
       
   265           Sessions.base_info(
       
   266             options, session_name, dirs = session_dirs, include_sessions = include_sessions,
       
   267             session_ancestor = session_ancestor, session_requirements = session_requirements).check
       
   268 
       
   269         def build(no_build: Boolean = false): Build.Results =
       
   270           Build.build(options,
       
   271             selection = Sessions.Selection.session(base_info.session),
       
   272             build_heap = true, no_build = no_build, dirs = session_dirs, infos = base_info.infos)
       
   273 
       
   274         if (!build(no_build = true).ok) {
       
   275           val start_msg = "Build started for Isabelle/" + base_info.session + " ..."
       
   276           val fail_msg = "Session build failed -- prover process remains inactive!"
       
   277 
       
   278           val progress = channel.progress(verbose = true)
       
   279           progress.echo(start_msg); channel.writeln(start_msg)
       
   280 
       
   281           if (!build().ok) { progress.echo(fail_msg); error(fail_msg) }
       
   282         }
       
   283 
       
   284         val resources = new VSCode_Resources(options, base_info, log)
       
   285           {
       
   286             override def commit(change: Session.Change): Unit =
       
   287               if (change.deps_changed || undefined_blobs(change.version.nodes).nonEmpty)
       
   288                 delay_load.invoke()
       
   289           }
       
   290 
       
   291         val session_options = options.bool("editor_output_state") = true
       
   292         val session = new Session(session_options, resources)
       
   293 
       
   294         Some((base_info, session))
       
   295       }
       
   296       catch { case ERROR(msg) => reply_error(msg); None }
       
   297 
       
   298     for ((base_info, session) <- try_session) {
       
   299       session_.change(_ => Some(session))
       
   300 
       
   301       session.commands_changed += prover_output
       
   302       session.syslog_messages += syslog_messages
       
   303 
       
   304       dynamic_output.init()
       
   305 
       
   306       try {
       
   307         Isabelle_Process(session, options, base_info.sessions_structure, Sessions.store(options),
       
   308           modes = modes, logic = base_info.session).await_startup
       
   309         reply_ok("Welcome to Isabelle/" + base_info.session + " (" + Distribution.version + ")")
       
   310       }
       
   311       catch { case ERROR(msg) => reply_error(msg) }
       
   312     }
       
   313   }
       
   314 
       
   315   def shutdown(id: LSP.Id)
       
   316   {
       
   317     def reply(err: String): Unit = channel.write(LSP.Shutdown.reply(id, err))
       
   318 
       
   319     session_.change({
       
   320       case Some(session) =>
       
   321         session.commands_changed -= prover_output
       
   322         session.syslog_messages -= syslog_messages
       
   323 
       
   324         dynamic_output.exit()
       
   325 
       
   326         delay_load.revoke()
       
   327         file_watcher.shutdown()
       
   328         delay_input.revoke()
       
   329         delay_output.revoke()
       
   330         delay_caret_update.revoke()
       
   331         delay_preview.revoke()
       
   332 
       
   333         val result = session.stop()
       
   334         if (result.ok) reply("")
       
   335         else reply("Prover shutdown failed: " + result.rc)
       
   336         None
       
   337       case None =>
       
   338         reply("Prover inactive")
       
   339         None
       
   340     })
       
   341   }
       
   342 
       
   343   def exit() {
       
   344     log("\n")
       
   345     sys.exit(if (session_.value.isDefined) 2 else 0)
       
   346   }
       
   347 
       
   348 
       
   349   /* completion */
       
   350 
       
   351   def completion(id: LSP.Id, node_pos: Line.Node_Position)
       
   352   {
       
   353     val result =
       
   354       (for ((rendering, offset) <- rendering_offset(node_pos))
       
   355         yield rendering.completion(node_pos.pos, offset)) getOrElse Nil
       
   356     channel.write(LSP.Completion.reply(id, result))
       
   357   }
       
   358 
       
   359 
       
   360   /* spell-checker dictionary */
       
   361 
       
   362   def update_dictionary(include: Boolean, permanent: Boolean)
       
   363   {
       
   364     for {
       
   365       spell_checker <- resources.spell_checker.get
       
   366       caret <- resources.get_caret()
       
   367       rendering = caret.model.rendering()
       
   368       range = rendering.before_caret_range(caret.offset)
       
   369       Text.Info(_, word) <- Spell_Checker.current_word(rendering, range)
       
   370     } {
       
   371       spell_checker.update(word, include, permanent)
       
   372       update_output_visible()
       
   373     }
       
   374   }
       
   375 
       
   376   def reset_dictionary()
       
   377   {
       
   378     for (spell_checker <- resources.spell_checker.get)
       
   379     {
       
   380       spell_checker.reset()
       
   381       update_output_visible()
       
   382     }
       
   383   }
       
   384 
       
   385 
       
   386   /* hover */
       
   387 
       
   388   def hover(id: LSP.Id, node_pos: Line.Node_Position)
       
   389   {
       
   390     val result =
       
   391       for {
       
   392         (rendering, offset) <- rendering_offset(node_pos)
       
   393         info <- rendering.tooltips(VSCode_Rendering.tooltip_elements, Text.Range(offset, offset + 1))
       
   394       } yield {
       
   395         val range = rendering.model.content.doc.range(info.range)
       
   396         val contents = info.info.map(t => LSP.MarkedString(resources.output_pretty_tooltip(List(t))))
       
   397         (range, contents)
       
   398       }
       
   399     channel.write(LSP.Hover.reply(id, result))
       
   400   }
       
   401 
       
   402 
       
   403   /* goto definition */
       
   404 
       
   405   def goto_definition(id: LSP.Id, node_pos: Line.Node_Position)
       
   406   {
       
   407     val result =
       
   408       (for ((rendering, offset) <- rendering_offset(node_pos))
       
   409         yield rendering.hyperlinks(Text.Range(offset, offset + 1))) getOrElse Nil
       
   410     channel.write(LSP.GotoDefinition.reply(id, result))
       
   411   }
       
   412 
       
   413 
       
   414   /* document highlights */
       
   415 
       
   416   def document_highlights(id: LSP.Id, node_pos: Line.Node_Position)
       
   417   {
       
   418     val result =
       
   419       (for ((rendering, offset) <- rendering_offset(node_pos))
       
   420         yield {
       
   421           val model = rendering.model
       
   422           rendering.caret_focus_ranges(Text.Range(offset, offset + 1), model.content.text_range)
       
   423             .map(r => LSP.DocumentHighlight.text(model.content.doc.range(r)))
       
   424         }) getOrElse Nil
       
   425     channel.write(LSP.DocumentHighlights.reply(id, result))
       
   426   }
       
   427 
       
   428 
       
   429   /* main loop */
       
   430 
       
   431   def start()
       
   432   {
       
   433     log("Server started " + Date.now())
       
   434 
       
   435     def handle(json: JSON.T)
       
   436     {
       
   437       try {
       
   438         json match {
       
   439           case LSP.Initialize(id) => init(id)
       
   440           case LSP.Initialized(()) =>
       
   441           case LSP.Shutdown(id) => shutdown(id)
       
   442           case LSP.Exit(()) => exit()
       
   443           case LSP.DidOpenTextDocument(file, _, version, text) =>
       
   444             change_document(file, version, List(LSP.TextDocumentChange(None, text)))
       
   445             delay_load.invoke()
       
   446           case LSP.DidChangeTextDocument(file, version, changes) =>
       
   447             change_document(file, version, changes)
       
   448           case LSP.DidCloseTextDocument(file) => close_document(file)
       
   449           case LSP.Completion(id, node_pos) => completion(id, node_pos)
       
   450           case LSP.Include_Word(()) => update_dictionary(true, false)
       
   451           case LSP.Include_Word_Permanently(()) => update_dictionary(true, true)
       
   452           case LSP.Exclude_Word(()) => update_dictionary(false, false)
       
   453           case LSP.Exclude_Word_Permanently(()) => update_dictionary(false, true)
       
   454           case LSP.Reset_Words(()) => reset_dictionary()
       
   455           case LSP.Hover(id, node_pos) => hover(id, node_pos)
       
   456           case LSP.GotoDefinition(id, node_pos) => goto_definition(id, node_pos)
       
   457           case LSP.DocumentHighlights(id, node_pos) => document_highlights(id, node_pos)
       
   458           case LSP.Caret_Update(caret) => update_caret(caret)
       
   459           case LSP.State_Init(()) => State_Panel.init(server)
       
   460           case LSP.State_Exit(id) => State_Panel.exit(id)
       
   461           case LSP.State_Locate(id) => State_Panel.locate(id)
       
   462           case LSP.State_Update(id) => State_Panel.update(id)
       
   463           case LSP.State_Auto_Update(id, enabled) => State_Panel.auto_update(id, enabled)
       
   464           case LSP.Preview_Request(file, column) => request_preview(file, column)
       
   465           case LSP.Symbols_Request(()) => channel.write(LSP.Symbols())
       
   466           case _ => if (!LSP.ResponseMessage.is_empty(json)) log("### IGNORED")
       
   467         }
       
   468       }
       
   469       catch { case exn: Throwable => channel.log_error_message(Exn.message(exn)) }
       
   470     }
       
   471 
       
   472     @tailrec def loop()
       
   473     {
       
   474       channel.read() match {
       
   475         case Some(json) =>
       
   476           json match {
       
   477             case bulk: List[_] => bulk.foreach(handle)
       
   478             case _ => handle(json)
       
   479           }
       
   480           loop()
       
   481         case None => log("### TERMINATE")
       
   482       }
       
   483     }
       
   484     loop()
       
   485   }
       
   486 
       
   487 
       
   488   /* abstract editor operations */
       
   489 
       
   490   object editor extends Language_Server.Editor
       
   491   {
       
   492     /* session */
       
   493 
       
   494     override def session: Session = server.session
       
   495     override def flush(): Unit = resources.flush_input(session, channel)
       
   496     override def invoke(): Unit = delay_input.invoke()
       
   497 
       
   498 
       
   499     /* current situation */
       
   500 
       
   501     override def current_node(context: Unit): Option[Document.Node.Name] =
       
   502       resources.get_caret().map(_.model.node_name)
       
   503     override def current_node_snapshot(context: Unit): Option[Document.Snapshot] =
       
   504       resources.get_caret().map(_.model.snapshot())
       
   505 
       
   506     override def node_snapshot(name: Document.Node.Name): Document.Snapshot =
       
   507     {
       
   508       resources.get_model(name) match {
       
   509         case Some(model) => model.snapshot()
       
   510         case None => session.snapshot(name)
       
   511       }
       
   512     }
       
   513 
       
   514     def current_command(snapshot: Document.Snapshot): Option[Command] =
       
   515     {
       
   516       resources.get_caret() match {
       
   517         case Some(caret) => snapshot.current_command(caret.node_name, caret.offset)
       
   518         case None => None
       
   519       }
       
   520     }
       
   521     override def current_command(context: Unit, snapshot: Document.Snapshot): Option[Command] =
       
   522       current_command(snapshot)
       
   523 
       
   524 
       
   525     /* overlays */
       
   526 
       
   527     override def node_overlays(name: Document.Node.Name): Document.Node.Overlays =
       
   528       resources.node_overlays(name)
       
   529 
       
   530     override def insert_overlay(command: Command, fn: String, args: List[String]): Unit =
       
   531       resources.insert_overlay(command, fn, args)
       
   532 
       
   533     override def remove_overlay(command: Command, fn: String, args: List[String]): Unit =
       
   534       resources.remove_overlay(command, fn, args)
       
   535 
       
   536 
       
   537     /* hyperlinks */
       
   538 
       
   539     override def hyperlink_command(
       
   540       focus: Boolean, snapshot: Document.Snapshot, id: Document_ID.Generic,
       
   541       offset: Symbol.Offset = 0): Option[Hyperlink] =
       
   542     {
       
   543       if (snapshot.is_outdated) None
       
   544       else
       
   545         snapshot.find_command_position(id, offset).map(node_pos =>
       
   546           new Hyperlink {
       
   547             def follow(unit: Unit) { channel.write(LSP.Caret_Update(node_pos, focus)) }
       
   548           })
       
   549     }
       
   550 
       
   551 
       
   552     /* dispatcher thread */
       
   553 
       
   554     override def assert_dispatcher[A](body: => A): A = session.assert_dispatcher(body)
       
   555     override def require_dispatcher[A](body: => A): A = session.require_dispatcher(body)
       
   556     override def send_dispatcher(body: => Unit): Unit = session.send_dispatcher(body)
       
   557     override def send_wait_dispatcher(body: => Unit): Unit = session.send_wait_dispatcher(body)
       
   558   }
       
   559 }