/** * Aptana Studio * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions). * Please see the license.html included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package com.aptana.editor.ruby; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.ui.internal.editors.text.EditorsPlugin; import org.eclipse.ui.texteditor.ChainedPreferenceStore; import org.eclipse.ui.texteditor.IDocumentProvider; import com.aptana.core.logging.IdeLog; import com.aptana.editor.common.AbstractThemeableEditor; import com.aptana.editor.common.CommonEditorPlugin; import com.aptana.editor.common.outline.CommonOutlineItem; import com.aptana.editor.common.text.reconciler.IFoldingComputer; import com.aptana.editor.ruby.internal.text.RubyFoldingComputer; import com.aptana.editor.ruby.outline.RubyOutlineContentProvider; import com.aptana.editor.ruby.outline.RubyOutlineLabelProvider; import com.aptana.parsing.ast.IParseNode; import com.aptana.parsing.lexer.IRange; import com.aptana.ruby.core.IImportContainer; import com.aptana.ruby.core.IRubyConstants; import com.aptana.ruby.core.IRubyElement; import com.aptana.ruby.core.IRubyMethod; import com.aptana.ruby.core.IRubyType; @SuppressWarnings("restriction") public class RubySourceEditor extends AbstractThemeableEditor { private static final char[] PAIR_MATCHING_CHARS = new char[] { '(', ')', '{', '}', '[', ']', '`', '`', '\'', '\'', '"', '"', '|', '|', '\u201C', '\u201D', '\u2018', '\u2019' }; // curly double quotes, curly single quotes private Map<Annotation, Position> fTagPairOccurrences; private boolean fIncludeBlocks; @Override protected void initializeEditor() { super.initializeEditor(); setPreferenceStore(getChainedPreferenceStore()); setSourceViewerConfiguration(new RubySourceViewerConfiguration(getPreferenceStore(), this)); setDocumentProvider(RubyEditorPlugin.getDefault().getRubyDocumentProvider()); } public static IPreferenceStore getChainedPreferenceStore() { return new ChainedPreferenceStore(new IPreferenceStore[] { RubyEditorPlugin.getDefault().getPreferenceStore(), CommonEditorPlugin.getDefault().getPreferenceStore(), EditorsPlugin.getDefault().getPreferenceStore() }); } public char[] getPairMatchingCharacters() { return PAIR_MATCHING_CHARS; } @Override public ITreeContentProvider getOutlineContentProvider() { return new RubyOutlineContentProvider(); } @Override public ILabelProvider getOutlineLabelProvider() { return new RubyOutlineLabelProvider(); } @Override protected void setSelectedElement(IRange element) { if (element instanceof CommonOutlineItem) { IParseNode node = ((CommonOutlineItem) element).getReferenceNode(); if (node instanceof IImportContainer) { // just sets the highlight range and moves the cursor setHighlightRange(element.getStartingOffset(), element.getLength(), true); return; } } super.setSelectedElement(element); } @Override protected void selectionChanged() { super.selectionChanged(); ISelection selection = getSelectionProvider().getSelection(); if (selection.isEmpty()) { return; } ITextSelection textSelection = (ITextSelection) selection; updateOccurrences(textSelection); } @Override protected Object getOutlineElementAt(int caret) { fIncludeBlocks = false; Object obj = super.getOutlineElementAt(caret); fIncludeBlocks = true; return obj; } protected IParseNode getASTNodeAt(int offset) { IParseNode root = getAST(); if (root == null) { return null; } IParseNode node = root.getNodeAtOffset(offset); if (!fIncludeBlocks && node != null && node.getNodeType() == IRubyElement.BLOCK) { node = node.getParent(); } return node; } private void updateOccurrences(ITextSelection textSelection) { IDocumentProvider documentProvider = getDocumentProvider(); if (documentProvider == null) { return; } IAnnotationModel annotationModel = documentProvider.getAnnotationModel(getEditorInput()); if (annotationModel == null) { return; } int offset = textSelection.getOffset(); IParseNode currentNode = getASTNodeAt(offset); if (fTagPairOccurrences != null) { // if the offset is included by one of these two positions, we don't need to wipe and re-calculate! for (Position pos : fTagPairOccurrences.values()) { if (pos.includes(offset)) { return; } } // New position, wipe the existing annotations in preparation for re-calculating... for (Annotation a : fTagPairOccurrences.keySet()) { annotationModel.removeAnnotation(a); } fTagPairOccurrences = null; } // Calculate current pair Map<Annotation, Position> occurrences = new HashMap<Annotation, Position>(); List<Position> positions = new ArrayList<Position>(); if (currentNode != null) { if (currentNode instanceof IRubyType) { // Match "end" to "class/module ..." int endOffset = currentNode.getEndingOffset(); int startOffset = currentNode.getStartingOffset(); int length = 5; IRubyType type = (IRubyType) currentNode; if (type.isModule()) { length = 6; } if ((offset <= endOffset && offset >= endOffset - 2) || (offset >= startOffset && offset <= startOffset + length)) { positions.add(new Position(startOffset, length)); positions.add(new Position(endOffset - 2, 3)); } } else if (currentNode instanceof IRubyMethod) { // Match "end" to "def ..." int endOffset = currentNode.getEndingOffset(); int startOffset = currentNode.getStartingOffset(); if ((offset <= endOffset && offset >= endOffset - 2) || (offset >= startOffset && offset <= startOffset + 3)) { positions.add(new Position(startOffset, 3)); positions.add(new Position(endOffset - 2, 3)); } } else if (currentNode.getNodeType() == IRubyElement.BLOCK) { // Match "end" to "do ..." only if it's a do/end block int endOffset = currentNode.getEndingOffset(); IDocument document = getSourceViewer().getDocument(); if (endOffset >= document.getLength()) { endOffset--; } char endText = 'a'; try { endText = document.getChar(endOffset); } catch (BadLocationException e) { IdeLog.logError(RubyEditorPlugin.getDefault(), "Unable to get text at end of block, end offset: " //$NON-NLS-1$ + endOffset, e); } if (endText == 'd') { int startOffset = currentNode.getStartingOffset(); if ((offset <= endOffset && offset >= endOffset - 2) || (offset >= startOffset && offset <= startOffset + 3)) { positions.add(new Position(startOffset, 2)); positions.add(new Position(endOffset - 2, 3)); } } } // else if (currentNode instanceof IRubyField) // { // TODO Find occurrences of variables! // } // TODO Also match if/else/unless/begin/rescue/end blocks! } if (!positions.isEmpty()) { for (Position pos : positions) { occurrences.put(new Annotation(IRubyConstants.BLOCK_PAIR_OCCURRENCES_ID, false, null), pos); } for (Map.Entry<Annotation, Position> entry : occurrences.entrySet()) { annotationModel.addAnnotation(entry.getKey(), entry.getValue()); } fTagPairOccurrences = occurrences; } else { // no new pair, so don't highlight anything fTagPairOccurrences = null; } } /* * (non-Javadoc) * @see com.aptana.editor.common.AbstractThemeableEditor#getPluginPreferenceStore() */ @Override protected IPreferenceStore getPluginPreferenceStore() { return RubyEditorPlugin.getDefault().getPreferenceStore(); } @Override public IFoldingComputer createFoldingComputer(IDocument document) { return new RubyFoldingComputer(this, document); } @Override public String getContentType() { return IRubyConstants.CONTENT_TYPE_RUBY; } }