Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FSharpWorkspace prototype #17920

Open
wants to merge 17 commits into
base: lsp
Choose a base branch
from
1 change: 1 addition & 0 deletions src/Compiler/Facilities/AsyncMemoize.fs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type internal JobEvent =
| Cleared

type internal ICacheKey<'TKey, 'TVersion> =
// TODO Key should probably be renamed to Identifier
abstract member GetKey: unit -> 'TKey
abstract member GetVersion: unit -> 'TVersion
abstract member GetLabel: unit -> string
Expand Down
127 changes: 73 additions & 54 deletions src/Compiler/Service/FSharpProjectSnapshot.fs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

module FSharp.Compiler.CodeAnalysis.ProjectSnapshot

Expand Down Expand Up @@ -41,10 +41,10 @@ module internal Helpers =
let addFileNameAndVersion (file: IFileSnapshot) =
addFileName file >> Md5Hasher.addBytes file.Version

let signatureHash projectCoreVersion (sourceFiles: IFileSnapshot seq) =
let signatureHash projectBaseVersion (sourceFiles: IFileSnapshot seq) =
let mutable lastFile = ""

((projectCoreVersion, Set.empty), sourceFiles)
((projectBaseVersion, Set.empty), sourceFiles)
||> Seq.fold (fun (res, sigs) file ->
if file.IsSignatureFile then
lastFile <- file.FileName
Expand Down Expand Up @@ -72,6 +72,13 @@ type FSharpFileSnapshot(FileName: string, Version: string, GetSource: unit -> Ta
static member Create(fileName: string, version: string, getSource: unit -> Task<ISourceTextNew>) =
FSharpFileSnapshot(fileName, version, getSource)

static member CreateFromString(filename: string, content: string) =
FSharpFileSnapshot(
filename,
Md5Hasher.hashString content |> Md5Hasher.toString,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably eventually change to something more proper, can't ship with md5 hashing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most definitely. We should probably bring in some version of xxHash.

Copy link
Member

@vzarytovskii vzarytovskii Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. That was my thinking as well.

fun () -> Task.FromResult(SourceTextNew.ofString content)
)

static member CreateFromFileSystem(fileName: string) =
FSharpFileSnapshot(
fileName,
Expand Down Expand Up @@ -171,11 +178,27 @@ type ReferenceOnDisk =
{ Path: string; LastModified: DateTime }

/// A snapshot of an F# project. The source file type can differ based on which stage of compilation the snapshot is used for.
type internal ProjectSnapshotBase<'T when 'T :> IFileSnapshot>(projectCore: ProjectCore, sourceFiles: 'T list) =
type internal ProjectSnapshotBase<'T when 'T :> IFileSnapshot>
(projectCore: ProjectCore, referencedProjects: FSharpReferencedProjectSnapshot list, sourceFiles: 'T list) =

let noFileVersionsHash =
// Version of project without source files
let baseVersion =
lazy
(projectCore.Version
|> Md5Hasher.addBytes' (referencedProjects |> Seq.map _.Version))

let baseVersionString = lazy (baseVersion.Value |> Md5Hasher.toString)

let baseCacheKeyWith (label, version) =
{ new ICacheKey<_, _> with
member _.GetLabel() = $"{label} ({projectCore.Label})"
member _.GetKey() = projectCore.Identifier
member _.GetVersion() = baseVersionString.Value, version
}

let noFileVersionsHash =
lazy
(baseVersion.Value
|> Md5Hasher.addStrings (sourceFiles |> Seq.map (fun x -> x.FileName)))

let noFileVersionsKey =
Expand All @@ -191,7 +214,7 @@ type internal ProjectSnapshotBase<'T when 'T :> IFileSnapshot>(projectCore: Proj

let fullHash =
lazy
(projectCore.Version
(baseVersion.Value
|> Md5Hasher.addStrings (
sourceFiles
|> Seq.collect (fun x ->
Expand All @@ -213,10 +236,10 @@ type internal ProjectSnapshotBase<'T when 'T :> IFileSnapshot>(projectCore: Proj
hash |> Md5Hasher.addString file.FileName |> Md5Hasher.addBytes file.Version

let signatureHash =
lazy (signatureHash projectCore.Version (sourceFiles |> Seq.map (fun x -> x :> IFileSnapshot)))
lazy (signatureHash baseVersion.Value (sourceFiles |> Seq.map (fun x -> x :> IFileSnapshot)))

let signatureKey =
lazy (projectCore.CacheKeyWith("Signature", signatureHash.Value |> fst |> Md5Hasher.toString))
lazy (baseCacheKeyWith ("Signature", signatureHash.Value |> fst |> Md5Hasher.toString))

let lastFileHash =
lazy
Expand Down Expand Up @@ -246,7 +269,7 @@ type internal ProjectSnapshotBase<'T when 'T :> IFileSnapshot>(projectCore: Proj
member _.Identifier = projectCore.Identifier
member _.ReferencesOnDisk = projectCore.ReferencesOnDisk
member _.OtherOptions = projectCore.OtherOptions
member _.ReferencedProjects = projectCore.ReferencedProjects
member _.ReferencedProjects = referencedProjects

member _.IsIncompleteTypeCheckEnvironment =
projectCore.IsIncompleteTypeCheckEnvironment
Expand Down Expand Up @@ -275,7 +298,7 @@ type internal ProjectSnapshotBase<'T when 'T :> IFileSnapshot>(projectCore: Proj
|> Option.defaultWith (fun () -> failwith (sprintf "Unable to find file %s in project %s" fileName projectCore.ProjectFileName))

member private _.With(sourceFiles: 'T list) =
ProjectSnapshotBase(projectCore, sourceFiles)
ProjectSnapshotBase(projectCore, referencedProjects, sourceFiles)

/// Create a new snapshot with given source files replacing files in this snapshot with the same name. Other files remain unchanged.
member this.Replace(changedSourceFiles: 'T list) =
Expand Down Expand Up @@ -352,28 +375,31 @@ type internal ProjectSnapshotBase<'T when 'T :> IFileSnapshot>(projectCore: Proj

fileKey.WithExtraVersion(fileSnapshot.Version |> Md5Hasher.toString)

/// Cache key for the project without source files
member this.BaseCacheKeyWith(label, version) = baseCacheKeyWith (label, version)

/// Project snapshot with filenames and versions given as initial input
and internal ProjectSnapshot = ProjectSnapshotBase<FSharpFileSnapshot>

/// Project snapshot with file sources loaded
and internal ProjectSnapshotWithSources = ProjectSnapshotBase<FSharpFileSnapshotWithSource>

/// All required information for compiling a project except the source files. It's kept separate so it can be reused
/// All required information for compiling a project except the source files and referenced projects. It's kept separate so it can be reused
/// for different stages of a project snapshot and also between changes to the source files.
and internal ProjectCore
(
ProjectFileName: string,
OutputFileName: string option,
ProjectId: string option,
ReferencesOnDisk: ReferenceOnDisk list,
OtherOptions: string list,
ReferencedProjects: FSharpReferencedProjectSnapshot list,
IsIncompleteTypeCheckEnvironment: bool,
UseScriptResolutionRules: bool,
LoadTime: DateTime,
UnresolvedReferences: FSharpUnresolvedReferencesSet option,
OriginalLoadReferences: (range * string * string) list,
Stamp: int64 option
) as self =
) =

let hashForParsing =
lazy
Expand All @@ -387,16 +413,7 @@ and internal ProjectCore
lazy
(hashForParsing.Value
|> Md5Hasher.addStrings (ReferencesOnDisk |> Seq.map (fun r -> r.Path))
|> Md5Hasher.addDateTimes (ReferencesOnDisk |> Seq.map (fun r -> r.LastModified))
|> Md5Hasher.addBytes' (
ReferencedProjects
|> Seq.map (function
| FSharpReference(_name, p) -> p.ProjectSnapshot.SignatureVersion
| PEReference(getStamp, _) -> Md5Hasher.empty |> Md5Hasher.addDateTime (getStamp ())
| ILModuleReference(_name, getStamp, _) -> Md5Hasher.empty |> Md5Hasher.addDateTime (getStamp ()))
))

let fullHashString = lazy (fullHash.Value |> Md5Hasher.toString)
|> Md5Hasher.addDateTimes (ReferencesOnDisk |> Seq.map (fun r -> r.LastModified)))

let commandLineOptions =
lazy
Expand All @@ -408,21 +425,17 @@ and internal ProjectCore
}
|> Seq.toList)

let outputFileName = lazy (OtherOptions |> findOutputFileName)

let key = lazy (ProjectFileName, outputFileName.Value |> Option.defaultValue "")

let cacheKey =
let outputFileName =
lazy
({ new ICacheKey<_, _> with
member _.GetLabel() = self.Label
member _.GetKey() = self.Identifier
member _.GetVersion() = fullHashString.Value
})
(OutputFileName
|> Option.orElseWith (fun () -> OtherOptions |> findOutputFileName))

let identifier =
lazy (ProjectFileName, outputFileName.Value |> Option.defaultValue "")

member val ProjectDirectory = !! Path.GetDirectoryName(ProjectFileName)
member _.OutputFileName = outputFileName.Value
member _.Identifier: ProjectIdentifier = key.Value
member _.Identifier: ProjectIdentifier = identifier.Value
member _.Version = fullHash.Value
member _.Label = ProjectFileName |> shortPath
member _.VersionForParsing = hashForParsing.Value
Expand All @@ -433,30 +446,14 @@ and internal ProjectCore
member _.ProjectId = ProjectId
member _.ReferencesOnDisk = ReferencesOnDisk
member _.OtherOptions = OtherOptions
member _.ReferencedProjects = ReferencedProjects

member _.IsIncompleteTypeCheckEnvironment = IsIncompleteTypeCheckEnvironment
member _.UseScriptResolutionRules = UseScriptResolutionRules
member _.LoadTime = LoadTime
member _.UnresolvedReferences = UnresolvedReferences
member _.OriginalLoadReferences = OriginalLoadReferences
member _.Stamp = Stamp

member _.CacheKeyWith(label, version) =
{ new ICacheKey<_, _> with
member _.GetLabel() = $"{label} ({self.Label})"
member _.GetKey() = self.Identifier
member _.GetVersion() = fullHashString.Value, version
}

member _.CacheKeyWith(label, key, version) =
{ new ICacheKey<_, _> with
member _.GetLabel() = $"{label} ({self.Label})"
member _.GetKey() = key, self.Identifier
member _.GetVersion() = fullHashString.Value, version
}

member _.CacheKey = cacheKey.Value

and [<NoComparison; CustomEquality; Experimental("This FCS API is experimental and subject to change.")>] FSharpReferencedProjectSnapshot =
/// <summary>
/// A reference to an F# project. The physical data for it is stored/cached inside of the compiler service.
Expand Down Expand Up @@ -503,6 +500,12 @@ and [<NoComparison; CustomEquality; Experimental("This FCS API is experimental a
static member CreateFSharp(projectOutputFile, snapshot: FSharpProjectSnapshot) =
FSharpReference(projectOutputFile, snapshot)

member this.Version =
match this with
| FSharpReference(_name, p) -> p.ProjectSnapshot.SignatureVersion
| PEReference(getStamp, _) -> Md5Hasher.empty |> Md5Hasher.addDateTime (getStamp ())
| ILModuleReference(_name, getStamp, _) -> Md5Hasher.empty |> Md5Hasher.addDateTime (getStamp ())

override this.Equals(o) =
match o with
| :? FSharpReferencedProjectSnapshot as o ->
Expand All @@ -523,6 +526,17 @@ and [<NoComparison; CustomEquality; Experimental("This FCS API is experimental a
and [<Experimental("This FCS API is experimental and subject to change.")>] FSharpProjectIdentifier =
| FSharpProjectIdentifier of projectFileName: string * outputFileName: string

member this.OutputFileName =
match this with
| FSharpProjectIdentifier(_, outputFileName) -> outputFileName

member this.ProjectFileName =
match this with
| FSharpProjectIdentifier(projectFileName, _) -> projectFileName

override this.ToString() =
$"{shortPath this.ProjectFileName} 🡒 {shortPath this.OutputFileName}"

/// A snapshot of an F# project. This type contains all the necessary information for type checking a project.
and [<Experimental("This FCS API is experimental and subject to change.")>] FSharpProjectSnapshot internal (projectSnapshot) =

Expand All @@ -549,10 +563,12 @@ and [<Experimental("This FCS API is experimental and subject to change.")>] FSha
member _.UnresolvedReferences = projectSnapshot.UnresolvedReferences
member _.OriginalLoadReferences = projectSnapshot.OriginalLoadReferences
member _.Stamp = projectSnapshot.Stamp
member _.OutputFileName = projectSnapshot.OutputFileName

static member Create
(
projectFileName: string,
outputFileName: string option,
projectId: string option,
sourceFiles: FSharpFileSnapshot list,
referencesOnDisk: ReferenceOnDisk list,
Expand All @@ -569,10 +585,10 @@ and [<Experimental("This FCS API is experimental and subject to change.")>] FSha
let projectCore =
ProjectCore(
projectFileName,
outputFileName,
projectId,
referencesOnDisk,
otherOptions,
referencedProjects,
isIncompleteTypeCheckEnvironment,
useScriptResolutionRules,
loadTime,
Expand All @@ -581,7 +597,8 @@ and [<Experimental("This FCS API is experimental and subject to change.")>] FSha
stamp
)

ProjectSnapshotBase(projectCore, sourceFiles) |> FSharpProjectSnapshot
ProjectSnapshotBase(projectCore, referencedProjects, sourceFiles)
|> FSharpProjectSnapshot

static member FromOptions(options: FSharpProjectOptions, getFileSnapshot, ?snapshotAccumulator) =
let snapshotAccumulator = defaultArg snapshotAccumulator (Dictionary())
Expand Down Expand Up @@ -629,6 +646,7 @@ and [<Experimental("This FCS API is experimental and subject to change.")>] FSha
let snapshot =
FSharpProjectSnapshot.Create(
projectFileName = options.ProjectFileName,
outputFileName = None,
projectId = options.ProjectId,
sourceFiles = (sourceFiles |> List.ofArray),
referencesOnDisk = (referencesOnDisk |> List.ofArray),
Expand Down Expand Up @@ -683,7 +701,7 @@ and [<Experimental("This FCS API is experimental and subject to change.")>] FSha

let compilerArgs = File.ReadAllLines responseFile.FullName

let directoryName : string =
let directoryName: string =
match responseFile.DirectoryName with
| null -> failwith "Directory name of the response file is null"
| str -> str
Expand Down Expand Up @@ -720,6 +738,7 @@ and [<Experimental("This FCS API is experimental and subject to change.")>] FSha

FSharpProjectSnapshot.Create(
projectFileName = projectFileName,
outputFileName = None,
projectId = None,
sourceFiles = (fsharpFiles |> List.map FSharpFileSnapshot.CreateFromFileSystem),
referencesOnDisk =
Expand Down
11 changes: 6 additions & 5 deletions src/Compiler/Service/TransparentCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -826,8 +826,8 @@ type internal TransparentCompiler
let mutable BootstrapInfoIdCounter = 0

/// Bootstrap info that does not depend source files
let ComputeBootstrapInfoStatic (projectSnapshot: ProjectCore, tcConfig: TcConfig, assemblyName: string, loadClosureOpt) =
let cacheKey = projectSnapshot.CacheKeyWith("BootstrapInfoStatic", assemblyName)
let ComputeBootstrapInfoStatic (projectSnapshot: ProjectSnapshotBase<_>, tcConfig: TcConfig, assemblyName: string, loadClosureOpt) =
let cacheKey = projectSnapshot.BaseCacheKeyWith("BootstrapInfoStatic", assemblyName)

caches.BootstrapInfoStatic.Get(
cacheKey,
Expand Down Expand Up @@ -957,7 +957,7 @@ type internal TransparentCompiler
let outFile, _, assemblyName = tcConfigB.DecideNames sourceFiles

let! bootstrapId, tcImports, tcGlobals, initialTcInfo, importsInvalidatedByTypeProvider =
ComputeBootstrapInfoStatic(projectSnapshot.ProjectCore, tcConfig, assemblyName, loadClosureOpt)
ComputeBootstrapInfoStatic(projectSnapshot, tcConfig, assemblyName, loadClosureOpt)

// Check for the existence of loaded sources and prepend them to the sources list if present.
let loadedSources =
Expand Down Expand Up @@ -1056,7 +1056,7 @@ type internal TransparentCompiler
|> Seq.map (fun f -> LoadSource f isExe (f.FileName = bootstrapInfo.LastFileName))
|> MultipleDiagnosticsLoggers.Parallel

return ProjectSnapshotWithSources(projectSnapshot.ProjectCore, sources |> Array.toList)
return ProjectSnapshotWithSources(projectSnapshot.ProjectCore, projectSnapshot.ReferencedProjects, sources |> Array.toList)

}

Expand Down Expand Up @@ -1467,7 +1467,7 @@ type internal TransparentCompiler
|> Seq.map (ComputeParseFile projectSnapshot tcConfig)
|> MultipleDiagnosticsLoggers.Parallel

return ProjectSnapshotBase<_>(projectSnapshot.ProjectCore, parsedInputs |> Array.toList)
return ProjectSnapshotBase<_>(projectSnapshot.ProjectCore, projectSnapshot.ReferencedProjects, parsedInputs |> Array.toList)
}

// Type check file and all its dependencies
Expand Down Expand Up @@ -2394,6 +2394,7 @@ type internal TransparentCompiler
FSharpProjectSnapshot.Create(
fileName + ".fsproj",
None,
None,
sourceFiles,
references,
otherFlags,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ type CapabilitiesManager(scOverrides: IServerCapabilitiesOverride seq) =
TextDocumentSync = TextDocumentSyncOptions(OpenClose = true, Change = TextDocumentSyncKind.Full),
DiagnosticOptions =
DiagnosticOptions(WorkDoneProgress = true, InterFileDependencies = true, Identifier = "potato", WorkspaceDiagnostics = true),
CompletionProvider =
CompletionOptions(TriggerCharacters=[|"."; " "|], ResolveProvider=true, WorkDoneProgress=true),
CompletionProvider = CompletionOptions(TriggerCharacters = [| "."; " " |], ResolveProvider = true, WorkDoneProgress = true),
HoverProvider = SumType<bool, HoverOptions>(HoverOptions(WorkDoneProgress = true))
)

Expand All @@ -33,4 +32,4 @@ type CapabilitiesManager(scOverrides: IServerCapabilitiesOverride seq) =
member this.GetInitializeParams() =
match initializeParams with
| Some params' -> params'
| None -> failwith "InitializeParams is null"
| None -> failwith "InitializeParams is null"
Loading