diff --git a/org.eclipse.jdt.ls.core/META-INF/MANIFEST.MF b/org.eclipse.jdt.ls.core/META-INF/MANIFEST.MF index fbcbf6f085..f1a19afd71 100644 --- a/org.eclipse.jdt.ls.core/META-INF/MANIFEST.MF +++ b/org.eclipse.jdt.ls.core/META-INF/MANIFEST.MF @@ -53,7 +53,8 @@ Export-Package: org.eclipse.jdt.ls.core.internal;x-friends:="org.eclipse.jdt.ls. org.eclipse.jdt.ls.core.internal.javadoc;x-friends:="org.eclipse.jdt.ls.tests", org.eclipse.jdt.ls.core.internal.lsp;x-friends:="org.eclipse.jdt.ls.tests", org.eclipse.jdt.ls.core.internal.managers;x-friends:="org.eclipse.jdt.ls.tests", - org.eclipse.jdt.ls.core.internal.preferences;x-friends:="org.eclipse.jdt.ls.tests" + org.eclipse.jdt.ls.core.internal.preferences;x-friends:="org.eclipse.jdt.ls.tests", + org.eclipse.jdt.ls.core.internal.highlighting;x-friends:="org.eclipse.jdt.ls.tests" Bundle-ClassPath: lib/jsoup-1.9.2.jar, lib/remark-1.0.0.jar, . diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JavaClientConnection.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JavaClientConnection.java index 2e82089d68..3b5c8bd1d1 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JavaClientConnection.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JavaClientConnection.java @@ -28,6 +28,7 @@ import org.eclipse.lsp4j.MessageType; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.RegistrationParams; +import org.eclipse.lsp4j.SemanticHighlightingParams; import org.eclipse.lsp4j.ShowMessageRequestParams; import org.eclipse.lsp4j.UnregistrationParams; import org.eclipse.lsp4j.WorkspaceEdit; @@ -195,6 +196,13 @@ public void registerCapability(RegistrationParams params) { client.registerCapability(params); } + /** + * @see {@link LanguageClient#semanticHighlighting(SemanticHighlightingParams)} + */ + public void semanticHighlighting(SemanticHighlightingParams params) { + client.semanticHighlighting(params); + } + public void disconnect() { if (logHandler != null) { logHandler.uninstall(); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentLifeCycleHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentLifeCycleHandler.java index 4caa6046be..5d2ffbfdae 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentLifeCycleHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentLifeCycleHandler.java @@ -10,6 +10,8 @@ *******************************************************************************/ package org.eclipse.jdt.ls.core.internal.handlers; +import static com.google.common.collect.Lists.newArrayList; + import java.io.File; import java.util.ArrayList; import java.util.Arrays; @@ -46,12 +48,18 @@ import org.eclipse.jdt.ls.core.internal.JDTUtils; import org.eclipse.jdt.ls.core.internal.JavaClientConnection; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jdt.ls.core.internal.highlighting.HighlightedPosition; +import org.eclipse.jdt.ls.core.internal.highlighting.SemanticHighlightingService; +import org.eclipse.jdt.ls.core.internal.highlighting.SemanticHighlightingService.HighlightedPositionDiffContext; import org.eclipse.jdt.ls.core.internal.managers.ProjectsManager; import org.eclipse.jdt.ls.core.internal.managers.ProjectsManager.CHANGE_TYPE; import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager; import org.eclipse.jdt.ls.core.internal.preferences.Preferences; import org.eclipse.jdt.ls.core.internal.preferences.Preferences.Severity; import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.BadPositionCategoryException; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.lsp4j.Command; import org.eclipse.lsp4j.DidChangeTextDocumentParams; @@ -60,12 +68,15 @@ import org.eclipse.lsp4j.DidSaveTextDocumentParams; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextDocumentContentChangeEvent; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; import org.eclipse.text.edits.DeleteEdit; import org.eclipse.text.edits.InsertEdit; import org.eclipse.text.edits.MalformedTreeException; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; +import com.google.common.collect.Iterables; + public class DocumentLifeCycleHandler { public static final String DOCUMENT_LIFE_CYCLE_JOBS = "DocumentLifeCycleJobs"; @@ -76,12 +87,14 @@ public class DocumentLifeCycleHandler { private CoreASTProvider sharedASTProvider; private WorkspaceJob validationTimer; private Set toReconcile = new HashSet<>(); + private SemanticHighlightingService semanticHighlightingService; public DocumentLifeCycleHandler(JavaClientConnection connection, PreferenceManager preferenceManager, ProjectsManager projectsManager, boolean delayValidation) { this.connection = connection; this.preferenceManager = preferenceManager; this.projectsManager = projectsManager; this.sharedASTProvider = CoreASTProvider.getInstance(); + this.semanticHighlightingService = new SemanticHighlightingService(this.connection, this.sharedASTProvider, this.preferenceManager); if (delayValidation) { this.validationTimer = new WorkspaceJob("Validate documents") { @Override @@ -282,15 +295,17 @@ public void handleOpen(DidOpenTextDocumentParams params) { buffer.setContents(newContent); } triggerValidation(unit); + installSemanticHighlightings(unit); // see https://github.com/redhat-developer/vscode-java/issues/274 checkPackageDeclaration(uri, unit); - } catch (JavaModelException e) { - JavaLanguageServerPlugin.logException("Error while opening document", e); + } catch (JavaModelException | BadPositionCategoryException e) { + JavaLanguageServerPlugin.logException("Error while opening document. URI: " + uri, e); } } public void handleChanged(DidChangeTextDocumentParams params) { - ICompilationUnit unit = JDTUtils.resolveCompilationUnit(params.getTextDocument().getUri()); + String uri = params.getTextDocument().getUri(); + ICompilationUnit unit = JDTUtils.resolveCompilationUnit(uri); if (unit == null || !unit.isWorkingCopy() || params.getContentChanges().isEmpty() || unit.getResource().isDerived()) { return; @@ -301,6 +316,7 @@ public void handleChanged(DidChangeTextDocumentParams params) { sharedASTProvider.disposeAST(); } List contentChanges = params.getContentChanges(); + List diffContexts = newArrayList(); for (TextDocumentContentChangeEvent changeEvent : contentChanges) { Range range = changeEvent.getRange(); @@ -325,12 +341,33 @@ public void handleChanged(DidChangeTextDocumentParams params) { } else { edit = new ReplaceEdit(startOffset, length, text); } - IDocument document = JsonRpcHelpers.toDocument(unit.getBuffer()); - edit.apply(document, TextEdit.NONE); + + // Avoid any computation if the `SemanticHighlightingService#isEnabled` is `false`. + if (semanticHighlightingService.isEnabled()) { + IDocument oldState = new Document(unit.getBuffer().getContents()); + IDocument newState = JsonRpcHelpers.toDocument(unit.getBuffer()); + //@formatter:off + List oldPositions = diffContexts.isEmpty() + ? semanticHighlightingService.getHighlightedPositions(uri) + : Iterables.getLast(diffContexts).newPositions; + //@formatter:on + edit.apply(newState, TextEdit.NONE); + // This is a must. Make the document immutable. + // Otherwise, any consecutive `newStates` get out-of-sync due to the shared buffer from the compilation unit. + newState = new Document(newState.get()); + List newPositions = semanticHighlightingService.calculateHighlightedPositions(unit, true); + DocumentEvent event = new DocumentEvent(newState, startOffset, length, text); + diffContexts.add(new HighlightedPositionDiffContext(oldState, event, oldPositions, newPositions)); + } else { + IDocument document = JsonRpcHelpers.toDocument(unit.getBuffer()); + edit.apply(document, TextEdit.NONE); + } + } triggerValidation(unit); - } catch (JavaModelException | MalformedTreeException | BadLocationException e) { - JavaLanguageServerPlugin.logException("Error while handling document change", e); + updateSemanticHighlightings(params.getTextDocument(), diffContexts); + } catch (JavaModelException | MalformedTreeException | BadLocationException | BadPositionCategoryException e) { + JavaLanguageServerPlugin.logException("Error while handling document change. URI: " + uri, e); } } @@ -354,8 +391,9 @@ public void handleClosed(DidCloseTextDocumentParams params) { unit.delete(true, null); } } + uninstallSemanticHighlightings(uri); } catch (CoreException e) { - JavaLanguageServerPlugin.logException("Error while handling document close", e); + JavaLanguageServerPlugin.logException("Error while handling document close. URI: " + uri, e); } } @@ -374,7 +412,7 @@ public void handleSaved(DidSaveTextDocumentParams params) { unit.discardWorkingCopy(); unit.becomeWorkingCopy(new NullProgressMonitor()); } catch (JavaModelException e) { - JavaLanguageServerPlugin.logException("Error while handling document save", e); + JavaLanguageServerPlugin.logException("Error while handling document save. URI: " + uri, e); } } } @@ -423,4 +461,16 @@ private ICompilationUnit checkPackageDeclaration(String uri, ICompilationUnit un return unit; } + protected void installSemanticHighlightings(ICompilationUnit unit) throws JavaModelException, BadPositionCategoryException { + this.semanticHighlightingService.install(unit); + } + + protected void uninstallSemanticHighlightings(String uri) { + this.semanticHighlightingService.uninstall(uri); + } + + protected void updateSemanticHighlightings(VersionedTextDocumentIdentifier textDocument, List diffContexts) throws BadLocationException, BadPositionCategoryException, JavaModelException { + this.semanticHighlightingService.update(textDocument, diffContexts); + } + } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/InitHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/InitHandler.java index 7835be8374..32ab5e1c17 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/InitHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/InitHandler.java @@ -36,6 +36,7 @@ import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; import org.eclipse.jdt.ls.core.internal.ResourceUtils; import org.eclipse.jdt.ls.core.internal.ServiceStatus; +import org.eclipse.jdt.ls.core.internal.highlighting.SemanticHighlightingService; import org.eclipse.jdt.ls.core.internal.managers.ProjectsManager; import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager; import org.eclipse.jdt.ls.core.internal.preferences.Preferences; @@ -46,6 +47,7 @@ import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; import org.eclipse.lsp4j.SaveOptions; +import org.eclipse.lsp4j.SemanticHighlightingServerCapabilities; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.TextDocumentSyncKind; import org.eclipse.lsp4j.TextDocumentSyncOptions; @@ -198,6 +200,12 @@ InitializeResult initialize(InitializeParams param) { } capabilities.setTextDocumentSync(textDocumentSyncOptions); + if (preferenceManager.getClientPreferences().isSemanticHighlightingSupported()) { + SemanticHighlightingServerCapabilities semanticHighlightingCapabilities = new SemanticHighlightingServerCapabilities(); + semanticHighlightingCapabilities.setScopes(SemanticHighlightingService.getAllScopes()); + capabilities.setSemanticHighlighting(semanticHighlightingCapabilities); + } + WorkspaceServerCapabilities wsCapabilities = new WorkspaceServerCapabilities(); WorkspaceFoldersOptions wsFoldersOptions = new WorkspaceFoldersOptions(); wsFoldersOptions.setSupported(Boolean.TRUE); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JsonRpcHelpers.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JsonRpcHelpers.java index ca7808dd91..00de4a0784 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JsonRpcHelpers.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JsonRpcHelpers.java @@ -37,10 +37,23 @@ private JsonRpcHelpers(){ * @return */ public static int toOffset(IBuffer buffer, int line, int column){ + if (buffer != null) { + return toOffset(toDocument(buffer), line, column); + } + return -1; + } + + /** + * Convert line, column to a document offset. + * + * @param document + * @param line + * @param column + * @return + */ + public static int toOffset(IDocument document, int line, int column) { try { - if (buffer != null) { - return toDocument(buffer).getLineOffset(line) + column; - } + return document.getLineOffset(line) + column; } catch (BadLocationException e) { JavaLanguageServerPlugin.logException(e.getMessage(), e); } @@ -55,11 +68,21 @@ public static int toOffset(IBuffer buffer, int line, int column){ * @return */ public static int[] toLine(IBuffer buffer, int offset){ - IDocument document = toDocument(buffer); + return toLine(toDocument(buffer), offset); + } + + /** + * Convert the document offset to line number and column. + * + * @param document + * @param line + * @return + */ + public static int[] toLine(IDocument document, int offset) { try { int line = document.getLineOfOffset(offset); int column = offset - document.getLineOffset(line); - return new int[] {line, column}; + return new int[] { line, column }; } catch (BadLocationException e) { JavaLanguageServerPlugin.logException(e.getMessage(), e); } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/HighlightedPosition.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/HighlightedPosition.java new file mode 100644 index 0000000000..a20b345f65 --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/HighlightedPosition.java @@ -0,0 +1,175 @@ +/******************************************************************************* + * Copyright (c) 2000, 2008 IBM Corporation 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + * TypeFox - port to jdt.ls + * + * Copied from https://github.com/eclipse/eclipse.jdt.ui/blob/d41fa3326c5b75a6419c81fcecb37d7d7fb3ac43/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/SemanticHighlightingManager.java + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.highlighting; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jface.text.Position; + +import com.google.common.base.Equivalence; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +/** + * Highlighted positions with the corresponding TM scopes. + */ +public class HighlightedPosition extends Position { + + /** Highlighting (TM) scopes of the position */ + private List fScopes; + + /** Lock object */ + private Object fLock; + + public static Function COPY = p -> HighlightedPosition.copy(p); + + public static HighlightedPosition copy(HighlightedPosition original) { + return new HighlightedPosition(original.offset, original.length, original.fScopes, original.fLock); + } + + /** + * Initialize the styled positions with the given offset, length and TM scopes. + * + * @param offset + * The position offset + * @param length + * The position length + * @param scopes + * The position's TM scopes for the highlighting + * @param lock + * The lock object + */ + public HighlightedPosition(int offset, int length, List scopes, Object lock) { + super(offset, length); + fScopes = scopes; + fLock= lock; + } + + /** + * Uses reference equality for the highlighting. + * + * @param off + * The offset + * @param len + * The length + * @param scopes + * The highlighting scopes for the position + * @return true iff the given offset, length and highlighting are + * equal to the internal ones. + */ + public boolean isEqual(int off, int len, Collection scopes) { + synchronized (fLock) { + return !isDeleted() && getOffset() == off && getLength() == len && fScopes.equals(ImmutableSet.copyOf(scopes)); + } + } + + /** + * Is this position contained in the given range (inclusive)? Synchronizes on position updater. + * + * @param off The range offset + * @param len The range length + * @return true iff this position is not delete and contained in the given range. + */ + public boolean isContained(int off, int len) { + synchronized (fLock) { + return !isDeleted() && off <= getOffset() && off + len >= getOffset() + getLength(); + } + } + + public void update(int off, int len) { + synchronized (fLock) { + super.setOffset(off); + super.setLength(len); + } + } + + /* + * @see org.eclipse.jface.text.Position#setLength(int) + */ + @Override + public void setLength(int length) { + synchronized (fLock) { + super.setLength(length); + } + } + + /* + * @see org.eclipse.jface.text.Position#setOffset(int) + */ + @Override + public void setOffset(int offset) { + synchronized (fLock) { + super.setOffset(offset); + } + } + + /* + * @see org.eclipse.jface.text.Position#delete() + */ + @Override + public void delete() { + synchronized (fLock) { + super.delete(); + } + } + + /* + * @see org.eclipse.jface.text.Position#undelete() + */ + @Override + public void undelete() { + synchronized (fLock) { + super.undelete(); + } + } + + /** + * @return Returns the highlighting (TM) scopes. + */ + public List getHighlightingScopes() { + return fScopes; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(super.toString()); + sb.append(" scopes: "); + sb.append(Iterables.toString(fScopes)); + return sb.toString(); + } + + + public static final class HighlightedPositionEquivalence extends Equivalence { + + public static final HighlightedPositionEquivalence INSTANCE = new HighlightedPositionEquivalence(); + + private HighlightedPositionEquivalence() { + } + + @Override + protected boolean doEquivalent(HighlightedPosition a, HighlightedPosition b) { + return a.length != b.length && a.offset != b.offset && a.isDeleted != b.isDeleted && Objects.equals(a.fScopes, b.fScopes); + } + + @Override + protected int doHash(HighlightedPosition t) { + return Objects.hash(t.length, t.offset, t.isDeleted, t.fScopes); + } + + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlighting.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlighting.java new file mode 100644 index 0000000000..fda37f251f --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlighting.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2000, 2008 IBM Corporation 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + * TypeFox - port to jdt.ls + * + * Copied from https://github.com/eclipse/eclipse.jdt.ui/blob/d41fa3326c5b75a6419c81fcecb37d7d7fb3ac43/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/SemanticHighlighting.java + *******************************************************************************/ + +package org.eclipse.jdt.ls.core.internal.highlighting; + +import java.util.List; + +import com.google.common.collect.Iterables; + +/** + * Semantic highlighting + */ +public interface SemanticHighlighting { + + public abstract List getScopes(); + + /** + * @return the display name + */ + public default String getDisplayName() { + final StringBuilder sb = new StringBuilder(); + sb.append(this.getClass().getSimpleName()); + sb.append("["); + sb.append(Iterables.toString(getScopes())); + sb.append("]"); + return sb.toString(); + } + + /** + * Returns true if the semantic highlighting consumes the semantic + * token. + *

+ * NOTE: Implementors are not allowed to keep a reference on the token or on any + * object retrieved from the token. + *

+ * + * @param token + * the semantic token for a + * {@link org.eclipse.jdt.core.dom.SimpleName} + * @return true iff the semantic highlighting consumes the semantic + * token + */ + public boolean consumes(SemanticToken token); + + /** + * Returns true if the semantic highlighting consumes the semantic + * token. + *

+ * NOTE: Implementors are not allowed to keep a reference on the token or on any + * object retrieved from the token. + *

+ * + * @param token + * the semantic token for a + * {@link org.eclipse.jdt.core.dom.NumberLiteral}, + * {@link org.eclipse.jdt.core.dom.BooleanLiteral} or + * {@link org.eclipse.jdt.core.dom.CharacterLiteral} + * @return true iff the semantic highlighting consumes the semantic + * token + */ + public default boolean consumesLiteral(SemanticToken token) { + return false; + } + +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingDiffCalculator.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingDiffCalculator.java new file mode 100644 index 0000000000..64686f1314 --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingDiffCalculator.java @@ -0,0 +1,247 @@ +/******************************************************************************* + * Copyright (c) 2018 TypeFox 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * TypeFox - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.highlighting; + +import static com.google.common.collect.Lists.newArrayList; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.jdt.ls.core.internal.handlers.JsonRpcHelpers; +import org.eclipse.jdt.ls.core.internal.highlighting.SemanticHighlightingService.HighlightedPositionDiffContext; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.Position; +import org.eclipse.lsp4j.SemanticHighlightingInformation; +import org.eclipse.lsp4j.util.SemanticHighlightingTokens; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; + +public class SemanticHighlightingDiffCalculator { + + public List getDiffInfos(HighlightedPositionDiffContext context) throws BadLocationException { + + IDocument newState = context.newState; + IDocument oldState = context.oldState; + + // Can be negative or zero too. + int lineShiftCount = this.getLineShift(oldState, context.event); + + int eventOffset = context.event.getOffset(); + int eventOldLength = context.event.getLength(); + int eventEnd = eventOffset + eventOldLength; + + Map infosPerLine = Maps.newHashMap(); + Multimap tokensPerLine = HashMultimap.create(); + Multimap pendingPositions = HashMultimap.create(); + Map newPositions = Maps.newHashMap(); + for (HighlightedPosition newPosition : context.newPositions) { + LookupKey key = createKey(newState, newPosition); + newPositions.put(key, newPosition); + pendingPositions.put(key.line, newPosition); + } + + for (HighlightedPosition oldPosition : context.oldPositions) { + int[] oldLineAndColumn = getLineAndColumn(oldState, oldPosition); + int originalOldLine = oldLineAndColumn[0]; + int oldColumn = oldLineAndColumn[1]; + int oldOffset = oldPosition.getOffset(); + int oldLength = oldPosition.getLength(); + int oldEnd = oldOffset + oldLength; + // If the position is before the change (event), no need to shift the line. Otherwise we consider the line shift. + int adjustedOldLine = oldEnd < eventEnd ? originalOldLine : originalOldLine + lineShiftCount; + + int scope = SemanticHighlightingService.getIndex(oldPosition.getHighlightingScopes()); + LookupKey key = createKey(adjustedOldLine, oldColumn, getTextAt(oldState, oldPosition), scope); + HighlightedPosition newPosition = newPositions.remove(key); + if (newPosition == null && !infosPerLine.containsKey(originalOldLine)) { + infosPerLine.put(originalOldLine, new SemanticHighlightingInformation(originalOldLine, null)); + } + } + + for (Entry entries : newPositions.entrySet()) { + LookupKey lookupKey = entries.getKey(); + int line = lookupKey.line; + int length = lookupKey.text.length(); + int character = lookupKey.column; + int scope = lookupKey.scope; + SemanticHighlightingInformation info = infosPerLine.get(line); + if (info == null) { + info = new SemanticHighlightingInformation(line, null); + infosPerLine.put(line, info); + } + tokensPerLine.put(line, new SemanticHighlightingTokens.Token(character, length, scope)); + // If a line contains at least one change, we need to invalidate the entire line by consuming all pending positions. + Collection pendings = pendingPositions.removeAll(line); + if (pendings != null) { + for (HighlightedPosition pendingPosition : pendings) { + if (pendingPosition != entries.getValue()) { + int[] lineAndColumn = getLineAndColumn(newState, pendingPosition); + int pendingCharacter = lineAndColumn[1]; + int pendingLength = pendingPosition.length; + int pendingScope = SemanticHighlightingService.getIndex(pendingPosition.getHighlightingScopes()); + tokensPerLine.put(line, new SemanticHighlightingTokens.Token(pendingCharacter, pendingLength, pendingScope)); + } + } + } + } + + for (Entry> entry : tokensPerLine.asMap().entrySet()) { + List tokens = newArrayList(entry.getValue()); + Collections.sort(tokens); + infosPerLine.get(entry.getKey()).setTokens(SemanticHighlightingTokens.encode(tokens)); + } + + return FluentIterable.from(infosPerLine.values()).toSortedList(HighlightingInformationComparator.INSTANCE); + } + + protected int[] getLineAndColumn(IDocument document, HighlightedPosition position) { + //@formatter:off + int[] lineAndColumn = JsonRpcHelpers.toLine(document, position.offset); + Assert.isNotNull( + lineAndColumn, + "Cannot retrieve the line and column information for document. Position was: " + position + " Document was:>" + document.get() + "<." + ); + return lineAndColumn; + //@formatter:off + } + + protected int getLineShift(IDocument oldState, DocumentEvent event) throws BadLocationException { + // Insert edit. + if (event.fLength == 0) { + Preconditions.checkNotNull(event.fText, "fText"); + int beforeEndLine = event.fDocument.getLineOfOffset(event.fOffset + event.fLength); + int afterEndLine = event.fDocument.getLineOfOffset(event.fOffset + event.fText.length()); + return afterEndLine - beforeEndLine; + // Delete edit. + } else if (event.fText == null || event.fText.isEmpty()) { + return event.fDocument.getLineOfOffset(event.fOffset) - oldState.getLineOfOffset(event.fOffset + event.fLength); + // Replace edit. + } else { + int startLine = oldState.getLineOfOffset(event.fOffset + event.fLength); + int lineShift = event.fDocument.getLineOfOffset(event.fOffset + event.fText.length()) - startLine; + return lineShift; + } + } + + protected String getTextAt(IDocument document, Position position) throws BadLocationException { + return document.get(position.offset, position.length); + } + + protected LookupKey createKey(IDocument document, HighlightedPosition position) throws BadLocationException { + int[] lineAndColumn = getLineAndColumn(document, position); + int scope = SemanticHighlightingService.getIndex(position.getHighlightingScopes()); + return createKey(lineAndColumn[0], lineAndColumn[1], getTextAt(document, position), scope); + } + + protected LookupKey createKey(int line, int column, String text, int scope) { + return new LookupKey(line, column, text, scope); + } + + protected static class HighlightingInformationComparator implements Comparator { + + protected static final Comparator INSTANCE = new HighlightingInformationComparator(); + + @Override + public int compare(SemanticHighlightingInformation left, SemanticHighlightingInformation right) { + //@formatter:off + return ComparisonChain.start() + .compare(left.getLine(), right.getLine()) + .result(); + //@formatter:on + } + } + + private static final class LookupKey { + + private final int line; + private final int column; + private final String text; + private final int scope; + + private LookupKey(int line, int column, String text, int scope) { + this.line = line; + this.column = column; + this.text = text; + this.scope = scope; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + column; + result = prime * result + line; + result = prime * result + scope; + result = prime * result + ((text == null) ? 0 : text.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + LookupKey other = (LookupKey) obj; + if (column != other.column) { + return false; + } + if (line != other.line) { + return false; + } + if (scope != other.scope) { + return false; + } + if (text == null) { + if (other.text != null) { + return false; + } + } else if (!text.equals(other.text)) { + return false; + } + return true; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Line: "); + sb.append(line); + sb.append("\nColumn: "); + sb.append(column); + sb.append("\nText: "); + sb.append(text); + sb.append("\nScopes: "); + sb.append(Iterables.toString(SemanticHighlightingService.getScopes(scope))); + return sb.toString(); + } + + } + +} diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingPresenter.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingPresenter.java new file mode 100644 index 0000000000..68aa78228e --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingPresenter.java @@ -0,0 +1,471 @@ +/******************************************************************************* + * Copyright (c) 2000, 2008 IBM Corporation 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + * TypeFox - port to jdt.ls + * + * Copied from https://github.com/eclipse/eclipse.jdt.ui/blob/d41fa3326c5b75a6419c81fcecb37d7d7fb3ac43/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/SemanticHighlightingPresenter.java + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.highlighting; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +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.IPositionUpdater; +import org.eclipse.jface.text.Position; + + +/** + * Semantic highlighting presenter - Customized for the Java LS. + * + * @since 3.0 + */ +public class SemanticHighlightingPresenter { + + /** + * Semantic highlighting position updater. + */ + public static class HighlightingPositionUpdater implements IPositionUpdater { + + /** The position category. */ + private final String fCategory; + + /** + * Creates a new updater for the given category. + * + * @param category the new category. + */ + public HighlightingPositionUpdater(String category) { + fCategory= category; + } + + /* + * @see org.eclipse.jface.text.IPositionUpdater#update(org.eclipse.jface.text.DocumentEvent) + */ + @Override + public void update(DocumentEvent event) { + + int eventOffset= event.getOffset(); + int eventOldLength= event.getLength(); + int eventEnd= eventOffset + eventOldLength; + + try { + Position[] positions= event.getDocument().getPositions(fCategory); + + for (int i= 0; i != positions.length; i++) { + + HighlightedPosition position= (HighlightedPosition) positions[i]; + + // Also update deleted positions because they get deleted by the background thread and removed/invalidated only in the UI runnable +// if (position.isDeleted()) +// continue; + + int offset= position.getOffset(); + int length= position.getLength(); + int end= offset + length; + + if (offset > eventEnd) { + updateWithPrecedingEvent(position, event); + } else if (end < eventOffset) { + updateWithSucceedingEvent(position, event); + } else if (offset <= eventOffset && end >= eventEnd) { + updateWithIncludedEvent(position, event); + } else if (offset <= eventOffset) { + updateWithOverEndEvent(position, event); + } else if (end >= eventEnd) { + updateWithOverStartEvent(position, event); + } else { + updateWithIncludingEvent(position, event); + } + } + } catch (BadPositionCategoryException e) { + // ignore and return + } + } + + /** + * Update the given position with the given event. The event precedes the position. + * + * @param position The position + * @param event The event + */ + private void updateWithPrecedingEvent(HighlightedPosition position, DocumentEvent event) { + String newText= event.getText(); + int eventNewLength= newText != null ? newText.length() : 0; + int deltaLength= eventNewLength - event.getLength(); + + position.setOffset(position.getOffset() + deltaLength); + } + + /** + * Update the given position with the given event. The event succeeds the position. + * + * @param position The position + * @param event The event + */ + private void updateWithSucceedingEvent(HighlightedPosition position, DocumentEvent event) { + } + + /** + * Update the given position with the given event. The event is included by the position. + * + * @param position The position + * @param event The event + */ + private void updateWithIncludedEvent(HighlightedPosition position, DocumentEvent event) { + int eventOffset= event.getOffset(); + String newText= event.getText(); + if (newText == null) + { + newText= ""; //$NON-NLS-1$ + } + int eventNewLength= newText.length(); + + int deltaLength= eventNewLength - event.getLength(); + + int offset= position.getOffset(); + int length= position.getLength(); + int end= offset + length; + + int includedLength= 0; + while (includedLength < eventNewLength && Character.isJavaIdentifierPart(newText.charAt(includedLength))) { + includedLength++; + } + if (includedLength == eventNewLength) { + position.setLength(length + deltaLength); + } else { + int newLeftLength= eventOffset - offset + includedLength; + + int excludedLength= eventNewLength; + while (excludedLength > 0 && Character.isJavaIdentifierPart(newText.charAt(excludedLength - 1))) { + excludedLength--; + } + int newRightOffset= eventOffset + excludedLength; + int newRightLength= end + deltaLength - newRightOffset; + + if (newRightLength == 0) { + position.setLength(newLeftLength); + } else { + if (newLeftLength == 0) { + position.update(newRightOffset, newRightLength); + } else { + position.setLength(newLeftLength); + // addPositionFromUI(newRightOffset, newRightLength, position.getHighlightingScopes()); + HighlightedPosition highlightedPosition = new HighlightedPosition(newRightOffset, newRightLength, position.getHighlightingScopes(), new Object()); + try { + event.fDocument.addPosition(fCategory, highlightedPosition); + } catch (BadLocationException | BadPositionCategoryException e) { + JavaLanguageServerPlugin.logException("Error when adding new highlighting position to the document", e); + } + } + } + } + } + + /** + * Update the given position with the given event. The event overlaps with the end of the position. + * + * @param position The position + * @param event The event + */ + private void updateWithOverEndEvent(HighlightedPosition position, DocumentEvent event) { + String newText= event.getText(); + if (newText == null) + { + newText= ""; //$NON-NLS-1$ + } + int eventNewLength= newText.length(); + + int includedLength= 0; + while (includedLength < eventNewLength && Character.isJavaIdentifierPart(newText.charAt(includedLength))) { + includedLength++; + } + position.setLength(event.getOffset() - position.getOffset() + includedLength); + } + + /** + * Update the given position with the given event. The event overlaps with the start of the position. + * + * @param position The position + * @param event The event + */ + private void updateWithOverStartEvent(HighlightedPosition position, DocumentEvent event) { + int eventOffset= event.getOffset(); + int eventEnd= eventOffset + event.getLength(); + + String newText= event.getText(); + if (newText == null) + { + newText= ""; //$NON-NLS-1$ + } + int eventNewLength= newText.length(); + + int excludedLength= eventNewLength; + while (excludedLength > 0 && Character.isJavaIdentifierPart(newText.charAt(excludedLength - 1))) { + excludedLength--; + } + int deleted= eventEnd - position.getOffset(); + int inserted= eventNewLength - excludedLength; + position.update(eventOffset + excludedLength, position.getLength() - deleted + inserted); + } + + /** + * Update the given position with the given event. The event includes the position. + * + * @param position The position + * @param event The event + */ + private void updateWithIncludingEvent(HighlightedPosition position, DocumentEvent event) { + position.delete(); + position.update(event.getOffset(), 0); + } + } + + /** Position updater */ + public final IPositionUpdater fPositionUpdater = new HighlightingPositionUpdater(getPositionCategory()); + + /** UI's current highlighted positions - can contain null elements */ + private List fPositions= new ArrayList<>(); + /** UI position lock */ + private Object fPositionLock= new Object(); + + /** true iff the current reconcile is canceled. */ + private boolean fIsCanceled= false; + + /** + * Creates and returns a new highlighted position with the given offset, length and highlighting. + *

+ * NOTE: Also called from background thread. + *

+ * + * @param offset The offset + * @param length The length + * @param highlighting The highlighting + * @return The new highlighted position + */ + public HighlightedPosition createHighlightedPosition(int offset, int length, List highlighting) { + // TODO: reuse deleted positions + return new HighlightedPosition(offset, length, highlighting, fPositionUpdater); + } + + /** + * Adds all current positions to the given list. + *

+ * NOTE: Called from background thread. + *

+ * + * @param list The list + */ + public void addAllPositions(List list) { + synchronized (fPositionLock) { + list.addAll(fPositions); + } + } + + /** + * Create a runnable for updating the presentation. + *

+ * NOTE: Called from background thread. + *

+ * @param textPresentation the text presentation + * @param addedPositions the added positions + * @param removedPositions the removed positions + * @return the runnable or null, if reconciliation should be canceled + */ + public void createUpdateRunnable(IDocument document, List addedPositions, List removedPositions) { + + // TODO: do clustering of positions and post multiple fast runnables + final HighlightedPosition[] added= new HighlightedPosition[addedPositions.size()]; + addedPositions.toArray(added); + final HighlightedPosition[] removed= new HighlightedPosition[removedPositions.size()]; + removedPositions.toArray(removed); + updatePresentation(document, added, removed); + } + + /** + * Invalidate the presentation of the positions based on the given added positions and the existing deleted positions. + * Also unregisters the deleted positions from the document and patches the positions of this presenter. + *

+ * NOTE: Indirectly called from background thread by UI runnable. + *

+ * @param textPresentation the text presentation or null, if the presentation should computed in the UI thread + * @param addedPositions the added positions + * @param removedPositions the removed positions + */ + public void updatePresentation(IDocument document, HighlightedPosition[] addedPositions, HighlightedPosition[] removedPositions) { + +// checkOrdering("added positions: ", Arrays.asList(addedPositions)); //$NON-NLS-1$ +// checkOrdering("removed positions: ", Arrays.asList(removedPositions)); //$NON-NLS-1$ +// checkOrdering("old positions: ", fPositions); //$NON-NLS-1$ + + // TODO: double-check consistency with document.getPositions(...) + // TODO: reuse removed positions + if (isCanceled()) { + return; + } + + String positionCategory= getPositionCategory(); + + List removedPositionsList= Arrays.asList(removedPositions); + + try { + synchronized (fPositionLock) { + List oldPositions= fPositions; + int newSize= Math.max(fPositions.size() + addedPositions.length - removedPositions.length, 10); + + /* + * The following loop is a kind of merge sort: it merges two List, each + * sorted by position.offset, into one new list. The first of the two is the + * previous list of positions (oldPositions), from which any deleted positions get + * removed on the fly. The second of two is the list of added positions. The result + * is stored in newPositions. + */ + List newPositions= new ArrayList<>(newSize); + Position position= null; + Position addedPosition= null; + for (int i= 0, j= 0, n= oldPositions.size(), m= addedPositions.length; i < n || position != null || j < m || addedPosition != null;) { + // loop variant: i + j < old(i + j) + + // a) find the next non-deleted Position from the old list + while (position == null && i < n) { + position= oldPositions.get(i++); + if (position.isDeleted() || contain(removedPositionsList, position)) { + document.removePosition(positionCategory, position); + position= null; + } + } + + // b) find the next Position from the added list + if (addedPosition == null && j < m) { + addedPosition= addedPositions[j++]; + document.addPosition(positionCategory, addedPosition); + } + + // c) merge: add the next of position/addedPosition with the lower offset + if (position != null) { + if (addedPosition != null) { + if (position.getOffset() <= addedPosition.getOffset()) { + newPositions.add(position); + position= null; + } else { + newPositions.add(addedPosition); + addedPosition= null; + } + } else { + newPositions.add(position); + position= null; + } + } else if (addedPosition != null) { + newPositions.add(addedPosition); + addedPosition= null; + } + } + fPositions= newPositions; + } + } catch (BadPositionCategoryException e) { + // Should not happen + JavaLanguageServerPlugin.logException("Error occurred when updating the presentation.", e); + } catch (BadLocationException e) { + // Should not happen + JavaLanguageServerPlugin.logException("Error occurred when updating the presentation.", e); + } +// checkOrdering("new positions: ", fPositions); //$NON-NLS-1$ + } + +// private void checkOrdering(String s, List positions) { +// Position previous= null; +// for (int i= 0, n= positions.size(); i < n; i++) { +// Position current= (Position) positions.get(i); +// if (previous != null && previous.getOffset() + previous.getLength() > current.getOffset()) +// return; +// } +// } + + /** + * Returns true iff the positions contain the position. + * @param positions the positions, must be ordered by offset but may overlap + * @param position the position + * @return true iff the positions contain the position + */ + private boolean contain(List positions, Position position) { + return indexOf(positions, position) != -1; + } + + /** + * Returns index of the position in the positions, -1 if not found. + * @param positions the positions, must be ordered by offset but may overlap + * @param position the position + * @return the index + */ + private int indexOf(List positions, Position position) { + int index= computeIndexAtOffset(positions, position.getOffset()); + int size= positions.size(); + while (index < size) { + if (positions.get(index) == position) { + return index; + } + index++; + } + return -1; + } + + /** + * Returns the index of the first position with an offset equal or greater than the given offset. + * + * @param positions the positions, must be ordered by offset and must not overlap + * @param offset the offset + * @return the index of the last position with an offset equal or greater than the given offset + */ + private int computeIndexAtOffset(List positions, int offset) { + int i= -1; + int j= positions.size(); + while (j - i > 1) { + int k= (i + j) >> 1; + Position position= positions.get(k); + if (position.getOffset() >= offset) { + j= k; + } else { + i= k; + } + } + return j; + } + + /** + * @return Returns true iff the current reconcile is canceled. + *

+ * NOTE: Also called from background thread. + *

+ */ + public boolean isCanceled() { + return fIsCanceled; + } + + /** + * Set whether or not the current reconcile is canceled. + * + * @param isCanceled true iff the current reconcile is canceled + */ + public void setCanceled(boolean isCanceled) { + fIsCanceled = isCanceled; + } + + /** + * @return The semantic reconciler position's category. + */ + public String getPositionCategory() { + return toString(); + } + +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingReconciler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingReconciler.java new file mode 100644 index 0000000000..034b28ed79 --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingReconciler.java @@ -0,0 +1,407 @@ +/******************************************************************************* + * Copyright (c) 2000, 2008 IBM Corporation 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + * TypeFox - port to jdt.ls + * + * Copied from https://github.com/eclipse/eclipse.jdt.ui/blob/d41fa3326c5b75a6419c81fcecb37d7d7fb3ac43/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/SemanticHighlightingReconciler.java + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.highlighting; + +import static java.util.Collections.emptyList; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.BooleanLiteral; +import org.eclipse.jdt.core.dom.CharacterLiteral; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ConstructorInvocation; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.NumberLiteral; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.SimpleType; +import org.eclipse.jdt.core.dom.SuperConstructorInvocation; +import org.eclipse.jdt.internal.corext.dom.GenericVisitor; +import org.eclipse.jdt.ls.core.internal.highlighting.SemanticHighlightings.DeprecatedMemberHighlighting; +import org.eclipse.jdt.ls.core.internal.highlighting.SemanticHighlightings.VarKeywordHighlighting; +import org.eclipse.jface.text.BadPositionCategoryException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.Position; + +import com.google.common.base.Preconditions; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; + +/** + * Semantic highlighting reconciler - Background thread implementation. + * + * @since 3.0 + */ +@SuppressWarnings("restriction") +public class SemanticHighlightingReconciler { + + /** + * Collects positions from the AST. + */ + private class PositionCollector extends GenericVisitor { + + /** The semantic token */ + private SemanticToken fToken= new SemanticToken(); + + /* + * @see org.eclipse.jdt.internal.corext.dom.GenericVisitor#visitNode(org.eclipse.jdt.core.dom.ASTNode) + */ + @Override + protected boolean visitNode(ASTNode node) { + if ((node.getFlags() & ASTNode.MALFORMED) == ASTNode.MALFORMED) { + retainPositions(node.getStartPosition(), node.getLength()); + return false; + } + return true; + } + + /* + * @see org.eclipse.jdt.core.dom.ASTVisitor#visit(org.eclipse.jdt.core.dom.BooleanLiteral) + */ + @Override + public boolean visit(BooleanLiteral node) { + return visitLiteral(node); + } + + /* + * @see org.eclipse.jdt.core.dom.ASTVisitor#visit(org.eclipse.jdt.core.dom.CharacterLiteral) + */ + @Override + public boolean visit(CharacterLiteral node) { + return visitLiteral(node); + } + + /* + * @see org.eclipse.jdt.core.dom.ASTVisitor#visit(org.eclipse.jdt.core.dom.NumberLiteral) + */ + @Override + public boolean visit(NumberLiteral node) { + return visitLiteral(node); + } + + private boolean visitLiteral(Expression node) { + fToken.update(node); + for (int i= 0, n= fJobSemanticHighlightings.length; i < n; i++) { + SemanticHighlighting semanticHighlighting= fJobSemanticHighlightings[i]; + if (semanticHighlighting.consumesLiteral(fToken)) { + int offset= node.getStartPosition(); + int length= node.getLength(); + if (offset > -1 && length > 0) { + addPosition(offset, length, fJobHighlightings.get(i)); + } + break; + } + } + fToken.clear(); + return false; + } + + /* + * @see org.eclipse.jdt.internal.corext.dom.GenericVisitor#visit(org.eclipse.jdt.core.dom.ConstructorInvocation) + * @since 3.5 + */ + @Override + public boolean visit(ConstructorInvocation node) { + // XXX Hack for performance reasons (should loop over fJobSemanticHighlightings can call consumes(*)) + if (fJobDeprecatedMemberHighlighting != null) { + IMethodBinding constructorBinding= node.resolveConstructorBinding(); + if (constructorBinding != null && constructorBinding.isDeprecated()) { + int offset= node.getStartPosition(); + int length= 4; + if (offset > -1 && length > 0) { + addPosition(offset, length, fJobDeprecatedMemberHighlighting); + } + } + } + return true; + } + + /* + * @see org.eclipse.jdt.internal.corext.dom.GenericVisitor#visit(org.eclipse.jdt.core.dom.ConstructorInvocation) + * @since 3.5 + */ + @Override + public boolean visit(SuperConstructorInvocation node) { + // XXX Hack for performance reasons (should loop over fJobSemanticHighlightings can call consumes(*)) + if (fJobDeprecatedMemberHighlighting != null) { + IMethodBinding constructorBinding= node.resolveConstructorBinding(); + if (constructorBinding != null && constructorBinding.isDeprecated()) { + int offset= node.getStartPosition(); + int length= 5; + if (offset > -1 && length > 0) { + addPosition(offset, length, fJobDeprecatedMemberHighlighting); + } + } + } + return true; + } + + @Override + public boolean visit(SimpleType node) { + if (node.getAST().apiLevel() >= AST.JLS10 && node.isVar()) { + int offset= node.getStartPosition(); + int length= node.getLength(); + if (offset > -1 && length > 0) { + for (int i= 0; i < fJobSemanticHighlightings.length; i++) { + SemanticHighlighting semanticHighlighting= fJobSemanticHighlightings[i]; + if (semanticHighlighting instanceof VarKeywordHighlighting) { + addPosition(offset, length, fJobHighlightings.get(i)); + return false; + } + } + } + } + return true; + } + + /* + * @see org.eclipse.jdt.core.dom.ASTVisitor#visit(org.eclipse.jdt.core.dom.SimpleName) + */ + @Override + public boolean visit(SimpleName node) { + fToken.update(node); + for (int i= 0, n= fJobSemanticHighlightings.length; i < n; i++) { + SemanticHighlighting semanticHighlighting= fJobSemanticHighlightings[i]; + if (semanticHighlighting.consumes(fToken)) { + int offset= node.getStartPosition(); + int length= node.getLength(); + if (offset > -1 && length > 0) { + addPosition(offset, length, fJobHighlightings.get(i)); + } + break; + } + } + fToken.clear(); + return false; + } + + /** + * Add a position with the given range and highlighting iff it does not exist + * already. + * + * @param offset + * The range offset + * @param length + * The range length + * @param scopes + * The highlighting + */ + private void addPosition(int offset, int length, List scopes) { + boolean isExisting= false; + // TODO: use binary search + for (int i= 0, n= fRemovedPositions.size(); i < n; i++) { + HighlightedPosition position= (HighlightedPosition) fRemovedPositions.get(i); + if (position == null) { + continue; + } + if (position.isEqual(offset, length, scopes)) { + isExisting= true; + fRemovedPositions.set(i, null); + fNOfRemovedPositions--; + break; + } + } + + if (!isExisting) { + Position position = fJobPresenter.createHighlightedPosition(offset, length, scopes); + fAddedPositions.add(position); + } + } + + /** + * Retain the positions completely contained in the given range. + * @param offset The range offset + * @param length The range length + */ + private void retainPositions(int offset, int length) { + // TODO: use binary search + for (int i= 0, n= fRemovedPositions.size(); i < n; i++) { + HighlightedPosition position= (HighlightedPosition) fRemovedPositions.get(i); + if (position != null && position.isContained(offset, length)) { + fRemovedPositions.set(i, null); + fNOfRemovedPositions--; + } + } + } + } + + /** Position collector */ + private PositionCollector fCollector= new PositionCollector(); + + /** The semantic highlighting presenter */ + private SemanticHighlightingPresenter fPresenter = new SemanticHighlightingPresenter(); + /** Semantic highlightings */ + private SemanticHighlighting[] fSemanticHighlightings = SemanticHighlightings.getSemanticHighlightings(); + /** Highlightings */ + private List> fHighlightings = FluentIterable.from(Arrays.asList(fSemanticHighlightings)).transform(highlighting -> (List) ImmutableList.copyOf(highlighting.getScopes())).toList(); + + /** Background job's added highlighted positions */ + private List fAddedPositions= new ArrayList<>(); + /** Background job's removed highlighted positions */ + private List fRemovedPositions= new ArrayList<>(); + /** Number of removed positions */ + private int fNOfRemovedPositions; + + /** + * Reconcile operation lock. + * @since 3.2 + */ + private final Object fReconcileLock= new Object(); + /** + * true if any thread is executing + * reconcile, false otherwise. + * @since 3.2 + */ + private boolean fIsReconciling= false; + + /** The semantic highlighting presenter - cache for background thread, only valid during {@link #reconciled(CompilationUnit, boolean, IProgressMonitor)} */ + private SemanticHighlightingPresenter fJobPresenter = new SemanticHighlightingPresenter(); + /** Semantic highlightings - cache for background thread, only valid during {@link #reconciled(CompilationUnit, boolean, IProgressMonitor)} */ + private SemanticHighlighting[] fJobSemanticHighlightings; + /** Highlightings - cache for background thread, only valid during {@link #reconciled(CompilationUnit, boolean, IProgressMonitor)} */ + private List> fJobHighlightings; + + /** + * XXX Hack for performance reasons (should loop over fJobSemanticHighlightings can call consumes(*)) + * @since 3.5 + */ + private List fJobDeprecatedMemberHighlighting; + + public List reconciled(IDocument document, ASTNode ast, boolean forced, IProgressMonitor progressMonitor) throws BadPositionCategoryException { + // ensure at most one thread can be reconciling at any time + synchronized (fReconcileLock) { + if (fIsReconciling) { + return emptyList(); + } else { + fIsReconciling= true; + } + } + fJobPresenter= fPresenter; + fJobSemanticHighlightings= fSemanticHighlightings; + fJobHighlightings= fHighlightings; + + try { + if (ast == null) { + return emptyList(); + } + + ASTNode[] subtrees= getAffectedSubtrees(ast); + if (subtrees.length == 0) { + return emptyList(); + } + + startReconcilingPositions(); + + if (!fJobPresenter.isCanceled()) { + fJobDeprecatedMemberHighlighting= null; + for (int i= 0, n= fJobSemanticHighlightings.length; i < n; i++) { + SemanticHighlighting semanticHighlighting= fJobSemanticHighlightings[i]; + if (semanticHighlighting instanceof DeprecatedMemberHighlighting) { + fJobDeprecatedMemberHighlighting = fJobHighlightings.get(i); + break; + } + } + reconcilePositions(subtrees); + } + + // We need to manually install the position category + if (!document.containsPositionCategory(fPresenter.getPositionCategory())) { + document.addPositionCategory(fPresenter.getPositionCategory()); + } + + updatePresentation(document, fAddedPositions, fRemovedPositions); + stopReconcilingPositions(); + Position[] positions = document.getPositions(fPresenter.getPositionCategory()); + for (Position position : positions) { + Preconditions.checkState(position instanceof HighlightedPosition); + } + return FluentIterable.from(Arrays.asList(positions)).filter(HighlightedPosition.class).toList(); + } finally { + fJobPresenter= null; + fJobSemanticHighlightings= null; + fJobHighlightings= null; + fJobDeprecatedMemberHighlighting= null; + synchronized (fReconcileLock) { + fIsReconciling= false; + } + } + } + + /** + * @param node Root node + * @return Array of subtrees that may be affected by past document changes + */ + private ASTNode[] getAffectedSubtrees(ASTNode node) { + // TODO: only return nodes which are affected by document changes - would require an 'anchor' concept for taking distant effects into account + return new ASTNode[] { node }; + } + + /** + * Start reconciling positions. + */ + private void startReconcilingPositions() { + fJobPresenter.addAllPositions(fRemovedPositions); + fNOfRemovedPositions= fRemovedPositions.size(); + } + + /** + * Reconcile positions based on the AST subtrees + * + * @param subtrees the AST subtrees + */ + private void reconcilePositions(ASTNode[] subtrees) { + // FIXME: remove positions not covered by subtrees + + + + for (int i= 0, n= subtrees.length; i < n; i++) { + subtrees[i].accept(fCollector); + } + List oldPositions= fRemovedPositions; + List newPositions= new ArrayList<>(fNOfRemovedPositions); + for (int i= 0, n= oldPositions.size(); i < n; i ++) { + Position current= oldPositions.get(i); + if (current != null) { + newPositions.add(current); + } + } + fRemovedPositions= newPositions; + } + + /** + * Update the presentation. + * + * @param textPresentation the text presentation + * @param addedPositions the added positions + * @param removedPositions the removed positions + */ + private void updatePresentation(IDocument document, List addedPositions, List removedPositions) { + fJobPresenter.createUpdateRunnable(document, addedPositions, removedPositions); + } + + /** + * Stop reconciling positions. + */ + private void stopReconcilingPositions() { + fRemovedPositions.clear(); + fNOfRemovedPositions= 0; + fAddedPositions.clear(); + } + +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingService.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingService.java new file mode 100644 index 0000000000..d584d5778c --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingService.java @@ -0,0 +1,250 @@ +/******************************************************************************* + * Copyright (c) 2018 TypeFox 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * TypeFox - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.highlighting; + +import static com.google.common.base.Suppliers.memoize; +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Maps.newHashMap; +import static java.util.Collections.emptyList; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.manipulation.CoreASTProvider; +import org.eclipse.jdt.ls.core.internal.JDTUtils; +import org.eclipse.jdt.ls.core.internal.JavaClientConnection; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jdt.ls.core.internal.handlers.JsonRpcHelpers; +import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager; +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.Position; +import org.eclipse.lsp4j.SemanticHighlightingInformation; +import org.eclipse.lsp4j.SemanticHighlightingParams; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; +import org.eclipse.lsp4j.util.SemanticHighlightingTokens; + +import com.google.common.base.Supplier; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableBiMap.Builder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; + +/** + * A stateful service for installing, un-installing, and updating semantic + * highlighting information on the text documents. Behaves as a NOOP if the + * semantic highlighting support is disabled on the language client. + */ +public class SemanticHighlightingService { + + private static Builder> BUILDER = ImmutableBiMap.builder(); + static { + //@formatter:off + final AtomicInteger i = new AtomicInteger(); + Stream.of(SemanticHighlightings.getSemanticHighlightings()) + .map(SemanticHighlighting::getScopes) + .forEach(scopes -> BUILDER.put(i.getAndIncrement(), scopes)); + //@formatter:on + } + /** + * Lookup table for the scopes. + */ + private static BiMap> LOOKUP_TABLE = BUILDER.build(); + + /** + * Returns with a view of all scopes supported by the LS. + */ + public static List> getAllScopes() { + List> result = newArrayList(); + for (int i = 0; i < LOOKUP_TABLE.keySet().size(); i++) { + List scopes = LOOKUP_TABLE.get(i); + Assert.isNotNull(scopes, "No scopes are available for index: " + i); + result.add(scopes); + } + return ImmutableList.copyOf(result); + } + + /** + * Returns with the scopes for the given scope index. Never {@code null} nor + * empty. If not found, throws an exception. + */ + public static List getScopes(int index) { + List scopes = LOOKUP_TABLE.get(index); + Assert.isNotNull(scopes, "No scopes were registered for index: " + index); + return scopes; + } + + /** + * Returns with the scope for the index. If not found, throws an exception. + */ + public static int getIndex(List scopes) { + Integer index = LOOKUP_TABLE.inverse().get(scopes); + Assert.isNotNull(index, "Cannot get index for scopes: " + Iterables.toString(scopes)); + return index; + } + + /** + * Wraps contextual information about highlighted position changes. Although the + * instances are immutable, the references are not. + */ + public static class HighlightedPositionDiffContext { + + //@formatter:off + public final IDocument oldState; + public final IDocument newState; + public final DocumentEvent event; + public final List oldPositions; + public final List newPositions; + + public HighlightedPositionDiffContext( + IDocument oldState, + DocumentEvent event, + Iterable oldPositions, + Iterable newPositions) { + + this.oldState = oldState; + this.newState = event.fDocument; + this.event = event; + this.oldPositions = ImmutableList.copyOf(oldPositions); + this.newPositions = ImmutableList.copyOf(newPositions); + } + //@formatter:on + + } + + private final Supplier enabled; + private final JavaClientConnection connection; + private final Map> cache; + private CoreASTProvider astProvider; + private SemanticHighlightingDiffCalculator diffCalculator; + + public SemanticHighlightingService(JavaClientConnection connection, CoreASTProvider astProvider, PreferenceManager preferenceManager) { + this(connection, astProvider, memoize(() -> preferenceManager.getClientPreferences().isSemanticHighlightingSupported())); + } + + public SemanticHighlightingService(JavaClientConnection connection, CoreASTProvider astProvider, Supplier enabled) { + this.connection = connection; + this.astProvider = astProvider; + this.enabled = enabled; // XXX: move this out and have a factory instead, that creates a NOOP service instance. + this.cache = newHashMap(); + this.diffCalculator = new SemanticHighlightingDiffCalculator(); + } + + public boolean isEnabled() { + return enabled.get(); + } + + public void uninstall(String uri) { + if (enabled.get()) { + this.cache.remove(uri); + } + } + + public List install(ICompilationUnit unit) throws JavaModelException, BadPositionCategoryException { + if (enabled.get()) { + List positions = calculateHighlightedPositions(unit, false); + String uri = JDTUtils.getFileURI(unit.getResource()); + this.cache.put(uri, positions); + if (!positions.isEmpty()) { + IDocument document = JsonRpcHelpers.toDocument(unit.getBuffer()); + List infos = toInfos(document, positions); + VersionedTextDocumentIdentifier textDocument = new VersionedTextDocumentIdentifier(uri, 1); + notifyClient(textDocument, infos); + } + return ImmutableList.copyOf(positions); + } + return emptyList(); + } + + public List calculateHighlightedPositions(ICompilationUnit unit, boolean cache) throws JavaModelException, BadPositionCategoryException { + if (enabled.get()) { + IDocument document = JsonRpcHelpers.toDocument(unit.getBuffer()); + ASTNode ast = getASTNode(unit); + List positions = calculateHighlightedPositions(document, ast); + if (cache) { + String uri = JDTUtils.getFileURI(unit.getResource()); + this.cache.put(uri, positions); + } + return ImmutableList.copyOf(positions); + } + return emptyList(); + } + + public List getHighlightedPositions(String uri) { + return ImmutableList.copyOf(cache.getOrDefault(uri, emptyList())); + } + + public void update(VersionedTextDocumentIdentifier textDocument, List diffContexts) throws BadLocationException, BadPositionCategoryException, JavaModelException { + if (enabled.get() && !diffContexts.isEmpty()) { + List deltaInfos = newArrayList(); + for (HighlightedPositionDiffContext context : diffContexts) { + deltaInfos.addAll(diffCalculator.getDiffInfos(context)); + } + if (!deltaInfos.isEmpty()) { + notifyClient(textDocument, deltaInfos); + } + } + } + + protected List calculateHighlightedPositions(IDocument document, ASTNode ast) throws BadPositionCategoryException { + return new SemanticHighlightingReconciler().reconciled(document, ast, false, new NullProgressMonitor()); + } + + protected ASTNode getASTNode(ICompilationUnit unit) { + // TODO: This seems odd here. + // I had problems when opened the second compilation unit in the editor. + // It was still using the previous AST. + this.astProvider.disposeAST(); + return this.astProvider.getAST(unit, CoreASTProvider.WAIT_YES, new NullProgressMonitor()); + } + + protected List toInfos(IDocument document, List positions) { + Multimap infos = HashMultimap.create(); + for (HighlightedPosition position : positions) { + int[] lineAndColumn = JsonRpcHelpers.toLine(document, position.offset); + if (lineAndColumn == null) { + JavaLanguageServerPlugin.logError("Cannot locate line and column information for the semantic highlighting position: " + position + ". Skipping it."); + continue; + } + int line = lineAndColumn[0]; + int character = lineAndColumn[1]; + int length = position.length; + int scope = LOOKUP_TABLE.inverse().get(position.getHighlightingScopes()); + infos.put(line, new SemanticHighlightingTokens.Token(character, length, scope)); + } + //@formatter:off + return infos.asMap().entrySet().stream() + .map(entry -> new SemanticHighlightingInformation(entry.getKey(), SemanticHighlightingTokens.encode(entry.getValue()))) + .collect(Collectors.toList()); + //@formatter:on + } + + protected void notifyClient(VersionedTextDocumentIdentifier textDocument, List infos) { + if (infos.isEmpty()) { + return; + } + this.connection.semanticHighlighting(new SemanticHighlightingParams(textDocument, infos)); + } + +} diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightings.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightings.java new file mode 100644 index 0000000000..165854841f --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightings.java @@ -0,0 +1,1026 @@ +/******************************************************************************* + * Copyright (c) 2000, 2008 IBM Corporation 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + * Andre Soereng - [syntax highlighting] highlight numbers - https://bugs.eclipse.org/bugs/show_bug.cgi?id=63573 + * TypeFox - port to jdt.ls + * + * Copied from https://github.com/eclipse/eclipse.jdt.ui/blob/d41fa3326c5b75a6419c81fcecb37d7d7fb3ac43/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/SemanticHighlightings.java + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.highlighting; + +import java.util.List; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.AnnotationTypeMemberDeclaration; +import org.eclipse.jdt.core.dom.ArrayAccess; +import org.eclipse.jdt.core.dom.CastExpression; +import org.eclipse.jdt.core.dom.ClassInstanceCreation; +import org.eclipse.jdt.core.dom.ConditionalExpression; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.FieldDeclaration; +import org.eclipse.jdt.core.dom.IBinding; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.IVariableBinding; +import org.eclipse.jdt.core.dom.InfixExpression; +import org.eclipse.jdt.core.dom.MemberValuePair; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.Modifier; +import org.eclipse.jdt.core.dom.NameQualifiedType; +import org.eclipse.jdt.core.dom.ParameterizedType; +import org.eclipse.jdt.core.dom.PrefixExpression; +import org.eclipse.jdt.core.dom.QualifiedName; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.SimpleType; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor; +import org.eclipse.jdt.core.dom.VariableDeclaration; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.eclipse.jdt.internal.corext.dom.ASTNodes; +import org.eclipse.jdt.internal.corext.dom.Bindings; + +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ListMultimap; + +/** + * Semantic highlightings + * + * @since 3.0 + */ +@SuppressWarnings("restriction") +public class SemanticHighlightings { + + private static final ImmutableListMultimap.Builder SCOPES_BUILDER = ImmutableListMultimap.builder(); + + /** + * A named preference part that controls the highlighting of static final + * fields. + */ + public static final String STATIC_FINAL_FIELD = "staticFinalField"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(STATIC_FINAL_FIELD, "storage.modifier.static.java", "storage.modifier.final.java", "variable.other.definition.java", "meta.definition.variable.java", "meta.class.body.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of static fields. + */ + public static final String STATIC_FIELD = "staticField"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(STATIC_FIELD, "storage.modifier.static.java", "variable.other.definition.java", "meta.definition.variable.java", "meta.class.body.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of fields. + */ + public static final String FIELD = "field"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(FIELD, "meta.definition.variable.java", "meta.class.body.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of method + * declarations. + */ + public static final String METHOD_DECLARATION = "methodDeclarationName"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(METHOD_DECLARATION, "entity.name.function.java", "meta.method.identifier.java", "meta.method.java", "meta.class.body.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of static method + * invocations. + */ + public static final String STATIC_METHOD_INVOCATION = "staticMethodInvocation"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(STATIC_METHOD_INVOCATION, "storage.modifier.static.java", "entity.name.function.java", "meta.function-call.java", "meta.method.body.java", "meta.method.java", "meta.class.body.java", "meta.class.java", + "source.java"); + } + + /** + * A named preference part that controls the highlighting of inherited method + * invocations. + */ + public static final String INHERITED_METHOD_INVOCATION = "inheritedMethodInvocation"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(INHERITED_METHOD_INVOCATION, "entity.name.function.java", "meta.function-call.java", "meta.method.body.java", "meta.method.java", "meta.class.body.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of annotation element + * references. + * + * @since 3.1 + */ + public static final String ANNOTATION_ELEMENT_REFERENCE = "annotationElementReference"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(ANNOTATION_ELEMENT_REFERENCE, "constant.other.key.java", "meta.declaration.annotation.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of abstract method + * invocations. + */ + public static final String ABSTRACT_METHOD_INVOCATION = "abstractMethodInvocation"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(ABSTRACT_METHOD_INVOCATION, "storage.modifier.abstract.java", "entity.name.function.java", "meta.function-call.java", "meta.method.body.java", "meta.method.java", "meta.class.body.java", "meta.class.java", + "source.java"); + } + + /** + * A named preference part that controls the highlighting of local variables. + */ + public static final String LOCAL_VARIABLE_DECLARATION = "localVariableDeclaration"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(LOCAL_VARIABLE_DECLARATION, "variable.other.definition.java", "meta.definition.variable.java", "meta.method.body.java", "meta.method.java", "meta.class.body.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of local variables. + */ + public static final String LOCAL_VARIABLE = "localVariable"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(LOCAL_VARIABLE, "meta.method.body.java", "meta.method.java", "meta.class.body.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of parameter + * variables. + */ + public static final String PARAMETER_VARIABLE = "parameterVariable"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(PARAMETER_VARIABLE, "variable.parameter.java", "meta.method.identifier.java", "meta.method.java", "meta.class.body.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of deprecated members. + */ + public static final String DEPRECATED_MEMBER = "deprecatedMember"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(DEPRECATED_MEMBER, "invalid.deprecated.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of type parameters. + * + * @since 3.1 + */ + public static final String TYPE_VARIABLE = "typeParameter"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(TYPE_VARIABLE, "storage.type.generic.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of methods + * (invocations and declarations). + * + * @since 3.1 + */ + public static final String METHOD = "method"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(METHOD, "entity.name.function.java", "meta.method.identifier.java", "meta.function-call.java", "meta.method.java", "meta.class.body.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of auto(un)boxed + * expressions. + * + * @since 3.1 + */ + public static final String AUTOBOXING = "autoboxing"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(AUTOBOXING, "variable.other.autoboxing.java", "meta.method.body.java", "meta.method.java", "meta.class.body.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of classes. + * + * @since 3.2 + */ + public static final String CLASS = "class"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(CLASS, "entity.name.type.class.java", "meta.class.identifier.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of enums. + * + * @since 3.2 + */ + public static final String ENUM = "enum"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(ENUM, "entity.name.type.enum.java", "meta.enum.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of interfaces. + * + * @since 3.2 + */ + public static final String INTERFACE = "interface"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(INTERFACE, "entity.name.type.interface.java", "meta.class.identifier.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of annotations. + * + * @since 3.2 + */ + public static final String ANNOTATION = "annotation"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(ANNOTATION, "storage.type.annotation.java", "meta.declaration.annotation.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of type arguments. + * + * @since 3.2 + */ + public static final String TYPE_ARGUMENT = "typeArgument"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(TYPE_ARGUMENT, "storage.type.generic.java", "meta.definition.class.implemented.interfaces.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of numbers. + * + * @since 3.4 + */ + public static final String NUMBER = "number"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(NUMBER, "constant.numeric.decimal.java", "meta.definition.variable.java", "meta.method.body.java", "meta.method.java", "meta.class.body.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of abstract classes. + * + * @since 3.7 + */ + public static final String ABSTRACT_CLASS = "abstractClass"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(ABSTRACT_CLASS, "storage.modifier.abstract.java", "entity.name.type.class.java", "meta.class.identifier.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of inherited fields. + * + * @since 3.8 + */ + public static final String INHERITED_FIELD = "inheritedField"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(INHERITED_FIELD, "meta.function-call.java", "meta.method.body.java", "meta.method.java", "meta.class.body.java", "meta.class.java", "source.java"); + } + + /** + * A named preference part that controls the highlighting of 'var' keywords. + */ + public static final String VAR_KEYWORD = "varKeyword"; //$NON-NLS-1$ + static { + SCOPES_BUILDER.putAll(VAR_KEYWORD, "keyword.other.var.java", "meta.class.body.java", "meta.class.java", "source.java"); + } + + private static final ListMultimap SCOPES = SCOPES_BUILDER.build(); + + /** + * Semantic highlightings + */ + private static SemanticHighlighting[] fgSemanticHighlightings; + + /** + * Semantic highlighting for static final fields. + */ + private static final class StaticFinalFieldHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(STATIC_FINAL_FIELD); + } + + @Override + public boolean consumes(SemanticToken token) { + IBinding binding = token.getBinding(); + return binding != null && binding.getKind() == IBinding.VARIABLE && ((IVariableBinding) binding).isField() && (binding.getModifiers() & (Modifier.FINAL | Modifier.STATIC)) == (Modifier.FINAL | Modifier.STATIC); + } + } + + /** + * Semantic highlighting for static fields. + */ + private static final class StaticFieldHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(STATIC_FIELD); + } + + @Override + public boolean consumes(SemanticToken token) { + IBinding binding = token.getBinding(); + return binding != null && binding.getKind() == IBinding.VARIABLE && ((IVariableBinding) binding).isField() && (binding.getModifiers() & Modifier.STATIC) == Modifier.STATIC; + } + } + + /** + * Semantic highlighting for fields. + */ + private static final class FieldHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(FIELD); + } + + @Override + public boolean consumes(SemanticToken token) { + IBinding binding = token.getBinding(); + return binding != null && binding.getKind() == IBinding.VARIABLE && ((IVariableBinding) binding).isField(); + } + } + + /** + * Semantic highlighting for auto(un)boxed expressions. + */ + private static final class AutoboxHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(AUTOBOXING); + } + + @Override + public boolean consumesLiteral(SemanticToken token) { + return isAutoUnBoxing(token.getLiteral()); + } + + @Override + public boolean consumes(SemanticToken token) { + return isAutoUnBoxing(token.getNode()); + } + + private boolean isAutoUnBoxing(Expression node) { + if (isAutoUnBoxingExpression(node)) { + return true; + } + // special cases: the autoboxing conversions happens at a + // location that is not mapped directly to a simple name + // or a literal, but can still be mapped somehow + // A) expressions + StructuralPropertyDescriptor desc = node.getLocationInParent(); + if (desc == ArrayAccess.ARRAY_PROPERTY || desc == InfixExpression.LEFT_OPERAND_PROPERTY || desc == InfixExpression.RIGHT_OPERAND_PROPERTY || desc == ConditionalExpression.THEN_EXPRESSION_PROPERTY + || desc == PrefixExpression.OPERAND_PROPERTY || desc == CastExpression.EXPRESSION_PROPERTY || desc == ConditionalExpression.ELSE_EXPRESSION_PROPERTY) { + ASTNode parent = node.getParent(); + if (parent instanceof Expression) { + return isAutoUnBoxingExpression((Expression) parent); + } + } + // B) constructor invocations + if (desc == QualifiedName.NAME_PROPERTY) { + node = (Expression) node.getParent(); + desc = node.getLocationInParent(); + } + if (desc == SimpleType.NAME_PROPERTY || desc == NameQualifiedType.NAME_PROPERTY) { + ASTNode parent = node.getParent(); + if (parent != null && parent.getLocationInParent() == ClassInstanceCreation.TYPE_PROPERTY) { + parent = parent.getParent(); + return isAutoUnBoxingExpression((ClassInstanceCreation) parent); + } + } + return false; + } + + private boolean isAutoUnBoxingExpression(Expression expression) { + return expression.resolveBoxing() || expression.resolveUnboxing(); + } + } + + /** + * Semantic highlighting for method declarations. + */ + private static final class MethodDeclarationHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(METHOD_DECLARATION); + } + + @Override + public boolean consumes(SemanticToken token) { + StructuralPropertyDescriptor location = token.getNode().getLocationInParent(); + return location == MethodDeclaration.NAME_PROPERTY || location == AnnotationTypeMemberDeclaration.NAME_PROPERTY; + } + } + + /** + * Semantic highlighting for static method invocations. + */ + private static final class StaticMethodInvocationHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(STATIC_METHOD_INVOCATION); + } + + @Override + public boolean consumes(SemanticToken token) { + SimpleName node = token.getNode(); + if (node.isDeclaration()) { + return false; + } + + IBinding binding = token.getBinding(); + return binding != null && binding.getKind() == IBinding.METHOD && (binding.getModifiers() & Modifier.STATIC) == Modifier.STATIC; + } + } + + /** + * Semantic highlighting for annotation element references. + * + * @since 3.1 + */ + private static final class AnnotationElementReferenceHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(ANNOTATION_ELEMENT_REFERENCE); + } + + @Override + public boolean consumes(SemanticToken token) { + SimpleName node = token.getNode(); + if (node.getParent() instanceof MemberValuePair) { + IBinding binding = token.getBinding(); + boolean isAnnotationElement = binding != null && binding.getKind() == IBinding.METHOD; + + return isAnnotationElement; + } + + return false; + } + } + + /** + * Semantic highlighting for abstract method invocations. + */ + private static final class AbstractMethodInvocationHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(ABSTRACT_METHOD_INVOCATION); + } + + @Override + public boolean consumes(SemanticToken token) { + SimpleName node = token.getNode(); + if (node.isDeclaration()) { + return false; + } + + IBinding binding = token.getBinding(); + boolean isAbstractMethod = binding != null && binding.getKind() == IBinding.METHOD && (binding.getModifiers() & Modifier.ABSTRACT) == Modifier.ABSTRACT; + if (!isAbstractMethod) { + return false; + } + + // filter out annotation value references + if (binding != null) { + ITypeBinding declaringType = ((IMethodBinding) binding).getDeclaringClass(); + if (declaringType.isAnnotation()) { + return false; + } + } + + return true; + } + } + + /** + * Semantic highlighting for inherited method invocations. + */ + private static final class InheritedMethodInvocationHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(INHERITED_METHOD_INVOCATION); + } + + @Override + public boolean consumes(SemanticToken token) { + SimpleName node = token.getNode(); + if (node.isDeclaration()) { + return false; + } + + IBinding binding = token.getBinding(); + if (binding == null || binding.getKind() != IBinding.METHOD) { + return false; + } + + ITypeBinding currentType = Bindings.getBindingOfParentType(node); + ITypeBinding declaringType = ((IMethodBinding) binding).getDeclaringClass(); + if (currentType == declaringType || currentType == null) { + return false; + } + + return Bindings.isSuperType(declaringType, currentType); + } + } + + /** + * Semantic highlighting for inherited method invocations. + */ + private static final class MethodHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(METHOD); + } + + @Override + public boolean consumes(SemanticToken token) { + IBinding binding = getBinding(token); + return binding != null && binding.getKind() == IBinding.METHOD; + } + } + + /** + * Semantic highlighting for local variable declarations. + */ + private static final class LocalVariableDeclarationHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(LOCAL_VARIABLE_DECLARATION); + } + + @Override + public boolean consumes(SemanticToken token) { + SimpleName node = token.getNode(); + StructuralPropertyDescriptor location = node.getLocationInParent(); + if (location == VariableDeclarationFragment.NAME_PROPERTY || location == SingleVariableDeclaration.NAME_PROPERTY) { + ASTNode parent = node.getParent(); + if (parent instanceof VariableDeclaration) { + parent = parent.getParent(); + return parent == null || !(parent instanceof FieldDeclaration); + } + } + return false; + } + } + + /** + * Semantic highlighting for local variables. + */ + private static final class LocalVariableHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(LOCAL_VARIABLE); + } + + @Override + public boolean consumes(SemanticToken token) { + IBinding binding = token.getBinding(); + if (binding != null && binding.getKind() == IBinding.VARIABLE && !((IVariableBinding) binding).isField()) { + ASTNode decl = token.getRoot().findDeclaringNode(binding); + return decl instanceof VariableDeclaration; + } + return false; + } + } + + /** + * Semantic highlighting for parameter variables. + */ + private static final class ParameterVariableHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(PARAMETER_VARIABLE); + } + + @Override + public boolean consumes(SemanticToken token) { + IBinding binding = token.getBinding(); + if (binding != null && binding.getKind() == IBinding.VARIABLE && !((IVariableBinding) binding).isField()) { + ASTNode decl = token.getRoot().findDeclaringNode(binding); + return decl != null && decl.getLocationInParent() == MethodDeclaration.PARAMETERS_PROPERTY; + } + return false; + } + } + + /** + * Semantic highlighting for deprecated members. + */ + /*default*/ static final class DeprecatedMemberHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(DEPRECATED_MEMBER); + } + + @Override + public boolean consumes(SemanticToken token) { + IBinding binding = getBinding(token); + if (binding != null) { + if (binding.isDeprecated()) { + return true; + } + if (binding instanceof IMethodBinding) { + IMethodBinding methodBinding = (IMethodBinding) binding; + ITypeBinding declaringClass = methodBinding.getDeclaringClass(); + if (declaringClass == null) { + return false; + } + if (declaringClass.isAnonymous()) { + ITypeBinding[] interfaces = declaringClass.getInterfaces(); + if (interfaces.length > 0) { + return interfaces[0].isDeprecated(); + } else { + return declaringClass.getSuperclass().isDeprecated(); + } + } + return declaringClass.isDeprecated() && !(token.getNode().getParent() instanceof MethodDeclaration); + } else if (binding instanceof IVariableBinding) { + IVariableBinding variableBinding = (IVariableBinding) binding; + ITypeBinding declaringClass = variableBinding.getDeclaringClass(); + return declaringClass != null && declaringClass.isDeprecated() && !(token.getNode().getParent() instanceof VariableDeclaration); + } + } + return false; + } + } + + /** + * Semantic highlighting for type variables. + * + * @since 3.1 + */ + private static final class TypeVariableHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(TYPE_VARIABLE); + } + + @Override + public boolean consumes(SemanticToken token) { + + // 1: match types in type parameter lists + SimpleName name = token.getNode(); + ASTNode node = name.getParent(); + if (node.getNodeType() != ASTNode.SIMPLE_TYPE && node.getNodeType() != ASTNode.TYPE_PARAMETER) { + return false; + } + + // 2: match generic type variable references + IBinding binding = token.getBinding(); + return binding instanceof ITypeBinding && ((ITypeBinding) binding).isTypeVariable(); + } + } + + /** + * Semantic highlighting for classes. + * + * @since 3.2 + */ + private static final class ClassHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(CLASS); + } + + @Override + public boolean consumes(SemanticToken token) { + + // 1: match types + SimpleName name = token.getNode(); + ASTNode node = name.getParent(); + int nodeType = node.getNodeType(); + if (nodeType != ASTNode.SIMPLE_TYPE && nodeType != ASTNode.THIS_EXPRESSION && nodeType != ASTNode.QUALIFIED_TYPE && nodeType != ASTNode.QUALIFIED_NAME && nodeType != ASTNode.TYPE_DECLARATION + && nodeType != ASTNode.METHOD_INVOCATION) { + return false; + } + while (nodeType == ASTNode.QUALIFIED_NAME) { + node = node.getParent(); + nodeType = node.getNodeType(); + if (nodeType == ASTNode.IMPORT_DECLARATION) { + return false; + } + } + + // 2: match classes + IBinding binding = token.getBinding(); + return binding instanceof ITypeBinding && ((ITypeBinding) binding).isClass(); + } + } + + /** + * Semantic highlighting for enums. + * + * @since 3.2 + */ + private static final class EnumHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(ENUM); + } + + @Override + public boolean consumes(SemanticToken token) { + + // 1: match types + SimpleName name = token.getNode(); + ASTNode node = name.getParent(); + int nodeType = node.getNodeType(); + if (nodeType != ASTNode.METHOD_INVOCATION && nodeType != ASTNode.SIMPLE_TYPE && nodeType != ASTNode.QUALIFIED_TYPE && nodeType != ASTNode.QUALIFIED_NAME && nodeType != ASTNode.QUALIFIED_NAME + && nodeType != ASTNode.ENUM_DECLARATION) { + return false; + } + while (nodeType == ASTNode.QUALIFIED_NAME) { + node = node.getParent(); + nodeType = node.getNodeType(); + if (nodeType == ASTNode.IMPORT_DECLARATION) { + return false; + } + } + + // 2: match enums + IBinding binding = token.getBinding(); + return binding instanceof ITypeBinding && ((ITypeBinding) binding).isEnum(); + } + } + + /** + * Semantic highlighting for interfaces. + * + * @since 3.2 + */ + private static final class InterfaceHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(INTERFACE); + } + + @Override + public boolean consumes(SemanticToken token) { + + // 1: match types + SimpleName name = token.getNode(); + ASTNode node = name.getParent(); + int nodeType = node.getNodeType(); + if (nodeType != ASTNode.SIMPLE_TYPE && nodeType != ASTNode.QUALIFIED_TYPE && nodeType != ASTNode.QUALIFIED_NAME && nodeType != ASTNode.TYPE_DECLARATION) { + return false; + } + while (nodeType == ASTNode.QUALIFIED_NAME) { + node = node.getParent(); + nodeType = node.getNodeType(); + if (nodeType == ASTNode.IMPORT_DECLARATION) { + return false; + } + } + + // 2: match interfaces + IBinding binding = token.getBinding(); + return binding instanceof ITypeBinding && ((ITypeBinding) binding).isInterface(); + } + } + + /** + * Semantic highlighting for annotation types. + * + * @since 3.2 + */ + private static final class AnnotationHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(ANNOTATION); + } + + @Override + public boolean consumes(SemanticToken token) { + + // 1: match types + SimpleName name = token.getNode(); + ASTNode node = name.getParent(); + int nodeType = node.getNodeType(); + if (nodeType != ASTNode.SIMPLE_TYPE && nodeType != ASTNode.QUALIFIED_TYPE && nodeType != ASTNode.QUALIFIED_NAME && nodeType != ASTNode.ANNOTATION_TYPE_DECLARATION && nodeType != ASTNode.MARKER_ANNOTATION + && nodeType != ASTNode.NORMAL_ANNOTATION && nodeType != ASTNode.SINGLE_MEMBER_ANNOTATION) { + return false; + } + while (nodeType == ASTNode.QUALIFIED_NAME) { + node = node.getParent(); + nodeType = node.getNodeType(); + if (nodeType == ASTNode.IMPORT_DECLARATION) { + return false; + } + } + + // 2: match annotations + IBinding binding = token.getBinding(); + return binding instanceof ITypeBinding && ((ITypeBinding) binding).isAnnotation(); + } + } + + /** + * Semantic highlighting for annotation types. + * + * @since 3.2 + */ + private static final class TypeArgumentHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(TYPE_ARGUMENT); + } + + @Override + public boolean consumes(SemanticToken token) { + + // 1: match types + SimpleName name = token.getNode(); + ASTNode node = name.getParent(); + int nodeType = node.getNodeType(); + if (nodeType != ASTNode.SIMPLE_TYPE && nodeType != ASTNode.QUALIFIED_TYPE) { + return false; + } + + // 2: match type arguments + StructuralPropertyDescriptor locationInParent = node.getLocationInParent(); + if (locationInParent == ParameterizedType.TYPE_ARGUMENTS_PROPERTY) { + return true; + } + + return false; + } + } + + /** + * Semantic highlighting for numbers. + * + * @since 3.4 + */ + private static final class NumberHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(NUMBER); + } + + @Override + public boolean consumes(SemanticToken token) { + return false; + } + + @Override + public boolean consumesLiteral(SemanticToken token) { + Expression expr = token.getLiteral(); + return expr != null && expr.getNodeType() == ASTNode.NUMBER_LITERAL; + } + } + + /** + * Semantic highlighting for classes. + * + * @since 3.7 + */ + private static final class AbstractClassHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(ABSTRACT_CLASS); + } + + @Override + public boolean consumes(SemanticToken token) { + + // 1: match types + SimpleName name = token.getNode(); + ASTNode node = name.getParent(); + int nodeType = node.getNodeType(); + if (nodeType != ASTNode.SIMPLE_TYPE && nodeType != ASTNode.THIS_EXPRESSION && nodeType != ASTNode.QUALIFIED_TYPE && nodeType != ASTNode.QUALIFIED_NAME && nodeType != ASTNode.TYPE_DECLARATION + && nodeType != ASTNode.METHOD_INVOCATION) { + return false; + } + while (nodeType == ASTNode.QUALIFIED_NAME) { + node = node.getParent(); + nodeType = node.getNodeType(); + if (nodeType == ASTNode.IMPORT_DECLARATION) { + return false; + } + } + + // 2: match classes + IBinding binding = token.getBinding(); + if (binding instanceof ITypeBinding) { + ITypeBinding typeBinding = (ITypeBinding) binding; + // see also ClassHighlighting + return typeBinding.isClass() && (typeBinding.getModifiers() & Modifier.ABSTRACT) != 0; + } + + return false; + } + } + + /** + * Semantic highlighting for inherited field access. + * + * @since 3.8 + */ + private static final class InheritedFieldHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(INHERITED_FIELD); + } + + @Override + public boolean consumes(final SemanticToken token) { + final SimpleName node = token.getNode(); + if (node.isDeclaration()) { + return false; + } + + final IBinding binding = token.getBinding(); + if (binding == null || binding.getKind() != IBinding.VARIABLE) { + return false; + } + + ITypeBinding currentType = Bindings.getBindingOfParentType(node); + ITypeBinding declaringType = ((IVariableBinding) binding).getDeclaringClass(); + if (declaringType == null || currentType == declaringType) { + return false; + } + + return Bindings.isSuperType(declaringType, currentType); + } + } + + /** + * Semantic highlighting for 'var' keyword. + */ + /*default*/ static final class VarKeywordHighlighting implements SemanticHighlighting { + + @Override + public List getScopes() { + return SCOPES.get(VAR_KEYWORD); + } + + @Override + public boolean consumes(SemanticToken token) { + return false; + } + } + + /** + * @return The semantic highlightings, the order defines the precedence of + * matches, the first match wins. + */ + public static SemanticHighlighting[] getSemanticHighlightings() { + if (fgSemanticHighlightings == null) { + fgSemanticHighlightings = new SemanticHighlighting[] { new DeprecatedMemberHighlighting(), new AutoboxHighlighting(), new StaticFinalFieldHighlighting(), new StaticFieldHighlighting(), new InheritedFieldHighlighting(), + new FieldHighlighting(), new MethodDeclarationHighlighting(), new StaticMethodInvocationHighlighting(), new AbstractMethodInvocationHighlighting(), new AnnotationElementReferenceHighlighting(), + new InheritedMethodInvocationHighlighting(), new ParameterVariableHighlighting(), new LocalVariableDeclarationHighlighting(), new LocalVariableHighlighting(), new TypeVariableHighlighting(), // before type arguments! + new MethodHighlighting(), // before types to get ctors + new TypeArgumentHighlighting(), // before other types + new AbstractClassHighlighting(), // before classes + new ClassHighlighting(), new EnumHighlighting(), new AnnotationHighlighting(), // before interfaces + new InterfaceHighlighting(), new NumberHighlighting(), new VarKeywordHighlighting(), }; + } + return fgSemanticHighlightings; + } + + /** + * Extracts the binding from the token's simple name. Works around bug 62605 to + * return the correct constructor binding in a ClassInstanceCreation. + * + * @param token + * the token to extract the binding from + * @return the token's binding, or null + */ + private static IBinding getBinding(SemanticToken token) { + ASTNode node = token.getNode(); + ASTNode normalized = ASTNodes.getNormalizedNode(node); + if (normalized.getLocationInParent() == ClassInstanceCreation.TYPE_PROPERTY) { + // work around: https://bugs.eclipse.org/bugs/show_bug.cgi?id=62605 + return ((ClassInstanceCreation) normalized.getParent()).resolveConstructorBinding(); + } + return token.getBinding(); + } + + /** + * Do not instantiate + */ + private SemanticHighlightings() { + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticToken.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticToken.java new file mode 100644 index 0000000000..004aeee507 --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticToken.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 2000, 2008 IBM Corporation 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + * TypeFox - port to jdt.ls + * + * Copied from https://github.com/eclipse/eclipse.jdt.ui/blob/d41fa3326c5b75a6419c81fcecb37d7d7fb3ac43/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/SemanticToken.java + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.highlighting; + +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.IBinding; +import org.eclipse.jdt.core.dom.SimpleName; + +/** + * Semantic token + */ +public final class SemanticToken { + + /** AST node */ + private SimpleName fNode; + private Expression fLiteral; + + /** Binding */ + private IBinding fBinding; + /** Is the binding resolved? */ + private boolean fIsBindingResolved = false; + + /** AST root */ + private CompilationUnit fRoot; + private boolean fIsRootResolved = false; + + /** + * @return Returns the binding, can be null. + */ + public IBinding getBinding() { + if (!fIsBindingResolved) { + fIsBindingResolved = true; + if (fNode != null) { + fBinding = fNode.resolveBinding(); + } + } + + return fBinding; + } + + /** + * @return the AST node (a {@link SimpleName}) + */ + public SimpleName getNode() { + return fNode; + } + + /** + * @return the AST node (a Boolean-, Character- or NumberLiteral) + */ + public Expression getLiteral() { + return fLiteral; + } + + /** + * @return the AST root + */ + public CompilationUnit getRoot() { + if (!fIsRootResolved) { + fIsRootResolved = true; + fRoot = (CompilationUnit) (fNode != null ? fNode : fLiteral).getRoot(); + } + + return fRoot; + } + + /** + * Update this token with the given AST node. + *

+ * NOTE: Allowed to be used by {@link SemanticHighlightingReconciler} only. + *

+ * + * @param node + * the AST simple name + */ + void update(SimpleName node) { + clear(); + fNode = node; + } + + /** + * Update this token with the given AST node. + *

+ * NOTE: Allowed to be used by {@link SemanticHighlightingReconciler} only. + *

+ * + * @param literal + * the AST literal + */ + void update(Expression literal) { + clear(); + fLiteral = literal; + } + + /** + * Clears this token. + *

+ * NOTE: Allowed to be used by {@link SemanticHighlightingReconciler} only. + *

+ */ + void clear() { + fNode = null; + fLiteral = null; + fBinding = null; + fIsBindingResolved = false; + fRoot = null; + fIsRootResolved = false; + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java index 5913269aaa..54d1b332f5 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java @@ -188,6 +188,13 @@ public boolean isHierarchicalDocumentSymbolSupported() { && capabilities.getTextDocument().getDocumentSymbol() != null && capabilities.getTextDocument().getDocumentSymbol().getHierarchicalDocumentSymbolSupport() != null && capabilities.getTextDocument().getDocumentSymbol().getHierarchicalDocumentSymbolSupport().booleanValue(); + } + + public boolean isSemanticHighlightingSupported() { + //@formatter:off + return v3supported && capabilities.getTextDocument().getSemanticHighlightingCapabilities() != null + && capabilities.getTextDocument().getSemanticHighlightingCapabilities().getSemanticHighlighting() != null + && capabilities.getTextDocument().getSemanticHighlightingCapabilities().getSemanticHighlighting().booleanValue(); //@formatter:on } } diff --git a/org.eclipse.jdt.ls.target/org.eclipse.jdt.ls.tp.target b/org.eclipse.jdt.ls.target/org.eclipse.jdt.ls.tp.target index 49c3f9ac49..dc685cb38b 100644 --- a/org.eclipse.jdt.ls.target/org.eclipse.jdt.ls.tp.target +++ b/org.eclipse.jdt.ls.target/org.eclipse.jdt.ls.tp.target @@ -38,7 +38,6 @@ - diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingTest.java new file mode 100644 index 0000000000..1c05919ca4 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/highlighting/SemanticHighlightingTest.java @@ -0,0 +1,818 @@ +/******************************************************************************* + * Copyright (c) 2018 TypeFox. 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * TypeFox. - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.highlighting; + +import static com.google.common.collect.Lists.newArrayList; +import static org.eclipse.lsp4j.util.SemanticHighlightingTokens.decode; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.ls.core.internal.JDTUtils; +import org.eclipse.jdt.ls.core.internal.JavaClientConnection; +import org.eclipse.jdt.ls.core.internal.handlers.DocumentLifeCycleHandler; +import org.eclipse.jdt.ls.core.internal.highlighting.SemanticHighlightingService; +import org.eclipse.jdt.ls.core.internal.managers.AbstractProjectsManagerBasedTest; +import org.eclipse.jdt.ls.core.internal.preferences.ClientPreferences; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SemanticHighlightingInformation; +import org.eclipse.lsp4j.SemanticHighlightingParams; +import org.eclipse.lsp4j.TextDocumentContentChangeEvent; +import org.eclipse.lsp4j.TextDocumentItem; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; +import org.eclipse.lsp4j.util.SemanticHighlightingTokens; +import org.eclipse.lsp4j.util.SemanticHighlightingTokens.Token; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import com.google.common.collect.FluentIterable; +import com.google.common.collect.Lists; + +@RunWith(MockitoJUnitRunner.class) +public class SemanticHighlightingTest extends AbstractProjectsManagerBasedTest { + + //@formatter:off + private static final String CONTENT = "package _package;\n" + + "\n" + + "abstract class AnyClass {\n" + + "\n" + + " protected String inheritedField = \"\";\n" + + "\n" + + " abstract void abstractMethod(String s);\n" + + "\n" + + " public static void staticMethod() {\n" + + " }\n" + + "\n" + + "}\n" + + "\n" + + "abstract class AbstractClassName {\n" + + "\n" + + "}\n" + + "\n" + + "interface InterfaceName {\n" + + "\n" + + " default Integer bar(Integer number) {\n" + + " return number + 1;\n" + + " }\n" + + "\n" + + "}\n" + + "\n" + + "/**\n" + + " * This is about ClassName.\n" + + " * {@link com.yourCompany.aPackage.Interface}\n" + + " * @author author\n" + + " * @deprecated use OtherClass\n" + + " */\n" + + "public class ClassName extends AnyClass implements InterfaceName {\n" + + " enum Color { RED, GREEN, BLUE };\n" + + " /* This comment may span multiple lines. */\n" + + " static Object staticField;\n" + + " // This comment may span only this line\n" + + " private E field;\n" + + " private AbstractClassName field2;\n" + + " // TASK: refactor\n" + + " @SuppressWarnings(value=\"all\")\n" + + " public int foo(Integer parameter) {\n" + + " abstractMethod(inheritedField);\n" + + " int local= 42*hashCode();\n" + + " staticMethod();\n" + + " Thread.currentThread().stop();\n" + + " return bar(local) + parameter;\n" + + " }\n" + + "\n" + + " public void abstractMethod(String s) { }\n" + + "}"; + //@formatter:off + + private DocumentLifeCycleHandler lifeCycleHandler; + private TestJavaClientConnection javaClient; + + @Override + protected ClientPreferences initPreferenceManager(boolean supportClassFileContents) { + ClientPreferences clientPreferences = super.initPreferenceManager(supportClassFileContents); + when(clientPreferences.isClassFileContentSupported()).thenReturn(supportClassFileContents); + return clientPreferences; + } + + @Before + public void setup() throws Exception { + javaClient = new TestJavaClientConnection(client); + lifeCycleHandler = new DocumentLifeCycleHandler(javaClient, preferenceManager, projectsManager, false); + } + + @After + public void tearDown() throws Exception { + javaClient.disconnect(); + for (ICompilationUnit unit : JavaCore.getWorkingCopies(null)) { + unit.discardWorkingCopy(); + } + } + + @Test + public void testDidOpen() throws Exception { + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("ClassName.java", CONTENT, false, null); + openDocument(unit, unit.getSource(), 1); + assertEquals(1, javaClient.params.size()); + assertTrue(!javaClient.params.get(0).getLines().isEmpty()); + } + + @Test + public void testDidOpen_autoBoxing() throws Exception { + String content = "package _package;\n" + + "\n" + + "public class A {\n" + + " public static void main(String[] args) {\n" + + " Integer integer = Integer.valueOf(36);\n" + + " System.out.println(10 + integer);\n" + + " }\n" + + "}"; + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + + assertEquals(1, javaClient.params.size()); + List lines = javaClient.params.get(0).getLines(); + assertEquals(4, lines.size()); + SemanticHighlightingInformation line5 = FluentIterable.from(lines).firstMatch(line -> line.getLine() == 5).get(); + SemanticHighlightingTokens.Token unboxingToken = FluentIterable.from(decode(line5.getTokens())).firstMatch(token -> token.character == 28).get(); + assertEquals(unboxingToken.length, 7); + assertThat(SemanticHighlightingService.getScopes(unboxingToken.scope), hasItem("variable.other.autoboxing.java")); + } + + @Test + public void testDidChange() throws Exception { + StringBuilder sb = new StringBuilder(); + String beginContent = "package _package;\n\npublic class A { }\n"; + String endContent = "class B { }\n"; + sb.append(beginContent); + sb.append(endContent); + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", sb.toString(), false, null); + openDocument(unit, unit.getSource(), version); + + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(2, params.getLines().size()); + + SemanticHighlightingInformation informationA = params.getLines().get(0); + assertEquals(2, informationA.getLine()); + List informationATokens = decode(informationA.getTokens()); + assertEquals(1, informationATokens.size()); + SemanticHighlightingTokens.Token tokenA = informationATokens.get(0); + assertEquals(13, tokenA.character); + assertEquals(1, tokenA.length); + assertThat(SemanticHighlightingService.getScopes(tokenA.scope), hasItem("entity.name.type.class.java")); + + SemanticHighlightingInformation informationB = params.getLines().get(1); + assertEquals(3, informationB.getLine()); + List informationBTokens = decode(informationB.getTokens()); + assertEquals(1, informationBTokens.size()); + SemanticHighlightingTokens.Token tokenB = informationBTokens.get(0); + assertEquals(6, tokenB.character); + assertEquals(1, tokenB.length); + assertThat(SemanticHighlightingService.getScopes(tokenB.scope), hasItem("entity.name.type.class.java")); + + javaClient.params.clear(); + String insertContent = "class InsertedClass { }\n"; + Range insertRange = JDTUtils.toRange(unit, beginContent.length(), 0); + changeDocument(unit, insertContent, version++, insertRange, 0); + params = javaClient.params.get(0); + + informationB = params.getLines().get(0); + informationBTokens = decode(informationB.getTokens()); + assertEquals(3, informationB.getLine()); + assertEquals(1, informationBTokens.size()); + tokenB = informationBTokens.get(0); + assertEquals(6, tokenB.character); + assertEquals(13, tokenB.length); + assertThat(SemanticHighlightingService.getScopes(tokenB.scope), hasItem("entity.name.type.class.java")); + } + + @Test + public void testDidChange_insertNewLineAbove() throws Exception { + //@formatter:off + String content = "package _package;\n" + + "\n" + + "public class A { }\n" + + "class SB { }\n" + + "class SC { }"; + //@formatter:on + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(3, params.getLines().size()); + + javaClient.params.clear(); + String insertContent = "\n"; + Range insertRange = JDTUtils.toRange(unit, 0, 0); + changeDocument(unit, insertContent, version++, insertRange, 0); + assertEquals(0, javaClient.params.size()); + } + + @Test + public void testDidChange_insertNewLineBelow() throws Exception { + //@formatter:off + String content = "package _package;\n" + + "\n" + + "public class A { }\n" + + "class SB { }\n" + + "class SC { }"; + //@formatter:on + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(3, params.getLines().size()); + + javaClient.params.clear(); + String insertContent = "\n"; + Range insertRange = JDTUtils.toRange(unit, content.length(), 0); + changeDocument(unit, insertContent, version++, insertRange, 0); + assertEquals(0, javaClient.params.size()); + } + + @Test + public void testDidChange_insertNewLineMiddle() throws Exception { + //@formatter:off + String content = "package _package;\n" + + "\n" + + "public class A { }\n" + + "class SB { }\n" + + "class SC { }"; + //@formatter:on + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(3, params.getLines().size()); + + javaClient.params.clear(); + String insertContent = "\n"; + Range insertRange = JDTUtils.toRange(unit, content.indexOf("class SC"), 0); + changeDocument(unit, insertContent, version++, insertRange, 0); + assertEquals(0, javaClient.params.size()); + } + + @Test + public void testDidChange_deleteNewLineAbove() throws Exception { + //@formatter:off + String content = "// delete this line\n" + + "package _package;\n" + + "\n" + + "public class A { }\n" + + "class SB { }\n" + + "class SC { }"; + //@formatter:on + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(3, params.getLines().size()); + + javaClient.params.clear(); + Range deleteRange = JDTUtils.toRange(unit, content.indexOf("// delete this line"), "// delete this line\n".length()); + changeDocument(unit, "", version++, deleteRange, "// delete this line\n".length()); + assertEquals(0, javaClient.params.size()); + } + + @Test + public void testDidChange_deleteNewLineBelow() throws Exception { + //@formatter:off + String content = "package _package;\n" + + "\n" + + "public class A { }\n" + + "class SB { }\n" + + "class SC { }\n" + + "// delete this line\n"; + //@formatter:on + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(3, params.getLines().size()); + + javaClient.params.clear(); + Range deleteRange = JDTUtils.toRange(unit, content.indexOf("// delete this line"), "// delete this line\n".length()); + changeDocument(unit, "", version++, deleteRange, "// delete this line\n".length()); + assertEquals(0, javaClient.params.size()); + } + + @Test + public void testDidChange_deleteNewLineMiddle() throws Exception { + //@formatter:off + String content = "package _package;\n" + + "\n" + + "// delete this line\n" + + "\n" + + "public class A { }\n" + + "class SB { }\n" + + "class SC { }"; + //@formatter:on + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(3, params.getLines().size()); + + javaClient.params.clear(); + Range deleteRange = JDTUtils.toRange(unit, content.indexOf("// delete this line"), "// delete this line\n".length()); + changeDocument(unit, "", version++, deleteRange, "// delete this line\n".length()); + assertEquals(0, javaClient.params.size()); + } + + @Test + public void testDidChange_editLineMiddle_Add() throws Exception { + //@formatter:off + String content = "package _package;\n" + + "\n" + + "public class A { }\n" + + "class SB { }\n" + + "class SC { }"; + //@formatter:on + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(3, params.getLines().size()); + + javaClient.params.clear(); + Range editRange = JDTUtils.toRange(unit, content.indexOf("public class A"), "public class A".length()); + changeDocument(unit, "class SA { } public class A", version++, editRange, "public class A".length()); + assertEquals(1, javaClient.params.size()); + assertEquals(1, javaClient.params.get(0).getLines().size()); + assertEquals(2, javaClient.params.get(0).getLines().get(0).getLine()); + List tokens = decode(javaClient.params.get(0).getLines().get(0).getTokens()); + + SemanticHighlightingTokens.Token tokenSA = tokens.get(0); + assertEquals(6, tokenSA.character); + assertEquals(2, tokenSA.length); + assertThat(SemanticHighlightingService.getScopes(tokenSA.scope), hasItem("entity.name.type.class.java")); + + SemanticHighlightingTokens.Token tokenA = tokens.get(1); + assertEquals(26, tokenA.character); + assertEquals(1, tokenA.length); + assertThat(SemanticHighlightingService.getScopes(tokenA.scope), hasItem("entity.name.type.class.java")); + } + + @Test + public void testDidChange_editLineMiddle_Remove() throws Exception { + //@formatter:off + String content = "package _package;\n" + + "\n" + + "class SA { } public class A { }\n" + + "class SB { }\n" + + "class SC { }"; + //@formatter:on + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(3, params.getLines().size()); + + javaClient.params.clear(); + Range editRange = JDTUtils.toRange(unit, content.indexOf("class SA { } "), "class SA { } ".length()); + changeDocument(unit, "", version++, editRange, "class SA { } ".length()); + assertEquals(1, javaClient.params.size()); + assertEquals(1, javaClient.params.get(0).getLines().size()); + assertEquals(2, javaClient.params.get(0).getLines().get(0).getLine()); + List tokens = decode(javaClient.params.get(0).getLines().get(0).getTokens()); + + SemanticHighlightingTokens.Token tokenA = tokens.get(0); + assertEquals(13, tokenA.character); + assertEquals(1, tokenA.length); + assertThat(SemanticHighlightingService.getScopes(tokenA.scope), hasItem("entity.name.type.class.java")); + } + + @Test + public void testDidChange_insertWhitespace() throws Exception { + //@formatter:off + String content = "package _package;\n" + + "\n" + + "public class A {\n" + + " public static void main(String[] args) {\n" + + " String s = new String(\"foo\");\n" + + " }\n" + + "}"; + //@formatter:on + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(3, params.getLines().size()); + + javaClient.params.clear(); + String insertContent = " "; + Range insertRange = JDTUtils.toRange(unit, content.indexOf(" String s = new ") + " String s = new ".length(), 0); + changeDocument(unit, insertContent, version++, insertRange, 0); + assertEquals(1, javaClient.params.size()); + + List lines = javaClient.params.get(0).getLines(); + assertEquals(1, lines.size()); + assertEquals(4, lines.get(0).getLine()); + + List tokens = decode(lines.get(0).getTokens()); + assertEquals(3, tokens.size()); + assertEquals(4, tokens.get(0).character); // 4 is the indentation offset. + assertEquals(6, tokens.get(0).length); + assertEquals(11, tokens.get(1).character); + assertEquals(1, tokens.get(1).length); + assertEquals(20, tokens.get(2).character); + assertEquals(6, tokens.get(2).length); + } + + @Test + public void testDidChange_singleLineComment() throws Exception { + //@formatter:off + String content = "package _package;\n" + + "\n" + + "public class A {\n" + + " public static void main(String[] args) {\n" + + " String s = new String(\"foo\");\n" + + " }\n" + + "}"; + //@formatter:on + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(3, params.getLines().size()); + + javaClient.params.clear(); + String insertContent = " // "; + Range insertRange = JDTUtils.toRange(unit, content.indexOf(" String s = new "), 0); + changeDocument(unit, insertContent, version++, insertRange, 0); + assertEquals(1, javaClient.params.size()); + + List lines = javaClient.params.get(0).getLines(); + assertEquals(1, lines.size()); + assertEquals(4, lines.get(0).getLine()); + List tokens = decode(lines.get(0).getTokens()); + assertEquals(0, tokens.size()); + } + + @Test + public void testDidChange_blockComment() throws Exception { + //@formatter:off + String content = "package _package;\n" + + "\n" + + "public class A {\n" + + " public static void main(String[] args) {\n" + + " String s = new String(\"foo\");\n" + + " Integer i = Integer.valueOf(36);\n" + + " }\n" + + "}"; + //@formatter:on + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(4, params.getLines().size()); + + // This will result in a compiler error that we fix with the next change. + javaClient.params.clear(); + changeDocument(unit, version++, new TextDocumentContentChangeEvent(new Range(new Position(3, 42), new Position(3, 42)), 0, "/*")); + + assertEquals(1, javaClient.params.size()); + List lines = javaClient.params.get(0).getLines(); + assertEquals(3, lines.size()); + + assertEquals(3, lines.get(0).getLine()); + assertEquals(0, decode(lines.get(0).getTokens()).size()); + assertEquals(4, lines.get(1).getLine()); + assertEquals(0, decode(lines.get(1).getTokens()).size()); + assertEquals(5, lines.get(2).getLine()); + assertEquals(0, decode(lines.get(2).getTokens()).size()); + + javaClient.params.clear(); + changeDocument(unit, version++, new TextDocumentContentChangeEvent(new Range(new Position(6, 2), new Position(6, 2)), 0, "*/")); + + assertEquals(1, javaClient.params.size()); + + lines = javaClient.params.get(0).getLines(); + assertEquals(1, lines.size()); + assertEquals(3, lines.get(0).getLine()); + + List tokens = decode(lines.get(0).getTokens()); + assertEquals(3, tokens.size()); + + assertEquals(21, tokens.get(0).character); + assertEquals(4, tokens.get(0).length); + + assertEquals(26, tokens.get(1).character); + assertEquals(6, tokens.get(1).length); + + assertEquals(35, tokens.get(2).character); + assertEquals(4, tokens.get(2).length); + } + + @Test + public void testDidChange_insertConsecutiveLinesMiddle_singleChange() throws Exception { + //@formatter:off + String content = "package _package;\n" + + "\n" + + "class B {\n" + + "}\n" + + "public class A {\n" + + " public B b;\n" + + " public A a;\n" + + "}"; + //@formatter:on + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(4, params.getLines().size()); + + javaClient.params.clear(); + String insertContent = "\n"; + Range insertRange = JDTUtils.toRange(unit, content.indexOf(" public A a;\n"), 0); + changeDocument(unit, insertContent, version++, insertRange, 0); + assertEquals(0, javaClient.params.size()); + + // Now, insert a new line again. We're expecting zero deltas. + javaClient.params.clear(); + insertRange = JDTUtils.toRange(unit, content.indexOf(" public A a;\n"), 0); + changeDocument(unit, insertContent, version++, insertRange, 0); + assertEquals(0, javaClient.params.size()); + } + + @Test + public void testDidChange_insertConsecutiveLinesMiddle_multipleChanges() throws Exception { + //@formatter:off + String content = "package _package;\n" + + "\n" + + "class B {\n" + + "}\n" + + "public class A {\n" + + " public B b;\n" + + " public A a;\n" + + "}"; + //@formatter:on + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(4, params.getLines().size()); + + // monaco sends the changes in the following way when entering the first NL after `public B b;` + /* + [TextDocumentContentChangeEvent [ + range = Range [ + start = Position [ + line = 5 + character = 13 + ] + end = Position [ + line = 5 + character = 13 + ] + ] + rangeLength = 0 + text = "\n " + ]] + */ + javaClient.params.clear(); + changeDocument(unit, version++, new TextDocumentContentChangeEvent(new Range(new Position(5, 13), new Position(5, 13)), 0, "\n ")); + assertEquals(0, javaClient.params.size()); + + // monaco sends the changes in the following way when entering the second NL at the current position of the cursor. + /* + [TextDocumentContentChangeEvent [ + range = Range [ + start = Position [ + line = 6 + character = 2 + ] + end = Position [ + line = 6 + character = 2 + ] + ] + rangeLength = 0 + text = "\n " + ], TextDocumentContentChangeEvent [ + range = Range [ + start = Position [ + line = 6 + character = 0 + ] + end = Position [ + line = 6 + character = 2 + ] + ] + rangeLength = 2 + text = "" + ]] + */ + javaClient.params.clear(); + //@formatter:off + changeDocument(unit, version++, + new TextDocumentContentChangeEvent(new Range(new Position(6, 2), new Position(6, 2)), 0, "\n "), + new TextDocumentContentChangeEvent(new Range(new Position(6, 0), new Position(6, 2)), 2, "") + ); + //@formatter:on + assertEquals(0, javaClient.params.size()); + } + + @Test + public void testDidChange_insertClassMiddle() throws Exception { + //@formatter:off + String content = "package _package;\n" + + "\n" + + "class B {\n" + + "}\n" + + "public class A {\n" + + " public B b;\n" + + "}"; + //@formatter:on + + int version = 1; + IJavaProject project = newEmptyProject(); + IPackageFragmentRoot src = project.getPackageFragmentRoot(project.getProject().getFolder("src")); + IPackageFragment _package = src.createPackageFragment("_package", false, null); + ICompilationUnit unit = _package.createCompilationUnit("A.java", content, false, null); + openDocument(unit, unit.getSource(), version); + assertEquals(1, javaClient.params.size()); + SemanticHighlightingParams params = javaClient.params.get(0); + assertEquals(3, params.getLines().size()); + + javaClient.params.clear(); + String insertContent = "class C { private A a; }\n"; + Range insertRange = JDTUtils.toRange(unit, content.indexOf("class B {"), 0); + changeDocument(unit, insertContent, version++, insertRange, 0); + assertEquals(1, javaClient.params.size()); + assertEquals(1, javaClient.params.get(0).getLines().size()); + assertEquals(2, javaClient.params.get(0).getLines().get(0).getLine()); + List tokens = decode(javaClient.params.get(0).getLines().get(0).getTokens()); + assertEquals(3, tokens.size()); + SemanticHighlightingTokens.Token tokenC = tokens.get(0); + assertEquals(6, tokenC.character); + assertEquals(1, tokenC.length); + + SemanticHighlightingTokens.Token tokenCFieldAType = tokens.get(1); + assertEquals(18, tokenCFieldAType.character); + assertEquals(1, tokenCFieldAType.length); + + SemanticHighlightingTokens.Token tokenCFieldA = tokens.get(2); + assertEquals(20, tokenCFieldA.character); + assertEquals(1, tokenCFieldA.length); + } + + protected void openDocument(ICompilationUnit unit, String content, int version) { + DidOpenTextDocumentParams openParms = new DidOpenTextDocumentParams(); + TextDocumentItem textDocument = new TextDocumentItem(); + textDocument.setLanguageId("java"); + textDocument.setText(content); + textDocument.setUri(JDTUtils.toURI(unit)); + textDocument.setVersion(version); + openParms.setTextDocument(textDocument); + lifeCycleHandler.didOpen(openParms); + } + + protected void changeDocument(ICompilationUnit unit, int version, TextDocumentContentChangeEvent event, TextDocumentContentChangeEvent... rest) { + DidChangeTextDocumentParams changeParms = new DidChangeTextDocumentParams(); + VersionedTextDocumentIdentifier textDocument = new VersionedTextDocumentIdentifier(); + textDocument.setUri(JDTUtils.toURI(unit)); + textDocument.setVersion(version); + changeParms.setTextDocument(textDocument); + changeParms.setContentChanges(Lists.asList(event, rest)); + lifeCycleHandler.didChange(changeParms); + } + + protected void changeDocument(ICompilationUnit unit, String content, int version, Range range, int length) { + DidChangeTextDocumentParams changeParms = new DidChangeTextDocumentParams(); + VersionedTextDocumentIdentifier textDocument = new VersionedTextDocumentIdentifier(); + textDocument.setUri(JDTUtils.toURI(unit)); + textDocument.setVersion(version); + changeParms.setTextDocument(textDocument); + TextDocumentContentChangeEvent event = new TextDocumentContentChangeEvent(); + if (range != null) { + event.setRange(range); + event.setRangeLength(length); + } + event.setText(content); + List contentChanges = new ArrayList<>(); + contentChanges.add(event); + changeParms.setContentChanges(contentChanges); + lifeCycleHandler.didChange(changeParms); + } + + private static final class TestJavaClientConnection extends JavaClientConnection { + + public List params; + + public TestJavaClientConnection(JavaLanguageClient client) { + super(client); + this.params = newArrayList(); + } + + @Override + public void semanticHighlighting(SemanticHighlightingParams params) { + this.params.add(params); + super.semanticHighlighting(params); + } + + } + +} diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/AbstractProjectsManagerBasedTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/AbstractProjectsManagerBasedTest.java index 3579ae060b..8be4c656a5 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/AbstractProjectsManagerBasedTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/AbstractProjectsManagerBasedTest.java @@ -138,15 +138,16 @@ public IBuffer createBuffer(ICompilationUnit workingCopy) { }); } - protected void initPreferenceManager(boolean supportClassFileContents) { + protected ClientPreferences initPreferenceManager(boolean supportClassFileContents) { PreferenceManager.initialize(); when(preferenceManager.getPreferences()).thenReturn(preferences); when(preferenceManager.getPreferences(any())).thenReturn(preferences); when(preferenceManager.isClientSupportsClassFileContent()).thenReturn(supportClassFileContents); ClientPreferences clientPreferences = mock(ClientPreferences.class); when(clientPreferences.isProgressReportSupported()).thenReturn(true); - when(clientPreferences.isClassFileContentSupported()).thenReturn(supportClassFileContents); + when(clientPreferences.isSemanticHighlightingSupported()).thenReturn(true); when(preferenceManager.getClientPreferences()).thenReturn(clientPreferences); + return clientPreferences; } protected IJavaProject newEmptyProject() throws Exception {