├── YuckLS ├── Core │ ├── Models │ │ ├── ILoggable.cs │ │ └── YuckTypes.cs │ ├── YuckCompleter.cs │ ├── YuckCheck.cs │ └── SExpression.cs ├── yuckls ├── YuckLS.csproj ├── GlobalUsings.cs ├── Services │ ├── IBufferService.cs │ ├── BufferService.cs │ └── EwwWorkspace.cs ├── Program.cs └── Handlers │ ├── CompletionHandler.cs │ └── TextDocumentSyncHandler.cs ├── YuckLS.Test ├── GlobalUsings.cs ├── UnitTest1.cs ├── YuckLS.Test.csproj └── SExpressionTest.cs ├── YuckLS.sln ├── README.md ├── .gitignore └── .editorconfig /YuckLS/Core/Models/ILoggable.cs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /YuckLS.Test/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | global using Moq; -------------------------------------------------------------------------------- /YuckLS/yuckls: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec /usr/lib/yuckls/YuckLS "$@" 4 | 5 | -------------------------------------------------------------------------------- /YuckLS.Test/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | namespace YuckLS.Test; 2 | 3 | public class UnitTest1 4 | { 5 | [Fact] 6 | public void Test1() 7 | { 8 | 9 | } 10 | } -------------------------------------------------------------------------------- /YuckLS/Core/YuckCompleter.cs: -------------------------------------------------------------------------------- 1 | namespace YuckLS.Core; 2 | using YuckLS.Handlers; 3 | using YuckLS.Core.Models; 4 | internal sealed class YuckCompleter(string _text, ILogger _logger, YuckLS.Services.IEwwWorkspace _workspace ) 5 | { 6 | private readonly SExpression _sExpression = new(_text, _logger , _workspace); 7 | public CompletionList GetCompletions() 8 | { 9 | var completionContext = _sExpression.TryGetCompletionContext(); 10 | if (completionContext == default) return new CompletionList(); 11 | return completionContext.Completions(); 12 | } 13 | } -------------------------------------------------------------------------------- /YuckLS/YuckLS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /YuckLS/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; 2 | global using OmniSharp.Extensions.LanguageServer.Protocol.Document; 3 | global using OmniSharp.Extensions.LanguageServer.Protocol.Models; 4 | global using OmniSharp.Extensions.LanguageServer.Protocol.Server; 5 | global using Microsoft.Extensions.Logging; 6 | global using Microsoft.Extensions.DependencyInjection; 7 | global using OmniSharp.Extensions.LanguageServer.Server; 8 | global using Serilog; 9 | global using OmniSharp.Extensions.LanguageServer.Protocol; 10 | global using MediatR; 11 | global using System.Linq; 12 | global using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities; 13 | using System.Runtime.CompilerServices; -------------------------------------------------------------------------------- /YuckLS.Test/YuckLS.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /YuckLS.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YuckLS", "YuckLS\YuckLS.csproj", "{2D7E319C-C840-4359-BCB0-A335A10D4615}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YuckLS.Test", "YuckLS.Test\YuckLS.Test.csproj", "{27D4F45A-542C-49D5-81DB-047D12F1FEBC}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(SolutionProperties) = preSolution 16 | HideSolutionNode = FALSE 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {2D7E319C-C840-4359-BCB0-A335A10D4615}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {2D7E319C-C840-4359-BCB0-A335A10D4615}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {2D7E319C-C840-4359-BCB0-A335A10D4615}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {2D7E319C-C840-4359-BCB0-A335A10D4615}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {27D4F45A-542C-49D5-81DB-047D12F1FEBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {27D4F45A-542C-49D5-81DB-047D12F1FEBC}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {27D4F45A-542C-49D5-81DB-047D12F1FEBC}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {27D4F45A-542C-49D5-81DB-047D12F1FEBC}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /YuckLS/Services/IBufferService.cs: -------------------------------------------------------------------------------- 1 | namespace YuckLS.Services; 2 | using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; 3 | 4 | internal sealed class Buffer(string _text) 5 | { 6 | public string GetText() => _text; 7 | 8 | private static readonly string[] separator = ["\n", "\r\n"]; 9 | 10 | public string? GetTextTillLine(Position position) 11 | { 12 | string[] lines = _text.Split(separator, StringSplitOptions.None); 13 | string text = string.Join("\n", lines[..position.Line]); 14 | string line = lines[position.Line][..position.Character]; 15 | return $"{text}\n{line}"; 16 | } 17 | } 18 | //special thanks to avalonia for this implementation 19 | //see http://www.github.com/avaloniaui/avaloniavscode 20 | /// 21 | ///Manages file buffer in memory as user makes edits 22 | /// 23 | internal interface IBufferService 24 | { 25 | /// 26 | ///Add a new Buffer to the concurrentDictionary that stores open buffers. Will be invoked on documentOpen 27 | /// 28 | public void Add(DocumentUri key, string text); 29 | 30 | /// 31 | ///Get full text from a buffer if it exists 32 | /// 33 | public string? GetText(DocumentUri key); 34 | 35 | /// 36 | ///Remove a Buffer from the concurrentDictionary that stores all buffers. Will be invoked on documentClose 37 | public void Remove(DocumentUri key); 38 | 39 | /// 40 | ///Apply an incremental change to an opened buffer. 41 | /// 42 | public void ApplyIncrementalChange(DocumentUri key, Range range, string text); 43 | 44 | /// 45 | ///Apply a full change to an opened buffer 46 | /// 47 | public void ApplyFullChange(DocumentUri key, string text); 48 | 49 | /// 50 | ///Get all text from beginning of line to current position 51 | /// 12 | options.WithInput(Console.OpenStandardInput()) 13 | .WithOutput(Console.OpenStandardOutput()) 14 | .ConfigureLogging(p => 15 | p.AddSerilog(Log.Logger) 16 | .AddLanguageProtocolLogging() 17 | .SetMinimumLevel(LogLevel.Trace) 18 | ) 19 | .WithHandler() 20 | .WithHandler() 21 | .WithServices(s => 22 | s.AddSingleton(new ConfigurationItem { Section = "Yuck Server" }) 23 | .AddSingleton(GetServer) 24 | .AddSingleton(new TextDocumentSelector(new TextDocumentFilter { Pattern = "**/*.yuck"})) 25 | .AddSingleton() 26 | .AddSingleton() 27 | ) 28 | ); 29 | await server.WaitForExit; 30 | } 31 | private static void InitLogging() 32 | { 33 | //temporary log location for my computer 34 | if(Path.Exists("/home/noble/Documents/YuckLS/YuckLS/")){ 35 | string logfile = Path.Combine("/home/noble/Documents/YuckLS/YuckLS/", "yucklsp.log"); 36 | Log.Logger = new LoggerConfiguration() 37 | .WriteTo.File(logfile) 38 | .Enrich.FromLogContext() 39 | .MinimumLevel.Error() 40 | .CreateLogger(); 41 | } 42 | else{ 43 | string logfile = Path.Combine(Path.GetTempPath(), "yucklsp.log"); 44 | Log.Logger = new LoggerConfiguration() 45 | .WriteTo.File(logfile) 46 | .Enrich.FromLogContext() 47 | .MinimumLevel.Verbose() 48 | .CreateLogger(); 49 | } 50 | } 51 | private static ILanguageServer? GetServer() => server; 52 | 53 | 54 | } -------------------------------------------------------------------------------- /YuckLS/Services/BufferService.cs: -------------------------------------------------------------------------------- 1 | namespace YuckLS.Services; 2 | using System.Collections.Concurrent; 3 | 4 | using OmniSharp.Extensions.LanguageServer.Protocol.Models; 5 | 6 | /// 7 | internal sealed class BufferService(Microsoft.Extensions.Logging.ILogger _logger) : IBufferService 8 | { 9 | private readonly ConcurrentDictionary _buffers = new(); 10 | 11 | /// 12 | public void Add(DocumentUri key, string text) 13 | { 14 | _logger.LogTrace($"** Adding new file to opened buffers: {key}"); 15 | _buffers.TryAdd(key, new Buffer(text)); 16 | } 17 | 18 | /// 19 | public void Remove(DocumentUri key) 20 | { 21 | // throw new NotImplementedException(); 22 | } 23 | 24 | public string? GetText(DocumentUri key){ 25 | Buffer? fullBuffer = null; 26 | _buffers.TryGetValue(key, out fullBuffer); 27 | 28 | if(fullBuffer != null){ 29 | return fullBuffer.GetText(); 30 | } 31 | //document does not exist in dictionary 32 | return null; 33 | } 34 | private static string Splice(string buffer, Range range, string text) 35 | { 36 | var start = GetIndex(buffer, range.Start); 37 | var end = GetIndex(buffer, range.End); 38 | return buffer[..start] + text + buffer[end..]; 39 | } 40 | 41 | private static int GetIndex(string buffer, Position position) 42 | { 43 | var index = 0; 44 | for (var i = 0; i < position.Line; ++i) 45 | { 46 | index = buffer.IndexOf('\n', index) + 1; 47 | } 48 | return index + position.Character; 49 | } 50 | 51 | public string? GetTextTillPosition(DocumentUri key, Position position) 52 | { 53 | return _buffers[key].GetTextTillLine(position); 54 | } 55 | 56 | public void ApplyIncrementalChange(DocumentUri key, Range range, string text) 57 | { 58 | var buffer = _buffers[key]; 59 | var newText = Splice(buffer.GetText(), range, text); 60 | _buffers.TryUpdate(key, new Buffer(newText), buffer); 61 | } 62 | 63 | public void ApplyFullChange(DocumentUri key, string text) 64 | { 65 | var buffer = _buffers[key]; 66 | _buffers.TryUpdate(key, new Buffer(text), buffer); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Highly experimental partially functional LSP implementation for the yuck configuration language. See https://www.github.com/elkowar/eww. 2 | ## Building. 3 | Install dotnet-sdk-8.0 on your computer from your package manager. 4 | 5 | # clone and compile 6 | ``` 7 | git clone https://www.github.com/eugenenoble2005/yuckls.git 8 | cd yuckls/YuckLS 9 | dotnet build --output dist 10 | ``` 11 | Note where you cloned the repo to and where the compiled binaries are (/dist). You can now set it up for an editor 12 | # Install from aur 13 | You can install the master branch from the aur. 14 | ``` 15 | yay -S yuckls-git 16 | ``` 17 | The executable can be started by running ```yuckls``` 18 | 19 | # Neovim 20 | ``` 21 | -- put this in your init.lua 22 | vim.api.nvim_create_autocmd( 23 | {"BufEnter", "BufWinEnter"}, 24 | { 25 | pattern = {"*.yuck"}, 26 | callback = function(event) 27 | print(string.format("starting yuck;s for %s", vim.inspect(event))) 28 | vim.lsp.start { 29 | name = "YuckLs", 30 | cmd = {"dotnet", "/home/gitrepos/yuckls/YuckLS/dist/YuckLS.dll"}, --this must be where you cloned this repo to. 31 | --cmd = {"yuckls"} -- if installed from aur 32 | root_dir = vim.fn.getcwd() 33 | } 34 | end 35 | } 36 | ) 37 | ``` 38 | 39 | # Helix 40 | ``` 41 | # put this in your languages.toml 42 | [language-server] 43 | yuckls = {command="dotnet" , args = ["/home/gitrepos/yuckls/YuckLS/dist/YuckLS.dll"]} #must be where you cloned this repo to 44 | #yuckls = {command = "yuckls } #if installed from aur 45 | [[language]] 46 | name="yuck" 47 | scope="source.yuck" 48 | injection-regex="yuck" 49 | file-types=["yuck"] 50 | language-servers = ["yuckls"] 51 | ``` 52 | 53 | ## ROADMAP 54 | ✔️ Basic in built type completions for widgets and properties 55 | 56 | ✔️ Basic Completions for custom widgets 57 | 58 | ✔️ Basic Diagnostics and error reporting 59 | 60 | ❌ Go to definition for custom widgets and variables 61 | 62 | ❌ Buffer formatting 63 | 64 | # Limitations 65 | For some reason i already regret, i decided to write the parser with just regex and string parsing instead of more standard methods like using lexers or ASTs. It's quirky but should work fine for many simple cases. Perhaps in the future or until a better solution is available, i might rewrite the whole thing to use ANTLR or something more flexible and concise. 66 | 67 | 68 | 69 | ![Screenshot_29-Dec_19-11-31_29472](https://github.com/user-attachments/assets/e4cb5a39-8692-42f6-8906-f9afe8201f30) 70 | 71 | 72 | -------------------------------------------------------------------------------- /YuckLS/Handlers/CompletionHandler.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Runtime.CompilerServices; 3 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] 4 | [assembly: InternalsVisibleTo("YuckLS.Test")] 5 | namespace YuckLS.Handlers; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | using YuckLS.Core; 10 | using YuckLS.Services; 11 | 12 | internal sealed class CompletionHandler( 13 | ILogger _logger, 14 | TextDocumentSelector _textDocumentSelector, 15 | IBufferService _bufferService, 16 | YuckLS.Services.IEwwWorkspace _workspace 17 | ) : CompletionHandlerBase 18 | { 19 | private readonly string[] _triggerChars = { "(", ":" , " " , "${"}; 20 | public override Task Handle(CompletionItem request, CancellationToken cancellationToken) 21 | { 22 | var ci = new CompletionItem 23 | { 24 | Label = request.Label, 25 | Kind = request.Kind, 26 | InsertText = request.InsertText, 27 | }; 28 | return Task.FromResult(ci); 29 | } 30 | public override Task Handle(CompletionParams request, CancellationToken cancellationToken) 31 | { 32 | string? text = _bufferService.GetTextTillPosition(request.TextDocument.Uri, request.Position); 33 | if (text is null) 34 | return Task.FromResult(new CompletionList()); 35 | 36 | YuckCompleter yuckCompleter = new YuckCompleter(text, _logger, _workspace); 37 | var completions = yuckCompleter.GetCompletions(); 38 | if (completions is null || completions.Count() == 0) 39 | return Task.FromResult(new CompletionList()); 40 | return Task.FromResult(completions); 41 | } 42 | 43 | protected override CompletionRegistrationOptions CreateRegistrationOptions(CompletionCapability capability, ClientCapabilities clientCapabilities) 44 | { 45 | return new() 46 | { 47 | DocumentSelector = _textDocumentSelector, 48 | TriggerCharacters = new Container(_triggerChars), 49 | AllCommitCharacters = new Container("\n"), 50 | ResolveProvider = true 51 | }; 52 | // throw new NotImplementedException(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /YuckLS/Services/EwwWorkspace.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace YuckLS.Services; 3 | using YuckLS.Core.Models; 4 | using YuckLS.Core; 5 | public interface IEwwWorkspace 6 | { 7 | public YuckType[] UserDefinedTypes {get;} 8 | public YuckVariable[] UserDefinedVariables {get;} 9 | public void LoadWorkspace(); 10 | } 11 | internal sealed class EwwWorkspace(ILogger _logger, ILoggerFactory _loggerFactory, IServiceProvider _serviceProvider) : IEwwWorkspace 12 | { 13 | //store include paths and whether they have been visited 14 | private System.Collections.Concurrent.ConcurrentDictionary _includePaths = new(); 15 | private YuckType[] _userDefinedTypes = new YuckType[] { }; 16 | private YuckVariable[] _userDefinedVariable = new YuckVariable [] {}; 17 | private string? _ewwRoot = null; 18 | 19 | public YuckVariable[] UserDefinedVariables => _userDefinedVariable; 20 | 21 | YuckType[] IEwwWorkspace.UserDefinedTypes => _userDefinedTypes ; 22 | 23 | public void LoadWorkspace() 24 | { 25 | _logger.LogError("Loading workspace"); 26 | //empty user types and include paths 27 | _userDefinedTypes = new YuckType[] {}; 28 | _userDefinedVariable = new YuckVariable[] {}; 29 | _includePaths = new(); 30 | var current_path = Directory.GetCurrentDirectory(); 31 | 32 | //recursively traverse upwards until we find an eww.yuck file. 33 | while (_ewwRoot == null) 34 | { 35 | if (File.Exists(Path.Combine(current_path, "eww.yuck"))) 36 | { 37 | _ewwRoot = Path.Combine(current_path, "eww.yuck"); 38 | } 39 | else 40 | { 41 | var parent_dir = Directory.GetParent(current_path); 42 | if (parent_dir is null) return; //no parent dir, break for now 43 | current_path = parent_dir.FullName; 44 | } 45 | } 46 | //run initial load at eww.yuck 47 | LoadVariables(_ewwRoot); 48 | 49 | //recursively load any included files by checking if any file path has not been visited 50 | while (_includePaths.Where(p => p.Value == false).Count() > 0) 51 | { 52 | LoadVariables(_includePaths.Where(p => p.Value == false).First().Key); 53 | } 54 | } 55 | 56 | 57 | internal void LoadVariables(string filepath) 58 | { 59 | if (!File.Exists(filepath)) 60 | { 61 | _includePaths.AddOrUpdate(filepath, true, (key, oldvalue) => true); 62 | return; 63 | }; 64 | bool isLoaded = false; 65 | _includePaths.TryGetValue(filepath, out isLoaded); 66 | //prevent infinte loads 67 | if (isLoaded == true) return; 68 | //mark this file as loaded 69 | _includePaths.AddOrUpdate(filepath, true, (key, oldvalue) => true); 70 | var ewwRootBuffer = File.ReadAllText(filepath); 71 | var _completionHandlerLogger = _loggerFactory.CreateLogger(); 72 | var _ewwWorkspace = _serviceProvider.GetRequiredService(); 73 | var sExpression = new SExpression(ewwRootBuffer, _completionHandlerLogger, _ewwWorkspace); 74 | var customVariables = sExpression.GetVariables(); 75 | _userDefinedTypes = _userDefinedTypes.Concat(customVariables.customWidgets.ToArray()).ToArray(); 76 | _userDefinedVariable = _userDefinedVariable.Concat(customVariables.customVariables.ToArray()).ToArray(); 77 | List includePaths = sExpression.GetIncludes(); 78 | foreach (string include in includePaths) 79 | { 80 | if(_ewwRoot is null) continue; 81 | string? parentDir = Directory.GetParent(_ewwRoot)?.FullName; 82 | if(parentDir is null) continue; 83 | string includePath = Path.Combine(parentDir, include); 84 | _includePaths.TryAdd(includePath, false); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /YuckLS/Handlers/TextDocumentSyncHandler.cs: -------------------------------------------------------------------------------- 1 | namespace YuckLS.Handlers; 2 | using YuckLS.Services; 3 | using OmniSharp.Extensions.LanguageServer.Protocol.Models; 4 | using OmniSharp.Extensions.LanguageServer.Protocol; 5 | using YuckLS.Core; 6 | internal sealed class TextDocumentSyncHandler( 7 | ILogger _logger, 8 | ILanguageServerConfiguration _configuration, 9 | IServiceProvider _serviceProvider, 10 | IBufferService _bufferService, 11 | TextDocumentSelector _textDocumentSelector, 12 | IEwwWorkspace _ewwWorkspace, 13 | ILoggerFactory _loggerFactory 14 | ) : TextDocumentSyncHandlerBase 15 | { 16 | private CancellationTokenSource _diagnosticsDebounceToken = new(); 17 | public override TextDocumentAttributes GetTextDocumentAttributes(DocumentUri uri) 18 | { 19 | return new TextDocumentAttributes(uri, uri.Scheme!, "yuck"); 20 | } 21 | 22 | public override async Task Handle(DidOpenTextDocumentParams request, CancellationToken cancellationToken) 23 | { 24 | _ewwWorkspace.LoadWorkspace(); 25 | var conf = await _configuration.GetScopedConfiguration(request.TextDocument.Uri, cancellationToken); 26 | _bufferService.Add(request.TextDocument.Uri, request.TextDocument.Text); 27 | //load diagnostics on document open 28 | _ = LoadDiagnosticsAsync(request.TextDocument.Uri); 29 | return Unit.Value; 30 | } 31 | 32 | public override Task Handle(DidChangeTextDocumentParams request, CancellationToken cancellationToken) 33 | { 34 | foreach (var change in request.ContentChanges) 35 | { 36 | if (change.Range != null) 37 | { 38 | _bufferService.ApplyIncrementalChange(request.TextDocument.Uri, change.Range, change.Text); 39 | } 40 | else 41 | { 42 | _bufferService.ApplyFullChange(request.TextDocument.Uri, change.Text); 43 | } 44 | } 45 | _ = LoadDiagnosticsAsync(request.TextDocument.Uri); 46 | return Task.FromResult(Unit.Value); 47 | } 48 | private async Task LoadDiagnosticsAsync(DocumentUri documentUri) 49 | { 50 | //cancel previous requests 51 | await _diagnosticsDebounceToken.CancelAsync(); 52 | _diagnosticsDebounceToken = new(); 53 | 54 | try 55 | { 56 | await Task.Delay(300, _diagnosticsDebounceToken.Token); 57 | } 58 | catch 59 | { 60 | return; 61 | } 62 | string? _text = _bufferService.GetText(documentUri); 63 | if (_text != null) 64 | { 65 | var completionHandlerLogger = _loggerFactory.CreateLogger(); 66 | YuckCheck yuckCheck = new(_text, completionHandlerLogger, _ewwWorkspace); 67 | var diagnostics = yuckCheck.TryGetDiagnosticsAsync(_diagnosticsDebounceToken.Token); 68 | var _languageServer = _serviceProvider.GetRequiredService(); 69 | _languageServer.PublishDiagnostics(new PublishDiagnosticsParams 70 | { 71 | Uri = documentUri, 72 | Diagnostics = await diagnostics 73 | }); 74 | } 75 | } 76 | public override Task Handle(DidSaveTextDocumentParams request, CancellationToken cancellationToken) 77 | { 78 | //reload workspace when buffer is saved , i'm not sure that this is the most ideal way to do this 79 | _ewwWorkspace.LoadWorkspace(); 80 | _logger.LogTrace($"did save file {request.TextDocument.Uri}"); 81 | return Task.FromResult(Unit.Value); 82 | } 83 | 84 | public override Task Handle(DidCloseTextDocumentParams request, CancellationToken cancellationToken) 85 | { 86 | return Task.FromResult(Unit.Value); 87 | } 88 | 89 | protected override TextDocumentSyncRegistrationOptions CreateRegistrationOptions(TextSynchronizationCapability capability, ClientCapabilities clientCapabilities) 90 | { 91 | return new TextDocumentSyncRegistrationOptions 92 | { 93 | DocumentSelector = _textDocumentSelector, 94 | Change = OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities.TextDocumentSyncKind.Incremental, 95 | Save = new SaveOptions() { IncludeText = false } 96 | }; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /YuckLS.Test/SExpressionTest.cs: -------------------------------------------------------------------------------- 1 | namespace YuckLS.Test; 2 | using YuckLS.Core; 3 | public class SExpressionTest 4 | { 5 | 6 | /**Test cases for isTopLevel() 7 | * All testcases must end with '(' Because that's the only thing that will trigger this method. Cases must not have whitespaces at the end. 8 | * */ 9 | private string[] _isTopLevelTestCases = new string[] { 10 | //1 11 | @"(defwindow 12 | (defvar 13 | (", 14 | 15 | //2 16 | @"(", 17 | 18 | //3 19 | @"((defpoll medianame :inital '34242')) 20 | (defwindow media ;unclosed window 21 | (label :name testlabel) 22 | (", 23 | //4 24 | @"(defpoll medianame :initial '3313') 25 | (defwindow media ;unclosed window 26 | :geometry (" 27 | , 28 | //5 29 | @"(defpoll medianame :initial '3313') 30 | (defwindow media 31 | :geometry (geometry :anchor bottom left) 32 | ) 33 | () 34 | (name :ping) 35 | (", 36 | 37 | //6 38 | @"(defwidget media 39 | ;(defpoll (defwidget)) 40 | ) 41 | ;(defpoll) 42 | (", 43 | //7 44 | @"(defwidget media 45 | ; ) 46 | 47 | (", 48 | 49 | //8 50 | @"(defwidget 51 | ) 52 | (defpoll 'curl ; ()fsfsfsfsf') 53 | (", 54 | 55 | //9 56 | @"(defwindow ) 57 | (defpoll hyprvr :interval ""Hyprland --version|awk '{print $1; exit}'"") 58 | (", 59 | //10 60 | @"( 61 | ;dgdgdg'( 62 | )(" 63 | }; 64 | 65 | /*Test cases fot GetParentNode() 66 | *All test cases must end with '(' and must not be top level. That is isTopLevel() must return false for it. 67 | */ 68 | private string[] _getParentNodeTestCases = new string[]{ 69 | //1 70 | @"(defwindow 71 | (box 72 | (", 73 | 74 | //2 75 | @"(defpoll 76 | (defwidget 77 | deflisten 78 | (defvar 79 | (", 80 | //3 81 | @"(defpoll 82 | (defwindow 83 | )) 84 | ()() 85 | (defwindow 86 | (box 87 | (label :name Label-name) 88 | ) 89 | ) 90 | (defwindow 91 | (defwidget 92 | (", 93 | 94 | //4 95 | @"(defpoll 96 | (defwindow 97 | (label xx 98 | (box ) 99 | (box ) 100 | (table ) 101 | (", 102 | 103 | //5 104 | @"(defpoll 105 | ;(defwidget 106 | ( 107 | )", 108 | 109 | //6 110 | @" 111 | (defwindow 112 | (box :orientation horizontal 113 | :halign center 114 | text 115 | (button :onclick notify-send 'Hello' 'Hello, ${name}' 116 | Greet)) 117 | (", 118 | 119 | //7 120 | @" 121 | (box :orientation v :spacing -200 :class media-box :visible isSpotify 122 | (box :height 300px :style background-image:url('${mediaart}') :class mediaart) 123 | ; (label :text cava :class mediaartist 124 | (label :text mediatitle :class mediatitle :truncate true 125 | :placeholder hh 126 | ) 127 | (label :text mediaartist :class mediaartist 128 | :gddgdg 129 | (box ) 130 | ) 131 | (", 132 | //8 133 | @"( 134 | (defwindow 135 | ;comment it's 136 | (box :class ""(node "" 137 | (" 138 | }; 139 | 140 | //check bracket pairs test cases 141 | private string[] _bracketPairsTestCases = new string[] { 142 | //1 143 | "(make (fast (system))", 144 | 145 | //2 146 | @")california (dreaming) 147 | (defwindow (box))) 148 | ", 149 | //3 150 | @"()()()(())(close(man))" , 151 | //4 152 | @"(defwindow 153 | (box 154 | (label) 155 | )) 156 | (defvar (box) 157 | " 158 | }; 159 | [Fact] 160 | public void IsTopLevelTest() 161 | { 162 | var CompletionHandlerLoggerMock = new Mock>(); 163 | var ewwWorkspaceMock = new Mock(); 164 | var test1 = new SExpression(_isTopLevelTestCases[0], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).IsTopLevel(); 165 | var test2 = new SExpression(_isTopLevelTestCases[1], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).IsTopLevel(); 166 | var test3 = new SExpression(_isTopLevelTestCases[2], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).IsTopLevel(); 167 | var test4 = new SExpression(_isTopLevelTestCases[3], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).IsTopLevel(); 168 | var test5 = new SExpression(_isTopLevelTestCases[4], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).IsTopLevel(); 169 | var test6 = new SExpression(_isTopLevelTestCases[5], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).IsTopLevel(); 170 | var test7 = new SExpression(_isTopLevelTestCases[6], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).IsTopLevel(); 171 | var test8 = new SExpression(_isTopLevelTestCases[7], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).IsTopLevel(); 172 | var test9 = new SExpression(_isTopLevelTestCases[8], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).IsTopLevel(); 173 | var test10 = new SExpression(_isTopLevelTestCases[9], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).IsTopLevel(); 174 | Assert.False(test1); 175 | Assert.True(test2); 176 | Assert.False(test3); 177 | Assert.False(test4); 178 | Assert.True(test5); 179 | Assert.True(test6); 180 | Assert.False(test7); 181 | Assert.True(test8); 182 | Assert.True(test9); 183 | Assert.True(test10); 184 | } 185 | 186 | [Fact] 187 | public void GetParentNodeTest() 188 | { 189 | //\(\w+[^\(]*\) 190 | var CompletionHandlerLoggerMock = new Mock>(); 191 | var ewwWorkspaceMock = new Mock(); 192 | var test1 = new SExpression(_getParentNodeTestCases[0], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).GetParentNode(); 193 | var test2 = new SExpression(_getParentNodeTestCases[1], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).GetParentNode(); 194 | var test3 = new SExpression(_getParentNodeTestCases[2], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).GetParentNode(); 195 | var test4 = new SExpression(_getParentNodeTestCases[3], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).GetParentNode(); 196 | var test5 = new SExpression(_getParentNodeTestCases[4], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).GetParentNode(); 197 | var test6 = new SExpression(_getParentNodeTestCases[5], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).GetParentNode(); 198 | var test7 = new SExpression(_getParentNodeTestCases[6], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).GetParentNode(); 199 | var test8 = new SExpression(_getParentNodeTestCases[7], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).GetParentNode(); 200 | Assert.Equal(test1, "box"); 201 | Assert.Equal(test2, "defvar"); 202 | Assert.Equal(test3, "defwidget"); 203 | Assert.Equal(test4, "label"); 204 | Assert.Equal(test5, "defpoll"); 205 | Assert.Equal(test6, "defwindow"); 206 | Assert.Equal(test7, "box"); 207 | Assert.Equal(test8, "box"); 208 | } 209 | 210 | [Fact] 211 | public void CheckBracketPairsTest() 212 | { 213 | var CompletionHandlerLoggerMock = new Mock>(); 214 | var ewwWorkspaceMock = new Mock(); 215 | var test1 = new SExpression(_bracketPairsTestCases[0], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).CheckBracketPairs(); 216 | var test2 = new SExpression(_bracketPairsTestCases[1], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).CheckBracketPairs(); 217 | var test3 = new SExpression(_bracketPairsTestCases[2], CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).CheckBracketPairs(); 218 | var test4 = new SExpression(_bracketPairsTestCases[3],CompletionHandlerLoggerMock.Object, ewwWorkspaceMock.Object).CheckBracketPairs(); 219 | Assert.Equal(test1, new List { 0 }); 220 | Assert.Equal(test2, new List { 0, _bracketPairsTestCases[1].TrimEnd().Length-1 }); 221 | Assert.Equal(test3, new List { }); 222 | Assert.Equal(test4, new List {_bracketPairsTestCases[3].IndexOf("(defvar")}); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from `dotnet new gitignore` 5 | 6 | # dotenv files 7 | .env 8 | YuckLS/yucklsp.log 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # Tye 69 | .tye/ 70 | 71 | # ASP.NET Scaffolding 72 | ScaffoldingReadMe.txt 73 | 74 | # StyleCop 75 | StyleCopReport.xml 76 | 77 | # Files built by Visual Studio 78 | *_i.c 79 | *_p.c 80 | *_h.h 81 | *.ilk 82 | *.meta 83 | *.obj 84 | *.iobj 85 | *.pch 86 | *.pdb 87 | *.ipdb 88 | *.pgc 89 | *.pgd 90 | *.rsp 91 | *.sbr 92 | *.tlb 93 | *.tli 94 | *.tlh 95 | *.tmp 96 | *.tmp_proj 97 | *_wpftmp.csproj 98 | *.log 99 | *.tlog 100 | *.vspscc 101 | *.vssscc 102 | .builds 103 | *.pidb 104 | *.svclog 105 | *.scc 106 | 107 | # Chutzpah Test files 108 | _Chutzpah* 109 | 110 | # Visual C++ cache files 111 | ipch/ 112 | *.aps 113 | *.ncb 114 | *.opendb 115 | *.opensdf 116 | *.sdf 117 | *.cachefile 118 | *.VC.db 119 | *.VC.VC.opendb 120 | 121 | # Visual Studio profiler 122 | *.psess 123 | *.vsp 124 | *.vspx 125 | *.sap 126 | 127 | # Visual Studio Trace Files 128 | *.e2e 129 | 130 | # TFS 2012 Local Workspace 131 | $tf/ 132 | 133 | # Guidance Automation Toolkit 134 | *.gpState 135 | 136 | # ReSharper is a .NET coding add-in 137 | _ReSharper*/ 138 | *.[Rr]e[Ss]harper 139 | *.DotSettings.user 140 | 141 | # TeamCity is a build add-in 142 | _TeamCity* 143 | 144 | # DotCover is a Code Coverage Tool 145 | *.dotCover 146 | 147 | # AxoCover is a Code Coverage Tool 148 | .axoCover/* 149 | !.axoCover/settings.json 150 | 151 | # Coverlet is a free, cross platform Code Coverage Tool 152 | coverage*.json 153 | coverage*.xml 154 | coverage*.info 155 | 156 | # Visual Studio code coverage results 157 | *.coverage 158 | *.coveragexml 159 | 160 | # NCrunch 161 | _NCrunch_* 162 | .*crunch*.local.xml 163 | nCrunchTemp_* 164 | 165 | # MightyMoose 166 | *.mm.* 167 | AutoTest.Net/ 168 | 169 | # Web workbench (sass) 170 | .sass-cache/ 171 | 172 | # Installshield output folder 173 | [Ee]xpress/ 174 | 175 | # DocProject is a documentation generator add-in 176 | DocProject/buildhelp/ 177 | DocProject/Help/*.HxT 178 | DocProject/Help/*.HxC 179 | DocProject/Help/*.hhc 180 | DocProject/Help/*.hhk 181 | DocProject/Help/*.hhp 182 | DocProject/Help/Html2 183 | DocProject/Help/html 184 | 185 | # Click-Once directory 186 | publish/ 187 | 188 | # Publish Web Output 189 | *.[Pp]ublish.xml 190 | *.azurePubxml 191 | # Note: Comment the next line if you want to checkin your web deploy settings, 192 | # but database connection strings (with potential passwords) will be unencrypted 193 | *.pubxml 194 | *.publishproj 195 | 196 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 197 | # checkin your Azure Web App publish settings, but sensitive information contained 198 | # in these scripts will be unencrypted 199 | PublishScripts/ 200 | 201 | # NuGet Packages 202 | *.nupkg 203 | # NuGet Symbol Packages 204 | *.snupkg 205 | # The packages folder can be ignored because of Package Restore 206 | **/[Pp]ackages/* 207 | # except build/, which is used as an MSBuild target. 208 | !**/[Pp]ackages/build/ 209 | # Uncomment if necessary however generally it will be regenerated when needed 210 | #!**/[Pp]ackages/repositories.config 211 | # NuGet v3's project.json files produces more ignorable files 212 | *.nuget.props 213 | *.nuget.targets 214 | 215 | # Microsoft Azure Build Output 216 | csx/ 217 | *.build.csdef 218 | 219 | # Microsoft Azure Emulator 220 | ecf/ 221 | rcf/ 222 | 223 | # Windows Store app package directories and files 224 | AppPackages/ 225 | BundleArtifacts/ 226 | Package.StoreAssociation.xml 227 | _pkginfo.txt 228 | *.appx 229 | *.appxbundle 230 | *.appxupload 231 | 232 | # Visual Studio cache files 233 | # files ending in .cache can be ignored 234 | *.[Cc]ache 235 | # but keep track of directories ending in .cache 236 | !?*.[Cc]ache/ 237 | 238 | # Others 239 | ClientBin/ 240 | ~$* 241 | *~ 242 | *.dbmdl 243 | *.dbproj.schemaview 244 | *.jfm 245 | *.pfx 246 | *.publishsettings 247 | orleans.codegen.cs 248 | 249 | # Including strong name files can present a security risk 250 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 251 | #*.snk 252 | 253 | # Since there are multiple workflows, uncomment next line to ignore bower_components 254 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 255 | #bower_components/ 256 | 257 | # RIA/Silverlight projects 258 | Generated_Code/ 259 | 260 | # Backup & report files from converting an old project file 261 | # to a newer Visual Studio version. Backup files are not needed, 262 | # because we have git ;-) 263 | _UpgradeReport_Files/ 264 | Backup*/ 265 | UpgradeLog*.XML 266 | UpgradeLog*.htm 267 | ServiceFabricBackup/ 268 | *.rptproj.bak 269 | 270 | # SQL Server files 271 | *.mdf 272 | *.ldf 273 | *.ndf 274 | 275 | # Business Intelligence projects 276 | *.rdl.data 277 | *.bim.layout 278 | *.bim_*.settings 279 | *.rptproj.rsuser 280 | *- [Bb]ackup.rdl 281 | *- [Bb]ackup ([0-9]).rdl 282 | *- [Bb]ackup ([0-9][0-9]).rdl 283 | 284 | # Microsoft Fakes 285 | FakesAssemblies/ 286 | 287 | # GhostDoc plugin setting file 288 | *.GhostDoc.xml 289 | 290 | # Node.js Tools for Visual Studio 291 | .ntvs_analysis.dat 292 | node_modules/ 293 | 294 | # Visual Studio 6 build log 295 | *.plg 296 | 297 | # Visual Studio 6 workspace options file 298 | *.opt 299 | 300 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 301 | *.vbw 302 | 303 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 304 | *.vbp 305 | 306 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 307 | *.dsw 308 | *.dsp 309 | 310 | # Visual Studio 6 technical files 311 | *.ncb 312 | *.aps 313 | 314 | # Visual Studio LightSwitch build output 315 | **/*.HTMLClient/GeneratedArtifacts 316 | **/*.DesktopClient/GeneratedArtifacts 317 | **/*.DesktopClient/ModelManifest.xml 318 | **/*.Server/GeneratedArtifacts 319 | **/*.Server/ModelManifest.xml 320 | _Pvt_Extensions 321 | 322 | # Paket dependency manager 323 | .paket/paket.exe 324 | paket-files/ 325 | 326 | # FAKE - F# Make 327 | .fake/ 328 | 329 | # CodeRush personal settings 330 | .cr/personal 331 | 332 | # Python Tools for Visual Studio (PTVS) 333 | __pycache__/ 334 | *.pyc 335 | 336 | # Cake - Uncomment if you are using it 337 | # tools/** 338 | # !tools/packages.config 339 | 340 | # Tabs Studio 341 | *.tss 342 | 343 | # Telerik's JustMock configuration file 344 | *.jmconfig 345 | 346 | # BizTalk build output 347 | *.btp.cs 348 | *.btm.cs 349 | *.odx.cs 350 | *.xsd.cs 351 | 352 | # OpenCover UI analysis results 353 | OpenCover/ 354 | 355 | # Azure Stream Analytics local run output 356 | ASALocalRun/ 357 | 358 | # MSBuild Binary and Structured Log 359 | *.binlog 360 | 361 | # NVidia Nsight GPU debugger configuration file 362 | *.nvuser 363 | 364 | # MFractors (Xamarin productivity tool) working folder 365 | .mfractor/ 366 | 367 | # Local History for Visual Studio 368 | .localhistory/ 369 | 370 | # Visual Studio History (VSHistory) files 371 | .vshistory/ 372 | 373 | # BeatPulse healthcheck temp database 374 | healthchecksdb 375 | 376 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 377 | MigrationBackup/ 378 | 379 | # Ionide (cross platform F# VS Code tools) working folder 380 | .ionide/ 381 | 382 | # Fody - auto-generated XML schema 383 | FodyWeavers.xsd 384 | 385 | # VS Code files for those working on multiple tools 386 | .vscode/* 387 | !.vscode/settings.json 388 | !.vscode/tasks.json 389 | !.vscode/launch.json 390 | !.vscode/extensions.json 391 | *.code-workspace 392 | 393 | # Local History for Visual Studio Code 394 | .history/ 395 | 396 | # Windows Installer files from build outputs 397 | *.cab 398 | *.msi 399 | *.msix 400 | *.msm 401 | *.msp 402 | 403 | # JetBrains Rider 404 | *.sln.iml 405 | .idea 406 | 407 | ## 408 | ## Visual studio for Mac 409 | ## 410 | 411 | 412 | # globs 413 | Makefile.in 414 | *.userprefs 415 | *.usertasks 416 | config.make 417 | config.status 418 | aclocal.m4 419 | install-sh 420 | autom4te.cache/ 421 | *.tar.gz 422 | tarballs/ 423 | test-results/ 424 | 425 | # Mac bundle stuff 426 | *.dmg 427 | *.app 428 | 429 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 430 | # General 431 | .DS_Store 432 | .AppleDouble 433 | .LSOverride 434 | 435 | # Icon must end with two \r 436 | Icon 437 | 438 | 439 | # Thumbnails 440 | ._* 441 | 442 | # Files that might appear in the root of a volume 443 | .DocumentRevisions-V100 444 | .fseventsd 445 | .Spotlight-V100 446 | .TemporaryItems 447 | .Trashes 448 | .VolumeIcon.icns 449 | .com.apple.timemachine.donotpresent 450 | 451 | # Directories potentially created on remote AFP share 452 | .AppleDB 453 | .AppleDesktop 454 | Network Trash Folder 455 | Temporary Items 456 | .apdisk 457 | 458 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 459 | # Windows thumbnail cache files 460 | Thumbs.db 461 | ehthumbs.db 462 | ehthumbs_vista.db 463 | 464 | # Dump file 465 | *.stackdump 466 | 467 | # Folder config file 468 | [Dd]esktop.ini 469 | 470 | # Recycle Bin used on file shares 471 | $RECYCLE.BIN/ 472 | 473 | # Windows Installer files 474 | *.cab 475 | *.msi 476 | *.msix 477 | *.msm 478 | *.msp 479 | 480 | # Windows shortcuts 481 | *.lnk 482 | 483 | # Vim temporary swap files 484 | *.swp 485 | -------------------------------------------------------------------------------- /YuckLS/Core/YuckCheck.cs: -------------------------------------------------------------------------------- 1 | namespace YuckLS.Core; 2 | 3 | using YuckLS.Handlers; 4 | using YuckLS.Core.Models; 5 | using YuckLS.Services; 6 | using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; 7 | //extremely experimental syntax checker 8 | internal sealed class YuckCheck(string _text, Microsoft.Extensions.Logging.ILogger _logger, IEwwWorkspace _workspace) 9 | { 10 | private readonly SExpression _sExpression = new(_text, _logger, _workspace); 11 | //thread safe collection 12 | private System.Collections.Concurrent.ConcurrentBag _diagnostics = new(); 13 | public async Task> TryGetDiagnosticsAsync(CancellationToken ctx) 14 | { 15 | List diagnosticTasks = new(){ 16 | Task.Run(() => { 17 | GetBracketPairsErrors(ctx); 18 | }), 19 | Task.Run(()=>{ 20 | GetUnkownTypeErrors(ctx); 21 | }), 22 | Task.Run(()=>{ 23 | GetInvalidTopLevelDefinitionErrors(ctx); 24 | }), 25 | Task.Run(()=>{ 26 | GetInvalidPropertyDefinition(ctx); 27 | }), 28 | Task.Run(()=>{ 29 | GetInvalidPropertyValuesErrors(ctx); 30 | }) 31 | }; 32 | await Task.WhenAll(diagnosticTasks); 33 | return _diagnostics.ToList(); 34 | } 35 | private void GetBracketPairsErrors(CancellationToken ctx) 36 | { 37 | if (ctx.IsCancellationRequested) return; 38 | var unclosedBrackets = _sExpression.CheckBracketPairs(); 39 | foreach (int pos in unclosedBrackets) 40 | { 41 | if (ctx.IsCancellationRequested) return; 42 | _diagnostics.Add(new() 43 | { 44 | Range = new Range(convertIndexToPosition(pos), convertIndexToPosition(pos + 1)), 45 | Severity = DiagnosticSeverity.Error, 46 | Message = "Unmatched bracket pair.", 47 | }); 48 | //we need to convert the pos to a range. 49 | } 50 | } 51 | 52 | private void GetUnkownTypeErrors(CancellationToken ctx) 53 | { 54 | if (ctx.IsCancellationRequested) return; 55 | var nodes = _sExpression.GetAllNodes(); 56 | foreach (var node in nodes) 57 | { 58 | if (ctx.IsCancellationRequested) return; 59 | var typeCollection = YuckTypesProvider.YuckTypes.Concat(_workspace.UserDefinedTypes).ToArray(); 60 | //check if this node exists in the collection 61 | if (typeCollection.Where(p => p.name == node.nodeName).Count() != 0) continue; 62 | 63 | _diagnostics.Add(new() 64 | { 65 | Range = new Range(convertIndexToPosition(node.index), convertIndexToPosition(node.index + node.nodeName.Length)), 66 | Severity = DiagnosticSeverity.Error, 67 | Message = $"Type or widget '{node.nodeName}' does not exist. This may cause issues. ", 68 | }); 69 | } 70 | } 71 | //will probably fuse this into a generic function that just looks for definitions in the wrong place in general 72 | private void GetInvalidTopLevelDefinitionErrors(CancellationToken ctx) 73 | { 74 | if (ctx.IsCancellationRequested) return; 75 | var nodes = _sExpression.GetAllNodes(); 76 | //plan is to split the text at the index of each node and check is the first half would be able to declare a top level widget 77 | foreach (var node in nodes) 78 | { 79 | if (ctx.IsCancellationRequested) return; 80 | var yuckType = YuckTypesProvider.YuckTypes.Concat(_workspace.UserDefinedTypes).Where(p => p.name == node.nodeName).FirstOrDefault(); 81 | if (yuckType is null) continue; 82 | //split the string, 83 | string part1 = _sExpression.FullText.Substring(0, node.index); 84 | //create a new sexpression 85 | var sexpression = new SExpression(part1, _logger, _workspace); 86 | //check that is an invalid top level declaration 87 | if (sexpression.IsTopLevel() && !yuckType.IsTopLevel) 88 | { 89 | _diagnostics.Add(new() 90 | { 91 | Range = new Range(convertIndexToPosition(node.index), convertIndexToPosition(node.index + node.nodeName.Length)), 92 | Message = $"Did not expect '{node.nodeName}' here, expected top level declaration: defwindow, defwidget, defpoll, defvar, include, deflisten.", 93 | Severity = DiagnosticSeverity.Error 94 | }); 95 | } 96 | else if (!sexpression.IsTopLevel() && yuckType.IsTopLevel) 97 | { 98 | _diagnostics.Add(new() 99 | { 100 | Range = new Range(convertIndexToPosition(node.index), convertIndexToPosition(node.index + node.nodeName.Length)), 101 | Message = $"'{node.nodeName}' should be declared at the top level and not nested within another declaration", 102 | Severity = DiagnosticSeverity.Error 103 | }); 104 | } 105 | } 106 | } 107 | private void GetInvalidPropertyDefinition(CancellationToken ctx) 108 | { 109 | if (ctx.IsCancellationRequested) return; 110 | var properties = _sExpression.GetAllProperyDefinitions(); 111 | //split the text at the index of each property, get the parent type and check if that type has a defintion for this property 112 | foreach (var property in properties) 113 | { 114 | if (ctx.IsCancellationRequested) return; 115 | var part1 = _sExpression.FullText.Substring(0, property.index); 116 | //create a new sexpression 117 | var sexpression = new SExpression(part1, _logger, _workspace); 118 | string? parentNode = sexpression.GetParentNode(); 119 | if (parentNode is null) continue; 120 | var parentType = YuckTypesProvider.YuckTypes.Concat(_workspace.UserDefinedTypes).Where(p => p.name == parentNode).FirstOrDefault(); 121 | if (parentType is null) continue; 122 | 123 | //try to get property, 124 | var realProperty = parentType.properties.Where(p => p.name == property.propertyName).FirstOrDefault(); 125 | if (realProperty is null) 126 | { 127 | _diagnostics.Add(new() 128 | { 129 | Range = new Range(convertIndexToPosition(property.index), convertIndexToPosition(property.index + property.propertyName.Length)), 130 | Message = $"'{parentType.name}' does not contain a definition for {property.propertyName}", 131 | Severity = DiagnosticSeverity.Error 132 | }); 133 | } 134 | } 135 | } 136 | 137 | private void GetInvalidPropertyValuesErrors(CancellationToken ctx) 138 | { 139 | if (ctx.IsCancellationRequested) return; 140 | var propertyValues = _sExpression.GetAllPropertyValues(); 141 | foreach (var propertyValue in propertyValues) 142 | { 143 | if (ctx.IsCancellationRequested) return; 144 | //GET PARENT NODE AND PROPERTY 145 | var part1 = _sExpression.FullText.Substring(0, propertyValue.index); 146 | //create a new sexpression 147 | var sexpression = new SExpression(part1, _logger, _workspace); 148 | string? parentNode = sexpression.GetParentNode(); 149 | string? parentProperty = propertyValue.property; 150 | 151 | if (parentNode is null || parentProperty is null) continue; 152 | 153 | var parentType = YuckTypesProvider.YuckTypes.Concat(_workspace.UserDefinedTypes).Where(p => p.name == parentNode).FirstOrDefault(); 154 | if (parentType is null) continue; 155 | 156 | var parentPropertyType = parentType.properties.Where(p => p.name == parentProperty).FirstOrDefault(); 157 | if (parentPropertyType is null) continue; 158 | 159 | var diagnosticsRange = new Range(convertIndexToPosition(propertyValue.index), convertIndexToPosition(propertyValue.index + propertyValue.propertyValue.Length)); 160 | string readableExpectedType = ""; 161 | switch (parentPropertyType.dataType) 162 | { 163 | case YuckDataType.YuckString: 164 | readableExpectedType = "string"; 165 | break; 166 | case YuckDataType.YuckInt: 167 | readableExpectedType = "int"; 168 | break; 169 | case YuckDataType.YuckBool: 170 | readableExpectedType = "bool"; 171 | break; 172 | case YuckDataType.YuckDuration: 173 | readableExpectedType = "duration"; 174 | break; 175 | case YuckDataType.YuckFloat: 176 | readableExpectedType = "float"; 177 | break; 178 | default: 179 | readableExpectedType = "custom"; 180 | break; 181 | } 182 | // _logger.LogError($"property is {parentProperty} and property value is {propertyValue.propertyValue}"); 183 | //wildcard, every property should be able to take a user defined variable although with a warning that the type might not match; 184 | if (_workspace.UserDefinedVariables.Where(p => p.name == propertyValue.propertyValue).Count() != 0) 185 | { 186 | _diagnostics.Add(new() 187 | { 188 | Range = diagnosticsRange, 189 | Message = $"{parentProperty} expects {readableExpectedType} here but got a variable instead. This might cause issues ", 190 | Severity = DiagnosticSeverity.Warning 191 | }); 192 | continue; 193 | } 194 | if (parentPropertyType.dataType == YuckDataType.YuckString) 195 | { 196 | //should have single or double quotes on either side. Pretty easy 197 | if ((propertyValue.propertyValue.First() == '\"' && propertyValue.propertyValue.Last() == '\"') 198 | || (propertyValue.propertyValue.First() == '\'' && propertyValue.propertyValue.Last() == '\'') 199 | ) 200 | { 201 | continue; 202 | } 203 | _diagnostics.Add(new() 204 | { 205 | Range = diagnosticsRange, 206 | Message = $"{parentProperty} expects type string here but string not given", 207 | Severity = DiagnosticSeverity.Warning 208 | }); 209 | } 210 | else if (parentPropertyType.dataType == YuckDataType.YuckInt) 211 | { 212 | //try to parse this as an int, 213 | if (!int.TryParse(propertyValue.propertyValue, out int placeholder)) 214 | { 215 | _diagnostics.Add(new() 216 | { 217 | Range = diagnosticsRange, 218 | Message = $"{parentProperty} expects type int, but int not given", 219 | Severity = DiagnosticSeverity.Warning 220 | }); 221 | } 222 | } 223 | else if (parentPropertyType.dataType == YuckDataType.YuckBool) 224 | { 225 | //try to parse as a bool 226 | if (!bool.TryParse(propertyValue.propertyValue, out bool placeholder)) 227 | { 228 | _diagnostics.Add(new() 229 | { 230 | Range = diagnosticsRange, 231 | Message = $"{parentProperty} expects type bool, but bool not given", 232 | Severity = DiagnosticSeverity.Warning 233 | }); 234 | } 235 | } 236 | else if (parentPropertyType.dataType == YuckDataType.YuckFloat) 237 | { 238 | //try to parse as a float 239 | if (!float.TryParse(propertyValue.propertyValue, out float placeholder)) 240 | { 241 | _diagnostics.Add(new() 242 | { 243 | Range = diagnosticsRange, 244 | Message = $"{parentProperty} expects type float, but float not given", 245 | Severity = DiagnosticSeverity.Warning 246 | }); 247 | } 248 | } 249 | else if (parentPropertyType.dataType == YuckDataType.YuckDuration) 250 | { 251 | //figure thi out later 252 | } 253 | else 254 | { 255 | //wildcard, do nothing for now 256 | } 257 | } 258 | } 259 | 260 | //i dont fully understand how this method works 261 | private Position convertIndexToPosition(int pos) 262 | { 263 | //we need to somehow convert an index of the text to a position... after removing comments and string somehow 264 | int lineNumber = 0; 265 | int position = 0; 266 | int index = 0; 267 | 268 | foreach (var line in _text.Split(Environment.NewLine)) 269 | { 270 | //check if position is in range of current line 271 | int lineLength = line.Length + 1; 272 | if (index + lineLength > pos) 273 | { 274 | position = pos - index + 1; 275 | break; 276 | } 277 | 278 | index += lineLength; 279 | lineNumber++; 280 | } 281 | return new Position(lineNumber, position); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # All files 4 | [*] 5 | indent_style = space 6 | 7 | # Xml files 8 | [*.xml] 9 | indent_size = 2 10 | 11 | # C# files 12 | [*.cs] 13 | 14 | #### Core EditorConfig Options #### 15 | 16 | # Indentation and spacing 17 | indent_size = 4 18 | tab_width = 4 19 | 20 | # New line preferences 21 | end_of_line = crlf 22 | insert_final_newline = false 23 | 24 | #### .NET Coding Conventions #### 25 | [*.{cs,vb}] 26 | 27 | # Organize usings 28 | dotnet_separate_import_directive_groups = true 29 | dotnet_sort_system_directives_first = true 30 | file_header_template = unset 31 | 32 | # this. and Me. preferences 33 | dotnet_style_qualification_for_event = false:silent 34 | dotnet_style_qualification_for_field = false:silent 35 | dotnet_style_qualification_for_method = false:silent 36 | dotnet_style_qualification_for_property = false:silent 37 | 38 | # Language keywords vs BCL types preferences 39 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 40 | dotnet_style_predefined_type_for_member_access = true:silent 41 | 42 | # Parentheses preferences 43 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 44 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 45 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 46 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 47 | 48 | # Modifier preferences 49 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 50 | 51 | # Expression-level preferences 52 | dotnet_style_coalesce_expression = true:suggestion 53 | dotnet_style_collection_initializer = true:suggestion 54 | dotnet_style_explicit_tuple_names = true:suggestion 55 | dotnet_style_null_propagation = true:suggestion 56 | dotnet_style_object_initializer = true:suggestion 57 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 58 | dotnet_style_prefer_auto_properties = true:suggestion 59 | dotnet_style_prefer_compound_assignment = true:suggestion 60 | dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion 61 | dotnet_style_prefer_conditional_expression_over_return = true:suggestion 62 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 63 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 64 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 65 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 66 | dotnet_style_prefer_simplified_interpolation = true:suggestion 67 | 68 | # Field preferences 69 | dotnet_style_readonly_field = true:warning 70 | 71 | # Parameter preferences 72 | dotnet_code_quality_unused_parameters = all:suggestion 73 | 74 | # Suppression preferences 75 | dotnet_remove_unnecessary_suppression_exclusions = none 76 | 77 | #### C# Coding Conventions #### 78 | [*.cs] 79 | 80 | # var preferences 81 | csharp_style_var_elsewhere = false:silent 82 | csharp_style_var_for_built_in_types = false:silent 83 | csharp_style_var_when_type_is_apparent = false:silent 84 | 85 | # Expression-bodied members 86 | csharp_style_expression_bodied_accessors = true:silent 87 | csharp_style_expression_bodied_constructors = false:silent 88 | csharp_style_expression_bodied_indexers = true:silent 89 | csharp_style_expression_bodied_lambdas = true:suggestion 90 | csharp_style_expression_bodied_local_functions = false:silent 91 | csharp_style_expression_bodied_methods = false:silent 92 | csharp_style_expression_bodied_operators = false:silent 93 | csharp_style_expression_bodied_properties = true:silent 94 | 95 | # Pattern matching preferences 96 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 97 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 98 | csharp_style_prefer_not_pattern = true:suggestion 99 | csharp_style_prefer_pattern_matching = true:silent 100 | csharp_style_prefer_switch_expression = true:suggestion 101 | 102 | # Null-checking preferences 103 | csharp_style_conditional_delegate_call = true:suggestion 104 | 105 | # Modifier preferences 106 | csharp_prefer_static_local_function = true:warning 107 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent 108 | 109 | # Code-block preferences 110 | csharp_prefer_braces = true:silent 111 | csharp_prefer_simple_using_statement = true:suggestion 112 | 113 | # Expression-level preferences 114 | csharp_prefer_simple_default_expression = true:suggestion 115 | csharp_style_deconstructed_variable_declaration = true:suggestion 116 | csharp_style_inlined_variable_declaration = true:suggestion 117 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 118 | csharp_style_prefer_index_operator = true:suggestion 119 | csharp_style_prefer_range_operator = true:suggestion 120 | csharp_style_throw_expression = true:suggestion 121 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 122 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 123 | 124 | # 'using' directive preferences 125 | csharp_using_directive_placement = outside_namespace:silent 126 | 127 | #### C# Formatting Rules #### 128 | 129 | # New line preferences 130 | csharp_new_line_before_catch = true 131 | csharp_new_line_before_else = true 132 | csharp_new_line_before_finally = true 133 | csharp_new_line_before_members_in_anonymous_types = true 134 | csharp_new_line_before_members_in_object_initializers = true 135 | csharp_new_line_before_open_brace = all 136 | csharp_new_line_between_query_expression_clauses = true 137 | 138 | # Indentation preferences 139 | csharp_indent_block_contents = true 140 | csharp_indent_braces = false 141 | csharp_indent_case_contents = true 142 | csharp_indent_case_contents_when_block = true 143 | csharp_indent_labels = one_less_than_current 144 | csharp_indent_switch_labels = true 145 | 146 | # Space preferences 147 | csharp_space_after_cast = false 148 | csharp_space_after_colon_in_inheritance_clause = true 149 | csharp_space_after_comma = true 150 | csharp_space_after_dot = false 151 | csharp_space_after_keywords_in_control_flow_statements = true 152 | csharp_space_after_semicolon_in_for_statement = true 153 | csharp_space_around_binary_operators = before_and_after 154 | csharp_space_around_declaration_statements = false 155 | csharp_space_before_colon_in_inheritance_clause = true 156 | csharp_space_before_comma = false 157 | csharp_space_before_dot = false 158 | csharp_space_before_open_square_brackets = false 159 | csharp_space_before_semicolon_in_for_statement = false 160 | csharp_space_between_empty_square_brackets = false 161 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 162 | csharp_space_between_method_call_name_and_opening_parenthesis = false 163 | csharp_space_between_method_call_parameter_list_parentheses = false 164 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 165 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 166 | csharp_space_between_method_declaration_parameter_list_parentheses = false 167 | csharp_space_between_parentheses = false 168 | csharp_space_between_square_brackets = false 169 | 170 | # Wrapping preferences 171 | csharp_preserve_single_line_blocks = true 172 | csharp_preserve_single_line_statements = true 173 | 174 | #### Naming styles #### 175 | [*.{cs,vb}] 176 | 177 | # Naming rules 178 | 179 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion 180 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces 181 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase 182 | 183 | dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion 184 | dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces 185 | dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase 186 | 187 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion 188 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters 189 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase 190 | 191 | dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion 192 | dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods 193 | dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase 194 | 195 | dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion 196 | dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties 197 | dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase 198 | 199 | dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion 200 | dotnet_naming_rule.events_should_be_pascalcase.symbols = events 201 | dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase 202 | 203 | dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion 204 | dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables 205 | dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase 206 | 207 | dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion 208 | dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants 209 | dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase 210 | 211 | dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion 212 | dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters 213 | dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase 214 | 215 | dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion 216 | dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields 217 | dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase 218 | 219 | dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion 220 | dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields 221 | dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase 222 | 223 | dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion 224 | dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields 225 | dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase 226 | 227 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion 228 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields 229 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase 230 | 231 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion 232 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields 233 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase 234 | 235 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion 236 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields 237 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase 238 | 239 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion 240 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields 241 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase 242 | 243 | dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion 244 | dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums 245 | dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase 246 | 247 | dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion 248 | dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions 249 | dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase 250 | 251 | dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion 252 | dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members 253 | dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase 254 | 255 | # Symbol specifications 256 | 257 | dotnet_naming_symbols.interfaces.applicable_kinds = interface 258 | dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 259 | dotnet_naming_symbols.interfaces.required_modifiers = 260 | 261 | dotnet_naming_symbols.enums.applicable_kinds = enum 262 | dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 263 | dotnet_naming_symbols.enums.required_modifiers = 264 | 265 | dotnet_naming_symbols.events.applicable_kinds = event 266 | dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 267 | dotnet_naming_symbols.events.required_modifiers = 268 | 269 | dotnet_naming_symbols.methods.applicable_kinds = method 270 | dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 271 | dotnet_naming_symbols.methods.required_modifiers = 272 | 273 | dotnet_naming_symbols.properties.applicable_kinds = property 274 | dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 275 | dotnet_naming_symbols.properties.required_modifiers = 276 | 277 | dotnet_naming_symbols.public_fields.applicable_kinds = field 278 | dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal 279 | dotnet_naming_symbols.public_fields.required_modifiers = 280 | 281 | dotnet_naming_symbols.private_fields.applicable_kinds = field 282 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 283 | dotnet_naming_symbols.private_fields.required_modifiers = 284 | 285 | dotnet_naming_symbols.private_static_fields.applicable_kinds = field 286 | dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 287 | dotnet_naming_symbols.private_static_fields.required_modifiers = static 288 | 289 | dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum 290 | dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 291 | dotnet_naming_symbols.types_and_namespaces.required_modifiers = 292 | 293 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 294 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 295 | dotnet_naming_symbols.non_field_members.required_modifiers = 296 | 297 | dotnet_naming_symbols.type_parameters.applicable_kinds = namespace 298 | dotnet_naming_symbols.type_parameters.applicable_accessibilities = * 299 | dotnet_naming_symbols.type_parameters.required_modifiers = 300 | 301 | dotnet_naming_symbols.private_constant_fields.applicable_kinds = field 302 | dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 303 | dotnet_naming_symbols.private_constant_fields.required_modifiers = const 304 | 305 | dotnet_naming_symbols.local_variables.applicable_kinds = local 306 | dotnet_naming_symbols.local_variables.applicable_accessibilities = local 307 | dotnet_naming_symbols.local_variables.required_modifiers = 308 | 309 | dotnet_naming_symbols.local_constants.applicable_kinds = local 310 | dotnet_naming_symbols.local_constants.applicable_accessibilities = local 311 | dotnet_naming_symbols.local_constants.required_modifiers = const 312 | 313 | dotnet_naming_symbols.parameters.applicable_kinds = parameter 314 | dotnet_naming_symbols.parameters.applicable_accessibilities = * 315 | dotnet_naming_symbols.parameters.required_modifiers = 316 | 317 | dotnet_naming_symbols.public_constant_fields.applicable_kinds = field 318 | dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal 319 | dotnet_naming_symbols.public_constant_fields.required_modifiers = const 320 | 321 | dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field 322 | dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal 323 | dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static 324 | 325 | dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field 326 | dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 327 | dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static 328 | 329 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 330 | dotnet_naming_symbols.local_functions.applicable_accessibilities = * 331 | dotnet_naming_symbols.local_functions.required_modifiers = 332 | 333 | # Naming styles 334 | 335 | dotnet_naming_style.pascalcase.required_prefix = 336 | dotnet_naming_style.pascalcase.required_suffix = 337 | dotnet_naming_style.pascalcase.word_separator = 338 | dotnet_naming_style.pascalcase.capitalization = pascal_case 339 | 340 | dotnet_naming_style.ipascalcase.required_prefix = I 341 | dotnet_naming_style.ipascalcase.required_suffix = 342 | dotnet_naming_style.ipascalcase.word_separator = 343 | dotnet_naming_style.ipascalcase.capitalization = pascal_case 344 | 345 | dotnet_naming_style.tpascalcase.required_prefix = T 346 | dotnet_naming_style.tpascalcase.required_suffix = 347 | dotnet_naming_style.tpascalcase.word_separator = 348 | dotnet_naming_style.tpascalcase.capitalization = pascal_case 349 | 350 | dotnet_naming_style._camelcase.required_prefix = _ 351 | dotnet_naming_style._camelcase.required_suffix = 352 | dotnet_naming_style._camelcase.word_separator = 353 | dotnet_naming_style._camelcase.capitalization = camel_case 354 | 355 | dotnet_naming_style.camelcase.required_prefix = 356 | dotnet_naming_style.camelcase.required_suffix = 357 | dotnet_naming_style.camelcase.word_separator = 358 | dotnet_naming_style.camelcase.capitalization = camel_case 359 | 360 | dotnet_naming_style.s_camelcase.required_prefix = s_ 361 | dotnet_naming_style.s_camelcase.required_suffix = 362 | dotnet_naming_style.s_camelcase.word_separator = 363 | dotnet_naming_style.s_camelcase.capitalization = camel_case 364 | 365 | -------------------------------------------------------------------------------- /YuckLS/Core/SExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using YuckLS.Core.Models; 3 | using System.Runtime.CompilerServices; 4 | using YuckLS.Services; 5 | [assembly: InternalsVisibleTo("YuckLS.Test")] 6 | namespace YuckLS.Core; 7 | //very raw string parsing. Eventually when i get the time to learn how, ill use modern approaches like tokenization 8 | internal class SExpression 9 | { 10 | private readonly string _text; 11 | //completion text refers to chopped text at the cursor position for completion. 12 | private readonly string _completionText; 13 | public string FullText => _fullText; 14 | //full text refers to text used for getting variables and diagnostics 15 | private readonly string _fullText; 16 | 17 | private readonly ILogger _logger; 18 | private IEwwWorkspace _workspace; 19 | public SExpression(string _text, ILogger _logger, IEwwWorkspace _workspace) 20 | { 21 | this._text = _text; 22 | this._logger = _logger; 23 | _completionText = this._text; 24 | _fullText = this._text; 25 | //recursively delete char in quotes to prevent interferance 26 | _completionText = RemoveAllQuotedChars(_completionText); 27 | _fullText = RemoveAllQuotedChars(_fullText); 28 | //delete comments from text to prevent interferance, this must be done after characters in quotes have been removed or completer might break and pop last char from text(the completion trigger) 29 | if (_completionText.Length > 0) 30 | { 31 | _completionText = RemoveComments(_completionText)[..^1]; 32 | _fullText = RemoveComments(_fullText); 33 | } 34 | this._workspace = _workspace; 35 | } 36 | 37 | 38 | private const char _openTag = '('; 39 | private const char _OpenProperties = ':'; 40 | private const char _openCompleterForProperties = ' '; 41 | /// 42 | ///Try to get a completion trigger from the end of the string which is where the cursor would be. 43 | ///From what i've understood for Elkowar's docs, at least 2 different inputs should trigger completion. 44 | ///1.) Open S-Expression like so : '(', should complete with valid widget types like Box, Window, Expression Types like defpoll, defvar ,e.t.c 45 | ///2.) Creating properties like so: ':' should complete with valid properties of a containing S-Expression tag e.g (defwindow :) should autocomplete with properties like :monitor :stacking e.t.c or propertied of the monitor widget 46 | /// 47 | public YuckCompletionContext? TryGetCompletionContext() 48 | { 49 | if (_text.Last() == _openTag) 50 | { 51 | //if user's cursor is about to create a top level tag 52 | if (IsTopLevel()) return new TopLevelYuckCompletionContext(); 53 | //a parent node must exist if the cursor is not top level 54 | string? parentNode = GetParentNode(); 55 | //lookup the parentNode in yuck types 56 | YuckType? parentType = null; 57 | foreach (var yuckType in YuckTypesProvider.YuckTypes.Concat(_workspace.UserDefinedTypes)) 58 | { 59 | if (yuckType.name == parentNode) 60 | { 61 | parentType = yuckType; 62 | } 63 | } 64 | //if parentType is still null, then parentNode is none standard. Perhaps custom widget? 65 | if (parentType == null) return default; 66 | //check if parentType supports GTK widget nodes 67 | if (parentType.AreWidgetsEmbeddable) return new WidgetYuckCompletionContext(_workspace); 68 | } 69 | 70 | 71 | else if (_text.Last() == _OpenProperties) 72 | { 73 | //try to get the parentNode 74 | string? parentNode = GetParentNode(); 75 | if (parentNode == null) return default; 76 | 77 | //try to parse the parentNode to a yuck type. Add custom yuck types to the array. 78 | YuckType? parentType = YuckTypesProvider.YuckTypes.Concat(_workspace.UserDefinedTypes)?.Where(type => type.name == parentNode)?.FirstOrDefault(); 79 | if (parentType == null) return default; 80 | 81 | return new PropertyYuckCompletionContext() { parentType = parentType }; 82 | } 83 | 84 | 85 | else if (_text.Last() == _openCompleterForProperties) 86 | { 87 | string? parentNode = GetParentNode(); 88 | string? parentPropertyNode = GetParentProperty(); 89 | if (parentPropertyNode == null || parentNode == null) return default; 90 | 91 | YuckType? parentType = YuckTypesProvider.YuckTypes?.Where(type => type?.name == parentNode)?.FirstOrDefault(); 92 | if (parentType is null) return default; 93 | YuckProperty? parentProperty = parentType?.properties?.Where(type => type?.name == parentPropertyNode)?.FirstOrDefault(); 94 | 95 | if (parentType is null || parentProperty is null) return default; 96 | return new PropertySuggestionCompletionContext(_workspace) { parentType = parentType, parentProperty = parentProperty }; 97 | } 98 | 99 | 100 | else 101 | { } 102 | 103 | return default; 104 | } 105 | /// 106 | ///Determine is the cursor position can declare a top level widget 107 | /// 108 | 109 | internal protected bool IsTopLevel() 110 | { 111 | int depth = 0; 112 | foreach (char c in _completionText) 113 | { 114 | if (c == '(') 115 | { 116 | depth++; 117 | } 118 | if (c == ')') 119 | { 120 | depth--; 121 | } 122 | } 123 | return depth == 0; 124 | } 125 | 126 | /// 127 | ///Gets the parent node for the cursor's position. E.g (box , the parent node is box 128 | /// 129 | internal protected string? GetParentNode() 130 | { 131 | //i could not figure out how to do this in one command 132 | //recursively delete any tags that are closed even on multilines 133 | int matchCount = 0; 134 | string _cleanedText = _completionText; 135 | string patternForClosedNodes = @"\(\w+[^\(]*?\)"; 136 | do 137 | { 138 | matchCount = Regex.Matches(_cleanedText, patternForClosedNodes, RegexOptions.IgnoreCase).Count; 139 | _cleanedText = Regex.Replace(_cleanedText, patternForClosedNodes, "", RegexOptions.IgnoreCase); 140 | } while (matchCount > 0); 141 | var matches = Regex.Matches(_cleanedText, @"\([a-zA-Z0-9-_]+", RegexOptions.IgnoreCase); 142 | if (matches.Count > 0) 143 | { 144 | //trim line breaks and remove properties from node 145 | var value = matches.Last().Value.Trim().Split()[0]; 146 | if (value[0] == '(') return value.Substring(1); 147 | } 148 | return null; 149 | } 150 | /// 151 | ///Will attempt to remove all characters in quotes by looping through every character and checking if the current character can close an open open quote 152 | /// 153 | /// 154 | private string RemoveAllQuotedChars(string text) 155 | { 156 | //copy text to another variable 157 | string textCopy = text; 158 | bool hasEncounteredOpenQuote = false; 159 | int literalCount = 0; //string in literals like ${"i am a literal"} 160 | char quote = default; 161 | bool inCommentBlock = false; //whether we are in a comment block, since we are removing quotes before comments, we need to ignore any quotes in comments so that they will be removed later by the removecomments method 162 | int indexOfOpenQuote = 0; 163 | for (int i = 0; i < textCopy.Length; i++) 164 | { 165 | //if we havent found a quote opener, skip to next char 166 | if (!hasEncounteredOpenQuote) 167 | { 168 | if (text[i] == ';') 169 | { 170 | inCommentBlock = true; //we are in a comment block since we've encountered ';' without being in a quote 171 | } 172 | if (text[i] == '\n' || text[i] == '\r') 173 | { 174 | inCommentBlock = false; 175 | } 176 | if (text[i] == '\"' || text[i] == '\'' || text[i] == '`') 177 | { 178 | if(inCommentBlock) continue; //ignore everything to the end of the line 179 | hasEncounteredOpenQuote = true; 180 | quote = text[i]; 181 | indexOfOpenQuote = i; 182 | } 183 | } 184 | else 185 | { 186 | //match string literal, this might crash if text[i+1] is out of range. Will fix that. 187 | if (text[i] == '$' && text[i + 1] == '{' && text[i - 1] != '\\') 188 | { 189 | //start counting string literal 190 | literalCount++; 191 | } 192 | if (text[i] == '}' && literalCount > 0) 193 | { 194 | //we need to exit the current string literal by decrementing the literal count. This is stupid af because it will break if the closing curly bracket is escaped 195 | literalCount--; 196 | } 197 | //if the current char mathches the open quote 198 | if (text[i] == quote) 199 | { 200 | //we need to check that the current quote is not escaped to start another quote and that we are not in a string literal 201 | if (text[i - 1] == '\\' || literalCount > 0) { continue; } 202 | 203 | int lengthToReplace = (i - indexOfOpenQuote) + 1; 204 | string maskedSubstring = new string('*', lengthToReplace); 205 | textCopy = textCopy.Remove(indexOfOpenQuote, lengthToReplace) 206 | .Insert(indexOfOpenQuote, maskedSubstring); 207 | hasEncounteredOpenQuote = false; 208 | quote = default; 209 | indexOfOpenQuote = 0; 210 | } 211 | 212 | } 213 | } 214 | return textCopy; 215 | } 216 | //this used to completely remove the substring, changed it to substitute it with something else because we might need to retain the original indices 217 | private string RemoveComments(string input) 218 | { 219 | //should probably use regex for this 220 | string[] lines = input.Split(new[] { Environment.NewLine }, StringSplitOptions.None); 221 | for (int i = 0; i < lines.Length; i++) 222 | { 223 | int semicolonIndex = lines[i].IndexOf(';'); 224 | if (semicolonIndex >= 0) 225 | { 226 | int lengthToMask = lines[i].Length - semicolonIndex; 227 | lines[i] = lines[i].Substring(0, semicolonIndex) + new string('#', lengthToMask); 228 | 229 | //this was the original code which completely removed the substring 230 | // lines[i] = lines[i].Substring(0, semicolonIndex); 231 | } 232 | } 233 | return string.Join(Environment.NewLine, lines); 234 | } 235 | /// 236 | ///Pretty much just do some simple string parsing to get the property on which the completion request was invoked on. e.g (box :height , height would be the property. 237 | /// 238 | internal protected string? GetParentProperty() 239 | { 240 | //select last line 241 | string lastLine = _completionText.Split(Environment.NewLine).Last(); 242 | string lastNode = lastLine.Split(" ").Last().Trim(); 243 | 244 | if (lastNode is not null && lastNode.Length > 0 && lastNode[0] == ':') return lastNode[1..].Trim(); 245 | return null; 246 | } 247 | 248 | internal protected (List customWidgets, List customVariables) GetVariables() 249 | { 250 | //remove comments and strings from text 251 | List customYuckTypes = new(); 252 | List customVariables = new(); 253 | var text = _fullText; 254 | string varDefPatterns = @"\((deflisten|defpoll|defvar|defwidget)[^)]*\)"; 255 | //string varDefPatterns = @"\((defwidget)[^)]*\)"; 256 | var matches = Regex.Matches(text, varDefPatterns); 257 | 258 | //go through matches 259 | foreach (var match in matches.ToArray()) 260 | { 261 | var varDef = match.Value; 262 | var varType = varDef.Split(" ")[0].Trim(); //could be (deflisten, (defpoll , (defvar , e.t.c 263 | // if (varType != "(defwidget") continue; 264 | var varDefSplit = varDef.Split(" "); 265 | string? typeName = null; 266 | List widgetProperties = new(); 267 | //widgetname would just be the first text 268 | foreach (string prop in varDefSplit) 269 | { 270 | //_logger.LogError(prop); 271 | //we are looking to the first lone text. Strings have already been deleted. 272 | if (prop.Length < 1 || prop.Trim()[0] == '[' || prop.Trim()[0] == '(' || prop == null) continue; 273 | 274 | typeName = prop.Split("[")[0]; 275 | //break because we only need one match 276 | break; 277 | } 278 | if (typeName is null) continue; 279 | //if the type is not a widget defination we just stop here and return it. 280 | if (varType != "(defwidget") 281 | { 282 | string variableType = ""; 283 | switch (varType) 284 | { 285 | case "(deflisten": 286 | variableType = "Listener"; 287 | break; 288 | case "(defpoll": 289 | variableType = "Poll"; 290 | break; 291 | case "(defvar": 292 | variableType = "Variable"; 293 | break; 294 | default: break; 295 | } 296 | customVariables.Add(new() 297 | { 298 | name = typeName, 299 | description = $"A custom ${variableType}" 300 | }); 301 | continue; 302 | } 303 | //now to find widget properties. I could probably do this in the loop above but i dont want to confuse myself 304 | var indexOfPropertiesOpener = varDef.IndexOf("["); 305 | var indexOfPropertiesCloser = varDef.IndexOf("]"); 306 | if (indexOfPropertiesOpener == -1 || indexOfPropertiesCloser == -1) continue; //invalid syntax, continue to next match 307 | string propertiesSplice = varDef.Substring(indexOfPropertiesOpener + 1, indexOfPropertiesCloser - indexOfPropertiesOpener - 1); 308 | 309 | if (propertiesSplice.Trim().Length == 0) widgetProperties = new(); //no properties defined , 310 | foreach (var prop in propertiesSplice.Split(" ")) 311 | { 312 | var property = prop.Trim(); 313 | if (property == null || property.Length < 1) continue; 314 | if (property[0] == '?') property = property.Substring(1); 315 | widgetProperties.Add(new() 316 | { 317 | name = property, 318 | description = $"Custom property for {typeName}", 319 | dataType = YuckDataType.YuckString, 320 | }); 321 | } 322 | customYuckTypes.Add(new() 323 | { 324 | //split prop from [ incase user didn't space proeprties from widget name 325 | name = typeName, 326 | description = "A custom yuck type", 327 | properties = widgetProperties.ToArray(), 328 | IsUserDefined = true, 329 | AreWidgetsEmbeddable = true 330 | }); 331 | 332 | } 333 | return (customYuckTypes, customVariables); 334 | } 335 | internal protected List GetIncludes() 336 | { 337 | List results = new(); 338 | string includePatterns = @"\((include)[^)]*\)"; 339 | var text = RemoveComments(_text); 340 | var matches = Regex.Matches(text, includePatterns); 341 | if (matches.Count() == 0) return results; 342 | foreach (var match in matches.ToArray()) 343 | { 344 | //this will break very easily 345 | var indexOfQuoteOpener = match.Value.IndexOf("\""); 346 | var indexOfQuoteCloser = match.Value.IndexOf("\"", indexOfQuoteOpener + 1); 347 | 348 | if (indexOfQuoteOpener == -1 || indexOfQuoteCloser == -1) continue; 349 | string pathSlice = match.Value.Substring(indexOfQuoteOpener + 1, indexOfQuoteCloser - indexOfQuoteOpener - 1); 350 | 351 | results.Add(pathSlice); 352 | } 353 | return results; 354 | } 355 | /// 356 | ///check for any brackets that are idle 357 | /// 358 | internal protected List CheckBracketPairs() 359 | { 360 | //store the indexes of idle open or close brackets. We need to get the indices on the original char 361 | List problematicIndices = new(); 362 | Stack brackets = new(); 363 | var text = _fullText; 364 | int index = 0; 365 | foreach (char x in text) 366 | { 367 | if (x == '(') brackets.Push(index); 368 | //if we meet a closer 369 | else if (x == ')') 370 | { 371 | //if there are valid openers, remove one as we have found a match 372 | if (brackets.Count > 0) 373 | { 374 | brackets.Pop(); 375 | } 376 | 377 | //if there are no valid openers, we have encountered brackets that can never be closed 378 | else 379 | { 380 | problematicIndices.Add(index); 381 | } 382 | } 383 | index++; 384 | } 385 | foreach (int x in brackets) 386 | { 387 | if (!problematicIndices.Contains(x)) problematicIndices.Add(x); 388 | } 389 | return problematicIndices; 390 | } 391 | 392 | /// 393 | ///Get every node in the document and their position(index) 394 | /// 395 | internal protected List<(string nodeName, int index)> GetAllNodes() 396 | { 397 | //fix thid 398 | List<(string node, int index)> result = new(); 399 | string patternForNodes = @"\(\w+[^ )|^\r\n]*"; 400 | var matches = Regex.Matches(_fullText, patternForNodes).ToArray(); 401 | foreach (var match in matches) 402 | { 403 | string nodeName = match.Value[0] == '(' ? match.Value[1..] : match.Value; 404 | result.Add((nodeName.Trim(), match.Index)); 405 | } 406 | return result; 407 | } 408 | /// 409 | ///Get every property defined on a node and it's position 410 | /// 411 | internal protected List<(string propertyName, int index)> GetAllProperyDefinitions() 412 | { 413 | List<(string property, int index)> result = new(); 414 | string patternForProperties = @":[\w-]+"; 415 | var matches = Regex.Matches(_fullText, patternForProperties).ToArray(); 416 | foreach (var match in matches) 417 | { 418 | string propertyName = match.Value[0] == ':' ? match.Value[1..] : match.Value; 419 | result.Add((propertyName, match.Index)); 420 | } 421 | return result; 422 | } 423 | 424 | /// 425 | ///Get every property value and it's position 426 | /// 427 | internal protected List<(string propertyValue, string property, int index)> GetAllPropertyValues() 428 | { 429 | List<(string, string, int)> result = new(); 430 | //i'm basically copying and pasting the method above, we should find the property first and then get the next word after it. 431 | string patternForProperties = @":[\w-]+"; 432 | var matches = Regex.Matches(_fullText, patternForProperties).ToArray(); 433 | foreach (var match in matches) 434 | { 435 | //go through the chars manually and find the property value. Doing it this way because it makes it easier to map onto _text and find the true value that has not been obfuscated 436 | int startIndex = match.Value.Length + match.Index; 437 | int endIndex = startIndex; //start counting initially from the start index 438 | bool foundSequence = false; 439 | foreach (char c in _fullText.Substring(startIndex)) 440 | { 441 | if (foundSequence) 442 | { 443 | if (c == ' ' || c == ')' || c == '\n' || c == '\r') break; //we've found the end of the sequence 444 | } 445 | else 446 | { 447 | if (c != ' ' && c != ')' && c != '\n' && c != '\r') 448 | { 449 | //begin counting sequence 450 | foundSequence = true; 451 | } 452 | } 453 | endIndex++; 454 | } 455 | string truePropertyValue = _text.Substring(startIndex, endIndex - startIndex).Trim(); 456 | if (truePropertyValue.Length > 0) 457 | { 458 | truePropertyValue = truePropertyValue.Split("#")[0]; 459 | } 460 | result.Add((truePropertyValue, match.Value[1..], match.Index + truePropertyValue.Length)); 461 | } 462 | return result; 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /YuckLS/Core/Models/YuckTypes.cs: -------------------------------------------------------------------------------- 1 | namespace YuckLS.Core.Models; 2 | using YuckLS.Services; 3 | //really wish i had rust enums right about now 4 | internal abstract class YuckCompletionContext 5 | { 6 | protected List _items = new(); 7 | public abstract CompletionList Completions(); 8 | } 9 | internal class TopLevelYuckCompletionContext : YuckCompletionContext 10 | { 11 | public override CompletionList Completions() 12 | { 13 | foreach (var yuckType in YuckTypesProvider.YuckTypes.Where(p => p.IsTopLevel == true)) 14 | { 15 | _items.Add(new() 16 | { 17 | Label = yuckType.name, 18 | Documentation = new StringOrMarkupContent(yuckType.description), 19 | Kind = CompletionItemKind.Class, 20 | InsertText = yuckType.name 21 | }); 22 | } 23 | return new CompletionList(_items); 24 | } 25 | } 26 | internal class WidgetYuckCompletionContext(IEwwWorkspace _workspace) : YuckCompletionContext 27 | { 28 | public override CompletionList Completions() 29 | { 30 | var workspace = _workspace; 31 | //get gtk types and user defined types 32 | foreach (var yuckType in YuckTypesProvider.YuckTypes.Where(p => p.IsGtkWidgetType == true).Concat(workspace.UserDefinedTypes)) 33 | { 34 | _items.Add(new() 35 | { 36 | Label = yuckType.name, 37 | Documentation = new StringOrMarkupContent(yuckType.description), 38 | Kind = CompletionItemKind.Class, 39 | InsertText = yuckType.name 40 | }); 41 | } 42 | return new CompletionList(_items); 43 | } 44 | } 45 | internal class PropertyYuckCompletionContext : YuckCompletionContext 46 | { 47 | public required YuckType parentType; 48 | 49 | public override CompletionList Completions() 50 | { 51 | var properties = parentType.properties; 52 | if (properties is null || properties.Count() == 0) return new CompletionList(); 53 | foreach (var property in properties) 54 | { 55 | _items.Add(new() 56 | { 57 | Label = property.name, 58 | Documentation = new StringOrMarkupContent(property.description), 59 | Kind = CompletionItemKind.Property, 60 | InsertText = property.name 61 | }); 62 | } 63 | return new CompletionList(_items); 64 | } 65 | } 66 | internal class PropertySuggestionCompletionContext(IEwwWorkspace _workspace) : YuckCompletionContext 67 | { 68 | public required YuckType parentType; 69 | public required YuckProperty parentProperty; 70 | public override CompletionList Completions() 71 | { 72 | var suggestions = parentProperty.possibleValues; 73 | if (suggestions is not null && suggestions.Count() > 0) 74 | { 75 | foreach (var suggestion in suggestions) 76 | { 77 | _items.Add(new() 78 | { 79 | Label = suggestion, 80 | Kind = CompletionItemKind.EnumMember, 81 | InsertText = $"\"{suggestion}\"", 82 | Documentation = "A recommended variable for this type" 83 | }); 84 | } 85 | } 86 | //there is definetly a neater more efficient way to do this 87 | foreach (var customVariable in _workspace.UserDefinedVariables.Select(p => p.name).ToArray()) 88 | { 89 | _items.Add(new() 90 | { 91 | Label = customVariable, 92 | Kind = CompletionItemKind.Variable, 93 | InsertText = customVariable, 94 | Documentation = _workspace.UserDefinedVariables.Where(p => p.name == customVariable).First().description 95 | }); 96 | } 97 | return new CompletionList(_items); 98 | } 99 | } 100 | public class YuckType 101 | { 102 | public required string name; 103 | public YuckProperty[] properties = new YuckProperty[] { }; 104 | public required string description; 105 | public bool IsTopLevel = false; 106 | public bool AreWidgetsEmbeddable = false; 107 | public bool IsGtkWidgetType = false; 108 | public bool IsUserDefined = false; 109 | } 110 | public class YuckVariable 111 | { 112 | public required string name; 113 | public required string description; 114 | } 115 | public class YuckProperty 116 | { 117 | public required string name; 118 | public required string description; 119 | public required YuckDataType? dataType; 120 | public string[]? possibleValues; 121 | } 122 | 123 | public enum YuckDataType 124 | { 125 | YuckString, 126 | YuckInt, 127 | YuckBool, 128 | YuckDuration, 129 | YuckFloat, 130 | Custom 131 | }; 132 | 133 | /* 134 | * I should not hard-code these. I just don't know a better way to do it. 135 | */ 136 | public static class YuckTypesProvider 137 | { 138 | private static readonly YuckProperty[] _commonGtkWidgetProperties = new YuckProperty[] { 139 | new() { 140 | name = "class", 141 | description = "css class name", 142 | dataType = YuckDataType.YuckString 143 | }, 144 | new() { 145 | name = "valign", 146 | description = "how to align this vertically. possible values: 'fill', 'baseline', 'center', 'start', 'end'", 147 | dataType = YuckDataType.YuckString, 148 | possibleValues = new string[] {"fill", "baseline", "center", "start" , "end"} 149 | }, 150 | new() { 151 | name = "halign", 152 | description = "how to align this horizontally. possible values: 'fill', 'baseline', 'center', 'start', 'end'", 153 | dataType = YuckDataType.YuckString, 154 | possibleValues = new string[] {"fill", "baseline", "center", "start" , "end"} 155 | }, 156 | new() { 157 | name = "vexpand", 158 | description = "should this container expand vertically. Default: false.", 159 | dataType = YuckDataType.YuckBool, 160 | }, 161 | new() { 162 | name = "hexpand", 163 | description = "should this container expand horizontally. Default: false.", 164 | dataType = YuckDataType.YuckBool 165 | }, 166 | new() { 167 | name = "width", 168 | description = "int width of this element. note that this can not restrict the size if the contents stretch it", 169 | dataType = YuckDataType.YuckInt 170 | }, 171 | new() { 172 | name = "height", 173 | description = "int width of this element. note that this can not restrict the size if the contents stretch it", 174 | dataType = YuckDataType.YuckInt 175 | }, 176 | new() { 177 | name = "active", 178 | description = "If this widget can be interacted with", 179 | dataType = YuckDataType.YuckBool, 180 | }, 181 | new() { 182 | name = "tooltip", 183 | description = " tooltip text (on hover)", 184 | dataType = YuckDataType.YuckString, 185 | }, 186 | new() { 187 | name = "visible", 188 | description = "visibility of the widget", 189 | dataType = YuckDataType.YuckBool, 190 | }, 191 | new() { 192 | name = "style", 193 | description = " inline scss style applied to the widget", 194 | dataType = YuckDataType.YuckString, 195 | }, 196 | new() { 197 | name = "css", 198 | description = "scss code applied to the widget, i.e.: button {color: red;}", 199 | dataType = YuckDataType.YuckString 200 | } 201 | }; 202 | private static YuckProperty[] _timeOutProperty = new YuckProperty[] { 203 | new() { 204 | name = "timeout", 205 | description = "timeout of the command: Default: '200ms'", 206 | dataType = YuckDataType.YuckDuration, 207 | }, 208 | 209 | }; 210 | public static readonly YuckType[] YuckTypes = new YuckType[] { 211 | /* Top Level */ 212 | new() { 213 | name = "defwidget", 214 | description = "Define a top level widget", 215 | IsTopLevel = true, 216 | AreWidgetsEmbeddable = true, 217 | }, 218 | new() { 219 | name = "defwindow", 220 | description = "Define a top level window", 221 | IsTopLevel = true, 222 | AreWidgetsEmbeddable = true, 223 | properties = new YuckProperty[] { 224 | new() { 225 | name = "monitor", 226 | description = "The monitor index to display this window on", 227 | dataType = YuckDataType.Custom, 228 | }, 229 | new() { 230 | name = "geometry", 231 | description = "The geometry of the window", 232 | dataType = YuckDataType.Custom, 233 | }, 234 | new() { 235 | name = "stacking", 236 | description = "Where the window should appear in the stack. Possible values: fg, bg. X11 ONLY", 237 | dataType = YuckDataType.YuckString, 238 | possibleValues = new [] { 239 | "fg", 240 | "bg" 241 | }, 242 | }, 243 | new() { 244 | name = "wm-ignore", 245 | description = "Whether the window manager should ignore this window. This is useful for dashboard-style widgets that don't need to interact with other windows at all. Note that this makes some of the other properties not have any effect. Either true or false. X11 ONLY", 246 | dataType = YuckDataType.YuckBool, 247 | }, 248 | new() { 249 | name = "reserve", 250 | description = "Specify how the window manager should make space for your window. This is useful for bars, which should not overlap any other windows. X11 ONLY", 251 | dataType = YuckDataType.YuckBool, 252 | }, 253 | new() { 254 | name = "windowtype", 255 | description = "Specify what type of window this is. This will be used by your window manager to determine how it should handle your window. Possible values: normal, dock, toolbar, dialog, desktop. Default: dock if reserve is specified, normal otherwise. X11 ONLY", 256 | dataType = YuckDataType.YuckString, 257 | possibleValues = new string[] { 258 | "normal", 259 | "dock", 260 | "toolbar", 261 | "dialog", 262 | "desktop" 263 | }, 264 | }, 265 | new() { 266 | name = "stacking", 267 | description = "Where the window should appear in the stack. Possible values: fg, bg, overlay, bottom. WAYLAND ONLY", 268 | dataType = YuckDataType.YuckString, 269 | possibleValues = new string[] { 270 | "fg", 271 | "bg", 272 | "overlay", 273 | "bottom" 274 | }, 275 | }, 276 | new() { 277 | name = "exclusive", 278 | description = "Whether the compositor should reserve space for the window automatically. Either true or false. WAYLAND ONLY", 279 | dataType = YuckDataType.YuckBool, 280 | }, 281 | new() { 282 | name = "focusable", 283 | description = "Whether the window should be able to be focused. This is necessary for any widgets that use the keyboard to work. Either true or false. WAYLAND ONLY", 284 | dataType = YuckDataType.YuckBool, 285 | }, 286 | new() { 287 | name = "namespace", 288 | description = "Set the wayland layersurface namespace eww uses. Accepts a string value. WAYLAND ONLY", 289 | dataType = YuckDataType.YuckString 290 | } 291 | } 292 | }, 293 | new() { 294 | name = "defvar", 295 | description = "Define a variable", 296 | IsTopLevel = true 297 | }, 298 | new() { 299 | name = "deflisten", 300 | description = "Define a listener", 301 | IsTopLevel = true, 302 | properties = new YuckProperty[] { 303 | new() { 304 | name = "initial", 305 | description = "Initial value while the listener loads", 306 | dataType = YuckDataType.Custom, 307 | } 308 | } 309 | }, 310 | new() { 311 | name = "include", 312 | description = "Include another yuck source file", 313 | IsTopLevel = true 314 | }, 315 | new() { 316 | name = "defpoll", 317 | description = "Define a poll", 318 | IsTopLevel = true, 319 | properties = new YuckProperty[] { 320 | new() { 321 | name = "interval", 322 | description = "How frequently to update the poll", 323 | dataType = YuckDataType.YuckString, 324 | }, 325 | new() { 326 | name = "initial", 327 | description = "Initial value before the poll first loads", 328 | dataType = YuckDataType.YuckString, 329 | }, 330 | new() { 331 | name = "run-while", 332 | description = "Condition that must be fulfilled for poll to run", 333 | dataType = YuckDataType.YuckBool, 334 | } 335 | } 336 | }, 337 | 338 | /****** GTK Widgets ******/ 339 | new() { 340 | name = "combo-box-text", 341 | description = "A combo box allowing the user to choose between several items.", 342 | IsGtkWidgetType = true, 343 | properties = new YuckProperty[] { 344 | new() { 345 | name = "items", 346 | description = "Items that should be displayed in the combo box", 347 | dataType = YuckDataType.YuckString, 348 | }, 349 | 350 | new() { 351 | name = "onchange", 352 | description = "runs the code when a item was selected, replacing {} with the item as a string", 353 | dataType = YuckDataType.YuckString 354 | } 355 | }.Concat(_commonGtkWidgetProperties).ToArray() 356 | }, 357 | new() { 358 | name = "expander", 359 | description = "A widget that can expand and collapse, showing/hiding its children.", 360 | IsGtkWidgetType = true, 361 | AreWidgetsEmbeddable = true, 362 | properties = new YuckProperty[] { 363 | new() { 364 | name = "name", 365 | description = "Name of the expander", 366 | dataType = YuckDataType.YuckString, 367 | }, 368 | new() { 369 | name = "expanded", 370 | description = "sets if the tree is expanded", 371 | dataType = YuckDataType.YuckBool 372 | } 373 | }.Concat(_commonGtkWidgetProperties).Concat(_timeOutProperty).ToArray() 374 | }, 375 | new() { 376 | name = "revealer", 377 | description = "A widget that can reveal a child with an animation.", 378 | IsGtkWidgetType = true, 379 | AreWidgetsEmbeddable = true, 380 | properties = new YuckProperty[]{ 381 | new(){ 382 | name = "transition", 383 | description = "the name of the transition", 384 | dataType = YuckDataType.YuckString, 385 | possibleValues = new string[] {"slideright", "slideleft", "slideup", "slidedown", "crossfade", "none"} 386 | }, 387 | new(){ 388 | name = "reveal", 389 | description = "sets if the child is revealed or not", 390 | dataType = YuckDataType.YuckBool, 391 | }, 392 | new(){ 393 | name = "duration", 394 | description = "the duration of the reveal transition.", 395 | dataType = YuckDataType.YuckDuration 396 | } 397 | } 398 | }, 399 | new() { 400 | name = "checkbox", 401 | description = "A checkbox that can trigger events on checked/unchecked.", 402 | IsGtkWidgetType = true, 403 | properties = new YuckProperty[] { 404 | new() { 405 | name = "checked", 406 | description = "whether the checkbox is toggled or not when created", 407 | dataType = YuckDataType.YuckBool, 408 | }, 409 | new() { 410 | name = "timeout", 411 | description = " timeout of the command. Default: 200ms", 412 | dataType = YuckDataType.YuckDuration, 413 | }, 414 | new() { 415 | name = "onchecked", 416 | description = " action (command) to be executed when checked by the user", 417 | dataType = YuckDataType.YuckString, 418 | }, 419 | new() { 420 | name = "onunchecked", 421 | description = " similar to onchecked but when the widget is unchecked", 422 | dataType = YuckDataType.YuckString 423 | } 424 | }.Concat(_commonGtkWidgetProperties).ToArray() 425 | }, 426 | new() { 427 | name = "color-button", 428 | description = "A button opening a color chooser window.", 429 | IsGtkWidgetType = true, 430 | properties = new YuckProperty[] { 431 | new() { 432 | name = "use-alpha", 433 | description = "bool to whether or not use alpha", 434 | dataType = YuckDataType.YuckBool 435 | }, 436 | new() { 437 | name = "onchange", 438 | description = " runs the code when the color was selected", 439 | dataType = YuckDataType.YuckString 440 | }, 441 | }.Concat(_commonGtkWidgetProperties).Concat(_timeOutProperty).ToArray() 442 | }, 443 | new() { 444 | name = "color-chooser", 445 | description = "A color chooser widget.", 446 | IsGtkWidgetType = true, 447 | properties = new YuckProperty[] { 448 | new() { 449 | name = "use-alpha", 450 | description = "bool to whether or not use alpha", 451 | dataType = YuckDataType.YuckBool 452 | }, 453 | new() { 454 | name = "onchange", 455 | description = " runs the code when the color was selected", 456 | dataType = YuckDataType.YuckString 457 | }, 458 | }.Concat(_commonGtkWidgetProperties).Concat(_timeOutProperty).ToArray() 459 | }, 460 | new() { 461 | name = "scale", 462 | description = "A slider widget.", 463 | IsGtkWidgetType = true, 464 | properties = new YuckProperty[] { 465 | new() { 466 | name = "flipped", 467 | description = "flip the direction", 468 | dataType = YuckDataType.YuckBool 469 | }, 470 | new() { 471 | name = "marks", 472 | description = "draw marks", 473 | dataType = YuckDataType.YuckString 474 | }, 475 | new() { 476 | name = "draw-value", 477 | description = "draw the value of the property", 478 | dataType = YuckDataType.YuckBool, 479 | }, 480 | new() { 481 | name = "round-digits", 482 | description = "Sets the number of decimals to round the value to when it changes", 483 | dataType = YuckDataType.YuckInt 484 | }, 485 | new() { 486 | name = "value", 487 | description = "the value", 488 | dataType = YuckDataType.YuckFloat 489 | }, 490 | new() { 491 | name = "min", 492 | description = "the minimum value", 493 | dataType = YuckDataType.YuckFloat 494 | }, 495 | new() { 496 | name = "max", 497 | description = "the maximum value", 498 | dataType = YuckDataType.YuckFloat 499 | }, 500 | new() { 501 | name = "onchange", 502 | description = "command executed once the value is changes. The placeholder {}, used in the command will be replaced by the new value.", 503 | dataType = YuckDataType.YuckString 504 | }, 505 | new() { 506 | name = "orientation", 507 | description = "orientation of the widget. Possible values: 'vertical', 'v', 'horizontal', 'h'", 508 | dataType = YuckDataType.YuckString, 509 | possibleValues = new[] {"vertical","horizontal"} 510 | } 511 | }.Concat(_commonGtkWidgetProperties).Concat(_timeOutProperty).ToArray() 512 | }, 513 | new() { 514 | name = "progress", 515 | description = "A progress bar.", 516 | IsGtkWidgetType = true, 517 | properties = new YuckProperty[] { 518 | new() { 519 | name = "flipped", 520 | description = "flip the direction", 521 | dataType = YuckDataType.YuckBool 522 | }, 523 | new() { 524 | name = "value", 525 | description = "value of the progress bar (between 0-100)", 526 | dataType = YuckDataType.YuckFloat 527 | }, 528 | new() { 529 | name = "orientation", 530 | description = "orientation of the widget. Possible values: 'vertical', 'v', 'horizontal', 'h'", 531 | dataType = YuckDataType.YuckString, 532 | possibleValues = new string[]{ "vertical", "horizontal"} 533 | }, 534 | }.Concat(_commonGtkWidgetProperties).ToArray() 535 | }, 536 | new() { 537 | name = "input", 538 | description = "An input field. For this to be usable, set focusable = true on the window.", 539 | IsGtkWidgetType = true, 540 | properties = new YuckProperty[] { 541 | new() { 542 | name = "value", 543 | description = "the content of the text field", 544 | dataType = YuckDataType.YuckString 545 | }, 546 | new() { 547 | name = "onchange", 548 | description = "Command to run when the text changes. The placeholder {} will be replaced by the value", 549 | dataType = YuckDataType.YuckString 550 | }, 551 | new() { 552 | name = "onaccept", 553 | description = " Command to run when the user hits return in the input field. The placeholder {} will be replaced by the value", 554 | dataType = YuckDataType.YuckString 555 | }, 556 | new() { 557 | name = "password", 558 | description = " if the input is obscured", 559 | dataType = YuckDataType.YuckBool 560 | }, 561 | }.Concat(_commonGtkWidgetProperties).Concat(_timeOutProperty).ToArray() 562 | }, 563 | new() { 564 | name = "button", 565 | description = "A button containing any widget as its child. Events are triggered on release.", 566 | IsGtkWidgetType = true, 567 | AreWidgetsEmbeddable = true, 568 | properties = new YuckProperty[] { 569 | new() { 570 | name = "onclick", 571 | description = " command to run when the button is activated either by leftclicking or keyboard", 572 | dataType = YuckDataType.YuckString, 573 | }, 574 | new() { 575 | name = "onmiddleclick", 576 | description = "command to run when the button is middleclicked", 577 | dataType = YuckDataType.YuckBool, 578 | }, 579 | new() { 580 | name = "onrightclick", 581 | description = "command to run when the button is rightclicked", 582 | dataType = YuckDataType.YuckString 583 | }, 584 | }.Concat(_timeOutProperty).Concat(_commonGtkWidgetProperties).ToArray() 585 | }, 586 | new() { 587 | name = "image", 588 | description = "A widget displaying an image.", 589 | IsGtkWidgetType = true, 590 | properties = new YuckProperty[] { 591 | new() { 592 | name = "path", 593 | description = " path to the image file", 594 | dataType = YuckDataType.YuckString 595 | }, 596 | new() { 597 | name = "image-width", 598 | description = "width of the image", 599 | dataType = YuckDataType.YuckInt 600 | }, 601 | new() { 602 | name = "image-height", 603 | description = "height of the image", 604 | dataType = YuckDataType.YuckInt 605 | }, 606 | new() { 607 | name = "preserve-aspect-ratio", 608 | description = "whether to keep the aspect ratio when resizing an image. Default: true, false doesn't work for all image types", 609 | dataType = YuckDataType.YuckBool 610 | }, 611 | new() { 612 | name = "fill-svg", 613 | description = "sets the color of svg images", 614 | dataType = YuckDataType.YuckString 615 | }, 616 | new() { 617 | name = "icon", 618 | description = "name of a theme icon", 619 | dataType = YuckDataType.YuckString, 620 | }, 621 | new() { 622 | name = "icon-size", 623 | description = "size of the theme icon", 624 | dataType = YuckDataType.YuckString 625 | }, 626 | }.Concat(_commonGtkWidgetProperties).ToArray() 627 | }, 628 | new() { 629 | name = "box", 630 | description = "The main layout container.", 631 | IsGtkWidgetType = true, 632 | AreWidgetsEmbeddable = true, 633 | properties = new YuckProperty[] { 634 | new() { 635 | name = "orientation", 636 | description = "orientation of the box. possible values: 'vertical', 'v', 'horizontal', 'h'", 637 | dataType = YuckDataType.YuckString, 638 | possibleValues = new string[] {"vertical","horizontal"} 639 | }, 640 | new() { 641 | name = "spacing", 642 | description = "spacing between elements", 643 | dataType = YuckDataType.YuckInt 644 | }, 645 | new() { 646 | name = "space-evenly", 647 | description = "space the widgets evenly.", 648 | dataType = YuckDataType.YuckBool 649 | } 650 | }.Concat(_commonGtkWidgetProperties).ToArray() 651 | }, 652 | new() { 653 | name = "overlay", 654 | description = "A widget that places its children on top of each other.", 655 | IsGtkWidgetType = true, 656 | AreWidgetsEmbeddable = true, 657 | properties = new YuckProperty[] {}.Concat(_commonGtkWidgetProperties).ToArray() 658 | }, 659 | new() { 660 | name = "tooltip", 661 | description = "A widget that has a custom tooltip.", 662 | IsGtkWidgetType = true, 663 | AreWidgetsEmbeddable = true, 664 | properties = new YuckProperty[] {}.Concat(_commonGtkWidgetProperties).ToArray() 665 | }, 666 | new() { 667 | name = "centerbox", 668 | IsGtkWidgetType = true, 669 | AreWidgetsEmbeddable = true, 670 | description = "A box that must contain EXACTLY 3 children that will be laid out at the start, center, and end of the container", 671 | properties = new YuckProperty[] { 672 | new() { 673 | name = "orientation", 674 | description = "orientation of the widget. Possible values: 'vertical', 'v', 'horizontal', 'h'", 675 | dataType = YuckDataType.YuckString, 676 | possibleValues = new string[] {"vertical","horizontal"} 677 | }, 678 | }.Concat(_commonGtkWidgetProperties).ToArray() 679 | }, 680 | new() { 681 | name = "scroll", 682 | description = "A container with a single child that can scroll.", 683 | IsGtkWidgetType = true, 684 | AreWidgetsEmbeddable = true, 685 | properties = new YuckProperty[] { 686 | new() { 687 | name = "hscroll", 688 | description = "scroll horizontally", 689 | dataType = YuckDataType.YuckBool 690 | }, 691 | new() { 692 | name = "vscroll", 693 | description = "scroll vertically", 694 | dataType = YuckDataType.YuckBool 695 | } 696 | }.Concat(_commonGtkWidgetProperties).ToArray() 697 | }, 698 | new() { 699 | name = "eventbox", 700 | description = "A container which can receive events and must contain exactly one child.", 701 | IsGtkWidgetType = true, 702 | AreWidgetsEmbeddable = true, 703 | properties = new YuckProperty[] { 704 | new() { 705 | name = "onscroll", 706 | description = "event to execute when the user scrolls with the mouse over the widget.", 707 | dataType = YuckDataType.YuckString 708 | }, 709 | new() { 710 | name = "onhover", 711 | description = "event to execute when the user hovers over the widget", 712 | dataType = YuckDataType.YuckString, 713 | }, 714 | new() { 715 | name = "onhoverlost", 716 | description = "event to execute when the user losts hovers over the widget", 717 | dataType = YuckDataType.YuckString 718 | }, 719 | new() { 720 | name = "cursor", 721 | description = "Cursor to show while hovering (see gtk3-cursors for possible names)", 722 | dataType = YuckDataType.YuckString 723 | }, 724 | new() { 725 | name = "ondropped", 726 | description = "Command to execute when something is dropped on top of this element. The placeholder {} used in the command will be replaced with the uri to the dropped thing.", 727 | dataType = YuckDataType.YuckString 728 | }, 729 | new() { 730 | name = "dragvalue", 731 | description = "URI that will be provided when dragging from this widget", 732 | dataType = YuckDataType.YuckString 733 | }, 734 | new() { 735 | name = "dragtype", 736 | description = "Type of value that should be dragged from this widget. Possible values: 'file', 'text'", 737 | dataType = YuckDataType.YuckString, 738 | possibleValues = new string[] {"file","text"} 739 | }, 740 | new() { 741 | name = "onclick", 742 | description = "command to run when the widget is clicked", 743 | dataType = YuckDataType.YuckString 744 | }, 745 | new() { 746 | name = "onmiddleclick", 747 | description = "command to run when the widget is middleclicked", 748 | dataType = YuckDataType.YuckString 749 | }, 750 | new() { 751 | name = "onrightclick", 752 | description = "command to run when the widget is rightclicked", 753 | dataType = YuckDataType.YuckString 754 | } 755 | }.Concat(_commonGtkWidgetProperties).ToArray() 756 | }, 757 | new() { 758 | name = "label", 759 | description = "A text widget giving you more control over how the text is displayed.", 760 | IsGtkWidgetType = true, 761 | properties = new YuckProperty[] { 762 | new() { 763 | name = "text", 764 | description = "the text to display", 765 | dataType = YuckDataType.YuckString, 766 | }, 767 | new() { 768 | name = "truncate", 769 | description = "whether to truncate text (or pango markup). If show-truncated is false, or if limit-width has a value, this property has no effect and truncation is enabled.", 770 | dataType = YuckDataType.YuckBool 771 | }, 772 | new() { 773 | name = "limit-width", 774 | description = "maximum count of characters to display", 775 | dataType = YuckDataType.YuckInt, 776 | }, 777 | new() { 778 | name = "truncate-left", 779 | description = "whether to truncate on the left side", 780 | dataType = YuckDataType.YuckBool, 781 | }, 782 | new() { 783 | name = "show-truncated", 784 | description = "show whether the text was truncated. Disabling it will also disable dynamic truncation (the labels won't be truncated more than limit-width, even if there is not enough space for them), and will completly disable truncation on pango markup.", 785 | dataType = YuckDataType.YuckBool, 786 | }, 787 | new() { 788 | name = "uindent", 789 | description = " whether to remove leading spaces", 790 | dataType = YuckDataType.YuckBool, 791 | }, 792 | new() { 793 | name = "markup", 794 | description = "Pango markup to display", 795 | dataType = YuckDataType.YuckString, 796 | }, 797 | new() { 798 | name = "wrap", 799 | description = "Wrap the text. This mainly makes sense if you set the width of this widget.", 800 | dataType = YuckDataType.YuckBool, 801 | }, 802 | new() { 803 | name = "angle", 804 | description = "the angle of rotation for the label (between 0 - 360)", 805 | dataType = YuckDataType.YuckFloat, 806 | }, 807 | new() { 808 | name = "gravity", 809 | description = " the gravity of the string (south, east, west, north, auto). Text will want to face the direction of gravity.", 810 | dataType = YuckDataType.YuckString, 811 | }, 812 | new() { 813 | name = "xalign", 814 | description = " the alignment of the label text on the x axis (between 0 - 1, 0 -> left, 0.5 -> center, 1 -> right)", 815 | dataType = YuckDataType.YuckFloat, 816 | }, 817 | new() { 818 | name = "yalign", 819 | description = "the alignment of the label text on the y axis (between 0 - 1, 0 -> bottom, 0.5 -> center, 1 -> top)", 820 | dataType = YuckDataType.YuckFloat 821 | }, 822 | new() { 823 | name = "justify", 824 | description = " the justification of the label text (left, right, center, fill)", 825 | dataType = YuckDataType.YuckString, 826 | possibleValues = new string[] {"left","right","center","fill"} 827 | } 828 | }.Concat(_commonGtkWidgetProperties).ToArray() 829 | }, 830 | new() { 831 | name = "literal", 832 | description = "A widget that allows you to render arbitrary yuck.", 833 | IsGtkWidgetType = true, 834 | properties = new YuckProperty[] { 835 | new() { 836 | name = "content", 837 | description = " inline yuck that will be rendered as a widget.", 838 | dataType = YuckDataType.YuckString 839 | }, 840 | }.Concat(_commonGtkWidgetProperties).ToArray() 841 | }, 842 | new() { 843 | name = "calendar", 844 | description = "A widget that displays a calendar.", 845 | IsGtkWidgetType = true, 846 | properties = new YuckProperty[] { 847 | new() { 848 | name = "day", 849 | description = "the selected day", 850 | dataType = YuckDataType.YuckFloat 851 | }, 852 | new() { 853 | name = "month", 854 | description = "the selected month", 855 | dataType = YuckDataType.YuckFloat 856 | }, 857 | new() { 858 | name = "year", 859 | description = "the selected year", 860 | dataType = YuckDataType.YuckFloat 861 | }, 862 | new() { 863 | name = "show-details", 864 | description = "show details", 865 | dataType = YuckDataType.YuckBool 866 | }, 867 | new() { 868 | name = "show-heading", 869 | description = "show heading line", 870 | dataType = YuckDataType.YuckBool 871 | }, 872 | new() { 873 | name = "show-day-names", 874 | description = "show names of days", 875 | dataType = YuckDataType.YuckBool 876 | }, 877 | new() { 878 | name = "show-week-numbers", 879 | description = "show week numbers", 880 | dataType = YuckDataType.YuckBool 881 | }, 882 | new() { 883 | name = "onclick", 884 | description = "command to run when the user selects a date. The {0} placeholder will be replaced by the selected day, {1} will be replaced by the month, and {2} by the year.", 885 | dataType = YuckDataType.YuckString 886 | }, 887 | }.Concat(_commonGtkWidgetProperties).Concat(_timeOutProperty).ToArray() 888 | }, 889 | new() { 890 | name = "stack", 891 | description = "A widget that displays one of its children at a time.", 892 | IsGtkWidgetType = true, 893 | AreWidgetsEmbeddable = true, 894 | properties = new YuckProperty[] { 895 | new() { 896 | name = "selected", 897 | description = "index of child which should be shown", 898 | dataType = YuckDataType.YuckInt 899 | }, 900 | new() { 901 | name = "transition", 902 | description = "he name of the transition. Possible values: 'slideright', 'slideleft', 'slideup', 'slidedown', 'crossfade', 'none'", possibleValues = new string[] { 903 | "slideright", "slideleft" ,"slideup" ,"slidedown" , "crossfade" , "none" 904 | }, 905 | dataType = YuckDataType.YuckString 906 | }, 907 | new() { 908 | name = "same-size", 909 | description = "sets whether all children should be the same size", 910 | dataType = YuckDataType.YuckBool 911 | } 912 | }.Concat(_commonGtkWidgetProperties).ToArray() 913 | }, 914 | new(){ 915 | name = "for", 916 | description = "Loop over a dataset", 917 | //i mean... technically 918 | IsGtkWidgetType = true 919 | }, 920 | new() { 921 | name = "transform", 922 | description = "A widget that applies transformations to its content. They are applied in the following order: rotate -> translate -> scale.", 923 | IsGtkWidgetType = true, 924 | AreWidgetsEmbeddable = true, 925 | properties = new YuckProperty[] { 926 | new() { 927 | name = "rotate", 928 | description = "the percentage to rotate", 929 | dataType = YuckDataType.YuckFloat 930 | }, 931 | new() { 932 | name = "transform-origin-x", 933 | description = "x coordinate of origin of transformation (px or %)", 934 | dataType = YuckDataType.YuckString 935 | }, 936 | new() { 937 | name = "transform-origin-y", 938 | description = "y coordinate of origin of transformation (px or %)", 939 | dataType = YuckDataType.YuckString 940 | }, 941 | new() { 942 | name = "translate-x", 943 | description = "the amount to translate in the x direction (px or %)", 944 | dataType = YuckDataType.YuckString, 945 | }, 946 | new() { 947 | name = "translate-y", 948 | description = "the amount to translate in the y direction (px or %)", 949 | dataType = YuckDataType.YuckString, 950 | }, 951 | new() { 952 | name = "scale-x", 953 | description = "the amount to scale in the x direction (px or %)", 954 | dataType = YuckDataType.YuckString 955 | }, 956 | new() { 957 | name = "scale-y", 958 | description = "the amount to scale in the y direction (px or %)", 959 | dataType = YuckDataType.YuckString 960 | } 961 | }.Concat(_commonGtkWidgetProperties).ToArray() 962 | }, 963 | new() { 964 | name = "circular-progress", 965 | description = "A widget that displays a circular progress bar.", 966 | IsGtkWidgetType = true, 967 | properties = new YuckProperty[] { 968 | new() { 969 | name = "value", 970 | description = " the value, between 0 - 100", 971 | dataType = YuckDataType.YuckFloat 972 | }, 973 | new() { 974 | name = "start-at", 975 | description = "the percentage that the circle should start at", 976 | dataType = YuckDataType.YuckFloat 977 | }, 978 | new() { 979 | name = "thickness", 980 | description = "the thickness of the circle", 981 | dataType = YuckDataType.YuckFloat 982 | }, 983 | new() { 984 | name = "clockwise", 985 | description = "whether the progress bar spins clockwise or counter clockwise", 986 | dataType = YuckDataType.YuckBool 987 | } 988 | }.Concat(_commonGtkWidgetProperties).ToArray() 989 | }, 990 | new() { 991 | name = "graph", 992 | description = "A widget that displays a graph showing how a given value changes over time.", 993 | IsGtkWidgetType = true, 994 | properties = new YuckProperty[] { 995 | new() { 996 | name = "value", 997 | description = "the value, between 0 - 100", 998 | dataType = YuckDataType.YuckFloat 999 | }, 1000 | new() { 1001 | name = "thickness", 1002 | description = "the thickness of the line", 1003 | dataType = YuckDataType.YuckFloat 1004 | }, 1005 | new() { 1006 | name = "time-range", 1007 | description = "the range of time to show", 1008 | dataType = YuckDataType.YuckDuration, 1009 | }, 1010 | new() { 1011 | name = "min", 1012 | description = "the minimum value to show (defaults to 0 if value_max is provided)", 1013 | dataType = YuckDataType.YuckFloat 1014 | }, 1015 | new() { 1016 | name = "max", 1017 | description = "the maximum value to show", 1018 | dataType = YuckDataType.YuckFloat 1019 | }, 1020 | new() { 1021 | name = "dynamic", 1022 | description = "whether the y range should dynamically change based on value", 1023 | dataType = YuckDataType.YuckBool 1024 | }, 1025 | new() { 1026 | name = "line-style", 1027 | description = " changes the look of the edges in the graph. Values: 'miter' (default), 'round'", 1028 | dataType = YuckDataType.YuckString 1029 | }, 1030 | new() { 1031 | name = "flip-x", 1032 | description = " whether the x axis should go from high to low", 1033 | dataType = YuckDataType.YuckBool 1034 | }, 1035 | new() { 1036 | name = "flip-y", 1037 | description = " whether the y axis should go from high to low", 1038 | dataType = YuckDataType.YuckBool 1039 | }, 1040 | new() { 1041 | name = "vertical", 1042 | description = " if set to true, the x and y axes will be exchanged", 1043 | dataType = YuckDataType.YuckBool 1044 | } 1045 | }.Concat(_commonGtkWidgetProperties).ToArray() 1046 | }, 1047 | new() { 1048 | name = "systray", 1049 | description = "Tray for system notifier icons.", 1050 | IsGtkWidgetType = true, 1051 | properties = new YuckProperty[] { 1052 | new() { 1053 | name = "spacing", 1054 | description = "spacing between elements", 1055 | dataType = YuckDataType.YuckInt, 1056 | }, 1057 | new() { 1058 | name = "orientation", 1059 | description = "orientation of the box. possible values: 'vertical', 'v', 'horizontal', 'h'", 1060 | dataType = YuckDataType.YuckString, 1061 | possibleValues = new string[] {"vertical" , "horizontal"} 1062 | }, 1063 | new() { 1064 | name = "space-evenly", 1065 | description = "space the widgets evenly.", 1066 | dataType = YuckDataType.YuckBool 1067 | }, 1068 | new() { 1069 | name = "size", 1070 | description = "size of icons in the tray", 1071 | dataType = YuckDataType.YuckInt, 1072 | }, 1073 | new() { 1074 | name = "prepend-new", 1075 | description = "prepend new icons", 1076 | dataType = YuckDataType.YuckBool 1077 | }, 1078 | new(){ 1079 | name = "icon-size", 1080 | description = "Size of the icons", 1081 | dataType = YuckDataType.YuckInt 1082 | } 1083 | }.Concat(_commonGtkWidgetProperties).ToArray() 1084 | }, 1085 | 1086 | //special types 1087 | new() { 1088 | name = "geometry", 1089 | description = "Size and positional description for a the window geometry property", 1090 | //not ideal, will fix this later 1091 | IsGtkWidgetType = true, 1092 | properties = new YuckProperty[] { 1093 | new() { 1094 | name = "x", 1095 | description = "The x-axis position", 1096 | dataType = YuckDataType.Custom 1097 | }, 1098 | new() { 1099 | name = "y", 1100 | description = "The y-axis position", 1101 | dataType = YuckDataType.Custom 1102 | }, 1103 | new() { 1104 | name = "width", 1105 | description = "The width of the window", 1106 | dataType = YuckDataType.YuckString 1107 | }, 1108 | new() { 1109 | name = "height", 1110 | description = "The height of the window", 1111 | dataType = YuckDataType.YuckString 1112 | }, 1113 | new() { 1114 | name = "anchor", 1115 | description = "The positional anchor of the window e.g top left, top center, bottom right, e.t.c", 1116 | dataType = YuckDataType.YuckString 1117 | } 1118 | } 1119 | }, 1120 | new() { 1121 | name = "struts", 1122 | description = "Reserve struts for X11", 1123 | //not ideal, will fix this later, 1124 | IsGtkWidgetType = true, 1125 | properties = new YuckProperty[] { 1126 | new() { 1127 | name = "distance", 1128 | description = "The distance", 1129 | dataType = YuckDataType.YuckString 1130 | }, 1131 | new() { 1132 | name = "side", 1133 | description = "The side allowance", 1134 | dataType = YuckDataType.YuckString 1135 | } 1136 | } 1137 | } 1138 | }; 1139 | 1140 | } 1141 | --------------------------------------------------------------------------------