diff --git a/src/Pure/General/pretty.scala b/src/Pure/General/pretty.scala --- a/src/Pure/General/pretty.scala +++ b/src/Pure/General/pretty.scala @@ -1,189 +1,189 @@ /* Title: Pure/General/pretty.scala Author: Makarius Generic pretty printing module. */ package isabelle import scala.annotation.tailrec object Pretty { /* XML constructors */ val space: XML.Body = List(XML.Text(Symbol.space)) def spaces(n: Int): XML.Body = if (n == 0) Nil else if (n == 1) space else List(XML.Text(Symbol.spaces(n))) def block(consistent: Boolean, indent: Int, body: XML.Body): XML.Tree = XML.Elem(Markup.Block(consistent, indent), body) def block(indent: Int, body: XML.Body): XML.Tree = block(false, indent, body) def block(body: XML.Body): XML.Tree = block(2, body) def brk(width: Int, indent: Int = 0): XML.Tree = XML.Elem(Markup.Break(width, indent), spaces(width)) val fbrk: XML.Tree = XML.newline def fbreaks(ts: List[XML.Tree]): XML.Body = Library.separate(fbrk, ts) val Separator: XML.Body = List(XML.elem(Markup.SEPARATOR, space), fbrk) def separate(ts: List[XML.Tree]): XML.Body = Library.separate(Separator, ts.map(List(_))).flatten /* text metric -- standardized to width of space */ abstract class Metric { val unit: Double def apply(s: String): Double } object Default_Metric extends Metric { val unit = 1.0 def apply(s: String): Double = s.length.toDouble } /* markup trees with physical blocks and breaks */ private def force_nat(i: Int): Int = i max 0 private sealed abstract class Tree { def length: Double } private case class Block( markup: Option[(Markup, Option[XML.Body])], consistent: Boolean, indent: Int, body: List[Tree], length: Double) extends Tree private case class Break(force: Boolean, width: Int, indent: Int) extends Tree { def length: Double = width.toDouble } private case class Str(string: String, length: Double) extends Tree private val FBreak = Break(true, 1, 0) private def make_block( markup: Option[(Markup, Option[XML.Body])], consistent: Boolean, indent: Int, body: List[Tree]): Tree = { val indent1 = force_nat(indent) @tailrec def body_length(prts: List[Tree], len: Double): Double = { val (line, rest) = Library.take_prefix[Tree]({ case Break(true, _, _) => false case _ => true }, prts) val len1 = ((0.0 /: line) { case (l, t) => l + t.length }) max len - rest match { + (rest: @unchecked) match { case Break(true, _, ind) :: rest1 => body_length(Break(false, indent1 + ind, 0) :: rest1, len1) case Nil => len1 } } Block(markup, consistent, indent1, body, body_length(body, 0.0)) } /* formatted output */ private sealed case class Text(tx: XML.Body = Nil, pos: Double = 0.0, nl: Int = 0) { def newline: Text = copy(tx = fbrk :: tx, pos = 0.0, nl = nl + 1) def string(s: String, len: Double): Text = copy(tx = if (s == "") tx else XML.Text(s) :: tx, pos = pos + len) def blanks(wd: Int): Text = string(Symbol.spaces(wd), wd.toDouble) def content: XML.Body = tx.reverse } private def break_dist(trees: List[Tree], after: Double): Double = trees match { case (_: Break) :: _ => 0.0 case t :: ts => t.length + break_dist(ts, after) case Nil => after } private def force_break(tree: Tree): Tree = tree match { case Break(false, wd, ind) => Break(true, wd, ind) case _ => tree } private def force_all(trees: List[Tree]): List[Tree] = trees.map(force_break) private def force_next(trees: List[Tree]): List[Tree] = trees match { case Nil => Nil case (t: Break) :: ts => force_break(t) :: ts case t :: ts => t :: force_next(ts) } val default_margin: Double = 76.0 val default_breakgain: Double = default_margin / 20 def formatted(input: XML.Body, margin: Double = default_margin, breakgain: Double = default_breakgain, metric: Metric = Default_Metric): XML.Body = { val emergencypos = (margin / 2).round.toInt def make_tree(inp: XML.Body): List[Tree] = inp flatMap { case XML.Wrapped_Elem(markup, body1, body2) => List(make_block(Some(markup, Some(body1)), false, 0, make_tree(body2))) case XML.Elem(markup, body) => markup match { case Markup.Block(consistent, indent) => List(make_block(None, consistent, indent, make_tree(body))) case Markup.Break(width, indent) => List(Break(false, force_nat(width), force_nat(indent))) case Markup(Markup.ITEM, _) => List(make_block(None, false, 2, make_tree(XML.elem(Markup.BULLET, space) :: space ::: body))) case _ => List(make_block(Some((markup, None)), false, 0, make_tree(body))) } case XML.Text(text) => Library.separate(FBreak, split_lines(text).map(s => Str(s, metric(s)))) } def format(trees: List[Tree], blockin: Int, after: Double, text: Text): Text = trees match { case Nil => text case Block(markup, consistent, indent, body, blen) :: ts => val pos1 = (text.pos + indent).ceil.toInt val pos2 = pos1 % emergencypos val blockin1 = if (pos1 < emergencypos) pos1 else pos2 val d = break_dist(ts, after) val body1 = if (consistent && text.pos + blen > margin - d) force_all(body) else body val btext = markup match { case None => format(body1, blockin1, d, text) case Some((m, markup_body)) => val btext0 = format(body1, blockin1, d, text.copy(tx = Nil)) val elem = markup_body match { case None => XML.Elem(m, btext0.content) case Some(b) => XML.Wrapped_Elem(m, b, btext0.content) } btext0.copy(tx = elem :: text.tx) } val ts1 = if (text.nl < btext.nl) force_next(ts) else ts format(ts1, blockin, after, btext) case Break(force, wd, ind) :: ts => if (!force && text.pos + wd <= ((margin - break_dist(ts, after)) max (blockin + breakgain))) format(ts, blockin, after, text.blanks(wd)) else format(ts, blockin, after, text.newline.blanks(blockin + ind)) case Str(s, len) :: ts => format(ts, blockin, after, text.string(s, len)) } format(make_tree(input), 0, 0.0, Text()).content } def string_of(input: XML.Body, margin: Double = default_margin, breakgain: Double = default_breakgain, metric: Metric = Default_Metric): String = XML.content(formatted(input, margin = margin, breakgain = breakgain, metric = metric)) } diff --git a/src/Pure/General/ssh.scala b/src/Pure/General/ssh.scala --- a/src/Pure/General/ssh.scala +++ b/src/Pure/General/ssh.scala @@ -1,501 +1,507 @@ /* Title: Pure/General/ssh.scala Author: Makarius SSH client based on JSch (see also http://www.jcraft.com/jsch/examples). */ package isabelle import java.io.{InputStream, OutputStream, ByteArrayOutputStream} import scala.collection.mutable import scala.util.matching.Regex import com.jcraft.jsch.{JSch, Logger => JSch_Logger, Session => JSch_Session, SftpException, - OpenSSHConfig, UserInfo, Channel => JSch_Channel, ChannelExec, ChannelSftp, SftpATTRS} + OpenSSHConfig, UserInfo, Channel => JSch_Channel, ChannelExec, ChannelSftp, SftpATTRS, + JSchException} object SSH { /* target machine: user@host syntax */ object Target { val User_Host: Regex = "^([^@]+)@(.+)$".r def parse(s: String): (String, String) = s match { case User_Host(user, host) => (user, host) case _ => ("", s) } def unapplySeq(s: String): Option[List[String]] = parse(s) match { case (_, "") => None case (user, host) => Some(List(user, host)) } } val default_port = 22 def make_port(port: Int): Int = if (port > 0) port else default_port def port_suffix(port: Int): String = if (port == default_port) "" else ":" + port def user_prefix(user: String): String = proper_string(user) match { case None => "" case Some(name) => name + "@" } def connect_timeout(options: Options): Int = options.seconds("ssh_connect_timeout").ms.toInt def alive_interval(options: Options): Int = options.seconds("ssh_alive_interval").ms.toInt def alive_count_max(options: Options): Int = options.int("ssh_alive_count_max") /* init context */ def init_context(options: Options): Context = { val config_dir = Path.explode(options.string("ssh_config_dir")) if (!config_dir.is_dir) error("Bad ssh config directory: " + config_dir) val jsch = new JSch val config_file = Path.explode(options.string("ssh_config_file")) if (config_file.is_file) jsch.setConfigRepository(OpenSSHConfig.parseFile(File.platform_path(config_file))) val known_hosts = config_dir + Path.explode("known_hosts") if (!known_hosts.is_file) known_hosts.file.createNewFile jsch.setKnownHosts(File.platform_path(known_hosts)) val identity_files = space_explode(':', options.string("ssh_identity_files")).map(Path.explode) - for (identity_file <- identity_files if identity_file.is_file) - jsch.addIdentity(File.platform_path(identity_file)) + for (identity_file <- identity_files if identity_file.is_file) { + try { jsch.addIdentity(File.platform_path(identity_file)) } + catch { + case exn: JSchException => + error("Error in ssh identity file " + identity_file + ": " + exn.getMessage) + } + } new Context(options, jsch) } def open_session(options: Options, host: String, user: String = "", port: Int = 0, actual_host: String = "", proxy_host: String = "", proxy_user: String = "", proxy_port: Int = 0, permissive: Boolean = false): Session = init_context(options).open_session( host = host, user = user, port = port, actual_host = actual_host, proxy_host = proxy_host, proxy_user = proxy_user, proxy_port = proxy_port, permissive = permissive) class Context private[SSH](val options: Options, val jsch: JSch) { def update_options(new_options: Options): Context = new Context(new_options, jsch) private def connect_session(host: String, user: String = "", port: Int = 0, host_key_permissive: Boolean = false, nominal_host: String = "", nominal_user: String = "", on_close: () => Unit = () => ()): Session = { val session = jsch.getSession(proper_string(user).orNull, host, make_port(port)) session.setUserInfo(No_User_Info) session.setServerAliveInterval(alive_interval(options)) session.setServerAliveCountMax(alive_count_max(options)) session.setConfig("MaxAuthTries", "3") if (host_key_permissive) session.setConfig("StrictHostKeyChecking", "no") if (nominal_host != "") session.setHostKeyAlias(nominal_host) if (options.bool("ssh_compression")) { session.setConfig("compression.s2c", "zlib@openssh.com,zlib,none") session.setConfig("compression.c2s", "zlib@openssh.com,zlib,none") session.setConfig("compression_level", "9") } session.connect(connect_timeout(options)) new Session(options, session, on_close, proper_string(nominal_host) getOrElse host, proper_string(nominal_user) getOrElse user) } def open_session( host: String, user: String = "", port: Int = 0, actual_host: String = "", proxy_host: String = "", proxy_user: String = "", proxy_port: Int = 0, permissive: Boolean = false): Session = { val connect_host = proper_string(actual_host) getOrElse host if (proxy_host == "") connect_session(host = connect_host, user = user, port = port) else { val proxy = connect_session(host = proxy_host, port = proxy_port, user = proxy_user) val fw = try { proxy.port_forwarding(remote_host = connect_host, remote_port = make_port(port)) } catch { case exn: Throwable => proxy.close; throw exn } try { connect_session(host = fw.local_host, port = fw.local_port, host_key_permissive = permissive, nominal_host = host, nominal_user = user, user = user, on_close = () => { fw.close; proxy.close }) } catch { case exn: Throwable => fw.close; proxy.close; throw exn } } } } /* logging */ def logging(verbose: Boolean = true, debug: Boolean = false) { JSch.setLogger(if (verbose) new Logger(debug) else null) } private class Logger(debug: Boolean) extends JSch_Logger { def isEnabled(level: Int): Boolean = level != JSch_Logger.DEBUG || debug def log(level: Int, msg: String) { level match { case JSch_Logger.ERROR | JSch_Logger.FATAL => Output.error_message(msg) case JSch_Logger.WARN => Output.warning(msg) case _ => Output.writeln(msg) } } } /* user info */ object No_User_Info extends UserInfo { def getPassphrase: String = null def getPassword: String = null def promptPassword(msg: String): Boolean = false def promptPassphrase(msg: String): Boolean = false def promptYesNo(msg: String): Boolean = false def showMessage(msg: String): Unit = Output.writeln(msg) } /* port forwarding */ object Port_Forwarding { def open(ssh: Session, ssh_close: Boolean, local_host: String, local_port: Int, remote_host: String, remote_port: Int): Port_Forwarding = { val port = ssh.session.setPortForwardingL(local_host, local_port, remote_host, remote_port) new Port_Forwarding(ssh, ssh_close, local_host, port, remote_host, remote_port) } } class Port_Forwarding private[SSH]( ssh: SSH.Session, ssh_close: Boolean, val local_host: String, val local_port: Int, val remote_host: String, val remote_port: Int) extends AutoCloseable { override def toString: String = local_host + ":" + local_port + ":" + remote_host + ":" + remote_port def close() { ssh.session.delPortForwardingL(local_host, local_port) if (ssh_close) ssh.close() } } /* Sftp channel */ type Attrs = SftpATTRS sealed case class Dir_Entry(name: String, is_dir: Boolean) { def is_file: Boolean = !is_dir } /* exec channel */ private val exec_wait_delay = Time.seconds(0.3) class Exec private[SSH](session: Session, channel: ChannelExec) extends AutoCloseable { override def toString: String = "exec " + session.toString def close() { channel.disconnect } val exit_status: Future[Int] = Future.thread("ssh_wait") { while (!channel.isClosed) exec_wait_delay.sleep channel.getExitStatus } val stdin: OutputStream = channel.getOutputStream val stdout: InputStream = channel.getInputStream val stderr: InputStream = channel.getErrStream // connect after preparing streams channel.connect(connect_timeout(session.options)) def result( progress_stdout: String => Unit = (_: String) => (), progress_stderr: String => Unit = (_: String) => (), strict: Boolean = true): Process_Result = { stdin.close def read_lines(stream: InputStream, progress: String => Unit): List[String] = { val result = new mutable.ListBuffer[String] val line_buffer = new ByteArrayOutputStream(100) def line_flush() { val line = Library.trim_line(line_buffer.toString(UTF8.charset_name)) progress(line) result += line line_buffer.reset } var c = 0 var finished = false while (!finished) { while ({ c = stream.read; c != -1 && c != 10 }) line_buffer.write(c) if (c == 10) line_flush() else if (channel.isClosed) { if (line_buffer.size > 0) line_flush() finished = true } else exec_wait_delay.sleep } result.toList } val out_lines = Future.thread("ssh_stdout") { read_lines(stdout, progress_stdout) } val err_lines = Future.thread("ssh_stderr") { read_lines(stderr, progress_stderr) } def terminate() { close out_lines.join err_lines.join exit_status.join } val rc = try { exit_status.join } catch { case Exn.Interrupt() => terminate(); Exn.Interrupt.return_code } close if (strict && rc == Exn.Interrupt.return_code) throw Exn.Interrupt() Process_Result(rc, out_lines.join, err_lines.join) } } /* session */ class Session private[SSH]( val options: Options, val session: JSch_Session, on_close: () => Unit, val nominal_host: String, val nominal_user: String) extends System with AutoCloseable { def update_options(new_options: Options): Session = new Session(new_options, session, on_close, nominal_host, nominal_user) def host: String = if (session.getHost == null) "" else session.getHost override def hg_url: String = "ssh://" + user_prefix(nominal_user) + nominal_host + "/" override def toString: String = user_prefix(session.getUserName) + host + port_suffix(session.getPort) + (if (session.isConnected) "" else " (disconnected)") /* port forwarding */ def port_forwarding( remote_port: Int, remote_host: String = "localhost", local_port: Int = 0, local_host: String = "localhost", ssh_close: Boolean = false): Port_Forwarding = Port_Forwarding.open(this, ssh_close, local_host, local_port, remote_host, remote_port) /* sftp channel */ val sftp: ChannelSftp = session.openChannel("sftp").asInstanceOf[ChannelSftp] sftp.connect(connect_timeout(options)) def close() { sftp.disconnect; session.disconnect; on_close() } val settings: Map[String, String] = { val home = sftp.getHome Map("HOME" -> home, "USER_HOME" -> home) } override def expand_path(path: Path): Path = path.expand_env(settings) def remote_path(path: Path): String = expand_path(path).implode override def bash_path(path: Path): String = Bash.string(remote_path(path)) def chmod(permissions: Int, path: Path): Unit = sftp.chmod(permissions, remote_path(path)) def mv(path1: Path, path2: Path): Unit = sftp.rename(remote_path(path1), remote_path(path2)) def rm(path: Path): Unit = sftp.rm(remote_path(path)) def mkdir(path: Path): Unit = sftp.mkdir(remote_path(path)) def rmdir(path: Path): Unit = sftp.rmdir(remote_path(path)) private def test_entry(path: Path, as_dir: Boolean): Boolean = try { val is_dir = sftp.stat(remote_path(path)).isDir if (as_dir) is_dir else !is_dir } catch { case _: SftpException => false } override def is_dir(path: Path): Boolean = test_entry(path, true) override def is_file(path: Path): Boolean = test_entry(path, false) def is_link(path: Path): Boolean = try { sftp.lstat(remote_path(path)).isLink } catch { case _: SftpException => false } override def mkdirs(path: Path): Unit = if (!is_dir(path)) { execute( "perl -e \"use File::Path make_path; make_path('" + remote_path(path) + "');\"") if (!is_dir(path)) error("Failed to create directory: " + quote(remote_path(path))) } def read_dir(path: Path): List[Dir_Entry] = { if (!is_dir(path)) error("No such directory: " + path.toString) val dir_name = remote_path(path) val dir = sftp.ls(dir_name) (for { i <- (0 until dir.size).iterator a = dir.get(i).asInstanceOf[AnyRef] name = Untyped.get[String](a, "filename") attrs = Untyped.get[Attrs](a, "attrs") if name != "." && name != ".." } yield { Dir_Entry(name, if (attrs.isLink) { try { sftp.stat(dir_name + "/" + name).isDir } catch { case _: SftpException => false } } else attrs.isDir) }).toList.sortBy(_.name) } def find_files( start: Path, pred: Path => Boolean = _ => true, include_dirs: Boolean = false, follow_links: Boolean = false): List[Path] = { val result = new mutable.ListBuffer[Path] def check(path: Path) { if (pred(path)) result += path } def find(dir: Path) { if (include_dirs) check(dir) if (follow_links || !is_link(dir)) { for (entry <- read_dir(dir)) { val path = dir + Path.basic(entry.name) if (entry.is_file) check(path) else find(path) } } } if (is_file(start)) check(start) else find(start) result.toList } def open_input(path: Path): InputStream = sftp.get(remote_path(path)) def open_output(path: Path): OutputStream = sftp.put(remote_path(path)) def read_file(path: Path, local_path: Path): Unit = sftp.get(remote_path(path), File.platform_path(local_path)) def read_bytes(path: Path): Bytes = using(open_input(path))(Bytes.read_stream(_)) def read(path: Path): String = using(open_input(path))(File.read_stream) def write_file(path: Path, local_path: Path): Unit = sftp.put(File.platform_path(local_path), remote_path(path)) def write_bytes(path: Path, bytes: Bytes): Unit = using(open_output(path))(bytes.write_stream) def write(path: Path, text: String): Unit = using(open_output(path))(stream => Bytes(text).write_stream(stream)) /* exec channel */ def exec(command: String): Exec = { val channel = session.openChannel("exec").asInstanceOf[ChannelExec] channel.setCommand("export USER_HOME=\"$HOME\"\n" + command) new Exec(this, channel) } override def execute(command: String, progress_stdout: String => Unit = (_: String) => (), progress_stderr: String => Unit = (_: String) => (), strict: Boolean = true): Process_Result = exec(command).result(progress_stdout, progress_stderr, strict) /* tmp dirs */ def rm_tree(dir: Path): Unit = rm_tree(remote_path(dir)) def rm_tree(remote_dir: String): Unit = execute("rm -r -f " + Bash.string(remote_dir)).check def tmp_dir(): String = execute("mktemp -d -t tmp.XXXXXXXXXX").check.out def with_tmp_dir[A](body: Path => A): A = { val remote_dir = tmp_dir() try { body(Path.explode(remote_dir)) } finally { rm_tree(remote_dir) } } } /* system operations */ trait System { def hg_url: String = "" def expand_path(path: Path): Path = path.expand def bash_path(path: Path): String = File.bash_path(path) def is_dir(path: Path): Boolean = path.is_dir def is_file(path: Path): Boolean = path.is_file def mkdirs(path: Path): Unit = Isabelle_System.mkdirs(path) def execute(command: String, progress_stdout: String => Unit = (_: String) => (), progress_stderr: String => Unit = (_: String) => (), strict: Boolean = true): Process_Result = Isabelle_System.bash(command, progress_stdout = progress_stdout, progress_stderr = progress_stderr, strict = strict) } object Local extends System } diff --git a/src/Pure/General/timing.scala b/src/Pure/General/timing.scala --- a/src/Pure/General/timing.scala +++ b/src/Pure/General/timing.scala @@ -1,76 +1,76 @@ /* Title: Pure/General/timing.scala Author: Makarius Support for time measurement. */ package isabelle import java.util.Locale object Timing { val zero: Timing = Timing(Time.zero, Time.zero, Time.zero) def timeit[A]( message: String = "", enabled: Boolean = true, output: String => Unit = Output.warning(_))(e: => A): A = { if (enabled) { val start = Time.now() val result = Exn.capture(e) val stop = Time.now() val timing = stop - start if (timing.is_relevant) { output( (if (message == null || message == "") "" else message + ": ") + timing.message + " elapsed time") } Exn.release(result) } else e } } sealed case class Timing(elapsed: Time, cpu: Time, gc: Time) { def is_zero: Boolean = elapsed.is_zero && cpu.is_zero && gc.is_zero def is_relevant: Boolean = elapsed.is_relevant || cpu.is_relevant || gc.is_relevant def resources: Time = cpu + gc def factor: Option[Double] = { val t1 = elapsed.seconds val t2 = resources.seconds if (t1 >= 3.0 && t2 >= 3.0) Some(t2 / t1) else None } def + (t: Timing): Timing = Timing(elapsed + t.elapsed, cpu + t.cpu, gc + t.gc) def message: String = elapsed.message + " elapsed time, " + cpu.message + " cpu time, " + gc.message + " GC time" def message_resources: String = { val factor_text = factor match { - case Some(f) => String.format(Locale.ROOT, ", factor %.2f", new java.lang.Double(f)) + case Some(f) => String.format(Locale.ROOT, ", factor %.2f", java.lang.Double.valueOf(f)) case None => "" } if (resources.seconds >= 3.0) elapsed.message_hms + " elapsed time, " + resources.message_hms + " cpu time" + factor_text else elapsed.message_hms + " elapsed time" + factor_text } override def toString: String = message def json: JSON.Object.T = JSON.Object("elapsed" -> elapsed.seconds, "cpu" -> cpu.seconds, "gc" -> gc.seconds) } diff --git a/src/Pure/Thy/latex.scala b/src/Pure/Thy/latex.scala --- a/src/Pure/Thy/latex.scala +++ b/src/Pure/Thy/latex.scala @@ -1,178 +1,179 @@ /* Title: Pure/Thy/latex.scala Author: Makarius Support for LaTeX. */ package isabelle import java.io.{File => JFile} import scala.annotation.tailrec import scala.util.matching.Regex object Latex { /** latex errors **/ def latex_errors(dir: Path, root_name: String): List[String] = { val root_log_path = dir + Path.explode(root_name).ext("log") if (root_log_path.is_file) { for { (msg, pos) <- filter_errors(dir, File.read(root_log_path)) } yield "Latex error" + Position.here(pos) + ":\n" + Library.prefix_lines(" ", msg) } else Nil } /* generated .tex file */ private val File_Pattern = """^%:%file=(.+)%:%$""".r private val Line_Pattern = """^*%:%(\d+)=(\d+)%:%$""".r def read_tex_file(tex_file: Path): Tex_File = { val positions = Line.logical_lines(File.read(tex_file)).reverse. takeWhile(_ != "\\endinput").reverse val source_file = positions match { case File_Pattern(file) :: _ => Some(file) case _ => None } val source_lines = if (source_file.isEmpty) Nil else positions.flatMap(line => line match { case Line_Pattern(Value.Int(line), Value.Int(source_line)) => Some(line -> source_line) case _ => None }) new Tex_File(tex_file, source_file, source_lines) } final class Tex_File private[Latex]( tex_file: Path, source_file: Option[String], source_lines: List[(Int, Int)]) { override def toString: String = tex_file.toString def source_position(l: Int): Option[Position.T] = source_file match { case None => None case Some(file) => val source_line = (0 /: source_lines.iterator.takeWhile({ case (m, _) => m <= l }))( { case (_, (_, n)) => n }) if (source_line == 0) None else Some(Position.Line_File(source_line, file)) } def position(line: Int): Position.T = source_position(line) getOrElse Position.Line_File(line, tex_file.implode) } /* latex log */ def filter_errors(dir: Path, root_log: String): List[(String, Position.T)] = { val seen_files = Synchronized(Map.empty[JFile, Tex_File]) def check_tex_file(path: Path): Option[Tex_File] = seen_files.change_result(seen => seen.get(path.file) match { case None => if (path.is_file) { val tex_file = read_tex_file(path) (Some(tex_file), seen + (path.file -> tex_file)) } else (None, seen) case some => (some, seen) }) def tex_file_position(path: Path, line: Int): Position.T = check_tex_file(path) match { case Some(tex_file) => tex_file.position(line) case None => Position.Line_File(line, path.implode) } object File_Line_Error { val Pattern: Regex = """^(.*?\.\w\w\w):(\d+): (.*)$""".r def unapply(line: String): Option[(Path, Int, String)] = line match { case Pattern(file, Value.Int(line), message) => val path = File.standard_path(file) if (Path.is_wellformed(path)) { val file = (dir + Path.explode(path)).canonical val msg = Library.perhaps_unprefix("LaTeX Error: ", message) if (file.is_file) Some((file, line, msg)) else None } else None case _ => None } } val Line_Error = """^l\.\d+ (.*)$""".r val More_Error = List( "", "