Skip to content
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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

SCWells72
Copy link
Contributor

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.

…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 {
Copy link
Contributor Author

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:

JetBrains/intellij-community@8df3d04#diff-08fc4fd41510ee4662c41d3f2a671ae2f654d1a2f6ff7608765f427c26eaeae7

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:

  1. Leave it like this until the older IDE versions are no longer supported, then uncomment this and it will just work.
  2. Remove this commented code and TextMate support can be reimplemented when incompatible older versions are no longer supported.
  3. 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 {
Copy link
Contributor Author

@SCWells72 SCWells72 Dec 20, 2024

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(
Copy link
Contributor Author

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(
Copy link
Contributor Author

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)) {
Copy link
Contributor Author

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)) {
Copy link
Contributor Author

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)) {
Copy link
Contributor Author

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);
Copy link
Contributor Author

@SCWells72 SCWells72 Dec 20, 2024

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 {
Copy link
Contributor Author

@SCWells72 SCWells72 Dec 20, 2024

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) {
Copy link
Contributor Author

@SCWells72 SCWells72 Dec 20, 2024

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, () -> {
Copy link
Contributor Author

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) {
Copy link
Contributor Author

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 AbstractFileTypes.

Copy link
Contributor Author

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
Copy link
Contributor Author

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);
Copy link
Contributor Author

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
Copy link
Contributor Author

@SCWells72 SCWells72 Dec 20, 2024

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) {
Copy link
Contributor Author

@SCWells72 SCWells72 Dec 20, 2024

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)) {
Copy link
Contributor Author

@SCWells72 SCWells72 Dec 20, 2024

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);
}

Copy link
Contributor Author

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,
Copy link
Contributor Author

@SCWells72 SCWells72 Dec 20, 2024

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.

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>>() {
Copy link
Contributor Author

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.

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))) {
Copy link
Contributor Author

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.

@SCWells72 SCWells72 marked this pull request as ready for review December 21, 2024 14:29
Copy link
Contributor Author

@SCWells72 SCWells72 Dec 21, 2024

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant