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
2 changes: 2 additions & 0 deletions src/CSharpLanguageServer/Conversions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ module Location =
|> Option.bind (fun filePath -> if File.Exists filePath then Some filePath else None)
|> Option.map (fun filePath -> toLspLocation filePath (loc.GetLineSpan().Span))

//Console.Error.WriteLine("loc={0}; mapped={1}; source={2}", loc, mappedSourceLocation, sourceLocation)

mappedSourceLocation |> Option.orElse sourceLocation

| _ -> None
Expand Down
8 changes: 4 additions & 4 deletions src/CSharpLanguageServer/Handlers/CodeAction.fs
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,8 @@ module CodeAction =
let provider (clientCapabilities: ClientCapabilities) : U2<bool, CodeActionOptions> option =
let literalSupport =
clientCapabilities.TextDocument
|> Option.bind (fun x -> x.CodeAction)
|> Option.bind (fun x -> x.CodeActionLiteralSupport)
|> Option.bind _.CodeAction
|> Option.bind _.CodeActionLiteralSupport

match literalSupport with
| Some _ ->
Expand All @@ -341,8 +341,8 @@ module CodeAction =

let clientSupportsCodeActionEditResolveWithEditAndData =
context.ClientCapabilities.TextDocument
|> Option.bind (fun x -> x.CodeAction)
|> Option.bind (fun x -> x.ResolveSupport)
|> Option.bind _.CodeAction
|> Option.bind _.ResolveSupport
|> Option.map (fun resolveSupport -> resolveSupport.Properties |> Array.contains "edit")
|> Option.defaultValue false

Expand Down
163 changes: 147 additions & 16 deletions src/CSharpLanguageServer/Handlers/Completion.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@ 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.Conversions
open CSharpLanguageServer.Logging
open CSharpLanguageServer.RoslynHelpers

[<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 @@ -117,7 +121,7 @@ module Completion =
member __.GetDescriptionAsync(doc, item, ct) =
service.GetDescriptionAsync(doc, item, ct)

let provider (clientCapabilities: ClientCapabilities) : CompletionOptions option =
let provider (_cc: ClientCapabilities) : CompletionOptions option =
Some
{ ResolveProvider = Some true
TriggerCharacters = Some([| "."; "'" |])
Expand Down Expand Up @@ -180,13 +184,121 @@ module Completion =
synopsis, documentationText
| _, _ -> None, None

let handle
let getCompletionsForRazorDocument
(solution: Solution)
(p: CompletionParams)
: Async<option<Microsoft.CodeAnalysis.Completion.CompletionList * Document>> =
async {
match! getRazorDocumentForUri solution p.TextDocument.Uri with
| None -> return None
| Some(project, compilation, cshtmlPath, cshtmlTree) ->
let! ct = Async.CancellationToken
let! sourceText = cshtmlTree.GetTextAsync() |> Async.AwaitTask

let razorTextDocument =
solution.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

//logger.LogInformation("razorSourceText={0}", razorSourceText)

//logger.LogInformation("doc={0}", sourceText)

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

let root = cshtmlTree.GetRoot()

let mutable position: int option = None
let mutable tokenForPosition: SyntaxToken option = None
let mutable debug: string 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)

position <- Some(t.Span.Start + tokenStartCharacterOffset)

debug <-
Some(
String.Format(
"token={0}; pos.Character={1}; cshtmlSpan.StartLinePosition.Character={2}; offset={3}",
t,
pos.Character,
cshtmlSpan.StartLinePosition.Character,
tokenStartCharacterOffset
)
)

tokenForPosition <- Some(t)

//logger.LogInformation(debug |> Option.defaultValue "")

//let position = Position.toRoslynPosition sourceText.Lines translatedPosition
//logger.LogInformation("position in .cs={position}", position)

let posInCS = sourceText.Lines.GetLinePosition(position.Value)
//logger.LogInformation("lineposition={x}", posInCS)

// a hack to make <span>@Model.|</span> autocompletion to work:
// - force a dot if present on .cscshtml but missing on .cs
let newSourceText =
// TODO: check if the text in cshtml is '.', though!
let cshtmlPosition = Position.toRoslynPosition razorSourceText.Lines p.Position
let charInCshtml: char = razorSourceText[cshtmlPosition - 1]

//logger.LogInformation("charInCshtml={0}", charInCshtml)

if charInCshtml = '.' && string tokenForPosition <> "." then
sourceText.WithChanges(new TextChange(new TextSpan(position.Value - 1, 0), "."))
else
sourceText

//logger.LogInformation("newSourceText={0}", newSourceText)

let! doc = tryAddDocument logger (cshtmlPath + ".cs") (newSourceText.ToString()) solution

let doc = doc.Value

//logger.LogError("handle: doc={doc}", doc)

let completionService =
Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc)
|> RoslynCompletionServiceWrapper

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

let completionTrigger = CompletionContext.toCompletionTrigger p.Context

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

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

let getCompletionsForCSharpDocument
(context: ServerRequestContext)
(p: CompletionParams)
: Async<LspResult<U2<CompletionItem array, CompletionList> option>> =
: Async<option<Microsoft.CodeAnalysis.Completion.CompletionList * Document>> =
async {
match context.GetDocument p.TextDocument.Uri with
| None -> return None |> LspResult.success
| None -> return None

| Some doc ->
let! ct = Async.CancellationToken
let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask
Expand Down Expand Up @@ -216,6 +328,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! roslynCompletionsAndDoc =
if p.TextDocument.Uri.EndsWith(".cshtml") then
getCompletionsForRazorDocument context.Solution p
else
getCompletionsForCSharpDocument context p

match roslynCompletionsAndDoc 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 @@ -232,33 +361,35 @@ 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
}

let resolve (_context: ServerRequestContext) (item: CompletionItem) : AsyncLspResult<CompletionItem> = async {

let roslynDocAndItemMaybe =
item.Data
|> Option.bind deserialize<string option>
|> Option.bind completionItemMemoryCacheGet

match roslynDocAndItemMaybe with
| Some(doc, roslynCompletionItem) ->
logger.LogInformation("resolve, doc={0}, item={1}", doc, roslynCompletionItem)

let completionService =
Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc)
|> nonNull "Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc)"
Expand Down
4 changes: 2 additions & 2 deletions src/CSharpLanguageServer/Handlers/DocumentSymbol.fs
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,8 @@ module DocumentSymbol =
async {
let canEmitDocSymbolHierarchy =
context.ClientCapabilities.TextDocument
|> Option.bind (fun cc -> cc.DocumentSymbol)
|> Option.bind (fun cc -> cc.HierarchicalDocumentSymbolSupport)
|> Option.bind _.DocumentSymbol
|> Option.bind _.HierarchicalDocumentSymbolSupport
|> Option.defaultValue false

match context.GetDocument p.TextDocument.Uri with
Expand Down
3 changes: 3 additions & 0 deletions src/CSharpLanguageServer/Handlers/References.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ open Ionide.LanguageServerProtocol.JsonRpc

open CSharpLanguageServer.State
open CSharpLanguageServer.Conversions
open CSharpLanguageServer.Logging

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

let provider (_: ClientCapabilities) : U2<bool, ReferenceOptions> option = Some(U2.C1 true)

let handle (context: ServerRequestContext) (p: ReferenceParams) : AsyncLspResult<Location[] option> = async {
Expand Down
4 changes: 2 additions & 2 deletions src/CSharpLanguageServer/Handlers/Rename.fs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ module Rename =

let private prepareSupport (clientCapabilities: ClientCapabilities) =
clientCapabilities.TextDocument
|> Option.bind (fun x -> x.Rename)
|> Option.bind (fun x -> x.PrepareSupport)
|> Option.bind _.Rename
|> Option.bind _.PrepareSupport
|> Option.defaultValue false

let provider (clientCapabilities: ClientCapabilities) : U2<bool, RenameOptions> option =
Expand Down
Loading