sourcepetanque::Pos.fan

//
// Copyright (c) 2012, Brian Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   10 Aug 08  Brian Frank  Creation
//

using fwt
using syntax

**
** Position in the document as zero based line and column number
**
const final class Pos
{
  ** Construct with zero based line and column
  new make(Int line, Int col)
  {
    this.line = line
    this.col  = col
  }

  ** Zero based line number
  const Int line

  ** Zero based column number
  const Int col

  ** Hash based on line and col
  override Int hash() { line.shiftl(8).xor(col) }

  ** Equality is based on line and col
  override Bool equals(Obj? that)
  {
    x := that as Pos
    if (x == null) return false
    return this.line == x.line && this.col == x.col
  }

  ** Compare by line, then col
  override Int compare(Obj that)
  {
    x := (Pos)that
    if (this.line < x.line) return -1
    if (this.line > x.line) return 1
    return this.col <=> x.col
  }

  ** Return zero based "line:col"
  override Str toStr() { "$line:$col" }

  ** Convert to span with this pos as both start and end
  Span toSpan() { Span(this, this) }

//////////////////////////////////////////////////////////////////////////
// Navigation
//////////////////////////////////////////////////////////////////////////

  internal Pos up(Doc doc)
  {
    if (line == 0) return this
    return Pos(line-1, col)
  }

  internal Pos down(Doc doc)
  {
    if (line >= doc.lineCount-1) return this
    return Pos(line+1, col)
  }

  internal Pos left(Doc doc)
  {
    if (col > 0) return Pos(line, col-1)
    if (line == 0) return this
    return Pos(line-1, doc.lines[line-1].size)
  }

  internal Pos right(Doc doc)
  {
    if (col <= doc.lines[line].size-1) return Pos(line, col+1)
    if (line >= doc.lineCount-1) return this
    return Pos(line+1, 0)
  }

  internal Pos home(Doc doc)
  {
    line := doc.line(this.line)
    nonws := 0
    while (nonws < line.size && line[nonws].isSpace) nonws++
    c := this.col <= nonws ? 0 : nonws
    return Pos(this.line, c)
  }

  internal Pos end(Doc doc)
  {
    line := doc.line(line)
    if (line.isEmpty) return Pos(this.line, 0)
    col := line.size-1
    while (col >= 0 && line[col].isSpace) col--
    return Pos(this.line, col+1)
  }

  internal Pos prevWord(Doc doc)
  {
    c := this.col
    if (c <= 0) return left(doc)
    line := doc.line(this.line)
    if (c >= line.size) c = line.size
    while (c > 0 && !isWord(line[c-1])) --c
    while (c > 0 && isWord(line[c-1])) --c
    return Pos(this.line, c)
  }

  internal Pos nextWord(Doc doc)
  {
    c := this.col
    line := doc.line(this.line)
    if (c >= line.size) return right(doc)
    while (c < line.size && !isWord(line[c])) ++c
    while (c < line.size && isWord(line[c])) ++c
    return Pos(this.line, c)
  }

  internal Pos endWord(Doc doc)
  {
    c := this.col
    line := doc.line(this.line)
    if (c >= line.size) return right(doc)

    // in space, then delete up to next non-space
    if (line[c].isSpace)
    {
      while (c < line.size && line[c].isSpace) ++c
    }

    // if in word then delete up to next non-word + spaces,
    // of in non-word then up to next word + spaces
    else
    {
      if (isWord(line[c]))
        while (c < line.size && isWord(line[c])) ++c
      else
        while (c < line.size && !isWord(line[c])) ++c
      while (c < line.size && line[c].isSpace) ++c
    }

    return Pos(this.line, c)
  }

  private static Bool isWord(Int char) { char.isAlphaNum || char == '_' }

}