BsVSCodeEditor.cpp 16 KB

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