Skip to content

Commit

Permalink
Indexing Update (#160)
Browse files Browse the repository at this point in the history
  • Loading branch information
saranshsaini authored Jan 30, 2025
1 parent ca4acb1 commit 1962aab
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 69 deletions.
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

0 comments on commit 1962aab

Please sign in to comment.