Editor/HotCodeEditor/CodeFileTree.cs
using Sandbox;
using Editor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
/// <summary>
/// Whether a file is open in a tab, and whether it's the one being edited.
/// </summary>
public enum FileState
{
Closed,
Open,
Active
}
/// <summary>
/// A lazily-built tree node for a file or folder on disk. Only shows code/text files
/// and skips build output folders.
/// </summary>
public class CodeFileNode : TreeNode
{
public static readonly HashSet<string> HiddenFolders = new( StringComparer.OrdinalIgnoreCase ) { "bin", "obj", ".vs", ".git", ".idea", ".sbox", "node_modules" };
static readonly HashSet<string> CodeExtensions = new( StringComparer.OrdinalIgnoreCase )
{
".cs", ".razor", ".scss", ".css", ".json", ".sbproj", ".csproj", ".sln", ".slnx",
".md", ".txt", ".shader", ".hlsl", ".editorconfig", ".gitignore", ".xml", ".yml", ".yaml"
};
public FileSystemInfo Info { get; }
public bool IsFolder => Info is DirectoryInfo;
public string FullPath => Info.FullName;
private readonly HotCodeEditor _host;
public CodeFileNode( HotCodeEditor host, FileSystemInfo info ) : base( info.FullName )
{
_host = host;
Info = info;
_host.RegisterNode( this );
}
public override string Name => Info.Name;
public override bool HasChildren => IsFolder;
public override string GetTooltip() => Info.FullName;
public static bool IsCodeFile( string path ) => CodeExtensions.Contains( Path.GetExtension( path ) ) || CodeExtensions.Contains( Path.GetFileName( path ) );
/// <summary>
/// True for paths inside bin/obj/.git etc, which the tree never shows.
/// </summary>
public static bool IsInHiddenFolder( string path ) => path.Split( Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar ).Any( HiddenFolders.Contains );
public override void OnPaint( VirtualWidget item )
{
PaintSelection( item );
var state = IsFolder ? FileState.Closed : _host.StateOf( FullPath );
// Folders in s&box's folder colour; files coloured by type
var open = IsFolder && item.IsOpen;
Paint.SetPen( IsFolder ? Theme.Folder : HotCodeStyle.FileIconColor( FullPath ) );
Paint.DrawIcon( item.Rect, IsFolder ? (open ? "folder_open" : "folder") : HotCodeStyle.FileIcon( FullPath ), 15, TextFlag.LeftCenter );
// The file you're editing is bold; other open files are bright, the rest are dimmer
Paint.SetDefaultFont( HotCodeStyle.FontSize, state == FileState.Active ? 700 : 400 );
Paint.SetPen( IsFolder || state != FileState.Closed || item.Selected ? Theme.Text : Theme.TextLight );
Paint.DrawText( item.Rect.Shrink( 22, 0, 0, 0 ), Info.Name, TextFlag.LeftCenter );
if ( state != FileState.Closed && _host.IsUnsaved( FullPath ) )
{
Paint.ClearPen();
Paint.SetBrush( Theme.Text );
Paint.DrawCircle( new Vector2( item.Rect.Right - 10, item.Rect.Center.y ), 6 );
}
}
public override bool OnContextMenu()
{
_host.ShowFileContextMenu( this );
return true;
}
protected override void BuildChildren()
{
if ( Info is not DirectoryInfo dir )
return;
Clear();
IEnumerable<FileSystemInfo> entries;
try
{
dir.Refresh();
entries = dir.GetFileSystemInfos();
}
catch ( Exception e ) when ( e is IOException or UnauthorizedAccessException )
{
Log.Warning( $"[HotCode] Can't read {dir.FullName}: {e.Message}" );
return;
}
var folders = entries.OfType<DirectoryInfo>()
.Where( x => !HiddenFolders.Contains( x.Name ) && ShouldShowFolder( x ) )
.OrderBy( x => x.Name, StringComparer.OrdinalIgnoreCase );
var files = entries.OfType<FileInfo>()
.Where( x => IsCodeFile( x.Name ) )
.OrderBy( x => x.Name, StringComparer.OrdinalIgnoreCase );
AddItems( folders.Select( x => new CodeFileNode( _host, x ) ) );
AddItems( files.Select( x => new CodeFileNode( _host, x ) ) );
}
/// <summary>
/// Hide folders full of things we'd never open (e.g. Assets full of textures),
/// but keep empty ones so a folder you just made shows up.
/// </summary>
static bool ShouldShowFolder( DirectoryInfo dir )
{
try
{
var any = false;
foreach ( var f in dir.EnumerateFiles( "*", SearchOption.AllDirectories ) )
{
if ( IsInHiddenFolder( f.FullName ) ) continue;
if ( IsCodeFile( f.Name ) ) return true;
any = true;
}
return !any;
}
catch ( Exception e ) when ( e is IOException or UnauthorizedAccessException )
{
return false;
}
}
}