-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: codeBlockProvider
improvements
#716
base: main
Are you sure you want to change the base?
feat: codeBlockProvider
improvements
#716
Conversation
…selection range and folding block information.
…race pair derivation because of a backward incompatible change in the plugin API in September, 2023. Once LSP4IJ no longer supports that older version, this can be restored. Until this, it will degrade gracefully to use default brace pairs.
* Utilities for working with TextMate files in LSP4IJ. | ||
*/ | ||
@ApiStatus.Internal | ||
public final class LSPIJTextMateUtils { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a new class for working with TextMate-specific stuff. Right now it's primarily for deriving the language's brace pairs, but as stated in the PR description, I can't do that in a way that works across all supported IDE versions due to a breaking change in TextMateBracePair
back in September, 2023:
Well, I can make it work via reflection, but I'm not sure if it's worth going that far. I guess the options here are:
- Leave it like this until the older IDE versions are no longer supported, then uncomment this and it will just work.
- Remove this commented code and TextMate support can be reimplemented when incompatible older versions are no longer supported.
- Use reflection to support both older and newer versions until older verions are no longer supported.
Thoughts?
/** | ||
* Code block provider that uses information from {@link LSPSelectionRangeSupport} and {@link LSPFoldingRangeBuilder}. | ||
*/ | ||
public class LSPCodeBlockProvider implements CodeBlockProvider { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that this is no longer strictly tied to the folding range feature -- which it still uses, but alongside the selection range feature now -- so I moved it into its own package. Unfortunately there are enough other changes that Git doesn't see it as a move so there's not a good before/after diff here.
|
||
// 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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Selection ranges from the current offset generally provide more detailed code block information, so try finding the closest containing block using that feature first.
} | ||
|
||
// Failing that, try to find it using the folding ranges | ||
return getUsingFoldingRanges( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we couldn't find a good code block from the selection ranges, try to find one using the folding ranges.
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)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first two conditions here are for when the caret starts exactly on an open or close brace(/bracket/paren) and the evaluated range begins/ends on that offset and ends/begins with the appropriate paired brace. Basically these are an exact brace match.
} | ||
} | ||
// 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)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise we see whether or not the selection range starts and ends with a supported brace pair and, if so, we add it to the list of containing code blocks.
} | ||
|
||
// Return the closest (i.e., smallest) containing text range | ||
if (!ContainerUtil.isEmpty(containingTextRanges)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And then we return the smallest containing code block if at least one was found.
int offset, | ||
@Nullable Character openBraceChar, | ||
@Nullable Character closeBraceChar) { | ||
List<FoldingRange> foldingRanges = LSPFoldingRangeBuilder.getFoldingRanges(file); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has been updated to behave similar to how we process selection ranges.
* Utilities for deriving information about code blocks. | ||
*/ | ||
@ApiStatus.Internal | ||
public final class LSPCodeBlockUtils { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I derived another utility class specifically for code block-related utility methods such as isCodeBlockStart/EndChar()
and getCodeBlockStart/EndChar()
. This is also what tries to use the file type-specific information to derive supported brace pairs. It works for both TextMate files and plain text files backed by AbstractFileType
information, but as stated a few times already, the TextMate aspect is currently commented out due to a breaking API change across the range of supported IDE versions.
Map<Character, Character> bracePairs = file instanceof TextMateFile ? LSPIJTextMateUtils.getBracePairs(file) : | ||
file.getFileType() instanceof AbstractFileType ? getAbstractFileTypeBracePairs(file) : | ||
null; | ||
if (bracePairs == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Degrade gracefully to the previous behavior where we assumed that the brace pairs were the defaults of curly braces, square brackets, and parens.
|
||
@NotNull | ||
private static Map<Character, Character> getBracePairsFwd(@NotNull PsiFile file) { | ||
return CachedValuesManager.getCachedValue(file, () -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've confirmed that the caching here is properly evicted if cached files' file types are changed, e.g., from TextMate to plain text or vice-versa.
} | ||
|
||
@Nullable | ||
private static Map<Character, Character> getAbstractFileTypeBracePairs(@NotNull PsiFile file) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is how supported brace pairs are derived for plain text files backed by AbstractFileType
s.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was moved to the codeBlockProvider
package and was already described above.
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved into LSPCodeBlockUtils
.
@@ -72,7 +65,7 @@ protected void buildLanguageFoldRegions(@NotNull List<FoldingDescriptor> descrip | |||
List<FoldingRange> foldingRanges = getFoldingRanges(file); | |||
if (!ContainerUtil.isEmpty(foldingRanges)) { | |||
for (FoldingRange foldingRange : foldingRanges) { | |||
TextRange textRange = getTextRange(foldingRange, document); | |||
TextRange textRange = getTextRange(foldingRange, file, document); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to pass the PSI file so that we can derive file type-specific brace pairs.
@@ -86,7 +79,8 @@ protected void buildLanguageFoldRegions(@NotNull List<FoldingDescriptor> descrip | |||
} | |||
|
|||
@NotNull | |||
static List<FoldingRange> getFoldingRanges(@Nullable PsiFile file) { | |||
@ApiStatus.Internal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I haven't mentioned this already, but because things are now spread out more across different packages, I've added @ApiStatus.Internal
to pretty much anything that has to be public
but wasn't already part of the published plugin API. Please let me know if you see anything that's become public
that doesn't have that annotation.
@@ -176,24 +175,6 @@ static TextRange getTextRange( | |||
return textRange; | |||
} | |||
|
|||
static boolean isOpenBraceChar(@Nullable Character character) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These were all moved into LSPCodeBlockUtils
.
@@ -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)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These were also renamed to have less "brace"-specific naming.
textRanges.addAll(expandToWholeLinesWithBlanks(editorText, parentSelectionTextRange)); | ||
} | ||
} | ||
return new ArrayList<>(textRanges); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved into LSPSelectionRangeSupport
so that it can be used by LSPCodeBlockProvider
.
private Integer previousOffset; | ||
|
||
public LSPSelectionRangeSupport(@NotNull PsiFile file) { | ||
super(file); | ||
} | ||
|
||
@NotNull | ||
@ApiStatus.Internal | ||
public static List<SelectionRange> getSelectionRanges(@NotNull PsiFile file, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These were moved from LSPExtendWordSelectionHandler
.
...t/java/com/redhat/devtools/lsp4ij/features/foldingRange/TypeScriptCodeBlockProviderTest.java
Outdated
Show resolved
Hide resolved
List<FoldingRange> mockFoldingRanges = JSONUtils.getLsp4jGson().fromJson(mockFoldingRangesJson, new TypeToken<List<FoldingRange>>() { | ||
}.getType()); | ||
MockLanguageServer.INSTANCE.setFoldingRanges(mockFoldingRanges); | ||
|
||
List<SelectionRange> mockSelectionRanges = JSONUtils.getLsp4jGson().fromJson(mockSelectionRangesJson, new TypeToken<List<SelectionRange>>() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the mock selection ranges.
…screte test cases.
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))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We sometimes have to perform a pseudo-"trim" on the tail of the folding range to find the closing brace.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This ends up being a big diff because I had to add mock selection range responses and I added considerably more test cases for more complex code blocks. I also broke each use case out into its own test method. I'd recommend just taking a look at the test class itself in the IDE instead of here.
…anges pretty much exactly as it does for languages with native support, at least with regard to the behavior when the action is activated while the caret is at the beginning of a line.
…native languages when starting for a full line.
Opening this as a draft PR initially to drive discussion, but I'm hoping it will be promoted to a working PR pretty quickly.
This work was driven by some inconsistent behavior and shortcomings I saw with the LSP formatting range-based code block provider, and I figured that by also using the information from the LSP selection range capability, it should be possible to address most if not all of those issues. In practice, that does actually seem to be the case.
This set of changes also includes an update to derive actual language-specific brace pairs from TextMate (
textmate
language) or the IDE's abstract file type (TEXT
language), but due to a breaking change in the TextMate API in the oldest supported IDE versions, the TextMate aspect is currently commented out. Once support for those oldest versions (the breaking change was made in September, 2023 and it stable in all subsequent IDE versions) has been removed, TextMate support can be restored. Alternatively I could use reflection to support both until there's no longer a need for the older API.I'll add other details alongside the PR changes themselves.