blob: f48263f1063af6556a181da62ff7849d797f5c6b [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2007, 2021 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.ecommons.text;
import java.util.Arrays;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.osgi.util.NLS;
import org.eclipse.statet.jcommons.text.core.TextRegion;
import org.eclipse.statet.ecommons.text.IIndentSettings.IndentationType;
/**
* Util to compute and edit line indentations
*/
public class IndentUtil {
public static final int COLUMN_IDX= 0;
public static final int OFFSET_IDX= 1;
public static final char[] repeat(final char c, final int n) {
final char[] chars= new char[n];
Arrays.fill(chars, c);
return chars;
}
public interface ILineIndent extends TextRegion {
int getIndentOffset();
int getIndentColumn();
boolean isBlank();
}
private static abstract class LineIndent implements ILineIndent {
private static final class Blank extends LineIndent {
public Blank(final int lineOffset, final int indentOffset, final int indentColumn) {
super(lineOffset, indentOffset, indentColumn);
}
@Override
public boolean isBlank() {
return true;
}
}
private static final class NonBlank extends LineIndent {
public NonBlank(final int lineOffset, final int indentOffset, final int indentColumn) {
super(lineOffset, indentOffset, indentColumn);
}
@Override
public boolean isBlank() {
return false;
}
}
private final int lineOffset;
private final int indentOffset;
private final int indentColumn;
public LineIndent(final int lineOffset, final int indentOffset, final int indentColumn) {
this.lineOffset= lineOffset;
this.indentOffset= indentOffset;
this.indentColumn= indentColumn;
}
@Override
public int getStartOffset() {
return this.lineOffset;
}
@Override
public int getEndOffset() {
return this.indentOffset;
}
@Override
public int getLength() {
return this.indentOffset - this.lineOffset;
}
@Override
public int getIndentOffset() {
return this.indentOffset;
}
@Override
public int getIndentColumn() {
return this.indentColumn;
}
@Override
public abstract boolean isBlank();
}
public static abstract class IndentEditAction {
private int indentColumn;
public IndentEditAction() {
}
public IndentEditAction(final int indentColumn) {
this.indentColumn= indentColumn;
}
public int getIndentColumn(final int line, final int lineOffset)
throws BadLocationException {
return this.indentColumn;
}
public abstract void doEdit(int line, int lineOffset, int length, StringBuilder text)
throws BadLocationException;
}
private static interface EditStrategy {
public void editInIndent(int firstLine, int lastLine, IndentEditAction action)
throws BadLocationException;
public void changeIndent(final int firstLine, final int lastLine, IndentEditAction action)
throws BadLocationException;
public String copyLineIndent(int line)
throws BadLocationException;
}
private class ConserveStrategy implements EditStrategy {
@Override
public void editInIndent(final int firstLine, final int lastLine, final IndentEditAction action)
throws BadLocationException {
final StringBuilder replacement= new StringBuilder(20);
ITER_LINES : for (int line= firstLine; line <= lastLine; line++) {
final IRegion lineInfo= IndentUtil.this.document.getLineInformation(line);
final int indentColumn= action.getIndentColumn(line, lineInfo.getOffset());
if (indentColumn < 0) {
continue ITER_LINES;
}
if (indentColumn > 0) {
replacement.setLength(0);
int indentation= 0;
int offset= lineInfo.getOffset();
boolean changed= false;
ITER_CHARS : while (indentation < indentColumn) {
final int c= getDocumentChar(offset);
int tabStart, tabEnd, spaceCount;
switch (c) {
case ' ':
indentation++;
offset++;
replacement.append(' ');
continue ITER_CHARS;
case '\t':
tabStart= (indentation/IndentUtil.this.tabWidth) * IndentUtil.this.tabWidth;
tabEnd= tabStart + IndentUtil.this.tabWidth;
if (tabEnd > indentColumn) {
spaceCount= tabEnd - indentation;
replacement.append(repeat(' ', spaceCount));
changed= true;
}
else {
replacement.append('\t');
}
indentation= tabEnd;
offset++;
continue ITER_CHARS;
case '\r':
case '\n':
case -1:
tabStart= (indentation/IndentUtil.this.tabWidth) * IndentUtil.this.tabWidth;
tabEnd= tabStart + IndentUtil.this.tabWidth;
if (IndentUtil.this.tabAsDefault && (tabEnd <= indentColumn)) {
spaceCount= indentation-tabStart;
replacement.delete(replacement.length()-spaceCount, replacement.length());
replacement.append('\t');
indentation= tabEnd;
changed= true;
}
else {
spaceCount= indentColumn-indentation;
replacement.append(repeat(' ', spaceCount));
indentation+= spaceCount;
changed= true;
}
continue ITER_CHARS;
default:
throw new IllegalArgumentException(createNoIndentationCharMessage(c));
}
}
if (changed) {
action.doEdit(line, lineInfo.getOffset(), offset-lineInfo.getOffset(), replacement);
continue ITER_LINES;
}
}
action.doEdit(line, lineInfo.getOffset(), 0, null);
continue ITER_LINES;
}
}
@Override
public void changeIndent(final int firstLine, final int lastLine, final IndentEditAction action)
throws BadLocationException {
final StringBuilder replacement= new StringBuilder(20);
ITER_LINES : for (int line= firstLine; line <= lastLine; line++) {
final IRegion lineInfo= IndentUtil.this.document.getLineInformation(line);
final int indentColumn= action.getIndentColumn(line, lineInfo.getOffset());
if (indentColumn < 0) {
continue ITER_LINES;
}
replacement.setLength(0);
int column= 0;
int offset= lineInfo.getOffset();
ITER_CHARS : while (column < indentColumn) {
final int c= getDocumentChar(offset);
int tabStart, tabEnd, spaceCount;
switch (c) {
case ' ':
column++;
offset++;
replacement.append(' ');
continue ITER_CHARS;
case '\t':
tabStart= (column/IndentUtil.this.tabWidth) * IndentUtil.this.tabWidth;
tabEnd= tabStart + IndentUtil.this.tabWidth;
if (tabEnd > indentColumn) {
spaceCount= indentColumn - column;
replacement.append(repeat(' ', spaceCount));
column= indentColumn;
}
else {
replacement.append('\t');
column= tabEnd;
}
offset++;
continue ITER_CHARS;
default:
break ITER_CHARS;
}
}
ITER_CHARS : while (true) {
final int c= getDocumentChar(offset);
if (c != ' ' && c != '\t') {
break ITER_CHARS;
}
offset++;
}
if (column < indentColumn) {
appendIndent(replacement, column, indentColumn);
}
action.doEdit(line, lineInfo.getOffset(), offset-lineInfo.getOffset(), replacement);
continue ITER_LINES;
}
}
@Override
public String copyLineIndent(final int line) throws BadLocationException {
final IRegion lineInfo= IndentUtil.this.document.getLineInformation(line);
int offset= lineInfo.getOffset();
ITERATE_CHAR : while (true) {
final int c= getDocumentChar(offset++);
switch (c) {
case ' ':
continue ITERATE_CHAR;
case '\t':
continue ITERATE_CHAR;
default:
--offset;
break ITERATE_CHAR;
}
}
return IndentUtil.this.document.get(lineInfo.getOffset(), offset-lineInfo.getOffset());
}
}
private class CorrectStrategy implements EditStrategy {
@Override
public void editInIndent(final int firstLine, final int lastLine, final IndentEditAction action)
throws BadLocationException {
final StringBuilder replacement= new StringBuilder(20);
ITER_LINES : for (int line= firstLine; line <= lastLine; line++) {
final int lineOffset= IndentUtil.this.document.getLineOffset(line);
final int indentColumn= action.getIndentColumn(line, lineOffset);
if (indentColumn < 0) {
continue ITER_LINES;
}
final int[] current= getLineIndent(line, true);
replacement.setLength(0);
appendIndent(replacement, indentColumn);
if (current[COLUMN_IDX] >= 0) {
appendSpaces(replacement, current[COLUMN_IDX]-indentColumn);
}
action.doEdit(line, lineOffset, current[OFFSET_IDX]-lineOffset, replacement);
continue ITER_LINES;
}
}
@Override
public void changeIndent(final int firstLine, final int lastLine, final IndentEditAction action)
throws BadLocationException {
final StringBuilder replacement= new StringBuilder(20);
ITER_LINES : for (int line= firstLine; line <= lastLine; line++) {
final int lineOffset= IndentUtil.this.document.getLineOffset(line);
final int indentColumn= action.getIndentColumn(line, lineOffset);
if (indentColumn < 0) {
continue ITER_LINES;
}
final int[] current= getLineIndent(line, false);
replacement.setLength(0);
appendIndent(replacement, indentColumn);
action.doEdit(line, lineOffset, current[OFFSET_IDX]-lineOffset, replacement);
continue ITER_LINES;
}
}
@Override
public String copyLineIndent(final int line) throws BadLocationException {
final IRegion lineInfo= IndentUtil.this.document.getLineInformation(line);
int column= 0;
int offset= lineInfo.getOffset();
ITERATE_CHAR : while (true) {
final int c= getDocumentChar(offset++);
switch (c) {
case ' ':
column++;
continue ITERATE_CHAR;
case '\t':
column+= IndentUtil.this.tabWidth - (column % IndentUtil.this.tabWidth);
continue ITERATE_CHAR;
default:
break ITERATE_CHAR;
}
}
return createIndentString(column);
}
}
private final IDocument document;
private final int tabWidth;
private final boolean tabAsDefault;
private final int numOfSpaces;
private final EditStrategy editStrategy;
// public IndentUtil(final IDocument document, final booeditStrategy,
// final boolean tabsAsDefault, final int tabWidth, final int numOfSpaces) {
// document= document;
// switch (editStrategy) {
// case CONSERVE_STRATEGY:
// fConservative= new ConserveStrategy();
// break;
// case CORRECT_STRATEGY:
// fConservative= new CorrectStrategy();
// break;
// }
// fTabAsDefault= tabsAsDefault;
// fTabWidth= tabWidth;
// fNumOfSpaces= numOfSpaces;
// }
//
public IndentUtil(final IDocument document, final IIndentSettings settings) {
this.document= document;
this.editStrategy= (settings.getReplaceConservative()) ?
new ConserveStrategy() : new CorrectStrategy();
this.tabAsDefault= (settings.getIndentDefaultType() == IndentationType.TAB);
this.tabWidth= settings.getTabSize();
this.numOfSpaces= settings.getIndentSpacesCount();
}
public final IDocument getDocument() {
return this.document;
}
public final int getTabWidth() {
return this.tabWidth;
}
/**
* Return the indentation indentColumn of the specified line.
*
* @param line line to check
* @param markBlankLine if true, empty lines have are marked with a indentColumn of -1
* @return column and offset of line indent
* @throws BadLocationException
*/
@Deprecated
public int[] getLineIndent(final int line, final boolean markBlankLine) throws BadLocationException {
final IRegion lineInfo= this.document.getLineInformation(line);
int column= 0;
int offset= lineInfo.getOffset();
ITERATE_CHAR : while (true) {
final int c= getDocumentChar(offset++);
switch (c) {
case ' ':
column++;
continue ITERATE_CHAR;
case '\t':
column+= this.tabWidth - (column % this.tabWidth);
continue ITERATE_CHAR;
case '\r':
case '\n':
case -1:
if (markBlankLine) {
return new int[] { -1, --offset };
}
//$FALL-THROUGH$
default:
return new int[] { column, --offset };
}
}
}
/**
* Creates a line indentation string with same depth as the given line.
* The exact string depends on the configured strategy.
* @param line the line number
* @return line indentation string
* @throws BadLocationException
*/
public String copyLineIndent(final int line) throws BadLocationException {
return this.editStrategy.copyLineIndent(line);
}
// public int getIndentationindentColumn(String chars) throws BadLocationException {
//
// int indentation= 0;
// ITER_CHARS : for (int i= 0; i < chars.length(); i++) {
// char c= document.getChar(i);
// switch (c) {
// case ' ':
// indentation++;
// continue ITER_CHARS;
// case '\t':
// indentation+= fTabWidth;
// continue ITER_CHARS;
// default:
// throw new IllegalArgumentException("No indentation char: '"+c+"'."); //$NON-NLS-1$ //$NON-NLS-2$
// }
// }
// return indentation;
// }
/**
* Returns the common (min) indentation indentColumn of all lines. Empty lines are ignored.
* @param startLine line index of first line
* @param endLine line index of last line
* @return
* @throws BadLocationException
*/
public int getMultilineIndentColumn(final int startLine, final int endLine) throws BadLocationException {
int indentation= Integer.MAX_VALUE;
for (int line= startLine; line <= endLine; line++) {
final int[] lineIndent= getLineIndent(line, true);
if (lineIndent[COLUMN_IDX] >= 0) {
indentation= Math.min(indentation, lineIndent[COLUMN_IDX]);
}
}
if (indentation == Integer.MAX_VALUE) {
indentation= 0;
}
return indentation;
}
/**
* Prepares the indentation of the line, so you can insert text at the
* given indentation indentColumn.
*
* @param line line index
* @return the returned object of your action
* @throws BadLocationException
*/
public void editInIndent(final int firstLine, final int lastLine, final IndentEditAction action)
throws BadLocationException {
this.editStrategy.editInIndent(firstLine, lastLine, action);
}
public void changeIndent(final int firstLine, final int lastLine, final IndentEditAction action)
throws BadLocationException {
this.editStrategy.changeIndent(firstLine, lastLine, action);
}
/**
* Returns the indentation of the specified line.
*
* @param line line to check
* @return the line indent
*/
public final ILineIndent getIndent(final CharSequence line) {
int offset= 0;
int column= 0;
ITER_CHARS : for (; offset < line.length(); offset++) {
final char c= line.charAt(offset);
switch (c) {
case ' ':
column++;
continue ITER_CHARS;
case '\t':
column+= this.tabWidth - (column % this.tabWidth);
continue ITER_CHARS;
case '\r':
case '\n':
break ITER_CHARS;
default:
return new LineIndent.NonBlank(0, offset, column);
}
}
return new LineIndent.Blank(0, offset, column);
}
/**
* Returns the column of the specified offset.
*
* Linebreak are not specially handled.
*
* @param offset index in string
* @return char column
*/
public final int getColumn(final CharSequence line, final int offset) {
int checkOffset= 0;
int column= 0;
ITER_CHARS : while (checkOffset < offset) {
switch (line.charAt(checkOffset++)) {
case '\t':
column+= this.tabWidth - (column % this.tabWidth);
continue ITER_CHARS;
default:
column++;
continue ITER_CHARS;
}
}
return column;
}
/**
* Returns the column of the specified offset.
*
* Linebreak are not specially handled.
*
* @param line text
* @param offset index in text
* @param startColumn column of the text
* @return char column
*/
public final int getColumn(final CharSequence line, final int offset, final int startColumn) {
int checkOffset= 0;
int column= startColumn;
ITER_CHARS : while (checkOffset < offset) {
switch (line.charAt(checkOffset++)) {
case '\t':
column+= this.tabWidth - (column % this.tabWidth);
continue ITER_CHARS;
default:
column++;
continue ITER_CHARS;
}
}
return column;
}
/**
* Returns the char offset in indentation with column ≥ the specified column.
*
* @param line text to check
* @param column indentColumn to search for
* @return the offset
*/
public final int getIndentedOffsetAt(final CharSequence line, final int column) {
int currentOffset= 0;
int currentColumn= 0;
ITER_CHARS : for (; currentOffset < line.length() && currentColumn < column; currentOffset++) {
final char c= line.charAt(currentOffset);
switch (c) {
case ' ':
currentColumn++;
continue ITER_CHARS;
case '\t':
currentColumn+= this.tabWidth - (currentColumn % this.tabWidth);
continue ITER_CHARS;
default:
throw new IllegalArgumentException(createNoIndentationCharMessage(c));
}
}
return currentOffset;
}
/**
* Returns the indentation of the specified line.
*
* @param lineNum the index of the line to check
* @return the line indent
* @throws BadLocationException
*/
public final ILineIndent getIndent(final int lineNum) throws BadLocationException {
int offset= this.document.getLineOffset(lineNum);
int column= 0;
final int bound= this.document.getLength();
ITER_CHARS : for (; offset < bound; offset++) {
final char c= this.document.getChar(offset);
switch (c) {
case ' ':
column++;
continue ITER_CHARS;
case '\t':
column+= this.tabWidth - (column % this.tabWidth);
continue ITER_CHARS;
case '\r':
case '\n':
break ITER_CHARS;
default:
return new LineIndent.NonBlank(0, offset, column);
}
}
return new LineIndent.Blank(0, offset, column);
}
/**
* Returns the column of the specified offset.
*
* Linebreaks are not specially handled.
*
* @param offset offset in document
* @return the column
* @throws BadLocationException
*/
public final int getColumn(final int offset) throws BadLocationException {
return getColumn(this.document.getLineOfOffset(offset), offset);
}
/**
* Returns the column of the specified offset.
*
* Linebreaks are not specially handled.
*
* @param lineNum the index of the line to check
* @param offset the offset in document
* @return the column
* @throws BadLocationException
*/
public final int getColumn(final int lineNum, final int offset) throws BadLocationException {
int checkOffset= this.document.getLineOffset(lineNum);
int column= 0;
ITER_CHARS : while (checkOffset < offset) {
switch (this.document.getChar(checkOffset++)) {
case '\t':
column+= this.tabWidth - (column % this.tabWidth);
continue ITER_CHARS;
default:
column++;
continue ITER_CHARS;
}
}
return column;
}
/**
* Returns the document offset in indentation with column ≥ the specified column.
*
* @param lineNum the index of the line to check
* @param column the column
* @return the offset of the column
* @throws BadLocationException
* @throws IllegalArgumentException
*/
public final int getIndentedOffsetAt(final int lineNum, final int column)
throws BadLocationException {
final IRegion lineInfo= this.document.getLineInformation(lineNum);
int currentOffset= lineInfo.getOffset();
int currentColumn= 0;
ITER_CHARS : while (currentColumn < column) {
final char c= this.document.getChar(currentOffset++);
switch (c) {
case ' ':
currentColumn++;
continue ITER_CHARS;
case '\t':
currentColumn+= this.tabWidth - (currentColumn % this.tabWidth);
continue ITER_CHARS;
default:
throw new IllegalArgumentException(createNoIndentationCharMessage(c));
}
}
return currentOffset;
}
/**
* Returns the last document offset with column ≤ the specified column.
*
* @param lineNum the index of the line to check
* @param column the column
* @return the offset of the column or -1 if line is shorter
* @throws BadLocationException
*/
public final int getOffsetAtMax(final int lineNum, final int column)
throws BadLocationException {
final IRegion lineInfo= this.document.getLineInformation(lineNum);
int offset= lineInfo.getOffset();
final int bound= lineInfo.getLength();
int current= 0;
ITER_CHARS : while (current < column) {
if (offset >= bound) {
return -1;
}
switch (this.document.getChar(offset++)) {
case '\t':
current+= this.tabWidth - (current % this.tabWidth);
continue ITER_CHARS;
default:
current++;
continue ITER_CHARS;
}
}
return (current > column) ? offset - 1 : offset;
}
/**
* Returns the configured width of a default indentation.
*
* @return number of visual char columns
*/
public int getLevelColumns() {
if (this.tabAsDefault) {
return this.tabWidth;
}
else {
return this.numOfSpaces;
}
}
/**
* Computes the indentation column adding the specified levels to the current indentColumn.
*
* @param currentColumn indentColumn in visual char columns
* @param levels number of indentation levels
* @return indentColumn in visual char columns
*/
public int getNextLevelColumn(final int currentColumn, final int levels) {
final int columns= getLevelColumns();
return ((currentColumn / columns + levels) * columns);
}
/**
* Creates a string for indentation of specified indentColumn (respects the preferences).
* @param indentColumn
* @return
*/
public String createIndentString(final int indentColumn) {
if (this.tabAsDefault) {
return new StringBuilder(indentColumn)
.append(repeat('\t', indentColumn / this.tabWidth))
.append(repeat(' ', indentColumn % this.tabWidth))
.toString();
}
else {
return new String(repeat(' ', indentColumn));
}
}
public final String createIndentCompletionString(final int currentColumn) {
if (this.tabAsDefault) {
return "\t"; //$NON-NLS-1$
}
else {
final int rest= currentColumn % this.numOfSpaces;
return new String(repeat(' ', this.numOfSpaces - rest));
}
}
public final String createTabCompletionString(final int currentColumn) {
if (this.tabAsDefault) {
return "\t"; //$NON-NLS-1$
}
else {
return createTabSpacesCompletionString(currentColumn);
}
}
public final String createTabSpacesCompletionString(final int currentColumn) {
final int rest= currentColumn % this.tabWidth;
return new String(repeat(' ', this.tabWidth - rest));
}
protected final int getDocumentChar(final int idx) throws BadLocationException {
if (idx >= 0 && idx < this.document.getLength()) {
return this.document.getChar(idx);
}
if (idx == -1 || idx == this.document.getLength()) {
return -1;
}
throw new BadLocationException();
}
public final void appendIndent(final StringBuilder sb, final int indentColumn) {
if (this.tabAsDefault) {
sb.append(repeat('\t', indentColumn / this.tabWidth));
sb.append(repeat(' ', indentColumn % this.tabWidth));
}
else {
sb.append(repeat(' ', indentColumn));
}
}
public final void appendIndent(final StringBuilder sb, final int currentColumn, final int indentColumn) {
if (this.tabAsDefault) {
final int tabDiff= (indentColumn / this.tabWidth) - (currentColumn / this.tabWidth) ;
if (tabDiff > 0) {
final int spaces= currentColumn % this.tabWidth;
if (spaces > 0) {
sb.append(repeat(' ', this.tabWidth - spaces));
sb.append(repeat('\t', tabDiff - 1));
}
else {
sb.append(repeat('\t', tabDiff));
}
sb.append(repeat(' ', indentColumn % this.tabWidth));
}
else {
sb.append(repeat(' ', indentColumn - currentColumn));
}
}
else {
sb.append(repeat(' ', indentColumn));
}
}
// protected final void appendIndentCompletion(final StringBuilder s, final int currentColumn) {
// if (fTabAsDefault) {
// s.append('\t');
// }
// else {
// s.append(repeat(' ', fNumOfSpaces-(currentColumn % fNumOfSpaces)));
// }
// }
protected final void appendSpaces(final StringBuilder s, final int num) {
s.append(repeat(' ', num));
}
private String createNoIndentationCharMessage(final int c) {
return NLS.bind("No indentation char: ''{0}''.", ((char)c)); //$NON-NLS-1$
}
}