commit 9c0de9611bb0204ddfda84b04431a7dc52e6ab87 Author: Ignacio Etcheverry Date: Sat Jun 13 13:40:06 2020 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e59da71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,711 @@ +# Rider +.idea/ + +# Visual Studio Code +.vscode/ + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Rider +.idea/ + +# Visual Studio Code +.vscode/ + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ diff --git a/GodotAddinVS.sln b/GodotAddinVS.sln new file mode 100644 index 0000000..0bcfa8f --- /dev/null +++ b/GodotAddinVS.sln @@ -0,0 +1,51 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29806.167 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotAddinVS", "GodotAddinVS\GodotAddinVS.csproj", "{7D64803D-2F8D-4597-9762-A316C74E9816}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotCompletionProviders", "GodotCompletionProviders\GodotCompletionProviders.csproj", "{A9EA6427-C5E2-4207-BBBF-A1F44A361339}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotCompletionProviders.Test", "GodotCompletionProviders.Test\GodotCompletionProviders.Test.csproj", "{B2BAAEA3-8B1D-4584-A5D1-9D4EF487E0DF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + net_4_5_Debug|Any CPU = net_4_5_Debug|Any CPU + net_4_5_Release|Any CPU = net_4_5_Release|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7D64803D-2F8D-4597-9762-A316C74E9816}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D64803D-2F8D-4597-9762-A316C74E9816}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D64803D-2F8D-4597-9762-A316C74E9816}.net_4_5_Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D64803D-2F8D-4597-9762-A316C74E9816}.net_4_5_Debug|Any CPU.Build.0 = Debug|Any CPU + {7D64803D-2F8D-4597-9762-A316C74E9816}.net_4_5_Release|Any CPU.ActiveCfg = Release|Any CPU + {7D64803D-2F8D-4597-9762-A316C74E9816}.net_4_5_Release|Any CPU.Build.0 = Release|Any CPU + {7D64803D-2F8D-4597-9762-A316C74E9816}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D64803D-2F8D-4597-9762-A316C74E9816}.Release|Any CPU.Build.0 = Release|Any CPU + {A9EA6427-C5E2-4207-BBBF-A1F44A361339}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9EA6427-C5E2-4207-BBBF-A1F44A361339}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9EA6427-C5E2-4207-BBBF-A1F44A361339}.net_4_5_Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9EA6427-C5E2-4207-BBBF-A1F44A361339}.net_4_5_Debug|Any CPU.Build.0 = Debug|Any CPU + {A9EA6427-C5E2-4207-BBBF-A1F44A361339}.net_4_5_Release|Any CPU.ActiveCfg = Debug|Any CPU + {A9EA6427-C5E2-4207-BBBF-A1F44A361339}.net_4_5_Release|Any CPU.Build.0 = Debug|Any CPU + {A9EA6427-C5E2-4207-BBBF-A1F44A361339}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9EA6427-C5E2-4207-BBBF-A1F44A361339}.Release|Any CPU.Build.0 = Release|Any CPU + {B2BAAEA3-8B1D-4584-A5D1-9D4EF487E0DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2BAAEA3-8B1D-4584-A5D1-9D4EF487E0DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2BAAEA3-8B1D-4584-A5D1-9D4EF487E0DF}.net_4_5_Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2BAAEA3-8B1D-4584-A5D1-9D4EF487E0DF}.net_4_5_Debug|Any CPU.Build.0 = Debug|Any CPU + {B2BAAEA3-8B1D-4584-A5D1-9D4EF487E0DF}.net_4_5_Release|Any CPU.ActiveCfg = Debug|Any CPU + {B2BAAEA3-8B1D-4584-A5D1-9D4EF487E0DF}.net_4_5_Release|Any CPU.Build.0 = Debug|Any CPU + {B2BAAEA3-8B1D-4584-A5D1-9D4EF487E0DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2BAAEA3-8B1D-4584-A5D1-9D4EF487E0DF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {60B650D3-F98F-4016-99A7-C3C7B12E6BBC} + EndGlobalSection +EndGlobal diff --git a/GodotAddinVS.sln.DotSettings b/GodotAddinVS.sln.DotSettings new file mode 100644 index 0000000..f286055 --- /dev/null +++ b/GodotAddinVS.sln.DotSettings @@ -0,0 +1,7 @@ + + True + True + True + True + True + True \ No newline at end of file diff --git a/GodotAddinVS/Debugging/GodotDebugTarget.cs b/GodotAddinVS/Debugging/GodotDebugTarget.cs new file mode 100644 index 0000000..61a7463 --- /dev/null +++ b/GodotAddinVS/Debugging/GodotDebugTarget.cs @@ -0,0 +1,33 @@ +using System; + +namespace GodotAddinVS.Debugging +{ + public class GodotDebugTarget + { + private const string DebugTargetsGuidStr = "4E50788E-B023-4F77-AFE9-797603876907"; + public static readonly Guid DebugTargetsGuid = new Guid(DebugTargetsGuidStr); + + public Guid Guid { get; } + + public uint Id { get; } + + public string Name { get; } + + public ExecutionType ExecutionType { get; } + + public GodotDebugTarget(ExecutionType executionType, string name) + { + Guid = DebugTargetsGuid; + Id = 0x8192 + (uint) executionType; + ExecutionType = executionType; + Name = name; + } + } + + public enum ExecutionType : uint + { + PlayInEditor = 0, + Launch, + Attach + } +} diff --git a/GodotAddinVS/Debugging/GodotDebugTargetSelection.cs b/GodotAddinVS/Debugging/GodotDebugTargetSelection.cs new file mode 100644 index 0000000..538b63f --- /dev/null +++ b/GodotAddinVS/Debugging/GodotDebugTargetSelection.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.Shell.Interop; + +namespace GodotAddinVS.Debugging +{ + public class GodotDebugTargetSelection : IVsProjectCfgDebugTargetSelection + { + private static readonly List DebugTargets = new List() + { + new GodotDebugTarget(ExecutionType.PlayInEditor, "Play in Editor"), + new GodotDebugTarget(ExecutionType.Launch, "Launch"), + new GodotDebugTarget(ExecutionType.Attach, "Attach") + }; + + public static readonly GodotDebugTargetSelection Instance = new GodotDebugTargetSelection(); + + private IVsDebugTargetSelectionService _debugTargetSelectionService; + + public GodotDebugTarget CurrentDebugTarget { get; private set; } = DebugTargets.First(); + + public void GetCurrentDebugTarget(out Guid pguidDebugTargetType, + out uint pDebugTargetTypeId, out string pbstrCurrentDebugTarget) + { + pguidDebugTargetType = CurrentDebugTarget.Guid; + pDebugTargetTypeId = CurrentDebugTarget.Id; + pbstrCurrentDebugTarget = CurrentDebugTarget.Name; + } + + public Array GetDebugTargetListOfType(Guid guidDebugTargetType, uint debugTargetTypeId) + { + return DebugTargets + .Where(t => t.Guid == guidDebugTargetType && t.Id == debugTargetTypeId) + .Select(t => t.Name).ToArray(); + } + + public bool HasDebugTargets( + IVsDebugTargetSelectionService pDebugTargetSelectionService, out Array pbstrSupportedTargetCommandIDs) + { + _debugTargetSelectionService = pDebugTargetSelectionService; + pbstrSupportedTargetCommandIDs = DebugTargets + .Select(t => $"{t.Guid}:{t.Id}").ToArray(); + return true; + } + + public void SetCurrentDebugTarget(Guid guidDebugTargetType, + uint debugTargetTypeId, string bstrCurrentDebugTarget) + { + Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); + + CurrentDebugTarget = DebugTargets + .First(t => t.Guid == guidDebugTargetType && t.Id == debugTargetTypeId); + _debugTargetSelectionService?.UpdateDebugTargets(); + } + } +} diff --git a/GodotAddinVS/Debugging/GodotDebuggableProjectCfg.cs b/GodotAddinVS/Debugging/GodotDebuggableProjectCfg.cs new file mode 100644 index 0000000..c1a9d29 --- /dev/null +++ b/GodotAddinVS/Debugging/GodotDebuggableProjectCfg.cs @@ -0,0 +1,164 @@ +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.OLE.Interop; +using Microsoft.VisualStudio.Shell.Interop; +using Mono.Debugging.Soft; +using Mono.Debugging.VisualStudio; +using System; +using System.Net; +using System.Runtime.InteropServices; + +namespace GodotAddinVS.Debugging +{ + internal class GodotDebuggableProjectCfg : IVsDebuggableProjectCfg, IVsProjectFlavorCfg + { + private IVsProjectFlavorCfg _baseProjectCfg; + private readonly EnvDTE.Project _baseProject; + + public GodotDebuggableProjectCfg(IVsProjectFlavorCfg baseProjectCfg, EnvDTE.Project project) + { + _baseProject = project; + _baseProjectCfg = baseProjectCfg; + } + + public int get_DisplayName(out string pbstrDisplayName) + { + throw new NotImplementedException(); + } + + public int get_IsDebugOnly(out int pfIsDebugOnly) + { + throw new NotImplementedException(); + } + + public int get_IsReleaseOnly(out int pfIsReleaseOnly) + { + throw new NotImplementedException(); + } + + public int EnumOutputs(out IVsEnumOutputs ppIVsEnumOutputs) + { + throw new NotImplementedException(); + } + + public int OpenOutput(string szOutputCanonicalName, out IVsOutput ppIVsOutput) + { + throw new NotImplementedException(); + } + + public int get_ProjectCfgProvider(out IVsProjectCfgProvider ppIVsProjectCfgProvider) + { + throw new NotImplementedException(); + } + + public int get_BuildableProjectCfg(out IVsBuildableProjectCfg ppIVsBuildableProjectCfg) + { + throw new NotImplementedException(); + } + + public int get_CanonicalName(out string pbstrCanonicalName) + { + throw new NotImplementedException(); + } + + public int get_Platform(out Guid pguidPlatform) + { + throw new NotImplementedException(); + } + + public int get_IsPackaged(out int pfIsPackaged) + { + throw new NotImplementedException(); + } + + public int get_IsSpecifyingOutputSupported(out int pfIsSpecifyingOutputSupported) + { + throw new NotImplementedException(); + } + + public int get_TargetCodePage(out uint puiTargetCodePage) + { + throw new NotImplementedException(); + } + + public int get_UpdateSequenceNumber(ULARGE_INTEGER[] puliUSN) + { + throw new NotImplementedException(); + } + + public int get_RootURL(out string pbstrRootURL) + { + throw new NotImplementedException(); + } + + public int DebugLaunch(uint grfLaunch) + { + Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); + + var random = new Random(DateTime.Now.Millisecond); + var port = 8800 + random.Next(0, 100); + + var startArgs = new SoftDebuggerListenArgs(_baseProject.Name, IPAddress.Loopback, port) {MaxConnectionAttempts = 3}; + + var startInfo = new GodotStartInfo(startArgs, null, _baseProject) { WorkingDirectory = GodotPackage.Instance.GodotSolutionEventsListener?.SolutionDir}; + var session = new GodotDebuggerSession(); + + var launcher = new MonoDebuggerLauncher(new Progress()); + + launcher.StartSession(startInfo, session); + + return VSConstants.S_OK; + } + + public int QueryDebugLaunch(uint grfLaunch, out int pfCanLaunch) + { + pfCanLaunch = 1; + return VSConstants.S_OK; + } + + public int get_CfgType(ref Guid iidCfg, out IntPtr ppCfg) + { + Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); + + ppCfg = IntPtr.Zero; + + try + { + if (iidCfg == typeof(IVsDebuggableProjectCfg).GUID) + { + ppCfg = Marshal.GetComInterfaceForObject(this, typeof(IVsDebuggableProjectCfg)); + return VSConstants.S_OK; + } + + if (iidCfg == typeof(IVsProjectCfgDebugTargetSelection).GUID) + { + ppCfg = Marshal.GetComInterfaceForObject(GodotDebugTargetSelection.Instance, + typeof(IVsProjectCfgDebugTargetSelection)); + return VSConstants.S_OK; + } + + if ((ppCfg == IntPtr.Zero) && (_baseProjectCfg != null)) + { + return _baseProjectCfg.get_CfgType(ref iidCfg, out ppCfg); + } + } + catch (InvalidCastException) + { + } + + return VSConstants.E_NOINTERFACE; + } + + public int Close() + { + Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); + + if (_baseProjectCfg != null) + { + _baseProjectCfg.Close(); + _baseProjectCfg = null; + } + + return VSConstants.S_OK; + } + } +} diff --git a/GodotAddinVS/Debugging/GodotDebuggerSession.cs b/GodotAddinVS/Debugging/GodotDebuggerSession.cs new file mode 100644 index 0000000..efa6d03 --- /dev/null +++ b/GodotAddinVS/Debugging/GodotDebuggerSession.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using GodotTools.IdeMessaging; +using GodotTools.IdeMessaging.Requests; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Shell.Interop; +using Mono.Debugging.Client; +using Mono.Debugging.Soft; + +namespace GodotAddinVS.Debugging +{ + internal class GodotDebuggerSession : SoftDebuggerSession + { + private bool _attached; + private NetworkStream _godotRemoteDebuggerStream; + private Process _process; + + // TODO: Unused. Find a way to trigger this. + public void SendReloadScripts() + { + var executionType = GodotDebugTargetSelection.Instance.CurrentDebugTarget.ExecutionType; + + switch (executionType) + { + case ExecutionType.Launch: + GodotVariantEncoder.Encode( + new List {"reload_scripts"}, + _godotRemoteDebuggerStream + ); + _godotRemoteDebuggerStream.Flush(); + break; + case ExecutionType.PlayInEditor: + case ExecutionType.Attach: + var godotMessagingClient = + GodotPackage.Instance.GodotSolutionEventsListener?.GodotMessagingClient; + godotMessagingClient?.SendRequest(new ReloadScriptsRequest()); + break; + default: + throw new ArgumentOutOfRangeException(executionType.ToString()); + } + } + + private string GetGodotExecutablePath() + { + var options = (GeneralOptionsPage)GodotPackage.Instance.GetDialogPage(typeof(GeneralOptionsPage)); + + if (options.AlwaysUseConfiguredExecutable) + return options.GodotExecutablePath; + + var godotMessagingClient = GodotPackage.Instance.GodotSolutionEventsListener?.GodotMessagingClient; + + string godotPath = godotMessagingClient?.GodotEditorExecutablePath; + + if (!string.IsNullOrEmpty(godotPath) && File.Exists(godotPath)) + { + // If the setting is not yet assigned any value, set it to the currently connected Godot editor path + if (string.IsNullOrEmpty(options.GodotExecutablePath)) + options.GodotExecutablePath = godotPath; + return godotPath; + } + + return options.GodotExecutablePath; + } + + private void EndSessionWithError(string title, string errorMessage) + { + _ = GodotPackage.Instance.ShowErrorMessageBoxAsync(title, errorMessage); + EndSession(); + } + + protected override void OnRun(DebuggerStartInfo startInfo) + { + var godotStartInfo = (GodotStartInfo)startInfo; + + var executionType = GodotDebugTargetSelection.Instance.CurrentDebugTarget.ExecutionType; + + switch (executionType) + { + case ExecutionType.PlayInEditor: + { + _attached = false; + StartListening(godotStartInfo, out var assignedDebugPort); + + var godotMessagingClient = + GodotPackage.Instance.GodotSolutionEventsListener?.GodotMessagingClient; + + if (godotMessagingClient == null || !godotMessagingClient.IsConnected) + { + EndSessionWithError("Play Error", "No Godot editor instance connected"); + return; + } + + const string host = "127.0.0.1"; + + var playRequest = new DebugPlayRequest {DebuggerHost = host, DebuggerPort = assignedDebugPort}; + _ = godotMessagingClient.SendRequest(playRequest) + .ContinueWith(t => + { + if (t.Result.Status != MessageStatus.Ok) + EndSessionWithError("Play Error", $"Received Play response with status: {MessageStatus.Ok}"); + }, TaskScheduler.Default); + + // TODO: Read the editor player stdout and stderr somehow + + break; + } + case ExecutionType.Launch: + { + _attached = false; + StartListening(godotStartInfo, out var assignedDebugPort); + + // Listener to replace the Godot editor remote debugger. + // We use it to notify the game when assemblies should be reloaded. + var remoteDebugListener = new TcpListener(IPAddress.Any, 0); + remoteDebugListener.Start(); + _ = remoteDebugListener.AcceptTcpClientAsync() + .ContinueWith(OnGodotRemoteDebuggerConnectedAsync, TaskScheduler.Default); + + string workingDir = startInfo.WorkingDirectory; + const string host = "127.0.0.1"; + int remoteDebugPort = ((IPEndPoint)remoteDebugListener.LocalEndpoint).Port; + + // Launch Godot to run the game and connect to our remote debugger + + var processStartInfo = new ProcessStartInfo(GetGodotExecutablePath()) + { + Arguments = $"--path {workingDir} --remote-debug {host}:{remoteDebugPort}", // TODO: Doesn't work with 4.0dev. Should be tcp://host:port which doesn't work in 3.2... + WorkingDirectory = workingDir, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + // Tells Godot to connect to the mono debugger we just started + processStartInfo.EnvironmentVariables["GODOT_MONO_DEBUGGER_AGENT"] = + "--debugger-agent=transport=dt_socket" + + $",address={host}:{assignedDebugPort}" + + ",server=n"; + + _process = new Process {StartInfo = processStartInfo}; + + _process.OutputDataReceived += (sendingProcess, outLine) => OutputData(outLine.Data, false); + _process.ErrorDataReceived += (sendingProcess, outLine) => OutputData(outLine.Data, true); + + if (!_process.Start()) + { + EndSessionWithError("Launch Error", "Failed to start Godot process"); + return; + } + + _process.BeginOutputReadLine(); + + if (_process.HasExited) + { + EndSessionWithError("Launch Error", $"Godot process exited with code: {_process.ExitCode}"); + return; + } + + _process.Exited += (sender, args) => EndSession(); + + OnDebuggerOutput(false, $"Godot PID:{_process.Id}{Environment.NewLine}"); + + break; + } + case ExecutionType.Attach: + { + _attached = true; + StartConnecting(godotStartInfo); + break; + } + default: + throw new ArgumentOutOfRangeException(executionType.ToString()); + } + + if (!_attached) + { + var options = (GeneralOptionsPage)GodotPackage.Instance.GetDialogPage(typeof(GeneralOptionsPage)); + + // If a connection is never established and we try to stop debugging, Visual Studio will freeze + // for a long time for some reason. I have no idea why this happens. There may be something + // we're doing wrong. For now we'll limit the time we wait for incoming connections. + _ = Task.Delay(options.DebuggerListenTimeout).ContinueWith(r => + { + if (!HasExited && !IsConnected) + { + EndSession(); + + if (_process != null && !_process.HasExited) + _process.Kill(); + } + }, TaskScheduler.Default); + } + } + + protected override void OnExit() + { + if (_attached) + { + base.OnDetach(); + } + else + { + base.OnExit(); + + if (_process != null && !_process.HasExited) + _process.Kill(); + } + } + + [SuppressMessage("ReSharper", "VSTHRD103")] + private async Task OnGodotRemoteDebuggerConnectedAsync(Task task) + { + var tcpClient = task.Result; + _godotRemoteDebuggerStream = tcpClient.GetStream(); + var buffer = new byte[1000]; + while (tcpClient.Connected) + { + // There is no library to decode this messages, so + // we just pump buffer so it doesn't go out of memory + var readBytes = await _godotRemoteDebuggerStream.ReadAsync(buffer, 0, buffer.Length); + _ = readBytes; + } + } + + private void OutputData(string data, bool isStdErr) + { + try + { + OnTargetOutput(isStdErr, data + Environment.NewLine); + _ = Task.Run(async () => + { + await GodotPackage.Instance.JoinableTaskFactory.SwitchToMainThreadAsync(); + IVsOutputWindowPane outputPane = GodotPackage.Instance.GetOutputPane(VSConstants.OutputWindowPaneGuid.DebugPane_guid, "Output"); + outputPane.OutputStringThreadSafe(data + Environment.NewLine); + }); + } + catch (Exception e) + { + Console.Error.WriteLine(e); + } + } + } +} diff --git a/GodotAddinVS/Debugging/GodotStartInfo.cs b/GodotAddinVS/Debugging/GodotStartInfo.cs new file mode 100644 index 0000000..3f6a6ba --- /dev/null +++ b/GodotAddinVS/Debugging/GodotStartInfo.cs @@ -0,0 +1,14 @@ +using EnvDTE; +using Mono.Debugging.Soft; +using Mono.Debugging.VisualStudio; + +namespace GodotAddinVS.Debugging +{ + internal class GodotStartInfo : StartInfo + { + public GodotStartInfo(SoftDebuggerStartArgs args, DebuggingOptions options, Project startupProject) : + base(args, options, startupProject) + { + } + } +} diff --git a/GodotAddinVS/GeneralOptionsPage.cs b/GodotAddinVS/GeneralOptionsPage.cs new file mode 100644 index 0000000..0132ddc --- /dev/null +++ b/GodotAddinVS/GeneralOptionsPage.cs @@ -0,0 +1,48 @@ +using System.ComponentModel; +using Microsoft.VisualStudio.Shell; + +namespace GodotAddinVS +{ + public class GeneralOptionsPage : DialogPage + { + [Category("Debugging")] + [DisplayName("Always Use Configured Executable")] + [Description("When disabled, Visual Studio will attempt to get the Godot executable path from a running Godot editor instance")] + public bool AlwaysUseConfiguredExecutable { get; set; } = false; + + [Category("Debugging")] + [DisplayName("Godot Executable Path")] + [Description("Path to the Godot executable to use when launching the application for debugging")] + public string GodotExecutablePath { get; set; } = ""; + + [Category("Debugging")] + [DisplayName("Debugger Listen Timeout")] + [Description("Time in milliseconds after which the debugging session will end if no debugger is connected")] + public int DebuggerListenTimeout { get; set; } = 10000; + + [Category("Code Completion")] + [DisplayName("Provide Node Path Completions")] + [Description("Whether to provide code completion for node paths when a Godot editor is connected")] + public bool ProvideNodePathCompletions { get; set; } = true; + + [Category("Code Completion")] + [DisplayName("Provide Input Action Completions")] + [Description("Whether to provide code completion for input actions when a Godot editor is connected")] + public bool ProvideInputActionCompletions { get; set; } = true; + + [Category("Code Completion")] + [DisplayName("Provide Resource Path Completions")] + [Description("Whether to provide code completion for resource paths when a Godot editor is connected")] + public bool ProvideResourcePathCompletions { get; set; } = true; + + [Category("Code Completion")] + [DisplayName("Provide Scene Path Completions")] + [Description("Whether to provide code completion for scene paths when a Godot editor is connected")] + public bool ProvideScenePathCompletions { get; set; } = true; + + [Category("Code Completion")] + [DisplayName("Provide Signal Name Completions")] + [Description("Whether to provide code completion for signal names when a Godot editor is connected")] + public bool ProvideSignalNameCompletions { get; set; } = true; + } +} diff --git a/GodotAddinVS/GodotAddinVS.csproj b/GodotAddinVS/GodotAddinVS.csproj new file mode 100644 index 0000000..565b57f --- /dev/null +++ b/GodotAddinVS/GodotAddinVS.csproj @@ -0,0 +1,124 @@ + + + + 16.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + 8 + + + + Debug + AnyCPU + 2.0 + {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {7D64803D-2F8D-4597-9762-A316C74E9816} + Library + Properties + GodotAddinVS + GodotAddinVS + v4.7.2 + true + true + true + false + false + true + true + Program + $(DevEnvDir)devenv.exe + /rootsuffix Exp + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + Component + + + + + + + + + + Designer + + + + + + + + + + + compile; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + Always + true + + + Always + true + + + Menus.ctmenu + + + + + + + + + + + {a9ea6427-c5e2-4207-bbbf-a1f44a361339} + GodotCompletionProviders + + + + $(GetVsixSourceItemsDependsOn);IncludeNuGetResolvedAssets + + + + + + + + + \ No newline at end of file diff --git a/GodotAddinVS/GodotFlavoredProject.cs b/GodotAddinVS/GodotFlavoredProject.cs new file mode 100644 index 0000000..a65776d --- /dev/null +++ b/GodotAddinVS/GodotFlavoredProject.cs @@ -0,0 +1,53 @@ +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Shell.Flavor; +using Microsoft.VisualStudio.Shell.Interop; +using System; +using System.Runtime.InteropServices; +using GodotAddinVS.Debugging; + +namespace GodotAddinVS +{ + [ComVisible(true)] + [ClassInterface(ClassInterfaceType.None)] + [Guid(GodotPackage.GodotProjectGuid)] + internal class GodotFlavoredProject : FlavoredProjectBase, IVsProjectFlavorCfgProvider + { + private IVsProjectFlavorCfgProvider _innerFlavorConfig; + private GodotPackage _package; + + public GodotFlavoredProject(GodotPackage package) + { + _package = package; + } + + public int CreateProjectFlavorCfg(IVsCfg pBaseProjectCfg, out IVsProjectFlavorCfg ppFlavorCfg) + { + Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); + + ppFlavorCfg = null; + + if (_innerFlavorConfig != null) + { + GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID.VSHPROPID_ExtObject, out var project); + + _innerFlavorConfig.CreateProjectFlavorCfg(pBaseProjectCfg, out IVsProjectFlavorCfg cfg); + ppFlavorCfg = new GodotDebuggableProjectCfg(cfg, project as EnvDTE.Project); + } + + return ppFlavorCfg != null ? VSConstants.S_OK : VSConstants.E_FAIL; + } + + protected override void SetInnerProject(IntPtr innerIUnknown) + { + Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); + + object inner = Marshal.GetObjectForIUnknown(innerIUnknown); + _innerFlavorConfig = inner as IVsProjectFlavorCfgProvider; + + if (serviceProvider == null) + serviceProvider = _package; + + base.SetInnerProject(innerIUnknown); + } + } +} diff --git a/GodotAddinVS/GodotFlavoredProjectFactory.cs b/GodotAddinVS/GodotFlavoredProjectFactory.cs new file mode 100644 index 0000000..19537cb --- /dev/null +++ b/GodotAddinVS/GodotFlavoredProjectFactory.cs @@ -0,0 +1,22 @@ +using Microsoft.VisualStudio.Shell.Flavor; +using System; +using System.Runtime.InteropServices; + +namespace GodotAddinVS +{ + [Guid(GodotPackage.GodotProjectGuid)] + public class GodotFlavoredProjectFactory : FlavoredProjectFactoryBase + { + private readonly GodotPackage _package; + + public GodotFlavoredProjectFactory(GodotPackage package) + { + _package = package; + } + + protected override object PreCreateForOuter(IntPtr outerProjectIUnknown) + { + return new GodotFlavoredProject(_package); + } + } +} diff --git a/GodotAddinVS/GodotMessaging/MessageHandler.cs b/GodotAddinVS/GodotMessaging/MessageHandler.cs new file mode 100644 index 0000000..5b7e117 --- /dev/null +++ b/GodotAddinVS/GodotMessaging/MessageHandler.cs @@ -0,0 +1,55 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using EnvDTE; +using GodotTools.IdeMessaging; +using GodotTools.IdeMessaging.Requests; +using Microsoft.VisualStudio.Shell; + +namespace GodotAddinVS.GodotMessaging +{ + public class MessageHandler : ClientMessageHandler + { + private static ILogger Logger => GodotPackage.Instance.Logger; + + protected override async Task HandleOpenFile(OpenFileRequest request) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + var dte = GodotPackage.Instance.GetService(); + + try + { + dte.ItemOperations.OpenFile(request.File); + } + catch (ArgumentException e) + { + Logger?.LogError("ItemOperations.OpenFile: Invalid path or file not found", e); + return new OpenFileResponse {Status = MessageStatus.InvalidRequestBody}; + } + + if (request.Line != null) + { + var textSelection = (TextSelection)dte.ActiveDocument.Selection; + + if (request.Column != null) + { + textSelection.MoveToLineAndOffset(request.Line.Value, request.Column.Value); + } + else + { + textSelection.GotoLine(request.Line.Value, Select: true); + } + } + + var mainWindow = dte.MainWindow; + mainWindow.Activate(); + SetForegroundWindow(new IntPtr(mainWindow.HWnd)); + + return new OpenFileResponse {Status = MessageStatus.Ok}; + } + + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(IntPtr hWnd); + } +} diff --git a/GodotAddinVS/GodotPackage.cs b/GodotAddinVS/GodotPackage.cs new file mode 100644 index 0000000..85a75cd --- /dev/null +++ b/GodotAddinVS/GodotPackage.cs @@ -0,0 +1,110 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using GodotCompletionProviders; +using GodotTools.IdeMessaging; +using GodotTools.IdeMessaging.Requests; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using ILogger = GodotCompletionProviders.ILogger; +using Task = System.Threading.Tasks.Task; + +namespace GodotAddinVS +{ + /// + /// This is the class that implements the package exposed by this assembly. + /// + /// + /// + /// The minimum requirement for a class to be considered a valid package for Visual Studio + /// is to implement the IVsPackage interface and register itself with the shell. + /// This package uses the helper classes defined inside the Managed Package Framework (MPF) + /// to do it: it derives from the Package class that provides the implementation of the + /// IVsPackage interface and uses the registration attributes defined in the framework to + /// register itself and its components with the shell. These attributes tell the pkgdef creation + /// utility what data to put into .pkgdef file. + /// + /// + /// To get loaded into VS, the package must be referred by <Asset Type="Microsoft.VisualStudio.VsPackage" ...> in .vsixmanifest file. + /// + /// + [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] + [Guid(PackageGuidString)] + [ProvideProjectFactory(typeof(GodotFlavoredProjectFactory), "Godot.Project", null, "csproj", "csproj", null, + LanguageVsTemplate = "CSharp", TemplateGroupIDsVsTemplate = "Godot")] + [ProvideOptionPage(typeof(GeneralOptionsPage), + "Godot", "General", 0, 0, true)] + [ProvideMenuResource("Menus.ctmenu", 1)] + public sealed class GodotPackage : AsyncPackage + { + /// + /// GodotPackage GUID string. + /// + public const string PackageGuidString = "fbf828da-088b-482a-a550-befaed4b5d25"; + + public const string GodotProjectGuid = "8F3E2DF0-C35C-4265-82FC-BEA011F4A7ED"; + + #region Package Members + + public static GodotPackage Instance { get; private set; } + + public GodotPackage() + { + Instance = this; + } + + /// + /// Initialization of the package; this method is called right after the package is sited, so this is the place + /// where you can put all the initialization code that rely on services provided by VisualStudio. + /// + /// A cancellation token to monitor for initialization cancellation, which can occur when VS is shutting down. + /// A provider for progress updates. + /// A task representing the async work of package initialization, or an already completed task if there is none. Do not return null from this method. + protected override async Task InitializeAsync(CancellationToken cancellationToken, + IProgress progress) + { + // When initialized asynchronously, the current thread may be a background thread at this point. + // Do any initialization that requires the UI thread after switching to the UI thread. + await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + RegisterProjectFactory(new GodotFlavoredProjectFactory(this)); + + GodotSolutionEventsListener = new GodotSolutionEventsListener(this); + + var completionProviderContext = new GodotVsProviderContext(this); + BaseCompletionProvider.Context = completionProviderContext; + } + + internal GodotSolutionEventsListener GodotSolutionEventsListener { get; private set; } + + public GodotVSLogger Logger { get; } = new GodotVSLogger(); + + public async Task ShowErrorMessageBoxAsync(string title, string message) + { + await JoinableTaskFactory.SwitchToMainThreadAsync(); + + // ReSharper disable once SuspiciousTypeConversion.Global + var uiShell = (IVsUIShell)await GetServiceAsync(typeof(SVsUIShell)); + + if (uiShell == null) + throw new ServiceUnavailableException(typeof(SVsUIShell)); + + var clsid = Guid.Empty; + Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(uiShell.ShowMessageBox( + 0, + ref clsid, + title, + message, + string.Empty, + 0, + OLEMSGBUTTON.OLEMSGBUTTON_OK, + OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST, + OLEMSGICON.OLEMSGICON_CRITICAL, + 0, + pnResult: out _)); + } + + #endregion + } +} diff --git a/GodotAddinVS/GodotPackage.vsct b/GodotAddinVS/GodotPackage.vsct new file mode 100644 index 0000000..4187b3e --- /dev/null +++ b/GodotAddinVS/GodotPackage.vsct @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GodotAddinVS/GodotSolutionEventsListener.cs b/GodotAddinVS/GodotSolutionEventsListener.cs new file mode 100644 index 0000000..68f0e1a --- /dev/null +++ b/GodotAddinVS/GodotSolutionEventsListener.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Design; +using System.IO; +using System.Linq; +using EnvDTE; +using GodotAddinVS.Debugging; +using GodotAddinVS.GodotMessaging; +using GodotTools.IdeMessaging; +using GodotTools.IdeMessaging.Requests; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; + +namespace GodotAddinVS +{ + internal class GodotSolutionEventsListener : SolutionEventsListener + { + private static readonly object RegisterLock = new object(); + private bool _registered; + + private string _godotProjectDir; + + private DebuggerEvents DebuggerEvents { get; set; } + + private IServiceContainer ServiceContainer => (IServiceContainer)ServiceProvider; + + public string SolutionDir + { + get + { + ThreadHelper.ThrowIfNotOnUIThread(); + Solution.GetSolutionInfo(out string solutionDir, out string solutionFile, out string userOptsFile); + _ = solutionFile; + _ = userOptsFile; + return solutionDir; + } + } + + public Client GodotMessagingClient { get; private set; } + + public GodotSolutionEventsListener(IServiceProvider serviceProvider) + : base(serviceProvider) + { + ThreadHelper.ThrowIfNotOnUIThread(); + Init(); + } + + public override int OnBeforeCloseProject(IVsHierarchy hierarchy, int removed) + { + return 0; + } + + private static IEnumerable ParseProjectTypeGuids(string projectTypeGuids) + { + string[] strArray = projectTypeGuids.Split(';'); + var guidList = new List(strArray.Length); + + foreach (string input in strArray) + { + if (Guid.TryParse(input, out var result)) + guidList.Add(result); + } + + return guidList.ToArray(); + } + + private static bool IsGodotProject(IVsHierarchy hierarchy) + { + ThreadHelper.ThrowIfNotOnUIThread(); + return hierarchy is IVsAggregatableProject aggregatableProject && + aggregatableProject.GetAggregateProjectTypeGuids(out string projectTypeGuids) == 0 && + ParseProjectTypeGuids(projectTypeGuids) + .Any(g => g == typeof(GodotFlavoredProjectFactory).GUID); + } + + public override int OnAfterOpenProject(IVsHierarchy hierarchy, int added) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (!IsGodotProject(hierarchy)) + return 0; + + lock (RegisterLock) + { + if (_registered) + return 0; + + _godotProjectDir = SolutionDir; + + DebuggerEvents = ServiceProvider.GetService().Events.DebuggerEvents; + DebuggerEvents.OnEnterDesignMode += DebuggerEvents_OnEnterDesignMode; + + GodotMessagingClient?.Dispose(); + GodotMessagingClient = new Client(identity: "VisualStudio", + _godotProjectDir, new MessageHandler(), GodotPackage.Instance.Logger); + GodotMessagingClient.Connected += OnClientConnected; + GodotMessagingClient.Start(); + + ServiceContainer.AddService(typeof(Client), GodotMessagingClient); + + _registered = true; + } + + return 0; + } + + public override int OnBeforeCloseSolution(object pUnkReserved) + { + lock (RegisterLock) + _registered = false; + Close(); + return 0; + } + + protected override void Dispose(bool disposing) + { + if (!disposing) + return; + Close(); + } + + private void OnClientConnected() + { + var options = (GeneralOptionsPage)GodotPackage.Instance.GetDialogPage(typeof(GeneralOptionsPage)); + + // If the setting is not yet assigned any value, set it to the currently connected Godot editor path + if (string.IsNullOrEmpty(options.GodotExecutablePath)) + { + string godotPath = GodotMessagingClient?.GodotEditorExecutablePath; + if (!string.IsNullOrEmpty(godotPath) && File.Exists(godotPath)) + options.GodotExecutablePath = godotPath; + } + } + + private void DebuggerEvents_OnEnterDesignMode(dbgEventReason reason) + { + if (reason != dbgEventReason.dbgEventReasonStopDebugging) + return; + + if (GodotMessagingClient == null || !GodotMessagingClient.IsConnected) + return; + + var currentDebugTarget = GodotDebugTargetSelection.Instance.CurrentDebugTarget; + + if (currentDebugTarget != null && currentDebugTarget.ExecutionType == ExecutionType.PlayInEditor) + _ = GodotMessagingClient.SendRequest(new StopPlayRequest()); + } + + private void Close() + { + if (GodotMessagingClient != null) + { + ServiceContainer.RemoveService(typeof(Client)); + GodotMessagingClient.Dispose(); + GodotMessagingClient = null; + } + + if (DebuggerEvents != null) + { + DebuggerEvents.OnEnterDesignMode -= DebuggerEvents_OnEnterDesignMode; + DebuggerEvents = null; + } + } + } +} diff --git a/GodotAddinVS/GodotVSLogger.cs b/GodotAddinVS/GodotVSLogger.cs new file mode 100644 index 0000000..638234c --- /dev/null +++ b/GodotAddinVS/GodotVSLogger.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.VisualStudio.Shell.Interop; +using System.Threading.Tasks; +using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper; + +namespace GodotAddinVS +{ + // ReSharper disable once InconsistentNaming + public class GodotVSLogger : GodotTools.IdeMessaging.ILogger, GodotCompletionProviders.ILogger + { + private async Task LogMessageAsync(__ACTIVITYLOG_ENTRYTYPE actType, string message) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + // ReSharper disable once SuspiciousTypeConversion.Global + var log = (IVsActivityLog)GodotPackage.Instance.GetService(); + + if (log == null) + return; + + _ = log.LogEntry((uint)actType, this.ToString(), message); + } + + public void LogDebug(string message) + { + _ = LogMessageAsync(__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION, message); + } + + public void LogInfo(string message) + { + _ = LogMessageAsync(__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION, message); + } + + public void LogWarning(string message) + { + _ = LogMessageAsync(__ACTIVITYLOG_ENTRYTYPE.ALE_WARNING, message); + } + + public void LogError(string message) + { + _ = LogMessageAsync(__ACTIVITYLOG_ENTRYTYPE.ALE_ERROR, message); + } + + public void LogError(string message, Exception e) + { + _ = LogMessageAsync(__ACTIVITYLOG_ENTRYTYPE.ALE_ERROR, message + "\n" + e); + } + } +} diff --git a/GodotAddinVS/GodotVariant.cs b/GodotAddinVS/GodotVariant.cs new file mode 100644 index 0000000..93e380a --- /dev/null +++ b/GodotAddinVS/GodotVariant.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace GodotAddinVS +{ + // Incomplete implementation of the Godot's Variant encoder. Add missing parts as needed. + + public class GodotVariantEncoder + { + private readonly List _buffer = new List(); + + public int Length => _buffer.Count; + + public byte[] ToArray() => _buffer.ToArray(); + + public void AddBytes(params byte[] bytes) => + _buffer.AddRange(bytes); + + public void AddInt(int value) => + AddBytes(BitConverter.GetBytes(value)); + + public void AddType(GodotVariant.Type type) => + AddInt((int) type); + + public void AddString(string value) + { + byte[] utf8Bytes = Encoding.UTF8.GetBytes(value); + + AddType(GodotVariant.Type.String); + AddInt(utf8Bytes.Length); + AddBytes(utf8Bytes); + AddBytes(0); // Godot's UTF8 converter adds a trailing whitespace + + while (_buffer.Count % 4 != 0) + _buffer.Add(0); + } + + public void AddArray(List array) + { + AddType(GodotVariant.Type.Array); + AddInt(array.Count); + + foreach (var element in array) + { + if (element.VariantType == GodotVariant.Type.String) + AddString(element.Get()); + else + throw new NotImplementedException(); + } + } + + public static void Encode(GodotVariant variant, Stream stream) + { + using (var writer = new BinaryWriter(stream, new UTF8Encoding(false, true), leaveOpen: true)) + { + var encoder = new GodotVariantEncoder(); + switch (variant.VariantType) + { + case GodotVariant.Type.String: + encoder.AddString((string) variant.Value); + break; + case GodotVariant.Type.Array: + encoder.AddArray((List) variant.Value); + break; + default: + throw new NotImplementedException(); + } + + // ReSharper disable once RedundantCast + writer.Write((int) encoder.Length); + writer.Write(encoder.ToArray()); + } + } + } + + public class GodotVariant + { + public enum Type + { + Nil = 0, + Bool = 1, + Int = 2, + Real = 3, + String = 4, + Vector2 = 5, + Rect2 = 6, + Vector3 = 7, + Transform2d = 8, + Quat = 10, + Aabb = 11, + Basis = 12, + Transform = 13, + Color = 14, + NodePath = 15, + Rid = 16, + Object = 17, + Dictionary = 18, + Array = 19, + RawArray = 20, + IntArray = 21, + RealArray = 22, + StringArray = 23, + Vector2Array = 24, + Vector3Array = 25, + ColorArray = 26, + Max = 27 + } + + public object Value { get; } + public Type VariantType { get; } + + public T Get() + { + return (T) Value; + } + + public override string ToString() + { + return Value.ToString(); + } + + public GodotVariant(string value) + { + Value = value; + VariantType = Type.String; + } + + public GodotVariant(List value) + { + Value = value; + VariantType = Type.Array; + } + + public static implicit operator GodotVariant(string value) => new GodotVariant(value); + public static implicit operator GodotVariant(List value) => new GodotVariant(value); + } +} diff --git a/GodotAddinVS/GodotVsProviderContext.cs b/GodotAddinVS/GodotVsProviderContext.cs new file mode 100644 index 0000000..2b52bca --- /dev/null +++ b/GodotAddinVS/GodotVsProviderContext.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; +using GodotCompletionProviders; +using GodotTools.IdeMessaging; +using GodotTools.IdeMessaging.Requests; +using ILogger = GodotCompletionProviders.ILogger; + +namespace GodotAddinVS +{ + internal class GodotVsProviderContext : IProviderContext + { + private readonly GodotPackage _package; + + public GodotVsProviderContext(GodotPackage package) + { + _package = package; + } + + public ILogger GetLogger() => _package.Logger; + + public bool AreCompletionsEnabledFor(CompletionKind completionKind) + { + var options = (GeneralOptionsPage)GodotPackage.Instance.GetDialogPage(typeof(GeneralOptionsPage)); + + if (options == null) + return false; + + return completionKind switch + { + CompletionKind.NodePaths => options.ProvideNodePathCompletions, + CompletionKind.InputActions => options.ProvideInputActionCompletions, + CompletionKind.ResourcePaths => options.ProvideResourcePathCompletions, + CompletionKind.ScenePaths => options.ProvideScenePathCompletions, + CompletionKind.Signals => options.ProvideSignalNameCompletions, + _ => false + }; + } + + public bool CanRequestCompletionsFromServer() + { + var godotMessagingClient = _package.GodotSolutionEventsListener?.GodotMessagingClient; + return godotMessagingClient != null && godotMessagingClient.IsConnected; + } + + public async Task RequestCompletion(CompletionKind completionKind, string absoluteFilePath) + { + var godotMessagingClient = _package.GodotSolutionEventsListener?.GodotMessagingClient; + + if (godotMessagingClient == null) + throw new InvalidOperationException(); + + var request = new CodeCompletionRequest {Kind = (CodeCompletionRequest.CompletionKind)completionKind, ScriptFile = absoluteFilePath}; + var response = await godotMessagingClient.SendRequest(request); + + if (response.Status == MessageStatus.Ok) + return response.Suggestions; + + GetLogger().LogError($"Received code completion response with status '{response.Status}'."); + return new string[] { }; + } + } +} diff --git a/GodotAddinVS/LICENSE.txt b/GodotAddinVS/LICENSE.txt new file mode 100644 index 0000000..feeb8a8 --- /dev/null +++ b/GodotAddinVS/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Godot Engine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/GodotAddinVS/NuGet.Config b/GodotAddinVS/NuGet.Config new file mode 100644 index 0000000..509dd87 --- /dev/null +++ b/GodotAddinVS/NuGet.Config @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/GodotAddinVS/Properties/AssemblyInfo.cs b/GodotAddinVS/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..df881f4 --- /dev/null +++ b/GodotAddinVS/Properties/AssemblyInfo.cs @@ -0,0 +1,32 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GodotAddinVS")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GodotAddinVS")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/GodotAddinVS/SolutionEventsListener.cs b/GodotAddinVS/SolutionEventsListener.cs new file mode 100644 index 0000000..6b4e735 --- /dev/null +++ b/GodotAddinVS/SolutionEventsListener.cs @@ -0,0 +1,80 @@ +using System; +using Microsoft; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Shell.Interop; + +namespace GodotAddinVS +{ + internal abstract class SolutionEventsListener : IVsSolutionEvents, IDisposable + { + private static volatile object _disposalLock = new object(); + private uint _eventsCookie; + private bool _disposed; + + protected SolutionEventsListener(IServiceProvider serviceProvider) + { + Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); + + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + + Solution = ServiceProvider.GetService(typeof(SVsSolution)) as IVsSolution; + Assumes.Present(Solution); + } + + protected IVsSolution Solution { get; } + + protected IServiceProvider ServiceProvider { get; } + + public virtual int OnAfterOpenSolution(object pUnkReserved, int fNewSolution) => VSConstants.E_NOTIMPL; + + public virtual int OnBeforeCloseSolution(object pUnkReserved) => VSConstants.E_NOTIMPL; + + public virtual int OnAfterCloseSolution(object reserved) => VSConstants.E_NOTIMPL; + + public virtual int OnQueryCloseSolution(object pUnkReserved, ref int cancel) => VSConstants.E_NOTIMPL; + + public virtual int OnAfterOpenProject(IVsHierarchy hierarchy, int added) => VSConstants.E_NOTIMPL; + + public virtual int OnAfterLoadProject(IVsHierarchy stubHierarchy, IVsHierarchy realHierarchy) => VSConstants.E_NOTIMPL; + + public virtual int OnBeforeUnloadProject(IVsHierarchy realHierarchy, IVsHierarchy rtubHierarchy) => VSConstants.E_NOTIMPL; + + public virtual int OnBeforeCloseProject(IVsHierarchy hierarchy, int removed) => VSConstants.E_NOTIMPL; + + public virtual int OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int cancel) => VSConstants.E_NOTIMPL; + + public virtual int OnQueryCloseProject(IVsHierarchy hierarchy, int removing, ref int cancel) => VSConstants.E_NOTIMPL; + + public void Dispose() + { + Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Init() + { + Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); + ErrorHandler.ThrowOnFailure(Solution.AdviseSolutionEvents(this, out _eventsCookie)); + } + + protected virtual void Dispose(bool disposing) + { + Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); + + if (_disposed) + return; + + lock (_disposalLock) + { + if (disposing && _eventsCookie != 0U && Solution != null) + { + Solution.UnadviseSolutionEvents(_eventsCookie); + _eventsCookie = 0U; + } + + _disposed = true; + } + } + } +} diff --git a/GodotAddinVS/icon.png b/GodotAddinVS/icon.png new file mode 100644 index 0000000..6ad9b43 Binary files /dev/null and b/GodotAddinVS/icon.png differ diff --git a/GodotAddinVS/source.extension.vsixmanifest b/GodotAddinVS/source.extension.vsixmanifest new file mode 100644 index 0000000..306ab71 --- /dev/null +++ b/GodotAddinVS/source.extension.vsixmanifest @@ -0,0 +1,27 @@ + + + + + Godot Addin + Support for Godot Engine C# projects, including debugging and extended code completion. + LICENSE.txt + icon.png + Godot + + + + + + + + + + + + + + + + diff --git a/GodotCompletionProviders.Test/GodotCompletionProviders.Test.csproj b/GodotCompletionProviders.Test/GodotCompletionProviders.Test.csproj new file mode 100644 index 0000000..ce408d9 --- /dev/null +++ b/GodotCompletionProviders.Test/GodotCompletionProviders.Test.csproj @@ -0,0 +1,179 @@ + + + + + + Debug + AnyCPU + {B2BAAEA3-8B1D-4584-A5D1-9D4EF487E0DF} + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + Properties + GodotCompletionProviders.Test + GodotCompletionProviders.Test + v4.7.2 + 512 + 8 + enable + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Microsoft.CodeAnalysis.Common.3.3.1\lib\netstandard2.0\Microsoft.CodeAnalysis.dll + True + + + ..\packages\Microsoft.CodeAnalysis.CSharp.3.3.1\lib\netstandard2.0\Microsoft.CodeAnalysis.CSharp.dll + True + + + ..\packages\Microsoft.CodeAnalysis.CSharp.Features.3.3.1\lib\netstandard2.0\Microsoft.CodeAnalysis.CSharp.Features.dll + True + + + ..\packages\Microsoft.CodeAnalysis.CSharp.Workspaces.3.3.1\lib\netstandard2.0\Microsoft.CodeAnalysis.CSharp.Workspaces.dll + True + + + ..\packages\Microsoft.CodeAnalysis.Features.3.3.1\lib\netstandard2.0\Microsoft.CodeAnalysis.Features.dll + True + + + ..\packages\Microsoft.CodeAnalysis.FlowAnalysis.Utilities.2.9.5\lib\netstandard1.3\Microsoft.CodeAnalysis.FlowAnalysis.Utilities.dll + True + + + ..\packages\Microsoft.CodeAnalysis.Workspaces.Common.3.3.1\lib\netstandard2.0\Microsoft.CodeAnalysis.Workspaces.dll + True + + + ..\packages\Microsoft.DiaSymReader.1.3.0\lib\net20\Microsoft.DiaSymReader.dll + True + + + + + ..\packages\System.Buffers.4.4.0\lib\netstandard2.0\System.Buffers.dll + True + + + ..\packages\System.Collections.Immutable.1.5.0\lib\netstandard2.0\System.Collections.Immutable.dll + True + + + ..\packages\System.Composition.AttributedModel.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.AttributedModel.dll + True + + + ..\packages\System.Composition.Convention.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.Convention.dll + True + + + ..\packages\System.Composition.Hosting.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.Hosting.dll + True + + + ..\packages\System.Composition.Runtime.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.Runtime.dll + True + + + ..\packages\System.Composition.TypedParts.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.TypedParts.dll + True + + + + + ..\packages\System.Memory.4.5.3\lib\netstandard2.0\System.Memory.dll + True + + + + ..\packages\System.Numerics.Vectors.4.4.0\lib\net46\System.Numerics.Vectors.dll + True + + + ..\packages\System.Reflection.Metadata.1.6.0\lib\netstandard2.0\System.Reflection.Metadata.dll + True + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + True + + + ..\packages\System.Text.Encoding.CodePages.4.5.1\lib\net461\System.Text.Encoding.CodePages.dll + True + + + ..\packages\System.Threading.Tasks.Extensions.4.5.3\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll + True + + + + ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + + + ..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll + + + ..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll + + + ..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll + + + + + + + + + + + + + + + + + + + + + + {a9ea6427-c5e2-4207-bbbf-a1f44a361339} + GodotCompletionProviders + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. + + + + + diff --git a/GodotCompletionProviders.Test/InputActionTests.cs b/GodotCompletionProviders.Test/InputActionTests.cs new file mode 100644 index 0000000..888e9e2 --- /dev/null +++ b/GodotCompletionProviders.Test/InputActionTests.cs @@ -0,0 +1,110 @@ +using System.Threading.Tasks; +using Xunit; + +namespace GodotCompletionProviders.Test +{ + [Collection("Sequential")] + public class InputActionTests : TestsBase + { + private const string StubCode = @" +namespace Godot +{ + public class StringName + { + public StringName() { } + public StringName(string from) { } + public static implicit operator StringName(string from) => throw new NotImplementedException(); + public static implicit operator string(StringName from) => throw new NotImplementedException(); + } + + public static class Input + { + public static bool IsActionPressed(StringName action) => throw new NotImplementedException(); + public static bool IsActionJustPressed(StringName action) => throw new NotImplementedException(); + public static bool IsActionJustReleased(StringName action) => throw new NotImplementedException(); + public static float GetActionStrength(StringName action) => throw new NotImplementedException(); + public static void ActionPress(StringName action, float strength = 1f) => throw new NotImplementedException(); + public static void ActionRelease(StringName action) => throw new NotImplementedException(); + } +} +"; + + public InputActionTests() : base(new InputActionCompletionProvider()) + { + } + + private Task ProvidesForFull(string statements) + { + string testCode = $@" +using Godot; +{statements}"; + string code = Utils.ReadSingleCaretTestCode(testCode, out int caretPosition); + return ShouldProvideCompletion(StubCode, code, caretPosition); + } + + private async Task ProvidesFor(string statements) => + (await ProvidesForFull(statements)).ShouldProvideCompletion; + + [Fact] + public void TestTestNotSomethingElse() + { + Assert.False(ProvidesFor("Input.Foo(⛶)").Result); + Assert.False(ProvidesFor("Input.Foo(, ⛶)").Result); + Assert.False(ProvidesFor("Input.Foo(, , ⛶)").Result); + } + + [Fact] + public void TestIsActionPressed() + { + Assert.True(ProvidesFor("Input.IsActionPressed(⛶").Result); + Assert.True(ProvidesFor("Input.IsActionPressed(⛶)").Result); + Assert.False(ProvidesFor("Input.IsActionPressed(, ⛶").Result); + Assert.False(ProvidesFor("Input.IsActionPressed(, ⛶)").Result); + } + + [Fact] + public void TestIsActionJustPressed() + { + Assert.True(ProvidesFor("Input.IsActionJustPressed(⛶").Result); + Assert.True(ProvidesFor("Input.IsActionJustPressed(⛶)").Result); + Assert.False(ProvidesFor("Input.IsActionJustPressed(, ⛶").Result); + Assert.False(ProvidesFor("Input.IsActionJustPressed(, ⛶)").Result); + } + + [Fact] + public void TestIsActionJustReleased() + { + Assert.True(ProvidesFor("Input.IsActionJustReleased(⛶").Result); + Assert.True(ProvidesFor("Input.IsActionJustReleased(⛶)").Result); + Assert.False(ProvidesFor("Input.IsActionJustReleased(, ⛶").Result); + Assert.False(ProvidesFor("Input.IsActionJustReleased(, ⛶)").Result); + } + + [Fact] + public void TestGetActionStrength() + { + Assert.True(ProvidesFor("Input.GetActionStrength(⛶").Result); + Assert.True(ProvidesFor("Input.GetActionStrength(⛶)").Result); + Assert.False(ProvidesFor("Input.GetActionStrength(, ⛶").Result); + Assert.False(ProvidesFor("Input.GetActionStrength(, ⛶)").Result); + } + + [Fact] + public void TestActionPress() + { + Assert.True(ProvidesFor("Input.ActionPress(⛶").Result); + Assert.True(ProvidesFor("Input.ActionPress(⛶)").Result); + Assert.False(ProvidesFor("Input.ActionPress(, ⛶").Result); + Assert.False(ProvidesFor("Input.ActionPress(, ⛶)").Result); + } + + [Fact] + public void TestActionRelease() + { + Assert.True(ProvidesFor("Input.ActionRelease(⛶").Result); + Assert.True(ProvidesFor("Input.ActionRelease(⛶)").Result); + Assert.False(ProvidesFor("Input.ActionRelease(, ⛶").Result); + Assert.False(ProvidesFor("Input.ActionRelease(, ⛶)").Result); + } + } +} diff --git a/GodotCompletionProviders.Test/NodePathTests.cs b/GodotCompletionProviders.Test/NodePathTests.cs new file mode 100644 index 0000000..afe7c4f --- /dev/null +++ b/GodotCompletionProviders.Test/NodePathTests.cs @@ -0,0 +1,257 @@ +using System.Threading.Tasks; +using Xunit; + +namespace GodotCompletionProviders.Test +{ + [Collection("Sequential")] + public class NodePathTests : TestsBase + { + private const string StubCode = @" +namespace Godot +{ + public class NodePath + { + public NodePath() { } + public NodePath(string from) { } + public static implicit operator NodePath(string from) => throw new NotImplementedException(); + public static implicit operator string(NodePath from) => throw new NotImplementedException(); + } + + public class Object { } + public class Node : Godot.Object { } +} +"; + + public NodePathTests() : base(new NodePathCompletionProvider()) + { + } + + private Task ProvidesForFull(string classMemberDeclaration) + { + string testCode = $@" +using Godot; +{classMemberDeclaration}"; + string code = Utils.ReadSingleCaretTestCode(testCode, out int caretPosition); + return ShouldProvideCompletion(StubCode, code, caretPosition); + } + + private async Task ProvidesFor(string classMemberDeclaration) => + (await ProvidesForFull(classMemberDeclaration)).ShouldProvideCompletion; + + [Fact] + public void TestFieldDeclarations() + { + Assert.True(ProvidesFor("NodePath npField = ⛶;").Result); + } + + [Fact] + public void TestPropertyDeclarations() + { + Assert.True(ProvidesFor("NodePath npProp1 => ⛶;").Result); + Assert.True(ProvidesFor("NodePath npProp2 { get => ⛶; }").Result); + Assert.True(ProvidesFor("NodePath npProp3 { get { return ⛶; } }").Result); + Assert.True(ProvidesFor("NodePath npProp4 { get; set; } = ⛶;").Result); + } + + [Fact] + public void TestInvocationArgument() + { + // First argument + Assert.True(ProvidesFor(@" +void FirstParam(NodePath path) { } +FirstParam(⛶); +").Result); + + // Second argument + Assert.True(ProvidesFor(@" +void SecondParam(object nothing, NodePath path) { } +SecondParam(null, ⛶); +").Result); + + // First argument of generic method invocation + Assert.True(ProvidesFor(@" +void FirstParamGeneric(NodePath path) { } +FirstParamGeneric(⛶); +").Result); + } + + [Fact] + public void TestNodePathCreationExpression() + { + Assert.True(ProvidesFor(@"_ = new NodePath(⛶);").Result); + } + + [Fact] + public void TestExplicitCast() + { + Assert.True(ProvidesFor(@"_ = (NodePath)⛶").Result); + Assert.True(ProvidesFor(@"_ = (NodePath)(⛶)").Result); + Assert.True(ProvidesFor(@"_ = (NodePath)⛶;").Result); + Assert.True(ProvidesFor(@"_ = (NodePath)(⛶);").Result); + + Assert.False(ProvidesFor(@"_ = ((NodePath))⛶").Result); + Assert.False(ProvidesFor(@"_ = ((NodePath))⛶;").Result); + } + + [Fact] + public void TestBinaryOperation() + { + Assert.True(ProvidesFor(@"_ = new NodePath() == ⛶;").Result); + Assert.True(ProvidesFor(@"_ = new NodePath() != ⛶;").Result); + + // TODO: Not supported by the type inference service. + //Assert.True(ProvidesFor(@"_ = ⛶ == new NodePath();").Result); + //Assert.True(ProvidesFor(@"_ = ⛶ != new NodePath();").Result); + } + + [Fact] + public void TestAssignment() + { + // Assignment in local declaration + Assert.True(ProvidesFor(@"NodePath npLocal = ⛶;").Result); + + // Assignment to a previously declared local + Assert.True(ProvidesFor(@" +NodePath npLocal; +npLocal = ⛶;").Result); + + // Assignment to a previously declared field + Assert.True(ProvidesFor(@" +NodePath npField; +void Foo() { npField = ⛶; } +").Result); + + // Assignment to a previously declared property + Assert.True(ProvidesFor(@" +NodePath npProp { get; } +void Foo() { npProp = ⛶; } +").Result); + + // Assignment to ref parameter + Assert.True(ProvidesFor(@"void Foo(ref NodePath npRefParam) { npRefParam = ⛶; }").Result); + + // Assignment to out parameter + Assert.True(ProvidesFor(@"void Foo(out NodePath npOutParam) { npOutParam = ⛶; }").Result); + } + + [Fact] + public void TestElementAccessArgument() + { + Assert.True(ProvidesFor(@" +System.Collections.Generic.Dictionary npDictLocal = default; +_ = npDictLocal[⛶]; +").Result); + } + + [Fact] + public void TestParenthesizedExpression() + { + Assert.True(ProvidesFor(@"NodePath _ = (⛶);").Result); + Assert.True(ProvidesFor(@"NodePath _ = ((⛶));").Result); + Assert.True(ProvidesFor(@"NodePath _ = (((⛶)));").Result); + + Assert.True(ProvidesFor(@" +void FirstParam(NodePath path) { } +FirstParam((⛶)); +").Result); + } + + [Fact] + public void TestNullCoalescing() + { + // TODO: Not supported by the type inference service. + // Null-coalescing operator + //Assert.True(ProvidesFor(@"NodePath _ = null ?? ⛶;").Result); + // Null-coalescing assignment + //Assert.True(ProvidesFor(@"NodePath _ ??= ⛶;").Result); + + // Null-coalescing assignment in expression + Assert.True(ProvidesFor(@" +NodePath _; +_ = _ ??= ⛶; +").Result); + } + + [Fact] + public void TestConditionalOperator() + { + Assert.True(ProvidesFor(@"NodePath _ = false ? ⛶ : default;").Result); + Assert.True(ProvidesFor(@"NodePath _ = false ? default : ⛶;").Result); + } + + [Fact] + public void TestByRefParametersAssignment() + { + // Return statement expression + Assert.True(ProvidesFor(@"NodePath Foo() { return ⛶; }").Result); + + // Return expression of expression-bodied method + Assert.True(ProvidesFor(@"NodePath Foo() => ⛶;").Result); + + // Yield return expression + Assert.True(ProvidesFor(@"IEnumerable Foo() { yield return ⛶; }").Result); + } + + [Fact] + public void TestStringLiteral() + { + // Empty string literal + Assert.True(ProvidesForFull(@"NodePath _ = ""⛶").Result.CheckLiteralResult("")); + Assert.True(ProvidesForFull(@"NodePath _ = ""⛶;").Result.CheckLiteralResult(";")); + + // Inside string literal (not closed, at end) + Assert.True(ProvidesForFull(@"NodePath _ = ""Foo⛶").Result.CheckLiteralResult("Foo")); + // Inside string literal (not closed, in between) + Assert.True(ProvidesForFull(@"NodePath _ = ""Foo⛶Bar").Result.CheckLiteralResult("FooBar")); + // At end of string literal (closed) + Assert.True(ProvidesForFull(@"NodePath _ = ""Foo⛶"";").Result.CheckLiteralResult("Foo")); + // Inside string literal (closed, in between) + Assert.True(ProvidesForFull(@"NodePath _ = ""Foo⛶Bar"";").Result.CheckLiteralResult("FooBar")); + } + + [Fact] + public void TestVerbatimStringLiteral() + { + // Empty verbatim string literal + Assert.True(ProvidesForFull(@"NodePath _ = @""⛶").Result.CheckLiteralResult("")); + Assert.True(ProvidesForFull(@"NodePath _ = @""⛶;").Result.CheckLiteralResult(";")); + + // Inside verbatim string literal (not closed, at end) + Assert.True(ProvidesForFull(@"NodePath _ = @""Foo⛶").Result.CheckLiteralResult("Foo")); + // Inside verbatim string literal (not closed, in between) + Assert.True(ProvidesForFull(@"NodePath _ = @""Foo⛶Bar").Result.CheckLiteralResult("FooBar")); + // Inside verbatim string literal (closed, at end) + Assert.True(ProvidesForFull(@"NodePath _ = @""Foo⛶"";").Result.CheckLiteralResult("Foo")); + // Inside verbatim string literal (closed, in between) + Assert.True(ProvidesForFull(@"NodePath _ = @""Foo⛶Bar"";").Result.CheckLiteralResult("FooBar")); + } + + [Fact] + public void TestInterpolatedStringLiteral() + { + // Empty interpolated string + Assert.True(ProvidesForFull(@"NodePath _ = $""⛶").Result.CheckLiteralResult("")); + Assert.True(ProvidesForFull(@"NodePath _ = $""⛶;").Result.CheckLiteralResult(";")); + + // Interpolated string literal without interpolations are supported + // Inside interpolated string literal (not closed, at end) + Assert.True(ProvidesForFull(@"NodePath _ = $""Foo⛶").Result.CheckLiteralResult("Foo")); + // Inside interpolated string literal (not closed, in between) + Assert.True(ProvidesForFull(@"NodePath _ = $""Foo⛶Bar").Result.CheckLiteralResult("FooBar")); + // Inside interpolated string literal (closed, at end) + Assert.True(ProvidesForFull(@"NodePath _ = $""Foo⛶"";").Result.CheckLiteralResult("Foo")); + // Inside interpolated string literal (closed, in between) + Assert.True(ProvidesForFull(@"NodePath _ = $""Foo⛶Bar"";").Result.CheckLiteralResult("FooBar")); + + // Interpolated string literal with interpolations are not supported and must not provide completion + // Inside interpolated string literal (not closed, at end) + Assert.False(ProvidesFor(@"string aux = ""; NodePath _ = $""Foo{aux}⛶").Result); + // Inside interpolated string literal (not closed, in between) + Assert.False(ProvidesFor(@"string aux = ""; NodePath _ = $""Foo{aux}⛶Bar").Result); + // Inside interpolated string literal (closed, at end) + Assert.False(ProvidesFor(@"string aux = ""; NodePath _ = $""Foo{aux}⛶"";").Result); + // Inside interpolated string literal (closed, in between) + Assert.False(ProvidesFor(@"string aux = ""; NodePath _ = $""Foo{aux}⛶Bar"";").Result); + } + } +} diff --git a/GodotCompletionProviders.Test/Properties/AssemblyInfo.cs b/GodotCompletionProviders.Test/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7612bd5 --- /dev/null +++ b/GodotCompletionProviders.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GodotCompletionProviders.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GodotCompletionProviders.Test")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("B2BAAEA3-8B1D-4584-A5D1-9D4EF487E0DF")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/GodotCompletionProviders.Test/ResourcePathTests.cs b/GodotCompletionProviders.Test/ResourcePathTests.cs new file mode 100644 index 0000000..f272f79 --- /dev/null +++ b/GodotCompletionProviders.Test/ResourcePathTests.cs @@ -0,0 +1,74 @@ +using System.Threading.Tasks; +using Xunit; + +namespace GodotCompletionProviders.Test +{ + [Collection("Sequential")] + public class ResourcePathTests : TestsBase + { + private const string StubCode = @" +namespace Godot +{ + public static class GD + { + public static Resource Load(string path) => throw new NotImplementedException(); + public static T Load(string path) where T : class => throw new NotImplementedException(); + } + + public static class ResourceLoader + { + public static Resource Load(string path, string typeHint = "", bool noCache = false) => throw new NotImplementedException(); + public static T Load(string path, string typeHint = null, bool noCache = false) where T : class => throw new NotImplementedException(); + } +} +"; + + public ResourcePathTests() : base(new ResourcePathCompletionProvider()) + { + } + + private Task ProvidesForFull(string statements) + { + string testCode = $@" +using Godot; +{statements}"; + string code = Utils.ReadSingleCaretTestCode(testCode, out int caretPosition); + return ShouldProvideCompletion(StubCode, code, caretPosition); + } + + private async Task ProvidesFor(string statements) => + (await ProvidesForFull(statements)).ShouldProvideCompletion; + + [Fact] + public void TestNotSomethingElse() + { + Assert.False(ProvidesFor("ResourceLoader.Foo(⛶)").Result); + Assert.False(ProvidesFor("ResourceLoader.Foo(, ⛶)").Result); + Assert.False(ProvidesFor("ResourceLoader.Foo(, , ⛶)").Result); + } + + [Fact] + public void TestResourceLoaderLoad() + { + Assert.True(ProvidesFor("ResourceLoader.Load(⛶").Result); + Assert.True(ProvidesFor("ResourceLoader.Load(⛶)").Result); + Assert.False(ProvidesFor("ResourceLoader.Load(, ⛶").Result); + Assert.False(ProvidesFor("ResourceLoader.Load(, ⛶)").Result); + + // Generic + Assert.True(ProvidesFor("ResourceLoader.Load(⛶)").Result); + } + + [Fact] + public void TestGdLoad() + { + Assert.True(ProvidesFor("GD.Load(⛶").Result); + Assert.True(ProvidesFor("GD.Load(⛶)").Result); + Assert.False(ProvidesFor("GD.Load(, ⛶").Result); + Assert.False(ProvidesFor("GD.Load(, ⛶)").Result); + + // Generic + Assert.True(ProvidesFor("GD.Load(⛶)").Result); + } + } +} diff --git a/GodotCompletionProviders.Test/ScenePathTests.cs b/GodotCompletionProviders.Test/ScenePathTests.cs new file mode 100644 index 0000000..60fae26 --- /dev/null +++ b/GodotCompletionProviders.Test/ScenePathTests.cs @@ -0,0 +1,52 @@ +using System.Threading.Tasks; +using Xunit; + +namespace GodotCompletionProviders.Test +{ + [Collection("Sequential")] + public class ScenePathTests : TestsBase + { + private const string StubCode = @" +namespace Godot +{ + public class SceneTree + { + public Error ChangeScene(string path) => throw new NotImplementedException(); + } +} +"; + + public ScenePathTests() : base(new ScenePathCompletionProvider()) + { + } + + private Task ProvidesForFull(string statements) + { + string testCode = $@" +using Godot; +{statements}"; + string code = Utils.ReadSingleCaretTestCode(testCode, out int caretPosition); + return ShouldProvideCompletion(StubCode, code, caretPosition); + } + + private async Task ProvidesFor(string statements) => + (await ProvidesForFull(statements)).ShouldProvideCompletion; + + [Fact] + public void TestNotSomethingElse() + { + Assert.False(ProvidesFor("((SceneTree)null).Foo(⛶)").Result); + Assert.False(ProvidesFor("((SceneTree)null).Foo(, ⛶)").Result); + Assert.False(ProvidesFor("((SceneTree)null).Foo(, , ⛶)").Result); + } + + [Fact] + public void TestChangeScene() + { + Assert.True(ProvidesFor("((SceneTree)null).ChangeScene(⛶").Result); + Assert.True(ProvidesFor("((SceneTree)null).ChangeScene(⛶)").Result); + Assert.False(ProvidesFor("((SceneTree)null).ChangeScene(, ⛶").Result); + Assert.False(ProvidesFor("((SceneTree)null).ChangeScene(, ⛶)").Result); + } + } +} diff --git a/GodotCompletionProviders.Test/SignalNameTests.cs b/GodotCompletionProviders.Test/SignalNameTests.cs new file mode 100644 index 0000000..99e63b0 --- /dev/null +++ b/GodotCompletionProviders.Test/SignalNameTests.cs @@ -0,0 +1,101 @@ +using System.Threading.Tasks; +using Xunit; + +namespace GodotCompletionProviders.Test +{ + [Collection("Sequential")] + public class SignalNameTests : TestsBase + { + private const string StubCode = @" +namespace Godot +{ + public class StringName + { + public StringName() { } + public StringName(string from) { } + public static implicit operator StringName(string from) => throw new NotImplementedException(); + public static implicit operator string(StringName from) => throw new NotImplementedException(); + } + + public class Object + { + public Error Connect(StringName signal, Callable callable, Godot.Collections.Array binds = null, uint flags = 0) => + throw new NotImplementedException(); + public void Disconnect(StringName signal, Callable callable) => throw new NotImplementedException(); + public bool IsConnected(StringName signal, Callable callable) => throw new NotImplementedException(); + public void EmitSignal(StringName signal, params object[] @args) => throw new NotImplementedException(); + public SignalAwaiter ToSignal(Object source, StringName signal) => throw new NotImplementedException(); + } +} +"; + + public SignalNameTests() : base(new SignalNameCompletionProvider()) + { + } + + private Task ProvidesForFull(string statements) + { + string testCode = $@" +using Godot; +{statements}"; + string code = Utils.ReadSingleCaretTestCode(testCode, out int caretPosition); + return ShouldProvideCompletion(StubCode, code, caretPosition); + } + + private async Task ProvidesFor(string statements) => + (await ProvidesForFull(statements)).ShouldProvideCompletion; + + [Fact] + public void TestNotSomethingElse() + { + Assert.False(ProvidesFor("((Object)null).Foo(⛶)").Result); + Assert.False(ProvidesFor("((Object)null).Foo(, ⛶)").Result); + Assert.False(ProvidesFor("((Object)null).Foo(, , ⛶)").Result); + } + + [Fact] + public void TestConnect() + { + Assert.True(ProvidesFor("((Object)null).Connect(⛶").Result); + Assert.True(ProvidesFor("((Object)null).Connect(⛶)").Result); + Assert.False(ProvidesFor("((Object)null).Connect(, ⛶").Result); + Assert.False(ProvidesFor("((Object)null).Connect(, ⛶)").Result); + } + + [Fact] + public void TestDisconnect() + { + Assert.True(ProvidesFor("((Object)null).Disconnect(⛶").Result); + Assert.True(ProvidesFor("((Object)null).Disconnect(⛶)").Result); + Assert.False(ProvidesFor("((Object)null).Disconnect(, ⛶").Result); + Assert.False(ProvidesFor("((Object)null).Disconnect(, ⛶)").Result); + } + + [Fact] + public void TestIsConnected() + { + Assert.True(ProvidesFor("((Object)null).IsConnected(⛶").Result); + Assert.True(ProvidesFor("((Object)null).IsConnected(⛶)").Result); + Assert.False(ProvidesFor("((Object)null).IsConnected(, ⛶").Result); + Assert.False(ProvidesFor("((Object)null).IsConnected(, ⛶)").Result); + } + + [Fact] + public void TestEmitSignal() + { + Assert.True(ProvidesFor("((Object)null).EmitSignal(⛶").Result); + Assert.True(ProvidesFor("((Object)null).EmitSignal(⛶)").Result); + Assert.False(ProvidesFor("((Object)null).EmitSignal(, ⛶").Result); + Assert.False(ProvidesFor("((Object)null).EmitSignal(, ⛶)").Result); + } + + [Fact] + public void TestToSignal() + { + Assert.True(ProvidesFor("((Object)null).ToSignal(, ⛶").Result); + Assert.True(ProvidesFor("((Object)null).ToSignal(, ⛶)").Result); + Assert.False(ProvidesFor("((Object)null).ToSignal(⛶").Result); + Assert.False(ProvidesFor("((Object)null).ToSignal(⛶)").Result); + } + } +} diff --git a/GodotCompletionProviders.Test/TestsBase.cs b/GodotCompletionProviders.Test/TestsBase.cs new file mode 100644 index 0000000..2a0af67 --- /dev/null +++ b/GodotCompletionProviders.Test/TestsBase.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Text; +using Xunit; + +namespace GodotCompletionProviders.Test +{ + public abstract class TestsBase + { + private readonly BaseCompletionProvider _completionProvider; + + protected TestsBase(BaseCompletionProvider completionProvider) + { + _completionProvider = completionProvider; + } + + // ReSharper disable once MemberCanBeMadeStatic.Global + protected Task ShouldProvideCompletion(string stubCode, string testCode, int caretPosition) + { + var host = MefHostServices.Create(MefHostServices.DefaultAssemblies); + Assert.NotNull(host); + var workspace = new AdhocWorkspace(host); + + var projectId = ProjectId.CreateNewId(); + + var stubDocumentInfo = DocumentInfo.Create( + DocumentId.CreateNewId(projectId), "Stub.cs", sourceCodeKind: SourceCodeKind.Regular, + loader: TextLoader.From(TextAndVersion.Create(SourceText.From(stubCode), VersionStamp.Create()))); + + var testDocumentInfo = DocumentInfo.Create( + DocumentId.CreateNewId(projectId), "TestFile.cs", sourceCodeKind: SourceCodeKind.Script, + loader: TextLoader.From(TextAndVersion.Create(SourceText.From(testCode), VersionStamp.Create()))); + + var projectInfo = ProjectInfo + .Create(projectId, VersionStamp.Create(), "TestProject", "TestProject", LanguageNames.CSharp) + .WithMetadataReferences(new[] {MetadataReference.CreateFromFile(typeof(object).Assembly.Location)}) + .WithDocuments(new[] {stubDocumentInfo, testDocumentInfo}); + var project = workspace.AddProject(projectInfo); + + var testDocument = project.GetDocument(testDocumentInfo.Id); + + return _completionProvider.ShouldProvideCompletion(testDocument, caretPosition); + } + } +} diff --git a/GodotCompletionProviders.Test/Utils.cs b/GodotCompletionProviders.Test/Utils.cs new file mode 100644 index 0000000..803e523 --- /dev/null +++ b/GodotCompletionProviders.Test/Utils.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; + +namespace GodotCompletionProviders.Test +{ + public static class Utils + { + private static int IndexOfAny(this string str, char[] anyOf, out char which) + { + for (int i = 0; i < str.Length; i++) + { + char c = str[i]; + + foreach (char charInAnyOf in anyOf) + { + if (c == charInAnyOf) + { + which = c; + return i; + } + } + } + + which = default; + return -1; + } + + public static string ReadMultiCaretTestCode(string testCode, ICollection mustPassCaretPositions, ICollection mustNotPassCaretPositions) + { + string code = testCode; + + const char mustPassChar = '✔'; + const char mustNotPassChar = '✘'; + + int indexOfCaret; + while ((indexOfCaret = code.IndexOfAny(new[] {mustPassChar, mustNotPassChar}, out char which)) >= 0) + { + (which == mustPassChar ? mustPassCaretPositions : mustNotPassCaretPositions).Add(indexOfCaret); + code = code.Remove(indexOfCaret, 1); + } + + return code; + } + + public static string ReadSingleCaretTestCode(string testCode, out int caretPosition) + { + const char caretChar = '⛶'; + + string code = testCode; + + caretPosition = code.IndexOf(caretChar); + + if (caretPosition >= 0) + code = code.Remove(caretPosition, 1); + + return code; + } + + public static bool CheckLiteralResult(this BaseCompletionProvider.CheckResult result, string expected) + { + if (!result.ShouldProvideCompletion) + return false; + + if (result.StringSyntax == null) + return false; + + return result.StringSyntaxValue is string strValue && strValue == expected; + } + } +} diff --git a/GodotCompletionProviders.Test/packages.config b/GodotCompletionProviders.Test/packages.config new file mode 100644 index 0000000..254a486 --- /dev/null +++ b/GodotCompletionProviders.Test/packages.config @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GodotCompletionProviders/BaseCompletionProvider.cs b/GodotCompletionProviders/BaseCompletionProvider.cs new file mode 100644 index 0000000..a3eeac1 --- /dev/null +++ b/GodotCompletionProviders/BaseCompletionProvider.cs @@ -0,0 +1,150 @@ +using System.Collections.Immutable; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Completion; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Text; + +namespace GodotCompletionProviders +{ + public abstract class BaseCompletionProvider : CompletionProvider + { + private readonly CompletionKind _kind; + private readonly string _inlineDescription; + + // No idea how else to pass this as we don't create the provider instance + // ReSharper disable once UnassignedField.Global + // ReSharper disable once MemberCanBePrivate.Global + public static IProviderContext Context; + + protected BaseCompletionProvider(CompletionKind kind, string inlineDescription) + { + _kind = kind; + _inlineDescription = inlineDescription; + } + + public struct CheckResult + { + public bool ShouldProvideCompletion; + public SyntaxNode StringSyntax; + + public object StringSyntaxValue => RoslynUtils.GetStringSyntaxValue(StringSyntax); + + public static CheckResult False() => new CheckResult {ShouldProvideCompletion = false}; + + public static CheckResult True(SyntaxNode stringSyntax) => + new CheckResult {ShouldProvideCompletion = true, StringSyntax = stringSyntax}; + } + + public abstract Task ShouldProvideCompletion(Document document, int position); + + // ReSharper disable once VirtualMemberNeverOverridden.Global + protected virtual async Task ShouldProvideCompletion(CompletionContext context) + { + var document = context.Document; + + if (document == null) + return CheckResult.False(); + + return await ShouldProvideCompletion(document, context.Position); + } + + // ReSharper disable once VirtualMemberNeverOverridden.Global + protected virtual bool ShouldTriggerCompletion() + { + if (Context == null) + return false; + + return Context.AreCompletionsEnabledFor(_kind) && Context.CanRequestCompletionsFromServer(); + } + + public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options) => + ShouldTriggerCompletion(); + + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + if (!ShouldTriggerCompletion()) + return; + + var checkResult = await ShouldProvideCompletion(context); + + if (!checkResult.ShouldProvideCompletion) + return; + + string scriptFile = Path.GetFullPath(context.Document.FilePath); + + var suggestions = await Context.RequestCompletion(_kind, scriptFile); + + if (suggestions.Length == 0) + return; + + ImmutableDictionary properties = null; + + if (checkResult.StringSyntax != null) + { + var propertiesBuilder = ImmutableDictionary.CreateBuilder(); + propertiesBuilder.Add("GodotSpan.Start", checkResult.StringSyntax.Span.Start.ToString()); + + // TODO: + // This is commented out because of cases like the following: `Foo("Bar$$, 10, "Baz");` + // Instead of replacing only `"Bar` it would replace `"Bar, 10, "`. It gets even worse + // with verbatim string literals which can be multiline. Unless we can find a way to + // avoid this, it's better to only replace up to the caret position, even if that means + // something like `Foo("Bar$$Baz")` will result in `Foo("BarINSERTED"Baz"). + // + // propertiesBuilder.Add("GodotSpan.Length", checkResult.StringSyntax.Span.Length.ToString()); + + propertiesBuilder.Add("GodotSpan.Length", (context.Position - checkResult.StringSyntax.Span.Start).ToString()); + + properties = propertiesBuilder.ToImmutable(); + } + + foreach (string suggestion in suggestions) + { + var completionItem = CompletionItem.Create( + displayText: suggestion, + filterText: null, + sortText: null, + properties: properties, + tags: ImmutableArray.Empty, + rules: null, + displayTextPrefix: null, + displayTextSuffix: null, + inlineDescription: _inlineDescription + ); + context.AddItem(completionItem); + } + } + + public override Task GetChangeAsync( + Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken) + { + int? spanStart = null; + int? spanLength = null; + + if (item.Properties.TryGetValue("GodotSpan.Start", out string spanStartStr)) + { + if (int.TryParse(spanStartStr, out int startResult)) + { + spanStart = startResult; + } + + if (item.Properties.TryGetValue("GodotSpan.Length", out string spanLengthStr)) + { + if (int.TryParse(spanLengthStr, out int lengthResult)) + { + spanLength = lengthResult; + } + } + } + + var span = spanStart.HasValue && spanLength.HasValue ? + new TextSpan(spanStart.Value, spanLength.Value) : + item.Span; + + return Task.FromResult(CompletionChange.Create(new TextChange(span, item.DisplayText))); + } + } +} diff --git a/GodotCompletionProviders/CompletionKind.cs b/GodotCompletionProviders/CompletionKind.cs new file mode 100644 index 0000000..64622e0 --- /dev/null +++ b/GodotCompletionProviders/CompletionKind.cs @@ -0,0 +1,16 @@ +namespace GodotCompletionProviders +{ + public enum CompletionKind + { + InputActions = 0, + NodePaths, + ResourcePaths, + ScenePaths, + ShaderParams, + Signals, + ThemeColors, + ThemeConstants, + ThemeFonts, + ThemeStyles + } +} diff --git a/GodotCompletionProviders/GodotCompletionProviders.csproj b/GodotCompletionProviders/GodotCompletionProviders.csproj new file mode 100644 index 0000000..30be006 --- /dev/null +++ b/GodotCompletionProviders/GodotCompletionProviders.csproj @@ -0,0 +1,26 @@ + + + netstandard2.0 + 8 + GodotCompletionProviders + 1.0.0 + $(Version) + Godot Engine contributors + + godot + https://github.com/godotengine/godot-csharp-visualstudio/tree/master/GodotCompletionProviders + MIT + + Set of Roslyn C# code completion providers for Godot. + + These providers alone don't actually provide the suggestions. IProviderContext must be implemented and set in BaseCompletionProvider.Context for that. + + + + + + + diff --git a/GodotCompletionProviders/ILogger.cs b/GodotCompletionProviders/ILogger.cs new file mode 100644 index 0000000..ec1dd14 --- /dev/null +++ b/GodotCompletionProviders/ILogger.cs @@ -0,0 +1,13 @@ +using System; + +namespace GodotCompletionProviders +{ + public interface ILogger + { + void LogDebug(string message); + void LogInfo(string message); + void LogWarning(string message); + void LogError(string message); + void LogError(string message, Exception e); + } +} diff --git a/GodotCompletionProviders/IProviderContext.cs b/GodotCompletionProviders/IProviderContext.cs new file mode 100644 index 0000000..7098bf5 --- /dev/null +++ b/GodotCompletionProviders/IProviderContext.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace GodotCompletionProviders +{ + public interface IProviderContext + { + ILogger GetLogger(); + bool AreCompletionsEnabledFor(CompletionKind completionKind); + bool CanRequestCompletionsFromServer(); + Task RequestCompletion(CompletionKind completionKind, string absoluteFilePath); + } +} diff --git a/GodotCompletionProviders/InputActionCompletionProvider.cs b/GodotCompletionProviders/InputActionCompletionProvider.cs new file mode 100644 index 0000000..17128c9 --- /dev/null +++ b/GodotCompletionProviders/InputActionCompletionProvider.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Completion; + +namespace GodotCompletionProviders +{ + [ExportCompletionProvider(nameof(InputActionCompletionProvider), LanguageNames.CSharp)] + public class InputActionCompletionProvider : SpecificInvocationCompletionProvider + { + // TODO: Support offline (not connected to a Godot editor) completion of input actions (parse godot.project). + + private static readonly IEnumerable ExpectedInvocations = new[] + { + new ExpectedInvocation {MethodContainingType = InputType, MethodName = "IsActionPressed", ArgumentIndex = 0, ArgumentTypes = StringTypes}, + new ExpectedInvocation {MethodContainingType = InputType, MethodName = "IsActionJustPressed", ArgumentIndex = 0, ArgumentTypes = StringTypes}, + new ExpectedInvocation {MethodContainingType = InputType, MethodName = "IsActionJustReleased", ArgumentIndex = 0, ArgumentTypes = StringTypes}, + new ExpectedInvocation {MethodContainingType = InputType, MethodName = "GetActionStrength", ArgumentIndex = 0, ArgumentTypes = StringTypes}, + new ExpectedInvocation {MethodContainingType = InputType, MethodName = "ActionPress", ArgumentIndex = 0, ArgumentTypes = StringTypes}, + new ExpectedInvocation {MethodContainingType = InputType, MethodName = "ActionRelease", ArgumentIndex = 0, ArgumentTypes = StringTypes} + }; + + public InputActionCompletionProvider() : base(ExpectedInvocations, CompletionKind.InputActions, "InputAction") + { + } + } +} diff --git a/GodotCompletionProviders/NodePathCompletionProvider.cs b/GodotCompletionProviders/NodePathCompletionProvider.cs new file mode 100644 index 0000000..4074f86 --- /dev/null +++ b/GodotCompletionProviders/NodePathCompletionProvider.cs @@ -0,0 +1,111 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Completion; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace GodotCompletionProviders +{ + [ExportCompletionProvider(nameof(NodePathCompletionProvider), LanguageNames.CSharp)] + public class NodePathCompletionProvider : BaseCompletionProvider + { + // TODO: If generic GetNode, filter by type + + public NodePathCompletionProvider() : base(CompletionKind.NodePaths, "NodePath") + { + } + + public override async Task ShouldProvideCompletion(Document document, int position) + { + if (!document.SupportsSyntaxTree || !document.SupportsSemanticModel) + return CheckResult.False(); + + var syntaxRoot = await document.GetSyntaxRootAsync(); + + if (syntaxRoot == null) + return CheckResult.False(); + + var semanticModel = await document.GetSemanticModelAsync(); + + if (semanticModel == null) + return CheckResult.False(); + + // Walk up and save literal expression if present (we can autocomplete literals). + var currentToken = syntaxRoot.FindToken(position - 1); + var currentNode = currentToken.Parent; + var literalExpression = RoslynUtils.WalkUpStringSyntaxOnce(ref currentNode, ref position); + + // Walk up parenthesis because the inference service doesn't handle that. + currentToken = syntaxRoot.FindToken(position - 1); + currentNode = currentToken.Parent; + if (currentToken.Kind() != SyntaxKind.CloseParenToken) + RoslynUtils.WalkUpParenthesisExpressions(ref currentNode, ref position); + + var inferredTypes = RoslynUtils.InferTypes(semanticModel, position, null, CancellationToken.None); + + if (inferredTypes.Any(RoslynUtils.TypeIsNodePath)) + return CheckResult.True(literalExpression); + + // Our own custom inference for NodePath + + if (IsPathConstructorArgumentOfNodePath(syntaxRoot, semanticModel, currentNode, position)) + return CheckResult.True(literalExpression); + + if (IsParenthesizedExprActuallyCastToNodePath(semanticModel, currentNode)) + return CheckResult.True(literalExpression); + + return CheckResult.False(); + } + + private static bool IsPathConstructorArgumentOfNodePath(SyntaxNode syntaxRoot, SemanticModel semanticModel, SyntaxNode currentNode, int position) + { + // new NodePath($$) for NodePath(string) ctor + + if (!(currentNode is ArgumentListSyntax argumentList && currentNode.Parent is ObjectCreationExpressionSyntax objectCreation)) + return false; + + var previousToken = syntaxRoot.FindToken(position - 1); + + if (previousToken != argumentList.OpenParenToken) + return false; + + if (argumentList.Arguments.Count > 1) + return false; // The NodePath constructor we are looking for has only one parameter + + int index = RoslynUtils.GetArgumentListIndex(argumentList, previousToken); + + var info = semanticModel.GetSymbolInfo(objectCreation.Type); + + if (!(info.Symbol is INamedTypeSymbol type)) + return false; + + if (type.TypeKind == TypeKind.Delegate) + return false; + + if (!RoslynUtils.TypeIsNodePath(type)) + return false; + + var constructors = type.InstanceConstructors.Where(m => m.Parameters.Length == 1); + var types = RoslynUtils.InferTypeInArgument(index, constructors.Select(m => m.Parameters), argumentOpt: null); + + return types.Any(RoslynUtils.TypeIsString); + } + + private static bool IsParenthesizedExprActuallyCastToNodePath(SemanticModel semanticModel, SyntaxNode currentNode) + { + // (NodePath)$$ which is detected as a parenthesized expression rather than a cast + + if (!(currentNode is ParenthesizedExpressionSyntax parenthesizedExpression)) + return false; + + if (!(parenthesizedExpression.Expression is IdentifierNameSyntax identifierNameSyntax)) + return false; + + var typeInfo = semanticModel.GetTypeInfo(identifierNameSyntax).Type; + + return typeInfo != null && RoslynUtils.TypeIsNodePath(typeInfo); + } + } +} diff --git a/GodotCompletionProviders/ResourcePathCompletionProvider.cs b/GodotCompletionProviders/ResourcePathCompletionProvider.cs new file mode 100644 index 0000000..09da095 --- /dev/null +++ b/GodotCompletionProviders/ResourcePathCompletionProvider.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Completion; + +namespace GodotCompletionProviders +{ + [ExportCompletionProvider(nameof(ResourcePathCompletionProvider), LanguageNames.CSharp)] + public class ResourcePathCompletionProvider : SpecificInvocationCompletionProvider + { + // TODO: If generic Load, filter by type + // TODO: Support offline (not connected to a Godot editor) completion of resource paths (from the file system). + + private static readonly IEnumerable ExpectedInvocations = new[] + { + new ExpectedInvocation {MethodContainingType = GdType, MethodName = "Load", ArgumentIndex = 0, ArgumentTypes = StringTypes}, + new ExpectedInvocation {MethodContainingType = ResourceLoaderType, MethodName = "Load", ArgumentIndex = 0, ArgumentTypes = StringTypes} + }; + + public ResourcePathCompletionProvider() : base(ExpectedInvocations, CompletionKind.ResourcePaths, "Resource") + { + } + } +} diff --git a/GodotCompletionProviders/RoslynUtils.cs b/GodotCompletionProviders/RoslynUtils.cs new file mode 100644 index 0000000..dc1eb3e --- /dev/null +++ b/GodotCompletionProviders/RoslynUtils.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace GodotCompletionProviders +{ + internal static class RoslynUtils + { + public static bool TypeIsNodePath(ITypeSymbol type) => + type.Name == "NodePath" && type.ContainingNamespace.Name == "Godot"; + + public static bool TypeIsString(ITypeSymbol type) => + type.Name == "String" && type.ContainingNamespace.Name == "System"; + + public static void WalkUpParenthesisExpressions(ref SyntaxNode currentNode, ref int position) + { + while (currentNode.Kind() == SyntaxKind.ParenthesizedExpression) + { + position = currentNode.SpanStart; + currentNode = currentNode.Parent; + } + } + + private static object StringSyntaxValueFromInterpolated(InterpolatedStringExpressionSyntax interpolatedStringExpression) + { + if (interpolatedStringExpression.Contents.Count > 1) + return null; + + if (interpolatedStringExpression.Contents.Count == 1) + { + if (interpolatedStringExpression.Contents[0] is InterpolatedStringTextSyntax interpolatedStringTextSyntax) + return interpolatedStringTextSyntax.TextToken.Value; + } + + return ""; + } + + private static object StringSyntaxValueFromInterpolated(InterpolatedStringTextSyntax interpolatedStringText) => + interpolatedStringText.Parent is InterpolatedStringExpressionSyntax interpolatedStringExpression ? + StringSyntaxValueFromInterpolated(interpolatedStringExpression) : + null; + + public static object GetStringSyntaxValue(SyntaxNode stringSyntax) + { + return stringSyntax switch + { + LiteralExpressionSyntax literalExpression => literalExpression.Token.Kind() == SyntaxKind.StringLiteralToken ? literalExpression.Token.Value : null, + InterpolatedStringTextSyntax interpolatedStringText => StringSyntaxValueFromInterpolated(interpolatedStringText), + InterpolatedStringExpressionSyntax interpolatedStringExpression => StringSyntaxValueFromInterpolated(interpolatedStringExpression), + _ => null + }; + } + + private static SyntaxNode StringSyntaxFromInterpolated(InterpolatedStringExpressionSyntax interpolatedStringExpression) + { + if (interpolatedStringExpression.Contents.Count > 1) + return null; + + if (interpolatedStringExpression.Contents.Count == 1) + { + if (!(interpolatedStringExpression.Contents[0] is InterpolatedStringTextSyntax)) + return null; + } + + return interpolatedStringExpression; + } + + private static SyntaxNode StringSyntaxFromInterpolated(InterpolatedStringTextSyntax interpolatedStringText) => + interpolatedStringText.Parent is InterpolatedStringExpressionSyntax interpolatedStringExpression ? + StringSyntaxFromInterpolated(interpolatedStringExpression) : + null; + + public static SyntaxNode WalkUpStringSyntaxOnce(ref SyntaxNode currentNode, ref int position) + { + var result = currentNode switch + { + LiteralExpressionSyntax literalExpression => literalExpression.Token.Kind() == SyntaxKind.StringLiteralToken ? literalExpression : null, + InterpolatedStringTextSyntax interpolatedStringText => StringSyntaxFromInterpolated(interpolatedStringText), + InterpolatedStringExpressionSyntax interpolatedStringExpression => StringSyntaxFromInterpolated(interpolatedStringExpression), + _ => null + }; + + if (result != null) + position = result.SpanStart; + + return result; + } + + // Borrowed from Roslyn + private static SyntaxToken GetOpenToken(BaseArgumentListSyntax node) + { + if (node == null) + return default; + + return node.Kind() switch + { + SyntaxKind.ArgumentList => ((ArgumentListSyntax)node).OpenParenToken, + SyntaxKind.BracketedArgumentList => ((BracketedArgumentListSyntax)node).OpenBracketToken, + _ => default + }; + } + + // Borrowed from Roslyn + public static int GetArgumentListIndex(BaseArgumentListSyntax argumentList, SyntaxToken previousToken) + { + if (previousToken == GetOpenToken(argumentList)) + return 0; + + int tokenIndex = argumentList.Arguments.GetWithSeparators().IndexOf(previousToken); + return (tokenIndex + 1) / 2; + } + + // Borrowed from Roslyn + private static RefKind GetRefKind(this ArgumentSyntax argument) + { + switch (argument?.RefKindKeyword.Kind()) + { + case SyntaxKind.RefKeyword: + return RefKind.Ref; + case SyntaxKind.OutKeyword: + return RefKind.Out; + case SyntaxKind.InKeyword: + return RefKind.In; + default: + return RefKind.None; + } + } + + // Borrowed from Roslyn + internal static IEnumerable InferTypeInArgument( + int index, + IEnumerable> parameterizedSymbols, + ArgumentSyntax argumentOpt) + { + var name = argumentOpt != null && argumentOpt.NameColon != null ? argumentOpt.NameColon.Name.Identifier.ValueText : null; + var refKind = argumentOpt.GetRefKind(); + return InferTypeInArgument(index, parameterizedSymbols, name, refKind); + } + + // Borrowed from Roslyn + private static IEnumerable InferTypeInArgument( + int index, + IEnumerable> parameterizedSymbols, + string name, + RefKind refKind) + { + // If the callsite has a named argument, then try to find a method overload that has a + // parameter with that name. If we can find one, then return the type of that one. + if (name != null) + { + var matchingNameParameters = parameterizedSymbols.SelectMany(m => m) + .Where(p => p.Name == name) + .Select(p => p.Type); + + return matchingNameParameters; + } + + var allParameters = new List(); + var matchingRefParameters = new List(); + + foreach (var parameterSet in parameterizedSymbols) + { + if (index < parameterSet.Length) + { + var parameter = parameterSet[index]; + allParameters.Add(parameter.Type); + + if (parameter.RefKind == refKind) + { + matchingRefParameters.Add(parameter.Type); + } + } + } + + return matchingRefParameters.Count > 0 ? matchingRefParameters.ToImmutableArray() : allParameters.ToImmutableArray(); + } + + // Borrowed from Roslyn + private static ImmutableArray GetBestOrAllSymbols(this SymbolInfo info) + { + if (info.Symbol != null) + return ImmutableArray.Create(info.Symbol); + + if (info.CandidateSymbols.Length > 0) + return info.CandidateSymbols; + + return ImmutableArray.Empty; + } + + public static bool IsExpectedInvocationArgument(SemanticModel semanticModel, SyntaxToken previousToken, + InvocationExpressionSyntax invocation, ArgumentListSyntax argumentList, + IEnumerable expectedInvocations) + { + if (previousToken != argumentList.OpenParenToken && previousToken.Kind() != SyntaxKind.CommaToken) + return false; + + // ReSharper disable PossibleMultipleEnumeration + + expectedInvocations = expectedInvocations.Where(ei => argumentList.Arguments.Count <= ei.ArgumentIndex + 1); + + if (!expectedInvocations.Any()) + return false; + + int index = GetArgumentListIndex(argumentList, previousToken); + + expectedInvocations = expectedInvocations.Where(ei => index == ei.ArgumentIndex); + + if (!expectedInvocations.Any()) + return false; + + var info = semanticModel.GetSymbolInfo(invocation); + var methods = info.GetBestOrAllSymbols().OfType(); + + if (info.Symbol == null) + { + var memberGroupMethods = semanticModel.GetMemberGroup(invocation.Expression).OfType(); + methods = methods.Concat(memberGroupMethods).Distinct(); + } + + foreach (var expected in expectedInvocations) + { + var filteredMethods = methods.Where(m => + m.ContainingType.ContainingNamespace.Name == expected.MethodContainingType.Namespace && + m.ContainingType.Name == expected.MethodContainingType.Name && + m.Name == expected.MethodName); + + var types = InferTypeInArgument(index, filteredMethods.Select(m => m.Parameters), argumentOpt: null); + + if (types.Any(t => expected.ArgumentTypes + .Any(at => t.Name == at.Name && t.ContainingNamespace.Name == at.Namespace))) + { + return true; + } + } + + return false; + + // ReSharper restore PossibleMultipleEnumeration + } + + private static Type _inferenceServiceType; + private static object _inferenceService; + private static MethodInfo _inferTypesMethod; + + internal static ImmutableArray InferTypes( + SemanticModel semanticModel, int position, + string nameOpt, CancellationToken cancellationToken) + { + // I know, I know... Don't look at me like that >_> + const string inferenceServiceTypeQualifiedName = + "Microsoft.CodeAnalysis.CSharp.CSharpTypeInferenceService, Microsoft.CodeAnalysis.CSharp.Workspaces"; + _inferenceServiceType ??= Type.GetType(inferenceServiceTypeQualifiedName, throwOnError: true); + _inferenceService ??= Activator.CreateInstance(_inferenceServiceType); + _inferTypesMethod ??= _inferenceServiceType.GetMethod("InferTypes", + new[] {typeof(SemanticModel), typeof(int), typeof(string), typeof(CancellationToken)}); + + if (_inferTypesMethod == null) + throw new MissingMethodException("Couldn't find InferTypes"); + + return (ImmutableArray)_inferTypesMethod.Invoke(_inferenceService, + new object[] {semanticModel, position, null, CancellationToken.None}); + } + } +} diff --git a/GodotCompletionProviders/ScenePathCompletionProvider.cs b/GodotCompletionProviders/ScenePathCompletionProvider.cs new file mode 100644 index 0000000..32861ca --- /dev/null +++ b/GodotCompletionProviders/ScenePathCompletionProvider.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Completion; + +namespace GodotCompletionProviders +{ + [ExportCompletionProvider(nameof(ScenePathCompletionProvider), LanguageNames.CSharp)] + public class ScenePathCompletionProvider : SpecificInvocationCompletionProvider + { + // TODO: Support offline (not connected to a Godot editor) completion of scene paths (from the file system). + + private static readonly IEnumerable ExpectedInvocations = new[] + { + new ExpectedInvocation {MethodContainingType = SceneTreeType, MethodName = "ChangeScene", ArgumentIndex = 0, ArgumentTypes = StringTypes} + }; + + public ScenePathCompletionProvider() : base(ExpectedInvocations, CompletionKind.ScenePaths, "Scene") + { + } + } +} diff --git a/GodotCompletionProviders/SignalNameCompletionProvider.cs b/GodotCompletionProviders/SignalNameCompletionProvider.cs new file mode 100644 index 0000000..98f9682 --- /dev/null +++ b/GodotCompletionProviders/SignalNameCompletionProvider.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Completion; + +namespace GodotCompletionProviders +{ + [ExportCompletionProvider(nameof(SignalNameCompletionProvider), LanguageNames.CSharp)] + public class SignalNameCompletionProvider : SpecificInvocationCompletionProvider + { + private static readonly IEnumerable ExpectedInvocations = new[] + { + new ExpectedInvocation {MethodContainingType = GodotObjectType, MethodName = "Connect", ArgumentIndex = 0, ArgumentTypes = StringTypes}, + new ExpectedInvocation {MethodContainingType = GodotObjectType, MethodName = "Disconnect", ArgumentIndex = 0, ArgumentTypes = StringTypes}, + new ExpectedInvocation {MethodContainingType = GodotObjectType, MethodName = "IsConnected", ArgumentIndex = 0, ArgumentTypes = StringTypes}, + new ExpectedInvocation {MethodContainingType = GodotObjectType, MethodName = "EmitSignal", ArgumentIndex = 0, ArgumentTypes = StringTypes}, + new ExpectedInvocation {MethodContainingType = GodotObjectType, MethodName = "ToSignal", ArgumentIndex = 1, ArgumentTypes = StringTypes}, + }; + + public SignalNameCompletionProvider() : base(ExpectedInvocations, CompletionKind.Signals, "Signal") + { + } + } +} diff --git a/GodotCompletionProviders/SpecificInvocationCompletionProvider.cs b/GodotCompletionProviders/SpecificInvocationCompletionProvider.cs new file mode 100644 index 0000000..0bf56c9 --- /dev/null +++ b/GodotCompletionProviders/SpecificInvocationCompletionProvider.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace GodotCompletionProviders +{ + public abstract class SpecificInvocationCompletionProvider : BaseCompletionProvider + { + internal static readonly TypeName GodotObjectType = new TypeName {Namespace = "Godot", Name = "Object"}; + internal static readonly TypeName SceneTreeType = new TypeName {Namespace = "Godot", Name = "SceneTree"}; + internal static readonly TypeName GdType = new TypeName {Namespace = "Godot", Name = "GD"}; + internal static readonly TypeName ResourceLoaderType = new TypeName {Namespace = "Godot", Name = "ResourceLoader"}; + internal static readonly TypeName InputType = new TypeName {Namespace = "Godot", Name = "Input"}; + internal static readonly TypeName StringNameType = new TypeName {Namespace = "Godot", Name = "StringName"}; + internal static readonly TypeName StringType = new TypeName {Namespace = "System", Name = "String"}; + + internal static readonly IEnumerable StringTypes = new[] {StringNameType, StringType}; + + public struct TypeName + { + public string Namespace; + public string Name; + } + + public struct ExpectedInvocation + { + public TypeName MethodContainingType; + public string MethodName; + public int ArgumentIndex; + public IEnumerable ArgumentTypes; + } + + private readonly IEnumerable _expectedInvocations; + + protected SpecificInvocationCompletionProvider(IEnumerable expectedInvocations, CompletionKind kind, string inlineDescription) : base(kind, inlineDescription) + { + _expectedInvocations = expectedInvocations; + } + + public override async Task ShouldProvideCompletion(Document document, int position) + { + if (!document.SupportsSyntaxTree || !document.SupportsSemanticModel) + return CheckResult.False(); + + var syntaxRoot = await document.GetSyntaxRootAsync(); + + if (syntaxRoot == null) + return CheckResult.False(); + + var semanticModel = await document.GetSemanticModelAsync(); + + if (semanticModel == null) + return CheckResult.False(); + + // Walk up and save literal expression if present (we can autocomplete literals). + var currentToken = syntaxRoot.FindToken(position - 1); + var currentNode = currentToken.Parent; + var literalExpression = RoslynUtils.WalkUpStringSyntaxOnce(ref currentNode, ref position); + + // Walk up parenthesis because the inference service doesn't handle that. + currentToken = syntaxRoot.FindToken(position - 1); + currentNode = currentToken.Parent; + if (currentToken.Kind() != SyntaxKind.CloseParenToken) + RoslynUtils.WalkUpParenthesisExpressions(ref currentNode, ref position); + + if (!(currentNode is ArgumentListSyntax argumentList && currentNode.Parent is InvocationExpressionSyntax invocation)) + return CheckResult.False(); + + var previousToken = syntaxRoot.FindToken(position - 1); + + if (previousToken != argumentList.OpenParenToken && previousToken.Kind() != SyntaxKind.CommaToken) + return CheckResult.False(); + + if (RoslynUtils.IsExpectedInvocationArgument(semanticModel, previousToken, invocation, argumentList, _expectedInvocations)) + return CheckResult.True(literalExpression); + + return CheckResult.False(); + } + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..feeb8a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Godot Engine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..509dd87 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,10 @@ + + + + + + + + + +