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

perf: Simplify access to client config information (fixes #912) #925

Merged
merged 9 commits into from
Mar 24, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,19 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Predicate;

/**
Expand Down Expand Up @@ -208,6 +219,33 @@ public boolean hasAny(@NotNull PsiFile file,
return false;
}

/**
* Applies the provided processor to all language servers for the specified file.
*
* @param file the file
* @param processor the processor
*/
public void processLanguageServers(@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 allows the caller to apply some logic to all started language servers.

@NotNull Consumer<LanguageServerWrapper> processor) {
var startedServers = getStartedServers();
if (startedServers.isEmpty()) {
return;
}

MatchedLanguageServerDefinitions mappings = getMatchedLanguageServerDefinitions(file, true);
if (mappings == MatchedLanguageServerDefinitions.NO_MATCH) {
return;
}

Set<LanguageServerDefinition> matchedServerDefinitions = mappings.getMatched();
for (var startedServer : startedServers) {
if (ServerStatus.started.equals(startedServer.getServerStatus()) &&
matchedServerDefinitions.contains(startedServer.getServerDefinition())) {
processor.accept(startedServer);
}
}
}

@NotNull
public CompletableFuture<@NotNull List<LanguageServerItem>> getLanguageServers(@Nullable Predicate<LSPClientFeatures> beforeStartingServerFilter,
@Nullable Predicate<LSPClientFeatures> afterStartingServerFilter) {
Expand Down Expand Up @@ -259,7 +297,7 @@ public boolean hasAny(@NotNull PsiFile file,
@Nullable LanguageServerDefinition matchServerDefinition) {
// Collect started (or not) language servers which matches the given file.
CompletableFuture<Collection<LanguageServerWrapper>> matchedServers = getMatchedLanguageServersWrappers(psiFile, matchServerDefinition, beforeStartingServerFilter);
var matchedServersNow= matchedServers.getNow(Collections.emptyList());
var matchedServersNow = matchedServers.getNow(Collections.emptyList());
if (matchedServers.isDone() && matchedServersNow.isEmpty()) {
// None language servers matches the given file
return CompletableFuture.completedFuture(Collections.emptyList());
Expand Down Expand Up @@ -330,7 +368,7 @@ private static Map<LanguageServerWrapper, LanguageServerWrapper.LSPFileConnectio
if (uri != null && !ls.isConnectedTo(uri)) {
// The file is not connected to the current language server
// Get the required information for the didOpen (text and languageId) which requires a ReadAction.
if(document != null) {
if (document != null) {
if (connectionFileInfo == null) {
connectionFileInfo = new HashMap<>();
}
Expand Down Expand Up @@ -397,7 +435,7 @@ private CompletableFuture<Collection<LanguageServerWrapper>> getMatchedLanguageS
/**
* Get or create a language server wrapper for the given server definitions and add then to the given matched servers.
*
* @param psiFile the file.
* @param psiFile the file.
* @param serverDefinitions the server definitions.
* @param matchedServers the list to update with get/created language server.
* @param beforeStartingServerFilter
Expand Down Expand Up @@ -470,7 +508,7 @@ public CompletableFuture<Set<LanguageServerDefinition>> getAsyncMatched() {
/**
* Returns the matched language server definitions for the given file.
*
* @param psiFile the file.
* @param psiFile the file.
* @param ignoreMatch true if {@link DocumentMatcher} must be ignored when mapping matches the given file and false otherwise.
* @return the matched language server definitions for the given file.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,32 @@
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.util.containers.ContainerUtil;
import com.redhat.devtools.lsp4ij.LSPIJEditorUtils;
import com.redhat.devtools.lsp4ij.LanguageServerItem;
import com.redhat.devtools.lsp4ij.LanguageServiceAccessor;
import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures;
import com.redhat.devtools.lsp4ij.client.features.LSPFormattingFeature;
import com.redhat.devtools.lsp4ij.client.features.LSPFormattingFeature.FormattingScope;
import com.redhat.devtools.lsp4ij.client.features.LSPOnTypeFormattingFeature;
import com.redhat.devtools.lsp4ij.client.indexing.ProjectIndexingManager;
import com.redhat.devtools.lsp4ij.features.codeBlockProvider.LSPCodeBlockProvider;
import com.redhat.devtools.lsp4ij.features.completion.LSPCompletionTriggerTypedHandler;
import com.redhat.devtools.lsp4ij.features.selectionRange.LSPSelectionRangeSupport;
import com.redhat.devtools.lsp4ij.server.definition.launching.ClientConfigurationSettings.ClientSideOnTypeFormattingSettings;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.intellij.codeInsight.editorActions.ExtendWordSelectionHandlerBase.expandToWholeLinesWithBlanks;
import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.isDoneNormally;
import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.waitUntilDone;

/**
* Typed handler for LSP4IJ-managed files that performs automatic on-type formatting for specific keystrokes.
Expand All @@ -50,29 +53,60 @@ public Result charTyped(char charTyped,
@NotNull Project project,
@NotNull Editor editor,
@NotNull PsiFile file) {
LSPFormattingFeature formattingFeature = getFormattingFeature(file);
if (formattingFeature != null) {
boolean rangeFormattingSupported = formattingFeature.isRangeFormattingSupported(file);
if (!ProjectIndexingManager.isIndexing(project)) {
// Gather all of the relevant client configuration
Ref<Boolean> rangeFormattingSupportedRef = Ref.create(false);
ClientSideOnTypeFormattingSettings onTypeFormattingSettings = new ClientSideOnTypeFormattingSettings();
LanguageServiceAccessor.getInstance(project).processLanguageServers(
file,
ls -> {
// Only include servers that support formatting and don't support server-side on-type formatting
LSPClientFeatures clientFeatures = ls.getClientFeatures();
LSPFormattingFeature formattingFeature = clientFeatures.getFormattingFeature();
LSPOnTypeFormattingFeature onTypeFormattingFeature = clientFeatures.getOnTypeFormattingFeature();
if (formattingFeature.isEnabled(file) && formattingFeature.isSupported(file) &&
(!onTypeFormattingFeature.isEnabled(file) || !onTypeFormattingFeature.isSupported(file))) {
rangeFormattingSupportedRef.set(rangeFormattingSupportedRef.get() || formattingFeature.isRangeFormattingSupported(file));

onTypeFormattingSettings.formatOnCloseBrace |= formattingFeature.isFormatOnCloseBrace(file);
FormattingScope formatOnCloseBraceScope = formattingFeature.getFormatOnCloseBraceScope(file);
if (formatOnCloseBraceScope.compareTo(onTypeFormattingSettings.formatOnCloseBraceScope) > 0) {
onTypeFormattingSettings.formatOnCloseBraceScope = formatOnCloseBraceScope;
}
onTypeFormattingSettings.formatOnCloseBraceCharacters += formattingFeature.getFormatOnCloseBraceCharacters(file);

onTypeFormattingSettings.formatOnStatementTerminator |= formattingFeature.isFormatOnStatementTerminator(file);
FormattingScope formatOnStatementTerminatorScope = formattingFeature.getFormatOnStatementTerminatorScope(file);
if (formatOnStatementTerminatorScope.compareTo(onTypeFormattingSettings.formatOnStatementTerminatorScope) > 0) {
onTypeFormattingSettings.formatOnStatementTerminatorScope = formatOnStatementTerminatorScope;
}
onTypeFormattingSettings.formatOnStatementTerminatorCharacters += formattingFeature.getFormatOnStatementTerminatorCharacters(file);

onTypeFormattingSettings.formatOnCompletionTrigger |= formattingFeature.isFormatOnCompletionTrigger(file);
onTypeFormattingSettings.formatOnCompletionTriggerCharacters += formattingFeature.getFormatOnCompletionTriggerCharacters(file);
}
}
);
boolean rangeFormattingSupported = rangeFormattingSupportedRef.get();

// Close braces
if (formattingFeature.isFormatOnCloseBrace(file) &&
// Make sure the formatter supports formatting of the configured scope
((formattingFeature.getFormatOnCloseBraceScope(file) == FormattingScope.FILE) || rangeFormattingSupported)) {
if (onTypeFormattingSettings.formatOnCloseBrace &&
// Make sure the formatter supports formatting of the configured scope
((onTypeFormattingSettings.formatOnCloseBraceScope == FormattingScope.FILE) || rangeFormattingSupported)) {
Map.Entry<Character, Character> bracePair = ContainerUtil.find(
LSPIJEditorUtils.getBracePairs(file).entrySet(),
entry -> entry.getValue() == charTyped
);
if (bracePair != null) {
String formatOnCloseBraceCharacters = formattingFeature.getFormatOnCloseBraceCharacters(file);
Character openBraceChar = bracePair.getKey();
Character closeBraceChar = bracePair.getValue();
if (StringUtil.isEmpty(formatOnCloseBraceCharacters) ||
(formatOnCloseBraceCharacters.indexOf(closeBraceChar) > -1)) {
if (StringUtil.isEmpty(onTypeFormattingSettings.formatOnCloseBraceCharacters) ||
(onTypeFormattingSettings.formatOnCloseBraceCharacters.indexOf(closeBraceChar) > -1)) {
return handleCloseBraceTyped(
project,
editor,
file,
formattingFeature,
onTypeFormattingSettings.formatOnCloseBraceScope,
openBraceChar,
closeBraceChar
);
Expand All @@ -81,32 +115,30 @@ public Result charTyped(char charTyped,
}

// Statement terminators
if (formattingFeature.isFormatOnStatementTerminator(file) &&
// Make sure the formatter supports formatting of the configured scope
((formattingFeature.getFormatOnStatementTerminatorScope(file) == FormattingScope.FILE) || rangeFormattingSupported)) {
String formatOnStatementTerminatorCharacters = formattingFeature.getFormatOnStatementTerminatorCharacters(file);
if (StringUtil.isNotEmpty(formatOnStatementTerminatorCharacters) &&
(formatOnStatementTerminatorCharacters.indexOf(charTyped) > -1)) {
if (onTypeFormattingSettings.formatOnStatementTerminator &&
// Make sure the formatter supports formatting of the configured scope
((onTypeFormattingSettings.formatOnStatementTerminatorScope == FormattingScope.FILE) || rangeFormattingSupported)) {
if (StringUtil.isNotEmpty(onTypeFormattingSettings.formatOnStatementTerminatorCharacters) &&
(onTypeFormattingSettings.formatOnStatementTerminatorCharacters.indexOf(charTyped) > -1)) {
return handleStatementTerminatorTyped(
project,
editor,
file,
formattingFeature,
onTypeFormattingSettings.formatOnStatementTerminatorScope,
charTyped
);
}
}

// Completion triggers
if (formattingFeature.isFormatOnCompletionTrigger(file) &&
// Make sure the formatter supports range formatting
rangeFormattingSupported &&
// It must be a completion trigger character for the language no matter what
LSPCompletionTriggerTypedHandler.hasLanguageServerSupportingCompletionTriggerCharacters(charTyped, project, file)) {
if (onTypeFormattingSettings.formatOnCompletionTrigger &&
// Make sure the formatter supports range formatting
rangeFormattingSupported &&
// It must be a completion trigger character for the language no matter what
LSPCompletionTriggerTypedHandler.hasLanguageServerSupportingCompletionTriggerCharacters(charTyped, project, file)) {
// But the subset that should trigger completion can be configured
String formatOnCompletionTriggerCharacters = formattingFeature.getFormatOnCompletionTriggerCharacters(file);
if (StringUtil.isEmpty(formatOnCompletionTriggerCharacters) ||
(formatOnCompletionTriggerCharacters.indexOf(charTyped) > -1)) {
if (StringUtil.isEmpty(onTypeFormattingSettings.formatOnCompletionTriggerCharacters) ||
(onTypeFormattingSettings.formatOnCompletionTriggerCharacters.indexOf(charTyped) > -1)) {
return handleCompletionTriggerTyped(
project,
editor,
Expand All @@ -119,51 +151,16 @@ public Result charTyped(char charTyped,
return super.charTyped(charTyped, project, editor, file);
}

@Nullable
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 we can remove this.

private static LSPFormattingFeature getFormattingFeature(@NotNull PsiFile file) {
if (ProjectIndexingManager.isIndexingAll()) {
// while indexing, we do nothing
return null;
}
if (!hasLanguageServerSupportingOnlyFormatting(file)) {
// The file is not associated to a language server which supports only formatting
return null;
}
Project project = file.getProject();
CompletableFuture<@NotNull List<LanguageServerItem>> future = LanguageServiceAccessor.getInstance(project)
.getLanguageServers(file,
// Client-side on-type formatting shouldn't trigger a language server to start
f -> f.getFormattingFeature().isEnabled(file),
f -> f.getFormattingFeature().isFormattingSupported(file)
);

// Wait until the future while 500ms and stop the wait if there are some ProcessCanceledException.
try {
waitUntilDone(future, file, 500);
} catch (Exception e) {
return null;
}
if (!isDoneNormally(future)) {
return null;
}

// Just return the first matching language server, if any
List<LanguageServerItem> languageServers = future.getNow(Collections.emptyList());
LanguageServerItem languageServer = ContainerUtil.getFirstItem(languageServers);
return languageServer != null ? languageServer.getClientFeatures().getFormattingFeature() : null;
}

@NotNull
private static Result handleCloseBraceTyped(@NotNull Project project,
@NotNull Editor editor,
@NotNull PsiFile file,
@NotNull LSPFormattingFeature formattingFeature,
@NotNull FormattingScope formattingScope,
char openBraceChar,
char closeBraceChar) {
TextRange formatTextRange = null;

// Statement-level scope is not supported for code blocks
FormattingScope formattingScope = formattingFeature.getFormatOnCloseBraceScope(file);
if (formattingScope == FormattingScope.STATEMENT) {
return Result.CONTINUE;
}
Expand Down Expand Up @@ -218,15 +215,14 @@ else if (formattingScope == FormattingScope.FILE) {
private static Result handleStatementTerminatorTyped(@NotNull Project project,
@NotNull Editor editor,
@NotNull PsiFile file,
@NotNull LSPFormattingFeature formattingFeature,
@NotNull FormattingScope formattingScope,
char statementTerminatorChar) {
TextRange formatTextRange = null;

int offset = editor.getCaretModel().getOffset();
int beforeOffset = offset - 1;

// If appropriate, find the statement that was just terminated
FormattingScope formattingScope = formattingFeature.getFormatOnStatementTerminatorScope(file);
if (formattingScope == FormattingScope.STATEMENT) {
List<TextRange> selectionTextRanges = LSPSelectionRangeSupport.getSelectionTextRanges(file, editor, beforeOffset);
if (!ContainerUtil.isEmpty(selectionTextRanges)) {
Expand Down Expand Up @@ -329,15 +325,4 @@ private static void format(@NotNull Project project,
CodeStyleManager.getInstance(project).reformatText(file, Collections.singletonList(textRange));
}
}

private static boolean hasLanguageServerSupportingOnlyFormatting(@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.

And we can remove this.

return LanguageServiceAccessor.getInstance(file.getProject())
.hasAny(file, ls -> {
var clientFeatures = ls.getClientFeatures();
return clientFeatures.getFormattingFeature().isEnabled(file) &&
clientFeatures.getFormattingFeature().isSupported(file) &&
(!clientFeatures.getOnTypeFormattingFeature().isEnabled(file) ||
!clientFeatures.getOnTypeFormattingFeature().isSupported(file));
});
}
}
Loading
Loading