diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index ba03f663967..fe59843a4fc 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -1,5 +1,6 @@ ### Added +* Refactoring to convert a file written as `namespace A.B` with a single nested `module C =` into a root-level `module A.B.C`, and a root-level `module A.B.C` back into `namespace A.B` with a nested `module C =`. ([PR #20536](https://github.com/dotnet/fsharp/pull/20536)) * Code-fixes for FS3888 (compiler-semantic attribute on the `.fs` but not the `.fsi`): copy the attribute into the `.fsi`, or remove it from the `.fs`. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880)) * Expand `` in IDE tooltips, completion, and signature help, inheriting XML documentation from base classes, interfaces, overridden members, and constructors. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188)) diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 319bdd5a264..eb83c75fba2 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -105,6 +105,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx index 1f1f632d770..c2c49323b9c 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx @@ -368,4 +368,10 @@ Use live (unsaved) buffers for analysis Returns: + + Convert to 'module {0}' + + + Convert to 'namespace {0}' with nested module '{1}' + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Refactor/ConvertNamespaceModule.fs b/vsintegration/src/FSharp.Editor/Refactor/ConvertNamespaceModule.fs new file mode 100644 index 00000000000..639ac929ceb --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/ConvertNamespaceModule.fs @@ -0,0 +1,329 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Collections.Generic +open System.Composition + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.CodeActions +open Microsoft.CodeAnalysis.CodeRefactorings +open Microsoft.CodeAnalysis.Formatting +open Microsoft.CodeAnalysis.Text +open Microsoft.VisualStudio.FSharp.Editor.Telemetry + +open FSharp.Compiler.Syntax +open FSharp.Compiler.SyntaxTrivia +open FSharp.Compiler.Text + +open CancellableTasks + +[] +module private NamespaceModuleConversion = + + [] + type Shape = + | Nested of + namespacePath: LongIdent * + namespaceIsRecursive: bool * + namespaceKeyword: range * + opens: range list * + moduleIdent: Ident * + moduleIsRecursive: bool * + moduleKeyword: range * + equals: range * + bodyColumn: int + | Root of path: LongIdent * keyword: range * moduleRange: range + + let spanOf (sourceText: SourceText) (m: range) = + RoslynHelpers.FSharpRangeToTextSpan(sourceText, m) + + let textBetween (sourceText: SourceText) (first: Ident) (last: Ident) = + sourceText.ToString(TextSpan.FromBounds((spanOf sourceText first.idRange).Start, (spanOf sourceText last.idRange).End)) + + let isBlank (sourceText: SourceText) start finish = + String.IsNullOrWhiteSpace(sourceText.ToString(TextSpan.FromBounds(start, finish))) + + let restOfLineIsBlank (sourceText: SourceText) (m: range) = + let position = (spanOf sourceText m).End + isBlank sourceText position (sourceText.Lines.GetLineFromPosition position).End + + /// The declarations of a namespace are a single nested module, optionally preceded by `open`s that would + /// apply equally to a root module: moving them into it changes nothing they resolve against. + let private tryOpensBeforeNestedModule (decls: SynModuleDecl list) = + match List.rev decls with + | nestedModule :: reversedOpens when + reversedOpens + |> List.forall (function + | SynModuleDecl.Open _ -> true + | _ -> false) + -> + match nestedModule with + | SynModuleDecl.NestedModule _ -> Some(reversedOpens |> List.rev |> List.map _.Range, nestedModule) + | _ -> None + | _ -> None + + let tryShape (sourceText: SourceText) (parseTree: ParsedInput) = + match parseTree with + | ParsedInput.ImplFile(ParsedImplFileInput( + contents = [ SynModuleOrNamespace( + longId = namespacePath + isRecursive = namespaceIsRecursive + kind = SynModuleOrNamespaceKind.DeclaredNamespace + decls = decls + trivia = namespaceTrivia) ])) -> + match tryOpensBeforeNestedModule decls with + | Some(opens, + SynModuleDecl.NestedModule( + moduleInfo = moduleInfo + isRecursive = moduleIsRecursive + decls = (firstDeclaration :: _ as declarations) + range = moduleRange + trivia = moduleTrivia)) -> + match namespaceTrivia.LeadingKeyword, moduleInfo.LongIdent, moduleTrivia.ModuleKeyword, moduleTrivia.EqualsRange with + | SynModuleOrNamespaceLeadingKeyword.Namespace namespaceKeyword, [ moduleIdent ], Some moduleKeyword, Some equals when + namespaceKeyword.StartColumn = 0 + && moduleKeyword.StartColumn = 0 + && moduleKeyword.StartLine = equals.EndLine + && firstDeclaration.Range.StartLine > equals.EndLine + && Position.posEq moduleRange.End (List.last declarations).Range.End + && restOfLineIsBlank sourceText (List.last namespacePath).idRange + -> + ValueSome( + Nested( + namespacePath, + namespaceIsRecursive, + namespaceKeyword, + opens, + moduleIdent, + moduleIsRecursive, + moduleKeyword, + equals, + firstDeclaration.Range.StartColumn + ) + ) + | _ -> ValueNone + | _ -> ValueNone + + | ParsedInput.ImplFile(ParsedImplFileInput( + contents = [ SynModuleOrNamespace( + longId = (_ :: _ :: _ as path) + kind = SynModuleOrNamespaceKind.NamedModule + decls = firstDeclaration :: _ + range = moduleRange + trivia = moduleTrivia) ])) -> + match moduleTrivia.LeadingKeyword with + | SynModuleOrNamespaceLeadingKeyword.Module keyword when + keyword.StartColumn = 0 + && (List.last path).idRange.EndLine = keyword.StartLine + && firstDeclaration.Range.StartLine > keyword.StartLine + -> + ValueSome(Root(path, keyword, moduleRange)) + | _ -> ValueNone + + | _ -> ValueNone + + let isOnHeader (caretLine: int) shape = + match shape with + | Nested(namespaceKeyword = namespaceKeyword; moduleKeyword = moduleKeyword) -> + caretLine = namespaceKeyword.StartLine || caretLine = moduleKeyword.StartLine + | Root(keyword = keyword) -> caretLine = keyword.StartLine + + let dotted (idents: LongIdent) = + idents |> List.map _.idText |> String.concat "." + + let title shape = + match shape with + | Nested(namespacePath = namespacePath; moduleIdent = moduleIdent) -> + String.Format(SR.ConvertToRootModule(), $"{dotted namespacePath}.{moduleIdent.idText}") + | Root(path = path) -> + String.Format(SR.ConvertToNamespaceWithNestedModule(), dotted (List.take (path.Length - 1) path), (List.last path).idText) + + let linesInsideLiterals (parseTree: ParsedInput) = + (HashSet(), parseTree) + ||> ParsedInput.fold (fun lines _ node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Const(range = m)) + | SyntaxNode.SynExpr(SynExpr.InterpolatedString(range = m)) + | SyntaxNode.SynPat(SynPat.Const(range = m)) -> + for line in m.StartLine + 1 .. m.EndLine do + lines.Add(Line.toZ line) |> ignore + | _ -> () + + lines) + + let leadingSpaces (sourceText: SourceText) (line: TextLine) = + let mutable position = line.Start + + while position < line.End && sourceText[position] = ' ' do + position <- position + 1 + + position - line.Start + + let lineBreakOf (sourceText: SourceText) (line: TextLine) = + match line.EndIncludingLineBreak - line.End with + | 0 -> Environment.NewLine + | length -> sourceText.ToString(TextSpan(line.End, length)) + + let changes (sourceText: SourceText) (parseTree: ParsedInput) (indentSize: int) shape = + let lines = sourceText.Lines + let literalLines = linesInsideLiterals parseTree + + match shape with + | Nested( + namespacePath = namespacePath + namespaceIsRecursive = namespaceIsRecursive + namespaceKeyword = namespaceKeyword + opens = opens + moduleIdent = moduleIdent + moduleIsRecursive = moduleIsRecursive + equals = equals + bodyColumn = bodyColumn) -> + let namespaceLine = Line.toZ namespaceKeyword.StartLine + let headerLine = Line.toZ equals.EndLine + let lineBreak = lineBreakOf sourceText lines[headerLine] + + let openLines = + opens + |> List.collect (fun m -> [ Line.toZ m.StartLine .. Line.toZ m.EndLine ]) + |> Set.ofList + + // The namespace line and the moved `open`s are deleted, each with the blank lines right after it; + // comments, XML doc comments and attributes among them stay in front of the new header. + let deletedRanges = + let removedLines = openLines.Add namespaceLine + + let deletedLines = + ((false, []), [ namespaceLine .. headerLine - 1 ]) + ||> List.fold (fun (afterDeleted, deleted) i -> + if + removedLines.Contains i + || (afterDeleted && String.IsNullOrWhiteSpace(lines[i].ToString())) + then + true, i :: deleted + else + false, deleted) + |> snd + + ([], deletedLines) + ||> List.fold (fun ranges i -> + match ranges with + | struct (first, last) :: rest when first = i + 1 -> struct (i, last) :: rest + | _ -> struct (i, i) :: ranges) + + let moduleSpan = spanOf sourceText moduleIdent.idRange + let equalsEnd = (spanOf sourceText equals).End + + let headerEnd = + if isBlank sourceText equalsEnd lines[headerLine].End then + lines[headerLine].End + else + equalsEnd + + let recursive = + if namespaceIsRecursive && not moduleIsRecursive then + "rec " + else + "" + + let rootPath = + $"{recursive}{textBetween sourceText namespacePath.Head (List.last namespacePath)}.{sourceText.ToString moduleSpan}" + + [ + // A root-style `module A.B.C` must be the file's first declaration, so the `open`s move to right + // after the header. + for struct (first, last) in deletedRanges do + TextChange(TextSpan.FromBounds(lines[first].Start, lines[last].EndIncludingLineBreak), "") + + TextChange(TextSpan.FromBounds(moduleSpan.Start, headerEnd), rootPath) + + if not (List.isEmpty opens) then + let openText = + opens + |> List.map (fun m -> sourceText.ToString(spanOf sourceText m)) + |> String.concat lineBreak + + TextChange(TextSpan(lines[headerLine].EndIncludingLineBreak, 0), $"{lineBreak}{openText}{lineBreak}{lineBreak}") + + for i in headerLine + 1 .. lines.Count - 1 do + let removed = min bodyColumn (leadingSpaces sourceText lines[i]) + + if removed > 0 && not (literalLines.Contains i) then + TextChange(TextSpan(lines[i].Start, removed), "") + ] + + | Root(path, keyword, moduleRange) -> + let lastIdent = List.last path + let namespaceIdents = List.take (path.Length - 1) path + let lineBreak = lineBreakOf sourceText lines[Line.toZ keyword.StartLine] + let lastSpan = spanOf sourceText lastIdent.idRange + let padding = String(' ', indentSize) + + [ + TextChange( + TextSpan(lines[Line.toZ moduleRange.StartLine].Start, 0), + $"namespace {textBetween sourceText path.Head (List.last namespaceIdents)}{lineBreak}{lineBreak}" + ) + TextChange( + TextSpan.FromBounds((spanOf sourceText path.Head.idRange).Start, lastSpan.End), + $"{sourceText.ToString lastSpan} =" + ) + + for i in Line.toZ keyword.StartLine + 1 .. lines.Count - 1 do + if not (String.IsNullOrWhiteSpace(lines[i].ToString()) || literalLines.Contains i) then + TextChange(TextSpan(lines[i].Start, 0), padding) + ] + +[] +type internal FSharpConvertNamespaceModuleRefactoring [] () = + inherit CodeRefactoringProvider() + + static let hasSignatureFile (document: Document) = + let signaturePath = document.FilePath + "i" + + document.Project.Documents + |> Seq.exists (fun d -> String.Equals(d.FilePath, signaturePath, StringComparison.OrdinalIgnoreCase)) + + override _.ComputeRefactoringsAsync context = + cancellableTask { + let document = context.Document + + if not (document.IsFSharpSignatureFile || document.IsFSharpScript) then + let! cancellationToken = CancellableTask.getCancellationToken () + let! sourceText = document.GetTextAsync cancellationToken + let! parseResults = document.GetFSharpParseResultsAsync(nameof FSharpConvertNamespaceModuleRefactoring) + + let caretLine = + Line.fromZ (sourceText.Lines.GetLineFromPosition context.Span.Start).LineNumber + + match NamespaceModuleConversion.tryShape sourceText parseResults.ParseTree with + | ValueSome shape when + NamespaceModuleConversion.isOnHeader caretLine shape + && not (hasSignatureFile document) + -> + let title = NamespaceModuleConversion.title shape + + let changedDocument = + cancellableTask { + let! cancellationToken = CancellableTask.getCancellationToken () + let! options = document.GetOptionsAsync cancellationToken + + let indentSize = + options.GetOption(FormattingOptions.IndentationSize, FSharpConstants.FSharpLanguageName) + + TelemetryReporter.ReportSingleEvent( + TelemetryEvents.RefactoringActivated, + [| "name", box (nameof FSharpConvertNamespaceModuleRefactoring) |] + ) + + let changes = + NamespaceModuleConversion.changes sourceText parseResults.ParseTree indentSize shape + + return document.WithText(sourceText.WithChanges changes) + } + + context.RegisterRefactoring(CodeAction.Create(title, changedDocument, title)) + | _ -> () + } + |> CancellableTask.startAsTask context.CancellationToken diff --git a/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs b/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs index 230f4988438..a5bf5e6b4ef 100644 --- a/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs +++ b/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs @@ -18,6 +18,9 @@ module TelemetryEvents = [] let CodefixActivated = "codefixactivated" + [] + let RefactoringActivated = "refactoringactivated" + [] let Hints = "hints" diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf index cd8c46bf705..631fb0a2105 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf @@ -90,11 +90,21 @@ Navrhnout názvy pro nerozpoznané identifikátory; Převést na anonymní záznam + + Convert to 'namespace {0}' with nested module '{1}' + Convert to 'namespace {0}' with nested module '{1}' + + Use '<>' for inequality check Pro kontrolu nerovnosti použijte <>. + + Convert to 'module {0}' + Convert to 'module {0}' + + Use '=' for equality check Pro kontrolu rovnosti použijte =. diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf index bce1941f0b1..acfc82e475f 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf @@ -90,11 +90,21 @@ Namen für nicht aufgelöste Bezeichner vorschlagen; In anonymen Datensatz konvertieren + + Convert to 'namespace {0}' with nested module '{1}' + Convert to 'namespace {0}' with nested module '{1}' + + Use '<>' for inequality check "<>" für die Überprüfung auf Ungleichheit verwenden + + Convert to 'module {0}' + Convert to 'module {0}' + + Use '=' for equality check "=" für Gleichheitsüberprüfung verwenden diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf index fa8cb62c422..833256024fe 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf @@ -90,11 +90,21 @@ Sugerir nombres para identificadores sin resolver; Convertir en registro anónimo + + Convert to 'namespace {0}' with nested module '{1}' + Convert to 'namespace {0}' with nested module '{1}' + + Use '<>' for inequality check Usar "<>" para la comprobación de desigualdad + + Convert to 'module {0}' + Convert to 'module {0}' + + Use '=' for equality check Usar "=" para la comprobación de igualdad diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf index e7ec71e839e..cf7767bbf68 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf @@ -90,11 +90,21 @@ Suggérer des noms pour les identificateurs non résolus ; Convertir en enregistrement anonyme + + Convert to 'namespace {0}' with nested module '{1}' + Convert to 'namespace {0}' with nested module '{1}' + + Use '<>' for inequality check Utiliser '<>' pour vérifier l'inégalité + + Convert to 'module {0}' + Convert to 'module {0}' + + Use '=' for equality check Utiliser '=' pour vérifier l'égalité diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf index 327a7ca362f..584de05fd36 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf @@ -90,11 +90,21 @@ Suggerisci i nomi per gli identificatori non risolti; Converti in record anonimo + + Convert to 'namespace {0}' with nested module '{1}' + Convert to 'namespace {0}' with nested module '{1}' + + Use '<>' for inequality check Usare '<>' per il controllo di disuguaglianza + + Convert to 'module {0}' + Convert to 'module {0}' + + Use '=' for equality check Usare '=' per il controllo di uguaglianza diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf index d45234c011a..23f5583fa68 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf @@ -90,11 +90,21 @@ Suggest names for unresolved identifiers; 匿名レコードに変換 + + Convert to 'namespace {0}' with nested module '{1}' + Convert to 'namespace {0}' with nested module '{1}' + + Use '<>' for inequality check 非等値のチェックには '<>' を使用します + + Convert to 'module {0}' + Convert to 'module {0}' + + Use '=' for equality check 等値性のチェックには '=' を使用します diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf index 3248e0641ea..84c85ae096a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf @@ -90,11 +90,21 @@ Suggest names for unresolved identifiers; 익명 레코드로 변환 + + Convert to 'namespace {0}' with nested module '{1}' + Convert to 'namespace {0}' with nested module '{1}' + + Use '<>' for inequality check 같지 않음 검사에 '<>' 사용 + + Convert to 'module {0}' + Convert to 'module {0}' + + Use '=' for equality check 같음 검사에 '=' 사용 diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf index abc39f15da5..fdd7115f934 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf @@ -90,11 +90,21 @@ Sugeruj nazwy dla nierozpoznanych identyfikatorów; Konwertuj na rekord anonimowy + + Convert to 'namespace {0}' with nested module '{1}' + Convert to 'namespace {0}' with nested module '{1}' + + Use '<>' for inequality check Użyj operatora „<>” do sprawdzenia nierówności + + Convert to 'module {0}' + Convert to 'module {0}' + + Use '=' for equality check Użyj znaku „=” w celu sprawdzenia równości diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf index dfde43120f5..9aaaab84236 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf @@ -90,11 +90,21 @@ Sugerir nomes para identificadores não resolvidos; Converter em Registro Anônimo + + Convert to 'namespace {0}' with nested module '{1}' + Convert to 'namespace {0}' with nested module '{1}' + + Use '<>' for inequality check Usar '<>' para a verificação de desigualdade + + Convert to 'module {0}' + Convert to 'module {0}' + + Use '=' for equality check Usar '=' para verificação de igualdade diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf index 47cda215312..930f1928d3e 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf @@ -90,11 +90,21 @@ Suggest names for unresolved identifiers; Преобразовать в анонимную запись + + Convert to 'namespace {0}' with nested module '{1}' + Convert to 'namespace {0}' with nested module '{1}' + + Use '<>' for inequality check Используйте "<>" для проверки на неравенство + + Convert to 'module {0}' + Convert to 'module {0}' + + Use '=' for equality check Используйте "=" для проверки равенства diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf index 58aa5d54c43..d39eb549563 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf @@ -90,11 +90,21 @@ Kullanılmayan değerleri analiz et ve bunlara düzeltmeler öner; Anonim Kayda Dönüştür + + Convert to 'namespace {0}' with nested module '{1}' + Convert to 'namespace {0}' with nested module '{1}' + + Use '<>' for inequality check Eşitsizlik denetimi için '<>' kullanın + + Convert to 'module {0}' + Convert to 'module {0}' + + Use '=' for equality check Eşitlik denetimi için '=' kullan diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf index 4fa703776fb..f79959235af 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf @@ -90,11 +90,21 @@ Suggest names for unresolved identifiers; 转换为匿名记录 + + Convert to 'namespace {0}' with nested module '{1}' + Convert to 'namespace {0}' with nested module '{1}' + + Use '<>' for inequality check 使用 "<>" 进行不相等检查 + + Convert to 'module {0}' + Convert to 'module {0}' + + Use '=' for equality check 使用 "=" 进行同等性检查 diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf index fd46ef9919a..48aa72e3a20 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf @@ -90,11 +90,21 @@ Suggest names for unresolved identifiers; 轉換為匿名記錄 + + Convert to 'namespace {0}' with nested module '{1}' + Convert to 'namespace {0}' with nested module '{1}' + + Use '<>' for inequality check 使用 '<>' 進行不等式檢查 + + Convert to 'module {0}' + Convert to 'module {0}' + + Use '=' for equality check 使用 '=' 檢查是否相等 diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index ecce1205b8c..4210d42c07a 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -74,6 +74,7 @@ + diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertNamespaceModuleTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertNamespaceModuleTests.fs new file mode 100644 index 00000000000..3db51cc4ba4 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertNamespaceModuleTests.fs @@ -0,0 +1,351 @@ +module FSharp.Editor.Tests.Refactors.ConvertNamespaceModuleTests + +open System +open System.Threading + +open Microsoft.CodeAnalysis.CodeActions +open Microsoft.CodeAnalysis.CodeRefactorings +open Microsoft.CodeAnalysis.Text + +open Microsoft.VisualStudio.FSharp.Editor + +open Xunit + +open FSharp.Editor.Tests.Helpers +open FSharp.Editor.Tests.Refactors.RefactorTestFramework + +let private caretAt (code: string) (marker: string) = + code.IndexOf(marker, StringComparison.Ordinal) + +let private refactored (code: string) (marker: string) = + use context = TestContext.CreateWithCode code + + let document = + tryRefactor code (caretAt code marker) context (new FSharpConvertNamespaceModuleRefactoring()) + + (document.GetTextAsync() |> GetTaskResult).ToString() + +let private actionsAt (code: string) (marker: string) = + use context = TestContext.CreateWithCode code + tryGetRefactoringActions code (caretAt code marker) context (new FSharpConvertNamespaceModuleRefactoring()) + +// Both contain a triple-quoted string, which a triple-quoted literal cannot hold. +let private nestedHelpers = + "namespace My.Company\n\n/// Utilities.\n[]\nmodule private Helpers =\n let inline twice x = x + x\n let banner = \"\"\"\n not\n touched\"\"\"\n" + +let private rootHelpers = + "/// Utilities.\n[]\nmodule private My.Company.Helpers\nlet inline twice x = x + x\nlet banner = \"\"\"\n not\n touched\"\"\"\n" + +[] +[] +[] +let ``Namespace with a single nested module converts to a root module`` (marker: string) = + Assert.Equal(rootHelpers, refactored nestedHelpers marker) + +[] +let ``Root module converts to a namespace with a nested module`` () = + Assert.Equal(nestedHelpers, refactored rootHelpers "module") + +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +let ``Nested module converts to a root module`` (before: string, after: string) = + Assert.Equal(after, refactored before "namespace") + +[] +[] +[] +[] internal A.B.C +let x = 1 +""", + """ +namespace A.B + +module [] internal C = + let x = 1 +""")>] +[] +[] +let ``Root module converts to a nested module`` (before: string, after: string) = + Assert.Equal(after, refactored before "module") + +[] +let ``Converting to a nested module and back restores the root module`` () = + let original = + """ +module A.B.C + +let x = 1 +""" + + Assert.Equal(original, refactored (refactored original "module") "namespace") + +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +let ``No action`` (code: string, marker: string) = Assert.Empty(actionsAt code marker) + +[] +let ``No action when the file has a signature`` () = + let code = + """ +namespace A.B + +module C = + let x = 1 +""" + + let signature = + """ +namespace A.B + +module C = + val x: int +""" + + let document = RoslynTestHelpers.GetFsiAndFsDocuments signature code |> Seq.last + let actions = ResizeArray() + + let context = + CodeRefactoringContext(document, TextSpan(caretAt code "namespace", 1), (fun action -> actions.Add action), CancellationToken.None) + + (new FSharpConvertNamespaceModuleRefactoring()).ComputeRefactoringsAsync(context).GetAwaiter().GetResult() + + Assert.False(document.IsFSharpSignatureFile) + Assert.Empty(actions)