|
| 1 | +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. |
| 2 | + |
| 3 | +namespace Microsoft.VisualStudio.FSharp.Editor |
| 4 | + |
| 5 | +open System |
| 6 | +open System.Composition |
| 7 | +open System.Collections.Immutable |
| 8 | + |
| 9 | +open Microsoft.CodeAnalysis.Formatting |
| 10 | +open Microsoft.CodeAnalysis.Text |
| 11 | +open Microsoft.CodeAnalysis.CodeFixes |
| 12 | + |
| 13 | +open FSharp.Compiler |
| 14 | +open FSharp.Compiler.CodeAnalysis |
| 15 | +open FSharp.Compiler.Diagnostics |
| 16 | +open FSharp.Compiler.EditorServices |
| 17 | +open FSharp.Compiler.Symbols |
| 18 | +open FSharp.Compiler.Syntax |
| 19 | +open FSharp.Compiler.Text |
| 20 | +open FSharp.Compiler.Tokenization |
| 21 | + |
| 22 | +open CancellableTasks |
| 23 | + |
| 24 | +[<NoEquality; NoComparison>] |
| 25 | +type internal InterfaceState = |
| 26 | + { |
| 27 | + InterfaceData: InterfaceData |
| 28 | + EndPosOfWith: pos option |
| 29 | + AppendBracketAt: int option |
| 30 | + Tokens: Tokenizer.SavedTokenInfo[] |
| 31 | + } |
| 32 | + |
| 33 | +// state machine not statically compilable |
| 34 | +// TODO: rewrite token arithmetics properly here |
| 35 | +#nowarn "3511" |
| 36 | + |
| 37 | +[<ExportCodeFixProvider(FSharpConstants.FSharpLanguageName, Name = CodeFix.ImplementInterface); Shared>] |
| 38 | +type internal ImplementInterfaceCodeFixProvider [<ImportingConstructor>] () = |
| 39 | + inherit CodeFixProvider() |
| 40 | + |
| 41 | + let queryInterfaceState appendBracketAt (pos: pos) (tokens: Tokenizer.SavedTokenInfo[]) (ast: ParsedInput) = |
| 42 | + let line = pos.Line - 1 |
| 43 | + |
| 44 | + InterfaceStubGenerator.TryFindInterfaceDeclaration pos ast |
| 45 | + |> Option.map (fun iface -> |
| 46 | + let endPosOfWidth = |
| 47 | + tokens |
| 48 | + |> Array.tryPick (fun (t: Tokenizer.SavedTokenInfo) -> |
| 49 | + if t.Tag = FSharpTokenTag.WITH || t.Tag = FSharpTokenTag.OWITH then |
| 50 | + Some(Position.fromZ line (t.RightColumn + 1)) |
| 51 | + else |
| 52 | + None) |
| 53 | + |
| 54 | + let appendBracketAt = |
| 55 | + match iface, appendBracketAt with |
| 56 | + | InterfaceData.ObjExpr _, Some _ -> appendBracketAt |
| 57 | + | _ -> None |
| 58 | + |
| 59 | + { |
| 60 | + InterfaceData = iface |
| 61 | + EndPosOfWith = endPosOfWidth |
| 62 | + AppendBracketAt = appendBracketAt |
| 63 | + Tokens = tokens |
| 64 | + }) |
| 65 | + |
| 66 | + let getLineIdent (lineStr: string) = |
| 67 | + lineStr.Length - lineStr.TrimStart(' ').Length |
| 68 | + |
| 69 | + let inferStartColumn indentSize state (sourceText: SourceText) = |
| 70 | + match InterfaceStubGenerator.GetMemberNameAndRanges state.InterfaceData with |
| 71 | + | (_, range) :: _ -> |
| 72 | + let lineStr = sourceText.Lines[ range.StartLine - 1 ].ToString() |
| 73 | + getLineIdent lineStr |
| 74 | + | [] -> |
| 75 | + match state.InterfaceData with |
| 76 | + | InterfaceData.Interface _ as iface -> |
| 77 | + // 'interface ISomething with' is often in a new line, we use the indentation of that line |
| 78 | + let lineStr = sourceText.Lines[ iface.Range.StartLine - 1 ].ToString() |
| 79 | + getLineIdent lineStr + indentSize |
| 80 | + | InterfaceData.ObjExpr _ as iface -> |
| 81 | + state.Tokens |
| 82 | + |> Array.tryPick (fun (t: Tokenizer.SavedTokenInfo) -> |
| 83 | + if t.Tag = FSharpTokenTag.NEW then |
| 84 | + Some(t.LeftColumn + indentSize) |
| 85 | + else |
| 86 | + None) |
| 87 | + // There is no reference point, we indent the content at the start column of the interface |
| 88 | + |> Option.defaultValue iface.Range.StartColumn |
| 89 | + |
| 90 | + let getChanges (sourceText: SourceText) state displayContext implementedMemberSignatures entity indentSize verboseMode = |
| 91 | + let startColumn = inferStartColumn indentSize state sourceText |
| 92 | + let objectIdentifier = "this" |
| 93 | + let defaultBody = "raise (System.NotImplementedException())" |
| 94 | + let typeParams = state.InterfaceData.TypeParameters |
| 95 | + |
| 96 | + let stub = |
| 97 | + let stub = |
| 98 | + InterfaceStubGenerator.FormatInterface |
| 99 | + startColumn |
| 100 | + indentSize |
| 101 | + typeParams |
| 102 | + objectIdentifier |
| 103 | + defaultBody |
| 104 | + displayContext |
| 105 | + implementedMemberSignatures |
| 106 | + entity |
| 107 | + verboseMode |
| 108 | + |
| 109 | + stub.TrimEnd(Environment.NewLine.ToCharArray()) |
| 110 | + |
| 111 | + let stubChange = |
| 112 | + match state.EndPosOfWith with |
| 113 | + | Some pos -> |
| 114 | + let currentPos = sourceText.Lines[pos.Line - 1].Start + pos.Column |
| 115 | + TextChange(TextSpan(currentPos, 0), stub) |
| 116 | + | None -> |
| 117 | + let range = state.InterfaceData.Range |
| 118 | + let currentPos = sourceText.Lines[range.EndLine - 1].Start + range.EndColumn |
| 119 | + TextChange(TextSpan(currentPos, 0), " with" + stub) |
| 120 | + |
| 121 | + match state.AppendBracketAt with |
| 122 | + | Some index -> [ stubChange; TextChange(TextSpan(index, 0), " }") ] |
| 123 | + | None -> [ stubChange ] |
| 124 | + |
| 125 | + let getSuggestions |
| 126 | + ( |
| 127 | + sourceText: SourceText, |
| 128 | + results: FSharpCheckFileResults, |
| 129 | + state: InterfaceState, |
| 130 | + displayContext, |
| 131 | + entity, |
| 132 | + indentSize |
| 133 | + ) = |
| 134 | + if InterfaceStubGenerator.HasNoInterfaceMember entity then |
| 135 | + CancellableTask.singleton Seq.empty |
| 136 | + else |
| 137 | + let membersAndRanges = |
| 138 | + InterfaceStubGenerator.GetMemberNameAndRanges state.InterfaceData |
| 139 | + |
| 140 | + let interfaceMembers = InterfaceStubGenerator.GetInterfaceMembers entity |
| 141 | + |
| 142 | + let hasTypeCheckError = |
| 143 | + results.Diagnostics |
| 144 | + |> Array.exists (fun e -> e.Severity = FSharpDiagnosticSeverity.Error) |
| 145 | + // This comparison is a bit expensive |
| 146 | + if hasTypeCheckError && List.length membersAndRanges <> Seq.length interfaceMembers then |
| 147 | + |
| 148 | + let getMemberByLocation (name, range: range) = |
| 149 | + let lineStr = sourceText.Lines[ range.EndLine - 1 ].ToString() |
| 150 | + results.GetSymbolUseAtLocation(range.EndLine, range.EndColumn, lineStr, [ name ]) |
| 151 | + |
| 152 | + cancellableTask { |
| 153 | + let! implementedMemberSignatures = |
| 154 | + InterfaceStubGenerator.GetImplementedMemberSignatures getMemberByLocation displayContext state.InterfaceData |
| 155 | + |
| 156 | + let getCodeFix title verboseMode = |
| 157 | + let changes = |
| 158 | + getChanges sourceText state displayContext implementedMemberSignatures entity indentSize verboseMode |
| 159 | + |
| 160 | + { |
| 161 | + Name = CodeFix.ImplementInterface |
| 162 | + Message = title |
| 163 | + Changes = changes |
| 164 | + } |
| 165 | + |
| 166 | + return |
| 167 | + seq { |
| 168 | + getCodeFix (SR.ImplementInterface()) true |
| 169 | + getCodeFix (SR.ImplementInterfaceWithoutTypeAnnotation()) false |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + else |
| 174 | + CancellableTask.singleton Seq.empty |
| 175 | + |
| 176 | + override _.FixableDiagnosticIds = ImmutableArray.Create "FS0366" |
| 177 | + |
| 178 | + override this.RegisterCodeFixesAsync context = context.RegisterFsharpFixes this |
| 179 | + |
| 180 | + interface IFSharpMultiCodeFixProvider with |
| 181 | + member _.GetCodeFixesAsync context = |
| 182 | + cancellableTask { |
| 183 | + let! cancellationToken = CancellableTask.getCancellationToken () |
| 184 | + |
| 185 | + let! parseResults, checkFileResults = |
| 186 | + context.Document.GetFSharpParseAndCheckResultsAsync(nameof ImplementInterfaceCodeFixProvider) |
| 187 | + |
| 188 | + let! sourceText = context.GetSourceTextAsync() |
| 189 | + |
| 190 | + let textLine = sourceText.Lines.GetLineFromPosition context.Span.Start |
| 191 | + |
| 192 | + let! _, _, parsingOptions, _ = context.Document.GetFSharpCompilationOptionsAsync(nameof ImplementInterfaceCodeFixProvider) |
| 193 | + |
| 194 | + let defines = CompilerEnvironment.GetConditionalDefinesForEditing parsingOptions |
| 195 | + let langVersionOpt = Some parsingOptions.LangVersionText |
| 196 | + // Notice that context.Span doesn't return reliable ranges to find tokens at exact positions. |
| 197 | + // That's why we tokenize the line and try to find the last successive identifier token |
| 198 | + let tokens = |
| 199 | + Tokenizer.tokenizeLine ( |
| 200 | + context.Document.Id, |
| 201 | + sourceText, |
| 202 | + context.Span.Start, |
| 203 | + context.Document.FilePath, |
| 204 | + defines, |
| 205 | + langVersionOpt, |
| 206 | + parsingOptions.StrictIndentation, |
| 207 | + cancellationToken |
| 208 | + ) |
| 209 | + |
| 210 | + let startLeftColumn = context.Span.Start - textLine.Start |
| 211 | + |
| 212 | + let rec tryFindIdentifierToken acc i = |
| 213 | + if i >= tokens.Length then |
| 214 | + acc |
| 215 | + else |
| 216 | + match tokens[i] with |
| 217 | + | t when t.LeftColumn < startLeftColumn -> |
| 218 | + // Skip all the tokens starting before the context |
| 219 | + tryFindIdentifierToken acc (i + 1) |
| 220 | + | t when t.Tag = FSharpTokenTag.Identifier -> tryFindIdentifierToken (Some t) (i + 1) |
| 221 | + | t when t.Tag = FSharpTokenTag.DOT || Option.isNone acc -> tryFindIdentifierToken acc (i + 1) |
| 222 | + | _ -> acc |
| 223 | + |
| 224 | + let token = tryFindIdentifierToken None 0 |
| 225 | + |
| 226 | + match token with |
| 227 | + | None -> return Seq.empty |
| 228 | + | Some token -> |
| 229 | + let fixupPosition = textLine.Start + token.RightColumn |
| 230 | + let interfacePos = Position.fromZ textLine.LineNumber token.RightColumn |
| 231 | + // We rely on the observation that the lastChar of the context should be '}' if that character is present |
| 232 | + let appendBracketAt = |
| 233 | + match sourceText[context.Span.End - 1] with |
| 234 | + | '}' -> None |
| 235 | + | _ -> Some context.Span.End |
| 236 | + |
| 237 | + let interfaceState = |
| 238 | + queryInterfaceState appendBracketAt interfacePos tokens parseResults.ParseTree |
| 239 | + |
| 240 | + match interfaceState with |
| 241 | + | None -> return Seq.empty |
| 242 | + | Some interfaceState -> |
| 243 | + let symbol = |
| 244 | + Tokenizer.getSymbolAtPosition ( |
| 245 | + context.Document.Id, |
| 246 | + sourceText, |
| 247 | + fixupPosition, |
| 248 | + context.Document.FilePath, |
| 249 | + defines, |
| 250 | + SymbolLookupKind.Greedy, |
| 251 | + false, |
| 252 | + false, |
| 253 | + langVersionOpt, |
| 254 | + parsingOptions.StrictIndentation, |
| 255 | + cancellationToken |
| 256 | + ) |
| 257 | + |
| 258 | + match symbol with |
| 259 | + | None -> return Seq.empty |
| 260 | + | Some symbol -> |
| 261 | + let fcsTextLineNumber = textLine.LineNumber + 1 |
| 262 | + let lineContents = textLine.ToString() |
| 263 | + let! options = context.Document.GetOptionsAsync(cancellationToken) |
| 264 | + |
| 265 | + let tabSize = |
| 266 | + options.GetOption(FormattingOptions.TabSize, FSharpConstants.FSharpLanguageName) |
| 267 | + |
| 268 | + let symbolUse = |
| 269 | + checkFileResults.GetSymbolUseAtLocation( |
| 270 | + fcsTextLineNumber, |
| 271 | + symbol.Ident.idRange.EndColumn, |
| 272 | + lineContents, |
| 273 | + symbol.FullIsland |
| 274 | + ) |
| 275 | + |
| 276 | + match symbolUse with |
| 277 | + | None -> return Seq.empty |
| 278 | + | Some symbolUse -> |
| 279 | + match symbolUse.Symbol with |
| 280 | + | :? FSharpEntity as entity when |
| 281 | + // Things get complicated with interface inheritance: https://github.com/dotnet/fsharp/issues/5813 |
| 282 | + // With enough enthusiasm this probably can be handled though, |
| 283 | + // in that case change the check to `InterfaceStubGenerator.IsInterface entity` |
| 284 | + entity.AllInterfaces.Count = 1 |
| 285 | + -> |
| 286 | + |
| 287 | + return! |
| 288 | + getSuggestions ( |
| 289 | + sourceText, |
| 290 | + checkFileResults, |
| 291 | + interfaceState, |
| 292 | + symbolUse.DisplayContext, |
| 293 | + entity, |
| 294 | + tabSize |
| 295 | + ) |
| 296 | + | _ -> return Seq.empty |
| 297 | + } |
0 commit comments