Skip to content
Merged
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,31 @@ Enables snippets you declare in XML syntax. The `FilePath` is a path to the root
### Snippets

In addition to the XML snippets, you can declare some snippets using `Snippets` object. The probably main difference is that the XML is more readable for longer snippets and better escapes special characters in CDATA sections. Use these as you wish.

### Variables

The `Variables` section lets you declare named values that are substituted into snippet text at apply/copy time. This makes it easy to share snippets across machines or operating systems.

**Syntax:** use `{VariableName}` placeholders inside any snippet text.

**Configuration example:**

```json
{
"Variables": {
"ShellExt": "ps1"
},
"Snippets": {
"Install": "Run install.{ShellExt} to get started."
}
}
```

On macOS/Linux you would set `"ShellExt": "sh"`, on Windows `"ShellExt": "ps1"`. The snippet text `"Run install.{ShellExt} to get started."` expands to the correct value at apply/copy time, while the snippet source stays unchanged.

**Notes:**
- Variable names are matched exactly as declared (case-sensitive).
- Unknown tokens (variables not declared in `Variables`) are left verbatim in the expanded text — you typically don't need to escape literal `{Name}` content unless `Name` happens to match a declared variable.
- To prevent a `{Name}` span from being expanded (when the name collides with a declared variable), wrap it as `{{Name}}`. The braces are preserved in the output.
- If a snippet text fails to parse (e.g. contains JSON-like `{...}` syntax), the text is returned unchanged.
- Snippet search always runs against the raw (unexpanded) template text.
1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<FrameworkPackageVersion>10.0.0</FrameworkPackageVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Neptuo" Version="6.0.2" />
<PackageVersion Include="Neptuo.Windows.Converters" Version="1.0.0" />
<PackageVersion Include="Neptuo.Windows.HotKeys" Version="2.0.0" />
<PackageVersion Include="Neptuo.Windows.Threading" Version="1.0.1" />
Expand Down
5 changes: 4 additions & 1 deletion src/Neptuo.Productivity.SnippetManager.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using Neptuo.Productivity.SnippetManager.Variables;

namespace Neptuo.Productivity.SnippetManager;

Expand Down Expand Up @@ -68,7 +69,8 @@ public override void OnFrameworkInitializationCompleted()
enabled => configurationWatcher?.EnableRaisingEventsFromConfigurationWatcher(enabled),
shutdown,
GetExampleConfiguration,
GetCurrentHotkey
GetCurrentHotkey,
configuration.Variables
);

private void RequestShutdown(IClassicDesktopStyleApplicationLifetime desktop)
Expand Down Expand Up @@ -119,6 +121,7 @@ private Configuration GetExampleConfiguration()
{
var example = new Configuration();
example.General = GeneralConfiguration.Example;
example.Variables = VariablesConfiguration.Example;
snippetProviders.AddExampleConfigurations(example.Providers);
return example;
}
Expand Down
11 changes: 9 additions & 2 deletions src/Neptuo.Productivity.SnippetManager.Avalonia/Navigator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Avalonia.Media;
using Avalonia.Threading;
using Neptuo.Productivity.SnippetManager.Services;
using Neptuo.Productivity.SnippetManager.Variables;
using Neptuo.Productivity.SnippetManager.ViewModels;
using Neptuo.Productivity.SnippetManager.ViewModels.Commands;
using Neptuo.Productivity.SnippetManager.Views;
Expand All @@ -24,9 +25,10 @@ public class Navigator : IClipboardService, ISendTextService
private readonly Func<Configuration> getExampleConfiguration;
private readonly Func<string> getCurrentHotkey;
private readonly ConfigurationRepository configurationRepository;
private readonly SnippetExpansionPipeline expansionPipeline;
private int? lastExternalProcessId;

public Navigator(ISnippetProvider snippetProvider, ConfigurationRepository configurationRepository, Action<bool> setConfigChangeEnabled, Action shutdown, Func<Configuration> getExampleConfiguration, Func<string> getCurrentHotkey)
public Navigator(ISnippetProvider snippetProvider, ConfigurationRepository configurationRepository, Action<bool> setConfigChangeEnabled, Action shutdown, Func<Configuration> getExampleConfiguration, Func<string> getCurrentHotkey, VariablesConfiguration? variables)
{
this.snippetProvider = snippetProvider;
this.configurationRepository = configurationRepository;
Expand All @@ -36,6 +38,11 @@ public Navigator(ISnippetProvider snippetProvider, ConfigurationRepository confi
this.getCurrentHotkey = getCurrentHotkey;
this.snippetProviderContext = new();
this.snippetProviderContext.Changed += OnModelsChanged;
this.expansionPipeline = new SnippetExpansionPipeline(
new TokenSnippetVariableScanner(),
new ConfigurationVariableValueResolver(variables),
new TokenSnippetTextExpander()
);

snippetProviderInitializeTask = snippetProvider.InitializeAsync(snippetProviderContext);
}
Expand All @@ -58,7 +65,7 @@ public void OpenMain(bool stickToActiveCaret = true)
{
main = new MainWindow();
main.Closed += (sender, e) => { main = null; };
main.ViewModel = new MainViewModel(snippetProviderContext, new ApplySnippetCommand(this), new CopySnippetCommand(this));
main.ViewModel = new MainViewModel(snippetProviderContext, new ApplySnippetCommand(this, expansionPipeline), new CopySnippetCommand(this, expansionPipeline));
UpdateWindowPositionAnchor(main, stickToActiveCaret);
shouldRefreshSnippets = true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
using Neptuo.Productivity.SnippetManager.Models;
using Neptuo.Productivity.SnippetManager.Variables;
using Neptuo.Productivity.SnippetManager.ViewModels;
using Neptuo.Productivity.SnippetManager.ViewModels.Commands;

namespace Neptuo.Productivity.SnippetManager.Views.DesignData;

internal class ViewModelLocator
{
private static readonly SnippetExpansionPipeline emptyPipeline = new SnippetExpansionPipeline(
new TokenSnippetVariableScanner(),
new ConfigurationVariableValueResolver(null),
new TokenSnippetTextExpander()
);

private static MainViewModel? mainViewModel;
public static MainViewModel MainViewModel
{
Expand All @@ -17,8 +24,8 @@ public static MainViewModel MainViewModel

mainViewModel = new MainViewModel(
tree,
new ApplySnippetCommand(InteropService.Instance),
new CopySnippetCommand(InteropService.Instance)
new ApplySnippetCommand(InteropService.Instance, emptyPipeline),
new CopySnippetCommand(InteropService.Instance, emptyPipeline)
);

mainViewModel.Snippets.AddRange(tree.GetRoots());
Expand Down
5 changes: 4 additions & 1 deletion src/Neptuo.Productivity.SnippetManager.UI/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using System.Text.Json;
using System.Windows;
using Neptuo.Productivity.SnippetManager.Variables;
using Application = System.Windows.Application;
using MessageBox = System.Windows.MessageBox;

Expand Down Expand Up @@ -55,7 +56,8 @@ protected override void OnStartup(StartupEventArgs e)
configurationRepository,
enabled => configurationWatcher.EnableRaisingEventsFromConfigurationWatcher(enabled),
Shutdown,
GetExampleConfiguration
GetExampleConfiguration,
configuration.Variables
);

private string GetXmlConfigurationPath()
Expand All @@ -73,6 +75,7 @@ private Configuration GetExampleConfiguration()
{
var example = new Configuration();
example.General = GeneralConfiguration.Example;
example.Variables = VariablesConfiguration.Example;
snippetProviders.AddExampleConfigurations(example.Providers);

return example;
Expand Down
11 changes: 9 additions & 2 deletions src/Neptuo.Productivity.SnippetManager.UI/Navigator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Windows.Forms;
using System.Windows.Input;
using Neptuo.Productivity.SnippetManager.Services;
using Neptuo.Productivity.SnippetManager.Variables;
using Neptuo.Productivity.SnippetManager.ViewModels;
using Neptuo.Productivity.SnippetManager.ViewModels.Commands;
using Neptuo.Productivity.SnippetManager.Views;
Expand All @@ -24,8 +25,9 @@ public class Navigator : IClipboardService, ISendTextService
private readonly Action shutdown;
private readonly Func<Configuration> getExampleConfiguration;
private readonly ConfigurationRepository configurationRepository;
private readonly SnippetExpansionPipeline expansionPipeline;

public Navigator(ISnippetProvider snippetProvider, ConfigurationRepository configurationRepository, Action<bool> setConfigChangeEnabled, Action shutdown, Func<Configuration> getExampleConfiguration)
public Navigator(ISnippetProvider snippetProvider, ConfigurationRepository configurationRepository, Action<bool> setConfigChangeEnabled, Action shutdown, Func<Configuration> getExampleConfiguration, VariablesConfiguration? variables)
{
this.snippetProvider = snippetProvider;
this.configurationRepository = configurationRepository;
Expand All @@ -34,6 +36,11 @@ public Navigator(ISnippetProvider snippetProvider, ConfigurationRepository confi
this.getExampleConfiguration = getExampleConfiguration;
this.snippetProviderContext = new();
this.snippetProviderContext.Changed += OnModelsChanged;
this.expansionPipeline = new SnippetExpansionPipeline(
new TokenSnippetVariableScanner(),
new ConfigurationVariableValueResolver(variables),
new TokenSnippetTextExpander()
);

snippetProviderInitializeTask = snippetProvider.InitializeAsync(snippetProviderContext);
}
Expand All @@ -52,7 +59,7 @@ public void OpenMain(bool stickToActiveCaret = true)
{
main = new MainWindow();
main.Closed += (sender, e) => { main = null; };
main.ViewModel = new MainViewModel(snippetProviderContext, new ApplySnippetCommand(this), new CopySnippetCommand(this));
main.ViewModel = new MainViewModel(snippetProviderContext, new ApplySnippetCommand(this, expansionPipeline), new CopySnippetCommand(this, expansionPipeline));
UpdateWindowStickPointToCaret(main, stickToActiveCaret);

_ = UpdateSnippetsAsync(main.ViewModel);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
using Neptuo.Productivity.SnippetManager.Models;
using Neptuo.Productivity.SnippetManager.Variables;
using Neptuo.Productivity.SnippetManager.ViewModels;
using Neptuo.Productivity.SnippetManager.ViewModels.Commands;

namespace Neptuo.Productivity.SnippetManager.Views.DesignData;

internal class ViewModelLocator
{
private static readonly SnippetExpansionPipeline emptyPipeline = new SnippetExpansionPipeline(
new TokenSnippetVariableScanner(),
new ConfigurationVariableValueResolver(null),
new TokenSnippetTextExpander()
);

private static MainViewModel? mainViewModel;
public static MainViewModel MainViewModel
{
Expand All @@ -17,8 +24,8 @@ public static MainViewModel MainViewModel

mainViewModel = new MainViewModel(
tree,
new ApplySnippetCommand(InteropService.Instance),
new CopySnippetCommand(InteropService.Instance)
new ApplySnippetCommand(InteropService.Instance, emptyPipeline),
new CopySnippetCommand(InteropService.Instance, emptyPipeline)
);

var gitHub = tree.GetRoots().First(s => s.Title == "GitHub");
Expand Down
3 changes: 3 additions & 0 deletions src/Neptuo.Productivity.SnippetManager/Configuration.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System.Text.Json.Serialization;
using Neptuo.Productivity.SnippetManager.Variables;

namespace Neptuo.Productivity.SnippetManager;

public class Configuration
{
public GeneralConfiguration? General { get; set; }

public VariablesConfiguration? Variables { get; set; }

[JsonIgnore]
public Dictionary<string, object> Providers { get; } = new();
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<PackageReference Include="Neptuo" />
<PackageReference Include="Neptuo.Observables" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Neptuo.Productivity.SnippetManager.Variables;

public class CompositeVariableValueResolver(IEnumerable<IVariableValueResolver> resolvers) : IVariableValueResolver
{
public bool TryGetValue(string name, out string? value)
{
foreach (var resolver in resolvers)
{
if (resolver.TryGetValue(name, out value))
return true;
}

value = null;
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Neptuo.Productivity.SnippetManager.Variables;

public class ConfigurationVariableValueResolver(VariablesConfiguration? configuration) : IVariableValueResolver
{
public bool TryGetValue(string name, out string? value)
{
if (configuration != null && configuration.TryGetValue(name, out value))
return true;

value = null;
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Neptuo.Productivity.SnippetManager.Variables;

public interface ISnippetTextExpander
{
string Expand(string text, IReadOnlyDictionary<string, string?> values);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Neptuo.Productivity.SnippetManager.Variables;

public interface ISnippetVariableScanner
{
IReadOnlyList<VariableReference> Scan(string text);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Neptuo.Productivity.SnippetManager.Variables;

public interface IVariableValueResolver
{
bool TryGetValue(string name, out string? value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Neptuo.Productivity.SnippetManager.Variables;

public class SnippetExpansionPipeline(
ISnippetVariableScanner scanner,
IVariableValueResolver resolver,
ISnippetTextExpander expander)
{
public string Apply(string text)
{
var references = scanner.Scan(text);
if (references.Count == 0)
return text;

var values = new Dictionary<string, string?>();
foreach (var reference in references)
{
resolver.TryGetValue(reference.Name, out var value);
values[reference.Name] = value;
}

return expander.Expand(text, values);
}
Comment thread
maraf marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Text;
using Neptuo.Text.Tokens;

namespace Neptuo.Productivity.SnippetManager.Variables;

public class TokenSnippetTextExpander : ISnippetTextExpander
{
public string Expand(string text, IReadOnlyDictionary<string, string?> values)
{
var parser = CreateParser();
var parsedTokens = new List<TokenEventArgs>();
parser.OnParsedToken += (sender, e) => parsedTokens.Add(e);

if (!parser.Parse(text) || parsedTokens.Count == 0)
return text;

var sb = new StringBuilder();
int lastEnd = 0;

foreach (var tokenEvent in parsedTokens)
{
sb.Append(text, lastEnd, tokenEvent.StartPosition - lastEnd);

if (values.TryGetValue(tokenEvent.Token.Fullname, out var value) && value != null)
sb.Append(value);
else
sb.Append(text, tokenEvent.StartPosition, tokenEvent.EndPosition - tokenEvent.StartPosition);

lastEnd = tokenEvent.EndPosition;
}

sb.Append(text, lastEnd, text.Length - lastEnd);

return sb.ToString();
}

private static TokenParser CreateParser()
{
var parser = new TokenParser();
parser.Configuration.AllowTextContent = true;
parser.Configuration.AllowMultipleTokens = true;
parser.Configuration.AllowEscapeSequence = true;
parser.Configuration.AllowAttributes = false;
parser.Configuration.AllowDefaultAttributes = false;
return parser;
}
}
Loading
Loading