/* * $Id$ * * Copyright (c) 2006 by the TeXlipse team. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package net.sourceforge.texlipse.editor; import java.util.HashMap; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.texlipse.TexlipsePlugin; import net.sourceforge.texlipse.editor.partitioner.FastLaTeXPartitionScanner; import net.sourceforge.texlipse.properties.TexlipseProperties; import org.eclipse.core.resources.IProject; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.BadPartitioningException; import org.eclipse.jface.text.BadPositionCategoryException; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentExtension; import org.eclipse.jface.text.IDocumentExtension3; import org.eclipse.jface.text.IPositionUpdater; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.jface.text.link.ILinkedModeListener; import org.eclipse.jface.text.link.LinkedModeModel; import org.eclipse.jface.text.link.LinkedModeUI; import org.eclipse.jface.text.link.LinkedPosition; import org.eclipse.jface.text.link.LinkedPositionGroup; 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.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.part.FileEditorInput; import org.eclipse.ui.texteditor.link.EditorLinkedModeUI; /** * This class is used for automatic bracket (and dollar sign) closing * and for replacement of quotation marks. Most parts are copied from * the Eclipse JDT. * * @author Boris von Loesch * @author Oskar Ojala */ public class BracketInserter implements VerifyKeyListener, ILinkedModeListener { private class ExitPolicy implements IExitPolicy { final char fExitCharacter; final char fEscapeCharacter; final Stack<BracketLevel> fStack; final int fSize; final ISourceViewer sourceViewer; public ExitPolicy(char exitCharacter, char escapeCharacter, Stack<BracketLevel> stack, ISourceViewer viewer) { fExitCharacter= exitCharacter; fEscapeCharacter= escapeCharacter; fStack= stack; fSize= fStack.size(); sourceViewer = viewer; } /* * @see org.eclipse.jdt.internal.ui.text.link.LinkedPositionUI.ExitPolicy#doExit(org.eclipse.jdt.internal.ui.text.link.LinkedPositionManager, org.eclipse.swt.events.VerifyEvent, int, int) */ public ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length) { if (fSize == fStack.size() && !isMasked(offset)) { if (event.character == fExitCharacter) { BracketLevel level= (BracketLevel) fStack.peek(); if (level.fFirstPosition.offset > offset || level.fSecondPosition.offset < offset) return null; if (level.fSecondPosition.offset == offset && length == 0) // don't enter the character if if its the closing peer return new ExitFlags(ILinkedModeListener.UPDATE_CARET, false); } // when entering an anonymous class between the parenthesis', we don't want // to jump after the closing parenthesis when return is pressed if (event.character == SWT.CR && offset > 0) { IDocument document= sourceViewer.getDocument(); try { if (document.getChar(offset - 1) == '{') return new ExitFlags(ILinkedModeListener.EXIT_ALL, true); } catch (BadLocationException e) { } } } return null; } private boolean isMasked(int offset) { IDocument document= sourceViewer.getDocument(); try { return fEscapeCharacter == document.getChar(offset - 1); } catch (BadLocationException e) { } return false; } } private static class BracketLevel { int fOffset; int fLength; LinkedModeUI fUI; Position fFirstPosition; Position fSecondPosition; } /** * Position updater that takes any changes at the borders of a position to * not belong to the position. * * @since 3.0 */ private static class ExclusivePositionUpdater implements IPositionUpdater { /** The position category. */ private final String fCategory; /** * Creates a new updater for the given <code>category</code>. * * @param category the new category. */ public ExclusivePositionUpdater(String category) { fCategory= category; } /* * @see org.eclipse.jface.text.IPositionUpdater#update(org.eclipse.jface.text.DocumentEvent) */ public void update(DocumentEvent event) { int eventOffset= event.getOffset(); int eventOldLength= event.getLength(); int eventNewLength= event.getText() == null ? 0 : event.getText().length(); int deltaLength= eventNewLength - eventOldLength; try { Position[] positions= event.getDocument().getPositions(fCategory); for (int i= 0; i != positions.length; i++) { Position position= positions[i]; if (position.isDeleted()) continue; int offset= position.getOffset(); int length= position.getLength(); int end= offset + length; if (offset >= eventOffset + eventOldLength) // position comes // after change - shift position.setOffset(offset + deltaLength); else if (end <= eventOffset) { // position comes way before change - // leave alone } else if (offset <= eventOffset && end >= eventOffset + eventOldLength) { // event completely internal to the position - adjust length position.setLength(length + deltaLength); } else if (offset < eventOffset) { // event extends over end of position - adjust length int newEnd= eventOffset; position.setLength(newEnd - offset); } else if (end > eventOffset + eventOldLength) { // event extends from before position into it - adjust offset // and length // offset becomes end of event, length adjusted accordingly int newOffset= eventOffset + eventNewLength; position.setOffset(newOffset); position.setLength(end - newOffset); } else { // event consumes the position - delete it position.delete(); } } } catch (BadPositionCategoryException e) { // ignore and return } } /** * Returns the position category. * * @return the position category */ public String getCategory() { return fCategory; } } private final String CATEGORY= toString(); private IPositionUpdater fUpdater= new ExclusivePositionUpdater(CATEGORY); private Stack<BracketLevel> fBracketLevelStack= new Stack<BracketLevel>(); private final ISourceViewer sourceViewer; private final IEditorPart editor; private static HashMap<String, String> quotes; private static final Pattern SIMPLE_COMMAND_PATTERN = Pattern.compile("\\\\.\\{\\\\?\\w\\}"); public BracketInserter(ISourceViewer viewer, IEditorPart editor) { this.sourceViewer = viewer; this.editor = editor; if (quotes == null) { quotes = new HashMap<String, String>(); quotes.put("eno", "``"); quotes.put("enc", "''"); quotes.put("fio", "''"); quotes.put("fic", "''"); quotes.put("fro", "\"<"); quotes.put("frc", "\">"); quotes.put("deo", "\"`"); quotes.put("dec", "\"'"); } } /** * Tests if the given character is a bracket or paren * * @param c Chracter to test * @return True if <code>c</code> is a bracket or paren, false otherwise */ private static boolean isClosingBracket(char c){ if (c == ')' || c == '}' || c == ']' || c == '$') return true; return false; } /** * Returns the opposing paren, e.g. returns ')' for '(' * * @param character A bracket or paren * @return The opposing bracket or paren */ private static char getPeerCharacter(char character) { switch (character) { case '(': return ')'; case ')': return '('; case '{': return '}'; case '}': return '{'; case '[': return ']'; case '$': return '$'; case ']': return '['; default: return 0; } } /** * Returns true if <i>next</i> is a character that could stand * behind a closing quotation mark instead of a white space * @param next * @return */ private boolean isLikePunctuationMark(char next) { switch (next) { case '.': case ',': case '!': case '?': case ';': case ':': case '-': case ')': case ']': case '}': case '=': return true; default: return false; } } /** * Returns true if <i>next</i> is a character that could stand * before an opening quotation mark instead of a white space * @param next * @return */ private boolean isLikeOpeningBrace(char next) { switch (next) { case '(': case '[': case '{': case '~': case '=': case ':': return true; default: return false; } } /** * Gets the quote wanted for the current language * * @param opening * True if the opening quote is wanted, false if the closing * quote is wanted * @return String containing the quotes as TeX code */ private String getQuotes (boolean opening){ String replacement; IProject project = ((FileEditorInput)editor.getEditorInput()).getFile().getProject(); String lang = TexlipseProperties.getProjectProperty(project, TexlipseProperties.LANGUAGE_PROPERTY); String postfix = opening ? "o" : "c"; replacement = quotes.get(lang + postfix); return (replacement != null ? replacement : quotes.get("en" + postfix)); } /* * @see org.eclipse.swt.custom.VerifyKeyListener#verifyKey(org.eclipse.swt.events.VerifyEvent) */ public void verifyKey(VerifyEvent event) { // TODO separate math mode from normal typing? // early pruning to slow down normal typing as little as possible if (!event.doit) return; switch (event.character) { case '(': case '{': case '[': case '$': case '"': case '.': case '\b': break; default: return; } IDocument document = sourceViewer.getDocument(); final Point selection = sourceViewer.getSelectedRange(); final int offset = selection.x; final int length = selection.y; final char character = event.character; try { if (document instanceof IDocumentExtension3) { try { String contentType = ((IDocumentExtension3) document).getContentType( TexEditor.TEX_PARTITIONING, offset, false); if (FastLaTeXPartitionScanner.TEX_VERBATIM.equals(contentType)) { //No features inside verbatim environments return; } } catch (BadPartitioningException e) { TexlipsePlugin.log("Bad partitioning", e); } } char next = ' '; char last = ' '; try { if (offset > 0) last = document.getChar(offset-1); next = document.getChar(offset); } catch (BadLocationException e) { //Could happen if this is the beginning or end of a document } if (last == '\\') return; if (character == '"'){ //Replace quotation marks if (!TexlipsePlugin.getDefault().getPreferenceStore().getBoolean(TexlipseProperties.SMART_QUOTES)) return; String mark; if (Character.isWhitespace(last) || isLikeOpeningBrace(last)) { mark = getQuotes(true); } else if (Character.isWhitespace(next) || isLikePunctuationMark(next)) { mark = getQuotes(false); } else { return; } document.replace(offset, length, mark); sourceViewer.setSelectedRange(offset+mark.length(), 0); event.doit = false; return; } // Smart backspace if (character == '\b') { if (!TexlipsePlugin.getDefault().getPreferenceStore().getBoolean(TexlipseProperties.SMART_BACKSPACE)) { return; } if (last == '}' && offset > 4) { // \={o} or \'{\i} int distance; if (document.getChar(offset-5) == '\\') { distance = 5; } else if (offset > 5 && document.getChar(offset-6) == '\\') { distance = 6; } else { return; } String deletion = document.get(offset - distance, distance); Matcher m = SIMPLE_COMMAND_PATTERN.matcher(deletion); if (m.matches()) { document.replace(offset - distance, distance, ""); event.doit = false; } } else if (Character.isLetter(last)) { // FIXME can't handle unicode // \'a if (offset > 2 && document.getChar(offset-3) == '\\') { // "\\\\\\W\\w" if (!Character.isLetter(document.getChar(offset-2))) { document.replace(offset - 3, 3, ""); event.doit = false; } } } return; } // Smart \ldots if (character == '.') { if (!TexlipsePlugin.getDefault().getPreferenceStore().getBoolean(TexlipseProperties.SMART_LDOTS)) { return; } if (last == '.' && document.getChar(offset-2) == '.') { String replacement = "\\ldots"; document.replace(offset-2, length+2, replacement); sourceViewer.setSelectedRange(offset + replacement.length() - 2, 0); event.doit = false; } return; } // Smart parens if (!TexlipsePlugin.getDefault().getPreferenceStore().getBoolean(TexlipseProperties.SMART_PARENS)) return; if (Character.isWhitespace(next) || isClosingBracket(next)){ //For a dollar sign we need a whitespace before and after the letter if (character == '$' && !Character.isWhitespace(last)) return; } else { return; } boolean left = false; if (last == 't'){ //Maybe we have \left then we will also append \right try{ String prev = document.get(offset - 6, 6); if (prev.charAt(0) != '\\' && "\\left".equals(prev.substring(1))) left = true; } catch (BadLocationException e) { //Could happen, no worry } } final char closingCharacter= getPeerCharacter(character); final StringBuffer buffer= new StringBuffer(); buffer.append(character); if (left) buffer.append("\\right"); buffer.append(closingCharacter); document.replace(offset, length, buffer.toString()); // The code below does the fancy "templateish" enter-to-exit-braces BracketLevel level= new BracketLevel(); fBracketLevelStack.push(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.fOffset= offset; level.fLength= 2; if (left) level.fLength += 6; // set up position tracking for our magic peers if (fBracketLevelStack.size() == 1) { document.addPositionCategory(CATEGORY); document.addPositionUpdater(fUpdater); } level.fFirstPosition= new Position(offset, 1); level.fSecondPosition= new Position(offset + 1, level.fLength - 1); document.addPosition(CATEGORY, level.fFirstPosition); document.addPosition(CATEGORY, level.fSecondPosition); level.fUI= new EditorLinkedModeUI(model, sourceViewer); level.fUI.setSimpleMode(true); level.fUI.setExitPolicy(new ExitPolicy(closingCharacter, (char)0, fBracketLevelStack, sourceViewer)); level.fUI.setExitPosition(sourceViewer, offset + level.fLength, 0, Integer.MAX_VALUE); level.fUI.setCyclingMode(LinkedModeUI.CYCLE_NEVER); level.fUI.enter(); IRegion newSelection= level.fUI.getSelectedRegion(); sourceViewer.setSelectedRange(newSelection.getOffset(), newSelection.getLength()); event.doit= false; } catch (BadLocationException e) { } catch (BadPositionCategoryException e) { } } /* * @see org.eclipse.jface.text.link.ILinkedModeListener#left(org.eclipse.jface.text.link.LinkedModeModel, int) */ public void left(LinkedModeModel environment, int flags) { final BracketLevel level= fBracketLevelStack.pop(); 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() { public void perform(IDocument d, IDocumentListener owner) { if ((level.fFirstPosition.isDeleted || level.fFirstPosition.length == 0) && !level.fSecondPosition.isDeleted && level.fSecondPosition.offset == level.fFirstPosition.offset) { try { document.replace(level.fSecondPosition.offset, level.fSecondPosition.length, null); } catch (BadLocationException e) { //JavaPlugin.log(e); } } if (fBracketLevelStack.size() == 0) { document.removePositionUpdater(fUpdater); try { document.removePositionCategory(CATEGORY); } catch (BadPositionCategoryException e) { //JavaPlugin.log(e); } } } }); } } /* * @see org.eclipse.jface.text.link.ILinkedModeListener#suspend(org.eclipse.jface.text.link.LinkedModeModel) */ public void suspend(LinkedModeModel environment) { } /* * @see org.eclipse.jface.text.link.ILinkedModeListener#resume(org.eclipse.jface.text.link.LinkedModeModel, int) */ public void resume(LinkedModeModel environment, int flags) { } }