1 /** 2 Copyright: Copyright (c) 2014-2018 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 7 module voxelman.gui.textedit.linebuffer; 8 9 import voxelman.math; 10 import std.stdio; 11 import std.format : formattedWrite; 12 import voxelman.gui.textedit.cursor; 13 14 enum int NUM_BYTES_HIGH_BIT = 1<<31; 15 enum int NUM_BYTES_MASK = NUM_BYTES_HIGH_BIT - 1; 16 17 struct LineInfo 18 { 19 this(size_t startOffset, size_t numBytes, size_t numBytesEol) 20 { 21 assert(numBytesEol > 0 && numBytesEol < 3); 22 this.startOffset = startOffset; 23 this.numBytesStorage = ((numBytesEol-1) << 31) | numBytes; 24 } 25 26 size_t startOffset; 27 size_t numBytesStorage; // excluding newline 28 29 size_t endOffset() { return startOffset + numBytes; } 30 size_t nextStartOffset() { return startOffset + numBytesTotal; } 31 size_t numBytesEol() { return (numBytesStorage >> 31) + 1; } 32 size_t numBytes() { return numBytesStorage & NUM_BYTES_MASK; } // excluding newline 33 void numBytes(int bytes) { numBytesStorage = numBytesStorage & NUM_BYTES_HIGH_BIT | bytes; } // excluding newline 34 size_t numBytesTotal() { return numBytes + numBytesEol; } 35 36 void toString(scope void delegate(const(char)[]) sink) 37 { 38 sink.formattedWrite("LineInfo(off %s:%s; size %s:%s:%s)", 39 startOffset, endOffset, numBytes, numBytesEol, numBytesTotal); 40 } 41 } 42 43 // should be only used for small files (< int.max lines) 44 import voxelman.text.lexer; 45 struct LineInfoBuffer 46 { 47 import voxelman.container.gapbuffer; 48 GapBuffer!LineInfo lines; 49 int maxLineSize = 0; 50 int numLines = 0; 51 int lastLine() { return numLines > 0 ? numLines - 1 : 0; } 52 int textEnd = 0; 53 54 void clear() { 55 lines.clear(); 56 lines.put(LineInfo(0, 0, 1)); 57 maxLineSize = 0; 58 numLines = 1; 59 textEnd = 0; 60 lastValidLine = 0; 61 } 62 63 size_t lastValidLine; 64 65 ref LineInfo lineInfo(int lineIndex) 66 { 67 if (numLines == 0) lines.put(LineInfo(0, 0, 1)); 68 updateOffsetsToLine(lineIndex); 69 return lines[lineIndex]; 70 } 71 alias opIndex = lineInfo; 72 73 size_t lineStartOffset(int lineIndex) 74 { 75 if (numLines == 0) return 0; 76 updateOffsetsToLine(lineIndex); 77 return lines[lineIndex].startOffset; 78 } 79 80 // Does not include newline bytes 81 size_t numLineBytes(int lineIndex) 82 { 83 if (numLines == 0) return 0; 84 updateOffsetsToLine(lineIndex); 85 return lines[lineIndex].numBytes; 86 } 87 88 size_t numLineTotalBytes(int lineIndex) 89 { 90 if (numLines == 0) return 0; 91 updateOffsetsToLine(lineIndex); 92 return lines[lineIndex].numBytesTotal; 93 } 94 95 size_t lineEndOffset(int lineIndex) 96 { 97 if (numLines == 0) return 0; 98 updateOffsetsToLine(lineIndex); 99 return lines[lineIndex].endOffset; 100 } 101 102 int calc(R)(R text) 103 { 104 import std.algorithm : map; 105 import std.range; 106 import std.string; 107 import std.utf : byDchar; 108 109 lines.clear(); 110 111 auto stream = CharStream!R(text); 112 size_t startOffset; 113 size_t lineSize; 114 115 while(!stream.empty) 116 { 117 auto bytePos = stream.currentOffset; 118 if (stream.matchAnyOf('\n', '\v', '\f') || (stream.match('\r') && stream.matchOpt('\n'))) 119 { 120 auto lineBytes = bytePos - startOffset; 121 auto eolBytes = stream.currentOffset - bytePos; 122 lines.put(LineInfo(cast(int)startOffset, cast(int)lineBytes, cast(int)eolBytes)); 123 maxLineSize = max(maxLineSize, cast(int)lineSize); 124 startOffset = stream.currentOffset; 125 } 126 else 127 { 128 stream.next; 129 ++lineSize; 130 } 131 } 132 133 // last line 134 auto lineBytes = stream.currentOffset - startOffset; 135 lines.put(LineInfo(cast(int)startOffset, cast(int)lineBytes, 1)); 136 137 textEnd = cast(int)stream.currentOffset; 138 numLines = cast(int)lines.length; 139 lastValidLine = lastLine; 140 141 return numLines; 142 } 143 144 // returns number of inserted lines 145 int onPaste(R)(const Cursor at, R text) 146 { 147 auto stream = CharStream!R(text); 148 size_t startOffset; 149 size_t lineSize; 150 size_t lineIndex = at.line; 151 152 LineInfo firstLine = lineInfo(at.line); 153 154 auto oldBytesLeft = at.byteOffset - firstLine.startOffset; 155 auto oldBytesRight = firstLine.numBytes - oldBytesLeft; 156 auto oldEolBytes = at.line == lastLine ? 1 : firstLine.numBytesEol; 157 158 while(!stream.empty) 159 { 160 auto bytePos = stream.currentOffset; 161 if (stream.matchAnyOf('\n', '\v', '\f') || (stream.match('\r') && stream.matchOpt('\n'))) 162 { 163 auto lineBytes = bytePos - startOffset; 164 auto eolBytes = stream.currentOffset - bytePos; 165 if (lineIndex == at.line) 166 { 167 // first line 168 auto totalNewBytes = oldBytesLeft + lineBytes; 169 lines[at.line] = LineInfo(firstLine.startOffset, totalNewBytes, eolBytes); 170 } 171 else 172 lines.putAt(lineIndex, LineInfo(cast(int)(at.byteOffset + startOffset), cast(int)lineBytes, cast(int)eolBytes)); 173 maxLineSize = max(maxLineSize, cast(int)lineSize); 174 startOffset = stream.currentOffset; 175 ++lineIndex; 176 } 177 else 178 { 179 stream.next; 180 ++lineSize; 181 } 182 } 183 184 auto lineBytes = stream.currentOffset - startOffset; 185 186 // last line 187 if (lineIndex == at.line) 188 { 189 // and first line. no trailing newline 190 auto totalNewBytes = oldBytesLeft + lineBytes + oldBytesRight; 191 lines[at.line] = LineInfo(firstLine.startOffset, totalNewBytes, oldEolBytes); 192 } 193 else 194 { 195 // not a first line in text 196 auto totalNewBytes = lineBytes + oldBytesRight; 197 lines.putAt(lineIndex, LineInfo(cast(int)(at.byteOffset + startOffset), cast(int)totalNewBytes, cast(int)oldEolBytes)); 198 } 199 textEnd += cast(int)stream.currentOffset; 200 numLines = cast(int)lines.length; 201 lastValidLine = lineIndex; 202 203 return cast(int)(lineIndex - at.line); 204 } 205 206 void onRemove(Cursor from, Cursor to) 207 { 208 auto numBytesRemoved = to.byteOffset - from.byteOffset; 209 if (numBytesRemoved == 0) return; 210 211 textEnd -= numBytesRemoved; 212 213 // we collapse all info into the first line 214 // extra lines are removed. First line stays in place. 215 216 LineInfo first = lineInfo(from.line); 217 LineInfo last = lineInfo(to.line); // can be the same as first 218 //-writefln("first %s", first); 219 //-writefln("last %s", last); 220 221 // from.byteOffset >= first.startOffset; firstBytesLeft >= 0; 222 auto firstBytesLeft = from.byteOffset - first.startOffset; 223 //-writefln("firstBytesLeft %s", firstBytesLeft); 224 // to.byteOffset >= last.startOffset; lastBytesRemoved >= 0; 225 auto lastBytesRemoved = to.byteOffset - last.startOffset; 226 //-writefln("lastBytesRemoved %s", lastBytesRemoved); 227 auto lastBytesRight = last.numBytes - lastBytesRemoved; 228 //-writefln("lastBytesRight %s", lastBytesRight); 229 auto totalFirstBytes = firstBytesLeft + lastBytesRight; 230 //-writefln("totalFirstBytes %s", totalFirstBytes); 231 232 // update data in first line 233 lines[from.line] = LineInfo(first.startOffset, totalFirstBytes, last.numBytesEol); 234 //-writefln("lines[from.line] = %s", lines[from.line]); 235 236 // remove extra lines 237 size_t numLinesToRemove = to.line - from.line; 238 if (numLinesToRemove > 0) 239 { 240 lines.remove(from.line + 1, numLinesToRemove); 241 numLines -= numLinesToRemove; 242 } 243 244 lastValidLine = from.line; 245 } 246 247 void updateOffsetsToLine(int toLine) 248 { 249 assert(toLine <= lastLine); 250 if (toLine <= lastValidLine) return; 251 //writefln("updateOffsetsToLine %s", toLine); 252 253 auto startOffset = lines[lastValidLine].startOffset; 254 auto prevBytes = lines[lastValidLine].numBytesTotal; 255 foreach(ref line; lines[lastValidLine+1..toLine+1]) 256 { 257 startOffset += prevBytes; 258 line.startOffset = startOffset; 259 prevBytes = line.numBytesTotal; 260 } 261 lastValidLine = toLine; 262 } 263 264 void print() 265 { 266 foreach(line; lines[]) 267 { 268 writefln("%s %s+%s=%s", line.startOffset, line.numBytes, line.numBytesEol, line.numBytesTotal); 269 assert(line.numBytes + line.numBytesEol == line.numBytesTotal); 270 } 271 } 272 } 273 274 unittest 275 { 276 LineInfoBuffer buf; buf.calc("abcd"); 277 buf.onRemove(Cursor(0, 0), Cursor(4, 0)); 278 assert(buf[0] == LineInfo(0, 0, 1)); 279 } 280 281 unittest 282 { 283 LineInfoBuffer buf; buf.calc("abcd"); 284 buf.onRemove(Cursor(1, 0), Cursor(3, 0)); 285 assert(buf[0] == LineInfo(0, 2, 1)); 286 } 287 288 unittest 289 { 290 LineInfoBuffer buf; buf.calc("ab\ncd"); 291 assert(buf.numLines == 2); 292 assert(buf.lastLine == 1); 293 assert(buf.textEnd == 5); 294 295 buf.onRemove(Cursor(1, 0), Cursor(4, 1)); 296 297 assert(buf[0] == LineInfo(0, 2, 1)); 298 assert(buf.numLines == 1); 299 assert(buf.lastLine == 0); 300 assert(buf.textEnd == 2); 301 }