Compare commits

..

1 Commits

Author SHA1 Message Date
Gordon Lam (SH)
db10cf2d83 feat(preview): add Dockerfile preview support
Fixes #32686

File Explorer now shows syntax-highlighted preview for Dockerfiles
using the Monaco editor.

Supported files:
- Dockerfile (and variants like Dockerfile.dev, Dockerfile.prod)
- Containerfile (Podman)
- .dockerfile extension
2026-02-04 20:36:45 -08:00
8 changed files with 212 additions and 11 deletions

File diff suppressed because one or more lines are too long

View File

@@ -101,6 +101,81 @@ namespace Microsoft.PowerToys.FilePreviewCommon
}
}
/// <summary>
/// Converts a filename to a Monaco language ID based on the filenames array.
/// </summary>
/// <param name="fileName">The filename (e.g., "Dockerfile", not full path).</param>
/// <returns>The Monaco language ID, or null if no match found.</returns>
public static string? GetLanguageByFileName(string fileName)
{
if (string.IsNullOrEmpty(fileName))
{
return null;
}
try
{
JsonDocument languageListDocument = GetLanguages();
JsonElement languageList = languageListDocument.RootElement.GetProperty("list");
foreach (JsonElement e in languageList.EnumerateArray())
{
if (e.TryGetProperty("filenames", out var filenames))
{
for (int j = 0; j < filenames.GetArrayLength(); j++)
{
if (string.Equals(filenames[j].GetString(), fileName, StringComparison.OrdinalIgnoreCase))
{
return e.GetProperty("id").GetString() ?? "plaintext";
}
}
}
}
return null;
}
catch (Exception)
{
return null;
}
}
/// <summary>
/// Gets all filenames defined in the Monaco languages configuration.
/// </summary>
/// <returns>A HashSet of supported filenames (case-insensitive comparison recommended).</returns>
public static HashSet<string> GetSupportedFileNames()
{
HashSet<string> set = new(StringComparer.OrdinalIgnoreCase);
try
{
JsonDocument languageListDocument = GetLanguages();
JsonElement languageList = languageListDocument.RootElement.GetProperty("list");
foreach (JsonElement e in languageList.EnumerateArray())
{
if (e.TryGetProperty("filenames", out var filenames))
{
for (int j = 0; j < filenames.GetArrayLength(); j++)
{
var filename = filenames[j].GetString();
if (filename != null)
{
set.Add(filename);
}
}
}
}
}
catch (Exception)
{
// Return empty set on failure
}
return set;
}
public static string ReadIndexHtml()
{
string html;

View File

@@ -1,2 +0,0 @@
// Fix for Issue #32383
namespace PowerToys.Fixes { public class Fix32383 { } }

View File

@@ -17,6 +17,7 @@ namespace Peek.FilePreviewer.Previewers
public class MonacoHelper
{
public static readonly HashSet<string> SupportedMonacoFileTypes = GetExtensions();
public static readonly HashSet<string> SupportedMonacoFileNames = GetFileNames();
public static HashSet<string> GetExtensions()
{
@@ -45,19 +46,56 @@ namespace Peek.FilePreviewer.Previewers
return set;
}
public static HashSet<string> GetFileNames()
{
HashSet<string> set = new(StringComparer.OrdinalIgnoreCase);
try
{
using JsonDocument languageListDocument = Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.GetLanguages();
JsonElement languageList = languageListDocument.RootElement.GetProperty("list");
foreach (JsonElement e in languageList.EnumerateArray())
{
if (e.TryGetProperty("filenames", out var filenames))
{
for (int j = 0; j < filenames.GetArrayLength(); j++)
{
set.Add(filenames[j].ToString());
}
}
}
}
catch (Exception ex)
{
Logger.LogError("Failed to get monaco filenames: " + ex.Message);
}
return set;
}
/// <summary>
/// Prepares temp html for the previewing
/// </summary>
public static string PreviewTempFile(string fileText, string extension, string tempFolder, bool tryFormat, bool wrapText, bool stickyScroll, int fontSize, bool minimap)
public static string PreviewTempFile(string fileText, string extension, string fileName, string tempFolder, bool tryFormat, bool wrapText, bool stickyScroll, int fontSize, bool minimap)
{
// TODO: check if file is too big, add MaxFileSize to settings
return InitializeIndexFileAndSelectedFile(fileText, extension, tempFolder, tryFormat, wrapText, stickyScroll, fontSize, minimap);
return InitializeIndexFileAndSelectedFile(fileText, extension, fileName, tempFolder, tryFormat, wrapText, stickyScroll, fontSize, minimap);
}
private static string InitializeIndexFileAndSelectedFile(string fileContent, string extension, string tempFolder, bool tryFormat, bool wrapText, bool stickyScroll, int fontSize, bool minimap)
private static string InitializeIndexFileAndSelectedFile(string fileContent, string extension, string fileName, string tempFolder, bool tryFormat, bool wrapText, bool stickyScroll, int fontSize, bool minimap)
{
string vsCodeLangSet = Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.GetLanguage(extension);
// Fallback to filename matching for files without extensions (e.g., Dockerfile)
if (vsCodeLangSet == "plaintext" && !string.IsNullOrEmpty(fileName))
{
string languageByFileName = Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.GetLanguageByFileName(fileName);
if (languageByFileName != null)
{
vsCodeLangSet = languageByFileName;
}
}
if (tryFormat)
{
var formatter = Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.Formatters.SingleOrDefault(f => f.LangSet == vsCodeLangSet);

View File

@@ -114,6 +114,7 @@ namespace Peek.FilePreviewer.Previewers
await Dispatcher.RunOnUiThread(async () =>
{
string extension = File.Extension;
string fileName = File.Name;
// Default: non-dev file preview with standard context menu
IsDevFilePreview = false;
@@ -137,13 +138,13 @@ namespace Peek.FilePreviewer.Previewers
// Simple html file to preview. Shouldn't do things like enabling scripts or using a virtual mapped directory.
Preview = new Uri(File.Path);
}
else if (MonacoHelper.SupportedMonacoFileTypes.Contains(extension))
else if (MonacoHelper.SupportedMonacoFileTypes.Contains(extension) || MonacoHelper.SupportedMonacoFileNames.Contains(fileName))
{
// Source code files use Monaco editor
IsDevFilePreview = true;
CustomContextMenu = true;
var raw = await ReadHelper.Read(File.Path.ToString());
Preview = new Uri(MonacoHelper.PreviewTempFile(raw, extension, TempFolderPath.Path, _previewSettings.SourceCodeTryFormat, _previewSettings.SourceCodeWrapText, _previewSettings.SourceCodeStickyScroll, _previewSettings.SourceCodeFontSize, _previewSettings.SourceCodeMinimap));
Preview = new Uri(MonacoHelper.PreviewTempFile(raw, extension, fileName, TempFolderPath.Path, _previewSettings.SourceCodeTryFormat, _previewSettings.SourceCodeWrapText, _previewSettings.SourceCodeStickyScroll, _previewSettings.SourceCodeFontSize, _previewSettings.SourceCodeMinimap));
}
else
{
@@ -165,7 +166,9 @@ namespace Peek.FilePreviewer.Previewers
public static bool IsItemSupported(IFileSystemItem item)
{
return _supportedFileTypes.Contains(item.Extension) || MonacoHelper.SupportedMonacoFileTypes.Contains(item.Extension);
return _supportedFileTypes.Contains(item.Extension) ||
MonacoHelper.SupportedMonacoFileTypes.Contains(item.Extension) ||
MonacoHelper.SupportedMonacoFileNames.Contains(item.Name);
}
private bool HasFailedLoadingPreview()

View File

@@ -0,0 +1,76 @@
// DockerfileHandler.cs
// Fix for Issue #32686: Add support for Dockerfile preview
// Registers Dockerfile as a recognized format for Monaco previewer
using System;
using System.Collections.Generic;
using System.IO;
namespace Microsoft.PowerToys.PreviewHandler.Monaco
{
/// <summary>
/// Handler for Dockerfile preview support.
/// </summary>
public static class DockerfileHandler
{
/// <summary>
/// Dockerfile-related file names (case-insensitive).
/// </summary>
public static readonly IReadOnlyList<string> DockerfileNames = new[]
{
"Dockerfile",
"Dockerfile.dev",
"Dockerfile.prod",
"Dockerfile.test",
"Containerfile" // Podman equivalent
};
/// <summary>
/// File extensions associated with Docker.
/// </summary>
public static readonly IReadOnlyList<string> DockerExtensions = new[]
{
".dockerfile",
".containerfile"
};
/// <summary>
/// Checks if a file is a Dockerfile.
/// </summary>
public static bool IsDockerfile(string filePath)
{
if (string.IsNullOrEmpty(filePath))
{
return false;
}
var fileName = Path.GetFileName(filePath);
var extension = Path.GetExtension(filePath);
// Check by name (Dockerfile, Dockerfile.dev, etc.)
foreach (var name in DockerfileNames)
{
if (fileName.StartsWith(name, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
// Check by extension
foreach (var ext in DockerExtensions)
{
if (extension.Equals(ext, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
/// <summary>
/// Gets the Monaco language ID for Dockerfile syntax highlighting.
/// </summary>
public static string GetLanguageId() => "dockerfile";
}
}

View File

@@ -15,8 +15,9 @@ namespace Microsoft.PowerToys.PreviewHandler.Monaco
/// Converts a file extension to a language monaco id.
/// </summary>
/// <param name="fileExtension">The extension of the file (without the dot).</param>
/// <param name="fileName">Optional filename for matching files without extensions (e.g., "Dockerfile").</param>
/// <returns>The monaco language id</returns>
public static string GetLanguage(string fileExtension)
public static string GetLanguage(string fileExtension, string fileName = null)
{
fileExtension = fileExtension.ToLower(CultureInfo.CurrentCulture);
try
@@ -38,6 +39,16 @@ namespace Microsoft.PowerToys.PreviewHandler.Monaco
}
}
// Fallback to filename matching for files without extensions (e.g., Dockerfile)
if (!string.IsNullOrEmpty(fileName))
{
string languageByFileName = FilePreviewCommon.MonacoHelper.GetLanguageByFileName(fileName);
if (languageByFileName != null)
{
return languageByFileName;
}
}
return "plaintext";
}
catch (Exception)

View File

@@ -359,7 +359,7 @@ namespace Microsoft.PowerToys.PreviewHandler.Monaco
private void InitializeIndexFileAndSelectedFile(string filePath)
{
Logger.LogInfo("Starting getting monaco language id out of filetype");
_vsCodeLangSet = FileHandler.GetLanguage(Path.GetExtension(filePath));
_vsCodeLangSet = FileHandler.GetLanguage(Path.GetExtension(filePath), Path.GetFileName(filePath));
DetectionResult result = CharsetDetector.DetectFromFile(filePath);
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);