Skip to content
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
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3151.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3151
affected:
- src/CodeIndex/Cli/QueryCommandRunner.cs
- tests/CodeIndex.Tests/QueryCommandRunnerTests.cs
---

## English

- **CLI LSP locations now resolve indexed relative paths from the indexed project root (#3151)** - `--format lsp` now builds file URIs from the active indexed project root when indexed paths are relative, falling back to the current directory only when no indexed root is available.

## 日本語

- **CLI LSP location が indexed relative path を indexed project root から解決するようになりました (#3151)** - `--format lsp` は indexed path が相対パスの場合に active な indexed project root から file URI を構築し、indexed root が利用できない場合だけ current directory にフォールバックします。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3183.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3183
affected:
- src/CodeIndex/Mcp/McpToolHandlers.cs
- tests/CodeIndex.Tests/McpServerTests.cs
---

## English

- **MCP project filters now resolve against the indexed project root (#3183)** - MCP tools that accept `project` now expand solution project filters from the database's active indexed project root instead of the server process current directory.

## 日本語

- **MCP の project filter が indexed project root 基準で解決されるようになりました (#3183)** - `project` を受け取る MCP tool は server process の current directory ではなく、database の active indexed project root から solution project filter を展開します。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3189.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3189
affected:
- src/CodeIndex/Cli/QueryCommandRunner.cs
- tests/CodeIndex.Tests/QueryCommandRunnerTests.cs
---

## English

- **CLI project filters now resolve against the indexed project root (#3189)** - query commands with `--project` now expand solution project filters from the active indexed project root, so explicit `--db` queries launched from another current directory do not inspect the wrong workspace.

## 日本語

- **CLI の project filter が indexed project root 基準で解決されるようになりました (#3189)** - `--project` 付き query コマンドは active な indexed project root から solution project filter を展開するため、別の current directory から明示 `--db` query を実行しても誤った workspace を参照しません。
52 changes: 46 additions & 6 deletions src/CodeIndex/Cli/QueryCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ public static class QueryCommandRunner

[ThreadStatic]
private static DbReader? s_batchReader;
[ThreadStatic]
private static string? s_batchDbPath;
[ThreadStatic]
private static bool s_batchDbPathExplicit;
[ThreadStatic]
private static string? s_activeQueryProjectRoot;

private static DateTime GetUtcNow() => TimeProvider.GetUtcNow().UtcDateTime;

Expand Down Expand Up @@ -268,6 +274,7 @@ private sealed record StatusReadinessField(
public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions)
{
var dbPath = Path.Combine(".cdidx", "codeindex.db");
var dbPathExplicit = false;
for (var i = 0; i < cmdArgs.Length; i++)
{
var arg = cmdArgs[i];
Expand All @@ -279,6 +286,7 @@ public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions)
return CommandExitCodes.UsageError;
}
dbPath = cmdArgs[++i];
dbPathExplicit = true;
continue;
}

Expand All @@ -290,6 +298,7 @@ public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions)
Console.Error.WriteLine(BuildMissingOptionValueError("--db"));
return CommandExitCodes.UsageError;
}
dbPathExplicit = true;
continue;
}

Expand All @@ -314,6 +323,8 @@ public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions)

db.TryMigrateForRead();
s_batchReader = new DbReader(db);
s_batchDbPath = dbPath;
s_batchDbPathExplicit = dbPathExplicit;
var firstFailure = CommandExitCodes.Success;
var lineNumber = 0;
while (TryReadBatchLine(Console.In, out var line, out var lineExceededLimit))
Expand Down Expand Up @@ -347,6 +358,8 @@ public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions)
finally
{
s_batchReader = null;
s_batchDbPath = null;
s_batchDbPathExplicit = false;
}
}

Expand Down Expand Up @@ -1147,11 +1160,14 @@ public static void AttachLspLocations(IEnumerable<ReferenceResult> results)
}
}

public static LspLocation BuildLspLocation(string path, int startLine, int startColumn, int endLine, int endColumn)
public static LspLocation BuildLspLocation(string path, int startLine, int startColumn, int endLine, int endColumn, string? projectRoot = null)
{
var baseRoot = string.IsNullOrWhiteSpace(projectRoot)
? s_activeQueryProjectRoot ?? Environment.CurrentDirectory
: projectRoot;
var absolutePath = Path.IsPathFullyQualified(path)
? path
: Path.GetFullPath(path, Environment.CurrentDirectory);
: Path.GetFullPath(path, baseRoot);
return new LspLocation
{
Uri = new Uri(absolutePath).AbsoluteUri,
Expand Down Expand Up @@ -6736,11 +6752,15 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
}
}

var dbResolution = DbPathResolver.ResolveForQuery(Environment.CurrentDirectory, dbPath, dataDir);
var resolvedDbPath = dbResolution.DbPath;

if (parseErrors == null && projectFilters.Count > 0)
{
try
{
foreach (var glob in SolutionProjectResolver.ResolveProjectDirectoryGlobs(Environment.CurrentDirectory, projectFilters, solutionFilter))
var projectRoot = ResolveProjectFilterRoot(resolvedDbPath, dbPathExplicit);
foreach (var glob in SolutionProjectResolver.ResolveProjectDirectoryGlobs(projectRoot, projectFilters, solutionFilter))
pathPatterns.Add(glob);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException)
Expand All @@ -6760,8 +6780,6 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
if (validateDefaultMaxLineWidth && !maxLineWidthExplicit && defaultMaxLineWidthError != null)
AddParseError(defaultMaxLineWidthError);

var dbResolution = DbPathResolver.ResolveForQuery(Environment.CurrentDirectory, dbPath, dataDir);
var resolvedDbPath = dbResolution.DbPath;
if (readOnly)
{
var canAppendReadOnlyFlags = !SqliteFileUri.StartsWithFileScheme(resolvedDbPath) ||
Expand Down Expand Up @@ -6853,6 +6871,18 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
};
}

private static string ResolveProjectFilterRoot(string dbPath, bool dbPathExplicit)
{
var effectiveDbPath = s_batchReader != null && !string.IsNullOrWhiteSpace(s_batchDbPath)
? s_batchDbPath!
: dbPath;
var effectiveDbPathExplicit = s_batchReader != null && !string.IsNullOrWhiteSpace(s_batchDbPath)
? s_batchDbPathExplicit
: dbPathExplicit;
return DbPathResolver.ResolveProjectRootForQuery(effectiveDbPath, effectiveDbPathExplicit)
?? Environment.CurrentDirectory;
}

private static List<string> ParseMapSections(string rawValue, Action<string> addParseError)
{
var sections = new List<string>();
Expand Down Expand Up @@ -7416,7 +7446,17 @@ private static int WithDb(QueryCommandOptions options, JsonSerializerOptions jso
}

reader.IncludeGenerated = options.IncludeGenerated;
var exitCode = reader.RunWithGeneratedScope(() => action(reader));
var previousProjectRoot = s_activeQueryProjectRoot;
s_activeQueryProjectRoot = ResolveProjectFilterRoot(dbPath, options.DbPathExplicit);
int exitCode;
try
{
exitCode = reader.RunWithGeneratedScope(() => action(reader));
}
finally
{
s_activeQueryProjectRoot = previousProjectRoot;
}
var profileEntries = profiling ? Database.DbDebug.EndProfile() : [];
if (options.Profile)
WriteProfilePayload(profileEntries, jsonOptions);
Expand Down
13 changes: 9 additions & 4 deletions src/CodeIndex/Mcp/McpToolHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1014,20 +1014,21 @@ private JsonObject ToAnalyzeSymbolJsonObject(SymbolAnalysisResult analysis)
return string.IsNullOrWhiteSpace(value) ? null : new List<string> { value };
}

private static List<string>? ReadScopedPathList(JsonNode? args)
private List<string>? ReadScopedPathList(JsonNode? args)
{
var paths = ReadPathList(args, "path") ?? [];
var projects = ReadPathList(args, "project") ?? [];
if (projects.Count == 0)
return paths.Count == 0 ? null : paths;

var solution = args?["solution"]?.GetValue<string>();
foreach (var glob in SolutionProjectResolver.ResolveProjectDirectoryGlobs(Environment.CurrentDirectory, projects, solution))
var projectRoot = ResolveProjectFilterRoot();
foreach (var glob in SolutionProjectResolver.ResolveProjectDirectoryGlobs(projectRoot, projects, solution))
paths.Add(glob);
return paths.Count == 0 ? null : paths;
}

private static JsonObject? ValidateProjectFilterArguments(JsonNode? args)
private JsonObject? ValidateProjectFilterArguments(JsonNode? args)
{
var projects = ReadPathList(args, "project") ?? [];
if (projects.Count == 0)
Expand All @@ -1036,7 +1037,7 @@ private JsonObject ToAnalyzeSymbolJsonObject(SymbolAnalysisResult analysis)
var solution = args?["solution"]?.GetValue<string>();
try
{
_ = SolutionProjectResolver.ResolveProjectDirectoryGlobs(Environment.CurrentDirectory, projects, solution);
_ = SolutionProjectResolver.ResolveProjectDirectoryGlobs(ResolveProjectFilterRoot(), projects, solution);
return null;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException)
Expand All @@ -1053,6 +1054,10 @@ private JsonObject ToAnalyzeSymbolJsonObject(SymbolAnalysisResult analysis)
}
}

private string ResolveProjectFilterRoot()
=> DbPathResolver.ResolveProjectRootForQuery(_dbPath, _dbPathExplicit)
?? Environment.CurrentDirectory;

private static bool TryReadSinceArgument(JsonNode? args, out DateTime? since, out string? error)
{
var sinceStr = args?["since"]?.GetValue<string>();
Expand Down
42 changes: 42 additions & 0 deletions tests/CodeIndex.Tests/McpServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9999,6 +9999,48 @@ public void Call(ServiceB service)
}
}

[Fact]
public void ToolsCall_ProjectScopeUsesIndexedProjectRootWhenCurrentDirectoryDiffers_Issue3183()
{
var projectRoot = TestProjectHelper.CreateTempProject("cdidx_mcp_project_scope_indexed_root");
var otherRoot = TestProjectHelper.CreateTempProject("cdidx_mcp_project_scope_other_cwd");
var originalCurrentDirectory = Environment.CurrentDirectory;
try
{
Directory.CreateDirectory(Path.Combine(projectRoot, "src", "AppA"));
Directory.CreateDirectory(Path.Combine(projectRoot, "src", "AppB"));
File.WriteAllText(Path.Combine(projectRoot, "Repo.sln"), """
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppA", "src\AppA\AppA.csproj", "{11111111-1111-1111-1111-111111111111}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppB", "src\AppB\AppB.csproj", "{22222222-2222-2222-2222-222222222222}"
EndProject
""");
File.WriteAllText(Path.Combine(projectRoot, "src", "AppA", "AppA.csproj"), "<Project Sdk=\"Microsoft.NET.Sdk\" />");
File.WriteAllText(Path.Combine(projectRoot, "src", "AppB", "AppB.csproj"), "<Project Sdk=\"Microsoft.NET.Sdk\" />");

var dbPath = TestProjectHelper.CreateProjectDb(projectRoot);
TestProjectHelper.InsertIndexedFile(dbPath, "src/AppA/ServiceA.cs", "csharp", "public class ServiceA { }\n");
TestProjectHelper.InsertIndexedFile(dbPath, "src/AppB/ServiceB.cs", "csharp", "public class ServiceB { }\n");

Environment.CurrentDirectory = otherRoot;
using var server = new McpServer(dbPath, ConsoleUi.LoadVersion(), dbPathExplicit: true);
var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search","arguments":{"query":"Service","project":"AppA","exactSubstring":true}}}""")!;
var response = server.HandleMessage(request)!;

Assert.False(response["result"]!["isError"]?.GetValue<bool>() ?? false);
var results = response["result"]!["structuredContent"]!["results"]!.AsArray();
var result = Assert.Single(results);
Assert.Equal("src/AppA/ServiceA.cs", result!["path"]!.GetValue<string>());
}
finally
{
Environment.CurrentDirectory = originalCurrentDirectory;
TestProjectHelper.DeleteDirectory(otherRoot);
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void ToolsCall_Index_Rebuild_IgnoresUnreadableDirectoriesWhenCollectingMarkerFingerprints()
{
Expand Down
76 changes: 76 additions & 0 deletions tests/CodeIndex.Tests/QueryCommandRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,82 @@ public void ParseArgs_ProjectFilterExpandsSolutionProjectToPathGlob_Issue1707()
}
}

[Fact]
public void ParseArgs_ProjectFilterUsesIndexedProjectRootForExplicitDb_Issue3189()
{
var projectRoot = TestProjectHelper.CreateTempProject("cdidx_solution_filter_explicit_db");
var otherRoot = TestProjectHelper.CreateTempProject("cdidx_solution_filter_other_cwd");
var originalCurrentDirectory = Environment.CurrentDirectory;
try
{
Directory.CreateDirectory(Path.Combine(projectRoot, "src", "App"));
File.WriteAllText(Path.Combine(projectRoot, "CodeIndex.sln"), """
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "src\App\App.csproj", "{11111111-1111-1111-1111-111111111111}"
EndProject
""");
File.WriteAllText(Path.Combine(projectRoot, "src", "App", "App.csproj"), "<Project Sdk=\"Microsoft.NET.Sdk\" />");
var dbPath = TestProjectHelper.CreateProjectDb(projectRoot);

Environment.CurrentDirectory = otherRoot;
var options = QueryCommandRunner.ParseArgs(
["Auth", "--db", dbPath, "--project", "App"],
jsonDefault: false,
allowNamedQuery: true);

Assert.Equal("Auth", options.Query);
Assert.Equal(["App"], options.ProjectFilters);
Assert.Equal(["src/App/*"], options.PathPatterns);
Assert.Null(options.ParseError);
}
finally
{
Environment.CurrentDirectory = originalCurrentDirectory;
TestProjectHelper.DeleteDirectory(otherRoot);
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void RunDefinition_LspFormatUsesIndexedProjectRootForExplicitDb_Issue3151()
{
var projectRoot = TestProjectHelper.CreateTempProject("cdidx_lsp_explicit_db_root");
var otherRoot = TestProjectHelper.CreateTempProject("cdidx_lsp_other_cwd");
var originalCurrentDirectory = Environment.CurrentDirectory;
try
{
var dbPath = TestProjectHelper.CreateProjectDb(projectRoot);
TestProjectHelper.InsertIndexedFile(
dbPath,
"src/App/Service.cs",
"csharp",
"""
public class Service
{
public void Run() { }
}
""");

Environment.CurrentDirectory = otherRoot;
var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunDefinition(
["Service", "--db", dbPath, "--format", "lsp", "--exact-name", "--lang", "csharp", "--kind", "class"],
_jsonOptions));

Assert.Equal(CommandExitCodes.Success, exitCode);
Assert.Equal(string.Empty, stderr);
using var document = JsonDocument.Parse(stdout);
var location = Assert.Single(document.RootElement.EnumerateArray());
var expectedUri = new Uri(Path.Combine(projectRoot, "src", "App", "Service.cs")).AbsoluteUri;
Assert.Equal(expectedUri, location.GetProperty("uri").GetString());
}
finally
{
Environment.CurrentDirectory = originalCurrentDirectory;
TestProjectHelper.DeleteDirectory(otherRoot);
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Theory]
[InlineData("30m", 30 * 60)]
[InlineData("2h", 2 * 60 * 60)]
Expand Down
Loading