diff --git a/BeefBuild/src/BuildApp.bf b/BeefBuild/src/BuildApp.bf index 1653c4b6..096df7a3 100644 --- a/BeefBuild/src/BuildApp.bf +++ b/BeefBuild/src/BuildApp.bf @@ -10,12 +10,22 @@ namespace BeefBuild { class BuildApp : IDEApp { + public enum MainVerbState + { + None, + UpdateList, + End + } + const int cProgressSize = 30; int mProgressIdx = 0; public bool mIsTest; public bool mTestIncludeIgnored; public bool mDidRun; public bool mWantsGenerate = false; + public bool mHandledVerb; + public String mRunArgs ~ delete _; + MainVerbState mMainVerbState; /*void Test() { @@ -112,20 +122,33 @@ namespace BeefBuild OutputErrorLine("The project '{}' is not empty, but '-generate' was specified.", mWorkspace.mStartupProject.mProjectName); } } - - if (!mFailed) - { - if (mIsTest) - { - RunTests(mTestIncludeIgnored, false); - } - else if (mVerb != .New) - Compile(.Normal, null); - } } public override bool HandleCommandLineParam(String key, String value) { + if (mRunArgs != null) + { + if (!mRunArgs.IsEmpty) + mRunArgs.Append(" "); + if (value != null) + { + String qKey = scope .(key); + String qValue = scope .(value); + IDEApp.QuoteIfNeeded(qKey); + IDEApp.QuoteIfNeeded(qValue); + mRunArgs.Append(qKey); + mRunArgs.Append('='); + mRunArgs.Append(qValue); + } + else + { + String qKey = scope .(key); + IDEApp.QuoteIfNeeded(qKey); + mRunArgs.Append(qKey); + } + return true; + } + if (key.StartsWith("--")) key.Remove(0, 1); @@ -133,6 +156,10 @@ namespace BeefBuild { switch (key) { + case "-args": + if (mRunArgs == null) + mRunArgs = new .(); + return true; case "-new": mVerb = .New; return true; @@ -157,12 +184,63 @@ namespace BeefBuild case "-noir": mConfig_NoIR = true; return true; + case "-update": + if (mWantUpdateVersionLocks == null) + mWantUpdateVersionLocks = new .(); + return true; case "-version": mVerb = .GetVersion; return true; case "-crash": Runtime.FatalError("-crash specified on command line"); } + + if (!key.StartsWith('-')) + { + switch (mMainVerbState) + { + case .None: + mMainVerbState = .End; + switch (key) + { + case "build": + mVerb = .None; + case "new": + mVerb = .New; + case "generate": + mWantsGenerate = true; + case "run": + if (mVerbosity == .Default) + mVerbosity = .Minimal; + mVerb = .Run; + case "test": + mIsTest = true; + case "testall": + mIsTest = true; + mTestIncludeIgnored = true; + case "clean": + mWantsClean = true; + case "version": + mVerb = .GetVersion; + case "crash": + Runtime.FatalError("-crash specified on command line"); + case "update": + mVerb = .Update; + mWantUpdateVersionLocks = new .(); + mMainVerbState = .UpdateList; + default: + mMainVerbState = .None; + } + if (mMainVerbState != .None) + return true; + case .UpdateList: + mWantUpdateVersionLocks.Add(new .(key)); + return true; + case .End: + return false; + default: + } + } } else { @@ -188,6 +266,11 @@ namespace BeefBuild case "-platform": mPlatformName.Set(value); return true; + case "-update": + if (mWantUpdateVersionLocks == null) + mWantUpdateVersionLocks = new .(); + mWantUpdateVersionLocks.Add(new .(value)); + return true; case "-verbosity": if (value == "quiet") mVerbosity = .Quiet; @@ -281,33 +364,55 @@ namespace BeefBuild { base.Update(batchStart); - if (mCompilingBeef) + if (mWorkspace.mProjectLoadState != .Loaded) { - WriteProgress(mBfBuildCompiler.GetCompletionPercentage()); + // Wait for workspace to complete loading } - - if ((!IsCompiling) && (!AreTestsRunning())) + else { - if ((mVerb == .Run) && (!mDidRun) && (!mFailed)) + if ((!mFailed) && (!mHandledVerb)) { - let curPath = scope String(); - Directory.GetCurrentDirectory(curPath); - - let workspaceOptions = gApp.GetCurWorkspaceOptions(); - let options = gApp.GetCurProjectOptions(mWorkspace.mStartupProject); - let targetPaths = scope List(); - defer ClearAndDeleteItems(targetPaths); - this.[Friend]GetTargetPaths(mWorkspace.mStartupProject, gApp.mPlatformName, workspaceOptions, options, targetPaths); - if (targetPaths.IsEmpty) - return; - - ExecutionQueueCmd executionCmd = QueueRun(targetPaths[0], "", curPath); - executionCmd.mIsTargetRun = true; - mDidRun = true; - return; + mHandledVerb = true; + if (mIsTest) + { + RunTests(mTestIncludeIgnored, false); + } + else if (mVerb == .Update) + { + // No-op here + } + else if (mVerb != .New) + Compile(.Normal, null); } - Stop(); + if (mCompilingBeef) + { + WriteProgress(mBfBuildCompiler.GetCompletionPercentage()); + } + + if ((!IsCompiling) && (!AreTestsRunning())) + { + if ((mVerb == .Run) && (!mDidRun) && (!mFailed)) + { + let curPath = scope String(); + Directory.GetCurrentDirectory(curPath); + + let workspaceOptions = gApp.GetCurWorkspaceOptions(); + let options = gApp.GetCurProjectOptions(mWorkspace.mStartupProject); + let targetPaths = scope List(); + defer ClearAndDeleteItems(targetPaths); + this.[Friend]GetTargetPaths(mWorkspace.mStartupProject, gApp.mPlatformName, workspaceOptions, options, targetPaths); + if (targetPaths.IsEmpty) + return; + + ExecutionQueueCmd executionCmd = QueueRun(targetPaths[0], mRunArgs ?? "", curPath); + executionCmd.mIsTargetRun = true; + mDidRun = true; + return; + } + + Stop(); + } } } } diff --git a/IDE/src/BeefConfig.bf b/IDE/src/BeefConfig.bf index bdb1e1fc..01171e19 100644 --- a/IDE/src/BeefConfig.bf +++ b/IDE/src/BeefConfig.bf @@ -123,6 +123,7 @@ namespace IDE List mConfigPathQueue = new List() ~ DeleteContainerAndItems!(_); List mLibDirectories = new List() ~ DeleteContainerAndItems!(_); List mWatchers = new .() ~ DeleteContainerAndItems!(_); + public String mManagedLibPath = new .() ~ delete _; public bool mLibsChanged; void LibsChanged() @@ -221,6 +222,15 @@ namespace IDE } } + data.GetString("ManagedLibDir", mManagedLibPath); + if ((mManagedLibPath.IsEmpty) && (!mLibDirectories.IsEmpty)) + { + var libPath = Path.GetAbsolutePath(mLibDirectories[0].mPath, gApp.mInstallDir, .. scope .()); + var managedPath = Path.GetAbsolutePath("../BeefManaged", libPath, .. scope .()); + if (Directory.Exists(managedPath)) + mManagedLibPath.Set(managedPath); + } + mConfigFiles.Add(configFile); return .Ok; diff --git a/IDE/src/IDEApp.bf b/IDE/src/IDEApp.bf index 83b14379..8c8884ec 100644 --- a/IDE/src/IDEApp.bf +++ b/IDE/src/IDEApp.bf @@ -76,6 +76,7 @@ namespace IDE OpenOrNew, Test, Run, + Update, GetVersion } @@ -182,6 +183,7 @@ namespace IDE public MainFrame mMainFrame; public GlobalUndoManager mGlobalUndoManager = new GlobalUndoManager() ~ delete _; public SourceControl mSourceControl = new SourceControl() ~ delete _; + public GitManager mGitManager = new .() ~ delete _; public WidgetWindow mPopupWindow; public RecentFileSelector mRecentFileSelector; @@ -223,6 +225,7 @@ namespace IDE public WakaTime mWakaTime ~ delete _; public PackMan mPackMan = new PackMan() ~ delete _; + public HashSet mWantUpdateVersionLocks ~ DeleteContainerAndItems!(_); public Settings mSettings = new Settings() ~ delete _; public Workspace mWorkspace = new Workspace() ~ delete _; public FileWatcher mFileWatcher = new FileWatcher() ~ delete _; @@ -2145,6 +2148,91 @@ namespace IDE return true; } + bool SaveWorkspaceLockData(bool force = false) + { + if ((mWorkspace.mProjectLockMap.IsEmpty) && (!force)) + return true; + + StructuredData sd = scope StructuredData(); + sd.CreateNew(); + sd.Add("FileVersion", 1); + using (sd.CreateObject("Locks")) + { + List projectNames = scope .(mWorkspace.mProjectLockMap.Keys); + projectNames.Sort(); + + for (var projectName in projectNames) + { + var lock = mWorkspace.mProjectLockMap[projectName]; + switch (lock) + { + case .Git(let url, let tag, let hash): + using (sd.CreateObject(projectName)) + { + using (sd.CreateObject("Git")) + { + sd.Add("URL", url); + sd.Add("Tag", tag); + sd.Add("Hash", hash); + } + } + default: + } + } + } + + String jsonString = scope String(); + sd.ToTOML(jsonString); + + String lockFileName = scope String(); + GetWorkspaceLockFileName(lockFileName); + if (lockFileName.IsEmpty) + return false; + return SafeWriteTextFile(lockFileName, jsonString); + } + + bool LoadWorkspaceLockData() + { + String lockFilePath = scope String(); + GetWorkspaceLockFileName(lockFilePath); + if (lockFilePath.IsEmpty) + return true; + + var sd = scope StructuredData(); + if (sd.Load(lockFilePath) case .Err) + return false; + + for (var projectName in sd.Enumerate("Locks")) + { + Workspace.Lock lock = default; + if (sd.Contains("Git")) + { + using (sd.Open("Git")) + { + var url = sd.GetString("URL", .. new .()); + var tag = sd.GetString("Tag", .. new .()); + var hash = sd.GetString("Hash", .. new .()); + lock = .Git(url, tag, hash); + } + } + + mWorkspace.SetLock(projectName, lock); + } + + return true; + } + + bool GetWorkspaceLockFileName(String outResult) + { + if (mWorkspace.mDir == null) + return false; + if (mWorkspace.mCompositeFile != null) + outResult.Append(mWorkspace.mCompositeFile.mFilePath, ".bfuser"); + else + outResult.Append(mWorkspace.mDir, "/BeefSpace_Lock.toml"); + return true; + } + void GetDefaultLayoutDataFileName(String outResult) { outResult.Append(mInstallDir, "/DefaultLayout.toml"); @@ -2377,6 +2465,17 @@ namespace IDE project.mDependencies.Add(dep); } + public void ProjectCreated(Project project) + { + mProjectPanel.InitProject(project, mProjectPanel.GetSelectedWorkspaceFolder()); + mProjectPanel.Sort(); + mWorkspace.FixOptions(); + mWorkspace.mHasChanged = true; + + mWorkspace.ClearProjectNameCache(); + CurrentWorkspaceConfigChanged(); + } + public Project CreateProject(String projName, String projDir, Project.TargetType targetType) { Project project = new Project(); @@ -2391,13 +2490,8 @@ namespace IDE AddNewProjectToWorkspace(project); project.FinishCreate(); - mProjectPanel.InitProject(project, mProjectPanel.GetSelectedWorkspaceFolder()); - mProjectPanel.Sort(); - mWorkspace.FixOptions(); - mWorkspace.mHasChanged = true; + ProjectCreated(project); - mWorkspace.ClearProjectNameCache(); - CurrentWorkspaceConfigChanged(); return project; } @@ -2517,6 +2611,8 @@ namespace IDE mBookmarkManager.Clear(); + mPackMan.CancelAll(); + OutputLine("Workspace closed."); } @@ -2757,9 +2853,16 @@ namespace IDE return .Err; } + public void CheckDependenciesLoaded() + { + for (var project in mWorkspace.mProjects) + project.CheckDependenciesLoaded(); + } + void FlushDeferredLoadProjects(bool addToUI = false) { bool hasDeferredProjects = false; + bool loadFailed = false; while (true) { @@ -2767,11 +2870,29 @@ namespace IDE for (int projectIdx = 0; projectIdx < mWorkspace.mProjects.Count; projectIdx++) { var project = mWorkspace.mProjects[projectIdx]; + + if (project.mDeferState == .Searching) + { + if (mPackMan.mFailed) + { + // Just let it fail now + LoadFailed(); + project.mDeferState = .None; + project.mFailed = true; + loadFailed = true; + } + else + { + hasDeferredProjects = true; + } + } + if ((project.mDeferState == .ReadyToLoad) || (project.mDeferState == .Pending)) { hadLoad = true; var projectPath = project.mProjectPath; + if (project.mDeferState == .Pending) { hasDeferredProjects = true; @@ -2794,9 +2915,26 @@ namespace IDE } if (hasDeferredProjects) + { mWorkspace.mProjectLoadState = .Preparing; + } else + { mWorkspace.mProjectLoadState = .Loaded; + SaveWorkspaceLockData(); + CheckDependenciesLoaded(); + } + + if (loadFailed) + { + mProjectPanel.RebuildUI(); + } + } + + public void CancelWorkspaceLoading() + { + mPackMan.CancelAll(); + FlushDeferredLoadProjects(); } protected void LoadWorkspace(BeefVerb verb) @@ -2937,6 +3075,7 @@ namespace IDE } else { + LoadWorkspaceLockData(); mWorkspace.mProjectFileEntries.Add(new .(workspaceFileName)); if (mVerb == .New) @@ -3044,9 +3183,10 @@ namespace IDE outRelaunchCmd.Append(" -safe"); } - public void RetryProjectLoad(Project project) + public void RetryProjectLoad(Project project, bool reloadConfig) { - LoadConfig(); + if (reloadConfig) + LoadConfig(); var projectPath = project.mProjectPath; if (!project.Load(projectPath)) @@ -3054,6 +3194,8 @@ namespace IDE Fail(scope String()..AppendF("Failed to load project '{0}' from '{1}'", project.mProjectName, projectPath)); LoadFailed(); project.mFailed = true; + FlushDeferredLoadProjects(); + mProjectPanel?.RebuildUI(); } else { @@ -3061,7 +3203,7 @@ namespace IDE mWorkspace.FixOptions(); project.mFailed = false; - mProjectPanel.RebuildUI(); + mProjectPanel?.RebuildUI(); CurrentWorkspaceConfigChanged(); } } @@ -3080,7 +3222,17 @@ namespace IDE String verConfigDir = mWorkspace.mDir; if (let project = mWorkspace.FindProject(projectName)) + { + switch (useVerSpec) + { + case .Git(let url, let ver): + if (ver != null) + mPackMan.UpdateGitConstraint(url, ver); + default: + } + return project; + } if (useVerSpec case .SemVer) { @@ -3153,27 +3305,24 @@ namespace IDE case .SemVer(let semVer): // case .Git(let url, let ver): - var verReference = new Project.VerReference(); - verReference.mSrcProjectName = new String(projectName); - verReference.mVerSpec = _.Duplicate(); - project.mVerReferences.Add(verReference); - + var checkPath = scope String(); if (mPackMan.CheckLock(projectName, checkPath)) { projectFilePath = scope:: String(checkPath); } else + { + mPackMan.GetWithVersion(projectName, url, ver); isDeferredLoad = true; + } default: Fail("Invalid version specifier"); return .Err(.InvalidVersionSpec); } if ((projectFilePath == null) && (!isDeferredLoad)) - { return .Err(.NotFound); - } if (isDeferredLoad) { @@ -3197,6 +3346,59 @@ namespace IDE return .Ok(project); } + public void UpdateProjectVersionLocks(params Span projectNames) + { + bool removedLock = false; + + for (var projectName in projectNames) + { + if (var kv = gApp.mWorkspace.mProjectLockMap.GetAndRemoveAlt(projectName)) + { + removedLock = true; + delete kv.key; + kv.value.Dispose(); + } + } + + if (removedLock) + { + if (SaveAll()) + { + SaveWorkspaceLockData(true); + CloseOldBeefManaged(); + ReloadWorkspace(); + } + } + } + + public void UpdateProjectVersionLocks(Span projectNames) + { + List svNames = scope .(); + for (var name in projectNames) + svNames.Add(name); + UpdateProjectVersionLocks(params (Span)svNames); + } + + public void NotifyProjectVersionLocks(Span projectNames) + { + if (projectNames.IsEmpty) + return; + + String message = scope .(); + message.Append((projectNames.Length == 1) ? "Project " : "Projects "); + for (var projectName in projectNames) + { + if (@projectName.Index > 0) + message.Append(", "); + message.AppendF($"'{projectName}'"); + } + + message.Append((projectNames.Length == 1) ? " has " : " have "); + + message.AppendF("modified version constraints. Use 'Update Version Lock' in the project or workspace right-click menus to apply the new constraints."); + MessageDialog("Version Constraints Modified", message, DarkTheme.sDarkTheme.mIconWarning); + } + protected void WorkspaceLoaded() { scope AutoBeefPerf("IDE.WorkspaceLoaded"); @@ -3848,9 +4050,9 @@ namespace IDE return dialog; } - public void MessageDialog(String title, String text) + public void MessageDialog(String title, String text, Image icon = null) { - Dialog dialog = ThemeFactory.mDefault.CreateDialog(title, text); + Dialog dialog = ThemeFactory.mDefault.CreateDialog(title, text, icon); dialog.mDefaultButton = dialog.AddButton("OK"); dialog.mEscButton = dialog.mDefaultButton; dialog.PopupWindow(mMainWindow); @@ -7433,6 +7635,37 @@ namespace IDE CloseDocument(activeDocumentPanel); } + public void CloseOldBeefManaged() + { + List pendingClosePanels = scope .(); + WithSourceViewPanels(scope (sourceViewPanel) => + { + if (sourceViewPanel.mProjectSource != null) + { + var checkHash = gApp.mPackMan.GetHashFromFilePath(sourceViewPanel.mFilePath, .. scope .()); + if (!checkHash.IsEmpty) + { + bool foundHash = false; + + if (gApp.mWorkspace.mProjectLockMap.TryGet(sourceViewPanel.mProjectSource.mProject.mProjectName, ?, var lock)) + { + if (lock case .Git(let url, let tag, let hash)) + { + if (hash == checkHash) + foundHash = true; + } + } + + if (!foundHash) + pendingClosePanels.Add(sourceViewPanel); + } + } + }); + + for (var sourceViewPanel in pendingClosePanels) + CloseDocument(sourceViewPanel); + } + public SourceViewPanel ShowProjectItem(ProjectItem projectItem, bool showTemp = true, bool setFocus = true) { if (projectItem is ProjectSource) @@ -7845,7 +8078,7 @@ namespace IDE case "-autoshutdown": mDebugAutoShutdownCounter = 200; case "-new": - mVerb = .New; + mVerb = .Open; case "-testNoExit": mExitWhenTestScriptDone = false; case "-firstRun": @@ -8090,15 +8323,16 @@ namespace IDE #endif } - public virtual void OutputErrorLine(String format, params Object[] args) + public virtual void OutputErrorLine(StringView format, params Object[] args) { mWantShowOutput = true; var errStr = scope String(); - errStr.Append("ERROR: ", format); + errStr.Append("ERROR: "); + errStr.Append(format); OutputLineSmart(errStr, params args); } - public virtual void OutputWarnLine(String format, params Object[] args) + public virtual void OutputWarnLine(StringView format, params Object[] args) { var warnStr = scope String(); warnStr.AppendF(format, params args); @@ -8113,7 +8347,7 @@ namespace IDE OutputLine(outStr); } - public virtual void OutputLineSmart(String format, params Object[] args) + public virtual void OutputLineSmart(StringView format, params Object[] args) { String outStr; if (args.Count > 0) @@ -10027,7 +10261,7 @@ namespace IDE #endif } mWorkspace.ClearProjectNameCache(); - mProjectPanel.RehupProjects(); + mProjectPanel?.RehupProjects(); } /*public string GetClangDepConfigName(Project project) @@ -10064,8 +10298,8 @@ namespace IDE RemoveProjectItems(project); } - mBfResolveCompiler.QueueDeferredResolveAll(); - mBfResolveCompiler.QueueRefreshViewCommand(.FullRefresh); + mBfResolveCompiler?.QueueDeferredResolveAll(); + mBfResolveCompiler?.QueueRefreshViewCommand(.FullRefresh); return; } @@ -10093,11 +10327,14 @@ namespace IDE { mWantsBeefClean = true; + var checkDeclName = (project.mProjectName !== project.mProjectNameDecl) && (mWorkspace.FindProject(project.mProjectNameDecl) == project); + for (var checkProject in mWorkspace.mProjects) { for (var dep in checkProject.mDependencies) { - if (dep.mProjectName == project.mProjectName) + if ((dep.mProjectName == project.mProjectName) || + ((checkDeclName) && (dep.mProjectName == project.mProjectNameDecl))) { dep.mProjectName.Set(newName); checkProject.SetChanged(); @@ -10115,14 +10352,27 @@ namespace IDE } project.mProjectName.Set(newName); + if (project.mProjectNameDecl != project.mProjectName) + delete project.mProjectNameDecl; + project.mProjectNameDecl = project.mProjectName; project.SetChanged(); mWorkspace.ClearProjectNameCache(); + + mProjectPanel.RebuildUI(); } public void RemoveProject(Project project) { RemoveProjectItems(project); + if (mWorkspace.mProjectLockMap.GetAndRemove(project.mProjectName) case .Ok(let kv)) + { + delete kv.key; + kv.value.Dispose(); + if (mWorkspace.mProjectLockMap.IsEmpty) + SaveWorkspaceLockData(true); + } + project.mDeleted = true; mWorkspace.SetChanged(); mWorkspace.mProjects.Remove(project); @@ -10911,7 +11161,7 @@ namespace IDE } } - static void QuoteIfNeeded(String str) + protected static void QuoteIfNeeded(String str) { if (!str.Contains(' ')) return; @@ -12398,6 +12648,8 @@ namespace IDE base.Init(); mSettings.Apply(); + mGitManager.Init(); + //Yoop(); /*for (int i = 0; i < 100*1024*1024; i++) @@ -14865,6 +15117,8 @@ namespace IDE if (mLongUpdateProfileId != 0) DoLongUpdateCheck(); + mGitManager.Update(); + mPackMan.Update(); if (mWakaTime != null) mWakaTime.Update(); if (mFindResultsPanel != null) @@ -15053,7 +15307,6 @@ namespace IDE [Import("user32.lib"), CLink, CallingConvention(.Stdcall)] public static extern bool MessageBeep(MessageBeepType type); #endif - } static diff --git a/IDE/src/IDEUtils.bf b/IDE/src/IDEUtils.bf index 1b2c41e8..ddbf5eab 100644 --- a/IDE/src/IDEUtils.bf +++ b/IDE/src/IDEUtils.bf @@ -9,6 +9,7 @@ using Beefy; using Beefy.gfx; using Beefy.theme.dark; using IDE.ui; +using System.Diagnostics; namespace IDE { @@ -128,6 +129,29 @@ namespace IDE return true; } + public static void SafeKill(int processId) + { + var beefConExe = scope $"{gApp.mInstallDir}/BeefCon.exe"; + + ProcessStartInfo procInfo = scope ProcessStartInfo(); + procInfo.UseShellExecute = false; + procInfo.SetFileName(beefConExe); + procInfo.SetArguments(scope $"{processId} kill"); + procInfo.ActivateWindow = false; + + var process = scope SpawnedProcess(); + process.Start(procInfo).IgnoreError(); + } + + public static void SafeKill(SpawnedProcess process) + { + if (process.WaitFor(0)) + return; + SafeKill(process.ProcessId); + if (!process.WaitFor(2000)) + process.Kill(); + } + public static bool IsDirectoryEmpty(StringView dirPath) { for (let entry in Directory.Enumerate(scope String()..AppendF("{}/*.*", dirPath), .Directories | .Files)) diff --git a/IDE/src/Project.bf b/IDE/src/Project.bf index 336c2da3..02cb0733 100644 --- a/IDE/src/Project.bf +++ b/IDE/src/Project.bf @@ -1091,9 +1091,13 @@ namespace IDE public class GeneralOptions { [Reflect] + public String mProjectNameDecl; // Points to mProjectNameDecl in Project + [Reflect] public TargetType mTargetType; [Reflect] public List mAliases = new .() ~ DeleteContainerAndItems!(_); + [Reflect] + public SemVer mVersion = new SemVer("") ~ delete _; } public class BeefGlobalOptions @@ -1321,6 +1325,7 @@ namespace IDE { public VerSpec mVerSpec ~ _.Dispose(); public String mProjectName ~ delete _; + public bool mDependencyChecked; } public enum DeferState @@ -1337,13 +1342,20 @@ namespace IDE public VerSpec mVerSpec ~ _.Dispose(); } + public class ManagedInfo + { + public SemVer mVersion = new .("") ~ delete _; + public String mInfo ~ delete _; + } + public Monitor mMonitor = new Monitor() ~ delete _; public String mNamespace = new String() ~ delete _; public String mProjectDir = new String() ~ delete _; public String mProjectName = new String() ~ delete _; + public String mProjectNameDecl = mProjectName ~ { if (mProjectNameDecl != mProjectName) delete _; } public String mProjectPath = new String() ~ delete _; + public ManagedInfo mManagedInfo ~ delete _; public DeferState mDeferState; - public List mVerReferences = new .() ~ DeleteContainerAndItems!(_); //public String mLastImportDir = new String() ~ delete _; public bool mHasChanged; @@ -1410,6 +1422,16 @@ namespace IDE } } + public SemVer Version + { + get + { + if (mManagedInfo != null) + return mManagedInfo.mVersion; + return mGeneralOptions.mVersion; + } + } + void SetupDefaultOptions(Options options) { options.mBuildOptions.mOtherLinkFlags.Set("$(LinkFlags)"); @@ -1443,6 +1465,7 @@ namespace IDE mGeneralOptions.mTargetType = .CustomBuild; SetupDefaultConfigs(); + mGeneralOptions.mProjectNameDecl = mProjectNameDecl; mBeefGlobalOptions.mStartupObject.Set("Program"); } @@ -1526,6 +1549,21 @@ namespace IDE Path.GetDirectoryPath(mProjectPath, mProjectDir); if (structuredData.Load(ProjectFileName) case .Err) return false; + + String managedText = scope .(); + if (File.ReadAllText(scope $"{mProjectDir}/BeefManaged.toml", managedText) case .Ok) + { + mManagedInfo = new .(); + mManagedInfo.mInfo = new .(managedText); + + StructuredData msd = scope .(); + if (msd.LoadFromString(managedText) case .Ok) + { + if (mManagedInfo.mVersion.mVersion == null) + mManagedInfo.mVersion.mVersion = new .(); + msd.GetString("Version", mManagedInfo.mVersion.mVersion); + } + } } else { @@ -1615,7 +1653,8 @@ namespace IDE using (data.CreateObject("Project")) { if (!IsSingleFile) - data.Add("Name", mProjectName); + data.Add("Name", mProjectNameDecl); + data.ConditionalAdd("Version", mGeneralOptions.mVersion.mVersion); data.ConditionalAdd("TargetType", mGeneralOptions.mTargetType, GetDefaultTargetType()); data.ConditionalAdd("StartupObject", mBeefGlobalOptions.mStartupObject, IsSingleFile ? "Program" : ""); var defaultNamespace = scope String(); @@ -1955,7 +1994,24 @@ namespace IDE using (data.Open("Project")) { if (!IsSingleFile) - data.GetString("Name", mProjectName); + { + var projectName = data.GetString("Name", .. scope .()); + if ((!mProjectName.IsEmpty) && (projectName != mProjectName)) + { + // If the name we specified clashes with the delclared project name in the config + if (mProjectNameDecl === mProjectName) + { + mProjectNameDecl = new .(projectName); + mGeneralOptions.mProjectNameDecl = mProjectNameDecl; + gApp.mWorkspace.ClearProjectNameCache(); + } + else + mProjectNameDecl.Set(projectName); + } + else + mProjectName.Set(projectName); + } + data.GetString("Version", mGeneralOptions.mVersion.mVersion); ReadStrings("Aliases", mGeneralOptions.mAliases); data.GetString("StartupObject", mBeefGlobalOptions.mStartupObject, IsSingleFile ? "Program" : ""); var defaultNamespace = scope String(); @@ -2029,7 +2085,7 @@ namespace IDE { case .Ok(let project): case .Err(let err): - gApp.OutputLineSmart("ERROR: Unable to load project '{0}' specified in project '{1}'", dep.mProjectName, mProjectName); + // Give an error later } } @@ -2232,6 +2288,35 @@ namespace IDE mRootFolder.StartWatching(); } + public void CheckDependenciesLoaded() + { + for (var dep in mDependencies) + { + if (!dep.mDependencyChecked) + { + var project = gApp.mWorkspace.FindProject(dep.mProjectName); + if (project != null) + { + var projectVersion = project.Version; + if (!projectVersion.mVersion.IsEmpty) + { + if (dep.mVerSpec case .Git(let url, let ver)) + { + if (!SemVer.IsVersionMatch(projectVersion, ver)) + gApp.OutputLineSmart($"WARNING: Project '{mProjectName}' has version constraint '{ver}' for '{dep.mProjectName}' which is not satisfied by selected version '{projectVersion}'"); + } + } + } + else + { + gApp.OutputLineSmart("ERROR: Unable to load project '{0}' specified in project '{1}'", dep.mProjectName, mProjectName); + } + + dep.mDependencyChecked = true; + } + } + } + public void FinishCreate(bool allowCreateDir = true) { if (!mRootFolder.mIsWatching) @@ -2414,29 +2499,35 @@ namespace IDE } } - public bool HasDependency(String projectName, bool checkRecursively = true) + public VerSpec* GetDependency(String projectName, bool checkRecursively = true) { HashSet checkedProject = scope .(); - bool CheckDependency(Project project) + VerSpec* CheckDependency(Project project) { if (!checkedProject.Add(project)) - return false; + return null; for (var dependency in project.mDependencies) { if (dependency.mProjectName == projectName) - return true; + return &dependency.mVerSpec; let depProject = gApp.mWorkspace.FindProject(dependency.mProjectName); - if ((depProject != null) && (checkRecursively) && (CheckDependency(depProject))) - return true; + if ((depProject != null) && (checkRecursively)) + { + var verSpec = CheckDependency(depProject); + if (verSpec != null) + return verSpec; + } } - return false; + return null; } return CheckDependency(this); } + public bool HasDependency(String projectName, bool checkRecursively = true) => GetDependency(projectName, checkRecursively) != null; + public void SetupDefault(Options options, String configName, String platformName) { bool isRelease = configName.Contains("Release"); diff --git a/IDE/src/Workspace.bf b/IDE/src/Workspace.bf index cdfaae08..a1dab9b6 100644 --- a/IDE/src/Workspace.bf +++ b/IDE/src/Workspace.bf @@ -223,7 +223,8 @@ namespace IDE None, Loaded, ReadyToLoad, - Preparing + Preparing, + Failed } public class ConfigSelection : IHashable, IEquatable @@ -488,26 +489,25 @@ namespace IDE public VerSpec mVerSpec ~ _.Dispose(); } - public class Lock + public enum Lock { - public enum Location - { - case Cache; - case Local(String path); + case Cache; + case Local(String path); + case Git(String url, String tag, String hash); - public void Dispose() + public void Dispose() + { + switch (this) { - switch (this) - { - case .Cache: - case .Local(let path): - delete path; - } + case .Cache: + case .Local(let path): + delete path; + case Git(var url, var tag, var hash): + delete url; + delete tag; + delete hash; } } - - public String mVersion ~ delete _; - public Location mLocation ~ _.Dispose(); } public Monitor mMonitor = new Monitor() ~ delete _; @@ -519,7 +519,15 @@ namespace IDE public List mProjectSpecs = new .() ~ DeleteContainerAndItems!(_); public List mProjectFileEntries = new .() ~ DeleteContainerAndItems!(_); public Dictionary mProjectNameMap = new .() ~ DeleteDictionaryAndKeys!(_); - public Dictionary mProjectLockMap = new .() ~ DeleteDictionaryAndKeysAndValues!(_); + public Dictionary mProjectLockMap = new .() ~ + { + for (var kv in ref _) + { + delete kv.key; + kv.valueRef.Dispose(); + } + delete _; + } public Project mStartupProject; public bool mLoading; public bool mNeedsCreate; @@ -578,6 +586,20 @@ namespace IDE ClearAndDeleteItems(mPlatforms); } + public void SetLock(StringView projectName, Lock lock) + { + if (mProjectLockMap.TryAddAlt(projectName, var keyPtr, var valuePtr)) + { + *keyPtr = new .(projectName); + *valuePtr = lock; + } + else + { + valuePtr.Dispose(); + *valuePtr = lock; + } + } + public void GetPlatformList(List outList) { if (mPlatforms.IsEmpty) @@ -947,16 +969,33 @@ namespace IDE { using (mMonitor.Enter()) { + int GetNamePriority(StringView name, Project project) + { + if (project.mProjectName == name) + return 2; + if (project.mProjectNameDecl == name) + return 1; + return 0; + } + void Add(String name, Project project) { - bool added = mProjectNameMap.TryAdd(name, var keyPtr, var valuePtr); - if (!added) - return; - *keyPtr = new String(name); - *valuePtr = project; + if (mProjectNameMap.TryAdd(name, var keyPtr, var valuePtr)) + { + *keyPtr = new String(name); + *valuePtr = project; + } + else + { + if (GetNamePriority(name, project) > GetNamePriority(*keyPtr, *valuePtr)) + *valuePtr = project; + } } Add(project.mProjectName, project); + + if (project.mProjectNameDecl !== project.mProjectName) + Add(project.mProjectNameDecl, project); for (var alias in project.mGeneralOptions.mAliases) Add(alias, project); @@ -979,7 +1018,11 @@ namespace IDE } for (var project in mProjects) + { Add(project.mProjectName, project); + if (project.mProjectName != project.mProjectNameDecl) + Add(project.mProjectNameDecl, project); + } for (var project in mProjects) { diff --git a/IDE/src/ui/BuildPropertiesDialog.bf b/IDE/src/ui/BuildPropertiesDialog.bf index b5ae8a23..887b9f9f 100644 --- a/IDE/src/ui/BuildPropertiesDialog.bf +++ b/IDE/src/ui/BuildPropertiesDialog.bf @@ -10,6 +10,87 @@ namespace IDE.ui class BuildPropertiesDialog : TargetedPropertiesDialog { + protected class DependencyEntry : IEquatable, IMultiValued + { + public bool mUse; + public String mURL ~ delete _; + public String mVersion ~ delete _; + + public this() + { + + } + + public ~this() + { + } + + public this(DependencyEntry val) + { + mUse = val.mUse; + if (val.mURL != null) + mURL = new .(val.mURL); + if (val.mVersion != null) + mVersion = new .(val.mVersion); + } + + public bool Equals(Object val) + { + if (var rhsDE = val as DependencyEntry) + { + return + (mUse == rhsDE.mUse) && + (mURL == rhsDE.mURL) && + (mVersion == rhsDE.mVersion); + } + return false; + } + + public void GetValue(int idx, String outValue) + { + if ((idx == 1) && (mURL != null)) + outValue.Set(mURL); + if ((idx == 2) && (mVersion != null)) + outValue.Set(mVersion); + } + + public bool SetValue(int idx, StringView value) + { + if (idx == 1) + { + if (value.IsEmpty) + { + DeleteAndNullify!(mURL); + } + else + { + String.NewOrSet!(mURL, value); + mURL.Trim(); + } + } + if (idx == 2) + { + if (value.IsEmpty) + { + DeleteAndNullify!(mVersion); + } + else + { + String.NewOrSet!(mVersion, value); + mVersion.Trim(); + } + } + return true; + } + + public void Set(DependencyEntry value) + { + mUse = value.mUse; + SetValue(1, value.mURL); + SetValue(2, value.mVersion); + } + } + protected class DistinctOptionBuilder { BuildPropertiesDialog mDialog; diff --git a/IDE/src/ui/ProjectPanel.bf b/IDE/src/ui/ProjectPanel.bf index 91f2ba50..641f7a3d 100644 --- a/IDE/src/ui/ProjectPanel.bf +++ b/IDE/src/ui/ProjectPanel.bf @@ -145,6 +145,7 @@ namespace IDE.ui bool mImportFolderDeferred; bool mImportProjectDeferred; bool mImportInstalledDeferred; + bool mImportRemoteDeferred; public Dictionary mListViewToProjectMap = new .() ~ delete _; public Dictionary mProjectToListViewMap = new .() ~ delete _; public Dictionary mListViewToWorkspaceFolderMap = new .() ~ delete _; @@ -2904,6 +2905,15 @@ namespace IDE.ui #endif } + void ImportRemoteProject() + { +#if !CLI + RemoteProjectDialog dialog = new .(); + dialog.Init(); + dialog.PopupWindow(gApp.mMainWindow); +#endif + } + public void ShowProjectProperties(Project project) { var projectProperties = new ProjectProperties(project); @@ -3081,6 +3091,14 @@ namespace IDE.ui }); if (gApp.IsCompiling) anItem.SetDisabled(true); + + anItem = menu.AddItem("Add From Remote..."); + anItem.mOnMenuItemSelected.Add(new (item) => { + mImportRemoteDeferred = true; + }); + if (gApp.IsCompiling) + anItem.SetDisabled(true); + anItem = menu.AddItem("New Folder"); anItem.mOnMenuItemSelected.Add(new (item) => { var workspaceFolder = GetSelectedWorkspaceFolder(); @@ -3110,7 +3128,18 @@ namespace IDE.ui } else if (gApp.mWorkspace.IsInitialized) { + var item = menu.AddItem("Update Version Locks"); + item.mDisabled = gApp.mWorkspace.mProjectLockMap.IsEmpty; + item.mOnMenuItemSelected.Add(new (item) => + { + List projectNames = scope .(); + for (var projectName in gApp.mWorkspace.mProjectLockMap.Keys) + projectNames.Add(projectName); + gApp.UpdateProjectVersionLocks(params (Span)projectNames); + }); + AddOpenContainingFolder(); + menu.AddItem(); AddWorkspaceMenuItems(); @@ -3140,7 +3169,7 @@ namespace IDE.ui { var projectItem = GetSelectedProjectItem(); if (projectItem != null) - gApp.RetryProjectLoad(projectItem.mProject); + gApp.RetryProjectLoad(projectItem.mProject, true); }); menu.AddItem(); //handled = true; @@ -3168,6 +3197,18 @@ namespace IDE.ui SetAsStartupProject(projectItem.mProject); }); + item = menu.AddItem("Update Version Lock"); + item.mDisabled = (projectItem == null) || (!gApp.mWorkspace.mProjectLockMap.ContainsKey(projectItem.mProject.mProjectName)); + item.mOnMenuItemSelected.Add(new (item) => + { + var projectItem = GetSelectedProjectItem(); + if (projectItem != null) + { + let project = projectItem.mProject; + gApp.UpdateProjectVersionLocks(project.mProjectName); + } + }); + item = menu.AddItem("Lock Project"); if (projectItem.mProject.mLocked) item.mIconImage = DarkTheme.sDarkTheme.GetImage(.Check); @@ -3570,6 +3611,12 @@ namespace IDE.ui ImportInstalledProject(); } + if (mImportRemoteDeferred) + { + mImportRemoteDeferred= false; + ImportRemoteProject(); + } + ValidateCutClipboard(); } diff --git a/IDE/src/ui/ProjectProperties.bf b/IDE/src/ui/ProjectProperties.bf index f53b8e55..02638e6c 100644 --- a/IDE/src/ui/ProjectProperties.bf +++ b/IDE/src/ui/ProjectProperties.bf @@ -10,13 +10,12 @@ using Beefy.events; using Beefy.theme.dark; using Beefy.gfx; using Beefy.geom; +using IDE.Util; namespace IDE.ui { public class ProjectProperties : BuildPropertiesDialog { - ValueContainer mVC; - enum CategoryType { General, /// @@ -27,6 +26,7 @@ namespace IDE.ui Dependencies, Beef_Global, Platform, + Managed, Targeted, /// Beef_Targeted, @@ -36,10 +36,11 @@ namespace IDE.ui COUNT } - + public Project mProject; - Dictionary> mDependencyValuesMap ~ DeleteDictionaryAndKeysAndValues!(_); + Dictionary mDependencyValuesMap ~ DeleteDictionaryAndKeysAndValues!(_); Project.Options[] mCurProjectOptions ~ delete _; + List mUpdateProjectLocks = new .() ~ DeleteContainerAndItems!(_); float mLockFlashPct; public int32 mNewDebugSessionCountdown; @@ -96,6 +97,7 @@ namespace IDE.ui AddCategoryItem(globalItem, "Dependencies"); AddCategoryItem(globalItem, "Beef"); AddCategoryItem(globalItem, "Platform"); + AddCategoryItem(globalItem, "Managed"); globalItem.Open(true, true); var targetedItem = AddCategoryItem(root, "Targeted"); @@ -149,7 +151,8 @@ namespace IDE.ui case .General, .Project, .Dependencies, - .Beef_Global: + .Beef_Global, + .Managed: return .None; case .Platform: return .Platform; @@ -432,6 +435,7 @@ namespace IDE.ui default: } } + case .Managed: case .Build, .Debugging, .Beef_Targeted: DeleteDistinctBuildOptions(); DistinctBuildOptions defaultTypeOptions = scope:: .(); @@ -530,7 +534,9 @@ namespace IDE.ui else { mCurPropertiesTargets = new Object[1]; - if (categoryType == .Project) + if (categoryType == .Managed) + mCurPropertiesTargets[0] = mProject.mManagedInfo; + else if (categoryType == .Project) mCurPropertiesTargets[0] = mProject.mGeneralOptions; else if (categoryType == .Beef_Global) mCurPropertiesTargets[0] = mProject.mBeefGlobalOptions; @@ -600,6 +606,8 @@ namespace IDE.ui } } } + else if (categoryType == CategoryType.Managed) + PopulateManagedOptions(); else if (categoryType == CategoryType.Build) PopulateBuildOptions(); else if (categoryType == CategoryType.Beef_Global ) @@ -619,6 +627,8 @@ namespace IDE.ui void PopulateGeneralOptions() { var root = (DarkListViewItem)mPropPage.mPropertiesListView.GetRoot(); + var (listViewItem, propEntry) = AddPropertiesItem(root, "Project Name", "mProjectNameDecl"); + AddPropertiesItem(root, "Project Name Aliases", "mAliases"); AddPropertiesItem(root, "Target Type", "mTargetType", scope String[] ( "Console Application", @@ -627,9 +637,21 @@ namespace IDE.ui "Custom Build", "Test" )); - AddPropertiesItem(root, "Project Name Aliases", "mAliases"); + AddPropertiesItem(root, "Version", "mVersion.mVersion"); } + void PopulateManagedOptions() + { + if (mCurPropertiesTargets[0] == null) + return; + var root = (DarkListViewItem)mPropPage.mPropertiesListView.GetRoot(); + var (listViewItem, propEntry) = AddPropertiesItem(root, "Version", "mVersion.mVersion"); + propEntry.mReadOnly = true; + (listViewItem, propEntry) = AddPropertiesItem(root, "Info", "mInfo"); + propEntry.mAllowMultiline = true; + propEntry.mReadOnly = true; + } + void PopulateWindowsOptions() { var root = (DarkListViewItem)mPropPage.mPropertiesListView.GetRoot(); @@ -696,7 +718,21 @@ namespace IDE.ui void PopulateDependencyOptions() { - mDependencyValuesMap = new Dictionary>(); + mPropPage.mPropertiesListView.mColumns[0].Label = "Project"; + mPropPage.mPropertiesListView.mColumns[0].mMinWidth = GS!(100); + mPropPage.mPropertiesListView.mColumns[0].mWidth = GS!(180); + + mPropPage.mPropertiesListView.mColumns[1].Label = ""; + mPropPage.mPropertiesListView.mColumns[1].mMinWidth = GS!(20); + mPropPage.mPropertiesListView.mColumns[1].mWidth = GS!(20); + + mPropPage.mPropertiesListView.AddColumn(180, "Remote URL"); + mPropPage.mPropertiesListView.mColumns[2].mMinWidth = GS!(100); + + mPropPage.mPropertiesListView.AddColumn(180, "Ver Constraint"); + mPropPage.mPropertiesListView.mColumns[3].mMinWidth = GS!(100); + + mDependencyValuesMap = new .(); var root = (DarkListViewItem)mPropPage.mPropertiesListView.GetRoot(); var category = root; @@ -719,63 +755,184 @@ namespace IDE.ui projectNames.Sort(scope (a, b) => String.Compare(a, b, true)); for (var projectName in projectNames) - { - var dependencyContainer = new ValueContainer(); - dependencyContainer.mValue = mProject.HasDependency(projectName, false); - mDependencyValuesMap[new String(projectName)] = dependencyContainer; + { + var project = gApp.mWorkspace.FindProject(projectName); + + var dependencyEntry = new DependencyEntry(); + var verSpec = mProject.GetDependency(projectName, false); + if (verSpec != null) + { + dependencyEntry.mUse = true; + if (verSpec case .Git(let url, let ver)) + { + dependencyEntry.mURL = new .(url); + if (ver != null) + dependencyEntry.mVersion = new .(ver.mVersion); + } + } + mDependencyValuesMap[new String(projectName)] = dependencyEntry; var (listViewItem, propItem) = AddPropertiesItem(category, projectName); if (IDEApp.sApp.mWorkspace.FindProject(projectName) == null) listViewItem.mTextColor = Color.Mult(DarkTheme.COLOR_TEXT, 0xFFFF6060); - var subItem = listViewItem.CreateSubItem(1); + var subItem = (DarkListViewItem)listViewItem.CreateSubItem(1); var checkbox = new DarkCheckBox(); - checkbox.Checked = dependencyContainer.mValue; + checkbox.Checked = dependencyEntry.mUse; checkbox.Resize(0, 0, DarkTheme.sUnitSize, DarkTheme.sUnitSize); subItem.AddWidget(checkbox); PropEntry[] propEntries = new PropEntry[1]; PropEntry propEntry = new PropEntry(); - propEntry.mTarget = dependencyContainer; - //propEntry.mFieldInfo = dependencyContainer.GetType().GetField("mValue").Value; - propEntry.mOrigValue = Variant.Create(dependencyContainer.mValue); - propEntry.mCurValue = propEntry.mOrigValue; + propEntry.mTarget = dependencyEntry; + propEntry.mOrigValue = Variant.Create(dependencyEntry); + propEntry.mCurValue = Variant.Create(new DependencyEntry(dependencyEntry), true); propEntry.mListViewItem = listViewItem; propEntry.mCheckBox = checkbox; propEntry.mApplyAction = new () => { - if (propEntry.mCurValue.Get()) + bool updateProjectLock = false; + + var dependencyEntry = propEntry.mCurValue.Get(); + if (dependencyEntry.mUse) { - if (!mProject.HasDependency(listViewItem.mLabel)) + VerSpec verSpec = default; + if (dependencyEntry.mURL != null) + verSpec = .Git(new .(dependencyEntry.mURL), (dependencyEntry.mVersion != null) ? new .(dependencyEntry.mVersion) : null); + else if (dependencyEntry.mVersion != null) + verSpec = .SemVer(new .(dependencyEntry.mVersion)); + else + verSpec = .SemVer(new .("*")); + + var verSpecPtr = mProject.GetDependency(listViewItem.mLabel); + if (verSpecPtr == null) { + if (verSpec case .Git(let url, let ver)) + updateProjectLock = true; + var dep = new Project.Dependency(); dep.mProjectName = new String(listViewItem.mLabel); - dep.mVerSpec = .SemVer(new .("*")); + dep.mVerSpec = verSpec; mProject.mDependencies.Add(dep); } + else + { + if (*verSpecPtr != verSpec) + { + if ((*verSpecPtr case .Git) || + (verSpecPtr case .Git)) + updateProjectLock = true; + verSpecPtr.Dispose(); + *verSpecPtr = verSpec; + } + } } else { int idx = mProject.mDependencies.FindIndex(scope (dep) => dep.mProjectName == listViewItem.mLabel); if (idx != -1) { + var dep = mProject.mDependencies[idx]; + if (dep.mVerSpec case .Git) + updateProjectLock = true; delete mProject.mDependencies[idx]; mProject.mDependencies.RemoveAt(idx); } } - propEntry.mOrigValue = propEntry.mCurValue; + + var origDependencyEntry = propEntry.mOrigValue.Get(); + origDependencyEntry.Set(dependencyEntry); + + if (updateProjectLock) + mUpdateProjectLocks.Add(new .(listViewItem.Label)); }; - checkbox.mOnMouseUp.Add(new (evt) => { PropEntry.DisposeVariant(ref propEntry.mCurValue); propEntry.mCurValue = Variant.Create(checkbox.Checked); }); + checkbox.mOnMouseUp.Add(new (evt) => + { + var dependencyEntry = propEntry.mCurValue.Get(); + dependencyEntry.mUse = !dependencyEntry.mUse; + if (dependencyEntry.mUse) + { + var projectName = listViewItem.Label; + + for (var projectSpec in gApp.mWorkspace.mProjectSpecs) + { + if (projectSpec.mProjectName == projectName) + { + if (projectSpec.mVerSpec case .Git(let url, let ver)) + { + dependencyEntry.SetValue(1, url); + dependencyEntry.SetValue(2, ver.mVersion); + } + } + } + var propEntries = mPropPage.mPropEntries[listViewItem]; + UpdatePropertyValue(propEntries); + } + else + { + DeleteAndNullify!(dependencyEntry.mURL); + DeleteAndNullify!(dependencyEntry.mVersion); + var propEntries = mPropPage.mPropEntries[listViewItem]; + UpdatePropertyValue(propEntries); + } + + }); + + + subItem = (.)listViewItem.GetOrCreateSubItem(2); + if (dependencyEntry.mURL != null) + subItem.Label = dependencyEntry.mURL; + subItem.mOnMouseDown.Add(new => DepPropValueClicked); + + subItem = (.)listViewItem.GetOrCreateSubItem(3); + if (dependencyEntry.mVersion != null) + { + subItem.Label = dependencyEntry.mVersion; + if (project != null) + { + var version = project.Version; + if (!version.IsEmpty) + { + if (!SemVer.IsVersionMatch(version.mVersion, dependencyEntry.mVersion)) + subItem.mTextColor = Color.Mult(DarkTheme.COLOR_TEXT, 0xFFFFFF60); + } + } + } + subItem.mOnMouseDown.Add(new => DepPropValueClicked); propEntries[0] = propEntry; mPropPage.mPropEntries[listViewItem] = propEntries; } } + protected void DepPropValueClicked(MouseEvent theEvent) + { + DarkListViewItem clickedItem = (DarkListViewItem)theEvent.mSender; + if (clickedItem.mColumnIdx == 0) + { + clickedItem.mListView.SetFocus(); + clickedItem.mListView.GetRoot().SelectItemExclusively(clickedItem); + return; + } + + if (theEvent.mX != -1) + { + clickedItem.mListView.GetRoot().SelectItemExclusively(null); + } + + DarkListViewItem item = (DarkListViewItem)clickedItem; + DarkListViewItem rootItem = (DarkListViewItem)clickedItem.GetSubItem(0); + + PropEntry[] propertyEntries = mPropPage.mPropEntries[rootItem]; + if (propertyEntries[0].mDisabled) + return; + EditValue(item, propertyEntries, clickedItem.mColumnIdx - 1); + } + protected override Object[] PhysAddNewDistinctBuildOptions() { if (mCurProjectOptions == null) @@ -985,6 +1142,8 @@ namespace IDE.ui /*if (!AssertNotCompilingOrRunning()) return false;*/ + String newProjectName = scope .(); + using (mProject.mMonitor.Enter()) { for (var targetedConfigData in mConfigDatas) @@ -1000,9 +1159,18 @@ namespace IDE.ui for (var propEntry in propEntries) { if (propEntry.HasChanged()) - { - configDataHadChange = true; - propEntry.ApplyValue(); + { + if ((propEntry.mFieldInfo != default) && (propEntry.mFieldInfo.Name == "mProjectNameDecl")) + { + var newName = propEntry.mCurValue.Get(); + newProjectName.Append(newName); + newProjectName.Trim(); + } + else + { + configDataHadChange = true; + propEntry.ApplyValue(); + } } } if (propPage == mPropPage) @@ -1060,6 +1228,9 @@ namespace IDE.ui ClearTargetedData(); } + if (!newProjectName.IsEmpty) + gApp.RenameProject(mProject, newProjectName); + return true; } @@ -1081,6 +1252,7 @@ namespace IDE.ui { base.Close(); SetWorkspaceData(false); + gApp.NotifyProjectVersionLocks(mUpdateProjectLocks); } public override void PopupWindow(WidgetWindow parentWindow, float offsetX = 0, float offsetY = 0) diff --git a/IDE/src/ui/PropertiesDialog.bf b/IDE/src/ui/PropertiesDialog.bf index 7793841c..96ef72b1 100644 --- a/IDE/src/ui/PropertiesDialog.bf +++ b/IDE/src/ui/PropertiesDialog.bf @@ -114,6 +114,12 @@ namespace IDE.ui public class PropertiesDialog : IDEDialog { + public interface IMultiValued + { + void GetValue(int idx, String outValue); + bool SetValue(int idx, StringView value); + } + class OwnedStringList : List { @@ -215,6 +221,7 @@ namespace IDE.ui public String mRelPath ~ delete _; public bool mIsTypeWildcard; public bool mAllowMultiline; + public bool mReadOnly; public Insets mEditInsets ~ delete _; public ~this() @@ -305,6 +312,18 @@ namespace IDE.ui } return true; } + else if (type.IsObject) + { + var lhsObj = lhs.Get(); + var rhsObj = rhs.Get(); + + if ((var lhsEq = lhsObj as IEquatable) && (var rhsEq = rhsObj as IEquatable)) + { + return lhsEq.Equals(rhsEq); + } + + return false; + } else // Could be an int or enum return Variant.Equals!(lhs, rhs); } @@ -815,7 +834,7 @@ namespace IDE.ui if (mPropEditWidget != null) { - DarkListViewItem editItem = (DarkListViewItem)mEditingListViewItem.GetSubItem(1); + DarkListViewItem editItem = (DarkListViewItem)mEditingListViewItem; let propEntry = mEditingProps[0]; float xPos; @@ -871,7 +890,7 @@ namespace IDE.ui editWidget.GetText(newValue); newValue.Trim(); - DarkListViewItem item = (DarkListViewItem)mEditingListViewItem; + DarkListViewItem rootItem = (DarkListViewItem)mEditingListViewItem.GetSubItem(0); //DarkListViewItem valueItem = (DarkListViewItem)item.GetSubItem(1); if (!editWidget.mEditWidgetContent.HasUndoData()) @@ -920,14 +939,14 @@ namespace IDE.ui { // } - else if (editingProp.mListViewItem != item) + else if (editingProp.mListViewItem != rootItem) { List curEntries = editingProp.mCurValue.Get>(); List entries = new List(curEntries.GetEnumerator()); for (int32 childIdx = 0; childIdx < editingProp.mListViewItem.GetChildCount(); childIdx++) { - if (item == editingProp.mListViewItem.GetChildAtIndex(childIdx)) + if (rootItem == editingProp.mListViewItem.GetChildAtIndex(childIdx)) { if (childIdx >= entries.Count) entries.Add(new String(newValue)); @@ -1027,6 +1046,11 @@ namespace IDE.ui setValue = false; } } + else if ((curVariantType.IsObject) && (var multiValue = prevValue.Get() as IMultiValued)) + { + multiValue.SetValue(mEditingListViewItem.mColumnIdx - 1, newValue); + setValue = false; + } else editingProp.mCurValue = Variant.Create(new String(newValue), true); @@ -1247,37 +1271,49 @@ namespace IDE.ui if (ewc.mIsMultiline) editWidget.InitScrollbars(false, true); + if (propEntry.mReadOnly) + editWidget.mEditWidgetContent.mIsReadOnly = true; + editWidget.mScrollContentInsets.Set(GS!(3), GS!(3), GS!(1), GS!(3)); editWidget.Content.mTextInsets.Set(GS!(-3), GS!(2), 0, GS!(2)); //editWidget.RehupSize(); if (subValueIdx != -1) { - List stringList = propEntry.mCurValue.Get>(); - if (subValueIdx < stringList.Count) - editWidget.SetText(stringList[subValueIdx]); + var obj = propEntry.mCurValue.Get(); + if (var multiValued = obj as IMultiValued) + { + var label = multiValued.GetValue(subValueIdx, .. scope .()); + editWidget.SetText(label); + } + else + { + List stringList = obj as List; + if (subValueIdx < stringList.Count) + editWidget.SetText(stringList[subValueIdx]); - MoveItemWidget moveItemWidget; - if (subValueIdx > 0) - { - moveItemWidget = new MoveItemWidget(); - editWidget.AddWidget(moveItemWidget); - moveItemWidget.Resize(6, editWidget.mY - GS!(16), GS!(20), GS!(20)); - moveItemWidget.mArrowDir = -1; - moveItemWidget.mOnMouseDown.Add(new (evt) => { MoveEditingItem(subValueIdx, -1); }); - if (!ewc.mIsMultiline) - editWidget.mOnKeyDown.Add(new (evt) => { if (evt.mKeyCode == KeyCode.Up) MoveEditingItem(subValueIdx, -1); }); - } + MoveItemWidget moveItemWidget; + if (subValueIdx > 0) + { + moveItemWidget = new MoveItemWidget(); + editWidget.AddWidget(moveItemWidget); + moveItemWidget.Resize(6, editWidget.mY - GS!(16), GS!(20), GS!(20)); + moveItemWidget.mArrowDir = -1; + moveItemWidget.mOnMouseDown.Add(new (evt) => { MoveEditingItem(subValueIdx, -1); }); + if (!ewc.mIsMultiline) + editWidget.mOnKeyDown.Add(new (evt) => { if (evt.mKeyCode == KeyCode.Up) MoveEditingItem(subValueIdx, -1); }); + } - if (subValueIdx < stringList.Count - 1) - { - moveItemWidget = new MoveItemWidget(); - editWidget.AddWidget(moveItemWidget); - moveItemWidget.Resize(6, editWidget.mY + GS!(16), GS!(20), GS!(20)); - moveItemWidget.mArrowDir = 1; - moveItemWidget.mOnMouseDown.Add(new (evt) => { MoveEditingItem(subValueIdx, 1); }); - if (!ewc.mIsMultiline) - editWidget.mOnKeyDown.Add(new (evt) => { if (evt.mKeyCode == KeyCode.Down) MoveEditingItem(subValueIdx, 1); }); - } + if (subValueIdx < stringList.Count - 1) + { + moveItemWidget = new MoveItemWidget(); + editWidget.AddWidget(moveItemWidget); + moveItemWidget.Resize(6, editWidget.mY + GS!(16), GS!(20), GS!(20)); + moveItemWidget.mArrowDir = 1; + moveItemWidget.mOnMouseDown.Add(new (evt) => { MoveEditingItem(subValueIdx, 1); }); + if (!ewc.mIsMultiline) + editWidget.mOnKeyDown.Add(new (evt) => { if (evt.mKeyCode == KeyCode.Down) MoveEditingItem(subValueIdx, 1); }); + } + } } else { @@ -1363,8 +1399,8 @@ namespace IDE.ui hasChanged = true; } - if (propEntry.mFieldInfo == default(FieldInfo)) - return; + /*if (propEntry.mFieldInfo == default(FieldInfo)) + return;*/ var curVariantType = propEntry.mCurValue.VariantType; @@ -1516,6 +1552,15 @@ namespace IDE.ui valueItem.Label = allValues; FixLabel(valueItem); } + else if ((curVariantType.IsObject) && (var multiValue = propEntry.mCurValue.Get() as IMultiValued)) + { + for (int columnIdx in 1..(); @@ -2026,9 +2071,10 @@ namespace IDE.ui clickedItem.mListView.GetRoot().SelectItemExclusively(null); } - DarkListViewItem item = (DarkListViewItem)clickedItem.GetSubItem(0); + DarkListViewItem item = (DarkListViewItem)clickedItem; + DarkListViewItem rootItem = (DarkListViewItem)item.GetSubItem(0); - PropEntry[] propertyEntries = mPropPage.mPropEntries[item]; + PropEntry[] propertyEntries = mPropPage.mPropEntries[rootItem]; if (propertyEntries[0].mDisabled) return; EditValue(item, propertyEntries); @@ -2039,7 +2085,7 @@ namespace IDE.ui var propEntry = propEntries[0]; DarkListViewItem parentItem = propEntry.mListViewItem; DarkListViewItem clickedItem = (DarkListViewItem)parentItem.GetChildAtIndex(idx); - DarkListViewItem item = (DarkListViewItem)clickedItem.GetSubItem(0); + DarkListViewItem item = (DarkListViewItem)clickedItem.GetSubItem(1); EditValue(item, propEntries, idx); } diff --git a/IDE/src/ui/RemoteProjectDialog.bf b/IDE/src/ui/RemoteProjectDialog.bf new file mode 100644 index 00000000..97724cd2 --- /dev/null +++ b/IDE/src/ui/RemoteProjectDialog.bf @@ -0,0 +1,179 @@ +#pragma warning disable 168 +using System; +using System.Collections; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Beefy; +using Beefy.gfx; +using Beefy.theme.dark; +using Beefy.widgets; +using Beefy.theme; +using IDE.Util; + +namespace IDE.ui +{ + public class RemoteProjectDialog : IDEDialog + { + public EditWidget mURLEdit; + public EditWidget mVersionEdit; + public DarkComboBox mTargetComboBox; + static String[1] sApplicationTypeNames = + .("Git"); + public bool mNameChanged; + public String mDirBase ~ delete _; + + public this() + { + mTitle = new String("Add Remote Project"); + } + + public override void CalcSize() + { + mWidth = GS!(320); + mHeight = GS!(200); + } + + enum CreateFlags + { + None, + NonEmptyDirOkay = 1, + } + + bool CreateProject(CreateFlags createFlags = .None) + { + var app = IDEApp.sApp; + String url = scope String(); + mURLEdit.GetText(url); + url.Trim(); + + if (url.IsEmpty) + { + mURLEdit.SetFocus(); + app.Fail("Invalid URL"); + return false; + } + + var projName = Path.GetFileName(url, .. scope .()); + + var version = mVersionEdit.GetText(.. scope .())..Trim(); + + var otherProject = app.mWorkspace.FindProject(projName); + if (otherProject != null) + { + mURLEdit.SetFocus(); + app.Fail("A project with this name already exists in the workspace."); + return false; + } + + VerSpec verSpec = .Git(url, scope .(version)); + if (var project = gApp.AddProject(projName, verSpec)) + { + //gApp.ProjectCreated(project); + app.mWorkspace.SetChanged(); + + gApp.[Friend]FlushDeferredLoadProjects(true); + //gApp.RetryProjectLoad(project, false); + //gApp.AddProjectToWorkspace(project); + + var projectSpec = new Workspace.ProjectSpec(); + projectSpec.mProjectName = new .(project.mProjectName); + projectSpec.mVerSpec = .Git(new .(url), new .(version)); + gApp.mWorkspace.mProjectSpecs.Add(projectSpec); + } + + return true; + } + + public void UpdateProjectName() + { + if (!mNameChanged) + { + String path = scope .(); + mURLEdit.GetText(path); + path.Trim(); + if ((path.EndsWith('\\')) || (path.EndsWith('/'))) + path.RemoveFromEnd(1); + + String projName = scope .(); + Path.GetFileName(path, projName); + mVersionEdit.SetText(projName); + } + } + + public void Init() + { + mDefaultButton = AddButton("Create", new (evt) => + { + if (!CreateProject()) evt.mCloseDialog = false; + }); + mEscButton = AddButton("Cancel", new (evt) => Close()); + + if (gApp.mWorkspace.IsInitialized) + mDirBase = new String(gApp.mWorkspace.mDir); + else + mDirBase = new String(); + mURLEdit = new DarkEditWidget(); + + AddEdit(mURLEdit); + mURLEdit.mOnContentChanged.Add(new (dlg) => + { + + }); + + mVersionEdit = AddEdit(""); + mVersionEdit.mOnContentChanged.Add(new (dlg) => + { + if (mVersionEdit.mHasFocus) + mNameChanged = true; + }); + + mTargetComboBox = new DarkComboBox(); + mTargetComboBox.Label = sApplicationTypeNames[0]; + mTargetComboBox.mPopulateMenuAction.Add(new (dlg) => + { + for (var applicationTypeName in sApplicationTypeNames) + { + var item = dlg.AddItem(applicationTypeName); + item.mOnMenuItemSelected.Add(new (item) => + { + mTargetComboBox.Label = item.mLabel; + MarkDirty(); + }); + } + }); + AddWidget(mTargetComboBox); + mTabWidgets.Add(mTargetComboBox); + } + + public override void PopupWindow(WidgetWindow parentWindow, float offsetX = 0, float offsetY = 0) + { + base.PopupWindow(parentWindow, offsetX, offsetY); + mURLEdit.SetFocus(); + } + + public override void ResizeComponents() + { + base.ResizeComponents(); + + float curY = mHeight - GS!(30) - mButtonBottomMargin; + mVersionEdit.Resize(GS!(16), curY - GS!(36), mWidth - GS!(16) * 2, GS!(24)); + + curY -= GS!(50); + mURLEdit.Resize(GS!(16), curY - GS!(36), mWidth - GS!(16) * 2, GS!(24)); + + curY -= GS!(60); + mTargetComboBox.Resize(GS!(16), curY - GS!(36), mWidth - GS!(16) * 2, GS!(28)); + } + + public override void Draw(Graphics g) + { + base.Draw(g); + + g.DrawString("Remote Project URL", mURLEdit.mX, mURLEdit.mY - GS!(20)); + g.DrawString("Version Constraint (Blank for HEAD)", mVersionEdit.mX, mVersionEdit.mY - GS!(20)); + } + } + + +} diff --git a/IDE/src/ui/RenameSymbolDialog.bf b/IDE/src/ui/RenameSymbolDialog.bf index 53d4f468..056af1ca 100644 --- a/IDE/src/ui/RenameSymbolDialog.bf +++ b/IDE/src/ui/RenameSymbolDialog.bf @@ -414,7 +414,8 @@ namespace IDE.ui if (mGettingSymbolInfo) { - gApp.Fail("Cannot rename symbols here"); + if (gApp.mWorkspace.mProjectLoadState == .Loaded) + gApp.Fail("Cannot rename symbols here"); mGettingSymbolInfo = false; return; } @@ -430,6 +431,12 @@ namespace IDE.ui if ((mKind == Kind.ShowFileReferences) || (mResolveParams.mLocalId != -1)) { mParser = IDEApp.sApp.mBfResolveSystem.FindParser(mSourceViewPanel.mProjectSource); + if (mParser == null) + { + mGettingSymbolInfo = false; + return; + } + if ((mResolveParams != null) && (mResolveParams.mLocalId != -1)) mParser.SetAutocomplete(mCursorPos); else diff --git a/IDE/src/ui/SourceViewPanel.bf b/IDE/src/ui/SourceViewPanel.bf index 41855c1a..f6f88458 100644 --- a/IDE/src/ui/SourceViewPanel.bf +++ b/IDE/src/ui/SourceViewPanel.bf @@ -1932,7 +1932,7 @@ namespace IDE.ui } else if (resolveType == ResolveType.GetCurrentLocation) { - PrimaryNavigationBar.SetLocation(autocompleteInfo); + PrimaryNavigationBar.SetLocation(autocompleteInfo ?? ""); } else if ((resolveType == .Autocomplete) || (resolveType == .GetFixits)) { diff --git a/IDE/src/ui/StatusBar.bf b/IDE/src/ui/StatusBar.bf index ebd369c6..f3db2678 100644 --- a/IDE/src/ui/StatusBar.bf +++ b/IDE/src/ui/StatusBar.bf @@ -22,7 +22,7 @@ namespace IDE.ui public DarkButton mSafeModeButton; public bool mWasCompiling; public int mEvalCount; - public ImageWidget mCancelSymSrvButton; + public ImageWidget mCancelButton; public int mDirtyDelay; public int mStatusBoxUpdateCnt = -1; @@ -117,8 +117,8 @@ namespace IDE.ui mConfigComboBox.Resize(mWidth - btnLeft, GS!(0), GS!(120), GS!(24)); mPlatformComboBox.Resize(mWidth - btnLeft - GS!(120), GS!(0), GS!(120), GS!(24)); - if (mCancelSymSrvButton != null) - mCancelSymSrvButton.Resize(GS!(546), 0, GS!(20), GS!(20)); + if (mCancelButton != null) + mCancelButton.Resize(GS!(546), 0, GS!(20), GS!(20)); if (mSafeModeButton != null) { @@ -182,19 +182,31 @@ namespace IDE.ui else mEvalCount = 0; + void ShowCancelButton() + { + if (mCancelButton == null) + { + mCancelButton = new ImageWidget(); + mCancelButton.mImage = DarkTheme.sDarkTheme.GetImage(.Close); + mCancelButton.mOverImage = DarkTheme.sDarkTheme.GetImage(.CloseOver); + mCancelButton.mOnMouseClick.Add(new (evt) => + { + if (gApp.mWorkspace.mProjectLoadState == .Preparing) + { + gApp.CancelWorkspaceLoading(); + } + else + gApp.mDebugger.CancelSymSrv(); + }); + AddWidget(mCancelButton); + ResizeComponents(); + } + } + if (debugState == .SearchingSymSrv) { MarkDirtyEx(); - - if (mCancelSymSrvButton == null) - { - mCancelSymSrvButton = new ImageWidget(); - mCancelSymSrvButton.mImage = DarkTheme.sDarkTheme.GetImage(.Close); - mCancelSymSrvButton.mOverImage = DarkTheme.sDarkTheme.GetImage(.CloseOver); - mCancelSymSrvButton.mOnMouseClick.Add(new (evt) => { gApp.mDebugger.CancelSymSrv(); }); - AddWidget(mCancelSymSrvButton); - ResizeComponents(); - } + ShowCancelButton(); float len = GS!(200); float x = GS!(350); @@ -209,15 +221,45 @@ namespace IDE.ui } } } + else if (gApp.mWorkspace.mProjectLoadState == .Preparing) + { + MarkDirtyEx(); + ShowCancelButton(); + + float len = GS!(200); + float x = GS!(350); + Rect completionRect = Rect(x, GS!(1), len, GS!(17)); + + String status = scope .(); + + for (var workItem in gApp.mPackMan.mWorkItems) + { + if (workItem.mGitInstance == null) + break; + + //DrawCompletion(workItem.mGitInstance.mProgress); + status.AppendF($"Retrieving {workItem.mProjectName}: {(int)(workItem.mGitInstance.mProgress * 100)}%"); + } + + Point mousePos; + if (DarkTooltipManager.CheckMouseover(this, 25, out mousePos, true)) + { + if (completionRect.Contains(mousePos.x, mousePos.y)) + { + if (!status.IsEmpty) + DarkTooltipManager.ShowTooltip(status, this, mousePos.x, mousePos.y); + } + } + } else { if ((DarkTooltipManager.sTooltip != null) && (DarkTooltipManager.sTooltip.mRelWidget == this)) DarkTooltipManager.sTooltip.Close(); - if (mCancelSymSrvButton != null) + if (mCancelButton != null) { - RemoveAndDelete(mCancelSymSrvButton); - mCancelSymSrvButton = null; + RemoveAndDelete(mCancelButton); + mCancelButton = null; } } @@ -367,6 +409,16 @@ namespace IDE.ui float statusLabelPos = (int)GS!(-1.3f); + void DrawCompletion(float pct) + { + Rect completionRect = Rect(GS!(200), GS!(2), GS!(120), GS!(15)); + using (g.PushColor(0xFF000000)) + g.FillRect(completionRect.mX, completionRect.mY, completionRect.mWidth, completionRect.mHeight); + completionRect.Inflate(GS!(-1), GS!(-1)); + using (g.PushColor(0xFF00FF00)) + g.FillRect(completionRect.mX, completionRect.mY, completionRect.mWidth * pct, completionRect.mHeight); + } + //completionPct = 0.4f; if ((gApp.mDebugger?.mIsComptimeDebug == true) && ((gApp.mDebugger.IsPaused()) || (debugState == .DebugEval))) @@ -375,12 +427,7 @@ namespace IDE.ui } else if (completionPct.HasValue) { - Rect completionRect = Rect(GS!(200), GS!(2), GS!(120), GS!(15)); - using (g.PushColor(0xFF000000)) - g.FillRect(completionRect.mX, completionRect.mY, completionRect.mWidth, completionRect.mHeight); - completionRect.Inflate(GS!(-1), GS!(-1)); - using (g.PushColor(0xFF00FF00)) - g.FillRect(completionRect.mX, completionRect.mY, completionRect.mWidth * completionPct.Value, completionRect.mHeight); + DrawCompletion(completionPct.Value); } else if ((gApp.mDebugger.mIsRunning) && (gApp.HaveSourcesChanged())) { @@ -394,7 +441,7 @@ namespace IDE.ui g.DrawString("Source Changed", GS!(200), statusLabelPos, FontAlign.Centered, GS!(120)); } - void DrawStatusBox(StringView str, int32 updateCnt = -1) + void DrawStatusBox(StringView str, int32 updateCnt = -1, bool showCancelButton = false) { if (mStatusBoxUpdateCnt == -1) mStatusBoxUpdateCnt = 0; @@ -415,8 +462,18 @@ namespace IDE.ui using (g.PushColor(Color.FromHSV(0.1f, 0.5f, (float)Math.Max(pulsePct * 0.15f + 0.3f, 0.3f)))) g.FillRect(completionRect.mX, completionRect.mY, completionRect.mWidth, completionRect.mHeight); - if (mCancelSymSrvButton != null) - mCancelSymSrvButton.mX = completionRect.Right - GS!(16); + if (mCancelButton != null) + { + if (showCancelButton) + { + mCancelButton.SetVisible(true); + mCancelButton.mX = completionRect.Right - GS!(16); + } + else + { + mCancelButton.SetVisible(false); + } + } using (g.PushColor(DarkTheme.COLOR_TEXT)) g.DrawString(str, x, statusLabelPos, FontAlign.Centered, len); @@ -429,10 +486,6 @@ namespace IDE.ui chordState.Append(", ..."); DrawStatusBox(chordState); } - else if (mCancelSymSrvButton != null) - { - DrawStatusBox("Retrieving Debug Symbols... "); - } else if (mEvalCount > 20) { DrawStatusBox("Evaluating Expression"); @@ -451,10 +504,16 @@ namespace IDE.ui } else if (gApp.mWorkspace.mProjectLoadState == .Preparing) { - DrawStatusBox("Loading Projects"); + DrawStatusBox("Loading Projects", -1, true); + } + else if (mCancelButton != null) + { + DrawStatusBox("Retrieving Debug Symbols... ", -1, true); } else if (gApp.mDeferredShowSource != null) + { DrawStatusBox("Queued Showing Source"); + } else mStatusBoxUpdateCnt = -1; diff --git a/IDE/src/ui/WorkspaceProperties.bf b/IDE/src/ui/WorkspaceProperties.bf index 639dc1b9..6438bb7d 100644 --- a/IDE/src/ui/WorkspaceProperties.bf +++ b/IDE/src/ui/WorkspaceProperties.bf @@ -11,6 +11,7 @@ using Beefy.theme.dark; using Beefy.theme; using Beefy.events; using System.Diagnostics; +using IDE.Util; //#define A //#define B @@ -40,6 +41,7 @@ namespace IDE.ui enum CategoryType { General, + Dependencies, Beef_Global, Targeted, @@ -53,6 +55,7 @@ namespace IDE.ui ConfigDataGroup mCurConfigDataGroup; Workspace.Options[] mCurWorkspaceOptions ~ delete _; + List mUpdateProjectLocks = new .() ~ DeleteContainerAndItems!(_); public this() { @@ -62,8 +65,9 @@ namespace IDE.ui var root = (DarkListViewItem)mCategorySelector.GetRoot(); var globalItem = AddCategoryItem(root, "General"); - var item = AddCategoryItem(globalItem, "Beef"); + var item = AddCategoryItem(globalItem, "Dependencies"); item.Focused = true; + AddCategoryItem(globalItem, "Beef"); globalItem.Open(true, true); var targetedItem = AddCategoryItem(root, "Targeted"); @@ -124,6 +128,7 @@ namespace IDE.ui { case .General, //.Targeted, + .Dependencies, .Beef_Global: return .None; default: @@ -454,7 +459,9 @@ namespace IDE.ui mPropPage.mPropertiesListView.mShowColumnGrid = true; mPropPage.mPropertiesListView.mShowGridLines = true; - if (categoryType == CategoryType.Beef_Global) + if (categoryType == CategoryType.Dependencies) + PopulateDependencyOptions(); + else if (categoryType == CategoryType.Beef_Global) PopulateBeefGlobalOptions(); else if (categoryType == CategoryType.Build) PopulateBuildOptions(); @@ -705,6 +712,230 @@ namespace IDE.ui } } + void PopulateDependencyOptions() + { + mPropPage.mPropertiesListView.mColumns[0].Label = "Project"; + mPropPage.mPropertiesListView.mColumns[0].mMinWidth = GS!(100); + mPropPage.mPropertiesListView.mColumns[0].mWidth = GS!(180); + + mPropPage.mPropertiesListView.mColumns[1].Label = ""; + mPropPage.mPropertiesListView.mColumns[1].mMinWidth = GS!(20); + mPropPage.mPropertiesListView.mColumns[1].mWidth = GS!(20); + + mPropPage.mPropertiesListView.AddColumn(180, "Remote URL"); + mPropPage.mPropertiesListView.mColumns[2].mMinWidth = GS!(100); + + mPropPage.mPropertiesListView.AddColumn(180, "Ver Constraint"); + mPropPage.mPropertiesListView.mColumns[3].mMinWidth = GS!(100); + + //mDependencyValuesMap = new .(); + + var root = (DarkListViewItem)mPropPage.mPropertiesListView.GetRoot(); + var category = root; + + List projectNames = scope List(); + for (int32 projectIdx = 0; projectIdx < IDEApp.sApp.mWorkspace.mProjects.Count; projectIdx++) + { + var project = IDEApp.sApp.mWorkspace.mProjects[projectIdx]; + /*if (project == mProject) + continue;*/ + projectNames.Add(project.mProjectName); + } + + /*for (var dep in mProject.mDependencies) + { + if (!projectNames.Contains(dep.mProjectName)) + projectNames.Add(dep.mProjectName); + }*/ + + + projectNames.Sort(scope (a, b) => String.Compare(a, b, true)); + + for (var projectName in projectNames) + { + var dependencyEntry = new DependencyEntry(); + + for (var projectSpec in gApp.mWorkspace.mProjectSpecs) + { + if (projectSpec.mProjectName == projectName) + { + dependencyEntry.mUse = true; + if (projectSpec.mVerSpec case .Git(let url, let ver)) + { + dependencyEntry.mURL = new .(url); + if (ver != null) + dependencyEntry.mVersion = new .(ver.mVersion); + } + } + } + + /*var verSpec = mProject.GetDependency(projectName, false); + if (verSpec != null) + { + dependencyEntry.mUse = true; + if (verSpec case .Git(let url, let ver)) + { + dependencyEntry.mURL = new .(url); + if (ver != null) + dependencyEntry.mVersion = new .(ver.mVersion); + } + } + mDependencyValuesMap[new String(projectName)] = dependencyEntry;*/ + + var (listViewItem, propItem) = AddPropertiesItem(category, projectName); + if (IDEApp.sApp.mWorkspace.FindProject(projectName) == null) + listViewItem.mTextColor = Color.Mult(DarkTheme.COLOR_TEXT, 0xFFFF6060); + + var subItem = (DarkListViewItem)listViewItem.CreateSubItem(1); + + var checkbox = new DarkCheckBox(); + checkbox.Checked = dependencyEntry.mUse; + checkbox.Resize(0, 0, DarkTheme.sUnitSize, DarkTheme.sUnitSize); + subItem.AddWidget(checkbox); + + PropEntry[] propEntries = new PropEntry[1]; + + PropEntry propEntry = new PropEntry(); + propEntry.mTarget = dependencyEntry; + propEntry.mOrigValue = Variant.Create(dependencyEntry, true); + propEntry.mCurValue = Variant.Create(new DependencyEntry(dependencyEntry), true); + + propEntry.mListViewItem = listViewItem; + propEntry.mCheckBox = checkbox; + propEntry.mApplyAction = new () => + { + bool updateProjectLock = false; + + var dependencyEntry = propEntry.mCurValue.Get(); + + VerSpec verSpec = default; + if (dependencyEntry.mUse) + { + if (dependencyEntry.mURL != null) + verSpec = .Git(new .(dependencyEntry.mURL), (dependencyEntry.mVersion != null) ? new .(dependencyEntry.mVersion) : null); + else if (dependencyEntry.mVersion != null) + verSpec = .SemVer(new .(dependencyEntry.mVersion)); + else + verSpec = .SemVer(new .("*")); + } + + FindBlock: do + { + for (var projectSpec in gApp.mWorkspace.mProjectSpecs) + { + if (projectSpec.mProjectName == projectName) + { + if (!dependencyEntry.mUse) + { + if (projectSpec.mVerSpec case .Git) + updateProjectLock = true; + @projectSpec.Remove(); + delete projectSpec; + break FindBlock; + } + + if (projectSpec.mVerSpec != verSpec) + { + if ((projectSpec.mVerSpec case .Git) || + (verSpec case .Git)) + updateProjectLock = true; + } + + projectSpec.mVerSpec.Dispose(); + projectSpec.mVerSpec = verSpec; + break FindBlock; + } + } + + if (dependencyEntry.mUse) + { + Workspace.ProjectSpec projectSpec = new .(); + projectSpec.mProjectName = new .(projectName); + projectSpec.mVerSpec = verSpec; + gApp.mWorkspace.mProjectSpecs.Add(projectSpec); + if (verSpec case .Git) + updateProjectLock = true; + var origDependencyEntry = propEntry.mOrigValue.Get(); + origDependencyEntry.Set(dependencyEntry); + } + } + + if (updateProjectLock) + mUpdateProjectLocks.Add(new .(listViewItem.Label)); + }; + + checkbox.mOnMouseUp.Add(new (evt) => + { + var dependencyEntry = propEntry.mCurValue.Get(); + dependencyEntry.mUse = !dependencyEntry.mUse; + if (dependencyEntry.mUse) + { + var projectName = listViewItem.Label; + + for (var projectSpec in gApp.mWorkspace.mProjectSpecs) + { + if (projectSpec.mProjectName == projectName) + { + if (projectSpec.mVerSpec case .Git(let url, let ver)) + { + dependencyEntry.SetValue(1, url); + dependencyEntry.SetValue(2, ver.mVersion); + } + } + } + var propEntries = mPropPage.mPropEntries[listViewItem]; + UpdatePropertyValue(propEntries); + } + else + { + DeleteAndNullify!(dependencyEntry.mURL); + DeleteAndNullify!(dependencyEntry.mVersion); + var propEntries = mPropPage.mPropEntries[listViewItem]; + UpdatePropertyValue(propEntries); + } + + }); + + + subItem = (.)listViewItem.GetOrCreateSubItem(2); + if (dependencyEntry.mURL != null) + subItem.Label = dependencyEntry.mURL; + subItem.mOnMouseDown.Add(new => DepPropValueClicked); + + subItem = (.)listViewItem.GetOrCreateSubItem(3); + if (dependencyEntry.mVersion != null) + subItem.Label = dependencyEntry.mVersion; + subItem.mOnMouseDown.Add(new => DepPropValueClicked); + + propEntries[0] = propEntry; + mPropPage.mPropEntries[listViewItem] = propEntries; + } + } + + protected void DepPropValueClicked(MouseEvent theEvent) + { + DarkListViewItem clickedItem = (DarkListViewItem)theEvent.mSender; + if (clickedItem.mColumnIdx == 0) + { + clickedItem.mListView.SetFocus(); + clickedItem.mListView.GetRoot().SelectItemExclusively(clickedItem); + return; + } + + if (theEvent.mX != -1) + { + clickedItem.mListView.GetRoot().SelectItemExclusively(null); + } + + DarkListViewItem item = (DarkListViewItem)clickedItem; + DarkListViewItem rootItem = (DarkListViewItem)clickedItem.GetSubItem(0); + + PropEntry[] propertyEntries = mPropPage.mPropEntries[rootItem]; + if (propertyEntries[0].mDisabled) + return; + EditValue(item, propertyEntries, clickedItem.mColumnIdx - 1); + } + void PopulateBeefGlobalOptions() { var root = (DarkListViewItem)mPropPage.mPropertiesListView.GetRoot(); @@ -939,6 +1170,7 @@ namespace IDE.ui { base.Close(); SetWorkspaceData(false); + gApp.NotifyProjectVersionLocks(mUpdateProjectLocks); } public override void CalcSize() diff --git a/IDE/src/util/GitManager.bf b/IDE/src/util/GitManager.bf new file mode 100644 index 00000000..2fec1884 --- /dev/null +++ b/IDE/src/util/GitManager.bf @@ -0,0 +1,326 @@ +#pragma warning disable 168 + +using System.Diagnostics; +using System; +using System.Threading; +using System.IO; +using System.Collections; + +namespace IDE.util; + +class GitManager +{ + public enum Error + { + Unknown + } + + public class GitInstance : RefCounted + { + public class TagInfo + { + public String mHash ~ delete _; + public String mTag ~ delete _; + } + + public GitManager mGitManager; + public bool mFailed; + public bool mDone; + public bool mStarted; + public bool mRemoved; + + public String mArgs ~ delete _; + public String mPath ~ delete _; + public float mProgress; + public float mProgressRecv; + public float mProgressDeltas; + public float mProgressFiles; + + public Stopwatch mStopwatch = new .()..Start() ~ delete _; + + public SpawnedProcess mProcess ~ delete _; + public Monitor mMonitor = new .() ~ delete _; + public List mDeferredOutput = new .() ~ DeleteContainerAndItems!(_); + public List mTagInfos = new .() ~ DeleteContainerAndItems!(_); + + public Thread mOutputThread ~ delete _; + public Thread mErrorThread ~ delete _; + + public this(GitManager gitManager) + { + mGitManager = gitManager; + } + + public ~this() + { + IDEUtils.SafeKill(mProcess); + mOutputThread?.Join(); + mErrorThread?.Join(); + + if (!mRemoved) + mGitManager.mGitInstances.Remove(this); + } + + public void Init(StringView args, StringView path) + { + mArgs = new .(args); + if (path != default) + mPath = new .(path); + } + + public void Start() + { + if (mStarted) + return; + mStarted = true; + + ProcessStartInfo psi = scope ProcessStartInfo(); + + String gitPath = scope .(); +#if BF_PLATFORM_WINDOWS + Path.GetAbsolutePath(gApp.mInstallDir, "git/cmd/git.exe", gitPath); + if (!File.Exists(gitPath)) + gitPath.Clear(); + + if (gitPath.IsEmpty) + { + Path.GetAbsolutePath(gApp.mInstallDir, "../../bin/git/cmd/git.exe", gitPath); + if (!File.Exists(gitPath)) + gitPath.Clear(); + } + + if (gitPath.IsEmpty) + { + Path.GetAbsolutePath(gApp.mInstallDir, "../../../bin/git/cmd/git.exe", gitPath); + if (!File.Exists(gitPath)) + gitPath.Clear(); + } +#endif + if (gitPath.IsEmpty) + gitPath.Set("git"); + + psi.SetFileName(gitPath); + psi.SetArguments(mArgs); + if (mPath != null) + psi.SetWorkingDirectory(mPath); + psi.UseShellExecute = false; + psi.RedirectStandardError = true; + psi.RedirectStandardOutput = true; + psi.CreateNoWindow = true; + + mProcess = new SpawnedProcess(); + if (mProcess.Start(psi) case .Err) + { + gApp.OutputErrorLine("Failed to execute Git"); + mFailed = true; + return; + } + + mOutputThread = new Thread(new => ReadOutputThread); + mOutputThread.Start(false); + + mErrorThread = new Thread(new => ReadErrorThread); + mErrorThread.Start(false); + } + + public void ReadOutputThread() + { + FileStream fileStream = scope FileStream(); + if (mProcess.AttachStandardOutput(fileStream) case .Err) + return; + StreamReader streamReader = scope StreamReader(fileStream, null, false, 4096); + + int count = 0; + while (true) + { + count++; + var buffer = scope String(); + if (streamReader.ReadLine(buffer) case .Err) + break; + using (mMonitor.Enter()) + { + mDeferredOutput.Add(new .(buffer)); + } + } + } + + public void ReadErrorThread() + { + FileStream fileStream = scope FileStream(); + if (mProcess.AttachStandardError(fileStream) case .Err) + return; + StreamReader streamReader = scope StreamReader(fileStream, null, false, 4096); + + while (true) + { + var buffer = scope String(); + if (streamReader.ReadLine(buffer) case .Err) + break; + + using (mMonitor.Enter()) + { + //mDeferredOutput.Add(new $"{mStopwatch.ElapsedMilliseconds / 1000.0:0.0}: {buffer}"); + mDeferredOutput.Add(new .(buffer)); + } + } + } + + public void Update() + { + using (mMonitor.Enter()) + { + while (!mDeferredOutput.IsEmpty) + { + var line = mDeferredOutput.PopFront(); + defer delete line; + //Debug.WriteLine($"GIT: {line}"); + + if (line.StartsWith("Cloning into ")) + { + // May be starting a submodule + mProgressRecv = 0; + mProgressDeltas = 0; + mProgressFiles = 0; + } + + if (line.StartsWith("remote: Counting objects")) + { + mProgressRecv = 0.001f; + } + + if (line.StartsWith("Receiving objects: ")) + { + var pctStr = line.Substring("Receiving objects: ".Length, 3)..Trim(); + mProgressRecv = float.Parse(pctStr).GetValueOrDefault() / 100.0f; + } + + if (line.StartsWith("Resolving deltas: ")) + { + var pctStr = line.Substring("Resolving deltas: ".Length, 3)..Trim(); + mProgressDeltas = float.Parse(pctStr).GetValueOrDefault() / 100.0f; + mProgressRecv = 1.0f; + } + + if (line.StartsWith("Updating files: ")) + { + var pctStr = line.Substring("Updating files: ".Length, 3)..Trim(); + mProgressFiles = float.Parse(pctStr).GetValueOrDefault() / 100.0f; + mProgressRecv = 1.0f; + mProgressDeltas = 1.0f; + } + + StringView version = default; + + int refTagIdx = line.IndexOf("\trefs/tags/"); + if (refTagIdx == 40) + version = line.Substring(40 + "\trefs/tags/".Length); + + if ((line.Length == 45) && (line.EndsWith("HEAD"))) + version = "HEAD"; + + if (!version.IsEmpty) + { + TagInfo tagInfo = new .(); + tagInfo.mHash = new .(line, 0, 40); + tagInfo.mTag = new .(version); + mTagInfos.Add(tagInfo); + } + } + } + + float pct = 0; + if (mProgressRecv > 0) + pct = 0.1f + (mProgressRecv * 0.3f) + (mProgressDeltas * 0.4f) + (mProgressFiles * 0.2f); + + if (pct > mProgress) + { + mProgress = pct; + //Debug.WriteLine($"Completed Pct: {pct}"); + } + + if (mProcess.WaitFor(0)) + { + if (mProcess.ExitCode != 0) + mFailed = true; + mDone = true; + } + } + + public void Cancel() + { + if (!mProcess.WaitFor(0)) + { + //Debug.WriteLine($"GitManager Cancel {mProcess.ProcessId}"); + IDEUtils.SafeKill(mProcess); + } + } + } + + public const int sMaxActiveGitInstances = 4; + + public List mGitInstances = new .() ~ + { + for (var gitInstance in _) + gitInstance.ReleaseRef(); + delete _; + }; + + public void Init() + { + //StartGit("-v"); + + //Repository repository = Clone("https://github.com/llvm/llvm-project", "c:/temp/__LLVM"); + + //Repository repository = Clone("https://github.com/Starpelly/raylib-beef", "c:/temp/__RAYLIB"); + /*while (true) + { + Thread.Sleep(500); + Debug.WriteLine($"Repository {repository.mStatus} {repository.GetCompletedPct()}"); + }*/ + } + + public GitInstance StartGit(StringView cmd, StringView path = default) + { + //Debug.WriteLine($"GIT STARTING: {cmd} in {path}"); + + GitInstance gitInst = new .(this); + gitInst.Init(cmd, path); + mGitInstances.Add(gitInst); + return gitInst; + } + + public GitInstance Clone(StringView url, StringView path) + { + return StartGit(scope $"clone -v --progress --recurse-submodules {url} \"{path}\""); + } + + public GitInstance Checkout(StringView path, StringView hash) + { + return StartGit(scope $"checkout -b BeefManaged {hash}", path); + } + + public GitInstance GetTags(StringView url) + { + return StartGit(scope $"ls-remote {url}"); + } + + public void Update() + { + for (var gitInstance in mGitInstances) + { + if (@gitInstance.Index >= sMaxActiveGitInstances) + break; + + if (!gitInstance.mStarted) + gitInstance.Start(); + gitInstance.Update(); + + if (gitInstance.mDone) + { + @gitInstance.Remove(); + gitInstance.mRemoved = true; + gitInstance.ReleaseRef(); + } + } + } +} \ No newline at end of file diff --git a/IDE/src/util/PackMan.bf b/IDE/src/util/PackMan.bf index 94a58523..ab151543 100644 --- a/IDE/src/util/PackMan.bf +++ b/IDE/src/util/PackMan.bf @@ -1,35 +1,397 @@ +#pragma warning disable 168 + using System; using IDE.Util; - -#if BF_PLATFORM_WINDOWS -using static Git.GitApi; -#define SUPPORT_GIT -#endif +using System.Collections; +using System.Security.Cryptography; +using System.IO; +using Beefy.utils; +using System.Threading; namespace IDE.util { class PackMan { - class GitHelper + public class WorkItem { - static bool sInitialized; - - public this() + public enum Kind { - if (!sInitialized) - { -#if SUPPORT_GIT -#unwarn - var result = git_libgit2_init(); - sInitialized = true; -#endif - } + None, + FindVersion, + Clone, + Checkout } + + public Kind mKind; + public String mProjectName ~ delete _; + public String mURL ~ delete _; + public List mConstraints ~ DeleteContainerAndItems!(_); + public String mTag ~ delete _; + public String mHash ~ delete _; + public String mPath ~ delete _; + public GitManager.GitInstance mGitInstance ~ _?.ReleaseRef(); + + public ~this() + { + mGitInstance?.Cancel(); + } + } + + public List mWorkItems = new .() ~ DeleteContainerAndItems!(_); + public bool mInitialized; + public String mManagedPath ~ delete _; + public bool mFailed; + + public void Fail(StringView error) + { + gApp.OutputErrorLine(error); + + if (!mFailed) + { + mFailed = true; + gApp.[Friend]FlushDeferredLoadProjects(); + } + } + + public bool CheckInit() + { + if (mInitialized) + return true; + + if (gApp.mBeefConfig.mManagedLibPath.IsEmpty) + return false; + + mManagedPath = new .(gApp.mBeefConfig.mManagedLibPath); + mInitialized = true; + return true; + } + + public void GetPath(StringView url, StringView hash, String outPath) + { + //var urlHash = SHA256.Hash(url.ToRawData()).ToString(.. scope .()); + //outPath.AppendF($"{mManagedPath}/{urlHash}/{hash}"); + outPath.AppendF($"{mManagedPath}/{hash}"); } public bool CheckLock(StringView projectName, String outPath) { + if (!CheckInit()) + return false; + + if (gApp.mWantUpdateVersionLocks != null) + { + if ((gApp.mWantUpdateVersionLocks.IsEmpty) || (gApp.mWantUpdateVersionLocks.ContainsAlt(projectName))) + return false; + } + + if (!gApp.mWorkspace.mProjectLockMap.TryGetAlt(projectName, ?, var lock)) + return false; + + switch (lock) + { + case .Git(let url, let tag, let hash): + var path = GetPath(url, hash, .. scope .()); + var managedFilePath = scope $"{path}/BeefManaged.toml"; + if (File.Exists(managedFilePath)) + { + outPath.Append(path); + outPath.Append("/BeefProj.toml"); + return true; + } + default: + } + return false; } + + public void CloneCompleted(StringView projectName, StringView url, StringView tag, StringView hash, StringView path) + { + gApp.mWorkspace.SetLock(projectName, .Git(new .(url), new .(tag), new .(hash))); + + StructuredData sd = scope .(); + sd.CreateNew(); + sd.Add("FileVersion", 1); + sd.Add("Version", tag); + sd.Add("GitURL", url); + sd.Add("GitTag", tag); + sd.Add("GitHash", hash); + var tomlText = sd.ToTOML(.. scope .()); + var managedFilePath = scope $"{path}/BeefManaged.toml"; + File.WriteAllText(managedFilePath, tomlText).IgnoreError(); + } + + public void GetWithHash(StringView projectName, StringView url, StringView tag, StringView hash) + { + if (!CheckInit()) + return; + + String destPath = GetPath(url, hash, .. scope .()); + var urlPath = Path.GetDirectoryPath(destPath, .. scope .()); + Directory.CreateDirectory(urlPath).IgnoreError(); + if (Directory.Exists(destPath)) + { + var managedFilePath = scope $"{destPath}/BeefManaged.toml"; + if (File.Exists(managedFilePath)) + { + if (gApp.mVerbosity >= .Normal) + { + if (tag.IsEmpty) + gApp.OutputLine($"Git selecting library '{projectName}' at {hash.Substring(0, 7)}"); + else + gApp.OutputLine($"Git selecting library '{projectName}' tag '{tag}' at {hash.Substring(0, 7)}"); + } + + CloneCompleted(projectName, url, tag, hash, destPath); + ProjectReady(projectName, destPath); + return; + } + + String tempDir = new $"{destPath}__{(int32)Internal.GetTickCountMicro():X}"; + + //if (Directory.DelTree(destPath) case .Err) + if (Directory.Move(destPath, tempDir) case .Err) + { + delete tempDir; + Fail(scope $"Failed to remove directory '{destPath}'"); + return; + } + + ThreadPool.QueueUserWorkItem(new () => + { + Directory.DelTree(tempDir); + } + ~ + { + delete tempDir; + }); + } + + if (gApp.mVerbosity >= .Normal) + { + if (tag.IsEmpty) + gApp.OutputLine($"Git cloning library '{projectName}' at {hash.Substring(0, 7)}..."); + else + gApp.OutputLine($"Git cloning library '{projectName}' tag '{tag}' at {hash.Substring(0, 7)}"); + } + + WorkItem workItem = new .(); + workItem.mKind = .Clone; + workItem.mProjectName = new .(projectName); + workItem.mURL = new .(url); + workItem.mTag = new .(tag); + workItem.mHash = new .(hash); + workItem.mPath = new .(destPath); + mWorkItems.Add(workItem); + } + + public void GetWithVersion(StringView projectName, StringView url, SemVer semVer) + { + if (!CheckInit()) + return; + + bool ignoreLock = false; + if (gApp.mWantUpdateVersionLocks != null) + { + if ((gApp.mWantUpdateVersionLocks.IsEmpty) || (gApp.mWantUpdateVersionLocks.ContainsAlt(projectName))) + ignoreLock = true; + } + + if ((!ignoreLock) && (gApp.mWorkspace.mProjectLockMap.TryGetAlt(projectName, ?, var lock))) + { + switch (lock) + { + case .Git(let checkURL, let tag, let hash): + if (checkURL == url) + GetWithHash(projectName, url, tag, hash); + return; + default: + } + } + + if (gApp.mVerbosity >= .Normal) + gApp.OutputLine($"Git retrieving version list for '{projectName}'"); + + WorkItem workItem = new .(); + workItem.mKind = .FindVersion; + workItem.mProjectName = new .(projectName); + workItem.mURL = new .(url); + if (semVer != null) + workItem.mConstraints = new .() { new String(semVer.mVersion) }; + mWorkItems.Add(workItem); + } + + public void UpdateGitConstraint(StringView url, SemVer semVer) + { + for (var workItem in mWorkItems) + { + if ((workItem.mKind == .FindVersion) && (workItem.mURL == url)) + { + if (workItem.mConstraints == null) + workItem.mConstraints = new .(); + workItem.mConstraints.Add(new String(semVer.mVersion)); + } + } + } + + public void Checkout(StringView projectName, StringView url, StringView path, StringView tag, StringView hash) + { + if (!CheckInit()) + return; + + WorkItem workItem = new .(); + workItem.mKind = .Checkout; + workItem.mProjectName = new .(projectName); + workItem.mURL = new .(url); + workItem.mTag = new .(tag); + workItem.mHash = new .(hash); + workItem.mPath = new .(path); + mWorkItems.Add(workItem); + } + + public void ProjectReady(StringView projectName, StringView path) + { + if (var project = gApp.mWorkspace.FindProject(projectName)) + { + String projectPath = scope $"{path}/BeefProj.toml"; + + project.mProjectPath.Set(projectPath); + gApp.RetryProjectLoad(project, false); + } + } + + public void Update() + { + bool executingGit = false; + + // First handle active git items + for (var workItem in mWorkItems) + { + if (workItem.mGitInstance == null) + continue; + + if (!workItem.mGitInstance.mDone) + { + executingGit = true; + continue; + } + + if (!workItem.mGitInstance.mFailed) + { + switch (workItem.mKind) + { + case .FindVersion: + gApp.CompilerLog(""); + + StringView bestTag = default; + StringView bestHash = default; + + for (var tag in workItem.mGitInstance.mTagInfos) + { + if ((tag.mTag == "HEAD") && (workItem.mConstraints == null)) + bestHash = tag.mHash; + else if (workItem.mConstraints != null) + { + bool hasMatch = false; + for (var constraint in workItem.mConstraints) + { + if (SemVer.IsVersionMatch(tag.mTag, constraint)) + { + hasMatch = true; + break; + } + } + + if (hasMatch) + { + if ((bestTag.IsEmpty) || (SemVer.Compare(tag.mTag, bestTag) > 0)) + { + bestTag = tag.mTag; + bestHash = tag.mHash; + } + } + } + } + + if (bestHash != default) + { + GetWithHash(workItem.mProjectName, workItem.mURL, bestTag, bestHash); + } + else + { + String constraints = scope .(); + for (var constraint in workItem.mConstraints) + { + if (!constraints.IsEmpty) + constraints.Append(", "); + constraints.Append('\''); + constraints.Append(constraint); + constraints.Append('\''); + } + + Fail(scope $"Failed to locate version for '{workItem.mProjectName}' with constraints '{constraints}'"); + } + case .Clone: + Checkout(workItem.mProjectName, workItem.mURL, workItem.mPath, workItem.mTag, workItem.mHash); + case .Checkout: + CloneCompleted(workItem.mProjectName, workItem.mURL, workItem.mTag, workItem.mHash, workItem.mPath); + ProjectReady(workItem.mProjectName, workItem.mPath); + + if (gApp.mVerbosity >= .Normal) + gApp.OutputLine($"Git cloning library '{workItem.mProjectName}' done."); + default: + } + } + + @workItem.Remove(); + delete workItem; + } + + if (!executingGit) + { + // First handle active git items + for (var workItem in mWorkItems) + { + if (workItem.mGitInstance != null) + continue; + + switch (workItem.mKind) + { + case .FindVersion: + workItem.mGitInstance = gApp.mGitManager.GetTags(workItem.mURL)..AddRef(); + case .Checkout: + workItem.mGitInstance = gApp.mGitManager.Checkout(workItem.mPath, workItem.mHash)..AddRef(); + case .Clone: + workItem.mGitInstance = gApp.mGitManager.Clone(workItem.mURL, workItem.mPath)..AddRef(); + default: + } + } + } + } + + public void GetHashFromFilePath(StringView filePath, String path) + { + if (mManagedPath == null) + return; + + if (!filePath.StartsWith(mManagedPath)) + return; + + StringView hashPart = filePath.Substring(mManagedPath.Length); + if (hashPart.Length < 42) + return; + + hashPart.RemoveFromStart(1); + hashPart.Length = 40; + path.Append(hashPart); + } + + public void CancelAll() + { + if (mWorkItems.IsEmpty) + return; + + Fail("Aborted project transfer"); + mWorkItems.ClearAndDeleteItems(); + } } } diff --git a/IDE/src/util/SemVer.bf b/IDE/src/util/SemVer.bf index c7632ef0..d319b94d 100644 --- a/IDE/src/util/SemVer.bf +++ b/IDE/src/util/SemVer.bf @@ -2,10 +2,51 @@ using System; namespace IDE.Util { + [Reflect] class SemVer { + public struct Parts + { + public enum Kind + { + case Empty; + case Num(int32 val); + case Wild; + + public int32 NumOrDefault + { + get + { + if (this case .Num(let val)) + return val; + return 0; + } + } + } + + public Kind[3] mPart; + public StringView mPreRelease; + + public Kind Major => mPart[0]; + public Kind Minor => mPart[1]; + public Kind Patch => mPart[2]; + } + + enum CompareKind + { + Caret, // Default + Tilde, + Equal, + Gt, + Gte, + Lt, + Lte + } + public String mVersion ~ delete _; + public bool IsEmpty => String.IsNullOrEmpty(mVersion); + public this() { @@ -27,5 +68,240 @@ namespace IDE.Util mVersion = new String(ver); return .Ok; } + + public static Result GetParts(StringView version) + { + int startIdx = 0; + int partIdx = 0; + + if (version.IsEmpty) + return .Err; + + if (version.StartsWith("V", .OrdinalIgnoreCase)) + startIdx++; + + Parts parts = .(); + + Result SetPart(Parts.Kind kind) + { + if (partIdx >= 3) + return .Err; + parts.mPart[partIdx] = kind; + partIdx++; + return .Ok; + } + + Result FlushPart(int i) + { + StringView partStr = version.Substring(startIdx, i - startIdx); + if (!partStr.IsEmpty) + { + int32 partNum = Try!(int32.Parse(partStr)); + Try!(SetPart(.Num(partNum))); + } + return .Ok; + } + + for (int i in startIdx ..< version.Length) + { + char8 c = version[i]; + if (c.IsWhiteSpace) + return .Err; + + if (c == '.') + { + Try!(FlushPart(i)); + startIdx = i + 1; + continue; + } + else if (c.IsNumber) + { + continue; + } + else if (c == '-') + { + if (partIdx == 0) + return .Err; + parts.mPreRelease = version.Substring(i); + return .Ok(parts); + } + else if (c == '*') + { + Try!(SetPart(.Wild)); + continue; + } + + return .Err; + } + Try!(FlushPart(version.Length)); + + return parts; + } + + public Result GetParts() + { + return GetParts(mVersion); + } + + public static bool IsVersionMatch(StringView fullVersion, StringView wildcard) + { + int commaPos = wildcard.IndexOf(','); + if (commaPos != -1) + return IsVersionMatch(fullVersion, wildcard.Substring(0, commaPos)..Trim()) && IsVersionMatch(fullVersion, wildcard.Substring(commaPos + 1)..Trim()); + + var wildcard; + + wildcard.Trim(); + CompareKind compareKind = .Caret; + if (wildcard.StartsWith('^')) + { + compareKind = .Caret; + wildcard.RemoveFromStart(1); + } + else if (wildcard.StartsWith('~')) + { + compareKind = .Tilde; + wildcard.RemoveFromStart(1); + } + else if (wildcard.StartsWith('=')) + { + compareKind = .Equal; + wildcard.RemoveFromStart(1); + } + else if (wildcard.StartsWith('>')) + { + compareKind = .Gt; + wildcard.RemoveFromStart(1); + if (wildcard.StartsWith('=')) + { + compareKind = .Gte; + wildcard.RemoveFromStart(1); + } + } + else if (wildcard.StartsWith('<')) + { + compareKind = .Lt; + wildcard.RemoveFromStart(1); + if (wildcard.StartsWith('=')) + { + compareKind = .Lte; + wildcard.RemoveFromStart(1); + } + } + wildcard.Trim(); + + // Does we include equality? + if ((compareKind != .Gt) && (compareKind != .Lt)) + { + if (fullVersion == wildcard) + return true; + } + + Parts full; + if (!(GetParts(fullVersion) case .Ok(out full))) + return false; + Parts wild; + if (!(GetParts(wildcard) case .Ok(out wild))) + return false; + + // Don't allow a general wildcard to match a pre-prelease + if ((!full.mPreRelease.IsEmpty) && (full.mPreRelease != wild.mPreRelease)) + return false; + + for (int partIdx < 3) + { + if (wild.mPart[partIdx] case .Wild) + return true; + int comp = full.mPart[partIdx].NumOrDefault <=> wild.mPart[partIdx].NumOrDefault; + switch (compareKind) + { + case .Caret: + if ((full.mPart[partIdx].NumOrDefault > 0) || (wild.mPart[partIdx].NumOrDefault > 0)) + { + if (comp != 0) + return false; + // First number matches, now make sure we are at least a high enough version on the other numbers + compareKind = .Gte; + } + case .Tilde: + if (wild.mPart[partIdx] case .Empty) + return true; + if (partIdx == 2) + { + if (comp < 0) + return false; + } + else if (comp != 0) + return false; + case .Equal: + if (wild.mPart[partIdx] case .Empty) + return true; + if (comp != 0) + return false; + case .Gt: + if (comp > 0) + return true; + if (partIdx == 2) + return false; + if (comp < 0) + return false; + case .Gte: + if (comp < 0) + return false; + case .Lt: + if (comp < 0) + return true; + if (partIdx == 2) + return false; + if (comp > 0) + return false; + case .Lte: + if (comp > 0) + return false; + default: + } + } + + return true; + } + + public static bool IsVersionMatch(SemVer fullVersion, SemVer wildcard) => IsVersionMatch(fullVersion.mVersion, wildcard.mVersion); + + public static Result Compare(StringView lhs, StringView rhs) + { + Parts lhsParts; + if (!(GetParts(lhs) case .Ok(out lhsParts))) + return .Err; + Parts rhsParts; + if (!(GetParts(rhs) case .Ok(out rhsParts))) + return .Err; + + int comp = 0; + for (int partIdx < 3) + { + comp = lhsParts.mPart[partIdx].NumOrDefault <=> rhsParts.mPart[partIdx].NumOrDefault; + if (comp != 0) + return comp; + } + + // Don't allow a general wildcard to match a pre-prelease + if ((!lhsParts.mPreRelease.IsEmpty) || (!rhsParts.mPreRelease.IsEmpty)) + { + if (lhsParts.mPreRelease.IsEmpty) + return 1; + if (rhsParts.mPreRelease.IsEmpty) + return -1; + return lhsParts.mPreRelease <=> rhsParts.mPreRelease; + } + + return comp; + } + + public override void ToString(String strBuffer) + { + strBuffer.Append(mVersion); + } + + public static int operator<=>(Self lhs, Self rhs) => (lhs?.mVersion ?? "") <=> (rhs?.mVersion ?? ""); } } diff --git a/IDE/src/util/VerSpec.bf b/IDE/src/util/VerSpec.bf index d9d9df2f..7396c134 100644 --- a/IDE/src/util/VerSpec.bf +++ b/IDE/src/util/VerSpec.bf @@ -39,6 +39,8 @@ namespace IDE.Util case .Path(let path): return .Path(new String(path)); case .Git(let url, let ver): + if (ver == null) + return .Git(new String(url), null); return .Git(new String(url), new SemVer(ver)); } } @@ -113,7 +115,7 @@ namespace IDE.Util using (data.CreateObject(name)) { data.Add("Git", path); - if (ver != null) + if ((ver != null) && (!ver.mVersion.IsEmpty)) data.Add("Version", ver.mVersion); } case .SemVer(var ver):