<commit_before># # ICE Editor view
#
# Copyright (c) 2014 <NAME>
# MIT License

define ['ice-draw', 'ice-model'], (draw, model) ->
  NO_MULTILINE = 0
  MULTILINE_START = 1
  MULTILINE_MIDDLE = 2
  MULTILINE_END = 3
  MULTILINE_END_START = 4

  ANY_DROP = 0
  BLOCK_ONLY = 1
  MOSTLY_BLOCK = 2
  MOSTLY_VALUE = 3
  VALUE_ONLY = 4

  DEFAULT_OPTIONS =
    padding: 5
    indentWidth: 10
    indentTongueHeight: 10
    tabOffset: 10
    tabWidth: 15
    tabHeight: 5
    tabSideWidth: 0.125
    dropAreaHeight: 20
    indentDropAreaMinWidth: 50
    minSocketWidth: 10
    textHeight: 15
    textPadding: 1
    emptyLineWidth: 50
    highlightAreaHeight: 10
    bevelClip: 3
    shadowBlur: 5
    ctx: document.createElement('canvas').getContext('2d')
    colors:
      return: '#f2a6a6'
      control: '#efcf8f'
      value: '#8cec79'
      command: '#8fbfef'

      yellow: '#ecec79'
      cyan: '#79ecd9'
      violet: '#bfa6f2'
      magenta: '#f2a6e5'

  YES = -> yes
  NO = -> no

  exports = {}

  defaultStyleObject = -> {selected: 0, grayscale: 0}

  # # View
  # The View class contains options and caches
  # for rendering. The controller instantiates a View
  # and interacts with things through it.
  #
  # Inner classes in the View correspond to Model
  # types (e.g. SocketViewNode, etc.) all of which
  # will have access to their View's caches
  # and options object.
  exports.View = class View
    constructor: (@opts = {}) ->
      # @map maps Model objects
      # to corresponding View objects,
      # so that rerendering the same model
      # can be fast
      @map = {}

      # Apply default options
      for option of DEFAULT_OPTIONS
        unless option of @opts
          @opts[option] = DEFAULT_OPTIONS[option]

      # Do our measurement hack
      draw._setCTX @opts.ctx

    # Simple method for clearing caches
    clearCache: -> @map = {}

    # ## getViewNodeFor
    # Given a model object,
    # give the corresponding renderer object
    # under this View. If one does not exist,
    # create one, then preserve it in our map.
    getViewNodeFor: (model) ->
      if model.id of @map
        return @map[model.id]
      else
        return @createView(model)

    hasViewNodeFor: (model) -> model? and model.id of @map

    # ## createView
    # Given a model object, create a renderer object
    # of the appropriate type.
    createView: (model) ->
      switch model.type
        when 'text' then new TextViewNode model, this
        when 'block' then new BlockViewNode model, this
        when 'indent' then new IndentViewNode model, this
        when 'socket' then new SocketViewNode model, this
        when 'segment' then new SegmentViewNode model, this
        when 'cursor' then new CursorViewNode model, this

    # # GenericViewNode
    # Class from which all renderer classes will
    # extend.
    class GenericViewNode
      constructor: (@model, @view) ->
        # Record ourselves in the map
        # from model to renderer
        @view.map[@model.id] = this

        # *Zeroth pass variables*
        # computeChildren
        @lineLength = 0 # How many lines does this take up?
        @children = [] # All children, flat
        @lineChildren = [] # Children who own each line
        @multilineChildrenData = [] # Where do indents start/end?

        # *First pass variables*
        # computeMargins
        @margins = {left:0, right:0, top:0, bottom:0}
        @topLineSticksToBottom = false
        @bottomLineSticksToTop = false

        # *Pre-second pass variables*
        # computeMinDimensions
        # draw.Size type, {width:n, height:m}
        @minDimensions = [] # Dimensions on each line
        @minDistanceToBase = [] # {above:n, below:n}

        # *Second pass variables*
        # computeDimensions
        # draw.Size type, {width:n, height:m}
        @dimensions = [] # Dimensions on each line
        @distanceToBase = [] # {above:n, below:n}

        @bevels =
          topLeft: false
          topRight: false
          bottomLeft: false
          bottomRight: false

        # *Third/fifth pass variables*
        # computeBoundingBoxX, computeBoundingBoxY
        # draw.Rectangle type, {x:0, y:0, width:200, height:100}
        @bounds = [] # Bounding boxes on each line
        @changedBoundingBox = true # Did any bounding boxes change just now?

        # *Fourth pass variables*
        # computeGlue
        # {height:2, draw:true}
        @glue = {}

        # *Sixth pass variables*
        # computePath
        @path = new draw.Path()

        # *Seventh pass variables*
        # computeDropAreas
        # each one is a draw.Path (or null)
        @dropArea = @highlightArea = null

        # Versions. The corresponding
        # Model will keep corresponding version
        # numbers, and each of our passes can
        # become a no-op when we are up-to-date (so
        # that rerendering is fast when there are
        # few or no changes to the Model).
        @computedVersion = -1

      serialize: (line) ->
        result = []
        for prop in [
            'lineLength'
            'margins'
            'topLineSticksToBottom'
            'bottomLineSticksToTop'
            'changedBoundingBox'
            'path'
            'highlightArea'
            'computedVersion']
          result.push(prop + ': ' + JSON.stringify(@[prop]))
        for child, i in @children
          result.push("child #{i}: {startLine: #{child.startLine}, " +
                      "endLine: #{child.endLine}}")
        if line isnt null
          for prop in [
              'multilineChildrenData'
              'minDimensions'
              'minDistanceToBase'
              'dimensions'
              'distanceToBase'
              'bounds'
              'glue']
            result.push("#{prop} #{line}: #{JSON.stringify(@[prop][line])}")
          for child, i in @lineChildren[line]
            result.push("line #{line} child #{i}: " +
                        "{startLine: #{child.startLine}, " +
                        "endLine: #{child.endLine}}}")
        return result.join('\n')

      # ## computeChildren (GenericViewNode)
      # Find out which of our children lie on each line that we
      # own, and also how many lines we own.
      #
      # Return the number of lines we own.
      #
      # This is basically a void computeChildren that should be
      # overridden.
      computeChildren: -> @lineLength

      # ## computeMargins (GenericViewNode)
      # Compute the amount of margin required outside the child
      # on the top, bottom, left, and right.
      #
      # This is a void computeMargins that should be overridden.
      computeMargins: ->
        if @computedVersion is @model.version and
           (not @model.parent? or
           @model.parent.version is @view.getViewNodeFor(@model.parent).computedVersion)
          return @margins

        # the margins I need depend on the type of my parent
        parenttype = @model.parent?.type
        padding = @view.opts.padding

        left = if @model.isFirstOnLine() or @lineLength > 1 then padding else 0
        right = if @model.isLastOnLine() or @lineLength > 1 then padding else 0

        if parenttype is 'block' and @model.type is 'indent'
          @margins =
            top: 0
            bottom: @view.opts.indentTongueHeight

            firstLeft: 0
            midLeft: @view.opts.indentWidth
            lastLeft: @view.opts.indentWidth

            firstRight: 0
            midRight: 0
            lastRight: padding

        else if @model.type is 'text' and parenttype is 'socket'
          @margins =
            top: @view.opts.textPadding
            bottom: @view.opts.textPadding

            firstLeft: @view.opts.textPadding
            midLeft: @view.opts.textPadding
            lastLeft: @view.opts.textPadding

            firstRight: @view.opts.textPadding
            midRight: @view.opts.textPadding
            lastRight: @view.opts.textPadding

        else if @model.type is 'text' and parenttype is 'block'
          @margins =
            top: padding
            bottom: padding

            firstLeft: left
            midLeft: left
            lastLeft: left

            firstRight: right
            midRight: right
            lastRight: right

        else if parenttype is 'block'
          @margins =
            top: padding
            bottom: padding

            firstLeft: left
            midLeft: padding
            lastLeft: padding

            firstRight: right
            midRight: 0
            lastRight: right
        else
          @margins = {
            firstLeft: 0, midLeft:0, lastLeft: 0
            firstRight: 0, midRight:0, lastRight: 0
            top:0, bottom:0
          }

        @firstMargins =
          left: @margins.firstLeft
          right: @margins.firstRight
          top: @margins.top
          bottom: if @lineLength is 1 then @margins.bottom else 0

        @midMargins =
          left: @margins.midLeft
          right: @margins.midRight
          top: 0
          bottom: 0

        @lastMargins =
          left: @margins.lastLeft
          right: @margins.lastRight
          top: if @lineLength is 1 then @margins.top else 0
          bottom: @margins.bottom

        for childObj in @children
          @view.getViewNodeFor(childObj.child).computeMargins()

        return null

      getMargins: (line) ->
        if line is 0 then @firstMargins
        else if line is @lineLength - 1 then @lastMargins
        else @midMargins

      computeBevels: ->
        @bevels =
          topLeft: false
          topRight: false
          bottomLeft: false
          bottomRight: false

      
      # ## computeMinDimensions (GenericViewNode)
      # Compute the size of our bounding box on each
      # line that we contain.
      #
      # Return child node.
      #
      # This is a void computeDimensinos that should be overridden.
      computeMinDimensions: ->
        if @minDimensions.length > @lineLength
          @minDimensions.length = @minDistanceToBase.length = @lineLength
        else
          until @minDimensions.length is @lineLength
            @minDimensions.push new draw.Size 0, 0
            @minDistanceToBase.push {above: 0, below: 0}

        for i in [0...@lineLength]
          @minDimensions[i].width = @minDimensions[i].height = 0
          @minDistanceToBase[i].above = @minDistanceToBase[i].below = 0

        return null


      # ## computeDimensions (GenericViewNode)
      # Compute the size of our bounding box on each
      # line that we contain.
      #
      # Return child node.
      #
      # This is a void computeDimensinos that should be overridden.
      computeDimensions: (startLine, force, root = false) ->
        if @computedVersion is @model.version and not force
          return

        oldDimensions = @dimensions
        oldDistanceToBase = @distanceToBase

        # copy min dimensions
        if @dimensions.length > @lineLength
          @dimensions.length = @distanceToBase.length = @lineLength
        else
          until @dimensions.length is @lineLength
            @dimensions.push new draw.Size 0, 0
            @distanceToBase.push {above: 0, below: 0}

        for size, i in @minDimensions
          @dimensions[i].width = size.width; @dimensions[i].height = size.height
          @distanceToBase[i].above = @minDistanceToBase[i].above
          @distanceToBase[i].below = @minDistanceToBase[i].below

        if @model.parent? and not root and
            (@topLineSticksToBottom or @bottomLineSticksToTop)
          parentNode = @view.getViewNodeFor @model.parent

          # grow below if "stick to bottom" is set.
          if @topLineSticksToBottom
            distance = @distanceToBase[0]
            distance.below = Math.max(distance.below,
                parentNode.distanceToBase[startLine].below)
            @dimensions[0] = new draw.Size(
                @dimensions[0].width,
                distance.below + distance.above)

          # grow above if "stick to top" is set.
          if @bottomLineSticksToTop
            lineCount = @distanceToBase.length
            distance = @distanceToBase[lineCount - 1]
            distance.above = Math.max(distance.above,
                parentNode.distanceToBase[startLine + lineCount - 1].above)
            @dimensions[lineCount - 1] = new draw.Size(
                @dimensions[lineCount - 1].width,
                distance.below + distance.above)

        changed = (oldDimensions.length != @lineLength)
        if not changed then for line in [0...@lineLength]
          if !oldDimensions[line].equals(@dimensions[line]) or
              oldDistanceToBase[line].above != @distanceToBase[line].above or
              oldDistanceToBase[line].below != @distanceToBase[line].below
            changed = true
            break

        @changedBoundingBox or= changed

        for childObj in @children
          @view.getViewNodeFor(childObj.child).computeDimensions(
              childObj.startLine, changed)

        return null

      # ## computeBoundingBoxX (GenericViewNode)
      # Given the left edge coordinate for our bounding box on
      # this line, recursively layout the x-coordinates of us
      # and all our children on this line.
      computeBoundingBoxX: (left, line) ->
        # Attempt to use our cache. Because modifications in the parent
        # can affect the shape of child blocks, we can't only rely
        # on versioning. For instance, changing `fd 10` to `forward 10`
        # does not update the version on `10`, but the bounding box for
        # `10` still needs to change. So we must also check
        # that the coordinate we are given to begin the bounding box on matches.
        if @computedVersion is @model.version and
           left is @bounds[line]?.x and not @changedBoundingBox or
           @bounds[line]?.x is left and
           @bounds[line]?.width is @dimensions[line].width
          return @bounds[line]
        
        @changedBoundingBox = true

        # Avoid re-instantiating a Rectangle object,
        # if possible.
        if @bounds[line]?<commit_msg>Fix caching bug; might take performance hit<commit_after># # ICE Editor view
#
# Copyright (c) 2014 <NAME>
# MIT License

define ['ice-draw', 'ice-model'], (draw, model) ->
  NO_MULTILINE = 0
  MULTILINE_START = 1
  MULTILINE_MIDDLE = 2
  MULTILINE_END = 3
  MULTILINE_END_START = 4

  ANY_DROP = 0
  BLOCK_ONLY = 1
  MOSTLY_BLOCK = 2
  MOSTLY_VALUE = 3
  VALUE_ONLY = 4

  DEFAULT_OPTIONS =
    padding: 5
    indentWidth: 10
    indentTongueHeight: 10
    tabOffset: 10
    tabWidth: 15
    tabHeight: 5
    tabSideWidth: 0.125
    dropAreaHeight: 20
    indentDropAreaMinWidth: 50
    minSocketWidth: 10
    textHeight: 15
    textPadding: 1
    emptyLineWidth: 50
    highlightAreaHeight: 10
    bevelClip: 3
    shadowBlur: 5
    ctx: document.createElement('canvas').getContext('2d')
    colors:
      return: '#f2a6a6'
      control: '#efcf8f'
      value: '#8cec79'
      command: '#8fbfef'

      yellow: '#ecec79'
      cyan: '#79ecd9'
      violet: '#bfa6f2'
      magenta: '#f2a6e5'

  YES = -> yes
  NO = -> no

  exports = {}

  defaultStyleObject = -> {selected: 0, grayscale: 0}

  # # View
  # The View class contains options and caches
  # for rendering. The controller instantiates a View
  # and interacts with things through it.
  #
  # Inner classes in the View correspond to Model
  # types (e.g. SocketViewNode, etc.) all of which
  # will have access to their View's caches
  # and options object.
  exports.View = class View
    constructor: (@opts = {}) ->
      # @map maps Model objects
      # to corresponding View objects,
      # so that rerendering the same model
      # can be fast
      @map = {}

      # Apply default options
      for option of DEFAULT_OPTIONS
        unless option of @opts
          @opts[option] = DEFAULT_OPTIONS[option]

      # Do our measurement hack
      draw._setCTX @opts.ctx

    # Simple method for clearing caches
    clearCache: -> @map = {}

    # ## getViewNodeFor
    # Given a model object,
    # give the corresponding renderer object
    # under this View. If one does not exist,
    # create one, then preserve it in our map.
    getViewNodeFor: (model) ->
      if model.id of @map
        return @map[model.id]
      else
        return @createView(model)

    hasViewNodeFor: (model) -> model? and model.id of @map

    # ## createView
    # Given a model object, create a renderer object
    # of the appropriate type.
    createView: (model) ->
      switch model.type
        when 'text' then new TextViewNode model, this
        when 'block' then new BlockViewNode model, this
        when 'indent' then new IndentViewNode model, this
        when 'socket' then new SocketViewNode model, this
        when 'segment' then new SegmentViewNode model, this
        when 'cursor' then new CursorViewNode model, this

    # # GenericViewNode
    # Class from which all renderer classes will
    # extend.
    class GenericViewNode
      constructor: (@model, @view) ->
        # Record ourselves in the map
        # from model to renderer
        @view.map[@model.id] = this

        # *Zeroth pass variables*
        # computeChildren
        @lineLength = 0 # How many lines does this take up?
        @children = [] # All children, flat
        @lineChildren = [] # Children who own each line
        @multilineChildrenData = [] # Where do indents start/end?

        # *First pass variables*
        # computeMargins
        @margins = {left:0, right:0, top:0, bottom:0}
        @topLineSticksToBottom = false
        @bottomLineSticksToTop = false

        # *Pre-second pass variables*
        # computeMinDimensions
        # draw.Size type, {width:n, height:m}
        @minDimensions = [] # Dimensions on each line
        @minDistanceToBase = [] # {above:n, below:n}

        # *Second pass variables*
        # computeDimensions
        # draw.Size type, {width:n, height:m}
        @dimensions = [] # Dimensions on each line
        @distanceToBase = [] # {above:n, below:n}

        @bevels =
          topLeft: false
          topRight: false
          bottomLeft: false
          bottomRight: false

        # *Third/fifth pass variables*
        # computeBoundingBoxX, computeBoundingBoxY
        # draw.Rectangle type, {x:0, y:0, width:200, height:100}
        @bounds = [] # Bounding boxes on each line
        @changedBoundingBox = true # Did any bounding boxes change just now?

        # *Fourth pass variables*
        # computeGlue
        # {height:2, draw:true}
        @glue = {}

        # *Sixth pass variables*
        # computePath
        @path = new draw.Path()

        # *Seventh pass variables*
        # computeDropAreas
        # each one is a draw.Path (or null)
        @dropArea = @highlightArea = null

        # Versions. The corresponding
        # Model will keep corresponding version
        # numbers, and each of our passes can
        # become a no-op when we are up-to-date (so
        # that rerendering is fast when there are
        # few or no changes to the Model).
        @computedVersion = -1

      serialize: (line) ->
        result = []
        for prop in [
            'lineLength'
            'margins'
            'topLineSticksToBottom'
            'bottomLineSticksToTop'
            'changedBoundingBox'
            'path'
            'highlightArea'
            'computedVersion']
          result.push(prop + ': ' + JSON.stringify(@[prop]))
        for child, i in @children
          result.push("child #{i}: {startLine: #{child.startLine}, " +
                      "endLine: #{child.endLine}}")
        if line isnt null
          for prop in [
              'multilineChildrenData'
              'minDimensions'
              'minDistanceToBase'
              'dimensions'
              'distanceToBase'
              'bounds'
              'glue']
            result.push("#{prop} #{line}: #{JSON.stringify(@[prop][line])}")
          for child, i in @lineChildren[line]
            result.push("line #{line} child #{i}: " +
                        "{startLine: #{child.startLine}, " +
                        "endLine: #{child.endLine}}}")
        return result.join('\n')

      # ## computeChildren (GenericViewNode)
      # Find out which of our children lie on each line that we
      # own, and also how many lines we own.
      #
      # Return the number of lines we own.
      #
      # This is basically a void computeChildren that should be
      # overridden.
      computeChildren: -> @lineLength

      # ## computeMargins (GenericViewNode)
      # Compute the amount of margin required outside the child
      # on the top, bottom, left, and right.
      #
      # This is a void computeMargins that should be overridden.
      computeMargins: ->
        if @computedVersion is @model.version and
           (not @model.parent? or
           @model.parent.version is @view.getViewNodeFor(@model.parent).computedVersion)
          return @margins

        # the margins I need depend on the type of my parent
        parenttype = @model.parent?.type
        padding = @view.opts.padding

        left = if @model.isFirstOnLine() or @lineLength > 1 then padding else 0
        right = if @model.isLastOnLine() or @lineLength > 1 then padding else 0

        if parenttype is 'block' and @model.type is 'indent'
          @margins =
            top: 0
            bottom: @view.opts.indentTongueHeight

            firstLeft: 0
            midLeft: @view.opts.indentWidth
            lastLeft: @view.opts.indentWidth

            firstRight: 0
            midRight: 0
            lastRight: padding

        else if @model.type is 'text' and parenttype is 'socket'
          @margins =
            top: @view.opts.textPadding
            bottom: @view.opts.textPadding

            firstLeft: @view.opts.textPadding
            midLeft: @view.opts.textPadding
            lastLeft: @view.opts.textPadding

            firstRight: @view.opts.textPadding
            midRight: @view.opts.textPadding
            lastRight: @view.opts.textPadding

        else if @model.type is 'text' and parenttype is 'block'
          @margins =
            top: padding
            bottom: padding

            firstLeft: left
            midLeft: left
            lastLeft: left

            firstRight: right
            midRight: right
            lastRight: right

        else if parenttype is 'block'
          @margins =
            top: padding
            bottom: padding

            firstLeft: left
            midLeft: padding
            lastLeft: padding

            firstRight: right
            midRight: 0
            lastRight: right
        else
          @margins = {
            firstLeft: 0, midLeft:0, lastLeft: 0
            firstRight: 0, midRight:0, lastRight: 0
            top:0, bottom:0
          }

        @firstMargins =
          left: @margins.firstLeft
          right: @margins.firstRight
          top: @margins.top
          bottom: if @lineLength is 1 then @margins.bottom else 0

        @midMargins =
          left: @margins.midLeft
          right: @margins.midRight
          top: 0
          bottom: 0

        @lastMargins =
          left: @margins.lastLeft
          right: @margins.lastRight
          top: if @lineLength is 1 then @margins.top else 0
          bottom: @margins.bottom

        for childObj in @children
          @view.getViewNodeFor(childObj.child).computeMargins()

        return null

      getMargins: (line) ->
        if line is 0 then @firstMargins
        else if line is @lineLength - 1 then @lastMargins
        else @midMargins

      computeBevels: ->
        @bevels =
          topLeft: false
          topRight: false
          bottomLeft: false
          bottomRight: false

      
      # ## computeMinDimensions (GenericViewNode)
      # Compute the size of our bounding box on each
      # line that we contain.
      #
      # Return child node.
      #
      # This is a void computeDimensinos that should be overridden.
      computeMinDimensions: ->
        if @minDimensions.length > @lineLength
          @minDimensions.length = @minDistanceToBase.length = @lineLength
        else
          until @minDimensions.length is @lineLength
            @minDimensions.push new draw.Size 0, 0
            @minDistanceToBase.push {above: 0, below: 0}

        for i in [0...@lineLength]
          @minDimensions[i].width = @minDimensions[i].height = 0
          @minDistanceToBase[i].above = @minDistanceToBase[i].below = 0

        return null


      # ## computeDimensions (GenericViewNode)
      # Compute the size of our bounding box on each
      # line that we contain.
      #
      # Return child node.
      computeDimensions: (startLine, force, root = false) ->
        if @computedVersion is @model.version and not force
          return

        oldDimensions = @dimensions
        oldDistanceToBase = @distanceToBase
        
        @dimensions = (new draw.Size 0, 0 for [0...@lineLength])
        @distanceToBase = ({above: 0, below: 0} for [0...@lineLength])

        for size, i in @minDimensions
          @dimensions[i].width = size.width; @dimensions[i].height = size.height
          @distanceToBase[i].above = @minDistanceToBase[i].above
          @distanceToBase[i].below = @minDistanceToBase[i].below

        if @model.parent? and not root and
            (@topLineSticksToBottom or @bottomLineSticksToTop)
          parentNode = @view.getViewNodeFor @model.parent

          # grow below if "stick to bottom" is set.
          if @topLineSticksToBottom
            distance = @distanceToBase[0]
            distance.below = Math.max(distance.below,
                parentNode.distanceToBase[startLine].below)
            @dimensions[0] = new draw.Size(
                @dimensions[0].width,
                distance.below + distance.above)

          # grow above if "stick to top" is set.
          if @bottomLineSticksToTop
            lineCount = @distanceToBase.length
            distance = @distanceToBase[lineCount - 1]
            distance.above = Math.max(distance.above,
                parentNode.distanceToBase[startLine + lineCount - 1].above)
            @dimensions[lineCount - 1] = new draw.Size(
                @dimensions[lineCount - 1].width,
                distance.below + distance.above)

        changed = (oldDimensions.length != @lineLength)
        if not changed then for line in [0...@lineLength]
          if !oldDimensions[line].equals(@dimensions[line]) or
              oldDistanceToBase[line].above != @distanceToBase[line].above or
              oldDistanceToBase[line].below != @distanceToBase[line].below
            changed = true
            break

        @changedBoundingBox or= changed
        console.log 'force:', changed

        for childObj in @children
          @view.getViewNodeFor(childObj.child).computeDimensions(
              childObj.startLine, changed)

        return null

      # ## computeBoundingBoxX (GenericViewNode)
      # Given the left edge coordinate for our bounding box on
      # this line, recursively layout the x-coordinates of us
      # and all our children on this line.
      computeBoundingBoxX: (left, line) ->
        # Attempt to use our cache. Because modifications in the parent
        # can affect the shape of child blocks, we can't only rely
        # on versioning. For instance, changing `fd 10` to `forward 10`
        # does not update the version on `10`, but the bounding box for
        # `10` still needs to change. So we must also check
        # that the coordinate we are given to begin the bounding box on matches.
        if @computedVersion is @model.version and
           left is @bounds[line]?.x and not @changedBoundingBox or
           @bounds[line]?.x is left and
           @bounds[line]?.width is @dimensions[line].width
          return @bounds[line]
        
        @changedBoundingBox = true

        # Avoid re-instantiating a Rectangle object,
        # if possible.
        if @bounds[line]?
{'fraction_non_alphanumeric': 0.08768, 'fraction_numerical': 0.009386666666666666, 'mean_word_length': 1.9503828805202978, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 48, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4069718', 'n_tokens_mistral': 8257, 'n_tokens_neox': 7771, 'n_words': 2439}