diff --git a/.vscode/launch.json b/.vscode/launch.json index c30698aca..6dc4f793b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -52,7 +52,7 @@ "args": [ "--debug", "--filter", - "FSAC.lsp.${input:loader}.${input:lsp-server}.${input:testName}" + "FSAC.lsp.${input:loader}.${input:compiler}.${input:testName}" ] }, { @@ -103,13 +103,13 @@ "type": "pickString" }, { - "id": "lsp-server", - "description": "The lsp serrver", + "id": "compiler", + "description": "The compiler to use", "options": [ - "FSharpLspServer", - "AdaptiveLspServer" + "BackgroundCompiler", + "TransparentCompiler" ], - "default": "AdaptiveLspServer", + "default": "BackgroundCompiler", "type": "pickString", }, { @@ -118,4 +118,4 @@ "type": "promptString" } ] -} \ No newline at end of file +} diff --git a/benchmarks/SourceTextBenchmarks.fs b/benchmarks/SourceTextBenchmarks.fs index 9d6f359fc..3c86b5321 100644 --- a/benchmarks/SourceTextBenchmarks.fs +++ b/benchmarks/SourceTextBenchmarks.fs @@ -3,6 +3,7 @@ namespace Benchmarks open System open FSharp.Data.Adaptive open Microsoft.CodeAnalysis.Text + type FileVersion = int @@ -11,28 +12,40 @@ module Helpers = open FSharp.UMX open System.Collections.Generic - let fileContents = IO.File.ReadAllText(@"C:\Users\jimmy\Repositories\public\TheAngryByrd\span-playground\Romeo and Juliet by William Shakespeare.txt") + let fileContents = + IO.File.ReadAllText( + @"C:\Users\jimmy\Repositories\public\TheAngryByrd\span-playground\Romeo and Juliet by William Shakespeare.txt" + ) + + let initRoslynSourceText () = SourceText.From(fileContents) - let initRoslynSourceText () = - SourceText.From(fileContents) + let convertToTextSpan (sourceText: SourceText, range: Ionide.LanguageServerProtocol.Types.Range) = + let start = + uint32 sourceText.Lines.[int (max 0u range.Start.Line)].Start + + range.Start.Character - let convertToTextSpan (sourceText : SourceText, range : Ionide.LanguageServerProtocol.Types.Range) = - let start = sourceText.Lines.[max 0 (range.Start.Line)].Start + range.Start.Character let endPosition = - sourceText.Lines.[min (range.End.Line) (sourceText.Lines.Count - 1)].Start - + range.End.Character - TextSpan(start, endPosition - start) + uint32 sourceText.Lines.[int32 (min range.End.Line (uint32 (sourceText.Lines.Count - 1)))].Start + + range.End.Character - let addToSourceText (sourceText : SourceText, range : Ionide.LanguageServerProtocol.Types.Range, text : string) = - let textSpan = convertToTextSpan(sourceText, range) + TextSpan(int start, int endPosition - int start) + + let addToSourceText (sourceText: SourceText, range: Ionide.LanguageServerProtocol.Types.Range, text: string) = + let textSpan = convertToTextSpan (sourceText, range) let newText = sourceText.WithChanges([| TextChange(textSpan, text) |]) newText - let addToSourceTextMany (sourceText : SourceText, spans : IEnumerable) = - let textSpans = spans |> Seq.map (fun (range, text) -> TextChange(convertToTextSpan(sourceText, range), text)) |> Seq.toArray + let addToSourceTextMany + (sourceText: SourceText, spans: IEnumerable) + = + let textSpans = + spans + |> Seq.map (fun (range, text) -> TextChange(convertToTextSpan (sourceText, range), text)) + |> Seq.toArray + let newText = sourceText.WithChanges(textSpans) newText @@ -40,17 +53,33 @@ open BenchmarkDotNet open BenchmarkDotNet.Attributes open Helpers open BenchmarkDotNet.Jobs + [] [] -type SourceText_LineChanges_Benchmarks ()= +type SourceText_LineChanges_Benchmarks() = [] member val public N = 0 with get, set [] - member this.Roslyn_Text_changeText_everyUpdate () = + member this.Roslyn_Text_changeText_everyUpdate() = let mutable file = initRoslynSourceText () - file <- addToSourceText(file, { Start = { Line = 0; Character = 5 }; End = { Line = 0; Character = 5 } }, "World") - for i in 1..this.N do - file <- addToSourceText(file, { Start = { Line = 0; Character = 10 }; End = { Line = 0; Character = 10 } }, "\nLOL") + + file <- + addToSourceText ( + file, + { Start = { Line = 0u; Character = 5u } + End = { Line = 0u; Character = 5u } }, + "World" + ) + + for i in 1 .. this.N do + file <- + addToSourceText ( + file, + { Start = { Line = 0u; Character = 10u } + End = { Line = 0u; Character = 10u } }, + "\nLOL" + ) + file.Lines |> Seq.toArray |> ignore diff --git a/nuget.config b/nuget.config new file mode 100644 index 000000000..6f82b6baa --- /dev/null +++ b/nuget.config @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/paket.dependencies b/paket.dependencies index 40fc752da..1354c1e70 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -5,7 +5,7 @@ framework: netstandard2.0, netstandard2.1, net6.0, net7.0, net8.0 source https://api.nuget.org/v3/index.json # this is the FCS nightly feed, re-enable at your own risk! # source: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json -#source: ./libs +# source: ./libs storage: none strategy: min lowest_matching: true @@ -52,7 +52,7 @@ nuget Expecto.Diff nuget YoloDev.Expecto.TestSdk nuget AltCover nuget GitHubActionsTestLogger -nuget Ionide.LanguageServerProtocol >= 0.4.23 +nuget Ionide.LanguageServerProtocol 0.6.0 nuget Microsoft.Extensions.Caching.Memory nuget OpenTelemetry.Api >= 1.3.2 nuget OpenTelemetry.Exporter.OpenTelemetryProtocol >= 1.3.2 # 1.4 bumps to 7.0 versions of System.Diagnostics libs, so can't use it diff --git a/paket.lock b/paket.lock index e32373207..3784602b8 100644 --- a/paket.lock +++ b/paket.lock @@ -134,7 +134,7 @@ NUGET System.Reflection.Metadata (>= 5.0) Ionide.Analyzers (0.11) Ionide.KeepAChangelog.Tasks (0.1.8) - copy_local: true - Ionide.LanguageServerProtocol (0.4.23) + Ionide.LanguageServerProtocol (0.6.0) FSharp.Core (>= 6.0) Newtonsoft.Json (>= 13.0.1) StreamJsonRpc (>= 2.16.36) diff --git a/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs b/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs index 458c1f62f..9458913f5 100644 --- a/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs +++ b/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs @@ -165,7 +165,14 @@ let writeAbstractClassStub let getMemberByLocation (_: string, range: Range, _: Range) = match doc.GetLine range.Start with | Some lineText -> - match Lexer.getSymbol range.Start.Line range.Start.Column lineText SymbolLookupKind.ByLongIdent [||] with + match + Lexer.getSymbol + (uint32 range.Start.Line) + (uint32 range.Start.Column) + lineText + SymbolLookupKind.ByLongIdent + [||] + with | Some sym -> checkResultForFile.GetCheckResults.GetSymbolUseAtLocation( range.StartLine, diff --git a/src/FsAutoComplete.Core/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index 4c50e4ae4..c61320b00 100644 --- a/src/FsAutoComplete.Core/Commands.fs +++ b/src/FsAutoComplete.Core/Commands.fs @@ -267,14 +267,15 @@ module Commands = let getNamespaceSuggestions (tyRes: ParseAndCheckResults) (pos: Position) (line: LineStr) = async { - match Lexer.findLongIdents (pos.Column, line) with + match Lexer.findLongIdents (uint32 pos.Column, line) with | None -> return CoreResponse.InfoRes "Ident not found" | Some(_, idents) -> match ParsedInput.GetEntityKind(pos, tyRes.GetParseResults.ParseTree) with | None -> return CoreResponse.InfoRes "EntityKind not found" | Some entityKind -> - let symbol = Lexer.getSymbol pos.Line pos.Column line SymbolLookupKind.Fuzzy [||] + let symbol = + Lexer.getSymbol (uint32 pos.Line) (uint32 pos.Column) line SymbolLookupKind.Fuzzy [||] match symbol with | None -> return CoreResponse.InfoRes "Symbol at position not found" diff --git a/src/FsAutoComplete.Core/FileSystem.fs b/src/FsAutoComplete.Core/FileSystem.fs index 43744a574..54ae88996 100644 --- a/src/FsAutoComplete.Core/FileSystem.fs +++ b/src/FsAutoComplete.Core/FileSystem.fs @@ -122,7 +122,7 @@ type IFSACSourceText = /// Provides safe access to a line of the file via FCS-provided Position abstract member GetLine: position: Position -> option /// Provide safe access to the length of a line of the file via FCS-provided Position - abstract member GetLineLength: position: Position -> option + abstract member GetLineLength: position: Position -> option abstract member GetCharUnsafe: position: Position -> char /// Provides safe access to a character of the file via FCS-provided Position. /// Also available in indexer form: x[pos] @@ -262,11 +262,11 @@ module RoslynSourceText = else Some((x :> ISourceText).GetLineString(pos.Line - 1)) - member x.GetLineLength(pos: Position) : int option = + member x.GetLineLength(pos: Position) : uint32 option = if pos.Line > totalLinesLength () then None else - Some((x :> ISourceText).GetLineString(pos.Line - 1).Length) + Some(uint32 ((x :> ISourceText).GetLineString(pos.Line - 1).Length)) member x.GetCharUnsafe(pos: Position) : char = (x :> IFSACSourceText).GetLine(pos).Value[pos.Column - 1] @@ -331,7 +331,7 @@ module RoslynSourceText = let! np = (x :> IFSACSourceText).PrevPos pos let! prevLineLength = (x :> IFSACSourceText).GetLineLength(np) - if np.Column < 1 || prevLineLength < np.Column then + if np.Column < 1 || prevLineLength < uint32 np.Column then return! (x :> IFSACSourceText).TryGetPrevChar(np) else return np, (x :> IFSACSourceText).GetCharUnsafe np diff --git a/src/FsAutoComplete.Core/FileSystem.fsi b/src/FsAutoComplete.Core/FileSystem.fsi index 6829c439d..6fe817e17 100644 --- a/src/FsAutoComplete.Core/FileSystem.fsi +++ b/src/FsAutoComplete.Core/FileSystem.fsi @@ -66,7 +66,7 @@ type IFSACSourceText = /// Provides safe access to a line of the file via FCS-provided Position abstract member GetLine: position: Position -> option /// Provide safe access to the length of a line of the file via FCS-provided Position - abstract member GetLineLength: position: Position -> option + abstract member GetLineLength: position: Position -> option abstract member GetCharUnsafe: position: Position -> char /// Provides safe access to a character of the file via FCS-provided Position. /// Also available in indexer form: x[pos] diff --git a/src/FsAutoComplete.Core/KeywordList.fs b/src/FsAutoComplete.Core/KeywordList.fs index 907806e00..ff97983e0 100644 --- a/src/FsAutoComplete.Core/KeywordList.fs +++ b/src/FsAutoComplete.Core/KeywordList.fs @@ -52,7 +52,7 @@ module KeywordList = InsertText = Some kv.Key FilterText = Some kv.Key SortText = Some kv.Key - Documentation = Some(Documentation.String kv.Value) + Documentation = Some(U2.C1 kv.Value) Label = label }) |> Seq.toArray diff --git a/src/FsAutoComplete.Core/Lexer.fs b/src/FsAutoComplete.Core/Lexer.fs index 128564a9f..803dc487c 100644 --- a/src/FsAutoComplete.Core/Lexer.fs +++ b/src/FsAutoComplete.Core/Lexer.fs @@ -17,9 +17,9 @@ type SymbolKind = type LexerSymbol = { Kind: SymbolKind - Line: int - LeftColumn: int - RightColumn: int + Line: uint32 + LeftColumn: uint32 + RightColumn: uint32 Text: string } [] @@ -32,12 +32,12 @@ type SymbolLookupKind = type private DraftToken = { Kind: SymbolKind Token: FSharpTokenInfo - RightColumn: int } + RightColumn: uint32 } static member inline Create kind token = { Kind = kind Token = token - RightColumn = token.LeftColumn + token.FullMatchedLength - 1 } + RightColumn = uint32 (token.LeftColumn + token.FullMatchedLength - 1) } module Lexer = let logger = LogProvider.getLoggerByName "Lexer" @@ -129,7 +129,7 @@ module Lexer = match lastToken with //Operator starting with . (like .>>) should be operator | Some({ Kind = SymbolKind.Dot } as lastToken) when - isOperator token && token.LeftColumn <= lastToken.RightColumn + isOperator token && token.LeftColumn <= int lastToken.RightColumn -> let mergedToken = { lastToken.Token with @@ -141,7 +141,7 @@ module Lexer = { lastToken with Token = mergedToken Kind = SymbolKind.Operator } - | Some t when token.LeftColumn <= t.RightColumn -> acc, lastToken + | Some t when token.LeftColumn <= int t.RightColumn -> acc, lastToken | Some({ Kind = SymbolKind.ActivePattern } as lastToken) when token.Tag = FSharpTokenTag.BAR || token.Tag = FSharpTokenTag.IDENT @@ -157,7 +157,7 @@ module Lexer = Some { lastToken with Token = mergedToken - RightColumn = lastToken.RightColumn + token.FullMatchedLength } + RightColumn = lastToken.RightColumn + uint32 token.FullMatchedLength } | _ -> match token, lineStr with | GenericTypeParameterPrefix -> acc, Some(DraftToken.Create GenericTypeParameter token) @@ -179,7 +179,7 @@ module Lexer = // ^ operator | Some { Kind = SymbolKind.StaticallyResolvedTypeParameter } -> { Kind = SymbolKind.Operator - RightColumn = token.RightColumn - 1 + RightColumn = uint32 (token.RightColumn - 1) Token = token } | _ -> let kind = @@ -198,8 +198,8 @@ module Lexer = // Returns symbol at a given position. let private getSymbolFromTokens (tokens: FSharpTokenInfo list) - line - col + (line: uint32) + (col: uint32) (lineStr: string) lookupKind : LexerSymbol option = @@ -211,11 +211,11 @@ module Lexer = | SymbolLookupKind.Simple | SymbolLookupKind.Fuzzy -> tokens - |> List.filter (fun x -> x.Token.LeftColumn <= col && x.RightColumn + 1 >= col) + |> List.filter (fun x -> x.Token.LeftColumn <= int col && x.RightColumn + 1u >= col) | SymbolLookupKind.ForCompletion -> tokens - |> List.filter (fun x -> x.Token.LeftColumn <= col && x.RightColumn >= col) - | SymbolLookupKind.ByLongIdent -> tokens |> List.filter (fun x -> x.Token.LeftColumn <= col) + |> List.filter (fun x -> x.Token.LeftColumn <= int col && x.RightColumn >= col) + | SymbolLookupKind.ByLongIdent -> tokens |> List.filter (fun x -> x.Token.LeftColumn <= int col) match lookupKind with | SymbolLookupKind.ByLongIdent -> @@ -228,8 +228,8 @@ module Lexer = if t2.Tag = FSharpTokenTag.DOT then tryFindStartColumn remainingTokens else - Some t1.LeftColumn - | { Kind = Ident; Token = t } :: _ -> Some t.LeftColumn + Some(uint32 t1.LeftColumn) + | { Kind = Ident; Token = t } :: _ -> Some(uint32 t.LeftColumn) | _ :: _ | [] -> None @@ -248,8 +248,8 @@ module Lexer = { Kind = Ident Line = line LeftColumn = leftCol - RightColumn = first.RightColumn + 1 - Text = lineStr.[leftCol .. first.RightColumn] }) + RightColumn = first.RightColumn + 1u + Text = lineStr.[int leftCol .. int first.RightColumn] }) | SymbolLookupKind.Fuzzy -> // Select IDENT token. If failed, select OPERATOR token. tokensUnderCursor @@ -266,8 +266,8 @@ module Lexer = |> Option.map (fun token -> { Kind = token.Kind Line = line - LeftColumn = token.Token.LeftColumn - RightColumn = token.RightColumn + 1 + LeftColumn = uint32 token.Token.LeftColumn + RightColumn = token.RightColumn + 1u Text = lineStr.Substring(token.Token.LeftColumn, token.Token.FullMatchedLength) }) | SymbolLookupKind.ForCompletion | SymbolLookupKind.Simple -> @@ -276,11 +276,11 @@ module Lexer = |> Option.map (fun token -> { Kind = token.Kind Line = line - LeftColumn = token.Token.LeftColumn - RightColumn = token.RightColumn + 1 + LeftColumn = uint32 token.Token.LeftColumn + RightColumn = token.RightColumn + 1u Text = lineStr.Substring(token.Token.LeftColumn, token.Token.FullMatchedLength) }) - let getSymbol line col lineStr lookupKind (args: string[]) = + let getSymbol (line: uint32) (col: uint32) lineStr lookupKind (args: string[]) = let tokens = tokenizeLine args lineStr try @@ -307,25 +307,25 @@ module Lexer = // (we look for full identifier in the backward direction, but only // for a short identifier forward - this means that when you hover // 'B' in 'A.B.C', you will get intellisense for 'A.B' module) - let findIdents col lineStr lookupType = + let findIdents (col: uint32) lineStr lookupType = if lineStr = "" then None else - getSymbol 0 col lineStr lookupType [||] |> Option.bind tryGetLexerSymbolIslands + getSymbol 0u col lineStr lookupType [||] |> Option.bind tryGetLexerSymbolIslands let findLongIdents (col, lineStr) = findIdents col lineStr SymbolLookupKind.Fuzzy - let findLongIdentsAndResidue (col, lineStr: string) = - let lineStr = lineStr.Substring(0, System.Math.Max(0, col)) + let findLongIdentsAndResidue (col: uint32, lineStr: string) = + let lineStr = lineStr.Substring(0, int col) - match getSymbol 0 col lineStr SymbolLookupKind.ByLongIdent [||] with + match getSymbol 0u col lineStr SymbolLookupKind.ByLongIdent [||] with | Some sym -> match sym.Text with | "" -> [], "" | text -> let res = text.Split '.' |> List.ofArray |> List.rev - if lineStr.[col - 1] = '.' then + if lineStr.[int col - 1] = '.' then res |> List.rev, "" else match res with diff --git a/src/FsAutoComplete.Core/Lexer.fsi b/src/FsAutoComplete.Core/Lexer.fsi index cab9ee678..4a5167a90 100644 --- a/src/FsAutoComplete.Core/Lexer.fsi +++ b/src/FsAutoComplete.Core/Lexer.fsi @@ -16,9 +16,9 @@ type SymbolKind = type LexerSymbol = { Kind: SymbolKind - Line: int - LeftColumn: int - RightColumn: int + Line: uint32 + LeftColumn: uint32 + RightColumn: uint32 Text: string } [] @@ -31,7 +31,7 @@ type SymbolLookupKind = type private DraftToken = { Kind: SymbolKind Token: FSharpTokenInfo - RightColumn: int } + RightColumn: uint32 } static member inline Create: kind: SymbolKind -> token: FSharpTokenInfo -> DraftToken @@ -41,9 +41,14 @@ module Lexer = val tokenizeLine: args: string[] -> lineStr: string -> FSharpTokenInfo list val getSymbol: - line: int -> col: int -> lineStr: string -> lookupKind: SymbolLookupKind -> args: string[] -> LexerSymbol option - - val findIdents: col: int -> lineStr: string -> lookupType: SymbolLookupKind -> (int * string array) option - val findLongIdents: col: int * lineStr: string -> (int * string array) option - val findLongIdentsAndResidue: col: int * lineStr: string -> string list * string - val findClosestIdent: col: int -> lineStr: string -> (int * string array) option + line: uint32 -> + col: uint32 -> + lineStr: string -> + lookupKind: SymbolLookupKind -> + args: string[] -> + LexerSymbol option + + val findIdents: col: uint32 -> lineStr: string -> lookupType: SymbolLookupKind -> (uint32 * string array) option + val findLongIdents: col: uint32 * lineStr: string -> (uint32 * string array) option + val findLongIdentsAndResidue: col: uint32 * lineStr: string -> string list * string + val findClosestIdent: col: uint32 -> lineStr: string -> (uint32 * string array) option diff --git a/src/FsAutoComplete.Core/ParseAndCheckResults.fs b/src/FsAutoComplete.Core/ParseAndCheckResults.fs index 91fa41d52..9b3622c82 100644 --- a/src/FsAutoComplete.Core/ParseAndCheckResults.fs +++ b/src/FsAutoComplete.Core/ParseAndCheckResults.fs @@ -86,13 +86,13 @@ type ParseAndCheckResults } member x.TryFindIdentifierDeclaration (pos: Position) (lineStr: LineStr) = - match Lexer.findLongIdents (pos.Column, lineStr) with + match Lexer.findLongIdents (uint32 pos.Column, lineStr) with | None -> async.Return(ResultOrString.Error "Could not find ident at this location") | Some(col, identIsland) -> let identIsland = Array.toList identIsland let declarations = - checkResults.GetDeclarationLocation(pos.Line, col, lineStr, identIsland, preferFlag = false) + checkResults.GetDeclarationLocation(pos.Line, int col, lineStr, identIsland, preferFlag = false) let decompile assembly externalSym = match Decompiler.tryFindExternalDeclaration checkResults (assembly, externalSym) with @@ -123,7 +123,7 @@ type ParseAndCheckResults let tryRecoverExternalSymbolForNonexistentDecl (rangeInNonexistentFile: FSharp.Compiler.Text.Range) : ResultOrString * string> = - match Lexer.findLongIdents (pos.Column - 1, lineStr) with + match Lexer.findLongIdents (uint32 (pos.Column - 1), lineStr) with | None -> ResultOrString.Error( sprintf "Range for nonexistent file found, no ident found: %s" rangeInNonexistentFile.FileName @@ -132,7 +132,7 @@ type ParseAndCheckResults let identIsland = Array.toList identIsland let symbolUse = - checkResults.GetSymbolUseAtLocation(pos.Line, col, lineStr, identIsland) + checkResults.GetSymbolUseAtLocation(pos.Line, int col, lineStr, identIsland) match symbolUse with | None -> @@ -229,13 +229,13 @@ type ParseAndCheckResults member __.TryFindTypeDeclaration (pos: Position) (lineStr: LineStr) = async { - match Lexer.findLongIdents (pos.Column, lineStr) with + match Lexer.findLongIdents (uint32 pos.Column, lineStr) with | None -> return Error "Cannot find ident at this location" | Some(col, identIsland) -> let identIsland = Array.toList identIsland let symbol = - checkResults.GetSymbolUseAtLocation(pos.Line, col, lineStr, identIsland) + checkResults.GetSymbolUseAtLocation(pos.Line, int col, lineStr, identIsland) match symbol with | None -> return Error "Cannot find symbol at this location" @@ -319,7 +319,7 @@ type ParseAndCheckResults } member __.TryGetToolTip (pos: Position) (lineStr: LineStr) = - match Lexer.findLongIdents (pos.Column, lineStr) with + match Lexer.findLongIdents (uint32 pos.Column, lineStr) with | None -> logger.info (Log.setMessageI $"Cannot find ident for tooltip: {pos.Column:column} in {lineStr:lineString}") None @@ -327,7 +327,7 @@ type ParseAndCheckResults let identIsland = Array.toList identIsland // TODO: Display other tooltip types, for example for strings or comments where appropriate let tip = - checkResults.GetToolTip(pos.Line, col, lineStr, identIsland, FSharpTokenTag.Identifier) + checkResults.GetToolTip(pos.Line, int col, lineStr, identIsland, FSharpTokenTag.Identifier) match tip with | ToolTipText(elems) when elems |> List.forall ((=) ToolTipElement.None) -> @@ -354,7 +354,7 @@ type ParseAndCheckResults | Completion.Context.StringLiteral -> None | Completion.Context.SynType | Completion.Context.Unknown -> - match Lexer.findLongIdents (pos.Column, lineStr) with + match Lexer.findLongIdents (uint32 pos.Column, lineStr) with | None -> logger.info (Log.setMessageI $"Cannot find ident for tooltip: {pos.Column:column} in {lineStr:lineString}") None @@ -362,10 +362,10 @@ type ParseAndCheckResults let identIsland = Array.toList identIsland // TODO: Display other tooltip types, for example for strings or comments where appropriate let tip = - checkResults.GetToolTip(pos.Line, col, lineStr, identIsland, FSharpTokenTag.Identifier) + checkResults.GetToolTip(pos.Line, int col, lineStr, identIsland, FSharpTokenTag.Identifier) let symbol = - checkResults.GetSymbolUseAtLocation(pos.Line, col, lineStr, identIsland) + checkResults.GetSymbolUseAtLocation(pos.Line, int col, lineStr, identIsland) match tip with | EmptyTooltip when symbol.IsNone -> @@ -418,16 +418,16 @@ type ParseAndCheckResults |> Some member __.TryGetFormattedDocumentation (pos: Position) (lineStr: LineStr) = - match Lexer.findLongIdents (pos.Column, lineStr) with + match Lexer.findLongIdents (uint32 pos.Column, lineStr) with | None -> Error "Cannot find ident" | Some(col, identIsland) -> let identIsland = Array.toList identIsland // TODO: Display other tooltip types, for example for strings or comments where appropriate let tip = - checkResults.GetToolTip(pos.Line, col, lineStr, identIsland, FSharpTokenTag.Identifier) + checkResults.GetToolTip(pos.Line, int col, lineStr, identIsland, FSharpTokenTag.Identifier) let symbol = - checkResults.GetSymbolUseAtLocation(pos.Line, col, lineStr, identIsland) + checkResults.GetSymbolUseAtLocation(pos.Line, int col, lineStr, identIsland) match tip with | ToolTipText(elems) when elems |> List.forall ((=) ToolTipElement.None) && symbol.IsNone -> @@ -513,22 +513,22 @@ type ParseAndCheckResults Ok(symbol.XmlDocSig, symbol.Assembly.FileName |> Option.defaultValue "", symbol.XmlDoc, signature, footer, cn) member __.TryGetSymbolUse (pos: Position) (lineStr: LineStr) : FSharpSymbolUse option = - match Lexer.findLongIdents (pos.Column, lineStr) with + match Lexer.findLongIdents (uint32 pos.Column, lineStr) with | None -> None | Some(colu, identIsland) -> let identIsland = Array.toList identIsland - checkResults.GetSymbolUseAtLocation(pos.Line, colu, lineStr, identIsland) + checkResults.GetSymbolUseAtLocation(pos.Line, int colu, lineStr, identIsland) member x.TryGetSymbolUseFromIdent (sourceText: ISourceText) (ident: Ident) : FSharpSymbolUse option = let line = sourceText.GetLineString(ident.idRange.EndLine - 1) x.GetCheckResults.GetSymbolUseAtLocation(ident.idRange.EndLine, ident.idRange.EndColumn, line, [ ident.idText ]) member __.TryGetSymbolUses (pos: Position) (lineStr: LineStr) : FSharpSymbolUse list = - match Lexer.findLongIdents (pos.Column, lineStr) with + match Lexer.findLongIdents (uint32 pos.Column, lineStr) with | None -> [] | Some(colu, identIsland) -> let identIsland = Array.toList identIsland - checkResults.GetSymbolUsesAtLocation(pos.Line, colu, lineStr, identIsland) + checkResults.GetSymbolUsesAtLocation(pos.Line, int colu, lineStr, identIsland) member x.TryGetSymbolUseAndUsages (pos: Position) (lineStr: LineStr) = let symbolUse = x.TryGetSymbolUse pos lineStr @@ -540,14 +540,14 @@ type ParseAndCheckResults Ok(symbolUse, symbolUses) member __.TryGetSignatureData (pos: Position) (lineStr: LineStr) = - match Lexer.findLongIdents (pos.Column, lineStr) with + match Lexer.findLongIdents (uint32 pos.Column, lineStr) with | None -> ResultOrString.Error "No ident at this location" | Some(colu, identIsland) -> let identIsland = Array.toList identIsland let symbolUse = - checkResults.GetSymbolUseAtLocation(pos.Line, colu, lineStr, identIsland) + checkResults.GetSymbolUseAtLocation(pos.Line, int colu, lineStr, identIsland) match symbolUse with | None -> ResultOrString.Error "No symbol information found" @@ -592,12 +592,12 @@ type ParseAndCheckResults | _ -> ResultOrString.Error "Not a member, function or value" member __.TryGetF1Help (pos: Position) (lineStr: LineStr) = - match Lexer.findLongIdents (pos.Column, lineStr) with + match Lexer.findLongIdents (uint32 pos.Column, lineStr) with | None -> ResultOrString.Error "No ident at this location" | Some(colu, identIsland) -> let identIsland = Array.toList identIsland - let help = checkResults.GetF1Keyword(pos.Line, colu, lineStr, identIsland) + let help = checkResults.GetF1Keyword(pos.Line, int colu, lineStr, identIsland) match help with | None -> ResultOrString.Error "No symbol information found" @@ -636,7 +636,7 @@ type ParseAndCheckResults && not (PrettyNaming.IsOperatorDisplayName entity.Symbol.DisplayName)) let token = - Lexer.getSymbol pos.Line (pos.Column - 1) lineStr SymbolLookupKind.ForCompletion [||] + Lexer.getSymbol (uint32 pos.Line) (uint32 pos.Column - 1u) lineStr SymbolLookupKind.ForCompletion [||] logger.info ( Log.setMessage "TryGetCompletions - token: {token}" diff --git a/src/FsAutoComplete.Core/SignatureHelp.fs b/src/FsAutoComplete.Core/SignatureHelp.fs index e7067263e..439113a97 100644 --- a/src/FsAutoComplete.Core/SignatureHelp.fs +++ b/src/FsAutoComplete.Core/SignatureHelp.fs @@ -20,7 +20,7 @@ type SignatureHelpInfo = /// all potential overloads of the member at the position where signature help was invoked Methods: MethodGroupItem[] /// if present, the index of the method we think is the current one (will never be outside the bounds of the Methods array) - ActiveOverload: int option + ActiveOverload: uint32 option /// if present, the index of the parameter on the active method (will never be outside the bounds of the Parameters array on the selected method) ActiveParameter: uint option SigHelpKind: SignatureHelpKind @@ -42,11 +42,14 @@ let private getSignatureHelpForFunctionApplication } let! possibleApplicationSymbolLineStr = lines.GetLine possibleApplicationSymbolEnd - let! (endCol, names) = Lexer.findLongIdents (possibleApplicationSymbolEnd.Column, possibleApplicationSymbolLineStr) + + let! (endCol, names) = + Lexer.findLongIdents (uint32 possibleApplicationSymbolEnd.Column, possibleApplicationSymbolLineStr) + let idents = List.ofArray names let! symbolUse = - tyRes.GetCheckResults.GetSymbolUseAtLocation(possibleApplicationSymbolEnd.Line, endCol, lineStr, idents) + tyRes.GetCheckResults.GetSymbolUseAtLocation(possibleApplicationSymbolEnd.Line, int endCol, lineStr, idents) let isValid (mfv: FSharpMemberOrFunctionOrValue) = not (PrettyNaming.IsOperatorDisplayName mfv.DisplayName) @@ -58,7 +61,7 @@ let private getSignatureHelpForFunctionApplication let tooltip = tyRes.GetCheckResults.GetToolTip( possibleApplicationSymbolEnd.Line, - endCol, + int endCol, possibleApplicationSymbolLineStr, idents, FSharpTokenTag.IDENT @@ -195,6 +198,7 @@ let private getSignatureHelpForMethod let methodCandidate = filteredMethods |> Array.tryFindIndex (fun m -> m.Parameters.Length >= argumentIndex + 1) + |> Option.map uint32 return diff --git a/src/FsAutoComplete/CodeFixes.fs b/src/FsAutoComplete/CodeFixes.fs index b6469a651..9392e6078 100644 --- a/src/FsAutoComplete/CodeFixes.fs +++ b/src/FsAutoComplete/CodeFixes.fs @@ -14,7 +14,8 @@ module FcsRange = FSharp.Compiler.Text.Range type FcsRange = FSharp.Compiler.Text.Range type FcsPos = FSharp.Compiler.Text.Position -module LspTypes = Ionide.LanguageServerProtocol.Types +type LspRange = Ionide.LanguageServerProtocol.Types.Range +type LspPosition = Ionide.LanguageServerProtocol.Types.Position module Types = open FsAutoComplete.FCSPatches @@ -23,9 +24,9 @@ module Types = type IsEnabled = unit -> bool - type GetRangeText = string -> LspTypes.Range -> Async> + type GetRangeText = string -> LspRange -> Async> type GetFileLines = string -> Async> - type GetLineText = IFSACSourceText -> LspTypes.Range -> Async> + type GetLineText = IFSACSourceText -> LspRange -> Async> type GetParseResultsForFile = string @@ -84,7 +85,7 @@ module Types = { TextDocument = { Uri = fileUri.Uri Version = fileVersion } - Edits = edits } + Edits = edits |> Array.map U2.C1 } let workspaceEdit = WorkspaceEdit.Create([| edit |], clientCapabilities) @@ -136,30 +137,29 @@ module SourceText = module WithEmptyHandling = let getLineCount (sourceText: ISourceText) = match sourceText.GetLineCount() with - | 0 -> 1 - | c -> c + | 0 -> 1u + | c -> uint32 c // or // max 1 (sourceText.GetLineCount()) - let inline private assertLineIndex lineIndex sourceText = - assert (0 <= lineIndex && lineIndex < getLineCount sourceText) + let inline private assertLineIndex (lineIndex: uint32) sourceText = assert (lineIndex < getLineCount sourceText) let getLineString lineIndex (sourceText: ISourceText) = assertLineIndex lineIndex sourceText - if lineIndex = 0 && sourceText.GetLineCount() = 0 then + if lineIndex = 0u && sourceText.GetLineCount() = 0 then "" else - sourceText.GetLineString lineIndex + sourceText.GetLineString(int lineIndex) - let isFirstLine lineIndex (sourceText: ISourceText) = + let isFirstLine (lineIndex: uint32) (sourceText: ISourceText) = assertLineIndex lineIndex sourceText // No need to check for inside `getLineCount`: there's always at least one line (might be empty) - lineIndex = 0 + lineIndex = 0u let isLastLine lineIndex (sourceText: ISourceText) = assertLineIndex lineIndex sourceText - lineIndex = (getLineCount sourceText) - 1 + lineIndex = (getLineCount sourceText) - 1u /// Returns position after last character in specified line. /// Same as line length. @@ -176,36 +176,36 @@ module SourceText = let afterLastCharacterPosition lineIndex (sourceText: ISourceText) = assertLineIndex lineIndex sourceText let line = sourceText |> getLineString lineIndex - line.Length + uint32 line.Length /// helpers for iterating along text lines module Navigation = - let findPosForCharacter (lines: string[]) (pos: int) = - let mutable lineNumber = 0 - let mutable runningLength = 0 + let findPosForCharacter (lines: string[]) (pos: uint32) = + let mutable lineNumber = 0u + let mutable runningLength = 0u let mutable found = false let mutable fcsPos = Unchecked.defaultof while not found do - let line = lines.[lineNumber] - let lineLength = line.Length + let line = lines.[int lineNumber] + let lineLength = uint32 line.Length if pos <= runningLength + lineLength then let column = pos - runningLength found <- true - fcsPos <- FSharp.Compiler.Text.Position.mkPos lineNumber column + fcsPos <- FSharp.Compiler.Text.Position.mkPos (int lineNumber) (int column) else - lineNumber <- lineNumber + 1 + lineNumber <- lineNumber + 1u runningLength <- runningLength + lineLength fcsPos - let inc (lines: IFSACSourceText) (pos: LspTypes.Position) : LspTypes.Position option = + let inc (lines: IFSACSourceText) (pos: LspPosition) : LspPosition option = lines.NextPos(protocolPosToPos pos) |> Option.map fcsPosToLsp - let dec (lines: IFSACSourceText) (pos: LspTypes.Position) : LspTypes.Position option = + let dec (lines: IFSACSourceText) (pos: LspPosition) : LspPosition option = lines.PrevPos(protocolPosToPos pos) |> Option.map fcsPosToLsp let rec decMany lines pos count = @@ -213,10 +213,10 @@ module Navigation = let mutable pos = pos let mutable count = count - while count > 0 do + while count > 0u do let! nextPos = dec lines pos pos <- nextPos - count <- count - 1 + count <- count - 1u return pos } @@ -226,20 +226,20 @@ module Navigation = let mutable pos = pos let mutable count = count - while count > 0 do + while count > 0u do let! nextPos = inc lines pos pos <- nextPos - count <- count - 1 + count <- count - 1u return pos } - let walkBackUntilConditionWithTerminal (lines: IFSACSourceText) pos condition terminal = + let walkBackUntilConditionWithTerminal (lines: IFSACSourceText) (pos: LspPosition) condition terminal = let fcsStartPos = protocolPosToPos pos lines.WalkBackwards(fcsStartPos, terminal, condition) |> Option.map fcsPosToLsp - let walkForwardUntilConditionWithTerminal (lines: IFSACSourceText) pos condition terminal = + let walkForwardUntilConditionWithTerminal (lines: IFSACSourceText) (pos: LspPosition) condition terminal = let fcsStartPos = protocolPosToPos pos @@ -254,13 +254,13 @@ module Navigation = /// Tries to detect the last cursor position in line before `currentLine` (0-based). /// /// Returns `None` iff there's no prev line -> `currentLine` is first line - let tryEndOfPrevLine (lines: IFSACSourceText) currentLine = + let tryEndOfPrevLine (lines: IFSACSourceText) (currentLine: uint32) = if SourceText.WithEmptyHandling.isFirstLine currentLine lines then None else - let prevLine = currentLine - 1 + let prevLine = currentLine - 1u - { Line = prevLine + { Line = uint32 prevLine Character = lines |> SourceText.WithEmptyHandling.afterLastCharacterPosition prevLine } |> Some @@ -271,8 +271,11 @@ module Navigation = if SourceText.WithEmptyHandling.isLastLine currentLine lines then None else - let nextLine = currentLine + 1 - { Line = nextLine; Character = 0 } |> Some + let nextLine = currentLine + 1u + + { Line = uint32 nextLine + Character = 0u } + |> Some /// Gets the range to delete the complete line `lineIndex` (0-based). /// Deleting the line includes a linebreak if possible @@ -293,12 +296,12 @@ module Navigation = match tryStartOfNextLine lines lineIndex with | Some fin -> // delete trailing linebreak - { Start = { Line = lineIndex; Character = 0 } + { Start = { Line = lineIndex; Character = 0u } End = fin } | None -> // single line // -> just delete all text in line - { Start = { Line = lineIndex; Character = 0 } + { Start = { Line = lineIndex; Character = 0u } End = { Line = lineIndex Character = lines |> SourceText.WithEmptyHandling.afterLastCharacterPosition lineIndex } } @@ -339,7 +342,12 @@ module Run = handler let ifDiagnosticByCode codes handler : CodeFix = - runDiagnostics (fun d -> d.Code.IsSome && Set.contains d.Code.Value codes) handler + runDiagnostics + (fun d -> + match d.CodeAsString with + | Some c -> Set.contains c codes + | None -> false) + handler let ifImplementationFileBackedBySignature (getProjectOptionsForFile: GetProjectOptionsForFile) diff --git a/src/FsAutoComplete/CodeFixes.fsi b/src/FsAutoComplete/CodeFixes.fsi index a3d58f49d..8d5dc8f5e 100644 --- a/src/FsAutoComplete/CodeFixes.fsi +++ b/src/FsAutoComplete/CodeFixes.fsi @@ -12,13 +12,13 @@ open FSharp.Compiler.CodeAnalysis.ProjectSnapshot module FcsRange = FSharp.Compiler.Text.Range type FcsRange = FSharp.Compiler.Text.Range type FcsPos = FSharp.Compiler.Text.Position -module LspTypes = Ionide.LanguageServerProtocol.Types + module Types = type IsEnabled = unit -> bool - type GetRangeText = string -> LspTypes.Range -> Async> + type GetRangeText = string -> Ionide.LanguageServerProtocol.Types.Range -> Async> type GetFileLines = string -> Async> - type GetLineText = IFSACSourceText -> LspTypes.Range -> Async> + type GetLineText = IFSACSourceText -> Ionide.LanguageServerProtocol.Types.Range -> Async> type GetParseResultsForFile = string @@ -55,7 +55,7 @@ module Types = static member OfDiagnostic: fileUri: TextDocumentIdentifier -> - fileVersion: int option -> + fileVersion: int32 option -> title: string -> diagnostic: Diagnostic option -> edits: TextEdit array -> @@ -86,10 +86,10 @@ module SourceText = /// Note: There's always at least empty single line /// -> source MUST at least be empty (cannot not exist) module WithEmptyHandling = - val getLineCount: sourceText: ISourceText -> int - val getLineString: lineIndex: int -> sourceText: ISourceText -> string - val isFirstLine: lineIndex: int -> sourceText: ISourceText -> bool - val isLastLine: lineIndex: int -> sourceText: ISourceText -> bool + val getLineCount: sourceText: ISourceText -> uint32 + val getLineString: lineIndex: uint32 -> sourceText: ISourceText -> string + val isFirstLine: lineIndex: uint32 -> sourceText: ISourceText -> bool + val isLastLine: lineIndex: uint32 -> sourceText: ISourceText -> bool /// Returns position after last character in specified line. /// Same as line length. /// @@ -102,44 +102,72 @@ module SourceText = /// assert(afterLastCharacterPosition 2 text = 7) /// assert(afterLastCharacterPosition 2 text = 0) /// ``` - val afterLastCharacterPosition: lineIndex: int -> sourceText: ISourceText -> int + val afterLastCharacterPosition: lineIndex: uint32 -> sourceText: ISourceText -> uint32 /// helpers for iterating along text lines module Navigation = - val findPosForCharacter: lines: string[] -> pos: int -> FcsPos - val inc: lines: IFSACSourceText -> pos: LspTypes.Position -> LspTypes.Position option - val dec: lines: IFSACSourceText -> pos: LspTypes.Position -> LspTypes.Position option - val decMany: lines: IFSACSourceText -> pos: LspTypes.Position -> count: int -> LspTypes.Position option - val incMany: lines: IFSACSourceText -> pos: LspTypes.Position -> count: int -> LspTypes.Position option + val findPosForCharacter: lines: string[] -> pos: uint32 -> FcsPos + + val inc: + lines: IFSACSourceText -> + pos: Ionide.LanguageServerProtocol.Types.Position -> + Ionide.LanguageServerProtocol.Types.Position option + + val dec: + lines: IFSACSourceText -> + pos: Ionide.LanguageServerProtocol.Types.Position -> + Ionide.LanguageServerProtocol.Types.Position option + + val decMany: + lines: IFSACSourceText -> + pos: Ionide.LanguageServerProtocol.Types.Position -> + count: uint32 -> + Ionide.LanguageServerProtocol.Types.Position option + + val incMany: + lines: IFSACSourceText -> + pos: Ionide.LanguageServerProtocol.Types.Position -> + count: uint32 -> + Ionide.LanguageServerProtocol.Types.Position option val walkBackUntilConditionWithTerminal: lines: IFSACSourceText -> - pos: LspTypes.Position -> + pos: Ionide.LanguageServerProtocol.Types.Position -> condition: (char -> bool) -> terminal: (char -> bool) -> - LspTypes.Position option + Ionide.LanguageServerProtocol.Types.Position option val walkForwardUntilConditionWithTerminal: lines: IFSACSourceText -> - pos: LspTypes.Position -> + pos: Ionide.LanguageServerProtocol.Types.Position -> condition: (char -> bool) -> terminal: (char -> bool) -> - LspTypes.Position option + Ionide.LanguageServerProtocol.Types.Position option val walkBackUntilCondition: - lines: IFSACSourceText -> pos: LspTypes.Position -> condition: (char -> bool) -> LspTypes.Position option + lines: IFSACSourceText -> + pos: Ionide.LanguageServerProtocol.Types.Position -> + condition: (char -> bool) -> + Ionide.LanguageServerProtocol.Types.Position option val walkForwardUntilCondition: - lines: IFSACSourceText -> pos: LspTypes.Position -> condition: (char -> bool) -> LspTypes.Position option + lines: IFSACSourceText -> + pos: Ionide.LanguageServerProtocol.Types.Position -> + condition: (char -> bool) -> + Ionide.LanguageServerProtocol.Types.Position option /// Tries to detect the last cursor position in line before `currentLine` (0-based). /// /// Returns `None` iff there's no prev line -> `currentLine` is first line - val tryEndOfPrevLine: lines: IFSACSourceText -> currentLine: int -> LspTypes.Position option + val tryEndOfPrevLine: + lines: IFSACSourceText -> currentLine: uint32 -> Ionide.LanguageServerProtocol.Types.Position option + /// Tries to detect the first cursor position in line after `currentLine` (0-based). /// /// Returns `None` iff there's no next line -> `currentLine` is last line - val tryStartOfNextLine: lines: IFSACSourceText -> currentLine: int -> LspTypes.Position option + val tryStartOfNextLine: + lines: IFSACSourceText -> currentLine: uint32 -> Ionide.LanguageServerProtocol.Types.Position option + /// Gets the range to delete the complete line `lineIndex` (0-based). /// Deleting the line includes a linebreak if possible /// -> range starts either at end of previous line (-> includes leading linebreak) @@ -147,7 +175,7 @@ module Navigation = /// /// Special case: there's just one line /// -> delete text of (single) line - val rangeToDeleteFullLine: lineIndex: int -> lines: IFSACSourceText -> LspTypes.Range + val rangeToDeleteFullLine: lineIndex: uint32 -> lines: IFSACSourceText -> Ionide.LanguageServerProtocol.Types.Range module Run = open Types diff --git a/src/FsAutoComplete/CodeFixes/AddMissingFunKeyword.fs b/src/FsAutoComplete/CodeFixes/AddMissingFunKeyword.fs index 689bc8a9a..a904bb7ac 100644 --- a/src/FsAutoComplete/CodeFixes/AddMissingFunKeyword.fs +++ b/src/FsAutoComplete/CodeFixes/AddMissingFunKeyword.fs @@ -28,10 +28,10 @@ let fix (getFileLines: GetFileLines) (getLineText: GetLineText) : CodeFix = lines { Start = { diagnostic.Range.Start with - Character = 0 } + Character = 0u } End = { diagnostic.Range.End with - Character = lineLen } } + Character = uint32 lineLen } } let! prevPos = dec lines diagnostic.Range.Start @@ -45,9 +45,10 @@ let fix (getFileLines: GetFileLines) (getLineText: GetLineText) : CodeFix = | Some firstNonWhitespacePos -> let fcsPos = protocolPosToPos firstNonWhitespacePos - match Lexer.getSymbol fcsPos.Line fcsPos.Column line SymbolLookupKind.Fuzzy [||] with + match Lexer.getSymbol (uint32 fcsPos.Line) (uint32 fcsPos.Column) line SymbolLookupKind.Fuzzy [||] with | Some lexSym -> - let fcsStartPos = FSharp.Compiler.Text.Position.mkPos lexSym.Line lexSym.LeftColumn + let fcsStartPos = + FSharp.Compiler.Text.Position.mkPos (int lexSym.Line) (int lexSym.LeftColumn) let symbolStartRange = fcsPosToProtocolRange fcsStartPos diff --git a/src/FsAutoComplete/CodeFixes/AddMissingRecKeyword.fs b/src/FsAutoComplete/CodeFixes/AddMissingRecKeyword.fs index 0310e0846..2b967209b 100644 --- a/src/FsAutoComplete/CodeFixes/AddMissingRecKeyword.fs +++ b/src/FsAutoComplete/CodeFixes/AddMissingRecKeyword.fs @@ -42,18 +42,18 @@ let fix (getFileLines: GetFileLines) (getLineText: GetLineText) : CodeFix = let! line = getLineText lines - { Start = - { diagnostic.Range.Start with - Character = 0 } + { Start = diagnostic.Range.Start.StartOfLine() End = { diagnostic.Range.End with Character = lineLen } } - match Lexer.getSymbol fcsPos.Line fcsPos.Column line SymbolLookupKind.Fuzzy [||] with + match Lexer.getSymbol (uint32 fcsPos.Line) (uint32 fcsPos.Column) line SymbolLookupKind.Fuzzy [||] with | Some lexSym -> - let fcsStartPos = FSharp.Compiler.Text.Position.mkPos lexSym.Line lexSym.LeftColumn + let fcsStartPos = + FSharp.Compiler.Text.Position.mkPos (int lexSym.Line) (int lexSym.LeftColumn) - let fcsEndPos = FSharp.Compiler.Text.Position.mkPos lexSym.Line lexSym.RightColumn + let fcsEndPos = + FSharp.Compiler.Text.Position.mkPos (int lexSym.Line) (int lexSym.RightColumn) let protocolRange = fcsRangeToLsp (FSharp.Compiler.Text.Range.mkRange (UMX.untag fileName) fcsStartPos fcsEndPos) diff --git a/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs b/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs index 33f9b1e12..29abee8ab 100644 --- a/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs +++ b/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs @@ -26,11 +26,16 @@ let fix let! (tyRes, line, lines) = getParseResultsForFile fileName fcsRange.Start let! (endColumn, identIslands) = - Lexer.findLongIdents (fcsRange.Start.Column, line) + Lexer.findLongIdents (uint32 fcsRange.Start.Column, line) |> Result.ofOption (fun _ -> "No long ident at position") match - tyRes.GetCheckResults.GetDeclarationLocation(fcsRange.Start.Line, endColumn, line, List.ofArray identIslands) + tyRes.GetCheckResults.GetDeclarationLocation( + fcsRange.Start.Line, + int endColumn, + line, + List.ofArray identIslands + ) with | FindDeclResult.DeclFound declRange when declRange.FileName = UMX.untag fileName -> let! projectOptions = getProjectOptionsForFile fileName @@ -43,8 +48,8 @@ let fix let! declLexerSymbol = Lexer.getSymbol - declRange.Start.Line - declRange.Start.Column + (uint32 declRange.Start.Line) + (uint32 declRange.Start.Column) declText SymbolLookupKind.ByLongIdent (Array.ofList projectOptions.OtherOptions) diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs index d1858b2e3..dd628a799 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -82,6 +82,7 @@ type Int = type Offset = int + /// Range inside a **single** line inside a source text. /// /// Invariant: `Start.Line = End.Line` (-> `Range.inSingleLine`) @@ -120,23 +121,25 @@ type ORange = member inline r.ToRangeFrom(pos: Position) : Range = { Start = { Line = pos.Line - Character = pos.Character + r.Start } + Character = uint32 (int pos.Character + r.Start) } End = { Line = pos.Line - Character = pos.Character + r.End } } + Character = uint32 (int pos.Character + r.End) } } member inline r.ToRangeInside(range: Range) : Range = assert (Range.inSingleLine range) - assert (r.Length <= range.Length) + assert (r.Length <= int range.Length) r.ToRangeFrom(range.Start) member inline r.ShiftBy(d: Offset) = { Start = r.Start + d; End = r.End + d } + member inline r.ShiftBy(d: uint32) = r.ShiftBy(int d) + /// Note: doesn't care about `Line`, only `Character` member inline private r.ShiftToStartOf(pos: Position) : ORange = r.ShiftBy(pos.Character) member inline private r.ShiftInside(range: Range) : ORange = assert (Range.inSingleLine range) - assert (r.Length <= range.Length) + assert (r.Length <= int range.Length) r.ShiftToStartOf(range.Start) member inline r.SpanIn(str: String) = str.AsSpan(r.Start, r.Length) @@ -152,7 +155,7 @@ type ORange = /// Assumes: `range` is inside single line static member inline CoverAllOf(range: Range) = assert (Range.inSingleLine range) - { Start = 0; End = range.Length } + { Start = 0; End = int range.Length } static member inline CoverAllOf(text: ReadOnlySpan<_>) = { Start = 0; End = text.Length } @@ -177,11 +180,11 @@ module ORange = /// /// Note: Tuple instead of `ValueTuple` (`struct`) for better inlining. /// Check when used: Tuple should not actually be created! - let inline splitFront length (range: ORange) = + let inline splitFront (length: uint32) (range: ORange) = ({ range with - End = range.Start + length }, + End = range.Start + int length }, { range with - Start = range.Start + length }) + Start = range.Start + int length }) /// Split `range` after `length` counting from the back. /// @@ -214,12 +217,15 @@ module ORange = type Extensions() = /// Returns `-1` if no matching element [] - static member inline TryFindIndex(span: ReadOnlySpan<_>, [] f) = - let mutable idx = -1 + static member inline TryFindIndex(span: ReadOnlySpan<_>, [] f) : uint32 voption = + let mutable idx = ValueNone let mutable i = 0 - while idx < 0 && i < span.Length do - if f (span[i]) then idx <- i else i <- i + 1 + while idx = ValueNone && i < span.Length do + if f (span[i]) then + idx <- ValueSome(uint32 i) + else + i <- i + 1 idx @@ -239,10 +245,9 @@ module Parse = let text = range.SpanIn text let i = text.TryFindIndex(f) - if i < 0 then - range, range.EmptyAtEnd - else - range |> ORange.splitFront i + match i with + | ValueNone -> range, range.EmptyAtEnd + | ValueSome idx -> range |> ORange.splitFront idx let inline while' (text: ReadOnlySpan, range: ORange, [] f) = until (text, range, (fun c -> not (f c))) @@ -251,7 +256,7 @@ module Parse = let text = range.SpanIn text if text.IsEmpty then range.EmptyAtStart, range - elif f text[0] then range |> ORange.splitFront 1 + elif f text[0] then range |> ORange.splitFront 1u else range.EmptyAtStart, range /// Helper functions to splat tuples. With inlining: prevent tuple creation @@ -352,9 +357,9 @@ module Sign = if text.IsEmpty then Positive, range.EmptyAtStart, range elif text[0] = '-' then - Tuple.splatR Negative (range |> ORange.splitFront 1) + Tuple.splatR Negative (range |> ORange.splitFront 1u) elif text[0] = '+' then - Tuple.splatR Positive (range |> ORange.splitFront 1) + Tuple.splatR Positive (range |> ORange.splitFront 1u) else Positive, range.EmptyAtStart, range @@ -378,11 +383,11 @@ module Base = if text.Length > 2 && text[0] = '0' then match text[1] with | 'x' - | 'X' -> Tuple.splatR Base.Hexadecimal (range |> ORange.splitFront 2) + | 'X' -> Tuple.splatR Base.Hexadecimal (range |> ORange.splitFront 2u) | 'o' - | 'O' -> Tuple.splatR Base.Octal (range |> ORange.splitFront 2) + | 'O' -> Tuple.splatR Base.Octal (range |> ORange.splitFront 2u) | 'b' - | 'B' -> Tuple.splatR Base.Binary (range |> ORange.splitFront 2) + | 'B' -> Tuple.splatR Base.Binary (range |> ORange.splitFront 2u) | _ -> Base.Decimal, range.EmptyAtStart, range else Base.Decimal, range.EmptyAtStart, range @@ -700,8 +705,8 @@ module CommonFixes = if (replacement.StartsWith("-", StringComparison.Ordinal) || replacement.StartsWith("+", StringComparison.Ordinal)) - && range.Start.Character > 0 - && "!$%&*+-./<=>?@^|~".Contains(lineStr[range.Start.Character - 1]) + && range.Start.Character <> 0u + && "!$%&*+-./<=>?@^|~".Contains(lineStr[int (range.Start.Character - 1u)]) then " " + replacement else diff --git a/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fs b/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fs index 60f75d947..27cf2bf5a 100644 --- a/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fs +++ b/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fs @@ -29,10 +29,10 @@ let fix let! caseLine = lines.GetLine(nextLine) |> Result.ofOption (fun _ -> "No case line") - let caseCol = caseLine.IndexOf('|') + 3 // Find column of first case in pattern matching + let caseCol = uint32 (caseLine.IndexOf('|')) + 3u // Find column of first case in pattern matching let casePos = - { Line = nextLine.Line - 1 + { Line = uint32 nextLine.Line - 1u Character = caseCol } let casePosFCS = protocolPosToPos casePos diff --git a/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs b/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs index e7c5a866b..5540a5d82 100644 --- a/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs +++ b/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs @@ -22,7 +22,14 @@ let fix let! tyRes, line, _lines = getParseResultsForFile fileName fcsPos let! opts = getProjectOptionsForFile fileName - match Lexer.getSymbol fcsPos.Line fcsPos.Column line SymbolLookupKind.Fuzzy (Array.ofList opts.OtherOptions) with + match + Lexer.getSymbol + (uint32 fcsPos.Line) + (uint32 fcsPos.Column) + line + SymbolLookupKind.Fuzzy + (Array.ofList opts.OtherOptions) + with | Some _symbol -> match! tyRes.TryFindDeclaration fcsPos line with | FindDeclarationResult.Range declRange when declRange.FileName = (UMX.untag fileName) -> diff --git a/src/FsAutoComplete/CodeFixes/RemoveUnusedBinding.fs b/src/FsAutoComplete/CodeFixes/RemoveUnusedBinding.fs index cbf73146b..d72c35cf3 100644 --- a/src/FsAutoComplete/CodeFixes/RemoveUnusedBinding.fs +++ b/src/FsAutoComplete/CodeFixes/RemoveUnusedBinding.fs @@ -83,7 +83,7 @@ let fix (getParseResults: GetParseResultsForFile) : CodeFix = // walk back to the start of the keyword, which is always `let` or `use` let! keywordStartColumn = - decMany lines endOfPrecedingKeyword 3 + decMany lines endOfPrecedingKeyword 3u |> Result.ofOption (fun _ -> "failed to walk") let replacementRange = diff --git a/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs b/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs index a47b28620..209a8a7f3 100644 --- a/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs +++ b/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs @@ -45,17 +45,17 @@ let fix /// insert a line of text at a given line let insertLine line lineStr = { Range = - { Start = { Line = line; Character = 0 } - End = { Line = line; Character = 0 } } + { Start = { Line = line; Character = 0u } + End = { Line = line; Character = 0u } } NewText = lineStr } - let adjustInsertionPoint (lines: ISourceText) (ctx: InsertionContext) = - let l = ctx.Pos.Line + let adjustInsertionPoint (lines: ISourceText) (ctx: InsertionContext) : uint32 = + let l = uint32 ctx.Pos.Line let retVal = match ctx.ScopeKind with - | ScopeKind.TopModule when l > 1 -> - let line = lines.GetLineString(l - 2) + | ScopeKind.TopModule when l > 1u -> + let line = lines.GetLineString(int (l - 2u)) let isImplicitTopLevelModule = not ( @@ -63,14 +63,14 @@ let fix && not (line.EndsWith("=", StringComparison.Ordinal)) ) - if isImplicitTopLevelModule then 1 else l - | ScopeKind.TopModule -> 1 + if isImplicitTopLevelModule then 1u else l + | ScopeKind.TopModule -> 1u | ScopeKind.Namespace -> let mostRecentNamespaceInScope = - let lineNos = if l = 0 then [] else [ 0 .. l - 1 ] + let lineNos = if l = 0u then [] else [ 0u .. l - 1u ] lineNos - |> List.mapi (fun i line -> i, lines.GetLineString line) + |> List.mapi (fun i line -> uint32 i, lines.GetLineString(int line)) |> List.choose (fun (i, lineStr) -> if lineStr.StartsWith("namespace", StringComparison.Ordinal) then Some i @@ -80,15 +80,15 @@ let fix match mostRecentNamespaceInScope with // move to the next line below "namespace" and convert it to F# 1-based line number - | Some line -> line + 2 + | Some line -> line + 2u | None -> l | _ -> l let containsAttribute (x: string) = x.Contains "[<" - let currentLine = System.Math.Max(retVal - 2, 0) |> lines.GetLineString + let currentLine = max (int retVal - 2) 0 |> lines.GetLineString if currentLine |> containsAttribute then - retVal + 1 + retVal + 1u else retVal @@ -103,7 +103,7 @@ let fix let openFix (text: ISourceText) file diagnostic (word: string) (ns, name: string, ctx, _multiple) : Fix = let insertPoint = adjustInsertionPoint text ctx - let docLine = insertPoint - 1 + let docLine = insertPoint - 1u let actualOpen = if name.EndsWith(word, StringComparison.Ordinal) && name <> word then @@ -118,13 +118,13 @@ let fix let column = // HACK: This is a work around for inheriting the correct column of the current module // It seems the column we get from FCS is incorrect - let previousLine = docLine - 1 - let insertionPointIsNotOutOfBoundsOfTheFile = docLine > 0 + let previousLine = docLine - 1u + let insertionPointIsNotOutOfBoundsOfTheFile = docLine > 0u - let theThereAreOtherOpensInThisModule () = text.GetLineString(previousLine).Contains "open " + let theThereAreOtherOpensInThisModule () = text.GetLineString(int previousLine).Contains "open " if insertionPointIsNotOutOfBoundsOfTheFile && theThereAreOtherOpensInThisModule () then - text.GetLineString(previousLine).Split("open") |> Seq.head |> Seq.length // inherit the previous opens whitespace + text.GetLineString(int previousLine).Split("open") |> Seq.head |> Seq.length // inherit the previous opens whitespace else ctx.Pos.Column diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index ca71444db..717a4ae8b 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -22,40 +22,47 @@ module FcsPos = FSharp.Compiler.Text.Position module FcsPos = let subtractColumn (pos: FcsPos) (column: int) = FcsPos.mkPos pos.Line (pos.Column - column) +module Json = + let fromObject (obj: 'a) = + Newtonsoft.Json.Linq.JToken.FromObject(obj, Ionide.LanguageServerProtocol.Server.jsonRpcFormatter.JsonSerializer) + [] module Conversions = - module Lsp = Ionide.LanguageServerProtocol.Types + type LspPosition = Ionide.LanguageServerProtocol.Types.Position + type LspRange = Ionide.LanguageServerProtocol.Types.Range + type LspLocation = Ionide.LanguageServerProtocol.Types.Location + /// convert an LSP position to a compiler position - let protocolPosToPos (pos: Lsp.Position) : FcsPos = FcsPos.mkPos (pos.Line + 1) (pos.Character) + let protocolPosToPos (pos: LspPosition) : FcsPos = FcsPos.mkPos (int pos.Line + 1) (int pos.Character) - let protocolPosToRange (pos: Lsp.Position) : Lsp.Range = { Start = pos; End = pos } + let protocolPosToRange (pos: LspPosition) : LspRange = { Start = pos; End = pos } /// convert a compiler position to an LSP position - let fcsPosToLsp (pos: FcsPos) : Lsp.Position = - { Line = pos.Line - 1 - Character = pos.Column } + let fcsPosToLsp (pos: FcsPos) : LspPosition = + { Line = uint32 pos.Line - 1u + Character = uint32 pos.Column } /// convert a compiler range to an LSP range - let fcsRangeToLsp (range: FcsRange) : Lsp.Range = + let fcsRangeToLsp (range: FcsRange) : LspRange = { Start = fcsPosToLsp range.Start End = fcsPosToLsp range.End } - let protocolRangeToRange fn (range: Lsp.Range) : FcsRange = + let protocolRangeToRange fn (range: LspRange) : FcsRange = FcsRange.mkRange fn (protocolPosToPos range.Start) (protocolPosToPos range.End) /// convert an FCS position to a single-character range in LSP - let fcsPosToProtocolRange (pos: FcsPos) : Lsp.Range = + let fcsPosToProtocolRange (pos: FcsPos) : LspRange = { Start = fcsPosToLsp pos End = fcsPosToLsp pos } - let fcsRangeToLspLocation (range: FcsRange) : Lsp.Location = + let fcsRangeToLspLocation (range: FcsRange) : LspLocation = let fileUri = Path.FilePathToUri range.FileName let lspRange = fcsRangeToLsp range { Uri = fileUri; Range = lspRange } - let findDeclToLspLocation (decl: FsAutoComplete.FindDeclarationResult) : Lsp.Location = + let findDeclToLspLocation (decl: FsAutoComplete.FindDeclarationResult) : LspLocation = match decl with | FsAutoComplete.FindDeclarationResult.ExternalDeclaration ex -> let fileUri = Path.FilePathToUri ex.File @@ -68,8 +75,16 @@ module Conversions = { Uri = fileUri Range = - { Start = { Line = 0; Character = 0 } - End = { Line = 0; Character = 0 } } } + { Start = { Line = 0u; Character = 0u } + End = { Line = 0u; Character = 0u } } } + + type Diagnostic with + /// The diagnostic's code, which usually appear in the user interface. + member x.CodeAsString = + match x.Code with + | Some(U2.C1 code) -> code.ToString(System.Globalization.CultureInfo.InvariantCulture) |> Some + | Some(U2.C2 code) -> code |> Some + | None -> None type TextDocumentIdentifier with @@ -106,19 +121,19 @@ module Conversions = let fcsErrorToDiagnostic (error: FSharpDiagnostic) = { Range = { Start = - { Line = error.StartLine - 1 - Character = error.StartColumn } + { Line = uint32 (max 0 (error.StartLine - 1)) + Character = uint32 (max 0 error.StartColumn) } End = - { Line = error.EndLine - 1 - Character = error.EndColumn } } + { Line = uint32 (max 0 (error.EndLine - 1)) + Character = uint32 (max 0 error.EndColumn) } } Severity = fcsSeverityToDiagnostic error.Severity Source = Some "F# Compiler" Message = handleUnicodeParagraph error.Message - Code = Some(string error.ErrorNumber) + Code = Some(U2.C1 error.ErrorNumber) RelatedInformation = Some [||] Tags = None Data = None - CodeDescription = Some { Href = Some(Uri(urlForCompilerCode error.ErrorNumber)) } } + CodeDescription = Some { Href = urlForCompilerCode error.ErrorNumber } } let getSymbolInformations (uri: DocumentUri) @@ -166,7 +181,7 @@ module Conversions = let getCodeLensInformation (uri: DocumentUri) (typ: string) (topLevel: NavigationTopLevelDeclaration) : CodeLens[] = let map (decl: NavigationItem) : CodeLens = { Command = None - Data = Some(Newtonsoft.Json.Linq.JToken.FromObject [| uri; typ |]) + Data = Some(Json.fromObject [| uri; typ |]) Range = fcsRangeToLsp decl.Range } let shouldKeep (n: NavigationItem) = @@ -189,11 +204,11 @@ module Conversions = topLevel.Nested |> Array.Parallel.choose (fun n -> if shouldKeep n then Some(map n) else None) - let getLine (lines: string[]) (pos: Lsp.Position) = lines.[pos.Line] + let getLine (lines: string[]) (pos: LspPosition) = lines.[int pos.Line] - let getText (lines: string[]) (r: Lsp.Range) = - lines.[r.Start.Line] - .Substring(r.Start.Character, r.End.Character - r.Start.Character) + let getText (lines: string[]) (r: LspRange) = + lines.[int r.Start.Line] + .Substring(int r.Start.Character, int (r.End.Character - r.Start.Character)) [] module internal GlyphConversions = @@ -1137,8 +1152,8 @@ let encodeSemanticHighlightRanges ClassificationUtils.SemanticTokenModifier list)) array) = let fileStart = - { Start = { Line = 0; Character = 0 } - End = { Line = 0; Character = 0 } } + { Start = { Line = 0u; Character = 0u } + End = { Line = 0u; Character = 0u } } let computeLine (prev: Ionide.LanguageServerProtocol.Types.Range) @@ -1198,3 +1213,22 @@ let encodeSemanticHighlightRanges type FSharpInlayHintsRequest = { TextDocument: TextDocumentIdentifier Range: Range } + +[] +module Extensions = + type ReadOnlySpan<'t> with + member x.Slice(start: uint32, length: uint32) = x.Slice(int start, int length) + member x.get_Item(index: uint32) = x.Item(int index) + + type Ionide.LanguageServerProtocol.Types.Position with + member x.StartOfLine() = { Line = x.Line; Character = 0u } + + type String with + member x.AsSpan(start: uint32, length: uint32) = x.AsSpan(int start, int length) + + let inline getFilePathAndPosition<'t + when 't: (member TextDocument: TextDocumentIdentifier) + and 't: (member Position: Ionide.LanguageServerProtocol.Types.Position)> + (p: 't) + = + p.TextDocument.GetFilePath() |> Utils.normalizePath, protocolPosToPos p.Position diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index 04ab54066..25b0ce2e2 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -9,28 +9,31 @@ open System.Collections.Generic open Ionide.ProjInfo.ProjectSystem open FSharp.Compiler.Diagnostics open FSharp.Compiler.EditorServices +open FSharp.UMX type FcsRange = FSharp.Compiler.Text.Range module FcsRange = FSharp.Compiler.Text.Range type FcsPos = FSharp.Compiler.Text.Position module FcsPos = FSharp.Compiler.Text.Position -module Lsp = Ionide.LanguageServerProtocol.Types - module FcsPos = val subtractColumn: pos: FcsPos -> column: int -> FcsPos +module Json = + val fromObject: obj: 'a -> Newtonsoft.Json.Linq.JToken + [] module Conversions = - module Lsp = Ionide.LanguageServerProtocol.Types + type LspPosition = Ionide.LanguageServerProtocol.Types.Position + type LspRange = Ionide.LanguageServerProtocol.Types.Range /// convert an LSP position to a compiler position - val protocolPosToPos: pos: Lsp.Position -> FcsPos - val protocolPosToRange: pos: Lsp.Position -> Range + val protocolPosToPos: pos: LspPosition -> FcsPos + val protocolPosToRange: pos: LspPosition -> Range /// convert a compiler position to an LSP position val fcsPosToLsp: pos: FcsPos -> Position /// convert a compiler range to an LSP range val fcsRangeToLsp: range: FcsRange -> Range - val protocolRangeToRange: fn: string -> range: Lsp.Range -> FcsRange + val protocolRangeToRange: fn: string -> range: LspRange -> FcsRange /// convert an FCS position to a single-character range in LSP val fcsPosToProtocolRange: pos: FcsPos -> Range val fcsRangeToLspLocation: range: FcsRange -> Location @@ -39,6 +42,10 @@ module Conversions = [] val unicodeParagraphCharacter: string = "\u001d" + type Diagnostic with + + member CodeAsString: string option + type TextDocumentIdentifier with member GetFilePath: unit -> string @@ -72,8 +79,8 @@ module Conversions = val getCodeLensInformation: uri: DocumentUri -> typ: string -> topLevel: NavigationTopLevelDeclaration -> CodeLens array - val getLine: lines: string[] -> pos: Lsp.Position -> string - val getText: lines: string[] -> r: Lsp.Range -> string + val getLine: lines: string[] -> pos: LspPosition -> string + val getText: lines: string[] -> r: LspRange -> string [] module internal GlyphConversions = @@ -463,3 +470,21 @@ val encodeSemanticHighlightRanges: type FSharpInlayHintsRequest = { TextDocument: TextDocumentIdentifier Range: Range } + + +[] +module Extensions = + + type ReadOnlySpan<'t> with + member Slice: start: uint32 * length: uint32 -> ReadOnlySpan<'t> + member get_Item: index: uint32 -> 't + + type Ionide.LanguageServerProtocol.Types.Position with + member StartOfLine: unit -> Ionide.LanguageServerProtocol.Types.Position + + type String with + member AsSpan: start: uint32 * length: uint32 -> ReadOnlySpan + + val inline getFilePathAndPosition<'t + when 't: (member TextDocument: TextDocumentIdentifier) + and 't: (member Position: Ionide.LanguageServerProtocol.Types.Position)> : p: 't -> string * FcsPos diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 75c1d1431..554036c91 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -98,8 +98,6 @@ type AdaptiveFSharpLspServer let pos = p.GetFcsPos() filePath, pos - - member private x.handleSemanticTokens (filePath: string) range : AsyncLspResult = asyncResult { @@ -306,7 +304,7 @@ type AdaptiveFSharpLspServer // mix old and new, warn and mimic old behavior | Some p, Some _, _ | Some p, _, Some _ -> - let m = + let m: ShowMessageParams = { Type = MessageType.Warning Message = "Do not mix usage of FSIExtraParameters and (FSIExtraInteractiveParameters or FSIExtraSharedParameters)." } @@ -353,7 +351,7 @@ type AdaptiveFSharpLspServer let inlineValueToggle: InlineValueOptions option = match c.InlineValues.Enabled with - | Some true -> Some { ResolveProvider = Some false } + | Some true -> Some { WorkDoneProgress = None } | Some false -> None | None -> None @@ -392,12 +390,12 @@ type AdaptiveFSharpLspServer transact (fun () -> state.RootPath <- actualRootPath - state.ClientCapabilities <- p.Capabilities - lspClient.ClientCapabilities <- p.Capabilities + state.ClientCapabilities <- Some p.Capabilities + lspClient.ClientCapabilities <- Some p.Capabilities state.DiagnosticCollections.ClientSupportsDiagnostics <- match p.Capabilities with - | Some { TextDocument = Some { PublishDiagnostics = Some _ } } -> true + | { TextDocument = Some { PublishDiagnostics = Some _ } } -> true | _ -> false state.Config <- c @@ -408,13 +406,22 @@ type AdaptiveFSharpLspServer TextDocumentSync = Helpers.defaultServerCapabilities.TextDocumentSync |> Option.map (fun x -> - { x with - Change = Some TextDocumentSyncKind.Incremental }) - InlineValueProvider = inlineValueToggle } - - return - { InitializeResult.Default with - Capabilities = defaultSettings } + match x with + | U2.C1 x -> + U2.C1 + { x with + Change = Some TextDocumentSyncKind.Incremental } + | x -> x) + InlineValueProvider = inlineValueToggle |> Option.map U3.C2 } + + let response: Ionide.LanguageServerProtocol.Types.InitializeResult = + { Capabilities = defaultSettings + ServerInfo = + Some + { InitializeResultServerInfo.Name = "FsAutoComplete" + Version = Some <| FsAutoComplete.Utils.Version.info().Version } } + + return response with e -> trace |> Tracing.recordException e @@ -426,23 +433,19 @@ type AdaptiveFSharpLspServer return! returnException e logCfg } - override __.Initialized(p: InitializedParams) = + override __.Initialized() = async { - let tags = [ "InitializedParams", box p ] - use trace = fsacActivitySource.StartActivityForType(thisType, tags = tags) + use trace = fsacActivitySource.StartActivityForType(thisType) try - logger.info (Log.setMessage "Initialized request {p}" >> Log.addContextDestructured "p" p) + logger.info (Log.setMessage "Initialized request") let! _ = state.ParseAllFiles() return () with e -> trace |> Tracing.recordException e - logException - e - (Log.setMessage "Initialized Request Errored {p}" - >> Log.addContextDestructured "p" p) + logException e (Log.setMessage "Initialized Request Errored") return () } @@ -623,7 +626,7 @@ type AdaptiveFSharpLspServer do! match p.Context with | Some({ TriggerKind = CompletionTriggerKind.TriggerCharacter } as context) -> - volatileFile.Source.TryGetChar pos = context.TriggerCharacter + volatileFile.Source.TryGetChar pos |> Option.map string = context.TriggerCharacter | _ -> true |> Result.requireTrue $"TextDocumentCompletion was sent before TextDocumentDidChange" @@ -741,7 +744,11 @@ type AdaptiveFSharpLspServer let mapHelpText (ci: CompletionItem) (text: HelpText) = match text with | HelpText.Simple(symbolName, text) -> - let d = Documentation.Markup(markdown text) + let d: U2<_, MarkupContent> = + U2.C2( + { Kind = MarkupKind.Markdown + Value = text } + ) { ci with Detail = Some symbolName @@ -759,7 +766,7 @@ type AdaptiveFSharpLspServer let insertPos = { (fcsPos |> fcsPosToLsp) with - Character = 0 } + Character = 0u } let displayText = match config.ExternalAutocomplete, ci.Label.Split(" (open ") with @@ -772,7 +779,11 @@ type AdaptiveFSharpLspServer TextEdit.Range = { Start = insertPos; End = insertPos } } |], $"{displayText} (open {ns})" - let d = Documentation.Markup(markdown comment) + let d = + U2.C2( + { Kind = MarkupKind.Markdown + Value = comment } + ) { ci with Detail = Some si @@ -862,7 +873,8 @@ type AdaptiveFSharpLspServer - let charAtCaret = p.Context |> Option.bind (fun c -> c.TriggerCharacter) + let charAtCaret = + p.Context |> Option.bind (fun c -> c.TriggerCharacter) |> Option.map char match! SignatureHelp.getSignatureHelpFor (tyRes, pos, volatileFile.Source, charAtCaret, None) @@ -882,10 +894,14 @@ type AdaptiveFSharpLspServer let parameters = m.Parameters |> Array.map (fun p -> - { ParameterInformation.Label = U2.First p.ParameterName - Documentation = Some(Documentation.String p.CanonicalTypeTextForSorting) }) + { ParameterInformation.Label = U2.C1 p.ParameterName + Documentation = Some(U2.C1 p.CanonicalTypeTextForSorting) }) - let d = Documentation.Markup(markdown comment) + let d = + U2.C2( + { Kind = MarkupKind.Markdown + Value = comment } + ) { SignatureInformation.Label = signature Documentation = Some d @@ -944,42 +960,35 @@ type AdaptiveFSharpLspServer match TipFormatter.tryFormatTipEnhanced tooltipResult.ToolTipText formatCommentStyle with | TipFormatter.TipFormatterResult.Success tooltipInfo -> - // Display the signature as a code block - let signature = - tooltipResult.Signature - |> TipFormatter.prepareSignature - |> (fun content -> MarkedString.WithLanguage { Language = "fsharp"; Value = content }) - - // Display each footer line as a separate line - let footerLines = - tooltipResult.Footer - |> TipFormatter.prepareFooterLines - |> Array.map MarkedString.String - - let contents = - [| signature - MarkedString.String tooltipInfo.DocComment - match tooltipResult.SymbolInfo with - | TryGetToolTipEnhancedResult.Keyword _ -> () - | TryGetToolTipEnhancedResult.Symbol symbolInfo -> - TipFormatter.renderShowDocumentationLink - tooltipInfo.HasTruncatedExamples - symbolInfo.XmlDocSig - symbolInfo.Assembly - |> MarkedString.String - yield! footerLines |] - let response = - { Contents = MarkedStrings contents + { Contents = + U3.C3 + [| + // Display the signature as a code block + tooltipResult.Signature + |> TipFormatter.prepareSignature + |> (fun content -> U2.C2 { Language = "fsharp"; Value = content }) + U2.C1 tooltipInfo.DocComment + match tooltipResult.SymbolInfo with + | TryGetToolTipEnhancedResult.Keyword _ -> () + | TryGetToolTipEnhancedResult.Symbol symbolInfo -> + TipFormatter.renderShowDocumentationLink + tooltipInfo.HasTruncatedExamples + symbolInfo.XmlDocSig + symbolInfo.Assembly + |> U2.C1 + // Display each footer line as a separate line + yield! tooltipResult.Footer |> TipFormatter.prepareFooterLines |> Array.map U2.C1 |] + Range = None } return (Some response) | TipFormatter.TipFormatterResult.Error error -> - let contents = [| MarkedString.String ""; MarkedString.String error |] + let contents = [| U2.C1 ""; U2.C1 error |] let response = - { Contents = MarkedStrings contents + { Contents = U3.C3 contents Range = None } return (Some response) @@ -1014,7 +1023,7 @@ type AdaptiveFSharpLspServer Commands.renameSymbolRange state.GetDeclarationLocation false pos lineStr volatileFile.Source tyRes |> AsyncResult.mapError (fun msg -> JsonRpc.Error.Create(JsonRpc.ErrorCodes.invalidParams, msg)) - return range |> fcsRangeToLsp |> PrepareRenameResult.Range |> Some + return range |> fcsRangeToLsp |> U3.C1 |> Some } override x.TextDocumentRename(p: RenameParams) = @@ -1028,7 +1037,7 @@ type AdaptiveFSharpLspServer >> Log.addContextDestructured "params" p ) - let (filePath, pos) = getFilePathAndPosition p + let (filePath, pos) = LspHelpers.Extensions.getFilePathAndPosition p let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr @@ -1055,7 +1064,7 @@ type AdaptiveFSharpLspServer kvp.Value |> Array.map (fun range -> let range = fcsRangeToLsp range - { Range = range; NewText = newName }) + U2.C1 { Range = range; NewText = newName }) let file: string = kvp.Key @@ -1105,7 +1114,7 @@ type AdaptiveFSharpLspServer let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr let! decl = tyRes.TryFindDeclaration pos lineStr |> AsyncResult.ofStringErr - return decl |> findDeclToLspLocation |> GotoResult.Single |> Some + return decl |> findDeclToLspLocation |> U2.C1 |> Some with e -> trace |> Tracing.recordException e @@ -1133,7 +1142,7 @@ type AdaptiveFSharpLspServer let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr let! decl = tyRes.TryFindTypeDeclaration pos lineStr |> AsyncResult.ofStringErr - return decl |> findDeclToLspLocation |> GotoResult.Single |> Some + return decl |> findDeclToLspLocation |> U2.C1 |> Some with e -> trace |> Tracing.recordException e @@ -1266,8 +1275,8 @@ type AdaptiveFSharpLspServer match mappedRanges with | [||] -> return None - | [| single |] -> return Some(GotoResult.Single single) - | multiple -> return Some(GotoResult.Multiple multiple) + | [| single |] -> return Some(U2.C1 single) + | multiple -> return Some(U2.C2 multiple) with e -> trace |> Tracing.recordException e @@ -1296,7 +1305,7 @@ type AdaptiveFSharpLspServer decls |> Array.collect (fun top -> getSymbolInformations p.TextDocument.Uri state.GlyphToSymbolKind top (fun _s -> true)) - |> U2.First + |> U2.C1 |> Some with e -> trace |> Tracing.recordException e @@ -1333,7 +1342,7 @@ type AdaptiveFSharpLspServer ns |> Array.collect (fun n -> getSymbolInformations uri glyphToSymbolKind n (applyQuery symbolRequest.Query))) - |> U2.First + |> U2.C1 |> Some return res @@ -1368,7 +1377,7 @@ type AdaptiveFSharpLspServer let handlerFormattedDoc (sourceText: IFSACSourceText, formatted: string) = let range = - let zero = { Line = 0; Character = 0 } + let zero = { Line = 0u; Character = 0u } let lastPos = sourceText.LastFilePosition { Start = zero @@ -1405,10 +1414,10 @@ type AdaptiveFSharpLspServer let range = FormatSelectionRange( - p.Range.Start.Line + 1, - p.Range.Start.Character, - p.Range.End.Line + 1, - p.Range.End.Character + int p.Range.Start.Line + 1, + int p.Range.Start.Character, + int p.Range.End.Line + 1, + int p.Range.End.Character ) let tryGetFileCheckerOptionsWithLines file = state.GetOpenFileSource file @@ -1419,11 +1428,11 @@ type AdaptiveFSharpLspServer let handlerFormattedRangeDoc (_sourceText: IFSACSourceText, formatted: string, range: FormatSelectionRange) = let range = { Start = - { Line = range.StartLine - 1 - Character = range.StartColumn } + { Line = uint32 range.StartLine - 1u + Character = uint32 range.StartColumn } End = - { Line = range.EndLine - 1 - Character = range.EndColumn } } + { Line = uint32 range.EndLine - 1u + Character = uint32 range.EndColumn } } [| { Range = range; NewText = formatted } |] @@ -1505,7 +1514,7 @@ type AdaptiveFSharpLspServer |> List.map (CodeAction.OfFix tryGetFileVersion clientCapabilities) |> Async.parallel75 - return Some(fixes |> Array.map U2.Second) + return Some(fixes |> Array.map U2.C2) with e -> trace |> Tracing.recordException e @@ -1539,7 +1548,10 @@ type AdaptiveFSharpLspServer if config.CodeLenses.References.Enabled then yield! decls |> Array.collect (getCodeLensInformation p.TextDocument.Uri "reference") |] - return Some res + match res with + | [||] -> return None + | res -> return Some res + with e -> trace |> Tracing.recordException e @@ -1551,7 +1563,6 @@ type AdaptiveFSharpLspServer } override __.CodeLensResolve(p: CodeLens) = - // JB:TODO see how to reuse existing code logger.info ( Log.setMessage "CodeLensResolve Request: {params}" >> Log.addContextDestructured "params" p @@ -1619,9 +1630,9 @@ type AdaptiveFSharpLspServer let writePayload (sourceFile: string, triggerPos: pos, usageLocations: range[]) = Some - [| JToken.FromObject(Path.LocalPathToUri sourceFile) - JToken.FromObject(fcsPosToLsp triggerPos) - JToken.FromObject(usageLocations |> Array.map fcsRangeToLspLocation) |] + [| Json.fromObject (Path.LocalPathToUri sourceFile) + Json.fromObject (fcsPosToLsp triggerPos) + Json.fromObject (usageLocations |> Array.map fcsRangeToLspLocation) |] handler (fun p pos tyRes sourceText lineStr typ file -> @@ -1922,7 +1933,7 @@ type AdaptiveFSharpLspServer let disableLongTooltip = "fsharp.inlayHints.disableLongTooltip" if config.InlayHints.disableLongTooltip then - h.Tooltip |> Option.map (InlayHintTooltip.String) + h.Tooltip |> Option.map U2.C1 else let lines = ResizeArray() @@ -1946,13 +1957,18 @@ type AdaptiveFSharpLspServer lines.Add "" lines.Add(t)) - String.concat "\n" lines |> markdown |> InlayHintTooltip.Markup |> Some + String.concat "\n" lines + |> fun line -> + { Kind = MarkupKind.Markdown + Value = line } + |> U2.C2 + |> Some let hints: InlayHint[] = hints |> Array.map (fun h -> { Position = fcsPosToLsp h.Pos - Label = InlayHintLabel.String h.Text + Label = U2.C1 h.Text Kind = match h.Kind with | InlayHints.HintKind.Type -> Types.InlayHintKind.Type @@ -2013,7 +2029,7 @@ type AdaptiveFSharpLspServer |> Array.map (fun (pos, lineHints) -> { InlineValueText.Range = fcsPosToProtocolRange pos Text = lineHints } - |> InlineValue.InlineValueText) + |> U3.C1) |> Some return hints @@ -2191,13 +2207,13 @@ type AdaptiveFSharpLspServer let! decl = tyRes.TryFindDeclaration pos lineStr |> AsyncResult.ofStringErr let! lexedResult = - Lexer.getSymbol pos.Line pos.Column lineStr SymbolLookupKind.Fuzzy [||] + Lexer.getSymbol (uint32 pos.Line) (uint32 pos.Column) lineStr SymbolLookupKind.Fuzzy [||] |> Result.ofOption (fun () -> "No symbol found") |> Result.ofStringErr let location = findDeclToLspLocation decl - let returnValue = + let returnValue: CallHierarchyItem[] = [| { Name = lexedResult.Text Kind = SymbolKind.Function Tags = None @@ -2294,7 +2310,7 @@ type AdaptiveFSharpLspServer ) let pos = - FSharp.Compiler.Text.Position.mkPos (p.Position.Line) (p.Position.Character + 2) + FSharp.Compiler.Text.Position.mkPos (int p.Position.Line) (int p.Position.Character + 2) let filePath = p.TextDocument.GetFilePath() |> Utils.normalizePath let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr @@ -2344,16 +2360,19 @@ type AdaptiveFSharpLspServer InsertText = text } -> let edit: ApplyWorkspaceEditParams = + let doc: OptionalVersionedTextDocumentIdentifier = + { Uri = p.TextDocument.Uri + Version = Some p.TextDocument.Version } + + let edits = + [| U2.C1 + { Range = fcsPosToProtocolRange insertPos + NewText = text } |] + { Label = Some "Generate Xml Documentation" Edit = - { DocumentChanges = - Some - [| { TextDocument = - { Uri = p.TextDocument.Uri - Version = Some p.TextDocument.Version } - Edits = - [| { Range = fcsPosToProtocolRange insertPos - NewText = text } |] } |] + { DocumentChanges = Some [| U4.C1({ TextDocument = doc; Edits = edits }: TextDocumentEdit) |] + ChangeAnnotations = None Changes = None } } let! _ = lspClient.WorkspaceApplyEdit edit @@ -3047,10 +3066,10 @@ type AdaptiveFSharpLspServer try logger.info ( Log.setMessage "WorkDoneProgressCancel Request: {params}" - >> Log.addContextDestructured "params" param.token + >> Log.addContextDestructured "params" param.Token ) - state.CancelServerProgress param.token + state.CancelServerProgress param.Token with e -> trace |> Tracing.recordException e @@ -3058,7 +3077,7 @@ type AdaptiveFSharpLspServer logException e (Log.setMessage "WorkDoneProgressCancel Request Errored {p}" - >> Log.addContextDestructured "token" param.token) + >> Log.addContextDestructured "token" param.Token) return () } diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index b4c53b35f..0d581c916 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -600,7 +600,7 @@ type AdaptiveState opens |> Array.map (fun n -> { Range = fcsRangeToLsp n - Code = Some "FSAC0001" + Code = Some(U2.C2 "FSAC0001") Severity = Some DiagnosticSeverity.Hint Source = Some "FSAC" Message = "Unused open statement" @@ -618,7 +618,7 @@ type AdaptiveState decls |> Array.map (fun n -> { Range = fcsRangeToLsp n - Code = Some "FSAC0003" + Code = Some(U2.C2 "FSAC0003") Severity = Some DiagnosticSeverity.Hint Source = Some "FSAC" Message = "This value is unused" @@ -640,7 +640,7 @@ type AdaptiveState ({ Range = range RelativeName = _relName }) -> { Diagnostic.Range = fcsRangeToLsp range - Code = Some "FSAC0002" + Code = Some(U2.C2 "FSAC0002") Severity = Some DiagnosticSeverity.Hint Source = Some "FSAC" Message = "This qualifier is redundant" @@ -658,7 +658,7 @@ type AdaptiveState ranges |> Array.map (fun range -> { Diagnostic.Range = fcsRangeToLsp range - Code = Some "FSAC0004" + Code = Some(U2.C2 "FSAC0004") Severity = Some DiagnosticSeverity.Hint Source = Some "FSAC" Message = "Parentheses can be removed" @@ -742,7 +742,7 @@ type AdaptiveState |> Some { Range = range - Code = Option.ofObj m.Code + Code = Option.ofObj m.Code |> Option.map U2.C2 Severity = Some severity Source = Some $"F# Analyzers (%s{m.Type})" Message = m.Message @@ -1028,12 +1028,12 @@ type AdaptiveState let file = (file, changes) ||> Seq.fold (fun text (change, version, touched) -> - match change.Range with - | None -> // replace entire content + match change with + | U2.C2 change -> // replace entire content VolatileFile.Create(sourceTextFactory.Create(filePath, change.Text), version, touched) - | Some rangeToReplace -> + | U2.C1 change -> // replace just this slice - let fcsRangeToReplace = protocolRangeToRange (UMX.untag filePath) rangeToReplace + let fcsRangeToReplace = protocolRangeToRange (UMX.untag filePath) change.Range try match text.Source.ModifyText(fcsRangeToReplace, change.Text) with @@ -1894,7 +1894,7 @@ type AdaptiveState try let! (text) = forceFindOpenFileOrRead file |> Async.map Option.ofResult let! line = tryGetLineStr pos text.Source |> Option.ofResult - return! Lexer.getSymbol pos.Line pos.Column line SymbolLookupKind.Fuzzy [||] + return! Lexer.getSymbol (uint32 pos.Line) (uint32 pos.Column) line SymbolLookupKind.Fuzzy [||] with _ -> return! None } diff --git a/src/FsAutoComplete/LspServers/Common.fs b/src/FsAutoComplete/LspServers/Common.fs index bcbc95eb9..cc96f3cba 100644 --- a/src/FsAutoComplete/LspServers/Common.fs +++ b/src/FsAutoComplete/LspServers/Common.fs @@ -219,47 +219,75 @@ module Helpers = let defaultServerCapabilities = { ServerCapabilities.Default with - HoverProvider = Some true - RenameProvider = Some(U2.Second { PrepareProvider = Some true }) - DefinitionProvider = Some true - TypeDefinitionProvider = Some true - ImplementationProvider = Some true - ReferencesProvider = Some true - DocumentHighlightProvider = Some true - DocumentSymbolProvider = Some(U2.Second { Label = Some "F#" }) - WorkspaceSymbolProvider = Some(U2.Second { ResolveProvider = Some true }) - DocumentFormattingProvider = Some true - DocumentRangeFormattingProvider = Some true + HoverProvider = Some(U2.C1 true) + RenameProvider = + Some( + U2.C2 + { PrepareProvider = Some true + WorkDoneProgress = Some false } + ) + DefinitionProvider = Some(U2.C1 true) + TypeDefinitionProvider = Some(U3.C1 true) + ImplementationProvider = Some(U3.C1 true) + ReferencesProvider = Some(U2.C1 true) + DocumentHighlightProvider = Some(U2.C1 true) + DocumentSymbolProvider = + Some( + U2.C2 + { Label = Some "F#" + WorkDoneProgress = Some false } + ) + WorkspaceSymbolProvider = + Some( + U2.C2 + { ResolveProvider = Some true + WorkDoneProgress = Some false } + ) + DocumentFormattingProvider = Some(U2.C1 true) + DocumentRangeFormattingProvider = Some(U2.C1 true) SignatureHelpProvider = Some - { TriggerCharacters = Some [| '('; ','; ' ' |] - RetriggerCharacters = Some [| ','; ')'; ' ' |] } + { TriggerCharacters = Some [| "("; ","; " " |] + RetriggerCharacters = Some [| ","; ")"; " " |] + WorkDoneProgress = Some false } CompletionProvider = Some { ResolveProvider = Some true - TriggerCharacters = Some([| '.'; ''' |]) + TriggerCharacters = Some([| "."; "'" |]) AllCommitCharacters = None //TODO: what chars should commit completions? - CompletionItem = None } - CodeLensProvider = Some { CodeLensOptions.ResolveProvider = Some true } + CompletionItem = None + WorkDoneProgress = Some false } + CodeLensProvider = + Some + { CodeLensOptions.ResolveProvider = Some true + WorkDoneProgress = Some false } CodeActionProvider = Some( - U2.Second + U2.C2 { CodeActionKinds = None - ResolveProvider = None } + ResolveProvider = None + WorkDoneProgress = Some false } ) TextDocumentSync = Some + <| U2.C1 { TextDocumentSyncOptions.Default with OpenClose = Some true Change = Some TextDocumentSyncKind.Incremental - Save = Some { IncludeText = Some true } } - FoldingRangeProvider = Some true - SelectionRangeProvider = Some true - CallHierarchyProvider = Some true + Save = Some <| U2.C2 { IncludeText = Some true } } + FoldingRangeProvider = Some(U3.C1 true) + SelectionRangeProvider = Some(U3.C1 true) + CallHierarchyProvider = Some(U3.C1 true) SemanticTokensProvider = Some + <| U2.C1 { Legend = createTokenLegend - Range = Some true - Full = Some(U2.First true) } - InlayHintProvider = Some { ResolveProvider = Some false } } + Range = Some <| U2.C1 true + Full = Some(U2.C1 true) + WorkDoneProgress = Some false } + InlayHintProvider = + Some + <| U3.C2 + { ResolveProvider = Some false + WorkDoneProgress = Some false } } diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index 08bbd6a2d..5ab36cd5c 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -36,7 +36,8 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe override __.WorkspaceConfiguration(p) = sendServerRequest.Send "workspace/configuration" (box p) - override __.WorkspaceApplyEdit(p) = sendServerRequest.Send "workspace/applyEdit" (box p) + override __.WorkspaceApplyEdit(p: ApplyWorkspaceEditParams) : AsyncLspResult = + sendServerRequest.Send "workspace/applyEdit" (box p) override __.WorkspaceSemanticTokensRefresh() = sendServerNotification "workspace/semanticTokens/refresh" () |> Async.Ignore @@ -71,17 +72,18 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe override x.WorkDoneProgressCreate(token) = match x.ClientCapabilities with - | Some { Window = Some { workDoneProgress = Some true } } -> - let progressCreate: WorkDoneProgressCreateParams = { token = token } + | Some { Window = Some { WorkDoneProgress = Some true } } -> + let progressCreate: WorkDoneProgressCreateParams = { Token = token } sendServerRequest.Send "window/workDoneProgress/create" (box progressCreate) | _ -> async { return Error(JsonRpc.Error.InternalErrorMessage "workDoneProgress is disabled") } override x.Progress(token, value) = - let progress: ProgressParams<_> = { token = token; value = value } - sendServerNotification "$/progress" (box progress) |> Async.Ignore - + let progress: ProgressParams = + { Token = token + Value = Json.fromObject value } + sendServerNotification "$/progress" (box progress) |> Async.Ignore type ServerProgressReport(lspClient: FSharpLspClient, ?token: ProgressToken, ?cancellableDefault: bool) = @@ -91,7 +93,7 @@ type ServerProgressReport(lspClient: FSharpLspClient, ?token: ProgressToken, ?ca let locker = new SemaphoreSlim(1, 1) let cts = new CancellationTokenSource() - member val ProgressToken = defaultArg token (ProgressToken.Second((Guid.NewGuid().ToString()))) + member val ProgressToken = defaultArg token (ProgressToken.C2((Guid.NewGuid().ToString()))) member val CancellationToken = cts.Token diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fsi b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi index b9a182f97..4fbadc76e 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fsi +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi @@ -20,7 +20,7 @@ type FSharpLspClient = override ClientUnregisterCapability: UnregistrationParams -> AsyncLspResult override WorkspaceWorkspaceFolders: unit -> AsyncLspResult override WorkspaceConfiguration: ConfigurationParams -> AsyncLspResult - override WorkspaceApplyEdit: ApplyWorkspaceEditParams -> AsyncLspResult + override WorkspaceApplyEdit: ApplyWorkspaceEditParams -> AsyncLspResult override WorkspaceSemanticTokensRefresh: unit -> Async override TextDocumentPublishDiagnostics: PublishDiagnosticsParams -> Async ///Custom notification for workspace/solution/project loading events diff --git a/test/FsAutoComplete.Tests.Lsp/CallHierarchyTests.fs b/test/FsAutoComplete.Tests.Lsp/CallHierarchyTests.fs index 4e9ca9b91..b3f0ad427 100644 --- a/test/FsAutoComplete.Tests.Lsp/CallHierarchyTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CallHierarchyTests.fs @@ -1,4 +1,5 @@ module FsAutoComplete.Tests.CallHierarchy + open Expecto open FSharp.Compiler.Syntax open FSharp.Compiler @@ -12,86 +13,88 @@ open Helpers open Utils.Server open Ionide.LanguageServerProtocol.Types -let examples = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "CallHierarchy") +let examples = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "CallHierarchy") let incomingExamples = Path.Combine(examples, "IncomingCalls") -let sourceFactory : ISourceTextFactory = RoslynSourceTextFactory() -let resultGet = function | Ok x -> x | Error e -> failwithf "%A" e +let sourceFactory: ISourceTextFactory = RoslynSourceTextFactory() + +let resultGet = + function + | Ok x -> x + | Error e -> failwithf "%A" e + let resultOptionGet = function - | Ok (Some x) -> x - | Ok (None) -> failwithf "Expected Some, got None" + | Ok(Some x) -> x + | Ok(None) -> failwithf "Expected Some, got None" | Error e -> failwithf "%A" e module CallHierarchyPrepareParams = - let create (uri : DocumentUri) (line : int) (character : int) : CallHierarchyPrepareParams = - { - TextDocument = { Uri = uri } - Position = { Character = character; Line = line } - } + let create (uri: DocumentUri) line character : CallHierarchyPrepareParams = + { TextDocument = { Uri = uri } + Position = + { Character = uint32 character + Line = uint32 line } + WorkDoneToken = None } module LspRange = - let create (startLine : int) (startCharacter : int) (endLine : int) (endCharacter : int) : Range = - { - Start = { Character = startCharacter; Line = startLine } - End = { Character = endCharacter; Line = endLine } - } + let create startLine startCharacter endLine endCharacter : Range = + { Start = + { Character = uint32 startCharacter + Line = uint32 startLine } + End = + { Character = uint32 endCharacter + Line = uint32 endLine } } let incomingTests createServer = - serverTestList "IncomingTests" createServer defaultConfigDto (Some incomingExamples) (fun server -> [ - testCaseAsync "Example1" <| async { + serverTestList "IncomingTests" createServer defaultConfigDto (Some incomingExamples) (fun server -> + [ testCaseAsync "Example1" + <| async { let! (aDoc, _) = Server.openDocument "Example1.fsx" server use aDoc = aDoc let! server = server - let prepareParams = CallHierarchyPrepareParams.create aDoc.Uri 2 9 - let! prepareResult = server.Server.TextDocumentPrepareCallHierarchy prepareParams |> Async.map resultOptionGet - - let expectedPrepareResult : HierarchyItem array = [| - { - Data = None - Detail = None - Kind = SymbolKind.Function - Name = "bar" - Range = LspRange.create 2 8 2 11 - SelectionRange = LspRange.create 2 8 2 11 - Tags = None - Uri = aDoc.Uri - } - |] + let prepareParams = CallHierarchyPrepareParams.create aDoc.Uri 2u 9u + + let! prepareResult = + server.Server.TextDocumentPrepareCallHierarchy prepareParams + |> Async.map resultOptionGet + + let expectedPrepareResult: CallHierarchyItem array = + [| { Data = None + Detail = None + Kind = SymbolKind.Function + Name = "bar" + Range = LspRange.create 2 8 2 11 + SelectionRange = LspRange.create 2 8 2 11 + Tags = None + Uri = aDoc.Uri } |] Expect.equal prepareResult expectedPrepareResult "prepareResult" - let expectedIncomingResult : CallHierarchyIncomingCall array = - [| - { - FromRanges = [| - LspRange.create 8 12 8 15 - |] - From = { - Data = None - Detail = Some "From Example1.fsx" - Kind = SymbolKind.Function - Name = "foo" - Range = LspRange.create 6 12 8 18 - SelectionRange = LspRange.create 6 12 6 15 - Tags = None - Uri = aDoc.Uri - } - } - |] - - let incomingParams : CallHierarchyIncomingCallsParams = { - Item = prepareResult[0] - } - let! incomingResult = server.Server.CallHierarchyIncomingCalls incomingParams |> Async.map resultOptionGet + let expectedIncomingResult: CallHierarchyIncomingCall array = + [| { FromRanges = [| LspRange.create 8 12 8 15 |] + From = + { Data = None + Detail = Some "From Example1.fsx" + Kind = SymbolKind.Function + Name = "foo" + Range = LspRange.create 6 12 8 18 + SelectionRange = LspRange.create 6 12 6 15 + Tags = None + Uri = aDoc.Uri } } |] + + let incomingParams: CallHierarchyIncomingCallsParams = + { Item = prepareResult[0] + PartialResultToken = None + WorkDoneToken = None } + + let! incomingResult = + server.Server.CallHierarchyIncomingCalls incomingParams + |> Async.map resultOptionGet Expect.equal incomingResult expectedIncomingResult "incomingResult" - } - ]) + } ]) -let tests createServer = - testList "CallHierarchy" [ - incomingTests createServer - ] +let tests createServer = testList "CallHierarchy" [ incomingTests createServer ] diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/HelpersTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/HelpersTests.fs index 1e9f079e5..e7521bd8e 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/HelpersTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/HelpersTests.fs @@ -9,160 +9,193 @@ open Utils.TextEdit open Ionide.LanguageServerProtocol.Types open FSharp.UMX -let makeText (textFactory : FsAutoComplete.ISourceTextFactory) (text: string) = +let makeText (textFactory: FsAutoComplete.ISourceTextFactory) (text: string) = (textFactory).Create(UMX.tag "fake.fsx", text) let private navigationTests textFactory = - testList (nameof Navigation) [ - let extractTwoCursors text = - let (text, poss) = Cursors.extract text - let text = makeText textFactory text - (text, (poss[0], poss[1])) - - testList (nameof tryEndOfPrevLine) [ - testCase "can get end of prev line when not border line" <| fun _ -> - let text = """let foo = 4 + testList + (nameof Navigation) + [ let extractTwoCursors text = + let (text, poss) = Cursors.extract text + let text = makeText textFactory text + (text, (poss[0], poss[1])) + + testList + (nameof tryEndOfPrevLine) + [ testCase "can get end of prev line when not border line" + <| fun _ -> + let text = + """let foo = 4 let bar = 5 let baz = 5$0 let $0x = 5 let y = 7 let z = 4""" - let (text, (expected, current)) = text |> extractTwoCursors - let actual = tryEndOfPrevLine text current.Line - Expect.equal actual (Some expected) "Incorrect pos" - testCase "can get end of prev line when last line" <| fun _ -> - let text = """let foo = 4 + let (text, (expected, current)) = text |> extractTwoCursors + let actual = tryEndOfPrevLine text current.Line + Expect.equal actual (Some expected) "Incorrect pos" + + testCase "can get end of prev line when last line" + <| fun _ -> + let text = + """let foo = 4 let bar = 5 let baz = 5 let x = 5 let y = 7$0 let z$0 = 4""" - let (text, (expected, current)) = text |> extractTwoCursors - let actual = tryEndOfPrevLine text current.Line - Expect.equal actual (Some expected) "Incorrect pos" - testCase "cannot get end of prev line when first line" <| fun _ -> - let text = """let $0foo$0 = 4 + let (text, (expected, current)) = text |> extractTwoCursors + let actual = tryEndOfPrevLine text current.Line + Expect.equal actual (Some expected) "Incorrect pos" + + testCase "cannot get end of prev line when first line" + <| fun _ -> + let text = + """let $0foo$0 = 4 let bar = 5 let baz = 5 let x = 5 let y = 7 let z = 4""" - let (text, (_, current)) = text |> extractTwoCursors - let actual = tryEndOfPrevLine text current.Line - Expect.isNone actual "No prev line in first line" - - testCase "cannot get end of prev line when single line" <| fun _ -> - let text = makeText textFactory "let foo = 4" - let line = 0 - let actual = tryEndOfPrevLine text line - Expect.isNone actual "No prev line in first line" - ] - testList (nameof tryStartOfNextLine) [ - // this would be WAY easier by just using `{ Line = current.Line + 1; Character = 0 }`... - testCase "can get start of next line when not border line" <| fun _ -> - let text = """let foo = 4 + + let (text, (_, current)) = text |> extractTwoCursors + let actual = tryEndOfPrevLine text current.Line + Expect.isNone actual "No prev line in first line" + + testCase "cannot get end of prev line when single line" + <| fun _ -> + let text = makeText textFactory "let foo = 4" + let line = 0u + let actual = tryEndOfPrevLine text line + Expect.isNone actual "No prev line in first line" ] + + testList + (nameof tryStartOfNextLine) + [ + // this would be WAY easier by just using `{ Line = current.Line + 1; Character = 0 }`... + testCase "can get start of next line when not border line" + <| fun _ -> + let text = + """let foo = 4 let bar = 5 let baz = 5 let $0x = 5 $0let y = 7 let z = 4""" - let (text, (current, expected)) = text |> extractTwoCursors - let actual = tryStartOfNextLine text current.Line - Expect.equal actual (Some expected) "Incorrect pos" - testCase "can get start of next line when first line" <| fun _ -> - let text = """let $0foo = 4 + let (text, (current, expected)) = text |> extractTwoCursors + let actual = tryStartOfNextLine text current.Line + Expect.equal actual (Some expected) "Incorrect pos" + + testCase "can get start of next line when first line" + <| fun _ -> + let text = + """let $0foo = 4 $0let bar = 5 let baz = 5 let x = 5 let y = 7 let z = 4""" - let (text, (current, expected)) = text |> extractTwoCursors - let actual = tryStartOfNextLine text current.Line - Expect.equal actual (Some expected) "Incorrect pos" - testCase "cannot get start of next line when last line" <| fun _ -> - let text = """let foo = 4 + let (text, (current, expected)) = text |> extractTwoCursors + let actual = tryStartOfNextLine text current.Line + Expect.equal actual (Some expected) "Incorrect pos" + + testCase "cannot get start of next line when last line" + <| fun _ -> + let text = + """let foo = 4 let bar = 5 let baz = 5 let x = 5 let y = 7 let $0z$0 = 4""" - let (text, (current, _)) = text |> extractTwoCursors - let actual = tryStartOfNextLine text current.Line - Expect.isNone actual "No next line in last line" - - testCase "cannot get start of next line when single line" <| fun _ -> - let text = makeText textFactory "let foo = 4" - let line = 0 - let actual = tryStartOfNextLine text line - Expect.isNone actual "No next line in first line" - ] - testList (nameof rangeToDeleteFullLine) [ - testCase "can get all range for single line" <| fun _ -> - let text = "$0let foo = 4$0" - let (text, (start, fin)) = text |> extractTwoCursors - let expected = { Start = start; End = fin } - - let line = fin.Line - let actual = text |> rangeToDeleteFullLine line - Expect.equal actual expected "Incorrect range" - - testCase "can get line range with leading linebreak in not border line" <| fun _ -> - let text = """let foo = 4 + + let (text, (current, _)) = text |> extractTwoCursors + let actual = tryStartOfNextLine text current.Line + Expect.isNone actual "No next line in last line" + + testCase "cannot get start of next line when single line" + <| fun _ -> + let text = makeText textFactory "let foo = 4" + let line = 0u + let actual = tryStartOfNextLine text line + Expect.isNone actual "No next line in first line" ] + + testList + (nameof rangeToDeleteFullLine) + [ testCase "can get all range for single line" + <| fun _ -> + let text = "$0let foo = 4$0" + let (text, (start, fin)) = text |> extractTwoCursors + let expected = { Start = start; End = fin } + + let line = fin.Line + let actual = text |> rangeToDeleteFullLine line + Expect.equal actual expected "Incorrect range" + + testCase "can get line range with leading linebreak in not border line" + <| fun _ -> + let text = + """let foo = 4 let bar = 5 let baz = 5$0 let x = 5$0 let y = 7 let z = 4""" - let (text, (start, fin)) = text |> extractTwoCursors - let expected = { Start = start; End = fin } - let line = fin.Line - let actual = text |> rangeToDeleteFullLine line - Expect.equal actual expected "Incorrect range" + let (text, (start, fin)) = text |> extractTwoCursors + let expected = { Start = start; End = fin } + + let line = fin.Line + let actual = text |> rangeToDeleteFullLine line + Expect.equal actual expected "Incorrect range" - testCase "can get line range with leading linebreak in last line" <| fun _ -> - let text = """let foo = 4 + testCase "can get line range with leading linebreak in last line" + <| fun _ -> + let text = + """let foo = 4 let bar = 5 let baz = 5 let x = 5 let y = 7$0 let z = 4$0""" - let (text, (start, fin)) = text |> extractTwoCursors - let expected = { Start = start; End = fin } - let line = fin.Line - let actual = text |> rangeToDeleteFullLine line - Expect.equal actual expected "Incorrect range" + let (text, (start, fin)) = text |> extractTwoCursors + let expected = { Start = start; End = fin } - testCase "can get line range with trailing linebreak in first line" <| fun _ -> - let text = """$0let foo = 4 + let line = fin.Line + let actual = text |> rangeToDeleteFullLine line + Expect.equal actual expected "Incorrect range" + + testCase "can get line range with trailing linebreak in first line" + <| fun _ -> + let text = + """$0let foo = 4 $0let bar = 5 let baz = 5 let x = 5 let y = 7 let z = 4""" - let (text, (start, fin)) = text |> extractTwoCursors - let expected = { Start = start; End = fin } - - let line = start.Line - let actual = text |> rangeToDeleteFullLine line - Expect.equal actual expected "Incorrect range" - - testCase "can get all range for single empty line" <| fun _ -> - let text = makeText textFactory "" - let pos = { Line = 0; Character = 0 } - let expected = { Start = pos; End = pos } - - let line = pos.Line - let actual = text |> rangeToDeleteFullLine line - Expect.equal actual expected "Incorrect range" - ] - ] - -let tests textFactory = testList ($"{nameof(FsAutoComplete)}.{nameof(FsAutoComplete.CodeFix)}") [ - navigationTests textFactory -] + + let (text, (start, fin)) = text |> extractTwoCursors + let expected = { Start = start; End = fin } + + let line = start.Line + let actual = text |> rangeToDeleteFullLine line + Expect.equal actual expected "Incorrect range" + + testCase "can get all range for single empty line" + <| fun _ -> + let text = makeText textFactory "" + let pos = { Line = 0u; Character = 0u } + let expected = { Start = pos; End = pos } + + let line = pos.Line + let actual = text |> rangeToDeleteFullLine line + Expect.equal actual expected "Incorrect range" ] ] + +let tests textFactory = + testList ($"{nameof (FsAutoComplete)}.{nameof (FsAutoComplete.CodeFix)}") [ navigationTests textFactory ] diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs index fec4b6fe6..07ffae74c 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs @@ -4,16 +4,20 @@ module private FsAutoComplete.Tests.CodeFixTests.Utils open Ionide.LanguageServerProtocol.Types open FsAutoComplete.Logging open FsToolkit.ErrorHandling +open FsAutoComplete.LspHelpers + module Diagnostics = let expectCode code (diags: Diagnostic[]) = let diagMsgs = diags - |> Array.choose (fun d -> Option.zip d.Code (Some d.Message)) - |> Array.map(fun (code, msg) -> $"{code}: {msg}") + |> Array.choose (fun d -> Option.zip d.CodeAsString (Some d.Message)) + |> Array.map (fun (code, msg) -> $"{code}: {msg}") |> String.concat ", " + Expecto.Flip.Expect.exists $"There should be a Diagnostic with code %s{code} but were: {diagMsgs} " - (fun (d: Diagnostic) -> d.Code = Some code) + (fun (d: Diagnostic) -> + d.CodeAsString = Some code) diags let acceptAll = ignore diff --git a/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs index 655eff2aa..5dfe64b8f 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs @@ -26,7 +26,10 @@ module private CodeLens = let! (doc, diags) = Server.createUntitledDocument text server do! assertNoDiagnostics diags - let p: CodeLensParams = { TextDocument = doc.TextDocumentIdentifier } + let p: CodeLensParams = + { TextDocument = doc.TextDocumentIdentifier + PartialResultToken = None + WorkDoneToken = None } let! lenses = doc.Server.Server.TextDocumentCodeLens p |> AsyncResult.mapError string @@ -104,15 +107,15 @@ let tests state = |> Array.ofSeq Expect.equal filePath doc.Uri "File path should be the doc we're checking" - Expect.equal triggerPos { Line = 1; Character = 6 } "Position should be 1:6" + Expect.equal triggerPos { Line = 1u; Character = 6u } "Position should be 1:6" Expect.hasLength referenceRanges 1 "There should be 1 reference range for the `func` function" Expect.equal referenceRanges[0] { Uri = doc.Uri Range = - { Start = { Line = 3; Character = 19 } - End = { Line = 3; Character = 23 } } } + { Start = { Line = 3u; Character = 19u } + End = { Line = 3u; Character = 23u } } } "Reference range should be 0:0") testCaseAsync "can show reference counts for 1-character identifier" <| CodeLens.check server """ diff --git a/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs b/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs index 57487a7f7..9fb697606 100644 --- a/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs @@ -10,6 +10,49 @@ open FsAutoComplete.Lsp open FsToolkit.ErrorHandling open Helpers.Expecto.ShadowedTimeouts + +let documentChanges path range text : DidChangeTextDocumentParams = + { TextDocument = + { Uri = Path.FilePathToUri path + Version = 1 } + + ContentChanges = + [| U2.C1 + <| { Range = range + RangeLength = Some 0u + Text = text } |] } + +type CompletionKind = + | Nada + | Invoked + | Char of char + | Incomplete + +let completion doc pos trigger : CompletionParams = + { TextDocument = { Uri = Path.FilePathToUri doc } + WorkDoneToken = None + PartialResultToken = None + Position = pos + Context = + match trigger with + | Nada -> None + | Invoked -> + Some + { TriggerKind = CompletionTriggerKind.Invoked + TriggerCharacter = None } + | Char c -> + Some + { TriggerKind = CompletionTriggerKind.TriggerCharacter + TriggerCharacter = Some(string c) } + | Incomplete -> + Some + { TriggerKind = CompletionTriggerKind.TriggerForIncompleteCompletions + TriggerCharacter = None } } + +let pos l c = { Line = l; Character = c } + +let posRange l c = { Start = pos l c; End = pos l c } + let tests state = let server = async { @@ -36,13 +79,7 @@ let tests state = (async { let! server, path = server - let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 3; Character = 9 } // the '.' in 'Async.' - Context = - Some - { TriggerKind = CompletionTriggerKind.TriggerCharacter - TriggerCharacter = Some '.' } } + let completionParams: CompletionParams = completion path (pos 3u 9u) (Char '.') // the '.' in 'Async.' let! response = server.TextDocumentCompletion completionParams @@ -70,32 +107,16 @@ let tests state = let! server, path = server let lineUnderTest = """Path.GetDirectoryName("foo")""" - let line = 15 - let character = lineUnderTest.Length + let line = 15u + let character = uint32 lineUnderTest.Length let textChange: DidChangeTextDocumentParams = - { TextDocument = - { Uri = Path.FilePathToUri path - Version = 1 } - ContentChanges = - [| { Range = - Some - { Start = { Line = line; Character = character } - End = { Line = line; Character = character } } - RangeLength = Some 0 - Text = "." } |] } + documentChanges path (posRange line character) "." let! c = server.TextDocumentDidChange textChange |> Async.StartChild let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = - { Line = line - Character = character + 1 } - Context = - Some - { TriggerKind = CompletionTriggerKind.TriggerCharacter - TriggerCharacter = Some '.' } } + completion path (pos line (character + 1u)) (Char '.') let! response = server.TextDocumentCompletion completionParams do! c @@ -123,16 +144,11 @@ let tests state = let! server, path = server let lineUnderTest = """Path.GetDirectoryName("foo").""" - let line = 14 - let character = lineUnderTest.Length + let line = 14u + let character = uint32 lineUnderTest.Length let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = line; Character = character } // the '.' in 'GetDirectoryName().' - Context = - Some - { TriggerKind = CompletionTriggerKind.TriggerCharacter - TriggerCharacter = Some '.' } } + completion path (pos line character) (Char '.') let! response = server.TextDocumentCompletion completionParams @@ -158,33 +174,17 @@ let tests state = (async { let! server, path = server - let line = 18 + let line = 18u let lineUnderTest = "\"bareString\"" - let character = lineUnderTest.Length + let character = uint32 lineUnderTest.Length let textChange: DidChangeTextDocumentParams = - { TextDocument = - { Uri = Path.FilePathToUri path - Version = 1 } - ContentChanges = - [| { Range = - Some - { Start = { Line = line; Character = character } - End = { Line = line; Character = character } } - RangeLength = Some 0 - Text = "." } |] } + documentChanges path (posRange line character) "." let! c = server.TextDocumentDidChange textChange |> Async.StartChild let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = - { Line = line - Character = character + 1 } - Context = - Some - { TriggerKind = CompletionTriggerKind.TriggerCharacter - TriggerCharacter = Some '.' } } + completion path (pos line (character + 1u)) (Char '.') let! response = server.TextDocumentCompletion completionParams do! c @@ -211,17 +211,12 @@ let tests state = (async { let! server, path = server - let line = 17 + let line = 17u let lineUnderTest = """"bareString".""" - let character = lineUnderTest.Length + let character = uint32 lineUnderTest.Length let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = line; Character = character } - Context = - Some - { TriggerKind = CompletionTriggerKind.TriggerCharacter - TriggerCharacter = Some '.' } } + completion path (pos line character) (Char '.') let! response = server.TextDocumentCompletion completionParams @@ -247,33 +242,17 @@ let tests state = (async { let! server, path = server - let line = 21 + let line = 21u let lineUnderTest = "[1;2;3]" - let character = lineUnderTest.Length + let character = uint32 lineUnderTest.Length let textChange: DidChangeTextDocumentParams = - { TextDocument = - { Uri = Path.FilePathToUri path - Version = 1 } - ContentChanges = - [| { Range = - Some - { Start = { Line = line; Character = character } - End = { Line = line; Character = character } } - RangeLength = Some 0 - Text = "." } |] } + documentChanges path (posRange line character) "." let! c = server.TextDocumentDidChange textChange |> Async.StartChild let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = - { Line = line - Character = character + 1 } - Context = - Some - { TriggerKind = CompletionTriggerKind.TriggerCharacter - TriggerCharacter = Some '.' } } + completion path (pos line (character + 1u)) (Char '.') let! response = server.TextDocumentCompletion completionParams do! c @@ -300,17 +279,12 @@ let tests state = (async { let! server, path = server - let line = 20 + let line = 20u let lineUnderTest = "[1;2;3]." - let character = lineUnderTest.Length + let character = uint32 lineUnderTest.Length let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = line; Character = character } - Context = - Some - { TriggerKind = CompletionTriggerKind.TriggerCharacter - TriggerCharacter = Some '.' } } + completion path (pos line character) (Char '.') let! response = server.TextDocumentCompletion completionParams @@ -336,14 +310,7 @@ let tests state = (async { let! server, path = server - let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 6; Character = 5 } // the '.' in 'List.' - Context = - Some - { TriggerKind = CompletionTriggerKind.TriggerCharacter - TriggerCharacter = Some '.' } } - + let completionParams: CompletionParams = completion path (pos 6u 5u) (Char '.') let! response = server.TextDocumentCompletion completionParams match response with @@ -364,14 +331,7 @@ let tests state = (async { let! server, path = server - let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 8; Character = 16 } // the '.' in 'List.' - Context = - Some - { TriggerKind = CompletionTriggerKind.TriggerCharacter - TriggerCharacter = Some '.' } } - + let completionParams: CompletionParams = completion path (pos 8u 16u) (Char '.') let! response = server.TextDocumentCompletion completionParams match response with @@ -391,13 +351,7 @@ let tests state = (async { let! server, path = server - let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 11; Character = 10 } // after Lis partial type name in Id record field declaration - Context = - Some - { TriggerKind = CompletionTriggerKind.Invoked - TriggerCharacter = None } } + let completionParams: CompletionParams = completion path (pos 11u 10u) Invoked let! response = server.TextDocumentCompletion completionParams @@ -423,13 +377,7 @@ let tests state = (async { let! server, path = server - let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 8; Character = 12 } // after the 'L' in 'List.' - Context = - Some - { TriggerKind = CompletionTriggerKind.Invoked - TriggerCharacter = None } } + let completionParams: CompletionParams = completion path (pos 8u 12u) Invoked let! response = server.TextDocumentCompletion completionParams @@ -451,14 +399,7 @@ let tests state = (async { let! server, path = server - let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 8; Character = 11 } // before the 'L' in 'List.' - Context = - Some - { TriggerKind = CompletionTriggerKind.Invoked - TriggerCharacter = None } } - + let completionParams: CompletionParams = completion path (pos 8u 11u) Invoked let! response = server.TextDocumentCompletion completionParams match response with @@ -479,13 +420,7 @@ let tests state = (asyncResult { let! server, path = server - let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 3; Character = 9 } // the '.' in 'Async.' - Context = - Some - { TriggerKind = CompletionTriggerKind.TriggerCharacter - TriggerCharacter = Some '.' } } + let completionParams: CompletionParams = completion path (pos 3u 9u) Invoked let! response = server.TextDocumentCompletion completionParams |> AsyncResult.map Option.get let ctokMember = response.Items[0] @@ -509,14 +444,7 @@ let tests state = (async { let! server, path = server - let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 23; Character = 8 } // the '.' in 'List.' - Context = - Some - { TriggerKind = CompletionTriggerKind.TriggerCharacter - TriggerCharacter = Some '.' } } - + let completionParams: CompletionParams = completion path (pos 23u 8u) (Char '.') let! response = server.TextDocumentCompletion completionParams match response with @@ -537,14 +465,7 @@ let tests state = (async { let! server, path = server - let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 24; Character = 9 } // the '.' in 'List.' - Context = - Some - { TriggerKind = CompletionTriggerKind.TriggerCharacter - TriggerCharacter = Some '.' } } - + let completionParams: CompletionParams = completion path (pos 24u 9u) (Char '.') let! response = server.TextDocumentCompletion completionParams match response with @@ -594,10 +515,7 @@ let autocompleteTest state = (async { let! server, path = serverConfig - let p: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 8; Character = 2 } - Context = None } + let p: CompletionParams = completion path (pos 8u 2u) Nada let! res = server.TextDocumentCompletion p @@ -616,11 +534,7 @@ let autocompleteTest state = (async { let! server, path = serverConfig - let p: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 10; Character = 2 } - Context = None } - + let p: CompletionParams = completion path (pos 10u 2u) Nada let! res = server.TextDocumentCompletion p match res with @@ -638,10 +552,7 @@ let autocompleteTest state = (async { let! server, path = serverConfig - let p: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 12; Character = 7 } - Context = None } + let p: CompletionParams = completion path (pos 12u 7u) Nada let! res = server.TextDocumentCompletion p @@ -660,10 +571,7 @@ let autocompleteTest state = (async { let! server, path = serverConfig - let p: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 14; Character = 18 } - Context = None } + let p: CompletionParams = completion path (pos 14u 18u) Nada let! res = server.TextDocumentCompletion p @@ -681,10 +589,7 @@ let autocompleteTest state = (async { let! server, path = serverConfig - let p: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 25; Character = 4 } - Context = None } + let p: CompletionParams = completion path (pos 25u 4u) Nada let! res = server.TextDocumentCompletion p @@ -701,10 +606,7 @@ let autocompleteTest state = (async { let! server, path = serverConfig - let p: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 32; Character = 25 } - Context = None } + let p: CompletionParams = completion path (pos 32u 25u) Nada let! res = server.TextDocumentCompletion p @@ -759,7 +661,7 @@ let autoOpenTests state = let text = edit.NewText let pos = edit.Range.Start - let indentation = pos.Character + (text.Length - text.TrimStart().Length) + let indentation = pos.Character + uint32 (text.Length - text.TrimStart().Length) { Line = pos.Line Character = indentation } @@ -768,6 +670,8 @@ let autoOpenTests state = async { let p = { CodeActionParams.TextDocument = { Uri = Path.FilePathToUri path } + WorkDoneToken = None + PartialResultToken = None Range = { Start = cursor; End = cursor } Context = { Diagnostics = @@ -775,7 +679,7 @@ let autoOpenTests state = Severity = Some DiagnosticSeverity.Error // Message required for QuickFix to fire ("is not defined") Message = $"The value or constructor '{word}' is not defined." - Code = Some "39" + Code = Some(U2.C1 39) Source = Some "F# Compiler" RelatedInformation = None Tags = None @@ -790,14 +694,16 @@ let autoOpenTests state = ca.Kind = Some "quickfix" && ca.Title.StartsWith("open ", StringComparison.Ordinal)) + let (|SingleEdit|_|) (action: CodeAction) = + match action with + | { Edit = Some { DocumentChanges = Some [| U4.C1 { Edits = [| U2.C1 edit |] } |] } } -> Some edit + | _ -> None + match! server.TextDocumentCodeAction p with | Error e -> return failtestf "Quick fix Request failed: %A" e | Ok None -> return failtest "Quick fix Request none" - | Ok(Some(CodeActions(ContainsOpenAction quickfix))) -> - let ns = quickfix.Title.Substring("open ".Length) - - let edit = quickfix.Edit.Value.DocumentChanges.Value.[0].Edits.[0] - + | Ok(Some(CodeActions(ContainsOpenAction({ Title = title } & SingleEdit edit)))) -> + let ns = title.Substring("open ".Length) let openPos = calcOpenPos edit return (edit, ns, openPos) | Ok _ -> return failtest $"Quick fix on `{word}` doesn't contain open action" @@ -828,11 +734,7 @@ let autoOpenTests state = <| async { let! server, path = server - let p: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - // Line AND Column are ZERO-based! - Position = cursor - Context = None } + let p: CompletionParams = completion path cursor Nada match! server.TextDocumentCompletion p with | Error e -> failtestf "Request failed: %A" e @@ -884,44 +786,44 @@ let autoOpenTests state = let parseData (line, column) (lineStr: string) (data: string) = match data.Split(',') with | [| l; c |] -> - let calcN (current: int) (n: string) = + let calcN (current: uint32) (n: string) = let n = n.Trim() match n.[0] with | '|' -> //relative to indentation of current line - let ind = lineStr.Length - lineStr.TrimStart().Length + let ind = uint32 (lineStr.Length - lineStr.TrimStart().Length) match n.Substring(1).Trim() with | "" -> ind - | n -> ind + int n + | n -> uint32 (int ind + int n) | '+' | '-' -> // relative to current position - current + int n + uint32 (int current + int n) | _ -> // absolute - int n + uint32 n let (l, c) = (calcN line l, calcN column c) { Line = l; Character = c } | _ -> failwithf "Invalid data in line (%i,%i) '%s'" line column lineStr - let extractData (lineNumber: int) (line: string) = + let extractData (lineNumber: uint32) (line: string) = let m = regex.Match line if not m.Success then None else let data = m.Groups.["data"] - let (l, c) = (lineNumber, m.Index) + let (l, c) = (lineNumber, uint32 m.Index) let openPos = parseData (l, c) line data.Value let cursorPos = { Line = l; Character = c } (cursorPos, openPos) |> Some System.IO.File.ReadAllLines path - |> Seq.mapi (fun i l -> (i, l)) + |> Seq.mapi (fun i l -> (uint32 i, l)) |> Seq.filter (fun (_, l) -> l.Contains "(*") |> Seq.choose (fun (i, l) -> extractData i l) |> Seq.toList @@ -1020,14 +922,7 @@ let fullNameExternalAutocompleteTest state = (async { let! server, path = serverConfig - let p: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = line; Character = character } - Context = - Some - { TriggerKind = CompletionTriggerKind.Invoked - TriggerCharacter = None } } - + let p: CompletionParams = completion path (pos line character) Invoked let! res = server.TextDocumentCompletion p match res with @@ -1037,12 +932,12 @@ let fullNameExternalAutocompleteTest state = }) let makeAutocompleteTestList (serverConfig: (IFSharpLspServer * string) Async) = - [ makeAutocompleteTest serverConfig "Autocomplete for Array.map contains no backticks" (0, 8) (fun res -> + [ makeAutocompleteTest serverConfig "Autocomplete for Array.map contains no backticks" (0u, 8u) (fun res -> let n = res.Items |> Array.tryFind (fun i -> i.Label = "Array.map") Expect.isSome n "Completion doesn't exist" Expect.equal n.Value.InsertText (Some "Array.map") "Autocomplete for Array.map contains backticks") - makeAutocompleteTest serverConfig "Autocomplete for ``a.b`` contains backticks" (2, 1) (fun res -> + makeAutocompleteTest serverConfig "Autocomplete for ``a.b`` contains backticks" (2u, 1u) (fun res -> let n = res.Items |> Array.tryFind (fun i -> i.Label = "a.b") Expect.isSome n "Completion doesn't exist" Expect.equal n.Value.InsertText (Some "``a.b``") "Autocomplete for a.b contains no backticks") @@ -1052,13 +947,7 @@ let fullNameExternalAutocompleteTest state = (asyncResult { let! server, path = serverConfig - let p: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 3; Character = 4 } - Context = - Some - { TriggerKind = CompletionTriggerKind.Invoked - TriggerCharacter = None } } + let p: CompletionParams = completion path (pos 3u 4u) Invoked let! response = server.TextDocumentCompletion p |> AsyncResult.map Option.get @@ -1079,24 +968,28 @@ let fullNameExternalAutocompleteTest state = } |> AsyncResult.bimap id (fun e -> failwithf "%O" e)) - makeAutocompleteTest serverConfig "Check Autocomplete for System.Text.RegularExpressions.Regex" (4, 5) (fun res -> - let n = - res.Items - |> Array.tryFind (fun i -> i.Label = "Regex (System.Text.RegularExpressions)") + makeAutocompleteTest + serverConfig + "Check Autocomplete for System.Text.RegularExpressions.Regex" + (4u, 5u) + (fun res -> + let n = + res.Items + |> Array.tryFind (fun i -> i.Label = "Regex (System.Text.RegularExpressions)") - Expect.isSome n "Completion doesn't exist" + Expect.isSome n "Completion doesn't exist" - Expect.equal - n.Value.InsertText - (Some "System.Text.RegularExpressions.Regex") - "Autocomplete for Regex is not System.Text.RegularExpressions.Regex" + Expect.equal + n.Value.InsertText + (Some "System.Text.RegularExpressions.Regex") + "Autocomplete for Regex is not System.Text.RegularExpressions.Regex" - Expect.equal - n.Value.FilterText - (Some "RegexSystem.Text.RegularExpressions.Regex") - "Autocomplete for Regex is not System.Text.RegularExpressions.Regex") + Expect.equal + n.Value.FilterText + (Some "RegexSystem.Text.RegularExpressions.Regex") + "Autocomplete for Regex is not System.Text.RegularExpressions.Regex") - makeAutocompleteTest serverConfig "Autocomplete for Result is just Result" (5, 6) (fun res -> + makeAutocompleteTest serverConfig "Autocomplete for Result is just Result" (5u, 6u) (fun res -> let n = res.Items |> Array.tryFind (fun i -> i.Label = "Result") Expect.isSome n "Completion doesn't exist" Expect.equal n.Value.InsertText (Some "Result") "Autocomplete contains given symbol") ] diff --git a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs index 6542717e0..3ca93fd60 100644 --- a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs @@ -40,7 +40,7 @@ let initTests createServer = Locale = None RootUri = None InitializationOptions = Some(Server.serialize defaultConfigDto) - Capabilities = Some clientCaps + Capabilities = clientCaps ClientInfo = Some { Name = "FSAC Tests" @@ -49,69 +49,94 @@ let initTests createServer = Some [| { Uri = Path.FilePathToUri tempDir Name = "Test Folder" } |] - trace = None } + Trace = None + WorkDoneToken = None } let! result = server.Initialize p match result with | Result.Ok res -> - do! server.Initialized(InitializedParams()) + do! server.Initialized() Expect.equal res.Capabilities.CodeActionProvider (Some( - U2.Second + U2.C2 { CodeActionOptions.ResolveProvider = None - CodeActionOptions.CodeActionKinds = None } + CodeActionOptions.CodeActionKinds = None + WorkDoneProgress = Some false } )) "Code Action Provider" Expect.equal res.Capabilities.CodeLensProvider - (Some { CodeLensOptions.ResolveProvider = Some true }) + (Some + { CodeLensOptions.ResolveProvider = Some true + WorkDoneProgress = Some false }) "Code Lens Provider" - Expect.equal res.Capabilities.DefinitionProvider (Some true) "Definition Provider" - Expect.equal res.Capabilities.DocumentFormattingProvider (Some true) "Document Formatting Provider" - Expect.equal res.Capabilities.DocumentHighlightProvider (Some true) "Document Highlighting Provider" + Expect.equal res.Capabilities.DefinitionProvider (Some(U2.C1 true)) "Definition Provider" + Expect.equal res.Capabilities.DocumentFormattingProvider (Some(U2.C1 true)) "Document Formatting Provider" + Expect.equal res.Capabilities.DocumentHighlightProvider (Some(U2.C1 true)) "Document Highlighting Provider" Expect.equal res.Capabilities.DocumentLinkProvider None "Document Link Provider" Expect.equal res.Capabilities.DocumentOnTypeFormattingProvider None "Document OnType Formatting Provider" - Expect.equal res.Capabilities.DocumentRangeFormattingProvider (Some true) "Document Range Formatting Provider" + + Expect.equal + res.Capabilities.DocumentRangeFormattingProvider + (Some(U2.C1 true)) + "Document Range Formatting Provider" Expect.equal res.Capabilities.DocumentSymbolProvider - (Some(U2.Second { Label = Some "F#" })) + (Some( + U2.C2 + { Label = Some "F#" + WorkDoneProgress = Some false } + )) "Document Symbol Provider" Expect.equal res.Capabilities.ExecuteCommandProvider None "Execute Command Provider" Expect.equal res.Capabilities.Experimental None "Experimental" - Expect.equal res.Capabilities.HoverProvider (Some true) "Hover Provider" - Expect.equal res.Capabilities.ImplementationProvider (Some true) "Implementation Provider" - Expect.equal res.Capabilities.ReferencesProvider (Some true) "References Provider" - Expect.equal res.Capabilities.RenameProvider (Some(U2.Second { PrepareProvider = Some true })) "Rename Provider" + Expect.equal res.Capabilities.HoverProvider (Some(U2.C1 true)) "Hover Provider" + Expect.equal res.Capabilities.ImplementationProvider (Some(U3.C1 true)) "Implementation Provider" + Expect.equal res.Capabilities.ReferencesProvider (Some(U2.C1 true)) "References Provider" + + Expect.equal + res.Capabilities.RenameProvider + (Some( + U2.C2 + { PrepareProvider = Some true + WorkDoneProgress = Some false } + )) + "Rename Provider" Expect.equal res.Capabilities.SignatureHelpProvider (Some - { TriggerCharacters = Some [| '('; ','; ' ' |] - RetriggerCharacters = Some [| ','; ')'; ' ' |] }) + { TriggerCharacters = Some [| "("; ","; " " |] + RetriggerCharacters = Some [| ","; ")"; " " |] + WorkDoneProgress = Some false }) "Signature Help Provider" let td = { TextDocumentSyncOptions.Default with OpenClose = Some true Change = Some TextDocumentSyncKind.Incremental - Save = Some { IncludeText = Some true } } + Save = Some(U2.C2 { IncludeText = Some true }) } - Expect.equal res.Capabilities.TextDocumentSync (Some td) "Text Document Provider" - Expect.equal res.Capabilities.TypeDefinitionProvider (Some true) "Type Definition Provider" + Expect.equal res.Capabilities.TextDocumentSync (Some(U2.C1 td)) "Text Document Provider" + Expect.equal res.Capabilities.TypeDefinitionProvider (Some(U3.C1 true)) "Type Definition Provider" Expect.equal res.Capabilities.WorkspaceSymbolProvider - (Some(U2.Second { ResolveProvider = Some true })) + (Some( + U2.C2 + { ResolveProvider = Some true + WorkDoneProgress = Some false } + )) "Workspace Symbol Provider" - Expect.equal res.Capabilities.FoldingRangeProvider (Some true) "Folding Range Provider active" + Expect.equal res.Capabilities.FoldingRangeProvider (Some(U3.C1 true)) "Folding Range Provider active" | Result.Error _e -> failtest "Initialization failed" }) @@ -134,20 +159,25 @@ let documentSymbolTest state = "Get Document Symbols" (async { let! server, path = server - let p: DocumentSymbolParams = { TextDocument = { Uri = Path.FilePathToUri path } } + + let p: DocumentSymbolParams = + { TextDocument = { Uri = Path.FilePathToUri path } + WorkDoneToken = None + PartialResultToken = None } + let! res = server.TextDocumentDocumentSymbol p match res with | Result.Error e -> failtestf "Request failed: %A" e | Result.Ok None -> failtest "Request none" - | Result.Ok(Some(U2.First res)) -> + | Result.Ok(Some(U2.C1 res)) -> Expect.equal res.Length 15 "Document Symbol has all symbols" Expect.exists res (fun n -> n.Name = "MyDateTime" && n.Kind = SymbolKind.Class) "Document symbol contains given symbol" - | Result.Ok(Some(U2.Second _res)) -> raise (NotImplementedException("DocumentSymbol isn't used in FSAC yet")) + | Result.Ok(Some(U2.C2 _res)) -> raise (NotImplementedException("DocumentSymbol isn't used in FSAC yet")) }) ] let foldingTests state = @@ -174,7 +204,11 @@ let foldingTests state = let! server, libraryPath = server let! rangeResponse = - server.TextDocumentFoldingRange({ TextDocument = { Uri = Path.FilePathToUri libraryPath } }) + server.TextDocumentFoldingRange( + { TextDocument = { Uri = Path.FilePathToUri libraryPath } + WorkDoneToken = None + PartialResultToken = None } + ) match rangeResponse with | Ok(Some(ranges)) -> @@ -183,37 +217,37 @@ let foldingTests state = | LspResult.Error e -> failtestf "Error from range LSP call: %A" e }) ] +let inline _lang<'t when 't: (member Language: string)> (x: 't) = x.Language + +let inline _value<'t when 't: (member Value: string)> (x: 't) = x.Value + +[] +let inline (|FSharpLanguage|_|) x = if _lang x = "fsharp" then ValueSome() else ValueNone + +let inline (|Value|) x = _value x let tooltipTests state = let (|Signature|_|) (hover: Hover) = match hover with - | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp"; Value = tooltip } - MarkedString.String _docComment - MarkedString.String _fullname - MarkedString.String _assembly |] } -> Some tooltip - | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp"; Value = tooltip } - MarkedString.String _docComment - MarkedString.String _showDocumentationLink - MarkedString.String _fullname - MarkedString.String _assembly |] } -> Some tooltip + | { Contents = U3.C3 [| U2.C2(FSharpLanguage & Value tooltip); U2.C1 _docComment; U2.C1 _fullname; U2.C1 _assembly |] } -> + Some tooltip + | { Contents = U3.C3 [| U2.C2(FSharpLanguage & Value tooltip) + U2.C1 _docComment + U2.C1 _showDocumentationLink + U2.C1 _fullname + U2.C1 _assembly |] } -> Some tooltip | _ -> None let (|Description|_|) (hover: Hover) = match hover with - | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp" - Value = _tooltip } - MarkedString.String description |] } -> Some description - | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp" - Value = _tooltip } - MarkedString.String description - MarkedString.String _fullname - MarkedString.String _assembly |] } -> Some description - | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp" - Value = _tooltip } - MarkedString.String description - MarkedString.String _showDocumentationLink - MarkedString.String _fullname - MarkedString.String _assembly |] } -> Some description + | { Contents = U3.C3 [| U2.C2(FSharpLanguage & Value _tooltip); U2.C1 description |] } -> Some description + | { Contents = U3.C3 [| U2.C2(FSharpLanguage & Value _tooltip); U2.C1 description; U2.C1 _fullname; U2.C1 _assembly |] } -> + Some description + | { Contents = U3.C3 [| U2.C2(FSharpLanguage & Value _tooltip) + U2.C1 description + U2.C1 _showDocumentationLink + U2.C1 _fullname + U2.C1 _assembly |] } -> Some description | _ -> None let server = @@ -279,21 +313,21 @@ let tooltipTests state = [ testList "tests" [ verifyDescription - 0 - 2 + 0u + 2u [ "**Description**" "" "" "Used to associate, or bind, a name to a value or function." "" ] // `let` keyword - verifySignature 0 4 "val arrayOfTuples: (int * int) array" // verify that even the first letter of the tooltip triggers correctly - verifySignature 0 5 "val arrayOfTuples: (int * int) array" // inner positions trigger - verifySignature 1 5 "val listOfTuples: list" // verify we default to prefix-generics style - verifySignature 2 5 "val listOfStructTuples: list" // verify we render struct tuples in a round-tripabble format - verifySignature 3 5 "val floatThatShouldHaveGenericReportedInTooltip: float" // verify we strip measure annotations + verifySignature 0u 4u "val arrayOfTuples: (int * int) array" // verify that even the first letter of the tooltip triggers correctly + verifySignature 0u 5u "val arrayOfTuples: (int * int) array" // inner positions trigger + verifySignature 1u 5u "val listOfTuples: list" // verify we default to prefix-generics style + verifySignature 2u 5u "val listOfStructTuples: list" // verify we render struct tuples in a round-tripabble format + verifySignature 3u 5u "val floatThatShouldHaveGenericReportedInTooltip: float" // verify we strip measure annotations verifyDescription - 4 - 4 + 4u + 4u [ "**Description**" "" "Print to a string using the given format." @@ -310,8 +344,8 @@ let tooltipTests state = "" "* `'T` is `System.String`" ] // verify fancy descriptions for external library functions and correct backticks for multiple segments verifyDescription - 13 - 11 + 13u + 11u [ "**Description**" "" "" @@ -327,37 +361,40 @@ let tooltipTests state = "**Returns**" "" "" ] - verifySignature 14 5 "val nestedTuples: int * ((int * int) * int)" // verify that tuples render correctly (parens, etc) - verifySignature 15 5 "val nestedStructTuples: int * struct (int * int)" // verify we can differentiate between struct and non-struct tuples - verifySignature 21 9 "val speed: float" // verify we nicely-render measure annotations + verifySignature 14u 5u "val nestedTuples: int * ((int * int) * int)" // verify that tuples render correctly (parens, etc) + verifySignature 15u 5u "val nestedStructTuples: int * struct (int * int)" // verify we can differentiate between struct and non-struct tuples + verifySignature 21u 9u "val speed: float" // verify we nicely-render measure annotations // verify formatting of function-parameters to values. NOTE: we want to wrap them in parens for user clarity eventually. verifySignature - 26 - 5 + 26u + 5u (concatLines [ "val funcWithFunParam:"; " f: (int -> unit) ->"; " i: int"; " -> unit" ]) // verify formatting of tuple args. NOTE: we want to wrap tuples in parens for user clarify eventually. verifySignature - 30 - 12 + 30u + 12u (concatLines [ "val funcWithTupleParam:"; " int *"; " int"; " -> int * int" ]) // verify formatting of struct tuple args in parameter tooltips. verifySignature - 32 - 12 + 32u + 12u (concatLines [ "val funcWithStructTupleParam:" " f: struct (int * int)" " -> struct (int * int)" ]) - verifySignature 36 15 (concatLines [ "member Foo:"; " stuff: int * int * int"; " -> int" ]) - verifySignature 37 15 (concatLines [ "member Bar:"; " a: int *"; " b: int *"; " c: int"; " -> int" ]) + verifySignature 36u 15u (concatLines [ "member Foo:"; " stuff: int * int * int"; " -> int" ]) + verifySignature + 37u + 15u + (concatLines [ "member Bar:"; " a: int *"; " b: int *"; " c: int"; " -> int" ]) // verify formatting for multi-char operators - verifySignature 39 7 (concatLines [ "val ( .>> ):"; " x: int ->"; " y: int"; " -> int" ]) + verifySignature 39u 7u (concatLines [ "val ( .>> ):"; " x: int ->"; " y: int"; " -> int" ]) // verify formatting for single-char operators - verifySignature 41 6 (concatLines [ "val ( ^ ):"; " x: int ->"; " y: int"; " -> int" ]) + verifySignature 41u 6u (concatLines [ "val ( ^ ):"; " x: int ->"; " y: int"; " -> int" ]) // verify rendering of generic constraints verifySignature - 43 - 13 + 43u + 13u (concatLines [ "val inline add:" " x: 'a (requires static member ( + ) ) ->" @@ -365,39 +402,39 @@ let tooltipTests state = " -> 'c" ]) //verify rendering of solved generic constraints in tooltips for members where they are solved verifyDescription - 45 - 15 + 45u + 15u [ "**Generic Parameters**" "" "* `'a` is `int`" "* `'b` is `int`" "* `'c` is `int`" ] verifySignature - 48 - 28 + 48u + 28u (concatLines [ "static member Start:" " body : (MailboxProcessor -> Async) *" " ?cancellationToken: System.Threading.CancellationToken" " -> MailboxProcessor" ]) - verifySignature 54 9 "Case2 of string * newlineBefore: bool * newlineAfter: bool" + verifySignature 54u 9u "Case2 of string * newlineBefore: bool * newlineAfter: bool" verifySignature - 60 - 7 + 60u + 7u (concatLines [ "active pattern Value: " " input: Expr" " -> option" ]) verifySignature - 65 - 7 + 65u + 7u (concatLines [ "active pattern DefaultValue: " " input: Expr" " -> option" ]) verifySignature - 70 - 7 + 70u + 7u (concatLines [ "active pattern ValueWithName: " " input: Expr" @@ -497,7 +534,10 @@ let diagnosticsTest state = | Core.Result.Error errors -> Expect.exists errors - (fun error -> error.Code = Some "39" || error.Code = Some "41") + (fun error -> + match error.CodeAsString with + | Some("39" | "41") -> true + | _ -> false) "should have an error FS0039(identifier not defined) or FS0041(a unique overload for method 'TryParse' could not be determined based on type information prior to this program point)" Expect.all diff --git a/test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs b/test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs index eeec0d555..07995ab1d 100644 --- a/test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs @@ -9,9 +9,10 @@ open FsAutoComplete.Lsp open FsToolkit.ErrorHandling open Utils.Server open Helpers.Expecto.ShadowedTimeouts +open FsAutoComplete.LspHelpers let tests state = - let createServer() = + let createServer () = async { let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "EmptyFileTests") @@ -23,88 +24,101 @@ let tests state = return server, events, scriptPath } |> Async.Cache - let server1 = createServer() - let server2 = createServer() + + let server1 = createServer () + let server2 = createServer () testSequenced <| testList "empty file features" [ testList "tests" - [ - testCaseAsync - "no parsing/checking errors" - (async { - let! server, events, scriptPath = server1 - do! server.TextDocumentDidOpen { TextDocument = loadDocument scriptPath } - - match! waitForParseResultsForFile "EmptyFile.fsx" events with - | Ok _ -> () // all good, no parsing/checking errors - | Core.Result.Error errors -> failwithf "Errors while parsing script %s: %A" scriptPath errors - }) - - testCaseAsync - "auto completion does not throw and is empty" - (async { - let! server, _, path = server1 - do! server.TextDocumentDidOpen { TextDocument = loadDocument path } - - let completionParams: CompletionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 0; Character = 0 } - Context = - Some - { TriggerKind = CompletionTriggerKind.Invoked - TriggerCharacter = None } } - - match! server.TextDocumentCompletion completionParams with - | Ok (Some _) -> failtest "An empty file has empty completions" - | Ok None -> () - | Error e -> failtestf "Got an error while retrieving completions: %A" e - }) - testCaseAsync - "type 'c' for checking error and autocompletion starts with 'async'" - (async { - let! server, events, scriptPath = server2 - do! server.TextDocumentDidOpen { TextDocument = loadDocument scriptPath } - - do! server.TextDocumentDidChange { - TextDocument = { Uri = Path.FilePathToUri scriptPath; Version = 1 } - ContentChanges = [| { - Range = Some { Start = { Line = 0; Character = 0 }; End = { Line = 0; Character = 0 } } - RangeLength = Some 0 - Text = "c" - } |] - } - - let! completions = - server.TextDocumentCompletion { - TextDocument = { Uri = Path.FilePathToUri scriptPath } - Position = { Line = 0; Character = 1 } - Context = - Some - { TriggerKind = CompletionTriggerKind.Invoked - TriggerCharacter = None } - } |> Async.StartChild - - let! compilerResults = waitForCompilerDiagnosticsForFile "EmptyFile.fsx" events |> Async.StartChild - - match! compilerResults with - | Ok () -> failtest "should get an F# compiler checking error from a 'c' by itself" - | Core.Result.Error errors -> - Expect.hasLength errors 1 "should have only an error FS0039: identifier not defined" - Expect.exists errors (fun error -> error.Code = Some "39") $"should have an error FS0039: identifier not defined %A{errors}" - - match! completions with - | Ok (Some completions) -> - Expect.isGreaterThan - completions.Items.Length - 30 - "should have a complete completion list all containing c" - - let firstItem = completions.Items.[0] - Expect.equal firstItem.Label "async" "first member should be async" - | Ok None -> failtest "Should have gotten some completion items" - | Error e -> failtestf "Got an error while retrieving completions: %A" e - }) - ]] + [ testCaseAsync + "no parsing/checking errors" + (async { + let! server, events, scriptPath = server1 + do! server.TextDocumentDidOpen { TextDocument = loadDocument scriptPath } + + match! waitForParseResultsForFile "EmptyFile.fsx" events with + | Ok _ -> () // all good, no parsing/checking errors + | Core.Result.Error errors -> failwithf "Errors while parsing script %s: %A" scriptPath errors + }) + + testCaseAsync + "auto completion does not throw and is empty" + (async { + let! server, _, path = server1 + do! server.TextDocumentDidOpen { TextDocument = loadDocument path } + + let completionParams: CompletionParams = + { TextDocument = { Uri = Path.FilePathToUri path } + Position = { Line = 0u; Character = 0u } + Context = + Some + { TriggerKind = CompletionTriggerKind.Invoked + TriggerCharacter = None } + WorkDoneToken = None + PartialResultToken = None } + + match! server.TextDocumentCompletion completionParams with + | Ok(Some _) -> failtest "An empty file has empty completions" + | Ok None -> () + | Error e -> failtestf "Got an error while retrieving completions: %A" e + }) + testCaseAsync + "type 'c' for checking error and autocompletion starts with 'async'" + (async { + let! server, events, scriptPath = server2 + do! server.TextDocumentDidOpen { TextDocument = loadDocument scriptPath } + + do! + server.TextDocumentDidChange + { TextDocument = + { Uri = Path.FilePathToUri scriptPath + Version = 1 } + ContentChanges = + [| U2.C1 + { Range = + { Start = { Line = 0u; Character = 0u } + End = { Line = 0u; Character = 0u } } + RangeLength = Some 0u + Text = "c" } |] } + // wait for typechecking to propogate? + do! Async.Sleep 1000 + + let! completions = + server.TextDocumentCompletion + { TextDocument = { Uri = Path.FilePathToUri scriptPath } + Position = { Line = 0u; Character = 1u } + Context = + Some + { TriggerKind = CompletionTriggerKind.Invoked + TriggerCharacter = None } + WorkDoneToken = None + PartialResultToken = None } + |> Async.StartChild + + let! compilerResults = waitForCompilerDiagnosticsForFile "EmptyFile.fsx" events |> Async.StartChild + + match! compilerResults with + | Ok() -> failtest "should get an F# compiler checking error from a 'c' by itself" + | Core.Result.Error errors -> + Expect.hasLength errors 1 "should have only an error FS0039: identifier not defined" + + Expect.exists + errors + (fun error -> error.CodeAsString = Some "39") + $"should have an error FS0039: identifier not defined %A{errors}" + + match! completions with + | Ok(Some completions) -> + Expect.isGreaterThan + completions.Items.Length + 30 + "should have a complete completion list all containing c" + + let firstItem = completions.Items.[0] + Expect.equal firstItem.Label "async" "first member should be async" + | Ok None -> failtest "Should have gotten some completion items" + | Error e -> failtestf "Got an error while retrieving completions: %A" e + }) ] ] diff --git a/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs b/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs index 191ed0e1d..dd88c82d2 100644 --- a/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs @@ -111,15 +111,18 @@ let linterTests state = } |> Async.Cache - let urlFor (code: string option) = - Some { Href = Some(System.Uri $"http://fsprojects.github.io/FSharpLint/how-tos/rules/%s{code.Value}.html") } + let urlFor (code: U2 option) = + match code with + | None -> None + | Some(U2.C1 intCode) -> Some { Href = $"http://fsprojects.github.io/FSharpLint/how-tos/rules/%d{intCode}.html" } + | Some(U2.C2 strCode) -> Some { Href = $"http://fsprojects.github.io/FSharpLint/how-tos/rules/%s{strCode}.html" } let expectedDiagnostics = [| { Range = - { Start = { Line = 0; Character = 7 } - End = { Line = 0; Character = 11 } } + { Start = { Line = 0u; Character = 7u } + End = { Line = 0u; Character = 11u } } Severity = Some DiagnosticSeverity.Information - Code = Some "FL0042" + Code = Some(U2.C2("FL0042")) Source = Some "F# Linter" Message = "Consider changing `test` to PascalCase." RelatedInformation = None @@ -128,10 +131,10 @@ let linterTests state = CodeDescription = None }, "Test" { Range = - { Start = { Line = 1; Character = 16 } - End = { Line = 1; Character = 25 } } + { Start = { Line = 1u; Character = 16u } + End = { Line = 1u; Character = 25u } } Severity = Some DiagnosticSeverity.Information - Code = Some "FL0065" + Code = Some(U2.C2("FL0065")) Source = Some "F# Linter" Message = "`not (a = b)` might be able to be refactored into `a <> b`." RelatedInformation = None @@ -140,10 +143,10 @@ let linterTests state = CodeDescription = None }, "a <> b" { Range = - { Start = { Line = 2; Character = 16 } - End = { Line = 2; Character = 26 } } + { Start = { Line = 2u; Character = 16u } + End = { Line = 2u; Character = 26u } } Severity = Some DiagnosticSeverity.Information - Code = Some "FL0065" + Code = Some(U2.C2("FL0065")) Source = Some "F# Linter" Message = "`not (a <> b)` might be able to be refactored into `a = b`." RelatedInformation = None @@ -152,10 +155,10 @@ let linterTests state = CodeDescription = None }, "a = b" { Range = - { Start = { Line = 3; Character = 12 } - End = { Line = 3; Character = 22 } } + { Start = { Line = 3u; Character = 12u } + End = { Line = 3u; Character = 22u } } Severity = Some DiagnosticSeverity.Information - Code = Some "FL0065" + Code = Some(U2.C2("FL0065")) Source = Some "F# Linter" Message = "`fun x -> x` might be able to be refactored into `id`." RelatedInformation = None @@ -164,10 +167,10 @@ let linterTests state = CodeDescription = None }, "id" { Range = - { Start = { Line = 4; Character = 12 } - End = { Line = 4; Character = 20 } } + { Start = { Line = 4u; Character = 12u } + End = { Line = 4u; Character = 20u } } Severity = Some DiagnosticSeverity.Information - Code = Some "FL0065" + Code = Some(U2.C2("FL0065")) Source = Some "F# Linter" Message = "`not true` might be able to be refactored into `false`." RelatedInformation = None @@ -176,10 +179,10 @@ let linterTests state = CodeDescription = None }, "false" { Range = - { Start = { Line = 5; Character = 12 } - End = { Line = 5; Character = 21 } } + { Start = { Line = 5u; Character = 12u } + End = { Line = 5u; Character = 21u } } Severity = Some DiagnosticSeverity.Information - Code = Some "FL0065" + Code = Some(U2.C2("FL0065")) Source = Some "F# Linter" Message = "`not false` might be able to be refactored into `true`." RelatedInformation = None @@ -188,10 +191,10 @@ let linterTests state = CodeDescription = None }, "true" { Range = - { Start = { Line = 7; Character = 14 } - End = { Line = 7; Character = 21 } } + { Start = { Line = 7u; Character = 14u } + End = { Line = 7u; Character = 21u } } Severity = Some DiagnosticSeverity.Information - Code = Some "FL0065" + Code = Some(U2.C2("FL0065")) Source = Some "F# Linter" Message = "`a <> true` might be able to be refactored into `not a`." RelatedInformation = None @@ -200,10 +203,10 @@ let linterTests state = CodeDescription = None }, "not a" { Range = - { Start = { Line = 8; Character = 14 } - End = { Line = 8; Character = 20 } } + { Start = { Line = 8u; Character = 14u } + End = { Line = 8u; Character = 20u } } Severity = Some DiagnosticSeverity.Information - Code = Some "FL0065" + Code = Some(U2.C2("FL0065")) Source = Some "F# Linter" Message = "`x = null` might be able to be refactored into `isNull x`." RelatedInformation = None @@ -212,10 +215,10 @@ let linterTests state = CodeDescription = None }, "isNull a" { Range = - { Start = { Line = 9; Character = 14 } - End = { Line = 9; Character = 37 } } + { Start = { Line = 9u; Character = 14u } + End = { Line = 9u; Character = 37u } } Severity = Some DiagnosticSeverity.Information - Code = Some "FL0065" + Code = Some(U2.C2("FL0065")) Source = Some "F# Linter" Message = "`List.head (List.sort x)` might be able to be refactored into `List.min x`." RelatedInformation = None @@ -256,11 +259,11 @@ let formattingTests state = let editForWholeFile sourceFile expectedFile = let sourceLines = File.ReadAllLines sourceFile - let start = { Line = 0; Character = 0 } + let start = { Line = 0u; Character = 0u } let ``end`` = - { Line = sourceLines.Length - 1 - Character = sourceLines.[sourceLines.Length - 1].Length } + { Line = uint32 sourceLines.Length - 1u + Character = uint32 sourceLines.[sourceLines.Length - 1].Length } let expectedText = File.ReadAllText expectedFile @@ -283,12 +286,12 @@ let formattingTests state = server.TextDocumentFormatting { TextDocument = { Uri = Path.FilePathToUri sourceFile } Options = - { TabSize = 4 + { TabSize = 4u InsertSpaces = true TrimTrailingWhitespace = None InsertFinalNewline = None - TrimFinalNewlines = None - AdditionalData = System.Collections.Generic.Dictionary<_, _>() } } + TrimFinalNewlines = None } + WorkDoneToken = None } with | Ok(Some [| edit |]) -> let normalized = @@ -346,10 +349,10 @@ let analyzerTests state = let expected = [| { Range = - { Start = { Line = 3; Character = 13 } - End = { Line = 3; Character = 31 } } + { Start = { Line = 3u; Character = 13u } + End = { Line = 3u; Character = 31u } } Severity = Some DiagnosticSeverity.Warning - Code = Some "OV001" + Code = Some(U2.C2("OV001")) Source = Some "F# Analyzers (Option.Value analyzer)" Message = "Option.Value shouldn't be used" RelatedInformation = None @@ -415,6 +418,6 @@ let signatureTests state = "signature evaluation" [ testList "tests" - [ verifySignature 0 (4, 16) "val arrayOfTuples: (int * int) array" - verifySignature 1 (4, 15) "val listOfTuples: (int * int) list" - verifySignature 2 (4, 15) "val someFunction: a: 'a -> unit" ] ] + [ verifySignature 0u (4u, 16u) "val arrayOfTuples: (int * int) array" + verifySignature 1u (4u, 15u) "val listOfTuples: (int * int) list" + verifySignature 2u (4u, 15u) "val someFunction: a: 'a -> unit" ] ] diff --git a/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs b/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs index 8014fb433..8516a7628 100644 --- a/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs @@ -43,8 +43,10 @@ let private scriptTests state = let request: ReferenceParams = { TextDocument = { Uri = Path.FilePathToUri scriptPath } - Position = { Line = 2; Character = 0 } // beginning of the usage of the `foo` function - Context = { IncludeDeclaration = true } } // beginning of the usage of the `foo` function + Position = { Line = 2u; Character = 0u } // beginning of the usage of the `foo` function + Context = { IncludeDeclaration = true } + WorkDoneToken = None + PartialResultToken = None } // beginning of the usage of the `foo` function let! response = server.TextDocumentReferences request @@ -58,8 +60,8 @@ let private scriptTests state = Expect.equal reference.Range - { Start = { Line = 0; Character = 4 } - End = { Line = 0; Character = 7 } } + { Start = { Line = 0u; Character = 4u } + End = { Line = 0u; Character = 7u } } "should point to the definition of `foo`" }) ] @@ -251,10 +253,12 @@ let private solutionTests state = let length = mark.Length let line = i - 1 // marker is line AFTER actual range - { Start = { Line = line; Character = col } + { Start = + { Line = uint32 line + Character = uint32 col } End = - { Line = line - Character = col + length } } + { Line = uint32 line + Character = uint32 (col + length) } } let loc = { Uri = path |> normalizePath |> Path.LocalPathToUri @@ -347,7 +351,9 @@ let private solutionTests state = let request: ReferenceParams = { TextDocument = doc.TextDocumentIdentifier Position = cursor - Context = { IncludeDeclaration = true } } + Context = { IncludeDeclaration = true } + WorkDoneToken = None + PartialResultToken = None } let! refs = doc.Server.Server.TextDocumentReferences request @@ -430,7 +436,9 @@ let private untitledTests state = let request: ReferenceParams = { TextDocument = cursorDoc.TextDocumentIdentifier Position = cursor - Context = { IncludeDeclaration = true } } + Context = { IncludeDeclaration = true } + WorkDoneToken = None + PartialResultToken = None } let! refs = cursorDoc.Server.Server.TextDocumentReferences request @@ -465,7 +473,9 @@ let private rangeTests state = let request: ReferenceParams = { TextDocument = doc.TextDocumentIdentifier Position = cursors.Cursor.Value - Context = { IncludeDeclaration = true } } + Context = { IncludeDeclaration = true } + WorkDoneToken = None + PartialResultToken = None } let! refs = doc.Server.Server.TextDocumentReferences request @@ -628,8 +638,8 @@ let tests state = let tryFixupRangeTests (sourceTextFactory: ISourceTextFactory) = - testSequenced <| - testList + testSequenced + <| testList ($"{nameof Tokenizer.tryFixupRange}") [ let checker = lazy (FSharpChecker.Create()) @@ -651,14 +661,14 @@ let tryFixupRangeTests (sourceTextFactory: ISourceTextFactory) = | _ -> failtest "CheckFile aborted" // Expect.isEmpty checkResults.Diagnostics "There should be no check diags" Expect.hasLength checkResults.Diagnostics 0 "There should be no check diags" - let line = source.Lines[cursor.Line] + let line = source.Lines[int cursor.Line] let (col, idents) = Lexer.findIdents cursor.Character line SymbolLookupKind.Fuzzy |> Flip.Expect.wantSome "Should find idents" let symbolUse = - checkResults.GetSymbolUseAtLocation(cursor.Line + 1, col, line, List.ofArray idents) + checkResults.GetSymbolUseAtLocation(int cursor.Line + 1, int col, line, List.ofArray idents) |> Flip.Expect.wantSome "Should find symbol" let! ct = Async.CancellationToken @@ -721,7 +731,7 @@ let tryFixupRangeTests (sourceTextFactory: ISourceTextFactory) = let locs = ranges |> Array.map (fun r -> { Uri = ""; Range = r }) let marked = markRanges source.String locs // append range strings for additional range diff - let rangeStrs = ranges |> Seq.map (fun r -> r.DebuggerDisplay) |> String.concat "\n" + let rangeStrs = ranges |> Seq.map (fun r -> string r) |> String.concat "\n" marked + "\n" + "\n" + rangeStrs Expect.equal (markRanges actual) (markRanges expected) "Should be correct ranges" diff --git a/test/FsAutoComplete.Tests.Lsp/GoToTests.fs b/test/FsAutoComplete.Tests.Lsp/GoToTests.fs index 06085ed4d..22a3ec87d 100644 --- a/test/FsAutoComplete.Tests.Lsp/GoToTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/GoToTests.fs @@ -14,21 +14,24 @@ open Utils.TextEdit open Helpers.Expecto.ShadowedTimeouts let executeProcess (wd: string) (processName: string) (processArgs: string) = - let psi = new Diagnostics.ProcessStartInfo(processName, processArgs) - psi.UseShellExecute <- false - psi.RedirectStandardOutput <- true - psi.RedirectStandardError <- true - psi.CreateNoWindow <- true - psi.WorkingDirectory <- wd - let proc = Diagnostics.Process.Start(psi) - let output = new Text.StringBuilder() - let error = new Text.StringBuilder() - proc.OutputDataReceived.Add(fun args -> output.Append(args.Data) |> ignore) - proc.ErrorDataReceived.Add(fun args -> error.Append(args.Data) |> ignore) - proc.BeginErrorReadLine() - proc.BeginOutputReadLine() - proc.WaitForExit() - {| ExitCode = proc.ExitCode; StdOut = output.ToString(); StdErr = error.ToString() |} + let psi = new Diagnostics.ProcessStartInfo(processName, processArgs) + psi.UseShellExecute <- false + psi.RedirectStandardOutput <- true + psi.RedirectStandardError <- true + psi.CreateNoWindow <- true + psi.WorkingDirectory <- wd + let proc = Diagnostics.Process.Start(psi) + let output = new Text.StringBuilder() + let error = new Text.StringBuilder() + proc.OutputDataReceived.Add(fun args -> output.Append(args.Data) |> ignore) + proc.ErrorDataReceived.Add(fun args -> error.Append(args.Data) |> ignore) + proc.BeginErrorReadLine() + proc.BeginOutputReadLine() + proc.WaitForExit() + + {| ExitCode = proc.ExitCode + StdOut = output.ToString() + StdErr = error.ToString() |} ///GoTo tests let private gotoTest state = @@ -81,17 +84,17 @@ let private gotoTest state = let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 4; Character = 30 } } + Position = { Line = 4u; Character = 30u } } let! res = server.TextDocumentDefinition p match res with | Result.Error e -> failtestf "Request failed: %A" e | Result.Ok None -> failtest "Request none" - | Result.Ok(Some(GotoResult.Multiple _)) -> failtest "Should only get one location" - | Result.Ok(Some(GotoResult.Single r)) when r.Uri.EndsWith("startup", StringComparison.Ordinal) -> + | Result.Ok(Some(U2.C2 _t)) -> failtest "Should only get one location" + | Result.Ok(Some(U2.C1 r)) when r.Uri.EndsWith("startup", StringComparison.Ordinal) -> failtest "Should not generate the startup dummy file" - | Result.Ok(Some(GotoResult.Single r)) -> + | Result.Ok(Some(U2.C1 r)) -> Expect.stringEnds r.Uri ".cs" "should have generated a C# code file" Expect.stringContains @@ -109,7 +112,7 @@ let private gotoTest state = let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 2; Character = 15 } } + Position = { Line = 2u; Character = 15u } } let! res = server.TextDocumentDefinition p @@ -126,7 +129,7 @@ let private gotoTest state = let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 2; Character = 29 } } + Position = { Line = 2u; Character = 29u } } let! res = server.TextDocumentDefinition p @@ -135,14 +138,14 @@ let private gotoTest state = | Result.Ok None -> failtest "Request none" | Result.Ok(Some res) -> match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> + | U2.C2 _ -> failtest "Should be single GotoResult" + | U2.C1 res -> Expect.stringContains res.Uri "Definition.fs" "Result should be in Definition.fs" Expect.equal res.Range - { Start = { Line = 2; Character = 4 } - End = { Line = 2; Character = 16 } } + { Start = { Line = 2u; Character = 4u } + End = { Line = 2u; Character = 16u } } "Result should have correct range" }) @@ -153,7 +156,7 @@ let private gotoTest state = let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 4; Character = 24 } } + Position = { Line = 4u; Character = 24u } } let! res = server.TextDocumentDefinition p @@ -162,14 +165,14 @@ let private gotoTest state = | Result.Ok None -> failtest "Request none" | Result.Ok(Some res) -> match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> + | U2.C2 _ -> failtest "Should be single GotoResult" + | U2.C1 res -> Expect.stringContains res.Uri "Definition.fs" "Result should be in Definition.fs" Expect.equal res.Range - { Start = { Line = 6; Character = 4 } - End = { Line = 6; Character = 19 } } + { Start = { Line = 6u; Character = 4u } + End = { Line = 6u; Character = 19u } } "Result should have correct range" }) @@ -180,7 +183,7 @@ let private gotoTest state = let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri definitionPath } - Position = { Line = 8; Character = 11 } } + Position = { Line = 8u; Character = 11u } } let! res = server.TextDocumentImplementation p @@ -189,8 +192,8 @@ let private gotoTest state = | Result.Ok None -> failtest "Request none" | Result.Ok(Some res) -> match res with - | GotoResult.Single _res -> failtest "Should be multiple GotoResult" - | GotoResult.Multiple _res -> + | U2.C1 _res -> failtest "Should be multiple GotoResult" + | U2.C2 _res -> // TODO??? // Expect.exists res (fun r -> r.Uri.Contains "Library.fs" && r.Range = { Start = {Line = 7; Character = 8 }; End = {Line = 7; Character = 30 }}) "First result should be in Library.fs" // Expect.exists res (fun r -> r.Uri.Contains "Library.fs" && r.Range = { Start = {Line = 13; Character = 14 }; End = {Line = 13; Character = 36 }}) "Second result should be in Library.fs" @@ -205,7 +208,7 @@ let private gotoTest state = // check for the 'button' member in giraffe view engine let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 9; Character = 34 } } + Position = { Line = 9u; Character = 34u } } let! res = server.TextDocumentDefinition p @@ -214,8 +217,8 @@ let private gotoTest state = | Result.Ok None -> failtest "Request none" | Result.Ok(Some res) -> match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> + | U2.C2 _ -> failtest "Should be single GotoResult" + | U2.C1 res -> Expect.stringContains res.Uri "GiraffeViewEngine.fs" "Result should be in GiraffeViewEngine" let localPath = Path.FileUriToLocalPath res.Uri @@ -232,7 +235,7 @@ let private gotoTest state = // check for the 'List.concat' member in FSharp.Core let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 12; Character = 36 } } + Position = { Line = 12u; Character = 36u } } let! res = server.TextDocumentDefinition p @@ -241,8 +244,8 @@ let private gotoTest state = | Result.Ok None -> failtest "Request none" | Result.Ok(Some res) -> match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> + | U2.C2 _ -> failtest "Should be single GotoResult" + | U2.C1 res -> Expect.stringContains res.Uri "FSharp.Core/list.fs" "Result should be in FSharp.Core's list.fs" let localPath = Path.FileUriToLocalPath res.Uri @@ -260,7 +263,7 @@ let private gotoTest state = // check for the 'Stirng.Join' member in the BCL let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 14; Character = 79 } } + Position = { Line = 14u; Character = 79u } } let! res = server.TextDocumentDefinition p @@ -269,8 +272,8 @@ let private gotoTest state = | Result.Ok None -> failtest "Request none" | Result.Ok(Some res) -> match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> + | U2.C2 _ -> failtest "Should be single GotoResult" + | U2.C1 res -> let localPath = Path.FileUriToLocalPath res.Uri if @@ -293,7 +296,7 @@ let private gotoTest state = let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 26; Character = 23 } } + Position = { Line = 26u; Character = 23u } } let! res = server.TextDocumentDefinition p @@ -312,7 +315,7 @@ let private gotoTest state = let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 4; Character = 24 } } + Position = { Line = 4u; Character = 24u } } let! res = server.TextDocumentTypeDefinition p @@ -321,14 +324,14 @@ let private gotoTest state = | Result.Ok None -> failtest "Request none" | Result.Ok(Some res) -> match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> + | U2.C2 _ -> failtest "Should be single GotoResult" + | U2.C1 res -> Expect.stringContains res.Uri "Definition.fs" "Result should be in Definition.fs" Expect.equal res.Range - { Start = { Line = 4; Character = 5 } - End = { Line = 4; Character = 6 } } + { Start = { Line = 4u; Character = 5u } + End = { Line = 4u; Character = 6u } } "Result should have correct range" }) @@ -339,7 +342,7 @@ let private gotoTest state = let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 4; Character = 20 } } + Position = { Line = 4u; Character = 20u } } let! res = server.TextDocumentTypeDefinition p @@ -348,14 +351,14 @@ let private gotoTest state = | Result.Ok None -> failtest "Request none" | Result.Ok(Some res) -> match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> + | U2.C2 _ -> failtest "Should be single GotoResult" + | U2.C1 res -> Expect.stringContains res.Uri "Definition.fs" "Result should be in Definition.fs" Expect.equal res.Range - { Start = { Line = 4; Character = 5 } - End = { Line = 4; Character = 6 } } + { Start = { Line = 4u; Character = 5u } + End = { Line = 4u; Character = 6u } } "Result should have correct range" }) @@ -372,7 +375,7 @@ let private gotoTest state = *) let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 12; Character = 16 } } + Position = { Line = 12u; Character = 16u } } let! res = server.TextDocumentTypeDefinition p @@ -381,8 +384,8 @@ let private gotoTest state = | Result.Ok None -> failtest "Request none" | Result.Ok(Some res) -> match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> + | U2.C2 _ -> failtest "Should be single GotoResult" + | U2.C1 res -> Expect.stringContains res.Uri "FSharp.Core/prim-types" "Result should be in FSharp.Core's prim-types" let localPath = Path.FileUriToLocalPath res.Uri @@ -404,7 +407,7 @@ let private gotoTest state = *) let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 16; Character = 6 } } + Position = { Line = 16u; Character = 6u } } let! res = server.TextDocumentTypeDefinition p @@ -413,8 +416,8 @@ let private gotoTest state = | Result.Ok None -> failtest "Request none" | Result.Ok(Some res) -> match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> + | U2.C2 _ -> failtest "Should be single GotoResult" + | U2.C1 res -> let localPath = Path.FileUriToLocalPath res.Uri Expect.stringContains @@ -440,7 +443,7 @@ let private gotoTest state = *) let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 16; Character = 42 } } + Position = { Line = 16u; Character = 42u } } let! res = server.TextDocumentTypeDefinition p @@ -449,8 +452,8 @@ let private gotoTest state = | Result.Ok None -> failtest "Request none" | Result.Ok(Some res) -> match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> + | U2.C2 _ -> failtest "Should be single GotoResult" + | U2.C1 res -> let localPath = Path.FileUriToLocalPath res.Uri Expect.stringContains @@ -476,7 +479,7 @@ let private gotoTest state = *) let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 18; Character = 12 } } + Position = { Line = 18u; Character = 12u } } let! res = server.TextDocumentTypeDefinition p @@ -485,8 +488,8 @@ let private gotoTest state = | Result.Ok None -> failtest "Request none" | Result.Ok(Some res) -> match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> + | U2.C2 _ -> failtest "Should be single GotoResult" + | U2.C1 res -> Expect.stringContains res.Uri "FSharp.Core/prim-types" "Result should be in FSharp.Core's prim-types" let localPath = Path.FileUriToLocalPath res.Uri @@ -508,7 +511,7 @@ let private gotoTest state = *) let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 24; Character = 5 } } + Position = { Line = 24u; Character = 5u } } let! res = server.TextDocumentTypeDefinition p @@ -517,8 +520,8 @@ let private gotoTest state = | Result.Ok None -> failtest "Request none" | Result.Ok(Some res) -> match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> + | U2.C2 _ -> failtest "Should be single GotoResult" + | U2.C1 res -> Expect.stringContains res.Uri "FSharp.Core/prim-types" "Result should be in FSharp.Core's prim-types" let localPath = Path.FileUriToLocalPath res.Uri @@ -550,16 +553,15 @@ let private scriptGotoTests state = let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri scriptPath } - Position = { Line = 0; Character = 10 } } + Position = { Line = 0u; Character = 10u } } let! res = server.TextDocumentDefinition p match res with | Error e -> failtestf "Request failed: %A" e | Ok None -> failtest "Request none" - | Ok(Some(GotoResult.Multiple _)) -> failtest "Should only get one location" - | Ok(Some(GotoResult.Single r)) -> - Expect.stringEnds r.Uri "/simple.fsx" "should navigate to the mentioned script file" + | Ok(Some(U2.C2 _)) -> failtest "Should only get one location" + | Ok(Some(U2.C1 r)) -> Expect.stringEnds r.Uri "/simple.fsx" "should navigate to the mentioned script file" }) testCaseAsync "Go-to-definition on first char of identifier works" @@ -568,7 +570,7 @@ let private scriptGotoTests state = let p: TextDocumentPositionParams = { TextDocument = { Uri = Path.FilePathToUri scriptPath } - Position = { Line = 5; Character = 0 } // beginning of the usage of `testFunction` in the script file + Position = { Line = 5u; Character = 0u } // beginning of the usage of `testFunction` in the script file } let! res = server.TextDocumentDefinition p @@ -576,14 +578,14 @@ let private scriptGotoTests state = match res with | Error e -> failtestf "Request failed: %A" e | Ok None -> failtest "Request none" - | Ok(Some(GotoResult.Multiple _)) -> failtest "Should only get one location" - | Ok(Some(GotoResult.Single r)) -> + | Ok(Some(U2.C2 _)) -> failtest "Should only get one location" + | Ok(Some(U2.C1 r)) -> Expect.stringEnds r.Uri (Path.GetFileName scriptPath) "should navigate to the mentioned script file" Expect.equal r.Range - { Start = { Line = 3; Character = 4 } - End = { Line = 3; Character = 16 } } + { Start = { Line = 3u; Character = 4u } + End = { Line = 3u; Character = 16u } } "should point to the range of the definition of `testFunction`" }) ] @@ -616,8 +618,8 @@ let private untitledGotoTests state = match res with | Error e -> failtestf "Request failed: %A" e | Ok None -> failtest "Request none" - | Ok(Some(GotoResult.Multiple _)) -> failtest "Should only get one location" - | Ok(Some(GotoResult.Single r)) -> + | Ok(Some(U2.C2 _)) -> failtest "Should only get one location" + | Ok(Some(U2.C1 r)) -> Expect.stringEnds r.Uri doc.Uri "should navigate to source file" Expect.equal r.Range declRange "should point to the range of variable declaration" } @@ -647,8 +649,8 @@ let private untitledGotoTests state = match res with | Error e -> failtestf "Request failed: %A" e | Ok None -> failtest "Request none" - | Ok(Some(GotoResult.Multiple _)) -> failtest "Should only get one location" - | Ok(Some(GotoResult.Single r)) -> + | Ok(Some(U2.C2 _)) -> failtest "Should only get one location" + | Ok(Some(U2.C1 r)) -> Expect.stringEnds r.Uri doc.Uri "should navigate to source file" Expect.equal r.Range declRange "should point to the range of function declaration" } ]) diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fs b/test/FsAutoComplete.Tests.Lsp/Helpers.fs index 42ed4c770..0db75f198 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fs +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fs @@ -115,8 +115,10 @@ type DisposableDirectory(directory: string, deleteParentDir) = attempts <- 0 with _ -> attempts <- attempts - 1 + if attempts = 0 then reraise () + Thread.Sleep(15) @@ -225,7 +227,10 @@ let createAdaptiveServer workspaceLoader sourceTextFactory useTransparentCompile let loader = workspaceLoader () let client = FSharpLspClient(recordNotifications, recordRequests) - let server = new AdaptiveFSharpLspServer(loader, client, sourceTextFactory, useTransparentCompiler) + + let server = + new AdaptiveFSharpLspServer(loader, client, sourceTextFactory, useTransparentCompiler) + server :> IFSharpLspServer, serverInteractions :> ClientEvents let defaultConfigDto: FSharpConfigDto = @@ -290,17 +295,22 @@ let defaultConfigDto: FSharpConfigDto = Debug = None } let clientCaps: ClientCapabilities = - let dynCaps: DynamicCapabilities = { DynamicRegistration = Some true } + + let didChangeConfigCaps: DidChangeConfigurationClientCapabilities = + { DynamicRegistration = Some true } + + let executeCommandsClientCaps: ExecuteCommandClientCapabilities = + { DynamicRegistration = Some true } let workspaceCaps: WorkspaceClientCapabilities = - let weCaps: WorkspaceEditCapabilities = + let weCaps: WorkspaceEditClientCapabilities = { DocumentChanges = Some true ResourceOperations = None FailureHandling = None NormalizesLineEndings = None ChangeAnnotationSupport = None } - let symbolCaps: SymbolCapabilities = + let symbolCaps: WorkspaceSymbolClientCapabilities = { DynamicRegistration = Some true SymbolKind = None TagSupport = None @@ -320,92 +330,84 @@ let clientCaps: ClientCapabilities = { ApplyEdit = Some true WorkspaceEdit = Some weCaps - DidChangeConfiguration = Some dynCaps + DidChangeConfiguration = Some didChangeConfigCaps DidChangeWatchedFiles = None Symbol = Some symbolCaps SemanticTokens = Some semanticTokenCaps InlayHint = Some inlayHintCaps InlineValue = Some inlineValueCaps CodeLens = Some codeLensCaps - ExecuteCommand = Some dynCaps + ExecuteCommand = Some executeCommandsClientCaps WorkspaceFolders = Some false Configuration = Some true FileOperations = None Diagnostics = Some { RefreshSupport = Some false } } let textCaps: TextDocumentClientCapabilities = - let syncCaps: SynchronizationCapabilities = + let syncCaps: TextDocumentSyncClientCapabilities = { DynamicRegistration = Some true WillSave = Some true WillSaveWaitUntil = Some true DidSave = Some true } - let publishDiagCaps: PublishDiagnosticsCapabilities = - let diagnosticTags: DiagnosticTagSupport = { ValueSet = [||] } - + let publishDiagCaps: PublishDiagnosticsClientCapabilities = { RelatedInformation = Some true - TagSupport = Some diagnosticTags + TagSupport = Some { ValueSet = [||] } VersionSupport = Some false CodeDescriptionSupport = Some true DataSupport = Some false } - let ciCaps: CompletionItemCapabilities = - { SnippetSupport = Some true - CommitCharactersSupport = Some true - DocumentationFormat = None - DeprecatedSupport = Some false - PreselectSupport = Some false - TagSupport = None - InsertReplaceSupport = Some false - ResolveSupport = None - InsertTextModeSupport = None - LabelDetailsSupport = Some true } - - let cikCaps: CompletionItemKindCapabilities = { ValueSet = None } - - let compCaps: CompletionCapabilities = + let compCaps: CompletionClientCapabilities = { DynamicRegistration = Some true - CompletionItem = Some ciCaps - CompletionItemKind = Some cikCaps + CompletionItem = + Some + { SnippetSupport = Some true + CommitCharactersSupport = Some true + DocumentationFormat = None + DeprecatedSupport = Some false + PreselectSupport = Some false + TagSupport = None + InsertReplaceSupport = Some false + ResolveSupport = None + InsertTextModeSupport = None + LabelDetailsSupport = Some true } + CompletionItemKind = Some { ValueSet = None } ContextSupport = Some true InsertTextMode = Some InsertTextMode.AsIs CompletionList = Some { ItemDefaults = None } } - let hoverCaps: HoverCapabilities = + let hoverCaps: HoverClientCapabilities = { DynamicRegistration = Some true - ContentFormat = Some [| "markdown" |] } - - let sigCaps: SignatureHelpCapabilities = - let siCaps: SignatureInformationCapabilities = - { DocumentationFormat = Some [| "markdown" |] - ParameterInformation = Some { LabelOffsetSupport = Some true } - ActiveParameterSupport = Some true } + ContentFormat = Some [| MarkupKind.Markdown |] } + let sigCaps: SignatureHelpClientCapabilities = { DynamicRegistration = Some true - SignatureInformation = Some siCaps + SignatureInformation = + Some + { DocumentationFormat = Some [| MarkupKind.Markdown |] + ParameterInformation = Some { LabelOffsetSupport = Some true } + ActiveParameterSupport = Some true } ContextSupport = Some true } - let docSymCaps: DocumentSymbolCapabilities = - let skCaps: SymbolKindCapabilities = { ValueSet = None } - + let docSymCaps: DocumentSymbolClientCapabilities = { DynamicRegistration = Some true - SymbolKind = Some skCaps + SymbolKind = Some { ValueSet = None } HierarchicalDocumentSymbolSupport = Some false TagSupport = None LabelSupport = Some true } - let foldingRangeCaps: FoldingRangeCapabilities = + let foldingRangeCaps: FoldingRangeClientCapabilities = { DynamicRegistration = Some true LineFoldingOnly = Some true - RangeLimit = Some 100 + RangeLimit = Some 100u FoldingRange = Some { CollapsedText = Some true } FoldingRangeKind = None } let semanticTokensCaps: SemanticTokensClientCapabilities = { DynamicRegistration = Some true Requests = - { Range = Some true - Full = Some(U2.First true) } + { Range = Some(U2.C1 true) + Full = Some(U2.C1 true) } TokenTypes = [||] TokenModifiers = [||] Formats = [| TokenFormat.Relative |] @@ -428,8 +430,7 @@ let clientCaps: ClientCapabilities = ResolveSupport = None } let _inlineValueCaps: InlineValueClientCapabilities = - { DynamicRegistration = Some true - ResolveSupport = None } + { DynamicRegistration = Some true } let renameCaps: RenameClientCapabilities = { DynamicRegistration = Some true @@ -437,66 +438,97 @@ let clientCaps: ClientCapabilities = PrepareSupport = Some false PrepareSupportDefaultBehavior = Some PrepareSupportDefaultBehavior.Identifier } - let linkCaps: DynamicLinkSupportCapabilities = + let referenceCaps: ReferenceClientCapabilities = { DynamicRegistration = Some true } + + let declarationCaps: DeclarationClientCapabilities = { DynamicRegistration = Some true LinkSupport = Some false } - let defCaps: DynamicLinkSupportCapabilities = + let defCaps: DefinitionClientCapabilities = { DynamicRegistration = Some true LinkSupport = Some false } - let typeDefCaps: DynamicLinkSupportCapabilities = + let typeDefCaps: TypeDefinitionClientCapabilities = { DynamicRegistration = Some true LinkSupport = Some false } - let implCaps: DynamicLinkSupportCapabilities = + let implCaps: ImplementationClientCapabilities = { DynamicRegistration = Some true LinkSupport = Some false } - let docLinkCaps: DocumentLinkCapabilities = + let docLinkCaps: DocumentLinkClientCapabilities = { DynamicRegistration = Some true TooltipSupport = Some true } - let diagCaps: DiagnosticCapabilities = + let diagCaps: DiagnosticClientCapabilities = { DynamicRegistration = Some true RelatedDocumentSupport = Some true } + let highlightCaps: DocumentHighlightClientCapabilities = + { DynamicRegistration = Some true } + + let formattingCaps: DocumentFormattingClientCapabilities = + { DynamicRegistration = Some true } + + let rangeFormattingCaps: DocumentRangeFormattingClientCapabilities = + { DynamicRegistration = Some true } + + let onTypeFormattingCaps: DocumentOnTypeFormattingClientCapabilities = + { DynamicRegistration = Some true } + + let codeLensCaps: CodeLensClientCapabilities = { DynamicRegistration = Some true } + + let selectionRangeCaps: SelectionRangeClientCapabilities = + { DynamicRegistration = Some true } + + let monikerCaps: MonikerClientCapabilities = { DynamicRegistration = Some true } + + let colorProviderCaps: DocumentColorClientCapabilities = + { DynamicRegistration = Some true } + + let linkedEditingRangeCaps: LinkedEditingRangeClientCapabilities = + { DynamicRegistration = Some true } + + let inlineValueCaps: InlineValueClientCapabilities = + { DynamicRegistration = Some true } + { Synchronization = Some syncCaps PublishDiagnostics = Some publishDiagCaps Completion = Some compCaps Hover = Some hoverCaps SignatureHelp = Some sigCaps - References = Some dynCaps - DocumentHighlight = Some dynCaps + References = Some referenceCaps + DocumentHighlight = Some highlightCaps DocumentSymbol = Some docSymCaps - Formatting = Some dynCaps - RangeFormatting = Some dynCaps - OnTypeFormatting = Some dynCaps + Formatting = Some formattingCaps + RangeFormatting = Some rangeFormattingCaps + OnTypeFormatting = Some onTypeFormattingCaps Definition = Some defCaps CodeAction = Some codeActionCaps - CodeLens = Some dynCaps + CodeLens = Some codeLensCaps DocumentLink = Some docLinkCaps Rename = Some renameCaps FoldingRange = Some foldingRangeCaps - SelectionRange = Some dynCaps + SelectionRange = Some selectionRangeCaps SemanticTokens = Some semanticTokensCaps InlayHint = Some inlayHintCaps CallHierarchy = None TypeHierarchy = None - Declaration = Some linkCaps + Declaration = Some declarationCaps TypeDefinition = Some typeDefCaps Implementation = Some implCaps - ColorProvider = Some dynCaps - LinkedEditingRange = Some dynCaps - Moniker = Some dynCaps - InlineValue = Some dynCaps + ColorProvider = Some colorProviderCaps + LinkedEditingRange = Some linkedEditingRangeCaps + Moniker = Some monikerCaps + InlineValue = Some inlineValueCaps Diagnostic = Some diagCaps } { Workspace = Some workspaceCaps TextDocument = Some textCaps Experimental = None Window = None - General = None } + General = None + NotebookDocument = None } open Expecto.Logging open Expecto.Logging.Message @@ -567,7 +599,7 @@ let serverInitialize path (config: FSharpConfigDto) createServer = RootPath = Some path RootUri = Some(sprintf "file://%s" path) InitializationOptions = Some(Server.serialize config) - Capabilities = Some clientCaps + Capabilities = clientCaps ClientInfo = Some { Name = "FSAC Tests" @@ -576,14 +608,15 @@ let serverInitialize path (config: FSharpConfigDto) createServer = Some [| { Uri = Path.FilePathToUri path Name = "Test Folder" } |] - trace = None - Locale = None } + Trace = None + Locale = None + WorkDoneToken = None } let! result = server.Initialize p match result with | Result.Ok _res -> - do! server.Initialized(InitializedParams()) + do! server.Initialized() return (server, clientNotifications) | Result.Error _e -> return failwith "Initialization failed" } @@ -662,7 +695,10 @@ let editsFor (file: string) = let forFile = edits - |> Array.collect (fun e -> if e.TextDocument.Uri = fileAsUri then e.Edits else [||]) + |> Array.collect (fun e -> + match e with + | U4.C1 e -> if e.TextDocument.Uri = fileAsUri then e.Edits else [||] + | _ -> [||]) match forFile with | [||] -> None @@ -758,7 +794,7 @@ let (|CodeActions|_|) (t: TextDocumentCodeActionResult) = let actions = t |> Array.choose (function - | U2.Second action -> Some action + | U2.C2 action -> Some action | _ -> None) match actions with diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fsi b/test/FsAutoComplete.Tests.Lsp/Helpers.fsi index 2303333d4..f1880de2c 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fsi +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fsi @@ -13,64 +13,64 @@ open System.Threading open FSharp.UMX module Expecto = - open System.Threading.Tasks - val inline testBuilderWithTimeout: ts: TimeSpan -> name: string -> testCase: TestCode -> focus: FocusState -> Test - val inline testCaseWithTimeout: ts: TimeSpan -> name: string -> test: (unit -> unit) -> Test - val inline ftestCaseWithTimeout: ts: TimeSpan -> name: string -> test: (unit -> unit) -> Test - val inline ptestCaseWithTimeout: ts: TimeSpan -> name: string -> test: (unit -> unit) -> Test - val inline testCaseAsyncWithTimeout: ts: TimeSpan -> name: string -> test: Async -> Test - val inline ftestCaseAsyncWithTimeout: ts: TimeSpan -> name: string -> test: Async -> Test - val inline ptestCaseAsyncWithTimeout: ts: TimeSpan -> name: string -> test: Async -> Test - val inline testCaseTaskWithTimeout: ts: TimeSpan -> name: string -> test: (unit -> Task) -> Test - val inline ftestCaseTaskWithTimeout: ts: TimeSpan -> name: string -> test: (unit -> Task) -> Test - val inline ptestCaseTaskWithTimeout: ts: TimeSpan -> name: string -> test: (unit -> Task) -> Test - val DEFAULT_TIMEOUT: TimeSpan - - /// Contains testCase functions that have a `DEFAULT_TIMEOUT` set to them - module ShadowedTimeouts = - val testCase: (string -> (unit -> unit) -> Test) - val ptestCase: (string -> (unit -> unit) -> Test) - val ftestCase: (string -> (unit -> unit) -> Test) - val testCaseAsync: (string -> Async -> Test) - val ptestCaseAsync: (string -> Async -> Test) - val ftestCaseAsync: (string -> Async -> Test) + open System.Threading.Tasks + val inline testBuilderWithTimeout: ts: TimeSpan -> name: string -> testCase: TestCode -> focus: FocusState -> Test + val inline testCaseWithTimeout: ts: TimeSpan -> name: string -> test: (unit -> unit) -> Test + val inline ftestCaseWithTimeout: ts: TimeSpan -> name: string -> test: (unit -> unit) -> Test + val inline ptestCaseWithTimeout: ts: TimeSpan -> name: string -> test: (unit -> unit) -> Test + val inline testCaseAsyncWithTimeout: ts: TimeSpan -> name: string -> test: Async -> Test + val inline ftestCaseAsyncWithTimeout: ts: TimeSpan -> name: string -> test: Async -> Test + val inline ptestCaseAsyncWithTimeout: ts: TimeSpan -> name: string -> test: Async -> Test + val inline testCaseTaskWithTimeout: ts: TimeSpan -> name: string -> test: (unit -> Task) -> Test + val inline ftestCaseTaskWithTimeout: ts: TimeSpan -> name: string -> test: (unit -> Task) -> Test + val inline ptestCaseTaskWithTimeout: ts: TimeSpan -> name: string -> test: (unit -> Task) -> Test + val DEFAULT_TIMEOUT: TimeSpan + + /// Contains testCase functions that have a `DEFAULT_TIMEOUT` set to them + module ShadowedTimeouts = + val testCase: (string -> (unit -> unit) -> Test) + val ptestCase: (string -> (unit -> unit) -> Test) + val ftestCase: (string -> (unit -> unit) -> Test) + val testCaseAsync: (string -> Async -> Test) + val ptestCaseAsync: (string -> Async -> Test) + val ftestCaseAsync: (string -> Async -> Test) type DisposableDirectory = - new: directory: string * deleteParentDir : bool -> DisposableDirectory - static member Create: ?name : string -> DisposableDirectory - static member From: sourceDir: DirectoryInfo -> DisposableDirectory - member DirectoryInfo: DirectoryInfo - interface IDisposable + new: directory: string * deleteParentDir: bool -> DisposableDirectory + static member Create: ?name: string -> DisposableDirectory + static member From: sourceDir: DirectoryInfo -> DisposableDirectory + member DirectoryInfo: DirectoryInfo + interface IDisposable [] type Async = - /// Behaves like AwaitObservable, but calls the specified guarding function - /// after a subscriber is registered with the observable. - static member GuardedAwaitObservable: ev1: IObservable<'T1> -> guardFunction: (unit -> unit) -> Async<'T1> - /// Creates an asynchronous workflow that will be resumed when the - /// specified observables produces a value. The workflow will return - /// the value produced by the observable. - static member AwaitObservable: ev1: IObservable<'T1> -> Async<'T1> - /// Creates an asynchronous workflow that runs the asynchronous workflow - /// given as an argument at most once. When the returned workflow is - /// started for the second time, it reuses the result of the - /// previous execution. - static member Cache: input: Async<'T> -> Async<'T> + /// Behaves like AwaitObservable, but calls the specified guarding function + /// after a subscriber is registered with the observable. + static member GuardedAwaitObservable: ev1: IObservable<'T1> -> guardFunction: (unit -> unit) -> Async<'T1> + /// Creates an asynchronous workflow that will be resumed when the + /// specified observables produces a value. The workflow will return + /// the value produced by the observable. + static member AwaitObservable: ev1: IObservable<'T1> -> Async<'T1> + /// Creates an asynchronous workflow that runs the asynchronous workflow + /// given as an argument at most once. When the returned workflow is + /// started for the second time, it reuses the result of the + /// previous execution. + static member Cache: input: Async<'T> -> Async<'T> val logger: Lazy type Cacher<'t> = System.Reactive.Subjects.ReplaySubject<'t> type ClientEvents = IObservable module Range = - val rangeContainsPos: range: Range -> pos: Position -> bool + val rangeContainsPos: range: Range -> pos: Position -> bool val record: cacher: Cacher<'a * 'b> -> ('a -> 'b -> AsyncLspResult<'c>) val createAdaptiveServer: - workspaceLoader: (unit -> #Ionide.ProjInfo.IWorkspaceLoader) - -> sourceTextFactory: ISourceTextFactory - -> useTransparentCompiler : bool - -> IFSharpLspServer * ClientEvents + workspaceLoader: (unit -> #Ionide.ProjInfo.IWorkspaceLoader) -> + sourceTextFactory: ISourceTextFactory -> + useTransparentCompiler: bool -> + IFSharpLspServer * ClientEvents val defaultConfigDto: FSharpConfigDto val clientCaps: ClientCapabilities @@ -91,11 +91,11 @@ val dotnetRestore: dir: string -> Async val dotnetToolRestore: dir: string -> Async val serverInitialize: - path: string -> - config: FSharpConfigDto -> - createServer: (unit -> IFSharpLspServer * 'a) -> - Async - when 'a :> IObservable<'b * 'c> + path: string -> + config: FSharpConfigDto -> + createServer: (unit -> IFSharpLspServer * 'a) -> + Async + when 'a :> IObservable<'b * 'c> val loadDocument: path: string -> TextDocumentItem val parseProject: projectFilePath: string -> server: IFSharpLspServer -> Async @@ -104,7 +104,10 @@ val internal defaultTimeout: TimeSpan val waitForWorkspaceFinishedParsing: events: ClientEvents -> Async val workspaceEdits: (IObservable -> IObservable) -val editsFor: file: string -> (IObservable -> IObservable) + +val editsFor: + file: string -> (IObservable -> IObservable array>) + val fileDiagnostics: file: string -> (IObservable -> IObservable) val fileDiagnosticsForUri: uri: string -> (IObservable -> IObservable) val diagnosticsFromSource: desiredSource: String -> (IObservable -> IObservable) @@ -118,11 +121,11 @@ val waitForDiagnosticErrorForFile: file: string -> (IObservable -> val waitForFsacDiagnosticsForFile: file: string -> (IObservable -> Async>) val waitForCompilerDiagnosticsForFile: - file: string -> (IObservable -> Async>) + file: string -> (IObservable -> Async>) val waitForParsedScript: event: ClientEvents -> Async val waitForTestDetected: fileName: string -> events: ClientEvents -> Async -val waitForEditsForFile: file: string -> (IObservable -> Async) +val waitForEditsForFile: file: string -> (IObservable -> Async array>) val trySerialize: t: string -> 't option val (|As|_|): m: PlainNotification -> 't option val (|CodeActions|_|): t: TextDocumentCodeActionResult -> CodeAction array option diff --git a/test/FsAutoComplete.Tests.Lsp/HighlightingTests.fs b/test/FsAutoComplete.Tests.Lsp/HighlightingTests.fs index 78c93951a..1910017f4 100644 --- a/test/FsAutoComplete.Tests.Lsp/HighlightingTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/HighlightingTests.fs @@ -22,8 +22,7 @@ let tests state = match! waitForParseResultsForFile "Script.fsx" event with | Ok() -> return server | Error errors -> - let errorStrings = - errors |> Array.map (fun e -> e.DebuggerDisplay) |> String.concat "\n\t* " + let errorStrings = errors |> Array.map (fun e -> string e) |> String.concat "\n\t* " return failtestf "Errors while parsing highlighting script:\n\t* %s" errorStrings } @@ -57,9 +56,11 @@ let tests state = let range = { Start = - { Line = startLine - Character = startCol } - End = { Line = endLine; Character = endCol } } + { Line = uint32 startLine + Character = uint32 startCol } + End = + { Line = uint32 endLine + Character = uint32 endCol } } range, tokenType, tokenMods) @@ -68,7 +69,9 @@ let tests state = let fullHighlights = async { let p: SemanticTokensParams = - { TextDocument = { Uri = Path.FilePathToUri scriptPath } } + { TextDocument = { Uri = Path.FilePathToUri scriptPath } + WorkDoneToken = None + PartialResultToken = None } let! server = server let! highlights = server.TextDocumentSemanticTokensFull p @@ -124,7 +127,9 @@ let tests state = match! server.TextDocumentSemanticTokensRange { Range = range - TextDocument = { Uri = Path.FilePathToUri scriptPath } } + TextDocument = { Uri = Path.FilePathToUri scriptPath } + WorkDoneToken = None + PartialResultToken = None } with | Ok(Some highlights) -> let decoded = decodeHighlighting highlights.Data @@ -141,9 +146,9 @@ let tests state = "Document Highlighting Tests" [ testList "tests" - [ tokenIsOfType (0, 29) ClassificationUtils.SemanticTokenTypes.TypeParameter fullHighlights // the `^a` type parameter in the SRTP constraint - tokenIsOfType (0, 44) ClassificationUtils.SemanticTokenTypes.Member fullHighlights // the `PeePee` member in the SRTP constraint - tokenIsOfType (3, 52) ClassificationUtils.SemanticTokenTypes.Type fullHighlights // the `string` type annotation in the PooPoo srtp member - tokenIsOfType (6, 21) ClassificationUtils.SemanticTokenTypes.EnumMember fullHighlights // the `PeePee` AP application in the `yeet` function definition - tokenIsOfType (9, 10) ClassificationUtils.SemanticTokenTypes.Type fullHighlights //the `SomeJson` type alias should be a type - tokenIsOfType (15, 2) ClassificationUtils.SemanticTokenTypes.Module fullHighlights ] ] // tests that module coloration isn't overwritten by function coloration when a module function is used, so Foo in Foo.x should be module-colored + [ tokenIsOfType (0u, 29u) ClassificationUtils.SemanticTokenTypes.TypeParameter fullHighlights // the `^a` type parameter in the SRTP constraint + tokenIsOfType (0u, 44u) ClassificationUtils.SemanticTokenTypes.Member fullHighlights // the `PeePee` member in the SRTP constraint + tokenIsOfType (3u, 52u) ClassificationUtils.SemanticTokenTypes.Type fullHighlights // the `string` type annotation in the PooPoo srtp member + tokenIsOfType (6u, 21u) ClassificationUtils.SemanticTokenTypes.EnumMember fullHighlights // the `PeePee` AP application in the `yeet` function definition + tokenIsOfType (9u, 10u) ClassificationUtils.SemanticTokenTypes.Type fullHighlights //the `SomeJson` type alias should be a type + tokenIsOfType (15u, 2u) ClassificationUtils.SemanticTokenTypes.Module fullHighlights ] ] // tests that module coloration isn't overwritten by function coloration when a module function is used, so Foo in Foo.x should be module-colored diff --git a/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs b/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs index 078535b59..794efb1a8 100644 --- a/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs @@ -36,7 +36,7 @@ let docFormattingTest state = let! doc = server.FSharpDocumentation { TextDocument = { Uri = path } - Position = { Character = 5; Line = 0 } } // Map.map + Position = { Character = 5u; Line = 0u } } // Map.map match doc with | Result.Error err -> failtest $"Doc error: {err.Message}" @@ -53,7 +53,7 @@ let docFormattingTest state = let! doc = server.FSharpDocumentation { TextDocument = { Uri = path } - Position = { Character = 7; Line = 1 } } // List.unzip3 + Position = { Character = 7u; Line = 1u } } // List.unzip3 match doc with | Result.Error err -> failtest $"Doc error: {err.Message}" diff --git a/test/FsAutoComplete.Tests.Lsp/InlayHintTests.fs b/test/FsAutoComplete.Tests.Lsp/InlayHintTests.fs index 55321d0a0..5e190ca96 100644 --- a/test/FsAutoComplete.Tests.Lsp/InlayHintTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/InlayHintTests.fs @@ -20,8 +20,8 @@ module private FSharpInlayHints = let private at (text, pos, kind, edits) : InlayHint = { Label = match kind with - | InlayHintKind.Type -> InlayHintLabel.String(": " + InlayHints.truncated text) - | InlayHintKind.Parameter -> InlayHintLabel.String(InlayHints.truncated text + " =") + | InlayHintKind.Type -> U2.C1(": " + InlayHints.truncated text) + | InlayHintKind.Parameter -> U2.C1(InlayHints.truncated text + " =") | _ -> failwith $"unknown hint kind %O{kind}" // this is for truncated text, which we do not currently hit in our tests TextEdits = edits |> Option.map List.toArray @@ -109,20 +109,20 @@ module private FSharpInlayHints = [ ty "int" [ { Range = - { Start = { Line = 0; Character = 6 } - End = { Line = 0; Character = 6 } } + { Start = { Line = 0u; Character = 6u } + End = { Line = 0u; Character = 6u } } NewText = "(" } { Range = - { Start = { Line = 0; Character = 10 } - End = { Line = 0; Character = 10 } } + { Start = { Line = 0u; Character = 10u } + End = { Line = 0u; Character = 10u } } NewText = ": " } { Range = - { Start = { Line = 0; Character = 10 } - End = { Line = 0; Character = 10 } } + { Start = { Line = 0u; Character = 10u } + End = { Line = 0u; Character = 10u } } NewText = "int" } { Range = - { Start = { Line = 0; Character = 10 } - End = { Line = 0; Character = 10 } } + { Start = { Line = 0u; Character = 10u } + End = { Line = 0u; Character = 10u } } NewText = ")" } ] ] ] testList @@ -156,7 +156,7 @@ module private LspInlayHints = diags |> Array.filter (function // Ignore extra parens diagnostics here. - | { Code = Some "FSAC0004" } -> false + | { Code = Some(U2.C2 "FSAC0004") } -> false | _ -> true) use doc = doc @@ -197,7 +197,7 @@ module private LspInlayHints = | Some edits, Some textAfterEdits -> let appliedText = text - |> TextEdits.applyWithErrorCheck (edits |> List.ofArray) + |> TextEdits.applyWithErrorCheck edits |> Flip.Expect.wantOk "TextEdits are erroneous" Expect.equal appliedText textAfterEdits "Text after applying TextEdits does not match expected" @@ -250,12 +250,12 @@ module private LspInlayHints = do! checkInRange server text range validateHints } - let private fromCursor: Position = { Line = -1; Character = -1 } + let private fromCursor: Position = { Line = 0u; Character = 0u } let private mkBasicHint (kind: InlayHintKind) (pos: Position) (label: string) : InlayHint = { Kind = Some kind Position = pos - Label = InlayHintLabel.String label + Label = U2.C1 label TextEdits = None Tooltip = None PaddingLeft = @@ -282,7 +282,7 @@ module private LspInlayHints = let truncated (hint: InlayHint, edits) = let label = match hint.Label with - | InlayHintLabel.String label -> label + | U2.C1 label -> label | _ -> failtestf "invalid label: %A" hint.Label let (name, kind) = @@ -305,8 +305,8 @@ module private LspInlayHints = | InlayHintKind.Parameter -> truncatedName + " =" | InlayHintKind.Type -> ": " + truncatedName | _ -> failwith "unreachable" - |> InlayHintLabel.String - Tooltip = InlayHintTooltip.String name |> Some } + |> U2.C1 + Tooltip = U2.C1 name |> Some } (hint, edits) @@ -1410,8 +1410,8 @@ module InlayHintAndExplicitType = let tryGetInlayHintAt pos doc = async { let allRange = - { Start = { Line = 0; Character = 0 } - End = { Line = 1234; Character = 1234 } } + { Start = { Line = 0u; Character = 0u } + End = { Line = 1234u; Character = 1234u } } let! hints = doc |> Document.inlayHintsAt allRange return hints |> Array.tryFind (fun h -> h.Position = pos) @@ -1478,7 +1478,7 @@ module InlayHintAndExplicitType = let recheckAfterAppliedEdits (doc: Document) (cursorBeforeEdits: Position) - (edits: TextEdit list) + (edits: TextEdit[]) (textAfterEdits: string) = async { @@ -1501,8 +1501,8 @@ module InlayHintAndExplicitType = let actual = match inlayHint.Label with - | InlayHintLabel.String lbl -> lbl - | InlayHintLabel.Parts parts -> parts |> Array.map (fun part -> part.Value) |> String.concat "" + | U2.C1 lbl -> lbl + | U2.C2 parts -> parts |> Array.map (fun part -> part.Value) |> String.concat "" let actual = let actual = actual.TrimStart() @@ -1519,8 +1519,7 @@ module InlayHintAndExplicitType = | Edit textAfterEdits -> let inlayHint = Expect.wantSome inlayHint "There should be a Inlay Hint" - let edits = - Expect.wantSome inlayHint.TextEdits "There should be TextEdits" |> List.ofArray + let edits = Expect.wantSome inlayHint.TextEdits "There should be TextEdits" let actual = text |> TextEdits.apply edits |> Flip.Expect.wantOk "TextEdits should succeed" diff --git a/test/FsAutoComplete.Tests.Lsp/InlineHintsTests.fs b/test/FsAutoComplete.Tests.Lsp/InlineHintsTests.fs index 17bc613bd..d0e456580 100644 --- a/test/FsAutoComplete.Tests.Lsp/InlineHintsTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/InlineHintsTests.fs @@ -10,29 +10,25 @@ open Utils.TextEdit open Utils.ServerTests open Helpers.Expecto.ShadowedTimeouts -let private testInlineHints (server : CachedServer) text checkResp = +let private testInlineHints (server: CachedServer) text checkResp = async { - let range, text = - text - |> Text.trimTripleQuotation - |> Cursor.assertExtractRange + let range, text = text |> Text.trimTripleQuotation |> Cursor.assertExtractRange let! doc, _diags = server |> Server.createUntitledDocument text use doc = doc - let inlineValueRequest: InlineValueParams = { - Context = { - FrameId = 0 - StoppedLocation = range - } - Range = range - TextDocument = doc.TextDocumentIdentifier - } + let inlineValueRequest: InlineValueParams = + { Context = { FrameId = 0; StoppedLocation = range } + Range = range + TextDocument = doc.TextDocumentIdentifier + WorkDoneToken = None } + let! resp = doc.Server.Server.TextDocumentInlineValue inlineValueRequest checkResp resp } -let good = """ +let good = + """ let test value = $0 value @@ -44,26 +40,20 @@ value "foo" """ let tests state = - serverTestList "inline hints" state defaultConfigDto None (fun server -> [ - testCaseAsync - "Gets inline hints for simple pipeline" - (testInlineHints - server - good - (fun resp -> + serverTestList "inline hints" state defaultConfigDto None (fun server -> + [ testCaseAsync + "Gets inline hints for simple pipeline" + (testInlineHints server good (fun resp -> Expect.isOk resp "should get inline hints for valid code fragment" let resp = resp |> Result.defaultWith (fun _ -> failwith "Unpossible!") Expect.isSome resp "should get inline hints for valid code fragment" let resp = resp |> Option.get Expect.hasLength resp 4 "THERE ARE FOUR LIGHTS!!" + let hints = resp - |> Array.choose ( - function - | InlineValue.InlineValueText hint -> Some hint.Text - | _ -> None - ) - Expect.containsAll hints [nameof string; nameof int] "should be 3 strings and an int" - ) - ) - ]) + |> Array.choose (function + | U3.C1 hint -> Some hint.Text + | _ -> None) + + Expect.containsAll hints [ nameof string; nameof int ] "should be 3 strings and an int")) ]) diff --git a/test/FsAutoComplete.Tests.Lsp/RenameTests.fs b/test/FsAutoComplete.Tests.Lsp/RenameTests.fs index 671adcc2a..91a15c819 100644 --- a/test/FsAutoComplete.Tests.Lsp/RenameTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/RenameTests.fs @@ -16,6 +16,46 @@ open Helpers.Expecto.ShadowedTimeouts let private normalizePathCasing = Path.FilePathToUri >> Path.FileUriToLocalPath >> Path.FilePathToUri +let renameRaw ident pos newName = + { TextDocument = ident + Position = pos + NewName = newName + WorkDoneToken = None } + +let rename (doc: Document) = renameRaw doc.TextDocumentIdentifier + +let pos l c = { Line = l; Character = c } + +module Expect = + [] + let private (|ForFile|_|) (fileName: string) (edit: TextDocumentEdit) = + if edit.TextDocument.Uri.Contains fileName then + ValueSome() + else + ValueNone + + [] + let private (|HasEditIn|_|) (range: Ionide.LanguageServerProtocol.Types.Range) (edit: TextDocumentEdit) = + if + edit.Edits + |> Array.exists (fun r -> + match r with + | U2.C1 e -> e.Range = range + | U2.C2 e -> e.Range = range) + then + ValueSome() + else + ValueNone + + let hasEditAtRange (edits: U4 array) fileName range = + Expect.exists + edits + (fun n -> + match n with + | U4.C1(ForFile fileName & HasEditIn range) -> true + | _ -> false) + $"Rename contains changes in %s{fileName}" + let private sameProjectTests state = let testDir = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "RenameTest", "SameProject") @@ -27,10 +67,7 @@ let private sameProjectTests state = let! (doc, diags) = server |> Server.openDocument "Program.fs" Expect.isEmpty diags "There should be no errors in the checked file" - let p: RenameParams = - { TextDocument = doc.TextDocumentIdentifier - Position = { Line = 7; Character = 12 } - NewName = "y" } + let p: RenameParams = rename doc (pos 7u 12u) "y" let! server = server let! res = server.Server.TextDocumentRename p @@ -44,27 +81,17 @@ let private sameProjectTests state = | Some result -> Expect.equal result.Length 2 "Rename has all changes" - Expect.exists + Expect.hasEditAtRange result - (fun n -> - n.TextDocument.Uri.Contains "Program.fs" - && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 7; Character = 12 } - End = { Line = 7; Character = 13 } })) - "Rename contains changes in Program.fs" - - Expect.exists + "Program.fs" + { Start = { Line = 7u; Character = 12u } + End = { Line = 7u; Character = 13u } } + + Expect.hasEditAtRange result - (fun n -> - n.TextDocument.Uri.Contains "Test.fs" - && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 2; Character = 4 } - End = { Line = 2; Character = 5 } })) - "Rename contains changes in Test.fs" - - () + "Test.fs" + { Start = { Line = 2u; Character = 4u } + End = { Line = 2u; Character = 5u } } }) testCaseAsync @@ -77,10 +104,7 @@ let private sameProjectTests state = Expect.isEmpty programDiags "There should be no errors in Program.fs" Expect.isEmpty testDiags "There should be no errors in Test.fs" - let p: RenameParams = - { TextDocument = testDoc.TextDocumentIdentifier - Position = { Line = 2; Character = 4 } - NewName = "y" } + let p: RenameParams = rename testDoc (pos 2u 4u) "y" let! server = server let! res = server.Server.TextDocumentRename p @@ -95,27 +119,17 @@ let private sameProjectTests state = // TODO Expect.equal result.Length 2 "Rename has all changes" - Expect.exists + Expect.hasEditAtRange result - (fun n -> - n.TextDocument.Uri.Contains "Program.fs" - && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 7; Character = 12 } - End = { Line = 7; Character = 13 } })) - "Rename contains changes in Program.fs" - - Expect.exists + "Program.fs" + { Start = { Line = 7u; Character = 12u } + End = { Line = 7u; Character = 13u } } + + Expect.hasEditAtRange result - (fun n -> - n.TextDocument.Uri.Contains "Test.fs" - && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 2; Character = 4 } - End = { Line = 2; Character = 5 } })) - "Rename contains changes in Test.fs" - - () + "Test.fs" + { Start = { Line = 2u; Character = 4u } + End = { Line = 2u; Character = 5u } } }) ]) @@ -132,10 +146,7 @@ let private sameScriptTests state = use doc = doc Expect.isEmpty diags "There should be no diags" - let p: RenameParams = - { TextDocument = doc.TextDocumentIdentifier - Position = cursor - NewName = newName } + let p: RenameParams = rename doc cursor newName let! res = doc.Server.Server.TextDocumentRename p @@ -152,18 +163,27 @@ let private sameScriptTests state = | Result.Ok edits -> failtestf "got some unexpected edits: %A" edits Expect.hasLength edits 1 "should have just one file worth of edits" - let edit = edits.[0] - Expect.equal edit.TextDocument.Uri doc.Uri "should be for this file" - let edits = edit.Edits |> List.ofArray |> TextEdits.sortByRange - let actual = - text - |> TextEdits.applyWithErrorCheck edits - |> Flip.Expect.wantOk "TextEdits should be valid" + match edits.[0] with + | U4.C1 edit -> + Expect.equal edit.TextDocument.Uri doc.Uri "should be for this file" + + let edits = + edit.Edits + |> Array.choose (function + | U2.C1 x -> Some x + | _ -> None) + |> TextEdits.sortByRange - let expected = expectedText |> Text.trimTripleQuotation + let actual = + text + |> TextEdits.applyWithErrorCheck edits + |> Flip.Expect.wantOk "TextEdits should be valid" - Expect.equal actual expected "Text after TextEdits should be correct" + let expected = expectedText |> Text.trimTripleQuotation + + Expect.equal actual expected "Text after TextEdits should be correct" + | _ -> failtest "should be a text document edit" } let checkRename textWithCursor newName expectedText = checkRename' textWithCursor newName (Some expectedText) @@ -288,9 +308,8 @@ let private crossProjectTests state = // now, request renames let renameHelloUsageInUsageFile: RenameParams = - { TextDocument = { Uri = normalizePathCasing usageFile } - Position = { Line = 6; Character = 28 } - NewName = "sup" } + renameRaw { Uri = normalizePathCasing usageFile } (pos 6u 28u) "sup" + let! res = server.TextDocumentRename(renameHelloUsageInUsageFile) match res with @@ -299,29 +318,18 @@ let private crossProjectTests state = | Result.Ok(Some { DocumentChanges = Some edits }) -> Expect.equal edits.Length 2 "Rename has the correct expected edits" - Expect.exists + Expect.hasEditAtRange edits - (fun n -> - n.TextDocument.Uri.Contains "LibA" - && n.TextDocument.Uri.Contains "Library.fs" - && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 3; Character = 8 } - End = { Line = 3; Character = 13 } } - && r.NewText = "sup")) - "Rename contains changes in LibA" - - Expect.exists + "LibA/Library.fs" + { Start = { Line = 3u; Character = 8u } + End = { Line = 3u; Character = 13u } } + + Expect.hasEditAtRange edits - (fun n -> - n.TextDocument.Uri.Contains "LibB" - && n.TextDocument.Uri.Contains "Library.fs" - && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 6; Character = 28 } - End = { Line = 6; Character = 33 } } - && r.NewText = "sup")) - "Rename contains changes in LibB" + "LibB/Library.fs" + { Start = { Line = 6u; Character = 28u } + End = { Line = 6u; Character = 33u } } + | Result.Ok edits -> failtestf "got some unexpected edits: %A" edits }) testCaseAsync @@ -341,9 +349,7 @@ let private crossProjectTests state = // now, request renames let renameHelloUsageInUsageFile: RenameParams = - { TextDocument = { Uri = normalizePathCasing usageFile } - Position = { Line = 9; Character = 37 } // in the 'yell' part of 'A.Say.yell' - NewName = "sup" } + renameRaw { Uri = normalizePathCasing usageFile } (pos 9u 37u) "sup" let! res = server.TextDocumentRename(renameHelloUsageInUsageFile) @@ -353,29 +359,17 @@ let private crossProjectTests state = | Result.Ok(Some { DocumentChanges = Some edits }) -> Expect.equal edits.Length 2 "Rename has the correct expected edits" - Expect.exists + Expect.hasEditAtRange edits - (fun n -> - n.TextDocument.Uri.Contains "LibA" - && n.TextDocument.Uri.Contains "Library.fs" - && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 6; Character = 8 } - End = { Line = 6; Character = 12 } } - && r.NewText = "sup")) - $"Rename contains changes in LibA in the list %A{edits}" - - Expect.exists + "LibA/Library.fs" + { Start = { Line = 6u; Character = 8u } + End = { Line = 6u; Character = 12u } } + + Expect.hasEditAtRange edits - (fun n -> - n.TextDocument.Uri.Contains "LibB" - && n.TextDocument.Uri.Contains "Library.fs" - && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 9; Character = 37 } - End = { Line = 9; Character = 41 } } - && r.NewText = "sup")) - $"Rename contains changes in LibB in the list %A{edits}" + "LibB/Library.fs" + { Start = { Line = 9u; Character = 37u } + End = { Line = 9u; Character = 41u } } | Result.Ok edits -> failtestf "got some unexpected edits: %A" edits }) ] @@ -390,7 +384,8 @@ let private prepareRenameTests state = let p: PrepareRenameParams = { TextDocument = doc.TextDocumentIdentifier - Position = cursor } + Position = cursor + WorkDoneToken = None } let! res = doc.Server.Server.TextDocumentPrepareRename p diff --git a/test/FsAutoComplete.Tests.Lsp/ScriptTests.fs b/test/FsAutoComplete.Tests.Lsp/ScriptTests.fs index 9b916371c..7d1f585b4 100644 --- a/test/FsAutoComplete.Tests.Lsp/ScriptTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/ScriptTests.fs @@ -123,8 +123,8 @@ let dependencyManagerTests state = } |> Async.Cache - testSequenced <| - testList + testSequenced + <| testList "dependencyManager integrations" [ testList "tests" @@ -153,7 +153,7 @@ let dependencyManagerTests state = | Ok _ -> failwith "Expected to fail typechecking a script with a dependency manager that's missing" | Core.Result.Error e -> match e with - | [| { Code = Some "998" }; _ |] -> () // this is the error code that signals a missing dependency manager, so this is a 'success' + | [| { Code = Some(U2.C2 "998" | U2.C1 998) }; _ |] -> () // this is the error code that signals a missing dependency manager, so this is a 'success' | e -> failwithf "Unexpected error during typechecking: %A" e }) ] ] @@ -179,8 +179,8 @@ let scriptProjectOptionsCacheTests state = return server, events, workingDir, scriptPath, options } - testSequenced <| - testList + testSequenced + <| testList "ScriptProjectOptionsCache" [ testList "tests" diff --git a/test/FsAutoComplete.Tests.Lsp/SignatureHelpTests.fs b/test/FsAutoComplete.Tests.Lsp/SignatureHelpTests.fs index f4eca713c..7e98da5f5 100644 --- a/test/FsAutoComplete.Tests.Lsp/SignatureHelpTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SignatureHelpTests.fs @@ -21,6 +21,7 @@ let private testSignatureHelp' (server: CachedServer) text pos triggerType check let sigHelpRequest: SignatureHelpParams = { TextDocument = doc.TextDocumentIdentifier + WorkDoneToken = None Position = pos Context = Some @@ -31,7 +32,7 @@ let private testSignatureHelp' (server: CachedServer) text pos triggerType check TriggerCharacter = match triggerType with | Manual -> None - | Char c -> Some c + | Char c -> Some(string c) IsRetrigger = false ActiveSignatureHelp = None } } @@ -75,7 +76,7 @@ let private functionApplicationEdgeCasesTests server = """ Manual (fun resp -> match resp with | Some sigHelp -> - Expect.equal sigHelp.ActiveSignature (Some 0) "should have suggested the first overload" + Expect.equal sigHelp.ActiveSignature (Some 0u) "should have suggested the first overload" Expect.equal sigHelp.Signatures.[0].Label @@ -142,7 +143,7 @@ let private overloadEdgeCasesTests server = [ let text = "let ___ = new System.IO.MemoryStream ( )" for c in 37..39 do - let pos = { Line = 0; Character = c } + let pos = { Line = 0u; Character = uint32 c } testCaseAsync $"Can get overloads at whitespace position {c - 37} of unattached parens" <| testSignatureHelp' @@ -160,7 +161,7 @@ let private overloadEdgeCasesTests server = [ let text = "let _____ = new System.IO.MemoryStream(42)" for c in 39..41 do - let pos = { Line = 0; Character = c } + let pos = { Line = 0u; Character = uint32 c } testCaseAsync $"Can get overloads at whitespace position {c - 39} of attached parens" <| testSignatureHelp' @@ -178,7 +179,7 @@ let issuesTests server = testList "issues" [ testCaseAsync "issue #950 - exception when in first column in first line" - <| testSignatureHelp' server "Syste" { Line = 0; Character = 0 } Manual (fun r -> + <| testSignatureHelp' server "Syste" { Line = 0u; Character = 0u } Manual (fun r -> let err = Expect.wantError r "No signature help at first position" Expect.equal err.Message "Couldn't find previous non-whitespace char" "Should fail because no prev char") testCaseAsync "type names aren't backticked" @@ -208,6 +209,6 @@ let tests state = System.IO.Directory.EnumerateDirectories("/var", $0) // signature help triggered at the space after the comma should suggest overload 1, not overload 0 """ Manual (fun resp -> Expect.isSome resp "should get sigdata when triggered on applications" - Expect.equal (Some 1) resp.Value.ActiveSignature "should have suggested the second overload") ] + Expect.equal (Some 1u) resp.Value.ActiveSignature "should have suggested the second overload") ] issuesTests server ]) diff --git a/test/FsAutoComplete.Tests.Lsp/UnsedDeclarationsTests.fs b/test/FsAutoComplete.Tests.Lsp/UnsedDeclarationsTests.fs index 7f9d5c383..5d1d9741c 100644 --- a/test/FsAutoComplete.Tests.Lsp/UnsedDeclarationsTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/UnsedDeclarationsTests.fs @@ -38,7 +38,7 @@ let private checkUsageAt server doc sourceWithoutCursor cursor expected = let isUnused (diag: Diagnostic) = diag.Source = Some "FSAC" - && diag.Code = Some "FSAC0003" + && diag.Code = Some(U2.C2 "FSAC0003") && diag.Message = "This value is unused" && diag.Tags |> Option.map (Array.contains DiagnosticTag.Unnecessary) @@ -318,7 +318,7 @@ module private Internal = for (marker, pos) in cursors do let expected = if marker = "$P" then Used else Unused - let title = $"%A{expected} at %s{pos.DebuggerDisplay}" + let title = $"%A{expected} at %d{pos.Line}, %d{pos.Character}" testCaseAsync title <| checkUsageAt server file source pos expected ] ]) let tests state = diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs index 597085564..dbe85b1b9 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs @@ -10,6 +10,7 @@ open Ionide.ProjInfo.Logging /// Checks for CodeFixes, CodeActions open System.Runtime.ExceptionServices + /// /// Prefixes: /// * `check`: Check to use inside a `testCaseAsync`. Not a Test itself! @@ -118,6 +119,7 @@ module CodeFix = = async { let mutable attempts = 5 + while attempts > 0 do try let (range, text) = @@ -134,15 +136,20 @@ module CodeFix = validateDiagnostics chooseFix (expected ()) + attempts <- 0 - with - | ex -> + with ex -> attempts <- attempts - 1 + if attempts = 0 then ExceptionDispatchInfo.Capture(ex).Throw() return failwith "Unreachable" else - _logger.warn (Log.setMessage "Retrying test after failure" >> Log.addContext "attempts" (5 - attempts)) + _logger.warn ( + Log.setMessage "Retrying test after failure" + >> Log.addContext "attempts" (5 - attempts) + ) + do! Async.Sleep 15 } @@ -248,7 +255,7 @@ module CodeFix = [ for (i, range) in cursorRanges |> Seq.indexed do let pos = if range |> Range.isPosition then - range.Start.DebuggerDisplay + range.DebuggerDisplay else $"{range.Start.DebuggerDisplay}..{range.End.DebuggerDisplay}" diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/Server.Tests.fs b/test/FsAutoComplete.Tests.Lsp/Utils/Server.Tests.fs index 9fcd9fc98..6d0b7822d 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/Server.Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/Server.Tests.fs @@ -13,6 +13,7 @@ open Utils.Server open Utils.Utils open FsToolkit.ErrorHandling open FSharpx.Control +open FsAutoComplete.LspHelpers let tests state = testList @@ -127,7 +128,7 @@ let tests state = Expect.exists diags - (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = i) + (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = uint32 i) $"No unused value error in line {i}") for i in 1..2 do @@ -140,7 +141,7 @@ let tests state = Expect.exists diags - (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = i) + (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = uint32 i) $"No unused value error in line {i}") }) ]) @@ -170,13 +171,13 @@ let tests state = use doc = doc Expect.hasLength diags 4 "" - Expect.exists diags (fun d -> d.Message = "Unused open statement" && d.Range.Start.Line = 0) "" - Expect.exists diags (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = 1) "" - Expect.exists diags (fun d -> d.Message.Contains "bar" && d.Range.Start.Line = 1) "" + Expect.exists diags (fun d -> d.Message = "Unused open statement" && d.Range.Start.Line = 0u) "" + Expect.exists diags (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = 1u) "" + Expect.exists diags (fun d -> d.Message.Contains "bar" && d.Range.Start.Line = 1u) "" Expect.exists diags - (fun d -> d.Message = "This qualifier is redundant" && d.Range.Start.Line = 2) + (fun d -> d.Message = "This qualifier is redundant" && d.Range.Start.Line = 2u) "" @@ -186,13 +187,13 @@ let tests state = let! diags = doc |> Document.changeTextTo source Expect.hasLength diags 4 "" - Expect.exists diags (fun d -> d.Message = "Unused open statement" && d.Range.Start.Line = 0) "" - Expect.exists diags (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = 1) "" - Expect.exists diags (fun d -> d.Message.Contains "foo" && d.Range.Start.Line = 1) "" + Expect.exists diags (fun d -> d.Message = "Unused open statement" && d.Range.Start.Line = 0u) "" + Expect.exists diags (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = 1u) "" + Expect.exists diags (fun d -> d.Message.Contains "foo" && d.Range.Start.Line = 1u) "" Expect.exists diags - (fun d -> d.Message = "This qualifier is redundant" && d.Range.Start.Line = 2) + (fun d -> d.Message = "This qualifier is redundant" && d.Range.Start.Line = 2u) "" @@ -202,13 +203,13 @@ let tests state = let! diags = doc |> Document.changeTextTo source Expect.hasLength diags 4 "" - Expect.exists diags (fun d -> d.Message = "Unused open statement" && d.Range.Start.Line = 0) "" - Expect.exists diags (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = 1) "" - Expect.exists diags (fun d -> d.Message.Contains "baz" && d.Range.Start.Line = 1) "" + Expect.exists diags (fun d -> d.Message = "Unused open statement" && d.Range.Start.Line = 0u) "" + Expect.exists diags (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = 1u) "" + Expect.exists diags (fun d -> d.Message.Contains "baz" && d.Range.Start.Line = 1u) "" Expect.exists diags - (fun d -> d.Message = "This qualifier is redundant" && d.Range.Start.Line = 2) + (fun d -> d.Message = "This qualifier is redundant" && d.Range.Start.Line = 2u) "" }) ]) ] @@ -309,7 +310,7 @@ let tests state = "The value or constructor 'bar' is not defined." "Should be not defined error" - Expect.equal diag.Range.Start.Line 0 "Error should be in line 1" + Expect.equal diag.Range.Start.Line 0u "Error should be in line 1" }) testCaseAsync "can load script file again" @@ -325,7 +326,7 @@ let tests state = "The value or constructor 'bar' is not defined." "Should be not defined error" - Expect.equal diag.Range.Start.Line 0 "Error should be in line 1" + Expect.equal diag.Range.Start.Line 0u "Error should be in line 1" }) ]) serverTestList @@ -344,12 +345,12 @@ let tests state = diags (fun diag -> diag.Message.Contains "The value or constructor 'bar' is not defined." - && diag.Range.Start.Line = 0) + && diag.Range.Start.Line = 0u) "Should be not defined error" Expect.exists diags - (fun diag -> diag.Message.Contains "This value is unused" && diag.Range.Start.Line = 0) + (fun diag -> diag.Message.Contains "This value is unused" && diag.Range.Start.Line = 0u) "Should be unused value" }) testCaseAsync @@ -362,12 +363,12 @@ let tests state = diags (fun diag -> diag.Message.Contains "The value or constructor 'bar' is not defined." - && diag.Range.Start.Line = 0) + && diag.Range.Start.Line = 0u) "Should be not defined error" Expect.exists diags - (fun diag -> diag.Message.Contains "This value is unused" && diag.Range.Start.Line = 0) + (fun diag -> diag.Message.Contains "This value is unused" && diag.Range.Start.Line = 0u) "Should be unused value" }) ]) @@ -391,7 +392,7 @@ let tests state = "The value or constructor 'otherBar' is not defined." "Should be not defined error" - Expect.equal diag.Range.Start.Line 5 "Error should be in line 6" + Expect.equal diag.Range.Start.Line 5u "Error should be in line 6" }) testCaseAsync "can load file in project again" @@ -407,7 +408,7 @@ let tests state = "The value or constructor 'otherBar' is not defined." "Should be not defined error" - Expect.equal diag.Range.Start.Line 5 "Error should be in line 6" + Expect.equal diag.Range.Start.Line 5u "Error should be in line 6" }) testCaseAsync "can load other file in project" @@ -423,7 +424,7 @@ let tests state = "The value or constructor 'programBar' is not defined." "Should be not defined error" - Expect.equal diag.Range.Start.Line 4 "Error should be in line 5" + Expect.equal diag.Range.Start.Line 4u "Error should be in line 5" }) ]) serverTestList "dir with project and all analyzers" state allAnalyzersConfig projectDir (fun server -> @@ -441,7 +442,7 @@ let tests state = "The value or constructor 'otherBar' is not defined." "Should be not defined error" - Expect.equal diag.Range.Start.Line 5 "Error should be in line 6" + Expect.equal diag.Range.Start.Line 5u "Error should be in line 6" }) testCaseAsync "can load file in project again" @@ -457,7 +458,7 @@ let tests state = "The value or constructor 'otherBar' is not defined." "Should be not defined error" - Expect.equal diag.Range.Start.Line 5 "Error should be in line 6" + Expect.equal diag.Range.Start.Line 5u "Error should be in line 6" }) testCaseAsync "can load other file in project" @@ -469,17 +470,17 @@ let tests state = diags (fun diag -> diag.Message.Contains "The value or constructor 'programBar' is not defined." - && diag.Range.Start.Line = 4) + && diag.Range.Start.Line = 4u) "Should be not defined error" // `argv` Expect.exists diags - (fun diag -> diag.Message.Contains "This value is unused" && diag.Range.Start.Line = 11) + (fun diag -> diag.Message.Contains "This value is unused" && diag.Range.Start.Line = 11u) "Should be unused value" Expect.exists diags - (fun diag -> diag.Message.Contains "Unused open statement" && diag.Range.Start.Line = 2) + (fun diag -> diag.Message.Contains "Unused open statement" && diag.Range.Start.Line = 2u) "Should be unused open" }) ]) ] ] @@ -597,12 +598,12 @@ let tests state = let groups = diags - |> Array.map (fun d -> + |> Array.map (fun (d : Diagnostic) -> // simplify `The value or constructor 'value' is not defined.` error (contains names and recommendations) - if d.Code = Some "39" then - "The value or constructor is not defined" - else - d.Message) + match d.CodeAsString with + | Some "39" -> "The value or constructor is not defined" + | _ -> d.Message + ) |> Array.countBy id |> Map.ofArray @@ -637,7 +638,7 @@ let tests state = testCaseAsync "diagnostics for some analyzers" (async { - let checkDiags (unusedOpen, unusedValue, simplifyName) diags = + let checkDiags (unusedOpen, unusedValue, simplifyName) (diags: Diagnostic[]) = let actual = {| UnusedOpen = diags |> Array.exists (fun d -> d.Message = "Unused open statement") UnusedDecl = diags |> Array.exists (fun d -> d.Message = "This value is unused") @@ -838,7 +839,9 @@ f2 "bar" |> ignore let ps: CompletionParams = { TextDocument = doc.TextDocumentIdentifier Position = pos - Context = None } + Context = None + WorkDoneToken = None + PartialResultToken = None } let! res = doc.Server.Server.TextDocumentCompletion ps Expect.isOk res "Should be ok result" @@ -848,7 +851,7 @@ f2 "bar" |> ignore testCaseAsync "can get completions" <| async { let! (doc, _) = doc - let! completions = doc |> completionAt { Line = 1; Character = 24 } + let! completions = doc |> completionAt { Line = 1u; Character = 24u } Expect.isSome completions "Should be some completions" let completions = completions.Value Expect.isNonEmpty completions.Items "Should be completions" @@ -862,7 +865,7 @@ f2 "bar" |> ignore testCaseAsync "can get completions again" <| async { let! (doc, _) = doc - let! completions = doc |> completionAt { Line = 1; Character = 24 } + let! completions = doc |> completionAt { Line = 1u; Character = 24u } Expect.isSome completions "Should be some completions" let completions = completions.Value Expect.isNonEmpty completions.Items "Should be completions" @@ -879,7 +882,7 @@ f2 "bar" |> ignore let ps: TextDocumentPositionParams = { TextDocument = doc.TextDocumentIdentifier - Position = { Line = 0; Character = 6 } } + Position = { Line = 0u; Character = 6u } } let! res = doc.Server.Server.TextDocumentHover ps Expect.isOk res "Should have hover data" diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs index 79048a600..065617682 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs @@ -28,26 +28,22 @@ type CachedServer = Async type Document = { Server: Server - FilePath : string + FilePath: string Uri: DocumentUri mutable Version: int } + member doc.TextDocumentIdentifier: TextDocumentIdentifier = { Uri = doc.Uri } member doc.VersionedTextDocumentIdentifier: VersionedTextDocumentIdentifier = - { Uri = doc.Uri - Version = doc.Version } + { Uri = doc.Uri; Version = doc.Version } member x.Diagnostics = - x.Server.Events - |> fileDiagnosticsForUri x.TextDocumentIdentifier.Uri + x.Server.Events |> fileDiagnosticsForUri x.TextDocumentIdentifier.Uri - member x.CompilerDiagnostics = - x.Diagnostics - |> diagnosticsFromSource "F# Compiler" + member x.CompilerDiagnostics = x.Diagnostics |> diagnosticsFromSource "F# Compiler" interface IDisposable with - override doc.Dispose() : unit = - doc |> Document.close |> Async.RunSynchronously + override doc.Dispose() : unit = doc |> Document.close |> Async.RunSynchronously module Server = let private initialize path (config: FSharpConfigDto) createServer = @@ -65,7 +61,7 @@ module Server = for file in System.IO.Directory.EnumerateFiles(path, "*.fsproj", SearchOption.AllDirectories) do do! file |> Path.GetDirectoryName |> dotnetRestore - let (server : IFSharpLspServer, events : IObservable<_>) = createServer () + let (server: IFSharpLspServer, events: IObservable<_>) = createServer () events |> Observable.add logEvent let p: InitializeParams = @@ -74,7 +70,7 @@ module Server = Locale = None RootUri = path |> Option.map (sprintf "file://%s") InitializationOptions = Some(Server.serialize config) - Capabilities = Some clientCaps + Capabilities = clientCaps ClientInfo = Some { Name = "FSAC Tests" @@ -84,11 +80,13 @@ module Server = |> Option.map (fun p -> [| { Uri = Path.FilePathToUri p Name = "Test Folder" } |]) - trace = None } + Trace = None + WorkDoneToken = None } match! server.Initialize p with | Ok _ -> - do! server.Initialized (InitializedParams()) + do! server.Initialized() + return { RootPath = path Server = server @@ -131,9 +129,7 @@ module Server = async { let! server = server - let doc = - server - |> createDocument String.Empty (server |> nextUntitledDocUri) + let doc = server |> createDocument String.Empty (server |> nextUntitledDocUri) let! diags = doc |> Document.openWith initialText @@ -161,17 +157,13 @@ module Server = server |> createDocument fullPath - ( - fullPath - // normalize path is necessary: otherwise might be different lower/upper cases in uri for tests and LSP server: - // on windows `E:\...`: `file:///E%3A/...` (before normalize) vs. `file:///e%3A/..` (after normalize) - |> normalizePath - |> Path.LocalPathToUri - ) - - let! diags = - doc - |> Document.openWith (File.ReadAllText fullPath) + (fullPath + // normalize path is necessary: otherwise might be different lower/upper cases in uri for tests and LSP server: + // on windows `E:\...`: `file:///E%3A/...` (before normalize) vs. `file:///e%3A/..` (after normalize) + |> normalizePath + |> Path.LocalPathToUri) + + let! diags = doc |> Document.openWith (File.ReadAllText fullPath) return (doc, diags) } @@ -197,9 +189,7 @@ module Server = // To avoid hitting the typechecker cache, we need to update the file's timestamp IO.File.SetLastWriteTimeUtc(fullPath, DateTime.UtcNow) - let doc = - server - |> createDocument fullPath (Path.FilePathToUri fullPath) + let doc = server |> createDocument fullPath (Path.FilePathToUri fullPath) let! diags = doc |> Document.openWith initialText @@ -211,11 +201,7 @@ module Document = open System.Threading.Tasks let private typedEvents<'t> typ : _ -> System.IObservable<'t> = - Observable.choose (fun (typ', _o) -> - if typ' = typ then - Some(unbox _o) - else - None) + Observable.choose (fun (typ', _o) -> if typ' = typ then Some(unbox _o) else None) /// `textDocument/publishDiagnostics` /// @@ -225,11 +211,7 @@ module Document = let diagnosticsStream (doc: Document) = doc.Server.Events |> typedEvents "textDocument/publishDiagnostics" - |> Observable.choose (fun n -> - if n.Uri = doc.Uri then - Some n.Diagnostics - else - None) + |> Observable.choose (fun n -> if n.Uri = doc.Uri then Some n.Diagnostics else None) /// `fsharp/documentAnalyzed` let analyzedStream (doc: Document) = @@ -241,21 +223,19 @@ module Document = /// in ms let private waitForLateDiagnosticsDelay = let envVar = "FSAC_WaitForLateDiagnosticsDelay" + System.Environment.GetEnvironmentVariable envVar |> Option.ofObj |> Option.map (fun d -> match System.Int32.TryParse d with | (true, d) -> d - | (false, _) -> - failwith $"Environment Variable '%s{envVar}' exists, but is not a correct int number ('%s{d}')" - ) + | (false, _) -> failwith $"Environment Variable '%s{envVar}' exists, but is not a correct int number ('%s{d}')") |> Option.orElseWith (fun _ -> - // set in Github Actions: https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables - match System.Environment.GetEnvironmentVariable "CI" with - | null -> None - | _ -> Some 25 - ) - |> Option.defaultValue 7 // testing locally + // set in Github Actions: https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables + match System.Environment.GetEnvironmentVariable "CI" with + | null -> None + | _ -> Some 25) + |> Option.defaultValue 7 // testing locally /// Waits (if necessary) and gets latest diagnostics. /// @@ -298,6 +278,7 @@ module Document = >> Log.addContext "uri" doc.Uri >> Log.addContext "version" doc.Version ) + let tcs = TaskCompletionSource<_>() use _ = @@ -313,7 +294,7 @@ module Document = ) |> Observable.bufferSpan (timeout) // |> Observable.timeoutSpan timeout - |> Observable.subscribe(fun x -> tcs.SetResult x) + |> Observable.subscribe (fun x -> tcs.SetResult x) let! result = tcs.Task |> Async.AwaitTask @@ -322,11 +303,10 @@ module Document = /// Note: Mutates passed `doc` - let private incrVersion (doc: Document) = - System.Threading.Interlocked.Increment(&doc.Version) + let private incrVersion (doc: Document) = System.Threading.Interlocked.Increment(&doc.Version) /// Note: Mutates passed `doc` - let private incrVersionedTextDocumentIdentifier (doc: Document): VersionedTextDocumentIdentifier = + let private incrVersionedTextDocumentIdentifier (doc: Document) : VersionedTextDocumentIdentifier = { Uri = doc.Uri Version = incrVersion doc } @@ -343,8 +323,8 @@ module Document = try return! doc |> waitForLatestDiagnostics Helpers.defaultTimeout - with - | :? TimeoutException -> return failwith $"Timeout waiting for latest diagnostics for {doc.Uri}" + with :? TimeoutException -> + return failwith $"Timeout waiting for latest diagnostics for {doc.Uri}" } let close (doc: Document) = @@ -361,22 +341,18 @@ module Document = async { let p: DidChangeTextDocumentParams = { TextDocument = doc |> incrVersionedTextDocumentIdentifier - ContentChanges = - [| { Range = None - RangeLength = None - Text = text } |] } + ContentChanges = [| U2.C2 { Text = text } |] } do! doc.Server.Server.TextDocumentDidChange p do! Async.Sleep(TimeSpan.FromMilliseconds 250.) return! doc |> waitForLatestDiagnostics Helpers.defaultTimeout } - let saveText (text : string) (doc : Document) = + let saveText (text: string) (doc: Document) = async { - let p : DidSaveTextDocumentParams = { - Text = Some text - TextDocument = doc.TextDocumentIdentifier - } + let p: DidSaveTextDocumentParams = + { Text = Some text + TextDocument = doc.TextDocumentIdentifier } // Simulate the file being written to disk so we don't hit the typechecker cache IO.File.SetLastWriteTimeUtc(doc.FilePath, DateTime.UtcNow) do! doc.Server.Server.TextDocumentDidSave p @@ -387,8 +363,7 @@ module Document = let private assertOk result = Expect.isOk result "Expected success" - result - |> Result.defaultWith (fun _ -> failtest "not reachable") + result |> Result.defaultWith (fun _ -> failtest "not reachable") let private assertSome opt = Expect.isSome opt "Expected to have Some" @@ -400,22 +375,31 @@ module Document = async { let ps: CodeActionParams = { TextDocument = doc.TextDocumentIdentifier + WorkDoneToken = None + PartialResultToken = None Range = range - Context = { Diagnostics = diagnostics; Only = None; TriggerKind = None } } + Context = + { Diagnostics = diagnostics + Only = None + TriggerKind = None } } let! res = doc.Server.Server.TextDocumentCodeAction ps return res |> assertOk } - let inlayHintsAt range (doc: Document) = async { - let ps: InlayHintParams = { - Range = range - TextDocument = doc.TextDocumentIdentifier + let inlayHintsAt range (doc: Document) = + async { + let ps: InlayHintParams = + { Range = range + TextDocument = doc.TextDocumentIdentifier + WorkDoneToken = None } + + let! res = doc.Server.Server.TextDocumentInlayHint ps + return res |> assertOk |> assertSome + } + + let resolveInlayHint inlayHint (doc: Document) = + async { + let! res = doc.Server.Server.InlayHintResolve inlayHint + return res |> assertOk } - let! res = doc.Server.Server.TextDocumentInlayHint ps - return res |> assertOk |> assertSome - } - let resolveInlayHint inlayHint (doc: Document) = async { - let! res = doc.Server.Server.InlayHintResolve inlayHint - return res |> assertOk - } diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.Tests.fs b/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.Tests.fs index aff5ff7a6..c35fa35a6 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.Tests.fs @@ -106,11 +106,11 @@ printfn "Result=%i" b >> Cursor.tryExtractIndex >> Option.defaultWith (fun _ -> failtest "No cursor found") - let _assertResultIs (idx: int, text: string) = + let _assertResultIs (idx: uint32, text: string) = assertAndGetIndex >> Expect.equal "should be correct cursor position and text" (idx, text) - let assertCursorAt (idx: int) = + let assertCursorAt (idx: uint32) = assertAndGetIndex >> fst >> Expect.equal "should have found cursor at correct position" idx @@ -122,23 +122,23 @@ printfn "Result=%i" b [ testCase "in empty string" <| fun _ -> let text = "$0" - let expected = 0 + let expected = 0u text |> assertCursorAt expected testCase "start of single line" <| fun _ -> let text = "$0Foo bar baz" - let expected = 0 + let expected = 0u text |> assertCursorAt expected testCase "end of single word" <| fun _ -> let text = "foo$0" // Note: out of string range: cursor is AFTER last character - let expected = 3 + let expected = 3u text |> assertCursorAt expected testCase "end of single line" <| fun _ -> let text = "foo bar baz$0" - let expected = 11 + let expected = 11u text |> assertCursorAt expected testCase "removes cursor marker from single line" <| fun _ -> @@ -156,7 +156,7 @@ printfn "Result=%i" b $0 """ - let expected = 0 + let expected = 0u text |> assertCursorAt expected testCase "in empty string indented" <| fun _ -> @@ -166,7 +166,7 @@ $0 $0 """ - let expected = 0 + let expected = 0u text |> assertCursorAt expected testCase "in F# code unindented" <| fun _ -> @@ -180,7 +180,7 @@ let b = printfn "Result=%i" b """ - let expected = 16 + let expected = 16u text |> assertCursorAt expected testCase "in F# code indented" <| fun _ -> @@ -194,7 +194,7 @@ printfn "Result=%i" b printfn "Result=%i" b """ - let expected = 16 + let expected = 16u text |> assertCursorAt expected testCase "removes cursor in F# code unindented" <| fun _ -> @@ -273,7 +273,7 @@ printfn "Result=%i" b <| fun _ -> let text = "let $Avalue$B = $C42" let actual = text |> Cursor.tryExtractPositionMarkedWithAnyOf [| "$B"; "$C"; "$A" |] - let expected = Some(("$A", pos 0 4), "let value$B = $C42") + let expected = Some(("$A", pos 0u 4u), "let value$B = $C42") actual |> Expect.equal "should be correct marker" expected ] @@ -380,23 +380,23 @@ printfn "Result=%i" b [ testCase "in empty string" <| fun _ -> let text = "$0" - let expected = pos 0 0 + let expected = pos 0u 0u text |> assertCursorAt expected testCase "start of single line" <| fun _ -> let text = "$0Foo bar baz" - let expected = pos 0 0 + let expected = pos 0u 0u text |> assertCursorAt expected testCase "end of single word" <| fun _ -> let text = "foo$0" // Note: out of string range: cursor is AFTER last character - let expected = pos 0 3 + let expected = pos 0u 3u text |> assertCursorAt expected testCase "end of single line" <| fun _ -> let text = "foo bar baz$0" - let expected = pos 0 11 + let expected = pos 0u 11u text |> assertCursorAt expected testCase "removes cursor marker from single line" <| fun _ -> @@ -414,7 +414,7 @@ printfn "Result=%i" b $0 """ - let expected = pos 0 0 + let expected = pos 0u 0u text |> assertCursorAt expected testCase "in empty string indented" <| fun _ -> @@ -424,7 +424,7 @@ $0 $0 """ - let expected = pos 0 0 + let expected = pos 0u 0u text |> assertCursorAt expected testCase "in F# code unindented" <| fun _ -> @@ -438,7 +438,7 @@ let b = printfn "Result=%i" b """ - let expected = pos 2 4 // 0-based, first line (with `"""`) is removed + let expected = pos 2u 4u // 0-based, first line (with `"""`) is removed text |> assertCursorAt expected testCase "in F# code indented" <| fun _ -> @@ -452,7 +452,7 @@ printfn "Result=%i" b printfn "Result=%i" b """ - let expected = pos 2 4 // 0-based, first line (with `"""`) is removed, leading indentation removed + let expected = pos 2u 4u // 0-based, first line (with `"""`) is removed, leading indentation removed text |> assertCursorAt expected testCase "removes cursor in F# code unindented" <| fun _ -> @@ -567,7 +567,7 @@ let b = printfn "Result=%i" b """ - let expected = { Start = pos 2 4; End = pos 2 7 } + let expected = { Start = pos 2u 4u; End = pos 2u 7u } text |> assertRangeIs expected testCase "can extract range over multiple lines" @@ -582,7 +582,7 @@ let b = printfn "$0Result=%i" b """ - let expected = { Start = pos 2 4; End = pos 5 9 } + let expected = { Start = pos 2u 4u; End = pos 5u 9u } text |> assertRangeIs expected testCase "can extract position" @@ -597,7 +597,7 @@ let b = printfn "Result=%i" b """ - let expected = { Start = pos 2 7; End = pos 2 7 } + let expected = { Start = pos 2u 7u; End = pos 2u 7u } text |> assertRangeIs expected testCase "removes cursor markers from line" @@ -636,7 +636,7 @@ let b = printfn "$0Result$0=%i$0" b$0 """ - let expectedRange = { Start = pos 0 7; End = pos 2 8 } + let expectedRange = { Start = pos 0u 7u; End = pos 2u 8u } let expectedText = !- """ @@ -658,6 +658,7 @@ printfn "$0Result$0=%i$0" b$0 let idx = textWithCursor.IndexOf(Cursor.Marker, StringComparison.Ordinal) Expect.isGreaterThanOrEqual "Text has no cursor" (idx, 0) let text = textWithCursor.Remove(idx, Cursor.Marker.Length) + let idx = uint32 idx text |> Cursor.beforeIndex idx @@ -669,8 +670,8 @@ printfn "$0Result$0=%i$0" b$0 [ testCase "empty string" <| fun _ -> let text = "" - let idx = 0 - let expected = pos 0 0 + let idx = 0u + let expected = pos 0u 0u text |> Cursor.beforeIndex idx @@ -679,23 +680,23 @@ printfn "$0Result$0=%i$0" b$0 testCase "empty string with cursor" <| fun _ -> let text = "$0" - let expected = pos 0 0 + let expected = pos 0u 0u assertBeforeIndex expected text testCase "single line string - start" <| fun _ -> let text = "$0let foo = 42" - let expected = pos 0 0 + let expected = pos 0u 0u assertBeforeIndex expected text testCase "single line string - middle" <| fun _ -> let text = "let foo $0= 42" - let expected = pos 0 8 + let expected = pos 0u 8u assertBeforeIndex expected text testCase "single line string - end" <| fun _ -> let text = "let foo = 42$0" - let expected = pos 0 12 + let expected = pos 0u 12u assertBeforeIndex expected text ] testList @@ -712,7 +713,7 @@ let b = printfn "Result=%i" b """ - let expected = pos 0 0 + let expected = pos 0u 0u assertBeforeIndex expected text testCase "middle of first line" <| fun _ -> @@ -726,7 +727,7 @@ let b = printfn "Result=%i" b """ - let expected = pos 0 7 + let expected = pos 0u 7u assertBeforeIndex expected text testCase "end of first line" <| fun _ -> @@ -740,7 +741,7 @@ let b = printfn "Result=%i" b """ - let expected = pos 0 10 + let expected = pos 0u 10u assertBeforeIndex expected text testCase "start of 4th line" <| fun _ -> @@ -754,7 +755,7 @@ $0let b = printfn "Result=%i" b """ - let expected = pos 3 0 + let expected = pos 3u 0u assertBeforeIndex expected text testCase "middle of 4th line" <| fun _ -> @@ -768,7 +769,7 @@ let $0b = printfn "Result=%i" b """ - let expected = pos 3 4 + let expected = pos 3u 4u assertBeforeIndex expected text testCase "end of 4th line" <| fun _ -> @@ -782,7 +783,7 @@ let b =$0 printfn "Result=%i" b """ - let expected = pos 3 7 + let expected = pos 3u 7u assertBeforeIndex expected text testCase "start of last line" <| fun _ -> @@ -795,7 +796,7 @@ let b = a + 5 $0printfn "Result=%i" b""" - let expected = pos 5 0 + let expected = pos 5u 0u assertBeforeIndex expected text testCase "middle of last line" <| fun _ -> @@ -808,7 +809,7 @@ let b = a + 5 printfn "$0Result=%i" b""" - let expected = pos 5 9 + let expected = pos 5u 9u assertBeforeIndex expected text testCase "end of last line" <| fun _ -> @@ -821,7 +822,7 @@ let b = a + 5 printfn "Result=%i" b$0""" - let expected = pos 5 21 + let expected = pos 5u 21u assertBeforeIndex expected text ] ] let tryIndexOfTests = @@ -868,17 +869,17 @@ printfn "Result=%i" b$0""" [ testCase "inside" <| fun _ -> let text = "$0" - let expected = 0 + let expected = 0u text |> assertIndexOf expected testCase "out of char range in empty string" <| fun _ -> let text = "" - let pos = pos 0 1 + let pos = pos 0u 1u text |> assertNoIndexAt pos testCase "out of line range in empty string" <| fun _ -> let text = "" - let pos = pos 1 0 + let pos = pos 1u 0u text |> assertNoIndexAt pos ] testList @@ -886,27 +887,27 @@ printfn "Result=%i" b$0""" [ testCase "out of char range" <| fun _ -> let text = "foo bar baz" - let pos = pos 0 (11 + 1) + let pos = pos 0u (11u + 1u) text |> assertNoIndexAt pos testCase "out of line range" <| fun _ -> let text = "foo bar baz" - let pos = pos 1 0 + let pos = pos 1u 0u text |> assertNoIndexAt pos testCase "start" <| fun _ -> let text = "$0foo bar baz" - let expected = 0 + let expected = 0u text |> assertIndexOf expected testCase "middle" <| fun _ -> let text = "foo b$0ar baz" - let expected = 5 + let expected = 5u text |> assertIndexOf expected testCase "end" <| fun _ -> let text = "foo bar baz$0" - let expected = 11 + let expected = 11u text |> assertIndexOf expected ] testList @@ -915,24 +916,27 @@ printfn "Result=%i" b$0""" <| fun _ -> // chars: 11 + `\n` + 17 let text = "$0foo bar baz\nlorem ipsum dolor" - let expected = 0 + let expected = 0u text |> assertIndexOf expected testCase "middle of 1st line" <| fun _ -> let text = "foo b$0ar baz\nlorem ipsum dolor" - let expected = 5 + let expected = 5u text |> assertIndexOf expected testCase "end of 1st line" <| fun _ -> let text = "foo bar baz$0\nlorem ipsum dolor" - let expected = 10 (*1st line; 0-based*) + 1 (*\n*) // on `\n`; 10: Index is 0-based: string with length=11 -> max index = 10 + let expected = 10u (*1st line; 0-based*) + 1u (*\n*) // on `\n`; 10: Index is 0-based: string with length=11 -> max index = 10 text |> assertIndexOf expected testCase "start of 2nd line" <| fun _ -> let text = "foo bar baz\n$0lorem ipsum dolor" let expected = - 10 (*1st line; 0-based*) + 1 (*\n*) + 0 (*2nd line*) + 1 (*index after cursor*) + 10u (*1st line; 0-based*) + + 1u (*\n*) + + 0u (*2nd line*) + + 1u (*index after cursor*) text |> assertIndexOf expected testCase "middle of 2nd line" @@ -940,7 +944,10 @@ printfn "Result=%i" b$0""" let text = "foo bar baz\nlorem ip$0sum dolor" let expected = - 10 (*1st line; 0-based*) + 1 (*\n*) + 8 (*2nd line*) + 1 (*index after cursor*) + 10u (*1st line; 0-based*) + + 1u (*\n*) + + 8u (*2nd line*) + + 1u (*index after cursor*) text |> assertIndexOf expected testCase "end of 2nd line" @@ -948,26 +955,26 @@ printfn "Result=%i" b$0""" let text = "foo bar baz\nlorem ipsum dolor$0" let expected = - 10 (*1st line; 0-based*) - + 1 (*\n*) - + 17 (*2nd line*) - + 1 (*index afrer cursor*) + 10u (*1st line; 0-based*) + + 1u (*\n*) + + 17u (*2nd line*) + + 1u (*index afrer cursor*) text |> assertIndexOf expected testCase "out of char range in 1st line" <| fun _ -> let text = "foo bar baz\nlorem ipsum dolor" - let pos = pos 0 (11 + 1) + let pos = pos 0u (11u + 1u) text |> assertNoIndexAt pos testCase "out of char range in 2nd line" <| fun _ -> let text = "foo bar baz\nlorem ipsum dolor" - let pos = pos 1 (17 + 1) + let pos = pos 1u (17u + 1u) text |> assertNoIndexAt pos testCase "out of line range" <| fun _ -> let text = "foo bar baz\nlorem ipsum dolor" - let pos = pos 2 0 + let pos = pos 2u 0u text |> assertNoIndexAt pos ] testList @@ -984,7 +991,7 @@ let b = printfn "Result=%i" b """ - text |> assertIndexOf 0 + text |> assertIndexOf 0u testCase "end of text" <| fun _ -> let text = @@ -997,7 +1004,7 @@ let b = printfn "Result=%i" b $0""" - text |> assertIndexOf 61 + text |> assertIndexOf 61u testCase "middle of 1st line" <| fun _ -> let text = @@ -1010,7 +1017,7 @@ let b = printfn "Result=%i" b """ - text |> assertIndexOf 6 + text |> assertIndexOf 6u testCase "end of 1st line" <| fun _ -> let text = @@ -1023,7 +1030,7 @@ let b = printfn "Result=%i" b """ - text |> assertIndexOf 10 + text |> assertIndexOf 10u testCase "start of 4th line" <| fun _ -> let text = @@ -1036,7 +1043,7 @@ $0let b = printfn "Result=%i" b """ - text |> assertIndexOf 23 + text |> assertIndexOf 23u testCase "middle of 4th line" <| fun _ -> let text = @@ -1049,7 +1056,7 @@ let $0b = printfn "Result=%i" b """ - text |> assertIndexOf 27 + text |> assertIndexOf 27u testCase "end of 4th line" <| fun _ -> let text = @@ -1062,7 +1069,7 @@ let b = $0 printfn "Result=%i" b """ - text |> assertIndexOf 31 ] ] + text |> assertIndexOf 31u ] ] let identityTests = testList @@ -1113,7 +1120,7 @@ printfn "Result=%i" b failtest "No cursor" let text = textWithCursor.Replace(Cursor.Marker, "") - (idx, text) + (uint32 idx, text) let roundTrip idx text = let pos = Cursor.beforeIndex idx text @@ -1222,33 +1229,33 @@ $0printfn "$0Result=%i" b$0 (nameof Cursor.afterEdits) [ testCase "doesn't move cursor when insert after cursor in different line" <| fun _ -> - let cursor = pos 1 2 + let cursor = pos 1u 2u let edits = - [ { Range = posRange (pos 2 5) - NewText = "foo bar" } ] + [| { Range = posRange (pos 2u 5u) + NewText = "foo bar" } |] cursor |> Cursor.afterEdits edits |> Expect.equal "Cursor should not move" cursor testCase "doesn't move cursor when remove after cursor in different line" <| fun _ -> - let cursor = pos 1 2 + let cursor = pos 1u 2u let edits = - [ { Range = range (pos 2 5) (pos 3 4) - NewText = "" } ] + [| { Range = range (pos 2u 5u) (pos 3u 4u) + NewText = "" } |] cursor |> Cursor.afterEdits edits |> Expect.equal "Cursor should not move" cursor testCase "doesn't move cursor when replace after cursor in different line" <| fun _ -> - let cursor = pos 1 2 + let cursor = pos 1u 2u let edits = - [ { Range = range (pos 2 5) (pos 3 4) - NewText = "foo bar" } ] + [| { Range = range (pos 2u 5u) (pos 3u 4u) + NewText = "foo bar" } |] cursor |> Cursor.afterEdits edits @@ -1256,33 +1263,33 @@ $0printfn "$0Result=%i" b$0 testCase "doesn't move cursor when insert before cursor in different line and just inside line" <| fun _ -> - let cursor = pos 2 2 + let cursor = pos 2u 2u let edits = - [ { Range = posRange (pos 1 5) - NewText = "foo bar" } ] + [| { Range = posRange (pos 1u 5u) + NewText = "foo bar" } |] cursor |> Cursor.afterEdits edits |> Expect.equal "Cursor should not move" cursor testCase "doesn't move cursor when remove before cursor in different line and just inside line" <| fun _ -> - let cursor = pos 2 2 + let cursor = pos 2u 2u let edits = - [ { Range = range (pos 1 5) (pos 1 7) - NewText = "" } ] + [| { Range = range (pos 1u 5u) (pos 1u 7u) + NewText = "" } |] cursor |> Cursor.afterEdits edits |> Expect.equal "Cursor should not move" cursor testCase "doesn't move cursor when replace before cursor in different line and just inside line" <| fun _ -> - let cursor = pos 2 2 + let cursor = pos 2u 2u let edits = - [ { Range = range (pos 1 5) (pos 1 7) - NewText = "foo bar" } ] + [| { Range = range (pos 1u 5u) (pos 1u 7u) + NewText = "foo bar" } |] cursor |> Cursor.afterEdits edits @@ -1290,119 +1297,119 @@ $0printfn "$0Result=%i" b$0 testCase "moves cursor down a line when inserting new line before cursor in different line" <| fun _ -> - let cursor = pos 2 2 + let cursor = pos 2u 2u let edits = - [ { Range = posRange (pos 1 5) - NewText = "foo\nbar" } ] + [| { Range = posRange (pos 1u 5u) + NewText = "foo\nbar" } |] cursor |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move down a line" { cursor with Line = cursor.Line + 1 } + |> Expect.equal "Cursor should move down a line" { cursor with Line = cursor.Line + 1u } testCase "moves cursor up a line when removing line before cursor in different line" <| fun _ -> - let cursor = pos 3 2 + let cursor = pos 3u 2u let edits = - [ { Range = range (pos 1 5) (pos 2 4) - NewText = "" } ] + [| { Range = range (pos 1u 5u) (pos 2u 4u) + NewText = "" } |] cursor |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move up a line" { cursor with Line = cursor.Line - 1 } + |> Expect.equal "Cursor should move up a line" { cursor with Line = cursor.Line - 1u } testCase "moves cursor up a line when removing a line and inserting inside line before cursor in different line" <| fun _ -> - let cursor = pos 3 2 + let cursor = pos 3u 2u let edits = - [ { Range = range (pos 1 5) (pos 2 4) - NewText = "foo bar" } ] + [| { Range = range (pos 1u 5u) (pos 2u 4u) + NewText = "foo bar" } |] cursor |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move up a line" { cursor with Line = cursor.Line - 1 } + |> Expect.equal "Cursor should move up a line" { cursor with Line = cursor.Line - 1u } testCase "doesn't move cursor when removing a line and inserting a line before cursor in different line" <| fun _ -> - let cursor = pos 3 2 + let cursor = pos 3u 2u let edits = - [ { Range = range (pos 1 5) (pos 2 4) - NewText = "foo\nbar" } ] + [| { Range = range (pos 1u 5u) (pos 2u 4u) + NewText = "foo\nbar" } |] cursor |> Cursor.afterEdits edits |> Expect.equal "Cursor should not move" cursor testCase "moves cursor down when removing a line and inserting two lines before cursor in different line" <| fun _ -> - let cursor = pos 3 2 + let cursor = pos 3u 2u let edits = - [ { Range = range (pos 1 5) (pos 2 4) - NewText = "foo\nbar\nbaz" } ] + [| { Range = range (pos 1u 5u) (pos 2u 4u) + NewText = "foo\nbar\nbaz" } |] cursor |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move down a line" { cursor with Line = cursor.Line + 1 } + |> Expect.equal "Cursor should move down a line" { cursor with Line = cursor.Line + 1u } testCase "moves cursor back when inserting inside same line in front of cursor" <| fun _ -> - let cursor = pos 3 2 + let cursor = pos 3u 2u let edits = - [ { Range = posRange (pos 3 1) - NewText = "foo" } ] + [| { Range = posRange (pos 3u 1u) + NewText = "foo" } |] cursor |> Cursor.afterEdits edits |> Expect.equal "Cursor should move back" { cursor with - Character = cursor.Character + 3 } + Character = cursor.Character + 3u } testCase "moves cursor forward when deleting inside same line in front of cursor" <| fun _ -> - let cursor = pos 3 7 + let cursor = pos 3u 7u let edits = - [ { Range = range (pos 3 2) (pos 3 5) - NewText = "" } ] + [| { Range = range (pos 3u 2u) (pos 3u 5u) + NewText = "" } |] cursor |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move forward" { cursor with Character = 4 } + |> Expect.equal "Cursor should move forward" { cursor with Character = 4u } testCase "moves cursor forward and up when deleting inside and pre same line in front of cursor" <| fun _ -> - let cursor = pos 3 7 + let cursor = pos 3u 7u let edits = - [ { Range = range (pos 2 2) (pos 3 5) - NewText = "" } ] + [| { Range = range (pos 2u 2u) (pos 3u 5u) + NewText = "" } |] cursor |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move forward and up" (pos 2 4) + |> Expect.equal "Cursor should move forward and up" (pos 2u 4u) testCase "moves cursor to front of delete when cursor inside" <| fun _ -> - let cursor = pos 3 7 + let cursor = pos 3u 7u let edits = - [ { Range = range (pos 2 2) (pos 3 10) - NewText = "" } ] + [| { Range = range (pos 2u 2u) (pos 3u 10u) + NewText = "" } |] cursor |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move to start of delete" (pos 2 2) + |> Expect.equal "Cursor should move to start of delete" (pos 2u 2u) testCase "cursor stays when insert at cursor position" <| fun _ -> - let cursor = pos 2 5 + let cursor = pos 2u 5u let edits = - [ { Range = posRange (pos 2 5) - NewText = "foo bar" } ] + [| { Range = posRange (pos 2u 5u) + NewText = "foo bar" } |] cursor |> Cursor.afterEdits edits @@ -1410,15 +1417,15 @@ $0printfn "$0Result=%i" b$0 testCase "cursor moves to front when replacement with cursor inside" <| fun _ -> - let cursor = pos 3 7 + let cursor = pos 3u 7u let edits = - [ { Range = range (pos 2 3) (pos 5 2) - NewText = "foo bar" } ] + [| { Range = range (pos 2u 3u) (pos 5u 2u) + NewText = "foo bar" } |] cursor |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move to start of delete" { Line = 2; Character = 3 } + |> Expect.equal "Cursor should move to start of delete" { Line = 2u; Character = 3u } testList "multiple edits" @@ -1444,36 +1451,35 @@ $0printfn "$0Result=%i" b$0 |> Text.trimTripleQuotation let edits = - [ - // doesn't change cursor - {| Marker = "$1"; NewText = "barbaz" |} - // - 2 lines + 1 line - {| Marker = "$2" - NewText = "baz = 42\nlet " |} - // -1 line + 2 lines - {| Marker = "$3" - NewText = "c\n b =\n (a+c-" |} - // -1 line - 3 chars - {| Marker = "$4"; NewText = "" |} - // +3 line -all chars + couple new chars - {| Marker = "$5" - NewText = " static\n\n\n mutable" |} - // move to front of edit chars - {| Marker = "$6" - NewText = "incrementNumber" |} - // doesn't change cursor - {| Marker = "$7" - NewText = "foo bar\nbaz\nlorem ipsum" |} ] - - let markers = - edits |> List.map (fun e -> e.Marker) |> List.append [ "$0" ] |> List.toArray + [| + // doesn't change cursor + {| Marker = "$1"; NewText = "barbaz" |} + // - 2 lines + 1 line + {| Marker = "$2" + NewText = "baz = 42\nlet " |} + // -1 line + 2 lines + {| Marker = "$3" + NewText = "c\n b =\n (a+c-" |} + // -1 line - 3 chars + {| Marker = "$4"; NewText = "" |} + // +3 line -all chars + couple new chars + {| Marker = "$5" + NewText = " static\n\n\n mutable" |} + // move to front of edit chars + {| Marker = "$6" + NewText = "incrementNumber" |} + // doesn't change cursor + {| Marker = "$7" + NewText = "foo bar\nbaz\nlorem ipsum" |} |] + + let markers = edits |> Array.map (fun e -> e.Marker) |> Array.append [| "$0" |] let (text, cursors) = textWithCursors |> Cursors.extractGroupedWith markers let cursor = cursors["$0"] |> List.head let edits = edits - |> List.map (fun e -> + |> Array.map (fun e -> let range = match cursors[e.Marker] with | [ s; e ] -> range s e @@ -1502,7 +1508,7 @@ $0printfn "$0Result=%i" b$0 |> Seq.choose (fun (l, line) -> match line.IndexOf("incrementNumber", StringComparison.Ordinal) with | -1 -> None - | c -> Some(pos l c)) + | c -> Some(pos (uint32 l) (uint32 c))) |> Seq.exactlyOne cursor @@ -1516,8 +1522,8 @@ $0printfn "$0Result=%i" b$0 let individually = edits - |> List.rev - |> List.fold (fun cursor edit -> cursor |> Cursor.afterEdits [ edit ]) cursor + |> Array.rev + |> Array.fold (fun cursor edit -> cursor |> Cursor.afterEdits [| edit |]) cursor let together = cursor |> Cursor.afterEdits edits @@ -1529,22 +1535,22 @@ $0printfn "$0Result=%i" b$0 testCase "Can add type annotation with parens while cursor stays at end of identifier" <| fun _ -> // `let foo$0 = 42` - let cursor = pos 0 7 + let cursor = pos 0u 7u let edits = - [ { Range = posRange (pos 0 4) - NewText = "(" } - { Range = posRange (pos 0 7) - NewText = ": int" } - { Range = posRange (pos 0 7) - NewText = ")" } ] + [| { Range = posRange (pos 0u 4u) + NewText = "(" } + { Range = posRange (pos 0u 7u) + NewText = ": int" } + { Range = posRange (pos 0u 7u) + NewText = ")" } |] cursor |> Cursor.afterEdits edits |> Expect.equal "Cursor should move to end of identifier" { cursor with - Character = cursor.Character + 1 } ] + Character = cursor.Character + 1u } ] let tests = testList @@ -1688,11 +1694,11 @@ printfn "Result=%i" b$0 """ let expectedPoss = - [ ("$F", pos 0 4) - ("$V", pos 1 4) - ("$0", pos 2 4) - ("$F", pos 2 10) - ("$V", pos 2 12) ] + [ ("$F", pos 0u 4u) + ("$V", pos 1u 4u) + ("$0", pos 2u 4u) + ("$F", pos 2u 10u) + ("$V", pos 2u 12u) ] let expected = (expectedText, expectedPoss) @@ -1727,7 +1733,7 @@ module private Text = testCase "empty string" <| fun _ -> let text = "" - let range = just <| pos 0 0 + let range = just <| pos 0u 0u let expected = "" text @@ -1825,7 +1831,7 @@ $0printfn "$0Result=%i" b$0 let expected = "\nbaz\nlorem ipsum" // text |> assertAfterRemovingIs expected let text = "foo bar\nbaz\nlorem ipsum" - let range = range (pos 0 0) (pos 0 7) + let range = range (pos 0u 0u) (pos 0u 7u) text |> assertRemoveRange range expected testCase "remove first line with line break" <| fun _ -> @@ -1834,7 +1840,7 @@ $0printfn "$0Result=%i" b$0 let expected = "baz\nlorem ipsum" // text |> assertAfterRemovingIs expected let text = "foo bar\nbaz\nlorem ipsum" - let range = range (pos 0 0) (pos 1 0) + let range = range (pos 0u 0u) (pos 1u 0u) text |> assertRemoveRange range expected testCase "remove 2nd line without line breaks" <| fun _ -> @@ -2665,31 +2671,31 @@ let baz = 2 (nameof TextEdit.tryFindError) [ testCase "valid delete edit should should be ok" <| fun _ -> - { Range = { Start = pos 2 2; End = pos 3 3 } + { Range = { Start = pos 2u 2u; End = pos 3u 3u } NewText = "" } |> TextEdit.tryFindError |> Flip.Expect.isNone "Valid delete should be ok" testCase "valid insert edit should should be ok" <| fun _ -> - { Range = { Start = pos 2 3; End = pos 2 3 } + { Range = { Start = pos 2u 3u; End = pos 2u 3u } NewText = "foo" } |> TextEdit.tryFindError |> Flip.Expect.isNone "Valid delete should be ok" testCase "valid replace edit should should be ok" <| fun _ -> - { Range = { Start = pos 2 3; End = pos 4 9 } + { Range = { Start = pos 2u 3u; End = pos 4u 9u } NewText = "foo" } |> TextEdit.tryFindError |> Flip.Expect.isNone "Valid delete should be ok" testCase "empty edit should fail" <| fun _ -> - { Range = { Start = pos 2 4; End = pos 2 4 } + { Range = { Start = pos 2u 4u; End = pos 2u 4u } NewText = "" } |> TextEdit.tryFindError |> Flip.Expect.isSome "Empty edit should fail" testCase "edit with End before Start should fail" <| fun _ -> - { Range = { Start = pos 3 4; End = pos 2 2 } + { Range = { Start = pos 3u 4u; End = pos 2u 2u } NewText = "" } |> TextEdit.tryFindError |> Flip.Expect.isSome "End before Start should fail" ] @@ -2701,7 +2707,7 @@ module private TextEdits = let sortByRangeTests = testList (nameof TextEdits.sortByRange) - [ let test (edits: TextEdit list) = + [ let test (edits: TextEdit[]) = let sorted = edits |> TextEdits.sortByRange Expect.equal (sorted.Length) (edits.Length) "Sorted edits should have same length as input edits" @@ -2712,23 +2718,23 @@ module private TextEdits = // -> preserve order let unsorted = sorted - |> List.pairwise - |> List.filter (fun (r, succ) -> not <| Position.leq r.Range.Start succ.Range.Start) + |> Array.pairwise + |> Array.filter (fun (r, succ) -> not <| Position.leq r.Range.Start succ.Range.Start) // isEmpty doesn't print list when failure... - if not (unsorted |> List.isEmpty) then + if not (unsorted |> Array.isEmpty) then logger.error (eventX "Unsorted: {list}" >> setField "list" unsorted) Expect.isEmpty unsorted "All edits should be sorted" // Note: for this to work edits must be different (-> different NewText) - let idxInEdits (edit: TextEdit) = edits |> List.findIndex ((=) edit) + let idxInEdits (edit: TextEdit) = edits |> Array.findIndex ((=) edit) let unordered = sorted - |> List.indexed - |> List.pairwise - |> List.filter (fun ((_, r), (_, succ)) -> r.Range.Start = succ.Range.Start) - |> List.choose (fun ((i1, e1), (i2, e2)) -> + |> Array.indexed + |> Array.pairwise + |> Array.filter (fun ((_, r), (_, succ)) -> r.Range.Start = succ.Range.Start) + |> Array.choose (fun ((i1, e1), (i2, e2)) -> let iSrc1, iSrc2 = (idxInEdits e1, idxInEdits e2) assert (iSrc1 <> iSrc2) @@ -2740,17 +2746,19 @@ module private TextEdits = SortedIndices = (i1, i2) |} |> Some) // isEmpty doesn't print list when failure... - if not (unordered |> List.isEmpty) then + if not (unordered |> Array.isEmpty) then logger.error (eventX "Unordered: {list}" >> setField "list" unordered) Expect.isEmpty unordered "Edits with same start should keep order" testCase "can sort distinct ranges" <| fun _ -> - [ (1, 5); (1, 1); (3, 2); (8, 5); (5, 4); (5, 6); (4, 11); (1, 7) ] - |> List.mapi (fun i (l, c) -> + [| (1, 5); (1, 1); (3, 2); (8, 5); (5, 4); (5, 6); (4, 11); (1, 7) |] + |> Array.mapi (fun i (l, c) -> // end doesn't really matter (no overlap allowed) - let start = { Line = l; Character = c } + let start = + { Line = uint32 l + Character = uint32 c } { Range = { Start = start; End = start } NewText = $"{i}=({l},{c})" }) @@ -2758,10 +2766,12 @@ module private TextEdits = testCase "can sort all same position ranges" <| fun _ -> - List.replicate 10 (2, 4) - |> List.mapi (fun i (l, c) -> + Array.replicate 10 (2, 4) + |> Array.mapi (fun i (l, c) -> // end doesn't really matter (no overlap allowed) - let start = { Line = l; Character = c } + let start = + { Line = uint32 l + Character = uint32 c } { Range = { Start = start; End = start } NewText = $"{i}=({l},{c})" }) @@ -2769,20 +2779,22 @@ module private TextEdits = testCase "can sort mix of same and different positions" <| fun _ -> - [ (1, 5) - (1, 1) - (3, 2) - (5, 4) - (1, 5) - (8, 5) - (5, 4) - (5, 6) - (4, 11) - (4, 11) - (1, 7) ] - |> List.mapi (fun i (l, c) -> + [| (1, 5) + (1, 1) + (3, 2) + (5, 4) + (1, 5) + (8, 5) + (5, 4) + (5, 6) + (4, 11) + (4, 11) + (1, 7) |] + |> Array.mapi (fun i (l, c) -> // end doesn't really matter (no overlap allowed) - let start = { Line = l; Character = c } + let start = + { Line = uint32 l + Character = uint32 c } { Range = { Start = start; End = start } NewText = $"{i}=({l},{c})" }) @@ -2793,22 +2805,22 @@ module private TextEdits = (nameof TextEdits.tryFindError) [ testCase "valid single edit should succeed" <| fun _ -> - [ { NewText = "foo" - Range = { Start = pos 1 2; End = pos 1 5 } } ] + [| { NewText = "foo" + Range = { Start = pos 1u 2u; End = pos 1u 5u } } |] |> TextEdits.tryFindError |> Flip.Expect.isNone "valid single edit should succeed" testCase "valid multiple edits should succeed" <| fun _ -> - [ { NewText = "foo" - Range = { Start = pos 1 2; End = pos 1 5 } } - { NewText = "bar" - Range = { Start = pos 5 2; End = pos 5 2 } } - { NewText = "baz" - Range = { Start = pos 2 2; End = pos 3 3 } } ] + [| { NewText = "foo" + Range = { Start = pos 1u 2u; End = pos 1u 5u } } + { NewText = "bar" + Range = { Start = pos 5u 2u; End = pos 5u 2u } } + { NewText = "baz" + Range = { Start = pos 2u 2u; End = pos 3u 3u } } |] |> TextEdits.tryFindError |> Flip.Expect.isNone "valid multiple edits should succeed" testCase "no edit should fail" - <| fun _ -> TextEdits.tryFindError [] |> Flip.Expect.isSome "No edit should fail" + <| fun _ -> TextEdits.tryFindError [||] |> Flip.Expect.isSome "No edit should fail" let replace (start, fin) text : TextEdit = { NewText = text Range = { Start = start; End = fin } } @@ -2822,41 +2834,41 @@ module private TextEdits = testCase "single empty edit should fail" <| fun _ -> - TextEdits.tryFindError [ empty (pos 2 3) ] + TextEdits.tryFindError [| empty (pos 2u 3u) |] |> Flip.Expect.isSome "Empty edit should fail" testCase "multiple empty edits should fail" <| fun _ -> - TextEdits.tryFindError [ empty (pos 2 3); empty (pos 3 5); empty (pos 1 1) ] + TextEdits.tryFindError [| empty (pos 2u 3u); empty (pos 3u 5u); empty (pos 1u 1u) |] |> Flip.Expect.isSome "Empty edit should fail" testCase "empty edit in list with valid edits should fail" <| fun _ -> - [ filler <| replace (pos 1 2, pos 1 5) "0" - filler <| replace (pos 5 2, pos 5 2) "1" - empty (pos 1 7) - filler <| replace (pos 2 2, pos 3 3) "1" ] + [| filler <| replace (pos 1u 2u, pos 1u 5u) "0" + filler <| replace (pos 5u 2u, pos 5u 2u) "1" + empty (pos 1u 7u) + filler <| replace (pos 2u 2u, pos 3u 3u) "1" |] |> TextEdits.tryFindError |> Flip.Expect.isSome "Empty edit should fail" testCase "two overlapping edits (Back/Front) on one line should fail" <| fun _ -> - [ replace (pos 1 2, pos 1 5) "front overlap" - replace (pos 1 3, pos 1 7) "back overlap" ] + [| replace (pos 1u 2u, pos 1u 5u) "front overlap" + replace (pos 1u 3u, pos 1u 7u) "back overlap" |] |> TextEdits.tryFindError |> Flip.Expect.isSome "Overlapping edits should fail" testCase "two overlapping edits (Front/Back) on one line should fail" <| fun _ -> - [ replace (pos 1 3, pos 1 7) "back overlap" - replace (pos 1 2, pos 1 5) "front overlap" ] + [| replace (pos 1u 3u, pos 1u 7u) "back overlap" + replace (pos 1u 2u, pos 1u 5u) "front overlap" |] |> TextEdits.tryFindError |> Flip.Expect.isSome "Overlapping edits should fail" testCase "two overlapping edits (Back/Front) over multiple lines should fail" <| fun _ -> - [ replace (pos 1 2, pos 3 5) "front overlap" - replace (pos 2 3, pos 5 7) "back overlap" ] + [| replace (pos 1u 2u, pos 3u 5u) "front overlap" + replace (pos 2u 3u, pos 5u 7u) "back overlap" |] |> TextEdits.tryFindError |> Flip.Expect.isSome "Overlapping edits should fail" @@ -2865,101 +2877,104 @@ module private TextEdits = // valid because: cursor is between characters // -> replace prior to (3,5); replace after (3,5) // -> do not interfere with each other - [ replace (pos 1 2, pos 3 5) "front"; replace (pos 3 5, pos 5 7) "back" ] + [| replace (pos 1u 2u, pos 3u 5u) "front" + replace (pos 3u 5u, pos 5u 7u) "back" |] |> TextEdits.tryFindError |> Flip.Expect.isNone "Touching edits should succeed" testCase "two overlapping edits (Front/Back) over multiple lines should fail" <| fun _ -> - [ replace (pos 2 3, pos 5 7) "back overlap" - replace (pos 1 2, pos 3 5) "front overlap" ] + [| replace (pos 2u 3u, pos 5u 7u) "back overlap" + replace (pos 1u 2u, pos 3u 5u) "front overlap" |] |> TextEdits.tryFindError |> Flip.Expect.isSome "Overlapping edits should fail" testCase "overlapping edits (Back/Front) in list with valid edits should fail" <| fun _ -> - [ filler <| replace (pos 1 1, pos 1 1) "0" - filler <| replace (pos 17 8, pos 19 8) "1" - replace (pos 1 2, pos 3 5) "front overlap" - filler <| replace (pos 7 5, pos 8 9) "2" - replace (pos 2 3, pos 5 7) "back overlap" - filler <| replace (pos 11 1, pos 15 9) "3" ] + [| filler <| replace (pos 1u 1u, pos 1u 1u) "0" + filler <| replace (pos 17u 8u, pos 19u 8u) "1" + replace (pos 1u 2u, pos 3u 5u) "front overlap" + filler <| replace (pos 7u 5u, pos 8u 9u) "2" + replace (pos 2u 3u, pos 5u 7u) "back overlap" + filler <| replace (pos 11u 1u, pos 15u 9u) "3" |] |> TextEdits.tryFindError |> Flip.Expect.isSome "Overlapping edits should fail" testCase "replace inside another replace should fail" <| fun _ -> - [ replace (pos 2 3, pos 4 1) "inside"; replace (pos 1 2, pos 5 7) "outside" ] + [| replace (pos 2u 3u, pos 4u 1u) "inside" + replace (pos 1u 2u, pos 5u 7u) "outside" |] |> TextEdits.tryFindError |> Flip.Expect.isSome "Inside edits should fail" testCase "replace with another replace inside should fail" <| fun _ -> - [ replace (pos 1 2, pos 5 7) "outside"; replace (pos 2 3, pos 4 1) "inside" ] + [| replace (pos 1u 2u, pos 5u 7u) "outside" + replace (pos 2u 3u, pos 4u 1u) "inside" |] |> TextEdits.tryFindError |> Flip.Expect.isSome "Inside edits should fail" testCase "inserts with same position should succeed" <| fun _ -> - [ insert (pos 2 4) "insert 1"; insert (pos 2 4) "insert 2" ] + [| insert (pos 2u 4u) "insert 1"; insert (pos 2u 4u) "insert 2" |] |> TextEdits.tryFindError |> Flip.Expect.isNone "Same position inserts should succeed" testCase "inserts with same position followed by replace starting at same position should succeed" <| fun _ -> - [ insert (pos 2 4) "insert 1" - insert (pos 2 4) "insert 2" - replace (pos 2 4, pos 4 7) "replace" ] + [| insert (pos 2u 4u) "insert 1" + insert (pos 2u 4u) "insert 2" + replace (pos 2u 4u, pos 4u 7u) "replace" |] |> TextEdits.tryFindError |> Flip.Expect.isNone "Same position inserts followed by replace should succeed" testCase "replace before insert on same position should fail" <| fun _ -> - [ replace (pos 2 4, pos 4 7) "replace"; insert (pos 2 4) "a" ] + [| replace (pos 2u 4u, pos 4u 7u) "replace"; insert (pos 2u 4u) "a" |] |> TextEdits.tryFindError |> Flip.Expect.isSome "Replace before insert on same position should fail" testCase "inserts with same position followed by replace at same position intermingled with other valid edits should succeed" <| fun _ -> - [ filler <| replace (pos 6 7, pos 7 9) "0" - insert (pos 2 4) "insert 1" - filler <| replace (pos 1 4, pos 2 1) "1" - filler <| replace (pos 11 17, pos 18 19) "2" - insert (pos 2 4) "insert 2" - filler <| replace (pos 6 1, pos 6 2) "3" - replace (pos 2 4, pos 4 7) "replace" - filler <| replace (pos 9 2, pos 9 7) "4" ] + [| filler <| replace (pos 6u 7u, pos 7u 9u) "0" + insert (pos 2u 4u) "insert 1" + filler <| replace (pos 1u 4u, pos 2u 1u) "1" + filler <| replace (pos 11u 17u, pos 18u 19u) "2" + insert (pos 2u 4u) "insert 2" + filler <| replace (pos 6u 1u, pos 6u 2u) "3" + replace (pos 2u 4u, pos 4u 7u) "replace" + filler <| replace (pos 9u 2u, pos 9u 7u) "4" |] |> TextEdits.tryFindError |> Flip.Expect.isNone "Same position inserts followed by replace should succeed" testCase "replace before insert on same position intermingled with other valid edits should fail" <| fun _ -> - [ filler <| replace (pos 6 7, pos 7 9) "0" - insert (pos 2 4) "insert 1" - filler <| replace (pos 1 4, pos 2 1) "1" - filler <| replace (pos 11 17, pos 18 19) "2" - replace (pos 2 4, pos 4 7) "replace" - filler <| replace (pos 6 1, pos 6 2) "3" - insert (pos 2 4) "insert 2" - filler <| replace (pos 9 2, pos 9 7) "4" ] + [| filler <| replace (pos 6u 7u, pos 7u 9u) "0" + insert (pos 2u 4u) "insert 1" + filler <| replace (pos 1u 4u, pos 2u 1u) "1" + filler <| replace (pos 11u 17u, pos 18u 19u) "2" + replace (pos 2u 4u, pos 4u 7u) "replace" + filler <| replace (pos 6u 1u, pos 6u 2u) "3" + insert (pos 2u 4u) "insert 2" + filler <| replace (pos 9u 2u, pos 9u 7u) "4" |] |> TextEdits.tryFindError |> Flip.Expect.isSome "Replace before insert on same position should fail" testCase "two replaces in same position should fail" <| fun _ -> - [ replace (pos 2 4, pos 5 9) "replace 1" - replace (pos 2 4, pos 4 7) "replace 2" ] + [| replace (pos 2u 4u, pos 5u 9u) "replace 1" + replace (pos 2u 4u, pos 4u 7u) "replace 2" |] |> TextEdits.tryFindError |> Flip.Expect.isSome "Two replaces in same position should fail" testCase "two replaces in same position intermingled with other valid edits should fail should fail" <| fun _ -> - [ filler <| replace (pos 6 7, pos 7 9) "0" - replace (pos 2 4, pos 5 9) "replace 1" - filler <| replace (pos 1 4, pos 2 1) "1" - replace (pos 2 4, pos 4 7) "replace 2" - filler <| replace (pos 6 1, pos 6 2) "2" ] + [| filler <| replace (pos 6u 7u, pos 7u 9u) "0" + replace (pos 2u 4u, pos 5u 9u) "replace 1" + filler <| replace (pos 1u 4u, pos 2u 1u) "1" + replace (pos 2u 4u, pos 4u 7u) "replace 2" + filler <| replace (pos 6u 1u, pos 6u 2u) "2" |] |> TextEdits.tryFindError |> Flip.Expect.isSome "Two replaces in same position should fail" ] diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.fs b/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.fs index fb06abb78..c30d26d28 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.fs @@ -12,12 +12,13 @@ open FsToolkit.ErrorHandling /// Note: Only `\n` is supported. Neither `\r\n` nor `\r` produce correct results. module Cursor = /// 0-based - let inline private pos line column: Position = { Line = line; Character = column } + let inline private pos line column : Position = { Line = line; Character = column } /// Cursor Marker in text. /// Single marker: Position /// Two markers: Range - let [] Marker = "$0" + [] + let Marker = "$0" (* Identities: @@ -34,16 +35,13 @@ module Cursor = /// /// Index might be `text.Length` (-> cursor AFTER last character). /// All other out of text range indices throw exception. - let beforeIndex (i: int) (text: string) : Position = - assert(i >= 0) - assert(i <= text.Length) + let beforeIndex (i: uint32) (text: string) : Position = + assert (i <= uint32 text.Length) - let linesBefore = - text.Substring(0, i) - |> Text.lines + let linesBefore = text.Substring(0, int i) |> Text.lines // line & char are 0-based - let line = linesBefore.Length - 1 - let char = linesBefore |> Array.last |> String.length + let line = uint32 (linesBefore.Length - 1) + let char = linesBefore |> Array.last |> String.length |> uint32 pos line char /// Returns index of first `$0` (`Cursor.Marker`) and the updated input text without the cursor marker. @@ -53,13 +51,11 @@ module Cursor = let tryExtractIndex (text: string) = match text.IndexOf(Marker, StringComparison.Ordinal) with | -1 -> None - | i -> - (i, text.Remove(i, Marker.Length)) - |> Some + | i -> (uint32 i, text.Remove(i, Marker.Length)) |> Some + /// `tryExtractIndex`, but fails when there's no cursor let assertExtractIndex = - tryExtractIndex - >> Option.defaultWith (fun _ -> failtest "No cursor") + tryExtractIndex >> Option.defaultWith (fun _ -> failtest "No cursor") /// Extracts first cursor marked with any of `markers`. Remaining cursors aren't touched let tryExtractPositionMarkedWithAnyOf (markers: string[]) (text: string) = @@ -67,36 +63,39 @@ module Cursor = let markersInLine = markers |> Array.choose (fun marker -> - match line.IndexOf(marker, StringComparison.Ordinal) with - | -1 -> None - | column -> Some (marker, column) - ) + match line.IndexOf(marker, StringComparison.Ordinal) with + | -1 -> None + | column -> Some(marker, uint32 column)) + match markersInLine with | [||] -> None | _ -> - let (marker, column) = markersInLine |> Array.minBy snd - let line = line.Substring(0, column) + line.Substring(column + marker.Length) - Some (marker, column, line) + let (marker, column) = markersInLine |> Array.minBy snd + + let line = + line.Substring(0, int column) + line.Substring(int column + marker.Length) + + Some(marker, column, line) // Note: Input `lines` gets mutated to remove cursor let tryFindAnyCursor (lines: string[]) = lines - |> Seq.mapi (fun i l -> (i,l)) - |> Seq.tryPick (fun (i,line) -> - tryFindAnyCursorInLine line - |> Option.map (fun (marker, c, line) -> (marker, pos i c, line)) - ) + |> Seq.mapi (fun i l -> (uint32 i, l)) + |> Seq.tryPick (fun (i, line) -> + tryFindAnyCursorInLine line + |> Option.map (fun (marker, c, line) -> (marker, pos i c, line))) |> function - | None -> None - | Some (marker, p,line) -> - lines.[p.Line] <- line - Some ((marker, p), lines) + | None -> None + | Some(marker, p, line) -> + lines.[int p.Line] <- line + Some((marker, p), lines) let lines = text |> Text.lines + match tryFindAnyCursor lines with | None -> None - | Some ((marker, p), lines) -> - let text = lines |> String.concat "\n" - Some ((marker, p), text) + | Some((marker, p), lines) -> + let text = lines |> String.concat "\n" + Some((marker, p), text) /// Returns Position of first `$0` (`Cursor.Marker`) and the updated input text without the cursor marker. /// Only the first `$0` is processed. @@ -105,10 +104,10 @@ module Cursor = let tryExtractPosition = tryExtractPositionMarkedWithAnyOf [| Marker |] >> Option.map (fun ((_, pos), line) -> (pos, line)) + /// `tryExtractPosition`, but fails when there's no cursor let assertExtractPosition = - tryExtractPosition - >> Option.defaultWith (fun _ -> failtest "No cursor") + tryExtractPosition >> Option.defaultWith (fun _ -> failtest "No cursor") /// Returns Range between the first two `$0` (`Cursor.Marker`) and the updated text without the two cursor markers. /// @@ -116,14 +115,14 @@ module Cursor = let tryExtractRange (text: string) = match tryExtractPosition text with | None -> None - | Some (start, text) -> - let (fin, text) = tryExtractPosition text |> Option.defaultValue (start, text) - let range = { Start = start; End = fin } - Some (range, text) + | Some(start, text) -> + let (fin, text) = tryExtractPosition text |> Option.defaultValue (start, text) + let range = { Start = start; End = fin } + Some(range, text) + /// `tryExtractRange`, but fails when there's no cursor. let assertExtractRange = - tryExtractRange - >> Option.defaultWith (fun _ -> failtest "No cursor(s)") + tryExtractRange >> Option.defaultWith (fun _ -> failtest "No cursor(s)") /// Position is between characters, while index is on character. /// For Insert & Remove: character indices @@ -138,25 +137,22 @@ module Cursor = let lines = text |> Text.lines // check in range - if pos.Line >= lines.Length then - $"Line {pos.Line} is out of text range. Text has {lines.Length} lines." - |> Error - elif pos.Character > lines.[pos.Line].Length then + if pos.Line >= uint32 lines.Length then + $"Line {pos.Line} is out of text range. Text has {lines.Length} lines." |> Error + elif pos.Character > uint32 lines.[int pos.Line].Length then // `>`: character can be AFTER last char in string - $"Character {pos.Character} is out of line range {pos.Line}. Line {pos.Line} has length of {lines[pos.Line].Length}." + $"Character {pos.Character} is out of line range {pos.Line}. Line {pos.Line} has length of {lines[int pos.Line].Length}." |> Error else let offsetToLine = lines - |> Seq.take pos.Line // `Line` is 0-based -> can be used as length - |> Seq.sumBy (String.length >> (+) 1) // `+ 1`: `\n` + |> Seq.take (int pos.Line) // `Line` is 0-based -> can be used as length + |> Seq.sumBy (String.length >> uint32 >> (+) 1u) // `+ 1`: `\n` + + offsetToLine + pos.Character |> Ok - offsetToLine + pos.Character - |> Ok /// `tryIndexOf`, but fails when position is invalid - let assertIndexOf pos = - tryIndexOf pos - >> Result.valueOr (failtestf "Invalid position: %s") + let assertIndexOf pos = tryIndexOf pos >> Result.valueOr (failtestf "Invalid position: %s") /// Calculates cursors position after all edits are applied. /// @@ -190,63 +186,68 @@ module Cursor = /// Replacement is considered: First delete (-> move cursor to front), then insert (-> cursor stays) /// /// Note: `edits` must be sorted by range! - let afterEdits (edits: TextEdit list) (pos: Position) = + let afterEdits (edits: TextEdit[]) (pos: Position) = edits - |> List.filter (fun edit -> edit.Range.Start < pos) - |> List.rev - |> List.fold (fun pos edit -> - // remove deleted range from pos - let pos = - if Range.isPosition edit.Range then - // just insert - pos - elif edit.Range |> Range.containsLoosely pos then - // pos inside edit -> fall to start of delete - edit.Range.Start - else - // everything to delete is before cursor - let (s,e) = edit.Range.Start, edit.Range.End - // always <= 0 (nothing gets inserted here) - let deltaLine = s.Line - e.Line - let deltaChar = - if e.Line < pos.Line then - // doesn't touch line of pos - 0 - else - - e.Character + s.Character - { Line = pos.Line + deltaLine; Character = pos.Character + deltaChar } - - // add new text to pos - let pos = - if System.String.IsNullOrEmpty edit.NewText then - // just delete - pos - elif pos <= edit.Range.Start then - // insert is after pos -> doesn't change cursor - // happens when cursor inside replacement -> cursor move to front of deletion - pos - else - let lines = - edit.NewText - |> Text.removeCarriageReturn - |> Text.lines - let deltaLine = lines.Length - 1 - let deltaChar = - if edit.Range.Start.Line = pos.Line then - let lastLine = lines |> Array.last - if lines.Length = 1 then - // doesn't introduce new line - lastLine.Length + |> Array.filter (fun edit -> edit.Range.Start < pos) + |> Array.rev + |> Array.fold + (fun pos edit -> + // remove deleted range from pos + let pos = + if Range.isPosition edit.Range then + // just insert + pos + elif edit.Range |> Range.containsLoosely pos then + // pos inside edit -> fall to start of delete + edit.Range.Start + else + // everything to delete is before cursor + let (s, e) = edit.Range.Start, edit.Range.End + // always <= 0 (nothing gets inserted here) + let deltaLine = s.Line - e.Line + + let deltaChar = + if e.Line < pos.Line then + // doesn't touch line of pos + 0 else - // inserts new line - - edit.Range.Start.Character + lastLine.Length - else - // doesn't touch line of pos - 0 - { Line = pos.Line + deltaLine; Character = pos.Character + deltaChar } + -(int e.Character) + int s.Character + + { Line = pos.Line + deltaLine + Character = uint32 (int pos.Character + deltaChar) } + + // add new text to pos + let pos = + if System.String.IsNullOrEmpty edit.NewText then + // just delete + pos + elif pos <= edit.Range.Start then + // insert is after pos -> doesn't change cursor + // happens when cursor inside replacement -> cursor move to front of deletion + pos + else + let lines = edit.NewText |> Text.removeCarriageReturn |> Text.lines + let deltaLine = lines.Length - 1 + + let deltaChar = + if edit.Range.Start.Line = pos.Line then + let lastLine = lines |> Array.last + + if lines.Length = 1 then + // doesn't introduce new line + lastLine.Length + else + // inserts new line + -(int edit.Range.Start.Character) + lastLine.Length + else + // doesn't touch line of pos + 0 + + { Line = uint32 (int pos.Line + deltaLine) + Character = uint32 (int pos.Character + deltaChar) } + pos) pos - ) pos module Cursors = /// For each cursor (`$0`) in text: return text with just that one cursor @@ -259,20 +260,18 @@ module Cursors = | i -> let textWithSingleCursor = textWithCursors.Substring(0, i + Cursor.Marker.Length) - + - textWithCursors.Substring(i + Cursor.Marker.Length).Replace(Cursor.Marker, "") + + textWithCursors.Substring(i + Cursor.Marker.Length).Replace(Cursor.Marker, "") + let textWithCursors = textWithCursors.Remove(i, Cursor.Marker.Length) collect (textWithSingleCursor :: textsWithSingleCursor) textWithCursors + collect [] textWithCursors /// Returns all cursor (`$0`) positions and the text without any cursors. /// /// Unlike `iter` this extracts positions instead of reducing to texts with one cursor let extract (textWithCursors: string) = - let tps = - textWithCursors - |> iter - |> List.map (Cursor.assertExtractPosition) + let tps = textWithCursors |> iter |> List.map (Cursor.assertExtractPosition) let text = tps |> List.head |> snd let poss = tps |> List.map fst (text, poss) @@ -283,20 +282,24 @@ module Cursors = let extractWith (markers: string[]) (text: string) = let rec collect poss text = match Cursor.tryExtractPositionMarkedWithAnyOf markers text with - | None -> (text,poss) - | Some ((marker, pos), text) -> - let poss = (marker, pos) :: poss - collect poss text + | None -> (text, poss) + | Some((marker, pos), text) -> + let poss = (marker, pos) :: poss + collect poss text + let (text, cursors) = collect [] text (text, cursors |> List.rev) + /// Like `extractWith`, but additional groups cursor positions by marker let extractGroupedWith (markers: string[]) (text: string) = let (text, cursors) = extractWith markers text + let cursors = cursors |> List.groupBy fst |> List.map (fun (marker, poss) -> (marker, poss |> List.map snd)) |> Map.ofList + (text, cursors) @@ -305,12 +308,14 @@ module Text = let private indicesOf (range: Range) (text: string) = result { let! start = Cursor.tryIndexOf range.Start text + if range.Start = range.End then return (start, start) else let! fin = Cursor.tryIndexOf range.End text return (start, fin) } + let remove (range: Range) (text: string) = result { if range.Start = range.End then @@ -318,7 +323,7 @@ module Text = else let! (start, fin) = indicesOf range text // Including start, excluding fin (cursor is BEFORE char) - return text.Remove(start, fin - start) + return text.Remove(int start, int (fin - start)) } let insert (pos: Position) (insert: string) (text: string) = @@ -328,13 +333,11 @@ module Text = else let! idx = Cursor.tryIndexOf pos text // insert BEFORE idx (cursor is BEFORE char) - return text.Insert (idx, insert) + return text.Insert(int idx, insert) } let replace (range: Range) (replacement: string) (text: string) = - text - |> remove range - |> Result.bind (insert range.Start replacement) + text |> remove range |> Result.bind (insert range.Start replacement) module TextEdit = @@ -349,10 +352,7 @@ module TextEdit = let inserts (edit: TextEdit) = not <| System.String.IsNullOrEmpty edit.NewText let replaces (edit: TextEdit) = deletes edit && inserts edit - let doesNothing (edit: TextEdit) = - not (edit |> deletes) - && - not (edit |> inserts) + let doesNothing (edit: TextEdit) = not (edit |> deletes) && not (edit |> inserts) // **Note**: // VS Code allows TextEdits, that might not be strictly valid according to LSP Specs [^1]: @@ -374,15 +374,7 @@ module TextEdit = /// * Does something (-> must insert or delete (or both -> replace) something) /// * Note: empty edit is technically valid, but in practice it's most likely an error let tryFindError (edit: TextEdit) = - if edit.Range.Start.Line < 0 then - Some "Expected positive Start.Line, but was negative" - else if edit.Range.Start.Character < 0 then - Some "Expected positive Start.Character, but was negative" - else if edit.Range.End.Line < 0 then - Some "Expected positive End.Line, but was negative" - else if edit.Range.End.Character < 0 then - Some "Expected positive End.Character, but was negative" - else if edit.Range.Start > edit.Range.End then + if edit.Range.Start > edit.Range.End then Some "Expected Range.Start <= Range.End, but was Start > End" else if edit |> doesNothing then Some "Expected change, but does nothing (neither delete nor insert)" @@ -404,50 +396,61 @@ module TextEdits = /// > or any number of inserts followed by a single remove or replace edit. /// > If multiple inserts have the same position, the order in the array defines the order /// > in which the inserted strings appear in the resulting text. - let tryFindError (edits: TextEdit list) = - let rec tryFindOverlappingEditExample (edits: TextEdit list) = + let tryFindError (edits: TextEdit[]) = + let rec tryFindOverlappingEditExample (edits: TextEdit[]) = match edits with - | [] | [_] -> None - | edit :: edits -> - match edits |> List.tryFind (fun e -> Range.overlapsLoosely edit.Range e.Range) with - | Some overlapping -> - Some (edit, overlapping) - | None -> - tryFindOverlappingEditExample edits + | [||] + | [| _ |] -> None + | edits -> + let edit = edits.[0] + let edits = edits.[1..] + + match edits |> Array.tryFind (fun e -> Range.overlapsLoosely edit.Range e.Range) with + | Some overlapping -> Some(edit, overlapping) + | None -> tryFindOverlappingEditExample edits + let (|Overlapping|_|) = tryFindOverlappingEditExample + let (|Invalids|_|) = - List.choose (fun edit -> edit |> TextEdit.tryFindError |> Option.map (fun err -> (edit, err))) - >> function | [] -> None | errs -> Some errs - let findSameStarts (edits: TextEdit list) = + Array.choose (fun edit -> edit |> TextEdit.tryFindError |> Option.map (fun err -> (edit, err))) + >> function + | [||] -> None + | errs -> Some errs + + let findSameStarts (edits: TextEdit[]) = edits - |> List.groupBy (fun e -> e.Range.Start) - |> List.filter (fun (_, es) -> List.length es > 1) - |> List.map snd + |> Array.groupBy (fun e -> e.Range.Start) + |> Array.choose (fun (_, es) -> if es.Length > 1 then Some es else None) + /// For same position: all inserts must be before at most one Delete/Replace /// Note: doesn't check edits for same position - let rec replaceNotLast (edits: TextEdit list) = + let rec replaceNotLast (edits: TextEdit[]) = match edits with - | [] | [_] -> false - | a::edits -> - assert(List.length edits >= 1) - (TextEdit.deletes a) || (replaceNotLast edits) + | [||] + | [| _ |] -> false + | edits -> + let a = edits.[0] + let edits = edits.[1..] + assert (edits.Length >= 1) + (TextEdit.deletes a) || (replaceNotLast edits) + let (|ReplaceNotLast|_|) = findSameStarts - >> List.filter (replaceNotLast) - >> function | [] -> None | ss -> Some ss + >> Array.filter (replaceNotLast) + >> function + | [||] -> None + | ss -> Some ss match edits with // there must be edits - | [] -> Some "Expected at least one TextEdit, but were none" + | [||] -> Some "Expected at least one TextEdit, but were none" // edits should be valid | Invalids errs -> - sprintf - "Expected all TextEdits to be valid, but there was at least one erroneous Edit. Invalid Edits: %A" - errs + sprintf "Expected all TextEdits to be valid, but there was at least one erroneous Edit. Invalid Edits: %A" errs |> Some // No overlapping - | Overlapping (edit1, edit2) -> - Some $"Expected no overlaps, but at least two edits overlap: {edit1.Range} and {edit2.Range}" + | Overlapping(edit1, edit2) -> + Some $"Expected no overlaps, but at least two edits overlap: {edit1.Range} and {edit2.Range}" // For same position: all inserts must be before at most one Delete/Replace | ReplaceNotLast errs -> sprintf @@ -459,27 +462,23 @@ module TextEdits = /// Sorts edits by range (`Start`). /// Order is preserved for edits with same `Start`. - let sortByRange (edits: TextEdit list) = + let sortByRange (edits: TextEdit[]) = edits - |> List.sortWith (fun e1 e2 -> + |> Array.sortWith (fun e1 e2 -> match e1.Range.Start.Line.CompareTo(e2.Range.Start.Line) with - | 0 -> - e1.Range.Start.Character.CompareTo(e2.Range.Start.Character) - | r -> r - ) + | 0 -> e1.Range.Start.Character.CompareTo(e2.Range.Start.Character) + | r -> r) /// Applies the passed edits from last to first (sorted by range) let apply edits text = - let edits = edits |> sortByRange |> List.rev - List.fold (fun text edit -> text |> Result.bind (TextEdit.apply edit)) (Ok text) edits + let edits = edits |> sortByRange |> Array.rev + Array.fold (fun text edit -> text |> Result.bind (TextEdit.apply edit)) (Ok text) edits /// `tryFindError` before `apply` let applyWithErrorCheck edits text = match tryFindError edits with | Some error -> Error error - | None -> - text - |> apply edits + | None -> text |> apply edits module WorkspaceEdit = /// Extract `TextEdit[]` from either `DocumentChanges` or `Changes`. @@ -499,41 +498,41 @@ module WorkspaceEdit = let checkDocument (uri) (version) = if uri <> textDocument.Uri then Some $"Edit should be for document `{textDocument.Uri}`, but was for `{uri}`" + else if Some textDocument.Version <> version then + // only compare `Version` when `textDocument` and `version` has a Version. Otherwise ignore + Some $"Edit should be for document version `{textDocument.Version}`, but version was `{version}`" else - if Some textDocument.Version <> version then - // only compare `Version` when `textDocument` and `version` has a Version. Otherwise ignore - Some $"Edit should be for document version `{textDocument.Version}`, but version was `{version}`" - else - None - - match (workspaceEdit.DocumentChanges, workspaceEdit.Changes) with - | None, None -> - Error "Expected changes, but `DocumentChanges` and `Changes` were both `None`." - | Some _, Some _ -> - Error "Expected either `DocumentChanges` or `Changes`, but was both." - | Some [||], None -> - Error "Expected changes, but `DocumentChanges` was empty." - | Some changes, None -> - match changes |> Array.tryPick (fun c -> checkDocument c.TextDocument.Uri c.TextDocument.Version) with - | Some error -> Error error - | _ -> - changes - |> Seq.map (fun c -> c.Edits) - |> Seq.collect id - |> Seq.toList - |> Ok - | None, Some changes when changes.IsEmpty -> - Error "Expected changes, but `Changes` was empty." - | None, Some changes -> - match changes |> Seq.tryPick (fun c -> checkDocument c.Key None) with - | Some error -> Error error - | _ -> - changes.Values - |> Seq.collect id - |> Seq.toList - |> Ok + None + + (match (workspaceEdit.DocumentChanges, workspaceEdit.Changes) with + | None, None -> Error "Expected changes, but `DocumentChanges` and `Changes` were both `None`." + | Some _, Some _ -> Error "Expected either `DocumentChanges` or `Changes`, but was both." + | Some [||], None -> Error "Expected changes, but `DocumentChanges` was empty." + | Some changes, None -> + let changes = + changes + |> Array.choose (function + | U4.C1(e: TextDocumentEdit) -> Some e + | _ -> None) + + match + changes + |> Array.tryPick (fun c -> checkDocument c.TextDocument.Uri c.TextDocument.Version) + with + | Some error -> Error error + | _ -> changes |> Seq.map (fun c -> c.Edits) |> Seq.collect id |> Seq.toArray |> Ok + | None, Some changes when changes.IsEmpty -> Error "Expected changes, but `Changes` was empty." + | None, Some changes -> + match changes |> Seq.tryPick (fun c -> checkDocument c.Key None) with + | Some error -> Error error + | _ -> changes.Values |> Seq.collect id |> Seq.toArray |> Array.map U2.C1 |> Ok) |> Result.bind (fun edits -> - match TextEdits.tryFindError edits with - | Some error -> Error error - | None -> Ok edits - ) + let edits = + edits + |> Array.choose (function + | U2.C1 e -> Some e + | _ -> None) + + match TextEdits.tryFindError edits with + | Some error -> Error error + | None -> Ok edits) diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.fsi b/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.fsi index 7379f9af1..f69e991fd 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.fsi +++ b/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.fsi @@ -10,155 +10,155 @@ open FsToolkit.ErrorHandling /// /// Note: Only `\n` is supported. Neither `\r\n` nor `\r` produce correct results. module Cursor = - /// 0-based - val inline private pos: line: int -> column: int -> Position + /// 0-based + val inline private pos: line: uint32 -> column: uint32 -> Position - /// Cursor Marker in text. - /// Single marker: Position - /// Two markers: Range - [] - val Marker: string = "$0" + /// Cursor Marker in text. + /// Single marker: Position + /// Two markers: Range + [] + val Marker: string = "$0" - /// Returns Cursor Position BEFORE index - /// - /// Index might be `text.Length` (-> cursor AFTER last character). - /// All other out of text range indices throw exception. - val beforeIndex: i: int -> text: string -> Position - /// Returns index of first `$0` (`Cursor.Marker`) and the updated input text without the cursor marker. - /// - /// Note: Cursor Position is BEFORE index. - /// Note: Index might be `text.Length` (-> Cursor AFTER last char in text) - val tryExtractIndex: text: string -> (int * string) option - /// `tryExtractIndex`, but fails when there's no cursor - val assertExtractIndex: (string -> int * string) - /// Extracts first cursor marked with any of `markers`. Remaining cursors aren't touched - val tryExtractPositionMarkedWithAnyOf: markers: string[] -> text: string -> ((string * Position) * string) option - /// Returns Position of first `$0` (`Cursor.Marker`) and the updated input text without the cursor marker. - /// Only the first `$0` is processed. - /// - /// Note: Cursor Position is BETWEEN characters and might be outside of text range (cursor AFTER last character) - val tryExtractPosition: (string -> (Position * string) option) - /// `tryExtractPosition`, but fails when there's no cursor - val assertExtractPosition: (string -> Position * string) - /// Returns Range between the first two `$0` (`Cursor.Marker`) and the updated text without the two cursor markers. - /// - /// If there's only one cursor marker, the range covers exactly that position (`Start = End`) - val tryExtractRange: text: string -> (Range * string) option - /// `tryExtractRange`, but fails when there's no cursor. - val assertExtractRange: (string -> Range * string) - /// Position is between characters, while index is on character. - /// For Insert & Remove: character indices - /// - /// Returned index is AFTER cursor: - /// * `Column=0`: before first char; `Index=0`: on first char - /// * `Column=1`: after first char, before 2nd char; `Index=1`: on 2nd char - /// * `Column=max`: after last char; `Index=max`: AFTER last char in line (-> `\n` or end of string) - val tryIndexOf: pos: Position -> text: string -> Result - /// `tryIndexOf`, but fails when position is invalid - val assertIndexOf: pos: Position -> (string -> int) - /// Calculates cursors position after all edits are applied. - /// - /// When cursor inside a changed area: - /// * deleted: cursor moves to start of deletion: - /// ```fsharp - /// let foo = 42 $|+ $013 $|+ 123 - /// ``` - /// -> delete inside `$|` - /// ```fsharp - /// let foo = 42 $0+ 123 - /// ``` - /// * inserted: cursor stays at start of insert - /// ```fsharp - /// let foo = 42 $0+ 123 - /// ``` - /// -> insert at cursor pos - /// ```fsharp - /// let foo = 42 $0+ 13 + 123 - /// ``` - /// * changes: cursors moved to start of replacement - /// ```fsharp - /// let foo = 42 $|+ $013 $|+ 123 - /// ``` - /// -> replace inside `$|` - /// ```fsharp - /// let foo = 42 $0- 7 + 123 - /// ``` - /// -> like deletion - /// * Implementation detail: - /// Replacement is considered: First delete (-> move cursor to front), then insert (-> cursor stays) - /// - /// Note: `edits` must be sorted by range! - val afterEdits: edits: TextEdit list -> pos: Position -> Position + /// Returns Cursor Position BEFORE index + /// + /// Index might be `text.Length` (-> cursor AFTER last character). + /// All other out of text range indices throw exception. + val beforeIndex: i: uint32 -> text: string -> Position + /// Returns index of first `$0` (`Cursor.Marker`) and the updated input text without the cursor marker. + /// + /// Note: Cursor Position is BEFORE index. + /// Note: Index might be `text.Length` (-> Cursor AFTER last char in text) + val tryExtractIndex: text: string -> (uint32 * string) option + /// `tryExtractIndex`, but fails when there's no cursor + val assertExtractIndex: (string -> uint32 * string) + /// Extracts first cursor marked with any of `markers`. Remaining cursors aren't touched + val tryExtractPositionMarkedWithAnyOf: markers: string[] -> text: string -> ((string * Position) * string) option + /// Returns Position of first `$0` (`Cursor.Marker`) and the updated input text without the cursor marker. + /// Only the first `$0` is processed. + /// + /// Note: Cursor Position is BETWEEN characters and might be outside of text range (cursor AFTER last character) + val tryExtractPosition: (string -> (Position * string) option) + /// `tryExtractPosition`, but fails when there's no cursor + val assertExtractPosition: (string -> Position * string) + /// Returns Range between the first two `$0` (`Cursor.Marker`) and the updated text without the two cursor markers. + /// + /// If there's only one cursor marker, the range covers exactly that position (`Start = End`) + val tryExtractRange: text: string -> (Range * string) option + /// `tryExtractRange`, but fails when there's no cursor. + val assertExtractRange: (string -> Range * string) + /// Position is between characters, while index is on character. + /// For Insert & Remove: character indices + /// + /// Returned index is AFTER cursor: + /// * `Column=0`: before first char; `Index=0`: on first char + /// * `Column=1`: after first char, before 2nd char; `Index=1`: on 2nd char + /// * `Column=max`: after last char; `Index=max`: AFTER last char in line (-> `\n` or end of string) + val tryIndexOf: pos: Position -> text: string -> Result + /// `tryIndexOf`, but fails when position is invalid + val assertIndexOf: pos: Position -> (string -> uint32) + /// Calculates cursors position after all edits are applied. + /// + /// When cursor inside a changed area: + /// * deleted: cursor moves to start of deletion: + /// ```fsharp + /// let foo = 42 $|+ $013 $|+ 123 + /// ``` + /// -> delete inside `$|` + /// ```fsharp + /// let foo = 42 $0+ 123 + /// ``` + /// * inserted: cursor stays at start of insert + /// ```fsharp + /// let foo = 42 $0+ 123 + /// ``` + /// -> insert at cursor pos + /// ```fsharp + /// let foo = 42 $0+ 13 + 123 + /// ``` + /// * changes: cursors moved to start of replacement + /// ```fsharp + /// let foo = 42 $|+ $013 $|+ 123 + /// ``` + /// -> replace inside `$|` + /// ```fsharp + /// let foo = 42 $0- 7 + 123 + /// ``` + /// -> like deletion + /// * Implementation detail: + /// Replacement is considered: First delete (-> move cursor to front), then insert (-> cursor stays) + /// + /// Note: `edits` must be sorted by range! + val afterEdits: edits: TextEdit[] -> pos: Position -> Position module Cursors = - /// For each cursor (`$0`) in text: return text with just that one cursor - /// - /// Note: doesn't trim input! - val iter: textWithCursors: string -> string list - /// Returns all cursor (`$0`) positions and the text without any cursors. - /// - /// Unlike `iter` this extracts positions instead of reducing to texts with one cursor - val extract: textWithCursors: string -> string * Position list - /// Like `extract`, but instead of just extracting Cursors marked with `Cursor.Marker` (`$0`), - /// this here extract all specified markers. - val extractWith: markers: string[] -> text: string -> string * (string * Position) list - /// Like `extractWith`, but additional groups cursor positions by marker - val extractGroupedWith: markers: string[] -> text: string -> string * Map + /// For each cursor (`$0`) in text: return text with just that one cursor + /// + /// Note: doesn't trim input! + val iter: textWithCursors: string -> string list + /// Returns all cursor (`$0`) positions and the text without any cursors. + /// + /// Unlike `iter` this extracts positions instead of reducing to texts with one cursor + val extract: textWithCursors: string -> string * Position list + /// Like `extract`, but instead of just extracting Cursors marked with `Cursor.Marker` (`$0`), + /// this here extract all specified markers. + val extractWith: markers: string[] -> text: string -> string * (string * Position) list + /// Like `extractWith`, but additional groups cursor positions by marker + val extractGroupedWith: markers: string[] -> text: string -> string * Map module Text = - val remove: range: Range -> text: string -> Result - val insert: pos: Position -> insert: string -> text: string -> Result - val replace: range: Range -> replacement: string -> text: string -> Result + val remove: range: Range -> text: string -> Result + val insert: pos: Position -> insert: string -> text: string -> Result + val replace: range: Range -> replacement: string -> text: string -> Result module TextEdit = - val apply: edit: TextEdit -> (string -> Result) - val deletes: edit: TextEdit -> bool - val inserts: edit: TextEdit -> bool - val replaces: edit: TextEdit -> bool - val doesNothing: edit: TextEdit -> bool - /// Checks passed `edit` for errors: - /// * Positive Lines & Characters in Ranges - /// * Note: doesn't test if range is inside text! Just simple positive test. - /// * Start Range must be before or equal End Range - /// * Does something (-> must insert or delete (or both -> replace) something) - /// * Note: empty edit is technically valid, but in practice it's most likely an error - val tryFindError: edit: TextEdit -> string option + val apply: edit: TextEdit -> (string -> Result) + val deletes: edit: TextEdit -> bool + val inserts: edit: TextEdit -> bool + val replaces: edit: TextEdit -> bool + val doesNothing: edit: TextEdit -> bool + /// Checks passed `edit` for errors: + /// * Positive Lines & Characters in Ranges + /// * Note: doesn't test if range is inside text! Just simple positive test. + /// * Start Range must be before or equal End Range + /// * Does something (-> must insert or delete (or both -> replace) something) + /// * Note: empty edit is technically valid, but in practice it's most likely an error + val tryFindError: edit: TextEdit -> string option module TextEdits = - /// Checks edits for: - /// * There's at least one TextEdit - /// * All TextEdits are valid (`TextEdit.tryFindError`) - /// * Edits don't overlap - /// * For same position: All inserted before at most one replace (or delete) - /// - /// - /// [LSP Specification for `TextEdit[]`](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEditArray) - /// > Text edits ranges must never overlap, that means no part of the original document must be manipulated by more than one edit. - /// > However, it is possible that multiple edits have the same start position: multiple inserts, - /// > or any number of inserts followed by a single remove or replace edit. - /// > If multiple inserts have the same position, the order in the array defines the order - /// > in which the inserted strings appear in the resulting text. - val tryFindError: edits: TextEdit list -> string option - /// Sorts edits by range (`Start`). - /// Order is preserved for edits with same `Start`. - val sortByRange: edits: TextEdit list -> TextEdit list - /// Applies the passed edits from last to first (sorted by range) - val apply: edits: TextEdit list -> text: string -> Result - /// `tryFindError` before `apply` - val applyWithErrorCheck: edits: TextEdit list -> text: string -> Result + /// Checks edits for: + /// * There's at least one TextEdit + /// * All TextEdits are valid (`TextEdit.tryFindError`) + /// * Edits don't overlap + /// * For same position: All inserted before at most one replace (or delete) + /// + /// + /// [LSP Specification for `TextEdit[]`](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEditArray) + /// > Text edits ranges must never overlap, that means no part of the original document must be manipulated by more than one edit. + /// > However, it is possible that multiple edits have the same start position: multiple inserts, + /// > or any number of inserts followed by a single remove or replace edit. + /// > If multiple inserts have the same position, the order in the array defines the order + /// > in which the inserted strings appear in the resulting text. + val tryFindError: edits: TextEdit[] -> string option + /// Sorts edits by range (`Start`). + /// Order is preserved for edits with same `Start`. + val sortByRange: edits: TextEdit[] -> TextEdit[] + /// Applies the passed edits from last to first (sorted by range) + val apply: edits: TextEdit[] -> text: string -> Result + /// `tryFindError` before `apply` + val applyWithErrorCheck: edits: TextEdit[] -> text: string -> Result module WorkspaceEdit = - /// Extract `TextEdit[]` from either `DocumentChanges` or `Changes`. - /// All edits MUST be for passed `textDocument`. - /// - /// Checks for errors: - /// * Either `DocumentChanges` or `Changes`, but not both - /// * FsAutoComplete sends only `DocumentChanges` - /// * All edits inside `textDocument` - /// * Version is only checked if: Version in `textDocument` and Version in `workspaceEdit.DocumentChanges.*` - /// * Using `TextEdit.tryFindError`: - /// * At least one edit - /// * No empty edit - /// * No overlaps - val tryExtractTextEditsInSingleFile: - textDocument: VersionedTextDocumentIdentifier -> workspaceEdit: WorkspaceEdit -> Result + /// Extract `TextEdit[]` from either `DocumentChanges` or `Changes`. + /// All edits MUST be for passed `textDocument`. + /// + /// Checks for errors: + /// * Either `DocumentChanges` or `Changes`, but not both + /// * FsAutoComplete sends only `DocumentChanges` + /// * All edits inside `textDocument` + /// * Version is only checked if: Version in `textDocument` and Version in `workspaceEdit.DocumentChanges.*` + /// * Using `TextEdit.tryFindError`: + /// * At least one edit + /// * No empty edit + /// * No overlaps + val tryExtractTextEditsInSingleFile: + textDocument: VersionedTextDocumentIdentifier -> workspaceEdit: WorkspaceEdit -> Result diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/Utils.Tests.fs b/test/FsAutoComplete.Tests.Lsp/Utils/Utils.Tests.fs index 1ece5d58e..cd8443a9e 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/Utils.Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/Utils.Tests.fs @@ -4,418 +4,479 @@ open Utils.Utils open Expecto module private Expect = - let private failureTests = testList (nameof Expect.failure) [ - testCaseAsync "failtest should be success" (Expect.failure <| async { - failtest "some error" - }) - testCaseAsync "equal failure should be success" (Expect.failure <| async { - Expect.equal 1 2 "" - }) - testCaseAsync "no failure should fail" (async { - try - do! - async { return 1 } - |> Expect.failure - - failtest "should not succeed" - with - | :? AssertException -> () - | ex -> failtest "Expected AssertException, but was %A" (ex.GetType()) - }) - testCaseAsync "`failwith` (`System.Exception`) should fail" (async { - let msg = "some error" - try - do! - async { return failwith msg } - |> Expect.failure - - failtest "should not succeed" - with - | ex when ex.Message = msg -> () - | ex -> failtest "Expected System.Exception, but was %A" (ex.GetType()) - }) - testCaseAsync "`raise NotImplementedException` should fail" (async { - let msg = "oh no" - try - do! - async { return raise (System.NotImplementedException(msg)) } - |> Expect.failure - - failtest "should not succeed" - with - | :? System.NotImplementedException as ex -> - Expect.equal ex.Message msg "Should have correct error message" - | ex -> failtest "Expected System.Exception, but was %A" (ex.GetType()) - }) - ] - - let tests = testList (nameof Expect) [ - failureTests - ] + let private failureTests = + testList + (nameof Expect.failure) + [ testCaseAsync "failtest should be success" (Expect.failure <| async { failtest "some error" }) + testCaseAsync "equal failure should be success" (Expect.failure <| async { Expect.equal 1 2 "" }) + testCaseAsync + "no failure should fail" + (async { + try + do! async { return 1 } |> Expect.failure + + failtest "should not succeed" + with + | :? AssertException -> () + | ex -> failtest "Expected AssertException, but was %A" (ex.GetType()) + }) + testCaseAsync + "`failwith` (`System.Exception`) should fail" + (async { + let msg = "some error" + + try + do! async { return failwith msg } |> Expect.failure + + failtest "should not succeed" + with + | ex when ex.Message = msg -> () + | ex -> failtest "Expected System.Exception, but was %A" (ex.GetType()) + }) + testCaseAsync + "`raise NotImplementedException` should fail" + (async { + let msg = "oh no" + + try + do! async { return raise (System.NotImplementedException(msg)) } |> Expect.failure + + failtest "should not succeed" + with + | :? System.NotImplementedException as ex -> Expect.equal ex.Message msg "Should have correct error message" + | ex -> failtest "Expected System.Exception, but was %A" (ex.GetType()) + }) ] + + let tests = testList (nameof Expect) [ failureTests ] module private Range = open Ionide.LanguageServerProtocol.Types - let inline pos line column : Position = { Line = line; Character = column} + let inline pos line column : Position = { Line = line; Character = column } let inline range p1 p2 = { Start = p1; End = p2 } - let touchesTests = testList (nameof Range.touches) [ - testCase "completely disjoint ranges don't touch" <| fun _ -> - let r1 = { Start = pos 1 5; End = pos 3 7} - let r2 = { Start = pos 5 3; End = pos 7 8} - - let touch = Range.touches r1 r2 - Expect.isFalse touch "Should not touch" - testCase "range 1 inside range 2 don't touch" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 3 7} - let r2 = { Start = pos 1 3; End = pos 7 8} - - let touch = Range.touches r1 r2 - Expect.isFalse touch "Should not touch" - let touch = Range.touches r2 r1 - Expect.isFalse touch "Should not touch" - testCase "two same single positions touch" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 2 5} - let r2 = { Start = pos 2 5; End = pos 2 5} - - let touch = Range.touches r1 r2 - Expect.isTrue touch "Should touch" - testCase "common End/Start touch" <| fun _ -> - let r1 = { Start = pos 1 5; End = pos 2 5} - let r2 = { Start = pos 2 5; End = pos 5 3} - - let touch = Range.touches r1 r2 - Expect.isTrue touch "Should touch" - let touch = Range.touches r2 r1 - Expect.isTrue touch "Should touch" - testCase "two same ranges don't touch" <| fun _ -> - let r1 = { Start = pos 1 5; End = pos 2 5} - let r2 = { Start = pos 1 5; End = pos 2 5} - - let touch = Range.touches r1 r2 - Expect.isFalse touch "Should not touch" - testCase "strictly overlapping ranges don't touch" <| fun _ -> - let r1 = { Start = pos 1 5; End = pos 3 7} - let r2 = { Start = pos 2 3; End = pos 5 8} - - let touch = Range.touches r1 r2 - Expect.isFalse touch "Should not touch" - ] - let private overlapsStrictlyTests = testList (nameof Range.overlapsStrictly) [ - testCase "completely distinct ranges on different lines don't overlap" <| fun _ -> - let r1 = { Start = pos 1 3; End = pos 2 7 } - let r2 = { Start = pos 4 5; End = pos 7 8 } - - let overlap = Range.overlapsStrictly r1 r2 - Expect.isFalse overlap "Should not overlap" - - testCase "completely distinct ranges on same line don't overlap" <| fun _ -> - let r1 = { Start = pos 3 3; End = pos 3 7 } - let r2 = { Start = pos 3 8; End = pos 3 11 } - - let overlap = Range.overlapsStrictly r1 r2 - Expect.isFalse overlap "Should not overlap" - - testCase "ranges with same End/Start overlap" <| fun _ -> - let r1 = { Start = pos 2 3; End = pos 3 7 } - let r2 = { Start = pos 3 7; End = pos 4 11 } - - let overlap = Range.overlapsStrictly r1 r2 - Expect.isTrue overlap "Should overlap" - - testCase "ranges with same Start/End overlap" <| fun _ -> - let r1 = { Start = pos 3 7; End = pos 4 11 } - let r2 = { Start = pos 2 3; End = pos 3 7 } - - let overlap = Range.overlapsStrictly r2 r1 - Expect.isTrue overlap "Should overlap" - - testCase "position ranges on same position overlap" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 2 5} - let r2 = { Start = pos 2 5; End = pos 2 5} - - let overlap = Range.overlapsStrictly r1 r2 - Expect.isTrue overlap "Should overlap" - - testCase "same ranges overlap" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 3 7} - let r2 = { Start = pos 2 5; End = pos 3 7} - - let overlap = Range.overlapsStrictly r1 r2 - Expect.isTrue overlap "Should overlap" - - testCase "completely inside overlap" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 5 7} - let r2 = { Start = pos 3 1; End = pos 4 3} - - let overlap = Range.overlapsStrictly r1 r2 - Expect.isTrue overlap "Should overlap" - let overlap = Range.overlapsStrictly r2 r1 - Expect.isTrue overlap "Should overlap" - - testCase "overlapping ranges overlap" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 3 7} - let r2 = { Start = pos 3 1; End = pos 5 6} - - let overlap = Range.overlapsStrictly r1 r2 - Expect.isTrue overlap "Should overlap" - let overlap = Range.overlapsStrictly r2 r1 - Expect.isTrue overlap "Should overlap" - ] - - let private overlapsLooselyTests = testList (nameof Range.overlapsLoosely) [ - testCase "completely distinct ranges on different lines don't overlap" <| fun _ -> - let r1 = { Start = pos 1 3; End = pos 2 7 } - let r2 = { Start = pos 4 5; End = pos 7 8 } - - let overlap = Range.overlapsLoosely r1 r2 - Expect.isFalse overlap "Should not overlap" - - testCase "completely distinct ranges on same line don't overlap" <| fun _ -> - let r1 = { Start = pos 3 3; End = pos 3 7 } - let r2 = { Start = pos 3 8; End = pos 3 11 } - - let overlap = Range.overlapsLoosely r1 r2 - Expect.isFalse overlap "Should not overlap" - - testCase "ranges with same End/Start don't overlap" <| fun _ -> - let r1 = { Start = pos 2 3; End = pos 3 7 } - let r2 = { Start = pos 3 7; End = pos 4 11 } - - let overlap = Range.overlapsLoosely r1 r2 - Expect.isFalse overlap "Should not overlap" - - testCase "ranges with same Start/End don't overlap" <| fun _ -> - let r1 = { Start = pos 3 7; End = pos 4 11 } - let r2 = { Start = pos 2 3; End = pos 3 7 } - - let overlap = Range.overlapsLoosely r2 r1 - Expect.isFalse overlap "Should not overlap" - - testCase "position ranges on same position don't overlap" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 2 5} - let r2 = { Start = pos 2 5; End = pos 2 5} - - let overlap = Range.overlapsLoosely r1 r2 - Expect.isFalse overlap "Should not overlap" - - testCase "same ranges overlap" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 3 7} - let r2 = { Start = pos 2 5; End = pos 3 7} - - let overlap = Range.overlapsLoosely r1 r2 - Expect.isTrue overlap "Should overlap" - - testCase "completely inside overlap" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 5 7} - let r2 = { Start = pos 3 1; End = pos 4 3} - - let overlap = Range.overlapsLoosely r1 r2 - Expect.isTrue overlap "Should overlap" - let overlap = Range.overlapsLoosely r2 r1 - Expect.isTrue overlap "Should overlap" - - testCase "overlapping ranges overlap" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 3 7} - let r2 = { Start = pos 3 1; End = pos 5 6} - - let overlap = Range.overlapsLoosely r1 r2 - Expect.isTrue overlap "Should overlap" - let overlap = Range.overlapsLoosely r2 r1 - Expect.isTrue overlap "Should overlap" - ] - - let isDisjointStrictlyTests = testList (nameof Range.isDisjointStrictly) [ - testCase "completely distinct ranges on different lines are disjoint" <| fun _ -> - let r1 = { Start = pos 1 3; End = pos 2 7 } - let r2 = { Start = pos 4 5; End = pos 7 8 } - - let disjoint = Range.isDisjointStrictly r1 r2 - Expect.isTrue disjoint "Should be disjoint" - - testCase "completely distinct ranges on same line are disjoint" <| fun _ -> - let r1 = { Start = pos 3 3; End = pos 3 7 } - let r2 = { Start = pos 3 8; End = pos 3 11 } - - let disjoint = Range.isDisjointStrictly r1 r2 - Expect.isTrue disjoint "Should be disjoint" - - testCase "ranges with same End/Start aren't disjoint" <| fun _ -> - let r1 = { Start = pos 2 3; End = pos 3 7 } - let r2 = { Start = pos 3 7; End = pos 4 11 } - - let disjoint = Range.isDisjointStrictly r1 r2 - Expect.isFalse disjoint "Should not be disjoint" - - testCase "ranges with same Start/End aren't disjoint" <| fun _ -> - let r1 = { Start = pos 3 7; End = pos 4 11 } - let r2 = { Start = pos 2 3; End = pos 3 7 } - - let disjoint = Range.isDisjointStrictly r2 r1 - Expect.isFalse disjoint "Should not be disjoint" - - testCase "position ranges on same position aren't disjoint" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 2 5} - let r2 = { Start = pos 2 5; End = pos 2 5} - - let disjoint = Range.isDisjointStrictly r1 r2 - Expect.isFalse disjoint "Should not be disjoint" - - testCase "same ranges aren't disjoint" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 3 7} - let r2 = { Start = pos 2 5; End = pos 3 7} - - let disjoint = Range.isDisjointStrictly r1 r2 - Expect.isFalse disjoint "Should not be disjoint" - - testCase "completely inside aren't disjoint" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 5 7} - let r2 = { Start = pos 3 1; End = pos 4 3} - - let disjoint = Range.isDisjointStrictly r1 r2 - Expect.isFalse disjoint "Should not be disjoint" - let disjoint = Range.isDisjointStrictly r2 r1 - Expect.isFalse disjoint "Should not be disjoint" - - testCase "overlapping ranges aren't disjoint" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 3 7} - let r2 = { Start = pos 3 1; End = pos 5 6} - - let disjoint = Range.isDisjointStrictly r1 r2 - Expect.isFalse disjoint "Should not be disjoint" - let disjoint = Range.isDisjointStrictly r2 r1 - Expect.isFalse disjoint "Should not be disjoint" - ] - - let isDisjointLooselyTests = testList (nameof Range.isDisjointLoosely) [ - testCase "completely distinct ranges on different lines are disjoint" <| fun _ -> - let r1 = { Start = pos 1 3; End = pos 2 7 } - let r2 = { Start = pos 4 5; End = pos 7 8 } - - let disjoint = Range.isDisjointLoosely r1 r2 - Expect.isTrue disjoint "Should be disjoint" - - testCase "completely distinct ranges on same line are disjoint" <| fun _ -> - let r1 = { Start = pos 3 3; End = pos 3 7 } - let r2 = { Start = pos 3 8; End = pos 3 11 } - - let disjoint = Range.isDisjointLoosely r1 r2 - Expect.isTrue disjoint "Should be disjoint" - - testCase "ranges with same End/Start aren't disjoint" <| fun _ -> - let r1 = { Start = pos 2 3; End = pos 3 7 } - let r2 = { Start = pos 3 7; End = pos 4 11 } - - let disjoint = Range.isDisjointLoosely r1 r2 - Expect.isTrue disjoint "Should be disjoint" - - testCase "ranges with same Start/End aren't disjoint" <| fun _ -> - let r1 = { Start = pos 3 7; End = pos 4 11 } - let r2 = { Start = pos 2 3; End = pos 3 7 } - - let disjoint = Range.isDisjointLoosely r2 r1 - Expect.isTrue disjoint "Should be disjoint" - - testCase "position ranges on same position are disjoint" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 2 5} - let r2 = { Start = pos 2 5; End = pos 2 5} - - let disjoint = Range.isDisjointLoosely r1 r2 - Expect.isTrue disjoint "Should be disjoint" - - testCase "same ranges aren't disjoint" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 3 7} - let r2 = { Start = pos 2 5; End = pos 3 7} - - let disjoint = Range.isDisjointLoosely r1 r2 - Expect.isFalse disjoint "Should not be disjoint" - - testCase "completely inside aren't disjoint" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 5 7} - let r2 = { Start = pos 3 1; End = pos 4 3} - - let disjoint = Range.isDisjointLoosely r1 r2 - Expect.isFalse disjoint "Should not be disjoint" - let disjoint = Range.isDisjointLoosely r2 r1 - Expect.isFalse disjoint "Should not be disjoint" - - testCase "overlapping ranges aren't disjoint" <| fun _ -> - let r1 = { Start = pos 2 5; End = pos 3 7} - let r2 = { Start = pos 3 1; End = pos 5 6} - - let disjoint = Range.isDisjointLoosely r1 r2 - Expect.isFalse disjoint "Should not be disjoint" - let disjoint = Range.isDisjointLoosely r2 r1 - Expect.isFalse disjoint "Should not be disjoint" - ] - - let tests = testList (nameof Range) [ - touchesTests - overlapsStrictlyTests - overlapsLooselyTests - isDisjointStrictlyTests - isDisjointLooselyTests - ] + let touchesTests = + testList + (nameof Range.touches) + [ testCase "completely disjoint ranges don't touch" + <| fun _ -> + let r1 = { Start = pos 1u 5u; End = pos 3u 7u } + let r2 = { Start = pos 5u 3u; End = pos 7u 8u } + + let touch = Range.touches r1 r2 + Expect.isFalse touch "Should not touch" + testCase "range 1 inside range 2 don't touch" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 3u 7u } + let r2 = { Start = pos 1u 3u; End = pos 7u 8u } + + let touch = Range.touches r1 r2 + Expect.isFalse touch "Should not touch" + let touch = Range.touches r2 r1 + Expect.isFalse touch "Should not touch" + testCase "two same single positions touch" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 2u 5u } + let r2 = { Start = pos 2u 5u; End = pos 2u 5u } + + let touch = Range.touches r1 r2 + Expect.isTrue touch "Should touch" + testCase "common End/Start touch" + <| fun _ -> + let r1 = { Start = pos 1u 5u; End = pos 2u 5u } + let r2 = { Start = pos 2u 5u; End = pos 5u 3u } + + let touch = Range.touches r1 r2 + Expect.isTrue touch "Should touch" + let touch = Range.touches r2 r1 + Expect.isTrue touch "Should touch" + testCase "two same ranges don't touch" + <| fun _ -> + let r1 = { Start = pos 1u 5u; End = pos 2u 5u } + let r2 = { Start = pos 1u 5u; End = pos 2u 5u } + + let touch = Range.touches r1 r2 + Expect.isFalse touch "Should not touch" + testCase "strictly overlapping ranges don't touch" + <| fun _ -> + let r1 = { Start = pos 1u 5u; End = pos 3u 7u } + let r2 = { Start = pos 2u 3u; End = pos 5u 8u } + + let touch = Range.touches r1 r2 + Expect.isFalse touch "Should not touch" ] + + let private overlapsStrictlyTests = + testList + (nameof Range.overlapsStrictly) + [ testCase "completely distinct ranges on different lines don't overlap" + <| fun _ -> + let r1 = { Start = pos 1u 3u; End = pos 2u 7u } + let r2 = { Start = pos 4u 5u; End = pos 7u 8u } + + let overlap = Range.overlapsStrictly r1 r2 + Expect.isFalse overlap "Should not overlap" + + testCase "completely distinct ranges on same line don't overlap" + <| fun _ -> + let r1 = { Start = pos 3u 3u; End = pos 3u 7u } + let r2 = { Start = pos 3u 8u; End = pos 3u 11u } + + let overlap = Range.overlapsStrictly r1 r2 + Expect.isFalse overlap "Should not overlap" + + testCase "ranges with same End/Start overlap" + <| fun _ -> + let r1 = { Start = pos 2u 3u; End = pos 3u 7u } + let r2 = { Start = pos 3u 7u; End = pos 4u 11u } + + let overlap = Range.overlapsStrictly r1 r2 + Expect.isTrue overlap "Should overlap" + + testCase "ranges with same Start/End overlap" + <| fun _ -> + let r1 = { Start = pos 3u 7u; End = pos 4u 11u } + let r2 = { Start = pos 2u 3u; End = pos 3u 7u } + + let overlap = Range.overlapsStrictly r2 r1 + Expect.isTrue overlap "Should overlap" + + testCase "position ranges on same position overlap" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 2u 5u } + let r2 = { Start = pos 2u 5u; End = pos 2u 5u } + + let overlap = Range.overlapsStrictly r1 r2 + Expect.isTrue overlap "Should overlap" + + testCase "same ranges overlap" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 3u 7u } + let r2 = { Start = pos 2u 5u; End = pos 3u 7u } + + let overlap = Range.overlapsStrictly r1 r2 + Expect.isTrue overlap "Should overlap" + + testCase "completely inside overlap" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 5u 7u } + let r2 = { Start = pos 3u 1u; End = pos 4u 3u } + + let overlap = Range.overlapsStrictly r1 r2 + Expect.isTrue overlap "Should overlap" + let overlap = Range.overlapsStrictly r2 r1 + Expect.isTrue overlap "Should overlap" + + testCase "overlapping ranges overlap" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 3u 7u } + let r2 = { Start = pos 3u 1u; End = pos 5u 6u } + + let overlap = Range.overlapsStrictly r1 r2 + Expect.isTrue overlap "Should overlap" + let overlap = Range.overlapsStrictly r2 r1 + Expect.isTrue overlap "Should overlap" ] + + let private overlapsLooselyTests = + testList + (nameof Range.overlapsLoosely) + [ testCase "completely distinct ranges on different lines don't overlap" + <| fun _ -> + let r1 = { Start = pos 1u 3u; End = pos 2u 7u } + let r2 = { Start = pos 4u 5u; End = pos 7u 8u } + + let overlap = Range.overlapsLoosely r1 r2 + Expect.isFalse overlap "Should not overlap" + + testCase "completely distinct ranges on same line don't overlap" + <| fun _ -> + let r1 = { Start = pos 3u 3u; End = pos 3u 7u } + let r2 = { Start = pos 3u 8u; End = pos 3u 11u } + + let overlap = Range.overlapsLoosely r1 r2 + Expect.isFalse overlap "Should not overlap" + + testCase "ranges with same End/Start don't overlap" + <| fun _ -> + let r1 = { Start = pos 2u 3u; End = pos 3u 7u } + let r2 = { Start = pos 3u 7u; End = pos 4u 11u } + + let overlap = Range.overlapsLoosely r1 r2 + Expect.isFalse overlap "Should not overlap" + + testCase "ranges with same Start/End don't overlap" + <| fun _ -> + let r1 = { Start = pos 3u 7u; End = pos 4u 11u } + let r2 = { Start = pos 2u 3u; End = pos 3u 7u } + + let overlap = Range.overlapsLoosely r2 r1 + Expect.isFalse overlap "Should not overlap" + + testCase "position ranges on same position don't overlap" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 2u 5u } + let r2 = { Start = pos 2u 5u; End = pos 2u 5u } + + let overlap = Range.overlapsLoosely r1 r2 + Expect.isFalse overlap "Should not overlap" + + testCase "same ranges overlap" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 3u 7u } + let r2 = { Start = pos 2u 5u; End = pos 3u 7u } + + let overlap = Range.overlapsLoosely r1 r2 + Expect.isTrue overlap "Should overlap" + + testCase "completely inside overlap" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 5u 7u } + let r2 = { Start = pos 3u 1u; End = pos 4u 3u } + + let overlap = Range.overlapsLoosely r1 r2 + Expect.isTrue overlap "Should overlap" + let overlap = Range.overlapsLoosely r2 r1 + Expect.isTrue overlap "Should overlap" + + testCase "overlapping ranges overlap" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 3u 7u } + let r2 = { Start = pos 3u 1u; End = pos 5u 6u } + + let overlap = Range.overlapsLoosely r1 r2 + Expect.isTrue overlap "Should overlap" + let overlap = Range.overlapsLoosely r2 r1 + Expect.isTrue overlap "Should overlap" ] + + let isDisjointStrictlyTests = + testList + (nameof Range.isDisjointStrictly) + [ testCase "completely distinct ranges on different lines are disjoint" + <| fun _ -> + let r1 = { Start = pos 1u 3u; End = pos 2u 7u } + let r2 = { Start = pos 4u 5u; End = pos 7u 8u } + + let disjoint = Range.isDisjointStrictly r1 r2 + Expect.isTrue disjoint "Should be disjoint" + + testCase "completely distinct ranges on same line are disjoint" + <| fun _ -> + let r1 = { Start = pos 3u 3u; End = pos 3u 7u } + let r2 = { Start = pos 3u 8u; End = pos 3u 11u } + + let disjoint = Range.isDisjointStrictly r1 r2 + Expect.isTrue disjoint "Should be disjoint" + + testCase "ranges with same End/Start aren't disjoint" + <| fun _ -> + let r1 = { Start = pos 2u 3u; End = pos 3u 7u } + let r2 = { Start = pos 3u 7u; End = pos 4u 11u } + + let disjoint = Range.isDisjointStrictly r1 r2 + Expect.isFalse disjoint "Should not be disjoint" + + testCase "ranges with same Start/End aren't disjoint" + <| fun _ -> + let r1 = { Start = pos 3u 7u; End = pos 4u 11u } + let r2 = { Start = pos 2u 3u; End = pos 3u 7u } + + let disjoint = Range.isDisjointStrictly r2 r1 + Expect.isFalse disjoint "Should not be disjoint" + + testCase "position ranges on same position aren't disjoint" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 2u 5u } + let r2 = { Start = pos 2u 5u; End = pos 2u 5u } + + let disjoint = Range.isDisjointStrictly r1 r2 + Expect.isFalse disjoint "Should not be disjoint" + + testCase "same ranges aren't disjoint" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 3u 7u } + let r2 = { Start = pos 2u 5u; End = pos 3u 7u } + + let disjoint = Range.isDisjointStrictly r1 r2 + Expect.isFalse disjoint "Should not be disjoint" + + testCase "completely inside aren't disjoint" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 5u 7u } + let r2 = { Start = pos 3u 1u; End = pos 4u 3u } + + let disjoint = Range.isDisjointStrictly r1 r2 + Expect.isFalse disjoint "Should not be disjoint" + let disjoint = Range.isDisjointStrictly r2 r1 + Expect.isFalse disjoint "Should not be disjoint" + + testCase "overlapping ranges aren't disjoint" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 3u 7u } + let r2 = { Start = pos 3u 1u; End = pos 5u 6u } + + let disjoint = Range.isDisjointStrictly r1 r2 + Expect.isFalse disjoint "Should not be disjoint" + let disjoint = Range.isDisjointStrictly r2 r1 + Expect.isFalse disjoint "Should not be disjoint" ] + + let isDisjointLooselyTests = + testList + (nameof Range.isDisjointLoosely) + [ testCase "completely distinct ranges on different lines are disjoint" + <| fun _ -> + let r1 = { Start = pos 1u 3u; End = pos 2u 7u } + let r2 = { Start = pos 4u 5u; End = pos 7u 8u } + + let disjoint = Range.isDisjointLoosely r1 r2 + Expect.isTrue disjoint "Should be disjoint" + + testCase "completely distinct ranges on same line are disjoint" + <| fun _ -> + let r1 = { Start = pos 3u 3u; End = pos 3u 7u } + let r2 = { Start = pos 3u 8u; End = pos 3u 11u } + + let disjoint = Range.isDisjointLoosely r1 r2 + Expect.isTrue disjoint "Should be disjoint" + + testCase "ranges with same End/Start aren't disjoint" + <| fun _ -> + let r1 = { Start = pos 2u 3u; End = pos 3u 7u } + let r2 = { Start = pos 3u 7u; End = pos 4u 11u } + + let disjoint = Range.isDisjointLoosely r1 r2 + Expect.isTrue disjoint "Should be disjoint" + + testCase "ranges with same Start/End aren't disjoint" + <| fun _ -> + let r1 = { Start = pos 3u 7u; End = pos 4u 11u } + let r2 = { Start = pos 2u 3u; End = pos 3u 7u } + + let disjoint = Range.isDisjointLoosely r2 r1 + Expect.isTrue disjoint "Should be disjoint" + + testCase "position ranges on same position are disjoint" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 2u 5u } + let r2 = { Start = pos 2u 5u; End = pos 2u 5u } + + let disjoint = Range.isDisjointLoosely r1 r2 + Expect.isTrue disjoint "Should be disjoint" + + testCase "same ranges aren't disjoint" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 3u 7u } + let r2 = { Start = pos 2u 5u; End = pos 3u 7u } + + let disjoint = Range.isDisjointLoosely r1 r2 + Expect.isFalse disjoint "Should not be disjoint" + + testCase "completely inside aren't disjoint" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 5u 7u } + let r2 = { Start = pos 3u 1u; End = pos 4u 3u } + + let disjoint = Range.isDisjointLoosely r1 r2 + Expect.isFalse disjoint "Should not be disjoint" + let disjoint = Range.isDisjointLoosely r2 r1 + Expect.isFalse disjoint "Should not be disjoint" + + testCase "overlapping ranges aren't disjoint" + <| fun _ -> + let r1 = { Start = pos 2u 5u; End = pos 3u 7u } + let r2 = { Start = pos 3u 1u; End = pos 5u 6u } + + let disjoint = Range.isDisjointLoosely r1 r2 + Expect.isFalse disjoint "Should not be disjoint" + let disjoint = Range.isDisjointLoosely r2 r1 + Expect.isFalse disjoint "Should not be disjoint" ] + + let tests = + testList + (nameof Range) + [ touchesTests + overlapsStrictlyTests + overlapsLooselyTests + isDisjointStrictlyTests + isDisjointLooselyTests ] module private Text = - let private trimTripleQuotationTests = testList (nameof Text.trimTripleQuotation) [ - let check input expected = - let actual = input |> Text.trimTripleQuotation - Expect.equal actual expected "Invalid trimming" - - testList "normal string" [ - testCase "empty string" <| fun _ -> - let text = "" - let expected = text - check text expected - testCase "single line with text" <| fun _ -> - let text = "foo bar" - let expected = text - check text expected - testCase "multi lines with text without indentation" <| fun _ -> - let text = "foo bar\nlorem ipsum\ndolor\nsit" - let expected = text - check text expected - testCase "leading new line and no indentation" <| fun _ -> - let text = "\nfoo bar\nlorem ipsum\ndolor\nsit" - let expected = text.Substring 1 - check text expected - testCase "single line with indentation" <| fun _ -> - let text = " foo bar" - let expected = text.TrimStart() - check text expected - testCase "multi lines with all same indentation" <| fun _ -> - let text = " foo bar\n lorem ipsum\n dolor sit" - let expected = "foo bar\nlorem ipsum\ndolor sit" - check text expected - testCase "multi lines with all different indentation" <| fun _ -> - let text = " foo bar\n lorem ipsum\n dolor sit" - let expected = " foo bar\nlorem ipsum\n dolor sit" - check text expected - testCase "leading new line and multi line with all different indentation" <| fun _ -> - let text = "\n foo bar\n lorem ipsum\n dolor sit" - let expected = " foo bar\nlorem ipsum\n dolor sit" - check text expected - testCase "multi lines with empty lines" <| fun _ -> - let text = " foo bar\n \n baz\n lorem ipsum\n \n\n dolor sit\n \n ---" - let expected = "foo bar\n\nbaz\n lorem ipsum\n \n\ndolor sit\n\n ---" - check text expected - testCase "last whitespace line gets trimmed" <| fun _ -> - let text = "foo bar\n " - let expected = "foo bar\n" - check text expected - testCase "whitespace in last line doesn't get trimmed if there are other chars" <| fun _ -> - let text = "foo bar\nbaz " - let expected = text - check text expected - testCase "trim leading nl, indentation, trailing whitespace line" <| fun _ -> - let text = "\n foo bar\n\n baz\n lorem ipsum\n dolor sit\n " - let expected = "foo bar\n\n baz\nlorem ipsum\n dolor sit\n" - check text expected - ] - testList "triple quotation" [ - testCase "fsharp code written on beginning of line" <| fun _ -> - let text = """ + let private trimTripleQuotationTests = + testList + (nameof Text.trimTripleQuotation) + [ let check input expected = + let actual = input |> Text.trimTripleQuotation + Expect.equal actual expected "Invalid trimming" + + testList + "normal string" + [ testCase "empty string" + <| fun _ -> + let text = "" + let expected = text + check text expected + testCase "single line with text" + <| fun _ -> + let text = "foo bar" + let expected = text + check text expected + testCase "multi lines with text without indentation" + <| fun _ -> + let text = "foo bar\nlorem ipsum\ndolor\nsit" + let expected = text + check text expected + testCase "leading new line and no indentation" + <| fun _ -> + let text = "\nfoo bar\nlorem ipsum\ndolor\nsit" + let expected = text.Substring 1 + check text expected + testCase "single line with indentation" + <| fun _ -> + let text = " foo bar" + let expected = text.TrimStart() + check text expected + testCase "multi lines with all same indentation" + <| fun _ -> + let text = " foo bar\n lorem ipsum\n dolor sit" + let expected = "foo bar\nlorem ipsum\ndolor sit" + check text expected + testCase "multi lines with all different indentation" + <| fun _ -> + let text = " foo bar\n lorem ipsum\n dolor sit" + let expected = " foo bar\nlorem ipsum\n dolor sit" + check text expected + testCase "leading new line and multi line with all different indentation" + <| fun _ -> + let text = "\n foo bar\n lorem ipsum\n dolor sit" + let expected = " foo bar\nlorem ipsum\n dolor sit" + check text expected + testCase "multi lines with empty lines" + <| fun _ -> + let text = + " foo bar\n \n baz\n lorem ipsum\n \n\n dolor sit\n \n ---" + + let expected = "foo bar\n\nbaz\n lorem ipsum\n \n\ndolor sit\n\n ---" + check text expected + testCase "last whitespace line gets trimmed" + <| fun _ -> + let text = "foo bar\n " + let expected = "foo bar\n" + check text expected + testCase "whitespace in last line doesn't get trimmed if there are other chars" + <| fun _ -> + let text = "foo bar\nbaz " + let expected = text + check text expected + testCase "trim leading nl, indentation, trailing whitespace line" + <| fun _ -> + let text = "\n foo bar\n\n baz\n lorem ipsum\n dolor sit\n " + let expected = "foo bar\n\n baz\nlorem ipsum\n dolor sit\n" + check text expected ] + + testList + "triple quotation" + [ testCase "fsharp code written on beginning of line" + <| fun _ -> + let text = + """ module Foo let rec handle (a: int) = @@ -438,7 +499,9 @@ printfn "Result=%A" a else handle a """ - let expected = """module Foo + + let expected = + """module Foo let rec handle (a: int) = if a = 15 then @@ -460,9 +523,12 @@ printfn "Result=%A" a else handle a """ // whitespace in last empty line is trimmed, but `\n` is kept - check text expected - testCase "fsharp code written with indention to match surrounding code" <| fun _ -> - let text = """ + + check text expected + testCase "fsharp code written with indention to match surrounding code" + <| fun _ -> + let text = + """ module Foo let rec handle (a: int) = @@ -485,7 +551,9 @@ handle a else handle a """ - let expected = """module Foo + + let expected = + """module Foo let rec handle (a: int) = if a = 15 then @@ -507,9 +575,12 @@ if a < 12 then else handle a """ - check text expected - testCase "trimming already trimmed string doesn't change string" <| fun _ -> - let text = """ + + check text expected + testCase "trimming already trimmed string doesn't change string" + <| fun _ -> + let text = + """ module Foo let rec handle (a: int) = @@ -532,11 +603,14 @@ else else handle a """ - let once = text |> Text.trimTripleQuotation - let twice = once |> Text.trimTripleQuotation - Expect.equal twice once "trimming should not change a trimmed string" - testCase "independent trimmings should trim same" <| fun _ -> - let text = """ + + let once = text |> Text.trimTripleQuotation + let twice = once |> Text.trimTripleQuotation + Expect.equal twice once "trimming should not change a trimmed string" + testCase "independent trimmings should trim same" + <| fun _ -> + let text = + """ module Foo let rec handle (a: int) = @@ -559,17 +633,11 @@ else else handle a """ - let a = text |> Text.trimTripleQuotation - let b = text |> Text.trimTripleQuotation - Expect.equal b a "both trimmings should be same" - ] - ] - let tests = testList (nameof Text) [ - trimTripleQuotationTests - ] - -let tests = testList (nameof Utils) [ - Expect.tests - Range.tests - Text.tests -] + + let a = text |> Text.trimTripleQuotation + let b = text |> Text.trimTripleQuotation + Expect.equal b a "both trimmings should be same" ] ] + + let tests = testList (nameof Text) [ trimTripleQuotationTests ] + +let tests = testList (nameof Utils) [ Expect.tests; Range.tests; Text.tests ] diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/Utils.fs b/test/FsAutoComplete.Tests.Lsp/Utils/Utils.fs index cb75e3b36..a8f86e6bb 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/Utils.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/Utils.fs @@ -44,8 +44,8 @@ module Position = open Ionide.LanguageServerProtocol.Types let inline assertPositive (pos: Position) = - assert (pos.Line >= 0) - assert (pos.Character >= 0) + assert (pos.Line >= 0u) + assert (pos.Character >= 0u) let inline eq p1 p2 = // p1.Line = p2.Line && p1.Character = p2.Character diff --git a/test/FsAutoComplete.Tests.Lsp/XmlGenerationTests.fs b/test/FsAutoComplete.Tests.Lsp/XmlGenerationTests.fs index 4205c7ddd..29b154f34 100644 --- a/test/FsAutoComplete.Tests.Lsp/XmlGenerationTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/XmlGenerationTests.fs @@ -23,7 +23,7 @@ let tests state = do! server.TextDocumentDidOpen tdop match! waitForParseResultsForFile "Script.fsx" serverRequests with - | Ok () -> return server, scriptPath, serverRequests + | Ok() -> return server, scriptPath, serverRequests | Error errors -> let errorStrings = errors @@ -42,31 +42,30 @@ let tests state = let! server, filePath, serverRequests = server let fileUri = Path.FilePathToUri filePath - let! edits = - waitForEditsForFile filePath serverRequests - |> Async.StartChild + let! edits = waitForEditsForFile filePath serverRequests |> Async.StartChild let! result = server.FSharpDocumentationGenerator( { TextDocument = { Uri = fileUri; Version = 1 } // the start of the 'add' symbol name - Position = { Line = 0; Character = 5 } } + Position = { Line = 0u; Character = 5u } } ) match result with | Error e -> failtestf "Couldn't generate xml docs: %A" e - | Ok () -> () + | Ok() -> () let! edits = edits let expectedXml = $"""/// {NewLine}/// {NewLine}/// {NewLine}/// {NewLine}""" - let expectedEdits: TextEdit [] = + let expectedEdits: U2[] = [| { Range = - { Start = { Line = 0; Character = 0 } - End = { Line = 0; Character = 0 } } - NewText = expectedXml } |] + { Start = { Line = 0u; Character = 0u } + End = { Line = 0u; Character = 0u } } + NewText = expectedXml } + |> U2.C1 |] Expect.equal edits expectedEdits "Should have generated the xml docs at the start position" }) ]