From f794848794a5edc6562fe14121a29dce9b377fd5 Mon Sep 17 00:00:00 2001 From: Anh-Dung Phan Date: Wed, 28 Dec 2016 13:46:42 +0100 Subject: [PATCH 1/4] Add a service for XmlDocParser --- .../FSharp.LanguageService.Compiler.fsproj | 3 + .../vs/ServiceInterfaceStubGenerator.fs | 96 +----- src/fsharp/vs/ServiceXmlDocParser.fs | 284 ++++++++++++++++++ 3 files changed, 290 insertions(+), 93 deletions(-) create mode 100644 src/fsharp/vs/ServiceXmlDocParser.fs diff --git a/src/fsharp/FSharp.LanguageService.Compiler/FSharp.LanguageService.Compiler.fsproj b/src/fsharp/FSharp.LanguageService.Compiler/FSharp.LanguageService.Compiler.fsproj index 015f7f23d82..abcca2ff436 100644 --- a/src/fsharp/FSharp.LanguageService.Compiler/FSharp.LanguageService.Compiler.fsproj +++ b/src/fsharp/FSharp.LanguageService.Compiler/FSharp.LanguageService.Compiler.fsproj @@ -553,6 +553,9 @@ Service/ServiceAssemblyContent.fs + + Service/ServiceXmlDocParser.fs + Service/service.fsi diff --git a/src/fsharp/vs/ServiceInterfaceStubGenerator.fs b/src/fsharp/vs/ServiceInterfaceStubGenerator.fs index 0d385afb0b8..b93b2230c91 100644 --- a/src/fsharp/vs/ServiceInterfaceStubGenerator.fs +++ b/src/fsharp/vs/ServiceInterfaceStubGenerator.fs @@ -1,4 +1,6 @@ -namespace Microsoft.FSharp.Compiler.SourceCodeServices +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.FSharp.Compiler.SourceCodeServices open System open System.Diagnostics @@ -7,98 +9,6 @@ open Microsoft.FSharp.Compiler open Microsoft.FSharp.Compiler.Ast open Microsoft.FSharp.Compiler.Range open Microsoft.FSharp.Compiler.SourceCodeServices - -[] -[] -module Array = - /// pass an array byref to reverse it in place - let revInPlace (array: 'T []) = - if Array.isEmpty array then () else - let arrlen, revlen = array.Length-1, array.Length/2 - 1 - for idx in 0 .. revlen do - let t1 = array.[idx] - let t2 = array.[arrlen-idx] - array.[idx] <- t2 - array.[arrlen-idx] <- t1 - - /// Async implementation of Array.map. - let mapAsync (mapping : 'T -> Async<'U>) (array : 'T[]) : Async<'U[]> = - let len = Array.length array - let result = Array.zeroCreate len - - async { // Apply the mapping function to each array element. - for i in 0 .. len - 1 do - let! mappedValue = mapping array.[i] - result.[i] <- mappedValue - - // Return the completed results. - return result - } - -[] -[] -module String = - open System.IO - - let inline toCharArray (str: string) = str.ToCharArray() - - let lowerCaseFirstChar (str: string) = - if String.IsNullOrEmpty str - || Char.IsLower(str, 0) then str else - let strArr = toCharArray str - match Array.tryHead strArr with - | None -> str - | Some c -> - strArr.[0] <- Char.ToLower c - String (strArr) - - let extractTrailingIndex (str: string) = - match str with - | null -> null, None - | _ -> - let charr = str.ToCharArray() - Array.revInPlace charr - let digits = Array.takeWhile Char.IsDigit charr - Array.revInPlace digits - String digits - |> function - | "" -> str, None - | index -> str.Substring (0, str.Length - index.Length), Some (int index) - - /// Remove all trailing and leading whitespace from the string - /// return null if the string is null - let trim (value: string) = if isNull value then null else value.Trim() - - /// Splits a string into substrings based on the strings in the array separators - let split options (separator: string []) (value: string) = - if isNull value then null else value.Split(separator, options) - - let (|StartsWith|_|) pattern value = - if String.IsNullOrWhiteSpace value then - None - elif value.StartsWith pattern then - Some() - else None - - let (|Contains|_|) pattern value = - if String.IsNullOrWhiteSpace value then - None - elif value.Contains pattern then - Some() - else None - - let getLines (str: string) = - use reader = new StringReader(str) - [| - let line = ref (reader.ReadLine()) - while not (isNull !line) do - yield !line - line := reader.ReadLine() - if str.EndsWith("\n") then - // last trailing space not returned - // http://stackoverflow.com/questions/19365404/stringreader-omits-trailing-linebreak - yield String.Empty - |] [] module internal CodeGenerationUtils = diff --git a/src/fsharp/vs/ServiceXmlDocParser.fs b/src/fsharp/vs/ServiceXmlDocParser.fs new file mode 100644 index 00000000000..21c338506b6 --- /dev/null +++ b/src/fsharp/vs/ServiceXmlDocParser.fs @@ -0,0 +1,284 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.FSharp.Compiler.SourceCodeServices + +[] +[] +module Array = + /// pass an array byref to reverse it in place + let revInPlace (array: 'T []) = + if Array.isEmpty array then () else + let arrlen, revlen = array.Length-1, array.Length/2 - 1 + for idx in 0 .. revlen do + let t1 = array.[idx] + let t2 = array.[arrlen-idx] + array.[idx] <- t2 + array.[arrlen-idx] <- t1 + + /// Async implementation of Array.map. + let mapAsync (mapping : 'T -> Async<'U>) (array : 'T[]) : Async<'U[]> = + let len = Array.length array + let result = Array.zeroCreate len + + async { // Apply the mapping function to each array element. + for i in 0 .. len - 1 do + let! mappedValue = mapping array.[i] + result.[i] <- mappedValue + + // Return the completed results. + return result + } + +[] +[] +module String = + open System + open System.IO + + let inline toCharArray (str: string) = str.ToCharArray() + + let lowerCaseFirstChar (str: string) = + if String.IsNullOrEmpty str + || Char.IsLower(str, 0) then str else + let strArr = toCharArray str + match Array.tryHead strArr with + | None -> str + | Some c -> + strArr.[0] <- Char.ToLower c + String (strArr) + + let extractTrailingIndex (str: string) = + match str with + | null -> null, None + | _ -> + let charr = str.ToCharArray() + Array.revInPlace charr + let digits = Array.takeWhile Char.IsDigit charr + Array.revInPlace digits + String digits + |> function + | "" -> str, None + | index -> str.Substring (0, str.Length - index.Length), Some (int index) + + /// Remove all trailing and leading whitespace from the string + /// return null if the string is null + let trim (value: string) = if isNull value then null else value.Trim() + + /// Splits a string into substrings based on the strings in the array separators + let split options (separator: string []) (value: string) = + if isNull value then null else value.Split(separator, options) + + let (|StartsWith|_|) pattern value = + if String.IsNullOrWhiteSpace value then + None + elif value.StartsWith pattern then + Some() + else None + + let (|Contains|_|) pattern value = + if String.IsNullOrWhiteSpace value then + None + elif value.Contains pattern then + Some() + else None + + let getLines (str: string) = + use reader = new StringReader(str) + [| + let line = ref (reader.ReadLine()) + while not (isNull !line) do + yield !line + line := reader.ReadLine() + if str.EndsWith("\n") then + // last trailing space not returned + // http://stackoverflow.com/questions/19365404/stringreader-omits-trailing-linebreak + yield String.Empty + |] + +/// Represent an Xml documentation block in source code +type XmlDocable = + | XmlDocable of line:int * indent:int * paramNames:string list + +module internal XmlDocParsing = + open Microsoft.FSharp.Compiler.Range + open Microsoft.FSharp.Compiler.Ast + + let (|ConstructorPats|) = function + | Pats ps -> ps + | NamePatPairs(xs, _) -> List.map snd xs + + let rec digNamesFrom = function + | SynPat.Named(_innerPat,id,_isTheThisVar,_access,_range) -> [id.idText] + | SynPat.Typed(pat,_type,_range) -> digNamesFrom pat + | SynPat.Attrib(pat,_attrs,_range) -> digNamesFrom pat + | SynPat.LongIdent(_lid,_idOpt,_typDeclsOpt,ConstructorPats pats,_access,_range) -> + pats |> List.collect digNamesFrom + | SynPat.Tuple(pats,_range) + | SynPat.StructTuple(pats,_range) -> pats |> List.collect digNamesFrom + | SynPat.Paren(pat,_range) -> digNamesFrom pat + | SynPat.OptionalVal (id, _) -> [id.idText] + | SynPat.Or _ // no one uses ors in fun decls + | SynPat.Ands _ // no one uses ands in fun decls + | SynPat.ArrayOrList _ // no one uses this in fun decls + | SynPat.Record _ // no one uses this in fun decls + | SynPat.Null _ + | SynPat.Const _ + | SynPat.Wild _ + | SynPat.IsInst _ + | SynPat.QuoteExpr _ + | SynPat.DeprecatedCharRange _ + | SynPat.InstanceMember _ + | SynPat.FromParseError _ -> [] + + let getXmlDocablesImpl(sourceCodeLinesOfTheFile: string [], input: ParsedInput option) = + let indentOf (lineNum: int) = + let mutable i = 0 + let line = sourceCodeLinesOfTheFile.[lineNum-1] // -1 because lineNum reported by xmldocs are 1-based, but array is 0-based + while i < line.Length && line.Chars(i) = ' ' do + i <- i + 1 + i + + let isEmptyXmlDoc (preXmlDoc: PreXmlDoc) = + match preXmlDoc.ToXmlDoc() with + | XmlDoc [||] -> true + | XmlDoc [|x|] when x.Trim() = "" -> true + | _ -> false + + let rec getXmlDocablesSynModuleDecl = function + | SynModuleDecl.NestedModule(_, _, synModuleDecls, _, _) -> + (synModuleDecls |> List.collect getXmlDocablesSynModuleDecl) + | SynModuleDecl.Let(_, synBindingList, range) -> + let anyXmlDoc = + synBindingList |> List.exists (fun (SynBinding.Binding(_, _, _, _, _, preXmlDoc, _, _, _, _, _, _)) -> + not <| isEmptyXmlDoc preXmlDoc) + if anyXmlDoc then [] else + let synAttributes = + synBindingList |> List.collect (fun (SynBinding.Binding(_, _, _, _, a, _, _, _, _, _, _, _)) -> a) + let fullRange = synAttributes |> List.fold (fun r a -> unionRanges r a.Range) range + let line = fullRange.StartLine + let indent = indentOf line + [ for SynBinding.Binding(_, _, _, _, _, _, synValData, synPat, _, _, _, _) in synBindingList do + match synValData with + | SynValData(_memberFlagsOpt, SynValInfo(args, _), _) when not (List.isEmpty args) -> + let parameters = + args + |> List.collect ( + List.collect (fun (SynArgInfo(_, _, ident)) -> + match ident with + | Some ident -> [ident.idText] + | None -> [])) + match parameters with + | [] -> + let paramNames = digNamesFrom synPat + yield! paramNames + | _ :: _ -> + yield! parameters + | _ -> () ] + |> fun paramNames -> [ XmlDocable(line,indent,paramNames) ] + | SynModuleDecl.Types(synTypeDefnList, _) -> (synTypeDefnList |> List.collect getXmlDocablesSynTypeDefn) + | SynModuleDecl.NamespaceFragment(synModuleOrNamespace) -> getXmlDocablesSynModuleOrNamespace synModuleOrNamespace + | SynModuleDecl.ModuleAbbrev _ + | SynModuleDecl.DoExpr _ + | SynModuleDecl.Exception _ + | SynModuleDecl.Open _ + | SynModuleDecl.Attributes _ + | SynModuleDecl.HashDirective _ -> [] + + and getXmlDocablesSynModuleOrNamespace (SynModuleOrNamespace(_, _, _, synModuleDecls, _, _, _, _)) = + (synModuleDecls |> List.collect getXmlDocablesSynModuleDecl) + + and getXmlDocablesSynTypeDefn (SynTypeDefn.TypeDefn(ComponentInfo(synAttributes, _, _, _, preXmlDoc, _, _, compRange), synTypeDefnRepr, synMemberDefns, tRange)) = + let stuff = + match synTypeDefnRepr with + | SynTypeDefnRepr.ObjectModel(_, synMemberDefns, _) -> (synMemberDefns |> List.collect getXmlDocablesSynMemberDefn) + | SynTypeDefnRepr.Simple(_synTypeDefnSimpleRepr, _range) -> [] + | SynTypeDefnRepr.Exception _ -> [] + let docForTypeDefn = + if isEmptyXmlDoc preXmlDoc then + let fullRange = synAttributes |> List.fold (fun r a -> unionRanges r a.Range) (unionRanges compRange tRange) + let line = fullRange.StartLine + let indent = indentOf line + [XmlDocable(line,indent,[])] + else [] + docForTypeDefn @ stuff @ (synMemberDefns |> List.collect getXmlDocablesSynMemberDefn) + + and getXmlDocablesSynMemberDefn = function + | SynMemberDefn.Member(SynBinding.Binding(_, _, _, _, synAttributes, preXmlDoc, _, synPat, _, _, _, _), memRange) -> + if isEmptyXmlDoc preXmlDoc then + let fullRange = synAttributes |> List.fold (fun r a -> unionRanges r a.Range) memRange + let line = fullRange.StartLine + let indent = indentOf line + let paramNames = digNamesFrom synPat + [XmlDocable(line,indent,paramNames)] + else [] + | SynMemberDefn.AbstractSlot(ValSpfn(synAttributes, _, _, _, SynValInfo(args, _), _, _, preXmlDoc, _, _, _), _, range) -> + if isEmptyXmlDoc preXmlDoc then + let fullRange = synAttributes |> List.fold (fun r a -> unionRanges r a.Range) range + let line = fullRange.StartLine + let indent = indentOf line + let paramNames = args |> List.collect (fun az -> az |> List.choose (fun (SynArgInfo(_synAttributes, _, idOpt)) -> match idOpt with | Some id -> Some(id.idText) | _ -> None)) + [XmlDocable(line,indent,paramNames)] + else [] + | SynMemberDefn.Interface(_synType, synMemberDefnsOption, _range) -> + match synMemberDefnsOption with + | None -> [] + | Some(x) -> x |> List.collect getXmlDocablesSynMemberDefn + | SynMemberDefn.NestedType(synTypeDefn, _, _) -> getXmlDocablesSynTypeDefn synTypeDefn + | SynMemberDefn.AutoProperty(synAttributes, _, _, _, _, _, _, _, _, _, range) -> + let fullRange = synAttributes |> List.fold (fun r a -> unionRanges r a.Range) range + let line = fullRange.StartLine + let indent = indentOf line + [XmlDocable(line, indent, [])] + | SynMemberDefn.Open _ + | SynMemberDefn.ImplicitCtor _ + | SynMemberDefn.ImplicitInherit _ + | SynMemberDefn.Inherit _ + | SynMemberDefn.ValField _ + | SynMemberDefn.LetBindings _ -> [] + + and getXmlDocablesInput input = + match input with + | ParsedInput.ImplFile(ParsedImplFileInput(_, _, _, _, _, symModules, _))-> + symModules |> List.collect getXmlDocablesSynModuleOrNamespace + | ParsedInput.SigFile _ -> [] + + async { + // Get compiler options for the 'project' implied by a single script file + match input with + | Some input -> + return getXmlDocablesInput input + | None -> + // Should not fail here, just in case + return [] + } + +module internal XmlDocComment = + let private ws (s: string, pos) = + let res = s.TrimStart() + Some (res, pos + (s.Length - res.Length)) + + let private str (prefix: string) (s: string, pos) = + match s.StartsWith prefix with + | true -> + let res = s.Substring prefix.Length + Some (res, pos + (s.Length - res.Length)) + | _ -> None + + let private eol (s: string, pos) = + match s with + | "" -> Some ("", pos) + | _ -> None + + let inline private (>=>) f g = f >> Option.bind g + + // if it's a blank XML comment with trailing "<", returns Some (index of the "<"), otherwise returns None + let isBlank (s: string) = + let parser = ws >=> str "///" >=> ws >=> str "<" >=> eol + let res = parser (s.TrimEnd(), 0) |> Option.map snd |> Option.map (fun x -> x - 1) + res + +module internal XmlDocParser = + /// Get the list of Xml documentation from current source code + let getXmlDocables (sourceCodeOfTheFile, input) = + let sourceCodeLinesOfTheFile = String.getLines sourceCodeOfTheFile + XmlDocParsing.getXmlDocablesImpl (sourceCodeLinesOfTheFile, input) \ No newline at end of file From 83db0c6c253bebf59869b0aa48c2d5113b11e72b Mon Sep 17 00:00:00 2001 From: Anh-Dung Phan Date: Wed, 28 Dec 2016 16:18:20 +0100 Subject: [PATCH 2/4] Add corresponding VS service --- .../Commands/FsiCommandService.fs | 4 +- .../Commands/XmlDocCommandService.fs | 108 ++++++++++++++++++ .../src/FSharp.Editor/FSharp.Editor.fsproj | 1 + 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs diff --git a/vsintegration/src/FSharp.Editor/Commands/FsiCommandService.fs b/vsintegration/src/FSharp.Editor/Commands/FsiCommandService.fs index 574d7add6b7..59b39547f00 100644 --- a/vsintegration/src/FSharp.Editor/Commands/FsiCommandService.fs +++ b/vsintegration/src/FSharp.Editor/Commands/FsiCommandService.fs @@ -1,4 +1,6 @@ -namespace Microsoft.VisualStudio.FSharp.Editor +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor open System open Microsoft.VisualStudio.Text.Editor diff --git a/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs b/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs new file mode 100644 index 00000000000..7fdec69ed7a --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open Microsoft.VisualStudio.Text.Editor +open Microsoft.VisualStudio.OLE.Interop +open System.ComponentModel.Composition +open Microsoft.VisualStudio +open Microsoft.VisualStudio.Editor +open Microsoft.VisualStudio.TextManager.Interop +open Microsoft.VisualStudio.Utilities +open Microsoft.VisualStudio.Shell +open Microsoft.VisualStudio.Shell.Interop +open EnvDTE + +type XmlDocCommandFilter + ( + wpfTextView: IWpfTextView, + fileName: string, + projectFactory: ProjectFactory, + languageService: VSLanguageService, + openDocumentsTracker: IOpenDocumentsTracker + ) as self = + + /// Get the char for a command. + let getTypedChar(pvaIn: IntPtr) = + char (Marshal.GetObjectForNativeVariant(pvaIn) :?> uint16) + + let mutable nextTarget = null + + member this.AttachToViewAdapter (viewAdapter: IVsTextView) = + match viewAdapter.AddCommandFilter this with + | VSConstants.S_OK, next -> + nextTarget <- next + | errorCode, _ -> + ErrorHandler.ThrowOnFailure errorCode |> ignore + + interface IOleCommandTarget with + member __.Exec(pguidCmdGroup: byref, nCmdID: uint32, nCmdexecopt: uint32, pvaIn: IntPtr, pvaOut: IntPtr) = + if pguidCmdGroup = VSConstants.VSStd2K && nCmdID = uint32 VSConstants.VSStd2KCmdID.TYPECHAR then + match getTypedChar pvaIn with + | ('/' | '<') as lastChar -> + let indexOfCaret = wpfTextView.Caret.Position.BufferPosition.Position + - wpfTextView.Caret.Position.BufferPosition.GetContainingLine().Start.Position + + let curLine = wpfTextView.Caret.Position.BufferPosition.GetContainingLine().GetText() + let lineWithLastCharInserted = curLine.Insert (indexOfCaret, string lastChar) + + match XmlDocComment.isBlank lineWithLastCharInserted with + | Some i when i = indexOfCaret -> + asyncMaybe { + // XmlDocable line #1 are 1-based, editor is 0-based + let curLineNum = wpfTextView.Caret.Position.BufferPosition.GetContainingLine().LineNumber + 1 + let! project = project() + let! parseResults = languageService.ParseFileInProject (fileName, project) + let! source = openDocumentsTracker.TryGetDocumentText fileName + let! xmlDocables = XmlDocParser.getXmlDocables (source, parseResults.ParseTree) |> liftAsync + let xmlDocablesBelowThisLine = + // +1 because looking below current line for e.g. a 'member' + xmlDocables |> List.filter (fun (XmlDocable(line,_indent,_paramNames)) -> line = curLineNum+1) + match xmlDocablesBelowThisLine with + | [] -> () + | XmlDocable(_line,indent,paramNames)::_t -> + // delete the slashes the user typed (they may be indented wrong) + wpfTextView.TextBuffer.Delete(wpfTextView.Caret.Position.BufferPosition.GetContainingLine().Extent.Span) |> ignore + // add the new xmldoc comment + let toInsert = new System.Text.StringBuilder() + toInsert.Append(' ', indent).AppendLine("/// ") + .Append(' ', indent).AppendLine("/// ") + .Append(' ', indent).Append("/// ") |> ignore + paramNames + |> List.iter (fun p -> + toInsert.AppendLine().Append(' ', indent).Append(sprintf "/// " p) |> ignore) + let _newSS = wpfTextView.TextBuffer.Insert(wpfTextView.Caret.Position.BufferPosition.Position, toInsert.ToString()) + // move the caret to between the summary tags + let lastLine = wpfTextView.Caret.Position.BufferPosition.GetContainingLine() + let middleSummaryLine = wpfTextView.TextSnapshot.GetLineFromLineNumber(lastLine.LineNumber - 1 - paramNames.Length) + wpfTextView.Caret.MoveTo(wpfTextView.GetTextViewLineContainingBufferPosition(middleSummaryLine.Start)) |> ignore + } + |> Async.Ignore + |> Async.StartImmediate + | Some _ | None -> () + | _ -> () + if not (isNull nextTarget) then + nextTarget.Exec(&pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut) + else + VSConstants.E_FAIL + + member __.QueryStatus(pguidCmdGroup: byref, cCmds: uint32, prgCmds: OLECMD [], pCmdText: IntPtr) = + if not (isNull nextTarget) then + nextTarget.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText) + else + VSConstants.E_FAIL + +[)>] +[] +[] +type internal XmlDocCommandFilterProvider [] + ([)>] serviceProvider: System.IServiceProvider, + editorFactory: IVsEditorAdaptersFactoryService) = + interface IWpfTextViewCreationListener with + member __.TextViewCreated(textView) = + match editorFactory.GetViewAdapter(textView) with + | null -> () + | textViewAdapter -> + let commandFilter = XmlDocCommandFilter serviceProvider + commandFilter.AttachToViewAdapter textViewAdapter \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 1193b8c4884..7a6de528297 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -60,6 +60,7 @@ + From f130571779c8fe2f8d2d61c872443fad1e37d490 Mon Sep 17 00:00:00 2001 From: Anh-Dung Phan Date: Wed, 28 Dec 2016 20:08:30 +0100 Subject: [PATCH 3/4] Use VS workspace to retrieve correct documents --- .../Commands/XmlDocCommandService.fs | 113 +++++++++++------- 1 file changed, 71 insertions(+), 42 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs b/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs index 7fdec69ed7a..43f9fd5075b 100644 --- a/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs +++ b/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs @@ -8,21 +8,34 @@ open Microsoft.VisualStudio.OLE.Interop open System.ComponentModel.Composition open Microsoft.VisualStudio open Microsoft.VisualStudio.Editor +open Microsoft.VisualStudio.Text open Microsoft.VisualStudio.TextManager.Interop open Microsoft.VisualStudio.Utilities open Microsoft.VisualStudio.Shell open Microsoft.VisualStudio.Shell.Interop +open Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem +open Microsoft.FSharp.Compiler.SourceCodeServices +open Microsoft.VisualStudio.FSharp.LanguageService +open System.Runtime.InteropServices open EnvDTE -type XmlDocCommandFilter +type internal XmlDocCommandFilter ( wpfTextView: IWpfTextView, - fileName: string, - projectFactory: ProjectFactory, - languageService: VSLanguageService, - openDocumentsTracker: IOpenDocumentsTracker - ) as self = - + filePath: string, + checkerProvider: FSharpCheckerProvider, + projectInfoManager: ProjectInfoManager, + workspace: VisualStudioWorkspaceImpl + ) = + let checker = checkerProvider.Checker + + let document = + // There may be multiple documents with the same file path. + // However, for the purpose of generating XmlDoc comments, it is ok to keep only the first document. + lazy(match workspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath) |> Seq.toList with + | [] -> None + | documentId :: _ -> Some (workspace.CurrentSolution.GetDocument documentId)) + /// Get the char for a command. let getTypedChar(pvaIn: IntPtr) = char (Marshal.GetObjectForNativeVariant(pvaIn) :?> uint16) @@ -43,44 +56,53 @@ type XmlDocCommandFilter | ('/' | '<') as lastChar -> let indexOfCaret = wpfTextView.Caret.Position.BufferPosition.Position - wpfTextView.Caret.Position.BufferPosition.GetContainingLine().Start.Position - let curLine = wpfTextView.Caret.Position.BufferPosition.GetContainingLine().GetText() let lineWithLastCharInserted = curLine.Insert (indexOfCaret, string lastChar) match XmlDocComment.isBlank lineWithLastCharInserted with | Some i when i = indexOfCaret -> - asyncMaybe { + async { + try // XmlDocable line #1 are 1-based, editor is 0-based - let curLineNum = wpfTextView.Caret.Position.BufferPosition.GetContainingLine().LineNumber + 1 - let! project = project() - let! parseResults = languageService.ParseFileInProject (fileName, project) - let! source = openDocumentsTracker.TryGetDocumentText fileName - let! xmlDocables = XmlDocParser.getXmlDocables (source, parseResults.ParseTree) |> liftAsync - let xmlDocablesBelowThisLine = - // +1 because looking below current line for e.g. a 'member' - xmlDocables |> List.filter (fun (XmlDocable(line,_indent,_paramNames)) -> line = curLineNum+1) - match xmlDocablesBelowThisLine with - | [] -> () - | XmlDocable(_line,indent,paramNames)::_t -> - // delete the slashes the user typed (they may be indented wrong) - wpfTextView.TextBuffer.Delete(wpfTextView.Caret.Position.BufferPosition.GetContainingLine().Extent.Span) |> ignore - // add the new xmldoc comment - let toInsert = new System.Text.StringBuilder() - toInsert.Append(' ', indent).AppendLine("/// ") - .Append(' ', indent).AppendLine("/// ") - .Append(' ', indent).Append("/// ") |> ignore - paramNames - |> List.iter (fun p -> - toInsert.AppendLine().Append(' ', indent).Append(sprintf "/// " p) |> ignore) - let _newSS = wpfTextView.TextBuffer.Insert(wpfTextView.Caret.Position.BufferPosition.Position, toInsert.ToString()) - // move the caret to between the summary tags - let lastLine = wpfTextView.Caret.Position.BufferPosition.GetContainingLine() - let middleSummaryLine = wpfTextView.TextSnapshot.GetLineFromLineNumber(lastLine.LineNumber - 1 - paramNames.Length) - wpfTextView.Caret.MoveTo(wpfTextView.GetTextViewLineContainingBufferPosition(middleSummaryLine.Start)) |> ignore - } - |> Async.Ignore + let curLineNum = wpfTextView.Caret.Position.BufferPosition.GetContainingLine().LineNumber + 1 + match document.Value with + | None -> () + | Some document -> + match projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document) with + | None -> () + | Some options -> + let! sourceText = document.GetTextAsync() + let sourceText = sourceText.ToString() + let! parseResults = checker.ParseFileInProject(filePath, sourceText, options) + let! xmlDocables = XmlDocParser.getXmlDocables (sourceText, parseResults.ParseTree) + let xmlDocablesBelowThisLine = + // +1 because looking below current line for e.g. a 'member' + xmlDocables |> List.filter (fun (XmlDocable(line,_indent,_paramNames)) -> line = curLineNum+1) + match xmlDocablesBelowThisLine with + | [] -> () + | XmlDocable(_line,indent,paramNames)::_xs -> + // delete the slashes the user typed (they may be indented wrong) + wpfTextView.TextBuffer.Delete(wpfTextView.Caret.Position.BufferPosition.GetContainingLine().Extent.Span) |> ignore + // add the new xmldoc comment + let toInsert = new System.Text.StringBuilder() + toInsert.Append(' ', indent).AppendLine("/// ") + .Append(' ', indent).AppendLine("/// ") + .Append(' ', indent).Append("/// ") |> ignore + paramNames + |> List.iter (fun p -> + toInsert.AppendLine().Append(' ', indent).Append(sprintf "/// " p) |> ignore) + let _newSS = wpfTextView.TextBuffer.Insert(wpfTextView.Caret.Position.BufferPosition.Position, toInsert.ToString()) + // move the caret to between the summary tags + let lastLine = wpfTextView.Caret.Position.BufferPosition.GetContainingLine() + let middleSummaryLine = wpfTextView.TextSnapshot.GetLineFromLineNumber(lastLine.LineNumber - 1 - paramNames.Length) + wpfTextView.Caret.MoveTo(wpfTextView.GetTextViewLineContainingBufferPosition(middleSummaryLine.Start)) |> ignore + with ex -> + Assert.Exception ex + () + } |> Async.StartImmediate - | Some _ | None -> () + | Some _ + | None -> () | _ -> () if not (isNull nextTarget) then nextTarget.Exec(&pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut) @@ -96,13 +118,20 @@ type XmlDocCommandFilter [)>] [] [] -type internal XmlDocCommandFilterProvider [] - ([)>] serviceProvider: System.IServiceProvider, +type internal XmlDocCommandFilterProvider + [] + (checkerProvider: FSharpCheckerProvider, + projectInfoManager: ProjectInfoManager, + workspace: VisualStudioWorkspaceImpl, + textDocumentFactoryService: ITextDocumentFactoryService, editorFactory: IVsEditorAdaptersFactoryService) = interface IWpfTextViewCreationListener with member __.TextViewCreated(textView) = match editorFactory.GetViewAdapter(textView) with | null -> () | textViewAdapter -> - let commandFilter = XmlDocCommandFilter serviceProvider - commandFilter.AttachToViewAdapter textViewAdapter \ No newline at end of file + match textDocumentFactoryService.TryGetTextDocument(textView.TextBuffer) with + | true, doc -> + let commandFilter = XmlDocCommandFilter(textView, doc.FilePath, checkerProvider, projectInfoManager, workspace) + commandFilter.AttachToViewAdapter textViewAdapter + | _ -> () \ No newline at end of file From 17d5acf56fa863eb9aca669a3d70c99c19aef20e Mon Sep 17 00:00:00 2001 From: Anh-Dung Phan Date: Thu, 29 Dec 2016 10:51:14 +0100 Subject: [PATCH 4/4] Use current snapshot to ensure source text is fresh --- .../src/FSharp.Editor/Commands/XmlDocCommandService.fs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs b/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs index 43f9fd5075b..51fcb52d4eb 100644 --- a/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs +++ b/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs @@ -71,8 +71,7 @@ type internal XmlDocCommandFilter match projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document) with | None -> () | Some options -> - let! sourceText = document.GetTextAsync() - let sourceText = sourceText.ToString() + let sourceText = wpfTextView.TextBuffer.CurrentSnapshot.GetText() let! parseResults = checker.ParseFileInProject(filePath, sourceText, options) let! xmlDocables = XmlDocParser.getXmlDocables (sourceText, parseResults.ParseTree) let xmlDocablesBelowThisLine =