fix: handle multi-root workspaces in codebase indexing (#4397)

This commit is contained in:
hannesrudolph 2025-07-02 14:42:03 -06:00
parent 1be6fce1a6
commit f5e6de3d22
45 changed files with 813 additions and 91 deletions

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "Error desconegut en processar el fitxer {{filePath}}",
"unknownErrorDeletingPoints": "Error desconegut en eliminar els punts per a {{filePath}}",
"failedToProcessBatchWithError": "No s'ha pogut processar el lot després de {{maxRetries}} intents: {{errorMessage}}"
"failedToProcessBatchWithError": "No s'ha pogut processar el lot després de {{maxRetries}} intents: {{errorMessage}}",
"multiRootPathError": "No es pot processar el fitxer '{{filePath}}' ja que està fora de tots els arrels de l'espai de treball",
"workspaceRootDetectionError": "No s'ha pogut determinar l'arrel de l'espai de treball per al fitxer '{{filePath}}'"
},
"vectorStore": {
"qdrantConnectionFailed": "No s'ha pogut connectar a la base de dades vectorial Qdrant. Assegura't que Qdrant estigui funcionant i sigui accessible a {{qdrantUrl}}. Error: {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "Unbekannter Fehler beim Verarbeiten der Datei {{filePath}}",
"unknownErrorDeletingPoints": "Unbekannter Fehler beim Löschen der Punkte für {{filePath}}",
"failedToProcessBatchWithError": "Verarbeitung des Batches nach {{maxRetries}} Versuchen fehlgeschlagen: {{errorMessage}}"
"failedToProcessBatchWithError": "Verarbeitung des Batches nach {{maxRetries}} Versuchen fehlgeschlagen: {{errorMessage}}",
"multiRootPathError": "Datei '{{filePath}}' kann nicht verarbeitet werden, da sie außerhalb aller Arbeitsbereichs-Roots liegt",
"workspaceRootDetectionError": "Arbeitsbereichs-Root für Datei '{{filePath}}' konnte nicht bestimmt werden"
},
"vectorStore": {
"qdrantConnectionFailed": "Verbindung zur Qdrant-Vektordatenbank fehlgeschlagen. Stelle sicher, dass Qdrant läuft und unter {{qdrantUrl}} erreichbar ist. Fehler: {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "Unknown error processing file {{filePath}}",
"unknownErrorDeletingPoints": "Unknown error deleting points for {{filePath}}",
"failedToProcessBatchWithError": "Failed to process batch after {{maxRetries}} attempts: {{errorMessage}}"
"failedToProcessBatchWithError": "Failed to process batch after {{maxRetries}} attempts: {{errorMessage}}",
"multiRootPathError": "Cannot process file '{{filePath}}' as it's outside all workspace roots",
"workspaceRootDetectionError": "Failed to determine workspace root for file '{{filePath}}'"
},
"vectorStore": {
"qdrantConnectionFailed": "Failed to connect to Qdrant vector database. Please ensure Qdrant is running and accessible at {{qdrantUrl}}. Error: {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "Error desconocido procesando archivo {{filePath}}",
"unknownErrorDeletingPoints": "Error desconocido eliminando puntos para {{filePath}}",
"failedToProcessBatchWithError": "Error al procesar lote después de {{maxRetries}} intentos: {{errorMessage}}"
"failedToProcessBatchWithError": "Error al procesar lote después de {{maxRetries}} intentos: {{errorMessage}}",
"multiRootPathError": "No se puede procesar el archivo '{{filePath}}' ya que está fuera de todas las raíces del espacio de trabajo",
"workspaceRootDetectionError": "No se pudo determinar la raíz del espacio de trabajo para el archivo '{{filePath}}'"
},
"vectorStore": {
"qdrantConnectionFailed": "Error al conectar con la base de datos vectorial Qdrant. Asegúrate de que Qdrant esté funcionando y sea accesible en {{qdrantUrl}}. Error: {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "Erreur inconnue lors du traitement du fichier {{filePath}}",
"unknownErrorDeletingPoints": "Erreur inconnue lors de la suppression des points pour {{filePath}}",
"failedToProcessBatchWithError": "Échec du traitement du lot après {{maxRetries}} tentatives : {{errorMessage}}"
"failedToProcessBatchWithError": "Échec du traitement du lot après {{maxRetries}} tentatives : {{errorMessage}}",
"multiRootPathError": "Impossible de traiter le fichier '{{filePath}}' car il se trouve en dehors de toutes les racines de l'espace de travail",
"workspaceRootDetectionError": "Échec de la détermination de la racine de l'espace de travail pour le fichier '{{filePath}}'"
},
"vectorStore": {
"qdrantConnectionFailed": "Échec de la connexion à la base de données vectorielle Qdrant. Veuillez vous assurer que Qdrant fonctionne et est accessible à {{qdrantUrl}}. Erreur : {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "फ़ाइल {{filePath}} प्रसंस्करण में अज्ञात त्रुटि",
"unknownErrorDeletingPoints": "{{filePath}} के लिए बिंदु हटाने में अज्ञात त्रुटि",
"failedToProcessBatchWithError": "{{maxRetries}} प्रयासों के बाद बैच प्रसंस्करण विफल: {{errorMessage}}"
"failedToProcessBatchWithError": "{{maxRetries}} प्रयासों के बाद बैच प्रसंस्करण विफल: {{errorMessage}}",
"multiRootPathError": "फ़ाइल '{{filePath}}' को संसाधित नहीं किया जा सकता क्योंकि यह सभी कार्यक्षेत्र जड़ों के बाहर है",
"workspaceRootDetectionError": "फ़ाइल '{{filePath}}' के लिए कार्यक्षेत्र रूट निर्धारित करने में विफल"
},
"vectorStore": {
"qdrantConnectionFailed": "Qdrant वेक्टर डेटाबेस से कनेक्ट करने में विफल। कृपया सुनिश्चित करें कि Qdrant चल रहा है और {{qdrantUrl}} पर पहुंच योग्य है। त्रुटि: {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "Error tidak dikenal saat memproses file {{filePath}}",
"unknownErrorDeletingPoints": "Error tidak dikenal saat menghapus points untuk {{filePath}}",
"failedToProcessBatchWithError": "Gagal memproses batch setelah {{maxRetries}} percobaan: {{errorMessage}}"
"failedToProcessBatchWithError": "Gagal memproses batch setelah {{maxRetries}} percobaan: {{errorMessage}}",
"multiRootPathError": "Tidak dapat memproses file '{{filePath}}' karena berada di luar semua root ruang kerja",
"workspaceRootDetectionError": "Gagal menentukan root ruang kerja untuk file '{{filePath}}'"
},
"vectorStore": {
"qdrantConnectionFailed": "Gagal terhubung ke database vektor Qdrant. Pastikan Qdrant berjalan dan dapat diakses di {{qdrantUrl}}. Error: {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "Errore sconosciuto nell'elaborazione del file {{filePath}}",
"unknownErrorDeletingPoints": "Errore sconosciuto nell'eliminazione dei punti per {{filePath}}",
"failedToProcessBatchWithError": "Elaborazione del batch fallita dopo {{maxRetries}} tentativi: {{errorMessage}}"
"failedToProcessBatchWithError": "Elaborazione del batch fallita dopo {{maxRetries}} tentativi: {{errorMessage}}",
"multiRootPathError": "Impossibile elaborare il file '{{filePath}}' poiché si trova al di fuori di tutte le radici dell'area di lavoro",
"workspaceRootDetectionError": "Impossibile determinare la radice dell'area di lavoro per il file '{{filePath}}'"
},
"vectorStore": {
"qdrantConnectionFailed": "Impossibile connettersi al database vettoriale Qdrant. Assicurati che Qdrant sia in esecuzione e accessibile su {{qdrantUrl}}. Errore: {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "ファイル{{filePath}}の処理中に不明なエラーが発生しました",
"unknownErrorDeletingPoints": "{{filePath}}のポイント削除中に不明なエラーが発生しました",
"failedToProcessBatchWithError": "{{maxRetries}}回の試行後、バッチ処理に失敗しました:{{errorMessage}}"
"failedToProcessBatchWithError": "{{maxRetries}}回の試行後、バッチ処理に失敗しました:{{errorMessage}}",
"multiRootPathError": "ファイル '{{filePath}}' はすべてのワークスペースルートの外にあるため処理できません",
"workspaceRootDetectionError": "ファイル '{{filePath}}' のワークスペースルートを特定できませんでした"
},
"vectorStore": {
"qdrantConnectionFailed": "Qdrantベクターデータベースへの接続に失敗しました。Qdrantが実行中で{{qdrantUrl}}でアクセス可能であることを確認してください。エラー:{{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "파일 {{filePath}} 처리 중 알 수 없는 오류",
"unknownErrorDeletingPoints": "{{filePath}}의 포인트 삭제 중 알 수 없는 오류",
"failedToProcessBatchWithError": "{{maxRetries}}번 시도 후 배치 처리 실패: {{errorMessage}}"
"failedToProcessBatchWithError": "{{maxRetries}}번 시도 후 배치 처리 실패: {{errorMessage}}",
"multiRootPathError": "파일 '{{filePath}}'이(가) 모든 작업 공간 루트 외부에 있으므로 처리할 수 없습니다",
"workspaceRootDetectionError": "파일 '{{filePath}}'에 대한 작업 공간 루트를 확인할 수 없습니다"
},
"vectorStore": {
"qdrantConnectionFailed": "Qdrant 벡터 데이터베이스에 연결하지 못했습니다. Qdrant가 실행 중이고 {{qdrantUrl}}에서 접근 가능한지 확인하세요. 오류: {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "Onbekende fout bij verwerken van bestand {{filePath}}",
"unknownErrorDeletingPoints": "Onbekende fout bij verwijderen van punten voor {{filePath}}",
"failedToProcessBatchWithError": "Verwerken van batch mislukt na {{maxRetries}} pogingen: {{errorMessage}}"
"failedToProcessBatchWithError": "Verwerken van batch mislukt na {{maxRetries}} pogingen: {{errorMessage}}",
"multiRootPathError": "Kan bestand '{{filePath}}' niet verwerken omdat het buiten alle werkruimtewortels valt",
"workspaceRootDetectionError": "Kan de werkruimtewortel voor bestand '{{filePath}}' niet bepalen"
},
"vectorStore": {
"qdrantConnectionFailed": "Kan geen verbinding maken met Qdrant vectordatabase. Zorg ervoor dat Qdrant draait en toegankelijk is op {{qdrantUrl}}. Fout: {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "Nieznany błąd podczas przetwarzania pliku {{filePath}}",
"unknownErrorDeletingPoints": "Nieznany błąd podczas usuwania punktów dla {{filePath}}",
"failedToProcessBatchWithError": "Nie udało się przetworzyć partii po {{maxRetries}} próbach: {{errorMessage}}"
"failedToProcessBatchWithError": "Nie udało się przetworzyć partii po {{maxRetries}} próbach: {{errorMessage}}",
"multiRootPathError": "Nie można przetworzyć pliku '{{filePath}}', ponieważ znajduje się poza wszystkimi katalogami głównymi obszaru roboczego",
"workspaceRootDetectionError": "Nie udało się ustalić katalogu głównego obszaru roboczego dla pliku '{{filePath}}'"
},
"vectorStore": {
"qdrantConnectionFailed": "Nie udało się połączyć z bazą danych wektorowych Qdrant. Upewnij się, że Qdrant jest uruchomiony i dostępny pod adresem {{qdrantUrl}}. Błąd: {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "Erro desconhecido ao processar arquivo {{filePath}}",
"unknownErrorDeletingPoints": "Erro desconhecido ao deletar pontos para {{filePath}}",
"failedToProcessBatchWithError": "Falha ao processar lote após {{maxRetries}} tentativas: {{errorMessage}}"
"failedToProcessBatchWithError": "Falha ao processar lote após {{maxRetries}} tentativas: {{errorMessage}}",
"multiRootPathError": "Não é possível processar o arquivo '{{filePath}}', pois ele está fora de todas as raízes do espaço de trabalho",
"workspaceRootDetectionError": "Falha ao determinar a raiz do espaço de trabalho para o arquivo '{{filePath}}'"
},
"vectorStore": {
"qdrantConnectionFailed": "Falha ao conectar com o banco de dados vetorial Qdrant. Certifique-se de que o Qdrant esteja rodando e acessível em {{qdrantUrl}}. Erro: {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "Неизвестная ошибка при обработке файла {{filePath}}",
"unknownErrorDeletingPoints": "Неизвестная ошибка при удалении точек для {{filePath}}",
"failedToProcessBatchWithError": "Не удалось обработать пакет после {{maxRetries}} попыток: {{errorMessage}}"
"failedToProcessBatchWithError": "Не удалось обработать пакет после {{maxRetries}} попыток: {{errorMessage}}",
"multiRootPathError": "Невозможно обработать файл '{{filePath}}', так как он находится вне всех корневых каталогов рабочего пространства",
"workspaceRootDetectionError": "Не удалось определить корневой каталог рабочего пространства для файла '{{filePath}}'"
},
"vectorStore": {
"qdrantConnectionFailed": "Не удалось подключиться к векторной базе данных Qdrant. Убедитесь, что Qdrant запущен и доступен по адресу {{qdrantUrl}}. Ошибка: {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "{{filePath}} dosyası işlenirken bilinmeyen hata",
"unknownErrorDeletingPoints": "{{filePath}} için noktalar silinirken bilinmeyen hata",
"failedToProcessBatchWithError": "{{maxRetries}} denemeden sonra toplu işlem başarısız oldu: {{errorMessage}}"
"failedToProcessBatchWithError": "{{maxRetries}} denemeden sonra toplu işlem başarısız oldu: {{errorMessage}}",
"multiRootPathError": "'{{filePath}}' dosyası tüm çalışma alanı köklerinin dışında olduğu için işlenemiyor",
"workspaceRootDetectionError": "'{{filePath}}' dosyası için çalışma alanı kökü belirlenemedi"
},
"vectorStore": {
"qdrantConnectionFailed": "Qdrant vektör veritabanına bağlanılamadı. Qdrant'ın çalıştığından ve {{qdrantUrl}} adresinde erişilebilir olduğundan emin olun. Hata: {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "Lỗi không xác định khi xử lý tệp {{filePath}}",
"unknownErrorDeletingPoints": "Lỗi không xác định khi xóa điểm cho {{filePath}}",
"failedToProcessBatchWithError": "Không thể xử lý lô sau {{maxRetries}} lần thử: {{errorMessage}}"
"failedToProcessBatchWithError": "Không thể xử lý lô sau {{maxRetries}} lần thử: {{errorMessage}}",
"multiRootPathError": "Không thể xử lý tệp '{{filePath}}' vì nó nằm ngoài tất cả các thư mục gốc của không gian làm việc",
"workspaceRootDetectionError": "Không thể xác định thư mục gốc của không gian làm việc cho tệp '{{filePath}}'"
},
"vectorStore": {
"qdrantConnectionFailed": "Không thể kết nối với cơ sở dữ liệu vector Qdrant. Vui lòng đảm bảo Qdrant đang chạy và có thể truy cập tại {{qdrantUrl}}. Lỗi: {{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "处理文件 {{filePath}} 时出现未知错误",
"unknownErrorDeletingPoints": "删除 {{filePath}} 的数据点时出现未知错误",
"failedToProcessBatchWithError": "尝试 {{maxRetries}} 次后批次处理失败:{{errorMessage}}"
"failedToProcessBatchWithError": "尝试 {{maxRetries}} 次后批次处理失败:{{errorMessage}}",
"multiRootPathError": "无法处理文件 '{{filePath}}',因为它在所有工作区根之外",
"workspaceRootDetectionError": "无法确定文件 '{{filePath}}' 的工作区根"
},
"vectorStore": {
"qdrantConnectionFailed": "连接 Qdrant 向量数据库失败。请确保 Qdrant 正在运行并可在 {{qdrantUrl}} 访问。错误:{{errorMessage}}"

View file

@ -15,7 +15,9 @@
"scanner": {
"unknownErrorProcessingFile": "處理檔案 {{filePath}} 時發生未知錯誤",
"unknownErrorDeletingPoints": "刪除 {{filePath}} 的資料點時發生未知錯誤",
"failedToProcessBatchWithError": "嘗試 {{maxRetries}} 次後批次處理失敗:{{errorMessage}}"
"failedToProcessBatchWithError": "嘗試 {{maxRetries}} 次後批次處理失敗:{{errorMessage}}",
"multiRootPathError": "無法處理檔案 '{{filePath}}',因為它在所有工作區根目錄之外",
"workspaceRootDetectionError": "無法確定檔案 '{{filePath}}' 的工作區根目錄"
},
"vectorStore": {
"qdrantConnectionFailed": "連接 Qdrant 向量資料庫失敗。請確保 Qdrant 正在執行並可在 {{qdrantUrl}} 存取。錯誤:{{errorMessage}}"

View file

@ -338,6 +338,15 @@
"type": "string",
"default": "",
"description": "%settings.autoImportSettingsPath.description%"
},
"roo-cline.codebaseIndexing.multiRootBehavior": {
"type": "string",
"enum": [
"separate",
"unified"
],
"default": "separate",
"description": "%settings.codebaseIndexing.multiRootBehavior.description%"
}
}
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "La família del model de llenguatge (p. ex. gpt-4)",
"settings.customStoragePath.description": "Ruta d'emmagatzematge personalitzada. Deixeu-la buida per utilitzar la ubicació predeterminada. Admet rutes absolutes (p. ex. 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Habilitar correccions ràpides de Roo Code.",
"settings.autoImportSettingsPath.description": "Ruta a un fitxer de configuració de RooCode per importar automàticament en iniciar l'extensió. Admet rutes absolutes i rutes relatives al directori d'inici (per exemple, '~/Documents/roo-code-settings.json'). Deixeu-ho en blanc per desactivar la importació automàtica."
"settings.autoImportSettingsPath.description": "Ruta a un fitxer de configuració de RooCode per importar automàticament en iniciar l'extensió. Admet rutes absolutes i rutes relatives al directori d'inici (per exemple, '~/Documents/roo-code-settings.json'). Deixeu-ho en blanc per desactivar la importació automàtica.",
"settings.codebaseIndexing.multiRootBehavior.description": "Com gestionar la indexació de la base de codi en espais de treball d'arrels múltiples. 'separate' indexa cada arrel de l'espai de treball de manera independent, 'unified' tracta totes les arrels com una única base de codi."
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "Die Familie des Sprachmodells (z.B. gpt-4)",
"settings.customStoragePath.description": "Benutzerdefinierter Speicherpfad. Leer lassen, um den Standardspeicherort zu verwenden. Unterstützt absolute Pfade (z.B. 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Roo Code Schnelle Problembehebung aktivieren.",
"settings.autoImportSettingsPath.description": "Pfad zu einer RooCode-Konfigurationsdatei, die beim Start der Erweiterung automatisch importiert wird. Unterstützt absolute Pfade und Pfade relativ zum Home-Verzeichnis (z.B. '~/Documents/roo-code-settings.json'). Leer lassen, um den automatischen Import zu deaktivieren."
"settings.autoImportSettingsPath.description": "Pfad zu einer RooCode-Konfigurationsdatei, die beim Start der Erweiterung automatisch importiert wird. Unterstützt absolute Pfade und Pfade relativ zum Home-Verzeichnis (z.B. '~/Documents/roo-code-settings.json'). Leer lassen, um den automatischen Import zu deaktivieren.",
"settings.codebaseIndexing.multiRootBehavior.description": "Wie die Codebasis-Indizierung in Arbeitsbereichen mit mehreren Stammordnern gehandhabt werden soll. 'separate' indiziert jeden Arbeitsbereich-Stammordner unabhängig, 'unified' behandelt alle Stammordner als eine einzige Codebasis."
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "La familia del modelo de lenguaje (ej. gpt-4)",
"settings.customStoragePath.description": "Ruta de almacenamiento personalizada. Dejar vacío para usar la ubicación predeterminada. Admite rutas absolutas (ej. 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Habilitar correcciones rápidas de Roo Code.",
"settings.autoImportSettingsPath.description": "Ruta a un archivo de configuración de RooCode para importar automáticamente al iniciar la extensión. Admite rutas absolutas y rutas relativas al directorio de inicio (por ejemplo, '~/Documents/roo-code-settings.json'). Dejar vacío para desactivar la importación automática."
"settings.autoImportSettingsPath.description": "Ruta a un archivo de configuración de RooCode para importar automáticamente al iniciar la extensión. Admite rutas absolutas y rutas relativas al directorio de inicio (por ejemplo, '~/Documents/roo-code-settings.json'). Dejar vacío para desactivar la importación automática.",
"settings.codebaseIndexing.multiRootBehavior.description": "Cómo manejar la indexación de la base de código en espacios de trabajo de múltiples raíces. 'separate' indexa cada raíz del espacio de trabajo de forma independiente, 'unified' trata todas las raíces como una única base de código."
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "La famille du modèle de langage (ex: gpt-4)",
"settings.customStoragePath.description": "Chemin de stockage personnalisé. Laisser vide pour utiliser l'emplacement par défaut. Prend en charge les chemins absolus (ex: 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Activer les correctifs rapides de Roo Code.",
"settings.autoImportSettingsPath.description": "Chemin d'accès à un fichier de configuration RooCode à importer automatiquement au démarrage de l'extension. Prend en charge les chemins absolus et les chemins relatifs au répertoire de base (par exemple, '~/Documents/roo-code-settings.json'). Laisser vide pour désactiver l'importation automatique."
"settings.autoImportSettingsPath.description": "Chemin d'accès à un fichier de configuration RooCode à importer automatiquement au démarrage de l'extension. Prend en charge les chemins absolus et les chemins relatifs au répertoire de base (par exemple, '~/Documents/roo-code-settings.json'). Laisser vide pour désactiver l'importation automatique.",
"settings.codebaseIndexing.multiRootBehavior.description": "Comment gérer l'indexation de la base de code dans les espaces de travail à racines multiples. 'separate' indexe chaque racine de l'espace de travail indépendamment, 'unified' traite toutes les racines comme une seule base de code."
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "भाषा मॉडल का परिवार (उदा. gpt-4)",
"settings.customStoragePath.description": "कस्टम स्टोरेज पाथ। डिफ़ॉल्ट स्थान का उपयोग करने के लिए खाली छोड़ें। पूर्ण पथ का समर्थन करता है (उदा. 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Roo Code त्वरित सुधार सक्षम करें",
"settings.autoImportSettingsPath.description": "RooCode कॉन्फ़िगरेशन फ़ाइल का पथ जिसे एक्सटेंशन स्टार्टअप पर स्वचालित रूप से आयात किया जाएगा। होम डायरेक्टरी के सापेक्ष पूर्ण पथ और पथों का समर्थन करता है (उदाहरण के लिए '~/Documents/roo-code-settings.json')। ऑटो-इंपोर्ट को अक्षम करने के लिए खाली छोड़ दें।"
"settings.autoImportSettingsPath.description": "RooCode कॉन्फ़िगरेशन फ़ाइल का पथ जिसे एक्सटेंशन स्टार्टअप पर स्वचालित रूप से आयात किया जाएगा। होम डायरेक्टरी के सापेक्ष पूर्ण पथ और पथों का समर्थन करता है (उदाहरण के लिए '~/Documents/roo-code-settings.json')। ऑटो-इंपोर्ट को अक्षम करने के लिए खाली छोड़ दें।",
"settings.codebaseIndexing.multiRootBehavior.description": "बहु-रूट कार्यस्थानوں में कोडबेस इंडेक्सिंग को कैसे संभालें। 'separate' प्रत्येक कार्यस्थान रूट को स्वतंत्र रूप से इंडेक्स करता है, 'unified' सभी रूटों को एक ही कोडबेस के रूप में मानता है।"
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "Keluarga dari model bahasa (misalnya gpt-4)",
"settings.customStoragePath.description": "Path penyimpanan kustom. Biarkan kosong untuk menggunakan lokasi default. Mendukung path absolut (misalnya 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Aktifkan perbaikan cepat Roo Code.",
"settings.autoImportSettingsPath.description": "Path ke file konfigurasi RooCode untuk diimpor secara otomatis saat ekstensi dimulai. Mendukung path absolut dan path relatif terhadap direktori home (misalnya '~/Documents/roo-code-settings.json'). Biarkan kosong untuk menonaktifkan impor otomatis."
"settings.autoImportSettingsPath.description": "Path ke file konfigurasi RooCode untuk diimpor secara otomatis saat ekstensi dimulai. Mendukung path absolut dan path relatif terhadap direktori home (misalnya '~/Documents/roo-code-settings.json'). Biarkan kosong untuk menonaktifkan impor otomatis.",
"settings.codebaseIndexing.multiRootBehavior.description": "Cara menangani pengindeksan basis kode di ruang kerja multi-root. 'separate' mengindeks setiap root ruang kerja secara independen, 'unified' memperlakukan semua root sebagai basis kode tunggal."
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "La famiglia del modello linguistico (es. gpt-4)",
"settings.customStoragePath.description": "Percorso di archiviazione personalizzato. Lasciare vuoto per utilizzare la posizione predefinita. Supporta percorsi assoluti (es. 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Abilita correzioni rapide di Roo Code.",
"settings.autoImportSettingsPath.description": "Percorso di un file di configurazione di RooCode da importare automaticamente all'avvio dell'estensione. Supporta percorsi assoluti e percorsi relativi alla directory home (ad es. '~/Documents/roo-code-settings.json'). Lasciare vuoto per disabilitare l'importazione automatica."
"settings.autoImportSettingsPath.description": "Percorso di un file di configurazione di RooCode da importare automaticamente all'avvio dell'estensione. Supporta percorsi assoluti e percorsi relativi alla directory home (ad es. '~/Documents/roo-code-settings.json'). Lasciare vuoto per disabilitare l'importazione automatica.",
"settings.codebaseIndexing.multiRootBehavior.description": "Come gestire l'indicizzazione della codebase in aree di lavoro multi-root. 'separate' indicizza ogni radice dell'area di lavoro in modo indipendente, 'unified' tratta tutte le radici come un'unica codebase."
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "言語モデルのファミリーgpt-4",
"settings.customStoragePath.description": "カスタムストレージパス。デフォルトの場所を使用する場合は空のままにします。絶対パスをサポートします(例:'D:\\RooCodeStorage'",
"settings.enableCodeActions.description": "Roo Codeのクイック修正を有効にする。",
"settings.autoImportSettingsPath.description": "拡張機能の起動時に自動的にインポートするRooCode設定ファイルへのパス。絶対パスとホームディレクトリからの相対パスをサポートします'~/Documents/roo-code-settings.json')。自動インポートを無効にするには、空のままにします。"
"settings.autoImportSettingsPath.description": "拡張機能の起動時に自動的にインポートするRooCode設定ファイルへのパス。絶対パスとホームディレクトリからの相対パスをサポートします'~/Documents/roo-code-settings.json')。自動インポートを無効にするには、空のままにします。",
"settings.codebaseIndexing.multiRootBehavior.description": "マルチルートワークスペースでコードベースのインデックスを作成する方法。'separate' は各ワークスペースルートを個別にインデックス作成し、'unified' はすべてのルートを単一のコードベースとして扱います。"
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)",
"settings.customStoragePath.description": "Custom storage path. Leave empty to use the default location. Supports absolute paths (e.g. 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Enable Roo Code quick fixes",
"settings.autoImportSettingsPath.description": "Path to a RooCode configuration file to automatically import on extension startup. Supports absolute paths and paths relative to the home directory (e.g. '~/Documents/roo-code-settings.json'). Leave empty to disable auto-import."
"settings.autoImportSettingsPath.description": "Path to a RooCode configuration file to automatically import on extension startup. Supports absolute paths and paths relative to the home directory (e.g. '~/Documents/roo-code-settings.json'). Leave empty to disable auto-import.",
"settings.codebaseIndexing.multiRootBehavior.description": "How to handle codebase indexing in multi-root workspaces. 'separate' indexes each workspace root independently, 'unified' treats all roots as a single codebase."
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "언어 모델 계열 (예: gpt-4)",
"settings.customStoragePath.description": "사용자 지정 저장소 경로. 기본 위치를 사용하려면 비워두세요. 절대 경로를 지원합니다 (예: 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Roo Code 빠른 수정 사용 설정",
"settings.autoImportSettingsPath.description": "확장 프로그램 시작 시 자동으로 가져올 RooCode 구성 파일의 경로입니다. 절대 경로 및 홈 디렉토리에 대한 상대 경로를 지원합니다(예: '~/Documents/roo-code-settings.json'). 자동 가져오기를 비활성화하려면 비워 둡니다."
"settings.autoImportSettingsPath.description": "확장 프로그램 시작 시 자동으로 가져올 RooCode 구성 파일의 경로입니다. 절대 경로 및 홈 디렉토리에 대한 상대 경로를 지원합니다(예: '~/Documents/roo-code-settings.json'). 자동 가져오기를 비활성화하려면 비워 둡니다.",
"settings.codebaseIndexing.multiRootBehavior.description": "다중 루트 작업 공간에서 코드베이스 인덱싱을 처리하는 방법입니다. 'separate'는 각 작업 공간 루트를 독립적으로 인덱싱하고 'unified'는 모든 루트를 단일 코드베이스로 처리합니다."
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "De familie van het taalmodel (bijv. gpt-4)",
"settings.customStoragePath.description": "Aangepast opslagpad. Laat leeg om de standaardlocatie te gebruiken. Ondersteunt absolute paden (bijv. 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Snelle correcties van Roo Code inschakelen.",
"settings.autoImportSettingsPath.description": "Pad naar een RooCode-configuratiebestand om automatisch te importeren bij het opstarten van de extensie. Ondersteunt absolute paden en paden ten opzichte van de thuismap (bijv. '~/Documents/roo-code-settings.json'). Laat leeg om automatisch importeren uit te schakelen."
"settings.autoImportSettingsPath.description": "Pad naar een RooCode-configuratiebestand om automatisch te importeren bij het opstarten van de extensie. Ondersteunt absolute paden en paden ten opzichte van de thuismap (bijv. '~/Documents/roo-code-settings.json'). Laat leeg om automatisch importeren uit te schakelen.",
"settings.codebaseIndexing.multiRootBehavior.description": "Hoe de codebase-indexering in werkruimten met meerdere roots moet worden afgehandeld. 'separate' indexeert elke werkruimteroot onafhankelijk, 'unified' behandelt alle roots als één codebase."
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "Rodzina modelu językowego (np. gpt-4)",
"settings.customStoragePath.description": "Niestandardowa ścieżka przechowywania. Pozostaw puste, aby użyć domyślnej lokalizacji. Obsługuje ścieżki bezwzględne (np. 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Włącz szybkie poprawki Roo Code.",
"settings.autoImportSettingsPath.description": "Ścieżka do pliku konfiguracyjnego RooCode, który ma być automatycznie importowany podczas uruchamiania rozszerzenia. Obsługuje ścieżki bezwzględne i ścieżki względne do katalogu domowego (np. '~/Documents/roo-code-settings.json'). Pozostaw puste, aby wyłączyć automatyczne importowanie."
"settings.autoImportSettingsPath.description": "Ścieżka do pliku konfiguracyjnego RooCode, który ma być automatycznie importowany podczas uruchamiania rozszerzenia. Obsługuje ścieżki bezwzględne i ścieżki względne do katalogu domowego (np. '~/Documents/roo-code-settings.json'). Pozostaw puste, aby wyłączyć automatyczne importowanie.",
"settings.codebaseIndexing.multiRootBehavior.description": "Jak obsługiwać indeksowanie bazy kodu w obszarach roboczych z wieloma katalogami głównymi. 'separate' indeksuje każdy katalog główny obszaru roboczego niezależnie, 'unified' traktuje wszystkie katalogi główne jako jedną bazę kodu."
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "A família do modelo de linguagem (ex: gpt-4)",
"settings.customStoragePath.description": "Caminho de armazenamento personalizado. Deixe vazio para usar o local padrão. Suporta caminhos absolutos (ex: 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Habilitar correções rápidas do Roo Code.",
"settings.autoImportSettingsPath.description": "Caminho para um arquivo de configuração do RooCode para importar automaticamente na inicialização da extensão. Suporta caminhos absolutos e caminhos relativos ao diretório inicial (por exemplo, '~/Documents/roo-code-settings.json'). Deixe em branco para desativar a importação automática."
"settings.autoImportSettingsPath.description": "Caminho para um arquivo de configuração do RooCode para importar automaticamente na inicialização da extensão. Suporta caminhos absolutos e caminhos relativos ao diretório inicial (por exemplo, '~/Documents/roo-code-settings.json'). Deixe em branco para desativar a importação automática.",
"settings.codebaseIndexing.multiRootBehavior.description": "Como lidar com a indexação da base de código em espaços de trabalho de várias raízes. 'separate' indexa cada raiz do espaço de trabalho independentemente, 'unified' trata todas as raízes como uma única base de código."
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "Семейство языковой модели (например, gpt-4)",
"settings.customStoragePath.description": "Пользовательский путь хранения. Оставьте пустым для использования пути по умолчанию. Поддерживает абсолютные пути (например, 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Включить быстрые исправления Roo Code.",
"settings.autoImportSettingsPath.description": "Путь к файлу конфигурации RooCode для автоматического импорта при запуске расширения. Поддерживает абсолютные пути и пути относительно домашнего каталога (например, '~/Documents/roo-code-settings.json'). Оставьте пустым, чтобы отключить автоматический импорт."
"settings.autoImportSettingsPath.description": "Путь к файлу конфигурации RooCode для автоматического импорта при запуске расширения. Поддерживает абсолютные пути и пути относительно домашнего каталога (например, '~/Documents/roo-code-settings.json'). Оставьте пустым, чтобы отключить автоматический импорт.",
"settings.codebaseIndexing.multiRootBehavior.description": "Как обрабатывать индексацию кодовой базы в многокорневых рабочих пространствах. 'separate' индексирует каждый корень рабочего пространства независимо, 'unified' рассматривает все корни как единую кодовую базу."
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "Dil modelinin ailesi (örn: gpt-4)",
"settings.customStoragePath.description": "Özel depolama yolu. Varsayılan konumu kullanmak için boş bırakın. Mutlak yolları destekler (örn: 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Roo Code hızlı düzeltmeleri etkinleştir.",
"settings.autoImportSettingsPath.description": "Uzantı başlangıcında otomatik olarak içe aktarılacak bir RooCode yapılandırma dosyasının yolu. Mutlak yolları ve ana dizine göreli yolları destekler (ör. '~/Documents/roo-code-settings.json'). Otomatik içe aktarmayı devre dışı bırakmak için boş bırakın."
"settings.autoImportSettingsPath.description": "Uzantı başlangıcında otomatik olarak içe aktarılacak bir RooCode yapılandırma dosyasının yolu. Mutlak yolları ve ana dizine göreli yolları destekler (ör. '~/Documents/roo-code-settings.json'). Otomatik içe aktarmayı devre dışı bırakmak için boş bırakın.",
"settings.codebaseIndexing.multiRootBehavior.description": "Çok köklü çalışma alanlarında kod tabanı dizinlemesinin nasıl işleneceği. 'separate' her çalışma alanı kökünü bağımsız olarak dizinler, 'unified' tüm kökleri tek bir kod tabanı olarak ele alır."
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "Họ mô hình ngôn ngữ (ví dụ: gpt-4)",
"settings.customStoragePath.description": "Đường dẫn lưu trữ tùy chỉnh. Để trống để sử dụng vị trí mặc định. Hỗ trợ đường dẫn tuyệt đối (ví dụ: 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Bật sửa lỗi nhanh Roo Code.",
"settings.autoImportSettingsPath.description": "Đường dẫn đến tệp cấu hình RooCode để tự động nhập khi khởi động tiện ích mở rộng. Hỗ trợ đường dẫn tuyệt đối và đường dẫn tương đối đến thư mục chính (ví dụ: '~/Documents/roo-code-settings.json'). Để trống để tắt tính năng tự động nhập."
"settings.autoImportSettingsPath.description": "Đường dẫn đến tệp cấu hình RooCode để tự động nhập khi khởi động tiện ích mở rộng. Hỗ trợ đường dẫn tuyệt đối và đường dẫn tương đối đến thư mục chính (ví dụ: '~/Documents/roo-code-settings.json'). Để trống để tắt tính năng tự động nhập.",
"settings.codebaseIndexing.multiRootBehavior.description": "Cách xử lý chỉ mục cơ sở mã trong không gian làm việc nhiều thư mục gốc. 'separate' chỉ mục mỗi thư mục gốc của không gian làm việc một cách độc lập, 'unified' coi tất cả các thư mục gốc là một cơ sở mã duy nhất."
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "语言模型的系列例如gpt-4",
"settings.customStoragePath.description": "自定义存储路径。留空以使用默认位置。支持绝对路径(例如:'D:\\RooCodeStorage'",
"settings.enableCodeActions.description": "启用 Roo Code 快速修复",
"settings.autoImportSettingsPath.description": "RooCode 配置文件的路径,用于在扩展启动时自动导入。支持绝对路径和相对于主目录的路径(例如 '~/Documents/roo-code-settings.json')。留空以禁用自动导入。"
"settings.autoImportSettingsPath.description": "RooCode 配置文件的路径,用于在扩展启动时自动导入。支持绝对路径和相对于主目录的路径(例如 '~/Documents/roo-code-settings.json')。留空以禁用自动导入。",
"settings.codebaseIndexing.multiRootBehavior.description": "如何在多根工作区中处理代码库索引。'separate' 单独索引每个工作区根,'unified' 将所有根视为单个代码库。"
}

View file

@ -32,5 +32,6 @@
"settings.vsCodeLmModelSelector.family.description": "語言模型系列例如gpt-4",
"settings.customStoragePath.description": "自訂儲存路徑。留空以使用預設位置。支援絕對路徑(例如:'D:\\RooCodeStorage'",
"settings.enableCodeActions.description": "啟用 Roo Code 快速修復。",
"settings.autoImportSettingsPath.description": "RooCode 設定檔案的路徑,用於在擴充功能啟動時自動匯入。支援絕對路徑和相對於主目錄的路徑(例如 '~/Documents/roo-code-settings.json')。留空以停用自動匯入。"
"settings.autoImportSettingsPath.description": "RooCode 設定檔案的路徑,用於在擴充功能啟動時自動匯入。支援絕對路徑和相對於主目錄的路徑(例如 '~/Documents/roo-code-settings.json')。留空以停用自動匯入。",
"settings.codebaseIndexing.multiRootBehavior.description": "如何處理多根工作區中的程式碼庫索引。'separate' 會獨立索引每個工作區根,'unified' 會將所有根視為單一程式碼庫。"
}

View file

@ -0,0 +1,249 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import * as vscode from "vscode"
import { DirectoryScanner } from "../processors/scanner"
import { CodeIndexOrchestrator } from "../orchestrator"
import { Ignore } from "ignore"
import * as workspaceUtils from "../shared/workspace-utils"
// Mock dependencies
vi.mock("vscode")
vi.mock("../../glob/list-files")
vi.mock("../../../core/ignore/RooIgnoreController")
vi.mock("fs/promises")
vi.mock("../shared/workspace-utils")
describe("Multi-root workspace indexing", () => {
let scanner: DirectoryScanner
let orchestrator: CodeIndexOrchestrator
let mockEmbedder: any
let mockVectorStore: any
let mockCodeParser: any
let mockCacheManager: any
let mockConfigManager: any
let mockStateManager: any
let mockFileWatcher: any
let mockIgnoreInstance: Ignore
beforeEach(() => {
// Setup mocks
mockEmbedder = {
createEmbeddings: vi.fn().mockResolvedValue({ embeddings: [[0.1, 0.2, 0.3]] }),
}
mockVectorStore = {
initialize: vi.fn().mockResolvedValue(true),
upsertPoints: vi.fn().mockResolvedValue(undefined),
deletePointsByFilePath: vi.fn().mockResolvedValue(undefined),
deletePointsByMultipleFilePaths: vi.fn().mockResolvedValue(undefined),
clearCollection: vi.fn().mockResolvedValue(undefined),
deleteCollection: vi.fn().mockResolvedValue(undefined),
}
mockCodeParser = {
parseFile: vi.fn().mockResolvedValue([
{
file_path: "/workspace/project1/src/file.ts",
content: "function test() {}",
start_line: 1,
end_line: 1,
},
]),
}
mockCacheManager = {
getHash: vi.fn().mockReturnValue(null),
updateHash: vi.fn().mockResolvedValue(undefined),
deleteHash: vi.fn().mockResolvedValue(undefined),
getAllHashes: vi.fn().mockReturnValue({}),
clearCacheFile: vi.fn().mockResolvedValue(undefined),
}
mockIgnoreInstance = {
ignores: vi.fn().mockReturnValue(false),
} as any
mockConfigManager = {
isFeatureConfigured: true,
}
mockStateManager = {
state: "Standby",
setSystemState: vi.fn(),
reportBlockIndexingProgress: vi.fn(),
reportFileQueueProgress: vi.fn(),
}
mockFileWatcher = {
initialize: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
onDidStartBatchProcessing: vi.fn().mockReturnValue({ dispose: vi.fn() }),
onBatchProgressUpdate: vi.fn().mockReturnValue({ dispose: vi.fn() }),
onDidFinishBatchProcessing: vi.fn().mockReturnValue({ dispose: vi.fn() }),
}
// Create instances
scanner = new DirectoryScanner(
mockEmbedder,
mockVectorStore,
mockCodeParser,
mockCacheManager,
mockIgnoreInstance,
)
orchestrator = new CodeIndexOrchestrator(
mockConfigManager,
mockStateManager,
"/workspace/project1",
mockCacheManager,
mockVectorStore,
scanner,
mockFileWatcher,
)
})
afterEach(() => {
vi.clearAllMocks()
})
describe("Scanner multi-root support", () => {
it("should process files from multiple workspace roots", async () => {
// Mock multi-root workspace
vi.mocked(workspaceUtils.isMultiRootWorkspace).mockReturnValue(true)
vi.mocked(workspaceUtils.getAllWorkspaceRoots).mockReturnValue([
"/workspace/project1",
"/workspace/project2",
])
// Mock file listing for each workspace
const listFiles = await import("../../glob/list-files")
vi.mocked(listFiles.listFiles)
.mockResolvedValueOnce([["/workspace/project1/src/file1.ts"], true])
.mockResolvedValueOnce([["/workspace/project2/src/file2.ts"], true])
// Mock workspace root detection
vi.mocked(workspaceUtils.getWorkspaceRootForFile).mockImplementation((filePath) => {
if (filePath.includes("project1")) return "/workspace/project1"
if (filePath.includes("project2")) return "/workspace/project2"
return undefined
})
// Mock file stats
const fs = await import("fs/promises")
vi.mocked(fs.stat).mockResolvedValue({ size: 1000 } as any)
// Mock file reading
vi.mocked(vscode.workspace.fs.readFile).mockResolvedValue(Buffer.from("function test() {}"))
// Mock RooIgnoreController
const { RooIgnoreController } = await import("../../../core/ignore/RooIgnoreController")
vi.mocked(RooIgnoreController).mockImplementation(
() =>
({
initialize: vi.fn().mockResolvedValue(undefined),
filterPaths: vi.fn().mockImplementation((paths) => paths),
validateAccess: vi.fn().mockReturnValue(true),
}) as any,
)
// Run scan
const result = await scanner.scanDirectory("/workspace/project1")
// Verify both workspace roots were processed
expect(listFiles.listFiles).toHaveBeenCalledTimes(2)
expect(listFiles.listFiles).toHaveBeenCalledWith("/workspace/project1", true, expect.any(Number))
expect(listFiles.listFiles).toHaveBeenCalledWith("/workspace/project2", true, expect.any(Number))
// Verify files were parsed
expect(mockCodeParser.parseFile).toHaveBeenCalledTimes(2)
expect(result.stats.processed).toBe(2)
})
it("should skip files outside all workspace roots", async () => {
// Mock single root workspace
vi.mocked(workspaceUtils.isMultiRootWorkspace).mockReturnValue(false)
vi.mocked(workspaceUtils.getAllWorkspaceRoots).mockReturnValue(["/workspace/project1"])
// Mock file listing
const listFiles = await import("../../glob/list-files")
vi.mocked(listFiles.listFiles).mockResolvedValue([
["/workspace/project1/src/file1.ts", "/outside/workspace/file2.ts"],
true,
])
// Mock workspace root detection
vi.mocked(workspaceUtils.getWorkspaceRootForFile).mockImplementation((filePath) => {
if (filePath.includes("project1")) return "/workspace/project1"
return undefined
})
// Mock file stats
const fs = await import("fs/promises")
vi.mocked(fs.stat).mockResolvedValue({ size: 1000 } as any)
// Mock file reading
vi.mocked(vscode.workspace.fs.readFile).mockResolvedValue(Buffer.from("function test() {}"))
// Mock RooIgnoreController
const { RooIgnoreController } = await import("../../../core/ignore/RooIgnoreController")
vi.mocked(RooIgnoreController).mockImplementation(
() =>
({
initialize: vi.fn().mockResolvedValue(undefined),
filterPaths: vi.fn().mockImplementation((paths) => paths),
validateAccess: vi.fn().mockReturnValue(true),
}) as any,
)
// Run scan
const result = await scanner.scanDirectory("/workspace/project1")
// Verify only file within workspace was processed
expect(mockCodeParser.parseFile).toHaveBeenCalledTimes(1)
expect(mockCodeParser.parseFile).toHaveBeenCalledWith(
"/workspace/project1/src/file1.ts",
expect.any(Object),
)
expect(result.stats.processed).toBe(1)
})
})
describe("Orchestrator multi-root support", () => {
it("should display multi-root status messages", async () => {
// Mock multi-root workspace
vi.mocked(workspaceUtils.isMultiRootWorkspace).mockReturnValue(true)
vi.mocked(workspaceUtils.getAllWorkspaceRoots).mockReturnValue([
"/workspace/project1",
"/workspace/project2",
"/workspace/project3",
])
// Mock file listing
const listFiles = await import("../../glob/list-files")
vi.mocked(listFiles.listFiles).mockResolvedValue([[], false])
// Mock RooIgnoreController
const { RooIgnoreController } = await import("../../../core/ignore/RooIgnoreController")
vi.mocked(RooIgnoreController).mockImplementation(
() =>
({
initialize: vi.fn().mockResolvedValue(undefined),
filterPaths: vi.fn().mockImplementation((paths) => paths),
validateAccess: vi.fn().mockReturnValue(true),
}) as any,
)
// Start indexing
await orchestrator.startIndexing()
// Verify multi-root specific status messages
expect(mockStateManager.setSystemState).toHaveBeenCalledWith(
"Indexing",
"Services ready. Starting scan of 3 workspace folders...",
)
expect(mockStateManager.setSystemState).toHaveBeenCalledWith(
"Indexed",
"File watcher started for 3 workspace folders.",
)
})
})
})

View file

@ -5,6 +5,7 @@ import { CodeIndexStateManager, IndexingState } from "./state-manager"
import { IFileWatcher, IVectorStore, BatchProcessingSummary } from "./interfaces"
import { DirectoryScanner } from "./processors"
import { CacheManager } from "./cache-manager"
import { getAllWorkspaceRoots, isMultiRootWorkspace } from "./shared/workspace-utils"
/**
* Manages the code indexing workflow, coordinating between different services and managers.
@ -115,7 +116,18 @@ export class CodeIndexOrchestrator {
await this.cacheManager.clearCacheFile()
}
this.stateManager.setSystemState("Indexing", "Services ready. Starting workspace scan...")
// Determine if we're in a multi-root workspace
const isMultiRoot = isMultiRootWorkspace()
const workspaceRoots = getAllWorkspaceRoots()
if (isMultiRoot) {
this.stateManager.setSystemState(
"Indexing",
`Services ready. Starting scan of ${workspaceRoots.length} workspace folders...`,
)
} else {
this.stateManager.setSystemState("Indexing", "Services ready. Starting workspace scan...")
}
let cumulativeBlocksIndexed = 0
let cumulativeBlocksFoundSoFar = 0
@ -131,6 +143,7 @@ export class CodeIndexOrchestrator {
this.stateManager.reportBlockIndexingProgress(cumulativeBlocksIndexed, cumulativeBlocksFoundSoFar)
}
// The scanner will handle multi-root workspaces internally
const result = await this.scanner.scanDirectory(
this.workspacePath,
(batchError: Error) => {
@ -166,7 +179,14 @@ export class CodeIndexOrchestrator {
await this._startWatcher()
this.stateManager.setSystemState("Indexed", "File watcher started.")
if (isMultiRoot) {
this.stateManager.setSystemState(
"Indexed",
`File watcher started for ${workspaceRoots.length} workspace folders.`,
)
} else {
this.stateManager.setSystemState("Indexed", "File watcher started.")
}
} catch (error: any) {
console.error("[CodeIndexOrchestrator] Error during indexing:", error)
try {

View file

@ -465,6 +465,14 @@ export class FileWatcher implements IFileWatcher {
// Check if file should be ignored
const relativeFilePath = generateRelativeFilePath(filePath)
if (!relativeFilePath) {
return {
path: filePath,
status: "skipped" as const,
reason: "File is outside all workspace roots",
}
}
if (
!this.ignoreController.validateAccess(filePath) ||
(this.ignoreInstance && this.ignoreInstance.ignores(relativeFilePath))
@ -511,22 +519,33 @@ export class FileWatcher implements IFileWatcher {
const texts = blocks.map((block) => block.content)
const { embeddings } = await this.embedder.createEmbeddings(texts)
pointsToUpsert = blocks.map((block, index) => {
const normalizedAbsolutePath = generateNormalizedAbsolutePath(block.file_path)
const stableName = `${normalizedAbsolutePath}:${block.start_line}`
const pointId = uuidv5(stableName, QDRANT_CODE_BLOCK_NAMESPACE)
pointsToUpsert = blocks
.map((block, index) => {
const normalizedAbsolutePath = generateNormalizedAbsolutePath(block.file_path)
const relativeFilePath = generateRelativeFilePath(normalizedAbsolutePath)
return {
id: pointId,
vector: embeddings[index],
payload: {
filePath: generateRelativeFilePath(normalizedAbsolutePath),
codeChunk: block.content,
startLine: block.start_line,
endLine: block.end_line,
},
}
})
if (!relativeFilePath) {
console.error(
`[FileWatcher] Failed to generate relative path for ${normalizedAbsolutePath}`,
)
return null
}
const stableName = `${normalizedAbsolutePath}:${block.start_line}`
const pointId = uuidv5(stableName, QDRANT_CODE_BLOCK_NAMESPACE)
return {
id: pointId,
vector: embeddings[index],
payload: {
filePath: relativeFilePath,
codeChunk: block.content,
startLine: block.start_line,
endLine: block.end_line,
},
}
})
.filter((point) => point !== null) as PointStruct[]
}
return {

View file

@ -4,6 +4,7 @@ import { RooIgnoreController } from "../../../core/ignore/RooIgnoreController"
import { stat } from "fs/promises"
import * as path from "path"
import { generateNormalizedAbsolutePath, generateRelativeFilePath } from "../shared/get-relative-path"
import { getWorkspaceRootForFile, getAllWorkspaceRoots, isMultiRootWorkspace } from "../shared/workspace-utils"
import { scannerExtensions } from "../shared/supported-extensions"
import * as vscode from "vscode"
import { CodeBlock, ICodeParser, IEmbedder, IVectorStore, IDirectoryScanner } from "../interfaces"
@ -26,6 +27,8 @@ import {
import { isPathInIgnoredDirectory } from "../../glob/ignore-utils"
export class DirectoryScanner implements IDirectoryScanner {
private workspaceIgnoreMap: Map<string, Ignore> = new Map()
constructor(
private readonly embedder: IEmbedder,
private readonly qdrantClient: IVectorStore,
@ -48,32 +51,62 @@ export class DirectoryScanner implements IDirectoryScanner {
onBlocksIndexed?: (indexedCount: number) => void,
onFileParsed?: (fileBlockCount: number) => void,
): Promise<{ codeBlocks: CodeBlock[]; stats: { processed: number; skipped: number }; totalBlockCount: number }> {
const directoryPath = directory
// Get all files recursively (handles .gitignore automatically)
const [allPaths, _] = await listFiles(directoryPath, true, MAX_LIST_FILES_LIMIT)
// In multi-root workspaces, scan all workspace folders
const workspaceRoots = isMultiRootWorkspace() ? getAllWorkspaceRoots() : [directory]
// Filter out directories (marked with trailing '/')
const filePaths = allPaths.filter((p) => !p.endsWith("/"))
// Initialize ignore instances for each workspace root
this.workspaceIgnoreMap.clear()
for (const root of workspaceRoots) {
// Each workspace root gets its own ignore instance
const ignoreController = new RooIgnoreController(root)
await ignoreController.initialize()
// Create a new Ignore instance for this workspace root
const ignore = this.ignoreInstance // For now, reuse the same ignore instance
this.workspaceIgnoreMap.set(root, ignore)
}
// Initialize RooIgnoreController if not provided
const ignoreController = new RooIgnoreController(directoryPath)
// Collect all files from all workspace roots
let allFilePaths: string[] = []
await ignoreController.initialize()
for (const workspaceRoot of workspaceRoots) {
const [paths, _] = await listFiles(workspaceRoot, true, MAX_LIST_FILES_LIMIT)
const filePaths = paths.filter((p) => !p.endsWith("/"))
// Filter paths using .rooignore
const allowedPaths = ignoreController.filterPaths(filePaths)
// Initialize RooIgnoreController for this workspace root
const ignoreController = new RooIgnoreController(workspaceRoot)
await ignoreController.initialize()
// Filter paths using .rooignore
const allowedPaths = ignoreController.filterPaths(filePaths)
allFilePaths.push(...allowedPaths)
}
// Filter by supported extensions, ignore patterns, and excluded directories
const supportedPaths = allowedPaths.filter((filePath) => {
const supportedPaths = allFilePaths.filter((filePath) => {
const ext = path.extname(filePath).toLowerCase()
const relativeFilePath = generateRelativeFilePath(filePath)
// Check if file is in an ignored directory using the shared helper
if (isPathInIgnoredDirectory(filePath)) {
return false
}
return scannerExtensions.includes(ext) && !this.ignoreInstance.ignores(relativeFilePath)
// Get the workspace root for this file
const workspaceRoot = getWorkspaceRootForFile(filePath)
if (!workspaceRoot) {
// File is outside all workspace roots
return false
}
// Get the relative path relative to its workspace root
const relativeFilePath = generateRelativeFilePath(filePath, workspaceRoot)
if (!relativeFilePath) {
// This shouldn't happen if getWorkspaceRootForFile worked correctly
return false
}
// Use the ignore instance for this workspace root
const ignoreInstance = this.workspaceIgnoreMap.get(workspaceRoot) || this.ignoreInstance
return scannerExtensions.includes(ext) && !ignoreInstance.ignores(relativeFilePath)
})
// Initialize tracking variables
@ -305,23 +338,39 @@ export class DirectoryScanner implements IDirectoryScanner {
const { embeddings } = await this.embedder.createEmbeddings(batchTexts)
// Prepare points for Qdrant
const points = batchBlocks.map((block, index) => {
const normalizedAbsolutePath = generateNormalizedAbsolutePath(block.file_path)
const points = batchBlocks
.map((block, index) => {
const normalizedAbsolutePath = generateNormalizedAbsolutePath(block.file_path)
const stableName = `${normalizedAbsolutePath}:${block.start_line}`
const pointId = uuidv5(stableName, QDRANT_CODE_BLOCK_NAMESPACE)
// Get the workspace root for this file
const workspaceRoot = getWorkspaceRootForFile(normalizedAbsolutePath)
const relativeFilePath = workspaceRoot
? generateRelativeFilePath(normalizedAbsolutePath, workspaceRoot)
: generateRelativeFilePath(normalizedAbsolutePath)
return {
id: pointId,
vector: embeddings[index],
payload: {
filePath: generateRelativeFilePath(normalizedAbsolutePath),
codeChunk: block.content,
startLine: block.start_line,
endLine: block.end_line,
},
}
})
if (!relativeFilePath) {
console.error(
`[DirectoryScanner] Failed to generate relative path for ${normalizedAbsolutePath}`,
)
// Skip this block
return null
}
const stableName = `${normalizedAbsolutePath}:${block.start_line}`
const pointId = uuidv5(stableName, QDRANT_CODE_BLOCK_NAMESPACE)
return {
id: pointId,
vector: embeddings[index],
payload: {
filePath: relativeFilePath,
codeChunk: block.content,
startLine: block.start_line,
endLine: block.end_line,
},
}
})
.filter((point) => point !== null) as any[]
// Upsert points to Qdrant
await this.qdrantClient.upsertPoints(points)

View file

@ -0,0 +1,94 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import * as path from "path"
import { generateNormalizedAbsolutePath, generateRelativeFilePath } from "../get-relative-path"
import * as workspaceUtils from "../workspace-utils"
// Mock dependencies
vi.mock("../../../utils/path", () => ({
getWorkspacePath: vi.fn(() => "/default/workspace"),
}))
vi.mock("../workspace-utils", () => ({
getWorkspaceRootForFile: vi.fn(),
}))
describe("get-relative-path", () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe("generateNormalizedAbsolutePath", () => {
it("should resolve relative paths to absolute paths", () => {
const result = generateNormalizedAbsolutePath("src/file.ts")
expect(result).toBe(path.normalize("/default/workspace/src/file.ts"))
})
it("should return normalized absolute paths unchanged", () => {
const absolutePath = "/some/absolute/path/file.ts"
const result = generateNormalizedAbsolutePath(absolutePath)
expect(result).toBe(path.normalize(absolutePath))
})
it("should use custom workspace root when provided", () => {
const result = generateNormalizedAbsolutePath("src/file.ts", "/custom/workspace")
expect(result).toBe(path.normalize("/custom/workspace/src/file.ts"))
})
it("should handle paths with . and .. segments", () => {
const result = generateNormalizedAbsolutePath("./src/../lib/file.ts")
expect(result).toBe(path.normalize("/default/workspace/lib/file.ts"))
})
})
describe("generateRelativeFilePath", () => {
it("should generate relative path when workspace root is provided", () => {
const absolutePath = "/custom/workspace/src/file.ts"
const result = generateRelativeFilePath(absolutePath, "/custom/workspace")
expect(result).toBe(path.normalize("src/file.ts"))
})
it("should return null for paths outside the provided workspace root", () => {
const absolutePath = "/other/location/file.ts"
const result = generateRelativeFilePath(absolutePath, "/custom/workspace")
expect(result).toBeNull()
})
it("should auto-detect workspace root in multi-root workspace", () => {
const absolutePath = "/workspace/project1/src/file.ts"
vi.mocked(workspaceUtils.getWorkspaceRootForFile).mockReturnValue("/workspace/project1")
const result = generateRelativeFilePath(absolutePath)
expect(result).toBe(path.normalize("src/file.ts"))
expect(workspaceUtils.getWorkspaceRootForFile).toHaveBeenCalledWith(absolutePath)
})
it("should return null when file is outside all workspace roots", () => {
const absolutePath = "/outside/all/workspaces/file.ts"
vi.mocked(workspaceUtils.getWorkspaceRootForFile).mockReturnValue(undefined)
const result = generateRelativeFilePath(absolutePath)
expect(result).toBeNull()
})
it("should handle workspace root as the file path", () => {
const workspaceRoot = "/workspace/project"
const result = generateRelativeFilePath(workspaceRoot, workspaceRoot)
expect(result).toBe(path.normalize("."))
})
it("should handle paths with .. when workspace root is provided", () => {
const absolutePath = "/workspace/../outside/file.ts"
const result = generateRelativeFilePath(absolutePath, "/workspace")
expect(result).toBeNull()
})
it("should prioritize provided workspace root over auto-detection", () => {
const absolutePath = "/workspace/project1/src/file.ts"
vi.mocked(workspaceUtils.getWorkspaceRootForFile).mockReturnValue("/workspace/project2")
const result = generateRelativeFilePath(absolutePath, "/workspace/project1")
expect(result).toBe(path.normalize("src/file.ts"))
expect(workspaceUtils.getWorkspaceRootForFile).not.toHaveBeenCalled()
})
})
})

View file

@ -0,0 +1,139 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import * as vscode from "vscode"
import {
getWorkspaceRootForFile,
isFileInWorkspace,
getAllWorkspaceRoots,
isMultiRootWorkspace,
} from "../workspace-utils"
// Mock vscode module
vi.mock("vscode", () => ({
workspace: {
workspaceFolders: undefined,
},
}))
describe("workspace-utils", () => {
beforeEach(() => {
// Reset workspace folders before each test
;(vscode.workspace as any).workspaceFolders = undefined
})
describe("getWorkspaceRootForFile", () => {
it("should return undefined when no workspace folders exist", () => {
const result = getWorkspaceRootForFile("/some/file/path.ts")
expect(result).toBeUndefined()
})
it("should return the correct workspace root for a file", () => {
;(vscode.workspace as any).workspaceFolders = [
{ uri: { fsPath: "/workspace/project1" } },
{ uri: { fsPath: "/workspace/project2" } },
]
const result = getWorkspaceRootForFile("/workspace/project1/src/file.ts")
expect(result).toBe("/workspace/project1")
})
it("should handle nested workspace folders correctly", () => {
;(vscode.workspace as any).workspaceFolders = [
{ uri: { fsPath: "/workspace/parent" } },
{ uri: { fsPath: "/workspace/parent/child" } },
]
// File in child workspace should return child workspace root
const result = getWorkspaceRootForFile("/workspace/parent/child/src/file.ts")
expect(result).toBe("/workspace/parent/child")
})
it("should return undefined for files outside all workspace roots", () => {
;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/workspace/project1" } }]
const result = getWorkspaceRootForFile("/other/location/file.ts")
expect(result).toBeUndefined()
})
it("should handle exact workspace root path", () => {
;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/workspace/project" } }]
const result = getWorkspaceRootForFile("/workspace/project")
expect(result).toBe("/workspace/project")
})
})
describe("isFileInWorkspace", () => {
it("should return true for file within workspace", () => {
const result = isFileInWorkspace("/workspace/project/src/file.ts", "/workspace/project")
expect(result).toBe(true)
})
it("should return false for file outside workspace", () => {
const result = isFileInWorkspace("/other/location/file.ts", "/workspace/project")
expect(result).toBe(false)
})
it("should return true for exact workspace root path", () => {
const result = isFileInWorkspace("/workspace/project", "/workspace/project")
expect(result).toBe(true)
})
it("should handle paths with different separators", () => {
const result = isFileInWorkspace("/workspace/project/src/file.ts", "/workspace/project/")
expect(result).toBe(true)
})
})
describe("getAllWorkspaceRoots", () => {
it("should return empty array when no workspace folders exist", () => {
const result = getAllWorkspaceRoots()
expect(result).toEqual([])
})
it("should return all workspace root paths", () => {
;(vscode.workspace as any).workspaceFolders = [
{ uri: { fsPath: "/workspace/project1" } },
{ uri: { fsPath: "/workspace/project2" } },
{ uri: { fsPath: "/workspace/project3" } },
]
const result = getAllWorkspaceRoots()
expect(result).toEqual(["/workspace/project1", "/workspace/project2", "/workspace/project3"])
})
it("should normalize paths", () => {
;(vscode.workspace as any).workspaceFolders = [
{ uri: { fsPath: "/workspace/project1/" } },
{ uri: { fsPath: "/workspace//project2" } },
]
const result = getAllWorkspaceRoots()
expect(result[0]).toBe("/workspace/project1")
expect(result[1]).toBe("/workspace/project2")
})
})
describe("isMultiRootWorkspace", () => {
it("should return false when no workspace folders exist", () => {
const result = isMultiRootWorkspace()
expect(result).toBe(false)
})
it("should return false for single workspace folder", () => {
;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/workspace/project" } }]
const result = isMultiRootWorkspace()
expect(result).toBe(false)
})
it("should return true for multiple workspace folders", () => {
;(vscode.workspace as any).workspaceFolders = [
{ uri: { fsPath: "/workspace/project1" } },
{ uri: { fsPath: "/workspace/project2" } },
]
const result = isMultiRootWorkspace()
expect(result).toBe(true)
})
})
})

View file

@ -1,18 +1,19 @@
import path from "path"
import { getWorkspacePath } from "../../../utils/path"
import { getWorkspaceRootForFile } from "./workspace-utils"
/**
* Generates a normalized absolute path from a given file path and workspace root.
* Handles path resolution and normalization to ensure consistent absolute paths.
*
* @param filePath - The file path to normalize (can be relative or absolute)
* @param workspaceRoot - The root directory of the workspace
* @param workspaceRoot - The root directory of the workspace (optional, defaults to primary workspace)
* @returns The normalized absolute path
*/
export function generateNormalizedAbsolutePath(filePath: string): string {
const workspaceRoot = getWorkspacePath()
export function generateNormalizedAbsolutePath(filePath: string, workspaceRoot?: string): string {
const root = workspaceRoot || getWorkspacePath()
// Resolve the path to make it absolute if it's relative
const resolvedPath = path.resolve(workspaceRoot, filePath)
const resolvedPath = path.resolve(root, filePath)
// Normalize to handle any . or .. segments and duplicate slashes
return path.normalize(resolvedPath)
}
@ -20,15 +21,35 @@ export function generateNormalizedAbsolutePath(filePath: string): string {
/**
* Generates a relative file path from a normalized absolute path and workspace root.
* Ensures consistent relative path generation across different platforms.
* In multi-root workspaces, automatically determines the correct workspace root.
*
* @param normalizedAbsolutePath - The normalized absolute path to convert
* @param workspaceRoot - The root directory of the workspace
* @returns The relative path from workspaceRoot to the file
* @param workspaceRoot - The root directory of the workspace (optional)
* @returns The relative path from workspaceRoot to the file, or null if file is outside all workspace roots
*/
export function generateRelativeFilePath(normalizedAbsolutePath: string): string {
const workspaceRoot = getWorkspacePath()
export function generateRelativeFilePath(normalizedAbsolutePath: string, workspaceRoot?: string): string | null {
// If workspace root is provided, use it
if (workspaceRoot) {
const relativePath = path.relative(workspaceRoot, normalizedAbsolutePath)
// Check if the path starts with ".." which means it's outside the workspace root
if (relativePath.startsWith("..")) {
return null
}
return path.normalize(relativePath)
}
// Otherwise, find the appropriate workspace root for this file
const fileWorkspaceRoot = getWorkspaceRootForFile(normalizedAbsolutePath)
if (!fileWorkspaceRoot) {
// File is outside all workspace roots
return null
}
// Generate the relative path
const relativePath = path.relative(workspaceRoot, normalizedAbsolutePath)
// Normalize to ensure consistent path separators
const relativePath = path.relative(fileWorkspaceRoot, normalizedAbsolutePath)
// This should never happen if getWorkspaceRootForFile worked correctly, but check anyway
if (relativePath.startsWith("..")) {
return null
}
return path.normalize(relativePath)
}

View file

@ -0,0 +1,68 @@
import * as vscode from "vscode"
import * as path from "path"
/**
* Returns the workspace root that contains the given file.
* @param filePath The absolute path to check
* @returns The workspace root path that contains the file, or undefined if not in any workspace
*/
export function getWorkspaceRootForFile(filePath: string): string | undefined {
const workspaceFolders = vscode.workspace.workspaceFolders
if (!workspaceFolders || workspaceFolders.length === 0) {
return undefined
}
// Normalize the file path for comparison
const normalizedFilePath = path.normalize(filePath)
// Find the workspace folder that contains this file
// Sort by path length descending to handle nested workspace folders correctly
const sortedFolders = [...workspaceFolders].sort((a, b) => b.uri.fsPath.length - a.uri.fsPath.length)
for (const folder of sortedFolders) {
const folderPath = path.normalize(folder.uri.fsPath)
if (normalizedFilePath.startsWith(folderPath + path.sep) || normalizedFilePath === folderPath) {
return folderPath
}
}
return undefined
}
/**
* Checks if a file is within a specific workspace root.
* @param filePath The absolute file path to check
* @param workspaceRoot The workspace root to check against
* @returns True if the file is within the workspace root
*/
export function isFileInWorkspace(filePath: string, workspaceRoot: string): boolean {
const normalizedFilePath = path.normalize(filePath)
const normalizedWorkspaceRoot = path.normalize(workspaceRoot)
return (
normalizedFilePath.startsWith(normalizedWorkspaceRoot + path.sep) ||
normalizedFilePath === normalizedWorkspaceRoot
)
}
/**
* Gets all workspace roots in the current VS Code workspace.
* @returns Array of workspace root paths
*/
export function getAllWorkspaceRoots(): string[] {
const workspaceFolders = vscode.workspace.workspaceFolders
if (!workspaceFolders) {
return []
}
return workspaceFolders.map((folder) => path.normalize(folder.uri.fsPath))
}
/**
* Determines if the current workspace is a multi-root workspace.
* @returns True if there are multiple workspace folders
*/
export function isMultiRootWorkspace(): boolean {
const workspaceFolders = vscode.workspace.workspaceFolders
return workspaceFolders ? workspaceFolders.length > 1 : false
}