OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include <sstream> | 5 #include <sstream> |
6 | 6 |
7 #include "base/command_line.h" | 7 #include "base/command_line.h" |
8 #include "base/files/file_util.h" | 8 #include "base/files/file_util.h" |
9 #include "base/strings/string_split.h" | 9 #include "base/strings/string_split.h" |
10 #include "tools/gn/commands.h" | 10 #include "tools/gn/commands.h" |
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
61 const int kPenaltyExcess = 10000; | 61 const int kPenaltyExcess = 10000; |
62 const int kPenaltyBrokenLineOnOneLiner = 5000; | 62 const int kPenaltyBrokenLineOnOneLiner = 5000; |
63 | 63 |
64 enum Precedence { | 64 enum Precedence { |
65 kPrecedenceLowest, | 65 kPrecedenceLowest, |
66 kPrecedenceAssign, | 66 kPrecedenceAssign, |
67 kPrecedenceOr, | 67 kPrecedenceOr, |
68 kPrecedenceAnd, | 68 kPrecedenceAnd, |
69 kPrecedenceCompare, | 69 kPrecedenceCompare, |
70 kPrecedenceAdd, | 70 kPrecedenceAdd, |
| 71 kPrecedenceUnary, |
71 kPrecedenceSuffix, | 72 kPrecedenceSuffix, |
72 kPrecedenceUnary, | |
73 }; | 73 }; |
74 | 74 |
75 int CountLines(const std::string& str) { | 75 int CountLines(const std::string& str) { |
76 std::vector<std::string> lines; | 76 std::vector<std::string> lines; |
77 base::SplitStringDontTrim(str, '\n', &lines); | 77 base::SplitStringDontTrim(str, '\n', &lines); |
78 return static_cast<int>(lines.size()); | 78 return static_cast<int>(lines.size()); |
79 } | 79 } |
80 | 80 |
81 class Printer { | 81 class Printer { |
82 public: | 82 public: |
(...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
171 std::string output_; // Output buffer. | 171 std::string output_; // Output buffer. |
172 std::vector<Token> comments_; // Pending end-of-line comments. | 172 std::vector<Token> comments_; // Pending end-of-line comments. |
173 int margin() const { return stack_.back().margin; } | 173 int margin() const { return stack_.back().margin; } |
174 | 174 |
175 int penalty_depth_; | 175 int penalty_depth_; |
176 int GetPenaltyForLineBreak() const { | 176 int GetPenaltyForLineBreak() const { |
177 return penalty_depth_ * kPenaltyLineBreak; | 177 return penalty_depth_ * kPenaltyLineBreak; |
178 } | 178 } |
179 | 179 |
180 struct IndentState { | 180 struct IndentState { |
181 IndentState() : margin(0), continuation_requires_indent(false) {} | 181 IndentState() |
182 IndentState(int margin, bool continuation_requires_indent) | 182 : margin(0), |
| 183 continuation_requires_indent(false), |
| 184 parent_is_boolean_or(false) {} |
| 185 IndentState(int margin, |
| 186 bool continuation_requires_indent, |
| 187 bool parent_is_boolean_or) |
183 : margin(margin), | 188 : margin(margin), |
184 continuation_requires_indent(continuation_requires_indent) {} | 189 continuation_requires_indent(continuation_requires_indent), |
| 190 parent_is_boolean_or(parent_is_boolean_or) {} |
185 | 191 |
186 // The left margin (number of spaces). | 192 // The left margin (number of spaces). |
187 int margin; | 193 int margin; |
188 | 194 |
189 bool continuation_requires_indent; | 195 bool continuation_requires_indent; |
| 196 |
| 197 bool parent_is_boolean_or; |
190 }; | 198 }; |
191 // Stack used to track | 199 // Stack used to track |
192 std::vector<IndentState> stack_; | 200 std::vector<IndentState> stack_; |
193 | 201 |
194 // Gives the precedence for operators in a BinaryOpNode. | 202 // Gives the precedence for operators in a BinaryOpNode. |
195 std::map<base::StringPiece, Precedence> precedence_; | 203 std::map<base::StringPiece, Precedence> precedence_; |
196 | 204 |
197 DISALLOW_COPY_AND_ASSIGN(Printer); | 205 DISALLOW_COPY_AND_ASSIGN(Printer); |
198 }; | 206 }; |
199 | 207 |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
232 TrimWhitespaceASCII(token.value().as_string(), base::TRIM_ALL, &trimmed); | 240 TrimWhitespaceASCII(token.value().as_string(), base::TRIM_ALL, &trimmed); |
233 Print(trimmed); | 241 Print(trimmed); |
234 } | 242 } |
235 | 243 |
236 void Printer::Newline() { | 244 void Printer::Newline() { |
237 if (!comments_.empty()) { | 245 if (!comments_.empty()) { |
238 Print(" "); | 246 Print(" "); |
239 // Save the margin, and temporarily set it to where the first comment | 247 // Save the margin, and temporarily set it to where the first comment |
240 // starts so that multiple suffix comments are vertically aligned. This | 248 // starts so that multiple suffix comments are vertically aligned. This |
241 // will need to be fancier once we enforce 80 col. | 249 // will need to be fancier once we enforce 80 col. |
242 stack_.push_back(IndentState(CurrentColumn(), false)); | 250 stack_.push_back(IndentState(CurrentColumn(), false, false)); |
243 int i = 0; | 251 int i = 0; |
244 for (const auto& c : comments_) { | 252 for (const auto& c : comments_) { |
245 if (i > 0) { | 253 if (i > 0) { |
246 Trim(); | 254 Trim(); |
247 Print("\n"); | 255 Print("\n"); |
248 PrintMargin(); | 256 PrintMargin(); |
249 } | 257 } |
250 TrimAndPrintToken(c); | 258 TrimAndPrintToken(c); |
251 ++i; | 259 ++i; |
252 } | 260 } |
(...skipping 153 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
406 Print("."); | 414 Print("."); |
407 Expr(accessor->member(), kPrecedenceLowest, std::string()); | 415 Expr(accessor->member(), kPrecedenceLowest, std::string()); |
408 } else { | 416 } else { |
409 CHECK(accessor->index()); | 417 CHECK(accessor->index()); |
410 Print("["); | 418 Print("["); |
411 Expr(accessor->index(), kPrecedenceLowest, "]"); | 419 Expr(accessor->index(), kPrecedenceLowest, "]"); |
412 } | 420 } |
413 } else if (const BinaryOpNode* binop = root->AsBinaryOp()) { | 421 } else if (const BinaryOpNode* binop = root->AsBinaryOp()) { |
414 CHECK(precedence_.find(binop->op().value()) != precedence_.end()); | 422 CHECK(precedence_.find(binop->op().value()) != precedence_.end()); |
415 AnnotatePreferedMultilineAssignment(binop); | 423 AnnotatePreferedMultilineAssignment(binop); |
| 424 |
416 Precedence prec = precedence_[binop->op().value()]; | 425 Precedence prec = precedence_[binop->op().value()]; |
417 AddParen(prec, outer_prec, &parenthesized); | 426 |
| 427 // Since binary operators format left-to-right, it is ok for the left side |
| 428 // use the same operator without parentheses, so the left uses prec. For the |
| 429 // same reason, the right side cannot reuse the same operator, or else "x + |
| 430 // (y + z)" would format as "x + y + z" which means "(x + y) + z". So, treat |
| 431 // the right expression as appearing one precedence level higher. |
| 432 // However, because the source parens are not in the parse tree, as a |
| 433 // special case for && and || we insert strictly-redundant-but-helpful-for- |
| 434 // human-readers parentheses. |
| 435 int prec_left = prec; |
| 436 int prec_right = prec + 1; |
| 437 if (binop->op().value() == "&&" && stack_.back().parent_is_boolean_or) { |
| 438 Print("("); |
| 439 parenthesized = true; |
| 440 } else { |
| 441 AddParen(prec_left, outer_prec, &parenthesized); |
| 442 } |
| 443 |
418 int start_line = CurrentLine(); | 444 int start_line = CurrentLine(); |
419 int start_column = CurrentColumn(); | 445 int start_column = CurrentColumn(); |
420 bool is_assignment = binop->op().value() == "=" || | 446 bool is_assignment = binop->op().value() == "=" || |
421 binop->op().value() == "+=" || | 447 binop->op().value() == "+=" || |
422 binop->op().value() == "-="; | 448 binop->op().value() == "-="; |
423 // A sort of funny special case for the long lists that are common in .gn | 449 // A sort of funny special case for the long lists that are common in .gn |
424 // files, don't indent them + 4, even though they're just continuations when | 450 // files, don't indent them + 4, even though they're just continuations when |
425 // they're simple lists like "x = [ a, b, c, ... ]" | 451 // they're simple lists like "x = [ a, b, c, ... ]" |
426 const ListNode* right_as_list = binop->right()->AsList(); | 452 const ListNode* right_as_list = binop->right()->AsList(); |
427 int indent_column = | 453 int indent_column = |
428 (is_assignment && | 454 (is_assignment && |
429 (!right_as_list || (!right_as_list->prefer_multiline() && | 455 (!right_as_list || (!right_as_list->prefer_multiline() && |
430 !ListWillBeMultiline(right_as_list->contents(), | 456 !ListWillBeMultiline(right_as_list->contents(), |
431 right_as_list->End())))) | 457 right_as_list->End())))) |
432 ? margin() + kIndentSize * 2 | 458 ? margin() + kIndentSize * 2 |
433 : start_column; | 459 : start_column; |
434 if (stack_.back().continuation_requires_indent) | 460 if (stack_.back().continuation_requires_indent) |
435 indent_column += kIndentSize * 2; | 461 indent_column += kIndentSize * 2; |
436 | 462 |
437 stack_.push_back(IndentState(indent_column, false)); | 463 stack_.push_back( |
| 464 IndentState(indent_column, false, binop->op().value() == "||")); |
438 Printer sub_left; | 465 Printer sub_left; |
439 InitializeSub(&sub_left); | 466 InitializeSub(&sub_left); |
440 sub_left.Expr(binop->left(), | 467 sub_left.Expr(binop->left(), |
441 prec, | 468 prec_left, |
442 std::string(" ") + binop->op().value().as_string()); | 469 std::string(" ") + binop->op().value().as_string()); |
443 bool left_is_multiline = CountLines(sub_left.String()) > 1; | 470 bool left_is_multiline = CountLines(sub_left.String()) > 1; |
444 // Avoid walking the whole left redundantly times (see timing of Format.046) | 471 // Avoid walking the whole left redundantly times (see timing of Format.046) |
445 // so pull the output and comments from subprinter. | 472 // so pull the output and comments from subprinter. |
446 Print(sub_left.String().substr(start_column)); | 473 Print(sub_left.String().substr(start_column)); |
447 std::copy(sub_left.comments_.begin(), | 474 std::copy(sub_left.comments_.begin(), |
448 sub_left.comments_.end(), | 475 sub_left.comments_.end(), |
449 std::back_inserter(comments_)); | 476 std::back_inserter(comments_)); |
450 | 477 |
451 // Single line. | 478 // Single line. |
452 Printer sub1; | 479 Printer sub1; |
453 InitializeSub(&sub1); | 480 InitializeSub(&sub1); |
454 sub1.Print(" "); | 481 sub1.Print(" "); |
455 int penalty_current_line = | 482 int penalty_current_line = |
456 sub1.Expr(binop->right(), prec + 1, std::string()); | 483 sub1.Expr(binop->right(), prec_right, std::string()); |
457 sub1.Print(suffix); | 484 sub1.Print(suffix); |
458 penalty_current_line += AssessPenalty(sub1.String()); | 485 penalty_current_line += AssessPenalty(sub1.String()); |
459 if (!is_assignment && left_is_multiline) { | 486 if (!is_assignment && left_is_multiline) { |
460 // In e.g. xxx + yyy, if xxx is already multiline, then we want a penalty | 487 // In e.g. xxx + yyy, if xxx is already multiline, then we want a penalty |
461 // for trying to continue as if this were one line. | 488 // for trying to continue as if this were one line. |
462 penalty_current_line += | 489 penalty_current_line += |
463 (CountLines(sub1.String()) - 1) * kPenaltyBrokenLineOnOneLiner; | 490 (CountLines(sub1.String()) - 1) * kPenaltyBrokenLineOnOneLiner; |
464 } | 491 } |
465 | 492 |
466 // Break after operator. | 493 // Break after operator. |
467 Printer sub2; | 494 Printer sub2; |
468 InitializeSub(&sub2); | 495 InitializeSub(&sub2); |
469 sub2.Newline(); | 496 sub2.Newline(); |
470 int penalty_next_line = sub2.Expr(binop->right(), prec + 1, std::string()); | 497 int penalty_next_line = |
| 498 sub2.Expr(binop->right(), prec_right, std::string()); |
471 sub2.Print(suffix); | 499 sub2.Print(suffix); |
472 penalty_next_line += AssessPenalty(sub2.String()); | 500 penalty_next_line += AssessPenalty(sub2.String()); |
473 | 501 |
474 if (penalty_current_line < penalty_next_line) { | 502 if (penalty_current_line < penalty_next_line) { |
475 Print(" "); | 503 Print(" "); |
476 Expr(binop->right(), prec + 1, std::string()); | 504 Expr(binop->right(), prec_right, std::string()); |
477 } else { | 505 } else { |
478 // Otherwise, put first argument and op, and indent next. | 506 // Otherwise, put first argument and op, and indent next. |
479 Newline(); | 507 Newline(); |
480 penalty += std::abs(CurrentColumn() - start_column) * | 508 penalty += std::abs(CurrentColumn() - start_column) * |
481 kPenaltyHorizontalSeparation; | 509 kPenaltyHorizontalSeparation; |
482 Expr(binop->right(), prec + 1, std::string()); | 510 Expr(binop->right(), prec_right, std::string()); |
483 } | 511 } |
484 stack_.pop_back(); | 512 stack_.pop_back(); |
485 penalty += (CurrentLine() - start_line) * GetPenaltyForLineBreak(); | 513 penalty += (CurrentLine() - start_line) * GetPenaltyForLineBreak(); |
486 } else if (const BlockNode* block = root->AsBlock()) { | 514 } else if (const BlockNode* block = root->AsBlock()) { |
487 Sequence( | 515 Sequence( |
488 kSequenceStyleBracedBlock, block->statements(), block->End(), false); | 516 kSequenceStyleBracedBlock, block->statements(), block->End(), false); |
489 } else if (const ConditionNode* condition = root->AsConditionNode()) { | 517 } else if (const ConditionNode* condition = root->AsConditionNode()) { |
490 Print("if ("); | 518 Print("if ("); |
491 // TODO(scottmg): The { needs to be included in the suffix here. | 519 // TODO(scottmg): The { needs to be included in the suffix here. |
492 Expr(condition->condition(), kPrecedenceLowest, ") "); | 520 Expr(condition->condition(), kPrecedenceLowest, ") "); |
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
563 force_multiline |= ListWillBeMultiline(list, end); | 591 force_multiline |= ListWillBeMultiline(list, end); |
564 | 592 |
565 if (list.size() == 0 && !force_multiline) { | 593 if (list.size() == 0 && !force_multiline) { |
566 // No elements, and not forcing newlines, print nothing. | 594 // No elements, and not forcing newlines, print nothing. |
567 } else if (list.size() == 1 && !force_multiline) { | 595 } else if (list.size() == 1 && !force_multiline) { |
568 Print(" "); | 596 Print(" "); |
569 Expr(list[0], kPrecedenceLowest, std::string()); | 597 Expr(list[0], kPrecedenceLowest, std::string()); |
570 CHECK(!list[0]->comments() || list[0]->comments()->after().empty()); | 598 CHECK(!list[0]->comments() || list[0]->comments()->after().empty()); |
571 Print(" "); | 599 Print(" "); |
572 } else { | 600 } else { |
573 stack_.push_back( | 601 stack_.push_back(IndentState(margin() + kIndentSize, |
574 IndentState(margin() + kIndentSize, style == kSequenceStyleList)); | 602 style == kSequenceStyleList, |
| 603 false)); |
575 size_t i = 0; | 604 size_t i = 0; |
576 for (const auto& x : list) { | 605 for (const auto& x : list) { |
577 Newline(); | 606 Newline(); |
578 // If: | 607 // If: |
579 // - we're going to output some comments, and; | 608 // - we're going to output some comments, and; |
580 // - we haven't just started this multiline list, and; | 609 // - we haven't just started this multiline list, and; |
581 // - there isn't already a blank line here; | 610 // - there isn't already a blank line here; |
582 // Then: insert one. | 611 // Then: insert one. |
583 if (i != 0 && x->comments() && !x->comments()->before().empty() && | 612 if (i != 0 && x->comments() && !x->comments()->before().empty() && |
584 !HaveBlankLine()) { | 613 !HaveBlankLine()) { |
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
653 // 2. starting on same line, broken at each comma but paren aligned; | 682 // 2. starting on same line, broken at each comma but paren aligned; |
654 // 3. broken to next line + 4, broken at each comma. | 683 // 3. broken to next line + 4, broken at each comma. |
655 std::string terminator = ")"; | 684 std::string terminator = ")"; |
656 if (have_block) | 685 if (have_block) |
657 terminator += " {"; | 686 terminator += " {"; |
658 terminator += suffix; | 687 terminator += suffix; |
659 | 688 |
660 // 1: Same line. | 689 // 1: Same line. |
661 Printer sub1; | 690 Printer sub1; |
662 InitializeSub(&sub1); | 691 InitializeSub(&sub1); |
663 sub1.stack_.push_back(IndentState(CurrentColumn(), true)); | 692 sub1.stack_.push_back(IndentState(CurrentColumn(), true, false)); |
664 int penalty_one_line = 0; | 693 int penalty_one_line = 0; |
665 for (size_t i = 0; i < list.size(); ++i) { | 694 for (size_t i = 0; i < list.size(); ++i) { |
666 penalty_one_line += sub1.Expr(list[i], kPrecedenceLowest, | 695 penalty_one_line += sub1.Expr(list[i], kPrecedenceLowest, |
667 i < list.size() - 1 ? ", " : std::string()); | 696 i < list.size() - 1 ? ", " : std::string()); |
668 } | 697 } |
669 sub1.Print(terminator); | 698 sub1.Print(terminator); |
670 penalty_one_line += AssessPenalty(sub1.String()); | 699 penalty_one_line += AssessPenalty(sub1.String()); |
671 // This extra penalty prevents a short second argument from being squeezed in | 700 // This extra penalty prevents a short second argument from being squeezed in |
672 // after a first argument that went multiline (and instead preferring a | 701 // after a first argument that went multiline (and instead preferring a |
673 // variant below). | 702 // variant below). |
674 penalty_one_line += | 703 penalty_one_line += |
675 (CountLines(sub1.String()) - 1) * kPenaltyBrokenLineOnOneLiner; | 704 (CountLines(sub1.String()) - 1) * kPenaltyBrokenLineOnOneLiner; |
676 | 705 |
677 // 2: Starting on same line, broken at commas. | 706 // 2: Starting on same line, broken at commas. |
678 Printer sub2; | 707 Printer sub2; |
679 InitializeSub(&sub2); | 708 InitializeSub(&sub2); |
680 sub2.stack_.push_back(IndentState(CurrentColumn(), true)); | 709 sub2.stack_.push_back(IndentState(CurrentColumn(), true, false)); |
681 int penalty_multiline_start_same_line = 0; | 710 int penalty_multiline_start_same_line = 0; |
682 for (size_t i = 0; i < list.size(); ++i) { | 711 for (size_t i = 0; i < list.size(); ++i) { |
683 penalty_multiline_start_same_line += sub2.Expr( | 712 penalty_multiline_start_same_line += sub2.Expr( |
684 list[i], kPrecedenceLowest, i < list.size() - 1 ? "," : std::string()); | 713 list[i], kPrecedenceLowest, i < list.size() - 1 ? "," : std::string()); |
685 if (i < list.size() - 1) { | 714 if (i < list.size() - 1) { |
686 sub2.Newline(); | 715 sub2.Newline(); |
687 } | 716 } |
688 } | 717 } |
689 sub2.Print(terminator); | 718 sub2.Print(terminator); |
690 penalty_multiline_start_same_line += AssessPenalty(sub2.String()); | 719 penalty_multiline_start_same_line += AssessPenalty(sub2.String()); |
691 | 720 |
692 // 3: Starting on next line, broken at commas. | 721 // 3: Starting on next line, broken at commas. |
693 Printer sub3; | 722 Printer sub3; |
694 InitializeSub(&sub3); | 723 InitializeSub(&sub3); |
695 sub3.stack_.push_back(IndentState(margin() + kIndentSize * 2, true)); | 724 sub3.stack_.push_back(IndentState(margin() + kIndentSize * 2, true, false)); |
696 sub3.Newline(); | 725 sub3.Newline(); |
697 int penalty_multiline_start_next_line = 0; | 726 int penalty_multiline_start_next_line = 0; |
698 for (size_t i = 0; i < list.size(); ++i) { | 727 for (size_t i = 0; i < list.size(); ++i) { |
699 if (i == 0) { | 728 if (i == 0) { |
700 penalty_multiline_start_next_line += | 729 penalty_multiline_start_next_line += |
701 std::abs(sub3.CurrentColumn() - start_column) * | 730 std::abs(sub3.CurrentColumn() - start_column) * |
702 kPenaltyHorizontalSeparation; | 731 kPenaltyHorizontalSeparation; |
703 } | 732 } |
704 penalty_multiline_start_next_line += sub3.Expr( | 733 penalty_multiline_start_next_line += sub3.Expr( |
705 list[i], kPrecedenceLowest, i < list.size() - 1 ? "," : std::string()); | 734 list[i], kPrecedenceLowest, i < list.size() - 1 ? "," : std::string()); |
(...skipping 15 matching lines...) Expand all Loading... |
721 force_multiline = true; | 750 force_multiline = true; |
722 } | 751 } |
723 } else { | 752 } else { |
724 force_multiline = true; | 753 force_multiline = true; |
725 } | 754 } |
726 | 755 |
727 if (list.size() == 0 && !force_multiline) { | 756 if (list.size() == 0 && !force_multiline) { |
728 // No elements, and not forcing newlines, print nothing. | 757 // No elements, and not forcing newlines, print nothing. |
729 } else { | 758 } else { |
730 if (penalty_multiline_start_next_line < penalty_multiline_start_same_line) { | 759 if (penalty_multiline_start_next_line < penalty_multiline_start_same_line) { |
731 stack_.push_back(IndentState(margin() + kIndentSize * 2, true)); | 760 stack_.push_back(IndentState(margin() + kIndentSize * 2, true, false)); |
732 Newline(); | 761 Newline(); |
733 } else { | 762 } else { |
734 stack_.push_back(IndentState(CurrentColumn(), true)); | 763 stack_.push_back(IndentState(CurrentColumn(), true, false)); |
735 } | 764 } |
736 | 765 |
737 for (size_t i = 0; i < list.size(); ++i) { | 766 for (size_t i = 0; i < list.size(); ++i) { |
738 const auto& x = list[i]; | 767 const auto& x = list[i]; |
739 if (i > 0) { | 768 if (i > 0) { |
740 if (fits_on_current_line && !force_multiline) | 769 if (fits_on_current_line && !force_multiline) |
741 Print(" "); | 770 Print(" "); |
742 else | 771 else |
743 Newline(); | 772 Newline(); |
744 } | 773 } |
(...skipping 190 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
935 printf("Wrote formatted to '%s'.\n", to_write.AsUTF8Unsafe().c_str()); | 964 printf("Wrote formatted to '%s'.\n", to_write.AsUTF8Unsafe().c_str()); |
936 } else { | 965 } else { |
937 printf("%s", output_string.c_str()); | 966 printf("%s", output_string.c_str()); |
938 } | 967 } |
939 } | 968 } |
940 | 969 |
941 return 0; | 970 return 0; |
942 } | 971 } |
943 | 972 |
944 } // namespace commands | 973 } // namespace commands |
OLD | NEW |