Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion paket.lock
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ NUGET
System.Reflection.Metadata (>= 5.0)
Ionide.Analyzers (0.7)
Ionide.KeepAChangelog.Tasks (0.1.8) - copy_local: true
Ionide.LanguageServerProtocol (0.4.20)
Ionide.LanguageServerProtocol (0.4.22)
FSharp.Core (>= 6.0)
Newtonsoft.Json (>= 13.0.1)
StreamJsonRpc (>= 2.16.36)
Expand Down
18 changes: 14 additions & 4 deletions src/FsAutoComplete.Core/KeywordList.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ open FSharp.Compiler.Tokenization
open FSharp.Compiler.EditorServices
open FSharp.Compiler.Symbols

// 44 is the 'This construct is deprecated' error - we've addressed these by moving to TextEdit for the completionItems here,
// but the helper function for the CompletionItem record has to init the field to None, so it's still being counted as used.
#nowarn "44"

module KeywordList =

let keywordDescriptions = FSharpKeywords.KeywordsWithDescription |> dict
Expand Down Expand Up @@ -39,15 +43,21 @@ module KeywordList =
"line", "Indicates the original source code line" ]
|> dict

let hashSymbolCompletionItems =
let private textEdit text pos : U2<TextEdit, _> =
U2.First(
{ Range = { Start = pos; End = pos }
NewText = text }
)

let hashSymbolCompletionItems pos =
hashDirectives
|> Seq.map (fun kv ->
let label = "#" + kv.Key

{ CompletionItem.Create(kv.Key) with
Data = Some(Newtonsoft.Json.Linq.JValue(label))
Kind = Some CompletionItemKind.Keyword
InsertText = Some kv.Key
TextEdit = Some(textEdit kv.Value pos)
FilterText = Some kv.Key
SortText = Some kv.Key
Documentation = Some(Documentation.String kv.Value)
Expand All @@ -57,13 +67,13 @@ module KeywordList =
let allKeywords: string list =
keywordDescriptions |> Seq.map ((|KeyValue|) >> fst) |> Seq.toList

let keywordCompletionItems =
let keywordCompletionItems pos =
allKeywords
|> List.mapi (fun id k ->
{ CompletionItem.Create(k) with
Data = Some(Newtonsoft.Json.Linq.JValue(k))
Kind = Some CompletionItemKind.Keyword
InsertText = Some k
TextEdit = Some(textEdit k pos)
SortText = Some(sprintf "1000000%d" id)
FilterText = Some k
Label = k })
Expand Down
47 changes: 38 additions & 9 deletions src/FsAutoComplete/LspHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module FcsRange = FSharp.Compiler.Text.Range
type FcsPos = FSharp.Compiler.Text.Position
module FcsPos = FSharp.Compiler.Text.Position

#nowarn "44" // We're not using the Deprecated member of DocumentSymbol here, but it's still required to be _set_.

module FcsPos =
let subtractColumn (pos: FcsPos) (column: int) = FcsPos.mkPos pos.Line (pos.Column - column)
Expand Down Expand Up @@ -115,13 +116,13 @@ module Conversions =
Data = None
CodeDescription = Some { Href = Some(Uri(urlForCompilerCode error.ErrorNumber)) } }

let getSymbolInformations
let getWorkspaceSymbols
(uri: DocumentUri)
(glyphToSymbolKind: FSharpGlyph -> SymbolKind option)
(topLevel: NavigationTopLevelDeclaration)
(symbolFilter: SymbolInformation -> bool)
: SymbolInformation[] =
let inner (container: string option) (decl: NavigationItem) : SymbolInformation option =
(symbolFilter: WorkspaceSymbol -> bool)
: WorkspaceSymbol[] =

Check notice

Code scanning / Ionide.Analyzers.Cli

Detect if generic type should be in the postfix position.

Prefer postfix syntax for arrays.
let inner (container: string option) (decl: NavigationItem) : WorkspaceSymbol option =
// We should nearly always have a kind, if the client doesn't send weird capabilities,
// if we don't why not assume module...
let kind = defaultArg (glyphToSymbolKind decl.Glyph) SymbolKind.Module
Expand All @@ -130,20 +131,48 @@ module Conversions =
{ Uri = uri
Range = fcsRangeToLsp decl.Range }

let sym: SymbolInformation =
let sym: WorkspaceSymbol =
{ Name = decl.LogicalName
Kind = kind
Location = location
ContainerName = container
Location = U2.First location
Kind = kind
Tags = None
Deprecated = None }
Data = None }

if symbolFilter sym then Some sym else None

[| yield! inner None topLevel.Declaration |> Option.toArray
yield! topLevel.Nested |> Array.choose (inner (Some topLevel.Declaration.LogicalName)) |]

let applyQuery (query: string) (info: SymbolInformation) =
let getDocumentSymbol
(glyphToSymbolKind: FSharpGlyph -> SymbolKind option)
(topLevelItem: NavigationTopLevelDeclaration)
: DocumentSymbol =
let makeChildDocumentSymbol (item: NavigationItem) : DocumentSymbol =
{ Name = item.LogicalName
Detail = None
Kind = defaultArg (glyphToSymbolKind item.Glyph) SymbolKind.Module
Deprecated = None
Range = fcsRangeToLsp item.Range
SelectionRange = fcsRangeToLsp item.Range
Children = None
Tags = None }

{ Name = topLevelItem.Declaration.LogicalName
// TODO: what Detail actually influences
Detail = None
Kind = defaultArg (glyphToSymbolKind topLevelItem.Declaration.Glyph) SymbolKind.Module
Deprecated = None
// TODO: it would be good if we could get just the 'interesting' part of the Declaration.Range here for the SelectionRange
Range = fcsRangeToLsp topLevelItem.Declaration.Range
SelectionRange = fcsRangeToLsp topLevelItem.Declaration.Range
Children =
match topLevelItem.Nested with
| [||] -> None
| children -> Some(Array.map makeChildDocumentSymbol children)
Tags = None }

let applyQuery (query: string) (info: WorkspaceSymbol) =
match query.Split([| '.' |], StringSplitOptions.RemoveEmptyEntries) with
| [||] -> false
| [| fullName |] -> info.Name.StartsWith(fullName, StringComparison.Ordinal)
Expand Down
13 changes: 9 additions & 4 deletions src/FsAutoComplete/LspHelpers.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,19 @@ module Conversions =
val urlForCompilerCode: number: int -> string
val fcsErrorToDiagnostic: error: FSharpDiagnostic -> Diagnostic

val getSymbolInformations:
val getWorkspaceSymbols:
uri: DocumentUri ->
glyphToSymbolKind: (FSharpGlyph -> SymbolKind option) ->
topLevel: NavigationTopLevelDeclaration ->
symbolFilter: (SymbolInformation -> bool) ->
SymbolInformation array
symbolFilter: (WorkspaceSymbol -> bool) ->
WorkspaceSymbol array

val applyQuery: query: string -> info: SymbolInformation -> bool
val getDocumentSymbol:
glyphToSymbolKind: (FSharpGlyph -> SymbolKind option) ->
topLevelItem: NavigationTopLevelDeclaration ->
DocumentSymbol

val applyQuery: query: string -> info: WorkspaceSymbol -> bool

val getCodeLensInformation:
uri: DocumentUri -> typ: string -> topLevel: NavigationTopLevelDeclaration -> CodeLens array
Expand Down
89 changes: 49 additions & 40 deletions src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ open System.Threading.Tasks
open FsAutoComplete.FCSPatches
open Helpers

#nowarn "44" // we create CompletionItems here that have two deprecated properties. Since records always have to set each property....

type AdaptiveFSharpLspServer
(workspaceLoader: IWorkspaceLoader, lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFactory) =

Expand Down Expand Up @@ -305,15 +307,22 @@ type AdaptiveFSharpLspServer
| Some false -> None
| None -> None

let actualRootPath =
match p.RootUri with
| Some rootUri -> Some(Path.FileUriToLocalPath rootUri)
| None -> p.RootPath
let workspaceRoot =
match p.WorkspaceFolders with
| None
| Some [||] ->
failwith
"Unable to start LSP server - no workspacePaths are available and we do not support the deprecated RootPath and RootUri options."
| Some folders -> Some(Path.FileUriToLocalPath folders[0].Uri)

let projs =
match p.RootPath, c.AutomaticWorkspaceInit with
match workspaceRoot, c.AutomaticWorkspaceInit with
// if no workspace root available, or one is available but we don't want to automatically
// initialize the workspace, then we don't have any projects to initialize.
// in this case we need the client to call WorkspacePeek/WorkspaceLoad explicitly
| None, _
| _, false -> state.WorkspacePaths
| _, false -> WorkspaceChosen.NotChosen
// otherwise, try to infer the workspace from the automatic workspace information
| Some actualRootPath, true ->
let peeks =
WorkspacePeek.peek
Expand Down Expand Up @@ -342,7 +351,7 @@ type AdaptiveFSharpLspServer
|> WorkspaceChosen.Projs

transact (fun () ->
state.RootPath <- actualRootPath
state.RootPath <- workspaceRoot
state.ClientCapabilities <- p.Capabilities
lspClient.ClientCapabilities <- p.Capabilities

Expand Down Expand Up @@ -545,7 +554,7 @@ type AdaptiveFSharpLspServer
if lineStr.StartsWith("#", StringComparison.Ordinal) then
let completionList =
{ IsIncomplete = false
Items = KeywordList.hashSymbolCompletionItems
Items = KeywordList.hashSymbolCompletionItems p.Position
ItemDefaults = None }


Expand Down Expand Up @@ -624,7 +633,13 @@ type AdaptiveFSharpLspServer
| Some no when config.FullNameExternalAutocomplete -> sprintf "%s.%s" no d.NameInCode
| _ -> d.NameInCode

let createCompletionItem (config: FSharpConfig) (id: int) (d: DeclarationListItem) =
let textEdit text pos : U2<TextEdit, _> =
U2.First(
{ Range = { Start = pos; End = pos }
NewText = text }
)

let createCompletionItem (config: FSharpConfig) insertPos (id: int) (d: DeclarationListItem) =
let code = getCodeToInsert d

/// The `label` for completion "System.Math.Ceiling" will be displayed as "Ceiling (System.Math)". This is to bias the viewer towards the member name,
Expand All @@ -641,7 +656,7 @@ type AdaptiveFSharpLspServer
{ CompletionItem.Create(d.NameInList) with
Data = Some(JValue(d.FullName))
Kind = (state.GlyphToCompletionKind) d.Glyph
InsertText = Some code
TextEdit = Some(textEdit code insertPos)
SortText = Some(sprintf "%06d" id)
FilterText = Some filterText
Label = label }
Expand All @@ -668,13 +683,13 @@ type AdaptiveFSharpLspServer

let includeKeywords = config.KeywordsAutocomplete && shouldKeywords

let items = decls |> Array.mapi (createCompletionItem config)
let items = decls |> Array.mapi (createCompletionItem config p.Position)

let its =
if not includeKeywords then
items
else
Array.append items KeywordList.keywordCompletionItems
Array.append items (KeywordList.keywordCompletionItems p.Position)

let completionList =
{ IsIncomplete = false
Expand Down Expand Up @@ -899,45 +914,46 @@ type AdaptiveFSharpLspServer
else
TipFormatter.FormatCommentStyle.Legacy

let md text : MarkupContent =
{ Kind = MarkupKind.Markdown
Value = text }

match TipFormatter.tryFormatTipEnhanced tooltipResult.ToolTipText formatCommentStyle with
| TipFormatter.TipFormatterResult.Success tooltipInfo ->

// Display the signature as a code block

let fsharpCode s = "```fsharp\n" + s + "\n```"

let signature =
tooltipResult.Signature
|> TipFormatter.prepareSignature
|> (fun content -> MarkedString.WithLanguage { Language = "fsharp"; Value = content })
tooltipResult.Signature |> TipFormatter.prepareSignature |> fsharpCode

// Display each footer line as a separate line
let footerLines =
tooltipResult.Footer
|> TipFormatter.prepareFooterLines
|> Array.map MarkedString.String
let footerLines = tooltipResult.Footer |> TipFormatter.prepareFooterLines

let contents =
[| signature
MarkedString.String tooltipInfo.DocComment
tooltipInfo.DocComment
match tooltipResult.SymbolInfo with
| TryGetToolTipEnhancedResult.Keyword _ -> ()
| TryGetToolTipEnhancedResult.Symbol symbolInfo ->
TipFormatter.renderShowDocumentationLink
tooltipInfo.HasTruncatedExamples
symbolInfo.XmlDocSig
symbolInfo.Assembly
|> MarkedString.String
yield! footerLines |]

let response =
{ Contents = MarkedStrings contents
{ Contents = contents |> String.join Environment.NewLine |> md |> MarkupContent
Range = None }

return (Some response)

| TipFormatter.TipFormatterResult.Error error ->
let contents = [| MarkedString.String "<Note>"; MarkedString.String error |]
let contents = md $"<Note>\n {error}"

let response =
{ Contents = MarkedStrings contents
{ Contents = MarkupContent contents
Range = None }

return (Some response)
Expand Down Expand Up @@ -1267,11 +1283,10 @@ type AdaptiveFSharpLspServer
| Some decls ->
return
decls
|> Array.collect (fun top ->
getSymbolInformations p.TextDocument.Uri state.GlyphToSymbolKind top (fun _s -> true))
|> U2.First
|> Array.map (getDocumentSymbol state.GlyphToSymbolKind)
|> U2.Second
|> Some
| None -> return! LspResult.internalError $"No declarations for {fn}"
| None -> return None
with e ->
trace |> Tracing.recordException e

Expand All @@ -1284,7 +1299,6 @@ type AdaptiveFSharpLspServer
return! returnException e
}


override __.WorkspaceSymbol(symbolRequest: WorkspaceSymbolParams) =
asyncResult {
let tags = [ "WorkspaceSymbolParams", box symbolRequest ]
Expand All @@ -1306,12 +1320,9 @@ type AdaptiveFSharpLspServer
let uri = Path.LocalPathToUri p

ns
|> Array.collect (fun n ->
getSymbolInformations uri glyphToSymbolKind n (applyQuery symbolRequest.Query)))
|> U2.First
|> Some
|> Array.collect (fun n -> getWorkspaceSymbols uri glyphToSymbolKind n (applyQuery symbolRequest.Query)))

return res
return Some(U2.Second res)
with e ->
trace |> Tracing.recordException e

Expand Down Expand Up @@ -3062,24 +3073,24 @@ type AdaptiveFSharpLspServer

override x.Dispose() = disposables.Dispose()

member this.WorkDoneProgressCancel(token: ProgressToken) : Async<unit> =
member this.WorkDoneProgressCancel(progressParams: WorkDoneProgressCancelParams) : Async<unit> =
async {

let tags = [ "ProgressToken", box token ]
let tags = [ "ProgressToken", box progressParams.token ]
use trace = fsacActivitySource.StartActivityForType(thisType, tags = tags)

try
logger.info (
Log.setMessage "WorkDoneProgressCancel Request: {params}"
>> Log.addContextDestructured "params" token
>> Log.addContextDestructured "params" progressParams.token
)

with e ->
trace |> Tracing.recordException e

logger.error (
Log.setMessage "WorkDoneProgressCancel Request Errored {p}"
>> Log.addContextDestructured "token" token
>> Log.addContextDestructured "token" progressParams.token
>> Log.addExn e
)

Expand Down Expand Up @@ -3147,8 +3158,6 @@ module AdaptiveFSharpLspServer =

}



let startCore toolsPath workspaceLoaderFactory sourceTextFactory =
use input = Console.OpenStandardInput()
use output = Console.OpenStandardOutput()
Expand Down
Loading