Skip to content

Commit

Permalink
Add InlayPresentation display mode configuration. (#90)
Browse files Browse the repository at this point in the history
Co-authored-by: zhaopengjun <[email protected]>
  • Loading branch information
PerryZhao and zhaopengjun authored Sep 23, 2024
1 parent a8791b6 commit 7e283ae
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 51 deletions.
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ ij_java_for_statement_wrap = off
ij_java_generate_final_locals = false
ij_java_generate_final_parameters = false
ij_java_if_brace_force = always
ij_java_imports_layout = java.**,|,javax.**,jakarta.**,|,*,|,org.springframework.**,|,$*
ij_java_imports_layout = brave.**,com.**,feign.**,io.**,|,java.**,|,javax.**,lombok.**,net.**,|,org.**,jakarta.**,|,$*
ij_java_indent_case_from_switch = true
ij_java_insert_inner_class_imports = false
ij_java_insert_override_annotation = true
Expand Down Expand Up @@ -202,7 +202,7 @@ ij_java_names_count_to_use_import_on_demand = 99
ij_java_new_line_after_lparen_in_annotation = false
ij_java_new_line_after_lparen_in_deconstruction_pattern = true
ij_java_new_line_after_lparen_in_record_header = false
ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.*
ij_java_packages_to_use_import_on_demand =
ij_java_parameter_annotation_wrap = off
ij_java_parameter_name_prefix =
ij_java_parameter_name_suffix =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.intellij.codeInsight.hints.InlayHintsSink;
import com.intellij.codeInsight.hints.presentation.InlayPresentation;
import com.intellij.codeInsight.hints.presentation.PresentationFactory;
import com.intellij.codeInsight.hints.presentation.SequencePresentation;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
Expand All @@ -14,22 +16,36 @@
import com.intellij.openapi.editor.CaretModel;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.PopupStep;
import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.SmartList;
import com.zhongan.devpilot.DevPilotIcons;
import com.zhongan.devpilot.enums.EditorActionEnum;
import com.zhongan.devpilot.gui.toolwindows.chat.DevPilotChatToolWindowService;
import com.zhongan.devpilot.settings.state.ChatShortcutSettingState;
import com.zhongan.devpilot.util.DevPilotMessageBundle;

import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class ChatShortcutHintCollector extends FactoryInlayHintsCollector {

Expand All @@ -51,56 +67,143 @@ public ChatShortcutHintCollector(@NotNull Editor editor, List<String> supportedE

@Override
public boolean collect(@NotNull PsiElement psiElement, @NotNull Editor editor, @NotNull InlayHintsSink inlayHintsSink) {
var chatShortcutSetting = ChatShortcutSettingState.getInstance();
if (!chatShortcutSetting.getEnable()) {
return true;
}

boolean isSourceCode = isSourceCode(editor);
var elementType = PsiUtilCore.getElementType(psiElement).toString();

if (ChatShortcutSettingState.getInstance().getEnable()) {

if ("CLASS".equals(elementType)) {
inlayHintsSink.addBlockElement(getAnchorOffset(psiElement), true, true, 1000,
factory.seq(factory.textSpacePlaceholder(computeInitialWhitespace(editor, psiElement), false),
factory.icon(DevPilotIcons.SYSTEM_ICON_INLAY),
buildClickableTextChatShortcutEntry(DevPilotMessageBundle.get("devpilot.inlay.shortcut.test"),
EditorActionEnum.GENERATE_TESTS, psiElement)));
} else if (supportedElementTypes.contains(elementType)) {
inlayHintsSink.addBlockElement(getAnchorOffset(psiElement), true, true, 1000,
factory.seq(factory.textSpacePlaceholder(computeInitialWhitespace(editor, psiElement), false),
factory.icon(DevPilotIcons.SYSTEM_ICON_INLAY),
buildClickableTextChatShortcutEntry(" " + DevPilotMessageBundle.get("devpilot.inlay.shortcut.explain")
+ " | ", EditorActionEnum.EXPLAIN_CODE, psiElement),
buildClickableTextChatShortcutEntry(DevPilotMessageBundle.get("devpilot.inlay.shortcut.fix")
+ " | ", EditorActionEnum.FIX_CODE, psiElement),
buildClickableTextChatShortcutEntry(DevPilotMessageBundle.get("devpilot.inlay.shortcut.inlineComment")
+ " | ", EditorActionEnum.GENERATE_COMMENTS, psiElement),
buildClickableMethodCommentsShortcutEntry(DevPilotMessageBundle.get("devpilot.inlay.shortcut.methodComments") +
" | ", psiElement),
buildClickableTextChatShortcutEntry(DevPilotMessageBundle.get("devpilot.inlay.shortcut.test"),
EditorActionEnum.GENERATE_TESTS, psiElement)));
if ("CLASS".equals(elementType)) {
if (isSourceCode) {
inlineRenderInlayPresentation(psiElement, editor, inlayHintsSink, Collections.singletonList(EditorActionEnum.GENERATE_TESTS));
}
return true;
}

if (!isSourceCode && supportedElementTypes.contains(elementType)) {
inlineRenderInlayPresentation(psiElement, editor, inlayHintsSink, Collections.singletonList(EditorActionEnum.EXPLAIN_CODE));
return true;
}

if (isSourceCode && supportedElementTypes.contains(elementType)) {
if (chatShortcutSetting.isInlineDisplay()) {
inlineRenderInlayPresentation(psiElement, editor, inlayHintsSink, buildInlayPresentationGroupData());
} else {
groupRenderPopupPresentation(inlayHintsSink, psiElement, editor);
}
}
return true;
}

private InlayPresentation buildClickableTextChatShortcutEntry(String text, EditorActionEnum actionEnum, PsiElement psiElement) {
return factory.seq(factory.referenceOnHover(factory.smallText(text), (mouseEvent, point) -> {
TextRange textRange = psiElement.getTextRange();
editor.getSelectionModel().setSelection(textRange.getStartOffset(), textRange.getEndOffset());
private InlayPresentation createInlayElement(Editor editor, PsiElement psiElement, List<EditorActionEnum> actions) {
List<InlayPresentation> presentations = new ArrayList<>();

service.handleActions(actionEnum, psiElement);
presentations.add(factory.textSpacePlaceholder(computeInitialWhitespace(editor, psiElement), false));
presentations.add(factory.icon(DevPilotIcons.SYSTEM_ICON_INLAY));

for (int i = 0, len = actions.size(); i < len; i++) {
String prefix = (i == 0) ? " " : StringUtils.EMPTY;
String postfix = (i == len - 1) ? StringUtils.EMPTY : " | ";
presentations.add(buildClickableInlayPresentation(prefix, postfix, actions.get(i), psiElement));
}

return factory.seq(presentations.toArray(new InlayPresentation[0]));
}

private void inlineRenderInlayPresentation(PsiElement psiElement, @NotNull Editor editor, @NotNull InlayHintsSink inlayHintsSink, List<EditorActionEnum> actions) {
inlayHintsSink.addBlockElement(getAnchorOffset(psiElement),
true,
true,
1000,
createInlayElement(editor, psiElement, actions));
}

private void groupRenderPopupPresentation(InlayHintsSink inlayHintsSink, PsiElement psiElement, Editor editor) {
PresentationFactory factory = getFactory();
Document document = editor.getDocument();
int offset = getAnchorOffset(psiElement);
int line = document.getLineNumber(offset);
int startOffset = document.getLineStartOffset(line);

InlayPresentation finalPresentation = createPopupPresentation(factory, editor, psiElement, startOffset, offset);
inlayHintsSink.addBlockElement(startOffset, true, true, 300, finalPresentation);
}

private InlayPresentation createPopupPresentation(PresentationFactory factory, Editor editor,
PsiElement psiElement, int startOffset, int offset) {
String linePrefix = editor.getDocument().getText(new TextRange(startOffset, offset));
int column = offset - startOffset + findOffsetBySpace(editor, linePrefix);

List<InlayPresentation> presentations = new SmartList<>();
presentations.add(factory.textSpacePlaceholder(column, true));
presentations.add(factory.smallScaledIcon(DevPilotIcons.SYSTEM_ICON_INLAY));
presentations.add(factory.smallScaledIcon(AllIcons.Actions.FindAndShowNextMatchesSmall));
presentations.add(factory.textSpacePlaceholder(1, true));

SequencePresentation shiftedPresentation = new SequencePresentation(presentations);

return factory.referenceOnHover(shiftedPresentation, (event, translated) ->
showPopup(editor, psiElement, event)
);
}

private void showPopup(Editor editor, PsiElement psiElement, MouseEvent event) {
List<EditorActionEnum> options = buildInlayPresentationGroupData();
JBPopup popup = JBPopupFactory.getInstance().createListPopup(new BaseListPopupStep<EditorActionEnum>(StringUtils.EMPTY, options) {
public @NotNull String getTextFor(EditorActionEnum value) {
return DevPilotMessageBundle.get(value.getInlayLabel());
}

public @Nullable PopupStep<?> onChosen(EditorActionEnum selectedValue, boolean finalChoice) {
handleActionCallback(selectedValue, psiElement);
return FINAL_CHOICE;
}
});
popup.showInScreenCoordinates(editor.getComponent(), event.getLocationOnScreen());
}

private List<EditorActionEnum> buildInlayPresentationGroupData() {
List<EditorActionEnum> options = new ArrayList<>();
options.add(EditorActionEnum.EXPLAIN_CODE);
options.add(EditorActionEnum.FIX_CODE);
options.add(EditorActionEnum.GENERATE_COMMENTS);
options.add(EditorActionEnum.COMMENT_METHOD);
options.add(EditorActionEnum.GENERATE_TESTS);
return options;
}

public static boolean isSourceCode(Editor editor) {
Document document = editor.getDocument();
VirtualFile file = FileDocumentManager.getInstance().getFile(document);
if (file == null || !file.isWritable()) {
return false;
}
FileType fileType = FileTypeManager.getInstance().getFileTypeByFileName(file.getName());
return !fileType.isBinary() && !document.getText().trim().isEmpty();
}

private InlayPresentation buildClickableInlayPresentation(String displayPrefixText, String displaySuffixText, EditorActionEnum actionEnum, PsiElement psiElement) {
return factory.seq(factory.referenceOnHover(factory.smallText(displayPrefixText + DevPilotMessageBundle.get(actionEnum.getInlayLabel()) + displaySuffixText), (mouseEvent, point) -> {
handleActionCallback(actionEnum, psiElement);
}));
}

private InlayPresentation buildClickableMethodCommentsShortcutEntry(String text, PsiElement psiElement) {
return factory.seq(factory.referenceOnHover(factory.smallText(text), (mouseEvent, point) -> {
TextRange textRange = psiElement.getTextRange();
editor.getSelectionModel().setSelection(textRange.getStartOffset(), textRange.getEndOffset());
private void handleActionCallback(EditorActionEnum actionEnum, PsiElement psiElement) {
TextRange textRange = psiElement.getTextRange();
editor.getSelectionModel().setSelection(textRange.getStartOffset(), textRange.getEndOffset());

if (EditorActionEnum.COMMENT_METHOD.equals(actionEnum)) {
ApplicationManager.getApplication().invokeLater(() -> {
moveCareToPreviousLineStart(editor, textRange.getStartOffset());
AnAction action = ActionManager.getInstance().getAction("com.zhongan.devpilot.actions.editor.generate.method.comments");
DataContext context = SimpleDataContext.getProjectContext(editor.getProject());
action.actionPerformed(new AnActionEvent(null, context, "", new Presentation(), ActionManager.getInstance(), 0));
});
}));
} else {
service.handleActions(actionEnum, psiElement);
}
}

private static int getAnchorOffset(@NotNull PsiElement psiElement) {
Expand All @@ -115,10 +218,22 @@ private static int getAnchorOffset(@NotNull PsiElement psiElement) {
return anchorOffset;
}

private int findOffsetBySpace(@NotNull Editor editor, String linePrefix) {
int tabWidth = editor.getSettings().getTabSize(editor.getProject());
int totalOffset = 0;

for (int i = 0; i < linePrefix.length(); ++i) {
if (linePrefix.charAt(i) == '\t') {
totalOffset += tabWidth;
}
}
return totalOffset;
}

private int computeInitialWhitespace(Editor editor, PsiElement psiElement) {
int lineNum = editor.getDocument().getLineNumber(psiElement.getTextRange().getStartOffset());
String textOnLine = editor.getDocument().getText(new TextRange(editor.getDocument().getLineStartOffset(lineNum),
editor.getDocument().getLineEndOffset(lineNum)));
editor.getDocument().getLineEndOffset(lineNum)));

int whitespaceCounter = 0;
for (char character : textOnLine.toCharArray()) {
Expand Down
21 changes: 14 additions & 7 deletions src/main/java/com/zhongan/devpilot/enums/EditorActionEnum.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,27 @@

public enum EditorActionEnum {

GENERATE_COMMENTS("devpilot.action.generate.comments", "Generate comments in the following code"),
GENERATE_COMMENTS("devpilot.action.generate.comments", "devpilot.inlay.shortcut.inlineComment", "Generate comments in the following code"),

COMMENT_METHOD("devpilot.action.generate.method.comments", ""),
COMMENT_METHOD("devpilot.action.generate.method.comments", "devpilot.inlay.shortcut.methodComments", ""),

GENERATE_TESTS("devpilot.action.generate.tests", "Generate Tests in the following code"),
GENERATE_TESTS("devpilot.action.generate.tests", "devpilot.inlay.shortcut.test", "Generate Tests in the following code"),

FIX_CODE("devpilot.action.fix", "Fix This in the following code"),
FIX_CODE("devpilot.action.fix", "devpilot.inlay.shortcut.fix", "Fix This in the following code"),

EXPLAIN_CODE("devpilot.action.explain", "Explain this in the following code"),
EXPLAIN_CODE("devpilot.action.explain", "devpilot.inlay.shortcut.explain", "Explain this in the following code"),

COMPLETE_CODE("devpilot.action.completions", "code completions");
COMPLETE_CODE("devpilot.action.completions", "", "code completions");

private final String label;

private final String inlayLabel;

private final String userMessage;

EditorActionEnum(String label, String userMessage) {
EditorActionEnum(String label, String inlayLabel, String userMessage) {
this.label = label;
this.inlayLabel = inlayLabel;
this.userMessage = userMessage;
}

Expand All @@ -41,6 +44,10 @@ public String getLabel() {
return label;
}

public String getInlayLabel() {
return inlayLabel;
}

public String getUserMessage() {
return userMessage;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UI;
import com.zhongan.devpilot.settings.state.AvailabilityCheck;
import com.zhongan.devpilot.settings.state.ChatShortcutSettingState;
import com.zhongan.devpilot.settings.state.CompletionSettingsState;
import com.zhongan.devpilot.settings.state.DevPilotLlmSettingsState;
import com.zhongan.devpilot.settings.state.LanguageSettingsState;
import com.zhongan.devpilot.util.DevPilotMessageBundle;

import javax.swing.JComponent;
import javax.swing.JPanel;

import org.jetbrains.annotations.NotNull;

public class DevPilotSettingsComponent {

private final JPanel mainPanel;
Expand All @@ -27,6 +31,8 @@ public class DevPilotSettingsComponent {

private Integer index;

private Integer methodInlayPresentationDisplayIndex;

public DevPilotSettingsComponent(DevPilotSettingsConfigurable devPilotSettingsConfigurable, DevPilotLlmSettingsState settings) {
fullNameField = new JBTextField(settings.getFullName(), 20);

Expand All @@ -41,12 +47,16 @@ public DevPilotSettingsComponent(DevPilotSettingsConfigurable devPilotSettingsCo
statusCheckRadio = new JBRadioButton(DevPilotMessageBundle.get("devpilot.settings.service.status.check.enable.desc"),
AvailabilityCheck.getInstance().getEnable());

methodInlayPresentationDisplayIndex = ChatShortcutSettingState.getInstance().getDisplayIndex();
Integer inlayPresentationDisplayIndex = ChatShortcutSettingState.getInstance().getDisplayIndex();

mainPanel = FormBuilder.createFormBuilder()
.addComponent(UI.PanelFactory.panel(fullNameField)
.withLabel(DevPilotMessageBundle.get("devpilot.setting.displayNameFieldLabel"))
.resizeX(false)
.createPanel())
.addComponent(createLanguageSectionPanel(languageIndex))
.addComponent(createMethodShortcutDisplayModeSectionPanel(inlayPresentationDisplayIndex))
.addComponent(new TitledSeparator(
DevPilotMessageBundle.get("devpilot.settings.service.code.completion.title")))
.addComponent(autoCompletionRadio)
Expand All @@ -59,6 +69,27 @@ public DevPilotSettingsComponent(DevPilotSettingsConfigurable devPilotSettingsCo
.getPanel();
}

private @NotNull JComponent createMethodShortcutDisplayModeSectionPanel(Integer inlayPresentationDisplayIndex) {
var comboBox = new ComboBox<>();
comboBox.addItem(DevPilotMessageBundle.get("devpilot.settings.methodShortcutHidden"));
comboBox.addItem(DevPilotMessageBundle.get("devpilot.settings.methodShortcutInlineDisplay"));
comboBox.addItem(DevPilotMessageBundle.get("devpilot.settings.methodShortcutGroupDisplay"));
comboBox.setSelectedIndex(inlayPresentationDisplayIndex);

comboBox.addActionListener(e -> {
var box = (ComboBox<?>) e.getSource();
methodInlayPresentationDisplayIndex = box.getSelectedIndex();
});

var panel = UI.PanelFactory.grid()
.add(UI.PanelFactory.panel(comboBox)
.withLabel(DevPilotMessageBundle.get("devpilot.settings.methodShortcutDisplayModeLabel"))
.resizeX(false))
.createPanel();
panel.setBorder(JBUI.Borders.emptyLeft(0));
return panel;
}

public JPanel createLanguageSectionPanel(Integer languageIndex) {
var comboBox = new ComboBox<>();
comboBox.addItem("English");
Expand Down Expand Up @@ -92,6 +123,10 @@ public Integer getLanguageIndex() {
return index;
}

public Integer getMethodInlayPresentationDisplayIndex() {
return methodInlayPresentationDisplayIndex;
}

public boolean getCompletionEnabled() {
return autoCompletionRadio.isSelected();
}
Expand Down
Loading

0 comments on commit 7e283ae

Please sign in to comment.