mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-20 01:02:02 +00:00
feat: quick link navigation
This commit is contained in:
parent
eb6cd0ffe2
commit
6c3f19b131
31 changed files with 587 additions and 159 deletions
|
|
@ -4,11 +4,14 @@ import static ee.carlrobert.codegpt.ui.UIUtil.createScrollPaneWithSmartScroller;
|
|||
|
||||
import com.intellij.openapi.Disposable;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.application.ModalityState;
|
||||
import com.intellij.openapi.application.ReadAction;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.editor.SelectionModel;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.ui.JBColor;
|
||||
import com.intellij.util.concurrency.AppExecutorUtil;
|
||||
import com.intellij.util.ui.JBUI;
|
||||
import ee.carlrobert.codegpt.CodeGPTKeys;
|
||||
import ee.carlrobert.codegpt.ReferencedFile;
|
||||
|
|
@ -178,6 +181,7 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
.referencedFiles(getReferencedFiles(selectedTags))
|
||||
.history(getHistory(getSelectedTags()))
|
||||
.psiStructure(psiStructure)
|
||||
.project(project)
|
||||
.chatMode(userInputPanel.getChatMode());
|
||||
|
||||
findTagOfType(selectedTags, PersonaTagDetails.class)
|
||||
|
|
@ -286,8 +290,15 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
|
||||
public void includeFiles(List<VirtualFile> referencedFiles) {
|
||||
userInputPanel.includeFiles(referencedFiles);
|
||||
totalTokensPanel.updateReferencedFilesTokens(
|
||||
referencedFiles.stream().map(it -> ReferencedFile.from(it).fileContent()).toList());
|
||||
ReadAction.nonBlocking(() ->
|
||||
referencedFiles.stream()
|
||||
.map(it -> ReferencedFile.from(it).fileContent())
|
||||
.toList()
|
||||
)
|
||||
.inSmartMode(project)
|
||||
.expireWith(project)
|
||||
.finishOnUiThread(ModalityState.any(), totalTokensPanel::updateReferencedFilesTokens)
|
||||
.submit(AppExecutorUtil.getAppExecutorService());
|
||||
}
|
||||
|
||||
private boolean hasReferencedFilePaths(Message message) {
|
||||
|
|
@ -498,6 +509,7 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
userMessagePanel.addReloadAction(() -> reloadMessage(
|
||||
ChatCompletionParameters.builder(conversation, message)
|
||||
.conversationType(ConversationType.DEFAULT)
|
||||
.project(project)
|
||||
.chatMode(userInputPanel.getChatMode())
|
||||
.build(),
|
||||
userMessagePanel));
|
||||
|
|
|
|||
|
|
@ -13,17 +13,21 @@ import com.intellij.openapi.actionSystem.AnAction;
|
|||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.application.ModalityState;
|
||||
import com.intellij.openapi.application.ReadAction;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
||||
import com.intellij.openapi.options.ShowSettingsUtil;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.VerticalFlowLayout;
|
||||
import com.intellij.openapi.util.Disposer;
|
||||
import com.intellij.openapi.util.io.FileUtil;
|
||||
import com.intellij.openapi.vfs.LocalFileSystem;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.ui.AnimatedIcon;
|
||||
import com.intellij.ui.PopupHandler;
|
||||
import com.intellij.ui.components.JBLabel;
|
||||
import com.intellij.util.concurrency.AppExecutorUtil;
|
||||
import com.intellij.util.ui.JBUI;
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel;
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle;
|
||||
|
|
@ -58,6 +62,7 @@ import ee.carlrobert.codegpt.toolwindow.ui.WebpageList;
|
|||
import ee.carlrobert.codegpt.ui.ThoughtProcessPanel;
|
||||
import ee.carlrobert.codegpt.ui.UIUtil;
|
||||
import ee.carlrobert.codegpt.util.EditorUtil;
|
||||
import ee.carlrobert.codegpt.util.MarkdownUtil;
|
||||
import java.awt.BorderLayout;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
|
|
|||
|
|
@ -3,16 +3,20 @@ package ee.carlrobert.codegpt.ui;
|
|||
import static javax.swing.event.HyperlinkEvent.EventType.ACTIVATED;
|
||||
|
||||
import com.intellij.ide.BrowserUtil;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
|
||||
import com.intellij.openapi.ui.panel.ComponentPanelBuilder;
|
||||
import com.intellij.ui.JBColor;
|
||||
import com.intellij.ui.ScrollPaneFactory;
|
||||
import com.intellij.ui.components.JBRadioButton;
|
||||
import com.intellij.ui.components.JBTextArea;
|
||||
import com.intellij.util.ui.HTMLEditorKitBuilder;
|
||||
import com.intellij.util.ui.JBUI;
|
||||
import com.intellij.util.ui.UI;
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle;
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ui.SmartScroller;
|
||||
import ee.carlrobert.codegpt.util.PsiLinkNavigator;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.CardLayout;
|
||||
import java.awt.Component;
|
||||
|
|
@ -43,6 +47,8 @@ import javax.swing.text.DefaultCaret;
|
|||
|
||||
public class UIUtil {
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(UIUtil.class);
|
||||
|
||||
public static JTextPane createTextPane(String text) {
|
||||
return createTextPane(text, true);
|
||||
}
|
||||
|
|
@ -54,6 +60,7 @@ public class UIUtil {
|
|||
public static JTextPane createTextPane(String text, boolean opaque, HyperlinkListener listener) {
|
||||
var textPane = new JTextPane();
|
||||
textPane.putClientProperty(JTextPane.HONOR_DISPLAY_PROPERTIES, true);
|
||||
textPane.setEditorKit(HTMLEditorKitBuilder.simple());
|
||||
textPane.addHyperlinkListener(listener);
|
||||
textPane.setContentType("text/html");
|
||||
textPane.setEditable(false);
|
||||
|
|
@ -103,12 +110,23 @@ public class UIUtil {
|
|||
}
|
||||
|
||||
public static void handleHyperlinkClicked(HyperlinkEvent event) {
|
||||
if (!ACTIVATED.equals(event.getEventType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
String desc = event.getDescription();
|
||||
if (desc != null && PsiLinkNavigator.isValidNavigationLink(desc)) {
|
||||
ApplicationManager.getApplication()
|
||||
.executeOnPooledThread(() -> PsiLinkNavigator.handle(desc));
|
||||
return;
|
||||
}
|
||||
|
||||
var url = event.getURL();
|
||||
if (ACTIVATED.equals(event.getEventType()) && url != null) {
|
||||
if (url != null) {
|
||||
try {
|
||||
BrowserUtil.browse(url.toURI());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
LOG.warn("Failed to browse URL: " + url, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -119,7 +137,6 @@ public class UIUtil {
|
|||
textArea.getActionMap().put("text-submit", onSubmit);
|
||||
}
|
||||
|
||||
|
||||
public static JPanel createRadioButtonsPanel(List<JBRadioButton> radioButtons) {
|
||||
var buttonGroup = new ButtonGroup();
|
||||
var radioPanel = new JPanel();
|
||||
|
|
|
|||
|
|
@ -116,19 +116,20 @@ class CodeCompletionEventListener(
|
|||
}
|
||||
|
||||
if (firstLineSent.get() && firstLine != null) {
|
||||
val remainingContent = finalResult.removePrefix(firstLine!!).toString()
|
||||
val first = firstLine ?: return
|
||||
val remainingContent = finalResult.removePrefix(first).toString()
|
||||
if (remainingContent.trim().isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val parsedContent = parseOutput(firstLine + remainingContent)
|
||||
val parsedContent = parseOutput(first + remainingContent)
|
||||
if (parsedContent.isNotEmpty()) {
|
||||
cache?.setCache(prefix, suffix, parsedContent)
|
||||
|
||||
CodeGPTKeys.REMAINING_CODE_COMPLETION.set(
|
||||
editor,
|
||||
PartialCodeCompletionResponse.newBuilder()
|
||||
.setPartialCompletion(parsedContent.removePrefix(firstLine ?: ""))
|
||||
.setPartialCompletion(parsedContent.removePrefix(first))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
|
@ -200,4 +201,4 @@ class CodeCompletionEventListener(
|
|||
.parse(prefix, suffix, input)
|
||||
.trimEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package ee.carlrobert.codegpt.completions
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import ee.carlrobert.codegpt.ReferencedFile
|
||||
import ee.carlrobert.codegpt.conversations.Conversation
|
||||
|
|
@ -26,6 +27,7 @@ class ChatCompletionParameters private constructor(
|
|||
var referencedFiles: List<ReferencedFile>?,
|
||||
var personaDetails: PersonaDetails?,
|
||||
var psiStructure: Set<ClassStructure>?,
|
||||
var project: Project?,
|
||||
var chatMode: ChatMode = ChatMode.ASK,
|
||||
var featureType: FeatureType = FeatureType.CHAT
|
||||
) : CompletionParameters {
|
||||
|
|
@ -39,6 +41,7 @@ class ChatCompletionParameters private constructor(
|
|||
referencedFiles(this@ChatCompletionParameters.referencedFiles)
|
||||
personaDetails(this@ChatCompletionParameters.personaDetails)
|
||||
psiStructure(this@ChatCompletionParameters.psiStructure)
|
||||
project(this@ChatCompletionParameters.project)
|
||||
chatMode(this@ChatCompletionParameters.chatMode)
|
||||
featureType(this@ChatCompletionParameters.featureType)
|
||||
}
|
||||
|
|
@ -54,6 +57,7 @@ class ChatCompletionParameters private constructor(
|
|||
private var personaDetails: PersonaDetails? = null
|
||||
private var psiStructure: Set<ClassStructure>? = null
|
||||
private var gitDiff: String = ""
|
||||
private var project: Project? = null
|
||||
private var chatMode: ChatMode = ChatMode.ASK
|
||||
private var featureType: FeatureType = FeatureType.CHAT
|
||||
|
||||
|
|
@ -83,6 +87,8 @@ class ChatCompletionParameters private constructor(
|
|||
|
||||
fun psiStructure(psiStructure: Set<ClassStructure>?) = apply { this.psiStructure = psiStructure }
|
||||
|
||||
fun project(project: Project?) = apply { this.project = project }
|
||||
|
||||
fun chatMode(chatMode: ChatMode) = apply { this.chatMode = chatMode }
|
||||
|
||||
fun featureType(featureType: FeatureType) = apply { this.featureType = featureType }
|
||||
|
|
@ -99,6 +105,7 @@ class ChatCompletionParameters private constructor(
|
|||
referencedFiles,
|
||||
personaDetails,
|
||||
psiStructure,
|
||||
project,
|
||||
chatMode,
|
||||
featureType
|
||||
)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ object CompletionRequestUtil {
|
|||
fun getPromptWithContext(
|
||||
referencedFiles: List<ReferencedFile>,
|
||||
userPrompt: String?,
|
||||
psiStructure: Set<ClassStructure>?
|
||||
psiStructure: Set<ClassStructure>?,
|
||||
): String {
|
||||
val includedFilesSettings = service<IncludedFilesSettings>().state
|
||||
val repeatableContext = includedFilesSettings.repeatableContext
|
||||
|
|
@ -77,7 +77,10 @@ object CompletionRequestUtil {
|
|||
}
|
||||
|
||||
return includedFilesSettings.promptTemplate
|
||||
.replace("{REPEATABLE_CONTEXT}", fileContext + structureContext.orEmpty())
|
||||
.replace("{REPEATABLE_CONTEXT}", buildString {
|
||||
append(fileContext)
|
||||
append(structureContext.orEmpty())
|
||||
})
|
||||
.replace("{QUESTION}", userPrompt!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import com.intellij.openapi.components.service
|
|||
import ee.carlrobert.codegpt.completions.BaseRequestFactory
|
||||
import ee.carlrobert.codegpt.completions.ChatCompletionParameters
|
||||
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
|
||||
import ee.carlrobert.codegpt.settings.models.ModelSettings
|
||||
import ee.carlrobert.codegpt.settings.prompts.FilteredPromptsService
|
||||
import ee.carlrobert.codegpt.settings.prompts.PromptsSettings
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
|
|
@ -22,7 +21,8 @@ class ClaudeRequestFactory : BaseRequestFactory() {
|
|||
|
||||
val selectedPersona = service<PromptsSettings>().state.personas.selectedPersona
|
||||
if (!selectedPersona.disabled) {
|
||||
system = service<FilteredPromptsService>().getFilteredPersonaPrompt(params.chatMode)
|
||||
val base = service<FilteredPromptsService>().getFilteredPersonaPrompt(params.chatMode)
|
||||
system = service<FilteredPromptsService>().applyClickableLinks(base)
|
||||
}
|
||||
|
||||
messages = params.conversation.messages
|
||||
|
|
|
|||
|
|
@ -10,10 +10,8 @@ import ee.carlrobert.codegpt.conversations.ConversationsState
|
|||
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
|
||||
import ee.carlrobert.codegpt.settings.prompts.FilteredPromptsService
|
||||
import ee.carlrobert.codegpt.settings.prompts.PromptsSettings
|
||||
import ee.carlrobert.codegpt.settings.service.google.GoogleSettings
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
import ee.carlrobert.codegpt.settings.service.ModelSelectionService
|
||||
import ee.carlrobert.codegpt.settings.models.ModelSettings
|
||||
import ee.carlrobert.codegpt.util.file.FileUtil
|
||||
import ee.carlrobert.llm.client.google.completion.GoogleCompletionContent
|
||||
import ee.carlrobert.llm.client.google.completion.GoogleCompletionRequest
|
||||
|
|
@ -130,15 +128,16 @@ class GoogleRequestFactory : BaseRequestFactory() {
|
|||
messages.add(GoogleCompletionContent("model", listOf(prevMessage.response)))
|
||||
}
|
||||
|
||||
if (params.imageDetails != null) {
|
||||
val imageDetails = params.imageDetails
|
||||
if (imageDetails != null) {
|
||||
messages.add(
|
||||
GoogleCompletionContent(
|
||||
listOf(
|
||||
GoogleContentPart(
|
||||
null,
|
||||
GoogleContentPart.Blob(
|
||||
params.imageDetails!!.mediaType,
|
||||
params.imageDetails!!.data
|
||||
imageDetails.mediaType,
|
||||
imageDetails.data
|
||||
)
|
||||
),
|
||||
GoogleContentPart(message.prompt)
|
||||
|
|
@ -192,16 +191,17 @@ class GoogleRequestFactory : BaseRequestFactory() {
|
|||
return when (params.conversationType) {
|
||||
ConversationType.DEFAULT -> {
|
||||
val selectedPersona = service<PromptsSettings>().state.personas.selectedPersona
|
||||
return if (!selectedPersona.disabled) {
|
||||
service<FilteredPromptsService>().getFilteredPersonaPrompt(params.chatMode)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (!selectedPersona.disabled) {
|
||||
val base = service<FilteredPromptsService>().getFilteredPersonaPrompt(params.chatMode)
|
||||
service<FilteredPromptsService>().applyClickableLinks(base)
|
||||
} else null
|
||||
}
|
||||
|
||||
ConversationType.FIX_COMPILE_ERRORS -> service<PromptsSettings>().state.coreActions.fixCompileErrors.instructions
|
||||
ConversationType.FIX_COMPILE_ERRORS -> service<FilteredPromptsService>()
|
||||
.applyClickableLinks(service<PromptsSettings>().state.coreActions.fixCompileErrors.instructions.orEmpty())
|
||||
|
||||
ConversationType.REVIEW_CHANGES -> service<PromptsSettings>().state.coreActions.reviewChanges.instructions
|
||||
ConversationType.REVIEW_CHANGES -> service<FilteredPromptsService>()
|
||||
.applyClickableLinks(service<PromptsSettings>().state.coreActions.reviewChanges.instructions.orEmpty())
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,14 +13,13 @@ import ee.carlrobert.codegpt.settings.prompts.PromptsSettings
|
|||
import ee.carlrobert.codegpt.settings.prompts.addProjectPath
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings
|
||||
import ee.carlrobert.codegpt.conversations.message.Message
|
||||
import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest
|
||||
|
||||
class LlamaRequestFactory : BaseRequestFactory() {
|
||||
|
||||
override fun createChatRequest(params: ChatCompletionParameters): LlamaCompletionRequest {
|
||||
val promptTemplate = getPromptTemplate()
|
||||
val systemPrompt =
|
||||
var systemPrompt =
|
||||
if (params.conversationType == ConversationType.FIX_COMPILE_ERRORS) {
|
||||
service<PromptsSettings>().state.coreActions.fixCompileErrors.instructions
|
||||
} else {
|
||||
|
|
@ -30,6 +29,7 @@ class LlamaRequestFactory : BaseRequestFactory() {
|
|||
).addProjectPath()
|
||||
}
|
||||
}
|
||||
systemPrompt = systemPrompt?.let { service<FilteredPromptsService>().applyClickableLinks(it) }
|
||||
|
||||
val prompt = promptTemplate.buildPrompt(
|
||||
systemPrompt,
|
||||
|
|
|
|||
|
|
@ -253,9 +253,12 @@ class OpenAIRequestFactory : BaseRequestFactory() {
|
|||
val selectedPersona = service<PromptsSettings>().state.personas.selectedPersona
|
||||
if (callParameters.conversationType == ConversationType.DEFAULT && !selectedPersona.disabled) {
|
||||
val sessionPersonaDetails = callParameters.personaDetails
|
||||
val instructions = sessionPersonaDetails?.instructions?.addProjectPath()
|
||||
?: service<FilteredPromptsService>().getFilteredPersonaPrompt(callParameters.chatMode)
|
||||
val baseInstructions = sessionPersonaDetails?.instructions?.addProjectPath()
|
||||
?: service<FilteredPromptsService>()
|
||||
.getFilteredPersonaPrompt(callParameters.chatMode)
|
||||
.addProjectPath()
|
||||
val instructions = service<FilteredPromptsService>()
|
||||
.applyClickableLinks(baseInstructions)
|
||||
val history = if (conversationsHistory.isNullOrEmpty()) {
|
||||
""
|
||||
} else {
|
||||
|
|
@ -265,29 +268,22 @@ class OpenAIRequestFactory : BaseRequestFactory() {
|
|||
}
|
||||
|
||||
if (instructions.isNotEmpty()) {
|
||||
val content = if (history.isBlank()) instructions else instructions.trimEnd() + "\n" + history
|
||||
messages.add(
|
||||
OpenAIChatCompletionStandardMessage(
|
||||
role,
|
||||
instructions + "\n" + history
|
||||
content
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (callParameters.conversationType == ConversationType.REVIEW_CHANGES) {
|
||||
messages.add(
|
||||
OpenAIChatCompletionStandardMessage(
|
||||
role,
|
||||
service<PromptsSettings>().state.coreActions.reviewChanges.instructions
|
||||
)
|
||||
)
|
||||
val base = service<PromptsSettings>().state.coreActions.reviewChanges.instructions
|
||||
messages.add(OpenAIChatCompletionStandardMessage(role, base))
|
||||
}
|
||||
if (callParameters.conversationType == ConversationType.FIX_COMPILE_ERRORS) {
|
||||
messages.add(
|
||||
OpenAIChatCompletionStandardMessage(
|
||||
role,
|
||||
service<PromptsSettings>().state.coreActions.fixCompileErrors.instructions
|
||||
)
|
||||
)
|
||||
val base = service<PromptsSettings>().state.coreActions.fixCompileErrors.instructions
|
||||
messages.add(OpenAIChatCompletionStandardMessage(role, base))
|
||||
}
|
||||
|
||||
for (prevMessage in callParameters.conversation.messages) {
|
||||
|
|
@ -333,15 +329,16 @@ class OpenAIRequestFactory : BaseRequestFactory() {
|
|||
)
|
||||
}
|
||||
|
||||
if (callParameters.imageDetails != null) {
|
||||
val imageDetails = callParameters.imageDetails
|
||||
if (imageDetails != null) {
|
||||
messages.add(
|
||||
OpenAIChatCompletionDetailedMessage(
|
||||
"user",
|
||||
listOf(
|
||||
OpenAIMessageImageURLContent(
|
||||
OpenAIImageUrl(
|
||||
callParameters.imageDetails!!.mediaType,
|
||||
callParameters.imageDetails!!.data
|
||||
imageDetails.mediaType,
|
||||
imageDetails.data
|
||||
)
|
||||
),
|
||||
OpenAIMessageTextContent(message.prompt)
|
||||
|
|
@ -355,7 +352,7 @@ class OpenAIRequestFactory : BaseRequestFactory() {
|
|||
CompletionRequestUtil.getPromptWithContext(
|
||||
referencedFiles,
|
||||
message.prompt,
|
||||
psiStructure
|
||||
psiStructure,
|
||||
)
|
||||
}
|
||||
messages.add(OpenAIChatCompletionStandardMessage("user", prompt))
|
||||
|
|
|
|||
|
|
@ -66,12 +66,12 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
|
|||
private var isUpdatingInlaySize = false
|
||||
private var resizeTimer: Timer? = null
|
||||
|
||||
private val project = editor.project!!
|
||||
private val project = requireNotNull(editor.project) { "Editor project is null" }
|
||||
private var inlayDisposable: Disposable? = null
|
||||
|
||||
private val psiStructureRepository = PsiStructureRepository(
|
||||
this,
|
||||
editor.project!!,
|
||||
project,
|
||||
tagManager,
|
||||
PsiStructureProvider(),
|
||||
CoroutineDispatchers()
|
||||
|
|
@ -85,7 +85,7 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
|
|||
)
|
||||
|
||||
private val userInputPanel = UserInputPanel(
|
||||
project = editor.project!!,
|
||||
project = project,
|
||||
totalTokensPanel = dummyTokensPanel,
|
||||
parentDisposable = this,
|
||||
featureType = FeatureType.INLINE_EDIT,
|
||||
|
|
@ -497,7 +497,7 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
|
|||
try {
|
||||
val refs = collectSelectedReferencedFiles()
|
||||
val diff = try {
|
||||
GitUtil.getCurrentChanges(editor.project!!)
|
||||
GitUtil.getCurrentChanges(project)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ class KotlinFileAnalyzer(
|
|||
val parameters = constructor.valueParameters.map { parameter ->
|
||||
val type = parameter.typeReference?.text ?: "TypeUnknown"
|
||||
val resolvedType = resolveType(type)
|
||||
ParameterInfo(parameter.name!!, resolvedType, getModifiers(parameter))
|
||||
ParameterInfo(parameter.name.orEmpty(), resolvedType, getModifiers(parameter))
|
||||
}
|
||||
val modifierList = getModifiers(constructor)
|
||||
return ConstructorStructure(parameters, modifierList)
|
||||
|
|
@ -260,10 +260,10 @@ class KotlinFileAnalyzer(
|
|||
val parameters = function.valueParameters.map { parameter ->
|
||||
val type = parameter.typeReference?.text ?: "TypeUnknown"
|
||||
val resolvedType = resolveType(type)
|
||||
ParameterInfo(parameter.name!!, resolvedType, getModifiers(parameter))
|
||||
ParameterInfo(parameter.name.orEmpty(), resolvedType, getModifiers(parameter))
|
||||
}
|
||||
val modifierList = getModifiers(function)
|
||||
return MethodStructure(function.name!!, resolvedReturnType, parameters, modifierList)
|
||||
return MethodStructure(function.name.orEmpty(), resolvedReturnType, parameters, modifierList)
|
||||
}
|
||||
|
||||
private fun resolveType(shortType: String): ClassName {
|
||||
|
|
@ -327,4 +327,4 @@ class KotlinFileAnalyzer(
|
|||
psiFileQueue.put(psiFile, ktFile.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import com.intellij.openapi.components.service
|
|||
import com.intellij.openapi.ui.DialogPanel
|
||||
import com.intellij.ui.PortField
|
||||
import com.intellij.ui.components.JBCheckBox
|
||||
import com.intellij.ui.components.JBTextField
|
||||
import com.intellij.ui.components.fields.IntegerField
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.util.ui.JBUI
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
|
|
@ -26,8 +24,17 @@ class ChatCompletionConfigurationForm {
|
|||
number = service<ConfigurationSettings>().state.chatCompletionSettings.psiStructureAnalyzeDepth
|
||||
}
|
||||
|
||||
private val clickableLinksCheckBox = JBCheckBox(
|
||||
CodeGPTBundle.get("configurationConfigurable.section.chatCompletion.clickableLinks.title"),
|
||||
service<ConfigurationSettings>().state.chatCompletionSettings.clickableLinksEnabled
|
||||
)
|
||||
|
||||
fun createPanel(): DialogPanel {
|
||||
return panel {
|
||||
row {
|
||||
cell(clickableLinksCheckBox)
|
||||
.comment(CodeGPTBundle.get("configurationConfigurable.section.chatCompletion.clickableLinks.description"))
|
||||
}
|
||||
row {
|
||||
cell(editorContextTagCheckBox)
|
||||
.comment(CodeGPTBundle.get("configurationConfigurable.section.chatCompletion.editorContextTag.description"))
|
||||
|
|
@ -50,6 +57,7 @@ class ChatCompletionConfigurationForm {
|
|||
editorContextTagCheckBox.isSelected = prevState.editorContextTagEnabled
|
||||
psiStructureCheckBox.isSelected = prevState.psiStructureEnabled
|
||||
psiStructureAnalyzeDepthField.number = prevState.psiStructureAnalyzeDepth
|
||||
clickableLinksCheckBox.isSelected = prevState.clickableLinksEnabled
|
||||
}
|
||||
|
||||
fun getFormState(): ChatCompletionSettingsState {
|
||||
|
|
@ -57,6 +65,7 @@ class ChatCompletionConfigurationForm {
|
|||
this.editorContextTagEnabled = editorContextTagCheckBox.isSelected
|
||||
this.psiStructureEnabled = psiStructureCheckBox.isSelected
|
||||
this.psiStructureAnalyzeDepth = psiStructureAnalyzeDepthField.number
|
||||
this.clickableLinksEnabled = clickableLinksCheckBox.isSelected
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class ChatCompletionSettingsState : BaseState() {
|
|||
var editorContextTagEnabled by property(true)
|
||||
var psiStructureEnabled by property(true)
|
||||
var psiStructureAnalyzeDepth by property(3)
|
||||
var clickableLinksEnabled by property(true)
|
||||
}
|
||||
|
||||
class CodeCompletionSettingsState : BaseState() {
|
||||
|
|
@ -57,4 +58,4 @@ class CodeCompletionSettingsState : BaseState() {
|
|||
var collectDependencyStructure by property(true)
|
||||
var contextAwareEnabled by property(false)
|
||||
var psiStructureAnalyzeDepth by property(2)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.intellij.openapi.components.Service
|
|||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import ee.carlrobert.codegpt.settings.configuration.ChatMode
|
||||
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
|
||||
import ee.carlrobert.codegpt.util.file.FileUtil.getResourceContent
|
||||
|
||||
/**
|
||||
|
|
@ -25,17 +26,15 @@ class FilteredPromptsService {
|
|||
|
||||
fun getFilteredPersonaPrompt(chatMode: ChatMode): String {
|
||||
val selectedPersona = service<PromptsSettings>().state.personas.selectedPersona
|
||||
|
||||
|
||||
return when (chatMode) {
|
||||
ChatMode.EDIT -> DEFAULT_PERSONA_EDIT_MODE_PROMPT
|
||||
ChatMode.ASK -> {
|
||||
if (isDefaultPersona(selectedPersona)) {
|
||||
PersonasState.DEFAULT_PERSONA_PROMPT
|
||||
} else {
|
||||
val originalPrompt = getOriginalPersonaPrompt()
|
||||
filterOutSearchReplaceInstructions(originalPrompt)
|
||||
}
|
||||
val originalPrompt = getOriginalPersonaPrompt()
|
||||
if (isDefaultPersona(selectedPersona)) originalPrompt
|
||||
else filterOutSearchReplaceInstructions(originalPrompt)
|
||||
}
|
||||
|
||||
ChatMode.AGENT -> PersonasState.DEFAULT_PERSONA_PROMPT
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +46,14 @@ class FilteredPromptsService {
|
|||
ChatMode.AGENT -> getSimpleEditCodePrompt()
|
||||
}
|
||||
|
||||
fun applyClickableLinks(prompt: String): String {
|
||||
val enabled =
|
||||
service<ConfigurationSettings>().state.chatCompletionSettings.clickableLinksEnabled
|
||||
if (!enabled) return prompt
|
||||
|
||||
return prompt + "\n" + PSI_LINKS_GUIDELINES
|
||||
}
|
||||
|
||||
private fun isDefaultPersona(persona: PersonaPromptDetailsState) =
|
||||
persona.id == DEFAULT_PERSONA_ID
|
||||
|
||||
|
|
@ -60,7 +67,7 @@ class FilteredPromptsService {
|
|||
|
||||
private fun getOriginalPersonaPrompt(): String {
|
||||
val selectedPersona = service<PromptsSettings>().state.personas.selectedPersona
|
||||
|
||||
|
||||
return selectedPersona.instructions ?: when {
|
||||
isDefaultPersona(selectedPersona) -> PersonasState.DEFAULT_PERSONA_PROMPT
|
||||
else -> ""
|
||||
|
|
@ -101,6 +108,9 @@ class FilteredPromptsService {
|
|||
|
||||
val DEFAULT_PERSONA_EDIT_MODE_PROMPT = getResourceContent(EDIT_MODE_PROMPT_RESOURCE)
|
||||
|
||||
private val PSI_LINKS_GUIDELINES =
|
||||
getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
|
||||
private val SEARCH_REPLACE_BLOCKS_REGEX = Regex(
|
||||
"When generating SEARCH/REPLACE blocks:.*?Keep SEARCH blocks concise while including necessary surrounding lines\\.",
|
||||
RegexOption.DOT_MATCHES_ALL
|
||||
|
|
@ -144,4 +154,4 @@ class FilteredPromptsService {
|
|||
CALCULATOR_EXAMPLE_REGEX to CALCULATOR_EXAMPLE_REPLACEMENT
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,13 +113,8 @@ class OllamaSettingsForm {
|
|||
.addComponentFillVertically(JPanel(), 0)
|
||||
.panel
|
||||
|
||||
fun getModel(featureType: FeatureType): String? {
|
||||
return if (modelComboBoxes[featureType]!!.isEnabled) {
|
||||
modelComboBoxes[featureType]!!.item
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
fun getModel(featureType: FeatureType): String? =
|
||||
modelComboBoxes[featureType]?.let { combo -> if (combo.isEnabled) combo.item else null }
|
||||
|
||||
fun getApiKey(): String? = String(apiKeyField.password).ifEmpty { null }
|
||||
|
||||
|
|
@ -137,11 +132,11 @@ class OllamaSettingsForm {
|
|||
fun applyChanges() {
|
||||
service<OllamaSettings>().state.run {
|
||||
host = hostField.text
|
||||
if (modelComboBoxes[FeatureType.CHAT]!!.isEnabled)
|
||||
model = modelComboBoxes[FeatureType.CHAT]!!.item
|
||||
if (modelComboBoxes[FeatureType.CHAT]?.isEnabled == true)
|
||||
model = modelComboBoxes[FeatureType.CHAT]?.item
|
||||
codeCompletionsEnabled = codeCompletionConfigurationForm.isCodeCompletionsEnabled
|
||||
fimTemplate = codeCompletionConfigurationForm.fimTemplate!!
|
||||
fimOverride = codeCompletionConfigurationForm.fimOverride ?: false
|
||||
fimTemplate = codeCompletionConfigurationForm.fimTemplate ?: fimTemplate
|
||||
fimOverride = codeCompletionConfigurationForm.fimOverride == true
|
||||
}
|
||||
setCredential(OllamaApikey, getApiKey())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class ErrorPopoverHandler(
|
|||
|
||||
private fun showErrorPopoverWithHover() {
|
||||
if (errorContent == null) return
|
||||
if (errorPopup != null && errorPopup!!.isVisible) return
|
||||
if (errorPopup?.isVisible == true) return
|
||||
|
||||
val documentationHint = DocumentationHintEditorPane(
|
||||
project,
|
||||
|
|
@ -96,4 +96,4 @@ class ErrorPopoverHandler(
|
|||
errorPopup = popup
|
||||
popup.showUnderneathOf(errorLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,9 +198,9 @@ class ChatHistoryToolWindow(private val project: Project) : BorderLayoutPanel()
|
|||
lastFilteredConversations = null
|
||||
projectFiltered
|
||||
} else if (searchText == lastSearchText && lastFilteredConversations != null) {
|
||||
lastFilteredConversations!!.filter { conversation ->
|
||||
lastFilteredConversations?.filter { conversation ->
|
||||
projectFiltered.contains(conversation)
|
||||
}
|
||||
}.orEmpty()
|
||||
} else {
|
||||
val startList = getOptimizedSearchStartList(searchText).filter { conversation ->
|
||||
projectFiltered.contains(conversation)
|
||||
|
|
@ -215,7 +215,7 @@ class ChatHistoryToolWindow(private val project: Project) : BorderLayoutPanel()
|
|||
|
||||
private fun getOptimizedSearchStartList(searchText: String): List<Conversation> {
|
||||
return if (lastSearchText.isNotEmpty() && searchText.startsWith(lastSearchText) && lastFilteredConversations != null) {
|
||||
lastFilteredConversations!!
|
||||
lastFilteredConversations.orEmpty()
|
||||
} else {
|
||||
allConversations
|
||||
}
|
||||
|
|
@ -601,4 +601,4 @@ class ChatHistoryToolWindow(private val project: Project) : BorderLayoutPanel()
|
|||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import com.intellij.openapi.Disposable
|
|||
import com.intellij.openapi.actionSystem.ActionPlaces
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.ReadAction
|
||||
import com.intellij.openapi.application.invokeLater
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.components.service
|
||||
|
|
@ -20,6 +22,7 @@ import com.intellij.ui.dsl.builder.AlignX
|
|||
import com.intellij.ui.dsl.builder.RightGap
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.util.IconUtil
|
||||
import com.intellij.util.concurrency.AppExecutorUtil
|
||||
import com.intellij.util.ui.AsyncProcessIcon
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
|
|
@ -105,9 +108,16 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
onSubmit = ::handleSubmit,
|
||||
onFilesDropped = { files ->
|
||||
includeFiles(files.toMutableList())
|
||||
totalTokensPanel.updateReferencedFilesTokens(files.map {
|
||||
ReferencedFile.from(it).fileContent()
|
||||
})
|
||||
ReadAction
|
||||
.nonBlocking<List<String>> {
|
||||
files.map { ReferencedFile.from(it).fileContent() }
|
||||
}
|
||||
.inSmartMode(project)
|
||||
.expireWith(project)
|
||||
.finishOnUiThread(ModalityState.any()) { contents ->
|
||||
totalTokensPanel.updateReferencedFilesTokens(contents)
|
||||
}
|
||||
.submit(AppExecutorUtil.getAppExecutorService())
|
||||
},
|
||||
featureType = featureType
|
||||
)
|
||||
|
|
@ -192,9 +202,16 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
}
|
||||
FileDragAndDrop.install(this) { files ->
|
||||
includeFiles(files.toMutableList())
|
||||
totalTokensPanel.updateReferencedFilesTokens(
|
||||
files.map { ReferencedFile.from(it).fileContent() }
|
||||
)
|
||||
ReadAction
|
||||
.nonBlocking<List<String>> {
|
||||
files.map { ReferencedFile.from(it).fileContent() }
|
||||
}
|
||||
.inSmartMode(project)
|
||||
.expireWith(project)
|
||||
.finishOnUiThread(ModalityState.any()) { contents ->
|
||||
totalTokensPanel.updateReferencedFilesTokens(contents)
|
||||
}
|
||||
.submit(AppExecutorUtil.getAppExecutorService())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,40 +7,42 @@ import ee.carlrobert.codegpt.toolwindow.chat.ResponseNodeRenderer
|
|||
import java.util.regex.Pattern
|
||||
|
||||
object MarkdownUtil {
|
||||
/**
|
||||
* Splits a given string into a list of strings where each element is either a code block
|
||||
* surrounded by triple backticks or a non-code block text.
|
||||
*
|
||||
* @param inputMarkdown The input markdown formatted string to be split.
|
||||
* @return A list of strings where each element is a code block or a non-code block text from the
|
||||
* input string.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun splitCodeBlocks(inputMarkdown: String): List<String> {
|
||||
val result: MutableList<String> = ArrayList()
|
||||
val pattern = Pattern.compile(
|
||||
"""(?m)^```[a-zA-Z0-9]*\r?\n.*?\r?\n```""",
|
||||
Pattern.DOTALL
|
||||
)
|
||||
val matcher = pattern.matcher(inputMarkdown)
|
||||
var start = 0
|
||||
while (matcher.find()) {
|
||||
result.add(inputMarkdown.substring(start, matcher.start()))
|
||||
result.add(matcher.group())
|
||||
start = matcher.end()
|
||||
}
|
||||
result.add(inputMarkdown.substring(start))
|
||||
return result.stream().filter(String::isNotBlank).toList()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun convertMdToHtml(message: String): String {
|
||||
val options = MutableDataSet()
|
||||
options.set(HtmlRenderer.SOFT_BREAK, "<br/>")
|
||||
val document = Parser.builder(options).build().parse(message)
|
||||
return HtmlRenderer.builder(options)
|
||||
.nodeRendererFactory(ResponseNodeRenderer.Factory())
|
||||
.build()
|
||||
.render(document)
|
||||
}
|
||||
/**
|
||||
* Splits a given string into a list of strings where each element is either a code block
|
||||
* surrounded by triple backticks or a non-code block text.
|
||||
*
|
||||
* @param inputMarkdown The input markdown formatted string to be split.
|
||||
* @return A list of strings where each element is a code block or a non-code block text from the
|
||||
* input string.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun splitCodeBlocks(inputMarkdown: String): List<String> {
|
||||
val result: MutableList<String> = ArrayList()
|
||||
val pattern = Pattern.compile(
|
||||
"""(?m)^```[a-zA-Z0-9]*\r?\n.*?\r?\n```""",
|
||||
Pattern.DOTALL
|
||||
)
|
||||
val matcher = pattern.matcher(inputMarkdown)
|
||||
var start = 0
|
||||
while (matcher.find()) {
|
||||
result.add(inputMarkdown.substring(start, matcher.start()))
|
||||
result.add(matcher.group())
|
||||
start = matcher.end()
|
||||
}
|
||||
result.add(inputMarkdown.substring(start))
|
||||
return result.stream().filter(String::isNotBlank).toList()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun convertMdToHtml(message: String): String {
|
||||
val options = MutableDataSet()
|
||||
options.set(HtmlRenderer.SOFT_BREAK, "<br/>")
|
||||
|
||||
val document = Parser.builder(options).build().parse(message)
|
||||
return HtmlRenderer.builder(options)
|
||||
.nodeRendererFactory(ResponseNodeRenderer.Factory())
|
||||
.build()
|
||||
.render(document)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
250
src/main/kotlin/ee/carlrobert/codegpt/util/PsiLinkNavigator.kt
Normal file
250
src/main/kotlin/ee/carlrobert/codegpt/util/PsiLinkNavigator.kt
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
package ee.carlrobert.codegpt.util
|
||||
|
||||
import com.intellij.ide.util.gotoByName.GotoClassModel2
|
||||
import com.intellij.ide.util.gotoByName.GotoFileModel
|
||||
import com.intellij.ide.util.gotoByName.GotoSymbolModel2
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.ReadAction
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.pom.Navigatable
|
||||
import com.intellij.psi.JavaPsiFacade
|
||||
import com.intellij.psi.PsiClass
|
||||
import com.intellij.psi.PsiManager
|
||||
import com.intellij.psi.PsiNamedElement
|
||||
import com.intellij.psi.search.FilenameIndex
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.intellij.util.concurrency.AppExecutorUtil
|
||||
import java.net.URLDecoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
object PsiLinkNavigator {
|
||||
private val logger = thisLogger()
|
||||
|
||||
private const val PSI_ELEMENT_PREFIX = "psi_element://"
|
||||
private const val FILE_PREFIX = "file://"
|
||||
|
||||
private val SUPPORTED_PROTOCOLS = listOf(PSI_ELEMENT_PREFIX, FILE_PREFIX)
|
||||
|
||||
@JvmStatic
|
||||
fun handle(description: String): Boolean {
|
||||
if (!isValidNavigationLink(description)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val protocol = extractProtocol(description)
|
||||
val target = extractTarget(description)
|
||||
|
||||
return try {
|
||||
ReadAction.nonBlocking<Navigatable?> {
|
||||
val resolver = NavigationResolverFactory.create(protocol)
|
||||
resolver.resolve(target)
|
||||
}
|
||||
.finishOnUiThread(ModalityState.nonModal()) { navigatable ->
|
||||
navigatable?.let { navigate(it) }
|
||||
}
|
||||
.submit(AppExecutorUtil.getAppExecutorService())
|
||||
true
|
||||
} catch (t: Throwable) {
|
||||
logger.warn("Failed to schedule navigation for: $target", t)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isValidNavigationLink(description: String?): Boolean {
|
||||
if (description.isNullOrBlank()) return false
|
||||
return SUPPORTED_PROTOCOLS.any { description.startsWith(it) }
|
||||
}
|
||||
|
||||
private fun extractProtocol(description: String): String {
|
||||
return SUPPORTED_PROTOCOLS.first { description.startsWith(it) }
|
||||
}
|
||||
|
||||
private fun extractTarget(description: String): String {
|
||||
val protocol = extractProtocol(description)
|
||||
val raw = description.removePrefix(protocol)
|
||||
return decode(raw)
|
||||
}
|
||||
|
||||
private fun decode(value: String): String = try {
|
||||
URLDecoder.decode(value, StandardCharsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to decode URL: $value", e)
|
||||
value
|
||||
}
|
||||
|
||||
private fun navigate(target: Navigatable) {
|
||||
if (target.canNavigate()) {
|
||||
target.navigate(true)
|
||||
logger.info("Successfully navigated to: $target")
|
||||
} else {
|
||||
logger.info("Target cannot navigate: $target")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class NavigationResolver {
|
||||
abstract fun resolve(target: String): Navigatable?
|
||||
|
||||
protected val logger = thisLogger()
|
||||
protected fun getProject(): Project? = ApplicationUtil.findCurrentProject()
|
||||
}
|
||||
|
||||
class PsiElementResolver : NavigationResolver() {
|
||||
override fun resolve(target: String): Navigatable? {
|
||||
val project = getProject() ?: return null
|
||||
logger.info("Resolving PSI element: $target")
|
||||
|
||||
findByJavaFQN(project, target)?.let { return it }
|
||||
|
||||
findByMemberSeparation(project, target)?.let { return it }
|
||||
|
||||
return searchInModels(project, target)
|
||||
}
|
||||
|
||||
private fun findByJavaFQN(project: Project, target: String): Navigatable? {
|
||||
try {
|
||||
val memberSeparatorIndex = target.indexOf('#')
|
||||
val className = if (memberSeparatorIndex > 0) {
|
||||
target.substring(0, memberSeparatorIndex)
|
||||
} else {
|
||||
target
|
||||
}
|
||||
|
||||
val javaPsiFacade = JavaPsiFacade.getInstance(project)
|
||||
val projectScope = GlobalSearchScope.projectScope(project)
|
||||
val allScope = GlobalSearchScope.allScope(project)
|
||||
|
||||
val psiClass = javaPsiFacade.findClass(className, projectScope)
|
||||
?: javaPsiFacade.findClass(className, allScope)
|
||||
|
||||
if (psiClass != null) {
|
||||
val memberName = if (memberSeparatorIndex > 0) {
|
||||
target.substring(memberSeparatorIndex + 1)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (memberName != null) {
|
||||
findMemberInClass(psiClass, memberName)?.let { return it }
|
||||
}
|
||||
return psiClass
|
||||
}
|
||||
|
||||
if (className.contains('$')) {
|
||||
val innerClassFQN = className.replace('$', '.')
|
||||
val innerClass = javaPsiFacade.findClass(innerClassFQN, projectScope)
|
||||
?: javaPsiFacade.findClass(innerClassFQN, allScope)
|
||||
if (innerClass != null) {
|
||||
return innerClass
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (t: Throwable) {
|
||||
logger.warn("Failed to resolve Java FQN: $target", t)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun findByMemberSeparation(project: Project, target: String): Navigatable? {
|
||||
val memberSeparatorIndex = target.indexOf('#')
|
||||
if (memberSeparatorIndex <= 0) return null
|
||||
|
||||
val owner = target.substring(0, memberSeparatorIndex)
|
||||
val member = target.substring(memberSeparatorIndex + 1)
|
||||
|
||||
searchInModels(project, owner)?.let { ownerElement ->
|
||||
if (ownerElement is PsiClass) {
|
||||
findMemberInClass(ownerElement, member)?.let { return it }
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findMemberInClass(psiClass: PsiClass, memberName: String): Navigatable? {
|
||||
psiClass.findMethodsByName(memberName, false).firstOrNull()?.let { return it }
|
||||
psiClass.findFieldByName(memberName, false)?.let { return it }
|
||||
psiClass.findInnerClassByName(memberName, false)?.let { return it }
|
||||
return null
|
||||
}
|
||||
|
||||
private fun searchInModels(project: Project, searchTerm: String): Navigatable? {
|
||||
try {
|
||||
val classModel = GotoClassModel2(project)
|
||||
classModel.getElementsByName(searchTerm, true, searchTerm)
|
||||
.filterIsInstance<Navigatable>()
|
||||
.firstOrNull { it.canNavigate() }
|
||||
?.let { return it }
|
||||
|
||||
val symbolModel = GotoSymbolModel2(project, project)
|
||||
symbolModel.getElementsByName(searchTerm, true, searchTerm)
|
||||
.filterIsInstance<Navigatable>()
|
||||
.firstOrNull { it.canNavigate() }
|
||||
?.let { return it }
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Search failed for term: $searchTerm", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
class FileResolver : NavigationResolver() {
|
||||
|
||||
override fun resolve(target: String): Navigatable? {
|
||||
val project = getProject() ?: return null
|
||||
logger.info("Resolving file: $target")
|
||||
|
||||
val memberSeparatorIndex = target.indexOf('#')
|
||||
val filePath = if (memberSeparatorIndex > 0) {
|
||||
target.substring(0, memberSeparatorIndex)
|
||||
} else {
|
||||
target
|
||||
}
|
||||
|
||||
val fileName = filePath.substringAfterLast('/')
|
||||
val matchingVirtualFile =
|
||||
FilenameIndex.getVirtualFilesByName(fileName, GlobalSearchScope.projectScope(project))
|
||||
.firstOrNull { vf ->
|
||||
vf.path.endsWith(filePath) || vf.name == fileName
|
||||
}
|
||||
|
||||
if (matchingVirtualFile != null) {
|
||||
val psiFile = PsiManager.getInstance(project).findFile(matchingVirtualFile)
|
||||
if (psiFile != null) {
|
||||
if (memberSeparatorIndex > 0) {
|
||||
val memberName = target.substring(memberSeparatorIndex + 1)
|
||||
val memberElement =
|
||||
PsiTreeUtil.findChildrenOfType(psiFile, PsiNamedElement::class.java)
|
||||
.firstOrNull { it.name == memberName }
|
||||
|
||||
return (memberElement as? Navigatable) ?: psiFile
|
||||
}
|
||||
return psiFile
|
||||
}
|
||||
}
|
||||
|
||||
return try {
|
||||
val fileModel = GotoFileModel(project)
|
||||
fileModel.getElementsByName(fileName, true, fileName)
|
||||
.filterIsInstance<Navigatable>()
|
||||
.firstOrNull { it.canNavigate() }
|
||||
} catch (t: Throwable) {
|
||||
logger.warn("File search failed for: $target", t)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object NavigationResolverFactory {
|
||||
fun create(protocol: String): NavigationResolver {
|
||||
return when (protocol) {
|
||||
"psi_element://" -> PsiElementResolver()
|
||||
"file://" -> FileResolver()
|
||||
else -> throw IllegalArgumentException("Unsupported protocol: $protocol")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,6 @@
|
|||
<depends optional="true" config-file="plugin-kotlin.xml">org.jetbrains.kotlin</depends>
|
||||
<depends optional="true" config-file="plugin-java.xml">com.intellij.modules.java</depends>
|
||||
<depends optional="true" config-file="plugin-python.xml">com.intellij.modules.python</depends>
|
||||
<!-- TODO-->
|
||||
<!-- <depends optional="true" config-file="plugin-js.xml">JavaScript</depends>-->
|
||||
<!-- <depends optional="true" config-file="plugin-go.xml">org.jetbrains.plugins.go</depends>-->
|
||||
<!-- <depends optional="true" config-file="plugin-ruby.xml">com.intellij.modules.ruby</depends>-->
|
||||
<!-- <depends optional="true" config-file="plugin-php.xml">com.jetbrains.php</depends>-->
|
||||
|
|
@ -199,15 +197,15 @@
|
|||
<separator/>
|
||||
</group>
|
||||
|
||||
<!--
|
||||
Removed add-to-group into deprecated/removed VCS groups for 2025.1+/Remote Dev.
|
||||
The action remains available via "Find Action" to avoid startup errors in Thin Client.
|
||||
-->
|
||||
<group id="CodeGPT.VcsLogContextMenu">
|
||||
<separator/>
|
||||
<action
|
||||
id="CodeGPT.ExplainGitCommitAction"
|
||||
class="ee.carlrobert.codegpt.actions.ExplainGitCommitAction"/>
|
||||
<add-to-group
|
||||
group-id="Vcs.Log.ContextMenu"
|
||||
relative-to-action="Vcs.Log.CompareRevisions"
|
||||
anchor="after"/>
|
||||
<separator/>
|
||||
</group>
|
||||
|
||||
|
|
@ -336,7 +334,7 @@
|
|||
text="ProxyAI"
|
||||
popup="true"
|
||||
icon="ee.carlrobert.codegpt.Icons.DefaultSmall">
|
||||
<add-to-group group-id="Vcs.MessageActionGroup" anchor="first"/>
|
||||
<!-- Removed add-to-group to Vcs.MessageActionGroup to prevent startup errors in 251.* -->
|
||||
<action
|
||||
id="CodeGPT.GenerateGitCommitMessage"
|
||||
text="Generate Commit Message"
|
||||
|
|
|
|||
|
|
@ -166,6 +166,8 @@ configurationConfigurable.section.chatCompletion.psiStructure.title=Enable depen
|
|||
configurationConfigurable.section.chatCompletion.psiStructure.analyzeDepth.title=Code analyze depth:
|
||||
configurationConfigurable.section.chatCompletion.psiStructure.analyzeDepth.comment=The parameter limits the depth of the PSI structure traversal. Currently, it is implemented only for the Kotlin language.
|
||||
configurationConfigurable.section.chatCompletion.psiStructure.description=If enabled, the class structure that is present in the imports of the attached files will be added in the context of the dialog. A structure refers to the source code in files that include constructors, fields, and methods, with all modifiers, arguments, and return types, but without an implementation. The implementation of dependencies is intentionally excluded in order to find a balance between a high-quality chat context and saving tokens.
|
||||
configurationConfigurable.section.chatCompletion.clickableLinks.title=Show clickable links for classes and methods
|
||||
configurationConfigurable.section.chatCompletion.clickableLinks.description=If enabled, code references in answers become clickable so you can jump to them in your IDE.
|
||||
settingsConfigurable.service.llama.predefinedModel.comment=Download and use vetted models from HuggingFace.
|
||||
settingsConfigurable.service.llama.customModel.comment=Use your own GGUF model file from a local path on your computer.
|
||||
settingsConfigurable.service.custom.openai.testConnection.label=Test Connection
|
||||
|
|
|
|||
|
|
@ -166,6 +166,8 @@ configurationConfigurable.section.chatCompletion.psiStructure.title=\u542F\u7528
|
|||
configurationConfigurable.section.chatCompletion.psiStructure.analyzeDepth.title=\u4EE3\u7801\u5206\u6790\u6DF1\u5EA6:
|
||||
configurationConfigurable.section.chatCompletion.psiStructure.analyzeDepth.comment=\u8BE5\u53C2\u6570\u9650\u5236PSI\u7ED3\u6784\u904D\u5386\u7684\u6DF1\u5EA6\u3002\u76EE\u524D\u4EC5\u9488\u5BF9Kotlin\u8BED\u8A00\u5B9E\u73B0\u3002
|
||||
configurationConfigurable.section.chatCompletion.psiStructure.description=\u5982\u679C\u542F\u7528\uFF0C\u9644\u52A0\u6587\u4EF6\u5BFC\u5165\u4E2D\u5B58\u5728\u7684\u7C7B\u7ED3\u6784\u5C06\u88AB\u6DFB\u52A0\u5230\u5BF9\u8BDD\u7684\u4E0A\u4E0B\u6587\u4E2D\u3002\u7ED3\u6784\u6307\u7684\u662F\u6587\u4EF6\u4E2D\u5305\u542B\u6784\u9020\u51FD\u6570\u3001\u5B57\u6BB5\u548C\u65B9\u6CD5\u7684\u6E90\u4EE3\u7801\uFF0C\u5305\u62EC\u6240\u6709\u4FEE\u9970\u7B26\u3001\u53C2\u6570\u548C\u8FD4\u56DE\u7C7B\u578B\uFF0C\u4F46\u4E0D\u5305\u62EC\u5B9E\u73B0\u3002\u4E3A\u4E86\u5728\u9AD8\u8D28\u91CF\u804A\u5929\u4E0A\u4E0B\u6587\u548C\u8282\u7701\u6807\u8BB0\u4E4B\u95F4\u627E\u5230\u5E73\u8861\uFF0C\u6545\u610F\u6392\u9664\u4E86\u4F9D\u8D56\u7684\u5B9E\u73B0\u3002
|
||||
configurationConfigurable.section.chatCompletion.clickableLinks.title=Show clickable links for classes and methods
|
||||
configurationConfigurable.section.chatCompletion.clickableLinks.description=If enabled, code references in answers become clickable so you can jump to them in your IDE.
|
||||
settingsConfigurable.service.llama.predefinedModel.comment=\u4ECEHuggingFace\u4E0B\u8F7D\u5E76\u4F7F\u7528\u7ECF\u8FC7\u5BA1\u67E5\u7684\u6A21\u578B\u3002
|
||||
settingsConfigurable.service.llama.customModel.comment=\u4F7F\u7528\u60A8\u8BA1\u7B97\u673A\u4E0A\u672C\u5730\u8DEF\u5F84\u4E2D\u7684GGUF\u6A21\u578B\u6587\u4EF6\u3002
|
||||
settingsConfigurable.service.custom.openai.testConnection.label=\u6D4B\u8BD5\u8FDE\u63A5
|
||||
|
|
@ -416,4 +418,4 @@ conversation.status.count.plural={0}\u4E2A\u5BF9\u8BDD
|
|||
conversation.status.sortedBy=\u6392\u5E8F\u65B9\u5F0F: {0}
|
||||
conversation.deleteConfirmation.message=\u60A8\u786E\u5B9A\u8981\u5220\u9664\u6B64\u5BF9\u8BDD\u5417?
|
||||
conversation.deleteConfirmation.title=\u5220\u9664\u5BF9\u8BDD
|
||||
chat.message.welcome=\u55E8 <strong>{0}</strong>, \u6211\u662F ProxyAI\uFF01\u4F60\u53EF\u4EE5\u95EE\u6211\u4EFB\u4F55\u95EE\u9898\uFF0C\u4F46\u5927\u591A\u6570\u4EBA\u4F1A\u8BF7\u6C42\u6211\u63D0\u4F9B\u4EE3\u7801\u65B9\u9762\u7684\u5E2E\u52A9\u3002\u4EE5\u4E0B\u662F\u4E00\u4E9B\u4F60\u53EF\u4EE5\u5411\u6211\u54A8\u8BE2\u7684\u95EE\u9898\uFF1A
|
||||
chat.message.welcome=\u55E8 <strong>{0}</strong>, \u6211\u662F ProxyAI\uFF01\u4F60\u53EF\u4EE5\u95EE\u6211\u4EFB\u4F55\u95EE\u9898\uFF0C\u4F46\u5927\u591A\u6570\u4EBA\u4F1A\u8BF7\u6C42\u6211\u63D0\u4F9B\u4EE3\u7801\u65B9\u9762\u7684\u5E2E\u52A9\u3002\u4EE5\u4E0B\u662F\u4E00\u4E9B\u4F60\u53EF\u4EE5\u5411\u6211\u54A8\u8BE2\u7684\u95EE\u9898\uFF1A
|
||||
|
|
|
|||
|
|
@ -78,4 +78,4 @@ Formatting Guidelines:
|
|||
|
||||
6. Do not provide an implementation plan for pure explanations or general questions.
|
||||
|
||||
7. When refactoring an entire file, output multiple code blocks as needed, keeping changes concise unless a more extensive update is required.
|
||||
7. When refactoring an entire file, output multiple code blocks as needed, keeping changes concise unless a more extensive update is required.
|
||||
|
|
|
|||
|
|
@ -73,5 +73,3 @@ Formatting Guidelines:
|
|||
5. Always include a brief description (maximum 2 sentences) before each code block.
|
||||
|
||||
6. Do not provide an implementation plan for pure explanations or general questions.
|
||||
|
||||
7. When refactoring an entire file, provide the complete updated file content in a single code block.
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
## JetBrains Navigation Links (MANDATORY)
|
||||
|
||||
**Link every concrete symbol** (class, method, field, constant, function) using these two protocols only:
|
||||
|
||||
### Navigation Protocols
|
||||
|
||||
**Java/Kotlin ONLY (.java, .kt files):**
|
||||
- Classes: `psi_element://fully.qualified.ClassName` (MUST be fully qualified)
|
||||
- Methods: `psi_element://fully.qualified.ClassName#methodName`
|
||||
- Fields: `psi_element://fully.qualified.ClassName#fieldName`
|
||||
- Constants: `psi_element://fully.qualified.ClassName#CONSTANT_NAME`
|
||||
|
||||
**All Other Languages (C/C++, JS, Python, etc.):**
|
||||
- Functions: `file://src/path/file.ext#functionName`
|
||||
- Constants/Variables: `file://src/path/file.ext#VARIABLE_NAME`
|
||||
- Files: `file://src/path/file.ext`
|
||||
|
||||
**No Link Available:**
|
||||
- Use backticks: `someSymbol` (when no file context or reference is possible)
|
||||
|
||||
### Critical Rules
|
||||
|
||||
1. **psi_element:// ONLY for Java/Kotlin**: Never use for other languages
|
||||
2. **file:// for everything else**: C/C++, JavaScript, Python, Go, etc.
|
||||
3. **Visible text = exact symbol name**: `[Repository]`, `[handleSubmit]`, `[API_KEY]`
|
||||
4. **Use backticks when no context**: If you can't determine file location, use `backticks`
|
||||
5. **Methods must include owner**: `fully.qualified.ClassName#methodName` or `file.ext#functionName`
|
||||
6. **MANDATORY**: Always use fully qualified class names - never use short class names alone
|
||||
|
||||
### Examples
|
||||
|
||||
**Java/Kotlin:**
|
||||
```
|
||||
The [System](psi_element://java.lang.System) class has [out](psi_element://java.lang.System#out) field.
|
||||
Use [UserRepository](psi_element://com.example.repository.UserRepository) with [findById](psi_element://com.example.repository.UserRepository#findById).
|
||||
The [name](psi_element://com.example.model.User#name) field in [User](psi_element://com.example.model.User) class.
|
||||
```
|
||||
|
||||
**C/C++:**
|
||||
```
|
||||
Call [logError](file://src/utils/logger.cpp#logError) function.
|
||||
The [MAX_SIZE](file://include/constants.h#MAX_SIZE) constant is defined.
|
||||
Implement [processData](file://src/processor.h#processData) in header.
|
||||
```
|
||||
|
||||
**JavaScript:**
|
||||
```
|
||||
The [handleClick](file://src/components/Button.js#handleClick) event handler.
|
||||
Export [API_BASE_URL](file://src/config.js#API_BASE_URL) from config.
|
||||
```
|
||||
|
||||
**Python:**
|
||||
```
|
||||
Use [parse_json](file://utils/parser.py#parse_json) function.
|
||||
The [DATABASE_URL](file://settings.py#DATABASE_URL) setting.
|
||||
```
|
||||
|
||||
**No Context Available:**
|
||||
```
|
||||
Use the `printf` function for formatting.
|
||||
The `malloc` function allocates memory.
|
||||
Consider using `async/await` pattern.
|
||||
```
|
||||
|
||||
**Files:**
|
||||
```
|
||||
Edit the [main.cpp](file://src/main.cpp) file.
|
||||
Check [UserService.java](file://src/service/UserService.java).
|
||||
```
|
||||
|
|
@ -7,6 +7,7 @@ import ee.carlrobert.codegpt.conversations.message.Message
|
|||
import ee.carlrobert.codegpt.settings.prompts.PersonaPromptDetailsState
|
||||
import ee.carlrobert.codegpt.settings.prompts.PromptsSettings
|
||||
import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel
|
||||
import ee.carlrobert.codegpt.util.file.FileUtil.getResourceContent
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.groups.Tuple
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
|
|
@ -33,10 +34,12 @@ class CompletionRequestProviderTest : IntegrationTest() {
|
|||
|
||||
val request = OpenAIRequestFactory().createChatRequest(callParameters)
|
||||
|
||||
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
|
||||
assertThat(request.messages)
|
||||
.extracting("role", "content")
|
||||
.containsExactly(
|
||||
Tuple.tuple("system", "TEST_SYSTEM_PROMPT\n"),
|
||||
Tuple.tuple("system", expectedSystem),
|
||||
Tuple.tuple("user", "TEST_PROMPT"),
|
||||
Tuple.tuple("assistant", firstMessage.response),
|
||||
Tuple.tuple("user", "TEST_PROMPT"),
|
||||
|
|
@ -64,10 +67,12 @@ class CompletionRequestProviderTest : IntegrationTest() {
|
|||
|
||||
val request = OpenAIRequestFactory().createChatRequest(callParameters)
|
||||
|
||||
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
|
||||
assertThat(request.messages)
|
||||
.extracting("role", "content")
|
||||
.containsExactly(
|
||||
Tuple.tuple("system", "TEST_SYSTEM_PROMPT\n"),
|
||||
Tuple.tuple("system", expectedSystem),
|
||||
Tuple.tuple("user", "FIRST_TEST_PROMPT"),
|
||||
Tuple.tuple("assistant", firstMessage.response),
|
||||
Tuple.tuple("user", "SECOND_TEST_PROMPT")
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import ee.carlrobert.llm.client.util.JSONUtil.*
|
|||
import org.apache.http.HttpHeaders
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import testsupport.IntegrationTest
|
||||
import ee.carlrobert.codegpt.util.file.FileUtil.getResourceContent
|
||||
|
||||
class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
|
||||
|
||||
|
|
@ -31,6 +32,8 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
|
|||
assertThat(request.uri.path).isEqualTo("/v1/chat/completions")
|
||||
assertThat(request.method).isEqualTo("POST")
|
||||
assertThat(request.headers[HttpHeaders.AUTHORIZATION]!![0]).isEqualTo("Bearer TEST_API_KEY")
|
||||
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
|
||||
assertThat(request.body)
|
||||
.extracting(
|
||||
"model",
|
||||
|
|
@ -39,7 +42,7 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
|
|||
.containsExactly(
|
||||
"gpt-4o",
|
||||
listOf(
|
||||
mapOf("role" to "system", "content" to "TEST_SYSTEM_PROMPT\n"),
|
||||
mapOf("role" to "system", "content" to expectedSystem),
|
||||
mapOf("role" to "user", "content" to "TEST_PROMPT")
|
||||
)
|
||||
)
|
||||
|
|
@ -75,6 +78,8 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
|
|||
conversation.addMessage(Message("Ping", "Pong"))
|
||||
expectLlama(StreamHttpExchange { request: RequestEntity ->
|
||||
assertThat(request.uri.path).isEqualTo("/completion")
|
||||
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
|
||||
assertThat(request.body)
|
||||
.extracting(
|
||||
"prompt",
|
||||
|
|
@ -83,7 +88,7 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
|
|||
)
|
||||
.containsExactly(
|
||||
LLAMA.buildPrompt(
|
||||
"TEST_SYSTEM_PROMPT",
|
||||
expectedSystem,
|
||||
"TEST_PROMPT",
|
||||
conversation.messages
|
||||
),
|
||||
|
|
@ -122,6 +127,8 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
|
|||
assertThat(request.uri.path).isEqualTo("/v1/chat/completions")
|
||||
assertThat(request.method).isEqualTo("POST")
|
||||
assertThat(request.headers[HttpHeaders.AUTHORIZATION]!![0]).isEqualTo("Bearer TEST_API_KEY")
|
||||
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
|
||||
assertThat(request.body)
|
||||
.extracting(
|
||||
"model",
|
||||
|
|
@ -130,7 +137,7 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
|
|||
.containsExactly(
|
||||
HuggingFaceModel.LLAMA_3_8B_Q6_K.code,
|
||||
listOf(
|
||||
mapOf("role" to "system", "content" to "TEST_SYSTEM_PROMPT\n"),
|
||||
mapOf("role" to "system", "content" to expectedSystem),
|
||||
mapOf("role" to "user", "content" to "TEST_PROMPT")
|
||||
)
|
||||
)
|
||||
|
|
@ -154,6 +161,8 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
|
|||
|
||||
fun testGoogleChatCompletionCall() {
|
||||
useGoogleService()
|
||||
service<ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings>().state
|
||||
.chatCompletionSettings.clickableLinksEnabled = true
|
||||
val customPersona = PersonaPromptDetailsState().apply {
|
||||
id = 999L
|
||||
name = "Test Persona"
|
||||
|
|
@ -166,13 +175,15 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
|
|||
assertThat(request.uri.path).isEqualTo("/v1/models/gemini-2.0-flash:streamGenerateContent")
|
||||
assertThat(request.method).isEqualTo("POST")
|
||||
assertThat(request.uri.query).isEqualTo("key=TEST_API_KEY&alt=sse")
|
||||
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
|
||||
assertThat(request.body)
|
||||
.extracting("contents", "systemInstruction")
|
||||
.containsExactly(
|
||||
listOf(
|
||||
mapOf("parts" to listOf(mapOf("text" to "TEST_PROMPT")), "role" to "user"),
|
||||
),
|
||||
mapOf("parts" to listOf(mapOf("text" to "TEST_SYSTEM_PROMPT")))
|
||||
mapOf("parts" to listOf(mapOf("text" to expectedSystem)))
|
||||
)
|
||||
listOf(
|
||||
jsonMapResponse(
|
||||
|
|
@ -212,6 +223,8 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
|
|||
assertThat(request.uri.path).isEqualTo("/v1/chat/completions")
|
||||
assertThat(request.method).isEqualTo("POST")
|
||||
assertThat(request.headers[HttpHeaders.AUTHORIZATION]!![0]).isEqualTo("Bearer TEST_API_KEY")
|
||||
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
|
||||
assertThat(request.body)
|
||||
.extracting(
|
||||
"model",
|
||||
|
|
@ -220,9 +233,9 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
|
|||
.containsExactly(
|
||||
"gpt-5-mini",
|
||||
listOf(
|
||||
mapOf("role" to "user", "content" to "TEST_SYSTEM_PROMPT\n"),
|
||||
mapOf("role" to "user", "content" to expectedSystem),
|
||||
mapOf("role" to "user", "content" to "TEST_PROMPT")
|
||||
)
|
||||
)
|
||||
)
|
||||
listOf(
|
||||
jsonMapResponse(
|
||||
|
|
|
|||
|
|
@ -34,12 +34,13 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
|
|||
val request = OpenAIRequestFactory().createChatRequest(callParameters)
|
||||
|
||||
val systemMessages = request.messages
|
||||
.filterIsInstance<ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage>()
|
||||
.filterIsInstance<OpenAIChatCompletionStandardMessage>()
|
||||
.filter { it.role == "system" }
|
||||
.map { it.content }
|
||||
assertThat(systemMessages).isNotEmpty()
|
||||
val systemContent = systemMessages.first()
|
||||
assertThat(systemContent).isEqualTo(
|
||||
val guidelinesEdit = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
assertThat(systemContent).startsWith(
|
||||
"You are an AI programming assistant integrated into a JetBrains IDE plugin. Your role is to answer coding questions, suggest new code, and perform refactoring or editing tasks. You have access to the following project information:\n" +
|
||||
"\n" +
|
||||
"Before we proceed with the main instructions, here is the content of relevant files in the project:\n" +
|
||||
|
|
@ -122,6 +123,7 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
|
|||
"\n" +
|
||||
"7. When refactoring an entire file, output multiple code blocks as needed, keeping changes concise unless a more extensive update is required.\n"
|
||||
)
|
||||
assertThat(systemContent).endsWith(guidelinesEdit)
|
||||
}
|
||||
|
||||
fun testDefaultPersonaIsFilteredInAskMode() {
|
||||
|
|
@ -137,12 +139,13 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
|
|||
val request = OpenAIRequestFactory().createChatRequest(callParameters)
|
||||
|
||||
val systemMessages = request.messages
|
||||
.filterIsInstance<ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage>()
|
||||
.filterIsInstance<OpenAIChatCompletionStandardMessage>()
|
||||
.filter { it.role == "system" }
|
||||
.map { it.content }
|
||||
assertThat(systemMessages).isNotEmpty()
|
||||
val systemContent = systemMessages.first()
|
||||
assertThat(systemContent).isEqualTo(
|
||||
val guidelinesAskDefault = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
assertThat(systemContent).startsWith(
|
||||
"You are an AI programming assistant integrated into a JetBrains IDE plugin. Your role is to answer coding questions, suggest new code, and perform refactoring or editing tasks. You have access to the following project information:\n" +
|
||||
"\n" +
|
||||
"Before we proceed with the main instructions, here is the content of relevant files in the project:\n" +
|
||||
|
|
@ -217,10 +220,9 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
|
|||
"\n" +
|
||||
"5. Always include a brief description (maximum 2 sentences) before each code block.\n" +
|
||||
"\n" +
|
||||
"6. Do not provide an implementation plan for pure explanations or general questions.\n" +
|
||||
"\n" +
|
||||
"7. When refactoring an entire file, provide the complete updated file content in a single code block.\n"
|
||||
"6. Do not provide an implementation plan for pure explanations or general questions.\n\n"
|
||||
)
|
||||
assertThat(systemContent).endsWith(guidelinesAskDefault)
|
||||
}
|
||||
|
||||
fun testChatRequestUsesFilteredPersonaPromptInAskMode() {
|
||||
|
|
@ -256,12 +258,13 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
|
|||
val request = OpenAIRequestFactory().createChatRequest(callParameters)
|
||||
|
||||
val systemMessages = request.messages
|
||||
.filterIsInstance<ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage>()
|
||||
.filterIsInstance<OpenAIChatCompletionStandardMessage>()
|
||||
.filter { it.role == "system" }
|
||||
.map { it.content }
|
||||
assertThat(systemMessages).isNotEmpty()
|
||||
val systemContent = systemMessages.first()
|
||||
assertThat(systemContent).isEqualTo(
|
||||
val guidelinesAskCustom = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
assertThat(systemContent).startsWith(
|
||||
"You are a helpful assistant.\n" +
|
||||
"For refactoring or editing an existing file, provide the complete modified code.\n" +
|
||||
"When providing code modifications:\n" +
|
||||
|
|
@ -279,6 +282,7 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
|
|||
" }\n" +
|
||||
" ```\n"
|
||||
)
|
||||
assertThat(systemContent).endsWith(guidelinesAskCustom)
|
||||
}
|
||||
|
||||
fun testChatRequestKeepsOriginalPersonaPromptInEditMode() {
|
||||
|
|
@ -304,7 +308,8 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
|
|||
.map { it.content }
|
||||
assertThat(systemMessages).isNotEmpty()
|
||||
val systemContent = systemMessages.first()
|
||||
assertThat(systemContent).isEqualTo(
|
||||
val guidelinesEditCustom = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
assertThat(systemContent).startsWith(
|
||||
"You are an AI programming assistant integrated into a JetBrains IDE plugin. Your role is to answer coding questions, suggest new code, and perform refactoring or editing tasks. You have access to the following project information:\n" +
|
||||
"\n" +
|
||||
"Before we proceed with the main instructions, here is the content of relevant files in the project:\n" +
|
||||
|
|
@ -387,6 +392,7 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
|
|||
"\n" +
|
||||
"7. When refactoring an entire file, output multiple code blocks as needed, keeping changes concise unless a more extensive update is required.\n"
|
||||
)
|
||||
assertThat(systemContent).endsWith(guidelinesEditCustom)
|
||||
}
|
||||
|
||||
fun testInlineEditSingleRequestNoHistory() {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import ee.carlrobert.llm.client.http.RequestEntity
|
|||
import ee.carlrobert.llm.client.http.exchange.StreamHttpExchange
|
||||
import ee.carlrobert.llm.client.util.JSONUtil.*
|
||||
import org.apache.http.HttpHeaders
|
||||
import ee.carlrobert.codegpt.util.file.FileUtil.getResourceContent
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import testsupport.IntegrationTest
|
||||
import java.io.IOException
|
||||
|
|
@ -36,6 +37,8 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
|
|||
assertThat(request.uri.path).isEqualTo("/v1/chat/completions")
|
||||
assertThat(request.method).isEqualTo("POST")
|
||||
assertThat(request.headers[HttpHeaders.AUTHORIZATION]!![0]).isEqualTo("Bearer TEST_API_KEY")
|
||||
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
|
||||
assertThat(request.body)
|
||||
.extracting(
|
||||
"model",
|
||||
|
|
@ -44,7 +47,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
|
|||
.containsExactly(
|
||||
"gpt-4o",
|
||||
listOf(
|
||||
mapOf("role" to "system", "content" to "TEST_SYSTEM_PROMPT\n"),
|
||||
mapOf("role" to "system", "content" to expectedSystem),
|
||||
mapOf("role" to "user", "content" to "Hello!")
|
||||
)
|
||||
)
|
||||
|
|
@ -108,6 +111,8 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
|
|||
assertThat(request.uri.path).isEqualTo("/v1/chat/completions")
|
||||
assertThat(request.method).isEqualTo("POST")
|
||||
assertThat(request.headers[HttpHeaders.AUTHORIZATION]!![0]).isEqualTo("Bearer TEST_API_KEY")
|
||||
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
|
||||
assertThat(request.body)
|
||||
.extracting(
|
||||
"model",
|
||||
|
|
@ -116,7 +121,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
|
|||
.containsExactly(
|
||||
"gpt-4o",
|
||||
listOf(
|
||||
mapOf("role" to "system", "content" to "TEST_SYSTEM_PROMPT\n"),
|
||||
mapOf("role" to "system", "content" to expectedSystem),
|
||||
mapOf(
|
||||
"role" to "user",
|
||||
"content" to """
|
||||
|
|
@ -203,6 +208,8 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
|
|||
assertThat(request.method).isEqualTo("POST")
|
||||
assertThat(request.headers[HttpHeaders.AUTHORIZATION]!![0]).isEqualTo("Bearer TEST_API_KEY")
|
||||
try {
|
||||
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
|
||||
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
|
||||
val testImageUrl = ("data:image/png;base64,"
|
||||
+ Base64.getEncoder()
|
||||
.encodeToString(Files.readAllBytes(Path.of(testImagePath))))
|
||||
|
|
@ -211,7 +218,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
|
|||
.containsExactly(
|
||||
"gpt-4-vision-preview",
|
||||
listOf(
|
||||
mapOf("role" to "system", "content" to "TEST_SYSTEM_PROMPT\n"),
|
||||
mapOf("role" to "system", "content" to expectedSystem),
|
||||
mapOf(
|
||||
"role" to "user", "content" to listOf(
|
||||
mapOf(
|
||||
|
|
@ -408,7 +415,9 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
|
|||
)
|
||||
.containsExactly(
|
||||
LLAMA.buildPrompt(
|
||||
"TEST_SYSTEM_PROMPT",
|
||||
(getResourceContent("/prompts/persona/psi-navigation-guidelines.txt").let { g ->
|
||||
"TEST_SYSTEM_PROMPT\n$g"
|
||||
}),
|
||||
"TEST_PROMPT",
|
||||
conversation.messages
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue