diff --git a/ReSharper.FSharp/src/FSharp.Fantomas.Host/src/FantomasCodeFormatter.cs b/ReSharper.FSharp/src/FSharp.Fantomas.Host/src/FantomasCodeFormatter.cs index a9b9addc0a..241ab9ae79 100644 --- a/ReSharper.FSharp/src/FSharp.Fantomas.Host/src/FantomasCodeFormatter.cs +++ b/ReSharper.FSharp/src/FSharp.Fantomas.Host/src/FantomasCodeFormatter.cs @@ -24,6 +24,7 @@ internal static class FantomasCodeFormatter private static readonly Version Version45 = Version.Parse("4.5"); private static readonly Version Version46 = Version.Parse("4.6"); + private static readonly Version Version60 = Version.Parse("6.0"); private static Type GetCodeFormatter() => FantomasAssembly @@ -100,12 +101,18 @@ private static Type GetFSharpParsingOptions() return Type.GetType(qualifiedName).NotNull($"{qualifiedName} must exist"); } - private static Type GetFormatConfigType() => - FantomasAssembly + private static Type GetFormatConfigType() + { + var formatConfig = FantomasAssembly .GetType($"{FantomasAssemblyName}.FormatConfig") - .NotNull("FormatConfig must exist") - .GetNestedType("FormatConfig") - .NotNull(); + .NotNull("FormatConfig must exist"); + + return CurrentVersion >= Version60 + ? formatConfig + : formatConfig + .GetNestedType("FormatConfig") + .NotNull(); + } private static readonly Type CodeFormatterType = GetCodeFormatter(); private static readonly Type FSharpParsingOptionsType = GetFSharpParsingOptions(); @@ -118,9 +125,8 @@ private static Type GetFormatConfigType() => private static readonly ConstructorInfo CreateFSharpParsingOptions = FSharpParsingOptionsType?.GetConstructors().Single(); - private static readonly MethodInfo FormatSelectionMethod = CodeFormatterType.GetMethod("FormatSelectionAsync"); - private static readonly MethodInfo FormatDocumentMethod = CodeFormatterType.GetMethod("FormatDocumentAsync"); private static readonly MethodInfo MakeRangeMethod = CodeFormatterType.GetMethod("MakeRange"); + private static readonly MethodInfo MakePositionMethod = CodeFormatterType.GetMethod("MakePosition"); private static readonly MethodInfo SourceOriginConstructor = GetSourceOriginStringConstructor(); private static readonly MethodInfo CreateOptionMethod = @@ -152,35 +158,67 @@ public static string FormatSelection(RdFantomasFormatSelectionArgs args) if (CurrentVersion >= FantomasProtocolConstants.Fantomas5Version) return FSharpAsync.StartAsTask( - FormatSelectionMethod.Invoke(null, new[] - { - args.FileName.EndsWith(".fsi"), // isSignature - args.Source, - range, - ConvertToFormatConfig(args.FormatConfig) - }) as dynamic, null, null) // FSharpAsync> + CodeFormatterType.InvokeMember("FormatSelectionAsync", + BindingFlags.Static | BindingFlags.InvokeMethod | BindingFlags.Public, Type.DefaultBinder, null, new[] + { + args.FileName.EndsWith(".fsi"), // isSignature + args.Source, + range, + ConvertToFormatConfig(args.FormatConfig) + }) as dynamic, null, null) // FSharpAsync> .Result.Item1.Replace("\r\n", args.NewLineText); return FSharpAsync.StartAsTask( - FormatSelectionMethod.Invoke(null, new[] - { - args.FileName, range, - SourceOriginConstructor.Invoke(null, new object[] { args.Source }), - ConvertToFormatConfig(args.FormatConfig), - CreateFSharpParsingOptions.Invoke(GetParsingOptions(args.ParsingOptions).ToArray()), - Checker - }) as FSharpAsync, null, null) + CodeFormatterType.InvokeMember("FormatSelectionAsync", + BindingFlags.Static | BindingFlags.InvokeMethod | BindingFlags.Public, Type.DefaultBinder, null, new[] + { + args.FileName, range, + SourceOriginConstructor.Invoke(null, new object[] { args.Source }), + ConvertToFormatConfig(args.FormatConfig), + CreateFSharpParsingOptions.Invoke(GetParsingOptions(args.ParsingOptions).ToArray()), + Checker + }) as FSharpAsync, null, null) .Result.Replace("\r\n", args.NewLineText); } - public static string FormatDocument(RdFantomasFormatDocumentArgs args) => - FSharpAsync.StartAsTask( - FormatDocumentMethod.Invoke(null, GetFormatDocumentOptions(args)) as FSharpAsync, - null, null) - .Result.Replace("\r\n", args.NewLineText); + public static RdFormatResult FormatDocument(RdFantomasFormatDocumentArgs args) + { + var formatDocumentOptions = GetFormatDocumentOptions(args); + var formatDocumentAsync = CodeFormatterType.InvokeMember("FormatDocumentAsync", + BindingFlags.Static | BindingFlags.InvokeMethod | BindingFlags.Public, Type.DefaultBinder, null, + formatDocumentOptions); + var formatResult = FSharpAsync.StartAsTask((dynamic)formatDocumentAsync, null, null).Result; + + // Prior to version 6, Fantomas returns formatted code, + // in version 6, the return FormatResult value includes the formatted code and the new cursor position. + // https://github.com/fsprojects/fantomas/issues/2727 + if (CurrentVersion < Version60) + return new RdFormatResult(formatResult.Replace("\r\n", args.NewLineText), null); + + var formattedCode = formatResult.Code; + var newCursorPosition = formatResult.Cursor == null + ? null + : new RdFcsPos(formatResult.Cursor.Value.Line - 1, formatResult.Cursor.Value.Column); + return new RdFormatResult(formattedCode.Replace("\r\n", args.NewLineText), newCursorPosition); + } private static object[] GetFormatDocumentOptions(RdFantomasFormatDocumentArgs args) { + if (CurrentVersion >= Version60) + { + var cursorPosition = args.CursorPosition is { } pos + ? MakePositionMethod.Invoke(null, new object[] { pos.Row + 1, pos.Column }) + : null; + + return new[] + { + args.FileName.EndsWith(".fsi"), // isSignature + args.Source, + ConvertToFormatConfig(args.FormatConfig), + cursorPosition + }; + } + if (CurrentVersion >= FantomasProtocolConstants.Fantomas5Version) return new[] { @@ -234,6 +272,7 @@ private static object ConvertToFormatConfig(string[] riderFormatConfigValues) var formatConfig = FSharpValue.MakeRecord(FormatConfigType, formatConfigValues, null); + if (CurrentVersion >= Version60) return formatConfig; return CurrentVersion >= FantomasProtocolConstants.Fantomas5Version ? CreateOptionMethod.Invoke(null, new[] { formatConfig }) //FSharpOption : formatConfig; diff --git a/ReSharper.FSharp/src/FSharp.Fantomas.Host/src/FantomasEndPoint.cs b/ReSharper.FSharp/src/FSharp.Fantomas.Host/src/FantomasEndPoint.cs index c5a57a6bb7..9af40cc965 100644 --- a/ReSharper.FSharp/src/FSharp.Fantomas.Host/src/FantomasEndPoint.cs +++ b/ReSharper.FSharp/src/FSharp.Fantomas.Host/src/FantomasEndPoint.cs @@ -47,7 +47,7 @@ private static string[] GetFormatConfigFields(Unit _) => private static string FormatSelection(RdFantomasFormatSelectionArgs args) => FantomasCodeFormatter.FormatSelection(args); - private static string FormatDocument(RdFantomasFormatDocumentArgs args) => + private static RdFormatResult FormatDocument(RdFantomasFormatDocumentArgs args) => FantomasCodeFormatter.FormatDocument(args); protected override void Run(Lifetime lifetime, RdSimpleDispatcher dispatcher) => dispatcher.Run(); diff --git a/ReSharper.FSharp/src/FSharp.Psi.Features/src/CodeCleanup/FSharpReformatCode.fs b/ReSharper.FSharp/src/FSharp.Psi.Features/src/CodeCleanup/FSharpReformatCode.fs index 3c9cf9e397..acc55393ac 100644 --- a/ReSharper.FSharp/src/FSharp.Psi.Features/src/CodeCleanup/FSharpReformatCode.fs +++ b/ReSharper.FSharp/src/FSharp.Psi.Features/src/CodeCleanup/FSharpReformatCode.fs @@ -4,6 +4,7 @@ open JetBrains.Application.Infra open JetBrains.Diagnostics open JetBrains.DocumentModel open JetBrains.DocumentModel.Impl +open JetBrains.Lifetimes open JetBrains.ProjectModel open JetBrains.ReSharper.Feature.Services.CodeCleanup open JetBrains.ReSharper.Plugins.FSharp.Psi @@ -12,10 +13,11 @@ open JetBrains.ReSharper.Psi open JetBrains.ReSharper.Psi.Tree open JetBrains.ReSharper.Psi.Util open JetBrains.ReSharper.Resources.Shell +open JetBrains.TextControl open JetBrains.Util.Text [] -type FSharpReformatCode() = +type FSharpReformatCode(textControlManager: ITextControlManager) = let REFORMAT_CODE_DESCRIPTOR = CodeCleanupOptionDescriptor( "FSReformatCode", CodeCleanupLanguage("F#", 2), @@ -33,7 +35,7 @@ type FSharpReformatCode() = | CodeCleanupService.DefaultProfileType.REFORMAT | CodeCleanupService.DefaultProfileType.CODE_STYLE -> profile.SetSetting(REFORMAT_CODE_DESCRIPTOR, true) - | _ -> + | _ -> Assertion.Fail($"Unexpected cleanup profile type: {nameof(profileType)}") member x.IsAvailable(sourceFile: IPsiSourceFile) = @@ -65,23 +67,34 @@ type FSharpReformatCode() = let newLineText = sourceFile.DetectLineEnding().GetPresentation() let parsingOptions = fsFile.CheckerService.FcsProjectProvider.GetParsingOptions(sourceFile) - let change = - if isNotNull rangeMarker then - try - let range = ofDocumentRange rangeMarker.DocumentRange - let formatted = - fantomasHost.FormatSelection(filePath, range, text, settings, parsingOptions, newLineText) - let offset = rangeMarker.DocumentRange.StartOffset.Offset - let oldLength = rangeMarker.DocumentRange.Length - Some(DocumentChange(document, offset, oldLength, formatted, stamp, modificationSide)) - with _ -> None - else - let formatted = fantomasHost.FormatDocument(filePath, text, settings, parsingOptions, newLineText) - Some(DocumentChange(document, 0, text.Length, formatted, stamp, modificationSide)) + if isNotNull rangeMarker then + try + let range = ofDocumentRange rangeMarker.DocumentRange + let formatted = fantomasHost.FormatSelection(filePath, range, text, settings, parsingOptions, newLineText) + let offset = rangeMarker.DocumentRange.StartOffset.Offset + let oldLength = rangeMarker.DocumentRange.Length + let documentChange = DocumentChange(document, offset, oldLength, formatted, stamp, modificationSide) + use _ = WriteLockCookie.Create() + document.ChangeDocument(documentChange, TimeStamp.NextValue) + sourceFile.GetPsiServices().Files.CommitAllDocuments() + with _ -> () + else + let textControl = textControlManager.VisibleTextControls + |> Seq.tryFind (fun c -> c.Document == document && c.Window.IsFocused.Value) + let cursorPosition = textControl |> Option.map (fun c -> c.Caret.Position.Value.ToDocLineColumn()) + let formatResult = fantomasHost.FormatDocument(filePath, text, settings, parsingOptions, newLineText, cursorPosition) + let newCursorPosition = formatResult.CursorPosition - match change with - | Some(change) -> - use cookie = WriteLockCookie.Create() - document.ChangeDocument(change, TimeStamp.NextValue) + document.ReplaceText(document.DocumentRange, formatResult.Code) sourceFile.GetPsiServices().Files.CommitAllDocuments() - | _ -> () + + if isNull newCursorPosition then () else + + // move cursor after current document transaction + let moveCursorLifetime = new LifetimeDefinition() + let codeCleanupService = solution.GetComponent() + codeCleanupService.WholeFileCleanupCompletedAfterSave.Advise(moveCursorLifetime.Lifetime, fun _ -> + moveCursorLifetime.Terminate() + textControl.Value.Caret.MoveTo(docLine newCursorPosition.Row, + docColumn newCursorPosition.Column, + CaretVisualPlacement.Generic)) diff --git a/ReSharper.FSharp/src/FSharp.Psi.Features/src/CodeCleanup/FantomasHost.fs b/ReSharper.FSharp/src/FSharp.Psi.Features/src/CodeCleanup/FantomasHost.fs index 00183a83a8..c859753521 100644 --- a/ReSharper.FSharp/src/FSharp.Psi.Features/src/CodeCleanup/FantomasHost.fs +++ b/ReSharper.FSharp/src/FSharp.Psi.Features/src/CodeCleanup/FantomasHost.fs @@ -5,6 +5,7 @@ open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Text open JetBrains.Application.Settings open JetBrains.Core +open JetBrains.DocumentModel open JetBrains.Lifetimes open JetBrains.ProjectModel open JetBrains.Rd.Tasks @@ -49,6 +50,11 @@ type FantomasHost(solution: ISolution, fantomasFactory: FantomasProcessFactory, let toRdFcsRange (range: range) = RdFcsRange(range.FileName, range.StartLine, range.StartColumn, range.EndLine, range.EndColumn) + let toRdFcsPos (caretPosition: DocumentCoords option) = + match caretPosition with + | Some caretPosition -> RdFcsPos(int caretPosition.Line, int caretPosition.Column) + | None -> null + let toRdFormatSettings (settings: FSharpFormatSettingsKey) = [| for field in formatConfigFields -> let fieldName = @@ -79,15 +85,15 @@ type FantomasHost(solution: ISolution, fantomasFactory: FantomasProcessFactory, connect() let args = RdFantomasFormatSelectionArgs(toRdFcsRange range, filePath, source, toRdFormatSettings settings, - toRdFcsParsingOptions options, newLineText) + toRdFcsParsingOptions options, newLineText, null) connection.Execute(fun () -> connection.ProtocolModel.FormatSelection.Sync(args, RpcTimeouts.Maximal)) - member x.FormatDocument(filePath, source, settings, options, newLineText) = + member x.FormatDocument(filePath, source, settings, options, newLineText, cursorPosition: DocumentCoords option) = connect() let args = RdFantomasFormatDocumentArgs(filePath, source, toRdFormatSettings settings, toRdFcsParsingOptions options, - newLineText) + newLineText, toRdFcsPos cursorPosition) connection.Execute(fun () -> connection.ProtocolModel.FormatDocument.Sync(args, RpcTimeouts.Maximal)) diff --git a/rider-fsharp/protocol/src/kotlin/model/RdFantomasModel.kt b/rider-fsharp/protocol/src/kotlin/model/RdFantomasModel.kt index 729f9b66a5..03dfab732b 100644 --- a/rider-fsharp/protocol/src/kotlin/model/RdFantomasModel.kt +++ b/rider-fsharp/protocol/src/kotlin/model/RdFantomasModel.kt @@ -25,17 +25,28 @@ object RdFantomasModel : Root() { field("endCol", int) } + private val rdFcsPos = structdef { + field("row", int) + field("column", int) + } + + private val rdFormatResult = structdef { + field("code", string) + field("cursorPosition", rdFcsPos.nullable) + } + private val rdFantomasFormatArgs = basestruct { field("fileName", string) field("source", string) field("formatConfig", array(string)) field("parsingOptions", rdFcsParsingOptions) field("newLineText", string) + field("cursorPosition", rdFcsPos.nullable) } init { call("getFormatConfigFields", void, array(string)) - call("formatDocument", structdef("rdFantomasFormatDocumentArgs") extends rdFantomasFormatArgs {}, string) + call("formatDocument", structdef("rdFantomasFormatDocumentArgs") extends rdFantomasFormatArgs {}, rdFormatResult) call("formatSelection", structdef("rdFantomasFormatSelectionArgs") extends rdFantomasFormatArgs { field("range", rdFcsRange) }, string) diff --git a/rider-fsharp/src/test/kotlin/com/jetbrains/rider/plugins/fsharp/test/cases/fantomas/FantomasRunOptionsTest.kt b/rider-fsharp/src/test/kotlin/com/jetbrains/rider/plugins/fsharp/test/cases/fantomas/FantomasRunOptionsTest.kt index 37a7039d97..7d46161726 100644 --- a/rider-fsharp/src/test/kotlin/com/jetbrains/rider/plugins/fsharp/test/cases/fantomas/FantomasRunOptionsTest.kt +++ b/rider-fsharp/src/test/kotlin/com/jetbrains/rider/plugins/fsharp/test/cases/fantomas/FantomasRunOptionsTest.kt @@ -194,6 +194,19 @@ class FantomasRunOptionsTest : EditorTestBase() { "5.2.1.0" ) + @Test + fun `local tool 6_0 with cursor`() { + withOpenedEditor("Simple.fs", "LargeFile.fs") { + withFantomasLocalTool("fantomas", "6.0.0-alpha-004") { + executeWithGold(testGoldFile) { + reformatCode() + checkFantomasVersion("6.0.0.0") + dumpOpenedDocument(it, project!!, true) + } + } + } + } + @Test fun `global tool`() { executeWithGold(testGoldFile) { diff --git a/rider-fsharp/testData/fantomas/FantomasRunOptionsTest/local tool 6_0 with cursor/gold/local tool 6_0 with cursor.gold b/rider-fsharp/testData/fantomas/FantomasRunOptionsTest/local tool 6_0 with cursor/gold/local tool 6_0 with cursor.gold new file mode 100644 index 0000000000..a22310452f --- /dev/null +++ b/rider-fsharp/testData/fantomas/FantomasRunOptionsTest/local tool 6_0 with cursor/gold/local tool 6_0 with cursor.gold @@ -0,0 +1,29 @@ +namespace JetBrains.ReSharper.Plugins.FSharp.Services.Formatter + +[] +type FSharpReformatCode(textControlManager: ITextControlManager) = + member x.Process(sourceFile, rangeMarker, _, _, _) = + if isNotNull rangeMarker then + try + let range = ofDocumentRange rangeMarker.DocumentRange + + let formatted = + fantomasHost.FormatSelection(filePath, range, text, settings, parsingOptions, newLineText) + + let offset = rangeMarker.DocumentRange.StartOffset.Offset + let oldLength = rangeMarker.DocumentRange.Length + + let documentChange = + DocumentChange(document, offset, oldLength, formatted, stamp, modificationSide) + + use _ = WriteLockCookie.Create() + document.ChangeDocument(documentChange, TimeStamp.NextValue) + sourceFile.GetPsiServices().Files.CommitAllDocuments() + with _ -> + () + else + let textControl = + textControlManager.VisibleTextControls + |> Seq.find (fun c -> c.Document == document) + + cursorPosition = textControl.Caret.Position.Value.ToDocLineColumn() diff --git a/rider-fsharp/testData/fantomas/FantomasRunOptionsTest/local tool 6_0 with cursor/source/LargeFile.fs b/rider-fsharp/testData/fantomas/FantomasRunOptionsTest/local tool 6_0 with cursor/source/LargeFile.fs new file mode 100644 index 0000000000..2c5e0136bd --- /dev/null +++ b/rider-fsharp/testData/fantomas/FantomasRunOptionsTest/local tool 6_0 with cursor/source/LargeFile.fs @@ -0,0 +1,19 @@ +namespace JetBrains.ReSharper.Plugins.FSharp.Services.Formatter + +[] +type FSharpReformatCode(textControlManager: ITextControlManager) = + member x.Process(sourceFile, rangeMarker, _, _, _) = + if isNotNull rangeMarker then + try + let range = ofDocumentRange rangeMarker.DocumentRange + let formatted = fantomasHost.FormatSelection(filePath, range, text, settings, parsingOptions, newLineText) + let offset = rangeMarker.DocumentRange.StartOffset.Offset + let oldLength = rangeMarker.DocumentRange.Length + let documentChange = DocumentChange(document, offset, oldLength, formatted, stamp, modificationSide) + use _ = WriteLockCookie.Create() + document.ChangeDocument(documentChange, TimeStamp.NextValue) + sourceFile.GetPsiServices().Files.CommitAllDocuments() + with _ -> () + else + let textControl = textControlManager.VisibleTextControls |> Seq.find (fun c -> c.Document == document) + cursorPosition = textControl.Caret.Position.Value.ToDocLineColumn();