Skip to content

Commit 71f6d73

Browse files
angelozerrfbricon
authored andcommitted
feat: Support syntax coloration for code block in LSP hover
Fixes #297 Signed-off-by: azerr <[email protected]>
1 parent cb04382 commit 71f6d73

22 files changed

+1000
-232
lines changed

docs/LSPSupport.md

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,13 +192,64 @@ Here is an example with the [Qute language server](https://github.com/redhat-dev
192192

193193
#### Hover
194194

195-
[textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) is implemented with `platform.backend.documentation.targetProvider`
196-
extension point to support any language and `textDocument/hover` works out-of-the-box.
195+
[textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) is implemented with the `platform.backend.documentation.targetProvider`
196+
extension point, to support any language, making `textDocument/hover` work out-of-the-box for all languages.
197197

198198
Here is an example with the [Qute language server](https://github.com/redhat-developer/quarkus-ls/tree/master/qute.ls) showing documentation while hovering over an `include` section:
199199

200200
![textDocument/hover](./images/lsp-support/textDocument_hover.png)
201201

202+
##### Syntax Coloration
203+
204+
LSP4IJ supports `Syntax Coloration` on hover and completion documentation. Here is an example with [Rust Analyzer](https://rust-analyzer.github.io/):
205+
206+
![textDocument/hover Syntax Coloration](./images/lsp-support/textDocument_hover_syntax_coloration.png)
207+
208+
`Syntax Coloration` is supported in:
209+
210+
* MarkDown [Fenced Code Block](https://www.markdownguide.org/extended-syntax/#fenced-code-blocks).
211+
212+
~~~
213+
```ts
214+
const s = "";
215+
const c = s.charAt(0);
216+
```
217+
~~~
218+
219+
It will apply the `TypeScript TextMate` highlighting, inferred from the `ts` file extension, and will be rendered like:
220+
221+
![textDocument/hover Syntax Coloration Code Block](./images/lsp-support/textDocument_hover_syntax_coloration_codeBlock.png)
222+
223+
* MarkDown [Indented Blockquote](https://www.markdownguide.org/basic-syntax/#blockquotes-1) (`>` following with `5 spaces`).
224+
225+
```
226+
> const s = '';
227+
> const c = s.charAt(0);
228+
```
229+
230+
will use the Syntax coloration which triggers the hover / completion.
231+
232+
![textDocument/hover Syntax Coloration Blockquote](./images/lsp-support/textDocument_hover_syntax_coloration_codeBlock.png)
233+
234+
###### Syntax coloration discovery
235+
236+
The rules to retrieve the proper syntax coloration are:
237+
238+
* if the code block defines the file extension, LSP4IJ tries to apply a matching TextMate grammar or custom syntax coloration by filename, mimetype.
239+
* if the code block defines a language Id (e.g. `typescript`) and the syntax coloration must be retrieved from TextMate, it will use the
240+
[fileNamePatterns extension point](./DeveloperGuide.md#file-name-pattern-mapping) or [./UserDefinedLanguageServer.md#mappings-tab](fileNamePattern of User defined language server)
241+
to find the file extension (e.g. `ts`) associated with the languageId (e.g. `typescript`)
242+
~~~
243+
```typescript
244+
const s = "";
245+
const c = s.charAt(0);
246+
```
247+
~~~
248+
* if the code block doesn't define the language, or indented blockquotes are used, it will fall back to the syntax coloration from the file in which the hover / completion documentation request was triggered.
249+
250+
If those strategies are insufficient for your needs, please [create an issue](https://github.com/redhat-developer/lsp4ij/issues) to request an extension point
251+
for mapping the language and the file extension.
252+
202253
#### CodeLens
203254

204255
[textDocument/codeLens](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeLens) is implemented with the `codeInsight.codeVisionProvider` extension point.
Loading
Loading

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ platformType=IC
1313
platformVersion=2023.2
1414
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
1515
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
16-
platformPlugins=com.redhat.devtools.intellij.telemetry:1.1.0.52
16+
platformPlugins=com.redhat.devtools.intellij.telemetry:1.1.0.52, textmate
1717
# Gradle Releases -> https://github.com/gradle/gradle/releases
1818
gradleVersion=8.5
1919
channel=nightly

src/main/java/com/redhat/devtools/lsp4ij/LanguageServersRegistry.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ public static LanguageServersRegistry getInstance() {
5858

5959
private final List<LanguageServerFileAssociation> fileAssociations = new ArrayList<>();
6060

61+
private final Map<String /* languageId (ex : typescript) */,
62+
List<String> /* file extensions (ex : ts) */> languageIdFileExtensionsCache = new HashMap<>();
63+
6164
private final Collection<LanguageServerDefinitionListener> listeners = new CopyOnWriteArrayList<>();
6265

6366
private final List<ProviderInfo<? extends Object>> inlayHintsProviders = new ArrayList<>();
@@ -71,7 +74,6 @@ private void initialize() {
7174
loadServersAndMappingsFromExtensionPoint();
7275
// Load language servers / mappings from user settings
7376
loadServersAndMappingFromSettings();
74-
7577
updateLanguages();
7678
}
7779

@@ -192,7 +194,7 @@ private void updateFindUsagesProvider(Set<Language> distinctLanguages) {
192194
LSPFindUsagesProvider provider = new LSPFindUsagesProvider();
193195
for (Language language : distinctLanguages) {
194196
var existingProviders = LanguageFindUsages.INSTANCE.allForLanguage(language);
195-
if (existingProviders.isEmpty() || (existingProviders.size() == 1 && existingProviders.get(0) instanceof EmptyFindUsagesProvider)) {
197+
if (existingProviders.isEmpty() || (existingProviders.size() == 1 && existingProviders.get(0) instanceof EmptyFindUsagesProvider)) {
196198
LanguageFindUsages.INSTANCE.addExplicitExtension(language, provider);
197199
}
198200
}
@@ -258,6 +260,21 @@ public void registerAssociation(@NotNull LanguageServerDefinition serverDefiniti
258260
@Nullable String languageId = mapping.getLanguageId();
259261
if (!StringUtils.isEmpty(languageId)) {
260262
serverDefinition.registerAssociation(matchers, languageId);
263+
// Update the mapping languageId -> file extensions
264+
var fileExtensions = languageIdFileExtensionsCache.get(languageId);
265+
if (fileExtensions == null) {
266+
fileExtensions = new ArrayList<>();
267+
languageIdFileExtensionsCache.put(languageId, fileExtensions);
268+
}
269+
fileExtensions.addAll(fileNamePatternMapping
270+
.getFileNamePatterns()
271+
.stream()
272+
.map(fileExtension -> {
273+
int index = fileExtension.indexOf('.');
274+
return index != -1 ? fileExtension.substring(index + 1, fileExtension.length()) : fileExtension;
275+
})
276+
.toList());
277+
261278
}
262279
fileAssociations.add(new LanguageServerFileAssociation(matchers, serverDefinition, mapping.getDocumentMatcher(), languageId));
263280
}
@@ -521,12 +538,24 @@ public List<ProviderInfo<? extends Object>> getInlayHintProviderInfos() {
521538
return inlayHintsProviders;
522539
}
523540

524-
public record UpdateServerDefinitionRequest(@NotNull Project project, @NotNull UserDefinedLanguageServerDefinition serverDefinition,
541+
public record UpdateServerDefinitionRequest(@NotNull Project project,
542+
@NotNull UserDefinedLanguageServerDefinition serverDefinition,
525543
@Nullable String name, @Nullable String commandLine,
526544
@Nullable Map<String, String> userEnvironmentVariables,
527545
boolean includeSystemEnvironmentVariables,
528546
@NotNull List<ServerMappingSettings> mappings,
529547
@Nullable String configurationContent,
530548
@Nullable String initializationOptionsContent) {
531549
}
550+
551+
/**
552+
* Returns the list of supported file extensions (ex: ts) for the given language Id (ex : typescript) and null otherwise.
553+
*
554+
* @param languageId the language Id (ex : typescript).
555+
* @return the list of supported file extensions (ex: ts) for the given language Id (ex : typescript) and null otherwise.
556+
*/
557+
@Nullable
558+
public List<String> getFileExtensions(String languageId) {
559+
return languageIdFileExtensionsCache.get(languageId);
560+
}
532561
}

src/main/java/com/redhat/devtools/lsp4ij/ServerMessageHandler.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,14 @@
2727
import com.intellij.openapi.ui.popup.JBPopupListener;
2828
import com.intellij.openapi.ui.popup.LightweightWindowEvent;
2929
import com.redhat.devtools.lsp4ij.console.LSPConsoleToolWindowPanel;
30+
import com.redhat.devtools.lsp4ij.features.documentation.MarkdownConverter;
3031
import com.redhat.devtools.lsp4ij.internal.StringUtils;
3132
import org.eclipse.lsp4j.*;
3233
import org.jetbrains.annotations.NotNull;
3334

3435
import javax.swing.*;
3536
import java.util.concurrent.CompletableFuture;
3637

37-
import static com.redhat.devtools.lsp4ij.features.documentation.MarkdownConverter.toHTML;
38-
3938
public class ServerMessageHandler {
4039

4140
public static final String LSP_WINDOW_SHOW_MESSAGE_GROUP_ID = "LSP/window/showMessage";
@@ -87,7 +86,7 @@ public static void showMessage(@NotNull String title,
8786
@NotNull Project project) {
8887
Notification notification = new Notification(LSP_WINDOW_SHOW_MESSAGE_GROUP_ID,
8988
title,
90-
toHTML(params.getMessage()),
89+
MarkdownConverter.getInstance(project).toHtml(params.getMessage()),
9190
messageTypeToNotificationType(params.getType()));
9291
notification.setListener(NotificationListener.URL_OPENING_LISTENER);
9392
notification.setIcon(messageTypeToIcon(params.getType()));
@@ -101,13 +100,14 @@ public static void showMessage(@NotNull String title,
101100
* @param params the message request parameters
102101
*/
103102
public static CompletableFuture<MessageActionItem> showMessageRequest(@NotNull LanguageServerWrapper serverWrapper,
104-
@NotNull ShowMessageRequestParams params) {
103+
@NotNull ShowMessageRequestParams params,
104+
@NotNull Project project) {
105105
CompletableFuture<MessageActionItem> future = new CompletableFuture<>();
106106
ApplicationManager.getApplication()
107107
.invokeLater(() -> {
108108
String languageServerName = serverWrapper.getServerDefinition().getDisplayName();
109109
String title = params.getMessage();
110-
String content = toHTML(params.getMessage());
110+
String content = MarkdownConverter.getInstance(project).toHtml(params.getMessage());
111111
final Notification notification = new Notification(
112112
LSP_WINDOW_SHOW_MESSAGE_REQUEST_GROUP_ID,
113113
languageServerName,

src/main/java/com/redhat/devtools/lsp4ij/client/LanguageClientImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public void telemetryEvent(Object object) {
7979

8080
@Override
8181
public final CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams requestParams) {
82-
return ServerMessageHandler.showMessageRequest(wrapper, requestParams);
82+
return ServerMessageHandler.showMessageRequest(wrapper, requestParams, getProject());
8383
}
8484

8585
@Override

src/main/java/com/redhat/devtools/lsp4ij/commands/CommandExecutor.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import com.intellij.openapi.vfs.VirtualFile;
3333
import com.intellij.psi.PsiFile;
3434
import com.redhat.devtools.lsp4ij.*;
35+
import com.redhat.devtools.lsp4ij.features.documentation.MarkdownConverter;
3536
import org.eclipse.lsp4j.*;
3637
import org.eclipse.lsp4j.services.LanguageServer;
3738
import org.jetbrains.annotations.NotNull;
@@ -47,8 +48,6 @@
4748
import java.util.concurrent.CompletableFuture;
4849
import java.util.stream.Stream;
4950

50-
import static com.redhat.devtools.lsp4ij.features.documentation.MarkdownConverter.toHTML;
51-
5251
/**
5352
* This class provides methods to execute LSP {@link Command} instances.
5453
*/
@@ -105,11 +104,12 @@ public static boolean executeCommand(LSPCommandContext context) {
105104
}
106105
}
107106
if (context.isShowNotificationError()) {
107+
Project project = context.getProject();
108108
String commandId = context.getCommand().getCommand();
109109
var preferredLanguageServer = context.getPreferredLanguageServer();
110110
String content = preferredLanguageServer != null ?
111-
toHTML(LanguageServerBundle.message("lsp.command.error.with.ls.content", commandId, preferredLanguageServer.getServerWrapper().getServerDefinition().getDisplayName())):
112-
toHTML(LanguageServerBundle.message("lsp.command.error.without.ls.content", commandId));
111+
MarkdownConverter.getInstance(project).toHtml(LanguageServerBundle.message("lsp.command.error.with.ls.content", commandId, preferredLanguageServer.getServerWrapper().getServerDefinition().getDisplayName())):
112+
MarkdownConverter.getInstance(project).toHtml(LanguageServerBundle.message("lsp.command.error.without.ls.content", commandId));
113113

114114
Notification notification = new Notification(LSPNotificationConstants.LSP4IJ_GENERAL_NOTIFICATIONS_ID,
115115
LanguageServerBundle.message("lsp.command.error.title", commandId),

src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionProposal.java

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
import java.util.concurrent.ExecutionException;
5050

5151
import static com.redhat.devtools.lsp4ij.features.completion.snippet.LspSnippetVariableConstants.*;
52-
import static com.redhat.devtools.lsp4ij.features.documentation.LSPDocumentationHelper.convertToHTML;
52+
import static com.redhat.devtools.lsp4ij.features.documentation.LSPDocumentationHelper.convertToHtml;
53+
import static com.redhat.devtools.lsp4ij.features.documentation.LSPDocumentationHelper.getValidMarkupContents;
5354
import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.isDoneNormally;
5455
import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.waitUntilDone;
5556
import static com.redhat.devtools.lsp4ij.ui.IconMapper.getIcon;
@@ -584,16 +585,22 @@ private static Integer getPrefixStartOffsetWhichMatchesLeftContent(@NotNull Docu
584585
public DocumentationResult computeDocumentation() {
585586
var documentation = item.getDocumentation();
586587
if (documentation != null) {
587-
MarkupContent content = getDocumentation(documentation);
588-
return DocumentationResult.documentation(convertToHTML(content, editor));
588+
var contents = getValidMarkupContents(item);
589+
if (contents.isEmpty()) {
590+
return null;
591+
}
592+
return DocumentationResult.documentation(convertToHtml(contents, file));
589593
} else if (supportResolveCompletion) {
590594
if (resolvedCompletionItemFuture != null && resolvedCompletionItemFuture.isDone()) {
591595
CompletionItem resolved = getResolvedCompletionItem();
592596
if (resolved != null) {
593597
item.setDocumentation(resolved.getDocumentation());
594598
}
595-
MarkupContent content = getDocumentation(documentation);
596-
return DocumentationResult.documentation(convertToHTML(content, editor));
599+
var contents = getValidMarkupContents(item);
600+
if (contents.isEmpty()) {
601+
return null;
602+
}
603+
return DocumentationResult.documentation(convertToHtml(contents, file));
597604
} else {
598605
DocumentationResult.asyncDocumentation(() -> {
599606
// The LSP completion item 'documentation' is not filled, try to resolve it
@@ -602,26 +609,17 @@ public DocumentationResult computeDocumentation() {
602609
if (resolved != null) {
603610
item.setDocumentation(resolved.getDocumentation());
604611
}
605-
MarkupContent content = getDocumentation(documentation);
606-
return DocumentationResult.documentation(convertToHTML(content, editor));
612+
var contents = getValidMarkupContents(item);
613+
if (contents.isEmpty()) {
614+
return null;
615+
}
616+
return DocumentationResult.documentation(convertToHtml(contents, file));
607617
});
608618
}
609619
}
610620
return null;
611621
}
612622

613-
private static MarkupContent getDocumentation(Either<String, MarkupContent> documentation) {
614-
if (documentation == null) {
615-
return null;
616-
}
617-
if (documentation.isLeft()) {
618-
String content = documentation.getLeft();
619-
return new MarkupContent(MarkupKind.PLAINTEXT, content);
620-
}
621-
return documentation.getRight();
622-
}
623-
624-
625623
@NotNull
626624
@Override
627625
public TargetPresentation computePresentation() {

0 commit comments

Comments
 (0)