diff --git a/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/editor/folding/AbstractFoldingRangeProviderTest.java b/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/editor/folding/AbstractFoldingRangeProviderTest.java new file mode 100644 index 000000000..74da2dc72 --- /dev/null +++ b/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/editor/folding/AbstractFoldingRangeProviderTest.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2021 TypeFox GmbH (http://www.typefox.io) and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.xtext.ide.tests.editor.folding; + +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.ide.editor.folding.FoldingRange; +import org.eclipse.xtext.ide.editor.folding.IFoldingRangeProvider; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.testing.util.ParseHelper; +import org.eclipse.xtext.util.CancelIndicator; +import org.junit.Assert; + +import com.google.inject.Inject; + +/** + * @author Mark Sujew - Initial contribution and API + */ +public abstract class AbstractFoldingRangeProviderTest { + + @Inject + private IFoldingRangeProvider foldingRangeProvider; + @Inject + private ParseHelper parseHelper; + + protected void assertFoldingRanges(CharSequence content, FoldingRange... expectedRanges) { + XtextResource resource; + try { + resource = (XtextResource) parseHelper.parse(content).eResource(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + List foldingRanges = foldingRangeProvider.getFoldingRanges(resource, CancelIndicator.NullImpl) + .stream().collect(Collectors.toList()); + + Assert.assertEquals(expectedRanges.length, foldingRanges.size()); + + for (int i = 0; i < foldingRanges.size(); i++) { + FoldingRange range = foldingRanges.get(i); + FoldingRange expected = expectedRanges[i]; + Assert.assertEquals(expected.getOffset(), range.getOffset()); + Assert.assertEquals(expected.getLength(), range.getLength()); + Assert.assertEquals(expected.getKind(), range.getKind()); + } + } +} diff --git a/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/editor/folding/FoldingRangeProviderTest.java b/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/editor/folding/FoldingRangeProviderTest.java new file mode 100644 index 000000000..7863082db --- /dev/null +++ b/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/editor/folding/FoldingRangeProviderTest.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2021 TypeFox GmbH (http://www.typefox.io) and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.xtext.ide.tests.editor.folding; + +import org.eclipse.xtext.ide.editor.folding.FoldingRange; +import org.eclipse.xtext.ide.editor.folding.FoldingRangeKind; +import org.eclipse.xtext.ide.tests.testlanguage.TestLanguageIdeInjectorProvider; +import org.eclipse.xtext.ide.tests.testlanguage.testLanguage.Model; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.XtextRunner; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * @author Mark Sujew - Initial contribution and API + */ +@RunWith(XtextRunner.class) +@InjectWith(TestLanguageIdeInjectorProvider.class) +public class FoldingRangeProviderTest extends AbstractFoldingRangeProviderTest { + + @Test + public void foldingRangesProvider() { + String actual = "package testLanguage {" + + "\n" + + "\n"; + int commentIndex = actual.length(); + + actual += "/*\n" + + " * This s a multiline comment" + + " */"; + int commentLength = actual.length() - commentIndex; + + actual += "\n"; + int typeIndex = actual.length(); + + actual += "type myType {" + + "\n" + + "}"; + int typeLength = actual.length() - typeIndex; + actual += "\n}"; + + assertFoldingRanges(actual, new FoldingRange(0, actual.length()), new FoldingRange(commentIndex, commentLength, FoldingRangeKind.COMMENT), new FoldingRange(typeIndex, typeLength)); + } + +} diff --git a/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/FoldingTest.java b/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/FoldingTest.java new file mode 100644 index 000000000..65a1aa810 --- /dev/null +++ b/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/FoldingTest.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2021 TypeFox GmbH (http://www.typefox.io) and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.xtext.ide.tests.server; + +import org.junit.Test; + +/** + * @author Mark Sujew - Initial contribution and API + */ +public class FoldingTest extends AbstractTestLangLanguageServerTest { + + @Test + public void testFoldingService() { + testFolding(it -> { + it.setModel("/*\nMultiline Comment\n*/\ntype Foo {\nint bar\n}"); + String expectedText = "[comment 0..2]\n[null 3..5]\n"; + it.setExpectedFoldings(expectedText); + }); + } + +} diff --git a/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/DefaultFoldingRangeAcceptor.java b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/DefaultFoldingRangeAcceptor.java new file mode 100644 index 000000000..c27f7b010 --- /dev/null +++ b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/DefaultFoldingRangeAcceptor.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2010, 2021 Michael Clay and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + *******************************************************************************/ +package org.eclipse.xtext.ide.editor.folding; + +import java.util.Collection; + +import org.eclipse.xtext.util.ITextRegion; + +/** + * @author Michael Clay - Initial contribution and API + * @author Sebastian Zarnekow - Introduced FoldedPosition + * @author Mark Sujew - Ported to IDE project + * + * @since 2.26 + */ +public class DefaultFoldingRangeAcceptor implements IFoldingRangeAcceptor { + private Collection result; + + public DefaultFoldingRangeAcceptor(Collection result) { + this.result = result; + } + + public Collection getFoldingRanges() { + return result; + } + + @Override + public void accept(int offset, int length, FoldingRangeKind kind, boolean initiallyFolded, + ITextRegion visualPlaceholderRegion) { + result.add(createFoldingRange(offset, length, kind, initiallyFolded, visualPlaceholderRegion)); + } + + protected FoldingRange createFoldingRange(int offset, int length, FoldingRangeKind kind, boolean initiallyFolded, + ITextRegion visualPlaceholderRegion) { + return new FoldingRange(offset, length, kind, initiallyFolded, visualPlaceholderRegion); + } +} diff --git a/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/DefaultFoldingRangeProvider.java b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/DefaultFoldingRangeProvider.java new file mode 100644 index 000000000..8eca1ba18 --- /dev/null +++ b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/DefaultFoldingRangeProvider.java @@ -0,0 +1,202 @@ +/******************************************************************************* + * Copyright (c) 2010, 2021 Michael Clay and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + *******************************************************************************/ +package org.eclipse.xtext.ide.editor.folding; + +import java.util.Collection; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.emf.common.util.TreeIterator; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.TerminalRule; +import org.eclipse.xtext.documentation.impl.AbstractMultiLineCommentProvider; +import org.eclipse.xtext.nodemodel.ICompositeNode; +import org.eclipse.xtext.nodemodel.INode; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.eclipse.xtext.parser.IParseResult; +import org.eclipse.xtext.resource.ILocationInFileProvider; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.util.CancelIndicator; +import org.eclipse.xtext.util.ITextRegion; +import org.eclipse.xtext.util.ITextRegionWithLineInformation; +import org.eclipse.xtext.util.LineAndColumn; +import org.eclipse.xtext.util.TextRegion; + +import com.google.inject.Inject; +import com.google.inject.name.Named; + +/** + * @author Michael Clay - Initial contribution and API + * @author Sebastian Zarnekow - Introduced FoldedRegion, use ILocationInFileProvider + * @author Mark Sujew - Ported to IDE project + * + * @since 2.26 + */ +public class DefaultFoldingRangeProvider implements IFoldingRangeProvider { + + @Inject + private ILocationInFileProvider locationInFileProvider; + + @Inject(optional = true) + @Named(AbstractMultiLineCommentProvider.RULE) + private String ruleName = "ML_COMMENT"; + + protected static final Pattern TEXT_PATTERN_IN_COMMENT = Pattern.compile("\\w"); + + @Override + public SortedSet getFoldingRanges(XtextResource xtextDocument, CancelIndicator cancelIndicator) { + SortedSet result = new TreeSet<>((a, b) -> Integer.compare(a.getOffset(), b.getOffset())); + IFoldingRangeAcceptor foldingRegionAcceptor = createAcceptor(xtextDocument, result); + computeObjectFolding(xtextDocument, foldingRegionAcceptor, cancelIndicator); + computeCommentFolding(xtextDocument, foldingRegionAcceptor); + return result; + } + + protected void computeObjectFolding(XtextResource resource, IFoldingRangeAcceptor foldingRangeAcceptor, + CancelIndicator cancelIndicator) { + IParseResult parseResult = resource.getParseResult(); + if (parseResult != null) { + EObject rootASTElement = parseResult.getRootASTElement(); + if (rootASTElement != null) { + if (cancelIndicator.isCanceled()) + throw new OperationCanceledException(); + if (isHandled(rootASTElement)) { + acceptObjectFolding(rootASTElement, foldingRangeAcceptor); + } + if (shouldProcessContent(rootASTElement)) { + TreeIterator allContents = rootASTElement.eAllContents(); + while (allContents.hasNext()) { + if (cancelIndicator.isCanceled()) + throw new OperationCanceledException(); + EObject eObject = allContents.next(); + if (isHandled(eObject)) { + acceptObjectFolding(eObject, foldingRangeAcceptor); + } + if (!shouldProcessContent(eObject)) { + allContents.prune(); + } + } + } + } + } + } + + protected void acceptObjectFolding(EObject eObject, IFoldingRangeAcceptor foldingRangeAcceptor) { + ITextRegion region = locationInFileProvider.getFullTextRegion(eObject); + if (region != null) { + INode eObjectNode = NodeModelUtils.getNode(eObject); + ITextRegion significant = buildSignificantRegion(locationInFileProvider.getSignificantTextRegion(eObject), + eObjectNode); + foldingRangeAcceptor.accept(region.getOffset(), region.getLength(), null, false, significant); + } + } + + protected ITextRegion buildSignificantRegion(ITextRegion significantRegion, INode node) { + if (significantRegion == null || node == null) { + return null; + } + int offset = significantRegion.getOffset(); + int endOffset = significantRegion.getOffset() + significantRegion.getLength(); + + int startLine; + int endLine; + if (significantRegion instanceof ITextRegionWithLineInformation) { + ITextRegionWithLineInformation lineInfoRegion = (ITextRegionWithLineInformation) significantRegion; + startLine = lineInfoRegion.getLineNumber(); + endLine = lineInfoRegion.getEndLineNumber(); + } else { + startLine = NodeModelUtils.getLineAndColumn(node, offset).getLine(); + endLine = NodeModelUtils.getLineAndColumn(node, endOffset).getLine(); + } + + if (startLine != endLine) { + /* + * The Eclipse IDE can only use the significant region if it starts and ends on the same line. + * Therefore, we here calculate the index of the end of the line. + */ + for (int index = offset; index < endOffset; index++) { + LineAndColumn lineInfo = NodeModelUtils.getLineAndColumn(node, index); + if (lineInfo.getLine() != startLine) { + return new TextRegion(offset, index - offset - 1); + } + } + } + return significantRegion; + } + + protected void computeCommentFolding(XtextResource resource, IFoldingRangeAcceptor foldingRangeAcceptor) { + IParseResult parseResult = resource.getParseResult(); + if (parseResult != null) { + EObject rootASTElement = parseResult.getRootASTElement(); + acceptCommentNodes(rootASTElement, foldingRangeAcceptor); + } + } + + protected void acceptCommentNodes(EObject eObject, IFoldingRangeAcceptor foldingRangeAcceptor) { + ICompositeNode node = NodeModelUtils.getNode(eObject); + if (node != null) { + for (INode leafNode : node.getAsTreeIterable()) { + if (leafNode.getGrammarElement() instanceof TerminalRule + && ruleName.equalsIgnoreCase(((TerminalRule) leafNode.getGrammarElement()).getName())) { + acceptCommentFolding(leafNode, foldingRangeAcceptor); + } + } + } + } + + protected void acceptCommentFolding(INode commentNode, IFoldingRangeAcceptor foldingRangeAcceptor) { + int offset = commentNode.getOffset(); + int length = commentNode.getLength(); + Matcher matcher = getTextPatternInComment().matcher(commentNode.getText()); + if (matcher.find()) { + TextRegion significant = new TextRegion(offset + matcher.start(), 0); + foldingRangeAcceptor.accept(offset, length, FoldingRangeKind.COMMENT, significant); + } else { + foldingRangeAcceptor.accept(offset, length, FoldingRangeKind.COMMENT); + } + } + + protected IFoldingRangeAcceptor createAcceptor(XtextResource resource, Collection foldingRanges) { + return new DefaultFoldingRangeAcceptor(foldingRanges); + } + + protected ILocationInFileProvider getLocationInFileProvider() { + return locationInFileProvider; + } + + protected String getMultilineCommentRuleName() { + return ruleName; + } + + /** + * @return the regular expression that finds the first significant part of a multi line comment. + */ + protected Pattern getTextPatternInComment() { + return TEXT_PATTERN_IN_COMMENT; + } + + /** + * @return true if the object should be folded if it spans more than one line. Default is + * false if and only if the object is the root object of the resource. + */ + protected boolean isHandled(EObject eObject) { + return eObject.eContainer() != null; + } + + /** + * @return clients should return false to abort the traversal of the model. + */ + protected boolean shouldProcessContent(EObject object) { + return true; + } +} diff --git a/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/FoldingRange.java b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/FoldingRange.java new file mode 100644 index 000000000..76bdc5413 --- /dev/null +++ b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/FoldingRange.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2021 TypeFox GmbH (http://www.typefox.io) and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.xtext.ide.editor.folding; + +import org.eclipse.xtext.util.ITextRegion; + +import com.google.common.base.Objects; + +/** + * Represents an abstraction for different folding implementations. + * Specifically Eclipse's FoldingRegion and the FoldingRange of the LSP specification. + * + * @author Mark Sujew - Initial contribution and API + * + * @since 2.26 + */ +public class FoldingRange { + + private final int offset; + private final int length; + private final FoldingRangeKind kind; + private final boolean initiallyFolded; + private final ITextRegion visualPlaceholderRegion; + + public FoldingRange(int offset, int length) { + this(offset, length, null); + } + + public FoldingRange(int offset, int length, FoldingRangeKind kind) { + this(offset, length, kind, false, null); + } + + public FoldingRange(int offset, int length, FoldingRangeKind kind, boolean initiallyFolded, + ITextRegion visualPlaceholderRegion) { + this.offset = offset; + this.length = length; + this.kind = kind; + this.initiallyFolded = initiallyFolded; + this.visualPlaceholderRegion = visualPlaceholderRegion; + } + + public boolean isInitiallyFolded() { + return initiallyFolded; + } + + public ITextRegion getVisualPlaceholderRegion() { + return visualPlaceholderRegion; + } + + public FoldingRangeKind getKind() { + return kind; + } + + public int getOffset() { + return offset; + } + + public int getLength() { + return length; + } + + @Override + public boolean equals(Object other) { + if (other instanceof FoldingRange) { + FoldingRange range = (FoldingRange) other; + return offset == range.offset && length == range.length && initiallyFolded == range.initiallyFolded + && Objects.equal(kind, range.kind) + && Objects.equal(visualPlaceholderRegion, range.visualPlaceholderRegion); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(offset, length, initiallyFolded, kind, visualPlaceholderRegion); + } + + @Override + public String toString() { + StringBuilder content = new StringBuilder(); + content.append("offset=").append(offset); + content.append(", length=").append(length); + content.append(", kind=").append(kind); + content.append(", initiallyFolded=").append(initiallyFolded); + return content.toString(); + } +} diff --git a/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/FoldingRangeKind.java b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/FoldingRangeKind.java new file mode 100644 index 000000000..228d7a85f --- /dev/null +++ b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/FoldingRangeKind.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2021 TypeFox GmbH (http://www.typefox.io) and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.xtext.ide.editor.folding; + +/** + * Contains the possible kinds of folding ranges defined by the LSP spec. + * + * @author Mark Sujew - Initial contribution and API + * + * @since 2.26 + */ +public class FoldingRangeKind { + public static final FoldingRangeKind COMMENT = new FoldingRangeKind("comment"); + public static final FoldingRangeKind IMPORTS = new FoldingRangeKind("imports"); + public static final FoldingRangeKind REGION = new FoldingRangeKind("region"); + + private String kind; + + private FoldingRangeKind(String kind) { + this.kind = kind; + } + + @Override + public String toString() { + return kind; + } + + @Override + public boolean equals(Object other) { + if (other instanceof FoldingRangeKind) { + return this.kind.equals(((FoldingRangeKind) other).kind); + } + return false; + } + + @Override + public int hashCode() { + return kind.hashCode(); + } +} diff --git a/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/IFoldingRangeAcceptor.java b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/IFoldingRangeAcceptor.java new file mode 100644 index 000000000..ec0feec78 --- /dev/null +++ b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/IFoldingRangeAcceptor.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2010, 2021 Michael Clay and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + *******************************************************************************/ +package org.eclipse.xtext.ide.editor.folding; + +import org.eclipse.xtext.util.ITextRegion; + +/** + * @author Michael Clay - Initial contribution and API + * @author Sebastian Zarnekow - Distinguish between total and identifying region + * @author Mark Sujew - Ported to IDE project + * + * @since 2.26 + */ +public interface IFoldingRangeAcceptor { + + default void accept(int offset, int length) { + accept(offset, length, false); + } + + default void accept(int offset, int length, FoldingRangeKind kind) { + accept(offset, length, kind, null); + } + + default void accept(int offset, int length, boolean initiallyFolded) { + accept(offset, length, null, initiallyFolded, null); + } + + default void accept(int offset, int length, ITextRegion visualPlaceholderRegion) { + accept(offset, length, null, false, visualPlaceholderRegion); + } + + default void accept(int offset, int length, FoldingRangeKind kind, ITextRegion visualPlaceholderRegion) { + accept(offset, length, kind, false, visualPlaceholderRegion); + } + + void accept(int offset, int length, FoldingRangeKind kind, boolean initiallyFolded, + ITextRegion visualPlaceholderRegion); +} diff --git a/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/IFoldingRangeProvider.java b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/IFoldingRangeProvider.java new file mode 100644 index 000000000..1a2e6760a --- /dev/null +++ b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/editor/folding/IFoldingRangeProvider.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2010, 2021 Michael Clay and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + *******************************************************************************/ +package org.eclipse.xtext.ide.editor.folding; + +import java.util.SortedSet; + +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.util.CancelIndicator; + +import com.google.inject.ImplementedBy; + +/** + * Compute the folding regions in the given document. This is a blocking action when opening the editor in e4, so + * clients should be careful to not resolve too many cross references in their implementation. + * + * @author Michael Clay - Initial contribution and API + * @author Sebastian Zarnekow - Refactoring, introduced FoldedPosition + * @author Mark Sujew - Ported to IDE project + * + * @since 2.26 + */ +@ImplementedBy(DefaultFoldingRangeProvider.class) +public interface IFoldingRangeProvider { + + /** + * @return the set of FoldingRanges for the given document + */ + SortedSet getFoldingRanges(XtextResource xtextDocument, CancelIndicator cancelIndicator); +} diff --git a/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/LanguageServerImpl.java b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/LanguageServerImpl.java index 1fe70d8db..3d008ce08 100644 --- a/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/LanguageServerImpl.java +++ b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/LanguageServerImpl.java @@ -56,6 +56,9 @@ import org.eclipse.lsp4j.ExecuteCommandCapabilities; import org.eclipse.lsp4j.ExecuteCommandOptions; import org.eclipse.lsp4j.ExecuteCommandParams; import org.eclipse.lsp4j.FileChangeType; +import org.eclipse.lsp4j.FoldingRange; +import org.eclipse.lsp4j.FoldingRangeCapabilities; +import org.eclipse.lsp4j.FoldingRangeRequestParams; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.HoverParams; import org.eclipse.lsp4j.InitializeParams; @@ -111,6 +114,7 @@ import org.eclipse.xtext.ide.server.commands.ExecutableCommandRegistry; import org.eclipse.xtext.ide.server.concurrent.RequestManager; import org.eclipse.xtext.ide.server.contentassist.ContentAssistService; import org.eclipse.xtext.ide.server.findReferences.WorkspaceResourceAccess; +import org.eclipse.xtext.ide.server.folding.FoldingRangeService; import org.eclipse.xtext.ide.server.formatting.FormattingService; import org.eclipse.xtext.ide.server.hover.IHoverService; import org.eclipse.xtext.ide.server.occurrences.IDocumentHighlightService; @@ -135,6 +139,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.inject.Inject; @@ -268,8 +273,14 @@ public class LanguageServerImpl implements LanguageServer, WorkspaceService, Tex textDocument = clientCapabilities.getTextDocument(); } RenameCapabilities rename = null; + FoldingRangeCapabilities folding = null; if (textDocument != null) { rename = textDocument.getRename(); + folding = textDocument.getFoldingRange(); + } + if (folding != null) { + serverCapabilities.setFoldingRangeProvider(allLanguages.stream() + .anyMatch(serviceProvider -> serviceProvider.get(FoldingRangeService.class) != null)); } Boolean prepareSupport = null; if (rename != null) { @@ -1007,6 +1018,26 @@ public class LanguageServerImpl implements LanguageServer, WorkspaceService, Tex options.setCancelIndicator(cancelIndicator); return renameService.prepareRename(options); } + + /** + * @since 2.26 + */ + public CompletableFuture> foldingRange(FoldingRangeRequestParams params) { + return requestManager.runRead(cancelIndicator -> foldingRange(params, cancelIndicator)); + } + + /** + * @since 2.26 + */ + protected List foldingRange(FoldingRangeRequestParams params, CancelIndicator cancelIndicator) { + URI uri = getURI(params.getTextDocument()); + FoldingRangeService foldingRangeService = getService(uri, FoldingRangeService.class); + if (foldingRangeService == null) { + return Lists.newArrayList(); + } + return workspaceManager.doRead(uri, (document, resource) -> foldingRangeService + .createFoldingRanges(document, resource, cancelIndicator)); + } @Override public void notify(String method, Object parameter) { diff --git a/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/folding/FoldingRangeService.java b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/folding/FoldingRangeService.java new file mode 100644 index 000000000..e19de14f5 --- /dev/null +++ b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/folding/FoldingRangeService.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2021 TypeFox GmbH (http://www.typefox.io) and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.xtext.ide.server.folding; + +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.lsp4j.FoldingRange; +import org.eclipse.lsp4j.Position; +import org.eclipse.xtext.ide.editor.folding.IFoldingRangeProvider; +import org.eclipse.xtext.ide.server.Document; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.util.CancelIndicator; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +/** + * @author Mark Sujew - Initial contribution and API + * + * @since 2.26 + */ +@Singleton +public class FoldingRangeService { + + @Inject + private IFoldingRangeProvider foldingRangeProvider; + + public List createFoldingRanges(Document document, XtextResource resource, + CancelIndicator cancelIndicator) { + return foldingRangeProvider.getFoldingRanges(resource, cancelIndicator).stream() + .map(range -> toFoldingRange(document, range)).filter(range -> isValidRange(range)) + .collect(Collectors.toList()); + } + + protected FoldingRange toFoldingRange(Document document, org.eclipse.xtext.ide.editor.folding.FoldingRange range) { + int offset = range.getOffset(); + int length = range.getLength(); + int endOffset = offset + length; + Position start = document.getPosition(offset); + Position end = document.getPosition(endOffset); + FoldingRange result = new FoldingRange(start.getLine(), end.getLine()); + if (range.getKind() != null) { + result.setKind(range.getKind().toString()); + } + return result; + } + + protected boolean isValidRange(FoldingRange range) { + return range.getStartLine() < range.getEndLine(); + } +} diff --git a/org.eclipse.xtext.testing/src/org/eclipse/xtext/testing/AbstractLanguageServerTest.xtend b/org.eclipse.xtext.testing/src/org/eclipse/xtext/testing/AbstractLanguageServerTest.xtend index fd83ac105..c3a4dd029 100644 --- a/org.eclipse.xtext.testing/src/org/eclipse/xtext/testing/AbstractLanguageServerTest.xtend +++ b/org.eclipse.xtext.testing/src/org/eclipse/xtext/testing/AbstractLanguageServerTest.xtend @@ -42,6 +42,8 @@ import org.eclipse.lsp4j.DocumentSymbol import org.eclipse.lsp4j.DocumentSymbolParams import org.eclipse.lsp4j.FileChangeType import org.eclipse.lsp4j.FileEvent +import org.eclipse.lsp4j.FoldingRange +import org.eclipse.lsp4j.FoldingRangeRequestParams import org.eclipse.lsp4j.Hover import org.eclipse.lsp4j.HoverParams import org.eclipse.lsp4j.InitializeParams @@ -392,6 +394,10 @@ abstract class AbstractLanguageServerTest implements Endpoint { protected dispatch def String toExpectation(CodeLens it) { return command.title + " " +range.toExpectation } + + protected dispatch def String toExpectation(FoldingRange it) { + return '''[«String.valueOf(kind)» «startLine»..«endLine»]''' + } @Accessors static class TestCodeLensConfiguration extends TextDocumentPositionConfiguration { String expectedCodeLensItems = '' @@ -711,6 +717,23 @@ abstract class AbstractLanguageServerTest implements Endpoint { val result = new Document(1, fileInfo.contents).applyChanges(newArrayList(changes.get()).reverse) assertEqualsStricter(configuration.expectedText, result.contents) } + + /** + * @since 2.26 + */ + protected def testFolding((FoldingConfiguration)=>void configurator) { + val extension configuration = new FoldingConfiguration + configuration.filePath = 'MyModel.' + fileExtension + configurator.apply(configuration) + + val fileInfo = initializeContext(configuration) + + val foldings = languageServer.foldingRange(new FoldingRangeRequestParams => [ + textDocument = new TextDocumentIdentifier(fileInfo.uri) + ]).get() + + assertEqualsStricter(configuration.expectedFoldings, foldings.toExpectation); + } override notify(String method, Object parameter) { this.notifications.add(method -> parameter) diff --git a/org.eclipse.xtext.testing/src/org/eclipse/xtext/testing/FoldingConfiguration.java b/org.eclipse.xtext.testing/src/org/eclipse/xtext/testing/FoldingConfiguration.java new file mode 100644 index 000000000..1bf15e5fa --- /dev/null +++ b/org.eclipse.xtext.testing/src/org/eclipse/xtext/testing/FoldingConfiguration.java @@ -0,0 +1,15 @@ +package org.eclipse.xtext.testing; + +public class FoldingConfiguration extends TextDocumentConfiguration { + + private String expectedFoldings; + + public String getExpectedFoldings() { + return expectedFoldings; + } + + public void setExpectedFoldings(String expectedFoldings) { + this.expectedFoldings = expectedFoldings; + } + +} diff --git a/org.eclipse.xtext.testing/xtend-gen/org/eclipse/xtext/testing/AbstractLanguageServerTest.java b/org.eclipse.xtext.testing/xtend-gen/org/eclipse/xtext/testing/AbstractLanguageServerTest.java index c4ced84d4..9e8b11eb7 100644 --- a/org.eclipse.xtext.testing/xtend-gen/org/eclipse/xtext/testing/AbstractLanguageServerTest.java +++ b/org.eclipse.xtext.testing/xtend-gen/org/eclipse/xtext/testing/AbstractLanguageServerTest.java @@ -53,6 +53,8 @@ import org.eclipse.lsp4j.DocumentSymbolCapabilities; import org.eclipse.lsp4j.DocumentSymbolParams; import org.eclipse.lsp4j.FileChangeType; import org.eclipse.lsp4j.FileEvent; +import org.eclipse.lsp4j.FoldingRange; +import org.eclipse.lsp4j.FoldingRangeRequestParams; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.HoverParams; import org.eclipse.lsp4j.InitializeParams; @@ -803,6 +805,21 @@ public abstract class AbstractLanguageServerTest implements Endpoint { return (_plus + _expectation); } + protected String _toExpectation(final FoldingRange it) { + StringConcatenation _builder = new StringConcatenation(); + _builder.append("["); + String _valueOf = String.valueOf(it.getKind()); + _builder.append(_valueOf); + _builder.append(" "); + int _startLine = it.getStartLine(); + _builder.append(_startLine); + _builder.append(".."); + int _endLine = it.getEndLine(); + _builder.append(_endLine); + _builder.append("]"); + return _builder.toString(); + } + protected void testCodeLens(final Procedure1 configurator) { try { @Extension @@ -1402,6 +1419,30 @@ public abstract class AbstractLanguageServerTest implements Endpoint { } } + /** + * @since 2.26 + */ + protected void testFolding(final Procedure1 configurator) { + try { + @Extension + final FoldingConfiguration configuration = new FoldingConfiguration(); + configuration.setFilePath(("MyModel." + this.fileExtension)); + configurator.apply(configuration); + final FileInfo fileInfo = this.initializeContext(configuration); + FoldingRangeRequestParams _foldingRangeRequestParams = new FoldingRangeRequestParams(); + final Procedure1 _function = (FoldingRangeRequestParams it) -> { + String _uri = fileInfo.getUri(); + TextDocumentIdentifier _textDocumentIdentifier = new TextDocumentIdentifier(_uri); + it.setTextDocument(_textDocumentIdentifier); + }; + FoldingRangeRequestParams _doubleArrow = ObjectExtensions.operator_doubleArrow(_foldingRangeRequestParams, _function); + final List foldings = this.languageServer.foldingRange(_doubleArrow).get(); + this.assertEqualsStricter(configuration.getExpectedFoldings(), this.toExpectation(foldings)); + } catch (Throwable _e) { + throw Exceptions.sneakyThrow(_e); + } + } + @Override public void notify(final String method, final Object parameter) { Pair _mappedTo = Pair.of(method, parameter); @@ -1459,6 +1500,8 @@ public abstract class AbstractLanguageServerTest implements Endpoint { return _toExpectation((DocumentHighlight)it); } else if (it instanceof DocumentSymbol) { return _toExpectation((DocumentSymbol)it); + } else if (it instanceof FoldingRange) { + return _toExpectation((FoldingRange)it); } else if (it instanceof Hover) { return _toExpectation((Hover)it); } else if (it instanceof Location) {