├── README.md ├── settings.gradle.kts ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── gradle.properties ├── src └── main │ ├── kotlin │ └── com │ │ └── example │ │ ├── HelloAction.kt │ │ ├── ContextPilotStatusBarWidgetFactory.kt │ │ ├── toolwindow │ │ ├── ContextPilotToolWindowFactory.kt │ │ ├── ContextAnalyzer.kt │ │ ├── ContextPilotToolWindowContent.kt │ │ ├── components │ │ │ ├── DiffPanel.kt │ │ │ ├── CommitsPanel.kt │ │ │ └── ContextFilesPanel.kt │ │ └── dialogs │ │ │ └── CommitDetailsDialog.kt │ │ ├── ContextPilotStatusBarWidget.kt │ │ ├── ContextPilotStartupActivity.kt │ │ ├── ContextPilotPlugin.kt │ │ └── actions │ │ ├── IndexWorkspaceAction.kt │ │ ├── GetCommitDescriptionsAction.kt │ │ ├── GetContextFilesAction.kt │ │ └── GenerateDiffsAction.kt │ └── resources │ └── META-INF │ └── plugin.xml ├── .gitignore ├── gradlew.bat └── gradlew /README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "contextpilot-intellij" -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Gradle memory settings 2 | org.gradle.jvmargs=-Xmx2g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 3 | 4 | # Opt-out flag for bundling Kotlin standard library 5 | kotlin.stdlib.default.dependency=false 6 | 7 | # Enable Gradle Daemon 8 | org.gradle.daemon=true 9 | 10 | # Enable parallel execution 11 | org.gradle.parallel=true 12 | 13 | # Enable configuration on demand 14 | org.gradle.configureondemand=true 15 | 16 | # Enable build cache 17 | org.gradle.caching=true -------------------------------------------------------------------------------- /src/main/kotlin/com/example/HelloAction.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.intellij.openapi.actionSystem.AnAction 4 | import com.intellij.openapi.actionSystem.AnActionEvent 5 | import com.intellij.openapi.ui.Messages 6 | 7 | class HelloAction : AnAction() { 8 | override fun actionPerformed(e: AnActionEvent) { 9 | Messages.showMessageDialog( 10 | e.project, 11 | "Hello from your IntelliJ Plugin!", 12 | "Hello", 13 | Messages.getInformationIcon() 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/ContextPilotStatusBarWidgetFactory.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.wm.StatusBar 5 | import com.intellij.openapi.wm.StatusBarWidget 6 | import com.intellij.openapi.wm.StatusBarWidgetFactory 7 | 8 | class ContextPilotStatusBarWidgetFactory : StatusBarWidgetFactory { 9 | override fun getId(): String = "ContextPilotWidget" 10 | override fun getDisplayName(): String = "ContextPilot" 11 | override fun isAvailable(project: Project): Boolean = true 12 | override fun createWidget(project: Project): StatusBarWidget = ContextPilotStatusBarWidget(project) 13 | override fun disposeWidget(widget: StatusBarWidget) { 14 | widget.dispose() 15 | } 16 | override fun canBeEnabledOn(statusBar: StatusBar): Boolean = true 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ IDEA 2 | .idea/ 3 | *.iml 4 | *.iws 5 | *.ipr 6 | out/ 7 | 8 | # Gradle 9 | .gradle/ 10 | build/ 11 | gradle-app.setting 12 | !gradle-wrapper.jar 13 | .gradletasknamecache 14 | 15 | # Kotlin 16 | *.class 17 | *.log 18 | *.ctxt 19 | *.jar 20 | *.war 21 | *.nar 22 | *.ear 23 | *.zip 24 | *.tar.gz 25 | *.rar 26 | hs_err_pid* 27 | replay_pid* 28 | 29 | # Mac OS 30 | .DS_Store 31 | .AppleDouble 32 | .LSOverride 33 | Icon 34 | ._* 35 | 36 | # Windows 37 | Thumbs.db 38 | Thumbs.db:encryptable 39 | ehthumbs.db 40 | ehthumbs_vista.db 41 | *.stackdump 42 | [Dd]esktop.ini 43 | 44 | # Linux 45 | *~ 46 | .fuse_hidden* 47 | .directory 48 | .Trash-* 49 | .nfs* 50 | 51 | # Logs and databases 52 | *.log 53 | *.sqlite 54 | *.db 55 | 56 | # Plugin-specific 57 | !libs/*.jar # Keep any required plugin dependency JARs 58 | sandbox/ # IntelliJ sandbox directory 59 | 60 | # Test output 61 | test-output/ 62 | reports/ 63 | coverage/ 64 | 65 | # Heap dumps 66 | *.hprof -------------------------------------------------------------------------------- /src/main/kotlin/com/example/toolwindow/ContextPilotToolWindowFactory.kt: -------------------------------------------------------------------------------- 1 | package com.example.toolwindow 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.wm.ToolWindow 5 | import com.intellij.openapi.wm.ToolWindowFactory 6 | import com.intellij.ui.content.ContentFactory 7 | import com.example.ContextPilotService 8 | import com.intellij.openapi.fileEditor.FileEditorManager 9 | import com.intellij.openapi.fileEditor.FileEditorManagerListener 10 | import com.intellij.openapi.vfs.VirtualFile 11 | import com.intellij.util.messages.MessageBusConnection 12 | 13 | class ContextPilotToolWindowFactory : ToolWindowFactory { 14 | override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { 15 | val contentFactory = ContentFactory.getInstance() 16 | val content = contentFactory.createContent( 17 | ContextPilotToolWindowContent(project), 18 | "", 19 | false 20 | ) 21 | toolWindow.contentManager.addContent(content) 22 | } 23 | 24 | override fun shouldBeAvailable(project: Project) = true 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/ContextPilotStatusBarWidget.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.intellij.openapi.actionSystem.ActionGroup 4 | import com.intellij.openapi.actionSystem.ActionManager 5 | import com.intellij.openapi.actionSystem.ActionPlaces 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.openapi.wm.StatusBar 8 | import com.intellij.openapi.wm.StatusBarWidget 9 | import com.intellij.openapi.wm.StatusBarWidget.WidgetPresentation 10 | import com.intellij.util.Consumer 11 | import java.awt.event.MouseEvent 12 | import javax.swing.Icon 13 | import com.intellij.icons.AllIcons 14 | import com.intellij.openapi.ui.popup.JBPopupFactory 15 | import com.intellij.openapi.ui.popup.ListPopup 16 | import com.intellij.openapi.actionSystem.DataContext 17 | import com.intellij.openapi.ui.popup.JBPopupFactory.ActionSelectionAid 18 | 19 | class ContextPilotStatusBarWidget(private val project: Project) : StatusBarWidget, StatusBarWidget.MultipleTextValuesPresentation { 20 | private var statusBar: StatusBar? = null 21 | 22 | override fun ID(): String = "ContextPilotWidget" 23 | 24 | override fun getPresentation(): WidgetPresentation = this 25 | 26 | override fun install(statusBar: StatusBar) { 27 | this.statusBar = statusBar 28 | } 29 | 30 | override fun dispose() { 31 | statusBar = null 32 | } 33 | 34 | override fun getTooltipText(): String = "Click to show ContextPilot actions" 35 | 36 | override fun getSelectedValue(): String = "ContextPilot" 37 | 38 | override fun getIcon(): Icon = AllIcons.Actions.Search 39 | 40 | override fun getClickConsumer(): Consumer? = Consumer { event -> 41 | val actionManager = ActionManager.getInstance() 42 | val group = actionManager.getAction("com.example.ContextPilot.MainMenu") as? ActionGroup ?: return@Consumer 43 | 44 | val popup: ListPopup = JBPopupFactory.getInstance() 45 | .createActionGroupPopup( 46 | "ContextPilot Actions", 47 | group, 48 | DataContext.EMPTY_CONTEXT, 49 | ActionSelectionAid.SPEEDSEARCH, 50 | true, 51 | null 52 | ) 53 | 54 | val component = event.component 55 | popup.showUnderneathOf(component) 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/ContextPilotStartupActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.startup.ProjectActivity 5 | import com.intellij.openapi.progress.ProgressManager 6 | import com.intellij.openapi.progress.Task 7 | import com.intellij.openapi.progress.ProgressIndicator 8 | import java.io.BufferedReader 9 | import java.io.InputStreamReader 10 | import java.io.File 11 | 12 | class ContextPilotStartupActivity : ProjectActivity { 13 | override suspend fun execute(project: Project) { 14 | val service = ContextPilotService.getInstance(project) 15 | 16 | if (!service.checkContextPilotVersion()) { 17 | // Don't show error on startup, just return 18 | return 19 | } 20 | 21 | val binaryPath = service.getContextPilotPath() ?: return 22 | val workspacePath = project.basePath ?: return 23 | 24 | // Always use absolute paths 25 | val absoluteWorkspacePath = File(workspacePath).absolutePath 26 | 27 | ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Indexing Workspace", false) { 28 | override fun run(indicator: ProgressIndicator) { 29 | indicator.isIndeterminate = false 30 | indicator.fraction = 0.0 31 | 32 | val command = "$binaryPath $absoluteWorkspacePath -t index" 33 | 34 | try { 35 | val process = ProcessBuilder(command.split(" ")) 36 | .directory(File(workspacePath)) 37 | .redirectErrorStream(true) 38 | .start() 39 | 40 | var filesIndexed = 0 41 | BufferedReader(InputStreamReader(process.inputStream)).use { reader -> 42 | var line: String? 43 | while (reader.readLine().also { line = it } != null) { 44 | if (line?.contains("Indexing file:") == true) { 45 | filesIndexed++ 46 | indicator.fraction = (filesIndexed % 100) / 100.0 47 | indicator.text = "Indexed $filesIndexed files..." 48 | } 49 | } 50 | } 51 | 52 | val exitCode = process.waitFor() 53 | if (exitCode == 0) { 54 | service.setLastIndexTime(System.currentTimeMillis() / 1000) 55 | } 56 | } catch (ex: Exception) { 57 | // Log error but don't show dialog on startup 58 | ex.printStackTrace() 59 | } 60 | } 61 | }) 62 | } 63 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | com.example.context-pilot 4 | Context Pilot 5 | Your Company 6 | Intelligent context-aware navigation for your codebase 7 | 8 | com.intellij.modules.platform 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/toolwindow/ContextAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package com.example.toolwindow 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.vfs.VirtualFile 5 | import com.intellij.openapi.application.ApplicationManager 6 | import com.intellij.diff.DiffContentFactory 7 | import com.intellij.diff.DiffManager 8 | import com.intellij.diff.requests.SimpleDiffRequest 9 | import com.intellij.openapi.vcs.VcsException 10 | import com.intellij.openapi.vcs.changes.Change 11 | import com.intellij.openapi.vcs.changes.ChangeListManager 12 | import com.intellij.openapi.vcs.history.VcsRevisionNumber 13 | import kotlinx.coroutines.* 14 | import com.example.ContextPilotService 15 | 16 | class ContextAnalyzer(private val project: Project) { 17 | private val scope = CoroutineScope(Dispatchers.IO + Job()) 18 | 19 | fun analyze(file: VirtualFile, callback: (AnalysisResult) -> Unit) { 20 | scope.launch { 21 | val result = AnalysisResult() 22 | 23 | // Run analyses in parallel 24 | val deferredResults = listOf( 25 | async { analyzeDiffs(file, result) }, 26 | async { analyzeContextFiles(file, result) }, 27 | async { analyzeCommits(file, result) } 28 | ) 29 | 30 | // Wait for all analyses to complete 31 | deferredResults.awaitAll() 32 | 33 | // Update UI on main thread 34 | withContext(Dispatchers.Main) { 35 | callback(result) 36 | } 37 | } 38 | } 39 | 40 | private suspend fun analyzeDiffs(file: VirtualFile, result: AnalysisResult) { 41 | val changeListManager = ChangeListManager.getInstance(project) 42 | val change = changeListManager.getChange(file) 43 | 44 | if (change != null) { 45 | result.diffs.add(DiffInfo( 46 | title = "Current Changes", 47 | change = change 48 | )) 49 | } 50 | } 51 | 52 | private suspend fun analyzeContextFiles(file: VirtualFile, result: AnalysisResult) { 53 | val contextPilotService = ContextPilotService.getInstance(project) 54 | // TODO: Implement context file analysis using ContextPilot service 55 | } 56 | 57 | private suspend fun analyzeCommits(file: VirtualFile, result: AnalysisResult) { 58 | // TODO: Implement commit history analysis 59 | } 60 | 61 | fun dispose() { 62 | scope.cancel() 63 | } 64 | } 65 | 66 | data class AnalysisResult( 67 | val diffs: MutableList = mutableListOf(), 68 | val contextFiles: MutableList = mutableListOf(), 69 | val commits: MutableList = mutableListOf() 70 | ) 71 | 72 | data class DiffInfo( 73 | val title: String, 74 | val change: Change 75 | ) 76 | 77 | data class ContextFileInfo( 78 | val file: VirtualFile, 79 | val relevanceScore: Double, 80 | val reason: String 81 | ) 82 | 83 | data class CommitInfo( 84 | val hash: String, 85 | val message: String, 86 | val author: String, 87 | val date: String 88 | ) -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/toolwindow/ContextPilotToolWindowContent.kt: -------------------------------------------------------------------------------- 1 | package com.example.toolwindow 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.ui.components.JBLoadingPanel 5 | import com.intellij.ui.components.JBTabbedPane 6 | import com.intellij.util.ui.JBUI 7 | import com.intellij.util.ui.UIUtil 8 | import com.intellij.openapi.fileEditor.FileEditorManager 9 | import com.intellij.openapi.fileEditor.FileEditorManagerListener 10 | import com.intellij.openapi.fileEditor.FileEditorManagerEvent 11 | import com.intellij.openapi.vfs.VirtualFile 12 | import com.intellij.util.messages.MessageBusConnection 13 | import com.example.toolwindow.components.DiffPanel 14 | import com.example.toolwindow.components.ContextFilesPanel 15 | import com.example.toolwindow.components.CommitsPanel 16 | import java.awt.BorderLayout 17 | import javax.swing.JPanel 18 | import javax.swing.SwingUtilities 19 | 20 | class ContextPilotToolWindowContent(private val project: Project) : JPanel() { 21 | private val loadingPanel: JBLoadingPanel 22 | private val tabbedPane: JBTabbedPane 23 | private var messageBusConnection: MessageBusConnection? = null 24 | private val contextAnalyzer = ContextAnalyzer(project) 25 | private val diffPanel = DiffPanel(project) 26 | private val contextFilesPanel = ContextFilesPanel(project) 27 | private val commitsPanel = CommitsPanel(project) 28 | 29 | init { 30 | layout = BorderLayout() 31 | 32 | // Create loading panel 33 | loadingPanel = JBLoadingPanel(BorderLayout(), project).apply { 34 | setLoadingText("Analyzing context...") 35 | } 36 | 37 | // Create tabbed pane with sections 38 | tabbedPane = JBTabbedPane().apply { 39 | add("Diffs", diffPanel) 40 | add("Context Files", contextFilesPanel) 41 | add("Related Commits", commitsPanel) 42 | } 43 | 44 | // Add components 45 | loadingPanel.add(tabbedPane, BorderLayout.CENTER) 46 | add(loadingPanel, BorderLayout.CENTER) 47 | 48 | // Setup file change listener 49 | setupFileChangeListener() 50 | } 51 | 52 | private fun setupFileChangeListener() { 53 | messageBusConnection = project.messageBus.connect() 54 | messageBusConnection?.subscribe( 55 | FileEditorManagerListener.FILE_EDITOR_MANAGER, 56 | object : FileEditorManagerListener { 57 | override fun fileOpened(source: FileEditorManager, file: VirtualFile) { 58 | updateContent(file) 59 | } 60 | 61 | override fun selectionChanged(event: FileEditorManagerEvent) { 62 | event.newFile?.let { updateContent(it) } 63 | } 64 | } 65 | ) 66 | } 67 | 68 | private fun updateContent(file: VirtualFile) { 69 | SwingUtilities.invokeLater { 70 | loadingPanel.startLoading() 71 | 72 | contextAnalyzer.analyze(file) { result -> 73 | diffPanel.setDiffs(result.diffs) 74 | contextFilesPanel.setContextFiles(result.contextFiles) 75 | commitsPanel.setCommits(result.commits) 76 | loadingPanel.stopLoading() 77 | } 78 | } 79 | } 80 | 81 | fun dispose() { 82 | messageBusConnection?.disconnect() 83 | contextAnalyzer.dispose() 84 | } 85 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/toolwindow/components/DiffPanel.kt: -------------------------------------------------------------------------------- 1 | package com.example.toolwindow.components 2 | 3 | import com.intellij.diff.DiffContentFactory 4 | import com.intellij.diff.DiffManager 5 | import com.intellij.diff.requests.SimpleDiffRequest 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.ui.components.JBList 8 | import com.intellij.ui.components.JBScrollPane 9 | import com.intellij.util.ui.JBUI 10 | import com.intellij.util.ui.UIUtil 11 | import com.example.toolwindow.DiffInfo 12 | import com.intellij.openapi.fileTypes.FileTypes 13 | import java.awt.BorderLayout 14 | import java.awt.Component 15 | import javax.swing.* 16 | import javax.swing.border.EmptyBorder 17 | 18 | class DiffPanel(private val project: Project) : JPanel() { 19 | private val listModel = DefaultListModel() 20 | private val list = JBList(listModel).apply { 21 | cellRenderer = DiffCellRenderer() 22 | selectionMode = ListSelectionModel.SINGLE_SELECTION 23 | } 24 | 25 | init { 26 | layout = BorderLayout() 27 | background = UIUtil.getListBackground() 28 | border = JBUI.Borders.empty(10) 29 | 30 | // Add list with scroll 31 | add(JBScrollPane(list), BorderLayout.CENTER) 32 | 33 | // Add list selection listener 34 | list.addListSelectionListener { e -> 35 | if (!e.valueIsAdjusting) { 36 | val selectedDiff = list.selectedValue 37 | if (selectedDiff != null) { 38 | showDiff(selectedDiff) 39 | } 40 | } 41 | } 42 | } 43 | 44 | fun setDiffs(diffs: List) { 45 | listModel.clear() 46 | diffs.forEach { listModel.addElement(it) } 47 | } 48 | 49 | private fun showDiff(diffInfo: DiffInfo) { 50 | val change = diffInfo.change 51 | val contentFactory = DiffContentFactory.getInstance() 52 | 53 | val beforeContent = change.beforeRevision?.let { 54 | contentFactory.create(it.content ?: "", it.file?.fileType ?: FileTypes.PLAIN_TEXT) 55 | } ?: contentFactory.createEmpty() 56 | 57 | val afterContent = change.afterRevision?.let { 58 | contentFactory.create(it.content ?: "", it.file?.fileType ?: FileTypes.PLAIN_TEXT) 59 | } ?: contentFactory.createEmpty() 60 | 61 | val request = SimpleDiffRequest( 62 | diffInfo.title, 63 | beforeContent, 64 | afterContent, 65 | change.beforeRevision?.file?.name ?: "Base version", 66 | change.afterRevision?.file?.name ?: "Current version" 67 | ) 68 | 69 | DiffManager.getInstance().showDiff(project, request) 70 | } 71 | } 72 | 73 | private class DiffCellRenderer : ListCellRenderer { 74 | private val panel = JPanel(BorderLayout()).apply { 75 | border = EmptyBorder(8, 8, 8, 8) 76 | } 77 | private val titleLabel = JLabel().apply { 78 | font = UIUtil.getLabelFont() 79 | } 80 | 81 | override fun getListCellRendererComponent( 82 | list: JList, 83 | value: DiffInfo, 84 | index: Int, 85 | isSelected: Boolean, 86 | cellHasFocus: Boolean 87 | ): Component { 88 | panel.background = if (isSelected) UIUtil.getListSelectionBackground(true) 89 | else UIUtil.getListBackground() 90 | titleLabel.foreground = if (isSelected) UIUtil.getListSelectionForeground(true) 91 | else UIUtil.getListForeground() 92 | 93 | titleLabel.text = value.title 94 | panel.removeAll() 95 | panel.add(titleLabel, BorderLayout.CENTER) 96 | 97 | return panel 98 | } 99 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/toolwindow/dialogs/CommitDetailsDialog.kt: -------------------------------------------------------------------------------- 1 | package com.example.toolwindow.dialogs 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.ui.DialogWrapper 5 | import com.intellij.ui.components.JBScrollPane 6 | import com.intellij.ui.components.JBTabbedPane 7 | import com.intellij.util.ui.JBUI 8 | import com.intellij.util.ui.UIUtil 9 | import com.example.toolwindow.CommitInfo 10 | import com.intellij.icons.AllIcons 11 | import com.intellij.ui.components.JBLabel 12 | import com.intellij.ui.components.JBTextArea 13 | import com.intellij.ide.ClipboardSynchronizer 14 | import com.intellij.openapi.ide.CopyPasteManager 15 | import java.awt.BorderLayout 16 | import java.awt.Dimension 17 | import java.awt.FlowLayout 18 | import java.awt.Font 19 | import java.awt.datatransfer.StringSelection 20 | import javax.swing.* 21 | import javax.swing.border.EmptyBorder 22 | 23 | class CommitDetailsDialog( 24 | project: Project, 25 | private val commitInfo: CommitInfo 26 | ) : DialogWrapper(project) { 27 | 28 | init { 29 | title = "Commit Details" 30 | init() 31 | } 32 | 33 | override fun createCenterPanel(): JComponent { 34 | val panel = JPanel(BorderLayout(10, 10)).apply { 35 | border = JBUI.Borders.empty(10) 36 | preferredSize = Dimension(600, 400) 37 | } 38 | 39 | // Header panel with commit hash and author 40 | val headerPanel = JPanel(BorderLayout()).apply { 41 | border = JBUI.Borders.empty(0, 0, 10, 0) 42 | 43 | // Hash and copy button 44 | val hashPanel = JPanel(FlowLayout(FlowLayout.LEFT, 5, 0)) 45 | val hashLabel = JBLabel(commitInfo.hash).apply { 46 | font = UIUtil.getLabelFont().deriveFont(Font.BOLD) 47 | icon = AllIcons.Vcs.CommitNode 48 | } 49 | val copyButton = JButton(AllIcons.Actions.Copy).apply { 50 | toolTipText = "Copy commit hash" 51 | addActionListener { 52 | // Copy hash to clipboard using IntelliJ's API 53 | CopyPasteManager.getInstance().setContents(StringSelection(commitInfo.hash)) 54 | } 55 | } 56 | hashPanel.add(hashLabel) 57 | hashPanel.add(copyButton) 58 | 59 | // Author and date 60 | val authorPanel = JPanel(FlowLayout(FlowLayout.LEFT, 5, 0)) 61 | val authorLabel = JBLabel(commitInfo.author).apply { 62 | icon = AllIcons.Vcs.Author 63 | } 64 | val dateLabel = JBLabel(commitInfo.date).apply { 65 | icon = AllIcons.Vcs.History 66 | } 67 | authorPanel.add(authorLabel) 68 | authorPanel.add(dateLabel) 69 | 70 | add(hashPanel, BorderLayout.NORTH) 71 | add(authorPanel, BorderLayout.CENTER) 72 | } 73 | 74 | // Message panel 75 | val messagePanel = JPanel(BorderLayout()).apply { 76 | border = JBUI.Borders.empty(10) 77 | 78 | val messageLabel = JBLabel("Commit Message:").apply { 79 | font = UIUtil.getLabelFont().deriveFont(Font.BOLD) 80 | } 81 | 82 | val messageArea = JBTextArea(commitInfo.message).apply { 83 | isEditable = false 84 | wrapStyleWord = true 85 | lineWrap = true 86 | background = UIUtil.getPanelBackground() 87 | border = JBUI.Borders.empty(5) 88 | font = UIUtil.getLabelFont() 89 | } 90 | 91 | add(messageLabel, BorderLayout.NORTH) 92 | add(JBScrollPane(messageArea), BorderLayout.CENTER) 93 | } 94 | 95 | // Add all components 96 | panel.add(headerPanel, BorderLayout.NORTH) 97 | panel.add(messagePanel, BorderLayout.CENTER) 98 | 99 | return panel 100 | } 101 | 102 | override fun createActions(): Array = arrayOf(okAction) 103 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/ContextPilotPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.intellij.openapi.actionSystem.* 4 | import com.intellij.openapi.components.Service 5 | import com.intellij.openapi.components.service 6 | import com.intellij.openapi.diagnostic.Logger 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.openapi.wm.StatusBar 9 | import com.intellij.openapi.wm.StatusBarWidget 10 | import com.intellij.openapi.wm.WindowManager 11 | import com.intellij.ide.DataManager 12 | import java.io.File 13 | import java.nio.file.Paths 14 | import java.awt.event.MouseEvent 15 | 16 | @Service(Service.Level.PROJECT) 17 | class ContextPilotService(private val project: Project) { 18 | private val logger = Logger.getInstance(ContextPilotService::class.java) 19 | private var lastIndexTime: Long? = null 20 | private var cachedVersionCheck: Boolean? = null 21 | private var contextPilotPath: String? = null 22 | private val MIN_CONTEXTPILOT_VERSION = "0.9.0" 23 | 24 | companion object { 25 | fun getInstance(project: Project): ContextPilotService = project.service() 26 | } 27 | 28 | fun parseVersion(versionStr: String): Triple { 29 | val match = Regex("(\\d+)\\.(\\d+)\\.(\\d+)").find(versionStr) 30 | return if (match != null) { 31 | Triple( 32 | match.groupValues[1].toInt(), 33 | match.groupValues[2].toInt(), 34 | match.groupValues[3].toInt() 35 | ) 36 | } else { 37 | Triple(0, 0, 0) 38 | } 39 | } 40 | 41 | fun isVersionCompatible(installed: String, required: String): Boolean { 42 | val (imaj, imin, ipat) = parseVersion(installed) 43 | val (rmaj, rmin, rpat) = parseVersion(required) 44 | if (imaj != rmaj) return imaj > rmaj 45 | if (imin != rmin) return imin > rmin 46 | return ipat >= rpat 47 | } 48 | 49 | fun getContextPilotPath(): String? = contextPilotPath 50 | 51 | fun checkContextPilotVersion(): Boolean { 52 | if (cachedVersionCheck != null && contextPilotPath != null) { 53 | return cachedVersionCheck!! 54 | } 55 | 56 | // Common installation paths to check 57 | val possiblePaths = listOf( 58 | "contextpilot", // Check PATH 59 | "${System.getProperty("user.home")}/.local/bin/contextpilot", 60 | "${System.getProperty("user.home")}/.cargo/bin/contextpilot", 61 | "/usr/local/bin/contextpilot", 62 | "/usr/bin/contextpilot" 63 | ) 64 | 65 | for (path in possiblePaths) { 66 | try { 67 | val process = ProcessBuilder(path, "--version") 68 | .redirectErrorStream(true) 69 | .start() 70 | 71 | val output = process.inputStream.bufferedReader().readText() 72 | val exitCode = process.waitFor() 73 | 74 | if (exitCode == 0) { 75 | val match = Regex("contextpilot\\s+(\\d+\\.\\d+\\.\\d+)").find(output) 76 | if (match != null) { 77 | val version = match.groupValues[1] 78 | if (isVersionCompatible(version, MIN_CONTEXTPILOT_VERSION)) { 79 | logger.info("Found contextpilot v$version at $path") 80 | cachedVersionCheck = true 81 | contextPilotPath = path 82 | return true 83 | } 84 | } 85 | } 86 | } catch (e: Exception) { 87 | logger.debug("Failed to check version for path: $path", e) 88 | } 89 | } 90 | 91 | logger.warn("ContextPilot binary not found or version incompatible") 92 | cachedVersionCheck = false 93 | contextPilotPath = null 94 | return false 95 | } 96 | 97 | fun getLastIndexTime(): Long? = lastIndexTime 98 | 99 | fun setLastIndexTime(time: Long) { 100 | lastIndexTime = time 101 | } 102 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/toolwindow/components/CommitsPanel.kt: -------------------------------------------------------------------------------- 1 | package com.example.toolwindow.components 2 | 3 | import com.intellij.icons.AllIcons 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.ui.components.JBList 6 | import com.intellij.ui.components.JBScrollPane 7 | import com.intellij.util.ui.JBUI 8 | import com.intellij.util.ui.UIUtil 9 | import com.example.toolwindow.CommitInfo 10 | import com.example.toolwindow.dialogs.CommitDetailsDialog 11 | import java.awt.BorderLayout 12 | import java.awt.Component 13 | import java.awt.FlowLayout 14 | import javax.swing.* 15 | import javax.swing.border.EmptyBorder 16 | 17 | class CommitsPanel(private val project: Project) : JPanel() { 18 | private val listModel = DefaultListModel() 19 | private val list = JBList(listModel).apply { 20 | cellRenderer = CommitCellRenderer() 21 | selectionMode = ListSelectionModel.SINGLE_SELECTION 22 | } 23 | 24 | init { 25 | layout = BorderLayout() 26 | background = UIUtil.getListBackground() 27 | border = JBUI.Borders.empty(10) 28 | 29 | // Add list with scroll 30 | add(JBScrollPane(list), BorderLayout.CENTER) 31 | 32 | // Add list selection listener 33 | list.addListSelectionListener { e -> 34 | if (!e.valueIsAdjusting) { 35 | val selectedCommit = list.selectedValue 36 | if (selectedCommit != null) { 37 | showCommitDetails(selectedCommit) 38 | } 39 | } 40 | } 41 | } 42 | 43 | private fun showCommitDetails(commit: CommitInfo) { 44 | CommitDetailsDialog(project, commit).show() 45 | } 46 | 47 | fun setCommits(commits: List) { 48 | listModel.clear() 49 | commits.forEach { listModel.addElement(it) } 50 | } 51 | } 52 | 53 | private class CommitCellRenderer : ListCellRenderer { 54 | private val panel = JPanel(BorderLayout()).apply { 55 | border = EmptyBorder(8, 8, 8, 8) 56 | } 57 | private val headerPanel = JPanel(FlowLayout(FlowLayout.LEFT, 5, 0)) 58 | private val commitIcon = JLabel(AllIcons.Vcs.CommitNode) 59 | private val hashLabel = JLabel().apply { 60 | font = UIUtil.getLabelFont(UIUtil.FontSize.SMALL) 61 | } 62 | private val messageLabel = JLabel().apply { 63 | font = UIUtil.getLabelFont() 64 | } 65 | private val detailsLabel = JLabel().apply { 66 | font = UIUtil.getLabelFont(UIUtil.FontSize.SMALL) 67 | } 68 | 69 | init { 70 | headerPanel.add(commitIcon) 71 | headerPanel.add(hashLabel) 72 | headerPanel.isOpaque = false 73 | } 74 | 75 | override fun getListCellRendererComponent( 76 | list: JList, 77 | value: CommitInfo, 78 | index: Int, 79 | isSelected: Boolean, 80 | cellHasFocus: Boolean 81 | ): Component { 82 | panel.background = if (isSelected) UIUtil.getListSelectionBackground(true) 83 | else UIUtil.getListBackground() 84 | 85 | val textColor = if (isSelected) UIUtil.getListSelectionForeground(true) 86 | else UIUtil.getListForeground() 87 | 88 | // Set commit hash 89 | hashLabel.text = value.hash.substring(0, 7) 90 | hashLabel.foreground = if (isSelected) textColor 91 | else UIUtil.getContextHelpForeground() 92 | 93 | // Set commit message 94 | messageLabel.text = value.message.lines().first() // Show first line only 95 | messageLabel.foreground = textColor 96 | 97 | // Set commit details 98 | detailsLabel.text = "${value.author} on ${value.date}" 99 | detailsLabel.foreground = if (isSelected) textColor 100 | else UIUtil.getContextHelpForeground() 101 | 102 | // Layout components 103 | panel.removeAll() 104 | panel.add(headerPanel, BorderLayout.NORTH) 105 | panel.add(messageLabel, BorderLayout.CENTER) 106 | panel.add(detailsLabel, BorderLayout.SOUTH) 107 | 108 | return panel 109 | } 110 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/toolwindow/components/ContextFilesPanel.kt: -------------------------------------------------------------------------------- 1 | package com.example.toolwindow.components 2 | 3 | import com.intellij.icons.AllIcons 4 | import com.intellij.ide.FileIconProvider 5 | import com.intellij.openapi.fileEditor.FileEditorManager 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.ui.components.JBList 8 | import com.intellij.ui.components.JBScrollPane 9 | import com.intellij.util.ui.JBUI 10 | import com.intellij.util.ui.UIUtil 11 | import com.example.toolwindow.ContextFileInfo 12 | import com.intellij.openapi.vfs.VirtualFileManager 13 | import com.intellij.openapi.util.IconLoader 14 | import java.awt.BorderLayout 15 | import java.awt.Component 16 | import java.awt.FlowLayout 17 | import javax.swing.* 18 | import javax.swing.border.EmptyBorder 19 | 20 | class ContextFilesPanel(private val project: Project) : JPanel() { 21 | private val listModel = DefaultListModel() 22 | private val list = JBList(listModel).apply { 23 | cellRenderer = ContextFileCellRenderer(project) 24 | selectionMode = ListSelectionModel.SINGLE_SELECTION 25 | } 26 | 27 | init { 28 | layout = BorderLayout() 29 | background = UIUtil.getListBackground() 30 | border = JBUI.Borders.empty(10) 31 | 32 | // Add list with scroll 33 | add(JBScrollPane(list), BorderLayout.CENTER) 34 | 35 | // Add list selection listener 36 | list.addListSelectionListener { e -> 37 | if (!e.valueIsAdjusting) { 38 | val selectedFile = list.selectedValue 39 | if (selectedFile != null) { 40 | FileEditorManager.getInstance(project).openFile(selectedFile.file, true) 41 | } 42 | } 43 | } 44 | } 45 | 46 | fun setContextFiles(files: List) { 47 | listModel.clear() 48 | files.forEach { listModel.addElement(it) } 49 | } 50 | } 51 | 52 | private class ContextFileCellRenderer(private val project: Project) : ListCellRenderer { 53 | private val panel = JPanel(BorderLayout()).apply { 54 | border = EmptyBorder(8, 8, 8, 8) 55 | } 56 | private val filePanel = JPanel(FlowLayout(FlowLayout.LEFT, 5, 0)) 57 | private val fileIcon = JLabel() 58 | private val fileName = JLabel() 59 | private val relevanceLabel = JLabel().apply { 60 | font = UIUtil.getLabelFont(UIUtil.FontSize.SMALL) 61 | } 62 | private val reasonLabel = JLabel().apply { 63 | font = UIUtil.getLabelFont(UIUtil.FontSize.SMALL) 64 | } 65 | 66 | init { 67 | filePanel.add(fileIcon) 68 | filePanel.add(fileName) 69 | filePanel.isOpaque = false 70 | } 71 | 72 | override fun getListCellRendererComponent( 73 | list: JList, 74 | value: ContextFileInfo, 75 | index: Int, 76 | isSelected: Boolean, 77 | cellHasFocus: Boolean 78 | ): Component { 79 | panel.background = if (isSelected) UIUtil.getListSelectionBackground(true) 80 | else UIUtil.getListBackground() 81 | 82 | val textColor = if (isSelected) UIUtil.getListSelectionForeground(true) 83 | else UIUtil.getListForeground() 84 | 85 | // Set file icon and name 86 | fileIcon.icon = value.file.fileType.icon 87 | ?: AllIcons.FileTypes.Any_type 88 | fileName.text = value.file.name 89 | fileName.foreground = textColor 90 | 91 | // Set relevance score with progress bar 92 | val relevancePercent = (value.relevanceScore * 100).toInt() 93 | relevanceLabel.text = "Relevance: $relevancePercent%" 94 | relevanceLabel.foreground = textColor 95 | 96 | // Set reason with icon 97 | reasonLabel.icon = AllIcons.General.Information 98 | reasonLabel.text = value.reason 99 | reasonLabel.foreground = if (isSelected) textColor 100 | else UIUtil.getContextHelpForeground() 101 | 102 | // Layout components 103 | panel.removeAll() 104 | panel.add(filePanel, BorderLayout.NORTH) 105 | panel.add(relevanceLabel, BorderLayout.CENTER) 106 | panel.add(reasonLabel, BorderLayout.SOUTH) 107 | 108 | return panel 109 | } 110 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/actions/IndexWorkspaceAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.actions 2 | 3 | import com.example.ContextPilotService 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.intellij.openapi.application.ApplicationManager 7 | import com.intellij.openapi.progress.ProgressIndicator 8 | import com.intellij.openapi.progress.ProgressManager 9 | import com.intellij.openapi.progress.Task 10 | import com.intellij.openapi.project.Project 11 | import com.intellij.openapi.ui.Messages 12 | import java.io.BufferedReader 13 | import java.io.InputStreamReader 14 | 15 | class IndexWorkspaceAction : AnAction() { 16 | override fun actionPerformed(e: AnActionEvent) { 17 | val project = e.project ?: return 18 | val service = ContextPilotService.getInstance(project) 19 | 20 | if (!service.checkContextPilotVersion()) { 21 | Messages.showErrorDialog( 22 | project, 23 | "ContextPilot binary not found or version incompatible. Please check installation.", 24 | "ContextPilot Error" 25 | ) 26 | return 27 | } 28 | 29 | val binaryPath = service.getContextPilotPath() 30 | if (binaryPath == null) { 31 | Messages.showErrorDialog( 32 | project, 33 | "ContextPilot binary path not found. This is unexpected as version check passed.", 34 | "ContextPilot Error" 35 | ) 36 | return 37 | } 38 | 39 | ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Indexing Workspace", false) { 40 | override fun run(indicator: ProgressIndicator) { 41 | indicator.isIndeterminate = false 42 | indicator.fraction = 0.0 43 | 44 | val workspacePath = project.basePath ?: return 45 | val command = "$binaryPath $workspacePath -t index" 46 | 47 | try { 48 | val process = ProcessBuilder(command.split(" ")) 49 | .directory(project.basePath?.let { java.io.File(it) }) 50 | .redirectErrorStream(true) 51 | .start() 52 | 53 | var filesIndexed = 0 54 | BufferedReader(InputStreamReader(process.inputStream)).use { reader -> 55 | var line: String? 56 | while (reader.readLine().also { line = it } != null) { 57 | if (line?.contains("Indexing file:") == true) { 58 | filesIndexed++ 59 | indicator.fraction = (filesIndexed % 100) / 100.0 60 | indicator.text = "Indexed $filesIndexed files..." 61 | } 62 | } 63 | } 64 | 65 | val exitCode = process.waitFor() 66 | if (exitCode == 0) { 67 | service.setLastIndexTime(System.currentTimeMillis() / 1000) 68 | ApplicationManager.getApplication().invokeLater { 69 | Messages.showInfoMessage( 70 | project, 71 | "Successfully indexed workspace ($filesIndexed files)", 72 | "ContextPilot" 73 | ) 74 | } 75 | } else { 76 | ApplicationManager.getApplication().invokeLater { 77 | Messages.showErrorDialog( 78 | project, 79 | "Failed to index workspace (exit code: $exitCode)", 80 | "ContextPilot Error" 81 | ) 82 | } 83 | } 84 | } catch (ex: Exception) { 85 | ApplicationManager.getApplication().invokeLater { 86 | Messages.showErrorDialog( 87 | project, 88 | "Error indexing workspace: ${ex.message}", 89 | "ContextPilot Error" 90 | ) 91 | } 92 | } 93 | } 94 | }) 95 | } 96 | 97 | override fun update(e: AnActionEvent) { 98 | e.presentation.isEnabled = e.project != null 99 | } 100 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/actions/GetCommitDescriptionsAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.actions 2 | 3 | import com.example.ContextPilotService 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.intellij.openapi.actionSystem.CommonDataKeys 7 | import com.intellij.openapi.editor.Editor 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.ui.Messages 10 | import com.intellij.openapi.diagnostic.Logger 11 | import com.intellij.openapi.fileEditor.FileEditorManager 12 | import com.intellij.testFramework.LightVirtualFile 13 | import com.intellij.openapi.fileTypes.FileTypeManager 14 | import com.intellij.openapi.application.ApplicationManager 15 | import org.json.JSONArray 16 | import java.io.File 17 | import java.time.Instant 18 | import java.time.ZoneId 19 | import java.time.format.DateTimeFormatter 20 | 21 | class GetCommitDescriptionsAction : AnAction() { 22 | private val logger = Logger.getInstance(GetCommitDescriptionsAction::class.java) 23 | 24 | override fun actionPerformed(e: AnActionEvent) { 25 | val project = e.project ?: return 26 | val editor = e.getData(CommonDataKeys.EDITOR) ?: return 27 | val service = ContextPilotService.getInstance(project) 28 | 29 | if (!service.checkContextPilotVersion()) { 30 | Messages.showErrorDialog( 31 | project, 32 | "ContextPilot binary not found or version incompatible. Please check installation.", 33 | "ContextPilot Error" 34 | ) 35 | return 36 | } 37 | 38 | val binaryPath = service.getContextPilotPath() 39 | if (binaryPath == null) { 40 | Messages.showErrorDialog( 41 | project, 42 | "ContextPilot binary path not found. This is unexpected as version check passed.", 43 | "ContextPilot Error" 44 | ) 45 | return 46 | } 47 | 48 | val currentFile = e.getData(CommonDataKeys.VIRTUAL_FILE)?.path ?: return 49 | val workspacePath = project.basePath ?: return 50 | 51 | // Always use absolute paths 52 | val absoluteWorkspacePath = File(workspacePath).absolutePath 53 | val absoluteFilePath = File(currentFile).absolutePath 54 | 55 | // Get current line or selection 56 | val document = editor.document 57 | val selectionModel = editor.selectionModel 58 | val startLine = if (selectionModel.hasSelection()) { 59 | document.getLineNumber(selectionModel.selectionStart) + 1 60 | } else { 61 | editor.caretModel.logicalPosition.line + 1 62 | } 63 | val endLine = if (selectionModel.hasSelection()) { 64 | document.getLineNumber(selectionModel.selectionEnd) + 1 65 | } else { 66 | startLine 67 | } 68 | 69 | val command = "$binaryPath $absoluteWorkspacePath $absoluteFilePath -t desc -s $startLine -e $endLine" 70 | logger.info("Running command: $command") 71 | 72 | try { 73 | val process = ProcessBuilder(command.split(" ")) 74 | .directory(File(workspacePath)) 75 | .redirectErrorStream(true) 76 | .start() 77 | 78 | val output = process.inputStream.bufferedReader().readText() 79 | val exitCode = process.waitFor() 80 | 81 | logger.info("Command output: $output") 82 | logger.info("Exit code: $exitCode") 83 | 84 | if (exitCode == 0) { 85 | try { 86 | val jsonArray = JSONArray(output) 87 | logger.info("Found ${jsonArray.length()} commits") 88 | 89 | if (jsonArray.length() == 0) { 90 | Messages.showInfoMessage(project, "No commits found for the selected code", "ContextPilot") 91 | return 92 | } 93 | 94 | val markdownContent = StringBuilder() 95 | markdownContent.append("# Commit History Analysis\n\n") 96 | markdownContent.append("This file contains the relevant commit history for the selected code.\n\n") 97 | markdownContent.append("File: `$absoluteFilePath`\n") 98 | markdownContent.append("Lines: $startLine to $endLine\n\n") 99 | markdownContent.append("---\n\n") 100 | 101 | for (i in 0 until jsonArray.length()) { 102 | val commitArray = jsonArray.getJSONArray(i) 103 | val title = commitArray.getString(0) 104 | val description = commitArray.getString(1) 105 | val author = commitArray.getString(2) 106 | val date = commitArray.getString(3) 107 | val commitUrl = commitArray.getString(4) 108 | 109 | // Format the date to be more readable 110 | val parsedDate = try { 111 | val instant = Instant.parse(date) 112 | DateTimeFormatter.ofPattern("MMM dd, yyyy HH:mm:ss") 113 | .withZone(ZoneId.systemDefault()) 114 | .format(instant) 115 | } catch (e: Exception) { 116 | date // Fallback to original format if parsing fails 117 | } 118 | 119 | markdownContent.append("## ${title}\n\n") 120 | markdownContent.append("**Author:** ${author}\n\n") 121 | markdownContent.append("**Date:** ${parsedDate}\n\n") 122 | if (description.isNotEmpty()) { 123 | markdownContent.append("**Description:**\n${description}\n\n") 124 | } 125 | markdownContent.append("**Commit URL:** [View on GitHub](${commitUrl})\n\n") 126 | markdownContent.append("---\n\n") 127 | } 128 | 129 | // Create a virtual file with markdown content 130 | ApplicationManager.getApplication().invokeLater { 131 | val fileName = "Commit History - ${File(absoluteFilePath).name}.md" 132 | val virtualFile = LightVirtualFile( 133 | fileName, 134 | FileTypeManager.getInstance().getFileTypeByExtension("md"), 135 | markdownContent.toString() 136 | ) 137 | 138 | // Open the file in the editor 139 | FileEditorManager.getInstance(project).openFile(virtualFile, true) 140 | } 141 | } catch (ex: Exception) { 142 | logger.error("Error processing commit descriptions", ex) 143 | Messages.showErrorDialog( 144 | project, 145 | "Error processing commit descriptions: ${ex.message}", 146 | "ContextPilot Error" 147 | ) 148 | } 149 | } else { 150 | logger.error("Command failed with exit code $exitCode: $output") 151 | Messages.showErrorDialog( 152 | project, 153 | "Failed to get commit information (exit code: $exitCode)\nOutput: $output", 154 | "ContextPilot Error" 155 | ) 156 | } 157 | } catch (ex: Exception) { 158 | logger.error("Error getting commit descriptions", ex) 159 | Messages.showErrorDialog( 160 | project, 161 | "Error getting commit descriptions: ${ex.message}", 162 | "ContextPilot Error" 163 | ) 164 | } 165 | } 166 | 167 | override fun update(e: AnActionEvent) { 168 | val project = e.project 169 | val editor = e.getData(CommonDataKeys.EDITOR) 170 | e.presentation.isEnabled = project != null && editor != null 171 | } 172 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/actions/GetContextFilesAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.actions 2 | 3 | import com.example.ContextPilotService 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.intellij.openapi.actionSystem.CommonDataKeys 7 | import com.intellij.openapi.editor.Editor 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.ui.Messages 10 | import com.intellij.openapi.diagnostic.Logger 11 | import com.intellij.openapi.fileEditor.FileEditorManager 12 | import com.intellij.testFramework.LightVirtualFile 13 | import com.intellij.openapi.fileTypes.FileTypeManager 14 | import com.intellij.openapi.application.ApplicationManager 15 | import com.intellij.openapi.vfs.LocalFileSystem 16 | import com.intellij.openapi.fileEditor.OpenFileDescriptor 17 | import com.intellij.util.PathUtil 18 | import org.json.JSONArray 19 | import java.io.File 20 | import java.nio.file.Paths 21 | import com.intellij.openapi.ui.popup.PopupChooserBuilder 22 | import com.intellij.ui.SimpleListCellRenderer 23 | import com.intellij.ui.components.JBLabel 24 | import com.intellij.ui.components.JBList 25 | import com.intellij.util.ui.JBUI 26 | import com.intellij.icons.AllIcons 27 | import com.intellij.openapi.util.IconLoader 28 | import javax.swing.Icon 29 | import javax.swing.JList 30 | 31 | data class RelatedFile( 32 | val path: String, 33 | val occurrences: Int, 34 | val isDirectory: Boolean = false 35 | ) { 36 | val name: String get() = File(path).name 37 | val parentPath: String get() = File(path).parent ?: "" 38 | 39 | // Add toString() for search functionality 40 | override fun toString(): String = "$name $parentPath" 41 | } 42 | 43 | class GetContextFilesAction : AnAction() { 44 | private val logger = Logger.getInstance(GetContextFilesAction::class.java) 45 | 46 | override fun actionPerformed(e: AnActionEvent) { 47 | val project = e.project ?: return 48 | val editor = e.getData(CommonDataKeys.EDITOR) ?: return 49 | val service = ContextPilotService.getInstance(project) 50 | 51 | if (!service.checkContextPilotVersion()) { 52 | Messages.showErrorDialog( 53 | project, 54 | "ContextPilot binary not found or version incompatible. Please check installation.", 55 | "ContextPilot Error" 56 | ) 57 | return 58 | } 59 | 60 | val binaryPath = service.getContextPilotPath() 61 | if (binaryPath == null) { 62 | Messages.showErrorDialog( 63 | project, 64 | "ContextPilot binary path not found. This is unexpected as version check passed.", 65 | "ContextPilot Error" 66 | ) 67 | return 68 | } 69 | 70 | val currentFile = e.getData(CommonDataKeys.VIRTUAL_FILE)?.path ?: return 71 | val workspacePath = project.basePath ?: return 72 | 73 | // Always use absolute paths 74 | val absoluteWorkspacePath = File(workspacePath).absolutePath 75 | val absoluteFilePath = File(currentFile).absolutePath 76 | 77 | // Get current line or selection 78 | val document = editor.document 79 | val selectionModel = editor.selectionModel 80 | val startLine = if (selectionModel.hasSelection()) { 81 | document.getLineNumber(selectionModel.selectionStart) + 1 82 | } else { 83 | editor.caretModel.logicalPosition.line + 1 84 | } 85 | val endLine = if (selectionModel.hasSelection()) { 86 | document.getLineNumber(selectionModel.selectionEnd) + 1 87 | } else { 88 | startLine 89 | } 90 | 91 | val command = "$binaryPath $absoluteWorkspacePath $absoluteFilePath -t query -s $startLine -e $endLine" 92 | logger.info("Running command: $command") 93 | 94 | try { 95 | val process = ProcessBuilder(command.split(" ")) 96 | .directory(File(workspacePath)) 97 | .redirectErrorStream(true) 98 | .start() 99 | 100 | val output = process.inputStream.bufferedReader().readText() 101 | val exitCode = process.waitFor() 102 | 103 | logger.info("Command output: $output") 104 | logger.info("Exit code: $exitCode") 105 | 106 | if (exitCode == 0) { 107 | val relatedFiles = output.split("\n") 108 | .map { it.trim() } 109 | .filter { it.isNotEmpty() } 110 | .map { line -> 111 | val parts = line.split(" - ") 112 | val filePath = parts[0].trim() 113 | val occurrences = parts.getOrNull(1)?.replace(Regex("[^0-9]"), "")?.toIntOrNull() ?: 0 114 | RelatedFile(filePath, occurrences) 115 | } 116 | .sortedByDescending { it.occurrences } 117 | 118 | if (relatedFiles.isEmpty()) { 119 | Messages.showInfoMessage(project, "No related files found", "ContextPilot") 120 | return 121 | } 122 | 123 | // Create a list component 124 | val list = JBList(relatedFiles) 125 | 126 | // Create a custom renderer for the list items 127 | list.cellRenderer = object : SimpleListCellRenderer() { 128 | override fun customize( 129 | list: JList, 130 | value: RelatedFile?, 131 | index: Int, 132 | selected: Boolean, 133 | hasFocus: Boolean 134 | ) { 135 | if (value == null) return 136 | 137 | // Set icon based on file type 138 | icon = when { 139 | value.isDirectory -> AllIcons.Nodes.Folder 140 | value.name.endsWith(".kt") -> AllIcons.FileTypes.Text // Use generic text icon for Kotlin 141 | value.name.endsWith(".java") -> AllIcons.FileTypes.Java 142 | value.name.endsWith(".xml") -> AllIcons.FileTypes.Xml 143 | value.name.endsWith(".gradle") -> AllIcons.FileTypes.Text // Use generic text icon for Gradle 144 | else -> AllIcons.FileTypes.Any_type 145 | } 146 | 147 | // Create a formatted label with filename and path 148 | val label = JBLabel().apply { 149 | text = "" + 150 | "${value.name} " + 151 | "${value.parentPath} " + 152 | "(${value.occurrences} matches)" + 153 | "" 154 | border = JBUI.Borders.empty(2) 155 | } 156 | 157 | text = label.text 158 | } 159 | } 160 | 161 | // Create and show the popup 162 | PopupChooserBuilder(list) 163 | .setTitle("Related Files (${relatedFiles.size})") 164 | .setItemChoosenCallback { 165 | val selectedFile = list.selectedValue 166 | if (selectedFile != null) { 167 | val fullPath = Paths.get(workspacePath, selectedFile.path).normalize().toString() 168 | val virtualFile = LocalFileSystem.getInstance().findFileByPath(fullPath) 169 | if (virtualFile != null) { 170 | FileEditorManager.getInstance(project).openFile(virtualFile, true) 171 | } 172 | } 173 | } 174 | .createPopup() 175 | .showInBestPositionFor(editor) 176 | 177 | } else { 178 | logger.error("Command failed with exit code $exitCode: $output") 179 | Messages.showErrorDialog( 180 | project, 181 | "Failed to get related files (exit code: $exitCode)\nOutput: $output", 182 | "ContextPilot Error" 183 | ) 184 | } 185 | } catch (ex: Exception) { 186 | logger.error("Error getting related files", ex) 187 | Messages.showErrorDialog( 188 | project, 189 | "Error getting related files: ${ex.message}", 190 | "ContextPilot Error" 191 | ) 192 | } 193 | } 194 | 195 | override fun update(e: AnActionEvent) { 196 | val project = e.project 197 | val editor = e.getData(CommonDataKeys.EDITOR) 198 | e.presentation.isEnabled = project != null && editor != null 199 | } 200 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/actions/GenerateDiffsAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.actions 2 | 3 | import com.example.ContextPilotService 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.intellij.openapi.actionSystem.CommonDataKeys 7 | import com.intellij.openapi.editor.Editor 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.ui.Messages 10 | import com.intellij.openapi.diagnostic.Logger 11 | import com.intellij.openapi.fileEditor.FileDocumentManager 12 | import com.intellij.openapi.fileEditor.FileEditorManager 13 | import com.intellij.openapi.vfs.VirtualFileManager 14 | import com.intellij.openapi.vfs.LocalFileSystem 15 | import com.intellij.openapi.application.ApplicationManager 16 | import com.intellij.openapi.command.WriteCommandAction 17 | import com.intellij.openapi.fileTypes.FileTypeManager 18 | import com.intellij.openapi.fileTypes.PlainTextFileType 19 | import com.intellij.testFramework.LightVirtualFile 20 | import org.json.JSONArray 21 | import java.io.File 22 | import java.io.BufferedReader 23 | import java.time.Instant 24 | import java.time.ZoneId 25 | import java.time.format.DateTimeFormatter 26 | 27 | class GenerateDiffsAction : AnAction() { 28 | private val logger = Logger.getInstance(GenerateDiffsAction::class.java) 29 | 30 | override fun actionPerformed(e: AnActionEvent) { 31 | val project = e.project ?: return 32 | val editor = e.getData(CommonDataKeys.EDITOR) ?: return 33 | val service = ContextPilotService.getInstance(project) 34 | 35 | if (!service.checkContextPilotVersion()) { 36 | Messages.showErrorDialog( 37 | project, 38 | "ContextPilot binary not found or version incompatible. Please check installation.", 39 | "ContextPilot Error" 40 | ) 41 | return 42 | } 43 | 44 | val binaryPath = service.getContextPilotPath() 45 | if (binaryPath == null) { 46 | Messages.showErrorDialog( 47 | project, 48 | "ContextPilot binary path not found. This is unexpected as version check passed.", 49 | "ContextPilot Error" 50 | ) 51 | return 52 | } 53 | 54 | val currentFile = e.getData(CommonDataKeys.VIRTUAL_FILE)?.path ?: return 55 | val workspacePath = project.basePath ?: return 56 | 57 | // Always use absolute paths 58 | val absoluteWorkspacePath = File(workspacePath).absolutePath 59 | val absoluteFilePath = File(currentFile).absolutePath 60 | 61 | // Get current line or selection 62 | val document = editor.document 63 | val selectionModel = editor.selectionModel 64 | val startLine = if (selectionModel.hasSelection()) { 65 | document.getLineNumber(selectionModel.selectionStart) + 1 66 | } else { 67 | editor.caretModel.logicalPosition.line + 1 68 | } 69 | val endLine = if (selectionModel.hasSelection()) { 70 | document.getLineNumber(selectionModel.selectionEnd) + 1 71 | } else { 72 | startLine 73 | } 74 | 75 | // First get commit descriptions 76 | val command = "$binaryPath $absoluteWorkspacePath $absoluteFilePath -t desc -s $startLine -e $endLine" 77 | logger.info("Running command: $command") 78 | 79 | try { 80 | val process = ProcessBuilder(command.split(" ")) 81 | .directory(File(workspacePath)) 82 | .redirectErrorStream(true) 83 | .start() 84 | 85 | val output = process.inputStream.bufferedReader().readText() 86 | val exitCode = process.waitFor() 87 | 88 | logger.info("Command output: $output") 89 | logger.info("Exit code: $exitCode") 90 | 91 | if (exitCode == 0) { 92 | try { 93 | val jsonArray = JSONArray(output) 94 | logger.info("Found ${jsonArray.length()} commits") 95 | val commitDiffs = mutableListOf() 96 | val markdownContent = StringBuilder() 97 | 98 | markdownContent.append("# Git History Analysis\n\n") 99 | markdownContent.append("This file contains the relevant git history for analysis. You can use this information to understand the code evolution and context.\n\n") 100 | markdownContent.append("File: `$absoluteFilePath`\n") 101 | markdownContent.append("Lines: $startLine to $endLine\n\n") 102 | markdownContent.append("---\n\n") 103 | 104 | for (i in 0 until jsonArray.length()) { 105 | val commitArray = jsonArray.getJSONArray(i) 106 | val title = commitArray.getString(0) 107 | val description = commitArray.getString(1) 108 | val author = commitArray.getString(2) 109 | val date = commitArray.getString(3) 110 | val commitUrl = commitArray.getString(4) 111 | 112 | // Extract commit hash from URL or use full URL if not in expected format 113 | val commitHash = try { 114 | commitUrl.split("/").last() 115 | } catch (e: Exception) { 116 | logger.warn("Could not extract commit hash from URL: $commitUrl") 117 | commitUrl 118 | } 119 | 120 | logger.info("Processing commit: $commitHash") 121 | 122 | // Get git diff for this commit - use list for command to handle spaces properly 123 | val gitCommand = listOf("git", "show", commitHash, "--", absoluteFilePath) 124 | logger.info("Running git command: ${gitCommand.joinToString(" ")}") 125 | 126 | val gitProcess = ProcessBuilder(gitCommand) 127 | .directory(File(workspacePath)) 128 | .redirectErrorStream(true) 129 | .start() 130 | 131 | val diffOutput = gitProcess.inputStream.bufferedReader().use { reader -> 132 | reader.readText() 133 | } 134 | val gitExitCode = gitProcess.waitFor() 135 | 136 | logger.info("Git command exit code: $gitExitCode") 137 | logger.info("Git command output length: ${diffOutput.length}") 138 | 139 | if (gitExitCode == 0) { 140 | // Format the date to be more readable 141 | val parsedDate = try { 142 | val instant = Instant.parse(date) 143 | DateTimeFormatter.ofPattern("MMM dd, yyyy HH:mm:ss") 144 | .withZone(ZoneId.systemDefault()) 145 | .format(instant) 146 | } catch (e: Exception) { 147 | date // Fallback to original format if parsing fails 148 | } 149 | 150 | markdownContent.append("## Commit: ${commitHash}\n\n") 151 | markdownContent.append("**Title:** ${title}\n\n") 152 | markdownContent.append("**Author:** ${author}\n\n") 153 | markdownContent.append("**Date:** ${parsedDate}\n\n") 154 | if (description.isNotEmpty()) { 155 | markdownContent.append("**Description:**\n${description}\n\n") 156 | } 157 | markdownContent.append("**Changes:**\n") 158 | markdownContent.append("```diff\n${diffOutput}\n```\n\n") 159 | markdownContent.append("**Commit URL:** ${commitUrl}\n\n") 160 | markdownContent.append("---\n\n") 161 | 162 | logger.info("Added diff for commit $commitHash") 163 | } else { 164 | logger.warn("Git command failed for commit $commitHash with exit code $gitExitCode") 165 | logger.warn("Git command output: $diffOutput") 166 | } 167 | } 168 | 169 | if (markdownContent.length > 0) { 170 | // Create a virtual file with markdown content 171 | ApplicationManager.getApplication().invokeLater { 172 | val fileName = "Git History - ${File(absoluteFilePath).name}.md" 173 | val virtualFile = LightVirtualFile( 174 | fileName, 175 | FileTypeManager.getInstance().getFileTypeByExtension("md"), 176 | markdownContent.toString() 177 | ) 178 | 179 | // Open the file in the editor 180 | FileEditorManager.getInstance(project).openFile(virtualFile, true) 181 | } 182 | } else { 183 | logger.info("No diffs found for any commits") 184 | Messages.showInfoMessage(project, "No diffs found for the selected code", "ContextPilot") 185 | } 186 | } catch (ex: Exception) { 187 | logger.error("Error processing diffs", ex) 188 | Messages.showErrorDialog( 189 | project, 190 | "Error processing diffs: ${ex.message}", 191 | "ContextPilot Error" 192 | ) 193 | } 194 | } else { 195 | logger.error("Command failed with exit code $exitCode: $output") 196 | Messages.showErrorDialog( 197 | project, 198 | "Failed to get commit information (exit code: $exitCode)\nOutput: $output", 199 | "ContextPilot Error" 200 | ) 201 | } 202 | } catch (ex: Exception) { 203 | logger.error("Error generating diffs", ex) 204 | Messages.showErrorDialog( 205 | project, 206 | "Error generating diffs: ${ex.message}", 207 | "ContextPilot Error" 208 | ) 209 | } 210 | } 211 | 212 | override fun update(e: AnActionEvent) { 213 | val project = e.project 214 | val editor = e.getData(CommonDataKeys.EDITOR) 215 | e.presentation.isEnabled = project != null && editor != null 216 | } 217 | } --------------------------------------------------------------------------------