fix  #1016

Format multi lines string 

```
@doc(
  """
          multi line
          do
          ${"abc"}
          """
)
@doc("""
def
""")
model Foo {}

alias T = """
          abc
          def ${"abc"}
          ghi
          """;

```

formats to

```
@doc("""
  multi line
  do
  ${"abc"}
  """)
@doc("""
  def
  """)
model Foo {}

alias T = """
  abc
  def ${"abc"}
  ghi
  """;
```
This commit is contained in:
Timothee Guerin 2024-06-11 13:06:12 -07:00 коммит произвёл GitHub
Родитель 7912e8c9c7
Коммит dd61517865
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 275 добавлений и 121 удалений

Просмотреть файл

@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: feature
packages:
- "@typespec/compiler"
---
Formatter: Indent or dedent multiline strings to the current indentation

Просмотреть файл

@ -244,13 +244,13 @@ and can contain markdown formatting.
```typespec ```typespec
@doc(""" @doc("""
Get status info for the service. Get status info for the service.
The status includes the current version of the service. The status includes the current version of the service.
The status value may be one of: The status value may be one of:
- `ok`: the service is operating normally - `ok`: the service is operating normally
- `degraded`: the service is operating in a degraded state - `degraded`: the service is operating in a degraded state
- `down`: the service is not operating - `down`: the service is not operating
""") """)
@tag("Status") @tag("Status")
@route("/status") @route("/status")
@get @get

Просмотреть файл

@ -21,10 +21,10 @@ Multi-line string literals are denoted using three double quotes `"""`.
```typespec ```typespec
alias Str = """ alias Str = """
This is a multi line string This is a multi line string
- opt 1 - opt 1
- opt 2 - opt 2
"""; """;
``` ```
- The opening `"""` must be followed by a new line. - The opening `"""` must be followed by a new line.

Просмотреть файл

@ -1,6 +1,11 @@
import type { AstPath, Doc, Printer } from "prettier"; import type { AstPath, Doc, Printer } from "prettier";
import { builders } from "prettier/doc"; import { builders } from "prettier/doc";
import { isIdentifierContinue, isIdentifierStart, utf16CodeUnits } from "../../core/charcode.js"; import {
CharCode,
isIdentifierContinue,
isIdentifierStart,
utf16CodeUnits,
} from "../../core/charcode.js";
import { compilerAssert } from "../../core/diagnostics.js"; import { compilerAssert } from "../../core/diagnostics.js";
import { Keywords } from "../../core/scanner.js"; import { Keywords } from "../../core/scanner.js";
import { import {
@ -86,7 +91,19 @@ import { commentHandler } from "./comment-handler.js";
import { needsParens } from "./needs-parens.js"; import { needsParens } from "./needs-parens.js";
import { DecorableNode, PrettierChildPrint, TypeSpecPrettierOptions } from "./types.js"; import { DecorableNode, PrettierChildPrint, TypeSpecPrettierOptions } from "./types.js";
import { util } from "./util.js"; import { util } from "./util.js";
const { align, breakParent, group, hardline, ifBreak, indent, join, line, softline } = builders; const {
align,
breakParent,
group,
hardline,
ifBreak,
indent,
join,
line,
softline,
literalline,
markAsRoot,
} = builders;
const { isNextLineEmpty } = util as any; const { isNextLineEmpty } = util as any;
@ -689,7 +706,8 @@ function printCallOrDecoratorArgs(
const shouldHug = const shouldHug =
node.arguments.length === 1 && node.arguments.length === 1 &&
(node.arguments[0].kind === SyntaxKind.ModelExpression || (node.arguments[0].kind === SyntaxKind.ModelExpression ||
node.arguments[0].kind === SyntaxKind.StringLiteral); node.arguments[0].kind === SyntaxKind.StringLiteral ||
node.arguments[0].kind === SyntaxKind.StringTemplateExpression);
if (shouldHug) { if (shouldHug) {
return [ return [
@ -1637,7 +1655,28 @@ function printStringLiteral(
options: TypeSpecPrettierOptions options: TypeSpecPrettierOptions
): Doc { ): Doc {
const node = path.node; const node = path.node;
return getRawText(node, options); const multiline = isMultiline(node, options);
const raw = getRawText(node, options);
if (multiline) {
const lines = splitLines(raw.slice(3));
const whitespaceIndent = lines[lines.length - 1].length - 3;
const newLines = trimMultilineString(lines, whitespaceIndent);
return [`"""`, indent(markAsRoot(newLines))];
} else {
return raw;
}
}
function isMultiline(
node: StringLiteralNode | StringTemplateExpressionNode,
options: TypeSpecPrettierOptions
) {
return (
options.originalText[node.pos] &&
options.originalText[node.pos + 1] === `"` &&
options.originalText[node.pos + 2] === `"`
);
} }
function printNumberLiteral( function printNumberLiteral(
@ -1871,14 +1910,79 @@ export function printStringTemplateExpression(
print: PrettierChildPrint print: PrettierChildPrint
) { ) {
const node = path.node; const node = path.node;
const content = [ const multiline = isMultiline(node, options);
getRawText(node.head, options), const rawHead = getRawText(node.head, options);
path.map((span: AstPath<StringTemplateSpanNode>) => { if (multiline) {
const expression = span.call(print, "expression"); const lastSpan = node.spans[node.spans.length - 1];
return [expression, getRawText(span.node.literal, options)]; const lastLines = splitLines(getRawText(lastSpan.literal, options));
}, "spans"), const whitespaceIndent = lastLines[lastLines.length - 1].length - 3;
]; const content = [
return content; trimMultilineString(splitLines(rawHead.slice(3)), whitespaceIndent),
path.map((span: AstPath<StringTemplateSpanNode>) => {
const expression = span.call(print, "expression");
const spanRawText = getRawText(span.node.literal, options);
const spanLines = splitLines(spanRawText);
return [
expression,
spanLines[0],
literalline,
trimMultilineString(spanLines.slice(1), whitespaceIndent),
];
}, "spans"),
];
return [`"""`, indent(markAsRoot([content]))];
} else {
const content = [
rawHead,
path.map((span: AstPath<StringTemplateSpanNode>) => {
const expression = span.call(print, "expression");
return [expression, getRawText(span.node.literal, options)];
}, "spans"),
];
return content;
}
}
function splitLines(text: string): string[] {
const lines = [];
let start = 0;
let pos = 0;
while (pos < text.length) {
const ch = text.charCodeAt(pos);
switch (ch) {
case CharCode.CarriageReturn:
if (text.charCodeAt(pos + 1) === CharCode.LineFeed) {
lines.push(text.slice(start, pos));
start = pos;
pos++;
} else {
lines.push(text.slice(start, pos));
start = pos;
}
break;
case CharCode.LineFeed:
lines.push(text.slice(start, pos));
start = pos;
break;
}
pos++;
}
lines.push(text.slice(start));
return lines;
}
function trimMultilineString(lines: string[], whitespaceIndent: number): Doc[] {
const newLines = [];
for (let i = 0; i < lines.length; i++) {
newLines.push(lines[i].slice(whitespaceIndent));
if (i < lines.length - 1) {
newLines.push(literalline);
}
}
return newLines;
} }
function printItemList<T extends Node>( function printItemList<T extends Node>(

Просмотреть файл

@ -1807,7 +1807,7 @@ namespace Foo {
}); });
}); });
describe("string literals", () => { describe("single line string literals", () => {
it("format single line string literal", async () => { it("format single line string literal", async () => {
await assertFormat({ await assertFormat({
code: ` code: `
@ -1835,14 +1835,37 @@ model Foo {}
`, `,
}); });
}); });
});
it("format multi line string literal", async () => { describe("multi line string literals", () => {
it("keeps trailing whitespaces", async () => {
await assertFormat({ await assertFormat({
code: ` code: `
@doc( """ @doc( """
3 whitespaces
and blank line above
"""
)
model Foo {}
`,
expected: `
@doc("""
3 whitespaces
this is a doc. and blank line above
that """)
model Foo {}
`,
});
});
it("keeps indent relative to closing quotes", async () => {
await assertFormat({
code: `
@doc( """
this is a doc.
that
span span
multiple lines. multiple lines.
""" """
@ -1851,12 +1874,31 @@ model Foo {}
`, `,
expected: ` expected: `
@doc(""" @doc("""
this is a doc.
this is a doc. that
that span
span multiple lines.
multiple lines. """)
""") model Foo {}
`,
});
});
it("keeps escaped charaters", async () => {
await assertFormat({
code: `
@doc( """
with \\n
and \\t
"""
)
model Foo {}
`,
expected: `
@doc("""
with \\n
and \\t
""")
model Foo {} model Foo {}
`, `,
}); });
@ -2847,11 +2889,11 @@ alias T = "foo \${{
await assertFormat({ await assertFormat({
code: ` code: `
alias T = """ alias T = """
This \${ "one" } goes over This \${ "one" } goes over
multiple multiple
\${ "two" } \${ "two" }
lines lines
""";`, """;`,
expected: ` expected: `
alias T = """ alias T = """
This \${"one"} goes over This \${"one"} goes over

Просмотреть файл

@ -80,9 +80,9 @@ model Kiosk {
} }
@doc(""" @doc("""
Describes a digital sign. Describes a digital sign.
Signs can include text, images, or both. Signs can include text, images, or both.
""") """)
model Sign { model Sign {
@doc("unique id") @doc("unique id")
id?: int32; // Output only. id?: int32; // Output only.

Просмотреть файл

@ -15,12 +15,12 @@ model Timestamp {
} }
@doc(""" @doc("""
An object that represents a latitude/longitude pair. This is expressed as a An object that represents a latitude/longitude pair. This is expressed as a
pair of doubles to represent degrees latitude and degrees longitude. Unless pair of doubles to represent degrees latitude and degrees longitude. Unless
specified otherwise, this must conform to the specified otherwise, this must conform to the
<a href="http://www.unoosa.org/pdf/icg/2012/template/WGS_84.pdf">WGS84 <a href="http://www.unoosa.org/pdf/icg/2012/template/WGS_84.pdf">WGS84
standard</a>. Values must be within normalized ranges. standard</a>. Values must be within normalized ranges.
""") """)
model LatLng { model LatLng {
// The latitude in degrees. It must be in the range [-90.0, +90.0]. // The latitude in degrees. It must be in the range [-90.0, +90.0].
latitude: float64; latitude: float64;

Просмотреть файл

@ -14,16 +14,16 @@ using TypeSpec.Http;
namespace GrpcLibrarySample; namespace GrpcLibrarySample;
@doc(""" @doc("""
This API represents a simple digital library. It lets you manage Shelf This API represents a simple digital library. It lets you manage Shelf
resources and Book resources in the library. It defines the following resources and Book resources in the library. It defines the following
resource model: resource model:
- The API has a collection of [Shelf][google.example.library.v1.Shelf] - The API has a collection of [Shelf][google.example.library.v1.Shelf]
resources, named `shelves/*` resources, named `shelves/*`
- Each Shelf has a collection of [Book][google.example.library.v1.Book] - Each Shelf has a collection of [Book][google.example.library.v1.Book]
resources, named `shelves/*/books/*` resources, named `shelves/*/books/*`
""") """)
@route("/v1") @route("/v1")
@tag("LibraryService") @tag("LibraryService")
namespace LibraryService { namespace LibraryService {
@ -37,9 +37,9 @@ namespace LibraryService {
op getShelf(...GetShelfRequest): Shelf | RpcStatus; op getShelf(...GetShelfRequest): Shelf | RpcStatus;
@doc(""" @doc("""
Lists shelves. The order is unspecified but deterministic. Newly created Lists shelves. The order is unspecified but deterministic. Newly created
shelves will not necessarily be added to the end of this list. shelves will not necessarily be added to the end of this list.
""") """)
@route("shelves") @route("shelves")
op listShelves(...ListRequestBase): ListShelvesResponse | RpcStatus; op listShelves(...ListRequestBase): ListShelvesResponse | RpcStatus;
@ -49,13 +49,13 @@ shelves will not necessarily be added to the end of this list.
op deleteShelf(...DeleteShelfRequest): void | RpcStatus; op deleteShelf(...DeleteShelfRequest): void | RpcStatus;
@doc(""" @doc("""
Merges two shelves by adding all books from the shelf named Merges two shelves by adding all books from the shelf named
`other_shelf_name` to shelf `name`, and deletes `other_shelf_name` to shelf `name`, and deletes
`other_shelf_name`. Returns the updated shelf. `other_shelf_name`. Returns the updated shelf.
The book ids of the moved books may not be the same as the original books. The book ids of the moved books may not be the same as the original books.
Returns NOT_FOUND if either shelf does not exist. Returns NOT_FOUND if either shelf does not exist.
This call is a no-op if the specified shelves are the same. This call is a no-op if the specified shelves are the same.
""") """)
@route("shelves/{name}:merge") @route("shelves/{name}:merge")
@post @post
op mergeShelves(...MergeShelvesRequest): Shelf | RpcStatus; op mergeShelves(...MergeShelvesRequest): Shelf | RpcStatus;
@ -70,10 +70,10 @@ This call is a no-op if the specified shelves are the same.
op getBook(...GetBookRequest): Book | RpcStatus; op getBook(...GetBookRequest): Book | RpcStatus;
@doc(""" @doc("""
Lists books in a shelf. The order is unspecified but deterministic. Newly Lists books in a shelf. The order is unspecified but deterministic. Newly
created books will not necessarily be added to the end of this list. created books will not necessarily be added to the end of this list.
Returns NOT_FOUND if the shelf does not exist. Returns NOT_FOUND if the shelf does not exist.
""") """)
@route("shelves/{name}/books") @route("shelves/{name}/books")
op listBooks(...ListBooksRequest): ListBooksResponse | RpcStatus; op listBooks(...ListBooksRequest): ListBooksResponse | RpcStatus;
@ -84,19 +84,19 @@ Returns NOT_FOUND if the shelf does not exist.
} }
@doc(""" @doc("""
The name of a book. The name of a book.
Book names have the form `shelves/{shelf_id}/books/{book_id}` Book names have the form `shelves/{shelf_id}/books/{book_id}`
""") """)
@pattern("shelves/\\w+/books/\\w+") @pattern("shelves/\\w+/books/\\w+")
scalar book_name extends string; scalar book_name extends string;
@doc("A single book in the library.") @doc("A single book in the library.")
model Book { model Book {
@doc(""" @doc("""
The resource name of the book. The resource name of the book.
Book names have the form `shelves/{shelf_id}/books/{book_id}`. Book names have the form `shelves/{shelf_id}/books/{book_id}`.
The name is ignored when creating a book. The name is ignored when creating a book.
""") """)
name: book_name; name: book_name;
@doc("The name of the book author.") @doc("The name of the book author.")
@ -110,19 +110,19 @@ The name is ignored when creating a book.
} }
@doc(""" @doc("""
The name of a shelf. The name of a shelf.
Shelf names have the form `shelves/{shelf_id}`. Shelf names have the form `shelves/{shelf_id}`.
""") """)
@pattern("shelves/\\w+") @pattern("shelves/\\w+")
scalar shelf_name extends string; scalar shelf_name extends string;
@doc("A Shelf contains a collection of books with a theme.") @doc("A Shelf contains a collection of books with a theme.")
model Shelf { model Shelf {
@doc(""" @doc("""
The resource name of the shelf. The resource name of the shelf.
Shelf names have the form `shelves/{shelf_id}`. Shelf names have the form `shelves/{shelf_id}`.
The name is ignored when creating a shelf. The name is ignored when creating a shelf.
""") """)
name: shelf_name; name: shelf_name;
@doc("The theme of the shelf") @doc("The theme of the shelf")
@ -159,9 +159,9 @@ model DeleteShelfRequest {
} }
@doc(""" @doc("""
Describes the shelf being removed (other_shelf_name) and updated Describes the shelf being removed (other_shelf_name) and updated
(name) in this merge (name) in this merge
""") """)
model MergeShelvesRequest { model MergeShelvesRequest {
@doc("The name of the shelf we're adding books to.") @doc("The name of the shelf we're adding books to.")
@path @path
@ -229,9 +229,9 @@ model DeleteBookRequest {
} }
@doc(""" @doc("""
Describes what book to move (name) and what shelf we're moving it Describes what book to move (name) and what shelf we're moving it
to (other_shelf_name). to (other_shelf_name).
""") """)
model MoveBookRequest { model MoveBookRequest {
@doc("The name of the book to move.") @doc("The name of the book to move.")
name: book_name; name: book_name;
@ -246,30 +246,30 @@ model MoveBookRequest {
model ListRequestBase { model ListRequestBase {
@doc(""" @doc("""
Requested page size. Server may return fewer shelves than requested. Requested page size. Server may return fewer shelves than requested.
If unspecified, server will pick an appropriate default. If unspecified, server will pick an appropriate default.
""") """)
@query @query
page_size?: int32; page_size?: int32;
@doc(""" @doc("""
A token identifying a page of results the server should return. A token identifying a page of results the server should return.
Typically, this is the value of Typically, this is the value of
[ListShelvesResponse.next_page_token][google.example.library.v1.ListShelvesResponse.next_page_token] [ListShelvesResponse.next_page_token][google.example.library.v1.ListShelvesResponse.next_page_token]
returned from the previous call to `ListShelves` method. returned from the previous call to `ListShelves` method.
""") """)
@query @query
page_token?: string; page_token?: string;
} }
model ListResponseBase { model ListResponseBase {
@doc(""" @doc("""
A token to retrieve next page of results. A token to retrieve next page of results.
Pass this value in the Pass this value in the
[ListShelvesRequest.page_token][google.example.library.v1.ListShelvesRequest.page_token] [ListShelvesRequest.page_token][google.example.library.v1.ListShelvesRequest.page_token]
field in the subsequent call to `ListShelves` method to retrieve the next field in the subsequent call to `ListShelves` method to retrieve the next
page of results. page of results.
""") """)
next_page_token?: string; next_page_token?: string;
} }

Просмотреть файл

@ -3,11 +3,11 @@ alias myConst = "foobar";
model Person { model Person {
simple: "Simple ${123} end"; simple: "Simple ${123} end";
multiline: """ multiline: """
Multi Multi
${123} ${123}
${true} ${true}
line line
"""; """;
ref: "Ref this alias ${myConst} end"; ref: "Ref this alias ${myConst} end";
template: Template<"custom">; template: Template<"custom">;
} }

Просмотреть файл

@ -244,13 +244,13 @@ and can contain markdown formatting.
```typespec ```typespec
@doc(""" @doc("""
Get status info for the service. Get status info for the service.
The status includes the current version of the service. The status includes the current version of the service.
The status value may be one of: The status value may be one of:
- `ok`: the service is operating normally - `ok`: the service is operating normally
- `degraded`: the service is operating in a degraded state - `degraded`: the service is operating in a degraded state
- `down`: the service is not operating - `down`: the service is not operating
""") """)
@tag("Status") @tag("Status")
@route("/status") @route("/status")
@get @get

Просмотреть файл

@ -21,10 +21,10 @@ Multi-line string literals are denoted using three double quotes `"""`.
```typespec ```typespec
alias Str = """ alias Str = """
This is a multi line string This is a multi line string
- opt 1 - opt 1
- opt 2 - opt 2
"""; """;
``` ```
- The opening `"""` must be followed by a new line. - The opening `"""` must be followed by a new line.