Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fantomas: support cursor api #484

Merged
merged 8 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,64 @@ 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;

if (CurrentVersion < Version60)
return new RdFormatResult(formatResult.Replace("\r\n", args.NewLineText), null);
Comment on lines +195 to +196
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment why this is needed, please?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to pass in lf as part of the configuration btw.
(And override whatever the user had)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a relic of the past. We can try to rewrite it in a separate PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a comment about the version check would stil be good here. With a link to a PR or a commit.


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 +269,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,33 @@ 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there are multiple editors with the same document? This check seems to ignore which editor was actually focused before the reformat. Please try to use data contexts or other ways to get the proper text control.

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();