/* * 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.util; import org.jkiss.dbeaver.Log; import org.eclipse.jface.text.*; import org.eclipse.jface.text.link.*; import org.eclipse.jface.text.link.LinkedModeUI.ExitFlags; import org.eclipse.jface.text.link.LinkedModeUI.IExitPolicy; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.text.templates.Template; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.VerifyKeyListener; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.graphics.Point; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.jkiss.dbeaver.ui.editors.sql.SQLEditorBase; import org.jkiss.dbeaver.ui.editors.sql.syntax.SQLPartitionScanner; import org.jkiss.dbeaver.ui.editors.sql.templates.SQLTemplatesPage; import java.util.ArrayList; import java.util.List; public class SQLSymbolInserter implements VerifyKeyListener, ILinkedModeListener { static protected final Log log = Log.getLog(SQLSymbolInserter.class); private boolean closeSingleQuotes = true; private boolean closeDoubleQuotes = true; private boolean closeBrackets = true; private final String CATEGORY = toString(); private IPositionUpdater positionUpdater = new ExclusivePositionUpdater(CATEGORY); private List<SymbolLevel> bracketLevelStack = new ArrayList<>(); private SQLEditorBase editor; private ISourceViewer sourceViewer; public SQLSymbolInserter(SQLEditorBase editor) { this.editor = editor; this.sourceViewer = editor.getViewer(); } public void setCloseSingleQuotesEnabled(boolean enabled) { closeSingleQuotes = enabled; } public void setCloseDoubleQuotesEnabled(boolean enabled) { closeDoubleQuotes = enabled; } public void setCloseBracketsEnabled(boolean enabled) { closeBrackets = enabled; } private boolean hasIdentifierToTheRight(IDocument document, int offset) { try { int end = offset; IRegion endLine = document.getLineInformationOfOffset(end); int maxEnd = endLine.getOffset() + endLine.getLength(); while (end != maxEnd && Character.isWhitespace(document.getChar(end))) ++end; return end != maxEnd && Character.isJavaIdentifierPart(document.getChar(end)); } catch (BadLocationException e) { // be conservative log.debug(e); return true; } } private boolean hasIdentifierToTheLeft(IDocument document, int offset) { try { IRegion startLine = document.getLineInformationOfOffset(offset); int minStart = startLine.getOffset(); return offset != minStart && Character.isJavaIdentifierPart(document.getChar(offset - 1)); } catch (BadLocationException e) { log.debug(e); return true; } } private boolean hasCharacterToTheRight(IDocument document, int offset, char character) { try { int end = offset; IRegion endLine = document.getLineInformationOfOffset(end); int maxEnd = endLine.getOffset() + endLine.getLength(); while (end != maxEnd && Character.isWhitespace(document.getChar(end))) { ++end; } return end != maxEnd && document.getChar(end) == character; } catch (BadLocationException e) { log.debug(e); // be conservative return true; } } @Override public void verifyKey(VerifyEvent event) { if (!event.doit) { return; } IDocument document = sourceViewer.getDocument(); final Point selection = sourceViewer.getSelectedRange(); final int offset = selection.x; final int length = selection.y; switch (event.character) { case '(': case '[': if (!closeBrackets) { return; } if (hasCharacterToTheRight(document, offset + length, event.character)) { return; } // fall through case '\'': if (event.character == '\'') { if (!closeSingleQuotes) { return; } if (hasIdentifierToTheLeft(document, offset) || hasIdentifierToTheRight(document, offset + length)) { return; } } // fall through case '"': if (event.character == '"') { if (!closeDoubleQuotes) { return; } if (hasIdentifierToTheLeft(document, offset) || hasIdentifierToTheRight(document, offset + length)) { return; } } try { ITypedRegion partition = TextUtilities.getPartition( document, SQLPartitionScanner.SQL_PARTITIONING, offset, true); if (!IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType()) && partition.getOffset() != offset) { return; } if (!editor.validateEditorInputState()) { return; } final char character = event.character; final char closingCharacter = getPeerCharacter(character); document.replace(offset, length, String.valueOf(character) + closingCharacter); SymbolLevel level = new SymbolLevel(); bracketLevelStack.add(level); LinkedPositionGroup group = new LinkedPositionGroup(); group.addPosition(new LinkedPosition(document, offset + 1, 0, LinkedPositionGroup.NO_STOP)); LinkedModeModel model = new LinkedModeModel(); model.addLinkingListener(this); model.addGroup(group); model.forceInstall(); level.offset = offset; level.length = 2; // set up position tracking for our magic peers if (bracketLevelStack.size() == 1) { document.addPositionCategory(CATEGORY); document.addPositionUpdater(positionUpdater); } level.firstPosition = new Position(offset, 1); level.secondPosition = new Position(offset + 1, 1); document.addPosition(CATEGORY, level.firstPosition); document.addPosition(CATEGORY, level.secondPosition); level.uI = new EditorLinkedModeUI(model, sourceViewer); level.uI.setSimpleMode(true); level.uI.setExitPolicy(new ExitPolicy(closingCharacter, getEscapeCharacter(closingCharacter), bracketLevelStack)); level.uI.setExitPosition(sourceViewer, offset + 2, 0, Integer.MAX_VALUE); level.uI.setCyclingMode(LinkedModeUI.CYCLE_NEVER); level.uI.enter(); IRegion newSelection = level.uI.getSelectedRegion(); sourceViewer.setSelectedRange(newSelection.getOffset(), newSelection.getLength()); event.doit = false; } catch (BadLocationException | BadPositionCategoryException e) { log.debug(e); } break; case SWT.TAB: { try { int curOffset = offset; // if (curOffset == document.getLength()) { // curOffset--; // endOffset--; // } while (curOffset > 0) { if (!Character.isJavaIdentifierPart(document.getChar(curOffset - 1))) { break; } curOffset--; } if (curOffset != offset) { String templateName = document.get(curOffset, offset - curOffset); SQLTemplatesPage templatesPage = editor.getTemplatesPage(); Template template = templatesPage.getTemplateStore().findTemplate(templateName); if (template != null && template.isAutoInsertable()) { sourceViewer.setSelectedRange(curOffset, offset - curOffset); templatesPage.insertTemplate(template, document); event.doit = false; } } } catch (BadLocationException e) { log.debug(e); } break; } } } /* * @see org.eclipse.jface.text.link.ILinkedModeListener#left(org.eclipse.jface.text.link.LinkedModeModel, int) */ @Override public void left(LinkedModeModel environment, int flags) { final SymbolLevel level = bracketLevelStack.remove(bracketLevelStack.size() - 1); if (flags != ILinkedModeListener.EXTERNAL_MODIFICATION) { return; } // remove brackets final IDocument document = sourceViewer.getDocument(); if (document instanceof IDocumentExtension) { IDocumentExtension extension = (IDocumentExtension) document; extension.registerPostNotificationReplace(null, new IDocumentExtension.IReplace() { @Override public void perform(IDocument d, IDocumentListener owner) { if ((level.firstPosition.isDeleted || level.firstPosition.length == 0) && !level.secondPosition.isDeleted && level.secondPosition.offset == level.firstPosition.offset) { try { document.replace(level.secondPosition.offset, level.secondPosition.length, null); } catch (BadLocationException e) { // do nothing } } if (bracketLevelStack.size() == 0) { document.removePositionUpdater(positionUpdater); try { document.removePositionCategory(CATEGORY); } catch (BadPositionCategoryException e) { // do nothing } } } } ); } } /* * @see org.eclipse.jface.text.link.ILinkedModeListener#suspend(org.eclipse.jface.text.link.LinkedModeModel) */ @Override public void suspend(LinkedModeModel environment) { } /* * @see org.eclipse.jface.text.link.ILinkedModeListener#resume(org.eclipse.jface.text.link.LinkedModeModel, int) */ @Override public void resume(LinkedModeModel environment, int flags) { } private static class SymbolLevel { int offset; int length; LinkedModeUI uI; Position firstPosition; Position secondPosition; } private class ExitPolicy implements IExitPolicy { final char exitCharacter; final char escapeCharacter; final List<SymbolLevel> stack; final int size; public ExitPolicy(char exitCharacter, char escapeCharacter, List<SymbolLevel> stack) { this.exitCharacter = exitCharacter; this.escapeCharacter = escapeCharacter; this.stack = stack; size = this.stack.size(); } @Override public ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length) { if (event.character == exitCharacter) { if (size == stack.size() && !isMasked(offset)) { SymbolLevel level = stack.get(stack.size() - 1); if (level.firstPosition.offset > offset || level.secondPosition.offset < offset) { return null; } if (level.secondPosition.offset == offset && length == 0) { // don't enter the character if if its the closing peer return new ExitFlags(ILinkedModeListener.UPDATE_CARET, false); } } } return null; } private boolean isMasked(int offset) { IDocument document = sourceViewer.getDocument(); try { return escapeCharacter == document.getChar(offset - 1); } catch (BadLocationException e) { log.debug(e); } return false; } } public static char getEscapeCharacter(char character) { switch (character) { case '"': case '\'': return '\\'; default: return 0; } } public static char getPeerCharacter(char character) { switch (character) { case '(': return ')'; case ')': return '('; case '[': return ']'; case ']': return '['; case '"': return character; case '\'': return character; default: throw new IllegalArgumentException(); } } public static class EditorLinkedModeUI extends LinkedModeUI { private static class EditorHistoryUpdater implements ILinkedModeUIFocusListener { @Override public void linkingFocusLost(LinkedPosition position, LinkedModeUITarget target) { // mark navigation history IWorkbenchWindow win= PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (win != null) { IWorkbenchPage page= win.getActivePage(); if (page != null) { IEditorPart part= page.getActiveEditor(); page.getNavigationHistory().markLocation(part); } } } @Override public void linkingFocusGained(LinkedPosition position, LinkedModeUITarget target) { } } public EditorLinkedModeUI(LinkedModeModel model, ITextViewer viewer) { super(model, viewer); setPositionListener(new EditorHistoryUpdater()); } } }