Merge changes from topic 'tags-api'

* changes:
  ListTags: Support filtering by substring and regex
  ProjectApi: Refactor to reduce duplicate code in tags and branches
  ListTags: Add support for pagination with --start and --limit
  ListBranches: Split filtering and pagination out to a utility class
  Make BranchInfo and TagInfo share a common base class
  Implement tags API
  ListTags: Create RevWalk in try-with-resource
  RefNames: Add support for refs/tags/ in shortName()
diff --git a/Documentation/doc.css.in b/Documentation/doc.css.in
index 6be89f6..429e81c 100644
--- a/Documentation/doc.css.in
+++ b/Documentation/doc.css.in
@@ -17,6 +17,10 @@
   border-bottom: 2px solid silver;
 }
 
+h1 {
+  margin-top: 1.5em;
+}
+
 p {
   margin: 0.5em 0 0.5em 0;
 }
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index d753d90..7497a64 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1339,11 +1339,15 @@
   )]}'
   {
     "theme": "ECLIPSE",
+    "key_map_type": "VIM",
     "tab_size": 4,
     "line_length": 80,
+    "cursor_blink_rate": 530,
     "hide_top_menu": true,
     "show_whitespace_errors": true,
-    "hide_line_numbers": true
+    "hide_line_numbers": true,
+    "match_brackets": true,
+    "auto_close_brackets": true
   }
 ----
 
@@ -1365,13 +1369,17 @@
 
   {
     "theme": "ECLIPSE",
+    "key_map_type": "VIM",
     "tab_size": 4,
     "line_length": 80,
+    "cursor_blink_rate": 530,
     "hide_top_menu": true,
     "show_tabs": true,
     "show_whitespace_errors": true,
     "syntax_highlighting": true,
-    "hide_line_numbers": true
+    "hide_line_numbers": true,
+    "match_brackets": true,
+    "auto_close_brackets": true
   }
 ----
 
@@ -1757,10 +1765,16 @@
 The CodeMirror theme. Currently only a subset of light and dark
 CodeMirror themes are supported. Light themes `DEFAULT`, `ECLIPSE`,
 `ELEGANT`, `NEAT`. Dark themes `MIDNIGHT`, `NIGHT`, `TWILIGHT`.
+|`key_map_type`                ||
+The CodeMirror key map. Currently only a subset of key maps are
+supported: `DEFAULT`, `EMACS`, `VIM`.
 |`tab_size`                    ||
 Number of spaces that should be used to display one tab.
 |`line_length`                 ||
 Number of characters that should be displayed per line.
+|`cursor_blink_rate`           ||
+Half-period in milliseconds used for cursor blinking.
+Setting it to 0 disables cursor blinking.
 |`hide_top_menu`               |not set if `false`|
 If true the top menu header and site header is hidden.
 |`show_tabs`                   |not set if `false`|
@@ -1771,6 +1785,10 @@
 Whether syntax highlighting should be enabled.
 |`hide_line_numbers`           |not set if `false`|
 Whether line numbers should be hidden.
+|`match_brackets`              |not set if `false`|
+Whether matching brackets should be highlighted.
+|`auto_close_brackets`         |not set if `false`|
+Whether brackets and quotes should be auto-closed during typing.
 |===========================================
 
 [[email-info]]
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index afdf36d..e0df4ca 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -172,6 +172,46 @@
   GET /groups/?n=25&S=50 HTTP/1.0
 ----
 
+[[suggest-group]]
+==== Suggest Group
+The `suggest` option indicates a user-entered string that
+should be auto-completed to group names.
+If this option is set and `n` is not set, then `n` defaults to 10.
+
+When using this option,
+the `project` or `p` option can be used to name the current project,
+to allow context-dependent suggestions.
+
+Not compatible with `visible-to-all`, `owned`, `user`, `match`, `q`,
+or `S`.
+(Attempts to use one of those options combined with `suggest` will
+error out.)
+
+.Request
+----
+  GET /groups/?suggest=ad&p=All-Projects HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "Administrators": {
+      "url": "#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b",
+      "options": {},
+      "description": "Gerrit Site Administrators",
+      "group_id": 1,
+      "owner": "Administrators",
+      "owner_id": "59b92f35489e62c80d1ab1bf0c2d17843038df8b",
+      "id": "59b92f35489e62c80d1ab1bf0c2d17843038df8b"
+    }
+  }
+----
+
 [[get-group]]
 === Get Group
 --
diff --git a/Documentation/user-inline-edit.txt b/Documentation/user-inline-edit.txt
index 2da0c25..fff2501 100644
--- a/Documentation/user-inline-edit.txt
+++ b/Documentation/user-inline-edit.txt
@@ -176,9 +176,6 @@
 ** "save-when-file-was-changed" or
 ** "close-when-no-changes"
 
-* Allow to activate different key maps, supported by CM: Emacs, Sublime, Vim. Load key
-maps dynamically. Currently default mode is used.
-
 * Implement conflict resolution during rebase of change edit using inline edit
 feature by creating new edit on top of current patch set with auto merge content
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 5b8b87f..c72edd7 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -402,6 +402,13 @@
   }
 
   @Test
+  public void testSuggestGroup() throws Exception {
+    Map<String, GroupInfo> groups = gApi.groups().list().withSuggest("adm").getAsMap();
+    assertThat(groups).containsKey("Administrators");
+    assertThat(groups).hasSize(1);
+  }
+
+  @Test
   public void testAllGroupInfoFieldsSetCorrectly() throws Exception {
     AccountGroup adminGroup = getFromCache("Administrators");
     Map<String, GroupInfo> groups =
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
index de6e3aa..8770c3c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.client.KeyMapType;
 import com.google.gerrit.extensions.client.Theme;
 
 import org.apache.http.HttpStatus;
@@ -36,22 +37,30 @@
 
     assertThat(out.lineLength).isEqualTo(100);
     assertThat(out.tabSize).isEqualTo(8);
+    assertThat(out.cursorBlinkRate).isEqualTo(0);
     assertThat(out.hideTopMenu).isNull();
     assertThat(out.showTabs).isTrue();
     assertThat(out.showWhitespaceErrors).isNull();
     assertThat(out.syntaxHighlighting).isTrue();
     assertThat(out.hideLineNumbers).isNull();
+    assertThat(out.matchBrackets).isTrue();
+    assertThat(out.autoCloseBrackets).isNull();
     assertThat(out.theme).isEqualTo(Theme.DEFAULT);
+    assertThat(out.keyMapType).isEqualTo(KeyMapType.DEFAULT);
 
     // change some default values
     out.lineLength = 80;
     out.tabSize = 4;
+    out.cursorBlinkRate = 500;
     out.hideTopMenu = true;
     out.showTabs = false;
     out.showWhitespaceErrors = true;
     out.syntaxHighlighting = false;
     out.hideLineNumbers = true;
+    out.matchBrackets = false;
+    out.autoCloseBrackets = true;
     out.theme = Theme.TWILIGHT;
+    out.keyMapType = KeyMapType.EMACS;
 
     r = adminSession.put(endPoint, out);
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
@@ -72,11 +81,15 @@
       EditPreferencesInfo in) {
     assertThat(out.lineLength).isEqualTo(in.lineLength);
     assertThat(out.tabSize).isEqualTo(in.tabSize);
+    assertThat(out.cursorBlinkRate).isEqualTo(in.cursorBlinkRate);
     assertThat(out.hideTopMenu).isEqualTo(in.hideTopMenu);
     assertThat(out.showTabs).isNull();
     assertThat(out.showWhitespaceErrors).isEqualTo(in.showWhitespaceErrors);
     assertThat(out.syntaxHighlighting).isNull();
     assertThat(out.hideLineNumbers).isEqualTo(in.hideLineNumbers);
+    assertThat(out.matchBrackets).isNull();
+    assertThat(out.autoCloseBrackets).isEqualTo(in.autoCloseBrackets);
     assertThat(out.theme).isEqualTo(in.theme);
+    assertThat(out.keyMapType).isEqualTo(in.keyMapType);
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
index ab09e5f..b909f31 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
@@ -63,6 +63,7 @@
     private int limit;
     private int start;
     private String substring;
+    private String suggest;
 
     public List<GroupInfo> get() throws RestApiException {
       Map<String, GroupInfo> map = getAsMap();
@@ -128,6 +129,11 @@
       return this;
     }
 
+    public ListRequest withSuggest(String suggest) {
+      this.suggest = suggest;
+      return this;
+    }
+
     public EnumSet<ListGroupsOption> getOptions() {
       return options;
     }
@@ -163,5 +169,9 @@
     public String getSubstring() {
       return substring;
     }
+
+    public String getSuggest() {
+      return suggest;
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
index e80d3d66..3e45523 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
@@ -18,23 +18,31 @@
 public class EditPreferencesInfo {
   public Integer tabSize;
   public Integer lineLength;
+  public Integer cursorBlinkRate;
   public Boolean hideTopMenu;
   public Boolean showTabs;
   public Boolean showWhitespaceErrors;
   public Boolean syntaxHighlighting;
   public Boolean hideLineNumbers;
+  public Boolean matchBrackets;
+  public Boolean autoCloseBrackets;
   public Theme theme;
+  public KeyMapType keyMapType;
 
   public static EditPreferencesInfo defaults() {
     EditPreferencesInfo i = new EditPreferencesInfo();
     i.tabSize = 8;
     i.lineLength = 100;
+    i.cursorBlinkRate = 0;
     i.hideTopMenu = false;
     i.showTabs = true;
     i.showWhitespaceErrors = false;
     i.syntaxHighlighting = true;
     i.hideLineNumbers = false;
+    i.matchBrackets = true;
+    i.autoCloseBrackets = false;
     i.theme = Theme.DEFAULT;
+    i.keyMapType = KeyMapType.DEFAULT;
     return i;
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java
new file mode 100644
index 0000000..261168d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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.
+
+package com.google.gerrit.extensions.client;
+
+public enum KeyMapType {
+  DEFAULT,
+  EMACS,
+  VIM
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
index d8ae4a8..8ee573e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.client.KeyMapType;
 import com.google.gerrit.extensions.client.Theme;
 import com.google.gwt.core.client.JavaScriptObject;
 
@@ -23,24 +24,32 @@
     EditPreferences p = createObject().cast();
     p.tabSize(in.tabSize);
     p.lineLength(in.lineLength);
+    p.cursorBlinkRate(in.cursorBlinkRate);
     p.hideTopMenu(in.hideTopMenu);
     p.showTabs(in.showTabs);
     p.showWhitespaceErrors(in.showWhitespaceErrors);
     p.syntaxHighlighting(in.syntaxHighlighting);
     p.hideLineNumbers(in.hideLineNumbers);
+    p.matchBrackets(in.matchBrackets);
+    p.autoCloseBrackets(in.autoCloseBrackets);
     p.theme(in.theme);
+    p.keyMapType(in.keyMapType);
     return p;
   }
 
   public final void copyTo(EditPreferencesInfo p) {
     p.tabSize = tabSize();
     p.lineLength = lineLength();
+    p.cursorBlinkRate = cursorBlinkRate();
     p.hideTopMenu = hideTopMenu();
     p.showTabs = showTabs();
     p.showWhitespaceErrors = showWhitespaceErrors();
     p.syntaxHighlighting = syntaxHighlighting();
     p.hideLineNumbers = hideLineNumbers();
+    p.matchBrackets = matchBrackets();
+    p.autoCloseBrackets = autoCloseBrackets();
     p.theme = theme();
+    p.keyMapType = keyMapType();
   }
 
   public final void theme(Theme i) {
@@ -48,13 +57,21 @@
   }
   private final native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
 
+  public final void keyMapType(KeyMapType i) {
+    setkeyMapTypeRaw(i != null ? i.toString() : KeyMapType.DEFAULT.toString());
+  }
+  private final native void setkeyMapTypeRaw(String i) /*-{ this.key_map_type = i }-*/;
+
   public final native void tabSize(int t) /*-{ this.tab_size = t }-*/;
   public final native void lineLength(int c) /*-{ this.line_length = c }-*/;
+  public final native void cursorBlinkRate(int r) /*-{ this.cursor_blink_rate = r }-*/;
   public final native void hideTopMenu(boolean s) /*-{ this.hide_top_menu = s }-*/;
   public final native void showTabs(boolean s) /*-{ this.show_tabs = s }-*/;
   public final native void showWhitespaceErrors(boolean s) /*-{ this.show_whitespace_errors = s }-*/;
   public final native void syntaxHighlighting(boolean s) /*-{ this.syntax_highlighting = s }-*/;
   public final native void hideLineNumbers(boolean s) /*-{ this.hide_line_numbers = s }-*/;
+  public final native void matchBrackets(boolean m) /*-{ this.match_brackets = m }-*/;
+  public final native void autoCloseBrackets(boolean c) /*-{ this.auto_close_brackets = c }-*/;
 
   public final Theme theme() {
     String s = themeRaw();
@@ -62,13 +79,22 @@
   }
   private final native String themeRaw() /*-{ return this.theme }-*/;
 
+  public final KeyMapType keyMapType() {
+    String s = keyMapTypeRaw();
+    return s != null ? KeyMapType.valueOf(s) : KeyMapType.DEFAULT;
+  }
+  private final native String keyMapTypeRaw() /*-{ return this.key_map_type }-*/;
+
   public final int tabSize() {return get("tab_size", 8); }
   public final int lineLength() {return get("line_length", 100); }
+  public final int cursorBlinkRate() {return get("cursor_blink_rate", 0); }
   public final native boolean hideTopMenu() /*-{ return this.hide_top_menu || false }-*/;
   public final native boolean showTabs() /*-{ return this.show_tabs || false }-*/;
   public final native boolean showWhitespaceErrors() /*-{ return this.show_whitespace_errors || false }-*/;
   public final native boolean syntaxHighlighting() /*-{ return this.syntax_highlighting || false }-*/;
   public final native boolean hideLineNumbers() /*-{ return this.hide_line_numbers || false }-*/;
+  public final native boolean matchBrackets() /*-{ return this.match_brackets || false }-*/;
+  public final native boolean autoCloseBrackets() /*-{ return this.auto_close_brackets || false }-*/;
   private final native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
 
   protected EditPreferences() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java
index 28b2aa8..f900c94 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.client.account.EditPreferences;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.NpIntTextBox;
+import com.google.gerrit.extensions.client.KeyMapType;
 import com.google.gerrit.extensions.client.Theme;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ChangeEvent;
@@ -55,12 +56,16 @@
   @UiField Anchor close;
   @UiField NpIntTextBox tabWidth;
   @UiField NpIntTextBox lineLength;
+  @UiField NpIntTextBox cursorBlinkRate;
   @UiField ToggleButton topMenu;
   @UiField ToggleButton syntaxHighlighting;
   @UiField ToggleButton showTabs;
   @UiField ToggleButton whitespaceErrors;
   @UiField ToggleButton lineNumbers;
+  @UiField ToggleButton matchBrackets;
+  @UiField ToggleButton autoCloseBrackets;
   @UiField ListBox theme;
+  @UiField ListBox keyMap;
   @UiField Button apply;
   @UiField Button save;
 
@@ -68,6 +73,7 @@
     this.view = view;
     initWidget(uiBinder.createAndBindUi(this));
     initTheme();
+    initKeyMapType();
   }
 
   void set(EditPreferences prefs) {
@@ -75,12 +81,16 @@
 
     tabWidth.setIntValue(prefs.tabSize());
     lineLength.setIntValue(prefs.lineLength());
+    cursorBlinkRate.setIntValue(prefs.cursorBlinkRate());
     topMenu.setValue(!prefs.hideTopMenu());
     syntaxHighlighting.setValue(prefs.syntaxHighlighting());
     showTabs.setValue(prefs.showTabs());
     whitespaceErrors.setValue(prefs.showWhitespaceErrors());
     lineNumbers.setValue(prefs.hideLineNumbers());
+    matchBrackets.setValue(prefs.matchBrackets());
+    autoCloseBrackets.setValue(prefs.autoCloseBrackets());
     setTheme(prefs.theme());
+    setKeyMapType(prefs.keyMapType());
   }
 
   @UiHandler("tabWidth")
@@ -101,6 +111,17 @@
     }
   }
 
+  @UiHandler("cursorBlinkRate")
+  void onCursoBlinkRate(ValueChangeEvent<String> e) {
+    String v = e.getValue();
+    if (v != null && v.length() > 0) {
+      // A negative value hides the cursor entirely:
+      // don't let user shoot himself in the foot.
+      prefs.cursorBlinkRate(Math.max(0, Integer.parseInt(v)));
+      view.getEditor().setOption("cursorBlinkRate", prefs.cursorBlinkRate());
+    }
+  }
+
   @UiHandler("topMenu")
   void onTopMenu(ValueChangeEvent<Boolean> e) {
     prefs.hideTopMenu(!e.getValue());
@@ -132,6 +153,18 @@
     view.setSyntaxHighlighting(prefs.syntaxHighlighting());
   }
 
+  @UiHandler("matchBrackets")
+  void onMatchBrackets(ValueChangeEvent<Boolean> e) {
+    prefs.matchBrackets(e.getValue());
+    view.getEditor().setOption("matchBrackets", prefs.matchBrackets());
+  }
+
+  @UiHandler("autoCloseBrackets")
+  void onCloseBrackets(ValueChangeEvent<Boolean> e) {
+    prefs.autoCloseBrackets(e.getValue());
+    view.getEditor().setOption("autoCloseBrackets", prefs.autoCloseBrackets());
+  }
+
   @UiHandler("theme")
   void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
     final Theme newTheme = Theme.valueOf(theme.getValue(theme.getSelectedIndex()));
@@ -150,6 +183,14 @@
     });
   }
 
+  @UiHandler("keyMap")
+  void onKeyMap(@SuppressWarnings("unused") ChangeEvent e) {
+    KeyMapType keyMapType = KeyMapType.valueOf(
+        keyMap.getValue(keyMap.getSelectedIndex()));
+    prefs.keyMapType(keyMapType);
+    view.getEditor().setOption("keyMap", keyMapType.name().toLowerCase());
+  }
+
   @UiHandler("apply")
   void onApply(@SuppressWarnings("unused") ClickEvent e) {
     close();
@@ -210,4 +251,27 @@
         Theme.TWILIGHT.name().toLowerCase(),
         Theme.TWILIGHT.name());
   }
+
+  private void setKeyMapType(KeyMapType v) {
+    String name = v != null ? v.name() : KeyMapType.DEFAULT.name();
+    for (int i = 0; i < keyMap.getItemCount(); i++) {
+      if (keyMap.getValue(i).equals(name)) {
+        keyMap.setSelectedIndex(i);
+        return;
+      }
+    }
+    keyMap.setSelectedIndex(0);
+  }
+
+  private void initKeyMapType() {
+    keyMap.addItem(
+        KeyMapType.DEFAULT.name().toLowerCase(),
+        KeyMapType.DEFAULT.name());
+    keyMap.addItem(
+        KeyMapType.EMACS.name().toLowerCase(),
+        KeyMapType.EMACS.name());
+    keyMap.addItem(
+        KeyMapType.VIM.name().toLowerCase(),
+        KeyMapType.VIM.name());
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml
index 84a70b4..ccf620c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml
@@ -166,6 +166,10 @@
         <td><g:ListBox ui:field='theme'/></td>
       </tr>
       <tr>
+        <th><ui:msg>Key Map</ui:msg></th>
+        <td><g:ListBox ui:field='keyMap'/></td>
+      </tr>
+      <tr>
         <th><ui:msg>Tab Width</ui:msg></th>
         <td><x:NpIntTextBox ui:field='tabWidth'
             visibleLength='4'
@@ -178,6 +182,12 @@
             alignment='RIGHT'/></td>
       </tr>
       <tr>
+        <th><ui:msg>Cursor Blink Rate</ui:msg></th>
+        <td><x:NpIntTextBox ui:field='cursorBlinkRate'
+            visibleLength='4'
+            alignment='RIGHT'/></td>
+      </tr>
+      <tr>
         <th><ui:msg>Top Menu</ui:msg></th>
         <td><g:ToggleButton ui:field='topMenu'>
           <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
@@ -213,6 +223,20 @@
         </g:ToggleButton></td>
       </tr>
       <tr>
+        <th><ui:msg>Match Brackets</ui:msg></th>
+        <td><g:ToggleButton ui:field='matchBrackets'>
+          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
+          <g:downFace><ui:msg>On</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Auto Close Brackets</ui:msg></th>
+        <td><g:ToggleButton ui:field='autoCloseBrackets'>
+          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
+          <g:downFace><ui:msg>On</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
         <td></td>
         <td>
           <g:Button ui:field='apply' styleName='{style.apply}'>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
index 60da8fb..3c91d25 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -44,6 +44,7 @@
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.client.KeyMapType;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
@@ -257,11 +258,18 @@
   @Override
   public void registerKeys() {
     super.registerKeys();
-    cm.addKeyMap(KeyMap.create()
+    KeyMap localKeyMap = KeyMap.create();
+    localKeyMap
         .on("Ctrl-L", gotoLine())
         .on("Cmd-L", gotoLine())
-        .on("Cmd-S", save())
-        .on("Ctrl-S", save()));
+        .on("Cmd-S", save());
+
+    // TODO(davido): Find a better way to prevent key maps collisions
+    if (prefs.keyMapType() != KeyMapType.EMACS) {
+      localKeyMap.on("Ctrl-S", save());
+    }
+
+    cm.addKeyMap(localKeyMap);
   }
 
   private Runnable gotoLine() {
@@ -433,15 +441,17 @@
     cm = CodeMirror.create(editor, Configuration.create()
         .set("value", content)
         .set("readOnly", false)
-        .set("cursorBlinkRate", 0)
+        .set("cursorBlinkRate", prefs.cursorBlinkRate())
         .set("cursorHeight", 0.85)
         .set("lineNumbers", prefs.hideLineNumbers())
         .set("tabSize", prefs.tabSize())
         .set("lineWrapping", false)
+        .set("matchBrackets", prefs.matchBrackets())
+        .set("autoCloseBrackets", prefs.autoCloseBrackets())
         .set("scrollbarStyle", "overlay")
         .set("styleSelectedText", true)
         .set("showTrailingSpace", prefs.showWhitespaceErrors())
-        .set("keyMap", "default")
+        .set("keyMap", prefs.keyMapType().name().toLowerCase())
         .set("theme", prefs.theme().name().toLowerCase())
         .set("mode", mode != null ? mode.mode() : null));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
index 97ba5d3..3d2c960 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
@@ -138,6 +138,7 @@
     list.setLimit(req.getLimit());
     list.setStart(req.getStart());
     list.setMatchSubstring(req.getSubstring());
+    list.setSuggest(req.getSuggest());
     try {
       return list.apply(tlr);
     } catch (OrmException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
index 86ce61e..fef09a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -60,6 +60,8 @@
   private static final int MAX_SIZE = 5 << 20;
   private static final String ZIP_TYPE = "application/zip";
   private static final Random rng = new Random();
+  private static final CharMatcher LOWERCASE_OR_DIGITS =
+      CharMatcher.inRange('a', 'z').or(CharMatcher.inRange('0', '9'));
 
   private final GitRepositoryManager repoManager;
   private final FileTypeRegistry registry;
@@ -128,8 +130,8 @@
   public BinaryResult downloadContent(ProjectState project, ObjectId revstr,
       String path, @Nullable String suffix)
           throws ResourceNotFoundException, IOException {
-    suffix = Strings.emptyToNull(CharMatcher.inRange('a', 'z')
-        .retainFrom(Strings.nullToEmpty(suffix)));
+    suffix = Strings.emptyToNull(
+        LOWERCASE_OR_DIGITS.retainFrom(Strings.nullToEmpty(suffix)));
 
     try (Repository repo = openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
index bf5193f..023e3d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
@@ -16,14 +16,18 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.client.ListGroupsOption;
 import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
@@ -32,6 +36,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.GetGroups;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupComparator;
 import com.google.gerrit.server.account.GroupControl;
@@ -64,6 +69,7 @@
   private final IdentifiedUser.GenericFactory userFactory;
   private final Provider<GetGroups> accountGetGroups;
   private final GroupJson json;
+  private final GroupBackend groupBackend;
 
   private EnumSet<ListGroupsOption> options =
       EnumSet.noneOf(ListGroupsOption.class);
@@ -73,6 +79,7 @@
   private int limit;
   private int start;
   private String matchSubstring;
+  private String suggest;
 
   @Option(name = "--project", aliases = {"-p"},
       usage = "projects for which the groups should be listed")
@@ -121,6 +128,11 @@
     this.matchSubstring = matchSubstring;
   }
 
+  @Option(name = "--suggest", usage = "to get a suggestion of groups")
+  public void setSuggest(String suggest) {
+    this.suggest = suggest;
+  }
+
   @Option(name = "-o", usage = "Output options per group")
   void addOption(ListGroupsOption o) {
     options.add(o);
@@ -137,7 +149,8 @@
       final GroupControl.GenericFactory genericGroupControlFactory,
       final Provider<IdentifiedUser> identifiedUser,
       final IdentifiedUser.GenericFactory userFactory,
-      final Provider<GetGroups> accountGetGroups, GroupJson json) {
+      final Provider<GetGroups> accountGetGroups, GroupJson json,
+      GroupBackend groupBackend) {
     this.groupCache = groupCache;
     this.groupControlFactory = groupControlFactory;
     this.genericGroupControlFactory = genericGroupControlFactory;
@@ -145,6 +158,7 @@
     this.userFactory = userFactory;
     this.accountGetGroups = accountGetGroups;
     this.json = json;
+    this.groupBackend = groupBackend;
   }
 
   public void setOptions(EnumSet<ListGroupsOption> options) {
@@ -161,7 +175,7 @@
 
   @Override
   public SortedMap<String, GroupInfo> apply(TopLevelResource resource)
-      throws OrmException {
+      throws OrmException, BadRequestException {
     SortedMap<String, GroupInfo> output = Maps.newTreeMap();
     for (GroupInfo info : get()) {
       output.put(MoreObjects.firstNonNull(
@@ -172,53 +186,107 @@
     return output;
   }
 
-  public List<GroupInfo> get() throws OrmException {
-    List<GroupInfo> groupInfos;
+  public List<GroupInfo> get() throws OrmException, BadRequestException {
+    if (!Strings.isNullOrEmpty(suggest)) {
+      return suggestGroups();
+    }
+
+    if (owned) {
+      return getGroupsOwnedBy(
+          user != null ? userFactory.create(user) : identifiedUser.get());
+    }
+
     if (user != null) {
-      if (owned) {
-        groupInfos = getGroupsOwnedBy(userFactory.create(user));
-      } else {
-        groupInfos = accountGetGroups.get().apply(
-            new AccountResource(userFactory.create(user)));
+      return accountGetGroups.get().apply(
+          new AccountResource(userFactory.create(user)));
+    }
+
+    return getAllGroups();
+  }
+
+  private List<GroupInfo> getAllGroups() throws OrmException {
+    List<GroupInfo> groupInfos;
+    List<AccountGroup> groupList;
+    if (!projects.isEmpty()) {
+      Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
+      for (final ProjectControl projectControl : projects) {
+        final Set<GroupReference> groupsRefs = projectControl.getAllGroups();
+        for (final GroupReference groupRef : groupsRefs) {
+          final AccountGroup group = groupCache.get(groupRef.getUUID());
+          if (group != null) {
+            groups.put(group.getGroupUUID(), group);
+          }
+        }
       }
+      groupList = filterGroups(groups.values());
     } else {
-      if (owned) {
-        groupInfos = getGroupsOwnedBy(identifiedUser.get());
-      } else {
-        List<AccountGroup> groupList;
-        if (!projects.isEmpty()) {
-          Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
-          for (final ProjectControl projectControl : projects) {
-            final Set<GroupReference> groupsRefs = projectControl.getAllGroups();
-            for (final GroupReference groupRef : groupsRefs) {
-              final AccountGroup group = groupCache.get(groupRef.getUUID());
-              if (group != null) {
-                groups.put(group.getGroupUUID(), group);
-              }
-            }
-          }
-          groupList = filterGroups(groups.values());
-        } else {
-          groupList = filterGroups(groupCache.all());
-        }
-        groupInfos = Lists.newArrayListWithCapacity(groupList.size());
-        int found = 0;
-        int foundIndex = 0;
-        for (AccountGroup group : groupList) {
-          if (foundIndex++ < start) {
-            continue;
-          }
-          if (limit > 0 && ++found > limit) {
-            break;
-          }
-          groupInfos.add(json.addOptions(options).format(
-              GroupDescriptions.forAccountGroup(group)));
-        }
+      groupList = filterGroups(groupCache.all());
+    }
+    groupInfos = Lists.newArrayListWithCapacity(groupList.size());
+    int found = 0;
+    int foundIndex = 0;
+    for (AccountGroup group : groupList) {
+      if (foundIndex++ < start) {
+        continue;
+      }
+      if (limit > 0 && ++found > limit) {
+        break;
+      }
+      groupInfos.add(json.addOptions(options).format(
+          GroupDescriptions.forAccountGroup(group)));
+    }
+    return groupInfos;
+  }
+
+  private List<GroupInfo> suggestGroups() throws OrmException, BadRequestException {
+    if (conflictingSuggestParameters()) {
+      throw new BadRequestException(
+          "You should only have no more than one --project and -n with --suggest");
+    }
+
+    List<GroupReference> groupRefs = Lists.newArrayList(Iterables.limit(
+        groupBackend.suggest(
+            suggest, Iterables.getFirst(projects, null)),
+        limit <= 0 ? 10 : Math.min(limit, 10)));
+
+    List<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(groupRefs.size());
+    for (final GroupReference ref : groupRefs) {
+      GroupDescription.Basic desc = groupBackend.get(ref.getUUID());
+      if (desc != null) {
+        groupInfos.add(json.addOptions(options).format(desc));
       }
     }
     return groupInfos;
   }
 
+  private boolean conflictingSuggestParameters() {
+    if (Strings.isNullOrEmpty(suggest)) {
+      return false;
+    }
+    if (projects.size() > 1) {
+      return true;
+    }
+    if (visibleToAll) {
+      return true;
+    }
+    if (user != null) {
+      return true;
+    }
+    if (owned) {
+      return true;
+    }
+    if (start != 0) {
+      return true;
+    }
+    if (!groupsToInspect.isEmpty()) {
+      return true;
+    }
+    if (!Strings.isNullOrEmpty(matchSubstring)) {
+      return true;
+    }
+    return false;
+  }
+
   private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user)
       throws OrmException {
     List<GroupInfo> groups = Lists.newArrayList();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index ad72b13..75072e8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -19,10 +19,12 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GetGroups;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupJson;
@@ -71,12 +73,13 @@
         final Provider<IdentifiedUser> identifiedUser,
         final IdentifiedUser.GenericFactory userFactory,
         final Provider<GetGroups> accountGetGroups,
-        final GroupJson json) {
+        final GroupJson json,
+        GroupBackend groupBackend) {
       super(groupCache, groupControlFactory, genericGroupControlFactory,
-          identifiedUser, userFactory, accountGetGroups, json);
+          identifiedUser, userFactory, accountGetGroups, json, groupBackend);
     }
 
-    void display(final PrintWriter out) throws OrmException {
+    void display(final PrintWriter out) throws OrmException, BadRequestException {
       final ColumnFormatter formatter = new ColumnFormatter(out, '\t');
       for (final GroupInfo info : get()) {
         formatter.addColumn(MoreObjects.firstNonNull(info.name, "n/a"));
diff --git a/lib/codemirror/cm.defs b/lib/codemirror/cm.defs
index 8259252..ca1c9ae 100644
--- a/lib/codemirror/cm.defs
+++ b/lib/codemirror/cm.defs
@@ -9,10 +9,13 @@
   'lib/codemirror.js',
   'mode/meta.js',
   'keymap/vim.js',
+  'keymap/emacs.js',
 ]
 
 CM_ADDONS = [
   'dialog/dialog.js',
+  'edit/closebrackets.js',
+  'edit/matchbrackets.js',
   'edit/trailingspace.js',
   'scroll/annotatescrollbar.js',
   'scroll/simplescrollbars.js',