001: /*
002: * WrapText.java
003: *
004: * Copyright (C) 1998-2004 Peter Graves
005: * $Id: WrapText.java,v 1.11 2004/09/20 00:13:44 piso Exp $
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License
009: * as published by the Free Software Foundation; either version 2
010: * of the License, or (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
020: */
021:
022: package org.armedbear.j;
023:
024: import gnu.regexp.RE;
025: import gnu.regexp.REMatch;
026: import gnu.regexp.UncheckedRE;
027: import javax.swing.undo.CompoundEdit;
028: import org.armedbear.j.mail.SendMail;
029:
030: public final class WrapText implements Constants {
031: private final Editor editor;
032: private final Buffer buffer;
033: private Position dot;
034: private Position mark;
035: private final int wrapCol;
036: private final int tabWidth;
037: private final boolean isHtml;
038:
039: public WrapText(Editor editor) {
040: this .editor = editor;
041: buffer = editor.getBuffer();
042: dot = editor.getDot();
043: mark = editor.getMark();
044: wrapCol = buffer.getIntegerProperty(Property.WRAP_COL);
045: tabWidth = buffer.getTabWidth();
046: isHtml = buffer.getModeId() == HTML_MODE;
047: }
048:
049: public void wrapRegion() {
050: wrapRegion(new Region(buffer, dot, mark));
051: }
052:
053: public void wrapParagraphsInRegion() {
054: final Region r;
055: if (dot != null && mark != null) {
056: r = new Region(buffer, dot, mark);
057: } else {
058: Line line = buffer.getFirstLine();
059: if (buffer instanceof SendMail) {
060: // Skip headers and header separator line.
061: while (line != null) {
062: String s = line.trim();
063: line = line.next();
064: if (s.equals(SendMail.getHeaderSeparator()))
065: break;
066: }
067: }
068: if (line == null)
069: return;
070: r = new Region(buffer, new Position(line, 0), buffer
071: .getEnd());
072: }
073: Position savedDot = dot.copy();
074: boolean seenDot = false;
075: CompoundEdit compoundEdit = buffer.beginCompoundEdit();
076: editor.moveDotTo(new Position(r.getBeginLine(), 0));
077: while (true) {
078: while (dot.getLine().isBlank() && dot.getNextLine() != null)
079: dot.setLine(dot.getNextLine());
080: Position start = dot.copy();
081: Position end = findEndOfParagraph(dot);
082: if (!seenDot && !savedDot.isBefore(start)
083: && savedDot.isBefore(end)) {
084: editor.moveDotTo(savedDot);
085: wrapParagraph();
086: savedDot = dot.copy();
087: seenDot = true;
088: } else
089: wrapParagraph();
090: if (buffer.needsRenumbering())
091: buffer.renumber();
092: Position pos = findEndOfParagraph(dot);
093: if (!pos.getLine().isBefore(r.getEndLine()))
094: break;
095: if (pos.getLine() == dot.getLine())
096: break;
097: editor.moveDotTo(pos);
098: }
099: if (buffer.contains(savedDot.getLine()))
100: editor.moveDotTo(savedDot);
101: buffer.endCompoundEdit(compoundEdit);
102: }
103:
104: public void wrapLine() {
105: // Don't try to wrap header lines in mail composition buffers!
106: if (buffer instanceof SendMail)
107: if (((SendMail) buffer).isHeaderLine(dot.getLine()))
108: return;
109: Position begin = new Position(dot.getLine(), 0);
110: Position end;
111: if (dot.getNextLine() != null)
112: end = new Position(dot.getNextLine(), 0);
113: else
114: end = new Position(dot.getLine(), dot.getLineLength());
115: Region r = new Region(buffer, begin, end);
116: wrapRegion(r);
117: }
118:
119: public void wrapParagraph() {
120: String prefix = getPrefix(dot.getLine());
121: final Position begin, end;
122: if (prefix != null) {
123: int prefixLength = prefix.length();
124: begin = findStartOfQuotedText(dot, prefix, prefixLength);
125: end = findEndOfQuotedText(dot, prefix, prefixLength);
126: } else {
127: begin = findStartOfParagraph(dot);
128: end = findEndOfParagraph(dot);
129: }
130: if (begin != null && end != null) {
131: Region r = new Region(buffer, begin, end);
132: wrapRegion(r, prefix);
133: }
134: }
135:
136: public void unwrapParagraph() {
137: Position begin = findStartOfParagraph(dot);
138: Position end = findEndOfParagraph(dot);
139: if (begin != null && end != null) {
140: Region r = new Region(buffer, begin, end);
141: unwrapRegion(r);
142: }
143: }
144:
145: private void wrapCommentInternal() {
146: String commentStart = null;
147: final Line dotLine = dot.getLine();
148: final String trim = dotLine.trim();
149:
150: switch (buffer.getModeId()) {
151: case JAVA_MODE:
152: case JAVASCRIPT_MODE:
153: case C_MODE:
154: case CPP_MODE:
155: case VERILOG_MODE:
156: if (trim.startsWith("// "))
157: commentStart = "// ";
158: else if (trim.startsWith("* "))
159: commentStart = "* ";
160: break;
161: case PERL_MODE:
162: case PROPERTIES_MODE:
163: if (trim.startsWith("# "))
164: commentStart = "# ";
165: break;
166: default:
167: commentStart = buffer.getMode().getCommentStart();
168: break;
169: }
170:
171: if (commentStart != null) {
172: int index = dotLine.getText().indexOf(commentStart);
173: if (index < 0)
174: return;
175: String prefix = dotLine.getText().substring(0,
176: index + commentStart.length());
177: Position begin = findStartOfComment(dot, commentStart);
178: Position end = findEndOfComment(dot, commentStart);
179: if (begin != null && end != null) {
180: try {
181: buffer.lockWrite();
182: } catch (InterruptedException e) {
183: Log.error(e);
184: return;
185: }
186: try {
187: processRegion(new Region(buffer, begin, end),
188: prefix, true);
189: } finally {
190: buffer.unlockWrite();
191: }
192: }
193: }
194: }
195:
196: private void wrapRegion(Region r) {
197: wrapRegion(r, null);
198: }
199:
200: private void wrapRegion(Region r, String prefix) {
201: try {
202: r.getBuffer().lockWrite();
203: } catch (InterruptedException e) {
204: Log.error(e);
205: return;
206: }
207: try {
208: processRegion(r, prefix, true);
209: } finally {
210: r.getBuffer().unlockWrite();
211: }
212: }
213:
214: private void unwrapRegion(Region r) {
215: try {
216: r.getBuffer().lockWrite();
217: } catch (InterruptedException e) {
218: Log.error(e);
219: return;
220: }
221: try {
222: processRegion(r, null, false);
223: } finally {
224: r.getBuffer().unlockWrite();
225: }
226: }
227:
228: private void processRegion(Region r, String prefix, boolean wrap) {
229: // Remember original contents of region.
230: final String before = r.toString();
231:
232: if (before.length() == 0)
233: return;
234:
235: int offsetBefore = buffer.getAbsoluteOffset(dot);
236:
237: int originalModificationCount = buffer.getModCount();
238:
239: // Save undo information before detabbing region (which may also move
240: // dot).
241: CompoundEdit compoundEdit = new CompoundEdit();
242: compoundEdit.addEdit(new UndoMove(editor));
243: editor.setMark(null);
244:
245: // Detab region (may move dot).
246: detab(r);
247:
248: int savedOffset = buffer.getAbsoluteOffset(dot);
249:
250: final String detabbed = r.toString();
251:
252: // Working copy.
253: String s = detabbed;
254:
255: // Remove trailing '\n'.
256: if (s.charAt(s.length() - 1) == '\n')
257: s = s.substring(0, s.length() - 1);
258:
259: FastStringBuffer sb = new FastStringBuffer();
260:
261: if (prefix != null) {
262: prefix = Utilities.detab(prefix, tabWidth);
263: } else {
264: // If not specified, prefix is whitespace at start of first line.
265: for (int i = 0; i < s.length(); i++) {
266: char c = s.charAt(i);
267: if (c == '\t') {
268: // String should be detabbed at this point.
269: Log.error("tab found unexpectedly at offset " + i);
270: Debug.assertTrue(false);
271: }
272: if (c == ' ')
273: sb.append(c);
274: else
275: break;
276: }
277: prefix = sb.toString();
278: }
279:
280: final int prefixLength = prefix.length();
281:
282: if (prefixLength > 0)
283: s = s.substring(prefixLength);
284:
285: if (!Utilities.isWhitespace(prefix)) {
286: // Replace prefix with spaces.
287: sb.setLength(0);
288: int index;
289: int begin = 0;
290: while ((index = s.indexOf("\n" + prefix, begin)) >= 0) {
291: sb.append(s.substring(begin, index));
292: sb.append('\n');
293: sb.append(Utilities.spaces(prefixLength));
294: begin = index + prefixLength + 1;
295: }
296: sb.append(s.substring(begin));
297: s = sb.toString();
298: }
299:
300: sb.setLength(0);
301: sb.append(prefix);
302: final int start = buffer.getAbsoluteOffset(r.getBegin());
303: char lastChar = '\0';
304: int numSkipped = 0;
305: final int limit = s.length();
306: for (int i = 0; i < limit; i++) {
307: char c = s.charAt(i);
308: if (c == '\n') {
309: if (lastChar == '-') {
310: // Line ends with '-'. We want to remove the '\n' and skip
311: // leading spaces on the next line. We can accomplish this
312: // by pretending the '-' is a space...
313: lastChar = ' ';
314: }
315: c = ' ';
316: }
317: if (c == ' ') {
318: if (lastChar == ' ') {
319: if (start + prefixLength + i <= savedOffset)
320: ++numSkipped;
321: } else {
322: sb.append(c);
323: lastChar = c;
324: }
325: } else {
326: // Not a space.
327: sb.append(c);
328: lastChar = c;
329: }
330: }
331:
332: savedOffset -= numSkipped;
333:
334: final String unwrapped = sb.toString();
335:
336: String toBeInserted = null;
337:
338: if (wrap) {
339: if (getCol(prefix, prefixLength) >= wrapCol) {
340: editor
341: .status("Can't wrap (indentation extends beyond wrap column)");
342: return;
343: }
344:
345: sb.setLength(0);
346: String remaining = unwrapped;
347: int where = start;
348: int startCol = 0;
349: int adjust = 0;
350:
351: // All the tabs have been replaced with spaces, so we can just use
352: // the length of the remaining string.
353: while (remaining.length() > wrapCol - startCol) {
354: int breakOffset = findBreak(remaining, wrapCol
355: - startCol);
356: sb.append(remaining.substring(0, breakOffset));
357: where += breakOffset;
358: sb.append('\n');
359: if (where <= savedOffset)
360: ++adjust;
361: remaining = remaining.substring(breakOffset);
362: if (remaining.length() > 0
363: && remaining.charAt(0) == ' ') {
364: remaining = remaining.substring(1);
365: if (where <= savedOffset)
366: --adjust;
367: ++where;
368: }
369: if (prefix.length() > 0) {
370: sb.append(prefix);
371: if (where <= savedOffset)
372: adjust += prefixLength;
373: startCol = getCol(prefix, prefixLength);
374: }
375: }
376: sb.append(remaining);
377: toBeInserted = sb.toString();
378: savedOffset += adjust;
379: } else
380: toBeInserted = unwrapped;
381:
382: toBeInserted += "\n";
383:
384: if (toBeInserted.equals(detabbed)) {
385: // No change. Restore status quo.
386: dot.moveTo(r.getBegin());
387: r.delete();
388: buffer.insertString(dot, before);
389: Position pos = buffer.getPosition(offsetBefore);
390: Debug.assertTrue(pos != null);
391: if (pos != null)
392: editor.getDot().moveTo(pos);
393: editor.getDisplay().setShift(0);
394:
395: // Buffer has not been modified.
396: buffer.setModCount(originalModificationCount);
397: return;
398: }
399:
400: // Commit undo information.
401: buffer.addEdit(compoundEdit);
402:
403: dot.moveTo(r.getBegin());
404: editor.addUndoDeleteRegion(r);
405: r.delete();
406: editor.addUndo(SimpleEdit.INSERT_STRING);
407:
408: // This leaves dot at the end of the inserted string.
409: buffer.insertString(dot, toBeInserted);
410:
411: // If we don't need to entab, we no longer need r. If we do need to
412: // entab, the bounds of the region may have changed, so we reconstruct
413: // it here.
414: r = buffer.getUseTabs() ? new Region(buffer, r.getBegin(), dot)
415: : null;
416:
417: // Move dot where it needs to go before entabbing, so the entabbing
418: // code can update it correctly.
419: Position pos = buffer.getPosition(savedOffset);
420: Debug.assertTrue(pos != null);
421: if (pos != null)
422: editor.moveDotTo(pos);
423:
424: if (r != null)
425: entab(r);
426:
427: editor.getDisplay().setShift(0);
428: editor.moveCaretToDotCol();
429: buffer.endCompoundEdit(compoundEdit);
430: }
431:
432: private int getCol(String s, int offset) {
433: if (offset > s.length())
434: offset = s.length();
435: int col = 0;
436: for (int i = 0; i < offset; i++) {
437: if (s.charAt(i) == '\t')
438: col += tabWidth - col % tabWidth;
439: else
440: ++col;
441: }
442: return col;
443: }
444:
445: private int findBreak(String s, int maxLength) {
446: int breakOffset = 0;
447: final int limit = Math.min(s.length(), maxLength);
448: for (int i = 0; i < limit; i++) {
449: char c = s.charAt(i);
450: if (c == ' ')
451: breakOffset = i;
452: else if (c == '-') {
453: if (i < limit - 1)
454: breakOffset = i + 1;
455: } else if (isHtml && c == '<') {
456: // Avoid end tags on the first pass.
457: if (i < s.length() - 1 && (c = s.charAt(i + 1)) != '/') {
458: // Start tag.
459: breakOffset = i;
460: // Avoid breaks within <a> tags if possible.
461: if (c == 'a' || c == 'A') {
462: if (s.regionMatches(true, i, "<a ", 0, 3)) {
463: // It's an <a> tag. Look for end tag.
464: int index = s.toLowerCase().indexOf("</a>",
465: i + 3);
466: if (index >= 0 && index + 4 < limit) {
467: breakOffset = index + 4;
468: i = breakOffset;
469: } else {
470: // Don't break at the space after "<a".
471: i += 2;
472: }
473: }
474: }
475: }
476: }
477: }
478: if (breakOffset == 0 && isHtml) {
479: // No break found. Now we'll settle for an end tag.
480: for (int i = 0; i < limit; i++) {
481: if (s.charAt(i) == '<')
482: breakOffset = i;
483: }
484: }
485: if (breakOffset == 0) // No tabs or spaces fouund.
486: breakOffset = maxLength;
487: return breakOffset;
488: }
489:
490: private static Position findStartOfParagraph(Position startingPoint) {
491: Position pos = new Position(startingPoint);
492: while (true) {
493: Line previousLine = pos.getPreviousLine();
494: if (previousLine == null || previousLine.isBlank())
495: break;
496: String s = previousLine.trim();
497: if (s.equals(SendMail.getHeaderSeparator()))
498: break;
499: if (s.endsWith(">"))
500: break;
501: pos.setLine(previousLine);
502: s = s.toLowerCase();
503: if (s.startsWith("<p>") || s.startsWith("<br>"))
504: break;
505: }
506: pos.setOffset(0);
507: return pos;
508: }
509:
510: private static Position findEndOfParagraph(Position startingPoint) {
511: Line line = startingPoint.getLine();
512: while (true) {
513: if (line.next() == null)
514: return new Position(line, line.length());
515: line = line.next();
516: if (line.isBlank())
517: return new Position(line, 0);
518: String s = line.trim().toLowerCase();
519: // Honor HTML breaks.
520: if (s.startsWith("<p>") || s.startsWith("</p>")
521: || s.startsWith("<br>") || s.startsWith("<li>")
522: || s.startsWith("</li>") || s.startsWith("<dl>")
523: || s.startsWith("</dl>") || s.startsWith("</body>")
524: || s.startsWith("<pre>")) {
525: return new Position(line, 0);
526: }
527: if (s.endsWith("<p>") || s.endsWith("</p>")
528: || s.endsWith("<br>") || s.endsWith("</li>")) {
529: if (line.next() != null)
530: return new Position(line.next(), 0);
531: return new Position(line, line.length());
532: }
533: }
534: }
535:
536: private static Position findStartOfComment(Position startingPoint,
537: String commentStart) {
538: Line beginLine = null;
539: for (Line line = startingPoint.getLine(); line != null; line = line
540: .previous()) {
541: if (!line.trim().startsWith(commentStart))
542: break;
543: int index = line.getText().indexOf(commentStart);
544: String remaining = line.substring(
545: index + commentStart.length()).trim();
546: if (remaining.endsWith("</pre>"))
547: break;
548: if (remaining.endsWith("<p>"))
549: break;
550: if (remaining.startsWith("<p>")) {
551: beginLine = line;
552: break;
553: }
554: beginLine = line;
555: }
556: if (beginLine != null)
557: return new Position(beginLine, 0);
558: return null;
559: }
560:
561: private static Position findEndOfComment(Position startingPoint,
562: String commentStart) {
563: Line endLine = null;
564: for (Line line = startingPoint.getLine(); line != null; line = line
565: .next()) {
566: if (!line.trim().startsWith(commentStart))
567: break;
568: int index = line.getText().indexOf(commentStart);
569: String remaining = line.substring(
570: index + commentStart.length()).trim();
571: if (remaining.startsWith("<p>"))
572: break;
573: if (remaining.startsWith("<pre>"))
574: break;
575: endLine = line;
576: }
577: if (endLine == null)
578: return null;
579: if (endLine.next() != null)
580: return new Position(endLine.next(), 0);
581: return new Position(endLine, endLine.length());
582: }
583:
584: private static final RE prefixRE = new UncheckedRE("^>[> ]*");
585:
586: private static String getPrefix(Line line) {
587: REMatch match = prefixRE.getMatch(line.getText());
588: return match != null ? match.toString() : null;
589: }
590:
591: private static Position findStartOfQuotedText(Position pos,
592: String prefix, int prefixLength) {
593: Line start = pos.getLine();
594: for (Line line = start.previous(); line != null; line = line
595: .previous()) {
596: if (!prefix.equals(getPrefix(line)))
597: break;
598: if (line.substring(prefixLength).trim().length() == 0)
599: break; // Blank line (except for prefix string).
600: start = line;
601: }
602: return new Position(start, 0);
603: }
604:
605: private static Position findEndOfQuotedText(Position pos,
606: String prefix, int prefixLength) {
607: Line end = pos.getLine();
608: for (Line line = end.next(); line != null; line = line.next()) {
609: if (!prefix.equals(getPrefix(line)))
610: break;
611: if (line.substring(prefixLength).trim().length() == 0)
612: break; // Blank line (except for prefix string).
613: end = line;
614: }
615: if (end.next() != null)
616: return new Position(end.next(), 0);
617: return new Position(end, end.length());
618: }
619:
620: private void detab(Region r) {
621: Debug.assertTrue(r.getBeginOffset() == 0);
622: Debug.assertTrue(r.getEndOffset() == 0
623: || r.getEndOffset() == r.getEndLine().length());
624: Debug.assertTrue(buffer == r.getBuffer());
625: int dotCol = buffer.getCol(dot);
626: Line line = r.getBeginLine();
627: while (line != null && line != r.getEndLine()) {
628: line.setText(Utilities.detab(line.getText(), tabWidth));
629: line = line.next();
630: }
631: if (line == r.getEndLine()
632: && r.getEndOffset() == r.getEndLine().length())
633: line.setText(Utilities.detab(line.getText(), tabWidth));
634: // Don't assume dot.line has been detabbed (dot might not be in the
635: // detabbed region).
636: dot.moveToCol(dotCol, tabWidth);
637: }
638:
639: private void entab(Region r) {
640: Debug.assertTrue(r.getBeginOffset() == 0);
641: Debug.assertTrue(r.getEndOffset() == 0
642: || r.getEndOffset() == r.getEndLine().length());
643: Debug.assertTrue(buffer == r.getBuffer());
644: int dotCol = buffer.getCol(dot);
645: Line line = r.getBeginLine();
646: while (line != null && line != r.getEndLine()) {
647: line.setText(Utilities.entab(line.getText(), tabWidth));
648: line = line.next();
649: }
650: if (line == r.getEndLine()
651: && r.getEndOffset() == r.getEndLine().length())
652: line.setText(Utilities.entab(line.getText(), tabWidth));
653: dot.moveToCol(dotCol, tabWidth);
654: }
655:
656: public static void wrapComment() {
657: final Editor editor = Editor.currentEditor();
658: if (!editor.checkReadOnly())
659: return;
660: new WrapText(editor).wrapCommentInternal();
661: }
662:
663: public static void toggleWrap() {
664: final Buffer buffer = Editor.currentEditor().getBuffer();
665: boolean b = !buffer.getBooleanProperty(Property.WRAP);
666: buffer.setProperty(Property.WRAP, b);
667: buffer.saveProperties();
668: }
669: }
|