diff --git a/src/FsAutoComplete.Core/Utils.fs b/src/FsAutoComplete.Core/Utils.fs index 0fad06fb8..a80e2795c 100644 --- a/src/FsAutoComplete.Core/Utils.fs +++ b/src/FsAutoComplete.Core/Utils.fs @@ -515,6 +515,19 @@ type ReadOnlySpanExtensions = if found then i else -1 + [] + static member IndexOfAnyExcept(span: ReadOnlySpan, values: ReadOnlySpan) = + let mutable i = 0 + let mutable found = false + + while not found && i < span.Length do + if values.IndexOf span[i] < 0 then + found <- true + else + i <- i + 1 + + if found then i else -1 + [] static member LastIndexOfAnyExcept(span: ReadOnlySpan, value0: char, value1: char) = let mutable i = span.Length - 1 diff --git a/src/FsAutoComplete.Core/Utils.fsi b/src/FsAutoComplete.Core/Utils.fsi index 2d2bd94bc..c2b106c2c 100644 --- a/src/FsAutoComplete.Core/Utils.fsi +++ b/src/FsAutoComplete.Core/Utils.fsi @@ -185,6 +185,9 @@ type ReadOnlySpanExtensions = [] static member IndexOfAnyExcept: span: ReadOnlySpan * value0: char * value1: char -> int + [] + static member IndexOfAnyExcept: span: ReadOnlySpan * values: ReadOnlySpan -> int + [] static member LastIndexOfAnyExcept: span: ReadOnlySpan * value0: char * value1: char -> int #endif diff --git a/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryParentheses.fs b/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryParentheses.fs index fbc5f97ab..318ba0367 100644 --- a/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -7,6 +7,7 @@ open FsAutoComplete.CodeFix.Types open FsToolkit.ErrorHandling open FsAutoComplete open FsAutoComplete.LspHelpers +open FSharp.Compiler.Text let title = "Remove unnecessary parentheses" @@ -14,6 +15,27 @@ let title = "Remove unnecessary parentheses" module private Patterns = let inline toPat f x = if f x then ValueSome() else ValueNone + /// Starts with //. + [] + let (|StartsWithSingleLineComment|_|) (s: string) = + if s.AsSpan().TrimStart(' ').StartsWith("//".AsSpan()) then + ValueSome StartsWithSingleLineComment + else + ValueNone + + /// Starts with match, e.g., + /// + /// (match … with + /// | … -> …) + [] + let (|StartsWithMatch|_|) (s: string) = + let s = s.AsSpan().TrimStart ' ' + + if s.StartsWith("match".AsSpan()) && (s.Length = 5 || s[5] = ' ') then + ValueSome StartsWithMatch + else + ValueNone + [] module Char = [] @@ -90,8 +112,8 @@ let fix (getFileLines: GetFileLines) : CodeFix = | None -> id let (|ShiftLeft|NoShift|ShiftRight|) (sourceText: IFSACSourceText) = - let startLineNo = range.StartLine - let endLineNo = range.EndLine + let startLineNo = Line.toZ range.StartLine + let endLineNo = Line.toZ range.EndLine if startLineNo = endLineNo then NoShift @@ -105,11 +127,17 @@ let fix (getFileLines: GetFileLines) : CodeFix = match line.AsSpan(startCol).IndexOfAnyExcept(' ', ')') with | -1 -> loop innerOffsides (lineNo + 1) 0 | i -> - match innerOffsides with - | NoneYet -> loop (FirstLine(i + startCol)) (lineNo + 1) 0 - | FirstLine innerOffsides -> loop (FollowingLine(innerOffsides, i + startCol)) (lineNo + 1) 0 - | FollowingLine(firstLine, innerOffsides) -> - loop (FollowingLine(firstLine, min innerOffsides (i + startCol))) (lineNo + 1) 0 + match line[i + startCol ..] with + | StartsWithMatch + | StartsWithSingleLineComment -> loop innerOffsides (lineNo + 1) 0 + | _ -> + match innerOffsides with + | NoneYet -> loop (FirstLine(i + startCol)) (lineNo + 1) 0 + + | FirstLine innerOffsides -> loop (FollowingLine(innerOffsides, i + startCol)) (lineNo + 1) 0 + + | FollowingLine(firstLine, innerOffsides) -> + loop (FollowingLine(firstLine, min innerOffsides (i + startCol))) (lineNo + 1) 0 else innerOffsides @@ -133,24 +161,27 @@ let fix (getFileLines: GetFileLines) : CodeFix = let newText = let (|ShouldPutSpaceBefore|_|) (s: string) = - // ……(……) - // ↑↑ ↑ - (sourceText.TryGetChar(range.Start.IncColumn -1), sourceText.TryGetChar range.Start) - ||> Option.map2 (fun twoBefore oneBefore -> - match twoBefore, oneBefore, s[0] with - | _, _, ('\n' | '\r') -> None - | '[', '|', (Punctuation | LetterOrDigit) -> None - | _, '[', '<' -> Some ShouldPutSpaceBefore - | _, ('(' | '[' | '{'), _ -> None - | _, '>', _ -> Some ShouldPutSpaceBefore - | ' ', '=', _ -> Some ShouldPutSpaceBefore - | _, '=', ('(' | '[' | '{') -> None - | _, '=', (Punctuation | Symbol) -> Some ShouldPutSpaceBefore - | _, LetterOrDigit, '(' -> None - | _, (LetterOrDigit | '`'), _ -> Some ShouldPutSpaceBefore - | _, (Punctuation | Symbol), (Punctuation | Symbol) -> Some ShouldPutSpaceBefore - | _ -> None) - |> Option.flatten + match s with + | StartsWithMatch -> None + | _ -> + // ……(……) + // ↑↑ ↑ + (sourceText.TryGetChar(range.Start.IncColumn -1), sourceText.TryGetChar range.Start) + ||> Option.map2 (fun twoBefore oneBefore -> + match twoBefore, oneBefore, s[0] with + | _, _, ('\n' | '\r') -> None + | '[', '|', (Punctuation | LetterOrDigit) -> None + | _, '[', '<' -> Some ShouldPutSpaceBefore + | _, ('(' | '[' | '{'), _ -> None + | _, '>', _ -> Some ShouldPutSpaceBefore + | ' ', '=', _ -> Some ShouldPutSpaceBefore + | _, '=', ('(' | '[' | '{') -> None + | _, '=', (Punctuation | Symbol) -> Some ShouldPutSpaceBefore + | _, LetterOrDigit, '(' -> None + | _, (LetterOrDigit | '`'), _ -> Some ShouldPutSpaceBefore + | _, (Punctuation | Symbol), (Punctuation | Symbol) -> Some ShouldPutSpaceBefore + | _ -> None) + |> Option.flatten let (|ShouldPutSpaceAfter|_|) (s: string) = // (……)… @@ -160,22 +191,45 @@ let fix (getFileLines: GetFileLines) : CodeFix = match s[s.Length - 1], endChar with | '>', ('|' | ']') -> Some ShouldPutSpaceAfter | _, (')' | ']' | '[' | '}' | '.' | ';' | ',' | '|') -> None + | _, ('+' | '-' | '%' | '&' | '!' | '~') -> None | (Punctuation | Symbol), (Punctuation | Symbol | LetterOrDigit) -> Some ShouldPutSpaceAfter | LetterOrDigit, LetterOrDigit -> Some ShouldPutSpaceAfter | _ -> None) + let (|WouldTurnInfixIntoPrefix|_|) (s: string) = + // (……)… + // ↑ ↑ + sourceText.TryGetChar(range.End.IncColumn 1) + |> Option.bind (fun endChar -> + match s[s.Length - 1], endChar with + | (Punctuation | Symbol), ('+' | '-' | '%' | '&' | '!' | '~') -> + match sourceText.GetLine range.End with + | None -> None + | Some line -> + // (……)+… + // ↑ + match line.AsSpan(range.EndColumn).IndexOfAnyExcept("*/%-+:^@><=!|$.?".AsSpan()) with + | -1 -> None + | i when line[range.EndColumn + i] <> ' ' -> Some WouldTurnInfixIntoPrefix + | _ -> None + | _ -> None) + match adjusted with - | ShouldPutSpaceBefore & ShouldPutSpaceAfter -> " " + adjusted + " " - | ShouldPutSpaceBefore -> " " + adjusted - | ShouldPutSpaceAfter -> adjusted + " " - | adjusted -> adjusted + | WouldTurnInfixIntoPrefix -> ValueNone + | ShouldPutSpaceBefore & ShouldPutSpaceAfter -> ValueSome(" " + adjusted + " ") + | ShouldPutSpaceBefore -> ValueSome(" " + adjusted) + | ShouldPutSpaceAfter -> ValueSome(adjusted + " ") + | adjusted -> ValueSome adjusted return - [ { Edits = [| { Range = d.Range; NewText = newText } |] + newText + |> ValueOption.map (fun newText -> + { Edits = [| { Range = d.Range; NewText = newText } |] File = codeActionParams.TextDocument Title = title SourceDiagnostic = Some d - Kind = FixKind.Fix } ] + Kind = FixKind.Fix }) + |> ValueOption.toList | _notParens -> return [] }) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs index d02985128..55b8c8f2a 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs @@ -1606,7 +1606,7 @@ let private generateXmlDocumentationTests state = /// type MyRecord = { Foo: int } """ - + testCaseAsync "documentation for record type with multiple attribute lists" <| CodeFix.check server @@ -3436,7 +3436,68 @@ let private removeUnnecessaryParenthesesTests state = longFunctionName longVarName1 longVarName2 - """ ]) + """ + + testCaseAsync "Handles outlaw match exprs" + <| CodeFix.check + server + """ + 3 > (match x with + | 1 + | _ -> 3)$0 + """ + (Diagnostics.expectCode "FSAC0004") + selector + """ + 3 > match x with + | 1 + | _ -> 3 + """ + + testCaseAsync "Handles even more outlaw match exprs" + <| CodeFix.check + server + """ + 3 > ( match x with + | 1 + | _ -> 3)$0 + """ + (Diagnostics.expectCode "FSAC0004") + selector + """ + 3 > match x with + | 1 + | _ -> 3 + """ + + testCaseAsync "Handles single-line comments" + <| CodeFix.check + server + """ + 3 > (match x with + // Lol. + | 1 + | _ -> 3)$0 + """ + (Diagnostics.expectCode "FSAC0004") + selector + """ + 3 > match x with + // Lol. + | 1 + | _ -> 3 + """ + + testCaseAsync "Keep parens when removal would cause reparse of infix as prefix" + <| CodeFix.checkNotApplicable + server + """ + ""+(Unchecked.defaultof)$0+"" + """ + (Diagnostics.expectCode "FSAC0004") + selector + + ]) let tests textFactory state = testList