Luthetus.TextEditor 4.8.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Luthetus.TextEditor --version 4.8.0
                    
NuGet\Install-Package Luthetus.TextEditor -Version 4.8.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Luthetus.TextEditor" Version="4.8.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Luthetus.TextEditor" Version="4.8.0" />
                    
Directory.Packages.props
<PackageReference Include="Luthetus.TextEditor" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Luthetus.TextEditor --version 4.8.0
                    
#r "nuget: Luthetus.TextEditor, 4.8.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#addin nuget:?package=Luthetus.TextEditor&version=4.8.0
                    
Install Luthetus.TextEditor as a Cake Addin
#tool nuget:?package=Luthetus.TextEditor&version=4.8.0
                    
Install Luthetus.TextEditor as a Cake Tool

Full documentation is available in the GitHub repo:

Luthetus.TextEditor (v4.9.0)

Installation

Goal

  • Reference the Luthetus.TextEditor Nuget Package
  • Register the Services
  • Reference the CSS
  • Reference the JavaScript
  • In MainLayout.razor render the <Luthetus.Common.RazorLib.Installations.Displays.LuthetusCommonInitializer /> and the <Luthetus.TextEditor.RazorLib.Installations.Displays.LuthetusTextEditorInitializer /> Blazor components

Steps

  • Reference the Luthetus.TextEditor NuGet Package

Use your preferred way to install NuGet Packages to install Luthetus.TextEditor.

  • Register the Services

Go to the file that you register your services and add the following lines of C# code.

/* using Luthetus.Common.RazorLib.Installations.Models;
using Luthetus.Common.RazorLib.BackgroundTasks.Models;
using Luthetus.TextEditor.RazorLib.Installations.Models;
using Luthetus.TextEditor.RazorLib.CompilerServices;
using Luthetus.TextEditor.RazorLib.Decorations.Models; */

// Use either Wasm or ServerSide depending on how your app is being hosted.
// var luthetusHostingKind = LuthetusHostingKind.ServerSide;
var luthetusHostingKind = LuthetusHostingKind.Wasm;

var hostingInformation = new LuthetusHostingInformation(
    luthetusHostingKind,
    LuthetusPurposeKind.TextEditor,
    new BackgroundTaskService());

services.AddLuthetusTextEditor(hostingInformation);

// CompilerServiceRegistry
//
// You can create your own implementation of:
// - ICompilerServiceRegistry
// - IDecorationMapperRegistry
//
// In order to support any file extension.
//
// The "Default" ones will treat all file extensions
// as plain text.
//
return services
    .AddScoped<ICompilerServiceRegistry, CompilerServiceRegistryDefault>()
    .AddScoped<IDecorationMapperRegistry, DecorationMapperRegistryDefault>();
  • Reference the CSS

Go to the file that you reference CSS files from and add the following CSS references.

<link href="_content/Luthetus.Common/luthetusCommon.css" rel="stylesheet" />
<link href="_content/Luthetus.TextEditor/luthetusTextEditor.css" rel="stylesheet" />
  • Reference the JavaScript

Go to the file that you reference JavaScript files from and add the following JavaScript reference below the Blazor framework JavaScript reference

<script src="_content/Luthetus.Common/luthetusCommon.js"></script>
<script src="_content/Luthetus.TextEditor/luthetusTextEditor.js"></script>
  • In App.razor add the following towards the top of the file:

<Luthetus.Common.RazorLib.Installations.Displays.LuthetusCommonInitializer />
<Luthetus.TextEditor.RazorLib.Installations.Displays.LuthetusTextEditorInitializer />

Luthetus.TextEditor (v4.9.0)

Usage

Goal

  • Render a C# Text Editor which makes use of the CSharpCompilerService

Steps

  • Create a codebehind for the file: Pages/Index.razor
// Pages/Index.razor.cs

using Microsoft.AspNetCore.Components;

namespace Luthetus.Tutorials.RazorLib.Pages;

public partial class Index : ComponentBase
{
}
  • There are 2 parts to rendering a Text Editor.

  • First, one needs to make a TextEditorModel.

  • Then, one needs to make a TextEditorViewModel.

  • In Index.razor.cs add the following line of code:

// using Luthetus.TextEditor.RazorLib.Lexers.Models;
public static ResourceUri ResourceUri { get; } = new("/index.txt");
  • The ResourceUri is a unique identifier for a given TextEditorModel.

  • One might think of a TextEditorModel as mapping to a file on their filesystem, as an example.

  • Override the Blazor lifecycle method named: OnInitialized()

protected override void OnInitialized()
{
    base.OnInitialized();
}
  • Inside this override we can register the TextEditorModel.

  • We need access to the ITextEditorService to register a TextEditorModel. So, inject the ITextEditorService.

// using Luthetus.TextEditor.RazorLib;
[Inject]
private ITextEditorService TextEditorService { get; set; } = null!;
  • The ITextEditorService has public properties that encapsulate the API for a given datatype in the Luthetus.TextEditor namespace. For example, TextEditorService.ModelApi accesses the ModelApi property, which has all of the API related to the TextEditorModel datatype.

  • By invoking TextEditorService.ModelApi.RegisterCustom(...);, we can register a TextEditorModel. The RegisterCustom(...) method takes as parameters: an instance of TextEditorEditContext, and a TextEditorModel. So we need to make the TextEditorModel instance.

  • In the override for OnInitialized(), create an instance of a TextEditorModel.

// using Luthetus.TextEditor.RazorLib.TextEditors.Models;
var model = new TextEditorModel(
	ResourceUri,
	DateTime.UtcNow,
	ExtensionNoPeriodFacts.TXT,
	"public class MyClass\n{\n\t\n}\n",
	decorationMapper: null,
	compilerService: null);
  • Now, we need the TextEditorEditContext. Invoke TextEditorService.WorkerArbitrary.PostUnique(...). The first argument is a "name" for the work item. The second argument is a Func that will provide you a TextEditorEditContext instance, and expects you to return a ValueTask.

  • Inside the TextEditorService.WorkerArbitrary.PostUnique(...) Func argument, go on to in the body of the Func, invoke TextEditorService.Model.RegisterCustom(...). Pass the TextEditorEditContext that the Func provided, and the instance of the TextEditorModel.

protected override void OnInitialized()
{
	// using Luthetus.TextEditor.RazorLib.TextEditors.Models;
    var model = new TextEditorModel(
		ResourceUri,
		DateTime.UtcNow,
		ExtensionNoPeriodFacts.TXT,
		"public class MyClass\n{\n\t\n}\n",
	    decorationMapper: null,
        compilerService: null);

	TextEditorService.WorkerArbitrary.PostUnique(nameof(Index), editContext =>
	{
    	TextEditorService.ModelApi.RegisterCustom(editContext, model);
		return ValueTask.CompletedTask;
	});

    base.OnInitialized();
}
  • In order to render a TextEditorModel, we need a TextEditorViewModel. A file on one's operating system would relate to a model. Whereas, an open text editor would relate to a viewModel.

  • Many TextEditorViewModel can reference an underlying TextEditorModel. But, only 1 model for a given file on one's operating system can exist.

NOTE: I used a file system as an example but, a TextEditorModel can be made "out of thin air" just the same.

  • We need a TextEditorViewModelKey so we can maintain the TextEditorViewModel state between page changes.

  • Add the following line of code:

// using Luthetus.Common.RazorLib.Keys.Models;
public static Key<TextEditorViewModel> ViewModelKey { get; } = Key<TextEditorViewModel>.NewKey();
  • Now, inside the TextEditorService.WorkerArbitrary.PostUnique(...) Func argument, invoke TextEditorService.ViewModelApi.Register(...) after the line that you registered the model.

NOTE: The argument 'Category' to 'TextEditorService.ViewModelApi.Register(...)' can be passed as 'new Category("main")'. It acts only as a way to filter a list of view models.

TextEditorService.ViewModelApi.Register(
    editContext,
    ViewModelKey,
    ResourceUri,
    new Category("main"));
  • My Pages/Index.razor.cs file as of this step looks as follows:
using Microsoft.AspNetCore.Components;
using Luthetus.Common.RazorLib.Keys.Models;
using Luthetus.TextEditor.RazorLib;
using Luthetus.TextEditor.RazorLib.Lexers.Models;
using Luthetus.TextEditor.RazorLib.TextEditors.Models;

namespace NugetPaTe490.ServerSide.Pages;

public partial class Index : ComponentBase
{
	[Inject]
	private ITextEditorService TextEditorService { get; set; } = null!;

	public static ResourceUri ResourceUri { get; } = new("/index.txt");
	public static Key<TextEditorViewModel> ViewModelKey { get; } = Key<TextEditorViewModel>.NewKey();
	
	protected override void OnInitialized()
	{
	    var model = new TextEditorModel(
			ResourceUri,
			DateTime.UtcNow,
			ExtensionNoPeriodFacts.TXT,
			"public class MyClass\n{\n\t\n}\n",
		    decorationMapper: null,
	        compilerService: null);
	
		TextEditorService.WorkerArbitrary.PostUnique(nameof(Index), editContext =>
		{
	    	TextEditorService.ModelApi.RegisterCustom(editContext, model);
	    	
	    	TextEditorService.ViewModelApi.Register(
	    		editContext,
			    ViewModelKey,
			    ResourceUri,
			    new Category("main"));
	    	
	    	return ValueTask.CompletedTask;
		});
	
	    base.OnInitialized();
	}
}
  • In the .razor markup (Pages/Index.razor) render the Blazor component:

<TextEditorViewModelDisplay/>
  • This component takes only one required parameter, any others are for customization.

  • The required parameter named TextEditorViewModelKey can be given the property named ViewModelKey, which was made in the codebehind.

@page "/"

@using Luthetus.TextEditor.RazorLib.TextEditors.Displays;

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<TextEditorViewModelDisplay TextEditorViewModelKey="ViewModelKey" />
  • A plain text editor without syntax highlighting should render now when the app is ran.

Height of the text editor is 100% of the parent element.

For the Blazor default project template I'll provide a quick hack I did to get things to work, but this only demonstrates the idea of how you'd fix a height issue, not well written CSS.



@page "/"

@using Luthetus.TextEditor.RazorLib.TextEditors.Displays;

<PageTitle>Index</PageTitle>

<div style="height: 5em;">
	<h1>Hello, world!</h1>
	
	Welcome to your new app.
</div>

@* 10px for padding *@
<div style="height: calc(100% - 5em - 10px);">
	<TextEditorViewModelDisplay TextEditorViewModelKey="ViewModelKey"/>
</div>


@inherits LayoutComponentBase

<PageTitle>Luthetus.Tutorials</PageTitle>

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>

        
        <article class="content px-4" style="height: calc(100% - 56px);">
            @Body
        </article>
    </main>
</div>

The text editor automatically detects the user agent resize events and re-renders at a 3 second debounce (BrowserResizeInterop).

  • Now we can add in the C# Compiler Service.

  • Reference the Luthetus.CompilerServices.CSharp Nuget Package

Go to the file that you register your services and locate where you previously added the scoped ICompilerServiceRegistry, and IDecorationMapperRegistry. Instead of using the "default" implementations we can provide our own Type that will implement the interface.

/*using Luthetus.TextEditor.RazorLib.CompilerServices.Interfaces;
using Luthetus.TextEditor.RazorLib.Decorations.Models;*/

// NOTE: the next step creates the implementations
services
    .AddScoped<ICompilerServiceRegistry, CompilerServiceRegistry>()
    .AddScoped<IDecorationMapperRegistry, DecorationMapperRegistry>();

Add CompilerServiceRegistry.cs as follows:

using Luthetus.Common.RazorLib.Clipboards.Models;
using Luthetus.TextEditor.RazorLib;
using Luthetus.TextEditor.RazorLib.CompilerServices;
using Luthetus.TextEditor.RazorLib.TextEditors.Models;
using Luthetus.CompilerServices.CSharp.CompilerServiceCase;

namespace NugetPaTe490.ServerSide;

public class CompilerServiceRegistry : ICompilerServiceRegistry
{
    private readonly Dictionary<string, ICompilerService> _map = new();

    public IReadOnlyList<ICompilerService> CompilerServiceList => _map.Values.ToList();

    public CompilerServiceRegistry(ITextEditorService textEditorService, IClipboardService clipboardService)
    {
        CSharpCompilerService = new CSharpCompilerService(textEditorService, clipboardService);
        DefaultCompilerService = new CompilerServiceDoNothing();
        
        _map.Add(ExtensionNoPeriodFacts.C_SHARP_CLASS, CSharpCompilerService);
    }

    public CSharpCompilerService CSharpCompilerService { get; }
    public CompilerServiceDoNothing DefaultCompilerService { get; }

    public ICompilerService GetCompilerService(string extensionNoPeriod)
    {
        if (_map.TryGetValue(extensionNoPeriod, out var compilerService))
            return compilerService;

        return DefaultCompilerService;
    }
}

Add DecorationMapperRegistry.cs as follows:

using Luthetus.TextEditor.RazorLib.Decorations.Models;
using Luthetus.TextEditor.RazorLib.TextEditors.Models;

namespace NugetPaTe490.ServerSide;

public class DecorationMapperRegistry : IDecorationMapperRegistry
{
    private Dictionary<string, IDecorationMapper> _map { get; } = new();

    public DecorationMapperRegistry()
    {
        GenericDecorationMapper = new GenericDecorationMapper();
        DefaultDecorationMapper = new TextEditorDecorationMapperDefault();

        _map.Add(ExtensionNoPeriodFacts.C_SHARP_CLASS, GenericDecorationMapper);
    }

    public GenericDecorationMapper GenericDecorationMapper { get; }
    public TextEditorDecorationMapperDefault DefaultDecorationMapper { get; }

    public IDecorationMapper GetDecorationMapper(string extensionNoPeriod)
    {
        if (_map.TryGetValue(extensionNoPeriod, out var decorationMapper))
            return decorationMapper;

        return DefaultDecorationMapper;
    }
}
  • In the Pages/Index.razor.cs file we now can provide the CSharpCompilerService when instantiating a TextEditorModel.

  • Inject the ICompilerServiceRegistry and the IDecorationMapperRegistry

/*using Luthetus.TextEditor.RazorLib.CompilerServices;
using Luthetus.TextEditor.RazorLib.Decorations.Models;*/

[Inject]
private ICompilerServiceRegistry CompilerServiceRegistry { get; set; } = null!;
[Inject]
private IDecorationMapperRegistry DecorationMapperRegistry { get; set; } = null!;
  • In OnInitialized(), prior to constructing the TextEditorModel we can get the decoration mapper, and compiler service we wish to use.
var genericDecorationMapper = DecorationMapperRegistry.GetDecorationMapper(
    ExtensionNoPeriodFacts.C_SHARP_CLASS);

var cSharpCompilerService = CompilerServiceRegistry.GetCompilerService(
    ExtensionNoPeriodFacts.C_SHARP_CLASS);
  • Then pass in genericDecorationMapper and cSharpCompilerService instead of the nulls when invoking the TextEditorModel constructor.

  • One last step, on the line immediately following, TextEditorService.ModelApi.RegisterCustom(editContext, model); add the line: cSharpCompilerService.RegisterResource(model.ResourceUri, shouldTriggerResourceWasModified: true);

  • My Pages/Index.razor.cs file as of this step is shown in the following code snippet:

using Microsoft.AspNetCore.Components;
using Luthetus.Common.RazorLib.Keys.Models;
using Luthetus.TextEditor.RazorLib;
using Luthetus.TextEditor.RazorLib.Lexers.Models;
using Luthetus.TextEditor.RazorLib.TextEditors.Models;
using Luthetus.TextEditor.RazorLib.CompilerServices;
using Luthetus.TextEditor.RazorLib.Decorations.Models;

namespace NugetPaTe490.ServerSide.Pages;

public partial class Index : ComponentBase
{
	[Inject]
	private ITextEditorService TextEditorService { get; set; } = null!;
	[Inject]
	private ICompilerServiceRegistry CompilerServiceRegistry { get; set; } = null!;
	[Inject]
	private IDecorationMapperRegistry DecorationMapperRegistry { get; set; } = null!;

	public static ResourceUri ResourceUri { get; } = new("/index.txt");
	public static Key<TextEditorViewModel> ViewModelKey { get; } = Key<TextEditorViewModel>.NewKey();
	
	protected override void OnInitialized()
	{
		var genericDecorationMapper = DecorationMapperRegistry.GetDecorationMapper(
		    ExtensionNoPeriodFacts.C_SHARP_CLASS);
		
		var cSharpCompilerService = CompilerServiceRegistry.GetCompilerService(
		    ExtensionNoPeriodFacts.C_SHARP_CLASS);
		
	    var model = new TextEditorModel(
			ResourceUri,
			DateTime.UtcNow,
			ExtensionNoPeriodFacts.TXT,
			"public class MyClass\n{\n\t\n}\n",
		    genericDecorationMapper,
	        cSharpCompilerService);
	
		TextEditorService.WorkerArbitrary.PostUnique(nameof(Index), editContext =>
		{
	    	TextEditorService.ModelApi.RegisterCustom(editContext, model);
	    	cSharpCompilerService.RegisterResource(model.ResourceUri, shouldTriggerResourceWasModified: true);
	    	
	    	TextEditorService.ViewModelApi.Register(
	    		editContext,
			    ViewModelKey,
			    ResourceUri,
			    new Category("main"));
	    	
	    	return ValueTask.CompletedTask;
		});
	
	    base.OnInitialized();
	}
}
  • Now you should have a Text Editor with the CSharpCompilerService.

  • I followed this tutorial myself, and a mistake I made was that I never changed the "default" registry implementations -- I only made the classes. After fixing this, my service registrations look like the following:

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using NugetPaTe490.ServerSide;
using NugetPaTe490.ServerSide.Data;
using Luthetus.Common.RazorLib.Installations.Models;
using Luthetus.Common.RazorLib.BackgroundTasks.Models;
using Luthetus.TextEditor.RazorLib.Installations.Models;
using Luthetus.TextEditor.RazorLib.CompilerServices;
using Luthetus.TextEditor.RazorLib.Decorations.Models;

var builder = WebApplication.CreateBuilder(args);

var luthetusHostingKind = LuthetusHostingKind.ServerSide;

var hostingInformation = new LuthetusHostingInformation(
    luthetusHostingKind,
    LuthetusPurposeKind.TextEditor,
    new BackgroundTaskService());

builder.Services.AddLuthetusTextEditor(hostingInformation);

builder.Services
    .AddScoped<ICompilerServiceRegistry, CompilerServiceRegistry>()
    .AddScoped<IDecorationMapperRegistry, DecorationMapperRegistry>();
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Luthetus.TextEditor:

Package Downloads
Luthetus.ExtendCompilerServices

An optional, opinionated implementation for various ICompilerService features.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
4.9.0 38 4/23/2025
4.8.0 90 4/5/2025
4.7.0 162 3/29/2025
4.6.0 492 3/25/2025
4.5.0 158 3/20/2025
4.3.0 171 3/10/2025
4.2.0 221 3/6/2025
4.1.0 104 2/7/2025
4.0.0 125 1/30/2025
3.9.0 112 1/22/2025
3.8.0 111 1/20/2025
3.7.0 107 1/18/2025
3.6.0 141 12/31/2024
3.5.0 117 12/21/2024
3.4.0 130 12/6/2024
3.3.0 121 11/28/2024
3.2.0 132 10/7/2024
3.1.0 119 10/3/2024
2.7.0 133 9/21/2024
2.6.0 128 9/19/2024
2.5.0 171 9/5/2024
2.4.0 148 9/2/2024
2.3.0 139 8/28/2024
2.2.0 158 8/21/2024
2.1.0 153 8/14/2024
2.0.0 139 8/13/2024
1.4.0 211 10/6/2023
1.3.0 213 8/20/2023
1.2.0 150 8/19/2023
1.1.0 172 8/18/2023
1.0.0 191 8/16/2023