Skip to content

Commit

Permalink
Fix #1906
Browse files Browse the repository at this point in the history
  • Loading branch information
Happypig375 committed Aug 12, 2023
1 parent 72103b5 commit 8ff5882
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 10 deletions.
8 changes: 8 additions & 0 deletions src/FsAutoComplete.Core/FileSystem.fs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ type NamedText(fileName: string<LocalPath>, str: string) =

/// Provides safe access to a substring of the file via FCS-provided Range
member x.GetText(m: FSharp.Compiler.Text.Range) : Result<string, string> =
// indexing into first line of empty file can be encountered when typing from an empty file
// if we don't check it, GetLineString will throw IndexOutOfRangeException
if (x :> ISourceText).GetLineCount() = 0 then Ok "" else
if not (Range.rangeContainsRange x.TotalRange 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
Expand Down Expand Up @@ -248,6 +251,8 @@ type NamedText(fileName: string<LocalPath>, str: string) =

/// Provides safe access to a line of the file via FCS-provided Position
member x.GetLine(pos: FSharp.Compiler.Text.Position) : string option =
// indexing into first line of empty file can be encountered when typing from an empty file
if (x :> ISourceText).GetLineCount() = 0 then Some "" else
if pos.Line < 1 || pos.Line > getLines.Value.Length then
None
else
Expand All @@ -265,6 +270,9 @@ type NamedText(fileName: string<LocalPath>, str: string) =
/// Also available in indexer form: <code lang="fsharp">x[pos]</code></summary>
member x.TryGetChar(pos: FSharp.Compiler.Text.Position) : char option =
option {
// indexing into first line of empty file can be encountered when typing from an empty file
// if we don't check it, GetLineUnsafe will throw IndexOutOfRangeException
do! Option.guard ((x :> ISourceText).GetLineCount() > 0)
do! Option.guard (Range.rangeContainsPos (x.TotalRange) pos)
let lineText = x.GetLineUnsafe(pos)

Expand Down
25 changes: 16 additions & 9 deletions src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ type AdaptiveFSharpLspServer
do
disposables.Add
<| fileChecked.Publish.Subscribe(fun (parseAndCheck, volatileFile, ct) ->
if volatileFile.Source.Length = 0 then () else // Don't analyze and error on an empty file
async {
let config = config |> AVal.force
do! builtInCompilerAnalyzers config volatileFile parseAndCheck
Expand Down Expand Up @@ -1054,16 +1055,21 @@ type AdaptiveFSharpLspServer
Log.setMessage "forceFindOpenFileOrRead else - {file}"
>> Log.addContextDestructured "file" file
)
if File.Exists (UMX.untag file) then
use s = File.openFileStreamForReadingAsync file

use s = File.openFileStreamForReadingAsync file

let! source = sourceTextFactory.Create(file, s) |> Async.AwaitCancellableValueTask

return
{ LastTouched = File.getLastWriteTimeOrDefaultNow file
Source = source
Version = 0 }
let! source = sourceTextFactory.Create(file, s) |> Async.AwaitCancellableValueTask

return
{ LastTouched = File.getLastWriteTimeOrDefaultNow file
Source = source
Version = 0 }

else // When a user does "File -> New Text File -> Select a language -> F#" without saving, the file won't exist
return
{ LastTouched = DateTime.UtcNow
Source = sourceTextFactory.Create(file, "")
Version = 0 }
with e ->
logger.warn (
Log.setMessage "Could not read file {file}"
Expand Down Expand Up @@ -2432,7 +2438,8 @@ type AdaptiveFSharpLspServer
let (filePath, pos) = getFilePathAndPosition p

let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr

if volatileFile.Source.Length = 0 then return None else // An empty file has empty completions. Otherwise we would error down there

let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr

if lineStr.StartsWith "#" then
Expand Down
110 changes: 110 additions & 0 deletions test/FsAutoComplete.Tests.Lsp/EmptyFileTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
module FsAutoComplete.Tests.EmptyFileTests

open Expecto
open System.IO
open Helpers
open Ionide.LanguageServerProtocol.Types
open FsAutoComplete.Utils
open FsAutoComplete.Lsp
open FsToolkit.ErrorHandling
open Utils.Server
open Helpers.Expecto.ShadowedTimeouts

let tests state =
let createServer() =
async {
let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "EmptyFileTests")

let scriptPath = Path.Combine(path, "EmptyFile.fsx")

let! (server, events) = serverInitialize path defaultConfigDto state

do! waitForWorkspaceFinishedParsing events
return server, events, scriptPath
}
|> Async.Cache
let server1 = createServer()
let server2 = createServer()

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
do! server.TextDocumentDidClose { TextDocument = { Uri = Path.FilePathToUri scriptPath } }
})

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 } }

let! response = server.TextDocumentCompletion completionParams

match response 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

match! waitForCompilerDiagnosticsForFile "EmptyFile.fsx" events with
| Ok () -> failtest "should get a checking error from an 'c' by itself"
| Core.Result.Error errors ->
Expect.hasLength errors 1 "should have an error FS0039: identifier not defined"
Expect.exists errors (fun error -> error.Code = Some "39") "should have an error FS0039: identifier not defined"

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
})
]]
2 changes: 1 addition & 1 deletion test/FsAutoComplete.Tests.Lsp/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ let lspTests =
InlayHintTests.tests createServer
DependentFileChecking.tests createServer
UnusedDeclarationsTests.tests createServer

EmptyFileTests.tests createServer
] ]

/// Tests that do not require a LSP server
Expand Down
Empty file.

0 comments on commit 8ff5882

Please sign in to comment.