--- /dev/null
+/*
+ * 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;
+
+import com.jetbrains.python.vp.Presenter;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Presenter for command-line interface to be paired with view.
+ *
+ * @author Ilya.Kazakevich
+ */
+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);
+
+ /**
+ * Called by view when user requests for completion (like tab)
+ *
+ * @param valueFromSuggestionList value selected from suggestion list (if any selected)
+ */
+ void completionRequested(@Nullable String valueFromSuggestionList);
+
+ /**
+ * Called by view when user asks for suggestions (CTRL+Space)
+ */
+ void suggestionRequested();
+
+ /**
+ * Called by view when user wants to execute command (Enter is presed)
+ *
+ * @param valueFromSuggestionList value selected from suggestion list (if any selected)
+ */
+ void executionRequested(@Nullable String valueFromSuggestionList);
+}
--- /dev/null
+/*
+ * 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;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Adapter that holds view.
+ * @author Ilya.Kazakevich
+ */
+
+public abstract class CommandInterfacePresenterAdapter implements CommandInterfacePresenter {
+ @NotNull
+ protected final CommandInterfaceView myView;
+
+ protected CommandInterfacePresenterAdapter(@NotNull final CommandInterfaceView view) {
+ myView = view;
+ }
+
+ @Override
+ public void launch() {
+ myView.show();
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+
+import com.jetbrains.python.suggestionList.SuggestionsBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * View for command-line interface to be paired with view.
+ *
+ * @author Ilya.Kazakevich
+ */
+public interface CommandInterfaceView {
+
+ /**
+ * Launches view
+ */
+ void show();
+
+ /**
+ * 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)
+ */
+ void displaySuggestions(@NotNull SuggestionsBuilder suggestions, boolean absolute, @Nullable String toSelect);
+
+ /**
+ * Displays error (like red line)
+ *
+ * @param lastOnly underline only last letter
+ */
+ void showError(boolean lastOnly);
+
+ /**
+ * Change text to the one provided
+ *
+ * @param newText text to display
+ */
+ void forceText(@NotNull String newText);
+
+ /**
+ * Display text in sub part (like hint)
+ *
+ * @param subText text to display
+ */
+ void setSubText(@NotNull String subText);
+
+ /**
+ * Hide suggestion list
+ */
+ void removeSuggestions();
+
+ /**
+ * Displays baloon with message right under the last letter.
+ *
+ * @param message text to display
+ */
+ void displayInfoBaloon(@NotNull String message);
+
+ /**
+ * @return text, entered by user
+ */
+ @NotNull
+ String getText();
+
+ /**
+ * Enlarges view to make it as big as required to display appropriate number of chars
+ *
+ * @param widthInChars number of chars
+ */
+ void setPreferredWidthInChars(int widthInChars);
+}
--- /dev/null
+/*
+ * 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 org.jetbrains.annotations.NotNull;
+
+/**
+ * Command argument
+ *
+ * @author Ilya.Kazakevich
+ */
+public class Argument {
+ private final boolean myNamed;
+ @NotNull
+ private final String myName;
+
+ /**
+ * @param named is named argument or not
+ * @param name name of argument
+ */
+ public Argument(final boolean named, @NotNull final String name) {
+ myNamed = named;
+ myName = name;
+ }
+
+ /**
+ * @return is named argument or not
+ */
+ public boolean isNamed() {
+ return myNamed;
+ }
+
+ /**
+ * @return name of argument
+ */
+ @NotNull
+ public String getName() {
+ return myName;
+ }
+}
--- /dev/null
+/*
+ * 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 org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+
+/**
+ * Command
+ *
+ * @author Ilya.Kazakevich
+ */
+public class Command {
+ @NotNull
+ private final String myName;
+ @NotNull
+ private final List<Argument> myArguments = new ArrayList<Argument>();
+
+ /**
+ * @param name command name
+ * @param arguments command arguments
+ */
+ public Command(@NotNull final String name, @NotNull final Argument... arguments) {
+ this(name, Arrays.asList(arguments));
+ }
+
+ public Command(@NotNull final String name, @NotNull final Collection<Argument> arguments) {
+ myName = name;
+ myArguments.addAll(arguments);
+ }
+
+ /**
+ * @return command name
+ */
+ @NotNull
+ String getName() {
+ return myName;
+ }
+
+ /**
+ * @return command arguments
+ */
+ @NotNull
+ List<Argument> getArguments() {
+ return Collections.unmodifiableList(myArguments);
+ }
+}
--- /dev/null
+/*
+ * 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 com.jetbrains.python.commandInterface.CommandInterfacePresenterAdapter;
+import com.jetbrains.python.commandInterface.CommandInterfaceView;
+import com.jetbrains.python.commandInterface.commandsWithArgs.Strategy.SuggestionInfo;
+import com.jetbrains.python.suggestionList.SuggestionsBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.regex.Pattern;
+
+/**
+ * Command-line interface presenter that is command-based
+ *
+ * @author Ilya.Kazakevich
+ */
+public class CommandInterfacePresenterCommandBased extends CommandInterfacePresenterAdapter {
+ private static final Pattern EMPTY_SPACE = Pattern.compile("\\s+");
+ /**
+ * [name] -> command. Linked is used to preserve order.
+ */
+ private final Map<String, Command> myCommands = new LinkedHashMap<String, Command>();
+ /**
+ * currenly used strategy (see interface for more info)
+ */
+ private Strategy myStrategy;
+ /**
+ * When user asks for completion, last word should be removed if it is tied to suggestion
+ */
+ private boolean myLastSuggestionTiedToWord;
+
+
+ /**
+ * @param view view
+ * @param commands available commands
+ */
+ public CommandInterfacePresenterCommandBased(@NotNull final CommandInterfaceView view,
+ @NotNull final Iterable<Command> commands) {
+ super(view);
+ for (final Command command : commands) {
+ myCommands.put(command.getName(), command);
+ }
+ }
+
+ /**
+ * @param view view
+ * @param commands available commands
+ */
+ public CommandInterfacePresenterCommandBased(@NotNull final CommandInterfaceView view,
+ @NotNull final Command... 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) {
+ myLastSuggestionTiedToWord = false;
+ configureStrategy();
+ myView.setSubText(myStrategy.getSubText());
+ switch (myStrategy.getShowErrorInfo()) {
+ case NO:
+ break;
+ case FULL:
+ myView.showError(false);
+ break;
+ case RELATIVE:
+ myView.showError(true);
+ break;
+ }
+
+ 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
+ myLastSuggestionTiedToWord = true;
+ 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()) {
+ myView
+ .displaySuggestions(new SuggestionsBuilder(suggestions, true), suggestionInfo.myAbsolute, null);
+ }
+ else {
+ myView.removeSuggestions();
+ }
+ }
+
+ /**
+ * Finds and sets appropriate strategy
+ */
+ private void configureStrategy() {
+ if (myView.getText().isEmpty()) {
+ myStrategy = new NoCommandStrategy(this);
+ return;
+ }
+ final String[] parts = getTextAsParts();
+ if (parts.length > 0) {
+ final Command command = myCommands.get(parts[0]);
+ if (command != null) {
+ myStrategy = new InCommandStrategy(command, this);
+ return;
+ }
+ }
+ myStrategy = new NoCommandStrategy(this); // Junk
+ }
+
+ @Override
+ public void completionRequested(@Nullable final String valueFromSuggestionList) {
+ if (valueFromSuggestionList != null) {
+ final SuggestionInfo suggestionInfo = myStrategy.getSuggestionInfo();
+ if (suggestionInfo.getSuggestions().contains(valueFromSuggestionList)) {
+ final List<String> words = new ArrayList<String>(Arrays.asList(getTextAsParts()));
+ if (!words.isEmpty() && myLastSuggestionTiedToWord) {
+ words.remove(words.size() - 1);
+ }
+ words.add(valueFromSuggestionList);
+ myView.forceText(StringUtil.join(words, " "));
+ }
+ }
+ myView.removeSuggestions();
+ }
+
+ @Override
+ public void suggestionRequested() {
+ myLastSuggestionTiedToWord = false;
+
+ final SuggestionInfo suggestionInfo = myStrategy.getSuggestionInfo();
+ final List<String> suggestions = suggestionInfo.getSuggestions();
+ if (!suggestions.isEmpty()) {
+ myView.displaySuggestions(new SuggestionsBuilder(suggestions, true), suggestionInfo.myAbsolute, null);
+ }
+ }
+
+ @Override
+ public void executionRequested(@Nullable final String valueFromSuggestionList) {
+
+ }
+
+ /**
+ * @return [command_name => command] all available commands
+ */
+ @NotNull
+ Map<String, Command> getCommands() {
+ return Collections.unmodifiableMap(myCommands);
+ }
+
+ /**
+ * @return current text splitted into parts
+ */
+ @NotNull
+ String[] getTextAsParts() {
+ final String[] parts = EMPTY_SPACE.split(myView.getText());
+ return (((parts.length == 1) && parts[0].isEmpty()) ? ArrayUtil.EMPTY_STRING_ARRAY : parts);
+ }
+
+ /**
+ * @return last part of splitted text (if any). I.e. "foo bar spam" will return "spam"
+ */
+ @Nullable
+ String getLastPart() {
+ final String[] parts = getTextAsParts();
+ return ((parts.length > 0) ? parts[parts.length - 1] : null);
+ }
+
+ /**
+ * @return view
+ */
+ @NotNull
+ CommandInterfaceView getView() {
+ return myView;
+ }
+
+ /**
+ * @return maximum length command window may need
+ */
+ private int getMaximumCommandWithArgsLength() {
+ int maxLength = 0;
+ for (final Command command : myCommands.values()) {
+ int commandLength = command.getName().length() + 1;
+ for (final Argument argument : command.getArguments()) {
+ commandLength += argument.getName().length() + 1;
+ }
+ if (commandLength > maxLength) {
+ maxLength = commandLength;
+ }
+ }
+ return maxLength;
+ }
+}
--- /dev/null
+/*
+ * 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 org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Strategy implementation for case when user entered command
+ *
+ * @author Ilya.Kazakevich
+ */
+class InCommandStrategy extends Strategy {
+ @NotNull
+ private final List<String> myArguments = new ArrayList<String>();
+ @NotNull
+ private final Command myCommand;
+
+ /**
+ * @param command command enrtered by user
+ * @param presenter presenter
+ */
+ InCommandStrategy(@NotNull final Command command, @NotNull final CommandInterfacePresenterCommandBased presenter) {
+ super(presenter);
+ final List<String> parts = Arrays.asList(presenter.getTextAsParts());
+ assert !parts.isEmpty() && parts.get(0).equals(command.getName()) : "At least first argument should be command";
+ myArguments.addAll(parts.subList(1, parts.size()));
+ myCommand = command;
+ }
+
+ @NotNull
+ @Override
+ public String getSubText() {
+ return "D";
+ }
+
+ @NotNull
+ @Override
+ SuggestionInfo getSuggestionInfo() {
+ final List<String> strings = new ArrayList<String>();
+ for (final Argument argument : myCommand.getArguments()) {
+ if (!myArguments.contains(argument.getName())) {
+ strings.add(argument.getName());
+ }
+ }
+ return new SuggestionInfo(false, false, strings);
+ }
+
+ @Override
+ boolean isUnknownTextExists() {
+ if (myPresenter.getTextAsParts().length == 1) {
+ return false; // Command only
+ }
+ final String lastPart = myPresenter.getLastPart();
+ return ((lastPart != null) && !getSuggestionInfo().getSuggestions().contains(lastPart));
+ }
+
+ @NotNull
+ @Override
+ ErrorInfo getShowErrorInfo() {
+ final boolean noArgsLeft = getSuggestionInfo().getSuggestions().isEmpty();
+ return (noArgsLeft ? ErrorInfo.NO : ErrorInfo.RELATIVE);
+ }
+}
--- /dev/null
+/*
+ * 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 org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+
+/**
+ * Strategy implementation for case when no command parsed
+ *
+ * @author Ilya.Kazakevich
+ */
+class NoCommandStrategy extends Strategy {
+ NoCommandStrategy(@NotNull final CommandInterfacePresenterCommandBased presenter) {
+ super(presenter);
+ }
+
+ @NotNull
+ @Override
+ String getSubText() {
+ return "vse ploho";
+ }
+
+ @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
+ ErrorInfo getShowErrorInfo() {
+ return (isTextBoxEmpty() ? ErrorInfo.NO : ErrorInfo.FULL);
+ }
+
+
+ private boolean isTextBoxEmpty() {
+ return myPresenter.getView().getText().isEmpty();
+ }
+}
--- /dev/null
+/*
+ * 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 org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+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();
+
+
+ /**
+ * @return errors
+ */
+ @NotNull
+ abstract ErrorInfo getShowErrorInfo();
+
+ /**
+ * @return if text entered by user contains some unknown commands
+ */
+ abstract boolean isUnknownTextExists();
+
+ /**
+ * Display error or not
+ */
+ enum ErrorInfo {
+ /**
+ * Yes, mark whole text as error
+ */
+ FULL,
+ /**
+ * Yes, mark last part as error
+ */
+ RELATIVE,
+ /**
+ * No, do not mark anything like error
+ */
+ NO
+ }
+
+ @SuppressWarnings("PackageVisibleField") // No need to hide field: everything is internal API in package, anyway
+ static class SuggestionInfo {
+ /**
+ * Suggestions
+ */
+ private final List<String> mySuggestions = new ArrayList<String>();
+ /**
+ * Display them at absolute location or relative to last letter
+ */
+ final boolean myAbsolute;
+ /**
+ * Show then any time, or only when user requests them
+ */
+ final boolean myShowOnlyWhenRequested;
+
+ /**
+ * @param absolute Display them at absolute location or relative to last letter
+ * @param showOnlyWhenRequested Show then any time, or only when user requests them
+ * @param suggestions Suggestions
+ */
+ SuggestionInfo(final boolean absolute,
+ final boolean showOnlyWhenRequested,
+ @NotNull final List<String> suggestions) {
+ myAbsolute = absolute;
+ myShowOnlyWhenRequested = showOnlyWhenRequested;
+ mySuggestions.addAll(suggestions);
+ }
+
+ /**
+ * @return suggestions
+ */
+ @NotNull
+ List<String> getSuggestions() {
+ return Collections.unmodifiableList(mySuggestions);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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.
+ */
+
+/**
+ * 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}
+ * @author Ilya.Kazakevich
+ */
+package com.jetbrains.python.commandInterface.commandsWithArgs;
\ No newline at end of file
--- /dev/null
+/*
+ * 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.
+ */
+
+/**
+ * View/Presenter pair that implements so-called "command line interface".
+ * It has several abilities, including (but not limited):
+ * <ol>
+ * <li>Suggestion box</li>
+ * <li>Error marking</li>
+ * <li>Popups</li>
+ * <li>AutoCompletion</li>
+ * </ol>
+ * <p>
+ * System consists of {@link com.jetbrains.python.commandInterface.CommandInterfacePresenter}
+ * and appropriate {@link com.jetbrains.python.commandInterface.CommandInterfaceView}.
+ * See {@link com.jetbrains.python.vp} for more info.
+ * </p>
+ *
+ * <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}
+ * </p>
+ *
+ *
+ * @author Ilya.Kazakevich
+ */
+package com.jetbrains.python.commandInterface;
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.jetbrains.python.commandInterface.swingView.CommandInterfaceViewSwingImpl">
+ <grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="6" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <margin top="4" left="4" bottom="4" right="2"/>
+ <constraints>
+ <xy x="1" y="67" width="404" height="245"/>
+ </constraints>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <hspacer id="91552">
+ <constraints>
+ <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false">
+ <minimum-size width="-1" height="5"/>
+ </grid>
+ </constraints>
+ </hspacer>
+ <component id="4067e" class="javax.swing.JLabel" binding="myLabel">
+ <constraints>
+ <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="Label"/>
+ </properties>
+ </component>
+ <grid id="30372" class="com.jetbrains.python.commandInterface.swingView.SmartTextField" binding="myMainTextField" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <margin top="0" left="0" bottom="0" right="0"/>
+ <constraints>
+ <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <enabled value="true"/>
+ <font/>
+ </properties>
+ <border type="none"/>
+ <children/>
+ </grid>
+ <hspacer id="4b381">
+ <constraints>
+ <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ </hspacer>
+ <component id="7a794" class="javax.swing.JLabel" binding="mySubLabel">
+ <constraints>
+ <grid row="4" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="Label"/>
+ </properties>
+ </component>
+ <hspacer id="c568">
+ <constraints>
+ <grid row="5" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ </hspacer>
+ </children>
+ </grid>
+</form>
--- /dev/null
+/*
+ * 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.swingView;
+
+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.popup.Balloon.Position;
+import com.intellij.openapi.ui.popup.JBPopup;
+import com.intellij.openapi.ui.popup.JBPopupAdapter;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.ui.popup.LightweightWindowEvent;
+import com.intellij.ui.awt.RelativePoint;
+import com.jetbrains.python.commandInterface.CommandInterfacePresenter;
+import com.jetbrains.python.commandInterface.CommandInterfaceView;
+import com.jetbrains.python.suggestionList.SuggestionList;
+import com.jetbrains.python.suggestionList.SuggestionsBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import java.awt.*;
+import java.awt.event.*;
+
+/**
+ * Command-interface view implementation based on Swing
+ *
+ * @author Ilya.Kazakevich
+ */
+public class CommandInterfaceViewSwingImpl extends JBPopupAdapter implements CommandInterfaceView, DocumentListener {
+ /**
+ * Pop-up we displayed in
+ */
+ @NotNull
+ private final JBPopup myMainPopUp;
+ private JPanel myPanel;
+ /**
+ * Upper label
+ */
+ private JLabel myLabel;
+ /**
+ * Text field
+ */
+ private SmartTextField myMainTextField;
+ /**
+ * Lower (sub) label
+ */
+ private JLabel mySubLabel;
+ @NotNull
+ private final CommandInterfacePresenter myPresenter;
+ /**
+ * List to display suggestions
+ */
+ @NotNull
+ private final SuggestionList myList;
+ /**
+ * 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
+ */
+ private boolean myInForcedTextMode;
+
+ /**
+ * @param presenter our presenter
+ * @param title view title to display
+ * @param project project
+ * @param placeholderText text for placeholder (to be displayed when there is not text)
+ */
+ public CommandInterfaceViewSwingImpl(@NotNull final CommandInterfacePresenter presenter,
+ @NotNull final String title,
+ @NotNull final Project project,
+ @Nullable final String placeholderText) {
+ myPresenter = presenter;
+ myLabel.setText(title);
+ myPlaceHolderText = placeholderText;
+
+ myMainPopUp = JBPopupFactory.getInstance().createComponentPopupBuilder(myPanel, myMainTextField)
+ .setFocusable(true)
+ .setRequestFocus(true)
+ .createPopup();
+ myMainTextField.setRequestFocusEnabled(true);
+ 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
+ }
+
+ myMainTextField
+ .setPreferredWidthInPx(windowSize);
+ myList = new SuggestionList(new MySuggestionListListener());
+ }
+
+
+ @Override
+ public void show() {
+ myMainTextField.getDocument().addDocumentListener(this);
+
+ myMainPopUp.addListener(this);
+ if (myPlaceHolderText != null) {
+ myMainTextField.setWaterMarkPlaceHolderText(myPlaceHolderText);
+ }
+
+
+ myMainTextField.addFocusListener(new FocusAdapter() {
+ @Override
+ public void focusLost(final FocusEvent e) {
+ super.focusLost(e);
+ myMainPopUp.cancel();
+ }
+ });
+ myMainTextField.setFocusTraversalKeysEnabled(false);
+ myMainTextField.addKeyListener(new MyKeyListener());
+ myMainPopUp.showInFocusCenter();
+ }
+
+
+ @Override
+ 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
+ if (!absolute) {
+ left = myMainTextField.getTextEndPosition();
+ }
+ myList.showSuggestions(suggestions, new RelativePoint(myPanel, new Point(left, myPanel.getHeight())), toSelect);
+ }
+
+
+ @Override
+ public void onClosed(final LightweightWindowEvent event) {
+ super.onClosed(event);
+ myList.close();
+ }
+
+ @Override
+ public void showError(final boolean lastOnly) {
+ myMainTextField.underlineText(Color.RED, lastOnly);
+ }
+
+
+ @Override
+ public void insertUpdate(final DocumentEvent e) {
+ processDocumentChange();
+ }
+
+ @Override
+ public void removeUpdate(final DocumentEvent e) {
+ processDocumentChange();
+ }
+
+ private void processDocumentChange() {
+ myMainTextField.hideUnderline();
+ myPresenter.textChanged(myInForcedTextMode);
+ }
+
+ @Override
+ public void changedUpdate(final DocumentEvent e) {
+
+ }
+
+ @Override
+ public void forceText(@NotNull final String newText) {
+ myInForcedTextMode = true;
+ myMainTextField.setText(newText);
+ myInForcedTextMode = false;
+ }
+
+ @Override
+ public void removeSuggestions() {
+ myList.close();
+ }
+
+ @Override
+ public void displayInfoBaloon(@NotNull final String message) {
+ final RelativePoint point = new RelativePoint(myMainTextField, new Point(myMainTextField.getTextEndPosition(), 0));
+ JBPopupFactory.getInstance().createBalloonBuilder(new JLabel(message)).createBalloon().show(point, Position.above);
+ }
+
+ @Override
+ public void setSubText(@NotNull final String subText) {
+ mySubLabel.setText(subText);
+ }
+
+
+ /**
+ * Reacts on keys, pressed by user
+ */
+ private class MyKeyListener extends KeyAdapter {
+ @Override
+ public void keyPressed(final KeyEvent e) {
+ super.keyPressed(e);
+ final int keyCode = e.getKeyCode();
+ if (keyCode == KeyEvent.VK_UP) {
+ myList.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();
+ }
+ }
+ }
+
+ @NotNull
+ @Override
+ public String getText() {
+ return myMainTextField.getText();
+ }
+
+ @Override
+ public void setPreferredWidthInChars(final int widthInChars) {
+ myMainTextField.setPreferredWidthInChars(widthInChars);
+ }
+
+ /**
+ * Listener for suggestion list
+ */
+ private class MySuggestionListListener extends JBPopupAdapter {
+
+ @Override
+ public void onClosed(final LightweightWindowEvent event) {
+ super.onClosed(event);
+ removeSuggestions();
+ }
+ }
+}
--- /dev/null
+/*
+ * 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.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.ui.StatusText;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * Text field that has width to be changed and accepts error underline
+ *
+ * @author Ilya.Kazakevich
+ */
+@SuppressWarnings({"NonSerializableFieldInSerializableClass", "SerializableHasSerializationMethods"}) // Will never serialize it
+public class SmartTextField extends JTextField {
+ /**
+ * Placeholder for this textbox
+ */
+ private StatusText myPlaceHolder;
+ /**
+ * error (underline) info in format "lastOnly => Color" where "lastOnly" is to underline last letter only (not the whole line)
+ * Null if nothing should be displayed.
+ */
+ @Nullable
+ private Pair<Boolean, Color> myUnderlineInfo;
+ private int myPreferredWidth;
+
+ public SmartTextField() {
+ setFont(EditorColorsManager.getInstance().getGlobalScheme().getFont(EditorFontType.CONSOLE_PLAIN));
+ }
+
+ @Override
+ protected void paintComponent(final Graphics g) {
+ super.paintComponent(g);
+ if (myPlaceHolder != null) {
+ myPlaceHolder.paint(this, g);
+ }
+ if (myUnderlineInfo != null) {
+ final int lineStart = (myUnderlineInfo.first ? getTextEndPosition() : getColumnWidth());
+ final int lineEnd = getTextEndPosition() + (myUnderlineInfo.first ? getColumnWidth() : 0);
+ g.setColor(myUnderlineInfo.second);
+ final int verticalPosition = getHeight() - 5;
+ g.drawLine(lineStart, verticalPosition, lineEnd, verticalPosition);
+ }
+ }
+
+ /**
+ * @return place (in px) where entered text ends.
+ */
+ int getTextEndPosition() {
+ return (getText().length() + 1) * getColumnWidth();
+ }
+
+ void setWaterMarkPlaceHolderText(@NotNull final String watermark) {
+ myPlaceHolder = new MyStatusText(this);
+ myPlaceHolder.setText(watermark);
+ }
+
+
+ @Override
+ public Dimension getPreferredSize() {
+ final Dimension dimension = super.getPreferredSize();
+ final int placeHolderTextLength = ((myPlaceHolder != null) ? myPlaceHolder.getText().length() : 0);
+ final int columns = Math.max(Math.max(getText().length(), placeHolderTextLength), getColumns());
+ final int desiredSize = columns * getColumnWidth();
+ return new Dimension(Math.max(Math.max(myPreferredWidth, dimension.width), desiredSize), dimension.height);
+ }
+
+ /**
+ * Display underline
+ *
+ * @param color color to underline
+ * @param lastOnly last letter only (whole line otherwise)
+ */
+ void underlineText(@NotNull final Color color, final boolean lastOnly) {
+ myUnderlineInfo = new Pair<Boolean, Color>(lastOnly, color);
+ }
+
+ /**
+ * Removes underline
+ */
+ void hideUnderline() {
+ myUnderlineInfo = null;
+ }
+
+ /**
+ * Sets appropriate width in chars
+ * @param widthInChars num of chars
+ */
+ void setPreferredWidthInChars(final int widthInChars) {
+ setColumns(widthInChars);
+ }
+
+ /**
+ * Sets appropriate width in pixels
+ * @param width width in px
+ */
+ void setPreferredWidthInPx(final int width) {
+ myPreferredWidth = width;
+ }
+
+ /**
+ * Wrapper to display placeholder
+ */
+ private class MyStatusText extends StatusText {
+ MyStatusText(final JComponent owner) {
+ super(owner);
+ }
+
+ @Override
+ protected boolean isStatusVisible() {
+ return SmartTextField.this.getText().isEmpty();
+ }
+ }
+}
--- /dev/null
+/*
+ * 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.
+ */
+
+/**
+ * Command-line like interface view GUI. See {@link com.jetbrains.python.commandInterface.swingView.CommandInterfaceViewSwingImpl}
+ * @author Ilya.Kazakevich
+ */
+package com.jetbrains.python.commandInterface.swingView;
\ No newline at end of file
private static final String EXEC_LINE = "execLine";
private static final String EXEC_MULTILINE = "execMultipleLines";
- private static final String GET_COMPLETIONS = "getCompletions";
+ private static final String GET_COMPLETIONS = "getSuggestions";
private static final String GET_DESCRIPTION = "getDescription";
private static final String GET_FRAME = "getFrame";
private static final String GET_VARIABLE = "getVariable";
--- /dev/null
+/*
+ * 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.suggestionList;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Suggestion to display to user
+ *
+ * @author Ilya.Kazakevich
+ */
+public class Suggestion {
+ @NotNull
+ private final String myText;
+ private final boolean myStrong;
+
+ /**
+ * @param text text to display
+ * @param strong is strong or not. Strong element will be marked somehow when displayed.
+ */
+ public Suggestion(@NotNull final String text, final boolean strong) {
+ myText = text;
+ myStrong = strong;
+ }
+
+ @NotNull
+ public String getText() {
+ return myText;
+ }
+
+ public boolean isStrong() {
+ return myStrong;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Suggestion)) return false;
+
+ Suggestion that = (Suggestion)o;
+
+ if (myStrong != that.myStrong) return false;
+ if (!myText.equals(that.myText)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = myText.hashCode();
+ result = 31 * result + (myStrong ? 1 : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Suggestion{" +
+ "myText='" + myText + '\'' +
+ ", myStrong=" + myStrong +
+ '}';
+ }
+}
--- /dev/null
+/*
+ * 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.suggestionList;
+
+import com.intellij.openapi.editor.colors.EditorColors;
+import com.intellij.openapi.editor.colors.EditorColorsManager;
+import com.intellij.openapi.ui.popup.JBPopup;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.ui.popup.JBPopupListener;
+import com.intellij.ui.awt.RelativePoint;
+import com.intellij.util.PlatformIcons;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.List;
+
+/**
+ * List of suggestions to be displayed.
+ * Suggestions may be separated into groups: Group1[suggestion1, suggestion2, suggestion3].
+ * Each suggestion may also be marked as strong (see {@link com.jetbrains.python.suggestionList.Suggestion}
+ *
+ * @author Ilya.Kazakevich
+ */
+public class SuggestionList {
+ /**
+ * Listens popup close event
+ */
+ @Nullable
+ private final JBPopupListener myListener;
+ /**
+ * Popup itself
+ */
+ @Nullable
+ private JBPopup myListPopUp;
+ /**
+ * Model of suggestion list
+ */
+ private final DefaultListModel myListModel = new DefaultListModel();
+ /**
+ * Suggestion list
+ */
+ private final JList myList = new JList(myListModel);
+
+ public SuggestionList() {
+ myListener = null;
+ }
+
+ /**
+ * @param listener popup listener (will be called back when popup closed)
+ */
+ public SuggestionList(@SuppressWarnings("NullableProblems") @NotNull final JBPopupListener listener) {
+ myListener = listener;
+ }
+
+ /**
+ * @param values suggestions wrapped into suggestion builder.
+ * Prefered usage pattern.
+ * @param displayPoint point on screen to display suggestions
+ * @param elementToSelect element in list to be selected (if any)
+ * @see com.jetbrains.python.suggestionList.SuggestionsBuilder
+ */
+ public void showSuggestions(@NotNull final SuggestionsBuilder values,
+ @NotNull final RelativePoint displayPoint,
+ @Nullable final String elementToSelect) {
+ showSuggestions(values.getList(), displayPoint, elementToSelect);
+ }
+
+ /**
+ * @param values suggestions in format [group1[suggestions]]. See class doc for info about groups.
+ * We recommend you <strong>not to use</strong> this method.
+ * Use {@link #showSuggestions(SuggestionsBuilder, com.intellij.ui.awt.RelativePoint, String)} instead.
+ * {@link com.jetbrains.python.suggestionList.SuggestionsBuilder} is easier to use
+ * @param displayPoint point on screen to display suggestions
+ * @param elementToSelect element in list to be selected (if any)
+ */
+ public void showSuggestions(@NotNull final List<List<Suggestion>> values,
+ @NotNull final RelativePoint displayPoint,
+ @Nullable final String elementToSelect) {
+ close();
+ myList.setCellRenderer(new MyCellRenderer());
+ myListModel.clear();
+ if (values.isEmpty()) {
+ return;
+ }
+ // Fill and select
+
+ // Iterate through groups adding suggestions. Odd groups should be marked differently.
+ for (int groupId = 0; groupId < values.size(); groupId++) {
+ final List<Suggestion> suggestions = values.get(groupId);
+ for (int suggestionId = 0; suggestionId < suggestions.size(); suggestionId++) {
+ final Suggestion suggestion = suggestions.get(suggestionId);
+ myListModel.addElement(new SuggestionListElement((groupId % 2) == 0, suggestion));
+ if (suggestion.getText().equals(elementToSelect)) {
+ myList.setSelectedIndex(suggestionId + groupId);
+ }
+ }
+ }
+ if ((elementToSelect == null) && (!myListModel.isEmpty())) {
+ myList.setSelectedIndex(0); // Select first element
+ }
+
+ // Use scroll bars
+ final JScrollPane content = new JScrollPane(myList, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
+ ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
+ myListPopUp =
+ JBPopupFactory.getInstance().createComponentPopupBuilder(content, null)
+ .createPopup();
+ if (myListener != null) {
+ myListPopUp.addListener(myListener);
+ }
+ myListPopUp.show(displayPoint);
+ }
+
+
+ /**
+ * Close suggestion list
+ */
+ public void close() {
+ if (myListPopUp != null) {
+ myListPopUp.cancel();
+ }
+ }
+
+ /**
+ * Move selection
+ *
+ * @param directionUp up if true. Down otherwise
+ */
+ public void moveSelection(final boolean directionUp) {
+ if (myListModel.isEmpty()) {
+ return;
+ }
+ // Handle separation
+ int newIndex = myList.getSelectedIndex() + (directionUp ? -1 : 1);
+ if (newIndex < 0) {
+ newIndex = 0;
+ }
+ if (newIndex >= myListModel.size()) {
+ newIndex = myListModel.size() - 1;
+ }
+ myList.setSelectedIndex(newIndex);
+ myList.scrollRectToVisible(myList.getCellBounds(newIndex, newIndex));
+ }
+
+ /**
+ * @return currently selected value (null if nothing is selected)
+ */
+ @Nullable
+ public String getValue() {
+ final Object value = myList.getSelectedValue();
+ return ((value == null) ? "" : getElement(value).mySuggestion.getText());
+ }
+
+ /**
+ * Element that represents suggestion
+ */
+ private static class SuggestionListElement {
+ /**
+ * is part of odd group
+ */
+ private final boolean myOddGroup;
+ /**
+ * suggestion itself
+ */
+ @NotNull
+ private final Suggestion mySuggestion;
+
+ private SuggestionListElement(final boolean oddGroup,
+ @NotNull final Suggestion suggestion) {
+ myOddGroup = oddGroup;
+ mySuggestion = suggestion;
+ }
+ }
+
+ /**
+ * Cell renderer for suggestion
+ */
+ private static class MyCellRenderer extends DefaultListCellRenderer {
+ @NotNull
+ private static final Color ODD_GROUP_SELECTED_BACKGROUND_COLOR =
+ EditorColorsManager.getInstance().getGlobalScheme().getColor(EditorColors.SELECTION_BACKGROUND_COLOR);
+ @NotNull
+ private static final Color ODD_GROUP_BACKGROUND_COLOR =
+ EditorColorsManager.getInstance().getGlobalScheme().getColor(EditorColors.GUTTER_BACKGROUND);
+
+ @Override
+ public Component getListCellRendererComponent(final JList list,
+ final Object value,
+ final int index,
+ final boolean isSelected,
+ final boolean cellHasFocus) {
+ final SuggestionListElement element = getElement(value);
+ // Out parent always uses label
+ final Component component = super.getListCellRendererComponent(list, element.mySuggestion.getText(), index, isSelected,
+ cellHasFocus);
+
+
+ if (element.myOddGroup) {
+ component.setBackground(isSelected ? ODD_GROUP_SELECTED_BACKGROUND_COLOR : ODD_GROUP_BACKGROUND_COLOR);
+ }
+
+ if (!(component instanceof JLabel)) {
+ return component; // We can't change any property here
+ }
+
+ final JLabel label = (JLabel)component;
+ if (element.mySuggestion.isStrong()) {
+ // We use icons to display "strong" element
+ label.setIcon(PlatformIcons.FOLDER_ICON);
+ }
+
+
+ return component;
+ }
+ }
+
+ /**
+ * Converts argument to {@link SuggestionListElement} which always should be.
+ *
+ * @param value argument to convert
+ * @return converted argument
+ */
+ @NotNull
+ private static SuggestionListElement getElement(final Object value) {
+ assert value instanceof SuggestionListElement : "Value is not valid element: " + value;
+ return (SuggestionListElement)value;
+ }
+}
--- /dev/null
+/*
+ * 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.suggestionList;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+
+/**
+ * Builds list of suggestions.
+ * You may create suggestions from words, you may add new groups and do other useful things.
+ * It works like chain pattern, so it returns itself.
+ *
+ * @author Ilya.Kazakevich
+ */
+public class SuggestionsBuilder {
+ @NotNull
+ private final List<List<Suggestion>> myList = new ArrayList<List<Suggestion>>();
+
+ public SuggestionsBuilder() {
+ myList.add(new ArrayList<Suggestion>());
+ }
+
+ /**
+ * @param words list of words to add (to the first group)
+ * @param sort sort passed words
+ */
+ public SuggestionsBuilder(@NotNull List<String> words, final boolean sort) {
+ this();
+ if (sort) {
+ // No guarantee passed argument is mutable
+ //noinspection AssignmentToMethodParameter
+ words = new ArrayList<String>(words);
+ Collections.sort(words);
+ }
+ add(words);
+ }
+
+ /**
+ * Creates next group and sets it as default
+ */
+ public SuggestionsBuilder nextGroup() {
+ if (!getCurrentGroup().isEmpty()) {
+ myList.add(new ArrayList<Suggestion>());
+ }
+ return this;
+ }
+
+ /**
+ * @param words words to add to current group
+ */
+ public final SuggestionsBuilder add(@NotNull final String... words) {
+ return add(Arrays.asList(words));
+ }
+
+ /**
+ * @param words words to add to current group
+ */
+ public final SuggestionsBuilder add(@NotNull final Collection<String> words) {
+ for (final String word : words) {
+ add(word, false);
+ }
+ return this;
+ }
+
+ /**
+ * @param text text to add to current group
+ * @param strong strong or not
+ */
+ public SuggestionsBuilder add(@NotNull final String text, final boolean strong) {
+ getCurrentGroup().add(new Suggestion(text, strong));
+ return this;
+ }
+
+ /**
+ * @return elements from current group
+ */
+ @NotNull
+ private List<Suggestion> getCurrentGroup() {
+ return myList.get(myList.size() - 1);
+ }
+
+ /**
+ * @return all suggestions in format [group1[sugg1, sugg2]]
+ */
+ @NotNull
+ List<List<Suggestion>> getList() {
+ return Collections.unmodifiableList(myList);
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SuggestionsBuilder)) return false;
+
+ SuggestionsBuilder builder = (SuggestionsBuilder)o;
+
+ if (!myList.equals(builder.myList)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return myList.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "SuggestionsBuilder{" +
+ "myList=" + myList +
+ '}';
+ }
+}
--- /dev/null
+/*
+ * 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.
+ */
+
+/**
+ * List of suggestions to be displayed to user
+ * Start from {@link com.jetbrains.python.suggestionList.SuggestionList}
+ * @author Ilya.Kazakevich
+ */
+package com.jetbrains.python.suggestionList;
\ No newline at end of file