//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 17 Sep 08 Brian Frank Creation
//
using concurrent
using fwt
**
** Mark is used to identify a uri with an optional
** line and column position.
**
const class Mark
{
**
** Default constructor with it-block.
**
new make(|This| f) { f(this) }
**
** Attempt to parse an arbitrary line of text into a mark.
** We attempt to match anything that looks like a absolute
** file name. If we match a filename, then we look for an
** optional line and column number no more than a few chars
** from the filename. This will correctly handle output from
** various compilers including Fantom compilers, javac, and the
** C# compiler. Return null if file path found.
**
static new fromStr(Str text) { MarkParser(text).parse }
**
** Uri of the resource
**
const Uri uri := ``
**
** One based line number or null if unknown.
** Note that fwt widgets are zero based.
**
const Int? line
**
** One based line column or null if unknown
** Note that fwt widgets are zero based.
**
const Int? col
**
** Hash code is based on uri, line, and col.
**
override Int hash()
{
hash := uri.hash
if (line != null) hash = hash.xor(line.shiftl(21))
if (col != null) hash = hash.xor(col.shiftl(11))
return hash
}
**
** Equality is based on uri, line, and col.
**
override Bool equals(Obj? that)
{
x := that as Mark
if (x == null) return false
return uri == x.uri && line == x.line && col == x.col
}
**
** Compare URIs, then lines, then columns
**
override Int compare(Obj that)
{
x := (Mark)that
cmp := uri <=> x.uri
if (cmp == 0) cmp = line <=> x.line
if (cmp == 0) cmp = col <=> x.col
return cmp
}
**
** Return string formatted as "uri:line:col" where the
** line and col are optional if null.
**
override Str toStr()
{
s := uri.toStr
if (line != null) s += ":$line"
if (col != null) s += ":$col"
return s
}
}
**************************************************************************
** MarkParser
**************************************************************************
**
** MarkParser is used to implement Mark.fromStr.
** It also keeps track of the string indices for
** the filename so console can shrink it.
**
internal class MarkParser
{
new make(Str text) { this.text = text }
Mark? parse()
{
try
{
return doParse
}
catch (Err e)
{
e.trace
return null
}
}
private Mark? doParse()
{
// use case insensitive compare on windows
text := this.text
if (Desktop.isWindows) text = text.lower
// attempt to match one of the root indices
Str? root := null
Int? s := null
rootDirs.each |Str rootDir|
{
x := text.index(rootDir)
if (x == null) return
if (s == null || x < s) { s = x; root = rootDir }
}
if (s == null) return null
// match up anything that looks like a directory
e := s + root.size
if (text.size <= e) return null
f := File.os(text[s..e])
while (text[e] == '/' || text[e] == '\\')
{
slash := Desktop.isWindows ? text.index("\\", e+1) : null
if (slash == null) slash = text.index("/", e+1)
if (slash == null) break
testf := File.os(text[s..slash])
if (!testf.exists) break
f = testf
e = slash
}
// try and find the longest matching file name in that directory
rest := text[e+1..-1]
Str[] names := f.list.map |File x->Str| { Desktop.isWindows ? x.name.lower : x.name }
names.sortr |Str a, Str b->Int| { a.size <=> b.size }
Str? n := names.eachWhile |Str n->Str?| { rest.startsWith(n) ? n : null }
if (n != null)
{
f = File.make(f.uri + n.toUri, false)
e += n.size
}
fileStart = s
fileEnd = e
// we now have our uri
Uri? uri := null
try { uri = f.normalize.uri } catch { return null }
// try to find a number for line
Int? num := null
for (i:=e+1; i<e+8 && i<text.size; ++i)
if (text[i].isDigit) { num=i; break }
if (num == null) return Mark { it.uri = uri }
// parse out line number
line := text[num] - '0'
while (++num < text.size && text[num].isDigit)
line = line*10 + (text[num] - '0')
// try to find a column number
e = num; num = null;
for (i:=e; i<e+8 && i<text.size; ++i)
if (text[i].isDigit) { num=i; break }
if (num == null) return Mark { it.uri = uri; it.line = line }
// parse out line number
col := text[num] - '0'
while (++num < text.size && text[num].isDigit)
col = col*10 + (text[num] - '0')
return Mark { it.uri = uri; it.line = line; it.col = col }
}
**
** Get a listing of the file system root paths
** to use for matching absolute filepaths.
**
internal static Str[] rootDirs()
{
Str[]? roots := Actor.locals["flux.Mark.roots"]
if (roots == null)
{
roots = Str[,]
File.osRoots.each |File f|
{
f.list.each |File x|
{
path := x.osPath
if (Desktop.isWindows) path = path.lower
roots.add(path)
}
}
roots.sortr |Str a, Str b->Int| { a.size <=> b.size }
Actor.locals["flux.Mark.roots"] = roots
}
return roots
}
Str? text
Int fileStart := -1
Int fileEnd := -1
}