Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commita6cfc81

Browse files
saulcartermp
authored andcommitted
Add editor formatting service to auto-deindent closing brackets (#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 testsStill WIP, other tests still broken* Fix failing indentation tests* Add formatting service tests* Add more brace matching testsFixes #2092
1 parent6794923 commita6cfc81

File tree

8 files changed

+290
-54
lines changed

8 files changed

+290
-54
lines changed

‎vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj‎

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

‎vsintegration/src/FSharp.Editor/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 letuserOpName="BraceMatching"
19-
static memberGetBraceMatchingResult(checker:FSharpChecker,sourceText,fileName,options,position:int)=
18+
19+
static letdefaultUserOpName="BraceMatching"
20+
21+
static memberGetBraceMatchingResult(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
letisPositionInRange range=
2325
match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, range)with
2426
| None->false
25-
| Some range-> range.Contains(position)
27+
| Some range->
28+
letlength= 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+
namespaceMicrosoft.VisualStudio.FSharp.Editor
4+
5+
openSystem.Composition
6+
openSystem.Collections.Generic
7+
8+
openMicrosoft.CodeAnalysis
9+
openMicrosoft.CodeAnalysis.Editor
10+
openMicrosoft.CodeAnalysis.Formatting
11+
openMicrosoft.CodeAnalysis.Host.Mef
12+
openMicrosoft.CodeAnalysis.Text
13+
14+
openMicrosoft.FSharp.Compiler.SourceCodeServices
15+
openSystem.Threading
16+
17+
[<Shared>]
18+
[<ExportLanguageService(typeof<IEditorFormattingService>, FSharpConstants.FSharpLanguageName)>]
19+
typeinternalFSharpEditorFormattingService
20+
[<ImportingConstructor>]
21+
(
22+
checkerProvider: FSharpCheckerProvider,
23+
projectInfoManager: FSharpProjectOptionsManager
24+
)=
25+
26+
static memberGetFormattingChanges(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+
letline= sourceText.Lines.[sourceText.Lines.IndexOf position]
40+
41+
letdefines= CompilerEnvironment.GetCompilationDefinesForEditing(filePath, projectOptions.OtherOptions|> List.ofArray)
42+
43+
lettokens= 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.LeftColumnthen
56+
// Replace the indentation on this line with the indentation of the left bracket
57+
let!leftSpan= RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, left)
58+
59+
letindentChars(line:TextLine)=
60+
line.ToString()
61+
|> Seq.takeWhile((=)' ')
62+
|> Seq.length
63+
64+
letstartIndent= indentChars sourceText.Lines.[sourceText.Lines.IndexOf leftSpan.Start]
65+
letcurrentIndent= 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+
letindentStyle= options.GetOption(FormattingOptions.SmartIndent, FSharpConstants.FSharpLanguageName)
77+
letprojectOptionsOpt= projectInfoManager.TryGetOptionsForEditingDocumentOrProject document
78+
let!textChange= FSharpEditorFormattingService.GetFormattingChanges(document.Id, sourceText, document.FilePath, checkerProvider.Checker, indentStyle, projectOptionsOpt, position)
79+
80+
return
81+
match textChangewith
82+
| Some change->
83+
ResizeArray([change]):> IList<_>
84+
85+
| None->
86+
ResizeArray():> IList<_>
87+
}
88+
89+
interface IEditorFormattingServicewith
90+
member valSupportsFormatDocument=false
91+
member valSupportsFormatSelection=false
92+
member valSupportsFormatOnPaste=false
93+
member valSupportsFormatOnReturn=true
94+
95+
override__.SupportsFormattingOnTypedCharacter(document,ch)=
96+
if FSharpIndentationService.IsSmartIndentEnabled document.Project.Solution.Workspace.Optionsthen
97+
match chwith
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+
overridethis.GetFormattingChangesAsync(document,_typedChar,position,cancellationToken)=
112+
this.GetFormattingChangesAsync(document, position, cancellationToken)
113+
|> RoslynHelpers.StartAsyncAsTask cancellationToken
114+
115+
overridethis.GetFormattingChangesOnReturnAsync(document,position,cancellationToken)=
116+
this.GetFormattingChangesAsync(document, position, cancellationToken)
117+
|> RoslynHelpers.StartAsyncAsTask cancellationToken

‎vsintegration/src/FSharp.Editor/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 memberGetDesiredIndentation(documentId:DocumentId,sourceText:SourceText,filePath:string,lineNumber:int,tabSize:int,optionsOpt:FSharpProjectOptions option):Option<int>=
23+
static memberIsSmartIndentEnabled(options:Microsoft.CodeAnalysis.Options.OptionSet)=
24+
options.GetOption(FormattingOptions.SmartIndent, FSharpConstants.FSharpLanguageName)= FormattingOptions.IndentStyle.Smart
25+
26+
static memberGetDesiredIndentation(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 rectryFindPreviousNonEmptyLine l=
2630
if l<=0then None
@@ -31,16 +35,18 @@ type internal FSharpIndentationService
3135
else
3236
tryFindPreviousNonEmptyLine(l-1)
3337

34-
let rectryFindLastNoneWhitespaceOrCommentToken(line:TextLine)=maybe{
35-
let!options=optionsOpt
36-
letdefines= CompilerEnvironment.GetCompilationDefinesForEditing(filePath,options.OtherOptions|> Seq.toList)
38+
let rectryFindLastNonWhitespaceOrCommentToken(line:TextLine)=maybe{
39+
let!projectOptions=projectOptions
40+
letdefines= CompilerEnvironment.GetCompilationDefinesForEditing(filePath,projectOptions.OtherOptions|> Seq.toList)
3741
lettokens= 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.Tagwith
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+
letlastIndent=
80+
previousLine.ToString()
81+
|> Seq.takeWhile((=)' ')
82+
|> Seq.length
7283

73-
let recloop column spaces=
74-
if previousLine.Start+ column>= previousLine.Endthen
75-
spaces
84+
// Only use smart indentation after tokens that need indentation
85+
// if the option is enabled
86+
letlastToken=
87+
if indentStyle= FormattingOptions.IndentStyle.Smartthen
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-
letlastIndent= loop00
90+
None
8391

84-
letlastToken= tryFindLastNoneWhitespaceOrCommentToken previousLine
8592
return
8693
match lastTokenwith
87-
| Some(NeedIndent)->(lastIndent/tabSize+1)* tabSize
94+
| SomeNeedIndent->(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-
lettabSize= options.GetOption(FormattingOptions.TabSize, FSharpConstants.FSharpLanguageName)
104+
lettabSize= options.GetOption<int>(FormattingOptions.TabSize, FSharpConstants.FSharpLanguageName)
105+
letindentStyle= options.GetOption(FormattingOptions.SmartIndent, FSharpConstants.FSharpLanguageName)
98106
letprojectOptionsOpt= projectInfoManager.TryGetOptionsForEditingDocumentOrProject document
99-
letindent= FSharpIndentationService.GetDesiredIndentation(document.Id, sourceText, document.FilePath, lineNumber, tabSize, projectOptionsOpt)
107+
letindent= FSharpIndentationService.GetDesiredIndentation(document.Id, sourceText, document.FilePath, lineNumber, tabSize,indentStyle,projectOptionsOpt)
100108
return
101109
match indentwith
102110
| None-> Nullable()

‎vsintegration/tests/unittests/BraceMatchingServiceTests.fs‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type BraceMatchingServiceTests() =
3636
letposition= fileContents.IndexOf(marker)
3737
Assert.IsTrue(position>=0,"Cannot find marker '{0}' in file contents", marker)
3838

39-
match FSharpBraceMatchingService.GetBraceMatchingResult(checker, sourceText, fileName, options, position)|> Async.RunSynchronouslywith
39+
match FSharpBraceMatchingService.GetBraceMatchingResult(checker, sourceText, fileName, options, position,"UnitTest")|> Async.RunSynchronouslywith
4040
| None->()
4141
| Some(left, right)-> Assert.Fail("Found match for brace '{0}'", marker)
4242

@@ -48,7 +48,7 @@ type BraceMatchingServiceTests() =
4848
Assert.IsTrue(startMarkerPosition>=0,"Cannot find start marker '{0}' in file contents", startMarkerPosition)
4949
Assert.IsTrue(endMarkerPosition>=0,"Cannot find end marker '{0}' in file contents", endMarkerPosition)
5050

51-
match FSharpBraceMatchingService.GetBraceMatchingResult(checker, sourceText, fileName, options, startMarkerPosition)|> Async.RunSynchronouslywith
51+
match FSharpBraceMatchingService.GetBraceMatchingResult(checker, sourceText, fileName, options, startMarkerPosition,"UnitTest")|> Async.RunSynchronouslywith
5252
| None-> Assert.Fail("Didn't find a match for start brace at position '{0}", startMarkerPosition)
5353
| Some(left, right)->
5454
letendPositionInRange(range)=
@@ -157,3 +157,26 @@ let main argv =
157157
(printfn "%A '%A' '%A'" (arg1) (arg2) (arg3))endBrace
158158
0 // return an integer exit code"""
159159
this.VerifyBraceMatch(code,"(printfn",")endBrace")
160+
161+
[<TestCase("let a1 = [ 0 .. 100 ]",[|9;10;20;21|])>]
162+
[<TestCase("let a2 = [| 0 .. 100 |]",[|9;10;11;21;22;23|])>]
163+
[<TestCase("let a3 = <@ 0 @>",[|9;10;11;14;15;16|])>]
164+
[<TestCase("let a4 = <@@ 0 @@>",[|9;10;11;12;15;15;16;17|])>]
165+
[<TestCase("let a6 = ( () )",[|9;10;16;17|])>]
166+
[<TestCase("[<ReflectedDefinition>]\nlet a7 = 70",[|0;1;2;21;22;23|])>]
167+
[<TestCase("let a8 = seq { yield() }",[|13;14;23;24|])>]
168+
memberthis.BraceMatchingBothSides_Bug2092(fileContents:string,matchingPositions:int[])=
169+
// https://github.com/Microsoft/visualfsharp/issues/2092
170+
letsourceText= SourceText.From(fileContents)
171+
172+
matchingPositions
173+
|> Array.iter(fun position->
174+
match FSharpBraceMatchingService.GetBraceMatchingResult(checker, sourceText, fileName, options, position,"UnitTest")|> Async.RunSynchronouslywith
175+
| Some_->()
176+
| None->
177+
match positionwith
178+
|0->""
179+
|_-> fileContents.[position-1]|> sprintf" (previous character '%c')"
180+
|> sprintf"Didn't find a matching brace at position '%d', character '%c'%s" position fileContents.[position]
181+
|> Assert.Fail
182+
)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp