Cursor.coffee

Token      = require './Token'
TokenTypes = require './TokenTypes'

NON_TEXT_OBJECT = new Token('none', -1, '')

module.exports = \

##
# Cursor for navigating and manipulating text
class Cursor

	##
	# Create a new cursor over a text
	#
	# @param {String} text Text to traverse
	# @param {String} [pos=0] - foo
	constructor: (@text, @pos) ->
		@pos or= 0
		@_calculate_positions()

	_calculate_offsets : ->
		@offsets = {}
		@offsets.char = @pos
		for tobjType of TokenTypes
			continue if tobjType is 'char'
			for idx, candidate of @positions[tobjType]
				if @pos >= candidate.index and @pos < candidate.index + candidate.length
					@offsets[tobjType] = parseInt(idx)

	_calculate_positions : ->
		@positions = {}
		for tobjType, re of TokenTypes
			@positions[tobjType] = []
			while (group = re.exec(@text)) != null
				@positions[tobjType].push new Token(tobjType, group.index, group[0])
		@_calculate_offsets()
		return @

	##
	# Delete a text object
	del: (tobj) ->
		@text = @text.substring(0, tobj.index) + @text.substring(tobj.index + tobj.length)
		@_calculate_positions()
		return @

	##
	# Insert a text object
	ins: (tobj) ->
		@text = @text.substring(0, tobj.index) + tobj.text + @text.substring(tobj.index)
		@_calculate_positions()
		return @

	##
	# Replace a text object
	sub: (oldObj) ->
		with : (newObj) =>
			@del(oldObj)
			@ins(newObj, oldObj.index)
			return @

	##
	# Move the current position relative to the current position.
	# @param {Number} amount Number of units to move (can be negative)
	# @param {String} [tobjType='char'] Text object type
	# @see TokenTypes
	move : (amount, tobjType) ->
		@moveTo(@pos + amount, tobjType)

	##
	# Move the current position to an absolute position
	# @param {Number} pos Position to move to
	# @param {String} [tobjType='char'] Unit to measure movements
	moveTo : (pos, tobjType) ->
		tobjType or= 'char'
		if tobjType isnt 'char' then throw new Error("'moveTo' only implemented for 'char' at the moment.")
		# console.log "Moving to #{tobjType}##{pos}"
		@pos = pos
		@_calculate_offsets()

	##
	# Get the text object at the current cursor position
	# @param {String} [tobjType='char'] The text object type to get
	cur: (tobjType) ->
		tobjType or= 'char'
		@positions[tobjType][@offsets[tobjType]] or null

	_cur_plus_offset: (tobjType, offset) ->
		# if not currently in a  e.g. word, number, sentence ...
		curOffset = @offsets[tobjType]
		if curOffset?
			curOffset += offset
		else
			for idx, candidate of @positions[tobjType]
				if (offset > 0 and @pos >= candidate.index) or (offset < 0 and @pos < candidate.index + candidate.length)
					curOffset = candidate.index
			curOffset += offset
		@positions[tobjType][curOffset]

	next: (amount, tobjType) ->
		if amount and amount of TokenTypes then [amount, tobjType] = [1, amount]
		@_cur_plus_offset(tobjType, Math.abs amount)

	# prev: (amount, tobjType) ->
	#   if amount and amount of TokenTypes then [amount, tobjType] = [1, amount]
	#   @_cur_plus_offset(tobjType, -1 * Math.abs amount)

	##
	# Whether the cursor is at the end of the text.
	# @return {Boolean} whether the cursor is at the end of the text.
	atEOF : () -> return @pos == @text.length

	##
	# whether the cursor is at the start of the text.
	# @return {Boolean} whether the cursor is at the start of the text.
	atSOF : () -> return @pos == 0