diff --git a/README.md b/README.md index 2e0c20c..e2c39ab 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,9 @@ C:\Users\joshraphael\rascript-language-server_v0.0.1_win-x64.exe - Syntax Highlighting - Custom RAScript syntax highlighting using TextMate. - Function navigation - Jump to a functions defintion. - Code Completion - Completion results appear for symbols as you type. -- Hover Info - Documentation appears when you hover over a function. +- Hover Info - Documentation appears when you hover over a function or class. ## Projects Using rascript-language-server - [vscode-rascript](https://github.com/joshraphael/vscode-rascript) - VSCode language client for RAScript. -- [sublime-rascript](https://github.com/joshraphael/sublime-rascript) - SublimeText language client for RAScript. \ No newline at end of file +- [sublime-rascript](https://github.com/joshraphael/sublime-rascript) - SublimeText language client for RAScript. +- [npp-rascript](https://github.com/joshraphael/npp-rascript) - Notepad++ language client for RAScript. \ No newline at end of file diff --git a/src/BufferManager.cs b/src/BufferManager.cs index f336354..d45d896 100644 --- a/src/BufferManager.cs +++ b/src/BufferManager.cs @@ -40,7 +40,7 @@ public async Task UpdateBufferAsync(string documentPath, StringBuilder buffer, P } } } - p.loadCodeNotes(codeNotes); + // p.loadCodeNotes(codeNotes); RAScript rascript = new RAScript(documentPath, buffer, p); _buffers.AddOrUpdate(documentPath, rascript, (k, v) => rascript); } diff --git a/src/CompletionProvider.cs b/src/CompletionProvider.cs index 2c20551..45ae2fc 100644 --- a/src/CompletionProvider.cs +++ b/src/CompletionProvider.cs @@ -37,13 +37,31 @@ public override Task Handle(CompletionParams request, Cancellati List items = new List(); if (parser != null) { - foreach (var k in parser.GetKeywords()) + HashSet functionSet = [.. parser.completionFunctions]; + foreach (string fnName in functionSet) { - CompletionItemKind kind = parser.GetKeywordCompletionItemKind(k) ?? CompletionItemKind.Text; items.Add(new CompletionItem() { - Label = k, - Kind = kind, + Label = fnName, + Kind = CompletionItemKind.Function, + }); + } + HashSet variableSet = [.. parser.completionVariables]; + foreach (string varName in variableSet) + { + items.Add(new CompletionItem() + { + Label = varName, + Kind = CompletionItemKind.Variable, + }); + } + HashSet classSet = [.. parser.completionClasses]; + foreach (string className in classSet) + { + items.Add(new CompletionItem() + { + Label = className, + Kind = CompletionItemKind.Class, }); } } diff --git a/src/DefinitionProvider.cs b/src/DefinitionProvider.cs index 844c620..0ee1c07 100644 --- a/src/DefinitionProvider.cs +++ b/src/DefinitionProvider.cs @@ -26,24 +26,37 @@ public DefinitionProvider(ILanguageServerFacade router, BufferManager bufferMana public override Task Handle(DefinitionParams request, CancellationToken cancellationToken) { var documentPath = request.TextDocument.Uri.ToString(); - var line = request.Position.Line; - var character = request.Position.Character; var buffer = _bufferManager.GetBuffer(documentPath); var txt = buffer?.GetDocumentText(); - if (txt != null && txt.Length > 0) + if (buffer != null && txt != null && txt.Length > 0) { - var word = buffer?.GetParser().GetWordAtPosition(txt, line, character); - if (word != null && word.Length != 0) + var word = buffer.GetParser().GetWordAtPosition(request.Position); + if (word != null && word.Word.Length != 0) { - Position? pos = buffer?.GetParser().GetLinkLocation(word); - if (pos != null) + int startOffset = buffer.GetParser().GetOffsetAt(word.Start); + int endOffset = buffer.GetParser().GetOffsetAt(word.End); + string hoverClass = buffer.GetParser().DetectClass(startOffset); + if (txt[endOffset+1] != '(') { - var location = new LocationOrLocationLinks(new LocationOrLocationLink(new Location + return Task.FromResult(null); // not a function (maybe string, or just varaible named the same) + } + int origWordOffset = buffer.GetParser().GetOffsetAt(request.Position); + List? list = buffer.GetParser().GetClassFunctionDefinitions(word.Word); + if (list != null) + { + WordScope scope = buffer.GetParser().GetScope(word.Start); + List filteredList = list.Where(buffer.GetParser().ClassFilter(scope.Global, scope.UsingThis, hoverClass)).ToList(); + // can only link to one location, so anything that has multiple definitions wont work for code jumping + if (filteredList.Count == 1) { - Uri = request.TextDocument.Uri, - Range = new Range(pos, pos) - })); - return Task.FromResult(location); + ClassFunction el = filteredList[0]; + LocationOrLocationLinks location = new LocationOrLocationLinks(new LocationOrLocationLink(new Location + { + Uri = request.TextDocument.Uri, + Range = new Range(el.Pos, el.Pos) + })); + return Task.FromResult(location); + } } } } diff --git a/src/FunctionDefinitions.cs b/src/FunctionDefinitions.cs index 7891a66..476a96a 100644 --- a/src/FunctionDefinitions.cs +++ b/src/FunctionDefinitions.cs @@ -1078,12 +1078,4 @@ public FunctionDefinitions() }; } } - - public class FunctionDefinition - { - public required string Key { get; set; } - public required string URL { get; set; } - public required string[] Args { get; set; } - public required string[] CommentDoc { get; set; } - } } \ No newline at end of file diff --git a/src/HoverProvider.cs b/src/HoverProvider.cs index 1265372..6a18913 100644 --- a/src/HoverProvider.cs +++ b/src/HoverProvider.cs @@ -23,28 +23,153 @@ class HoverProvider(ILanguageServerFacade router, BufferManager bufferManager) : public override Task Handle(HoverParams request, CancellationToken cancellationToken) { var documentPath = request.TextDocument.Uri.ToString(); - var line = request.Position.Line; - var character = request.Position.Character; + var t = request.Position; var buffer = _bufferManager.GetBuffer(documentPath); var txt = buffer?.GetDocumentText(); - if (txt != null && txt.Length > 0) + if (buffer != null && txt != null && txt.Length > 0) { - var word = buffer?.GetParser().GetWordAtPosition(txt, line, character); - if (word != null && word.Length != 0) + var word = buffer.GetParser().GetWordAtPosition(request.Position); + if (word != null && word.Word.Length != 0) { - var hoverText = buffer?.GetParser().GetHoverText(word); - if (hoverText != null && hoverText.Length > 0) + int startingOffset = buffer.GetParser().GetOffsetAt(word.Start); + int endingOffset = buffer.GetParser().GetOffsetAt(word.End); + string hoverClass = buffer.GetParser().DetectClass(startingOffset); + int offset = startingOffset - 1; + + // Special case: this keyword should show the class hover info + if (word.Word == "this") { - var content = new List(); - foreach (var l in hoverText) + List? classDefinitions = buffer.GetParser().GetHoverData(hoverClass); + if (classDefinitions != null) { - content.Add(new MarkedString(l)); + foreach (HoverData hoverData in classDefinitions) + { + if (hoverData.ClassName == "") + { + var content = new List(); + foreach (var l in hoverData.Lines) + { + content.Add(new MarkedString(l)); + } + Hover result = new() + { + Contents = new MarkedStringsOrMarkupContent(content) + }; + return Task.FromResult(result); + } + } } - Hover result = new() + } + WordScope scope = buffer.GetParser().GetScope(word.Start); + List? definitions = buffer.GetParser().GetHoverData(word.Word); + if (definitions != null) + { + WordType wordType = buffer.GetParser().GetWordType(word); + if (!wordType.Function && !wordType.Class) { - Contents = new MarkedStringsOrMarkupContent(content.ToArray()) - }; - return Task.FromResult(result); + // only provide hover data for classes and functions + return Task.FromResult(null); + } + // if we are hovering over the actual function signature itself, find it and return it + foreach (HoverData definition in definitions) + { + // magic number 9 here is length of word function plus a space in between the function name + if (startingOffset >= definition.Index && startingOffset <= definition.Index + 9 + definition.Key.Length) + { + var content = new List(); + foreach (var l in definition.Lines) + { + content.Add(new MarkedString(l)); + } + Hover result = new() + { + Contents = new MarkedStringsOrMarkupContent(content) + }; + return Task.FromResult(result); + } + } + + // determine list of definitions for function calls found in code bodies + List filteredDefinitions = new List(); + foreach (HoverData definition in definitions) + { + if (scope.Global) + { + if (definition.ClassName == "") + { + // this should only be one occurence, but we can handle multiple + filteredDefinitions.Add(definition); + } + } + else + { + if (definition.ClassName != "") + { + // Special case: we can determine the exact definition is the definition if using this. + if (scope.UsingThis && hoverClass == definition.ClassName) + { + var content = new List(); + foreach (var l in definition.Lines) + { + content.Add(new MarkedString(l)); + } + Hover result = new() + { + Contents = new MarkedStringsOrMarkupContent(content) + }; + return Task.FromResult(result); + } + // if its a function, further filter down by arg list length + // otherwise just append if its a class + if (wordType.Function) + { + int numArgs = buffer.GetParser().CountArgsAt(endingOffset); + if (numArgs == definition.Args.Length) + { + filteredDefinitions.Add(definition); + } + } + else + { + filteredDefinitions.Add(definition); + } + } + } + } + if (filteredDefinitions.Count == 1) + { + HoverData definition = filteredDefinitions[0]; + var content = new List(); + foreach (var l in definition.Lines) + { + content.Add(new MarkedString(l)); + } + Hover result = new() + { + Contents = new MarkedStringsOrMarkupContent(content) + }; + return Task.FromResult(result); + } + else + { + // Special case: more than one functions in different classes are named the same and we cant determine the exact hover data + string[] lines = []; + foreach (HoverData defintion in filteredDefinitions) + { + lines = lines.Concat(defintion.Lines).ToArray(); + } + HoverData definition = filteredDefinitions[0]; + var content = new List(); + foreach (var l in lines) + { + content.Add(new MarkedString(l)); + } + Hover result = new() + { + Contents = new MarkedStringsOrMarkupContent(content) + }; + return Task.FromResult(result); + } } } } diff --git a/src/Models.cs b/src/Models.cs new file mode 100644 index 0000000..7eb0041 --- /dev/null +++ b/src/Models.cs @@ -0,0 +1,62 @@ +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace RAScriptLanguageServer +{ + public class ClassScope + { + public required int Start { get; set; } + public required int End { get; set; } + public required Dictionary Functions { get; set; } + public required string[] ConstructorArgs { get; set; } + } + public class CommentBounds + { + public required int Start { get; set; } + public required int End { get; set; } + public required string Type { get; set; } + public required string Raw { get; set; } + } + public class FunctionDefinition + { + public required string Key { get; set; } + public required string URL { get; set; } + public required string[] Args { get; set; } + public required string[] CommentDoc { get; set; } + } + + public class ClassFunction + { + public required string ClassName { get; set; } + public required string Name { get; set; } + public required Position Pos { get; set; } + public required string[] Args { get; set; } + } + + public class HoverData + { + public required string Key { get; set; } + public required int Index { get; set; } + public required string ClassName { get; set; } + public required string[] Args { get; set; } + public required string[] Lines { get; set; } + } + + public class WordLocation + { + public required string Word { get; set; } + public required Position Start { get; set; } + public required Position End { get; set; } + } + + public class WordScope + { + public required bool Global { get; set; } + public required bool UsingThis { get; set; } + } + + public class WordType + { + public required bool Function { get; set; } + public required bool Class { get; set; } + } +} \ No newline at end of file diff --git a/src/Parser.cs b/src/Parser.cs index e6b1ee8..63e0c60 100644 --- a/src/Parser.cs +++ b/src/Parser.cs @@ -4,221 +4,691 @@ using System.Text; using RASharp.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Window; +using System.Collections; +using System.Security.AccessControl; +using Microsoft.VisualBasic; namespace RAScriptLanguageServer { public class Parser { public readonly ILanguageServerFacade _router; - private readonly string text; + private readonly string _text; private readonly TextPositions textPositions; - private readonly Dictionary functionLocations; - private readonly Dictionary comments; - private readonly Dictionary keywordKinds; - private readonly List keywords; - private readonly FunctionDefinition[] functionDefinitions; + // private readonly Dictionary functionLocations; + // private readonly Dictionary comments; + // private readonly Dictionary keywordKinds; + // private readonly List keywords; + // private readonly FunctionDefinition[] functionDefinitions; + private readonly CommentBounds[] commentBounds; + private readonly Dictionary classes; + private readonly Dictionary> functionDefinitions; + private readonly Dictionary> words; + public readonly List completionFunctions; + public readonly List completionVariables; + public readonly List completionClasses; private int gameID; private GetCodeNotes? codeNotes; - public Parser(ILanguageServerFacade router, FunctionDefinitions functionDefinitions, string text) + // public Parser(ILanguageServerFacade router, FunctionDefinitions functionDefinitions, string text) + // { + // _router = router; + // this._text = text; + // this._textPositions = new TextPositions(_router, text); + // this.functionLocations = new Dictionary(); + // this.comments = new Dictionary(); + // this.keywordKinds = new Dictionary(); + // this.keywords = new List(); + // this.functionDefinitions = functionDefinitions.functionDefinitions; + // this.commentBounds = this.GetCommentBoundsList(); + // var data = this.GetClassData(); + // this.classes = this.GetClassData(); + // this.functionDefinitionsNew = new Dictionary>(); + // this.words = new Dictionary>(); + // this.completionFunctions = new List(); + // this.completionVariables = new List(); + // this.completionClasses = new List(); + // this.gameID = 0; // game id's start at 1 on RA + // this.Load(); + // Dictionary.KeyCollection keyColl = this.keywordKinds.Keys; + // foreach (string k in keyColl) + // { + // this.keywords.Add(k); + // } + // } + + public Parser(ILanguageServerFacade router, FunctionDefinitions builtinFunctionDefinitions, string text) { _router = router; - this.text = text; + this._text = text; this.textPositions = new TextPositions(_router, text); - this.functionLocations = new Dictionary(); - this.comments = new Dictionary(); - this.keywordKinds = new Dictionary(); - this.keywords = new List(); - this.functionDefinitions = functionDefinitions.functionDefinitions; - this.gameID = 0; // game id's start at 1 on RA - this.Load(); - Dictionary.KeyCollection keyColl = this.keywordKinds.Keys; - foreach (string k in keyColl) - { - this.keywords.Add(k); + this.commentBounds = this.GetCommentBoundsList(); + this.classes = this.GetClassData(); + this.functionDefinitions = new Dictionary>(); + this.words = new Dictionary>(); + this.completionFunctions = new List(); + this.completionVariables = new List(); + this.completionClasses = new List(); + + // Parse each built in function in the document + for (int i = 0; i < builtinFunctionDefinitions.functionDefinitions.Length; i++) + { + FunctionDefinition fn = builtinFunctionDefinitions.functionDefinitions[i]; + + // Add hover data + string comment = string.Join("\n", fn.CommentDoc); + HoverData hover = this.NewHoverText(fn.Key, -1, "function", "", comment, fn.URL, fn.Args); + List? data = this.GetHoverData(fn.Key); + if (data != null) + { + data.Add(hover); + } + else + { + this.words[fn.Key] = new List + { + hover + }; + } + + // Add completion data + completionFunctions.Add(fn.Key); } - } - public void loadCodeNotes(GetCodeNotes? codeNotes) - { - this.codeNotes = codeNotes; - if (this.codeNotes != null && this.codeNotes.Success) + // Parse each class in the document + foreach (var entry in this.classes) { - foreach (var note in this.codeNotes.CodeNotes) + string className = entry.Key; + ClassScope classScope = entry.Value; + + // Add hover info + Position pos = this.textPositions.GetPosition(classScope.Start); + string comment = this.GetCommentText(pos); + HoverData hover = this.NewHoverText(className, classScope.Start, "class", "", comment, "", classScope.ConstructorArgs); + List? data = this.GetHoverData(className); + if (data != null) { - this.comments[note.Address] = [ - $"`{note.Address}`", - "---", - $"```txt\n{note.Note}\n```", - "---", - $"Author: [{note.User}](https://retroachievements.org/user/{note.User})", - ]; + data.Add(hover); + } + else + { + this.words[className] = new List + { + hover + }; + } + + // Add completion data + completionClasses.Add(className); + } + if (this._text != null && this._text != "") + { + // Parse each function in the document + foreach (Match ItemMatch in Regex.Matches(text, @"(\bfunction\b)[\t ]*([a-zA-Z][\w]*)[\t ]*\(([^\(\)]*)\)")) // keep in sync with syntax file rascript.tmLanguage.json #function-definitions regex + { + // dont parse if its in a comment + if (this.InCommentBounds(ItemMatch.Index)) + { + continue; + } + string className = this.DetectClass(ItemMatch.Index); + Position pos = this.textPositions.GetPosition(ItemMatch.Index); + string comment = this.GetCommentText(pos); + string funcName = ItemMatch.Groups.Values.ElementAt(2).ToString(); + string[] args = ItemMatch.Groups.Values.ElementAt(3).ToString().Split(",").Select(s => s.Trim()).ToArray(); + + // add definition info + ClassFunction definition = new ClassFunction + { + ClassName = className, + Name = funcName, + Pos = pos, + Args = args + }; + List? data = this.GetClassFunctionDefinitions(funcName); + if (data != null) + { + data.Add(definition); + } + else + { + this.functionDefinitions[funcName] = new List + { + definition + }; + } + + // add hover info + HoverData hover = this.NewHoverText(funcName, ItemMatch.Index, "function", className, comment, "", args); + List? hoverData = this.GetHoverData(funcName); + if (hoverData != null) + { + hoverData.Add(hover); + } + else + { + this.words[funcName] = new List + { + hover + }; + } + + // add completion info + completionFunctions.Add(funcName); + } + + // Parse each variable in the document + foreach (Match ItemMatch in Regex.Matches(text, @"([a-zA-Z_][\w]*)[\t ]*=")) + { + // dont parse if its in a comment + if (this.InCommentBounds(ItemMatch.Index)) + { + continue; + } + + string varName = ItemMatch.Groups.Values.ElementAt(1).ToString(); + + // add completion info + completionVariables.Add(varName); } } } + // public void loadCodeNotes(GetCodeNotes? codeNotes) + // { + // this.codeNotes = codeNotes; + // if (this.codeNotes != null && this.codeNotes.Success) + // { + // foreach (var note in this.codeNotes.CodeNotes) + // { + // this.comments[note.Address] = [ + // $"`{note.Address}`", + // "---", + // $"```txt\n{note.Note}\n```", + // "---", + // $"Author: [{note.User}](https://retroachievements.org/user/{note.User})", + // ]; + // } + // } + // } + public GetCodeNotes? GetCodeNotes() { return this.codeNotes; } - private void Load() + // private void Load() + // { + // var classes = this.GetClassData(); + // for (int i = 0; i < this.functionDefinitions.Length; i++) + // { + // FunctionDefinition fn = this.functionDefinitions[i]; + // string comment = string.Join("\n", fn.CommentDoc); + // this.comments[fn.Key] = NewHoverData(fn.Key, -1, "", comment, fn.URL, fn.Args); + // this.keywordKinds[fn.Key] = CompletionItemKind.Function; + // } + // if (text != null && text != "") + // { + // foreach (Match ItemMatch in Regex.Matches(text, @"\/\/\s*#ID\s*=\s*(\d+)")) + // { + // string gameIDStr = ItemMatch.Groups.Values.ElementAt(1).ToString(); + // try + // { + // int gameID = int.Parse(gameIDStr); + // if (gameID > 0) + // { + // this.gameID = gameID; + // } + // } + // catch (FormatException) + // { + // this.gameID = 0; // reset the game id + // } + // } + // foreach (Match ItemMatch in Regex.Matches(text, @"(\w+)\s*=")) + // { + // string varName = ItemMatch.Groups.Values.ElementAt(1).ToString(); + // this.keywordKinds[varName] = CompletionItemKind.Variable; + // } + // foreach (Match ItemMatch in Regex.Matches(text, @"(\bfunction\b)[\t ]*([a-zA-Z][\w]*)[\t ]*\(([^\(\)]*)\)")) // keep in sync with syntax file rascript.tmLanguage.json #function-definitions regex + // { + // string className = DetectClass(ItemMatch.Index, classes); + // string funcName = ItemMatch.Groups.Values.ElementAt(2).ToString(); + // Position pos = this._textPositions.GetPosition(ItemMatch.Index); + // functionLocations[funcName] = pos; + // this.keywordKinds[funcName] = CompletionItemKind.Function; + // string[] args = ItemMatch.Groups.Values.ElementAt(3).ToString().Split(",").Select(s => s.Trim()).ToArray(); + // string comment = this.GetCommentText(pos); + // this.comments[funcName] = NewHoverData(funcName, ItemMatch.Index, className, comment, null, args); + // } + // } + // } + + public bool InCommentBounds(int index) { - for (int i = 0; i < this.functionDefinitions.Length; i++) + for (int i = 0; i < this.commentBounds.Length; i++) { - FunctionDefinition fn = this.functionDefinitions[i]; - string comment = string.Join("\n", fn.CommentDoc); - this.comments[fn.Key] = NewHoverData(fn.Key, comment, fn.URL, fn.Args); - this.keywordKinds[fn.Key] = CompletionItemKind.Function; + CommentBounds bound = this.commentBounds[i]; + if (index >= bound.Start && index <= bound.End) + { + return true; + } } - if (text != null && text != "") + return false; + } + + public string DetectClass(int funcPos) { + foreach (var data in this.classes) + { + if (funcPos >= data.Value.Start && funcPos <= data.Value.End) + { + return data.Key; + } + } + return ""; + } + + private Dictionary GetClassData() + { + Dictionary classes = new Dictionary(); + foreach (Match ItemMatch in Regex.Matches(this._text, @"(\bclass\b)[\t ]*([a-zA-Z_][\w]*)")) // keep in sync with syntax file rascript.tmLanguage.json #function-definitions regex { - foreach (Match ItemMatch in Regex.Matches(text, @"\/\/\s*#ID\s*=\s*(\d+)")) + // dont parse if its in a comment + if (this.InCommentBounds(ItemMatch.Index)) { - string gameIDStr = ItemMatch.Groups.Values.ElementAt(1).ToString(); - try + continue; + } + int postClassNameInd = ItemMatch.Index + ItemMatch.Groups.Values.ElementAt(0).Length; + int ind = postClassNameInd; + Stack stack = new Stack(); + string strippedText = ""; // this is used to determine the implicit arguments to a class constructor + while (ind < this._text.Length) + { + // anything other than white space or open curly brace is an error and we just wont parse this class + if (this._text[ind] != ' ' && this._text[ind] != '\n' && this._text[ind] != '\r' && this._text[ind] != '\t' && this._text[ind] != '{') { - int gameID = int.Parse(gameIDStr); - if (gameID > 0) + break; + } + if (this._text[ind] == '{') + { + // get the position of the opening curly brace + stack.Push(ind); + break; + } + ind++; + } + if (stack.Count == 1) + { + // if we have a curly brace scope, start parsing to find the end of the scope + ind = stack.Peek() + 1; // next char after our first open curly brace + while (ind < this._text.Length) + { + if (this._text[ind] == '}') + { + stack.Pop(); + } + else if (this._text[ind] == '{') { - this.gameID = gameID; + stack.Push(ind); + } + else + { + if (stack.Count == 1) + { + // if the code is at the first level of the class (not in a function) append it to our stripped class + strippedText = strippedText + this._text[ind]; + } } + if (stack.Count == 0) + { + // we have found our end position of the scope, break out + break; + } + ind++; } - catch (FormatException) + List args = new List(); + foreach (Match ItemMatch2 in Regex.Matches(strippedText, @"([a-zA-Z_][\w]*)[\t ]*=")) { - this.gameID = 0; // reset the game id + args.Add(ItemMatch2.Groups.Values.ElementAt(1).ToString()); } + ClassScope scope = new ClassScope() + { + Start = ItemMatch.Index, + End = ind, + Functions = new Dictionary(), + ConstructorArgs = args.ToArray() + }; + classes.Add(ItemMatch.Groups.Values.ElementAt(2).ToString(), scope); } - foreach (Match ItemMatch in Regex.Matches(text, @"(\w+)\s*=")) + } + return classes; + } + + public int CountArgsAt(int offset) + { + int count = 0; + offset++; // move one over, the end offset should be at the character at the end of the function name + if (this._text[offset] == '(') + { + offset++; + while (offset < this._text.Length) { - string varName = ItemMatch.Groups.Values.ElementAt(1).ToString(); - this.keywordKinds[varName] = CompletionItemKind.Variable; + if (this._text[offset] == ')') + { + break; + } + if (count == 0) + { + count = 1; + } + else + { + if (this._text[offset] == ',') + { + count++; + } + } + offset++; } - foreach (Match ItemMatch in Regex.Matches(text, @"(\bfunction\b)\s*(\w+)\s*\(([^\(\)]*)\)")) // keep in sync with syntax file rascript.tmLanguage.json #function-definitions regex + } + return count; + } + + private CommentBounds[] GetCommentBoundsList() + { + List commentBounds = new List(); + bool inComment = false; + int tempStart = 0; + if (this._text.Length < 2) + { + return commentBounds.ToArray(); + } + for (int i = 1; i < this._text.Length; i++) + { + if (inComment) { - string funcName = ItemMatch.Groups.Values.ElementAt(2).ToString(); - Position pos = this.textPositions.GetPosition(ItemMatch.Index); - functionLocations[funcName] = pos; - this.keywordKinds[funcName] = CompletionItemKind.Function; - string comment = ""; - string untrimmedComment = ""; - bool blockCommentStarStyle = true; - if (pos.Line > 0) - { - int offset = 1; - bool inBlock = false; - while (pos.Line - offset >= 0) + if (this._text[i] == '\n' || this._text[i] == '\r') + { + inComment = false; + CommentBounds commentBound = new CommentBounds() { - int lineNum = pos.Line - offset; - string? line = this.textPositions.GetLineAt(lineNum); - if (line != null) + Start = tempStart, + End = i - 1, + Type = "Line", + Raw = this._text[tempStart..i] + }; + commentBounds.Add(commentBound); + } + } + else + { + if (this._text[i - 1] == '/' && this._text[i] == '/') + { + inComment = true; + tempStart = i - 1; + } + } + if (i == this._text.Length - 1 && inComment) + { + inComment = false; + CommentBounds commentBound = new CommentBounds() + { + Start = tempStart, + End = i, + Type = "Line", + Raw = this._text[tempStart..] + }; + commentBounds.Add(commentBound); + } + } + // parse different comment types seperately incase they are mixed together, + // the bounds between these two could overlap technically + + // get bounds of block comments + inComment = false; + tempStart = 0; + for (int i = 1; i < this._text.Length; i++) + { + if (inComment) + { + if (this._text[i - 1] == '*' && this._text[i] == '/') // end + { + inComment = false; + CommentBounds commentBound = new CommentBounds + { + Start = tempStart, + End = i - 1, + Type = "Block", + Raw = this._text[tempStart..i] + }; + commentBounds.Add(commentBound); + } + } + else + { + if (this._text[i - 1] == '/' && this._text[i] == '*') // start + { + inComment = true; + tempStart = i - 1; + } + } + if (i == this._text.Length - 1 && inComment) + { + inComment = false; + CommentBounds commentBound = new CommentBounds() + { + Start = tempStart, + End = i, + Type = "Block", + Raw = this._text[tempStart..] + }; + commentBounds.Add(commentBound); + } + } + return commentBounds.ToArray(); + } + + private string GetCommentText(Position pos) + { + string comment = ""; + string untrimmedComment = ""; + bool blockCommentStarStyle = true; + if (pos.Line > 0) + { + int offset = 1; + bool inBlock = false; + while (pos.Line - offset >= 0) + { + int lineNum = pos.Line - offset; + string? line = this.textPositions.GetLineAt(lineNum); + if (line != null) + { + line = line.TrimStart(); + if (offset == 1) + { + bool isBlock = Regex.IsMatch(line, @"^.*\*\/$"); + if (isBlock) { - line = line.TrimStart(); - if (offset == 1) + inBlock = true; + } + } + if (inBlock) // Block comment + { + bool endBlock = Regex.IsMatch(line, @"^.*\/\*.*$"); + if (endBlock) + { + // Trim start token + string[] trimmedLine = Regex.Split(line, @"\/\*(.*)", RegexOptions.Singleline).Skip(1).ToArray(); + string newLine = string.Join("", trimmedLine).TrimStart(); + + // Trim end token + trimmedLine = newLine.Split("*/"); // use whats after the star token + if (trimmedLine.Length > 2) { - bool isBlock = Regex.IsMatch(line, @"^.*\*\/$"); - if (isBlock) - { - inBlock = true; - } + trimmedLine = trimmedLine[..^1]; // remove last element } - if (inBlock) // Block comment + newLine = string.Join("", trimmedLine).TrimStart(); + if (blockCommentStarStyle) { - bool endBlock = Regex.IsMatch(line, @"^.*\/\*.*$"); - if (endBlock) - { - // Trim start token - string[] trimmedLine = Regex.Split(line, @"\/\*(.*)", RegexOptions.Singleline).Skip(1).ToArray(); - string newLine = string.Join("", trimmedLine).TrimStart(); - - // Trim end token - trimmedLine = newLine.Split("*/"); // use whats after the star token - if (trimmedLine.Length > 2) - { - trimmedLine = trimmedLine[..^1]; // remove last element - } - newLine = string.Join("", trimmedLine).TrimStart(); - if (blockCommentStarStyle) - { - bool starComment = Regex.IsMatch(newLine, @"^\*.*"); - if (!starComment) - { - blockCommentStarStyle = false; - } - } - untrimmedComment = "//" + newLine + "\n" + untrimmedComment; // keep an untrimmed version of the comment in case the entire block is prefixed with stars - - // Trim first '*' token (in case they comment that way) - trimmedLine = Regex.Split(newLine, @"^\*(.*)", RegexOptions.Singleline).ToArray(); - if (trimmedLine.Length > 2) - { - trimmedLine = trimmedLine[1..]; // remove first element - } - newLine = string.Join("", trimmedLine).TrimStart(); - comment = "//" + newLine + "\n" + comment; - break; - } - else // at end of comment block + bool starComment = Regex.IsMatch(newLine, @"^\*.*"); + if (!starComment) { - // Trim end token (guaranteed to not have text after end token if the user wants comments to appear in hover box) - string[] trimmedLine = line.Split("*/"); - if (trimmedLine.Length > 2) - { - trimmedLine = trimmedLine[..^1]; // remove last element - } - string newLine = string.Join("", trimmedLine).TrimStart(); - - if (blockCommentStarStyle) - { - bool starComment = Regex.IsMatch(newLine, @"^\*.*"); - if (!starComment) - { - blockCommentStarStyle = false; - } - } - untrimmedComment = "//" + newLine + "\n" + untrimmedComment; // keep an untrimmed version of the comment in case the entire block is prefixed with stars - - // Trim first '*' token (in case they comment that way) - trimmedLine = Regex.Split(newLine, @"^\*(.*)", RegexOptions.Singleline).ToArray(); - if (trimmedLine.Length > 2) - { - trimmedLine = trimmedLine[1..]; // remove first element - } - newLine = string.Join("", trimmedLine).TrimStart(); - comment = "//" + newLine + "\n" + comment; + blockCommentStarStyle = false; } } - else // Single line comment + untrimmedComment = "//" + newLine + "\n" + untrimmedComment; // keep an untrimmed version of the comment in case the entire block is prefixed with stars + + // Trim first '*' token (in case they comment that way) + trimmedLine = Regex.Split(newLine, @"^\*(.*)", RegexOptions.Singleline).ToArray(); + if (trimmedLine.Length > 2) { - bool isComment = Regex.IsMatch(line, @"^\/\/.*$"); - if (isComment) - { - comment = line + "\n" + comment; - } - else + trimmedLine = trimmedLine[1..]; // remove first element + } + newLine = string.Join("", trimmedLine).TrimStart(); + comment = "//" + newLine + "\n" + comment; + break; + } + else // at end of comment block + { + // Trim end token (guaranteed to not have text after end token if the user wants comments to appear in hover box) + string[] trimmedLine = line.Split("*/"); + if (trimmedLine.Length > 2) + { + trimmedLine = trimmedLine[..^1]; // remove last element + } + string newLine = string.Join("", trimmedLine).TrimStart(); + + if (blockCommentStarStyle) + { + bool starComment = Regex.IsMatch(newLine, @"^\*.*"); + if (!starComment) { - break; + blockCommentStarStyle = false; } } + untrimmedComment = "//" + newLine + "\n" + untrimmedComment; // keep an untrimmed version of the comment in case the entire block is prefixed with stars + + // Trim first '*' token (in case they comment that way) + trimmedLine = Regex.Split(newLine, @"^\*(.*)", RegexOptions.Singleline).ToArray(); + if (trimmedLine.Length > 2) + { + trimmedLine = trimmedLine[1..]; // remove first element + } + newLine = string.Join("", trimmedLine).TrimStart(); + comment = "//" + newLine + "\n" + comment; + } + } + else // Single line comment + { + bool isComment = Regex.IsMatch(line, @"^\/\/.*$"); + if (isComment) + { + comment = line + "\n" + comment; + } + else + { + break; } - offset = offset + 1; } } - // do something - string[] args = ItemMatch.Groups.Values.ElementAt(3).ToString().Split(",").Select(s => s.Trim()).ToArray(); - if (blockCommentStarStyle) + offset = offset + 1; + } + } + string finalComment = untrimmedComment; + if (blockCommentStarStyle) + { + finalComment = comment; + } + return finalComment; + } + + private HoverData NewHoverText(string key, int index, string type, string className, string text, string? docUrl, string[] args) + { + string argStr = string.Join(", ", args); + string[] commentLines = Regex.Split(text, @"\r?\n"); + List lines = new List(); + string prefix = "function "; + if (className != "") + { + prefix = $"// class {className}\nfunction "; + } + lines.Add($"```rascript\n{prefix}{key}({argStr})\n```"); + if (type == "class") + { + string fnLine = lines[0]; + lines.Clear(); + lines.Add($"```rascript\nclass {key}\n```"); + lines.Add(fnLine); + } + if (text != null && text != "") + { + lines.Add("---"); + string curr = ""; + bool codeBlock = false; + for (int i = 0; i < commentLines.Length; i++) + { + string line = Regex.Replace(commentLines[i], @"^\/\/", "").TrimStart(); + if (line.StartsWith("```")) { - this.comments[funcName] = NewHoverData(funcName, comment, null, args); + codeBlock = !codeBlock; + if (codeBlock) + { + curr = line; + } + else + { + curr = curr + "\n" + line; + lines.Add(curr); + curr = ""; + } + continue; + } + if (line.StartsWith('|') || line.StartsWith('*')) + { + line = line + "\n"; + } + if (codeBlock) + { + curr = curr + "\n" + line; } else { - this.comments[funcName] = NewHoverData(funcName, untrimmedComment, null, args); + if (line == "") + { + lines.Add(curr); + curr = ""; + } + else + { + curr = curr + " " + line; + } } } + if (curr != "") + { + lines.Add(curr); + } + if (codeBlock) + { + lines.Add("```"); + } } + if (docUrl != null && docUrl != "") + { + lines.Add("---"); + lines.Add($"[Wiki link for `{key}()`]({docUrl})"); + } + return new HoverData + { + Key = key, + Index = index, + ClassName = className, + Args = args, + Lines = lines.ToArray() + }; } - private string[] NewHoverData(string key, string text, string? docUrl, string[] args) + private string[] NewHoverData(string key, int index, string className, string text, string? docUrl, string[] args) { string argStr = string.Join(", ", args); string[] commentLines = Regex.Split(text, @"\r?\n"); @@ -285,17 +755,40 @@ private string[] NewHoverData(string key, string text, string? docUrl, string[] return lines.ToArray(); } - public string GetWordAtPosition(string txt, long lineNum, long character) + public Func ClassFilter(bool global, bool usingThis, string className) { - var lines = txt.Split('\n'); - var line = lines[lineNum]; - var index = Convert.ToInt32(character); + return (el) => + { + if (global) + { + return el.ClassName == ""; + } + else if (usingThis) + { + return el.ClassName == className; + } + return el.ClassName != ""; + }; + } + + public WordLocation GetWordAtPosition(Position pos) + { + var lines = this._text.Split('\n'); + var line = lines[pos.Line]; + var index = Convert.ToInt32(pos.Character); + var leftIndex = Convert.ToInt32(pos.Character); + var rightIndex = Convert.ToInt32(pos.Character); if (index >= line.Length) { - return ""; + return new WordLocation + { + Word = "", + Start = pos, + End = pos + }; } - var initialChar = line[Convert.ToInt32(character)]; + var initialChar = line[Convert.ToInt32(pos.Character)]; StringBuilder word = new StringBuilder(); if (IsWordLetter(initialChar)) { @@ -309,6 +802,7 @@ public string GetWordAtPosition(string txt, long lineNum, long character) if (IsWordLetter(line[i])) { word.Insert(0, line[i]); // Prepend + leftIndex = i; continue; } break; @@ -321,48 +815,176 @@ public string GetWordAtPosition(string txt, long lineNum, long character) if (IsWordLetter(line[i])) { word.Append(line[i]); + rightIndex = i; continue; } break; } } } - return word.ToString(); + return new WordLocation + { + Word = word.ToString(), + Start = new Position + { + Line = pos.Line, + Character = leftIndex, + }, + End = new Position + { + Line = pos.Line, + Character = rightIndex, + }, + }; } - public static bool IsWordLetter(char c) + public int GetOffsetAt(Position pos) { - return char.IsLetterOrDigit(c) || c == '_'; + var lines = this._text.Split('\n'); + var line = lines[pos.Line]; + var index = Convert.ToInt32(pos.Character); + var leftInd = Convert.ToInt32(pos.Character); + var rightInd = Convert.ToInt32(pos.Character); + + if (index >= line.Length) + { + return -1; + } + var realLines = new List(); + for (int i = 0; i < pos.Line; i++) + { + realLines.Add(lines[i]); + } + var partialString = line.Substring(0, index); + realLines.Add(partialString); + return String.Join("\n", realLines.ToArray()).Length; } - public Position? GetLinkLocation(string word) + public WordScope GetScope(Position pos) { - if (this.functionLocations.ContainsKey(word)) + bool global = true; + bool usingThis = false; + int offset = this.GetOffsetAt(pos) - 1; + + while (global && offset >= 0) { - return this.functionLocations[word]; + if (this._text[offset] != ' ' && this._text[offset] != '\t' && this._text[offset] != '.') + { + break; + } + if (this._text[offset] == '.') + { + // in here means the previous non whitespace character next to the word hovered over is a dot which is the class attribute accessor operator + global = false; + if (offset - 4 >= 0) + { + if (this._text[offset - 4] == 't' && this._text[offset - 3] == 'h' && this._text[offset - 2] == 'i' && this._text[offset - 1] == 's') + { + usingThis = true; + } + } + break; + } + offset--; } - return null; + return new WordScope + { + Global = global, + UsingThis = usingThis + }; } - public string[]? GetHoverText(string word) + public WordType GetWordType(WordLocation location) { - if (this.comments.ContainsKey(word)) + bool fn = false; + bool cls = false; + int startOffset = this.GetOffsetAt(location.Start); + int endOffset = this.GetOffsetAt(location.End); + + // check for function + if (endOffset+1 <= this._text.Length && this._text[endOffset+1] == '(') { - return this.comments[word]; + fn = true; } - return null; + int offset = startOffset - 1; + while (offset >= 0) + { + // start searching for the previous word to be 'class' + if (this._text[offset] != ' ' && this._text[offset] != '\t' && this._text[offset] != 's') + { + break; + } + if (this._text[offset] == 's') + { + if (offset - 4 >= 0) + { + if (this._text[offset - 4] == 'c' && this._text[offset - 3] == 'l' && this._text[offset - 2] == 'a' && this._text[offset - 1] == 's') + { + cls = true; + } + } + break; + } + offset--; + } + return new WordType + { + Function = fn, + Class = cls, + }; + } + + public static bool IsWordLetter(char c) + { + return char.IsLetterOrDigit(c) || c == '_'; } - public string[] GetKeywords() + // public Position? GetLinkLocation(string word) + // { + // if (this.functionLocations.ContainsKey(word)) + // { + // return this.functionLocations[word]; + // } + // return null; + // } + + // public string[]? GetHoverText(string word) + // { + // if (this.comments.ContainsKey(word)) + // { + // return this.comments[word]; + // } + // return null; + // } + + // public string[] GetKeywords() + // { + // return this.keywords.ToArray(); + // } + + // public CompletionItemKind? GetKeywordCompletionItemKind(string keyword) + // { + // if (this.keywordKinds.ContainsKey(keyword)) + // { + // return this.keywordKinds[keyword]; + // } + // return null; + // } + + public List? GetHoverData(string className) { - return this.keywords.ToArray(); + if (this.words.ContainsKey(className)) + { + return this.words[className]; + } + return null; } - public CompletionItemKind? GetKeywordCompletionItemKind(string keyword) + public List? GetClassFunctionDefinitions(string className) { - if (this.keywordKinds.ContainsKey(keyword)) + if (this.functionDefinitions.ContainsKey(className)) { - return this.keywordKinds[keyword]; + return this.functionDefinitions[className]; } return null; }