BsVSCodeEditor.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
  2. //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
  3. #include "Private/Win32/BsVSCodeEditor.h"
  4. #include <windows.h>
  5. #include <atlbase.h>
  6. #include "FileSystem/BsFileSystem.h"
  7. #include "FileSystem/BsDataStream.h"
  8. // Import EnvDTE
  9. // Uncomment this code to generate the dte80a.tlh file below. We're not using #import directly because it is not
  10. // compatible with multi-processor compilation of the build
  11. //#pragma warning(disable: 4278)
  12. //#import "libid:80cc9f66-e7d8-4ddd-85b6-d9e6cd0e93e2" version("8.0") lcid("0") raw_interfaces_only named_guids
  13. //#pragma warning(default: 4278)
  14. #include "Private/Win32/dte80a.tlh"
  15. #include "Private/Win32/Setup.Configuration.h"
  16. #include "String/BsUnicode.h"
  17. _COM_SMARTPTR_TYPEDEF(ISetupInstance, __uuidof(ISetupInstance));
  18. _COM_SMARTPTR_TYPEDEF(ISetupInstance2, __uuidof(ISetupInstance2));
  19. _COM_SMARTPTR_TYPEDEF(IEnumSetupInstances, __uuidof(IEnumSetupInstances));
  20. _COM_SMARTPTR_TYPEDEF(ISetupConfiguration, __uuidof(ISetupConfiguration));
  21. _COM_SMARTPTR_TYPEDEF(ISetupConfiguration2, __uuidof(ISetupConfiguration2));
  22. _COM_SMARTPTR_TYPEDEF(ISetupHelper, __uuidof(ISetupHelper));
  23. _COM_SMARTPTR_TYPEDEF(ISetupPackageReference, __uuidof(ISetupPackageReference));
  24. _COM_SMARTPTR_TYPEDEF(ISetupPropertyStore, __uuidof(ISetupPropertyStore));
  25. _COM_SMARTPTR_TYPEDEF(ISetupInstanceCatalog, __uuidof(ISetupInstanceCatalog));
  26. namespace bs
  27. {
  28. /**
  29. * Reads a string value from the specified key in the registry.
  30. *
  31. * @param[in] key Registry key to read from.
  32. * @param[in] name Identifier of the value to read from.
  33. * @param[in] value Output value read from the key.
  34. * @param[in] defaultValue Default value to return if the key or identifier doesn't exist.
  35. */
  36. static LONG getRegistryStringValue(HKEY hKey, const WString& name, WString& value, const WString& defaultValue)
  37. {
  38. value = defaultValue;
  39. wchar_t strBuffer[512];
  40. DWORD strBufferSize = sizeof(strBuffer);
  41. ULONG result = RegQueryValueExW(hKey, name.c_str(), 0, nullptr, (LPBYTE)strBuffer, &strBufferSize);
  42. if (result == ERROR_SUCCESS)
  43. value = strBuffer;
  44. return result;
  45. }
  46. /** Contains data about a Visual Studio project. */
  47. struct VSProjectInfo
  48. {
  49. WString GUID;
  50. WString name;
  51. Path path;
  52. };
  53. /**
  54. * Handles retrying of calls that fail to access Visual Studio. This is due to the weird nature of VS when calling its
  55. * methods from external code. If this message filter isn't registered some calls will just fail silently.
  56. */
  57. class VSMessageFilter : public IMessageFilter
  58. {
  59. DWORD __stdcall HandleInComingCall(DWORD dwCallType, HTASK htaskCaller, DWORD dwTickCount, LPINTERFACEINFO lpInterfaceInfo) override
  60. {
  61. return SERVERCALL_ISHANDLED;
  62. }
  63. DWORD __stdcall RetryRejectedCall(HTASK htaskCallee, DWORD dwTickCount, DWORD dwRejectType) override
  64. {
  65. if (dwRejectType == SERVERCALL_RETRYLATER)
  66. {
  67. // Retry immediatey
  68. return 99;
  69. }
  70. // Cancel the call
  71. return -1;
  72. }
  73. DWORD __stdcall MessagePending(HTASK htaskCallee, DWORD dwTickCount, DWORD dwPendingType) override
  74. {
  75. return PENDINGMSG_WAITDEFPROCESS;
  76. }
  77. /** COM requirement. Returns instance of an interface of provided type. */
  78. HRESULT __stdcall QueryInterface(REFIID iid, void** ppvObject) override
  79. {
  80. if(iid == IID_IDropTarget || iid == IID_IUnknown)
  81. {
  82. AddRef();
  83. *ppvObject = this;
  84. return S_OK;
  85. }
  86. else
  87. {
  88. *ppvObject = nullptr;
  89. return E_NOINTERFACE;
  90. }
  91. }
  92. /** COM requirement. Increments objects reference count. */
  93. ULONG __stdcall AddRef() override
  94. {
  95. return InterlockedIncrement(&mRefCount);
  96. }
  97. /** COM requirement. Decreases the objects reference count and deletes the object if its zero. */
  98. ULONG __stdcall Release() override
  99. {
  100. LONG count = InterlockedDecrement(&mRefCount);
  101. if(count == 0)
  102. {
  103. bs_delete(this);
  104. return 0;
  105. }
  106. else
  107. {
  108. return count;
  109. }
  110. }
  111. private:
  112. LONG mRefCount;
  113. };
  114. /** Contains various helper functionality for interacting with a Visual Studio instance running on this machine. */
  115. class VisualStudio
  116. {
  117. public:
  118. /**
  119. * Scans the running processes to find a running Visual Studio instance with the specified version and open solution.
  120. *
  121. * @param[in] clsID Class ID of the specific Visual Studio version we are looking for.
  122. * @param[in] solutionPath Path to the solution the instance needs to have open.
  123. * @return DTE object that may be used to interact with the Visual Studio instance, or null if
  124. * not found.
  125. */
  126. static CComPtr<EnvDTE::_DTE> findRunningInstance(const CLSID& clsID, const Path& solutionPath)
  127. {
  128. CComPtr<IRunningObjectTable> runningObjectTable = nullptr;
  129. if (FAILED(GetRunningObjectTable(0, &runningObjectTable)))
  130. return nullptr;
  131. CComPtr<IEnumMoniker> enumMoniker = nullptr;
  132. if (FAILED(runningObjectTable->EnumRunning(&enumMoniker)))
  133. return nullptr;
  134. CComPtr<IMoniker> dteMoniker = nullptr;
  135. if (FAILED(CreateClassMoniker(clsID, &dteMoniker)))
  136. return nullptr;
  137. WString wideSolutionPath = UTF8::toWide(solutionPath.toString(Path::PathType::Windows));
  138. CComBSTR bstrSolution(wideSolutionPath.c_str());
  139. CComPtr<IMoniker> moniker;
  140. ULONG count = 0;
  141. while (enumMoniker->Next(1, &moniker, &count) == S_OK)
  142. {
  143. if (moniker->IsEqual(dteMoniker))
  144. {
  145. CComPtr<IUnknown> curObject = nullptr;
  146. HRESULT result = runningObjectTable->GetObject(moniker, &curObject);
  147. moniker = nullptr;
  148. if (result != S_OK)
  149. continue;
  150. CComPtr<EnvDTE::_DTE> dte;
  151. curObject->QueryInterface(__uuidof(EnvDTE::_DTE), (void**)&dte);
  152. if (dte == nullptr)
  153. continue;
  154. CComPtr<EnvDTE::_Solution> solution;
  155. if (FAILED(dte->get_Solution(&solution)))
  156. continue;
  157. CComBSTR fullName;
  158. if (FAILED(solution->get_FullName(&fullName)))
  159. continue;
  160. if (fullName == bstrSolution)
  161. return dte;
  162. }
  163. }
  164. return nullptr;
  165. }
  166. /**
  167. * Opens a new Visual Studio instance of the specified version with the provided solution.
  168. *
  169. * @param[in] clsID Class ID of the specific Visual Studio version to start.
  170. * @param[in] solutionPath Path to the solution the instance needs to open.
  171. */
  172. static CComPtr<EnvDTE::_DTE> openInstance(const CLSID& clsid, const Path& solutionPath)
  173. {
  174. CComPtr<IUnknown> newInstance = nullptr;
  175. if (FAILED(::CoCreateInstance(clsid, nullptr, CLSCTX_LOCAL_SERVER, EnvDTE::IID__DTE, (LPVOID*)&newInstance)))
  176. return nullptr;
  177. CComPtr<EnvDTE::_DTE> dte;
  178. newInstance->QueryInterface(__uuidof(EnvDTE::_DTE), (void**)&dte);
  179. if (dte == nullptr)
  180. return nullptr;
  181. dte->put_UserControl(TRUE);
  182. CComPtr<EnvDTE::_Solution> solution;
  183. if (FAILED(dte->get_Solution(&solution)))
  184. return nullptr;
  185. WString wideSolutionPath = UTF8::toWide(solutionPath.toString(Path::PathType::Windows));
  186. CComBSTR bstrSolution(wideSolutionPath.c_str());
  187. if (FAILED(solution->Open(bstrSolution)))
  188. return nullptr;
  189. // Wait until VS opens
  190. UINT32 elapsed = 0;
  191. while (elapsed < 10000)
  192. {
  193. EnvDTE::Window* window = nullptr;
  194. if (SUCCEEDED(dte->get_MainWindow(&window)))
  195. return dte;
  196. Sleep(100);
  197. elapsed += 100;
  198. }
  199. return nullptr;
  200. }
  201. /**
  202. * Opens a file on a specific line in a running Visual Studio instance.
  203. *
  204. * @param[in] dte DTE object retrieved from findRunningInstance() or openInstance().
  205. * @param[in] filePath Path of the file to open. File should be a part of the VS solution.
  206. * @param[in] line Line on which to focus Visual Studio after the file is open.
  207. */
  208. static bool openFile(CComPtr<EnvDTE::_DTE> dte, const Path& filePath, UINT32 line)
  209. {
  210. // Open file
  211. CComPtr<EnvDTE::ItemOperations> itemOperations;
  212. if (FAILED(dte->get_ItemOperations(&itemOperations)))
  213. return false;
  214. WString wideFilePath = UTF8::toWide(filePath.toString(Path::PathType::Windows));
  215. CComBSTR bstrFilePath(wideFilePath.c_str());
  216. CComBSTR bstrKind(EnvDTE::vsViewKindPrimary);
  217. CComPtr<EnvDTE::Window> window = nullptr;
  218. if (FAILED(itemOperations->OpenFile(bstrFilePath, bstrKind, &window)))
  219. return false;
  220. // Scroll to line
  221. CComPtr<EnvDTE::Document> activeDocument;
  222. if (SUCCEEDED(dte->get_ActiveDocument(&activeDocument)))
  223. {
  224. CComPtr<IDispatch> selection;
  225. if (SUCCEEDED(activeDocument->get_Selection(&selection)))
  226. {
  227. CComPtr<EnvDTE::TextSelection> textSelection;
  228. if (selection != nullptr && SUCCEEDED(selection->QueryInterface(&textSelection)))
  229. {
  230. textSelection->GotoLine(line, TRUE);
  231. }
  232. }
  233. }
  234. // Bring the window in focus
  235. window = nullptr;
  236. if (SUCCEEDED(dte->get_MainWindow(&window)))
  237. {
  238. window->Activate();
  239. HWND hWnd;
  240. window->get_HWnd((LONG*)&hWnd);
  241. SetForegroundWindow(hWnd);
  242. }
  243. return true;
  244. }
  245. };
  246. VSCodeEditor::VSCodeEditor(VisualStudioVersion version, const Path& execPath, const WString& CLSID)
  247. :mVersion(version), mExecPath(execPath), mCLSID(CLSID)
  248. {
  249. }
  250. void VSCodeEditor::openFile(const Path& solutionPath, const Path& filePath, UINT32 lineNumber) const
  251. {
  252. CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
  253. CLSID clsID;
  254. if (FAILED(CLSIDFromString(mCLSID.c_str(), &clsID)))
  255. {
  256. CoUninitialize();
  257. return;
  258. }
  259. CComPtr<EnvDTE::_DTE> dte = VisualStudio::findRunningInstance(clsID, solutionPath);
  260. if (dte == nullptr)
  261. dte = VisualStudio::openInstance(clsID, solutionPath);
  262. if (dte == nullptr)
  263. {
  264. CoUninitialize();
  265. return;
  266. }
  267. VSMessageFilter* newFilter = new VSMessageFilter();
  268. IMessageFilter* oldFilter;
  269. CoRegisterMessageFilter(newFilter, &oldFilter);
  270. EnvDTE::Window* window = nullptr;
  271. if (SUCCEEDED(dte->get_MainWindow(&window)))
  272. window->Activate();
  273. VisualStudio::openFile(dte, filePath, lineNumber);
  274. CoRegisterMessageFilter(oldFilter, nullptr);
  275. CoUninitialize();
  276. }
  277. void VSCodeEditor::syncSolution(const CodeSolutionData& data, const Path& outputPath) const
  278. {
  279. CSProjectVersion csProjVer;
  280. switch(mVersion)
  281. {
  282. case VisualStudioVersion::VS2008: csProjVer = CSProjectVersion::VS2008; break;
  283. case VisualStudioVersion::VS2010: csProjVer = CSProjectVersion::VS2010; break;
  284. case VisualStudioVersion::VS2012: csProjVer = CSProjectVersion::VS2012; break;
  285. case VisualStudioVersion::VS2013: csProjVer = CSProjectVersion::VS2013; break;
  286. case VisualStudioVersion::VS2015: csProjVer = CSProjectVersion::VS2015; break;
  287. default:
  288. case VisualStudioVersion::VS2017: csProjVer = CSProjectVersion::VS2017; break;
  289. }
  290. String solutionString = CSProject::writeSolution(csProjVer, data);
  291. solutionString = StringUtil::replaceAll(solutionString, "\n", "\r\n");
  292. Path solutionPath = outputPath;
  293. solutionPath.append(data.name + ".sln");
  294. for (auto& project : data.projects)
  295. {
  296. String projectString = CSProject::writeProject(csProjVer, project);
  297. projectString = StringUtil::replaceAll(projectString, "\n", "\r\n");
  298. Path projectPath = outputPath;
  299. projectPath.append(project.name + ".csproj");
  300. SPtr<DataStream> projectStream = FileSystem::createAndOpenFile(projectPath);
  301. projectStream->write(projectString.c_str(), projectString.size() * sizeof(String::value_type));
  302. projectStream->close();
  303. }
  304. SPtr<DataStream> solutionStream = FileSystem::createAndOpenFile(solutionPath);
  305. solutionStream->write(solutionString.c_str(), solutionString.size() * sizeof(String::value_type));
  306. solutionStream->close();
  307. }
  308. VSCodeEditorFactory::VSCodeEditorFactory()
  309. :mAvailableVersions(getAvailableVersions())
  310. {
  311. for (auto& version : mAvailableVersions)
  312. mAvailableEditors.push_back(version.first);
  313. }
  314. Map<CodeEditorType, VSCodeEditorFactory::VSVersionInfo> VSCodeEditorFactory::getAvailableVersions() const
  315. {
  316. #if BS_ARCH_TYPE == BS_ARCHITECTURE_x86_64
  317. BOOL is64bit = 1;
  318. #else
  319. BOOL is64bit = 0;
  320. HANDLE process = GetCurrentProcess();
  321. IsWow64Process(process, (PBOOL)&is64bit);
  322. #endif
  323. WString registryKeyRoot;
  324. if (is64bit)
  325. registryKeyRoot = L"SOFTWARE\\Wow6432Node\\Microsoft";
  326. else
  327. registryKeyRoot = L"SOFTWARE\\Microsoft";
  328. struct VersionData
  329. {
  330. CodeEditorType type;
  331. WString registryKey;
  332. WString name;
  333. WString executable;
  334. };
  335. // Pre-2017 versions all have registry entries we can search
  336. Map<VisualStudioVersion, VersionData> versionToVersionNumber =
  337. {
  338. { VisualStudioVersion::VS2008, { CodeEditorType::VS2008, L"VisualStudio\\9.0", L"Visual Studio 2008", L"devenv.exe" } },
  339. { VisualStudioVersion::VS2010, { CodeEditorType::VS2010, L"VisualStudio\\10.0", L"Visual Studio 2010", L"devenv.exe" } },
  340. { VisualStudioVersion::VS2012, { CodeEditorType::VS2012, L"VisualStudio\\11.0", L"Visual Studio 2012", L"devenv.exe" } },
  341. { VisualStudioVersion::VS2013, { CodeEditorType::VS2013, L"VisualStudio\\12.0", L"Visual Studio 2013", L"devenv.exe" } },
  342. { VisualStudioVersion::VS2015, { CodeEditorType::VS2015, L"VisualStudio\\14.0", L"Visual Studio 2015", L"devenv.exe" } }
  343. };
  344. Map<CodeEditorType, VSVersionInfo> versionInfo;
  345. for(auto version : versionToVersionNumber)
  346. {
  347. WString registryKey = registryKeyRoot + L"\\" + version.second.registryKey;
  348. HKEY regKey;
  349. LONG result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, registryKey.c_str(), 0, KEY_READ, &regKey);
  350. if (result != ERROR_SUCCESS)
  351. continue;
  352. WString installPath;
  353. getRegistryStringValue(regKey, L"InstallDir", installPath, StringUtil::WBLANK);
  354. if (installPath.empty())
  355. continue;
  356. WString clsID;
  357. getRegistryStringValue(regKey, L"ThisVersionDTECLSID", clsID, StringUtil::WBLANK);
  358. VSVersionInfo info;
  359. info.name = version.second.name;
  360. info.execPath = UTF8::fromWide(installPath.append(version.second.executable));
  361. info.CLSID = clsID;
  362. info.version = version.first;
  363. versionInfo[version.second.type] = info;
  364. }
  365. // 2017 and later need to be queried through Setup Config
  366. CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
  367. ISetupConfigurationPtr query;
  368. auto hr = query.CreateInstance(__uuidof(SetupConfiguration));
  369. if (hr == REGDB_E_CLASSNOTREG || FAILED(hr))
  370. {
  371. CoUninitialize();
  372. return versionInfo;
  373. }
  374. ISetupConfiguration2Ptr query2(query);
  375. IEnumSetupInstancesPtr e;
  376. hr = query2->EnumAllInstances(&e);
  377. if (FAILED(hr))
  378. {
  379. CoUninitialize();
  380. return versionInfo;
  381. }
  382. ISetupHelperPtr helper(query);
  383. ISetupInstance* pInstances[1] = {};
  384. hr = e->Next(1, pInstances, nullptr);
  385. while (hr == S_OK)
  386. {
  387. ISetupInstancePtr instance(pInstances[0], false);
  388. bstr_t bstrName;
  389. hr = instance->GetDisplayName(0, bstrName.GetAddress());
  390. if (FAILED(hr))
  391. {
  392. hr = e->Next(1, pInstances, nullptr);
  393. continue;
  394. }
  395. bstr_t bstrVersion;
  396. hr = instance->GetInstallationVersion(bstrVersion.GetAddress());
  397. if (FAILED(hr))
  398. {
  399. hr = e->Next(1, pInstances, nullptr);
  400. continue;
  401. }
  402. bstr_t bstrInstallationPath;
  403. hr = instance->GetInstallationPath(bstrInstallationPath.GetAddress());
  404. if (FAILED(hr))
  405. {
  406. hr = e->Next(1, pInstances, nullptr);
  407. continue;
  408. }
  409. Vector<WString> versionBits = StringUtil::split(WString(bstrVersion), L".");
  410. if (versionBits.size() < 1)
  411. {
  412. hr = e->Next(1, pInstances, nullptr);
  413. continue;
  414. }
  415. // VS2017
  416. if (versionBits[0] == L"15")
  417. {
  418. VSVersionInfo info;
  419. info.name = WString(bstrName);
  420. info.execPath = Path(UTF8::fromWide(WString(bstrInstallationPath))) + Path("Common7/IDE/devenv.exe");
  421. info.version = VisualStudioVersion::VS2017;
  422. info.CLSID = L"{3829D1F4-A427-4C75-B63C-7ABB7521B225}";
  423. versionInfo[CodeEditorType::VS2017] = info;
  424. }
  425. hr = e->Next(1, pInstances, nullptr);
  426. }
  427. CoUninitialize();
  428. return versionInfo;
  429. }
  430. CodeEditor* VSCodeEditorFactory::create(CodeEditorType type) const
  431. {
  432. auto findIter = mAvailableVersions.find(type);
  433. if (findIter == mAvailableVersions.end())
  434. return nullptr;
  435. return bs_new<VSCodeEditor>(findIter->second.version, findIter->second.execPath, findIter->second.CLSID);
  436. }
  437. }