diff --git a/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/Code4MeBundle.java b/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/Code4MeBundle.java index ad97495..d94b7c8 100644 --- a/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/Code4MeBundle.java +++ b/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/Code4MeBundle.java @@ -5,8 +5,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.PropertyKey; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import java.util.function.Supplier; public class Code4MeBundle extends DynamicBundle { @@ -14,8 +12,6 @@ public class Code4MeBundle extends DynamicBundle { private static final String PATH_TO_BUNDLE = "messages.Code4MeBundle"; private static final Code4MeBundle instance = new Code4MeBundle(); - private static final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); - public static @Nls String message( @NotNull @PropertyKey(resourceBundle = PATH_TO_BUNDLE) String key, Object... params @@ -31,10 +27,6 @@ public static Supplier messagePointer( return instance.getLazyMessage(key, params); } - public static ScheduledExecutorService getExecutorService() { - return executorService; - } - private Code4MeBundle() { super(PATH_TO_BUNDLE); } diff --git a/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/Code4MeScheduledThreadPoolExecutor.java b/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/Code4MeScheduledThreadPoolExecutor.java new file mode 100644 index 0000000..7951e73 --- /dev/null +++ b/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/Code4MeScheduledThreadPoolExecutor.java @@ -0,0 +1,45 @@ +package me.code4me.plugin; + +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class Code4MeScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor { + + private static final Code4MeScheduledThreadPoolExecutor instance = new Code4MeScheduledThreadPoolExecutor(); + + public static Code4MeScheduledThreadPoolExecutor getInstance() { + return instance; + } + + private Code4MeScheduledThreadPoolExecutor() { + super(1); + } + + @Override + public ScheduledFuture schedule(@NotNull Runnable command, long delay, @NotNull TimeUnit unit) { + return super.schedule(() -> { + try { + command.run(); + } catch (Throwable th) { + th.printStackTrace(); + throw th; + } + }, delay, unit); + } + + @Override + public ScheduledFuture schedule(@NotNull Callable callable, long delay, @NotNull TimeUnit unit) { + return super.schedule(() -> { + try { + return callable.call(); + } catch (Throwable th) { + th.printStackTrace(); + throw th; + } + }, delay, unit); + } +} diff --git a/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/api/PredictionAutocompleteResponse.java b/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/api/PredictionAutocompleteResponse.java index 03f9a28..6ab5fd6 100644 --- a/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/api/PredictionAutocompleteResponse.java +++ b/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/api/PredictionAutocompleteResponse.java @@ -4,11 +4,13 @@ public class PredictionAutocompleteResponse extends Code4MeResponse { private final String[] predictions; private final String verifyToken; + private final boolean survey; - public PredictionAutocompleteResponse(String[] predictions, String verifyToken) { + public PredictionAutocompleteResponse(String[] predictions, String verifyToken, boolean survey) { super(200); this.predictions = predictions; this.verifyToken = verifyToken; + this.survey = survey; } public String[] getPredictions() { @@ -18,4 +20,8 @@ public String[] getPredictions() { public String getVerifyToken() { return verifyToken; } + + public boolean isSurvey() { + return survey; + } } diff --git a/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/completions/Code4MeCompletionContributor.java b/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/completions/Code4MeCompletionContributor.java index 1cd2dcd..4a5b764 100644 --- a/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/completions/Code4MeCompletionContributor.java +++ b/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/completions/Code4MeCompletionContributor.java @@ -10,6 +10,7 @@ import com.intellij.codeInsight.hint.HintManager; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.notification.NotificationAction; import com.intellij.notification.NotificationGroupManager; import com.intellij.notification.NotificationType; import com.intellij.openapi.application.ApplicationManager; @@ -23,17 +24,21 @@ import com.intellij.patterns.PlatformPatterns; import me.code4me.plugin.Code4MeIcons; import me.code4me.plugin.Code4MeBundle; +import me.code4me.plugin.Code4MeScheduledThreadPoolExecutor; import me.code4me.plugin.api.PredictionAutocompleteRequest; import me.code4me.plugin.api.PredictionVerifyRequest; import me.code4me.plugin.api.Code4MeErrorResponse; import me.code4me.plugin.exceptions.ApiServerException; import me.code4me.plugin.services.Code4MeApiService; +import me.code4me.plugin.services.Code4MeSettingsService; import me.code4me.plugin.util.Code4MeUtil; import org.jetbrains.annotations.NotNull; import java.awt.EventQueue; import java.util.Arrays; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; public class Code4MeCompletionContributor extends CompletionContributor { @@ -61,13 +66,37 @@ public static void suggestCompletion( project.getService(Code4MeApiService.class).fetchAutoCompletion(project, request).thenAccept(res -> { String[] predictions = res.getPredictions(); + + Code4MeSettingsService settingsService = project.getService(Code4MeSettingsService.class); + Code4MeSettingsService.Settings settings = settingsService.getSettings(); + if (!settings.isIgnoringSurvey() && res.isSurvey()) { + NotificationGroupManager.getInstance() + .getNotificationGroup("Code4Me Notifications") + .createNotification( + Code4MeBundle.message("survey-title"), + Code4MeBundle.message("survey-content"), + NotificationType.INFORMATION + ).addAction(NotificationAction.createSimple( + Code4MeBundle.message("survey-redirect-action"), + () -> project.getService(Code4MeApiService.class).redirectToCode4MeSurvey(project)) + ).addAction(NotificationAction.createSimple( + Code4MeBundle.message("survey-cancel-action"), + () -> { + settings.setIgnoringSurvey(true); + settingsService.save(); + }) + ).notify(project); + } + EventQueue.invokeLater(() -> { if (predictions == null || Arrays.stream(predictions).allMatch(String::isBlank)) { HintManager.getInstance().showInformationHint(editor, "No Code4Me Suggestions available"); } else { + String verifyToken = res.getVerifyToken(); completionCache.setPredictions(predictions); completionCache.setOffset(offset); - completionCache.setVerifyToken(res.getVerifyToken()); + completionCache.setVerifyToken(verifyToken); + completionCache.setTimeoutSupplier(() -> checkCodeChanges(project, verifyToken, null, offset, doc)); completionCache.setEmpty(false); ApplicationManager.getApplication().invokeLater(() -> { @@ -87,7 +116,7 @@ public static void suggestCompletion( }); } - private static void checkCodeChanges( + private static ScheduledFuture checkCodeChanges( Project project, String verifyToken, String chosenPrediction, @@ -105,9 +134,10 @@ public void documentChanged(@NotNull DocumentEvent event) { }; doc.addDocumentListener(listener); - Code4MeBundle.getExecutorService().schedule(() -> { + return Code4MeScheduledThreadPoolExecutor.getInstance().schedule(() -> { doc.removeDocumentListener(listener); - String groundTruth = doc.getText().substring(atomicOffset.get()).split("\n")[0]; + String[] lines = doc.getText().substring(atomicOffset.get()).split("\n"); + String groundTruth = lines.length == 0 ? "" : lines[0]; project.getService(Code4MeApiService.class).sendCompletionData( project, new PredictionVerifyRequest(verifyToken, chosenPrediction, groundTruth) @@ -144,16 +174,21 @@ protected void addCompletions( return; } + ScheduledFuture dataRequest = completionCache.getTimeoutSupplier().get(); + for (String prediction : completionCache.getPredictions()) { result.addElement(prioritize(LookupElementBuilder.create(prediction) .withIcon(Code4MeIcons.PLUGIN_ICON) - .withInsertHandler((cxt, item) -> checkCodeChanges( - cxt.getProject(), - completionCache.getVerifyToken(), - prediction, - completionCache.getOffset(), - cxt.getDocument() - )) + .withInsertHandler((cxt, item) -> { + if (!dataRequest.cancel(true)) return; + checkCodeChanges( + cxt.getProject(), + completionCache.getVerifyToken(), + prediction, + completionCache.getOffset(), + cxt.getDocument() + ); + }) .withTypeText("Code4Me") )); } @@ -180,12 +215,14 @@ private static class CompletionCache { private int offset; private String verifyToken; private boolean empty; + private Supplier> timeoutSupplier; public CompletionCache() { this.predictions = new String[0]; this.offset = -1; this.verifyToken = ""; this.empty = true; + this.timeoutSupplier = null; } public void setPredictions(String[] predictions) { @@ -219,5 +256,13 @@ public void setEmpty(boolean empty) { public boolean isEmpty() { return empty; } + + public void setTimeoutSupplier(Supplier> timeoutSupplier) { + this.timeoutSupplier = timeoutSupplier; + } + + public Supplier> getTimeoutSupplier() { + return timeoutSupplier; + } } } diff --git a/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/services/Code4MeApiService.java b/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/services/Code4MeApiService.java index afd90de..3883437 100644 --- a/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/services/Code4MeApiService.java +++ b/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/services/Code4MeApiService.java @@ -1,6 +1,7 @@ package me.code4me.plugin.services; import com.google.gson.Gson; +import com.intellij.ide.BrowserUtil; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; @@ -37,6 +38,7 @@ public class Code4MeApiService { private static final String BASE_URL = "https://code4me.me/api/v1"; private static final String PREDICTION_AUTOCOMPLETE_ENDPOINT = "/prediction/autocomplete"; private static final String PREDICTION_VERIFY_ENDPOINT = "/prediction/verify"; + private static final String SURVEY_ENDPOINT = "/survey?user_id=%s"; private final AtomicBoolean lock = new AtomicBoolean(false); @@ -157,4 +159,9 @@ private Code4MeResponse parseResponseBody( } } } + + public void redirectToCode4MeSurvey(Project project) { + String userToken = project.getService(Code4MeSettingsService.class).getSettings().getUserToken(); + BrowserUtil.browse(BASE_URL + String.format(SURVEY_ENDPOINT, userToken)); + } } diff --git a/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/services/Code4MeSettingsService.java b/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/services/Code4MeSettingsService.java index a8df043..034758f 100644 --- a/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/services/Code4MeSettingsService.java +++ b/code4me-jetbrains-plugin/src/main/java/me/code4me/plugin/services/Code4MeSettingsService.java @@ -24,7 +24,7 @@ public Code4MeSettingsService() { String settingsJson = PasswordSafe.getInstance().getPassword(credentialAttributes); if (settingsJson == null) { - this.settings = new Settings(generateToken(), true); + this.settings = new Settings(generateToken(), true, false); this.save(); } else { this.settings = gson.fromJson(settingsJson, Settings.class); @@ -51,10 +51,12 @@ public static class Settings { private String userToken; private boolean triggerPoints; + private Boolean ignoringSurvey; - public Settings(String userToken, boolean triggerPoints) { + public Settings(String userToken, boolean triggerPoints, boolean ignoringSurvey) { this.userToken = userToken; this.triggerPoints = triggerPoints; + this.ignoringSurvey = ignoringSurvey; } public void setUserToken(String userToken) { @@ -72,5 +74,13 @@ public boolean isTriggerPoints() { public void setTriggerPoints(boolean triggerPoints) { this.triggerPoints = triggerPoints; } + + public boolean isIgnoringSurvey() { + return Boolean.TRUE.equals(ignoringSurvey); + } + + public void setIgnoringSurvey(boolean ignoringSurvey) { + this.ignoringSurvey = ignoringSurvey; + } } } diff --git a/code4me-jetbrains-plugin/src/main/resources/messages/Code4MeBundle.properties b/code4me-jetbrains-plugin/src/main/resources/messages/Code4MeBundle.properties index f10d171..b51fc4e 100644 --- a/code4me-jetbrains-plugin/src/main/resources/messages/Code4MeBundle.properties +++ b/code4me-jetbrains-plugin/src/main/resources/messages/Code4MeBundle.properties @@ -4,3 +4,7 @@ project-opened-setup-content=Thanks for using the Code4Me Plugin.

You can project-opened-title=Code4Me plugin project-opened-content=Thanks for using the Code4Me Plugin. You can use the alt + shift + K keybind to query an autocompletion. project-opened-settings-action=Open settings +survey-title=Code4me survey +survey-content=Thanks for using the Code4Me plugin. We would greatly appreciate it if you could help our study by filling in the survey. +survey-redirect-action=Fill in survey +survey-cancel-action=Do not show again diff --git a/code4me-server/README.md b/code4me-server/README.md index a770551..567d886 100644 --- a/code4me-server/README.md +++ b/code4me-server/README.md @@ -66,7 +66,7 @@ Content-Type: application/json ``` { "verifyToken": string, - "chosenPrediction": string, + "chosenPrediction": string | null, "groundTruth": string } ``` diff --git a/code4me-server/src/api.py b/code4me-server/src/api.py index b7cd300..84b6d0b 100644 --- a/code4me-server/src/api.py +++ b/code4me-server/src/api.py @@ -67,7 +67,7 @@ def verify(): request.get_json(), [ ("verifyToken", str, False), - ("chosenPrediction", str, False), + ("chosenPrediction", str, True), ("groundTruth", str, False), ], )