diff --git a/.config/dotnet/common.props b/.config/dotnet/common.props
index 25f664a9..292c2ba3 100644
--- a/.config/dotnet/common.props
+++ b/.config/dotnet/common.props
@@ -33,8 +33,16 @@
all
runtime; build; native; contentfiles; analyzers
-
-
+
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/src/editors/rider/src/main/resources/META-INF/plugin.xml b/src/editors/rider/src/main/resources/META-INF/plugin.xml
index a7971d06..1ede632d 100644
--- a/src/editors/rider/src/main/resources/META-INF/plugin.xml
+++ b/src/editors/rider/src/main/resources/META-INF/plugin.xml
@@ -16,8 +16,7 @@
-
+
@@ -25,7 +24,8 @@
id="Forge Solution"
anchor="left"
icon="/icons/forge.svg"
- factoryClass="com.forgelsp.rider.toolwindow.ForgeSolutionToolWindowFactory"/>
+ factoryClass="com.forgelsp.rider.toolwindow.ForgeSolutionToolWindowFactory"
+ />
@@ -33,23 +33,22 @@
id="Forge NuGet"
anchor="bottom"
icon="/icons/forge.svg"
- factoryClass="com.forgelsp.rider.toolwindow.nuget.ForgeNuGetToolWindowFactory"/>
+ factoryClass="com.forgelsp.rider.toolwindow.nuget.ForgeNuGetToolWindowFactory"
+ />
-
+
+ instance="com.forgelsp.rider.settings.ForgeSettingsConfigurable"
+ />
-
+
diff --git a/src/sharplsp/tests/fixtures/NuGetTest/NuGetTest.csproj b/src/sharplsp/tests/fixtures/NuGetTest/NuGetTest.csproj
index 9d246f81..7674d18b 100644
--- a/src/sharplsp/tests/fixtures/NuGetTest/NuGetTest.csproj
+++ b/src/sharplsp/tests/fixtures/NuGetTest/NuGetTest.csproj
@@ -6,6 +6,5 @@
-
-
+
diff --git a/src/sharplsp/tests/fixtures/ProfileTarget/Program.cs b/src/sharplsp/tests/fixtures/ProfileTarget/Program.cs
index f890a070..7204c68c 100644
--- a/src/sharplsp/tests/fixtures/ProfileTarget/Program.cs
+++ b/src/sharplsp/tests/fixtures/ProfileTarget/Program.cs
@@ -6,7 +6,11 @@
using System.Text.Json;
using var cts = new CancellationTokenSource();
-Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
+Console.CancelKeyPress += (_, e) =>
+{
+ e.Cancel = true;
+ cts.Cancel();
+};
// Self-terminate when orphaned: if the parent (the test host) dies abnormally —
// e.g. nextest SIGKILLs a timed-out test — Rust-side `Drop` cleanup never runs
@@ -27,7 +31,10 @@
Task.Run(() => StringBuilderAllocation(cts.Token), cts.Token),
};
-try { await Task.WhenAll(tasks).ConfigureAwait(false); }
+try
+{
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+}
catch (OperationCanceledException) { }
// Cancel `cts` as soon as this process is reparented away from its original
@@ -102,7 +109,9 @@ static void StartWindowsParentDeathWatchdog(CancellationTokenSource cts)
{
// Wait failure means we can no longer observe the ancestor;
// treat it as death so we never linger as an unwatched orphan.
- WatchdogLog($"wait on ancestor {watched.Id} failed ({ex.GetType().Name}) -> cancel");
+ WatchdogLog(
+ $"wait on ancestor {watched.Id} failed ({ex.GetType().Name}) -> cancel"
+ );
}
cts.Cancel();
@@ -220,19 +229,22 @@ static void LockContention(CancellationToken ct)
var queue = new Queue();
var locker = new object();
const int maxQueueDepth = 256;
- var producer = Task.Run(() =>
- {
- var i = 0;
- while (!ct.IsCancellationRequested)
+ var producer = Task.Run(
+ () =>
{
- lock (locker)
+ var i = 0;
+ while (!ct.IsCancellationRequested)
{
- if (queue.Count < maxQueueDepth)
- queue.Enqueue(i++);
+ lock (locker)
+ {
+ if (queue.Count < maxQueueDepth)
+ queue.Enqueue(i++);
+ }
+ Thread.SpinWait(128);
}
- Thread.SpinWait(128);
- }
- }, ct);
+ },
+ ct
+ );
while (!ct.IsCancellationRequested)
{
@@ -261,7 +273,8 @@ static void DeepCallStack(CancellationToken ct)
static int SumCharValues(string text)
{
var sum = 0;
- foreach (var c in text) sum += c;
+ foreach (var c in text)
+ sum += c;
return sum;
}
@@ -272,9 +285,7 @@ static void StringBuilderAllocation(CancellationToken ct)
while (!ct.IsCancellationRequested)
{
iteration++;
- _ = iteration % 2 == 0
- ? BuildWithStringBuilder(64)
- : BuildWithConcatenation(64);
+ _ = iteration % 2 == 0 ? BuildWithStringBuilder(64) : BuildWithConcatenation(64);
}
}
@@ -300,7 +311,8 @@ static string BuildLargeJsonPayload(int entries)
sb.Append('{');
for (var i = 0; i < entries; i++)
{
- if (i > 0) sb.Append(',');
+ if (i > 0)
+ sb.Append(',');
sb.Append(System.FormattableString.Invariant($"\"key{i}\":\"value{i}\""));
}
sb.Append('}');
@@ -318,14 +330,16 @@ internal static class NativeMethods
// (SYSLIB1062), not worth enabling for one getppid (suppressed in the csproj).
[System.Runtime.InteropServices.DllImport("libc")]
[System.Runtime.InteropServices.DefaultDllImportSearchPaths(
- System.Runtime.InteropServices.DllImportSearchPath.System32)]
+ System.Runtime.InteropServices.DllImportSearchPath.System32
+ )]
internal static extern int getppid();
// Windows has no getppid(2); the parent PID lives in
// PROCESS_BASIC_INFORMATION.InheritedFromUniqueProcessId, reachable only
// via NtQueryInformationProcess (info class 0 = ProcessBasicInformation).
[System.Runtime.InteropServices.StructLayout(
- System.Runtime.InteropServices.LayoutKind.Sequential)]
+ System.Runtime.InteropServices.LayoutKind.Sequential
+ )]
private struct ProcessBasicInformation
{
public IntPtr ExitStatus;
@@ -338,13 +352,15 @@ private struct ProcessBasicInformation
[System.Runtime.InteropServices.DllImport("ntdll.dll")]
[System.Runtime.InteropServices.DefaultDllImportSearchPaths(
- System.Runtime.InteropServices.DllImportSearchPath.System32)]
+ System.Runtime.InteropServices.DllImportSearchPath.System32
+ )]
private static extern int NtQueryInformationProcess(
IntPtr processHandle,
int processInformationClass,
ref ProcessBasicInformation processInformation,
int processInformationLength,
- out int returnLength);
+ out int returnLength
+ );
/// Creator (parent) PID of the process behind
/// on Windows, or -1 when it cannot be determined.
@@ -356,7 +372,8 @@ internal static int GetParentPid(IntPtr processHandle)
0,
ref info,
System.Runtime.InteropServices.Marshal.SizeOf(),
- out _);
+ out _
+ );
return status == 0 ? unchecked((int)info.InheritedFromUniqueProcessId.ToInt64()) : -1;
}
}
diff --git a/src/sidecars/SharpLsp.Sidecar.CSharp.Tests/WorkspaceManagerFeatureCoverageTests.cs b/src/sidecars/SharpLsp.Sidecar.CSharp.Tests/WorkspaceManagerFeatureCoverageTests.cs
index dfc53311..60752679 100644
--- a/src/sidecars/SharpLsp.Sidecar.CSharp.Tests/WorkspaceManagerFeatureCoverageTests.cs
+++ b/src/sidecars/SharpLsp.Sidecar.CSharp.Tests/WorkspaceManagerFeatureCoverageTests.cs
@@ -312,6 +312,37 @@ public async Task ResolveCompletion_after_completion_returns_resolve_result()
Assert.Empty(guarded.AdditionalEdits);
}
+ [Fact]
+ public async Task ResolveCompletion_with_empty_span_skips_primary_edit_in_additional_edits()
+ {
+ // GitHub double-insertion bug: triggering completion immediately after a dot
+ // produces a length-0 completion span. ResolveCompletion must use IntersectsWith
+ // to correctly skip the primary edit so it doesn't get double-applied.
+ using var manager = await OpenAsync();
+
+ // Add a dot to trigger completion on an empty span.
+ var newSource = Source.Replace("Total = result;", "Total = result.;");
+ await manager.UpdateDocumentTextAsync(_sourcePath, newSource);
+
+ // Position: line 19, char 23 (immediately after `result.`)
+ var completions = await manager.GetCompletionsAsync(_sourcePath, 19, 23);
+ var items = Unwrap(completions);
+
+ // Find a valid completion like "ToString"
+ var toString = items.Find(item => item.Label == "ToString");
+ Assert.NotNull(toString);
+
+ var resolved = await manager.ResolveCompletionAsync(
+ toString!.Index,
+ CancellationToken.None
+ );
+ Assert.NotNull(resolved);
+
+ // The primary edit (inserting "ToString") MUST be skipped. AdditionalEdits
+ // should be empty (or at least not contain "ToString" at the cursor).
+ Assert.DoesNotContain(resolved.AdditionalEdits, edit => edit.NewText == "ToString");
+ }
+
[Fact]
public async Task CodeLenses_report_reference_counts_for_members()
{
diff --git a/src/sidecars/SharpLsp.Sidecar.CSharp.Tests/WorkspaceManagerSingleFileTests.cs b/src/sidecars/SharpLsp.Sidecar.CSharp.Tests/WorkspaceManagerSingleFileTests.cs
index c0aa603b..720c9c28 100644
--- a/src/sidecars/SharpLsp.Sidecar.CSharp.Tests/WorkspaceManagerSingleFileTests.cs
+++ b/src/sidecars/SharpLsp.Sidecar.CSharp.Tests/WorkspaceManagerSingleFileTests.cs
@@ -392,7 +392,11 @@ public async Task FileBasedApp_closure_file_count_is_bounded()
}
var app = Write("Many.cs", "#:include many/*.cs\nConsole.WriteLine(1);\n");
- var closure = await DocumentClosure.ExpandFileBasedAsync(app, CancellationToken.None);
+ var closure = await DocumentClosure.ExpandFileBasedAsync(
+ app,
+ rootText: null,
+ CancellationToken.None
+ );
Assert.Equal(64, closure.Files.Count);
Assert.Contains(
@@ -415,7 +419,11 @@ public async Task FileBasedApp_include_depth_is_bounded()
}
var app = Write("Chain.cs", "#:include chain0.cs\nConsole.WriteLine(1);\n");
- var closure = await DocumentClosure.ExpandFileBasedAsync(app, CancellationToken.None);
+ var closure = await DocumentClosure.ExpandFileBasedAsync(
+ app,
+ rootText: null,
+ CancellationToken.None
+ );
Assert.Contains(
closure.Issues,
diff --git a/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/DocumentClosure.cs b/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/DocumentClosure.cs
index aaf38f8a..b0a8f252 100644
--- a/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/DocumentClosure.cs
+++ b/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/DocumentClosure.cs
@@ -6,8 +6,14 @@ namespace SharpLsp.Sidecar.CSharp.Workspace;
/// One source file in a file-based app or script closure.
internal sealed record ClosureFile(string Path, string Text, bool IsRoot);
+internal sealed record PackageRef(string Name, string Version);
+
/// Result of expanding a closure: the files, plus any non-fatal problems.
-internal sealed record Closure(IReadOnlyList Files, IReadOnlyList Issues);
+internal sealed record Closure(
+ IReadOnlyList Files,
+ IReadOnlyList Packages,
+ IReadOnlyList Issues
+);
///
/// Expands the compilation closure of a project-less document. Implements [SCRIPT-CLOSURE].
@@ -24,9 +30,13 @@ internal static class DocumentClosure
private const int MaxDepth = 8;
/// Expand a C# file-based app closure: root file plus transitive #:include.
- public static Task ExpandFileBasedAsync(string rootPath, CancellationToken ct)
+ public static Task ExpandFileBasedAsync(
+ string rootPath,
+ string? rootText = null,
+ CancellationToken ct = default
+ )
{
- return ExpandAsync(rootPath, IncludedPaths, ct);
+ return ExpandAsync(rootPath, rootText, IncludedPaths, ct);
}
///
@@ -34,13 +44,17 @@ public static Task ExpandFileBasedAsync(string rootPath, CancellationTo
/// compilation's SourceReferenceResolver; adding the loaded files as documents too
/// would compile them twice. Implements [SCRIPT-CSX-RESOLVERS].
///
- public static Task ExpandScriptAsync(string rootPath, CancellationToken ct)
+ public static Task ExpandScriptAsync(
+ string rootPath,
+ string? rootText = null,
+ CancellationToken ct = default
+ )
{
- return ExpandAsync(rootPath, NoChildren, ct);
+ return ExpandAsync(rootPath, rootText, NoChildren, ct);
}
private static IEnumerable NoChildren(
- string text,
+ IReadOnlyList directives,
string filePath,
ExpansionState state
)
@@ -50,23 +64,26 @@ ExpansionState state
private static async Task ExpandAsync(
string rootPath,
+ string? rootText,
ChildResolver children,
CancellationToken ct
)
{
var state = new ExpansionState(children);
- await VisitAsync(rootPath, isRoot: true, depth: 0, state, ct).ConfigureAwait(false);
- return new Closure(state.Files, state.Issues);
+ await VisitAsync(rootPath, rootText, isRoot: true, depth: 0, state, ct)
+ .ConfigureAwait(false);
+ return new Closure(state.Files, state.Packages, state.Issues);
}
private delegate IEnumerable ChildResolver(
- string text,
+ IReadOnlyList directives,
string filePath,
ExpansionState state
);
private static async Task VisitAsync(
string path,
+ string? textOverride,
bool isRoot,
int depth,
ExpansionState state,
@@ -80,7 +97,11 @@ CancellationToken ct
return;
}
- var read = await ReadAsync(full, ct).ConfigureAwait(false);
+ var read =
+ isRoot && textOverride != null
+ ? textOverride
+ : await ReadAsync(full, ct).ConfigureAwait(false);
+
if (read is null)
{
state.Issues.Add($"Could not read '{full}'; it was excluded from the closure.");
@@ -88,9 +109,32 @@ CancellationToken ct
}
state.Files.Add(new ClosureFile(full, read, isRoot));
- foreach (var child in state.Children(read, full, state))
+
+ var tree = CSharpSyntaxTree.ParseText(
+ read,
+ FileBasedParseOptions,
+ path: full,
+ cancellationToken: ct
+ );
+ var root = await tree.GetRootAsync(ct).ConfigureAwait(false);
+ var directives = FileLevelDirectives.Parse(root);
+
+ foreach (var directive in directives)
+ {
+ if (
+ directive.Kind == FileDirectiveKind.Package
+ && !string.IsNullOrEmpty(directive.Name)
+ && !string.IsNullOrEmpty(directive.Value)
+ )
+ {
+ state.Packages.Add(new PackageRef(directive.Name, directive.Value));
+ }
+ }
+
+ foreach (var child in state.Children(directives, full, state))
{
- await VisitAsync(child, isRoot: false, depth + 1, state, ct).ConfigureAwait(false);
+ await VisitAsync(child, textOverride: null, isRoot: false, depth + 1, state, ct)
+ .ConfigureAwait(false);
}
}
@@ -117,13 +161,11 @@ private static void RecordBound(string full, int depth, ExpansionState state)
).WithFeatures([new KeyValuePair("FileBasedProgram", "true")]);
private static IEnumerable IncludedPaths(
- string text,
+ IReadOnlyList directives,
string filePath,
ExpansionState state
)
{
- var tree = CSharpSyntaxTree.ParseText(text, FileBasedParseOptions, path: filePath);
- var directives = FileLevelDirectives.Parse(tree.GetRoot());
var baseDir = Path.GetDirectoryName(filePath) ?? ".";
return directives
.Where(d => d.Kind == FileDirectiveKind.Include)
@@ -191,6 +233,7 @@ private sealed class ExpansionState(ChildResolver children)
public ChildResolver Children { get; } = children;
public HashSet Visited { get; } = new(StringComparer.OrdinalIgnoreCase);
public List Files { get; } = [];
+ public List Packages { get; } = [];
public List Issues { get; } = [];
}
}
diff --git a/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/WorkspaceManager.Helpers.cs b/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/WorkspaceManager.Helpers.cs
index 6394d1a1..8c103c66 100644
--- a/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/WorkspaceManager.Helpers.cs
+++ b/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/WorkspaceManager.Helpers.cs
@@ -186,7 +186,7 @@ _lastCompletionList is null
foreach (var textChange in change.TextChanges)
{
- if (textChange.Span.OverlapsWith(completionSpan))
+ if (textChange.Span.IntersectsWith(completionSpan))
{
continue;
}
diff --git a/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/WorkspaceManager.SingleFile.cs b/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/WorkspaceManager.SingleFile.cs
index 7e6be6f9..fbc8909c 100644
--- a/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/WorkspaceManager.SingleFile.cs
+++ b/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/WorkspaceManager.SingleFile.cs
@@ -94,8 +94,8 @@ CancellationToken ct
)
{
return kind == ProjectlessKind.Script
- ? DocumentClosure.ExpandScriptAsync(path, ct)
- : DocumentClosure.ExpandFileBasedAsync(path, ct);
+ ? DocumentClosure.ExpandScriptAsync(path, rootText: null, ct)
+ : DocumentClosure.ExpandFileBasedAsync(path, rootText: null, ct);
}
private async Task LoadClosureAsync(
@@ -107,10 +107,25 @@ CancellationToken ct
{
_adhocWorkspace ??= new AdhocWorkspace();
- var project = _adhocWorkspace.AddProject(BuildProjectInfo(kind, rootPath));
+ var packageReferences = await ResolvePackagesAsync(closure.Packages, ct)
+ .ConfigureAwait(false);
+ var project = _adhocWorkspace.AddProject(
+ BuildProjectInfo(kind, rootPath, packageReferences)
+ );
+ DocumentId? rootDocumentId = null;
foreach (var file in closure.Files)
{
- _ = _adhocWorkspace.AddDocument(BuildDocumentInfo(project.Id, file, kind));
+ var docInfo = BuildDocumentInfo(project.Id, file, kind);
+ if (file.IsRoot)
+ {
+ rootDocumentId = docInfo.Id;
+ }
+ _ = _adhocWorkspace.AddDocument(docInfo);
+ }
+
+ if (rootDocumentId != null)
+ {
+ _documentPackages[rootDocumentId] = closure.Packages;
}
if (kind == ProjectlessKind.FileBasedApp)
@@ -133,7 +148,158 @@ CancellationToken ct
return new VoidResult.Ok(Unit.Value);
}
- private static ProjectInfo BuildProjectInfo(ProjectlessKind kind, string rootPath)
+ internal async Task UpdateProjectlessClosureAsync(
+ Document document,
+ string newText,
+ CancellationToken ct
+ )
+ {
+ if (_solution is null)
+ {
+ return VoidResult.Failure("No active solution.");
+ }
+
+ var kind = Classify(document.FilePath!);
+ var closure =
+ kind == ProjectlessKind.Script
+ ? await DocumentClosure
+ .ExpandScriptAsync(document.FilePath!, newText, ct)
+ .ConfigureAwait(false)
+ : await DocumentClosure
+ .ExpandFileBasedAsync(document.FilePath!, newText, ct)
+ .ConfigureAwait(false);
+
+ var currentProject = _solution.GetProject(document.Project.Id);
+ if (currentProject == null)
+ {
+ return VoidResult.Failure("Project not found.");
+ }
+
+ var currentDocIds = currentProject.Documents.ToDictionary(d => d.FilePath!, d => d.Id);
+ var closurePaths = closure.Files.Select(f => f.Path).ToHashSet();
+
+ var nextSolution = _solution;
+
+ // Add new documents
+ foreach (var file in closure.Files)
+ {
+ if (!currentDocIds.ContainsKey(file.Path))
+ {
+ var docInfo = BuildDocumentInfo(currentProject.Id, file, kind);
+ nextSolution = nextSolution.AddDocument(docInfo);
+ }
+ }
+
+ // Remove orphaned documents (except the root document)
+ foreach (var (path, docId) in currentDocIds)
+ {
+ if (path.EndsWith(GlobalUsingsFileName, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (!closurePaths.Contains(path) && path != document.FilePath)
+ {
+ nextSolution = nextSolution.RemoveDocument(docId);
+ }
+ }
+
+ // Handle package references
+ if (
+ _documentPackages.TryGetValue(document.Id, out var oldPackages)
+ && !closure.Packages.SequenceEqual(oldPackages)
+ )
+ {
+ var newReferences = await ResolvePackagesAsync(closure.Packages, ct)
+ .ConfigureAwait(false);
+ var updatedProject = nextSolution
+ .GetProject(currentProject.Id)!
+ .WithMetadataReferences(
+ Basic.Reference.Assemblies.Net100.References.All.Concat(newReferences)
+ );
+ nextSolution = updatedProject.Solution;
+ _documentPackages[document.Id] = closure.Packages;
+ }
+
+ _solution = nextSolution;
+ return new VoidResult.Ok(Unit.Value);
+ }
+
+ private static async Task> ResolvePackagesAsync(
+ IReadOnlyList packages,
+ CancellationToken ct
+ )
+ {
+ if (packages.Count == 0)
+ {
+ return [];
+ }
+
+ var tempDir = Path.Combine(
+ Path.GetTempPath(),
+ "SharpLsp_Packages_" + Guid.NewGuid().ToString("N")
+ );
+ _ = Directory.CreateDirectory(tempDir);
+ try
+ {
+ var projPath = Path.Combine(tempDir, "restore.csproj");
+ var packageItems = string.Join(
+ "\n",
+ packages.Select(p =>
+ $""
+ )
+ );
+ var xml =
+ $@"
+
+ net10.0
+
+
+ {packageItems}
+
+";
+ await File.WriteAllTextAsync(projPath, xml, ct).ConfigureAwait(false);
+
+ var psi = new System.Diagnostics.ProcessStartInfo("dotnet", "restore --verbosity quiet")
+ {
+ WorkingDirectory = tempDir,
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ };
+ using var process = System.Diagnostics.Process.Start(psi);
+ if (process != null)
+ {
+ await process.WaitForExitAsync(ct).ConfigureAwait(false);
+ }
+
+ using var workspace = Microsoft.CodeAnalysis.MSBuild.MSBuildWorkspace.Create(
+ new Dictionary
+ {
+ ["DesignTimeBuild"] = "true",
+ ["BuildingInsideVisualStudio"] = "true",
+ ["SkipCompilerExecution"] = "true",
+ }
+ );
+ var project = await workspace
+ .OpenProjectAsync(projPath, cancellationToken: ct)
+ .ConfigureAwait(false);
+ return project.MetadataReferences.OfType();
+ }
+ finally
+ {
+ try
+ {
+ Directory.Delete(tempDir, true);
+ }
+ catch { }
+ }
+ }
+
+ private static ProjectInfo BuildProjectInfo(
+ ProjectlessKind kind,
+ string rootPath,
+ IEnumerable extraReferences
+ )
{
var name = Path.GetFileNameWithoutExtension(rootPath);
var isScript = kind == ProjectlessKind.Script;
@@ -146,9 +312,11 @@ private static ProjectInfo BuildProjectInfo(ProjectlessKind kind, string rootPat
filePath: rootPath,
compilationOptions: BuildCompilationOptions(isScript, rootPath),
parseOptions: BuildParseOptions(isScript),
- // Tier 2 reference resolution: in-memory BCL only. `#:package` symbols do not bind
- // until the synthesized-project path lands. [SCRIPT-FILEBASED-REFERENCES-FALLBACK]
- metadataReferences: Basic.Reference.Assemblies.Net100.References.All
+ // Tier 2 reference resolution: in-memory BCL only. `#:package` symbols bind
+ // via MSBuildWorkspace synthetic evaluation fallback. [SCRIPT-FILEBASED-REFERENCES-FALLBACK]
+ metadataReferences: Basic.Reference.Assemblies.Net100.References.All.Concat(
+ extraReferences
+ )
);
}
diff --git a/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/WorkspaceManager.cs b/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/WorkspaceManager.cs
index 4e31e9ca..5adce3db 100644
--- a/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/WorkspaceManager.cs
+++ b/src/sidecars/SharpLsp.Sidecar.CSharp/Workspace/WorkspaceManager.cs
@@ -76,6 +76,11 @@ public void Dispose()
StringComparer.OrdinalIgnoreCase
);
+ private readonly System.Collections.Generic.Dictionary<
+ Microsoft.CodeAnalysis.DocumentId,
+ System.Collections.Generic.IReadOnlyList
+ > _documentPackages = new();
+
public bool IsLoaded => _solution is not null;
/// Open a solution or project file via MSBuildWorkspace.
@@ -147,6 +152,18 @@ public async Task UpdateDocumentTextAsync(
}
_solution = _solution.WithDocumentText(document.Id, SourceText.From(newText));
+
+ // Auto-update the closure and packages if they changed during a live edit
+ if (document.Project.Solution.Workspace is AdhocWorkspace)
+ {
+ var updateResult = await UpdateProjectlessClosureAsync(document, newText, ct)
+ .ConfigureAwait(false);
+ if (updateResult.IsError)
+ {
+ return updateResult;
+ }
+ }
+
return new VoidResult.Ok(Unit.Value);
}
finally