content: /**
   *  Copyright (c) 2015-2016 Angelo ZERR.
   *  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
   *
   *  Contributors:
   *  Angelo Zerr <angelo.zerr@gmail.com> - initial API and implementation
   */
  package ts.eclipse.ide.jsdt.internal.ui.editor;
  
  import java.util.Stack;
  
  import org.eclipse.jface.text.BadLocationException;
  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.IDocumentListener;
  import org.eclipse.jface.text.IPositionUpdater;
  import org.eclipse.jface.text.IRegion;
  import org.eclipse.jface.text.ITypedRegion;
  import org.eclipse.jface.text.Position;
  import org.eclipse.jface.text.TextUtilities;
  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.LinkedModeUI.ExitFlags;
  import org.eclipse.jface.text.link.LinkedModeUI.IExitPolicy;
  import org.eclipse.jface.text.link.LinkedPosition;
  import org.eclipse.jface.text.link.LinkedPositionGroup;
  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.texteditor.ITextEditorExtension3;
  import org.eclipse.ui.texteditor.ITextEditorExtension3.InsertMode;
  import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
  import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin;
  import org.eclipse.wst.jsdt.internal.ui.text.JavaHeuristicScanner;
  import org.eclipse.wst.jsdt.internal.ui.text.Symbols;
  import org.eclipse.wst.jsdt.ui.text.IJavaScriptPartitions;
  
  public class BracketInserter implements VerifyKeyListener, ILinkedModeListener {
  
  	private final JavaScriptLightWeightEditor editor;
  
  	private class ExitPolicy implements IExitPolicy {
  
  		final char fExitCharacter;
  		final char fEscapeCharacter;
  		final Stack fStack;
  		final int fSize;
  
  		public ExitPolicy(char exitCharacter, char escapeCharacter, Stack stack) {
  			fExitCharacter = exitCharacter;
  			fEscapeCharacter = escapeCharacter;
  			fStack = stack;
  			fSize = fStack.size();
  		}
  
  		/*
  		 * @see org.eclipse.wst.jsdt.internal.ui.text.link.LinkedPositionUI.
  		 * ExitPolicy#doExit(org.eclipse.wst.jsdt.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 = getSourceViewer().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 = getSourceViewer().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.
  	 *
  	 * 
  	 */
  	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;
  		}
  
  	}
  
  	public BracketInserter(JavaScriptLightWeightEditor editor) {
  		this.editor = editor;
  	}
  
  	private boolean fCloseBrackets = true;
  	private boolean fCloseStrings = true;
  	private boolean fCloseAngularBrackets = true;
  	private final String CATEGORY = toString();
  	private IPositionUpdater fUpdater = new ExclusivePositionUpdater(CATEGORY);
  	private Stack fBracketLevelStack = new Stack();
  
  	public void setCloseBracketsEnabled(boolean enabled) {
  		fCloseBrackets = enabled;
  	}
  
  	public void setCloseStringsEnabled(boolean enabled) {
  		fCloseStrings = enabled;
  	}
  
  	public void setCloseAngularBracketsEnabled(boolean enabled) {
  		fCloseAngularBrackets = enabled;
  	}
  
  	private boolean isAngularIntroducer(String identifier) {
  		return identifier.length() > 0 && (Character.isUpperCase(identifier.charAt(0)) || identifier.startsWith("final") //$NON-NLS-1$
  				|| identifier.startsWith("public") //$NON-NLS-1$
  				|| identifier.startsWith("public") //$NON-NLS-1$
  				|| identifier.startsWith("protected") //$NON-NLS-1$
  				|| identifier.startsWith("private")); //$NON-NLS-1$
  	}
  
  	/*
  	 * @see
  	 * org.eclipse.swt.custom.VerifyKeyListener#verifyKey(org.eclipse.swt.events
  	 * .VerifyEvent)
  	 */
  	public void verifyKey(VerifyEvent event) {
  
  		// early pruning to slow down normal typing as little as possible
  		if (!event.doit || getInsertMode() != ITextEditorExtension3.SMART_INSERT)
  			return;
  		switch (event.character) {
  		case '(':
  		case '<':
  		case '[':
  		case '\'':
  		case '\"':
  		case '{':
  			break;
  		default:
  			return;
  		}
  
  		final ISourceViewer sourceViewer = getSourceViewer();
  		IDocument document = sourceViewer.getDocument();
  
  		final Point selection = sourceViewer.getSelectedRange();
  		final int offset = selection.x;
  		final int length = selection.y;
  
  		try {
  			IRegion startLine = document.getLineInformationOfOffset(offset);
  			IRegion endLine = document.getLineInformationOfOffset(offset + length);
  
  			JavaHeuristicScanner scanner = new JavaHeuristicScanner(document);
  			int nextToken = scanner.nextToken(offset + length, endLine.getOffset() + endLine.getLength());
  			String next = nextToken == Symbols.TokenEOF ? null
  					: document.get(offset, scanner.getPosition() - offset).trim();
  			int prevToken = scanner.previousToken(offset - 1, startLine.getOffset());
  			int prevTokenOffset = scanner.getPosition() + 1;
  			String previous = prevToken == Symbols.TokenEOF ? null
  					: document.get(prevTokenOffset, offset - prevTokenOffset).trim();
  
  			switch (event.character) {
  			case '(':
  				if (!fCloseBrackets || nextToken == Symbols.TokenLPAREN || nextToken == Symbols.TokenIDENT
  						|| next != null && next.length() > 1)
  					return;
  				break;
  
  			case '<':
  				if (!(fCloseAngularBrackets && fCloseBrackets) || nextToken == Symbols.TokenLESSTHAN
  						|| prevToken != Symbols.TokenLBRACE && prevToken != Symbols.TokenRBRACE
  								&& prevToken != Symbols.TokenSEMICOLON && prevToken != Symbols.TokenSYNCHRONIZED
  								&& prevToken != Symbols.TokenSTATIC
  								&& (prevToken != Symbols.TokenIDENT || !isAngularIntroducer(previous))
  								&& prevToken != Symbols.TokenEOF)
  					return;
  				break;
  
  			case '{':
  			case '[':
  				if (!fCloseBrackets || nextToken == Symbols.TokenIDENT || next != null && next.length() > 1)
  					return;
  				break;
  
  			case '\'':
  			case '"':
  				if (!fCloseStrings || nextToken == Symbols.TokenIDENT || prevToken == Symbols.TokenIDENT
  						|| next != null && next.length() > 1 || previous != null && previous.length() > 1)
  					return;
  				break;
  
  			default:
  				return;
  			}
  
  			ITypedRegion partition = TextUtilities.getPartition(document, IJavaScriptPartitions.JAVA_PARTITIONING,
  					offset, true);
  			if (!IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType()))
  				return;
  
  			if (!validateEditorInputState())
  				return;
  
  			final char character = event.character;
  			final char closingCharacter = getPeerCharacter(character);
  			final StringBuffer buffer = new StringBuffer();
  			buffer.append(character);
  			buffer.append(closingCharacter);
  
  			document.replace(offset, length, buffer.toString());
  
  			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;
  
  			// 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, 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, getEscapeCharacter(closingCharacter), fBracketLevelStack));
  			level.fUI.setExitPosition(sourceViewer, offset + 2, 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) {
  			JavaScriptPlugin.log(e);
  		} catch (BadPositionCategoryException e) {
  			JavaScriptPlugin.log(e);
  		}
  	}
  
  	private boolean validateEditorInputState() {
  		return editor.validateEditorInputState();
  	}
  
  	private InsertMode getInsertMode() {
  		return editor.getInsertMode();
  	}
  
  	private ISourceViewer getSourceViewer() {
  		return editor.getViewer();
  	}
  
  	/*
  	 * @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 = (BracketLevel) fBracketLevelStack.pop();
  
  		if (flags != ILinkedModeListener.EXTERNAL_MODIFICATION)
  			return;
  
  		// remove brackets
  		final ISourceViewer sourceViewer = getSourceViewer();
  		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, ""); //$NON-NLS-1$
  						} catch (BadLocationException e) {
  							JavaScriptPlugin.log(e);
  						}
  					}
  
  					if (fBracketLevelStack.size() == 0) {
  						document.removePositionUpdater(fUpdater);
  						try {
  							document.removePositionCategory(CATEGORY);
  						} catch (BadPositionCategoryException e) {
  							JavaScriptPlugin.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) {
  	}
  
  	private static char getEscapeCharacter(char character) {
  		switch (character) {
  		case '"':
  		case '\'':
  			return '\\';
  		default:
  			return 0;
  		}
  	}
  
  	private static char getPeerCharacter(char character) {
  		switch (character) {
  		case '(':
  			return ')';
  
  		case ')':
  			return '(';
  
  		case '<':
  			return '>';
  
  		case '>':
  			return '<';
  
  		case '[':
  			return ']';
  
  		case ']':
  			return '[';
  
  		case '{':
  			return '}';
  
  		case '}':
  			return '{';
  
  		case '"':
  			return character;
  
  		case '\'':
  			return character;
  
  		default:
  			throw new IllegalArgumentException();
  		}
  	}
  }