├── code-completion.iml ├── resources └── META-INF │ └── plugin.xml ├── src └── dev │ └── ja │ └── samples │ └── completion │ ├── Dictionary.java │ ├── DictionaryCompletionProvider.java │ ├── DictionaryLoader.java │ ├── StringLiteralDictionaryContributor.java │ └── StringLiteralPattern.java └── test └── dev └── ja └── samples └── completion ├── StringLiteralDictionaryAutoPopupContributorTest.java └── StringLiteralDictionaryContributorTest.java /code-completion.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | dev.j-a.sample.code-completion 3 | Code Completion 4 | 0.1.0 5 | Joachim Ansorg 6 | 7 | 11 | 12 | 13 | 14 | com.intellij.modules.lang 15 | 16 | 17 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/dev/ja/samples/completion/Dictionary.java: -------------------------------------------------------------------------------- 1 | package dev.ja.samples.completion; 2 | 3 | import com.intellij.spellchecker.BundledDictionaryProvider; 4 | import com.intellij.spellchecker.SpellCheckerManager; 5 | import com.intellij.spellchecker.StreamLoader; 6 | import com.intellij.spellchecker.compress.CompressedDictionary; 7 | import com.intellij.spellchecker.engine.Transformation; 8 | import com.intellij.util.containers.ContainerUtil; 9 | 10 | import java.io.InputStream; 11 | import java.util.List; 12 | 13 | /** 14 | * @author jansorg 15 | */ 16 | class Dictionary { 17 | private static final List bundledDictionaries = ContainerUtil.createLockFreeCopyOnWriteList(); 18 | 19 | // load() loads all build-in dictionaries in a thread-safe manner 20 | static void load() { 21 | for (BundledDictionaryProvider provider : BundledDictionaryProvider.EP_NAME.getExtensions()) { 22 | for (String name : provider.getBundledDictionaries()) { 23 | InputStream stream = SpellCheckerManager.class.getResourceAsStream(name); 24 | if (stream != null) { 25 | StreamLoader loader = new StreamLoader(stream, name); 26 | CompressedDictionary dict = CompressedDictionary.create(loader, new Transformation()); 27 | bundledDictionaries.add(dict); 28 | } 29 | } 30 | } 31 | } 32 | 33 | static List get() { 34 | return bundledDictionaries; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/dev/ja/samples/completion/DictionaryCompletionProvider.java: -------------------------------------------------------------------------------- 1 | package dev.ja.samples.completion; 2 | 3 | import com.intellij.codeInsight.completion.CompletionParameters; 4 | import com.intellij.codeInsight.completion.CompletionProvider; 5 | import com.intellij.codeInsight.completion.CompletionResultSet; 6 | import com.intellij.codeInsight.lookup.LookupElementBuilder; 7 | import com.intellij.openapi.progress.ProgressManager; 8 | import com.intellij.spellchecker.compress.CompressedDictionary; 9 | import com.intellij.util.ProcessingContext; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * @author jansorg 16 | */ 17 | class DictionaryCompletionProvider extends CompletionProvider { 18 | private final boolean onlyManual; 19 | 20 | /** 21 | * @param onlyManual if true, then completions are only returned when the user manually requested it 22 | */ 23 | DictionaryCompletionProvider(boolean onlyManual) { 24 | this.onlyManual = onlyManual; 25 | } 26 | 27 | @Override 28 | protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet result) { 29 | if (parameters.isAutoPopup() && onlyManual) { 30 | return; 31 | } 32 | 33 | String prefix = result.getPrefixMatcher().getPrefix(); 34 | if (prefix.isEmpty()) { 35 | return; 36 | } 37 | 38 | // make sure that our prefix is the last word 39 | // for plain text file, all the content up to the caret is the prefix 40 | // we don't want that, because we're only completing a single word 41 | CompletionResultSet dictResult; 42 | int lastSpace = prefix.lastIndexOf(' '); 43 | if (lastSpace >= 0 && lastSpace < prefix.length() - 1) { 44 | prefix = prefix.substring(lastSpace + 1); 45 | dictResult = result.withPrefixMatcher(prefix); 46 | } else { 47 | dictResult = result; 48 | } 49 | 50 | int length = prefix.length(); 51 | char firstChar = prefix.charAt(0); 52 | boolean isUppercase = Character.isUpperCase(firstChar); 53 | 54 | List dicts = Dictionary.get(); 55 | if (dicts == null || dicts.isEmpty()) { 56 | return; 57 | } 58 | 59 | for (CompressedDictionary dict : dicts) { 60 | dict.getWords(Character.toLowerCase(firstChar), length, length + 20, word -> { 61 | ProgressManager.checkCanceled(); 62 | 63 | LookupElementBuilder element; 64 | if (isUppercase) { 65 | element = LookupElementBuilder.create(word.substring(0, 1).toUpperCase() + word.substring(1)); 66 | } else { 67 | element = LookupElementBuilder.create(word); 68 | } 69 | dictResult.addElement(element); 70 | }); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/dev/ja/samples/completion/DictionaryLoader.java: -------------------------------------------------------------------------------- 1 | package dev.ja.samples.completion; 2 | 3 | import com.intellij.openapi.application.PreloadingActivity; 4 | import com.intellij.openapi.progress.ProgressIndicator; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | 8 | /** 9 | * Preloading activity to load the dictionary at startup. 10 | * 11 | * @author jansorg 12 | */ 13 | public class DictionaryLoader extends PreloadingActivity { 14 | @Override 15 | public void preload(@NotNull ProgressIndicator indicator) { 16 | Dictionary.load(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/dev/ja/samples/completion/StringLiteralDictionaryContributor.java: -------------------------------------------------------------------------------- 1 | package dev.ja.samples.completion; 2 | 3 | import com.intellij.codeInsight.completion.CompletionContributor; 4 | import com.intellij.codeInsight.completion.CompletionType; 5 | import com.intellij.patterns.PlatformPatterns; 6 | import com.intellij.psi.PlainTextTokenTypes; 7 | 8 | /** 9 | * @author jansorg 10 | */ 11 | public class StringLiteralDictionaryContributor extends CompletionContributor { 12 | public StringLiteralDictionaryContributor() { 13 | // completions for plain text files 14 | extend(CompletionType.BASIC, 15 | PlatformPatterns.psiElement(PlainTextTokenTypes.PLAIN_TEXT), 16 | new DictionaryCompletionProvider(false)); 17 | 18 | // completions for content of string literals 19 | extend(CompletionType.BASIC, 20 | PlatformPatterns.psiElement().with(new StringLiteralPattern()), 21 | new DictionaryCompletionProvider(false)); 22 | 23 | // always suggest when invoked manually 24 | extend(CompletionType.BASIC, 25 | PlatformPatterns.not(PlatformPatterns.alwaysFalse()), 26 | new DictionaryCompletionProvider(true)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/dev/ja/samples/completion/StringLiteralPattern.java: -------------------------------------------------------------------------------- 1 | package dev.ja.samples.completion; 2 | 3 | import com.intellij.lang.ASTNode; 4 | import com.intellij.lang.Language; 5 | import com.intellij.lang.LanguageParserDefinitions; 6 | import com.intellij.lang.ParserDefinition; 7 | import com.intellij.patterns.PatternCondition; 8 | import com.intellij.psi.PsiElement; 9 | import com.intellij.psi.tree.TokenSet; 10 | import com.intellij.psi.util.PsiUtilCore; 11 | import com.intellij.util.ProcessingContext; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | /** 15 | * Pattern which only accepts PsiElements, which either are a string literal or a comment of the current language. 16 | * It also accepts if the element's parent matches these conditions. 17 | * 18 | * @author jansorg 19 | */ 20 | class StringLiteralPattern extends PatternCondition { 21 | StringLiteralPattern() { 22 | super("stringLiteralPattern()"); 23 | } 24 | 25 | @Override 26 | public boolean accepts(@NotNull PsiElement psi, ProcessingContext context) { 27 | Language language = PsiUtilCore.findLanguageFromElement(psi); 28 | ParserDefinition definition = LanguageParserDefinitions.INSTANCE.forLanguage(language); 29 | if (definition == null) { 30 | return false; 31 | } 32 | 33 | // suggest completions in string and comment literals 34 | TokenSet tokens = TokenSet.orSet( 35 | definition.getStringLiteralElements(), 36 | definition.getCommentTokens()); 37 | 38 | ASTNode node = psi.getNode(); 39 | if (node == null) { 40 | return false; 41 | } 42 | 43 | if (tokens.contains(node.getElementType())) { 44 | return true; 45 | } 46 | 47 | return false; 48 | // node = node.getTreeParent(); 49 | // return node != null && tokens.contains(node.getElementType()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/dev/ja/samples/completion/StringLiteralDictionaryAutoPopupContributorTest.java: -------------------------------------------------------------------------------- 1 | package dev.ja.samples.completion; 2 | 3 | import com.intellij.testFramework.fixtures.BasePlatformTestCase; 4 | import com.intellij.testFramework.fixtures.CompletionAutoPopupTester; 5 | import org.junit.Assert; 6 | 7 | /** 8 | * We're using the Junit 3 base class, because we the Junit4 variant doesn't support runInDispatchThread() properly. 9 | * 10 | * @author jansorg 11 | */ 12 | public class StringLiteralDictionaryAutoPopupContributorTest extends BasePlatformTestCase { 13 | @Override 14 | protected void setUp() throws Exception { 15 | super.setUp(); 16 | // preloadingActivity don't seem to be run in tests 17 | Dictionary.load(); 18 | } 19 | 20 | @Override 21 | protected boolean runInDispatchThread() { 22 | return false; 23 | } 24 | 25 | public void testAutoPopupCompletions() { 26 | CompletionAutoPopupTester tester = new CompletionAutoPopupTester(myFixture); 27 | tester.runWithAutoPopupEnabled(() -> { 28 | myFixture.configureByText("test.txt", ""); 29 | 30 | tester.typeWithPauses("ob"); 31 | 32 | tester.joinCompletion(); 33 | Assert.assertNotNull(tester.getLookup()); 34 | Assert.assertEquals(314, tester.getLookup().getItems().size()); 35 | 36 | // ob -> obfusc 37 | tester.typeWithPauses("fusc"); 38 | tester.joinCompletion(); 39 | Assert.assertNotNull(tester.getLookup()); 40 | Assert.assertEquals(8, tester.getLookup().getItems().size()); 41 | }); 42 | } 43 | } -------------------------------------------------------------------------------- /test/dev/ja/samples/completion/StringLiteralDictionaryContributorTest.java: -------------------------------------------------------------------------------- 1 | package dev.ja.samples.completion; 2 | 3 | import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixture4TestCase; 4 | import org.junit.Assert; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | /** 9 | * @author jansorg 10 | */ 11 | public class StringLiteralDictionaryContributorTest extends LightPlatformCodeInsightFixture4TestCase { 12 | @Before 13 | public void loadDicts() { 14 | // preloadingActivity don't seem to be run in tests 15 | Dictionary.load(); 16 | } 17 | 18 | @Test 19 | public void noEmptyPrefix() { 20 | myFixture.configureByText("test.txt", ""); 21 | Assert.assertEquals("expected no completions for an empty file", 0, myFixture.completeBasic().length); 22 | 23 | // whitespace suffix 24 | myFixture.configureByText("test.txt", "foo"); 25 | myFixture.type(" "); 26 | Assert.assertEquals("expected no completions after whitespace", 0, myFixture.completeBasic().length); 27 | } 28 | 29 | @Test 30 | public void completions() { 31 | myFixture.configureByText("test.txt", ""); 32 | 33 | myFixture.type("ob"); 34 | Assert.assertEquals(314, myFixture.completeBasic().length); 35 | 36 | // ob -> obfus 37 | myFixture.type("fus"); 38 | Assert.assertEquals(8, myFixture.completeBasic().length); 39 | } 40 | } --------------------------------------------------------------------------------