/*
* 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.syntax;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.*;
import org.eclipse.jface.text.contentassist.*;
import org.eclipse.jface.text.templates.Template;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.*;
import org.jkiss.dbeaver.model.runtime.AbstractJob;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.sql.SQLConstants;
import org.jkiss.dbeaver.model.sql.SQLScriptElement;
import org.jkiss.dbeaver.model.struct.DBSObject;
import org.jkiss.dbeaver.model.struct.DBSObjectContainer;
import org.jkiss.dbeaver.model.struct.DBSObjectFilter;
import org.jkiss.dbeaver.model.struct.DBSObjectReference;
import org.jkiss.dbeaver.registry.sql.SQLCommandHandlerDescriptor;
import org.jkiss.dbeaver.registry.sql.SQLCommandsRegistry;
import org.jkiss.dbeaver.ui.TextUtils;
import org.jkiss.dbeaver.ui.UIUtils;
import org.jkiss.dbeaver.ui.editors.sql.SQLEditorBase;
import org.jkiss.dbeaver.ui.editors.sql.SQLPreferenceConstants;
import org.jkiss.dbeaver.ui.editors.sql.templates.SQLContext;
import org.jkiss.dbeaver.ui.editors.sql.templates.SQLTemplateCompletionProposal;
import org.jkiss.dbeaver.ui.editors.sql.templates.SQLTemplatesRegistry;
import org.jkiss.utils.ArrayUtils;
import org.jkiss.utils.CommonUtils;
import java.util.*;
/**
* The SQL content assist processor. This content assist processor proposes text
* completions and computes context information for a SQL content type.
*/
public class SQLCompletionProcessor implements IContentAssistProcessor
{
private static final Log log = Log.getLog(SQLCompletionProcessor.class);
static final String ALL_COLUMNS_PATTERN = "*";
enum QueryType {
TABLE,
COLUMN
}
private static IContextInformationValidator VALIDATOR = new Validator();
private static boolean lookupTemplates = false;
private static boolean simpleMode = false;
public static boolean isLookupTemplates() {
return lookupTemplates;
}
public static void setLookupTemplates(boolean lookupTemplates) {
SQLCompletionProcessor.lookupTemplates = lookupTemplates;
}
static void setSimpleMode(boolean simpleMode) {
SQLCompletionProcessor.simpleMode = simpleMode;
}
private final SQLEditorBase editor;
public SQLCompletionProcessor(SQLEditorBase editor)
{
this.editor = editor;
}
@Override
public ICompletionProposal[] computeCompletionProposals(
ITextViewer viewer,
int documentOffset)
{
final SQLCompletionAnalyzer.CompletionRequest request = new SQLCompletionAnalyzer.CompletionRequest(editor, documentOffset, simpleMode);
SQLWordPartDetector wordDetector = request.wordDetector =
new SQLWordPartDetector(viewer.getDocument(), editor.getSyntaxManager(), documentOffset);
request.wordPart = wordDetector.getWordPart();
if (lookupTemplates) {
return makeTemplateProposals(viewer, request);
}
try {
String commandPrefix = editor.getSyntaxManager().getControlCommandPrefix();
if (request.wordDetector.getStartOffset() >= commandPrefix.length() &&
viewer.getDocument().get(request.wordDetector.getStartOffset() - commandPrefix.length(), commandPrefix.length()).equals(commandPrefix))
{
return makeCommandProposals(request, request.wordPart);
}
} catch (BadLocationException e) {
log.debug(e);
}
String searchPrefix = request.wordPart;
request.queryType = null;
{
final String prevKeyWord = wordDetector.getPrevKeyWord();
if (!CommonUtils.isEmpty(prevKeyWord)) {
if (editor.getSyntaxManager().getDialect().isEntityQueryWord(prevKeyWord)) {
// TODO: its an ugly hack. Need a better way
if (SQLConstants.KEYWORD_INTO.equals(prevKeyWord) &&
!CommonUtils.isEmpty(wordDetector.getPrevWords()) &&
("(".equals(wordDetector.getPrevDelimiter()) || ",".equals(wordDetector.getPrevDelimiter())))
{
request.queryType = QueryType.COLUMN;
} else {
request.queryType = QueryType.TABLE;
}
} else if (editor.getSyntaxManager().getDialect().isAttributeQueryWord(prevKeyWord)) {
request.queryType = QueryType.COLUMN;
if (!request.simpleMode && CommonUtils.isEmpty(request.wordPart) && wordDetector.getPrevDelimiter().equals(ALL_COLUMNS_PATTERN)) {
wordDetector.moveToDelimiter();
searchPrefix = ALL_COLUMNS_PATTERN;
}
}
}
}
request.wordPart = searchPrefix;
if (request.wordPart != null) {
if (editor.getDataSource() != null) {
ProposalSearchJob searchJob = new ProposalSearchJob(request);
searchJob.schedule();
// Wait until job finished
UIUtils.waitJobCompletion(searchJob);
}
}
if (!CommonUtils.isEmpty(request.wordPart)) {
// Keyword assist
List<String> matchedKeywords = editor.getSyntaxManager().getDialect().getMatchedKeywords(request.wordPart);
if (!request.simpleMode) {
// Sort using fuzzy match
Collections.sort(matchedKeywords, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return TextUtils.fuzzyScore(o1, request.wordPart) - TextUtils.fuzzyScore(o2, request.wordPart);
}
});
}
for (String keyWord : matchedKeywords) {
DBPKeywordType keywordType = editor.getSyntaxManager().getDialect().getKeywordType(keyWord);
if (keywordType != null) {
request.proposals.add(
SQLCompletionAnalyzer.createCompletionProposal(
request,
keyWord,
keyWord,
keywordType,
null,
false,
null)
);
}
}
}
// Remove duplications
for (int i = 0; i < request.proposals.size(); i++) {
SQLCompletionProposal proposal = request.proposals.get(i);
for (int j = i + 1; j < request.proposals.size(); ) {
SQLCompletionProposal proposal2 = request.proposals.get(j);
if (proposal.getDisplayString().equals(proposal2.getDisplayString())) {
request.proposals.remove(j);
} else {
j++;
}
}
}
DBSObject selectedObject = DBUtils.getActiveInstanceObject(editor.getDataSource());
boolean hideDups = editor.getActivePreferenceStore().getBoolean(SQLPreferenceConstants.HIDE_DUPLICATE_PROPOSALS) && selectedObject != null;
if (hideDups) {
for (int i = 0; i < request.proposals.size(); i++) {
SQLCompletionProposal proposal = request.proposals.get(i);
for (int j = 0; j < request.proposals.size(); ) {
SQLCompletionProposal proposal2 = request.proposals.get(j);
if (i != j && proposal.hasStructObject() && proposal2.hasStructObject() &&
CommonUtils.equalObjects(proposal.getObject().getName(), proposal2.getObject().getName()) &&
proposal.getObjectContainer() == selectedObject) {
request.proposals.remove(j);
} else {
j++;
}
}
}
}
if (hideDups) {
// Remove duplicates from non-active schema
if (selectedObject instanceof DBSObjectContainer) {
//List<ICompletionProposal>
}
}
return ArrayUtils.toArray(ICompletionProposal.class, request.proposals);
}
private ICompletionProposal[] makeCommandProposals(SQLCompletionAnalyzer.CompletionRequest request, String prefix) {
final String controlCommandPrefix = editor.getSyntaxManager().getControlCommandPrefix();
if (prefix.startsWith(controlCommandPrefix)) {
prefix = prefix.substring(controlCommandPrefix.length());
}
final List<SQLCommandCompletionProposal> commandProposals = new ArrayList<>();
for (SQLCommandHandlerDescriptor command : SQLCommandsRegistry.getInstance().getCommandHandlers()) {
if (command.getId().startsWith(prefix)) {
commandProposals.add(new SQLCommandCompletionProposal(request, command));
}
}
return commandProposals.toArray(new ICompletionProposal[commandProposals.size()]);
}
@NotNull
private ICompletionProposal[] makeTemplateProposals(ITextViewer viewer, SQLCompletionAnalyzer.CompletionRequest request) {
String wordPart = request.wordPart.toLowerCase();
final List<SQLTemplateCompletionProposal> templateProposals = new ArrayList<>();
// Templates
for (Template template : editor.getTemplatesPage().getTemplateStore().getTemplates()) {
if (template.getName().toLowerCase().startsWith(wordPart)) {
templateProposals.add(new SQLTemplateCompletionProposal(
template,
new SQLContext(
SQLTemplatesRegistry.getInstance().getTemplateContextRegistry().getContextType(template.getContextTypeId()),
viewer.getDocument(),
new Position(request.wordDetector.getStartOffset(), request.wordDetector.getLength()),
editor),
new Region(request.documentOffset, 0),
null));
}
}
Collections.sort(templateProposals, new Comparator<SQLTemplateCompletionProposal>() {
@Override
public int compare(SQLTemplateCompletionProposal o1, SQLTemplateCompletionProposal o2) {
return o1.getDisplayString().compareTo(o2.getDisplayString());
}
});
return templateProposals.toArray(new ICompletionProposal[templateProposals.size()]);
}
/**
* This method is incomplete in that it does not implement logic to produce
* some context help relevant to SQL. It just hard codes two strings to
* demonstrate the action
*
* @see org.eclipse.jface.text.contentassist.IContentAssistProcessor#computeContextInformation(ITextViewer,
* int)
*/
@Nullable
@Override
public IContextInformation[] computeContextInformation(
ITextViewer viewer, int documentOffset)
{
SQLScriptElement statementInfo = editor.extractQueryAtPos(documentOffset);
if (statementInfo == null || CommonUtils.isEmpty(statementInfo.getText())) {
return null;
}
IContextInformation[] result = new IContextInformation[1];
result[0] = new ContextInformation(statementInfo.getText(), statementInfo.getText());
return result;
}
@Override
public char[] getCompletionProposalAutoActivationCharacters()
{
boolean useKeystrokes = editor.getActivePreferenceStore().getBoolean(SQLPreferenceConstants.ENABLE_KEYSTROKE_ACTIVATION);
return useKeystrokes ?
".abcdefghijklmnopqrstuvwxyz_$".toCharArray() :
new char[] {'.', };
}
@Nullable
@Override
public char[] getContextInformationAutoActivationCharacters()
{
return null;
}
@Nullable
@Override
public String getErrorMessage()
{
return null;
}
@Override
public IContextInformationValidator getContextInformationValidator()
{
return VALIDATOR;
}
/**
* Simple content assist tip closer. The tip is valid in a range of 5
* characters around its popup location.
*/
protected static class Validator implements IContextInformationValidator, IContextInformationPresenter
{
int fInstallOffset;
@Override
public boolean isContextInformationValid(int offset)
{
return Math.abs(fInstallOffset - offset) < 5;
}
@Override
public void install(IContextInformation info,
ITextViewer viewer, int offset)
{
fInstallOffset = offset;
}
@Override
public boolean updatePresentation(int documentPosition,
TextPresentation presentation)
{
return false;
}
}
private class ProposalSearchJob extends AbstractJob {
private final SQLCompletionAnalyzer.CompletionRequest request;
ProposalSearchJob(SQLCompletionAnalyzer.CompletionRequest request) {
super("Search proposals...");
setSystem(true);
this.request = request;
}
@Override
protected IStatus run(DBRProgressMonitor monitor) {
try {
monitor.beginTask("Seeking for completion proposals", 1);
try {
monitor.subTask("Make structure proposals");
SQLCompletionAnalyzer analyzer = new SQLCompletionAnalyzer(monitor, request);
analyzer.runAnalyzer();
} finally {
monitor.done();
}
applyFilters();
return Status.OK_STATUS;
} catch (Throwable e) {
log.error(e);
return Status.CANCEL_STATUS;
}
}
private void applyFilters() {
DBPDataSource dataSource = editor.getDataSource();
if (dataSource == null) {
return;
}
DBPDataSourceContainer dsContainer = dataSource.getContainer();
Map<DBSObject, Map<Class, List<SQLCompletionProposal>>> containerMap = new HashMap<>();
for (SQLCompletionProposal proposal : request.proposals) {
DBSObject container = proposal.getObjectContainer();
DBPNamedObject object = proposal.getObject();
if (object == null) {
continue;
}
Map<Class, List<SQLCompletionProposal>> typeMap = containerMap.get(container);
if (typeMap == null) {
typeMap = new HashMap<>();
containerMap.put(container, typeMap);
}
Class objectType = object instanceof DBSObjectReference ? ((DBSObjectReference) object).getObjectClass() : object.getClass();
List<SQLCompletionProposal> list = typeMap.get(objectType);
if (list == null) {
list = new ArrayList<>();
typeMap.put(objectType, list);
}
list.add(proposal);
}
for (Map.Entry<DBSObject, Map<Class, List<SQLCompletionProposal>>> entry : containerMap.entrySet()) {
for (Map.Entry<Class, List<SQLCompletionProposal>> typeEntry : entry.getValue().entrySet()) {
DBSObjectFilter filter = dsContainer.getObjectFilter(typeEntry.getKey(), entry.getKey(), true);
if (filter != null && filter.isEnabled()) {
for (SQLCompletionProposal proposal : typeEntry.getValue()) {
if (!filter.matches(proposal.getObject().getName())) {
request.proposals.remove(proposal);
}
}
}
}
}
}
}
}