/// <summary> /// Returns the CodeFragment at the given position if such a fragment exists and null otherwise. /// If the given position is equal to the end of the fragment, that fragment is returned if includeEnd is set to true. /// If a fragment is determined for the given position, returns the corresponding token index as out parameter. /// Note that token indices are no longer valid as soon as the file is modified (possibly e.g. by queued background processing). /// Any query or attempt operation on the returned token index may result in an exception once it lost its validity. /// Returns null if the given file or the specified position is null, /// or if the specified position is not within the current Content range. /// </summary> public static CodeFragment?TryGetFragmentAt( this FileContentManager file, Position?pos, out CodeFragment.TokenIndex?tIndex, bool includeEnd = false) { tIndex = null; if (file == null || pos == null || !file.ContainsPosition(pos)) { return(null); } var start = pos.Line; var previous = file.GetTokenizedLine(start).Where(token => token.Range.Start.Column <= pos.Column).ToImmutableArray(); while (!previous.Any() && --start >= 0) { previous = file.GetTokenizedLine(start); } if (!previous.Any()) { return(null); } var lastPreceding = previous.Last().WithLineNumOffset(start); var overlaps = includeEnd ? pos <= lastPreceding.Range.End : pos < lastPreceding.Range.End; tIndex = overlaps ? new CodeFragment.TokenIndex(file, start, previous.Length - 1) : null; return(overlaps ? lastPreceding : null); }
/// <summary> /// Givent a position, verifies that the position is within the given file, and /// returns the effective indentation (i.e. the indentation when ignoring excess brackets throughout the file) /// at that position (i.e. not including the char at the given position). /// </summary> internal static int IndentationAt(this FileContentManager file, Position pos) { if (!file.ContainsPosition(pos)) { throw new ArgumentException("given position is not within file"); } var line = file.GetLine(pos.Line); var index = pos.Column; // check if the given position is within a string or a comment, or denotes an excess bracket, // and find the next closest position that isn't if (index >= line.WithoutEnding.Length) { return(line.FinalIndentation()); // not necessary, but saves doing the rest of the computation } index = line.FindInCode(trimmed => trimmed.Length - 1, 0, pos.Column); // if the given position is within a string, then this is the most convenient way to get the closest position that isn't.. if (index < 0) { return(line.Indentation); // perfectly valid scenario (if there is no relevant code before the given position) } // check how much the indentation changes in all valid (i.e. non-comment, non-string, non-excess-brackets) code up to that point // NOTE: per specification, this routine returns the indentation not incuding the char at the given position, // but if this char was within non-code text, then we need to include this char ... see (*) var indexInCode = IndexInRelevantCode(index, line); QsCompilerError.Verify(indexInCode >= 0, "index in code should be positive"); var code = RelevantCode(line).Substring(0, index < pos.Column ? indexInCode + 1 : indexInCode); // (*) - yep, that's a bit awkward, but the cleanest I can come up with right now var indentation = line.Indentation + NrIndents(code) - NrUnindents(code); QsCompilerError.Verify(indentation >= 0, "computed indentation at any position in the file should be positive"); return(indentation); }
/// <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 ((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 || !file.ContainsPosition(pos)) { return(null); } file.SyncRoot.EnterReadLock(); try { var declarations = file.CallableDeclarationTokens(); var precedingDecl = declarations.TakeWhile(tIndex => tIndex.GetFragment().Range.Start < pos); if (!precedingDecl.Any()) { return(null); } var closestCallable = precedingDecl.Last(); var callablePosition = closestCallable.GetFragment().Range.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.Range.Start < pos); var lastPreceding = precedingSpec.Any() ? precedingSpec.Last() : null; if (specializations.Any() && lastPreceding == null) { // the given position is within a callable declaration return((callableName, callablePosition), (null, null)); } return(lastPreceding == null ? ((callableName, callablePosition), (QsSpecializationKind.QsBody, callablePosition)) : ((callableName, callablePosition), (GetSpecializationKind(lastPreceding), lastPreceding.Range.Start))); } finally { file.SyncRoot.ExitReadLock(); } }
/// <summary> /// Returns the name of the namespace to which the given position belongs. /// Returns null if the given file or position is null, or if no such namespace can be found /// (e.g. because the namespace name is invalid). /// </summary> public static string?TryGetNamespaceAt(this FileContentManager file, Position pos) { if (file == null || pos == null || !file.ContainsPosition(pos)) { return(null); } var namespaces = file.GetNamespaceDeclarations(); var preceding = namespaces.TakeWhile(tuple => tuple.Item2.Start < pos); return(preceding.Any() ? preceding.Last().Item1 : null); }
/// <summary> /// Returns the position right after where the fragment containing the given position ends. /// If the closest previous ending was on the last character in a line, then the returned position is on the same line after the last character. /// Updates the given position to point to the first character in the fragment that contains code. /// Throws an ArgumentException if the given position is not smaller than the position after the last piece of code in the file (given by LastInFile). /// Throws an ArgumentException if the given position is not within file. /// </summary> internal static Position FragmentEnd(this FileContentManager file, ref Position current) { if (!file.ContainsPosition(current)) { throw new ArgumentException("given position is not within file"); } var lastInFile = LastInFile(file); if (lastInFile <= current) { throw new ArgumentException("no fragment exists at the given position"); }
/// <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> private static (CompletionScope?, QsFragmentKind?) GetCompletionEnvironment( FileContentManager file, Position position, out CodeFragment?fragment) { if (!file.ContainsPosition(position)) { fragment = null; return(null, null); } var token = GetTokenAtOrBefore(file, position); if (token is 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(index => index.GetFragment()) .ToImmutableList(); var scope = parents.IsEmpty ? CompletionScope.TopLevel : parents.FirstOrDefault()?.Kind?.IsNamespaceDeclaration ?? false ? CompletionScope.NamespaceTopLevel : parents.Any(parent => parent.Kind?.IsFunctionDeclaration ?? false) ? CompletionScope.Function : parents.FirstOrDefault()?.Kind?.IsOperationDeclaration ?? false ? CompletionScope.OperationTopLevel : parents.Any(parent => parent.Kind?.IsOperationDeclaration ?? false) ? CompletionScope.Operation : null; var previous = relativeIndentation == 0 && IsPositionAfterDelimiter(file, fragment, position) ? fragment.Kind : relativeIndentation == 0 ? token.PreviousOnScope()?.GetFragment().Kind : relativeIndentation == 1 ? token.GetNonEmptyParent()?.GetFragment().Kind : null; return(scope, previous); }