Skip to content

feat: Custom file view provider for LSP4IJ TextMate files that uses semantic token information to create a simple PSI "tree" (fixes #98, #531, and #813) #853

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

Closed

Conversation

SCWells72
Copy link
Contributor

@SCWells72 SCWells72 commented Feb 18, 2025

Okay, I'll be adding quite a bit of detail to the changes themselves, but at a high-level, this adds a FileViewProviderFactory for LSP4IJ TextMate files and plain text/abstract file type files in Community Edition IDEs to simulate a flat PSI tree based the language server's reported semantic tokens for the file including whether those elements represent declarations, references, comments, string and numeric literals, etc.

Non-user-defined language server integrations can also opt-in for these features without changing their files' PSI element hierarchies by registering a file view provider factory for their files' language or file type. This has been confirmed to work properly via collaboration with @ericdallo and is documented in LSPApi.md.

The end result is that the following issues are fixed and features are added:

  • For LSP files against servers that support sufficiently descriptive semantic tokens, only declarations and references to declarations are highlighted on Ctrl/Cmd+Mouseover, and nothing else is ever highlighted.
  • For LSP files against servers that do not support semantic tokens such as the CSS language server, the entire file is no longer highlighted on Ctrl/Cmd+Mouseover. This is also the behavior for LSP files against language servers that do support semantic tokens when the semantic tokens for the file are not yet populated.
  • The tooltip shown when hovering in this manner is now much more descriptive, showing the declaration type if known from the semantic tokens and their modifiers followed by the declaration name and, if hovering over a reference that resolves to a declaration in another file, the target file name is also shown. Also, the symbolic names are displayed as code in these tooltips.
  • Attempting to navigate on a reference takes you to the declaration. Attempting to navigate on a declaration shows its usages.

This feature can be disabled for user-defined language servers via the client configuration setting editor.enableSemanticTokensFileViewProvider. That setting defaults to true for all language server definitions, but as stated above, unless a non-user-defined language server integration opts in explicitly by registering a file view provider, it is effectively disabled.

Here's a little demo:

LSP_File_View_Provider_Demo

Regarding concerns about memory bloat or slowness from something like this, I confirmed the following:

  • IDE memory usage before opening large file was ~200MB (after multiple forced GCs).
  • Opened a ~30K line TypeScript file (lib.dom.d.ts).
  • IDE memory usage increased about 45MB (again, foricing GCs).
  • Verified that file editing performance was normal.
  • Verified that the new Ctrl/Cmd+Mouseover behavior worked properly and tracked accurately after making changes.
  • Verified that the standard Go To Declarations or Usages worked properly in the file from both references and declarations.

…Mate files that uses semantic token information -- if available -- to create a simple PSI "tree" (albeit flat) with the information from the semantic tokens.
* @param language the language
* @return true if the language of the file is supported by a language server and false otherwise.
*/
public boolean isFileSupported(@Nullable VirtualFile virtualFile, @Nullable Language language) {
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 needed a new signature of this to use in FileViewProviderFactory.createFileViewProvider() that doesn't need the PSI file at all as it's in the process of creating that file. We do, however, have the virtual file and language at that time, so this just perfoms the same general evaluation based on that information.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the same logic than

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 can't keep the exact same logic because I can't get the PSI file for the virtual file in this context of creating the PSI file. That will result in a StackOverflowError. This is the closest it can be to the original:

    public boolean isFileSupported(@Nullable VirtualFile file, @Nullable Language language) {
        if (file == null) {
            return false;
        }
        FileType fileType = file.getFileType();
        if (fileAssociations
                .stream()
                .anyMatch(mapping -> mapping.match(language, fileType, file.getName()))) {
            if (!file.isInLocalFileSystem()) {
                if (file instanceof LightVirtualFile) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

which, using the IDE's inspection-based suggestions, simplifies down to:

    public boolean isFileSupported(@Nullable VirtualFile file, @Nullable Language language) {
        if (file == null) {
            return false;
        }
        String fileName = file.getName();
        FileType fileType = file.getFileType();
        if (fileAssociations
                .stream()
                .anyMatch(mapping -> mapping.match(language, fileType, fileName))) {
            return file.isInLocalFileSystem() || !(file instanceof LightVirtualFile);
        }
        return false;
    }

Are you okay with that, or would you prefer the first option above?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed it is better, please use that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm wait it is not the properversion, There is a check with phiycal also.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no PSI file to check for being physical here. That's the point...there needs to be a check for whether a virtual file and its language are supported by a configured language server definition while creating the PSI file in the view provider factory. So it's limited to what can be checked about a virtual file, and we're already asking if it's in the local file system and/or a light virtual file. Does that help explain the use case and associated limitations?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry, you must not get the PsiFile if I have understood. It could look like this:

public boolean isFileSupported(@Nullable VirtualFile file, @NotNull Project project) {
        if (file == null) {
            return false;
        }
        Language language = LSPIJUtils.getFileLanguage(file, project);
       return isFileSupported(file, language, true);
    }
}

public boolean isFileSupported(@Nullable VirtualFile file, @NotNull Language language, boolean checkPsiFile) {
        if (file == null) {
            return false;
        }
       FileType fileType = file.getFileType();
        if (fileAssociations
                .stream()
                .anyMatch(mapping -> mapping.match(language, fileType, file.getName()))) {
            if (!file.isInLocalFileSystem()) {
                if (file instanceof LightVirtualFile) {
                    return false;
                }
                if (checkPsiFile) {
                  PsiFile psiFile = LSPIJUtils.getPsiFile(file, project);
                  if (psiFile != null && !psiFile.isPhysical()) {
                      return false;
                  }
                }
            }
            return true;
        }
        return false;
}

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 line:

                PsiFile psiFile = LSPIJUtils.getPsiFile(file, project);

would lead to a StackOverflowException in this specific context because that PSI file is being created when this check is made. That's the whole reason for this VirtualFile/Language-based check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that, it is the reason why I add checkPsiFile parameter. In your case you will call the method with false.

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 don't have a Project in this signature to retrieve a project-specific PsiFile for the VirtualFile, so that won't really work.

I think this should cover the concerns/confusion:

    /**
     * Returns true if the virtual file and optional language are supported by a language server and false otherwise.
     * This signature should be used only when it is not yet possible yet to check the PSI file because it is in the
     * process of being created via a file view provider factory. If a PSI file is available, one of the other
     * signatures of {@link #isFileSupported} should be used instead.
     *
     * @param virtualFile the virtual file
     * @param language    the language
     * @return true if the virtual file is supported by a configured language server and false otherwise.
     */
    @ApiStatus.Internal
    public boolean isFileSupported(@Nullable VirtualFile virtualFile, @Nullable Language language) {
        if (virtualFile == null) {
            return false;
        }

        FileType fileType = virtualFile.getFileType();
        String fileName = virtualFile.getName();
        if (fileAssociations
                .stream()
                .anyMatch(mapping -> mapping.match(language, fileType, fileName))) {
            return virtualFile.isInLocalFileSystem() || !(virtualFile instanceof LightVirtualFile);
        }

        return false;
    }

The method is clearly documented for a specific use case and refers the caller to the other signatures if not for that use case. The parameter is clearly named virtualFile to clarify that it operates on a VirtualFile and not a PsiFile. The absence of a Project in the parameter list makes retrieval of the correct PsiFile an issue anyway. Finally, it is annotated as @ApiStatus.Internal to make it clear that this signature should not be used outside of the internal implementation of LSP4IJ.

I'll get this committed along with the change to lazy-create the semantic token PSI element, then I'll add a client config-based feature flag to disable this behavior if desired.

@@ -27,7 +30,12 @@
*/
public class LSPTargetElementEvaluator extends TargetElementEvaluatorEx2 {
@Override
public @Nullable PsiElement adjustReferenceOrReferencedElement(@NotNull PsiFile file, @NotNull Editor editor, int offset, int flags, @Nullable PsiElement refElement) {
@Nullable
public PsiElement adjustReferenceOrReferencedElement(@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.

These are just formatting changes.

@@ -36,7 +44,13 @@ public class LSPTargetElementEvaluator extends TargetElementEvaluatorEx2 {
return null;
}

// Try to find the word at the caret and return a fake PSI element for it
// See if the view provider can provide an element
FileViewProvider viewProvider = file.getViewProvider();
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 this file uses a file view provider based on semantic tokens, we can use it to find the semantic token-based element at the specified offset.

@@ -42,25 +47,53 @@ public class LSPGotoDeclarationHandler implements GotoDeclarationHandler {

@Nullable
@Override
public PsiElement[] getGotoDeclarationTargets(@Nullable PsiElement sourceElement, int offset, Editor editor) {
public PsiElement[] getGotoDeclarationTargets(@Nullable PsiElement sourceElement,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting-only changes.

Project project = editor.getProject();
if (project == null || project.isDisposed()) {
return PsiElement.EMPTY_ARRAY;
}
PsiFile psiFile = sourceElement.getContainingFile();
PsiFile psiFile = sourceElement != null ? sourceElement.getContainingFile() : null;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed a potential NPE.

if (psiFile == null) {
return PsiElement.EMPTY_ARRAY;
}
if (!LanguageServersRegistry.getInstance().isFileSupported(psiFile)) {
return PsiElement.EMPTY_ARRAY;
}

// If this was called for a populated semantic tokens view provider, just get the target directly from it
if (psiFile.getViewProvider() instanceof LSPSemanticTokensFileViewProvider semanticTokensFileViewProvider) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty much what the comment above say, but if this is a semantic tokens-backed file, we can see if the element at the specified offset is a reference and, if so, we can resolve that reference to get the target element(s).

PsiReference reference = semanticTokensFileViewProvider.findReferenceAt(offset);
PsiElement target = reference != null ? reference.resolve() : null;
return target != null ? new PsiElement[]{target} : PsiElement.EMPTY_ARRAY;
} else if (semanticTokensFileViewProvider.isDeclaration(offset)) {
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 it's a declaration, there's nothing to return. The IDE already handles declaration self-references.

return PsiElement.EMPTY_ARRAY;
}

// NOTE: It's important that we short-circuit any further processing of this potential reference if this
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But here's a critical part of this change. If it's 100% definitely in a semantic tokens-backed file and it's not a reference or declaration, we must not allow other handlers to process or the entire file will be highlighted as a link. Instead we throw a ProcessCanceledException to stop further processing, and that's why/how the issue with the entire file being highlighted as a link is fixed.

}

@ApiStatus.Internal
public static PsiElement @NotNull [] getGotoDeclarationTargets(@NotNull Editor editor,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted the part that uses LSP to resolve the reference into a reusable method as we'll need it elsewhere as well.

@@ -239,10 +240,10 @@ private static Set<PsiReference> getExternalReferences(@NotNull PsiFile file, in
@Override
public void run(@NotNull ProgressIndicator progressIndicator) {
progressIndicator.setIndeterminate(true);
LSPExternalReferencesFinder.processExternalReferences(file, offset, reference -> {
ReadAction.run(() -> LSPExternalReferencesFinder.processExternalReferences(file, offset, reference -> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticed while testing that this needed to be wrapped in a read action.

public void highlight(@NotNull PsiFile file,
@NotNull Document document,
@NotNull Consumer<HighlightInfo> addInfo) {
// Try to populate the file's view provider with these tokens if possible
FileViewProvider viewProvider = file.getViewProvider();
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 this file has a view provider that is based on semantic tokens, populate them while already processing them. That way we don't introduce any new LSP calls for this purpose. We just use information we're already getting anyway.

@@ -117,6 +124,11 @@ public void highlight(@NotNull PsiFile file,
addInfo.accept(highlightInfo);
}

// If this file uses a view provider based on semantic tokens, add this one
if (semanticTokensViewProvider != null) {
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 where we add the new token to the file's view provider if appropriate.

/**
* Represents a concrete semantic token in a file.
*/
final class LSPSemanticToken {
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 the wrapper for LSP-provided semantic token information that also knows how to create a PSI element based on that information.

SemanticTokenTypes.Decorator,
// TODO: Need a mechanism for language server definitions to register token types and token modifiers
// including how they should be interpreted for syntax highlighting and categorization
"member" // JavaScript/TypeScript-specific
Copy link
Contributor Author

@SCWells72 SCWells72 Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warrants some discussion because it's closely related to the static semantic token modifier we discussed a bit back. The TypeScript language server has a non-standard (but critical) semantic token type, member, that represents a class member declaration, e.g., a field/property or method. If that's not added here, you lose quite a bit of Ctrl/Cmd+Hover behavior in JavaScript/TypeScript files.

We definitely need to have some way for a given language server definition -- custom or user-defined -- to be able to register custom token types and token modifiers along with how they should be interpreted by LSP4IJ in different contexts, e.g., syntax highlighting, token type/modifier-to-element type translation, etc.

I'm happy to do that work, but I believe it should happen separately from this set of changes. For the moment, I would like to temporarily include member as a hard-coded token type just as label is already included as hard-coded token modifier, and then I'll circle back in another PR and make this all extensible including migrating those two hard-coded examples -- and static -- into that extension model.

@NotNull List<String> tokenModifiers) {
if (tokenType != null) {
// If this is an identifier name token, see if it's a declaration or a reference
if (IDENTIFIER_NAME_TOKEN_TYPES.contains(tokenType)) {
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 a big piece of the magic in this set of changes. We can use the combination of token type and token modifiers to understand how a specific portion of the file should be interpreted. The most important aspect is knowing that a given word is the name identifier for a declaration or a reference to a declaration, but we also know concretely that a given token is a comment, string or numeric literal, regular expression (which actually gives us a good opportunity for RegExp language injection), operator, etc.

* Documentation provider for {@link LSPSemanticTokensFileViewProvider} files that provides quick navigation info
* based on information from the semantic token.
*/
public class LSPSemanticTokenDocumentationProvider extends AbstractDocumentationProvider {
Copy link
Contributor Author

@SCWells72 SCWells72 Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lang.documentationProvider provides description text for semantic token-based elements in a quick navigation info context.

*/
class LSPSemanticTokenElementType extends IElementType {
// These are the concrete element types that we can derive from semantic token types and modifiers
static final LSPSemanticTokenElementType DECLARATION = new LSPSemanticTokenElementType("DECLARATION");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have actual PsiElements, we need IElementTypes for them. These are the known element types for an LSP4IJ TextMate file backed by semantic tokens.

/**
* A PSI element for a semantic token in a {@link LSPSemanticTokensFileViewProvider} file.
*/
public class LSPSemanticTokenPsiElement extends LSPPsiElement implements PsiNameIdentifierOwner {
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 this is the actual element. Note that it also implements PsiNameIdentifierOwner which is what enables the IDE's Go To Declaration or Usages to work for usages when activated on a declaration. See #getNameIdentifier() below.

* If not registered, an error is raised by the IDE about not being able to find an element manipulator. This is
* fundamentally a no-op and just returns the original element.
*/
public class LSPSemanticTokenPsiElementManipulator extends AbstractElementManipulator<LSPSemanticTokenPsiElement> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it says above, this must be registered now that we have named elements or there will be a runtime exception. The big reason for this is to handle element renames, but we can basically no-op because the actual rename changes are being applied by LSP text edits and not in PSI.

* A PSI reference to a semantic token declaration that defers uses
* {@link LSPGotoDeclarationHandler#getGotoDeclarationTargets(PsiElement, int, Editor)} for deferred resolution.
*/
class LSPSemanticTokenPsiReference extends PsiReferenceBase<LSPSemanticTokenPsiElement> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we have a PsiElement and an IElementType. We also need a PsiReference for those elements that are references. This is it.


@Override
@Nullable
public PsiElement resolve() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It resolves lazily using the extracted method in LSPGotoDeclarationHandler.

* semantic tokens.
*/
@ApiStatus.Internal
public class LSPSemanticTokensFileViewProvider extends SingleRootFileViewProvider {
Copy link
Contributor Author

@SCWells72 SCWells72 Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual file view provider for a semantic tokens-based TextMate file. It's a standard single root file view provider.

I'll let the comment-based documentation explain everything else.

PsiFile file = getSupportedFile();
if (file != null) {
// By caching the storage on the file this way, it's automatically evicted when the file changes
return CachedValuesManager.getCachedValue(file, new CachedValueProvider<>() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully you're already familiar with CachedValuesManager, but I'm using it here and one other place below to cache file-specific information that should expire when the PsiFile is rendered invalid by edits, file type changes, etc.


@Override
@NotNull
public FileViewProvider createFileViewProvider(@NotNull VirtualFile virtualFile,
Copy link
Contributor Author

@SCWells72 SCWells72 Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this is the file view provider factory. It uses the new method for checking whether the provided file and language or file type combination is configured for LSP4IJ before creating our file view provider. Otherwise it just uses the standard file view provider that would have been used for the file.

@@ -23,10 +26,17 @@
*/
public class LSPUsageElementDescriptionProvider implements ElementDescriptionProvider {
@Override
public @Nullable @NlsSafe String getElementDescription(@NotNull PsiElement element, @NotNull ElementDescriptionLocation location) {
@Nullable
Copy link
Contributor Author

@SCWells72 SCWells72 Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated this to use the semantic token information to provide better element descriptions if possible.

if (element instanceof LSPUsageTriggeredPsiElement) {
return LanguageServerBundle.message("usage.description");
}
// If this is for Ctrl/Cmd+Mouse hover, try to get the element description from the documentation provider
Copy link
Contributor Author

@SCWells72 SCWells72 Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this handles the Ctrl/Cmd+Mouseover use case.

NOTE: This can only be registered for TextMate files because abstract file type plain text files have no
language and a variable file type.
-->
<lang.fileViewProviderFactory
Copy link
Contributor Author

@SCWells72 SCWells72 Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EP registrations for the TextMate file view provider and other things needed to make it whole.

@SCWells72 SCWells72 marked this pull request as ready for review February 18, 2025 21:38
protected LSPSemanticTokensFileViewProviderFixtureTestCase(@NotNull String fileNamePattern,
@NotNull String languageId) {
super(fileNamePattern);
setLanguageId(languageId);
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 must always set a language ID or the file type mapping isn't available via the language server definition. Not sure if that's intentional or not, but see LanguageServersRegistry#registerAssociation() which does the following for each mapping type:

if (!StringUtils.isEmpty(languageId)) {
    serverDefinition.registerAssociation(language, languageId);
}

return file;
}

protected void assertViewProviderEnabled(@NotNull String fileName,
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 the following two methods are the primary assertion/verification interface for the test subclasses alongside the token verifiers above.

@SCWells72
Copy link
Contributor Author

@angelozerr I augmented the unit tests for this PR yesterday to verify significantly more details about the semantic token information proxied by the file view provider and to confirm the dynamic file view provider factory used for plain text/abstract file type files. @mmeyer724 and @ericdallo have been using -- or at least trying out -- these changes for their respective environments and can hopefully help provide a sense of confidence in the changes alongside the others you've asked to provide feedback.

I think you may still be on PTO with your family, so please don't look at it until you're back and settled, but once you do get a chance, please provide whatever feedback (and/or questions) you have so that we can start working toward a merge if possible.

…e resulting nodes in the Find Usages views have declaration type-specific icons if available.

@Override
@Nullable
public Icon getIcon(boolean open) {
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 the real element can provide an icon, delegate to it.

* @return the corresponding icon, or null if no icon could be found
*/
@Nullable
public static Icon getIcon(@Nullable String tokenType) {
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 another signature to map a value from SemanticTokenTypes to the appropriate icon.

…okenTypes and updated logic that behaves based on semantic token type to use that information, specifically the syntax highlighter and icon mapper.
SemanticTokenTypes.Operator,
SemanticTokenTypes.Decorator
),
Arrays.stream(LSPSemanticTokenTypes.values())
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 will be a recurring pattern where the standard semantic token types are merged with any custom ones. The notion of "custom" needs to be fleshed out more in response to #871.

@@ -29,6 +29,12 @@ public class DefaultSemanticTokensColorsProvider implements SemanticTokensColors
public @Nullable TextAttributesKey getTextAttributesKey(@NotNull String tokenType,
@NotNull List<String> tokenModifiers,
@NotNull PsiFile file) {
// If this is for a custom semantic token type that expresses an inheritance relationship, swap it out now
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 type of logic will exist anywhere we're implementing behavior based on a known semantic token type. It allows a custom semantic token type to base its behavior on a standard semantic token type.

* A custom semantic token type.
*/
@ApiStatus.Internal
public class LSPSemanticTokenType {
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 and the next class are an absolute minimalist implementation of #871.

@ApiStatus.Internal
public class LSPSemanticTokenTypes {

// TypeScript "member"
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 obviously hard-coded for now or JavaScript/TypeScript simply wouldn't work very well. It's the same token type we discussed previously, and I agree that there needs to be a first-class way for a language server to register custom semantic token types and modifiers and how they should be interpreted by LSP4IJ. See #871 for details on a more complete implementation.

I'd like to propose that this hard-coded exception be allowed right now and, once all of this work is merged, I'll take care of #871, either as a derivative of what's proposed there or whatever other approach is determined to be used instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please create an issue to avoid doing that. Have you planned to work on that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's already captured in #871 as noted above.

@Nullable
public static Icon getIcon(@Nullable String tokenType) {
if (tokenType != null) {
// If this is for a custom semantic token type that expresses an inheritance relationship, swap it out now
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 again, support for custom semantic token types.

@@ -29,6 +29,18 @@ public class DefaultSemanticTokensColorsProvider implements SemanticTokensColors
public @Nullable TextAttributesKey getTextAttributesKey(@NotNull String tokenType,
@NotNull List<String> tokenModifiers,
@NotNull PsiFile file) {
LSPSemanticTokenType lspSemanticTokenType = LSPSemanticTokenTypes.valueOf(tokenType);
if (lspSemanticTokenType != null) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minimalist support for custom semantic token types where the inheritFrom and textAttributesKey information is used if available.

public class LSPSemanticTokenTypes {

// https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide#standard-token-types-and-modifiers
private static final LSPSemanticTokenType Label = new LSPSemanticTokenType(
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 encapsulates the notion of a label and removes the hard-coded text attributes association from DefaultSemanticTokensColorProvider.

@angelozerr
Copy link
Contributor

@angelozerr I augmented the unit tests for this PR yesterday to verify significantly more details about the semantic token information proxied by the file view provider and to confirm the dynamic file view provider factory used for plain text/abstract file type files. @mmeyer724 and @ericdallo have been using -- or at least trying out -- these changes for their respective environments and can hopefully help provide a sense of confidence in the changes alongside the others you've asked to provide feedback.

I think you may still be on PTO with your family, so please don't look at it until you're back and settled, but once you do get a chance, please provide whatever feedback (and/or questions) you have so that we can start working toward a merge if possible.

Indeed and I'm back, I will review your PR next week.

@angelozerr
Copy link
Contributor

@SCWells72 it seems this branch have some conflicts:

image

@SCWells72
Copy link
Contributor Author

@SCWells72 it seems this branch have some conflicts:

This is the same thing we've seen before where it's only conflicts for rebase. I'm not showing any conflicts for merge:

image

Let's do the review against this PR/branch since it includes all of the context, and when we feel it's ready for merge, I'll create a fresh branch for the merge.

/**
* The semantic token type name
*/
public String name;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using getter/setter should be better, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@angelozerr
Copy link
Contributor

Just one word: it is so AMAZING!!!!

I had 2 few comments, please fix it and recreate the PR in order I can merge it.

Thanks so much @SCWells72 for your fantastic work!

@SCWells72
Copy link
Contributor Author

Merge will occur via #882.

@SCWells72 SCWells72 closed this Mar 11, 2025
@SCWells72 SCWells72 deleted the lsp_file_view_provider branch March 11, 2025 21:50
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.

4 participants