diff options
| author | Markus Keller | 2012-10-08 22:41:28 +0000 |
|---|---|---|
| committer | Matthias Sohn | 2012-10-08 22:41:28 +0000 |
| commit | e9ca88c8ca3e48126ae036d6e3ae902fe3012d2b (patch) | |
| tree | 2dd9780fa42989f0dbb182c5885bb7d549142faa | |
| parent | 34b09b2724bd235aeab9d529538412883cc93d7b (diff) | |
| download | egit-e9ca88c8ca3e48126ae036d6e3ae902fe3012d2b.tar.gz egit-e9ca88c8ca3e48126ae036d6e3ae902fe3012d2b.tar.xz egit-e9ca88c8ca3e48126ae036d6e3ae902fe3012d2b.zip | |
Hard wrap should insert line breaks after user finishes editing
In the old implementation, hard-wrapping interfered with the user's
typing. This fix uses a segment listener on the StyledText to give a
preview of the final wrapping, and it only inserts the additional line
delimiters when the user commits the message.
Bug: 387932
Change-Id: Ib240752de554fcb3d4baa0a551eeb903303d528e
Signed-off-by: Markus Keller <markus_keller@ch.ibm.com>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
5 files changed, 135 insertions, 177 deletions
diff --git a/org.eclipse.egit.ui.test/src/org/eclipse/egit/ui/internal/dialogs/SpellcheckableMessageAreaTest.java b/org.eclipse.egit.ui.test/src/org/eclipse/egit/ui/internal/dialogs/SpellcheckableMessageAreaTest.java index cc4a615f2e..f52091994d 100644 --- a/org.eclipse.egit.ui.test/src/org/eclipse/egit/ui/internal/dialogs/SpellcheckableMessageAreaTest.java +++ b/org.eclipse.egit.ui.test/src/org/eclipse/egit/ui/internal/dialogs/SpellcheckableMessageAreaTest.java @@ -1,6 +1,7 @@ /******************************************************************************* * Copyright (C) 2010, Robin Stocker * Copyright (C) 2011, Matthias Sohn <matthias.sohn@sap.com> + * Copyright (C) 2012, IBM Corporation (Markus Keller <markus_keller@ch.ibm.com>) * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -11,10 +12,7 @@ package org.eclipse.egit.ui.internal.dialogs; import static org.junit.Assert.assertEquals; -import java.util.List; - -import org.eclipse.egit.ui.internal.dialogs.SpellcheckableMessageArea; -import org.eclipse.egit.ui.internal.dialogs.SpellcheckableMessageArea.WrapEdit; +import org.eclipse.egit.core.internal.Utils; import org.junit.Test; public class SpellcheckableMessageAreaTest { @@ -83,6 +81,12 @@ public class SpellcheckableMessageAreaTest { } @Test + public void dontWrapWordLongerThanOneLineAndKeepSpaceAtFront() { + String input = " 1234567890123456789012345678901234567890123456789012345678901234567890123456789012"; + assertWrappedEquals(input, input); + } + + @Test public void wrapSecondLongLine() { String input = "First line\n123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789.12"; String expected = "First line\n123456789 123456789 123456789 123456789 123456789 123456789 123456789\n123456789.12"; @@ -114,18 +118,20 @@ public class SpellcheckableMessageAreaTest { } @Test - public void lineAfterWrappedWordShouldBeJoined() { + public void lineAfterWrappedWordShouldNotBeJoined() { String input = "000000001 000000002 000000003 000000004 000000005 000000006 000000007 000000008\n000000009"; - String expected = "000000001 000000002 000000003 000000004 000000005 000000006 000000007\n000000008 000000009"; + String expected = "000000001 000000002 000000003 000000004 000000005 000000006 000000007\n000000008\n000000009"; assertWrappedEquals(expected, input); } @Test - public void lineAfterWrappedWordShouldBeJoinedAndJoinedLineWrapCorrectly() { + public void lineAfterWrappedWordShouldNotBeJoined2() { String input = "000000001 000000002 000000003 000000004 000000005 000000006 000000007 000000008\n" + "000000009 000000010 000000011 000000012 000000013 000000014 000000015 000000016"; String expected = "000000001 000000002 000000003 000000004 000000005 000000006 000000007\n" - + "000000008 000000009 000000010 000000011 000000012 000000013 000000014\n000000015 000000016"; + + "000000008\n" + + "000000009 000000010 000000011 000000012 000000013 000000014 000000015\n" + + "000000016"; assertWrappedEquals(expected, input); } @@ -155,9 +161,9 @@ public class SpellcheckableMessageAreaTest { } @Test - public void lineAfterWrappedWordShouldBeJoinedIfItStartsWithAParenthesis() { + public void lineAfterWrappedWordShouldNotBeJoined3() { String input = "* 000000001 000000002 000000003 000000004 000000005 000000006 000000007 000000008\n(paren)"; - String expected = "* 000000001 000000002 000000003 000000004 000000005 000000006 000000007\n000000008 (paren)"; + String expected = "* 000000001 000000002 000000003 000000004 000000005 000000006 000000007\n000000008\n(paren)"; assertWrappedEquals(expected, input); } @@ -179,14 +185,7 @@ public class SpellcheckableMessageAreaTest { } private static String wrap(String text, String lineDelimiter) { - StringBuilder sb = new StringBuilder(text); - List<WrapEdit> wrapEdits = SpellcheckableMessageArea - .calculateWrapEdits(text, - SpellcheckableMessageArea.MAX_LINE_WIDTH, lineDelimiter); - for (WrapEdit wrapEdit : wrapEdits) { - sb.replace(wrapEdit.getStart(), - wrapEdit.getStart() + wrapEdit.getLength(), wrapEdit.getReplacement()); - } - return sb.toString(); + String wrapped = SpellcheckableMessageArea.hardWrap(Utils.normalizeLineEndings(text)); + return wrapped.replaceAll("\n", lineDelimiter); } } diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CommitDialog.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CommitDialog.java index 960da6a638..201d2adf5a 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CommitDialog.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CommitDialog.java @@ -7,6 +7,7 @@ * Copyright (C) 2011, Mathias Kinzler <mathias.kinzler@sap.com> * Copyright (C) 2012, Daniel Megert <daniel_megert@ch.ibm.com> * Copyright (C) 2012, Robin Stocker <robin@nibor.org> + * Copyright (C) 2012, IBM Corporation (Markus Keller <markus_keller@ch.ibm.com>) * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -56,7 +57,6 @@ import org.eclipse.jface.dialogs.TitleAreaDialog; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.jface.preference.PreferenceDialog; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; @@ -81,7 +81,6 @@ import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerComparator; import org.eclipse.jface.viewers.ViewerFilter; -import org.eclipse.jface.window.Window; import org.eclipse.jgit.api.AddCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.Constants; @@ -583,11 +582,8 @@ public class CommitDialog extends TitleAreaDialog { public void widgetSelected(SelectionEvent e) { String[] pages = new String[] { UIPreferences.PAGE_COMMIT_PREFERENCES }; - PreferenceDialog dialog = PreferencesUtil - .createPreferenceDialogOn(getShell(), pages[0], pages, - null); - if (Window.OK == dialog.open()) - commitText.reconfigure(); + PreferencesUtil.createPreferenceDialogOn(getShell(), pages[0], + pages, null).open(); } }); diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CommitMessageComponent.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CommitMessageComponent.java index eb68a76c81..8aa2237800 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CommitMessageComponent.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CommitMessageComponent.java @@ -6,6 +6,7 @@ * Copyright (C) 2007, Shawn O. Pearce <spearce@spearce.org> * Copyright (C) 2011, Mathias Kinzler <mathias.kinzler@sap.com> * Copyright (C) 2011, Jens Baumgart <jens.baumgart@sap.com> + * Copyright (C) 2012, IBM Corporation (Markus Keller <markus_keller@ch.ibm.com>) * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -195,7 +196,10 @@ public class CommitMessageComponent { } /** - * @return The message the user entered + * Returns the commit message, converting platform-specific line endings to + * '\n' and hard-wrapping lines if necessary. + * + * @return the message */ public String getCommitMessage() { commitMessage = commitText.getCommitMessage(); @@ -347,7 +351,7 @@ public class CommitMessageComponent { * */ public void updateStateFromUI() { - commitMessage = commitText.getCommitMessage(); + commitMessage = commitText.getText(); author = authorText.getText().trim(); committer = committerText.getText().trim(); } diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CreateTagDialog.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CreateTagDialog.java index d35f09420e..50228f94fa 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CreateTagDialog.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CreateTagDialog.java @@ -2,6 +2,7 @@ * Copyright (C) 2010, Dariusz Luksza <dariusz@luksza.org> * Copyright (C) 2011, Mathias Kinzler <mathias.kinzler@sap.com> * Copyright (C) 2011, Matthias Sohn <matthias.sohn@sap.com> + * Copyright (C) 2012, IBM Corporation (Markus Keller <markus_keller@ch.ibm.com>) * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -369,7 +370,7 @@ public class CreateTagDialog extends TitleAreaDialog { tagName = tagNameText.getText(); if (commitCombo != null) tagCommit = commitCombo.getValue(); - tagMessage = tagMessageText.getText(); + tagMessage = tagMessageText.getCommitMessage(); overwriteTag = overwriteButton.getSelection(); //$FALL-THROUGH$ continue propagating OK button action default: diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/SpellcheckableMessageArea.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/SpellcheckableMessageArea.java index 3803a1eb35..a9a85a8a02 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/SpellcheckableMessageArea.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/SpellcheckableMessageArea.java @@ -14,12 +14,11 @@ *******************************************************************************/ package org.eclipse.egit.ui.internal.dialogs; +import java.util.Arrays; import java.util.Collections; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.regex.Pattern; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IAdaptable; @@ -65,14 +64,15 @@ import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jgit.util.IntList; import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.BidiSegmentEvent; +import org.eclipse.swt.custom.BidiSegmentListener; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; -import org.eclipse.swt.events.ModifyEvent; -import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; @@ -101,9 +101,6 @@ public class SpellcheckableMessageArea extends Composite { static final int MAX_LINE_WIDTH = 72; - // Symbol or Punctuation, Dash or Punctuation, Other - private static final Pattern STARTS_WITH_SYMBOL = Pattern.compile("[\\p{S}\\p{Pd}\\p{Po}].*"); //$NON-NLS-1$ - private static class TextViewerAction extends Action implements IUpdate { private int fOperationCode= -1; @@ -212,7 +209,7 @@ public class SpellcheckableMessageArea extends Composite { private final SourceViewer sourceViewer; - private ModifyListener hardWrapModifyListener; + private BidiSegmentListener hardWrapSegmentListener; /** * @param parent @@ -261,6 +258,18 @@ public class SpellcheckableMessageArea extends Composite { createMarginPainter(); configureHardWrap(); + final IPropertyChangeListener propertyChangeListener = new IPropertyChangeListener() { + public void propertyChange(PropertyChangeEvent event) { + if (UIPreferences.COMMIT_DIALOG_HARD_WRAP_MESSAGE.equals(event.getProperty())) { + getDisplay().asyncExec(new Runnable() { + public void run() { + configureHardWrap(); + } + }); + } + } + }; + Activator.getDefault().getPreferenceStore().addPropertyChangeListener(propertyChangeListener); final SourceViewerDecorationSupport support = configureAnnotationPreferences(); if (isEditable(sourceViewer)) { @@ -316,6 +325,7 @@ public class SpellcheckableMessageArea extends Composite { getTextWidget().addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent disposeEvent) { support.uninstall(); + Activator.getDefault().getPreferenceStore().removePropertyChangeListener(propertyChangeListener); } }); } @@ -326,38 +336,27 @@ public class SpellcheckableMessageArea extends Composite { private void configureHardWrap() { if (shouldHardWrap()) { - if (hardWrapModifyListener == null) { - final StyledText textWidget = getTextWidget(); - - hardWrapModifyListener = new ModifyListener() { - - private boolean active = true; - - public void modifyText(ModifyEvent e) { - if (!active) - return; - String lineDelimiter = textWidget.getLineDelimiter(); - List<WrapEdit> wrapEdits = calculateWrapEdits( - textWidget.getText(), MAX_LINE_WIDTH, - lineDelimiter); - // Prevent infinite loop because replaceTextRange causes a ModifyEvent - active = false; - textWidget.setRedraw(false); - try { - for (WrapEdit wrapEdit : wrapEdits) - textWidget.replaceTextRange(wrapEdit.getStart(), wrapEdit.getLength(), - wrapEdit.getReplacement()); - } finally { - textWidget.setRedraw(true); - active = true; + if (hardWrapSegmentListener == null) { + StyledText textWidget = getTextWidget(); + hardWrapSegmentListener = new BidiSegmentListener() { + public void lineGetSegments(BidiSegmentEvent e) { + int[] segments = calculateWrapOffsets(e.lineText, MAX_LINE_WIDTH); + if (segments != null) { + char[] segmentsChars = new char[segments.length]; + Arrays.fill(segmentsChars, '\n'); + e.segments = segments; + e.segmentsChars = segmentsChars; } } }; - textWidget.addModifyListener(hardWrapModifyListener); + textWidget.addBidiSegmentListener(hardWrapSegmentListener); + textWidget.setText(textWidget.getText()); // XXX: workaround for https://bugs.eclipse.org/384886 } - } else if (hardWrapModifyListener != null) { - getTextWidget().removeModifyListener(hardWrapModifyListener); - hardWrapModifyListener = null; + } else if (hardWrapSegmentListener != null) { + StyledText textWidget = getTextWidget(); + textWidget.removeBidiSegmentListener(hardWrapSegmentListener); + textWidget.setText(textWidget.getText()); // XXX: workaround for https://bugs.eclipse.org/384886 + hardWrapSegmentListener = null; } } @@ -739,20 +738,41 @@ public class SpellcheckableMessageArea extends Composite { } /** - * Return the commit message, converting platform-specific line endings. + * Returns the commit message, converting platform-specific line endings to '\n' + * and hard-wrapping lines if necessary. * * @return commit message */ public String getCommitMessage() { String text = getText(); - return Utils.normalizeLineEndings(text); + text = Utils.normalizeLineEndings(text); + if (shouldHardWrap()) + text = hardWrap(text); + return text; } /** - * Reconfigure this widget if a preference has changed. + * Hard-wraps the given text. + * + * @param text the text to wrap, must use '\n' as line delimiter + * @return the wrapped text */ - public void reconfigure() { - configureHardWrap(); + public static String hardWrap(String text) { + int[] wrapOffsets = calculateWrapOffsets(text, MAX_LINE_WIDTH); + if (wrapOffsets != null) { + StringBuilder builder = new StringBuilder(text.length() + wrapOffsets.length); + int prev = 0; + for (int cur : wrapOffsets) { + builder.append(text.substring(prev, cur)); + for (int j = cur; j > prev && builder.charAt(builder.length() - 1) == ' '; j--) + builder.deleteCharAt(builder.length() - 1); + builder.append('\n'); + prev = cur; + } + builder.append(text.substring(prev)); + return builder.toString(); + } + return text; } /** @@ -814,124 +834,62 @@ public class SpellcheckableMessageArea extends Composite { } /** - * Calculate a list of {@link WrapEdit} which can be applied to the text to - * get a new text that is wrapped at word boundaries. Existing line breaks - * are left alone (text is not reflowed). + * Calculates wrap offsets for the given line, so that resulting lines are + * no longer than <code>maxLineLength</code> if possible. * - * @param text - * the text to calculate the wrap edits for + * @param line + * the line to wrap (can contain '\n', but no other line delimiters) * @param maxLineLength * the maximum line length - * @param lineDelimiter - * line delimiter used in text and for wrapping - * @return a list of {@link WrapEdit} objects which specify how the text - * should be edited to obtain the wrapped text. Offsets of later - * edits are already adjusted for the fact that wrapping a line may - * shift the text backwards. So the list can just be iterated and - * each edit applied in order. + * @return an array of offsets where hard-wraps should be inserted, or + * <code>null</code> if the line does not need to be wrapped */ - public static List<WrapEdit> calculateWrapEdits(final String text, final int maxLineLength, final String lineDelimiter) { - List<WrapEdit> wrapEdits = new LinkedList<WrapEdit>(); - - final int lineDelimiterLength = lineDelimiter.length(); - final int spaceLength = 1; - - int offset = 0; - boolean lastChunkWasWrapped = false; - int lastChunkWrappedOffset = 0; - - String[] chunks = text.split(lineDelimiter, -1); - for (int chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { - String chunk = chunks[chunkIndex]; - - String[] words = chunk.split(" ", -1); //$NON-NLS-1$ - int lineLength = 0; - - for (int wordIndex = 0; wordIndex < words.length; wordIndex++) { - String word = words[wordIndex]; - int wordLength = word.length(); - boolean adjustForSpace = wordIndex != 0; - - if (wordIndex == 0 && lastChunkWasWrapped) { - if (wordLength != 0 && !STARTS_WITH_SYMBOL.matcher(word).matches()) { - wrapEdits.add(new WrapEdit(offset - lineDelimiterLength, lineDelimiterLength, " ")); //$NON-NLS-1$ - /* adjust for join edit above */ - offset -= lineDelimiterLength; - adjustForSpace = true; - lineLength = offset - lastChunkWrappedOffset; + public static int[] calculateWrapOffsets(final String line, final int maxLineLength) { + if (line.length() == 0) + return null; + + IntList wrapOffsets = new IntList(); + int wordStart = 0; + int lineStart = 0; + boolean lastWasSpace = true; + boolean onlySpaces = true; + for (int i = 0; i < line.length(); i++) { + char ch = line.charAt(i); + if (ch == ' ') { + lastWasSpace = true; + } else if (ch == '\n') { + lineStart = i + 1; + wordStart = i + 1; + lastWasSpace = true; + onlySpaces = true; + } else { // a word character + if (lastWasSpace) { + lastWasSpace = false; + if (!onlySpaces) { // don't break line with <spaces><veryLongWord> + wordStart = i; } - lastChunkWasWrapped = false; + } else { + onlySpaces = false; } - - int newLineLength = lineLength + spaceLength + wordLength; - if (newLineLength > maxLineLength) { - /* don't break before a single long word */ - if (lineLength != 0) { - wrapEdits.add(new WrapEdit(offset, spaceLength, lineDelimiter)); - /* adjust for the shifting of text after the edit is applied */ - offset += lineDelimiterLength; - adjustForSpace = false; - lastChunkWasWrapped = true; - lastChunkWrappedOffset = offset; + if (i >= lineStart + maxLineLength) { + if (wordStart != lineStart) { // don't break before a single long word + wrapOffsets.add(wordStart); + lineStart = wordStart; + onlySpaces = true; } - lineLength = 0; } - - if (adjustForSpace) { - offset += spaceLength; - lineLength += spaceLength; - } - - offset += wordLength; - lineLength += wordLength; } - - offset += lineDelimiterLength; - } - - return wrapEdits; - } - - /** - * Edit for replacing a text range with another text to wrap or join lines. - */ - public static class WrapEdit { - private final int start; - private final int length; - private final String replacement; - - /** - * @param start see {@link #getStart()} - * @param length see {@link #getLength()} - * @param replacement see {@link #getReplacement()} - */ - public WrapEdit(int start, int length, String replacement) { - this.start = start; - this.length = length; - this.replacement = replacement; - } - - /** - * @return character offset of where the edit should be applied on the - * text - */ - public int getStart() { - return start; - } - - /** - * @return number of characters which should be replaced by the - * replacement text - */ - public int getLength() { - return length; } - /** - * @return the text which replaces the edit range - */ - public String getReplacement() { - return replacement; + int size = wrapOffsets.size(); + if (size == 0) { + return null; + } else { + int[] result = new int[size]; + for (int i = 0; i < size; i++) { + result[i] = wrapOffsets.get(i); + } + return result; } } } |
