/*
* 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.core.DBeaverCore;
import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore;
import org.jkiss.dbeaver.ui.editors.sql.SQLPreferenceConstants;
import org.jkiss.dbeaver.utils.GeneralUtils;
import java.text.BreakIterator;
/**
* Auto indent strategy for SQL multi-line comments
*/
public class SQLCommentAutoIndentStrategy extends DefaultIndentLineAutoEditStrategy {
private static final int DEFAULT_MARGIN = 5;
private static final int DEFAULT_TAB_WIDTH = 4;
private String partitioning;
/**
* Creates a new SQL multi-line comment auto indent strategy for the given document partitioning.
*
* @param partitioning the document partitioning
*/
public SQLCommentAutoIndentStrategy(String partitioning)
{
this.partitioning = partitioning;
}
private static String getLineDelimiter(IDocument document)
{
try {
if (document.getNumberOfLines() > 1) {
return document.getLineDelimiter(0);
}
}
catch (BadLocationException e) {
// _log.error(EditorMessages.error_badLocationException, e);
}
return GeneralUtils.getDefaultLineSeparator();
}
/**
* Copies the indentation of the previous line and add a star. If the SQL multi-line comment just started on this
* line add standard method tags and close the comment.
*
* @param d the document to work on
* @param c the command to deal with
*/
private void commentIndentAfterNewLine(IDocument d, DocumentCommand c)
{
if (c.offset == -1 || d.getLength() == 0) {
return;
}
try {
// find start of line
int p = (c.offset == d.getLength() ? c.offset - 1 : c.offset);
IRegion info = d.getLineInformationOfOffset(p);
int start = info.getOffset();
// find white spaces
int end = findEndOfWhiteSpace(d, start, c.offset);
StringBuilder buf = new StringBuilder(c.text);
if (end >= start) {
String indentation = commentExtractLinePrefix(d, d.getLineOfOffset(c.offset));
buf.append(indentation);
if (end < c.offset) {
//If it is the sinle line comment '//', don't append '*'.
if (d.getChar(end) == '/' && d.getChar(end + 1) != '/') {
// SQL multi-line comment started on this line
buf.append(" * "); //$NON-NLS-1$
if (DBeaverCore.getGlobalPreferenceStore().getBoolean(
SQLPreferenceConstants.SQLEDITOR_CLOSE_COMMENTS)
&& isNewComment(d, c.offset, partitioning)) {
String lineDelimiter = getLineDelimiter(d);
String endTag = lineDelimiter + indentation + " */"; //$NON-NLS-1$
d.replace(c.offset, 0, endTag); //$NON-NLS-1$
}
}
}
}
c.text = buf.toString();
}
catch (BadLocationException excp) {
// stop work
}
}
protected void commentIndentForCommentEnd(IDocument d, DocumentCommand c)
{
if (c.offset < 2 || d.getLength() == 0) {
return;
}
try {
if ("* ".equals(d.get(c.offset - 2, 2))) {
//$NON-NLS-1$
// modify document command
c.length++;
c.offset--;
}
}
catch (BadLocationException excp) {
// stop work
}
}
/**
* Guesses if the command operates within a newly created SQL multi-line comment or not. If in doubt, it will assume
* that the SQL multi-line comment is new.
*/
private static boolean isNewComment(IDocument document, int commandOffset, String partitioning)
{
try {
int lineIndex = document.getLineOfOffset(commandOffset) + 1;
if (lineIndex >= document.getNumberOfLines()) {
return true;
}
IRegion line = document.getLineInformation(lineIndex);
ITypedRegion partition = TextUtilities.getPartition(document, partitioning, commandOffset, false);
int partitionEnd = partition.getOffset() + partition.getLength();
if (line.getOffset() >= partitionEnd) {
return false;
}
if (document.getLength() == partitionEnd) {
return true; // partition goes to end of document - probably a new comment
}
String comment = document.get(partition.getOffset(), partition.getLength());
if (comment.indexOf("/*", 2) != -1) //$NON-NLS-1$
{
return true; // enclosed another comment -> probably a new comment
}
return false;
}
catch (BadLocationException e) {
return false;
}
}
/*
* @see IAutoIndentStrategy#customizeDocumentCommand
*/
@Override
public void customizeDocumentCommand(IDocument document, DocumentCommand command)
{
try {
if (command.text != null && command.length == 0) {
String[] lineDelimiters = document.getLegalLineDelimiters();
int index = TextUtilities.endsWith(lineDelimiters, command.text);
if (index > -1) {
// ends with line delimiter
if (lineDelimiters[index].equals(command.text)) {
// just the line delimiter
commentIndentAfterNewLine(document, command);
}
return;
}
}
if (command.text != null && command.text.equals("/")) {
//$NON-NLS-1$
commentIndentForCommentEnd(document, command);
return;
}
ITypedRegion partition = TextUtilities.getPartition(document, partitioning, command.offset, true);
int partitionStart = partition.getOffset();
int partitionEnd = partition.getLength() + partitionStart;
String text = command.text;
int offset = command.offset;
int length = command.length;
/*
// partition change
final int PREFIX_LENGTH = SQLConstants.ML_COMMENT_START.length();
final int POSTFIX_LENGTH = SQLConstants.ML_COMMENT_END.length(); //$NON-NLS-1$
if ((offset < partitionStart + PREFIX_LENGTH || offset + length > partitionEnd - POSTFIX_LENGTH)
|| text != null && text.length() >= 2
&& ((text.contains(SQLConstants.ML_COMMENT_END)) || (document.getChar(offset) == '*' && text.startsWith(
"/")))) //$NON-NLS-1$ //$NON-NLS-2$
{
return;
}
*/
}
catch (BadLocationException e) {
// _log.error(EditorMessages.error_badLocationException, e);
}
}
private void flushCommand(IDocument document, DocumentCommand command)
throws BadLocationException
{
if (!command.doit) {
return;
}
document.replace(command.offset, command.length, command.text);
command.doit = false;
if (command.text != null) {
command.offset += command.text.length();
}
command.length = 0;
command.text = null;
}
protected void commentWrapParagraphOnInsert(IDocument document, DocumentCommand command)
throws BadLocationException
{
int line = document.getLineOfOffset(command.offset);
IRegion region = document.getLineInformation(line);
int lineOffset = region.getOffset();
int lineLength = region.getLength();
String lineContents = document.get(lineOffset, lineLength);
StringBuilder buffer = new StringBuilder(lineContents);
int start = command.offset - lineOffset;
int end = command.length + start;
buffer.replace(start, end, command.text);
// handle whitespace
if (command.text != null && command.text.length() != 0 && command.text.trim().length() == 0) {
String endOfLine = document.get(command.offset, lineOffset + lineLength - command.offset);
// end of line
if (endOfLine.length() == 0) {
// move caret to next line
flushCommand(document, command);
if (isLineTooShort(document, line)) {
int[] caretOffset =
{
command.offset
};
commentWrapParagraphFromLine(document, line, caretOffset, false);
command.offset = caretOffset[0];
return;
}
// move caret to next line if possible
if (line < document.getNumberOfLines() - 1 && isCommentLine(document, line + 1)) {
String lineDelimiter = document.getLineDelimiter(line);
String nextLinePrefix = commentExtractLinePrefix(document, line + 1);
command.offset += lineDelimiter.length() + nextLinePrefix.length();
}
return;
// inside whitespace at end of line
} else if (endOfLine.trim().length() == 0) {
// simply insert space
return;
}
}
// change in prefix region
String prefix = commentExtractLinePrefix(document, line);
boolean wrapAlways = command.offset >= lineOffset && command.offset <= lineOffset + prefix.length();
// must insert the text now because it may include whitepace
flushCommand(document, command);
if (wrapAlways || calculateDisplayedWidth(buffer.toString()) > getMargin() || isLineTooShort(document, line)) {
int[] caretOffset =
{
command.offset
};
commentWrapParagraphFromLine(document, line, caretOffset, wrapAlways);
if (!wrapAlways) {
command.offset = caretOffset[0];
}
}
}
/**
* Method commentWrapParagraphFromLine.
*/
private void commentWrapParagraphFromLine(IDocument document, int line, int[] caretOffset, boolean always)
throws BadLocationException
{
String indent = commentExtractLinePrefix(document, line);
if (!always) {
if (!indent.trim().startsWith("*")) //$NON-NLS-1$
{
return;
}
if (indent.trim().startsWith("*/")) //$NON-NLS-1$
{
return;
}
if (!isLineTooLong(document, line) && !isLineTooShort(document, line)) {
return;
}
}
boolean caretRelativeToParagraphOffset = false;
int caret = caretOffset[0];
int caretLine = document.getLineOfOffset(caret);
int lineOffset = document.getLineOffset(line);
int paragraphOffset = lineOffset + indent.length();
if (paragraphOffset < caret) {
caret -= paragraphOffset;
caretRelativeToParagraphOffset = true;
} else {
caret -= lineOffset;
}
StringBuilder buffer = new StringBuilder();
int currentLine = line;
while (line == currentLine || isCommentLine(document, currentLine)) {
if (buffer.length() != 0 && !Character.isWhitespace(buffer.charAt(buffer.length() - 1))) {
buffer.append(' ');
if (currentLine <= caretLine) {
// in this case caretRelativeToParagraphOffset is always true
++caret;
}
}
String string = getLineContents(document, currentLine);
buffer.append(string);
currentLine++;
}
String paragraph = buffer.toString();
if (paragraph.trim().length() == 0) {
return;
}
caretOffset[0] = caretRelativeToParagraphOffset ? caret : 0;
String delimiter = document.getLineDelimiter(0);
String wrapped = formatParagraph(paragraph, caretOffset, indent, delimiter, getMargin());
int beginning = document.getLineOffset(line);
int end = document.getLineOffset(currentLine);
document.replace(beginning, end - beginning, wrapped);
caretOffset[0] = caretRelativeToParagraphOffset ? caretOffset[0] + beginning : caret + beginning;
}
/**
* Line break iterator to handle whitespaces as first class citizens.
*/
private static class LineBreakIterator {
private final String _string;
private final BreakIterator _iterator = BreakIterator.getLineInstance();
private int _start;
private int _end;
private int _bufferedEnd;
public LineBreakIterator(String string)
{
_string = string;
_iterator.setText(string);
}
public int first()
{
_bufferedEnd = -1;
_start = _iterator.first();
return _start;
}
public int next()
{
if (_bufferedEnd != -1) {
_start = _end;
_end = _bufferedEnd;
_bufferedEnd = -1;
return _end;
}
_start = _end;
_end = _iterator.next();
if (_end == BreakIterator.DONE) {
return _end;
}
final String string = _string.substring(_start, _end);
// whitespace
if (string.trim().length() == 0) {
return _end;
}
final String word = string.trim();
if (word.length() == string.length()) {
return _end;
}
// suspected whitespace
_bufferedEnd = _end;
return _start + word.length();
}
}
/**
* Formats a paragraph, using break iterator.
*
* @param offset an offset within the paragraph, which will be updated with respect to formatting.
*/
private static String formatParagraph(String paragraph, int[] offset, String prefix, String lineDelimiter,
int margin)
{
LineBreakIterator iterator = new LineBreakIterator(paragraph);
StringBuilder paragraphBuffer = new StringBuilder();
StringBuilder lineBuffer = new StringBuilder();
StringBuilder whiteSpaceBuffer = new StringBuilder();
int index = offset[0];
int indexBuffer = -1;
// line delimiter could be null
if (lineDelimiter == null) {
lineDelimiter = ""; //$NON-NLS-1$
}
for (int start = iterator.first(), end = iterator.next(); end != BreakIterator.DONE; start = end, end = iterator
.next()) {
String word = paragraph.substring(start, end);
// word is whitespace
if (word.trim().length() == 0) {
whiteSpaceBuffer.append(word);
// first word of line is always appended
} else if (lineBuffer.length() == 0) {
lineBuffer.append(prefix);
lineBuffer.append(whiteSpaceBuffer.toString());
lineBuffer.append(word);
} else {
String line = lineBuffer.toString() + whiteSpaceBuffer.toString() + word;
// margin exceeded
if (calculateDisplayedWidth(line) > margin) {
// flush line buffer and wrap paragraph
paragraphBuffer.append(lineBuffer.toString());
paragraphBuffer.append(lineDelimiter);
lineBuffer.setLength(0);
lineBuffer.append(prefix);
lineBuffer.append(word);
// flush index buffer
if (indexBuffer != -1) {
offset[0] = indexBuffer;
// correct for caret in whitespace at the end of line
if (whiteSpaceBuffer.length() != 0 && index < start
&& index >= start - whiteSpaceBuffer.length()) {
offset[0] -= (index - (start - whiteSpaceBuffer.length()));
}
indexBuffer = -1;
}
whiteSpaceBuffer.setLength(0);
// margin not exceeded
} else {
lineBuffer.append(whiteSpaceBuffer.toString());
lineBuffer.append(word);
whiteSpaceBuffer.setLength(0);
}
}
if (index >= start && index < end) {
indexBuffer = paragraphBuffer.length() + lineBuffer.length() + (index - start);
if (word.trim().length() != 0) {
indexBuffer -= word.length();
}
}
}
// flush line buffer
paragraphBuffer.append(lineBuffer.toString());
paragraphBuffer.append(lineDelimiter);
// flush index buffer
if (indexBuffer != -1) {
offset[0] = indexBuffer;
}
// last position is not returned by break iterator
else if (offset[0] == paragraph.length()) {
offset[0] = paragraphBuffer.length() - lineDelimiter.length();
}
return paragraphBuffer.toString();
}
private static DBPPreferenceStore getPreferenceStore()
{
return DBeaverCore.getGlobalPreferenceStore();
}
/**
* Returns the displayed width of a string, taking in account the displayed tab width. The result can be compared
* against the print margin.
*/
private static int calculateDisplayedWidth(String string)
{
final int tabWidth = DEFAULT_TAB_WIDTH;
/*getPreferenceStore().getInt(
AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);*/
int column = 0;
for (int i = 0; i < string.length(); i++) {
if ('\t' == string.charAt(i)) {
column += tabWidth - (column % tabWidth);
} else {
column++;
}
}
return column;
}
private String commentExtractLinePrefix(IDocument d, int line)
throws BadLocationException
{
IRegion region = d.getLineInformation(line);
int lineOffset = region.getOffset();
int index = findEndOfWhiteSpace(d, lineOffset, lineOffset + d.getLineLength(line));
if (d.getChar(index) == '*') {
index++;
if (index != lineOffset + region.getLength() && d.getChar(index) == ' ') {
index++;
}
}
return d.get(lineOffset, index - lineOffset);
}
private String getLineContents(IDocument d, int line)
throws BadLocationException
{
int offset = d.getLineOffset(line);
int length = d.getLineLength(line);
String lineDelimiter = d.getLineDelimiter(line);
if (lineDelimiter != null) {
length = length - lineDelimiter.length();
}
String lineContents = d.get(offset, length);
int trim = commentExtractLinePrefix(d, line).length();
return lineContents.substring(trim);
}
private static String getLine(IDocument document, int line)
throws BadLocationException
{
IRegion region = document.getLineInformation(line);
return document.get(region.getOffset(), region.getLength());
}
/**
* Returns <code>true</code> if the comment line is too short, <code>false</code> otherwise.
*/
private boolean isLineTooShort(IDocument document, int line)
throws BadLocationException
{
if (!isCommentLine(document, line + 1)) {
return false;
}
String nextLine = getLineContents(document, line + 1);
return nextLine.trim().length() != 0;
}
/**
* Returns <code>true</code> if the line is too long, <code>false</code> otherwise.
*/
private boolean isLineTooLong(IDocument document, int line)
throws BadLocationException
{
String lineContents = getLine(document, line);
return calculateDisplayedWidth(lineContents) > getMargin();
}
private static int getMargin()
{
return DEFAULT_MARGIN;//getPreferenceStore().getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN_COLUMN);
}
/**
* returns true if the specified line is part of a paragraph and should be merged with the previous line.
*/
private boolean isCommentLine(IDocument document, int line)
throws BadLocationException
{
if (document.getNumberOfLines() < line)
return false;
int offset = document.getLineOffset(line);
int length = document.getLineLength(line);
int firstChar = findEndOfWhiteSpace(document, offset, offset + length);
length -= firstChar - offset;
String lineContents = document.get(firstChar, length);
String prefix = lineContents.trim();
if (!prefix.startsWith("*") || prefix.startsWith("*/")) //$NON-NLS-1$ //$NON-NLS-2$
{
return false;
}
//lineContents = lineContents.substring(1).trim().toLowerCase();
return true;
}
protected void commentHandleBackspaceDelete(IDocument document, DocumentCommand c)
{
try {
String text = document.get(c.offset, c.length);
int line = document.getLineOfOffset(c.offset);
int lineOffset = document.getLineOffset(line);
// erase line delimiter
String lineDelimiter = document.getLineDelimiter(line);
if (lineDelimiter != null && lineDelimiter.equals(text)) {
String prefix = commentExtractLinePrefix(document, line + 1);
// strip prefix if any
if (prefix.length() > 0) {
int length = document.getLineDelimiter(line).length() + prefix.length();
document.replace(c.offset, length, null);
c.doit = false;
c.length = 0;
return;
}
// backspace: beginning of a SQL multi-line comment line
} else if (document.getChar(c.offset - 1) == '*'
&& commentExtractLinePrefix(document, line).length() - 1 >= c.offset - lineOffset) {
lineDelimiter = document.getLineDelimiter(line - 1);
String prefix = commentExtractLinePrefix(document, line);
int length = (lineDelimiter != null ? lineDelimiter.length() : 0) + prefix.length();
document.replace(c.offset - length + 1, length, null);
c.doit = false;
c.offset -= length - 1;
c.length = 0;
return;
} else {
document.replace(c.offset, c.length, null);
c.doit = false;
c.length = 0;
}
}
catch (BadLocationException e) {
// _log.error(EditorMessages.error_badLocationException, e);
}
try {
int line = document.getLineOfOffset(c.offset);
int lineOffset = document.getLineOffset(line);
String prefix = commentExtractLinePrefix(document, line);
boolean always = c.offset > lineOffset && c.offset <= lineOffset + prefix.length();
int[] caretOffset =
{
c.offset
};
commentWrapParagraphFromLine(document, document.getLineOfOffset(c.offset), caretOffset, always);
c.offset = caretOffset[0];
}
catch (BadLocationException e) {
// _log.error(EditorMessages.error_badLocationException, e);
}
}
}