diff --git a/CHANGELOG.md b/CHANGELOG.md index 037f62bd..1609bff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - A Unity Project scoped settings that allows you to override some of the default behaviours of Yarn Spinner - settings are saved to a file in `ProjectSettings\Packages\dev.yarnspinner\YarnSpinnerProjectSettings.json` - these can be changed via `Edit -> Project Settings -> Yarn Spinner` - - currently supports two convenience features of Yarn Spinner: + - currently supports three convenience features of Yarn Spinner: - automatically associating assets with localisations - automatically linking YarnCommand and YarnFunction attributed methods to the dialogue runner - - enabling/disabling C# linking will force an entire C# reimport + - generating a `ysls` file for all of your Yarn attributed methods + - this is save to `ProjectSettings\Packages\dev.yarnspinner\generated.ysls.json` + - this is an experimental feature to support better editor integration down the line + - as such this defaults to *not* being generated + - enabling/disabling C# linking or ysls generation will force an entire C# reimport - enabling/disabling asset linking will force a reimport of all `yarnprojects` - `Yarn.Unity.ActionAnalyser.Action` now has a `MethodIdentifierName` property, which is the short form of the method name. - `LineView` now will add in line breaks when it encounters a self closing `[br /]` marker. diff --git a/Editor/Analysis/ActionsRegistrationGenerator~/ActionsGenerator.cs b/Editor/Analysis/ActionsRegistrationGenerator~/ActionsGenerator.cs index 79cb5a53..48c828b3 100644 --- a/Editor/Analysis/ActionsRegistrationGenerator~/ActionsGenerator.cs +++ b/Editor/Analysis/ActionsRegistrationGenerator~/ActionsGenerator.cs @@ -26,6 +26,8 @@ public void Execute(GeneratorExecutionContext context) // so what we do is we use the included Compilation Assembly additional file that Unity gives us. // This file if opened has the path of the Unity project, which we can then use to get the settings // if any stage of this fails then we bail out and assume that codegen is desired + string projectPath = null; + Yarn.Unity.Editor.YarnSpinnerProjectSettings settings = null; if (context.AdditionalFiles.Any()) { var relevants = context.AdditionalFiles.Where(i => i.Path.Contains($"{context.Compilation.AssemblyName}.AdditionalFile.txt")); @@ -36,11 +38,12 @@ public void Execute(GeneratorExecutionContext context) { try { - var projectPath = File.ReadAllText(arsgacsaf.Path); + projectPath = File.ReadAllText(arsgacsaf.Path); var fullPath = Path.Combine(projectPath, Yarn.Unity.Editor.YarnSpinnerProjectSettings.YarnSpinnerProjectSettingsPath); output.WriteLine($"Attempting to read settings file at {fullPath}"); - if (!Yarn.Unity.Editor.YarnSpinnerProjectSettings.GetOrCreateSettings(projectPath, output).automaticallyLinkAttributedYarnCommandsAndFunctions) + settings = Yarn.Unity.Editor.YarnSpinnerProjectSettings.GetOrCreateSettings(projectPath, output); + if (!settings.automaticallyLinkAttributedYarnCommandsAndFunctions) { output.WriteLine("Skipping codegen due to settings."); output.Dispose(); @@ -190,7 +193,7 @@ public void Execute(GeneratorExecutionContext context) } // removing any actions that failed validation - actions.Where(x => !removals.Contains(x.Name)).ToList(); + actions = actions.Where(x => !removals.Contains(x.Name)).ToList(); output.Write($"Generating source code..."); @@ -208,6 +211,50 @@ public void Execute(GeneratorExecutionContext context) context.AddSource($"YarnActionRegistration-{compilation.AssemblyName}.Generated.cs", sourceText); + if (settings != null) + { + if (settings.generateYSLSFile) + { + output.Write($"Generating ysls..."); + // generating the ysls + YSLSGenerator generator = new YSLSGenerator(); + generator.logger = output; + foreach (var action in actions) + { + generator.AddAction(action); + } + var ysls = generator.Serialise(); + output.WriteLine($"Done."); + + if (!string.IsNullOrEmpty(projectPath)) + { + output.Write($"Writing generated ysls..."); + + var fullPath = Path.Combine(projectPath, Yarn.Unity.Editor.YarnSpinnerProjectSettings.YarnSpinnerGeneratedYSLSPath); + try + { + System.IO.File.WriteAllText(fullPath, ysls); + output.WriteLine($"Done."); + } + catch (Exception e) + { + output.WriteLine($"Unable to write ysls to disk: {e.Message}"); + } + } + else + { + output.WriteLine("unable to identify project path, ysls will not be written to disk"); + } + } + else + { + output.WriteLine($"skipping ysls generation due to settings"); + } + } + else + { + output.WriteLine($"skipping ysls generation due to settings not being found"); + } return; @@ -486,3 +533,209 @@ public void OnVisitSyntaxNode(SyntaxNode syntaxNode) } } } + +internal class YSLSGenerator +{ + struct YarnActionParameter + { + internal string Name; + internal string Type; + internal bool IsParamsArray; + + internal Dictionary ToDictionary() + { + var dict = new Dictionary(); + dict["Name"] = Name; + dict["Type"] = Type; + dict["IsParamsArray"] = IsParamsArray; + return dict; + } + } + struct YarnActionCommand + { + internal string YarnName; + internal string DefinitionName; + internal string Signature; + internal string FileName; + internal YarnActionParameter[] Parameters; + + internal Dictionary ToDictionary() + { + var dict = new Dictionary(); + dict["YarnName"] = YarnName; + dict["DefinitionName"] = DefinitionName; + dict["Signature"] = Signature; + dict["Language"] = "csharp"; + + if (!string.IsNullOrEmpty(FileName)) + { + dict["FileName"] = FileName; + } + + if (Parameters.Length > 0) + { + var pl = new List>(); + foreach (var p in Parameters) + { + pl.Add(p.ToDictionary()); + } + dict["Parameters"] = pl; + } + return dict; + } + } + struct YarnActionFunction + { + internal string YarnName; + internal string DefinitionName; + internal string Signature; + internal YarnActionParameter[] Parameters; + internal string ReturnType; + internal string FileName; + + internal Dictionary ToDictionary() + { + var dict = new Dictionary(); + dict["YarnName"] = YarnName; + dict["DefinitionName"] = DefinitionName; + dict["Signature"] = Signature; + dict["ReturnType"] = ReturnType; + dict["Language"] = "csharp"; + + if (!string.IsNullOrEmpty(FileName)) + { + dict["FileName"] = FileName; + } + + if (Parameters.Length > 0) + { + var pl = new List>(); + foreach (var p in Parameters) + { + pl.Add(p.ToDictionary()); + } + dict["Parameters"] = pl; + } + return dict; + } + } + + internal Yarn.Unity.ILogger logger; + List commands = new List(); + List functions = new List(); + + internal string Serialise() + { + var commandLine = "\"Commands\":[]"; + var functionLine = "\"Functions\":[]"; + // do we have any commands? + if (commands.Count() > 0) + { + var result = string.Join(",", commands.Select(c => Yarn.Unity.Editor.Json.Serialize(c.ToDictionary()))); + commandLine = $"\"Commands\":[{result}]"; + } + // do we have any functions? + if (functions.Count() > 0) + { + var result = string.Join(",", functions.Select(f => Yarn.Unity.Editor.Json.Serialize(f.ToDictionary()))); + functionLine = $"\"Functions\":[{result}]"; + } + return $"{{{commandLine},{functionLine}}}"; + } + internal void AddAction(YarnAction action) + { + switch (action.Type) + { + case ActionType.Command: + AddCommand(action); + break; + case ActionType.Function: + AddFunction(action); + break; + case ActionType.NotAnAction: + logger.WriteLine($"attempted to make a ysls string for {action.Name}, but it is not a command or function"); + break; + } + } + private void AddFunction(YarnAction action) + { + var parameters = GenerateParams(action.Parameters); + var Signature = $"{action.Name}({string.Join(", ", parameters.Select(p => p.Name))})"; + if (parameters.Length == 0) + { + Signature = $"{action.Name}()"; + } + var function = new YarnActionFunction + { + YarnName = action.Name, + DefinitionName = action.MethodIdentifierName, + Signature = Signature, + Parameters = parameters, + ReturnType = InternalTypeToYarnType(action.MethodSymbol.ReturnType), + FileName = action.SourceFileName, + }; + functions.Add(function); + } + private void AddCommand(YarnAction action) + { + var parameters = GenerateParams(action.Parameters); + var Signature = $"<<{action.Name} {string.Join(" ", parameters.Select(p => p.Name))}>>"; + if (parameters.Length == 0) + { + Signature = $"<<{action.Name}>>"; + } + var command = new YarnActionCommand + { + YarnName = action.Name, + DefinitionName = action.MethodIdentifierName, + Signature = Signature, + Parameters = parameters, + FileName = action.SourceFileName, + }; + commands.Add(command); + } + private YarnActionParameter[] GenerateParams(List parameters) + { + List parameterList = new List(); + foreach (var param in parameters) + { + var paramType = InternalTypeToYarnType(param.Type); + + var parameter = new YarnActionParameter + { + Name = param.Name, + Type = paramType, + IsParamsArray = false, + }; + parameterList.Add(parameter); + } + return parameterList.ToArray(); + } + private string InternalTypeToYarnType(ITypeSymbol symbol) + { + var type = "any"; + switch (symbol.SpecialType) + { + case SpecialType.System_Boolean: + type = "boolean"; + break; + case SpecialType.System_SByte: + case SpecialType.System_Byte: + case SpecialType.System_Int16: + case SpecialType.System_UInt16: + case SpecialType.System_Int32: + case SpecialType.System_UInt32: + case SpecialType.System_Int64: + case SpecialType.System_UInt64: + case SpecialType.System_Decimal: + case SpecialType.System_Single: + case SpecialType.System_Double: + type = "number"; + break; + case SpecialType.System_String: + type = "string"; + break; + } + return type; + } +} diff --git a/Editor/Analysis/Analyser.cs b/Editor/Analysis/Analyser.cs index 51f2d07a..0e6e5396 100644 --- a/Editor/Analysis/Analyser.cs +++ b/Editor/Analysis/Analyser.cs @@ -598,5 +598,126 @@ public static Type GetTypeByName(string name) return null; } + + // these are basically just ripped straight from the LSP + // should maybe look at making these more accessible, for now the code dupe is fine IMO + public static string GetActionTrivia(MethodDeclarationSyntax method, Yarn.Unity.ILogger logger) + { + // The main string to use as the function's documentation. + if (method.HasLeadingTrivia) + { + var trivias = method.GetLeadingTrivia(); + var structuredTrivia = trivias.LastOrDefault(t => t.HasStructure); + if (structuredTrivia.Kind() != SyntaxKind.None) + { + // The method contains structured trivia. Extract the + // documentation for it. + logger.WriteLine("trivia is structured"); + return GetDocumentationFromStructuredTrivia(structuredTrivia); + } + else + { + // There isn't any structured trivia, but perhaps there's a + // comment above the method, which we can use as our + // documentation. + logger.WriteLine("trivia is unstructured"); + return GetDocumentationFromUnstructuredTrivia(trivias); + } + } + else + { + return null; + } + } + private static string GetDocumentationFromUnstructuredTrivia(SyntaxTriviaList trivias) + { + string documentation; + bool emptyLineFlag = false; + var documentationParts = Enumerable.Empty(); + + // loop in reverse order until hit something that doesn't look like it's related + foreach (var trivia in trivias.Reverse()) + { + var doneWithTrivia = false; + switch (trivia.Kind()) + { + case SyntaxKind.EndOfLineTrivia: + // if we hit two lines in a row without a comment/attribute inbetween, we're done collecting trivia + if (emptyLineFlag == true) { doneWithTrivia = true; } + emptyLineFlag = true; + break; + case SyntaxKind.WhitespaceTrivia: + break; + case SyntaxKind.Attribute: + emptyLineFlag = false; + break; + case SyntaxKind.SingleLineCommentTrivia: + case SyntaxKind.MultiLineCommentTrivia: + documentationParts = documentationParts.Prepend(trivia.ToString().Trim('/', ' ')); + emptyLineFlag = false; + break; + default: + doneWithTrivia = true; + break; + } + + if (doneWithTrivia) + { + break; + } + } + + documentation = string.Join(" ", documentationParts); + return documentation; + } + private static string GetDocumentationFromStructuredTrivia(SyntaxTrivia structuredTrivia) + { + string documentation; + var triviaStructure = structuredTrivia.GetStructure(); + if (triviaStructure == null) + { + return null; + } + + string ExtractStructuredTrivia(string tagName) + { + // Find the tag that matches the requested name. + var triviaMatch = triviaStructure + .ChildNodes() + .OfType() + .FirstOrDefault(x => + x.StartTag.Name.ToString() == tagName + ); + + if (triviaMatch != null + && triviaMatch.Kind() != SyntaxKind.None + && triviaMatch.Content.Any()) + { + // Get all content from this element that isn't a newline, and + // join it up into a single string. + var v = triviaMatch + .Content[0] + .ChildTokens() + .Where(ct => ct.Kind() != SyntaxKind.XmlTextLiteralNewLineToken) + .Select(ct => ct.ValueText.Trim()); + + return string.Join(" ", v).Trim(); + } + + return null; + } + + var summary = ExtractStructuredTrivia("summary"); + var remarks = ExtractStructuredTrivia("remarks"); + + documentation = summary ?? triviaStructure.ToString(); + + if (remarks != null) + { + documentation += "\n\n" + remarks; + } + + return documentation; + } } } diff --git a/Editor/YarnSpinnerProjectSettings.cs b/Editor/YarnSpinnerProjectSettings.cs index 5931d082..4c3ba803 100644 --- a/Editor/YarnSpinnerProjectSettings.cs +++ b/Editor/YarnSpinnerProjectSettings.cs @@ -15,9 +15,11 @@ namespace Yarn.Unity.Editor class YarnSpinnerProjectSettings { public static string YarnSpinnerProjectSettingsPath => Path.Combine("ProjectSettings", "Packages", "dev.yarnspinner", "YarnSpinnerProjectSettings.json"); + public static string YarnSpinnerGeneratedYSLSPath => Path.Combine("ProjectSettings", "Packages", "dev.yarnspinner", "generated.ysls.json"); public bool autoRefreshLocalisedAssets = true; public bool automaticallyLinkAttributedYarnCommandsAndFunctions = true; + public bool generateYSLSFile = false; // need to make it os the output can be passed in also so it can log internal static YarnSpinnerProjectSettings GetOrCreateSettings(string path = null, Yarn.Unity.ILogger iLogger = null) @@ -51,6 +53,7 @@ internal static YarnSpinnerProjectSettings GetOrCreateSettings(string path = nul settings.autoRefreshLocalisedAssets = true; settings.automaticallyLinkAttributedYarnCommandsAndFunctions = true; + settings.generateYSLSFile = false; settings.WriteSettings(path, logger); return settings; @@ -70,10 +73,12 @@ private static YarnSpinnerProjectSettings FromJson(string jsonString, Yarn.Unity bool automaticallyLinkAttributedYarnCommandsAndFunctions = true; bool autoRefreshLocalisedAssets = true; + bool generateYSLSFile = false; try { automaticallyLinkAttributedYarnCommandsAndFunctions = (bool)jsonDict["automaticallyLinkAttributedYarnCommandsAndFunctions"]; autoRefreshLocalisedAssets = (bool)jsonDict["autoRefreshLocalisedAssets"]; + generateYSLSFile = (bool)jsonDict["generateYSLSFile"]; } catch (System.Exception e) { @@ -82,6 +87,7 @@ private static YarnSpinnerProjectSettings FromJson(string jsonString, Yarn.Unity settings.automaticallyLinkAttributedYarnCommandsAndFunctions = automaticallyLinkAttributedYarnCommandsAndFunctions; settings.autoRefreshLocalisedAssets = autoRefreshLocalisedAssets; + settings.generateYSLSFile = generateYSLSFile; return settings; } @@ -100,6 +106,7 @@ internal void WriteSettings(string path = null, Yarn.Unity.ILogger iLogger = nul var dictForm = new System.Collections.Generic.Dictionary(); dictForm["automaticallyLinkAttributedYarnCommandsAndFunctions"] = this.automaticallyLinkAttributedYarnCommandsAndFunctions; dictForm["autoRefreshLocalisedAssets"] = this.autoRefreshLocalisedAssets; + dictForm["generateYSLSFile"] = this.generateYSLSFile; var jsonValue = Json.Serialize(dictForm); diff --git a/Editor/YarnSpinnerProjectSettingsProvider.cs b/Editor/YarnSpinnerProjectSettingsProvider.cs index f0eb5caa..6a7341ce 100644 --- a/Editor/YarnSpinnerProjectSettingsProvider.cs +++ b/Editor/YarnSpinnerProjectSettingsProvider.cs @@ -30,30 +30,33 @@ public override void OnGUI(string searchContext) using (new EditorGUI.IndentLevelScope()) { EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField("Update Localised Assets", GUILayout.Width(290), GUILayout.ExpandWidth(false)); var localisedAssetUpdate = EditorGUILayout.Toggle(unsavedSettings.autoRefreshLocalisedAssets, GUILayout.ExpandWidth(false)); - EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField("Generate linkings for Functions and Commands", GUILayout.Width(290), GUILayout.ExpandWidth(false)); var linkingAttributedFuncs = EditorGUILayout.Toggle(unsavedSettings.automaticallyLinkAttributedYarnCommandsAndFunctions, GUILayout.ExpandWidth(false)); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Generate YSLS file for attributed methods", GUILayout.Width(290), GUILayout.ExpandWidth(false)); + var generateYSLS = EditorGUILayout.Toggle(unsavedSettings.generateYSLSFile, GUILayout.ExpandWidth(false)); EditorGUILayout.EndHorizontal(); if (changeCheck.changed) { unsavedSettings.autoRefreshLocalisedAssets = localisedAssetUpdate; unsavedSettings.automaticallyLinkAttributedYarnCommandsAndFunctions = linkingAttributedFuncs; + unsavedSettings.generateYSLSFile = generateYSLS; } bool disabledReimportButton = true; if ( unsavedSettings.automaticallyLinkAttributedYarnCommandsAndFunctions != baseSettings.automaticallyLinkAttributedYarnCommandsAndFunctions || - unsavedSettings.autoRefreshLocalisedAssets != baseSettings.autoRefreshLocalisedAssets + unsavedSettings.autoRefreshLocalisedAssets != baseSettings.autoRefreshLocalisedAssets || + unsavedSettings.generateYSLSFile != baseSettings.generateYSLSFile ) { disabledReimportButton = false; @@ -79,10 +82,15 @@ public override void OnGUI(string searchContext) { needsCSharpRecompilation = true; } + if (baseSettings.generateYSLSFile != unsavedSettings.generateYSLSFile) + { + needsCSharpRecompilation = true; + } // saving the changed settings out to disk baseSettings.autoRefreshLocalisedAssets = unsavedSettings.autoRefreshLocalisedAssets; baseSettings.automaticallyLinkAttributedYarnCommandsAndFunctions = unsavedSettings.automaticallyLinkAttributedYarnCommandsAndFunctions; + baseSettings.generateYSLSFile = unsavedSettings.generateYSLSFile; baseSettings.WriteSettings(); // now we can reimport diff --git a/SourceGenerator/YarnSpinner.Unity.SourceCodeGenerator.dll b/SourceGenerator/YarnSpinner.Unity.SourceCodeGenerator.dll index b5037a97..660281bb 100644 Binary files a/SourceGenerator/YarnSpinner.Unity.SourceCodeGenerator.dll and b/SourceGenerator/YarnSpinner.Unity.SourceCodeGenerator.dll differ