diff --git a/clients/eclipse/feature/feature.xml b/clients/eclipse/feature/feature.xml index a7d93cc17569..cb0cd824006f 100644 --- a/clients/eclipse/feature/feature.xml +++ b/clients/eclipse/feature/feature.xml @@ -2,7 +2,7 @@ @@ -19,6 +19,6 @@ + version="0.0.2.26"/> diff --git a/clients/eclipse/plugin/META-INF/MANIFEST.MF b/clients/eclipse/plugin/META-INF/MANIFEST.MF index 606601c995ec..7230fab47635 100644 --- a/clients/eclipse/plugin/META-INF/MANIFEST.MF +++ b/clients/eclipse/plugin/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Tabby Plugin for Eclipse Bundle-SymbolicName: com.tabbyml.tabby4eclipse;singleton:=true -Bundle-Version: 0.0.2.25 +Bundle-Version: 0.0.2.26 Bundle-Activator: com.tabbyml.tabby4eclipse.Activator Bundle-Vendor: com.tabbyml Require-Bundle: org.eclipse.ui, diff --git a/clients/eclipse/plugin/plugin.xml b/clients/eclipse/plugin/plugin.xml index eb2061be7bb2..5cd00f7d994d 100644 --- a/clients/eclipse/plugin/plugin.xml +++ b/clients/eclipse/plugin/plugin.xml @@ -130,6 +130,16 @@ name="Accept Inline Completion" id="com.tabbyml.tabby4eclipse.commands.inlineCompletion.accept"> + + + + + + + + @@ -241,6 +259,15 @@ + + + + + + + + + diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Accept.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Accept.java index 490db7a512e4..109e19954d6a 100644 --- a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Accept.java +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/Accept.java @@ -5,6 +5,7 @@ import org.eclipse.core.commands.ExecutionException; import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.inlineCompletion.IInlineCompletionService.AcceptType; import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionService; public class Accept extends AbstractHandler { @@ -13,7 +14,7 @@ public class Accept extends AbstractHandler { @Override public Object execute(ExecutionEvent event) throws ExecutionException { logger.debug("Accept the current inline completion."); - InlineCompletionService.getInstance().accept(); + InlineCompletionService.getInstance().accept(AcceptType.FULL_COMPLETION); return null; } diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/AcceptNextLine.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/AcceptNextLine.java new file mode 100644 index 000000000000..b013ad0771a0 --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/AcceptNextLine.java @@ -0,0 +1,26 @@ +package com.tabbyml.tabby4eclipse.commands.inlineCompletion; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.inlineCompletion.IInlineCompletionService.AcceptType; +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionService; + +public class AcceptNextLine extends AbstractHandler { + private Logger logger = new Logger("Commands.InlineCompletion.AcceptNextLine"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Accept next line of the current inline completion."); + InlineCompletionService.getInstance().accept(AcceptType.NEXT_LINE); + return null; + } + + @Override + public boolean isEnabled() { + return InlineCompletionService.getInstance().isCompletionItemVisible(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/AcceptNextWord.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/AcceptNextWord.java new file mode 100644 index 000000000000..4ddb31d2666f --- /dev/null +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/commands/inlineCompletion/AcceptNextWord.java @@ -0,0 +1,26 @@ +package com.tabbyml.tabby4eclipse.commands.inlineCompletion; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import com.tabbyml.tabby4eclipse.Logger; +import com.tabbyml.tabby4eclipse.inlineCompletion.IInlineCompletionService.AcceptType; +import com.tabbyml.tabby4eclipse.inlineCompletion.InlineCompletionService; + +public class AcceptNextWord extends AbstractHandler { + private Logger logger = new Logger("Commands.InlineCompletion.AcceptNextLine"); + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + logger.debug("Accept next word of the current inline completion."); + InlineCompletionService.getInstance().accept(AcceptType.NEXT_WORD); + return null; + } + + @Override + public boolean isEnabled() { + return InlineCompletionService.getInstance().isCompletionItemVisible(); + } + +} diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/editor/EditorUtils.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/editor/EditorUtils.java index b7447f4dc853..8d7a9be0b6a8 100644 --- a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/editor/EditorUtils.java +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/editor/EditorUtils.java @@ -17,6 +17,7 @@ import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.contexts.IContextService; import org.eclipse.ui.texteditor.ITextEditor; import com.tabbyml.tabby4eclipse.chat.ChatMessage.FileContext; @@ -67,6 +68,10 @@ public static Display getDisplay(ITextEditor textEditor) { return getStyledTextWidget(textEditor).getDisplay(); } + public static IContextService getContextService(ITextEditor textEditor) { + return textEditor.getSite().getService(IContextService.class); + } + public static void asyncExec(Runnable runnable) { PlatformUI.getWorkbench().getDisplay().asyncExec(runnable); } @@ -127,7 +132,7 @@ public static int getCurrentOffsetInDocument(ITextEditor textEditor) throws Ille throw new IllegalStateException("Failed to get current offset in document."); }); } - + public static String getSelectedText() { ITextEditor editor = getActiveTextEditor(); if (editor != null) { @@ -135,7 +140,7 @@ public static String getSelectedText() { } return null; } - + public static String getSelectedText(ITextEditor textEditor) { ISelection selection = textEditor.getSelectionProvider().getSelection(); if (selection instanceof ITextSelection textSelection) { diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/IInlineCompletionService.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/IInlineCompletionService.java index 2cb098d573cc..cc192ea29138 100644 --- a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/IInlineCompletionService.java +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/IInlineCompletionService.java @@ -42,10 +42,16 @@ public interface IInlineCompletionService { */ public void previous(); + enum AcceptType { + FULL_COMPLETION, NEXT_LINE, NEXT_WORD, + } + /** * Accept the current completion item ghost text. + * + * @param type the type of completion to accept. */ - public void accept(); + public void accept(AcceptType type); /** * Dismiss the current completion item ghost text. diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/InlineCompletionService.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/InlineCompletionService.java index 2c900bac7855..012659dcc7e6 100644 --- a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/InlineCompletionService.java +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/inlineCompletion/InlineCompletionService.java @@ -3,6 +3,8 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; @@ -12,6 +14,8 @@ import org.eclipse.lsp4e.LSPEclipseUtils; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.ui.contexts.IContextActivation; +import org.eclipse.ui.contexts.IContextService; import org.eclipse.ui.texteditor.ITextEditor; import com.tabbyml.tabby4eclipse.Logger; @@ -34,9 +38,12 @@ private static class LazyHolder { private static final IInlineCompletionService INSTANCE = new InlineCompletionService(); } + private static final String INLINE_COMPLETION_VISIBLE_CONTEXT_ID = "com.tabbyml.tabby4eclipse.inlineCompletionVisible"; + private Logger logger = new Logger("InlineCompletionService"); private InlineCompletionRenderer renderer = new InlineCompletionRenderer(); private InlineCompletionContext current; + private IContextActivation inlineCompletionVisibleContext; @Override public boolean isCompletionItemVisible() { @@ -76,6 +83,7 @@ public void trigger(boolean isManualTrigger) { logger.info("Provide inline completion for TextEditor " + textEditor.getTitle() + " at offset " + offset + " with modification stamp " + modificationStamp); renderer.hide(); + deactivateInlineCompletionVisibleContext(textEditor); if (current != null) { if (current.job != null && !current.job.isDone()) { logger.info("Cancel the current job due to new request."); @@ -105,6 +113,7 @@ public void trigger(boolean isManualTrigger) { InlineCompletionList list = request.convertInlineCompletionList(completionList); current.response = new InlineCompletionContext.Response(list); renderer.show(textViewer, current.response.getActiveCompletionItem()); + activateInlineCompletionVisibleContext(textEditor); EventParams eventParams = buildTelemetryEventParams(EventParams.Type.VIEW); postTelemetryEvent(eventParams); } catch (BadLocationException e) { @@ -191,7 +200,7 @@ private int calcCycleIndex(int index, int listSize, int step) { } @Override - public void accept() { + public void accept(AcceptType acceptType) { ITextEditor textEditor = EditorUtils.getActiveTextEditor(); logger.info("Accept inline completion in TextEditor " + textEditor.toString()); if (current == null || current.request == null || current.response == null) { @@ -199,23 +208,47 @@ public void accept() { } int offset = current.request.offset; InlineCompletionItem item = current.response.getActiveCompletionItem(); - EventParams eventParams = buildTelemetryEventParams(EventParams.Type.SELECT); + EventParams eventParams = buildTelemetryEventParams(EventParams.Type.SELECT, + acceptType == AcceptType.FULL_COMPLETION ? null : "line"); renderer.hide(); + deactivateInlineCompletionVisibleContext(textEditor); current = null; int prefixReplaceLength = item.getReplaceRange().getPrefixLength(); int suffixReplaceLength = item.getReplaceRange().getSuffixLength(); String text = item.getInsertText().substring(prefixReplaceLength); - if (text.isEmpty()) { + String textToInsert; + + if (acceptType == AcceptType.NEXT_WORD) { + Pattern pattern = Pattern.compile("\\w+|\\W+"); + Matcher matcher = pattern.matcher(text); + if (matcher.find()) { + textToInsert = matcher.group(); + } else { + textToInsert = text; + } + } else if (acceptType == AcceptType.NEXT_LINE) { + List lines = List.of(text.split("\n")); + String line = lines.get(0); + if (text.isEmpty() && lines.size() > 1) { + line += "\n"; + line += lines.get(1); + } + textToInsert = line; + } else { + textToInsert = text; + } + + if (textToInsert.isEmpty()) { return; } IDocument document = EditorUtils.getDocument(textEditor); EditorUtils.syncExec(textEditor, () -> { try { - document.replace(offset, suffixReplaceLength, text); - ITextSelection selection = new TextSelection(offset + text.length(), 0); + document.replace(offset, suffixReplaceLength, textToInsert); + ITextSelection selection = new TextSelection(offset + textToInsert.length(), 0); textEditor.getSelectionProvider().setSelection(selection); postTelemetryEvent(eventParams); } catch (BadLocationException e) { @@ -230,6 +263,8 @@ public void dismiss() { logger.info("Dismiss inline completion."); EventParams eventParams = buildTelemetryEventParams(EventParams.Type.DISMISS); renderer.hide(); + ITextEditor textEditor = EditorUtils.getActiveTextEditor(); + deactivateInlineCompletionVisibleContext(textEditor); postTelemetryEvent(eventParams); } if (current != null) { @@ -269,6 +304,22 @@ private void postTelemetryEvent(EventParams params) { } } + private void activateInlineCompletionVisibleContext(ITextEditor editor) { + IContextService contextService = EditorUtils.getContextService(editor); + if (contextService != null) { + logger.debug("Activating inline completion visible context."); + inlineCompletionVisibleContext = contextService.activateContext(INLINE_COMPLETION_VISIBLE_CONTEXT_ID); + } + } + + private void deactivateInlineCompletionVisibleContext(ITextEditor editor) { + IContextService contextService = EditorUtils.getContextService(editor); + if (contextService != null && inlineCompletionVisibleContext != null) { + logger.debug("Deactivating inline completion visible context."); + contextService.deactivateContext(inlineCompletionVisibleContext); + } + } + private class InlineCompletionContext { private static class Request { private Logger logger = new Logger("InlineCompletionContext.Request");