From 59c351370de754162ec6398010fd10ab85a89a4d Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 30 Jun 2020 07:18:43 -0700 Subject: [PATCH] feat(formatting/#785): Default format provider when no extension provider (#2022) * Formatting * Formatting * Add default format strategy * Process indentation ranges * Additional cases * Hook up language configuration * Remove test that no longer applies --- src/Core/Indentation.re | 33 ++++ src/Core/IndentationGuesser.re | 20 +- src/Core/LanguageConfiguration.re | 89 ++++++--- src/Core/LanguageConfiguration.rei | 5 + src/Core/Utility/StringEx.re | 26 ++- src/Feature/Formatting/DefaultFormatter.re | 158 ++++++++++++++++ src/Feature/Formatting/DefaultFormatter.rei | 17 ++ src/Feature/Formatting/Feature_Formatting.re | 175 ++++++++++++++---- src/Feature/Formatting/Feature_Formatting.rei | 7 +- src/Feature/Formatting/dune | 3 +- src/Store/Features.re | 13 ++ src/Store/VimStoreConnector.re | 6 +- test/Core/LanguageConfigurationTest.re | 9 - 13 files changed, 463 insertions(+), 98 deletions(-) create mode 100644 src/Feature/Formatting/DefaultFormatter.re create mode 100644 src/Feature/Formatting/DefaultFormatter.rei diff --git a/src/Core/Indentation.re b/src/Core/Indentation.re index cfbb685eae..82f2bbc997 100644 --- a/src/Core/Indentation.re +++ b/src/Core/Indentation.re @@ -4,6 +4,23 @@ * Helpers for dealing with indentation level */ +open Utility; + +let getLeadingWhitespace = (s: string) => { + let rec loop = (i, spaces, tabs) => + if (i >= String.length(s)) { + (spaces, tabs, false); + } else { + switch (s.[i]) { + | ' ' => loop(i + 1, spaces + 1, tabs) + | '\t' => loop(i + 1, spaces, tabs + 1) + | _ => (spaces, tabs, true) + }; + }; + + loop(0, 0, 0); +}; + let getLevel = (settings: IndentationSettings.t, text: string) => { let tabSize = settings.tabSize; @@ -38,6 +55,22 @@ let getLevel = (settings: IndentationSettings.t, text: string) => { allWhitespace^ ? 0 : indentLevel^; }; +let applyLevel = + (~indentation: IndentationSettings.t, ~level: int, str: string) => { + str + |> StringEx.findNonWhitespace + |> Option.map(idx => { + let desiredWhitespace = + switch (indentation.mode) { + | Tabs => String.make(level, '\t') + | Spaces => String.make(level * indentation.size, ' ') + }; + + desiredWhitespace ++ String.sub(str, idx, String.length(str) - idx); + }) + |> Option.value(~default=str); +}; + let getForBuffer = (~buffer, configuration: Configuration.t) => { let bufferIndentation = Buffer.getIndentation(buffer); switch (bufferIndentation) { diff --git a/src/Core/IndentationGuesser.re b/src/Core/IndentationGuesser.re index b2a6e5045c..056c1f6bfc 100644 --- a/src/Core/IndentationGuesser.re +++ b/src/Core/IndentationGuesser.re @@ -8,21 +8,6 @@ module Constants = { let minSpaces = 2; }; -let getLeadingWhitespace = (s: string) => { - let rec loop = (i, spaces, tabs) => - if (i >= String.length(s)) { - (spaces, tabs, false); - } else { - switch (s.[i]) { - | ' ' => loop(i + 1, spaces + 1, tabs) - | '\t' => loop(i + 1, spaces, tabs + 1) - | _ => (spaces, tabs, true) - }; - }; - - loop(0, 0, 0); -}; - type t = { mode: IndentationSettings.mode, size: int, @@ -62,7 +47,8 @@ let guessIndentation = let line = getLine(i); let prevLine = i == 0 ? "" : getLine(i - 1); - let (spaceCount, tabCount, foundChar) = getLeadingWhitespace(line); + let (spaceCount, tabCount, foundChar) = + Indentation.getLeadingWhitespace(line); /* Only consider lines with non-whitespace */ if (foundChar) { @@ -75,7 +61,7 @@ let guessIndentation = }; let (prevSpaceCount, _, prevFoundChar) = - getLeadingWhitespace(prevLine); + Indentation.getLeadingWhitespace(prevLine); if (prevFoundChar) { let diff = abs(prevSpaceCount - spaceCount); if (diff >= Constants.minSpaces) { diff --git a/src/Core/LanguageConfiguration.re b/src/Core/LanguageConfiguration.re index 9792d4ab4e..6645b4163c 100644 --- a/src/Core/LanguageConfiguration.re +++ b/src/Core/LanguageConfiguration.re @@ -92,6 +92,34 @@ module BracketPair = { let endsWithOpenPair = ({openPair, _}, str) => { StringEx.endsWith(~postfix=openPair, str); }; + + let isJustClosingPair = ({closePair, _}, str) => { + let len = String.length(str); + + let rec loop = (foundPair, idx) => + if (idx >= len) { + foundPair; + } else if (foundPair) { + false; + // We found the closing pair... but there's other stuff after + } else { + let c = str.[idx]; + + if (c == ' ' || c == '\t') { + loop(foundPair, idx + 1); + } else if (c == closePair.[0]) { + loop(true, idx + 1); + } else { + false; + }; + }; + + if (String.length(closePair) == 1) { + loop(false, 0); + } else { + false; + }; + }; }; let defaultBrackets: list(BracketPair.t) = @@ -226,36 +254,45 @@ let toVimAutoClosingPairs = (syntaxScope: SyntaxScope.t, configuration: t) => { ); }; -let toAutoIndent = +let shouldIncreaseIndent = ( - {increaseIndentPattern, decreaseIndentPattern, brackets, _}, ~previousLine as str, ~beforePreviousLine as _, + {increaseIndentPattern, brackets, _}, ) => { - let increase = - increaseIndentPattern - |> Option.map(regex => OnigRegExp.test(str, regex)) - // If no indentation pattern, fall-back to bracket pair - |> OptionEx.or_lazy(() => - Some( - List.exists( - bracket => BracketPair.endsWithOpenPair(bracket, str), - brackets, - ), - ) + increaseIndentPattern + |> Option.map(regex => OnigRegExp.test(str, regex)) + // If no indentation pattern, fall-back to bracket pair + |> OptionEx.or_lazy(() => + Some( + List.exists( + bracket => BracketPair.endsWithOpenPair(bracket, str), + brackets, + ), ) - |> Option.value(~default=false); - - let decrease = - decreaseIndentPattern - |> Option.map(regex => OnigRegExp.test(str, regex)) - |> Option.value(~default=false); - - if (increase) { - Vim.AutoIndent.IncreaseIndent; - } else if (decrease) { - Vim.AutoIndent.DecreaseIndent; - } else { - Vim.AutoIndent.KeepIndent; + ) + |> Option.value(~default=false); +}; + +let shouldDecreaseIndent = (~line, {decreaseIndentPattern, brackets, _}) => { + decreaseIndentPattern + |> Option.map(regex => OnigRegExp.test(line, regex)) + |> OptionEx.or_lazy(() => { + Some( + List.exists( + bracket => BracketPair.isJustClosingPair(bracket, line), + brackets, + ), + ) + }) + |> Option.value(~default=false); +}; + +let toAutoIndent = (languageConfig, ~previousLine, ~beforePreviousLine) => { + let increase = + shouldIncreaseIndent(~previousLine, ~beforePreviousLine, languageConfig); + + if (increase) {Vim.AutoIndent.IncreaseIndent} else { + Vim.AutoIndent.KeepIndent }; }; diff --git a/src/Core/LanguageConfiguration.rei b/src/Core/LanguageConfiguration.rei index 213f2f8566..10663a0869 100644 --- a/src/Core/LanguageConfiguration.rei +++ b/src/Core/LanguageConfiguration.rei @@ -40,6 +40,11 @@ type t = { let default: t; +let shouldIncreaseIndent: + (~previousLine: string, ~beforePreviousLine: option(string), t) => bool; + +let shouldDecreaseIndent: (~line: string, t) => bool; + let decode: Json.decoder(t); let toVimAutoClosingPairs: (SyntaxScope.t, t) => Vim.AutoClosingPairs.t; diff --git a/src/Core/Utility/StringEx.re b/src/Core/Utility/StringEx.re index a0f5c5fff6..c0d636fdd6 100644 --- a/src/Core/Utility/StringEx.re +++ b/src/Core/Utility/StringEx.re @@ -112,19 +112,29 @@ let trimRight = str => { aux(length - 1); }; -let indentation = str => { - let rec loop = i => - if (i >= String.length(str)) { - i; - } else if (isSpace(str.[i])) { - loop(i + 1); +let findNonWhitespace = str => { + let len = String.length(str); + let rec loop = idx => + if (idx >= len) { + None; } else { - i; + let char = str.[idx]; + if (char != '\t' && char != ' ') { + Some(idx); + } else { + loop(idx + 1); + }; }; - loop(0); }; +let isOnlyWhitespace = str => { + switch (findNonWhitespace(str)) { + | None => true + | Some(_) => false + }; +}; + let extractSnippet = (~maxLength, ~charStart, ~charEnd, text) => { let originalLength = String.length(text); diff --git a/src/Feature/Formatting/DefaultFormatter.re b/src/Feature/Formatting/DefaultFormatter.re new file mode 100644 index 0000000000..1bda016f2b --- /dev/null +++ b/src/Feature/Formatting/DefaultFormatter.re @@ -0,0 +1,158 @@ +open EditorCoreTypes; +open Oni_Core; +open Oni_Core.Utility; + +module Internal = { + let doFormat = (~indentation, ~languageConfiguration, lines) => { + let len: int = Array.length(lines); + let out = Array.copy(lines); + + if (len > 0) { + let previousLine = ref(lines[0]); + let beforePreviousLine = ref(None); + let currentIndentationLevel = ref(None); + + for (idx in 0 to len - 1) { + let line = lines[idx]; + + if (StringEx.isOnlyWhitespace(line)) { + out[idx] = ""; + } else { + let indentLevel = + switch (currentIndentationLevel^) { + | None => Indentation.getLevel(indentation, line) + | Some(indent) => + let increaseIndentAmount = + LanguageConfiguration.shouldIncreaseIndent( + ~previousLine=previousLine^, + ~beforePreviousLine=beforePreviousLine^, + languageConfiguration, + ) + ? 1 : 0; + + let decreaseIndentAmount = + LanguageConfiguration.shouldDecreaseIndent( + ~line, + languageConfiguration, + ) + ? (-1) : 0; + + indent + increaseIndentAmount + decreaseIndentAmount; + }; + + out[idx] = + Indentation.applyLevel( + ~indentation, + ~level=indentLevel, + lines[idx], + ); + + currentIndentationLevel := Some(indentLevel); + beforePreviousLine := Some(previousLine^); + previousLine := line; + }; + }; + }; + out; + }; + + let%test_module "format" = + (module + { + let buffer = lines => lines |> Array.of_list; + + let indent2Spaces = + IndentationSettings.{mode: Spaces, size: 2, tabSize: 2}; + let indent3Spaces = + IndentationSettings.{mode: Spaces, size: 3, tabSize: 3}; + + let indentTabs = IndentationSettings.{mode: Tabs, size: 1, tabSize: 2}; + + let languageConfiguration = LanguageConfiguration.default; + + let%test "empty array" = { + buffer([]) + |> doFormat(~indentation=indent2Spaces, ~languageConfiguration) + == [||]; + }; + + let%test "no indent" = { + buffer(["abc"]) + |> doFormat(~indentation=indent2Spaces, ~languageConfiguration) + == [|"abc"|]; + }; + + let%test "simple indent" = { + buffer(["{", "abc"]) + |> doFormat(~indentation=indent2Spaces, ~languageConfiguration) + == [|"{", " abc"|]; + }; + + let%test "increase / decrease indent" = { + buffer(["{", "abc", "}"]) + |> doFormat(~indentation=indent2Spaces, ~languageConfiguration) + == [|"{", " abc", "}"|]; + }; + + let%test "2-spaces replaced with 3-spaces" = { + buffer(["{", " abc", "}"]) + |> doFormat(~indentation=indent3Spaces, ~languageConfiguration) + == [|"{", " abc", "}"|]; + }; + + let%test "spaces replaced with tabs" = { + buffer(["{", " abc", "}"]) + |> doFormat(~indentation=indentTabs, ~languageConfiguration) + == [|"{", "\tabc", "}"|]; + }; + + let%test "tabs replaced with spaces" = { + buffer(["{", "\tabc", "}"]) + |> doFormat(~indentation=indent2Spaces, ~languageConfiguration) + == [|"{", " abc", "}"|]; + }; + + let%test "initial whitespace is ignored" = { + buffer([" ", "{", "abc", "}"]) + |> doFormat(~indentation=indent2Spaces, ~languageConfiguration) + == [|"", "{", " abc", "}"|]; + }; + + let%test "extraneous whitespace is ignored" = { + buffer(["{", " ", "abc", "}"]) + |> doFormat(~indentation=indent2Spaces, ~languageConfiguration) + == [|"{", "", " abc", "}"|]; + }; + + let%test "increase / decrease indent (tabs)" = { + buffer(["{", "abc", "}"]) + |> doFormat(~indentation=indentTabs, ~languageConfiguration) + == [|"{", "\tabc", "}"|]; + }; + }); +}; + +let format = (~indentation, ~languageConfiguration, ~startLineNumber, lines) => { + let len: int = Array.length(lines); + let lenIdx = + Index.toZeroBased(startLineNumber) + len - 1 |> Index.fromZeroBased; + let out = Internal.doFormat(~indentation, ~languageConfiguration, lines); + + let edit = + Vim.Edit.{ + range: + Range.{ + start: { + line: startLineNumber, + column: Index.zero, + }, + stop: { + line: lenIdx, + column: Index.fromZeroBased(Zed_utf8.length(lines[len - 1])), + }, + }, + text: out, + }; + + [edit]; +}; diff --git a/src/Feature/Formatting/DefaultFormatter.rei b/src/Feature/Formatting/DefaultFormatter.rei new file mode 100644 index 0000000000..3479c1f609 --- /dev/null +++ b/src/Feature/Formatting/DefaultFormatter.rei @@ -0,0 +1,17 @@ +open EditorCoreTypes; +open Oni_Core; + +// DefaultFormatter is used as a fall-back formatter when +// a formatter is not provided by a language extension. + +// [format(~indentation, ~languageConfiguration, lines)] returns +// a set of edits to correctly indent the provided [lines], based on +// [indentation] and [languageConfiguration] settings. +let format: + ( + ~indentation: IndentationSettings.t, + ~languageConfiguration: LanguageConfiguration.t, + ~startLineNumber: Index.t, + array(string) + ) => + list(Vim.Edit.t); diff --git a/src/Feature/Formatting/Feature_Formatting.re b/src/Feature/Formatting/Feature_Formatting.re index e5ac3b20e1..3f97ddb8c3 100644 --- a/src/Feature/Formatting/Feature_Formatting.re +++ b/src/Feature/Formatting/Feature_Formatting.re @@ -1,4 +1,6 @@ +open EditorCoreTypes; open Exthost; +open Oni_Core; // A format [session] describes a currently in-progress format request. type session = { @@ -30,11 +32,15 @@ let initial = { [@deriving show] type command = | FormatDocument - | FormatRange; + | FormatSelection; [@deriving show] type msg = | Command(command) + | FormatRange({ + startLine: Index.t, + endLine: Index.t, + }) | DocumentFormatterAvailable({ handle: int, selector: Exthost.DocumentSelector.t, @@ -97,56 +103,103 @@ module Internal = { text: textToArray(edit.text), }; + let fallBackToDefaultFormatter = + (~indentation, ~languageConfiguration, ~buffer, range: Range.t) => { + let lines = buffer |> Oni_Core.Buffer.getLines; + + let startLine = Index.toZeroBased(range.start.line); + let stopLine = Index.toZeroBased(range.stop.line); + + if (startLine >= 0 + && startLine < Array.length(lines) + && stopLine < Array.length(lines)) { + let lines = Array.sub(lines, startLine, stopLine - startLine + 1); + + lines |> Array.iter(prerr_endline); + + let edits = + DefaultFormatter.format( + ~indentation, + ~languageConfiguration, + ~startLineNumber=range.start.line, + lines, + ); + + let displayName = "Default"; + if (edits == []) { + FormattingApplied({displayName, editCount: 0}); + } else { + let effect = + Service_Vim.Effects.applyEdits( + ~bufferId=buffer |> Oni_Core.Buffer.getId, + ~version=buffer |> Oni_Core.Buffer.getVersion, + ~edits, + fun + | Ok () => + EditCompleted({editCount: List.length(edits), displayName}) + | Error(msg) => EditRequestFailed({sessionId: 0, msg}), + ); + Effect(effect); + }; + } else { + FormatError("Invalid range specified"); + }; + }; + let runFormat = ( + ~languageConfiguration, ~formatFn, ~model, ~configuration, ~matchingFormatters, ~buf, - ~filetype, ~extHostClient, + ~range, ) => { let sessionId = model.nextSessionId; let indentation = Oni_Core.Indentation.getForBuffer(~buffer=buf, configuration); - let effects = - matchingFormatters - |> List.map(formatter => - formatFn( - ~handle=formatter.handle, - ~uri=Oni_Core.Buffer.getUri(buf), - ~options= - Exthost.FormattingOptions.{ - tabSize: indentation.tabSize, - insertSpaces: - indentation.mode == Oni_Core.IndentationSettings.Spaces, - }, - extHostClient, - res => { - switch (res) { - | Ok(edits) => - EditsReceived({ - displayName: formatter.displayName, - sessionId, - edits: List.map(extHostEditToVimEdit, edits), - }) - | Error(msg) => EditRequestFailed({sessionId, msg}) - } - }) - ) - |> Isolinear.Effect.batch; - if (matchingFormatters == []) { ( model, - FormatError( - Printf.sprintf("No format providers available for %s", filetype), + fallBackToDefaultFormatter( + ~indentation, + ~languageConfiguration, + ~buffer=buf, + range, ), ); } else { + let effects = + matchingFormatters + |> List.map(formatter => + formatFn( + ~handle=formatter.handle, + ~uri=Oni_Core.Buffer.getUri(buf), + ~options= + Exthost.FormattingOptions.{ + tabSize: indentation.tabSize, + insertSpaces: + indentation.mode == Oni_Core.IndentationSettings.Spaces, + }, + extHostClient, + res => { + switch (res) { + | Ok(edits) => + EditsReceived({ + displayName: formatter.displayName, + sessionId, + edits: List.map(extHostEditToVimEdit, edits), + }) + | Error(msg) => EditRequestFailed({sessionId, msg}) + } + }) + ) + |> Isolinear.Effect.batch; + (model |> startSession(~sessionId, ~buffer=buf), Effect(effects)); }; }; @@ -154,6 +207,7 @@ module Internal = { let update = ( + ~languageConfiguration, ~configuration, ~maybeSelection, ~maybeBuffer, @@ -162,7 +216,47 @@ let update = msg, ) => { switch (msg) { - | Command(FormatRange) => + | FormatRange({startLine, endLine}) => + switch (maybeBuffer) { + | Some(buf) => + let range = + Range.{ + start: { + line: startLine, + column: Index.zero, + }, + stop: { + line: endLine, + column: Index.zero, + }, + }; + let filetype = + buf + |> Oni_Core.Buffer.getFileType + |> Option.value(~default="plaintext"); + + let matchingFormatters = + model.availableRangeFormatters + |> List.filter(({selector, _}) => + DocumentSelector.matches(~filetype, selector) + ); + + Internal.runFormat( + ~languageConfiguration, + ~formatFn= + Service_Exthost.Effects.LanguageFeatures.provideDocumentRangeFormattingEdits( + ~range, + ), + ~model, + ~configuration, + ~matchingFormatters, + ~buf, + ~extHostClient, + ~range, + ); + | None => (model, FormatError("No range selected")) + } + | Command(FormatSelection) => switch (maybeBuffer, maybeSelection) { | (Some(buf), Some(range)) => let filetype = @@ -177,6 +271,7 @@ let update = ); Internal.runFormat( + ~languageConfiguration, ~formatFn= Service_Exthost.Effects.LanguageFeatures.provideDocumentRangeFormattingEdits( ~range, @@ -185,8 +280,8 @@ let update = ~configuration, ~matchingFormatters, ~buf, - ~filetype, ~extHostClient, + ~range, ); | _ => (model, FormatError("No range selected.")) } @@ -207,13 +302,25 @@ let update = ); Internal.runFormat( + ~languageConfiguration, ~formatFn=Service_Exthost.Effects.LanguageFeatures.provideDocumentFormattingEdits, ~model, ~configuration, ~matchingFormatters, ~buf, - ~filetype, ~extHostClient, + ~range= + Range.{ + start: { + line: Index.zero, + column: Index.zero, + }, + stop: { + line: + Oni_Core.Buffer.getNumberOfLines(buf) |> Index.fromZeroBased, + column: Index.zero, + }, + }, ); } | DocumentFormatterAvailable({handle, selector, displayName}) => ( diff --git a/src/Feature/Formatting/Feature_Formatting.rei b/src/Feature/Formatting/Feature_Formatting.rei index 016c83f8f7..381f93cd5d 100644 --- a/src/Feature/Formatting/Feature_Formatting.rei +++ b/src/Feature/Formatting/Feature_Formatting.rei @@ -8,11 +8,15 @@ let initial: model; [@deriving show] type command = | FormatDocument - | FormatRange; + | FormatSelection; [@deriving show] type msg = | Command(command) + | FormatRange({ + startLine: Index.t, + endLine: Index.t, + }) | DocumentFormatterAvailable({ handle: int, selector: Exthost.DocumentSelector.t, @@ -48,6 +52,7 @@ type outmsg = let update: ( + ~languageConfiguration: Oni_Core.LanguageConfiguration.t, ~configuration: Oni_Core.Configuration.t, ~maybeSelection: option(Range.t), ~maybeBuffer: option(Oni_Core.Buffer.t), diff --git a/src/Feature/Formatting/dune b/src/Feature/Formatting/dune index 94c8c872fc..133f371ba2 100644 --- a/src/Feature/Formatting/dune +++ b/src/Feature/Formatting/dune @@ -1,6 +1,7 @@ (library (name Feature_Formatting) (public_name Oni2.feature.formatting) + (inline_tests) (libraries Oni2.core Oni2.components @@ -11,4 +12,4 @@ Revery isolinear ) - (preprocess (pps ppx_let ppx_deriving.show brisk-reconciler.ppx))) + (preprocess (pps ppx_let ppx_deriving.show brisk-reconciler.ppx ppx_inline_test))) diff --git a/src/Store/Features.re b/src/Store/Features.re index e63521655f..ab14416512 100644 --- a/src/Store/Features.re +++ b/src/Store/Features.re @@ -1,8 +1,11 @@ open Isolinear; open Oni_Core; +open Oni_Core.Utility; open Oni_Model; open Actions; +module Ext = Oni_Extensions; + module Internal = { let notificationEffect = (~kind, message) => { Feature_Notification.Effects.create(~kind, message) @@ -45,8 +48,18 @@ let update = state.layout |> Feature_Layout.activeEditor |> Feature_Editor.Editor.selectionOrCursorRange; + + let languageConfiguration = + maybeBuffer + |> OptionEx.flatMap(Oni_Core.Buffer.getFileType) + |> OptionEx.flatMap( + Ext.LanguageInfo.getLanguageConfiguration(state.languageInfo), + ) + |> Option.value(~default=LanguageConfiguration.default); + let (model', eff) = Feature_Formatting.update( + ~languageConfiguration, ~configuration=state.configuration, ~maybeBuffer, ~maybeSelection=Some(selection), diff --git a/src/Store/VimStoreConnector.re b/src/Store/VimStoreConnector.re index de6f846c34..9ac24112ce 100644 --- a/src/Store/VimStoreConnector.re +++ b/src/Store/VimStoreConnector.re @@ -127,9 +127,11 @@ let start = dispatch( Actions.Formatting(Feature_Formatting.Command(FormatDocument)), ) - | Format(Range(_)) => + | Format(Range({startLine, endLine, _})) => dispatch( - Actions.Formatting(Feature_Formatting.Command(FormatRange)), + Actions.Formatting( + Feature_Formatting.FormatRange({startLine, endLine}), + ), ), ); diff --git a/test/Core/LanguageConfigurationTest.re b/test/Core/LanguageConfigurationTest.re index 596347e433..14cc56aa5f 100644 --- a/test/Core/LanguageConfigurationTest.re +++ b/test/Core/LanguageConfigurationTest.re @@ -152,15 +152,6 @@ describe("LanguageConfiguration", ({describe, test, _}) => { == Vim.AutoIndent.IncreaseIndent, true, ); - expect.equal( - LanguageConfiguration.toAutoIndent( - langConfig, - ~previousLine="def", - ~beforePreviousLine=None, - ) - == Vim.AutoIndent.DecreaseIndent, - true, - ); }); test("falls back to brackets", ({expect, _}) => { let parsedLangConfig =