PY-11855 Run manage.py task improvements

Refactored to new architecture (Presenter  / ChunkDriver).
This commit is contained in:
Ilya.Kazakevich
2015-02-10 01:04:51 +03:00
parent f45d41104b
commit a431dfe358
45 changed files with 1699 additions and 1319 deletions
@@ -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
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}"
@@ -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<Integer> {
/**
* 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<Integer> {
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<Integer> range) {
this(word, range.getFrom(), range.getTo());
}
@NotNull
public String getText() {
return myWord;
@@ -121,7 +131,7 @@ public final class WordWithPosition extends Range<Integer> {
* @return parse result
*/
@NotNull
static List<WordWithPosition> splitText(@NotNull final String text) {
public static List<WordWithPosition> splitText(@NotNull final String text) {
// TODO: Rewrite using regex or scanner?
int position = 0;
int wordStart = -1;
@@ -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();
}
@@ -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<Integer> AFTER_LAST_CHARACTER_RANGE = new Range<Integer>(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<WordWithPosition> 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:
* <ol>
* <li>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</li>
* <li>Default text: In all other cases view displays default text (if available).</li>
* </ol>
* <p/>
* 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<Range<Integer>> 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<WordWithPosition> infoBalloons, @NotNull final Collection<WordWithPosition> 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.
* <strong>Caution: Each call removes previuos balloons!</strong>
* 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<WordWithPosition> 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();
}
@@ -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<ChunkAndInfo> {
@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;
}
}
@@ -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 <strong>always</strong> 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<WordWithPosition> chunks);
}
@@ -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<ChunkAndInfo> myChunkAndInfos = new TreeSet<ChunkAndInfo>();
@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<WordWithPosition> chunks = WordWithPosition.splitText(myView.getText());
final ParseInfo parseInfo = myChunkDriver.parse(chunks);
myExecutor = parseInfo.getExecutor();
final List<ChunkInfo> 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<WordWithPosition> infoBalloons = new ArrayList<WordWithPosition>();
final Collection<WordWithPosition> errorBalloons = new ArrayList<WordWithPosition>();
for (final ChunkAndInfo chunkInfoPair : myChunkAndInfos) {
final ChunkInfo chunkInfo = chunkInfoPair.getChunkInfo();
Range<Integer> 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<Range<Integer>> placesWhereSuggestionAvailable = new ArrayList<Range<Integer>>();
for (final ChunkAndInfo chunkAndInfo : myChunkAndInfos) {
Range<Integer> 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<String> suggestions = new ArrayList<String>(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<String> suggestions, @NotNull final String textToMatch) {
// TODO: use guava instead?
final Iterator<String> 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<Integer> 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<String> suggestions = new ArrayList<String>(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();
}
}
}
@@ -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;
}
}
@@ -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<ChunkInfo> myChunkInfo = new ArrayList<ChunkInfo>();
@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> 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> 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<ChunkInfo> getChunkInfo() {
return Collections.unmodifiableList(myChunkInfo);
}
}
@@ -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<String> mySuggestions = new ArrayList<String>();
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<String> suggestions) {
myShowSuggestionsAutomatically = showSuggestionsAutomatically;
myShowAbsolute = showAbsolute;
mySuggestions.addAll(suggestions);
}
/**
* @return List of suggestions to display
*/
@NotNull
public List<String> 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;
}
}
@@ -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 <strong>chunk</strong>
* 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;
@@ -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;
@@ -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 <strong>values</strong>.
* Values should be provided as list. I.e. for <pre>my_command foo bar</pre> there should be list of "foo, bar".
*
* @param argumentValuesToCheck values to check
* @return validation result
*/
@NotNull
ArgumentsValuesValidationInfo validateArgumentValues(@NotNull final List<String> argumentValuesToCheck);
Pair<Boolean, Argument> getArgument(int argumentPosition);
}
@@ -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);
}
@@ -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<String, Command> myCommands = new TreeMap<String, Command>(); // 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<? extends Command> commands) {
myCommandLineParser = commandLineParser;
for (final Command command : commands) {
myCommands.put(command.getName(), command);
}
myModule = module;
}
@Override
@NotNull
public ParseInfo parse(@NotNull final List<WordWithPosition> 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> chunkInfo = new ArrayList<ChunkInfo>();
// 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<Boolean, Argument> 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<String> availableValues = argument.getAvailableValues();
final Pair<CommandLinePartType, WordWithPosition> 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<Boolean, Argument> nextArgumentPair = commandArgumentsInfo.getArgument(commandLine.getParts().size());
if (nextArgumentPair != null) {
// Next arg exists
final Argument nextArgument = nextArgumentPair.getSecond();
final List<String> 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<ChunkInfo> result = new ArrayList<ChunkInfo>();
// 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);
}
}
}
@@ -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<Argument> 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<Boolean, Argument> 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<String> argumentValuesToCheck) {
final Map<Integer, ArgumentValueError> errors = new HashMap<Integer, ArgumentValueError>();
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<String> availableValues = argument.getAvailableValues();
if (availableValues != null && !availableValues.contains(userValue)) {
errors.put(i, ArgumentValueError.BAD_VALUE);
}
}
return new ArgumentsValuesValidationInfo(errors, argumentValuesToCheck.size() < myMinArguments);
}
}
@@ -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<Boolean, Argument> getArgument(final int argumentPosition) {
return null;
}
}
@@ -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<String> argumentValuesToCheck) {
return ArgumentsValuesValidationInfo.NO_ERROR; // Actually, we have no idea
public Pair<Boolean, Argument> getArgument(final int argumentPosition) {
return Pair.create(false, new Argument(myHelp));
}
}
@@ -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;
@@ -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.<Integer, ArgumentValueError>emptyMap(), false);
private final Map<Integer, ArgumentValueError> myPositionOfErrorArguments = new HashMap<Integer, ArgumentValueError>();
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<Integer, ArgumentValueError> positionOfErrorArguments,
final boolean notEnoughArguments) {
myPositionOfErrorArguments.putAll(positionOfErrorArguments);
myNotEnoughArguments = notEnoughArguments;
}
/**
* @return map of [argument_position, its_value_error]
*/
@NotNull
Map<Integer, ArgumentValueError> 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
}
}
@@ -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;
}
}
@@ -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<String> 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;
}
}
@@ -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 <C> Command type
* @author Ilya.Kazakevich
*/
public class CommandInterfacePresenterCommandBased<C extends Command> extends CommandInterfacePresenterAdapter {
/**
* [name] -> command. Linked is used to preserve order.
*/
private final Map<String, C> myCommands = new LinkedHashMap<String, C>();
/**
* 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<C> 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<SpecialErrorPlace, List<WordWithPosition>> errorInfo = myStrategy.getErrorInfo();
myView.showErrors(errorInfo.getSecond(), errorInfo.first);
myView.setBalloons(myStrategy.getBalloonsToShow());
final SuggestionInfo suggestionInfo = myStrategy.getSuggestionInfo();
final List<String> suggestions = new ArrayList<String>(suggestionInfo.getSuggestions());
final String lastPart = getLastPart();
if ((lastPart != null) && myStrategy.isUnknownTextExists()) {
//Filter to starts from
final Iterator<String> 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<CommandExecutionInfo> history = getHistory();
final Collection<String> historyCommands = new LinkedHashSet<String>();
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<CommandExecutionInfo> 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<String> words = commandLine != null ? commandLine.getAsWords() : new ArrayList<String>();
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<String> 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<String, C> 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<String> words = commandLine.getAsWords();
return words.get(words.size() - 1);
}
/**
* @return view
*/
@NotNull
CommandInterfaceView getView() {
return myView;
}
}
@@ -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<String> myArguments = new ArrayList<String>();
@NotNull
private final Command myCommand;
@NotNull
private final ParsedCommandLine myCommandLine;
/**
* @param command command enrtered by user
* @param presenter presenter
*/
InCommandStrategy(@NotNull final Command command,
@NotNull final 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<String> availableValues = nextArgument.getAvailableValues();
if (availableValues != null) { // If has available values
return new SuggestionInfo(false, false, availableValues);
}
}
return new SuggestionInfo(false, false, Collections.<String>emptyList());
}
@NotNull
@Override
List<WordWithPosition> getBalloonsToShow() {
// Display argument balloons right from command end to last argument end
final ArgumentsInfo argumentsInfo = myCommand.getArgumentsInfo();
final List<WordWithPosition> 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<WordWithPosition> result = new ArrayList<WordWithPosition>(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<SpecialErrorPlace, List<WordWithPosition>> getErrorInfo() {
final List<WordWithPosition> userProvidedValues = myCommandLine.getArguments();
SpecialErrorPlace specialError = null;
final List<WordWithPosition> errors = new ArrayList<WordWithPosition>();
final ArgumentsValuesValidationInfo validation =
myCommand.getArgumentsInfo().validateArgumentValues(WordWithPosition.fetchText(userProvidedValues));
if (validation.isNotEnoughArguments()) {
specialError = SpecialErrorPlace.AFTER_LAST_CHAR;
}
for (final Entry<Integer, ArgumentValueError> 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);
}
}
@@ -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<String> argumentValuesToCheck) {
if (argumentValuesToCheck.isEmpty()) {
return ArgumentsValuesValidationInfo.NO_ERROR;
}
final Map<Integer, ArgumentValueError> errors =
new HashMap<Integer, ArgumentsValuesValidationInfo.ArgumentValueError>();
for (final int errorPosition : ContiguousSet.create(Range.closedOpen(0, argumentValuesToCheck.size()), DiscreteDomain.integers())) {
errors.put(errorPosition, ArgumentValueError.EXCESS);
}
return new ArgumentsValuesValidationInfo(errors, false);
}
}
@@ -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<SpecialErrorPlace, List<WordWithPosition>>
NO_ERROR = Pair.create(null, Collections.<WordWithPosition>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<String>(myPresenter.getCommands().keySet()));
}
@Override
boolean isUnknownTextExists() {
return !myPresenter.getView().getText().isEmpty();
}
@NotNull
@Override
Pair<SpecialErrorPlace, List<WordWithPosition>> 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.<WordWithPosition>emptyList());
}
private boolean isTextBoxEmpty() {
return myPresenter.getView().getText().isEmpty();
}
@Nullable
@Override
CommandExecutionInfo getCommandToExecute() {
return 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<WordWithPosition> getBalloonsToShow() {
return Collections.emptyList();
}
/**
* @return command that entered in box, or null of just entered
*/
@Nullable
abstract CommandExecutionInfo getCommandToExecute();
/**
* @return errors
*/
@NotNull
abstract Pair<SpecialErrorPlace, List<WordWithPosition>> getErrorInfo();
/**
* @return if text entered by user contains some unknown commands
*/
abstract boolean isUnknownTextExists();
}
@@ -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<String> mySuggestions = new ArrayList<String>();
/**
* Display them at absolute location or relative to last letter
*/
final boolean myAbsolute;
/**
* Show then any time, or only when user requests them
*/
final boolean myShowOnlyWhenRequested;
/**
* @param absolute Display them at absolute location or relative to last letter
* @param showOnlyWhenRequested Show then any time, or only when user requests them
* @param suggestions Suggestions
*/
SuggestionInfo(final boolean absolute,
final boolean showOnlyWhenRequested,
@NotNull final List<String> suggestions) {
myAbsolute = absolute;
myShowOnlyWhenRequested = showOnlyWhenRequested;
mySuggestions.addAll(suggestions);
}
/**
* @return suggestions
*/
@NotNull
List<String> getSuggestions() {
return Collections.unmodifiableList(mySuggestions);
}
}
@@ -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.
*/
/**
* <h1>Optparse-based commandline interface presenter</h1>
* <p>
* 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 <a href="https://docs.python.org/2/library/optparse.html">optparse</a> terminology, so
* read it first and use {@link com.jetbrains.python.optParse} package
* </p>
* <h2>Arguments and validation</h2>
* <p>
* Optparse arguments are <strong>positional and unnamed</strong>.
* 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
* </p>
*
*
* @see com.jetbrains.python.optParse
* @author Ilya.Kazakevich
*/
package com.jetbrains.python.commandInterface.commandsWithArgs;
@@ -19,7 +19,7 @@
* It has several abilities, including (but not limited):
* <ol>
* <li>Suggestion box</li>
* <li>Error marking</li>
* <li>Error emphasising</li>
* <li>Popups</li>
* <li>AutoCompletion</li>
* </ol>
@@ -31,9 +31,15 @@
*
* <p>
* 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}
* </p>
*
* <p>
* 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.
* </p>
*
*
*
* @author Ilya.Kazakevich
*/
@@ -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<Range<Integer>> myPlacesWhereSuggestionsAvailable = new ArrayList<Range<Integer>>();
@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<WordWithPosition> myBalloons = new ArrayList<WordWithPosition>();
@NotNull
private final List<WordWithPosition> myInfoBalloons = new ArrayList<WordWithPosition>();
/**
* 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<WordWithPosition> myErrorBalloons = new ArrayList<WordWithPosition>();
/**
* 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<WordWithPosition> 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<Integer> 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<WordWithPosition> 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<WordWithPosition> 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<WordWithPosition> infoBalloons,
@NotNull final Collection<WordWithPosition> 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 <strong>special case</strong> {@link #AFTER_LAST_CHARACTER_RANGE}.
*
* @param range range to check
* @return true if special case
*/
private static boolean isAfterLastCharRange(@NotNull final Range<Integer> 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<WordWithPosition> 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<Range<Integer>> 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<Balloon> myCurrentBaloons = new HashSet<Balloon>();
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();
}
}
}
}
}
@@ -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());
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -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<UnderlineInfo> myUnderlineInfo = new ArrayList<UnderlineInfo>();
private int myPreferredWidth;
/**
* (color, special_place) tuple to underline special place, or null if no underline required
*/
@Nullable
private Pair<Color, SpecialErrorPlace> 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
@@ -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();
}
}
@@ -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<Pair<CommandLinePartType, WordWithPosition>> myParts = new ArrayList<Pair<CommandLinePartType, WordWithPosition>>();
@NotNull
private final WordWithPosition myCommand;
CommandLineParseResult(
@NotNull final WordWithPosition command,
@NotNull final Collection<Pair<CommandLinePartType, WordWithPosition>> 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<Pair<CommandLinePartType, WordWithPosition>> 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<WordWithPosition> getPartsNoType() {
final Collection<WordWithPosition> result = new ArrayList<WordWithPosition>();
for (final Pair<CommandLinePartType, WordWithPosition> part : myParts) {
result.add(part.second);
}
return result;
}
}
@@ -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<WordWithPosition> commandLineParts) throws MalformedCommandLineException;
}
@@ -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
}
@@ -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;
@@ -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<WordWithPosition> myArguments = new ArrayList<WordWithPosition>();
/**
* @param commandLine command line to parse
* @throws MalformedCommandLineException if malformed commandline can't be parsed
*/
public ParsedCommandLine(@NotNull final String commandLine) throws MalformedCommandLineException {
final Deque<WordWithPosition> parts = new LinkedList<WordWithPosition>(WordWithPosition.splitText(commandLine));
@Override
public CommandLineParseResult parse(@NotNull final List<WordWithPosition> commandLineParts) throws MalformedCommandLineException {
final Deque<WordWithPosition> parts = new ArrayDeque<WordWithPosition>(commandLineParts);
if (parts.isEmpty()) {
throw new MalformedCommandLineException("No command provided");
}
myCommand = parts.pop();
if (myCommand.getText().startsWith("-")) {
final WordWithPosition command = parts.pop();
final List<Pair<CommandLinePartType, WordWithPosition>> resultParts = new ArrayList<Pair<CommandLinePartType, WordWithPosition>>();
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<WordWithPosition> getArguments() {
return Collections.unmodifiableList(myArguments);
}
/**
* @return all parts for command line as simple words
*/
@NotNull
public List<String> getAsWords() {
final List<String> result = new ArrayList<String>();
result.add(myCommand.getText());
for (final WordWithPosition argument : myArguments) {
result.add(argument.getText());
}
// TODO: Add options as well
return result;
}
// TODO: Add options, arguments and option arguments
}
@@ -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;
@@ -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.
*/
/**
* <p>
* An <a href="https://docs.python.org/2/library/optparse.html">optparse</a> module twin, that parses command line.
* Unlike any other GNU/Posix parsers, it knows how to:</p>
* <ol>
* <li>Parse options and args with out of any knowledge about required args</li>
* <li>Provide actual places in command line where exactly such args or opts exist./li>
* </ol>
* <p>
* Be sure to read optparse manual
* (epecially <a href="https://docs.python.org/2/library/optparse.html#terminology">terminology</a>) part.
* </p>
* <p>Package entry point is {@link com.jetbrains.python.optParse.ParsedCommandLine}</p>
* @author Ilya.Kazakevich
*/
package com.jetbrains.python.optParse;
@@ -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();
}
/**