You're breathtaking!

This commit is contained in:
Wojtek Figat
2020-12-07 23:40:54 +01:00
commit 6fb9eee74c
5143 changed files with 1153594 additions and 0 deletions

View File

@@ -0,0 +1,259 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "CodeEditor.h"
#include "CodeEditors/SystemDefaultCodeEditor.h"
#include "Engine/Platform/FileSystem.h"
#include "ScriptsBuilder.h"
#include "CodeEditors/VisualStudioCodeEditor.h"
#if USE_VISUAL_STUDIO_DTE
#include "CodeEditors/VisualStudio/VisualStudioEditor.h"
#endif
#include "Engine/Core/Log.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Platform/Thread.h"
#include "Engine/Threading/IRunnable.h"
void OnAsyncBegin(Thread* thread);
void OnAsyncEnd();
class AsyncOpenTask : public IRunnable
{
private:
bool _isSolutionOpenTask;
String _path;
int32 _line;
CodeEditor* _editor;
public:
AsyncOpenTask(const String& path, int32 line, CodeEditor* editor)
: _isSolutionOpenTask(false)
, _path(path)
, _line(line)
, _editor(editor)
{
}
AsyncOpenTask(CodeEditor* editor)
: _isSolutionOpenTask(true)
, _line(0)
, _editor(editor)
{
}
static void OpenSolution(CodeEditor* editor)
{
auto task = New<AsyncOpenTask>(editor);
auto thread = Thread::Create(task, task->ToString(), ThreadPriority::BelowNormal);
if (thread == nullptr)
{
Delete(task);
LOG(Error, "Failed to start a thread to open solution");
return;
}
OnAsyncBegin(thread);
}
static void OpenFile(CodeEditor* editor, const String& filePath, int32 line)
{
auto task = New<AsyncOpenTask>(filePath, line, editor);
auto thread = Thread::Create(task, task->ToString(), ThreadPriority::BelowNormal);
if (thread == nullptr)
{
Delete(task);
LOG(Error, "Failed to start a thread to open file");
return;
}
OnAsyncBegin(thread);
}
public:
// [IRunnable]
String ToString() const override
{
return TEXT("Code Editor open");
}
int32 Run() override
{
if (_isSolutionOpenTask)
{
_editor->OpenSolution();
}
else
{
_editor->OpenFile(_path, _line);
}
return 0;
}
void AfterWork(bool wasKilled) override
{
OnAsyncEnd();
Delete(this);
}
};
class CodeEditingManagerService : public EngineService
{
public:
CodeEditingManagerService()
: EngineService(TEXT("Code Editing Manager"))
{
}
bool Init() override;
void Dispose() override;
};
CodeEditingManagerService CodeEditingManagerServiceInstance;
Array<CodeEditor*> CodeEditors;
Thread* AsyncOpenThread = nullptr;
Action CodeEditingManager::AsyncOpenBegin;
Action CodeEditingManager::AsyncOpenEnd;
const Array<CodeEditor*>& CodeEditingManager::GetEditors()
{
return CodeEditors;
}
bool CodeEditingManager::IsAsyncOpenRunning()
{
return AsyncOpenThread != nullptr;
}
CodeEditor* CodeEditingManager::GetCodeEditor(CodeEditorTypes editorType)
{
for (int32 i = 0; i < CodeEditors.Count(); i++)
{
if (CodeEditors[i]->GetType() == editorType)
return CodeEditors[i];
}
return nullptr;
}
void CodeEditingManager::OpenFile(CodeEditorTypes editorType, const String& path, int32 line)
{
const auto editor = GetCodeEditor(editorType);
if (editor)
{
OpenFile(editor, path, line);
}
else
{
LOG(Warning, "Missing code editor type {0}", (int32)editorType);
}
}
void CodeEditingManager::OpenFile(CodeEditor* editor, const String& path, int32 line)
{
// Ensure that file exists
if (!FileSystem::FileExists(path))
return;
// Ensure that no async task is running
if (IsAsyncOpenRunning())
{
// TODO: enqueue action and handle many actions in the queue
LOG(Warning, "Cannot use code editor during async open action.");
return;
}
if (editor->UseAsyncForOpen())
{
AsyncOpenTask::OpenFile(editor, path, line);
}
else
{
editor->OpenFile(path, line);
}
}
void CodeEditingManager::OpenSolution(CodeEditorTypes editorType)
{
const auto editor = GetCodeEditor(editorType);
if (editor)
{
editor->OpenSolution();
}
else
{
LOG(Warning, "Missing code editor type {0}", (int32)editorType);
}
}
void CodeEditingManager::OnFileAdded(CodeEditorTypes editorType, const String& path)
{
const auto editor = GetCodeEditor(editorType);
if (editor)
{
editor->OnFileAdded(path);
}
else
{
LOG(Warning, "Missing code editor type {0}", (int32)editorType);
}
}
void CodeEditingManager::OpenSolution(CodeEditor* editor)
{
// Ensure that no async task is running
if (IsAsyncOpenRunning())
{
// TODO: enqueue action and handle many actions in the queue
LOG(Warning, "Cannot use code editor during async open action.");
return;
}
if (editor->UseAsyncForOpen())
{
AsyncOpenTask::OpenSolution(editor);
}
else
{
editor->OpenSolution();
}
}
void OnAsyncBegin(Thread* thread)
{
ASSERT(AsyncOpenThread == nullptr);
AsyncOpenThread = thread;
CodeEditingManager::AsyncOpenBegin();
}
void OnAsyncEnd()
{
ASSERT(AsyncOpenThread != nullptr);
AsyncOpenThread = nullptr;
CodeEditingManager::AsyncOpenEnd();
}
bool CodeEditingManagerService::Init()
{
// Try get editors
#if USE_VISUAL_STUDIO_DTE
VisualStudioEditor::FindEditors(&CodeEditors);
#endif
VisualStudioCodeEditor::FindEditors(&CodeEditors);
CodeEditors.Add(New<SystemDefaultCodeEditor>());
return false;
}
void CodeEditingManagerService::Dispose()
{
// Stop async task
if (CodeEditingManager::IsAsyncOpenRunning())
{
AsyncOpenThread->Kill(true);
}
// Cleanup
CodeEditors.ClearDelete();
}

View File

@@ -0,0 +1,196 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Types.h"
#include "Engine/Core/Delegate.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Scripting/ScriptingType.h"
/// <summary>
/// Types of in-build code editors.
/// </summary>
API_ENUM(Namespace="FlaxEditor", Attributes="HideInEditor") enum class CodeEditorTypes
{
/// <summary>
/// Custom/external editor
/// </summary>
Custom,
/// <summary>
/// Default program associated by the file extension on the system
/// </summary>
SystemDefault,
/// <summary>
/// Visual Studio 2008
/// </summary>
VS2008,
/// <summary>
/// Visual Studio 2010
/// </summary>
VS2010,
/// <summary>
/// Visual Studio 2012
/// </summary>
VS2012,
/// <summary>
/// Visual Studio 2013
/// </summary>
VS2013,
/// <summary>
/// Visual Studio 2059
/// </summary>
VS2015,
/// <summary>
/// Visual Studio 2017
/// </summary>
VS2017,
/// <summary>
/// Visual Studio 2019
/// </summary>
VS2019,
/// <summary>
/// Visual Studio Code
/// </summary>
VSCode,
MAX
};
/// <summary>
/// Base class for all code editors.
/// </summary>
class CodeEditor
{
public:
/// <summary>
/// Finalizes an instance of the <see cref="CodeEditor"/> class.
/// </summary>
virtual ~CodeEditor() = default;
/// <summary>
/// Gets the type of the editor (used by the in-build editors).
/// </summary>
/// <returns>The name</returns>
virtual CodeEditorTypes GetType() const
{
return CodeEditorTypes::Custom;
}
/// <summary>
/// Gets the name of the editor.
/// </summary>
/// <returns>The name</returns>
virtual String GetName() const = 0;
/// <summary>
/// Opens the file.
/// </summary>
/// <param name="path">The file path.</param>
/// <param name="line">The target line (use 0 to not use it).</param>
virtual void OpenFile(const String& path, int32 line) = 0;
/// <summary>
/// Opens the solution project.
/// </summary>
virtual void OpenSolution() = 0;
/// <summary>
/// Called when source file gets added to the workspace. Can be used to automatically include new files into the project files.
/// </summary>
/// <param name="path">The path.</param>
virtual void OnFileAdded(const String& path)
{
}
/// <summary>
/// Determines whether use the asynchronous task for open solution/file.
/// </summary>
/// <returns>True if use async thread for open task.</returns>
virtual bool UseAsyncForOpen() const
{
return false;
}
};
/// <summary>
/// Editor utility to managed and use different code editors. Allows to open solution and source code files.
/// </summary>
API_CLASS(Static, Namespace="FlaxEditor") class CodeEditingManager
{
DECLARE_SCRIPTING_TYPE_NO_SPAWN(CodeEditingManager);
public:
/// <summary>
/// Gets all found editors.
/// </summary>
/// <returns>The editors. Read-only.</returns>
static const Array<CodeEditor*>& GetEditors();
/// <summary>
/// Determines whether asynchronous open action is running in a background.
/// </summary>
/// <returns><c>true</c> if asynchronous open action is running in a background; otherwise, <c>false</c>.</returns>
API_PROPERTY() static bool IsAsyncOpenRunning();
/// <summary>
/// Gets the in-build code editor or null if not found.
/// </summary>
/// <param name="editorType">Type of the editor.</param>
/// <returns>The editor object or null if not found.</returns>
static CodeEditor* GetCodeEditor(CodeEditorTypes editorType);
/// <summary>
/// Opens the file. Handles async opening.
/// </summary>
/// <param name="editorType">The code editor type.</param>
/// <param name="path">The file path.</param>
/// <param name="line">The target line (use 0 to not use it).</param>
API_FUNCTION() static void OpenFile(CodeEditorTypes editorType, const String& path, int32 line);
/// <summary>
/// Opens the file. Handles async opening.
/// </summary>
/// <param name="editor">The code editor.</param>
/// <param name="path">The file path.</param>
/// <param name="line">The target line (use 0 to not use it).</param>
static void OpenFile(CodeEditor* editor, const String& path, int32 line);
/// <summary>
/// Opens the solution project. Handles async opening.
/// </summary>
/// <param name="editorType">The code editor type.</param>
API_FUNCTION() static void OpenSolution(CodeEditorTypes editorType);
/// <summary>
/// Called when source file gets added to the workspace. Can be used to automatically include new files into the project files.
/// </summary>
/// <param name="editorType">The code editor type.</param>
/// <param name="path">The path.</param>
API_FUNCTION() static void OnFileAdded(CodeEditorTypes editorType, const String& path);
/// <summary>
/// Opens the solution project. Handles async opening.
/// </summary>
/// <param name="editor">The code editor.</param>
static void OpenSolution(CodeEditor* editor);
/// <summary>
/// The asynchronous open begins.
/// </summary>
static Action AsyncOpenBegin;
/// <summary>
/// The asynchronous open ends.
/// </summary>
static Action AsyncOpenEnd;
};

View File

@@ -0,0 +1,23 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "SystemDefaultCodeEditor.h"
#include "Engine/Core/Types/StringView.h"
CodeEditorTypes SystemDefaultCodeEditor::GetType() const
{
return CodeEditorTypes::SystemDefault;
}
String SystemDefaultCodeEditor::GetName() const
{
return TEXT("System Default");
}
void SystemDefaultCodeEditor::OpenFile(const String& path, int32 line)
{
Platform::StartProcess(path, StringView::Empty, StringView::Empty);
}
void SystemDefaultCodeEditor::OpenSolution()
{
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Editor/Scripting/CodeEditor.h"
/// <summary>
/// Basic implementation for editing source code on a target platform.
/// </summary>
class SystemDefaultCodeEditor : public CodeEditor
{
public:
// [CodeEditor]
CodeEditorTypes GetType() const override;
String GetName() const override;
void OpenFile(const String& path, int32 line) override;
void OpenSolution() override;
};

View File

@@ -0,0 +1,796 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if USE_VISUAL_STUDIO_DTE
// Import EnvDTE
#pragma warning(disable : 4278)
#pragma warning(disable : 4146)
#import "libid:80cc9f66-e7d8-4ddd-85b6-d9e6cd0e93e2" version("8.0") lcid("0") raw_interfaces_only named_guids
#pragma warning(default : 4146)
#pragma warning(default : 4278)
// Import Microsoft.VisualStudio.Setup.Configuration.Native
#include <Microsoft.VisualStudio.Setup.Configuration.Native/Setup.Configuration.h>
#pragma comment(lib, "Microsoft.VisualStudio.Setup.Configuration.Native.lib")
#include "VisualStudioConnection.h"
#include "Engine/Platform/Windows/ComPtr.h"
/// <summary>
/// Handles retrying of calls that fail to access Visual Studio.
/// This is due to the weird nature of VS when calling its methods from external code.
/// If this message filter isn't registered some calls will just fail silently.
/// </summary>
class VSMessageFilter : public IMessageFilter
{
private:
LONG _refCount;
public:
VSMessageFilter()
{
_refCount = 0;
}
virtual ~VSMessageFilter()
{
}
public:
DWORD __stdcall HandleInComingCall(DWORD dwCallType, HTASK htaskCaller, DWORD dwTickCount, LPINTERFACEINFO lpInterfaceInfo) override
{
return SERVERCALL_ISHANDLED;
}
DWORD __stdcall RetryRejectedCall(HTASK htaskCallee, DWORD dwTickCount, DWORD dwRejectType) override
{
if (dwRejectType == SERVERCALL_RETRYLATER)
{
// Retry immediatey
return 99;
}
// Cancel the call
return -1;
}
DWORD __stdcall MessagePending(HTASK htaskCallee, DWORD dwTickCount, DWORD dwPendingType) override
{
return PENDINGMSG_WAITDEFPROCESS;
}
// COM requirement. Returns instance of an interface of provided type.
HRESULT __stdcall QueryInterface(REFIID iid, void** ppvObject) override
{
if (iid == IID_IDropTarget || iid == IID_IUnknown)
{
AddRef();
*ppvObject = this;
return S_OK;
}
*ppvObject = nullptr;
return E_NOINTERFACE;
}
// COM requirement. Increments objects reference count.
ULONG __stdcall AddRef() override
{
return _InterlockedIncrement(&_refCount);
}
// COM requirement. Decreases the objects reference count and deletes the object if its zero.
ULONG __stdcall Release() override
{
LONG count = _InterlockedDecrement(&_refCount);
if (count == 0)
{
delete this;
return 0;
}
return count;
}
};
// Helper macro
#define CHECK_VS_RESULT(target) if (FAILED(result)) return #target " failed with result: " + std::to_string((int)result);
#define USE_ITEM_OPERATIONS_OPEN 0
#define USE_PROJECT_ITEM_OPEN 1
#define USE_DOCUMENT_OPEN 0
class LocalBSTR
{
public:
BSTR Str;
public:
LocalBSTR()
{
Str = nullptr;
}
LocalBSTR(const LPSTR str)
{
const auto wslen = MultiByteToWideChar(CP_ACP, 0, str, (int)strlen(str), nullptr, 0);
Str = SysAllocStringLen(0, wslen);
MultiByteToWideChar(CP_ACP, 0, str, (int)strlen(str), Str, wslen);
}
LocalBSTR(const wchar_t* str)
{
Str = ::SysAllocString(str);
}
~LocalBSTR()
{
if (Str)
{
::SysFreeString(Str);
}
}
public:
operator const BSTR() const
{
return Str;
}
operator const wchar_t*() const
{
return Str;
}
};
namespace VisualStudio
{
bool SameFile(HANDLE h1, HANDLE h2)
{
BY_HANDLE_FILE_INFORMATION bhfi1 = { 0 };
BY_HANDLE_FILE_INFORMATION bhfi2 = { 0 };
if (::GetFileInformationByHandle(h1, &bhfi1) && ::GetFileInformationByHandle(h2, &bhfi2))
{
return ((bhfi1.nFileIndexHigh == bhfi2.nFileIndexHigh) && (bhfi1.nFileIndexLow == bhfi2.nFileIndexLow) && (bhfi1.dwVolumeSerialNumber == bhfi2.dwVolumeSerialNumber));
}
return false;
}
bool AreFilePathsEqual(const wchar_t* path1, const wchar_t* path2)
{
if (wcscmp(path1, path2) == 0)
return true;
HANDLE file1 = CreateFileW(path1, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
HANDLE file2 = CreateFileW(path2, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
bool result = SameFile(file1, file2);
CloseHandle(file1);
CloseHandle(file2);
return result;
}
class ConnectionInternal
{
public:
const wchar_t* ClsID;
LocalBSTR SolutionPath;
CLSID CLSID;
ComPtr<EnvDTE::_DTE> DTE;
public:
ConnectionInternal(const wchar_t* clsID, const wchar_t* solutionPath)
: ClsID(clsID)
, SolutionPath(solutionPath)
{
}
public:
bool IsValid() const
{
return DTE != nullptr;
}
};
// Scans the running processes to find a running Visual Studio instance with the specified version and open solution.
//
// @param[in] clsID Class ID of the specific Visual Studio version we are looking for.
// @param[in] solutionPath Path to the solution the instance needs to have open.
// @return DTE object that may be used to interact with the Visual Studio instance, or null if
// not found.
static Result FindRunningInstance(ConnectionHandle connection)
{
HRESULT result;
ComPtr<IRunningObjectTable> runningObjectTable;
result = GetRunningObjectTable(0, &runningObjectTable);
CHECK_VS_RESULT("VisualStudio::FindRunningInstance - GetRunningObjectTable");
ComPtr<IEnumMoniker> enumMoniker;
result = runningObjectTable->EnumRunning(&enumMoniker);
CHECK_VS_RESULT("VisualStudio::FindRunningInstance - EnumRunning");
ComPtr<IMoniker> dteMoniker;
result = CreateClassMoniker(connection->CLSID, &dteMoniker);
CHECK_VS_RESULT("VisualStudio::FindRunningInstance - CreateClassMoniker");
ComPtr<IMoniker> moniker;
ULONG count = 0;
while (enumMoniker->Next(1, &moniker, &count) == S_OK)
{
if (moniker->IsEqual(dteMoniker))
{
ComPtr<IUnknown> curObject;
result = runningObjectTable->GetObjectW(moniker, &curObject);
moniker.Detach();
if (result != S_OK)
continue;
ComPtr<EnvDTE::_DTE> dte;
curObject->QueryInterface(__uuidof(EnvDTE::_DTE), (void**)&dte);
if (dte == nullptr)
continue;
ComPtr<EnvDTE::_Solution> solution;
if (FAILED(dte->get_Solution(&solution)))
continue;
LocalBSTR fullName;
if (FAILED(solution->get_FullName(&fullName.Str)))
continue;
if (AreFilePathsEqual(connection->SolutionPath, fullName))
{
// Found
connection->DTE = dte;
break;
}
}
}
return Result::Ok;
}
// Opens a new Visual Studio instance of the specified version with the provided solution.
//
// @param[in] clsID Class ID of the specific Visual Studio version to start.
// @param[in] solutionPath Path to the solution the instance needs to open.
static Result OpenInstance(ConnectionHandle connection)
{
HRESULT result;
ComPtr<IUnknown> newInstance = nullptr;
result = CoCreateInstance(connection->CLSID, nullptr, CLSCTX_LOCAL_SERVER, EnvDTE::IID__DTE, (LPVOID*)&newInstance);
CHECK_VS_RESULT("VisualStudio::OpenInstance - CoCreateInstance");
newInstance->QueryInterface(&connection->DTE);
if (connection->DTE == nullptr)
return "Invalid DTE handle";
connection->DTE->put_UserControl(TRUE);
ComPtr<EnvDTE::_Solution> solution;
result = connection->DTE->get_Solution(&solution);
CHECK_VS_RESULT("VisualStudio::OpenInstance - dte->get_Solution");
result = solution->Open(connection->SolutionPath);
CHECK_VS_RESULT("VisualStudio::OpenInstance - solution->Open");
// Wait until VS opens
int maxWaitMs = 10 * 1000;
int elapsedMs = 0;
int stepTimeMs = 100;
while (elapsedMs < maxWaitMs)
{
ComPtr<EnvDTE::Window> window = nullptr;
if (SUCCEEDED(connection->DTE->get_MainWindow(&window)))
return Result::Ok;
Sleep(stepTimeMs);
elapsedMs += stepTimeMs;
}
return "Visual Studio open timout";
}
static ComPtr<EnvDTE::ProjectItem> FindItem(const ComPtr<EnvDTE::ProjectItems>& projectItems, BSTR filePath)
{
long count;
projectItems->get_Count(&count);
if (count == 0)
return nullptr;
for (long i = 1; i <= count; i++) // They are counting from [1..Count]
{
ComPtr<EnvDTE::ProjectItem> projectItem;
projectItems->Item(_variant_t(i), &projectItem);
if (!projectItem)
continue;
short fileCount = 0;
projectItem->get_FileCount(&fileCount);
for (short fileIndex = 1; fileIndex <= fileCount; fileIndex++)
{
_bstr_t filename;
projectItem->get_FileNames(fileIndex, filename.GetAddress());
if (filename.GetBSTR() != nullptr && AreFilePathsEqual(filePath, filename))
{
return projectItem;
}
}
ComPtr<EnvDTE::ProjectItems> childProjectItems;
projectItem->get_ProjectItems(&childProjectItems);
if (childProjectItems)
{
ComPtr<EnvDTE::ProjectItem> result = FindItem(childProjectItems, filePath);
if (result)
return result;
}
}
return nullptr;
}
static ComPtr<EnvDTE::ProjectItem> FindItem(const ComPtr<EnvDTE::_Solution>& solution, BSTR filePath)
{
HRESULT result;
ComPtr<EnvDTE::Projects> projects;
result = solution->get_Projects(&projects);
if (FAILED(result))
return nullptr;
long projectsCount = 0;
result = projects->get_Count(&projectsCount);
if (FAILED(result))
return nullptr;
for (long projectIndex = 1; projectIndex <= projectsCount; projectIndex++) // They are counting from [1..Count]
{
ComPtr<EnvDTE::Project> project;
result = projects->Item(_variant_t(projectIndex), &project);
if (FAILED(result) || !project)
continue;
ComPtr<EnvDTE::ProjectItems> projectItems;
result = project->get_ProjectItems(&projectItems);
if (FAILED(result) || !projectItems)
continue;
auto projectItem = FindItem(projectItems, filePath);
if (projectItem)
{
return projectItem;
}
}
return nullptr;
}
// Opens a file on a specific line in a running Visual Studio instance.
//
// @param[in] dte DTE object retrieved from FindRunningInstance() or OpenInstance().
// @param[in] filePath Path of the file to open. File should be a part of the VS solution.
// @param[in] line Line on which to focus Visual Studio after the file is open.
static Result OpenFile(ConnectionHandle handle, BSTR filePath, unsigned int line)
{
HRESULT result;
LocalBSTR viewKind(EnvDTE::vsViewKindPrimary);
#if USE_ITEM_OPERATIONS_OPEN
ComPtr<EnvDTE::ItemOperations> itemOperations;
result = handle->DTE->get_ItemOperations(&itemOperations);
CHECK_VS_RESULT("VisualStudio::OpenFile - DTE->get_ItemOperations");
#endif
// Check if that file is opened
VARIANT_BOOL isOpen = 0;
#if USE_ITEM_OPERATIONS_OPEN
result = itemOperations->IsFileOpen(filePath, viewKind.Str, &isOpen);
CHECK_VS_RESULT("VisualStudio::OpenFile - itemOperations->IsFileOpen");
#else
result = handle->DTE->get_IsOpenFile(viewKind.Str, filePath, &isOpen);
CHECK_VS_RESULT("VisualStudio::OpenFile - DTE->get_IsOpenFile");
#endif
// Open or navigate to a window with a file
ComPtr<EnvDTE::Window> window;
ComPtr<EnvDTE::Document> document;
if (isOpen == 0)
{
// Open file
#if USE_DOCUMENT_OPEN
ComPtr<EnvDTE::Documents> documents;
result = handle->DTE->get_Documents(&documents);
CHECK_VS_RESULT("VisualStudio::OpenInstance - DTE->get_Documents");
ComPtr<EnvDTE::Document> tmp;
LocalBSTR kind(_T("Auto"));
result = documents->Open(filePath, kind.Str, VARIANT_FALSE, &tmp);
CHECK_VS_RESULT("VisualStudio::OpenInstance - documents->Open");
result = tmp->get_ActiveWindow(&window);
CHECK_VS_RESULT("VisualStudio::OpenFile - tmp->get_ActiveWindow");
#elif USE_PROJECT_ITEM_OPEN
ComPtr<EnvDTE::_Solution> solution;
result = handle->DTE->get_Solution(&solution);
CHECK_VS_RESULT("VisualStudio::OpenInstance - DTE->get_Solution");
ComPtr<EnvDTE::ProjectItem> projectItem = FindItem(solution, filePath);
if (projectItem)
{
result = projectItem->Open(viewKind, &window);
CHECK_VS_RESULT("VisualStudio::OpenFile - projectItem->Open");
}
#elif USE_ITEM_OPERATIONS_OPEN
result = itemOperations->OpenFile(filePath, viewKind, &window);
CHECK_VS_RESULT("VisualStudio::OpenFile - itemOperations->OpenFile");
#else
result = handle->DTE->OpenFile(viewKind, filePath, &window);
CHECK_VS_RESULT("VisualStudio::OpenFile - DTE->OpenFile");
#endif
if (window == nullptr)
return Result::Ok;
// Activate window and get document handle
result = window->Activate();
CHECK_VS_RESULT("VisualStudio::OpenFile - window->Activate");
result = handle->DTE->get_ActiveDocument(&document);
CHECK_VS_RESULT("VisualStudio::OpenFile - dte->get_ActiveDocument");
}
else
{
// Find opened document
ComPtr<EnvDTE::Documents> documents;
result = handle->DTE->get_Documents(&documents);
CHECK_VS_RESULT("VisualStudio::OpenFile - DTE->get_Documents");
long documentsCount;
result = documents->get_Count(&documentsCount);
CHECK_VS_RESULT("VisualStudio::OpenFile - documents->get_Count");
for (int i = 1; i <= documentsCount; i++) // They are counting from [1..Count]
{
ComPtr<EnvDTE::Document> tmp;
result = documents->Item(_variant_t(i), &tmp);
CHECK_VS_RESULT("VisualStudio::OpenFile - documents->Item");
if (tmp == nullptr)
continue;
BSTR tmpPath;
result = tmp->get_FullName(&tmpPath);
CHECK_VS_RESULT("VisualStudio::OpenFile - tmp->get_FullName");
if (AreFilePathsEqual(filePath, tmpPath))
{
result = tmp->Activate();
CHECK_VS_RESULT("VisualStudio::OpenFile - tmp->Activate");
// Found
document = tmp;
break;
}
}
}
if (document == nullptr)
return "Cannot open a file";
// Check if need to select a given line
if (line != 0)
{
ComPtr<IDispatch> selection;
result = document->get_Selection(&selection);
CHECK_VS_RESULT("VisualStudio::OpenFile - activeDocument->get_Selection");
if (selection == nullptr)
return Result::Ok;
ComPtr<EnvDTE::TextSelection> textSelection;
result = selection->QueryInterface(&textSelection);
CHECK_VS_RESULT("VisualStudio::OpenFile - selection->QueryInterface");
textSelection->GotoLine(line, VARIANT_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 Result::Ok;
}
// Adds a file to the project opened in a running Visual Studio instance.
//
// @param[in] dte DTE object retrieved from FindRunningInstance() or OpenInstance().
// @param[in] filePath Path of the file to add.
// @param[in] localPath Path of the file to add relative to the solution folder.
static Result AddFile(ConnectionHandle handle, BSTR filePath, const wchar_t* localPath)
{
HRESULT result;
ComPtr<EnvDTE::_Solution> solution;
result = handle->DTE->get_Solution(&solution);
CHECK_VS_RESULT("VisualStudio::OpenInstance - DTE->get_Solution");
ComPtr<EnvDTE::ProjectItem> projectItem = FindItem(solution, filePath);
if (projectItem)
{
// Already added
return Result::Ok;
}
ComPtr<EnvDTE::Projects> projects;
result = solution->get_Projects(&projects);
if (FAILED(result))
return nullptr;
long projectsCount = 0;
result = projects->get_Count(&projectsCount);
if (FAILED(result))
return nullptr;
ComPtr<EnvDTE::Project> project;
wchar_t buffer[500];
// Place .Build.cs scripts into BuildScripts project
const int localPathLength = (int)wcslen(localPath);
if (localPathLength >= 10 && _wcsicmp(localPath + localPathLength - ARRAY_COUNT(".Build.cs") + 1, TEXT(".Build.cs")) == 0)
{
for (long projectIndex = 1; projectIndex <= projectsCount; projectIndex++) // They are counting from [1..Count]
{
result = projects->Item(_variant_t(projectIndex), &project);
if (FAILED(result) || !project)
continue;
_bstr_t name;
if (SUCCEEDED(project->get_Name(name.GetAddress())) && wcscmp(name, TEXT("BuildScripts")) == 0)
break;
project = nullptr;
}
}
else
{
// TODO: find the project to add script to? for .cs scripts it should be csproj, for c++ vcproj?
// Find parent folder
wchar_t* path = filePath + wcslen(localPath) + 1;
int length = (int)wcslen(path);
memcpy(buffer, path, length * sizeof(wchar_t));
for (int i = length - 1; i >= 0; i--)
{
if (buffer[i] == '\\' || buffer[i] == '/')
{
buffer[i] = '\0';
length = i;
break;
}
}
const LocalBSTR parentPath(TEXT("MyProject"));
projectItem = FindItem(solution, parentPath);
// ...
}
// TODO: add file and all missing parent folders to the project
return Result::Ok;
}
class CleanupHelper
{
public:
IMessageFilter* oldFilter;
VSMessageFilter* newFilter;
CleanupHelper()
{
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
newFilter = new VSMessageFilter();
CoRegisterMessageFilter(newFilter, &oldFilter);
}
~CleanupHelper()
{
CoRegisterMessageFilter(oldFilter, nullptr);
CoUninitialize();
}
};
};
VisualStudio::Result VisualStudio::Result::Ok;
int VisualStudio::GetVisualStudioVersions(InstanceInfo* infos, int infosCount)
{
// Try to create the CoCreate the class; if that fails, likely no instances are registered
ComPtr<ISetupConfiguration2> query;
HRESULT result = CoCreateInstance(__uuidof(SetupConfiguration), nullptr, CLSCTX_ALL, __uuidof(ISetupConfiguration2), (LPVOID*)&query);
if (FAILED(result))
{
return 0;
}
// Get the enumerator
ComPtr<IEnumSetupInstances> enumSetupInstances;
result = query->EnumAllInstances(&enumSetupInstances);
if (FAILED(result))
{
return 0;
}
// Check the state and version of the enumerated instances
int32 count = 0;
ComPtr<ISetupInstance> instance;
while(count < infosCount)
{
ULONG fetched = 0;
result = enumSetupInstances->Next(1, &instance, &fetched);
if (FAILED(result) || fetched == 0)
{
break;
}
ComPtr<ISetupInstance2> setupInstance2;
result = instance->QueryInterface(__uuidof(ISetupInstance2), (LPVOID*)&setupInstance2);
if (SUCCEEDED(result))
{
InstanceState state;
result = setupInstance2->GetState(&state);
if (SUCCEEDED(result) && (state & eLocal) != 0)
{
BSTR installationVersion;
result = setupInstance2->GetInstallationVersion(&installationVersion);
if (SUCCEEDED(result))
{
BSTR installationPath;
result = setupInstance2->GetInstallationPath(&installationPath);
if (SUCCEEDED(result))
{
BSTR productPath;
result = setupInstance2->GetProductPath(&productPath);
if (SUCCEEDED(result))
{
auto& info = infos[count++];
char version[3];
version[0] = (char)installationVersion[0];
version[1] = (char)installationVersion[1];
version[2] = 0;
info.VersionMajor = atoi(version);
swprintf_s(info.ExecutablePath, MAX_PATH, L"%s\\%s", installationPath, productPath);
wchar_t progID[100];
swprintf_s(progID, 100, L"VisualStudio.DTE.%d.0", info.VersionMajor);
CLSID clsid;
CLSIDFromProgID(progID, &clsid);
StringFromGUID2(clsid, info.CLSID, 40);
SysFreeString(productPath);
}
SysFreeString(installationPath);
}
SysFreeString(installationVersion);
}
}
}
}
return count;
}
void VisualStudio::OpenConnection(ConnectionHandle& connection, const wchar_t* clsID, const wchar_t* solutionPath)
{
connection = new ConnectionInternal(clsID, solutionPath);
}
void VisualStudio::CloseConnection(ConnectionHandle& connection)
{
if (connection)
{
delete connection;
connection = nullptr;
}
}
bool VisualStudio::IsActive(const ConnectionHandle& connection)
{
// Check if already opened
if (connection->IsValid())
return true;
// Try to find active
auto e = FindRunningInstance(connection);
return connection->DTE != nullptr;
}
VisualStudio::Result VisualStudio::OpenSolution(ConnectionHandle connection)
{
// Check if already opened
if (connection->IsValid())
return Result::Ok;
// Temporary data
CleanupHelper helper;
HRESULT result;
// Cache VS version CLSID
result = CLSIDFromString(connection->ClsID, &connection->CLSID);
CHECK_VS_RESULT("VisualStudio::CLSIDFromString");
// Get or open VS with solution
auto e = FindRunningInstance(connection);
if (e.Failed())
return e;
if (connection->DTE == nullptr)
{
e = OpenInstance(connection);
if (e.Failed())
return e;
if (connection->DTE == nullptr)
return "Cannot open Visual Studio";
}
// Focus VS main window
ComPtr<EnvDTE::Window> window;
if (SUCCEEDED(connection->DTE->get_MainWindow(&window)))
window->Activate();
return Result::Ok;
}
VisualStudio::Result VisualStudio::OpenFile(ConnectionHandle connection, const wchar_t* path, unsigned int line)
{
// Ensure to have valid connection
auto result = OpenSolution(connection);
if (result.Failed())
return result;
// Open file
CleanupHelper helper;
const LocalBSTR pathBstr(path);
return OpenFile(connection, pathBstr.Str, line);
}
VisualStudio::Result VisualStudio::AddFile(ConnectionHandle connection, const wchar_t* path, const wchar_t* localPath)
{
// Ensure to have valid connection
auto result = OpenSolution(connection);
if (result.Failed())
return result;
// Add file
CleanupHelper helper;
LocalBSTR pathBstr(path);
return AddFile(connection, pathBstr.Str, localPath);
}
#endif

View File

@@ -0,0 +1,108 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#if USE_VISUAL_STUDIO_DTE
#include "Engine/Platform/Win32/IncludeWindowsHeaders.h"
#include <string>
/// <summary>
/// Contains various helper classes for interacting with a Visual Studio instance running on this machine.
/// </summary>
namespace VisualStudio
{
/// <summary>
/// Visual Studio connection operation result
/// </summary>
struct Result
{
static Result Ok;
std::string Message;
Result()
{
Message = "";
}
Result(const char* msg)
{
Message = msg;
}
Result(const std::string& msg)
{
Message = msg;
}
bool Failed() const
{
return Message.size() > 0;
}
};
struct InstanceInfo
{
wchar_t CLSID[40];
wchar_t ExecutablePath[MAX_PATH];
int VersionMajor;
};
class ConnectionInternal;
typedef ConnectionInternal* ConnectionHandle;
int GetVisualStudioVersions(InstanceInfo* infos, int infosCount);
void OpenConnection(ConnectionHandle& connection, const wchar_t* clsID, const wchar_t* solutionPath);
void CloseConnection(ConnectionHandle& connection);
bool IsActive(const ConnectionHandle& connection);
Result OpenSolution(ConnectionHandle connection);
Result OpenFile(ConnectionHandle connection, const wchar_t* path, unsigned int line);
Result AddFile(ConnectionHandle connection, const wchar_t* path, const wchar_t* localPath);
/// <summary>
/// Visual Studio connection wrapper class
/// </summary>
class Connection
{
private:
ConnectionHandle Handle;
public:
Connection(const wchar_t* clsID, const wchar_t* solutionPath)
{
OpenConnection(Handle, clsID, solutionPath);
}
~Connection()
{
CloseConnection(Handle);
}
public:
bool IsActive() const
{
return VisualStudio::IsActive(Handle);
}
Result OpenSolution() const
{
return VisualStudio::OpenSolution(Handle);
}
Result OpenFile(const wchar_t* path, unsigned int line) const
{
return VisualStudio::OpenFile(Handle, path, line);
}
Result AddFile(const wchar_t* path, const wchar_t* localPath) const
{
return VisualStudio::AddFile(Handle, path, localPath);
}
};
}
#endif

View File

@@ -0,0 +1,207 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if USE_VISUAL_STUDIO_DTE
#include "VisualStudioEditor.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Core/Log.h"
#include "Editor/Editor.h"
#include "Editor/Scripting/ScriptsBuilder.h"
#include "Engine/Engine/Globals.h"
#include "Engine/Platform/Win32/IncludeWindowsHeaders.h"
#include "VisualStudioConnection.h"
VisualStudioEditor::VisualStudioEditor(VisualStudioVersion version, const String& execPath, const String& CLSID)
: _version(version)
, _execPath(execPath)
, _CLSID(CLSID)
{
switch (version)
{
case VisualStudioVersion::VS2008:
_type = CodeEditorTypes::VS2008;
break;
case VisualStudioVersion::VS2010:
_type = CodeEditorTypes::VS2010;
break;
case VisualStudioVersion::VS2012:
_type = CodeEditorTypes::VS2012;
break;
case VisualStudioVersion::VS2013:
_type = CodeEditorTypes::VS2013;
break;
case VisualStudioVersion::VS2015:
_type = CodeEditorTypes::VS2015;
break;
case VisualStudioVersion::VS2017:
_type = CodeEditorTypes::VS2017;
break;
case VisualStudioVersion::VS2019:
_type = CodeEditorTypes::VS2019;
break;
default: CRASH;
break;
}
_solutionPath = Globals::ProjectFolder / Editor::Project->Name + TEXT(".sln");
}
void VisualStudioEditor::FindEditors(Array<CodeEditor*>* output)
{
String installDir;
String clsID;
String regVsRootNode;
// Select registry node for
if (Platform::Is64BitPlatform())
regVsRootNode = TEXT("SOFTWARE\\Wow6432Node\\Microsoft\\VisualStudio\\");
else
regVsRootNode = TEXT("SOFTWARE\\Microsoft\\VisualStudio\\");
VisualStudio::InstanceInfo infos[8];
const int32 count = VisualStudio::GetVisualStudioVersions(infos, ARRAY_COUNT(infos));
for (int32 i = 0; i < count; i++)
{
auto& info = infos[i];
VisualStudioVersion version;
switch (info.VersionMajor)
{
case 16:
version = VisualStudioVersion::VS2019;
break;
case 15:
version = VisualStudioVersion::VS2017;
break;
default:
break;
}
String executablePath(info.ExecutablePath);
if (!FileSystem::FileExists(executablePath))
continue;
// Create editor
clsID = info.CLSID;
auto editor = New<VisualStudioEditor>(version, executablePath, clsID);
output->Add(editor);
}
struct VersionData
{
VisualStudioVersion Version;
const Char* RegistryKey;
};
const VersionData versions[] =
{
// Order matters
{ VisualStudioVersion::VS2015, TEXT("14.0"), },
{ VisualStudioVersion::VS2013, TEXT("12.0"), },
{ VisualStudioVersion::VS2012, TEXT("11.0"), },
{ VisualStudioVersion::VS2012, TEXT("11.0"), },
{ VisualStudioVersion::VS2008, TEXT("9.0") },
};
for (int32 i = 0; i < ARRAY_COUNT(versions); i++)
{
auto registryKey = regVsRootNode + versions[i].RegistryKey;
// Read install directory
if (Platform::ReadRegValue(HKEY_LOCAL_MACHINE, registryKey, TEXT("InstallDir"), &installDir) || installDir.IsEmpty())
continue;
// Ensure that file exists
String execPath = installDir + TEXT("devenv.exe");
if (!FileSystem::FileExists(execPath))
continue;
// Read version info id
clsID.Clear();
Platform::ReadRegValue(HKEY_LOCAL_MACHINE, registryKey, TEXT("ThisVersionDTECLSID"), &clsID);
// Create editor
auto editor = New<VisualStudioEditor>(versions[i].Version, execPath, clsID);
output->Add(editor);
}
}
CodeEditorTypes VisualStudioEditor::GetType() const
{
return _type;
}
String VisualStudioEditor::GetName() const
{
return String(ToString(_version));
}
void VisualStudioEditor::OpenFile(const String& path, int32 line)
{
// Generate project files if solution is missing
if (!FileSystem::FileExists(_solutionPath))
{
ScriptsBuilder::GenerateProject();
}
// Open file
const VisualStudio::Connection connection(*_CLSID, *_solutionPath);
const auto result = connection.OpenFile(*path, line);
if (result.Failed())
{
LOG(Warning, "Cannot open file \'{0}\':{1}. {2}.", path, line, String(result.Message.c_str()));
}
}
void VisualStudioEditor::OpenSolution()
{
// Generate project files if solution is missing
if (!FileSystem::FileExists(_solutionPath))
{
ScriptsBuilder::GenerateProject();
}
// Open solution
const VisualStudio::Connection connection(*_CLSID, *_solutionPath);
const auto result = connection.OpenSolution();
if (result.Failed())
{
LOG(Warning, "Cannot open solution. {0}", String(result.Message.c_str()));
}
}
void VisualStudioEditor::OnFileAdded(const String& path)
{
// TODO: finish dynamic files adding to the project - for now just regenerate it
ScriptsBuilder::GenerateProject();
return;
if (!FileSystem::FileExists(_solutionPath))
{
return;
}
// Edit solution
const VisualStudio::Connection connection(*_CLSID, *_solutionPath);
if (connection.IsActive())
{
String tmp = path;
tmp.Replace('/', '\\');
String tmp2 = tmp.Substring(Globals::ProjectSourceFolder.Length() + 1);
const auto result = connection.AddFile(*tmp, *tmp2);
if (result.Failed())
{
LOG(Warning, "Cannot add file to project. {0}", String(result.Message.c_str()));
}
}
}
bool VisualStudioEditor::UseAsyncForOpen() const
{
// Need to generate project files if missing first
if (!FileSystem::FileExists(_solutionPath))
return true;
// Open in async only when no solution opened
const VisualStudio::Connection connection(*_CLSID, *_solutionPath);
return !connection.IsActive();
}
#endif

View File

@@ -0,0 +1,68 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#if USE_VISUAL_STUDIO_DTE
#include "Engine/Core/Enums.h"
#include "Editor/Scripting/CodeEditor.h"
/// <summary>
/// Microsoft Visual Studio version types
/// </summary>
DECLARE_ENUM_7(VisualStudioVersion, VS2008, VS2010, VS2012, VS2013, VS2015, VS2017, VS2019);
/// <summary>
/// Implementation of code editor utility that is using Microsoft Visual Studio.
/// </summary>
class VisualStudioEditor : public CodeEditor
{
private:
VisualStudioVersion _version;
CodeEditorTypes _type;
String _execPath;
String _CLSID;
String _solutionPath;
public:
/// <summary>
/// Initializes a new instance of the <see cref="VisualStudioEditor"/> class.
/// </summary>
/// <param name="version">VS version</param>
/// <param name="execPath">Executable file path</param>
/// <param name="CLSID">CLSID of VS</param>
VisualStudioEditor(VisualStudioVersion version, const String& execPath, const String& CLSID);
public:
/// <summary>
/// Gets version of Visual Studio
/// </summary>
/// <returns>VS version</returns>
FORCE_INLINE VisualStudioVersion GetVersion() const
{
return _version;
}
public:
/// <summary>
/// Try to find installed Visual Studio instances. Adds them to the result list.
/// </summary>
/// <param name="output">The output editors.</param>
static void FindEditors(Array<CodeEditor*>* output);
public:
// [CodeEditor]
CodeEditorTypes GetType() const override;
String GetName() const override;
void OpenFile(const String& path, int32 line) override;
void OpenSolution() override;
void OnFileAdded(const String& path) override;
bool UseAsyncForOpen() const override;
};
#endif

View File

@@ -0,0 +1,76 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "VisualStudioCodeEditor.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Core/Log.h"
#include "Editor/Editor.h"
#include "Editor/Scripting/ScriptsBuilder.h"
#include "Engine/Engine/Globals.h"
#include "Engine/Platform/Win32/IncludeWindowsHeaders.h"
VisualStudioCodeEditor::VisualStudioCodeEditor(const String& execPath)
: _execPath(execPath)
, _workspacePath(Globals::ProjectFolder / Editor::Project->Name + TEXT(".code-workspace"))
{
}
void VisualStudioCodeEditor::FindEditors(Array<CodeEditor*>* output)
{
#if PLATFORM_WINDOWS
String cmd;
if (Platform::ReadRegValue(HKEY_CURRENT_USER, TEXT("SOFTWARE\\Classes\\Applications\\Code.exe\\shell\\open\\command"), TEXT(""), &cmd) || cmd.IsEmpty())
{
if (Platform::ReadRegValue(HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\Classes\\Applications\\Code.exe\\shell\\open\\command"), TEXT(""), &cmd) || cmd.IsEmpty())
{
return;
}
}
const String path = cmd.Substring(1, cmd.Length() - String(TEXT("\" \"%1\"")).Length() - 1);
if (FileSystem::FileExists(path))
{
output->Add(New<VisualStudioCodeEditor>(path));
}
#endif
}
CodeEditorTypes VisualStudioCodeEditor::GetType() const
{
return CodeEditorTypes::VSCode;
}
String VisualStudioCodeEditor::GetName() const
{
return TEXT("Visual Studio Code");
}
void VisualStudioCodeEditor::OpenFile(const String& path, int32 line)
{
// Generate project files if missing
if (!FileSystem::FileExists(Globals::ProjectFolder / TEXT(".vscode/tasks.json")))
{
ScriptsBuilder::GenerateProject(TEXT("-vscode"));
}
// Open file
line = line > 0 ? line : 1;
const String args = String::Format(TEXT("\"{0}\" -g \"{1}\":{2}"), _workspacePath, path, line);
Platform::StartProcess(_execPath, args, StringView::Empty);
}
void VisualStudioCodeEditor::OpenSolution()
{
// Generate project files if solution is missing
if (!FileSystem::FileExists(Globals::ProjectFolder / TEXT(".vscode/tasks.json")))
{
ScriptsBuilder::GenerateProject(TEXT("-vscode"));
}
// Open solution
const String args = String::Format(TEXT("\"{0}\""), _workspacePath);
Platform::StartProcess(_execPath, args, StringView::Empty);
}
bool VisualStudioCodeEditor::UseAsyncForOpen() const
{
return false;
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Editor/Scripting/CodeEditor.h"
/// <summary>
/// Implementation of code editor utility that is using Microsoft Visual Studio Code.
/// </summary>
class VisualStudioCodeEditor : public CodeEditor
{
private:
String _execPath;
String _workspacePath;
public:
/// <summary>
/// Initializes a new instance of the <see cref="VisualStudioEditor"/> class.
/// </summary>
/// <param name="execPath">Executable file path</param>
VisualStudioCodeEditor(const String& execPath);
public:
/// <summary>
/// Tries to find installed Visual Studio Code instance. Adds them to the result list.
/// </summary>
/// <param name="output">The output editors.</param>
static void FindEditors(Array<CodeEditor*>* output);
public:
// [CodeEditor]
CodeEditorTypes GetType() const override;
String GetName() const override;
void OpenFile(const String& path, int32 line) override;
void OpenSolution() override;
bool UseAsyncForOpen() const override;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,662 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "ScriptsBuilder.h"
#include "CodeEditor.h"
#include "Editor/Editor.h"
#include "Editor/Managed/ManagedEditor.h"
#include "Engine/Core/Types/String.h"
#include "Engine/Core/Types/StringBuilder.h"
#include "Engine/Debug/Exceptions/FileNotFoundException.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Platform/FileSystemWatcher.h"
#include "Engine/Threading/ThreadPool.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Scripting/MainThreadManagedInvokeAction.h"
#include "Engine/Scripting/ScriptingType.h"
#include "Engine/Scripting/BinaryModule.h"
#include "Engine/Scripting/ManagedCLR/MAssembly.h"
#include "Engine/Scripting/ManagedCLR/MClass.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Scripting/Script.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Level/Level.h"
#include "FlaxEngine.Gen.h"
enum class EventType
{
Unknown = -1,
CompileBegin = 0,
CompileStarted = 1,
CompileEndGood = 2,
CompileEndFailed = 3,
ReloadCalled = 4,
ReloadBegin = 5,
Reload = 6,
ReloadEnd = 7,
};
struct EventData
{
EventType Type;
};
namespace ScriptsBuilderImpl
{
CriticalSection _locker;
bool _isInited = false;
bool _isCompileRequested = false;
bool _isCompileRunning = false;
bool _wasProjectStructureChanged = false;
bool _lastCompilationFailed = false;
int32 _compilationsCount = 0;
DateTime _lastSourceCodeEdited = 0;
DateTime _lastCompileAction = 0;
Array<FileSystemWatcher*> SourceFoldersWatchers;
CriticalSection _compileEventsLocker;
Array<EventData> _compileEvents;
MMethod* Internal_OnEvent = nullptr;
MMethod* Internal_OnCompileEvent = nullptr;
MMethod* Internal_OnCodeEditorEvent = nullptr;
void CallEvent(EventType type);
void CallCompileEvent(EventData& data);
void CallCodeEditorEvent(bool isEnd);
void sourceDirEvent(const String& path, FileSystemAction action);
void onEditorAssemblyUnloading(MAssembly* assembly);
void onScriptsReloadStart();
void onScriptsReload();
void onScriptsReloadEnd();
void GetClassName(const MString& fullname, MString& className);
void onCodeEditorAsyncOpenBegin()
{
CallCodeEditorEvent(false);
}
void onCodeEditorAsyncOpenEnd()
{
CallCodeEditorEvent(true);
}
bool compileGameScriptsAsyncInner();
bool compileGameScriptsAsync();
}
using namespace ScriptsBuilderImpl;
class ScriptsBuilderService : public EngineService
{
public:
ScriptsBuilderService()
: EngineService(TEXT("Scripts Builder"))
{
}
bool Init() override;
void Update() override;
void Dispose() override;
};
ScriptsBuilderService ScriptsBuilderServiceInstance;
Delegate<bool> ScriptsBuilder::OnCompilationEnd;
Action ScriptsBuilder::OnCompilationSuccess;
Action ScriptsBuilder::OnCompilationFailed;
void ScriptsBuilderImpl::sourceDirEvent(const String& path, FileSystemAction action)
{
// Discard non-source files or generated files
if (!path.EndsWith(TEXT(".cs")) &&
!path.EndsWith(TEXT(".cpp")) &&
!path.EndsWith(TEXT(".h")) ||
path.EndsWith(TEXT(".Gen.cs")))
return;
ScopeLock scopeLock(_locker);
_lastSourceCodeEdited = DateTime::Now();
}
int32 ScriptsBuilder::GetCompilationsCount()
{
Platform::MemoryBarrier();
return _compilationsCount;
}
bool ScriptsBuilder::LastCompilationFailed()
{
return _lastCompilationFailed;
}
void ScriptsBuilder::FilterNamespaceText(String& value)
{
value.Replace(TEXT(" "), TEXT(""), StringSearchCase::CaseSensitive);
value.Replace(TEXT("."), TEXT(""), StringSearchCase::CaseSensitive);
value.Replace(TEXT("-"), TEXT(""), StringSearchCase::CaseSensitive);
}
bool ScriptsBuilder::IsSourceDirty()
{
ScopeLock scopeLock(_locker);
return _lastSourceCodeEdited > _lastCompileAction;
}
bool ScriptsBuilder::IsSourceWorkspaceDirty()
{
ScopeLock scopeLock(_locker);
return _wasProjectStructureChanged;
}
bool ScriptsBuilder::IsSourceDirty(const TimeSpan& timeout)
{
ScopeLock scopeLock(_locker);
return _lastSourceCodeEdited > (_lastCompileAction + timeout);
}
bool ScriptsBuilder::IsCompiling()
{
ScopeLock scopeLock(_locker);
return _isCompileRunning;
}
bool ScriptsBuilder::IsReady()
{
ScopeLock scopeLock(_locker);
return !IsSourceDirty() && !_isCompileRequested && !_isCompileRunning;
}
void ScriptsBuilder::MarkWorkspaceDirty()
{
ScopeLock scopeLock(_locker);
_lastSourceCodeEdited = DateTime::Now();
_wasProjectStructureChanged = true;
}
void ScriptsBuilder::CheckForCompile()
{
ScopeLock scopeLock(_locker);
if (IsSourceDirty())
Compile();
}
void ScriptsBuilderImpl::onScriptsReloadStart()
{
CallEvent(EventType::ReloadBegin);
}
void ScriptsBuilderImpl::onScriptsReload()
{
CallEvent(EventType::Reload);
}
void ScriptsBuilderImpl::onScriptsReloadEnd()
{
CallEvent(EventType::ReloadEnd);
}
void ScriptsBuilder::Compile()
{
ScopeLock scopeLock(_locker);
// Request compile job
_isCompileRequested = true;
}
bool ScriptsBuilder::RunBuildTool(const StringView& args)
{
const String buildToolPath = Globals::StartupFolder / TEXT("Binaries/Tools/Flax.Build.exe");
if (!FileSystem::FileExists(buildToolPath))
{
Log::FileNotFoundException(buildToolPath).SetLevel(LogType::Fatal);
return true;
}
#if PLATFORM_WINDOWS
// Prepare build options
StringBuilder cmdLine(args.Length() + buildToolPath.Length() + 30);
cmdLine.Append(TEXT("\""));
cmdLine.Append(buildToolPath);
cmdLine.Append(TEXT("\" "));
cmdLine.Append(args.Get(), args.Length());
cmdLine.Append(TEXT('\0'));
#else
// Use mono to run the build tool
const String monoPath = Globals::MonoPath / TEXT("bin/mono.exe");
if (!FileSystem::FileExists(monoPath))
{
Log::FileNotFoundException(monoPath).SetLevel(LogType::Fatal);
return true;
}
// Prepare build options
StringBuilder cmdLine(monoPath.Length() + args.Length() + buildToolPath.Length() + 30);
cmdLine.Append(TEXT("\""));
cmdLine.Append(monoPath);
cmdLine.Append(TEXT("\" \""));
cmdLine.Append(buildToolPath);
cmdLine.Append(TEXT("\" "));
cmdLine.Append(args.Get(), args.Length());
cmdLine.Append(TEXT('\0'));
// TODO: Set env var for the mono MONO_GC_PARAMS=nursery-size64m to boost build performance
#endif
// Call build tool
const int32 result = Platform::RunProcess(StringView(*cmdLine, cmdLine.Length()), StringView::Empty);
return result != 0;
}
bool ScriptsBuilder::GenerateProject(const StringView& customArgs)
{
String args(TEXT("-log -genproject "));
args += customArgs;
_wasProjectStructureChanged = false;
return RunBuildTool(args);
}
void ScriptsBuilderImpl::GetClassName(const MString& fullname, MString& className)
{
const auto lastDotIndex = fullname.FindLast('.');
if (lastDotIndex != -1)
{
//namespaceName = fullname.Substring(0, lastDotIndex);
className = fullname.Substring(lastDotIndex + 1);
}
else
{
className = fullname;
}
}
MClass* ScriptsBuilder::FindScript(const StringView& scriptName)
{
const MString scriptNameStd = scriptName.ToStringAnsi();
const ScriptingTypeHandle scriptingType = Scripting::FindScriptingType(scriptNameStd);
if (scriptingType)
{
MClass* mclass = scriptingType.GetType().ManagedClass;
if (mclass)
{
return mclass;
}
}
// Check all assemblies (ignoring the typename namespace)
auto& modules = BinaryModule::GetModules();
MString className;
GetClassName(scriptNameStd, className);
MClass* scriptClass = Script::GetStaticClass();
for (int32 j = 0; j < modules.Count(); j++)
{
auto managedModule = dynamic_cast<ManagedBinaryModule*>(modules[j]);
if (!managedModule)
continue;
auto assembly = managedModule->Assembly;
auto& classes = assembly->GetClasses();
for (auto i = classes.Begin(); i.IsNotEnd(); ++i)
{
MClass* mclass = i->Value;
// Managed scripts
if (mclass->IsSubClassOf(scriptClass) && !mclass->IsStatic() && !mclass->IsAbstract() && !mclass->IsInterface())
{
MString mclassName;
GetClassName(mclass->GetFullName(), mclassName);
if (className == mclassName)
{
LOG(Info, "Found {0} type for type {1} (assembly {2})", mclass->ToString(), String(scriptName.Get(), scriptName.Length()), assembly->ToString());
return mclass;
}
}
}
}
LOG(Warning, "Failed to find script class of name {0}", String(scriptNameStd));
return nullptr;
}
void ScriptsBuilder::GetExistingEditors(int32* result, int32 count)
{
auto& editors = CodeEditingManager::GetEditors();
for (int32 i = 0; i < editors.Count() && i < count; i++)
{
result[static_cast<int32>(editors[i]->GetType())] = 1;
}
}
void ScriptsBuilder::GetBinariesConfiguration(StringView& target, StringView& platform, StringView& architecture, StringView& configuration)
{
const Char *targetPtr, *platformPtr, *architecturePtr, *configurationPtr;
GetBinariesConfiguration(targetPtr, platformPtr, architecturePtr, configurationPtr);
target = targetPtr;
platform = platformPtr;
architecture = architecturePtr;
configuration = configurationPtr;
}
void ScriptsBuilder::GetBinariesConfiguration(const Char*& target, const Char*& platform, const Char*& architecture, const Char*& configuration)
{
// Special case when opening engine project
if (Editor::Project->ProjectFolderPath == Globals::StartupFolder)
{
target = platform = architecture = configuration = nullptr;
return;
}
target = Editor::Project->EditorTarget.GetText();
#if PLATFORM_WINDOWS
platform = TEXT("Windows");
#endif
#if PLATFORM_ARCH_X64
architecture = TEXT("x64");
#elif PLATFORM_ARCH_X86
architecture = TEXT("x86");
#endif
#if BUILD_DEBUG
configuration = TEXT("Debug");
#elif BUILD_DEVELOPMENT
configuration = TEXT("Development");
#elif BUILD_RELEASE
configuration = TEXT("Release");
#endif
}
bool ScriptsBuilderImpl::compileGameScriptsAsyncInner()
{
LOG(Info, "Starting scripts compilation...");
CallEvent(EventType::CompileStarted);
// Call compilation
const Char *target, *platform, *architecture, *configuration;
ScriptsBuilder::GetBinariesConfiguration(target, platform, architecture, configuration);
if (!target)
{
LOG(Info, "Missing EditorTarget in project. Skipping compilation.");
CallEvent(EventType::ReloadCalled);
Scripting::Reload();
return false;
}
auto args = String::Format(
TEXT("-log -logfile= -build -mutex -buildtargets={0} -skiptargets=FlaxEditor -platform={1} -arch={2} -configuration={3}"),
target, platform, architecture, configuration);
if (Scripting::HasGameModulesLoaded())
{
// Add postfix to output binaries to prevent file locking collisions when doing hot-reload in Editor
args += String::Format(TEXT(" -hotreload=\".HotReload.{0}\""), _compilationsCount - 1);
}
if (ScriptsBuilder::RunBuildTool(args))
return true;
// Reload scripts
CallEvent(EventType::ReloadCalled);
Scripting::Reload();
return false;
}
void ScriptsBuilderImpl::CallEvent(EventType type)
{
ScopeLock lock(_compileEventsLocker);
const int32 index = _compileEvents.Count();
_compileEvents.AddDefault(1);
auto& data = _compileEvents[index];
data.Type = type;
// Flush events on a main tread
if (IsInMainThread())
{
for (int32 i = 0; i < _compileEvents.Count(); i++)
CallCompileEvent(_compileEvents[i]);
_compileEvents.Clear();
}
}
void ScriptsBuilderImpl::CallCompileEvent(EventData& data)
{
ASSERT(IsInMainThread());
// Special case for a single events with no data
if (data.Type != EventType::Unknown)
{
// Call C# event
if (Internal_OnEvent == nullptr)
{
auto scriptsBuilderClass = ScriptsBuilder::GetStaticClass();
if (scriptsBuilderClass)
Internal_OnEvent = scriptsBuilderClass->GetMethod("Internal_OnEvent", 1);
if (Internal_OnEvent == nullptr)
{
LOG(Fatal, "Invalid Editor assembly!");
}
}
/*MonoObject* exception = nullptr;
void* args[1];
args[0] = &data.Type;
Internal_OnEvent->Invoke(nullptr, args, &exception);
if (exception)
{
DebugLog::LogException(exception);
}*/
MainThreadManagedInvokeAction::ParamsBuilder params;
params.AddParam(data.Type);
MainThreadManagedInvokeAction::Invoke(Internal_OnEvent, params);
}
}
void ScriptsBuilderImpl::CallCodeEditorEvent(bool isEnd)
{
if (Internal_OnCodeEditorEvent == nullptr)
{
auto scriptsBuilderClass = ScriptsBuilder::GetStaticClass();
if (scriptsBuilderClass)
Internal_OnCodeEditorEvent = scriptsBuilderClass->GetMethod("Internal_OnCodeEditorEvent", 1);
ASSERT(Internal_OnCodeEditorEvent);
}
MainThreadManagedInvokeAction::ParamsBuilder params;
params.AddParam(isEnd);
MainThreadManagedInvokeAction::Invoke(Internal_OnCodeEditorEvent, params);
}
void ScriptsBuilderImpl::onEditorAssemblyUnloading(MAssembly* assembly)
{
Internal_OnEvent = nullptr;
Internal_OnCompileEvent = nullptr;
Internal_OnCodeEditorEvent = nullptr;
}
bool ScriptsBuilderImpl::compileGameScriptsAsync()
{
// Start
{
ScopeLock scopeLock(_locker);
_isCompileRequested = false;
_lastCompileAction = DateTime::Now();
_compilationsCount++;
_isCompileRunning = true;
ScriptsBuilderServiceInstance.Init();
CallEvent(EventType::CompileBegin);
}
// Do work
const bool success = !compileGameScriptsAsyncInner();
// End
{
ScopeLock scopeLock(_locker);
_lastCompilationFailed = !success;
ScriptsBuilder::OnCompilationEnd(success);
if (success)
ScriptsBuilder::OnCompilationSuccess();
else
ScriptsBuilder::OnCompilationFailed();
if (success)
CallEvent(EventType::CompileEndGood);
else
CallEvent(EventType::CompileEndFailed);
_isCompileRunning = false;
}
return false;
}
bool ScriptsBuilderService::Init()
{
// Check flag
if (_isInited)
return false;
_isInited = true;
// Link for Editor assembly unload event to clear cached Internal_OnCompilationEnd to prevent errors
auto editorAssembly = GetBinaryModuleFlaxEngine()->Assembly;
editorAssembly->Unloading.Bind(onEditorAssemblyUnloading);
// Listen to scripts reloading events and forward them to c#
Level::ScriptsReloadStart.Bind(onScriptsReloadStart);
Level::ScriptsReload.Bind(onScriptsReload);
Level::ScriptsReloadEnd.Bind(onScriptsReloadEnd);
// Listen to code editors manager events
CodeEditingManager::AsyncOpenBegin.Bind(onCodeEditorAsyncOpenBegin);
CodeEditingManager::AsyncOpenEnd.Bind(onCodeEditorAsyncOpenEnd);
// Create source folder watcher to handle scripts modification events (create/delete scripts events are handled by the editor itself)
auto project = Editor::Project;
HashSet<ProjectInfo*> projects;
project->GetAllProjects(projects);
for (auto e : projects)
{
ProjectInfo* project = e.Item;
if (project->Name == TEXT("Flax"))
continue;
auto watcher = New<FileSystemWatcher>(project->ProjectFolderPath / TEXT("Source"), true);
watcher->OnEvent.Bind(sourceDirEvent);
SourceFoldersWatchers.Add(watcher);
}
// Verify project
if (project->EditorTarget.IsEmpty() || project->GameTarget.IsEmpty())
{
const String& name = project->Name;
String codeName;
for (int32 i = 0; i < name.Length(); i++)
{
Char c = name[i];
if (StringUtils::IsAlnum(c) && c != ' ' && c != '.')
codeName += c;
}
project->GameTarget = codeName + TEXT("Target");
project->EditorTarget = codeName + TEXT("EditorTarget");
LOG(Warning, "Missing EditorTarget property in opened project, using deducted target name {0}", Editor::Project->EditorTarget);
}
// Remove any reaming files from previous Editor run hot-reloads
const Char *target, *platform, *architecture, *configuration;
ScriptsBuilder::GetBinariesConfiguration(target, platform, architecture, configuration);
if (target)
{
const String targetOutput = Globals::ProjectFolder / TEXT("Binaries") / target / platform / architecture / configuration;
Array<String> files;
FileSystem::DirectoryGetFiles(files, targetOutput, TEXT("*.HotReload.*"), DirectorySearchOption::TopDirectoryOnly);
if (files.HasItems())
LOG(Info, "Removing {0} files from previous Editor run hot-reloads", files.Count());
for (auto& file : files)
{
FileSystem::DeleteFile(file);
}
}
bool forceRecompile = false;
// Check last Editor version that was using a project is different from current
if (Editor::IsOldProjectOpened)
forceRecompile = true;
// Check if need to force recompile game scripts
if (forceRecompile)
{
LOG(Warning, "Forcing scripts recompilation");
FileSystem::DeleteDirectory(Globals::ProjectCacheFolder / TEXT("Intermediate"));
ScriptsBuilder::Compile();
}
return false;
}
void ScriptsBuilderService::Update()
{
// Send compilation events
{
ScopeLock scopeLock(_compileEventsLocker);
for (int32 i = 0; i < _compileEvents.Count(); i++)
CallCompileEvent(_compileEvents[i]);
_compileEvents.Clear();
}
// Check if compile code (if has been edited)
const TimeSpan timeToCallCompileIfDirty = TimeSpan::FromMilliseconds(50);
auto mainWindow = Engine::MainWindow;
if (ScriptsBuilder::IsSourceDirty(timeToCallCompileIfDirty) && mainWindow && mainWindow->IsFocused())
{
// Check if auto reload is enabled
if (Editor::Managed->CanAutoReloadScripts())
ScriptsBuilder::Compile();
}
ScopeLock scopeLock(_locker);
// Check if compilation has been requested (some time ago since we want to batch calls to reduce amount of compilations)
if (_isCompileRequested)
{
// Check if compilation isn't running
if (!_isCompileRunning)
{
// Check if editor state can perform scripts reloading
if (Editor::Managed->CanReloadScripts())
{
// Call compilation (and switch flags)
_isCompileRequested = false;
_isCompileRunning = true;
Function<bool()> action(compileGameScriptsAsync);
Task::StartNew(action);
}
}
}
}
void ScriptsBuilderService::Dispose()
{
// Don't exit while scripts compilation is still running
if (ScriptsBuilder::IsCompiling())
{
LOG(Warning, "Scripts compilation is running, waiting for the end...");
int32 timeOutMilliseconds = 5000;
while (ScriptsBuilder::IsCompiling() && timeOutMilliseconds > 0)
{
const int sleepTimeMilliseconds = 50;
timeOutMilliseconds -= sleepTimeMilliseconds;
Platform::Sleep(sleepTimeMilliseconds);
}
LOG(Warning, "Scripts compilation wait ended");
}
SourceFoldersWatchers.ClearDelete();
}

View File

@@ -0,0 +1,132 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
namespace FlaxEditor
{
partial class ScriptsBuilder
{
/// <summary>
/// Compilation end event delegate.
/// </summary>
/// <param name="success">False if compilation has failed, otherwise true.</param>
public delegate void CompilationEndDelegate(bool success);
/// <summary>
/// Compilation message events delegate.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="file">The target file.</param>
/// <param name="line">The target line.</param>
public delegate void CompilationMessageDelegate(string message, string file, int line);
/// <summary>
/// Occurs when compilation ends.
/// </summary>
public static event CompilationEndDelegate CompilationEnd;
/// <summary>
/// Occurs when compilation success.
/// </summary>
public static event Action CompilationSuccess;
/// <summary>
/// Occurs when compilation failed.
/// </summary>
public static event Action CompilationFailed;
/// <summary>
/// Occurs when compilation begins.
/// </summary>
public static event Action CompilationBegin;
/// <summary>
/// Occurs when compilation just started.
/// </summary>
public static event Action CompilationStarted;
/// <summary>
/// Occurs when user scripts reload action is called.
/// </summary>
public static event Action ScriptsReloadCalled;
/// <summary>
/// Occurs when user scripts reload starts.
/// User objects should be removed at this point to reduce leaks and issues. Game scripts and game editor scripts assemblies will be reloaded.
/// </summary>
public static event Action ScriptsReloadBegin;
/// <summary>
/// Occurs when user scripts reload is performed (just before the actual reload, scenes are serialized and unloaded). All user objects should be cleanup.
/// </summary>
public static event Action ScriptsReload;
/// <summary>
/// Occurs when user scripts reload ends.
/// </summary>
public static event Action ScriptsReloadEnd;
/// <summary>
/// Occurs when code editor starts asynchronous open a file or a solution.
/// </summary>
public static event Action CodeEditorAsyncOpenBegin;
/// <summary>
/// Occurs when code editor ends asynchronous open a file or a solution.
/// </summary>
public static event Action CodeEditorAsyncOpenEnd;
internal enum EventType
{
CompileBegin = 0,
CompileStarted = 1,
CompileEndGood = 2,
CompileEndFailed = 3,
ReloadCalled = 4,
ReloadBegin = 5,
Reload = 6,
ReloadEnd = 7,
}
internal static void Internal_OnEvent(EventType type)
{
switch (type)
{
case EventType.CompileBegin:
CompilationBegin?.Invoke();
break;
case EventType.CompileStarted:
CompilationStarted?.Invoke();
break;
case EventType.CompileEndGood:
CompilationEnd?.Invoke(true);
CompilationSuccess?.Invoke();
break;
case EventType.CompileEndFailed:
CompilationEnd?.Invoke(false);
CompilationFailed?.Invoke();
break;
case EventType.ReloadCalled:
ScriptsReloadCalled?.Invoke();
break;
case EventType.ReloadBegin:
ScriptsReloadBegin?.Invoke();
break;
case EventType.Reload:
ScriptsReload?.Invoke();
break;
case EventType.ReloadEnd:
ScriptsReloadEnd?.Invoke();
break;
}
}
internal static void Internal_OnCodeEditorEvent(bool isEnd)
{
if (isEnd)
CodeEditorAsyncOpenEnd?.Invoke();
else
CodeEditorAsyncOpenBegin?.Invoke();
}
}
}

View File

@@ -0,0 +1,131 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Delegate.h"
#include "Engine/Core/Types/String.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Scripting/ScriptingType.h"
/// <summary>
/// Game scrips building service. Compiles user C# scripts into binary assemblies. Exposes many events used to track scripts compilation and reloading.
/// </summary>
API_CLASS(Static, Namespace="FlaxEditor") class ScriptsBuilder
{
DECLARE_SCRIPTING_TYPE_NO_SPAWN(ScriptsBuilder);
public:
typedef Delegate<const String&, const String&, int32> CompileMsgDelegate;
public:
/// <summary>
/// Action called on compilation end, bool param is true if success, otherwise false
/// </summary>
static Delegate<bool> OnCompilationEnd;
/// <summary>
/// Action called when compilation success
/// </summary>
static Action OnCompilationSuccess;
/// <summary>
/// Action called when compilation fails
/// </summary>
static Action OnCompilationFailed;
public:
/// <summary>
/// Gets amount of source code compile actions since Editor startup.
/// </summary>
API_PROPERTY() static int32 GetCompilationsCount();
/// <summary>
/// Checks if last scripting building failed due to errors.
/// </summary>
/// <returns>True if last compilation result was a fail, otherwise false.</returns>
API_PROPERTY() static bool LastCompilationFailed();
/// <summary>
/// Returns true if source code has been edited.
/// </summary>
/// <returns>True if source code is dirty, otherwise false.</returns>
API_PROPERTY() static bool IsSourceDirty();
/// <summary>
/// Returns true if source code workspace has been edited (source file moved or deleted).
/// </summary>
/// <returns>True if source code workspace is dirty, otherwise false.</returns>
API_PROPERTY() static bool IsSourceWorkspaceDirty();
/// <summary>
/// Returns true if source code has been edited and is dirty for given amount of time
/// </summary>
/// <param name="timeout">Time to use for checking.</param>
/// <returns>True if source code is dirty, otherwise false.</returns>
static bool IsSourceDirty(const TimeSpan& timeout);
/// <summary>
/// Returns true if scripts are being now compiled/reloaded.
/// </summary>
/// <returns>True if scripts are being compiled/reloaded now.</returns>
API_PROPERTY() static bool IsCompiling();
/// <summary>
/// Returns true if source code has been compiled and assemblies are ready to load.
/// </summary>
/// <returns>True if assemblies are ready to load.</returns>
API_PROPERTY() static bool IsReady();
/// <summary>
/// Indicates that scripting directory has been modified so scripts need to be rebuild.
/// </summary>
API_FUNCTION() static void MarkWorkspaceDirty();
/// <summary>
/// Checks if need to compile source code. If so calls compilation.
/// </summary>
API_FUNCTION() static void CheckForCompile();
/// <summary>
/// Requests project source code compilation.
/// </summary>
API_FUNCTION() static void Compile();
/// <summary>
/// Invokes the Flax.Build tool in the current project workspace and waits for the process end (blocking). Prints the build tool output to the log. Can be invoked from any thread.
/// </summary>
/// <param name="args">The Flax.Build tool invocation arguments.</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() static bool RunBuildTool(const StringView& args);
/// <summary>
/// Generates the project files.
/// </summary>
/// <param name="customArgs">The additional Flax.Build tool invocation arguments.</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() static bool GenerateProject(const StringView& customArgs = StringView::Empty);
/// <summary>
/// Tries to find a script type with the given name.
/// </summary>
/// <param name="scriptName">The script full name.</param>
/// <returns>Found script type or null if missing or invalid name.</returns>
API_FUNCTION() static MClass* FindScript(const StringView& scriptName);
// Gets the list of existing in-build code editors.
API_FUNCTION() static void GetExistingEditors(int32* result, int32 count);
// Gets the options for the game scripts to use for the Editor.
API_FUNCTION() static void GetBinariesConfiguration(API_PARAM(Out) StringView& target, API_PARAM(Out) StringView& platform, API_PARAM(Out) StringView& architecture, API_PARAM(Out) StringView& configuration);
static void GetBinariesConfiguration(const Char*& target, const Char*& platform, const Char*& architecture, const Char*& configuration);
public:
/// <summary>
/// Filters the project namespace text value to be valid (remove whitespace characters and other invalid ones).
/// </summary>
/// <param name="value">The in/out value.</param>
static void FilterNamespaceText(String& value);
};

View File

@@ -0,0 +1,290 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Reflection;
using FlaxEngine;
namespace FlaxEditor.Scripting
{
/// <summary>
/// Editor scripting utilities and helper functions.
/// </summary>
public static class TypeUtils
{
/// <summary>
/// Custom list of scripting types containers. Can be used by custom scripting languages to provide types info for the editor.
/// </summary>
public static List<IScriptTypesContainer> CustomTypes = new List<IScriptTypesContainer>();
/// <summary>
/// Gets the script type of the object.
/// </summary>
/// <param name="o">The object.</param>
/// <returns>The scripting type of that object.</returns>
public static ScriptType GetObjectType(object o)
{
if (o is FlaxEngine.Object flaxObject && FlaxEngine.Object.GetUnmanagedPtr(flaxObject) != IntPtr.Zero)
{
var type = flaxObject.GetType();
var typeName = flaxObject.TypeName;
return type.FullName != typeName ? GetType(typeName) : new ScriptType(type);
}
return o != null ? new ScriptType(o.GetType()) : ScriptType.Null;
}
/// <summary>
/// Gets the default value for the given type (can be value type or reference type).
/// </summary>
/// <param name="type">The type.</param>
/// <returns>The created instance.</returns>
public static object GetDefaultValue(ScriptType type)
{
if (type.Type == typeof(string))
return string.Empty;
if (type.Type == typeof(Color))
return Color.White;
if (type.Type == typeof(Quaternion))
return Quaternion.Identity;
if (type.Type == typeof(Transform))
return Transform.Identity;
if (type.Type == typeof(Matrix))
return Matrix.Identity;
if (type.Type == typeof(BoundingBox))
return BoundingBox.Zero;
if (type.Type == typeof(Rectangle))
return Rectangle.Empty;
if (type.Type == typeof(ChannelMask))
return ChannelMask.Red;
if (type.Type == typeof(MaterialSceneTextures))
return MaterialSceneTextures.BaseColor;
if (type.IsValueType)
return type.CreateInstance();
if (new ScriptType(typeof(object)).IsAssignableFrom(type))
return null;
if (type.CanCreateInstance)
return type.CreateInstance();
throw new NotSupportedException("Cannot create default value for type " + type);
}
/// <summary>
/// Gets all the derived types from the given base type (excluding that type) within the given assembly.
/// </summary>
/// <param name="assembly">The target assembly to check its types.</param>
/// <param name="baseType">The base type.</param>
/// <param name="result">The result collection. Elements will be added to it. Clear it before usage.</param>
public static void GetDerivedTypes(Assembly assembly, ScriptType baseType, List<ScriptType> result)
{
GetDerivedTypes(assembly, baseType, result, type => true);
}
/// <summary>
/// Gets all the derived types from the given base type (excluding that type) within all the loaded assemblies.
/// </summary>
/// <param name="baseType">The base type.</param>
/// <param name="result">The result collection. Elements will be added to it. Clear it before usage.</param>
public static void GetDerivedTypes(ScriptType baseType, List<ScriptType> result)
{
GetDerivedTypes(baseType, result, type => true, assembly => true);
}
/// <summary>
/// Gets all the derived types from the given base type (excluding that type) within the given assembly.
/// </summary>
/// <param name="assembly">The target assembly to check its types.</param>
/// <param name="baseType">The base type.</param>
/// <param name="result">The result collection. Elements will be added to it. Clear it before usage.</param>
/// <param name="checkFunc">Additional callback used to check if the given type is valid. Returns true if add type, otherwise false.</param>
public static void GetDerivedTypes(Assembly assembly, ScriptType baseType, List<ScriptType> result, Func<ScriptType, bool> checkFunc)
{
var types = assembly.GetTypes();
for (int i = 0; i < types.Length; i++)
{
var t = new ScriptType(types[i]);
if (baseType.IsAssignableFrom(t) && t != baseType && checkFunc(t))
result.Add(t);
}
}
/// <summary>
/// Gets all the derived types from the given base type (excluding that type) within all the loaded assemblies.
/// </summary>
/// <param name="baseType">The base type.</param>
/// <param name="result">The result collection. Elements will be added to it. Clear it before usage.</param>
/// <param name="checkFunc">Additional callback used to check if the given type is valid. Returns true if add type, otherwise false.</param>
public static void GetDerivedTypes(ScriptType baseType, List<ScriptType> result, Func<ScriptType, bool> checkFunc)
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
for (int i = 0; i < assemblies.Length; i++)
{
GetDerivedTypes(assemblies[i], baseType, result, checkFunc);
}
}
/// <summary>
/// Gets all the derived types from the given base type (excluding that type) within all the loaded assemblies.
/// </summary>
/// <param name="baseType">The base type.</param>
/// <param name="result">The result collection. Elements will be added to it. Clear it before usage.</param>
/// <param name="checkFunc">Additional callback used to check if the given type is valid. Returns true if add type, otherwise false.</param>
/// <param name="checkAssembly">Additional callback used to check if the given assembly is valid. Returns true if search for types in the given assembly, otherwise false.</param>
public static void GetDerivedTypes(ScriptType baseType, List<ScriptType> result, Func<ScriptType, bool> checkFunc, Func<Assembly, bool> checkAssembly)
{
// C#/C++ types
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
for (int i = 0; i < assemblies.Length; i++)
{
if (checkAssembly(assemblies[i]))
GetDerivedTypes(assemblies[i], baseType, result, checkFunc);
}
// Custom types
foreach (var customTypesInfo in CustomTypes)
customTypesInfo.GetDerivedTypes(baseType, result, checkFunc);
}
/// <summary>
/// Gets all the types that have the given attribute defined within the given assembly.
/// </summary>
/// <param name="assembly">The target assembly to check its types.</param>
/// <param name="attributeType">The attribute type.</param>
/// <param name="result">The result collection. Elements will be added to it. Clear it before usage.</param>
/// <param name="checkFunc">Additional callback used to check if the given type is valid. Returns true if add type, otherwise false.</param>
public static void GetTypesWithAttributeDefined(Assembly assembly, Type attributeType, List<ScriptType> result, Func<ScriptType, bool> checkFunc)
{
var types = assembly.GetTypes();
for (int i = 0; i < types.Length; i++)
{
var t = new ScriptType(types[i]);
if (Attribute.IsDefined(t.Type, attributeType, false) && checkFunc(t))
result.Add(t);
}
}
/// <summary>
/// Gets all the types that have the given attribute defined within all the loaded assemblies.
/// </summary>
/// <param name="attributeType">The attribute type.</param>
/// <param name="result">The result collection. Elements will be added to it. Clear it before usage.</param>
/// <param name="checkFunc">Additional callback used to check if the given type is valid. Returns true if add type, otherwise false.</param>
/// <param name="checkAssembly">Additional callback used to check if the given assembly is valid. Returns true if search for types in the given assembly, otherwise false.</param>
public static void GetTypesWithAttributeDefined(Type attributeType, List<ScriptType> result, Func<ScriptType, bool> checkFunc, Func<Assembly, bool> checkAssembly)
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
for (int i = 0; i < assemblies.Length; i++)
{
if (checkAssembly(assemblies[i]))
GetTypesWithAttributeDefined(assemblies[i], attributeType, result, checkFunc);
}
}
/// <summary>
/// Tries to get the object type from the given full typename. Searches in-build Flax Engine/Editor assemblies and game assemblies.
/// </summary>
/// <param name="typeName">The full name of the type.</param>
/// <returns>The type or null if failed.</returns>
public static Type GetManagedType(string typeName)
{
if (string.IsNullOrEmpty(typeName))
return null;
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
for (int i = 0; i < assemblies.Length; i++)
{
var assembly = assemblies[i];
if (assembly != null)
{
var type = assembly.GetType(typeName);
if (type != null)
return type;
}
}
return null;
}
/// <summary>
/// Tries to get the object type from the given full typename. Searches in-build Flax Engine/Editor assemblies and game assemblies.
/// </summary>
/// <param name="typeName">The full name of the type.</param>
/// <returns>The type or null if failed.</returns>
public static ScriptType GetType(string typeName)
{
if (string.IsNullOrEmpty(typeName))
return ScriptType.Null;
// C#/C++ types
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
for (int i = 0; i < assemblies.Length; i++)
{
var assembly = assemblies[i];
if (assembly != null)
{
var type = assembly.GetType(typeName);
if (type != null)
{
return new ScriptType(type);
}
}
}
// Custom types
foreach (var customTypesInfo in CustomTypes)
{
var type = customTypesInfo.GetType(typeName);
if (type)
{
return type;
}
}
return ScriptType.Null;
}
/// <summary>
/// Tries to create object instance of the given full typename. Searches in-build Flax Engine/Editor assemblies and game assemblies.
/// </summary>
/// <param name="typeName">The full name of the type.</param>
/// <returns>The created object or null if failed.</returns>
public static object CreateInstance(string typeName)
{
var type = GetType(typeName);
if (type)
{
object obj = null;
try
{
return obj = type.CreateInstance();
}
catch (Exception ex)
{
Debug.LogException(ex);
}
return obj;
}
return null;
}
/// <summary>
/// Checks if the input type represents a structure (value type but not enum nor primitive type).
/// </summary>
/// <param name="type">The input type of the object to check.</param>
/// <returns>Returns true if the input type represents a structure (value type but not enum nor primitive type).</returns>
public static bool IsStructure(this Type type)
{
return type.IsValueType && !type.IsEnum && !type.IsPrimitive;
}
internal static bool IsDelegate(Type type)
{
return typeof(MulticastDelegate).IsAssignableFrom(type.BaseType);
}
internal static Type GetType(ScriptType type)
{
while (type.Type == null)
type = type.BaseType;
return type.Type;
}
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Types/String.h"
struct CodeLocation
{
String File;
uint32 Line;
};