#include "Win32/BsVSCodeEditor.h" #include #include #include "BsFileSystem.h" #include "BsDataStream.h" // Import EnvDTE #import "libid:80cc9f66-e7d8-4ddd-85b6-d9e6cd0e93e2" version("8.0") lcid("0") raw_interfaces_only named_guids namespace BansheeEngine { LONG getRegistryStringValue(HKEY hKey, const WString& name, WString& value, const WString& defaultValue) { value = defaultValue; wchar_t strBuffer[512]; DWORD strBufferSize = sizeof(strBuffer); ULONG result = RegQueryValueExW(hKey, name.c_str(), 0, nullptr, (LPBYTE)strBuffer, &strBufferSize); if (result == ERROR_SUCCESS) value = strBuffer; return result; } struct VSProjectInfo { WString GUID; WString name; Path path; }; class VisualStudio { private: static const WString SLN_TEMPLATE; static const WString PROJ_ENTRY_TEMPLATE; static const WString PROJ_PLATFORM_TEMPLATE; static const WString PROJ_TEMPLATE; static const WString REFERENCE_ENTRY_TEMPLATE; static const WString REFERENCE_PROJECT_ENTRY_TEMPLATE; static const WString REFERENCE_PATH_ENTRY_TEMPLATE; static const WString CODE_ENTRY_TEMPLATE; static const WString NON_CODE_ENTRY_TEMPLATE; public: static CComPtr findRunningInstance(const CLSID& clsID, const Path& solutionPath) { CComPtr runningObjectTable = nullptr; if (FAILED(GetRunningObjectTable(0, &runningObjectTable))) return nullptr; CComPtr enumMoniker = nullptr; if (FAILED(runningObjectTable->EnumRunning(&enumMoniker))) return nullptr; CComPtr dteMoniker = nullptr; if (FAILED(CreateClassMoniker(clsID, &dteMoniker))) return nullptr; CComBSTR bstrSolution(solutionPath.toWString(Path::PathType::Windows).c_str()); CComPtr moniker; ULONG count = 0; while (enumMoniker->Next(1, &moniker, &count) == S_OK) { if (moniker->IsEqual(dteMoniker)) { CComPtr curObject = nullptr; HRESULT result = runningObjectTable->GetObject(moniker, &curObject); moniker = nullptr; if (result != S_OK) continue; CComPtr dte; curObject->QueryInterface(__uuidof(EnvDTE::_DTE), (void**)&dte); if (dte == nullptr) return nullptr; CComPtr solution; if (FAILED(dte->get_Solution(&solution))) continue; CComBSTR fullName; if (FAILED(solution->get_FullName(&fullName))) continue; if (fullName == bstrSolution) return dte; } } return nullptr; } static CComPtr openInstance(const CLSID& clsid, const Path& solutionPath) { CComPtr newInstance = nullptr; if (FAILED(::CoCreateInstance(clsid, nullptr, CLSCTX_LOCAL_SERVER, EnvDTE::IID__DTE, (LPVOID*)&newInstance))) return nullptr; CComPtr dte; newInstance->QueryInterface(__uuidof(EnvDTE::_DTE), (void**)&dte); if (dte == nullptr) return nullptr; dte->put_UserControl(TRUE); CComPtr solution; if (FAILED(dte->get_Solution(&solution))) return nullptr; CComBSTR bstrSolution(solutionPath.toWString(Path::PathType::Windows).c_str()); if (FAILED(solution->Open(bstrSolution))) return nullptr; // Wait until VS opens UINT32 elapsed = 0; while (elapsed < 10000) { EnvDTE::Window* window = nullptr; if (SUCCEEDED(dte->get_MainWindow(&window))) return dte; Sleep(100); elapsed += 100; } return nullptr; } static bool openFile(CComPtr dte, const Path& filePath, UINT32 line) { // Open file CComPtr itemOperations; if (FAILED(dte->get_ItemOperations(&itemOperations))) return false; CComBSTR bstrFilePath(filePath.toWString(Path::PathType::Windows).c_str()); CComBSTR bstrKind(EnvDTE::vsViewKindPrimary); CComPtr window = nullptr; if (FAILED(itemOperations->OpenFile(bstrFilePath, bstrKind, &window))) return false; // Scroll to line CComPtr activeDocument; if (SUCCEEDED(dte->get_ActiveDocument(&activeDocument))) { CComPtr selection; if (SUCCEEDED(activeDocument->get_Selection(&selection))) { CComPtr textSelection; if (SUCCEEDED(selection->QueryInterface(&textSelection))) { textSelection->GotoLine(line, TRUE); } } } // Bring the window in focus window = nullptr; if (SUCCEEDED(dte->get_MainWindow(&window))) { window->Activate(); HWND hWnd; window->get_HWnd((LONG*)&hWnd); SetForegroundWindow(hWnd); } return true; } static String getSolutionGUID(const WString& solutionName) { static const String guidTemplate = "{0}-{1}-{2}-{3}-{4}"; String hash = md5(L"SLN_" + solutionName); return StringUtil::format(guidTemplate, hash.substr(0, 8), hash.substr(8, 4), hash.substr(12, 4), hash.substr(16, 4), hash.substr(20, 12)); } static String getProjectGUID(const WString& projectName) { static const String guidTemplate = "{0}-{1}-{2}-{3}-{4}"; String hash = md5(L"PRJ_" + projectName); return StringUtil::format(guidTemplate, hash.substr(0, 8), hash.substr(8, 4), hash.substr(12, 4), hash.substr(16, 4), hash.substr(20, 12)); } static WString writeSolution(VisualStudioVersion version, const CodeSolutionData& data) { struct VersionData { WString formatVersion; }; Map versionData = { { VisualStudioVersion::VS2008, { L"10.0" } }, { VisualStudioVersion::VS2010, { L"11.0" } }, { VisualStudioVersion::VS2012, { L"12.0" } }, { VisualStudioVersion::VS2013, { L"12.0" } }, { VisualStudioVersion::VS2015, { L"12.0" } } }; WString solutionGUID = toWString(getSolutionGUID(data.name)); WStringStream projectEntriesStream; WStringStream projectPlatformsStream; for (auto& project : data.projects) { WString guid = toWString(getProjectGUID(project.name)); projectEntriesStream << StringUtil::format(PROJ_ENTRY_TEMPLATE, solutionGUID, project.name, project.name + L".csproj", guid) << std::endl; projectPlatformsStream << StringUtil::format(PROJ_PLATFORM_TEMPLATE, guid) << std::endl; } WString projectEntries = projectEntriesStream.str(); WString projectPlatforms = projectPlatformsStream.str(); return StringUtil::format(SLN_TEMPLATE, versionData[version].formatVersion, projectEntries, projectPlatforms); } static WString writeProject(VisualStudioVersion version, const CodeProjectData& projectData) { struct VersionData { WString toolsVersion; }; Map versionData = { { VisualStudioVersion::VS2008, { L"3.5" } }, { VisualStudioVersion::VS2010, { L"4.0" } }, { VisualStudioVersion::VS2012, { L"4.0" } }, { VisualStudioVersion::VS2013, { L"12.0" } }, { VisualStudioVersion::VS2015, { L"13.0" } } }; WStringStream tempStream; for (auto& codeEntry : projectData.codeFiles) tempStream << StringUtil::format(CODE_ENTRY_TEMPLATE, codeEntry.toWString()) << std::endl; WString codeEntries = tempStream.str(); tempStream.str(L""); tempStream.clear(); for (auto& nonCodeEntry : projectData.nonCodeFiles) tempStream << StringUtil::format(NON_CODE_ENTRY_TEMPLATE, nonCodeEntry.toWString()) << std::endl; WString nonCodeEntries = tempStream.str(); tempStream.str(L""); tempStream.clear(); for (auto& referenceEntry : projectData.assemblyReferences) { if (referenceEntry.path.isEmpty()) tempStream << StringUtil::format(REFERENCE_ENTRY_TEMPLATE, referenceEntry.name) << std::endl; else tempStream << StringUtil::format(REFERENCE_PATH_ENTRY_TEMPLATE, referenceEntry.name, referenceEntry.path.toWString()) << std::endl; } WString referenceEntries = tempStream.str(); tempStream.str(L""); tempStream.clear(); for (auto& referenceEntry : projectData.projectReferences) { WString projectGUID = toWString(getProjectGUID(referenceEntry.name)); tempStream << StringUtil::format(REFERENCE_PROJECT_ENTRY_TEMPLATE, referenceEntry.name, projectGUID) << std::endl; } WString projectReferenceEntries = tempStream.str(); tempStream.str(L""); tempStream.clear(); for (auto& define : projectData.defines) tempStream << define << L";"; WString defines = tempStream.str(); WString projectGUID = toWString(getProjectGUID(projectData.name)); return StringUtil::format(PROJ_ENTRY_TEMPLATE, versionData[version].toolsVersion, projectGUID, projectData.name, defines, referenceEntries, projectReferenceEntries, codeEntries, nonCodeEntries); } }; const WString VisualStudio::SLN_TEMPLATE = LR"(Microsoft Visual Studio Solution File, Format Version {0} {1} Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2} EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection EndGlobal)"; const WString VisualStudio::PROJ_ENTRY_TEMPLATE = LR"(Project("\{{0}\}") = "{1}", "{2}", "\{{3}\}" EndProject)"; const WString VisualStudio::PROJ_PLATFORM_TEMPLATE = LR"(\{{0}\}.Debug|Any CPU.ActiveCfg = Debug|Any CPU \{{0}\}.Debug|Any CPU.Build.0 = Debug|Any CPU \{{0}\}.Release|Any CPU.ActiveCfg = Release|Any CPU \{{0}\}.Release|Any CPU.Build.0 = Release|Any CPU)"; const WString VisualStudio::PROJ_TEMPLATE = LR"literal( Debug AnyCPU \{{1}\} Library Properties {2} v4.0 512 Resources 2.0 true full false Internal\Temp\Assemblies\Debug\ DEBUG;TRACE;{3} prompt 4 pdbonly true Internal\Temp\Assemblies\Release\ TRACE;{3} prompt 4 {4} {5} {6} {7} )literal"; const WString VisualStudio::REFERENCE_ENTRY_TEMPLATE = LR"( )"; const WString VisualStudio::REFERENCE_PATH_ENTRY_TEMPLATE = LR"( {1} )"; const WString VisualStudio::REFERENCE_PROJECT_ENTRY_TEMPLATE = LR"( \{{1}\} {0} )"; const WString VisualStudio::CODE_ENTRY_TEMPLATE = LR"( )"; const WString VisualStudio::NON_CODE_ENTRY_TEMPLATE = LR"( )"; VSCodeEditor::VSCodeEditor(VisualStudioVersion version, const Path& execPath, const WString& CLSID) :mCLSID(CLSID), mExecPath(execPath), mVersion(version) { } void VSCodeEditor::openFile(const Path& solutionPath, const Path& filePath, UINT32 lineNumber) const { CLSID clsID; if (FAILED(CLSIDFromString(mCLSID.c_str(), &clsID))) return; CComPtr dte = VisualStudio::findRunningInstance(clsID, solutionPath); if (dte == nullptr) dte = VisualStudio::openInstance(clsID, solutionPath); if (dte == nullptr) return; VisualStudio::openFile(dte, filePath, lineNumber); } void VSCodeEditor::syncSolution(const CodeSolutionData& data, const Path& outputPath) const { WString solutionString = VisualStudio::writeSolution(mVersion, data); Path solutionPath = outputPath; solutionPath.append(data.name + L".sln"); for (auto& project : data.projects) { WString projectString = VisualStudio::writeProject(mVersion, project); Path projectPath = outputPath; projectPath.append(project.name + L".csproj"); DataStreamPtr projectStream = FileSystem::createAndOpenFile(projectPath); projectStream->write(projectString.c_str(), projectString.size()); projectStream->close(); } DataStreamPtr solutionStream = FileSystem::createAndOpenFile(solutionPath); solutionStream->write(solutionString.c_str(), solutionString.size()); solutionStream->close(); } VSCodeEditorFactory::VSCodeEditorFactory() :mAvailableVersions(getAvailableVersions()) { for (auto& version : mAvailableVersions) mAvailableEditors.push_back(version.first); } Map VSCodeEditorFactory::getAvailableVersions() const { #if BS_ARCH_TYPE == BS_ARCHITECTURE_x86_64 bool is64bit = true; #else bool is64bit = false; IsWow64Process(GetCurrentProcess(), &is64bit); #endif WString registryKeyRoot; if (is64bit) registryKeyRoot = L"SOFTWARE\\Microsoft"; else registryKeyRoot = L"SOFTWARE\\Wow6432Node\\Microsoft"; struct VersionData { WString registryKey; WString name; WString executable; }; Map versionToVersionNumber = { { VisualStudioVersion::VS2008, { L"VisualStudio\\9.0", L"Visual Studio 2008", L"devenv.exe" } }, { VisualStudioVersion::VS2010, { L"VisualStudio\\10.0", L"Visual Studio 2010", L"devenv.exe" } }, { VisualStudioVersion::VS2012, { L"VisualStudio\\11.0", L"Visual Studio 2012", L"devenv.exe" } }, { VisualStudioVersion::VS2013, { L"VisualStudio\\12.0", L"Visual Studio 2013", L"devenv.exe" } }, { VisualStudioVersion::VS2015, { L"VisualStudio\\13.0", L"Visual Studio 2015", L"devenv.exe" } } }; Map versionInfo; for(auto version : versionToVersionNumber) { WString registryKey = registryKeyRoot + L"\\" + version.second.registryKey; HKEY regKey; LONG result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, registryKeyRoot.c_str(), 0, KEY_READ, ®Key); if (result != ERROR_SUCCESS) continue; WString installPath; getRegistryStringValue(regKey, L"InstallDir", installPath, StringUtil::WBLANK); if (installPath.empty()) continue; WString clsID; getRegistryStringValue(regKey, L"ThisVersionDTECLSID", clsID, StringUtil::WBLANK); VSVersionInfo info; info.name = version.second.name; info.execPath = installPath.append(version.second.executable); info.CLSID = clsID; info.version = version.first; versionInfo[info.name] = info; } // TODO - Also query for VSExpress and VSCommunity (their registry keys are different) return versionInfo; } CodeEditor* VSCodeEditorFactory::create(const WString& editor) const { auto findIter = mAvailableVersions.find(editor); if (findIter == mAvailableVersions.end()) return nullptr; // TODO - Also create VSExpress and VSCommunity editors return bs_new(findIter->second.version, findIter->second.execPath, findIter->second.CLSID); } }