/*
* WrapText.java
*
* Copyright (C) 1998-2004 Peter Graves
* $Id: WrapText.java,v 1.11 2004/09/20 00:13:44 piso Exp $
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.armedbear.j;
import gnu.regexp.RE;
import gnu.regexp.REMatch;
import gnu.regexp.UncheckedRE;
import javax.swing.undo.CompoundEdit;
import org.armedbear.j.mail.SendMail;
public final class WrapText implements Constants
{
private final Editor editor;
private final Buffer buffer;
private Position dot;
private Position mark;
private final int wrapCol;
private final int tabWidth;
private final boolean isHtml;
public WrapText(Editor editor)
{
this.editor = editor;
buffer = editor.getBuffer();
dot = editor.getDot();
mark = editor.getMark();
wrapCol = buffer.getIntegerProperty(Property.WRAP_COL);
tabWidth = buffer.getTabWidth();
isHtml = buffer.getModeId() == HTML_MODE;
}
public void wrapRegion()
{
wrapRegion(new Region(buffer, dot, mark));
}
public void wrapParagraphsInRegion()
{
final Region r;
if (dot != null && mark != null) {
r = new Region(buffer, dot, mark);
} else {
Line line = buffer.getFirstLine();
if (buffer instanceof SendMail) {
// Skip headers and header separator line.
while (line != null) {
String s = line.trim();
line = line.next();
if (s.equals(SendMail.getHeaderSeparator()))
break;
}
}
if (line == null)
return;
r = new Region(buffer, new Position(line, 0), buffer.getEnd());
}
Position savedDot = dot.copy();
boolean seenDot = false;
CompoundEdit compoundEdit = buffer.beginCompoundEdit();
editor.moveDotTo(new Position(r.getBeginLine(), 0));
while (true) {
while (dot.getLine().isBlank() && dot.getNextLine() != null)
dot.setLine(dot.getNextLine());
Position start = dot.copy();
Position end = findEndOfParagraph(dot);
if (!seenDot && !savedDot.isBefore(start) && savedDot.isBefore(end)) {
editor.moveDotTo(savedDot);
wrapParagraph();
savedDot = dot.copy();
seenDot = true;
} else
wrapParagraph();
if (buffer.needsRenumbering())
buffer.renumber();
Position pos = findEndOfParagraph(dot);
if (!pos.getLine().isBefore(r.getEndLine()))
break;
if (pos.getLine() == dot.getLine())
break;
editor.moveDotTo(pos);
}
if (buffer.contains(savedDot.getLine()))
editor.moveDotTo(savedDot);
buffer.endCompoundEdit(compoundEdit);
}
public void wrapLine()
{
// Don't try to wrap header lines in mail composition buffers!
if (buffer instanceof SendMail)
if (((SendMail)buffer).isHeaderLine(dot.getLine()))
return;
Position begin = new Position(dot.getLine(), 0);
Position end;
if (dot.getNextLine() != null)
end = new Position(dot.getNextLine(), 0);
else
end = new Position(dot.getLine(), dot.getLineLength());
Region r = new Region(buffer, begin, end);
wrapRegion(r);
}
public void wrapParagraph()
{
String prefix = getPrefix(dot.getLine());
final Position begin, end;
if (prefix != null) {
int prefixLength = prefix.length();
begin = findStartOfQuotedText(dot, prefix, prefixLength);
end = findEndOfQuotedText(dot, prefix, prefixLength);
} else {
begin = findStartOfParagraph(dot);
end = findEndOfParagraph(dot);
}
if (begin != null && end != null) {
Region r = new Region(buffer, begin, end);
wrapRegion(r, prefix);
}
}
public void unwrapParagraph()
{
Position begin = findStartOfParagraph(dot);
Position end = findEndOfParagraph(dot);
if (begin != null && end != null) {
Region r = new Region(buffer, begin, end);
unwrapRegion(r);
}
}
private void wrapCommentInternal()
{
String commentStart = null;
final Line dotLine = dot.getLine();
final String trim = dotLine.trim();
switch (buffer.getModeId()) {
case JAVA_MODE:
case JAVASCRIPT_MODE:
case C_MODE:
case CPP_MODE:
case VERILOG_MODE:
if (trim.startsWith("// "))
commentStart = "// ";
else if (trim.startsWith("* "))
commentStart = "* ";
break;
case PERL_MODE:
case PROPERTIES_MODE:
if (trim.startsWith("# "))
commentStart = "# ";
break;
default:
commentStart = buffer.getMode().getCommentStart();
break;
}
if (commentStart != null) {
int index = dotLine.getText().indexOf(commentStart);
if (index < 0)
return;
String prefix =
dotLine.getText().substring(0, index + commentStart.length());
Position begin = findStartOfComment(dot, commentStart);
Position end = findEndOfComment(dot, commentStart);
if (begin != null && end != null) {
try {
buffer.lockWrite();
}
catch (InterruptedException e) {
Log.error(e);
return;
}
try {
processRegion(new Region(buffer, begin, end), prefix, true);
}
finally {
buffer.unlockWrite();
}
}
}
}
private void wrapRegion(Region r)
{
wrapRegion(r, null);
}
private void wrapRegion(Region r, String prefix)
{
try {
r.getBuffer().lockWrite();
}
catch (InterruptedException e) {
Log.error(e);
return;
}
try {
processRegion(r, prefix, true);
}
finally {
r.getBuffer().unlockWrite();
}
}
private void unwrapRegion(Region r)
{
try {
r.getBuffer().lockWrite();
}
catch (InterruptedException e) {
Log.error(e);
return;
}
try {
processRegion(r, null, false);
}
finally {
r.getBuffer().unlockWrite();
}
}
private void processRegion(Region r, String prefix, boolean wrap)
{
// Remember original contents of region.
final String before = r.toString();
if (before.length() == 0)
return;
int offsetBefore = buffer.getAbsoluteOffset(dot);
int originalModificationCount = buffer.getModCount();
// Save undo information before detabbing region (which may also move
// dot).
CompoundEdit compoundEdit = new CompoundEdit();
compoundEdit.addEdit(new UndoMove(editor));
editor.setMark(null);
// Detab region (may move dot).
detab(r);
int savedOffset = buffer.getAbsoluteOffset(dot);
final String detabbed = r.toString();
// Working copy.
String s = detabbed;
// Remove trailing '\n'.
if (s.charAt(s.length() - 1) == '\n')
s = s.substring(0, s.length() - 1);
FastStringBuffer sb = new FastStringBuffer();
if (prefix != null) {
prefix = Utilities.detab(prefix, tabWidth);
} else {
// If not specified, prefix is whitespace at start of first line.
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '\t') {
// String should be detabbed at this point.
Log.error("tab found unexpectedly at offset " + i);
Debug.assertTrue(false);
}
if (c == ' ')
sb.append(c);
else
break;
}
prefix = sb.toString();
}
final int prefixLength = prefix.length();
if (prefixLength > 0)
s = s.substring(prefixLength);
if (!Utilities.isWhitespace(prefix)) {
// Replace prefix with spaces.
sb.setLength(0);
int index;
int begin = 0;
while ((index = s.indexOf("\n" + prefix, begin)) >= 0) {
sb.append(s.substring(begin, index));
sb.append('\n');
sb.append(Utilities.spaces(prefixLength));
begin = index + prefixLength + 1;
}
sb.append(s.substring(begin));
s = sb.toString();
}
sb.setLength(0);
sb.append(prefix);
final int start = buffer.getAbsoluteOffset(r.getBegin());
char lastChar = '\0';
int numSkipped = 0;
final int limit = s.length();
for (int i = 0; i < limit; i++) {
char c = s.charAt(i);
if (c == '\n') {
if (lastChar == '-') {
// Line ends with '-'. We want to remove the '\n' and skip
// leading spaces on the next line. We can accomplish this
// by pretending the '-' is a space...
lastChar = ' ';
}
c = ' ';
}
if (c == ' ') {
if (lastChar == ' ') {
if (start + prefixLength + i <= savedOffset)
++numSkipped;
} else {
sb.append(c);
lastChar = c;
}
} else {
// Not a space.
sb.append(c);
lastChar = c;
}
}
savedOffset -= numSkipped;
final String unwrapped = sb.toString();
String toBeInserted = null;
if (wrap) {
if (getCol(prefix, prefixLength) >= wrapCol) {
editor.status("Can't wrap (indentation extends beyond wrap column)");
return;
}
sb.setLength(0);
String remaining = unwrapped;
int where = start;
int startCol = 0;
int adjust = 0;
// All the tabs have been replaced with spaces, so we can just use
// the length of the remaining string.
while (remaining.length() > wrapCol - startCol) {
int breakOffset = findBreak(remaining, wrapCol - startCol);
sb.append(remaining.substring(0, breakOffset));
where += breakOffset;
sb.append('\n');
if (where <= savedOffset)
++adjust;
remaining = remaining.substring(breakOffset);
if (remaining.length() > 0 && remaining.charAt(0) == ' ') {
remaining = remaining.substring(1);
if (where <= savedOffset)
--adjust;
++where;
}
if (prefix.length() > 0) {
sb.append(prefix);
if (where <= savedOffset)
adjust += prefixLength;
startCol = getCol(prefix, prefixLength);
}
}
sb.append(remaining);
toBeInserted = sb.toString();
savedOffset += adjust;
} else
toBeInserted = unwrapped;
toBeInserted += "\n";
if (toBeInserted.equals(detabbed)) {
// No change. Restore status quo.
dot.moveTo(r.getBegin());
r.delete();
buffer.insertString(dot, before);
Position pos = buffer.getPosition(offsetBefore);
Debug.assertTrue(pos != null);
if (pos != null)
editor.getDot().moveTo(pos);
editor.getDisplay().setShift(0);
// Buffer has not been modified.
buffer.setModCount(originalModificationCount);
return;
}
// Commit undo information.
buffer.addEdit(compoundEdit);
dot.moveTo(r.getBegin());
editor.addUndoDeleteRegion(r);
r.delete();
editor.addUndo(SimpleEdit.INSERT_STRING);
// This leaves dot at the end of the inserted string.
buffer.insertString(dot, toBeInserted);
// If we don't need to entab, we no longer need r. If we do need to
// entab, the bounds of the region may have changed, so we reconstruct
// it here.
r = buffer.getUseTabs() ? new Region(buffer, r.getBegin(), dot) : null;
// Move dot where it needs to go before entabbing, so the entabbing
// code can update it correctly.
Position pos = buffer.getPosition(savedOffset);
Debug.assertTrue(pos != null);
if (pos != null)
editor.moveDotTo(pos);
if (r != null)
entab(r);
editor.getDisplay().setShift(0);
editor.moveCaretToDotCol();
buffer.endCompoundEdit(compoundEdit);
}
private int getCol(String s, int offset)
{
if (offset > s.length())
offset = s.length();
int col = 0;
for (int i = 0; i < offset; i++) {
if (s.charAt(i) == '\t')
col += tabWidth - col % tabWidth;
else
++col;
}
return col;
}
private int findBreak(String s, int maxLength)
{
int breakOffset = 0;
final int limit = Math.min(s.length(), maxLength);
for (int i = 0; i < limit; i++) {
char c = s.charAt(i);
if (c == ' ')
breakOffset = i;
else if (c == '-') {
if (i < limit-1)
breakOffset = i+1;
} else if (isHtml && c == '<') {
// Avoid end tags on the first pass.
if (i < s.length()-1 && (c = s.charAt(i+1)) != '/') {
// Start tag.
breakOffset = i;
// Avoid breaks within <a> tags if possible.
if (c == 'a' || c == 'A') {
if (s.regionMatches(true, i, "<a ", 0, 3)) {
// It's an <a> tag. Look for end tag.
int index = s.toLowerCase().indexOf("</a>", i+3);
if (index >= 0 && index+4 < limit) {
breakOffset = index+4;
i = breakOffset;
} else {
// Don't break at the space after "<a".
i += 2;
}
}
}
}
}
}
if (breakOffset == 0 && isHtml) {
// No break found. Now we'll settle for an end tag.
for (int i = 0; i < limit; i++) {
if (s.charAt(i) == '<')
breakOffset = i;
}
}
if (breakOffset == 0) // No tabs or spaces fouund.
breakOffset = maxLength;
return breakOffset;
}
private static Position findStartOfParagraph(Position startingPoint)
{
Position pos = new Position(startingPoint);
while (true) {
Line previousLine = pos.getPreviousLine();
if (previousLine == null || previousLine.isBlank())
break;
String s = previousLine.trim();
if (s.equals(SendMail.getHeaderSeparator()))
break;
if (s.endsWith(">"))
break;
pos.setLine(previousLine);
s = s.toLowerCase();
if (s.startsWith("<p>") || s.startsWith("<br>"))
break;
}
pos.setOffset(0);
return pos;
}
private static Position findEndOfParagraph(Position startingPoint)
{
Line line = startingPoint.getLine();
while (true) {
if (line.next() == null)
return new Position(line, line.length());
line = line.next();
if (line.isBlank())
return new Position(line, 0);
String s = line.trim().toLowerCase();
// Honor HTML breaks.
if (s.startsWith("<p>") ||
s.startsWith("</p>") ||
s.startsWith("<br>") ||
s.startsWith("<li>") ||
s.startsWith("</li>") ||
s.startsWith("<dl>") ||
s.startsWith("</dl>") ||
s.startsWith("</body>") ||
s.startsWith("<pre>")) {
return new Position(line, 0);
}
if (s.endsWith("<p>") ||
s.endsWith("</p>") ||
s.endsWith("<br>") ||
s.endsWith("</li>")) {
if (line.next() != null)
return new Position(line.next(), 0);
return new Position(line, line.length());
}
}
}
private static Position findStartOfComment(Position startingPoint, String commentStart)
{
Line beginLine = null;
for (Line line = startingPoint.getLine(); line != null; line = line.previous()) {
if (!line.trim().startsWith(commentStart))
break;
int index = line.getText().indexOf(commentStart);
String remaining = line.substring(index + commentStart.length()).trim();
if (remaining.endsWith("</pre>"))
break;
if (remaining.endsWith("<p>"))
break;
if (remaining.startsWith("<p>")) {
beginLine = line;
break;
}
beginLine = line;
}
if (beginLine != null)
return new Position(beginLine, 0);
return null;
}
private static Position findEndOfComment(Position startingPoint, String commentStart)
{
Line endLine = null;
for (Line line = startingPoint.getLine(); line != null; line = line.next()) {
if (!line.trim().startsWith(commentStart))
break;
int index = line.getText().indexOf(commentStart);
String remaining = line.substring(index + commentStart.length()).trim();
if (remaining.startsWith("<p>"))
break;
if (remaining.startsWith("<pre>"))
break;
endLine = line;
}
if (endLine == null)
return null;
if (endLine.next() != null)
return new Position(endLine.next(), 0);
return new Position(endLine, endLine.length());
}
private static final RE prefixRE = new UncheckedRE("^>[> ]*");
private static String getPrefix(Line line)
{
REMatch match = prefixRE.getMatch(line.getText());
return match != null ? match.toString() : null;
}
private static Position findStartOfQuotedText(Position pos, String prefix,
int prefixLength)
{
Line start = pos.getLine();
for (Line line = start.previous(); line != null; line = line.previous()) {
if (!prefix.equals(getPrefix(line)))
break;
if (line.substring(prefixLength).trim().length() == 0)
break; // Blank line (except for prefix string).
start = line;
}
return new Position(start, 0);
}
private static Position findEndOfQuotedText(Position pos, String prefix,
int prefixLength)
{
Line end = pos.getLine();
for (Line line = end.next(); line != null; line = line.next()) {
if (!prefix.equals(getPrefix(line)))
break;
if (line.substring(prefixLength).trim().length() == 0)
break; // Blank line (except for prefix string).
end = line;
}
if (end.next() != null)
return new Position(end.next(), 0);
return new Position(end, end.length());
}
private void detab(Region r)
{
Debug.assertTrue(r.getBeginOffset() == 0);
Debug.assertTrue(r.getEndOffset() == 0 || r.getEndOffset() == r.getEndLine().length());
Debug.assertTrue(buffer == r.getBuffer());
int dotCol = buffer.getCol(dot);
Line line = r.getBeginLine();
while (line != null && line != r.getEndLine()) {
line.setText(Utilities.detab(line.getText(), tabWidth));
line = line.next();
}
if (line == r.getEndLine() && r.getEndOffset() == r.getEndLine().length())
line.setText(Utilities.detab(line.getText(), tabWidth));
// Don't assume dot.line has been detabbed (dot might not be in the
// detabbed region).
dot.moveToCol(dotCol, tabWidth);
}
private void entab(Region r)
{
Debug.assertTrue(r.getBeginOffset() == 0);
Debug.assertTrue(r.getEndOffset() == 0 ||
r.getEndOffset() == r.getEndLine().length());
Debug.assertTrue(buffer == r.getBuffer());
int dotCol = buffer.getCol(dot);
Line line = r.getBeginLine();
while (line != null && line != r.getEndLine()) {
line.setText(Utilities.entab(line.getText(), tabWidth));
line = line.next();
}
if (line == r.getEndLine() && r.getEndOffset() == r.getEndLine().length())
line.setText(Utilities.entab(line.getText(), tabWidth));
dot.moveToCol(dotCol, tabWidth);
}
public static void wrapComment()
{
final Editor editor = Editor.currentEditor();
if (!editor.checkReadOnly())
return;
new WrapText(editor).wrapCommentInternal();
}
public static void toggleWrap()
{
final Buffer buffer = Editor.currentEditor().getBuffer();
boolean b = !buffer.getBooleanProperty(Property.WRAP);
buffer.setProperty(Property.WRAP, b);
buffer.saveProperties();
}
}
|