diff --git a/core/org.eclipse.cdt.ui.tests/ui/org/eclipse/cdt/ui/tests/text/HyperlinkTest.java b/core/org.eclipse.cdt.ui.tests/ui/org/eclipse/cdt/ui/tests/text/HyperlinkTest.java index f3290e9892d..32eaf4cddaa 100644 --- a/core/org.eclipse.cdt.ui.tests/ui/org/eclipse/cdt/ui/tests/text/HyperlinkTest.java +++ b/core/org.eclipse.cdt.ui.tests/ui/org/eclipse/cdt/ui/tests/text/HyperlinkTest.java @@ -48,7 +48,8 @@ import org.eclipse.cdt.internal.ui.editor.CElementHyperlinkDetector; * @author Mike Kucera */ public class HyperlinkTest extends TestCase { - private static final String CPP_FILE_NAME = "hyperlink_test_cpp.cpp"; + private static final String CPP_FILE_NAME_1 = "hyperlink_test_cpp.cpp"; + private static final String CPP_FILE_NAME_2 = "hyperlink_test_cpp_without_ast.cpp"; private static final String CPP_CODE = "#include \n" + "#define SOMEMACRO macro_token1 macro_token2 \n" + @@ -91,6 +92,10 @@ public class HyperlinkTest extends TestCase { } private void setUpEditor(String fileName, String code) throws Exception { + setUpEditor(fileName, code, true); + } + + private void setUpEditor(String fileName, String code, boolean buildAST) throws Exception { super.setUp(); project= CProjectHelper.createCCProject(super.getName(), "unused", IPDOMManager.ID_NO_INDEXER); ICContainer cContainer= CProjectHelper.addCContainer(project, "src"); @@ -100,15 +105,17 @@ public class HyperlinkTest extends TestCase { assertTrue(file.exists()); editor = (CEditor) EditorTestHelper.openInEditor(file, true); EditorTestHelper.joinReconciler(EditorTestHelper.getSourceViewer(editor), 10, 1000, 10); - // Since CElementHyperlinkDetector doesn't wait for an AST to be created, - // we trigger AST creation ahead of time. - IWorkingCopyManager manager = CUIPlugin.getDefault().getWorkingCopyManager(); - IWorkingCopy workingCopy = manager.getWorkingCopy(editor.getEditorInput()); - IStatus status= ASTProvider.getASTProvider().runOnAST(workingCopy, ASTProvider.WAIT_IF_OPEN, null, new ASTRunnable() { - public IStatus runOnAST(ILanguage lang, IASTTranslationUnit ast) { - return Status.OK_STATUS; - } - }); + if (buildAST) { + // Since CElementHyperlinkDetector doesn't wait for an AST to be created, + // we trigger AST creation ahead of time. + IWorkingCopyManager manager = CUIPlugin.getDefault().getWorkingCopyManager(); + IWorkingCopy workingCopy = manager.getWorkingCopy(editor.getEditorInput()); + IStatus status= ASTProvider.getASTProvider().runOnAST(workingCopy, ASTProvider.WAIT_IF_OPEN, null, new ASTRunnable() { + public IStatus runOnAST(ILanguage lang, IASTTranslationUnit ast) { + return Status.OK_STATUS; + } + }); + } } @Override @@ -140,7 +147,7 @@ public class HyperlinkTest extends TestCase { public void testHyperlinksCpp() throws Exception { // entire include highlighted - setUpEditor(CPP_FILE_NAME, CPP_CODE); + setUpEditor(CPP_FILE_NAME_1, CPP_CODE, true); assertHyperlink(CPP_CODE.indexOf("#include") + 2, 0, "#include ".length()); assertHyperlink(CPP_CODE.indexOf("") + 2, 0, "#include ".length()); @@ -192,8 +199,63 @@ public class HyperlinkTest extends TestCase { assertHyperlink(CPP_CODE.indexOf("+ p"), CPP_CODE.indexOf("+ p"), 1); } + public void testHyperlinksCppWithoutAST() throws Exception { + // entire include highlighted + setUpEditor(CPP_FILE_NAME_2, CPP_CODE, false); + + assertHyperlink(CPP_CODE.indexOf("#include") + 2, 0, "#include ".length()); + assertHyperlink(CPP_CODE.indexOf("") + 2, 0, "#include ".length()); + assertHyperlink(CPP_CODE.indexOf("") + "".length()); + + // hovering over the whitespace inside an include still results in a hyperlink + assertHyperlink(CPP_CODE.indexOf("") - 1, 0, "#include ".length()); + + // no hyperlinks in macro bodies + assertNotHyperlink(CPP_CODE.indexOf("#define") + 1); + assertHyperlink(CPP_CODE.indexOf("SOMEMACRO"), CPP_CODE.indexOf("SOMEMACRO"), "SOMEMACRO".length()); + // see bug 259015 +// assertNotHyperlink(CPP_CODE.indexOf("macro_token1") + 1); +// assertNotHyperlink(CPP_CODE.indexOf("macro_token2") + 1); + + // no hyperlinks for comments + assertNotHyperlink(CPP_CODE.indexOf("//") + 1); + assertNotHyperlink(CPP_CODE.indexOf("COMMENT") + 1); + + // no hyperlinks for keywords + assertNotHyperlink(CPP_CODE.indexOf("class") + 1); + assertNotHyperlink(CPP_CODE.indexOf("public") + 1); + assertNotHyperlink(CPP_CODE.indexOf("private") + 1); + assertNotHyperlink(CPP_CODE.indexOf("int x") + 1); + assertNotHyperlink(CPP_CODE.indexOf("char") + 1); + assertNotHyperlink(CPP_CODE.indexOf("void") + 1); + + // no hyperlinks for punctuation + assertNotHyperlink(CPP_CODE.indexOf("{")); + assertNotHyperlink(CPP_CODE.indexOf("}")); + assertNotHyperlink(CPP_CODE.indexOf("(" + 1)); + assertNotHyperlink(CPP_CODE.indexOf(")")); + assertNotHyperlink(CPP_CODE.indexOf(":")); + assertNotHyperlink(CPP_CODE.indexOf(";")); + + // no hyperlinks inside strings + assertNotHyperlink(CPP_CODE.indexOf("STRING") + 1); + assertNotHyperlink(CPP_CODE.indexOf("STRING") + 6); + assertNotHyperlink(CPP_CODE.indexOf("LITERAL") + 1); + + assertHyperlink(CPP_CODE.indexOf("Point {") + 1, CPP_CODE.indexOf("Point {"), "Point".length()); + assertHyperlink(CPP_CODE.indexOf("Point()") + 1, CPP_CODE.indexOf("Point()"), "Point".length()); + // No-AST hyperlink detection doesn't know that tilde is part of the destructor name. + assertHyperlink(CPP_CODE.indexOf("~Point()") + 1, CPP_CODE.indexOf("~Point()") + 1, "Point".length()); + assertHyperlink(CPP_CODE.indexOf("set(") + 1, CPP_CODE.indexOf("set("), "set".length()); + assertHyperlink(CPP_CODE.indexOf("getX()") + 1, CPP_CODE.indexOf("getX()"), "getX".length()); + assertHyperlink(CPP_CODE.indexOf("getY()") + 1, CPP_CODE.indexOf("getY()"), "getY".length()); + + // Overloaded operators are not hyperlinked when AST is not available. + //assertHyperlink(CPP_CODE.indexOf("+ p"), CPP_CODE.indexOf("+ p"), 1); + } + public void testHyperlinksCKeywords() throws Exception { - setUpEditor(C_FILE_NAME_1, C_CODE_1); + setUpEditor(C_FILE_NAME_1, C_CODE_1, true); // 'class' is not a keyword in C, it should be hyperlinked assertHyperlink(C_CODE_1.indexOf("class") + 1, C_CODE_1.indexOf("class"), "class".length()); @@ -203,7 +265,7 @@ public class HyperlinkTest extends TestCase { } public void testHyperlinksInactiveCode() throws Exception { - setUpEditor(C_FILE_NAME_2, C_CODE_2); + setUpEditor(C_FILE_NAME_2, C_CODE_2, true); assertNotHyperlink(C_CODE_2.indexOf("#ifdef") + 2); assertNotHyperlink(C_CODE_2.indexOf("#else") + 2); diff --git a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/editor/CElementHyperlinkDetector.java b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/editor/CElementHyperlinkDetector.java index 7d5b262a3f4..87c2b68765b 100644 --- a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/editor/CElementHyperlinkDetector.java +++ b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/editor/CElementHyperlinkDetector.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2009 QNX Software Systems and others. + * Copyright (c) 2000, 2010 QNX Software Systems and others. * 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 @@ -9,11 +9,11 @@ * QNX Software Systems - Initial API and implementation * IBM Corporation * Markus Schorn (Wind River Systems) + * Sergey Prigogin (Google) *******************************************************************************/ package org.eclipse.cdt.internal.ui.editor; -import java.util.Arrays; - +import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.action.IAction; @@ -40,6 +40,8 @@ import org.eclipse.cdt.ui.CUIPlugin; import org.eclipse.cdt.ui.text.ICPartitions; import org.eclipse.cdt.internal.core.model.ASTCache.ASTRunnable; +import org.eclipse.cdt.internal.formatter.scanner.Scanner; +import org.eclipse.cdt.internal.formatter.scanner.Token; import org.eclipse.cdt.internal.ui.text.CWordFinder; @@ -47,9 +49,9 @@ public class CElementHyperlinkDetector extends AbstractHyperlinkDetector { public CElementHyperlinkDetector() { } - + public IHyperlink[] detectHyperlinks(final ITextViewer textViewer, final IRegion region, boolean canShowMultipleHyperlinks) { - ITextEditor textEditor= (ITextEditor)getAdapter(ITextEditor.class); + ITextEditor textEditor= (ITextEditor) getAdapter(ITextEditor.class); if (region == null || !(textEditor instanceof CEditor)) return null; @@ -57,97 +59,140 @@ public class CElementHyperlinkDetector extends AbstractHyperlinkDetector { if (openAction == null) return null; - // check partition type - try { - String partitionType= TextUtilities.getContentType(textViewer.getDocument(), ICPartitions.C_PARTITIONING, region.getOffset(), false); - if (!IDocument.DEFAULT_CONTENT_TYPE.equals(partitionType) && !ICPartitions.C_PREPROCESSOR.equals(partitionType)) { - return null; - } - } catch (BadLocationException exc) { - return null; - } - + IDocument document = textViewer.getDocument(); final IWorkingCopy workingCopy = CUIPlugin.getDefault().getWorkingCopyManager().getWorkingCopy(textEditor.getEditorInput()); if (workingCopy == null) { return null; } - final IHyperlink[] result= {null}; + final IRegion[] hyperlinkRegion = { null }; // Do not wait for AST if it's not available yet. Waiting for AST would block the UI thread // for the duration of the parsing. IStatus status= ASTProvider.getASTProvider().runOnAST(workingCopy, ASTProvider.WAIT_NO, null, new ASTRunnable() { public IStatus runOnAST(ILanguage lang, IASTTranslationUnit ast) { - if (ast != null) { - final int offset= region.getOffset(); - final int length= Math.max(1, region.getLength()); - final IASTNodeSelector nodeSelector= ast.getNodeSelector(null); - IASTName selectedName= nodeSelector.findEnclosingName(offset, length); - IASTFileLocation linkLocation= null; - if (selectedName != null) { // found a name - // prefer include statement over the include name - if (selectedName.getParent() instanceof IASTPreprocessorIncludeStatement) { - linkLocation= selectedName.getParent().getFileLocation(); - } - else { - linkLocation= selectedName.getFileLocation(); - } - } - else { - final IASTNode implicit = nodeSelector.findEnclosingImplicitName(offset, length); - if(implicit != null) { - linkLocation = implicit.getFileLocation(); - } - else { - // search for include statement - final IASTNode cand= nodeSelector.findEnclosingNode(offset, length); - if (cand instanceof IASTPreprocessorIncludeStatement) { - linkLocation= cand.getFileLocation(); - } - } - } - if (linkLocation != null) { - result[0]= new CElementHyperlink( - new Region(linkLocation.getNodeOffset(), linkLocation.getNodeLength()), openAction); + if (ast == null) + return Status.CANCEL_STATUS; + final int offset= region.getOffset(); + final int length= Math.max(1, region.getLength()); + final IASTNodeSelector nodeSelector= ast.getNodeSelector(null); + IASTName selectedName= nodeSelector.findEnclosingName(offset, length); + IASTFileLocation linkLocation= null; + if (selectedName != null) { // found a name + // Prefer include statement over the include name + if (selectedName.getParent() instanceof IASTPreprocessorIncludeStatement) { + linkLocation= selectedName.getParent().getFileLocation(); } else { - // consider fallback navigation - final IDocument document= textViewer.getDocument(); - IRegion wordRegion= CWordFinder.findWord(document, offset); - if (wordRegion != null) { - try { - String word = document.get(wordRegion.getOffset(), wordRegion.getLength()); - if(word.length() > 0 && !Character.isDigit(word.charAt(0)) && !isLanguageKeyword(lang, word)) { - result[0]= new CElementHyperlink( - new Region(wordRegion.getOffset(), wordRegion.getLength()), openAction); - } - } catch (BadLocationException exc) { - // ignore - } + linkLocation= selectedName.getFileLocation(); + } + } else { + final IASTNode implicit = nodeSelector.findEnclosingImplicitName(offset, length); + if (implicit != null) { + linkLocation = implicit.getFileLocation(); + } else { + // Search for include statement + final IASTNode cand= nodeSelector.findEnclosingNode(offset, length); + if (cand instanceof IASTPreprocessorIncludeStatement) { + linkLocation= cand.getFileLocation(); } } } + if (linkLocation != null) { + hyperlinkRegion[0] = new Region(linkLocation.getNodeOffset(), linkLocation.getNodeLength()); + } return Status.OK_STATUS; } }); - if (!status.isOK()) { - CUIPlugin.log(status); + + if (status == Status.CANCEL_STATUS) { + // AST is not available yet, try to compute the hyperlink without it. + try { + // Check partition type. + String partitionType= TextUtilities.getContentType(document, ICPartitions.C_PARTITIONING, region.getOffset(), false); + if (IDocument.DEFAULT_CONTENT_TYPE.equals(partitionType)) { + // Regular code. + hyperlinkRegion[0] = getIdentifier(document, region.getOffset(), workingCopy.getLanguage()); + } else if (ICPartitions.C_PREPROCESSOR.equals(partitionType)) { + // Preprocessor directive. + Scanner scanner= new Scanner(); + scanner.setSplitPreprocessor(true); + scanner.setSource(document.get().toCharArray()); + scanner.setCurrentPosition(findPreprocessorDirectiveStart(document, region.getOffset())); + Token token = scanner.nextToken(); + if (token.getType() == Token.tPREPROCESSOR_INCLUDE) { + int endPos = token.getOffset() + token.getLength(); + // Trim trailing whitespace. + while (Character.isWhitespace(document.getChar(--endPos))) { + } + endPos++; + if (region.getOffset() <= endPos) { + hyperlinkRegion[0] = new Region(token.getOffset(), endPos - token.getOffset()); + } + } else { + hyperlinkRegion[0] = getIdentifier(document, region.getOffset(), workingCopy.getLanguage()); + } + } + } catch (BadLocationException e) { + // Ignore to return null. + } catch (CoreException e) { + // Ignore to return null. + } } - - return result[0] == null ? null : result; + if (hyperlinkRegion[0] == null) + return null; + return new IHyperlink[] { new CElementHyperlink(hyperlinkRegion[0], openAction) }; + } + + /** + * Returns the identifier at the given offset, or {@code null} if the there is no identifier + * at the offset. + */ + private static IRegion getIdentifier(IDocument document, int offset, ILanguage language) throws BadLocationException { + IRegion wordRegion= CWordFinder.findWord(document, offset); + if (wordRegion != null && wordRegion.getLength() > 0) { + String word = document.get(wordRegion.getOffset(), wordRegion.getLength()); + if (!Character.isDigit(word.charAt(0)) && !isLanguageKeyword(language, word)) { + return wordRegion; + } + } + return null; } private static boolean isLanguageKeyword(ILanguage lang, String word) { ICLanguageKeywords keywords= (ICLanguageKeywords) lang.getAdapter(ICLanguageKeywords.class); if (keywords != null) { - if (Arrays.asList(keywords.getKeywords()).contains(word)) { - return true; + for (String keyword : keywords.getKeywords()) { + if (keyword.equals(word)) + return true; } - if (Arrays.asList(keywords.getBuiltinTypes()).contains(word)) { - return true; + for (String type : keywords.getBuiltinTypes()) { + if (type.equals(word)) + return true; } - if (Arrays.asList(keywords.getPreprocessorKeywords()).contains('#'+word)) { - return true; + for (String keyword : keywords.getPreprocessorKeywords()) { + if (keyword.charAt(0) == '#' && keyword.regionMatches(1, word, 0, word.length())) + return true; } } return false; } + + /** + * Finds beginning of a preprocessor directive. + */ + private static int findPreprocessorDirectiveStart(IDocument document, int offset) throws BadLocationException { + while (true) { + IRegion lineRegion = document.getLineInformationOfOffset(offset); + int lineOffset = lineRegion.getOffset(); + if (lineOffset == 0) + return lineOffset; + int lineEnd = lineOffset + lineRegion.getLength(); + for (offset = lineOffset; offset < lineEnd && Character.isWhitespace(document.getChar(offset)); offset++) { + } + if (offset < document.getLength() && document.getChar(offset) == '#') { + return lineOffset; + } + // The line doesn't start with #, try previous line. + offset = lineOffset - 1; + } + } }