diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80bb4b57..8634150e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,17 +75,12 @@ jobs: run: | PROPERTIES="$(./gradlew properties --console=plain -q)" VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" - CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" echo "version=$VERSION" >> $GITHUB_OUTPUT echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT - - echo "changelog<> $GITHUB_OUTPUT - echo "$CHANGELOG" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT ./gradlew listProductsReleases # prepare list of IDEs for Plugin Verifier - + # Build plugin - name: Build plugin run: ./gradlew buildPlugin @@ -179,14 +174,6 @@ jobs: distribution: zulu java-version: 17 - # Run Qodana inspections - - name: Qodana - Code Inspection - uses: JetBrains/qodana-action@v2024.1.5 - with: - cache-default-branch-only: true - env: - QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} - # Run plugin structure verification along with IntelliJ Plugin Verifier verify: name: Verify plugin diff --git a/.gitignore b/.gitignore index 734a3588..ee04a2aa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/test/**/build/ ### IntelliJ IDEA ### +.idea/ .idea/modules.xml .idea/jarRepositories.xml .idea/compiler.xml @@ -43,4 +44,6 @@ bin/ .DS_Store tmp/ -*.log \ No newline at end of file +*.log +src/main/resources/static/main.html +src/main/resources/static/main.js diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b81..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 535d5cb8..00000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -devchat \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 1bec35e5..00000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123c..00000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index ce358e4b..00000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 7d3b3e85..00000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index 6d0ee1c2..00000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 2304d430..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml deleted file mode 100644 index 2b63946d..00000000 --- a/.idea/uiDesigner.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 247ccb34..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/gui b/gui index 87fb8b5d..c0112245 160000 --- a/gui +++ b/gui @@ -1 +1 @@ -Subproject commit 87fb8b5d9d6cd937c449e00218b1777c38ee1ba2 +Subproject commit c011224587af65658d5637d5016f6b60024054a4 diff --git a/src/main/kotlin/ai/devchat/common/PathUtils.kt b/src/main/kotlin/ai/devchat/common/PathUtils.kt index bc638ac9..e1c8e503 100644 --- a/src/main/kotlin/ai/devchat/common/PathUtils.kt +++ b/src/main/kotlin/ai/devchat/common/PathUtils.kt @@ -1,14 +1,17 @@ package ai.devchat.common +import kotlinx.coroutines.* import java.io.File import java.io.IOException import java.nio.file.* import java.nio.file.attribute.BasicFileAttributes - +import java.security.MessageDigest +import java.util.concurrent.Executors object PathUtils { val workPath: String = Paths.get(System.getProperty("user.home"), ".chat").toString() val workflowPath: String = Paths.get(workPath, "scripts").toString() + val workflowMericoPath: String = Paths.get(workPath, "scripts", "merico").toString() val sitePackagePath: String = Paths.get(workPath, "site-packages").toString() val pythonPath: String = "$sitePackagePath:$workflowPath" val mambaWorkPath = Paths.get(workPath, "mamba").toString() @@ -25,10 +28,14 @@ object PathUtils { else -> throw RuntimeException("Unsupported OS: ${OSInfo.OS_NAME}") }}-code_editor" + if (OSInfo.isWindows) ".exe" else "" - fun copyResourceDirToPath(resourcePath: String, outputPath: String, overwrite: Boolean = false): String { + private val dispatcher = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher() + + suspend fun copyResourceDirToPath(resourcePath: String, outputPath: String, overwrite: Boolean = false): String = withContext(dispatcher) { + Log.info("copy files stage 1") val uri = javaClass.getResource(resourcePath)?.toURI() ?: throw IllegalArgumentException( "Resource not found: $resourcePath" ) + Log.info("copy files stage 2") val sourcePath = if (uri.scheme == "jar") { val fileSystem = try { FileSystems.newFileSystem(uri, emptyMap()) @@ -39,18 +46,27 @@ object PathUtils { } else { Paths.get(uri) } + Log.info("copy files stage 3") val targetPath = Paths.get(outputPath) if (!Files.exists(targetPath.parent)) Files.createDirectories(targetPath.parent) - if (overwrite && Files.exists(targetPath)) targetPath.toFile().deleteRecursively() + if (!overwrite && Files.exists(targetPath)) { + return@withContext targetPath.toString() + } +// if (overwrite && Files.exists(targetPath)) targetPath.toFile().deleteRecursively() + Log.info("copy files stage 4") // Handle single file copying if (Files.isRegularFile(sourcePath)) { Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING) targetPath.toFile().setExecutable(true) - return targetPath.toString() + return@withContext targetPath.toString() } + Log.info("copy files stage 5") + val jobs = mutableListOf>() + + Log.info("copy files stage 6") Files.walkFileTree(sourcePath, object : SimpleFileVisitor() { @Throws(IOException::class) override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { @@ -63,15 +79,27 @@ object PathUtils { @Throws(IOException::class) override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { val target = targetPath.resolve(sourcePath.relativize(file).toString()) - if (!Files.exists(target) || attrs.lastModifiedTime() > Files.getLastModifiedTime(target)) { - Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING) - Files.setLastModifiedTime(target, attrs.lastModifiedTime()) - } + jobs.add(async { + if (shouldCopyFile(file, target, attrs, overwrite)) { + Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING) + Files.setLastModifiedTime(target, attrs.lastModifiedTime()) + } + }) return FileVisitResult.CONTINUE } }) + Log.info("copy files stage 7") + + jobs.awaitAll() + Log.info("copy files stage 8") + targetPath.toString() + } - return targetPath.toString() + private fun shouldCopyFile(source: Path, target: Path, sourceAttrs: BasicFileAttributes, overwrite: Boolean): Boolean { + if (!Files.exists(target)) return true + if (!overwrite) return false + if (sourceAttrs.lastModifiedTime() != Files.getLastModifiedTime(target)) return true + return false } fun createTempFile(content: String, prefix: String = "devchat-tmp-", suffix: String = ""): String? { @@ -84,4 +112,4 @@ object PathUtils { return null } } -} +} \ No newline at end of file diff --git a/src/main/kotlin/ai/devchat/core/DevChatClient.kt b/src/main/kotlin/ai/devchat/core/DevChatClient.kt index 90321396..1b722c36 100644 --- a/src/main/kotlin/ai/devchat/core/DevChatClient.kt +++ b/src/main/kotlin/ai/devchat/core/DevChatClient.kt @@ -53,7 +53,7 @@ data class ChatResponse( var date: String? = null, var content: String? = "", @SerialName("finish_reason") var finishReason: String? = "", - @SerialName("is_error") var isError: Boolean = false, + var isError: Boolean = false, var extra: JsonElement? = null ) { fun reset() { @@ -327,6 +327,14 @@ class DevChatClient(val project: Project, private val localServicePort: Int) { onFinish(-1) cancelMessage() } + if (chunk.isError) { + val errorMessage = chunk.content?.takeIf { it.isNotBlank() } + ?: "Unknown error occurred" + onError(errorMessage) + Log.warn("Error on sending message: ${chunk.toString()}") + onFinish(1) + cancelMessage() + } onData(chunk) } onFinish(0) diff --git a/src/main/kotlin/ai/devchat/installer/DevChatSetupThread.kt b/src/main/kotlin/ai/devchat/installer/DevChatSetupThread.kt deleted file mode 100644 index 0a7aa1db..00000000 --- a/src/main/kotlin/ai/devchat/installer/DevChatSetupThread.kt +++ /dev/null @@ -1,127 +0,0 @@ -package ai.devchat.installer - -import ai.devchat.common.* -import ai.devchat.common.Constants.ASSISTANT_NAME_EN -import ai.devchat.core.DevChatClient -import ai.devchat.plugin.DevChatService -import ai.devchat.storage.CONFIG -import ai.devchat.storage.DevChatState -import com.intellij.ide.plugins.PluginManagerCore -import com.intellij.openapi.extensions.PluginId -import com.intellij.openapi.project.Project -import java.io.BufferedReader -import java.io.File -import java.nio.file.Paths -import kotlin.system.measureTimeMillis - -class DevChatSetupThread(val project: Project) : Thread() { - private val minimalPythonVersion: String = "3.8" - private val defaultPythonVersion: String = "3.11.4" - private val devChatService = project.getService(DevChatService::class.java) - private val devChatVersion = PluginManagerCore.getPlugin( - PluginId.getId(DevChatBundle.message("plugin.id")) - )?.version - - override fun run() { - Log.info("Work path is: ${PathUtils.workPath}") - Notifier.info("Starting $ASSISTANT_NAME_EN initialization...") - try { - Log.info("Start configuring the $ASSISTANT_NAME_EN CLI environment.") - val executionTime = measureTimeMillis { - setupPython(PythonEnvManager()) - } - Log.info("-----------> Time took to setup python: ${executionTime/1000} s") - installTools() - updateWorkflows() - devChatService.browser?.let { - Log.info("-----------> Executing JS callback onInitializationFinish") - it.executeJS("onInitializationFinish") - } - DevChatState.instance.lastVersion = devChatVersion - Notifier.info("$ASSISTANT_NAME_EN initialization has completed successfully.") - } catch (e: Exception) { - Log.error("Failed to install $ASSISTANT_NAME_EN CLI: $e\n" + e.stackTrace.joinToString("\n")) - Notifier.error("$ASSISTANT_NAME_EN initialization has failed. Please check the logs for more details.") - } - } - - private fun setupPython(envManager: PythonEnvManager) { - val overwrite = devChatVersion != DevChatState.instance.lastVersion - PathUtils.copyResourceDirToPath("/tools/site-packages", PathUtils.sitePackagePath, overwrite) - "python_for_chat".let { k -> - if (OSInfo.isWindows) { - val installDir = Paths.get(PathUtils.workPath, "python-win").toString() - PathUtils.copyResourceDirToPath("/tools/python-3.11.6-embed-amd64", installDir, overwrite) - val pthFile = File(Paths.get(installDir, "python311._pth").toString()) - val pthContent = pthFile.readText().replace( - "%PYTHONPATH%", - "${PathUtils.sitePackagePath}${System.lineSeparator()}${PathUtils.workflowPath}" - ) - pthFile.writeText(pthContent) - CONFIG[k] = Paths.get(installDir, "python.exe").toString() - } else if ((CONFIG[k] as? String).isNullOrEmpty()) { - CONFIG[k] = getSystemPython(minimalPythonVersion) ?: envManager.createEnv( - "devchat", defaultPythonVersion - ).pythonCommand - } - } - devChatService.pythonReady = true - } - - private fun installTools() { - val overwrite = devChatVersion != DevChatState.instance.lastVersion - PathUtils.copyResourceDirToPath( - "/tools/code-editor/${PathUtils.codeEditorBinary}", - Paths.get(PathUtils.toolsPath, PathUtils.codeEditorBinary).toString(), - overwrite - ) - PathUtils.copyResourceDirToPath( - "/tools/sonar-rspec", - Paths.get(PathUtils.toolsPath, "sonar-rspec").toString(), - overwrite - ) - PathUtils.copyResourceDirToPath("/workflows", PathUtils.workflowPath) - } - - private fun updateWorkflows() { - try { - val dcClient: DevChatClient = devChatService.client!! - dcClient.updateWorkflows() - dcClient.updateCustomWorkflows() - } catch (e: Exception) { - Log.warn("Failed to update workflows: $e") - } - } - - private fun getSystemPython(minimalVersion: String): String? { - val (minMajor, minMinor) = minimalVersion.split(".").take(2).map(String::toInt) - val process = ProcessBuilder( - if (OSInfo.isWindows) listOf("cmd","/c","python --version") - else listOf("/bin/bash","-c", "python3 --version") - ).start() - val output = process.inputStream.bufferedReader().use(BufferedReader::readLine) - process.waitFor() - - return output?.let { - val (major, minor) = it.split(" ")[1].split(".").take(2).map(String::toInt) - val cmd = "import sys; print(sys.executable)" - val pb = ProcessBuilder( - if (OSInfo.isWindows) listOf("cmd","/c","python -c \"$cmd\"") - else listOf("/bin/bash","-c", "python3 -c \"$cmd\"") - ) - pb.environment()["PYTHONUTF8"] = "1" - val proc = pb.start() - val python = proc.inputStream.bufferedReader().use(BufferedReader::readText).trim() - val errs = proc.errorStream.bufferedReader().use(BufferedReader::readText) - val exitCode = proc.waitFor() - if (exitCode != 0) { - Log.warn("Failed to get system: $errs") - } - when { - major > minMajor -> python - major == minMajor && minor >= minMinor -> python - else -> null - } - } - } -} diff --git a/src/main/kotlin/ai/devchat/installer/PythonEnvManager.kt b/src/main/kotlin/ai/devchat/installer/PythonEnvManager.kt index 3cf4f453..c1b6b5ff 100644 --- a/src/main/kotlin/ai/devchat/installer/PythonEnvManager.kt +++ b/src/main/kotlin/ai/devchat/installer/PythonEnvManager.kt @@ -28,6 +28,7 @@ class PythonEnvManager { val dstFile = File(PathUtils.mambaBinPath) if (!dstFile.exists()) { Log.info("Installing Mamba to: " + dstFile.path) + Log.info("Current os platform is ${OSInfo.platform}") val dstDir = dstFile.parentFile dstDir.exists() || dstDir.mkdirs() || throw RuntimeException("Unable to create directory: $dstDir") javaClass.getResource( diff --git a/src/main/kotlin/ai/devchat/plugin/DevChatToolWindowFactory.kt b/src/main/kotlin/ai/devchat/plugin/DevChatToolWindowFactory.kt index 3011e9f7..3b0b8043 100644 --- a/src/main/kotlin/ai/devchat/plugin/DevChatToolWindowFactory.kt +++ b/src/main/kotlin/ai/devchat/plugin/DevChatToolWindowFactory.kt @@ -3,7 +3,6 @@ package ai.devchat.plugin import ai.devchat.common.Log import ai.devchat.core.DevChatClient import ai.devchat.core.DevChatWrapper -import ai.devchat.installer.DevChatSetupThread import ai.devchat.storage.ActiveConversation import ai.devchat.storage.RecentFilesTracker import com.intellij.openapi.Disposable @@ -12,6 +11,7 @@ import com.intellij.openapi.components.service import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer +import com.intellij.ui.content.Content import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory import com.intellij.ui.JBColor @@ -22,6 +22,19 @@ import javax.swing.BorderFactory import javax.swing.JLabel import javax.swing.JPanel import javax.swing.SwingConstants +import ai.devchat.common.* +import ai.devchat.storage.CONFIG +import ai.devchat.storage.DevChatState +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.extensions.PluginId +import java.io.BufferedReader +import java.io.File +import java.nio.file.Paths +import ai.devchat.common.Constants.ASSISTANT_NAME_EN +import kotlin.system.measureTimeMillis +import ai.devchat.installer.PythonEnvManager +import com.intellij.openapi.application.ApplicationManager + @Service(Service.Level.PROJECT) class DevChatService(project: Project) { @@ -36,77 +49,270 @@ class DevChatService(project: Project) { } class DevChatToolWindowFactory : ToolWindowFactory, DumbAware, Disposable { + private val minimalPythonVersion: String = "3.8" + private val defaultPythonVersion: String = "3.11.4" + private val devChatVersion = PluginManagerCore.getPlugin( + PluginId.getId(DevChatBundle.message("plugin.id")) + )?.version + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { - project.service() - val devChatService = project.getService(DevChatService::class.java) + Log.info("-----------> DevChatToolWindowFactory.createToolWindowContent started") + + try { + project.service() + Log.info("-----------> RecentFilesTracker service initialized") + + val devChatService = project.getService(DevChatService::class.java) + Log.info("-----------> DevChatService obtained") + + val panel = JPanel(BorderLayout()) + val content = toolWindow.contentManager.factory.createContent(panel, "", false) + Disposer.register(content, this) + Log.info("-----------> Content created and disposers registered") + + val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + Log.error("-----------> Initialization failed: ${throwable.message}") + Notifier.error("$ASSISTANT_NAME_EN initialization has failed. Please check the logs for more details.") + } + + CoroutineScope(Dispatchers.Default + exceptionHandler).launch { + try { + ApplicationManager.getApplication().invokeLater { + Notifier.info("Starting $ASSISTANT_NAME_EN initialization...") + } + + // Step 1: Setup Python and install tools + ApplicationManager.getApplication().invokeLater { + Notifier.info("Checking and installing Python environment. This may take a while...") + } + setupPythonAndTools(project, devChatService) + Log.info("-----------> setup python and install tools") + + // Step 2: Start local service + val localService = startLocalService(project, content, devChatService) + if (localService == null) { + Log.error("-----------> Failed to start local service") + ApplicationManager.getApplication().invokeLater { + Notifier.error("$ASSISTANT_NAME_EN initialization has failed. Please check the logs for more details.") + } + return@launch + } + Log.info("-----------> start local service") + + // Step 3: Start IDE server + val ideServer = IDEServer(project).start() + Disposer.register(content, ideServer) + devChatService.ideServicePort = ideServer.port + Log.info("-----------> IDE server started on port ${ideServer.port}") + + // Step 4: Create and setup DevChatWrapper + val wrapper = DevChatWrapper(project) + Disposer.register(content, wrapper) + devChatService.wrapper = wrapper + Log.info("-----------> DevChatWrapper created and registered") + + // Step 5: Create ActiveConversation + devChatService.activeConversation = ActiveConversation() + Log.info("-----------> ActiveConversation created") + + // Step 6: Update workflows + updateWorkflows(devChatService.client!!) + Log.info("-----------> update workflows") + + // Step 7: Initialize and add browser component + ApplicationManager.getApplication().invokeAndWait { + try { + initializeBrowser(project, devChatService, panel, content) + toolWindow.contentManager.addContent(content) + Log.info("-----------> Content added to tool window") + } catch (e: Exception) { + Log.error("Error initializing browser: ${e.message}") + Notifier.error("Failed to initialize browser. Please check the logs for more details.") + } + } + Log.info("-----------> initializeBrowser") + + // Step 8: Update DevChatState + DevChatState.instance.lastVersion = devChatVersion + Log.info("-----------> DevChatState updated with new version") + + // Step 9: Execute JS callback + devChatService.browser?.let { + Log.info("-----------> Executing JS callback onInitializationFinish") + it.executeJS("onInitializationFinish") + } + + ApplicationManager.getApplication().invokeLater { + Notifier.info("$ASSISTANT_NAME_EN initialization has completed successfully.") + } + } catch (e: Exception) { + Log.error("Failed to initialize $ASSISTANT_NAME_EN: $e\n" + e.stackTrace.joinToString("\n")) + ApplicationManager.getApplication().invokeLater { + Notifier.error("$ASSISTANT_NAME_EN initialization has failed. Please check the logs for more details.") + } + } + } + } catch (e: Exception) { + Log.error("-----------> Exception during DevChatToolWindowFactory initialization: ${e.message}") + Notifier.error("Failed to initialize $ASSISTANT_NAME_EN. Please check the logs for more details.") + } + } + + private suspend fun setupPythonAndTools(project: Project, devChatService: DevChatService) { + Log.info("Start configuring the $ASSISTANT_NAME_EN CLI environment.") + val executionTime = measureTimeMillis { + try { + Log.info("Creating PythonEnvManager") + val pythonEnvManager = PythonEnvManager() + Log.info("Setting up Python") + setupPython(pythonEnvManager, devChatService) + Log.info("Installing workflows") + installWorkflows() + Log.info("Installing tools") + CoroutineScope(Dispatchers.IO).launch { + installTools() + } + } catch (e: Exception) { + Log.error("Error in setupPythonAndTools: ${e.message}") + } + } + Log.info("-----------> Time took to setup python and workflows: ${executionTime/1000} s") +} + +private suspend fun setupPython(envManager: PythonEnvManager, devChatService: DevChatService) { + val overwrite = devChatVersion != DevChatState.instance.lastVersion + Log.info("start to copy site-packages files") + PathUtils.copyResourceDirToPath("/tools/site-packages", PathUtils.sitePackagePath, overwrite) + Log.info("copy site-packages files finished") + var t1 = CONFIG["python_for_chat"] + Log.info("Load config file finished") + "python_for_chat".let { k -> + if (OSInfo.isWindows) { + val installDir = Paths.get(PathUtils.workPath, "python-win").toString() + Log.info("start to copy python-win files") + PathUtils.copyResourceDirToPath("/tools/python-3.11.6-embed-amd64", installDir, overwrite) + Log.info("copy python-win files finished") + val pthFile = File(Paths.get(installDir, "python311._pth").toString()) + val pthContent = pthFile.readText().replace( + "%PYTHONPATH%", + "${PathUtils.sitePackagePath}${System.lineSeparator()}${PathUtils.workflowPath}" + ) + pthFile.writeText(pthContent) + CONFIG[k] = Paths.get(installDir, "python.exe").toString() + } else if ((CONFIG[k] as? String).isNullOrEmpty()) { + CONFIG[k] = getSystemPython(minimalPythonVersion) ?: envManager.createEnv( + "devchat", defaultPythonVersion + ).pythonCommand + } + } + devChatService.pythonReady = true +} + +private suspend fun installWorkflows() { + Log.info("Start checking and copying workflows files") + val workflowMericoDir = File(PathUtils.workflowMericoPath) + + if (!workflowMericoDir.exists() || !workflowMericoDir.isDirectory || workflowMericoDir.listFiles()?.isEmpty() == true) { + Log.info("Workflow Merico directory is missing or empty. Creating and populating it.") + PathUtils.copyResourceDirToPath("/workflows", PathUtils.workflowPath) + } else { + Log.info("Workflow Merico directory exists and is not empty. Skipping copy.") + } + + Log.info("Finished checking and copying workflows files") +} + +private suspend fun installTools() { + val overwrite = devChatVersion != DevChatState.instance.lastVersion + Log.info("start to copy tools files") + PathUtils.copyResourceDirToPath( + "/tools/code-editor/${PathUtils.codeEditorBinary}", + Paths.get(PathUtils.toolsPath, PathUtils.codeEditorBinary).toString(), + overwrite + ) + PathUtils.copyResourceDirToPath( + "/tools/sonar-rspec", + Paths.get(PathUtils.toolsPath, "sonar-rspec").toString(), + overwrite + ) + Log.info("copy tools files finished") +} + + private suspend fun startLocalService(project: Project, content: Content, devChatService: DevChatService): LocalService? { + return withTimeoutOrNull(60000) { + val localService = LocalService(project).start() + localService?.let { + Disposer.register(content, it) + devChatService.localServicePort = it.port!! + devChatService.client = DevChatClient(project, it.port!!) + Log.info("-----------> DevChat client ready on port ${it.port}") + it + } + } + } + + private fun initializeBrowser(project: Project, devChatService: DevChatService, panel: JPanel, content: Content) { val browser = Browser(project) devChatService.browser = browser + Log.info("-----------> Browser created and set in DevChatService") + + // Register the browser to be disposed with the content + Disposer.register(content, browser) + Log.info("-----------> Browser registered for disposal") - val panel = JPanel(BorderLayout()) if (!JBCefApp.isSupported()) { Log.error("JCEF is not supported.") panel.add(JLabel("JCEF is not supported", SwingConstants.CENTER)) } else { panel.add(browser.jbCefBrowser.component, BorderLayout.CENTER) + Log.info("-----------> JCEF browser component added to panel") } panel.border = BorderFactory.createMatteBorder(0, 1, 0, 1, JBColor.LIGHT_GRAY) + } - val content = toolWindow.contentManager.factory.createContent(panel, "", false) - Disposer.register(content, this) - Disposer.register(content, browser) + private fun updateWorkflows(client: DevChatClient) { + try { + client.updateWorkflows() + client.updateCustomWorkflows() + Log.info("-----------> Workflows updated successfully") + } catch (e: Exception) { + Log.warn("-----------> Failed to update workflows: ${e.message}") + } + } - DevChatSetupThread(project).start() + private fun getSystemPython(minimalVersion: String): String? { + val (minMajor, minMinor) = minimalVersion.split(".").take(2).map(String::toInt) + val process = ProcessBuilder( + if (OSInfo.isWindows) listOf("cmd","/c","python --version") + else listOf("/bin/bash","-c", "python3 --version") + ).start() + val output = process.inputStream.bufferedReader().use(BufferedReader::readLine) + process.waitFor() - val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - Log.error("-----------> Failed to start local service") - throwable.printStackTrace() - } - CoroutineScope(Dispatchers.Default + exceptionHandler).launch { - withTimeoutOrNull(60000) { - while (!devChatService.pythonReady) { - Log.info("-----------> Waiting python ready...") - delay(100) - } - LocalService(project).start() - }?.let { - Disposer.register(content, it) - devChatService.localServicePort = it.port!! - devChatService.client = DevChatClient(project, it.port!!) - Log.info("-----------> DevChat client ready") + return output?.let { + val (major, minor) = it.split(" ")[1].split(".").take(2).map(String::toInt) + val cmd = "import sys; print(sys.executable)" + val pb = ProcessBuilder( + if (OSInfo.isWindows) listOf("cmd","/c","python -c \"$cmd\"") + else listOf("/bin/bash","-c", "python3 -c \"$cmd\"") + ) + pb.environment()["PYTHONUTF8"] = "1" + val proc = pb.start() + val python = proc.inputStream.bufferedReader().use(BufferedReader::readText).trim() + val errs = proc.errorStream.bufferedReader().use(BufferedReader::readText) + val exitCode = proc.waitFor() + if (exitCode != 0) { + Log.warn("Failed to get system: $errs") + } + when { + major > minMajor -> python + major == minMajor && minor >= minMinor -> python + else -> null } } - IDEServer(project).start().let { - Disposer.register(content, it) - devChatService.ideServicePort = it.port - } - DevChatWrapper(project).let { - Disposer.register(content, it) - devChatService.wrapper = it - } - devChatService.activeConversation = ActiveConversation() - - toolWindow.contentManager.addContent(content) } -// private fun startLocalService() { -// val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception -> -// Log.error("Failed to start local service: ${exception.message}") -// } -// coroutineScope = CoroutineScope(Dispatchers.Default) -// coroutineScope!!.launch(coroutineExceptionHandler) { -// try { -// while (!pythonReady) { -// delay(100) -// ensureActive() -// } -// localService = LocalService().start() -// awaitCancellation() -// } finally { -// localService?.stop() -// } -// } -// } - - - override fun dispose() {} -} + override fun dispose() { + Log.info("-----------> DevChatToolWindowFactory disposed") + } +} \ No newline at end of file diff --git a/src/main/kotlin/ai/devchat/plugin/LocalService.kt b/src/main/kotlin/ai/devchat/plugin/LocalService.kt index b01a5cc0..88e99df6 100644 --- a/src/main/kotlin/ai/devchat/plugin/LocalService.kt +++ b/src/main/kotlin/ai/devchat/plugin/LocalService.kt @@ -1,5 +1,7 @@ package ai.devchat.plugin +import java.net.Socket +import java.io.IOException import ai.devchat.common.Log import ai.devchat.common.Notifier import ai.devchat.common.PathUtils @@ -58,8 +60,23 @@ class LocalService(project: Project): Disposable { } processHandler?.startNotify() - Log.info("Local service started on port: $port") - Notifier.info("Local service started at $port.") + + var attempts = 0 + val maxAttempts = 10 + while (attempts < maxAttempts) { + try { + val socket = Socket("localhost", port!!) + socket.close() + Log.info("Local service started on port: $port") + Notifier.info("Local service started at $port.") + return this + } catch (e: IOException) { + attempts++ + Thread.sleep(500) // 等待500毫秒后重试 + } + } + + Log.warn("Local service may not have started properly") return this } diff --git a/src/main/kotlin/ai/devchat/plugin/completion/actions/TriggerCompletion.kt b/src/main/kotlin/ai/devchat/plugin/completion/actions/TriggerCompletion.kt index 0d455f52..459889b8 100644 --- a/src/main/kotlin/ai/devchat/plugin/completion/actions/TriggerCompletion.kt +++ b/src/main/kotlin/ai/devchat/plugin/completion/actions/TriggerCompletion.kt @@ -1,6 +1,8 @@ package ai.devchat.plugin.completion.actions +import ai.devchat.common.DevChatBundle import ai.devchat.plugin.completion.editor.CompletionProvider +import ai.devchat.storage.CONFIG import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent @@ -19,6 +21,11 @@ class TriggerCompletion : AnAction() { override fun update(e: AnActionEvent) { e.presentation.isEnabled = e.project != null && e.getData(CommonDataKeys.EDITOR) != null + if ((CONFIG["language"] as? String) == "zh") { + e.presentation.text = DevChatBundle.message("action.triggerCompletion.text.zh") + } else { + e.presentation.text = DevChatBundle.message("action.triggerCompletion.text") + } } override fun getActionUpdateThread(): ActionUpdateThread { diff --git a/src/main/kotlin/ai/devchat/plugin/completion/agent/Agent.kt b/src/main/kotlin/ai/devchat/plugin/completion/agent/Agent.kt index 2934dc99..14827292 100644 --- a/src/main/kotlin/ai/devchat/plugin/completion/agent/Agent.kt +++ b/src/main/kotlin/ai/devchat/plugin/completion/agent/Agent.kt @@ -4,6 +4,7 @@ import ai.devchat.storage.CONFIG import com.google.gson.Gson import com.google.gson.annotations.SerializedName import com.intellij.openapi.diagnostic.Logger +import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* @@ -271,41 +272,50 @@ class Agent(val scope: CoroutineScope) { return completion } - suspend fun provideCompletions( - completionRequest: CompletionRequest - ): CompletionResponse? = suspendCancellableCoroutine { continuation -> - currentRequest = RequestInfo.fromCompletionRequest(completionRequest) - val model = CONFIG["complete_model"] as? String - var startTime = System.currentTimeMillis() - val prompt = ContextBuilder( - completionRequest.file, - completionRequest.position - ).createPrompt(model) - val promptBuildingElapse = System.currentTimeMillis() - startTime +suspend fun provideCompletions( + completionRequest: CompletionRequest +): CompletionResponse? = suspendCancellableCoroutine { continuation -> + currentRequest = RequestInfo.fromCompletionRequest(completionRequest) + val model = CONFIG["complete_model"] as? String + var startTime = System.currentTimeMillis() + logger.info("offset: ${completionRequest.position}") + val prompt = ContextBuilder( + completionRequest.file, + completionRequest.position + ).createPrompt(model) + logger.info("Prompt: $prompt") + // output prompt length + logger.info("Prompt length: ${prompt.length}") + val promptBuildingElapse = System.currentTimeMillis() - startTime - scope.launch { - startTime = System.currentTimeMillis() - val chunks = request(prompt) - .let(::toLines) - .let(::stopAtFirstBrace) - .let(::stopAtDuplicateLine) - .let(::stopAtBlockEnds) - val completion = aggregate(chunks) - val llmRequestElapse = System.currentTimeMillis() - startTime - val offset = completionRequest.position - val replaceRange = CompletionResponse.Choice.Range(start = offset, end = offset) - val text = if (completion.text != prevCompletion) completion.text else "" - val choice = CompletionResponse.Choice(index = 0, text = text, replaceRange = replaceRange) - val response = CompletionResponse(completion.id, model, listOf(choice), promptBuildingElapse, llmRequestElapse) - continuation.resumeWith(Result.success(response)) - prevCompletion = completion.text - } + scope.launch { + startTime = System.currentTimeMillis() + val chunks = request(prompt) + .let(::toLines) + .let(::stopAtFirstBrace) + .let(::stopAtDuplicateLine) + .let(::stopAtBlockEnds) + val completion = aggregate(chunks) + val llmRequestElapse = System.currentTimeMillis() - startTime + val offset = completionRequest.position + val replaceRange = CompletionResponse.Choice.Range(start = offset, end = offset) + val text = if (completion.text != prevCompletion) completion.text else "" + val choice = CompletionResponse.Choice(index = 0, text = text, replaceRange = replaceRange) + val response = CompletionResponse(completion.id, model, listOf(choice), promptBuildingElapse, llmRequestElapse) - continuation.invokeOnCancellation { - logger.warn("Agent request cancelled") - } + // 添加日志输出 + logger.info("Code completion response: $response") + logger.info("Final completion text: ${completion.text}") + + continuation.resumeWith(Result.success(response)) + prevCompletion = completion.text } + continuation.invokeOnCancellation { + logger.warn("Agent request cancelled") + } +} + suspend fun postEvent(logEventRequest: LogEventRequest): Unit = suspendCancellableCoroutine { val devChatEndpoint = CONFIG["providers.devchat.api_base"] as? String val devChatAPIKey = CONFIG["providers.devchat.api_key"] as? String diff --git a/src/main/kotlin/ai/devchat/plugin/completion/agent/ContextBuilder.kt b/src/main/kotlin/ai/devchat/plugin/completion/agent/ContextBuilder.kt index e80c4672..fde5b46b 100644 --- a/src/main/kotlin/ai/devchat/plugin/completion/agent/ContextBuilder.kt +++ b/src/main/kotlin/ai/devchat/plugin/completion/agent/ContextBuilder.kt @@ -1,5 +1,7 @@ package ai.devchat.plugin.completion.agent +import com.intellij.openapi.application.ReadAction +import com.intellij.psi.PsiDocumentManager import ai.devchat.common.Constants.LANGUAGE_COMMENT_PREFIX import ai.devchat.common.IDEUtils.findAccessibleVariables import ai.devchat.common.IDEUtils.findCalleeInParent @@ -9,23 +11,27 @@ import ai.devchat.common.Log import ai.devchat.storage.RecentFilesTracker import com.intellij.psi.PsiFile import com.intellij.psi.util.PsiUtilCore.getPsiFile +import ai.devchat.storage.CONFIG -const val MAX_CONTEXT_TOKENS = 6000 +val MAX_CONTEXT_TOKENS: Int + get() = (CONFIG["complete_context_limit"] as? Int) ?: 6000 const val LINE_SEPARATOR = '\n' fun String.tokenCount(): Int { - var count = 0 - var isPrevWhiteSpace = true - for (char in this) { - if (char.isWhitespace()) { - isPrevWhiteSpace = true - } else { - if (isPrevWhiteSpace) count++ - isPrevWhiteSpace = false - } - } - return count +// var count = 0 +// var isPrevWhiteSpace = true +// for (char in this) { +// if (char.isWhitespace()) { +// isPrevWhiteSpace = true +// } else { +// if (isPrevWhiteSpace) count++ +// isPrevWhiteSpace = false +// } +// } +// return count + // use length as token count + return this.length } @@ -69,7 +75,21 @@ data class CodeSnippet ( class ContextBuilder(val file: PsiFile, val offset: Int) { val filepath: String = file.virtualFile.path - val content: String = file.text + val content: String by lazy { + ReadAction.compute { + val psiDocumentManager = PsiDocumentManager.getInstance(file.project) + val document = psiDocumentManager.getDocument(file) + if (document != null) { + psiDocumentManager.doPostponedOperationsAndUnblockDocument(document) + document.text + } else { + file.text + } + } + } +// val content: String by lazy { +// ReadAction.compute { file.text } +// } private val commentPrefix: String = LANGUAGE_COMMENT_PREFIX[file.language.id.lowercase()] ?: "//" private var tokenCount: Int = 0 @@ -96,6 +116,16 @@ class ContextBuilder(val file: PsiFile, val offset: Int) { }.lastOrNull()?.first?.last ?: content.length tokenCount += suffixTokens + val debugPrefixStart = maxOf(0, offset - 100) + val debugSuffixEnd = minOf(content.length, offset + 100) + Log.info("Debug: Offset 前 100 个字节文本:") + Log.info(content.substring(debugPrefixStart, offset)) + Log.info("\n--- Offset 位置 ---\n") + Log.info("Debug: Offset 后 100 个字节文本:") + Log.info(content.substring(offset, debugSuffixEnd)) + + + return Pair( content.substring(prefixStart, offset), content.substring(offset, suffixEnd) @@ -181,7 +211,7 @@ class ContextBuilder(val file: PsiFile, val offset: Int) { // similarBlockContext, // gitDiffContext, ).joinToString("") - Log.info("Extras completion context:\n$extras") +// Log.info("Extras completion context:\n$extras") return if (!model.isNullOrEmpty() && model.contains("deepseek")) "<|fim▁begin|>$extras$commentPrefix$filepath\n\n$prefix<|fim▁hole|>$suffix<|fim▁end|>" else diff --git a/src/main/kotlin/ai/devchat/plugin/completion/editor/EditorListener.kt b/src/main/kotlin/ai/devchat/plugin/completion/editor/EditorListener.kt index 7ff25a5e..0f50e131 100644 --- a/src/main/kotlin/ai/devchat/plugin/completion/editor/EditorListener.kt +++ b/src/main/kotlin/ai/devchat/plugin/completion/editor/EditorListener.kt @@ -49,19 +49,30 @@ class EditorListener : EditorFactoryListener { val enabled = CONFIG["complete_enable"] as? Boolean ?: false if (enabled) { inlineCompletionService.shownInlineCompletion?.let { - if (it.ongoing) return + if (it.ongoing) { + logger.debug("DocumentListener: documentChanged $event, but ongoing inline completion.") + return + } } + + + completionProvider.ongoingCompletion.value.let { if (it != null && it.editor == editor && it.offset == editor.caretModel.primaryCaret.offset) { // keep ongoing completion logger.debug("Keep ongoing completion.") } else { + logger.debug("DocumentListener: documentChanged $event, need to completion.") invokeLater { completionProvider.provideCompletion(editor, editor.caretModel.primaryCaret.offset) } } } + } else { + logger.debug("DocumentListener: documentChanged $event, but completion is disabled.") } + } else { + logger.debug("DocumentListener: documentChanged $event, but not selected editor.") } } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 7bcf0668..33225b90 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -101,8 +101,10 @@ + diff --git a/src/main/resources/messages/DevChatBundle.properties b/src/main/resources/messages/DevChatBundle.properties index e4681bcb..7bdaf066 100644 --- a/src/main/resources/messages/DevChatBundle.properties +++ b/src/main/resources/messages/DevChatBundle.properties @@ -14,6 +14,8 @@ action.addToDevChat.text.zh=\u6DFB\u52A0\u5230 ${ASSISTANT_NAME_ZH} action.docComment.text.zh=\u884C\u95F4\u6CE8\u91CA action.explainCode.text.zh=\u4EE3\u7801\u89E3\u91CA action.fix.text.zh=\u4EE3\u7801\u7EA0\u9519 +action.triggerCompletion.text=Inline Completion +action.triggerCompletion.text.zh=\u4EE3\u7801\u8865\u5168 assistant.name.zh=${ASSISTANT_NAME_ZH} assistant.name.en=${ASSISTANT_NAME_EN} plugin.id=${PLUGIN_ID} diff --git a/src/main/resources/static/main.html b/src/main/resources/static/main.html deleted file mode 100644 index 0c7a4429..00000000 --- a/src/main/resources/static/main.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - Intellij Plugin Demo - - - - - - - -

-

-
-    
-
-
-
-
diff --git a/src/main/resources/static/main.js b/src/main/resources/static/main.js
deleted file mode 100644
index 916f66f7..00000000
--- a/src/main/resources/static/main.js
+++ /dev/null
@@ -1,13 +0,0 @@
-setTimeout(function () {
-    console.log("wait...")
-}, 2000);
-
-function displayResponseFromJava(message) {
-    document.getElementById('response').innerHTML = JSON.stringify(message, null, 2);
-}
-
-document.getElementById('sendButton').addEventListener('click', function () {
-    var el = document.getElementById('textInput');
-    window.JSJavaBridge.callJava(el.value);
-    el.value = JSON.stringify(JSON.parse(el.value), null, 2);
-});
diff --git a/src/main/resources/static/readme.md b/src/main/resources/static/readme.md
new file mode 100644
index 00000000..b2d354b0
--- /dev/null
+++ b/src/main/resources/static/readme.md
@@ -0,0 +1 @@
+Files in this director are copied from gui.
\ No newline at end of file
diff --git a/tools b/tools
index 434e6a93..b6bccf6a 160000
--- a/tools
+++ b/tools
@@ -1 +1 @@
-Subproject commit 434e6a93916b703f9d64c1ce3146b16fe4f5a117
+Subproject commit b6bccf6a463c4437398499aa7ff17552e49ecf56