/// <summary> /// Returns a sequence of suggestions on how errors for unknown types and callable in the given diagnostics can be fixed, /// given the file for which those diagnostics were generated and the corresponding compilation. /// The given line number is used to determine the containing namespace. /// Returns an empty enumerable if any of the given arguments is null. /// </summary> internal static IEnumerable <(string, WorkspaceEdit)> SuggestionsForUnknownIdentifiers (this FileContentManager file, CompilationUnit compilation, int lineNr, IEnumerable <Diagnostic> diagnostics) { if (file == null || diagnostics == null) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var unknownCallables = diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.UnknownIdentifier)); var unknownTypes = diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.UnknownType)); if (!unknownCallables.Any() && !unknownTypes.Any()) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var suggestionsForIds = unknownCallables.Select(d => d.Range.Start) .SelectMany(pos => file.NamespaceSuggestionsForIdAtPosition(pos, compilation, out var _)); var suggestionsForTypes = unknownTypes.Select(d => d.Range.Start) .SelectMany(pos => file.NamespaceSuggestionsForTypeAtPosition(pos, compilation, out var _)); return(file.OpenDirectiveSuggestions(lineNr, suggestionsForIds.Concat(suggestionsForTypes).ToArray()) .Select(edit => (edit.NewText.Trim().Trim(';'), file.GetWorkspaceEdit(edit)))); }
// the actual update routine /// <summary> /// Attempts to perform the necessary updates when replacing the range [start, start + count) by newText for the given file /// wrapping each step in a QsCompilerError.RaiseOnFailure. /// </summary> private static void Update(this FileContentManager file, int start, int count, IEnumerable <string> newText) { CodeLine[] replacements = QsCompilerError.RaiseOnFailure( () => ComputeCodeLines(newText, start > 0 ? file.GetLine(start - 1) : null).ToArray(), "scope tracking update failed during computing the replacements"); IEnumerable <CodeLine>?updateRemaining = QsCompilerError.RaiseOnFailure( () => ComputeUpdates(file, start, count, replacements), "scope tracking update failed during computing the updates"); QsCompilerError.RaiseOnFailure( () => { if (updateRemaining == null) { file.ContentUpdate(start, count, replacements); } else { file.ContentUpdate(start, file.NrLines() - start, replacements.Concat(updateRemaining).ToArray()); } }, "the proposed ContentUpdate failed"); QsCompilerError.RaiseOnFailure( () => { if (updateRemaining == null) { file.AddScopeDiagnostics(file.ComputeScopeDiagnostics(start, replacements.Length)); } else { file.AddScopeDiagnostics(file.ComputeScopeDiagnostics(start)); } file.AddScopeDiagnostics(file.CheckForMissingClosings()); }, "updating the scope diagnostics failed"); }
/// <summary> /// Returns the workspace edit that describes the changes to be done if the symbol at the given position - if any - is renamed to the given name. /// Returns null if no symbol exists at the specified position, /// or if some parameters are unspecified (null), /// or if the specified position is not a valid position within the file. /// </summary> public static WorkspaceEdit Rename(this FileContentManager file, CompilationUnit compilation, Position position, string newName) { if (newName == null || file == null) { return(null); } var found = file.TryGetReferences(compilation, position, out var declLocation, out var locations); if (!found) { return(null); } if (declLocation != null) { locations = new[] { declLocation }.Concat(locations); } var changes = locations.ToLookup(loc => loc.Uri, loc => new TextEdit { Range = loc.Range, NewText = newName }); return(new WorkspaceEdit { DocumentChanges = changes .Select(change => new TextDocumentEdit { TextDocument = new VersionedTextDocumentIdentifier { Uri = change.Key, Version = 1 }, // setting version to null here won't work in VS Code ... Edits = change.ToArray() }) .ToArray(), Changes = changes.ToDictionary( items => CompilationUnitManager.TryGetFileId(items.Key, out var name) ? name.Value : null, items => items.ToArray()) });
/// <summary> /// Returns true if the given file contains any tokens overlapping with the given fragment. /// The range of the tokens in the file is assumed to be relative to their start line (the index at which they are listed), /// whereas the range of the given fragment is assumed to be the absolute range. /// Throws an ArgumentNullException if the given file or range is null. /// Throws an ArgumentOutOfRangeException if the given range is not a valid range within file. /// </summary> internal static bool ContainsTokensOverlappingWith(this FileContentManager file, Range range) { if (file == null) { throw new ArgumentNullException(nameof(file)); } if (!Utils.IsValidRange(range, file)) { throw new ArgumentOutOfRangeException(nameof(range)); } var(start, end) = (range.Start.Line, range.End.Line); if (start != end && file.GetTokenizedLines(start + 1, end - start - 1).SelectMany(x => x).Any()) { return(true); } ; var inRange = file.GetTokenizedLine(start).Where(TokensAfter(new Position(0, range.Start.Character))); // checking tokens overlapping with range.Start below inRange = start == end ? inRange.Where(TokensStartingBefore(new Position(0, range.End.Character))) : inRange.Concat(file.GetTokenizedLine(end).Where(TokensStartingBefore(new Position(0, range.End.Character)))); if (inRange.Any()) { QsCompilerError.Raise($"{range.DiagnosticString()} overlaps for start = {start}, end = {end}, \n\n" + $"{String.Join("\n", file.GetTokenizedLine(start).Select(x => $"{x.GetRange().DiagnosticString()}"))},\n\n " + $"{String.Join("\n", file.GetTokenizedLine(end).Select(x => $"{x.GetRange().DiagnosticString()}"))},"); return(true); } var overlapsWithStart = file.TryGetFragmentAt(range.Start, out var _); return(overlapsWithStart != null); }
/// <summary> /// Returns a sequence of suggestions for update-and-reassign statements based on the generated diagnostics, /// and given the file for which those diagnostics were generated. /// Returns an empty enumerable if any of the given arguments is null. /// </summary> internal static IEnumerable <(string, WorkspaceEdit)> SuggestionsForUpdateAndReassignStatements (this FileContentManager file, IEnumerable <Diagnostic> diagnostics) { if (file == null || diagnostics == null) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var updateOfArrayItemExprs = diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.UpdateOfArrayItemExpr)); (string, WorkspaceEdit) SuggestedCopyAndUpdateExpr(CodeFragment fragment) { var exprInfo = Parsing.ProcessUpdateOfArrayItemExpr.Invoke(fragment.Text); // Skip if the statement did not match a pattern for which we can give a code action if (exprInfo == null || (exprInfo.Item1.Line == 1 && exprInfo.Item1.Column == 1)) { return("", null); } // Convert set <identifier>[<index>] = <rhs> to set <identifier> w/= <index> <- <rhs> var rhs = $"{exprInfo.Item3} {Keywords.qsCopyAndUpdateOp.cont} {exprInfo.Item4}"; var outputStr = $"{Keywords.qsValueUpdate.id} {exprInfo.Item2} {Keywords.qsCopyAndUpdateOp.op}= {rhs}"; var fragmentRange = fragment.GetRange(); var edit = new TextEdit { Range = fragmentRange.Copy(), NewText = outputStr }; return("Replace with an update-and-reassign statement.", file.GetWorkspaceEdit(edit)); } return(updateOfArrayItemExprs .Select(d => file?.TryGetFragmentAt(d.Range.Start, out var _, includeEnd: true)) .Where(frag => frag != null) .Select(frag => SuggestedCopyAndUpdateExpr(frag)) .Where(s => s.Item2 != null)); }
/// <summary> /// Returns the completion environment at the given position in the file or null if the environment cannot be /// determined. Stores the code fragment found at or before the given position into an out parameter. /// </summary> /// <exception cref="ArgumentNullException">Thrown when any argument is null.</exception> /// <exception cref="ArgumentException">Thrown when the position is invalid.</exception> private static (CompletionScope, QsFragmentKind) GetCompletionEnvironment( FileContentManager file, Position position, out CodeFragment fragment) { if (file == null) { throw new ArgumentNullException(nameof(file)); } if (!Utils.IsValidPosition(position)) { throw new ArgumentException(nameof(position)); } if (!Utils.IsValidPosition(position, file)) { // FileContentManager.IndentationAt will fail if the position is not within the file. fragment = null; return(null, null); } var token = GetTokenAtOrBefore(file, position); if (token == null) { fragment = null; return(null, null); } fragment = token.GetFragment(); var relativeIndentation = fragment.Indentation - file.IndentationAt(position); QsCompilerError.Verify(Math.Abs(relativeIndentation) <= 1); var parents = new[] { token }.Concat(token.GetNonEmptyParents()) .Skip(relativeIndentation + 1) .Select(t => t.GetFragment()); CompletionScope scope = null; if (!parents.Any()) { scope = CompletionScope.TopLevel; } else if (parents.Any() && parents.First().Kind.IsNamespaceDeclaration) { scope = CompletionScope.NamespaceTopLevel; } else if (parents.Where(parent => parent.Kind.IsFunctionDeclaration).Any()) { scope = CompletionScope.Function; } else if (parents.Any() && parents.First().Kind.IsOperationDeclaration) { scope = CompletionScope.OperationTopLevel; } else if (parents.Where(parent => parent.Kind.IsOperationDeclaration).Any()) { scope = CompletionScope.Operation; } QsFragmentKind previous = null; if (relativeIndentation == 0 && IsPositionAfterDelimiter(file, fragment, position)) { previous = fragment.Kind; } else if (relativeIndentation == 0) { previous = token.PreviousOnScope()?.GetFragment().Kind; } else if (relativeIndentation == 1) { previous = token.GetNonEmptyParent()?.GetFragment().Kind; } return(scope, previous); }
/// <summary> /// Returns a sequence of suggestions for replacing ranges over array indices with the corresponding library call, /// provided the corresponding library is referenced. /// Returns an empty enumerable if this is not the case or any of the given arguments is null. /// </summary> internal static IEnumerable <(string, WorkspaceEdit)> SuggestionsForIndexRange (this FileContentManager file, CompilationUnit compilation, LSP.Range range) { if (file == null || compilation == null || range?.Start == null) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var indexRangeNamespaces = compilation.GlobalSymbols.NamespacesContainingCallable(BuiltIn.IndexRange.Name); if (!indexRangeNamespaces.Contains(BuiltIn.IndexRange.Namespace)) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var suggestedOpenDir = file.OpenDirectiveSuggestions(range.Start.Line, BuiltIn.IndexRange.Namespace); /// Returns true the given expression is of the form "0 .. Length(args) - 1", /// as well as the range of the entire expression and the argument tuple "(args)" as out parameters. bool IsIndexRange(QsExpression iterExpr, Position offset, out LSP.Range exprRange, out LSP.Range argRange) { if (iterExpr.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .RangeLiteral rangeExpression && iterExpr.Range.IsValue && // iterable expression is a valid range literal rangeExpression.Item1.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .IntLiteral intLiteralExpression && intLiteralExpression.Item == 0L && // .. starting at 0 .. rangeExpression.Item2.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .SUB SUBExpression && // .. and ending in subracting .. SUBExpression.Item2.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .IntLiteral subIntLiteralExpression && subIntLiteralExpression.Item == 1L && // .. 1 from .. SUBExpression.Item1.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .CallLikeExpression callLikeExression && // .. a call .. callLikeExression.Item1.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .Identifier identifier && // .. to and identifier .. identifier.Item1.Symbol is QsSymbolKind <QsSymbol> .Symbol symName && symName.Item.Value == BuiltIn.Length.Name.Value && // .. "Length" called with .. callLikeExression.Item2.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .ValueTuple valueTuple && callLikeExression.Item2.Range.IsValue) // .. a valid argument tuple { exprRange = DiagnosticTools.GetAbsoluteRange(offset, iterExpr.Range.Item); argRange = DiagnosticTools.GetAbsoluteRange(offset, callLikeExression.Item2.Range.Item); return(true); } (exprRange, argRange) = (null, null); return(false); } /// Returns the text edits for replacing an range over the indices with the corresponding library call if the given code fragment is a suitable for-loop intro. /// The returned edits do *not* include an edit for adding the corresponding open-directive if necessary. IEnumerable <TextEdit> IndexRangeEdits(CodeFragment fragment) { if (fragment.Kind is QsFragmentKind.ForLoopIntro forLoopIntro && // todo: in principle we could give these suggestions for any index range IsIndexRange(forLoopIntro.Item2, fragment.GetRange().Start, out var iterExprRange, out var argTupleRange)) { yield return(new TextEdit() { Range = new LSP.Range() { Start = iterExprRange.Start, End = argTupleRange.Start }, NewText = BuiltIn.IndexRange.Name.Value }); yield return(new TextEdit() { Range = new LSP.Range() { Start = argTupleRange.End, End = iterExprRange.End }, NewText = "" }); } } var fragments = file.FragmentsOverlappingWithRange(range); var edits = fragments.SelectMany(IndexRangeEdits); return(edits.Any() ? new[] { ("Use IndexRange to iterate over indices.", file.GetWorkspaceEdit(suggestedOpenDir.Concat(edits).ToArray())) }
/// <summary> /// Extracts the code fragments based on the current file content that need to be re-processed due to content changes on the given lines. /// Ignores any whitespace or comments at the beginning of the file (whether they have changed or not). /// Ignores any whitespace or comments that occur after the last piece of code in the file. /// Throws an ArgumentNullException if any of the arguments is null. /// </summary> private static IEnumerable <CodeFragment> FragmentsToProcess(this FileContentManager file, SortedSet <int> changedLines) { // NOTE: I suggest not to touch this routine unless absolutely necessary...(things *will* break) if (file == null) { throw new ArgumentNullException(nameof(file)); } if (changedLines == null) { throw new ArgumentNullException(nameof(changedLines)); } var iter = changedLines.GetEnumerator(); var lastInFile = LastInFile(file); Position processed = new Position(0, 0); while (iter.MoveNext()) { QsCompilerError.Verify(iter.Current >= 0 && iter.Current < file.NrLines(), "index out of range for changed line"); if (processed.Line < iter.Current) { var statementStart = file.PositionAfterPrevious(new Position(iter.Current, 0)); if (processed.IsSmallerThan(statementStart)) { processed = statementStart; } } while (processed.Line <= iter.Current && processed.IsSmallerThan(lastInFile)) { processed = processed.Copy(); // because we don't want to modify the ending of the previous code fragment ... var nextEnding = file.FragmentEnd(ref processed); var extractedPiece = file.GetCodeSnippet(new LSP.Range { Start = processed, End = nextEnding }); // constructing the CodeFragment - // NOTE: its Range.End is the position of the delimiting char (if such a char exists), i.e. the position right after Code ends // length = 0 can occur e.g. if the last piece of code in the file does not terminate with a statement ending if (extractedPiece.Length > 0) { var code = file.GetLine(nextEnding.Line).ExcessBracketPositions.Contains(nextEnding.Character - 1) ? extractedPiece.Substring(0, extractedPiece.Length - 1) : extractedPiece; if (code.Length == 0 || !CodeFragment.DelimitingChars.Contains(code.Last())) { code = $"{code}{CodeFragment.MissingDelimiter}"; } var endChar = nextEnding.Character - (extractedPiece.Length - code.Length) - 1; var codeRange = new LSP.Range { Start = processed, End = new Position(nextEnding.Line, endChar) }; yield return(new CodeFragment(file.IndentationAt(codeRange.Start), codeRange, code.Substring(0, code.Length - 1), code.Last())); } processed = nextEnding; } } }
/// <summary> /// Returns a sequence of suggestions on how deprecated syntax can be updated based on the generated diagnostics, /// and given the file for which those diagnostics were generated. /// Returns an empty enumerable if any of the given arguments is null. /// </summary> internal static IEnumerable <(string, WorkspaceEdit)> SuggestionsForDeprecatedSyntax (this FileContentManager file, IEnumerable <Diagnostic> diagnostics) { if (file == null || diagnostics == null) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var deprecatedUnitTypes = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedUnitType)); var deprecatedNOToperators = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedNOToperator)); var deprecatedANDoperators = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedANDoperator)); var deprecatedORoperators = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedORoperator)); var deprecatedOpCharacteristics = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedOpCharacteristics)); (string, WorkspaceEdit) ReplaceWith(string text, LSP.Range range) { bool NeedsWs(Char ch) => Char.IsLetterOrDigit(ch) || ch == '_'; if (range?.Start != null && range.End != null) { var beforeEdit = file.GetLine(range.Start.Line).Text.Substring(0, range.Start.Character); var afterEdit = file.GetLine(range.End.Line).Text.Substring(range.End.Character); if (beforeEdit.Any() && NeedsWs(beforeEdit.Last())) { text = $" {text}"; } if (afterEdit.Any() && NeedsWs(afterEdit.First())) { text = $"{text} "; } } var edit = new TextEdit { Range = range?.Copy(), NewText = text }; return($"Replace with \"{text.Trim()}\".", file.GetWorkspaceEdit(edit)); } // update deprecated keywords and operators var suggestionsForUnitType = deprecatedUnitTypes.Select(d => ReplaceWith(Keywords.qsUnit.id, d.Range)); var suggestionsForNOT = deprecatedNOToperators.Select(d => ReplaceWith(Keywords.qsNOTop.op, d.Range)); var suggestionsForAND = deprecatedANDoperators.Select(d => ReplaceWith(Keywords.qsANDop.op, d.Range)); var suggestionsForOR = deprecatedORoperators.Select(d => ReplaceWith(Keywords.qsORop.op, d.Range)); // update deprecated operation characteristics syntax var typeToQs = new ExpressionTypeToQs(new ExpressionToQs()); string CharacteristicsAnnotation(Characteristics c) { typeToQs.onCharacteristicsExpression(SymbolResolution.ResolveCharacteristics(c)); return($"{Keywords.qsCharacteristics.id} {typeToQs.Output}"); } var suggestionsForOpCharacteristics = deprecatedOpCharacteristics.SelectMany(d => { // TODO: TryGetQsSymbolInfo currently only returns information about the inner most leafs rather than all types etc. // Once it returns indeed all types in the fragment, the following code block should be replaced by the commented out code below. var fragment = file.TryGetFragmentAt(d.Range.Start, out var _); IEnumerable <Characteristics> GetCharacteristics(QsTuple <Tuple <QsSymbol, QsType> > argTuple) => SyntaxGenerator.ExtractItems(argTuple).SelectMany(item => item.Item2.ExtractCharacteristics()).Distinct(); var characteristicsInFragment = fragment?.Kind is QsFragmentKind.FunctionDeclaration function ? GetCharacteristics(function.Item2.Argument) : fragment?.Kind is QsFragmentKind.OperationDeclaration operation ? GetCharacteristics(operation.Item2.Argument) : fragment?.Kind is QsFragmentKind.TypeDefinition type ? GetCharacteristics(type.Item2) : Enumerable.Empty <Characteristics>(); //var symbolInfo = file.TryGetQsSymbolInfo(d.Range.Start, false, out var fragment); //var characteristicsInFragment = (symbolInfo?.UsedTypes ?? Enumerable.Empty<QsType>()) // .SelectMany(t => t.ExtractCharacteristics()).Distinct(); var fragmentStart = fragment?.GetRange()?.Start; return(characteristicsInFragment .Where(c => c.Range.IsValue && DiagnosticTools.GetAbsoluteRange(fragmentStart, c.Range.Item).Overlaps(d.Range)) .Select(c => ReplaceWith(CharacteristicsAnnotation(c), d.Range))); }); return(suggestionsForOpCharacteristics.ToArray() .Concat(suggestionsForUnitType) .Concat(suggestionsForNOT) .Concat(suggestionsForAND) .Concat(suggestionsForOR)); }
// external routines for context verification /// <summary> /// Given the line number of the lines that contain tokens that (possibly) have been modified, /// checks which callable declaration they can potentially belong to and returns the fully qualified name of those callables. /// </summary> internal static IEnumerable <(Range, QsQualifiedName)> CallablesWithContentModifications(this FileContentManager file, IEnumerable <int> changedLines) { var lastInFile = file.LastToken()?.GetFragment()?.Range?.End ?? file.End(); var callables = file.GetCallableDeclarations().Select(tuple => // these are sorted according to their line number { var ns = file.TryGetNamespaceAt(tuple.Item2.Start); QsCompilerError.Verify(ns != null, "namespace for callable declaration should not be null"); // invalid namespace names default to an unknown namespace name, but remain included in the compilation return(tuple.Item2.Start, new QsQualifiedName(ns, tuple.Item1)); }).ToList(); // NOTE: The range of modifications that has to trigger an update of the syntax tree for a callable // does need to go up to and include modifications to the line containing the next callable! // Otherwise inserting a callable declaration in the middle of an existing callable does not trigger the right behavior! (Range, QsQualifiedName) TypeCheckingRange((Position, QsQualifiedName) lastPreceding, IEnumerable <(Position, QsQualifiedName)> next) { var callableStart = lastPreceding.Item1; var callableEnd = next.Any() ? next.First().Item1 : lastInFile; return(Range.Create(callableStart, callableEnd), lastPreceding.Item2); } foreach (var lineNr in changedLines) { bool Precedes((Position, QsQualifiedName) tuple) => tuple.Item1.Line < lineNr; var preceding = callables.TakeWhile(Precedes); var following = callables.SkipWhile(Precedes); if (preceding.Any()) { yield return(TypeCheckingRange(preceding.Last(), following)); } if (following.Any() && following.First().Start.Line == lineNr) { yield return(TypeCheckingRange(following.First(), following.Skip(1))); } } }
/// <summary> /// Returns a sequence of suggestions on how errors for unknown types and callable in the given diagnostics can be fixed, /// given the file for which those diagnostics were generated and the corresponding compilation. /// The given line number is used to determine the containing namespace. /// Returns an empty enumerable if any of the given arguments is null. /// </summary> internal static IEnumerable <(string, WorkspaceEdit)> UnknownIdSuggestions( this FileContentManager file, CompilationUnit compilation, int lineNr, IReadOnlyCollection <Diagnostic> diagnostics) =>
/// <summary> /// Returns the position and the kind of the closest specialization preceding the given position, /// and the name of the callable it belongs to as well as its position as Nullable. /// Returns null if the given file or position is null, or if no preceding callable can be found (e.g. because the callable name is invalid). /// If a callable name but no specializations (preceding or otherwise) within that callable can be found, /// assumes that the correct specialization is an auto-inserted default body, /// and returns QsBody as well as the start position of the callable declaration along with the callable name and its position. /// If a callable name as well as existing specializations can be found, but no specialization precedes the given position, /// returns null for the specialization kind as well as for its position. /// </summary> public static ((NonNullable <string>, Position), (QsSpecializationKind, Position))? TryGetClosestSpecialization(this FileContentManager file, Position pos) { QsSpecializationKind GetSpecializationKind(CodeFragment fragment) { var specDecl = fragment.Kind.DeclaredSpecialization(); if (specDecl.IsNull) { return(null); } var((kind, gen), typeArgs) = specDecl.Item; // note: if we want to support type specializations we need to compute the signature of the spec to find the right one return(kind); } if (file == null || pos == null || !Utils.IsValidPosition(pos, file)) { return(null); } file.SyncRoot.EnterReadLock(); try { var declarations = file.CallableDeclarationTokens(); var precedingDecl = declarations.TakeWhile(tIndex => tIndex.GetFragment().GetRange().Start.IsSmallerThan(pos)); if (!precedingDecl.Any()) { return(null); } var closestCallable = precedingDecl.Last(); var callablePosition = closestCallable.GetFragment().GetRange().Start; var callableName = closestCallable.GetFragment().Kind.DeclaredCallableName(null); if (callableName == null) { return(null); } var specializations = FileHeader.FilterCallableSpecializations(closestCallable.GetChildren(deep: false).Select(tIndex => tIndex.GetFragment())); var precedingSpec = specializations.TakeWhile(fragment => fragment.GetRange().Start.IsSmallerThan(pos)); var lastPreceding = precedingSpec.Any() ? precedingSpec.Last() : null; if (specializations.Any() && lastPreceding == null) // the given position is within a callable declaration { return((NonNullable <string> .New(callableName), callablePosition), (null, null)); } return(lastPreceding == null ? ((NonNullable <string> .New(callableName), callablePosition), (QsSpecializationKind.QsBody, callablePosition)) : ((NonNullable <string> .New(callableName), callablePosition), (GetSpecializationKind(lastPreceding), lastPreceding.GetRange().Start))); } finally { file.SyncRoot.ExitReadLock(); } }
/// <summary> /// Returns the signature help information for a call expression if there is such an expression at the specified position. /// Returns null if some parameters are unspecified (null), /// or if the specified position is not a valid position within the currently processed file content, /// or if no call expression exists at the specified position at this time, /// or if no signature help information can be provided for the call expression at the specified position. /// </summary> public static SignatureHelp?SignatureHelp( this FileContentManager file, CompilationUnit compilation, Position?position, MarkupKind format = MarkupKind.PlainText) { // getting the relevant token (if any) var fragment = file?.TryGetFragmentAt(position, out var _, includeEnd: true); if (file is null || position is null || fragment?.Kind == null || compilation == null) { return(null); } var fragmentStart = fragment.Range.Start; // getting the overlapping call expressions (if any), and determine the header of the called callable bool OverlapsWithPosition(Range symRange) => (fragmentStart + symRange).ContainsEnd(position); var overlappingEx = fragment.Kind.CallExpressions().Where(ex => ex.Range.IsValue && OverlapsWithPosition(ex.Range.Item)).ToList(); if (!overlappingEx.Any()) { return(null); } overlappingEx.Sort((ex1, ex2) => // for nested call expressions, the last expressions (by range) is always the closest one { var(x, y) = (ex1.Range.Item, ex2.Range.Item); int result = x.Start.CompareTo(y.Start); return(result == 0 ? x.End.CompareTo(y.End) : result); }); var nsName = file.TryGetNamespaceAt(position); var(method, args) = overlappingEx.Last().Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .CallLikeExpression c ? (c.Item1, c.Item2) : (null, null); if (nsName == null || method == null || args == null) { return(null); } // getting the called identifier as well as what functors have been applied to it List <QsFunctor> FunctorApplications(ref QsExpression ex) { var(next, inner) = ex.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .AdjointApplication adj ? (QsFunctor.Adjoint, adj.Item) : ex.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .ControlledApplication ctl ? (QsFunctor.Controlled, ctl.Item) : (null, null); var fs = inner == null ? new List <QsFunctor>() : FunctorApplications(ref inner); if (next != null) { fs.Add(next); } ex = inner ?? ex; return(fs); } var functors = FunctorApplications(ref method); var id = method.Expression as QsExpressionKind <QsExpression, QsSymbol, QsType> .Identifier; if (id == null) { return(null); } // extracting and adapting the relevant information for the called callable ResolutionResult <CallableDeclarationHeader> .Found?methodDecl = null; if (id.Item1.Symbol is QsSymbolKind <QsSymbol> .Symbol sym) { methodDecl = compilation.GlobalSymbols.TryResolveAndGetCallable( sym.Item, nsName, file.FileName) as ResolutionResult <CallableDeclarationHeader> .Found; } else if (id.Item1.Symbol is QsSymbolKind <QsSymbol> .QualifiedSymbol qualSym) { methodDecl = compilation.GlobalSymbols.TryGetCallable( new QsQualifiedName(qualSym.Item1, qualSym.Item2), nsName, file.FileName) as ResolutionResult <CallableDeclarationHeader> .Found; } if (methodDecl == null) { return(null); } var(documentation, argTuple) = (methodDecl.Item.Documentation, methodDecl.Item.ArgumentTuple); var nrCtlApplications = functors.Where(f => f.Equals(QsFunctor.Controlled)).Count(); while (nrCtlApplications-- > 0) { var ctlQsName = QsLocalSymbol.NewValidName(nrCtlApplications == 0 ? "cs" : $"cs{nrCtlApplications}"); argTuple = SyntaxGenerator.WithControlQubits(argTuple, QsNullable <Position> .Null, ctlQsName, QsNullable <Range> .Null); } // now that we now what callable is called we need to check which argument should come next bool BeforePosition(Range symRange) => fragmentStart + symRange.End < position; IEnumerable <(Range?, string?)> ExtractParameterRanges( QsExpression?ex, QsTuple <LocalVariableDeclaration <QsLocalSymbol> > decl) { var @null = ((Range?)null, (string?)null); IEnumerable <(Range?, string?)> SingleItem(string paramName) { var arg = ex?.Range == null ? ((Range?)null, paramName) : ex.Range.IsValue ? (ex.Range.Item, paramName) : @null; // no signature help if there are invalid expressions return(new[] { arg }); } if (decl is QsTuple <LocalVariableDeclaration <QsLocalSymbol> > .QsTupleItem dItem) { return(SingleItem(dItem.Item.VariableName is QsLocalSymbol.ValidName n ? n.Item : "__argName__")); } var declItems = decl as QsTuple <LocalVariableDeclaration <QsLocalSymbol> > .QsTuple; var exItems = ex?.Expression as QsExpressionKind <QsExpression, QsSymbol, QsType> .ValueTuple; if (declItems == null) { return(new[] { @null }); } if (exItems == null && declItems.Item.Length > 1) { return(SingleItem(decl.PrintArgumentTuple())); } var argItems = exItems != null ? exItems.Item.ToImmutableArray <QsExpression?>() : ex == null ? ImmutableArray <QsExpression?> .Empty : ImmutableArray.Create <QsExpression?>(ex); return(argItems.AddRange(Enumerable.Repeat <QsExpression?>(null, declItems.Item.Length - argItems.Length)) .Zip(declItems.Item, (e, d) => (e, d)) .SelectMany(arg => ExtractParameterRanges(arg.Item1, arg.Item2))); } var callArgs = ExtractParameterRanges(args, argTuple).ToArray(); if (id == null || callArgs == null || callArgs.Any(item => item.Item2 == null)) { return(null); // no signature help if there are invalid expressions } // finally we can build the signature help information MarkupContent AsMarkupContent(string str) => new MarkupContent { Kind = format, Value = str }; ParameterInformation AsParameterInfo(string?paramName) => new ParameterInformation { Label = paramName, Documentation = AsMarkupContent(documentation.ParameterDescription(paramName)) }; var signatureLabel = $"{methodDecl.Item.QualifiedName.Name} {argTuple.PrintArgumentTuple()}"; foreach (var f in functors) { if (f.IsAdjoint) { signatureLabel = $"{Keywords.qsAdjointFunctor.id} {signatureLabel}"; } if (f.IsControlled) { signatureLabel = $"{Keywords.qsControlledFunctor.id} {signatureLabel}"; } } var doc = documentation.PrintSummary(format == MarkupKind.Markdown).Trim(); var info = new SignatureInformation { Documentation = AsMarkupContent(doc), Label = signatureLabel, // Note: the label needs to be expressed in a way that the active parameter is detectable Parameters = callArgs.Select(d => d.Item2).Select(AsParameterInfo).ToArray() }; var precedingArgs = callArgs .TakeWhile(item => item.Item1 == null || BeforePosition(item.Item1)) // skip args that have already been typed or - in the case of inner items - are missing .Reverse().SkipWhile(item => item.Item1 == null); // don't count missing, i.e. not yet typed items, of the relevant inner argument tuple return(new SignatureHelp { Signatures = new[] { info }, // since we don't support overloading there is just one signature here ActiveSignature = 0, ActiveParameter = precedingArgs.Count() }); }
/// <nodoc /> public SourceChangeAffectedContents(PathTable pathTable, FileContentManager fileContentManager) { m_pathTable = pathTable; m_fileContentManager = fileContentManager; }
public FileContentManagerSemanticPathExpander(FileContentManager fileContentManager, SemanticPathExpander innerExpander) { m_innerExpander = innerExpander; m_fileContentManager = fileContentManager; }
/// <summary> /// Returns true if the given range is valid, /// i.e. if both start and end are valid positions within the given file, and start is smaller than or equal to end. /// Throws an ArgumentNullException if an argument is null. /// </summary> internal static bool IsValidRange(LSP.Range range, FileContentManager file) => IsValidPosition(range?.Start, file) && IsValidPosition(range.End, file) && range.Start.IsSmallerThanOrEqualTo(range.End);
/// <summary> /// Loads configured symlink definitions (if not already loaded) /// Stores to cache for use by workers in distributed build /// Eagerly creates symlinks if lazy symlink creation is disabled /// </summary> public static async Task <Possible <SymlinkDefinitions> > TryPrepareSymlinkDefinitionsAsync( LoggingContext loggingContext, GraphReuseResult reuseResult, IConfiguration configuration, MasterService masterService, CacheInitializationTask cacheInitializerTask, PipExecutionContext context, ITempDirectoryCleaner tempDirectoryCleaner = null) { var pathTable = context.PathTable; bool isDistributedMaster = configuration.Distribution.BuildRole == DistributedBuildRoles.Master; Possible <SymlinkDefinitions> maybeSymlinkDefinitions = new Possible <SymlinkDefinitions>((SymlinkDefinitions)null); if (reuseResult?.IsFullReuse == true) { maybeSymlinkDefinitions = reuseResult.EngineSchedule.Scheduler.SymlinkDefinitions; } else if (configuration.Layout.SymlinkDefinitionFile.IsValid) { var symlinkFilePath = configuration.Layout.SymlinkDefinitionFile.ToString(pathTable); Logger.Log.SymlinkFileTraceMessage(loggingContext, I($"Loading symlink file from location '{symlinkFilePath}'.")); maybeSymlinkDefinitions = await SymlinkDefinitions.TryLoadAsync( loggingContext, pathTable, symlinkFilePath, symlinksDebugPath : configuration.Logging.LogsDirectory.Combine(pathTable, "DebugSymlinksDefinitions.log").ToString(pathTable), tempDirectoryCleaner : tempDirectoryCleaner); } if (!maybeSymlinkDefinitions.Succeeded || maybeSymlinkDefinitions.Result == null) { return(maybeSymlinkDefinitions); } // Need to store symlinks to cache for workers if (configuration.Distribution.BuildRole == DistributedBuildRoles.Master) { var possibleCacheInitializer = await cacheInitializerTask; if (!possibleCacheInitializer.Succeeded) { return(possibleCacheInitializer.Failure); } Logger.Log.SymlinkFileTraceMessage(loggingContext, I($"Storing symlink file for use by workers.")); var symlinkFile = configuration.Layout.SymlinkDefinitionFile.Expand(pathTable); var possibleStore = await TryStoreToCacheAsync( loggingContext, cache : possibleCacheInitializer.Result.CreateCacheForContext(context).ArtifactContentCache, symlinkFile : symlinkFile); if (!possibleStore.Succeeded) { return(possibleStore.Failure); } masterService.SymlinkFileContentHash = possibleStore.Result; Logger.Log.SymlinkFileTraceMessage(loggingContext, I($"Stored symlink file for use by workers.")); } if (!configuration.Schedule.UnsafeLazySymlinkCreation || configuration.Engine.PopulateSymlinkDirectories.Count != 0) { // Symlink definition file is defined, and BuildXL intends to create it eagerly. // At this point master and worker should have had its symlink definition file, if specified. if (!FileContentManager.CreateSymlinkEagerly(loggingContext, configuration, pathTable, maybeSymlinkDefinitions.Result, context.CancellationToken)) { return(new Failure <string>("Failed eagerly creating symlinks")); } } return(maybeSymlinkDefinitions); }
/// <nodoc /> public SourceChangeAffectedInputs(FileContentManager fileContentManager) { m_fileContentManager = fileContentManager; }
/// <summary> /// Returns completion items that match the given kind. /// </summary> /// <exception cref="ArgumentNullException">Thrown when any argument is null.</exception> /// <exception cref="ArgumentException">Thrown when the position is invalid.</exception> private static IEnumerable <CompletionItem> GetCompletionsForKind( FileContentManager file, CompilationUnit compilation, Position position, CompletionKind kind, string namespacePrefix = "") { if (file == null) { throw new ArgumentNullException(nameof(file)); } if (compilation == null) { throw new ArgumentNullException(nameof(compilation)); } if (!Utils.IsValidPosition(position)) { throw new ArgumentException(nameof(position)); } if (kind == null) { throw new ArgumentNullException(nameof(kind)); } if (namespacePrefix == null) { throw new ArgumentNullException(nameof(namespacePrefix)); } switch (kind) { case CompletionKind.Member member: return(GetCompletionsForKind(file, compilation, position, member.Item2, ResolveNamespaceAlias(file, compilation, position, member.Item1))); case CompletionKind.Keyword keyword: return(new[] { new CompletionItem { Label = keyword.Item, Kind = CompletionItemKind.Keyword } }); } var currentNamespace = file.TryGetNamespaceAt(position); var openNamespaces = namespacePrefix == "" ? GetOpenNamespaces(file, compilation, position) : new[] { namespacePrefix }; switch (kind.Tag) { case CompletionKind.Tags.UserDefinedType: return (GetTypeCompletions(file, compilation, currentNamespace, openNamespaces) .Concat(GetGlobalNamespaceCompletions(compilation, namespacePrefix)) .Concat(GetNamespaceAliasCompletions(file, compilation, position, namespacePrefix))); case CompletionKind.Tags.NamedItem: return(GetNamedItemCompletions(compilation)); case CompletionKind.Tags.Namespace: return (GetGlobalNamespaceCompletions(compilation, namespacePrefix) .Concat(GetNamespaceAliasCompletions(file, compilation, position, namespacePrefix))); case CompletionKind.Tags.Variable: return(GetLocalCompletions(file, compilation, position)); case CompletionKind.Tags.MutableVariable: return(GetLocalCompletions(file, compilation, position, mutableOnly: true)); case CompletionKind.Tags.Callable: return (GetCallableCompletions(file, compilation, currentNamespace, openNamespaces) .Concat(GetGlobalNamespaceCompletions(compilation, namespacePrefix)) .Concat(GetNamespaceAliasCompletions(file, compilation, position, namespacePrefix))); } return(Enumerable.Empty <CompletionItem>()); }
/// <summary> /// Given an SortedSet of changed lines, verifies the context for each token on one of these lines, /// and adds the computed diagnostics to the ones returned as out parameter. /// Marks the token indices which are to be excluded from compilation due to context errors. /// Returns the line numbers for which the context diagnostics have been recomputed. /// Throws an ArgumentNullException if any of the arguments is null. /// </summary> private static HashSet <int> VerifyContext(this FileContentManager file, SortedSet <int> changedLines, out List <Diagnostic> diagnostics) { if (file == null) { throw new ArgumentNullException(nameof(file)); } if (changedLines == null) { throw new ArgumentNullException(nameof(changedLines)); } IEnumerable <CodeFragment.TokenIndex> TokenIndices(int lineNr) => Enumerable.Range(0, file.GetTokenizedLine(lineNr).Count()).Select(index => new CodeFragment.TokenIndex(file, lineNr, index)); var tokensToVerify = changedLines.SelectMany(TokenIndices); var verifiedLines = new HashSet <int>(); List <Diagnostic> Verify(CodeFragment.TokenIndex tokenIndex) { var messages = new List <Diagnostic>(); var fragment = tokenIndex.GetFragment(); var context = tokenIndex.GetContext(); var fragmentStart = fragment.GetRange().Start; var(include, verifications) = Context.VerifySyntaxTokenContext(context); foreach (var msg in verifications) { messages.Add(Diagnostics.Generate(file.FileName.Value, msg, fragmentStart)); } if (include) { tokenIndex.MarkAsIncluded(); } else { tokenIndex.MarkAsExcluded(); } verifiedLines.Add(tokenIndex.Line); // if a token is newly included in or excluded from the compilation, // then this may impact context information for all following tokens var changedStatus = include ^ fragment.IncludeInCompilation; var next = tokenIndex.NextOnScope(); if (changedStatus && next != null && !changedLines.Contains(next.Line)) { // NOTE: since we invalidate context diagnostics on a per-line basis, we need to verify the whole line! var tokens = TokenIndices(next.Line); foreach (var token in tokens) { messages.AddRange(Verify(token)); } } return(messages); } diagnostics = tokensToVerify.SelectMany(tIndex => Verify(tIndex)).ToList(); return(verifiedLines); }
/// <summary> /// Returns true if the fragment has a delimiting character and the given position occurs after it. /// </summary> private static bool IsPositionAfterDelimiter(FileContentManager file, CodeFragment fragment, Position position) => fragment.FollowedBy != CodeFragment.MissingDelimiter && GetDelimiterPosition(file, fragment).IsSmallerThan(position);
/// <summary> /// Computes excess bracket errors for the given range of lines in file based on the corresponding CodeLine. /// Throws an ArgumentOutOfRangeException if start is not within file. /// </summary> private static IEnumerable <Diagnostic> ComputeScopeDiagnostics(this FileContentManager file, int start) => ComputeScopeDiagnostics(file, start, file == null ? 0 : file.NrLines() - start);
// routine(s) called by the FileContentManager upon updating a file /// <summary> /// Attempts to compute an incremental update for the change specified by start, count and newText, and updates file accordingly. /// The given argument newText replaces the entire lines from start to (but not including) start + count. /// If the given change is null, then (only) the currently queued unprocessed changes are processed. /// Throws an ArgumentNullException if file is null. /// Any other exceptions should be throws (and caught, and possibly re-thrown) during the updating. /// </summary> internal static void UpdateScopeTacking(this FileContentManager file, TextDocumentContentChangeEvent change) { if (file == null) { throw new ArgumentNullException(nameof(file)); } /// <summary> /// Replaces the lines in the range [start, end] with those for the given text. /// </summary> void ComputeUpdate(int start, int end, string text) { QsCompilerError.Verify(start >= 0 && end >= start && end < file.NrLines(), "invalid range for update"); // since both LF and CR in VS cause a line break on their own, // we need to check if the change causes subequent CR LF to merge into a single line break if (text.StartsWith(Utils.LF) && start > 0 && file.GetLine(start - 1).Text.EndsWith(Utils.CR)) { text = file.GetLine(--start).Text + text; } // we need to check if the change causes the next line to merge with the (last) changed line if (end + 1 < file.NrLines() && !Utils.EndOfLine.Match(text).Success) { text = text + file.GetLine(++end).Text; } var newLines = Utils.SplitLines(text); var count = end - start + 1; // note that the last line in the file won't end with a line break, // and is hence only captured by SplitLines if it is not empty // -> we therefore manually add the last line in the file if it is empty if (newLines.Length == 0 || // the case if the file will be empty after the update (start + count == file.NrLines() && Utils.EndOfLine.Match(newLines.Last()).Success)) { newLines = newLines.Concat(new string[] { string.Empty }).ToArray(); } QsCompilerError.Verify(newLines.Any(), "should have at least one line to replace"); file.Update(start, count, newLines); } file.SyncRoot.EnterUpgradeableReadLock(); try { // process the currently queued changes if necessary if (file.DequeueUnprocessedChanges(out int start, out string text)) { ComputeUpdate(start, start, text); } // process the given change if necessary if (change != null) { ComputeUpdate(change.Range.Start.Line, change.Range.End.Line, Utils.GetTextChangedLines(file, change)); } } finally { file.SyncRoot.ExitUpgradeableReadLock(); } }
/// <summary> /// Computes excess bracket errors for the given range of lines in file based on the corresponding CodeLine. /// Throws an ArgumentNullException if file is null. /// Throws an ArgumentOutOfRangeException if start is not within file. /// </summary> private static IEnumerable <Diagnostic> ComputeScopeDiagnostics(this FileContentManager file, int start) { return(ComputeScopeDiagnostics(file, start, file == null ? 0 : file.NrLines() - start)); } // will raise an exception if file is null