Skip to content
This repository was archived by the owner on Dec 23, 2024. It is now read-only.

Commit 34c3bcc

Browse files
saulcartermp
authored andcommitted
Add editor formatting service to auto-deindent closing brackets (dotnet#3313)
* Add editor formatting service for auto-deindent * Minor refactor of the indentation service - do not indent after 'function' * Only use smart indentation if indent style is set to 'Smart' * Fix broken unit test build * Implement review comments, fix build * Fix some broken brace matching tests Still WIP, other tests still broken * Fix failing indentation tests * Add formatting service tests * Add more brace matching tests Fixes dotnet#2092
1 parent ac1b856 commit 34c3bcc

File tree

4 files changed

+169
-39
lines changed

4 files changed

+169
-39
lines changed

FSharp.Editor.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
<Compile Include="Classification\ColorizationService.fs" />
5656
<Compile Include="Formatting\BraceMatchingService.fs" />
5757
<Compile Include="Formatting\IndentationService.fs" />
58+
<Compile Include="Formatting\EditorFormattingService.fs" />
5859
<Compile Include="Debugging\BreakpointResolutionService.fs" />
5960
<Compile Include="Debugging\LanguageDebugInfoService.fs" />
6061
<Compile Include="Diagnostics\DocumentDiagnosticAnalyzer.fs" />

Formatting/BraceMatchingService.fs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@ type internal FSharpBraceMatchingService
1515
projectInfoManager: FSharpProjectOptionsManager
1616
) =
1717

18-
static let userOpName = "BraceMatching"
19-
static member GetBraceMatchingResult(checker: FSharpChecker, sourceText, fileName, options, position: int) =
18+
19+
static let defaultUserOpName = "BraceMatching"
20+
21+
static member GetBraceMatchingResult(checker: FSharpChecker, sourceText, fileName, options, position: int, userOpName: string) =
2022
async {
21-
let! matchedBraces = checker.MatchBraces(fileName, sourceText.ToString(), options, userOpName = userOpName)
23+
let! matchedBraces = checker.MatchBraces(fileName, sourceText.ToString(), options, userOpName)
2224
let isPositionInRange range =
2325
match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, range) with
2426
| None -> false
25-
| Some range -> range.Contains(position)
27+
| Some range ->
28+
let length = position - range.Start
29+
length >= 0 && length <= range.Length
2630
return matchedBraces |> Array.tryFind(fun (left, right) -> isPositionInRange left || isPositionInRange right)
2731
}
2832

@@ -31,7 +35,7 @@ type internal FSharpBraceMatchingService
3135
asyncMaybe {
3236
let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document)
3337
let! sourceText = document.GetTextAsync(cancellationToken)
34-
let! (left, right) = FSharpBraceMatchingService.GetBraceMatchingResult(checkerProvider.Checker, sourceText, document.Name, options, position)
38+
let! (left, right) = FSharpBraceMatchingService.GetBraceMatchingResult(checkerProvider.Checker, sourceText, document.Name, options, position, defaultUserOpName)
3539
let! leftSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, left)
3640
let! rightSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, right)
3741
return BraceMatchingResult(leftSpan, rightSpan)
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
2+
3+
namespace Microsoft.VisualStudio.FSharp.Editor
4+
5+
open System.Composition
6+
open System.Collections.Generic
7+
8+
open Microsoft.CodeAnalysis
9+
open Microsoft.CodeAnalysis.Editor
10+
open Microsoft.CodeAnalysis.Formatting
11+
open Microsoft.CodeAnalysis.Host.Mef
12+
open Microsoft.CodeAnalysis.Text
13+
14+
open Microsoft.FSharp.Compiler.SourceCodeServices
15+
open System.Threading
16+
17+
[<Shared>]
18+
[<ExportLanguageService(typeof<IEditorFormattingService>, FSharpConstants.FSharpLanguageName)>]
19+
type internal FSharpEditorFormattingService
20+
[<ImportingConstructor>]
21+
(
22+
checkerProvider: FSharpCheckerProvider,
23+
projectInfoManager: FSharpProjectOptionsManager
24+
) =
25+
26+
static member GetFormattingChanges(documentId: DocumentId, sourceText: SourceText, filePath: string, checker: FSharpChecker, indentStyle: FormattingOptions.IndentStyle, projectOptions: FSharpProjectOptions option, position: int) =
27+
// Logic for determining formatting changes:
28+
// If first token on the current line is a closing brace,
29+
// match the indent with the indent on the line that opened it
30+
31+
asyncMaybe {
32+
33+
// Gate formatting on whether smart indentation is enabled
34+
// (this is what C# does)
35+
do! Option.guard (indentStyle = FormattingOptions.IndentStyle.Smart)
36+
37+
let! projectOptions = projectOptions
38+
39+
let line = sourceText.Lines.[sourceText.Lines.IndexOf position]
40+
41+
let defines = CompilerEnvironment.GetCompilationDefinesForEditing(filePath, projectOptions.OtherOptions |> List.ofArray)
42+
43+
let tokens = Tokenizer.tokenizeLine(documentId, sourceText, line.Start, filePath, defines)
44+
45+
let! firstMeaningfulToken =
46+
tokens
47+
|> List.tryFind (fun x ->
48+
x.Tag <> FSharpTokenTag.WHITESPACE &&
49+
x.Tag <> FSharpTokenTag.COMMENT &&
50+
x.Tag <> FSharpTokenTag.LINE_COMMENT)
51+
52+
let! (left, right) =
53+
FSharpBraceMatchingService.GetBraceMatchingResult(checker, sourceText, filePath, projectOptions, position, "FormattingService")
54+
55+
if right.StartColumn = firstMeaningfulToken.LeftColumn then
56+
// Replace the indentation on this line with the indentation of the left bracket
57+
let! leftSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, left)
58+
59+
let indentChars (line : TextLine) =
60+
line.ToString()
61+
|> Seq.takeWhile ((=) ' ')
62+
|> Seq.length
63+
64+
let startIndent = indentChars sourceText.Lines.[sourceText.Lines.IndexOf leftSpan.Start]
65+
let currentIndent = indentChars line
66+
67+
return TextChange(TextSpan(line.Start, currentIndent), String.replicate startIndent " ")
68+
else
69+
return! None
70+
}
71+
72+
member __.GetFormattingChangesAsync (document: Document, position: int, cancellationToken: CancellationToken) =
73+
async {
74+
let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask
75+
let! options = document.GetOptionsAsync(cancellationToken) |> Async.AwaitTask
76+
let indentStyle = options.GetOption(FormattingOptions.SmartIndent, FSharpConstants.FSharpLanguageName)
77+
let projectOptionsOpt = projectInfoManager.TryGetOptionsForEditingDocumentOrProject document
78+
let! textChange = FSharpEditorFormattingService.GetFormattingChanges(document.Id, sourceText, document.FilePath, checkerProvider.Checker, indentStyle, projectOptionsOpt, position)
79+
80+
return
81+
match textChange with
82+
| Some change ->
83+
ResizeArray([change]) :> IList<_>
84+
85+
| None ->
86+
ResizeArray() :> IList<_>
87+
}
88+
89+
interface IEditorFormattingService with
90+
member val SupportsFormatDocument = false
91+
member val SupportsFormatSelection = false
92+
member val SupportsFormatOnPaste = false
93+
member val SupportsFormatOnReturn = true
94+
95+
override __.SupportsFormattingOnTypedCharacter (document, ch) =
96+
if FSharpIndentationService.IsSmartIndentEnabled document.Project.Solution.Workspace.Options then
97+
match ch with
98+
| ')' | ']' | '}' -> true
99+
| _ -> false
100+
else
101+
false
102+
103+
override __.GetFormattingChangesAsync (_document, _span, cancellationToken) =
104+
async { return ResizeArray() :> IList<_> }
105+
|> RoslynHelpers.StartAsyncAsTask cancellationToken
106+
107+
override __.GetFormattingChangesOnPasteAsync (_document, _span, cancellationToken) =
108+
async { return ResizeArray() :> IList<_> }
109+
|> RoslynHelpers.StartAsyncAsTask cancellationToken
110+
111+
override this.GetFormattingChangesAsync (document, _typedChar, position, cancellationToken) =
112+
this.GetFormattingChangesAsync (document, position, cancellationToken)
113+
|> RoslynHelpers.StartAsyncAsTask cancellationToken
114+
115+
override this.GetFormattingChangesOnReturnAsync (document, position, cancellationToken) =
116+
this.GetFormattingChangesAsync (document, position, cancellationToken)
117+
|> RoslynHelpers.StartAsyncAsTask cancellationToken

Formatting/IndentationService.fs

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ type internal FSharpIndentationService
2020
[<ImportingConstructor>]
2121
(projectInfoManager: FSharpProjectOptionsManager) =
2222

23-
static member GetDesiredIndentation(documentId: DocumentId, sourceText: SourceText, filePath: string, lineNumber: int, tabSize: int, optionsOpt: FSharpProjectOptions option): Option<int> =
23+
static member IsSmartIndentEnabled (options: Microsoft.CodeAnalysis.Options.OptionSet) =
24+
options.GetOption(FormattingOptions.SmartIndent, FSharpConstants.FSharpLanguageName) = FormattingOptions.IndentStyle.Smart
25+
26+
static member GetDesiredIndentation(documentId: DocumentId, sourceText: SourceText, filePath: string, lineNumber: int, tabSize: int, indentStyle: FormattingOptions.IndentStyle, projectOptions: FSharpProjectOptions option): Option<int> =
27+
2428
// Match indentation with previous line
2529
let rec tryFindPreviousNonEmptyLine l =
2630
if l <= 0 then None
@@ -31,16 +35,18 @@ type internal FSharpIndentationService
3135
else
3236
tryFindPreviousNonEmptyLine (l - 1)
3337

34-
let rec tryFindLastNoneWhitespaceOrCommentToken (line: TextLine) = maybe {
35-
let! options = optionsOpt
36-
let defines = CompilerEnvironment.GetCompilationDefinesForEditing(filePath, options.OtherOptions |> Seq.toList)
38+
let rec tryFindLastNonWhitespaceOrCommentToken (line: TextLine) = maybe {
39+
let! projectOptions = projectOptions
40+
let defines = CompilerEnvironment.GetCompilationDefinesForEditing(filePath, projectOptions.OtherOptions |> Seq.toList)
3741
let tokens = Tokenizer.tokenizeLine(documentId, sourceText, line.Start, filePath, defines)
3842

3943
return!
4044
tokens
4145
|> List.rev
4246
|> List.tryFind (fun x ->
43-
x.Tag <> FSharpTokenTag.WHITESPACE && x.Tag <> FSharpTokenTag.COMMENT && x.Tag <> FSharpTokenTag.LINE_COMMENT)
47+
x.Tag <> FSharpTokenTag.WHITESPACE &&
48+
x.Tag <> FSharpTokenTag.COMMENT &&
49+
x.Tag <> FSharpTokenTag.LINE_COMMENT)
4450
}
4551

4652
let (|Eq|_|) y x =
@@ -49,42 +55,43 @@ type internal FSharpIndentationService
4955

5056
let (|NeedIndent|_|) (token: FSharpTokenInfo) =
5157
match token.Tag with
52-
| Eq FSharpTokenTag.EQUALS
53-
| Eq FSharpTokenTag.LARROW
54-
| Eq FSharpTokenTag.RARROW
55-
| Eq FSharpTokenTag.LPAREN
56-
| Eq FSharpTokenTag.LBRACK
57-
| Eq FSharpTokenTag.LBRACK_BAR
58-
| Eq FSharpTokenTag.LBRACK_LESS
59-
| Eq FSharpTokenTag.LBRACE
60-
| Eq FSharpTokenTag.BEGIN
61-
| Eq FSharpTokenTag.DO
62-
| Eq FSharpTokenTag.FUNCTION
63-
| Eq FSharpTokenTag.THEN
64-
| Eq FSharpTokenTag.ELSE
65-
| Eq FSharpTokenTag.STRUCT
66-
| Eq FSharpTokenTag.CLASS
67-
| Eq FSharpTokenTag.TRY -> Some ()
58+
| Eq FSharpTokenTag.EQUALS // =
59+
| Eq FSharpTokenTag.LARROW // <-
60+
| Eq FSharpTokenTag.RARROW // ->
61+
| Eq FSharpTokenTag.LPAREN // (
62+
| Eq FSharpTokenTag.LBRACK // [
63+
| Eq FSharpTokenTag.LBRACK_BAR // [|
64+
| Eq FSharpTokenTag.LBRACK_LESS // [<
65+
| Eq FSharpTokenTag.LBRACE // {
66+
| Eq FSharpTokenTag.BEGIN // begin
67+
| Eq FSharpTokenTag.DO // do
68+
| Eq FSharpTokenTag.THEN // then
69+
| Eq FSharpTokenTag.ELSE // else
70+
| Eq FSharpTokenTag.STRUCT // struct
71+
| Eq FSharpTokenTag.CLASS // class
72+
| Eq FSharpTokenTag.TRY -> // try
73+
Some ()
6874
| _ -> None
6975

7076
maybe {
7177
let! previousLine = tryFindPreviousNonEmptyLine lineNumber
78+
79+
let lastIndent =
80+
previousLine.ToString()
81+
|> Seq.takeWhile ((=) ' ')
82+
|> Seq.length
7283

73-
let rec loop column spaces =
74-
if previousLine.Start + column >= previousLine.End then
75-
spaces
84+
// Only use smart indentation after tokens that need indentation
85+
// if the option is enabled
86+
let lastToken =
87+
if indentStyle = FormattingOptions.IndentStyle.Smart then
88+
tryFindLastNonWhitespaceOrCommentToken previousLine
7689
else
77-
match previousLine.Text.[previousLine.Start + column] with
78-
| ' ' -> loop (column + 1) (spaces + 1)
79-
| '\t' -> loop (column + 1) (((spaces / tabSize) + 1) * tabSize)
80-
| _ -> spaces
81-
82-
let lastIndent = loop 0 0
90+
None
8391

84-
let lastToken = tryFindLastNoneWhitespaceOrCommentToken previousLine
8592
return
8693
match lastToken with
87-
| Some(NeedIndent) -> (lastIndent/tabSize + 1) * tabSize
94+
| Some NeedIndent -> (lastIndent/tabSize + 1) * tabSize
8895
| _ -> lastIndent
8996
}
9097

@@ -94,9 +101,10 @@ type internal FSharpIndentationService
94101
let! cancellationToken = Async.CancellationToken
95102
let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask
96103
let! options = document.GetOptionsAsync(cancellationToken) |> Async.AwaitTask
97-
let tabSize = options.GetOption(FormattingOptions.TabSize, FSharpConstants.FSharpLanguageName)
104+
let tabSize = options.GetOption<int>(FormattingOptions.TabSize, FSharpConstants.FSharpLanguageName)
105+
let indentStyle = options.GetOption(FormattingOptions.SmartIndent, FSharpConstants.FSharpLanguageName)
98106
let projectOptionsOpt = projectInfoManager.TryGetOptionsForEditingDocumentOrProject document
99-
let indent = FSharpIndentationService.GetDesiredIndentation(document.Id, sourceText, document.FilePath, lineNumber, tabSize, projectOptionsOpt)
107+
let indent = FSharpIndentationService.GetDesiredIndentation(document.Id, sourceText, document.FilePath, lineNumber, tabSize, indentStyle, projectOptionsOpt)
100108
return
101109
match indent with
102110
| None -> Nullable()

0 commit comments

Comments
 (0)