diff --git a/src/Components/LineLens.fs b/src/Components/LineLens.fs deleted file mode 100644 index 08de26f6..00000000 --- a/src/Components/LineLens.fs +++ /dev/null @@ -1,413 +0,0 @@ -module Ionide.VSCode.FSharp.LineLens - -open System -open System.Collections.Generic -open Fable.Core -open Fable.Import.VSCode -open Fable.Import.VSCode.Vscode -open Fable.Core.JsInterop -open DTO - -type Number = float - -let private logger = - ConsoleAndOutputChannelLogger(Some "LineLens", Level.DEBUG, None, Some Level.DEBUG) - -[] -module LineLensConfig = - - open System.Text.RegularExpressions - - type EnabledMode = - | Never - | ReplaceCodeLens - | Always - - let private parseEnabledMode (s: string) = - match s.ToLowerInvariant() with - | "never" -> Never - | "always" -> Always - | "replacecodelens" - | _ -> ReplaceCodeLens - - type LineLensConfig = - { enabled: EnabledMode; prefix: string } - - let defaultConfig = - { enabled = ReplaceCodeLens - prefix = " // " } - - let private themeRegex = Regex("\s*theme\((.+)\)\s*") - - let getConfig () = - let cfg = workspace.getConfiguration () - - let fsharpCodeLensConfig = - cfg.get("[fsharp]", JsObject.empty).tryGet ("editor.codeLens") - - { enabled = cfg.get ("FSharp.lineLens.enabled", "replacecodelens") |> parseEnabledMode - prefix = cfg.get ("FSharp.lineLens.prefix", defaultConfig.prefix) } - - let isEnabled conf = - match conf.enabled with - | Always -> true - | ReplaceCodeLens -> true - | _ -> false - -module Documents = - - type Cached = - { - /// vscode document version that was parsed - version: Number - /// Decorations - decorations: ResizeArray - /// Text editors where the decorations are shown - textEditors: ResizeArray - } - - type DocumentInfo = - { - /// Full uri of the document - uri: Uri - /// Current decoration cache - cache: Cached option - } - - type Documents = Dictionary - - let inline create () = Documents() - - let inline tryGet uri (documents: Documents) = documents.TryGet uri - - let inline getOrAdd uri (documents: Documents) = - match tryGet uri documents with - | Some x -> x - | None -> - let value = { uri = uri; cache = None } - documents.Add(uri, value) - value - - let inline set uri value (documents: Documents) = documents.[uri] <- value - - let update info (decorations: ResizeArray) version (documents: Documents) = - let updated = - { info with - cache = - Some - { version = version - decorations = decorations - textEditors = ResizeArray() } } - - documents |> set info.uri updated - updated - - let inline tryGetCached uri (documents: Documents) = - documents - |> tryGet uri - |> Option.bind (fun info -> info.cache |> Option.map (fun c -> info, c)) - - let inline tryGetCachedAtVersion uri version (documents: Documents) = - documents - |> tryGet uri - |> Option.bind (fun info -> - match info.cache with - | Some cache when cache.version = version -> Some(info, cache) - | _ -> None) - -let mutable private config = LineLensConfig.defaultConfig - -module LineLensDecorations = - - let create range text = - // What we add after the range - let attachment = createEmpty - attachment.color <- Some(U2.Case2(vscode.ThemeColor.Create "fsharp.linelens")) - attachment.contentText <- Some text - - // Theme for the range - let renderOptions = createEmpty - renderOptions.after <- Some attachment - - let decoration = createEmpty - decoration.range <- range - decoration.renderOptions <- Some renderOptions - decoration - - let decorationType = - let opt = createEmpty - opt.isWholeLine <- Some true - opt - -type State = - { documents: Documents.Documents - decorationType: TextEditorDecorationType - disposables: ResizeArray } - -module DecorationUpdate = - - let formatSignature (sign: SignatureData) : string = - let formatType = - function - | Contains "->" t -> sprintf "(%s)" t - | t -> t - - let args = - sign.Parameters - |> List.map (fun group -> group |> List.map (fun p -> formatType p.Type) |> String.concat " * ") - |> String.concat " -> " - - if String.IsNullOrEmpty args then - sign.OutputType - else - args + " -> " + formatType sign.OutputType - - let interestingSymbolPositions (symbols: Symbols[]) : DTO.Range[] = - symbols - |> Array.collect (fun syms -> - let interestingNested = - syms.Nested - |> Array.choose (fun sym -> - if - sym.GlyphChar <> "Fc" - && sym.GlyphChar <> "M" - && sym.GlyphChar <> "F" - && sym.GlyphChar <> "P" - || sym.IsAbstract - || sym.EnclosingEntity = "I" // interface - || sym.EnclosingEntity = "R" // record - || sym.EnclosingEntity = "D" // DU - || sym.EnclosingEntity = "En" // enum - || sym.EnclosingEntity = "E" // exception - then - None - else - Some sym.BodyRange) - - if syms.Declaration.GlyphChar <> "Fc" then - interestingNested - else - interestingNested |> Array.append [| syms.Declaration.BodyRange |]) - - let private lineRange (doc: TextDocument) (range: DTO.Range) = - let lineNumber = float range.StartLine - 1. - let textLine = doc.lineAt lineNumber - textLine.range - - let private getSignature (uri: Uri) (range: DTO.Range) = - async { - try - let! signaturesResult = - LanguageService.signatureData uri range.StartLine (range.StartColumn - 1) - |> Async.AwaitPromise - - return signaturesResult |> Option.map (fun r -> range, formatSignature r.Data) - with e -> - logger.Error("Error getting signature %o", e) - return None - } - - let private signatureToDecoration (doc: TextDocument) (range: DTO.Range, signature: string) = - LineLensDecorations.create (lineRange doc range) (config.prefix + signature) - - let private onePerLine (ranges: Range[]) = - ranges - |> Array.groupBy (fun r -> r.StartLine) - |> Array.choose (fun (_, ranges) -> if ranges.Length = 1 then Some(ranges.[0]) else None) - - let private needUpdate (uri: Uri) (version: Number) { documents = documents } = - (documents |> Documents.tryGetCachedAtVersion uri version).IsSome - - let private declarationsResultToSignatures declarationsResult uri = - promise { - let interesting = declarationsResult.Data |> interestingSymbolPositions - - let interesting = onePerLine interesting - - let! signatures = - interesting - |> Array.map (getSignature uri) - |> Async.Sequential // Need to be sequential otherwise we'll flood the server with requests causing threadpool exhaustion - |> Async.StartAsPromise - |> Promise.map (fun s -> s |> Array.choose (id)) - - return signatures - } - - /// Update the decorations stored for the document. - /// * If the info is already in cache, return that - /// * If it change during the process nothing is done and it return None, if a real change is done it return the new state - let updateDecorationsForDocument (document: TextDocument) (version: float) state = - promise { - let uri = document.uri - - match state.documents |> Documents.tryGetCachedAtVersion uri version with - | Some(info, _) -> - logger.Debug("Found existing decorations in cache for '%s' @%d", uri, version) - return Some info - | None when document.version = version -> - let text = document.getText () - let! declarationsResult = LanguageService.lineLenses uri - - match declarationsResult with - | None -> return None - | Some declarationsResult -> - - if document.version = version then - let! signatures = declarationsResultToSignatures declarationsResult uri - let info = state.documents |> Documents.getOrAdd uri - - if - document.version = version && info.cache.IsNone - || info.cache.Value.version <> version - then - let decorations = - signatures |> Seq.map (signatureToDecoration document) |> ResizeArray - - logger.Debug("New decorations generated for '%s' @%d", uri, version) - - return Some(state.documents |> Documents.update info decorations version) - else - return None - else - return None - | _ -> return None - } - - /// Set the decorations for the editor, filtering lines where the user recently typed - let setDecorationsForEditor (textEditor: TextEditor) (info: Documents.DocumentInfo) state = - match info.cache with - | Some cache when not (cache.textEditors.Contains(textEditor)) -> - cache.textEditors.Add(textEditor) - logger.Debug("Setting decorations for '%s' @%d", info.uri, cache.version) - textEditor.setDecorations (state.decorationType, U2.Case2(cache.decorations)) - | _ -> () - - /// Set the decorations for the editor if we have them for the current version of the document - let setDecorationsForEditorIfCurrentVersion (textEditor: TextEditor) state = - let uri = textEditor.document.uri - let version = textEditor.document.version - - match Documents.tryGetCachedAtVersion uri version state.documents with - | None -> () // An event will arrive later when we have generated decorations - | Some(info, _) -> setDecorationsForEditor textEditor info state - - let documentClosed (uri: Uri) state = - // We can/must drop all caches as versions are unique only while a document is open. - // If it's re-opened later versions will start at 1 again. - state.documents.Remove(uri) |> ignore - -let inline private isFsharpFile (doc: TextDocument) = - match doc with - | Document.FSharp when doc.uri.scheme = "file" -> true - | Document.FSharpScript when doc.uri.scheme = "file" -> true - | _ -> false - -let mutable private state: State option = None - -let private textEditorsChangedHandler (textEditors: ResizeArray) = - match state with - | Some state -> - for textEditor in textEditors do - if isFsharpFile textEditor.document then - DecorationUpdate.setDecorationsForEditorIfCurrentVersion textEditor state - | None -> () - -let removeDocument (uri: Uri) = - match state with - | Some state -> - let documentExistInCache = - state.documents - // Try to find the document in the cache - // We use the path as the search value because parsed URI are not unified by VSCode - |> Seq.tryFind (fun element -> element.Key.path = uri.path) - - match documentExistInCache with - | Some(KeyValue(uri, _)) -> - DecorationUpdate.documentClosed uri state - - window.visibleTextEditors - // Find the text editor related to the document in cache - |> Seq.tryFind (fun textEditor -> textEditor.document.uri = uri) - // If the text editor is found, remove the decorations - |> Option.iter (fun textEditor -> textEditor.setDecorations (state.decorationType, U2.Case1(ResizeArray()))) - - | None -> () - - | None -> () - -let private documentParsedHandler (event: Notifications.DocumentParsedEvent) = - match state with - | None -> () - | Some state -> - promise { - let! updatedInfo = DecorationUpdate.updateDecorationsForDocument event.document event.version state - - match updatedInfo with - | Some info -> - // Update all text editors where this document is shown (potentially more than one) - window.visibleTextEditors - |> Seq.filter (fun editor -> editor.document = event.document) - |> Seq.iter (fun editor -> DecorationUpdate.setDecorationsForEditor editor info state) - | _ -> () - } - |> logger.ErrorOnFailed "Updating after parse failed" - -let private closedTextDocumentHandler (textDocument: TextDocument) = - state |> Option.iter (DecorationUpdate.documentClosed textDocument.uri) - -let install () = - logger.Debug "Installing" - - let decorationType = - window.createTextEditorDecorationType (LineLensDecorations.decorationType) - - let disposables = ResizeArray() - - disposables.Add(window.onDidChangeVisibleTextEditors.Invoke(unbox textEditorsChangedHandler)) - disposables.Add(Notifications.onDocumentParsed.Invoke(unbox documentParsedHandler)) - disposables.Add(workspace.onDidCloseTextDocument.Invoke(unbox closedTextDocumentHandler)) - - let newState = - { decorationType = decorationType - disposables = disposables - documents = Documents.create () } - - state <- Some newState - - logger.Debug "Installed" - -let uninstall () = - logger.Debug "Uninstalling" - - match state with - | None -> () - | Some state -> - for disposable in state.disposables do - disposable.dispose () |> ignore - - state.decorationType.dispose () - - state <- None - logger.Debug "Uninstalled" - -let configChangedHandler () = - logger.Debug("Config Changed event") - - let wasEnabled = (LineLensConfig.isEnabled config) && state <> None - config <- LineLensConfig.getConfig () - let isEnabled = LineLensConfig.isEnabled config - - if wasEnabled <> isEnabled then - if isEnabled then install () else uninstall () - - -let activate (context: ExtensionContext) = - logger.Info "Activating" - - workspace.onDidChangeConfiguration - $ (configChangedHandler, (), context.subscriptions) - |> ignore - - configChangedHandler () - () diff --git a/src/Components/LineLens/LineLens.fs b/src/Components/LineLens/LineLens.fs new file mode 100644 index 00000000..dcd17636 --- /dev/null +++ b/src/Components/LineLens/LineLens.fs @@ -0,0 +1,153 @@ +module Ionide.VSCode.FSharp.LineLens + +open System +open System.Collections.Generic +open Fable.Core +open Fable.Import.VSCode +open Fable.Import.VSCode.Vscode +open Fable.Core.JsInterop +open DTO +open LineLensShared + +type Number = float + +let private logger = + ConsoleAndOutputChannelLogger(Some "LineLens", Level.DEBUG, None, Some Level.DEBUG) + +[] +module LineLensConfig = + + open System.Text.RegularExpressions + + type EnabledMode = + | Never + | ReplaceCodeLens + | Always + + let private parseEnabledMode (s: string) = + match s.ToLowerInvariant() with + | "never" -> false + | "always" -> true + | "replacecodelens" + | _ -> true + + + let defaultConfig = { enabled = true; prefix = " // " } + + let private themeRegex = Regex("\s*theme\((.+)\)\s*") + + let getConfig () = + let cfg = workspace.getConfiguration () + + let fsharpCodeLensConfig = + cfg.get("[fsharp]", JsObject.empty).tryGet ("editor.codeLens") + + { enabled = cfg.get ("FSharp.lineLens.enabled", "replacecodelens") |> parseEnabledMode + prefix = cfg.get ("FSharp.lineLens.prefix", defaultConfig.prefix) } + + +module DecorationUpdate = + let formatSignature (sign: SignatureData) : string = + let formatType = + function + | Contains "->" t -> sprintf "(%s)" t + | t -> t + + let args = + sign.Parameters + |> List.map (fun group -> group |> List.map (fun p -> formatType p.Type) |> String.concat " * ") + |> String.concat " -> " + + if String.IsNullOrEmpty args then + sign.OutputType + else + args + " -> " + formatType sign.OutputType + + let interestingSymbolPositions (symbols: Symbols[]) : DTO.Range[] = + symbols + |> Array.collect (fun syms -> + let interestingNested = + syms.Nested + |> Array.choose (fun sym -> + if + sym.GlyphChar <> "Fc" + && sym.GlyphChar <> "M" + && sym.GlyphChar <> "F" + && sym.GlyphChar <> "P" + || sym.IsAbstract + || sym.EnclosingEntity = "I" // interface + || sym.EnclosingEntity = "R" // record + || sym.EnclosingEntity = "D" // DU + || sym.EnclosingEntity = "En" // enum + || sym.EnclosingEntity = "E" // exception + then + None + else + Some sym.BodyRange) + + if syms.Declaration.GlyphChar <> "Fc" then + interestingNested + else + interestingNested |> Array.append [| syms.Declaration.BodyRange |]) + + let private lineRange (doc: TextDocument) (range: Vscode.Range) = + let textLine = doc.lineAt range.start.line + textLine.range + + let private getSignature (uri: Uri) (range: DTO.Range) = + async { + try + let! signaturesResult = + LanguageService.signatureData uri range.StartLine (range.StartColumn - 1) + |> Async.AwaitPromise + + return signaturesResult |> Option.map (fun r -> range|>CodeRange.fromDTO, formatSignature r.Data) + with e -> + logger.Error("Error getting signature %o", e) + return None + } + + let signatureToDecoration + (config: LineLensShared.LineLensConfig) + (doc: TextDocument) + (range: Vscode.Range, signature: string) + = + LineLensShared.LineLensDecorations.create "fsharp.linelens" (lineRange doc range) (config.prefix + signature) + + let private onePerLine (ranges: Range[]) = + ranges + |> Array.groupBy (fun r -> r.StartLine) + |> Array.choose (fun (_, ranges) -> if ranges.Length = 1 then Some(ranges.[0]) else None) + + let private needUpdate (uri: Uri) (version: Number) { documents = documents } = + (documents |> Documents.tryGetCachedAtVersion uri version).IsSome + + let declarationsResultToSignatures document declarationsResult uri = + promise { + let interesting = declarationsResult.Data |> interestingSymbolPositions + + let interesting = onePerLine interesting + + let! signatures = + interesting + |> Array.map (getSignature uri) + |> Async.Sequential // Need to be sequential otherwise we'll flood the server with requests causing threadpool exhaustion + |> Async.StartAsPromise + |> Promise.map (fun s -> s |> Array.choose (id)) + + return signatures + } + + +let private lineLensDecorationUpdate: LineLensShared.DecorationUpdate = + LineLensShared.DecorationUpdate.updateDecorationsForDocument + LanguageService.lineLenses + DecorationUpdate.declarationsResultToSignatures + DecorationUpdate.signatureToDecoration + + + +let createLineLens () = + LineLensShared.LineLens("LineLens", lineLensDecorationUpdate, LineLensConfig.getConfig) + +let Instance = createLineLens () diff --git a/src/Components/LineLens/LineLensShared.fs b/src/Components/LineLens/LineLensShared.fs new file mode 100644 index 00000000..9ac9e9c3 --- /dev/null +++ b/src/Components/LineLens/LineLensShared.fs @@ -0,0 +1,322 @@ +module Ionide.VSCode.FSharp.LineLensShared + +open System.Collections.Generic +open Fable.Core +open Fable.Import.VSCode +open Fable.Import.VSCode.Vscode +open Fable.Core.JsInterop +open Fable.Core.JS +open Logging + +type Number = float + + +module Documents = + + type Cached = + { + /// vscode document version that was parsed + version: Number + /// Decorations + decorations: ResizeArray + /// Text editors where the decorations are shown + textEditors: ResizeArray + } + + type DocumentInfo = + { + /// Full uri of the document + uri: Uri + /// Current decoration cache + cache: Cached option + } + + type Documents = Dictionary + + let inline create () = Documents() + + let inline tryGet uri (documents: Documents) = documents.TryGet uri + + let inline getOrAdd uri (documents: Documents) = + match tryGet uri documents with + | Some x -> x + | None -> + let value = { uri = uri; cache = None } + documents.Add(uri, value) + value + + let inline set uri value (documents: Documents) = documents.[uri] <- value + + let update info (decorations: ResizeArray) version (documents: Documents) = + let updated = + { info with + cache = + Some + { version = version + decorations = decorations + textEditors = ResizeArray() } } + + documents |> set info.uri updated + updated + + let inline tryGetCached uri (documents: Documents) = + documents + |> tryGet uri + |> Option.bind (fun info -> info.cache |> Option.map (fun c -> info, c)) + + let inline tryGetCachedAtVersion uri version (documents: Documents) = + documents + |> tryGet uri + |> Option.bind (fun info -> + match info.cache with + | Some cache when cache.version = version -> Some(info, cache) + | _ -> None) + +module LineLensDecorations = + + let create theme range text = + // What we add after the range + let attachment = createEmpty + attachment.color <- Some(U2.Case2(vscode.ThemeColor.Create theme)) + attachment.contentText <- Some text + + // Theme for the range + let renderOptions = createEmpty + renderOptions.after <- Some attachment + + let decoration = createEmpty + decoration.range <- range + decoration.renderOptions <- Some renderOptions + decoration + + let decorationType = + let opt = createEmpty + opt.isWholeLine <- Some true + opt + +type LineLensConfig = { enabled: bool; prefix: string } + +type LineLensState = + { documents: Documents.Documents + decorationType: TextEditorDecorationType + disposables: ResizeArray } + +type DecorationUpdate = + ConsoleAndOutputChannelLogger + -> LineLensConfig + -> (TextDocument) + -> (float) + -> LineLensState + -> Promise> + +module DecorationUpdate = + /// Update the decorations stored for the document. + /// * If the info is already in cache, return that + /// * If it change during the process nothing is done and it return None, if a real change is done it return the new state + let updateDecorationsForDocument<'a> + (fetchHintsData: Uri -> Promise>) + (hintsToSignature: TextDocument -> 'a -> Uri -> Promise<(Range * string) array>) + (signatureToDecoration: LineLensConfig -> TextDocument -> (Range * string) -> DecorationOptions) + (logger: ConsoleAndOutputChannelLogger) + config + (document: TextDocument) + (version: float) + state + = + promise { + let uri = document.uri + + match state.documents |> Documents.tryGetCachedAtVersion uri version with + | Some(info, _) -> + logger.Debug("Found existing decorations in cache for '%s' @%d", uri, version) + return Some info + | None when document.version = version -> + logger.Debug("getting new decorations for '%s' @%d", uri, version) + let! hintsResults = fetchHintsData uri + + match hintsResults with + | None -> return None + | Some hintsResults -> + if document.version = version then + + let! signatures = hintsToSignature document hintsResults uri + let info = state.documents |> Documents.getOrAdd uri + + if + document.version = version && info.cache.IsNone + || info.cache.Value.version <> version + then + let decorations = + signatures |> Seq.map (signatureToDecoration config document) |> ResizeArray + + logger.Debug("New decorations generated for '%s' @%d", uri, version) + + return Some(state.documents |> Documents.update info decorations version) + else + return None + else + return None + | _ -> return None + } + + + +let inline private isFsharpFile (doc: TextDocument) = + match doc with + | Document.FSharp when doc.uri.scheme = "file" -> true + | Document.FSharpScript when doc.uri.scheme = "file" -> true + | _ -> false + +///A generic type for a Decoration that is displayed at the end of a line +/// This is used in LineLens and PipelineHints +/// The bulk of the logic is the decorationUpdate function you provide +/// Normally this should be constructed using the `DecorationUpdate.updateDecorationsForDocument` function +/// which provides caching and filtering of the decorations +type LineLens + ( + name, + decorationUpdate: DecorationUpdate, + getConfig: unit -> LineLensConfig, + ?decorationType: DecorationRenderOptions + ) = + + let logger = + ConsoleAndOutputChannelLogger(Some $"LineLensRenderer-{name}", Level.DEBUG, None, Some Level.DEBUG) + + let decorationType = + decorationType |> Option.defaultValue LineLensDecorations.decorationType + + let mutable config = { enabled = true; prefix = "// " } + let mutable state: LineLensState option = None + + /// Set the decorations for the editor, filtering lines where the user recently typed + let setDecorationsForEditor (textEditor: TextEditor) (info: Documents.DocumentInfo) state = + match info.cache with + | Some cache when not (cache.textEditors.Contains(textEditor)) -> + cache.textEditors.Add(textEditor) + logger.Debug("Setting decorations for '%s' @%d", info.uri, cache.version) + textEditor.setDecorations (state.decorationType, U2.Case2(cache.decorations)) + | _ -> () + + /// Set the decorations for the editor if we have them for the current version of the document + let setDecorationsForEditorIfCurrentVersion (textEditor: TextEditor) state = + let uri = textEditor.document.uri + let version = textEditor.document.version + + match Documents.tryGetCachedAtVersion uri version state.documents with + | None -> () // An event will arrive later when we have generated decorations + | Some(info, _) -> setDecorationsForEditor textEditor info state + + let documentClosed (uri: Uri) state = + // We can/must drop all caches as versions are unique only while a document is open. + // If it's re-opened later versions will start at 1 again. + state.documents.Remove(uri) |> ignore + + + let textEditorsChangedHandler (textEditors: ResizeArray) = + match state with + | Some state -> + for textEditor in textEditors do + if isFsharpFile textEditor.document then + setDecorationsForEditorIfCurrentVersion textEditor state + | None -> () + + let documentParsedHandler (event: Notifications.DocumentParsedEvent) = + match state with + | None -> () + | Some state -> + promise { + let! updatedInfo = decorationUpdate logger config event.document event.version state + + match updatedInfo with + | Some info -> + // Update all text editors where this document is shown (potentially more than one) + window.visibleTextEditors + |> Seq.filter (fun editor -> editor.document = event.document) + |> Seq.iter (fun editor -> setDecorationsForEditor editor info state) + | _ -> () + } + |> logger.ErrorOnFailed "Updating after parse failed" + + let closedTextDocumentHandler (textDocument: TextDocument) = + state |> Option.iter (documentClosed textDocument.uri) + + let install decorationType = + logger.Debug "Installing" + + let decorationType = window.createTextEditorDecorationType (decorationType) + + let disposables = ResizeArray() + + disposables.Add(window.onDidChangeVisibleTextEditors.Invoke(unbox textEditorsChangedHandler)) + disposables.Add(Notifications.onDocumentParsed.Invoke(unbox documentParsedHandler)) + disposables.Add(workspace.onDidCloseTextDocument.Invoke(unbox closedTextDocumentHandler)) + + let newState = + { decorationType = decorationType + disposables = disposables + documents = Documents.create () } + + state <- Some newState + + logger.Debug "Installed" + + let uninstall () = + logger.Debug "Uninstalling" + + match state with + | None -> () + | Some state -> + for disposable in state.disposables do + disposable.dispose () |> ignore + + state.decorationType.dispose () + + state <- None + logger.Debug "Uninstalled" + + let configChangedHandler (config: LineLensConfig ref) decorationType = + logger.Debug("Config Changed event") + + let wasEnabled = (config.Value.enabled) && state <> None + config.Value <- getConfig () + let isEnabled = config.Value.enabled + + if wasEnabled <> isEnabled then + if isEnabled then install decorationType else uninstall () + + member t.removeDocument(uri: Uri) = + match state with + | Some state -> + let documentExistInCache = + state.documents + // Try to find the document in the cache + // We use the path as the search value because parsed URI are not unified by VSCode + |> Seq.tryFind (fun element -> element.Key.path = uri.path) + + match documentExistInCache with + | Some(KeyValue(uri, _)) -> + documentClosed uri state + + window.visibleTextEditors + // Find the text editor related to the document in cache + |> Seq.tryFind (fun textEditor -> textEditor.document.uri = uri) + // If the text editor is found, remove the decorations + |> Option.iter (fun textEditor -> + textEditor.setDecorations (state.decorationType, U2.Case1(ResizeArray()))) + + | None -> () + + | None -> () + + + + member t.activate(context: ExtensionContext) = + logger.Info "Activating" + let changeHandler = fun () -> configChangedHandler (ref config) decorationType + + workspace.onDidChangeConfiguration $ (changeHandler, (), context.subscriptions) + |> ignore + + changeHandler () + () diff --git a/src/Components/LineLens/PipelineHints.fs b/src/Components/LineLens/PipelineHints.fs new file mode 100644 index 00000000..8c06730b --- /dev/null +++ b/src/Components/LineLens/PipelineHints.fs @@ -0,0 +1,81 @@ +module Ionide.VSCode.FSharp.PipelineHints + +open System.Collections.Generic +open Fable.Core +open Fable.Import.VSCode +open Fable.Import.VSCode.Vscode +open Fable.Core.JsInterop +open DTO +open LineLensShared + +type Number = float + +let private logger = + ConsoleAndOutputChannelLogger(Some "PipelineHints", Level.DEBUG, None, Some Level.DEBUG) + +[] +module PipelineHintsConfig = + let defaultConfig = { enabled = false; prefix = " // " } + + let getConfig () = + let cfg = workspace.getConfiguration () + + { + // we can only enable the feature overall if it's explicitly enabled and + // inline values are disabled (because inline values deliver the same functionality) + enabled = + cfg.get ("FSharp.pipelineHints.enabled", defaultConfig.enabled) + && not (cfg.get ("FSharp.inlineValues.enabled", false)) + prefix = cfg.get ("FSharp.pipelineHints.prefix", defaultConfig.prefix) } + + +module PipelineDecorationUpdate = + + let interestingSymbolPositions + (doc: TextDocument) + (lines: PipelineHint[]) + : (Vscode.Range * string[] * Vscode.Range option)[] = + lines + |> Array.map (fun n -> + let textLine = doc.lineAt (float n.Line) + + let previousTextLine = + n.PrecedingNonPipeExprLine |> Option.map (fun l -> (doc.lineAt (float l)).range) + + textLine.range, n.Types, previousTextLine) + + let private getSignature (index: int) (range: Vscode.Range, tts: string[]) = + let tt = tts.[index] + let id = tt.IndexOf("is") + let res = tt.Substring(id + 3) + range, " " + res + + let private getSignatures (range: Vscode.Range, tts: string[], previousNonPipeLine: Vscode.Range option) = + match previousNonPipeLine with + | Some previousLine -> [| getSignature 0 (previousLine, tts); getSignature 1 (range, tts) |] + | None -> [| getSignature 1 (range, tts) |] + + + let declarationsResultToSignatures (doc: TextDocument) (declarationsResult: DTO.PipelineHintsResult) uri = + promise { + let interesting = declarationsResult.Data |> interestingSymbolPositions doc + + let signatures = interesting |> Array.collect (getSignatures) + return signatures + } + + let signatureToDecoration (config: LineLensShared.LineLensConfig) doc (r, s) = + LineLensShared.LineLensDecorations.create "fsharp.pipelineHints" r (config.prefix + s) + +let private pipelineHintsDecorationUpdate: LineLensShared.DecorationUpdate = + DecorationUpdate.updateDecorationsForDocument + LanguageService.pipelineHints + PipelineDecorationUpdate.declarationsResultToSignatures + PipelineDecorationUpdate.signatureToDecoration + + + +let createPipeLineHints () = + LineLensShared.LineLens("PipelineHints", pipelineHintsDecorationUpdate, PipelineHintsConfig.getConfig) + +let Instance = createPipeLineHints () diff --git a/src/Components/PipelineHints.fs b/src/Components/PipelineHints.fs deleted file mode 100644 index 616a38da..00000000 --- a/src/Components/PipelineHints.fs +++ /dev/null @@ -1,336 +0,0 @@ -module Ionide.VSCode.FSharp.PipelineHints - -open System.Collections.Generic -open Fable.Core -open Fable.Import.VSCode -open Fable.Import.VSCode.Vscode -open Fable.Core.JsInterop -open DTO - -type Number = float - -let private logger = - ConsoleAndOutputChannelLogger(Some "PipelineHints", Level.DEBUG, None, Some Level.DEBUG) - -[] -module PipelineHintsConfig = - - type PipelineHintsConfig = { enabled: bool; prefix: string } - - let defaultConfig = { enabled = false; prefix = " // " } - - let getConfig () = - let cfg = workspace.getConfiguration () - - { - // we can only enable the feature overall if it's explicitly enabled and - // inline values are disabled (because inline values deliver the same functionality) - enabled = - cfg.get ("FSharp.pipelineHints.enabled", defaultConfig.enabled) - && not (cfg.get ("FSharp.inlineValues.enabled", false)) - prefix = cfg.get ("FSharp.pipelineHints.prefix", defaultConfig.prefix) } - - -module Documents = - - type Cached = - { - /// vscode document version that was parsed - version: Number - /// Decorations - decorations: ResizeArray - /// Text editors where the decorations are shown - textEditors: ResizeArray - } - - type DocumentInfo = - { - /// Full uri of the document - uri: Uri - /// Current decoration cache - cache: Cached option - } - - type Documents = Dictionary - - let inline create () = Documents() - - let inline tryGet uri (documents: Documents) = documents.TryGet uri - - let inline getOrAdd uri (documents: Documents) = - match tryGet uri documents with - | Some x -> x - | None -> - let value = { uri = uri; cache = None } - documents.Add(uri, value) - value - - let inline set uri value (documents: Documents) = documents.[uri] <- value - - let update info (decorations: ResizeArray) version (documents: Documents) = - let updated = - { info with - cache = - Some - { version = version - decorations = decorations - textEditors = ResizeArray() } } - - documents |> set info.uri updated - updated - - let inline tryGetCached uri (documents: Documents) = - documents - |> tryGet uri - |> Option.bind (fun info -> info.cache |> Option.map (fun c -> info, c)) - - let inline tryGetCachedAtVersion uri version (documents: Documents) = - documents - |> tryGet uri - |> Option.bind (fun info -> - match info.cache with - | Some cache when cache.version = version -> Some(info, cache) - | _ -> None) - -let mutable private config = PipelineHintsConfig.defaultConfig - -module PipelineHintsDecorations = - - let create range text = - // What we add after the range - let attachment = createEmpty - attachment.color <- Some(U2.Case2(vscode.ThemeColor.Create "fsharp.pipelineHints")) - attachment.contentText <- Some text - - // Theme for the range - let renderOptions = createEmpty - renderOptions.after <- Some attachment - - let decoration = createEmpty - decoration.range <- range - decoration.renderOptions <- Some renderOptions - decoration - - let decorationType = - let opt = createEmpty - opt.isWholeLine <- Some true - opt - -type State = - { documents: Documents.Documents - decorationType: TextEditorDecorationType - disposables: ResizeArray } - -module DecorationUpdate = - - let interestingSymbolPositions - (doc: TextDocument) - (lines: PipelineHint[]) - : (Vscode.Range * string[] * Vscode.Range option)[] = - lines - |> Array.map (fun n -> - let textLine = doc.lineAt (float n.Line) - - let previousTextLine = - n.PrecedingNonPipeExprLine |> Option.map (fun l -> (doc.lineAt (float l)).range) - - textLine.range, n.Types, previousTextLine) - - let private getSignature (index: int) (range: Vscode.Range, tts: string[]) = - let tt = tts.[index] - let id = tt.IndexOf("is") - let res = tt.Substring(id + 3) - range, " " + res - - let private getSignatures (range: Vscode.Range, tts: string[], previousNonPipeLine: Vscode.Range option) = - match previousNonPipeLine with - | Some previousLine -> [| getSignature 0 (previousLine, tts); getSignature 1 (range, tts) |] - | None -> [| getSignature 1 (range, tts) |] - - - let private declarationsResultToSignatures (doc: TextDocument) (declarationsResult: DTO.PipelineHintsResult) uri = - promise { - let interesting = declarationsResult.Data |> interestingSymbolPositions doc - - let signatures = interesting |> Array.collect (getSignatures) - return signatures - } - - /// Update the decorations stored for the document. - /// * If the info is already in cache, return that - /// * If it change during the process nothing is done and it return None, if a real change is done it return the new state - let updateDecorationsForDocument (document: TextDocument) (version: float) state = - promise { - let uri = document.uri - - match state.documents |> Documents.tryGetCachedAtVersion uri version with - | Some(info, _) -> - logger.Debug("Found existing decorations in cache for '%s' @%d", uri, version) - return Some info - | None when document.version = version -> - let! hintsResults = LanguageService.pipelineHints uri - - match hintsResults with - | None -> return None - | Some hintsResults -> - if document.version = version then - - let! signatures = declarationsResultToSignatures document hintsResults uri - let info = state.documents |> Documents.getOrAdd uri - - if - document.version = version && info.cache.IsNone - || info.cache.Value.version <> version - then - let decorations = - signatures - |> Seq.map (fun (r, s) -> PipelineHintsDecorations.create r (config.prefix + s)) - |> ResizeArray - - logger.Debug("New decorations generated for '%s' @%d", uri, version) - - return Some(state.documents |> Documents.update info decorations version) - else - return None - else - return None - | _ -> return None - } - - /// Set the decorations for the editor, filtering lines where the user recently typed - let setDecorationsForEditor (textEditor: TextEditor) (info: Documents.DocumentInfo) state = - match info.cache with - | Some cache when not (cache.textEditors.Contains(textEditor)) -> - cache.textEditors.Add(textEditor) - logger.Debug("Setting decorations for '%s' @%d", info.uri, cache.version) - textEditor.setDecorations (state.decorationType, U2.Case2(cache.decorations)) - | _ -> () - - /// Set the decorations for the editor if we have them for the current version of the document - let setDecorationsForEditorIfCurrentVersion (textEditor: TextEditor) state = - let uri = textEditor.document.uri - let version = textEditor.document.version - - match Documents.tryGetCachedAtVersion uri version state.documents with - | None -> () // An event will arrive later when we have generated decorations - | Some(info, _) -> setDecorationsForEditor textEditor info state - - let documentClosed (uri: Uri) state = - // We can/must drop all caches as versions are unique only while a document is open. - // If it's re-opened later versions will start at 1 again. - state.documents.Remove(uri) |> ignore - -let inline private isFsharpFile (doc: TextDocument) = - match doc with - | Document.FSharp when doc.uri.scheme = "file" -> true - | Document.FSharpScript when doc.uri.scheme = "file" -> true - | _ -> false - -let mutable private state: State option = None - -let private textEditorsChangedHandler (textEditors: ResizeArray) = - match state with - | Some state -> - for textEditor in textEditors do - if isFsharpFile textEditor.document then - DecorationUpdate.setDecorationsForEditorIfCurrentVersion textEditor state - | None -> () - -let removeDocument (uri: Uri) = - match state with - | Some state -> - let documentExistInCache = - state.documents - // Try to find the document in the cache - // We use the path as the search value because parsed URI are not unified by VSCode - |> Seq.tryFind (fun element -> element.Key.path = uri.path) - - match documentExistInCache with - | Some(KeyValue(uri, _)) -> - DecorationUpdate.documentClosed uri state - - window.visibleTextEditors - // Find the text editor related to the document in cache - |> Seq.tryFind (fun textEditor -> textEditor.document.uri = uri) - // If the text editor is found, remove the decorations - |> Option.iter (fun textEditor -> textEditor.setDecorations (state.decorationType, U2.Case1(ResizeArray()))) - - | None -> () - - | None -> () - -let private documentParsedHandler (event: Notifications.DocumentParsedEvent) = - match state with - | None -> () - | Some state -> - promise { - let! updatedInfo = DecorationUpdate.updateDecorationsForDocument event.document event.version state - - match updatedInfo with - | Some info -> - // Update all text editors where this document is shown (potentially more than one) - window.visibleTextEditors - |> Seq.filter (fun editor -> editor.document = event.document) - |> Seq.iter (fun editor -> DecorationUpdate.setDecorationsForEditor editor info state) - | _ -> () - } - |> logger.ErrorOnFailed "Updating after parse failed" - -let private closedTextDocumentHandler (textDocument: TextDocument) = - state |> Option.iter (DecorationUpdate.documentClosed textDocument.uri) - -let install () = - logger.Debug "Installing" - - let decorationType = - window.createTextEditorDecorationType (PipelineHintsDecorations.decorationType) - - let disposables = ResizeArray() - - disposables.Add(window.onDidChangeVisibleTextEditors.Invoke(unbox textEditorsChangedHandler)) - disposables.Add(Notifications.onDocumentParsed.Invoke(unbox documentParsedHandler)) - disposables.Add(workspace.onDidCloseTextDocument.Invoke(unbox closedTextDocumentHandler)) - - let newState = - { decorationType = decorationType - disposables = disposables - documents = Documents.create () } - - state <- Some newState - - logger.Debug "Installed" - -let uninstall () = - logger.Debug "Uninstalling" - - match state with - | None -> () - | Some state -> - for disposable in state.disposables do - disposable.dispose () |> ignore - - state.decorationType.dispose () - - state <- None - logger.Debug "Uninstalled" - -let configChangedHandler () = - logger.Debug("Config Changed event") - - let wasEnabled = (config.enabled) && state <> None - config <- PipelineHintsConfig.getConfig () - let isEnabled = config.enabled - - if wasEnabled <> isEnabled then - if isEnabled then install () else uninstall () - - -let activate (context: ExtensionContext) = - logger.Info "Activating" - - workspace.onDidChangeConfiguration - $ (configChangedHandler, (), context.subscriptions) - |> ignore - - configChangedHandler () - () diff --git a/src/Components/SolutionExplorer.fs b/src/Components/SolutionExplorer.fs index d25a5df7..631fe855 100644 --- a/src/Components/SolutionExplorer.fs +++ b/src/Components/SolutionExplorer.fs @@ -785,8 +785,8 @@ module SolutionExplorer = let normalizedPath = filePath.Replace("\\", "/") let fileUri = vscode.Uri.parse $"file:///%s{normalizedPath}" - LineLens.removeDocument fileUri - PipelineHints.removeDocument fileUri + LineLens.Instance.removeDocument fileUri + PipelineHints.Instance.removeDocument fileUri let activate (context: ExtensionContext) = let emiter = vscode.EventEmitter.Create<_>() diff --git a/src/Ionide.FSharp.fsproj b/src/Ionide.FSharp.fsproj index f7f2bd1b..6389a4f4 100644 --- a/src/Ionide.FSharp.fsproj +++ b/src/Ionide.FSharp.fsproj @@ -1,56 +1,57 @@ - - - - netstandard2.0 - false - - - - True - paket-files/Fable.Import.VSCode.fs - - - True - paket-files/Helpers.fs - - - True - paket-files/Fable.Import.Showdown.fs - - - True - paket-files/Fable.Import.VSCode.LanguageServer.fs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + netstandard2.0 + false + + + + True + paket-files/Fable.Import.VSCode.fs + + + True + paket-files/Helpers.fs + + + True + paket-files/Fable.Import.Showdown.fs + + + True + paket-files/Fable.Import.VSCode.LanguageServer.fs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/fsharp.fs b/src/fsharp.fs index 7a5c6ce1..f0117aef 100644 --- a/src/fsharp.fs +++ b/src/fsharp.fs @@ -76,7 +76,7 @@ let activate (context: ExtensionContext) : JS.Promise = tryActivate "fsprojedit" FsProjEdit.activate context tryActivate "diagnostics" Diagnostics.activate context - tryActivate "linelens" LineLens.activate context + tryActivate "linelens" LineLens.Instance.activate context tryActivate "quickinfo" QuickInfo.activate context tryActivate "help" Help.activate context tryActivate "msbuild" MSBuild.activate context @@ -90,7 +90,7 @@ let activate (context: ExtensionContext) : JS.Promise = tryActivate "infopanel" InfoPanel.activate context tryActivate "codelens" CodeLensHelpers.activate context tryActivate "gitignore" Gitignore.activate context - tryActivate "pipelinehints" PipelineHints.activate context + tryActivate "pipelinehints" PipelineHints.Instance.activate context tryActivate "testExplorer" TestExplorer.activate context tryActivate "inlayhints" InlayHints.activate context