src/Tools/VSCode/src/vscode_resources.scala
author wenzelm
Mon Jun 26 15:57:20 2017 +0200 (2017-06-26 ago)
changeset 66195 bb886f13623a
parent 66152 18e1aba549f6
child 66234 836898197296
permissions -rw-r--r--
proper bootstrap_name (amending b42743f5b595);
     1 /*  Title:      Tools/VSCode/src/vscode_resources.scala
     2     Author:     Makarius
     3 
     4 Resources for VSCode Language Server: file-system access and global state.
     5 */
     6 
     7 package isabelle.vscode
     8 
     9 
    10 import isabelle._
    11 
    12 import java.io.{File => JFile}
    13 
    14 import scala.util.parsing.input.Reader
    15 
    16 
    17 object VSCode_Resources
    18 {
    19   /* internal state */
    20 
    21   sealed case class State(
    22     models: Map[JFile, Document_Model] = Map.empty,
    23     caret: Option[(JFile, Line.Position)] = None,
    24     overlays: Document.Overlays = Document.Overlays.empty,
    25     pending_input: Set[JFile] = Set.empty,
    26     pending_output: Set[JFile] = Set.empty)
    27   {
    28     def update_models(changed: Traversable[(JFile, Document_Model)]): State =
    29       copy(
    30         models = models ++ changed,
    31         pending_input = (pending_input /: changed) { case (set, (file, _)) => set + file },
    32         pending_output = (pending_output /: changed) { case (set, (file, _)) => set + file })
    33 
    34     def update_caret(new_caret: Option[(JFile, Line.Position)]): State =
    35       if (caret == new_caret) this
    36       else
    37         copy(
    38           caret = new_caret,
    39           pending_input = pending_input ++ caret.map(_._1) ++ new_caret.map(_._1))
    40 
    41     def get_caret(file: JFile): Option[Line.Position] =
    42       caret match {
    43         case Some((caret_file, caret_pos)) if caret_file == file => Some(caret_pos)
    44         case _ => None
    45       }
    46 
    47     lazy val document_blobs: Document.Blobs =
    48       Document.Blobs(
    49         (for {
    50           (_, model) <- models.iterator
    51           blob <- model.get_blob
    52         } yield (model.node_name -> blob)).toMap)
    53 
    54     def change_overlay(insert: Boolean, file: JFile,
    55         command: Command, fn: String, args: List[String]): State =
    56       copy(
    57         overlays =
    58           if (insert) overlays.insert(command, fn, args)
    59           else overlays.remove(command, fn, args),
    60         pending_input = pending_input + file)
    61   }
    62 
    63 
    64   /* caret */
    65 
    66   sealed case class Caret(file: JFile, model: Document_Model, offset: Text.Offset)
    67   {
    68     def node_name: Document.Node.Name = model.node_name
    69   }
    70 }
    71 
    72 class VSCode_Resources(
    73     val options: Options,
    74     session_base: Sessions.Base,
    75     log: Logger = No_Logger)
    76   extends Resources(session_base, log = log)
    77 {
    78   private val state = Synchronized(VSCode_Resources.State())
    79 
    80 
    81   /* options */
    82 
    83   def pide_extensions: Boolean = options.bool("vscode_pide_extensions")
    84   def unicode_symbols: Boolean = options.bool("vscode_unicode_symbols")
    85   def tooltip_margin: Int = options.int("vscode_tooltip_margin")
    86   def message_margin: Int = options.int("vscode_message_margin")
    87 
    88 
    89   /* document node name */
    90 
    91   def node_file(name: Document.Node.Name): JFile = new JFile(name.node)
    92 
    93   def node_name(file: JFile): Document.Node.Name =
    94     session_base.known.get_file(file, bootstrap = true) getOrElse {
    95       val node = file.getPath
    96       theory_name(Sessions.DRAFT, Thy_Header.theory_name(node)) match {
    97         case (true, theory) => Document.Node.Name.loaded_theory(theory)
    98         case (false, theory) =>
    99           val master_dir = if (theory == "") "" else file.getParent
   100           Document.Node.Name(node, master_dir, theory)
   101       }
   102     }
   103 
   104   override def append(dir: String, source_path: Path): String =
   105   {
   106     val path = source_path.expand
   107     if (dir == "" || path.is_absolute) File.platform_path(path)
   108     else if (path.is_current) dir
   109     else if (path.is_basic && !dir.endsWith("/") && !dir.endsWith(JFile.separator))
   110       dir + JFile.separator + File.platform_path(path)
   111     else if (path.is_basic) dir + File.platform_path(path)
   112     else new JFile(dir + JFile.separator + File.platform_path(path)).getCanonicalPath
   113   }
   114 
   115   def get_models: Map[JFile, Document_Model] = state.value.models
   116   def get_model(file: JFile): Option[Document_Model] = get_models.get(file)
   117   def get_model(name: Document.Node.Name): Option[Document_Model] = get_model(node_file(name))
   118 
   119 
   120   /* file content */
   121 
   122   def read_file_content(file: JFile): Option[String] =
   123     try { Some(Line.normalize(File.read(file))) }
   124     catch { case ERROR(_) => None }
   125 
   126   def get_file_content(file: JFile): Option[String] =
   127     get_model(file) match {
   128       case Some(model) => Some(model.content.text)
   129       case None => read_file_content(file)
   130     }
   131 
   132   override def with_thy_reader[A](name: Document.Node.Name, f: Reader[Char] => A): A =
   133   {
   134     val file = node_file(name)
   135     get_model(file) match {
   136       case Some(model) => f(Scan.char_reader(model.content.text))
   137       case None if file.isFile =>
   138         val reader = Scan.byte_reader(file)
   139         try { f(reader) } finally { reader.close }
   140       case None =>
   141         error("No such file: " + quote(file.toString))
   142     }
   143   }
   144 
   145 
   146   /* document models */
   147 
   148   def visible_node(name: Document.Node.Name): Boolean =
   149     get_model(name) match {
   150       case Some(model) => model.node_visible
   151       case None => false
   152     }
   153 
   154   def change_model(
   155     session: Session,
   156     editor: Server.Editor,
   157     file: JFile,
   158     text: String,
   159     range: Option[Line.Range] = None)
   160   {
   161     state.change(st =>
   162       {
   163         val model = st.models.getOrElse(file, Document_Model.init(session, editor, node_name(file)))
   164         val model1 = (model.change_text(text, range) getOrElse model).external(false)
   165         st.update_models(Some(file -> model1))
   166       })
   167   }
   168 
   169   def close_model(file: JFile): Boolean =
   170     state.change_result(st =>
   171       st.models.get(file) match {
   172         case None => (false, st)
   173         case Some(model) => (true, st.update_models(Some(file -> model.external(true))))
   174       })
   175 
   176   def sync_models(changed_files: Set[JFile]): Unit =
   177     state.change(st =>
   178       {
   179         val changed_models =
   180           (for {
   181             (file, model) <- st.models.iterator
   182             if changed_files(file) && model.external_file
   183             text <- read_file_content(file)
   184             model1 <- model.change_text(text)
   185           } yield (file, model1)).toList
   186         st.update_models(changed_models)
   187       })
   188 
   189 
   190   /* overlays */
   191 
   192   def node_overlays(name: Document.Node.Name): Document.Node.Overlays =
   193     state.value.overlays(name)
   194 
   195   def insert_overlay(command: Command, fn: String, args: List[String]): Unit =
   196     state.change(_.change_overlay(true, node_file(command.node_name), command, fn, args))
   197 
   198   def remove_overlay(command: Command, fn: String, args: List[String]): Unit =
   199     state.change(_.change_overlay(false, node_file(command.node_name), command, fn, args))
   200 
   201 
   202   /* resolve dependencies */
   203 
   204   def resolve_dependencies(
   205     session: Session,
   206     editor: Server.Editor,
   207     file_watcher: File_Watcher): (Boolean, Boolean) =
   208   {
   209     state.change_result(st =>
   210       {
   211         /* theory files */
   212 
   213         val thys =
   214           (for ((_, model) <- st.models.iterator if model.is_theory)
   215            yield (model.node_name, Position.none)).toList
   216 
   217         val thy_files = thy_info.dependencies(thys).deps.map(_.name)
   218 
   219 
   220         /* auxiliary files */
   221 
   222         val stable_tip_version =
   223           if (st.models.forall(entry => entry._2.is_stable))
   224             session.current_state().stable_tip_version
   225           else None
   226 
   227         val aux_files =
   228           stable_tip_version match {
   229             case Some(version) => undefined_blobs(version.nodes)
   230             case None => Nil
   231           }
   232 
   233 
   234         /* loaded models */
   235 
   236         val loaded_models =
   237           (for {
   238             node_name <- thy_files.iterator ++ aux_files.iterator
   239             file = node_file(node_name)
   240             if !st.models.isDefinedAt(file)
   241             text <- { file_watcher.register_parent(file); read_file_content(file) }
   242           }
   243           yield {
   244             val model = Document_Model.init(session, editor, node_name)
   245             val model1 = (model.change_text(text) getOrElse model).external(true)
   246             (file, model1)
   247           }).toList
   248 
   249         val invoke_input = loaded_models.nonEmpty
   250         val invoke_load = stable_tip_version.isEmpty
   251 
   252         ((invoke_input, invoke_load), st.update_models(loaded_models))
   253       })
   254   }
   255 
   256 
   257   /* pending input */
   258 
   259   def flush_input(session: Session)
   260   {
   261     state.change(st =>
   262       {
   263         val changed_models =
   264           (for {
   265             file <- st.pending_input.iterator
   266             model <- st.models.get(file)
   267             (edits, model1) <- model.flush_edits(st.document_blobs, st.get_caret(file))
   268           } yield (edits, (file, model1))).toList
   269 
   270         session.update(st.document_blobs, changed_models.flatMap(_._1))
   271         st.copy(
   272           models = st.models ++ changed_models.iterator.map(_._2),
   273           pending_input = Set.empty)
   274       })
   275   }
   276 
   277 
   278   /* pending output */
   279 
   280   def update_output(changed_nodes: Traversable[JFile]): Unit =
   281     state.change(st => st.copy(pending_output = st.pending_output ++ changed_nodes))
   282 
   283   def update_output_visible(): Unit =
   284     state.change(st => st.copy(pending_output = st.pending_output ++
   285       (for ((file, model) <- st.models.iterator if model.node_visible) yield file)))
   286 
   287   def flush_output(channel: Channel): Boolean =
   288   {
   289     state.change_result(st =>
   290       {
   291         val (postponed, flushed) =
   292           (for {
   293             file <- st.pending_output.iterator
   294             model <- st.models.get(file)
   295           } yield (file, model, model.rendering())).toList.partition(_._3.snapshot.is_outdated)
   296 
   297         val changed_iterator =
   298           for {
   299             (file, model, rendering) <- flushed.iterator
   300             (changed_diags, changed_decos, model1) = model.publish(rendering)
   301             if changed_diags.isDefined || changed_decos.isDefined
   302           }
   303           yield {
   304             for (diags <- changed_diags)
   305               channel.write(Protocol.PublishDiagnostics(file, rendering.diagnostics_output(diags)))
   306             if (pide_extensions) {
   307               for (decos <- changed_decos; deco <- decos)
   308                 channel.write(rendering.decoration_output(deco).json(file))
   309             }
   310             (file, model1)
   311           }
   312 
   313         (postponed.nonEmpty,
   314           st.copy(
   315             models = st.models ++ changed_iterator,
   316             pending_output = postponed.map(_._1).toSet))
   317       }
   318     )
   319   }
   320 
   321 
   322   /* output text */
   323 
   324   def output_text(s: String): String =
   325     if (unicode_symbols) Symbol.decode(s) else Symbol.encode(s)
   326 
   327   def output_xml(xml: XML.Tree): String =
   328     output_text(XML.content(xml))
   329 
   330   def output_pretty(body: XML.Body, margin: Int): String =
   331     output_text(Pretty.string_of(body, margin))
   332   def output_pretty_tooltip(body: XML.Body): String = output_pretty(body, tooltip_margin)
   333   def output_pretty_message(body: XML.Body): String = output_pretty(body, message_margin)
   334 
   335 
   336   /* caret handling */
   337 
   338   def update_caret(caret: Option[(JFile, Line.Position)])
   339   { state.change(_.update_caret(caret)) }
   340 
   341   def get_caret(): Option[VSCode_Resources.Caret] =
   342   {
   343     val st = state.value
   344     for {
   345       (file, pos) <- st.caret
   346       model <- st.models.get(file)
   347       offset <- model.content.doc.offset(pos)
   348     }
   349     yield VSCode_Resources.Caret(file, model, offset)
   350   }
   351 
   352 
   353   /* spell checker */
   354 
   355   val spell_checker = new Spell_Checker_Variable
   356   spell_checker.update(options)
   357 }