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 }