//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 30 Jul 08 Brian Frank Creation
//
using gfx
using fwt
using flux
using syntax
**
** Doc is the model for text edited in a `TextEditor`
**
class Doc : RichTextModel
{
//////////////////////////////////////////////////////////////////////////
// Construction
//////////////////////////////////////////////////////////////////////////
new make(TextEditorOptions options, SyntaxRules rules)
{
lines.add(Line { it.offset=0; it.text="" })
this.options = options
this.rules = rules
this.parser = Parser(this)
this.delimiter = options.lineDelimiter
}
//////////////////////////////////////////////////////////////////////////
// RichTextModel
//////////////////////////////////////////////////////////////////////////
override Str text
{
get { return lines.join(delimiter) |Line line->Str| { return line.text } }
set { modify(0, size, it) }
}
override Int charCount() { return size }
override Int lineCount() { return lines.size }
override Str line(Int lineIndex) { return lines[lineIndex].text }
override Int offsetAtLine(Int lineIndex) { return lines[lineIndex].offset }
override Int lineAtOffset(Int offset)
{
// binary search by offset, returns '-insertationPoint-1'
key := Line { it.offset = offset }
line := lines.binarySearch(key) |Line a, Line b->Int| { return a.offset <=> b.offset }
if (line < 0) line = -(line + 2)
if (line >= lines.size) line = lines.size-1
return line
}
override Void modify(Int startOffset, Int len, Str newText)
{
// compute the lines being replaced
endOffset := startOffset + len
startLineIndex := lineAtOffset(startOffset)
endLineIndex := lineAtOffset(endOffset)
startLine := lines[startLineIndex]
endLine := lines[endLineIndex]
oldText := textRange(startOffset, len)
// sample styles before insert
samplesBefore := [ lineStyling(endLineIndex+1), lineStyling(lines.size-1) ]
// compute the new text of the lines being replaced
offsetInStart := startOffset - startLine.offset
offsetInEnd := endOffset - endLine.offset
newLinesText := startLine.text[0..<offsetInStart] + newText + endLine.text[offsetInEnd..-1]
// split new text into new lines
newLines := Line[,] { capacity=32 }
newLinesText.splitLines.each |Str s|
{
newLines.add(parser.parseLine(s))
}
// merge in new lines
lines.removeRange(startLineIndex..endLineIndex)
lines.insertAll(startLineIndex, newLines)
// update total size, line offsets, and multi-line comments/strings
updateLines(lines)
// sample styles after insert
samplesAfter := [ lineStyling(startLineIndex+newLines.size), lineStyling(lines.size-1) ]
repaintToEnd := samplesBefore != samplesAfter
// fire modification event
tc := TextChange
{
it.startOffset = startOffset
it.startLine = startLineIndex
it.oldText = oldText
it.newText = newText
it.oldNumNewlines = oldText.numNewlines
it.newNumNewlines = newLines.size - 1
it.repaintLen = repaintToEnd ? size-startOffset : null
}
onModify.fire(Event { id =EventId.modified; data = tc })
}
**
** Walk all the lines:
** - update offset
** - update total size
** - compute style override for block comments
** - compute style override for multiline strings
**
private Void updateLines(Line[] lines)
{
n := 0
lastIndex := lines.size-1
delimiterSize := delimiter.size
commentLevel := 0
commentMin := rules.blockCommentsNest ? 100 : 1
inStr := false
// walk the lines
Block? block := null
lines.each |Line line, Int i|
{
// update offset and total running size
line.offset = n
n += line.text.size
if (i != lastIndex) n += delimiterSize
// update comment nesting count
commentLevel = (commentLevel + line.commentNesting).max(0).min(commentMin)
// if not inside a multi-line block, then the current line
// decides if opening if a new multi-line block (or null);
// otherwise this line either closes the current open block
// or is inside the block
if (block == null)
{
line.stylingOverride = null
block = line.opens
}
else
{
Line? closes := line.closes(block)
if (closes == null || commentLevel > 0)
{
// override this line as str/comment block
line.stylingOverride = block.stylingOverride
}
else
{
// close the current block, and re-parse line appropriately
line.stylingOverride = closes.styling
block = closes.opens
}
}
}
// update total size
size = n
}
override Obj[]? lineStyling(Int lineIndex)
{
try
{
// get configured styling
line := lines[lineIndex]
styling := line.stylingOverride ?: line.styling
// apply bracket styling if current line is matched brackets
if (lineIndex == bracketLine1 || lineIndex == bracketLine2)
{
styling = styling.dup
lineLen := line.text.size
if (lineIndex == bracketLine1) insertBracketMatch(styling, bracketCol1, lineLen)
if (lineIndex == bracketLine2) insertBracketMatch(styling, bracketCol2, lineLen)
}
return styling
}
catch
{
return null
}
}
override Color? lineBackground(Int lineIndex)
{
if (lineIndex == caretLine)
return options.highlightCurLine
else
return null
}
**
** Insert a bracket match style run of one character
** at the specified offset. There are four cases where
** "xxx" is run, and "^" is insertion point:
**
** x a) replace single char run
** xxx b) insert at end
** xxx c) move run to right one char, insert
** xxxxx d) breaking middle of run
** ^
**
private Void insertBracketMatch(Obj[] styling, Int offset, Int lineLen)
{
// find insert point in styling list;
i := 0; Int iOffset := 0; RichTextStyle iStyle := styling[1]
for (; i<styling.size; i+=2)
{
if (styling[i] >= offset) break
iStyle = styling[i+1]
}
iOffset = i<styling.size ? styling[i] : lineLen
// compute remaining chars in run
left := lineLen - offset - 1
if (i+2<styling.size)
left = ((Int)styling[i+2]) - offset - 1
// a) if we are replacing a single char run
if (offset == iOffset && left == 0)
{
styling[i+1] = options.bracketMatch
return
}
// b) if end of run, insert only
if (left == 0)
{
styling.insert(i, options.bracketMatch)
styling.insert(i, offset)
return
}
// c) if starting a run of more than one character
if (offset == iOffset)
{
styling[i] = offset+1 // move to left one char
styling.insert(i, options.bracketMatch)
styling.insert(i, offset)
return
}
// d) we are breaking the middle of run
styling.insert(i, iStyle)
styling.insert(i, offset+1)
styling.insert(i, options.bracketMatch)
styling.insert(i, offset)
}
//////////////////////////////////////////////////////////////////////////
// IO
//////////////////////////////////////////////////////////////////////////
**
** Load fresh document already parsed into lines.
**
internal Void load(Str[] strLines)
{
lines = Line[,] { capacity = strLines.size + 100 }
strLines.each |Str str|
{
lines.add(parser.parseLine(str))
}
if (lines.isEmpty) lines.add(parser.parseLine(""))
updateLines(lines)
}
**
** Save document to output stream (we assume charset
** is already configured).
**
internal Void save(OutStream out)
{
stripws := options.stripTrailingWhitespace
delimiter := this.delimiter
lastLine := lines.size-1
lines.each |Line line, Int i|
{
text := line.text
if (stripws) text = text.trimEnd
out.print(text)
if (i != lastLine || text.isEmpty) out.print(delimiter)
}
}
//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////
**
** Find the specified string in the document starting the
** search at the document offset and looking forward.
** Return null is not found. Note we don't currently
** support searching across multiple lines.
**
Int? findNext(Str s, Int offset, Bool matchCase)
{
offset = offset.max(0).min(size)
lineIndex := lineAtOffset(offset)
offsetInLine := offset - lines[lineIndex].offset
while (lineIndex < lines.size)
{
line := lines[lineIndex]
r := matchCase ?
line.text.index(s, offsetInLine) :
line.text.indexIgnoreCase(s, offsetInLine)
if (r != null) return line.offset+r
offsetInLine = 0 // after first line we always start at zero
lineIndex++
}
return null
}
**
** Find the specified string in the document starting the
** search at the document offset and looking backward.
** Return null is not found. Note we don't currently
** support searching across multiple lines.
**
Int? findPrev(Str s, Int offset, Bool matchCase)
{
offset = offset.max(0).min(size)
lineIndex := lineAtOffset(offset)
offsetInLine := offset - lines[lineIndex].offset
while (lineIndex >= 0)
{
line := lines[lineIndex]
r := matchCase ?
line.text.indexr(s, offsetInLine) :
line.text.indexrIgnoreCase(s, offsetInLine)
if (r != null) return line.offset+r
offsetInLine = -1 // after first line we always start at end
lineIndex--
}
return null
}
**
** Highlight all the marks found in this document.
**
internal Void updateMarks(Mark[] marks)
{
// TODO
//echo("-- Doc.updateMarks --")
//marks.each |Mark m| { echo(m) }
}
**
** Attempt to find the matching bracket the specified
** offset. If the bracket is an opening bracket then
** we search forward for the closing bracket taking into
** account nesting. If a closing bracket we search backward.
** Return null if no match.
**
internal Int? matchBracket(Int offset)
{
lineIndex := lineAtOffset(offset)
line := lines[lineIndex]
offsetInLine := offset-line.offset
// get matched pair
a := line.text[offsetInLine]
b := bracketPairs[a]
if (b == null) return null
forward := a < b
nesting := 0
while (true)
{
if (line.text[offsetInLine] == a) ++nesting
else if (line.text[offsetInLine] == b) --nesting
if (nesting == 0) return offset
if (forward)
{
offset++; offsetInLine++
while (offsetInLine >= line.text.size)
{
lineIndex++; offset += delimiter.size
if (lineIndex >= lines.size) return null
line = lines[lineIndex]; offsetInLine = 0
}
}
else
{
offset--; offsetInLine--
while (offsetInLine < 0)
{
lineIndex--; offset -= delimiter.size
if (lineIndex < 0) return null
line = lines[lineIndex]; offsetInLine = line.text.size-1
}
}
}
return null
}
**
** Set the two current matching bracket positions.
** These will get styled specially. It is up to the
** caller to repaint the dirty lines.
**
internal Void setBracketMatch(Int line1, Int col1, Int line2, Int col2)
{
if (line1 < line2 || col1 < col2)
{
bracketLine1 = line1; bracketCol1 = col1
bracketLine2 = line2; bracketCol2 = col2
}
else
{
bracketLine1 = line2; bracketCol1 = col2
bracketLine2 = line1; bracketCol2 = col1
}
}
internal const static Int:Int bracketPairs
static
{
m := Int:Int[:]
m['{'] = '}'; m['}'] = '{'
m['('] = ')'; m[')'] = '('
m['['] = ']'; m[']'] = '['
m['<'] = '>'; m['>'] = '<'
bracketPairs = m
}
//////////////////////////////////////////////////////////////////////////
// Debug
//////////////////////////////////////////////////////////////////////////
**
** Debug dump of the document model.
**
Void dump(OutStream out := Env.cur.out)
{
out.printLine("")
out.printLine("==== Doc.dump ===")
out.printLine("size=$size")
out.printLine("lines.size=$lines.size")
out.printLine("delimiter=$delimiter.toCode")
lines.each |Line line, Int i| { out.printLine("[${i.toStr.justr(3)} @ ${line.offset.toStr.justr(3)}] $line.text.toCode $line.debug") }
out.printLine("")
out.flush
}
//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////
** Text options for current document
TextEditorOptions options { private set }
** Syntax rules for current document
SyntaxRules rules { private set }
internal Int size := 0 // total char count
internal Line[] lines := Line[,] // lines
internal Str delimiter // line delimiter
internal Parser parser // to parse lines into styled segments
internal Int caretLine // current line for highlighting
internal Int? bracketLine1 // matched bracket 1 line index
internal Int? bracketLine2 // matched bracket 2 line index
internal Int? bracketCol1 // matched bracket 1 offset in line
internal Int? bracketCol2 // matched bracket 2 offset in line
}
**************************************************************************
** Line
**************************************************************************
**
** Line models one text line of a Doc
**
internal class Line
{
** Return 'text'.
override Str toStr() { return text }
** Zero based offset from start of document (this
** field is managed by the Doc).
Int offset { internal set; }
** Text of line (without delimiter)
Str text := ""
** Offset/RichTextStyle pairs
Obj[]? styling
** Override when line is inside a block comment or multi-line str
Obj[]? stylingOverride
** Opens n comments if > 0 or closes n comments if < 0
virtual Int commentNesting() { return 0 }
** If this line opens a multi-line block (comment/str),
** then return a block handle, else null.
virtual Block? opens() { return null }
** If this line closes the specified block, then return the new
** line which takes into account that this line is the closing line
** of a multi-line comment or string.
virtual Line? closes(Block open) { return null }
** Debug information
internal virtual Str debug() { return "" }
}
**************************************************************************
** FatLine
**************************************************************************
**
** FatLine subclasses "thin" lines to cache more parsed
** information. We use a subclass to avoid the extra memory
** overhead on lines which don't need these extra fields.
**
internal class FatLine : Line
{
** Opens n comments if > 0 or closes n comments if < 0
override Int commentNesting := 0
** If this line opens a multi-line block (comment/str),
** then return a block handle, else null.
override Block? opens
** If this line closes the specified block, then return the new
** line which takes into account that this line is the closing line
** of a multi-line comment or string.
override Line? closes(Block open)
{
if (closeBlocks == null) return null
for (i:=0; i<closeBlocks.size; ++i)
{
newLine := closeBlocks[i].closes(this, open)
if (newLine != null) return newLine
}
return null
}
** List of blocks this line potentially closes
** if used after an opening block
Block[]? closeBlocks
** Debug information
internal override Str debug()
{
return "{$commentNesting, $opens, $closeBlocks}"
}
}
**************************************************************************
** Block
**************************************************************************
**
** Blocks model multiple line syntax constructs: block comments
** and multi-line strings.
**
internal abstract class Block
{
** Which style override should be used inside the block?
abstract Obj[]? stylingOverride()
** If this block marker can be used to close the specified
** open block, then return the new line taking into account
** that the cur line is closing a mult-line block comment
** or str. Return null if this instance doesn't close open.
abstract Line? closes(Line line, Block open)
}