001: /*
002: * ChunkCache.java - Intermediate layer between token lists from a TokenMarker
003: * and what you see on screen
004: * :tabSize=8:indentSize=8:noTabs=false:
005: * :folding=explicit:collapseFolds=1:
006: *
007: * Copyright (C) 2001, 2005 Slava Pestov
008: *
009: * This program is free software; you can redistribute it and/or
010: * modify it under the terms of the GNU General Public License
011: * as published by the Free Software Foundation; either version 2
012: * of the License, or any later version.
013: *
014: * This program is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
017: * GNU General Public License for more details.
018: *
019: * You should have received a copy of the GNU General Public License
020: * along with this program; if not, write to the Free Software
021: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
022: */
023:
024: package org.gjt.sp.jedit.textarea;
025:
026: //{{{ Imports
027: import java.util.*;
028: import org.gjt.sp.jedit.buffer.JEditBuffer;
029: import org.gjt.sp.jedit.Debug;
030: import org.gjt.sp.jedit.syntax.*;
031: import org.gjt.sp.util.Log;
032:
033: //}}}
034:
035: /**
036: * Manages low-level text display tasks - the visible lines in the TextArea.
037: *
038: *
039: *
040: * @author Slava Pestov
041: * @version $Id: ChunkCache.java 8271 2006-12-28 08:05:51Z kpouer $
042: */
043: class ChunkCache {
044: //{{{ ChunkCache constructor
045: ChunkCache(TextArea textArea) {
046: this .textArea = textArea;
047: out = new ArrayList<Chunk>();
048: tokenHandler = new DisplayTokenHandler();
049: } //}}}
050:
051: //{{{ getMaxHorizontalScrollWidth() method
052: /**
053: * Returns the max line width of the textarea.
054: * It will check all lines the first invalid line.
055: *
056: * @return the max line width
057: */
058: int getMaxHorizontalScrollWidth() {
059: int max = 0;
060: for (int i = 0; i < firstInvalidLine; i++) {
061: LineInfo info = lineInfo[i];
062: if (info.width > max)
063: max = info.width;
064: }
065: return max;
066: } //}}}
067:
068: //{{{ getScreenLineOfOffset() method
069: /**
070: * @param line physical line number of document
071: * @param offset number of characters from the left of the line.
072: * @return number of pixels from the left of the textArea where the
073: * cursor should be
074: */
075: int getScreenLineOfOffset(int line, int offset) {
076: if (lineInfo.length == 0)
077: return -1;
078: if (line < textArea.getFirstPhysicalLine())
079: return -1;
080: if (line == textArea.getFirstPhysicalLine()
081: && offset < getLineInfo(0).offset)
082: return -1;
083: if (line > textArea.getLastPhysicalLine())
084: return -1;
085:
086: if (line == lastScreenLineP) {
087: LineInfo last = getLineInfo(lastScreenLine);
088:
089: if (offset >= last.offset
090: && offset < last.offset + last.length) {
091: return lastScreenLine;
092: }
093: }
094:
095: int screenLine = -1;
096:
097: // Find the screen line containing this offset
098: for (int i = 0; i < textArea.getVisibleLines(); i++) {
099: LineInfo info = getLineInfo(i);
100: if (info.physicalLine > line) {
101: // line is invisible?
102: return i - 1;
103: //return -1;
104: }
105: if (info.physicalLine == line) {
106: if (offset >= info.offset
107: && offset < info.offset + info.length) {
108: screenLine = i;
109: break;
110: }
111: }
112: }
113:
114: if (screenLine == -1)
115: return -1;
116:
117: lastScreenLineP = line;
118: lastScreenLine = screenLine;
119:
120: return screenLine;
121: } //}}}
122:
123: //{{{ recalculateVisibleLines() method
124: /**
125: * Recalculate visible lines.
126: * This is called when the TextArea geometry is changed or when the font is changed.
127: */
128: void recalculateVisibleLines() {
129: LineInfo[] newLineInfo = new LineInfo[textArea
130: .getVisibleLines()];
131:
132: int start;
133: if (lineInfo == null)
134: start = 0;
135: else {
136: start = Math.min(lineInfo.length, newLineInfo.length);
137: System.arraycopy(lineInfo, 0, newLineInfo, 0, start);
138: }
139:
140: for (int i = start; i < newLineInfo.length; i++)
141: newLineInfo[i] = new LineInfo();
142:
143: lineInfo = newLineInfo;
144:
145: lastScreenLine = lastScreenLineP = -1;
146: } //}}}
147:
148: //{{{ setBuffer() method
149: void setBuffer(JEditBuffer buffer) {
150: this .buffer = buffer;
151: lastScreenLine = lastScreenLineP = -1;
152: } //}}}
153:
154: //{{{ scrollDown() method
155: void scrollDown(int amount) {
156: int visibleLines = textArea.getVisibleLines();
157:
158: System.arraycopy(lineInfo, amount, lineInfo, 0, visibleLines
159: - amount);
160:
161: for (int i = visibleLines - amount; i < visibleLines; i++) {
162: lineInfo[i] = new LineInfo();
163: }
164:
165: firstInvalidLine -= amount;
166: if (firstInvalidLine < 0)
167: firstInvalidLine = 0;
168:
169: if (Debug.CHUNK_CACHE_DEBUG) {
170: System.err.println("f > t.f: only " + amount
171: + " need updates");
172: }
173:
174: lastScreenLine = lastScreenLineP = -1;
175: } //}}}
176:
177: //{{{ scrollUp() method
178: void scrollUp(int amount) {
179: System.arraycopy(lineInfo, 0, lineInfo, amount, textArea
180: .getVisibleLines()
181: - amount);
182:
183: for (int i = 0; i < amount; i++) {
184: lineInfo[i] = new LineInfo();
185: }
186:
187: // don't try this at home
188: int oldFirstInvalidLine = firstInvalidLine;
189: firstInvalidLine = 0;
190: updateChunksUpTo(amount);
191: firstInvalidLine = oldFirstInvalidLine + amount;
192: if (firstInvalidLine > textArea.getVisibleLines())
193: firstInvalidLine = textArea.getVisibleLines();
194:
195: if (Debug.CHUNK_CACHE_DEBUG) {
196: Log.log(Log.DEBUG, this , "f > t.f: only " + amount
197: + " need updates");
198: }
199:
200: lastScreenLine = lastScreenLineP = -1;
201: } //}}}
202:
203: //{{{ invalidateAll() method
204: void invalidateAll() {
205: firstInvalidLine = 0;
206: lastScreenLine = lastScreenLineP = -1;
207: } //}}}
208:
209: //{{{ invalidateChunksFrom() method
210: void invalidateChunksFrom(int screenLine) {
211: if (Debug.CHUNK_CACHE_DEBUG)
212: Log.log(Log.DEBUG, this , "Invalidate from " + screenLine);
213: firstInvalidLine = Math.min(screenLine, firstInvalidLine);
214:
215: if (screenLine <= lastScreenLine)
216: lastScreenLine = lastScreenLineP = -1;
217: } //}}}
218:
219: //{{{ invalidateChunksFromPhys() method
220: void invalidateChunksFromPhys(int physicalLine) {
221: for (int i = 0; i < firstInvalidLine; i++) {
222: LineInfo info = lineInfo[i];
223: if (info.physicalLine == -1
224: || info.physicalLine >= physicalLine) {
225: firstInvalidLine = i;
226: if (i <= lastScreenLine)
227: lastScreenLine = lastScreenLineP = -1;
228: break;
229: }
230: }
231: } //}}}
232:
233: //{{{ getLineInfo() method
234: LineInfo getLineInfo(int screenLine) {
235: updateChunksUpTo(screenLine);
236: return lineInfo[screenLine];
237: } //}}}
238:
239: //{{{ getLineSubregionCount() method
240: int getLineSubregionCount(int physicalLine) {
241: if (!textArea.softWrap)
242: return 1;
243:
244: out.clear();
245: lineToChunkList(physicalLine, out);
246:
247: int size = out.size();
248: if (size == 0)
249: return 1;
250: else
251: return size;
252: } //}}}
253:
254: //{{{ getSubregionOfOffset() method
255: /**
256: * Returns the subregion containing the specified offset. A subregion
257: * is a subset of a physical line. Each screen line corresponds to one
258: * subregion. Unlike the {@link #getScreenLineOfOffset(int, int)} method,
259: * this method works with non-visible lines too.
260: *
261: * @param offset the offset
262: * @param lineInfos a lineInfos array. Usualy the array is the result of
263: * {@link #getLineInfosForPhysicalLine(int)} call
264: *
265: * @return the subregion of the offset, or -1 if the offset was not in one of the given lineInfos
266: */
267: static int getSubregionOfOffset(int offset, LineInfo[] lineInfos) {
268: for (int i = 0; i < lineInfos.length; i++) {
269: LineInfo info = lineInfos[i];
270: if (offset >= info.offset
271: && offset < info.offset + info.length)
272: return i;
273: }
274:
275: return -1;
276: } //}}}
277:
278: //{{{ xToSubregionOffset() method
279: /**
280: * Converts an x co-ordinate within a subregion into an offset from the
281: * start of that subregion.
282: * @param physicalLine The physical line number
283: * @param subregion The subregion; if -1, then this is the last
284: * subregion.
285: * @param x The x co-ordinate
286: * @param round Round up to next character if x is past the middle of a
287: * character?
288: */
289: int xToSubregionOffset(int physicalLine, int subregion, int x,
290: boolean round) {
291: LineInfo[] infos = getLineInfosForPhysicalLine(physicalLine);
292: if (subregion == -1)
293: subregion += infos.length;
294: return xToSubregionOffset(infos[subregion], x, round);
295: } //}}}
296:
297: //{{{ xToSubregionOffset() method
298: /**
299: * Converts an x co-ordinate within a subregion into an offset from the
300: * start of that subregion.
301: * @param info The line info object
302: * @param x The x co-ordinate
303: * @param round Round up to next character if x is past the middle of a
304: * character?
305: */
306: static int xToSubregionOffset(LineInfo info, int x, boolean round) {
307: int offset = Chunk.xToOffset(info.chunks, x, round);
308: if (offset == -1 || offset == info.offset + info.length)
309: offset = info.offset + info.length - 1;
310:
311: return offset;
312: } //}}}
313:
314: //{{{ subregionOffsetToX() method
315: /**
316: * Converts an offset within a subregion into an x co-ordinate.
317: * @param physicalLine The physical line
318: * @param offset The offset
319: */
320: int subregionOffsetToX(int physicalLine, int offset) {
321: LineInfo[] infos = getLineInfosForPhysicalLine(physicalLine);
322: LineInfo info = infos[getSubregionOfOffset(offset, infos)];
323: return subregionOffsetToX(info, offset);
324: } //}}}
325:
326: //{{{ subregionOffsetToX() method
327: /**
328: * Converts an offset within a subregion into an x co-ordinate.
329: * @param info The line info object
330: * @param offset The offset
331: */
332: static int subregionOffsetToX(LineInfo info, int offset) {
333: return (int) Chunk.offsetToX(info.chunks, offset);
334: } //}}}
335:
336: //{{{ getSubregionStartOffset() method
337: /**
338: * Returns the start offset of the specified subregion of the specified
339: * physical line.
340: * @param line The physical line number
341: * @param offset An offset
342: */
343: int getSubregionStartOffset(int line, int offset) {
344: LineInfo[] lineInfos = getLineInfosForPhysicalLine(line);
345: LineInfo info = lineInfos[getSubregionOfOffset(offset,
346: lineInfos)];
347: return textArea.getLineStartOffset(info.physicalLine)
348: + info.offset;
349: } //}}}
350:
351: //{{{ getSubregionEndOffset() method
352: /**
353: * Returns the end offset of the specified subregion of the specified
354: * physical line.
355: * @param line The physical line number
356: * @param offset An offset
357: */
358: int getSubregionEndOffset(int line, int offset) {
359: LineInfo[] lineInfos = getLineInfosForPhysicalLine(line);
360: LineInfo info = lineInfos[getSubregionOfOffset(offset,
361: lineInfos)];
362: return textArea.getLineStartOffset(info.physicalLine)
363: + info.offset + info.length;
364: } //}}}
365:
366: //{{{ getBelowPosition() method
367: /**
368: * @param physicalLine The physical line number
369: * @param offset The offset
370: * @param x The location
371: * @param ignoreWrap If true, behave as if soft wrap is off even if it
372: * is on
373: */
374: int getBelowPosition(int physicalLine, int offset, int x,
375: boolean ignoreWrap) {
376: LineInfo[] lineInfos = getLineInfosForPhysicalLine(physicalLine);
377:
378: int subregion = getSubregionOfOffset(offset, lineInfos);
379:
380: if (subregion != lineInfos.length - 1 && !ignoreWrap) {
381: return textArea.getLineStartOffset(physicalLine)
382: + xToSubregionOffset(lineInfos[subregion + 1], x,
383: true);
384: } else {
385: int nextLine = textArea.displayManager
386: .getNextVisibleLine(physicalLine);
387:
388: if (nextLine == -1)
389: return -1;
390: else {
391: return textArea.getLineStartOffset(nextLine)
392: + xToSubregionOffset(nextLine, 0, x, true);
393: }
394: }
395: } //}}}
396:
397: //{{{ getAbovePosition() method
398: /**
399: * @param physicalLine The physical line number
400: * @param offset The offset
401: * @param x The location
402: * @param ignoreWrap If true, behave as if soft wrap is off even if it
403: * is on
404: */
405: int getAbovePosition(int physicalLine, int offset, int x,
406: boolean ignoreWrap) {
407: LineInfo[] lineInfos = getLineInfosForPhysicalLine(physicalLine);
408:
409: int subregion = getSubregionOfOffset(offset, lineInfos);
410:
411: if (subregion != 0 && !ignoreWrap) {
412: return textArea.getLineStartOffset(physicalLine)
413: + xToSubregionOffset(lineInfos[subregion - 1], x,
414: true);
415: } else {
416: int prevLine = textArea.displayManager
417: .getPrevVisibleLine(physicalLine);
418:
419: if (prevLine == -1)
420: return -1;
421: else {
422: return textArea.getLineStartOffset(prevLine)
423: + xToSubregionOffset(prevLine, -1, x, true);
424: }
425: }
426: } //}}}
427:
428: //{{{ needFullRepaint() method
429: /**
430: * The needFullRepaint variable becomes true when the number of screen
431: * lines in a physical line changes.
432: */
433: boolean needFullRepaint() {
434: boolean retVal = needFullRepaint;
435: needFullRepaint = false;
436: return retVal;
437: } //}}}
438:
439: //{{{ getLineInfosForPhysicalLine() method
440: LineInfo[] getLineInfosForPhysicalLine(int physicalLine) {
441: out.clear();
442:
443: if (!buffer.isLoading())
444: lineToChunkList(physicalLine, out);
445:
446: if (out.isEmpty())
447: out.add(null);
448:
449: List<LineInfo> returnValue = new ArrayList<LineInfo>(out.size());
450: getLineInfosForPhysicalLine(physicalLine, returnValue);
451: return returnValue.toArray(new LineInfo[out.size()]);
452: } //}}}
453:
454: //{{{ Private members
455:
456: //{{{ Instance variables
457: private final TextArea textArea;
458: private JEditBuffer buffer;
459: /**
460: * The lineInfo array. There is LineInfo for each line that is visible in the textArea.
461: * it can be resized by {@link #recalculateVisibleLines()}.
462: * The content is valid from 0 to {@link #firstInvalidLine}
463: */
464: private LineInfo[] lineInfo;
465: private final List<Chunk> out;
466:
467: /** The first invalid line. All lines before this one are valid. */
468: private int firstInvalidLine;
469: private int lastScreenLineP;
470: private int lastScreenLine;
471:
472: private boolean needFullRepaint;
473:
474: private final DisplayTokenHandler tokenHandler;
475:
476: //}}}
477:
478: //{{{ getLineInfosForPhysicalLine() method
479: private void getLineInfosForPhysicalLine(int physicalLine,
480: List<LineInfo> list) {
481: for (int i = 0; i < out.size(); i++) {
482: Chunk chunks = out.get(i);
483: LineInfo info = new LineInfo();
484: info.physicalLine = physicalLine;
485: if (i == 0) {
486: info.firstSubregion = true;
487: info.offset = 0;
488: } else
489: info.offset = chunks.offset;
490:
491: if (i == out.size() - 1) {
492: info.lastSubregion = true;
493: info.length = textArea.getLineLength(physicalLine)
494: - info.offset + 1;
495: } else {
496: info.length = out.get(i + 1).offset - info.offset;
497: }
498:
499: info.chunks = chunks;
500:
501: list.add(info);
502: }
503: } //}}}
504:
505: //{{{ getFirstScreenLine() method
506: /**
507: * Find a valid line closest to the last screen line.
508: */
509: private int getFirstScreenLine() {
510: for (int i = firstInvalidLine - 1; i >= 0; i--) {
511: if (lineInfo[i].lastSubregion)
512: return i + 1;
513: }
514:
515: return 0;
516: } //}}}
517:
518: //{{{ getUpdateStartLine() method
519: /**
520: * Return a physical line number.
521: */
522: private int getUpdateStartLine(int firstScreenLine) {
523: // for the first line displayed, take its physical line to be
524: // the text area's first physical line
525: if (firstScreenLine == 0) {
526: return textArea.getFirstPhysicalLine();
527: }
528: // otherwise, determine the next visible line
529: else {
530: int prevPhysLine = lineInfo[firstScreenLine - 1].physicalLine;
531: // if -1, the empty space at the end of the text area
532: // when the buffer has less lines than are visible
533: if (prevPhysLine == -1)
534: return -1;
535: else {
536: return textArea.displayManager
537: .getNextVisibleLine(prevPhysLine);
538: }
539: }
540: } //}}}
541:
542: //{{{ updateChunksUpTo() method
543: private void updateChunksUpTo(int lastScreenLine) {
544: // this method is a nightmare
545: if (lastScreenLine >= lineInfo.length)
546: throw new ArrayIndexOutOfBoundsException(lastScreenLine);
547:
548: // if one line's chunks are invalid, remaining lines are also
549: // invalid
550: if (lastScreenLine < firstInvalidLine)
551: return;
552:
553: int firstScreenLine = getFirstScreenLine();
554: int physicalLine = getUpdateStartLine(firstScreenLine);
555:
556: if (Debug.CHUNK_CACHE_DEBUG) {
557: Log.log(Log.DEBUG, this , "Updating chunks from "
558: + firstScreenLine + " to " + lastScreenLine);
559: }
560:
561: // Note that we rely on the fact that when a physical line is
562: // invalidated, all screen lines/subregions of that line are
563: // invalidated as well. See below comment for code that tries
564: // to uphold this assumption.
565:
566: out.clear();
567:
568: int offset = 0;
569: int length = 0;
570:
571: for (int i = firstScreenLine; i <= lastScreenLine; i++) {
572: LineInfo info = lineInfo[i];
573:
574: Chunk chunks;
575:
576: // get another line of chunks
577: if (out.isEmpty()) {
578: // unless this is the first time, increment
579: // the line number
580: if (physicalLine != -1 && i != firstScreenLine) {
581: physicalLine = textArea.displayManager
582: .getNextVisibleLine(physicalLine);
583: }
584:
585: // empty space
586: if (physicalLine == -1) {
587: info.chunks = null;
588: info.physicalLine = -1;
589: // fix the bug where the horiz.
590: // scroll bar was not updated
591: // after creating a new file.
592: info.width = 0;
593: continue;
594: }
595:
596: // chunk the line.
597: lineToChunkList(physicalLine, out);
598:
599: info.firstSubregion = true;
600:
601: int screenLines;
602:
603: // if the line has no text, out.size() == 0
604: if (out.isEmpty()) {
605: screenLines = 1;
606:
607: if (i == 0) {
608: if (textArea.displayManager.firstLine.skew > 0) {
609: Log
610: .log(
611: Log.ERROR,
612: this ,
613: "BUG: skew="
614: + textArea.displayManager.firstLine.skew
615: + ",out.size()="
616: + out.size());
617: textArea.displayManager.firstLine.skew = 0;
618: needFullRepaint = true;
619: lastScreenLine = lineInfo.length - 1;
620: }
621: }
622: chunks = null;
623: offset = 0;
624: length = 1;
625: }
626: // otherwise, the number of subregions
627: else {
628: screenLines = out.size();
629:
630: if (i == 0) {
631: int skew = textArea.displayManager.firstLine.skew;
632: if (skew >= out.size()) {
633: Log.log(Log.ERROR, this , "BUG: skew="
634: + skew + ",out.size()="
635: + out.size());
636: skew = 0;
637: needFullRepaint = true;
638: lastScreenLine = lineInfo.length - 1;
639: } else if (skew > 0) {
640: info.firstSubregion = false;
641: for (int j = 0; j < skew; j++)
642: out.remove(0);
643: }
644: }
645: chunks = out.remove(0);
646: offset = chunks.offset;
647: if (!out.isEmpty())
648: length = out.get(0).offset - offset;
649: else
650: length = textArea.getLineLength(physicalLine)
651: - offset + 1;
652: }
653: } else {
654: info.firstSubregion = false;
655:
656: chunks = out.remove(0);
657: offset = chunks.offset;
658: if (!out.isEmpty())
659: length = out.get(0).offset - offset;
660: else
661: length = textArea.getLineLength(physicalLine)
662: - offset + 1;
663: }
664:
665: boolean lastSubregion = out.isEmpty();
666:
667: if (i == lastScreenLine
668: && lastScreenLine != lineInfo.length - 1) {
669: /* if the user changes the syntax token at the
670: * end of a line, need to do a full repaint. */
671: if (tokenHandler.getLineContext() != info.lineContext) {
672: lastScreenLine++;
673: needFullRepaint = true;
674: }
675: /* If this line has become longer or shorter
676: * (in which case the new physical line number
677: * is different from the cached one) we need to:
678: * - continue updating past the last line
679: * - advise the text area to repaint
680: * On the other hand, if the line wraps beyond
681: * lastScreenLine, we need to keep updating the
682: * chunk list to ensure proper alignment of
683: * invalidation flags (see start of method) */
684: else if (info.physicalLine != physicalLine
685: || info.lastSubregion != lastSubregion) {
686: lastScreenLine++;
687: needFullRepaint = true;
688: }
689: /* We only cache entire physical lines at once;
690: * don't want to split a physical line into
691: * screen lines and only have some valid. */
692: else if (!out.isEmpty())
693: lastScreenLine++;
694: }
695:
696: info.physicalLine = physicalLine;
697: info.lastSubregion = lastSubregion;
698: info.offset = offset;
699: info.length = length;
700: info.chunks = chunks;
701: info.lineContext = tokenHandler.getLineContext();
702: }
703:
704: firstInvalidLine = Math.max(lastScreenLine + 1,
705: firstInvalidLine);
706: } //}}}
707:
708: //{{{ lineToChunkList() method
709: private void lineToChunkList(int physicalLine, List<Chunk> out) {
710: TextAreaPainter painter = textArea.getPainter();
711:
712: tokenHandler.init(painter.getStyles(), painter
713: .getFontRenderContext(), painter, out,
714: textArea.softWrap ? textArea.wrapMargin : 0.0f);
715: buffer.markTokens(physicalLine, tokenHandler);
716: } //}}}
717:
718: //}}}
719:
720: //{{{ LineInfo class
721: /** The informations on a line. (for fast access) */
722: static class LineInfo {
723: int physicalLine;
724: int offset;
725: int length;
726: boolean firstSubregion;
727: boolean lastSubregion;
728: Chunk chunks;
729: /** The line width. */
730: int width;
731: TokenMarker.LineContext lineContext;
732: } //}}}
733: }
|