BsVSCodeEditor.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. #include "Win32/BsVSCodeEditor.h"
  2. #include <windows.h>
  3. #include <atlbase.h>
  4. #include "BsFileSystem.h"
  5. #include "BsDataStream.h"
  6. // Import EnvDTE
  7. #pragma warning(disable: 4278)
  8. #import "libid:80cc9f66-e7d8-4ddd-85b6-d9e6cd0e93e2" version("8.0") lcid("0") raw_interfaces_only named_guids
  9. #pragma warning(default: 4278)
  10. namespace BansheeEngine
  11. {
  12. LONG getRegistryStringValue(HKEY hKey, const WString& name, WString& value, const WString& defaultValue)
  13. {
  14. value = defaultValue;
  15. wchar_t strBuffer[512];
  16. DWORD strBufferSize = sizeof(strBuffer);
  17. ULONG result = RegQueryValueExW(hKey, name.c_str(), 0, nullptr, (LPBYTE)strBuffer, &strBufferSize);
  18. if (result == ERROR_SUCCESS)
  19. value = strBuffer;
  20. return result;
  21. }
  22. struct VSProjectInfo
  23. {
  24. WString GUID;
  25. WString name;
  26. Path path;
  27. };
  28. class VisualStudio
  29. {
  30. private:
  31. static const String SLN_TEMPLATE;
  32. static const String PROJ_ENTRY_TEMPLATE;
  33. static const String PROJ_PLATFORM_TEMPLATE;
  34. static const String PROJ_TEMPLATE;
  35. static const String REFERENCE_ENTRY_TEMPLATE;
  36. static const String REFERENCE_PROJECT_ENTRY_TEMPLATE;
  37. static const String REFERENCE_PATH_ENTRY_TEMPLATE;
  38. static const String CODE_ENTRY_TEMPLATE;
  39. static const String NON_CODE_ENTRY_TEMPLATE;
  40. public:
  41. static CComPtr<EnvDTE::_DTE> findRunningInstance(const CLSID& clsID, const Path& solutionPath)
  42. {
  43. CComPtr<IRunningObjectTable> runningObjectTable = nullptr;
  44. if (FAILED(GetRunningObjectTable(0, &runningObjectTable)))
  45. return nullptr;
  46. CComPtr<IEnumMoniker> enumMoniker = nullptr;
  47. if (FAILED(runningObjectTable->EnumRunning(&enumMoniker)))
  48. return nullptr;
  49. CComPtr<IMoniker> dteMoniker = nullptr;
  50. if (FAILED(CreateClassMoniker(clsID, &dteMoniker)))
  51. return nullptr;
  52. CComBSTR bstrSolution(solutionPath.toWString(Path::PathType::Windows).c_str());
  53. CComPtr<IMoniker> moniker;
  54. ULONG count = 0;
  55. while (enumMoniker->Next(1, &moniker, &count) == S_OK)
  56. {
  57. if (moniker->IsEqual(dteMoniker))
  58. {
  59. CComPtr<IUnknown> curObject = nullptr;
  60. HRESULT result = runningObjectTable->GetObject(moniker, &curObject);
  61. moniker = nullptr;
  62. if (result != S_OK)
  63. continue;
  64. CComPtr<EnvDTE::_DTE> dte;
  65. curObject->QueryInterface(__uuidof(EnvDTE::_DTE), (void**)&dte);
  66. if (dte == nullptr)
  67. return nullptr;
  68. CComPtr<EnvDTE::_Solution> solution;
  69. if (FAILED(dte->get_Solution(&solution)))
  70. continue;
  71. CComBSTR fullName;
  72. if (FAILED(solution->get_FullName(&fullName)))
  73. continue;
  74. if (fullName == bstrSolution)
  75. return dte;
  76. }
  77. }
  78. return nullptr;
  79. }
  80. static CComPtr<EnvDTE::_DTE> openInstance(const CLSID& clsid, const Path& solutionPath)
  81. {
  82. CComPtr<IUnknown> newInstance = nullptr;
  83. if (FAILED(::CoCreateInstance(clsid, nullptr, CLSCTX_LOCAL_SERVER, EnvDTE::IID__DTE, (LPVOID*)&newInstance)))
  84. return nullptr;
  85. CComPtr<EnvDTE::_DTE> dte;
  86. newInstance->QueryInterface(__uuidof(EnvDTE::_DTE), (void**)&dte);
  87. if (dte == nullptr)
  88. return nullptr;
  89. dte->put_UserControl(TRUE);
  90. CComPtr<EnvDTE::_Solution> solution;
  91. if (FAILED(dte->get_Solution(&solution)))
  92. return nullptr;
  93. CComBSTR bstrSolution(solutionPath.toWString(Path::PathType::Windows).c_str());
  94. if (FAILED(solution->Open(bstrSolution)))
  95. return nullptr;
  96. // Wait until VS opens
  97. UINT32 elapsed = 0;
  98. while (elapsed < 10000)
  99. {
  100. EnvDTE::Window* window = nullptr;
  101. if (SUCCEEDED(dte->get_MainWindow(&window)))
  102. return dte;
  103. Sleep(100);
  104. elapsed += 100;
  105. }
  106. return nullptr;
  107. }
  108. static bool openFile(CComPtr<EnvDTE::_DTE> dte, const Path& filePath, UINT32 line)
  109. {
  110. // Open file
  111. CComPtr<EnvDTE::ItemOperations> itemOperations;
  112. if (FAILED(dte->get_ItemOperations(&itemOperations)))
  113. return false;
  114. CComBSTR bstrFilePath(filePath.toWString(Path::PathType::Windows).c_str());
  115. CComBSTR bstrKind(EnvDTE::vsViewKindPrimary);
  116. CComPtr<EnvDTE::Window> window = nullptr;
  117. if (FAILED(itemOperations->OpenFile(bstrFilePath, bstrKind, &window)))
  118. return false;
  119. // Scroll to line
  120. CComPtr<EnvDTE::Document> activeDocument;
  121. if (SUCCEEDED(dte->get_ActiveDocument(&activeDocument)))
  122. {
  123. CComPtr<IDispatch> selection;
  124. if (SUCCEEDED(activeDocument->get_Selection(&selection)))
  125. {
  126. CComPtr<EnvDTE::TextSelection> textSelection;
  127. if (SUCCEEDED(selection->QueryInterface(&textSelection)))
  128. {
  129. textSelection->GotoLine(line, TRUE);
  130. }
  131. }
  132. }
  133. // Bring the window in focus
  134. window = nullptr;
  135. if (SUCCEEDED(dte->get_MainWindow(&window)))
  136. {
  137. window->Activate();
  138. HWND hWnd;
  139. window->get_HWnd((LONG*)&hWnd);
  140. SetForegroundWindow(hWnd);
  141. }
  142. return true;
  143. }
  144. static String getProjectGUID(const WString& projectName)
  145. {
  146. static const String guidTemplate = "{0}-{1}-{2}-{3}-{4}";
  147. String hash = md5(projectName);
  148. String output = StringUtil::format(guidTemplate, hash.substr(0, 8),
  149. hash.substr(8, 4), hash.substr(12, 4), hash.substr(16, 4), hash.substr(20, 12));
  150. StringUtil::toUpperCase(output);
  151. return output;
  152. }
  153. static String writeSolution(VisualStudioVersion version, const CodeSolutionData& data)
  154. {
  155. struct VersionData
  156. {
  157. String formatVersion;
  158. };
  159. Map<VisualStudioVersion, VersionData> versionData =
  160. {
  161. { VisualStudioVersion::VS2008, { "10.00" } },
  162. { VisualStudioVersion::VS2010, { "11.00" } },
  163. { VisualStudioVersion::VS2012, { "12.00" } },
  164. { VisualStudioVersion::VS2013, { "12.00" } },
  165. { VisualStudioVersion::VS2015, { "12.00" } }
  166. };
  167. StringStream projectEntriesStream;
  168. StringStream projectPlatformsStream;
  169. for (auto& project : data.projects)
  170. {
  171. String guid = getProjectGUID(project.name);
  172. String projectName = toString(project.name);
  173. projectEntriesStream << StringUtil::format(PROJ_ENTRY_TEMPLATE, projectName, projectName + ".csproj", guid);
  174. projectPlatformsStream << StringUtil::format(PROJ_PLATFORM_TEMPLATE, guid);
  175. }
  176. String projectEntries = projectEntriesStream.str();
  177. String projectPlatforms = projectPlatformsStream.str();
  178. return StringUtil::format(SLN_TEMPLATE, versionData[version].formatVersion, projectEntries, projectPlatforms);
  179. }
  180. static String writeProject(VisualStudioVersion version, const CodeProjectData& projectData)
  181. {
  182. struct VersionData
  183. {
  184. String toolsVersion;
  185. };
  186. Map<VisualStudioVersion, VersionData> versionData =
  187. {
  188. { VisualStudioVersion::VS2008, { "3.5" } },
  189. { VisualStudioVersion::VS2010, { "4.0" } },
  190. { VisualStudioVersion::VS2012, { "4.0" } },
  191. { VisualStudioVersion::VS2013, { "12.0" } },
  192. { VisualStudioVersion::VS2015, { "13.0" } }
  193. };
  194. StringStream tempStream;
  195. for (auto& codeEntry : projectData.codeFiles)
  196. tempStream << StringUtil::format(CODE_ENTRY_TEMPLATE, codeEntry.toString());
  197. String codeEntries = tempStream.str();
  198. tempStream.str("");
  199. tempStream.clear();
  200. for (auto& nonCodeEntry : projectData.nonCodeFiles)
  201. tempStream << StringUtil::format(NON_CODE_ENTRY_TEMPLATE, nonCodeEntry.toString());
  202. String nonCodeEntries = tempStream.str();
  203. tempStream.str("");
  204. tempStream.clear();
  205. for (auto& referenceEntry : projectData.assemblyReferences)
  206. {
  207. String referenceName = toString(referenceEntry.name);
  208. if (referenceEntry.path.isEmpty())
  209. tempStream << StringUtil::format(REFERENCE_ENTRY_TEMPLATE, referenceName);
  210. else
  211. tempStream << StringUtil::format(REFERENCE_PATH_ENTRY_TEMPLATE, referenceName, referenceEntry.path.toString());
  212. }
  213. String referenceEntries = tempStream.str();
  214. tempStream.str("");
  215. tempStream.clear();
  216. for (auto& referenceEntry : projectData.projectReferences)
  217. {
  218. String referenceName = toString(referenceEntry.name);
  219. String projectGUID = getProjectGUID(referenceEntry.name);
  220. tempStream << StringUtil::format(REFERENCE_PROJECT_ENTRY_TEMPLATE, referenceName, projectGUID);
  221. }
  222. String projectReferenceEntries = tempStream.str();
  223. tempStream.str("");
  224. tempStream.clear();
  225. tempStream << toString(projectData.defines);
  226. String defines = tempStream.str();
  227. String projectGUID = getProjectGUID(projectData.name);
  228. return StringUtil::format(PROJ_TEMPLATE, versionData[version].toolsVersion, projectGUID,
  229. toString(projectData.name), defines, referenceEntries, projectReferenceEntries, codeEntries, nonCodeEntries);
  230. }
  231. };
  232. const String VisualStudio::SLN_TEMPLATE =
  233. R"(Microsoft Visual Studio Solution File, Format Version {0}
  234. # Visual Studio 2013
  235. VisualStudioVersion = 12.0.30723.0
  236. MinimumVisualStudioVersion = 10.0.40219.1{1}
  237. Global
  238. GlobalSection(SolutionConfigurationPlatforms) = preSolution
  239. Debug|Any CPU = Debug|Any CPU
  240. Release|Any CPU = Release|Any CPU
  241. EndGlobalSection
  242. GlobalSection(ProjectConfigurationPlatforms) = postSolution{2}
  243. EndGlobalSection
  244. GlobalSection(SolutionProperties) = preSolution
  245. HideSolutionNode = FALSE
  246. EndGlobalSection
  247. EndGlobal
  248. )";
  249. const String VisualStudio::PROJ_ENTRY_TEMPLATE =
  250. R"(
  251. Project("\{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC\}") = "{0}", "{1}", "\{{2}\}"
  252. EndProject)";
  253. const String VisualStudio::PROJ_PLATFORM_TEMPLATE =
  254. R"(
  255. \{{0}\}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
  256. \{{0}\}.Debug|Any CPU.Build.0 = Debug|Any CPU
  257. \{{0}\}.Release|Any CPU.ActiveCfg = Release|Any CPU
  258. \{{0}\}.Release|Any CPU.Build.0 = Release|Any CPU)";
  259. const String VisualStudio::PROJ_TEMPLATE =
  260. R"literal(<?xml version="1.0" encoding="utf-8"?>
  261. <Project ToolsVersion="{0}" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  262. <Import Project="$(MSBuildExtensionsPath)\\$(MSBuildToolsVersion)\\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\\$(MSBuildToolsVersion)\\Microsoft.Common.props')" />
  263. <PropertyGroup>
  264. <Configuration Condition = " '$(Configuration)' == '' ">Debug</Configuration>
  265. <Platform Condition = " '$(Platform)' == '' ">AnyCPU</Platform>
  266. <ProjectGuid>\{{1}\}</ProjectGuid>
  267. <OutputType>Library</OutputType>
  268. <AppDesignerFolder>Properties</AppDesignerFolder>
  269. <RootNamespace></RootNamespace>
  270. <AssemblyName>{2}</AssemblyName>
  271. <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
  272. <FileAlignment>512</FileAlignment>
  273. <BaseDirectory>Resources</BaseDirectory>
  274. <SchemaVersion>2.0</SchemaVersion>
  275. </PropertyGroup>
  276. <PropertyGroup Condition = " '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
  277. <DebugSymbols>true</DebugSymbols>
  278. <DebugType>full</DebugType>
  279. <Optimize>false</Optimize>
  280. <OutputPath>Internal\\Temp\\Assemblies\\Debug\\</OutputPath>
  281. <BaseIntermediateOutputPath>Internal\\Temp\\Assemblies\\</BaseIntermediateOutputPath>
  282. <DefineConstants>DEBUG;TRACE;{3}</DefineConstants>
  283. <ErrorReport>prompt</ErrorReport>
  284. <WarningLevel>4</WarningLevel >
  285. </PropertyGroup>
  286. <PropertyGroup Condition = " '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
  287. <DebugType>pdbonly</DebugType>
  288. <Optimize>true</Optimize>
  289. <OutputPath>Internal\\Temp\\Assemblies\\Release\\</OutputPath>
  290. <BaseIntermediateOutputPath>Internal\\Temp\\Assemblies\\</BaseIntermediateOutputPath>
  291. <DefineConstants>TRACE;{3}</DefineConstants>
  292. <ErrorReport>prompt</ErrorReport>
  293. <WarningLevel>4</WarningLevel>
  294. </PropertyGroup>
  295. <ItemGroup>{4}
  296. </ItemGroup>
  297. <ItemGroup>{5}
  298. </ItemGroup>
  299. <ItemGroup>{6}
  300. </ItemGroup>
  301. <ItemGroup>{7}
  302. </ItemGroup>
  303. <Import Project = "$(MSBuildToolsPath)\\Microsoft.CSharp.targets"/>
  304. </Project>)literal";
  305. const String VisualStudio::REFERENCE_ENTRY_TEMPLATE =
  306. R"(
  307. <Reference Include="{0}"/>)";
  308. const String VisualStudio::REFERENCE_PATH_ENTRY_TEMPLATE =
  309. R"(
  310. <Reference Include="{0}">
  311. <HintPath>{1}</HintPath>
  312. </Reference>)";
  313. const String VisualStudio::REFERENCE_PROJECT_ENTRY_TEMPLATE =
  314. R"(
  315. <ProjectReference Include="{0}.csproj">
  316. <Project>\{{1}\}</Project>
  317. <Name>{0}</Name>
  318. </ProjectReference>)";
  319. const String VisualStudio::CODE_ENTRY_TEMPLATE =
  320. R"(
  321. <Compile Include="{0}"/>)";
  322. const String VisualStudio::NON_CODE_ENTRY_TEMPLATE =
  323. R"(
  324. <None Include="{0}"/>)";
  325. VSCodeEditor::VSCodeEditor(VisualStudioVersion version, const Path& execPath, const WString& CLSID)
  326. :mCLSID(CLSID), mExecPath(execPath), mVersion(version)
  327. {
  328. }
  329. void VSCodeEditor::openFile(const Path& solutionPath, const Path& filePath, UINT32 lineNumber) const
  330. {
  331. CLSID clsID;
  332. if (FAILED(CLSIDFromString(mCLSID.c_str(), &clsID)))
  333. return;
  334. CComPtr<EnvDTE::_DTE> dte = VisualStudio::findRunningInstance(clsID, solutionPath);
  335. if (dte == nullptr)
  336. dte = VisualStudio::openInstance(clsID, solutionPath);
  337. if (dte == nullptr)
  338. return;
  339. VisualStudio::openFile(dte, filePath, lineNumber);
  340. }
  341. void VSCodeEditor::syncSolution(const CodeSolutionData& data, const Path& outputPath) const
  342. {
  343. String solutionString = VisualStudio::writeSolution(mVersion, data);
  344. solutionString = StringUtil::replaceAll(solutionString, "\n", "\r\n");
  345. Path solutionPath = outputPath;
  346. solutionPath.append(data.name + L".sln");
  347. for (auto& project : data.projects)
  348. {
  349. String projectString = VisualStudio::writeProject(mVersion, project);
  350. projectString = StringUtil::replaceAll(projectString, "\n", "\r\n");
  351. Path projectPath = outputPath;
  352. projectPath.append(project.name + L".csproj");
  353. DataStreamPtr projectStream = FileSystem::createAndOpenFile(projectPath);
  354. projectStream->write(projectString.c_str(), projectString.size() * sizeof(String::value_type));
  355. projectStream->close();
  356. }
  357. DataStreamPtr solutionStream = FileSystem::createAndOpenFile(solutionPath);
  358. solutionStream->write(solutionString.c_str(), solutionString.size() * sizeof(String::value_type));
  359. solutionStream->close();
  360. }
  361. VSCodeEditorFactory::VSCodeEditorFactory()
  362. :mAvailableVersions(getAvailableVersions())
  363. {
  364. for (auto& version : mAvailableVersions)
  365. mAvailableEditors.push_back(version.first);
  366. }
  367. Map<CodeEditorType, VSCodeEditorFactory::VSVersionInfo> VSCodeEditorFactory::getAvailableVersions() const
  368. {
  369. #if BS_ARCH_TYPE == BS_ARCHITECTURE_x86_64
  370. bool is64bit = true;
  371. #else
  372. bool is64bit = false;
  373. IsWow64Process(GetCurrentProcess(), &is64bit);
  374. #endif
  375. WString registryKeyRoot;
  376. if (is64bit)
  377. registryKeyRoot = L"SOFTWARE\\Wow6432Node\\Microsoft";
  378. else
  379. registryKeyRoot = L"SOFTWARE\\Microsoft";
  380. struct VersionData
  381. {
  382. CodeEditorType type;
  383. WString registryKey;
  384. WString name;
  385. WString executable;
  386. };
  387. Map<VisualStudioVersion, VersionData> versionToVersionNumber =
  388. {
  389. { VisualStudioVersion::VS2008, { CodeEditorType::VS2008, L"VisualStudio\\9.0", L"Visual Studio 2008", L"devenv.exe" } },
  390. { VisualStudioVersion::VS2010, { CodeEditorType::VS2010, L"VisualStudio\\10.0", L"Visual Studio 2010", L"devenv.exe" } },
  391. { VisualStudioVersion::VS2012, { CodeEditorType::VS2012, L"VisualStudio\\11.0", L"Visual Studio 2012", L"devenv.exe" } },
  392. { VisualStudioVersion::VS2013, { CodeEditorType::VS2013, L"VisualStudio\\12.0", L"Visual Studio 2013", L"devenv.exe" } },
  393. { VisualStudioVersion::VS2015, { CodeEditorType::VS2015, L"VisualStudio\\13.0", L"Visual Studio 2015", L"devenv.exe" } }
  394. };
  395. Map<CodeEditorType, VSVersionInfo> versionInfo;
  396. for(auto version : versionToVersionNumber)
  397. {
  398. WString registryKey = registryKeyRoot + L"\\" + version.second.registryKey;
  399. HKEY regKey;
  400. LONG result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, registryKey.c_str(), 0, KEY_READ, &regKey);
  401. if (result != ERROR_SUCCESS)
  402. continue;
  403. WString installPath;
  404. getRegistryStringValue(regKey, L"InstallDir", installPath, StringUtil::WBLANK);
  405. if (installPath.empty())
  406. continue;
  407. WString clsID;
  408. getRegistryStringValue(regKey, L"ThisVersionDTECLSID", clsID, StringUtil::WBLANK);
  409. VSVersionInfo info;
  410. info.name = version.second.name;
  411. info.execPath = installPath.append(version.second.executable);
  412. info.CLSID = clsID;
  413. info.version = version.first;
  414. versionInfo[version.second.type] = info;
  415. }
  416. // TODO - Also query for VSExpress and VSCommunity (their registry keys are different)
  417. return versionInfo;
  418. }
  419. CodeEditor* VSCodeEditorFactory::create(CodeEditorType type) const
  420. {
  421. auto findIter = mAvailableVersions.find(type);
  422. if (findIter == mAvailableVersions.end())
  423. return nullptr;
  424. // TODO - Also create VSExpress and VSCommunity editors
  425. return bs_new<VSCodeEditor>(findIter->second.version, findIter->second.execPath, findIter->second.CLSID);
  426. }
  427. }