PY-11855 Run manage.py task improvements
authorIlya.Kazakevich <Ilya.Kazakevich@jetbrains.com>
Tue, 3 Feb 2015 16:06:22 +0000 (19:06 +0300)
committerIlya.Kazakevich <Ilya.Kazakevich@jetbrains.com>
Tue, 3 Feb 2015 16:06:22 +0000 (19:06 +0300)
Arguments are parsed and added correctly

14 files changed:
python/src/com/jetbrains/python/commandInterface/CommandInterfaceView.java
python/src/com/jetbrains/python/commandInterface/commandsWithArgs/Command.java
python/src/com/jetbrains/python/commandInterface/commandsWithArgs/CommandAdapter.java
python/src/com/jetbrains/python/commandInterface/commandsWithArgs/CommandInterfacePresenterCommandBased.java
python/src/com/jetbrains/python/commandInterface/commandsWithArgs/InCommandStrategy.java
python/src/com/jetbrains/python/commandInterface/commandsWithArgs/Strategy.java
python/src/com/jetbrains/python/commandInterface/commandsWithArgs/SuggestionInfo.java [new file with mode: 0644]
python/src/com/jetbrains/python/commandInterface/commandsWithArgs/package-info.java
python/src/com/jetbrains/python/commandInterface/swingView/CommandInterfaceViewSwingImpl.java
python/src/com/jetbrains/python/commandInterface/swingView/SmartTextField.java
python/src/com/jetbrains/python/optParse/MalformedCommandLineException.java [new file with mode: 0644]
python/src/com/jetbrains/python/optParse/ParsedCommandLine.java [new file with mode: 0644]
python/src/com/jetbrains/python/optParse/WordWithPosition.java [new file with mode: 0644]
python/src/com/jetbrains/python/optParse/package-info.java [new file with mode: 0644]

index d1f88b3f2c558c98271abaac7f8ab221fd4d9ddd..e0780a72caf9459d4bb902e21ba006684ef91cdc 100644 (file)
 package com.jetbrains.python.commandInterface;
 
 import com.jetbrains.python.suggestionList.SuggestionsBuilder;
+import com.jetbrains.python.optParse.WordWithPosition;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import java.util.Collection;
+
 /**
  * View for command-line interface to be paired with view.
  *
@@ -71,7 +74,6 @@ public interface CommandInterfaceView {
    *
    * @param message text to display
    */
-  void displayInfoBaloon(@NotNull String message);
 
   /**
    * @return text, entered by user
@@ -85,4 +87,15 @@ public interface CommandInterfaceView {
    * @param widthInChars number of chars
    */
   void setPreferredWidthInChars(int widthInChars);
+
+  /**
+   * Displays help balloon when cursor meets certain place.
+   * Each balloon is described as start-end position (in chars) where it should be enabled
+   * and test to display.
+   * <strong>Caution: Each call removes previuos balloons!</strong>
+   *
+   * @param balloons list of balloons to display (i.e. you want to text 'foo' be displayed when user sets cursor on position
+   *                 from 1 to 3, so you add 'foo',1,4 here)
+   */
+  void setBalloons(@NotNull final Collection<WordWithPosition> balloons);
 }
index 3c477e385e3370111e4756f980cb6553417caad7..b639b992db888ebb2189543284ffafc8b18b8f7f 100644 (file)
@@ -45,4 +45,11 @@ public interface Command {
    */
   @Nullable
   String getHelp();
+
+
+  /**
+   * @return Argument help string to display on arguments
+   */
+  @Nullable
+  String getArgumentHelp();
 }
index 51a31af9106b61f9ceb14fdcb058549f25b9433e..b1509e012974fa3d33c4fcbbb3c80b30858888e5 100644 (file)
@@ -75,4 +75,10 @@ public class CommandAdapter implements Command {
   public final String getHelp() {
     return myHelp;
   }
+
+  @Nullable
+  @Override
+  public String getArgumentHelp() {
+    return null;
+  }
 }
index 400a57557d7ebce6b2291e357074bdafdae62a12..19c5291f109a1accd8173ce23ca1e75580d0be66 100644 (file)
@@ -19,22 +19,21 @@ import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.util.ArrayUtil;
 import com.jetbrains.python.commandInterface.CommandInterfacePresenterAdapter;
 import com.jetbrains.python.commandInterface.CommandInterfaceView;
-import com.jetbrains.python.commandInterface.commandsWithArgs.Strategy.SuggestionInfo;
+import com.jetbrains.python.optParse.MalformedCommandLineException;
+import com.jetbrains.python.optParse.ParsedCommandLine;
 import com.jetbrains.python.suggestionList.SuggestionsBuilder;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.*;
-import java.util.regex.Pattern;
 
 /**
  * Command-line interface presenter that is command-based
  *
- * @author Ilya.Kazakevich
  * @param <C> Command type
+ * @author Ilya.Kazakevich
  */
 public class CommandInterfacePresenterCommandBased<C extends Command> extends CommandInterfacePresenterAdapter {
-  private static final Pattern EMPTY_SPACE = Pattern.compile("\\s+");
   /**
    * [name] -> command. Linked is used to preserve order.
    */
@@ -92,6 +91,7 @@ public class CommandInterfacePresenterCommandBased<C extends Command> extends Co
         myView.showError(true);
         break;
     }
+    myView.setBalloons(myStrategy.getBalloonsToShow());
 
     final SuggestionInfo suggestionInfo = myStrategy.getSuggestionInfo();
     final List<String> suggestions = new ArrayList<String>(suggestionInfo.getSuggestions());
@@ -165,19 +165,15 @@ public class CommandInterfacePresenterCommandBased<C extends Command> extends Co
    * Finds and sets appropriate strategy
    */
   private void configureStrategy() {
-    if (myView.getText().isEmpty()) {
-      myStrategy = new NoCommandStrategy(this);
-      return;
-    }
-    final String[] parts = getTextAsParts();
-    if (parts.length > 0) {
-      final Command command = myCommands.get(parts[0]);
+    final ParsedCommandLine line = getParsedCommandLine();
+    if (line != null) {
+      final Command command = myCommands.get(line.getCommand().getText());
       if (command != null) {
-        myStrategy = new InCommandStrategy(command, this);
+        myStrategy = new InCommandStrategy(command, line, this);
         return;
       }
     }
-    myStrategy = new NoCommandStrategy(this); // Junk
+    myStrategy = new NoCommandStrategy(this); // No command or bad command found
   }
 
   @Override
@@ -185,7 +181,8 @@ public class CommandInterfacePresenterCommandBased<C extends Command> extends Co
     if (valueFromSuggestionList != null) {
       final SuggestionInfo suggestionInfo = myStrategy.getSuggestionInfo();
       if (suggestionInfo.getSuggestions().contains(valueFromSuggestionList)) {
-        final List<String> words = new ArrayList<String>(Arrays.asList(getTextAsParts()));
+        final ParsedCommandLine commandLine = getParsedCommandLine();
+        final List<String> words = commandLine != null ? commandLine.getAsWords() : new ArrayList<String>();
         if (!words.isEmpty() && myLastSuggestionTiedToWord) {
           words.remove(words.size() - 1);
         }
@@ -223,21 +220,30 @@ public class CommandInterfacePresenterCommandBased<C extends Command> extends Co
   }
 
   /**
-   * @return current text splitted into parts
+   * @return parsed commandline entered by user
    */
-  @NotNull
-  String[] getTextAsParts() {
-    final String[] parts = EMPTY_SPACE.split(myView.getText());
-    return (((parts.length == 1) && parts[0].isEmpty()) ? ArrayUtil.EMPTY_STRING_ARRAY : parts);
+  @Nullable
+  final ParsedCommandLine getParsedCommandLine() {
+    try {
+      return new ParsedCommandLine(myView.getText());
+    }
+    catch (final MalformedCommandLineException ignored) {
+      return null;
+    }
   }
 
+
   /**
    * @return last part of splitted text (if any). I.e. "foo bar spam" will return "spam"
    */
   @Nullable
-  String getLastPart() {
-    final String[] parts = getTextAsParts();
-    return ((parts.length > 0) ? parts[parts.length - 1] : null);
+   final String getLastPart() {
+    final ParsedCommandLine commandLine = getParsedCommandLine();
+    if (commandLine == null || commandLine.getAsWords().isEmpty()) {
+      return null;
+    }
+    final List<String> words = commandLine.getAsWords();
+    return words.get(words.size() - 1);
   }
 
   /**
index 684aa733349350e1a3836fa4e1131f6940e07f6b..f0a98d1f8fd3ab68d8e396fa9008f9e724f29443 100644 (file)
  */
 package com.jetbrains.python.commandInterface.commandsWithArgs;
 
+import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.util.ArrayUtil;
+import com.jetbrains.python.optParse.ParsedCommandLine;
+import com.jetbrains.python.optParse.WordWithPosition;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -28,22 +31,25 @@ import java.util.List;
  *
  * @author Ilya.Kazakevich
  */
-class InCommandStrategy extends Strategy {
+final class InCommandStrategy extends Strategy {
   @NotNull
   private final List<String> myArguments = new ArrayList<String>();
   @NotNull
   private final Command myCommand;
+  @NotNull
+  private final ParsedCommandLine myCommandLine;
 
   /**
    * @param command   command enrtered by user
    * @param presenter presenter
    */
-  InCommandStrategy(@NotNull final Command command, @NotNull final CommandInterfacePresenterCommandBased presenter) {
+  InCommandStrategy(@NotNull final Command command,
+                    @NotNull final ParsedCommandLine commandLine,
+                    @NotNull final CommandInterfacePresenterCommandBased<?> presenter) {
     super(presenter);
-    final List<String> parts = Arrays.asList(presenter.getTextAsParts());
-    assert !parts.isEmpty() && parts.get(0).equals(command.getName()) : "At least first argument should be command";
-    myArguments.addAll(parts.subList(1, parts.size()));
+    myArguments.addAll(WordWithPosition.fetchText(commandLine.getArguments()));
     myCommand = command;
+    myCommandLine = commandLine;
   }
 
   @NotNull
@@ -68,9 +74,29 @@ class InCommandStrategy extends Strategy {
     return new SuggestionInfo(false, false, strings);
   }
 
+  @NotNull
+  @Override
+  List<WordWithPosition> getBalloonsToShow() {
+    // Display argument balloons right from command end to last argument end
+    final String argumentHelp = myCommand.getArgumentHelp();
+    if (StringUtil.isEmpty(argumentHelp)) {
+      return Collections.emptyList();
+    }
+    final List<WordWithPosition> arguments = myCommandLine.getArguments();
+    if (arguments.isEmpty()) {
+      // If no arguments provided, then display popup right after command
+      return Collections.singletonList(new WordWithPosition(argumentHelp, myCommandLine.getCommand().getTo() + 1, Integer.MAX_VALUE));
+    }
+    final List<WordWithPosition> result = new ArrayList<WordWithPosition>(arguments.size());
+    for (final WordWithPosition argument : arguments) {
+      result.add(argument.copyWithDifferentText(argumentHelp));
+    }
+    return result;
+  }
+
   @Override
   boolean isUnknownTextExists() {
-    if (myPresenter.getTextAsParts().length == 1) {
+    if (myCommandLine.getAsWords().isEmpty()) {
       return false; // Command only
     }
     final String lastPart = myPresenter.getLastPart();
index 8801f57bd425a85a0e2c43a0302322049336615a..3facb047c0d7895950c019f50f65cbbc31109ace 100644 (file)
  */
 package com.jetbrains.python.commandInterface.commandsWithArgs;
 
+import com.jetbrains.python.optParse.WordWithPosition;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
@@ -30,12 +30,12 @@ import java.util.List;
  */
 abstract class Strategy {
   @NotNull
-  protected final CommandInterfacePresenterCommandBased myPresenter;
+  protected final CommandInterfacePresenterCommandBased<?> myPresenter;
 
   /**
    * @param presenter presenter
    */
-  protected Strategy(@NotNull final CommandInterfacePresenterCommandBased presenter) {
+  protected Strategy(@NotNull final CommandInterfacePresenterCommandBased<?> presenter) {
     myPresenter = presenter;
   }
 
@@ -51,6 +51,10 @@ abstract class Strategy {
   @NotNull
   abstract SuggestionInfo getSuggestionInfo();
 
+  @NotNull
+  List<WordWithPosition> getBalloonsToShow() {
+    return Collections.emptyList();
+  }
 
   /**
    * @return command that entered in box, or null of just entered
@@ -87,41 +91,4 @@ abstract class Strategy {
      */
     NO
   }
-
-  @SuppressWarnings("PackageVisibleField") // No need to hide field: everything is internal API in package, anyway
-  static class SuggestionInfo {
-    /**
-     * Suggestions
-     */
-    private final List<String> mySuggestions = new ArrayList<String>();
-    /**
-     * Display them at absolute location or relative to last letter
-     */
-    final boolean myAbsolute;
-    /**
-     * Show then any time, or only when user requests them
-     */
-    final boolean myShowOnlyWhenRequested;
-
-    /**
-     * @param absolute              Display them at absolute location or relative to last letter
-     * @param showOnlyWhenRequested Show then any time, or only when user requests them
-     * @param suggestions           Suggestions
-     */
-    SuggestionInfo(final boolean absolute,
-                   final boolean showOnlyWhenRequested,
-                   @NotNull final List<String> suggestions) {
-      myAbsolute = absolute;
-      myShowOnlyWhenRequested = showOnlyWhenRequested;
-      mySuggestions.addAll(suggestions);
-    }
-
-    /**
-     * @return suggestions
-     */
-    @NotNull
-    List<String> getSuggestions() {
-      return Collections.unmodifiableList(mySuggestions);
-    }
-  }
 }
diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/SuggestionInfo.java b/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/SuggestionInfo.java
new file mode 100644 (file)
index 0000000..c761d8d
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * 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.jetbrains.python.commandInterface.commandsWithArgs;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Information abouyt suggestion, provided by {@link com.jetbrains.python.commandInterface.commandsWithArgs.Strategy}
+ *
+ * @author Ilya.Kazakevich
+ */
+@SuppressWarnings("PackageVisibleField")
+// No do not need to hide field: everything is internal API in package, anyway
+final class SuggestionInfo {
+  /**
+   * Suggestions
+   */
+  private final List<String> mySuggestions = new ArrayList<String>();
+  /**
+   * Display them at absolute location or relative to last letter
+   */
+  final boolean myAbsolute;
+  /**
+   * Show then any time, or only when user requests them
+   */
+  final boolean myShowOnlyWhenRequested;
+
+  /**
+   * @param absolute              Display them at absolute location or relative to last letter
+   * @param showOnlyWhenRequested Show then any time, or only when user requests them
+   * @param suggestions           Suggestions
+   */
+  SuggestionInfo(final boolean absolute,
+                 final boolean showOnlyWhenRequested,
+                 @NotNull final List<String> suggestions) {
+    myAbsolute = absolute;
+    myShowOnlyWhenRequested = showOnlyWhenRequested;
+    mySuggestions.addAll(suggestions);
+  }
+
+  /**
+   * @return suggestions
+   */
+  @NotNull
+  List<String> getSuggestions() {
+    return Collections.unmodifiableList(mySuggestions);
+  }
+}
index 2db6a19bdcd30ccb442f5fed66e7cd72a4ab60c9..5ce549811ea48e86da4830de69d8ec393e953b5d 100644 (file)
 /**
  * Command-line like interface presenter that uses conception of command and its arguments.
  * See {@link com.jetbrains.python.commandInterface.commandsWithArgs.CommandInterfacePresenterCommandBased}
- * and its arguments: {@link com.jetbrains.python.commandInterface.commandsWithArgs.Argument}
+ * and its arguments: {@link com.jetbrains.python.commandInterface.commandsWithArgs.Argument}.
+ *
+ * It supports <a href="https://docs.python.org/2/library/optparse.html">optparse</a> terminology, so
+ * read it first and use {@link com.jetbrains.python.optParse} package
+ * @see com.jetbrains.python.optParse
  * @author Ilya.Kazakevich
  */
 package com.jetbrains.python.commandInterface.commandsWithArgs;
\ No newline at end of file
index 9715f387f7c157ae8224fa7f3b57c1d8dd2134b9..9eba919f58ae3aae4fb38ba61837afdf3771a378 100644 (file)
@@ -28,21 +28,27 @@ import com.jetbrains.python.commandInterface.CommandInterfacePresenter;
 import com.jetbrains.python.commandInterface.CommandInterfaceView;
 import com.jetbrains.python.suggestionList.SuggestionList;
 import com.jetbrains.python.suggestionList.SuggestionsBuilder;
+import com.jetbrains.python.optParse.WordWithPosition;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import javax.swing.*;
+import javax.swing.event.CaretEvent;
+import javax.swing.event.CaretListener;
 import javax.swing.event.DocumentEvent;
 import javax.swing.event.DocumentListener;
 import java.awt.*;
 import java.awt.event.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
 
 /**
  * Command-interface view implementation based on Swing
  *
  * @author Ilya.Kazakevich
  */
-public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements CommandInterfaceView, DocumentListener {
+public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements CommandInterfaceView, DocumentListener, CaretListener {
   /**
    * Pop-up we displayed in
    */
@@ -77,6 +83,8 @@ public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements Com
    * Flag that indicates we are in "test forced" mode: current text set by presenter, not by user
    */
   private boolean myInForcedTextMode;
+  @NotNull
+  private final List<WordWithPosition> myBalloons = new ArrayList<WordWithPosition>();
 
   /**
    * @param presenter       our presenter
@@ -118,7 +126,7 @@ public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements Com
   @Override
   public void show() {
     myMainTextField.getDocument().addDocumentListener(this);
-
+    myMainTextField.addCaretListener(this);
     myMainPopUp.addListener(this);
     if (myPlaceHolderText != null) {
       myMainTextField.setWaterMarkPlaceHolderText(myPlaceHolderText);
@@ -195,9 +203,26 @@ public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements Com
   }
 
   @Override
-  public void displayInfoBaloon(@NotNull final String message) {
-    final RelativePoint point = new RelativePoint(myMainTextField, new Point(myMainTextField.getTextEndPosition(), 0));
-    JBPopupFactory.getInstance().createBalloonBuilder(new JLabel(message)).createBalloon().show(point, Position.above);
+  public final void caretUpdate(final CaretEvent e) {
+    // When caret moved, we need to check if baloon has to be displayed
+    synchronized (myBalloons) {
+      for (final WordWithPosition balloon : myBalloons) {
+        final int position = myMainTextField.getCaretPosition();
+        if (position >= balloon.getFrom() && position <= balloon.getTo()) {
+          final RelativePoint point = new RelativePoint(myMainTextField, new Point(myMainTextField.getTextCursorPosition(), 0));
+          JBPopupFactory.getInstance().createBalloonBuilder(new JLabel(balloon.getText())).createBalloon()
+            .show(point, Position.above);
+        }
+      }
+    }
+  }
+
+  @Override
+  public void setBalloons(@NotNull final Collection<WordWithPosition> balloons) {
+    synchronized (myBalloons) {
+      myBalloons.clear();
+      myBalloons.addAll(balloons);
+    }
   }
 
   @Override
index f4b2aa67d9e84e601be7f0811a5ae603ee2e6895..8fec529e00c27e2074ad1c72e484e6f478345ea5 100644 (file)
@@ -70,6 +70,13 @@ public class SmartTextField extends JTextField {
     return (getText().length() + 1) * getColumnWidth();
   }
 
+  /**
+   * @return place (in chars) where caret.
+   */
+  int getTextCursorPosition() {
+    return (getCaretPosition() + 1) * getColumnWidth();
+  }
+
   void setWaterMarkPlaceHolderText(@NotNull final String watermark) {
     myPlaceHolder = new MyStatusText(this);
     myPlaceHolder.setText(watermark);
@@ -88,7 +95,7 @@ public class SmartTextField extends JTextField {
   /**
    * Display underline
    *
-   * @param color color to underline
+   * @param color    color to underline
    * @param lastOnly last letter only (whole line otherwise)
    */
   void underlineText(@NotNull final Color color, final boolean lastOnly) {
@@ -104,6 +111,7 @@ public class SmartTextField extends JTextField {
 
   /**
    * Sets appropriate width in chars
+   *
    * @param widthInChars num of chars
    */
   void setPreferredWidthInChars(final int widthInChars) {
@@ -112,6 +120,7 @@ public class SmartTextField extends JTextField {
 
   /**
    * Sets appropriate width in pixels
+   *
    * @param width width in px
    */
   void setPreferredWidthInPx(final int width) {
diff --git a/python/src/com/jetbrains/python/optParse/MalformedCommandLineException.java b/python/src/com/jetbrains/python/optParse/MalformedCommandLineException.java
new file mode 100644 (file)
index 0000000..3e21852
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * 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.jetbrains.python.optParse;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This exception is thrown when command line can't be parsed
+ *
+ * @author Ilya.Kazakevich
+ */
+public class MalformedCommandLineException extends Exception {
+  MalformedCommandLineException(@NotNull final String message) {
+    super(message);
+  }
+}
diff --git a/python/src/com/jetbrains/python/optParse/ParsedCommandLine.java b/python/src/com/jetbrains/python/optParse/ParsedCommandLine.java
new file mode 100644 (file)
index 0000000..f4cab57
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * 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.jetbrains.python.optParse;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+
+// TODO: Support options and their arguments
+
+/**
+ * <p>
+ * <a href="https://docs.python.org/2/library/optparse.html">Optparse</a>-based commandline parses.
+ * According to optparse manual, commandline should look like:
+ * <pre>command arg1 arg2 --long-bool-opt -s --another-opt opt_arg1 opt_arg2 --yet-another-opt=opt_arg3 arg4 </pre>.
+ * You should understand difference between argument, long option, short option, and option argument before using this class.
+ * It is documented in optparse manual.
+ * </p>
+ * <p>
+ * This class provides not only args and options (like many other parsers do), but also <strong>position in command line</strong>
+ * which may be useful when you want to mark argument somehow.
+ * </p>
+ *
+ * @author Ilya.Kazakevich
+ */
+public final class ParsedCommandLine {
+  @NotNull
+  private final WordWithPosition myCommand;
+  @NotNull
+  private final List<WordWithPosition> myArguments = new ArrayList<WordWithPosition>();
+
+  /**
+   * @param commandLine command line to parse
+   * @throws MalformedCommandLineException if malformed commandline can't be parsed
+   */
+  public ParsedCommandLine(@NotNull final String commandLine) throws MalformedCommandLineException {
+    final Deque<WordWithPosition> parts = new LinkedList<WordWithPosition>(WordWithPosition.splitText(commandLine));
+    if (parts.isEmpty()) {
+      throw new MalformedCommandLineException("No command provided");
+    }
+    myCommand = parts.pop();
+    if (myCommand.getText().startsWith("-")) {
+      throw new MalformedCommandLineException("Command can't start with option prefix");
+    }
+
+    // TODO: Support option arguments!!! Not only bool options exist! Check nargs!
+    for (final WordWithPosition part : parts) {
+      if (part.getText().startsWith("-")) {
+        // This is option!
+      }
+      else {
+        // TODO: Check optopn argument!
+        myArguments.add(part);
+      }
+    }
+  }
+
+
+  /**
+   * @return command (i.e. "startapp" in "startapp my_app")
+   */
+  @NotNull
+  public WordWithPosition getCommand() {
+    return myCommand;
+  }
+
+  /**
+   * @return all arguments (not options or option arguments!)
+   */
+  @NotNull
+  public List<WordWithPosition> getArguments() {
+    return Collections.unmodifiableList(myArguments);
+  }
+
+  /**
+   * @return all parts for command line as simple words
+   */
+  @NotNull
+  public List<String> getAsWords() {
+    final List<String> result = new ArrayList<String>();
+
+    result.add(myCommand.getText());
+
+    for (final WordWithPosition argument : myArguments) {
+      result.add(argument.getText());
+    }
+    // TODO: Add options as well
+    return result;
+  }
+
+  // TODO: Add options, arguments and option arguments
+}
diff --git a/python/src/com/jetbrains/python/optParse/WordWithPosition.java b/python/src/com/jetbrains/python/optParse/WordWithPosition.java
new file mode 100644 (file)
index 0000000..069ce71
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * 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.jetbrains.python.optParse;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Word with range position. To be used to mark some part of text or split it.
+ *
+ * @author Ilya.Kazakevich
+ */
+public final class WordWithPosition {
+  @NotNull
+  private final String myWord;
+  private final int myFrom;
+  private final int myTo;
+
+  /**
+   * @param word text
+   * @param from from where
+   * @param to   to where
+   */
+  public WordWithPosition(@NotNull final String word, final int from, final int to) {
+    myWord = word;
+    myFrom = from;
+    myTo = to;
+  }
+
+  @NotNull
+  public String getText() {
+    return myWord;
+  }
+
+  public int getFrom() {
+    return myFrom;
+  }
+
+  public int getTo() {
+    return myTo;
+  }
+
+  /**
+   * Returns texts only from collection of instances of this class
+   *
+   * @param words collection of instances of this class
+   * @return collection of strings (text)
+   */
+  @NotNull
+  public static Collection<String> fetchText(@NotNull final Collection<WordWithPosition> words) {
+    final Collection<String> result = new ArrayList<String>(words.size());
+    for (final WordWithPosition word : words) {
+      result.add(word.myWord);
+    }
+    return result;
+  }
+
+  /**
+   * Creates new instance with all fields copied but text
+   *
+   * @param newText new test to add
+   * @return new instance
+   */
+  @NotNull
+  public WordWithPosition copyWithDifferentText(@NotNull final String newText) {
+    return new WordWithPosition(newText, myFrom, myTo);
+  }
+
+  @Override
+  public String toString() {
+    return "WordWithPosition{" +
+           "myWord='" + myWord + '\'' +
+           ", myFrom=" + myFrom +
+           ", myTo=" + myTo +
+           '}';
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    WordWithPosition position = (WordWithPosition)o;
+
+    if (myFrom != position.myFrom) return false;
+    if (myTo != position.myTo) return false;
+    if (!myWord.equals(position.myWord)) return false;
+
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = myWord.hashCode();
+    result = 31 * result + myFrom;
+    result = 31 * result + myTo;
+    return result;
+  }
+
+
+  /**
+   * Tool that splits text into parts using one or more whitespace as delimiter.
+   * Each part contains text and boundaries (from and to)
+   *
+   * @param text text to split
+   * @return parse result
+   */
+  @NotNull
+  static List<WordWithPosition> splitText(@NotNull final String text) {
+    // TODO: Rewrite using regex or scanner?
+    int position = 0;
+    int wordStart = -1;
+    final List<WordWithPosition> parts = new ArrayList<WordWithPosition>();
+    for (final char c : text.toCharArray()) {
+      if (Character.isWhitespace(c) && wordStart != -1) {
+        // Close word
+        parts.add(new WordWithPosition(text.substring(wordStart, position), wordStart, position));
+        wordStart = -1;
+      }
+      else if (!Character.isWhitespace(c) && wordStart == -1) {
+        // Start word
+        wordStart = position;
+      }
+
+      position++;
+    }
+    if (wordStart != -1) {
+      // Adding last word
+      parts.add(new WordWithPosition(text.substring(wordStart), wordStart, position));
+    }
+    return parts;
+  }
+}
diff --git a/python/src/com/jetbrains/python/optParse/package-info.java b/python/src/com/jetbrains/python/optParse/package-info.java
new file mode 100644 (file)
index 0000000..3be946b
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * 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.
+ */
+
+/**
+ * <p>
+ * An <a href="https://docs.python.org/2/library/optparse.html">optparse</a> module twin, that parses command line.
+ * Unlike any other GNU/Posix parsers, it knows how to:</p>
+ * <ol>
+ *   <li>Parse options and args with out of any knowledge about required args</li>
+ *   <li>Provide actual places in command line where exactly such args or opts exist./li>
+ * </ol>
+ * <p>
+ * Be sure to read optparse manual
+ * (epecially <a href="https://docs.python.org/2/library/optparse.html#terminology">terminology</a>) part.
+ * </p>
+ * <p>Package entry point is {@link com.jetbrains.python.optParse.ParsedCommandLine}</p>
+ * @author Ilya.Kazakevich
+ */
+package com.jetbrains.python.optParse;
\ No newline at end of file