This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch gremlint-v2
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 51169585c43d5af6e2df48891dc3f1761c5f1b97
Author: Stephen Mallette <[email protected]>
AuthorDate: Mon Dec 22 10:04:23 2025 -0500

    Kept initial argument on same line as step for gremlint
    
    If the initial argument to a step exceeded max line length, gremlint would 
immediately line break and indent. It never looked like well formatted Gremlin 
in those cases. Changed it so that the initial argument would stay on same line 
and then align remaining parts to that column where the argument started.
    
    Added greedy argument packing
---
 .gitignore                                         |   2 -
 CHANGELOG.asciidoc                                 |   2 +
 docs/src/upgrade/release-3.8.1.asciidoc            |  85 +++++++++++++
 .../__tests__/closureIndentation.test.ts           |  77 +++++-------
 .../formatQuery/__tests__/defaultConfig.test.ts    |   5 +-
 .../determineWhatPartsOfCodeAreGremlin.test.ts     |   5 +-
 .../__tests__/dotsAfterLineBreaks.test.ts          |  18 ++-
 .../invalidIndentationAndMaxLineLength.test.ts     |   3 +-
 .../src/formatQuery/__tests__/layoutUtils.test.ts  |  68 ++++++++++
 .../__tests__/modulatorIndentation.test.ts         | 140 ++++++++++++---------
 .../__tests__/modulatorWrapping.test.ts            |  12 +-
 .../formatQuery/formatSyntaxTrees/formatClosure.ts |  56 +++++++--
 .../formatQuery/formatSyntaxTrees/formatMethod.ts  | 108 +++++++++++++---
 .../getStepGroups/reduceSingleStepInStepGroup.ts   |   2 +-
 .../formatTraversal/getStepGroups/utils.ts         |  18 +--
 .../formatSyntaxTrees/formatTraversal/index.ts     |  16 ++-
 gremlint/src/formatQuery/index.ts                  |   2 +-
 gremlint/src/formatQuery/layoutUtils.ts            |  49 ++++++++
 .../recreateQueryOnelinerFromSyntaxTree.ts         |  23 ++--
 .../recreateQueryStringFromFormattedSyntaxTrees.ts |  35 +++---
 gremlint/src/formatQuery/types.ts                  |   1 +
 21 files changed, 533 insertions(+), 194 deletions(-)

diff --git a/.gitignore b/.gitignore
index 6dc5464f5e..d27fcdbbb8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,8 +42,6 @@ NuGet.Config
 nuget*.exe
 BenchmarkDotNet.Artifacts/
 /Dockerfile
-docs/gremlint/
-gremlint/
 coverage.out
 .env
 gremlinconsoletest.egg-info
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 98850d85b8..349d8b2dfb 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -27,6 +27,8 @@ 
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 
 This release also includes changes from <<release-3-7-6, 3.7.6>>.
 
+* Improved Gremlint formatting to keep the first argument for a step on the 
same line if line breaks were required to meet max line length.
+* Improved Gremlint formatting to do greedy argument packing when possible so 
that more arguments can appear on a single line.
 
 [[release-3-8-0]]
 === TinkerPop 3.8.0 (Release Date: November 12, 2025)
diff --git a/docs/src/upgrade/release-3.8.1.asciidoc 
b/docs/src/upgrade/release-3.8.1.asciidoc
index 6da249a90d..a1e1c4dbf1 100644
--- a/docs/src/upgrade/release-3.8.1.asciidoc
+++ b/docs/src/upgrade/release-3.8.1.asciidoc
@@ -23,6 +23,91 @@ complete list of all the modifications that are part of this 
release.
 
 === Upgrading for Users
 
+==== Gremlint Improvements
+
+Gremlint's approach to newlines often produced formatting that didn't 
generally follow the espoused best practices.
+Specifically, if Gremlint found that arguments to a step would exceed the line 
length, it would instantly apply a
+newline and then do an indent. This formatting rule made certain Gremlin 
appear stretched out vertically in many cases.
+
+The following example demonstrates this stretching. The following bit of 
Gremlin is manually formatted according to
+common norms and best practices:
+
+[source,groovy]
+----
+g.V().as("v").
+  repeat(both().simplePath().as("v")).emit().
+  filter(project("x","y","z").by(select(first, "v")).
+                              by(select(last, "v")).
+                              by(select(all, "v").count(local)).as("triple").
+         coalesce(select("x","y").as("a").
+                    select("triples").unfold().as("t").
+                    select("x","y").where(eq("a")).
+                    select("t"),
+                  local(aggregate("triples"))).
+         select("z").as("length").
+         select("triple").select("z").where(eq("length"))).
+  select(all, "v").unfold().
+  groupCount()
+----
+
+In earlier versions of Gremlint, it would format that query to:
+
+[source,groovy]
+----
+g.V().as("v").
+  repeat(both().simplePath().as("v")).emit().
+  filter(
+    project("x", "y", "z").
+      by(select(first, "v")).
+      by(select(last, "v")).
+      by(select(all, "v").count(local)).
+      as("triple").
+    coalesce(
+      select("x", "y").as("a").
+      select("triples").
+      unfold().as("t").
+      select("x", "y").
+      where(eq("a")).
+      select("t"),
+      local(aggregate("triples"))).
+    select("z").as("length").
+    select("triple").
+    select("z").
+    where(eq("length"))).
+  select(all, "v").
+  unfold().
+  groupCount()
+----
+
+In this version, after the improvements mentioned above, Gremlint now produces:
+
+[source,groovy]
+----
+g.V().as("v").
+  repeat(both().simplePath().as("v")).emit().
+  filter(project("x", "y", "z").
+           by(select(first, "v")).
+           by(select(last, "v")).
+           by(select(all, "v").count(local)).
+           as("triple").
+         coalesce(select("x", "y").as("a").
+                  select("triples").
+                  unfold().as("t").
+                  select("x", "y").
+                  where(eq("a")).
+                  select("t"), local(aggregate("triples"))).
+         select("z").as("length").
+         select("triple").
+         select("z").
+         where(eq("length"))).
+  select(all, "v").
+  unfold().
+  groupCount()
+----
+
+This more compact representation presents a form much more in line with the 
manually formatted one. While there is still
+room to improve, Gremlint now produces a format that is more likely to be 
usable without additional manual formatting
+intervention
 
 === Upgrading for Providers
 
diff --git a/gremlint/src/formatQuery/__tests__/closureIndentation.test.ts 
b/gremlint/src/formatQuery/__tests__/closureIndentation.test.ts
index f0aaadfde9..43326fb31a 100644
--- a/gremlint/src/formatQuery/__tests__/closureIndentation.test.ts
+++ b/gremlint/src/formatQuery/__tests__/closureIndentation.test.ts
@@ -79,11 +79,10 @@ by{ it.get().value('sell_price') -
     ),
   ).toBe(
     `g.V().
-  filter(
-    out('Sells').
-    map{ it.get('sell_price') -
-         it.get('buy_price') }.
-    where(gt(50)))`,
+  filter(out('Sells').
+         map{ it.get('sell_price') -
+              it.get('buy_price') }.
+         where(gt(50)))`,
   );
 
   expect(
@@ -115,10 +114,9 @@ by{ it.get().value('sell_price') -
       { indentation: 0, maxLineLength: 22, shouldPlaceDotsAfterLineBreaks: 
false },
     ),
   ).toBe(`g.V().
-  filter(
-    map{ one   = 1
-         two   = 2
-         three = 3 }))`);
+  filter(map{ one   = 1
+              two   = 2
+              three = 3 }))`);
 
   expect(
     formatQuery(
@@ -149,10 +147,10 @@ by{ it.get().value('sell_price') -
       { indentation: 0, maxLineLength: 45, shouldPlaceDotsAfterLineBreaks: 
false },
     ),
   ).toBe(`g.V().
-  where(
-    map{ buyPrice  = it.get().value('buy_price');
-         sellPrice = it.get().value('sell_price');
-         sellPrice - buyPrice; }.is(gt(50)))`);
+  where(map{ buyPrice  = it.get().value('buy_price');
+             sellPrice = it.get().value('sell_price');
+             sellPrice - buyPrice; }.
+        is(gt(50)))`);
 
   expect(
     formatQuery(
@@ -183,10 +181,11 @@ by{ it.get().value('sell_price') -
       { indentation: 0, maxLineLength: 50, shouldPlaceDotsAfterLineBreaks: 
false },
     ),
   ).toBe(`g.V().
-  where(
-    out().map{ buyPrice  = it.get().value('buy_price');
-               sellPrice = it.get().value('sell_price');
-               sellPrice - buyPrice; }.is(gt(50)))`);
+  where(out().
+        map{ buyPrice  = it.get().value('buy_price');
+             sellPrice = it.get().value('sell_price');
+             sellPrice - buyPrice; }.
+        is(gt(50)))`);
   expect(
     formatQuery(
       `g.V().where(out().map{ buyPrice  = it.get().value('buy_price');
@@ -195,12 +194,11 @@ by{ it.get().value('sell_price') -
       { indentation: 0, maxLineLength: 45, shouldPlaceDotsAfterLineBreaks: 
false },
     ),
   ).toBe(`g.V().
-  where(
-    out().
-    map{ buyPrice  = it.get().value('buy_price');
-         sellPrice = it.get().value('sell_price');
-         sellPrice - buyPrice; }.
-    is(gt(50)))`);
+  where(out().
+        map{ buyPrice  = it.get().value('buy_price');
+             sellPrice = it.get().value('sell_price');
+             sellPrice - buyPrice; }.
+        is(gt(50)))`);
 
   // Test that relative indentation is preserved between all the lines within 
a closure when not all tokens in a stepGroup are methods (for instance, g in 
g.V() adds to the width of the stepGroup even if it is not a method)
   expect(
@@ -246,15 +244,12 @@ by{ it.get().value('sell_price') -
   ).toBe(`g.V(ids).
   has('factor_a').
   has('factor_b').
-  project(
-    'Factor A',
-    'Factor B',
-    'Product').
+  project('Factor A', 'Factor B',
+          'Product').
     by(values('factor_a')).
     by(values('factor_b')).
-    by(
-      map{ it.get().value('factor_a') *
-           it.get().value('factor_b') })`);
+    by(map{ it.get().value('factor_a') *
+            it.get().value('factor_b') })`);
 
   // Test that relative indentation is preserved between all lines within a 
closure when dots are placed after line breaks
   // When the whole query is long enough to wrap
@@ -300,12 +295,11 @@ by{ it.get().value('sell_price') -
       { indentation: 0, maxLineLength: 45, shouldPlaceDotsAfterLineBreaks: 
true },
     ),
   ).toBe(`g.V()
-  .where(
-    out()
-    .map{ buyPrice  = it.get().value('buy_price');
-          sellPrice = it.get().value('sell_price');
-          sellPrice - buyPrice; }
-    .is(gt(50)))`);
+  .where(out()
+         .map{ buyPrice  = it.get().value('buy_price');
+               sellPrice = it.get().value('sell_price');
+               sellPrice - buyPrice; }
+         .is(gt(50)))`);
 
   // When the query is long enough to wrap, but the traversal containing the 
closure is the first step in its traversal and not long enough to wrap
   expect(
@@ -323,15 +317,12 @@ by{ it.get().value('sell_price') -
   ).toBe(`g.V(ids)
   .has('factor_a')
   .has('factor_b')
-  .project(
-    'Factor A',
-    'Factor B',
-    'Product')
+  .project('Factor A', 'Factor B',
+           'Product')
     .by(values('factor_a'))
     .by(values('factor_b'))
-    .by(
-      map{ it.get().value('factor_a') *
-           it.get().value('factor_b') })`);
+    .by(map{ it.get().value('factor_a') *
+             it.get().value('factor_b') })`);
 
   // When the whole query is short enough to not wrap
   expect(
diff --git a/gremlint/src/formatQuery/__tests__/defaultConfig.test.ts 
b/gremlint/src/formatQuery/__tests__/defaultConfig.test.ts
index c6ace66189..2247b17903 100644
--- a/gremlint/src/formatQuery/__tests__/defaultConfig.test.ts
+++ b/gremlint/src/formatQuery/__tests__/defaultConfig.test.ts
@@ -49,9 +49,8 @@ test('It should be possible ot use formatQuery with a config, 
with a partial con
   ).toBe(`        g.V()
           .has('person', 'name', 'marko')
           .shortestPath()
-            .with(
-              ShortestPath.target,
-              __.has('name', 'josh'))
+            .with(ShortestPath.target,
+                  __.has('name', 'josh'))
             .with(ShortestPath.distance, 'weight')`);
 
   // Test using formatQuery with an empty config
diff --git 
a/gremlint/src/formatQuery/__tests__/determineWhatPartsOfCodeAreGremlin.test.ts 
b/gremlint/src/formatQuery/__tests__/determineWhatPartsOfCodeAreGremlin.test.ts
index 3f1a2e66e1..2123607fda 100644
--- 
a/gremlint/src/formatQuery/__tests__/determineWhatPartsOfCodeAreGremlin.test.ts
+++ 
b/gremlint/src/formatQuery/__tests__/determineWhatPartsOfCodeAreGremlin.test.ts
@@ -38,9 +38,8 @@ g.V().filter(values('name').filter(contains('Gremlint')))`,
 }
 
 g.V().
-  filter(
-    values('name').
-    filter(contains('Gremlint')))`);
+  filter(values('name').
+         filter(contains('Gremlint')))`);
 
   expect(
     formatQuery(
diff --git a/gremlint/src/formatQuery/__tests__/dotsAfterLineBreaks.test.ts 
b/gremlint/src/formatQuery/__tests__/dotsAfterLineBreaks.test.ts
index 831e1b8479..4c947f0202 100644
--- a/gremlint/src/formatQuery/__tests__/dotsAfterLineBreaks.test.ts
+++ b/gremlint/src/formatQuery/__tests__/dotsAfterLineBreaks.test.ts
@@ -34,10 +34,9 @@ test('If dots are configured to be placed after line breaks, 
make sure they are
   group().
     by(values('name', 'age').fold()).
   unfold().
-  filter(
-    select(values).
-    count(local).
-    is(gt(1)))`);
+  filter(select(values).
+         count(local).
+         is(gt(1)))`);
 
   expect(
     formatQuery(
@@ -51,11 +50,10 @@ test('If dots are configured to be placed after line 
breaks, make sure they are
   ).toBe(`g.V()
   .hasLabel('person')
   .group()
-    .by(
-      values('name', 'age').fold())
+    .by(values('name', 'age')
+        .fold())
   .unfold()
-  .filter(
-    select(values)
-    .count(local)
-    .is(gt(1)))`);
+  .filter(select(values)
+          .count(local)
+          .is(gt(1)))`);
 });
diff --git 
a/gremlint/src/formatQuery/__tests__/invalidIndentationAndMaxLineLength.test.ts 
b/gremlint/src/formatQuery/__tests__/invalidIndentationAndMaxLineLength.test.ts
index e743a11b0d..0aed02e7ea 100644
--- 
a/gremlint/src/formatQuery/__tests__/invalidIndentationAndMaxLineLength.test.ts
+++ 
b/gremlint/src/formatQuery/__tests__/invalidIndentationAndMaxLineLength.test.ts
@@ -26,6 +26,5 @@ test('The formatter should not crash when indentation is 
equal to maxLineLength'
       maxLineLength: 0,
       shouldPlaceDotsAfterLineBreaks: false,
     }),
-  ).toBe(`g.V(
-)`);
+  ).toBe(`g.V()`);
 });
diff --git a/gremlint/src/formatQuery/__tests__/layoutUtils.test.ts 
b/gremlint/src/formatQuery/__tests__/layoutUtils.test.ts
new file mode 100644
index 0000000000..af14c0c164
--- /dev/null
+++ b/gremlint/src/formatQuery/__tests__/layoutUtils.test.ts
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { willFitOnLine, getOneLinerWidth } from '../layoutUtils';
+import { TokenType, UnformattedSyntaxTree } from '../types';
+
+describe('layoutUtils', () => {
+  const config = {
+    globalIndentation: 0,
+    maxLineLength: 20,
+  };
+
+  const simpleWord: UnformattedSyntaxTree = {
+    type: TokenType.Word,
+    word: 'g.V()',
+  };
+
+  const longWord: UnformattedSyntaxTree = {
+    type: TokenType.Word,
+    word: 'veryLongTraversalDefinition',
+  };
+
+  describe('willFitOnLine', () => {
+    test('should return true if query fits', () => {
+      expect(willFitOnLine(simpleWord, config)).toBe(true);
+    });
+
+    test('should return false if query exceeds maxLineLength', () => {
+      expect(willFitOnLine(longWord, config)).toBe(false);
+    });
+
+    test('should account for global indentation', () => {
+      const indentedConfig = { ...config, globalIndentation: 16 };
+      expect(willFitOnLine(simpleWord, indentedConfig)).toBe(false);
+    });
+
+    test('should account for horizontal position', () => {
+      expect(willFitOnLine(simpleWord, config, 16)).toBe(false);
+    });
+  });
+
+  describe('getOneLinerWidth', () => {
+    test('should return the correct width', () => {
+      expect(getOneLinerWidth(simpleWord)).toBe(5);
+    });
+
+    test('should account for horizontal position in width if applicable 
(though usually trimmed)', () => {
+       // getOneLinerWidth trims the result
+       expect(getOneLinerWidth(simpleWord, 10)).toBe(5);
+    });
+  });
+});
diff --git a/gremlint/src/formatQuery/__tests__/modulatorIndentation.test.ts 
b/gremlint/src/formatQuery/__tests__/modulatorIndentation.test.ts
index 4ebb342921..8357b409bc 100644
--- a/gremlint/src/formatQuery/__tests__/modulatorIndentation.test.ts
+++ b/gremlint/src/formatQuery/__tests__/modulatorIndentation.test.ts
@@ -71,10 +71,9 @@ test('Wrapped modulators should be indented with two 
spaces', () => {
   group().
     by(values('name', 'age').fold()).
   unfold().
-  filter(
-    select(values).
-    count(local).
-    is(gt(1)))`);
+  filter(select(values).
+         count(local).
+         is(gt(1)))`);
   expect(
     formatQuery(
       
"g.V().hasLabel('person').groupCount().by(values('age').choose(is(lt(28)),constant('young'),choose(is(lt(30)),
 constant('old'), constant('very old'))))",
@@ -87,12 +86,9 @@ test('Wrapped modulators should be indented with two 
spaces', () => {
   ).toBe(`g.V().
   hasLabel('person').
   groupCount().
-    by(
-      values('age').
-      choose(
-        is(lt(28)),
-        constant('young'),
-        choose(is(lt(30)), constant('old'), constant('very old'))))`);
+    by(values('age').
+       choose(is(lt(28)), constant('young'),
+              choose(is(lt(30)), constant('old'), constant('very old'))))`);
 
   // Test emit()-modulator indentation
   expect(
@@ -161,10 +157,9 @@ test('Wrapped modulators should be indented with two 
spaces', () => {
     ),
   ).toBe(
     `g.V().
-  has(
-    'person',
-    'name',
-    'vadas').
+  has('person',
+      'name',
+      'vadas').
     as('e').
   in('knows').
     as('m').
@@ -187,10 +182,9 @@ test('Wrapped modulators should be indented with two 
spaces', () => {
     ),
   ).toBe(
     `g.V().
-  has(
-    'person',
-    'name',
-    'vadas').
+  has('person',
+      'name',
+      'vadas').
     as_('e').
   in('knows').
     as_('m').
@@ -260,9 +254,8 @@ test('Wrapped modulators should be indented with two 
spaces', () => {
     `g.V(v1).
   addE('knows').
     to(v2).
-  property(
-    'weight',
-    0.75).
+  property('weight',
+           0.75).
   iterate()`,
   );
 
@@ -279,12 +272,10 @@ test('Wrapped modulators should be indented with two 
spaces', () => {
   ).toBe(
     `g.V(6).
   repeat('a', both('created').simplePath()).
-    emit(
-      repeat('b', both('knows')).
-        until(
-          loops('b').as('b').
-          where(loops('a').as('b'))).
-      hasId(2)).
+    emit(repeat('b', both('knows')).
+           until(loops('b').as('b').
+                 where(loops('a').as('b'))).
+         hasId(2)).
   dedup()`,
   );
 
@@ -501,15 +492,14 @@ test('Wrapped modulators should be indented with two 
spaces', () => {
       },
     ),
   ).toBe(
-    `g.inject(
-  g.withComputer().
-    V().
-    shortestPath().
-      with(ShortestPath.distance, 'weight').
-      with(ShortestPath.includeEdges, true).
-      with(ShortestPath.maxDistance, 1).
-    toList().
-    toArray()).
+    `g.inject(g.withComputer().
+           V().
+           shortestPath().
+             with(ShortestPath.distance, 'weight').
+             with(ShortestPath.includeEdges, true).
+             with(ShortestPath.maxDistance, 1).
+           toList().
+           toArray()).
   map(unfold().values('name', 'weight').fold())`,
   );
   expect(
@@ -534,9 +524,8 @@ test('Wrapped modulators should be indented with two 
spaces', () => {
     `g.V().
   hasLabel('person').
   valueMap('name').
-    with(
-      WithOptions.tokens,
-      WithOptions.labels)`,
+    with(WithOptions.tokens,
+         WithOptions.labels)`,
   );
   expect(
     formatQuery(
@@ -552,9 +541,8 @@ test('Wrapped modulators should be indented with two 
spaces', () => {
   hasLabel('person').
   properties('location').
   valueMap().
-    with(
-      WithOptions.tokens,
-      WithOptions.values)`,
+    with(WithOptions.tokens,
+         WithOptions.values)`,
   );
 
   // Test with_()-modulator indentation
@@ -760,6 +748,26 @@ test('Wrapped modulators should be indented with two 
spaces', () => {
     with_(ShortestPath.target, __.has('name', 'josh')).
     with_(ShortestPath.includeEdges, true)`,
   );
+  expect(
+    formatQuery(
+      
"g.inject(g.withComputer().V().shortestPath().with_(ShortestPath.distance, 
'weight').with_(ShortestPath.includeEdges, 
true).with_(ShortestPath.maxDistance, 
1).toList().toArray()).map(unfold().values('name','weight').fold())",
+      {
+        indentation: 0,
+        maxLineLength: 52,
+        shouldPlaceDotsAfterLineBreaks: false,
+      },
+    ),
+  ).toBe(
+    `g.inject(g.withComputer().
+           V().
+           shortestPath().
+             with_(ShortestPath.distance, 'weight').
+             with_(ShortestPath.includeEdges, true).
+             with_(ShortestPath.maxDistance, 1).
+           toList().
+           toArray()).
+  map(unfold().values('name', 'weight').fold())`,
+  );
   expect(
     formatQuery(
       
"g.inject(g.withComputer().V().shortestPath().with_(ShortestPath.distance, 
'weight').with_(ShortestPath.includeEdges, 
true).with_(ShortestPath.maxDistance, 
1).toList().toArray()).map(unfold().values('name','weight').fold())",
@@ -770,15 +778,15 @@ test('Wrapped modulators should be indented with two 
spaces', () => {
       },
     ),
   ).toBe(
-    `g.inject(
-  g.withComputer().
-    V().
-    shortestPath().
-      with_(ShortestPath.distance, 'weight').
-      with_(ShortestPath.includeEdges, true).
-      with_(ShortestPath.maxDistance, 1).
-    toList().
-    toArray()).
+    `g.inject(g.withComputer().
+           V().
+           shortestPath().
+             with_(ShortestPath.distance,
+                   'weight').
+             with_(ShortestPath.includeEdges, true).
+             with_(ShortestPath.maxDistance, 1).
+           toList().
+           toArray()).
   map(unfold().values('name', 'weight').fold())`,
   );
   expect(
@@ -803,9 +811,8 @@ test('Wrapped modulators should be indented with two 
spaces', () => {
     `g.V().
   hasLabel('person').
   valueMap('name').
-    with_(
-      WithOptions.tokens,
-      WithOptions.labels)`,
+    with_(WithOptions.tokens,
+          WithOptions.labels)`,
   );
   expect(
     formatQuery(
@@ -821,9 +828,8 @@ test('Wrapped modulators should be indented with two 
spaces', () => {
   hasLabel('person').
   properties('location').
   valueMap().
-    with_(
-      WithOptions.tokens,
-      WithOptions.values)`,
+    with_(WithOptions.tokens,
+          WithOptions.values)`,
   );
 
   // Test write()-modulator indentation
@@ -838,4 +844,24 @@ test('Wrapped modulators should be indented with two 
spaces', () => {
     write().
   iterate()`,
   );
+
+
+  expect(
+    formatQuery(
+      `g.V(ids).
+  has('factor_a').
+  has('factor_b').
+  project('Factor A', 'Factor B', 'Product').
+    by(values('factor_a')).
+    by(values('factor_b')).
+    by(__.out('knows').in('created').has('product','code','xXz-343-94985AD'))`,
+      { indentation: 0, maxLineLength: 79, shouldPlaceDotsAfterLineBreaks: 
false },
+    ),
+  ).toBe(`g.V(ids).
+  has('factor_a').
+  has('factor_b').
+  project('Factor A', 'Factor B', 'Product').
+    by(values('factor_a')).
+    by(values('factor_b')).
+    by(__.out('knows').in('created').has('product', 'code', 
'xXz-343-94985AD'))`);
 });
diff --git a/gremlint/src/formatQuery/__tests__/modulatorWrapping.test.ts 
b/gremlint/src/formatQuery/__tests__/modulatorWrapping.test.ts
index 8859eac95a..5a8198bad2 100644
--- a/gremlint/src/formatQuery/__tests__/modulatorWrapping.test.ts
+++ b/gremlint/src/formatQuery/__tests__/modulatorWrapping.test.ts
@@ -105,8 +105,9 @@ test("Modulators should not be line-wrapped if they can fit 
on the line of the s
   hasLabel('person').
   group().by(values('name', 'age').fold()).
   unfold().
-  filter(
-    select(values).count(local).is(gt(1)))`);
+  filter(select(values).
+         count(local).
+         is(gt(1)))`);
   expect(
     formatQuery(
       "g.V().hasLabel('person').group().by(values('name', 
'age').fold()).unfold().filter(select(values).count(local).is(gt(1)))",
@@ -121,8 +122,7 @@ test("Modulators should not be line-wrapped if they can fit 
on the line of the s
   group().
     by(values('name', 'age').fold()).
   unfold().
-  filter(
-    select(values).
-    count(local).
-    is(gt(1)))`);
+  filter(select(values).
+         count(local).
+         is(gt(1)))`);
 });
diff --git a/gremlint/src/formatQuery/formatSyntaxTrees/formatClosure.ts 
b/gremlint/src/formatQuery/formatSyntaxTrees/formatClosure.ts
index 9bfd023f48..2a7aa703ff 100644
--- a/gremlint/src/formatQuery/formatSyntaxTrees/formatClosure.ts
+++ b/gremlint/src/formatQuery/formatSyntaxTrees/formatClosure.ts
@@ -28,50 +28,75 @@ import {
   UnformattedClosureSyntaxTree,
 } from '../types';
 import { withNoEndDotInfo } from './utils';
+import { getOneLinerWidth, willFitOnLine } from '../layoutUtils';
 
 const getClosureLineOfCodeIndentation = (
   relativeIndentation: number,
   horizontalPosition: number,
   methodWidth: number,
   lineNumber: number,
+  shouldStartWithDot: boolean,
 ) => {
   if (lineNumber === 0) return Math.max(relativeIndentation, 0);
-  return Math.max(relativeIndentation + horizontalPosition + methodWidth + 1, 
0);
+  return Math.max(
+    relativeIndentation + horizontalPosition + methodWidth + 
(shouldStartWithDot ? 1 : 0) + 1,
+    0,
+  );
 };
 
-const getFormattedClosureLineOfCode = (horizontalPosition: number, 
methodWidth: number) => (
-  { lineOfCode, relativeIndentation }: UnformattedClosureLineOfCode,
-  lineNumber: number,
-) => ({
+const getFormattedClosureLineOfCode = (
+  horizontalPosition: number,
+  methodWidth: number,
+  shouldStartWithDot: boolean,
+) => ({ lineOfCode, relativeIndentation }: UnformattedClosureLineOfCode, 
lineNumber: number) => ({
   lineOfCode,
   relativeIndentation,
-  localIndentation: getClosureLineOfCodeIndentation(relativeIndentation, 
horizontalPosition, methodWidth, lineNumber),
+  localIndentation: getClosureLineOfCodeIndentation(
+    relativeIndentation,
+    horizontalPosition,
+    methodWidth,
+    lineNumber,
+    shouldStartWithDot,
+  ),
 });
 
 const getFormattedClosureCodeBlock = (
   unformattedClosureCodeBlock: UnformattedClosureCodeBlock,
   horizontalPosition: number,
   methodWidth: number,
+  shouldStartWithDot: boolean,
 ) => {
-  return 
unformattedClosureCodeBlock.map(getFormattedClosureLineOfCode(horizontalPosition,
 methodWidth));
+  return unformattedClosureCodeBlock.map(
+    getFormattedClosureLineOfCode(horizontalPosition, methodWidth, 
shouldStartWithDot),
+  );
 };
 
 export const formatClosure = (formatSyntaxTree: GremlinSyntaxTreeFormatter) => 
(config: GremlintInternalConfig) => (
   syntaxTree: UnformattedClosureSyntaxTree,
 ): FormattedClosureSyntaxTree => {
   const { closureCodeBlock: unformattedClosureCodeBlock, method: 
unformattedMethod } = syntaxTree;
-  const { localIndentation, horizontalPosition, maxLineLength, 
shouldEndWithDot } = config;
-  const recreatedQuery = 
recreateQueryOnelinerFromSyntaxTree(localIndentation)(syntaxTree);
+  const {
+    localIndentation,
+    horizontalPosition,
+    maxLineLength,
+    shouldEndWithDot,
+    shouldStartWithDot,
+  } = config;
   const formattedMethod = 
formatSyntaxTree(withNoEndDotInfo(config))(unformattedMethod);
   const methodWidth = formattedMethod.width;
 
-  if (recreatedQuery.length <= maxLineLength) {
+  if (willFitOnLine(syntaxTree, config, horizontalPosition)) {
     return {
       type: TokenType.Closure,
       method: formattedMethod,
-      closureCodeBlock: 
getFormattedClosureCodeBlock(unformattedClosureCodeBlock, horizontalPosition, 
methodWidth),
+      closureCodeBlock: getFormattedClosureCodeBlock(
+        unformattedClosureCodeBlock,
+        horizontalPosition,
+        methodWidth,
+        shouldStartWithDot,
+      ),
       localIndentation,
-      width: recreatedQuery.trim().length,
+      width: getOneLinerWidth(syntaxTree, horizontalPosition),
       shouldStartWithDot: false,
       shouldEndWithDot: Boolean(shouldEndWithDot),
     };
@@ -80,7 +105,12 @@ export const formatClosure = (formatSyntaxTree: 
GremlinSyntaxTreeFormatter) => (
   return {
     type: TokenType.Closure,
     method: formattedMethod,
-    closureCodeBlock: 
getFormattedClosureCodeBlock(unformattedClosureCodeBlock, horizontalPosition, 
methodWidth),
+    closureCodeBlock: getFormattedClosureCodeBlock(
+      unformattedClosureCodeBlock,
+      horizontalPosition,
+      methodWidth,
+      shouldStartWithDot,
+    ),
     localIndentation: 0,
     width: 0,
     shouldStartWithDot: false,
diff --git a/gremlint/src/formatQuery/formatSyntaxTrees/formatMethod.ts 
b/gremlint/src/formatQuery/formatSyntaxTrees/formatMethod.ts
index fd4f92b4ff..5a2af97942 100644
--- a/gremlint/src/formatQuery/formatSyntaxTrees/formatMethod.ts
+++ b/gremlint/src/formatQuery/formatSyntaxTrees/formatMethod.ts
@@ -27,10 +27,12 @@ import {
   UnformattedMethodSyntaxTree,
 } from '../types';
 import { last, pipe, sum } from '../utils';
+import { willFitOnLine } from '../layoutUtils';
 import {
   withHorizontalPosition,
   withIncreasedHorizontalPosition,
   withIncreasedIndentation,
+  withIndentation,
   withNoEndDotInfo,
   withZeroDotInfo,
   withZeroIndentation,
@@ -40,10 +42,10 @@ import {
 export const formatMethod = (formatSyntaxTree: GremlinSyntaxTreeFormatter) => 
(config: GremlintInternalConfig) => (
   syntaxTree: UnformattedMethodSyntaxTree,
 ): FormattedMethodSyntaxTree => {
-  const recreatedQuery = 
recreateQueryOnelinerFromSyntaxTree(config.localIndentation)(syntaxTree);
   const method = formatSyntaxTree(withNoEndDotInfo(config))(syntaxTree.method);
-  const argumentsWillNotBeWrapped = recreatedQuery.length <= 
config.maxLineLength;
+  const argumentsWillNotBeWrapped = willFitOnLine(syntaxTree, config, 
config.horizontalPosition);
   if (argumentsWillNotBeWrapped) {
+    const recreatedQuery = recreateQueryOnelinerFromSyntaxTree(config, 
config.horizontalPosition)(syntaxTree);
     return {
       type: TokenType.Method,
       method,
@@ -61,7 +63,11 @@ export const formatMethod = (formatSyntaxTree: 
GremlinSyntaxTreeFormatter) => (c
                 withZeroIndentation,
                 withZeroDotInfo,
                 withIncreasedHorizontalPosition(
-                  method.width + 1 + argumentGroup.map(({ width }) => 
width).reduce(sum, 0) + argumentGroup.length,
+                  method.width +
+                    1 +
+                    (config.shouldStartWithDot ? 1 : 0) +
+                    argumentGroup.map(({ width }) => width).reduce(sum, 0) +
+                    argumentGroup.length,
                 ),
               )(config),
             )(syntaxTree),
@@ -69,34 +75,108 @@ export const formatMethod = (formatSyntaxTree: 
GremlinSyntaxTreeFormatter) => (c
         }, []),
       ],
       argumentsShouldStartOnNewLine: false,
-      localIndentation: config.localIndentation,
       shouldStartWithDot: false,
       shouldEndWithDot: Boolean(config.shouldEndWithDot),
+      localIndentation: config.localIndentation,
+      horizontalPosition: config.horizontalPosition,
       width: recreatedQuery.trim().length,
     };
   }
   // shouldEndWithDot has to reside on the method object, so the end dot can be
   // placed after the method parentheses. shouldStartWithDot has to be passed 
on
   // further down so the start dot can be placed after the indentation.
-  const argumentGroups = syntaxTree.arguments.map((step) => [
+  const horizontalPosition = config.horizontalPosition + method.width + 
(config.shouldStartWithDot ? 1 : 0) + 1;
+
+  const greedyArgumentGroups = syntaxTree.arguments.reduce(
+    (acc: { groups: FormattedSyntaxTree[][]; currentGroup: 
FormattedSyntaxTree[] }, arg) => {
+      const formattedArg = formatSyntaxTree(
+        pipe(withZeroIndentation, withZeroDotInfo, 
withHorizontalPosition(horizontalPosition))(config),
+      )(arg);
+      const currentGroupWidth =
+        acc.currentGroup.map(({ width }) => width).reduce(sum, 0) + 
Math.max(acc.currentGroup.length - 1, 0) * 2;
+      const potentialGroupWidth =
+        acc.currentGroup.length > 0 ? currentGroupWidth + 2 + 
formattedArg.width : formattedArg.width;
+
+      if (acc.currentGroup.length > 0 && horizontalPosition + 
potentialGroupWidth > config.maxLineLength) {
+        return {
+          groups: [...acc.groups, acc.currentGroup],
+          currentGroup: [
+            formatSyntaxTree(
+              pipe(withIndentation(horizontalPosition), withZeroDotInfo, 
withHorizontalPosition(horizontalPosition))(
+                config,
+              ),
+            )(arg),
+          ],
+        };
+      }
+      return {
+        ...acc,
+        currentGroup: [
+          ...acc.currentGroup,
+          formatSyntaxTree(
+            pipe(withIndentation(horizontalPosition), withZeroDotInfo, 
withHorizontalPosition(horizontalPosition))(
+              config,
+            ),
+          )(arg),
+        ],
+      };
+    },
+    { groups: [], currentGroup: [] },
+  );
+  const argumentGroups =
+    greedyArgumentGroups.currentGroup.length > 0
+      ? [...greedyArgumentGroups.groups, greedyArgumentGroups.currentGroup]
+      : greedyArgumentGroups.groups;
+
+  const indentationWidth = horizontalPosition;
+
+  const greedyFits = argumentGroups.every((group) => {
+    const groupWidth = group.map(({ width }) => width).reduce(sum, 0) + 
(group.length - 1) * 2;
+    return indentationWidth + groupWidth <= config.maxLineLength;
+  });
+
+  if (greedyFits) {
+    const lastArgumentGroup = last(argumentGroups);
+    // Add the width of the last line of parameters, the dots between them and 
the indentation of the parameters
+    const width = lastArgumentGroup
+      ? lastArgumentGroup.map(({ width }) => width).reduce(sum, 0) + 
(lastArgumentGroup.length - 1) * 2
+      : 0;
+    return {
+      type: TokenType.Method,
+      method,
+      arguments: syntaxTree.arguments,
+      argumentGroups,
+      argumentsShouldStartOnNewLine: false,
+      localIndentation: config.localIndentation,
+      horizontalPosition: config.horizontalPosition,
+      shouldStartWithDot: false,
+      shouldEndWithDot: Boolean(config.shouldEndWithDot),
+      width,
+    };
+  }
+
+  // Fallback to wrap-all
+  const wrapAllArgumentGroups = syntaxTree.arguments.map((step) => [
     formatSyntaxTree(
-      pipe(withIncreasedIndentation(2), withZeroDotInfo, 
withHorizontalPosition(config.localIndentation + 2))(config),
+      pipe(withIndentation(horizontalPosition), withZeroDotInfo, 
withHorizontalPosition(horizontalPosition))(config),
     )(step),
   ]);
-  const lastArgumentGroup = last(argumentGroups);
-  // Add the width of the last line of parameters, the dots between them and 
the indentation of the parameters
-  const width = lastArgumentGroup
-    ? lastArgumentGroup.map(({ width }) => width).reduce(sum, 0) + 
lastArgumentGroup.length - 1
+
+  const lastWrapAllArgumentGroup = last(wrapAllArgumentGroups);
+  const wrapAllWidth = lastWrapAllArgumentGroup
+    ? lastWrapAllArgumentGroup.map(({ width }) => width).reduce(sum, 0) + 
(lastWrapAllArgumentGroup.length - 1) * 2
     : 0;
+
   return {
     type: TokenType.Method,
     method,
     arguments: syntaxTree.arguments,
-    argumentGroups,
-    argumentsShouldStartOnNewLine: true,
+    argumentGroups: wrapAllArgumentGroups,
+    argumentsShouldStartOnNewLine: false,
+    localIndentation: config.localIndentation,
+    horizontalPosition: config.horizontalPosition,
     shouldStartWithDot: false,
     shouldEndWithDot: Boolean(config.shouldEndWithDot),
-    localIndentation: 0,
-    width,
+    width: wrapAllWidth,
   };
 };
diff --git 
a/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceSingleStepInStepGroup.ts
 
b/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceSingleStepInStepGroup.ts
index 4b90fe7aa2..7c43c86890 100644
--- 
a/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceSingleStepInStepGroup.ts
+++ 
b/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/reduceSingleStepInStepGroup.ts
@@ -69,7 +69,7 @@ const reduceSingleStepInStepGroup = (formatSyntaxTree: 
GremlinSyntaxTreeFormatte
             pipe(
               withIndentation(localIndentation),
               withDotInfo({ shouldStartWithDot, shouldEndWithDot }),
-              withHorizontalPosition(localIndentation + 
+config.shouldPlaceDotsAfterLineBreaks),
+              withHorizontalPosition(localIndentation),
             )(config),
           )(step),
         ],
diff --git 
a/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/utils.ts
 
b/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/utils.ts
index 13392fbe0c..ba3c13b22e 100644
--- 
a/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/utils.ts
+++ 
b/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/getStepGroups/utils.ts
@@ -25,6 +25,7 @@ import {
   UnformattedSyntaxTree,
 } from '../../../types';
 import { STEP_MODULATORS } from '../../../consts';
+import { willFitOnLine } from '../../../layoutUtils';
 import recreateQueryOnelinerFromSyntaxTree from 
'../../../recreateQueryOnelinerFromSyntaxTree';
 
 export const isTraversalSource = (step: UnformattedSyntaxTree | 
FormattedSyntaxTree): boolean => {
@@ -78,15 +79,14 @@ const isLineTooLongWithSubsequentModulators = (config: 
GremlintInternalConfig) =
     return indentationIncrease;
   })();
 
-  const recreatedQueryWithSubsequentModulators = 
recreateQueryOnelinerFromSyntaxTree(
-    config.localIndentation + stepGroupIndentationIncrease,
-  )({
-    type: TokenType.Traversal,
-    steps: stepsWithSubsequentModulators,
-  });
-
-  const lineIsTooLongWithSubsequentModulators = 
recreatedQueryWithSubsequentModulators.length > config.maxLineLength;
-  return lineIsTooLongWithSubsequentModulators;
+  return !willFitOnLine(
+    {
+      type: TokenType.Traversal,
+      steps: stepsWithSubsequentModulators,
+    },
+    config,
+    config.horizontalPosition + stepGroupIndentationIncrease,
+  );
 };
 
 // If the first step in a group is a modulator, then it must also be the last 
step in the group
diff --git 
a/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/index.ts 
b/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/index.ts
index 74417f4c2a..108ac6653f 100644
--- a/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/index.ts
+++ b/gremlint/src/formatQuery/formatSyntaxTrees/formatTraversal/index.ts
@@ -27,6 +27,7 @@ import {
   UnformattedTraversalSyntaxTree,
 } from '../../types';
 import { last, pipe, sum } from '../../utils';
+import { willFitOnLine } from '../../layoutUtils';
 import { withIncreasedHorizontalPosition, withZeroIndentation } from 
'../utils';
 import { getStepGroups } from './getStepGroups';
 import { isTraversalSource } from './getStepGroups/utils';
@@ -37,10 +38,17 @@ export const formatTraversal = (formatSyntaxTree: 
GremlinSyntaxTreeFormatter) =>
 ): FormattedTraversalSyntaxTree => {
   const initialHorizontalPositionIndentationIncrease =
     syntaxTree.steps[0] && isTraversalSource(syntaxTree.steps[0]) ? 
syntaxTree.initialHorizontalPosition : 0;
-  const recreatedQuery = recreateQueryOnelinerFromSyntaxTree(
-    config.localIndentation + initialHorizontalPositionIndentationIncrease,
-  )(syntaxTree);
-  if (recreatedQuery.length <= config.maxLineLength) {
+  if (
+    willFitOnLine(
+      syntaxTree,
+      config,
+      config.horizontalPosition + initialHorizontalPositionIndentationIncrease,
+    )
+  ) {
+    const recreatedQuery = recreateQueryOnelinerFromSyntaxTree(
+      config,
+      config.horizontalPosition + initialHorizontalPositionIndentationIncrease,
+    )(syntaxTree);
     return {
       type: TokenType.Traversal,
       steps: syntaxTree.steps,
diff --git a/gremlint/src/formatQuery/index.ts 
b/gremlint/src/formatQuery/index.ts
index 4f27e3e374..174f89127e 100644
--- a/gremlint/src/formatQuery/index.ts
+++ b/gremlint/src/formatQuery/index.ts
@@ -37,7 +37,7 @@ const getInternalGremlintConfig = ({
 }: GremlintUserConfig): GremlintInternalConfig => ({
   globalIndentation: indentation,
   localIndentation: 0,
-  maxLineLength: maxLineLength - indentation,
+  maxLineLength: maxLineLength,
   shouldPlaceDotsAfterLineBreaks,
   shouldStartWithDot: false,
   shouldEndWithDot: false,
diff --git a/gremlint/src/formatQuery/layoutUtils.ts 
b/gremlint/src/formatQuery/layoutUtils.ts
new file mode 100644
index 0000000000..311c48e0e5
--- /dev/null
+++ b/gremlint/src/formatQuery/layoutUtils.ts
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import recreateQueryOnelinerFromSyntaxTree from 
'./recreateQueryOnelinerFromSyntaxTree';
+import { GremlintInternalConfig, UnformattedSyntaxTree } from './types';
+
+/**
+ * Checks if a syntax tree or a set of syntax trees can fit on a single line
+ * within the maxLineLength constraint.
+ */
+export const willFitOnLine = (
+  syntaxTree: UnformattedSyntaxTree | { type: string; steps: 
UnformattedSyntaxTree[] },
+  config: Pick<GremlintInternalConfig, 'globalIndentation' | 'maxLineLength'>,
+  horizontalPosition: number = 0,
+): boolean => {
+  const recreatedQuery = recreateQueryOnelinerFromSyntaxTree({ 
globalIndentation: 0 }, horizontalPosition)(
+    syntaxTree as any,
+  );
+  return config.globalIndentation + recreatedQuery.length <= 
config.maxLineLength;
+};
+
+/**
+ * Calculates the width of a syntax tree when formatted as a one-liner.
+ */
+export const getOneLinerWidth = (
+  syntaxTree: UnformattedSyntaxTree,
+  horizontalPosition: number = 0,
+): number => {
+  const recreatedQuery = recreateQueryOnelinerFromSyntaxTree({ 
globalIndentation: 0 }, horizontalPosition)(
+    syntaxTree as any,
+  );
+  return recreatedQuery.trim().length;
+};
diff --git a/gremlint/src/formatQuery/recreateQueryOnelinerFromSyntaxTree.ts 
b/gremlint/src/formatQuery/recreateQueryOnelinerFromSyntaxTree.ts
index 9075d7e973..db01ecde81 100644
--- a/gremlint/src/formatQuery/recreateQueryOnelinerFromSyntaxTree.ts
+++ b/gremlint/src/formatQuery/recreateQueryOnelinerFromSyntaxTree.ts
@@ -25,6 +25,7 @@ import {
   UnformattedStringSyntaxTree,
   UnformattedTraversalSyntaxTree,
   UnformattedWordSyntaxTree,
+  GremlintInternalConfig,
 } from './types';
 import { last, spaces } from './utils';
 
@@ -36,27 +37,31 @@ type GremlinOnelinerSyntaxTree =
   | Pick<UnformattedStringSyntaxTree, 'type' | 'string'>
   | Pick<UnformattedWordSyntaxTree, 'type' | 'word'>;
 
-const recreateQueryOnelinerFromSyntaxTree = (localIndentation: number = 0) => (
+const recreateQueryOnelinerFromSyntaxTree = (config: 
Pick<GremlintInternalConfig, 'globalIndentation'>, localIndentation: number = 
0) => (
   syntaxTree: GremlinOnelinerSyntaxTree,
 ): string => {
+  const indentation = spaces(config.globalIndentation + localIndentation);
   switch (syntaxTree.type) {
     // This case will never occur
     case TokenType.NonGremlinCode:
       return syntaxTree.code;
     case TokenType.Traversal:
-      return spaces(localIndentation) + 
syntaxTree.steps.map(recreateQueryOnelinerFromSyntaxTree()).join('.');
+      return (
+        indentation +
+        syntaxTree.steps.map(recreateQueryOnelinerFromSyntaxTree({ 
globalIndentation: 0 })).join('.')
+      );
     case TokenType.Method:
       return (
-        spaces(localIndentation) +
-        recreateQueryOnelinerFromSyntaxTree()(syntaxTree.method) +
+        indentation +
+        recreateQueryOnelinerFromSyntaxTree({ globalIndentation: 0 
})(syntaxTree.method) +
         '(' +
-        
syntaxTree.arguments.map(recreateQueryOnelinerFromSyntaxTree()).join(', ') +
+        syntaxTree.arguments.map(recreateQueryOnelinerFromSyntaxTree({ 
globalIndentation: 0 })).join(', ') +
         ')'
       );
     case TokenType.Closure:
       return (
-        spaces(localIndentation) +
-        recreateQueryOnelinerFromSyntaxTree()(syntaxTree.method) +
+        indentation +
+        recreateQueryOnelinerFromSyntaxTree({ globalIndentation: 0 
})(syntaxTree.method) +
         '{' +
         last(
           syntaxTree.closureCodeBlock.map(
@@ -66,9 +71,9 @@ const recreateQueryOnelinerFromSyntaxTree = 
(localIndentation: number = 0) => (
         '}'
       );
     case TokenType.String:
-      return spaces(localIndentation) + syntaxTree.string;
+      return indentation + syntaxTree.string;
     case TokenType.Word:
-      return spaces(localIndentation) + syntaxTree.word;
+      return indentation + syntaxTree.word;
   }
 };
 
diff --git 
a/gremlint/src/formatQuery/recreateQueryStringFromFormattedSyntaxTrees.ts 
b/gremlint/src/formatQuery/recreateQueryStringFromFormattedSyntaxTrees.ts
index e7d9f6af61..2fa64b0dbf 100644
--- a/gremlint/src/formatQuery/recreateQueryStringFromFormattedSyntaxTrees.ts
+++ b/gremlint/src/formatQuery/recreateQueryStringFromFormattedSyntaxTrees.ts
@@ -26,20 +26,26 @@ const recreateQueryStringFromFormattedSyntaxTree = 
(syntaxTree: FormattedSyntaxT
   }
   if (syntaxTree.type === TokenType.Traversal) {
     return syntaxTree.stepGroups
-      .map((stepGroup) => 
stepGroup.steps.map(recreateQueryStringFromFormattedSyntaxTree).join('.'))
+      .map((stepGroup, index) => {
+        const firstStep = stepGroup.steps[0];
+        const indentationAmount =
+          index === 0 || firstStep.type === TokenType.NonGremlinCode ? 0 : 
firstStep.localIndentation;
+        const indentation = spaces(indentationAmount);
+        return indentation + 
stepGroup.steps.map(recreateQueryStringFromFormattedSyntaxTree).join('.');
+      })
       .join('\n');
   }
   if (syntaxTree.type === TokenType.Method) {
+    const methodOpeningParenthesis =
+      (syntaxTree.shouldStartWithDot ? '.' : '') + 
recreateQueryStringFromFormattedSyntaxTree(syntaxTree.method) + '(';
+    const indentation = spaces(syntaxTree.horizontalPosition + 
methodOpeningParenthesis.length);
     return (
-      (syntaxTree.shouldStartWithDot ? '.' : '') +
-      [
-        recreateQueryStringFromFormattedSyntaxTree(syntaxTree.method) + '(',
-        syntaxTree.argumentGroups
-          .map((args) => 
args.map(recreateQueryStringFromFormattedSyntaxTree).join(', '))
-          .join(',\n') +
-          ')' +
-          (syntaxTree.shouldEndWithDot ? '.' : ''),
-      ].join(syntaxTree.argumentsShouldStartOnNewLine ? '\n' : '')
+      methodOpeningParenthesis +
+      syntaxTree.argumentGroups
+        .map((args) => 
args.map(recreateQueryStringFromFormattedSyntaxTree).join(', '))
+        .join(',\n' + indentation) +
+      ')' +
+      (syntaxTree.shouldEndWithDot ? '.' : '')
     );
   }
   if (syntaxTree.type === TokenType.Closure) {
@@ -55,15 +61,10 @@ const recreateQueryStringFromFormattedSyntaxTree = 
(syntaxTree: FormattedSyntaxT
     );
   }
   if (syntaxTree.type === TokenType.String) {
-    return spaces(syntaxTree.localIndentation) + syntaxTree.string;
+    return syntaxTree.string;
   }
   if (syntaxTree.type === TokenType.Word) {
-    return (
-      spaces(syntaxTree.localIndentation) +
-      (syntaxTree.shouldStartWithDot ? '.' : '') +
-      syntaxTree.word +
-      (syntaxTree.shouldEndWithDot ? '.' : '')
-    );
+    return (syntaxTree.shouldStartWithDot ? '.' : '') + syntaxTree.word + 
(syntaxTree.shouldEndWithDot ? '.' : '');
   }
   // The following line is just here to convince TypeScript that the return 
type
   // is string and not string | undefined.
diff --git a/gremlint/src/formatQuery/types.ts 
b/gremlint/src/formatQuery/types.ts
index f85e91a166..45999c6261 100644
--- a/gremlint/src/formatQuery/types.ts
+++ b/gremlint/src/formatQuery/types.ts
@@ -117,6 +117,7 @@ export type FormattedMethodSyntaxTree = {
   argumentGroups: FormattedSyntaxTree[][];
   argumentsShouldStartOnNewLine: boolean;
   localIndentation: number;
+  horizontalPosition: number;
   width: number;
   shouldStartWithDot: boolean;
   shouldEndWithDot: boolean;

Reply via email to