PageRenderTime 57ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/runtime/src/jycessing/mode/PyInputHandler.java

https://github.com/aparrish/processing.py
Java | 403 lines | 297 code | 57 blank | 49 comment | 92 complexity | 6335e197e9ab0863c46e860201b6ec7d MD5 | raw file
  1. package jycessing.mode;
  2. import java.awt.Toolkit;
  3. import java.awt.event.ActionEvent;
  4. import java.awt.event.KeyEvent;
  5. import java.util.Stack;
  6. import java.util.regex.Matcher;
  7. import java.util.regex.Pattern;
  8. import processing.app.Sketch;
  9. import processing.app.syntax.JEditTextArea;
  10. import processing.app.syntax.PdeInputHandler;
  11. import processing.app.ui.Editor;
  12. /**
  13. * This class provides Pythonic handling of keystrokes.
  14. */
  15. public class PyInputHandler extends PdeInputHandler {
  16. final PyEditor pyEditor;
  17. // ctrl-alt on windows & linux, cmd-alt on os x
  18. private static int CTRL_ALT = ActionEvent.ALT_MASK
  19. | Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
  20. // 4 spaces per pep8
  21. private static final String TAB = " ";
  22. private static final int TAB_SIZE = TAB.length();
  23. public PyInputHandler(final Editor editor) {
  24. pyEditor = (PyEditor)editor;
  25. }
  26. private static boolean isPrintableChar(final char c) {
  27. if (c >= 32 && c <= 127) {
  28. return true;
  29. }
  30. if (c == KeyEvent.CHAR_UNDEFINED || Character.isISOControl(c)) {
  31. return false;
  32. }
  33. final Character.UnicodeBlock block = Character.UnicodeBlock.of(c);
  34. return block != null && block != Character.UnicodeBlock.SPECIALS;
  35. }
  36. JEditTextArea getTextArea() {
  37. return pyEditor.getTextArea();
  38. }
  39. @Override
  40. public boolean handlePressed(final KeyEvent event) {
  41. final char c = event.getKeyChar();
  42. final int code = event.getKeyCode();
  43. final int mods = event.getModifiers();
  44. final JEditTextArea textArea = getTextArea();
  45. final Sketch sketch = pyEditor.getSketch();
  46. // things that change the content of the text area
  47. if (!event.isMetaDown()
  48. && (code == KeyEvent.VK_BACK_SPACE || code == KeyEvent.VK_TAB || code == KeyEvent.VK_ENTER || isPrintableChar(c))) {
  49. sketch.setModified(true);
  50. }
  51. if (event.isMetaDown() && code == KeyEvent.VK_UP) {
  52. textArea.setCaretPosition(0);
  53. textArea.scrollToCaret();
  54. return true;
  55. }
  56. if (event.isMetaDown() && code == KeyEvent.VK_DOWN) {
  57. textArea.setCaretPosition(textArea.getDocumentLength());
  58. textArea.scrollToCaret();
  59. return true;
  60. }
  61. // ctrl-alt-[arrow] switches sketch tab
  62. if ((mods & CTRL_ALT) == CTRL_ALT) {
  63. if (code == KeyEvent.VK_LEFT) {
  64. sketch.handlePrevCode();
  65. return true;
  66. } else if (code == KeyEvent.VK_RIGHT) {
  67. sketch.handleNextCode();
  68. return true;
  69. }
  70. }
  71. final int thisLine = textArea.getCaretLine();
  72. final int thisPos = textArea.getCaretPosition();
  73. switch (code) {
  74. case KeyEvent.VK_BACK_SPACE:
  75. if (thisPos == textArea.getLineStartOffset(thisLine)) {
  76. // Let the user backspace onto the previous line.
  77. break;
  78. }
  79. final LineInfo currentLine = new LineInfo(thisLine);
  80. if (currentLine.caretInText) {
  81. // The caret is in the text; let the text editor handle this backspace.
  82. break;
  83. }
  84. // The caret is not in the text; treat it as a request to unindent.
  85. indent(-1);
  86. return true;
  87. case KeyEvent.VK_ESCAPE:
  88. textArea.selectNone();
  89. pyEditor.handleStop();
  90. return true;
  91. case KeyEvent.VK_TAB:
  92. indent(event.isShiftDown() ? -1 : 1);
  93. return true;
  94. case KeyEvent.VK_ENTER: // return
  95. final String text = textArea.getText(); // text
  96. textArea.setSelectedText(newline());
  97. break;
  98. }
  99. return false;
  100. }
  101. /**
  102. * A line is some whitespace followed by a bunch of whatever.
  103. */
  104. private static final Pattern LINE = Pattern.compile("^(\\s*)(.*)$");
  105. /**
  106. * Everything we need to know about a line in the text editor.
  107. */
  108. private class LineInfo {
  109. public final int lineNumber;
  110. // Expressed in units of "python indents", not in number of spaces.
  111. public final int indent;
  112. // The text content after whatever indent.
  113. public final String text;
  114. // Whether or not the caret happens to be positioned in the text portion of the line.
  115. public final boolean caretInText;
  116. LineInfo(final int lineNumber) {
  117. this.lineNumber = lineNumber;
  118. final JEditTextArea textArea = getTextArea();
  119. final Matcher m = LINE.matcher(textArea.getLineText(lineNumber));
  120. if (!m.matches()) {
  121. throw new AssertionError("How can a line have less than nothing in it?");
  122. }
  123. final String space = m.group(1);
  124. text = m.group(2);
  125. final int caretLinePos =
  126. textArea.getCaretPosition() - textArea.getLineStartOffset(lineNumber);
  127. caretInText = caretLinePos > space.length();
  128. // Calculate the current indent measured in tab stops of TAB_SIZE spaces.
  129. int currentIndent = 0;
  130. int spaceCounter = 0;
  131. for (int i = 0; i < space.length(); i++) {
  132. spaceCounter++;
  133. // A literal tab character advances to the next tab stop, as does the TAB_SIZEth space
  134. // character in a row.
  135. if (spaceCounter % TAB_SIZE == 0 || space.charAt(i) == '\t') {
  136. currentIndent++;
  137. spaceCounter = 0;
  138. }
  139. }
  140. indent = currentIndent;
  141. }
  142. @Override
  143. public String toString() {
  144. return String.format("<Line %d, indent %d, {%s}>", lineNumber, indent, text);
  145. }
  146. }
  147. /**
  148. * Maybe change the indent of the current selection. If sign is positive, then increase the
  149. * indent; otherwise, decrease it.
  150. * <p>If the last non-comment, non-blank line ends with ":", then the maximum indent for the
  151. * current line is one greater than the indent of that ":"-bearing line. Otherwise, the maximum
  152. * indent is equal to the indent of the last non-comment line.
  153. * <p>The minimum indent is 0.
  154. * @param sign The direction in which to modify the indent of the current line.
  155. */
  156. public void indent(final int sign) {
  157. final JEditTextArea textArea = getTextArea();
  158. final int startLine = textArea.getSelectionStartLine();
  159. final int stopLine = textArea.getSelectionStopLine();
  160. final int selectionStart = textArea.getSelectionStart();
  161. final int selectionStop = textArea.getSelectionStop();
  162. final LineInfo currentLine = new LineInfo(startLine);
  163. final int currentCaret = textArea.getCaretPosition();
  164. final int startLineEndRelativePos = textArea.getLineStopOffset(startLine) - selectionStart;
  165. final int stopLineEndRelativePos = textArea.getLineStopOffset(stopLine) - selectionStop;
  166. final int newIndent;
  167. if (sign > 0) {
  168. // Find previous non-blank non-comment line.
  169. LineInfo candidate = null;
  170. for (int i = startLine - 1; i >= 0; i--) {
  171. candidate = new LineInfo(i);
  172. if (candidate.text.length() > 0 && !candidate.text.startsWith("#")) {
  173. break;
  174. }
  175. }
  176. if (candidate == null) {
  177. newIndent = 0;
  178. } else {
  179. final String trimmed = candidate.text.trim();
  180. if (trimmed.endsWith(":") || trimmed.endsWith("(")) {
  181. newIndent = Math.min(candidate.indent + 1, currentLine.indent + 1);
  182. } else {
  183. newIndent = Math.min(candidate.indent, currentLine.indent + 1);
  184. }
  185. }
  186. } else {
  187. newIndent = Math.max(0, currentLine.indent - 1);
  188. }
  189. final int deltaIndent = newIndent - currentLine.indent;
  190. for (int i = startLine; i <= stopLine; i++) {
  191. indentLineBy(i, deltaIndent);
  192. }
  193. textArea.setSelectionStart(getAbsoluteCaretPositionRelativeToLineEnd(startLine,
  194. startLineEndRelativePos));
  195. textArea.setSelectionEnd(getAbsoluteCaretPositionRelativeToLineEnd(stopLine,
  196. stopLineEndRelativePos));
  197. }
  198. private int getAbsoluteCaretPositionRelativeToLineEnd(final int line,
  199. final int lineEndRelativePosition) {
  200. final JEditTextArea textArea = getTextArea();
  201. return Math.max(textArea.getLineStopOffset(line) - lineEndRelativePosition, textArea
  202. .getLineStartOffset(line));
  203. }
  204. private void indentLineBy(final int line, final int deltaIndent) {
  205. final JEditTextArea textArea = getTextArea();
  206. final LineInfo currentLine = new LineInfo(line);
  207. final int newIndent = Math.max(0, currentLine.indent + deltaIndent);
  208. final StringBuilder sb = new StringBuilder();
  209. for (int i = 0; i < newIndent; i++) {
  210. sb.append(TAB);
  211. }
  212. sb.append(currentLine.text);
  213. textArea.select(textArea.getLineStartOffset(line), textArea.getLineStopOffset(line) - 1);
  214. textArea.setSelectedText(sb.toString());
  215. textArea.selectNone();
  216. }
  217. private static final Pattern INITIAL_WHITESPACE = Pattern.compile("^(\\s*)");
  218. /*
  219. * This can be fooled by a line like
  220. * print "He said: #LOLHASHTAG!"
  221. */
  222. private static final Pattern TERMINAL_COLON = Pattern.compile(":\\s*(#.*)?$");
  223. private static final Pattern POP_CONTEXT = Pattern.compile("^\\s*(return|break|continue)\\b");
  224. private boolean isOpen(final char c) {
  225. return c == '(' || c == '[' || c == '{';
  226. }
  227. private boolean isClose(final char c) {
  228. return c == ')' || c == ']' || c == '}';
  229. }
  230. /**
  231. * Search for an unterminated paren or bracket. If found, return
  232. * its index in the given text. Otherwise return -1.
  233. * <p>Ignores syntax errors, treating (foo] as a valid construct.
  234. * <p>Assumes that the text contains no surrogate characters.
  235. * @param cursor The current cursor position in the given text.
  236. * @param text The text to search for an unterminated paren or bracket.
  237. * @return The index of the unterminated paren, or -1.
  238. */
  239. private int indexOfUnclosedParen() {
  240. final JEditTextArea textArea = getTextArea();
  241. final int cursor = textArea.getCaretPosition();
  242. final String text = textArea.getText();
  243. final Stack<Integer> stack = new Stack<Integer>();
  244. int column = 0;
  245. for (int i = 0; i < cursor; i++) {
  246. final char c = text.charAt(i);
  247. if (isOpen(c)) {
  248. stack.push(column);
  249. } else if (isClose(c)) {
  250. if (stack.size() == 0) {
  251. // Syntax error; bail.
  252. return -1;
  253. }
  254. stack.pop();
  255. }
  256. if (c == '\n') {
  257. column = 0;
  258. } else {
  259. column++;
  260. }
  261. }
  262. return stack.size() > 0 ? stack.pop() : -1;
  263. }
  264. private String indentOf(final String line) {
  265. final Matcher m = INITIAL_WHITESPACE.matcher(line);
  266. if (!m.find()) {
  267. throw new AssertionError("How can there be nothing?");
  268. }
  269. return m.group();
  270. }
  271. private String getInitialWhitespace() {
  272. final JEditTextArea textArea = getTextArea();
  273. final String text = textArea.getText();
  274. final int cursor = textArea.getCaretPosition();
  275. final int lineNumber = textArea.getLineOfOffset(cursor);
  276. final int lineStart = textArea.getLineStartOffset(lineNumber);
  277. final int lineEnd = textArea.getLineStopOffset(lineNumber);
  278. final String line = textArea.getLineText(lineNumber);
  279. final String defaultIndent = indentOf(line);
  280. // Search for an unmatched closing paren on this line.
  281. int balance = 0;
  282. for (int i = cursor - 1; i >= lineStart; i--) {
  283. if (isClose(text.charAt(i))) {
  284. balance++;
  285. } else if (isOpen(text.charAt(i))) {
  286. balance--;
  287. }
  288. }
  289. if (balance == 0) {
  290. return defaultIndent;
  291. }
  292. if (balance > 0) {
  293. int index = lineStart - 1;
  294. while (balance > 0 && index >= 0) {
  295. if (isClose(text.charAt(index))) {
  296. balance++;
  297. } else if (isOpen(text.charAt(index))) {
  298. balance--;
  299. }
  300. index--;
  301. }
  302. if (balance != 0) {
  303. // Syntax error
  304. return defaultIndent;
  305. }
  306. return indentOf(textArea.getLineText(textArea.getLineOfOffset(index)));
  307. }
  308. final int parenColumn = indexOfUnclosedParen();
  309. if (parenColumn > -1) {
  310. return nSpaces(parenColumn + 1);
  311. }
  312. return defaultIndent;
  313. }
  314. private String newline() {
  315. final JEditTextArea textArea = getTextArea();
  316. final int cursor = textArea.getCaretPosition();
  317. if (cursor <= 1) {
  318. return "\n";
  319. }
  320. final int lineNumber = textArea.getLineOfOffset(cursor);
  321. final int lineStart = textArea.getLineStartOffset(lineNumber);
  322. final String line = textArea.getLineText(lineNumber);
  323. final String initialWhitespace = getInitialWhitespace();
  324. final String lineTextBeforeCursor = line.substring(0, cursor - lineStart);
  325. if (Pattern.matches("\\s*", lineTextBeforeCursor)) {
  326. return "\n" + initialWhitespace;
  327. }
  328. if (TERMINAL_COLON.matcher(line).find()) {
  329. return "\n" + initialWhitespace + TAB;
  330. }
  331. // TODO: popping context on return should return to the indent of the last def.
  332. if (POP_CONTEXT.matcher(line).find()) {
  333. final int currentIndentLength = initialWhitespace.length();
  334. final int spaceCount = Math.max(0, currentIndentLength - 4);
  335. return "\n" + nSpaces(spaceCount);
  336. }
  337. return "\n" + initialWhitespace;
  338. }
  339. private static final String nSpaces(final int n) {
  340. final StringBuilder sb = new StringBuilder(n);
  341. for (int i = 0; i < n; i++) {
  342. sb.append(' ');
  343. }
  344. return sb.toString();
  345. }
  346. }