Skip to content

Commit 2b2f737

Browse files
matelqclaude
andcommitted
Mono: Add editor integration for external assembly scripts
GodotSharpEditor.OpenInExternalEditor resolves csharp:// paths to real source files via ResolveScriptFilePath() before passing to external editors (VS, Rider, VsCode, MonoDevelop, custom). When source is unavailable (NuGet packages), a warning is shown instead of an error. InspectorPlugin skips timestamp checks for csharp:// scripts. For generic types over assembly-backed base scripts, File.Exists() determines whether to check timestamps. GodotSharpDirs adds dotnet/project/project_directory setting to support layouts where the .csproj is in a subdirectory of the project root. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 733910a commit 2b2f737

File tree

4 files changed

+49
-13
lines changed

4 files changed

+49
-13
lines changed

doc/classes/ProjectSettings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,9 @@
11151115
<member name="dotnet/project/assembly_reload_attempts" type="int" setter="" getter="" default="3">
11161116
Number of times to attempt assembly reloading after rebuilding .NET assemblies. Effectively also the timeout in seconds to wait for unloading of script assemblies to finish.
11171117
</member>
1118+
<member name="dotnet/project/project_directory" type="String" setter="" getter="" default="&quot;&quot;">
1119+
Directory containing the [code].csproj[/code] file, relative to [code]res://[/code]. By default, the [code].csproj[/code] is expected in the project root alongside [code]project.godot[/code]. Set this when using a multi-project layout where the main C# project is in a subdirectory (e.g. [code]MainProject[/code]).
1120+
</member>
11181121
<member name="dotnet/project/solution_directory" type="String" setter="" getter="" default="&quot;&quot;">
11191122
Directory that contains the [code].sln[/code] file. By default, the [code].sln[/code] files is in the root of the project directory, next to the [code]project.godot[/code] and [code].csproj[/code] files.
11201123
Changing this value allows setting up a multi-project scenario where there are multiple [code].csproj[/code]. Keep in mind that the Godot project is considered one of the C# projects in the workspace and it's root directory should contain the [code]project.godot[/code] and [code].csproj[/code] next to each other.

modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,29 @@ public void ShowConfirmCreateSlnDialog()
180180
"code", "code-oss", "vscode", "vscode-oss", "visual-studio-code", "visual-studio-code-oss", "codium"
181181
};
182182

183-
[UsedImplicitly]
183+
private static string? ResolveScriptFilePath(Script script)
184+
{
185+
string resourcePath = script.ResourcePath;
186+
if (resourcePath.StartsWith("csharp://", StringComparison.Ordinal))
187+
{
188+
string? sourceFile = Godot.Bridge.ScriptManagerBridge.GetSourceFilePath(resourcePath);
189+
if (sourceFile != null && File.Exists(sourceFile))
190+
return sourceFile;
191+
return null;
192+
}
193+
return ProjectSettings.GlobalizePath(resourcePath);
194+
}
195+
184196
public Error OpenInExternalEditor(Script script, int line, int col)
185197
{
198+
string? resolvedFilePath = ResolveScriptFilePath(script);
199+
if (resolvedFilePath == null)
200+
{
201+
GD.PushWarning($"Cannot open script: source file not available for '{script.ResourcePath}'. " +
202+
"This script is from a compiled assembly (e.g. NuGet package) with no source on disk.");
203+
return Error.Ok;
204+
}
205+
186206
var editorId = _editorSettings.GetSetting(Settings.ExternalEditor).As<ExternalEditorId>();
187207

188208
switch (editorId)
@@ -192,7 +212,7 @@ public Error OpenInExternalEditor(Script script, int line, int col)
192212
return Error.Unavailable;
193213
case ExternalEditorId.CustomEditor:
194214
{
195-
string file = ProjectSettings.GlobalizePath(script.ResourcePath);
215+
string file = resolvedFilePath;
196216
string project = ProjectSettings.GlobalizePath("res://");
197217
// Since ProjectSettings.GlobalizePath replaces only "res:/", leaving a trailing slash, it is removed here.
198218
project = project[..^1];
@@ -256,7 +276,7 @@ public Error OpenInExternalEditor(Script script, int line, int col)
256276
}
257277
case ExternalEditorId.VisualStudio:
258278
{
259-
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
279+
string scriptPath = resolvedFilePath;
260280

261281
var args = new List<string>
262282
{
@@ -288,13 +308,13 @@ public Error OpenInExternalEditor(Script script, int line, int col)
288308
case ExternalEditorId.Rider:
289309
case ExternalEditorId.Fleet:
290310
{
291-
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
311+
string scriptPath = resolvedFilePath;
292312
RiderPathManager.OpenFile(editorId, GodotSharpDirs.ProjectSlnPath, scriptPath, line + 1, col);
293313
return Error.Ok;
294314
}
295315
case ExternalEditorId.MonoDevelop:
296316
{
297-
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
317+
string scriptPath = resolvedFilePath;
298318

299319
GodotIdeManager.LaunchIdeAsync().ContinueWith(launchTask =>
300320
{
@@ -366,7 +386,7 @@ public Error OpenInExternalEditor(Script script, int line, int col)
366386

367387
args.Add(Path.GetDirectoryName(GodotSharpDirs.ProjectSlnPath)!);
368388

369-
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
389+
string scriptPath = resolvedFilePath;
370390

371391
if (line >= 0)
372392
{

modules/mono/editor/GodotTools/GodotTools/Inspector/InspectorPlugin.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,22 @@ public override void _ParseBegin(GodotObject godotObject)
4242
break;
4343
}
4444

45-
if (scriptPath.StartsWith("csharp://"))
45+
if (scriptPath.StartsWith("csharp://", StringComparison.Ordinal))
4646
{
47-
// This is a virtual path used by generic types, extract the real path.
4847
var scriptPathSpan = scriptPath.AsSpan("csharp://".Length);
49-
scriptPathSpan = scriptPathSpan[..scriptPathSpan.IndexOf(':')];
50-
scriptPath = $"res://{scriptPathSpan}";
48+
int colonIdx = scriptPathSpan.IndexOf(':');
49+
if (colonIdx >= 0)
50+
{
51+
string basePath = $"res://{scriptPathSpan[..colonIdx]}";
52+
string globalBasePath = ProjectSettings.GlobalizePath(basePath);
53+
if (!File.Exists(globalBasePath))
54+
continue;
55+
scriptPath = basePath;
56+
}
57+
else
58+
{
59+
continue;
60+
}
5161
}
5262

5363
if (File.GetLastWriteTime(scriptPath) > BuildManager.LastValidBuildDateTime)

modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,11 @@ public static void DetermineProjectLocation()
7979
else if (!slnParentDir.StartsWith("res://", StringComparison.Ordinal))
8080
slnParentDir = "res://" + slnParentDir;
8181

82-
// The csproj should be in the same folder as project.godot.
83-
string csprojParentDir = "res://";
82+
string? csprojParentDir = (string?)ProjectSettings.GetSetting("dotnet/project/project_directory");
83+
if (string.IsNullOrEmpty(csprojParentDir))
84+
csprojParentDir = "res://";
85+
else if (!csprojParentDir.StartsWith("res://", StringComparison.Ordinal))
86+
csprojParentDir = "res://" + csprojParentDir;
8487

8588
// Set csproj path first and use it to find the sln/slnx file with the assembly
8689
_projectCsProjPath = Path.Combine(ProjectSettings.GlobalizePath(csprojParentDir),
@@ -182,7 +185,7 @@ public static string ProjectBaseOutputPath
182185
{
183186
if (_projectCsProjPath == null)
184187
DetermineProjectLocation();
185-
return Path.Combine(Path.GetDirectoryName(_projectCsProjPath)!, ".godot", "mono", "temp", "bin");
188+
return Path.Combine(ProjectSettings.GlobalizePath("res://"), ".godot", "mono", "temp", "bin");
186189
}
187190
}
188191

0 commit comments

Comments
 (0)