{ import: Object } { import: TestCase } { import: Utility } "----------------------------------------------------------------" "TextBufferChange records a change of TextBuffer " TextBufferChange : Object(start before after) TextBufferChange start: _start before: _before after: _after [ self := self new. start := _start. before := _before. after := _after. ^ self. ] TextBufferChange start [ ^start ] TextBufferChange before [ ^before ] TextBufferChange after [ ^after ] TextBufferChange delta [ ^after size - before size ] "Delta of change" TextBufferChange beforeEnd [ ^start + before size ] "Ending edit position before change" TextBufferChange afterEnd [ ^start + after size ] "Ending edit position after change" TextBufferChange inverse [ ^self start: start before: after after: before ] TextBufferChange printOn: aStream [ aStream nextPutAll: 'TextBufferChange start: ', start printString, ' before: ', before printString, ' after: ', after printString. ] "----------------------------------------------------------------" "TextBuffer represents various text model and algorithm. Terminology: - offset : Distance of a character from the head. First character is 0. - position : A pair of cols, rows. Logical coordinate of a character. " TextBuffer : Object() TextBuffer at: offset [] TextBuffer size [] TextBuffer contents [] TextBuffer contents: aString [ self replaceFrom: 0 size: self size with: aString ] TextBuffer from: start size: size [] TextBuffer getPosition: offset [] TextBuffer doWithHead: aBlock [] TextBuffer lineFrom: offset [] "Copy a line from the offset" TextBuffer replace: aTextBufferChange [ self replaceFrom: aTextBufferChange start size: aTextBufferChange before size with: aTextBufferChange after. ] TextBuffer replaceFrom: start size: size with: replacement [ "Replace text with replacement, trigger #onReplaced" | removed | removed := self from: start size: size. self _replaceFrom: start size: size with: replacement. self triggerSignal: #onReplaced with: (TextBufferChange start: start before: removed after: replacement). ] "----------------------------------------------------------------" "TextArrayBuffer is the simplest implementation of editable text." TextArrayBuffer : TextBuffer (string) TextArrayBuffer new [ self := super new. string := ''. ] TextArrayBuffer at: offset [ ^string at: offset ] TextArrayBuffer size [ ^string size ] TextArrayBuffer contents [ ^string ] TextArrayBuffer from: start size: size [ ^string copyFrom: start size: size ] TextArrayBuffer getPosition: offset [ "Answers location (cols, rows) at the offset." self doWithPosition: [ :char :i :cols :rows | (char isNil or: [ offset = i ]) ifTrue: [ ^cols, rows ]]. ] TextArrayBuffer getOffset: position [ "Answers closest offset. (-1, y) means last line, and (x, -1) means end of the line" | x y | self doWithPosition: [ :char :offset :cols :rows | (rows = position y and: [cols = position x]) ifTrue: [ ^offset ]. (position y >= 0 and: [rows > position y]) ifTrue: [ ^offset - 1 ]. "Overrun cols" char ifNil: [ position y = rows ifTrue: [ ^offset ]. "Overrun cols without \n" ^ self getOffset: position x, rows]. "Overrun rows" ]. ] TextArrayBuffer doWithPosition: aBlock [ "Enumerate the string with character, offset, and position" | cols rows | cols := rows := 0. string doWithIndex: [ :char :offset | aBlock value: char value: offset value: cols value: rows. char = $\n ifTrue: [ cols := 0. rows := rows + 1] ifFalse: [ cols := cols + 1]]. aBlock value: nil value: string size value: cols value: rows. ] TextArrayBuffer doWithHead: aBlock [ "Enumerate a block with offset and index of lines" self doWithPosition: [ :char :offset :cols :rows | cols = 0 ifTrue: [ aBlock value: offset value: rows ]]. ] TextArrayBuffer lineFrom: offset [ offset to: string size - 1 do: [:end | (string at: end) = $\n ifTrue: [ ^string copyFrom: offset to: end]]. ^ string copyFrom: offset to: string size - 1 ] TextArrayBuffer _replaceFrom: start size: size with: replacement [ | newSize repSize restSize | repSize := replacement size. newSize := string size + repSize - size. restSize := string size - (start + size). string := (String new: newSize) replaceFrom: 0 size: start with: (string copyFrom: 0 size: start); replaceFrom: start size: repSize with: replacement; replaceFrom: start + repSize size: restSize with: (string copyFrom: start + size size: restSize). ] "----------------------------------------------------------------" "TextLineBuffer uses just one font to the text. Actual data is represented as a collection of lines. Last line of text must not include new line. Empty line is placed if text ends with \n like: 'hello\n' -> #('hello\n' '') " TextLineBuffer : TextBuffer (lines positions) TextLineBuffer new [ self := super new. lines := OrderedCollection with: ''. positions := nil. "Cache of position: a list of #(linenum head size)" ] TextLineBuffer at: offset [ | p | p := self getPosition: offset. ^ (lines at: p y) at: p x. ] TextLineBuffer positions [ | offset | positions ifNotNil: [ ^positions ]. positions := OrderedCollection new. offset := 0. lines doWithIndex: [ :line :rows | positions add: (Array with: rows with: offset with: line size). offset := offset + line size]. ^ positions. ] TextLineBuffer doWithHead: aBlock [ "Enumerate a block with offset and index of lines" self positions doWithIndex: [ :pos :rows | aBlock value: pos second value: rows ]. ] TextLineBuffer size [ | size | ^lines inject: 0 into: [:sum :next | sum + next size] ] TextLineBuffer contents [ | aStream | aStream := WriteStream on: (String new: 8). lines do: [ :each | aStream nextPutAll: each ]. ^aStream contents. ] TextLineBuffer getPosition: offset [ | range pos | offset = 0 ifTrue: [ ^0, 0 ]. pos := self positions findBinary: [ :p | (p second <= offset and: [ offset < (p second + p third) ]) ifTrue: [ 0 ] ifFalse: [p second > offset ifTrue: [ -1 ] ifFalse: [ 1]]] ifNone: [ ^ (lines at: lines size - 1) size, (lines size - 1) ]. ^ offset - pos second, pos first ] TextLineBuffer getOffset: position [ | line right | self doWithHead: [ :head :rows | line := lines at: rows. (rows = position y or: [line = lines last]) ifTrue: [ right := line = lines last ifTrue: [ line size ] ifFalse: [ line size - 1 ]. position x < 0 ifTrue: [ ^ head + right ]. ^ head + (position x min: right) ]]. ] TextLineBuffer from: start size: size [ | p rest aStream line right | p := self getPosition: start. rest := size. aStream := WriteStream on: (String new: 8). [ rest = 0 ] whileFalse: [ line := lines at: p y. right := line size - p x. right > rest ifTrue: [ aStream nextPutAll: (line copyFrom: p x size: rest). ^ aStream contents ] ifFalse: [ aStream nextPutAll: (line copyFrom: p x size: line size - p x). rest := rest - right. p := 0, (p y + 1)]. ]. ^ aStream contents ] TextLineBuffer appendLine: newline [ "Append a line. If the line ends with \n, a new empty line is added" newline size = 0 ifTrue: [ ^self ]. newline := (lines last size = 0) ifTrue: [ newline ] ifFalse: [ lines last, newline ]. lines at: lines size - 1 put: newline. newline last = $\n ifTrue: [ lines add: '' ]. ] TextLineBuffer appendLines: newlines [ ^ newlines do: [ :line | self appendLine: line ]. ] TextLineBuffer _replaceFrom: start size: size with: replacement [ | p1 p2 old | p1 := self getPosition: start. p2 := self getPosition: start + size. old := lines. lines := OrderedCollection with: ''. self appendLines: (old copyFrom: 0 size: p1 y). self appendLine: ((old at: p1 y) copyFrom: 0 size: p1 x). self appendLines: (replacement split: $\n). self appendLine: ((old at: p2 y) copyFrom: p2 x size: (old at: p2 y) size - p2 x). self appendLines: (old copyFrom: p2 y + 1 size: old size - p2 y - 1). positions := nil. ] TextLineBuffer lineFrom: offset [ | p line | p := self getPosition: offset. line := lines at: p y. ^ line copyFrom: p x to: line size - 1. ] "----------------------------------------------------------------" TextBufferTest : TestCase (src) TextArrayBufferTest : TextBufferTest() TextArrayBufferTest setUp [ src := TextArrayBuffer new ] TextLineBufferTest : TextBufferTest() TextLineBufferTest setUp [ src := TextLineBuffer new ] TextBufferTest testWriteAndRead [ src contents: '0123456789\nABCDEFG'. self assert: src contents equals: '0123456789\nABCDEFG'. self assert: src size equals: 18. ] TextBufferTest testGetPosition [ src contents: '0123456789\nABCDEFG'. self assert: (src getPosition: 0) equals: 0, 0. self assert: (src getPosition: 10) equals: 10, 0. self assert: (src getPosition: 11) equals: 0, 1. self assert: (src getPosition: 18) equals: 7, 1. ] TextBufferTest testAt [ src contents: '0123456789\nABCDEFG'. self assert: (src at: 0) equals: $0. self assert: (src at: 10) equals: $\n. self assert: (src at: 11) equals: $A. self assert: (src at: 17) equals: $G. ] TextBufferTest testSizeTo [ src contents: '0123456789\nABCDEFG'. self assert: (src from: 0 size: 10) equals: '0123456789'. self assert: (src from: 0 size: 11) equals: '0123456789\n'. self assert: (src from: 6 size: 10) equals: '6789\nABCDE'. self assert: (src from: 6 size: 12) equals: '6789\nABCDEFG'. self assert: (src from: 12 size: 6) equals: 'BCDEFG'. ] TextBufferTest testReplaceFromSizeWith [ src contents: '0123456789\nABCDEFG'. src replaceFrom: 5 size: 5 with: 'ABCDE'. self assert: src contents equals: '01234ABCDE\nABCDEFG'. src replaceFrom: 11 size: 7 with: '12345'. self assert: src contents equals: '01234ABCDE\n12345'. src replaceFrom: 11 size: 5 with: 'ABCDEFG'. self assert: src contents equals: '01234ABCDE\nABCDEFG'. src replaceFrom: 5 size: 5 with: '56789\n01234'. self assert: src contents equals: '0123456789\n01234\nABCDEFG'. self assert: (src getPosition: 17) equals: 0, 2. src contents: '0123456789'. src replaceFrom: 0 size: 10 with: ''. self assert: src contents equals: '' ] TextBufferTest testLineFrom [ src contents: '0123456789\nABCDEFG'. self assert: (src lineFrom: 0) equals: '0123456789\n'. self assert: (src lineFrom: 3) equals: '3456789\n'. self assert: (src lineFrom: 15) equals: 'EFG'. ] TextBufferTest testGetOffset [ src contents: '0123456789\n012345678'. self assert: (src getOffset: 0, 0) equals: 0. self assert: (src getOffset: 1, 1) equals: 12. self assert: (src getOffset: 999, 0) equals: 10. self assert: (src getOffset: -1, 0) equals: 10. self assert: (src getOffset: 999, 1) equals: 20. self assert: (src getOffset: 1, 2) equals: 12. self assert: (src getOffset: 1, -1) equals: 12. self assert: (src getOffset: 999, 999) equals: 20. self assert: (src getOffset: -1, -1) equals: 20. ] TextBufferTest testInverse [ | delta | delta := TextBufferChange start: 3 before: 'love' after: 'peace'. self assert: delta inverse start equals: 3. self assert: delta inverse before equals: 'peace'. self assert: delta inverse after equals: 'love'. ] TextBufferTest testReplace [ | delta | src contents: '0123456789'. src replace: (TextBufferChange start: 1 before: '123' after: 'B'). self assert: src contents equals: '0B456789'. ]