/// <summary> /// Computes the excess closing and scope error updates for the given replacements at the position specified by start and count in the given file. /// Returns a sequence of CodeLines for the remaining file, if the made replacements require updating the remaining file as well, and null otherwise. /// </summary> /// <exception cref="ArgumentException"><paramref name="replacements"/> does not at least contain one <see cref="CodeLine"/>.</exception> /// <exception cref="ArgumentOutOfRangeException"> /// The range defined by <paramref name="start"/> and <paramref name="count"/> is not within <paramref name="file"/>, or <paramref name="count"/> is less than 1. /// </exception> private static IEnumerable <CodeLine>?ComputeUpdates(FileContentManager file, int start, int count, CodeLine[] replacements) { if (start < 0 || start >= file.NrLines()) { throw new ArgumentOutOfRangeException(nameof(start)); } if (count < 1 || start + count > file.NrLines()) { throw new ArgumentOutOfRangeException(nameof(count)); } if (replacements.Length == 0) { throw new ArgumentException("replacements cannot be empty"); } var continueAtInFile = start + count; var remainingLines = file.GetLines(continueAtInFile, file.NrLines() - continueAtInFile); // how much the effective indentation (i.e. absolute indentation plus nr of excess closings up to that point) changed determines how much an what we need to update: var indentationChange = GetIndentationChange(file, continueAtInFile, replacements.Last()); var requiresStringDelimiterUpdate = ContinueString(file.GetLine(continueAtInFile - 1)) ^ ContinueString(replacements.Last()); if (requiresStringDelimiterUpdate) { // we need to recompute everything if the interpretation of what is code and what is a string changes... // since the interpretation of the remaining file changed, we need to update the entire file from start onwards return(ComputeCodeLines(remainingLines.Select(line => line.Text), replacements.Last()).ToList()); } else if (indentationChange != 0) { // if the replacements has more effective closing brackets (not just excess closings!) than the current part that will be replaced has, // then we need check the text of the remaining file as well in order to compute the correct update // if it has less (indentationChange > 0), then we could in principle simplify things somewhat by simply discarding the corresponding number of excess closing brackets return(remainingLines.GetUpdatedLines(remainingLines.First().Indentation + indentationChange)); } else { return(null); } }
// 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 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)); }
/// <summary> /// Returns true if the given position is valid, i.e. if the line is within the given file, /// and the character is within the text on that line (including text.Length). /// Throws an ArgumentNullException is an argument is null. /// </summary> internal static bool IsValidPosition(Position pos, FileContentManager file) { if (file == null) { throw new ArgumentNullException(nameof(file)); } return(IsValidPosition(pos) && pos.Line < file.NrLines() && pos.Character <= file.GetLine(pos.Line).Text.Length); }
/// <summary> /// Computes excess bracket errors for the given range of lines in file based on the corresponding CodeLine. /// </summary> /// <exception cref="ArgumentOutOfRangeException">The range [<paramref name="start"/>, <paramref name="start"/> + <paramref name="count"/>) is not within <paramref name="file"/>.</exception> private static IEnumerable <Diagnostic> ComputeScopeDiagnostics(this FileContentManager file, int start, int count) { foreach (var line in file.GetLines(start, count)) { foreach (var pos in line.ExcessBracketPositions) { yield return(Errors.ExcessBracketError(file.FileName, Position.Create(start, pos))); } foreach (var pos in line.ErrorDelimiterPositions) { yield return(Errors.InvalidCharacterInInterpolatedArgument(file.FileName, Position.Create(start, pos), file.GetLine(start).Text[pos])); } ++start; } }
/// <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; } } }
// 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(); } }