add workspace and diagnostics to lsp (#7006)
This commit is contained in:
Родитель
f49548692d
Коммит
8121dd5cd6
|
@ -0,0 +1,49 @@
|
|||
<Project DefaultTargets="Build">
|
||||
|
||||
<!--
|
||||
|
||||
Usage: dotnet build <this-file> /p:ProjectFile=<path-to-project>
|
||||
Returns: The project's supported target frameworks, one per line, in the format of:
|
||||
DetectedTargetFramework=<tfm-1>
|
||||
DetectedTargetFramework=<tfm-2>
|
||||
...
|
||||
|
||||
Usage: dotnet build <this-file> /p:ProjectFile=<path-to-project> /p:TargetFramework=<tfm>
|
||||
Returns: The project's command line arguments, one per line, as they would be passed to fsc.exe in the format of:
|
||||
DetectedCommandLineArg=<arg-1>
|
||||
DetectedCommandLineArg=<arg-2>
|
||||
...
|
||||
|
||||
To avoid recreating a distribution of MSBuild just to get some evaluated project values, we instead inject a special
|
||||
targets file into the specified project which then prints the values with a well-known prefix that's easy to parse.
|
||||
|
||||
The benefits of doing it this way are:
|
||||
- We don't have to re-create a copy of MSBuild. That would be difficult, large, and change frequently.
|
||||
- This works on any OS and any TFM.
|
||||
- We use the exact version of MSBuild and the compiler that the user has instead of dealing with the potential
|
||||
mismatch between what this tool would include vs. what they actually have.
|
||||
|
||||
The downsides of this method are:
|
||||
- We're dependent upon the continued existence of some well-known Targets and ItemGroups, but the ones we depend on
|
||||
have been stable for some time.
|
||||
- An external process invoke is slow, but this is only done once at LSP instantiation.
|
||||
|
||||
-->
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- See this file for details on the magic. -->
|
||||
<InjetedTargetsFile>$(MSBuildThisFileDirectory)FSharp.Compiler.LanguageServer.DesignTime.targets</InjetedTargetsFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="Build">
|
||||
<Error Text="Property `ProjectFile` must be specified." Condition="'$(ProjectFile)' == ''" />
|
||||
|
||||
<!-- report TFMs if none specified -->
|
||||
<MSBuild Projects="$(ProjectFile)" Targets="ReportTargetFrameworks" Properties="CustomAfterMicrosoftCommonCrossTargetingTargets=$(InjetedTargetsFile);CustomAfterMicrosoftCommonTargets=$(InjetedTargetsFile)" Condition="'$(TargetFramework)' == ''" />
|
||||
|
||||
<!-- report command line arguments when TFM is given -->
|
||||
<MSBuild Projects="$(ProjectFile)" Targets="Restore" Condition="'$(TargetFramework)' != ''" />
|
||||
<MSBuild Projects="$(ProjectFile)" Targets="ReportCommandLineArgs" Properties="DesignTimeBuild=true;CustomAfterMicrosoftCommonTargets=$(InjetedTargetsFile);TargetFramework=$(TargetFramework)" Condition="'$(TargetFramework)' != ''" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,52 @@
|
|||
<Project>
|
||||
|
||||
<!--
|
||||
Accurately reporting the command line arguments requires that the target Restore is run _without_ these values having
|
||||
been set. The project file helper associated with this .targets file calls Restore before calling into the target
|
||||
that prints the command line arguments.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(DesignTimeBuild)' == 'true'">
|
||||
<ProvideCommandLineArgs>true</ProvideCommandLineArgs>
|
||||
<BuildProjectReferences>false</BuildProjectReferences>
|
||||
<SkipCompilerExecution>true</SkipCompilerExecution>
|
||||
<DisableRarCache>true</DisableRarCache>
|
||||
<AutoGenerateBindingRedirects>false</AutoGenerateBindingRedirects>
|
||||
<CopyBuildOutputToOutputDirectory>false</CopyBuildOutputToOutputDirectory>
|
||||
<CopyOutputSymbolsToOutputDirectory>false</CopyOutputSymbolsToOutputDirectory>
|
||||
<SkipCopyBuildProduct>true</SkipCopyBuildProduct>
|
||||
<AddModules>false</AddModules>
|
||||
<UseCommonOutputDirectory>true</UseCommonOutputDirectory>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- displays the target frameworks of a cross-targeted project -->
|
||||
<Target Name="_ComputeDependsOn">
|
||||
<!--
|
||||
In a multi-targeted project, the _ComputeTargetFrameworkItems populates the ItemGroup _TargetFramework with the
|
||||
appropriate values. If the project contains just a single value for $(TargetFramework) then the target
|
||||
_ComputeTargetFrameworkItems doesn't exist, so instead the helper target _PopulateTargetFrameworks is run which
|
||||
populates the same group.
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<WorkerDependsOn Condition="'$(TargetFramework)' == ''">_ComputeTargetFrameworkItems</WorkerDependsOn>
|
||||
<WorkerDependsOn Condition="'$(TargetFramework)' != ''">_PopulateTargetFrameworks</WorkerDependsOn>
|
||||
</PropertyGroup>
|
||||
</Target>
|
||||
<Target Name="_PopulateTargetFrameworks">
|
||||
<ItemGroup>
|
||||
<_TargetFramework Include="$(TargetFramework)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
<Target Name="_ReportTargetFrameworksWorker" DependsOnTargets="_ComputeDependsOn;$(WorkerDependsOn)">
|
||||
<Message Text="DetectedTargetFramework=%(_TargetFramework.Identity)" Importance="High" />
|
||||
</Target>
|
||||
<Target Name="ReportTargetFrameworks" DependsOnTargets="_ComputeDependsOn;_ReportTargetFrameworksWorker">
|
||||
</Target>
|
||||
|
||||
<!-- displays the command line arguments passed to fsc.exe -->
|
||||
<Target Name="ReportCommandLineArgs"
|
||||
DependsOnTargets="Build">
|
||||
<Message Text="DetectedCommandLineArg=%(FscCommandLineArgs.Identity)" Importance="High" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
|
@ -23,6 +23,15 @@
|
|||
<Compile Include="Program.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="FSharp.Compiler.LanguageServer.DesignTime.proj" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="FSharp.Compiler.LanguageServer.DesignTime.targets" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="FSharp.Compiler.LanguageServer.UnitTests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(MSBuildThisFileDirectory)..\FSharp.Compiler.Private\FSharp.Compiler.Private.fsproj" />
|
||||
<ProjectReference Include="$(MSBuildThisFileDirectory)..\FSharp.Core\FSharp.Core.fsproj" />
|
||||
|
|
|
@ -9,10 +9,18 @@ module FunctionNames =
|
|||
[<Literal>]
|
||||
let OptionsSet = "options/set"
|
||||
|
||||
[<Literal>]
|
||||
let TextDocumentPublishDiagnostics = "textDocument/publishDiagnostics"
|
||||
|
||||
type Options =
|
||||
{ usePreviewTextHover: bool }
|
||||
{ usePreviewTextHover: bool
|
||||
usePreviewDiagnostics: bool }
|
||||
static member Default() =
|
||||
{ usePreviewTextHover = false }
|
||||
{ usePreviewTextHover = false
|
||||
usePreviewDiagnostics = false }
|
||||
static member AllOn() =
|
||||
{ usePreviewTextHover = true
|
||||
usePreviewDiagnostics = true }
|
||||
|
||||
module Extensions =
|
||||
type JsonRpc with
|
||||
|
|
|
@ -5,8 +5,9 @@ namespace FSharp.Compiler.LanguageServer
|
|||
open Newtonsoft.Json.Linq
|
||||
open Newtonsoft.Json
|
||||
|
||||
// Interfaces as defined at https://microsoft.github.io/language-server-protocol/specification. The properties on
|
||||
// these types are camlCased to match the underlying JSON properties to avoid attributes on every field:
|
||||
// Interfaces as defined at https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/.
|
||||
// The properties on these types are camlCased to match the underlying JSON properties to avoid attributes on every
|
||||
// field:
|
||||
// [<JsonProperty("camlCased")>]
|
||||
|
||||
/// Represents a zero-based line and column of a text document.
|
||||
|
@ -32,7 +33,7 @@ type Diagnostic =
|
|||
{ range: Range
|
||||
severity: int option
|
||||
code: string
|
||||
source: string option // "F#"
|
||||
source: string option
|
||||
message: string
|
||||
relatedInformation: DiagnosticRelatedInformation[] option }
|
||||
static member Error = 1
|
||||
|
@ -46,7 +47,7 @@ type PublishDiagnosticsParams =
|
|||
|
||||
type ClientCapabilities =
|
||||
{ workspace: JToken option // TODO: WorkspaceClientCapabilities
|
||||
textDocument: JToken option // TODO: TextDocumentCapabilities
|
||||
textDocument: JToken option // TODO: TextDocumentClientCapabilities, publishDiagnostics: { relatedInformation: bool option }
|
||||
experimental: JToken option
|
||||
supportsVisualStudioExtensions: bool option }
|
||||
|
||||
|
|
|
@ -8,8 +8,10 @@ open System.Threading
|
|||
open Newtonsoft.Json.Linq
|
||||
open StreamJsonRpc
|
||||
|
||||
// https://microsoft.github.io/language-server-protocol/specification
|
||||
type Methods(state: State) =
|
||||
// https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/
|
||||
type Methods() =
|
||||
|
||||
let state = State()
|
||||
|
||||
/// Helper to run Async<'T> with a CancellationToken.
|
||||
let runAsync (cancellationToken: CancellationToken) (computation: Async<'T>) = Async.StartAsTask(computation, cancellationToken=cancellationToken)
|
||||
|
@ -29,8 +31,10 @@ type Methods(state: State) =
|
|||
[<Optional; DefaultParameterValue(null: JToken)>] initializationOptions: JToken,
|
||||
capabilities: ClientCapabilities,
|
||||
[<Optional; DefaultParameterValue(null: string)>] trace: string,
|
||||
[<Optional; DefaultParameterValue(null: WorkspaceFolder[])>] workspaceFolders: WorkspaceFolder[]
|
||||
[<Optional; DefaultParameterValue(null: WorkspaceFolder[])>] workspaceFolders: WorkspaceFolder[],
|
||||
[<Optional; DefaultParameterValue(CancellationToken())>] cancellationToken: CancellationToken
|
||||
) =
|
||||
state.Initialize rootPath rootUri (fun projectOptions -> TextDocument.PublishDiagnostics(state, projectOptions) |> Async.Start)
|
||||
{ InitializeResult.capabilities = ServerCapabilities.DefaultCapabilities() }
|
||||
|
||||
[<JsonRpcMethod("initialized")>]
|
||||
|
@ -63,5 +67,6 @@ type Methods(state: State) =
|
|||
(
|
||||
options: Options
|
||||
) =
|
||||
sprintf "got options %A" options |> Console.Error.WriteLine
|
||||
eprintfn "got options %A" options
|
||||
state.Options <- options
|
||||
state.InvalidateAllProjects()
|
||||
|
|
|
@ -12,17 +12,17 @@ type Server(sendingStream: Stream, receivingStream: Stream) =
|
|||
let converter = JsonOptionConverter() // special handler to convert between `Option<'T>` and `obj/null`.
|
||||
do formatter.JsonSerializer.Converters.Add(converter)
|
||||
let handler = new HeaderDelimitedMessageHandler(sendingStream, receivingStream, formatter)
|
||||
let state = State()
|
||||
let methods = Methods(state)
|
||||
let methods = Methods()
|
||||
let rpc = new JsonRpc(handler, methods)
|
||||
do methods.State.JsonRpc <- Some rpc
|
||||
|
||||
member __.StartListening() =
|
||||
rpc.StartListening()
|
||||
|
||||
member __.WaitForExitAsync() =
|
||||
async {
|
||||
do! Async.AwaitEvent (state.Shutdown)
|
||||
do! Async.AwaitEvent (state.Exit)
|
||||
do! Async.AwaitEvent (methods.State.Shutdown)
|
||||
do! Async.AwaitEvent (methods.State.Exit)
|
||||
}
|
||||
|
||||
interface IDisposable with
|
||||
|
|
|
@ -2,11 +2,209 @@
|
|||
|
||||
namespace FSharp.Compiler.LanguageServer
|
||||
|
||||
open System
|
||||
open System.Collections.Concurrent
|
||||
open System.Collections.Generic
|
||||
open System.Diagnostics
|
||||
open System.IO
|
||||
open System.Text.RegularExpressions
|
||||
open FSharp.Compiler.SourceCodeServices
|
||||
open StreamJsonRpc
|
||||
|
||||
module internal Solution =
|
||||
// easy unit testing
|
||||
let getProjectPaths (solutionContent: string) (solutionDir: string) =
|
||||
// This looks scary, but is much more lightweight than carrying along MSBuild just to have it parse the solution file.
|
||||
//
|
||||
// A valid line in .sln looks like:
|
||||
// Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ConsoleApp2", "ConsoleApp2\ConsoleApp2.fsproj", "{60A4BE67-7E03-4200-AD38-B0E5E8E049C1}"
|
||||
// and we're hoping to extract this: ------------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
//
|
||||
// therefore:
|
||||
// ^Project text 'Project' at the start of the line
|
||||
// .* any number of characters
|
||||
// \"" double quote character (it's doubled up to escape from the raw string literal here)
|
||||
// ( start of capture group
|
||||
// [^\""] not a quote
|
||||
// * many of those
|
||||
// \.fsproj literal string ".fsproj"
|
||||
// ) end of capture group
|
||||
// \"" double quote
|
||||
let pattern = Regex(@"^Project.*\""([^\""]*\.fsproj)\""")
|
||||
let lines = solutionContent.Split('\n')
|
||||
let relativeProjects =
|
||||
lines
|
||||
|> Array.map pattern.Match
|
||||
|> Array.filter (fun m -> m.Success)
|
||||
|> Array.map (fun m -> m.Groups.[1].Value)
|
||||
// .sln files by convention uses backslashes, which might not be appropriate at runtime
|
||||
|> Array.map (fun p -> p.Replace('\\', Path.DirectorySeparatorChar))
|
||||
let projects =
|
||||
relativeProjects
|
||||
|> Array.map (fun p -> if Path.IsPathRooted(p) then p else Path.Combine(solutionDir, p))
|
||||
projects
|
||||
|
||||
type State() =
|
||||
|
||||
let checker = FSharpChecker.Create()
|
||||
|
||||
let sourceFileToProjectMap = ConcurrentDictionary<string, FSharpProjectOptions>()
|
||||
|
||||
let shutdownEvent = new Event<_>()
|
||||
let exitEvent = new Event<_>()
|
||||
let cancelEvent = new Event<_>()
|
||||
let projectInvalidatedEvent = new Event<_>()
|
||||
|
||||
let fileChanged (args: FileSystemEventArgs) =
|
||||
match sourceFileToProjectMap.TryGetValue args.FullPath with
|
||||
| true, projectOptions -> projectInvalidatedEvent.Trigger(projectOptions)
|
||||
| false, _ -> ()
|
||||
let fileRenamed (args: RenamedEventArgs) =
|
||||
match sourceFileToProjectMap.TryGetValue args.FullPath with
|
||||
| true, projectOptions -> projectInvalidatedEvent.Trigger(projectOptions)
|
||||
| false, _ -> ()
|
||||
let fileWatcher = new FileSystemWatcher()
|
||||
do fileWatcher.IncludeSubdirectories <- true
|
||||
do fileWatcher.Changed.Add(fileChanged)
|
||||
do fileWatcher.Created.Add(fileChanged)
|
||||
do fileWatcher.Deleted.Add(fileChanged)
|
||||
do fileWatcher.Renamed.Add(fileRenamed)
|
||||
|
||||
let execProcess (name: string) (args: string) =
|
||||
let startInfo = ProcessStartInfo(name, args)
|
||||
eprintfn "executing: %s %s" name args
|
||||
startInfo.CreateNoWindow <- true
|
||||
startInfo.RedirectStandardOutput <- true
|
||||
startInfo.UseShellExecute <- false
|
||||
let lines = List<string>()
|
||||
use proc = new Process()
|
||||
proc.StartInfo <- startInfo
|
||||
proc.OutputDataReceived.Add(fun args -> lines.Add(args.Data))
|
||||
proc.Start() |> ignore
|
||||
proc.BeginOutputReadLine()
|
||||
proc.WaitForExit()
|
||||
lines.ToArray()
|
||||
|
||||
let linesWithPrefixClean (prefix: string) (lines: string[]) =
|
||||
lines
|
||||
|> Array.filter (isNull >> not)
|
||||
|> Array.map (fun line -> line.TrimStart(' '))
|
||||
|> Array.filter (fun line -> line.StartsWith(prefix))
|
||||
|> Array.map (fun line -> line.Substring(prefix.Length))
|
||||
|
||||
let getProjectOptions (rootDir: string) =
|
||||
if isNull rootDir then [||]
|
||||
else
|
||||
fileWatcher.Path <- rootDir
|
||||
fileWatcher.EnableRaisingEvents <- true
|
||||
|
||||
/// This function is meant to be temporary. Until we figure out what a language server for a project
|
||||
/// system looks like, we have to guess based on the files we find in the root.
|
||||
let getProjectOptions (projectPath: string) =
|
||||
let projectDir = Path.GetDirectoryName(projectPath)
|
||||
let normalizePath (path: string) =
|
||||
if Path.IsPathRooted(path) then path
|
||||
else Path.Combine(projectDir, path)
|
||||
|
||||
// To avoid essentially re-creating a copy of MSBuild alongside this tool, we instead fake a design-
|
||||
// time build with this project. The output of building this helper project is text that's easily
|
||||
// parsable. See the helper project for more information.
|
||||
let reporterProject = Path.Combine(Path.GetDirectoryName(typeof<State>.Assembly.Location), "FSharp.Compiler.LanguageServer.DesignTime.proj")
|
||||
let detectedTfmSentinel = "DetectedTargetFramework="
|
||||
let detectedCommandLineArgSentinel = "DetectedCommandLineArg="
|
||||
|
||||
let execTfmReporter =
|
||||
sprintf "build \"%s\" \"/p:ProjectFile=%s\"" reporterProject projectPath
|
||||
|> execProcess "dotnet"
|
||||
|
||||
let execArgReporter (tfm: string) =
|
||||
sprintf "build \"%s\" \"/p:ProjectFile=%s\" \"/p:TargetFramework=%s\"" reporterProject projectPath tfm
|
||||
|> execProcess "dotnet"
|
||||
|
||||
// find the target frameworks
|
||||
let targetFrameworks =
|
||||
execTfmReporter
|
||||
|> linesWithPrefixClean detectedTfmSentinel
|
||||
|
||||
let getArgs (tfm: string) =
|
||||
execArgReporter tfm
|
||||
|> linesWithPrefixClean detectedCommandLineArgSentinel
|
||||
|
||||
let tfmAndArgs =
|
||||
targetFrameworks
|
||||
|> Array.map (fun tfm -> tfm, getArgs tfm)
|
||||
|
||||
let separateArgs (args: string[]) =
|
||||
args
|
||||
|> Array.partition (fun a -> a.StartsWith("-"))
|
||||
|> (fun (options, files) ->
|
||||
let normalizedFiles = files |> Array.map normalizePath
|
||||
options, normalizedFiles)
|
||||
|
||||
// TODO: for now we're only concerned with the first TFM
|
||||
let _tfm, args = Array.head tfmAndArgs
|
||||
|
||||
let otherOptions, sourceFiles = separateArgs args
|
||||
|
||||
let projectOptions: FSharpProjectOptions =
|
||||
{ ProjectFileName = projectPath
|
||||
ProjectId = None
|
||||
SourceFiles = sourceFiles
|
||||
OtherOptions = otherOptions
|
||||
ReferencedProjects = [||] // TODO: populate from @(ProjectReference)
|
||||
IsIncompleteTypeCheckEnvironment = false
|
||||
UseScriptResolutionRules = false
|
||||
LoadTime = DateTime.Now
|
||||
UnresolvedReferences = None
|
||||
OriginalLoadReferences = []
|
||||
ExtraProjectInfo = None
|
||||
Stamp = None }
|
||||
projectOptions
|
||||
let topLevelProjects = Directory.GetFiles(rootDir, "*.fsproj")
|
||||
let watchableProjectPaths =
|
||||
match topLevelProjects with
|
||||
| [||] ->
|
||||
match Directory.GetFiles(rootDir, "*.sln") with
|
||||
// TODO: what to do with multiple .sln or a combo of .sln/.fsproj?
|
||||
| [| singleSolution |] ->
|
||||
let content = File.ReadAllText(singleSolution)
|
||||
let solutionDir = Path.GetDirectoryName(singleSolution)
|
||||
Solution.getProjectPaths content solutionDir
|
||||
| _ -> [||]
|
||||
| _ -> topLevelProjects
|
||||
let watchableProjectOptions =
|
||||
watchableProjectPaths
|
||||
|> Array.map getProjectOptions
|
||||
|
||||
// associate source files with project options
|
||||
let watchFile file projectOptions =
|
||||
sourceFileToProjectMap.AddOrUpdate(file, projectOptions, fun _ _ -> projectOptions)
|
||||
|
||||
for projectOptions in watchableProjectOptions do
|
||||
// watch .fsproj
|
||||
watchFile projectOptions.ProjectFileName projectOptions |> ignore
|
||||
// TODO: watch .deps.json
|
||||
for sourceFile in projectOptions.SourceFiles do
|
||||
let sourceFileFullPath =
|
||||
if Path.IsPathRooted(sourceFile) then sourceFile
|
||||
else
|
||||
let projectDir = Path.GetDirectoryName(projectOptions.ProjectFileName)
|
||||
Path.Combine(projectDir, sourceFile)
|
||||
watchFile sourceFileFullPath projectOptions |> ignore
|
||||
|
||||
watchableProjectOptions
|
||||
|
||||
member __.Checker = checker
|
||||
|
||||
/// Initialize the LSP at the specified location. According to the spec, `rootUri` is to be preferred over `rootPath`.
|
||||
member __.Initialize (rootPath: string) (rootUri: DocumentUri) (computeDiagnostics: FSharpProjectOptions -> unit) =
|
||||
let rootDir =
|
||||
if not (isNull rootUri) then Uri(rootUri).LocalPath
|
||||
else rootPath
|
||||
let projectOptions = getProjectOptions rootDir
|
||||
projectInvalidatedEvent.Publish.Add computeDiagnostics // compute diagnostics on project invalidation
|
||||
for projectOption in projectOptions do
|
||||
computeDiagnostics projectOption // compute initial set of diagnostics
|
||||
|
||||
[<CLIEvent>]
|
||||
member __.Shutdown = shutdownEvent.Publish
|
||||
|
@ -17,10 +215,19 @@ type State() =
|
|||
[<CLIEvent>]
|
||||
member __.Cancel = cancelEvent.Publish
|
||||
|
||||
[<CLIEvent>]
|
||||
member __.ProjectInvalidated = projectInvalidatedEvent.Publish
|
||||
|
||||
member __.DoShutdown() = shutdownEvent.Trigger()
|
||||
|
||||
member __.DoExit() = exitEvent.Trigger()
|
||||
|
||||
member __.DoCancel() = cancelEvent.Trigger()
|
||||
|
||||
member __.InvalidateAllProjects() =
|
||||
for projectOptions in sourceFileToProjectMap.Values do
|
||||
projectInvalidatedEvent.Trigger(projectOptions)
|
||||
|
||||
member val Options = Options.Default() with get, set
|
||||
|
||||
member val JsonRpc: JsonRpc option = None with get, set
|
||||
|
|
|
@ -2,13 +2,15 @@
|
|||
|
||||
namespace FSharp.Compiler.LanguageServer
|
||||
|
||||
open System
|
||||
open System.Threading
|
||||
|
||||
module TextDocument =
|
||||
|
||||
let mutable publishDiagnosticsCancellationTokenSource = new CancellationTokenSource()
|
||||
|
||||
let Hover (state: State) (textDocument: TextDocumentIdentifier) (position: Position) =
|
||||
async {
|
||||
Console.Error.WriteLine("hover at " + position.line.ToString() + "," + position.character.ToString())
|
||||
eprintfn "hover at %d, %d" position.line position.character
|
||||
if not state.Options.usePreviewTextHover then return None
|
||||
else
|
||||
let startCol, endCol =
|
||||
|
@ -21,10 +23,53 @@ module TextDocument =
|
|||
}
|
||||
}
|
||||
|
||||
let PublishDiagnostics(state: State) =
|
||||
let PublishDiagnostics(state: State, projectOptions: FSharp.Compiler.SourceCodeServices.FSharpProjectOptions) =
|
||||
// TODO: honor TextDocumentClientCapabilities.publishDiagnostics.relatedInformation
|
||||
// cancel any existing request to publish diagnostics
|
||||
publishDiagnosticsCancellationTokenSource.Cancel()
|
||||
publishDiagnosticsCancellationTokenSource <- new CancellationTokenSource()
|
||||
async {
|
||||
return {
|
||||
PublishDiagnosticsParams.uri = ""
|
||||
diagnostics = [||]
|
||||
}
|
||||
if not state.Options.usePreviewDiagnostics then return ()
|
||||
else
|
||||
eprintfn "starting diagnostics computation"
|
||||
match state.JsonRpc with
|
||||
| None -> eprintfn "state.JsonRpc was null; should not be?"
|
||||
| Some jsonRpc ->
|
||||
let! results = state.Checker.ParseAndCheckProject projectOptions
|
||||
let diagnostics = results.Errors
|
||||
let diagnosticsPerFile =
|
||||
diagnostics
|
||||
|> Array.fold (fun state t ->
|
||||
let existing = Map.tryFind t.FileName state |> Option.defaultValue []
|
||||
Map.add t.FileName (t :: existing) state) Map.empty
|
||||
for sourceFile in projectOptions.SourceFiles do
|
||||
let diagnostics =
|
||||
Map.tryFind sourceFile diagnosticsPerFile
|
||||
|> Option.defaultValue []
|
||||
|> List.map (fun d ->
|
||||
// F# errors count lines starting at 1, but LSP starts at 0
|
||||
let range: Range =
|
||||
{ start = { line = d.StartLineAlternate - 1; character = d.StartColumn }
|
||||
``end`` = { line = d.EndLineAlternate - 1; character = d.EndColumn } }
|
||||
let severity =
|
||||
match d.Severity with
|
||||
| FSharp.Compiler.SourceCodeServices.FSharpErrorSeverity.Warning -> Diagnostic.Warning
|
||||
| FSharp.Compiler.SourceCodeServices.FSharpErrorSeverity.Error -> Diagnostic.Error
|
||||
let res: Diagnostic =
|
||||
{ range = range
|
||||
severity = Some severity
|
||||
code = "FS" + d.ErrorNumber.ToString("0000")
|
||||
source = Some d.FileName
|
||||
message = d.Message
|
||||
relatedInformation = None }
|
||||
res)
|
||||
|> List.toArray
|
||||
let args: PublishDiagnosticsParams =
|
||||
{ uri = System.Uri(sourceFile).AbsoluteUri
|
||||
diagnostics = diagnostics }
|
||||
|
||||
// fire each notification separately
|
||||
jsonRpc.NotifyAsync(TextDocumentPublishDiagnostics, args) |> Async.AwaitTask |> Async.Start
|
||||
}
|
||||
|> (fun computation -> Async.StartAsTask(computation, cancellationToken=publishDiagnosticsCancellationTokenSource.Token))
|
||||
|> Async.AwaitTask
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
|
||||
|
||||
namespace FSharp.Compiler.LanguageServer.UnitTests
|
||||
|
||||
open System
|
||||
open System.IO
|
||||
open System.Linq
|
||||
open System.Threading.Tasks
|
||||
open FSharp.Compiler.LanguageServer
|
||||
open Nerdbank.Streams
|
||||
open NUnit.Framework
|
||||
|
||||
[<TestFixture>]
|
||||
type DiagnosticsTests() =
|
||||
|
||||
let createTestableProject (tfm: string) (sourceFiles: (string * string) list) =
|
||||
let testDir = new TemporaryDirectory()
|
||||
let directoryBuildText = "<Project />"
|
||||
File.WriteAllText(Path.Combine(testDir.Directory, "Directory.Build.props"), directoryBuildText)
|
||||
File.WriteAllText(Path.Combine(testDir.Directory, "Directory.Build.targets"), directoryBuildText)
|
||||
for name, contents in sourceFiles do
|
||||
File.WriteAllText(Path.Combine(testDir.Directory, name), contents)
|
||||
let compileItems =
|
||||
sourceFiles
|
||||
|> List.map fst
|
||||
|> List.map (sprintf " <Compile Include=\"%s\" />")
|
||||
|> List.fold (fun content line -> content + "\n" + line) ""
|
||||
let replacements =
|
||||
[ "{{COMPILE}}", compileItems
|
||||
"{{TARGETFRAMEWORK}}", tfm ]
|
||||
let projectTemplate =
|
||||
@"
|
||||
<Project Sdk=""Microsoft.NET.Sdk"">
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>{{TARGETFRAMEWORK}}</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
{{COMPILE}}
|
||||
</ItemGroup>
|
||||
</Project>"
|
||||
let projectFile =
|
||||
replacements
|
||||
|> List.fold (fun (content: string) (find, replace) -> content.Replace(find, replace)) projectTemplate
|
||||
File.WriteAllText(Path.Combine(testDir.Directory, "test.fsproj"), projectFile)
|
||||
testDir
|
||||
|
||||
let createRpcClient (tempDir: TemporaryDirectory) =
|
||||
let clientStream, serverStream = FullDuplexStream.CreatePair().ToTuple()
|
||||
let server = new Server(serverStream, serverStream)
|
||||
server.StartListening()
|
||||
let client = new TestClient(tempDir, clientStream, clientStream, server)
|
||||
client
|
||||
|
||||
let createClientTest (tfm: string) (sourceFiles: (string * string) list) =
|
||||
let testDir = createTestableProject tfm sourceFiles
|
||||
let client = createRpcClient testDir
|
||||
client
|
||||
|
||||
let getDiagnostics (content: string) =
|
||||
async {
|
||||
use client = createClientTest "netstandard2.0" [ "lib.fs", content ]
|
||||
let! diagnostics = client.WaitForDiagnosticsAsync client.Initialize ["lib.fs"]
|
||||
return diagnostics.["lib.fs"]
|
||||
}
|
||||
|
||||
[<Test>]
|
||||
member __.``No diagnostics for correct code``() =
|
||||
async {
|
||||
let! diagnostics = getDiagnostics @"
|
||||
namespace Test
|
||||
|
||||
module Numbers =
|
||||
let one: int = 1
|
||||
"
|
||||
Assert.AreEqual(0, diagnostics.Length)
|
||||
} |> Async.StartAsTask :> Task
|
||||
|
||||
[<Test>]
|
||||
member __.``Diagnostics for incorrect code``() =
|
||||
async {
|
||||
let! diagnostics = getDiagnostics @"
|
||||
namespace Test
|
||||
|
||||
module Numbers =
|
||||
let one: int = false
|
||||
"
|
||||
let diag = diagnostics.Single()
|
||||
Assert.AreEqual("FS0001", diag.code)
|
||||
Assert.AreEqual(Some 1, diag.severity)
|
||||
Assert.AreEqual(4, diag.range.start.line)
|
||||
Assert.AreEqual(19, diag.range.start.character)
|
||||
Assert.AreEqual(4, diag.range.``end``.line)
|
||||
Assert.AreEqual(24, diag.range.``end``.character)
|
||||
Assert.AreEqual("This expression was expected to have type\n 'int' \nbut here has type\n 'bool'", diag.message.Trim())
|
||||
Assert.IsTrue(diag.source.Value.EndsWith("lib.fs"))
|
||||
} |> Async.StartAsTask :> Task
|
||||
|
||||
[<Test; Ignore("FileSystemWatcher isn't 100% reliable.")>]
|
||||
member __.``Diagnostics added for updated incorrect code``() =
|
||||
async {
|
||||
let correct = @"
|
||||
namespace Test
|
||||
|
||||
module Numbers =
|
||||
let one: int = 1
|
||||
"
|
||||
let incorrect = @"
|
||||
namespace Test
|
||||
|
||||
module Numbers =
|
||||
let one: int = false
|
||||
"
|
||||
|
||||
// verify initial state
|
||||
use client = createClientTest "netstandard2.0" [ "lib.fs", correct ]
|
||||
let! diagnostics = client.WaitForDiagnosticsAsync client.Initialize ["lib.fs"]
|
||||
Assert.AreEqual(0, diagnostics.["lib.fs"].Length)
|
||||
|
||||
// touch file with incorrect data
|
||||
let touch () = File.WriteAllText(Path.Combine(client.RootPath, "lib.fs"), incorrect)
|
||||
let! diagnostics = client.WaitForDiagnostics touch ["lib.fs"]
|
||||
let diag = diagnostics.["lib.fs"].Single()
|
||||
Assert.AreEqual("FS0001", diag.code)
|
||||
} |> Async.StartAsTask :> Task
|
||||
|
||||
[<Test; Ignore("FileSystemWatcher isn't 100% reliable.")>]
|
||||
member __.``Diagnostics removed for updated correct code``() =
|
||||
async {
|
||||
let incorrect = @"
|
||||
namespace Test
|
||||
|
||||
module Numbers =
|
||||
let one: int = false
|
||||
"
|
||||
let correct = @"
|
||||
namespace Test
|
||||
|
||||
module Numbers =
|
||||
let one: int = 1
|
||||
"
|
||||
|
||||
// verify initial state
|
||||
use client = createClientTest "netstandard2.0" [ "lib.fs", incorrect ]
|
||||
let! diagnostics = client.WaitForDiagnosticsAsync client.Initialize ["lib.fs"]
|
||||
let diag = diagnostics.["lib.fs"].Single()
|
||||
Assert.AreEqual("FS0001", diag.code)
|
||||
|
||||
// touch file with correct data
|
||||
let touch () = File.WriteAllText(Path.Combine(client.RootPath, "lib.fs"), correct)
|
||||
let! diagnostics = client.WaitForDiagnostics touch ["lib.fs"]
|
||||
let libActualContents = File.ReadAllText(Path.Combine(client.RootPath, "lib.fs"))
|
||||
Assert.AreEqual(0, diagnostics.["lib.fs"].Length, "Actual on-disk contents of lib.fs:\n" + libActualContents)
|
||||
} |> Async.StartAsTask :> Task
|
|
@ -11,8 +11,12 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="TemporaryDirectory.fs" />
|
||||
<Compile Include="TestClient.fs" />
|
||||
<Compile Include="ProtocolTests.fs" />
|
||||
<Compile Include="SerializationTests.fs" />
|
||||
<Compile Include="DiagnosticsTests.fs" />
|
||||
<Compile Include="MiscTests.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
|
||||
|
||||
namespace FSharp.Compiler.LanguageServer.UnitTests
|
||||
|
||||
open System.IO
|
||||
open FSharp.Compiler.LanguageServer
|
||||
open NUnit.Framework
|
||||
|
||||
[<TestFixture>]
|
||||
type MiscTests() =
|
||||
|
||||
[<Test>]
|
||||
member __.``Find F# projects in a .sln file``() =
|
||||
let slnContent = @"
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29201.188
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project(""{F2A71F9B-5D33-465A-A702-920D77279786}"") = ""ConsoleApp1"", ""ConsoleApp1\ConsoleApp1.fsproj"", ""{60A4BE67-7E03-4200-AD38-B0E5E8E049C1}""
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{60A4BE67-7E03-4200-AD38-B0E5E8E049C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{60A4BE67-7E03-4200-AD38-B0E5E8E049C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{60A4BE67-7E03-4200-AD38-B0E5E8E049C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{60A4BE67-7E03-4200-AD38-B0E5E8E049C1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {80902CFC-54E6-4485-AC17-4516930C8B2B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
"
|
||||
let testDir = @"C:\Dir\With\Solution" // don't care about the potentially improper directory separators here, it's really just a dumb string
|
||||
let foundProjects = Solution.getProjectPaths slnContent testDir
|
||||
let expected = Path.Combine(testDir, "ConsoleApp1", "ConsoleApp1.fsproj") // proper directory separator characters will be used at runtime
|
||||
Assert.AreEqual([| expected |], foundProjects)
|
|
@ -31,21 +31,17 @@ type ProtocolTests() =
|
|||
client.StartListening()
|
||||
|
||||
// initialize
|
||||
let capabilities =
|
||||
{ ClientCapabilities.workspace = None
|
||||
let capabilities: ClientCapabilities =
|
||||
{ workspace = None
|
||||
textDocument = None
|
||||
experimental = None
|
||||
supportsVisualStudioExtensions = None }
|
||||
let! result =
|
||||
client.InvokeAsync<InitializeResult>(
|
||||
"initialize", // method
|
||||
0, // processId
|
||||
"rootPath",
|
||||
"rootUri",
|
||||
null, // initializationOptions
|
||||
capabilities, // client capabilities
|
||||
"none") // trace
|
||||
|> Async.AwaitTask
|
||||
client.InvokeWithParameterObjectAsync<InitializeResult>(
|
||||
"initialize",
|
||||
{| processId = Process.GetCurrentProcess().Id
|
||||
capabilities = capabilities |}
|
||||
) |> Async.AwaitTask
|
||||
Assert.True(result.capabilities.hoverProvider)
|
||||
do! client.NotifyAsync("initialized") |> Async.AwaitTask
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
|
||||
|
||||
namespace FSharp.Compiler.LanguageServer.UnitTests
|
||||
|
||||
open System
|
||||
open System.IO
|
||||
|
||||
type TemporaryDirectory() =
|
||||
|
||||
let directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())
|
||||
do Directory.CreateDirectory(directory) |> ignore
|
||||
|
||||
member __.Directory = directory
|
||||
|
||||
interface IDisposable with
|
||||
member __.Dispose() =
|
||||
try
|
||||
Directory.Delete(directory, true)
|
||||
with
|
||||
| _ -> ()
|
|
@ -0,0 +1,121 @@
|
|||
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
|
||||
|
||||
namespace FSharp.Compiler.LanguageServer.UnitTests
|
||||
|
||||
open System
|
||||
open System.Collections.Generic
|
||||
open System.Diagnostics
|
||||
open System.IO
|
||||
open System.Threading
|
||||
open FSharp.Compiler.LanguageServer
|
||||
open Newtonsoft.Json.Linq
|
||||
open StreamJsonRpc
|
||||
|
||||
type TestClient(tempDir: TemporaryDirectory, sendingStream: Stream, receivingStream: Stream, server: Server) =
|
||||
|
||||
let rootPath = tempDir.Directory
|
||||
let rootPath = if rootPath.EndsWith(Path.DirectorySeparatorChar.ToString()) then rootPath else rootPath + Path.DirectorySeparatorChar.ToString()
|
||||
let diagnosticsEvent = Event<_>()
|
||||
|
||||
let formatter = JsonMessageFormatter()
|
||||
let converter = JsonOptionConverter() // special handler to convert between `Option<'T>` and `obj/null`.
|
||||
do formatter.JsonSerializer.Converters.Add(converter)
|
||||
let handler = new HeaderDelimitedMessageHandler(sendingStream, receivingStream, formatter)
|
||||
let client = new JsonRpc(handler)
|
||||
let handler (functionName: string) (args: JToken): JToken =
|
||||
match functionName with
|
||||
| TextDocumentPublishDiagnostics ->
|
||||
let args = args.ToObject<PublishDiagnosticsParams>(formatter.JsonSerializer)
|
||||
let fullPath = Uri(args.uri).LocalPath
|
||||
let shortPath = if fullPath.StartsWith(rootPath) then fullPath.Substring(rootPath.Length) else fullPath
|
||||
diagnosticsEvent.Trigger((shortPath, args.diagnostics))
|
||||
null
|
||||
| _ -> null
|
||||
let addHandler (name: string) =
|
||||
client.AddLocalRpcMethod(name, new Func<JToken, JToken>(handler name))
|
||||
do addHandler TextDocumentPublishDiagnostics
|
||||
do client.StartListening()
|
||||
|
||||
member __.RootPath = rootPath
|
||||
|
||||
member __.Server = server
|
||||
|
||||
[<CLIEvent>]
|
||||
member __.PublishDiagnostics = diagnosticsEvent.Publish
|
||||
|
||||
member __.Initialize () =
|
||||
async {
|
||||
do! client.NotifyWithParameterObjectAsync(OptionsSet, {| options = Options.AllOn() |}) |> Async.AwaitTask
|
||||
let capabilities: ClientCapabilities =
|
||||
{ workspace = None
|
||||
textDocument = None
|
||||
experimental = None
|
||||
supportsVisualStudioExtensions = None }
|
||||
let! _result =
|
||||
client.InvokeWithParameterObjectAsync<InitializeResult>(
|
||||
"initialize", // method
|
||||
{| processId = Process.GetCurrentProcess().Id
|
||||
rootPath = rootPath
|
||||
capabilities = capabilities |}
|
||||
) |> Async.AwaitTask
|
||||
return ()
|
||||
}
|
||||
|
||||
member this.WaitForDiagnostics (triggerAction: unit -> unit) (fileNames: string list) =
|
||||
async {
|
||||
// prepare file diagnostic triggers
|
||||
let diagnosticTriggers = Dictionary<string, ManualResetEvent>()
|
||||
fileNames |> List.iter (fun f -> diagnosticTriggers.[f] <- new ManualResetEvent(false))
|
||||
|
||||
// prepare callback handler
|
||||
let diagnosticsMap = Dictionary<string, Diagnostic[]>()
|
||||
let handler (fileName: string, diagnostics: Diagnostic[]) =
|
||||
diagnosticsMap.[fileName] <- diagnostics
|
||||
// auto-generated files (e.g., AssemblyInfo.fs) won't be in the trigger map
|
||||
if diagnosticTriggers.ContainsKey(fileName) then
|
||||
diagnosticTriggers.[fileName].Set() |> ignore
|
||||
|
||||
// subscribe to the event
|
||||
let wrappedHandler = new Handler<string * Diagnostic[]>(fun _sender args -> handler args)
|
||||
this.PublishDiagnostics.AddHandler(wrappedHandler)
|
||||
triggerAction ()
|
||||
|
||||
// wait for all triggers to hit
|
||||
let! results =
|
||||
diagnosticTriggers
|
||||
|> Seq.map (fun entry ->
|
||||
async {
|
||||
let! result = Async.AwaitWaitHandle(entry.Value, millisecondsTimeout = int (TimeSpan.FromSeconds(10.0).TotalMilliseconds))
|
||||
return if result then None
|
||||
else
|
||||
let filePath = Path.Combine(rootPath, entry.Key)
|
||||
let actualContents = File.ReadAllText(filePath)
|
||||
Some <| sprintf "No diagnostics received for file '%s'. Contents:\n%s\n" entry.Key actualContents
|
||||
})
|
||||
|> Async.Parallel
|
||||
let results = results |> Array.choose (fun x -> x)
|
||||
if results.Length > 0 then
|
||||
let combinedErrors = String.Join("-----\n", results)
|
||||
let allDiagnosticsEvents =
|
||||
diagnosticsMap
|
||||
|> Seq.map (fun entry ->
|
||||
sprintf "File '%s' reported %d diagnostics." entry.Key entry.Value.Length)
|
||||
|> (fun s -> String.Join("\n", s))
|
||||
failwith <| sprintf "Error waiting for diagnostics:\n%s\n-----\n%s" combinedErrors allDiagnosticsEvents
|
||||
|
||||
// clean up event
|
||||
this.PublishDiagnostics.RemoveHandler(wrappedHandler)
|
||||
|
||||
// done
|
||||
return diagnosticsMap
|
||||
}
|
||||
|
||||
member this.WaitForDiagnosticsAsync (triggerAction: unit -> Async<unit>) (fileNames: string list) =
|
||||
this.WaitForDiagnostics (fun () -> triggerAction () |> Async.RunSynchronously) fileNames
|
||||
|
||||
interface IDisposable with
|
||||
member __.Dispose() =
|
||||
try
|
||||
(tempDir :> IDisposable).Dispose()
|
||||
with
|
||||
| _ -> ()
|
|
@ -34,6 +34,9 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () =
|
|||
let getProjectInfoManager(document: Document) =
|
||||
document.Project.Solution.Workspace.Services.GetService<FSharpCheckerWorkspaceService>().FSharpProjectOptionsManager
|
||||
|
||||
let getSettings(document: Document) =
|
||||
document.Project.Solution.Workspace.Services.GetService<EditorOptions>()
|
||||
|
||||
static let errorInfoEqualityComparer =
|
||||
{ new IEqualityComparer<FSharpErrorInfo> with
|
||||
member __.Equals (x, y) =
|
||||
|
@ -110,6 +113,10 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () =
|
|||
interface IFSharpDocumentDiagnosticAnalyzer with
|
||||
|
||||
member this.AnalyzeSyntaxAsync(document: Document, cancellationToken: CancellationToken): Task<ImmutableArray<Diagnostic>> =
|
||||
// if using LSP, just bail early
|
||||
let settings = getSettings document
|
||||
if settings.Advanced.UsePreviewDiagnostics then Task.FromResult(ImmutableArray<Diagnostic>.Empty)
|
||||
else
|
||||
let projectInfoManager = getProjectInfoManager document
|
||||
asyncMaybe {
|
||||
let! parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document, cancellationToken)
|
||||
|
@ -123,6 +130,10 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () =
|
|||
|> RoslynHelpers.StartAsyncAsTask cancellationToken
|
||||
|
||||
member this.AnalyzeSemanticsAsync(document: Document, cancellationToken: CancellationToken): Task<ImmutableArray<Diagnostic>> =
|
||||
// if using LSP, just bail early
|
||||
let settings = getSettings document
|
||||
if settings.Advanced.UsePreviewDiagnostics then Task.FromResult(ImmutableArray<Diagnostic>.Empty)
|
||||
else
|
||||
let projectInfoManager = getProjectInfoManager document
|
||||
asyncMaybe {
|
||||
let! parsingOptions, _, projectOptions = projectInfoManager.TryGetOptionsForDocumentOrProject(document, cancellationToken)
|
||||
|
|
|
@ -34,7 +34,8 @@ type FSharpContentDefinition() =
|
|||
type internal FSharpLanguageClient
|
||||
[<ImportingConstructor>]
|
||||
(
|
||||
lspService: LspService
|
||||
lspService: LspService,
|
||||
settings: EditorOptions
|
||||
) =
|
||||
inherit LanguageClient()
|
||||
override __.Name = "F# Language Service"
|
||||
|
@ -63,4 +64,7 @@ type internal FSharpLanguageClient
|
|||
member __.CustomMessageTarget = null
|
||||
member __.MiddleLayer = null
|
||||
member __.AttachForCustomMessageAsync(rpc: JsonRpc) =
|
||||
lspService.SetJsonRpc(rpc) |> Async.StartAsTask :> Task
|
||||
async {
|
||||
do! lspService.SetJsonRpc(rpc)
|
||||
do! lspService.SetOptions(settings.Advanced.AsLspOptions())
|
||||
} |> Async.StartAsTask :> Task
|
||||
|
|
|
@ -93,11 +93,16 @@ type CodeLensOptions =
|
|||
type AdvancedOptions =
|
||||
{ IsBlockStructureEnabled: bool
|
||||
IsOutliningEnabled: bool
|
||||
UsePreviewTextHover: bool }
|
||||
UsePreviewTextHover: bool
|
||||
UsePreviewDiagnostics: bool }
|
||||
static member Default =
|
||||
{ IsBlockStructureEnabled = true
|
||||
IsOutliningEnabled = true
|
||||
UsePreviewTextHover = false }
|
||||
UsePreviewTextHover = false
|
||||
UsePreviewDiagnostics = false }
|
||||
member this.AsLspOptions(): Options =
|
||||
{ usePreviewTextHover = this.UsePreviewTextHover
|
||||
usePreviewDiagnostics = this.UsePreviewDiagnostics }
|
||||
|
||||
[<CLIMutable>]
|
||||
type FormattingOptions =
|
||||
|
@ -203,8 +208,7 @@ module internal OptionsUI =
|
|||
async {
|
||||
let lspService = this.GetService<LspService>()
|
||||
let settings = this.GetService<EditorOptions>()
|
||||
let options =
|
||||
{ Options.usePreviewTextHover = settings.Advanced.UsePreviewTextHover }
|
||||
let options = settings.Advanced.AsLspOptions()
|
||||
do! lspService.SetOptions options
|
||||
} |> Async.Start
|
||||
|
||||
|
|
|
@ -27,8 +27,12 @@
|
|||
<!-- this group box should be disabled for branch `release/dev16.2`, enabled otherwise -->
|
||||
<!--
|
||||
<GroupBox Header="{x:Static local:Strings.Use_out_of_process_language_server}">
|
||||
<CheckBox x:Name="usePreviewTextHover" IsChecked="{Binding UsePreviewTextHover}"
|
||||
Content="{x:Static local:Strings.Text_hover}" />
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="usePreviewTextHover" IsChecked="{Binding UsePreviewTextHover}"
|
||||
Content="{x:Static local:Strings.Text_hover}" />
|
||||
<CheckBox x:Name="usePreviewDiagnostics" IsChecked="{Binding UsePreviewDiagnostics}"
|
||||
Content="{x:Static local:Strings.Diagnostics}" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
-->
|
||||
</StackPanel>
|
||||
|
|
|
@ -150,6 +150,15 @@ namespace Microsoft.VisualStudio.FSharp.UIResources {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Diagnostics.
|
||||
/// </summary>
|
||||
public static string Diagnostics {
|
||||
get {
|
||||
return ResourceManager.GetString("Diagnostics", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to D_ot underline.
|
||||
/// </summary>
|
||||
|
|
|
@ -237,4 +237,7 @@
|
|||
<data name="Text_hover" xml:space="preserve">
|
||||
<value>Text hover</value>
|
||||
</data>
|
||||
<data name="Diagnostics" xml:space="preserve">
|
||||
<value>Diagnostics</value>
|
||||
</data>
|
||||
</root>
|
|
@ -42,6 +42,11 @@
|
|||
<target state="translated">Seznamy dokončení</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Diagnostics">
|
||||
<source>Diagnostics</source>
|
||||
<target state="new">Diagnostics</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Language_Service_Performance">
|
||||
<source>Performance</source>
|
||||
<target state="translated">Výkon</target>
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
<target state="translated">Vervollständigungslisten</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Diagnostics">
|
||||
<source>Diagnostics</source>
|
||||
<target state="new">Diagnostics</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Language_Service_Performance">
|
||||
<source>Performance</source>
|
||||
<target state="translated">Leistung</target>
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
<target state="translated">Listas de finalización</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Diagnostics">
|
||||
<source>Diagnostics</source>
|
||||
<target state="new">Diagnostics</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Language_Service_Performance">
|
||||
<source>Performance</source>
|
||||
<target state="translated">Rendimiento</target>
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
<target state="translated">Listes de saisie semi-automatique</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Diagnostics">
|
||||
<source>Diagnostics</source>
|
||||
<target state="new">Diagnostics</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Language_Service_Performance">
|
||||
<source>Performance</source>
|
||||
<target state="translated">Performances</target>
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
<target state="translated">Elenchi di completamento</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Diagnostics">
|
||||
<source>Diagnostics</source>
|
||||
<target state="new">Diagnostics</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Language_Service_Performance">
|
||||
<source>Performance</source>
|
||||
<target state="translated">Prestazioni</target>
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
<target state="translated">入力候補一覧</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Diagnostics">
|
||||
<source>Diagnostics</source>
|
||||
<target state="new">Diagnostics</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Language_Service_Performance">
|
||||
<source>Performance</source>
|
||||
<target state="translated">パフォーマンス</target>
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
<target state="translated">완성 목록</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Diagnostics">
|
||||
<source>Diagnostics</source>
|
||||
<target state="new">Diagnostics</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Language_Service_Performance">
|
||||
<source>Performance</source>
|
||||
<target state="translated">성능</target>
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
<target state="translated">Listy uzupełniania</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Diagnostics">
|
||||
<source>Diagnostics</source>
|
||||
<target state="new">Diagnostics</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Language_Service_Performance">
|
||||
<source>Performance</source>
|
||||
<target state="translated">Wydajność</target>
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
<target state="translated">Listas de Conclusão</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Diagnostics">
|
||||
<source>Diagnostics</source>
|
||||
<target state="new">Diagnostics</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Language_Service_Performance">
|
||||
<source>Performance</source>
|
||||
<target state="translated">Desempenho</target>
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
<target state="translated">Списки завершения</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Diagnostics">
|
||||
<source>Diagnostics</source>
|
||||
<target state="new">Diagnostics</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Language_Service_Performance">
|
||||
<source>Performance</source>
|
||||
<target state="translated">Производительность</target>
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
<target state="translated">Tamamlama Listeleri</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Diagnostics">
|
||||
<source>Diagnostics</source>
|
||||
<target state="new">Diagnostics</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Language_Service_Performance">
|
||||
<source>Performance</source>
|
||||
<target state="translated">Performans</target>
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
<target state="translated">完成列表</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Diagnostics">
|
||||
<source>Diagnostics</source>
|
||||
<target state="new">Diagnostics</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Language_Service_Performance">
|
||||
<source>Performance</source>
|
||||
<target state="translated">性能</target>
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
<target state="translated">完成清單</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Diagnostics">
|
||||
<source>Diagnostics</source>
|
||||
<target state="new">Diagnostics</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="Language_Service_Performance">
|
||||
<source>Performance</source>
|
||||
<target state="translated">效能</target>
|
||||
|
|
Загрузка…
Ссылка в новой задаче