Skip to content

Commit

Permalink
Fantomas: support cursor api (#484)
Browse files Browse the repository at this point in the history
  • Loading branch information
DedSec256 authored Mar 3, 2023
1 parent 072d10f commit 6f52ffa
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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 =
Expand Down Expand Up @@ -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<Tuple<string, Range>>
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<Tuple<string, Range>>
.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<string>, 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<string>, null, null)
.Result.Replace("\r\n", args.NewLineText);
}

public static string FormatDocument(RdFantomasFormatDocumentArgs args) =>
FSharpAsync.StartAsTask(
FormatDocumentMethod.Invoke(null, GetFormatDocumentOptions(args)) as FSharpAsync<string>,
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[]
{
Expand Down Expand Up @@ -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>
: formatConfig;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

[<CodeCleanupModule>]
type FSharpReformatCode() =
type FSharpReformatCode(textControlManager: ITextControlManager) =
let REFORMAT_CODE_DESCRIPTOR = CodeCleanupOptionDescriptor<bool>(
"FSReformatCode",
CodeCleanupLanguage("F#", 2),
Expand All @@ -33,7 +35,7 @@ type FSharpReformatCode() =
| CodeCleanupService.DefaultProfileType.REFORMAT
| CodeCleanupService.DefaultProfileType.CODE_STYLE ->
profile.SetSetting<bool>(REFORMAT_CODE_DESCRIPTOR, true)
| _ ->
| _ ->
Assertion.Fail($"Unexpected cleanup profile type: {nameof(profileType)}")

member x.IsAvailable(sourceFile: IPsiSourceFile) =
Expand Down Expand Up @@ -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>()
codeCleanupService.WholeFileCleanupCompletedAfterSave.Advise(moveCursorLifetime.Lifetime, fun _ ->
moveCursorLifetime.Terminate()
textControl.Value.Caret.MoveTo(docLine newCursorPosition.Row,
docColumn newCursorPosition.Column,
CaretVisualPlacement.Generic))
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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))

Expand Down
13 changes: 12 additions & 1 deletion rider-fsharp/protocol/src/kotlin/model/RdFantomasModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace JetBrains.ReSharper.Plugins.FSharp.Services.Formatter

[<CodeCleanupModule>]
type FSharpReformatCode(textControlManager: ITextControlManager) =
member x.Process(sourceFile, rangeMarker, _, _, _) =
if isNotNull rangeMarker then
try<caret>
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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace JetBrains.ReSharper.Plugins.FSharp.Services.Formatter

[<CodeCleanupModule>]
type FSharpReformatCode(textControlManager: ITextControlManager) =
member x.Process(sourceFile, rangeMarker, _, _, _) =
if isNotNull rangeMarker then
try<caret>
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();

0 comments on commit 6f52ffa

Please sign in to comment.