From a431dfe358478db74f1ac7ab5bc9c433882e8223 Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Tue, 10 Feb 2015 01:04:51 +0300 Subject: [PATCH] PY-11855 Run manage.py task improvements Refactored to new architecture (Presenter / ChunkDriver). --- .../com/jetbrains/python/PyBundle.properties | 13 +- .../{optParse => }/WordWithPosition.java | 14 +- .../CommandInterfacePresenter.java | 10 +- .../CommandInterfaceView.java | 96 +++--- .../ChunkAndInfo.java | 83 ++++++ .../ChunkDriver.java | 39 +++ .../ChunkDriverBasedPresenter.java | 247 +++++++++++++++ .../chunkDriverBasedPresenter/ChunkInfo.java | 81 +++++ .../chunkDriverBasedPresenter/ParseInfo.java | 88 ++++++ .../SuggestionInfo.java | 70 +++++ .../package-info.java | 30 ++ .../Argument.java | 2 +- .../ArgumentsInfo.java | 29 +- .../Command.java | 14 +- .../CommandBasedChunkDriver.java | 187 ++++++++++++ .../KnownArgumentsInfo.java | 49 ++- .../NoArgumentsInfo.java | 41 +++ .../UnknownArgumentsInfo.java | 16 +- .../commandBasedChunkDriver/package-info.java | 27 ++ .../ArgumentsValuesValidationInfo.java | 78 ----- .../commandsWithArgs/CommandAdapter.java | 67 ----- .../CommandExecutionInfo.java | 110 ------- ...CommandInterfacePresenterCommandBased.java | 247 --------------- .../commandsWithArgs/InCommandStrategy.java | 150 ---------- .../commandsWithArgs/NoArgumentsInfo.java | 62 ---- .../commandsWithArgs/NoCommandStrategy.java | 78 ----- .../commandsWithArgs/Strategy.java | 79 ----- .../commandsWithArgs/SuggestionInfo.java | 65 ---- .../commandsWithArgs/package-info.java | 42 --- .../python/commandInterface/package-info.java | 10 +- .../CommandInterfaceViewSwingImpl.java | 281 ++++++++++++------ .../swingView/CompletionKeyStrokeAction.java | 34 +++ .../swingView/ExecutionKeyStrokeAction.java | 34 +++ .../swingView/KeyStrokeAction.java | 72 +++++ .../swingView/KeyStrokeInfo.java | 99 ++++++ .../swingView/SmartTextField.java | 47 +-- .../swingView/SuggestionKeyStrokeAction.java | 34 +++ .../CommandLineParseResult.java | 78 +++++ .../commandLineParser/CommandLineParser.java | 38 +++ .../CommandLinePartType.java | 36 +++ .../MalformedCommandLineException.java | 2 +- .../OptParseCommandLineParser.java} | 68 ++--- .../commandLineParser/package-info.java | 30 ++ .../python/optParse/package-info.java | 32 -- .../python/suggestionList/SuggestionList.java | 9 +- 45 files changed, 1699 insertions(+), 1319 deletions(-) rename python/src/com/jetbrains/python/{optParse => }/WordWithPosition.java (91%) create mode 100644 python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkAndInfo.java create mode 100644 python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkDriver.java create mode 100644 python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkDriverBasedPresenter.java create mode 100644 python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkInfo.java create mode 100644 python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ParseInfo.java create mode 100644 python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/SuggestionInfo.java create mode 100644 python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/package-info.java rename python/src/com/jetbrains/python/commandInterface/{commandsWithArgs => commandBasedChunkDriver}/Argument.java (96%) rename python/src/com/jetbrains/python/commandInterface/{commandsWithArgs => commandBasedChunkDriver}/ArgumentsInfo.java (51%) rename python/src/com/jetbrains/python/commandInterface/{commandsWithArgs => commandBasedChunkDriver}/Command.java (69%) create mode 100644 python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/CommandBasedChunkDriver.java rename python/src/com/jetbrains/python/commandInterface/{commandsWithArgs => commandBasedChunkDriver}/KnownArgumentsInfo.java (73%) create mode 100644 python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/NoArgumentsInfo.java rename python/src/com/jetbrains/python/commandInterface/{commandsWithArgs => commandBasedChunkDriver}/UnknownArgumentsInfo.java (72%) create mode 100644 python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/package-info.java delete mode 100644 python/src/com/jetbrains/python/commandInterface/commandsWithArgs/ArgumentsValuesValidationInfo.java delete mode 100644 python/src/com/jetbrains/python/commandInterface/commandsWithArgs/CommandAdapter.java delete mode 100644 python/src/com/jetbrains/python/commandInterface/commandsWithArgs/CommandExecutionInfo.java delete mode 100644 python/src/com/jetbrains/python/commandInterface/commandsWithArgs/CommandInterfacePresenterCommandBased.java delete mode 100644 python/src/com/jetbrains/python/commandInterface/commandsWithArgs/InCommandStrategy.java delete mode 100644 python/src/com/jetbrains/python/commandInterface/commandsWithArgs/NoArgumentsInfo.java delete mode 100644 python/src/com/jetbrains/python/commandInterface/commandsWithArgs/NoCommandStrategy.java delete mode 100644 python/src/com/jetbrains/python/commandInterface/commandsWithArgs/Strategy.java delete mode 100644 python/src/com/jetbrains/python/commandInterface/commandsWithArgs/SuggestionInfo.java delete mode 100644 python/src/com/jetbrains/python/commandInterface/commandsWithArgs/package-info.java create mode 100644 python/src/com/jetbrains/python/commandInterface/swingView/CompletionKeyStrokeAction.java create mode 100644 python/src/com/jetbrains/python/commandInterface/swingView/ExecutionKeyStrokeAction.java create mode 100644 python/src/com/jetbrains/python/commandInterface/swingView/KeyStrokeAction.java create mode 100644 python/src/com/jetbrains/python/commandInterface/swingView/KeyStrokeInfo.java create mode 100644 python/src/com/jetbrains/python/commandInterface/swingView/SuggestionKeyStrokeAction.java create mode 100644 python/src/com/jetbrains/python/commandLineParser/CommandLineParseResult.java create mode 100644 python/src/com/jetbrains/python/commandLineParser/CommandLineParser.java create mode 100644 python/src/com/jetbrains/python/commandLineParser/CommandLinePartType.java rename python/src/com/jetbrains/python/{optParse => commandLineParser}/MalformedCommandLineException.java (94%) rename python/src/com/jetbrains/python/{optParse/ParsedCommandLine.java => commandLineParser/OptParseCommandLineParser.java} (56%) create mode 100644 python/src/com/jetbrains/python/commandLineParser/package-info.java delete mode 100644 python/src/com/jetbrains/python/optParse/package-info.java diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index 03014b095ff9..08238498648c 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -852,7 +852,14 @@ remote.interpreter.configure.temp.files.path.label=PyCharm helpers path: # Message we display for inspection if user uses custom class type members that do not exist custom.type.mimic.name=Dynamic class based on {0} + # Values for command argument value validation -commandsWithArgs.validation.badValue=Argument can't have this value -commandsWithArgs.validation.excess=Excess argument -commandsWithArgs.enterCommand.label=Enter command here \ No newline at end of file +commandLine.validation.badCommand=Unknown command +commandLine.validation.argMissing=Required argument value is missing +commandLine.validation.argBadValue=Argument can't have this value +commandLine.validation.excessArg=Excess argument value +# And for labels +commandLine.subText.key.complete=Use {0} to complete selected variant +commandLine.subText.key.suggestions=Use {0} to view available values +commandLine.subText.key.executeUnknown=Click {0} to execute +commandLine.subText.key.executeCommand=Click {0} to execute "{1}" diff --git a/python/src/com/jetbrains/python/optParse/WordWithPosition.java b/python/src/com/jetbrains/python/WordWithPosition.java similarity index 91% rename from python/src/com/jetbrains/python/optParse/WordWithPosition.java rename to python/src/com/jetbrains/python/WordWithPosition.java index cdfdb27e8ed9..35f10c3a2e81 100644 --- a/python/src/com/jetbrains/python/optParse/WordWithPosition.java +++ b/python/src/com/jetbrains/python/WordWithPosition.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.jetbrains.python.optParse; +package com.jetbrains.python; import com.intellij.util.Range; import org.jetbrains.annotations.NotNull; @@ -33,6 +33,7 @@ public final class WordWithPosition extends Range { /** * Creates word with beam (it has start, but it is infinite) + * * @param word word * @param from start */ @@ -50,6 +51,15 @@ public final class WordWithPosition extends Range { myWord = word; } + /** + * Creates instance with certain text and range (start/end) + * @param word text + * @param range range + */ + public WordWithPosition(@NotNull final String word, @NotNull final Range range) { + this(word, range.getFrom(), range.getTo()); + } + @NotNull public String getText() { return myWord; @@ -121,7 +131,7 @@ public final class WordWithPosition extends Range { * @return parse result */ @NotNull - static List splitText(@NotNull final String text) { + public static List splitText(@NotNull final String text) { // TODO: Rewrite using regex or scanner? int position = 0; int wordStart = -1; diff --git a/python/src/com/jetbrains/python/commandInterface/CommandInterfacePresenter.java b/python/src/com/jetbrains/python/commandInterface/CommandInterfacePresenter.java index 79eb731c4481..1eeecac550e5 100644 --- a/python/src/com/jetbrains/python/commandInterface/CommandInterfacePresenter.java +++ b/python/src/com/jetbrains/python/commandInterface/CommandInterfacePresenter.java @@ -27,9 +27,8 @@ public interface CommandInterfacePresenter extends Presenter { /** * Called by view when user types new text or text changed by some other reason * - * @param inForcedTextMode text changed not by user but by calling {@link com.jetbrains.python.commandInterface.CommandInterfaceView#forceText(String)} */ - void textChanged(boolean inForcedTextMode); + void textChanged(); /** * Called by view when user requests for completion (like tab) @@ -39,14 +38,13 @@ public interface CommandInterfacePresenter extends Presenter { void completionRequested(@Nullable String valueFromSuggestionList); /** - * Called by view when user asks for suggestions (CTRL+Space) + * Called by view when user asks for suggestions (like CTRL+Space) */ void suggestionRequested(); /** - * Called by view when user wants to execute command (Enter is presed) + * Called by view when user wants to execute command (like enter) * - * @param valueFromSuggestionList value selected from suggestion list (if any selected) */ - void executionRequested(@Nullable String valueFromSuggestionList); + void executionRequested(); } diff --git a/python/src/com/jetbrains/python/commandInterface/CommandInterfaceView.java b/python/src/com/jetbrains/python/commandInterface/CommandInterfaceView.java index 3377431712c3..317d4caeed49 100644 --- a/python/src/com/jetbrains/python/commandInterface/CommandInterfaceView.java +++ b/python/src/com/jetbrains/python/commandInterface/CommandInterfaceView.java @@ -15,7 +15,8 @@ */ package com.jetbrains.python.commandInterface; -import com.jetbrains.python.optParse.WordWithPosition; +import com.intellij.util.Range; +import com.jetbrains.python.WordWithPosition; import com.jetbrains.python.suggestionList.SuggestionsBuilder; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -30,6 +31,13 @@ import java.util.List; */ public interface CommandInterfaceView { + /** + * Special place in command line that represents "after the last character" place. + * To be used in methods like {@link #setInfoAndErrors(java.util.Collection, java.util.Collection)} to mark it. + */ + @NotNull + Range AFTER_LAST_CHARACTER_RANGE = new Range(Integer.MAX_VALUE, Integer.MAX_VALUE); + /** * Launches view */ @@ -39,43 +47,34 @@ public interface CommandInterfaceView { * Suggests user some elements (for completion reason) * * @param suggestions what to suggest (see {@link com.jetbrains.python.suggestionList.SuggestionsBuilder}) - * @param absolute display list in its main position, or directly near the text - * @param toSelect word to select if list (if any) + * @param absolute display list in its main position, or directly near the caret + * @param toSelect word to select in list (if any) */ void displaySuggestions(@NotNull SuggestionsBuilder suggestions, boolean absolute, @Nullable String toSelect); - /** - * Emphasize errors (like red line and special message). - * - * @param errors list of errors (coordinates and error message. Message may be empty not to display any text) - * @param specialErrorPlace if you want to underline special place, you may provide it here - */ - void showErrors(@NotNull final List errors, @Nullable SpecialErrorPlace specialErrorPlace); /** - * Change text to the one provided + * Each time caret meets certain place, view should check whether some subtext has to be displayed. + * There are 2 types of subtext to be displayed: + *
    + *
  1. Suggestion Text: View says something like "click FOO to see list of suggestions". Only presenter knows exact places where + * suggestions are available, so it should provide them
  2. + *
  3. Default text: In all other cases view displays default text (if available).
  4. + *
+ *

+ * Presenter provides view list of special places * - * @param newText text to display + * @param defaultSubText default text + * @param suggestionAvailablePlaces list of places where suggestions are available in format [from, to]. */ - void forceText(@NotNull String newText); - - /** - * Display text in sub part (like hint) - * - * @param subText text to display - */ - void setSubText(@NotNull String subText); + void configureSubTexts(@Nullable String defaultSubText, + @NotNull List> suggestionAvailablePlaces); /** * Hide suggestion list */ void removeSuggestions(); - /** - * Displays baloon with message right under the last letter. - * - * @param message text to display - */ /** * @return text, entered by user @@ -83,41 +82,40 @@ public interface CommandInterfaceView { @NotNull String getText(); + /** - * Enlarges view to make it as big as required to display appropriate number of chars + * When caret meets certain place, view may display some info and some errors. + * Errors, how ever, may always be emphasized (with something like red line). + * This function configures view with pack of ranges and texts to display. + * Special place {@link #AFTER_LAST_CHARACTER_RANGE} may also be used. + * Each place is described as start-end position (in chars) where it should be enabled. + * Each call removes previously enabled information. * - * @param widthInChars number of chars + * @param errors places to be marked as errors with error text. + * @param infoBalloons places to display info balloon + * @see #AFTER_LAST_CHARACTER_RANGE */ - void setPreferredWidthInChars(int widthInChars); + void setInfoAndErrors(@NotNull final Collection infoBalloons, @NotNull final Collection errors); + /** - * 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. - * Caution: Each call removes previuos balloons! + * Inserts text after caret moving next chars to the right * - * @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) + * @param text text to insert */ - void setBalloons(@NotNull final Collection balloons); + void insertTextAfterCaret(@NotNull String text); /** - * @return true if current caret position is on the word (no on whitespace) + * Replaces current text with another one. + * + * @param from from + * @param to to + * @param newText text to replace */ - boolean isCaretOnWord(); - + void replaceText(final int from, final int to, @NotNull String newText); /** - * Special place that may be underlined + * @return position of caret (in chars) */ - enum SpecialErrorPlace { - /** - * Whole text (from start to end) - */ - WHOLE_TEXT, - /** - * Only after last character - */ - AFTER_LAST_CHAR - } + int getCaretPosition(); } diff --git a/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkAndInfo.java b/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkAndInfo.java new file mode 100644 index 000000000000..92a6910fdd5a --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkAndInfo.java @@ -0,0 +1,83 @@ +/* + * 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.chunkDriverBasedPresenter; + +import com.jetbrains.python.WordWithPosition; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Chunk and its info pair. Chunk may be null, while chunk info always present. + * @author Ilya.Kazakevich + */ +final class ChunkAndInfo implements Comparable { + @Nullable + private final WordWithPosition myChunk; + @NotNull + private final ChunkInfo myChunkInfo; + + ChunkAndInfo(@Nullable final WordWithPosition chunk, @NotNull final ChunkInfo chunkInfo) { + myChunk = chunk; + myChunkInfo = chunkInfo; + } + + /** + * @return chunk (word). may be null + */ + @Nullable + WordWithPosition getChunk() { + return myChunk; + } + + /** + * @return chunk info. + */ + @NotNull + ChunkInfo getChunkInfo() { + return myChunkInfo; + } + + @Override + public int compareTo(@NotNull final ChunkAndInfo o) { + if (myChunk == null && o.myChunk == null) { + return 0; + } + if (myChunk == null) { + return 1; + } + if (o.myChunk == null) { + return -1; + } + return myChunk.getFrom().compareTo(o.myChunk.getFrom()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ChunkAndInfo info = (ChunkAndInfo)o; + + if (myChunk != null ? !myChunk.equals(info.myChunk) : info.myChunk != null) return false; + + return true; + } + + @Override + public int hashCode() { + return myChunk != null ? myChunk.hashCode() : 0; + } +} diff --git a/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkDriver.java b/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkDriver.java new file mode 100644 index 000000000000..12740a530052 --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkDriver.java @@ -0,0 +1,39 @@ +/* + * 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.chunkDriverBasedPresenter; + +import com.jetbrains.python.WordWithPosition; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * Driver that knows how to parse pack of chunks into chunk info. + * + * @author Ilya.Kazakevich + */ +public interface ChunkDriver { + /** + * Parses chunks into pack of chunks. There always should be chunk+1 chunkInfos (one for the tail like + * {@link com.jetbrains.python.commandInterface.CommandInterfaceView#AFTER_LAST_CHARACTER_RANGE}). + * So, at least one chunk info should also exist! + * + * @param chunks chunks (parts of command line) + * @return parse info with chunks info. Warning: do not return less chunk infos than chunks provided. That leads to runtime error + */ + @NotNull + ParseInfo parse(@NotNull List chunks); +} diff --git a/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkDriverBasedPresenter.java b/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkDriverBasedPresenter.java new file mode 100644 index 000000000000..946b2718d20c --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkDriverBasedPresenter.java @@ -0,0 +1,247 @@ +/* + * 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.chunkDriverBasedPresenter; + +import com.intellij.util.Range; +import com.jetbrains.python.WordWithPosition; +import com.jetbrains.python.commandInterface.CommandInterfacePresenterAdapter; +import com.jetbrains.python.commandInterface.CommandInterfaceView; +import com.jetbrains.python.suggestionList.SuggestionsBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +// TODO: Test + +/** + * Presenter that uses {@link com.jetbrains.python.commandInterface.chunkDriverBasedPresenter.ChunkDriver} to parse pack of chunks + * to obtain {@link com.jetbrains.python.commandInterface.chunkDriverBasedPresenter.ChunkInfo}. + * Each chunk may be paired with certain chunk info. Such info tells presenter whether this chunk has info, error, suggestions and so on. + * If caret situated far from chunks, then next neariest chunk should be found (see {@link #findNearestChunkAndInfo()}. + * + * + * @author Ilya.Kazakevich + * @see com.jetbrains.python.commandInterface.chunkDriverBasedPresenter.ChunkInfo + */ +public final class ChunkDriverBasedPresenter extends CommandInterfacePresenterAdapter { + + @NotNull + private final ChunkDriver myChunkDriver; + @NotNull + private final SortedSet myChunkAndInfos = new TreeSet(); + @Nullable + private Runnable myExecutor; + + public ChunkDriverBasedPresenter(@NotNull final CommandInterfaceView view, + @NotNull final ChunkDriver chunkDriver) { + super(view); + myChunkDriver = chunkDriver; + } + + @Override + public void launch() { + super.launch(); + reparseText(true); + } + + @Override + public void textChanged() { + reparseText(false); + } + + private void reparseText(final boolean skipSuggestions) { + final List chunks = WordWithPosition.splitText(myView.getText()); + final ParseInfo parseInfo = myChunkDriver.parse(chunks); + myExecutor = parseInfo.getExecutor(); + final List chunkInfos = parseInfo.getChunkInfo(); + assert chunkInfos.size() >= chunks.size() : "Driver did not return enough chunks"; + assert !chunkInfos.isEmpty() : "At least one chunk info should exist"; + myChunkAndInfos.clear(); + for (int i = 0; i < chunkInfos.size(); i++) { + final ChunkInfo chunkInfo = chunkInfos.get(i); + final WordWithPosition chunk = chunks.size() > i ? chunks.get(i) : null; + myChunkAndInfos.add(new ChunkAndInfo(chunk, chunkInfo)); + } + + + // configure Errors And Balloons + + final Collection infoBalloons = new ArrayList(); + final Collection errorBalloons = new ArrayList(); + + + for (final ChunkAndInfo chunkInfoPair : myChunkAndInfos) { + final ChunkInfo chunkInfo = chunkInfoPair.getChunkInfo(); + Range chunk = chunkInfoPair.getChunk(); + if (chunk == null) { + // After the last! + chunk = CommandInterfaceView.AFTER_LAST_CHARACTER_RANGE; + } + final String error = chunkInfo.getError(); + if (error != null) { + errorBalloons.add(new WordWithPosition(error, chunk)); + } + final String info = chunkInfo.getInfoBalloon(); + if (info != null) { + infoBalloons.add(new WordWithPosition(info, chunk)); + } + } + + myView.setInfoAndErrors(infoBalloons, errorBalloons); + + + if (!skipSuggestions) { + configureSuggestion(false); + } + + // Configure subtexts + final List> placesWhereSuggestionAvailable = new ArrayList>(); + for (final ChunkAndInfo chunkAndInfo : myChunkAndInfos) { + Range chunk = chunkAndInfo.getChunk(); + // If some place has suggestions, then add it + if (chunkAndInfo.getChunkInfo().getSuggestions() != null) { + if (chunk == null) { + // If there is no such chunk, that means we are after the last character, so use "special case" here + //noinspection ReuseOfLocalVariable + chunk = CommandInterfaceView.AFTER_LAST_CHARACTER_RANGE; + } + placesWhereSuggestionAvailable.add(chunk); + } + } + myView.configureSubTexts(parseInfo.getStatusText(), placesWhereSuggestionAvailable); + } + + @Override + public void suggestionRequested() { + configureSuggestion(true); // Show or hide + } + + + /** + * Displays suggestions if needed. + * + * @param requestedExplicitly is suggesions where requested by user explicitly or not + */ + private void configureSuggestion(final boolean requestedExplicitly) { + myView.removeSuggestions(); + final ChunkAndInfo chunkAndInfo = findNearestChunkAndInfo(); + final ChunkInfo chunkInfo = chunkAndInfo.getChunkInfo(); + final WordWithPosition chunk = chunkAndInfo.getChunk(); + + final SuggestionInfo suggestionInfo = chunkInfo.getSuggestions(); + if (suggestionInfo == null || (!suggestionInfo.isShowSuggestionsAutomatically() && !requestedExplicitly)) { + return; + } + final List suggestions = new ArrayList(suggestionInfo.getSuggestions()); + if (chunk != null && !requestedExplicitly) { + filterLeaveOnlyMatching(suggestions, chunk.getText()); + } + // TODO: Place to add history + if (!suggestions.isEmpty()) { + // No need to display empty suggestions + myView + .displaySuggestions(new SuggestionsBuilder(suggestions), suggestionInfo.isShowAbsolute(), (chunk == null ? null : chunk.getText())); + } + } + + + /** + * Filters collection of suggestions leaving only those starts with certain text. + * @param suggestions list to filter + * @param textToMatch leave only parts that start with this param + */ + private static void filterLeaveOnlyMatching(@NotNull final Iterable suggestions, @NotNull final String textToMatch) { + // TODO: use guava instead? + final Iterator iterator = suggestions.iterator(); + while (iterator.hasNext()) { + if (!iterator.next().startsWith(textToMatch)) { + iterator.remove(); + } + } + } + + /** + * Searches for the nearest chunk and info to use. It may or may not find chunk, but it should always provide some chunk info. + * + * @return nearest chunk info and, probably, chunk. + */ + @NotNull + private ChunkAndInfo findNearestChunkAndInfo() { + final int caretPosition = myView.getCaretPosition(); + + for (final ChunkAndInfo chunkAndInfo : myChunkAndInfos) { + final Range range = chunkAndInfo.getChunk(); + if (range != null && range.isWithin(caretPosition)) { + return chunkAndInfo; + } + if (range != null && range.getFrom() > caretPosition) { + return new ChunkAndInfo(null, chunkAndInfo.getChunkInfo()); + } + } + + return new ChunkAndInfo(null, myChunkAndInfos.last().getChunkInfo()); + } + + + @Override + public void completionRequested(@Nullable final String valueFromSuggestionList) { + final ChunkAndInfo chunkAndInfo = findNearestChunkAndInfo(); + final WordWithPosition chunk = chunkAndInfo.getChunk(); + + if (valueFromSuggestionList != null) { + // Just insert it + if (chunk != null) { // If caret is on the chunk itself + myView.replaceText(chunk.getFrom(), chunk.getTo(), valueFromSuggestionList); + } + else { + myView.insertTextAfterCaret(valueFromSuggestionList); + } + return; + } + + //User did not provide text no insert, do our best to find one + + final ChunkInfo chunkInfo = chunkAndInfo.getChunkInfo(); + final SuggestionInfo suggestionInfo = chunkInfo.getSuggestions(); + if (suggestionInfo == null) { + return; // No suggestion available for this chunk + } + final List suggestions = new ArrayList(suggestionInfo.getSuggestions()); + if (chunk != null) { + filterLeaveOnlyMatching(suggestions, chunk.getText()); + } + if (suggestions.size() == 1) { + // Exclusive! + if (chunk != null) { + myView.replaceText(chunk.getFrom(), chunk.getTo(), suggestions.get(0)); + } + else { + myView.insertTextAfterCaret(suggestions.get(0)); + } + } + } + + @Override + public void executionRequested() { + if (myExecutor == null) { + // TODO: Display error somehow + } + else { + myExecutor.run(); + } + } +} diff --git a/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkInfo.java b/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkInfo.java new file mode 100644 index 000000000000..4397751ed011 --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ChunkInfo.java @@ -0,0 +1,81 @@ +/* + * 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.chunkDriverBasedPresenter; + +import org.jetbrains.annotations.Nullable; + +/** + * Information about certain place in text, provided by chunk driver. + * + * @author Ilya.Kazakevich + */ +public final class ChunkInfo { + @Nullable + private final String myInfoBalloon; + @Nullable + private final String myError; + @Nullable + private final SuggestionInfo mySuggestions; + + + public ChunkInfo(@Nullable final String infoBalloon, + @Nullable final String error) { + this(infoBalloon, error, null); + } + + + /** + * + * @param infoBalloon Info balloon to display when caret meets this place (null if display nothing) + * @param error Error balloon to display when caret meets this place and underline text as error (null if no error) + * @param suggestions list of suggestions available in this place (if any) + */ + public ChunkInfo(@Nullable final String infoBalloon, + @Nullable final String error, + @Nullable final SuggestionInfo suggestions) { + myInfoBalloon = infoBalloon; + myError = error; + mySuggestions = suggestions; + } + + + /** + * + * @return Info balloon to display when caret meets this place (null if display nothing) + */ + @Nullable + public String getInfoBalloon() { + return myInfoBalloon; + } + + /** + * + * @return Error balloon to display when caret meets this place and underline text as error (null if no error) + */ + @Nullable + public String getError() { + return myError; + } + + /** + * + * @return list of suggestions available in this place (if any) + */ + @Nullable + public SuggestionInfo getSuggestions() { + return mySuggestions; + } +} diff --git a/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ParseInfo.java b/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ParseInfo.java new file mode 100644 index 000000000000..005c4dae6150 --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/ParseInfo.java @@ -0,0 +1,88 @@ +/* + * 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.chunkDriverBasedPresenter; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Pack of {@link com.jetbrains.python.commandInterface.chunkDriverBasedPresenter.ChunkInfo} for certain chunks and other parsing info. + * + * @author Ilya.Kazakevich + */ +public final class ParseInfo { + @Nullable + private final String myStatusText; + @NotNull + private final List myChunkInfo = new ArrayList(); + @Nullable + private final Runnable myExecutor; + + /** + * @param chunkInfo Chunk info should match chunks in 1-to-1 manner: + * If "chunk1 chunk2 chunk3" were provided, you then need to return list where first chunkInfo matches first chunk ets. + * And there also should be one more chunkInfo for tail. + * @param statusText Status text {@link com.jetbrains.python.commandInterface.CommandInterfaceView view} may display. + * @param executor Engine to process command-line execution + */ + public ParseInfo(@NotNull final Collection chunkInfo, + @Nullable final String statusText, + @Nullable final Runnable executor) { + myStatusText = statusText; + myChunkInfo.addAll(chunkInfo); + myExecutor = executor; + } + + /** + * Simple parse info with out of status text and executor + * + * @param chunkInfo Chunk info (See {@link #ParseInfo(java.util.Collection, String, Runnable)} + * @see #ParseInfo(java.util.Collection, String, Runnable) + */ + public ParseInfo(@NotNull final Collection chunkInfo) { + this(chunkInfo, null, null); + } + + /** + * @return Status text {@link com.jetbrains.python.commandInterface.CommandInterfaceView view} may display. + */ + @Nullable + String getStatusText() { + return myStatusText; + } + + /** + * @return Engine to process command-line execution + */ + @Nullable + Runnable getExecutor() { + return myExecutor; + } + + /** + * @return Chunk info should match chunks in 1-to-1 manner, and there also should be one more chunkInfo for tail ( + * see {@link #ParseInfo(java.util.Collection, String, Runnable) ctor} manual) + */ + @NotNull + List getChunkInfo() { + return Collections.unmodifiableList(myChunkInfo); + } +} diff --git a/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/SuggestionInfo.java b/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/SuggestionInfo.java new file mode 100644 index 000000000000..59bea9abd12f --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/SuggestionInfo.java @@ -0,0 +1,70 @@ +/* + * 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.chunkDriverBasedPresenter; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Information about suggestions + * + * @author Ilya.Kazakevich + */ +public final class SuggestionInfo { + @NotNull + private final List mySuggestions = new ArrayList(); + private final boolean myShowSuggestionsAutomatically; + private final boolean myShowAbsolute; + + /** + * @param showSuggestionsAutomatically true it suggestions should be displayed even if user did not ask for that + * @param showAbsolute show suggestions at the absolute position (not relative to caret). + * @param suggestions List of suggestions to display + */ + public SuggestionInfo(final boolean showSuggestionsAutomatically, + final boolean showAbsolute, + @NotNull final Collection suggestions) { + myShowSuggestionsAutomatically = showSuggestionsAutomatically; + myShowAbsolute = showAbsolute; + mySuggestions.addAll(suggestions); + } + + /** + * @return List of suggestions to display + */ + @NotNull + public List getSuggestions() { + return Collections.unmodifiableList(mySuggestions); + } + + /** + * @return true it suggestions should be displayed even if user did not ask for that + */ + public boolean isShowSuggestionsAutomatically() { + return myShowSuggestionsAutomatically; + } + + /** + * @return show suggestions at the absolute position (not relative to caret). + */ + public boolean isShowAbsolute() { + return myShowAbsolute; + } +} diff --git a/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/package-info.java b/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/package-info.java new file mode 100644 index 000000000000..9281bc7f630c --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/chunkDriverBasedPresenter/package-info.java @@ -0,0 +1,30 @@ +/* + * 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. + */ + +/** + * {@link com.jetbrains.python.commandInterface.CommandInterfacePresenter} implementation based on ideas of chunk + * and {@link com.jetbrains.python.commandInterface.chunkDriverBasedPresenter.ChunkDriver}. + * This presenter explodes command line into several parts or chunks. + * Chunks then passed to {@link com.jetbrains.python.commandInterface.chunkDriverBasedPresenter.ChunkDriver driver} and it returns + * all information it has about each chunk. Presenter uses this information to display chunks correctly using view. + * To use this package, {@link com.jetbrains.python.commandInterface.chunkDriverBasedPresenter.ChunkDriver} should be implemented. + * + * See {@link com.jetbrains.python.commandInterface.chunkDriverBasedPresenter.ChunkDriverBasedPresenter} as entry point + * + * + * @author Ilya.Kazakevich + */ +package com.jetbrains.python.commandInterface.chunkDriverBasedPresenter; \ No newline at end of file diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/Argument.java b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/Argument.java similarity index 96% rename from python/src/com/jetbrains/python/commandInterface/commandsWithArgs/Argument.java rename to python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/Argument.java index 5826a45a4dd4..fc3f64192c17 100644 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/Argument.java +++ b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/Argument.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.jetbrains.python.commandInterface.commandsWithArgs; +package com.jetbrains.python.commandInterface.commandBasedChunkDriver; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/ArgumentsInfo.java b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/ArgumentsInfo.java similarity index 51% rename from python/src/com/jetbrains/python/commandInterface/commandsWithArgs/ArgumentsInfo.java rename to python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/ArgumentsInfo.java index 594b7ac37a70..ad43da45adcd 100644 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/ArgumentsInfo.java +++ b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/ArgumentsInfo.java @@ -13,39 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.jetbrains.python.commandInterface.commandsWithArgs; +package com.jetbrains.python.commandInterface.commandBasedChunkDriver; -import org.jetbrains.annotations.NotNull; +import com.intellij.openapi.util.Pair; import org.jetbrains.annotations.Nullable; -import java.util.List; - /** - * Information about command {@link com.jetbrains.python.commandInterface.commandsWithArgs.Argument arguments} and their value + * Information about command {@link Argument arguments} and their value * validation. - * Check optparse manual, package info and {@link com.jetbrains.python.commandInterface.commandsWithArgs.Argument} + * Check optparse manual, package info and {@link Argument} * manual for more info about arguments. * * @author Ilya.Kazakevich */ public interface ArgumentsInfo { /** - * Returns argument by its position. + * Returns argument by its position. It also returns hint whether argument is required or not. * * @param argumentPosition argument position - * @return null if no argument value is available at this position. Returns argument otherwise. + * @return null if no argument value is available at this position. + * Returns argument otherwise. Boolean here should tell you if argument is required (command is invalid with out of it) or optional ( + * it is acceptible, but command can work with our of it) */ @Nullable - Argument getArgument(int argumentPosition); - - - /** - * Validates argument values. - * Values should be provided as list. I.e. for

my_command foo bar
there should be list of "foo, bar". - * - * @param argumentValuesToCheck values to check - * @return validation result - */ - @NotNull - ArgumentsValuesValidationInfo validateArgumentValues(@NotNull final List argumentValuesToCheck); + Pair getArgument(int argumentPosition); } diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/Command.java b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/Command.java similarity index 69% rename from python/src/com/jetbrains/python/commandInterface/commandsWithArgs/Command.java rename to python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/Command.java index fb139df02ca2..694021964fea 100644 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/Command.java +++ b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/Command.java @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.jetbrains.python.commandInterface.commandsWithArgs; +package com.jetbrains.python.commandInterface.commandBasedChunkDriver; +import com.intellij.openapi.module.Module; +import com.jetbrains.python.commandLineParser.CommandLineParseResult; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -40,8 +42,16 @@ public interface Command { /** - * @return Information about command positional, unnamed {@link com.jetbrains.python.commandInterface.commandsWithArgs.Argument arguments} (not options!) + * @return Information about command positional, unnamed {@link Argument arguments} (not options!) */ @NotNull ArgumentsInfo getArgumentsInfo(); + + /** + * Execute command + * + * @param module module to execute command against + * @param commandLine command's command line + */ + void execute(@NotNull final Module module, @NotNull final CommandLineParseResult commandLine); } diff --git a/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/CommandBasedChunkDriver.java b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/CommandBasedChunkDriver.java new file mode 100644 index 000000000000..02fd1d3194c0 --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/CommandBasedChunkDriver.java @@ -0,0 +1,187 @@ +/* + * 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.commandBasedChunkDriver; + +import com.intellij.openapi.module.Module; +import com.intellij.openapi.util.Pair; +import com.jetbrains.python.PyBundle; +import com.jetbrains.python.WordWithPosition; +import com.jetbrains.python.commandInterface.chunkDriverBasedPresenter.ChunkDriver; +import com.jetbrains.python.commandInterface.chunkDriverBasedPresenter.ChunkInfo; +import com.jetbrains.python.commandInterface.chunkDriverBasedPresenter.ParseInfo; +import com.jetbrains.python.commandInterface.chunkDriverBasedPresenter.SuggestionInfo; +import com.jetbrains.python.commandLineParser.CommandLineParseResult; +import com.jetbrains.python.commandLineParser.CommandLineParser; +import com.jetbrains.python.commandLineParser.CommandLinePartType; +import com.jetbrains.python.commandLineParser.MalformedCommandLineException; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +/** + * Chunk driver that uses pack of commands. + * + * @author Ilya.Kazakevich + */ +public final class CommandBasedChunkDriver implements ChunkDriver { + @NotNull + private final CommandLineParser myCommandLineParser; + @NotNull + private final Map myCommands = new TreeMap(); // To sort commands by name + @NotNull + private final Module myModule; + + /** + * @param commandLineParser parser to use + * @param module module parsing takes place in + * @param commands available commands + */ + public CommandBasedChunkDriver(@NotNull final CommandLineParser commandLineParser, + @NotNull final Module module, + @NotNull final Collection commands) { + myCommandLineParser = commandLineParser; + for (final Command command : commands) { + myCommands.put(command.getName(), command); + } + myModule = module; + } + + @Override + @NotNull + public ParseInfo parse(@NotNull final List chunks) { + // TODO: Refactor to add command first to prevent copy/paste + if (chunks.isEmpty()) { + return createBadCommandInfo(chunks.size()); + } + + try { + final CommandLineParseResult commandLine = myCommandLineParser.parse(chunks); + final Command command = myCommands.get(commandLine.getCommand().getText()); + if (command == null) { + // Bad command inserted + return createBadCommandInfo(chunks.size()); + } + + // Command exists, lets check its arguments + + // TODO: Support options as well + + // First, validate values + final ArgumentsInfo commandArgumentsInfo = command.getArgumentsInfo(); + + + final List chunkInfo = new ArrayList(); + // First chunk iscommand and it seems to be ok + chunkInfo.add(new ChunkInfo(null, null, new SuggestionInfo(false, true, myCommands.keySet()))); + + + // Now add balloons, info and suggestions + for (int i = 0; i < commandLine.getParts().size(); i++) { + final Pair argumentPair = commandArgumentsInfo.getArgument(i); + if (argumentPair == null) { // Excess argument! + chunkInfo.add(new ChunkInfo(null, PyBundle.message("commandLine.validation.excessArg"))); + continue; + } + final Argument argument = argumentPair.getSecond(); + final List availableValues = argument.getAvailableValues(); + final Pair part = commandLine.getParts().get(i); + if (part.first != CommandLinePartType.ARGUMENT) { + // Only arguments are supported now, so we have nothing to say about this chunk + chunkInfo.add(new ChunkInfo(null, null)); + } + final String argumentValue = part.second.getText(); + String errorMessage = null; + if (availableValues != null && !availableValues.contains(argumentValue)) { + // Bad value + errorMessage = PyBundle.message("commandLine.validation.argBadValue"); + } + // Argument seems to be ok. We suggest values automatically only if value is bad + chunkInfo.add(new ChunkInfo(argument.getHelpText(), errorMessage, + (availableValues != null ? new SuggestionInfo(errorMessage != null, false, availableValues) : null))); + } + + + final Pair nextArgumentPair = commandArgumentsInfo.getArgument(commandLine.getParts().size()); + if (nextArgumentPair != null) { + // Next arg exists + final Argument nextArgument = nextArgumentPair.getSecond(); + final List availableValues = nextArgument.getAvailableValues(); + // Only add error if required + final String error = nextArgumentPair.first ? PyBundle.message("commandLine.validation.argMissing") : null; + final ChunkInfo lastArgInfo = + new ChunkInfo(nextArgument.getHelpText(), error, + (availableValues != null ? new SuggestionInfo(false, false, availableValues) : null)); + chunkInfo.add(lastArgInfo); + } + else { + // Looks like all arguments are satisfied. Adding empty chunk to prevent completion etc. + // This is a hack, but with out of it last chunkinfo will always be used, even 200 chars after last place + chunkInfo.add(new ChunkInfo(null, null)); + } + + assert chunkInfo.size() >= chunks.size() : "Contract broken: not enough chunks"; + + return new ParseInfo(chunkInfo, command.getHelp(), new MyExecutor(command, commandLine)); + } + catch (final MalformedCommandLineException ignored) { + // Junk enetered! + return createBadCommandInfo(chunks.size()); + } + } + + + /** + * Creates parse info signaling command is bad or junk + * + * @param numberOfChunks number of chunks provided by user (we must return chunk info for each chunk + 1, accroding to contract) + * @return parse info to return + */ + @NotNull + private ParseInfo createBadCommandInfo(final int numberOfChunks) { + final List result = new ArrayList(); + // We know that first chunk command line, but we can't say anything about outher chunks except they are bad. + // How ever, we must say something according to contract (number of infos should be equal or greater than number of chunks) + result + .add(new ChunkInfo(null, PyBundle.message("commandLine.validation.badCommand"), new SuggestionInfo(true, true, myCommands.keySet()))); + for (int i = 1; i < numberOfChunks; i++) { + result.add( + new ChunkInfo(null, PyBundle.message("commandLine.validation.badCommand"))); + } + + return new ParseInfo(result); + } + + + /** + * Adapter that executes command using {@link Command#execute(com.intellij.openapi.module.Module, com.jetbrains.python.commandLineParser.CommandLineParseResult)} + */ + private class MyExecutor implements Runnable { + @NotNull + private final Command myCommand; + @NotNull + private final CommandLineParseResult myCommandLine; + + MyExecutor(@NotNull final Command command, @NotNull final CommandLineParseResult line) { + myCommand = command; + myCommandLine = line; + } + + @Override + public void run() { + myCommand.execute(myModule, myCommandLine); + } + } +} diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/KnownArgumentsInfo.java b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/KnownArgumentsInfo.java similarity index 73% rename from python/src/com/jetbrains/python/commandInterface/commandsWithArgs/KnownArgumentsInfo.java rename to python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/KnownArgumentsInfo.java index 2e8762d10001..4c3a50295f78 100644 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/KnownArgumentsInfo.java +++ b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/KnownArgumentsInfo.java @@ -13,18 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.jetbrains.python.commandInterface.commandsWithArgs; +package com.jetbrains.python.commandInterface.commandBasedChunkDriver; import com.google.common.base.Preconditions; -import com.intellij.util.containers.hash.HashMap; -import com.jetbrains.python.commandInterface.commandsWithArgs.ArgumentsValuesValidationInfo.ArgumentValueError; +import com.intellij.openapi.util.Pair; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Map; /** * In some special cases we have insight about command arguments. @@ -61,6 +59,15 @@ public final class KnownArgumentsInfo implements ArgumentsInfo { */ private final int myMaxArguments; + /** + * For command with fixed number of arguments. In this case all arguments are fixed and required! + * + * @param arguments arguments this command have + */ + public KnownArgumentsInfo(@NotNull final Collection arguments) { + this(arguments, arguments.size(), arguments.size()); + } + /** * For commands with infinite number of values last argument accepts (my_command VAL1 VAL2 .. VALN) * @@ -91,36 +98,20 @@ public final class KnownArgumentsInfo implements ArgumentsInfo { @Nullable @Override - public Argument getArgument(final int argumentPosition) { + public Pair getArgument(final int argumentPosition) { + if (argumentPosition >= myMaxArguments) { + return null; + } + final boolean optional = argumentPosition >= myMinArguments; + if (myArguments.size() > argumentPosition) { - return myArguments.get(argumentPosition); + return Pair.create(!optional, myArguments.get(argumentPosition)); } // We may need last one - if (argumentPosition <= myMaxArguments) { - return myArguments.get(myArguments.size() - 1); + if (argumentPosition < myMaxArguments) { + return Pair.create(false, myArguments.get(myArguments.size() - 1)); } return null; } - - @NotNull - @Override - public ArgumentsValuesValidationInfo validateArgumentValues(@NotNull final List argumentValuesToCheck) { - final Map errors = new HashMap(); - - for (int i = 0; i < argumentValuesToCheck.size(); i++) { - final String userValue = argumentValuesToCheck.get(i); - final Argument argument = getArgument(i); - if (argument == null) { - errors.put(i, ArgumentValueError.EXCESS); - continue; - } - final List availableValues = argument.getAvailableValues(); - if (availableValues != null && !availableValues.contains(userValue)) { - errors.put(i, ArgumentValueError.BAD_VALUE); - } - } - - return new ArgumentsValuesValidationInfo(errors, argumentValuesToCheck.size() < myMinArguments); - } } diff --git a/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/NoArgumentsInfo.java b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/NoArgumentsInfo.java new file mode 100644 index 000000000000..9aaa0d5503b2 --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/NoArgumentsInfo.java @@ -0,0 +1,41 @@ +/* + * 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.commandBasedChunkDriver; + +import com.intellij.openapi.util.Pair; +import org.jetbrains.annotations.Nullable; + +/** + * TODO: Redundant?! + * Case when command has no arguments (for sure!) + * + * @author Ilya.Kazakevich + */ +public final class NoArgumentsInfo implements ArgumentsInfo { + /** + * Instance to use when command has no arguments + */ + public static final ArgumentsInfo INSTANCE = new NoArgumentsInfo(); + + private NoArgumentsInfo() { + } + + @Nullable + @Override + public Pair getArgument(final int argumentPosition) { + return null; + } +} diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/UnknownArgumentsInfo.java b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/UnknownArgumentsInfo.java similarity index 72% rename from python/src/com/jetbrains/python/commandInterface/commandsWithArgs/UnknownArgumentsInfo.java rename to python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/UnknownArgumentsInfo.java index 3199c3431d15..04889e4109c9 100644 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/UnknownArgumentsInfo.java +++ b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/UnknownArgumentsInfo.java @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.jetbrains.python.commandInterface.commandsWithArgs; +package com.jetbrains.python.commandInterface.commandBasedChunkDriver; +import com.intellij.openapi.util.Pair; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; - /** * For many commands we know nothing about arguments but their help text. * This strategy is for this case @@ -40,15 +39,10 @@ public final class UnknownArgumentsInfo implements ArgumentsInfo { myHelp = allArgumentsHelpText; } + @Nullable @Override - public Argument getArgument(final int argumentPosition) { - return new Argument(myHelp); // We can't say argument does not exist. - } - - @NotNull - @Override - public ArgumentsValuesValidationInfo validateArgumentValues(@NotNull final List argumentValuesToCheck) { - return ArgumentsValuesValidationInfo.NO_ERROR; // Actually, we have no idea + public Pair getArgument(final int argumentPosition) { + return Pair.create(false, new Argument(myHelp)); } } diff --git a/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/package-info.java b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/package-info.java new file mode 100644 index 000000000000..cc72ae21e259 --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/commandBasedChunkDriver/package-info.java @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * {@link com.jetbrains.python.commandInterface.chunkDriverBasedPresenter.ChunkDriver} implementation based on idea of + * {@link com.jetbrains.python.commandInterface.commandBasedChunkDriver.Command command} and its {@link com.jetbrains.python.commandInterface.commandBasedChunkDriver.Argument arguments}. + * + * See {@link com.jetbrains.python.commandInterface.commandBasedChunkDriver.CommandBasedChunkDriver} as entry point. + * It parses command line using {@link com.jetbrains.python.commandLineParser.CommandLineParser} and finds matching command and arguments + * provided by user + * + * @author Ilya.Kazakevich + */ +package com.jetbrains.python.commandInterface.commandBasedChunkDriver; \ No newline at end of file diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/ArgumentsValuesValidationInfo.java b/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/ArgumentsValuesValidationInfo.java deleted file mode 100644 index 239074741896..000000000000 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/ArgumentsValuesValidationInfo.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 com.intellij.util.containers.hash.HashMap; -import org.jetbrains.annotations.NotNull; - -import java.util.Collections; -import java.util.Map; - -/** - * Information about {@link com.jetbrains.python.commandInterface.commandsWithArgs.Argument arguments} values validation - * - * @author Ilya.Kazakevich - */ -public final class ArgumentsValuesValidationInfo { - /** - * Validation with out of any error - */ - @NotNull - static final ArgumentsValuesValidationInfo - NO_ERROR = new ArgumentsValuesValidationInfo(Collections.emptyMap(), false); - - private final Map myPositionOfErrorArguments = new HashMap(); - private final boolean myNotEnoughArguments; - - /** - * @param positionOfErrorArguments map of [argument_position, its_value_error] - * @param notEnoughArguments true if not enough arguments values provided (i.e. some required arg missed) - */ - ArgumentsValuesValidationInfo(@NotNull final Map positionOfErrorArguments, - final boolean notEnoughArguments) { - myPositionOfErrorArguments.putAll(positionOfErrorArguments); - myNotEnoughArguments = notEnoughArguments; - } - - /** - * @return map of [argument_position, its_value_error] - */ - @NotNull - Map getPositionOfErrorArguments() { - return Collections.unmodifiableMap(myPositionOfErrorArguments); - } - - /** - * @return if not enough argument values provided (i.e. some required arg missed) - */ - boolean isNotEnoughArguments() { - return myNotEnoughArguments; - } - - /** - * Type of argument value error. - */ - enum ArgumentValueError { - /** - * This argument is redundant - */ - EXCESS, - /** - * Argument has bad value - */ - BAD_VALUE - } -} diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/CommandAdapter.java b/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/CommandAdapter.java deleted file mode 100644 index 51d0a42942f0..000000000000 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/CommandAdapter.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 org.jetbrains.annotations.Nullable; - -/** - * Simple command implementation - * - * @author Ilya.Kazakevich - */ -public class CommandAdapter implements Command { - @NotNull - private final String myName; - @Nullable - private final String myHelp; - @NotNull - private final ArgumentsInfo myArgumentsInfo; - - /** - * @param help help text - * @param name command name - * @param argumentsInfo arguments info - */ - public CommandAdapter(@NotNull final String name, @Nullable final String help, @NotNull ArgumentsInfo argumentsInfo) { - myName = name; - myHelp = help; - myArgumentsInfo = argumentsInfo; - } - - /** - * @return command name - */ - @Override - @NotNull - public final String getName() { - return myName; - } - - - @Override - @Nullable - public final String getHelp() { - return myHelp; - } - - - @NotNull - @Override - public final ArgumentsInfo getArgumentsInfo() { - return myArgumentsInfo; - } -} diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/CommandExecutionInfo.java b/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/CommandExecutionInfo.java deleted file mode 100644 index b7e533b7da67..000000000000 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/CommandExecutionInfo.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2000-2014 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 com.intellij.openapi.util.text.StringUtil; -import com.intellij.util.ArrayUtil; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Arrays; -import java.util.List; - -/** - * Information about command and its arguments to save in history / pass to execution etc. - * - * @author Ilya.Kazakevich - */ -public class CommandExecutionInfo { - /** - * Command and arguments separator. - */ - private static final String SEPARATOR = " "; - @NotNull - private final String myCommandName; - @NotNull - private final String[] myArguments; - - /** - * @param commandName command - * @param arguments its arguments - */ - public CommandExecutionInfo(@NotNull final String commandName, @NotNull final String... arguments) { - myCommandName = commandName; - myArguments = arguments.clone(); - } - - /** - * @return command - */ - @NotNull - public String getCommandName() { - return myCommandName; - } - - /** - * @return command arguments - */ - @NotNull - public String[] getArguments() { - return myArguments.clone(); - } - - /** - * @return command in format "command arg1 arg2". Opposite to {@link #fromString(String)} - * @see #fromString(String) - */ - @NotNull - public String toString() { - // TODO: What if command or argument has space in it? Escape somehow! - return StringUtil.join(ArrayUtil.mergeArrays(new String[]{myCommandName}, myArguments), SEPARATOR); - } - - /** - * @param stringToUnserialize string created by {@link #toString()} - * @return command parsed from string - * @see #toString() - */ - @Nullable - public static CommandExecutionInfo fromString(@NotNull final String stringToUnserialize) { - // TODO: What if command or argument has space in it? Escape somehow! - final List strings = StringUtil.split(stringToUnserialize, SEPARATOR); - if (strings.isEmpty()) { - return null; - } - return new CommandExecutionInfo(strings.get(0), ArrayUtil.toStringArray(strings.subList(1, strings.size()))); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof CommandExecutionInfo)) return false; - - CommandExecutionInfo info = (CommandExecutionInfo)o; - - if (!Arrays.equals(myArguments, info.myArguments)) return false; - if (!myCommandName.equals(info.myCommandName)) return false; - - return true; - } - - @Override - public int hashCode() { - int result = myCommandName.hashCode(); - result = 31 * result + Arrays.hashCode(myArguments); - return result; - } -} diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/CommandInterfacePresenterCommandBased.java b/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/CommandInterfacePresenterCommandBased.java deleted file mode 100644 index 0090a9e13d65..000000000000 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/CommandInterfacePresenterCommandBased.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright 2000-2014 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 com.intellij.openapi.util.Pair; -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.CommandInterfaceView.SpecialErrorPlace; -import com.jetbrains.python.optParse.MalformedCommandLineException; -import com.jetbrains.python.optParse.ParsedCommandLine; -import com.jetbrains.python.optParse.WordWithPosition; -import com.jetbrains.python.suggestionList.SuggestionsBuilder; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; - -/** - * Command-line interface presenter that is command-based - * - * @param Command type - * @author Ilya.Kazakevich - */ -public class CommandInterfacePresenterCommandBased extends CommandInterfacePresenterAdapter { - /** - * [name] -> command. Linked is used to preserve order. - */ - private final Map myCommands = new LinkedHashMap(); - /** - * currenly used strategy (see interface for more info) - */ - private Strategy myStrategy; - - - /** - * @param view view - * @param commands available commands - */ - public CommandInterfacePresenterCommandBased(@NotNull final CommandInterfaceView view, - @NotNull final Iterable commands) { - super(view); - for (final C command : commands) { - myCommands.put(command.getName(), command); - } - } - - /** - * @param view view - * @param commands available commands - */ - public CommandInterfacePresenterCommandBased(@NotNull final CommandInterfaceView view, - @NotNull final C... commands) { - this(view, Arrays.asList(commands)); - } - - @Override - public void launch() { - /*myView.setPreferredWidthInChars(getMaximumCommandWithArgsLength());*/ - super.launch(); - myStrategy = new NoCommandStrategy(this); - } - - @Override - public void textChanged(final boolean inForcedTextMode) { - configureStrategy(); - myView.setSubText(myStrategy.getSubText()); - final Pair> errorInfo = myStrategy.getErrorInfo(); - myView.showErrors(errorInfo.getSecond(), errorInfo.first); - myView.setBalloons(myStrategy.getBalloonsToShow()); - - final SuggestionInfo suggestionInfo = myStrategy.getSuggestionInfo(); - final List suggestions = new ArrayList(suggestionInfo.getSuggestions()); - - final String lastPart = getLastPart(); - if ((lastPart != null) && myStrategy.isUnknownTextExists()) { - //Filter to starts from - final Iterator iterator = suggestions.iterator(); - while (iterator.hasNext()) { - final String textToCheck = iterator.next(); - - if (!textToCheck.startsWith(lastPart)) { - iterator.remove(); - } - } - } - - if (!suggestionInfo.myShowOnlyWhenRequested && !suggestions.isEmpty()) { - final SuggestionsBuilder suggestionsBuilder = getBuilderWithHistory(); - suggestionsBuilder.add(suggestions); - - myView - .displaySuggestions(suggestionsBuilder, suggestionInfo.myAbsolute, null); - } - else { - myView.removeSuggestions(); - } - } - - /** - * @return builder that already has history in its prefix group (see {@link com.jetbrains.python.suggestionList.SuggestionsBuilder}) - */ - @NotNull - private SuggestionsBuilder getBuilderWithHistory() { - return new SuggestionsBuilder(); - - // TODO: Uncomment when history would be fixed - /*final SuggestionsBuilder suggestionsBuilder = new SuggestionsBuilder(); - final List history = getHistory(); - final Collection historyCommands = new LinkedHashSet(); - for (final CommandExecutionInfo info : history) { - historyCommands.add(info.toString()); - } - - if (!historyCommands.isEmpty()) { - // TODO: Later implement folding by name - suggestionsBuilder.changeGroup(false); - suggestionsBuilder - .add(ArrayUtil.toStringArray(historyCommands)); - suggestionsBuilder.changeGroup(true); - } - - return suggestionsBuilder;*/ - } - - /** - * @return execution info from history. It is empty by default, child should implement it. - */ - @NotNull - protected List getHistory() { - return Collections.emptyList(); - } - - /** - * @return command that entered in box, or null of just entered - */ - @Nullable - protected CommandExecutionInfo getCommandToExecute() { - return myStrategy.getCommandToExecute(); - } - - /** - * Finds and sets appropriate strategy - */ - private void configureStrategy() { - final ParsedCommandLine line = getParsedCommandLine(); - if (line != null) { - final Command command = myCommands.get(line.getCommand().getText()); - if (command != null) { - myStrategy = new InCommandStrategy(command, line, this); - return; - } - } - myStrategy = new NoCommandStrategy(this); // No command or bad command found - } - - @Override - public void completionRequested(@Nullable final String valueFromSuggestionList) { - if (valueFromSuggestionList != null) { - final SuggestionInfo suggestionInfo = myStrategy.getSuggestionInfo(); - if (suggestionInfo.getSuggestions().contains(valueFromSuggestionList)) { - final ParsedCommandLine commandLine = getParsedCommandLine(); - final List words = commandLine != null ? commandLine.getAsWords() : new ArrayList(); - if (!words.isEmpty() && myView.isCaretOnWord()) { - words.remove(words.size() - 1); - } - words.add(valueFromSuggestionList); - myView.forceText(StringUtil.join(words, " ")); - } - } - myView.removeSuggestions(); - } - - @Override - public void suggestionRequested() { - final SuggestionInfo suggestionInfo = myStrategy.getSuggestionInfo(); - final List suggestions = suggestionInfo.getSuggestions(); - if (!suggestions.isEmpty()) { - final SuggestionsBuilder suggestionsBuilder = getBuilderWithHistory(); - suggestionsBuilder.add(suggestions); - myView.displaySuggestions(suggestionsBuilder, suggestionInfo.myAbsolute, null); - } - } - - @Override - public void executionRequested(@Nullable final String valueFromSuggestionList) { - - } - - /** - * @return [command_name => command] all available commands - */ - @NotNull - protected final Map getCommands() { - return Collections.unmodifiableMap(myCommands); - } - - /** - * @return parsed commandline entered by user - */ - @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 - final String getLastPart() { - final ParsedCommandLine commandLine = getParsedCommandLine(); - if (commandLine == null || commandLine.getAsWords().isEmpty()) { - return null; - } - final List words = commandLine.getAsWords(); - return words.get(words.size() - 1); - } - - /** - * @return view - */ - @NotNull - CommandInterfaceView getView() { - return myView; - } - -} diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/InCommandStrategy.java b/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/InCommandStrategy.java deleted file mode 100644 index 1d3be479937a..000000000000 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/InCommandStrategy.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2000-2014 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 com.intellij.openapi.util.Pair; -import com.intellij.util.ArrayUtil; -import com.jetbrains.python.PyBundle; -import com.jetbrains.python.commandInterface.CommandInterfaceView; -import com.jetbrains.python.commandInterface.CommandInterfaceView.SpecialErrorPlace; -import com.jetbrains.python.commandInterface.commandsWithArgs.ArgumentsValuesValidationInfo.ArgumentValueError; -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.Collections; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -/** - * Strategy implementation for case when user entered command - * - * @author Ilya.Kazakevich - */ -final class InCommandStrategy extends Strategy { - @NotNull - private final List myArguments = new ArrayList(); - @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 ParsedCommandLine commandLine, - @NotNull final CommandInterfacePresenterCommandBased presenter) { - super(presenter); - myArguments.addAll(WordWithPosition.fetchText(commandLine.getArguments())); - myCommand = command; - myCommandLine = commandLine; - } - - @NotNull - @Override - public String getSubText() { - final String help = myCommand.getHelp(); - if (help != null) { - return help; - } - return "Place to display help"; - } - - @NotNull - @Override - SuggestionInfo getSuggestionInfo() { - final Argument nextArgument = myCommand.getArgumentsInfo().getArgument(myCommandLine.getArguments().size()); - if (nextArgument != null) {// TODO: Check options! - // If next arg exists - final List availableValues = nextArgument.getAvailableValues(); - if (availableValues != null) { // If has available values - return new SuggestionInfo(false, false, availableValues); - } - } - return new SuggestionInfo(false, false, Collections.emptyList()); - } - - @NotNull - @Override - List getBalloonsToShow() { - // Display argument balloons right from command end to last argument end - final ArgumentsInfo argumentsInfo = myCommand.getArgumentsInfo(); - final List arguments = myCommandLine.getArguments(); - if (arguments.isEmpty()) { - // If no arguments provided, then display first argument popup right after command - final Argument firstArgument = argumentsInfo.getArgument(0); - if (firstArgument == null) { - return Collections.emptyList(); // Looks like no argument required - } - return Collections - .singletonList(new WordWithPosition(firstArgument.getHelpText(), myCommandLine.getCommand().getTo() + 1)); - } - - final List result = new ArrayList(arguments.size()); - for (int i = 0; i < arguments.size(); i++) { - final WordWithPosition argEnteredByUser = arguments.get(i); - final Argument argument = argumentsInfo.getArgument(i); - if (argument != null) { - // Display argument help - result.add(argEnteredByUser.copyWithDifferentText(argument.getHelpText())); - } - } - return result; - } - - @Override - boolean isUnknownTextExists() { - if (myCommandLine.getAsWords().isEmpty()) { - return false; // Command only - } - final String lastPart = myPresenter.getLastPart(); - return ((lastPart != null) && !getSuggestionInfo().getSuggestions().contains(lastPart)); - } - - @Nullable - @Override - CommandExecutionInfo getCommandToExecute() { - return new CommandExecutionInfo(myCommand.getName(), ArrayUtil.toStringArray(myArguments)); - } - - @NotNull - @Override - Pair> getErrorInfo() { - final List userProvidedValues = myCommandLine.getArguments(); - SpecialErrorPlace specialError = null; - final List errors = new ArrayList(); - - final ArgumentsValuesValidationInfo validation = - myCommand.getArgumentsInfo().validateArgumentValues(WordWithPosition.fetchText(userProvidedValues)); - if (validation.isNotEnoughArguments()) { - specialError = SpecialErrorPlace.AFTER_LAST_CHAR; - } - for (final Entry errorEntry : validation.getPositionOfErrorArguments().entrySet()) { - final String errorText = (errorEntry.getValue() == ArgumentValueError.BAD_VALUE ? - PyBundle.message("commandsWithArgs.validation.badValue") : - PyBundle.message("commandsWithArgs.validation.excess") ); - errors.add(userProvidedValues.get(errorEntry.getKey()).copyWithDifferentText(errorText)); - } - - - return Pair.create(specialError, errors); - } -} diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/NoArgumentsInfo.java b/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/NoArgumentsInfo.java deleted file mode 100644 index d08e8ccb16f8..000000000000 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/NoArgumentsInfo.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 com.google.common.collect.ContiguousSet; -import com.google.common.collect.DiscreteDomain; -import com.google.common.collect.Range; -import com.intellij.util.containers.hash.HashMap; -import com.jetbrains.python.commandInterface.commandsWithArgs.ArgumentsValuesValidationInfo.ArgumentValueError; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.Map; - -/** - * Case when command has no arguments (for sure!) - * - * @author Ilya.Kazakevich - */ -public final class NoArgumentsInfo implements ArgumentsInfo { - /** - * Instance to use when command has no arguments - */ - public static final ArgumentsInfo INSTANCE = new NoArgumentsInfo(); - - private NoArgumentsInfo() { - } - - @Nullable - @Override - public Argument getArgument(final int argumentPosition) { - return null; - } - - @NotNull - @Override - public ArgumentsValuesValidationInfo validateArgumentValues(@NotNull final List argumentValuesToCheck) { - if (argumentValuesToCheck.isEmpty()) { - return ArgumentsValuesValidationInfo.NO_ERROR; - } - final Map errors = - new HashMap(); - for (final int errorPosition : ContiguousSet.create(Range.closedOpen(0, argumentValuesToCheck.size()), DiscreteDomain.integers())) { - errors.put(errorPosition, ArgumentValueError.EXCESS); - } - return new ArgumentsValuesValidationInfo(errors, false); - } -} diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/NoCommandStrategy.java b/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/NoCommandStrategy.java deleted file mode 100644 index 16ea46fe9fc6..000000000000 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/NoCommandStrategy.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2000-2014 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 com.intellij.openapi.util.Pair; -import com.jetbrains.python.PyBundle; -import com.jetbrains.python.commandInterface.CommandInterfaceView.SpecialErrorPlace; -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; - -/** - * Strategy implementation for case when no command parsed - * - * @author Ilya.Kazakevich - */ -final class NoCommandStrategy extends Strategy { - - private static final Pair> - NO_ERROR = Pair.create(null, Collections.emptyList()); - - NoCommandStrategy(@NotNull final CommandInterfacePresenterCommandBased presenter) { - super(presenter); - } - - @NotNull - @Override - String getSubText() { - return PyBundle.message("commandsWithArgs.enterCommand.label"); - } - - @NotNull - @Override - SuggestionInfo getSuggestionInfo() { - return new SuggestionInfo(true, isTextBoxEmpty(), new ArrayList(myPresenter.getCommands().keySet())); - } - - @Override - boolean isUnknownTextExists() { - return !myPresenter.getView().getText().isEmpty(); - } - - - @NotNull - @Override - Pair> getErrorInfo() { - // No error if textbox empty, but mark everything as error if some text entered: it is junk (it can't be command, - // InCommand strategy were selected otherwise) - return isTextBoxEmpty() ? NO_ERROR : Pair.create(SpecialErrorPlace.WHOLE_TEXT, Collections.emptyList()); - } - - private boolean isTextBoxEmpty() { - return myPresenter.getView().getText().isEmpty(); - } - - @Nullable - @Override - CommandExecutionInfo getCommandToExecute() { - return null; - } -} diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/Strategy.java b/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/Strategy.java deleted file mode 100644 index 231cf0207c17..000000000000 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/Strategy.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2000-2014 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 com.intellij.openapi.util.Pair; -import com.jetbrains.python.commandInterface.CommandInterfaceView.SpecialErrorPlace; -import com.jetbrains.python.optParse.WordWithPosition; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Collections; -import java.util.List; - -/** - * Strategy that helps {@link com.jetbrains.python.commandInterface.commandsWithArgs.CommandInterfacePresenterCommandBased} - * to deal with 2 states: when no text entered (or some junk enetered) or command name entered - * - * @author Ilya.Kazakevich - */ -abstract class Strategy { - @NotNull - protected final CommandInterfacePresenterCommandBased myPresenter; - - /** - * @param presenter presenter - */ - protected Strategy(@NotNull final CommandInterfacePresenterCommandBased presenter) { - myPresenter = presenter; - } - - /** - * @return sub text to display - */ - @NotNull - abstract String getSubText(); - - /** - * @return suggestions - */ - @NotNull - abstract SuggestionInfo getSuggestionInfo(); - - // TODO: Merge baloon and error (actually the same) - @NotNull - List getBalloonsToShow() { - return Collections.emptyList(); - } - - /** - * @return command that entered in box, or null of just entered - */ - @Nullable - abstract CommandExecutionInfo getCommandToExecute(); - - - /** - * @return errors - */ - @NotNull - abstract Pair> getErrorInfo(); - - /** - * @return if text entered by user contains some unknown commands - */ - abstract boolean isUnknownTextExists(); -} diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/SuggestionInfo.java b/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/SuggestionInfo.java deleted file mode 100644 index c761d8d34619..000000000000 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/SuggestionInfo.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 mySuggestions = new ArrayList(); - /** - * 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 suggestions) { - myAbsolute = absolute; - myShowOnlyWhenRequested = showOnlyWhenRequested; - mySuggestions.addAll(suggestions); - } - - /** - * @return suggestions - */ - @NotNull - List getSuggestions() { - return Collections.unmodifiableList(mySuggestions); - } -} diff --git a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/package-info.java b/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/package-info.java deleted file mode 100644 index 2198b00dea60..000000000000 --- a/python/src/com/jetbrains/python/commandInterface/commandsWithArgs/package-info.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2000-2014 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. - */ - -/** - *

Optparse-based commandline interface presenter

- *

- * 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}. - * - * It supports optparse terminology, so - * read it first and use {@link com.jetbrains.python.optParse} package - *

- *

Arguments and validation

- *

- * Optparse arguments are positional and unnamed. - * Each {@link com.jetbrains.python.commandInterface.commandsWithArgs.Command command} provides - * {@link com.jetbrains.python.commandInterface.commandsWithArgs.ArgumentsInfo arguments info}. - * It can be used to obtain information about argument (like list of possible values) and it also used to validate argument values, - * provided by user. In most cases we have no idea about arguments: due to optparse limitations only help test is available. - * But sometimes we do know (like when args are documented). - * Different strategies exist, so be sure to check {@link com.jetbrains.python.commandInterface.commandsWithArgs.ArgumentsInfo} children - *

- * - * - * @see com.jetbrains.python.optParse - * @author Ilya.Kazakevich - */ -package com.jetbrains.python.commandInterface.commandsWithArgs; \ No newline at end of file diff --git a/python/src/com/jetbrains/python/commandInterface/package-info.java b/python/src/com/jetbrains/python/commandInterface/package-info.java index 19c0b13ac979..7f1581af3bc7 100644 --- a/python/src/com/jetbrains/python/commandInterface/package-info.java +++ b/python/src/com/jetbrains/python/commandInterface/package-info.java @@ -19,7 +19,7 @@ * It has several abilities, including (but not limited): *
    *
  1. Suggestion box
  2. - *
  3. Error marking
  4. + *
  5. Error emphasising
  6. *
  7. Popups
  8. *
  9. AutoCompletion
  10. *
@@ -31,9 +31,15 @@ * *

* There is also swing-based view implementation in {@link com.jetbrains.python.commandInterface.swingView} - * and presenter implementation based on idea of commands with arguments. See {@link com.jetbrains.python.commandInterface.commandsWithArgs} + * and presenter implementation based on idea of commands with arguments. See {@link com.jetbrains.python.commandInterface.chunkDriverBasedPresenter} *

* + *

+ * Presenter and View talk to each other only in term of text and chars. Presenter knows nothing about pixels and should never + * assume view have certain pixel size. + *

+ * + * * * @author Ilya.Kazakevich */ diff --git a/python/src/com/jetbrains/python/commandInterface/swingView/CommandInterfaceViewSwingImpl.java b/python/src/com/jetbrains/python/commandInterface/swingView/CommandInterfaceViewSwingImpl.java index 3071a8dc8665..955d07ee6580 100644 --- a/python/src/com/jetbrains/python/commandInterface/swingView/CommandInterfaceViewSwingImpl.java +++ b/python/src/com/jetbrains/python/commandInterface/swingView/CommandInterfaceViewSwingImpl.java @@ -17,16 +17,18 @@ package com.jetbrains.python.commandInterface.swingView; import com.google.common.base.Preconditions; import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx; -import com.intellij.openapi.fileEditor.impl.EditorWindow; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.ui.popup.*; import com.intellij.openapi.ui.popup.Balloon.Position; import com.intellij.ui.JBColor; import com.intellij.ui.awt.RelativePoint; +import com.intellij.util.Range; +import com.intellij.util.containers.HashSet; +import com.jetbrains.python.PyBundle; +import com.jetbrains.python.WordWithPosition; import com.jetbrains.python.commandInterface.CommandInterfacePresenter; import com.jetbrains.python.commandInterface.CommandInterfaceView; -import com.jetbrains.python.optParse.WordWithPosition; import com.jetbrains.python.suggestionList.SuggestionList; import com.jetbrains.python.suggestionList.SuggestionsBuilder; import org.jetbrains.annotations.NotNull; @@ -37,19 +39,31 @@ import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; import java.awt.*; -import java.awt.event.*; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Set; /** - * Command-interface view implementation based on Swing + * Command-interface view implementation based on Swing. + * It uses balloons to display errors and infos, drop-down for suggestions and also underlines errors * * @author Ilya.Kazakevich */ public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements CommandInterfaceView, DocumentListener, CaretListener { private static final JBColor ERROR_COLOR = JBColor.RED; + + /** + * We need to track balloons, so we have field with callback + */ + @NotNull + private final BalloonManager myBalloonManager = new BalloonManager(); /** * Pop-up we displayed in */ @@ -68,27 +82,38 @@ public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements Com * Lower (sub) label */ private JLabel mySubLabel; + /** + * "Suggestion area". Suggestion status is displayed when caret meets this area + */ + private final List> myPlacesWhereSuggestionsAvailable = new ArrayList>(); @NotNull private final CommandInterfacePresenter myPresenter; /** * List to display suggestions */ @NotNull - private final SuggestionList myList; + private final SuggestionList mySuggestionList; /** * Displayed when there is no text */ @Nullable private final String myPlaceHolderText; + /** - * Flag that indicates we are in "test forced" mode: current text set by presenter, not by user + * Information balloons that should be displayed when caret meets their boundaries. */ - private boolean myInForcedTextMode; - // TODO: Doc - @NotNull - private final List myBalloons = new ArrayList(); @NotNull + private final List myInfoBalloons = new ArrayList(); + /** + * Error balloons that should be displayed when caret meets their boundaries. + * Errors are always underlined, but balloons are displayed only if caret meets error + */ private final List myErrorBalloons = new ArrayList(); + /** + * Default subtext to display when caret is out of {@link #myPlacesWhereSuggestionsAvailable "suggestion" area} + */ + @Nullable + private String myDefaultSubText; /** * @param presenter our presenter @@ -112,18 +137,11 @@ public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements Com myMainTextField.setFocusable(true); - final EditorWindow window = FileEditorManagerEx.getInstanceEx(project).getCurrentWindow(); - final int windowSize; - if (window != null) { - windowSize = window.getSize().width; - } - else { - windowSize = 0; // Windows size is unknown - } + final int windowWidth = FileEditorManagerEx.getInstanceEx(project).getComponent().getRootPane().getWidth() - 10; // Little gap myMainTextField - .setPreferredWidthInPx(windowSize); - myList = new SuggestionList(new MySuggestionListListener()); + .setPreferredWidthInPx(windowWidth); + mySuggestionList = new SuggestionList(new MySuggestionListListener()); } @@ -145,7 +163,12 @@ public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements Com } }); myMainTextField.setFocusTraversalKeysEnabled(false); - myMainTextField.addKeyListener(new MyKeyListener()); + myMainTextField.addKeyListener(new MyKeyListener()); // Up/down arrows are not handles with actions + + // Register all available actions + for (final KeyStrokeInfo strokeInfo : KeyStrokeInfo.values()) { + strokeInfo.register(myPresenter, mySuggestionList, myMainTextField); + } myMainPopUp.showInFocusCenter(); } @@ -154,32 +177,18 @@ public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements Com public void displaySuggestions(@NotNull final SuggestionsBuilder suggestions, final boolean absolute, @Nullable final String toSelect) { int left = 0; - // Display text right after line ends if not in "absolute" mode + // Display text right after caret if (!absolute) { - left = myMainTextField.getTextEndPosition(); + left = myMainTextField.getTextCaretPositionInPx(); } - myList.showSuggestions(suggestions, new RelativePoint(myPanel, new Point(left, myPanel.getHeight())), toSelect); + mySuggestionList.showSuggestions(suggestions, new RelativePoint(myPanel, new Point(left, myPanel.getHeight())), toSelect); + configureAppropriateStatus(); } - @Override public void onClosed(final LightweightWindowEvent event) { super.onClosed(event); - myList.close(); - } - - @Override - public final void showErrors(@NotNull final List errors, @Nullable final SpecialErrorPlace specialErrorPlace) { - for (final WordWithPosition error : errors) { - myMainTextField.underlineText(ERROR_COLOR, error.getFrom(), error.getTo()); - } - if (specialErrorPlace != null) { - myMainTextField.underlineText(specialErrorPlace, ERROR_COLOR); - } - synchronized (myErrorBalloons) { - myErrorBalloons.clear(); - myErrorBalloons.addAll(errors); - } + mySuggestionList.close(); } @@ -188,6 +197,7 @@ public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements Com processDocumentChange(); } + @Override public void removeUpdate(final DocumentEvent e) { processDocumentChange(); @@ -195,7 +205,7 @@ public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements Com private void processDocumentChange() { myMainTextField.hideUnderline(); - myPresenter.textChanged(myInForcedTextMode); + myPresenter.textChanged(); } @Override @@ -204,75 +214,155 @@ public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements Com } @Override - public void forceText(@NotNull final String newText) { - myInForcedTextMode = true; - myMainTextField.setText(newText); - myInForcedTextMode = false; + public void removeSuggestions() { + mySuggestionList.close(); + configureAppropriateStatus(); } - @Override - public void removeSuggestions() { - myList.close(); - } @Override public final void caretUpdate(final CaretEvent e) { - // TODO: Stupid copy/paste, fix by method extract // When caret moved, we need to check if balloon has to be displayed - synchronized (myErrorBalloons) { - showBaloons(myErrorBalloons, Position.below, MessageType.ERROR); + displayBalloonsIfRequired(); + configureAppropriateStatus(); + } + + private void configureAppropriateStatus() { + if (!mySuggestionList.isClosed()) { + // Tell user she may use TAB to complete + mySubLabel.setText(PyBundle.message("commandLine.subText.key.complete", KeyStrokeInfo.COMPLETION.getText())); + return; } - synchronized (myBalloons) { - showBaloons(myBalloons, Position.above, MessageType.INFO); + + // If we are in "suggestion available" place -- tell it + for (final Range range : myPlacesWhereSuggestionsAvailable) { + final boolean specialCaseAfterLastChar = isAfterLastCharRange(range) && getCaretPosition() == myMainTextField.getText().length(); + if (range.isWithin(getCaretPosition()) || specialCaseAfterLastChar) { + mySubLabel.setText(PyBundle.message("commandLine.subText.key.suggestions", KeyStrokeInfo.SUGGESTION.getText())); + return; + } + } + + // We may simply tell user she may execute command + if (myDefaultSubText != null) { + mySubLabel.setText(PyBundle.message("commandLine.subText.key.executeCommand", KeyStrokeInfo.EXECUTION.getText(), myDefaultSubText)); + } + else { + mySubLabel.setText(PyBundle.message("commandLine.subText.key.executeUnknown", KeyStrokeInfo.EXECUTION.getText())); } } - // TODO: Doc - private void showBaloons(@NotNull final List balloons, - @NotNull final Position popUpPosition, - @NotNull final MessageType messageType) { + + private void displayBalloonsIfRequired() { + synchronized (myErrorBalloons) { + if (mySuggestionList.isClosed()) { // No need to display error popups when suggestion list is displayed. It intersects. + showBalloons(myErrorBalloons, Position.below, MessageType.ERROR); + } + } + synchronized (myInfoBalloons) { + showBalloons(myInfoBalloons, Position.above, MessageType.INFO); + } + } + + /** + * Displays some balloons + * + * @param balloons balloons to display + * @param popUpPosition where ti display them. Only {@link Position#above} and {@link Position#below} are supported! + * @param messageType may be {@link MessageType#ERROR} or {@link MessageType#INFO} for example + */ + private void showBalloons(@NotNull final List balloons, + @NotNull final Position popUpPosition, + @NotNull final MessageType messageType) { Preconditions.checkArgument(popUpPosition == Position.above || popUpPosition == Position.below, "Only above or below is supported"); for (final WordWithPosition balloon : balloons) { if (balloon.getText().isEmpty()) { continue; // Can't be displayed if empty } final int caretPosition = myMainTextField.getCaretPosition(); - if (caretPosition >= balloon.getFrom() && caretPosition <= balloon.getTo()) { + if ((caretPosition >= balloon.getFrom() && caretPosition <= balloon.getTo())) { final int top = (popUpPosition == Position.above ? 0 : myMainTextField.getHeight() * 2); // Display below a little bit lower - final RelativePoint point = new RelativePoint(myMainTextField, new Point(myMainTextField.getTextCursorPosition(), top)); + final RelativePoint point = new RelativePoint(myMainTextField, new Point(myMainTextField.getTextCaretPositionInPx(), top)); final Balloon balloonToShow = JBPopupFactory.getInstance().createBalloonBuilder(new JLabel(balloon.getText())).setFillColor(messageType.getPopupBackground()) .createBalloon(); balloonToShow.setAnimationEnabled(false); + myBalloonManager.registerBalloon(balloonToShow); balloonToShow.show(point, popUpPosition); } } } @Override - public final boolean isCaretOnWord() { - final int caretPosition = myMainTextField.getCaretPosition(); - if (caretPosition == 0) { - return false; // At the beginning of the line + public final void setInfoAndErrors(@NotNull final Collection infoBalloons, + @NotNull final Collection errors) { + synchronized (myInfoBalloons) { + myInfoBalloons.clear(); + myInfoBalloons.addAll(infoBalloons); } - return !Character.isWhitespace(myMainTextField.getText().toCharArray()[caretPosition - 1]); + synchronized (myErrorBalloons) { + myErrorBalloons.clear(); + myErrorBalloons.addAll(errors); + } + for (final WordWithPosition error : errors) { + if (isAfterLastCharRange(error)) { + // In "special" case we use last char + myMainTextField.underlineText(ERROR_COLOR, myMainTextField.getText().length(), myMainTextField.getText().length() + 1); + } + else { + myMainTextField.underlineText(ERROR_COLOR, error.getFrom(), error.getTo()); + } + } + } + + /** + * Checks if some range is special case {@link #AFTER_LAST_CHARACTER_RANGE}. + * + * @param range range to check + * @return true if special case + */ + private static boolean isAfterLastCharRange(@NotNull final Range range) { + return AFTER_LAST_CHARACTER_RANGE.getFrom().equals(range.getFrom()) && AFTER_LAST_CHARACTER_RANGE.getTo().equals(range.getTo()); } @Override - public void setBalloons(@NotNull final Collection balloons) { - synchronized (myBalloons) { - myBalloons.clear(); - myBalloons.addAll(balloons); + public final void insertTextAfterCaret(@NotNull final String text) { + try { + myMainTextField.getDocument().insertString(myMainTextField.getCaretPosition(), text, null); + } + catch (final BadLocationException e) { + // TODO: Display error somehow! + e.printStackTrace(); } } @Override - public void setSubText(@NotNull final String subText) { - mySubLabel.setText(subText); + public final void replaceText(final int from, final int to, @NotNull final String newText) { + myMainTextField.select(from, to); + myMainTextField.replaceSelection(newText); + myBalloonManager.closeAllBalloons(); + myPresenter.textChanged(); + displayBalloonsIfRequired(); // This crunch but we need to recalculate balloons in this case (position is changed!) } + @Override + public final int getCaretPosition() { + return myMainTextField.getCaretPosition(); + } + + + @Override + public final void configureSubTexts(@Nullable final String defaultSubText, + @NotNull final List> suggestionAvailablePlaces) { + synchronized (myPlacesWhereSuggestionsAvailable) { + myPlacesWhereSuggestionsAvailable.clear(); + myPlacesWhereSuggestionsAvailable.addAll(suggestionAvailablePlaces); + myDefaultSubText = defaultSubText; + } + configureAppropriateStatus(); + } /** * Reacts on keys, pressed by user @@ -281,21 +371,13 @@ public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements Com @Override public void keyPressed(final KeyEvent e) { super.keyPressed(e); + final int keyCode = e.getKeyCode(); if (keyCode == KeyEvent.VK_UP) { - myList.moveSelection(true); + mySuggestionList.moveSelection(true); } else if (keyCode == KeyEvent.VK_DOWN) { - myList.moveSelection(false); - } - else if (keyCode == KeyEvent.VK_ENTER) { - myPresenter.executionRequested(myList.getValue()); - } - else if (keyCode == KeyEvent.VK_TAB) { - myPresenter.completionRequested(myList.getValue()); - } - else if ((keyCode == KeyEvent.VK_SPACE) && (e.getModifiersEx() == InputEvent.CTRL_DOWN_MASK)) { - myPresenter.suggestionRequested(); + mySuggestionList.moveSelection(false); } } } @@ -306,10 +388,6 @@ public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements Com return myMainTextField.getText(); } - @Override - public void setPreferredWidthInChars(final int widthInChars) { - myMainTextField.setPreferredWidthInChars(widthInChars); - } /** * Listener for suggestion list @@ -322,4 +400,35 @@ public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements Com removeSuggestions(); } } + + /** + * Keeps tracks for baloons to close all of them in case of text inserion + */ + private static final class BalloonManager extends JBPopupAdapter { + @NotNull + private final Set myCurrentBaloons = new HashSet(); + + void registerBalloon(final Balloon balloon) { + synchronized (myCurrentBaloons) { + myCurrentBaloons.add(balloon); + balloon.addListener(this); + } + } + + @Override + public void onClosed(final LightweightWindowEvent event) { + synchronized (myCurrentBaloons) { + myCurrentBaloons.remove(event.asBalloon()); + } + super.onClosed(event); + } + + void closeAllBalloons() { + synchronized (myCurrentBaloons) { + for (final Balloon balloon : myCurrentBaloons) { + balloon.dispose(); + } + } + } + } } diff --git a/python/src/com/jetbrains/python/commandInterface/swingView/CompletionKeyStrokeAction.java b/python/src/com/jetbrains/python/commandInterface/swingView/CompletionKeyStrokeAction.java new file mode 100644 index 000000000000..8ed0c5af06ed --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/swingView/CompletionKeyStrokeAction.java @@ -0,0 +1,34 @@ +/* + * 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.swingView; + +import java.awt.event.ActionEvent; + +/** + * "Complete current command or argument" action + * + * @author Ilya.Kazakevich + */ +final class CompletionKeyStrokeAction extends KeyStrokeAction { + CompletionKeyStrokeAction() { + super(KeyStrokeInfo.COMPLETION); + } + + @Override + public void actionPerformed(final ActionEvent e) { + myPresenter.completionRequested(mySuggestionList.getValue()); + } +} diff --git a/python/src/com/jetbrains/python/commandInterface/swingView/ExecutionKeyStrokeAction.java b/python/src/com/jetbrains/python/commandInterface/swingView/ExecutionKeyStrokeAction.java new file mode 100644 index 000000000000..7f258822a968 --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/swingView/ExecutionKeyStrokeAction.java @@ -0,0 +1,34 @@ +/* + * 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.swingView; + +import java.awt.event.ActionEvent; + +/** + * "Execute command" action + * + * @author Ilya.Kazakevich + */ +final class ExecutionKeyStrokeAction extends KeyStrokeAction { + ExecutionKeyStrokeAction() { + super(KeyStrokeInfo.EXECUTION); + } + + @Override + public void actionPerformed(final ActionEvent e) { + myPresenter.executionRequested(); + } +} diff --git a/python/src/com/jetbrains/python/commandInterface/swingView/KeyStrokeAction.java b/python/src/com/jetbrains/python/commandInterface/swingView/KeyStrokeAction.java new file mode 100644 index 000000000000..a801580a5bf6 --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/swingView/KeyStrokeAction.java @@ -0,0 +1,72 @@ +/* + * 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.swingView; + +import com.jetbrains.python.commandInterface.CommandInterfacePresenter; +import com.jetbrains.python.suggestionList.SuggestionList; +import org.jetbrains.annotations.NotNull; + +import javax.swing.text.TextAction; + +/** + * Action that should be taken for certain {@link javax.swing.KeyStroke} (wrapped in {@link com.jetbrains.python.commandInterface.swingView.KeyStrokeInfo}). + * You need to call {@link #configure(com.jetbrains.python.commandInterface.CommandInterfacePresenter, com.jetbrains.python.suggestionList.SuggestionList)} + * to enable one. + * + * @author Ilya.Kazakevich + */ +@SuppressWarnings({"InstanceVariableMayNotBeInitialized", "NonSerializableFieldInSerializableClass"}) // Will never serialize +abstract class KeyStrokeAction extends TextAction { + @NotNull + private final String myName; + @NotNull + private final KeyStrokeInfo myStroke; + + protected CommandInterfacePresenter myPresenter; + protected SuggestionList mySuggestionList; + + /** + * @param stroke key stroke to bind this info to + */ + KeyStrokeAction(@NotNull final KeyStrokeInfo stroke) { + super(stroke.name()); + myName = stroke.name(); + myStroke = stroke; + } + + + /** + * Configures action. + * + * @param presenter presenter to be used for call back. + * @param suggestionList list of suggestions to be used for call back + * @return name of this action to add to {@link javax.swing.InputMap} + */ + @NotNull + final String configure(@NotNull final CommandInterfacePresenter presenter, @NotNull final SuggestionList suggestionList) { + myPresenter = presenter; + mySuggestionList = suggestionList; + return myName; + } + + /** + * @return stroke bound to this action + */ + @NotNull + final KeyStrokeInfo getStroke() { + return myStroke; + } +} diff --git a/python/src/com/jetbrains/python/commandInterface/swingView/KeyStrokeInfo.java b/python/src/com/jetbrains/python/commandInterface/swingView/KeyStrokeInfo.java new file mode 100644 index 000000000000..dcf8278e6612 --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/swingView/KeyStrokeInfo.java @@ -0,0 +1,99 @@ +/* + * 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.swingView; + +import com.intellij.openapi.keymap.KeymapUtil; +import com.jetbrains.python.commandInterface.CommandInterfacePresenter; +import com.jetbrains.python.suggestionList.SuggestionList; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +/** + * Key strokes to be used with view. + * Strokes paired with action. You need to register each action via {@link javax.swing.InputMap} + * + * @author Ilya.Kazakevich + */ +enum KeyStrokeInfo { + /** + * "Execute command" keystroke + */ + EXECUTION(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)), + /** + * "Complete current command or argument" keystroke + */ + COMPLETION(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0)), + /** + * "Display suggestions" keystroke. + */ + SUGGESTION(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, InputEvent.CTRL_MASK)),; + + /** + * List of actions. Each action should be bound to some {@link com.jetbrains.python.commandInterface.swingView.KeyStrokeInfo} + */ + @NotNull + private static final KeyStrokeAction[] ACTIONS = { + new CompletionKeyStrokeAction(), + new ExecutionKeyStrokeAction(), + new SuggestionKeyStrokeAction()}; + + private final KeyStroke myStroke; + + KeyStrokeInfo(@NotNull final KeyStroke stroke) { + myStroke = stroke; + } + + /** + * Registers action and binds it appropriate stroke. Call if for all instances to make all actions available. + * + * @param presenter presenter to be used as call back + * @param suggestionList suggestion list to be used as call back + * @param source Component with {@link javax.swing.InputMap} and {@link javax.swing.ActionMap} (swing view itself) + */ + void register(@NotNull final CommandInterfacePresenter presenter, + @NotNull final SuggestionList suggestionList, + @NotNull final JComponent source) { + final KeyStrokeAction action = getAction(); + final String strokeName = action.configure(presenter, suggestionList); + source.getInputMap().put(myStroke, strokeName); + source.getActionMap().put(strokeName, action); + } + + /** + * @return Human-readable name of this action (to display it to user) + */ + @NotNull + String getText() { + return KeymapUtil.getKeystrokeText(myStroke); + } + + + /** + * @return action paired with stroke + */ + @NotNull + private KeyStrokeAction getAction() { + for (final KeyStrokeAction action : ACTIONS) { + if (action.getStroke() == this) { + return action; + } + } + throw new IllegalStateException("Failed to find action for " + name()); + } +} diff --git a/python/src/com/jetbrains/python/commandInterface/swingView/SmartTextField.java b/python/src/com/jetbrains/python/commandInterface/swingView/SmartTextField.java index 0a247bbcec81..2e7ff1a07352 100644 --- a/python/src/com/jetbrains/python/commandInterface/swingView/SmartTextField.java +++ b/python/src/com/jetbrains/python/commandInterface/swingView/SmartTextField.java @@ -17,13 +17,9 @@ package com.jetbrains.python.commandInterface.swingView; import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.editor.colors.EditorFontType; -import com.intellij.openapi.util.Pair; import com.intellij.util.Range; import com.intellij.util.ui.StatusText; -import com.jetbrains.python.commandInterface.CommandInterfaceView; -import com.jetbrains.python.commandInterface.CommandInterfaceView.SpecialErrorPlace; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; @@ -47,11 +43,6 @@ public class SmartTextField extends JTextField { @NotNull private final Collection myUnderlineInfo = new ArrayList(); private int myPreferredWidth; - /** - * (color, special_place) tuple to underline special place, or null if no underline required - */ - @Nullable - private Pair mySpecialUnderlinePlace; public SmartTextField() { setFont(EditorColorsManager.getInstance().getGlobalScheme().getFont(EditorFontType.CONSOLE_PLAIN)); @@ -66,15 +57,9 @@ public class SmartTextField extends JTextField { synchronized (myUnderlineInfo) { for (final UnderlineInfo underlineInfo : myUnderlineInfo) { g.setColor(underlineInfo.myColor); + // To prevent too long underlines: last char should really be last underline(g, underlineInfo.getFrom(), underlineInfo.getTo()); } - if (mySpecialUnderlinePlace != null) { - final SpecialErrorPlace place = mySpecialUnderlinePlace.second; - g.setColor(mySpecialUnderlinePlace.first); - final int endPosition = getTextEndPosition(); - final int from = (place == SpecialErrorPlace.WHOLE_TEXT ? 0 : endPosition - getColumnWidth()); - underline(g, from, endPosition); - } } } @@ -90,17 +75,11 @@ public class SmartTextField extends JTextField { g.drawLine(from + getColumnWidth(), verticalPosition, to + getColumnWidth(), verticalPosition); } - /** - * @return place (in px) where entered text ends. - */ - int getTextEndPosition() { - return (getText().length() + 1) * getColumnWidth(); - } /** * @return place (in px) where caret. */ - int getTextCursorPosition() { + int getTextCaretPositionInPx() { return (getCaretPosition() + 1) * getColumnWidth(); } @@ -139,19 +118,9 @@ public class SmartTextField extends JTextField { void hideUnderline() { synchronized (myUnderlineInfo) { myUnderlineInfo.clear(); - mySpecialUnderlinePlace = null; } } - /** - * Sets appropriate width in chars - * - * @param widthInChars num of chars - */ - void setPreferredWidthInChars(final int widthInChars) { - setColumns(widthInChars); - } - /** * Sets appropriate width in pixels * @@ -161,18 +130,6 @@ public class SmartTextField extends JTextField { myPreferredWidth = width; } - /** - * Display underline in special place - * - * @param color color to underline - * @param specialUnderlinePlace special place to underline - */ - void underlineText(@NotNull final SpecialErrorPlace specialUnderlinePlace, - @NotNull final Color color) { - synchronized (myUnderlineInfo) { - mySpecialUnderlinePlace = Pair.create(color, specialUnderlinePlace); - } - } /** * Wrapper to display placeholder diff --git a/python/src/com/jetbrains/python/commandInterface/swingView/SuggestionKeyStrokeAction.java b/python/src/com/jetbrains/python/commandInterface/swingView/SuggestionKeyStrokeAction.java new file mode 100644 index 000000000000..088ff8c3fe27 --- /dev/null +++ b/python/src/com/jetbrains/python/commandInterface/swingView/SuggestionKeyStrokeAction.java @@ -0,0 +1,34 @@ +/* + * 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.swingView; + +import java.awt.event.ActionEvent; + +/** + * "Suggestion request" action + * + * @author Ilya.Kazakevich + */ +final class SuggestionKeyStrokeAction extends KeyStrokeAction { + SuggestionKeyStrokeAction() { + super(KeyStrokeInfo.SUGGESTION); + } + + @Override + public void actionPerformed(final ActionEvent e) { + myPresenter.suggestionRequested(); + } +} diff --git a/python/src/com/jetbrains/python/commandLineParser/CommandLineParseResult.java b/python/src/com/jetbrains/python/commandLineParser/CommandLineParseResult.java new file mode 100644 index 000000000000..a2a9756b0c26 --- /dev/null +++ b/python/src/com/jetbrains/python/commandLineParser/CommandLineParseResult.java @@ -0,0 +1,78 @@ +/* + * 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.commandLineParser; + +import com.intellij.openapi.util.Pair; +import com.jetbrains.python.WordWithPosition; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Command line parse result. + * It consists of command itself and its parts. + * Each part may be {@link com.jetbrains.python.commandLineParser.CommandLinePartType#ARGUMENT argument} or + * {@link com.jetbrains.python.commandLineParser.CommandLinePartType#OPTION option} or something else. + * + * @author Ilya.Kazakevich + */ +public final class CommandLineParseResult { + @NotNull + private final List> myParts = new ArrayList>(); + @NotNull + private final WordWithPosition myCommand; + + CommandLineParseResult( + @NotNull final WordWithPosition command, + @NotNull final Collection> parts) { + myCommand = command; + myParts.addAll(parts); + } + + /** + * @return command (i.e. "startapp" in "startapp my_app") + */ + @NotNull + public WordWithPosition getCommand() { + return myCommand; + } + + /** + * @return list of parts in format [part_type, value]. + * For example (rm my_folder): [{@link com.jetbrains.python.commandLineParser.CommandLinePartType#ARGUMENT argument}, my_folder] + */ + @NotNull + public List> getParts() { + return Collections.unmodifiableList(myParts); + } + + /** + * @return all command line parts with out of part information (just words and positions). + * Note tha command itself is not part, only args and options are + * @see #getParts() + */ + @NotNull + public Collection getPartsNoType() { + final Collection result = new ArrayList(); + for (final Pair part : myParts) { + result.add(part.second); + } + return result; + } +} diff --git a/python/src/com/jetbrains/python/commandLineParser/CommandLineParser.java b/python/src/com/jetbrains/python/commandLineParser/CommandLineParser.java new file mode 100644 index 000000000000..b3813d8dd616 --- /dev/null +++ b/python/src/com/jetbrains/python/commandLineParser/CommandLineParser.java @@ -0,0 +1,38 @@ +/* + * 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.commandLineParser; + +import com.jetbrains.python.WordWithPosition; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * Engine to parse command line. It understands how options and arguments are coded in certain commandline. + * It supportd {@link com.jetbrains.python.WordWithPosition} telling you exactly with part of + * command line is command or argument. That helps you to underline or emphisize some parts. + * + * @author Ilya.Kazakevich + */ +public interface CommandLineParser { + /** + * @param commandLineParts command line splitted into words. + * @return command line information + * @throws MalformedCommandLineException in case of bad commandline + */ + @NotNull + CommandLineParseResult parse(@NotNull List commandLineParts) throws MalformedCommandLineException; +} diff --git a/python/src/com/jetbrains/python/commandLineParser/CommandLinePartType.java b/python/src/com/jetbrains/python/commandLineParser/CommandLinePartType.java new file mode 100644 index 000000000000..0f2359e15087 --- /dev/null +++ b/python/src/com/jetbrains/python/commandLineParser/CommandLinePartType.java @@ -0,0 +1,36 @@ +/* + * 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.commandLineParser; + +/** + * Types of command line parts. + * + * @author Ilya.Kazakevich + */ +public enum CommandLinePartType { + /** + * Argument (or positional, or unnamed argument) something that has only value. Like "my_folder" in "rm my_folder" + */ + ARGUMENT, + /** + * Option is named but optional parameter. Like "-l" in "ls -l". + */ + OPTION, + /** + * Some part of command line that {@link com.jetbrains.python.commandLineParser.CommandLineParser} does not understand + */ + UNKNOWN +} diff --git a/python/src/com/jetbrains/python/optParse/MalformedCommandLineException.java b/python/src/com/jetbrains/python/commandLineParser/MalformedCommandLineException.java similarity index 94% rename from python/src/com/jetbrains/python/optParse/MalformedCommandLineException.java rename to python/src/com/jetbrains/python/commandLineParser/MalformedCommandLineException.java index 3e2185230360..8f69e96bf96e 100644 --- a/python/src/com/jetbrains/python/optParse/MalformedCommandLineException.java +++ b/python/src/com/jetbrains/python/commandLineParser/MalformedCommandLineException.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.jetbrains.python.optParse; +package com.jetbrains.python.commandLineParser; import org.jetbrains.annotations.NotNull; diff --git a/python/src/com/jetbrains/python/optParse/ParsedCommandLine.java b/python/src/com/jetbrains/python/commandLineParser/OptParseCommandLineParser.java similarity index 56% rename from python/src/com/jetbrains/python/optParse/ParsedCommandLine.java rename to python/src/com/jetbrains/python/commandLineParser/OptParseCommandLineParser.java index f4cab570ee45..ddcd0822e53c 100644 --- a/python/src/com/jetbrains/python/optParse/ParsedCommandLine.java +++ b/python/src/com/jetbrains/python/commandLineParser/OptParseCommandLineParser.java @@ -13,11 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.jetbrains.python.optParse; +package com.jetbrains.python.commandLineParser; +import com.intellij.openapi.util.Pair; +import com.jetbrains.python.WordWithPosition; import org.jetbrains.annotations.NotNull; -import java.util.*; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; // TODO: Support options and their arguments @@ -36,23 +41,17 @@ import java.util.*; * * @author Ilya.Kazakevich */ -public final class ParsedCommandLine { +public final class OptParseCommandLineParser implements CommandLineParser { @NotNull - private final WordWithPosition myCommand; - @NotNull - private final List myArguments = new ArrayList(); - - /** - * @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 parts = new LinkedList(WordWithPosition.splitText(commandLine)); + @Override + public CommandLineParseResult parse(@NotNull final List commandLineParts) throws MalformedCommandLineException { + final Deque parts = new ArrayDeque(commandLineParts); if (parts.isEmpty()) { throw new MalformedCommandLineException("No command provided"); } - myCommand = parts.pop(); - if (myCommand.getText().startsWith("-")) { + final WordWithPosition command = parts.pop(); + final List> resultParts = new ArrayList>(); + if (command.getText().startsWith("-")) { throw new MalformedCommandLineException("Command can't start with option prefix"); } @@ -60,46 +59,13 @@ public final class ParsedCommandLine { for (final WordWithPosition part : parts) { if (part.getText().startsWith("-")) { // This is option! + resultParts.add(Pair.create(CommandLinePartType.OPTION, part)); } else { // TODO: Check optopn argument! - myArguments.add(part); + resultParts.add(Pair.create(CommandLinePartType.ARGUMENT, part)); } } + return new CommandLineParseResult(command, resultParts); } - - - /** - * @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 getArguments() { - return Collections.unmodifiableList(myArguments); - } - - /** - * @return all parts for command line as simple words - */ - @NotNull - public List getAsWords() { - final List result = new ArrayList(); - - 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/commandLineParser/package-info.java b/python/src/com/jetbrains/python/commandLineParser/package-info.java new file mode 100644 index 000000000000..bbdd192167f4 --- /dev/null +++ b/python/src/com/jetbrains/python/commandLineParser/package-info.java @@ -0,0 +1,30 @@ +/* + * 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. + */ + +/** + * Engine to parse command-line. + * Command line consists of command itself, {@link com.jetbrains.python.commandLineParser.CommandLinePartType#ARGUMENT arguments} + * and {@link com.jetbrains.python.commandLineParser.CommandLinePartType#OPTION options}. + * Use need to split command line into {@link com.jetbrains.python.WordWithPosition chunks}, pass them to + * {@link com.jetbrains.python.commandLineParser.CommandLineParser parser} and obtain {@link com.jetbrains.python.commandLineParser.CommandLineParseResult}. + * + * Not like any other parsers, this package supports {@link com.jetbrains.python.WordWithPosition} telling you exactly with part of + * command line is command or argument. That helps you to underline or emphisize some parts. + * + * + * @author Ilya.Kazakevich + */ +package com.jetbrains.python.commandLineParser; \ No newline at end of file diff --git a/python/src/com/jetbrains/python/optParse/package-info.java b/python/src/com/jetbrains/python/optParse/package-info.java deleted file mode 100644 index 3be946b72036..000000000000 --- a/python/src/com/jetbrains/python/optParse/package-info.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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. - */ - -/** - *

- * An optparse module twin, that parses command line. - * Unlike any other GNU/Posix parsers, it knows how to:

- *
    - *
  1. Parse options and args with out of any knowledge about required args
  2. - *
  3. Provide actual places in command line where exactly such args or opts exist./li> - *
- *

- * Be sure to read optparse manual - * (epecially terminology) part. - *

- *

Package entry point is {@link com.jetbrains.python.optParse.ParsedCommandLine}

- * @author Ilya.Kazakevich - */ -package com.jetbrains.python.optParse; \ No newline at end of file diff --git a/python/src/com/jetbrains/python/suggestionList/SuggestionList.java b/python/src/com/jetbrains/python/suggestionList/SuggestionList.java index c43f23809806..d209235bec74 100644 --- a/python/src/com/jetbrains/python/suggestionList/SuggestionList.java +++ b/python/src/com/jetbrains/python/suggestionList/SuggestionList.java @@ -166,7 +166,14 @@ public class SuggestionList { return null; // Nothing is selected if list is invisible } final Object value = myList.getSelectedValue(); - return ((value == null) ? "" : getElement(value).mySuggestion.getText()); + return ((value == null) ? null : getElement(value).mySuggestion.getText()); + } + + /** + * @return true if no suggestion list is displayed now. + */ + public final synchronized boolean isClosed() { + return myListPopUp == null || myListPopUp.isDisposed(); } /**