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

Commit

Permalink
Reformat indentation on paste (dotnet#4702)
Browse files Browse the repository at this point in the history
* Format indentation on paste

* Fix pasting when next line should be indented

* Initial pass at 'Formatting' page

* Add some tests

* Fix tests
  • Loading branch information
saul authored and KevinRansom committed Oct 17, 2018
1 parent 6706fda commit 83e2353
Show file tree
Hide file tree
Showing 20 changed files with 231 additions and 55 deletions.
5 changes: 5 additions & 0 deletions Common/Constants.fs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ module internal Guids =
let languageServicePerformanceOptionPageIdString = "8FDA964A-263D-4B4E-9560-29897535217C"

[<Literal>]
/// "9007718C-357A-4327-A193-AB3EC38D7EE8"
let advancedSettingsPageIdSring = "9007718C-357A-4327-A193-AB3EC38D7EE8"

[<Literal>]
/// "9EBEBCE8-A79B-46B0-A8C5-A9818AEED17D"
let formattingOptionPageIdString = "9EBEBCE8-A79B-46B0-A8C5-A9818AEED17D"

let blueHighContrastThemeId = Guid "{ce94d289-8481-498b-8ca9-9b6191a315b9}"
15 changes: 9 additions & 6 deletions FSharp.Editor.resx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@
<data name="6011" xml:space="preserve">
<value>Performance</value>
</data>
<data name="6012" xml:space="preserve">
<value>Advanced</value>
</data>
<data name="6013" xml:space="preserve">
<value>CodeLens</value>
</data>
<data name="6014" xml:space="preserve">
<value>Formatting</value>
</data>
<data name="TheValueIsUnused" xml:space="preserve">
<value>The value is unused</value>
</data>
Expand Down Expand Up @@ -204,10 +213,4 @@
<data name="RenameValueToDoubleUnderscore" xml:space="preserve">
<value>Rename '{0}' to '__'</value>
</data>
<data name="6012" xml:space="preserve">
<value>Advanced</value>
</data>
<data name="6013" xml:space="preserve">
<value>CodeLens</value>
</data>
</root>
110 changes: 98 additions & 12 deletions Formatting/EditorFormattingService.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,21 @@ open Microsoft.CodeAnalysis.Text

open Microsoft.FSharp.Compiler.SourceCodeServices
open System.Threading
open System.Windows.Forms

[<Shared>]
[<ExportLanguageService(typeof<IEditorFormattingService>, FSharpConstants.FSharpLanguageName)>]
type internal FSharpEditorFormattingService
[<ImportingConstructor>]
(
checkerProvider: FSharpCheckerProvider,
projectInfoManager: FSharpProjectOptionsManager
projectInfoManager: FSharpProjectOptionsManager,
settings: EditorOptions
) =

static let toIList (xs : 'a seq) = ResizeArray(xs) :> IList<'a>

static let getIndentation (line : string) = line |> Seq.takeWhile ((=) ' ') |> Seq.length

static member GetFormattingChanges(documentId: DocumentId, sourceText: SourceText, filePath: string, checker: FSharpChecker, indentStyle: FormattingOptions.IndentStyle, options: (FSharpParsingOptions * FSharpProjectOptions) option, position: int) =
// Logic for determining formatting changes:
Expand Down Expand Up @@ -68,6 +74,77 @@ type internal FSharpEditorFormattingService
else
return! None
}

static member GetPasteChanges(documentId: DocumentId, sourceText: SourceText, filePath: string, formattingOptions: Microsoft.VisualStudio.FSharp.Editor.FormattingOptions, tabSize: int, parsingOptions: FSharpParsingOptions, currentClipboard: string, span: TextSpan) =
asyncMaybe {

do! Option.guard formattingOptions.FormatOnPaste

let startLineIdx = sourceText.Lines.IndexOf span.Start

// If we're starting and ending on the same line, we've got nothing to format
do! Option.guard (startLineIdx <> sourceText.Lines.IndexOf span.End)

let startLine = sourceText.Lines.[startLineIdx]

// VS quirk: if we're pasting on an empty line which has automatically been
// indented (i.e. by ISynchronousIndentationService), then the pasted span
// includes this automatic indentation. When pasting, we only care about what
// was actually in the clipboard.
let fixedSpan =
let pasteText = sourceText.GetSubText(span)
let pasteTextString = pasteText.ToString()

if currentClipboard.Length > 0 && pasteTextString.EndsWith currentClipboard then
let prepended = pasteTextString.[0..pasteTextString.Length-currentClipboard.Length-1]

// Only strip off leading indentation if the pasted span is otherwise
// identical to the clipboard (ignoring leading spaces).
if prepended |> Seq.forall ((=) ' ') then
TextSpan(span.Start + prepended.Length, span.Length - prepended.Length)
else
span
else
span

// Calculate the indentation of the line we pasted onto
let currentIndent =
let priorStartSpan = TextSpan(startLine.Span.Start, startLine.Span.Length - (startLine.Span.End - fixedSpan.Start))

sourceText.GetSubText(priorStartSpan).ToString()
|> Seq.takeWhile ((=) ' ')
|> Seq.length

let fixedPasteText = sourceText.GetSubText(fixedSpan)
let leadingIndentation = fixedPasteText.ToString() |> getIndentation

let stripIndentation charsToRemove =
let searchIndent = String.replicate charsToRemove " "
let newText = String.replicate currentIndent " "

fixedPasteText.Lines
|> Seq.indexed
|> Seq.choose (fun (i, line) ->
if line.ToString().StartsWith searchIndent then
TextChange(TextSpan(line.Start + fixedSpan.Start, charsToRemove), if i = 0 then "" else newText)
|> Some
else
None
)

if leadingIndentation > 0 then
return stripIndentation leadingIndentation
else
let nextLineShouldBeIndented = FSharpIndentationService.IndentShouldFollow(documentId, sourceText, filePath, span.Start, parsingOptions)

let removeIndentation =
let nextLineIndent = fixedPasteText.Lines.[1].ToString() |> getIndentation

if nextLineShouldBeIndented then nextLineIndent - tabSize
else nextLineIndent

return stripIndentation removeIndentation
}

member __.GetFormattingChangesAsync (document: Document, position: int, cancellationToken: CancellationToken) =
async {
Expand All @@ -76,20 +153,27 @@ type internal FSharpEditorFormattingService
let indentStyle = options.GetOption(FormattingOptions.SmartIndent, FSharpConstants.FSharpLanguageName)
let projectOptionsOpt = projectInfoManager.TryGetOptionsForEditingDocumentOrProject document
let! textChange = FSharpEditorFormattingService.GetFormattingChanges(document.Id, sourceText, document.FilePath, checkerProvider.Checker, indentStyle, projectOptionsOpt, position)

return
match textChange with
| Some change ->
ResizeArray([change]) :> IList<_>

| None ->
ResizeArray() :> IList<_>
return textChange |> Option.toList |> toIList
}

member __.OnPasteAsync (document: Document, span: TextSpan, currentClipboard: string, cancellationToken: CancellationToken) =
async {
let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask
let! options = document.GetOptionsAsync(cancellationToken) |> Async.AwaitTask
let tabSize = options.GetOption<int>(FormattingOptions.TabSize, FSharpConstants.FSharpLanguageName)

match projectInfoManager.TryGetOptionsForEditingDocumentOrProject document with
| Some (parsingOptions, _) ->
let! textChanges = FSharpEditorFormattingService.GetPasteChanges(document.Id, sourceText, document.FilePath, settings.Formatting, tabSize, parsingOptions, currentClipboard, span)
return textChanges |> Option.defaultValue Seq.empty |> toIList
| None ->
return toIList Seq.empty
}

interface IEditorFormattingService with
member val SupportsFormatDocument = false
member val SupportsFormatSelection = false
member val SupportsFormatOnPaste = false
member val SupportsFormatOnPaste = true
member val SupportsFormatOnReturn = true

override __.SupportsFormattingOnTypedCharacter (document, ch) =
Expand All @@ -104,8 +188,10 @@ type internal FSharpEditorFormattingService
async { return ResizeArray() :> IList<_> }
|> RoslynHelpers.StartAsyncAsTask cancellationToken

override __.GetFormattingChangesOnPasteAsync (_document, _span, cancellationToken) =
async { return ResizeArray() :> IList<_> }
override this.GetFormattingChangesOnPasteAsync (document, span, cancellationToken) =
let currentClipboard = Clipboard.GetText()

this.OnPasteAsync (document, span, currentClipboard, cancellationToken)
|> RoslynHelpers.StartAsyncAsTask cancellationToken

override this.GetFormattingChangesAsync (document, _typedChar, position, cancellationToken) =
Expand Down
71 changes: 34 additions & 37 deletions Formatting/IndentationService.fs
Original file line number Diff line number Diff line change
Expand Up @@ -23,38 +23,26 @@ type internal FSharpIndentationService
static member IsSmartIndentEnabled (options: Microsoft.CodeAnalysis.Options.OptionSet) =
options.GetOption(FormattingOptions.SmartIndent, FSharpConstants.FSharpLanguageName) = FormattingOptions.IndentStyle.Smart

static member GetDesiredIndentation(documentId: DocumentId, sourceText: SourceText, filePath: string, lineNumber: int, tabSize: int, indentStyle: FormattingOptions.IndentStyle, options: (FSharpParsingOptions * FSharpProjectOptions) option): Option<int> =

// Match indentation with previous line
let rec tryFindPreviousNonEmptyLine l =
if l <= 0 then None
else
let previousLine = sourceText.Lines.[l - 1]
if not (String.IsNullOrEmpty(previousLine.ToString())) then
Some previousLine
else
tryFindPreviousNonEmptyLine (l - 1)

let rec tryFindLastNonWhitespaceOrCommentToken (line: TextLine) = maybe {
let! parsingOptions, _projectOptions = options
static member IndentShouldFollow (documentId: DocumentId, sourceText: SourceText, filePath: string, position: int, parsingOptions: FSharpParsingOptions) =
let lastTokenOpt =
let defines = CompilerEnvironment.GetCompilationDefinesForEditing parsingOptions
let tokens = Tokenizer.tokenizeLine(documentId, sourceText, line.Start, filePath, defines)

return!
tokens
|> Array.rev
|> Array.tryFind (fun x ->
x.Tag <> FSharpTokenTag.WHITESPACE &&
x.Tag <> FSharpTokenTag.COMMENT &&
x.Tag <> FSharpTokenTag.LINE_COMMENT)
}
let tokens = Tokenizer.tokenizeLine(documentId, sourceText, position, filePath, defines)

tokens
|> Array.rev
|> Array.tryFind (fun x ->
x.Tag <> FSharpTokenTag.WHITESPACE &&
x.Tag <> FSharpTokenTag.COMMENT &&
x.Tag <> FSharpTokenTag.LINE_COMMENT)

let (|Eq|_|) y x =
if x = y then Some()
else None

let (|NeedIndent|_|) (token: Tokenizer.SavedTokenInfo) =
match token.Tag with
match lastTokenOpt with
| None -> false
| Some lastToken ->
match lastToken.Tag with
| Eq FSharpTokenTag.EQUALS // =
| Eq FSharpTokenTag.LARROW // <-
| Eq FSharpTokenTag.RARROW // ->
Expand All @@ -70,8 +58,20 @@ type internal FSharpIndentationService
| Eq FSharpTokenTag.STRUCT // struct
| Eq FSharpTokenTag.CLASS // class
| Eq FSharpTokenTag.TRY -> // try
Some ()
| _ -> None
true
| _ -> false

static member GetDesiredIndentation(documentId: DocumentId, sourceText: SourceText, filePath: string, lineNumber: int, tabSize: int, indentStyle: FormattingOptions.IndentStyle, options: (FSharpParsingOptions * FSharpProjectOptions) option): Option<int> =

// Match indentation with previous line
let rec tryFindPreviousNonEmptyLine l =
if l <= 0 then None
else
let previousLine = sourceText.Lines.[l - 1]
if not (String.IsNullOrEmpty(previousLine.ToString())) then
Some previousLine
else
tryFindPreviousNonEmptyLine (l - 1)

maybe {
let! previousLine = tryFindPreviousNonEmptyLine lineNumber
Expand All @@ -81,18 +81,15 @@ type internal FSharpIndentationService
|> Seq.takeWhile ((=) ' ')
|> Seq.length

let! parsingOptions, _ = options

// Only use smart indentation after tokens that need indentation
// if the option is enabled
let lastToken =
if indentStyle = FormattingOptions.IndentStyle.Smart then
tryFindLastNonWhitespaceOrCommentToken previousLine
else
None

return
match lastToken with
| Some NeedIndent -> (lastIndent/tabSize + 1) * tabSize
| _ -> lastIndent
if indentStyle = FormattingOptions.IndentStyle.Smart && FSharpIndentationService.IndentShouldFollow(documentId, sourceText, filePath, previousLine.Start, parsingOptions) then
(lastIndent/tabSize + 1) * tabSize
else
lastIndent
}

interface ISynchronousIndentationService with
Expand Down
1 change: 1 addition & 0 deletions LanguageService/LanguageService.fs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ type internal FSharpSettingsFactory
[<ProvideLanguageEditorOptionPage(typeof<OptionsUI.LanguageServicePerformanceOptionPage>, "F#", null, "Performance", "6011")>]
[<ProvideLanguageEditorOptionPage(typeof<OptionsUI.AdvancedSettingsOptionPage>, "F#", null, "Advanced", "6012")>]
[<ProvideLanguageEditorOptionPage(typeof<OptionsUI.CodeLensOptionPage>, "F#", null, "CodeLens", "6013")>]
[<ProvideLanguageEditorOptionPage(typeof<OptionsUI.FormattingOptionPage>, "F#", null, "Formatting", "6014")>]
[<ProvideFSharpVersionRegistration(FSharpConstants.projectPackageGuidString, "Microsoft Visual F#")>]
// 64 represents a hex number. It needs to be greater than 37 so the TextMate editor will not be chosen as higher priority.
[<ProvideEditorExtension(typeof<FSharpEditorFactory>, ".fs", 64)>]
Expand Down
14 changes: 14 additions & 0 deletions Options/EditorOptions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ type AdvancedOptions =
{ IsBlockStructureEnabled = true
IsOutliningEnabled = true }

[<CLIMutable>]
type FormattingOptions =
{ FormatOnPaste: bool }
static member Default =
{ FormatOnPaste = true }

[<Export>]
[<Export(typeof<IPersistSettings>)>]
type EditorOptions
Expand All @@ -111,13 +117,15 @@ type EditorOptions
store.Register AdvancedOptions.Default
store.Register IntelliSenseOptions.Default
store.Register CodeLensOptions.Default
store.Register FormattingOptions.Default

member __.IntelliSense : IntelliSenseOptions = store.Read()
member __.QuickInfo : QuickInfoOptions = store.Read()
member __.CodeFixes : CodeFixesOptions = store.Read()
member __.LanguageServicePerformance : LanguageServicePerformanceOptions = store.Read()
member __.Advanced: AdvancedOptions = store.Read()
member __.CodeLens: CodeLensOptions = store.Read()
member __.Formatting : FormattingOptions = store.Read()

interface Microsoft.CodeAnalysis.Host.IWorkspaceService

Expand Down Expand Up @@ -183,3 +191,9 @@ module internal OptionsUI =
inherit AbstractOptionPage<AdvancedOptions>()
override __.CreateView() =
upcast AdvancedOptionsControl()

[<Guid(Guids.formattingOptionPageIdString)>]
type internal FormattingOptionPage() =
inherit AbstractOptionPage<FormattingOptions>()
override __.CreateView() =
upcast FormattingOptionsControl()
5 changes: 5 additions & 0 deletions xlf/FSharp.Editor.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@
<target state="translated">CodeLens</target>
<note />
</trans-unit>
<trans-unit id="6014">
<source>Formatting</source>
<target state="new">Formatting</target>
<note />
</trans-unit>
</body>
</file>
</xliff>
5 changes: 5 additions & 0 deletions xlf/FSharp.Editor.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@
<target state="translated">CodeLens</target>
<note />
</trans-unit>
<trans-unit id="6014">
<source>Formatting</source>
<target state="new">Formatting</target>
<note />
</trans-unit>
</body>
</file>
</xliff>
5 changes: 5 additions & 0 deletions xlf/FSharp.Editor.en.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@
<target state="new">CodeLens</target>
<note />
</trans-unit>
<trans-unit id="6014">
<source>Formatting</source>
<target state="new">Formatting</target>
<note />
</trans-unit>
</body>
</file>
</xliff>
5 changes: 5 additions & 0 deletions xlf/FSharp.Editor.es.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@
<target state="translated">CodeLens</target>
<note />
</trans-unit>
<trans-unit id="6014">
<source>Formatting</source>
<target state="new">Formatting</target>
<note />
</trans-unit>
</body>
</file>
</xliff>
Loading

0 comments on commit 83e2353

Please sign in to comment.