diff --git a/build.gradle.kts b/build.gradle.kts index c431dba..82343bc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,6 +28,7 @@ repositories { dependencies { implementation("com.apollographql.apollo:apollo-runtime:2.5.11") implementation("org.apache.commons:commons-lang3:3.12.0") + implementation("com.github.rjeschke:txtmark:0.13") testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testCompile("org.mockito:mockito-core:4.1.0") diff --git a/src/main/graphql/io/codiga/api/GetRecipesForClient.graphql b/src/main/graphql/io/codiga/api/GetRecipesForClient.graphql index e15fd7b..9bb9b29 100644 --- a/src/main/graphql/io/codiga/api/GetRecipesForClient.graphql +++ b/src/main/graphql/io/codiga/api/GetRecipesForClient.graphql @@ -1,11 +1,13 @@ query GetRecipesForClient($fingerprint: String, $filename: String, $keywords: [String!]!, $dependencies: [String!]!, $parameters: String, $language: LanguageEnumeration!){ getRecipesForClient(fingerprint: $fingerprint, keywords: $keywords, filename: $filename, dependencies:$dependencies, parameters:$parameters,language:$language){ id + name code keywords imports language description + shortcut } } diff --git a/src/main/java/io/codiga/plugins/jetbrains/Constants.java b/src/main/java/io/codiga/plugins/jetbrains/Constants.java index 6ada55a..23b0d0d 100644 --- a/src/main/java/io/codiga/plugins/jetbrains/Constants.java +++ b/src/main/java/io/codiga/plugins/jetbrains/Constants.java @@ -11,8 +11,8 @@ public class Constants { public static final String LINE_SEPARATOR = "\n"; public static final char CHARACTER_SPACE = ' '; - public static final int NUMBER_OF_RECIPES_TO_KEEP_FOR_COMPLETION = 3; - public static final int MINIMUM_LINE_LENGTH_TO_TRIGGER_AUTOCOMPLETION = 5; + public static final int NUMBER_OF_RECIPES_TO_KEEP_FOR_COMPLETION = 5; + public static final int MINIMUM_LINE_LENGTH_TO_TRIGGER_AUTOCOMPLETION = 3; // Python-specific constants diff --git a/src/main/java/io/codiga/plugins/jetbrains/actions/AssistantUseRecipeAction.java b/src/main/java/io/codiga/plugins/jetbrains/actions/AssistantUseRecipeAction.java index d76b199..4768746 100644 --- a/src/main/java/io/codiga/plugins/jetbrains/actions/AssistantUseRecipeAction.java +++ b/src/main/java/io/codiga/plugins/jetbrains/actions/AssistantUseRecipeAction.java @@ -1,5 +1,7 @@ package io.codiga.plugins.jetbrains.actions; +import com.github.rjeschke.txtmark.Processor; +import com.intellij.ui.components.JBScrollPane; import io.codiga.api.GetRecipesForClientQuery; import io.codiga.api.type.LanguageEnumeration; import io.codiga.plugins.jetbrains.dependencies.DependencyManagement; @@ -27,8 +29,11 @@ import org.jetbrains.annotations.NotNull; import javax.swing.*; +import javax.swing.border.Border; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import javax.swing.event.HyperlinkEvent; +import javax.swing.event.HyperlinkListener; import java.awt.*; import java.awt.event.*; import java.math.BigDecimal; @@ -49,7 +54,7 @@ public class AssistantUseRecipeAction extends AnAction { public static final Logger LOGGER = Logger.getInstance(LOGGER_NAME); public static final String ENTER_SEARCH_TERM_TEXT = "(enter search terms)"; - + private final CodigaMarkdownDecorator codigaMarkdownDecorator = new CodigaMarkdownDecorator(); private final CodigaApi codigaApi = ApplicationManager.getApplication().getService(CodigaApi.class); // UI elements @@ -59,6 +64,8 @@ public class AssistantUseRecipeAction extends AnAction { private JButton nextButton = null; private JButton previousButton = null; private JButton okButton = null; + private final JEditorPane jEditorPane = new JEditorPane(); + private final JBScrollPane scrollPane = new JBScrollPane(jEditorPane); // status of the action: is code inserted, what are the recipes, etc. private boolean codeInserted = false; @@ -148,11 +155,18 @@ public void showCurrentRecipe(AnActionEvent anActionEvent) { // reindent the code based on the indentation of the current line. String indentedCode = indentOtherLines(code, indentationCurrentLine); + String finalDescription = recipe.description().length() == 0 ? "no description" : recipe.description(); + String finalDescriptionWithLink = finalDescription + String.format("\n\n[%s](https://app.codiga.io/marketplace/recipe/%s/view)", "View Recipe on Codiga", recipe.id()); + + + String html = Processor.process(finalDescriptionWithLink, codigaMarkdownDecorator); + jEditorPane.setContentType("text/html"); + jEditorPane.setText(html); // Update the label in the box with the description. - String finalDescription = recipe.description().length() == 0 ? "no description" : recipe.description(); - String descriptionLabelText = String.format("result %s/%s: %s", currentRecipeIndex + 1, currentRecipes.size(), finalDescription); + + String descriptionLabelText = String.format("result %s/%s: %s", currentRecipeIndex + 1, currentRecipes.size(), recipe.name()); jLabelResults.setText(descriptionLabelText); // add the code and update global variables to indicate code has been inserted. @@ -385,10 +399,34 @@ public void actionPerformed(@NotNull AnActionEvent event) { JPanel jPanelMiddle = new JPanel(new FlowLayout()); JPanel jPanelBottom = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JPanel jPanelDescription = new JPanel(new FlowLayout(FlowLayout.LEFT)); // bottom panel jPanelBottom.add(jLabelResults); + // description panel + jEditorPane.setContentType("text/html"); + jEditorPane.setText(Processor.process("Recipe description will appear here", codigaMarkdownDecorator)); + jEditorPane.setEditable(false); + + jEditorPane.addHyperlinkListener(new HyperlinkListener() { + @Override + public void hyperlinkUpdate(HyperlinkEvent hle) { + if (HyperlinkEvent.EventType.ACTIVATED.equals(hle.getEventType())) { + Desktop desktop = Desktop.getDesktop(); + try { + desktop.browse(hle.getURL().toURI()); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + } + }); + jPanelDescription.setBorder(BorderFactory.createEmptyBorder(0, CodigaIcons.Codiga_default_icon.getIconWidth() + 10, 0, 0)); + scrollPane.setPreferredSize(new Dimension(800 - (CodigaIcons.Codiga_default_icon.getIconWidth() + 10) * 2 , 200)); + scrollPane.setMinimumSize(new Dimension(800 - (CodigaIcons.Codiga_default_icon.getIconWidth() + 10) * 2, 200)); + jPanelDescription.add(scrollPane); + // middle panel JLabel codigaLabel = new JLabel(CodigaIcons.Codiga_default_icon); codigaLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 10)); @@ -493,6 +531,7 @@ public void keyPressed(KeyEvent ke) { // build the main panel jPanelMain.add(jPanelMiddle); jPanelMain.add(jPanelBottom); + jPanelMain.add(jPanelDescription); // Build the main window to keep it with an IntelliJ style windowWrapper = new WindowWrapperBuilder(WindowWrapper.Mode.FRAME, jPanelMain) @@ -506,8 +545,9 @@ public void keyPressed(KeyEvent ke) { return true; }) .build(); - windowWrapper.getWindow().setPreferredSize(new Dimension(800, 100)); - windowWrapper.getWindow().setSize(new Dimension(800, 100)); + + windowWrapper.getWindow().setPreferredSize(new Dimension(800, 300)); + windowWrapper.getWindow().setSize(new Dimension(800, 300)); windowWrapper.show(); diff --git a/src/main/java/io/codiga/plugins/jetbrains/actions/CodigaMarkdownDecorator.java b/src/main/java/io/codiga/plugins/jetbrains/actions/CodigaMarkdownDecorator.java new file mode 100644 index 0000000..925ceac --- /dev/null +++ b/src/main/java/io/codiga/plugins/jetbrains/actions/CodigaMarkdownDecorator.java @@ -0,0 +1,25 @@ +package io.codiga.plugins.jetbrains.actions; + +import com.github.rjeschke.txtmark.DefaultDecorator; + +public class CodigaMarkdownDecorator extends DefaultDecorator { + + private static final String style = " style=\"font-family: Arial;\" "; + + @Override + public void openHeadline(final StringBuilder out, final int level) + { + out.append(""); + } +} diff --git a/src/main/java/io/codiga/plugins/jetbrains/completion/CodigaCompletion.java b/src/main/java/io/codiga/plugins/jetbrains/completion/CodigaCompletion.java index 6e974ef..60f14b0 100644 --- a/src/main/java/io/codiga/plugins/jetbrains/completion/CodigaCompletion.java +++ b/src/main/java/io/codiga/plugins/jetbrains/completion/CodigaCompletion.java @@ -4,6 +4,7 @@ import com.intellij.codeInsight.completion.CompletionType; import com.intellij.openapi.diagnostic.Logger; import com.intellij.patterns.PlatformPatterns; +import com.intellij.psi.tree.IElementType; import static com.intellij.patterns.PlatformPatterns.psiElement; import static io.codiga.plugins.jetbrains.Constants.LOGGER_NAME; @@ -12,7 +13,7 @@ public class CodigaCompletion extends CompletionContributor { public static final Logger LOGGER = Logger.getInstance(LOGGER_NAME); public CodigaCompletion() { extend(CompletionType.BASIC, - PlatformPatterns.psiElement(), + PlatformPatterns.not(PlatformPatterns.alwaysFalse()), new CodigaCompletionProvider()); } } diff --git a/src/main/java/io/codiga/plugins/jetbrains/completion/CodigaCompletionProvider.java b/src/main/java/io/codiga/plugins/jetbrains/completion/CodigaCompletionProvider.java index 1225e2e..acf7b10 100644 --- a/src/main/java/io/codiga/plugins/jetbrains/completion/CodigaCompletionProvider.java +++ b/src/main/java/io/codiga/plugins/jetbrains/completion/CodigaCompletionProvider.java @@ -3,31 +3,42 @@ import com.intellij.codeInsight.completion.CompletionParameters; import com.intellij.codeInsight.completion.CompletionProvider; import com.intellij.codeInsight.completion.CompletionResultSet; +import com.intellij.codeInsight.completion.InsertionContext; +import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorModificationUtil; +import com.intellij.openapi.editor.markup.EffectType; +import com.intellij.openapi.editor.markup.HighlighterTargetArea; +import com.intellij.openapi.editor.markup.RangeHighlighter; +import com.intellij.openapi.editor.markup.TextAttributes; +import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.JBColor; import com.intellij.util.ProcessingContext; +import com.intellij.util.ThrowableRunnable; import icons.CodigaIcons; import io.codiga.api.GetRecipesForClientQuery; import io.codiga.api.type.LanguageEnumeration; import io.codiga.plugins.jetbrains.dependencies.DependencyManagement; import io.codiga.plugins.jetbrains.graphql.CodigaApi; import io.codiga.plugins.jetbrains.graphql.LanguageUtils; +import io.codiga.plugins.jetbrains.model.CodeInsertion; import io.codiga.plugins.jetbrains.settings.application.AppSettingsState; import org.jetbrains.annotations.NotNull; import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Base64; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import static io.codiga.plugins.jetbrains.Constants.*; +import static io.codiga.plugins.jetbrains.utils.CodeImportUtils.hasImport; +import static io.codiga.plugins.jetbrains.utils.CodePositionUtils.*; /** * Provide completion when the user type some code on one line. @@ -44,6 +55,68 @@ public class CodigaCompletionProvider extends CompletionProvider imports = recipe.imports(); + try { + + WriteCommandAction.writeCommandAction(project).run( + (ThrowableRunnable) () -> { + int firstInsertion = firstPositionToInsert(currentCode, recipe.language()); + + for(String importStatement: imports) { + if(!hasImport(currentCode, importStatement, recipe.language())) { + + String dependencyStatement = importStatement + LINE_SEPARATOR; + document.insertString(firstInsertion, dependencyStatement); + } + } + } + ); + } catch (Throwable e) { + e.printStackTrace(); + LOGGER.error("showCurrentRecipe - impossible to update the code from the recipe"); + LOGGER.error(e); + } + + // sent a callback that the recipe has been used. + long recipeId = ((BigDecimal) recipe.id()).longValue(); + codigaApi.recordRecipeUse(recipeId); + } + /** * Add the completion: call the API to get all completions and surface them * @param parameters @@ -69,6 +142,7 @@ protected void addCompletions(@NotNull CompletionParameters parameters, if(lineEnd > lineStart + 1){ currentLine = editor.getDocument().getText(new TextRange(lineStart, lineEnd - 1)); } + int indentationCurrentLine = getIndentation(currentLine); if (currentLine.length() < MINIMUM_LINE_LENGTH_TO_TRIGGER_AUTOCOMPLETION){ @@ -82,12 +156,11 @@ protected void addCompletions(@NotNull CompletionParameters parameters, } // Get all recipes parameters. - final List keywords = Arrays.asList(currentLine.split(" ")); + final List keywords = Arrays.asList(currentLine.split(" ")).stream().filter(p -> !p.isEmpty()).collect(Collectors.toList()); final VirtualFile virtualFile = parameters.getOriginalFile().getVirtualFile(); LanguageEnumeration language = LanguageUtils.getLanguageFromFilename(virtualFile.getCanonicalPath()); List dependenciesName = dependencyManagement.getDependencies(parameters.getOriginalFile()).stream().map(d -> d.getName()).collect(Collectors.toList()); final String filename = virtualFile.getName(); - // Get the recipes from the API. List recipes = codigaApi.getRecipesForClient( keywords, @@ -104,31 +177,25 @@ protected void addCompletions(@NotNull CompletionParameters parameters, * For each of them, add a completion item and add a routine to insert the code. */ for(GetRecipesForClientQuery.GetRecipesForClient recipe: recipes.stream().limit(NUMBER_OF_RECIPES_TO_KEEP_FOR_COMPLETION).collect(Collectors.toList())){ - String lookup = String.join(" ", recipe.keywords()); + List recipeKeywords = new ArrayList<>(recipe.keywords()); + if (recipe.shortcut() != null) { + recipeKeywords.add(recipe.shortcut()); + } + + + String lookup = String.join(" ", recipeKeywords); LookupElementBuilder element = LookupElementBuilder - .create(lookup) - .withTypeText(recipe.description()) + .create(recipe.name()) + .withTypeText(String.join(",", recipeKeywords)) .withLookupString(lookup) .withInsertHandler((insertionContext, lookupElement) -> { - insertionContext.setAddCompletionChar(false); - - // remove the code on the line - final int startOffetToRemove = insertionContext.getEditor().getCaretModel().getVisualLineStart(); - final int endOffetToRemove = insertionContext.getEditor().getCaretModel().getVisualLineEnd(); - insertionContext.getEditor().getDocument().deleteString(startOffetToRemove, endOffetToRemove ); - - // add the code and update the document. - String code = new String(Base64.getDecoder().decode(recipe.code())).replaceAll("\r\n", LINE_SEPARATOR); - EditorModificationUtil.insertStringAtCaret(insertionContext.getEditor(), code); - insertionContext.commitDocument(); - - // sent a callback that the recipe has been used. - long recipeId = ((BigDecimal) recipe.id()).longValue(); - codigaApi.recordRecipeUse(recipeId); + addRecipeInEditor(recipe, indentationCurrentLine, parameters, insertionContext); }) .withIcon(CodigaIcons.Codiga_default_icon); + + result.addElement(element); } }