Nanyx

Editor line navigation

Find the nearest line with content and the next change of indentation.

module editor.lines

import nanyx/text.regex

-- Navigating the lines of a text document: finding the nearest line with
-- content, and the next line where the indentation changes.

type TextLine = (number: int, text: string, firstNonWhitespace: int)
type TextDocument = (lineCount: int, lineAt: int -> TextLine)

export type Direction = #backwards | #forwards
export type Change = #greaterThan | #lessThan

export type RelativeIndentation =
  | #noIndentation
  | #lessIndentation
  | #moreIndentation
  | #sameIndentation

def TextLine.isBlank = { line -> line.text \trim == "" }

-- A "stop line" is one that is blank or contains only punctuation
export def TextLine.isStopLine = { line -> not (line.text \Regex.matches('[a-zA-Z0-9]')) }
export def TextLine.isSignificant = { line -> not line.isStopLine }

def changeToDiff: Change -> ((int, int) -> bool) = {
  | #greaterThan -> { > }
  | #lessThan -> { < }
}

def directionToDelta: Direction -> (int -> int) = {
  | #forwards -> { + 1 }
  | #backwards -> { - 1 }
}

-- Every line from `start` in the given direction, until the document ends
export def iterLines: (TextDocument, start: int, direction: Direction) -> seq(TextLine)
  = { document, start, direction ->
    def advance = directionToDelta(direction)
    seq {
      memory {
        def current = mut start
        while current >= 0 and current < document.lineCount ->
          yield document.lineAt(current)
          current := advance(current)
      }
    }
  }

type Neighbours = (#some(TextLine) | #none, #some(TextLine) | #none)

-- Walk outwards from a line, yielding the lines either side of it
export def iterLinesOutwards: (TextDocument, int) -> seq(Neighbours) = { document, lineNumber ->
  seq {
    memory {
      def back = mut lineNumber - 1
      def forward = mut lineNumber + 1

      while back >= 0 or forward < document.lineCount ->
        def backLine = if back >= 0 then #some(document.lineAt(back)) else #none
        def forwardLine = if forward < document.lineCount then #some(document.lineAt(forward)) else #none
        yield (backLine, forwardLine)
        back := back - 1
        forward := forward + 1
    }
  }
}

export def nearestSignificantLine: (TextDocument, int) -> TextLine = { document, lineNumber ->
  def current = document.lineAt(lineNumber)

  if current.isSignificant then current
  else
    def #some(line) =
      iterLinesOutwards(document, lineNumber)
        \flatMap { back, forward -> [back, forward] \Option.values }
        \find { .isSignificant }
        except #notFound -> #some(current)
    line
}

export def nextLineOfChangeOfIndentation
  : (TextDocument, TextLine, Change, Direction) -> #some(TextLine) | #none
  = { document, currentLine, change, direction ->
    def diff = changeToDiff(change)
    def start = directionToDelta(direction)(currentLine.number)

    iterLines(document, start = start, direction = direction)
      \filter { .isSignificant }
      \find { line -> diff(line.firstNonWhitespace, currentLine.firstNonWhitespace) }
      except #notFound -> #none
  }

export def relativeIndentation: (TextLine, TextLine) -> RelativeIndentation = { from, to ->
  if | to.isBlank -> #noIndentation
     | from.firstNonWhitespace > to.firstNonWhitespace -> #lessIndentation
     | from.firstNonWhitespace < to.firstNonWhitespace -> #moreIndentation
     | else -> #sameIndentation
}

This program lives in the repository as examples/editor.nyx.