/** * Copyright (c) 2009, 2010 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.execute; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.Region; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.swt.widgets.Control; import org.eclipse.ui.texteditor.ITextEditor; import com.mulgasoft.emacsplus.EmacsPlusUtils; import com.mulgasoft.emacsplus.MarkUtils; /** * Assorted methods for manipulating rectangles from regular Eclipse selections * * @author Mark Feber - initial API and implementation */ public class RectangleSupport extends ColumnSupport { public RectangleSupport(IDocument document, Control widget) { super(document,widget); } public RectangleSupport(IDocument document, ITextEditor editor) { this(document,MarkUtils.getTextWidget(editor)); } ITextSelection getCurrentSelection(ITextEditor editor){ ISelectionProvider selectionProvider = editor.getSelectionProvider(); return (ITextSelection) selectionProvider.getSelection(); } /** * Convert the selection information to a RectangleInfo object with information about the * offsets, lengths and columns involved * * @param editor * @param document * @param selection * @return the RectangleInfo object */ public RectangleInfo getRectangleInfo(ITextEditor editor, IDocument document,ITextSelection selection) { RectangleInfo result = null; try { if (selection != null) { result = new RectangleInfo(); // get begin and end offsets int beginOff = selection.getOffset(); int endOff = beginOff + selection.getLength(); // get begin and end line information IRegion bl = document.getLineInformationOfOffset(beginOff); IRegion el = document.getLineInformationOfOffset(endOff); int blOff = bl.getOffset(); int elOff = el.getOffset(); // determine the start and end columns int startCol = getColumn(document, blOff, beginOff - blOff,Integer.MAX_VALUE).getLength(); int endCol = getColumn(document, elOff, endOff - elOff,Integer.MAX_VALUE).getLength(); if (endCol < startCol) { int tmp = endCol; endCol = startCol; startCol = tmp; } result.setStartColumn(startCol); result.setEndColumn(endCol); // for each line in the rectangle, determine the offset and length int bline = document.getLineOfOffset(blOff); int eline = document.getLineOfOffset(elOff); LineInfo[] lines = new LineInfo[eline+1-bline]; for (int i = 0; i < lines.length; i ++) { IRegion line = document.getLineInformation(bline + i); IRegion start = getColumn(document,line.getOffset(),line.getLength(),startCol); IRegion end = getColumn(document,line.getOffset(),line.getLength(),endCol); lines[i] = new LineInfo(start.getOffset(),(end.getOffset()-start.getOffset()),end.getLength()); } result.setLines(lines); } } catch (BadLocationException e) { result = null; } return result; } /** * Copy (and optionally delete) the rectangle information at selection * * @param editor * @param document * @param selection treated as a rectangle * @param remove if true, delete the rectangle from the document * @return the rectangle contents as String[] * @throws BadLocationException */ public String[] copyRectangle(ITextEditor editor, IDocument document,ITextSelection selection, boolean remove) throws BadLocationException { String[] result = null; RectangleInfo rect = getRectangleInfo(editor, document, selection); // fetch the text LineInfo[] lines = rect.getLines(); result = new String[lines.length]; for (int i=lines.length -1; i >= 0; i--) { LineInfo line = lines[i]; String text = document.get(line.getOffset(), line.getLength()); // augment with spaces if necessary if (line.getColumn() < rect.getEndColumn()) { text += getSpaces(line.getColumn(), rect.getEndColumn() - line.getColumn()); } result[i] = text; if (remove) { // delete each region document.replace(line.getOffset(), line.getLength(), EMPTY_STR); } } return result; } /** * Update the text in the rectangle * * @param editor * @param document * @param selection * @param updateStr the string to add/replace in rectangle or null to add/replace with spaces * @param replace if true, replace current chars, else move them right * @return the new cursor offset */ public int updateRectangle(ITextEditor editor, IDocument document,ITextSelection selection, String updateStr, boolean replace, boolean whitespace) { int result = EmacsPlusUtils.NO_OFFSET; // default position is cursor RectangleInfo rect = getRectangleInfo(editor, document, selection); if (rect != null) { Position coff = new Position(MarkUtils.getCursorOffset(editor),0); // use widget to avoid unpleasant scrolling side effects of IRewriteTarget Control widget = MarkUtils.getTextWidget(editor); try { widget.setRedraw(false); document.addPosition(coff); boolean spacing = (updateStr == null); IRegion[] regs = rect.getRegions(); // get # of columns of insertion int colLen = rect.getEndColumn() - rect.getStartColumn(); // do in reverse order, so offsets don't need to be tracked as doc length changes for (int i=regs.length -1 ; i >= 0; i--) { if (spacing) { if (whitespace) { clearInitialWhitespace(document,regs[i]); } else { // space adding/replacing insertSpaces(document, rect.getStartColumn(), regs[i].getOffset(), colLen, replace, !replace).length(); } } else { IRegion reg = regs[i]; if (updateStr.length() > 0){ // insert any necessary space to reach start column IRegion treg = getInsertPosition(document, reg.getOffset(), rect.getStartColumn(), true); // offset may have changed, so construct a region with old length reg = new Region(treg.getOffset(),reg.getLength()); } document.replace(reg.getOffset(), (replace ? reg.getLength() : 0), updateStr); } } // adjust cursor position depending on command if (spacing && replace) { // clear: cursor at beginning last line of selection rect = getRectangleInfo(editor,document,getCurrentSelection(editor)); if (rect != null) { regs = rect.getRegions(); result = regs[regs.length-1].getOffset(); } } else if (spacing || (MarkUtils.getMark(editor) == MarkUtils.getCursorOffset(editor))) { // open: cursor at top of selection // others: Eclipse puts cursor at mark when cursor was at origin of selection, so restore result = regs[0].getOffset(); } else if (replace) { result = coff.getOffset() - (updateStr == null ? 0 : updateStr.length()); } } catch (Exception e) {} finally { widget.setRedraw(true); document.removePosition(coff); } } return result; } /** * Insert a rectangle at the cursor offset * * @param editor * @param document * @param lines the contents of the rectangle * @return the offset at the end of the insertion * @throws BadLocationException */ public int insertRectangle(ITextEditor editor, IDocument document, String[] lines) throws BadLocationException { int coff = MarkUtils.getCursorOffset(editor); int result = coff; Position caret= null; if (lines != null) { int offset = -1; // flag initial insert position int columnPos = -1; int line = document.getLineOfOffset(coff); int numLines = document.getNumberOfLines()-1; IRegion reg = null; // determine column position columnPos = getInsertPosition(document,coff,-1, false).getLength(); if (line + lines.length > numLines) { // if at eof, add any necessary blank lines for (int i=0; i < (line+lines.length) - numLines; i++) { reg = document.getLineInformation(numLines + i); document.replace(reg.getOffset() + reg.getLength(), 0, getEol()); } } // insert lines backwards as a bug in Eclipse doesn't always update // line position information rapidly enough for (int i= lines.length -1; i >= 0; i--) { //get beginning offset of line offset = document.getLineOffset(line + i); reg = getInsertPosition(document, offset, columnPos, true); document.replace(reg.getOffset(), 0, lines[i]); if (caret == null) { caret= new Position(reg.getOffset() + lines[i].length(), 0); // the offset position will be updated by the internals on insertion document.addPosition(caret); } } if (caret != null) { document.removePosition(caret); result = caret.offset; } // set mark at origin of yanked rectangle MarkUtils.setMark(editor,coff); } return result; } /** * Clear initial whitespace in the rectangle * Emacs ignores the end column on each line and removes all initial whitespace on the line * * @param document * @param reg * @throws BadLocationException */ private void clearInitialWhitespace(IDocument document, IRegion reg) throws BadLocationException { int j=0; IRegion lineInfo = document.getLineInformationOfOffset(reg.getOffset()); for ( ; j < lineInfo.getLength() - (reg.getOffset() - lineInfo.getOffset()); j++) { if (document.get(reg.getOffset()+j,1).charAt(0) > ' ') { break; } } if (j > 0) { document.replace(reg.getOffset(), j, EMPTY_STR); } } // Supporting classes private class LineInfo { int offset; int length; int column; /** * @param offset * @param length * @param column */ public LineInfo(int offset, int length, int column) { this.offset = offset; this.length = length; this.column = column; } public int getOffset() { return offset; } public int getLength() { return length; } public int getColumn() { return column; } } public class RectangleInfo { int startColumn; int endColumn; LineInfo[] lines; public int getStartColumn() { return startColumn; } public void setStartColumn(int startColumn) { this.startColumn = startColumn; } public int getEndColumn() { return endColumn; } public void setEndColumn(int endColumn) { this.endColumn = endColumn; } public IRegion[] getRegions() { IRegion[] result = new Region[lines.length]; for (int i = 0; i < lines.length; i++) { result[i] = new Region(lines[i].getOffset(), lines[i].getLength()); } return result; } public void setRegions(IRegion[] regions) { lines = new LineInfo[regions.length]; for (int i=0; i < regions.length; i++) { lines[i] = new LineInfo(regions[i].getOffset(),regions[i].getLength(),endColumn); } } public void setLines(LineInfo[] lines) { this.lines = lines; } public LineInfo[] getLines() { return lines; } } }