/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jkiss.dbeaver.ui.editors.sql.indent;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
public class SQLIndenter {
/**
* The document being scanned.
*/
private IDocument document;
/**
* The indentation accumulated by <code>findPreviousIndenationUnit</code>.
*/
private int indent;
/**
* The stateful scanposition for the indentation methods.
*/
private int position;
/**
* The most recent token.
*/
private int token;
/**
* The line of <code>fPosition</code>.
*/
private int line;
/**
* The scanner we will use to scan the document. It has to be installed on the same document as the one we get.
*/
private SQLHeuristicScanner scanner;
/**
* Creates a new instance.
*
* @param document the document to scan
* @param scanner the {@link SQLHeuristicScanner}to be used for scanning the document. It must be installed on the
* same <code>IDocument</code>.
*/
public SQLIndenter(IDocument document, SQLHeuristicScanner scanner)
{
assert (document != null);
assert (scanner != null);
this.document = document;
this.scanner = scanner;
}
/**
* Computes the indentation at the reference point of <code>position</code>.
*
* @param offset the offset in the document
* @return a String which reflects the indentation at the line in which the reference position to
* <code>offset</code> resides, or <code>null</code> if it cannot be determined
*/
public String getReferenceIndentation(int offset)
{
int unit;
unit = findReferencePosition(offset);
// if we were unable to find anything, return null
if (unit == SQLHeuristicScanner.NOT_FOUND) {
return null;
}
return getLeadingWhitespace(unit);
}
/**
* Computes the indentation at <code>offset</code>.
*
* @param offset the offset in the document
* @return a String which reflects the correct indentation for the line in which offset resides, or
* <code>null</code> if it cannot be determined
*/
public String computeIndentation(int offset)
{
return computeIndentation(offset, false);
}
/**
* Computes the indentation at <code>offset</code>.
*
* @param offset the offset in the document
* @param assumeOpening <code>true</code> if an opening statement should be assumed
* @return a String which reflects the correct indentation for the line in which offset resides, or
* <code>null</code> if it cannot be determined
*/
public String computeIndentation(int offset, boolean assumeOpening)
{
indent = 1;
// add additional indent
StringBuilder indent = createIndent(this.indent);
if (this.indent < 0) {
unindent(indent);
}
if (indent == null) {
return null;
}
//adding offset, after adding indent to keep consistency on whitespace of the last line.
indent.append(getReferenceIndentation(offset));
return indent.toString();
}
/**
* Returns the indentation of the line at <code>offset</code> as a <code>StringBuilder</code>. If the offset is
* not valid, the empty string is returned.
*
* @param offset the offset in the document
* @return the indentation (leading whitespace) of the line in which <code>offset</code> is located
*/
private String getLeadingWhitespace(int offset)
{
try {
IRegion line = document.getLineInformationOfOffset(offset);
int lineOffset = line.getOffset();
int nonWS = scanner.findNonWhitespaceForwardInAnyPartition(lineOffset, lineOffset + line.getLength());
return document.get(lineOffset, nonWS - lineOffset);
}
catch (BadLocationException e) {
// _log.debug(EditorMessages.error_badLocationException, e);
return "";
}
}
/**
* Reduces indentation in <code>indent</code> by one indentation unit.
*
* @param indent the indentation to be modified
*/
private void unindent(StringBuilder indent)
{
CharSequence oneIndent = createIndent();
int i = indent.lastIndexOf(oneIndent.toString()); //$NON-NLS-1$
if (i != -1) {
indent.delete(i, i + oneIndent.length());
}
}
/**
* Creates a string that represents the given number of indents (can be spaces or tabs..)
*
* @param indent the requested indentation level.
* @return the indentation specified by <code>indent</code>
*/
private StringBuilder createIndent(int indent)
{
StringBuilder oneIndent = createIndent();
StringBuilder ret = new StringBuilder();
while (indent-- > 0) {
ret.append(oneIndent);
}
return ret;
}
/**
* Creates a string that represents one indent (can be spaces or tabs..)
*
* @return one indentation
*/
private StringBuilder createIndent()
{
// get a sensible default when running without the infrastructure for testing
StringBuilder oneIndent = new StringBuilder();
oneIndent.append('\t');
return oneIndent;
}
/**
* Returns the reference position regarding to indentation for <code>offset</code>, or <code>NOT_FOUND</code>.
*
* @param offset the offset for which the reference is computed
* @return the reference statement relative to which <code>offset</code> should be indented, or
* {@link SQLHeuristicScanner#NOT_FOUND}
*/
public int findReferencePosition(int offset)
{
indent = 0; // the indentation modification
position = offset;
nextToken();
return skipToPreviousListItemOrListStart();
}
/**
* Returns the reference position for a list element. The algorithm tries to match any previous indentation on the
* same list. If there is none, the reference position returned is determined depending on the type of list: The
* indentation will either match the list scope introducer (e.g. for method declarations), so called deep indents,
* or simply increase the indentation by a number of standard indents.
*
* @return the reference position for a list item: either a previous list item that has its own indentation, or the
* list introduction start.
*/
private int skipToPreviousListItemOrListStart()
{
int startLine = line;
int startPosition = position;
while (true) {
nextToken();
// if any line item comes with its own indentation, adapt to it
if (line < startLine) {
try {
int lineOffset = document.getLineOffset(startLine);
int bound = Math.min(document.getLength(), startPosition + 1);
int align = scanner.findNonWhitespaceForwardInAnyPartition(lineOffset, bound);
}
catch (BadLocationException e) {
// _log.debug(EditorMessages.error_badLocationException, e);
// ignore and return just the position
}
return startPosition;
}
switch (token) {
case SQLIndentSymbols.TokenEOF:
return 0;
}
}
}
/**
* Reads the next token in backward direction from the heuristic scanner and sets the fields
* <code>fToken, fPreviousPosition</code> and <code>fPosition</code> accordingly.
*/
private void nextToken()
{
nextToken(position);
}
public void nextToken(int start)
{
token = scanner.previousToken(start - 1, SQLHeuristicScanner.UNBOUND);
position = scanner.getPosition() + 1;
try {
line = document.getLineOfOffset(position);
}
catch (BadLocationException e) {
line = -1;
}
}
}