diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt index d753d90cdf..7497a64503 100644 --- a/Documentation/rest-api-accounts.txt +++ b/Documentation/rest-api-accounts.txt @@ -1339,11 +1339,15 @@ link:#edit-preferences-info[EditPreferencesInfo] entity. )]}' { "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 @@ link:#edit-preferences-info[EditPreferencesInfo] entity. { "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 @@ preferences of a user. 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 whitespace errors should be shown. 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/user-inline-edit.txt b/Documentation/user-inline-edit.txt index 2da0c2530e..fff2501032 100644 --- a/Documentation/user-inline-edit.txt +++ b/Documentation/user-inline-edit.txt @@ -176,9 +176,6 @@ with thefollowing logic on click: ** "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/rest/account/EditPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java index de6e3aa017..8770c3c47e 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 static com.google.common.truth.Truth.assertThat; 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 @@ public class EditPreferencesIT extends AbstractDaemonTest { 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 @@ public class EditPreferencesIT extends AbstractDaemonTest { 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/client/EditPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java index e80d3d66bf..3e455230c1 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 @@ package com.google.gerrit.extensions.client; 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 0000000000..261168ddee --- /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 d8ae4a88cc..8ee573ecae 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 @@ public class EditPreferences extends JavaScriptObject { 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 @@ public class EditPreferences extends JavaScriptObject { } 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 @@ public class EditPreferences extends JavaScriptObject { } 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 28b2aa82b4..f900c94ac5 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.AccountApi; 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 @@ class EditPreferencesBox extends Composite { @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 @@ class EditPreferencesBox extends Composite { this.view = view; initWidget(uiBinder.createAndBindUi(this)); initTheme(); + initKeyMapType(); } void set(EditPreferences prefs) { @@ -75,12 +81,16 @@ class EditPreferencesBox extends Composite { 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 @@ class EditPreferencesBox extends Composite { } } + @UiHandler("cursorBlinkRate") + void onCursoBlinkRate(ValueChangeEvent 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 e) { prefs.hideTopMenu(!e.getValue()); @@ -132,6 +153,18 @@ class EditPreferencesBox extends Composite { view.setSyntaxHighlighting(prefs.syntaxHighlighting()); } + @UiHandler("matchBrackets") + void onMatchBrackets(ValueChangeEvent e) { + prefs.matchBrackets(e.getValue()); + view.getEditor().setOption("matchBrackets", prefs.matchBrackets()); + } + + @UiHandler("autoCloseBrackets") + void onCloseBrackets(ValueChangeEvent 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 @@ class EditPreferencesBox extends Composite { }); } + @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 @@ class EditPreferencesBox extends Composite { 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 84a70b42bb..ccf620c94a 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 @@ -165,6 +165,10 @@ limitations under the License. Theme + + Key Map + + Tab Width + + Cursor Blink Rate + + Top Menu @@ -212,6 +222,20 @@ limitations under the License. Show + + Match Brackets + + Off + On + + + + Auto Close Brackets + + Off + On + + 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 60da8fb6aa..3c91d25303 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.InlineHyperlink; 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 @@ public class EditScreen extends Screen { @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 @@ public class EditScreen extends Screen { 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/lib/codemirror/cm.defs b/lib/codemirror/cm.defs index 8259252289..ca1c9ae6fc 100644 --- a/lib/codemirror/cm.defs +++ b/lib/codemirror/cm.defs @@ -9,10 +9,13 @@ CM_JS = [ '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',