Skip to content

Commit

Permalink
Merge branch 'redhat-developer:main' into lsp_code_block_improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
SCWells72 authored Jan 10, 2025
2 parents a912033 + 562d822 commit 8503f91
Show file tree
Hide file tree
Showing 34 changed files with 606 additions and 62 deletions.
1 change: 1 addition & 0 deletions docs/LSPApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ public class MyLSPCodeLensFeature extends LSPCodeLensFeature {
| boolean isStrikeout(CompletionItem item) | Returns true if the IntelliJ lookup is strike out and false otherwise. | use `item.getDeprecated()` or `item.getTags().contains(CompletionItemTag.Deprecated)` |
| String getTailText(CompletionItem item) | Returns the IntelliJ lookup tail text from the given LSP completion item and null otherwise. | `item.getLabelDetails().getDetail()` |
| boolean isItemTextBold(CompletionItem item) | Returns the IntelliJ lookup item text bold from the given LSP completion item and null otherwise. | `item.getKind() == CompletionItemKind.Keyword` |
| boolean useContextAwareSorting(PsiFile file) | Returns `true` if client-side context-aware completion sorting should be used for the specified file and `false` otherwise. | `false` |
|

## LSP Declaration Feature
Expand Down
1 change: 1 addition & 0 deletions docs/UserDefinedLanguageServer.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ pre-filled with server name, command, mappings and potential configuration.
* [Scala Language Server (Metals)](./user-defined-ls/metals.md)
* [SourceKit-LSP](./user-defined-ls/sourcekit-lsp.md)
* [TypeScript Language Server](./user-defined-ls/typescript-language-server.md)
* [Vue Language Server](./user-defined-ls/vue-js-language-server.md)

If the template directory contains a `README.md` file, you can open the instructions by pressing the help icon.

Expand Down
25 changes: 25 additions & 0 deletions docs/user-defined-ls/vue-js-language-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
You can use [Vue Language Server](https://github.com/vuejs/language-tools/tree/master/packages/language-server) by following these instructions:
* [Install Node.js](https://nodejs.org/en/download)
* [npm install -g @vue/language-server](https://www.npmjs.com/package/@volar/vue-language-server)
* [npm install -g typescript](https://www.npmjs.com/package/typescript)

Configure, example on windows:
* Server > Command > `vue-language-server.cmd --stdio` OR `node "C:/Users/${user.name}/AppData/Roaming/npm/node_modules/@vue/language-server/bin/vue-language-server.js" --stdio`
* Mappings > File name patterns > press "+" > File name patterns like "`*.vue`" and language id like "`vue`"
* Configuration: `{}`
* Initialization Options, need set `${user.name}`:
``` JSON
{
"typescript": {
"tsdk": "C:/Users/${user.name}/AppData/Roaming/npm/node_modules/typescript/lib"
},
"vue": {
"hybridMode": false
}
}
```

You can use [Vue highlight](https://github.com/vuejs/vue-syntax-highlight) in the TextMate by following these instructions:
* Download and unzip in a folder
* File -> Settings -> Editor -> TextMate Bundles
* And press "+" `Select Path` to folder with highlight
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public boolean isCompletionSupported(@NotNull PsiFile file) {
* @param file the file.
* @return true the file associated with a language server can support resolve completion and false otherwise.
*/
public boolean isResolveCompletionSupported(@Nullable PsiFile file) {
public boolean isResolveCompletionSupported(@NotNull PsiFile file) {
return getCompletionCapabilityRegistry().isResolveCompletionSupported(file);
}

Expand Down Expand Up @@ -165,7 +165,8 @@ public String getItemText(@NotNull CompletionItem item) {
*/
@Nullable
public String getTypeText(CompletionItem item) {
return item.getDetail();
var labelDetails = item.getLabelDetails();
return labelDetails != null ? labelDetails.getDescription() : item.getDetail();
}

/**
Expand All @@ -182,7 +183,7 @@ public Icon getIcon(@NotNull CompletionItem item) {
/**
* Returns true if the IntelliJ lookup is strike out and false otherwise.
*
* @param item
* @param item the completion item
* @return true if the IntelliJ lookup is strike out and false otherwise.
*/
public boolean isStrikeout(@NotNull CompletionItem item) {
Expand Down Expand Up @@ -215,12 +216,12 @@ public boolean isItemTextBold(@NotNull CompletionItem item) {
/**
* Don't override this method, we need to revisit the API and the prefix computation (to customize it).
*
* @param context
* @param completionPrefix
* @param result
* @param lookupItem
* @param priority
* @param item
* @param context the completion context
* @param completionPrefix the completion prefix
* @param result the completion result set
* @param lookupItem the lookup item
* @param priority the completion priority
* @param item the completion item
*/
@ApiStatus.Internal
public void addLookupItem(@NotNull LSPCompletionContext context,
Expand Down Expand Up @@ -248,12 +249,13 @@ public void addLookupItem(@NotNull LSPCompletionContext context,
.addElement(prioritizedLookupItem);
}
} else {
// Should happen rarely, only when text edit is for multi-lines or if completion is triggered outside the text edit range.
// Add the IJ completion item (lookup item) which will use the IJ prefix respecting the language's case-sensitivity
// Add the IJ completion item (lookup item) by using the prefix matcher respecting the language's case-sensitivity
if (caseSensitive) {
result.addElement(prioritizedLookupItem);
result.withPrefixMatcher(result.getPrefixMatcher())
.addElement(prioritizedLookupItem);
} else {
result.caseInsensitive()
result.withPrefixMatcher(result.getPrefixMatcher())
.caseInsensitive()
.addElement(prioritizedLookupItem);
}
}
Expand Down Expand Up @@ -293,4 +295,15 @@ public void setServerCapabilities(@Nullable ServerCapabilities serverCapabilitie
completionCapabilityRegistry.setServerCapabilities(serverCapabilities);
}
}

/**
* Determines whether or not client-side context-aware completion sorting should be used for the specified file.
*
* @param file the file
* @return true if client-side context-aware completion sorting should be used; otherwise false
*/
public boolean useContextAwareSorting(@NotNull PsiFile file) {
// Default to disabled
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ public abstract class AbstractLSPFeatureSupport<Params, Result> {
* Returns the (cached or not) LSP requests for all language servers applying to a given Psi file or project.
*
* @param params the LSP parameters expected to execute LSP requests.
* @return the (cached or not) LSP requests for all language servers applying to a given Psi file or project.
* @return the (cached or not) LSP requests for all language servers applying to a given Psi file or project and null otherwise
* (rare case when the future is loaded and cancel is occurred in the same time which set the future to null).
*/
@Nullable
public CompletableFuture<Result> getFeatureData(Params params) {
if (!isValidLSPFuture()) {
// - the LSP requests have never been executed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ protected void buildRoot(@NotNull HierarchyNodeDescriptor descriptor,
}

protected void fillChildren(@NotNull HierarchyNodeDescriptor descriptor,
@NotNull CompletableFuture<List<CallHierarchyItemData>> callHierarchyFuture,
@Nullable CompletableFuture<List<CallHierarchyItemData>> callHierarchyFuture,
@NotNull List<LSPHierarchyNodeDescriptor> descriptors) {
if (isDoneNormally(callHierarchyFuture)) {
List<CallHierarchyItemData> items = callHierarchyFuture.getNow(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,30 @@
******************************************************************************/
package com.redhat.devtools.lsp4ij.features.completion;

import java.util.Comparator;

import com.intellij.codeInsight.completion.PrefixMatcher;
import com.intellij.openapi.util.text.StringUtil;
import org.eclipse.lsp4j.CompletionItem;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Comparator;

/**
* Compares {@link CompletionItem}s by their sortText property (falls back to comparing labels)
*/
public class CompletionItemComparator implements Comparator<CompletionItem> {
private final PrefixMatcher prefixMatcher;
private final String currentWord;
private final boolean caseSensitive;

public CompletionItemComparator(@Nullable PrefixMatcher prefixMatcher,
@Nullable String currentWord,
boolean caseSensitive) {
this.prefixMatcher = prefixMatcher;
this.currentWord = currentWord;
this.caseSensitive = caseSensitive;
}

@Override
public int compare(CompletionItem item1, CompletionItem item2) {
if (item1 == item2) {
Expand All @@ -29,24 +44,104 @@ public int compare(CompletionItem item1, CompletionItem item2) {
return 1;
}

int comparison = compareNullable(item1.getSortText(), item2.getSortText());
// If one is a better match for the current word than the other, sort it higher
int comparison = compareAgainstCurrentWord(item1, item2);
if (comparison != 0) {
return comparison;
}

// If one is a better completion for the current prefix than the other, sort it higher
comparison = compareAgainstPrefix(item1, item2);
if (comparison != 0) {
return comparison;
}

// Order by language server-provided sort text
comparison = compare(item1.getSortText(), item2.getSortText());
if (comparison != 0) {
return comparison;
}

// If sortText is equal, fall back to comparing labels
if (comparison == 0) {
comparison = item1.getLabel().compareTo(item2.getLabel());
return compare(item1.getLabel(), item2.getLabel());
}

private int compare(@Nullable String string1, @Nullable String string2) {
return StringUtil.compare(string1, string2, !caseSensitive);
}

private boolean equals(@Nullable String string1, @Nullable String string2) {
return StringUtil.compare(string1, string2, !caseSensitive) == 0;
}

private boolean startsWith(@Nullable String string, @Nullable String prefix) {
if ((string == null) || (prefix == null)) {
return false;
}
return caseSensitive ? StringUtil.startsWith(string, prefix) : StringUtil.startsWithIgnoreCase(string, prefix);
}

private int compareAgainstCurrentWord(@NotNull CompletionItem item1, @NotNull CompletionItem item2) {
if (currentWord != null) {
String label1 = item1.getLabel();
String label2 = item2.getLabel();
// Don't do this for completion offerings that are quoted strings
if (((label1 == null) || !StringUtil.isQuotedString(label1)) &&
((label2 == null) || !StringUtil.isQuotedString(label2))) {
// Exact match
if (equals(currentWord, label1) &&
((label2 == null) || !equals(currentWord, label2))) {
return -1;
} else if (equals(currentWord, label2) &&
((label1 == null) || !equals(currentWord, label1))) {
return 1;
}

return comparison;
// Starts with
else if ((startsWith(currentWord, label1) || startsWith(label1, currentWord)) &&
((label2 == null) || !(startsWith(currentWord, label2) || startsWith(label2, currentWord)))) {
return -1;
} else if ((startsWith(currentWord, label2) || startsWith(label2, currentWord)) &&
((label1 == null) || !(startsWith(currentWord, label1) || startsWith(label1, currentWord)))) {
return 1;
}
}
}

return 0;
}

private int compareNullable(@Nullable String s1, @Nullable String s2) {
if (s1 == s2) {
return 0;
} else if (s1 == null) {
return -1;
} else if (s2 == null) {
return 1;
private int compareAgainstPrefix(@NotNull CompletionItem item1, @NotNull CompletionItem item2) {
if (prefixMatcher != null) {
String prefix = prefixMatcher.getPrefix();
String label1 = item1.getLabel();
String label2 = item2.getLabel();
// Don't do this for completion offerings that are quoted strings
if (((label1 == null) || !StringUtil.isQuotedString(label1)) &&
((label2 == null) || !StringUtil.isQuotedString(label2))) {
// Start starts with
if (startsWith(label1, prefix) &&
(!startsWith(label2, prefix))) {
return -1;
} else if (startsWith(label2, prefix) &&
(!startsWith(label1, prefix))) {
return 1;
}
// Loose/camel-hump starts with
else if ((label1 != null) && prefixMatcher.isStartMatch(label1) &&
((label2 == null) || !prefixMatcher.isStartMatch(label2))) {
return -1;
} else if ((label2 != null) && prefixMatcher.isStartMatch(label2) &&
((label1 == null) || !prefixMatcher.isStartMatch(label1))) {
return 1;
} else if ((label1 != null) && prefixMatcher.isStartMatch(label1) &&
((label2 != null) && prefixMatcher.isStartMatch(label2))) {
// Better matches are ranked higher and we want those ordered earlier
return prefixMatcher.matchingDegree(label2) - prefixMatcher.matchingDegree(label1);
}
}
}
return s1.compareTo(s2);

return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.UserDataHolder;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.patterns.StandardPatterns;
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.LanguageServerItem;
import com.redhat.devtools.lsp4ij.client.ExecuteLSPFeatureStatus;
import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures;
import com.redhat.devtools.lsp4ij.client.features.LSPCompletionFeature;
import com.redhat.devtools.lsp4ij.client.features.LSPCompletionProposal;
import com.redhat.devtools.lsp4ij.client.indexing.ProjectIndexingManager;
Expand Down Expand Up @@ -102,8 +105,6 @@ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @No
}
}

private static final CompletionItemComparator completionProposalComparator = new CompletionItemComparator();

private void addCompletionItems(@NotNull CompletionParameters parameters,
@NotNull CompletionPrefix completionPrefix,
@NotNull Either<List<CompletionItem>, CompletionList> completion,
Expand All @@ -119,12 +120,23 @@ private void addCompletionItems(@NotNull CompletionParameters parameters,
items.addAll(completionList.getItems());
}

// Sort by item.sortText
items.sort(completionProposalComparator);
PsiFile originalFile = parameters.getOriginalFile();
LSPClientFeatures clientFeatures = languageServer.getClientFeatures();

// Sort the completions as appropriate based on client configuration
boolean useContextAwareSorting = clientFeatures.getCompletionFeature().useContextAwareSorting(originalFile);
if (useContextAwareSorting) {
// Cache-buster for prefix changes since that can affect ordering
result.restartCompletionOnPrefixChange(StandardPatterns.string().longerThan(0));
}
PrefixMatcher prefixMatcher = useContextAwareSorting ? result.getPrefixMatcher() : null;
String currentWord = useContextAwareSorting ? getCurrentWord(parameters) : null;
boolean caseSensitive = clientFeatures.isCaseSensitive(originalFile);
items.sort(new CompletionItemComparator(prefixMatcher, currentWord, caseSensitive));
int size = items.size();

Set<String> addedLookupStrings = new HashSet<>();
var completionFeature = languageServer.getClientFeatures().getCompletionFeature();
var completionFeature = clientFeatures.getCompletionFeature();
LSPCompletionFeature.LSPCompletionContext context = new LSPCompletionFeature.LSPCompletionContext(parameters, languageServer);
// Items now sorted by priority, low index == high priority
for (int i = 0; i < size; i++) {
Expand Down Expand Up @@ -186,6 +198,23 @@ private void addCompletionItems(@NotNull CompletionParameters parameters,
}
}

@Nullable
private static String getCurrentWord(@NotNull CompletionParameters parameters) {
PsiFile originalFile = parameters.getOriginalFile();
VirtualFile virtualFile = originalFile.getVirtualFile();
Document document = virtualFile != null ? LSPIJUtils.getDocument(virtualFile) : null;
if (document != null) {
int offset = parameters.getOffset();
TextRange wordTextRange = LSPIJUtils.getWordRangeAt(document, originalFile, offset);
if (wordTextRange != null) {
CharSequence documentChars = document.getCharsSequence();
CharSequence wordChars = documentChars.subSequence(wordTextRange.getStartOffset(), wordTextRange.getEndOffset());
return wordChars.toString();
}
}
return null;
}

protected void updateWithItemDefaults(@NotNull CompletionItem item,
@Nullable CompletionItemDefaults itemDefaults) {
if (itemDefaults == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public void format(@NotNull Document document,
return;
}
try {
List<? extends TextEdit> edits = formatFuture.getNow(null);
List<? extends TextEdit> edits = formatFuture != null ? formatFuture.getNow(null) : null;
String formatted = edits != null ? applyEdits(editor.getDocument(), edits) : formattingRequest.getDocumentText();
formattingRequest.onTextReady(formatted);
} catch (Exception e) {
Expand Down
Loading

0 comments on commit 8503f91

Please sign in to comment.