Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Indexing Update #160

Merged
merged 4 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 52 additions & 67 deletions CodeiumVS/LanguageServer/LanguageServer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CodeiumVS.Packets;
using Community.VisualStudio.Toolkit;
using EnvDTE;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Imaging;
Expand All @@ -21,6 +22,7 @@
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Xml.Linq;

namespace CodeiumVS;
Expand Down Expand Up @@ -745,17 +747,24 @@ private async Task InitializeTrackedWorkspaceAsync()
await _package.LogAsync($"Number of top-level projects: {dte.Solution.Projects.Count}");

var documents = dte.Documents;
var openFilePaths = new HashSet<string>();
var openFileProjects = new HashSet<EnvDTE.Project>();
if (_package.SettingsPage.IndexOpenFiles)
{
foreach (EnvDTE.Document doc in documents)
{
await _package.LogAsync($"Open File: {doc.Path}");
openFilePaths.Add(doc.Path);
ProjectItem projectItem = doc.ProjectItem;
if (projectItem != null)
{
EnvDTE.Project project = projectItem.ContainingProject;
if (project != null && !openFileProjects.Contains(project))
{
openFileProjects.Add(project);
}
}
}
}

var inputFilesToIndex = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var inputDirectoriesToIndex = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
string projectListPath = _package.SettingsPage.IndexingFilesListPath.Trim();
try
{
Expand All @@ -770,21 +779,21 @@ private async Task InitializeTrackedWorkspaceAsync()
{
if (Path.IsPathRooted(trimmedLine))
{
inputFilesToIndex.Add(trimmedLine);
inputDirectoriesToIndex.Add(trimmedLine);
}
}
}
await _package.LogAsync($"Loaded from {inputFilesToIndex.Count} files");
await _package.LogAsync($"Loaded from {inputDirectoriesToIndex.Count} directories");
}
}
catch (Exception ex)
{
await _package.LogAsync($"Error reading project list: {ex.Message}");
}

List<string> projectsToIndex = new List<string>(inputFilesToIndex);
List<string> projectsToIndex = new List<string>(inputDirectoriesToIndex);
int maxToIndex = 10;
projectsToIndex.AddRange(await GetFilesToIndex(inputFilesToIndex, openFilePaths, maxToIndex - projectsToIndex.Count, dte));
projectsToIndex.AddRange(await GetDirectoriesToIndex(inputDirectoriesToIndex, openFileProjects, maxToIndex - projectsToIndex.Count, dte));
await _package.LogAsync($"Number of projects to index: {projectsToIndex.Count}");

for (int i = 0; i < Math.Min(maxToIndex, projectsToIndex.Count); i++)
Expand All @@ -805,28 +814,24 @@ private async Task InitializeTrackedWorkspaceAsync()
}
}

private async Task<List<string>> GetFilesToIndex(HashSet<string> processedProjects, HashSet<string> openFilePaths, int remainingToFind, DTE dte)
private async Task<List<string>> GetDirectoriesToIndex(HashSet<string> processedProjects, HashSet<EnvDTE.Project> openFileProjects, int remainingToFind, DTE dte)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
HashSet<string> openFilesProjectsToIndexPath = new HashSet<string>();
HashSet<string> remainingProjectsToIndexPath = new HashSet<string>();
// Safeguard against any edge case
int maxRecursiveCalls = 25;
async Task AddFilesToIndexLists(EnvDTE.Project project)
{
maxRecursiveCalls--;
if (remainingToFind <= 0 || (openFilePaths.Count == 0 && remainingProjectsToIndexPath.Count >= remainingToFind) || maxRecursiveCalls == 0)
if (remainingToFind <= 0)
{
return;
}
string projectFullName = project.FullName;
string projectName = Path.GetFileNameWithoutExtension(projectFullName);
await _package.LogAsync($"Adding files to index of project: {projectFullName}");
if (!string.IsNullOrEmpty(projectFullName) && !processedProjects.Any(p => projectFullName.StartsWith(p)))
{
string projectName = Path.GetFileNameWithoutExtension(projectFullName);
IEnumerable<string> commonDirs = Enumerable.Empty<string>();
string projectDir = Path.GetDirectoryName(projectFullName);
string projectCommonRoot = projectDir;

// Parse the csproj file to find all source directories
// Parse the proj file to find all source directories
if (File.Exists(projectFullName) && (projectFullName.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) || projectFullName.EndsWith(".vcxproj", StringComparison.OrdinalIgnoreCase)))
{
try
Expand Down Expand Up @@ -857,59 +862,22 @@ async Task AddFilesToIndexLists(EnvDTE.Project project)
fullPaths.Add(fullPath);
}

if (fullPaths.Count > 0)
{
// Find the common root directory
string commonRoot = Path.GetDirectoryName(fullPaths[0]);
foreach (var path in fullPaths.Skip(1))
{
string directory = Path.GetDirectoryName(path);
while (!directory.StartsWith(commonRoot, StringComparison.OrdinalIgnoreCase) && commonRoot.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Where(s => !string.IsNullOrWhiteSpace(s)).Count() > 4)
{
commonRoot = Path.GetDirectoryName(commonRoot);
}
}

if (Directory.Exists(commonRoot))
{
await _package.LogAsync($"Common root directory: {commonRoot}");
projectCommonRoot = commonRoot;
}
}
commonDirs = Utilities.FileUtilities.FindMinimumEncompassingDirectories(fullPaths);
}
catch (Exception ex)
{
await _package.LogAsync($"Failed to parse project file {projectFullName}: {ex.Message}");
}
}

if (openFilePaths.Count != 0)
{
List<string> matchingFiles = new List<string>();
foreach (var filePath in openFilePaths)
{
if (filePath.StartsWith(projectCommonRoot, StringComparison.OrdinalIgnoreCase))
{
await _package.LogAsync($"Found in open files {filePath}");
matchingFiles.Add(filePath);
}
}
if (matchingFiles.Count > 0)
{
openFilesProjectsToIndexPath.Add(projectCommonRoot);
remainingToFind--;
foreach (var file in matchingFiles)
{
openFilePaths.Remove(file);
}
}
}
else
await _package.LogAsync($"Found set-covering directories for {projectName}: {commonDirs.Count()}");
foreach (var dir in commonDirs)
{
await _package.LogAsync($"Found in remaining {projectCommonRoot}");
remainingProjectsToIndexPath.Add(projectCommonRoot);
remainingToFind -= 1;
remainingProjectsToIndexPath.Add(dir);
}
processedProjects.Add(projectFullName);

processedProjects.Add(project.Name);
}

foreach (EnvDTE.ProjectItem item in project.ProjectItems)
Expand All @@ -929,22 +897,39 @@ async Task AddFilesToIndexLists(EnvDTE.Project project)
}
}

foreach (EnvDTE.Project project in openFileProjects)
{
try
{
await AddFilesToIndexLists(project);
}
catch (Exception ex)
{
await _package.LogAsync($"Failed to process open project: {ex.Message}");
continue;
}
}
foreach (EnvDTE.Project project in dte.Solution.Projects)
{
if (openFileProjects.Contains(project))
{
continue;
}
try
{
await AddFilesToIndexLists(project);
}
catch (Exception ex)
{
await _package.LogAsync($"Failed to process project: {ex.Message}");
await _package.LogAsync($"Failed to process remaining project: {ex.Message}");
continue;
}
if (remainingToFind <=0)
{
break;
}
}
List<string> result = new List<string>();
result.AddRange(openFilesProjectsToIndexPath);
result.AddRange(remainingProjectsToIndexPath);
return result;
return remainingProjectsToIndexPath.ToList();
}


Expand Down
103 changes: 102 additions & 1 deletion CodeiumVS/Utilities/FileUtilities.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace CodeiumVS.Utilities;

Expand Down Expand Up @@ -44,4 +47,102 @@ internal static void DeleteSafe(string path)
}
}
}

/// <summary>
/// Finds the minimum set of directories that encompass all the given files.
/// For example, given ["E:/a/b/c.txt", "E:/a/b/d/e.cpp"], returns ["E:/a/b"]
/// </summary>
/// <param name="filePaths">List of absolute file paths</param>
/// <returns>List of directory paths that collectively contain all input files with minimum redundancy</returns>
internal static List<string> FindMinimumEncompassingDirectories(IEnumerable<string> filePaths)
{
if (filePaths == null || !filePaths.Any())
return new List<string>();
// Get the directory paths of the file paths
var directoryPaths = filePaths.Select(Path.GetDirectoryName).Distinct().ToList();
CodeiumVSPackage.Instance?.Log($"Directories before minimization: {string.Join(", ", directoryPaths)}");
var result = GetMinimumDirectoryCover(directoryPaths);
CodeiumVSPackage.Instance?.Log($"Directories after minimization: {string.Join(", ", result)}");
return result.Where(dir => CountPathSegments(dir) > 1).ToList();
}


public static List<string> GetMinimumDirectoryCover(IEnumerable<string> directories)
{
// 1. Normalize all paths to full/absolute paths and remove duplicates
var normalizedDirs = directories
.Select(d => NormalizePath(d))
.Distinct()
.ToList();

// 2. Sort by ascending number of path segments (shallow first)
normalizedDirs.Sort((a, b) =>
CountPathSegments(a).CompareTo(CountPathSegments(b)));

var coverSet = new List<string>();

// 3. Greedy selection
foreach (var dir in normalizedDirs)
{
bool isCovered = false;

// Check if 'dir' is already covered by any directory in coverSet
foreach (var coverDir in coverSet)
{
if (IsSubdirectoryOrSame(coverDir, dir))
{
isCovered = true;
break;
}
}

// If not covered, add it to the cover set
if (!isCovered)
{
coverSet.Add(dir);
}
}

return coverSet;
}

/// <summary>
/// Checks if 'child' is the same or a subdirectory of 'parent'.
/// </summary>
private static bool IsSubdirectoryOrSame(string parent, string child)
{
// 1. Normalize both directories to their full path (remove extra slashes, etc.).
string parentFull = Path.GetFullPath(parent)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
string childFull = Path.GetFullPath(child)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);

// 2. Append a directory separator at the end of each path to ensure
// that "C:\Folder" won’t incorrectly match "C:\Folder2".
// e.g. "C:\Folder" -> "C:\Folder\"
parentFull += Path.DirectorySeparatorChar;
childFull += Path.DirectorySeparatorChar;

// 3. On Windows, paths are case-insensitive. Use OrdinalIgnoreCase
// to compare. On non-Windows systems, consider using Ordinal.
return childFull.StartsWith(parentFull, StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Normalize a directory path by getting its full path (removing trailing slash, etc).
/// </summary>
private static string NormalizePath(string path)
{
return Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}

/// <summary>
/// Count path segments based on splitting by directory separators.
/// E.g. "C:\Folder\Sub" -> 3 segments (on Windows).
/// </summary>
private static int CountPathSegments(string path)
{
return path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
.Count(segment => !string.IsNullOrEmpty(segment));
}
}
2 changes: 1 addition & 1 deletion CodeiumVS/source.extension.vsixmanifest
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
<Metadata>
<Identity Id="Codeium.VisualStudio" Version="1.8.98" Language="en-US" Publisher="Codeium" />
<Identity Id="Codeium.VisualStudio" Version="1.8.99" Language="en-US" Publisher="Codeium" />
<DisplayName>Codeium</DisplayName>
<Description xml:space="preserve">The modern coding superpower: free AI code acceleration plugin for your favorite languages. Type less. Code more. Ship faster.</Description>
<MoreInfo>https://www.codeium.com</MoreInfo>
Expand Down