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" />
|
<Compile Include="Program.fs" />
|
||||||
</ItemGroup>
|
</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>
|
<ItemGroup>
|
||||||
<ProjectReference Include="$(MSBuildThisFileDirectory)..\FSharp.Compiler.Private\FSharp.Compiler.Private.fsproj" />
|
<ProjectReference Include="$(MSBuildThisFileDirectory)..\FSharp.Compiler.Private\FSharp.Compiler.Private.fsproj" />
|
||||||
<ProjectReference Include="$(MSBuildThisFileDirectory)..\FSharp.Core\FSharp.Core.fsproj" />
|
<ProjectReference Include="$(MSBuildThisFileDirectory)..\FSharp.Core\FSharp.Core.fsproj" />
|
||||||
|
|
|
@ -9,10 +9,18 @@ module FunctionNames =
|
||||||
[<Literal>]
|
[<Literal>]
|
||||||
let OptionsSet = "options/set"
|
let OptionsSet = "options/set"
|
||||||
|
|
||||||
|
[<Literal>]
|
||||||
|
let TextDocumentPublishDiagnostics = "textDocument/publishDiagnostics"
|
||||||
|
|
||||||
type Options =
|
type Options =
|
||||||
{ usePreviewTextHover: bool }
|
{ usePreviewTextHover: bool
|
||||||
|
usePreviewDiagnostics: bool }
|
||||||
static member Default() =
|
static member Default() =
|
||||||
{ usePreviewTextHover = false }
|
{ usePreviewTextHover = false
|
||||||
|
usePreviewDiagnostics = false }
|
||||||
|
static member AllOn() =
|
||||||
|
{ usePreviewTextHover = true
|
||||||
|
usePreviewDiagnostics = true }
|
||||||
|
|
||||||
module Extensions =
|
module Extensions =
|
||||||
type JsonRpc with
|
type JsonRpc with
|
||||||
|
|
|
@ -5,8 +5,9 @@ namespace FSharp.Compiler.LanguageServer
|
||||||
open Newtonsoft.Json.Linq
|
open Newtonsoft.Json.Linq
|
||||||
open Newtonsoft.Json
|
open Newtonsoft.Json
|
||||||
|
|
||||||
// Interfaces as defined at https://microsoft.github.io/language-server-protocol/specification. The properties on
|
// Interfaces as defined at https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/.
|
||||||
// these types are camlCased to match the underlying JSON properties to avoid attributes on every field:
|
// The properties on these types are camlCased to match the underlying JSON properties to avoid attributes on every
|
||||||
|
// field:
|
||||||
// [<JsonProperty("camlCased")>]
|
// [<JsonProperty("camlCased")>]
|
||||||
|
|
||||||
/// Represents a zero-based line and column of a text document.
|
/// Represents a zero-based line and column of a text document.
|
||||||
|
@ -32,7 +33,7 @@ type Diagnostic =
|
||||||
{ range: Range
|
{ range: Range
|
||||||
severity: int option
|
severity: int option
|
||||||
code: string
|
code: string
|
||||||
source: string option // "F#"
|
source: string option
|
||||||
message: string
|
message: string
|
||||||
relatedInformation: DiagnosticRelatedInformation[] option }
|
relatedInformation: DiagnosticRelatedInformation[] option }
|
||||||
static member Error = 1
|
static member Error = 1
|
||||||
|
@ -46,7 +47,7 @@ type PublishDiagnosticsParams =
|
||||||
|
|
||||||
type ClientCapabilities =
|
type ClientCapabilities =
|
||||||
{ workspace: JToken option // TODO: WorkspaceClientCapabilities
|
{ workspace: JToken option // TODO: WorkspaceClientCapabilities
|
||||||
textDocument: JToken option // TODO: TextDocumentCapabilities
|
textDocument: JToken option // TODO: TextDocumentClientCapabilities, publishDiagnostics: { relatedInformation: bool option }
|
||||||
experimental: JToken option
|
experimental: JToken option
|
||||||
supportsVisualStudioExtensions: bool option }
|
supportsVisualStudioExtensions: bool option }
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,10 @@ open System.Threading
|
||||||
open Newtonsoft.Json.Linq
|
open Newtonsoft.Json.Linq
|
||||||
open StreamJsonRpc
|
open StreamJsonRpc
|
||||||
|
|
||||||
// https://microsoft.github.io/language-server-protocol/specification
|
// https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/
|
||||||
type Methods(state: State) =
|
type Methods() =
|
||||||
|
|
||||||
|
let state = State()
|
||||||
|
|
||||||
/// Helper to run Async<'T> with a CancellationToken.
|
/// Helper to run Async<'T> with a CancellationToken.
|
||||||
let runAsync (cancellationToken: CancellationToken) (computation: Async<'T>) = Async.StartAsTask(computation, cancellationToken=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,
|
[<Optional; DefaultParameterValue(null: JToken)>] initializationOptions: JToken,
|
||||||
capabilities: ClientCapabilities,
|
capabilities: ClientCapabilities,
|
||||||
[<Optional; DefaultParameterValue(null: string)>] trace: string,
|
[<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() }
|
{ InitializeResult.capabilities = ServerCapabilities.DefaultCapabilities() }
|
||||||
|
|
||||||
[<JsonRpcMethod("initialized")>]
|
[<JsonRpcMethod("initialized")>]
|
||||||
|
@ -63,5 +67,6 @@ type Methods(state: State) =
|
||||||
(
|
(
|
||||||
options: Options
|
options: Options
|
||||||
) =
|
) =
|
||||||
sprintf "got options %A" options |> Console.Error.WriteLine
|
eprintfn "got options %A" options
|
||||||
state.Options <- 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`.
|
let converter = JsonOptionConverter() // special handler to convert between `Option<'T>` and `obj/null`.
|
||||||
do formatter.JsonSerializer.Converters.Add(converter)
|
do formatter.JsonSerializer.Converters.Add(converter)
|
||||||
let handler = new HeaderDelimitedMessageHandler(sendingStream, receivingStream, formatter)
|
let handler = new HeaderDelimitedMessageHandler(sendingStream, receivingStream, formatter)
|
||||||
let state = State()
|
let methods = Methods()
|
||||||
let methods = Methods(state)
|
|
||||||
let rpc = new JsonRpc(handler, methods)
|
let rpc = new JsonRpc(handler, methods)
|
||||||
|
do methods.State.JsonRpc <- Some rpc
|
||||||
|
|
||||||
member __.StartListening() =
|
member __.StartListening() =
|
||||||
rpc.StartListening()
|
rpc.StartListening()
|
||||||
|
|
||||||
member __.WaitForExitAsync() =
|
member __.WaitForExitAsync() =
|
||||||
async {
|
async {
|
||||||
do! Async.AwaitEvent (state.Shutdown)
|
do! Async.AwaitEvent (methods.State.Shutdown)
|
||||||
do! Async.AwaitEvent (state.Exit)
|
do! Async.AwaitEvent (methods.State.Exit)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IDisposable with
|
interface IDisposable with
|
||||||
|
|
|
@ -2,11 +2,209 @@
|
||||||
|
|
||||||
namespace FSharp.Compiler.LanguageServer
|
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() =
|
type State() =
|
||||||
|
|
||||||
|
let checker = FSharpChecker.Create()
|
||||||
|
|
||||||
|
let sourceFileToProjectMap = ConcurrentDictionary<string, FSharpProjectOptions>()
|
||||||
|
|
||||||
let shutdownEvent = new Event<_>()
|
let shutdownEvent = new Event<_>()
|
||||||
let exitEvent = new Event<_>()
|
let exitEvent = new Event<_>()
|
||||||
let cancelEvent = 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>]
|
[<CLIEvent>]
|
||||||
member __.Shutdown = shutdownEvent.Publish
|
member __.Shutdown = shutdownEvent.Publish
|
||||||
|
@ -17,10 +215,19 @@ type State() =
|
||||||
[<CLIEvent>]
|
[<CLIEvent>]
|
||||||
member __.Cancel = cancelEvent.Publish
|
member __.Cancel = cancelEvent.Publish
|
||||||
|
|
||||||
|
[<CLIEvent>]
|
||||||
|
member __.ProjectInvalidated = projectInvalidatedEvent.Publish
|
||||||
|
|
||||||
member __.DoShutdown() = shutdownEvent.Trigger()
|
member __.DoShutdown() = shutdownEvent.Trigger()
|
||||||
|
|
||||||
member __.DoExit() = exitEvent.Trigger()
|
member __.DoExit() = exitEvent.Trigger()
|
||||||
|
|
||||||
member __.DoCancel() = cancelEvent.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 Options = Options.Default() with get, set
|
||||||
|
|
||||||
|
member val JsonRpc: JsonRpc option = None with get, set
|
||||||
|
|
|
@ -2,13 +2,15 @@
|
||||||
|
|
||||||
namespace FSharp.Compiler.LanguageServer
|
namespace FSharp.Compiler.LanguageServer
|
||||||
|
|
||||||
open System
|
open System.Threading
|
||||||
|
|
||||||
module TextDocument =
|
module TextDocument =
|
||||||
|
|
||||||
|
let mutable publishDiagnosticsCancellationTokenSource = new CancellationTokenSource()
|
||||||
|
|
||||||
let Hover (state: State) (textDocument: TextDocumentIdentifier) (position: Position) =
|
let Hover (state: State) (textDocument: TextDocumentIdentifier) (position: Position) =
|
||||||
async {
|
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
|
if not state.Options.usePreviewTextHover then return None
|
||||||
else
|
else
|
||||||
let startCol, endCol =
|
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 {
|
async {
|
||||||
return {
|
if not state.Options.usePreviewDiagnostics then return ()
|
||||||
PublishDiagnosticsParams.uri = ""
|
else
|
||||||
diagnostics = [||]
|
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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="TemporaryDirectory.fs" />
|
||||||
|
<Compile Include="TestClient.fs" />
|
||||||
<Compile Include="ProtocolTests.fs" />
|
<Compile Include="ProtocolTests.fs" />
|
||||||
<Compile Include="SerializationTests.fs" />
|
<Compile Include="SerializationTests.fs" />
|
||||||
|
<Compile Include="DiagnosticsTests.fs" />
|
||||||
|
<Compile Include="MiscTests.fs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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()
|
client.StartListening()
|
||||||
|
|
||||||
// initialize
|
// initialize
|
||||||
let capabilities =
|
let capabilities: ClientCapabilities =
|
||||||
{ ClientCapabilities.workspace = None
|
{ workspace = None
|
||||||
textDocument = None
|
textDocument = None
|
||||||
experimental = None
|
experimental = None
|
||||||
supportsVisualStudioExtensions = None }
|
supportsVisualStudioExtensions = None }
|
||||||
let! result =
|
let! result =
|
||||||
client.InvokeAsync<InitializeResult>(
|
client.InvokeWithParameterObjectAsync<InitializeResult>(
|
||||||
"initialize", // method
|
"initialize",
|
||||||
0, // processId
|
{| processId = Process.GetCurrentProcess().Id
|
||||||
"rootPath",
|
capabilities = capabilities |}
|
||||||
"rootUri",
|
) |> Async.AwaitTask
|
||||||
null, // initializationOptions
|
|
||||||
capabilities, // client capabilities
|
|
||||||
"none") // trace
|
|
||||||
|> Async.AwaitTask
|
|
||||||
Assert.True(result.capabilities.hoverProvider)
|
Assert.True(result.capabilities.hoverProvider)
|
||||||
do! client.NotifyAsync("initialized") |> Async.AwaitTask
|
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) =
|
let getProjectInfoManager(document: Document) =
|
||||||
document.Project.Solution.Workspace.Services.GetService<FSharpCheckerWorkspaceService>().FSharpProjectOptionsManager
|
document.Project.Solution.Workspace.Services.GetService<FSharpCheckerWorkspaceService>().FSharpProjectOptionsManager
|
||||||
|
|
||||||
|
let getSettings(document: Document) =
|
||||||
|
document.Project.Solution.Workspace.Services.GetService<EditorOptions>()
|
||||||
|
|
||||||
static let errorInfoEqualityComparer =
|
static let errorInfoEqualityComparer =
|
||||||
{ new IEqualityComparer<FSharpErrorInfo> with
|
{ new IEqualityComparer<FSharpErrorInfo> with
|
||||||
member __.Equals (x, y) =
|
member __.Equals (x, y) =
|
||||||
|
@ -110,6 +113,10 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () =
|
||||||
interface IFSharpDocumentDiagnosticAnalyzer with
|
interface IFSharpDocumentDiagnosticAnalyzer with
|
||||||
|
|
||||||
member this.AnalyzeSyntaxAsync(document: Document, cancellationToken: CancellationToken): Task<ImmutableArray<Diagnostic>> =
|
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
|
let projectInfoManager = getProjectInfoManager document
|
||||||
asyncMaybe {
|
asyncMaybe {
|
||||||
let! parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document, cancellationToken)
|
let! parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document, cancellationToken)
|
||||||
|
@ -123,6 +130,10 @@ type internal FSharpDocumentDiagnosticAnalyzer [<ImportingConstructor>] () =
|
||||||
|> RoslynHelpers.StartAsyncAsTask cancellationToken
|
|> RoslynHelpers.StartAsyncAsTask cancellationToken
|
||||||
|
|
||||||
member this.AnalyzeSemanticsAsync(document: Document, cancellationToken: CancellationToken): Task<ImmutableArray<Diagnostic>> =
|
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
|
let projectInfoManager = getProjectInfoManager document
|
||||||
asyncMaybe {
|
asyncMaybe {
|
||||||
let! parsingOptions, _, projectOptions = projectInfoManager.TryGetOptionsForDocumentOrProject(document, cancellationToken)
|
let! parsingOptions, _, projectOptions = projectInfoManager.TryGetOptionsForDocumentOrProject(document, cancellationToken)
|
||||||
|
|
|
@ -34,7 +34,8 @@ type FSharpContentDefinition() =
|
||||||
type internal FSharpLanguageClient
|
type internal FSharpLanguageClient
|
||||||
[<ImportingConstructor>]
|
[<ImportingConstructor>]
|
||||||
(
|
(
|
||||||
lspService: LspService
|
lspService: LspService,
|
||||||
|
settings: EditorOptions
|
||||||
) =
|
) =
|
||||||
inherit LanguageClient()
|
inherit LanguageClient()
|
||||||
override __.Name = "F# Language Service"
|
override __.Name = "F# Language Service"
|
||||||
|
@ -63,4 +64,7 @@ type internal FSharpLanguageClient
|
||||||
member __.CustomMessageTarget = null
|
member __.CustomMessageTarget = null
|
||||||
member __.MiddleLayer = null
|
member __.MiddleLayer = null
|
||||||
member __.AttachForCustomMessageAsync(rpc: JsonRpc) =
|
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 =
|
type AdvancedOptions =
|
||||||
{ IsBlockStructureEnabled: bool
|
{ IsBlockStructureEnabled: bool
|
||||||
IsOutliningEnabled: bool
|
IsOutliningEnabled: bool
|
||||||
UsePreviewTextHover: bool }
|
UsePreviewTextHover: bool
|
||||||
|
UsePreviewDiagnostics: bool }
|
||||||
static member Default =
|
static member Default =
|
||||||
{ IsBlockStructureEnabled = true
|
{ IsBlockStructureEnabled = true
|
||||||
IsOutliningEnabled = true
|
IsOutliningEnabled = true
|
||||||
UsePreviewTextHover = false }
|
UsePreviewTextHover = false
|
||||||
|
UsePreviewDiagnostics = false }
|
||||||
|
member this.AsLspOptions(): Options =
|
||||||
|
{ usePreviewTextHover = this.UsePreviewTextHover
|
||||||
|
usePreviewDiagnostics = this.UsePreviewDiagnostics }
|
||||||
|
|
||||||
[<CLIMutable>]
|
[<CLIMutable>]
|
||||||
type FormattingOptions =
|
type FormattingOptions =
|
||||||
|
@ -203,8 +208,7 @@ module internal OptionsUI =
|
||||||
async {
|
async {
|
||||||
let lspService = this.GetService<LspService>()
|
let lspService = this.GetService<LspService>()
|
||||||
let settings = this.GetService<EditorOptions>()
|
let settings = this.GetService<EditorOptions>()
|
||||||
let options =
|
let options = settings.Advanced.AsLspOptions()
|
||||||
{ Options.usePreviewTextHover = settings.Advanced.UsePreviewTextHover }
|
|
||||||
do! lspService.SetOptions options
|
do! lspService.SetOptions options
|
||||||
} |> Async.Start
|
} |> Async.Start
|
||||||
|
|
||||||
|
|
|
@ -27,8 +27,12 @@
|
||||||
<!-- this group box should be disabled for branch `release/dev16.2`, enabled otherwise -->
|
<!-- 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}">
|
<GroupBox Header="{x:Static local:Strings.Use_out_of_process_language_server}">
|
||||||
|
<StackPanel>
|
||||||
<CheckBox x:Name="usePreviewTextHover" IsChecked="{Binding UsePreviewTextHover}"
|
<CheckBox x:Name="usePreviewTextHover" IsChecked="{Binding UsePreviewTextHover}"
|
||||||
Content="{x:Static local:Strings.Text_hover}" />
|
Content="{x:Static local:Strings.Text_hover}" />
|
||||||
|
<CheckBox x:Name="usePreviewDiagnostics" IsChecked="{Binding UsePreviewDiagnostics}"
|
||||||
|
Content="{x:Static local:Strings.Diagnostics}" />
|
||||||
|
</StackPanel>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
-->
|
-->
|
||||||
</StackPanel>
|
</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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to D_ot underline.
|
/// Looks up a localized string similar to D_ot underline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -237,4 +237,7 @@
|
||||||
<data name="Text_hover" xml:space="preserve">
|
<data name="Text_hover" xml:space="preserve">
|
||||||
<value>Text hover</value>
|
<value>Text hover</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Diagnostics" xml:space="preserve">
|
||||||
|
<value>Diagnostics</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
|
@ -42,6 +42,11 @@
|
||||||
<target state="translated">Seznamy dokončení</target>
|
<target state="translated">Seznamy dokončení</target>
|
||||||
<note />
|
<note />
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Diagnostics">
|
||||||
|
<source>Diagnostics</source>
|
||||||
|
<target state="new">Diagnostics</target>
|
||||||
|
<note />
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Language_Service_Performance">
|
<trans-unit id="Language_Service_Performance">
|
||||||
<source>Performance</source>
|
<source>Performance</source>
|
||||||
<target state="translated">Výkon</target>
|
<target state="translated">Výkon</target>
|
||||||
|
|
|
@ -42,6 +42,11 @@
|
||||||
<target state="translated">Vervollständigungslisten</target>
|
<target state="translated">Vervollständigungslisten</target>
|
||||||
<note />
|
<note />
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Diagnostics">
|
||||||
|
<source>Diagnostics</source>
|
||||||
|
<target state="new">Diagnostics</target>
|
||||||
|
<note />
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Language_Service_Performance">
|
<trans-unit id="Language_Service_Performance">
|
||||||
<source>Performance</source>
|
<source>Performance</source>
|
||||||
<target state="translated">Leistung</target>
|
<target state="translated">Leistung</target>
|
||||||
|
|
|
@ -42,6 +42,11 @@
|
||||||
<target state="translated">Listas de finalización</target>
|
<target state="translated">Listas de finalización</target>
|
||||||
<note />
|
<note />
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Diagnostics">
|
||||||
|
<source>Diagnostics</source>
|
||||||
|
<target state="new">Diagnostics</target>
|
||||||
|
<note />
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Language_Service_Performance">
|
<trans-unit id="Language_Service_Performance">
|
||||||
<source>Performance</source>
|
<source>Performance</source>
|
||||||
<target state="translated">Rendimiento</target>
|
<target state="translated">Rendimiento</target>
|
||||||
|
|
|
@ -42,6 +42,11 @@
|
||||||
<target state="translated">Listes de saisie semi-automatique</target>
|
<target state="translated">Listes de saisie semi-automatique</target>
|
||||||
<note />
|
<note />
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Diagnostics">
|
||||||
|
<source>Diagnostics</source>
|
||||||
|
<target state="new">Diagnostics</target>
|
||||||
|
<note />
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Language_Service_Performance">
|
<trans-unit id="Language_Service_Performance">
|
||||||
<source>Performance</source>
|
<source>Performance</source>
|
||||||
<target state="translated">Performances</target>
|
<target state="translated">Performances</target>
|
||||||
|
|
|
@ -42,6 +42,11 @@
|
||||||
<target state="translated">Elenchi di completamento</target>
|
<target state="translated">Elenchi di completamento</target>
|
||||||
<note />
|
<note />
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Diagnostics">
|
||||||
|
<source>Diagnostics</source>
|
||||||
|
<target state="new">Diagnostics</target>
|
||||||
|
<note />
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Language_Service_Performance">
|
<trans-unit id="Language_Service_Performance">
|
||||||
<source>Performance</source>
|
<source>Performance</source>
|
||||||
<target state="translated">Prestazioni</target>
|
<target state="translated">Prestazioni</target>
|
||||||
|
|
|
@ -42,6 +42,11 @@
|
||||||
<target state="translated">入力候補一覧</target>
|
<target state="translated">入力候補一覧</target>
|
||||||
<note />
|
<note />
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Diagnostics">
|
||||||
|
<source>Diagnostics</source>
|
||||||
|
<target state="new">Diagnostics</target>
|
||||||
|
<note />
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Language_Service_Performance">
|
<trans-unit id="Language_Service_Performance">
|
||||||
<source>Performance</source>
|
<source>Performance</source>
|
||||||
<target state="translated">パフォーマンス</target>
|
<target state="translated">パフォーマンス</target>
|
||||||
|
|
|
@ -42,6 +42,11 @@
|
||||||
<target state="translated">완성 목록</target>
|
<target state="translated">완성 목록</target>
|
||||||
<note />
|
<note />
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Diagnostics">
|
||||||
|
<source>Diagnostics</source>
|
||||||
|
<target state="new">Diagnostics</target>
|
||||||
|
<note />
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Language_Service_Performance">
|
<trans-unit id="Language_Service_Performance">
|
||||||
<source>Performance</source>
|
<source>Performance</source>
|
||||||
<target state="translated">성능</target>
|
<target state="translated">성능</target>
|
||||||
|
|
|
@ -42,6 +42,11 @@
|
||||||
<target state="translated">Listy uzupełniania</target>
|
<target state="translated">Listy uzupełniania</target>
|
||||||
<note />
|
<note />
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Diagnostics">
|
||||||
|
<source>Diagnostics</source>
|
||||||
|
<target state="new">Diagnostics</target>
|
||||||
|
<note />
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Language_Service_Performance">
|
<trans-unit id="Language_Service_Performance">
|
||||||
<source>Performance</source>
|
<source>Performance</source>
|
||||||
<target state="translated">Wydajność</target>
|
<target state="translated">Wydajność</target>
|
||||||
|
|
|
@ -42,6 +42,11 @@
|
||||||
<target state="translated">Listas de Conclusão</target>
|
<target state="translated">Listas de Conclusão</target>
|
||||||
<note />
|
<note />
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Diagnostics">
|
||||||
|
<source>Diagnostics</source>
|
||||||
|
<target state="new">Diagnostics</target>
|
||||||
|
<note />
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Language_Service_Performance">
|
<trans-unit id="Language_Service_Performance">
|
||||||
<source>Performance</source>
|
<source>Performance</source>
|
||||||
<target state="translated">Desempenho</target>
|
<target state="translated">Desempenho</target>
|
||||||
|
|
|
@ -42,6 +42,11 @@
|
||||||
<target state="translated">Списки завершения</target>
|
<target state="translated">Списки завершения</target>
|
||||||
<note />
|
<note />
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Diagnostics">
|
||||||
|
<source>Diagnostics</source>
|
||||||
|
<target state="new">Diagnostics</target>
|
||||||
|
<note />
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Language_Service_Performance">
|
<trans-unit id="Language_Service_Performance">
|
||||||
<source>Performance</source>
|
<source>Performance</source>
|
||||||
<target state="translated">Производительность</target>
|
<target state="translated">Производительность</target>
|
||||||
|
|
|
@ -42,6 +42,11 @@
|
||||||
<target state="translated">Tamamlama Listeleri</target>
|
<target state="translated">Tamamlama Listeleri</target>
|
||||||
<note />
|
<note />
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Diagnostics">
|
||||||
|
<source>Diagnostics</source>
|
||||||
|
<target state="new">Diagnostics</target>
|
||||||
|
<note />
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Language_Service_Performance">
|
<trans-unit id="Language_Service_Performance">
|
||||||
<source>Performance</source>
|
<source>Performance</source>
|
||||||
<target state="translated">Performans</target>
|
<target state="translated">Performans</target>
|
||||||
|
|
|
@ -42,6 +42,11 @@
|
||||||
<target state="translated">完成列表</target>
|
<target state="translated">完成列表</target>
|
||||||
<note />
|
<note />
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Diagnostics">
|
||||||
|
<source>Diagnostics</source>
|
||||||
|
<target state="new">Diagnostics</target>
|
||||||
|
<note />
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Language_Service_Performance">
|
<trans-unit id="Language_Service_Performance">
|
||||||
<source>Performance</source>
|
<source>Performance</source>
|
||||||
<target state="translated">性能</target>
|
<target state="translated">性能</target>
|
||||||
|
|
|
@ -42,6 +42,11 @@
|
||||||
<target state="translated">完成清單</target>
|
<target state="translated">完成清單</target>
|
||||||
<note />
|
<note />
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Diagnostics">
|
||||||
|
<source>Diagnostics</source>
|
||||||
|
<target state="new">Diagnostics</target>
|
||||||
|
<note />
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Language_Service_Performance">
|
<trans-unit id="Language_Service_Performance">
|
||||||
<source>Performance</source>
|
<source>Performance</source>
|
||||||
<target state="translated">效能</target>
|
<target state="translated">效能</target>
|
||||||
|
|
Загрузка…
Ссылка в новой задаче