/**
* Copyright (c) 2009, 2013 Mark Feber, MulgaSoft
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.mulgasoft.emacsplus.commands;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.IRewriteTarget;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.Position;
import org.eclipse.swt.widgets.Control;
import org.eclipse.ui.texteditor.ITextEditor;
import com.mulgasoft.emacsplus.EmacsPlusActivator;
import com.mulgasoft.emacsplus.execute.ColumnSupport;
import com.mulgasoft.emacsplus.minibuffer.AlignMinibuffer;
import com.mulgasoft.emacsplus.minibuffer.AlignMinibuffer.AlignControl;
/**
* Align selection based on a regular expression
*
* Align the current region using an ad-hoc rule read from the minibuffer.
* The selection marks the limits of the region. This function will prompt
* for the regexp to align with. If no prefix arg was specified, you
* only need to supply the characters to be lined up and any preceding
* whitespace is replaced. If a prefix arg was specified, the full
* regexp with parenthesized whitespace should be supplied; it will also
* prompt for which parenthesis group within regexp to modify, the amount
* of spacing to use, and whether or not to repeat the rule throughout
* the line.
*
* @author Mark Feber - initial API and implementation
*/
public class AlignRegexpHandler extends MinibufferExecHandler {
private static final String ALIGN_PREFIX = EmacsPlusActivator.getResourceString("Align_Regexp"); //$NON-NLS-1$
/**
* @see com.mulgasoft.emacsplus.commands.MinibufferHandler#getMinibufferPrefix()
*/
public String getMinibufferPrefix() {
return ALIGN_PREFIX;
}
/**
* @see com.mulgasoft.emacsplus.commands.EmacsPlusCmdHandler#transform(ITextEditor, IDocument, ITextSelection, ExecutionEvent)
*/
@Override
protected int transform(ITextEditor editor, IDocument document, ITextSelection currentSelection, ExecutionEvent event)
throws BadLocationException {
return bufferTransform(new AlignMinibuffer(this, isUniversalPresent()), editor, event);
}
/**
* ^U specifies complex version of command
*
* @see com.mulgasoft.emacsplus.commands.EmacsPlusCmdHandler#isLooping()
*/
@Override
protected boolean isLooping() {
return false;
}
/**
* Get the column position of the line at numChars from start
*
* @param cs
* @param document
* @param start
* @param numChars
* @return the column position of (start+numChars)
*/
private int getColumnPosition(ColumnSupport cs, IDocument document, int start, int numChars) {
return cs.getColumn(document, start, numChars, Integer.MAX_VALUE).getLength();
}
private class Alignment {
public Alignment(int line, int start) {
l_number = line;
t_start = start;
}
int l_number; // line number in the document
int l_start; // line offset in the document
int t_start; // line (or segment) text start
int m_start; // match start
int m_len; // match length
int g_start; // (whitespace) group start
int g_column; // (whitespace) group start column
int g_len; // (whitespace) group length
int r_adjust; // adjust on repeat
int j_len; // justify length when group < 0
}
protected boolean doExecuteResult(ITextEditor editor, Object minibufferResult) {
AlignControl ac = (AlignControl)minibufferResult;
ITextSelection selection = getCurrentSelection(editor);
IDocument doc = getThisDocument();
IRewriteTarget rt = (IRewriteTarget) editor.getAdapter(IRewriteTarget.class);
Control widget = getTextWidget(editor);
int startLine = selection.getStartLine();
int endLine = selection.getEndLine();
int offset = selection.getOffset();
int endOffset = selection.getLength() + offset;
// remember cursor offset through text changes
Position coff = new Position(getCursorOffset(editor),0);
try {
doc.addPosition(coff);
Pattern pc = checkPattern(ac.getPattern(), editor);
if (pc == null) {
return true;
}
// use widget to avoid unpleasant scrolling side effects of IRewriteTarget
widget.setRedraw(false);
if (rt != null) {
rt.beginCompoundChange();
}
List<Alignment> alignments = new ArrayList<Alignment>();
ColumnSupport cs = new ColumnSupport(doc,editor);
// for (int i=0,l=startLine; l <=endLine; l++,i++) {
for (int l=endLine; l >= startLine; l--) {
IRegion lineInfo = doc.getLineInformation(l);
int l_offset = lineInfo.getOffset();
int s = l_offset;
int e = s + lineInfo.getLength();
if (s < offset) {
s = offset;
}
if (e > endOffset) {
e = endOffset;
}
Alignment a = null;
// match the line with the regexp, ignore any lines with no match
if ((a = getLineMatch(l,s,e,ac.getGroup(),pc,editor)) != null) {
alignments.add(a);
a.l_start = l_offset;
// determine the column on each line
a.g_column = getColumnPosition(cs, doc, l_offset, a.g_start - l_offset);
// the farthest group column is where replacement will start
if (a.g_column > ac.getMaxColumn()) {
ac.setMaxColumn(a.g_column);
}
}
}
while (!alignments.isEmpty()) {
alignments = alignmentReplace(alignments, ac, cs, pc, editor);
}
// and position point at correct cursor offset
selectAndReveal(editor, coff.offset, coff.offset);
} catch (BadLocationException e) {
// Shouldn't happen
e.printStackTrace();
}
finally {
widget.setRedraw(true);
doc.removePosition(coff);
if (rt != null) {
rt.endCompoundChange();
}
}
// and signal for cleanup
return true;
}
/**
* Replace the group segment with the computed spaces.
* On repeat, pre-populate the next matched segment information
*
* @param alignments
* @param ac control parameters for the alignment
* @param cs utility class for column computation
* @param pc validate pattern
* @param editor
* @return the list of next Alignment segments on repeat, or the empty list on no repeat
* @throws BadLocationException
*/
private List<Alignment> alignmentReplace(List<Alignment> alignments, AlignControl ac, ColumnSupport cs, Pattern pc, ITextEditor editor) throws BadLocationException {
List<Alignment> nextalignments = new ArrayList<Alignment>();
IDocument doc = getThisDocument();
int spacing = ac.getSpacing();
int maxColumn = ac.getMaxColumn();
ac.setMaxColumn(-1); // and reset
for (Alignment a : alignments) {
int spaceColumns;
// if valid absolute column position, use it
if (spacing < 0 && (spaceColumns = -spacing - a.g_column) >= 0) {
;
} else {
spaceColumns = (maxColumn - a.g_column) + (spacing <0 ? 1 : spacing);
}
// column overruns are replaced by a single space
String spaces = cs.getSpaces(a.g_column, (spaceColumns < 0 ? 1 : spaceColumns));
// if group < 0 then replace from g_start through initial whitespace only
int replLen = ((ac.getGroup() < 0) ? a.j_len : a.g_len);
doc.replace(a.g_start, replLen, spaces);
// after replacement compute the number of chars from beginning to current match end
int change = (spaces.length() - a.g_len);
int matchStartOffsetLen = (a.g_start + a.g_len) - a.l_start;
int matchSize = a.m_len;
a.r_adjust = (change + matchSize) + matchStartOffsetLen;
}
if (ac.getRepeat()) {
for (Alignment a : alignments) {
IRegion info = doc.getLineInformation(a.l_number);
int l_offset = info.getOffset();
Alignment nextMatch = null;
// the next match starts from the end of the previous match (r_adjust)
if ((nextMatch = getLineMatch(a.l_number, l_offset + a.r_adjust,
l_offset + info.getLength(), ac.getGroup(), pc, editor)) != null) {
nextalignments.add(nextMatch);
nextMatch.l_start = l_offset;
// determine the column on each line
nextMatch.g_column = getColumnPosition(cs, doc, l_offset, nextMatch.g_start
- l_offset);
// the farthest group column is where replacement will start
if (nextMatch.g_column > ac.getMaxColumn()) {
ac.setMaxColumn(nextMatch.g_column);
}
}
}
}
return nextalignments;
}
/**
* Match the document line against the regular expression. Populates the
* Alignment class with the group and pattern match locations
*
* @param line the line number in the document
* @param start the start offset in the line
* @param end the endo offset in the line
* @param group the group to modify within the regexp
* @param pc validated regexp Pattern
* @param editor
* @return the populated Alignment class or null if no match
*/
private Alignment getLineMatch(int line, int start, int end, int group, Pattern pc, ITextEditor editor){
Alignment alignment = null;
try {
String input = getThisDocument().get(start,end - start);
int group_start, group_end;
boolean justify = group < 0;
group = (justify ? -group : group);
if (input != null && input.length() > 0) {
Matcher m = pc.matcher(input);
if (m.find() && group <= m.groupCount()) {
group_start = m.start(group);
group_end = m.end(group);
if (!(group_start < 0)) { // matcher returns -1 if group didn't match
Alignment a = new Alignment(line, start);
a.g_start = group_start + a.t_start;
a.m_start = group_end + a.t_start;
a.m_len = m.end() - group_end;
a.g_len = a.m_start - a.g_start;
alignment = (a.m_len == 0 ? null : a); // disallow group only matches
// manually determine initial spaces in group when justifying
if (alignment != null && justify) {
int j_len = 0;
for (int i = group_start; i < group_end - group_start; i++) {
if (input.charAt(i) <= ' ') {
j_len = i;
} else {
break;
}
}
alignment.j_len = j_len;
}
}
}
}
} catch (BadLocationException e) {
// shouldn't happen
e.printStackTrace();
}
return alignment;
}
/**
* Perform a 'compilation' check for the regexp
*
* @param regexp the String regular expression
* @param editor used for error message if compilation fails
*
* @return Pattern if ok, else null
*/
private Pattern checkPattern(String regexp, ITextEditor editor) {
Pattern result = null;
try {
result = Pattern.compile(regexp);
} catch (PatternSyntaxException e) {
String msg = e.getLocalizedMessage().replaceAll("[\r\n]"," "); //$NON-NLS-1$ //$NON-NLS-2$
// check for and remove useless (in this context) pointer
if (msg.charAt(msg.length()-1) == '^') {
msg = msg.substring(0, msg.length()-2);
}
showResultMessage(editor, msg, true);
}
return result;
}
}