From 8b3d77af5c02f0771020f27409dfed62ae81b9c8 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Wed, 24 Jul 2019 15:48:48 -0700 Subject: [PATCH 1/4] First attempt, kinda works --- .../CompilationManager/DataStructures.cs | 24 +++++ .../CompilationManager/EditorSupport.cs | 100 +++++++++++++++++- 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/QsCompiler/CompilationManager/DataStructures.cs b/src/QsCompiler/CompilationManager/DataStructures.cs index 580d25af25..e352392de0 100644 --- a/src/QsCompiler/CompilationManager/DataStructures.cs +++ b/src/QsCompiler/CompilationManager/DataStructures.cs @@ -220,6 +220,30 @@ internal TokenIndex(FileContentManager file, int line, int index) this.Index = index; } + internal TokenIndex(FileContentManager file, Position position) + { + this.File = file ?? throw new ArgumentNullException(nameof(file)); + if (position.Line < 0 || position.Line >= file.NrTokenizedLines()) throw new ArgumentOutOfRangeException(nameof(position)); + + this.Line = position.Line; + + int index = -1; + var line = file.GetTokenizedLine(position.Line); + for (int i = 0; i < line.Count(); i++) + { + CodeFragment frag = line[i]; + // if the given position is within the fragment + if (frag.GetRange().Start.Character <= position.Character && + frag.GetRange().End.Character >= position.Character) + { + index = i; + break; + } + } + if (index < 0) throw new ArgumentOutOfRangeException(nameof(position)); + this.Index = index; + } + internal TokenIndex(TokenIndex tIndex) : this(tIndex.File, tIndex.Line, tIndex.Index) { } diff --git a/src/QsCompiler/CompilationManager/EditorSupport.cs b/src/QsCompiler/CompilationManager/EditorSupport.cs index c3b2e90b45..0880b8ee2c 100644 --- a/src/QsCompiler/CompilationManager/EditorSupport.cs +++ b/src/QsCompiler/CompilationManager/EditorSupport.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading.Tasks; +using Markdig.Syntax.Inlines; using Microsoft.Quantum.QsCompiler.CompilationBuilder.DataStructures; using Microsoft.Quantum.QsCompiler.DataTypes; using Microsoft.Quantum.QsCompiler.Diagnostics; @@ -14,6 +16,7 @@ using Microsoft.Quantum.QsCompiler.TextProcessing; using Microsoft.Quantum.QsCompiler.Transformations.SearchAndReplace; using Microsoft.VisualStudio.LanguageServer.Protocol; +using YamlDotNet.Core.Tokens; using QsSymbolInfo = Microsoft.Quantum.QsCompiler.SyntaxProcessing.SyntaxExtensions.SymbolInformation; @@ -346,6 +349,88 @@ public static WorkspaceEdit Rename(this FileContentManager file, CompilationUnit }; } + private static WorkspaceEdit ProcessForLoopIntroFrag(FileContentManager file, VersionedTextDocumentIdentifier versionedFileId, CodeFragment frag) + { + bool checkStartWithZero = false; + bool checkHasLengthCall = false; + bool checkSubOne = false; + + Position fragStart = frag.GetRange().Start; + + Position beforeArgsStart = null; + Position beforeArgsEnd = null; + Position afterArgsStart = null; + Position afterArgsEnd = null; + + var forLoopIntroExpression = (QsFragmentKind.ForLoopIntro)frag.Kind; + + if (!forLoopIntroExpression.Item2.Range.IsNull) + { + var rangeStart = ((QsFragmentKind.ForLoopIntro)frag.Kind).Item2.Range.Item.Item1; + var rangeEnd = ((QsFragmentKind.ForLoopIntro)frag.Kind).Item2.Range.Item.Item2; + + beforeArgsStart = new Position(fragStart.Line + rangeStart.Line - 1, fragStart.Character + rangeStart.Column - 1); + afterArgsEnd = new Position(fragStart.Line + rangeEnd.Line - 1, fragStart.Character + rangeEnd.Column - 1); + } + + if (forLoopIntroExpression.Item2.Expression is QsExpressionKind.RangeLiteral rangeExpression) + { + if (rangeExpression.Item1.Expression is QsExpressionKind.IntLiteral intLiteralExpression) + { + checkStartWithZero = intLiteralExpression.Item == 0L; + } + + if (rangeExpression.Item2.Expression is QsExpressionKind.SUB SUBExpression) + { + if (SUBExpression.Item1.Expression is QsExpressionKind.CallLikeExpression callLikeExression) + { + if (callLikeExression.Item1.Expression is QsExpressionKind.Identifier identifier) + { + var identifierSub = (QsSymbolKind.Symbol)identifier.Item1.Symbol; + checkHasLengthCall = identifierSub.Item.Value == "Length"; + } + + if (callLikeExression.Item2.Expression is QsExpressionKind.ValueTuple valueTuple) + { + if(!callLikeExression.Item2.Range.IsNull) + { + var argRangeStart = callLikeExression.Item2.Range.Item.Item1; + var argRangeEnd = callLikeExression.Item2.Range.Item.Item2; + + beforeArgsEnd = new Position(fragStart.Line + argRangeStart.Line - 1, fragStart.Character + argRangeStart.Column - 1); + afterArgsStart = new Position(fragStart.Line + argRangeEnd.Line - 1, fragStart.Character + argRangeEnd.Column - 1); + } + } + } + + if (SUBExpression.Item2.Expression is QsExpressionKind.IntLiteral subIntLiteralExpression) + { + checkSubOne = subIntLiteralExpression.Item == 1L; + } + } + } + + bool positionsAreNonNull = + beforeArgsStart != null && + beforeArgsEnd != null && + afterArgsStart != null && + afterArgsEnd != null; + if (positionsAreNonNull && checkStartWithZero && checkHasLengthCall && checkSubOne) + { + TextEdit beforeArgsEdit = new TextEdit() { Range = new Range() { Start = beforeArgsStart, End = beforeArgsEnd }, NewText = "IndexRange" }; + TextEdit afterArgsEdit = new TextEdit() { Range = new Range() { Start = afterArgsStart, End = afterArgsEnd }, NewText = "" }; + + // Build WorkspaceEdit + return new WorkspaceEdit + { + DocumentChanges = new[] { new TextDocumentEdit { TextDocument = versionedFileId, Edits = new[] { beforeArgsEdit, afterArgsEdit } } }, + Changes = new Dictionary { { file.FileName.Value, new[] { beforeArgsEdit, afterArgsEdit } } } + }; + } + + return null; + } + /// /// Returns a dictionary of workspace edits suggested by the compiler for the given location and context. /// The keys of the dictionary are suitable titles for each edit that can be presented to the user. @@ -357,6 +442,8 @@ public static ImmutableDictionary CodeActions(this FileCo if (compilation == null || context?.Diagnostics == null) return null; var versionedFileId = new VersionedTextDocumentIdentifier { Uri = file.Uri, Version = 1 }; // setting version to null here won't work in VS Code ... + CodeFragment frag = file?.TryGetFragmentAt(range.Start, true); + WorkspaceEdit GetWorkspaceEdit(TextEdit edit) => new WorkspaceEdit { DocumentChanges = new[] { new TextDocumentEdit { TextDocument = versionedFileId, Edits = new[] { edit } } }, @@ -369,6 +456,16 @@ public static ImmutableDictionary CodeActions(this FileCo var ambiguousTypes = context.Diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.AmbiguousType)); var unknownTypes = context.Diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.UnknownType)); + List<(string, WorkspaceEdit)> suggestionedIndexRangeReplaces = new List<(string, WorkspaceEdit)>(); + if (frag.Kind.IsForLoopIntro) + { + WorkspaceEdit edit = ProcessForLoopIntroFrag(file, versionedFileId, frag); + if (edit != null) + { + suggestionedIndexRangeReplaces.Add(("Use IndexRange instead of Length", edit)); + } + } + // suggestions for ambiguous ids and types (string, WorkspaceEdit) SuggestedNameQualification(NonNullable suggestedNS, string id, Position pos) @@ -385,7 +482,7 @@ public static ImmutableDictionary CodeActions(this FileCo .Select(ns => SuggestedNameQualification(ns, id, pos))); if (!unknownCallables.Any() && !unknownTypes.Any()) - { return suggestedIdQualifications.Concat(suggestedTypeQualifications).ToImmutableDictionary(s => s.Item1, s => s.Item2); } + { return suggestedIdQualifications.Concat(suggestedTypeQualifications).Concat(suggestionedIndexRangeReplaces).ToImmutableDictionary(s => s.Item1, s => s.Item2); } // suggestions for unknown ids and types @@ -418,6 +515,7 @@ public static ImmutableDictionary CodeActions(this FileCo return suggestionsForIds.Concat(suggestionsForTypes) .Concat(suggestedIdQualifications).Concat(suggestedTypeQualifications) + .Concat(suggestionedIndexRangeReplaces) .ToImmutableDictionary(s => s.Item1, s => s.Item2); } From e04f5592fb871292f6cafe9a1abe45ab22bc6cfd Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Wed, 24 Jul 2019 16:15:12 -0700 Subject: [PATCH 2/4] Fixed conversion from inner fragment position to global position so the code action can fix multi-line fragments. --- src/QsCompiler/CompilationManager/EditorSupport.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/QsCompiler/CompilationManager/EditorSupport.cs b/src/QsCompiler/CompilationManager/EditorSupport.cs index 0880b8ee2c..282c45e6e4 100644 --- a/src/QsCompiler/CompilationManager/EditorSupport.cs +++ b/src/QsCompiler/CompilationManager/EditorSupport.cs @@ -362,6 +362,12 @@ private static WorkspaceEdit ProcessForLoopIntroFrag(FileContentManager file, Ve Position afterArgsStart = null; Position afterArgsEnd = null; + Position ConvertFragPosToGlobalPos(QsPositionInfo posToConvert) => new Position + ( + fragStart.Line + posToConvert.Line - 1, + (posToConvert.Line == 1 ? fragStart.Character : 0) + posToConvert.Column - 1 + ); + var forLoopIntroExpression = (QsFragmentKind.ForLoopIntro)frag.Kind; if (!forLoopIntroExpression.Item2.Range.IsNull) @@ -369,8 +375,8 @@ private static WorkspaceEdit ProcessForLoopIntroFrag(FileContentManager file, Ve var rangeStart = ((QsFragmentKind.ForLoopIntro)frag.Kind).Item2.Range.Item.Item1; var rangeEnd = ((QsFragmentKind.ForLoopIntro)frag.Kind).Item2.Range.Item.Item2; - beforeArgsStart = new Position(fragStart.Line + rangeStart.Line - 1, fragStart.Character + rangeStart.Column - 1); - afterArgsEnd = new Position(fragStart.Line + rangeEnd.Line - 1, fragStart.Character + rangeEnd.Column - 1); + beforeArgsStart = ConvertFragPosToGlobalPos(rangeStart); + afterArgsEnd = ConvertFragPosToGlobalPos(rangeEnd); } if (forLoopIntroExpression.Item2.Expression is QsExpressionKind.RangeLiteral rangeExpression) @@ -397,8 +403,8 @@ private static WorkspaceEdit ProcessForLoopIntroFrag(FileContentManager file, Ve var argRangeStart = callLikeExression.Item2.Range.Item.Item1; var argRangeEnd = callLikeExression.Item2.Range.Item.Item2; - beforeArgsEnd = new Position(fragStart.Line + argRangeStart.Line - 1, fragStart.Character + argRangeStart.Column - 1); - afterArgsStart = new Position(fragStart.Line + argRangeEnd.Line - 1, fragStart.Character + argRangeEnd.Column - 1); + beforeArgsEnd = ConvertFragPosToGlobalPos(argRangeStart); + afterArgsStart = ConvertFragPosToGlobalPos(argRangeEnd); } } } From 562c04d27bed88f83619b4eef115513391b4a75a Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Wed, 24 Jul 2019 21:06:13 -0700 Subject: [PATCH 3/4] Pretty Messy, but now the code action will also provide the appropriate open directive, if it is missing. --- .../CompilationManager/EditorSupport.cs | 98 ++++++++++++++----- 1 file changed, 75 insertions(+), 23 deletions(-) diff --git a/src/QsCompiler/CompilationManager/EditorSupport.cs b/src/QsCompiler/CompilationManager/EditorSupport.cs index 282c45e6e4..a803de1f60 100644 --- a/src/QsCompiler/CompilationManager/EditorSupport.cs +++ b/src/QsCompiler/CompilationManager/EditorSupport.cs @@ -24,6 +24,10 @@ namespace Microsoft.Quantum.QsCompiler.CompilationBuilder { internal static class EditorSupport { + // ToDo: replace references to these constants with programmatic references method names + private const string LENGTH_METHOD_NAME = "Length"; + private const string INDEXRANGE_METHOD_NAME = "IndexRange"; + // utils for getting the necessary information for editor commands /// @@ -111,6 +115,18 @@ private static IEnumerable> NamespaceSuggestionsForTypeAtPos : ImmutableArray>.Empty; } + /// + /// Returns the CodeFragment token that represents the namespace + /// declaration for the namespace surrounding the given position. + /// + private static CodeFragment.TokenIndex GetNamespaceToken(FileContentManager file, Position position) + { + // going by line here is fine - I am ok with a failure if someone has muliple namespace and callable declarations on the same line... + return file.NamespaceDeclarationTokens() + .TakeWhile(t => t.Line <= position.Line) + .LastOrDefault(); + } + /// /// Sets the out parameter to the code fragment that overlaps with the given position in the given file /// if such a fragment exists, or to null otherwise. @@ -370,6 +386,8 @@ private static WorkspaceEdit ProcessForLoopIntroFrag(FileContentManager file, Ve var forLoopIntroExpression = (QsFragmentKind.ForLoopIntro)frag.Kind; + // ToDo: need to use a better method for matching fragments to expression patterns + if (!forLoopIntroExpression.Item2.Range.IsNull) { var rangeStart = ((QsFragmentKind.ForLoopIntro)frag.Kind).Item2.Range.Item.Item1; @@ -393,7 +411,7 @@ private static WorkspaceEdit ProcessForLoopIntroFrag(FileContentManager file, Ve if (callLikeExression.Item1.Expression is QsExpressionKind.Identifier identifier) { var identifierSub = (QsSymbolKind.Symbol)identifier.Item1.Symbol; - checkHasLengthCall = identifierSub.Item.Value == "Length"; + checkHasLengthCall = identifierSub.Item.Value == LENGTH_METHOD_NAME; } if (callLikeExression.Item2.Expression is QsExpressionKind.ValueTuple valueTuple) @@ -423,10 +441,10 @@ private static WorkspaceEdit ProcessForLoopIntroFrag(FileContentManager file, Ve afterArgsEnd != null; if (positionsAreNonNull && checkStartWithZero && checkHasLengthCall && checkSubOne) { - TextEdit beforeArgsEdit = new TextEdit() { Range = new Range() { Start = beforeArgsStart, End = beforeArgsEnd }, NewText = "IndexRange" }; + // Build WorkspaceEdit + TextEdit beforeArgsEdit = new TextEdit() { Range = new Range() { Start = beforeArgsStart, End = beforeArgsEnd }, NewText = INDEXRANGE_METHOD_NAME }; TextEdit afterArgsEdit = new TextEdit() { Range = new Range() { Start = afterArgsStart, End = afterArgsEnd }, NewText = "" }; - // Build WorkspaceEdit return new WorkspaceEdit { DocumentChanges = new[] { new TextDocumentEdit { TextDocument = versionedFileId, Edits = new[] { beforeArgsEdit, afterArgsEdit } } }, @@ -449,6 +467,7 @@ public static ImmutableDictionary CodeActions(this FileCo var versionedFileId = new VersionedTextDocumentIdentifier { Uri = file.Uri, Version = 1 }; // setting version to null here won't work in VS Code ... CodeFragment frag = file?.TryGetFragmentAt(range.Start, true); + CodeFragment.TokenIndex currNsToken = null; WorkspaceEdit GetWorkspaceEdit(TextEdit edit) => new WorkspaceEdit { @@ -456,6 +475,26 @@ public static ImmutableDictionary CodeActions(this FileCo Changes = new Dictionary { { file.FileName.Value, new[] { edit } } } }; + // determine the first fragment in the containing namespace + currNsToken = currNsToken ?? GetNamespaceToken(file, range.Start); + var firstInNs = currNsToken + ?.GetChildren(deep: false)?.FirstOrDefault()?.GetFragment(); + if (firstInNs == null) return null; + var insertOpenDirAt = firstInNs.GetRange().Start; + + // range and whitespace info for inserting open directives + var openDirEditRange = new Range { Start = insertOpenDirAt, End = insertOpenDirAt }; + var indentationAfterOpenDir = file.GetLine(insertOpenDirAt.Line).Text.Substring(0, insertOpenDirAt.Character); + var additionalLinesAfterOpenDir = firstInNs.Kind.OpenedNamespace().IsNull ? $"{Environment.NewLine}{Environment.NewLine}" : ""; + var whitespaceAfterOpenDir = $"{Environment.NewLine}{additionalLinesAfterOpenDir}{indentationAfterOpenDir}"; + + (string, WorkspaceEdit) SuggestedOpenDirective(NonNullable suggestedNS) + { + var directive = $"{Keywords.importDirectiveHeader.id} {suggestedNS.Value}"; + var edit = new TextEdit { Range = openDirEditRange, NewText = $"{directive};{whitespaceAfterOpenDir}" }; + return (directive, GetWorkspaceEdit(edit)); + } + // diagnostics based on which suggestions are given var ambiguousCallables = context.Diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.AmbiguousCallable)); var unknownCallables = context.Diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.UnknownIdentifier)); @@ -466,6 +505,39 @@ public static ImmutableDictionary CodeActions(this FileCo if (frag.Kind.IsForLoopIntro) { WorkspaceEdit edit = ProcessForLoopIntroFrag(file, versionedFileId, frag); + + // get namespace string + string namespaceString = string.Empty; + var currNs = currNsToken?.GetFragment(); + if (currNs != null && currNs.Kind is QsFragmentKind.NamespaceDeclaration namespaceFrag) + { + namespaceString = ((QsSymbolKind.Symbol)namespaceFrag.Item.Symbol).Item.Value; + } + if (namespaceString != string.Empty) + { + var opens = compilation.GlobalSymbols.OpenDirectives(NonNullable.New(namespaceString)); + var possibleNamespaces = compilation.GlobalSymbols.NamespacesContainingCallable(NonNullable.New(INDEXRANGE_METHOD_NAME)); + var currentOpenNamespaces = opens[file.FileName].Select(i => i.Item1); + + // if there is not a namespace opened that has the IndexRange function, add open directive to edit + if(!possibleNamespaces.Intersect(currentOpenNamespaces).Any()) + { + var arrayNamespace = possibleNamespaces.FirstOrDefault(); + var namespaceTextEdit = new TextEdit + { + Range = openDirEditRange, + NewText = $"{Keywords.importDirectiveHeader.id} {arrayNamespace.Value};{whitespaceAfterOpenDir}" + }; + + // append open directive edit to the list of edits + if (edit != null) + { + edit.DocumentChanges.First().Edits = edit.DocumentChanges.First().Edits.Concat(new[] { namespaceTextEdit }).ToArray(); + edit.Changes[file.FileName.Value] = edit.Changes[file.FileName.Value].Concat(new[] { namespaceTextEdit }).ToArray(); + } + } + } + if (edit != null) { suggestionedIndexRangeReplaces.Add(("Use IndexRange instead of Length", edit)); @@ -492,26 +564,6 @@ public static ImmutableDictionary CodeActions(this FileCo // suggestions for unknown ids and types - // determine the first fragment in the containing namespace - var firstInNs = file.NamespaceDeclarationTokens() - .TakeWhile(t => t.Line <= range.Start.Line).LastOrDefault() // going by line here is fine - I am ok with a failure if someone has muliple namespace and callable declarations on the same line... - ?.GetChildren(deep: false)?.FirstOrDefault()?.GetFragment(); - if (firstInNs == null) return null; - var insertOpenDirAt = firstInNs.GetRange().Start; - - // range and whitespace info for inserting open directives - var openDirEditRange = new Range { Start = insertOpenDirAt, End = insertOpenDirAt }; - var indentationAfterOpenDir = file.GetLine(insertOpenDirAt.Line).Text.Substring(0, insertOpenDirAt.Character); - var additionalLinesAfterOpenDir = firstInNs.Kind.OpenedNamespace().IsNull ? $"{Environment.NewLine}{Environment.NewLine}" : ""; - var whitespaceAfterOpenDir = $"{Environment.NewLine}{additionalLinesAfterOpenDir}{indentationAfterOpenDir}"; - - (string, WorkspaceEdit) SuggestedOpenDirective(NonNullable suggestedNS) - { - var directive = $"{Keywords.importDirectiveHeader.id} {suggestedNS.Value}"; - var edit = new TextEdit { Range = openDirEditRange, NewText = $"{directive};{whitespaceAfterOpenDir}" }; - return (directive, GetWorkspaceEdit(edit)); - } - var suggestionsForIds = unknownCallables.Select(d => d.Range.Start) .SelectMany(pos => file.NamespaceSuggestionsForIdAtPosition(pos, compilation, out var _)) .Select(SuggestedOpenDirective); From 65717eeec79331e50827f85756ac9bc111276e04 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Thu, 25 Jul 2019 14:14:48 -0700 Subject: [PATCH 4/4] Did some refactoring to clean things up a bit. Hopefully this is better than it was before. --- .../CompilationManager/EditorSupport.cs | 234 ++++++++++-------- 1 file changed, 136 insertions(+), 98 deletions(-) diff --git a/src/QsCompiler/CompilationManager/EditorSupport.cs b/src/QsCompiler/CompilationManager/EditorSupport.cs index a803de1f60..76ce61964c 100644 --- a/src/QsCompiler/CompilationManager/EditorSupport.cs +++ b/src/QsCompiler/CompilationManager/EditorSupport.cs @@ -121,7 +121,7 @@ private static IEnumerable> NamespaceSuggestionsForTypeAtPos /// private static CodeFragment.TokenIndex GetNamespaceToken(FileContentManager file, Position position) { - // going by line here is fine - I am ok with a failure if someone has muliple namespace and callable declarations on the same line... + // going by line here is fine - I am ok with a failure if someone has multiple namespace and callable declarations on the same line... return file.NamespaceDeclarationTokens() .TakeWhile(t => t.Line <= position.Line) .LastOrDefault(); @@ -365,13 +365,57 @@ public static WorkspaceEdit Rename(this FileContentManager file, CompilationUnit }; } - private static WorkspaceEdit ProcessForLoopIntroFrag(FileContentManager file, VersionedTextDocumentIdentifier versionedFileId, CodeFragment frag) + /// + /// Creates a returns a function that can be used to get an appropriate module name for an + /// unknown callable in the given namespace, or an empty string if the callable is known. + /// The function returned is specific to the given file content manager, compilation unit, + /// and namespace. + /// + /// + private static Func, string> getNsForUnknownCallableFactory(FileContentManager file, CompilationUnit compilation, CodeFragment.TokenIndex namespaceToken) { - bool checkStartWithZero = false; - bool checkHasLengthCall = false; - bool checkSubOne = false; + return (callableName) => + { + // get namespace string + string namespaceString = string.Empty; + var currNs = namespaceToken?.GetFragment(); + if (currNs != null && currNs.Kind is QsFragmentKind.NamespaceDeclaration namespaceFrag) + { + namespaceString = ((QsSymbolKind.Symbol)namespaceFrag.Item.Symbol).Item.Value; + } + if (namespaceString != string.Empty) + { + var opens = compilation.GlobalSymbols.OpenDirectives(NonNullable.New(namespaceString)); + var possibleNamespaces = compilation.GlobalSymbols.NamespacesContainingCallable(callableName); + var currentOpenNamespaces = opens[file.FileName].Select(i => i.Item1); - Position fragStart = frag.GetRange().Start; + // if there is not a namespace opened that has the callable, add open directive to edit + if (!possibleNamespaces.Intersect(currentOpenNamespaces).Any()) + { + return possibleNamespaces.FirstOrDefault().Value; + } + } + + return string.Empty; + }; + } + + /// + /// Processes the index range replace code action. It first checks if the current fragment + /// is a valid match for the code action's targeted expression pattern. If so, it will + /// create edits that will change something of the form "for (i in 0 .. Length(ary)-1)" to + /// "for (i in IndexRange(ary))". It will then check if IndexRange is known in the current + /// namespace, and if it is not, it will create an edit to add the appropriate open directive. + /// + /// Fragment the code action is triggered from + /// Function for creating workspace edits + /// Function that provides an appropriate namespace if the given callable is not known + /// Function for creating appropriate edits for adding open directives + /// The workspace edit appropriate for this code action, or null if the code action is not applicable + private static WorkspaceEdit IndexRangeReplaceCodeAction(CodeFragment currentFrag, Func makeWorkspaceEdit, + Func, string> getNsForUnknownCallable, Func, TextEdit> makeOpenDirectiveEdit) + { + Position fragStart = currentFrag.GetRange().Start; Position beforeArgsStart = null; Position beforeArgsEnd = null; @@ -384,52 +428,57 @@ private static WorkspaceEdit ProcessForLoopIntroFrag(FileContentManager file, Ve (posToConvert.Line == 1 ? fragStart.Character : 0) + posToConvert.Column - 1 ); - var forLoopIntroExpression = (QsFragmentKind.ForLoopIntro)frag.Kind; - // ToDo: need to use a better method for matching fragments to expression patterns - if (!forLoopIntroExpression.Item2.Range.IsNull) - { - var rangeStart = ((QsFragmentKind.ForLoopIntro)frag.Kind).Item2.Range.Item.Item1; - var rangeEnd = ((QsFragmentKind.ForLoopIntro)frag.Kind).Item2.Range.Item.Item2; - - beforeArgsStart = ConvertFragPosToGlobalPos(rangeStart); - afterArgsEnd = ConvertFragPosToGlobalPos(rangeEnd); - } - - if (forLoopIntroExpression.Item2.Expression is QsExpressionKind.RangeLiteral rangeExpression) + // all the checks for if the fragment matches the target pattern + bool checkStartWithZero = false; + bool checkHasLengthCall = false; + bool checkSubOne = false; + if (currentFrag.Kind is QsFragmentKind.ForLoopIntro forLoopIntroExpression) { - if (rangeExpression.Item1.Expression is QsExpressionKind.IntLiteral intLiteralExpression) + if (!forLoopIntroExpression.Item2.Range.IsNull) { - checkStartWithZero = intLiteralExpression.Item == 0L; + var rangeStart = ((QsFragmentKind.ForLoopIntro)currentFrag.Kind).Item2.Range.Item.Item1; + var rangeEnd = ((QsFragmentKind.ForLoopIntro)currentFrag.Kind).Item2.Range.Item.Item2; + + beforeArgsStart = ConvertFragPosToGlobalPos(rangeStart); + afterArgsEnd = ConvertFragPosToGlobalPos(rangeEnd); } - if (rangeExpression.Item2.Expression is QsExpressionKind.SUB SUBExpression) + if (forLoopIntroExpression.Item2.Expression is QsExpressionKind.RangeLiteral rangeExpression) { - if (SUBExpression.Item1.Expression is QsExpressionKind.CallLikeExpression callLikeExression) + if (rangeExpression.Item1.Expression is QsExpressionKind.IntLiteral intLiteralExpression) { - if (callLikeExression.Item1.Expression is QsExpressionKind.Identifier identifier) - { - var identifierSub = (QsSymbolKind.Symbol)identifier.Item1.Symbol; - checkHasLengthCall = identifierSub.Item.Value == LENGTH_METHOD_NAME; - } + checkStartWithZero = intLiteralExpression.Item == 0L; + } - if (callLikeExression.Item2.Expression is QsExpressionKind.ValueTuple valueTuple) + if (rangeExpression.Item2.Expression is QsExpressionKind.SUB SUBExpression) + { + if (SUBExpression.Item1.Expression is QsExpressionKind.CallLikeExpression callLikeExression) { - if(!callLikeExression.Item2.Range.IsNull) + if (callLikeExression.Item1.Expression is QsExpressionKind.Identifier identifier) { - var argRangeStart = callLikeExression.Item2.Range.Item.Item1; - var argRangeEnd = callLikeExression.Item2.Range.Item.Item2; + var identifierSub = (QsSymbolKind.Symbol)identifier.Item1.Symbol; + checkHasLengthCall = identifierSub.Item.Value == LENGTH_METHOD_NAME; + } - beforeArgsEnd = ConvertFragPosToGlobalPos(argRangeStart); - afterArgsStart = ConvertFragPosToGlobalPos(argRangeEnd); + if (callLikeExression.Item2.Expression is QsExpressionKind.ValueTuple valueTuple) + { + if (!callLikeExression.Item2.Range.IsNull) + { + var argRangeStart = callLikeExression.Item2.Range.Item.Item1; + var argRangeEnd = callLikeExression.Item2.Range.Item.Item2; + + beforeArgsEnd = ConvertFragPosToGlobalPos(argRangeStart); + afterArgsStart = ConvertFragPosToGlobalPos(argRangeEnd); + } } } - } - if (SUBExpression.Item2.Expression is QsExpressionKind.IntLiteral subIntLiteralExpression) - { - checkSubOne = subIntLiteralExpression.Item == 1L; + if (SUBExpression.Item2.Expression is QsExpressionKind.IntLiteral subIntLiteralExpression) + { + checkSubOne = subIntLiteralExpression.Item == 1L; + } } } } @@ -441,15 +490,24 @@ private static WorkspaceEdit ProcessForLoopIntroFrag(FileContentManager file, Ve afterArgsEnd != null; if (positionsAreNonNull && checkStartWithZero && checkHasLengthCall && checkSubOne) { - // Build WorkspaceEdit - TextEdit beforeArgsEdit = new TextEdit() { Range = new Range() { Start = beforeArgsStart, End = beforeArgsEnd }, NewText = INDEXRANGE_METHOD_NAME }; - TextEdit afterArgsEdit = new TextEdit() { Range = new Range() { Start = afterArgsStart, End = afterArgsEnd }, NewText = "" }; - - return new WorkspaceEdit + // build WorkspaceEdit + var edits = new List(); + edits.Add( new TextEdit() { Range = new Range() { Start = beforeArgsStart, End = beforeArgsEnd }, NewText = INDEXRANGE_METHOD_NAME } ); + edits.Add( new TextEdit() { Range = new Range() { Start = afterArgsStart, End = afterArgsEnd }, NewText = "" } ); + + // get namespace edit, if necessary + // this method will return empty string if the given callable is already found in the current namespace + string ns = getNsForUnknownCallable(NonNullable.New(INDEXRANGE_METHOD_NAME)); + if (ns != string.Empty) { - DocumentChanges = new[] { new TextDocumentEdit { TextDocument = versionedFileId, Edits = new[] { beforeArgsEdit, afterArgsEdit } } }, - Changes = new Dictionary { { file.FileName.Value, new[] { beforeArgsEdit, afterArgsEdit } } } - }; + var namespaceTextEdit = makeOpenDirectiveEdit(NonNullable.New(ns)); + if (namespaceTextEdit != null) + { + edits.Add(namespaceTextEdit); + } + } + + return makeWorkspaceEdit(edits.ToArray()); } return null; @@ -462,21 +520,28 @@ private static WorkspaceEdit ProcessForLoopIntroFrag(FileContentManager file, Ve /// public static ImmutableDictionary CodeActions(this FileContentManager file, CompilationUnit compilation, Range range, CodeActionContext context) { + /* ToDo: Method Needs Refactoring + * + * Each code action type should know how to check if it is valid for the code fragment, and should know + * how to create the appropriate description and workspace edit combo. Maybe a code action class would + * be useful? This handler should be little more than a list of calls for each code action to return + * a description/edit combo if the code action is applicable. + */ + if (range?.Start == null || range.End == null || file == null || !Utils.IsValidRange(range, file)) return null; if (compilation == null || context?.Diagnostics == null) return null; var versionedFileId = new VersionedTextDocumentIdentifier { Uri = file.Uri, Version = 1 }; // setting version to null here won't work in VS Code ... CodeFragment frag = file?.TryGetFragmentAt(range.Start, true); - CodeFragment.TokenIndex currNsToken = null; + CodeFragment.TokenIndex currNsToken = GetNamespaceToken(file, range.Start); - WorkspaceEdit GetWorkspaceEdit(TextEdit edit) => new WorkspaceEdit + WorkspaceEdit GetWorkspaceEdit(TextEdit[] edits) => new WorkspaceEdit { - DocumentChanges = new[] { new TextDocumentEdit { TextDocument = versionedFileId, Edits = new[] { edit } } }, - Changes = new Dictionary { { file.FileName.Value, new[] { edit } } } + DocumentChanges = new[] { new TextDocumentEdit { TextDocument = versionedFileId, Edits = edits.ToArray() } }, + Changes = new Dictionary { { file.FileName.Value, edits.ToArray() } } }; // determine the first fragment in the containing namespace - currNsToken = currNsToken ?? GetNamespaceToken(file, range.Start); var firstInNs = currNsToken ?.GetChildren(deep: false)?.FirstOrDefault()?.GetFragment(); if (firstInNs == null) return null; @@ -488,11 +553,22 @@ public static ImmutableDictionary CodeActions(this FileCo var additionalLinesAfterOpenDir = firstInNs.Kind.OpenedNamespace().IsNull ? $"{Environment.NewLine}{Environment.NewLine}" : ""; var whitespaceAfterOpenDir = $"{Environment.NewLine}{additionalLinesAfterOpenDir}{indentationAfterOpenDir}"; + TextEdit SuggestedOpenDirectiveEdit(NonNullable suggestedNS) + { + var directive = $"{Keywords.importDirectiveHeader.id} {suggestedNS.Value}"; + return new TextEdit { Range = openDirEditRange, NewText = $"{directive};{whitespaceAfterOpenDir}" }; + } + (string, WorkspaceEdit) SuggestedOpenDirective(NonNullable suggestedNS) { var directive = $"{Keywords.importDirectiveHeader.id} {suggestedNS.Value}"; - var edit = new TextEdit { Range = openDirEditRange, NewText = $"{directive};{whitespaceAfterOpenDir}" }; - return (directive, GetWorkspaceEdit(edit)); + return (directive, GetWorkspaceEdit(new[] { SuggestedOpenDirectiveEdit(suggestedNS) })); + } + + (string, WorkspaceEdit) SuggestedNameQualification(NonNullable suggestedNS, string id, Position pos) + { + var edit = new TextEdit { Range = new Range { Start = pos, End = pos }, NewText = $"{suggestedNS.Value}." }; + return ($"{suggestedNS.Value}.{id}", GetWorkspaceEdit(new[] { edit })); } // diagnostics based on which suggestions are given @@ -501,57 +577,19 @@ public static ImmutableDictionary CodeActions(this FileCo var ambiguousTypes = context.Diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.AmbiguousType)); var unknownTypes = context.Diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.UnknownType)); - List<(string, WorkspaceEdit)> suggestionedIndexRangeReplaces = new List<(string, WorkspaceEdit)>(); - if (frag.Kind.IsForLoopIntro) - { - WorkspaceEdit edit = ProcessForLoopIntroFrag(file, versionedFileId, frag); - - // get namespace string - string namespaceString = string.Empty; - var currNs = currNsToken?.GetFragment(); - if (currNs != null && currNs.Kind is QsFragmentKind.NamespaceDeclaration namespaceFrag) - { - namespaceString = ((QsSymbolKind.Symbol)namespaceFrag.Item.Symbol).Item.Value; - } - if (namespaceString != string.Empty) - { - var opens = compilation.GlobalSymbols.OpenDirectives(NonNullable.New(namespaceString)); - var possibleNamespaces = compilation.GlobalSymbols.NamespacesContainingCallable(NonNullable.New(INDEXRANGE_METHOD_NAME)); - var currentOpenNamespaces = opens[file.FileName].Select(i => i.Item1); - - // if there is not a namespace opened that has the IndexRange function, add open directive to edit - if(!possibleNamespaces.Intersect(currentOpenNamespaces).Any()) - { - var arrayNamespace = possibleNamespaces.FirstOrDefault(); - var namespaceTextEdit = new TextEdit - { - Range = openDirEditRange, - NewText = $"{Keywords.importDirectiveHeader.id} {arrayNamespace.Value};{whitespaceAfterOpenDir}" - }; - - // append open directive edit to the list of edits - if (edit != null) - { - edit.DocumentChanges.First().Edits = edit.DocumentChanges.First().Edits.Concat(new[] { namespaceTextEdit }).ToArray(); - edit.Changes[file.FileName.Value] = edit.Changes[file.FileName.Value].Concat(new[] { namespaceTextEdit }).ToArray(); - } - } - } + // suggestions for index range replacements - if (edit != null) - { - suggestionedIndexRangeReplaces.Add(("Use IndexRange instead of Length", edit)); - } + var suggestionedIndexRangeReplaces = new List<(string, WorkspaceEdit)>(); + WorkspaceEdit indexRangeRaplaceEdit = IndexRangeReplaceCodeAction(frag, GetWorkspaceEdit, + getNsForUnknownCallableFactory(file, compilation, currNsToken), SuggestedOpenDirectiveEdit); + + if (indexRangeRaplaceEdit != null) + { + suggestionedIndexRangeReplaces.Add(("Use IndexRange instead of Length", indexRangeRaplaceEdit)); } // suggestions for ambiguous ids and types - (string, WorkspaceEdit) SuggestedNameQualification(NonNullable suggestedNS, string id, Position pos) - { - var edit = new TextEdit { Range = new Range { Start = pos, End = pos }, NewText = $"{suggestedNS.Value}." }; - return ($"{suggestedNS.Value}.{id}", GetWorkspaceEdit(edit)); - } - var suggestedIdQualifications = ambiguousCallables.Select(d => d.Range.Start) .SelectMany(pos => file.NamespaceSuggestionsForIdAtPosition(pos, compilation, out var id) .Select(ns => SuggestedNameQualification(ns, id, pos)));