diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java b/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java
index c78bfc641..a2e706d77 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java
@@ -874,13 +874,38 @@ public static TextRange getWordRangeAt(@NotNull Document document,
public static TextRange getWordRangeAt(@NotNull Document document,
@Nullable PsiFile file,
int offset) {
+ return getWordRangeAt(document, file, offset, false);
+ }
+
+ /**
+ * Returns the word range from the document at given offset and null otherwise.
+ *
+ *
+ * - fo|o bar -> [foo]
+ * - fo|o.bar() -> [foo]
+ * - foo.b|ar() -> [bar]
+ * - foo.bar(|) -> null
+ * - foo | bar -> null
+ *
+ *
+ * @param document the document.
+ * @param file the PsiFile or null otherwise.
+ * @param offset the offset.
+ * @param excludeWhitespace if {@link PsiWhiteSpace whitespace} should not be returned
+ * @return the word range from the document at given offset and null otherwise.
+ */
+ @Nullable
+ public static TextRange getWordRangeAt(@NotNull Document document,
+ @Nullable PsiFile file,
+ int offset,
+ boolean excludeWhitespace) {
if (offset > document.getTextLength()) {
offset = document.getTextLength() - 1;
}
if (file != null && !SimpleLanguageUtils.isSupported(file.getLanguage())) {
// It is not TextMate, TEXT file (since those language doesn't tokenize the file)
// Try to use the PsiElement text range found at the given offset
- TextRange textRange = findBestTextRangeAt(file, offset);
+ TextRange textRange = findBestTextRangeAt(file, offset, excludeWhitespace);
if (textRange != null) {
return textRange;
}
@@ -896,11 +921,11 @@ public static TextRange getWordRangeAt(@NotNull Document document,
return (start < end) ? new TextRange(start, end) : null;
}
- private static TextRange findBestTextRangeAt(@Nullable PsiFile file, int offset) {
+ private static TextRange findBestTextRangeAt(@Nullable PsiFile file, int offset, boolean excludeWhitespace) {
PsiElement element = file != null ? file.findElementAt(Math.max(offset - 1, 0)) : null;
if (element != null) {
- // When element is whitespace, completion should never replace it - that's not prefix
- if (element instanceof PsiWhiteSpace) {
+ // When element is whitespace - exclude it if we need
+ if (excludeWhitespace && element instanceof PsiWhiteSpace) {
return null;
}
TextRange textRange = element.getTextRange();
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionFeature.java
index 8a8745f67..14d719201 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionFeature.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionFeature.java
@@ -335,4 +335,22 @@ public boolean isMultipleCaretsSupported(@NotNull PsiFile file) {
// Default to enabled
return true;
}
+
+ /**
+ * Whether to exclude {@link com.intellij.psi.PsiWhiteSpace whitespace token} from completion prefix.
+ *
+ *
+ * When disabled (the default), invoking a completion inside whitespace token
+ * will make this token a completion prefix, which means it will be replaced by {@code insertText}
+ * when user accept completion.
+ * When disabled, whitespace token will not be prefix, so when user accept completion,
+ * it will be just inserted at the caret, leaving whitespace as-is.
+ *
+ * @param file the file
+ * @return {@code true} if {@link com.intellij.psi.PsiWhiteSpace whitespace token} should not be completion prefix
+ */
+ public boolean excludeWhitespaceFromCompletionPrefix(@NotNull PsiFile file) {
+ // Disabled by default to leave original behavior
+ return false;
+ }
}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionProposal.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionProposal.java
index 232af119e..f12212aae 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionProposal.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionProposal.java
@@ -102,7 +102,11 @@ public LSPCompletionProposal(@NotNull CompletionItem item,
this.editor = completionContext.getParameters().getEditor();
this.completionContext = completionContext;
this.completionOffset = completionContext.getParameters().getOffset();
- this.prefixStartOffset = getPrefixStartOffset(editor.getDocument(), completionOffset);
+ this.prefixStartOffset = getPrefixStartOffset(
+ editor.getDocument(),
+ completionOffset,
+ completionFeature.excludeWhitespaceFromCompletionPrefix(completionContext.getParameters().getOriginalFile())
+ );
this.completionFeature = completionFeature;
putUserData(CodeCompletionHandlerBase.DIRECT_INSERTION, true);
}
@@ -663,7 +667,7 @@ private boolean isSelectedCompletionItem() {
// --------------- Prefix start offset
- private int getPrefixStartOffset(@NotNull Document document, int completionOffset) {
+ private int getPrefixStartOffset(@NotNull Document document, int completionOffset, boolean excludeWhitespace) {
Either textEdit = this.item.getTextEdit();
if (textEdit != null) {
// case 1: text edit is defined,
@@ -673,7 +677,7 @@ private int getPrefixStartOffset(@NotNull Document document, int completionOffse
// case 2: text edit is undefined, try to compute the prefix start offset by using insertText
String insertText = getInsertText();
- Integer prefixStartOffset = computePrefixStartFromInsertText(document, file, completionOffset, insertText);
+ Integer prefixStartOffset = computePrefixStartFromInsertText(document, file, completionOffset, insertText, excludeWhitespace);
return Objects.requireNonNullElse(prefixStartOffset, completionOffset);
}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/internal/CompletionUtils.java b/src/main/java/com/redhat/devtools/lsp4ij/internal/CompletionUtils.java
index 21aca9705..8e45ccc0e 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/internal/CompletionUtils.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/internal/CompletionUtils.java
@@ -1,68 +1,77 @@
-/*******************************************************************************
- * Copyright (c) 2025 Red Hat, Inc.
- * Distributed under license by Red Hat, Inc. All rights reserved.
- * This program is made available under the terms of the
- * Eclipse Public License v2.0 which accompanies this distribution,
- * and is available at https://www.eclipse.org/legal/epl-v20.html
- *
- * Contributors:
- * Red Hat, Inc. - initial API and implementation
- *****************************************************************************/
-package com.redhat.devtools.lsp4ij.internal;
-
-import com.intellij.openapi.editor.Document;
-import com.intellij.openapi.util.TextRange;
-import com.intellij.psi.PsiFile;
-import com.redhat.devtools.lsp4ij.LSPIJUtils;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * LSP/DAP Completion utilities.
- */
-public class CompletionUtils {
-
- @Nullable
- public static Integer computePrefixStartFromInsertText(@NotNull Document document,
- @Nullable PsiFile file,
- int completionOffset,
- String insertText) {
-
- // case 2.1: first strategy, we collect word range at
- // ex :
- // insertText= '(let [${1:binding} ${2:value}])'
- // document= le
- // we have to return |le as prefix start offset
-
- TextRange wordRange = LSPIJUtils.getWordRangeAt(document, file, completionOffset);
- if (wordRange != null) {
- return wordRange.getStartOffset();
- }
-
- // case 2.2: second strategy, we check if the left content of the completion offset
- // matches the full insertText left content
- // ex :
- // insertText= 'foo.bar'
- // document= {foo.b|}
- // we have to return {| as prefix start offset
-
- return getPrefixStartOffsetWhichMatchesLeftContent(document, completionOffset, insertText);
- }
-
- @Nullable
- private static Integer getPrefixStartOffsetWhichMatchesLeftContent(@NotNull Document document,
- int completionOffset,
- @NotNull String insertText) {
- int startOffset = Math.max(0, completionOffset - insertText.length());
- int endOffset = startOffset + Math.min(insertText.length(), completionOffset);
- String subDoc = document.getText(new TextRange(startOffset, endOffset)); // "".ch
- for (int i = 0; i < insertText.length() && i < completionOffset; i++) {
- String tentativeCommonString = subDoc.substring(i);
- if (insertText.startsWith(tentativeCommonString)) {
- return completionOffset - tentativeCommonString.length();
- }
- }
- return null;
- }
-
-}
+/*******************************************************************************
+ * Copyright (c) 2025 Red Hat, Inc.
+ * Distributed under license by Red Hat, Inc. All rights reserved.
+ * This program is made available under the terms of the
+ * Eclipse Public License v2.0 which accompanies this distribution,
+ * and is available at https://www.eclipse.org/legal/epl-v20.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ *****************************************************************************/
+package com.redhat.devtools.lsp4ij.internal;
+
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiFile;
+import com.redhat.devtools.lsp4ij.LSPIJUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * LSP/DAP Completion utilities.
+ */
+public class CompletionUtils {
+
+ @Nullable
+ public static Integer computePrefixStartFromInsertText(@NotNull Document document,
+ @Nullable PsiFile file,
+ int completionOffset,
+ String insertText) {
+ return computePrefixStartFromInsertText(document, file, completionOffset, insertText, false);
+ }
+
+ @Nullable
+ public static Integer computePrefixStartFromInsertText(@NotNull Document document,
+ @Nullable PsiFile file,
+ int completionOffset,
+ String insertText,
+ boolean excludeWhitespace) {
+
+ // case 2.1: first strategy, we collect word range at
+ // ex :
+ // insertText= '(let [${1:binding} ${2:value}])'
+ // document= le
+ // we have to return |le as prefix start offset
+
+ TextRange wordRange = LSPIJUtils.getWordRangeAt(document, file, completionOffset, excludeWhitespace);
+ if (wordRange != null) {
+ return wordRange.getStartOffset();
+ }
+
+ // case 2.2: second strategy, we check if the left content of the completion offset
+ // matches the full insertText left content
+ // ex :
+ // insertText= 'foo.bar'
+ // document= {foo.b|}
+ // we have to return {| as prefix start offset
+
+ return getPrefixStartOffsetWhichMatchesLeftContent(document, completionOffset, insertText);
+ }
+
+ @Nullable
+ private static Integer getPrefixStartOffsetWhichMatchesLeftContent(@NotNull Document document,
+ int completionOffset,
+ @NotNull String insertText) {
+ int startOffset = Math.max(0, completionOffset - insertText.length());
+ int endOffset = startOffset + Math.min(insertText.length(), completionOffset);
+ String subDoc = document.getText(new TextRange(startOffset, endOffset)); // "".ch
+ for (int i = 0; i < insertText.length() && i < completionOffset; i++) {
+ String tentativeCommonString = subDoc.substring(i);
+ if (insertText.startsWith(tentativeCommonString)) {
+ return completionOffset - tentativeCommonString.length();
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageTargetProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageTargetProvider.java
index 97530cc84..030a5b456 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageTargetProvider.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageTargetProvider.java
@@ -143,7 +143,7 @@ public class LSPUsageTargetProvider implements UsageTargetProvider {
*/
@NotNull
private static UsageTarget[] getLSPTargets(@NotNull Editor editor, @NotNull PsiFile file) {
- TextRange targetTextRange = LSPIJUtils.getWordRangeAt(editor.getDocument(), file, editor.getCaretModel().getOffset());
+ TextRange targetTextRange = LSPIJUtils.getWordRangeAt(editor.getDocument(), file, editor.getCaretModel().getOffset(), false);
if (targetTextRange == null) {
return UsageTarget.EMPTY_ARRAY;
}