Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/CSharpLanguageServer/CSharpLanguageServer.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
<ChangelogFile>CHANGELOG.md</ChangelogFile>
<Nullable>enable</Nullable>
<MSBuildTreatWarningsAsErrors>true</MSBuildTreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
Expand Down
170 changes: 141 additions & 29 deletions src/CSharpLanguageServer/Handlers/Completion.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@ namespace CSharpLanguageServer.Handlers
open System
open System.Reflection

open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.Text
open Microsoft.Extensions.Caching.Memory
open Ionide.LanguageServerProtocol.Server
open Ionide.LanguageServerProtocol.Types
open Ionide.LanguageServerProtocol.JsonRpc
open Microsoft.Extensions.Logging

open CSharpLanguageServer.State
open CSharpLanguageServer.Util
open CSharpLanguageServer.Roslyn.Conversions
open CSharpLanguageServer.Roslyn.Solution
open CSharpLanguageServer.Logging
open CSharpLanguageServer.Lsp.Workspace


[<RequireQualifiedAccess>]
module Completion =
let private _logger = Logging.getLoggerByName "Completion"
let private logger = Logging.getLoggerByName "Completion"

let private completionItemMemoryCache = new MemoryCache(new MemoryCacheOptions())

Expand Down Expand Up @@ -181,16 +186,118 @@ module Completion =
synopsis, documentationText
| _, _ -> None, None

let handle
let codeActionContextToCompletionTrigger (context: CompletionContext option) =
context
|> Option.bind (fun ctx ->
match ctx.TriggerKind with
| CompletionTriggerKind.Invoked
| CompletionTriggerKind.TriggerForIncompleteCompletions ->
Some Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke
| CompletionTriggerKind.TriggerCharacter ->
ctx.TriggerCharacter
|> Option.map Seq.head
|> Option.map Microsoft.CodeAnalysis.Completion.CompletionTrigger.CreateInsertionTrigger
| _ -> None)
|> Option.defaultValue Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke

let getCompletionsForRazorDocument
(p: CompletionParams)
(context: ServerRequestContext)
: Async<option<Microsoft.CodeAnalysis.Completion.CompletionList * Document>> =
async {
let wf = context.Workspace.SingletonFolder

match! solutionGetRazorDocumentForUri wf.Solution.Value p.TextDocument.Uri with
| None -> return None
| Some(project, compilation, cshtmlTree) ->
let! ct = Async.CancellationToken
let! sourceText = cshtmlTree.GetTextAsync() |> Async.AwaitTask

let razorTextDocument =
wf.Solution.Value
|> _.Projects
|> Seq.collect (fun p -> p.AdditionalDocuments)
|> Seq.filter (fun d -> Uri(d.FilePath, UriKind.Absolute) = Uri p.TextDocument.Uri)
|> Seq.head

let! razorSourceText = razorTextDocument.GetTextAsync() |> Async.AwaitTask

let posInCshtml = Position.toRoslynPosition sourceText.Lines p.Position
//logger.LogInformation("posInCshtml={posInCshtml=}", posInCshtml)
let pos = p.Position

let root = cshtmlTree.GetRoot()

let mutable positionAndToken: (int * SyntaxToken) option = None

for t in root.DescendantTokens() do
let cshtmlSpan = cshtmlTree.GetMappedLineSpan(t.Span)

if
cshtmlSpan.StartLinePosition.Line = (int pos.Line)
&& cshtmlSpan.EndLinePosition.Line = (int pos.Line)
&& cshtmlSpan.StartLinePosition.Character <= (int pos.Character)
then
let tokenStartCharacterOffset =
(int pos.Character - cshtmlSpan.StartLinePosition.Character)

positionAndToken <- Some(t.Span.Start + tokenStartCharacterOffset, t)

match positionAndToken with
| None -> return None
| Some(position, tokenForPosition) ->

let newSourceText =
let cshtmlPosition = Position.toRoslynPosition razorSourceText.Lines p.Position
let charInCshtml: char = razorSourceText[cshtmlPosition - 1]

if charInCshtml = '.' && string tokenForPosition.Value <> "." then
// a hack to make <span>@Model.|</span> autocompletion to work:
// - force a dot if present on .cscshtml but missing on .cs
sourceText.WithChanges(new TextChange(new TextSpan(position - 1, 0), "."))
else
sourceText

let cshtmlPath = Uri.toPath p.TextDocument.Uri
let! doc = solutionTryAddDocument (cshtmlPath + ".cs") (newSourceText.ToString()) wf.Solution.Value

match doc with
| None -> return None
| Some doc ->
let completionService =
Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc)
|> RoslynCompletionServiceWrapper

let completionOptions =
RoslynCompletionOptions.Default()
|> _.WithBool("ShowItemsFromUnimportedNamespaces", false)
|> _.WithBool("ShowNameSuggestions", false)

let completionTrigger = p.Context |> codeActionContextToCompletionTrigger

let! roslynCompletions =
completionService.GetCompletionsAsync(
doc,
position,
completionOptions,
completionTrigger,
ct
)
|> Async.map Option.ofObj

return roslynCompletions |> Option.map (fun rcl -> rcl, doc)
}

let getCompletionsForCSharpDocument
(p: CompletionParams)
: Async<LspResult<U2<CompletionItem array, CompletionList> option>> =
(context: ServerRequestContext)
: Async<option<Microsoft.CodeAnalysis.Completion.CompletionList * Document>> =
async {
let wf, docForUri =
p.TextDocument.Uri |> workspaceDocument context.Workspace AnyDocument

match docForUri with
| None -> return None |> LspResult.success
| None -> return None
| Some doc ->
let! ct = Async.CancellationToken
let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask
Expand All @@ -206,19 +313,7 @@ module Completion =
|> _.WithBool("ShowItemsFromUnimportedNamespaces", false)
|> _.WithBool("ShowNameSuggestions", false)

let completionTrigger =
p.Context
|> Option.bind (fun ctx ->
match ctx.TriggerKind with
| CompletionTriggerKind.Invoked
| CompletionTriggerKind.TriggerForIncompleteCompletions ->
Some Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke
| CompletionTriggerKind.TriggerCharacter ->
ctx.TriggerCharacter
|> Option.map Seq.head
|> Option.map Microsoft.CodeAnalysis.Completion.CompletionTrigger.CreateInsertionTrigger
| _ -> None)
|> Option.defaultValue Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke
let completionTrigger = p.Context |> codeActionContextToCompletionTrigger

let shouldTriggerCompletion =
p.Context
Expand All @@ -232,6 +327,23 @@ module Completion =
else
async.Return None

return roslynCompletions |> Option.map (fun rcl -> rcl, doc)
}

let handle
(context: ServerRequestContext)
(p: CompletionParams)
: Async<LspResult<U2<CompletionItem array, CompletionList> option>> =
async {
let getCompletions =
if p.TextDocument.Uri.EndsWith ".cshtml" then
getCompletionsForRazorDocument
else
getCompletionsForCSharpDocument

match! getCompletions p context with
| None -> return None |> LspResult.success
| Some(roslynCompletions, doc) ->
let toLspCompletionItemsWithCacheInfo (completions: Microsoft.CodeAnalysis.Completion.CompletionList) =
completions.ItemsList
|> Seq.map (fun item -> (item, Guid.NewGuid() |> string))
Expand All @@ -248,22 +360,21 @@ module Completion =
|> Array.ofSeq

let lspCompletionItemsWithCacheInfo =
roslynCompletions |> Option.map toLspCompletionItemsWithCacheInfo
roslynCompletions |> toLspCompletionItemsWithCacheInfo

// cache roslyn completion items
for (_, cacheItemId, roslynDoc, roslynItem) in
(lspCompletionItemsWithCacheInfo |> Option.defaultValue Array.empty) do
for _, cacheItemId, roslynDoc, roslynItem in lspCompletionItemsWithCacheInfo do
completionItemMemoryCacheSet cacheItemId roslynDoc roslynItem

let items =
lspCompletionItemsWithCacheInfo |> Array.map (fun (item, _, _, _) -> item)

return
lspCompletionItemsWithCacheInfo
|> Option.map (fun itemsWithCacheInfo ->
itemsWithCacheInfo |> Array.map (fun (item, _, _, _) -> item))
|> Option.map (fun items ->
{ IsIncomplete = true
Items = items
ItemDefaults = None })
|> Option.map U2.C2
{ IsIncomplete = true
Items = items
ItemDefaults = None }
|> U2.C2
|> Some
|> LspResult.success
}

Expand All @@ -276,7 +387,8 @@ module Completion =
match roslynDocAndItemMaybe with
| Some(doc, roslynCompletionItem) ->
let completionService =
Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc)
doc
|> Microsoft.CodeAnalysis.Completion.CompletionService.GetService
|> nonNull "Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc)"

let! ct = Async.CancellationToken
Expand Down
32 changes: 10 additions & 22 deletions src/CSharpLanguageServer/Handlers/Diagnostic.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,9 @@ open CSharpLanguageServer.Types
open CSharpLanguageServer.Util
open CSharpLanguageServer.Lsp.Workspace


[<RequireQualifiedAccess>]
module Diagnostic =
let provider
(clientCapabilities: ClientCapabilities)
: U2<DiagnosticOptions, DiagnosticRegistrationOptions> option =
let provider (_cc: ClientCapabilities) : U2<DiagnosticOptions, DiagnosticRegistrationOptions> option =
let registrationOptions: DiagnosticRegistrationOptions =
{ DocumentSelector = Some defaultDocumentSelector
WorkDoneProgress = None
Expand All @@ -38,27 +35,18 @@ module Diagnostic =
Items = [||]
RelatedDocuments = None }

let wf, docForUri =
p.TextDocument.Uri |> workspaceDocument context.Workspace AnyDocument

match docForUri with
| None -> return emptyReport |> U2.C1 |> LspResult.success
let! wf, semanticModel = p.TextDocument.Uri |> workspaceDocumentSemanticModel context.Workspace

| Some doc ->
let! ct = Async.CancellationToken
let! semanticModelMaybe = doc.GetSemanticModelAsync(ct) |> Async.AwaitTask

match semanticModelMaybe |> Option.ofObj with
let diagnostics =
match semanticModel with
| None -> [||]
| Some semanticModel ->
let diagnostics =
semanticModel.GetDiagnostics()
|> Seq.map Diagnostic.fromRoslynDiagnostic
|> Seq.map fst
|> Array.ofSeq

return { emptyReport with Items = diagnostics } |> U2.C1 |> LspResult.success
semanticModel.GetDiagnostics()
|> Seq.map Diagnostic.fromRoslynDiagnostic
|> Seq.map fst
|> Array.ofSeq

| None -> return emptyReport |> U2.C1 |> LspResult.success
return { emptyReport with Items = diagnostics } |> U2.C1 |> LspResult.success
}

let private getWorkspaceDiagnosticReports
Expand Down
64 changes: 32 additions & 32 deletions src/CSharpLanguageServer/Handlers/DocumentHighlight.fs
Original file line number Diff line number Diff line change
Expand Up @@ -21,46 +21,46 @@ module DocumentHighlight =
| :? INamespaceSymbol -> false
| _ -> true

let handle
(context: ServerRequestContext)
(p: DocumentHighlightParams)
: AsyncLspResult<DocumentHighlight[] option> =
async {
let! ct = Async.CancellationToken
let filePath = Uri.toPath p.TextDocument.Uri
// We only need to find references in the file (not the whole workspace), so we don't use
// context.FindSymbol & context.FindReferences here.
let private getHighlights symbol (project: Project) (docMaybe: Document option) (filePath: string) = async {
let! ct = Async.CancellationToken

// We only need to find references in the file (not the whole workspace), so we don't use
// context.FindSymbol & context.FindReferences here.
let getHighlights (symbol: ISymbol) (doc: Document) = async {
let docSet = ImmutableHashSet.Create(doc)
let docSet: ImmutableHashSet<Document> option =
docMaybe |> Option.map (fun doc -> ImmutableHashSet.Create(doc))

let! refs =
SymbolFinder.FindReferencesAsync(symbol, doc.Project.Solution, docSet, cancellationToken = ct)
|> Async.AwaitTask
let! refs =
SymbolFinder.FindReferencesAsync(symbol, project.Solution, docSet |> Option.toObj, cancellationToken = ct)
|> Async.AwaitTask

let! def =
SymbolFinder.FindSourceDefinitionAsync(symbol, doc.Project.Solution, cancellationToken = ct)
|> Async.AwaitTask
let! def =
SymbolFinder.FindSourceDefinitionAsync(symbol, project.Solution, cancellationToken = ct)
|> Async.AwaitTask

let locations =
refs
|> Seq.collect (fun r -> r.Locations)
|> Seq.map (fun rl -> rl.Location)
|> Seq.filter (fun l -> l.IsInSource && l.GetMappedLineSpan().Path = filePath)
|> Seq.append (def |> Option.ofObj |> Option.toList |> Seq.collect (fun sym -> sym.Locations))
let locations =
refs
|> Seq.collect (fun r -> r.Locations)
|> Seq.map (fun rl -> rl.Location)
|> Seq.filter (fun l -> l.IsInSource && l.GetMappedLineSpan().Path = filePath)
|> Seq.append (def |> Option.ofObj |> Option.toList |> Seq.collect (fun sym -> sym.Locations))

return
locations
|> Seq.choose Location.fromRoslynLocation
|> Seq.map (fun l ->
{ Range = l.Range
Kind = Some DocumentHighlightKind.Read })
}
return
locations
|> Seq.choose Location.fromRoslynLocation
|> Seq.map (fun l ->
{ Range = l.Range
Kind = Some DocumentHighlightKind.Read })
}

let handle
(context: ServerRequestContext)
(p: DocumentHighlightParams)
: AsyncLspResult<DocumentHighlight[] option> =
async {
match! workspaceDocumentSymbol context.Workspace AnyDocument p.TextDocument.Uri p.Position with
| Some wf, Some(symbol, _, Some doc) ->
| Some _wf, Some(symbol, project, docMaybe) ->
if shouldHighlight symbol then
let! highlights = getHighlights symbol doc
let! highlights = getHighlights symbol project docMaybe (Uri.toPath p.TextDocument.Uri)
return highlights |> Seq.toArray |> Some |> LspResult.success
else
return None |> LspResult.success
Expand Down
Loading
Loading