/*
* 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.*;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.core.DBeaverCore;
import org.jkiss.dbeaver.model.DBPKeywordType;
import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore;
import org.jkiss.dbeaver.model.sql.SQLSyntaxManager;
import org.jkiss.dbeaver.ui.editors.sql.SQLPreferenceConstants;
import org.jkiss.dbeaver.ui.editors.sql.syntax.SQLPartitionScanner;
import org.jkiss.dbeaver.utils.GeneralUtils;
import java.util.HashMap;
import java.util.Map;
public class SQLAutoIndentStrategy extends DefaultIndentLineAutoEditStrategy {
private static final Log log = Log.getLog(SQLAutoIndentStrategy.class);
private static final int MINIMUM_SOUCE_CODE_LENGTH = 10;
private String partitioning;
private SQLSyntaxManager syntaxManager;
private Map<Integer, String> autoCompletionMap = new HashMap<>();
private String[] delimiters;
/**
* Creates a new SQL auto indent strategy for the given document partitioning.
*/
public SQLAutoIndentStrategy(String partitioning, SQLSyntaxManager syntaxManager)
{
this.partitioning = partitioning;
this.syntaxManager = syntaxManager;
}
@Override
public void customizeDocumentCommand(IDocument document, DocumentCommand command)
{
// Do not check for doit because it is disabled in LinkedModeUI (e.g. when braket triggers position group)
// if (!command.doit) {
// return;
// }
if (command.offset < 0) {
return;
}
if (command.text != null && command.text.length() > MINIMUM_SOUCE_CODE_LENGTH) {
if (syntaxManager.getPreferenceStore().getBoolean(SQLPreferenceConstants.SQL_FORMAT_EXTRACT_FROM_SOURCE)) {
transformSourceCode(document, command);
}
} else if (command.length == 0 && command.text != null) {
final boolean lineDelimiter = isLineDelimiter(document, command.text);
try {
boolean isPrevLetter = command.offset > 0 && Character.isJavaIdentifierPart(document.getChar(command.offset - 1));
if (command.offset > 1 && isPrevLetter &&
(lineDelimiter || (command.text.length() == 1 && !Character.isJavaIdentifierPart(command.text.charAt(0)))) &&
syntaxManager.getPreferenceStore().getBoolean(SQLPreferenceConstants.SQL_FORMAT_KEYWORD_CASE_AUTO))
{
updateKeywordCase(document, command);
}
} catch (BadLocationException e) {
log.debug(e);
}
if (lineDelimiter) {
smartIndentAfterNewLine(document, command);
}
}
}
private boolean transformSourceCode(IDocument document, DocumentCommand command) {
String sourceCode = command.text;
int quoteStart = -1, quoteEnd = -1;
for (int i = 0; i < sourceCode.length(); i++) {
final char ch = sourceCode.charAt(i);
if (ch == '"') {
quoteStart = i;
break;
} else if (Character.isUnicodeIdentifierPart(ch) || ch == '{' || ch == '<' || ch == '[') {
// Letter or bracket before quote
return false;
}
}
for (int i = sourceCode.length() - 1; i >= 0; i--) {
final char ch = sourceCode.charAt(i);
if (ch == '"') {
quoteEnd = i;
break;
} else if (Character.isUnicodeIdentifierPart(ch)) {
// Letter before quote
return false;
}
}
if (quoteStart == -1 || quoteEnd == -1) {
return false;
}
// Let's check that source code has some whitespaces
boolean hasWhitespaces = false;
for (int i = quoteStart + 1; i < quoteEnd; i++) {
if (Character.isWhitespace(sourceCode.charAt(i))) {
hasWhitespaces = true;
break;
}
}
if (!hasWhitespaces) {
return false;
}
StringBuilder result = new StringBuilder(sourceCode.length());
char prevChar = (char)-1;
boolean inString = true;
for (int i = quoteStart + 1; i < quoteEnd; i++) {
final char ch = sourceCode.charAt(i);
if (prevChar == '\\' && inString) {
switch (ch) {
case 'n': result.append("\n"); break;
case 'r': result.append("\r"); break;
case 't': result.append("\t"); break;
default: result.append(ch); break;
}
} else {
switch (ch) {
case '"':
inString = !inString;
break;
case '\\':
break;
default:
if (inString) {
result.append(ch);
} else if (ch == '\n' && result.length() > 0) {
// Append linefeed even if it is outside of quotes
// (but only if string in quotes doesn't end with linefeed - we don't need doubles)
boolean endsWithLF = false;
for (int k = result.length(); k > 0; k--) {
final char lch = result.charAt(k - 1);
if (!Character.isWhitespace(lch)) {
break;
}
if (lch == '\n' || lch == '\r') {
endsWithLF = true;
break;
}
}
if (!endsWithLF) {
result.append(ch);
}
}
}
}
prevChar = ch;
}
try {
document.replace(command.offset, command.length, command.text);
document.replace(command.offset, command.text.length(), result.toString());
} catch (Exception e) {
log.warn(e);
}
command.caretOffset = command.offset + result.length();
command.text = null;
command.length = 0;
command.doit = false;
return true;
}
private boolean updateKeywordCase(final IDocument document, DocumentCommand command) throws BadLocationException {
final String commandPrefix = syntaxManager.getControlCommandPrefix();
// Whitespace - check for keyword
final int startPos, endPos;
int pos = command.offset - 1;
while (pos >= 0 && Character.isWhitespace(document.getChar(pos))) {
pos--;
}
endPos = pos + 1;
while (pos >= 0) {
char ch = document.getChar(pos);
if (!Character.isJavaIdentifierPart(ch) && commandPrefix.indexOf(ch) == -1) {
break;
}
pos--;
}
startPos = pos + 1;
final String keyword = document.get(startPos, endPos - startPos);
if (syntaxManager.getDialect().getKeywordType(keyword) == DBPKeywordType.KEYWORD) {
final String fixedKeyword = syntaxManager.getKeywordCase().transform(keyword);
if (!fixedKeyword.equals(keyword)) {
command.addCommand(startPos, endPos - startPos, fixedKeyword, null);
command.doit = false;
return true;
}
}
return false;
}
private void smartIndentAfterNewLine(IDocument document, DocumentCommand command)
{
clearCachedValues();
int docLength = document.getLength();
if (docLength == 0) {
return;
}
SQLHeuristicScanner scanner = new SQLHeuristicScanner(document, syntaxManager);
SQLIndenter indenter = new SQLIndenter(document, scanner);
//get previous token
int previousToken = scanner.previousToken(command.offset - 1, SQLHeuristicScanner.UNBOUND);
int nextToken = scanner.nextToken(command.offset, SQLHeuristicScanner.UNBOUND);
String indent;
String beginIndentaion = "";
if (isSupportedAutoCompletionToken(previousToken)) {
indent = indenter.computeIndentation(command.offset);
beginIndentaion = indenter.getReferenceIndentation(command.offset);
} else {
if (nextToken == SQLIndentSymbols.Tokenend || nextToken == SQLIndentSymbols.TokenEND) {
indent = indenter.getReferenceIndentation(command.offset + 1);
} else {
indent = indenter.getReferenceIndentation(command.offset);
}
}
if (indent == null) {
indent = ""; //$NON-NLS-1$
}
try {
int p = (command.offset == docLength ? command.offset - 1 : command.offset);
int line = document.getLineOfOffset(p);
StringBuilder buf = new StringBuilder(command.text + indent);
IRegion reg = document.getLineInformation(line);
int lineEnd = reg.getOffset() + reg.getLength();
int contentStart = findEndOfWhiteSpace(document, command.offset, lineEnd);
command.length = Math.max(contentStart - command.offset, 0);
int start = reg.getOffset();
ITypedRegion region = TextUtilities.getPartition(document, partitioning, start, true);
if (SQLPartitionScanner.CONTENT_TYPE_SQL_MULTILINE_COMMENT.equals(region.getType())) {
start = document.getLineInformationOfOffset(region.getOffset()).getOffset();
}
command.caretOffset = command.offset + buf.length();
command.shiftsCaret = false;
if (isSupportedAutoCompletionToken(previousToken) && !isClosed(document, command.offset, previousToken) && getTokenCount(start, command.offset, scanner, previousToken) > 0) {
buf.append(getLineDelimiter(document));
buf.append(beginIndentaion);
buf.append(getAutoCompletionTrail(previousToken));
}
command.text = buf.toString();
}
catch (BadLocationException e) {
log.error(e);
}
}
private static String getLineDelimiter(IDocument document)
{
try {
if (document.getNumberOfLines() > 1) {
return document.getLineDelimiter(0);
}
}
catch (BadLocationException e) {
log.error(e);
}
return GeneralUtils.getDefaultLineSeparator();
}
private boolean isLineDelimiter(IDocument document, String text)
{
if (delimiters == null) {
delimiters = document.getLegalLineDelimiters();
}
return delimiters != null && TextUtilities.equals(delimiters, text) > -1;
}
private void clearCachedValues()
{
autoCompletionMap.clear();
DBPPreferenceStore preferenceStore = DBeaverCore.getGlobalPreferenceStore();
boolean closeBeginEnd = preferenceStore.getBoolean(SQLPreferenceConstants.SQLEDITOR_CLOSE_BEGIN_END);
if (closeBeginEnd) {
autoCompletionMap.put(SQLIndentSymbols.Tokenbegin, SQLIndentSymbols.end);
autoCompletionMap.put(SQLIndentSymbols.TokenBEGIN, SQLIndentSymbols.END);
}
}
private boolean isSupportedAutoCompletionToken(int token)
{
return autoCompletionMap.containsKey(token);
}
private String getAutoCompletionTrail(int token)
{
return autoCompletionMap.get(token);
}
/**
* To count token numbers from start offset to end offset.
*/
private int getTokenCount(int startOffset, int endOffset, SQLHeuristicScanner scanner,
int token)
{
int tokenCount = 0;
while (startOffset < endOffset) {
int nextToken = scanner.nextToken(startOffset, endOffset);
int position = scanner.getPosition();
if (nextToken != SQLIndentSymbols.TokenEOF && scanner.isSameToken(nextToken, token)) {
tokenCount++;
}
startOffset = position;
}
return tokenCount;
}
private boolean isClosed(IDocument document, int offset, int token)
{
//currently only BEGIN/END is supported. Later more typing aids will be added here.
if (token == SQLIndentSymbols.TokenBEGIN || token == SQLIndentSymbols.Tokenbegin) {
return getBlockBalance(document, offset) <= 0;
}
return false;
}
/**
* Returns the block balance, i.e. zero if the blocks are balanced at <code>offset</code>, a negative number if
* there are more closing than opening peers, and a positive number if there are more opening than closing peers.
*/
private int getBlockBalance(IDocument document, int offset)
{
if (offset < 1) {
return -1;
}
if (offset >= document.getLength()) {
return 1;
}
int begin = offset;
int end = offset;
SQLHeuristicScanner scanner = new SQLHeuristicScanner(document, syntaxManager);
while (true) {
begin = scanner.findOpeningPeer(begin, SQLIndentSymbols.TokenBEGIN, SQLIndentSymbols.TokenEND);
end = scanner.findClosingPeer(end, SQLIndentSymbols.TokenBEGIN, SQLIndentSymbols.TokenEND);
if (begin == -1 && end == -1) {
return 0;
}
if (begin == -1) {
return -1;
}
if (end == -1) {
return 1;
}
}
}
}