diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LSPIJTextMateUtils.java b/src/main/java/com/redhat/devtools/lsp4ij/LSPIJTextMateUtils.java new file mode 100644 index 000000000..9ad9cf0e2 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/LSPIJTextMateUtils.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2025 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ + +package com.redhat.devtools.lsp4ij; + +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +/** + * Utilities for working with TextMate files in LSP4IJ. + */ +@ApiStatus.Internal +public final class LSPIJTextMateUtils { + + private LSPIJTextMateUtils() { + // Pure utility class + } + + /** + * Returns the simple/single-character brace pairs for the file if it's a TextMate file. + * + * @param file the PSI file + * @return the simple brace pairs for the file if it's a TextMate file; otherwise null + */ + @Nullable + @ApiStatus.Internal + public static Map getBracePairs(@NotNull PsiFile file) { + // TODO: Unfortunately the interface changed in this commit: + // https://github.com/JetBrains/intellij-community/commit/8df3d04be0db4c54732a15250b789aa5d9a6de47#diff-08fc4fd41510ee4662c41d3f2a671ae2f654d1a2f6ff7608765f427c26eaeae7 + // and would now require reflection to work in 2023.2 and later versions. Specifically it used to be + // "bracePair.left/right" which returned "char", but now it's "bracePair.getLeft()/getRight()" which return + // "CharSequence". I think it's worth leaving this in but commented out and returning "null" -- existing usages + // will degrade gracefully -- and then when all supported IDE versions have the same interface, this can be + // restored. Perhaps this even prompts removal of support for the oldest versions that have this issue? + return null; + +/* + if (!(file instanceof TextMateFile)) { + return null; + } + + Map bracePairs = new LinkedHashMap<>(); + Editor editor = LSPIJUtils.editorForElement(file); + TextMateScope selector = editor instanceof EditorEx ? TextMateEditorUtils.getCurrentScopeSelector((EditorEx) editor) : null; + if (selector != null) { + for (TextMateBracePair bracePair : getAllPairsForMatcher(selector)) { + CharSequence openBrace = bracePair.getLeft(); + CharSequence closeBrace = bracePair.getRight(); + if ((openBrace.length() == 1) && (closeBrace.length() == 1)) { + bracePairs.put(openBrace.charAt(0), closeBrace.charAt(0)); + } + } + } + return bracePairs; +*/ + } + +/* + // NOTE: Cloned from TextMateEditorUtils where this is private + @NotNull + private static Set getAllPairsForMatcher(@Nullable TextMateScope selector) { + if (selector == null) { + return Constants.DEFAULT_HIGHLIGHTING_BRACE_PAIRS; + } + Set result = new HashSet<>(); + List preferencesForSelector = TextMateService.getInstance().getPreferenceRegistry().getPreferences(selector); + for (Preferences preferences : preferencesForSelector) { + final Set highlightingPairs = preferences.getHighlightingPairs(); + if (highlightingPairs != null) { + if (highlightingPairs.isEmpty()) { + // smart typing pairs can be defined in preferences but can be empty (in order to disable smart typing completely) + return Collections.emptySet(); + } + result.addAll(highlightingPairs); + } + } + return result; + } +*/ +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/codeBlockProvider/LSPCodeBlockProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/features/codeBlockProvider/LSPCodeBlockProvider.java new file mode 100644 index 000000000..340cb8260 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/codeBlockProvider/LSPCodeBlockProvider.java @@ -0,0 +1,251 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ + +package com.redhat.devtools.lsp4ij.features.codeBlockProvider; + +import com.intellij.codeInsight.editorActions.CodeBlockProvider; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiFile; +import com.intellij.util.containers.ContainerUtil; +import com.redhat.devtools.lsp4ij.features.foldingRange.LSPFoldingRangeBuilder; +import com.redhat.devtools.lsp4ij.features.selectionRange.LSPSelectionRangeSupport; +import org.eclipse.lsp4j.FoldingRange; +import org.eclipse.lsp4j.SelectionRange; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * Code block provider that uses information from {@link LSPSelectionRangeSupport} and {@link LSPFoldingRangeBuilder}. + */ +public class LSPCodeBlockProvider implements CodeBlockProvider { + + @Override + @Nullable + public TextRange getCodeBlockRange(Editor editor, PsiFile file) { + Document document = editor.getDocument(); + CharSequence documentChars = document.getCharsSequence(); + int documentLength = documentChars.length(); + + // Adjust the offset slightly based on before/after brace to ensure evaluation occurs "within" the braced block + int offset = editor.getCaretModel().getOffset(); + Character beforeCharacter = offset > 0 ? documentChars.charAt(offset - 1) : null; + Character afterCharacter = offset < documentLength ? documentChars.charAt(offset) : null; + if (LSPCodeBlockUtils.isCodeBlockStartChar(file, afterCharacter)) { + offset++; + } else if (LSPCodeBlockUtils.isCodeBlockEndChar(file, beforeCharacter)) { + offset--; + } + + // See if we're anchored by a known brace character + int openBraceOffset = -1; + Character openBraceChar = null; + int closeBraceOffset = -1; + Character closeBraceChar = null; + if ((offset > 0) && LSPCodeBlockUtils.isCodeBlockStartChar(file, documentChars.charAt(offset - 1))) { + openBraceOffset = offset - 1; + openBraceChar = documentChars.charAt(offset - 1); + closeBraceChar = LSPCodeBlockUtils.getCodeBlockEndChar(file, openBraceChar); + } else if (LSPCodeBlockUtils.isCodeBlockEndChar(file, documentChars.charAt(offset))) { + closeBraceOffset = offset; + closeBraceChar = documentChars.charAt(offset); + openBraceChar = LSPCodeBlockUtils.getCodeBlockStartChar(file, closeBraceChar); + } else if ((offset < (documentLength - 1)) && LSPCodeBlockUtils.isCodeBlockEndChar(file, documentChars.charAt(offset + 1))) { + closeBraceOffset = offset + 1; + closeBraceChar = documentChars.charAt(offset + 1); + openBraceChar = LSPCodeBlockUtils.getCodeBlockStartChar(file, closeBraceChar); + } + + // Try to find it first using the selection ranges which tend to be more accurate; we must use the effective + // offset for selection ranges to act as if we're in the adjusted braced block + TextRange codeBlockRange = getUsingSelectionRanges( + file, + editor, + offset, + openBraceChar, + openBraceOffset, + closeBraceChar, + closeBraceOffset + ); + if (codeBlockRange != null) { + return codeBlockRange; + } + + // Failing that, try to find it using the folding ranges + return getUsingFoldingRanges( + file, + document, + offset, + openBraceChar, + openBraceOffset, + closeBraceChar, + closeBraceOffset + ); + } + + @Nullable + private TextRange getUsingSelectionRanges(@NotNull PsiFile file, + @NotNull Editor editor, + int offset, + @Nullable Character openBraceChar, + int openBraceOffset, + @Nullable Character closeBraceChar, + int closeBraceOffset) { + Document document = editor.getDocument(); + List selectionRanges = LSPSelectionRangeSupport.getSelectionRanges(file, document, offset); + if (!ContainerUtil.isEmpty(selectionRanges)) { + // Convert the selection ranges into text ranges + Set textRanges = new LinkedHashSet<>(selectionRanges.size()); + for (SelectionRange selectionRange : selectionRanges) { + textRanges.add(LSPSelectionRangeSupport.getTextRange(selectionRange, document)); + for (SelectionRange parentSelectionRange = selectionRange.getParent(); + parentSelectionRange != null; + parentSelectionRange = parentSelectionRange.getParent()) { + textRanges.add(LSPSelectionRangeSupport.getTextRange(parentSelectionRange, document)); + } + } + + CharSequence documentChars = document.getCharsSequence(); + int documentLength = documentChars.length(); + + // Find containing text ranges that are bounded by brace pairs + List containingTextRanges = new ArrayList<>(textRanges.size()); + for (TextRange textRange : textRanges) { + if (textRange.getLength() > 1) { + int startOffset = textRange.getStartOffset(); + int endOffset = textRange.getEndOffset(); + + char startChar = documentChars.charAt(startOffset); + char endChar = documentChars.charAt(endOffset - 1); + + // If aligned on an open brace and this ends with the expected close brace, use it + if ((startOffset == openBraceOffset)) { + if ((closeBraceChar != null) && (closeBraceChar == endChar)) { + return textRange; + } else { + return null; + } + } + // If aligned on a close brace and this starts with the expected open brace, use it + else if (((endOffset - 1) == closeBraceOffset)) { + if ((openBraceChar != null) && (openBraceChar == startChar)) { + return textRange; + } else { + return null; + } + } + // Otherwise see if it starts and ends with a known brace pair and we'll find the "closest" below + else if ((openBraceOffset == -1) && (closeBraceOffset == -1) && LSPCodeBlockUtils.isCodeBlockStartChar(file, startChar)) { + Character pairedCloseBraceChar = LSPCodeBlockUtils.getCodeBlockEndChar(file, startChar); + if ((pairedCloseBraceChar != null) && (pairedCloseBraceChar == endChar)) { + containingTextRanges.add(textRange); + } + } + // Also try to see if these are exactly the contents of a code block + else if ((openBraceOffset == -1) && (closeBraceOffset == -1) && (startOffset > 0) && (endOffset < documentLength)) { + startChar = documentChars.charAt(startOffset - 1); + endChar = documentChars.charAt(endOffset); + + if (LSPCodeBlockUtils.isCodeBlockStartChar(file, startChar)) { + Character pairedCloseBraceChar = LSPCodeBlockUtils.getCodeBlockEndChar(file, startChar); + if ((pairedCloseBraceChar != null) && (pairedCloseBraceChar == endChar)) { + containingTextRanges.add(textRange); + } + } + } + } + } + + // Return the closest (i.e., smallest) containing text range + if (!ContainerUtil.isEmpty(containingTextRanges)) { + containingTextRanges.sort(Comparator.comparingInt(TextRange::getLength)); + return ContainerUtil.getFirstItem(containingTextRanges); + } + } + + return null; + } + + @Nullable + private static TextRange getUsingFoldingRanges(@NotNull PsiFile file, + @NotNull Document document, + int offset, + @Nullable Character openBraceChar, + int openBraceOffset, + @Nullable Character closeBraceChar, + int closeBraceOffset) { + List foldingRanges = LSPFoldingRangeBuilder.getFoldingRanges(file); + if (!ContainerUtil.isEmpty(foldingRanges)) { + CharSequence documentChars = document.getCharsSequence(); + int documentLength = documentChars.length(); + + List containingTextRanges = new ArrayList<>(foldingRanges.size()); + for (FoldingRange foldingRange : foldingRanges) { + TextRange textRange = LSPFoldingRangeBuilder.getTextRange( + foldingRange, + file, + document, + openBraceChar, + closeBraceChar + ); + if ((textRange != null) && (textRange.getLength() > 1)) { + int startOffset = Math.max(0, textRange.getStartOffset() - 1); + int endOffset = Math.min(documentLength - 1, textRange.getEndOffset()); + // These ranges can tend to add whitespace at the end, so trim that before looking for braces + while ((endOffset > startOffset) && Character.isWhitespace(documentChars.charAt(endOffset))) { + endOffset--; + } + + char startChar = documentChars.charAt(startOffset); + char endChar = documentChars.charAt(endOffset); + + // If aligned on an open brace and this ends with the expected close brace, use it + if ((startOffset == openBraceOffset)) { + if ((closeBraceChar != null) && (closeBraceChar == endChar)) { + return textRange; + } else { + return null; + } + } + // If aligned on a close brace and this starts with the expected open brace, use it + else if ((endOffset == closeBraceOffset)) { + if ((openBraceChar != null) && (openBraceChar == startChar)) { + return textRange; + } else { + return null; + } + } + // Otherwise see if it starts and ends with a known brace pair and we'll find the "closest" below + else if (textRange.containsOffset(offset) && + (openBraceOffset == -1) && + (closeBraceOffset == -1) && + LSPCodeBlockUtils.isCodeBlockStartChar(file, startChar)) { + Character pairedCloseBraceChar = LSPCodeBlockUtils.getCodeBlockEndChar(file, startChar); + if ((pairedCloseBraceChar != null) && (pairedCloseBraceChar == endChar)) { + containingTextRanges.add(textRange); + } + } + } + } + + // Return the closest (i.e., smallest) containing text range + if (!ContainerUtil.isEmpty(containingTextRanges)) { + containingTextRanges.sort(Comparator.comparingInt(TextRange::getLength)); + return ContainerUtil.getFirstItem(containingTextRanges); + } + } + + return null; + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/codeBlockProvider/LSPCodeBlockUtils.java b/src/main/java/com/redhat/devtools/lsp4ij/features/codeBlockProvider/LSPCodeBlockUtils.java new file mode 100644 index 000000000..3b3626070 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/codeBlockProvider/LSPCodeBlockUtils.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * Copyright (c) 2025 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ + +package com.redhat.devtools.lsp4ij.features.codeBlockProvider; + +import com.intellij.ide.highlighter.custom.SyntaxTable; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.impl.AbstractFileType; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.CachedValueProvider.Result; +import com.intellij.psi.util.CachedValuesManager; +import com.intellij.util.containers.ContainerUtil; +import com.redhat.devtools.lsp4ij.LSPIJTextMateUtils; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.textmate.psi.TextMateFile; + +import java.util.HashMap; +import java.util.Map; + +/** + * Utilities for deriving information about code blocks. + */ +@ApiStatus.Internal +public final class LSPCodeBlockUtils { + + private static final Map.Entry BRACES_ENTRY = Map.entry('{', '}'); + private static final Map.Entry BRACKETS_ENTRY = Map.entry('[', ']'); + private static final Map.Entry PARENTHESES_ENTRY = Map.entry('(', ')'); + + // NOTE: JetBrains has maintained a long assumption that these are the primary structural block delimiters via + // AbstractFileType's explicit support for them. If/when other structural block delimiters are discovered for + // languages supported by LSP, we can revisit this hard-coded assumption. + private static final Map DEFAULT_BRACE_PAIRS = Map.ofEntries( + BRACES_ENTRY, + BRACKETS_ENTRY, + PARENTHESES_ENTRY + ); + + private LSPCodeBlockUtils() { + // Pure utility class + } + + /** + * Determines whether or not the specified character can start a code block in the provided file. + * + * @param file the PSI file + * @param character the optional character + * @return true if the character can start a code block in the file; otherwise false + */ + @ApiStatus.Internal + public static boolean isCodeBlockStartChar(@NotNull PsiFile file, @Nullable Character character) { + return (character != null) && getBracePairsFwd(file).containsKey(character); + } + + /** + * Determines whether or not the specified character can end a code block in the provided file. + * + * @param file the PSI file + * @param character the optional character + * @return true if the character can end a code block in the file; otherwise false + */ + @ApiStatus.Internal + public static boolean isCodeBlockEndChar(@NotNull PsiFile file, @Nullable Character character) { + return (character != null) && getBracePairsBwd(file).containsKey(character); + } + + /** + * Returns the code block start character for the specified code block end character in the provided file. + * + * @param file the PSI file + * @param codeBlockEndChar the optional code block end character + * @return the corresponding code block start character if found; otherwise null + */ + @Nullable + @ApiStatus.Internal + public static Character getCodeBlockStartChar(@NotNull PsiFile file, @Nullable Character codeBlockEndChar) { + return codeBlockEndChar != null ? getBracePairsBwd(file).get(codeBlockEndChar) : null; + } + + /** + * Returns the code block end character for the specified code block start character in the provided file. + * + * @param file the PSI file + * @param codeBlockStartChar the optional code block start character + * @return the corresponding code block end character if found; otherwise null + */ + @Nullable + @ApiStatus.Internal + public static Character getCodeBlockEndChar(@NotNull PsiFile file, @Nullable Character codeBlockStartChar) { + return codeBlockStartChar != null ? getBracePairsFwd(file).get(codeBlockStartChar) : null; + } + + @NotNull + private static Map getBracePairsFwd(@NotNull PsiFile file) { + return CachedValuesManager.getCachedValue(file, () -> { + Map bracePairs = file instanceof TextMateFile ? LSPIJTextMateUtils.getBracePairs(file) : + file.getFileType() instanceof AbstractFileType ? getAbstractFileTypeBracePairs(file) : + null; + if (bracePairs == null) { + bracePairs = DEFAULT_BRACE_PAIRS; + } + return Result.create(bracePairs, file); + }); + } + + @NotNull + private static Map getBracePairsBwd(@NotNull PsiFile file) { + return CachedValuesManager.getCachedValue(file, () -> Result.create(ContainerUtil.reverseMap(getBracePairsFwd(file)), file)); + } + + @Nullable + private static Map getAbstractFileTypeBracePairs(@NotNull PsiFile file) { + FileType fileType = file.getFileType(); + if (!(fileType instanceof AbstractFileType abstractFileType)) { + return null; + } + + Map bracePairs = new HashMap<>(); + SyntaxTable syntaxTable = abstractFileType.getSyntaxTable(); + if (syntaxTable.isHasBraces()) { + bracePairs.put(BRACES_ENTRY.getKey(), BRACES_ENTRY.getValue()); + } + if (syntaxTable.isHasBrackets()) { + bracePairs.put(BRACKETS_ENTRY.getKey(), BRACKETS_ENTRY.getValue()); + } + if (syntaxTable.isHasParens()) { + bracePairs.put(PARENTHESES_ENTRY.getKey(), PARENTHESES_ENTRY.getValue()); + } + return bracePairs; + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/foldingRange/LSPCodeBlockProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/features/foldingRange/LSPCodeBlockProvider.java deleted file mode 100644 index 8d8531f0d..000000000 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/foldingRange/LSPCodeBlockProvider.java +++ /dev/null @@ -1,86 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2024 Red Hat, Inc. - * Distributed under license by Red Hat, Inc. All rights reserved. - * This program is made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v20.html - * - * Contributors: - * Red Hat, Inc. - initial API and implementation - ******************************************************************************/ -package com.redhat.devtools.lsp4ij.features.foldingRange; - -import com.intellij.codeInsight.editorActions.CodeBlockProvider; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiFile; -import com.intellij.util.containers.ContainerUtil; -import org.eclipse.lsp4j.FoldingRange; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - -import static com.redhat.devtools.lsp4ij.features.foldingRange.LSPFoldingRangeBuilder.*; - -/** - * Code block provider that uses the folding range information from {@link LSPFoldingRangeBuilder}. - */ -public class LSPCodeBlockProvider implements CodeBlockProvider { - - @Override - public @Nullable TextRange getCodeBlockRange(Editor editor, PsiFile file) { - List foldingRanges = LSPFoldingRangeBuilder.getFoldingRanges(file); - if (!ContainerUtil.isEmpty(foldingRanges)) { - Document document = editor.getDocument(); - CharSequence documentChars = document.getCharsSequence(); - int documentLength = documentChars.length(); - - // Adjust the offset slightly based on before/after brace - int offset = editor.getCaretModel().getOffset(); - Character beforeCharacter = offset > 0 ? documentChars.charAt(offset - 1) : null; - Character afterCharacter = offset < documentLength ? documentChars.charAt(offset) : null; - if (isOpenBraceChar(afterCharacter)) { - offset++; - } else if (isCloseBraceChar(beforeCharacter)) { - offset--; - } - - // See if we're anchored by a known brace character - Character openBraceChar = null; - Character closeBraceChar = null; - if ((offset > 0) && isOpenBraceChar(documentChars.charAt(offset - 1))) { - openBraceChar = documentChars.charAt(offset - 1); - closeBraceChar = getCloseBraceChar(openBraceChar); - } else if ((offset < (documentLength - 1)) && isCloseBraceChar(documentChars.charAt(offset + 1))) { - closeBraceChar = documentChars.charAt(offset + 1); - openBraceChar = getOpenBraceChar(closeBraceChar); - } - - List containingTextRanges = new ArrayList<>(foldingRanges.size()); - for (FoldingRange foldingRange : foldingRanges) { - TextRange textRange = LSPFoldingRangeBuilder.getTextRange(foldingRange, document, openBraceChar, closeBraceChar); - if (textRange != null) { - // If this is the exact range for which a matching brace was requested, return it - if ((textRange.getStartOffset() == offset) || (textRange.getEndOffset() == offset)) { - return textRange; - } - // Otherwise add it to the list of containing ranges and we'll find the smallest at the end - else if (textRange.contains(offset)) { - containingTextRanges.add(textRange); - } - } - } - - // If we made it here and found containing text ranges, return the smallest one - if (!ContainerUtil.isEmpty(containingTextRanges)) { - containingTextRanges.sort(Comparator.comparingInt(TextRange::getLength)); - return ContainerUtil.getFirstItem(containingTextRanges); - } - } - - return null; - } -} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/foldingRange/LSPFoldingRangeBuilder.java b/src/main/java/com/redhat/devtools/lsp4ij/features/foldingRange/LSPFoldingRangeBuilder.java index c689e77e2..b829ad225 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/foldingRange/LSPFoldingRangeBuilder.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/foldingRange/LSPFoldingRangeBuilder.java @@ -8,6 +8,7 @@ * Contributors: * Red Hat, Inc. - initial API and implementation ******************************************************************************/ + package com.redhat.devtools.lsp4ij.features.foldingRange; import com.intellij.lang.ASTNode; @@ -24,9 +25,11 @@ import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.client.ExecuteLSPFeatureStatus; import com.redhat.devtools.lsp4ij.client.indexing.ProjectIndexingManager; +import com.redhat.devtools.lsp4ij.features.codeBlockProvider.LSPCodeBlockUtils; import org.eclipse.lsp4j.FoldingRange; import org.eclipse.lsp4j.FoldingRangeRequestParams; import org.eclipse.lsp4j.Position; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -34,7 +37,6 @@ import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -49,20 +51,11 @@ public class LSPFoldingRangeBuilder extends CustomFoldingBuilder { private static final Logger LOGGER = LoggerFactory.getLogger(LSPFoldingRangeBuilder.class); - // NOTE: JetBrains has maintained a long assumption that these are the primary structural block delimiters via - // AbstractFileType's explicit support for them. If/when other structural block delimiters are discovered for - // languages supported by LSP, we can revisit this hard-coded assumption. - private static final Map BRACE_PAIR_CHARS_FWD = Map.of( - '{', '}', - '[', ']', - '(', ')' - ); - private static final Map BRACE_PAIR_CHARS_BWD = ContainerUtil.reverseMap(BRACE_PAIR_CHARS_FWD); - @Override protected void buildLanguageFoldRegions(@NotNull List descriptors, @NotNull PsiElement root, - @NotNull Document document, boolean quick) { + @NotNull Document document, + boolean quick) { // if quick flag is set and not testing, we do nothing here if (quick && !ApplicationManager.getApplication().isUnitTestMode()) { return; @@ -72,7 +65,7 @@ protected void buildLanguageFoldRegions(@NotNull List descrip List foldingRanges = getFoldingRanges(file); if (!ContainerUtil.isEmpty(foldingRanges)) { for (FoldingRange foldingRange : foldingRanges) { - TextRange textRange = getTextRange(foldingRange, document); + TextRange textRange = getTextRange(foldingRange, file, document); if ((textRange != null) && (textRange.getLength() > 0)) { String collapsedText = foldingRange.getCollapsedText(); if (collapsedText != null) { @@ -86,7 +79,8 @@ protected void buildLanguageFoldRegions(@NotNull List descrip } @NotNull - static List getFoldingRanges(@Nullable PsiFile file) { + @ApiStatus.Internal + public static List getFoldingRanges(@Nullable PsiFile file) { if (ProjectIndexingManager.canExecuteLSPFeature(file) != ExecuteLSPFeatureStatus.NOW) { return Collections.emptyList(); } @@ -124,22 +118,27 @@ static List getFoldingRanges(@Nullable PsiFile file) { } @Nullable - private static TextRange getTextRange(@NotNull FoldingRange foldingRange, @NotNull Document document) { - return getTextRange(foldingRange, document, null, null); + private static TextRange getTextRange(@NotNull FoldingRange foldingRange, + @NotNull PsiFile file, + @NotNull Document document) { + return getTextRange(foldingRange, file, document, null, null); } /** * Returns the IDE text range for the LSP folding range, optionally bounded by the expected paired open/close braces. * * @param foldingRange the LSP folding range + * @param file the PSI file * @param document the document * @param openBraceChar the optional open brace character * @param closeBraceChar the optional paired close brace character * @return the corresponding IDE text range, or null if no valid text range could be derived */ @Nullable - static TextRange getTextRange( + @ApiStatus.Internal + public static TextRange getTextRange( @NotNull FoldingRange foldingRange, + @NotNull PsiFile file, @NotNull Document document, @Nullable Character openBraceChar, @Nullable Character closeBraceChar @@ -153,9 +152,9 @@ static TextRange getTextRange( Character startChar = start > 0 ? documentChars.charAt(start - 1) : null; if ((startChar != null) && ((openBraceChar == null) || (startChar == openBraceChar))) { // If necessary, infer the braces for this block - if ((openBraceChar == null) && isOpenBraceChar(startChar)) { + if ((openBraceChar == null) && LSPCodeBlockUtils.isCodeBlockStartChar(file, startChar)) { openBraceChar = startChar; - closeBraceChar = getCloseBraceChar(openBraceChar); + closeBraceChar = LSPCodeBlockUtils.getCodeBlockEndChar(file, openBraceChar); } int end = getEndOffset(foldingRange, document); @@ -176,24 +175,6 @@ static TextRange getTextRange( return textRange; } - static boolean isOpenBraceChar(@Nullable Character character) { - return (character != null) && BRACE_PAIR_CHARS_FWD.containsKey(character); - } - - static boolean isCloseBraceChar(@Nullable Character character) { - return (character != null) && BRACE_PAIR_CHARS_BWD.containsKey(character); - } - - @Nullable - static Character getOpenBraceChar(@Nullable Character closeBraceChar) { - return closeBraceChar != null ? BRACE_PAIR_CHARS_BWD.get(closeBraceChar) : null; - } - - @Nullable - static Character getCloseBraceChar(@Nullable Character openBraceChar) { - return openBraceChar != null ? BRACE_PAIR_CHARS_FWD.get(openBraceChar) : null; - } - private static int getStartOffset(@NotNull FoldingRange foldingRange, @NotNull Document document) { if (foldingRange.getStartCharacter() == null) { return document.getLineEndOffset(foldingRange.getStartLine()); diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/selectionRange/LSPExtendWordSelectionHandler.java b/src/main/java/com/redhat/devtools/lsp4ij/features/selectionRange/LSPExtendWordSelectionHandler.java index 5d45de6e8..2b4a8b900 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/selectionRange/LSPExtendWordSelectionHandler.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/selectionRange/LSPExtendWordSelectionHandler.java @@ -13,43 +13,29 @@ import com.intellij.codeInsight.editorActions.ExtendWordSelectionHandler; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.util.containers.ContainerUtil; -import com.redhat.devtools.lsp4ij.LSPFileSupport; -import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; -import com.redhat.devtools.lsp4ij.client.ExecuteLSPFeatureStatus; -import com.redhat.devtools.lsp4ij.client.indexing.ProjectIndexingManager; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SelectionRange; -import org.eclipse.lsp4j.TextDocumentIdentifier; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.util.*; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; import static com.intellij.codeInsight.editorActions.ExtendWordSelectionHandlerBase.expandToWholeLinesWithBlanks; -import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.isDoneNormally; -import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.waitUntilDone; /** * Implementation of the IDE's extendWordSelectionHandler EP for LSP4IJ files against textDocument/selectionRange. */ public class LSPExtendWordSelectionHandler implements ExtendWordSelectionHandler { - private static final Logger LOGGER = LoggerFactory.getLogger(LSPExtendWordSelectionHandler.class); - @Override public boolean canSelect(@NotNull PsiElement element) { if (!element.isValid()) { @@ -76,18 +62,35 @@ public boolean canSelect(@NotNull PsiElement element) { } @Override - public @Nullable List select(@NotNull PsiElement element, - @NotNull CharSequence editorText, - int offset, - @NotNull Editor editor) { + @Nullable + public List select(@NotNull PsiElement element, + @NotNull CharSequence editorText, + int offset, + @NotNull Editor editor) { PsiFile file = element.getContainingFile(); if (file == null || file.getVirtualFile() == null) { return null; } - // Get the selection ranges + // If the caret is at a line start, try to find the first non-whitespace character in the line and get the + // selection ranges for it + int effectiveOffset = offset; Document document = editor.getDocument(); - List selectionRanges = getSelectionRanges(file, document, offset); + int lineNumber = document.getLineNumber(offset); + int lineStartOffset = document.getLineStartOffset(lineNumber); + if (offset == lineStartOffset) { + int lineEndOffset = document.getLineEndOffset(lineNumber); + int selectionStartOffset = offset; + while (Character.isWhitespace(editorText.charAt(selectionStartOffset)) && selectionStartOffset <= lineEndOffset) { + selectionStartOffset++; + } + if (selectionStartOffset <= lineEndOffset) { + effectiveOffset = selectionStartOffset; + } + } + + // Get the selection ranges + List selectionRanges = LSPSelectionRangeSupport.getSelectionRanges(file, document, effectiveOffset); if (ContainerUtil.isEmpty(selectionRanges)) { return null; } @@ -95,70 +98,22 @@ public boolean canSelect(@NotNull PsiElement element) { // Convert the selection ranges into text ranges Set textRanges = new LinkedHashSet<>(selectionRanges.size()); for (SelectionRange selectionRange : selectionRanges) { - TextRange selectionTextRange = getTextRange(selectionRange, document); + TextRange selectionTextRange = LSPSelectionRangeSupport.getTextRange(selectionRange, document); textRanges.addAll(expandToWholeLinesWithBlanks(editorText, selectionTextRange)); for (SelectionRange parentSelectionRange = selectionRange.getParent(); parentSelectionRange != null; parentSelectionRange = parentSelectionRange.getParent()) { - TextRange parentSelectionTextRange = getTextRange(parentSelectionRange, document); + TextRange parentSelectionTextRange = LSPSelectionRangeSupport.getTextRange(parentSelectionRange, document); textRanges.addAll(expandToWholeLinesWithBlanks(editorText, parentSelectionTextRange)); } } - return new ArrayList<>(textRanges); - } - - private static @NotNull List getSelectionRanges(@NotNull PsiFile file, - @NotNull Document document, - int offset) { - if (ProjectIndexingManager.canExecuteLSPFeature(file) != ExecuteLSPFeatureStatus.NOW) { - return Collections.emptyList(); - } - - // Consume LSP 'textDocument/selectionRanges' request - LSPSelectionRangeSupport selectionRangeSupport = LSPFileSupport.getSupport(file).getSelectionRangeSupport(); - TextDocumentIdentifier textDocumentIdentifier = LSPIJUtils.toTextDocumentIdentifier(file.getVirtualFile()); - Position position = LSPIJUtils.toPosition(offset, document); - var params = new LSPSelectionRangeParams(textDocumentIdentifier, Collections.singletonList(position), offset); - CompletableFuture> selectionRangesFuture = selectionRangeSupport.getSelectionRanges(params); - try { - waitUntilDone(selectionRangesFuture, file); - } catch (ProcessCanceledException e) { - //Since 2024.2 ProcessCanceledException extends CancellationException so we can't use multicatch to keep backward compatibility - //TODO delete block when minimum required version is 2024.2 - selectionRangeSupport.cancel(); - return Collections.emptyList(); - } catch (CancellationException e) { - // cancel the LSP requests textDocument/selectionRanges - selectionRangeSupport.cancel(); - return Collections.emptyList(); - } catch (ExecutionException e) { - LOGGER.error("Error while consuming LSP 'textDocument/selectionRanges' request", e); - return Collections.emptyList(); - } - if (!isDoneNormally(selectionRangesFuture)) { - return Collections.emptyList(); + // If the original offset was at line start and the effective offset was not, remove smaller text ranges + if ((offset == lineStartOffset) && (offset != effectiveOffset)) { + textRanges.removeIf(textRange -> textRange.getStartOffset() > lineStartOffset); } - // textDocument/selectionRanges has been collected correctly, create list of IJ SelectionDescriptor from LSP SelectionRange list - return selectionRangesFuture.getNow(Collections.emptyList()); - } - - /** - * Converts the LSP selection range into the IDE text range. - * - * @param selectionRange the LSP selection range - * @param document the document for for which the selection range applies - * @return the corresponding text range - */ - private static @NotNull TextRange getTextRange(@NotNull SelectionRange selectionRange, - @NotNull Document document) { - Range range = selectionRange.getRange(); - Position start = range.getStart(); - Position end = range.getEnd(); - int startOffset = LSPIJUtils.toOffset(start, document); - int endOffset = LSPIJUtils.toOffset(end, document); - return TextRange.create(startOffset, endOffset); + return new ArrayList<>(textRanges); } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/selectionRange/LSPSelectionRangeSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/selectionRange/LSPSelectionRangeSupport.java index 30163b48a..520578277 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/selectionRange/LSPSelectionRangeSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/selectionRange/LSPSelectionRangeSupport.java @@ -10,19 +10,37 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.selectionRange; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LSPFileSupport; +import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.client.ExecuteLSPFeatureStatus; +import com.redhat.devtools.lsp4ij.client.indexing.ProjectIndexingManager; import com.redhat.devtools.lsp4ij.features.AbstractLSPDocumentFeatureSupport; import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.CompletableFutures; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SelectionRange; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.isDoneNormally; +import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.waitUntilDone; /** * LSP selectionRange support which loads and caches selection ranges by consuming: @@ -33,12 +51,72 @@ */ public class LSPSelectionRangeSupport extends AbstractLSPDocumentFeatureSupport> { + private static final Logger LOGGER = LoggerFactory.getLogger(LSPSelectionRangeSupport.class); + private Integer previousOffset; public LSPSelectionRangeSupport(@NotNull PsiFile file) { super(file); } + @NotNull + @ApiStatus.Internal + public static List getSelectionRanges(@NotNull PsiFile file, + @NotNull Document document, + int offset) { + if (ProjectIndexingManager.canExecuteLSPFeature(file) != ExecuteLSPFeatureStatus.NOW) { + return Collections.emptyList(); + } + + // Consume LSP 'textDocument/selectionRanges' request + LSPSelectionRangeSupport selectionRangeSupport = LSPFileSupport.getSupport(file).getSelectionRangeSupport(); + TextDocumentIdentifier textDocumentIdentifier = LSPIJUtils.toTextDocumentIdentifier(file.getVirtualFile()); + Position position = LSPIJUtils.toPosition(offset, document); + var params = new LSPSelectionRangeParams(textDocumentIdentifier, Collections.singletonList(position), offset); + CompletableFuture> selectionRangesFuture = selectionRangeSupport.getSelectionRanges(params); + try { + waitUntilDone(selectionRangesFuture, file); + } catch (ProcessCanceledException e) { + //Since 2024.2 ProcessCanceledException extends CancellationException so we can't use multicatch to keep backward compatibility + //TODO delete block when minimum required version is 2024.2 + selectionRangeSupport.cancel(); + return Collections.emptyList(); + } catch (CancellationException e) { + // cancel the LSP requests textDocument/selectionRanges + selectionRangeSupport.cancel(); + return Collections.emptyList(); + } catch (ExecutionException e) { + LOGGER.error("Error while consuming LSP 'textDocument/selectionRanges' request", e); + return Collections.emptyList(); + } + + if (!isDoneNormally(selectionRangesFuture)) { + return Collections.emptyList(); + } + + // textDocument/selectionRanges has been collected correctly, create list of IJ SelectionDescriptor from LSP SelectionRange list + return selectionRangesFuture.getNow(Collections.emptyList()); + } + + /** + * Converts the LSP selection range into the IDE text range. + * + * @param selectionRange the LSP selection range + * @param document the document for for which the selection range applies + * @return the corresponding text range + */ + @NotNull + @ApiStatus.Internal + public static TextRange getTextRange(@NotNull SelectionRange selectionRange, + @NotNull Document document) { + Range range = selectionRange.getRange(); + Position rangeStart = range.getStart(); + Position rangeEnd = range.getEnd(); + int startOffset = LSPIJUtils.toOffset(rangeStart, document); + int endOffset = LSPIJUtils.toOffset(rangeEnd, document); + return TextRange.create(startOffset, endOffset); + } + public CompletableFuture> getSelectionRanges(LSPSelectionRangeParams params) { int offset = params.getOffset(); if ((previousOffset != null) && !previousOffset.equals(offset)) { @@ -57,7 +135,6 @@ protected CompletableFuture> doLoad(LSPSelectionRangeParams private static @NotNull CompletableFuture> getSelectionRanges(@NotNull PsiFile file, @NotNull LSPSelectionRangeParams params, @NotNull CancellationSupport cancellationSupport) { - return getLanguageServers(file, f -> f.getSelectionRangeFeature().isEnabled(file), f -> f.getSelectionRangeFeature().isSupported(file)) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 29cd592eb..20a6049f9 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -322,17 +322,17 @@ L language="TEXT" implementationClass="com.redhat.devtools.lsp4ij.features.foldingRange.LSPFoldingRangeBuilder" order="first"/> - - + + diff --git a/src/test/java/com/redhat/devtools/lsp4ij/features/foldingRange/TypeScriptCodeBlockProviderTest.java b/src/test/java/com/redhat/devtools/lsp4ij/features/foldingRange/TypeScriptCodeBlockProviderTest.java index f6b6d4b4d..938c0b83c 100644 --- a/src/test/java/com/redhat/devtools/lsp4ij/features/foldingRange/TypeScriptCodeBlockProviderTest.java +++ b/src/test/java/com/redhat/devtools/lsp4ij/features/foldingRange/TypeScriptCodeBlockProviderTest.java @@ -22,21 +22,21 @@ public TypeScriptCodeBlockProviderTest() { super("*.ts"); } - public void testCodeBlocks() { - // language=json - String mockFoldingRangesJson = """ - [ - { - "startLine": 0, - "endLine": 3 - }, - { - "startLine": 1, - "endLine": 2 - } - ] - """; + // language=json + private static final String SIMPLE_MOCK_FOLDING_RANGES_JSON = """ + [ + { + "startLine": 0, + "endLine": 3 + }, + { + "startLine": 1, + "endLine": 2 + } + ] + """; + public void testSimpleMethodBodyCaretBeforeStatement() { assertCodeBlock( "demo.ts", """ @@ -46,9 +46,135 @@ export class Demo { } } """, - mockFoldingRangesJson - ); + SIMPLE_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 2, + "character": 8 + }, + "end": { + "line": 2, + "character": 15 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 8 + }, + "end": { + "line": 2, + "character": 19 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 8 + }, + "end": { + "line": 2, + "character": 27 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 8 + }, + "end": { + "line": 2, + "character": 28 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 12 + }, + "end": { + "line": 3, + "character": 4 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 11 + }, + "end": { + "line": 3, + "character": 5 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 3, + "character": 5 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 19 + }, + "end": { + "line": 4, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 4, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 4, + "character": 2 + } + } + } + } + } + } + } + } + } + } + } + } + ] + """); + } + public void testSimpleClassBodyCaretBeforeMethod() { assertCodeBlock( "demo.ts", """ @@ -58,33 +184,144 @@ export class Demo { } } """, - mockFoldingRangesJson - ); + SIMPLE_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 1, + "character": 8 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 3, + "character": 5 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 19 + }, + "end": { + "line": 4, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 4, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 4, + "character": 2 + } + } + } + } + } + } + } + ] + """); + } + public void testSimpleClassBodyCaretBeforeOpenBrace() { assertCodeBlock( "demo.ts", """ - export class Demo { + export class Demo { demo() { console.log('demo'); } } """, - mockFoldingRangesJson - ); + SIMPLE_MOCK_FOLDING_RANGES_JSON, + // language=json + "[]"); + } + public void testSimpleClassBodyCaretAfterOpenBrace() { assertCodeBlock( "demo.ts", """ - export class Demo { + export class Demo { demo() { console.log('demo'); } } """, - mockFoldingRangesJson - ); + SIMPLE_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 0, + "character": 18 + }, + "end": { + "line": 0, + "character": 19 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 4, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 4, + "character": 2 + } + } + } + } + } + ] + """); + } + public void testSimpleClassBodyCaretBeforeCloseBrace() { assertCodeBlock( "demo.ts", """ @@ -94,9 +331,51 @@ export class Demo { } } """, - mockFoldingRangesJson - ); + SIMPLE_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 4, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 4, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 4, + "character": 2 + } + } + } + } + } + ] + """); + } + public void testSimpleClassBodyCaretAfterCloseBrace() { assertCodeBlock( "demo.ts", """ @@ -106,9 +385,12 @@ export class Demo { } } """, - mockFoldingRangesJson - ); + SIMPLE_MOCK_FOLDING_RANGES_JSON, + // language=json + "[]"); + } + public void testSimpleCaretBeforeClassName() { assertCodeBlock( "demo.ts", """ @@ -118,7 +400,2737 @@ export class Demo { } } """, - mockFoldingRangesJson - ); + SIMPLE_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 0, + "character": 7 + }, + "end": { + "line": 0, + "character": 12 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 4, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 4, + "character": 2 + } + } + } + } + } + ] + """); + } + + // language=json + private static final String COMPLEX_MOCK_FOLDING_RANGES_JSON = """ + [ + { + "startLine": 0, + "endLine": 7 + }, + { + "startLine": 1, + "endLine": 1 + }, + { + "startLine": 2, + "endLine": 4 + }, + { + "startLine": 2, + "endLine": 3 + }, + { + "startLine": 5, + "endLine": 7 + }, + { + "startLine": 5, + "endLine": 6 + } + ] + """; + + public void testComplexCaretAfterOutermostOpenBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 0, + "character": 15 + }, + "end": { + "line": 0, + "character": 16 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + ] + """); + } + + public void testComplexCaretBeforeOutermostOpenBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 0, + "character": 15 + }, + "end": { + "line": 0, + "character": 16 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + ] + """); + } + + public void testComplexCaretBeforeOutermostCloseBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 8, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + ] + """); + } + + public void testComplexCaretAfterOutermostCloseBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 8, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + ] + """); + } + + public void testComplexCaretBeforeOutermostBlockStatement() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 1, + "character": 17 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 1, + "character": 37 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 2, + "character": 13 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 5, + "character": 14 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretAfterObjectLiteralOpenBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 1, + "character": 19 + }, + "end": { + "line": 1, + "character": 22 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 19 + }, + "end": { + "line": 1, + "character": 26 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 19 + }, + "end": { + "line": 1, + "character": 35 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 18 + }, + "end": { + "line": 1, + "character": 36 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 1, + "character": 37 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 2, + "character": 13 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 5, + "character": 14 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretBeforeObjectLiteralOpenBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 1, + "character": 19 + }, + "end": { + "line": 1, + "character": 22 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 19 + }, + "end": { + "line": 1, + "character": 26 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 19 + }, + "end": { + "line": 1, + "character": 35 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 18 + }, + "end": { + "line": 1, + "character": 36 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 1, + "character": 37 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 2, + "character": 13 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 5, + "character": 14 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretBeforeObjectLiteralCloseBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 1, + "character": 33 + }, + "end": { + "line": 1, + "character": 35 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 28 + }, + "end": { + "line": 1, + "character": 35 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 19 + }, + "end": { + "line": 1, + "character": 35 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 18 + }, + "end": { + "line": 1, + "character": 36 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 1, + "character": 37 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 2, + "character": 13 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 5, + "character": 14 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretAfterObjectLiteralCloseBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 1, + "character": 33 + }, + "end": { + "line": 1, + "character": 35 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 28 + }, + "end": { + "line": 1, + "character": 35 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 19 + }, + "end": { + "line": 1, + "character": 35 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 18 + }, + "end": { + "line": 1, + "character": 36 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 1, + "character": 37 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 2, + "character": 13 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 5, + "character": 14 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretBeforeObjectLiteralProperty() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 1, + "character": 28 + }, + "end": { + "line": 1, + "character": 31 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 28 + }, + "end": { + "line": 1, + "character": 35 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 19 + }, + "end": { + "line": 1, + "character": 35 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 18 + }, + "end": { + "line": 1, + "character": 36 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 1, + "character": 37 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 2, + "character": 13 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 5, + "character": 14 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretAfterPromiseThenOpenBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 2, + "character": 20 + }, + "end": { + "line": 2, + "character": 21 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 20 + }, + "end": { + "line": 4, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 14 + }, + "end": { + "line": 4, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 5, + "character": 14 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretBeforePromiseThenOpenBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 2, + "character": 20 + }, + "end": { + "line": 2, + "character": 21 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 20 + }, + "end": { + "line": 4, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 14 + }, + "end": { + "line": 4, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 5, + "character": 14 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretBeforePromiseThenCloseBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 4, + "character": 8 + }, + "end": { + "line": 4, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 20 + }, + "end": { + "line": 4, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 14 + }, + "end": { + "line": 4, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 5, + "character": 14 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretAfterPromiseThenCloseBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 4, + "character": 8 + }, + "end": { + "line": 4, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 20 + }, + "end": { + "line": 4, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 14 + }, + "end": { + "line": 4, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 5, + "character": 14 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretBeforePromiseThenStatement() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 3, + "character": 12 + }, + "end": { + "line": 3, + "character": 19 + } + }, + "parent": { + "range": { + "start": { + "line": 3, + "character": 12 + }, + "end": { + "line": 3, + "character": 23 + } + }, + "parent": { + "range": { + "start": { + "line": 3, + "character": 12 + }, + "end": { + "line": 3, + "character": 31 + } + }, + "parent": { + "range": { + "start": { + "line": 3, + "character": 12 + }, + "end": { + "line": 3, + "character": 32 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 21 + }, + "end": { + "line": 4, + "character": 8 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 20 + }, + "end": { + "line": 4, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 2, + "character": 14 + }, + "end": { + "line": 4, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 5, + "character": 14 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretAfterPromiseCatchOpenBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 5, + "character": 26 + }, + "end": { + "line": 5, + "character": 27 + } + }, + "parent": { + "range": { + "start": { + "line": 5, + "character": 26 + }, + "end": { + "line": 7, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 5, + "character": 15 + }, + "end": { + "line": 7, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretBeforePromiseCatchOpenBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 5, + "character": 26 + }, + "end": { + "line": 5, + "character": 27 + } + }, + "parent": { + "range": { + "start": { + "line": 5, + "character": 26 + }, + "end": { + "line": 7, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 5, + "character": 15 + }, + "end": { + "line": 7, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretBeforePromiseCatchCloseBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 7, + "character": 8 + }, + "end": { + "line": 7, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 5, + "character": 26 + }, + "end": { + "line": 7, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 5, + "character": 15 + }, + "end": { + "line": 7, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretAfterPromiseCatchCloseBrace() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 7, + "character": 8 + }, + "end": { + "line": 7, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 5, + "character": 26 + }, + "end": { + "line": 7, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 5, + "character": 15 + }, + "end": { + "line": 7, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + ] + """); + } + + public void testComplexCaretBeforePromiseCatchStatement() { + assertCodeBlock( + "demo.ts", + """ + if (condition) { + invokePromise({foo: '', bar: 10}) + .then(() => { + console.log('demo'); + }) + .catch((error) => { + console.error(error); + }); + } + """, + COMPLEX_MOCK_FOLDING_RANGES_JSON, + // language=json + """ + [ + { + "range": { + "start": { + "line": 6, + "character": 12 + }, + "end": { + "line": 6, + "character": 19 + } + }, + "parent": { + "range": { + "start": { + "line": 6, + "character": 12 + }, + "end": { + "line": 6, + "character": 25 + } + }, + "parent": { + "range": { + "start": { + "line": 6, + "character": 12 + }, + "end": { + "line": 6, + "character": 32 + } + }, + "parent": { + "range": { + "start": { + "line": 6, + "character": 12 + }, + "end": { + "line": 6, + "character": 33 + } + }, + "parent": { + "range": { + "start": { + "line": 5, + "character": 27 + }, + "end": { + "line": 7, + "character": 8 + } + }, + "parent": { + "range": { + "start": { + "line": 5, + "character": 26 + }, + "end": { + "line": 7, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 5, + "character": 15 + }, + "end": { + "line": 7, + "character": 9 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 10 + } + }, + "parent": { + "range": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 7, + "character": 11 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 8, + "character": 0 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "parent": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 8, + "character": 2 + } + } + } + } + } + } + } + } + } + } + } + } + } + } + ] + """); } } diff --git a/src/test/java/com/redhat/devtools/lsp4ij/fixtures/LSPCodeBlockProviderFixtureTestCase.java b/src/test/java/com/redhat/devtools/lsp4ij/fixtures/LSPCodeBlockProviderFixtureTestCase.java index a67ddf369..ce4674d4b 100644 --- a/src/test/java/com/redhat/devtools/lsp4ij/fixtures/LSPCodeBlockProviderFixtureTestCase.java +++ b/src/test/java/com/redhat/devtools/lsp4ij/fixtures/LSPCodeBlockProviderFixtureTestCase.java @@ -22,6 +22,7 @@ import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.mock.MockLanguageServer; import org.eclipse.lsp4j.FoldingRange; +import org.eclipse.lsp4j.SelectionRange; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -42,12 +43,18 @@ protected LSPCodeBlockProviderFixtureTestCase(String... fileNamePatterns) { protected void assertCodeBlock(@NotNull String fileName, @NotNull String fileBody, - @NotNull String mockFoldingRangesJson) { + @NotNull String mockFoldingRangesJson, + @NotNull String mockSelectionRangesJson) { MockLanguageServer.INSTANCE.setTimeToProceedQueries(100); + List mockFoldingRanges = JSONUtils.getLsp4jGson().fromJson(mockFoldingRangesJson, new TypeToken>() { }.getType()); MockLanguageServer.INSTANCE.setFoldingRanges(mockFoldingRanges); + List mockSelectionRanges = JSONUtils.getLsp4jGson().fromJson(mockSelectionRangesJson, new TypeToken>() { + }.getType()); + MockLanguageServer.INSTANCE.setSelectionRanges(mockSelectionRanges); + Project project = myFixture.getProject(); PsiFile file = myFixture.configureByText(fileName, stripTokens(fileBody)); Editor editor = myFixture.getEditor();