From d7277922ab4aee3c3aa4d564444a5d5175781308 Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Wed, 4 Feb 2026 14:39:19 -0500 Subject: [PATCH] docs(ui): add Output Window documentation Add comprehensive documentation for working with the Output Window: - Quick start with Community Toolkit - Creating custom output panes - Writing to built-in panes (General, Build, Debug) - Built-in pane GUIDs reference - Thread-safe vs standard output methods - Pane configuration options - Showing and activating panes - Complete reusable logger example - Best practices Closes #43 --- src/content/docs/output-window.mdx | 319 +++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 src/content/docs/output-window.mdx diff --git a/src/content/docs/output-window.mdx b/src/content/docs/output-window.mdx new file mode 100644 index 0000000..0c2c313 --- /dev/null +++ b/src/content/docs/output-window.mdx @@ -0,0 +1,319 @@ +--- +title: Output Window +description: Learn how to create custom output panes and write messages to the Output window. +category: fundamentals +order: 8 +--- + +import Callout from '@components/Callout.astro'; + +The Output window displays status messages, build output, debug information, and other runtime text. Extensions can create custom output panes to display their own messages. + +## Quick Start with Community Toolkit + +The simplest way to write to the Output window: + +```csharp +// Write to the General pane +await VS.Windows.WriteToOutputWindowAsync("Hello from my extension!"); + +// Write to a custom pane (created if it doesn't exist) +var pane = await VS.Windows.CreateOutputWindowPaneAsync("My Extension"); +await pane.WriteLineAsync("Extension initialized successfully"); +``` + +## Creating a Custom Output Pane + +### With Community Toolkit + +```csharp +public class MyExtension +{ + private OutputWindowPane _pane; + + public async Task InitializeAsync() + { + // Create or get existing pane + _pane = await VS.Windows.CreateOutputWindowPaneAsync("My Extension"); + } + + public async Task LogAsync(string message) + { + await _pane.WriteLineAsync($"[{DateTime.Now:HH:mm:ss}] {message}"); + } + + public async Task LogErrorAsync(string error) + { + await _pane.WriteLineAsync($"ERROR: {error}"); + await _pane.ActivateAsync(); // Bring pane to front + } +} +``` + +### Traditional Approach + +```csharp +public class OutputPaneManager +{ + private static readonly Guid PaneGuid = new Guid("YOUR-GUID-HERE"); + private IVsOutputWindowPane _pane; + + public async Task InitializeAsync(AsyncPackage package) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + var outputWindow = await package.GetServiceAsync(typeof(SVsOutputWindow)) as IVsOutputWindow; + + // Create the pane + outputWindow.CreatePane( + ref PaneGuid, + "My Extension", + fInitVisible: 1, + fClearWithSolution: 1); + + outputWindow.GetPane(ref PaneGuid, out _pane); + } + + public void WriteLine(string message) + { + ThreadHelper.ThrowIfNotOnUIThread(); + _pane?.OutputStringThreadSafe($"{message}{Environment.NewLine}"); + } + + public void Activate() + { + ThreadHelper.ThrowIfNotOnUIThread(); + _pane?.Activate(); + } + + public void Clear() + { + ThreadHelper.ThrowIfNotOnUIThread(); + _pane?.Clear(); + } +} +``` + +## Writing to Built-in Panes + +Visual Studio has several built-in output panes you can write to: + +### General Pane + +```csharp +// Community Toolkit +await VS.Windows.WriteToOutputWindowAsync("Message to General pane"); + +// Traditional +var outputWindow = (IVsOutputWindow)GetService(typeof(SVsOutputWindow)); +var generalPaneGuid = VSConstants.GUID_OutWindowGeneralPane; +outputWindow.GetPane(ref generalPaneGuid, out var generalPane); +generalPane.OutputStringThreadSafe("Message to General pane\n"); +``` + +### Build Pane + +```csharp +var buildPaneGuid = VSConstants.GUID_BuildOutputWindowPane; +outputWindow.GetPane(ref buildPaneGuid, out var buildPane); +buildPane.OutputStringThreadSafe("Build message\n"); +``` + +### Debug Pane + +```csharp +var debugPaneGuid = VSConstants.GUID_OutWindowDebugPane; +outputWindow.GetPane(ref debugPaneGuid, out var debugPane); +debugPane.OutputStringThreadSafe("Debug message\n"); +``` + +## Built-in Pane GUIDs + +| Pane | GUID | Constant | +|------|------|----------| +| General | `{65482C72-DEFA-41B7-902C-11C091889C83}` | `VSConstants.GUID_OutWindowGeneralPane` | +| Build | `{1BD8A850-02D1-11D1-BEE7-00A0C913D1F8}` | `VSConstants.GUID_BuildOutputWindowPane` | +| Debug | `{FC076020-078A-11D1-A7DF-00A0C9110051}` | `VSConstants.GUID_OutWindowDebugPane` | + +## Output Methods + +### OutputStringThreadSafe vs OutputString + +```csharp +// Thread-safe - can be called from any thread +pane.OutputStringThreadSafe("Safe from any thread\n"); + +// Must be on UI thread +await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); +pane.OutputString("Must be on UI thread\n"); +``` + + +Always prefer `OutputStringThreadSafe` unless you need special formatting or are already on the UI thread. + + +### Formatted Output + +```csharp +// Add timestamp prefix +public void Log(string message) +{ + var timestamp = DateTime.Now.ToString("HH:mm:ss.fff"); + _pane.OutputStringThreadSafe($"[{timestamp}] {message}\n"); +} + +// Log levels +public void LogInfo(string message) => Log($"INFO: {message}"); +public void LogWarning(string message) => Log($"WARN: {message}"); +public void LogError(string message) => Log($"ERROR: {message}"); +``` + +## Pane Options + +When creating a pane, you can configure its behavior: + +```csharp +outputWindow.CreatePane( + ref paneGuid, + "My Extension", + fInitVisible: 1, // 1 = visible in dropdown, 0 = hidden until first write + fClearWithSolution: 1 // 1 = clear when solution closes, 0 = preserve content +); +``` + +| Parameter | Values | Description | +|-----------|--------|-------------| +| `fInitVisible` | 0 or 1 | Whether pane appears in dropdown before first write | +| `fClearWithSolution` | 0 or 1 | Whether to clear content when solution closes | + +## Showing and Activating + +```csharp +// Show the Output window (bring to front) +await VS.Windows.ShowOutputWindowAsync(); + +// Activate your specific pane +await pane.ActivateAsync(); + +// Traditional approach +var outputWindow = (IVsOutputWindow)GetService(typeof(SVsOutputWindow)); +outputWindow.GetPane(ref paneGuid, out var pane); +pane.Activate(); + +// Show Output window via UI shell +var uiShell = (IVsUIShell)GetService(typeof(SVsUIShell)); +var outputWindowGuid = new Guid("{34E76E81-EE4A-11D0-AE2E-00A0C90FFFC3}"); +uiShell.FindToolWindow((uint)__VSFINDTOOLWIN.FTW_fForceCreate, ref outputWindowGuid, out var frame); +frame.Show(); +``` + +## Complete Example + +A reusable logging service for your extension: + +```csharp +using System; +using System.Threading.Tasks; +using Community.VisualStudio.Toolkit; +using Microsoft.VisualStudio.Shell; + +public interface IExtensionLogger +{ + Task LogAsync(string message); + Task LogWarningAsync(string message); + Task LogErrorAsync(string message); + Task ClearAsync(); +} + +public class ExtensionLogger : IExtensionLogger +{ + private readonly string _paneName; + private OutputWindowPane _pane; + + public ExtensionLogger(string paneName) + { + _paneName = paneName; + } + + private async Task EnsurePaneAsync() + { + _pane ??= await VS.Windows.CreateOutputWindowPaneAsync(_paneName); + } + + public async Task LogAsync(string message) + { + await EnsurePaneAsync(); + await _pane.WriteLineAsync($"[{DateTime.Now:HH:mm:ss}] {message}"); + } + + public async Task LogWarningAsync(string message) + { + await EnsurePaneAsync(); + await _pane.WriteLineAsync($"[{DateTime.Now:HH:mm:ss}] ⚠ WARNING: {message}"); + } + + public async Task LogErrorAsync(string message) + { + await EnsurePaneAsync(); + await _pane.WriteLineAsync($"[{DateTime.Now:HH:mm:ss}] ❌ ERROR: {message}"); + await _pane.ActivateAsync(); + } + + public async Task ClearAsync() + { + await EnsurePaneAsync(); + await _pane.ClearAsync(); + } +} + +// Usage in your package +public sealed class MyPackage : ToolkitPackage +{ + public static IExtensionLogger Logger { get; private set; } + + protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) + { + await base.InitializeAsync(cancellationToken, progress); + + Logger = new ExtensionLogger("My Extension"); + await Logger.LogAsync("Extension loaded successfully"); + } +} + +// Usage anywhere in your extension +await MyPackage.Logger.LogAsync("Processing file..."); +await MyPackage.Logger.LogErrorAsync("Failed to process file"); +``` + +## Best Practices + +1. **Use descriptive pane names** - Make it clear which extension owns the pane +2. **Add timestamps** - Helps users understand when events occurred +3. **Use thread-safe methods** - Prefer `OutputStringThreadSafe` for background operations +4. **Don't spam** - Only log meaningful information +5. **Activate on errors** - Bring the pane to attention when something goes wrong +6. **Clear appropriately** - Don't clear too often; users may want to scroll back + + +Avoid writing large amounts of text rapidly. This can freeze the UI. For high-volume logging, consider batching writes or using a separate log file. + + +## Deleting a Pane + +Remove a custom pane when it's no longer needed: + +```csharp +var outputWindow = (IVsOutputWindow)GetService(typeof(SVsOutputWindow)); +var paneGuid = new Guid("YOUR-PANE-GUID"); +outputWindow.DeletePane(ref paneGuid); +``` + + +You cannot delete built-in panes (General, Build, Debug). + + +## See Also + +- [Tool Windows](tool-windows) - Creating custom dockable windows +- [Error List](error-list) - Displaying errors, warnings, and messages +- [Status Bar](status-bar) - Brief status messages