diff --git a/src/FsAutoComplete.BackgroundServices/Program.fs b/src/FsAutoComplete.BackgroundServices/Program.fs index 210b16fc6..182baba9a 100644 --- a/src/FsAutoComplete.BackgroundServices/Program.fs +++ b/src/FsAutoComplete.BackgroundServices/Program.fs @@ -14,6 +14,8 @@ open FsAutoComplete open Ionide.ProjInfo.ProjectSystem open FSharp.UMX open System.Reactive.Linq +open FSharp.Compiler.Text + type BackgroundFileCheckType = | SourceFile of filePath: string | ScriptFile of filePath: string * tfm: FSIRefs.TFM @@ -146,13 +148,13 @@ type BackgroundServiceServer(state: State, client: FsacClient) = let finalOpts = Array.append okOtherOpts (Array.ofList refs) { projOptions with OtherOptions = finalOpts } - let getScriptOptions file lines tfm = + let getScriptOptions file text tfm = match tfm with | FSIRefs.NetFx -> - checker.GetProjectOptionsFromScript(file, SourceText.ofString lines, assumeDotNetFramework = true, useSdkRefs = false, useFsiAuxLib = true) + checker.GetProjectOptionsFromScript(file, text, assumeDotNetFramework = true, useSdkRefs = false, useFsiAuxLib = true) | FSIRefs.NetCore -> async { - let! (opts, errors) = checker.GetProjectOptionsFromScript(file, SourceText.ofString lines, assumeDotNetFramework = false, useSdkRefs = true, useFsiAuxLib = true) + let! (opts, errors) = checker.GetProjectOptionsFromScript(file, text, assumeDotNetFramework = false, useSdkRefs = true, useFsiAuxLib = true) return replaceRefs opts, errors } @@ -160,7 +162,7 @@ type BackgroundServiceServer(state: State, client: FsacClient) = | ScriptFile(file, tfm) -> state.Files.TryFind (Utils.normalizePath file) |> Option.map (fun st -> async { - let! (opts, _errors) = getScriptOptions file (st.Lines |> String.concat "\n") tfm + let! (opts, _errors) = getScriptOptions file st.Lines tfm let sf = getFilesFromOpts opts return @@ -189,8 +191,8 @@ type BackgroundServiceServer(state: State, client: FsacClient) = do! client.Notify {Value = sprintf "Typechecking %s" (UMX.untag file) } match state.Files.TryFind file, state.FileCheckOptions.TryFind file with | Some vf, Some opts -> - let txt = vf.Lines |> String.concat "\n" - let! pr, cr = checker.ParseAndCheckFileInProject(UMX.untag file, defaultArg vf.Version 0, SourceText.ofString txt, opts) + let txt = vf.Lines + let! pr, cr = checker.ParseAndCheckFileInProject(UMX.untag file, defaultArg vf.Version 0, txt, opts) match cr with | FSharpCheckFileAnswer.Aborted -> do! client.Notify {Value = sprintf "Typechecking aborted %s" (UMX.untag file) } @@ -206,9 +208,9 @@ type BackgroundServiceServer(state: State, client: FsacClient) = do! client.SendDiagnostics msg return () | Some vf, None when (UMX.untag file).EndsWith ".fsx" -> - let txt = vf.Lines |> String.concat "\n" - let! (opts, _errors) = checker.GetProjectOptionsFromScript(UMX.untag file, SourceText.ofString txt, assumeDotNetFramework = true, useSdkRefs = false) - let! pr, cr = checker.ParseAndCheckFileInProject(UMX.untag file, defaultArg vf.Version 0, SourceText.ofString txt, opts) + let txt = vf.Lines + let! (opts, _errors) = checker.GetProjectOptionsFromScript(UMX.untag file, txt, assumeDotNetFramework = true, useSdkRefs = false) + let! pr, cr = checker.ParseAndCheckFileInProject(UMX.untag file, defaultArg vf.Version 0, txt, opts) match cr with | FSharpCheckFileAnswer.Aborted -> do! client.Notify {Value = sprintf "Typechecking aborted %s" (UMX.untag file) } @@ -326,7 +328,10 @@ type BackgroundServiceServer(state: State, client: FsacClient) = do! client.Notify {Value = sprintf "File update %s" p.File.FilePath } let file = Utils.normalizePath p.File.FilePath - let vf = {Lines = p.Content.Split( [|'\n' |] ); Touched = DateTime.Now; Version = Some p.Version } + let vf = + { Lines = SourceText.ofString p.Content + Touched = DateTime.Now + Version = Some p.Version } state.Files.AddOrUpdate(file, (fun _ -> vf),( fun _ _ -> vf) ) |> ignore let! filesToCheck = defaultArg (getListOfFilesForProjectChecking p.File) (async.Return []) do! client.Notify { Value = sprintf "Files to check %A" filesToCheck } diff --git a/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs b/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs index f06781823..b2e6a7c40 100644 --- a/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs +++ b/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs @@ -96,10 +96,10 @@ let getMemberNameAndRanges (abstractClassData) = | AbstractClassData.ObjExpr (_, bindings, _) -> List.choose (|MemberNameAndRange|_|) bindings /// Try to find the start column, so we know what the base indentation should be -let inferStartColumn (codeGenServer : CodeGenerationService) (pos : Pos) (doc : Document) (lines: LineStr[]) (lineStr : string) (abstractClassData : AbstractClassData) (indentSize : int) = +let inferStartColumn (codeGenServer : CodeGenerationService) (pos : Pos) (doc : Document) (lines: ISourceText) (lineStr : string) (abstractClassData : AbstractClassData) (indentSize : int) = match getMemberNameAndRanges abstractClassData with | (_, range) :: _ -> - getLineIdent lines.[range.StartLine-1] + getLineIdent (lines.GetLineString(range.StartLine - 1)) | [] -> match abstractClassData with | AbstractClassData.ExplicitImpl _ -> @@ -122,7 +122,7 @@ let inferStartColumn (codeGenServer : CodeGenerationService) (pos : Pos) (doc : /// Try to write any missing members of the given abstract type at the given location. /// If the destination type isn't an abstract class, or if there are no missing members to implement, /// nothing is written. Otherwise, a list of missing members is generated and written -let writeAbstractClassStub (codeGenServer : CodeGenerationService) (checkResultForFile: ParseAndCheckResults) (doc : Document) (lines: LineStr[]) (lineStr : string) (abstractClassData : AbstractClassData) = +let writeAbstractClassStub (codeGenServer : CodeGenerationService) (checkResultForFile: ParseAndCheckResults) (doc : Document) (lines: ISourceText) (lineStr : string) (abstractClassData : AbstractClassData) = asyncMaybe { let pos = Pos.mkPos abstractClassData.AbstractTypeIdentRange.Start.Line (abstractClassData.AbstractTypeIdentRange.End.Column) let! (_lexerSym, usages) = codeGenServer.GetSymbolAndUseAtPositionOfKind(doc.FullName, pos, SymbolKind.Ident) diff --git a/src/FsAutoComplete.Core/CodeGeneration.fs b/src/FsAutoComplete.Core/CodeGeneration.fs index 3de69ba59..061e447a7 100644 --- a/src/FsAutoComplete.Core/CodeGeneration.fs +++ b/src/FsAutoComplete.Core/CodeGeneration.fs @@ -15,9 +15,9 @@ type CodeGenerationService(checker : FSharpCompilerServiceChecker, state : State member x.TokenizeLine(fileName, i) = match state.TryGetFileCheckerOptionsWithLines fileName with | ResultOrString.Error _ -> None - | ResultOrString.Ok (opts, lines) -> + | ResultOrString.Ok (opts, text) -> try - let line = lines.[ i - 1 ] + let line = text.GetLineString (i - 1) Lexer.tokenizeLine [||] line |> Some with | _ -> None diff --git a/src/FsAutoComplete.Core/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index 757689121..7b340f6a2 100644 --- a/src/FsAutoComplete.Core/Commands.fs +++ b/src/FsAutoComplete.Core/Commands.fs @@ -228,7 +228,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe match state.Files.TryGetValue file with | true, fileData -> - let res = analyzerHandler (file, fileData.Lines, pt, tast, parseAndCheck.GetCheckResults.PartialAssemblySignature.Entities |> Seq.toList, parseAndCheck.GetAllEntities) + let res = analyzerHandler (file, fileData.Lines.ToString().Split("\n"), pt, tast, parseAndCheck.GetCheckResults.PartialAssemblySignature.Entities |> Seq.toList, parseAndCheck.GetAllEntities) (res, file) |> NotificationEvent.AnalyzerMessage @@ -256,21 +256,22 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe match state.Files.TryFind file with | Some f -> Some (f.Lines) | None when File.Exists(UMX.untag file) -> - let ctn = File.ReadAllLines (UMX.untag file) - state.Files.[file] <- { Touched = DateTime.Now; Lines = ctn; Version = None } + let ctn = File.ReadAllText (UMX.untag file) + let text = SourceText.ofString ctn + state.Files.[file] <- { Touched = DateTime.Now; Lines = text; Version = None } let payload = if Utils.isAScript (UMX.untag file) then BackgroundServices.ScriptFile(UMX.untag file, Ionide.ProjInfo.ProjectSystem.FSIRefs.TFM.NetCore) else BackgroundServices.SourceFile (UMX.untag file) - backgroundService.UpdateFile(payload, ctn |> String.concat "\n", 0) - Some (ctn) + backgroundService.UpdateFile(payload, ctn, 0) + Some text | None -> None match sourceOpt with | None -> () | Some source -> let opts = state.GetProjectOptions' file |> Utils.projectOptionsToParseOptions async { - let! parseRes = checker.ParseFile(file, source |> String.concat "\n", opts) + let! parseRes = checker.ParseFile(file, source, opts) fileParsed.Trigger parseRes } |> Async.Start @@ -285,13 +286,13 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe let codeGenServer = CodeGenerationService(checker, state) - let docForText (lines: string []) (tyRes: ParseAndCheckResults): Document = + let docForText (lines: ISourceText) (tyRes: ParseAndCheckResults): Document = { LineCount = lines.Length FullName = tyRes.FileName // from the compiler, assumed safe - GetText = fun _ -> lines |> String.concat "\n" - GetLineText0 = fun i -> lines.[i] - GetLineText1 = fun i -> lines.[i - 1] + GetText = fun _ -> string lines + GetLineText0 = fun i -> lines.GetLineString i + GetLineText1 = fun i -> lines.GetLineString (i - 1) } let calculateNamespaceInsert (decl : FSharpDeclarationListItem) (pos : Pos) getLine: CompletionNamespaceInsert option = @@ -359,7 +360,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe member __.LastCheckResult with get() = lastCheckResult - member __.SetFileContent(file: string, lines: LineStr[], version, tfmIfScript) = + member __.SetFileContent(file: string, lines: ISourceText, version, tfmIfScript) = state.AddFileText(file, lines, version) let payload = let untagged = UMX.untag file @@ -367,7 +368,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe then BackgroundServices.ScriptFile(untagged, tfmIfScript) else BackgroundServices.SourceFile untagged - backgroundService.UpdateFile(payload, lines |> String.concat "\n", defaultArg version 0) + backgroundService.UpdateFile(payload, lines.ToString(), defaultArg version 0) member private x.MapResultAsync (successToString: 'a -> Async>, ?failureToString: string -> CoreResponse<'b>) = Async.bind <| function @@ -530,7 +531,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe member x.TryGetFileVersion = state.TryGetFileVersion - member x.Parse file lines version (isSdkScript: bool option) = + member x.Parse file (text: ISourceText) version (isSdkScript: bool option) = let tmf = isSdkScript |> Option.map (fun n -> if n then FSIRefs.NetCore else FSIRefs.NetFx) |> Option.defaultValue FSIRefs.NetFx do x.CancelQueue file @@ -559,13 +560,12 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe SourceFiles = opts.SourceFiles |> Array.filter FscArguments.isCompileFile |> Array.map (Path.GetFullPath) OtherOptions = opts.OtherOptions |> Array.map (fun n -> if FscArguments.isCompileFile(n) then Path.GetFullPath n else n) } - let text = String.concat "\n" lines if Utils.isAScript (UMX.untag file) then commandsLogger.info (Log.setMessage "Checking script file '{file}'" >> Log.addContextDestructured "file" file) - let hash = - lines + let hash = + text.Lines() |> Array.filter (fun n -> n.StartsWith "#r" || n.StartsWith "#load" || n.StartsWith "#I") |> Array.toList |> fun n -> n.GetHashCode () @@ -582,11 +582,11 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe } scriptFileProjectOptions.Trigger checkOptions - state.AddFileTextAndCheckerOptions(file, lines, normalizeOptions checkOptions, Some version) + state.AddFileTextAndCheckerOptions(file, text, normalizeOptions checkOptions, Some version) fileStateSet.Trigger () return! parse' file text checkOptions else - match state.GetCheckerOptions(file, lines) with + match state.RefreshCheckerOptions(file, text) with | Some c -> state.SetFileVersion file version fileStateSet.Trigger () @@ -603,26 +603,19 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe | ResultOrString.Error s, None -> match state.TryGetFileSource file with | ResultOrString.Error s -> return CoreResponse.ErrorRes s - | ResultOrString.Ok l -> - let text = String.concat "\n" l + | ResultOrString.Ok text -> let files = Array.singleton (UMX.untag file) let parseOptions = { FSharpParsingOptions.Default with SourceFiles = files} let! decls = checker.GetDeclarations(file, text, parseOptions, version) let decls = decls |> Array.map (fun a -> a,file) return CoreResponse.Res decls - | ResultOrString.Error _, Some l -> - let text = String.concat "\n" l + | ResultOrString.Error _, Some text -> let files = Array.singleton (UMX.untag file) let parseOptions = { FSharpParsingOptions.Default with SourceFiles = files} let! decls = checker.GetDeclarations(file, text, parseOptions, version) let decls = decls |> Array.map (fun a -> a,file) return CoreResponse.Res decls - | ResultOrString.Ok (checkOptions, source), _ -> - let text = - match lines with - | Some l -> String.concat "\n" l - | None -> source - + | ResultOrString.Ok (checkOptions, text), _ -> let parseOptions = Utils.projectOptionsToParseOptions checkOptions let! decls = checker.GetDeclarations(file, text, parseOptions, version) @@ -659,7 +652,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe match source with | None -> return CoreResponse.ErrorRes (sprintf "No help text available for symbol '%s'" sym) | Some source -> - let getSource = fun i -> source.[i - 1] + let getSource = fun i -> source.GetLineString (i - 1) let tip = match state.HelpText.TryFind sym with @@ -680,14 +673,14 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe member x.Colorization enabled = state.ColorizationOutput <- enabled member x.Error msg = [CoreResponse.ErrorRes msg] - member x.Completion (tyRes : ParseAndCheckResults) (pos: Pos) lineStr (lines : string[]) (fileName : string) filter includeKeywords includeExternal = async { + member x.Completion (tyRes : ParseAndCheckResults) (pos: Pos) lineStr (lines : ISourceText) (fileName : string) filter includeKeywords includeExternal = async { let getAllSymbols () = if includeExternal then tyRes.GetAllEntities true else [] let! res = tyRes.TryGetCompletions pos lineStr filter getAllSymbols match res with | Some (decls, residue, shouldKeywords) -> let declName (d: FSharpDeclarationListItem) = d.Name - let getLine = fun i -> lines.[i - 1] + let getLine = fun i -> lines.GetLineString(i - 1) //Init cache for current list state.Declarations.Clear() @@ -801,7 +794,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe |> x.AsCancellable tyRes.FileName |> AsyncResult.recoverCancellation /// Attempts to identify member overloads and infer current parameter positions for signature help at a given location - member x.MethodsForSignatureHelp (tyRes : ParseAndCheckResults, pos: Pos, lines: LineStr[], triggerChar, possibleSessionKind) = + member x.MethodsForSignatureHelp (tyRes : ParseAndCheckResults, pos: Pos, lines: ISourceText, triggerChar, possibleSessionKind) = SignatureHelp.getSignatureHelpFor (tyRes, pos, lines, triggerChar, possibleSessionKind) // member x.Lint (file: string): Async = @@ -910,15 +903,15 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe |> x.AsCancellable tyRes.FileName |> AsyncResult.recoverCancellation - member x.GetUnionPatternMatchCases (tyRes : ParseAndCheckResults) (pos: Pos) (lines: LineStr[]) (line: LineStr) = + member x.GetUnionPatternMatchCases (tyRes : ParseAndCheckResults) (pos: Pos) (lines: ISourceText) (line: LineStr) = async { let codeGenService = CodeGenerationService(checker, state) let doc = { Document.LineCount = lines.Length FullName = tyRes.FileName - GetText = fun _ -> lines |> String.concat "\n" - GetLineText0 = fun i -> lines.[i] - GetLineText1 = fun i -> lines.[i - 1] + GetText = fun _ -> string lines + GetLineText0 = fun i -> lines.GetLineString i + GetLineText1 = fun i -> lines.GetLineString (i - 1) } let! res = tryFindUnionDefinitionFromPos codeGenService pos doc @@ -935,7 +928,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe |> x.AsCancellable tyRes.FileName |> AsyncResult.recoverCancellation - member x.GetRecordStub (tyRes : ParseAndCheckResults) (pos: Pos) (lines: LineStr[]) (line: LineStr) = + member x.GetRecordStub (tyRes : ParseAndCheckResults) (pos: Pos) (lines: ISourceText) (line: LineStr) = async { let doc = docForText lines tyRes let! res = tryFindRecordDefinitionFromPos codeGenServer pos doc @@ -953,7 +946,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe |> x.AsCancellable tyRes.FileName |> AsyncResult.recoverCancellation - member x.GetInterfaceStub (tyRes : ParseAndCheckResults) (pos: Pos) (lines: LineStr[]) (lineStr: LineStr) = + member x.GetInterfaceStub (tyRes : ParseAndCheckResults) (pos: Pos) (lines: ISourceText) (lineStr: LineStr) = async { let doc = docForText lines tyRes let! res = tryFindInterfaceExprInBufferAtPos codeGenServer pos doc @@ -969,7 +962,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe |> x.AsCancellable tyRes.FileName |> AsyncResult.recoverCancellation - member x.GetAbstractClassStub (tyRes : ParseAndCheckResults) (objExprRange: Range) (lines: LineStr[]) (lineStr: LineStr) = + member x.GetAbstractClassStub (tyRes : ParseAndCheckResults) (objExprRange: Range) (lines: ISourceText) (lineStr: LineStr) = asyncResult { let doc = docForText lines tyRes let! abstractClass = @@ -1025,7 +1018,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe match tyResOpt with | None -> () | Some tyRes -> - let getSourceLine lineNo = source.[lineNo - 1] + let getSourceLine lineNo = source.GetLineString (lineNo - 1) let! simplified = SimplifyNames.getSimplifiableNames(tyRes.GetCheckResults, getSourceLine) let simplified = Array.ofSeq simplified notify.Trigger (NotificationEvent.SimplifyNames (file, simplified)) @@ -1041,7 +1034,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe | None -> return () | Some tyRes -> - let! unused = UnusedOpens.getUnusedOpens(tyRes.GetCheckResults, fun i -> source.[i - 1]) + let! unused = UnusedOpens.getUnusedOpens(tyRes.GetCheckResults, fun i -> source.GetLineString (i - 1)) notify.Trigger (NotificationEvent.UnusedOpens (file, (unused |> List.toArray))) } @@ -1052,10 +1045,9 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe member x.GetRangesAtPosition file positions = async { match state.TryGetFileCheckerOptionsWithLines file with - | Ok (opts, sourceLines) -> + | Ok (opts, text) -> let parseOpts = Utils.projectOptionsToParseOptions opts - let allSource = sourceLines |> String.concat "\n" - let! ast = checker.ParseFile(file, allSource, parseOpts) + let! ast = checker.ParseFile(file, text, parseOpts) return positions |> List.map (fun x -> UntypedAstUtils.getRangesAtPosition ast.ParseTree x @@ -1085,14 +1077,13 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe // } member x.ScopesForFile (file: string) = asyncResult { - let! (opts, sourceLines) = state.TryGetFileCheckerOptionsWithLines file + let! (opts, text) = state.TryGetFileCheckerOptionsWithLines file let parseOpts = Utils.projectOptionsToParseOptions opts - let allSource = sourceLines |> String.concat "\n" - let! ast = checker.ParseFile(file, allSource, parseOpts) + let! ast = checker.ParseFile(file, text, parseOpts) match ast.ParseTree with | None -> return! Error (ast.Errors |> Array.map string |> String.concat "\n") | Some ast' -> - let ranges = Structure.getOutliningRanges sourceLines ast' + let ranges = Structure.getOutliningRanges (text.ToString().Split("\n")) ast' return ranges } @@ -1101,8 +1092,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe member x.FormatDocument (file: string) = asyncResult { - let! (opts, lines) = x.TryGetFileCheckerOptionsWithLines file - let source = String.concat "\n" lines + let! (opts, text) = x.TryGetFileCheckerOptionsWithLines file let parsingOptions = Utils.projectOptionsToParseOptions opts let checker : FSharpChecker = checker.GetFSharpChecker() // ENHANCEMENT: consider caching the Fantomas configuration and reevaluate when the configuration file changes. @@ -1112,8 +1102,8 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe | None -> fantomasLogger.warn (Log.setMessage "No fantomas configuration found for file '{filePath}' or parent directories. Using the default configuration." >> Log.addContextDestructured "filePath" file) Fantomas.FormatConfig.FormatConfig.Default - let! formatted = Fantomas.CodeFormatter.FormatDocumentAsync(UMX.untag file, Fantomas.SourceOrigin.SourceString source, config, parsingOptions, checker) - return lines, formatted + let! formatted = Fantomas.CodeFormatter.FormatDocumentAsync(UMX.untag file, Fantomas.SourceOrigin.SourceText text, config, parsingOptions, checker) + return text, formatted } |> AsyncResult.foldResult Some (fun _ -> None) @@ -1144,7 +1134,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe async { let cnt = match state.TryGetFileSource file with - | Ok ctn -> String.concat "\n" ctn + | Ok ctn -> ctn.ToString() | _ -> File.ReadAllText (UMX.untag file) let parsedFile = if Utils.isAScript (UMX.untag file) then @@ -1160,7 +1150,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe result { let! contents = state.TryGetFileSource tyRes.FileName let getGenerics line (token: FSharpTokenInfo) = - let lineStr = contents.[line] + let lineStr = contents.GetLineString line let res = tyRes.TryGetToolTip (Pos.fromZ line token.RightColumn) lineStr match res with | Ok tip -> @@ -1192,7 +1182,7 @@ type Commands (checker: FSharpCompilerServiceChecker, state: State, backgroundSe currentIndex, false, acc let hints = - contents + Array.init (contents.GetLineCount()) (fun line -> contents.GetLineString line) |> Array.map (Lexer.tokenizeLine [||]) |> Array.mapi (fun currentIndex currentTokens -> currentIndex, currentTokens) |> Array.fold folder (0, false, []) diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index 7729b32de..fcdd7aea1 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -177,7 +177,7 @@ type FSharpCompilerServiceChecker(backgroundServiceEnabled, hasAnalyzers) = member private __.GetNetFxScriptOptions(file: string, source) = async { logQueueLength optsLogger (Log.setMessage "Getting NetFX options for script file {file}" >> Log.addContextDestructured "file" file) let allFlags = Array.append [| "--targetprofile:mscorlib" |] fsiAdditionalArguments - let! (opts, errors) = checker.GetProjectOptionsFromScript(UMX.untag file, SourceText.ofString source, assumeDotNetFramework = true, useFsiAuxLib = true, otherFlags = allFlags, userOpName = "getNetFrameworkScriptOptions") + let! (opts, errors) = checker.GetProjectOptionsFromScript(UMX.untag file, source, assumeDotNetFramework = true, useFsiAuxLib = true, otherFlags = allFlags, userOpName = "getNetFrameworkScriptOptions") let allModifications = addLoadedFiles >> resolveRelativeFilePaths return allModifications opts, errors } @@ -185,7 +185,7 @@ type FSharpCompilerServiceChecker(backgroundServiceEnabled, hasAnalyzers) = member private __.GetNetCoreScriptOptions(file: string, source) = async { logQueueLength optsLogger (Log.setMessage "Getting NetCore options for script file {file}" >> Log.addContextDestructured "file" file) let allFlags = Array.append [| "--targetprofile:netstandard" |] fsiAdditionalArguments - let! (opts, errors) = checker.GetProjectOptionsFromScript(UMX.untag file, SourceText.ofString source, assumeDotNetFramework = false, useSdkRefs = true, useFsiAuxLib = true, otherFlags = allFlags, userOpName = "getNetCoreScriptOptions") + let! (opts, errors) = checker.GetProjectOptionsFromScript(UMX.untag file, source, assumeDotNetFramework = false, useSdkRefs = true, useFsiAuxLib = true, otherFlags = allFlags, userOpName = "getNetCoreScriptOptions") let allModifications = replaceFrameworkRefs >> addLoadedFiles >> resolveRelativeFilePaths return allModifications opts, errors } @@ -243,14 +243,12 @@ type FSharpCompilerServiceChecker(backgroundServiceEnabled, hasAnalyzers) = member __.ParseFile(fn: string, source, fpo) = logQueueLength checkerLogger (Log.setMessage "ParseFile - {file}" >> Log.addContextDestructured "file" fn) - let source = SourceText.ofString source checker.ParseFile(UMX.untag fn, source, fpo) member __.ParseAndCheckFileInProject(filePath: string, version, source, options) = async { let opName = sprintf "ParseAndCheckFileInProject - %A" filePath logQueueLength checkerLogger (Log.setMessage "{opName}" >> Log.addContextDestructured "opName" opName) - let source = SourceText.ofString source let options = clearProjectReferences options try let! (p, c) = checker.ParseAndCheckFileInProject (UMX.untag filePath, version, source, options, userOpName = opName) @@ -269,7 +267,6 @@ type FSharpCompilerServiceChecker(backgroundServiceEnabled, hasAnalyzers) = member __.TryGetRecentCheckResultsForFile(file: string, options, ?source) = let opName = sprintf "TryGetRecentCheckResultsForFile - %A" file logQueueLength checkerLogger (Log.setMessage "{opName}" >> Log.addContextDestructured "opName" opName) - let source = source |> Option.map SourceText.ofString let options = clearProjectReferences options checker.TryGetRecentCheckResultsForFile(UMX.untag file, options, ?sourceText=source, userOpName=opName) |> Option.map (fun (pr, cr, _) -> ParseAndCheckResults (pr, cr, entityCache)) @@ -294,7 +291,6 @@ type FSharpCompilerServiceChecker(backgroundServiceEnabled, hasAnalyzers) = member __.GetDeclarations (fileName: string, source, options, version) = async { logQueueLength checkerLogger (Log.setMessage "GetDeclarations - {file}" >> Log.addContextDestructured "file" fileName) - let source = SourceText.ofString source let! parseResult = checker.ParseFile(UMX.untag fileName, source, options) return parseResult.GetNavigationItems().Declarations } diff --git a/src/FsAutoComplete.Core/FileSystem.fs b/src/FsAutoComplete.Core/FileSystem.fs index 55358785e..cce8ee840 100644 --- a/src/FsAutoComplete.Core/FileSystem.fs +++ b/src/FsAutoComplete.Core/FileSystem.fs @@ -4,20 +4,58 @@ open FSharp.Compiler.SourceCodeServices open System open FsAutoComplete.Logging open FSharp.UMX +open FSharp.Compiler.Text +open System.Runtime.CompilerServices type VolatileFile = { Touched: DateTime - Lines: string [] - Version: int option} + Lines: ISourceText + Version: int option } open System.IO + +[] +type SourceTextExtensions = + [] + static member GetText(t: ISourceText, m: FSharp.Compiler.Text.Range): Result = + let allFileRange = Range.mkRange m.FileName Pos.pos0 (t.GetLastFilePosition()) + if not (Range.rangeContainsRange allFileRange m) + then Error "%A{m} is outside of the bounds of the file" + else + if m.StartLine = m.EndLine then // slice of a single line, just do that + let lineText = t.GetLineString (m.StartLine - 1) + lineText.Substring(m.StartColumn, m.EndColumn - m.StartColumn) |> Ok + else + // multiline, use a builder + let builder = new System.Text.StringBuilder() + // slice of the first line + let firstLine = t.GetLineString (m.StartLine - 1) + builder.Append (firstLine.Substring(m.StartColumn)) |> ignore + // whole intermediate lines + for line in (m.StartLine + 1)..(m.EndLine - 1) do + builder.AppendLine (t.GetLineString(line - 1)) |> ignore + // final part, potential slice + let lastLine = t.GetLineString (m.EndLine - 1) + builder.Append (lastLine.Substring(0, m.EndColumn)) |> ignore + Ok (builder.ToString()) + + [] + static member inline Lines(t: ISourceText) = + Array.init (t.GetLineCount()) t.GetLineString + + [] + /// a safe alternative to GetLastCharacterPosition, which returns untagged indexes. this version + /// returns a FCS Pos to prevent confusion about line index offsets + static member GetLastFilePosition(t: ISourceText): FSharp.Compiler.Text.Pos = + let endLine, endChar = t.GetLastCharacterPosition() + Pos.mkPos endLine endChar + type FileSystem (actualFs: IFileSystem, tryFindFile: string -> VolatileFile option) = let getContent (filename: string) = filename |> tryFindFile - |> Option.map (fun file -> - System.Text.Encoding.UTF8.GetBytes (String.Join ("\n", file.Lines))) + |> Option.map (fun file -> file.Lines.ToString() |> System.Text.Encoding.UTF8.GetBytes) let fsLogger = LogProvider.getLoggerByName "FileSystem" /// translation of the BCL's Windows logic for Path.IsPathRooted. diff --git a/src/FsAutoComplete.Core/InterfaceStubGenerator.fs b/src/FsAutoComplete.Core/InterfaceStubGenerator.fs index ed7b74345..332aee0e9 100644 --- a/src/FsAutoComplete.Core/InterfaceStubGenerator.fs +++ b/src/FsAutoComplete.Core/InterfaceStubGenerator.fs @@ -108,10 +108,10 @@ let getInterfaceIdentifier (interfaceData : InterfaceData) (tokens : FSharpToken CodeGenerationUtils.findLastIdentifier tokens.[newKeywordIndex + 2..] tokens.[newKeywordIndex + 2] /// Try to find the start column, so we know what the base indentation should be -let inferStartColumn (codeGenServer : CodeGenerationService) (pos : Pos) (doc : Document) (lines: LineStr[]) (lineStr : string) (interfaceData : InterfaceData) (indentSize : int) = +let inferStartColumn (codeGenServer : CodeGenerationService) (pos : Pos) (doc : Document) (lines: ISourceText) (lineStr : string) (interfaceData : InterfaceData) (indentSize : int) = match getMemberNameAndRanges interfaceData with | (_, range) :: _ -> - getLineIdent lines.[range.StartLine-1] + getLineIdent (lines.GetLineString(range.StartLine - 1)) | [] -> match interfaceData with | InterfaceData.Interface _ as iface -> @@ -134,7 +134,7 @@ let inferStartColumn (codeGenServer : CodeGenerationService) (pos : Pos) (doc : /// Return None, if we failed to handle the interface implementation /// Return Some (insertPosition, generatedString): /// `insertPosition`: representation the position where the editor should insert the `generatedString` -let handleImplementInterface (codeGenServer : CodeGenerationService) (checkResultForFile: ParseAndCheckResults) (pos : Pos) (doc : Document) (lines: LineStr[]) (lineStr : string) (interfaceData : InterfaceData) = +let handleImplementInterface (codeGenServer : CodeGenerationService) (checkResultForFile: ParseAndCheckResults) (pos : Pos) (doc : Document) (lines: ISourceText) (lineStr : string) (interfaceData : InterfaceData) = async { let! result = asyncMaybe { let! _symbol, symbolUse = codeGenServer.GetSymbolAndUseAtPositionOfKind(doc.FullName, pos, SymbolKind.Ident) diff --git a/src/FsAutoComplete.Core/SignatureHelp.fs b/src/FsAutoComplete.Core/SignatureHelp.fs index 5c36695af..aa5ea06c1 100644 --- a/src/FsAutoComplete.Core/SignatureHelp.fs +++ b/src/FsAutoComplete.Core/SignatureHelp.fs @@ -20,20 +20,20 @@ type SignatureHelpInfo = { SigHelpKind: SignatureHelpKind } -let private lineText (lines: LineStr []) (pos: Pos) = lines.[pos.Line - 1] +let private lineText (lines: ISourceText) (pos: Pos) = lines.GetLineString(pos.Line - 1) -let private charAt (lines: LineStr []) (pos: Pos) = +let private charAt (lines: ISourceText) (pos: Pos) = (lineText lines pos).[pos.Column - 1] -let dec (lines: LineStr[]) (pos: Pos): Pos = +let dec (lines: ISourceText) (pos: Pos): Pos = if pos.Column = 0 then - let prevLine = lines.[pos.Line - 2] + let prevLine = lines.GetLineString (pos.Line - 2) // retreat to the end of the previous line Pos.mkPos (pos.Line - 1) (prevLine.Length - 1) else Pos.mkPos pos.Line (pos.Column - 1) -let inc (lines: LineStr[]) (pos: Pos): Pos = +let inc (lines: ISourceText) (pos: Pos): Pos = let currentLine = lineText lines pos if pos.Column - 1 = currentLine.Length then // advance to the beginning of the next line @@ -41,7 +41,7 @@ let inc (lines: LineStr[]) (pos: Pos): Pos = else Pos.mkPos pos.Line (pos.Column + 1) -let getText (lines: LineStr[]) (range: Range) = +let getText (lines: ISourceText) (range: Range) = if range.Start.Line = range.End.Line then let line = lineText lines range.Start line.Substring(range.StartColumn - 1, (range.End.Column - range.Start.Column)) @@ -50,12 +50,12 @@ let getText (lines: LineStr[]) (range: Range) = let startLine = lineText lines range.Start yield startLine.Substring(range.StartColumn - 1, (startLine.Length - 1 - range.Start.Column)) for lineNo in (range.Start.Line+1)..(range.End.Line-1) do - yield lines.[lineNo - 1] + yield lines.GetLineString(lineNo - 1) let endLine = lineText lines range.End yield endLine.Substring(0, range.End.Column - 1) }) -let private getSignatureHelpForFunctionApplication (tyRes: ParseAndCheckResults, caretPos: Pos, endOfPreviousIdentPos: Pos, lines: LineStr[]) : Async = +let private getSignatureHelpForFunctionApplication (tyRes: ParseAndCheckResults, caretPos: Pos, endOfPreviousIdentPos: Pos, lines: ISourceText) : Async = asyncMaybe { let lineStr = lineText lines endOfPreviousIdentPos let! possibleApplicationSymbolEnd = maybe { @@ -124,7 +124,7 @@ let private getSignatureHelpForFunctionApplication (tyRes: ParseAndCheckResults, return! None } -let private getSignatureHelpForMethod (tyRes: ParseAndCheckResults, caretPos: Pos, lines: LineStr[], triggerChar) = +let private getSignatureHelpForMethod (tyRes: ParseAndCheckResults, caretPos: Pos, lines: ISourceText, triggerChar) = asyncMaybe { let! paramLocations = tyRes.GetParseResults.FindNoteworthyParamInfoLocations caretPos let names = paramLocations.LongId @@ -194,7 +194,7 @@ let private getSignatureHelpForMethod (tyRes: ParseAndCheckResults, caretPos: Po } } -let getSignatureHelpFor (tyRes : ParseAndCheckResults, pos: Pos, lines: LineStr[], triggerChar, possibleSessionKind) = +let getSignatureHelpFor (tyRes : ParseAndCheckResults, pos: Pos, lines: ISourceText, triggerChar, possibleSessionKind) = asyncResult { let previousNonWhitespaceCharPos = let rec loop ch pos = diff --git a/src/FsAutoComplete.Core/State.fs b/src/FsAutoComplete.Core/State.fs index ca561103c..2552671e1 100644 --- a/src/FsAutoComplete.Core/State.fs +++ b/src/FsAutoComplete.Core/State.fs @@ -46,10 +46,10 @@ type State = ScriptProjectOptions = ConcurrentDictionary() ColorizationOutput = false } - member x.GetCheckerOptions(file: string, lines: LineStr[]) : FSharpProjectOptions option = + member x.RefreshCheckerOptions(file: string, text: ISourceText) : FSharpProjectOptions option = x.ProjectController.GetProjectOptions (UMX.untag file) |> Option.map (fun opts -> - x.Files.[file] <- { Lines = lines; Touched = DateTime.Now; Version = None } + x.Files.[file] <- { Lines = text; Touched = DateTime.Now; Version = None } opts ) @@ -81,13 +81,13 @@ type State = member x.SetLastCheckedVersion (file: string) (version: int) = x.LastCheckedVersion.[file] <- version - member x.AddFileTextAndCheckerOptions(file: string, lines: LineStr[], opts, version) = - let fileState = { Lines = lines; Touched = DateTime.Now; Version = version } + member x.AddFileTextAndCheckerOptions(file: string, text: ISourceText, opts, version) = + let fileState = { Lines = text; Touched = DateTime.Now; Version = version } x.Files.[file] <- fileState x.ProjectController.SetProjectOptions(UMX.untag file, opts) - member x.AddFileText(file: string, lines: LineStr[], version) = - let fileState = { Lines = lines; Touched = DateTime.Now; Version = version } + member x.AddFileText(file: string, text: ISourceText, version) = + let fileState = { Lines = text; Touched = DateTime.Now; Version = version } x.Files.[file] <- fileState member x.AddCancellationToken(file : string, token: CancellationTokenSource) = @@ -115,7 +115,7 @@ type State = ExtraProjectInfo = None Stamp = None} - member x.TryGetFileCheckerOptionsWithLines(file: string) : ResultOrString = + member x.TryGetFileCheckerOptionsWithLines(file: string) : ResultOrString = match x.Files.TryFind(file) with | None -> ResultOrString.Error (sprintf "File '%s' not parsed" (UMX.untag file)) | Some (volFile) -> @@ -124,21 +124,26 @@ type State = | None -> Ok (State.FileWithoutProjectOptions(file), volFile.Lines) | Some opts -> Ok (opts, volFile.Lines) - member x.TryGetFileCheckerOptionsWithSource(file: string) : ResultOrString = + member x.TryGetFileCheckerOptionsWithSource(file: string) : ResultOrString = match x.TryGetFileCheckerOptionsWithLines(file) with | ResultOrString.Error x -> ResultOrString.Error x - | Ok (opts, lines) -> Ok (opts, String.concat "\n" lines) + | Ok (opts, lines) -> Ok (opts, lines) - member x.TryGetFileSource(file: string) : ResultOrString = + member x.TryGetFileSource(file: string) : ResultOrString = match x.Files.TryFind(file) with | None -> ResultOrString.Error (sprintf "File '%s' not parsed" (UMX.untag file)) - | Some f -> Ok (f.Lines) + | Some f -> Ok f.Lines - member x.TryGetFileCheckerOptionsWithLinesAndLineStr(file: string, pos : Pos) : ResultOrString = + member x.TryGetFileCheckerOptionsWithLinesAndLineStr(file: string, pos : Pos) : ResultOrString = match x.TryGetFileCheckerOptionsWithLines(file) with - | ResultOrString.Error x -> ResultOrString.Error x - | Ok (opts, lines) -> - let ok = pos.Line <= lines.Length && pos.Line >= 1 && - pos.Column <= lines.[pos.Line - 1].Length + 1 && pos.Column >= 0 - if not ok then ResultOrString.Error "Position is out of range" - else Ok (opts, lines, lines.[pos.Line - 1]) + | Error x -> Error x + | Ok (opts, text) -> + let lineCount = text.GetLineCount() + if pos.Line < 1 || pos.Line > lineCount then Error "Position is out of range" + else + let line = text.GetLineString (pos.Line - 1) + let lineLength = line.Length + if pos.Column < 0 || pos.Column > lineLength // since column is 0-based, lineLength is actually longer than the column is allowed to be + then Error "Position is out of range" + else + Ok (opts, text, line) diff --git a/src/FsAutoComplete/CodeFixes.fs b/src/FsAutoComplete/CodeFixes.fs index f9e3ad4d0..f04480a1a 100644 --- a/src/FsAutoComplete/CodeFixes.fs +++ b/src/FsAutoComplete/CodeFixes.fs @@ -8,6 +8,7 @@ open LanguageServerProtocol.Types open FsAutoComplete.Logging open FSharp.UMX open FsToolkit.ErrorHandling +open FSharp.Compiler.Text module FcsRange = FSharp.Compiler.Text.Range type FcsRange = FSharp.Compiler.Text.Range @@ -18,12 +19,10 @@ module LspTypes = LanguageServerProtocol.Types module Types = type IsEnabled = unit -> bool - type FileLines = string [] - type FileLine = string - type GetRangeText = string -> LspTypes.Range -> ResultOrString - type GetFileLines = string -> ResultOrString - type GetLineText = FileLines -> LspTypes.Range -> FileLine - type GetParseResultsForFile = string -> FSharp.Compiler.Text.Pos -> Async> + type GetRangeText = string -> LspTypes.Range -> ResultOrString + type GetFileLines = string -> ResultOrString + type GetLineText = FSharp.Compiler.Text.ISourceText -> LspTypes.Range -> Result + type GetParseResultsForFile = string -> FSharp.Compiler.Text.Pos -> Async> type GetProjectOptionsForFile = string -> ResultOrString type FixKind = @@ -98,14 +97,11 @@ module Navigation = /// advance along positions from a starting location, incrementing in a known way until a condition is met. /// when the condition is met, return that position. /// if the condition is never met, return None - let walkPos (lines: string []) (pos: LspTypes.Position) posChange terminalCondition checkCondition: LspTypes.Position option = - let charAt (pos: LspTypes.Position) = lines.[pos.Line].[pos.Character] + let walkPos (lines: ISourceText) (pos: LspTypes.Position) posChange terminalCondition checkCondition: LspTypes.Position option = + let charAt (pos: LspTypes.Position) = lines.GetLineString(pos.Line).[pos.Character - 1] let firstPos = { Line = 0; Character = 0 } - - let finalPos = - { Line = lines.Length - 1 - Character = lines.[lines.Length - 1].Length - 1 } + let finalPos = fcsPosToLsp (lines.GetLastFilePosition()) let rec loop pos = let charAt = charAt pos @@ -117,8 +113,8 @@ module Navigation = loop pos - let inc (lines: string []) (pos: LspTypes.Position): LspTypes.Position = - let lineLength = lines.[pos.Line].Length + let inc (lines: ISourceText) (pos: LspTypes.Position): LspTypes.Position = + let lineLength = lines.GetLineString(pos.Line).Length if pos.Character = lineLength - 1 then { Line = pos.Line + 1; Character = 0 } @@ -126,13 +122,13 @@ module Navigation = { pos with Character = pos.Character + 1 } - let dec (lines: string []) (pos: LspTypes.Position): LspTypes.Position = + let dec (lines: ISourceText) (pos: LspTypes.Position): LspTypes.Position = if pos.Character = 0 then let newLine = pos.Line - 1 // decrement to end of previous line { pos with Line = newLine - Character = lines.[newLine].Length - 1 } + Character = lines.GetLineString(newLine).Length - 1 } else { pos with Character = pos.Character - 1 } @@ -145,16 +141,16 @@ module Navigation = if count <= 0 then pos else incMany lines (inc lines pos) (count - 1) - let walkBackUntilCondition (lines: string []) (pos: LspTypes.Position) = + let walkBackUntilCondition (lines: ISourceText) (pos: LspTypes.Position) = walkPos lines pos (dec lines) (fun c -> false) - let walkForwardUntilCondition (lines: string []) (pos: LspTypes.Position) = + let walkForwardUntilCondition (lines: ISourceText) (pos: LspTypes.Position) = walkPos lines pos (inc lines) (fun c -> false) let walkBackUntilConditionWithTerminal lines pos check terminal = walkPos lines pos (dec lines) terminal check - let walkForwardUntilConditionWithTerminal (lines: string []) (pos: LspTypes.Position) check terminal = + let walkForwardUntilConditionWithTerminal (lines: ISourceText) (pos: LspTypes.Position) check terminal = walkPos lines pos (inc lines) terminal check module Run = diff --git a/src/FsAutoComplete/CodeFixes/AddMissingFunKeyword.fs b/src/FsAutoComplete/CodeFixes/AddMissingFunKeyword.fs index 2815d9b7e..3b7a7ab51 100644 --- a/src/FsAutoComplete/CodeFixes/AddMissingFunKeyword.fs +++ b/src/FsAutoComplete/CodeFixes/AddMissingFunKeyword.fs @@ -17,13 +17,13 @@ let fix (getFileLines: GetFileLines) (getLineText: GetLineText): CodeFix = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath let! lines = getFileLines fileName - let errorText = getLineText lines diagnostic.Range + let! errorText = getLineText lines diagnostic.Range do! Result.guard (fun _ -> errorText = "->") "Expected error source code text not matched" let lineLen = - lines.[diagnostic.Range.Start.Line].Length + lines.GetLineString(diagnostic.Range.Start.Line).Length - let line = + let! line = getLineText lines { Start = diff --git a/src/FsAutoComplete/CodeFixes/AddMissingRecKeyword.fs b/src/FsAutoComplete/CodeFixes/AddMissingRecKeyword.fs index 8f0dca11d..97e6719d7 100644 --- a/src/FsAutoComplete/CodeFixes/AddMissingRecKeyword.fs +++ b/src/FsAutoComplete/CodeFixes/AddMissingRecKeyword.fs @@ -35,9 +35,9 @@ let fix (getFileLines: GetFileLines) (getLineText: GetLineText): CodeFix = let fcsPos = protocolPosToPos startOfBindingName let lineLen = - lines.[diagnostic.Range.Start.Line].Length + lines.GetLineString(diagnostic.Range.Start.Line).Length - let line = + let! line = getLineText lines { Start = diff --git a/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs b/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs index 3490e08b6..fdffbb009 100644 --- a/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs +++ b/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs @@ -25,8 +25,8 @@ let fix | FSharpFindDeclResult.DeclFound declRange when declRange.FileName = filename -> let! projectOptions = getProjectOptionsForFile typedFileName let protocolDeclRange = fcsRangeToLsp declRange - let declText = getText lines protocolDeclRange - let declTextLine = lines.[protocolDeclRange.Start.Line] //TODO: check + let! declText = lines.GetText declRange + let declTextLine = lines.GetLineString protocolDeclRange.Start.Line let! declLexerSymbol = Lexer.getSymbol declRange.Start.Line declRange.Start.Column declText SymbolLookupKind.ByLongIdent projectOptions.OtherOptions |> Result.ofOption (fun _ -> "No lexer symbol for declaration") let! declSymbolUse = tyRes.GetCheckResults.GetSymbolUseAtLocation(declRange.Start.Line, declRange.End.Column, declTextLine, declLexerSymbol.Text.Split('.') |> List.ofArray) |> Result.ofOption (fun _ -> "No lexer symbol") match declSymbolUse.Symbol with diff --git a/src/FsAutoComplete/CodeFixes/ChangeComparisonToMutableAssignment.fs b/src/FsAutoComplete/CodeFixes/ChangeComparisonToMutableAssignment.fs index 8115ec890..2920a54b4 100644 --- a/src/FsAutoComplete/CodeFixes/ChangeComparisonToMutableAssignment.fs +++ b/src/FsAutoComplete/CodeFixes/ChangeComparisonToMutableAssignment.fs @@ -24,7 +24,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = | None -> return [] | Some endPos -> let fcsPos = protocolPosToPos endPos - let line = getLine lines endPos + let line = lines.GetLineString endPos.Line let! symbol = tyRes.TryGetSymbolUse fcsPos line diff --git a/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fs b/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fs index b2d0b4f68..a74320610 100644 --- a/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fs +++ b/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fs @@ -19,10 +19,10 @@ let fix (getFileLines: GetFileLines) let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath - let! (lines: string []) = getFileLines fileName + let! lines = getFileLines fileName // try to find the first case already written let caseLine = diagnostic.Range.Start.Line + 1 - let caseCol = lines.[caseLine].IndexOf('|') + 3 // Find column of first case in patern matching + let caseCol = lines.GetLineString(caseLine).IndexOf('|') + 3 // Find column of first case in patern matching let casePos = { Line = caseLine; Character = caseCol } let casePosFCS = protocolPosToPos casePos diff --git a/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryReturnOrYield.fs b/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryReturnOrYield.fs index 6b23571f7..c92f0551e 100644 --- a/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryReturnOrYield.fs +++ b/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryReturnOrYield.fs @@ -24,8 +24,8 @@ let fix (getParseResultsForFile: GetParseResultsForFile) (getLineText: GetLineTe | None -> return [] | Some exprRange -> let protocolExprRange = fcsRangeToLsp exprRange - let exprText = getLineText lines protocolExprRange - let errorText = getLineText lines diagnostic.Range + let! exprText = getLineText lines protocolExprRange + let! errorText = getLineText lines diagnostic.Range let! title = if errorText.StartsWith "return!" diff --git a/src/FsAutoComplete/CodeFixes/ReplaceBangWithValueFunction.fs b/src/FsAutoComplete/CodeFixes/ReplaceBangWithValueFunction.fs index 01f01610e..cd6c83d41 100644 --- a/src/FsAutoComplete/CodeFixes/ReplaceBangWithValueFunction.fs +++ b/src/FsAutoComplete/CodeFixes/ReplaceBangWithValueFunction.fs @@ -18,7 +18,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) (getLineText: GetLineTe let! exprRange = parseResults.GetParseResults.TryRangeOfExpressionBeingDereferencedContainingPos selectionRange.Start |> Result.ofOption (fun _ -> "No expr found at that pos") let combinedRange = FSharp.Compiler.Text.Range.unionRanges derefRange exprRange let protocolRange = fcsRangeToLsp combinedRange - let badString = getLineText lines protocolRange + let! badString = getLineText lines protocolRange let replacementString = badString.[1..] + ".Value" return [ { Title = "Use `.Value` instead of dereference operator" diff --git a/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs b/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs index 4d1730afc..1fd29bfc7 100644 --- a/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs +++ b/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs @@ -7,6 +7,7 @@ open FsToolkit.ErrorHandling open FSharp.Compiler.SourceCodeServices open FsAutoComplete.LspHelpers open FsAutoComplete +open FSharp.Compiler.Text type LineText = string @@ -20,12 +21,12 @@ let fix (getParseResultsForFile: GetParseResultsForFile) (getNamespaceSuggestion End = { Line = line; Character = 0 } } NewText = lineStr } - let adjustInsertionPoint (lines: string []) (ctx: InsertContext) = + let adjustInsertionPoint (lines: ISourceText) (ctx: InsertContext) = let l = ctx.Pos.Line match ctx.ScopeKind with | TopModule when l > 1 -> - let line = lines.[l - 2] + let line = lines.GetLineString (l - 2) let isImplicitTopLevelModule = not @@ -36,7 +37,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) (getNamespaceSuggestion | TopModule -> 1 | ScopeKind.Namespace when l > 1 -> [ 0 .. l - 1 ] - |> List.mapi (fun i line -> i, lines.[line]) + |> List.mapi (fun i line -> i, lines.GetLineString line) |> List.tryPick (fun (i, lineStr) -> if lineStr.StartsWith "namespace" then Some i else None) |> function // move to the next line below "namespace" and convert it to F# 1-based line number @@ -54,8 +55,8 @@ let fix (getParseResultsForFile: GetParseResultsForFile) (getNamespaceSuggestion Title = $"Use %s{qual}" Kind = Fix } - let openFix fileLines file diagnostic (word: string) (ns, name: string, ctx, multiple): Fix = - let insertPoint = adjustInsertionPoint fileLines ctx + let openFix (text: ISourceText) file diagnostic (word: string) (ns, name: string, ctx, multiple): Fix = + let insertPoint = adjustInsertionPoint text ctx let docLine = insertPoint - 1 let actualOpen = @@ -75,10 +76,10 @@ let fix (getParseResultsForFile: GetParseResultsForFile) (getNamespaceSuggestion let edits = [| yield insertLine docLine lineStr - if fileLines.[docLine + 1].Trim() <> "" then yield insertLine (docLine + 1) "" + if text.GetLineString(docLine + 1).Trim() <> "" then yield insertLine (docLine + 1) "" if (ctx.Pos.Column = 0 || ctx.ScopeKind = Namespace) && docLine > 0 - && not ((fileLines.[docLine - 1]).StartsWith "open") then + && not (text.GetLineString(docLine - 1).StartsWith "open") then yield insertLine (docLine - 1) "" |] { Edits = edits diff --git a/src/FsAutoComplete/FsAutoComplete.Lsp.fs b/src/FsAutoComplete/FsAutoComplete.Lsp.fs index 4657bb759..6414d4939 100644 --- a/src/FsAutoComplete/FsAutoComplete.Lsp.fs +++ b/src/FsAutoComplete/FsAutoComplete.Lsp.fs @@ -17,6 +17,7 @@ open System.IO open FsToolkit.ErrorHandling open FSharp.UMX open FSharp.Analyzers +open FSharp.Compiler.Text module FcsRange = FSharp.Compiler.Text.Range type FcsRange = FSharp.Compiler.Text.Range @@ -116,7 +117,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS match contentChange, doc.Version with | Some contentChange, Some version -> if contentChange.Range.IsNone && contentChange.RangeLength.IsNone then - let content = contentChange.Text.Split('\n') + let content = SourceText.ofString contentChange.Text let tfmConfig = config.UseSdkScripts logger.info (Log.setMessage "ParseFile - Parsing {file}" >> Log.addContextDestructured "file" filePath) do! (commands.Parse filePath content version (Some tfmConfig) |> Async.Ignore) @@ -352,7 +353,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS commandDisposables.Add <| commands.Notify.Subscribe handleCommandEvents ///Helper function for handling Position requests using **recent** type check results - member x.positionHandler<'a, 'b when 'b :> ITextDocumentPositionParams> (f: 'b -> FcsPos -> ParseAndCheckResults -> string -> string [] -> AsyncLspResult<'a>) (arg: 'b) : AsyncLspResult<'a> = + member x.positionHandler<'a, 'b when 'b :> ITextDocumentPositionParams> (f: 'b -> FcsPos -> ParseAndCheckResults -> string -> ISourceText -> AsyncLspResult<'a>) (arg: 'b) : AsyncLspResult<'a> = async { let pos = arg.GetFcsPos() let file = arg.GetFilePath() |> Utils.normalizePath @@ -382,7 +383,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS } ///Helper function for handling Position requests using **latest** type check results - member x.positionHandlerWithLatest<'a, 'b when 'b :> ITextDocumentPositionParams> (f: 'b -> FcsPos -> ParseAndCheckResults -> string -> string [] -> AsyncLspResult<'a>) (arg: 'b) : AsyncLspResult<'a> = + member x.positionHandlerWithLatest<'a, 'b when 'b :> ITextDocumentPositionParams> (f: 'b -> FcsPos -> ParseAndCheckResults -> string -> ISourceText -> AsyncLspResult<'a>) (arg: 'b) : AsyncLspResult<'a> = async { let pos = arg.GetFcsPos() let file = arg.GetFilePath() |> Utils.normalizePath @@ -418,7 +419,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS } ///Helper function for handling file requests using **recent** type check results - member x.fileHandler<'a> (f: string -> ParseAndCheckResults -> string [] -> AsyncLspResult<'a>) (file: string) : AsyncLspResult<'a> = + member x.fileHandler<'a> (f: string -> ParseAndCheckResults -> ISourceText -> AsyncLspResult<'a>) (file: string) : AsyncLspResult<'a> = async { // logger.info (Log.setMessage "PositionHandler - Position request: {file} at {pos}" >> Log.addContextDestructured "file" file >> Log.addContextDestructured "pos" pos) @@ -489,8 +490,8 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS let getFileLines = commands.TryGetFileCheckerOptionsWithLines >> Result.map snd - let getRangeText fileName range = getFileLines fileName |> Result.map (fun lines -> getText lines range) - let getLineText lines range = getText lines range + let getLineText (lines: ISourceText) (range: LanguageServerProtocol.Types.Range) = lines.GetText (protocolRangeToRange "unknown.fsx" range) + let getRangeText fileName (range: LanguageServerProtocol.Types.Range) = getFileLines fileName |> Result.bind (fun lines -> lines.GetText (protocolRangeToRange (UMX.untag fileName) range)) let getProjectOptsAndLines = commands.TryGetFileCheckerOptionsWithLinesAndLineStr let tryGetProjectOptions = commands.TryGetFileCheckerOptionsWithLines >> Result.map fst @@ -669,7 +670,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS override __.TextDocumentDidOpen(p: DidOpenTextDocumentParams) = async { let doc = p.TextDocument let filePath = doc.GetFilePath() |> Utils.normalizePath - let content = doc.Text.Split('\n') + let content = SourceText.ofString doc.Text let tfmConfig = config.UseSdkScripts logger.info (Log.setMessage "TextDocumentDidOpen Request: {parms}" >> Log.addContextDestructured "parms" filePath ) @@ -698,7 +699,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS match contentChange, doc.Version with | Some contentChange, Some version -> if contentChange.Range.IsNone && contentChange.RangeLength.IsNone then - let content = contentChange.Text.Split('\n') + let content = SourceText.ofString contentChange.Text commands.SetFileContent(filePath, content, Some version, config.ScriptTFM) else () | _ -> () @@ -713,8 +714,8 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS } override __.TextDocumentCompletion(p: CompletionParams) = - let ensureInBounds (lines: LineStr array) (line, col) = - let lineStr = lines.[line] + let ensureInBounds (lines: ISourceText) (line, col) = + let lineStr = lines.GetLineString line if line <= lines.Length && line >= 0 && col <= lineStr.Length + 1 && col >= 0 then Ok () else @@ -732,7 +733,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS let pos = p.GetFcsPos() let! (options, lines) = commands.TryGetFileCheckerOptionsWithLines file |> Result.mapError JsonRpc.Error.InternalErrorMessage let line, col = p.Position.Line, p.Position.Character - let lineStr = lines.[line] + let lineStr = lines.GetLineString line let word = lineStr.Substring(0, min col lineStr.Length) do! ensureInBounds lines (line, col) @@ -1094,12 +1095,8 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS | Some (lines, formatted) -> let range = let zero = { Line = 0; Character = 0 } - let endLine = Array.length lines - 1 - let endCharacter = - Array.tryLast lines - |> Option.map (fun line -> line.Length) - |> Option.defaultValue 0 - { Start = zero; End = { Line = endLine; Character = endCharacter } } + let lastPos = lines.GetLastFilePosition() + { Start = zero; End = fcsPosToLsp lastPos } return LspResult.success(Some([| { Range = range; NewText = formatted } |])) | None -> diff --git a/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs b/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs index 84d40f547..5a313b3fa 100644 --- a/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs @@ -273,7 +273,7 @@ let formattingTests state = { Range = { Start = start; End = ``end`` }; NewText = normalizeLineEndings expectedText } let verifyFormatting scenario document = - testCaseAsync $"{scenario} {document}" (async { + testCaseAsync $"{scenario}-{document}" (async { let! (server, events, rootPath) = server let sourceFile = Path.Combine(rootPath, sprintf "%s.input.fsx" document) let expectedFile = Path.Combine(rootPath, sprintf "%s.expected.fsx" document)